Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# Support Delete Stage Tasks

## Overview

Allow staged update strategies to gate the implicit deletion stage with the existing Approval and TimedWait task types.

## Plan

1. Add `deleteStageTasks` to the shared update strategy API with the same validation as after-stage tasks.
2. Snapshot and initialize delete-stage task statuses.
3. Refactor the after-stage task evaluator to accept a stage configuration and status directly, then use it before deleting bindings.
4. Add API validation, unit, and cluster-scoped/namespaced integration coverage.
5. Regenerate API code and CRDs, then run targeted tests and repository quality checks.

## Success Criteria

- [x] Unset delete-stage tasks preserve immediate deletion.
- [x] Approval and timed waits can independently gate deletion.
- [x] Approval and timed waits run concurrently and both must pass.
- [x] Cluster-scoped and namespaced runs retain bindings until their gate passes.
- [x] Generated API and CRD artifacts are current.
- [x] Targeted tests and available repository quality checks pass.

## Approval

Implementation was explicitly requested in the issue task.

## Implementation Notes

- Delete-stage approvals reuse the after-stage task machinery and labels.
- The delete stage uses a label-safe value for approval requests while retaining its canonical status/spec stage name.
- Gates run only before deletion starts; stopping while gated leaves bindings intact.
- Documentation updates are deferred to the separate documentation repository.
4 changes: 4 additions & 0 deletions apis/placement/v1beta1/commons.go
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,8 @@ const (

// UpdateRunDeleteStageName is the name of delete stage in the staged update run.
UpdateRunDeleteStageName = FleetPrefix + "deleteStage"
// UpdateRunDeleteStageLabelValue is the label value used for the delete stage.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: extra line before this. follow the file structure.

UpdateRunDeleteStageLabelValue = "deleteStage"

// IsLatestUpdateRunApprovalLabel indicates if the approval is the latest approval on a staged run.
IsLatestUpdateRunApprovalLabel = FleetPrefix + "isLatestUpdateRunApproval"
Expand All @@ -186,6 +188,8 @@ const (

// AfterStageApprovalTaskNameFmt is the format of the after stage approval task name.
AfterStageApprovalTaskNameFmt = "%s-after-%s"
// DeleteStageApprovalTaskNameFmt is the format of the delete stage approval task name.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: extra line before this. follow the file structure.

DeleteStageApprovalTaskNameFmt = "%s-after-delete-stage"

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the difference between AfterStageApprovalTaskNameFmt and AfterStageApprovalTaskNameFmt? Why can't we have one generic? Names don't make that much sense.

AfterStageApprovalTaskNameFmt sounds generic like after any stage and that makes me think that Delete is also a stage, right?

)

var (
Expand Down
10 changes: 10 additions & 0 deletions apis/placement/v1beta1/stageupdate_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,16 @@ type UpdateStrategySpec struct {
// +kubebuilder:validation:MaxItems=31
// +kubebuilder:validation:Required
Stages []StageConfig `json:"stages"`

// DeleteStageTasks is the collection of tasks that must complete before the deletion stage starts.
// Each task is executed in parallel and there cannot be more than one task of the same type.
// +kubebuilder:validation:MaxItems=2
// +kubebuilder:validation:Optional
// +kubebuilder:validation:XValidation:rule="!(self.size() == 2 && self[0].type == self[1].type)",message="deleteStageTasks cannot have two tasks of the same type"
// +kubebuilder:validation:XValidation:rule="!self.exists(e, e.type == 'Approval' && has(e.waitTime))",message="DeleteStageTaskType is Approval, waitTime is not allowed"
// +kubebuilder:validation:XValidation:rule="!self.exists(e, e.type == 'TimedWait' && !has(e.waitTime))",message="DeleteStageTaskType is TimedWait, waitTime is required"
// +kubebuilder:validation:XValidation:rule="!self.exists(e, e.type == 'TimedWait' && has(e.waitTime) && duration(e.waitTime) <= duration('0s'))",message="DeleteStageTaskType is TimedWait, waitTime must be greater than zero"
DeleteStageTasks []StageTask `json:"deleteStageTasks,omitempty"`

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same comment as above. Why can't we have something generic and not specific to the Delete stage?

}

// ClusterStagedUpdateStrategyList contains a list of StagedUpdateStrategy.
Expand Down
7 changes: 7 additions & 0 deletions apis/placement/v1beta1/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -3242,6 +3242,43 @@ spec:
The update run fails to initialize if the strategy fails to produce a valid list of stages where each selected
cluster is included in exactly one stage.
properties:
deleteStageTasks:
description: |-
DeleteStageTasks is the collection of tasks that must complete before the deletion stage starts.
Each task is executed in parallel and there cannot be more than one task of the same type.
items:
description: StageTask is the pre or post stage task that needs
to be completed before starting or moving to the next stage.
properties:
type:
description: The type of the before or after stage task.
enum:
- TimedWait
- Approval
type: string
waitTime:
description: |-
The time to wait after all the clusters in the current stage complete the update before moving to the next stage.
Only hours (h), minutes (m), and seconds (s) units are accepted.
pattern: ^(?:(?:0|[1-9][0-9]*)(\.[0-9]+)?(?:s|m|h))+$
type: string
required:
- type
type: object
maxItems: 2
type: array
x-kubernetes-validations:
- message: deleteStageTasks cannot have two tasks of the same
type
rule: '!(self.size() == 2 && self[0].type == self[1].type)'
- message: DeleteStageTaskType is Approval, waitTime is not allowed
rule: '!self.exists(e, e.type == ''Approval'' && has(e.waitTime))'
- message: DeleteStageTaskType is TimedWait, waitTime is required
rule: '!self.exists(e, e.type == ''TimedWait'' && !has(e.waitTime))'
- message: DeleteStageTaskType is TimedWait, waitTime must be
greater than zero
rule: '!self.exists(e, e.type == ''TimedWait'' && has(e.waitTime)
&& duration(e.waitTime) <= duration(''0s''))'
stages:
description: Stage specifies the configuration for each update
stage.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -375,6 +375,42 @@ spec:
spec:
description: The desired state of ClusterStagedUpdateStrategy.
properties:
deleteStageTasks:
description: |-
DeleteStageTasks is the collection of tasks that must complete before the deletion stage starts.
Each task is executed in parallel and there cannot be more than one task of the same type.
items:
description: StageTask is the pre or post stage task that needs
to be completed before starting or moving to the next stage.
properties:
type:
description: The type of the before or after stage task.
enum:
- TimedWait
- Approval
type: string
waitTime:
description: |-
The time to wait after all the clusters in the current stage complete the update before moving to the next stage.
Only hours (h), minutes (m), and seconds (s) units are accepted.
pattern: ^(?:(?:0|[1-9][0-9]*)(\.[0-9]+)?(?:s|m|h))+$
type: string
required:
- type
type: object
maxItems: 2
type: array
x-kubernetes-validations:
- message: deleteStageTasks cannot have two tasks of the same type
rule: '!(self.size() == 2 && self[0].type == self[1].type)'
- message: DeleteStageTaskType is Approval, waitTime is not allowed
rule: '!self.exists(e, e.type == ''Approval'' && has(e.waitTime))'
- message: DeleteStageTaskType is TimedWait, waitTime is required
rule: '!self.exists(e, e.type == ''TimedWait'' && !has(e.waitTime))'
- message: DeleteStageTaskType is TimedWait, waitTime must be greater
than zero
rule: '!self.exists(e, e.type == ''TimedWait'' && has(e.waitTime)
&& duration(e.waitTime) <= duration(''0s''))'
stages:
description: Stage specifies the configuration for each update stage.
items:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2162,6 +2162,43 @@ spec:
The update run fails to initialize if the strategy fails to produce a valid list of stages where each selected
cluster is included in exactly one stage.
properties:
deleteStageTasks:
description: |-
DeleteStageTasks is the collection of tasks that must complete before the deletion stage starts.
Each task is executed in parallel and there cannot be more than one task of the same type.
items:
description: StageTask is the pre or post stage task that needs
to be completed before starting or moving to the next stage.
properties:
type:
description: The type of the before or after stage task.
enum:
- TimedWait
- Approval
type: string
waitTime:
description: |-
The time to wait after all the clusters in the current stage complete the update before moving to the next stage.
Only hours (h), minutes (m), and seconds (s) units are accepted.
pattern: ^(?:(?:0|[1-9][0-9]*)(\.[0-9]+)?(?:s|m|h))+$
type: string
required:
- type
type: object
maxItems: 2
type: array
x-kubernetes-validations:
- message: deleteStageTasks cannot have two tasks of the same
type
rule: '!(self.size() == 2 && self[0].type == self[1].type)'
- message: DeleteStageTaskType is Approval, waitTime is not allowed
rule: '!self.exists(e, e.type == ''Approval'' && has(e.waitTime))'
- message: DeleteStageTaskType is TimedWait, waitTime is required
rule: '!self.exists(e, e.type == ''TimedWait'' && !has(e.waitTime))'
- message: DeleteStageTaskType is TimedWait, waitTime must be
greater than zero
rule: '!self.exists(e, e.type == ''TimedWait'' && has(e.waitTime)
&& duration(e.waitTime) <= duration(''0s''))'
stages:
description: Stage specifies the configuration for each update
stage.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,42 @@ spec:
spec:
description: The desired state of StagedUpdateStrategy.
properties:
deleteStageTasks:
description: |-
DeleteStageTasks is the collection of tasks that must complete before the deletion stage starts.
Each task is executed in parallel and there cannot be more than one task of the same type.
items:
description: StageTask is the pre or post stage task that needs
to be completed before starting or moving to the next stage.
properties:
type:
description: The type of the before or after stage task.
enum:
- TimedWait
- Approval
type: string
waitTime:
description: |-
The time to wait after all the clusters in the current stage complete the update before moving to the next stage.
Only hours (h), minutes (m), and seconds (s) units are accepted.
pattern: ^(?:(?:0|[1-9][0-9]*)(\.[0-9]+)?(?:s|m|h))+$
type: string
required:
- type
type: object
maxItems: 2
type: array
x-kubernetes-validations:
- message: deleteStageTasks cannot have two tasks of the same type
rule: '!(self.size() == 2 && self[0].type == self[1].type)'
- message: DeleteStageTaskType is Approval, waitTime is not allowed
rule: '!self.exists(e, e.type == ''Approval'' && has(e.waitTime))'
- message: DeleteStageTaskType is TimedWait, waitTime is required
rule: '!self.exists(e, e.type == ''TimedWait'' && !has(e.waitTime))'
- message: DeleteStageTaskType is TimedWait, waitTime must be greater
than zero
rule: '!self.exists(e, e.type == ''TimedWait'' && has(e.waitTime)
&& duration(e.waitTime) <= duration(''0s''))'
stages:
description: Stage specifies the configuration for each update stage.
items:
Expand Down
7 changes: 6 additions & 1 deletion pkg/controllers/updaterun/controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -493,7 +493,7 @@ func handleApprovalRequestDelete(obj client.Object, q workqueue.TypedRateLimitin
}

