Skip to content

Commit fdca498

Browse files
committed
[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 <[email protected]>
1 parent fe7bdcf commit fdca498

12 files changed

Lines changed: 458 additions & 142 deletions

File tree

‎internal/controller/cozystackresource_controller.go‎

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,13 @@ import (
55
"crypto/sha256"
66
"encoding/hex"
77
"encoding/json"
8+
"fmt"
89
"slices"
910
"sync"
1011
"time"
1112

1213
cozyv1alpha1 "github.com/cozystack/cozystack/api/v1alpha1"
14+
helmv2 "github.com/fluxcd/helm-controller/api/v2"
1315

1416
appsv1 "k8s.io/api/apps/v1"
1517
corev1 "k8s.io/api/core/v1"
@@ -37,6 +39,25 @@ type CozystackResourceDefinitionReconciler struct {
3739
}
3840

3941
func (r *CozystackResourceDefinitionReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
42+
logger := log.FromContext(ctx)
43+
44+
// List all CozystackResourceDefinitions
45+
crdList := &cozyv1alpha1.CozystackResourceDefinitionList{}
46+
if err := r.List(ctx, crdList); err != nil {
47+
logger.Error(err, "failed to list CozystackResourceDefinitions")
48+
return ctrl.Result{}, err
49+
}
50+
51+
// Update HelmReleases for each CRD
52+
for i := range crdList.Items {
53+
crd := &crdList.Items[i]
54+
if err := r.updateHelmReleasesForCRD(ctx, crd); err != nil {
55+
logger.Error(err, "failed to update HelmReleases for CRD", "crd", crd.Name)
56+
// Continue with other CRDs even if one fails
57+
}
58+
}
59+
60+
// Continue with debounced restart logic
4061
return r.debouncedRestart(ctx)
4162
}
4263

@@ -61,6 +82,29 @@ func (r *CozystackResourceDefinitionReconciler) SetupWithManager(mgr ctrl.Manage
6182
}}
6283
}),
6384
).
85+
Watches(
86+
&helmv2.HelmRelease{},
87+
handler.EnqueueRequestsFromMapFunc(func(ctx context.Context, obj client.Object) []reconcile.Request {
88+
hr, ok := obj.(*helmv2.HelmRelease)
89+
if !ok {
90+
return nil
91+
}
92+
// Only watch HelmReleases with cozystack.io/ui=true label
93+
if hr.Labels == nil || hr.Labels["cozystack.io/ui"] != "true" {
94+
return nil
95+
}
96+
// Trigger reconciliation of all CRDs when a HelmRelease with the label is created/updated
97+
r.mu.Lock()
98+
r.lastEvent = time.Now()
99+
r.mu.Unlock()
100+
return []reconcile.Request{{
101+
NamespacedName: types.NamespacedName{
102+
Namespace: "cozy-system",
103+
Name: "cozystack-api",
104+
},
105+
}}
106+
}),
107+
).
64108
Complete(r)
65109
}
66110

