From 04f2acf3c661362261e3aa9fec74bf7a70e30763 Mon Sep 17 00:00:00 2001 From: Arvind Thirumurugan Date: Wed, 29 Oct 2025 15:14:57 -0700 Subject: [PATCH 01/20] interface: start/stop API for updateRun Signed-off-by: Arvind Thirumurugan --- ...025-10-29-1500-start-stop-updaterun-api.md | 104 ++++++++++++++++++ apis/placement/v1beta1/stageupdate_types.go | 17 ++- .../v1beta1/zz_generated.deepcopy.go | 9 +- ...etes-fleet.io_clusterstagedupdateruns.yaml | 12 +- ....kubernetes-fleet.io_stagedupdateruns.yaml | 12 +- 5 files changed, 149 insertions(+), 5 deletions(-) create mode 100644 .github/.copilot/breadcrumbs/2025-10-29-1500-start-stop-updaterun-api.md diff --git a/.github/.copilot/breadcrumbs/2025-10-29-1500-start-stop-updaterun-api.md b/.github/.copilot/breadcrumbs/2025-10-29-1500-start-stop-updaterun-api.md new file mode 100644 index 000000000..56e727414 --- /dev/null +++ b/.github/.copilot/breadcrumbs/2025-10-29-1500-start-stop-updaterun-api.md @@ -0,0 +1,104 @@ +# Start/Stop UpdateRun API Implementation + +## Date: 2025-10-29-1500 + +## Requirements + +**Primary Objective**: Implement the ability to start and stop an UpdateRun. Currently when we create an updateRun it immediately initializes and executes. Instead when an updateRun is created we initialize but won't execute until user starts the updateRun. And we want the ability to stop the updateRun and if resources are being propagated to a particular cluster when stopped it will complete propagation for the cluster and stop propagation for all other cluster in that stage. And when updateRun is started again it will continue where it left off. + +## Understanding Current Implementation + +From the codebase analysis: + +1. **Current Flow**: CreateUpdateRun → Initialize → Execute immediately +2. **Controller Logic**: Located in `/pkg/controllers/updaterun/controller.go` +3. **Main Reconcile Loop**: Calls `initialize()` then immediately `execute()` +4. **Conditions**: Uses "Initialized", "Progressing", "Succeeded" conditions +5. **Stage Execution**: Tracks per-stage and per-cluster status in `StageUpdatingStatus` + +## Implementation Plan + +### Phase 1: API Design - Add Start/Stop Controls +- [x] Task 1.1: Add `Started` field to UpdateRunSpec to control execution start +- [x] Task 1.2: Add new condition type `StagedUpdateRunConditionStarted` +- [x] Task 1.3: Update UpdateRunSpec validation to handle new field +- [x] Task 1.4: Update kubebuilder comments and validation tags + +### Phase 2: Controller Logic Updates +- [ ] Task 2.1: Modify controller.go reconcile loop to check Started field +- [ ] Task 2.2: Separate initialization from execution in controller flow +- [ ] Task 2.3: Add logic to mark UpdateRun as ready but not started after initialization +- [ ] Task 2.4: Handle start transition - when Started changes from false/nil to true +- [ ] Task 2.5: Implement stop transition - when Started changes from true to false + +### Phase 3: Graceful Stop Implementation +- [ ] Task 3.1: Track in-progress cluster updates during stop +- [ ] Task 3.2: Complete current cluster propagation before stopping +- [ ] Task 3.3: Mark stopped stage/clusters appropriately in status +- [ ] Task 3.4: Ensure resume from correct point when restarted + +### Phase 4: Status and Condition Management +- [ ] Task 4.1: Add Started condition management functions +- [ ] Task 4.2: Update condition progression: Initialize → Started → Progressing → Succeeded +- [ ] Task 4.3: Handle stop scenarios in condition updates +- [ ] Task 4.4: Update metrics to track start/stop events + +### Phase 5: Testing +- [ ] Task 5.1: Write unit tests for new spec field and conditions +- [ ] Task 5.2: Write integration tests for start/stop workflow +- [ ] Task 5.3: Write e2e tests for graceful stop and resume scenarios +- [ ] Task 5.4: Test edge cases (stop during cluster propagation, restart scenarios) + +### Phase 6: Documentation and Examples +- [ ] Task 6.1: Update API documentation +- [ ] Task 6.2: Add example YAML files showing start/stop usage +- [ ] Task 6.3: Update user guide with start/stop procedures + +## API Design + +### UpdateRunSpec Changes +```go +type UpdateRunSpec struct { + // ... existing fields ... + + // Started indicates whether the update run should be started. + // When false or nil, the update run will initialize but not execute. + // When true, the update run will begin execution. + // Changing from true to false will gracefully stop the update run. + // +kubebuilder:validation:Optional + Started *bool `json:"started,omitempty"` +} +``` + +### New Condition Type +```go +const ( + // ... existing conditions ... + + // StagedUpdateRunConditionStarted indicates whether the staged update run has been started. + // Its condition status can be one of the following: + // - "True": The staged update run has been started and is ready to progress. + // - "False": The staged update run is stopped or not yet started. + StagedUpdateRunConditionStarted StagedUpdateRunConditionType = "Started" +) +``` + +## Success Criteria + +1. **✅ Initialize Without Execute**: UpdateRun initializes successfully but waits for start signal +2. **✅ Start Control**: Setting `Started: true` begins execution from correct stage +3. **✅ Graceful Stop**: Setting `Started: false` completes current cluster and stops +4. **✅ Resume Capability**: Restarting continues from exact stopping point +5. **✅ Proper Conditions**: All condition transitions work correctly +6. **✅ Backward Compatibility**: Existing UpdateRuns continue to work (default to started) + +## Current Status: Phase 1 Complete ✅ + +**Completed**: +- Added `Started *bool` field to UpdateRunSpec with proper kubebuilder validation +- Added `StagedUpdateRunConditionStarted` condition type with proper documentation +- Updated kubebuilder printcolumn annotations to include Started condition in kubectl output +- Updated condition documentation to include "Started" in known conditions list +- Generated CRDs successfully with new API changes + +**Next**: Ready to proceed with Phase 2 - Controller Logic Updates \ No newline at end of file diff --git a/apis/placement/v1beta1/stageupdate_types.go b/apis/placement/v1beta1/stageupdate_types.go index db72de9d5..1dcd8530e 100644 --- a/apis/placement/v1beta1/stageupdate_types.go +++ b/apis/placement/v1beta1/stageupdate_types.go @@ -92,6 +92,7 @@ type UpdateRunObjList interface { // +kubebuilder:printcolumn:JSONPath=`.spec.resourceSnapshotIndex`,name="Resource-Snapshot-Index",type=string // +kubebuilder:printcolumn:JSONPath=`.status.policySnapshotIndexUsed`,name="Policy-Snapshot-Index",type=string // +kubebuilder:printcolumn:JSONPath=`.status.conditions[?(@.type=="Initialized")].status`,name="Initialized",type=string +// +kubebuilder:printcolumn:JSONPath=`.status.conditions[?(@.type=="Started")].status`,name="Started",type=string // +kubebuilder:printcolumn:JSONPath=`.status.conditions[?(@.type=="Succeeded")].status`,name="Succeeded",type=string // +kubebuilder:printcolumn:JSONPath=`.metadata.creationTimestamp`,name="Age",type=date // +kubebuilder:printcolumn:JSONPath=`.spec.stagedRolloutStrategyName`,name="Strategy",priority=1,type=string @@ -168,6 +169,13 @@ type UpdateRunSpec struct { // and recorded in the status field. // +kubebuilder:validation:Required StagedUpdateStrategyName string `json:"stagedRolloutStrategyName"` + + // Started indicates whether the update run should be started. + // When false or nil, the update run will initialize but not execute. + // When true, the update run will begin execution. + // Changing from true to false will gracefully stop the update run. + // +kubebuilder:validation:Optional + Started *bool `json:"started,omitempty"` } // UpdateStrategySpecGetterSetter offers the functionality to work with UpdateStrategySpec. @@ -366,7 +374,7 @@ type UpdateRunStatus struct { // +listMapKey=type // // Conditions is an array of current observed conditions for StagedUpdateRun. - // Known conditions are "Initialized", "Progressing", "Succeeded". + // Known conditions are "Initialized", "Started", "Progressing", "Succeeded". // +kubebuilder:validation:Optional Conditions []metav1.Condition `json:"conditions,omitempty"` } @@ -384,6 +392,12 @@ const ( // - "Unknown": The staged update run initialization has started. StagedUpdateRunConditionInitialized StagedUpdateRunConditionType = "Initialized" + // StagedUpdateRunConditionStarted indicates whether the staged update run has been started. + // Its condition status can be one of the following: + // - "True": The staged update run has been started and is ready to progress. + // - "False": The staged update run is stopped or not yet started. + StagedUpdateRunConditionStarted StagedUpdateRunConditionType = "Started" + // StagedUpdateRunConditionProgressing indicates whether the staged update run is making progress. // Its condition status can be one of the following: // - "True": The staged update run is making progress. @@ -746,6 +760,7 @@ func (c *ClusterApprovalRequestList) GetApprovalRequestObjs() []ApprovalRequestO // +kubebuilder:printcolumn:JSONPath=`.spec.resourceSnapshotIndex`,name="Resource-Snapshot-Index",type=string // +kubebuilder:printcolumn:JSONPath=`.status.policySnapshotIndexUsed`,name="Policy-Snapshot-Index",type=string // +kubebuilder:printcolumn:JSONPath=`.status.conditions[?(@.type=="Initialized")].status`,name="Initialized",type=string +// +kubebuilder:printcolumn:JSONPath=`.status.conditions[?(@.type=="Started")].status`,name="Started",type=string // +kubebuilder:printcolumn:JSONPath=`.status.conditions[?(@.type=="Succeeded")].status`,name="Succeeded",type=string // +kubebuilder:printcolumn:JSONPath=`.metadata.creationTimestamp`,name="Age",type=date // +kubebuilder:printcolumn:JSONPath=`.spec.stagedRolloutStrategyName`,name="Strategy",priority=1,type=string diff --git a/apis/placement/v1beta1/zz_generated.deepcopy.go b/apis/placement/v1beta1/zz_generated.deepcopy.go index 73d66c8fa..4dcbbf8d0 100644 --- a/apis/placement/v1beta1/zz_generated.deepcopy.go +++ b/apis/placement/v1beta1/zz_generated.deepcopy.go @@ -1122,7 +1122,7 @@ func (in *ClusterStagedUpdateRun) DeepCopyInto(out *ClusterStagedUpdateRun) { *out = *in out.TypeMeta = in.TypeMeta in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) - out.Spec = in.Spec + in.Spec.DeepCopyInto(&out.Spec) in.Status.DeepCopyInto(&out.Status) } @@ -2828,7 +2828,7 @@ func (in *StagedUpdateRun) DeepCopyInto(out *StagedUpdateRun) { *out = *in out.TypeMeta = in.TypeMeta in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) - out.Spec = in.Spec + in.Spec.DeepCopyInto(&out.Spec) in.Status.DeepCopyInto(&out.Status) } @@ -2978,6 +2978,11 @@ func (in *TopologySpreadConstraint) DeepCopy() *TopologySpreadConstraint { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *UpdateRunSpec) DeepCopyInto(out *UpdateRunSpec) { *out = *in + if in.Started != nil { + in, out := &in.Started, &out.Started + *out = new(bool) + **out = **in + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new UpdateRunSpec. diff --git a/config/crd/bases/placement.kubernetes-fleet.io_clusterstagedupdateruns.yaml b/config/crd/bases/placement.kubernetes-fleet.io_clusterstagedupdateruns.yaml index 3102c7f53..6e9897364 100644 --- a/config/crd/bases/placement.kubernetes-fleet.io_clusterstagedupdateruns.yaml +++ b/config/crd/bases/placement.kubernetes-fleet.io_clusterstagedupdateruns.yaml @@ -1112,6 +1112,9 @@ spec: - jsonPath: .status.conditions[?(@.type=="Initialized")].status name: Initialized type: string + - jsonPath: .status.conditions[?(@.type=="Started")].status + name: Started + type: string - jsonPath: .status.conditions[?(@.type=="Succeeded")].status name: Succeeded type: string @@ -1173,6 +1176,13 @@ spec: are computed according to the referenced strategy when the update run starts and recorded in the status field. type: string + started: + description: |- + Started indicates whether the update run should be started. + When false or nil, the update run will initialize but not execute. + When true, the update run will begin execution. + Changing from true to false will gracefully stop the update run. + type: boolean required: - placementName - resourceSnapshotIndex @@ -1416,7 +1426,7 @@ spec: conditions: description: |- Conditions is an array of current observed conditions for StagedUpdateRun. - Known conditions are "Initialized", "Progressing", "Succeeded". + Known conditions are "Initialized", "Started", "Progressing", "Succeeded". items: description: Condition contains details for one aspect of the current state of this API Resource. diff --git a/config/crd/bases/placement.kubernetes-fleet.io_stagedupdateruns.yaml b/config/crd/bases/placement.kubernetes-fleet.io_stagedupdateruns.yaml index ec319411f..fe5039b81 100644 --- a/config/crd/bases/placement.kubernetes-fleet.io_stagedupdateruns.yaml +++ b/config/crd/bases/placement.kubernetes-fleet.io_stagedupdateruns.yaml @@ -32,6 +32,9 @@ spec: - jsonPath: .status.conditions[?(@.type=="Initialized")].status name: Initialized type: string + - jsonPath: .status.conditions[?(@.type=="Started")].status + name: Started + type: string - jsonPath: .status.conditions[?(@.type=="Succeeded")].status name: Succeeded type: string @@ -92,6 +95,13 @@ spec: are computed according to the referenced strategy when the update run starts and recorded in the status field. type: string + started: + description: |- + Started indicates whether the update run should be started. + When false or nil, the update run will initialize but not execute. + When true, the update run will begin execution. + Changing from true to false will gracefully stop the update run. + type: boolean required: - placementName - resourceSnapshotIndex @@ -335,7 +345,7 @@ spec: conditions: description: |- Conditions is an array of current observed conditions for StagedUpdateRun. - Known conditions are "Initialized", "Progressing", "Succeeded". + Known conditions are "Initialized", "Started", "Progressing", "Succeeded". items: description: Condition contains details for one aspect of the current state of this API Resource. From 23516b562612533f72b1a94d9dd33905090266a8 Mon Sep 17 00:00:00 2001 From: Arvind Thirumurugan Date: Thu, 30 Oct 2025 12:49:35 -0700 Subject: [PATCH 02/20] address comments Signed-off-by: Arvind Thirumurugan --- ...025-10-29-1500-start-stop-updaterun-api.md | 104 ------------------ apis/placement/v1beta1/stageupdate_types.go | 22 +++- .../v1beta1/zz_generated.deepcopy.go | 6 +- ...etes-fleet.io_clusterstagedupdateruns.yaml | 16 +-- ....kubernetes-fleet.io_stagedupdateruns.yaml | 16 +-- 5 files changed, 38 insertions(+), 126 deletions(-) delete mode 100644 .github/.copilot/breadcrumbs/2025-10-29-1500-start-stop-updaterun-api.md diff --git a/.github/.copilot/breadcrumbs/2025-10-29-1500-start-stop-updaterun-api.md b/.github/.copilot/breadcrumbs/2025-10-29-1500-start-stop-updaterun-api.md deleted file mode 100644 index 56e727414..000000000 --- a/.github/.copilot/breadcrumbs/2025-10-29-1500-start-stop-updaterun-api.md +++ /dev/null @@ -1,104 +0,0 @@ -# Start/Stop UpdateRun API Implementation - -## Date: 2025-10-29-1500 - -## Requirements - -**Primary Objective**: Implement the ability to start and stop an UpdateRun. Currently when we create an updateRun it immediately initializes and executes. Instead when an updateRun is created we initialize but won't execute until user starts the updateRun. And we want the ability to stop the updateRun and if resources are being propagated to a particular cluster when stopped it will complete propagation for the cluster and stop propagation for all other cluster in that stage. And when updateRun is started again it will continue where it left off. - -## Understanding Current Implementation - -From the codebase analysis: - -1. **Current Flow**: CreateUpdateRun → Initialize → Execute immediately -2. **Controller Logic**: Located in `/pkg/controllers/updaterun/controller.go` -3. **Main Reconcile Loop**: Calls `initialize()` then immediately `execute()` -4. **Conditions**: Uses "Initialized", "Progressing", "Succeeded" conditions -5. **Stage Execution**: Tracks per-stage and per-cluster status in `StageUpdatingStatus` - -## Implementation Plan - -### Phase 1: API Design - Add Start/Stop Controls -- [x] Task 1.1: Add `Started` field to UpdateRunSpec to control execution start -- [x] Task 1.2: Add new condition type `StagedUpdateRunConditionStarted` -- [x] Task 1.3: Update UpdateRunSpec validation to handle new field -- [x] Task 1.4: Update kubebuilder comments and validation tags - -### Phase 2: Controller Logic Updates -- [ ] Task 2.1: Modify controller.go reconcile loop to check Started field -- [ ] Task 2.2: Separate initialization from execution in controller flow -- [ ] Task 2.3: Add logic to mark UpdateRun as ready but not started after initialization -- [ ] Task 2.4: Handle start transition - when Started changes from false/nil to true -- [ ] Task 2.5: Implement stop transition - when Started changes from true to false - -### Phase 3: Graceful Stop Implementation -- [ ] Task 3.1: Track in-progress cluster updates during stop -- [ ] Task 3.2: Complete current cluster propagation before stopping -- [ ] Task 3.3: Mark stopped stage/clusters appropriately in status -- [ ] Task 3.4: Ensure resume from correct point when restarted - -### Phase 4: Status and Condition Management -- [ ] Task 4.1: Add Started condition management functions -- [ ] Task 4.2: Update condition progression: Initialize → Started → Progressing → Succeeded -- [ ] Task 4.3: Handle stop scenarios in condition updates -- [ ] Task 4.4: Update metrics to track start/stop events - -### Phase 5: Testing -- [ ] Task 5.1: Write unit tests for new spec field and conditions -- [ ] Task 5.2: Write integration tests for start/stop workflow -- [ ] Task 5.3: Write e2e tests for graceful stop and resume scenarios -- [ ] Task 5.4: Test edge cases (stop during cluster propagation, restart scenarios) - -### Phase 6: Documentation and Examples -- [ ] Task 6.1: Update API documentation -- [ ] Task 6.2: Add example YAML files showing start/stop usage -- [ ] Task 6.3: Update user guide with start/stop procedures - -## API Design - -### UpdateRunSpec Changes -```go -type UpdateRunSpec struct { - // ... existing fields ... - - // Started indicates whether the update run should be started. - // When false or nil, the update run will initialize but not execute. - // When true, the update run will begin execution. - // Changing from true to false will gracefully stop the update run. - // +kubebuilder:validation:Optional - Started *bool `json:"started,omitempty"` -} -``` - -### New Condition Type -```go -const ( - // ... existing conditions ... - - // StagedUpdateRunConditionStarted indicates whether the staged update run has been started. - // Its condition status can be one of the following: - // - "True": The staged update run has been started and is ready to progress. - // - "False": The staged update run is stopped or not yet started. - StagedUpdateRunConditionStarted StagedUpdateRunConditionType = "Started" -) -``` - -## Success Criteria - -1. **✅ Initialize Without Execute**: UpdateRun initializes successfully but waits for start signal -2. **✅ Start Control**: Setting `Started: true` begins execution from correct stage -3. **✅ Graceful Stop**: Setting `Started: false` completes current cluster and stops -4. **✅ Resume Capability**: Restarting continues from exact stopping point -5. **✅ Proper Conditions**: All condition transitions work correctly -6. **✅ Backward Compatibility**: Existing UpdateRuns continue to work (default to started) - -## Current Status: Phase 1 Complete ✅ - -**Completed**: -- Added `Started *bool` field to UpdateRunSpec with proper kubebuilder validation -- Added `StagedUpdateRunConditionStarted` condition type with proper documentation -- Updated kubebuilder printcolumn annotations to include Started condition in kubectl output -- Updated condition documentation to include "Started" in known conditions list -- Generated CRDs successfully with new API changes - -**Next**: Ready to proceed with Phase 2 - Controller Logic Updates \ No newline at end of file diff --git a/apis/placement/v1beta1/stageupdate_types.go b/apis/placement/v1beta1/stageupdate_types.go index 1dcd8530e..4f36ee836 100644 --- a/apis/placement/v1beta1/stageupdate_types.go +++ b/apis/placement/v1beta1/stageupdate_types.go @@ -148,6 +148,18 @@ func (c *ClusterStagedUpdateRun) SetUpdateRunStatus(status UpdateRunStatus) { c.Status = status } +// UpdateRunExecutionState represents the desired execution state of an update run. +// +enum +type UpdateRunExecutionState string + +const ( + // UpdateRunExecutionStateStarted indicates the update run should be started and actively executing. + UpdateRunExecutionStateStarted UpdateRunExecutionState = "Started" + + // UpdateRunExecutionStateStopped indicates the update run should be stopped and not executing. + UpdateRunExecutionStateStopped UpdateRunExecutionState = "Stopped" +) + // UpdateRunSpec defines the desired rollout strategy and the snapshot indices of the resources to be updated. // It specifies a stage-by-stage update process across selected clusters for the given ResourcePlacement object. type UpdateRunSpec struct { @@ -170,12 +182,12 @@ type UpdateRunSpec struct { // +kubebuilder:validation:Required StagedUpdateStrategyName string `json:"stagedRolloutStrategyName"` - // Started indicates whether the update run should be started. - // When false or nil, the update run will initialize but not execute. - // When true, the update run will begin execution. - // Changing from true to false will gracefully stop the update run. + // ExecutionState indicates the desired execution state of the update run. + // When nil or "Stopped", the update run will initialize but not execute. + // When "Started", the update run will begin or continue execution. // +kubebuilder:validation:Optional - Started *bool `json:"started,omitempty"` + // +kubebuilder:validation:Enum=Started;Stopped + ExecutionState *UpdateRunExecutionState `json:"executionState,omitempty"` } // UpdateStrategySpecGetterSetter offers the functionality to work with UpdateStrategySpec. diff --git a/apis/placement/v1beta1/zz_generated.deepcopy.go b/apis/placement/v1beta1/zz_generated.deepcopy.go index 4dcbbf8d0..fe0f352dc 100644 --- a/apis/placement/v1beta1/zz_generated.deepcopy.go +++ b/apis/placement/v1beta1/zz_generated.deepcopy.go @@ -2978,9 +2978,9 @@ func (in *TopologySpreadConstraint) DeepCopy() *TopologySpreadConstraint { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *UpdateRunSpec) DeepCopyInto(out *UpdateRunSpec) { *out = *in - if in.Started != nil { - in, out := &in.Started, &out.Started - *out = new(bool) + if in.ExecutionState != nil { + in, out := &in.ExecutionState, &out.ExecutionState + *out = new(UpdateRunExecutionState) **out = **in } } diff --git a/config/crd/bases/placement.kubernetes-fleet.io_clusterstagedupdateruns.yaml b/config/crd/bases/placement.kubernetes-fleet.io_clusterstagedupdateruns.yaml index 6e9897364..c19a4f3a3 100644 --- a/config/crd/bases/placement.kubernetes-fleet.io_clusterstagedupdateruns.yaml +++ b/config/crd/bases/placement.kubernetes-fleet.io_clusterstagedupdateruns.yaml @@ -1157,6 +1157,15 @@ spec: description: The desired state of ClusterStagedUpdateRun. The spec is immutable. properties: + executionState: + description: |- + ExecutionState indicates the desired execution state of the update run. + When nil or "Stopped", the update run will initialize but not execute. + When "Started", the update run will begin or continue execution. + enum: + - Started + - Stopped + type: string placementName: description: |- PlacementName is the name of placement that this update run is applied to. @@ -1176,13 +1185,6 @@ spec: are computed according to the referenced strategy when the update run starts and recorded in the status field. type: string - started: - description: |- - Started indicates whether the update run should be started. - When false or nil, the update run will initialize but not execute. - When true, the update run will begin execution. - Changing from true to false will gracefully stop the update run. - type: boolean required: - placementName - resourceSnapshotIndex diff --git a/config/crd/bases/placement.kubernetes-fleet.io_stagedupdateruns.yaml b/config/crd/bases/placement.kubernetes-fleet.io_stagedupdateruns.yaml index fe5039b81..71743f3ea 100644 --- a/config/crd/bases/placement.kubernetes-fleet.io_stagedupdateruns.yaml +++ b/config/crd/bases/placement.kubernetes-fleet.io_stagedupdateruns.yaml @@ -76,6 +76,15 @@ spec: spec: description: The desired state of StagedUpdateRun. The spec is immutable. properties: + executionState: + description: |- + ExecutionState indicates the desired execution state of the update run. + When nil or "Stopped", the update run will initialize but not execute. + When "Started", the update run will begin or continue execution. + enum: + - Started + - Stopped + type: string placementName: description: |- PlacementName is the name of placement that this update run is applied to. @@ -95,13 +104,6 @@ spec: are computed according to the referenced strategy when the update run starts and recorded in the status field. type: string - started: - description: |- - Started indicates whether the update run should be started. - When false or nil, the update run will initialize but not execute. - When true, the update run will begin execution. - Changing from true to false will gracefully stop the update run. - type: boolean required: - placementName - resourceSnapshotIndex From 68402ec32a9e1f06008ee8c0b8902bcbc7bcfdd9 Mon Sep 17 00:00:00 2001 From: Arvind Thirumurugan Date: Thu, 30 Oct 2025 14:26:10 -0700 Subject: [PATCH 03/20] address comments Signed-off-by: Arvind Thirumurugan --- apis/placement/v1beta1/stageupdate_types.go | 25 ++++++++++--------- .../v1beta1/zz_generated.deepcopy.go | 9 ++----- ...etes-fleet.io_clusterstagedupdateruns.yaml | 20 ++++++++------- ....kubernetes-fleet.io_stagedupdateruns.yaml | 20 ++++++++------- 4 files changed, 37 insertions(+), 37 deletions(-) diff --git a/apis/placement/v1beta1/stageupdate_types.go b/apis/placement/v1beta1/stageupdate_types.go index 4f36ee836..2f21b0e69 100644 --- a/apis/placement/v1beta1/stageupdate_types.go +++ b/apis/placement/v1beta1/stageupdate_types.go @@ -148,16 +148,16 @@ func (c *ClusterStagedUpdateRun) SetUpdateRunStatus(status UpdateRunStatus) { c.Status = status } -// UpdateRunExecutionState represents the desired execution state of an update run. +// State represents the desired state of an update run. // +enum -type UpdateRunExecutionState string +type State string const ( - // UpdateRunExecutionStateStarted indicates the update run should be started and actively executing. - UpdateRunExecutionStateStarted UpdateRunExecutionState = "Started" + // StateStart indicates the update run should be started and actively executing. + StateStart State = "Start" - // UpdateRunExecutionStateStopped indicates the update run should be stopped and not executing. - UpdateRunExecutionStateStopped UpdateRunExecutionState = "Stopped" + // StateStop indicates the update run should be stopped and not executing. + StateStop State = "Stop" ) // UpdateRunSpec defines the desired rollout strategy and the snapshot indices of the resources to be updated. @@ -182,12 +182,13 @@ type UpdateRunSpec struct { // +kubebuilder:validation:Required StagedUpdateStrategyName string `json:"stagedRolloutStrategyName"` - // ExecutionState indicates the desired execution state of the update run. - // When nil or "Stopped", the update run will initialize but not execute. - // When "Started", the update run will begin or continue execution. - // +kubebuilder:validation:Optional - // +kubebuilder:validation:Enum=Started;Stopped - ExecutionState *UpdateRunExecutionState `json:"executionState,omitempty"` + // State indicates the desired state of the update run. + // When "Stop", the update run will initialize but not execute. + // When "Start", the update run will begin or continue execution. + // +kubebuilder:validation:Required + // +kubebuilder:validation:Enum=Start;Stop + // +kubebuilder:default="Stop" + State State `json:"state"` } // UpdateStrategySpecGetterSetter offers the functionality to work with UpdateStrategySpec. diff --git a/apis/placement/v1beta1/zz_generated.deepcopy.go b/apis/placement/v1beta1/zz_generated.deepcopy.go index fe0f352dc..73d66c8fa 100644 --- a/apis/placement/v1beta1/zz_generated.deepcopy.go +++ b/apis/placement/v1beta1/zz_generated.deepcopy.go @@ -1122,7 +1122,7 @@ func (in *ClusterStagedUpdateRun) DeepCopyInto(out *ClusterStagedUpdateRun) { *out = *in out.TypeMeta = in.TypeMeta in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) - in.Spec.DeepCopyInto(&out.Spec) + out.Spec = in.Spec in.Status.DeepCopyInto(&out.Status) } @@ -2828,7 +2828,7 @@ func (in *StagedUpdateRun) DeepCopyInto(out *StagedUpdateRun) { *out = *in out.TypeMeta = in.TypeMeta in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) - in.Spec.DeepCopyInto(&out.Spec) + out.Spec = in.Spec in.Status.DeepCopyInto(&out.Status) } @@ -2978,11 +2978,6 @@ func (in *TopologySpreadConstraint) DeepCopy() *TopologySpreadConstraint { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *UpdateRunSpec) DeepCopyInto(out *UpdateRunSpec) { *out = *in - if in.ExecutionState != nil { - in, out := &in.ExecutionState, &out.ExecutionState - *out = new(UpdateRunExecutionState) - **out = **in - } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new UpdateRunSpec. diff --git a/config/crd/bases/placement.kubernetes-fleet.io_clusterstagedupdateruns.yaml b/config/crd/bases/placement.kubernetes-fleet.io_clusterstagedupdateruns.yaml index c19a4f3a3..cf3320b48 100644 --- a/config/crd/bases/placement.kubernetes-fleet.io_clusterstagedupdateruns.yaml +++ b/config/crd/bases/placement.kubernetes-fleet.io_clusterstagedupdateruns.yaml @@ -1157,15 +1157,6 @@ spec: description: The desired state of ClusterStagedUpdateRun. The spec is immutable. properties: - executionState: - description: |- - ExecutionState indicates the desired execution state of the update run. - When nil or "Stopped", the update run will initialize but not execute. - When "Started", the update run will begin or continue execution. - enum: - - Started - - Stopped - type: string placementName: description: |- PlacementName is the name of placement that this update run is applied to. @@ -1185,10 +1176,21 @@ spec: are computed according to the referenced strategy when the update run starts and recorded in the status field. type: string + state: + default: Stop + description: |- + State indicates the desired state of the update run. + When "Stop", the update run will initialize but not execute. + When "Start", the update run will begin or continue execution. + enum: + - Start + - Stop + type: string required: - placementName - resourceSnapshotIndex - stagedRolloutStrategyName + - state type: object x-kubernetes-validations: - message: The spec field is immutable diff --git a/config/crd/bases/placement.kubernetes-fleet.io_stagedupdateruns.yaml b/config/crd/bases/placement.kubernetes-fleet.io_stagedupdateruns.yaml index 71743f3ea..070b6aa96 100644 --- a/config/crd/bases/placement.kubernetes-fleet.io_stagedupdateruns.yaml +++ b/config/crd/bases/placement.kubernetes-fleet.io_stagedupdateruns.yaml @@ -76,15 +76,6 @@ spec: spec: description: The desired state of StagedUpdateRun. The spec is immutable. properties: - executionState: - description: |- - ExecutionState indicates the desired execution state of the update run. - When nil or "Stopped", the update run will initialize but not execute. - When "Started", the update run will begin or continue execution. - enum: - - Started - - Stopped - type: string placementName: description: |- PlacementName is the name of placement that this update run is applied to. @@ -104,10 +95,21 @@ spec: are computed according to the referenced strategy when the update run starts and recorded in the status field. type: string + state: + default: Stop + description: |- + State indicates the desired state of the update run. + When "Stop", the update run will initialize but not execute. + When "Start", the update run will begin or continue execution. + enum: + - Start + - Stop + type: string required: - placementName - resourceSnapshotIndex - stagedRolloutStrategyName + - state type: object x-kubernetes-validations: - message: The spec field is immutable From 7f93790d4d9e880ffaa007d835e2114f770024b9 Mon Sep 17 00:00:00 2001 From: Arvind Thirumurugan Date: Thu, 30 Oct 2025 15:33:08 -0700 Subject: [PATCH 04/20] minor fix Signed-off-by: Arvind Thirumurugan --- apis/placement/v1beta1/stageupdate_types.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/apis/placement/v1beta1/stageupdate_types.go b/apis/placement/v1beta1/stageupdate_types.go index 2f21b0e69..729a736a4 100644 --- a/apis/placement/v1beta1/stageupdate_types.go +++ b/apis/placement/v1beta1/stageupdate_types.go @@ -185,9 +185,8 @@ type UpdateRunSpec struct { // State indicates the desired state of the update run. // When "Stop", the update run will initialize but not execute. // When "Start", the update run will begin or continue execution. - // +kubebuilder:validation:Required // +kubebuilder:validation:Enum=Start;Stop - // +kubebuilder:default="Stop" + // +kubebuilder:default=Stop State State `json:"state"` } From 439506031e2751a6dc2d6c1964ee4a61d713f212 Mon Sep 17 00:00:00 2001 From: Arvind Thirumurugan Date: Thu, 30 Oct 2025 17:32:43 -0700 Subject: [PATCH 05/20] minor fixes Signed-off-by: Arvind Thirumurugan --- apis/placement/v1beta1/stageupdate_types.go | 1 + .../placement.kubernetes-fleet.io_clusterstagedupdateruns.yaml | 1 - .../bases/placement.kubernetes-fleet.io_stagedupdateruns.yaml | 1 - pkg/controllers/updaterun/controller_integration_test.go | 1 + test/e2e/cluster_staged_updaterun_test.go | 1 + test/e2e/staged_updaterun_test.go | 1 + 6 files changed, 4 insertions(+), 2 deletions(-) diff --git a/apis/placement/v1beta1/stageupdate_types.go b/apis/placement/v1beta1/stageupdate_types.go index 729a736a4..b39aa0ff8 100644 --- a/apis/placement/v1beta1/stageupdate_types.go +++ b/apis/placement/v1beta1/stageupdate_types.go @@ -185,6 +185,7 @@ type UpdateRunSpec struct { // State indicates the desired state of the update run. // When "Stop", the update run will initialize but not execute. // When "Start", the update run will begin or continue execution. + // +kubebuilder:validation:Optional // +kubebuilder:validation:Enum=Start;Stop // +kubebuilder:default=Stop State State `json:"state"` diff --git a/config/crd/bases/placement.kubernetes-fleet.io_clusterstagedupdateruns.yaml b/config/crd/bases/placement.kubernetes-fleet.io_clusterstagedupdateruns.yaml index cf3320b48..cb7bc7cd9 100644 --- a/config/crd/bases/placement.kubernetes-fleet.io_clusterstagedupdateruns.yaml +++ b/config/crd/bases/placement.kubernetes-fleet.io_clusterstagedupdateruns.yaml @@ -1190,7 +1190,6 @@ spec: - placementName - resourceSnapshotIndex - stagedRolloutStrategyName - - state type: object x-kubernetes-validations: - message: The spec field is immutable diff --git a/config/crd/bases/placement.kubernetes-fleet.io_stagedupdateruns.yaml b/config/crd/bases/placement.kubernetes-fleet.io_stagedupdateruns.yaml index 070b6aa96..7d2ed18a3 100644 --- a/config/crd/bases/placement.kubernetes-fleet.io_stagedupdateruns.yaml +++ b/config/crd/bases/placement.kubernetes-fleet.io_stagedupdateruns.yaml @@ -109,7 +109,6 @@ spec: - placementName - resourceSnapshotIndex - stagedRolloutStrategyName - - state type: object x-kubernetes-validations: - message: The spec field is immutable diff --git a/pkg/controllers/updaterun/controller_integration_test.go b/pkg/controllers/updaterun/controller_integration_test.go index 8c8e38d06..3d56afb3e 100644 --- a/pkg/controllers/updaterun/controller_integration_test.go +++ b/pkg/controllers/updaterun/controller_integration_test.go @@ -341,6 +341,7 @@ func generateTestClusterStagedUpdateRun() *placementv1beta1.ClusterStagedUpdateR PlacementName: testCRPName, ResourceSnapshotIndex: testResourceSnapshotIndex, StagedUpdateStrategyName: testUpdateStrategyName, + State: placementv1beta1.StateStart, }, } } diff --git a/test/e2e/cluster_staged_updaterun_test.go b/test/e2e/cluster_staged_updaterun_test.go index 9880c2eef..455544a85 100644 --- a/test/e2e/cluster_staged_updaterun_test.go +++ b/test/e2e/cluster_staged_updaterun_test.go @@ -1668,6 +1668,7 @@ func createClusterStagedUpdateRunSucceed(updateRunName, crpName, resourceSnapsho PlacementName: crpName, ResourceSnapshotIndex: resourceSnapshotIndex, StagedUpdateStrategyName: strategyName, + State: placementv1beta1.StateStart, }, } Expect(hubClient.Create(ctx, updateRun)).To(Succeed(), "Failed to create ClusterStagedUpdateRun %s", updateRunName) diff --git a/test/e2e/staged_updaterun_test.go b/test/e2e/staged_updaterun_test.go index b5a5428b1..66c1b5bca 100644 --- a/test/e2e/staged_updaterun_test.go +++ b/test/e2e/staged_updaterun_test.go @@ -1201,6 +1201,7 @@ func createStagedUpdateRunSucceed(updateRunName, namespace, rpName, resourceSnap PlacementName: rpName, ResourceSnapshotIndex: resourceSnapshotIndex, StagedUpdateStrategyName: strategyName, + State: placementv1beta1.StateStart, }, } Expect(hubClient.Create(ctx, updateRun)).To(Succeed(), "Failed to create StagedUpdateRun %s", updateRunName) From 46fd853102aa6352553f054c059bcc6cc533c3b9 Mon Sep 17 00:00:00 2001 From: Arvind Thirumurugan Date: Thu, 30 Oct 2025 17:49:31 -0700 Subject: [PATCH 06/20] lint fix Signed-off-by: Arvind Thirumurugan --- test/e2e/cluster_staged_updaterun_test.go | 2 +- test/e2e/staged_updaterun_test.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/test/e2e/cluster_staged_updaterun_test.go b/test/e2e/cluster_staged_updaterun_test.go index 455544a85..2f2e6e068 100644 --- a/test/e2e/cluster_staged_updaterun_test.go +++ b/test/e2e/cluster_staged_updaterun_test.go @@ -1668,7 +1668,7 @@ func createClusterStagedUpdateRunSucceed(updateRunName, crpName, resourceSnapsho PlacementName: crpName, ResourceSnapshotIndex: resourceSnapshotIndex, StagedUpdateStrategyName: strategyName, - State: placementv1beta1.StateStart, + State: placementv1beta1.StateStart, }, } Expect(hubClient.Create(ctx, updateRun)).To(Succeed(), "Failed to create ClusterStagedUpdateRun %s", updateRunName) diff --git a/test/e2e/staged_updaterun_test.go b/test/e2e/staged_updaterun_test.go index 66c1b5bca..ce924293d 100644 --- a/test/e2e/staged_updaterun_test.go +++ b/test/e2e/staged_updaterun_test.go @@ -1201,7 +1201,7 @@ func createStagedUpdateRunSucceed(updateRunName, namespace, rpName, resourceSnap PlacementName: rpName, ResourceSnapshotIndex: resourceSnapshotIndex, StagedUpdateStrategyName: strategyName, - State: placementv1beta1.StateStart, + State: placementv1beta1.StateStart, }, } Expect(hubClient.Create(ctx, updateRun)).To(Succeed(), "Failed to create StagedUpdateRun %s", updateRunName) From fa7bcfe6acde7174214e2698c15f6fc2873b782b Mon Sep 17 00:00:00 2001 From: Arvind Thirumurugan Date: Fri, 31 Oct 2025 11:10:20 -0700 Subject: [PATCH 07/20] fix IT Signed-off-by: Arvind Thirumurugan --- .../v1beta1/api_validation_integration_test.go | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/test/apis/placement/v1beta1/api_validation_integration_test.go b/test/apis/placement/v1beta1/api_validation_integration_test.go index d0260d0ff..ca2909fac 100644 --- a/test/apis/placement/v1beta1/api_validation_integration_test.go +++ b/test/apis/placement/v1beta1/api_validation_integration_test.go @@ -1116,24 +1116,30 @@ var _ = Describe("Test placement v1beta1 API validation", func() { }) }) - Context("Test ClusterStagedUpdateRun API validation - valid cases", func() { + FContext("Test ClusterStagedUpdateRun API validation - valid cases", func() { It("Should allow creation of ClusterStagedUpdateRun with valid name length", func() { updateRun := placementv1beta1.ClusterStagedUpdateRun{ ObjectMeta: metav1.ObjectMeta{ Name: fmt.Sprintf(validupdateRunNameTemplate, GinkgoParallelProcess()), }, + Spec: placementv1beta1.UpdateRunSpec{ + State: placementv1beta1.StateStart, + }, } Expect(hubClient.Create(ctx, &updateRun)).Should(Succeed()) Expect(hubClient.Delete(ctx, &updateRun)).Should(Succeed()) }) }) - Context("Test ClusterStagedUpdateRun API validation - invalid cases", func() { + FContext("Test ClusterStagedUpdateRun API validation - invalid cases", func() { It("Should deny creation of ClusterStagedUpdateRun with name length > 127", func() { updateRun := placementv1beta1.ClusterStagedUpdateRun{ ObjectMeta: metav1.ObjectMeta{ Name: fmt.Sprintf(invalidupdateRunNameTemplate, GinkgoParallelProcess()), }, + Spec: placementv1beta1.UpdateRunSpec{ + State: placementv1beta1.StateStart, + }, } err := hubClient.Create(ctx, &updateRun) var statusErr *k8sErrors.StatusError @@ -1148,6 +1154,7 @@ var _ = Describe("Test placement v1beta1 API validation", func() { }, Spec: placementv1beta1.UpdateRunSpec{ PlacementName: "test-placement", + State: placementv1beta1.StateStart, }, } Expect(hubClient.Create(ctx, &updateRun)).Should(Succeed()) From 32e66fd085ab5b5da83a2481ccc705f1d8a793e7 Mon Sep 17 00:00:00 2001 From: Arvind Thirumurugan Date: Tue, 4 Nov 2025 18:30:10 -0800 Subject: [PATCH 08/20] update API Signed-off-by: Arvind Thirumurugan --- apis/placement/v1beta1/stageupdate_types.go | 38 +++++++++++-------- ...etes-fleet.io_clusterstagedupdateruns.yaml | 16 +++++--- ....kubernetes-fleet.io_stagedupdateruns.yaml | 16 +++++--- 3 files changed, 43 insertions(+), 27 deletions(-) diff --git a/apis/placement/v1beta1/stageupdate_types.go b/apis/placement/v1beta1/stageupdate_types.go index b39aa0ff8..008925d15 100644 --- a/apis/placement/v1beta1/stageupdate_types.go +++ b/apis/placement/v1beta1/stageupdate_types.go @@ -92,7 +92,7 @@ type UpdateRunObjList interface { // +kubebuilder:printcolumn:JSONPath=`.spec.resourceSnapshotIndex`,name="Resource-Snapshot-Index",type=string // +kubebuilder:printcolumn:JSONPath=`.status.policySnapshotIndexUsed`,name="Policy-Snapshot-Index",type=string // +kubebuilder:printcolumn:JSONPath=`.status.conditions[?(@.type=="Initialized")].status`,name="Initialized",type=string -// +kubebuilder:printcolumn:JSONPath=`.status.conditions[?(@.type=="Started")].status`,name="Started",type=string +// +kubebuilder:printcolumn:JSONPath=`.status.conditions[?(@.type=="Progressing")].status`,name="Progressing",type=string // +kubebuilder:printcolumn:JSONPath=`.status.conditions[?(@.type=="Succeeded")].status`,name="Succeeded",type=string // +kubebuilder:printcolumn:JSONPath=`.metadata.creationTimestamp`,name="Age",type=date // +kubebuilder:printcolumn:JSONPath=`.spec.stagedRolloutStrategyName`,name="Strategy",priority=1,type=string @@ -153,11 +153,18 @@ func (c *ClusterStagedUpdateRun) SetUpdateRunStatus(status UpdateRunStatus) { type State string const ( - // StateStart indicates the update run should be started and actively executing. + // StateNotStarted indicates the update run has been initialized but execution has not started. + // This is the default state after initialization completes. + StateNotStarted State = "NotStarted" + + // StateStart indicates the update run should execute (or resume execution if paused). StateStart State = "Start" - // StateStop indicates the update run should be stopped and not executing. + // StateStop indicates the update run should pause execution. StateStop State = "Stop" + + // StateAbandon indicates the update run should be abandoned and terminated. + StateAbandon State = "Abandon" ) // UpdateRunSpec defines the desired rollout strategy and the snapshot indices of the resources to be updated. @@ -183,11 +190,13 @@ type UpdateRunSpec struct { StagedUpdateStrategyName string `json:"stagedRolloutStrategyName"` // State indicates the desired state of the update run. - // When "Stop", the update run will initialize but not execute. - // When "Start", the update run will begin or continue execution. + // NotStarted: The update run is initialized but execution has not started (default). + // Start: The update run should execute or resume execution. + // Stop: The update run should pause execution. + // Abandon: The update run should be abandoned and terminated. // +kubebuilder:validation:Optional - // +kubebuilder:validation:Enum=Start;Stop - // +kubebuilder:default=Stop + // +kubebuilder:validation:Enum=NotStarted;Start;Stop;Abandon + // +kubebuilder:default=NotStarted State State `json:"state"` } @@ -387,7 +396,7 @@ type UpdateRunStatus struct { // +listMapKey=type // // Conditions is an array of current observed conditions for StagedUpdateRun. - // Known conditions are "Initialized", "Started", "Progressing", "Succeeded". + // Known conditions are "Initialized", "Progressing", "Succeeded", "Abandoned". // +kubebuilder:validation:Optional Conditions []metav1.Condition `json:"conditions,omitempty"` } @@ -405,12 +414,6 @@ const ( // - "Unknown": The staged update run initialization has started. StagedUpdateRunConditionInitialized StagedUpdateRunConditionType = "Initialized" - // StagedUpdateRunConditionStarted indicates whether the staged update run has been started. - // Its condition status can be one of the following: - // - "True": The staged update run has been started and is ready to progress. - // - "False": The staged update run is stopped or not yet started. - StagedUpdateRunConditionStarted StagedUpdateRunConditionType = "Started" - // StagedUpdateRunConditionProgressing indicates whether the staged update run is making progress. // Its condition status can be one of the following: // - "True": The staged update run is making progress. @@ -423,6 +426,11 @@ const ( // - "True": The staged update run is completed successfully. // - "False": The staged update run encountered an error and stopped. StagedUpdateRunConditionSucceeded StagedUpdateRunConditionType = "Succeeded" + + // StagedUpdateRunConditionAbandoned indicates whether the staged update run has been abandoned. + // Its condition status can be one of the following: + // - "True": The staged update run has been abandoned by user request. + StagedUpdateRunConditionAbandoned StagedUpdateRunConditionType = "Abandoned" ) // StageUpdatingStatus defines the status of the update run in a stage. @@ -773,7 +781,7 @@ func (c *ClusterApprovalRequestList) GetApprovalRequestObjs() []ApprovalRequestO // +kubebuilder:printcolumn:JSONPath=`.spec.resourceSnapshotIndex`,name="Resource-Snapshot-Index",type=string // +kubebuilder:printcolumn:JSONPath=`.status.policySnapshotIndexUsed`,name="Policy-Snapshot-Index",type=string // +kubebuilder:printcolumn:JSONPath=`.status.conditions[?(@.type=="Initialized")].status`,name="Initialized",type=string -// +kubebuilder:printcolumn:JSONPath=`.status.conditions[?(@.type=="Started")].status`,name="Started",type=string +// +kubebuilder:printcolumn:JSONPath=`.status.conditions[?(@.type=="Progressing")].status`,name="Progressing",type=string // +kubebuilder:printcolumn:JSONPath=`.status.conditions[?(@.type=="Succeeded")].status`,name="Succeeded",type=string // +kubebuilder:printcolumn:JSONPath=`.metadata.creationTimestamp`,name="Age",type=date // +kubebuilder:printcolumn:JSONPath=`.spec.stagedRolloutStrategyName`,name="Strategy",priority=1,type=string diff --git a/config/crd/bases/placement.kubernetes-fleet.io_clusterstagedupdateruns.yaml b/config/crd/bases/placement.kubernetes-fleet.io_clusterstagedupdateruns.yaml index cb7bc7cd9..0ecbae458 100644 --- a/config/crd/bases/placement.kubernetes-fleet.io_clusterstagedupdateruns.yaml +++ b/config/crd/bases/placement.kubernetes-fleet.io_clusterstagedupdateruns.yaml @@ -1112,8 +1112,8 @@ spec: - jsonPath: .status.conditions[?(@.type=="Initialized")].status name: Initialized type: string - - jsonPath: .status.conditions[?(@.type=="Started")].status - name: Started + - jsonPath: .status.conditions[?(@.type=="Progressing")].status + name: Progressing type: string - jsonPath: .status.conditions[?(@.type=="Succeeded")].status name: Succeeded @@ -1177,14 +1177,18 @@ spec: and recorded in the status field. type: string state: - default: Stop + default: NotStarted description: |- State indicates the desired state of the update run. - When "Stop", the update run will initialize but not execute. - When "Start", the update run will begin or continue execution. + NotStarted: The update run is initialized but execution has not started (default). + Start: The update run should execute or resume execution. + Stop: The update run should pause execution. + Abandon: The update run should be abandoned and terminated. enum: + - NotStarted - Start - Stop + - Abandon type: string required: - placementName @@ -1429,7 +1433,7 @@ spec: conditions: description: |- Conditions is an array of current observed conditions for StagedUpdateRun. - Known conditions are "Initialized", "Started", "Progressing", "Succeeded". + Known conditions are "Initialized", "Progressing", "Succeeded", "Abandoned". items: description: Condition contains details for one aspect of the current state of this API Resource. diff --git a/config/crd/bases/placement.kubernetes-fleet.io_stagedupdateruns.yaml b/config/crd/bases/placement.kubernetes-fleet.io_stagedupdateruns.yaml index 7d2ed18a3..19a9fdbdf 100644 --- a/config/crd/bases/placement.kubernetes-fleet.io_stagedupdateruns.yaml +++ b/config/crd/bases/placement.kubernetes-fleet.io_stagedupdateruns.yaml @@ -32,8 +32,8 @@ spec: - jsonPath: .status.conditions[?(@.type=="Initialized")].status name: Initialized type: string - - jsonPath: .status.conditions[?(@.type=="Started")].status - name: Started + - jsonPath: .status.conditions[?(@.type=="Progressing")].status + name: Progressing type: string - jsonPath: .status.conditions[?(@.type=="Succeeded")].status name: Succeeded @@ -96,14 +96,18 @@ spec: and recorded in the status field. type: string state: - default: Stop + default: NotStarted description: |- State indicates the desired state of the update run. - When "Stop", the update run will initialize but not execute. - When "Start", the update run will begin or continue execution. + NotStarted: The update run is initialized but execution has not started (default). + Start: The update run should execute or resume execution. + Stop: The update run should pause execution. + Abandon: The update run should be abandoned and terminated. enum: + - NotStarted - Start - Stop + - Abandon type: string required: - placementName @@ -348,7 +352,7 @@ spec: conditions: description: |- Conditions is an array of current observed conditions for StagedUpdateRun. - Known conditions are "Initialized", "Started", "Progressing", "Succeeded". + Known conditions are "Initialized", "Progressing", "Succeeded", "Abandoned". items: description: Condition contains details for one aspect of the current state of this API Resource. From c25aaa79fbf6b02fff8982513339f615d1bc95be Mon Sep 17 00:00:00 2001 From: Arvind Thirumurugan Date: Wed, 5 Nov 2025 10:47:07 -0800 Subject: [PATCH 09/20] remove focus Signed-off-by: Arvind Thirumurugan --- .../apis/placement/v1beta1/api_validation_integration_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/apis/placement/v1beta1/api_validation_integration_test.go b/test/apis/placement/v1beta1/api_validation_integration_test.go index ca2909fac..687fe7e2f 100644 --- a/test/apis/placement/v1beta1/api_validation_integration_test.go +++ b/test/apis/placement/v1beta1/api_validation_integration_test.go @@ -1116,7 +1116,7 @@ var _ = Describe("Test placement v1beta1 API validation", func() { }) }) - FContext("Test ClusterStagedUpdateRun API validation - valid cases", func() { + Context("Test ClusterStagedUpdateRun API validation - valid cases", func() { It("Should allow creation of ClusterStagedUpdateRun with valid name length", func() { updateRun := placementv1beta1.ClusterStagedUpdateRun{ ObjectMeta: metav1.ObjectMeta{ @@ -1131,7 +1131,7 @@ var _ = Describe("Test placement v1beta1 API validation", func() { }) }) - FContext("Test ClusterStagedUpdateRun API validation - invalid cases", func() { + Context("Test ClusterStagedUpdateRun API validation - invalid cases", func() { It("Should deny creation of ClusterStagedUpdateRun with name length > 127", func() { updateRun := placementv1beta1.ClusterStagedUpdateRun{ ObjectMeta: metav1.ObjectMeta{ From 96621fc3d26b18ae0fcf70ee39eaa10e19d42aa2 Mon Sep 17 00:00:00 2001 From: Arvind Thirumurugan Date: Thu, 6 Nov 2025 15:08:30 -0800 Subject: [PATCH 10/20] fix states Signed-off-by: Arvind Thirumurugan --- apis/placement/v1beta1/stageupdate_types.go | 18 +++++++++--------- ...netes-fleet.io_clusterstagedupdateruns.yaml | 6 +++--- ...t.kubernetes-fleet.io_stagedupdateruns.yaml | 6 +++--- .../updaterun/controller_integration_test.go | 2 +- .../v1beta1/api_validation_integration_test.go | 6 +++--- test/e2e/cluster_staged_updaterun_test.go | 2 +- test/e2e/staged_updaterun_test.go | 2 +- 7 files changed, 21 insertions(+), 21 deletions(-) diff --git a/apis/placement/v1beta1/stageupdate_types.go b/apis/placement/v1beta1/stageupdate_types.go index 008925d15..140d2e272 100644 --- a/apis/placement/v1beta1/stageupdate_types.go +++ b/apis/placement/v1beta1/stageupdate_types.go @@ -157,14 +157,14 @@ const ( // This is the default state after initialization completes. StateNotStarted State = "NotStarted" - // StateStart indicates the update run should execute (or resume execution if paused). - StateStart State = "Start" + // StateStarted indicates the update run should execute (or resume execution if paused). + StateStarted State = "Started" - // StateStop indicates the update run should pause execution. - StateStop State = "Stop" + // StateStopped indicates the update run should pause execution. + StateStopped State = "Stopped" - // StateAbandon indicates the update run should be abandoned and terminated. - StateAbandon State = "Abandon" + // StateAbandoned indicates the update run should be abandoned and terminated. + StateAbandoned State = "Abandoned" ) // UpdateRunSpec defines the desired rollout strategy and the snapshot indices of the resources to be updated. @@ -191,9 +191,9 @@ type UpdateRunSpec struct { // State indicates the desired state of the update run. // NotStarted: The update run is initialized but execution has not started (default). - // Start: The update run should execute or resume execution. - // Stop: The update run should pause execution. - // Abandon: The update run should be abandoned and terminated. + // Started: The update run should execute or resume execution. + // Stopped: The update run should pause execution. + // Abandoned: The update run should be abandoned and terminated. // +kubebuilder:validation:Optional // +kubebuilder:validation:Enum=NotStarted;Start;Stop;Abandon // +kubebuilder:default=NotStarted diff --git a/config/crd/bases/placement.kubernetes-fleet.io_clusterstagedupdateruns.yaml b/config/crd/bases/placement.kubernetes-fleet.io_clusterstagedupdateruns.yaml index 0ecbae458..542665e18 100644 --- a/config/crd/bases/placement.kubernetes-fleet.io_clusterstagedupdateruns.yaml +++ b/config/crd/bases/placement.kubernetes-fleet.io_clusterstagedupdateruns.yaml @@ -1181,9 +1181,9 @@ spec: description: |- State indicates the desired state of the update run. NotStarted: The update run is initialized but execution has not started (default). - Start: The update run should execute or resume execution. - Stop: The update run should pause execution. - Abandon: The update run should be abandoned and terminated. + Started: The update run should execute or resume execution. + Stopped: The update run should pause execution. + Abandoned: The update run should be abandoned and terminated. enum: - NotStarted - Start diff --git a/config/crd/bases/placement.kubernetes-fleet.io_stagedupdateruns.yaml b/config/crd/bases/placement.kubernetes-fleet.io_stagedupdateruns.yaml index 19a9fdbdf..3a7dbdbb8 100644 --- a/config/crd/bases/placement.kubernetes-fleet.io_stagedupdateruns.yaml +++ b/config/crd/bases/placement.kubernetes-fleet.io_stagedupdateruns.yaml @@ -100,9 +100,9 @@ spec: description: |- State indicates the desired state of the update run. NotStarted: The update run is initialized but execution has not started (default). - Start: The update run should execute or resume execution. - Stop: The update run should pause execution. - Abandon: The update run should be abandoned and terminated. + Started: The update run should execute or resume execution. + Stopped: The update run should pause execution. + Abandoned: The update run should be abandoned and terminated. enum: - NotStarted - Start diff --git a/pkg/controllers/updaterun/controller_integration_test.go b/pkg/controllers/updaterun/controller_integration_test.go index 3d56afb3e..2bd659a46 100644 --- a/pkg/controllers/updaterun/controller_integration_test.go +++ b/pkg/controllers/updaterun/controller_integration_test.go @@ -341,7 +341,7 @@ func generateTestClusterStagedUpdateRun() *placementv1beta1.ClusterStagedUpdateR PlacementName: testCRPName, ResourceSnapshotIndex: testResourceSnapshotIndex, StagedUpdateStrategyName: testUpdateStrategyName, - State: placementv1beta1.StateStart, + State: placementv1beta1.StateStarted, }, } } diff --git a/test/apis/placement/v1beta1/api_validation_integration_test.go b/test/apis/placement/v1beta1/api_validation_integration_test.go index 687fe7e2f..89363fb54 100644 --- a/test/apis/placement/v1beta1/api_validation_integration_test.go +++ b/test/apis/placement/v1beta1/api_validation_integration_test.go @@ -1123,7 +1123,7 @@ var _ = Describe("Test placement v1beta1 API validation", func() { Name: fmt.Sprintf(validupdateRunNameTemplate, GinkgoParallelProcess()), }, Spec: placementv1beta1.UpdateRunSpec{ - State: placementv1beta1.StateStart, + State: placementv1beta1.StateStarted, }, } Expect(hubClient.Create(ctx, &updateRun)).Should(Succeed()) @@ -1138,7 +1138,7 @@ var _ = Describe("Test placement v1beta1 API validation", func() { Name: fmt.Sprintf(invalidupdateRunNameTemplate, GinkgoParallelProcess()), }, Spec: placementv1beta1.UpdateRunSpec{ - State: placementv1beta1.StateStart, + State: placementv1beta1.StateStarted, }, } err := hubClient.Create(ctx, &updateRun) @@ -1154,7 +1154,7 @@ var _ = Describe("Test placement v1beta1 API validation", func() { }, Spec: placementv1beta1.UpdateRunSpec{ PlacementName: "test-placement", - State: placementv1beta1.StateStart, + State: placementv1beta1.StateStarted, }, } Expect(hubClient.Create(ctx, &updateRun)).Should(Succeed()) diff --git a/test/e2e/cluster_staged_updaterun_test.go b/test/e2e/cluster_staged_updaterun_test.go index 2f2e6e068..4a1eb1b66 100644 --- a/test/e2e/cluster_staged_updaterun_test.go +++ b/test/e2e/cluster_staged_updaterun_test.go @@ -1668,7 +1668,7 @@ func createClusterStagedUpdateRunSucceed(updateRunName, crpName, resourceSnapsho PlacementName: crpName, ResourceSnapshotIndex: resourceSnapshotIndex, StagedUpdateStrategyName: strategyName, - State: placementv1beta1.StateStart, + State: placementv1beta1.StateStarted, }, } Expect(hubClient.Create(ctx, updateRun)).To(Succeed(), "Failed to create ClusterStagedUpdateRun %s", updateRunName) diff --git a/test/e2e/staged_updaterun_test.go b/test/e2e/staged_updaterun_test.go index ce924293d..ba7ea5a7f 100644 --- a/test/e2e/staged_updaterun_test.go +++ b/test/e2e/staged_updaterun_test.go @@ -1201,7 +1201,7 @@ func createStagedUpdateRunSucceed(updateRunName, namespace, rpName, resourceSnap PlacementName: rpName, ResourceSnapshotIndex: resourceSnapshotIndex, StagedUpdateStrategyName: strategyName, - State: placementv1beta1.StateStart, + State: placementv1beta1.StateStarted, }, } Expect(hubClient.Create(ctx, updateRun)).To(Succeed(), "Failed to create StagedUpdateRun %s", updateRunName) From 2a98e6a463ac26b13c18914d2013f94a72f24143 Mon Sep 17 00:00:00 2001 From: Arvind Thirumurugan Date: Thu, 6 Nov 2025 15:13:57 -0800 Subject: [PATCH 11/20] fix enum validation Signed-off-by: Arvind Thirumurugan --- apis/placement/v1beta1/stageupdate_types.go | 2 +- ...ement.kubernetes-fleet.io_clusterstagedupdateruns.yaml | 8 ++++---- ...kubernetes-fleet.io_clusterstagedupdatestrategies.yaml | 2 +- .../placement.kubernetes-fleet.io_stagedupdateruns.yaml | 8 ++++---- ...cement.kubernetes-fleet.io_stagedupdatestrategies.yaml | 2 +- 5 files changed, 11 insertions(+), 11 deletions(-) diff --git a/apis/placement/v1beta1/stageupdate_types.go b/apis/placement/v1beta1/stageupdate_types.go index 140d2e272..1d5b20e98 100644 --- a/apis/placement/v1beta1/stageupdate_types.go +++ b/apis/placement/v1beta1/stageupdate_types.go @@ -195,7 +195,7 @@ type UpdateRunSpec struct { // Stopped: The update run should pause execution. // Abandoned: The update run should be abandoned and terminated. // +kubebuilder:validation:Optional - // +kubebuilder:validation:Enum=NotStarted;Start;Stop;Abandon + // +kubebuilder:validation:Enum=NotStarted;Started;Stopped;Abandoned // +kubebuilder:default=NotStarted State State `json:"state"` } diff --git a/config/crd/bases/placement.kubernetes-fleet.io_clusterstagedupdateruns.yaml b/config/crd/bases/placement.kubernetes-fleet.io_clusterstagedupdateruns.yaml index 542665e18..fc7aa524c 100644 --- a/config/crd/bases/placement.kubernetes-fleet.io_clusterstagedupdateruns.yaml +++ b/config/crd/bases/placement.kubernetes-fleet.io_clusterstagedupdateruns.yaml @@ -1186,9 +1186,9 @@ spec: Abandoned: The update run should be abandoned and terminated. enum: - NotStarted - - Start - - Stop - - Abandon + - Started + - Stopped + - Abandoned type: string required: - placementName @@ -2015,7 +2015,7 @@ spec: description: |- MaxConcurrency specifies the maximum number of clusters that can be updated concurrently within this stage. Value can be an absolute number (ex: 5) or a percentage of the total clusters in the stage (ex: 50%). - Absolute number is calculated from percentage by rounding up. + Fractional results are rounded down. A minimum of 1 update is enforced. If not specified, all clusters in the stage are updated sequentially (effectively maxConcurrency = 1). Defaults to 1. pattern: ^((100|[0-9]{1,2})%|[0-9]+)$ diff --git a/config/crd/bases/placement.kubernetes-fleet.io_clusterstagedupdatestrategies.yaml b/config/crd/bases/placement.kubernetes-fleet.io_clusterstagedupdatestrategies.yaml index 8791e4ce9..4d088a0ce 100644 --- a/config/crd/bases/placement.kubernetes-fleet.io_clusterstagedupdatestrategies.yaml +++ b/config/crd/bases/placement.kubernetes-fleet.io_clusterstagedupdatestrategies.yaml @@ -312,7 +312,7 @@ spec: description: |- MaxConcurrency specifies the maximum number of clusters that can be updated concurrently within this stage. Value can be an absolute number (ex: 5) or a percentage of the total clusters in the stage (ex: 50%). - Absolute number is calculated from percentage by rounding up. + Fractional results are rounded down. A minimum of 1 update is enforced. If not specified, all clusters in the stage are updated sequentially (effectively maxConcurrency = 1). Defaults to 1. pattern: ^((100|[0-9]{1,2})%|[0-9]+)$ diff --git a/config/crd/bases/placement.kubernetes-fleet.io_stagedupdateruns.yaml b/config/crd/bases/placement.kubernetes-fleet.io_stagedupdateruns.yaml index 3a7dbdbb8..19829f196 100644 --- a/config/crd/bases/placement.kubernetes-fleet.io_stagedupdateruns.yaml +++ b/config/crd/bases/placement.kubernetes-fleet.io_stagedupdateruns.yaml @@ -105,9 +105,9 @@ spec: Abandoned: The update run should be abandoned and terminated. enum: - NotStarted - - Start - - Stop - - Abandon + - Started + - Stopped + - Abandoned type: string required: - placementName @@ -934,7 +934,7 @@ spec: description: |- MaxConcurrency specifies the maximum number of clusters that can be updated concurrently within this stage. Value can be an absolute number (ex: 5) or a percentage of the total clusters in the stage (ex: 50%). - Absolute number is calculated from percentage by rounding up. + Fractional results are rounded down. A minimum of 1 update is enforced. If not specified, all clusters in the stage are updated sequentially (effectively maxConcurrency = 1). Defaults to 1. pattern: ^((100|[0-9]{1,2})%|[0-9]+)$ diff --git a/config/crd/bases/placement.kubernetes-fleet.io_stagedupdatestrategies.yaml b/config/crd/bases/placement.kubernetes-fleet.io_stagedupdatestrategies.yaml index fe0dfc567..898f92a88 100644 --- a/config/crd/bases/placement.kubernetes-fleet.io_stagedupdatestrategies.yaml +++ b/config/crd/bases/placement.kubernetes-fleet.io_stagedupdatestrategies.yaml @@ -174,7 +174,7 @@ spec: description: |- MaxConcurrency specifies the maximum number of clusters that can be updated concurrently within this stage. Value can be an absolute number (ex: 5) or a percentage of the total clusters in the stage (ex: 50%). - Absolute number is calculated from percentage by rounding up. + Fractional results are rounded down. A minimum of 1 update is enforced. If not specified, all clusters in the stage are updated sequentially (effectively maxConcurrency = 1). Defaults to 1. pattern: ^((100|[0-9]{1,2})%|[0-9]+)$ From 88a1a336c56fffacb20f2e41a703faaca560a970 Mon Sep 17 00:00:00 2001 From: Arvind Thirumurugan Date: Fri, 7 Nov 2025 12:14:42 -0800 Subject: [PATCH 12/20] address state comments Signed-off-by: Arvind Thirumurugan --- apis/placement/v1beta1/stageupdate_types.go | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/apis/placement/v1beta1/stageupdate_types.go b/apis/placement/v1beta1/stageupdate_types.go index 1d5b20e98..c7d40ded4 100644 --- a/apis/placement/v1beta1/stageupdate_types.go +++ b/apis/placement/v1beta1/stageupdate_types.go @@ -153,17 +153,16 @@ func (c *ClusterStagedUpdateRun) SetUpdateRunStatus(status UpdateRunStatus) { type State string const ( - // StateNotStarted indicates the update run has been initialized but execution has not started. - // This is the default state after initialization completes. + // StateNotStarted is the default state for an update run, where update run gets initialized but execution won't start until the state is marked as Started. StateNotStarted State = "NotStarted" // StateStarted indicates the update run should execute (or resume execution if paused). StateStarted State = "Started" - // StateStopped indicates the update run should pause execution. + // StateStopped indicates the update run should pause execution. The rollout can be resumed by marking the state as Started again. StateStopped State = "Stopped" - // StateAbandoned indicates the update run should be abandoned and terminated. + // StateAbandoned indicates the update run should be abandoned and terminated. An Abandoned rollout can not be Started again. It's a terminal state. StateAbandoned State = "Abandoned" ) From cc10bbb9ccc1b619d89a0afe951d2496d127152f Mon Sep 17 00:00:00 2001 From: Arvind Thirumurugan Date: Fri, 7 Nov 2025 12:25:23 -0800 Subject: [PATCH 13/20] address state comments Signed-off-by: Arvind Thirumurugan --- apis/placement/v1beta1/stageupdate_types.go | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/apis/placement/v1beta1/stageupdate_types.go b/apis/placement/v1beta1/stageupdate_types.go index c7d40ded4..e21455246 100644 --- a/apis/placement/v1beta1/stageupdate_types.go +++ b/apis/placement/v1beta1/stageupdate_types.go @@ -153,16 +153,19 @@ func (c *ClusterStagedUpdateRun) SetUpdateRunStatus(status UpdateRunStatus) { type State string const ( - // StateNotStarted is the default state for an update run, where update run gets initialized but execution won't start until the state is marked as Started. + // StateNotStarted is the default state for an update run, it describes user intent to initialize but not execute the update run. StateNotStarted State = "NotStarted" - // StateStarted indicates the update run should execute (or resume execution if paused). + // StateStarted describes user intent to execute (or resume execution if paused). + // This state can transition to Stopped or Abandoned. StateStarted State = "Started" - // StateStopped indicates the update run should pause execution. The rollout can be resumed by marking the state as Started again. + // StateStopped describes user intent to pause the update run. + // This state can transition back to Started. StateStopped State = "Stopped" - // StateAbandoned indicates the update run should be abandoned and terminated. An Abandoned rollout can not be Started again. It's a terminal state. + // StateAbandoned describes user intent to abandon the update run. + // This is a terminal state that cannot transition to any other state. StateAbandoned State = "Abandoned" ) From 40e8c2f7e68ec841c18dfa9c7317ef46de2ef5da Mon Sep 17 00:00:00 2001 From: Arvind Thirumurugan Date: Fri, 7 Nov 2025 12:34:13 -0800 Subject: [PATCH 14/20] minor fixes Signed-off-by: Arvind Thirumurugan --- apis/placement/v1beta1/stageupdate_types.go | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/apis/placement/v1beta1/stageupdate_types.go b/apis/placement/v1beta1/stageupdate_types.go index e21455246..be23909bc 100644 --- a/apis/placement/v1beta1/stageupdate_types.go +++ b/apis/placement/v1beta1/stageupdate_types.go @@ -153,19 +153,20 @@ func (c *ClusterStagedUpdateRun) SetUpdateRunStatus(status UpdateRunStatus) { type State string const ( - // StateNotStarted is the default state for an update run, it describes user intent to initialize but not execute the update run. + // StateNotStarted describes user intent to initialize but not execute the update run. + // This is the default state when an update run is created. StateNotStarted State = "NotStarted" // StateStarted describes user intent to execute (or resume execution if paused). - // This state can transition to Stopped or Abandoned. + // Users can subsequently set the state to Stopped or Abandoned. StateStarted State = "Started" // StateStopped describes user intent to pause the update run. - // This state can transition back to Started. + // Users can subsequently set the state to Started or Abandoned. StateStopped State = "Stopped" // StateAbandoned describes user intent to abandon the update run. - // This is a terminal state that cannot transition to any other state. + // This is a terminal state; once set, it cannot be changed. StateAbandoned State = "Abandoned" ) From a8d6080cae2d76fbe57e601f838869f30773b45d Mon Sep 17 00:00:00 2001 From: Arvind Thirumurugan Date: Fri, 7 Nov 2025 13:07:33 -0800 Subject: [PATCH 15/20] remove abandoned condition Signed-off-by: Arvind Thirumurugan --- apis/placement/v1beta1/stageupdate_types.go | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/apis/placement/v1beta1/stageupdate_types.go b/apis/placement/v1beta1/stageupdate_types.go index be23909bc..530f7168a 100644 --- a/apis/placement/v1beta1/stageupdate_types.go +++ b/apis/placement/v1beta1/stageupdate_types.go @@ -420,7 +420,7 @@ const ( // StagedUpdateRunConditionProgressing indicates whether the staged update run is making progress. // Its condition status can be one of the following: // - "True": The staged update run is making progress. - // - "False": The staged update run is waiting/paused. + // - "False": The staged update run is waiting/paused or abandoned. // - "Unknown" means it is unknown. StagedUpdateRunConditionProgressing StagedUpdateRunConditionType = "Progressing" @@ -429,11 +429,6 @@ const ( // - "True": The staged update run is completed successfully. // - "False": The staged update run encountered an error and stopped. StagedUpdateRunConditionSucceeded StagedUpdateRunConditionType = "Succeeded" - - // StagedUpdateRunConditionAbandoned indicates whether the staged update run has been abandoned. - // Its condition status can be one of the following: - // - "True": The staged update run has been abandoned by user request. - StagedUpdateRunConditionAbandoned StagedUpdateRunConditionType = "Abandoned" ) // StageUpdatingStatus defines the status of the update run in a stage. From 754500fe23b8417899cc77018619adbd74d1945a Mon Sep 17 00:00:00 2001 From: Arvind Thirumurugan Date: Fri, 7 Nov 2025 13:20:44 -0800 Subject: [PATCH 16/20] minor fix Signed-off-by: Arvind Thirumurugan --- apis/placement/v1beta1/stageupdate_types.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apis/placement/v1beta1/stageupdate_types.go b/apis/placement/v1beta1/stageupdate_types.go index 530f7168a..c5928560b 100644 --- a/apis/placement/v1beta1/stageupdate_types.go +++ b/apis/placement/v1beta1/stageupdate_types.go @@ -399,7 +399,7 @@ type UpdateRunStatus struct { // +listMapKey=type // // Conditions is an array of current observed conditions for StagedUpdateRun. - // Known conditions are "Initialized", "Progressing", "Succeeded", "Abandoned". + // Known conditions are "Initialized", "Progressing", "Succeeded". // +kubebuilder:validation:Optional Conditions []metav1.Condition `json:"conditions,omitempty"` } From b9be767873b6f5883f24268bc59817dbffa8ea13 Mon Sep 17 00:00:00 2001 From: Arvind Thirumurugan Date: Fri, 7 Nov 2025 13:25:20 -0800 Subject: [PATCH 17/20] minor fix Signed-off-by: Arvind Thirumurugan --- apis/placement/v1beta1/stageupdate_types.go | 2 +- .../placement.kubernetes-fleet.io_clusterstagedupdateruns.yaml | 2 +- .../bases/placement.kubernetes-fleet.io_stagedupdateruns.yaml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/apis/placement/v1beta1/stageupdate_types.go b/apis/placement/v1beta1/stageupdate_types.go index c5928560b..c7557116d 100644 --- a/apis/placement/v1beta1/stageupdate_types.go +++ b/apis/placement/v1beta1/stageupdate_types.go @@ -420,7 +420,7 @@ const ( // StagedUpdateRunConditionProgressing indicates whether the staged update run is making progress. // Its condition status can be one of the following: // - "True": The staged update run is making progress. - // - "False": The staged update run is waiting/paused or abandoned. + // - "False": The staged update run is waiting/paused/abandoned. // - "Unknown" means it is unknown. StagedUpdateRunConditionProgressing StagedUpdateRunConditionType = "Progressing" diff --git a/config/crd/bases/placement.kubernetes-fleet.io_clusterstagedupdateruns.yaml b/config/crd/bases/placement.kubernetes-fleet.io_clusterstagedupdateruns.yaml index fc7aa524c..eeeb031bf 100644 --- a/config/crd/bases/placement.kubernetes-fleet.io_clusterstagedupdateruns.yaml +++ b/config/crd/bases/placement.kubernetes-fleet.io_clusterstagedupdateruns.yaml @@ -1433,7 +1433,7 @@ spec: conditions: description: |- Conditions is an array of current observed conditions for StagedUpdateRun. - Known conditions are "Initialized", "Progressing", "Succeeded", "Abandoned". + Known conditions are "Initialized", "Progressing", "Succeeded". items: description: Condition contains details for one aspect of the current state of this API Resource. diff --git a/config/crd/bases/placement.kubernetes-fleet.io_stagedupdateruns.yaml b/config/crd/bases/placement.kubernetes-fleet.io_stagedupdateruns.yaml index 19829f196..bf50b60c3 100644 --- a/config/crd/bases/placement.kubernetes-fleet.io_stagedupdateruns.yaml +++ b/config/crd/bases/placement.kubernetes-fleet.io_stagedupdateruns.yaml @@ -352,7 +352,7 @@ spec: conditions: description: |- Conditions is an array of current observed conditions for StagedUpdateRun. - Known conditions are "Initialized", "Progressing", "Succeeded", "Abandoned". + Known conditions are "Initialized", "Progressing", "Succeeded". items: description: Condition contains details for one aspect of the current state of this API Resource. From 52da1d559d1c37cd0b739fbdabc17468c4a0f213 Mon Sep 17 00:00:00 2001 From: Arvind Thirumurugan Date: Tue, 11 Nov 2025 15:11:01 -0800 Subject: [PATCH 18/20] add omitempty for state Signed-off-by: Arvind Thirumurugan --- apis/placement/v1beta1/stageupdate_types.go | 4 ++-- pkg/controllers/updaterun/controller_integration_test.go | 1 - .../placement/v1beta1/api_validation_integration_test.go | 7 ------- test/e2e/cluster_staged_updaterun_test.go | 1 - test/e2e/staged_updaterun_test.go | 1 - 5 files changed, 2 insertions(+), 12 deletions(-) diff --git a/apis/placement/v1beta1/stageupdate_types.go b/apis/placement/v1beta1/stageupdate_types.go index c7557116d..eabf41c96 100644 --- a/apis/placement/v1beta1/stageupdate_types.go +++ b/apis/placement/v1beta1/stageupdate_types.go @@ -198,9 +198,9 @@ type UpdateRunSpec struct { // Stopped: The update run should pause execution. // Abandoned: The update run should be abandoned and terminated. // +kubebuilder:validation:Optional - // +kubebuilder:validation:Enum=NotStarted;Started;Stopped;Abandoned // +kubebuilder:default=NotStarted - State State `json:"state"` + // +kubebuilder:validation:Enum=NotStarted;Started;Stopped;Abandoned + State State `json:"state,omitempty"` } // UpdateStrategySpecGetterSetter offers the functionality to work with UpdateStrategySpec. diff --git a/pkg/controllers/updaterun/controller_integration_test.go b/pkg/controllers/updaterun/controller_integration_test.go index 2bd659a46..8c8e38d06 100644 --- a/pkg/controllers/updaterun/controller_integration_test.go +++ b/pkg/controllers/updaterun/controller_integration_test.go @@ -341,7 +341,6 @@ func generateTestClusterStagedUpdateRun() *placementv1beta1.ClusterStagedUpdateR PlacementName: testCRPName, ResourceSnapshotIndex: testResourceSnapshotIndex, StagedUpdateStrategyName: testUpdateStrategyName, - State: placementv1beta1.StateStarted, }, } } diff --git a/test/apis/placement/v1beta1/api_validation_integration_test.go b/test/apis/placement/v1beta1/api_validation_integration_test.go index 89363fb54..d0260d0ff 100644 --- a/test/apis/placement/v1beta1/api_validation_integration_test.go +++ b/test/apis/placement/v1beta1/api_validation_integration_test.go @@ -1122,9 +1122,6 @@ var _ = Describe("Test placement v1beta1 API validation", func() { ObjectMeta: metav1.ObjectMeta{ Name: fmt.Sprintf(validupdateRunNameTemplate, GinkgoParallelProcess()), }, - Spec: placementv1beta1.UpdateRunSpec{ - State: placementv1beta1.StateStarted, - }, } Expect(hubClient.Create(ctx, &updateRun)).Should(Succeed()) Expect(hubClient.Delete(ctx, &updateRun)).Should(Succeed()) @@ -1137,9 +1134,6 @@ var _ = Describe("Test placement v1beta1 API validation", func() { ObjectMeta: metav1.ObjectMeta{ Name: fmt.Sprintf(invalidupdateRunNameTemplate, GinkgoParallelProcess()), }, - Spec: placementv1beta1.UpdateRunSpec{ - State: placementv1beta1.StateStarted, - }, } err := hubClient.Create(ctx, &updateRun) var statusErr *k8sErrors.StatusError @@ -1154,7 +1148,6 @@ var _ = Describe("Test placement v1beta1 API validation", func() { }, Spec: placementv1beta1.UpdateRunSpec{ PlacementName: "test-placement", - State: placementv1beta1.StateStarted, }, } Expect(hubClient.Create(ctx, &updateRun)).Should(Succeed()) diff --git a/test/e2e/cluster_staged_updaterun_test.go b/test/e2e/cluster_staged_updaterun_test.go index 4a1eb1b66..9880c2eef 100644 --- a/test/e2e/cluster_staged_updaterun_test.go +++ b/test/e2e/cluster_staged_updaterun_test.go @@ -1668,7 +1668,6 @@ func createClusterStagedUpdateRunSucceed(updateRunName, crpName, resourceSnapsho PlacementName: crpName, ResourceSnapshotIndex: resourceSnapshotIndex, StagedUpdateStrategyName: strategyName, - State: placementv1beta1.StateStarted, }, } Expect(hubClient.Create(ctx, updateRun)).To(Succeed(), "Failed to create ClusterStagedUpdateRun %s", updateRunName) diff --git a/test/e2e/staged_updaterun_test.go b/test/e2e/staged_updaterun_test.go index ba7ea5a7f..b5a5428b1 100644 --- a/test/e2e/staged_updaterun_test.go +++ b/test/e2e/staged_updaterun_test.go @@ -1201,7 +1201,6 @@ func createStagedUpdateRunSucceed(updateRunName, namespace, rpName, resourceSnap PlacementName: rpName, ResourceSnapshotIndex: resourceSnapshotIndex, StagedUpdateStrategyName: strategyName, - State: placementv1beta1.StateStarted, }, } Expect(hubClient.Create(ctx, updateRun)).To(Succeed(), "Failed to create StagedUpdateRun %s", updateRunName) From 2c7c4dc681ad40849f660c6b08f7e6256a985618 Mon Sep 17 00:00:00 2001 From: Arvind Thirumurugan Date: Tue, 11 Nov 2025 18:04:43 -0800 Subject: [PATCH 19/20] add CEL validation for state transitions Signed-off-by: Arvind Thirumurugan --- apis/placement/v1beta1/stageupdate_types.go | 13 +- ...etes-fleet.io_clusterstagedupdateruns.yaml | 30 +- ....kubernetes-fleet.io_stagedupdateruns.yaml | 29 +- .../api_validation_integration_test.go | 378 +++++++++++++++++- 4 files changed, 436 insertions(+), 14 deletions(-) diff --git a/apis/placement/v1beta1/stageupdate_types.go b/apis/placement/v1beta1/stageupdate_types.go index eabf41c96..1f03fb625 100644 --- a/apis/placement/v1beta1/stageupdate_types.go +++ b/apis/placement/v1beta1/stageupdate_types.go @@ -108,9 +108,8 @@ type ClusterStagedUpdateRun struct { metav1.TypeMeta `json:",inline"` metav1.ObjectMeta `json:"metadata,omitempty"` - // The desired state of ClusterStagedUpdateRun. The spec is immutable. + // The desired state of ClusterStagedUpdateRun. // +kubebuilder:validation:Required - // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="The spec field is immutable" Spec UpdateRunSpec `json:"spec"` // The observed status of ClusterStagedUpdateRun. @@ -172,17 +171,23 @@ const ( // UpdateRunSpec defines the desired rollout strategy and the snapshot indices of the resources to be updated. // It specifies a stage-by-stage update process across selected clusters for the given ResourcePlacement object. +// +kubebuilder:validation:XValidation:rule="!has(oldSelf.state) || oldSelf.state != 'NotStarted' || self.state != 'Stopped'",message="invalid state transition: cannot transition from NotStarted to Stopped" +// +kubebuilder:validation:XValidation:rule="!has(oldSelf.state) || oldSelf.state != 'Started' || self.state != 'NotStarted'",message="invalid state transition: cannot transition from Started to NotStarted" +// +kubebuilder:validation:XValidation:rule="!has(oldSelf.state) || oldSelf.state != 'Stopped' || self.state != 'NotStarted'",message="invalid state transition: cannot transition from Stopped to NotStarted" +// +kubebuilder:validation:XValidation:rule="!has(oldSelf.state) || oldSelf.state != 'Abandoned' || self.state == 'Abandoned'",message="invalid state transition: Abandoned is a terminal state and cannot transition to any other state" type UpdateRunSpec struct { // PlacementName is the name of placement that this update run is applied to. // There can be multiple active update runs for each placement, but // it's up to the DevOps team to ensure they don't conflict with each other. // +kubebuilder:validation:Required // +kubebuilder:validation:MaxLength=255 + // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="placementName is immutable" PlacementName string `json:"placementName"` // The resource snapshot index of the selected resources to be updated across clusters. // The index represents a group of resource snapshots that includes all the resources a ResourcePlacement selected. // +kubebuilder:validation:Required + // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="resourceSnapshotIndex is immutable" ResourceSnapshotIndex string `json:"resourceSnapshotIndex"` // The name of the update strategy that specifies the stages and the sequence @@ -190,6 +195,7 @@ type UpdateRunSpec struct { // are computed according to the referenced strategy when the update run starts // and recorded in the status field. // +kubebuilder:validation:Required + // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="stagedRolloutStrategyName is immutable" StagedUpdateStrategyName string `json:"stagedRolloutStrategyName"` // State indicates the desired state of the update run. @@ -795,9 +801,8 @@ type StagedUpdateRun struct { metav1.TypeMeta `json:",inline"` metav1.ObjectMeta `json:"metadata,omitempty"` - // The desired state of StagedUpdateRun. The spec is immutable. + // The desired state of StagedUpdateRun. // +kubebuilder:validation:Required - // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="The spec field is immutable" Spec UpdateRunSpec `json:"spec"` // The observed status of StagedUpdateRun. diff --git a/config/crd/bases/placement.kubernetes-fleet.io_clusterstagedupdateruns.yaml b/config/crd/bases/placement.kubernetes-fleet.io_clusterstagedupdateruns.yaml index eeeb031bf..bf75d613d 100644 --- a/config/crd/bases/placement.kubernetes-fleet.io_clusterstagedupdateruns.yaml +++ b/config/crd/bases/placement.kubernetes-fleet.io_clusterstagedupdateruns.yaml @@ -1154,8 +1154,7 @@ spec: metadata: type: object spec: - description: The desired state of ClusterStagedUpdateRun. The spec is - immutable. + description: The desired state of ClusterStagedUpdateRun. properties: placementName: description: |- @@ -1164,11 +1163,17 @@ spec: it's up to the DevOps team to ensure they don't conflict with each other. maxLength: 255 type: string + x-kubernetes-validations: + - message: placementName is immutable + rule: self == oldSelf resourceSnapshotIndex: description: |- The resource snapshot index of the selected resources to be updated across clusters. The index represents a group of resource snapshots that includes all the resources a ResourcePlacement selected. type: string + x-kubernetes-validations: + - message: resourceSnapshotIndex is immutable + rule: self == oldSelf stagedRolloutStrategyName: description: |- The name of the update strategy that specifies the stages and the sequence @@ -1176,6 +1181,9 @@ spec: are computed according to the referenced strategy when the update run starts and recorded in the status field. type: string + x-kubernetes-validations: + - message: stagedRolloutStrategyName is immutable + rule: self == oldSelf state: default: NotStarted description: |- @@ -1196,8 +1204,22 @@ spec: - stagedRolloutStrategyName type: object x-kubernetes-validations: - - message: The spec field is immutable - rule: self == oldSelf + - message: 'invalid state transition: cannot transition from NotStarted + to Stopped' + rule: '!has(oldSelf.state) || oldSelf.state != ''NotStarted'' || self.state + != ''Stopped''' + - message: 'invalid state transition: cannot transition from Started to + NotStarted' + rule: '!has(oldSelf.state) || oldSelf.state != ''Started'' || self.state + != ''NotStarted''' + - message: 'invalid state transition: cannot transition from Stopped to + NotStarted' + rule: '!has(oldSelf.state) || oldSelf.state != ''Stopped'' || self.state + != ''NotStarted''' + - message: 'invalid state transition: Abandoned is a terminal state and + cannot transition to any other state' + rule: '!has(oldSelf.state) || oldSelf.state != ''Abandoned'' || self.state + == ''Abandoned''' status: description: The observed status of ClusterStagedUpdateRun. properties: diff --git a/config/crd/bases/placement.kubernetes-fleet.io_stagedupdateruns.yaml b/config/crd/bases/placement.kubernetes-fleet.io_stagedupdateruns.yaml index bf50b60c3..10fe5f738 100644 --- a/config/crd/bases/placement.kubernetes-fleet.io_stagedupdateruns.yaml +++ b/config/crd/bases/placement.kubernetes-fleet.io_stagedupdateruns.yaml @@ -74,7 +74,7 @@ spec: metadata: type: object spec: - description: The desired state of StagedUpdateRun. The spec is immutable. + description: The desired state of StagedUpdateRun. properties: placementName: description: |- @@ -83,11 +83,17 @@ spec: it's up to the DevOps team to ensure they don't conflict with each other. maxLength: 255 type: string + x-kubernetes-validations: + - message: placementName is immutable + rule: self == oldSelf resourceSnapshotIndex: description: |- The resource snapshot index of the selected resources to be updated across clusters. The index represents a group of resource snapshots that includes all the resources a ResourcePlacement selected. type: string + x-kubernetes-validations: + - message: resourceSnapshotIndex is immutable + rule: self == oldSelf stagedRolloutStrategyName: description: |- The name of the update strategy that specifies the stages and the sequence @@ -95,6 +101,9 @@ spec: are computed according to the referenced strategy when the update run starts and recorded in the status field. type: string + x-kubernetes-validations: + - message: stagedRolloutStrategyName is immutable + rule: self == oldSelf state: default: NotStarted description: |- @@ -115,8 +124,22 @@ spec: - stagedRolloutStrategyName type: object x-kubernetes-validations: - - message: The spec field is immutable - rule: self == oldSelf + - message: 'invalid state transition: cannot transition from NotStarted + to Stopped' + rule: '!has(oldSelf.state) || oldSelf.state != ''NotStarted'' || self.state + != ''Stopped''' + - message: 'invalid state transition: cannot transition from Started to + NotStarted' + rule: '!has(oldSelf.state) || oldSelf.state != ''Started'' || self.state + != ''NotStarted''' + - message: 'invalid state transition: cannot transition from Stopped to + NotStarted' + rule: '!has(oldSelf.state) || oldSelf.state != ''Stopped'' || self.state + != ''NotStarted''' + - message: 'invalid state transition: Abandoned is a terminal state and + cannot transition to any other state' + rule: '!has(oldSelf.state) || oldSelf.state != ''Abandoned'' || self.state + == ''Abandoned''' status: description: The observed status of StagedUpdateRun. properties: diff --git a/test/apis/placement/v1beta1/api_validation_integration_test.go b/test/apis/placement/v1beta1/api_validation_integration_test.go index d0260d0ff..c5398afb4 100644 --- a/test/apis/placement/v1beta1/api_validation_integration_test.go +++ b/test/apis/placement/v1beta1/api_validation_integration_test.go @@ -1141,13 +1141,15 @@ var _ = Describe("Test placement v1beta1 API validation", func() { Expect(statusErr.ErrStatus.Message).Should(MatchRegexp("metadata.name max length is 127")) }) - It("Should deny update of ClusterStagedUpdateRun spec", func() { + It("Should deny update of ClusterStagedUpdateRun placementName field", func() { updateRun := placementv1beta1.ClusterStagedUpdateRun{ ObjectMeta: metav1.ObjectMeta{ Name: fmt.Sprintf(validupdateRunNameTemplate, GinkgoParallelProcess()), }, Spec: placementv1beta1.UpdateRunSpec{ - PlacementName: "test-placement", + PlacementName: "test-placement", + ResourceSnapshotIndex: "1", + StagedUpdateStrategyName: "test-strategy", }, } Expect(hubClient.Create(ctx, &updateRun)).Should(Succeed()) @@ -1156,7 +1158,68 @@ var _ = Describe("Test placement v1beta1 API validation", func() { err := hubClient.Update(ctx, &updateRun) var statusErr *k8sErrors.StatusError Expect(errors.As(err, &statusErr)).To(BeTrue(), fmt.Sprintf("Update updateRun call produced error %s. Error type wanted is %s.", reflect.TypeOf(err), reflect.TypeOf(&k8sErrors.StatusError{}))) - Expect(statusErr.ErrStatus.Message).Should(MatchRegexp("The spec field is immutable")) + Expect(statusErr.ErrStatus.Message).Should(MatchRegexp("placementName is immutable")) + Expect(hubClient.Delete(ctx, &updateRun)).Should(Succeed()) + }) + + It("Should deny update of ClusterStagedUpdateRun resourceSnapshotIndex field", func() { + updateRun := placementv1beta1.ClusterStagedUpdateRun{ + ObjectMeta: metav1.ObjectMeta{ + Name: fmt.Sprintf(validupdateRunNameTemplate, GinkgoParallelProcess()), + }, + Spec: placementv1beta1.UpdateRunSpec{ + PlacementName: "test-placement", + ResourceSnapshotIndex: "1", + StagedUpdateStrategyName: "test-strategy", + }, + } + Expect(hubClient.Create(ctx, &updateRun)).Should(Succeed()) + + updateRun.Spec.ResourceSnapshotIndex = "2" + err := hubClient.Update(ctx, &updateRun) + var statusErr *k8sErrors.StatusError + Expect(errors.As(err, &statusErr)).To(BeTrue(), fmt.Sprintf("Update updateRun call produced error %s. Error type wanted is %s.", reflect.TypeOf(err), reflect.TypeOf(&k8sErrors.StatusError{}))) + Expect(statusErr.ErrStatus.Message).Should(MatchRegexp("resourceSnapshotIndex is immutable")) + Expect(hubClient.Delete(ctx, &updateRun)).Should(Succeed()) + }) + + It("Should deny update of ClusterStagedUpdateRun stagedRolloutStrategyName field", func() { + updateRun := placementv1beta1.ClusterStagedUpdateRun{ + ObjectMeta: metav1.ObjectMeta{ + Name: fmt.Sprintf(validupdateRunNameTemplate, GinkgoParallelProcess()), + }, + Spec: placementv1beta1.UpdateRunSpec{ + PlacementName: "test-placement", + ResourceSnapshotIndex: "1", + StagedUpdateStrategyName: "test-strategy", + }, + } + Expect(hubClient.Create(ctx, &updateRun)).Should(Succeed()) + + updateRun.Spec.StagedUpdateStrategyName = "test-strategy-2" + err := hubClient.Update(ctx, &updateRun) + var statusErr *k8sErrors.StatusError + Expect(errors.As(err, &statusErr)).To(BeTrue(), fmt.Sprintf("Update updateRun call produced error %s. Error type wanted is %s.", reflect.TypeOf(err), reflect.TypeOf(&k8sErrors.StatusError{}))) + Expect(statusErr.ErrStatus.Message).Should(MatchRegexp("stagedRolloutStrategyName is immutable")) + Expect(hubClient.Delete(ctx, &updateRun)).Should(Succeed()) + }) + + It("Should allow update of ClusterStagedUpdateRun state field", func() { + updateRun := placementv1beta1.ClusterStagedUpdateRun{ + ObjectMeta: metav1.ObjectMeta{ + Name: fmt.Sprintf(validupdateRunNameTemplate, GinkgoParallelProcess()), + }, + Spec: placementv1beta1.UpdateRunSpec{ + PlacementName: "test-placement", + ResourceSnapshotIndex: "1", + StagedUpdateStrategyName: "test-strategy", + State: placementv1beta1.StateNotStarted, + }, + } + Expect(hubClient.Create(ctx, &updateRun)).Should(Succeed()) + + updateRun.Spec.State = placementv1beta1.StateStarted + Expect(hubClient.Update(ctx, &updateRun)).Should(Succeed()) Expect(hubClient.Delete(ctx, &updateRun)).Should(Succeed()) }) }) @@ -1544,6 +1607,315 @@ var _ = Describe("Test placement v1beta1 API validation", func() { }) }) + Context("Test ClusterStagedUpdateRun State API validation - valid NotStarted state transitions", func() { + var updateRun *placementv1beta1.ClusterStagedUpdateRun + updateRunName := fmt.Sprintf(validupdateRunNameTemplate, GinkgoParallelProcess()) + + BeforeEach(func() { + updateRun = &placementv1beta1.ClusterStagedUpdateRun{ + ObjectMeta: metav1.ObjectMeta{ + Name: updateRunName, + }, + Spec: placementv1beta1.UpdateRunSpec{ + State: placementv1beta1.StateNotStarted, + }, + } + Expect(hubClient.Create(ctx, updateRun)).Should(Succeed()) + }) + + AfterEach(func() { + Expect(hubClient.Delete(ctx, updateRun)).Should(Succeed()) + }) + + It("should allow creation of ClusterStagedUpdateRun when state in unspecified", func() { + updateRunWithDefaultState := &placementv1beta1.ClusterStagedUpdateRun{ + ObjectMeta: metav1.ObjectMeta{ + Name: "unspecfied-state-update-run-" + fmt.Sprintf("%d", GinkgoParallelProcess()), + }, + Spec: placementv1beta1.UpdateRunSpec{ + // State not specified - should default to NotStarted + }, + } + Expect(hubClient.Create(ctx, updateRunWithDefaultState)).Should(Succeed()) + Expect(updateRunWithDefaultState.Spec.State).To(Equal(placementv1beta1.StateNotStarted)) + Expect(hubClient.Delete(ctx, updateRunWithDefaultState)).Should(Succeed()) + }) + + It("should allow creation of ClusterStagedUpdateRun with empty state (defaults to NotStarted)", func() { + updateRun := &placementv1beta1.ClusterStagedUpdateRun{ + ObjectMeta: metav1.ObjectMeta{ + Name: "empty-state-update-run-" + fmt.Sprintf("%d", GinkgoParallelProcess()), + }, + Spec: placementv1beta1.UpdateRunSpec{ + State: "", + }, + } + Expect(hubClient.Create(ctx, updateRun)).Should(Succeed()) + Expect(updateRun.Spec.State).To(Equal(placementv1beta1.StateNotStarted)) + Expect(hubClient.Delete(ctx, updateRun)).Should(Succeed()) + }) + + It("should allow transition from NotStarted to Started", func() { + updateRun.Spec.State = placementv1beta1.StateStarted + Expect(hubClient.Update(ctx, updateRun)).Should(Succeed()) + }) + + It("should allow transition from NotStarted to Abandoned", func() { + updateRun.Spec.State = placementv1beta1.StateAbandoned + Expect(hubClient.Update(ctx, updateRun)).Should(Succeed()) + }) + + It("should allow staying in NotStarted state", func() { + updateRun.Spec.State = placementv1beta1.StateNotStarted + Expect(hubClient.Update(ctx, updateRun)).Should(Succeed()) + }) + }) + + Context("Test ClusterStagedUpdateRun State API validation - valid Started state transitions", func() { + var updateRun *placementv1beta1.ClusterStagedUpdateRun + updateRunName := fmt.Sprintf(validupdateRunNameTemplate, GinkgoParallelProcess()) + + BeforeEach(func() { + updateRun = &placementv1beta1.ClusterStagedUpdateRun{ + ObjectMeta: metav1.ObjectMeta{ + Name: updateRunName, + }, + Spec: placementv1beta1.UpdateRunSpec{ + State: placementv1beta1.StateStarted, + }, + } + Expect(hubClient.Create(ctx, updateRun)).Should(Succeed()) + }) + + AfterEach(func() { + Expect(hubClient.Delete(ctx, updateRun)).Should(Succeed()) + }) + + It("should allow transition from Started to Stopped", func() { + updateRun.Spec.State = placementv1beta1.StateStopped + Expect(hubClient.Update(ctx, updateRun)).Should(Succeed()) + }) + + It("should allow transition from Started to Abandoned", func() { + updateRun.Spec.State = placementv1beta1.StateAbandoned + Expect(hubClient.Update(ctx, updateRun)).Should(Succeed()) + }) + + It("should allow staying in Started state", func() { + updateRun.Spec.State = placementv1beta1.StateStarted + Expect(hubClient.Update(ctx, updateRun)).Should(Succeed()) + }) + }) + + Context("Test ClusterStagedUpdateRun State API validation - valid Stopped state transitions", func() { + var updateRun *placementv1beta1.ClusterStagedUpdateRun + updateRunName := fmt.Sprintf(validupdateRunNameTemplate, GinkgoParallelProcess()) + + BeforeEach(func() { + updateRun = &placementv1beta1.ClusterStagedUpdateRun{ + ObjectMeta: metav1.ObjectMeta{ + Name: updateRunName, + }, + Spec: placementv1beta1.UpdateRunSpec{ + State: placementv1beta1.StateStarted, + }, + } + Expect(hubClient.Create(ctx, updateRun)).Should(Succeed()) + // Transition to Stopped state first + updateRun.Spec.State = placementv1beta1.StateStopped + Expect(hubClient.Update(ctx, updateRun)).Should(Succeed()) + }) + + AfterEach(func() { + Expect(hubClient.Delete(ctx, updateRun)).Should(Succeed()) + }) + + It("should allow transition from Stopped to Started", func() { + updateRun.Spec.State = placementv1beta1.StateStarted + Expect(hubClient.Update(ctx, updateRun)).Should(Succeed()) + }) + + It("should allow transition from Stopped to Abandoned", func() { + updateRun.Spec.State = placementv1beta1.StateAbandoned + Expect(hubClient.Update(ctx, updateRun)).Should(Succeed()) + }) + + It("should allow staying in Stopped state", func() { + updateRun.Spec.State = placementv1beta1.StateStopped + Expect(hubClient.Update(ctx, updateRun)).Should(Succeed()) + }) + }) + + Context("Test ClusterStagedUpdateRun State API validation - valid Abandoned state transitions", func() { + var updateRun *placementv1beta1.ClusterStagedUpdateRun + updateRunName := fmt.Sprintf(validupdateRunNameTemplate, GinkgoParallelProcess()) + + BeforeEach(func() { + updateRun = &placementv1beta1.ClusterStagedUpdateRun{ + ObjectMeta: metav1.ObjectMeta{ + Name: updateRunName, + }, + Spec: placementv1beta1.UpdateRunSpec{ + State: placementv1beta1.StateAbandoned, + }, + } + Expect(hubClient.Create(ctx, updateRun)).Should(Succeed()) + }) + + AfterEach(func() { + Expect(hubClient.Delete(ctx, updateRun)).Should(Succeed()) + }) + + It("should allow staying in Abandoned state", func() { + updateRun.Spec.State = placementv1beta1.StateAbandoned + Expect(hubClient.Update(ctx, updateRun)).Should(Succeed()) + }) + }) + + Context("Test ClusterStagedUpdateRun State API validation - invalid state transitions", func() { + var updateRun *placementv1beta1.ClusterStagedUpdateRun + updateRunName := fmt.Sprintf(validupdateRunNameTemplate, GinkgoParallelProcess()) + + AfterEach(func() { + if updateRun != nil { + Expect(hubClient.Delete(ctx, updateRun)).Should(Succeed()) + } + }) + + It("should deny transition from NotStarted to Stopped", func() { + updateRun = &placementv1beta1.ClusterStagedUpdateRun{ + ObjectMeta: metav1.ObjectMeta{ + Name: updateRunName, + }, + Spec: placementv1beta1.UpdateRunSpec{ + State: placementv1beta1.StateNotStarted, + }, + } + Expect(hubClient.Create(ctx, updateRun)).Should(Succeed()) + + updateRun.Spec.State = placementv1beta1.StateStopped + err := hubClient.Update(ctx, updateRun) + var statusErr *k8sErrors.StatusError + Expect(errors.As(err, &statusErr)).To(BeTrue(), fmt.Sprintf("Update ClusterStagedUpdateRun call produced error %s. Error type wanted is %s.", reflect.TypeOf(err), reflect.TypeOf(&k8sErrors.StatusError{}))) + Expect(statusErr.ErrStatus.Message).Should(MatchRegexp("invalid state transition: cannot transition from NotStarted to Stopped")) + }) + + It("should deny transition from Started to NotStarted", func() { + updateRun = &placementv1beta1.ClusterStagedUpdateRun{ + ObjectMeta: metav1.ObjectMeta{ + Name: updateRunName, + }, + Spec: placementv1beta1.UpdateRunSpec{ + State: placementv1beta1.StateStarted, + }, + } + Expect(hubClient.Create(ctx, updateRun)).Should(Succeed()) + + updateRun.Spec.State = placementv1beta1.StateNotStarted + err := hubClient.Update(ctx, updateRun) + var statusErr *k8sErrors.StatusError + Expect(errors.As(err, &statusErr)).To(BeTrue(), fmt.Sprintf("Update ClusterStagedUpdateRun call produced error %s. Error type wanted is %s.", reflect.TypeOf(err), reflect.TypeOf(&k8sErrors.StatusError{}))) + Expect(statusErr.ErrStatus.Message).Should(MatchRegexp("invalid state transition: cannot transition from Started to NotStarted")) + }) + + It("should deny transition from Stopped to NotStarted", func() { + updateRun = &placementv1beta1.ClusterStagedUpdateRun{ + ObjectMeta: metav1.ObjectMeta{ + Name: updateRunName, + }, + Spec: placementv1beta1.UpdateRunSpec{ + State: placementv1beta1.StateStarted, + }, + } + Expect(hubClient.Create(ctx, updateRun)).Should(Succeed()) + + // Transition to Stopped first + updateRun.Spec.State = placementv1beta1.StateStopped + Expect(hubClient.Update(ctx, updateRun)).Should(Succeed()) + + // Try to transition back to NotStarted + updateRun.Spec.State = placementv1beta1.StateNotStarted + err := hubClient.Update(ctx, updateRun) + var statusErr *k8sErrors.StatusError + Expect(errors.As(err, &statusErr)).To(BeTrue(), fmt.Sprintf("Update ClusterStagedUpdateRun call produced error %s. Error type wanted is %s.", reflect.TypeOf(err), reflect.TypeOf(&k8sErrors.StatusError{}))) + Expect(statusErr.ErrStatus.Message).Should(MatchRegexp("invalid state transition: cannot transition from Stopped to NotStarted")) + }) + + It("should deny transition from Abandoned to NotStarted", func() { + updateRun = &placementv1beta1.ClusterStagedUpdateRun{ + ObjectMeta: metav1.ObjectMeta{ + Name: updateRunName, + }, + Spec: placementv1beta1.UpdateRunSpec{ + State: placementv1beta1.StateAbandoned, + }, + } + Expect(hubClient.Create(ctx, updateRun)).Should(Succeed()) + + updateRun.Spec.State = placementv1beta1.StateNotStarted + err := hubClient.Update(ctx, updateRun) + var statusErr *k8sErrors.StatusError + Expect(errors.As(err, &statusErr)).To(BeTrue(), fmt.Sprintf("Update ClusterStagedUpdateRun call produced error %s. Error type wanted is %s.", reflect.TypeOf(err), reflect.TypeOf(&k8sErrors.StatusError{}))) + Expect(statusErr.ErrStatus.Message).Should(MatchRegexp("invalid state transition: Abandoned is a terminal state and cannot transition to any other state")) + }) + + It("should deny transition from Abandoned to Started", func() { + updateRun = &placementv1beta1.ClusterStagedUpdateRun{ + ObjectMeta: metav1.ObjectMeta{ + Name: updateRunName, + }, + Spec: placementv1beta1.UpdateRunSpec{ + State: placementv1beta1.StateAbandoned, + }, + } + Expect(hubClient.Create(ctx, updateRun)).Should(Succeed()) + + updateRun.Spec.State = placementv1beta1.StateStarted + err := hubClient.Update(ctx, updateRun) + var statusErr *k8sErrors.StatusError + Expect(errors.As(err, &statusErr)).To(BeTrue(), fmt.Sprintf("Update ClusterStagedUpdateRun call produced error %s. Error type wanted is %s.", reflect.TypeOf(err), reflect.TypeOf(&k8sErrors.StatusError{}))) + Expect(statusErr.ErrStatus.Message).Should(MatchRegexp("invalid state transition: Abandoned is a terminal state and cannot transition to any other state")) + }) + + It("should deny transition from Abandoned to Stopped", func() { + updateRun = &placementv1beta1.ClusterStagedUpdateRun{ + ObjectMeta: metav1.ObjectMeta{ + Name: updateRunName, + }, + Spec: placementv1beta1.UpdateRunSpec{ + State: placementv1beta1.StateAbandoned, + }, + } + Expect(hubClient.Create(ctx, updateRun)).Should(Succeed()) + + updateRun.Spec.State = placementv1beta1.StateStopped + err := hubClient.Update(ctx, updateRun) + var statusErr *k8sErrors.StatusError + Expect(errors.As(err, &statusErr)).To(BeTrue(), fmt.Sprintf("Update ClusterStagedUpdateRun call produced error %s. Error type wanted is %s.", reflect.TypeOf(err), reflect.TypeOf(&k8sErrors.StatusError{}))) + Expect(statusErr.ErrStatus.Message).Should(MatchRegexp("invalid state transition: Abandoned is a terminal state and cannot transition to any other state")) + }) + }) + + Context("Test ClusterStagedUpdateRun State API validation - invalid state values", func() { + It("should deny creation of ClusterStagedUpdateRun with invalid state value", func() { + updateRun := &placementv1beta1.ClusterStagedUpdateRun{ + ObjectMeta: metav1.ObjectMeta{ + Name: fmt.Sprintf(validupdateRunNameTemplate, GinkgoParallelProcess()), + }, + Spec: placementv1beta1.UpdateRunSpec{ + PlacementName: "test-placement", + ResourceSnapshotIndex: "1", + StagedUpdateStrategyName: "test-strategy", + State: "InvalidState", + }, + } + err := hubClient.Create(ctx, updateRun) + var statusErr *k8sErrors.StatusError + Expect(errors.As(err, &statusErr)).To(BeTrue(), fmt.Sprintf("Create ClusterStagedUpdateRun call produced error %s. Error type wanted is %s.", reflect.TypeOf(err), reflect.TypeOf(&k8sErrors.StatusError{}))) + Expect(statusErr.ErrStatus.Message).Should(MatchRegexp("supported values: \"NotStarted\", \"Started\", \"Stopped\", \"Abandoned\"")) + }) + }) + Context("Test ClusterResourceOverride API validation - valid cases", func() { It("should allow creation of ClusterResourceOverride without placement reference", func() { cro := createValidClusterResourceOverride( From 329acc3d1a0eb600312a706af86556dedca2a816 Mon Sep 17 00:00:00 2001 From: Arvind Thirumurugan Date: Wed, 12 Nov 2025 11:06:36 -0800 Subject: [PATCH 20/20] remove no transition update tests Signed-off-by: Arvind Thirumurugan --- .../api_validation_integration_test.go | 41 ------------------- 1 file changed, 41 deletions(-) diff --git a/test/apis/placement/v1beta1/api_validation_integration_test.go b/test/apis/placement/v1beta1/api_validation_integration_test.go index c5398afb4..d8eeb7d0c 100644 --- a/test/apis/placement/v1beta1/api_validation_integration_test.go +++ b/test/apis/placement/v1beta1/api_validation_integration_test.go @@ -1664,11 +1664,6 @@ var _ = Describe("Test placement v1beta1 API validation", func() { updateRun.Spec.State = placementv1beta1.StateAbandoned Expect(hubClient.Update(ctx, updateRun)).Should(Succeed()) }) - - It("should allow staying in NotStarted state", func() { - updateRun.Spec.State = placementv1beta1.StateNotStarted - Expect(hubClient.Update(ctx, updateRun)).Should(Succeed()) - }) }) Context("Test ClusterStagedUpdateRun State API validation - valid Started state transitions", func() { @@ -1700,11 +1695,6 @@ var _ = Describe("Test placement v1beta1 API validation", func() { updateRun.Spec.State = placementv1beta1.StateAbandoned Expect(hubClient.Update(ctx, updateRun)).Should(Succeed()) }) - - It("should allow staying in Started state", func() { - updateRun.Spec.State = placementv1beta1.StateStarted - Expect(hubClient.Update(ctx, updateRun)).Should(Succeed()) - }) }) Context("Test ClusterStagedUpdateRun State API validation - valid Stopped state transitions", func() { @@ -1739,37 +1729,6 @@ var _ = Describe("Test placement v1beta1 API validation", func() { updateRun.Spec.State = placementv1beta1.StateAbandoned Expect(hubClient.Update(ctx, updateRun)).Should(Succeed()) }) - - It("should allow staying in Stopped state", func() { - updateRun.Spec.State = placementv1beta1.StateStopped - Expect(hubClient.Update(ctx, updateRun)).Should(Succeed()) - }) - }) - - Context("Test ClusterStagedUpdateRun State API validation - valid Abandoned state transitions", func() { - var updateRun *placementv1beta1.ClusterStagedUpdateRun - updateRunName := fmt.Sprintf(validupdateRunNameTemplate, GinkgoParallelProcess()) - - BeforeEach(func() { - updateRun = &placementv1beta1.ClusterStagedUpdateRun{ - ObjectMeta: metav1.ObjectMeta{ - Name: updateRunName, - }, - Spec: placementv1beta1.UpdateRunSpec{ - State: placementv1beta1.StateAbandoned, - }, - } - Expect(hubClient.Create(ctx, updateRun)).Should(Succeed()) - }) - - AfterEach(func() { - Expect(hubClient.Delete(ctx, updateRun)).Should(Succeed()) - }) - - It("should allow staying in Abandoned state", func() { - updateRun.Spec.State = placementv1beta1.StateAbandoned - Expect(hubClient.Update(ctx, updateRun)).Should(Succeed()) - }) }) Context("Test ClusterStagedUpdateRun State API validation - invalid state transitions", func() {