diff --git a/cmd/entrypoint/main.go b/cmd/entrypoint/main.go index dead6fa6581..adf7a0a9d4a 100644 --- a/cmd/entrypoint/main.go +++ b/cmd/entrypoint/main.go @@ -178,7 +178,7 @@ func main() { if err := e.Go(); err != nil { breakpointExitPostFile := e.PostFile + breakpointExitSuffix switch t := err.(type) { //nolint:errorlint // checking for multiple types with errors.As is ugly. - case skipError: + case entrypoint.SkipError: log.Print("Skipping step because a previous step failed") os.Exit(1) case termination.MessageLengthError: diff --git a/cmd/entrypoint/waiter.go b/cmd/entrypoint/waiter.go index 656b015af74..3de2a3bc5f5 100644 --- a/cmd/entrypoint/waiter.go +++ b/cmd/entrypoint/waiter.go @@ -71,7 +71,7 @@ func (rw *realWaiter) Wait(ctx context.Context, file string, expectContent bool, if breakpointOnFailure { return nil } - return skipError("error file present, bail and skip the step") + return entrypoint.ErrSkipPreviousStepFailed } select { case <-ctx.Done(): @@ -86,9 +86,3 @@ func (rw *realWaiter) Wait(ctx context.Context, file string, expectContent bool, } } } - -type skipError string - -func (e skipError) Error() string { - return string(e) -} diff --git a/cmd/entrypoint/waiter_test.go b/cmd/entrypoint/waiter_test.go index c8d2b016a30..2f7500defd5 100644 --- a/cmd/entrypoint/waiter_test.go +++ b/cmd/entrypoint/waiter_test.go @@ -153,7 +153,7 @@ func TestRealWaiterWaitWithErrorWaitfile(t *testing.T) { if err == nil { t.Errorf("expected skipError upon encounter error waitfile") } - var skipErr skipError + var skipErr entrypoint.SkipError if errors.As(err, &skipErr) { close(doneCh) } else { @@ -292,7 +292,7 @@ func TestRealWaiterWaitContextWithErrorWaitfile(t *testing.T) { if err == nil { t.Errorf("expected skipError upon encounter error waitfile") } - var skipErr skipError + var skipErr entrypoint.SkipError if errors.As(err, &skipErr) { close(doneCh) } else { diff --git a/docs/pipeline-api.md b/docs/pipeline-api.md index 99c4b7f8483..fc20e6dee98 100644 --- a/docs/pipeline-api.md +++ b/docs/pipeline-api.md @@ -4638,6 +4638,16 @@ string + + +terminationReason
+ +string + + + + +

StepTemplate diff --git a/pkg/apis/pipeline/v1/openapi_generated.go b/pkg/apis/pipeline/v1/openapi_generated.go index c6a73d7afc3..d6f61ef5faf 100644 --- a/pkg/apis/pipeline/v1/openapi_generated.go +++ b/pkg/apis/pipeline/v1/openapi_generated.go @@ -3142,6 +3142,12 @@ func schema_pkg_apis_pipeline_v1_StepState(ref common.ReferenceCallback) common. }, }, }, + "terminationReason": { + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + Format: "", + }, + }, }, }, }, diff --git a/pkg/apis/pipeline/v1/swagger.json b/pkg/apis/pipeline/v1/swagger.json index 1424609e3b6..397a1ae46e1 100644 --- a/pkg/apis/pipeline/v1/swagger.json +++ b/pkg/apis/pipeline/v1/swagger.json @@ -1609,6 +1609,9 @@ "description": "Details about a terminated container", "$ref": "#/definitions/v1.ContainerStateTerminated" }, + "terminationReason": { + "type": "string" + }, "waiting": { "description": "Details about a waiting container", "$ref": "#/definitions/v1.ContainerStateWaiting" diff --git a/pkg/apis/pipeline/v1/taskrun_types.go b/pkg/apis/pipeline/v1/taskrun_types.go index 7c3cf232ee5..3454fa4ebdb 100644 --- a/pkg/apis/pipeline/v1/taskrun_types.go +++ b/pkg/apis/pipeline/v1/taskrun_types.go @@ -357,6 +357,7 @@ type StepState struct { Container string `json:"container,omitempty"` ImageID string `json:"imageID,omitempty"` Results []TaskRunStepResult `json:"results,omitempty"` + TerminationReason string `json:"terminationReason,omitempty"` } // SidecarState reports the results of running a sidecar in a Task. diff --git a/pkg/entrypoint/entrypointer.go b/pkg/entrypoint/entrypointer.go index c90b6d0960c..7df824d90da 100644 --- a/pkg/entrypoint/entrypointer.go +++ b/pkg/entrypoint/entrypointer.go @@ -53,11 +53,19 @@ func (e ContextError) Error() string { return string(e) } +type SkipError string + +func (e SkipError) Error() string { + return string(e) +} + var ( // ErrContextDeadlineExceeded is the error returned when the context deadline is exceeded ErrContextDeadlineExceeded = ContextError(context.DeadlineExceeded.Error()) // ErrContextCanceled is the error returned when the context is canceled ErrContextCanceled = ContextError(context.Canceled.Error()) + // ErrSkipPreviousStepFailed is the error returned when the step is skipped due to previous step error + ErrSkipPreviousStepFailed = SkipError("error file present, bail and skip the step") ) // IsContextDeadlineError determine whether the error is context deadline @@ -165,6 +173,11 @@ func (e Entrypointer) Go() error { Value: time.Now().Format(timeFormat), ResultType: result.InternalTektonResultType, }) + + if errors.Is(err, ErrSkipPreviousStepFailed) { + output = append(output, e.outputRunResult(pod.TerminationReasonSkipped)) + } + return err } } @@ -194,26 +207,18 @@ func (e Entrypointer) Go() error { } }() err = e.Runner.Run(ctx, e.Command...) - if errors.Is(err, ErrContextDeadlineExceeded) { - output = append(output, result.RunResult{ - Key: "Reason", - Value: "TimeoutExceeded", - ResultType: result.InternalTektonResultType, - }) - } } var ee *exec.ExitError switch { case err != nil && errors.Is(err, ErrContextCanceled): logger.Info("Step was canceling") - output = append(output, result.RunResult{ - Key: "Reason", - Value: "Cancelled", - ResultType: result.InternalTektonResultType, - }) + output = append(output, e.outputRunResult(pod.TerminationReasonCancelled)) e.WritePostFile(e.PostFile, ErrContextCanceled) e.WriteExitCodeFile(e.StepMetadataDir, syscall.SIGKILL.String()) + case errors.Is(err, ErrContextDeadlineExceeded): + e.WritePostFile(e.PostFile, err) + output = append(output, e.outputRunResult(pod.TerminationReasonTimeoutExceeded)) case err != nil && e.BreakpointOnFailure: logger.Info("Skipping writing to PostFile") case e.OnError == ContinueOnError && errors.As(err, &ee): @@ -336,3 +341,12 @@ func (e Entrypointer) waitingCancellation(ctx context.Context, cancel context.Ca cancel() return nil } + +// outputRunResult returns the run reason for a termination +func (e Entrypointer) outputRunResult(terminationReason string) result.RunResult { + return result.RunResult{ + Key: "Reason", + Value: terminationReason, + ResultType: result.InternalTektonResultType, + } +} diff --git a/pkg/entrypoint/entrypointer_test.go b/pkg/entrypoint/entrypointer_test.go index 127d70b992d..a917a5a0686 100644 --- a/pkg/entrypoint/entrypointer_test.go +++ b/pkg/entrypoint/entrypointer_test.go @@ -747,29 +747,203 @@ func TestIsContextCanceledError(t *testing.T) { } } +func TestTerminationReason(t *testing.T) { + tests := []struct { + desc string + waitFiles []string + onError string + runError error + expectedRunErr error + expectedExitCode *string + expectedWrotefile *string + expectedStatus []result.RunResult + }{ + { + desc: "reason completed", + expectedExitCode: ptr("0"), + expectedWrotefile: ptr("postfile"), + expectedStatus: []result.RunResult{ + { + Key: "StartedAt", + ResultType: result.InternalTektonResultType, + }, + }, + }, + { + desc: "reason continued", + onError: ContinueOnError, + runError: ptr(exec.ExitError{}), + expectedRunErr: ptr(exec.ExitError{}), + expectedExitCode: ptr("-1"), + expectedWrotefile: ptr("postfile"), + expectedStatus: []result.RunResult{ + { + Key: "ExitCode", + Value: "-1", + ResultType: result.InternalTektonResultType, + }, + { + Key: "StartedAt", + ResultType: result.InternalTektonResultType, + }, + }, + }, + { + desc: "reason errored", + runError: ptr(exec.Error{}), + expectedRunErr: ptr(exec.Error{}), + expectedWrotefile: ptr("postfile.err"), + expectedStatus: []result.RunResult{ + { + Key: "StartedAt", + ResultType: result.InternalTektonResultType, + }, + }, + }, + { + desc: "reason timedout", + runError: ErrContextDeadlineExceeded, + expectedRunErr: ErrContextDeadlineExceeded, + expectedWrotefile: ptr("postfile.err"), + expectedStatus: []result.RunResult{ + { + Key: "Reason", + Value: pod.TerminationReasonTimeoutExceeded, + ResultType: result.InternalTektonResultType, + }, + { + Key: "StartedAt", + ResultType: result.InternalTektonResultType, + }, + }, + }, + { + desc: "reason skipped", + waitFiles: []string{"file"}, + expectedRunErr: ErrSkipPreviousStepFailed, + expectedWrotefile: ptr("postfile.err"), + expectedStatus: []result.RunResult{ + { + Key: "Reason", + Value: pod.TerminationReasonSkipped, + ResultType: result.InternalTektonResultType, + }, + { + Key: "StartedAt", + ResultType: result.InternalTektonResultType, + }, + }, + }, + } + + for _, test := range tests { + t.Run(test.desc, func(t *testing.T) { + fw, fr, fpw := &fakeWaiter{skipStep: true}, &fakeRunner{runError: test.runError}, &fakePostWriter{} + + tmpFolder, err := os.MkdirTemp("", "") + if err != nil { + t.Fatalf("unexpected error creating temporary folder: %v", err) + } else { + defer os.RemoveAll(tmpFolder) + } + + terminationFile, err := os.CreateTemp(tmpFolder, "termination") + if err != nil { + t.Fatalf("unexpected error creating termination file: %v", err) + } + + e := Entrypointer{ + Command: append([]string{}, []string{}...), + WaitFiles: test.waitFiles, + PostFile: "postfile", + Waiter: fw, + Runner: fr, + PostWriter: fpw, + TerminationPath: terminationFile.Name(), + BreakpointOnFailure: false, + StepMetadataDir: tmpFolder, + OnError: test.onError, + } + + err = e.Go() + + if d := cmp.Diff(test.expectedRunErr, err); d != "" { + t.Fatalf("entrypoint error doesn't match %s", diff.PrintWantGot(d)) + } + + if d := cmp.Diff(test.expectedExitCode, fpw.exitCode); d != "" { + t.Fatalf("exitCode doesn't match %s", diff.PrintWantGot(d)) + } + + if d := cmp.Diff(test.expectedWrotefile, fpw.wrote); d != "" { + t.Fatalf("wrote file doesn't match %s", diff.PrintWantGot(d)) + } + + termination, err := getTermination(t, terminationFile.Name()) + if err != nil { + t.Fatalf("error getting termination output: %v", err) + } + + if d := cmp.Diff(test.expectedStatus, termination); d != "" { + t.Fatalf("termination status doesn't match %s", diff.PrintWantGot(d)) + } + }) + } +} + +func getTermination(t *testing.T, terminationFile string) ([]result.RunResult, error) { + t.Helper() + fileContents, err := os.ReadFile(terminationFile) + if err != nil { + return nil, err + } + + logger, _ := logging.NewLogger("", "status") + terminationStatus, err := termination.ParseMessage(logger, string(fileContents)) + if err != nil { + return nil, err + } + + for i, termination := range terminationStatus { + if termination.Key == "StartedAt" { + terminationStatus[i].Value = "" + } + } + + return terminationStatus, nil +} + type fakeWaiter struct { sync.Mutex waited []string waitCancelDuration time.Duration + skipStep bool } func (f *fakeWaiter) Wait(ctx context.Context, file string, _ bool, _ bool) error { - if file == pod.DownwardMountCancelFile && f.waitCancelDuration > 0 { + switch { + case file == pod.DownwardMountCancelFile && f.waitCancelDuration > 0: time.Sleep(f.waitCancelDuration) - } else if file == pod.DownwardMountCancelFile { + case file == pod.DownwardMountCancelFile: return nil + case f.skipStep: + return ErrSkipPreviousStepFailed } + f.Lock() f.waited = append(f.waited, file) f.Unlock() return nil } -type fakeRunner struct{ args *[]string } +type fakeRunner struct { + args *[]string + runError error +} func (f *fakeRunner) Run(ctx context.Context, args ...string) error { f.args = &args - return nil + return f.runError } type fakePostWriter struct { @@ -903,3 +1077,7 @@ func getMockSpireClient(ctx context.Context) (spire.EntrypointerAPIClient, spire return sc, sc, tr } + +func ptr[T any](value T) *T { + return &value +} diff --git a/pkg/pod/pod.go b/pkg/pod/pod.go index 97cb7567ca1..dbad7ccaf61 100644 --- a/pkg/pod/pod.go +++ b/pkg/pod/pod.go @@ -61,6 +61,18 @@ const ( // osSelectorLabel is the label Kubernetes uses for OS-specific workloads (https://kubernetes.io/docs/reference/labels-annotations-taints/#kubernetes-io-os) osSelectorLabel = "kubernetes.io/os" + + // TerminationReasonTimeoutExceeded indicates a step execution timed out. + TerminationReasonTimeoutExceeded = "TimeoutExceeded" + + // TerminationReasonSkipped indicates a step execution was skipped due to previous step failed. + TerminationReasonSkipped = "Skipped" + + // TerminationReasonContinued indicates a step errored but was ignored since onError was set to continue. + TerminationReasonContinued = "Continued" + + // TerminationReasonCancelled indicates a step was cancelled. + TerminationReasonCancelled = "Cancelled" ) // These are effectively const, but Go doesn't have such an annotation. diff --git a/pkg/pod/status.go b/pkg/pod/status.go index a6a842395ab..e6da606a3ed 100644 --- a/pkg/pod/status.go +++ b/pkg/pod/status.go @@ -272,6 +272,7 @@ func setTaskRunStatusBasedOnStepStatus(ctx context.Context, logger *zap.SugaredL } // Parse termination messages + terminationReason := "" if state.Terminated != nil && len(state.Terminated.Message) != 0 { msg := state.Terminated.Message @@ -311,14 +312,18 @@ func setTaskRunStatusBasedOnStepStatus(ctx context.Context, logger *zap.SugaredL if exitCode != nil { state.Terminated.ExitCode = *exitCode } + + terminationFromResults := extractTerminationReasonFromResults(results) + terminationReason = getTerminationReason(state.Terminated.Reason, terminationFromResults, exitCode) } } trs.Steps = append(trs.Steps, v1.StepState{ - ContainerState: *state, - Name: trimStepPrefix(s.Name), - Container: s.Name, - ImageID: s.ImageID, - Results: taskRunStepResults, + ContainerState: *state, + Name: trimStepPrefix(s.Name), + Container: s.Name, + ImageID: s.ImageID, + Results: taskRunStepResults, + TerminationReason: terminationReason, }) } @@ -488,6 +493,27 @@ func extractExitCodeFromResults(results []result.RunResult) (*int32, error) { return nil, nil //nolint:nilnil // would be more ergonomic to return a sentinel error } +func extractTerminationReasonFromResults(results []result.RunResult) string { + for _, r := range results { + if r.ResultType == result.InternalTektonResultType && r.Key == "Reason" { + return r.Value + } + } + return "" +} + +func getTerminationReason(terminatedStateReason string, terminationFromResults string, exitCodeFromResults *int32) string { + if terminationFromResults != "" { + return terminationFromResults + } + + if exitCodeFromResults != nil { + return TerminationReasonContinued + } + + return terminatedStateReason +} + func updateCompletedTaskRunStatus(logger *zap.SugaredLogger, trs *v1.TaskRunStatus, pod *corev1.Pod, onError v1.PipelineTaskOnErrorType) { if DidTaskRunFail(pod) { msg := getFailureMessage(logger, pod) @@ -612,7 +638,7 @@ func extractContainerFailureMessage(logger *zap.SugaredLogger, status corev1.Con msg := status.State.Terminated.Message r, _ := termination.ParseMessage(logger, msg) for _, runResult := range r { - if runResult.ResultType == result.InternalTektonResultType && runResult.Key == "Reason" && runResult.Value == "TimeoutExceeded" { + if runResult.ResultType == result.InternalTektonResultType && runResult.Key == "Reason" && runResult.Value == TerminationReasonTimeoutExceeded { return fmt.Sprintf("%q exited because the step exceeded the specified timeout limit", status.Name) } } diff --git a/pkg/pod/status_test.go b/pkg/pod/status_test.go index aa35cd05042..476bd2e74e7 100644 --- a/pkg/pod/status_test.go +++ b/pkg/pod/status_test.go @@ -1330,6 +1330,7 @@ func TestMakeTaskRunStatus(t *testing.T) { ContainerState: corev1.ContainerState{ Terminated: &corev1.ContainerStateTerminated{ ExitCode: 11, + Reason: "Continued", }, }, Name: "first", @@ -2341,6 +2342,209 @@ func TestGetStepResultsFromSidecarLogs_Error(t *testing.T) { } } +func TestGetStepTerminationReasonFromContainerStatus(t *testing.T) { + tests := []struct { + desc string + pod corev1.Pod + expectedTerminationReason map[string]string + }{ + { + desc: "Step skipped", + expectedTerminationReason: map[string]string{ + "step-1": "Skipped", + }, + pod: corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "pod", + }, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + {Name: "step-1"}, + }, + }, + Status: corev1.PodStatus{ + Phase: corev1.PodFailed, + ContainerStatuses: []corev1.ContainerStatus{ + { + Name: "step-1", + ImageID: "image-id-1", + State: corev1.ContainerState{ + Terminated: &corev1.ContainerStateTerminated{ + Message: `[{"key":"StartedAt","value":"2023-11-26T19:53:29.452Z","type":3},{"key":"Reason","value":"Skipped","type":3}]`, + ExitCode: 1, + Reason: "Error", + }, + }, + }, + }, + }, + }, + }, + { + desc: "Step continued", + expectedTerminationReason: map[string]string{ + "step-1": "Continued", + }, + pod: corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "pod", + }, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + {Name: "step-1"}, + }, + }, + Status: corev1.PodStatus{ + Phase: corev1.PodFailed, + ContainerStatuses: []corev1.ContainerStatus{ + { + Name: "step-1", + ImageID: "image-id-1", + State: corev1.ContainerState{ + Terminated: &corev1.ContainerStateTerminated{ + Message: `[{"key":"StartedAt","value":"2023-11-26T19:53:29.452Z","type":3},{"key":"ExitCode","value":"1","type":3}]`, + ExitCode: 0, + Reason: "Completed", + }, + }, + }, + }, + }, + }, + }, + { + desc: "Step timedout", + expectedTerminationReason: map[string]string{ + "step-1": "TimeoutExceeded", + }, + pod: corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "pod", + }, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + {Name: "step-1"}, + }, + }, + Status: corev1.PodStatus{ + Phase: corev1.PodFailed, + ContainerStatuses: []corev1.ContainerStatus{ + { + Name: "step-1", + ImageID: "image-id-1", + State: corev1.ContainerState{ + Terminated: &corev1.ContainerStateTerminated{ + Message: `[{"key":"StartedAt","value":"2023-11-26T19:53:29.452Z","type":3},{"key":"Reason","value":"TimeoutExceeded","type":3}]`, + ExitCode: 1, + Reason: "Error", + }, + }, + }, + }, + }, + }, + }, + { + desc: "Step completed", + expectedTerminationReason: map[string]string{ + "step-1": "Completed", + }, + pod: corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "pod", + }, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + {Name: "step-1"}, + }, + }, + Status: corev1.PodStatus{ + Phase: corev1.PodFailed, + ContainerStatuses: []corev1.ContainerStatus{ + { + Name: "step-1", + ImageID: "image-id-1", + State: corev1.ContainerState{ + Terminated: &corev1.ContainerStateTerminated{ + Message: `[{"key":"StartedAt","value":"2023-11-26T19:53:29.452Z","type":3}]`, + ExitCode: 0, + Reason: "Completed", + }, + }, + }, + }, + }, + }, + }, + { + desc: "Step error", + expectedTerminationReason: map[string]string{ + "step-1": "Error", + }, + pod: corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "pod", + }, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + {Name: "step-1"}, + }, + }, + Status: corev1.PodStatus{ + Phase: corev1.PodFailed, + ContainerStatuses: []corev1.ContainerStatus{ + { + Name: "step-1", + ImageID: "image-id-1", + State: corev1.ContainerState{ + Terminated: &corev1.ContainerStateTerminated{ + Message: `[{"key":"StartedAt","value":"2023-11-26T19:53:29.452Z","type":3}]`, + ExitCode: 1, + Reason: "Error", + }, + }, + }, + }, + }, + }, + }, + } + + for _, test := range tests { + t.Run(test.desc, func(t *testing.T) { + startTime := time.Date(2010, 1, 1, 1, 1, 1, 1, time.UTC) + tr := v1.TaskRun{ + ObjectMeta: metav1.ObjectMeta{ + Name: "task-run", + }, + Status: v1.TaskRunStatus{ + TaskRunStatusFields: v1.TaskRunStatusFields{ + StartTime: &metav1.Time{Time: startTime}, + }, + }, + } + logger, _ := logging.NewLogger("", "status") + kubeclient := fakek8s.NewSimpleClientset() + + trs, err := MakeTaskRunStatus(context.Background(), logger, tr, &test.pod, kubeclient, &v1.TaskSpec{}) + if err != nil { + t.Errorf("MakeTaskRunResult: %s", err) + } + + for _, step := range trs.Steps { + if step.Terminated == nil { + t.Errorf("Terminated status not found for %s", step.Name) + } + want := test.expectedTerminationReason[step.Container] + got := step.Terminated.Reason + if d := cmp.Diff(want, got, ignoreVolatileTime); d != "" { + t.Errorf("Diff %s", diff.PrintWantGot(d)) + } + } + }) + } +} + func TestGetTaskResultsFromSidecarLogs(t *testing.T) { sidecarLogResults := []result.RunResult{{ Key: "step-foo.step-res", diff --git a/test/taskrun_test.go b/test/taskrun_test.go index f772c53eff1..1a6f96fe40b 100644 --- a/test/taskrun_test.go +++ b/test/taskrun_test.go @@ -21,6 +21,7 @@ package test import ( "context" + "encoding/json" "fmt" "regexp" "strings" @@ -31,8 +32,10 @@ import ( v1 "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1" "github.com/tektoncd/pipeline/pkg/pod" "github.com/tektoncd/pipeline/test/parse" + jsonpatch "gomodules.xyz/jsonpatch/v2" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" knativetest "knative.dev/pkg/test" "knative.dev/pkg/test/helpers" ) @@ -114,7 +117,7 @@ spec: ContainerState: corev1.ContainerState{ Terminated: &corev1.ContainerStateTerminated{ ExitCode: 1, - Reason: "Error", + Reason: "Skipped", }, }, Name: "unnamed-2", @@ -209,3 +212,270 @@ spec: t.Fatalf("-got, +want: %v", d) } } + +func TestTaskRunStepsTerminationReasons(t *testing.T) { + ctx := context.Background() + c, namespace := setup(ctx, t) + defer tearDown(ctx, t, c, namespace) + fqImageName := getTestImage(busyboxImage) + + tests := []struct { + description string + shouldSucceed bool + taskRun string + shouldCancel bool + expectedStepStatus []v1.StepState + }{ + { + description: "termination completed", + shouldSucceed: true, + taskRun: ` +metadata: + name: %v + namespace: %v +spec: + taskSpec: + steps: + - image: %v + name: first + command: ['/bin/sh'] + args: ['-c', 'echo hello']`, + expectedStepStatus: []v1.StepState{ + { + Container: "step-first", + Name: "first", + ContainerState: corev1.ContainerState{ + Terminated: &corev1.ContainerStateTerminated{ + ExitCode: 0, + Reason: "Completed", + }, + }, + }, + }, + }, + { + description: "termination continued", + shouldSucceed: true, + taskRun: ` +metadata: + name: %v + namespace: %v +spec: + taskSpec: + steps: + - image: %v + onError: continue + name: first + command: ['/bin/sh'] + args: ['-c', 'echo hello; exit 1']`, + expectedStepStatus: []v1.StepState{ + { + Container: "step-first", + Name: "first", + ContainerState: corev1.ContainerState{ + Terminated: &corev1.ContainerStateTerminated{ + ExitCode: 1, + Reason: "Continued", + }, + }, + }, + }, + }, + { + description: "termination errored", + shouldSucceed: false, + taskRun: ` +metadata: + name: %v + namespace: %v +spec: + taskSpec: + steps: + - image: %v + name: first + command: ['/bin/sh'] + args: ['-c', 'echo hello; exit 1']`, + expectedStepStatus: []v1.StepState{ + { + Container: "step-first", + Name: "first", + ContainerState: corev1.ContainerState{ + Terminated: &corev1.ContainerStateTerminated{ + ExitCode: 1, + Reason: "Error", + }, + }, + }, + }, + }, + { + description: "termination timedout", + shouldSucceed: false, + taskRun: ` +metadata: + name: %v + namespace: %v +spec: + taskSpec: + steps: + - image: %v + name: first + timeout: 1s + command: ['/bin/sh'] + args: ['-c', 'echo hello; sleep 5s']`, + expectedStepStatus: []v1.StepState{ + { + Container: "step-first", + Name: "first", + ContainerState: corev1.ContainerState{ + Terminated: &corev1.ContainerStateTerminated{ + ExitCode: 1, + Reason: "TimeoutExceeded", + }, + }, + }, + }, + }, + { + description: "termination skipped", + shouldSucceed: false, + taskRun: ` +metadata: + name: %v + namespace: %v +spec: + taskSpec: + steps: + - image: %v + name: first + command: ['/bin/sh'] + args: ['-c', 'echo hello; exit 1'] + - image: %v + name: second + command: ['/bin/sh'] + args: ['-c', 'echo hello']`, + expectedStepStatus: []v1.StepState{ + { + Container: "step-first", + Name: "first", + ContainerState: corev1.ContainerState{ + Terminated: &corev1.ContainerStateTerminated{ + ExitCode: 1, + Reason: "Error", + }, + }, + }, + { + Container: "step-second", + Name: "second", + ContainerState: corev1.ContainerState{ + Terminated: &corev1.ContainerStateTerminated{ + ExitCode: 1, + Reason: "Skipped", + }, + }, + }, + }, + }, + { + description: "termination cancelled", + shouldSucceed: false, + shouldCancel: true, + taskRun: ` +metadata: + name: %v + namespace: %v +spec: + taskSpec: + steps: + - image: %v + name: first + command: ['/bin/sh'] + args: ['-c', 'sleep infinity; echo hello']`, + expectedStepStatus: []v1.StepState{ + { + Container: "step-first", + Name: "first", + ContainerState: corev1.ContainerState{ + Terminated: &corev1.ContainerStateTerminated{ + ExitCode: 1, + Reason: "TaskRunCancelled", + }, + }, + }, + }, + }, + } + + for _, test := range tests { + t.Run(test.description, func(t *testing.T) { + taskRunName := helpers.ObjectNameForTest(t) + values := []interface{}{taskRunName, namespace} + for range test.expectedStepStatus { + values = append(values, fqImageName) + } + taskRunYaml := fmt.Sprintf(test.taskRun, values...) + taskRun := parse.MustParseV1TaskRun(t, taskRunYaml) + + if _, err := c.V1TaskRunClient.Create(ctx, taskRun, metav1.CreateOptions{}); err != nil { + t.Fatalf("Failed to create TaskRun: %s", err) + } + + expectedTaskRunState := TaskRunFailed(taskRunName) + finalStatus := "Failed" + if test.shouldSucceed { + expectedTaskRunState = TaskRunSucceed(taskRunName) + finalStatus = "Succeeded" + } + + if test.shouldCancel { + expectedTaskRunState = FailedWithReason("TaskRunCancelled", taskRunName) + if err := cancelTaskRun(t, ctx, taskRunName, c); err != nil { + t.Fatalf("Error cancelling taskrun: %s", err) + } + } + + err := WaitForTaskRunState(ctx, c, taskRunName, expectedTaskRunState, finalStatus, v1Version) + if err != nil { + t.Fatalf("Error waiting for TaskRun to finish: %s", err) + } + + taskRunState, err := c.V1TaskRunClient.Get(ctx, taskRunName, metav1.GetOptions{}) + if err != nil { + t.Fatalf("Couldn't get expected TaskRun %s: %s", taskRunName, err) + } + + ignoreTerminatedFields := cmpopts.IgnoreFields(corev1.ContainerStateTerminated{}, "StartedAt", "FinishedAt", "ContainerID", "Message") + ignoreStepFields := cmpopts.IgnoreFields(v1.StepState{}, "ImageID") + if d := cmp.Diff(taskRunState.Status.Steps, test.expectedStepStatus, ignoreTerminatedFields, ignoreStepFields); d != "" { + t.Fatalf("-got, +want: %v", d) + } + }) + } +} + +func cancelTaskRun(t *testing.T, ctx context.Context, taskRunName string, c *clients) error { + t.Helper() + + err := WaitForTaskRunState(ctx, c, taskRunName, Running(taskRunName), "Running", v1Version) + if err != nil { + t.Fatalf("Error waiting for TaskRun to start running before cancelling: %s", err) + } + + patches := []jsonpatch.JsonPatchOperation{{ + Operation: "add", + Path: "/spec/status", + Value: "TaskRunCancelled", + }} + + patchBytes, err := json.Marshal(patches) + if err != nil { + return err + } + + if _, err := c.V1TaskRunClient.Patch(ctx, taskRunName, types.JSONPatchType, patchBytes, metav1.PatchOptions{}, ""); err != nil { + return err + } + + return nil +}