@@ -185,3 +229,115 @@ func sortCozyRDs(a, b cozyv1alpha1.CozystackResourceDefinition) int {
185229
}
186230
return 1
187231
}
232+
233+
// updateHelmReleasesForCRD updates all HelmReleases that match the application labels from CozystackResourceDefinition
234+
func (r *CozystackResourceDefinitionReconciler) updateHelmReleasesForCRD(ctx context.Context, crd *cozyv1alpha1.CozystackResourceDefinition) error {
235+
logger := log.FromContext(ctx)
236+
237+
// Use application labels to find HelmReleases
238+
// Labels: apps.cozystack.io/application.kind and apps.cozystack.io/application.group
239+
applicationKind := crd.Spec.Application.Kind
240+
241+
// Validate that applicationKind is non-empty
242+
if applicationKind == "" {
243+
logger.Error(fmt.Errorf("Application.Kind is empty"), "Skipping HelmRelease update: invalid CozystackResourceDefinition", "crd", crd.Name)
244+
return nil
245+
}
246+
247+
applicationGroup := "apps.cozystack.io" // All applications use this group
248+
249+
// Build label selector for HelmReleases
250+
// Only reconcile HelmReleases with cozystack.io/ui=true label
251+
labelSelector := client.MatchingLabels{
252+
"apps.cozystack.io/application.kind": applicationKind,
253+
"apps.cozystack.io/application.group": applicationGroup,
254+
"cozystack.io/ui": "true",
255+
}
256+
257+
// List all HelmReleases with matching labels
258+
hrList := &helmv2.HelmReleaseList{}
259+
if err := r.List(ctx, hrList, labelSelector); err != nil {
260+
logger.Error(err, "failed to list HelmReleases", "kind", applicationKind, "group", applicationGroup)
261+
return err
262+
}
263+
264+
logger.V(4).Info("Found HelmReleases to update", "crd", crd.Name, "kind", applicationKind, "count", len(hrList.Items))
265+
266+
// Update each HelmRelease
267+
for i := range hrList.Items {
268+
hr := &hrList.Items[i]
269+
if err := r.updateHelmReleaseChart(ctx, hr, crd); err != nil {
270+
logger.Error(err, "failed to update HelmRelease", "name", hr.Name, "namespace", hr.Namespace)
271+
continue
272+
}
273+
}
274+
275+
return nil
276+
}
277+
278+
// updateHelmReleaseChart updates the chart in HelmRelease based on CozystackResourceDefinition
279+
func (r *CozystackResourceDefinitionReconciler) updateHelmReleaseChart(ctx context.Context, hr *helmv2.HelmRelease, crd *cozyv1alpha1.CozystackResourceDefinition) error {
280+
logger := log.FromContext(ctx)
281+
hrCopy := hr.DeepCopy()
282+
updated := false
283+
284+
// Validate Chart configuration exists
285+
if crd.Spec.Release.Chart.Name == "" {
286+
logger.V(4).Info("Skipping HelmRelease chart update: Chart.Name is empty", "crd", crd.Name)
287+
return nil
288+
}
289+
290+
// Validate SourceRef fields
291+
if crd.Spec.Release.Chart.SourceRef.Kind == "" ||
292+
crd.Spec.Release.Chart.SourceRef.Name == "" ||
293+
crd.Spec.Release.Chart.SourceRef.Namespace == "" {
294+
logger.Error(fmt.Errorf("invalid SourceRef in CRD"), "Skipping HelmRelease chart update: SourceRef fields are incomplete",
295+
"crd", crd.Name,
296+
"kind", crd.Spec.Release.Chart.SourceRef.Kind,
297+
"name", crd.Spec.Release.Chart.SourceRef.Name,
298+
"namespace", crd.Spec.Release.Chart.SourceRef.Namespace)
299+
return nil
300+
}
301+
302+
// Get version and reconcileStrategy from CRD or use defaults
303+
version := ">= 0.0.0-0"
304+
reconcileStrategy := "Revision"
305+
// TODO: Add Version and ReconcileStrategy fields to CozystackResourceDefinitionChart if needed
306+
307+
// Build expected SourceRef
308+
expectedSourceRef := helmv2.CrossNamespaceObjectReference{
309+
Kind: crd.Spec.Release.Chart.SourceRef.Kind,
310+
Name: crd.Spec.Release.Chart.SourceRef.Name,
311+
Namespace: crd.Spec.Release.Chart.SourceRef.Namespace,
312+
}
313+
314+
if hrCopy.Spec.Chart == nil {
315+
// Need to create Chart spec
316+
hrCopy.Spec.Chart = &helmv2.HelmChartTemplate{
317+
Spec: helmv2.HelmChartTemplateSpec{
318+
Chart: crd.Spec.Release.Chart.Name,
319+
Version: version,
320+
ReconcileStrategy: reconcileStrategy,
321+
SourceRef: expectedSourceRef,
322+
},
323+
}
324+
updated = true
325+
} else {
326+
// Update existing Chart spec
327+
if hrCopy.Spec.Chart.Spec.Chart != crd.Spec.Release.Chart.Name ||
328+
hrCopy.Spec.Chart.Spec.SourceRef != expectedSourceRef {
329+
hrCopy.Spec.Chart.Spec.Chart = crd.Spec.Release.Chart.Name
330+
hrCopy.Spec.Chart.Spec.SourceRef = expectedSourceRef
331+
updated = true
332+
}
333+
}
334+
335+
if updated {
336+
logger.V(4).Info("Updating HelmRelease chart", "name", hr.Name, "namespace", hr.Namespace)
337+
if err := r.Update(ctx, hrCopy); err != nil {
338+
return fmt.Errorf("failed to update HelmRelease: %w", err)
339+
}
340+
}
341+
342+
return nil
343+
}

‎internal/lineagecontrollerwebhook/config.go‎

Lines changed: 42 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -2,49 +2,72 @@ package lineagecontrollerwebhook
22

33
import (
44
"fmt"
5+
"strings"
56

67
cozyv1alpha1 "github.com/cozystack/cozystack/api/v1alpha1"
78
helmv2 "github.com/fluxcd/helm-controller/api/v2"
89
)
910

