From dcdf397700730e6319f13a95b14352fdc168e222 Mon Sep 17 00:00:00 2001 From: Gianluca Mardente Date: Thu, 27 Aug 2026 08:57:24 +0200 Subject: [PATCH] feat: Relay --watch-namespaces to sveltos-agent sveltos-agent can now restrict its watches to declared namespaces in agentless mode. A managed cluster can be annotated with `agent.projectsveltos.io/watch-namespaces` (comma-separated namespace list on the Cluster/SveltosCluster). addon-controller already relays this to drift-detection-manager. This PR does the same for sveltos-agent. - New `getAgentWatchNamespaces`: reads `agent.projectsveltos.io/watch-namespaces` off the Cluster/SveltosCluster - Threaded through `prepareSveltosAgentYAML` into the `--watch-namespaces=` placeholder already present in both the in-cluster and agentless sveltos-agent manifests. - Folded into `getCurrentHash` alongside the existing `sveltosAgentPatches`/`sveltosApplierPatches` hashing. Required: the existing Cluster/SveltosCluster predicates already requeue the Classifier on any annotation change for free, but redeploy is gated by a hash comparison (`isConfigSame`). Without folding the value in, a changed annotation would requeue and then silently redeploy nothing. --- Makefile | 2 +- config/default/manager_auth_proxy_patch.yaml | 2 +- config/default/manager_image_patch.yaml | 4 +- controllers/classifier_deployer.go | 101 ++++++++++++++++-- controllers/classifier_deployer_test.go | 88 ++++++++++++++- controllers/export_test.go | 1 + .../mgmtcluster_classifier_controller.go | 6 +- controllers/mgmtcluster_classifier_test.go | 19 ++-- controllers/mgmtcluster_classifier_utils.go | 51 +++++---- go.mod | 2 +- go.sum | 4 +- manifest/deployment-agentless.yaml | 6 +- manifest/deployment-shard.yaml | 6 +- manifest/manifest.yaml | 6 +- pkg/agent/sveltos-agent-in-mgmt-cluster.go | 5 +- pkg/agent/sveltos-agent-in-mgmt-cluster.yaml | 5 +- pkg/agent/sveltos-agent.go | 5 +- pkg/agent/sveltos-agent.yaml | 5 +- 18 files changed, 246 insertions(+), 72 deletions(-) diff --git a/Makefile b/Makefile index d829a4e..d3d2598 100644 --- a/Makefile +++ b/Makefile @@ -46,7 +46,7 @@ ARCH ?= $(shell go env GOARCH) OS ?= $(shell uname -s | tr A-Z a-z) K8S_LATEST_VER ?= $(shell curl -s https://storage.googleapis.com/kubernetes-release/release/stable.txt) export CONTROLLER_IMG ?= $(REGISTRY)/$(IMAGE_NAME) -TAG ?= v1.14.0 +TAG ?= main ## Tool Binaries CONTROLLER_GEN := $(TOOLS_BIN_DIR)/controller-gen diff --git a/config/default/manager_auth_proxy_patch.yaml b/config/default/manager_auth_proxy_patch.yaml index 2cf81a0..9839d83 100644 --- a/config/default/manager_auth_proxy_patch.yaml +++ b/config/default/manager_auth_proxy_patch.yaml @@ -16,7 +16,7 @@ spec: - "--shard-key=" - --capi-onboard-annotation= - "--v=5" - - "--version=v1.14.0" + - "--version=main" - "--registry=" - "--agent-in-mgmt-cluster=false" env: diff --git a/config/default/manager_image_patch.yaml b/config/default/manager_image_patch.yaml index 50140bc..d94ebc6 100644 --- a/config/default/manager_image_patch.yaml +++ b/config/default/manager_image_patch.yaml @@ -7,9 +7,9 @@ spec: template: spec: initContainers: - - image: docker.io/projectsveltos/classifier:v1.14.0 + - image: docker.io/projectsveltos/classifier:main name: migrate containers: # Change the value of image field below to your controller image URL - - image: docker.io/projectsveltos/classifier:v1.14.0 + - image: docker.io/projectsveltos/classifier:main name: manager diff --git a/controllers/classifier_deployer.go b/controllers/classifier_deployer.go index 56b3742..299b06c 100644 --- a/controllers/classifier_deployer.go +++ b/controllers/classifier_deployer.go @@ -121,6 +121,13 @@ const ( // * **Strategic Merge Patch** // * **JSON Patch (RFC6902)** sveltosApplierOverrideAnnotation = "sveltosapplier.projectsveltos.io/config-override-ref" + + // This optional annotation restricts sveltos-agent's namespace-scoped watch mode (agentless + // mode only, requires a Sveltos Enterprise license granting NamespaceScopedAgents -- see + // sveltos-agent's own VerifyNamespaceScopeLicense) to the comma-separated namespaces it + // names. Same annotation key addon-controller's drift-detection-manager deploy path already + // uses, so one annotation on the Cluster/SveltosCluster controls both agents. + agentWatchNamespacesAnnotation = "agent.projectsveltos.io/watch-namespaces" ) func getSveltosAgentNamespace(sveltosNamespace string) string { @@ -584,10 +591,15 @@ func deploySveltosAgentWithKubeconfigInCluster(ctx context.Context, c client.Cli return err } + watchNamespaces, err := getAgentWatchNamespaces(ctx, c, clusterNamespace, clusterName, clusterType, logger) + if err != nil { + return err + } + logger.V(logs.LogDebug).Info("Deploying sveltos agent") // Deploy SveltosAgent err = deploySveltosAgentInManagedCluster(ctx, remoteRestConfig, clusterNamespace, clusterName, applicant, - "send-reports", clusterType, patches, false, logger) + "send-reports", clusterType, patches, false, watchNamespaces, logger) if err != nil { return err } @@ -989,6 +1001,23 @@ func (r *ClassifierReconciler) getCurrentHash(ctx context.Context, classifierSco currentHash = h.Sum(nil) } + watchNamespaces, err := getAgentWatchNamespaces(ctx, r.Client, cluster.Namespace, cluster.Name, + clusterproxy.GetClusterType(cluster), logger) + if err != nil { + return nil, err + } + if len(watchNamespaces) > 0 { + // Without this, a change to agentWatchNamespacesAnnotation on the Cluster/SveltosCluster + // still requeues the Classifier (predicates.ClusterPredicate/SveltosClusterPredicates + // already diff annotations wholesale), but processClassifier's isConfigSame check would + // find this hash unchanged and skip the redeploy that would actually apply the new value + // -- see getAgentWatchNamespaces' doc comment. + h := sha256.New() + h.Write(currentHash) + h.Write([]byte(strings.Join(watchNamespaces, ","))) + currentHash = h.Sum(nil) + } + var kubeconfig []byte if r.ClassifierReportMode == AgentSendReportsNoGateway { h := sha256.New() @@ -1566,7 +1595,7 @@ func deployReloaderReportCRD(ctx context.Context, clusterNamespace, clusterName, } func prepareSveltosAgentYAML(agentYAML, clusterNamespace, clusterName, mode string, - clusterType libsveltosv1beta1.ClusterType) string { + clusterType libsveltosv1beta1.ClusterType, watchNamespaces []string) string { if mode != "do-not-send-reports" { agentYAML = strings.ReplaceAll(agentYAML, "do-not-send-reports", "send-reports") @@ -1575,6 +1604,7 @@ func prepareSveltosAgentYAML(agentYAML, clusterNamespace, clusterName, mode stri agentYAML = strings.ReplaceAll(agentYAML, "cluster-namespace=", fmt.Sprintf("cluster-namespace=%s", clusterNamespace)) agentYAML = strings.ReplaceAll(agentYAML, "cluster-name=", fmt.Sprintf("cluster-name=%s", clusterName)) agentYAML = strings.ReplaceAll(agentYAML, "cluster-type=", fmt.Sprintf("cluster-type=%s", clusterType)) + agentYAML = strings.ReplaceAll(agentYAML, "watch-namespaces=", fmt.Sprintf("watch-namespaces=%s", strings.Join(watchNamespaces, ","))) agentYAML = strings.ReplaceAll(agentYAML, "v=5", "v=0") if getSveltosAgentEnableNATS() { @@ -1645,10 +1675,15 @@ func deploySveltosAgent(ctx context.Context, c client.Client, clusterNamespace, return err } + watchNamespaces, err := getAgentWatchNamespaces(ctx, c, clusterNamespace, clusterName, clusterType, logger) + if err != nil { + return err + } + // Deploy SveltosAgent if isPullMode { err = deploySveltosAgentInManagedCluster(ctx, nil, clusterNamespace, - clusterName, classifierName, "do-not-send-reports", clusterType, patches, true, logger) + clusterName, classifierName, "do-not-send-reports", clusterType, patches, true, watchNamespaces, logger) if err != nil { return err } @@ -1656,7 +1691,7 @@ func deploySveltosAgent(ctx context.Context, c client.Client, clusterNamespace, // Use management cluster restConfig restConfig := getManagementClusterConfig() return deploySveltosAgentInManagementCluster(ctx, restConfig, c, clusterNamespace, clusterName, - classifierName, "do-not-send-reports", clusterType, patches, logger) + classifierName, "do-not-send-reports", clusterType, patches, watchNamespaces, logger) } else { // Use managed cluster restConfig remoteRestConfig, err := clustercache.GetManager().GetKubernetesRestConfig(ctx, c, clusterNamespace, clusterName, @@ -1671,7 +1706,7 @@ func deploySveltosAgent(ctx context.Context, c client.Client, clusterNamespace, return err } err = deploySveltosAgentInManagedCluster(ctx, remoteRestConfig, clusterNamespace, - clusterName, classifierName, "do-not-send-reports", clusterType, patches, false, logger) + clusterName, classifierName, "do-not-send-reports", clusterType, patches, false, watchNamespaces, logger) if err != nil { return err } @@ -1686,12 +1721,12 @@ func replaceRegistry(agentYAML, registry string) string { func deploySveltosAgentInManagedCluster(ctx context.Context, remoteRestConfig *rest.Config, clusterNamespace, clusterName, classifierName, mode string, clusterType libsveltosv1beta1.ClusterType, - patches []libsveltosv1beta1.Patch, isPullMode bool, logger logr.Logger) error { + patches []libsveltosv1beta1.Patch, isPullMode bool, watchNamespaces []string, logger logr.Logger) error { logger.V(logs.LogDebug).Info("deploy sveltos-agent in the managed cluster") agentYAML := string(agent.GetSveltosAgentYAML()) - agentYAML = prepareSveltosAgentYAML(agentYAML, clusterNamespace, clusterName, mode, clusterType) + agentYAML = prepareSveltosAgentYAML(agentYAML, clusterNamespace, clusterName, mode, clusterType, watchNamespaces) return deploySveltosAgentResources(ctx, clusterNamespace, clusterName, classifierName, remoteRestConfig, agentYAML, nil, patches, isPullMode, logger) @@ -1717,12 +1752,12 @@ func upgradeSveltosApplierInManagedCluster(ctx context.Context, clusterNamespace func deploySveltosAgentInManagementCluster(ctx context.Context, restConfig *rest.Config, c client.Client, clusterNamespace, clusterName, classifierName, mode string, clusterType libsveltosv1beta1.ClusterType, - patches []libsveltosv1beta1.Patch, logger logr.Logger) error { + patches []libsveltosv1beta1.Patch, watchNamespaces []string, logger logr.Logger) error { logger.V(logs.LogDebug).Info("deploy sveltos-agent in the management cluster") agentYAML := string(agent.GetSveltosAgentInMgmtClusterYAML()) - agentYAML = prepareSveltosAgentYAML(agentYAML, clusterNamespace, clusterName, mode, clusterType) + agentYAML = prepareSveltosAgentYAML(agentYAML, clusterNamespace, clusterName, mode, clusterType, watchNamespaces) // Following labels are added on the objects representing the drift-detection-manager // for this cluster. @@ -2021,9 +2056,11 @@ func removeSveltosAgentFromManagementCluster(ctx context.Context, clusterNamespace, clusterName string, clusterType libsveltosv1beta1.ClusterType, logger logr.Logger) error { - // Get YAML containing sveltos-agent resources + // Get YAML containing sveltos-agent resources. Only used below to determine the resource + // identity to delete, not actually deployed, so the substituted arg values (watchNamespaces + // included) don't matter here -- nil is fine. agentYAML := string(agent.GetSveltosAgentInMgmtClusterYAML()) - agentYAML = prepareSveltosAgentYAML(agentYAML, clusterNamespace, clusterName, "", clusterType) + agentYAML = prepareSveltosAgentYAML(agentYAML, clusterNamespace, clusterName, "", clusterType, nil) // Classifier deploys sveltos-agent resources for each cluster. lbls := getSveltosAgentLabels(clusterNamespace, clusterName, clusterType) @@ -2272,6 +2309,48 @@ func getSveltosApplierPatches(ctx context.Context, c client.Client, return getSveltosApplierPatchesOld(ctx, c, logger) } +// getAgentWatchNamespaces reads agentWatchNamespacesAnnotation off the Cluster/SveltosCluster +// instance and returns the comma-separated namespace list it names, split and trimmed. Returns +// nil, nil (not an error) when the Cluster, the annotation, or its value is missing, same +// contract as getPerClusterPatches. sveltos-agent itself only honors this outside managed-cluster +// mode (see resolveScopedNamespaces in its own main.go), so callers here don't need to +// special-case agentless vs. managed-cluster mode: passing the value through unconditionally is +// harmless when sveltos-agent is deployed in the managed cluster. +func getAgentWatchNamespaces(ctx context.Context, c client.Client, + clusterNamespace, clusterName string, clusterType libsveltosv1beta1.ClusterType, + logger logr.Logger) ([]string, error) { + + cluster, err := clusterproxy.GetCluster(ctx, c, clusterNamespace, clusterName, clusterType) + if err != nil { + if apierrors.IsNotFound(err) { + return nil, nil + } + return nil, err + } + + annos := cluster.GetAnnotations() + if annos == nil { + return nil, nil + } + + value, ok := annos[agentWatchNamespacesAnnotation] + if !ok || value == "" { + return nil, nil + } + + var namespaces []string + for _, ns := range strings.Split(value, ",") { + ns = strings.TrimSpace(ns) + if ns != "" { + namespaces = append(namespaces, ns) + } + } + + logger.V(logs.LogDebug).Info(fmt.Sprintf("got watch-namespaces %v from annotation %s", + namespaces, agentWatchNamespacesAnnotation)) + return namespaces, nil +} + func addTemplateSpecLabels(u *unstructured.Unstructured, lbls map[string]string) (*unstructured.Unstructured, error) { var deployment appsv1.Deployment err := runtime.DefaultUnstructuredConverter.FromUnstructured(u.UnstructuredContent(), &deployment) diff --git a/controllers/classifier_deployer_test.go b/controllers/classifier_deployer_test.go index bed2883..0d179f1 100644 --- a/controllers/classifier_deployer_test.go +++ b/controllers/classifier_deployer_test.go @@ -48,7 +48,10 @@ import ( ) const ( - classifierCRDName = "classifiers.lib.projectsveltos.io" + classifierCRDName = "classifiers.lib.projectsveltos.io" + agentWatchNamespacesAnnotKey = "agent.projectsveltos.io/watch-namespaces" + watchNamespaceNs1 = "ns1" + watchNamespaceNs2 = "ns2" ) var _ = Describe("Classifier Deployer", func() { @@ -476,16 +479,22 @@ var _ = Describe("Classifier Deployer", func() { }) It("deploySveltosAgent deploys sveltos agent", func() { + watchNamespaces := []string{watchNamespaceNs1, watchNamespaceNs2} Expect(controllers.DeploySveltosAgentInManagedCluster(ctx, testEnv.Config, randomString(), randomString(), - randomString(), "do-not-send-reports", libsveltosv1beta1.ClusterTypeCapi, nil, false, logger)).To(Succeed()) + randomString(), "do-not-send-reports", libsveltosv1beta1.ClusterTypeCapi, nil, false, + watchNamespaces, logger)).To(Succeed()) // Eventual loop so testEnv Cache is synced + currentSveltosAgent := &appsv1.Deployment{} Eventually(func() error { - currentSveltosAgent := &appsv1.Deployment{} return testEnv.Get(context.TODO(), types.NamespacedName{Namespace: sveltosNamespace, Name: "sveltos-agent-manager"}, currentSveltosAgent) }, timeout, pollingInterval).Should(BeNil()) + + Expect(currentSveltosAgent.Spec.Template.Spec.Containers).ToNot(BeEmpty()) + Expect(currentSveltosAgent.Spec.Template.Spec.Containers[0].Args).To( + ContainElement("--watch-namespaces=ns1,ns2")) }) It("createAccessRequest creates AccessRequest instance", func() { @@ -607,14 +616,17 @@ var _ = Describe("Classifier Deployer", func() { _, err := keymanager.GetKeyManagerInstance(context.TODO(), testEnv.Client) Expect(err).To(BeNil()) + watchNamespaces := []string{watchNamespaceNs1, watchNamespaceNs2} Expect(controllers.DeploySveltosAgentInManagementCluster(context.TODO(), testEnv.Config, - testEnv.Client, clusterNamespace, clusterName, randomString(), "", clusterType, nil, logger)).To(Succeed()) + testEnv.Client, clusterNamespace, clusterName, randomString(), "", clusterType, nil, + watchNamespaces, logger)).To(Succeed()) expectedLabels := controllers.GetSveltosAgentLabels(clusterNamespace, clusterName, clusterType) listOptions := []client.ListOption{ client.InNamespace(controllers.GetSveltosAgentNamespace(sveltosNamespace)), } + var matchingDeployment *appsv1.Deployment Eventually(func() bool { deployments := &appsv1.DeploymentList{} err := testEnv.List(context.TODO(), deployments, listOptions...) @@ -629,12 +641,17 @@ var _ = Describe("Classifier Deployer", func() { for i := range deployments.Items { d := &deployments.Items[i] if verifyLabels(d.Labels, expectedLabels) { + matchingDeployment = d return true } } return false }, timeout, pollingInterval).Should(BeTrue()) + Expect(matchingDeployment.Spec.Template.Spec.Containers).ToNot(BeEmpty()) + Expect(matchingDeployment.Spec.Template.Spec.Containers[0].Args).To( + ContainElement("--watch-namespaces=ns1,ns2")) + Expect(sveltos_upgrade.StoreSveltosAgentVersion(context.TODO(), testEnv.Client, sveltosNamespace, "v1.0.0", clusterNamespace, clusterName, clusterType, true, logger)).To(Succeed()) @@ -930,6 +947,69 @@ metadata: verifyPatches(patches) }) + It("getAgentWatchNamespaces reads and trims the comma-separated namespace list from the annotation", func() { + sveltosCluster := &libsveltosv1beta1.SveltosCluster{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: randomString(), + Name: upstreamClusterNamePrefix + randomString(), + Annotations: map[string]string{ + agentWatchNamespacesAnnotKey: " foo ,bar,, baz", + }, + }, + } + + c := fake.NewClientBuilder().WithScheme(scheme).WithObjects(sveltosCluster).Build() + + namespaces, err := controllers.GetAgentWatchNamespaces(context.TODO(), c, sveltosCluster.Namespace, + sveltosCluster.Name, libsveltosv1beta1.ClusterTypeSveltos, logger) + Expect(err).To(BeNil()) + Expect(namespaces).To(Equal([]string{"foo", "bar", "baz"})) + }) + + It("getAgentWatchNamespaces returns nil, not an error, when the annotation is absent", func() { + sveltosCluster := &libsveltosv1beta1.SveltosCluster{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: randomString(), + Name: upstreamClusterNamePrefix + randomString(), + }, + } + + c := fake.NewClientBuilder().WithScheme(scheme).WithObjects(sveltosCluster).Build() + + namespaces, err := controllers.GetAgentWatchNamespaces(context.TODO(), c, sveltosCluster.Namespace, + sveltosCluster.Name, libsveltosv1beta1.ClusterTypeSveltos, logger) + Expect(err).To(BeNil()) + Expect(namespaces).To(BeNil()) + }) + + It("getAgentWatchNamespaces returns nil, not an error, when the annotation value is empty", func() { + sveltosCluster := &libsveltosv1beta1.SveltosCluster{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: randomString(), + Name: upstreamClusterNamePrefix + randomString(), + Annotations: map[string]string{ + agentWatchNamespacesAnnotKey: "", + }, + }, + } + + c := fake.NewClientBuilder().WithScheme(scheme).WithObjects(sveltosCluster).Build() + + namespaces, err := controllers.GetAgentWatchNamespaces(context.TODO(), c, sveltosCluster.Namespace, + sveltosCluster.Name, libsveltosv1beta1.ClusterTypeSveltos, logger) + Expect(err).To(BeNil()) + Expect(namespaces).To(BeNil()) + }) + + It("getAgentWatchNamespaces returns nil, not an error, when the Cluster does not exist", func() { + c := fake.NewClientBuilder().WithScheme(scheme).Build() + + namespaces, err := controllers.GetAgentWatchNamespaces(context.TODO(), c, randomString(), + upstreamClusterNamePrefix+randomString(), libsveltosv1beta1.ClusterTypeSveltos, logger) + Expect(err).To(BeNil()) + Expect(namespaces).To(BeNil()) + }) + It("getSveltosApplierPatches reads post render patches from per cluster ConfigMap", func() { configMapNamespace := randomString() configMapName := randomString() diff --git a/controllers/export_test.go b/controllers/export_test.go index 89f3302..220709a 100644 --- a/controllers/export_test.go +++ b/controllers/export_test.go @@ -63,6 +63,7 @@ var ( GetSveltosAgentLabels = getSveltosAgentLabels GetSveltosAgentNamespace = getSveltosAgentNamespace GetSveltosAgentPatches = getSveltosAgentPatches + GetAgentWatchNamespaces = getAgentWatchNamespaces GetSveltosApplierPatches = getSveltosApplierPatches CreateAccessRequest = createAccessRequest diff --git a/controllers/mgmtcluster_classifier_controller.go b/controllers/mgmtcluster_classifier_controller.go index 8fa4a53..e32600f 100644 --- a/controllers/mgmtcluster_classifier_controller.go +++ b/controllers/mgmtcluster_classifier_controller.go @@ -141,7 +141,8 @@ func (r *ManagementClusterClassifierReconciler) reconcileNormal( continue } - if err := ensureMgmtClassifierReport(ctx, r.Client, mcc.Name, ref.Namespace, ref.Name, clusterType); err != nil { + report, err := ensureMgmtClassifierReport(ctx, r.Client, mcc.Name, ref.Namespace, ref.Name, clusterType) + if err != nil { logger.V(logs.LogInfo).Error(err, fmt.Sprintf("failed to ensure report for cluster %s", key)) continue } @@ -156,8 +157,7 @@ func (r *ManagementClusterClassifierReconciler) reconcileNormal( conflictCount++ } - if err := updateMgmtClassifierReportStatus(ctx, r.Client, mcc.Name, ref.Namespace, ref.Name, - clusterType, managed, unmanaged); err != nil { + if err := updateMgmtClassifierReportStatus(ctx, r.Client, report, managed, unmanaged); err != nil { logger.V(logs.LogInfo).Error(err, fmt.Sprintf("failed to update report status for cluster %s", key)) } diff --git a/controllers/mgmtcluster_classifier_test.go b/controllers/mgmtcluster_classifier_test.go index b4e7fcf..aa15171 100644 --- a/controllers/mgmtcluster_classifier_test.go +++ b/controllers/mgmtcluster_classifier_test.go @@ -201,18 +201,21 @@ end WithStatusSubresource(&libsveltosv1beta1.ManagementClusterClassifierReport{}). Build() - err := controllers.EnsureMgmtClassifierReport(context.TODO(), fakeClient, + report, err := controllers.EnsureMgmtClassifierReport(context.TODO(), fakeClient, classifierName, ns, clusterName, clusterType) Expect(err).ToNot(HaveOccurred()) + Expect(report.Spec.ClassifierName).To(Equal(classifierName)) + Expect(report.Spec.ClusterName).To(Equal(clusterName)) + Expect(report.Spec.ClusterType).To(Equal(clusterType)) reportName := libsveltosv1beta1.GetManagementClusterClassifierReportName( classifierName, clusterName, &clusterType) - report := &libsveltosv1beta1.ManagementClusterClassifierReport{} + persisted := &libsveltosv1beta1.ManagementClusterClassifierReport{} Expect(fakeClient.Get(context.TODO(), - types.NamespacedName{Namespace: ns, Name: reportName}, report)).To(Succeed()) - Expect(report.Spec.ClassifierName).To(Equal(classifierName)) - Expect(report.Spec.ClusterName).To(Equal(clusterName)) - Expect(report.Spec.ClusterType).To(Equal(clusterType)) + types.NamespacedName{Namespace: ns, Name: reportName}, persisted)).To(Succeed()) + Expect(persisted.Spec.ClassifierName).To(Equal(classifierName)) + Expect(persisted.Spec.ClusterName).To(Equal(clusterName)) + Expect(persisted.Spec.ClusterType).To(Equal(clusterType)) }) It("is idempotent when the report already exists", func() { @@ -238,9 +241,11 @@ end WithStatusSubresource(&libsveltosv1beta1.ManagementClusterClassifierReport{}). Build() - err := controllers.EnsureMgmtClassifierReport(context.TODO(), fakeClient, + report, err := controllers.EnsureMgmtClassifierReport(context.TODO(), fakeClient, classifierName, ns, clusterName, clusterType) Expect(err).ToNot(HaveOccurred()) + Expect(report.Name).To(Equal(reportName)) + Expect(report.Namespace).To(Equal(ns)) }) }) diff --git a/controllers/mgmtcluster_classifier_utils.go b/controllers/mgmtcluster_classifier_utils.go index 6654846..1ad2b17 100644 --- a/controllers/mgmtcluster_classifier_utils.go +++ b/controllers/mgmtcluster_classifier_utils.go @@ -330,19 +330,23 @@ func getMgmtClassifierReport(ctx context.Context, c client.Client, return report, nil } -// ensureMgmtClassifierReport creates the report if it does not yet exist. Idempotent. +// ensureMgmtClassifierReport creates the report if it does not yet exist, and returns it either +// way. Idempotent. Callers that need to act on the report right after (e.g. patch its status) +// should use the returned object rather than reading it back through the cache: a Get immediately +// following this Create is not guaranteed to observe it, since r.Client is cache-backed and the +// informer may not have processed the write yet. func ensureMgmtClassifierReport(ctx context.Context, c client.Client, classifierName, clusterNamespace, clusterName string, - clusterType libsveltosv1beta1.ClusterType) error { + clusterType libsveltosv1beta1.ClusterType) (*libsveltosv1beta1.ManagementClusterClassifierReport, error) { name := libsveltosv1beta1.GetManagementClusterClassifierReportName(classifierName, clusterName, &clusterType) existing := &libsveltosv1beta1.ManagementClusterClassifierReport{} err := c.Get(ctx, types.NamespacedName{Namespace: clusterNamespace, Name: name}, existing) if err == nil { - return nil + return existing, nil } if !apierrors.IsNotFound(err) { - return err + return nil, err } report := &libsveltosv1beta1.ManagementClusterClassifierReport{ @@ -359,10 +363,18 @@ func ensureMgmtClassifierReport(ctx context.Context, c client.Client, ClusterType: clusterType, }, } - if createErr := c.Create(ctx, report); createErr != nil && !apierrors.IsAlreadyExists(createErr) { - return createErr + if createErr := c.Create(ctx, report); createErr != nil { + if !apierrors.IsAlreadyExists(createErr) { + return nil, createErr + } + // Lost a create race with another reconcile. Fetch what's actually there instead of + // returning our own copy, which was never persisted and has no resourceVersion. + if getErr := c.Get(ctx, types.NamespacedName{Namespace: clusterNamespace, Name: name}, existing); getErr != nil { + return nil, getErr + } + return existing, nil } - return nil + return report, nil } // deleteMgmtClassifierReport deletes the report for the given pair if it exists. @@ -380,25 +392,18 @@ func deleteMgmtClassifierReport(ctx context.Context, c client.Client, return c.Delete(ctx, report) } -// updateMgmtClassifierReportStatus patches the report status with the current label ownership state. +// updateMgmtClassifierReportStatus patches the report status with the current label ownership +// state. Takes the report object directly (from ensureMgmtClassifierReport) rather than looking +// it up again, to avoid a cache-read-after-write race on a report that may have just been created +// in the same reconcile. func updateMgmtClassifierReportStatus(ctx context.Context, c client.Client, - classifierName, clusterNamespace, clusterName string, - clusterType libsveltosv1beta1.ClusterType, + report *libsveltosv1beta1.ManagementClusterClassifierReport, managed []string, unmanaged []libsveltosv1beta1.UnManagedLabel) error { - return retry.RetryOnConflict(retry.DefaultRetry, func() error { - report, err := getMgmtClassifierReport(ctx, c, classifierName, clusterNamespace, clusterName, clusterType) - if err != nil { - if apierrors.IsNotFound(err) { - return nil - } - return err - } - patch := client.MergeFrom(report.DeepCopy()) - report.Status.ManagedLabels = managed - report.Status.UnManagedLabels = unmanaged - return c.Status().Patch(ctx, report, patch) - }) + patch := client.MergeFrom(report.DeepCopy()) + report.Status.ManagedLabels = managed + report.Status.UnManagedLabels = unmanaged + return c.Status().Patch(ctx, report, patch) } // applyLabelsToCluster adds classifierLabels to the cluster. diff --git a/go.mod b/go.mod index dd2d76b..2c3ff47 100644 --- a/go.mod +++ b/go.mod @@ -9,7 +9,7 @@ require ( github.com/onsi/ginkgo/v2 v2.32.1 github.com/onsi/gomega v1.42.1 github.com/pkg/errors v0.9.1 - github.com/projectsveltos/libsveltos v1.14.1-0.20260830144548-82677637a8ae + github.com/projectsveltos/libsveltos v1.14.1-0.20260901064342-1f002bcd7f58 github.com/prometheus/client_golang v1.24.1 github.com/spf13/pflag v1.0.10 github.com/yuin/gopher-lua v1.1.2 diff --git a/go.sum b/go.sum index 6b880c4..dc93c66 100644 --- a/go.sum +++ b/go.sum @@ -206,8 +206,8 @@ github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/projectsveltos/libsveltos v1.14.1-0.20260830144548-82677637a8ae h1:ULG/BXW/tHRT4UfjDzKHtaGDHJMRHYgZgy0IazxPZ18= -github.com/projectsveltos/libsveltos v1.14.1-0.20260830144548-82677637a8ae/go.mod h1:U6iGj5KoC/PcTD2vh3XU6gy7g11suThT6sZSEpmLEkU= +github.com/projectsveltos/libsveltos v1.14.1-0.20260901064342-1f002bcd7f58 h1:4dGVa8AWhlLDAjLDR4RwAwpK8OkIxcGEpw1jcEf1Edg= +github.com/projectsveltos/libsveltos v1.14.1-0.20260901064342-1f002bcd7f58/go.mod h1:U6iGj5KoC/PcTD2vh3XU6gy7g11suThT6sZSEpmLEkU= github.com/projectsveltos/lua-utils/glua-json v0.0.0-20251212200258-2b3cdcb7c0f5 h1:khnc+994UszxZYu69J+R5FKiLA/Nk1JQj0EYAkwTWz0= github.com/projectsveltos/lua-utils/glua-json v0.0.0-20251212200258-2b3cdcb7c0f5/go.mod h1:yVL8KQFa9tmcxgwl9nwIMtKgtmIVC1zaFRSCfOwYvPY= github.com/projectsveltos/lua-utils/glua-runes v0.0.0-20251212200258-2b3cdcb7c0f5 h1:YbsebwRwTRhV8QacvEAdFqxcxHdeu7JTVtsBovbkgos= diff --git a/manifest/deployment-agentless.yaml b/manifest/deployment-agentless.yaml index 189023e..cf76e69 100644 --- a/manifest/deployment-agentless.yaml +++ b/manifest/deployment-agentless.yaml @@ -26,7 +26,7 @@ spec: - --shard-key= - --capi-onboard-annotation= - --v=5 - - --version=v1.14.0 + - --version=main - --registry= - --agent-in-mgmt-cluster=true command: @@ -44,7 +44,7 @@ spec: valueFrom: fieldRef: fieldPath: metadata.namespace - image: docker.io/projectsveltos/classifier:v1.14.0 + image: docker.io/projectsveltos/classifier:main imagePullPolicy: IfNotPresent livenessProbe: failureThreshold: 3 @@ -92,7 +92,7 @@ spec: fieldPath: metadata.namespace - name: IS_INITIALIZATION value: "true" - image: docker.io/projectsveltos/classifier:v1.14.0 + image: docker.io/projectsveltos/classifier:main imagePullPolicy: IfNotPresent name: migrate resources: diff --git a/manifest/deployment-shard.yaml b/manifest/deployment-shard.yaml index e6d6c50..dc207b7 100644 --- a/manifest/deployment-shard.yaml +++ b/manifest/deployment-shard.yaml @@ -26,7 +26,7 @@ spec: - --shard-key={{.SHARD}} - --capi-onboard-annotation= - --v=5 - - --version=v1.14.0 + - --version=main - --registry= - --agent-in-mgmt-cluster=false command: @@ -44,7 +44,7 @@ spec: valueFrom: fieldRef: fieldPath: metadata.namespace - image: docker.io/projectsveltos/classifier:v1.14.0 + image: docker.io/projectsveltos/classifier:main imagePullPolicy: IfNotPresent livenessProbe: failureThreshold: 3 @@ -92,7 +92,7 @@ spec: fieldPath: metadata.namespace - name: IS_INITIALIZATION value: "true" - image: docker.io/projectsveltos/classifier:v1.14.0 + image: docker.io/projectsveltos/classifier:main imagePullPolicy: IfNotPresent name: migrate resources: diff --git a/manifest/manifest.yaml b/manifest/manifest.yaml index 223b717..61be210 100644 --- a/manifest/manifest.yaml +++ b/manifest/manifest.yaml @@ -259,7 +259,7 @@ spec: - --shard-key= - --capi-onboard-annotation= - --v=5 - - --version=v1.14.0 + - --version=main - --registry= - --agent-in-mgmt-cluster=false command: @@ -277,7 +277,7 @@ spec: valueFrom: fieldRef: fieldPath: metadata.namespace - image: docker.io/projectsveltos/classifier:v1.14.0 + image: docker.io/projectsveltos/classifier:main imagePullPolicy: IfNotPresent livenessProbe: failureThreshold: 3 @@ -325,7 +325,7 @@ spec: fieldPath: metadata.namespace - name: IS_INITIALIZATION value: "true" - image: docker.io/projectsveltos/classifier:v1.14.0 + image: docker.io/projectsveltos/classifier:main imagePullPolicy: IfNotPresent name: migrate resources: diff --git a/pkg/agent/sveltos-agent-in-mgmt-cluster.go b/pkg/agent/sveltos-agent-in-mgmt-cluster.go index 3075083..0fced94 100644 --- a/pkg/agent/sveltos-agent-in-mgmt-cluster.go +++ b/pkg/agent/sveltos-agent-in-mgmt-cluster.go @@ -42,11 +42,12 @@ spec: - --cluster-namespace= - --cluster-name= - --cluster-type= - - --version=v1.14.0 + - --version=main - --current-cluster=management-cluster - --run-mode=do-not-send-reports - --discard-managed-fields=true - --enable-nats-watcher=false + - --watch-namespaces= command: - /manager env: @@ -62,7 +63,7 @@ spec: valueFrom: fieldRef: fieldPath: metadata.namespace - image: docker.io/projectsveltos/sveltos-agent@sha256:213dcd1415aefe5f0c4260bc63f4587633675b53faf4406c43b137deaf578046 + image: docker.io/projectsveltos/sveltos-agent@sha256:892fd8c3f89d5886d1fcffa96c62f439522f21d55150df46c2f5d28e9b4a7057 livenessProbe: failureThreshold: 3 httpGet: diff --git a/pkg/agent/sveltos-agent-in-mgmt-cluster.yaml b/pkg/agent/sveltos-agent-in-mgmt-cluster.yaml index 3f85995..39b4ca8 100644 --- a/pkg/agent/sveltos-agent-in-mgmt-cluster.yaml +++ b/pkg/agent/sveltos-agent-in-mgmt-cluster.yaml @@ -24,11 +24,12 @@ spec: - --cluster-namespace= - --cluster-name= - --cluster-type= - - --version=v1.14.0 + - --version=main - --current-cluster=management-cluster - --run-mode=do-not-send-reports - --discard-managed-fields=true - --enable-nats-watcher=false + - --watch-namespaces= command: - /manager env: @@ -44,7 +45,7 @@ spec: valueFrom: fieldRef: fieldPath: metadata.namespace - image: docker.io/projectsveltos/sveltos-agent@sha256:213dcd1415aefe5f0c4260bc63f4587633675b53faf4406c43b137deaf578046 + image: docker.io/projectsveltos/sveltos-agent@sha256:892fd8c3f89d5886d1fcffa96c62f439522f21d55150df46c2f5d28e9b4a7057 livenessProbe: failureThreshold: 3 httpGet: diff --git a/pkg/agent/sveltos-agent.go b/pkg/agent/sveltos-agent.go index ed5f898..226f57c 100644 --- a/pkg/agent/sveltos-agent.go +++ b/pkg/agent/sveltos-agent.go @@ -201,11 +201,12 @@ spec: - --cluster-namespace= - --cluster-name= - --cluster-type= - - --version=v1.14.0 + - --version=main - --current-cluster=managed-cluster - --run-mode=do-not-send-reports - --discard-managed-fields=true - --enable-nats-watcher=false + - --watch-namespaces= command: - /manager env: @@ -221,7 +222,7 @@ spec: valueFrom: fieldRef: fieldPath: metadata.namespace - image: docker.io/projectsveltos/sveltos-agent@sha256:213dcd1415aefe5f0c4260bc63f4587633675b53faf4406c43b137deaf578046 + image: docker.io/projectsveltos/sveltos-agent@sha256:892fd8c3f89d5886d1fcffa96c62f439522f21d55150df46c2f5d28e9b4a7057 livenessProbe: failureThreshold: 3 httpGet: diff --git a/pkg/agent/sveltos-agent.yaml b/pkg/agent/sveltos-agent.yaml index 0b9f1ad..ea0b71d 100644 --- a/pkg/agent/sveltos-agent.yaml +++ b/pkg/agent/sveltos-agent.yaml @@ -183,11 +183,12 @@ spec: - --cluster-namespace= - --cluster-name= - --cluster-type= - - --version=v1.14.0 + - --version=main - --current-cluster=managed-cluster - --run-mode=do-not-send-reports - --discard-managed-fields=true - --enable-nats-watcher=false + - --watch-namespaces= command: - /manager env: @@ -203,7 +204,7 @@ spec: valueFrom: fieldRef: fieldPath: metadata.namespace - image: docker.io/projectsveltos/sveltos-agent@sha256:213dcd1415aefe5f0c4260bc63f4587633675b53faf4406c43b137deaf578046 + image: docker.io/projectsveltos/sveltos-agent@sha256:892fd8c3f89d5886d1fcffa96c62f439522f21d55150df46c2f5d28e9b4a7057 livenessProbe: failureThreshold: 3 httpGet: