Skip to content

Commit 53b373d

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 `PipelineRunCancelled` is deprecated (replaced by `Cancelled`)
1 parent 565c55b commit 53b373d

File tree

11 files changed

+705
-30
lines changed

11 files changed

+705
-30
lines changed

docs/pipelineruns.md

Lines changed: 39 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -525,8 +525,44 @@ Task Runs:
525525
## Cancelling a `PipelineRun`
526526

527527
To cancel a `PipelineRun` that's currently executing, update its definition
528-
to mark it as cancelled. When you do so, the spawned `TaskRuns` are also marked
529-
as cancelled and all associated `Pods` are deleted. For example:
528+
to mark it as "Cancelled". When you do so, the spawned `TaskRuns` are also marked
529+
as cancelled and all associated `Pods` are deleted. Pending final tasks are not scheduled.
530+
For example:
531+
532+
```yaml
533+
apiVersion: tekton.dev/v1beta1
534+
kind: PipelineRun
535+
metadata:
536+
name: go-example-git
537+
spec:
538+
# […]
539+
status: "Cancelled"
540+
```
541+
542+
## Gracefully cancelling a `PipelineRun`
543+
544+
To gracefully cancel a `PipelineRun` that's currently executing, update its definition
545+
to mark it as "CancelledRunFinally". When you do so, the spawned `TaskRuns` are also marked
546+
as cancelled and all associated `Pods` are deleted. Final tasks are scheduled normally.
547+
For example:
548+
549+
```yaml
550+
apiVersion: tekton.dev/v1beta1
551+
kind: PipelineRun
552+
metadata:
553+
name: go-example-git
554+
spec:
555+
# […]
556+
status: "CancelledRunFinally"
557+
```
558+
559+
560+
## Gracefully stopping a `PipelineRun`
561+
562+
To gracefully stop a `PipelineRun` that's currently executing, update its definition
563+
to mark it as "StoppedRunFinally". When you do so, the spawned `TaskRuns` are completed normally,
564+
but no new non-final task is scheduled. Final tasks are executed afterwards.
565+
For example:
530566

531567
```yaml
532568
apiVersion: tekton.dev/v1beta1
@@ -535,7 +571,7 @@ metadata:
535571
name: go-example-git
536572
spec:
537573
# […]
538-
status: "PipelineRunCancelled"
574+
status: "StoppedRunFinally"
539575
```
540576

541577
## Pending `PipelineRuns`

internal/builder/v1beta1/pipeline.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,16 @@ func PipelineRunCancelled(spec *v1beta1.PipelineRunSpec) {
120120
spec.Status = v1beta1.PipelineRunSpecStatusCancelled
121121
}
122122

