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) + } +}