From b6d27a81d10c4ca52dc63daff29dc7d766450009 Mon Sep 17 00:00:00 2001
From: Quan Zhang
Date: Sat, 18 Nov 2023 13:19:04 -0500
Subject: [PATCH] [TEP-0050] Implement PipelineTask OnError
Part of [#7165][#7165]. In [TEP-0050][tep-0050], we proposed to add an `OnError` API field under `PipelineTask` to configure error handling strategy.
This commits leverages `PipelineTask.OnError` API field introduced in the previous PR, implement the error handling strategy, update related docs and tests.
/kind feature
[tep-0050]: https://github.com/tektoncd/community/blob/main/teps/0050-ignore-task-failures.md
[#7165]: https://github.com/tektoncd/pipeline/issues/7165
---
docs/pipeline-api.md | 12 +-
docs/pipelines.md | 2 -
.../pipelineruns/alpha/ignore-task-error.yaml | 25 ++++
pkg/apis/pipeline/v1/openapi_generated.go | 2 +-
pkg/apis/pipeline/v1/pipeline_types.go | 2 -
pkg/apis/pipeline/v1/pipelinerun_types.go | 3 +
pkg/apis/pipeline/v1/swagger.json | 2 +-
pkg/apis/pipeline/v1/taskrun_types.go | 3 +
.../pipeline/v1beta1/openapi_generated.go | 2 +-
pkg/apis/pipeline/v1beta1/pipeline_types.go | 2 -
pkg/apis/pipeline/v1beta1/swagger.json | 2 +-
pkg/pod/status.go | 15 ++-
pkg/pod/status_test.go | 69 ++++++++++
pkg/reconciler/pipelinerun/pipelinerun.go | 3 +
.../pipelinerun/pipelinerun_test.go | 70 ++++++++++
.../resources/pipelinerunresolution.go | 4 +-
.../pipelinerun/resources/pipelinerunstate.go | 28 +++-
.../resources/pipelinerunstate_test.go | 45 +++++++
test/ignore_task_error_test.go | 127 ++++++++++++++++++
19 files changed, 391 insertions(+), 27 deletions(-)
create mode 100644 examples/v1/pipelineruns/alpha/ignore-task-error.yaml
create mode 100644 test/ignore_task_error_test.go
diff --git a/docs/pipeline-api.md b/docs/pipeline-api.md
index 884b5a1181d..ce318432829 100644
--- a/docs/pipeline-api.md
+++ b/docs/pipeline-api.md
@@ -2887,9 +2887,7 @@ PipelineTaskOnErrorType
(Optional)
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
-TODO(#7165)
+can be set to [ continue | stopAndFail ]
|
@@ -5138,6 +5136,10 @@ that references within the TaskRun could not be resolved
TaskRunReasonFailedValidation indicated that the reason for failure status is
that taskrun failed runtime validation
|
+"FailureIgnored" |
+TaskRunReasonFailureIgnored is the reason set when the Taskrun has failed due to pod execution error and the failure is ignored for the owning PipelineRun.
+TaskRuns failed due to reconciler/validation error should not use this reason.
+ |
"TaskRunImagePullFailed" |
TaskRunReasonImagePullFailed is the reason set when the step of a task fails due to image not being pulled
|
@@ -11611,9 +11613,7 @@ PipelineTaskOnErrorType
(Optional)
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
-TODO(#7165)
+can be set to [ continue | stopAndFail ]
|
diff --git a/docs/pipelines.md b/docs/pipelines.md
index 3bccd4e4f94..db366edec02 100644
--- a/docs/pipelines.md
+++ b/docs/pipelines.md
@@ -724,8 +724,6 @@ tasks:
> :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`.
-> :seedling: This feature is in **Preview Only** mode and not yet supported/implemented.
-
When a `PipelineTask` fails, the rest of the `PipelineTasks` are skipped and the `PipelineRun` is declared a failure. If you would like to
ignore such `PipelineTask` failure and continue executing the rest of the `PipelineTasks`, you can specify `onError` for such a `PipelineTask`.
diff --git a/examples/v1/pipelineruns/alpha/ignore-task-error.yaml b/examples/v1/pipelineruns/alpha/ignore-task-error.yaml
new file mode 100644
index 00000000000..a7ee3656c3e
--- /dev/null
+++ b/examples/v1/pipelineruns/alpha/ignore-task-error.yaml
@@ -0,0 +1,25 @@
+apiVersion: tekton.dev/v1
+kind: PipelineRun
+metadata:
+ generateName: pipelinerun-with-failing-task-
+spec:
+ pipelineSpec:
+ tasks:
+ - name: echo-continue
+ onError: continue
+ taskSpec:
+ steps:
+ - name: write
+ image: alpine
+ script: |
+ echo "this is a failing task"
+ exit 1
+ - name: echo
+ runAfter:
+ - echo-continue
+ taskSpec:
+ steps:
+ - name: write
+ image: alpine
+ script: |
+ echo "this is a success task"
diff --git a/pkg/apis/pipeline/v1/openapi_generated.go b/pkg/apis/pipeline/v1/openapi_generated.go
index fe5d925166f..06f71333bd5 100644
--- a/pkg/apis/pipeline/v1/openapi_generated.go
+++ b/pkg/apis/pipeline/v1/openapi_generated.go
@@ -1911,7 +1911,7 @@ func schema_pkg_apis_pipeline_v1_PipelineTask(ref common.ReferenceCallback) comm
},
"onError": {
SchemaProps: spec.SchemaProps{
- 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",
+ Description: "OnError defines the exiting behavior of a PipelineRun on error can be set to [ continue | stopAndFail ]",
Type: []string{"string"},
Format: "",
},
diff --git a/pkg/apis/pipeline/v1/pipeline_types.go b/pkg/apis/pipeline/v1/pipeline_types.go
index 00482590884..3cbc5295149 100644
--- a/pkg/apis/pipeline/v1/pipeline_types.go
+++ b/pkg/apis/pipeline/v1/pipeline_types.go
@@ -248,8 +248,6 @@ type PipelineTask struct {
// 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
- // TODO(#7165)
// +optional
OnError PipelineTaskOnErrorType `json:"onError,omitempty"`
}
diff --git a/pkg/apis/pipeline/v1/pipelinerun_types.go b/pkg/apis/pipeline/v1/pipelinerun_types.go
index cb70ac66fdc..34be57243ae 100644
--- a/pkg/apis/pipeline/v1/pipelinerun_types.go
+++ b/pkg/apis/pipeline/v1/pipelinerun_types.go
@@ -416,6 +416,9 @@ const (
PipelineRunReasonInvalidParamValue PipelineRunReason = "InvalidParamValue"
)
+// PipelineTaskOnErrorAnnotation is used to pass the failure strategy to TaskRun pods from PipelineTask OnError field
+const PipelineTaskOnErrorAnnotation = "pipeline.tekton.dev/pipeline-task-on-error"
+
func (t PipelineRunReason) String() string {
return string(t)
}
diff --git a/pkg/apis/pipeline/v1/swagger.json b/pkg/apis/pipeline/v1/swagger.json
index 8093c7e8389..cfae4b0e679 100644
--- a/pkg/apis/pipeline/v1/swagger.json
+++ b/pkg/apis/pipeline/v1/swagger.json
@@ -894,7 +894,7 @@
"type": "string"
},
"onError": {
- "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",
+ "description": "OnError defines the exiting behavior of a PipelineRun on error can be set to [ continue | stopAndFail ]",
"type": "string"
},
"params": {
diff --git a/pkg/apis/pipeline/v1/taskrun_types.go b/pkg/apis/pipeline/v1/taskrun_types.go
index f3c44a6bf8a..7c3cf232ee5 100644
--- a/pkg/apis/pipeline/v1/taskrun_types.go
+++ b/pkg/apis/pipeline/v1/taskrun_types.go
@@ -202,6 +202,9 @@ const (
// TaskRunReasonResourceVerificationFailed indicates that the task fails the trusted resource verification,
// it could be the content has changed, signature is invalid or public key is invalid
TaskRunReasonResourceVerificationFailed TaskRunReason = "ResourceVerificationFailed"
+ // TaskRunReasonFailureIgnored is the reason set when the Taskrun has failed due to pod execution error and the failure is ignored for the owning PipelineRun.
+ // TaskRuns failed due to reconciler/validation error should not use this reason.
+ TaskRunReasonFailureIgnored TaskRunReason = "FailureIgnored"
)
func (t TaskRunReason) String() string {
diff --git a/pkg/apis/pipeline/v1beta1/openapi_generated.go b/pkg/apis/pipeline/v1beta1/openapi_generated.go
index d9eaff0ecc4..3bd5ad6f1bf 100644
--- a/pkg/apis/pipeline/v1beta1/openapi_generated.go
+++ b/pkg/apis/pipeline/v1beta1/openapi_generated.go
@@ -2667,7 +2667,7 @@ func schema_pkg_apis_pipeline_v1beta1_PipelineTask(ref common.ReferenceCallback)
},
"onError": {
SchemaProps: spec.SchemaProps{
- 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",
+ Description: "OnError defines the exiting behavior of a PipelineRun on error can be set to [ continue | stopAndFail ]",
Type: []string{"string"},
Format: "",
},
diff --git a/pkg/apis/pipeline/v1beta1/pipeline_types.go b/pkg/apis/pipeline/v1beta1/pipeline_types.go
index a5c016b5905..256f24f1dcf 100644
--- a/pkg/apis/pipeline/v1beta1/pipeline_types.go
+++ b/pkg/apis/pipeline/v1beta1/pipeline_types.go
@@ -262,8 +262,6 @@ type PipelineTask struct {
// 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
- // TODO(#7165)
// +optional
OnError PipelineTaskOnErrorType `json:"onError,omitempty"`
}
diff --git a/pkg/apis/pipeline/v1beta1/swagger.json b/pkg/apis/pipeline/v1beta1/swagger.json
index 63c4af3d15b..671e6eba73d 100644
--- a/pkg/apis/pipeline/v1beta1/swagger.json
+++ b/pkg/apis/pipeline/v1beta1/swagger.json
@@ -1287,7 +1287,7 @@
"type": "string"
},
"onError": {
- "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",
+ "description": "OnError defines the exiting behavior of a PipelineRun on error can be set to [ continue | stopAndFail ]",
"type": "string"
},
"params": {
diff --git a/pkg/pod/status.go b/pkg/pod/status.go
index f68ef71518a..a6a842395ab 100644
--- a/pkg/pod/status.go
+++ b/pkg/pod/status.go
@@ -128,7 +128,12 @@ func MakeTaskRunStatus(ctx context.Context, logger *zap.SugaredLogger, tr v1.Tas
complete := areStepsComplete(pod) || pod.Status.Phase == corev1.PodSucceeded || pod.Status.Phase == corev1.PodFailed
if complete {
- updateCompletedTaskRunStatus(logger, trs, pod)
+ onError, ok := tr.Annotations[v1.PipelineTaskOnErrorAnnotation]
+ if ok {
+ updateCompletedTaskRunStatus(logger, trs, pod, v1.PipelineTaskOnErrorType(onError))
+ } else {
+ updateCompletedTaskRunStatus(logger, trs, pod, "")
+ }
} else {
updateIncompleteTaskRunStatus(trs, pod)
}
@@ -483,10 +488,14 @@ func extractExitCodeFromResults(results []result.RunResult) (*int32, error) {
return nil, nil //nolint:nilnil // would be more ergonomic to return a sentinel error
}
-func updateCompletedTaskRunStatus(logger *zap.SugaredLogger, trs *v1.TaskRunStatus, pod *corev1.Pod) {
+func updateCompletedTaskRunStatus(logger *zap.SugaredLogger, trs *v1.TaskRunStatus, pod *corev1.Pod, onError v1.PipelineTaskOnErrorType) {
if DidTaskRunFail(pod) {
msg := getFailureMessage(logger, pod)
- markStatusFailure(trs, v1.TaskRunReasonFailed.String(), msg)
+ if onError == v1.PipelineTaskContinue {
+ markStatusFailure(trs, v1.TaskRunReasonFailureIgnored.String(), msg)
+ } else {
+ markStatusFailure(trs, v1.TaskRunReasonFailed.String(), msg)
+ }
} else {
markStatusSuccess(trs)
}
diff --git a/pkg/pod/status_test.go b/pkg/pod/status_test.go
index 552502919e0..aa35cd05042 100644
--- a/pkg/pod/status_test.go
+++ b/pkg/pod/status_test.go
@@ -1542,6 +1542,75 @@ func TestMakeTaskRunStatus(t *testing.T) {
}
}
+func TestMakeRunStatus_OnError(t *testing.T) {
+ for _, c := range []struct {
+ name string
+ podStatus corev1.PodStatus
+ onError v1.PipelineTaskOnErrorType
+ want v1.TaskRunStatus
+ }{{
+ name: "onError: continue",
+ podStatus: corev1.PodStatus{
+ Phase: corev1.PodFailed,
+ Message: "boom",
+ },
+ onError: v1.PipelineTaskContinue,
+ want: v1.TaskRunStatus{
+ Status: statusFailure(string(v1.TaskRunReasonFailureIgnored), "boom"),
+ },
+ }, {
+ name: "onError: stopAndFail",
+ podStatus: corev1.PodStatus{
+ Phase: corev1.PodFailed,
+ Message: "boom",
+ },
+ onError: v1.PipelineTaskStopAndFail,
+ want: v1.TaskRunStatus{
+ Status: statusFailure(string(v1.TaskRunReasonFailed), "boom"),
+ },
+ }, {
+ name: "stand alone TaskRun",
+ podStatus: corev1.PodStatus{
+ Phase: corev1.PodFailed,
+ Message: "boom",
+ },
+ want: v1.TaskRunStatus{
+ Status: statusFailure(string(v1.TaskRunReasonFailed), "boom"),
+ },
+ }} {
+ t.Run(c.name, func(t *testing.T) {
+ pod := corev1.Pod{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "pod",
+ Namespace: "foo",
+ },
+ Status: c.podStatus,
+ }
+ tr := v1.TaskRun{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "task-run",
+ Namespace: "foo",
+ },
+ }
+ if c.onError != "" {
+ tr.Annotations = map[string]string{}
+ tr.Annotations[v1.PipelineTaskOnErrorAnnotation] = string(c.onError)
+ }
+
+ logger, _ := logging.NewLogger("", "status")
+ kubeclient := fakek8s.NewSimpleClientset()
+ got, err := MakeTaskRunStatus(context.Background(), logger, tr, &pod, kubeclient, &v1.TaskSpec{})
+ if err != nil {
+ t.Errorf("Unexpected err in MakeTaskRunResult: %s", err)
+ }
+
+ if d := cmp.Diff(c.want.Status, got.Status, ignoreVolatileTime); d != "" {
+ t.Errorf("Diff %s", diff.PrintWantGot(d))
+ }
+ })
+ }
+}
+
func TestMakeTaskRunStatusAlpha(t *testing.T) {
for _, c := range []struct {
desc string
diff --git a/pkg/reconciler/pipelinerun/pipelinerun.go b/pkg/reconciler/pipelinerun/pipelinerun.go
index d720f39fcc7..2b2b85d174d 100644
--- a/pkg/reconciler/pipelinerun/pipelinerun.go
+++ b/pkg/reconciler/pipelinerun/pipelinerun.go
@@ -958,6 +958,9 @@ func (c *Reconciler) createTaskRun(ctx context.Context, taskRunName string, para
if spanContext, err := getMarshalledSpanFromContext(ctx); err == nil {
tr.Annotations[TaskRunSpanContextAnnotation] = spanContext
}
+ if rpt.PipelineTask.OnError == v1.PipelineTaskContinue {
+ tr.Annotations[v1.PipelineTaskOnErrorAnnotation] = string(v1.PipelineTaskContinue)
+ }
if rpt.PipelineTask.Timeout != nil {
tr.Spec.Timeout = rpt.PipelineTask.Timeout
diff --git a/pkg/reconciler/pipelinerun/pipelinerun_test.go b/pkg/reconciler/pipelinerun/pipelinerun_test.go
index 50de7d2b5a2..85c78191d30 100644
--- a/pkg/reconciler/pipelinerun/pipelinerun_test.go
+++ b/pkg/reconciler/pipelinerun/pipelinerun_test.go
@@ -1060,6 +1060,76 @@ spec:
}
}
+// TestPipelineTaskErrorIsIgnored tests that a resource dependent PipelineTask with onError:continue is skipped
+// if the parent PipelineTask fails to produce the result
+func TestPipelineTaskErrorIsIgnored(t *testing.T) {
+ prs := []*v1.PipelineRun{parse.MustParseV1PipelineRun(t, `
+metadata:
+ name: test-pipeline-missing-results
+ namespace: foo
+spec:
+ serviceAccountName: test-sa-0
+ pipelineSpec:
+ tasks:
+ - name: task1
+ taskSpec:
+ results:
+ - name: result1
+ type: string
+ steps:
+ - name: failing-step
+ onError: continue
+ image: busybox
+ script: exit 1; echo -n 123 | tee $(results.result1.path)'
+ - name: task2
+ onError: continue
+ params:
+ - name: param1
+ value: $(tasks.task1.results.result1)
+ taskSpec:
+ params:
+ - name: param1
+ type: string
+ steps:
+ - name: foo
+ image: busybox
+ script: 'echo $(params.param1)'
+`)}
+ trs := []*v1.TaskRun{mustParseTaskRunWithObjectMeta(t,
+ taskRunObjectMeta("test-pipeline-missing-results-task1", "foo",
+ "test-pipeline-missing-results", "test-pipeline", "task1", true),
+ `
+spec:
+ serviceAccountName: test-sa
+ timeout: 1h0m0s
+status:
+ conditions:
+ - status: "True"
+ type: Succeeded
+`)}
+ cms := []*corev1.ConfigMap{withEnabledAlphaAPIFields(newFeatureFlagsConfigMap())}
+
+ d := test.Data{
+ PipelineRuns: prs,
+ TaskRuns: trs,
+ ConfigMaps: cms,
+ }
+ prt := newPipelineRunTest(t, d)
+ defer prt.Cancel()
+
+ reconciledRun, _ := prt.reconcileRun("foo", "test-pipeline-missing-results", []string{}, false)
+ cond := reconciledRun.Status.Conditions[0]
+ if cond.Status != corev1.ConditionTrue {
+ t.Fatalf("expected PipelineRun status to be True but got: %s", cond.Status)
+ }
+ if len(reconciledRun.Status.SkippedTasks) != 1 {
+ t.Fatalf("expected 1 skipped Task but got %v", len(reconciledRun.Status.SkippedTasks))
+ }
+ if reconciledRun.Status.SkippedTasks[0].Reason != v1.MissingResultsSkip {
+ t.Fatalf("expected 1 skipped Task with reason %s, but got %v", v1.MissingResultsSkip, reconciledRun.Status.SkippedTasks[0].Reason)
+ }
+}
+
func TestMissingResultWhenStepErrorIsIgnored(t *testing.T) {
prs := []*v1.PipelineRun{parse.MustParseV1PipelineRun(t, `
metadata:
diff --git a/pkg/reconciler/pipelinerun/resources/pipelinerunresolution.go b/pkg/reconciler/pipelinerun/resources/pipelinerunresolution.go
index e3c7652252f..93a9899dbdd 100644
--- a/pkg/reconciler/pipelinerun/resources/pipelinerunresolution.go
+++ b/pkg/reconciler/pipelinerun/resources/pipelinerunresolution.go
@@ -393,7 +393,9 @@ func (t *ResolvedPipelineTask) skipBecauseResultReferencesAreMissing(facts *Pipe
resolvedResultRefs, pt, err := ResolveResultRefs(facts.State, PipelineRunState{t})
rpt := facts.State.ToMap()[pt]
if rpt != nil {
- if err != nil && (t.IsFinalTask(facts) || rpt.Skip(facts).SkippingReason == v1.WhenExpressionsSkip) {
+ if err != nil &&
+ (t.PipelineTask.OnError == v1.PipelineTaskContinue ||
+ (t.IsFinalTask(facts) || rpt.Skip(facts).SkippingReason == v1.WhenExpressionsSkip)) {
return true
}
}
diff --git a/pkg/reconciler/pipelinerun/resources/pipelinerunstate.go b/pkg/reconciler/pipelinerun/resources/pipelinerunstate.go
index 331fc720d99..df5df9174c5 100644
--- a/pkg/reconciler/pipelinerun/resources/pipelinerunstate.go
+++ b/pkg/reconciler/pipelinerun/resources/pipelinerunstate.go
@@ -88,6 +88,8 @@ type pipelineRunStatusCount struct {
Succeeded int
// failed tasks count
Failed int
+ // failed but ignored tasks count
+ IgnoredFailed int
// cancelled tasks count
Cancelled int
// number of tasks which are still pending, have not executed
@@ -278,11 +280,11 @@ func (state PipelineRunState) getNextTasks(candidateTasks sets.String) []*Resolv
}
// IsStopping returns true if the PipelineRun won't be scheduling any new Task because
-// at least one task already failed or was cancelled in the specified dag
+// at least one task already failed (with onError: stopAndFail) or was cancelled in the specified dag
func (facts *PipelineRunFacts) IsStopping() bool {
for _, t := range facts.State {
if facts.isDAGTask(t.PipelineTask.Name) {
- if t.isFailure() {
+ if t.isFailure() && t.PipelineTask.OnError != v1.PipelineTaskContinue {
return true
}
}
@@ -417,7 +419,8 @@ func (facts *PipelineRunFacts) GetPipelineConditionStatus(ctx context.Context, p
// get the count of successful tasks, failed tasks, cancelled tasks, skipped task, and incomplete tasks
s := facts.getPipelineTasksCount()
// completed task is a collection of successful, failed, cancelled tasks (skipped tasks are reported separately)
- cmTasks := s.Succeeded + s.Failed + s.Cancelled
+ cmTasks := s.Succeeded + s.Failed + s.Cancelled + s.IgnoredFailed
+ totalFailedTasks := s.Failed + s.IgnoredFailed
// The completion reason is set from the TaskRun completion reason
// by default, set it to ReasonRunning
@@ -427,8 +430,14 @@ func (facts *PipelineRunFacts) GetPipelineConditionStatus(ctx context.Context, p
if s.Incomplete == 0 {
status := corev1.ConditionTrue
reason := v1.PipelineRunReasonSuccessful.String()
- message := fmt.Sprintf("Tasks Completed: %d (Failed: %d, Cancelled %d), Skipped: %d",
- cmTasks, s.Failed, s.Cancelled, s.Skipped)
+ var message string
+ if s.IgnoredFailed > 0 {
+ message = fmt.Sprintf("Tasks Completed: %d (Failed: %d (Ignored: %d), Cancelled %d), Skipped: %d",
+ cmTasks, totalFailedTasks, s.IgnoredFailed, s.Cancelled, s.Skipped)
+ } else {
+ message = fmt.Sprintf("Tasks Completed: %d (Failed: %d, Cancelled %d), Skipped: %d",
+ cmTasks, totalFailedTasks, s.Cancelled, s.Skipped)
+ }
// Set reason to ReasonCompleted - At least one is skipped
if s.Skipped > 0 {
reason = v1.PipelineRunReasonCompleted.String()
@@ -604,6 +613,7 @@ func (facts *PipelineRunFacts) getPipelineTasksCount() pipelineRunStatusCount {
Cancelled: 0,
Incomplete: 0,
SkippedDueToTimeout: 0,
+ IgnoredFailed: 0,
}
for _, t := range facts.State {
switch {
@@ -616,9 +626,13 @@ func (facts *PipelineRunFacts) getPipelineTasksCount() pipelineRunStatusCount {
// increment cancelled counter since the task is cancelled
case t.isCancelled():
s.Cancelled++
- // increment failure counter since the task has failed
+ // increment failure counter based on Task OnError type since the task has failed
case t.isFailure():
- s.Failed++
+ if t.PipelineTask.OnError == v1.PipelineTaskContinue {
+ s.IgnoredFailed++
+ } else {
+ s.Failed++
+ }
// increment skipped and skipped due to timeout counters since the task was skipped due to the pipeline, tasks, or finally timeout being reached before the task was launched
case t.Skip(facts).SkippingReason == v1.PipelineTimedOutSkip ||
t.Skip(facts).SkippingReason == v1.TasksTimedOutSkip ||
diff --git a/pkg/reconciler/pipelinerun/resources/pipelinerunstate_test.go b/pkg/reconciler/pipelinerun/resources/pipelinerunstate_test.go
index c08dd102d6f..a54b503aa1f 100644
--- a/pkg/reconciler/pipelinerun/resources/pipelinerunstate_test.go
+++ b/pkg/reconciler/pipelinerun/resources/pipelinerunstate_test.go
@@ -2141,6 +2141,51 @@ func TestGetPipelineConditionStatus_PipelineTimeouts(t *testing.T) {
}
}
+func TestGetPipelineConditionStatus_OnError(t *testing.T) {
+ var oneFailedStateOnError = PipelineRunState{{
+ PipelineTask: &v1.PipelineTask{
+ Name: "failed task ignored",
+ TaskRef: &v1.TaskRef{Name: "task"},
+ OnError: v1.PipelineTaskContinue,
+ },
+ TaskRunNames: []string{"pipelinerun-mytask1"},
+ TaskRuns: []*v1.TaskRun{makeFailed(trs[0])},
+ ResolvedTask: &resources.ResolvedTask{
+ TaskSpec: &task.Spec,
+ },
+ }, {
+ PipelineTask: &pts[0],
+ TaskRunNames: []string{"pipelinerun-mytask2"},
+ TaskRuns: []*v1.TaskRun{makeSucceeded(trs[0])},
+ ResolvedTask: &resources.ResolvedTask{
+ TaskSpec: &task.Spec,
+ },
+ }}
+ d, err := dagFromState(oneFailedStateOnError)
+ if err != nil {
+ t.Fatalf("Unexpected error while building DAG for state %v: %v", oneFinishedState, err)
+ }
+ pr := &v1.PipelineRun{
+ ObjectMeta: metav1.ObjectMeta{Name: "pipelinerun-onError-continue"},
+ Spec: v1.PipelineRunSpec{},
+ }
+ facts := PipelineRunFacts{
+ State: oneFailedStateOnError,
+ TasksGraph: d,
+ FinalTasksGraph: &dag.Graph{},
+ TimeoutsState: PipelineRunTimeoutsState{
+ Clock: testClock,
+ },
+ }
+ c := facts.GetPipelineConditionStatus(context.Background(), pr, zap.NewNop().Sugar(), testClock)
+ if c.Status != corev1.ConditionTrue {
+ t.Fatalf("Expected to get status %s but got %s", corev1.ConditionTrue, c.Status)
+ }
+ if c.Message != "Tasks Completed: 2 (Failed: 1 (Ignored: 1), Cancelled 0), Skipped: 0" {
+ t.Errorf("Unexpected Error Msg: %s", c.Message)
+ }
+}
+
func TestAdjustStartTime(t *testing.T) {
baseline := metav1.Time{Time: now}
diff --git a/test/ignore_task_error_test.go b/test/ignore_task_error_test.go
new file mode 100644
index 00000000000..becb0618ab7
--- /dev/null
+++ b/test/ignore_task_error_test.go
@@ -0,0 +1,127 @@
+//go:build e2e
+// +build e2e
+
+/*
+Copyright 2023 The Tekton Authors
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package test
+
+import (
+ "context"
+ "fmt"
+ "testing"
+
+ "github.com/google/go-cmp/cmp"
+ v1 "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1"
+ "github.com/tektoncd/pipeline/test/diff"
+ "github.com/tektoncd/pipeline/test/parse"
+ corev1 "k8s.io/api/core/v1"
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+ knativetest "knative.dev/pkg/test"
+)
+
+func TestFailingPipelineTaskOnContinue(t *testing.T) {
+ ctx := context.Background()
+ ctx, cancel := context.WithCancel(ctx)
+ defer cancel()
+ c, namespace := setup(ctx, t, requireAnyGate(map[string]string{"enable-api-fields": "alpha"}))
+ knativetest.CleanupOnInterrupt(func() { tearDown(ctx, t, c, namespace) }, t.Logf)
+ defer tearDown(ctx, t, c, namespace)
+
+ prName := "my-pipelinerun"
+ pr := parse.MustParseV1PipelineRun(t, fmt.Sprintf(`
+metadata:
+ name: %s
+ namespace: %s
+spec:
+ pipelineSpec:
+ tasks:
+ - name: failed-ignored-task
+ onError: continue
+ taskSpec:
+ results:
+ - name: result1
+ type: string
+ steps:
+ - name: failing-step
+ image: busybox
+ script: 'exit 1; echo -n 123 | tee $(results.result1.path)'
+ - name: order-dep-task
+ runAfter: ["failed-ignored-task"]
+ taskSpec:
+ steps:
+ - name: foo
+ image: busybox
+ script: 'echo hello'
+ - name: resource-dep-task
+ onError: continue
+ params:
+ - name: param1
+ value: $(tasks.failed-ignored-task.results.result1)
+ taskSpec:
+ params:
+ - name: param1
+ type: string
+ steps:
+ - name: foo
+ image: busybox
+ script: 'echo $(params.param1)'
+`, prName, namespace))
+
+ if _, err := c.V1PipelineRunClient.Create(ctx, pr, metav1.CreateOptions{}); err != nil {
+ t.Fatalf("Failed to create PipelineRun: %s", err)
+ }
+
+ // wait for PipelineRun to finish
+ t.Logf("Waiting for PipelineRun in namespace %s to finish", namespace)
+ if err := WaitForPipelineRunState(ctx, c, prName, timeout, PipelineRunSucceed(prName), "PipelineRunSucceeded", v1Version); err != nil {
+ t.Errorf("Error waiting for PipelineRun to finish: %s", err)
+ }
+
+ // validate pipelinerun success, with right TaskRun counts
+ pr, err := c.V1PipelineRunClient.Get(ctx, "my-pipelinerun", metav1.GetOptions{})
+ if err != nil {
+ t.Fatalf("Couldn't get expected PipelineRun my-pipelinerun: %s", err)
+ }
+ cond := pr.Status.Conditions[0]
+ if cond.Status != corev1.ConditionTrue {
+ t.Fatalf("Expect my-pipelinerun to success but got: %s", cond)
+ }
+ expectErrMsg := "Tasks Completed: 2 (Failed: 1 (Ignored: 1), Cancelled 0), Skipped: 1"
+ if d := cmp.Diff(expectErrMsg, cond.Message); d != "" {
+ t.Errorf("Got unexpected error message %s", diff.PrintWantGot(d))
+ }
+
+ // validate first TaskRun to fail but ignored
+ failedTaskRun, err := c.V1TaskRunClient.Get(ctx, "my-pipelinerun-failed-ignored-task", metav1.GetOptions{})
+ if err != nil {
+ t.Fatalf("Couldn't get expected TaskRun my-pipelinerun-failed-ignored-task: %s", err)
+ }
+ cond = failedTaskRun.Status.Conditions[0]
+ if cond.Status != corev1.ConditionFalse || cond.Reason != string(v1.TaskRunReasonFailureIgnored) {
+ t.Errorf("Expect failed-ignored-task Task in Failed status with reason %s, but got %s status with reason: %s", v1.TaskRunReasonFailureIgnored, cond.Status, cond.Reason)
+ }
+
+ // validate second TaskRun succeeded
+ orderDepTaskRun, err := c.V1TaskRunClient.Get(ctx, "my-pipelinerun-order-dep-task", metav1.GetOptions{})
+ if err != nil {
+ t.Fatalf("Couldn't get expected TaskRun my-pipelinerun-order-dep-task: %s", err)
+ }
+ cond = orderDepTaskRun.Status.Conditions[0]
+ if cond.Status != corev1.ConditionTrue {
+ t.Errorf("Expect order-dep-task Task to success but got: %s", cond)
+ }
+}