From c93429978a2e84b9437c39bd9289097e89c48a96 Mon Sep 17 00:00:00 2001 From: Gianluca Mardente Date: Thu, 11 Jun 2026 10:42:57 +0200 Subject: [PATCH] (bug) Cluster with pending updates Before this PR, ui-backedn incorrectly reported a cluster as having pending updates if a profile matched the cluster and had dependencies. This PR fixes that. --- internal/server/manager.go | 28 ++++++++++++++++++++++++++-- internal/server/manager_test.go | 21 +++++++++++++++++++++ 2 files changed, 47 insertions(+), 2 deletions(-) diff --git a/internal/server/manager.go b/internal/server/manager.go index db5ef4f..050b4f4 100644 --- a/internal/server/manager.go +++ b/internal/server/manager.go @@ -466,9 +466,17 @@ func (m *instance) ClusterIsProvisioning(clusterType libsveltosv1beta1.ClusterTy // clusterSummaryIsProvisioning returns true when the ClusterSummary is actively provisioning // without errors. Two conditions qualify: -// 1. Any feature has Provisioning status with no failure message (clean in-progress deploy). -// 2. The dependencies field is non-empty (waiting for another ClusterProfile to finish). +// 1. The dependencies field is non-empty and not all features are Provisioned yet +// (waiting for another ClusterProfile to finish). +// 2. Any feature has Provisioning status with no failure message (clean in-progress deploy). +// +// If all features are already Provisioned the ClusterSummary is done regardless of what +// the dependencies string says — the addon-controller writes informational messages like +// "All dependencies deployed" even after work is complete. func clusterSummaryIsProvisioning(features []ClusterFeatureSummary, dependencies string) bool { + if allFeaturesProvisioned(features) { + return false + } if dependencies != "" { return true } @@ -483,6 +491,22 @@ func clusterSummaryIsProvisioning(features []ClusterFeatureSummary, dependencies return false } +// allFeaturesProvisioned returns true when every feature in the ClusterSummary has +// reached FeatureStatusProvisioned. An empty feature list is not considered fully +// provisioned — the ClusterSummary may still be waiting on dependencies before +// deploying begins. +func allFeaturesProvisioned(features []ClusterFeatureSummary) bool { + if len(features) == 0 { + return false + } + for i := range features { + if features[i].Status != libsveltosv1beta1.FeatureStatusProvisioned { + return false + } + } + return true +} + // updateClusterProvisioning inserts or erases csRef from the per-cluster provisioning set. // Must be called with clusterStatusesMux held for writing. func (m *instance) updateClusterProvisioning(clusterKey string, csRef *corev1.ObjectReference, isProvisioning bool) { diff --git a/internal/server/manager_test.go b/internal/server/manager_test.go index 639fa2c..a70d7fa 100644 --- a/internal/server/manager_test.go +++ b/internal/server/manager_test.go @@ -652,6 +652,27 @@ var _ = Describe("Manager", func() { Expect(manager.ClusterIsProvisioning(libsveltosv1beta1.ClusterTypeCapi, cluster.Namespace, cluster.Name)).To(BeFalse()) }) + It("ClusterIsProvisioning returns false when all features are Provisioned even if dependencies string is set", func() { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + server.InitializeManagerInstance(ctx, nil, c, scheme, randomPort(), logger) + manager := server.GetManagerInstance() + + // addon-controller sets Status.Dependencies to "All dependencies deployed" even after + // the ClusterSummary is fully provisioned; ui-backend must not treat that as provisioning. + depMsg := "All dependencies deployed" + cs := createTestClusterSummary("cs-provisioned-with-dep-msg", cluster.Namespace, cluster.Namespace, cluster.Name, + []configv1beta1.FeatureSummary{ + {FeatureID: helmFeatureID, Status: libsveltosv1beta1.FeatureStatusProvisioned}, + }, + ) + cs.Status.Dependencies = &depMsg + manager.AddClusterProfileStatus(cs) + + Expect(manager.ClusterIsProvisioning(libsveltosv1beta1.ClusterTypeCapi, cluster.Namespace, cluster.Name)).To(BeFalse()) + }) + It("ClusterIsProvisioning clears when ClusterSummary is removed", func() { ctx, cancel := context.WithCancel(context.Background()) defer cancel()