From 0e0dabdd0812f2ca1ece63344044755b086092e6 Mon Sep 17 00:00:00 2001 From: Andrei Kvapil Date: Fri, 6 Jun 2025 15:41:51 +0200 Subject: [PATCH] [platform] Fix deps for paas-hosted bundle Signed-off-by: Andrei Kvapil --- cmd/cozystack-controller/main.go | 10 +- internal/controller/system_helm_reconciler.go | 139 ++++++++++++++++++ 2 files changed, 148 insertions(+), 1 deletion(-) create mode 100644 internal/controller/system_helm_reconciler.go diff --git a/cmd/cozystack-controller/main.go b/cmd/cozystack-controller/main.go index a5624cd3..3ffc3027 100644 --- a/cmd/cozystack-controller/main.go +++ b/cmd/cozystack-controller/main.go @@ -194,7 +194,15 @@ func main() { Client: mgr.GetClient(), Scheme: mgr.GetScheme(), }).SetupWithManager(mgr); err != nil { - setupLog.Error(err, "unable to create controller", "controller", "Workload") + setupLog.Error(err, "unable to create controller", "controller", "TenantHelmReconciler") + os.Exit(1) + } + + if err = (&controller.CozystackConfigReconciler{ + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + }).SetupWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create controller", "controller", "CozystackConfigReconciler") os.Exit(1) } diff --git a/internal/controller/system_helm_reconciler.go b/internal/controller/system_helm_reconciler.go new file mode 100644 index 00000000..83117c42 --- /dev/null +++ b/internal/controller/system_helm_reconciler.go @@ -0,0 +1,139 @@ +package controller + +import ( + "context" + "crypto/sha256" + "encoding/hex" + "fmt" + "sort" + "time" + + helmv2 "github.com/fluxcd/helm-controller/api/v2" + corev1 "k8s.io/api/core/v1" + kerrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/runtime" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/event" + "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/predicate" +) + +type CozystackConfigReconciler struct { + client.Client + Scheme *runtime.Scheme +} + +var configMapNames = []string{"cozystack", "cozystack-branding", "cozystack-scheduling"} + +const configMapNamespace = "cozy-system" +const digestAnnotation = "cozystack.io/cozy-config-digest" +const forceReconcileKey = "reconcile.fluxcd.io/forceAt" +const requestedAt = "reconcile.fluxcd.io/requestedAt" + +func (r *CozystackConfigReconciler) Reconcile(ctx context.Context, _ ctrl.Request) (ctrl.Result, error) { + log := log.FromContext(ctx) + + digest, err := r.computeDigest(ctx) + if err != nil { + log.Error(err, "failed to compute config digest") + return ctrl.Result{}, nil + } + + var helmList helmv2.HelmReleaseList + if err := r.List(ctx, &helmList); err != nil { + return ctrl.Result{}, fmt.Errorf("failed to list HelmReleases: %w", err) + } + + now := time.Now().Format(time.RFC3339Nano) + updated := 0 + + for _, hr := range helmList.Items { + isSystemApp := hr.Labels["cozystack.io/system-app"] == "true" + isTenantRoot := hr.Namespace == "tenant-root" && hr.Name == "tenant-root" + if !isSystemApp && !isTenantRoot { + continue + } + + if hr.Annotations == nil { + hr.Annotations = map[string]string{} + } + + if hr.Annotations[digestAnnotation] == digest { + continue + } + + patch := client.MergeFrom(hr.DeepCopy()) + hr.Annotations[digestAnnotation] = digest + hr.Annotations[forceReconcileKey] = now + hr.Annotations[requestedAt] = now + + if err := r.Patch(ctx, &hr, patch); err != nil { + log.Error(err, "failed to patch HelmRelease", "name", hr.Name, "namespace", hr.Namespace) + continue + } + updated++ + log.Info("patched HelmRelease with new config digest", "name", hr.Name, "namespace", hr.Namespace) + } + + log.Info("finished reconciliation", "updatedHelmReleases", updated) + return ctrl.Result{}, nil +} + +func (r *CozystackConfigReconciler) computeDigest(ctx context.Context) (string, error) { + hash := sha256.New() + + for _, name := range configMapNames { + var cm corev1.ConfigMap + err := r.Get(ctx, client.ObjectKey{Namespace: configMapNamespace, Name: name}, &cm) + if err != nil { + if kerrors.IsNotFound(err) { + continue // ignore missing + } + return "", err + } + + // Sort keys for consistent hashing + var keys []string + for k := range cm.Data { + keys = append(keys, k) + } + sort.Strings(keys) + + for _, k := range keys { + v := cm.Data[k] + fmt.Fprintf(hash, "%s:%s=%s\n", name, k, v) + } + } + + return hex.EncodeToString(hash.Sum(nil)), nil +} + +func (r *CozystackConfigReconciler) SetupWithManager(mgr ctrl.Manager) error { + return ctrl.NewControllerManagedBy(mgr). + WithEventFilter(predicate.Funcs{ + UpdateFunc: func(e event.UpdateEvent) bool { + cm, ok := e.ObjectNew.(*corev1.ConfigMap) + return ok && cm.Namespace == configMapNamespace && contains(configMapNames, cm.Name) + }, + CreateFunc: func(e event.CreateEvent) bool { + cm, ok := e.Object.(*corev1.ConfigMap) + return ok && cm.Namespace == configMapNamespace && contains(configMapNames, cm.Name) + }, + DeleteFunc: func(e event.DeleteEvent) bool { + cm, ok := e.Object.(*corev1.ConfigMap) + return ok && cm.Namespace == configMapNamespace && contains(configMapNames, cm.Name) + }, + }). + For(&corev1.ConfigMap{}). + Complete(r) +} + +func contains(slice []string, val string) bool { + for _, s := range slice { + if s == val { + return true + } + } + return false +}