Skip to content

Commit a71286b

Browse files
committed
Graceful Pipeline Run Termination
The implementation of TEP-0058: Graceful Pipeline Run Termination. The new `spec.Status` values: - `StoppedRunFinally` - To "stop" (i.e. let the tasks complete, then execute finally tasks) a Pipeline - `CancelledRunFinally` - To "cancel" (i.e. interrupt any executing non finally tasks, then execute finally tasks) - `Cancelled` - Same as today's `PipelineRunCancelled` - i.e. interrupt any executing tasks without running finally tasks The new `PipelineRun` condition reasons: - `CancelledRunningFinally` - indicates that pipeline has been gracefully cancelled, but final tasks are now running - `StoppedRunningFinally` - indicates that pipeline has been gracefully stopped, but final tasks are now running Status `PipelineRunCancelled` is deprecated (replaced by `Cancelled`). The same applies to the condition reason. New features hidden by alpha API fields flag.
1 parent 4cfc7b9 commit a71286b

19 files changed

+1580
-193
lines changed

docs/pipelineruns.md

Lines changed: 43 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -561,8 +561,9 @@ Task Runs:
561561
## Cancelling a `PipelineRun`
562562

563563
To cancel a `PipelineRun` that's currently executing, update its definition
564-
to mark it as cancelled. When you do so, the spawned `TaskRuns` are also marked
565-
as cancelled and all associated `Pods` are deleted. For example:
564+
to mark it as "PipelineRunCancelled". When you do so, the spawned `TaskRuns` are also marked
565+
as cancelled and all associated `Pods` are deleted. Pending final tasks are not scheduled.
566+
For example:
566567

567568
```yaml
568569
apiVersion: tekton.dev/v1beta1
@@ -574,6 +575,46 @@ spec:
574575
status: "PipelineRunCancelled"
575576
```
576577

578+
Warning: "PipelineRunCancelled" status is deprecated and would be removed in V1, please use "Cancelled" instead.
579+
580+
## Gracefully cancelling a `PipelineRun`
581+
582+
[Graceful pipeline run termination](https://github.com/tektoncd/community/blob/main/teps/0058-graceful-pipeline-run-termination.md)
583+
is currently an **_alpha feature_**.
584+
585+
To gracefully cancel a `PipelineRun` that's currently executing, update its definition
586+
to mark it as "CancelledRunFinally". When you do so, the spawned `TaskRuns` are also marked
587+
as cancelled and all associated `Pods` are deleted. Final tasks are scheduled normally.
588+
For example:
589+
590+
```yaml
591+
apiVersion: tekton.dev/v1beta1
592+
kind: PipelineRun
593+
metadata:
594+
name: go-example-git
595+
spec:
596+
# […]
597+
status: "CancelledRunFinally"
598+
```
599+
600+
601+
## Gracefully stopping a `PipelineRun`
602+
603+
To gracefully stop a `PipelineRun` that's currently executing, update its definition
604+
to mark it as "StoppedRunFinally". When you do so, the spawned `TaskRuns` are completed normally,
605+
but no new non-final task is scheduled. Final tasks are executed afterwards.
606+
For example:
607+
608+
```yaml
609+
apiVersion: tekton.dev/v1beta1
610+
kind: PipelineRun
611+
metadata:
612+
name: go-example-git
613+
spec:
614+
# […]
615+
status: "StoppedRunFinally"
616+
```
617+
577618
## Pending `PipelineRuns`
578619

579620
A `PipelineRun` can be created as a "pending" `PipelineRun` meaning that it will not actually be started until the pending status is cleared.

go.sum

Lines changed: 7 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

internal/builder/v1beta1/pipeline.go

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -115,12 +115,27 @@ func PipelineDescription(desc string) PipelineSpecOp {
115115
}
116116
}
117117

118-
// PipelineRunCancelled sets the status to cancel the PipelineRunSpec.
118+
// PipelineRunCancelled sets the status to Cancelled in the PipelineRunSpec.
119119
func PipelineRunCancelled(spec *v1beta1.PipelineRunSpec) {
120120
spec.Status = v1beta1.PipelineRunSpecStatusCancelled
121121
}
122122

