Skip to content

Commit 5dd432c

Browse files
committed
fix: fix nil selector should select all member clusters
Signed-off-by: Wantong Jiang <[email protected]>
1 parent 981e0fb commit 5dd432c

File tree

6 files changed

+62
-9
lines changed

6 files changed

+62
-9
lines changed

apis/placement/v1beta1/stageupdate_types.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,8 @@ type StageConfig struct {
127127

128128
// LabelSelector is a label query over all the joined member clusters. Clusters matching the query are selected
129129
// for this stage. There cannot be overlapping clusters between stages when the stagedUpdateRun is created.
130-
// If the label selector is absent, the stage includes all the selected clusters.
130+
// If the label selector is empty, the stage includes all the selected clusters.
131+
// If the label selector is nil, the stage does not include any selected clusters.
131132
// +kubebuilder:validation:Optional
132133
LabelSelector *metav1.LabelSelector `json:"labelSelector,omitempty"`
133134

config/crd/bases/placement.kubernetes-fleet.io_clusterstagedupdateruns.yaml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1818,7 +1818,8 @@ spec:
18181818
description: |-
18191819
LabelSelector is a label query over all the joined member clusters. Clusters matching the query are selected
18201820
for this stage. There cannot be overlapping clusters between stages when the stagedUpdateRun is created.
1821-
If the label selector is absent, the stage includes all the selected clusters.
1821+
If the label selector is empty, the stage includes all the selected clusters.
1822+
If the label selector is nil, the stage does not include any selected clusters.
18221823
properties:
18231824
matchExpressions:
18241825
description: matchExpressions is a list of label selector

config/crd/bases/placement.kubernetes-fleet.io_clusterstagedupdatestrategies.yaml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -222,7 +222,8 @@ spec:
222222
description: |-
223223
LabelSelector is a label query over all the joined member clusters. Clusters matching the query are selected
224224
for this stage. There cannot be overlapping clusters between stages when the stagedUpdateRun is created.
225-
If the label selector is absent, the stage includes all the selected clusters.
225+
If the label selector is empty, the stage includes all the selected clusters.
226+
If the label selector is nil, the stage does not include any selected clusters.
226227
properties:
227228
matchExpressions:
228229
description: matchExpressions is a list of label selector

pkg/controllers/updaterun/initialization.go

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import (
2222
"fmt"
2323
"sort"
2424
"strconv"
25+
"strings"
2526

2627
apierrors "k8s.io/apimachinery/pkg/api/errors"
2728
"k8s.io/apimachinery/pkg/api/meta"
@@ -380,13 +381,18 @@ func (r *Reconciler) computeRunStageStatus(
380381
// Check if the clusters are all placed.
381382
if len(allPlacedClusters) != len(allSelectedClusters) {
382383
missingErr := controller.NewUserError(fmt.Errorf("some clusters are not placed in any stage"))
384+
missingClusters := make([]string, 0, len(allSelectedClusters)-len(allPlacedClusters))
383385
for cluster := range allSelectedClusters {
384386
if _, ok := allPlacedClusters[cluster]; !ok {
385-
klog.ErrorS(missingErr, "Cluster is missing in any stage", "cluster", cluster, "clusterStagedUpdateStrategy", updateStrategyName, "clusterStagedUpdateRun", updateRunRef)
387+
missingClusters = append(missingClusters, cluster)
386388
}
387389
}
390+
// Sort the missing clusters by their names to generate a stable error message.
391+
sort.StringSlice(missingClusters).Sort()
392+
missingClustersStr := strings.Join(missingClusters, ", ")
393+
klog.ErrorS(missingErr, "Clusters are missing in any stage", "clusters", missingClustersStr, "clusterStagedUpdateStrategy", updateStrategyName, "clusterStagedUpdateRun", updateRunRef)
388394
// no more retries here.
389-
return fmt.Errorf("%w: %s", errInitializedFailed, missingErr.Error())
395+
return fmt.Errorf("%w: %s", errInitializedFailed, fmt.Sprintf("%s: %s", missingErr.Error(), missingClustersStr))
390396
}
391397
return nil
392398
}

pkg/controllers/updaterun/initialization_integration_test.go

Lines changed: 47 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -549,7 +549,7 @@ var _ = Describe("Updaterun initialization tests", func() {
549549
validateFailedInitCondition(ctx, updateRun, "referenced clusterStagedUpdateStrategy not found")
550550
})
551551

552-
Context("Test computeStagedStatus", func() {
552+
Context("Test computeRunStageStatus", func() {
553553
Context("Test validateAfterStageTask", func() {
554554
It("Should fail to initialize if any after stage task has 2 same tasks", func() {
555555
By("Creating a clusterStagedUpdateStrategy with 2 same after stage tasks")
@@ -617,7 +617,50 @@ var _ = Describe("Updaterun initialization tests", func() {
617617
Expect(k8sClient.Create(ctx, updateRun)).To(Succeed())
618618

619619
By("Validating the initialization failed")
620-
validateFailedInitCondition(ctx, updateRun, "some clusters are not placed in any stage")
620+
validateFailedInitCondition(ctx, updateRun, "some clusters are not placed in any stage: cluster-0, cluster-2, cluster-4, cluster-6, cluster-8")
621+
})
622+
623+
It("Should select all scheduled clusters if labelSelector is empty and select no clusters if labelSelector is nil", func() {
624+
By("Creating a clusterStagedUpdateStrategy with two stages, using empty labelSelector and nil labelSelector respectively")
625+
updateStrategy.Spec.Stages[0].LabelSelector = nil // no clusters selected
626+
updateStrategy.Spec.Stages[1].LabelSelector = &metav1.LabelSelector{} // all clusters selected
627+
Expect(k8sClient.Create(ctx, updateStrategy)).To(Succeed())
628+
629+
By("Creating a new clusterStagedUpdateRun")
630+
Expect(k8sClient.Create(ctx, updateRun)).To(Succeed())
631+
632+
By("Validating the clusterStagedUpdateRun status")
633+
Eventually(func() error {
634+
if err := k8sClient.Get(ctx, updateRunNamespacedName, updateRun); err != nil {
635+
return err
636+
}
637+
638+
want := generateSucceededInitializationStatus(crp, updateRun, policySnapshot, updateStrategy, clusterResourceOverride)
639+
// No clusters should be selected in the first stage.
640+
want.StagesStatus[0].Clusters = []placementv1beta1.ClusterUpdatingStatus{}
641+
// All clusters should be selected in the second stage and sorted by name.
642+
want.StagesStatus[1].Clusters = []placementv1beta1.ClusterUpdatingStatus{
643+
{ClusterName: "cluster-0"},
644+
{ClusterName: "cluster-1"},
645+
{ClusterName: "cluster-2"},
646+
{ClusterName: "cluster-3"},
647+
{ClusterName: "cluster-4"},
648+
{ClusterName: "cluster-5"},
649+
{ClusterName: "cluster-6"},
650+
{ClusterName: "cluster-7"},
651+
{ClusterName: "cluster-8"},
652+
{ClusterName: "cluster-9"},
653+
}
654+
// initialization should fail due to resourceSnapshot not found.
655+
want.Conditions = []metav1.Condition{
656+
generateFalseCondition(updateRun, placementv1beta1.StagedUpdateRunConditionInitialized),
657+
}
658+
659+
if diff := cmp.Diff(*want, updateRun.Status, cmpOptions...); diff != "" {
660+
return fmt.Errorf("status mismatch: (-want +got):\n%s", diff)
661+
}
662+
return nil
663+
}, timeout, interval).Should(Succeed(), "failed to validate the clusterStagedUpdateRun status")
621664
})
622665
})
623666

@@ -639,7 +682,7 @@ var _ = Describe("Updaterun initialization tests", func() {
639682
// Remove the CROs, as they are not added in this test.
640683
want.StagesStatus[0].Clusters[i].ClusterResourceOverrideSnapshots = nil
641684
}
642-
// initialization should fail.
685+
// initialization should fail due to resourceSnapshot not found.
643686
want.Conditions = []metav1.Condition{
644687
generateFalseCondition(updateRun, placementv1beta1.StagedUpdateRunConditionInitialized),
645688
}
@@ -648,7 +691,7 @@ var _ = Describe("Updaterun initialization tests", func() {
648691
return fmt.Errorf("status mismatch: (-want +got):\n%s", diff)
649692
}
650693
return nil
651-
}, timeout, interval).Should(Succeed(), "failed to validate the clusterStagedUpdateRun in the status")
694+
}, timeout, interval).Should(Succeed(), "failed to validate the clusterStagedUpdateRun status")
652695
})
653696
})
654697

pkg/utils/controller/metrics/metrics.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ var (
7373
Name: "fleet_workload_eviction_complete",
7474
Help: "Eviction complete status ",
7575
}, []string{"name", "isCompleted", "isValid"})
76+
7677
// FleetUpdateRunStatusLastTimestampSeconds is a prometheus metric which holds the
7778
// last update timestamp of update run status in seconds.
7879
FleetUpdateRunStatusLastTimestampSeconds = prometheus.NewGaugeVec(prometheus.GaugeOpts{

0 commit comments

Comments
 (0)