[registry] Add application labels and update filtering mechanism

- Add three application metadata labels to HelmRelease:
  - apps.cozystack.io/application.kind
  - apps.cozystack.io/application.group
  - apps.cozystack.io/application.name

- Replace shouldIncludeHelmRelease filtering with label-based filtering
  in Get, List, and Update methods

- Always add kind and group label requirements in List for precise filtering

- Update CozystackResourceDefinitionController to watch only HelmReleases
  with cozystack.io/ui=true label

- Update LineageControllerWebhook to extract metadata directly from
  HelmRelease labels instead of using mapping configuration

- Add functionality to update HelmRelease chart from CozystackResourceDefinition
  using label selectors

Signed-off-by: Andrei Kvapil <kvapss@gmail.com>
This commit is contained in:
Andrei Kvapil 2025-12-10 00:09:36 +01:00
parent fe7bdcf06b
commit 669bf3d2f5
No known key found for this signature in database
GPG key ID: 931CF7FEACEAF765
12 changed files with 460 additions and 144 deletions

View file

@ -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
}

View file

@ -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><application-name>
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
}

View file

@ -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 {

View file

@ -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:

View file

@ -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:

View file

@ -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:

View file

@ -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:

View file

@ -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:

View file

@ -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

View file

@ -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:

View file

@ -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 "<unknown>"
}
// 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())

163
scripts/migrations/22 Executable file
View file

@ -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-