123-
// PipelineRunPending sets the status to pending to the PipelineRunSpec.
123+
// PipelineRunCancelledDeprecated sets the status to PipelineRunCancelled in the PipelineRunSpec.
124+
func PipelineRunCancelledDeprecated(spec *v1beta1.PipelineRunSpec) {
125+
spec.Status = v1beta1.PipelineRunSpecStatusCancelledDeprecated
126+
}
127+
128+
// PipelineRunCancelledRunFinally sets the status to cancel and run finally in the PipelineRunSpec.
129+
func PipelineRunCancelledRunFinally(spec *v1beta1.PipelineRunSpec) {
130+
spec.Status = v1beta1.PipelineRunSpecStatusCancelledRunFinally
131+
}
132+
133+
// PipelineRunStoppedRunFinally sets the status to stop and run finally in the PipelineRunSpec.
134+
func PipelineRunStoppedRunFinally(spec *v1beta1.PipelineRunSpec) {
135+
spec.Status = v1beta1.PipelineRunSpecStatusStoppedRunFinally
136+
}
137+
138+
// PipelineRunPending sets the status to pending in the PipelineRunSpec.
124139
func PipelineRunPending(spec *v1beta1.PipelineRunSpec) {
125140
spec.Status = v1beta1.PipelineRunSpecStatusPending
126141
}

internal/builder/v1beta1/pipeline_test.go

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -469,6 +469,69 @@ func TestPipelineRunWithFinalTask(t *testing.T) {
469469
}
470470
}
471471

