From ded52c127972ad5ab2dcbc06d64eebc0f5832d88 Mon Sep 17 00:00:00 2001 From: Kirill Ilin Date: Fri, 30 Jan 2026 13:54:01 +0500 Subject: [PATCH] [dashboard] Add external ips count to Tenant details page Signed-off-by: Kirill Ilin --- internal/controller/dashboard/factory.go | 4 ++ pkg/apis/apps/v1alpha1/types.go | 3 ++ pkg/registry/apps/application/rest.go | 50 ++++++++++++++++++++---- 3 files changed, 49 insertions(+), 8 deletions(-) diff --git a/internal/controller/dashboard/factory.go b/internal/controller/dashboard/factory.go index 63bb7a82..1016b30a 100644 --- a/internal/controller/dashboard/factory.go +++ b/internal/controller/dashboard/factory.go @@ -196,6 +196,10 @@ func detailsTab(kind, endpoint, schemaJSON string, keysOrder [][]string) map[str ) } if kind == "Tenant" { + leftColStack = append(leftColStack, antdFlexVertical("tenant-external-ip-count", 4, []any{ + antdText("tenant-external-ip-count-label", true, "External IPs count", nil), + parsedText("tenant-external-ip-count-value", `{reqsJsonPath[0]['.status.externalIPsCount']['0']}`, nil), + })) rightColStack = append(rightColStack, antdFlexVertical("resource-quotas-block", 4, []any{ antdText("resource-quotas-label", true, "Resource Quotas", map[string]any{ diff --git a/pkg/apis/apps/v1alpha1/types.go b/pkg/apis/apps/v1alpha1/types.go index 0118b0de..51247efb 100644 --- a/pkg/apis/apps/v1alpha1/types.go +++ b/pkg/apis/apps/v1alpha1/types.go @@ -47,6 +47,9 @@ type ApplicationStatus struct { // Namespace holds the computed namespace for Tenant applications. // +optional Namespace string `json:"namespace,omitempty"` + // ExternalIPsCount holds the number of LoadBalancer services with assigned external IPs for Tenant applications. + // +optional + ExternalIPsCount int32 `json:"externalIPsCount,omitempty"` } // GetConditions returns the status conditions of the object. diff --git a/pkg/registry/apps/application/rest.go b/pkg/registry/apps/application/rest.go index b42f322c..a69e23db 100644 --- a/pkg/registry/apps/application/rest.go +++ b/pkg/registry/apps/application/rest.go @@ -27,6 +27,7 @@ import ( "time" helmv2 "github.com/fluxcd/helm-controller/api/v2" + corev1 "k8s.io/api/core/v1" metainternalversion "k8s.io/apimachinery/pkg/apis/meta/internalversion" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/fields" @@ -186,7 +187,7 @@ func (r *REST) Create(ctx context.Context, obj runtime.Object, createValidation } // Convert the created HelmRelease back to Application - convertedApp, err := r.ConvertHelmReleaseToApplication(helmRelease) + convertedApp, err := r.ConvertHelmReleaseToApplication(ctx, helmRelease) if err != nil { klog.Errorf("Conversion error from HelmRelease to Application for resource %s: %v", helmRelease.GetName(), err) return nil, fmt.Errorf("conversion error: %v", err) @@ -233,7 +234,7 @@ func (r *REST) Get(ctx context.Context, name string, options *metav1.GetOptions) } // Convert HelmRelease to Application - convertedApp, err := r.ConvertHelmReleaseToApplication(helmRelease) + convertedApp, err := r.ConvertHelmReleaseToApplication(ctx, helmRelease) if err != nil { klog.Errorf("Conversion error from HelmRelease to Application for resource %s: %v", name, err) return nil, fmt.Errorf("conversion error: %v", err) @@ -360,7 +361,7 @@ func (r *REST) List(ctx context.Context, options *metainternalversion.ListOption continue } - app, err := r.ConvertHelmReleaseToApplication(hr) + app, err := r.ConvertHelmReleaseToApplication(ctx, hr) if err != nil { klog.Errorf("Error converting HelmRelease %s to Application: %v", hr.GetName(), err) continue @@ -514,7 +515,7 @@ func (r *REST) Update(ctx context.Context, name string, objInfo rest.UpdatedObje } // Convert the updated HelmRelease back to Application - convertedApp, err := r.ConvertHelmReleaseToApplication(helmRelease) + convertedApp, err := r.ConvertHelmReleaseToApplication(ctx, helmRelease) if err != nil { klog.Errorf("Conversion error from HelmRelease to Application for resource %s: %v", helmRelease.GetName(), err) return nil, false, fmt.Errorf("conversion error: %v", err) @@ -796,7 +797,7 @@ func (r *REST) Watch(ctx context.Context, options *metainternalversion.ListOptio // Note: All HelmReleases already match the required labels due to server-side label selector filtering // Convert HelmRelease to Application - app, err := r.ConvertHelmReleaseToApplication(hr) + app, err := r.ConvertHelmReleaseToApplication(ctx, hr) if err != nil { klog.Errorf("Error converting HelmRelease to Application: %v", err) continue @@ -967,11 +968,11 @@ func filterPrefixedMap(original map[string]string, prefix string) map[string]str } // ConvertHelmReleaseToApplication converts a HelmRelease to an Application -func (r *REST) ConvertHelmReleaseToApplication(hr *helmv2.HelmRelease) (appsv1alpha1.Application, error) { +func (r *REST) ConvertHelmReleaseToApplication(ctx context.Context, hr *helmv2.HelmRelease) (appsv1alpha1.Application, error) { klog.V(6).Infof("Converting HelmRelease to Application for resource %s", hr.GetName()) // Convert HelmRelease struct to Application struct - app, err := r.convertHelmReleaseToApplication(hr) + app, err := r.convertHelmReleaseToApplication(ctx, hr) if err != nil { klog.Errorf("Error converting from HelmRelease to Application: %v", err) return appsv1alpha1.Application{}, err @@ -1029,7 +1030,7 @@ func validateNoInternalKeys(values *apiextv1.JSON) error { } // convertHelmReleaseToApplication implements the actual conversion logic -func (r *REST) convertHelmReleaseToApplication(hr *helmv2.HelmRelease) (appsv1alpha1.Application, error) { +func (r *REST) convertHelmReleaseToApplication(ctx context.Context, hr *helmv2.HelmRelease) (appsv1alpha1.Application, error) { // Filter out internal keys (starting with "_") from spec filteredSpec := filterInternalKeys(hr.Spec.Values) @@ -1071,6 +1072,12 @@ func (r *REST) convertHelmReleaseToApplication(hr *helmv2.HelmRelease) (appsv1al // Add namespace field for Tenant applications if r.kindName == "Tenant" { app.Status.Namespace = r.computeTenantNamespace(hr.Namespace, app.Name) + externalIPsCount, err := r.countTenantExternalIPs(ctx, app.Status.Namespace) + if err != nil { + klog.Warningf("Failed to count external IPs for tenant %s/%s: %v", hr.Namespace, app.Name, err) + } else { + app.Status.ExternalIPsCount = externalIPsCount + } } return app, nil @@ -1265,6 +1272,33 @@ func (r *REST) computeTenantNamespace(currentNamespace, tenantName string) strin } } +func (r *REST) countTenantExternalIPs(ctx context.Context, namespace string) (int32, error) { + if namespace == "" { + return 0, nil + } + + var services corev1.ServiceList + if err := r.c.List(ctx, &services, client.InNamespace(namespace)); err != nil { + return 0, err + } + + var count int32 + for i := range services.Items { + svc := &services.Items[i] + if svc.Spec.Type != corev1.ServiceTypeLoadBalancer { + continue + } + for _, ingress := range svc.Status.LoadBalancer.Ingress { + if ingress.IP != "" { + count++ + break + } + } + } + + return count, nil +} + // Destroy releases resources associated with REST func (r *REST) Destroy() { // No additional actions needed to release resources.