diff --git a/docs/pipelines.md b/docs/pipelines.md index e734ed1de73..e369ef847f1 100644 --- a/docs/pipelines.md +++ b/docs/pipelines.md @@ -658,8 +658,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/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 26afd74faaf..6504ebe47d1 100644 --- a/pkg/apis/pipeline/v1/pipelinerun_types.go +++ b/pkg/apis/pipeline/v1/pipelinerun_types.go @@ -410,6 +410,9 @@ const ( PipelineRunReasonCELEvaluationFailed PipelineRunReason = "CELEvaluationFailed" ) +// PipelineTaskOnErrorAnnotation is used to pass the failure strategy to TaskRun pods +const PipelineTaskOnErrorAnnotation = "pipeline.tekton.dev/pipeline-task-on-error" + func (t PipelineRunReason) String() string { return string(t) } diff --git a/pkg/apis/pipeline/v1/taskrun_types.go b/pkg/apis/pipeline/v1/taskrun_types.go index 0b8e9451975..fcd9f3987b4 100644 --- a/pkg/apis/pipeline/v1/taskrun_types.go +++ b/pkg/apis/pipeline/v1/taskrun_types.go @@ -190,6 +190,8 @@ const ( TaskRunReasonStopSidecarFailed = "TaskRunStopSidecarFailed" // TaskRunReasonInvalidParamValue indicates that the TaskRun Param input value is not allowed. TaskRunReasonInvalidParamValue = "InvalidParamValue" + // TaskRunReasonFailureIgnored is the reason set when the Taskrun has failed and the failure is ignored for the owning PipelineRun + TaskRunReasonFailureIgnored TaskRunReason = "FailureIgnored" ) func (t TaskRunReason) String() string { 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/pod/status.go b/pkg/pod/status.go index 7c52ca4ce32..0281aab6f2d 100644 --- a/pkg/pod/status.go +++ b/pkg/pod/status.go @@ -132,7 +132,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) } @@ -359,10 +364,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 77c0176399a..dbe904ed47b 100644 --- a/pkg/pod/status_test.go +++ b/pkg/pod/status_test.go @@ -1293,6 +1293,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 2d06ccd3ead..ebf9f9e658d 100644 --- a/pkg/reconciler/pipelinerun/pipelinerun.go +++ b/pkg/reconciler/pipelinerun/pipelinerun.go @@ -920,6 +920,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 e57d2883714..947a82def0c 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 99430adce49..a7b1be42941 100644 --- a/pkg/reconciler/pipelinerun/resources/pipelinerunresolution.go +++ b/pkg/reconciler/pipelinerun/resources/pipelinerunresolution.go @@ -392,7 +392,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 cd0991fa81c..1389f20cfe1 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..86aacef53ec --- /dev/null +++ b/test/ignore-task-error_test.go @@ -0,0 +1,148 @@ +//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" + "github.com/tektoncd/pipeline/pkg/apis/config" + 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" + "knative.dev/pkg/system" + 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) + knativetest.CleanupOnInterrupt(func() { tearDown(ctx, t, c, namespace) }, t.Logf) + defer resetAlphaFeatureFlagAndCleanup(ctx, t, c, namespace) + + // update feature flag + configMapData := map[string]string{ + "enable-api-fields": config.AlphaAPIFields, + } + if err := updateConfigMap(ctx, c.KubeClient, system.Namespace(), config.GetFeatureFlagsConfigName(), configMapData); err != nil { + t.Fatal(err) + } + + 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 successed + 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) + } +} + +func resetAlphaFeatureFlagAndCleanup(ctx context.Context, t *testing.T, c *clients, namespace string) { + t.Helper() + configMapData := map[string]string{ + "enable-api-fields": config.DefaultFeatureFlags.EnableAPIFields, + } + if err := updateConfigMap(ctx, c.KubeClient, system.Namespace(), config.GetFeatureFlagsConfigName(), configMapData); err != nil { + t.Fatal(err) + } + tearDown(ctx, t, c, namespace) +}