From c5fb760448b0d67ec8f4853e0851d79d98d02753 Mon Sep 17 00:00:00 2001 From: Gianluca Mardente Date: Sat, 7 Feb 2026 16:05:42 +0100 Subject: [PATCH] (bug) track ConfigMap with agent patches --- controllers/classifier_deployer.go | 22 ++------ controllers/classifier_predicates.go | 21 ++------ controllers/cluster_controller.go | 9 ++++ controllers/patch_tracker.go | 67 +++++++++++++++++++++++ controllers/sveltoscluster_controller.go | 69 ++++++++++++++++++++++++ 5 files changed, 153 insertions(+), 35 deletions(-) create mode 100644 controllers/patch_tracker.go diff --git a/controllers/classifier_deployer.go b/controllers/classifier_deployer.go index ebae8ef..ee65fb3 100644 --- a/controllers/classifier_deployer.go +++ b/controllers/classifier_deployer.go @@ -2187,29 +2187,17 @@ func getPerClusterPatches(ctx context.Context, c client.Client, return nil, nil // Annotation present but empty, or not present } - var configMapNamespace, configMapName string - // get configMap namespace, name from annotation - parts := strings.Split(configMapRef, "/") - const two = 2 - // If only one part is present, assume it is the ConfigMap name and use the cluster's namespace. - if len(parts) == 1 { - configMapNamespace = clusterNamespace - configMapName = parts[0] - } else if len(parts) == two { - configMapNamespace = parts[0] - configMapName = parts[1] - - // If the namespace part is empty (e.g., "/my-config"), use the cluster's namespace. - if configMapNamespace == "" { - configMapNamespace = clusterNamespace - } - } else { + cmInfo, err := getConfigMapNamespacedName(configMapRef, clusterNamespace) + if err != nil { logger.Error(nil, "invalid configMap reference format in annotation", "annotation", annotationKey, "value", configMapRef) return nil, fmt.Errorf("invalid configMap reference format: %s. Expected / or just ", configMapRef) } + configMapNamespace := cmInfo.Namespace + configMapName := cmInfo.Name + patches, err := getSveltosApplierPatchesNew(ctx, c, configMapNamespace, configMapName, logger) if err != nil { logger.Error(err, "failed to get ConfigMap with patches", diff --git a/controllers/classifier_predicates.go b/controllers/classifier_predicates.go index e8a7913..31ad998 100644 --- a/controllers/classifier_predicates.go +++ b/controllers/classifier_predicates.go @@ -21,6 +21,7 @@ import ( "github.com/go-logr/logr" corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/types" "sigs.k8s.io/controller-runtime/pkg/event" "sigs.k8s.io/controller-runtime/pkg/predicate" @@ -233,28 +234,12 @@ func ConfigMapPredicates(logger logr.Logger) predicate.Funcs { } func isConfigMapWithPatches(cm *corev1.ConfigMap) bool { - isPerClusterPatch := false - annotations := cm.Annotations - if annotations != nil { - _, ok := annotations[sveltosAgentOverrideAnnotation] - if ok { - isPerClusterPatch = true - } - _, ok = annotations[sveltosApplierOverrideAnnotation] - if ok { - isPerClusterPatch = true - } - } - - if isPerClusterPatch { - return true - } - if cm.Namespace == projectsveltos && cm.Name == getSveltosAgentConfigMap() { return true } - return false + tracker := getPatchTracker() + return tracker.IsPatchConfigMap(types.NamespacedName{Namespace: cm.Namespace, Name: cm.Name}) } // ClassifierReportPredicate predicates for ClassifierReport. ClassifierReconciler watches ClassifierReport events diff --git a/controllers/cluster_controller.go b/controllers/cluster_controller.go index 42d03ee..3070c17 100644 --- a/controllers/cluster_controller.go +++ b/controllers/cluster_controller.go @@ -20,6 +20,7 @@ import ( "context" "github.com/pkg/errors" + corev1 "k8s.io/api/core/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/runtime" clusterv1 "sigs.k8s.io/cluster-api/api/core/v1beta2" @@ -65,6 +66,14 @@ func (r *ClusterReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ct libsveltosv1beta1.ClusterTypeCapi, logger) } + clusterRef := &corev1.ObjectReference{ + Kind: clusterv1.ClusterKind, + APIVersion: clusterv1.GroupVersion.String(), + Namespace: cluster.Namespace, + Name: cluster.Name, + } + trackPatchConfigMaps(clusterRef, cluster.Annotations) + return ctrl.Result{}, nil } diff --git a/controllers/patch_tracker.go b/controllers/patch_tracker.go new file mode 100644 index 0000000..3d694da --- /dev/null +++ b/controllers/patch_tracker.go @@ -0,0 +1,67 @@ +/* +Copyright 2026. projectsveltos.io. All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package controllers + +import ( + "sync" + + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/types" + + libsveltosset "github.com/projectsveltos/libsveltos/lib/set" +) + +type PatchTracker struct { + // Maps ConfigMap NamespacedName to a set of Clusters using it + trackedConfigMaps map[types.NamespacedName]*libsveltosset.Set + mux sync.RWMutex +} + +var ( + instance *PatchTracker + once sync.Once +) + +// GetPatchTracker returns the singleton instance +func getPatchTracker() *PatchTracker { + once.Do(func() { + instance = &PatchTracker{ + trackedConfigMaps: make(map[types.NamespacedName]*libsveltosset.Set), + } + }) + return instance +} + +// TrackConfigMap registers a ConfigMap and the cluster that requested it +func (p *PatchTracker) TrackConfigMap(configMap types.NamespacedName, clusterRef *corev1.ObjectReference) { + p.mux.Lock() + defer p.mux.Unlock() + + if _, exists := p.trackedConfigMaps[configMap]; !exists { + p.trackedConfigMaps[configMap] = &libsveltosset.Set{} + } + p.trackedConfigMaps[configMap].Insert(clusterRef) +} + +// IsPatchConfigMap checks if a specific NamespacedName is known to contain patches +func (p *PatchTracker) IsPatchConfigMap(configMap types.NamespacedName) bool { + p.mux.RLock() + defer p.mux.RUnlock() + + _, exists := p.trackedConfigMaps[configMap] + return exists +} diff --git a/controllers/sveltoscluster_controller.go b/controllers/sveltoscluster_controller.go index 6536ca6..84a12c0 100644 --- a/controllers/sveltoscluster_controller.go +++ b/controllers/sveltoscluster_controller.go @@ -19,11 +19,14 @@ package controllers import ( "context" "fmt" + "strings" "github.com/go-logr/logr" "github.com/pkg/errors" + corev1 "k8s.io/api/core/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/reconcile" @@ -66,6 +69,14 @@ func (r *SveltosClusterReconciler) Reconcile(ctx context.Context, req ctrl.Reque libsveltosv1beta1.ClusterTypeSveltos, logger) } + clusterRef := &corev1.ObjectReference{ + Kind: libsveltosv1beta1.SveltosClusterKind, + APIVersion: libsveltosv1beta1.GroupVersion.String(), + Namespace: sveltosCluster.Namespace, + Name: sveltosCluster.Name, + } + trackPatchConfigMaps(clusterRef, sveltosCluster.Annotations) + return ctrl.Result{}, nil } @@ -102,3 +113,61 @@ func cleanClusterStaleResources(ctx context.Context, c client.Client, return reconcile.Result{}, nil } + +func trackPatchConfigMaps(clusterRef *corev1.ObjectReference, clusterAnnotations map[string]string) { + if clusterAnnotations == nil { + return + } + + configMapRef, ok := clusterAnnotations[sveltosAgentOverrideAnnotation] + if ok { + tracker := getPatchTracker() + + cmInfo, err := getConfigMapNamespacedName(configMapRef, clusterRef.Namespace) + if err == nil { + tracker.TrackConfigMap(cmInfo, clusterRef) + } + } + + configMapRef, ok = clusterAnnotations[sveltosApplierOverrideAnnotation] + if ok { + tracker := getPatchTracker() + + cmInfo, err := getConfigMapNamespacedName(configMapRef, clusterRef.Namespace) + if err == nil { + tracker.TrackConfigMap(cmInfo, clusterRef) + } + } +} + +// getConfigMapNamespacedName parses a "namespace/name" or "name" string. +// Returns an error if the format is invalid (e.g., empty or too many slashes). +func getConfigMapNamespacedName(ref, defaultNamespace string) (types.NamespacedName, error) { + if ref == "" { + return types.NamespacedName{}, fmt.Errorf("annotation value is empty") + } + + parts := strings.Split(ref, "/") + + const two = 2 // namespace and name + switch len(parts) { + case 1: + // Case: "my-configmap" + return types.NamespacedName{ + Namespace: defaultNamespace, + Name: parts[0], + }, nil + case two: + // Case: "my-namespace/my-configmap" + if parts[0] == "" || parts[1] == "" { + return types.NamespacedName{}, fmt.Errorf("invalid format '%s': namespace or name is empty", ref) + } + return types.NamespacedName{ + Namespace: parts[0], + Name: parts[1], + }, nil + default: + // Case: "too/many/slashes/here" or other malformed strings + return types.NamespacedName{}, fmt.Errorf("invalid format '%s': expected 'name' or 'namespace/name'", ref) + } +}