func removeWaitTimeFromUpdateRunStatus(updateRun placementv1beta1.UpdateRunObj) {
// Remove waitTime from the updateRun status for BeforeStageTask and AfterStageTask for type Approval.
// Remove waitTime from the updateRun status for Approval tasks.
updateRunStatus := updateRun.GetUpdateRunStatus()
if updateRunStatus.UpdateStrategySnapshot != nil {
for i := range updateRunStatus.UpdateStrategySnapshot.Stages {
Expand All @@ -508,6 +508,11 @@ func removeWaitTimeFromUpdateRunStatus(updateRun placementv1beta1.UpdateRunObj)
}
}
}
for i := range updateRunStatus.UpdateStrategySnapshot.DeleteStageTasks {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same question as above about being generic or not? Could we use an interface pattern for this?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just a suggestion. Doesn't mean that we should use.

if updateRunStatus.UpdateStrategySnapshot.DeleteStageTasks[i].Type == placementv1beta1.StageTaskTypeApproval {
updateRunStatus.UpdateStrategySnapshot.DeleteStageTasks[i].WaitTime = nil
}
}
}
}

Expand Down
22 changes: 22 additions & 0 deletions pkg/controllers/updaterun/controller_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -986,6 +986,28 @@ func TestRemoveWaitTimeFromUpdateRunStatus(t *testing.T) {
},
},
},
"should remove waitTime from Approval tasks only for DeleteStageTasks": {
inputUpdateRun: &placementv1beta1.ClusterStagedUpdateRun{
Status: placementv1beta1.UpdateRunStatus{
UpdateStrategySnapshot: &placementv1beta1.UpdateStrategySpec{
DeleteStageTasks: []placementv1beta1.StageTask{
{Type: placementv1beta1.StageTaskTypeApproval, WaitTime: &waitTime},
{Type: placementv1beta1.StageTaskTypeTimedWait, WaitTime: &waitTime},
},
},
},
},
wantUpdateRun: &placementv1beta1.ClusterStagedUpdateRun{
Status: placementv1beta1.UpdateRunStatus{
UpdateStrategySnapshot: &placementv1beta1.UpdateStrategySpec{
DeleteStageTasks: []placementv1beta1.StageTask{
{Type: placementv1beta1.StageTaskTypeApproval},
{Type: placementv1beta1.StageTaskTypeTimedWait, WaitTime: &waitTime},
},
},
},
},
},
"should handle multiple stages": {
inputUpdateRun: &placementv1beta1.ClusterStagedUpdateRun{
Status: placementv1beta1.UpdateRunStatus{
Expand Down
Loading
Loading