diff --git a/.codespellignore b/.codespellignore index e2e28306d..ac6601862 100644 --- a/.codespellignore +++ b/.codespellignore @@ -3,4 +3,4 @@ AfterAll CROs NotIn fo -allReady \ No newline at end of file +allReady diff --git a/apis/placement/v1alpha1/stagedupdate_types.go b/apis/placement/v1alpha1/stagedupdate_types.go index b2065bc01..661389dde 100644 --- a/apis/placement/v1alpha1/stagedupdate_types.go +++ b/apis/placement/v1alpha1/stagedupdate_types.go @@ -462,6 +462,11 @@ const ( // Its condition status can be: // - "True": The request is approved. ApprovalRequestConditionApproved ApprovalRequestConditionType = "Approved" + + // ApprovalRequestConditionApprovalAccepted indicates if the approved approval request was accepted. + // Its condition status can be: + // - "True": The request is approved. + ApprovalRequestConditionApprovalAccepted ApprovalRequestConditionType = "ApprovalAccepted" ) // ClusterApprovalRequestList contains a list of ClusterApprovalRequest. diff --git a/apis/placement/v1beta1/stageupdate_types.go b/apis/placement/v1beta1/stageupdate_types.go index 616106fc2..b6bb96e76 100644 --- a/apis/placement/v1beta1/stageupdate_types.go +++ b/apis/placement/v1beta1/stageupdate_types.go @@ -463,6 +463,11 @@ const ( // Its condition status can be: // - "True": The request is approved. ApprovalRequestConditionApproved ApprovalRequestConditionType = "Approved" + + // ApprovalRequestConditionApprovalAccepted indicates if the approved approval request was accepted. + // Its condition status can be: + // - "True": The request is approved. + ApprovalRequestConditionApprovalAccepted ApprovalRequestConditionType = "ApprovalAccepted" ) // ClusterApprovalRequestList contains a list of ClusterApprovalRequest. diff --git a/pkg/controllers/updaterun/controller.go b/pkg/controllers/updaterun/controller.go index 1fdaab22e..05d9e9679 100644 --- a/pkg/controllers/updaterun/controller.go +++ b/pkg/controllers/updaterun/controller.go @@ -235,28 +235,39 @@ func (r *Reconciler) SetupWithManager(mgr runtime.Manager) error { // We only care about when an approval request is approved. UpdateFunc: func(ctx context.Context, e event.UpdateEvent, q workqueue.RateLimitingInterface) { klog.V(2).InfoS("Handling a clusterApprovalRequest update event", "clusterApprovalRequest", klog.KObj(e.ObjectNew)) - handleClusterApprovalRequest(e.ObjectNew, q) - }, - GenericFunc: func(ctx context.Context, e event.GenericEvent, q workqueue.RateLimitingInterface) { - klog.V(2).InfoS("Handling a clusterApprovalRequest generic event", "clusterApprovalRequest", klog.KObj(e.Object)) - handleClusterApprovalRequest(e.Object, q) + handleClusterApprovalRequest(e.ObjectOld, e.ObjectNew, q) }, }).Complete(r) } // handleClusterApprovalRequest finds the ClusterStagedUpdateRun creating the ClusterApprovalRequest, -// and enqueues it to the ClusterStagedUpdateRun controller queue. -func handleClusterApprovalRequest(obj client.Object, q workqueue.RateLimitingInterface) { - approvalRequest, ok := obj.(*placementv1beta1.ClusterApprovalRequest) +// and enqueues it to the ClusterStagedUpdateRun controller queue only when the approved condition gets changed. +func handleClusterApprovalRequest(oldObj, newObj client.Object, q workqueue.RateLimitingInterface) { + oldAppReq, ok := oldObj.(*placementv1beta1.ClusterApprovalRequest) + if !ok { + klog.V(2).ErrorS(controller.NewUnexpectedBehaviorError(fmt.Errorf("cannot cast runtime object to ClusterApprovalRequest")), + "Invalid object type", "object", klog.KObj(oldObj)) + return + } + newAppReq, ok := newObj.(*placementv1beta1.ClusterApprovalRequest) if !ok { klog.V(2).ErrorS(controller.NewUnexpectedBehaviorError(fmt.Errorf("cannot cast runtime object to ClusterApprovalRequest")), - "Invalid object type", "object", klog.KObj(obj)) + "Invalid object type", "object", klog.KObj(newObj)) return } - updateRun := approvalRequest.Spec.TargetUpdateRun + + approvedInOld := condition.IsConditionStatusTrue(meta.FindStatusCondition(oldAppReq.Status.Conditions, string(placementv1beta1.ApprovalRequestConditionApproved)), oldAppReq.Generation) + approvedInNew := condition.IsConditionStatusTrue(meta.FindStatusCondition(newAppReq.Status.Conditions, string(placementv1beta1.ApprovalRequestConditionApproved)), newAppReq.Generation) + + if approvedInOld == approvedInNew { + klog.V(2).InfoS("The approval status is not changed, ignore queueing", "clusterApprovalRequest", klog.KObj(newAppReq)) + return + } + + updateRun := newAppReq.Spec.TargetUpdateRun if len(updateRun) == 0 { klog.V(2).ErrorS(controller.NewUnexpectedBehaviorError(fmt.Errorf("TargetUpdateRun field in ClusterApprovalRequest is empty")), - "Invalid clusterApprovalRequest", "clusterApprovalRequest", klog.KObj(approvalRequest)) + "Invalid clusterApprovalRequest", "clusterApprovalRequest", klog.KObj(newAppReq)) return } // enqueue to the updaterun controller queue. diff --git a/pkg/controllers/updaterun/controller_test.go b/pkg/controllers/updaterun/controller_test.go index 7effec96b..fad7b4f60 100644 --- a/pkg/controllers/updaterun/controller_test.go +++ b/pkg/controllers/updaterun/controller_test.go @@ -8,6 +8,7 @@ package updaterun import ( "testing" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/util/workqueue" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/controller/controllertest" @@ -18,47 +19,286 @@ import ( func TestHandleClusterApprovalRequest(t *testing.T) { tests := map[string]struct { - obj client.Object + oldObj client.Object + newObj client.Object shouldEnqueue bool queuedName string }{ "it should not enqueue anything if the obj is not a ClusterApprovalRequest": { - obj: &placementv1beta1.ClusterStagedUpdateRun{}, + oldObj: &placementv1beta1.ClusterStagedUpdateRun{}, shouldEnqueue: false, }, "it should not enqueue anything if targetUpdateRun in spec is empty": { - obj: &placementv1beta1.ClusterApprovalRequest{ + oldObj: &placementv1beta1.ClusterApprovalRequest{ + ObjectMeta: metav1.ObjectMeta{ + Generation: 1, + }, Spec: placementv1beta1.ApprovalRequestSpec{ TargetUpdateRun: "", }, }, + newObj: &placementv1beta1.ClusterApprovalRequest{ + ObjectMeta: metav1.ObjectMeta{ + Generation: 1, + }, + Spec: placementv1beta1.ApprovalRequestSpec{ + TargetUpdateRun: "", + }, + Status: placementv1beta1.ApprovalRequestStatus{ + Conditions: []metav1.Condition{ + { + Status: metav1.ConditionTrue, + Type: string(placementv1beta1.ApprovalRequestConditionApproved), + ObservedGeneration: 1, + }, + }, + }, + }, shouldEnqueue: false, }, - "it should enqueue the targetUpdateRun if it is not empty": { - obj: &placementv1beta1.ClusterApprovalRequest{ + "it should enqueue the targetUpdateRun if oldObj is not approved while newobj is approved": { + oldObj: &placementv1beta1.ClusterApprovalRequest{ + ObjectMeta: metav1.ObjectMeta{ + Generation: 1, + }, + Spec: placementv1beta1.ApprovalRequestSpec{ + TargetUpdateRun: "test", + }, + }, + newObj: &placementv1beta1.ClusterApprovalRequest{ + ObjectMeta: metav1.ObjectMeta{ + Generation: 1, + }, + Spec: placementv1beta1.ApprovalRequestSpec{ + TargetUpdateRun: "test", + }, + Status: placementv1beta1.ApprovalRequestStatus{ + Conditions: []metav1.Condition{ + { + Status: metav1.ConditionTrue, + Type: string(placementv1beta1.ApprovalRequestConditionApproved), + ObservedGeneration: 1, + }, + }, + }, + }, + shouldEnqueue: true, + queuedName: "test", + }, + "it should enqueue the targetUpdateRun if oldObj is not declined while newobj is approved": { + oldObj: &placementv1beta1.ClusterApprovalRequest{ + ObjectMeta: metav1.ObjectMeta{ + Generation: 1, + }, + Spec: placementv1beta1.ApprovalRequestSpec{ + TargetUpdateRun: "test", + }, + Status: placementv1beta1.ApprovalRequestStatus{ + Conditions: []metav1.Condition{ + { + Status: metav1.ConditionFalse, + Type: string(placementv1beta1.ApprovalRequestConditionApproved), + ObservedGeneration: 1, + }, + }, + }, + }, + newObj: &placementv1beta1.ClusterApprovalRequest{ + ObjectMeta: metav1.ObjectMeta{ + Generation: 1, + }, + Spec: placementv1beta1.ApprovalRequestSpec{ + TargetUpdateRun: "test", + }, + Status: placementv1beta1.ApprovalRequestStatus{ + Conditions: []metav1.Condition{ + { + Status: metav1.ConditionTrue, + Type: string(placementv1beta1.ApprovalRequestConditionApproved), + ObservedGeneration: 1, + }, + }, + }, + }, + shouldEnqueue: true, + queuedName: "test", + }, + "it should enqueue the targetUpdateRun if oldObj is approved while newobj is not approved": { + oldObj: &placementv1beta1.ClusterApprovalRequest{ + ObjectMeta: metav1.ObjectMeta{ + Generation: 1, + }, + Spec: placementv1beta1.ApprovalRequestSpec{ + TargetUpdateRun: "test", + }, + Status: placementv1beta1.ApprovalRequestStatus{ + Conditions: []metav1.Condition{ + { + Status: metav1.ConditionTrue, + Type: string(placementv1beta1.ApprovalRequestConditionApproved), + ObservedGeneration: 1, + }, + }, + }, + }, + newObj: &placementv1beta1.ClusterApprovalRequest{ + ObjectMeta: metav1.ObjectMeta{ + Generation: 1, + }, + Spec: placementv1beta1.ApprovalRequestSpec{ + TargetUpdateRun: "test", + }, + }, + shouldEnqueue: true, + queuedName: "test", + }, + "it should enqueue the targetUpdateRun if oldObj is approved while newobj is declined": { + oldObj: &placementv1beta1.ClusterApprovalRequest{ + ObjectMeta: metav1.ObjectMeta{ + Generation: 1, + }, Spec: placementv1beta1.ApprovalRequestSpec{ TargetUpdateRun: "test", }, + Status: placementv1beta1.ApprovalRequestStatus{ + Conditions: []metav1.Condition{ + { + Status: metav1.ConditionTrue, + Type: string(placementv1beta1.ApprovalRequestConditionApproved), + ObservedGeneration: 1, + }, + }, + }, + }, + newObj: &placementv1beta1.ClusterApprovalRequest{ + ObjectMeta: metav1.ObjectMeta{ + Generation: 1, + }, + Spec: placementv1beta1.ApprovalRequestSpec{ + TargetUpdateRun: "test", + }, + Status: placementv1beta1.ApprovalRequestStatus{ + Conditions: []metav1.Condition{ + { + Status: metav1.ConditionFalse, + Type: string(placementv1beta1.ApprovalRequestConditionApproved), + ObservedGeneration: 1, + }, + }, + }, }, shouldEnqueue: true, queuedName: "test", }, + "it should not enqueue the targetUpdateRun if neither oldObj nor newobj is approved": { + oldObj: &placementv1beta1.ClusterApprovalRequest{ + ObjectMeta: metav1.ObjectMeta{ + Generation: 1, + }, + Spec: placementv1beta1.ApprovalRequestSpec{ + TargetUpdateRun: "test", + }, + }, + newObj: &placementv1beta1.ClusterApprovalRequest{ + ObjectMeta: metav1.ObjectMeta{ + Generation: 1, + }, + Spec: placementv1beta1.ApprovalRequestSpec{ + TargetUpdateRun: "test", + }, + }, + shouldEnqueue: false, + }, + "it should not enqueue the targetUpdateRun if both oldObj and newobj are approved": { + oldObj: &placementv1beta1.ClusterApprovalRequest{ + ObjectMeta: metav1.ObjectMeta{ + Generation: 1, + }, + Spec: placementv1beta1.ApprovalRequestSpec{ + TargetUpdateRun: "test", + }, + Status: placementv1beta1.ApprovalRequestStatus{ + Conditions: []metav1.Condition{ + { + Status: metav1.ConditionTrue, + Type: string(placementv1beta1.ApprovalRequestConditionApproved), + ObservedGeneration: 1, + }, + }, + }, + }, + newObj: &placementv1beta1.ClusterApprovalRequest{ + ObjectMeta: metav1.ObjectMeta{ + Generation: 1, + }, + Spec: placementv1beta1.ApprovalRequestSpec{ + TargetUpdateRun: "test", + }, + Status: placementv1beta1.ApprovalRequestStatus{ + Conditions: []metav1.Condition{ + { + Status: metav1.ConditionTrue, + Type: string(placementv1beta1.ApprovalRequestConditionApproved), + ObservedGeneration: 1, + }, + }, + }, + }, + shouldEnqueue: false, + }, + "it should not enqueue the targetUpdateRun if both oldObj and newobj are declined": { + oldObj: &placementv1beta1.ClusterApprovalRequest{ + ObjectMeta: metav1.ObjectMeta{ + Generation: 1, + }, + Spec: placementv1beta1.ApprovalRequestSpec{ + TargetUpdateRun: "test", + }, + Status: placementv1beta1.ApprovalRequestStatus{ + Conditions: []metav1.Condition{ + { + Status: metav1.ConditionFalse, + Type: string(placementv1beta1.ApprovalRequestConditionApproved), + ObservedGeneration: 1, + }, + }, + }, + }, + newObj: &placementv1beta1.ClusterApprovalRequest{ + ObjectMeta: metav1.ObjectMeta{ + Generation: 1, + }, + Spec: placementv1beta1.ApprovalRequestSpec{ + TargetUpdateRun: "test", + }, + Status: placementv1beta1.ApprovalRequestStatus{ + Conditions: []metav1.Condition{ + { + Status: metav1.ConditionFalse, + Type: string(placementv1beta1.ApprovalRequestConditionApproved), + ObservedGeneration: 1, + }, + }, + }, + }, + shouldEnqueue: false, + }, } for name, tt := range tests { t.Run(name, func(t *testing.T) { queue := &controllertest.Queue{Interface: workqueue.New()} - handleClusterApprovalRequest(tt.obj, queue) + handleClusterApprovalRequest(tt.oldObj, tt.newObj, queue) if got := queue.Len() != 0; got != tt.shouldEnqueue { - t.Errorf("handleClusterApprovalRequest() shouldEnqueue test `%s` got %t, want %t", name, got, tt.shouldEnqueue) + t.Fatalf("handleClusterApprovalRequest() shouldEnqueue test `%s` got %t, want %t", name, got, tt.shouldEnqueue) } if tt.shouldEnqueue { item, _ := queue.Get() req, ok := item.(reconcile.Request) if !ok { - t.Errorf("handleClusterApprovalRequest() queuedItem test `%s` got %T, want reconcile.Request", name, item) + t.Fatalf("handleClusterApprovalRequest() queuedItem test `%s` got %T, want reconcile.Request", name, item) } if req.Name != tt.queuedName { - t.Errorf("handleClusterApprovalRequest() queuedName test `%s` got %s, want %s", name, req.Name, tt.queuedName) + t.Fatalf("handleClusterApprovalRequest() queuedName test `%s` got %s, want %s", name, req.Name, tt.queuedName) } } }) diff --git a/pkg/controllers/updaterun/execution.go b/pkg/controllers/updaterun/execution.go index 114bd892b..b4c468bac 100644 --- a/pkg/controllers/updaterun/execution.go +++ b/pkg/controllers/updaterun/execution.go @@ -306,11 +306,20 @@ func (r *Reconciler) checkAfterStageTasksStatus(ctx context.Context, updatingSta klog.ErrorS(unexpectedErr, "Found an approval request targeting wrong stage", "approvalRequestTask", requestRef, "stage", updatingStage.Name, "clusterStagedUpdateRun", updateRunRef) return false, fmt.Errorf("%w: %s", errStagedUpdatedAborted, unexpectedErr.Error()) } - if !condition.IsConditionStatusTrue(meta.FindStatusCondition(approvalRequest.Status.Conditions, string(placementv1beta1.ApprovalRequestConditionApproved)), approvalRequest.Generation) { + approvalAccepted := condition.IsConditionStatusTrue(meta.FindStatusCondition(approvalRequest.Status.Conditions, string(placementv1beta1.ApprovalRequestConditionApprovalAccepted)), approvalRequest.Generation) + // Approved state should not change once the approval is accepted. + if !approvalAccepted && !condition.IsConditionStatusTrue(meta.FindStatusCondition(approvalRequest.Status.Conditions, string(placementv1beta1.ApprovalRequestConditionApproved)), approvalRequest.Generation) { klog.V(2).InfoS("The approval request has not been approved yet", "approvalRequestTask", requestRef, "stage", updatingStage.Name, "clusterStagedUpdateRun", updateRunRef) return false, nil } klog.V(2).InfoS("The approval request has been approved", "approvalRequestTask", requestRef, "stage", updatingStage.Name, "clusterStagedUpdateRun", updateRunRef) + if !approvalAccepted { + if err := r.updateApprovalRequestAccepted(ctx, &approvalRequest); err != nil { + klog.ErrorS(err, "Failed to accept the approved approval request", "approvalRequest", requestRef, "stage", updatingStage.Name, "clusterStagedUpdateRun", updateRunRef) + // retriable err + return false, err + } + } markAfterStageRequestApproved(&updatingStageStatus.AfterStageTaskStatus[i], updateRun.Generation) } else { // retriable error @@ -349,6 +358,23 @@ func (r *Reconciler) updateBindingRolloutStarted(ctx context.Context, binding *p return nil } +// updateApprovalRequestAccepted updates the *approved* clusterApprovalRequest status to indicate the approval accepted. +func (r *Reconciler) updateApprovalRequestAccepted(ctx context.Context, appReq *placementv1beta1.ClusterApprovalRequest) error { + cond := metav1.Condition{ + Type: string(placementv1beta1.ApprovalRequestConditionApprovalAccepted), + Status: metav1.ConditionTrue, + ObservedGeneration: appReq.Generation, + Reason: condition.ApprovalRequestApprovalAcceptedReason, + } + meta.SetStatusCondition(&appReq.Status.Conditions, cond) + if err := r.Client.Status().Update(ctx, appReq); err != nil { + klog.ErrorS(err, "Failed to update approval request status", "clusterApprovalRequest", klog.KObj(appReq), "condition", cond) + return controller.NewUpdateIgnoreConflictError(err) + } + klog.V(2).InfoS("Updated binding as rolloutStarted", "clusterApprovalRequest", klog.KObj(appReq), "condition", cond) + return nil +} + // isBindingSyncedWithClusterStatus checks if the binding is up-to-date with the cluster status. func isBindingSyncedWithClusterStatus(resourceSnapshotName string, updateRun *placementv1beta1.ClusterStagedUpdateRun, binding *placementv1beta1.ClusterResourceBinding, cluster *placementv1beta1.ClusterUpdatingStatus) bool { if binding.Spec.ResourceSnapshotName != resourceSnapshotName { diff --git a/pkg/controllers/updaterun/execution_integration_test.go b/pkg/controllers/updaterun/execution_integration_test.go index d9c9c2dd1..4160f12dc 100644 --- a/pkg/controllers/updaterun/execution_integration_test.go +++ b/pkg/controllers/updaterun/execution_integration_test.go @@ -413,6 +413,14 @@ var _ = Describe("UpdateRun execution tests", func() { wantStatus.DeletionStageStatus.Clusters[i].Conditions = append(wantStatus.DeletionStageStatus.Clusters[i].Conditions, generateTrueCondition(updateRun, placementv1beta1.ClusterUpdatingConditionStarted)) } validateClusterStagedUpdateRunStatus(ctx, updateRun, wantStatus, "") + + By("Validating the approvalRequest has ApprovalAccepted status") + Eventually(func() (bool, error) { + if err := k8sClient.Get(ctx, types.NamespacedName{Name: wantApprovalRequest.Name}, approvalRequest); err != nil { + return false, err + } + return condition.IsConditionStatusTrue(meta.FindStatusCondition(approvalRequest.Status.Conditions, string(placementv1beta1.ApprovalRequestConditionApprovalAccepted)), approvalRequest.Generation), nil + }, timeout, interval).Should(BeTrue(), "failed to validate the approvalRequest approval accepted") }) It("Should delete all the clusterResourceBindings in the delete stage and complete the update run", func() { diff --git a/pkg/controllers/updaterun/initialization_integration_test.go b/pkg/controllers/updaterun/initialization_integration_test.go index a6c113ffc..405594c65 100644 --- a/pkg/controllers/updaterun/initialization_integration_test.go +++ b/pkg/controllers/updaterun/initialization_integration_test.go @@ -289,7 +289,7 @@ var _ = Describe("Updaterun initialization tests", func() { return err } if updateRun.Status.PolicySnapshotIndexUsed != policySnapshot.Labels[placementv1beta1.PolicyIndexLabel] { - return fmt.Errorf("updateRun status `PolicySnapshotIndexUsed` mismatch: got %s, want %s", updateRun.Status.PolicySnapshotIndexUsed, policySnapshot.Name) + return fmt.Errorf("updateRun status `PolicySnapshotIndexUsed` mismatch: got %s, want %s", updateRun.Status.PolicySnapshotIndexUsed, policySnapshot.Labels[placementv1beta1.PolicyIndexLabel]) } if updateRun.Status.PolicyObservedClusterCount != numberOfClustersAnnotation { return fmt.Errorf("updateRun status `PolicyObservedClusterCount` mismatch: got %d, want %d", updateRun.Status.PolicyObservedClusterCount, numberOfClustersAnnotation) @@ -322,7 +322,7 @@ var _ = Describe("Updaterun initialization tests", func() { return err } if updateRun.Status.PolicySnapshotIndexUsed != policySnapshot.Labels[placementv1beta1.PolicyIndexLabel] { - return fmt.Errorf("updateRun status `PolicySnapshotIndexUsed` mismatch: got %s, want %s", updateRun.Status.PolicySnapshotIndexUsed, policySnapshot.Name) + return fmt.Errorf("updateRun status `PolicySnapshotIndexUsed` mismatch: got %s, want %s", updateRun.Status.PolicySnapshotIndexUsed, policySnapshot.Labels[placementv1beta1.PolicyIndexLabel]) } if updateRun.Status.PolicyObservedClusterCount != 2 { return fmt.Errorf("updateRun status `PolicyObservedClusterCount` mismatch: got %d, want %d", updateRun.Status.PolicyObservedClusterCount, 2) @@ -354,7 +354,7 @@ var _ = Describe("Updaterun initialization tests", func() { return err } if updateRun.Status.PolicySnapshotIndexUsed != policySnapshot.Labels[placementv1beta1.PolicyIndexLabel] { - return fmt.Errorf("updateRun status `PolicySnapshotIndexUsed` mismatch: got %s, want %s", updateRun.Status.PolicySnapshotIndexUsed, policySnapshot.Name) + return fmt.Errorf("updateRun status `PolicySnapshotIndexUsed` mismatch: got %s, want %s", updateRun.Status.PolicySnapshotIndexUsed, policySnapshot.Labels[placementv1beta1.PolicyIndexLabel]) } if updateRun.Status.PolicyObservedClusterCount != -1 { return fmt.Errorf("updateRun status `PolicyObservedClusterCount` mismatch: got %d, want %d", updateRun.Status.PolicyObservedClusterCount, -1) diff --git a/pkg/utils/condition/condition.go b/pkg/utils/condition/condition.go index 6b7cb7369..56773dd19 100644 --- a/pkg/utils/condition/condition.go +++ b/pkg/utils/condition/condition.go @@ -155,6 +155,9 @@ const ( // AfterStageTaskWaitTimeElapsedReason is the reason string of condition if the wait time for after stage task has elapsed. AfterStageTaskWaitTimeElapsedReason = "AfterStageTaskWaitTimeElapsed" + + // ApprovalRequestApprovalAcceptedReason is the reason string of condition if the approval of the approval request has been accepted. + ApprovalRequestApprovalAcceptedReason = "ApprovalRequestApprovalAccepted" ) // A group of condition reason & message string which is used to populate the ClusterResourcePlacementEviction condition.