123+
// PipelineRunCancelledRunFinally sets the status to cancel and run finally the PipelineRunSpec.
124+
func PipelineRunCancelledRunFinally(spec *v1beta1.PipelineRunSpec) {
125+
spec.Status = v1beta1.PipelineRunSpecStatusCancelledRunFinally
126+
}
127+
128+
// PipelineRunStoppedRunFinally sets the status to stop and run finally the PipelineRunSpec.
129+
func PipelineRunStoppedRunFinally(spec *v1beta1.PipelineRunSpec) {
130+
spec.Status = v1beta1.PipelineRunSpecStatusStoppedRunFinally
131+
}
132+
123133
// PipelineRunPending sets the status to pending to the PipelineRunSpec.
124134
func PipelineRunPending(spec *v1beta1.PipelineRunSpec) {
125135
spec.Status = v1beta1.PipelineRunSpecStatusPending

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: 25 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+
// IsStopped returns true if the PipelineRun's spec status is set to Stopped state
92+
func (pr *PipelineRun) IsStopped() bool {
93+
return pr.Spec.Status == PipelineRunSpecStatusCancelledRunFinally
94+
}
95+
96+
// IsStopping returns true if the PipelineRun's spec status is set to Stopping state
97+
func (pr *PipelineRun) IsStopping() bool {
98+
return pr.Spec.Status == PipelineRunSpecStatusStoppedRunFinally
8999
}
90100

91101
func (pr *PipelineRun) GetTimeout(ctx context.Context) time.Duration {
@@ -193,9 +203,22 @@ type PipelineRunSpec struct {
193203
type PipelineRunSpecStatus string
194204

195205
const (
206+
// PipelineRunSpecStatusCancelledDeprecated indicates that the user wants to cancel the task,
207+
// if not already cancelled or terminated (deprecated: replaced by PipelineRunSpecStatusCancelled)
208+
PipelineRunSpecStatusCancelledDeprecated = "PipelineRunCancelled"
209+
196210
// PipelineRunSpecStatusCancelled indicates that the user wants to cancel the task,
197211
// if not already cancelled or terminated
198-
PipelineRunSpecStatusCancelled = "PipelineRunCancelled"
212+
PipelineRunSpecStatusCancelled = "Cancelled"
213+
214+
// PipelineRunSpecStatusCancelledRunFinally indicates that the user wants to cancel the pipeline run,
215+
// if not already cancelled or terminated, but ensure finally is run normally
216+
PipelineRunSpecStatusCancelledRunFinally = "CancelledRunFinally"
217+
218+
// PipelineRunSpecStatusStoppedRunFinally indicates that the user wants to stop the pipeline run,
219+
// wait for already running tasks to be completed and run finally
220+
// if not already cancelled or terminated
221+
PipelineRunSpecStatusStoppedRunFinally = "StoppedRunFinally"
199222

200223
// PipelineRunSpecStatusPending indicates that the user wants to postpone starting a PipelineRun
201224
// until some condition is met

pkg/apis/pipeline/v1beta1/pipelinerun_validation.go

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -82,11 +82,7 @@ func (ps *PipelineRunSpec) Validate(ctx context.Context) (errs *apis.FieldError)
8282
}
8383
}
8484

85-
if ps.Status != "" {
86-
if ps.Status != PipelineRunSpecStatusCancelled && ps.Status != PipelineRunSpecStatusPending {
87-
errs = errs.Also(apis.ErrInvalidValue(fmt.Sprintf("%s should be %s or %s", ps.Status, PipelineRunSpecStatusCancelled, PipelineRunSpecStatusPending), "status"))
88-
}
89-
}
85+
errs = errs.Also(validateSpecStatus(ps.Status))
9086

9187
if ps.Workspaces != nil {
9288
wsNames := make(map[string]int)
@@ -101,3 +97,18 @@ func (ps *PipelineRunSpec) Validate(ctx context.Context) (errs *apis.FieldError)
10197

10298
return errs
10399
}
100+
101+
func validateSpecStatus(status PipelineRunSpecStatus) *apis.FieldError {
102+
switch status {
103+
case "":
104+
return nil
105+
case PipelineRunSpecStatusCancelledDeprecated,
106+
PipelineRunSpecStatusCancelled,
107+
PipelineRunSpecStatusCancelledRunFinally,
108+
PipelineRunSpecStatusStoppedRunFinally:
109+
return nil
110+
}
111+
return apis.ErrInvalidValue(fmt.Sprintf("%s should be %s, %s, %s or %s", status,
112+
PipelineRunSpecStatusCancelled, PipelineRunSpecStatusCancelledRunFinally,
113+
PipelineRunSpecStatusStoppedRunFinally, PipelineRunSpecStatusPending), "status")
114+
}

pkg/reconciler/pipelinerun/cancel.go

Lines changed: 57 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import (
2020
"context"
2121
"encoding/json"
2222
"fmt"
23+
"github.com/tektoncd/pipeline/pkg/reconciler/pipeline/dag"
2324
"log"
2425
"strings"
2526
"time"
@@ -59,11 +60,51 @@ func init() {
5960

6061
// cancelPipelineRun marks the PipelineRun as cancelled and any resolved TaskRun(s) too.
6162
func cancelPipelineRun(ctx context.Context, logger *zap.SugaredLogger, pr *v1beta1.PipelineRun, clientSet clientset.Interface) error {
63+
errs := cancelPipelineTaskRuns(ctx, logger, pr, nil, clientSet)
64+
65+
// If we successfully cancelled all the TaskRuns and Runs, we can consider the PipelineRun cancelled.
66+
if len(errs) == 0 {
67+
pr.Status.SetCondition(&apis.Condition{
68+
Type: apis.ConditionSucceeded,
69+
Status: corev1.ConditionFalse,
70+
Reason: ReasonCancelled,
71+
Message: fmt.Sprintf("PipelineRun %q was cancelled", pr.Name),
72+
})
73+
// update pr completed time
74+
pr.Status.CompletionTime = &metav1.Time{Time: time.Now()}
75+
} else {
76+
e := strings.Join(errs, "\n")
77+
// Indicate that we failed to cancel the PipelineRun
78+
pr.Status.SetCondition(&apis.Condition{
79+
Type: apis.ConditionSucceeded,
80+
Status: corev1.ConditionUnknown,
81+
Reason: ReasonCouldntCancel,
82+
Message: fmt.Sprintf("PipelineRun %q was cancelled but had errors trying to cancel TaskRuns and/or Runs: %s", pr.Name, e),
83+
})
84+
return fmt.Errorf("error(s) from cancelling TaskRun(s) from PipelineRun %s: %s", pr.Name, e)
85+
}
86+
return nil
87+
}
88+
89+
// cancelPipelineTaskRuns patches `TaskRun` and `Run` with canceled status
90+
func cancelPipelineTaskRuns(ctx context.Context, logger *zap.SugaredLogger, pr *v1beta1.PipelineRun, omitTaskGraph *dag.Graph, clientSet clientset.Interface) []string {
6291
errs := []string{}
6392

93+
isTaskOmittedFromCancel := func(taskName string) bool {
94+
if omitTaskGraph == nil {
95+
return false
96+
}
97+
_, ok := omitTaskGraph.Nodes[taskName]
98+
return ok
99+
}
100+
64101
// Loop over the TaskRuns in the PipelineRun status.
65102
// If a TaskRun is not in the status yet we should not cancel it anyways.
66-
for taskRunName := range pr.Status.TaskRuns {
103+
for taskRunName, taskRun := range pr.Status.TaskRuns {
104+
if isTaskOmittedFromCancel(taskRun.PipelineTaskName) {
105+
continue
106+
}
107+
67108
logger.Infof("cancelling TaskRun %s", taskRunName)
68109

69110
if _, err := clientSet.TektonV1beta1().TaskRuns(pr.Namespace).Patch(ctx, taskRunName, types.JSONPatchType, cancelTaskRunPatchBytes, metav1.PatchOptions{}, ""); err != nil {
@@ -72,25 +113,28 @@ func cancelPipelineRun(ctx context.Context, logger *zap.SugaredLogger, pr *v1bet
72113
}
73114
}
74115
// Loop over the Runs in the PipelineRun status.
75-
for runName := range pr.Status.Runs {
116+
for runName, run := range pr.Status.Runs {
117+
if isTaskOmittedFromCancel(run.PipelineTaskName) {
118+
continue
119+
}
120+
76121
logger.Infof("cancelling Run %s", runName)
77122

78123
if _, err := clientSet.TektonV1alpha1().Runs(pr.Namespace).Patch(ctx, runName, types.JSONPatchType, cancelRunPatchBytes, metav1.PatchOptions{}, ""); err != nil {
79124
errs = append(errs, fmt.Errorf("Failed to patch Run `%s` with cancellation: %s", runName, err).Error())
80125
continue
81126
}
82127
}
83-
// If we successfully cancelled all the TaskRuns and Runs, we can consider the PipelineRun cancelled.
84-
if len(errs) == 0 {
85-
pr.Status.SetCondition(&apis.Condition{
86-
Type: apis.ConditionSucceeded,
87-
Status: corev1.ConditionFalse,
88-
Reason: ReasonCancelled,
89-
Message: fmt.Sprintf("PipelineRun %q was cancelled", pr.Name),
90-
})
91-
// update pr completed time
92-
pr.Status.CompletionTime = &metav1.Time{Time: time.Now()}
93-
} else {
128+
129+
return errs
130+
}
131+
132+
// stopPipelineRun marks the PipelineRun as stopping, any resolved TaskRun(s) as cancelled and runs finally.
133+
func stopPipelineRun(ctx context.Context, logger *zap.SugaredLogger, pr *v1beta1.PipelineRun, finalTaskGraph *dag.Graph, clientSet clientset.Interface) error {
134+
errs := cancelPipelineTaskRuns(ctx, logger, pr, finalTaskGraph, clientSet)
135+
136+
// If we successfully cancelled all the TaskRuns and Runs, we can proceed with the PipelineRun reconciliation to trigger finally.
137+
if len(errs) > 0 {
94138
e := strings.Join(errs, "\n")
95139
// Indicate that we failed to cancel the PipelineRun
96140
pr.Status.SetCondition(&apis.Condition{

pkg/reconciler/pipelinerun/cancel_test.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,14 @@ func TestCancelPipelineRun(t *testing.T) {
9595
{ObjectMeta: metav1.ObjectMeta{Name: "t1"}},
9696
{ObjectMeta: metav1.ObjectMeta{Name: "t2"}},
9797
},
98+
}, {
99+
name: "deprecated-state",
100+
pipelineRun: &v1beta1.PipelineRun{
101+
ObjectMeta: metav1.ObjectMeta{Name: "test-pipeline-run-cancelled"},
102+
Spec: v1beta1.PipelineRunSpec{
103+
Status: v1beta1.PipelineRunSpecStatusCancelledDeprecated,
104+
},
105+
},
98106
}}
99107
for _, tc := range testCases {
100108
tc := tc

pkg/reconciler/pipelinerun/pipelinerun.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -390,6 +390,15 @@ func (c *Reconciler) reconcile(ctx context.Context, pr *v1beta1.PipelineRun, get
390390
return controller.NewPermanentError(err)
391391
}
392392

393+
// once the list of final tasks is ready, check if pipeline is not stopped
394+
if pr.IsStopped() {
395+
// If the pipelinerun is stopped, cancel tasks, but run finally
396+
err := stopPipelineRun(ctx, logger, pr, dfinally, c.PipelineClientSet)
397+
if err != nil {
398+
return controller.NewPermanentError(err)
399+
}
400+
}
401+
393402
if err := pipelineSpec.Validate(ctx); err != nil {
394403
// This Run has failed, so we need to mark it as failed and stop reconciling it
395404
pr.Status.MarkFailed(ReasonFailedValidation,
@@ -489,6 +498,7 @@ func (c *Reconciler) reconcile(ctx context.Context, pr *v1beta1.PipelineRun, get
489498
// dag tasks graph and final tasks graph
490499
pipelineRunFacts := &resources.PipelineRunFacts{
491500
State: pipelineRunState,
501+
SpecStatus: pr.Spec.Status,
492502
TasksGraph: d,
493503
FinalTasksGraph: dfinally,
494504
}

0 commit comments

Comments
 (0)