diff --git a/cmd/entrypoint/main.go b/cmd/entrypoint/main.go index 9919d978957..74facc0b9de 100644 --- a/cmd/entrypoint/main.go +++ b/cmd/entrypoint/main.go @@ -43,6 +43,10 @@ var ( results = flag.String("results", "", "If specified, list of file names that might contain task results") timeout = flag.Duration("timeout", time.Duration(0), "If specified, sets timeout for step") breakpointOnFailure = flag.Bool("breakpoint_on_failure", false, "If specified, expect steps to not skip on failure") + onError = flag.String("on_error", "", "Set to \"continue\" to ignore an error and continue when a container terminates with a non-zero exit code."+ + " Set to \"fail\" to declare a failure with a step error and stop executing the rest of the steps.") + stepMetadataDir = flag.String("step_metadata_dir", "", "If specified, create directory to store the step metadata e.g. /tekton/steps//") + stepMetadataDirLink = flag.String("step_metadata_dir_link", "", "creates a symbolic link to the specified step_metadata_dir e.g. /tekton/steps//") ) const ( @@ -108,6 +112,9 @@ func main() { Results: strings.Split(*results, ","), Timeout: timeout, BreakpointOnFailure: *breakpointOnFailure, + OnError: *onError, + StepMetadataDir: *stepMetadataDir, + StepMetadataDirLink: *stepMetadataDirLink, } // Copy any creds injected by the controller into the $HOME directory of the current @@ -134,9 +141,15 @@ func main() { // same signature. if status, ok := t.Sys().(syscall.WaitStatus); ok { checkForBreakpointOnFailure(e, breakpointExitPostFile) - os.Exit(status.ExitStatus()) + // ignore a step error i.e. do not exit if a container terminates with a non-zero exit code when onError is set to "continue" + if e.OnError != entrypoint.ContinueOnError { + os.Exit(status.ExitStatus()) + } + } + // log and exit only if a step error must cause run failure + if e.OnError != entrypoint.ContinueOnError { + log.Fatalf("Error executing command (ExitError): %v", err) } - log.Fatalf("Error executing command (ExitError): %v", err) default: checkForBreakpointOnFailure(e, breakpointExitPostFile) log.Fatalf("Error executing command: %v", err) diff --git a/cmd/entrypoint/post_writer.go b/cmd/entrypoint/post_writer.go index 86e4aa543ed..43b2d47980d 100644 --- a/cmd/entrypoint/post_writer.go +++ b/cmd/entrypoint/post_writer.go @@ -12,11 +12,43 @@ type realPostWriter struct{} var _ entrypoint.PostWriter = (*realPostWriter)(nil) -func (*realPostWriter) Write(file string) { +// Write creates a file and writes content to that file if content is specified +// assumption here is the underlying directory structure already exists +func (*realPostWriter) Write(file string, content string) { if file == "" { return } - if _, err := os.Create(file); err != nil { + f, err := os.Create(file) + if err != nil { log.Fatalf("Creating %q: %v", file, err) } + + if content != "" { + if _, err := f.WriteString(content); err != nil { + log.Fatalf("Writing %q: %v", file, err) + } + } +} + +// CreateDirWithSymlink creates the specified directory and a symbolic link to that directory +func (*realPostWriter) CreateDirWithSymlink(source, link string) { + if source == "" { + return + } + if err := os.MkdirAll(source, 0770); err != nil { + log.Fatalf("Creating directory %q: %v", source, err) + } + + if link == "" { + return + } + // create a symlink if it does not exist + if _, err := os.Stat(link); os.IsNotExist(err) { + // check if a source exist before creating a symbolic link + if _, err := os.Stat(source); err == nil { + if err := os.Symlink(source, link); err != nil { + log.Fatalf("Creating a symlink %q: %v", link, err) + } + } + } } diff --git a/cmd/entrypoint/post_writer_test.go b/cmd/entrypoint/post_writer_test.go new file mode 100644 index 00000000000..a0ce5c1fe1e --- /dev/null +++ b/cmd/entrypoint/post_writer_test.go @@ -0,0 +1,76 @@ +package main + +import ( + "os" + "testing" +) + +func TestRealPostWriter_WriteFileContent(t *testing.T) { + tests := []struct { + name, file, content string + }{{ + name: "write a file content", + file: "sample.txt", + content: "this is a sample file", + }, { + name: "write a file without specifying any path", + }, { + name: "create an empty file", + file: "sample.txt", + }} + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + rw := realPostWriter{} + rw.Write(tt.file, tt.content) + if tt.file != "" { + defer os.Remove(tt.file) + if _, err := os.Stat(tt.file); err != nil { + t.Fatalf("Failed to create a file %q", tt.file) + } + b, err := os.ReadFile(tt.file) + if err != nil { + t.Fatalf("Failed to read the file %q", tt.file) + } + if tt.content != string(b) { + t.Fatalf("Failed to write the desired content %q to the file %q", tt.content, tt.file) + } + } + }) + } +} + +func TestRealPostWriter_CreateStepPath(t *testing.T) { + tests := []struct { + name, source, link string + }{{ + name: "Create a path with a file", + source: "sample.txt", + link: "0", + }, { + name: "Create a path without specifying any path", + }, { + name: "Create a sym link without specifying any link path", + source: "sample.txt", + }, { + name: "Create a sym link without specifying any source", + link: "0.txt", + }} + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + rw := realPostWriter{} + rw.CreateDirWithSymlink(tt.source, tt.link) + if tt.source != "" { + defer os.Remove(tt.source) + if _, err := os.Stat(tt.source); err != nil { + t.Fatalf("Failed to create a file %q", tt.source) + } + } + if tt.source != "" && tt.link != "" { + defer os.Remove(tt.link) + if _, err := os.Stat(tt.link); err != nil { + t.Fatalf("Failed to create a sym link %q", tt.link) + } + } + }) + } +} diff --git a/docs/developers/README.md b/docs/developers/README.md index 1a788c01838..84ed6e2b8c1 100644 --- a/docs/developers/README.md +++ b/docs/developers/README.md @@ -142,6 +142,8 @@ of how this directory is used: * These folders are [part of the Tekton API](../api_compatibility_policy.md): * `/tekton/results` is where [results](#results) are written to (path available to `Task` authors via [`$(results.name.path)`](../variables.md)) + * `/tekton/steps` is where the `step` exitCodes are written to + (path available to `Task` authors via [`$(steps..exitCode.path)`](../variables.md#variables-available-in-a-task)) * These folders are implementation details of Tekton and **users should not rely on this specific behavior as it may change in the future**: * `/tekton/tools` contains tools like the [entrypoint binary](#entrypoint-rewriting-and-step-ordering) @@ -466,3 +468,163 @@ flag to `alpha` in your test cluster to see your alpha integration tests run. When the flag in your cluster is `alpha` _all_ integration tests are executed, both `stable` and `alpha`. Setting the feature flag to `stable` will exclude `alpha` tests. + +## What and Why of `/tekton/steps` + +`/tekton/steps/` is an implicit volume mounted on a pod and created for storing the step specific information/metadata. +There is one more subdirectory created under `/tekton/steps/` for each step in a task. + +Let's take an example of a task with three steps, each exiting with non-zero exit code: + +```yaml +kind: TaskRun +apiVersion: tekton.dev/v1beta1 +metadata: + generateName: test-taskrun- +spec: + taskSpec: + steps: + - image: alpine + name: step0 + onError: continue + script: | + echo "This is step 0" + ls -1R /tekton/steps/ + exit 1 + - image: alpine + onError: continue + script: | + echo "This is step 1" + ls -1R /tekton/steps/ + exit 2 + - image: alpine + name: step2 + onError: continue + script: | + echo "This is step 2" + ls -1R /tekton/steps/ + exit 3 +``` + +The container `step-step0` for the first step `step0` shows three subdirectories (one for each step) under +`/tekton/steps/` and all three of them are empty. + +``` +kubectl logs pod/test-taskrun-2rb9k-pod-bphct -c step-step0 ++ echo 'This is step 0' ++ ls -1R /tekton/steps/ +This is step 0 +/tekton/steps/: +0 +1 +2 +step-step0 +step-step2 +step-unnamed-1 + +/tekton/steps/step-step0: +/tekton/steps/step-step2: +/tekton/steps/step-unnamed-1: ++ exit 1 +``` + +The container `step-unnamed-1` for the second step which has no name shows three subdirectories (one for each step) +under `/tekton/steps/` along with the `exitCode` file under the first step directory which has finished executing: + +``` +kubectl logs pod/test-taskrun-2rb9k-pod-bphct -c step-unnamed-1 +This is step 1 ++ echo 'This is step 1' ++ ls -1R /tekton/steps/ +/tekton/steps/: +0 +1 +2 +step-step0 +step-step2 +step-unnamed-1 + +/tekton/steps/step-step0: +exitCode + +/tekton/steps/step-step2: + +/tekton/steps/step-unnamed-1: ++ exit 2 +``` + +The container `step-step2` for the third step `step2` shows three subdirectories (one for each step) under +`/tekton/steps/` along with the `exitCode` file under the first and second step directory since both are done executing: + +``` +kubectl logs pod/test-taskrun-2rb9k-pod-bphct -c step-step2 +This is step 2 ++ echo 'This is step 2' ++ ls -1R /tekton/steps/ +/tekton/steps/: +0 +1 +2 +step-step0 +step-step2 +step-unnamed-1 + +/tekton/steps/step-step0: +exitCode + +/tekton/steps/step-step2: + +/tekton/steps/step-unnamed-1: +exitCode ++ exit 3 +``` + +The entrypoint is modified to include an additional two flags representing the step specific directory and a symbolic +link: + +``` +step_metadata_dir - the dir specified in this flag is created to hold a step specific metadata +step_metadata_dir_link - the dir specified in this flag is created as a symbolic link to step_metadata_dir +``` + +`step_metadata_dir` is set to `/tekton/steps/step-step0` and `step_metadata_dir_link` is set to `/tekton/steps/0` for +the entrypoint of the first step in the above example task. + +Notice an additional entries `0`, `1`, and `2` showing under `/tekton/steps/`. These are symbolic links created which are +linked with their respective step directories, `step-step0`, `step-unnamed-1`, and `step-step2`. These symbolic links +are created to provide simplified access to the step metadata directories i.e., instead of referring to a directory with +the step name, access it via the step index. The step index becomes complex and hard to keep track of in a task with +a long list of steps, for example, a task with 20 steps. Creating the step metadata directory using a step name +and creating a symbolic link using the step index gives the user flexibility, and an option to choose whatever works +best for them. + + +## How to access the exit code of a step from any subsequent step in a task + +The entrypoint now allows exiting with an error and continue running rest of the steps in a task i.e., it is possible +for a step to exit with a non-zero exit code. Now, it is possible to design a task with a step which can take an action +depending on the exit code of any prior steps. The user can access the exit code of a step by reading the file pointed +by the path variable `$(steps.step-.exitCode.path)` or `$(steps.step-unnamed-.exitCode.path)`. +For example: + +* `$(steps.step-my-awesome-step.exitCode.path)` where the step name is `my-awesome-step`. +* `$(steps.step-unnamed-0.exitCode.path)` where the first step in a task has no name. + +The exit code of a step is stored in a file named `exitCode` under a directory `/tekton/steps/step-/` or +`/tekton/steps/step-unnamed-/` which is reserved for any other step specific information in the future. + +If you would like to use the tekton internal path, you can access the exit code by reading the file +(which is not recommended though since the path might change in the future): + +```shell +cat /tekton/steps/step-/exitCode +``` + +And, access the step exit code without a step name: + +```shell +cat /tekton/steps/step-unnamed-/exitCode +``` + +Or, you can access the step metadata directory via symlink, for example, use `cat /tekton/steps/0/exitCode` for the +first step in a task. \ No newline at end of file diff --git a/docs/tasks.md b/docs/tasks.md index 34f2eca2bd8..1030d27d22e 100644 --- a/docs/tasks.md +++ b/docs/tasks.md @@ -13,6 +13,10 @@ weight: 200 - [Reserved directories](#reserved-directories) - [Running scripts within `Steps`](#running-scripts-within-steps) - [Specifying a timeout](#specifying-a-timeout) + - [Specifying `onError` for a `step`](#specifying-onerror-for-a-step) + - [Accessing Step's `exitCode` in subsequent `Steps`](#accessing-steps-exitcode-in-subsequent-steps) + - [Produce a task result with `onError`](#produce-a-task-result-with-onerror) + - [Breakpoint on failure with `onError`](#breakpoint-on-failure-with-onerror) - [Specifying `Parameters`](#specifying-parameters) - [Specifying `Resources`](#specifying-resources) - [Specifying `Workspaces`](#specifying-workspaces) @@ -282,6 +286,116 @@ steps: sleep 60 timeout: 5s ``` + +#### Specifying `onError` for a `step` + +This is an alpha feature. The `enable-api-fields` feature flag [must be set to `"alpha"`](./install.md) +to specify `onError` for a `step`. + +When a `step` in a `task` results in a failure, the rest of the steps in the `task` are skipped and the `taskRun` is +declared a failure. If you would like to ignore such step errors and continue executing the rest of the steps in +the task, you can specify `onError` for such a `step`. + +`onError` can be set to either `continue` or `fail` as part of the step definition. If `onError` is +set to `continue`, the entrypoint sets the original failed exit code of the [script](#running-scripts-within-steps) +in the container terminated state. A `step` with `onError` set to `continue` does not fail the `taskRun` and continues +executing the rest of the steps in a task. + +To ignore a step error, set `onError` to `continue`: + +```yaml +steps: + - image: docker.io/library/golang:latest + name: ignore-unit-test-failure + onError: continue + script: | + go test . +``` + +The original failed exit code of the [script](#running-scripts-within-steps) is available in the terminated state of +the container. + +``` +kubectl get tr taskrun-unit-test-t6qcl -o json | jq .status +{ + "conditions": [ + { + "message": "All Steps have completed executing", + "reason": "Succeeded", + "status": "True", + "type": "Succeeded" + } + ], + "steps": [ + { + "container": "step-ignore-unit-test-failure", + "imageID": "...", + "name": "ignore-unit-test-failure", + "terminated": { + "containerID": "...", + "exitCode": 1, + "reason": "Completed", + } + }, + ], +``` + +For an end-to-end example, see [the taskRun ignoring a step error](../examples/v1beta1/taskruns/alpha/ignore-step-error.yaml) +and [the pipelineRun ignoring a step error](../examples/v1beta1/pipelineruns/alpha/ignore-step-error.yaml). + +#### Accessing Step's `exitCode` in subsequent `Steps` + +A step can access the exit code of any previous step by reading the file pointed to by the `exitCode` path variable: + +```shell +cat $(steps.step-.exitCode.path) +``` + +The `exitCode` of a step without any name can be referenced using: + +```shell +cat $(steps.step-unnamed-.exitCode.path) +``` + +#### Produce a task result with `onError` + +When a step is set to ignore the step error and if that step is able to initialize a result file before failing, +that result is made available to its consumer task. + +```yaml +steps: + - name: ignore-failure-and-produce-a-result + onError: continue + image: busybox + script: | + echo -n 123 | tee $(results.result1.path) + exit 1 +``` + +The task consuming the result using the result reference `$(tasks.task1.results.result1)` in a `pipeline` will be able +to access the result and run with the resolved value. + +Now, a step can fail before initializing a result and the `pipeline` can ignore such step failure. But, the `pipeline` +will fail with `InvalidTaskResultReference` if it has a task consuming that task result. For example, any task +consuming `$(tasks.task1.results.result2)` will cause the pipeline to fail. + +```yaml +steps: + - name: ignore-failure-and-produce-a-result + onError: continue + image: busybox + script: | + echo -n 123 | tee $(results.result1.path) + exit 1 + echo -n 456 | tee $(results.result2.path) +``` + +#### Breakpoint on failure with `onError` + +[Debugging](taskruns.md#debugging-a-taskrun) a taskRun is supported to debug a container and comes with a set of +[tools](taskruns.md#debug-environment) to declare the step as a failure or a success. Specifying +[breakpoint](taskruns.md#breakpoint-on-failure) at the `taskRun` level overrides ignoring a step error using `onError`. + ### Specifying `Parameters` You can specify parameters, such as compilation flags or artifact names, that you want to supply to the `Task` at execution time. diff --git a/docs/variables.md b/docs/variables.md index 9715d136c78..bfbd7abd1e7 100644 --- a/docs/variables.md +++ b/docs/variables.md @@ -45,6 +45,8 @@ For instructions on using variable substitutions see the relevant section of [th | `context.taskRun.uid` | The uid of the `TaskRun` that this `Task` is running in. | | `context.task.name` | The name of this `Task`. | | `context.task.retry-count` | The current retry number of this `Task`. | +| `steps.step-.exitCode.path` | The path to the file where a Step's exit code is stored. | +| `steps.step-unnamed-.exitCode.path` | The path to the file where a Step's exit code is stored for a step without any name. | ### `PipelineResource` variables available in a `Task` diff --git a/examples/v1beta1/pipelineruns/alpha/ignore-step-error.yaml b/examples/v1beta1/pipelineruns/alpha/ignore-step-error.yaml new file mode 100644 index 00000000000..8fd46ff4ef0 --- /dev/null +++ b/examples/v1beta1/pipelineruns/alpha/ignore-step-error.yaml @@ -0,0 +1,59 @@ +kind: PipelineRun +apiVersion: tekton.dev/v1beta1 +metadata: + generateName: pipelinerun-with-failing-step- +spec: + serviceAccountName: 'default' + pipelineSpec: + tasks: + - name: task1 + taskSpec: + steps: + # not really doing anything here, just a hurdle to test the "ignore step error" + - image: alpine + onError: continue + name: exit-with-1 + script: | + exit 1 + # initialize a task result which will be validated by the next task + - image: alpine + name: write-a-result + onError: continue + script: | + echo -n 123 | tee $(results.task1-result.path) + exit 11 + results: + - name: task1-result + description: result of a task1 + - name: task2 + runAfter: ["task1"] + params: + - name: task1-result + value: $(tasks.task1.results.task1-result) + taskSpec: + params: + - name: task1-result + steps: + # again, not really doing anything here, just a hurdle to test the "ignore step error" + - image: alpine + onError: continue + name: exit-with-255 + script: | + exit 255 + # verify that the task result was produced by the first task, fail if the result does not match + - image: alpine + name: verify-a-task-result + script: | + ls /tekton/results/ + if [ $(params.task1-result) == 123 ]; then + echo "Yay! the task result matches which was initialized in the previous task while ignoring the step error" + else + echo "the task result does not match." + exit 1 + fi + # the last step of a task and one more hurdle + - image: alpine + name: exit-with-20 + onError: continue + script: | + exit 20 diff --git a/examples/v1beta1/taskruns/alpha/ignore-step-error.yaml b/examples/v1beta1/taskruns/alpha/ignore-step-error.yaml new file mode 100644 index 00000000000..a459105c645 --- /dev/null +++ b/examples/v1beta1/taskruns/alpha/ignore-step-error.yaml @@ -0,0 +1,49 @@ +kind: TaskRun +apiVersion: tekton.dev/v1beta1 +metadata: + generateName: taskrun-with-failing-step- +spec: + taskSpec: + steps: + # exit with 1 and ignore non zero exit code + - image: alpine + onError: continue + name: exit-with-1 + script: | + exit 1 + # check if the /tekton/steps/step-/exitCode got created and contains the exit code + # check if the symlink /tekton/steps/0/ got created + - image: alpine + name: verify-step-path + script: | + exitCode=`cat $(steps.step-exit-with-1.exitCode.path)` + if [ $exitCode == 1 ]; then + echo "Yay! the exit code can be accessed using the path variable and matches the previous step exit code" + else + echo "the exit code does not match." + exit 1 + fi + FILE=/tekton/steps/step-exit-with-1/exitCode + if [ -f "$FILE" ]; then + echo "$FILE exists." + echo "Yay! the file exists which was created by the controller to record the step exit code." + else + echo "$FILE does not exist." + exit 1 + fi + FILE=/tekton/steps/0/exitCode + if [ -f "$FILE" ]; then + echo "$FILE exists." + echo "Yay! the symlink exists which was created by the controller to record the step exit code." + else + echo "$FILE does not exist." + exit 1 + fi + exitCode=`cat $FILE` + if [ $exitCode == 1 ]; then + echo "Yay! the exit code matches to the previous step exit code" + else + echo "the exit code does not match." + exit 1 + fi +--- diff --git a/internal/builder/v1beta1/step.go b/internal/builder/v1beta1/step.go index e9c428c8d60..01ec2805d9c 100644 --- a/internal/builder/v1beta1/step.go +++ b/internal/builder/v1beta1/step.go @@ -86,3 +86,10 @@ func StepScript(script string) StepOp { step.Script = script } } + +// StepOnError sets the onError of a step +func StepOnError(e string) StepOp { + return func(step *v1beta1.Step) { + step.OnError = e + } +} diff --git a/pkg/apis/pipeline/paths.go b/pkg/apis/pipeline/paths.go index 9b51b482c09..5a15b2014e1 100644 --- a/pkg/apis/pipeline/paths.go +++ b/pkg/apis/pipeline/paths.go @@ -26,4 +26,6 @@ const ( // CredsDir is the directory where credentials are placed to meet the legacy credentials // helpers image (aka "creds-init") contract CredsDir = "/tekton/creds" + // StepsDir is the directory used for a step to store any metadata related to the step + StepsDir = "/tekton/steps" ) diff --git a/pkg/apis/pipeline/v1beta1/openapi_generated.go b/pkg/apis/pipeline/v1beta1/openapi_generated.go index 60edf78d6b6..63b5f1dd959 100644 --- a/pkg/apis/pipeline/v1beta1/openapi_generated.go +++ b/pkg/apis/pipeline/v1beta1/openapi_generated.go @@ -3090,6 +3090,13 @@ func schema_pkg_apis_pipeline_v1beta1_Step(ref common.ReferenceCallback) common. }, }, }, + "onError": { + SchemaProps: spec.SchemaProps{ + Description: "OnError defines the exiting behavior of a container on error can be set to [ continue | fail ] fail indicates exit the taskRun if the container exits with non-zero exit code continue indicates continue executing the rest of the steps irrespective of the container exit code", + Type: []string{"string"}, + Format: "", + }, + }, }, Required: []string{"name"}, }, diff --git a/pkg/apis/pipeline/v1beta1/swagger.json b/pkg/apis/pipeline/v1beta1/swagger.json index 91e566fa452..251d26ddbcb 100644 --- a/pkg/apis/pipeline/v1beta1/swagger.json +++ b/pkg/apis/pipeline/v1beta1/swagger.json @@ -1767,6 +1767,10 @@ "type": "string", "default": "" }, + "onError": { + "description": "OnError defines the exiting behavior of a container on error can be set to [ continue | fail ] fail indicates exit the taskRun if the container exits with non-zero exit code continue indicates continue executing the rest of the steps irrespective of the container exit code", + "type": "string" + }, "ports": { "description": "List of ports to expose from the container. Exposing a port here gives the system additional information about the network connections a container uses, but is primarily informational. Not specifying a port here DOES NOT prevent that port from being exposed. Any port which is listening on the default \"0.0.0.0\" address inside a container will be accessible from the network. Cannot be updated.", "type": "array", diff --git a/pkg/apis/pipeline/v1beta1/task_types.go b/pkg/apis/pipeline/v1beta1/task_types.go index 05fb550cbee..8a0fd66ef19 100644 --- a/pkg/apis/pipeline/v1beta1/task_types.go +++ b/pkg/apis/pipeline/v1beta1/task_types.go @@ -141,6 +141,12 @@ type Step struct { // not have access to it. // +optional Workspaces []WorkspaceUsage `json:"workspaces,omitempty"` + + // OnError defines the exiting behavior of a container on error + // can be set to [ continue | fail ] + // fail indicates exit the taskRun if the container exits with non-zero exit code + // continue indicates continue executing the rest of the steps irrespective of the container exit code + OnError string `json:"onError,omitempty"` } // Sidecar has nearly the same data structure as Step, consisting of a Container and an optional Script, but does not have the ability to timeout. diff --git a/pkg/apis/pipeline/v1beta1/task_validation.go b/pkg/apis/pipeline/v1beta1/task_validation.go index 0269b66f920..2d9f8aedb00 100644 --- a/pkg/apis/pipeline/v1beta1/task_validation.go +++ b/pkg/apis/pipeline/v1beta1/task_validation.go @@ -58,7 +58,7 @@ func (ts *TaskSpec) Validate(ctx context.Context) (errs *apis.FieldError) { }) } - errs = errs.Also(validateSteps(mergedSteps).ViaField("steps")) + errs = errs.Also(validateSteps(ctx, mergedSteps).ViaField("steps")) errs = errs.Also(ts.Resources.Validate(ctx).ViaField("resources")) errs = errs.Also(ValidateParameterTypes(ts.Params).ViaField("params")) errs = errs.Also(ValidateParameterVariables(ts.Steps, ts.Params)) @@ -168,16 +168,16 @@ func ValidateVolumes(volumes []corev1.Volume) (errs *apis.FieldError) { return errs } -func validateSteps(steps []Step) (errs *apis.FieldError) { +func validateSteps(ctx context.Context, steps []Step) (errs *apis.FieldError) { // Task must not have duplicate step names. names := sets.NewString() for idx, s := range steps { - errs = errs.Also(validateStep(s, names).ViaIndex(idx)) + errs = errs.Also(validateStep(ctx, s, names).ViaIndex(idx)) } return errs } -func validateStep(s Step, names sets.String) (errs *apis.FieldError) { +func validateStep(ctx context.Context, s Step, names sets.String) (errs *apis.FieldError) { if s.Image == "" { errs = errs.Also(apis.ErrMissingField("Image")) } @@ -220,6 +220,17 @@ func validateStep(s Step, names sets.String) (errs *apis.FieldError) { errs = errs.Also(apis.ErrGeneric(fmt.Sprintf(`volumeMount name %q cannot start with "tekton-internal-"`, vm.Name), "name").ViaFieldIndex("volumeMounts", j)) } } + + if s.OnError != "" { + errs = errs.Also(ValidateEnabledAPIFields(ctx, "step onError", config.AlphaAPIFields).ViaField("steps")) + if s.OnError != "continue" && s.OnError != "fail" { + errs = errs.Also(&apis.FieldError{ + Message: fmt.Sprintf("invalid value: %v", s.OnError), + Paths: []string{"onError"}, + Details: "Task step onError must be either continue or fail", + }) + } + } return errs } diff --git a/pkg/apis/pipeline/v1beta1/task_validation_test.go b/pkg/apis/pipeline/v1beta1/task_validation_test.go index 85646c4a40b..7d56176919b 100644 --- a/pkg/apis/pipeline/v1beta1/task_validation_test.go +++ b/pkg/apis/pipeline/v1beta1/task_validation_test.go @@ -1135,6 +1135,68 @@ func TestStepAndSidecarWorkspacesErrors(t *testing.T) { } } +func TestStepOnError(t *testing.T) { + tests := []struct { + name string + steps []v1beta1.Step + expectedError *apis.FieldError + }{{ + name: "valid step - valid onError usage - set to continue - alpha API", + steps: []v1beta1.Step{{ + OnError: "continue", + Container: corev1.Container{ + Image: "image", + Args: []string{"arg"}, + }, + }}, + }, { + name: "valid step - valid onError usage - set to fail - alpha API", + steps: []v1beta1.Step{{ + OnError: "fail", + Container: corev1.Container{ + Image: "image", + Args: []string{"arg"}, + }, + }}, + }, { + name: "invalid step - onError set to invalid value - alpha API", + steps: []v1beta1.Step{{ + OnError: "onError", + Container: corev1.Container{ + Image: "image", + Args: []string{"arg"}, + }, + }}, + expectedError: &apis.FieldError{ + Message: fmt.Sprintf("invalid value: onError"), + Paths: []string{"onError"}, + Details: "Task step onError must be either continue or fail", + }, + }} + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ts := &v1beta1.TaskSpec{ + Steps: tt.steps, + } + featureFlags, _ := config.NewFeatureFlagsFromMap(map[string]string{ + "enable-api-fields": "alpha", + }) + cfg := &config.Config{ + FeatureFlags: featureFlags, + } + ctx := config.ToContext(context.Background(), cfg) + ts.SetDefaults(ctx) + err := ts.Validate(ctx) + if tt.expectedError == nil && err != nil { + t.Errorf("TaskSpec.Validate() = %v", err) + } else if tt.expectedError != nil && err == nil { + t.Errorf("TaskSpec.Validate() = %v", err) + } + }) + } + +} + // TestIncompatibleAPIVersions exercises validation of fields that // require a specific feature gate version in order to work. func TestIncompatibleAPIVersions(t *testing.T) { @@ -1179,6 +1241,18 @@ func TestIncompatibleAPIVersions(t *testing.T) { }}, }}, }, + }, { + name: "step onError requires alpha", + requiredVersion: "alpha", + spec: v1beta1.TaskSpec{ + Steps: []v1beta1.Step{{ + OnError: "continue", + Container: corev1.Container{ + Image: "image", + Args: []string{"arg"}, + }, + }}, + }, }} versions := []string{"alpha", "stable"} for _, tt := range tests { diff --git a/pkg/entrypoint/entrypointer.go b/pkg/entrypoint/entrypointer.go index be2e4f41b99..787d4278d7f 100644 --- a/pkg/entrypoint/entrypointer.go +++ b/pkg/entrypoint/entrypointer.go @@ -18,10 +18,12 @@ package entrypoint import ( "context" + "errors" "fmt" "io/ioutil" "log" "os" + "os/exec" "path/filepath" "strconv" "strings" @@ -35,7 +37,9 @@ import ( // RFC3339 with millisecond const ( - timeFormat = "2006-01-02T15:04:05.000Z07:00" + timeFormat = "2006-01-02T15:04:05.000Z07:00" + ContinueOnError = "continue" + FailOnError = "fail" ) // Entrypointer holds fields for running commands with redirected @@ -71,6 +75,16 @@ type Entrypointer struct { Timeout *time.Duration // BreakpointOnFailure helps determine if entrypoint execution needs to adapt debugging requirements BreakpointOnFailure bool + // OnError defines exiting behavior of the entrypoint + // set it to "fail" to indicate the entrypoint to exit the taskRun if the container exits with non zero exit code + // set it to "continue" to indicate the entrypoint to continue executing the rest of the steps irrespective of the container exit code + OnError string + // StepMetadataDir is the directory for a step where the step related metadata can be stored + StepMetadataDir string + // StepMetadataDirLink is the directory which needs to be linked to the StepMetadataDir + // the symlink is mainly created for providing easier access to the step metadata + // i.e. use `/tekton/steps/0/exitCode` instead of `/tekton/steps/my-awesome-step/exitCode` + StepMetadataDirLink string } // Waiter encapsulates waiting for files to exist. @@ -87,7 +101,9 @@ type Runner interface { // PostWriter encapsulates writing a file when complete. type PostWriter interface { // Write writes to the path when complete. - Write(file string) + Write(file, content string) + // CreateDirWithSymlink creates directory and a symlink + CreateDirWithSymlink(source, link string) } // Go optionally waits for a file, runs the command, and writes a @@ -104,6 +120,10 @@ func (e Entrypointer) Go() error { _ = logger.Sync() }() + // Create the directory where we will store the exit codes (and eventually other metadata) of Steps. + // Create a symlink to the directory for easier access by the index instead of a step name. + e.PostWriter.CreateDirWithSymlink(e.StepMetadataDir, e.StepMetadataDirLink) + for _, f := range e.WaitFiles { if err := e.Waiter.Wait(f, e.WaitFileContent, e.BreakpointOnFailure); err != nil { // An error happened while waiting, so we bail @@ -153,9 +173,26 @@ func (e Entrypointer) Go() error { } } - if err != nil && e.BreakpointOnFailure { + var ee *exec.ExitError + switch { + case err != nil && e.BreakpointOnFailure: logger.Info("Skipping writing to PostFile") - } else { + case e.OnError == ContinueOnError && errors.As(err, &ee): + // with continue on error and an ExitError, write non-zero exit code and a post file + exitCode := strconv.Itoa(ee.ExitCode()) + output = append(output, v1beta1.PipelineResourceResult{ + Key: "ExitCode", + Value: exitCode, + ResultType: v1beta1.InternalTektonResultType, + }) + e.WritePostFile(e.PostFile, nil) + e.WriteExitCodeFile(e.StepMetadataDir, exitCode) + case err == nil: + // if err is nil, write zero exit code and a post file + e.WritePostFile(e.PostFile, nil) + e.WriteExitCodeFile(e.StepMetadataDirLink, "0") + default: + // for a step without continue on error and any error, write a post file with .err e.WritePostFile(e.PostFile, err) } @@ -215,6 +252,12 @@ func (e Entrypointer) WritePostFile(postFile string, err error) { postFile = fmt.Sprintf("%s.err", postFile) } if postFile != "" { - e.PostWriter.Write(postFile) + e.PostWriter.Write(postFile, "") } } + +// WriteExitCodeFile write the exitCodeFile +func (e Entrypointer) WriteExitCodeFile(stepPath, content string) { + exitCodeFile := filepath.Join(stepPath, "exitCode") + e.PostWriter.Write(exitCodeFile, content) +} diff --git a/pkg/entrypoint/entrypointer_test.go b/pkg/entrypoint/entrypointer_test.go index 988be23526a..6bca90389cc 100644 --- a/pkg/entrypoint/entrypointer_test.go +++ b/pkg/entrypoint/entrypointer_test.go @@ -23,6 +23,7 @@ import ( "fmt" "io/ioutil" "os" + "os/exec" "reflect" "testing" "time" @@ -119,9 +120,9 @@ func TestEntrypointerFailures(t *testing.T) { func TestEntrypointer(t *testing.T) { for _, c := range []struct { - desc, entrypoint, postFile string - waitFiles, args []string - breakpointOnFailure bool + desc, entrypoint, postFile, stepDir, stepDirLink string + waitFiles, args []string + breakpointOnFailure bool }{{ desc: "do nothing", }, { @@ -150,6 +151,11 @@ func TestEntrypointer(t *testing.T) { }, { desc: "breakpointOnFailure to wait or not to wait ", breakpointOnFailure: true, + }, { + desc: "create a step path", + entrypoint: "echo", + stepDir: "step-one", + stepDirLink: "0", }} { t.Run(c.desc, func(t *testing.T) { fw, fr, fpw := &fakeWaiter{}, &fakeRunner{}, &fakePostWriter{} @@ -165,6 +171,8 @@ func TestEntrypointer(t *testing.T) { TerminationPath: "termination", Timeout: &timeout, BreakpointOnFailure: c.breakpointOnFailure, + StepMetadataDir: c.stepDir, + StepMetadataDirLink: c.stepDirLink, }.Go() if err != nil { t.Fatalf("Entrypointer failed: %v", err) @@ -227,6 +235,18 @@ func TestEntrypointer(t *testing.T) { if err := os.Remove("termination"); err != nil { t.Errorf("Could not remove termination path: %s", err) } + + if c.stepDir != "" { + if c.stepDir != *fpw.source { + t.Error("Wanted step path created, got nil") + } + } + + if c.stepDirLink != "" { + if c.stepDirLink != *fpw.link { + t.Error("Wanted step path symbolic link created, got nil") + } + } }) } } @@ -250,6 +270,81 @@ func TestEntrypointer_ReadBreakpointExitCodeFromDisk(t *testing.T) { } } +func TestEntrypointer_OnError(t *testing.T) { + for _, c := range []struct { + desc, postFile, onError string + runner Runner + expectedError bool + }{{ + desc: "the step is exiting with 1, ignore the step error when onError is set to continue", + runner: &fakeExitErrorRunner{}, + postFile: "step-one", + onError: ContinueOnError, + expectedError: true, + }, { + desc: "the step is exiting with 0, ignore the step error irrespective of no error with onError set to continue", + runner: &fakeRunner{}, + postFile: "step-one", + onError: ContinueOnError, + expectedError: false, + }, { + desc: "the step is exiting with 1, treat the step error as failure with onError set to fail", + runner: &fakeExitErrorRunner{}, + expectedError: true, + postFile: "step-one", + onError: FailOnError, + }, { + desc: "the step is exiting with 0, treat the step error (but there is none) as failure with onError set to fail", + runner: &fakeRunner{}, + postFile: "step-one", + onError: FailOnError, + expectedError: false, + }} { + t.Run(c.desc, func(t *testing.T) { + fpw := &fakePostWriter{} + err := Entrypointer{ + Entrypoint: "echo", + WaitFiles: []string{}, + PostFile: c.postFile, + Args: []string{"some", "args"}, + Waiter: &fakeWaiter{}, + Runner: c.runner, + PostWriter: fpw, + TerminationPath: "termination", + OnError: c.onError, + }.Go() + + if c.expectedError && err == nil { + t.Fatalf("Entrypointer didn't fail") + } + + if c.onError == ContinueOnError { + switch { + case fpw.wrote == nil: + t.Error("Wanted post file written, got nil") + case fpw.exitCodeFile == nil: + t.Error("Wanted exitCode file written, got nil") + case *fpw.wrote != c.postFile: + t.Errorf("Wrote post file %q, want %q", *fpw.wrote, c.postFile) + case *fpw.exitCodeFile != "exitCode": + t.Errorf("Wrote exitCode file %q, want %q", *fpw.exitCodeFile, "exitCode") + case c.expectedError && *fpw.exitCode == "0": + t.Errorf("Wrote zero exit code but want non-zero when expecting an error") + } + } + + if c.onError == FailOnError { + switch { + case fpw.wrote == nil: + t.Error("Wanted post file written, got nil") + case c.expectedError && *fpw.wrote != c.postFile+".err": + t.Errorf("Wrote post file %q, want %q", *fpw.wrote, c.postFile+".err") + } + } + }) + } +} + type fakeWaiter struct{ waited []string } func (f *fakeWaiter) Wait(file string, _ bool, _ bool) error { @@ -264,9 +359,27 @@ func (f *fakeRunner) Run(ctx context.Context, args ...string) error { return nil } -type fakePostWriter struct{ wrote *string } +type fakePostWriter struct { + wrote *string + exitCodeFile *string + exitCode *string + source *string + link *string +} -func (f *fakePostWriter) Write(file string) { f.wrote = &file } +func (f *fakePostWriter) Write(file, content string) { + if content == "" { + f.wrote = &file + } else { + f.exitCodeFile = &file + f.exitCode = &content + } +} + +func (f *fakePostWriter) CreateDirWithSymlink(source, link string) { + f.source = &source + f.link = &link +} type fakeErrorWaiter struct{ waited *string } @@ -301,3 +414,10 @@ func (f *fakeTimeoutRunner) Run(ctx context.Context, args ...string) error { } return errors.New("runner failed") } + +type fakeExitErrorRunner struct{ args *[]string } + +func (f *fakeExitErrorRunner) Run(ctx context.Context, args ...string) error { + f.args = &args + return exec.Command("ls", "/bogus/path").Run() +} diff --git a/pkg/pod/entrypoint.go b/pkg/pod/entrypoint.go index fd1bfcd3943..091135aa979 100644 --- a/pkg/pod/entrypoint.go +++ b/pkg/pod/entrypoint.go @@ -25,6 +25,7 @@ import ( "path/filepath" "strings" + "github.com/tektoncd/pipeline/pkg/apis/pipeline" "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1beta1" "gomodules.xyz/jsonpatch/v2" corev1 "k8s.io/api/core/v1" @@ -113,6 +114,7 @@ func orderContainers(entrypointImage string, commonExtraEntrypointArgs []string, for i, s := range steps { var argsForEntrypoint []string + name := StepName(steps[i].Name, i) switch i { case 0: argsForEntrypoint = []string{ @@ -122,6 +124,8 @@ func orderContainers(entrypointImage string, commonExtraEntrypointArgs []string, // Start next step. "-post_file", filepath.Join(mountPoint, fmt.Sprintf("%d", i)), "-termination_path", terminationPath, + "-step_metadata_dir", filepath.Join(pipeline.StepsDir, name), + "-step_metadata_dir_link", filepath.Join(pipeline.StepsDir, fmt.Sprintf("%d", i)), } default: // All other steps wait for previous file, write next file. @@ -129,12 +133,19 @@ func orderContainers(entrypointImage string, commonExtraEntrypointArgs []string, "-wait_file", filepath.Join(mountPoint, fmt.Sprintf("%d", i-1)), "-post_file", filepath.Join(mountPoint, fmt.Sprintf("%d", i)), "-termination_path", terminationPath, + "-step_metadata_dir", filepath.Join(pipeline.StepsDir, name), + "-step_metadata_dir_link", filepath.Join(pipeline.StepsDir, fmt.Sprintf("%d", i)), } } argsForEntrypoint = append(argsForEntrypoint, commonExtraEntrypointArgs...) if taskSpec != nil { - if taskSpec.Steps != nil && len(taskSpec.Steps) >= i+1 && taskSpec.Steps[i].Timeout != nil { - argsForEntrypoint = append(argsForEntrypoint, "-timeout", taskSpec.Steps[i].Timeout.Duration.String()) + if taskSpec.Steps != nil && len(taskSpec.Steps) >= i+1 { + if taskSpec.Steps[i].Timeout != nil { + argsForEntrypoint = append(argsForEntrypoint, "-timeout", taskSpec.Steps[i].Timeout.Duration.String()) + } + if taskSpec.Steps[i].OnError != "" { + argsForEntrypoint = append(argsForEntrypoint, "-on_error", taskSpec.Steps[i].OnError) + } } argsForEntrypoint = append(argsForEntrypoint, resultArgument(steps, taskSpec.Results)...) } @@ -274,3 +285,12 @@ func trimStepPrefix(name string) string { return strings.TrimPrefix(name, stepPr // TrimSidecarPrefix returns the container name, stripped of its sidecar // prefix. func TrimSidecarPrefix(name string) string { return strings.TrimPrefix(name, sidecarPrefix) } + +// StepName returns the step name after adding "step-" prefix to the actual step name or +// returns "step-unnamed-" if not specified +func StepName(name string, i int) string { + if name != "" { + return fmt.Sprintf("%s%s", stepPrefix, name) + } + return fmt.Sprintf("%sunnamed-%d", stepPrefix, i) +} diff --git a/pkg/pod/entrypoint_test.go b/pkg/pod/entrypoint_test.go index 19c61d461c9..d6edc9b3dbc 100644 --- a/pkg/pod/entrypoint_test.go +++ b/pkg/pod/entrypoint_test.go @@ -23,6 +23,7 @@ import ( "github.com/google/go-cmp/cmp" "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1beta1" + "github.com/tektoncd/pipeline/pkg/entrypoint" "github.com/tektoncd/pipeline/test/diff" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -57,6 +58,8 @@ func TestOrderContainers(t *testing.T) { "-wait_file_content", "-post_file", "/tekton/tools/0", "-termination_path", "/tekton/termination", + "-step_metadata_dir", "/tekton/steps/step-unnamed-0", + "-step_metadata_dir_link", "/tekton/steps/0", "-entrypoint", "cmd", "--", "arg1", "arg2", }, @@ -69,6 +72,8 @@ func TestOrderContainers(t *testing.T) { "-wait_file", "/tekton/tools/0", "-post_file", "/tekton/tools/1", "-termination_path", "/tekton/termination", + "-step_metadata_dir", "/tekton/steps/step-unnamed-1", + "-step_metadata_dir_link", "/tekton/steps/1", "-entrypoint", "cmd1", "--", "cmd2", "cmd3", "arg1", "arg2", @@ -82,6 +87,8 @@ func TestOrderContainers(t *testing.T) { "-wait_file", "/tekton/tools/1", "-post_file", "/tekton/tools/2", "-termination_path", "/tekton/termination", + "-step_metadata_dir", "/tekton/steps/step-unnamed-2", + "-step_metadata_dir_link", "/tekton/steps/2", "-entrypoint", "cmd", "--", "arg1", "arg2", }, @@ -122,6 +129,8 @@ func TestOrderContainersWithDebugOnFailure(t *testing.T) { "-wait_file_content", "-post_file", "/tekton/tools/0", "-termination_path", "/tekton/termination", + "-step_metadata_dir", "/tekton/steps/step-unnamed-0", + "-step_metadata_dir_link", "/tekton/steps/0", "-breakpoint_on_failure", "-entrypoint", "cmd", "--", "arg1", "arg2", @@ -174,6 +183,8 @@ func TestEntryPointResults(t *testing.T) { "-wait_file_content", "-post_file", "/tekton/tools/0", "-termination_path", "/tekton/termination", + "-step_metadata_dir", "/tekton/steps/step-unnamed-0", + "-step_metadata_dir_link", "/tekton/steps/0", "-results", "sum,sub", "-entrypoint", "cmd", "--", "arg1", "arg2", @@ -187,6 +198,8 @@ func TestEntryPointResults(t *testing.T) { "-wait_file", "/tekton/tools/0", "-post_file", "/tekton/tools/1", "-termination_path", "/tekton/termination", + "-step_metadata_dir", "/tekton/steps/step-unnamed-1", + "-step_metadata_dir_link", "/tekton/steps/1", "-results", "sum,sub", "-entrypoint", "cmd1", "--", "cmd2", "cmd3", @@ -201,6 +214,8 @@ func TestEntryPointResults(t *testing.T) { "-wait_file", "/tekton/tools/1", "-post_file", "/tekton/tools/2", "-termination_path", "/tekton/termination", + "-step_metadata_dir", "/tekton/steps/step-unnamed-2", + "-step_metadata_dir_link", "/tekton/steps/2", "-results", "sum,sub", "-entrypoint", "cmd", "--", "arg1", "arg2", @@ -241,6 +256,8 @@ func TestEntryPointResultsSingleStep(t *testing.T) { "-wait_file_content", "-post_file", "/tekton/tools/0", "-termination_path", "/tekton/termination", + "-step_metadata_dir", "/tekton/steps/step-unnamed-0", + "-step_metadata_dir_link", "/tekton/steps/0", "-results", "sum,sub", "-entrypoint", "cmd", "--", "arg1", "arg2", @@ -277,6 +294,8 @@ func TestEntryPointSingleResultsSingleStep(t *testing.T) { "-wait_file_content", "-post_file", "/tekton/tools/0", "-termination_path", "/tekton/termination", + "-step_metadata_dir", "/tekton/steps/step-unnamed-0", + "-step_metadata_dir_link", "/tekton/steps/0", "-results", "sum", "-entrypoint", "cmd", "--", "arg1", "arg2", @@ -292,6 +311,67 @@ func TestEntryPointSingleResultsSingleStep(t *testing.T) { t.Errorf("Diff %s", diff.PrintWantGot(d)) } } + +func TestEntryPointOnError(t *testing.T) { + taskSpec := v1beta1.TaskSpec{ + Steps: []v1beta1.Step{{ + OnError: entrypoint.ContinueOnError, + }, { + OnError: entrypoint.FailOnError, + }}, + } + + steps := []corev1.Container{{ + Name: "failing-step", + Image: "step-1", + Command: []string{"cmd"}, + }, { + Name: "passing-step", + Image: "step-2", + Command: []string{"cmd"}, + }} + + want := []corev1.Container{{ + Name: "failing-step", + Image: "step-1", + Command: []string{entrypointBinary}, + Args: []string{ + "-wait_file", "/tekton/downward/ready", + "-wait_file_content", + "-post_file", "/tekton/tools/0", + "-termination_path", "/tekton/termination", + "-step_metadata_dir", "/tekton/steps/step-failing-step", + "-step_metadata_dir_link", "/tekton/steps/0", + "-on_error", "continue", + "-entrypoint", "cmd", "--", + }, + VolumeMounts: []corev1.VolumeMount{toolsMount, downwardMount}, + TerminationMessagePath: "/tekton/termination", + }, { + Name: "passing-step", + Image: "step-2", + Command: []string{entrypointBinary}, + Args: []string{ + "-wait_file", "/tekton/tools/0", + "-post_file", "/tekton/tools/1", + "-termination_path", "/tekton/termination", + "-step_metadata_dir", "/tekton/steps/step-passing-step", + "-step_metadata_dir_link", "/tekton/steps/1", + "-on_error", "fail", + "-entrypoint", "cmd", "--", + }, + VolumeMounts: []corev1.VolumeMount{toolsMount}, + TerminationMessagePath: "/tekton/termination", + }} + _, got, err := orderContainers(images.EntrypointImage, []string{}, steps, &taskSpec, nil) + if err != nil { + t.Fatalf("orderContainers: %v", err) + } + if d := cmp.Diff(want, got); d != "" { + t.Errorf("Diff %s", diff.PrintWantGot(d)) + } +} + func TestUpdateReady(t *testing.T) { for _, c := range []struct { desc string diff --git a/pkg/pod/pod.go b/pkg/pod/pod.go index cf38c10fa03..ca31ebef466 100644 --- a/pkg/pod/pod.go +++ b/pkg/pod/pod.go @@ -67,6 +67,9 @@ var ( }, { Name: "tekton-internal-results", MountPath: pipeline.DefaultResultPath, + }, { + Name: "tekton-internal-steps", + MountPath: pipeline.StepsDir, }} implicitVolumes = []corev1.Volume{{ Name: "tekton-internal-workspace", @@ -77,6 +80,9 @@ var ( }, { Name: "tekton-internal-results", VolumeSource: corev1.VolumeSource{EmptyDir: &corev1.EmptyDirVolumeSource{}}, + }, { + Name: "tekton-internal-steps", + VolumeSource: corev1.VolumeSource{EmptyDir: &corev1.EmptyDirVolumeSource{}}, }} ) @@ -238,11 +244,7 @@ func (b *Builder) Build(ctx context.Context, taskRun *v1beta1.TaskRun, taskSpec if s.WorkingDir == "" && shouldOverrideWorkingDir { stepContainers[i].WorkingDir = pipeline.WorkspaceDir } - if s.Name == "" { - stepContainers[i].Name = names.SimpleNameGenerator.RestrictLength(fmt.Sprintf("%sunnamed-%d", stepPrefix, i)) - } else { - stepContainers[i].Name = names.SimpleNameGenerator.RestrictLength(fmt.Sprintf("%s%s", stepPrefix, s.Name)) - } + stepContainers[i].Name = names.SimpleNameGenerator.RestrictLength(StepName(s.Name, i)) } // By default, use an empty pod template and take the one defined in the task run spec if any diff --git a/pkg/pod/pod_test.go b/pkg/pod/pod_test.go index 784a5c789a2..b4c43be3a4f 100644 --- a/pkg/pod/pod_test.go +++ b/pkg/pod/pod_test.go @@ -122,6 +122,10 @@ func TestPodBuild(t *testing.T) { "/tekton/tools/0", "-termination_path", "/tekton/termination", + "-step_metadata_dir", + "/tekton/steps/step-name", + "-step_metadata_dir_link", + "/tekton/steps/0", "-entrypoint", "cmd", "--", @@ -167,6 +171,10 @@ func TestPodBuild(t *testing.T) { "/tekton/tools/0", "-termination_path", "/tekton/termination", + "-step_metadata_dir", + "/tekton/steps/step-name", + "-step_metadata_dir_link", + "/tekton/steps/0", "-entrypoint", "cmd", "--", @@ -210,6 +218,10 @@ func TestPodBuild(t *testing.T) { "/tekton/tools/0", "-termination_path", "/tekton/termination", + "-step_metadata_dir", + "/tekton/steps/step-name", + "-step_metadata_dir_link", + "/tekton/steps/0", "-entrypoint", "cmd", "--", @@ -258,6 +270,10 @@ func TestPodBuild(t *testing.T) { "/tekton/tools/0", "-termination_path", "/tekton/termination", + "-step_metadata_dir", + "/tekton/steps/step-name", + "-step_metadata_dir_link", + "/tekton/steps/0", "-entrypoint", "cmd", "--", @@ -306,6 +322,10 @@ func TestPodBuild(t *testing.T) { "/tekton/tools/0", "-termination_path", "/tekton/termination", + "-step_metadata_dir", + "/tekton/steps/step-name", + "-step_metadata_dir_link", + "/tekton/steps/0", "-basic-docker=multi-creds=https://docker.io", "-basic-docker=multi-creds=https://us.gcr.io", "-basic-git=multi-creds=github.com", @@ -371,6 +391,10 @@ func TestPodBuild(t *testing.T) { "/tekton/tools/0", "-termination_path", "/tekton/termination", + "-step_metadata_dir", + "/tekton/steps/step-name", + "-step_metadata_dir_link", + "/tekton/steps/0", "-entrypoint", "cmd", "--", @@ -426,6 +450,10 @@ func TestPodBuild(t *testing.T) { "/tekton/tools/0", "-termination_path", "/tekton/termination", + "-step_metadata_dir", + "/tekton/steps/step-a-very-very-long-character-step-name-to-trigger-max-len----and-invalid-characters", + "-step_metadata_dir_link", + "/tekton/steps/0", "-entrypoint", "cmd", "--", @@ -466,6 +494,10 @@ func TestPodBuild(t *testing.T) { "/tekton/tools/0", "-termination_path", "/tekton/termination", + "-step_metadata_dir", + "/tekton/steps/step-ends-with-invalid-%%__$$", + "-step_metadata_dir_link", + "/tekton/steps/0", "-entrypoint", "cmd", "--", @@ -517,6 +549,10 @@ func TestPodBuild(t *testing.T) { "/tekton/tools/0", "-termination_path", "/tekton/termination", + "-step_metadata_dir", + "/tekton/steps/step-name", + "-step_metadata_dir_link", + "/tekton/steps/0", "-entrypoint", "cmd", "--", @@ -565,6 +601,10 @@ func TestPodBuild(t *testing.T) { "/tekton/tools/0", "-termination_path", "/tekton/termination", + "-step_metadata_dir", + "/tekton/steps/step-primary-name", + "-step_metadata_dir_link", + "/tekton/steps/0", "-entrypoint", "cmd", "--", @@ -634,6 +674,10 @@ _EOF_ "/tekton/tools/0", "-termination_path", "/tekton/termination", + "-step_metadata_dir", + "/tekton/steps/step-primary-name", + "-step_metadata_dir_link", + "/tekton/steps/0", "-entrypoint", "cmd", "--", @@ -692,6 +736,10 @@ _EOF_ "/tekton/tools/0", "-termination_path", "/tekton/termination", + "-step_metadata_dir", + "/tekton/steps/step-primary-name", + "-step_metadata_dir_link", + "/tekton/steps/0", "-entrypoint", "cmd", "--", @@ -752,6 +800,10 @@ _EOF_ "/tekton/tools/0", "-termination_path", "/tekton/termination", + "-step_metadata_dir", + "/tekton/steps/step-unnamed-0", + "-step_metadata_dir_link", + "/tekton/steps/0", "-entrypoint", "cmd", "--", @@ -779,6 +831,10 @@ _EOF_ "/tekton/tools/1", "-termination_path", "/tekton/termination", + "-step_metadata_dir", + "/tekton/steps/step-unnamed-1", + "-step_metadata_dir_link", + "/tekton/steps/1", "-entrypoint", "cmd", "--", @@ -869,6 +925,10 @@ _EOF_ "/tekton/tools/0", "-termination_path", "/tekton/termination", + "-step_metadata_dir", + "/tekton/steps/step-one", + "-step_metadata_dir_link", + "/tekton/steps/0", "-entrypoint", "/tekton/scripts/script-0-9l9zj", "--", @@ -893,6 +953,10 @@ _EOF_ "/tekton/tools/1", "-termination_path", "/tekton/termination", + "-step_metadata_dir", + "/tekton/steps/step-two", + "-step_metadata_dir_link", + "/tekton/steps/1", "-entrypoint", "/tekton/scripts/script-1-mz4c7", "--", @@ -917,6 +981,10 @@ _EOF_ "/tekton/tools/2", "-termination_path", "/tekton/termination", + "-step_metadata_dir", + "/tekton/steps/step-regular-step", + "-step_metadata_dir_link", + "/tekton/steps/2", "-entrypoint", "regular", "--", @@ -981,6 +1049,10 @@ _EOF_ "/tekton/tools/0", "-termination_path", "/tekton/termination", + "-step_metadata_dir", + "/tekton/steps/step-one", + "-step_metadata_dir_link", + "/tekton/steps/0", "-entrypoint", "/tekton/scripts/script-0-9l9zj", "--", @@ -1035,6 +1107,10 @@ _EOF_ "/tekton/tools/0", "-termination_path", "/tekton/termination", + "-step_metadata_dir", + "/tekton/steps/step-schedule-me", + "-step_metadata_dir_link", + "/tekton/steps/0", "-entrypoint", "cmd", "--", @@ -1084,6 +1160,10 @@ _EOF_ "/tekton/tools/0", "-termination_path", "/tekton/termination", + "-step_metadata_dir", + "/tekton/steps/step-image-pull", + "-step_metadata_dir_link", + "/tekton/steps/0", "-entrypoint", "cmd", "--", @@ -1135,6 +1215,10 @@ _EOF_ "/tekton/tools/0", "-termination_path", "/tekton/termination", + "-step_metadata_dir", + "/tekton/steps/step-host-aliases", + "-step_metadata_dir_link", + "/tekton/steps/0", "-entrypoint", "cmd", "--", @@ -1185,6 +1269,10 @@ _EOF_ "/tekton/tools/0", "-termination_path", "/tekton/termination", + "-step_metadata_dir", + "/tekton/steps/step-use-my-hostNetwork", + "-step_metadata_dir_link", + "/tekton/steps/0", "-entrypoint", "cmd", "--", @@ -1249,6 +1337,10 @@ _EOF_ "/tekton/tools/0", "-termination_path", "/tekton/termination", + "-step_metadata_dir", + "/tekton/steps/step-name", + "-step_metadata_dir_link", + "/tekton/steps/0", "-entrypoint", "cmd", "--", @@ -1287,6 +1379,10 @@ _EOF_ "/tekton/tools/0", "-termination_path", "/tekton/termination", + "-step_metadata_dir", + "/tekton/steps/step-name", + "-step_metadata_dir_link", + "/tekton/steps/0", "-timeout", "1s", "-entrypoint", @@ -1332,6 +1428,10 @@ _EOF_ "/tekton/tools/0", "-termination_path", "/tekton/termination", + "-step_metadata_dir", + "/tekton/steps/step-name", + "-step_metadata_dir_link", + "/tekton/steps/0", "-entrypoint", "cmd", "--", @@ -1370,6 +1470,10 @@ _EOF_ "/tekton/tools/0", "-termination_path", "/tekton/termination", + "-step_metadata_dir", + "/tekton/steps/step-name", + "-step_metadata_dir_link", + "/tekton/steps/0", "-entrypoint", "cmd", "--", @@ -1418,6 +1522,10 @@ _EOF_ "/tekton/tools/0", "-termination_path", "/tekton/termination", + "-step_metadata_dir", + "/tekton/steps/step-name", + "-step_metadata_dir_link", + "/tekton/steps/0", "-entrypoint", "cmd", "--", @@ -1576,6 +1684,10 @@ func TestPodBuildwithAlphaAPIEnabled(t *testing.T) { "/tekton/tools/0", "-termination_path", "/tekton/termination", + "-step_metadata_dir", + "/tekton/steps/step-name", + "-step_metadata_dir_link", + "/tekton/steps/0", "-breakpoint_on_failure", "-entrypoint", "cmd", diff --git a/pkg/pod/status.go b/pkg/pod/status.go index 2228ec7bb8c..9e2d9c7d727 100644 --- a/pkg/pod/status.go +++ b/pkg/pod/status.go @@ -19,6 +19,7 @@ package pod import ( "encoding/json" "fmt" + "strconv" "strings" "time" @@ -157,6 +158,11 @@ func setTaskRunStatusBasedOnStepStatus(logger *zap.SugaredLogger, stepStatuses [ logger.Errorf("error setting the start time of step %q in taskrun %q: %v", s.Name, tr.Name, err) merr = multierror.Append(merr, err) } + exitCode, err := extractExitCodeFromResults(results) + if err != nil { + logger.Errorf("error extracting the exit code of step %q in taskrun %q: %v", s.Name, tr.Name, err) + merr = multierror.Append(merr, err) + } taskResults, pipelineResourceResults, filteredResults := filterResultsAndResources(results) if tr.IsSuccessful() { trs.TaskRunResults = append(trs.TaskRunResults, taskResults...) @@ -172,6 +178,9 @@ func setTaskRunStatusBasedOnStepStatus(logger *zap.SugaredLogger, stepStatuses [ if time != nil { s.State.Terminated.StartedAt = *time } + if exitCode != nil { + s.State.Terminated.ExitCode = *exitCode + } } } trs.Steps = append(trs.Steps, v1beta1.StepState{ @@ -268,6 +277,21 @@ func extractStartedAtTimeFromResults(results []v1beta1.PipelineResourceResult) ( return nil, nil } +func extractExitCodeFromResults(results []v1beta1.PipelineResourceResult) (*int32, error) { + for _, result := range results { + if result.Key == "ExitCode" { + // We could just pass the string through but this provides extra validation + i, err := strconv.ParseUint(result.Value, 10, 32) + if err != nil { + return nil, fmt.Errorf("could not parse int value %q in ExitCode field: %w", result.Value, err) + } + exitCode := int32(i) + return &exitCode, nil + } + } + return nil, nil +} + func updateCompletedTaskRunStatus(logger *zap.SugaredLogger, trs *v1beta1.TaskRunStatus, pod *corev1.Pod) { if DidTaskRunFail(pod) { msg := getFailureMessage(logger, pod) diff --git a/pkg/pod/status_test.go b/pkg/pod/status_test.go index e08e169bc38..746adff3618 100644 --- a/pkg/pod/status_test.go +++ b/pkg/pod/status_test.go @@ -841,6 +841,60 @@ func TestMakeTaskRunStatus(t *testing.T) { CompletionTime: &metav1.Time{Time: time.Now()}, }, }, + }, { + desc: "include non zero exit code in a container termination message if entrypoint is set to ignore the error", + pod: corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "pod", + }, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{{ + Name: "step-first", + }, { + Name: "step-second", + }}, + }, + Status: corev1.PodStatus{ + Phase: corev1.PodSucceeded, + ContainerStatuses: []corev1.ContainerStatus{{ + Name: "step-first", + State: corev1.ContainerState{ + Terminated: &corev1.ContainerStateTerminated{ + Message: `[{"key":"ExitCode","value":"11","type":"InternalTektonResult"}]`, + }, + }, + }, { + Name: "step-second", + State: corev1.ContainerState{ + Terminated: &corev1.ContainerStateTerminated{}, + }, + }}, + }, + }, + want: v1beta1.TaskRunStatus{ + Status: statusSuccess(), + TaskRunStatusFields: v1beta1.TaskRunStatusFields{ + Steps: []v1beta1.StepState{{ + ContainerState: corev1.ContainerState{ + Terminated: &corev1.ContainerStateTerminated{ + ExitCode: 11, + }, + }, + Name: "first", + ContainerName: "step-first", + }, { + ContainerState: corev1.ContainerState{ + Terminated: &corev1.ContainerStateTerminated{ + ExitCode: 0, + }}, + Name: "second", + ContainerName: "step-second", + }}, + Sidecars: []v1beta1.SidecarState{}, + // We don't actually care about the time, just that it's not nil + CompletionTime: &metav1.Time{Time: time.Now()}, + }, + }, }} { t.Run(c.desc, func(t *testing.T) { now := metav1.Now() diff --git a/pkg/reconciler/taskrun/resources/apply.go b/pkg/reconciler/taskrun/resources/apply.go index aaf503e7a4a..b9671fa71e0 100644 --- a/pkg/reconciler/taskrun/resources/apply.go +++ b/pkg/reconciler/taskrun/resources/apply.go @@ -28,6 +28,7 @@ import ( "github.com/tektoncd/pipeline/pkg/apis/config" "github.com/tektoncd/pipeline/pkg/apis/pipeline" "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1beta1" + "github.com/tektoncd/pipeline/pkg/pod" "github.com/tektoncd/pipeline/pkg/substitution" ) @@ -198,6 +199,18 @@ func ApplyTaskResults(spec *v1beta1.TaskSpec) *v1beta1.TaskSpec { return ApplyReplacements(spec, stringReplacements, map[string][]string{}) } +// ApplyStepExitCodePath replaces the occurrences of exitCode path with the absolute tekton internal path +// Replace $(steps..exitCode.path) with pipeline.StepPath//exitCode +func ApplyStepExitCodePath(spec *v1beta1.TaskSpec) *v1beta1.TaskSpec { + stringReplacements := map[string]string{} + + for i, step := range spec.Steps { + stringReplacements[fmt.Sprintf("steps.%s.exitCode.path", pod.StepName(step.Name, i))] = + filepath.Join(pipeline.StepsDir, pod.StepName(step.Name, i), "exitCode") + } + return ApplyReplacements(spec, stringReplacements, map[string][]string{}) +} + // ApplyCredentialsPath applies a substitution of the key $(credentials.path) with the path that credentials // from annotated secrets are written to. func ApplyCredentialsPath(spec *v1beta1.TaskSpec, path string) *v1beta1.TaskSpec { diff --git a/pkg/reconciler/taskrun/resources/apply_test.go b/pkg/reconciler/taskrun/resources/apply_test.go index 97aa4cfd425..d55f298512c 100644 --- a/pkg/reconciler/taskrun/resources/apply_test.go +++ b/pkg/reconciler/taskrun/resources/apply_test.go @@ -1177,6 +1177,38 @@ func TestTaskResults(t *testing.T) { } } +func TestApplyStepExitCodePath(t *testing.T) { + names.TestingSeed() + ts := &v1beta1.TaskSpec{ + Steps: []v1beta1.Step{{ + Container: corev1.Container{ + Image: "bash:latest", + }, + Script: "#!/usr/bin/env bash\nexit 11", + }, { + Container: corev1.Container{ + Name: "failing-step", + Image: "bash:latest", + }, + Script: "#!/usr/bin/env bash\ncat $(steps.step-unnamed-0.exitCode.path)", + }, { + Container: corev1.Container{ + Name: "check-failing-step", + Image: "bash:latest", + }, + Script: "#!/usr/bin/env bash\ncat $(steps.step-failing-step.exitCode.path)", + }}, + } + expected := applyMutation(ts, func(spec *v1beta1.TaskSpec) { + spec.Steps[1].Script = "#!/usr/bin/env bash\ncat /tekton/steps/step-unnamed-0/exitCode" + spec.Steps[2].Script = "#!/usr/bin/env bash\ncat /tekton/steps/step-failing-step/exitCode" + }) + got := resources.ApplyStepExitCodePath(ts) + if d := cmp.Diff(expected, got); d != "" { + t.Errorf("ApplyStepExitCodePath() got diff %s", diff.PrintWantGot(d)) + } +} + func TestApplyCredentialsPath(t *testing.T) { for _, tc := range []struct { description string diff --git a/pkg/reconciler/taskrun/taskrun.go b/pkg/reconciler/taskrun/taskrun.go index 23e558ded64..f08a7d2a8d1 100644 --- a/pkg/reconciler/taskrun/taskrun.go +++ b/pkg/reconciler/taskrun/taskrun.go @@ -655,6 +655,9 @@ func (c *Reconciler) createPod(ctx context.Context, tr *v1beta1.TaskRun, rtr *re // Apply task result substitution ts = resources.ApplyTaskResults(ts) + // Apply step exitCode path substitution + ts = resources.ApplyStepExitCodePath(ts) + if validateErr := ts.Validate(ctx); validateErr != nil { logger.Errorf("Failed to create a pod for taskrun: %s due to task validation error %v", tr.Name, validateErr) return nil, validateErr diff --git a/pkg/reconciler/taskrun/taskrun_test.go b/pkg/reconciler/taskrun/taskrun_test.go index 05fbc2e9c3c..2f4d4d1b8d9 100644 --- a/pkg/reconciler/taskrun/taskrun_test.go +++ b/pkg/reconciler/taskrun/taskrun_test.go @@ -118,6 +118,17 @@ var ( tb.StepCommand("/mycmd"), ), ), tb.TaskNamespace("foo")) + + taskMultipleStepsIgnoreError = tb.Task("test-task-multi-steps-with-ignore-error", tb.TaskSpec( + tb.Step("foo", tb.StepName("step-0"), + tb.StepCommand("/mycmd"), + tb.StepOnError("continue"), + ), + tb.Step("foo", tb.StepName("step-1"), + tb.StepCommand("/mycmd"), + ), + ), tb.TaskNamespace("foo")) + clustertask = tb.ClusterTask("test-cluster-task", tb.ClusterTaskSpec(simpleStep)) taskSidecar = tb.Task("test-task-sidecar", tb.TaskSpec( tb.Sidecar("sidecar", "image-id"), @@ -226,6 +237,12 @@ var ( }, }, } + stepsVolume = corev1.Volume{ + Name: "tekton-internal-steps", + VolumeSource: corev1.VolumeSource{ + EmptyDir: &corev1.EmptyDirVolumeSource{}, + }, + } getPlaceToolsInitContainer = func(ops ...tb.ContainerOp) tb.PodSpecOp { actualOps := []tb.ContainerOp{ @@ -455,7 +472,7 @@ func TestReconcile_ExplicitDefaultSA(t *testing.T) { tb.OwnerReferenceAPIVersion(currentAPIVersion)), tb.PodSpec( tb.PodServiceAccountName(defaultSAName), - tb.PodVolumes(workspaceVolume, homeVolume, resultsVolume, toolsVolume, downwardVolume, corev1.Volume{ + tb.PodVolumes(workspaceVolume, homeVolume, resultsVolume, stepsVolume, toolsVolume, downwardVolume, corev1.Volume{ Name: "tekton-creds-init-home-0", VolumeSource: corev1.VolumeSource{EmptyDir: &corev1.EmptyDirVolumeSource{Medium: corev1.StorageMediumMemory}}, }), @@ -470,6 +487,10 @@ func TestReconcile_ExplicitDefaultSA(t *testing.T) { "/tekton/tools/0", "-termination_path", "/tekton/termination", + "-step_metadata_dir", + "/tekton/steps/step-simple-step", + "-step_metadata_dir_link", + "/tekton/steps/0", "-entrypoint", "/mycmd", "--", @@ -480,6 +501,7 @@ func TestReconcile_ExplicitDefaultSA(t *testing.T) { tb.VolumeMount("tekton-internal-workspace", workspaceDir), tb.VolumeMount("tekton-internal-home", "/tekton/home"), tb.VolumeMount("tekton-internal-results", "/tekton/results"), + tb.VolumeMount("tekton-internal-steps", "/tekton/steps"), tb.TerminationMessagePath("/tekton/termination"), ), ), @@ -497,7 +519,7 @@ func TestReconcile_ExplicitDefaultSA(t *testing.T) { tb.OwnerReferenceAPIVersion(currentAPIVersion)), tb.PodSpec( tb.PodServiceAccountName("test-sa"), - tb.PodVolumes(workspaceVolume, homeVolume, resultsVolume, toolsVolume, downwardVolume, corev1.Volume{ + tb.PodVolumes(workspaceVolume, homeVolume, resultsVolume, stepsVolume, toolsVolume, downwardVolume, corev1.Volume{ Name: "tekton-creds-init-home-0", VolumeSource: corev1.VolumeSource{EmptyDir: &corev1.EmptyDirVolumeSource{Medium: corev1.StorageMediumMemory}}, }), @@ -512,6 +534,10 @@ func TestReconcile_ExplicitDefaultSA(t *testing.T) { "/tekton/tools/0", "-termination_path", "/tekton/termination", + "-step_metadata_dir", + "/tekton/steps/step-sa-step", + "-step_metadata_dir_link", + "/tekton/steps/0", "-entrypoint", "/mycmd", "--", @@ -522,6 +548,7 @@ func TestReconcile_ExplicitDefaultSA(t *testing.T) { tb.VolumeMount("tekton-internal-workspace", workspaceDir), tb.VolumeMount("tekton-internal-home", "/tekton/home"), tb.VolumeMount("tekton-internal-results", "/tekton/results"), + tb.VolumeMount("tekton-internal-steps", "/tekton/steps"), tb.TerminationMessagePath("/tekton/termination"), ), ), @@ -627,7 +654,7 @@ func TestReconcile_FeatureFlags(t *testing.T) { tb.OwnerReferenceAPIVersion(currentAPIVersion)), tb.PodSpec( tb.PodServiceAccountName(config.DefaultServiceAccountValue), - tb.PodVolumes(workspaceVolume, homeVolume, resultsVolume, toolsVolume, downwardVolume, corev1.Volume{ + tb.PodVolumes(workspaceVolume, homeVolume, resultsVolume, stepsVolume, toolsVolume, downwardVolume, corev1.Volume{ Name: "tekton-creds-init-home-0", VolumeSource: corev1.VolumeSource{EmptyDir: &corev1.EmptyDirVolumeSource{Medium: corev1.StorageMediumMemory}}, }), @@ -642,6 +669,10 @@ func TestReconcile_FeatureFlags(t *testing.T) { "/tekton/tools/0", "-termination_path", "/tekton/termination", + "-step_metadata_dir", + "/tekton/steps/step-simple-step", + "-step_metadata_dir_link", + "/tekton/steps/0", "-entrypoint", "/mycmd", "--", @@ -653,6 +684,7 @@ func TestReconcile_FeatureFlags(t *testing.T) { tb.VolumeMount("tekton-internal-workspace", workspaceDir), tb.VolumeMount("tekton-internal-home", "/tekton/home"), tb.VolumeMount("tekton-internal-results", "/tekton/results"), + tb.VolumeMount("tekton-internal-steps", "/tekton/steps"), tb.TerminationMessagePath("/tekton/termination"), ), ), @@ -671,7 +703,7 @@ func TestReconcile_FeatureFlags(t *testing.T) { tb.OwnerReferenceAPIVersion(currentAPIVersion)), tb.PodSpec( tb.PodServiceAccountName(config.DefaultServiceAccountValue), - tb.PodVolumes(workspaceVolume, homeVolume, resultsVolume, toolsVolume, downwardVolume, corev1.Volume{ + tb.PodVolumes(workspaceVolume, homeVolume, resultsVolume, stepsVolume, toolsVolume, downwardVolume, corev1.Volume{ Name: "tekton-creds-init-home-0", VolumeSource: corev1.VolumeSource{EmptyDir: &corev1.EmptyDirVolumeSource{Medium: corev1.StorageMediumMemory}}, }), @@ -686,6 +718,10 @@ func TestReconcile_FeatureFlags(t *testing.T) { "/tekton/tools/0", "-termination_path", "/tekton/termination", + "-step_metadata_dir", + "/tekton/steps/step-simple-step", + "-step_metadata_dir_link", + "/tekton/steps/0", "-entrypoint", "/mycmd", "--", @@ -696,6 +732,7 @@ func TestReconcile_FeatureFlags(t *testing.T) { tb.VolumeMount("tekton-internal-workspace", workspaceDir), tb.VolumeMount("tekton-internal-home", "/tekton/home"), tb.VolumeMount("tekton-internal-results", "/tekton/results"), + tb.VolumeMount("tekton-internal-steps", "/tekton/steps"), tb.TerminationMessagePath("/tekton/termination"), ), ), @@ -1015,7 +1052,7 @@ func TestReconcile(t *testing.T) { tb.OwnerReferenceAPIVersion(currentAPIVersion)), tb.PodSpec( tb.PodServiceAccountName(config.DefaultServiceAccountValue), - tb.PodVolumes(workspaceVolume, homeVolume, resultsVolume, toolsVolume, downwardVolume, corev1.Volume{ + tb.PodVolumes(workspaceVolume, homeVolume, resultsVolume, stepsVolume, toolsVolume, downwardVolume, corev1.Volume{ Name: "tekton-creds-init-home-0", VolumeSource: corev1.VolumeSource{EmptyDir: &corev1.EmptyDirVolumeSource{Medium: corev1.StorageMediumMemory}}, }), @@ -1030,6 +1067,10 @@ func TestReconcile(t *testing.T) { "/tekton/tools/0", "-termination_path", "/tekton/termination", + "-step_metadata_dir", + "/tekton/steps/step-simple-step", + "-step_metadata_dir_link", + "/tekton/steps/0", "-entrypoint", "/mycmd", "--", @@ -1040,6 +1081,7 @@ func TestReconcile(t *testing.T) { tb.VolumeMount("tekton-internal-workspace", workspaceDir), tb.VolumeMount("tekton-internal-home", "/tekton/home"), tb.VolumeMount("tekton-internal-results", "/tekton/results"), + tb.VolumeMount("tekton-internal-steps", "/tekton/steps"), tb.TerminationMessagePath("/tekton/termination"), ), ), @@ -1061,7 +1103,7 @@ func TestReconcile(t *testing.T) { tb.OwnerReferenceAPIVersion(currentAPIVersion)), tb.PodSpec( tb.PodServiceAccountName("test-sa"), - tb.PodVolumes(workspaceVolume, homeVolume, resultsVolume, toolsVolume, downwardVolume, corev1.Volume{ + tb.PodVolumes(workspaceVolume, homeVolume, resultsVolume, stepsVolume, toolsVolume, downwardVolume, corev1.Volume{ Name: "tekton-creds-init-home-0", VolumeSource: corev1.VolumeSource{EmptyDir: &corev1.EmptyDirVolumeSource{Medium: corev1.StorageMediumMemory}}, }), @@ -1076,6 +1118,10 @@ func TestReconcile(t *testing.T) { "/tekton/tools/0", "-termination_path", "/tekton/termination", + "-step_metadata_dir", + "/tekton/steps/step-sa-step", + "-step_metadata_dir_link", + "/tekton/steps/0", "-entrypoint", "/mycmd", "--", @@ -1086,6 +1132,7 @@ func TestReconcile(t *testing.T) { tb.VolumeMount("tekton-internal-workspace", workspaceDir), tb.VolumeMount("tekton-internal-home", "/tekton/home"), tb.VolumeMount("tekton-internal-results", "/tekton/results"), + tb.VolumeMount("tekton-internal-steps", "/tekton/steps"), tb.TerminationMessagePath("/tekton/termination"), ), ), @@ -1108,7 +1155,7 @@ func TestReconcile(t *testing.T) { tb.PodSpec( tb.PodServiceAccountName(config.DefaultServiceAccountValue), tb.PodVolumes( - workspaceVolume, homeVolume, resultsVolume, toolsVolume, downwardVolume, corev1.Volume{ + workspaceVolume, homeVolume, resultsVolume, stepsVolume, toolsVolume, downwardVolume, corev1.Volume{ Name: "tekton-creds-init-home-0", VolumeSource: corev1.VolumeSource{EmptyDir: &corev1.EmptyDirVolumeSource{Medium: corev1.StorageMediumMemory}}, }, @@ -1150,6 +1197,10 @@ func TestReconcile(t *testing.T) { "/tekton/tools/0", "-termination_path", "/tekton/termination", + "-step_metadata_dir", + "/tekton/steps/step-create-dir-myimage-mssqb", + "-step_metadata_dir_link", + "/tekton/steps/0", "-entrypoint", "mkdir", "--", @@ -1161,12 +1212,25 @@ func TestReconcile(t *testing.T) { tb.VolumeMount("tekton-internal-workspace", workspaceDir), tb.VolumeMount("tekton-internal-home", "/tekton/home"), tb.VolumeMount("tekton-internal-results", "/tekton/results"), + tb.VolumeMount("tekton-internal-steps", "/tekton/steps"), tb.TerminationMessagePath("/tekton/termination"), ), tb.PodContainer("step-git-source-workspace-mz4c7", "override-with-git:latest", tb.Command(entrypointLocation), - tb.Args("-wait_file", "/tekton/tools/0", "-post_file", "/tekton/tools/1", "-termination_path", - "/tekton/termination", "-entrypoint", "/ko-app/git-init", "--", "-url", "https://foo.git", + tb.Args("-wait_file", + "/tekton/tools/0", + "-post_file", + "/tekton/tools/1", + "-termination_path", + "/tekton/termination", + "-step_metadata_dir", + "/tekton/steps/step-git-source-workspace-mz4c7", + "-step_metadata_dir_link", + "/tekton/steps/1", + "-entrypoint", + "/ko-app/git-init", + "--", + "-url", "https://foo.git", "-path", "/workspace/workspace"), tb.EnvVar("TEKTON_RESOURCE_NAME", "workspace"), tb.EnvVar("HOME", "/tekton/home"), @@ -1176,42 +1240,83 @@ func TestReconcile(t *testing.T) { tb.VolumeMount("tekton-internal-workspace", workspaceDir), tb.VolumeMount("tekton-internal-home", "/tekton/home"), tb.VolumeMount("tekton-internal-results", "/tekton/results"), + tb.VolumeMount("tekton-internal-steps", "/tekton/steps"), tb.TerminationMessagePath("/tekton/termination"), ), tb.PodContainer("step-mycontainer", "myimage", tb.Command(entrypointLocation), - tb.Args("-wait_file", "/tekton/tools/1", "-post_file", "/tekton/tools/2", "-termination_path", - "/tekton/termination", "-entrypoint", "/mycmd", "--", "--my-arg=foo", "--my-arg-with-default=bar", - "--my-arg-with-default2=thedefault", "--my-additional-arg=gcr.io/kristoff/sven", "--my-taskname-arg=test-task-with-substitution", + tb.Args("-wait_file", + "/tekton/tools/1", + "-post_file", + "/tekton/tools/2", + "-termination_path", + "/tekton/termination", + "-step_metadata_dir", + "/tekton/steps/step-mycontainer", + "-step_metadata_dir_link", + "/tekton/steps/2", + "-entrypoint", + "/mycmd", + "--", + "--my-arg=foo", + "--my-arg-with-default=bar", + "--my-arg-with-default2=thedefault", + "--my-additional-arg=gcr.io/kristoff/sven", + "--my-taskname-arg=test-task-with-substitution", "--my-taskrun-arg=test-taskrun-substitution"), tb.VolumeMount("tekton-internal-tools", "/tekton/tools"), tb.VolumeMount("tekton-creds-init-home-2", "/tekton/creds"), tb.VolumeMount("tekton-internal-workspace", workspaceDir), tb.VolumeMount("tekton-internal-home", "/tekton/home"), tb.VolumeMount("tekton-internal-results", "/tekton/results"), + tb.VolumeMount("tekton-internal-steps", "/tekton/steps"), tb.TerminationMessagePath("/tekton/termination"), ), tb.PodContainer("step-myothercontainer", "myotherimage", tb.Command(entrypointLocation), - tb.Args("-wait_file", "/tekton/tools/2", "-post_file", "/tekton/tools/3", "-termination_path", - "/tekton/termination", "-entrypoint", "/mycmd", "--", "--my-other-arg=https://foo.git"), + tb.Args("-wait_file", + "/tekton/tools/2", + "-post_file", + "/tekton/tools/3", + "-termination_path", + "/tekton/termination", + "-step_metadata_dir", + "/tekton/steps/step-myothercontainer", + "-step_metadata_dir_link", + "/tekton/steps/3", + "-entrypoint", + "/mycmd", + "--", + "--my-other-arg=https://foo.git"), tb.VolumeMount("tekton-internal-tools", "/tekton/tools"), tb.VolumeMount("tekton-creds-init-home-3", "/tekton/creds"), tb.VolumeMount("tekton-internal-workspace", workspaceDir), tb.VolumeMount("tekton-internal-home", "/tekton/home"), tb.VolumeMount("tekton-internal-results", "/tekton/results"), + tb.VolumeMount("tekton-internal-steps", "/tekton/steps"), tb.TerminationMessagePath("/tekton/termination"), ), tb.PodContainer("step-image-digest-exporter-9l9zj", "override-with-imagedigest-exporter-image:latest", tb.Command(entrypointLocation), - tb.Args("-wait_file", "/tekton/tools/3", "-post_file", "/tekton/tools/4", "-termination_path", - "/tekton/termination", "-entrypoint", "/ko-app/imagedigestexporter", "--", + tb.Args("-wait_file", + "/tekton/tools/3", + "-post_file", + "/tekton/tools/4", + "-termination_path", + "/tekton/termination", + "-step_metadata_dir", + "/tekton/steps/step-image-digest-exporter-9l9zj", + "-step_metadata_dir_link", + "/tekton/steps/4", + "-entrypoint", + "/ko-app/imagedigestexporter", "--", "-images", "[{\"name\":\"myimage\",\"type\":\"image\",\"url\":\"gcr.io/kristoff/sven\",\"digest\":\"\",\"OutputImageDir\":\"/workspace/output/myimage\"}]"), tb.VolumeMount("tekton-internal-tools", "/tekton/tools"), tb.VolumeMount("tekton-creds-init-home-4", "/tekton/creds"), tb.VolumeMount("tekton-internal-workspace", workspaceDir), tb.VolumeMount("tekton-internal-home", "/tekton/home"), tb.VolumeMount("tekton-internal-results", "/tekton/results"), + tb.VolumeMount("tekton-internal-steps", "/tekton/steps"), tb.TerminationMessagePath("/tekton/termination"), ), ), @@ -1232,7 +1337,7 @@ func TestReconcile(t *testing.T) { tb.OwnerReferenceAPIVersion(currentAPIVersion)), tb.PodSpec( tb.PodServiceAccountName(config.DefaultServiceAccountValue), - tb.PodVolumes(workspaceVolume, homeVolume, resultsVolume, toolsVolume, downwardVolume, corev1.Volume{ + tb.PodVolumes(workspaceVolume, homeVolume, resultsVolume, stepsVolume, toolsVolume, downwardVolume, corev1.Volume{ Name: "tekton-creds-init-home-0", VolumeSource: corev1.VolumeSource{EmptyDir: &corev1.EmptyDirVolumeSource{Medium: corev1.StorageMediumMemory}}, }, corev1.Volume{ @@ -1250,6 +1355,10 @@ func TestReconcile(t *testing.T) { "/tekton/tools/0", "-termination_path", "/tekton/termination", + "-step_metadata_dir", + "/tekton/steps/step-git-source-workspace-9l9zj", + "-step_metadata_dir_link", + "/tekton/steps/0", "-entrypoint", "/ko-app/git-init", "--", @@ -1267,17 +1376,30 @@ func TestReconcile(t *testing.T) { tb.VolumeMount("tekton-internal-workspace", workspaceDir), tb.VolumeMount("tekton-internal-home", "/tekton/home"), tb.VolumeMount("tekton-internal-results", "/tekton/results"), + tb.VolumeMount("tekton-internal-steps", "/tekton/steps"), tb.TerminationMessagePath("/tekton/termination"), ), tb.PodContainer("step-mycontainer", "myimage", tb.Command(entrypointLocation), - tb.Args("-wait_file", "/tekton/tools/0", "-post_file", "/tekton/tools/1", "-termination_path", - "/tekton/termination", "-entrypoint", "/mycmd", "--", "--my-arg=foo"), + tb.Args("-wait_file", + "/tekton/tools/0", + "-post_file", + "/tekton/tools/1", + "-termination_path", + "/tekton/termination", + "-step_metadata_dir", + "/tekton/steps/step-mycontainer", + "-step_metadata_dir_link", + "/tekton/steps/1", + "-entrypoint", + "/mycmd", + "--", "--my-arg=foo"), tb.VolumeMount("tekton-internal-tools", "/tekton/tools"), tb.VolumeMount("tekton-creds-init-home-1", "/tekton/creds"), tb.VolumeMount("tekton-internal-workspace", workspaceDir), tb.VolumeMount("tekton-internal-home", "/tekton/home"), tb.VolumeMount("tekton-internal-results", "/tekton/results"), + tb.VolumeMount("tekton-internal-steps", "/tekton/steps"), tb.TerminationMessagePath("/tekton/termination"), ), ), @@ -1299,7 +1421,7 @@ func TestReconcile(t *testing.T) { tb.OwnerReferenceAPIVersion(currentAPIVersion)), tb.PodSpec( tb.PodServiceAccountName(config.DefaultServiceAccountValue), - tb.PodVolumes(workspaceVolume, homeVolume, resultsVolume, toolsVolume, downwardVolume, corev1.Volume{ + tb.PodVolumes(workspaceVolume, homeVolume, resultsVolume, stepsVolume, toolsVolume, downwardVolume, corev1.Volume{ Name: "tekton-creds-init-home-0", VolumeSource: corev1.VolumeSource{EmptyDir: &corev1.EmptyDirVolumeSource{Medium: corev1.StorageMediumMemory}}, }), @@ -1314,6 +1436,10 @@ func TestReconcile(t *testing.T) { "/tekton/tools/0", "-termination_path", "/tekton/termination", + "-step_metadata_dir", + "/tekton/steps/step-simple-step", + "-step_metadata_dir_link", + "/tekton/steps/0", "-entrypoint", "/mycmd", "--", @@ -1324,6 +1450,7 @@ func TestReconcile(t *testing.T) { tb.VolumeMount("tekton-internal-workspace", workspaceDir), tb.VolumeMount("tekton-internal-home", "/tekton/home"), tb.VolumeMount("tekton-internal-results", "/tekton/results"), + tb.VolumeMount("tekton-internal-steps", "/tekton/steps"), tb.TerminationMessagePath("/tekton/termination"), ), ), @@ -1344,7 +1471,7 @@ func TestReconcile(t *testing.T) { tb.OwnerReferenceAPIVersion(currentAPIVersion)), tb.PodSpec( tb.PodServiceAccountName(config.DefaultServiceAccountValue), - tb.PodVolumes(workspaceVolume, homeVolume, resultsVolume, toolsVolume, downwardVolume, corev1.Volume{ + tb.PodVolumes(workspaceVolume, homeVolume, resultsVolume, stepsVolume, toolsVolume, downwardVolume, corev1.Volume{ Name: "tekton-creds-init-home-0", VolumeSource: corev1.VolumeSource{EmptyDir: &corev1.EmptyDirVolumeSource{Medium: corev1.StorageMediumMemory}}, }, corev1.Volume{ @@ -1362,6 +1489,10 @@ func TestReconcile(t *testing.T) { "/tekton/tools/0", "-termination_path", "/tekton/termination", + "-step_metadata_dir", + "/tekton/steps/step-git-source-workspace-9l9zj", + "-step_metadata_dir_link", + "/tekton/steps/0", "-entrypoint", "/ko-app/git-init", "--", @@ -1380,17 +1511,29 @@ func TestReconcile(t *testing.T) { tb.VolumeMount("tekton-internal-workspace", workspaceDir), tb.VolumeMount("tekton-internal-home", "/tekton/home"), tb.VolumeMount("tekton-internal-results", "/tekton/results"), + tb.VolumeMount("tekton-internal-steps", "/tekton/steps"), tb.TerminationMessagePath("/tekton/termination"), ), tb.PodContainer("step-mystep", "ubuntu", tb.Command(entrypointLocation), - tb.Args("-wait_file", "/tekton/tools/0", "-post_file", "/tekton/tools/1", "-termination_path", - "/tekton/termination", "-entrypoint", "/mycmd", "--"), + tb.Args("-wait_file", + "/tekton/tools/0", + "-post_file", + "/tekton/tools/1", + "-termination_path", + "/tekton/termination", + "-step_metadata_dir", + "/tekton/steps/step-mystep", + "-step_metadata_dir_link", + "/tekton/steps/1", + "-entrypoint", + "/mycmd", "--"), tb.VolumeMount("tekton-internal-tools", "/tekton/tools"), tb.VolumeMount("tekton-creds-init-home-1", "/tekton/creds"), tb.VolumeMount("tekton-internal-workspace", workspaceDir), tb.VolumeMount("tekton-internal-home", "/tekton/home"), tb.VolumeMount("tekton-internal-results", "/tekton/results"), + tb.VolumeMount("tekton-internal-steps", "/tekton/steps"), tb.TerminationMessagePath("/tekton/termination"), ), ), @@ -1412,7 +1555,7 @@ func TestReconcile(t *testing.T) { tb.OwnerReferenceAPIVersion(currentAPIVersion)), tb.PodSpec( tb.PodServiceAccountName(config.DefaultServiceAccountValue), - tb.PodVolumes(workspaceVolume, homeVolume, resultsVolume, toolsVolume, downwardVolume, corev1.Volume{ + tb.PodVolumes(workspaceVolume, homeVolume, resultsVolume, stepsVolume, toolsVolume, downwardVolume, corev1.Volume{ Name: "tekton-creds-init-home-0", VolumeSource: corev1.VolumeSource{EmptyDir: &corev1.EmptyDirVolumeSource{Medium: corev1.StorageMediumMemory}}, }), @@ -1427,6 +1570,10 @@ func TestReconcile(t *testing.T) { "/tekton/tools/0", "-termination_path", "/tekton/termination", + "-step_metadata_dir", + "/tekton/steps/step-simple-step", + "-step_metadata_dir_link", + "/tekton/steps/0", "-entrypoint", "/mycmd", "--"), @@ -1436,6 +1583,7 @@ func TestReconcile(t *testing.T) { tb.VolumeMount("tekton-internal-workspace", workspaceDir), tb.VolumeMount("tekton-internal-home", "/tekton/home"), tb.VolumeMount("tekton-internal-results", "/tekton/results"), + tb.VolumeMount("tekton-internal-steps", "/tekton/steps"), tb.TerminationMessagePath("/tekton/termination"), ), ), @@ -1456,7 +1604,7 @@ func TestReconcile(t *testing.T) { tb.OwnerReferenceAPIVersion(currentAPIVersion)), tb.PodSpec( tb.PodServiceAccountName(config.DefaultServiceAccountValue), - tb.PodVolumes(workspaceVolume, homeVolume, resultsVolume, toolsVolume, downwardVolume, corev1.Volume{ + tb.PodVolumes(workspaceVolume, homeVolume, resultsVolume, stepsVolume, toolsVolume, downwardVolume, corev1.Volume{ Name: "tekton-creds-init-home-0", VolumeSource: corev1.VolumeSource{EmptyDir: &corev1.EmptyDirVolumeSource{Medium: corev1.StorageMediumMemory}}, }), @@ -1471,6 +1619,10 @@ func TestReconcile(t *testing.T) { "/tekton/tools/0", "-termination_path", "/tekton/termination", + "-step_metadata_dir", + "/tekton/steps/step-mycontainer", + "-step_metadata_dir_link", + "/tekton/steps/0", "-entrypoint", // Important bit here: /tekton/creds "/mycmd /tekton/creds", @@ -1481,6 +1633,7 @@ func TestReconcile(t *testing.T) { tb.VolumeMount("tekton-internal-workspace", workspaceDir), tb.VolumeMount("tekton-internal-home", "/tekton/home"), tb.VolumeMount("tekton-internal-results", "/tekton/results"), + tb.VolumeMount("tekton-internal-steps", "/tekton/steps"), tb.TerminationMessagePath("/tekton/termination"), ), ), @@ -1502,7 +1655,7 @@ func TestReconcile(t *testing.T) { tb.OwnerReferenceAPIVersion(currentAPIVersion)), tb.PodSpec( tb.PodServiceAccountName(config.DefaultServiceAccountValue), - tb.PodVolumes(workspaceVolume, homeVolume, resultsVolume, toolsVolume, downwardVolume, corev1.Volume{ + tb.PodVolumes(workspaceVolume, homeVolume, resultsVolume, stepsVolume, toolsVolume, downwardVolume, corev1.Volume{ Name: "tekton-creds-init-home-0", VolumeSource: corev1.VolumeSource{EmptyDir: &corev1.EmptyDirVolumeSource{Medium: corev1.StorageMediumMemory}}, }), @@ -1517,6 +1670,10 @@ func TestReconcile(t *testing.T) { "/tekton/tools/0", "-termination_path", "/tekton/termination", + "-step_metadata_dir", + "/tekton/steps/step-simple-step", + "-step_metadata_dir_link", + "/tekton/steps/0", "-entrypoint", "/mycmd", "--", @@ -1527,6 +1684,7 @@ func TestReconcile(t *testing.T) { tb.VolumeMount("tekton-internal-workspace", workspaceDir), tb.VolumeMount("tekton-internal-home", "/tekton/home"), tb.VolumeMount("tekton-internal-results", "/tekton/results"), + tb.VolumeMount("tekton-internal-steps", "/tekton/steps"), tb.TerminationMessagePath("/tekton/termination"), ), ), @@ -2238,7 +2396,7 @@ func TestExpandMountPath(t *testing.T) { t.Fatalf("failed to find expanded Workspace mountpath %v", expectedMountPath) } - if a := pod.Spec.Containers[0].Args[10]; a != expectedReplacedArgs { + if a := pod.Spec.Containers[0].Args[14]; a != expectedReplacedArgs { t.Fatalf("failed to replace Workspace mountpath variable, expected %s, actual: %s", expectedReplacedArgs, a) } } @@ -3387,6 +3545,54 @@ func TestFailTaskRun(t *testing.T) { }, }, }, + }, { + name: "step-status-update-with-multiple-steps-and-some-continue-on-error", + taskRun: tb.TaskRun("test-taskrun-run-ignore-step-error", tb.TaskRunNamespace("foo"), tb.TaskRunSpec( + tb.TaskRunTaskRef(taskMultipleStepsIgnoreError.Name), + ), tb.TaskRunStatus(tb.StatusCondition(apis.Condition{ + Type: apis.ConditionSucceeded, + Status: corev1.ConditionTrue, + }), tb.StepState( + tb.SetStepStateTerminated(corev1.ContainerStateTerminated{ + StartedAt: metav1.Time{Time: time.Now()}, + FinishedAt: metav1.Time{Time: time.Now()}, + Reason: "Completed", + ExitCode: 12, + }), + ), tb.StepState( + tb.SetStepStateRunning(corev1.ContainerStateRunning{StartedAt: metav1.Time{Time: time.Now()}}), + ), + tb.PodName("foo-is-bar"))), + pod: &corev1.Pod{ObjectMeta: metav1.ObjectMeta{ + Namespace: "foo", + Name: "foo-is-bar", + }}, + reason: v1beta1.TaskRunReasonTimedOut, + message: "TaskRun test-taskrun-run-timeout-multiple-steps failed to finish within 10s", + expectedStatus: apis.Condition{ + Type: apis.ConditionSucceeded, + Status: corev1.ConditionFalse, + Reason: v1beta1.TaskRunReasonTimedOut.String(), + Message: "TaskRun test-taskrun-run-timeout-multiple-steps failed to finish within 10s", + }, + expectedStepStates: []v1beta1.StepState{ + { + ContainerState: corev1.ContainerState{ + Terminated: &corev1.ContainerStateTerminated{ + ExitCode: 12, + Reason: "Completed", + }, + }, + }, + { + ContainerState: corev1.ContainerState{ + Terminated: &corev1.ContainerStateTerminated{ + ExitCode: 1, + Reason: v1beta1.TaskRunReasonTimedOut.String(), + }, + }, + }, + }, }} for _, tc := range testCases { diff --git a/test/ignore_step_error_test.go b/test/ignore_step_error_test.go new file mode 100644 index 00000000000..5389a8778ef --- /dev/null +++ b/test/ignore_step_error_test.go @@ -0,0 +1,103 @@ +// +build e2e + +/* +Copyright 2021 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" + "testing" + + "github.com/tektoncd/pipeline/pkg/reconciler/pipelinerun" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + knativetest "knative.dev/pkg/test" +) + +func TestMissingResultWhenStepErrorIsIgnored(t *testing.T) { + ctx := context.Background() + ctx, cancel := context.WithCancel(ctx) + defer cancel() + c, namespace := setup(ctx, t, requireAlphaFeatureFlags) + knativetest.CleanupOnInterrupt(func() { tearDown(ctx, t, c, namespace) }, t.Logf) + defer tearDown(ctx, t, c, namespace) + + pipelineRun := mustParsePipelineRun(t, ` +metadata: + name: pipelinerun-with-failing-step +spec: + pipelineSpec: + tasks: + - name: task1 + taskSpec: + results: + - name: result1 + - name: result2 + steps: + - name: failing-step + onError: continue + image: busybox + script: 'echo -n 123 | tee $(results.result1.path); exit 1; echo -n 456 | tee $(results.result2.path)' + - name: task2 + runAfter: [ task1 ] + params: + - name: param1 + value: $(tasks.task1.results.result1) + - name: param2 + value: $(tasks.task1.results.result2) + taskSpec: + params: + - name: param1 + - name: param2 + steps: + - name: foo + image: busybox + script: 'exit 0'`) + + if _, err := c.PipelineRunClient.Create(ctx, pipelineRun, metav1.CreateOptions{}); err != nil { + t.Fatalf("Failed to create PipelineRun `%s`: %s", pipelineRun.Name, err) + } + + t.Logf("Waiting for PipelineRun in namespace %s to fail", namespace) + if err := WaitForPipelineRunState(ctx, c, pipelineRun.Name, pipelineRunTimeout, FailedWithReason(pipelinerun.ReasonInvalidTaskResultReference, pipelineRun.Name), "InvalidTaskResultReference"); err != nil { + t.Errorf("Error waiting for PipelineRun to fail: %s", err) + } + + taskrunList, err := c.TaskRunClient.List(ctx, metav1.ListOptions{LabelSelector: "tekton.dev/pipelineRun=" + pipelineRun.Name}) + if err != nil { + t.Fatalf("Error listing TaskRuns for PipelineRun %s: %s", pipelineRun.Name, err) + } + + if len(taskrunList.Items) != 1 { + t.Fatalf("The pipelineRun should have exactly 1 taskRun for the first task \"task1\"") + } + + taskrunItem := taskrunList.Items[0] + if taskrunItem.Labels["tekton.dev/pipelineTask"] != "task1" { + t.Fatalf("TaskRun was not found for the task \"task1\"") + } + + if len(taskrunItem.Status.TaskRunResults) != 1 { + t.Fatalf("task1 should have produced a result before failing the step") + } + + for _, r := range taskrunItem.Status.TaskRunResults { + if r.Name == "result1" && r.Value != "123" { + t.Fatalf("task1 should have initialized a result \"result1\" to \"123\"") + } + } + +}