472+
func TestPipelineRunCancelled(t *testing.T) {
473+
pipelineRun := tb.PipelineRun("pear", tb.PipelineRunNamespace("foo"),
474+
tb.PipelineRunSpec("pears", tb.PipelineRunCancelled))
475+
476+
expectedPipelineRun := &v1beta1.PipelineRun{
477+
ObjectMeta: metav1.ObjectMeta{
478+
Name: "pear",
479+
Namespace: "foo",
480+
},
481+
Spec: v1beta1.PipelineRunSpec{
482+
PipelineRef: &v1beta1.PipelineRef{Name: "pears"},
483+
Status: v1beta1.PipelineRunSpecStatusCancelled,
484+
Timeout: &metav1.Duration{Duration: 1 * time.Hour},
485+
},
486+
}
487+
488+
if diff := cmp.Diff(expectedPipelineRun, pipelineRun); diff != "" {
489+
t.Fatalf("PipelineRun diff -want, +got: %s", diff)
490+
}
491+
}
492+
493+
func TestPipelineRunCancelledRunFinally(t *testing.T) {
494+
pipelineRun := tb.PipelineRun("pear", tb.PipelineRunNamespace("foo"),
495+
tb.PipelineRunSpec("pears", tb.PipelineRunCancelledRunFinally))
496+
497+
expectedPipelineRun := &v1beta1.PipelineRun{
498+
ObjectMeta: metav1.ObjectMeta{
499+
Name: "pear",
500+
Namespace: "foo",
501+
},
502+
Spec: v1beta1.PipelineRunSpec{
503+
PipelineRef: &v1beta1.PipelineRef{Name: "pears"},
504+
Status: v1beta1.PipelineRunSpecStatusCancelledRunFinally,
505+
Timeout: &metav1.Duration{Duration: 1 * time.Hour},
506+
},
507+
}
508+
509+
if diff := cmp.Diff(expectedPipelineRun, pipelineRun); diff != "" {
510+
t.Fatalf("PipelineRun diff -want, +got: %s", diff)
511+
}
512+
}
513+
514+
func TestPipelineRunStoppedRunFinally(t *testing.T) {
515+
pipelineRun := tb.PipelineRun("pear", tb.PipelineRunNamespace("foo"),
516+
tb.PipelineRunSpec("pears", tb.PipelineRunStoppedRunFinally))
517+
518+
expectedPipelineRun := &v1beta1.PipelineRun{
519+
ObjectMeta: metav1.ObjectMeta{
520+
Name: "pear",
521+
Namespace: "foo",
522+
},
523+
Spec: v1beta1.PipelineRunSpec{
524+
PipelineRef: &v1beta1.PipelineRef{Name: "pears"},
525+
Status: v1beta1.PipelineRunSpecStatusStoppedRunFinally,
526+
Timeout: &metav1.Duration{Duration: 1 * time.Hour},
527+
},
528+
}
529+
530+
if diff := cmp.Diff(expectedPipelineRun, pipelineRun); diff != "" {
531+
t.Fatalf("PipelineRun diff -want, +got: %s", diff)
532+
}
533+
}
534+
472535
func getTaskSpec() v1beta1.TaskSpec {
473536
return v1beta1.TaskSpec{
474537
Steps: []v1beta1.Step{{Container: corev1.Container{

pkg/apis/pipeline/v1alpha1/pipelinerun_types.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ type PipelineRunSpecStatus = v1beta1.PipelineRunSpecStatus
101101
const (
102102
// PipelineRunSpecStatusCancelled indicates that the user wants to cancel the task,
103103
// if not already cancelled or terminated
104-
PipelineRunSpecStatusCancelled = v1beta1.PipelineRunSpecStatusCancelled
104+
PipelineRunSpecStatusCancelled = v1beta1.PipelineRunSpecStatusCancelledDeprecated
105105
)
106106

107107
// PipelineResourceRef can be used to refer to a specific instance of a Resource

pkg/apis/pipeline/v1beta1/pipelinerun_types.go

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,17 @@ func (pr *PipelineRun) HasStarted() bool {
8585

8686
// IsCancelled returns true if the PipelineRun's spec status is set to Cancelled state
8787
func (pr *PipelineRun) IsCancelled() bool {
88-
return pr.Spec.Status == PipelineRunSpecStatusCancelled
88+
return pr.Spec.Status == PipelineRunSpecStatusCancelled || pr.Spec.Status == PipelineRunSpecStatusCancelledDeprecated
89+
}
90+
91+
// IsGracefullyCancelled returns true if the PipelineRun's spec status is set to CancelledRunFinally state
92+
func (pr *PipelineRun) IsGracefullyCancelled() bool {
93+
return pr.Spec.Status == PipelineRunSpecStatusCancelledRunFinally
94+
}
95+
96+
// IsGracefullyStopped returns true if the PipelineRun's spec status is set to StoppedRunFinally state
97+
func (pr *PipelineRun) IsGracefullyStopped() bool {
98+
return pr.Spec.Status == PipelineRunSpecStatusStoppedRunFinally
8999
}
90100

91101
func (pr *PipelineRun) GetTimeout(ctx context.Context) time.Duration {
@@ -216,9 +226,22 @@ type TimeoutFields struct {
216226
type PipelineRunSpecStatus string
217227

218228
const (
229+
// Deprecated: "PipelineRunCancelled" indicates that the user wants to cancel the task,
230+
// if not already cancelled or terminated (replaced by "Cancelled")
231+
PipelineRunSpecStatusCancelledDeprecated = "PipelineRunCancelled"
232+
219233
// PipelineRunSpecStatusCancelled indicates that the user wants to cancel the task,
220234
// if not already cancelled or terminated
221-
PipelineRunSpecStatusCancelled = "PipelineRunCancelled"
235+
PipelineRunSpecStatusCancelled = "Cancelled"
236+
237+
// PipelineRunSpecStatusCancelledRunFinally indicates that the user wants to cancel the pipeline run,
238+
// if not already cancelled or terminated, but ensure finally is run normally
239+
PipelineRunSpecStatusCancelledRunFinally = "CancelledRunFinally"
240+
241+
// PipelineRunSpecStatusStoppedRunFinally indicates that the user wants to stop the pipeline run,
242+
// wait for already running tasks to be completed and run finally
243+
// if not already cancelled or terminated
244+
PipelineRunSpecStatusStoppedRunFinally = "StoppedRunFinally"
222245

223246
// PipelineRunSpecStatusPending indicates that the user wants to postpone starting a PipelineRun
224247
// until some condition is met
@@ -271,6 +294,12 @@ const (
271294
// PipelineRunReasonStopping indicates that no new Tasks will be scheduled by the controller, and the
272295
// pipeline will stop once all running tasks complete their work
273296
PipelineRunReasonStopping PipelineRunReason = "PipelineRunStopping"
297+
// PipelineRunReasonCancelledRunningFinally indicates that pipeline has been gracefully cancelled
298+
// and no new Tasks will be scheduled by the controller, but final tasks are now running
299+
PipelineRunReasonCancelledRunningFinally PipelineRunReason = "CancelledRunningFinally"
300+
// PipelineRunReasonStoppedRunningFinally indicates that pipeline has been gracefully stopped
301+
// and no new Tasks will be scheduled by the controller, but final tasks are now running
302+
PipelineRunReasonStoppedRunningFinally PipelineRunReason = "StoppedRunningFinally"
274303
)
275304

276305
func (t PipelineRunReason) String() string {

pkg/apis/pipeline/v1beta1/pipelinerun_types_test.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,28 @@ func TestPipelineRunIsCancelled(t *testing.T) {
135135
}
136136
}
137137

138+
func TestPipelineRunIsGracefullyCancelled(t *testing.T) {
139+
pr := &v1beta1.PipelineRun{
140+
Spec: v1beta1.PipelineRunSpec{
141+
Status: v1beta1.PipelineRunSpecStatusCancelledRunFinally,
142+
},
143+
}
144+
if !pr.IsGracefullyCancelled() {
145+
t.Fatal("Expected pipelinerun status to be gracefully cancelled")
146+
}
147+
}
148+
149+
func TestPipelineRunIsGracefullyStopped(t *testing.T) {
150+
pr := &v1beta1.PipelineRun{
151+
Spec: v1beta1.PipelineRunSpec{
152+
Status: v1beta1.PipelineRunSpecStatusStoppedRunFinally,
153+
},
154+
}
155+
if !pr.IsGracefullyStopped() {
156+
t.Fatal("Expected pipelinerun status to be gracefully stopped")
157+
}
158+
}
159+
138160
func TestPipelineRunHasVolumeClaimTemplate(t *testing.T) {
139161
pr := &v1beta1.PipelineRun{
140162
Spec: v1beta1.PipelineRunSpec{

pkg/apis/pipeline/v1beta1/pipelinerun_validation.go

Lines changed: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -115,11 +115,7 @@ func (ps *PipelineRunSpec) Validate(ctx context.Context) (errs *apis.FieldError)
115115
}
116116
}
117117

118-
if ps.Status != "" {
119-
if ps.Status != PipelineRunSpecStatusCancelled && ps.Status != PipelineRunSpecStatusPending {
120-
errs = errs.Also(apis.ErrInvalidValue(fmt.Sprintf("%s should be %s or %s", ps.Status, PipelineRunSpecStatusCancelled, PipelineRunSpecStatusPending), "status"))
121-
}
122-
}
118+
errs = errs.Also(validateSpecStatus(ctx, ps.Status))
123119

124120
if ps.Workspaces != nil {
125121
wsNames := make(map[string]int)
@@ -135,6 +131,32 @@ func (ps *PipelineRunSpec) Validate(ctx context.Context) (errs *apis.FieldError)
135131
return errs
136132
}
137133

134+
func validateSpecStatus(ctx context.Context, status PipelineRunSpecStatus) *apis.FieldError {
135+
switch status {
136+
case "":
137+
return nil
138+
case PipelineRunSpecStatusPending,
139+
PipelineRunSpecStatusCancelledDeprecated:
140+
return nil
141+
case PipelineRunSpecStatusCancelled,
142+
PipelineRunSpecStatusCancelledRunFinally,
143+
PipelineRunSpecStatusStoppedRunFinally:
144+
return ValidateEnabledAPIFields(ctx, "graceful termination", "alpha")
145+
}
146+
147+
cfg := config.FromContextOrDefaults(ctx)
148+
if cfg.FeatureFlags.EnableAPIFields == config.AlphaAPIFields {
149+
return apis.ErrInvalidValue(fmt.Sprintf("%s should be %s, %s, %s or %s", status,
150+
PipelineRunSpecStatusCancelled,
151+
PipelineRunSpecStatusCancelledRunFinally,
152+
PipelineRunSpecStatusStoppedRunFinally,
153+
PipelineRunSpecStatusPending), "status")
154+
}
155+
return apis.ErrInvalidValue(fmt.Sprintf("%s should be %s or %s", status,
156+
PipelineRunSpecStatusCancelledDeprecated,
157+
PipelineRunSpecStatusPending), "status")
158+
}
159+
138160
func validateTimeoutDuration(field string, d *metav1.Duration) (errs *apis.FieldError) {
139161
if d != nil && d.Duration < 0 {
140162
fieldPath := fmt.Sprintf("timeouts.%s", field)

0 commit comments

Comments
 (0)