10-
type chartRef struct {
11-
repo string
12-
chart string
13-
}
14-
1511
type appRef struct {
1612
group string
1713
kind string
1814
}
1915

2016
type runtimeConfig struct {
21-
chartAppMap map[chartRef]*cozyv1alpha1.CozystackResourceDefinition
22-
appCRDMap map[appRef]*cozyv1alpha1.CozystackResourceDefinition
17+
appCRDMap map[appRef]*cozyv1alpha1.CozystackResourceDefinition
2318
}
2419

2520
func (l *LineageControllerWebhook) initConfig() {
2621
l.initOnce.Do(func() {
2722
if l.config.Load() == nil {
2823
l.config.Store(&runtimeConfig{
29-
chartAppMap: make(map[chartRef]*cozyv1alpha1.CozystackResourceDefinition),
30-
appCRDMap: make(map[appRef]*cozyv1alpha1.CozystackResourceDefinition),
24+
appCRDMap: make(map[appRef]*cozyv1alpha1.CozystackResourceDefinition),
3125
})
3226
}
3327
})
3428
}
3529

36-
func (l *LineageControllerWebhook) Map(hr *helmv2.HelmRelease) (string, string, string, error) {
37-
cfg, ok := l.config.Load().(*runtimeConfig)
30+
// getApplicationLabel safely extracts an application label from HelmRelease
31+
func getApplicationLabel(hr *helmv2.HelmRelease, key string) (string, error) {
32+
if hr.Labels == nil {
33+
return "", fmt.Errorf("cannot map helm release %s/%s to dynamic app: labels are nil", hr.Namespace, hr.Name)
34+
}
35+
val, ok := hr.Labels[key]
3836
if !ok {
39-
return "", "", "", fmt.Errorf("failed to load chart-app mapping from config")
37+
return "", fmt.Errorf("cannot map helm release %s/%s to dynamic app: missing %s label", hr.Namespace, hr.Name, key)
38+
}
39+
return val, nil
40+
}
41+
42+
func (l *LineageControllerWebhook) Map(hr *helmv2.HelmRelease) (string, string, string, error) {
43+
// Extract application metadata from labels
44+
appKind, err := getApplicationLabel(hr, "apps.cozystack.io/application.kind")
45+
if err != nil {
46+
return "", "", "", err
4047
}
41-
if hr.Spec.Chart == nil {
42-
return "", "", "", fmt.Errorf("cannot map helm release %s/%s to dynamic app", hr.Namespace, hr.Name)
48+
49+
appGroup, err := getApplicationLabel(hr, "apps.cozystack.io/application.group")
50+
if err != nil {
51+
return "", "", "", err
4352
}
44-
s := hr.Spec.Chart.Spec
45-
val, ok := cfg.chartAppMap[chartRef{s.SourceRef.Name, s.Chart}]
46-
if !ok {
47-
return "", "", "", fmt.Errorf("cannot map helm release %s/%s to dynamic app", hr.Namespace, hr.Name)
53+
54+
appName, err := getApplicationLabel(hr, "apps.cozystack.io/application.name")
55+
if err != nil {
56+
return "", "", "", err
57+
}
58+
59+
// Construct API version from group
60+
apiVersion := fmt.Sprintf("%s/v1alpha1", appGroup)
61+
62+
// Extract prefix from HelmRelease name by removing the application name
63+
// HelmRelease name format: <prefix><application-name>
64+
prefix := strings.TrimSuffix(hr.Name, appName)
65+
66+
// Validate the derived prefix
67+
// This ensures correctness when appName appears multiple times in hr.Name
68+
if prefix+appName != hr.Name {
69+
return "", "", "", fmt.Errorf("cannot derive prefix from helm release %s/%s: name does not end with application name %s", hr.Namespace, hr.Name, appName)
4870
}
49-
return "apps.cozystack.io/v1alpha1", val.Spec.Application.Kind, val.Spec.Release.Prefix, nil
71+
72+
return apiVersion, appKind, prefix, nil
5073
}

‎internal/lineagecontrollerwebhook/controller.go‎

Lines changed: 1 addition & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -24,25 +24,15 @@ func (c *LineageControllerWebhook) Reconcile(ctx context.Context, req ctrl.Reque
2424
return ctrl.Result{}, err
2525
}
2626
cfg := &runtimeConfig{
27-
chartAppMap: make(map[chartRef]*cozyv1alpha1.CozystackResourceDefinition),
28-
appCRDMap: make(map[appRef]*cozyv1alpha1.CozystackResourceDefinition),
27+
appCRDMap: make(map[appRef]*cozyv1alpha1.CozystackResourceDefinition),
2928
}
3029
for _, crd := range crds.Items {
31-
chRef := chartRef{
32-
crd.Spec.Release.Chart.SourceRef.Name,
33-
crd.Spec.Release.Chart.Name,
34-
}
3530
appRef := appRef{
3631
"apps.cozystack.io",
3732
crd.Spec.Application.Kind,
3833
}
3934

4035
newRef := crd
41-
if _, exists := cfg.chartAppMap[chRef]; exists {
42-
l.Info("duplicate chart mapping detected; ignoring subsequent entry", "key", chRef)
43-
} else {
44-
cfg.chartAppMap[chRef] = &newRef
45-
}
4636
if _, exists := cfg.appCRDMap[appRef]; exists {
4737
l.Info("duplicate app mapping detected; ignoring subsequent entry", "key", appRef)
4838
} else {

‎packages/apps/tenant/templates/etcd.yaml‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@ metadata:
99
internal.cozystack.io/tenantmodule: "true"
1010
app.kubernetes.io/instance: {{ .Release.Name }}
1111
app.kubernetes.io/managed-by: {{ .Release.Service }}
12+
apps.cozystack.io/application.kind: Etcd
13+
apps.cozystack.io/application.group: apps.cozystack.io
14+
apps.cozystack.io/application.name: etcd
1215
spec:
1316
chart:
1417
spec:

‎packages/apps/tenant/templates/info.yaml‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@ metadata:
88
internal.cozystack.io/tenantmodule: "true"
99
app.kubernetes.io/instance: {{ .Release.Name }}
1010
app.kubernetes.io/managed-by: {{ .Release.Service }}
11+
apps.cozystack.io/application.kind: Info
12+
apps.cozystack.io/application.group: apps.cozystack.io
13+
apps.cozystack.io/application.name: info
1114
spec:
1215
chart:
1316
spec:

‎packages/apps/tenant/templates/ingress.yaml‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@ metadata:
99
internal.cozystack.io/tenantmodule: "true"
1010
app.kubernetes.io/instance: {{ .Release.Name }}
1111
app.kubernetes.io/managed-by: {{ .Release.Service }}
12+
apps.cozystack.io/application.kind: Ingress
13+
apps.cozystack.io/application.group: apps.cozystack.io
14+
apps.cozystack.io/application.name: ingress
1215
spec:
1316
chart:
1417
spec:

‎packages/apps/tenant/templates/monitoring.yaml‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@ metadata:
99
internal.cozystack.io/tenantmodule: "true"
1010
app.kubernetes.io/instance: {{ .Release.Name }}
1111
app.kubernetes.io/managed-by: {{ .Release.Service }}
12+
apps.cozystack.io/application.kind: Monitoring
13+
apps.cozystack.io/application.group: apps.cozystack.io
14+
apps.cozystack.io/application.name: monitoring
1215
spec:
1316
chart:
1417
spec:

‎packages/apps/tenant/templates/seaweedfs.yaml‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@ metadata:
99
internal.cozystack.io/tenantmodule: "true"
1010
app.kubernetes.io/instance: {{ .Release.Name }}
1111
app.kubernetes.io/managed-by: {{ .Release.Service }}
12+
apps.cozystack.io/application.kind: SeaweedFS
13+
apps.cozystack.io/application.group: apps.cozystack.io
14+
apps.cozystack.io/application.name: seaweedfs
1215
spec:
1316
chart:
1417
spec:

‎packages/core/platform/templates/apps.yaml‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,9 @@ metadata:
3636
namespace: tenant-root
3737
labels:
3838
cozystack.io/ui: "true"
39+
apps.cozystack.io/application.kind: Tenant
40+
apps.cozystack.io/application.group: apps.cozystack.io
41+
apps.cozystack.io/application.name: tenant-root
3942
spec:
4043
interval: 0s
4144
releaseName: tenant-root

‎packages/system/bootbox/templates/bootbox.yaml‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@ metadata:
55
helm.sh/resource-policy: keep
66
labels:
77
cozystack.io/ui: "true"
8+
apps.cozystack.io/application.kind: BootBox
9+
apps.cozystack.io/application.group: apps.cozystack.io
10+
apps.cozystack.io/application.name: bootbox
811
name: bootbox
912
namespace: tenant-root
1013
spec:

0 commit comments

Comments
 (0)