diff --git a/internal/controller/cozystackresource_controller.go b/internal/controller/cozystackresource_controller.go index 46884418..cc237fba 100644 --- a/internal/controller/cozystackresource_controller.go +++ b/internal/controller/cozystackresource_controller.go @@ -5,11 +5,13 @@ import ( "crypto/sha256" "encoding/hex" "encoding/json" + "fmt" "slices" "sync" "time" cozyv1alpha1 "github.com/cozystack/cozystack/api/v1alpha1" + helmv2 "github.com/fluxcd/helm-controller/api/v2" appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" @@ -37,6 +39,25 @@ type CozystackResourceDefinitionReconciler struct { } func (r *CozystackResourceDefinitionReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + logger := log.FromContext(ctx) + + // List all CozystackResourceDefinitions + crdList := &cozyv1alpha1.CozystackResourceDefinitionList{} + if err := r.List(ctx, crdList); err != nil { + logger.Error(err, "failed to list CozystackResourceDefinitions") + return ctrl.Result{}, err + } + + // Update HelmReleases for each CRD + for i := range crdList.Items { + crd := &crdList.Items[i] + if err := r.updateHelmReleasesForCRD(ctx, crd); err != nil { + logger.Error(err, "failed to update HelmReleases for CRD", "crd", crd.Name) + // Continue with other CRDs even if one fails + } + } + + // Continue with debounced restart logic return r.debouncedRestart(ctx) } @@ -61,6 +82,29 @@ func (r *CozystackResourceDefinitionReconciler) SetupWithManager(mgr ctrl.Manage }} }), ). + Watches( + &helmv2.HelmRelease{}, + handler.EnqueueRequestsFromMapFunc(func(ctx context.Context, obj client.Object) []reconcile.Request { + hr, ok := obj.(*helmv2.HelmRelease) + if !ok { + return nil + } + // Only watch HelmReleases with cozystack.io/ui=true label + if hr.Labels == nil || hr.Labels["cozystack.io/ui"] != "true" { + return nil + } + // Trigger reconciliation of all CRDs when a HelmRelease with the label is created/updated + r.mu.Lock() + r.lastEvent = time.Now() + r.mu.Unlock() + return []reconcile.Request{{ + NamespacedName: types.NamespacedName{ + Namespace: "cozy-system", + Name: "cozystack-api", + }, + }} + }), + ). Complete(r) } @@ -185,3 +229,115 @@ func sortCozyRDs(a, b cozyv1alpha1.CozystackResourceDefinition) int { } return 1 } + +// updateHelmReleasesForCRD updates all HelmReleases that match the application labels from CozystackResourceDefinition +func (r *CozystackResourceDefinitionReconciler) updateHelmReleasesForCRD(ctx context.Context, crd *cozyv1alpha1.CozystackResourceDefinition) error { + logger := log.FromContext(ctx) + + // Use application labels to find HelmReleases + // Labels: apps.cozystack.io/application.kind and apps.cozystack.io/application.group + applicationKind := crd.Spec.Application.Kind + + // Validate that applicationKind is non-empty + if applicationKind == "" { + logger.Error(fmt.Errorf("Application.Kind is empty"), "Skipping HelmRelease update: invalid CozystackResourceDefinition", "crd", crd.Name) + return nil + } + + applicationGroup := "apps.cozystack.io" // All applications use this group + + // Build label selector for HelmReleases + // Only reconcile HelmReleases with cozystack.io/ui=true label + labelSelector := client.MatchingLabels{ + "apps.cozystack.io/application.kind": applicationKind, + "apps.cozystack.io/application.group": applicationGroup, + "cozystack.io/ui": "true", + } + + // List all HelmReleases with matching labels + hrList := &helmv2.HelmReleaseList{} + if err := r.List(ctx, hrList, labelSelector); err != nil { + logger.Error(err, "failed to list HelmReleases", "kind", applicationKind, "group", applicationGroup) + return err + } + + logger.V(4).Info("Found HelmReleases to update", "crd", crd.Name, "kind", applicationKind, "count", len(hrList.Items)) + + // Update each HelmRelease + for i := range hrList.Items { + hr := &hrList.Items[i] + if err := r.updateHelmReleaseChart(ctx, hr, crd); err != nil { + logger.Error(err, "failed to update HelmRelease", "name", hr.Name, "namespace", hr.Namespace) + continue + } + } + + return nil +} + +// updateHelmReleaseChart updates the chart in HelmRelease based on CozystackResourceDefinition +func (r *CozystackResourceDefinitionReconciler) updateHelmReleaseChart(ctx context.Context, hr *helmv2.HelmRelease, crd *cozyv1alpha1.CozystackResourceDefinition) error { + logger := log.FromContext(ctx) + hrCopy := hr.DeepCopy() + updated := false + + // Validate Chart configuration exists + if crd.Spec.Release.Chart.Name == "" { + logger.V(4).Info("Skipping HelmRelease chart update: Chart.Name is empty", "crd", crd.Name) + return nil + } + + // Validate SourceRef fields + if crd.Spec.Release.Chart.SourceRef.Kind == "" || + crd.Spec.Release.Chart.SourceRef.Name == "" || + crd.Spec.Release.Chart.SourceRef.Namespace == "" { + logger.Error(fmt.Errorf("invalid SourceRef in CRD"), "Skipping HelmRelease chart update: SourceRef fields are incomplete", + "crd", crd.Name, + "kind", crd.Spec.Release.Chart.SourceRef.Kind, + "name", crd.Spec.Release.Chart.SourceRef.Name, + "namespace", crd.Spec.Release.Chart.SourceRef.Namespace) + return nil + } + + // Get version and reconcileStrategy from CRD or use defaults + version := ">= 0.0.0-0" + reconcileStrategy := "Revision" + // TODO: Add Version and ReconcileStrategy fields to CozystackResourceDefinitionChart if needed + + // Build expected SourceRef + expectedSourceRef := helmv2.CrossNamespaceObjectReference{ + Kind: crd.Spec.Release.Chart.SourceRef.Kind, + Name: crd.Spec.Release.Chart.SourceRef.Name, + Namespace: crd.Spec.Release.Chart.SourceRef.Namespace, + } + + if hrCopy.Spec.Chart == nil { + // Need to create Chart spec + hrCopy.Spec.Chart = &helmv2.HelmChartTemplate{ + Spec: helmv2.HelmChartTemplateSpec{ + Chart: crd.Spec.Release.Chart.Name, + Version: version, + ReconcileStrategy: reconcileStrategy, + SourceRef: expectedSourceRef, + }, + } + updated = true + } else { + // Update existing Chart spec + if hrCopy.Spec.Chart.Spec.Chart != crd.Spec.Release.Chart.Name || + hrCopy.Spec.Chart.Spec.SourceRef != expectedSourceRef { + hrCopy.Spec.Chart.Spec.Chart = crd.Spec.Release.Chart.Name + hrCopy.Spec.Chart.Spec.SourceRef = expectedSourceRef + updated = true + } + } + + if updated { + logger.V(4).Info("Updating HelmRelease chart", "name", hr.Name, "namespace", hr.Namespace) + if err := r.Update(ctx, hrCopy); err != nil { + return fmt.Errorf("failed to update HelmRelease: %w", err) + } + } + + return nil +} diff --git a/internal/lineagecontrollerwebhook/config.go b/internal/lineagecontrollerwebhook/config.go index 4ca45c13..7204ab66 100644 --- a/internal/lineagecontrollerwebhook/config.go +++ b/internal/lineagecontrollerwebhook/config.go @@ -2,49 +2,72 @@ package lineagecontrollerwebhook import ( "fmt" + "strings" cozyv1alpha1 "github.com/cozystack/cozystack/api/v1alpha1" helmv2 "github.com/fluxcd/helm-controller/api/v2" ) -type chartRef struct { - repo string - chart string -} - type appRef struct { group string kind string } type runtimeConfig struct { - chartAppMap map[chartRef]*cozyv1alpha1.CozystackResourceDefinition - appCRDMap map[appRef]*cozyv1alpha1.CozystackResourceDefinition + appCRDMap map[appRef]*cozyv1alpha1.CozystackResourceDefinition } func (l *LineageControllerWebhook) initConfig() { l.initOnce.Do(func() { if l.config.Load() == nil { l.config.Store(&runtimeConfig{ - chartAppMap: make(map[chartRef]*cozyv1alpha1.CozystackResourceDefinition), - appCRDMap: make(map[appRef]*cozyv1alpha1.CozystackResourceDefinition), + appCRDMap: make(map[appRef]*cozyv1alpha1.CozystackResourceDefinition), }) } }) } -func (l *LineageControllerWebhook) Map(hr *helmv2.HelmRelease) (string, string, string, error) { - cfg, ok := l.config.Load().(*runtimeConfig) +// getApplicationLabel safely extracts an application label from HelmRelease +func getApplicationLabel(hr *helmv2.HelmRelease, key string) (string, error) { + if hr.Labels == nil { + return "", fmt.Errorf("cannot map helm release %s/%s to dynamic app: labels are nil", hr.Namespace, hr.Name) + } + val, ok := hr.Labels[key] if !ok { - return "", "", "", fmt.Errorf("failed to load chart-app mapping from config") + return "", fmt.Errorf("cannot map helm release %s/%s to dynamic app: missing %s label", hr.Namespace, hr.Name, key) } - if hr.Spec.Chart == nil { - return "", "", "", fmt.Errorf("cannot map helm release %s/%s to dynamic app", hr.Namespace, hr.Name) - } - s := hr.Spec.Chart.Spec - val, ok := cfg.chartAppMap[chartRef{s.SourceRef.Name, s.Chart}] - if !ok { - return "", "", "", fmt.Errorf("cannot map helm release %s/%s to dynamic app", hr.Namespace, hr.Name) - } - return "apps.cozystack.io/v1alpha1", val.Spec.Application.Kind, val.Spec.Release.Prefix, nil + return val, nil +} + +func (l *LineageControllerWebhook) Map(hr *helmv2.HelmRelease) (string, string, string, error) { + // Extract application metadata from labels + appKind, err := getApplicationLabel(hr, "apps.cozystack.io/application.kind") + if err != nil { + return "", "", "", err + } + + appGroup, err := getApplicationLabel(hr, "apps.cozystack.io/application.group") + if err != nil { + return "", "", "", err + } + + appName, err := getApplicationLabel(hr, "apps.cozystack.io/application.name") + if err != nil { + return "", "", "", err + } + + // Construct API version from group + apiVersion := fmt.Sprintf("%s/v1alpha1", appGroup) + + // Extract prefix from HelmRelease name by removing the application name + // HelmRelease name format: + prefix := strings.TrimSuffix(hr.Name, appName) + + // Validate the derived prefix + // This ensures correctness when appName appears multiple times in hr.Name + if prefix+appName != hr.Name { + return "", "", "", fmt.Errorf("cannot derive prefix from helm release %s/%s: name does not end with application name %s", hr.Namespace, hr.Name, appName) + } + + return apiVersion, appKind, prefix, nil } diff --git a/internal/lineagecontrollerwebhook/controller.go b/internal/lineagecontrollerwebhook/controller.go index e7522f62..092d16e7 100644 --- a/internal/lineagecontrollerwebhook/controller.go +++ b/internal/lineagecontrollerwebhook/controller.go @@ -24,25 +24,15 @@ func (c *LineageControllerWebhook) Reconcile(ctx context.Context, req ctrl.Reque return ctrl.Result{}, err } cfg := &runtimeConfig{ - chartAppMap: make(map[chartRef]*cozyv1alpha1.CozystackResourceDefinition), - appCRDMap: make(map[appRef]*cozyv1alpha1.CozystackResourceDefinition), + appCRDMap: make(map[appRef]*cozyv1alpha1.CozystackResourceDefinition), } for _, crd := range crds.Items { - chRef := chartRef{ - crd.Spec.Release.Chart.SourceRef.Name, - crd.Spec.Release.Chart.Name, - } appRef := appRef{ "apps.cozystack.io", crd.Spec.Application.Kind, } newRef := crd - if _, exists := cfg.chartAppMap[chRef]; exists { - l.Info("duplicate chart mapping detected; ignoring subsequent entry", "key", chRef) - } else { - cfg.chartAppMap[chRef] = &newRef - } if _, exists := cfg.appCRDMap[appRef]; exists { l.Info("duplicate app mapping detected; ignoring subsequent entry", "key", appRef) } else { diff --git a/packages/apps/tenant/templates/etcd.yaml b/packages/apps/tenant/templates/etcd.yaml index 9db6ff6c..8cd720cc 100644 --- a/packages/apps/tenant/templates/etcd.yaml +++ b/packages/apps/tenant/templates/etcd.yaml @@ -9,6 +9,9 @@ metadata: internal.cozystack.io/tenantmodule: "true" app.kubernetes.io/instance: {{ .Release.Name }} app.kubernetes.io/managed-by: {{ .Release.Service }} + apps.cozystack.io/application.kind: Etcd + apps.cozystack.io/application.group: apps.cozystack.io + apps.cozystack.io/application.name: etcd spec: chart: spec: diff --git a/packages/apps/tenant/templates/info.yaml b/packages/apps/tenant/templates/info.yaml index 4b6f2640..4a66e422 100644 --- a/packages/apps/tenant/templates/info.yaml +++ b/packages/apps/tenant/templates/info.yaml @@ -8,6 +8,9 @@ metadata: internal.cozystack.io/tenantmodule: "true" app.kubernetes.io/instance: {{ .Release.Name }} app.kubernetes.io/managed-by: {{ .Release.Service }} + apps.cozystack.io/application.kind: Info + apps.cozystack.io/application.group: apps.cozystack.io + apps.cozystack.io/application.name: info spec: chart: spec: diff --git a/packages/apps/tenant/templates/ingress.yaml b/packages/apps/tenant/templates/ingress.yaml index b866300f..beb07342 100644 --- a/packages/apps/tenant/templates/ingress.yaml +++ b/packages/apps/tenant/templates/ingress.yaml @@ -9,6 +9,9 @@ metadata: internal.cozystack.io/tenantmodule: "true" app.kubernetes.io/instance: {{ .Release.Name }} app.kubernetes.io/managed-by: {{ .Release.Service }} + apps.cozystack.io/application.kind: Ingress + apps.cozystack.io/application.group: apps.cozystack.io + apps.cozystack.io/application.name: ingress spec: chart: spec: diff --git a/packages/apps/tenant/templates/monitoring.yaml b/packages/apps/tenant/templates/monitoring.yaml index edcc66d9..4986fc21 100644 --- a/packages/apps/tenant/templates/monitoring.yaml +++ b/packages/apps/tenant/templates/monitoring.yaml @@ -9,6 +9,9 @@ metadata: internal.cozystack.io/tenantmodule: "true" app.kubernetes.io/instance: {{ .Release.Name }} app.kubernetes.io/managed-by: {{ .Release.Service }} + apps.cozystack.io/application.kind: Monitoring + apps.cozystack.io/application.group: apps.cozystack.io + apps.cozystack.io/application.name: monitoring spec: chart: spec: diff --git a/packages/apps/tenant/templates/seaweedfs.yaml b/packages/apps/tenant/templates/seaweedfs.yaml index 936e294b..9a714b79 100644 --- a/packages/apps/tenant/templates/seaweedfs.yaml +++ b/packages/apps/tenant/templates/seaweedfs.yaml @@ -9,6 +9,9 @@ metadata: internal.cozystack.io/tenantmodule: "true" app.kubernetes.io/instance: {{ .Release.Name }} app.kubernetes.io/managed-by: {{ .Release.Service }} + apps.cozystack.io/application.kind: SeaweedFS + apps.cozystack.io/application.group: apps.cozystack.io + apps.cozystack.io/application.name: seaweedfs spec: chart: spec: diff --git a/packages/core/platform/templates/apps.yaml b/packages/core/platform/templates/apps.yaml index c7653e03..514dcffb 100644 --- a/packages/core/platform/templates/apps.yaml +++ b/packages/core/platform/templates/apps.yaml @@ -36,6 +36,9 @@ metadata: namespace: tenant-root labels: cozystack.io/ui: "true" + apps.cozystack.io/application.kind: Tenant + apps.cozystack.io/application.group: apps.cozystack.io + apps.cozystack.io/application.name: tenant-root spec: interval: 0s releaseName: tenant-root diff --git a/packages/system/bootbox/templates/bootbox.yaml b/packages/system/bootbox/templates/bootbox.yaml index 496e42e5..45cd07ce 100644 --- a/packages/system/bootbox/templates/bootbox.yaml +++ b/packages/system/bootbox/templates/bootbox.yaml @@ -5,6 +5,9 @@ metadata: helm.sh/resource-policy: keep labels: cozystack.io/ui: "true" + apps.cozystack.io/application.kind: BootBox + apps.cozystack.io/application.group: apps.cozystack.io + apps.cozystack.io/application.name: bootbox name: bootbox namespace: tenant-root spec: diff --git a/pkg/registry/apps/application/rest.go b/pkg/registry/apps/application/rest.go index 704cd709..39c0ddb8 100644 --- a/pkg/registry/apps/application/rest.go +++ b/pkg/registry/apps/application/rest.go @@ -32,6 +32,7 @@ import ( labels "k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/selection" "k8s.io/apimachinery/pkg/util/duration" "k8s.io/apimachinery/pkg/watch" "k8s.io/apiserver/pkg/endpoints/request" @@ -66,6 +67,13 @@ const ( AnnotationPrefix = "apps.cozystack.io-" ) +// Application label keys +const ( + ApplicationKindLabel = "apps.cozystack.io/application.kind" + ApplicationGroupLabel = "apps.cozystack.io/application.group" + ApplicationNameLabel = "apps.cozystack.io/application.name" +) + // Define the GroupVersionResource for HelmRelease var helmReleaseGVR = schema.GroupVersionResource{ Group: "helm.toolkit.fluxcd.io", @@ -157,6 +165,13 @@ func (r *REST) Create(ctx context.Context, obj runtime.Object, createValidation helmRelease.Labels = mergeMaps(r.releaseConfig.Labels, helmRelease.Labels) // Merge user labels with prefix helmRelease.Labels = mergeMaps(helmRelease.Labels, addPrefixedMap(app.Labels, LabelPrefix)) + // Add application metadata labels + if helmRelease.Labels == nil { + helmRelease.Labels = make(map[string]string) + } + helmRelease.Labels[ApplicationKindLabel] = r.kindName + helmRelease.Labels[ApplicationGroupLabel] = r.gvk.Group + helmRelease.Labels[ApplicationNameLabel] = app.Name // Note: Annotations from config are not handled as r.releaseConfig.Annotations is undefined klog.V(6).Infof("Creating HelmRelease %s in namespace %s", helmRelease.Name, app.Namespace) @@ -208,9 +223,10 @@ func (r *REST) Get(ctx context.Context, name string, options *metav1.GetOptions) return nil, err } - // Check if HelmRelease meets the required chartName and sourceRef criteria - if !r.shouldIncludeHelmRelease(helmRelease) { - klog.Errorf("HelmRelease %s does not match the required chartName and sourceRef criteria", helmReleaseName) + // Check if HelmRelease has required labels + if helmRelease.Labels == nil || helmRelease.Labels[ApplicationKindLabel] != r.kindName || + helmRelease.Labels[ApplicationGroupLabel] != r.gvk.Group { + klog.Errorf("HelmRelease %s does not match the required application labels", helmReleaseName) // Return a NotFound error for the Application resource return nil, apierrors.NewNotFound(r.gvr.GroupResource(), name) } @@ -266,6 +282,19 @@ func (r *REST) List(ctx context.Context, options *metainternalversion.ListOption } // Process label.selector + // Always add application metadata label requirements + appKindReq, err := labels.NewRequirement(ApplicationKindLabel, selection.Equals, []string{r.kindName}) + if err != nil { + klog.Errorf("Error creating application kind label requirement: %v", err) + return nil, fmt.Errorf("error creating application kind label requirement: %v", err) + } + appGroupReq, err := labels.NewRequirement(ApplicationGroupLabel, selection.Equals, []string{r.gvk.Group}) + if err != nil { + klog.Errorf("Error creating application group label requirement: %v", err) + return nil, fmt.Errorf("error creating application group label requirement: %v", err) + } + labelRequirements := []labels.Requirement{*appKindReq, *appGroupReq} + if options.LabelSelector != nil { ls := options.LabelSelector.String() parsedLabels, err := labels.Parse(ls) @@ -285,9 +314,12 @@ func (r *REST) List(ctx context.Context, options *metainternalversion.ListOption } prefixedReqs = append(prefixedReqs, *prefixedReq) } - helmLabelSelector = labels.NewSelector().Add(prefixedReqs...).String() + labelRequirements = append(labelRequirements, prefixedReqs...) } } + helmLabelSelector = labels.NewSelector().Add(labelRequirements...).String() + + klog.V(6).Infof("Using label selector: %s for kind: %s, group: %s", helmLabelSelector, r.kindName, r.gvk.Group) // Set ListOptions for HelmRelease with selector mapping metaOptions := metav1.ListOptions{ @@ -306,18 +338,30 @@ func (r *REST) List(ctx context.Context, options *metainternalversion.ListOption return nil, err } + klog.V(6).Infof("Found %d HelmReleases with label selector, filtering by labels...", len(hrList.Items)) + // Initialize Application items array items := make([]appsv1alpha1.Application, 0, len(hrList.Items)) // Iterate over HelmReleases and convert to Applications + // Filter by labels to ensure only relevant HelmReleases are included + // This is a safety check in case label selectors don't work perfectly or HelmReleases were created without labels for i := range hrList.Items { - if !r.shouldIncludeHelmRelease(&hrList.Items[i]) { + hr := &hrList.Items[i] + + // Verify that HelmRelease has the required labels + if hr.Labels == nil || hr.Labels[ApplicationKindLabel] != r.kindName || + hr.Labels[ApplicationGroupLabel] != r.gvk.Group { + klog.V(6).Infof("Skipping HelmRelease %s - missing or incorrect application labels (kind: %s, expected: %s, group: %s, expected: %s)", + hr.GetName(), + getLabelValue(hr.Labels, ApplicationKindLabel), r.kindName, + getLabelValue(hr.Labels, ApplicationGroupLabel), r.gvk.Group) continue } - app, err := r.ConvertHelmReleaseToApplication(&hrList.Items[i]) + app, err := r.ConvertHelmReleaseToApplication(hr) if err != nil { - klog.Errorf("Error converting HelmRelease %s to Application: %v", hrList.Items[i].GetName(), err) + klog.Errorf("Error converting HelmRelease %s to Application: %v", hr.GetName(), err) continue } @@ -436,18 +480,17 @@ func (r *REST) Update(ctx context.Context, name string, objInfo rest.UpdatedObje helmRelease.Labels = mergeMaps(r.releaseConfig.Labels, helmRelease.Labels) // Merge user labels with prefix helmRelease.Labels = mergeMaps(helmRelease.Labels, addPrefixedMap(app.Labels, LabelPrefix)) + // Add application metadata labels + if helmRelease.Labels == nil { + helmRelease.Labels = make(map[string]string) + } + helmRelease.Labels[ApplicationKindLabel] = r.kindName + helmRelease.Labels[ApplicationGroupLabel] = r.gvk.Group + helmRelease.Labels[ApplicationNameLabel] = app.Name // Note: Annotations from config are not handled as r.releaseConfig.Annotations is undefined klog.V(6).Infof("Updating HelmRelease %s in namespace %s", helmRelease.Name, helmRelease.Namespace) - // Before updating, ensure the HelmRelease meets the inclusion criteria - // This prevents updating HelmReleases that should not be managed as Applications - if !r.shouldIncludeHelmRelease(helmRelease) { - klog.Errorf("HelmRelease %s does not match the required chartName and sourceRef criteria", helmRelease.Name) - // Return a NotFound error for the Application resource - return nil, false, apierrors.NewNotFound(r.gvr.GroupResource(), name) - } - // Update the HelmRelease in Kubernetes err = r.c.Update(ctx, helmRelease, &client.UpdateOptions{Raw: &metav1.UpdateOptions{}}) if err != nil { @@ -455,13 +498,6 @@ func (r *REST) Update(ctx context.Context, name string, objInfo rest.UpdatedObje return nil, false, fmt.Errorf("failed to update HelmRelease: %v", err) } - // After updating, ensure the updated HelmRelease still meets the inclusion criteria - if !r.shouldIncludeHelmRelease(helmRelease) { - klog.Errorf("Updated HelmRelease %s does not match the required chartName and sourceRef criteria", helmRelease.GetName()) - // Return a NotFound error for the Application resource - return nil, false, apierrors.NewNotFound(r.gvr.GroupResource(), name) - } - // Convert the updated HelmRelease back to Application convertedApp, err := r.ConvertHelmReleaseToApplication(helmRelease) if err != nil { @@ -503,9 +539,10 @@ func (r *REST) Delete(ctx context.Context, name string, deleteValidation rest.Va return nil, false, err } - // Validate that the HelmRelease meets the inclusion criteria - if !r.shouldIncludeHelmRelease(helmRelease) { - klog.Errorf("HelmRelease %s does not match the required chartName and sourceRef criteria", helmReleaseName) + // Validate that the HelmRelease has required labels + if helmRelease.Labels == nil || helmRelease.Labels[ApplicationKindLabel] != r.kindName || + helmRelease.Labels[ApplicationGroupLabel] != r.gvk.Group { + klog.Errorf("HelmRelease %s does not match the required application labels", helmReleaseName) // Return NotFound error for Application resource return nil, false, apierrors.NewNotFound(r.gvr.GroupResource(), name) } @@ -523,7 +560,7 @@ func (r *REST) Delete(ctx context.Context, name string, deleteValidation rest.Va return nil, true, nil } -// Watch sets up a watch on HelmReleases, filters them based on sourceRef and prefix, and converts events to Applications +// Watch sets up a watch on HelmReleases, filters them based on application labels, and converts events to Applications func (r *REST) Watch(ctx context.Context, options *metainternalversion.ListOptions) (watch.Interface, error) { namespace, err := r.getNamespace(ctx) if err != nil { @@ -639,7 +676,10 @@ func (r *REST) Watch(ctx context.Context, options *metainternalversion.ListOptio continue } - if !r.shouldIncludeHelmRelease(hr) { + // Verify that HelmRelease has the required labels + if hr.Labels == nil || hr.Labels[ApplicationKindLabel] != r.kindName || + hr.Labels[ApplicationGroupLabel] != r.gvk.Group { + klog.V(6).Infof("Skipping HelmRelease %s in watch - missing or incorrect application labels", hr.GetName()) continue } @@ -694,14 +734,6 @@ func (r *REST) Watch(ctx context.Context, options *metainternalversion.ListOptio return customW, nil } -// Helper function to get HelmRelease name from object -func helmReleaseName(obj runtime.Object) string { - if app, ok := obj.(*appsv1alpha1.Application); ok { - return app.GetName() - } - return "" -} - // customWatcher wraps the original watcher and filters/converts events type customWatcher struct { resultChan chan watch.Event @@ -725,74 +757,6 @@ func (cw *customWatcher) ResultChan() <-chan watch.Event { return cw.resultChan } -// shouldIncludeHelmRelease determines if a HelmRelease should be included based on filtering criteria -func (r *REST) shouldIncludeHelmRelease(hr *helmv2.HelmRelease) bool { - // Nil check for Chart field - if hr.Spec.Chart == nil { - klog.V(6).Infof("HelmRelease %s has nil spec.chart field", hr.GetName()) - return false - } - - // Filter by Chart Name - chartName := hr.Spec.Chart.Spec.Chart - if chartName == "" { - klog.V(6).Infof("HelmRelease %s missing spec.chart.spec.chart field", hr.GetName()) - return false - } - if chartName != r.releaseConfig.Chart.Name { - klog.V(6).Infof("HelmRelease %s chart name %s does not match expected %s", hr.GetName(), chartName, r.releaseConfig.Chart.Name) - return false - } - - // Filter by SourceRefConfig and Prefix - return r.matchesSourceRefAndPrefix(hr) -} - -// matchesSourceRefAndPrefix checks both SourceRefConfig and Prefix criteria -func (r *REST) matchesSourceRefAndPrefix(hr *helmv2.HelmRelease) bool { - // Nil check for Chart field (defensive) - if hr.Spec.Chart == nil { - klog.V(6).Infof("HelmRelease %s has nil spec.chart field", hr.GetName()) - return false - } - - // Extract SourceRef fields - sourceRef := hr.Spec.Chart.Spec.SourceRef - sourceRefKind := sourceRef.Kind - sourceRefName := sourceRef.Name - sourceRefNamespace := sourceRef.Namespace - - if sourceRefKind == "" { - klog.V(6).Infof("HelmRelease %s missing spec.chart.spec.sourceRef.kind field", hr.GetName()) - return false - } - if sourceRefName == "" { - klog.V(6).Infof("HelmRelease %s missing spec.chart.spec.sourceRef.name field", hr.GetName()) - return false - } - if sourceRefNamespace == "" { - klog.V(6).Infof("HelmRelease %s missing spec.chart.spec.sourceRef.namespace field", hr.GetName()) - return false - } - - // Check if SourceRef matches the configuration - if sourceRefKind != r.releaseConfig.Chart.SourceRef.Kind || - sourceRefName != r.releaseConfig.Chart.SourceRef.Name || - sourceRefNamespace != r.releaseConfig.Chart.SourceRef.Namespace { - klog.V(6).Infof("HelmRelease %s sourceRef does not match expected values", hr.GetName()) - return false - } - - // Additional filtering by Prefix - name := hr.GetName() - if !strings.HasPrefix(name, r.releaseConfig.Prefix) { - klog.V(6).Infof("HelmRelease %s does not have the expected prefix %s", name, r.releaseConfig.Prefix) - return false - } - - return true -} - // getNamespace extracts the namespace from the context func (r *REST) getNamespace(ctx context.Context) (string, error) { namespace, ok := request.NamespaceFrom(ctx) @@ -804,15 +768,6 @@ func (r *REST) getNamespace(ctx context.Context) (string, error) { return namespace, nil } -// buildLabelSelector constructs a label selector string from a map of labels -func buildLabelSelector(labels map[string]string) string { - var selectors []string - for k, v := range labels { - selectors = append(selectors, fmt.Sprintf("%s=%s", k, v)) - } - return strings.Join(selectors, ",") -} - // mergeMaps combines two maps of labels or annotations func mergeMaps(a, b map[string]string) map[string]string { if a == nil && b == nil { @@ -861,6 +816,14 @@ func filterPrefixedMap(original map[string]string, prefix string) map[string]str return processed } +// getLabelValue safely gets a label value, returning empty string if labels is nil or key doesn't exist +func getLabelValue(labels map[string]string, key string) string { + if labels == nil { + return "" + } + return labels[key] +} + // ConvertHelmReleaseToApplication converts a HelmRelease to an Application func (r *REST) ConvertHelmReleaseToApplication(hr *helmv2.HelmRelease) (appsv1alpha1.Application, error) { klog.V(6).Infof("Converting HelmRelease to Application for resource %s", hr.GetName()) diff --git a/scripts/migrations/22 b/scripts/migrations/22 new file mode 100755 index 00000000..192e431c --- /dev/null +++ b/scripts/migrations/22 @@ -0,0 +1,163 @@ +#!/bin/sh +# Migration 22 --> 23 + +set -euo pipefail + +echo "Migrating HelmReleases: adding application labels for tenant-* namespaces" + +# Function to determine application type from HelmRelease name +determine_app_type() { + local name="$1" + local app_kind="" + local app_name="" + + # Try to match by prefix (longest match first) + case "$name" in + virtual-machine-*) + app_kind="VirtualMachine" + app_name="${name#virtual-machine-}" + ;; + vm-instance-*) + app_kind="VMInstance" + app_name="${name#vm-instance-}" + ;; + vm-disk-*) + app_kind="VMDisk" + app_name="${name#vm-disk-}" + ;; + virtualprivatecloud-*) + app_kind="VirtualPrivateCloud" + app_name="${name#virtualprivatecloud-}" + ;; + http-cache-*) + app_kind="HTTPCache" + app_name="${name#http-cache-}" + ;; + tcp-balancer-*) + app_kind="TCPBalancer" + app_name="${name#tcp-balancer-}" + ;; + clickhouse-*) + app_kind="ClickHouse" + app_name="${name#clickhouse-}" + ;; + foundationdb-*) + app_kind="FoundationDB" + app_name="${name#foundationdb-}" + ;; + ferretdb-*) + app_kind="FerretDB" + app_name="${name#ferretdb-}" + ;; + rabbitmq-*) + app_kind="RabbitMQ" + app_name="${name#rabbitmq-}" + ;; + kubernetes-*) + app_kind="Kubernetes" + app_name="${name#kubernetes-}" + ;; + bucket-*) + app_kind="Bucket" + app_name="${name#bucket-}" + ;; + kafka-*) + app_kind="Kafka" + app_name="${name#kafka-}" + ;; + mysql-*) + app_kind="MySQL" + app_name="${name#mysql-}" + ;; + nats-*) + app_kind="NATS" + app_name="${name#nats-}" + ;; + postgres-*) + app_kind="PostgreSQL" + app_name="${name#postgres-}" + ;; + redis-*) + app_kind="Redis" + app_name="${name#redis-}" + ;; + tenant-*) + app_kind="Tenant" + app_name="${name#tenant-}" + ;; + vpn-*) + app_kind="VPN" + app_name="${name#vpn-}" + ;; + bootbox) + app_kind="BootBox" + app_name="bootbox" + ;; + etcd) + app_kind="Etcd" + app_name="etcd" + ;; + info) + app_kind="Info" + app_name="info" + ;; + ingress|ingress-*) + app_kind="Ingress" + if [ "$name" = "ingress" ]; then + app_name="ingress" + else + app_name="${name#ingress-}" + fi + ;; + monitoring) + app_kind="Monitoring" + app_name="monitoring" + ;; + seaweedfs) + app_kind="SeaweedFS" + app_name="seaweedfs" + ;; + *) + # Unknown type + return 1 + ;; + esac + + echo "$app_kind|$app_name" + return 0 +} + +# Process all HelmReleases in tenant-* namespaces with cozystack.io/ui=true label +kubectl get helmreleases --all-namespaces -l cozystack.io/ui=true -o json | \ + jq -r '.items[] | select(.metadata.namespace | startswith("tenant-")) | "\(.metadata.namespace)|\(.metadata.name)"' | \ + while IFS='|' read -r namespace name; do + echo "Processing HelmRelease $namespace/$name" + + # Determine application type + app_type=$(determine_app_type "$name") + status=$? + if [ $status -ne 0 ] || [ -z "$app_type" ]; then + echo "Warning: Could not determine application type for $namespace/$name, skipping" + continue + fi + + app_kind=$(echo "$app_type" | cut -d'|' -f1) + app_name=$(echo "$app_type" | cut -d'|' -f2) + app_group="apps.cozystack.io" + + # Build labels string + labels="apps.cozystack.io/application.kind=$app_kind" + labels="$labels apps.cozystack.io/application.group=$app_group" + labels="$labels apps.cozystack.io/application.name=$app_name" + + # Apply labels using kubectl label --overwrite + kubectl label helmrelease -n "$namespace" "$name" --overwrite $labels + echo "Added application labels to $namespace/$name: $labels" + done + +echo "Migration completed" + +# Stamp version +kubectl create configmap -n cozy-system cozystack-version \ + --from-literal=version=23 --dry-run=client -o yaml | kubectl apply -f- +