Skip to content

Commit c9ff759

Browse files
QuanZhang-Williamtekton-robot
authored andcommitted
[TEP-0050] Add OnError field
In [TEP-0050][tep-0050], we proposed to add an `OnError` API field under `PipelineTask` to configure error handling strategy. This commits add the new `OnError` API field and the related validation, conversion and validation. The business logic will be added in the follow-up PRs. Note: OnError is in preview mode and not yet supported. /kind feature [tep-0050]: https://github.com/tektoncd/community/blob/main/teps/0050-ignore-task-failures.md
1 parent 4cae9cb commit c9ff759

14 files changed

+430
-2
lines changed

docs/pipeline-api.md

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2851,6 +2851,23 @@ PipelineSpec
28512851
Note: PipelineSpec is in preview mode and not yet supported</p>
28522852
</td>
28532853
</tr>
2854+
<tr>
2855+
<td>
2856+
<code>onError</code><br/>
2857+
<em>
2858+
<a href="#tekton.dev/v1.PipelineTaskOnErrorType">
2859+
PipelineTaskOnErrorType
2860+
</a>
2861+
</em>
2862+
</td>
2863+
<td>
2864+
<em>(Optional)</em>
2865+
<p>OnError defines the exiting behavior of a PipelineRun on error
2866+
can be set to [ continue | stopAndFail ]
2867+
Note: OnError is in preview mode and not yet supported
2868+
TODO(#7165)</p>
2869+
</td>
2870+
</tr>
28542871
</tbody>
28552872
</table>
28562873
<h3 id="tekton.dev/v1.PipelineTaskMetadata">PipelineTaskMetadata
@@ -2893,6 +2910,29 @@ map[string]string
28932910
</tr>
28942911
</tbody>
28952912
</table>
2913+
<h3 id="tekton.dev/v1.PipelineTaskOnErrorType">PipelineTaskOnErrorType
2914+
(<code>string</code> alias)</h3>
2915+
<p>
2916+
(<em>Appears on:</em><a href="#tekton.dev/v1.PipelineTask">PipelineTask</a>)
2917+
</p>
2918+
<div>
2919+
<p>PipelineTaskOnErrorType defines a list of supported failure handling behaviors of a PipelineTask on error</p>
2920+
</div>
2921+
<table>
2922+
<thead>
2923+
<tr>
2924+
<th>Value</th>
2925+
<th>Description</th>
2926+
</tr>
2927+
</thead>
2928+
<tbody><tr><td><p>&#34;continue&#34;</p></td>
2929+
<td><p>PipelineTaskContinue indicates to continue executing the rest of the DAG when the PipelineTask fails</p>
2930+
</td>
2931+
</tr><tr><td><p>&#34;stopAndFail&#34;</p></td>
2932+
<td><p>PipelineTaskStopAndFail indicates to stop and fail the PipelineRun if the PipelineTask fails</p>
2933+
</td>
2934+
</tr></tbody>
2935+
</table>
28962936
<h3 id="tekton.dev/v1.PipelineTaskParam">PipelineTaskParam
28972937
</h3>
28982938
<div>
@@ -10933,6 +10973,23 @@ PipelineSpec
1093310973
Note: PipelineSpec is in preview mode and not yet supported</p>
1093410974
</td>
1093510975
</tr>
10976+
<tr>
10977+
<td>
10978+
<code>onError</code><br/>
10979+
<em>
10980+
<a href="#tekton.dev/v1beta1.PipelineTaskOnErrorType">
10981+
PipelineTaskOnErrorType
10982+
</a>
10983+
</em>
10984+
</td>
10985+
<td>
10986+
<em>(Optional)</em>
10987+
<p>OnError defines the exiting behavior of a PipelineRun on error
10988+
can be set to [ continue | stopAndFail ]
10989+
Note: OnError is in preview mode and not yet supported
10990+
TODO(#7165)</p>
10991+
</td>
10992+
</tr>
1093610993
</tbody>
1093710994
</table>
1093810995
<h3 id="tekton.dev/v1beta1.PipelineTaskInputResource">PipelineTaskInputResource
@@ -11031,6 +11088,14 @@ map[string]string
1103111088
</tr>
1103211089
</tbody>
1103311090
</table>
11091+
<h3 id="tekton.dev/v1beta1.PipelineTaskOnErrorType">PipelineTaskOnErrorType
11092+
(<code>string</code> alias)</h3>
11093+
<p>
11094+
(<em>Appears on:</em><a href="#tekton.dev/v1beta1.PipelineTask">PipelineTask</a>)
11095+
</p>
11096+
<div>
11097+
<p>PipelineTaskOnErrorType defines a list of supported failure handling behaviors of a PipelineTask on error</p>
11098+
</div>
1103411099
<h3 id="tekton.dev/v1beta1.PipelineTaskOutputResource">PipelineTaskOutputResource
1103511100
</h3>
1103611101
<p>

docs/pipelines.md

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ weight: 203
2121
- [Tekton Bundles](#tekton-bundles)
2222
- [Using the `runAfter` field](#using-the-runafter-field)
2323
- [Using the `retries` field](#using-the-retries-field)
24+
- [Using the `onError` field](#using-the-onerror-field)
25+
- [Produce results with `OnError`](#produce-results-with-onerror)
2426
- [Guard `Task` execution using `when` expressions](#guard-task-execution-using-when-expressions)
2527
- [Guarding a `Task` and its dependent `Tasks`](#guarding-a-task-and-its-dependent-tasks)
2628
- [Cascade `when` expressions to the specific dependent `Tasks`](#cascade-when-expressions-to-the-specific-dependent-tasks)
@@ -606,6 +608,106 @@ tasks:
606608
name: build-push
607609
```
608610

611+
### Using the `onError` field
612+
613+
> :seedling: **Specifying `onError` in `PipelineTasks` is an [alpha](additional-configs.md#alpha-features) feature.** The `enable-api-fields` feature flag must be set to `"alpha"` to specify `onError` in a `PipelineTask`.
614+
615+
> :seedling: This feature is in **Preview Only** mode and not yet supported/implemented.
616+
617+
When a `PipelineTask` fails, the rest of the `PipelineTasks` are skipped and the `PipelineRun` is declared a failure. If you would like to
618+
ignore such `PipelineTask` failure and continue executing the rest of the `PipelineTasks`, you can specify `onError` for such a `PipelineTask`.
619+
620+
`OnError` can be set to `stopAndFail` (default) and `continue`. The failure of a `PipelineTask` with `stopAndFail` would stop and fail the whole `PipelineRun`. A `PipelineTask` fails with `continue` does not fail the whole `PipelineRun`, and the rest of the `PipelineTask` will continue to execute.
621+
622+
To ignore a `PipelineTask` failure, set `onError` to `continue`:
623+
624+
``` yaml
625+
apiVersion: tekton.dev/v1
626+
kind: Pipeline
627+
metadata:
628+
name: demo
629+
spec:
630+
tasks:
631+
- name: task1
632+
onError: continue
633+
taskSpec:
634+
steps:
635+
- name: step1
636+
image: alpine
637+
script: |
638+
exit 1
639+
```
640+
641+
At runtime, the failure is ignored to determine the `PipelineRun` status. The `PipelineRun` `message` contains the ignored failure info:
642+
643+
``` yaml
644+
status:
645+
conditions:
646+
- lastTransitionTime: "2023-09-28T19:08:30Z"
647+
message: 'Tasks Completed: 1 (Failed: 1 (Ignored: 1), Cancelled 0), Skipped: 0'
648+
reason: Succeeded
649+
status: "True"
650+
type: Succeeded
651+
...
652+
```
653+
654+
Note that the `TaskRun` status remains as it is irrelevant to `OnError`. Failed but ignored `TaskRuns` result in a `failed` status with reason
655+
`FailureIgnored`.
656+
657+
For example, the `TaskRun` created by the above `PipelineRun` has the following status:
658+
659+
``` bash
660+
$ kubectl get tr demo-run-task1
661+
NAME SUCCEEDED REASON STARTTIME COMPLETIONTIME
662+
demo-run-task1 False FailureIgnored 12m 12m
663+
```
664+
665+
To specify `onError` for a `step`, please see [specifying onError for a step](./tasks.md#specifying-onerror-for-a-step).
666+
667+
**Note:** Setting [`Retry`](#specifying-retries) and `OnError:continue` at the same time is **NOT** allowed.
668+
669+
### Produce results with `OnError`
670+
671+
When a `PipelineTask` is set to ignore error and the `PipelineTask` is able to initialize a result before failing, the result is made available to the consumer `PipelineTasks`.
672+
673+
``` yaml
674+
tasks:
675+
- name: task1
676+
onError: continue
677+
taskSpec:
678+
results:
679+
- name: result1
680+
steps:
681+
- name: step1
682+
image: alpine
683+
script: |
684+
echo -n 123 | tee $(results.result1.path)
685+
exit 1
686+
```
687+
688+
The consumer `PipelineTasks` can access the result by referencing `$(tasks.task1.results.result1)`.
689+
690+
If the result is **NOT** initialized before failing, and there is a `PipelineTask` consuming it:
691+
692+
``` yaml
693+
tasks:
694+
- name: task1
695+
onError: continue
696+
taskSpec:
697+
results:
698+
- name: result1
699+
steps:
700+
- name: step1
701+
image: alpine
702+
script: |
703+
exit 1
704+
echo -n 123 | tee $(results.result1.path)
705+
```
706+
707+
- If the consuming `PipelineTask` has `OnError:stopAndFail`, the `PipelineRun` will fail with `InvalidTaskResultReference`.
708+
- If the consuming `PipelineTask` has `OnError:continue`, the consuming `PipelineTask` will be skipped with reason `Results were missing`,
709+
and the `PipelineRun` will continue to execute.
710+
609711
### Guard `Task` execution using `when` expressions
610712

611713
To run a `Task` only when certain conditions are met, it is possible to _guard_ task execution using the `when` field. The `when` field allows you to list a series of references to `when` expressions.

pkg/apis/pipeline/v1/openapi_generated.go

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

pkg/apis/pipeline/v1/pipeline_types.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,13 +27,20 @@ import (
2727
"knative.dev/pkg/kmeta"
2828
)
2929

30+
// PipelineTaskOnErrorType defines a list of supported failure handling behaviors of a PipelineTask on error
31+
type PipelineTaskOnErrorType string
32+
3033
const (
3134
// PipelineTasksAggregateStatus is a param representing aggregate status of all dag pipelineTasks
3235
PipelineTasksAggregateStatus = "tasks.status"
3336
// PipelineTasks is a value representing a task is a member of "tasks" section of the pipeline
3437
PipelineTasks = "tasks"
3538
// PipelineFinallyTasks is a value representing a task is a member of "finally" section of the pipeline
3639
PipelineFinallyTasks = "finally"
40+
// PipelineTaskStopAndFail indicates to stop and fail the PipelineRun if the PipelineTask fails
41+
PipelineTaskStopAndFail PipelineTaskOnErrorType = "stopAndFail"
42+
// PipelineTaskContinue indicates to continue executing the rest of the DAG when the PipelineTask fails
43+
PipelineTaskContinue PipelineTaskOnErrorType = "continue"
3744
)
3845

3946
// +genclient
@@ -238,6 +245,13 @@ type PipelineTask struct {
238245
// Note: PipelineSpec is in preview mode and not yet supported
239246
// +optional
240247
PipelineSpec *PipelineSpec `json:"pipelineSpec,omitempty"`
248+
249+
// OnError defines the exiting behavior of a PipelineRun on error
250+
// can be set to [ continue | stopAndFail ]
251+
// Note: OnError is in preview mode and not yet supported
252+
// TODO(#7165)
253+
// +optional
254+
OnError PipelineTaskOnErrorType `json:"onError,omitempty"`
241255
}
242256

243257
// IsCustomTask checks whether an embedded TaskSpec is a Custom Task

pkg/apis/pipeline/v1/pipeline_types_test.go

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,98 @@ func TestPipelineTask_ValidateName(t *testing.T) {
7373
}
7474
}
7575

76+
func TestPipelineTask_OnError(t *testing.T) {
77+
tests := []struct {
78+
name string
79+
p PipelineTask
80+
expectedError *apis.FieldError
81+
wc func(context.Context) context.Context
82+
}{{
83+
name: "valid PipelineTask with onError:continue",
84+
p: PipelineTask{
85+
Name: "foo",
86+
OnError: PipelineTaskContinue,
87+
TaskRef: &TaskRef{Name: "foo"},
88+
},
89+
wc: cfgtesting.EnableAlphaAPIFields,
90+
}, {
91+
name: "valid PipelineTask with onError:stopAndFail",
92+
p: PipelineTask{
93+
Name: "foo",
94+
OnError: PipelineTaskStopAndFail,
95+
TaskRef: &TaskRef{Name: "foo"},
96+
},
97+
wc: cfgtesting.EnableAlphaAPIFields,
98+
}, {
99+
name: "invalid OnError value",
100+
p: PipelineTask{
101+
Name: "foo",
102+
OnError: "invalid-val",
103+
TaskRef: &TaskRef{Name: "foo"},
104+
},
105+
expectedError: apis.ErrInvalidValue("invalid-val", "OnError", "PipelineTask OnError must be either \"continue\" or \"stopAndFail\""),
106+
wc: cfgtesting.EnableAlphaAPIFields,
107+
}, {
108+
name: "OnError:stopAndFail and retries coexist - success",
109+
p: PipelineTask{
110+
Name: "foo",
111+
OnError: PipelineTaskStopAndFail,
112+
Retries: 1,
113+
TaskRef: &TaskRef{Name: "foo"},
114+
},
115+
wc: cfgtesting.EnableAlphaAPIFields,
116+
}, {
117+
name: "OnError:continue and retries coexists - failure",
118+
p: PipelineTask{
119+
Name: "foo",
120+
OnError: PipelineTaskContinue,
121+
Retries: 1,
122+
TaskRef: &TaskRef{Name: "foo"},
123+
},
124+
expectedError: apis.ErrGeneric("PipelineTask OnError cannot be set to \"continue\" when Retries is greater than 0"),
125+
wc: cfgtesting.EnableAlphaAPIFields,
126+
}, {
127+
name: "setting OnError in beta API version - failure",
128+
p: PipelineTask{
129+
Name: "foo",
130+
OnError: PipelineTaskContinue,
131+
TaskRef: &TaskRef{Name: "foo"},
132+
},
133+
expectedError: apis.ErrGeneric("OnError requires \"enable-api-fields\" feature gate to be \"alpha\" but it is \"beta\""),
134+
wc: cfgtesting.EnableBetaAPIFields,
135+
}, {
136+
name: "setting OnError in stable API version - failure",
137+
p: PipelineTask{
138+
Name: "foo",
139+
OnError: PipelineTaskContinue,
140+
TaskRef: &TaskRef{Name: "foo"},
141+
},
142+
expectedError: apis.ErrGeneric("OnError requires \"enable-api-fields\" feature gate to be \"alpha\" but it is \"stable\""),
143+
wc: cfgtesting.EnableStableAPIFields,
144+
}}
145+
for _, tt := range tests {
146+
t.Run(tt.name, func(t *testing.T) {
147+
ctx := context.Background()
148+
if tt.wc != nil {
149+
ctx = tt.wc(ctx)
150+
}
151+
err := tt.p.Validate(ctx)
152+
if tt.expectedError == nil {
153+
if err != nil {
154+
t.Error("PipelineTask.Validate() returned error for valid pipeline task")
155+
}
156+
} else {
157+
if err == nil {
158+
t.Error("PipelineTask.Validate() did not return error for invalid pipeline task with OnError")
159+
}
160+
if d := cmp.Diff(tt.expectedError.Error(), err.Error(), cmpopts.IgnoreUnexported(apis.FieldError{})); d != "" {
161+
t.Errorf("PipelineTask.Validate() errors diff %s", diff.PrintWantGot(d))
162+
}
163+
}
164+
})
165+
}
166+
}
167+
76168
func TestPipelineTask_ValidateRefOrSpec(t *testing.T) {
77169
tests := []struct {
78170
name string

pkg/apis/pipeline/v1/pipeline_validation.go

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,17 @@ func (pt PipelineTask) Validate(ctx context.Context) (errs *apis.FieldError) {
211211
NamespacedTaskKind: true,
212212
ClusterTaskRefKind: true,
213213
}
214+
215+
if pt.OnError != "" {
216+
errs = errs.Also(config.ValidateEnabledAPIFields(ctx, "OnError", config.AlphaAPIFields))
217+
if pt.OnError != PipelineTaskContinue && pt.OnError != PipelineTaskStopAndFail {
218+
errs = errs.Also(apis.ErrInvalidValue(pt.OnError, "OnError", "PipelineTask OnError must be either \"continue\" or \"stopAndFail\""))
219+
}
220+
if pt.OnError == PipelineTaskContinue && pt.Retries > 0 {
221+
errs = errs.Also(apis.ErrGeneric("PipelineTask OnError cannot be set to \"continue\" when Retries is greater than 0"))
222+
}
223+
}
224+
214225
// Pipeline task having taskRef/taskSpec with APIVersion is classified as custom task
215226
switch {
216227
case pt.TaskRef != nil && !taskKinds[pt.TaskRef.Kind]:
@@ -224,7 +235,7 @@ func (pt PipelineTask) Validate(ctx context.Context) (errs *apis.FieldError) {
224235
default:
225236
errs = errs.Also(pt.validateTask(ctx))
226237
}
227-
return
238+
return errs
228239
}
229240

230241
func (pt *PipelineTask) validateMatrix(ctx context.Context) (errs *apis.FieldError) {

pkg/apis/pipeline/v1/swagger.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -885,6 +885,10 @@
885885
"description": "Name is the name of this task within the context of a Pipeline. Name is used as a coordinate with the `from` and `runAfter` fields to establish the execution order of tasks relative to one another.",
886886
"type": "string"
887887
},
888+
"onError": {
889+
"description": "OnError defines the exiting behavior of a PipelineRun on error can be set to [ continue | stopAndFail ] Note: OnError is in preview mode and not yet supported",
890+
"type": "string"
891+
},
888892
"params": {
889893
"description": "Parameters declares parameters passed to this task.",
890894
"type": "array",

0 commit comments

Comments
 (0)