diff --git a/config/crd/bases/config.projectsveltos.io_clustersummaries.yaml b/config/crd/bases/config.projectsveltos.io_clustersummaries.yaml index 35de7103..89d29588 100644 --- a/config/crd/bases/config.projectsveltos.io_clustersummaries.yaml +++ b/config/crd/bases/config.projectsveltos.io_clustersummaries.yaml @@ -2480,6 +2480,7 @@ spec: - Removing - Removed - AgentRemoving + - Blocked type: string required: - featureID diff --git a/controllers/clustersummary_controller.go b/controllers/clustersummary_controller.go index a3b5bb50..f85dbfb8 100644 --- a/controllers/clustersummary_controller.go +++ b/controllers/clustersummary_controller.go @@ -427,6 +427,9 @@ func (r *ClusterSummaryReconciler) cleanupBeforeFinalizerRemoval(ctx context.Con } clusterSummaryScope.SetDependenciesMessage(&dependentMsg) if !allRemoved { + // Teardown is deferred, not in progress: reflect that in featureSummaries instead of + // leaving it frozen at whatever status it last had (e.g. a stale Provisioning). + r.resetFeatureStatus(clusterSummaryScope, libsveltosv1beta1.FeatureStatusBlocked) return reconcile.Result{Requeue: true, RequeueAfter: deleteRequeueAfter}, nil, true } @@ -440,6 +443,8 @@ func (r *ClusterSummaryReconciler) cleanupBeforeFinalizerRemoval(ctx context.Con } clusterSummaryScope.SetDependenciesMessage(&transitionMsg) if !allProvisioned { + // Same as the DependsOn gate above: teardown is deferred, not in progress. + r.resetFeatureStatus(clusterSummaryScope, libsveltosv1beta1.FeatureStatusBlocked) return reconcile.Result{Requeue: true, RequeueAfter: deleteRequeueAfter}, nil, true } diff --git a/controllers/clustersummary_controller_test.go b/controllers/clustersummary_controller_test.go index 796c0bb7..afda0e2a 100644 --- a/controllers/clustersummary_controller_test.go +++ b/controllers/clustersummary_controller_test.go @@ -1282,6 +1282,81 @@ var _ = Describe("ClustersummaryController", func() { Expect(removed).To(BeTrue()) }) + It("cleanupBeforeFinalizerRemoval marks featureSummaries Blocked while a dependent ClusterSummary still exists", func() { + // Same DependsOn setup as the areDependentsRemoved test above. This time going through + // cleanupBeforeFinalizerRemoval to check the caller does not leave featureSummaries frozen + // at whatever status they had when teardown was deferred (issue #1950). + dependentProfileName := randomString() + dependentSummaryName := clusterops.GetClusterSummaryName(configv1beta1.ClusterProfileKind, + dependentProfileName, clusterName, false) + dependentSummary := &configv1beta1.ClusterSummary{ + ObjectMeta: metav1.ObjectMeta{ + Name: dependentSummaryName, + Namespace: namespace, + Labels: map[string]string{ + clusterops.ClusterProfileLabelName: dependentProfileName, + configv1beta1.ClusterNameLabel: clusterName, + configv1beta1.ClusterTypeLabel: string(libsveltosv1beta1.ClusterTypeCapi), + }, + }, + Spec: configv1beta1.ClusterSummarySpec{ + ClusterNamespace: cluster.Namespace, + ClusterName: cluster.Name, + ClusterType: libsveltosv1beta1.ClusterTypeCapi, + ClusterProfileSpec: configv1beta1.Spec{ + DependsOn: []string{clusterProfile.Name}, + }, + }, + } + + clusterSummary.Spec.ClusterProfileSpec.PolicyRefs = []configv1beta1.PolicyRef{ + {Kind: string(libsveltosv1beta1.ConfigMapReferencedResourceKind), Namespace: randomString(), Name: randomString()}, + } + clusterSummary.Status.FeatureSummaries = []configv1beta1.FeatureSummary{ + {FeatureID: libsveltosv1beta1.FeatureResources, Status: libsveltosv1beta1.FeatureStatusProvisioning}, + } + + initObjects := []client.Object{ + dependentSummary, + clusterSummary, + clusterProfile, + } + + c := fake.NewClientBuilder().WithScheme(scheme).WithStatusSubresource(initObjects...).WithObjects(initObjects...).Build() + + addOwnerReference(context.TODO(), c, clusterSummary, clusterProfile) + + deployer := fakedeployer.GetClient(context.TODO(), textlogger.NewLogger(textlogger.NewConfig()), c) + reconciler := &controllers.ClusterSummaryReconciler{ + Client: c, + Scheme: scheme, + Deployer: deployer, + ClusterMap: make(map[corev1.ObjectReference]*libsveltosset.Set), + ReferenceMap: make(map[corev1.ObjectReference]*libsveltosset.Set), + PolicyMux: sync.Mutex{}, + } + + clusterSummaryScope, err := scope.NewClusterSummaryScope(&scope.ClusterSummaryScopeParams{ + Client: c, + Logger: textlogger.NewLogger(textlogger.NewConfig()), + ClusterSummary: clusterSummary, + ControllerName: testControllerNameSummary, + }) + Expect(err).To(BeNil()) + + // isDeleted: true skips the ResourceSummary-removal branch, which needs a real managed + // cluster connection; it plays no part in the gate this test is about. + _, err, done := controllers.CleanupBeforeFinalizerRemoval(reconciler, context.TODO(), clusterSummaryScope, + true, textlogger.NewLogger(textlogger.NewConfig())) + Expect(err).To(BeNil()) + Expect(done).To(BeTrue()) + + Expect(clusterSummary.Status.FeatureSummaries).To(HaveLen(1)) + Expect(clusterSummary.Status.FeatureSummaries[0].Status).To(Equal(libsveltosv1beta1.FeatureStatusBlocked)) + Expect(clusterSummary.Status.Dependencies).ToNot(BeNil()) + Expect(*clusterSummary.Status.Dependencies).To(ContainSubstring(dependentSummaryName)) + }) + It("processUndeployError requeues with deleteRequeueAfter and no error for WaitForProfileProcessingError", func() { initObjects := []client.Object{ clusterProfile, diff --git a/controllers/clustersummary_deployer.go b/controllers/clustersummary_deployer.go index a4a1ee2e..4ac3d474 100644 --- a/controllers/clustersummary_deployer.go +++ b/controllers/clustersummary_deployer.go @@ -400,6 +400,11 @@ func (r *ClusterSummaryReconciler) proceedDeployingFeatureInPullMode(ctx context case libsveltosv1beta1.FeatureStatusFailedNonRetriable, libsveltosv1beta1.FeatureStatusRemoving, libsveltosv1beta1.FeatureStatusAgentRemoving, libsveltosv1beta1.FeatureStatusRemoved: logger.V(logs.LogDebug).Info("proceed deploying") + case libsveltosv1beta1.FeatureStatusBlocked: + // Blocked is set locally by cleanupBeforeFinalizerRemoval while a predecessor waits on a + // successor (TransitionFrom); the agent never reports it in a ConfigurationGroup, so this + // case is unreachable in practice. Listed only to satisfy exhaustive. + logger.V(logs.LogDebug).Info("proceed deploying") } } else { provisioning := libsveltosv1beta1.FeatureStatusProvisioning @@ -937,6 +942,11 @@ func (r *ClusterSummaryReconciler) updateFeatureStatus(clusterSummaryScope *scop clusterSummaryScope.SetFeatureStatus(featureID, libsveltosv1beta1.FeatureStatusAgentRemoving, hash, nil) case libsveltosv1beta1.FeatureStatusRemoving: clusterSummaryScope.SetFeatureStatus(featureID, libsveltosv1beta1.FeatureStatusRemoving, hash, nil) + case libsveltosv1beta1.FeatureStatusBlocked: + // Not expected here: cleanupBeforeFinalizerRemoval sets Blocked directly via + // resetFeatureStatus, bypassing updateFeatureStatus. Handled the same way as the other + // non-terminal statuses above in case that ever changes. + clusterSummaryScope.SetFeatureStatus(featureID, libsveltosv1beta1.FeatureStatusBlocked, hash, nil) case libsveltosv1beta1.FeatureStatusFailed, libsveltosv1beta1.FeatureStatusFailedNonRetriable: failed := true clusterSummaryScope.SetFeatureStatus(featureID, *status, hash, &failed) diff --git a/controllers/clustersummary_transition_test.go b/controllers/clustersummary_transition_test.go index 39277493..f203b12a 100644 --- a/controllers/clustersummary_transition_test.go +++ b/controllers/clustersummary_transition_test.go @@ -258,6 +258,65 @@ var _ = Describe("TransitionFrom", func() { Expect(msg).To(ContainSubstring(successor.Name)) }) + It("cleanupBeforeFinalizerRemoval marks featureSummaries Blocked while a matching successor is not yet Provisioned", func() { + // Same setup as the areSuccessorsProvisioned test above. This time going through + // cleanupBeforeFinalizerRemoval to check the caller does not leave featureSummaries frozen + // at whatever status they had when teardown was deferred (issue #1950). + successor := &configv1beta1.ClusterProfile{ + ObjectMeta: metav1.ObjectMeta{Name: clusterProfileNamePrefix + randomString()}, + Spec: configv1beta1.Spec{ + TransitionFrom: []string{predecessor.Name}, + ClusterSelector: libsveltosv1beta1.Selector{ + LabelSelector: metav1.LabelSelector{ + MatchLabels: map[string]string{testDCLabelKey: testEngValue}, + }, + }, + }, + } + successorSummaryName := clusterops.GetClusterSummaryName(configv1beta1.ClusterProfileKind, + successor.Name, clusterName, false) + successorSummary := &configv1beta1.ClusterSummary{ + ObjectMeta: metav1.ObjectMeta{Name: successorSummaryName, Namespace: namespace}, + Spec: configv1beta1.ClusterSummarySpec{ + ClusterNamespace: cluster.Namespace, + ClusterName: cluster.Name, + ClusterType: libsveltosv1beta1.ClusterTypeCapi, + ClusterProfileSpec: configv1beta1.Spec{ + PolicyRefs: []configv1beta1.PolicyRef{ + {Kind: string(libsveltosv1beta1.ConfigMapReferencedResourceKind), Namespace: namespace, Name: randomString()}, + }, + }, + }, + // No FeatureSummaries yet: not Provisioned. + } + successorSummary.Labels = map[string]string{ + clusterops.ClusterProfileLabelName: successor.Name, + configv1beta1.ClusterNameLabel: clusterName, + configv1beta1.ClusterTypeLabel: string(libsveltosv1beta1.ClusterTypeCapi), + } + + predecessorSummary.Spec.ClusterProfileSpec.PolicyRefs = []configv1beta1.PolicyRef{ + {Kind: string(libsveltosv1beta1.ConfigMapReferencedResourceKind), Namespace: namespace, Name: randomString()}, + } + predecessorSummary.Status.FeatureSummaries = []configv1beta1.FeatureSummary{ + {FeatureID: libsveltosv1beta1.FeatureResources, Status: libsveltosv1beta1.FeatureStatusProvisioning}, + } + + reconciler := newReconciler(predecessorSummary, predecessor, cluster, successor, successorSummary) + + // isDeleted: true skips the ResourceSummary-removal branch, which needs a real managed + // cluster connection; it plays no part in the gate this test is about. + _, err, done := controllers.CleanupBeforeFinalizerRemoval(reconciler, context.TODO(), newScope(reconciler), + true, textlogger.NewLogger(textlogger.NewConfig())) + Expect(err).To(BeNil()) + Expect(done).To(BeTrue()) + + Expect(predecessorSummary.Status.FeatureSummaries).To(HaveLen(1)) + Expect(predecessorSummary.Status.FeatureSummaries[0].Status).To(Equal(libsveltosv1beta1.FeatureStatusBlocked)) + Expect(predecessorSummary.Status.Dependencies).ToNot(BeNil()) + Expect(*predecessorSummary.Status.Dependencies).To(ContainSubstring(successor.Name)) + }) + It("areSuccessorsProvisioned proceeds once the matching successor is Provisioned", func() { successor := &configv1beta1.ClusterProfile{ ObjectMeta: metav1.ObjectMeta{Name: clusterProfileNamePrefix + randomString()}, diff --git a/controllers/export_test.go b/controllers/export_test.go index d975be9c..ae941249 100644 --- a/controllers/export_test.go +++ b/controllers/export_test.go @@ -70,6 +70,7 @@ var ( ShouldRedeploy = (*ClusterSummaryReconciler).shouldRedeploy CanRemoveFinalizer = (*ClusterSummaryReconciler).canRemoveFinalizer ReconcileDelete = (*ClusterSummaryReconciler).reconcileDelete + CleanupBeforeFinalizerRemoval = (*ClusterSummaryReconciler).cleanupBeforeFinalizerRemoval AreDependenciesDeployed = (*ClusterSummaryReconciler).areDependenciesDeployed AreDependentsRemoved = (*ClusterSummaryReconciler).areDependentsRemoved AreSuccessorsProvisioned = (*ClusterSummaryReconciler).areSuccessorsProvisioned diff --git a/go.mod b/go.mod index d057d29c..07893654 100644 --- a/go.mod +++ b/go.mod @@ -20,7 +20,7 @@ require ( github.com/onsi/gomega v1.43.0 github.com/opencontainers/image-spec v1.1.1 github.com/pkg/errors v0.9.1 - github.com/projectsveltos/libsveltos v1.14.1-0.20260909063818-b0a031992dbc + github.com/projectsveltos/libsveltos v1.14.1-0.20260909140634-7473f8e8ef85 github.com/prometheus/client_golang v1.24.1 github.com/sigstore/cosign/v3 v3.1.3 github.com/sigstore/sigstore v1.10.9 diff --git a/go.sum b/go.sum index 95274b1b..311c22bc 100644 --- a/go.sum +++ b/go.sum @@ -641,8 +641,8 @@ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRI github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/poy/onpar v1.1.2 h1:QaNrNiZx0+Nar5dLgTVp5mXkyoVFIbepjyEoGSnhbAY= github.com/poy/onpar v1.1.2/go.mod h1:6X8FLNoxyr9kkmnlqpK6LSoiOtrO6MICtWwEuWkLjzg= -github.com/projectsveltos/libsveltos v1.14.1-0.20260909063818-b0a031992dbc h1:yDckm+M/oppO2YOqdooOLN5md+esuJovkrxH8KHojmE= -github.com/projectsveltos/libsveltos v1.14.1-0.20260909063818-b0a031992dbc/go.mod h1:U6iGj5KoC/PcTD2vh3XU6gy7g11suThT6sZSEpmLEkU= +github.com/projectsveltos/libsveltos v1.14.1-0.20260909140634-7473f8e8ef85 h1:U3vS4oqjCp7ifeIUFmwytKtDdM0b2QMLjbwQdns0axg= +github.com/projectsveltos/libsveltos v1.14.1-0.20260909140634-7473f8e8ef85/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/lib/crd/clustersummaries.go b/lib/crd/clustersummaries.go index 54d57b0a..9edb5903 100644 --- a/lib/crd/clustersummaries.go +++ b/lib/crd/clustersummaries.go @@ -2499,6 +2499,7 @@ spec: - Removing - Removed - AgentRemoving + - Blocked type: string required: - featureID diff --git a/manifest/manifest.yaml b/manifest/manifest.yaml index 667b92f4..21f36f9f 100644 --- a/manifest/manifest.yaml +++ b/manifest/manifest.yaml @@ -8901,6 +8901,7 @@ spec: - Removing - Removed - AgentRemoving + - Blocked type: string required: - featureID