[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:
parent
fe7bdcf06b
commit
669bf3d2f5
12 changed files with 460 additions and 144 deletions
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
163
scripts/migrations/22
Executable 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-
|
||||
|
||||
Loading…
Add table
Add a link
Reference in a new issue