From b395663c9d8bf51e34c8b1ce5907b6e3e8baef68 Mon Sep 17 00:00:00 2001 From: Chitrang Patel Date: Fri, 10 Nov 2023 16:18:13 -0500 Subject: [PATCH] TEP-0142: Surface step results via termination message This PR surfaces step results (i.e results written to $(step.results..path)) via termination messages. A followup PR will handle surfacing the results via sidecar logs. --- cmd/entrypoint/main.go | 2 + docs/pipeline-api.md | 32 ++- docs/stepactions.md | 161 +++++++++-- .../v1/taskruns/no-ci/stepaction-results.yaml | 24 ++ hack/ignored-openapi-violations.list | 2 + pkg/apis/pipeline/v1/openapi_generated.go | 17 +- pkg/apis/pipeline/v1/result_types.go | 3 + pkg/apis/pipeline/v1/swagger.json | 9 +- pkg/apis/pipeline/v1/taskrun_types.go | 7 +- pkg/apis/pipeline/v1/zz_generated.deepcopy.go | 7 + .../pipeline/v1beta1/openapi_generated.go | 17 +- pkg/apis/pipeline/v1beta1/result_types.go | 3 + pkg/apis/pipeline/v1beta1/swagger.json | 9 +- .../pipeline/v1beta1/taskrun_conversion.go | 12 + .../v1beta1/taskrun_conversion_test.go | 23 ++ pkg/apis/pipeline/v1beta1/taskrun_types.go | 7 +- .../pipeline/v1beta1/zz_generated.deepcopy.go | 7 + pkg/entrypoint/entrypointer.go | 27 +- pkg/entrypoint/entrypointer_test.go | 41 ++- pkg/pod/entrypoint.go | 28 +- pkg/pod/entrypoint_test.go | 100 ++++++- pkg/pod/pod.go | 4 +- pkg/pod/status.go | 89 +++++- pkg/pod/status_test.go | 257 +++++++++++++++++ pkg/reconciler/taskrun/resources/apply.go | 35 ++- .../taskrun/resources/apply_test.go | 46 +++- pkg/reconciler/taskrun/resources/taskspec.go | 3 + .../taskrun/resources/taskspec_test.go | 35 +++ pkg/reconciler/taskrun/taskrun.go | 2 +- pkg/reconciler/taskrun/taskrun_test.go | 33 ++- pkg/result/result.go | 4 + pkg/result/result_test.go | 4 + test/clients.go | 2 + test/stepaction_results_test.go | 258 ++++++++++++++++++ 34 files changed, 1231 insertions(+), 79 deletions(-) create mode 100644 examples/v1/taskruns/no-ci/stepaction-results.yaml create mode 100644 test/stepaction_results_test.go diff --git a/cmd/entrypoint/main.go b/cmd/entrypoint/main.go index 54422d789e5..dead6fa6581 100644 --- a/cmd/entrypoint/main.go +++ b/cmd/entrypoint/main.go @@ -49,6 +49,7 @@ var ( postFile = flag.String("post_file", "", "If specified, file to write upon completion") terminationPath = flag.String("termination_path", "/tekton/termination", "If specified, file to write upon termination") results = flag.String("results", "", "If specified, list of file names that might contain task results") + stepResults = flag.String("step_results", "", "step results if specified") timeout = flag.Duration("timeout", time.Duration(0), "If specified, sets timeout for step") stdoutPath = flag.String("stdout_path", "", "If specified, file to copy stdout to") stderrPath = flag.String("stderr_path", "", "If specified, file to copy stderr to") @@ -159,6 +160,7 @@ func main() { }, PostWriter: &realPostWriter{}, Results: strings.Split(*results, ","), + StepResults: strings.Split(*stepResults, ","), Timeout: timeout, BreakpointOnFailure: *breakpointOnFailure, OnError: *onError, diff --git a/docs/pipeline-api.md b/docs/pipeline-api.md index ffd32222dcd..88c0ab2f622 100644 --- a/docs/pipeline-api.md +++ b/docs/pipeline-api.md @@ -4625,6 +4625,18 @@ string + + +results
+ + +[]TaskRunResult + + + + + +

StepTemplate @@ -5143,10 +5155,10 @@ reasons that emerge from underlying resources are not included here

TaskRunResult

-(Appears on:TaskRunStatusFields) +(Appears on:StepState, TaskRunStatusFields)

-

TaskRunResult used to describe the results of a task

+

TaskRunStepResult is a type alias of TaskRunResult

@@ -13465,6 +13477,18 @@ string + + + +
+results
+ + +[]TaskRunResult + + +
+

StepTemplate @@ -14367,10 +14391,10 @@ reasons that emerge from underlying resources are not included here

TaskRunResult

-(Appears on:TaskRunStatusFields) +(Appears on:StepState, TaskRunStatusFields)

-

TaskRunResult used to describe the results of a task

+

TaskRunStepResult is a type alias of TaskRunResult

diff --git a/docs/stepactions.md b/docs/stepactions.md index 88af514d4c5..65d954ce8c9 100644 --- a/docs/stepactions.md +++ b/docs/stepactions.md @@ -8,6 +8,18 @@ weight: 201 # StepActions - [Overview](#overview) +- [Configuring a StepAction](#configuring-a-stepaction) + - [Declaring Parameters](#declaring-parameters) + - [Passing Params to StepAction](#passing-params-to-stepaction) + - [Emitting Results](#emitting-results) + - [Fetching Emitted Results from StepActions](#fetching-emitted-results-from-stepactions) + - [Declaring SecurityContext](#declaring-securitycontext) + - [Declaring VolumeMounts](#declaring-volumemounts) + - [Referencing a StepAction](#referencing-a-stepaction) + - [Specifying Remote StepActions](#specifying-remote-stepactions) +- [Known Limitations](#known-limitations) + - [Cannot pass Step Results between Steps](#cannot-pass-step-results-between-steps) + - [Cannot extract Step Results via Sidecar logs](#cannot-extract-step-results-via-sidecar-logs) ## Overview :warning: This feature is in a preview mode. @@ -39,7 +51,7 @@ A `StepAction` definition supports the following fields: - cannot be used at the same time as using `command`. - `env` - [`params`](#declaring-params) - - [`results`](#declaring-results) + - [`results`](#emitting-results) - [`securityContext`](#declaring-securitycontext) - [`volumeMounts`](#declaring-volumemounts) @@ -93,7 +105,33 @@ spec: ] ``` -### Declaring Results +#### Passing Params to StepAction + +A `StepAction` may require [params](#(declaring-parameters)). In this case, a `Task` needs to ensure that the `StepAction` has access to all the required `params`. +When referencing a `StepAction`, a `Step` can also provide it with `params`, just like how a `TaskRun` provides params to the underlying `Task`. + +```yaml +apiVersion: tekton.dev/v1 +kind: Task +metadata: + name: step-action +spec: + TaskSpec: + params: + - name: param-for-step-action + description: "this is a param that the step action needs." + steps: + - name: action-runner + ref: + name: step-action + params: + - name: step-action-param + value: $(params.param-for-step-action) +``` + +**Note:** If a `Step` declares `params` for an `inlined Step`, it will also lead to a validation error. This is because an `inlined Step` gets it's `params` from the `TaskRun`. + +### Emitting Results A `StepAction` also declares the results that it will emit. @@ -115,6 +153,85 @@ spec: date | tee $(results.current-date-human-readable.path) ``` +It is possible that a `StepAction` with `Results` is used multiple times in the same `Task` or multiple `StepActions` in the same `Task` produce `Results` with the same name. Resolving the `Result` names becomes critical otherwise there could be unexpected outcomes. The `Task` needs to be able to resolve these `Result` names clashes by mapping it to a different `Result` name. For this reason, we introduce the capability to store results on a `Step` level. + +`StepActions` can also emit `Results` to `$(step.results..path)`. + +```yaml +apiVersion: tekton.dev/v1alpha1 +kind: StepAction +metadata: + name: stepaction-declaring-results +spec: + results: + - name: current-date-unix-timestamp + description: The current date in unix timestamp format + - name: current-date-human-readable + description: The current date in human readable format + image: bash:latest + script: | + #!/usr/bin/env bash + date +%s | tee $(step.results.current-date-unix-timestamp.path) + date | tee $(step.results.current-date-human-readable.path) +``` + +`Results` from the above `StepAction` can be [fetched by the `Task`](#fetching-emitted-results-from-step-actions) in another `StepAction` via `$(steps..results.)`. + +#### Fetching Emitted Results from StepActions + +A `Task` can fetch `Results` produced by the `StepActions` (i.e. only `Results` emitted to `$(step.results..path)`, `NOT` $(results..path)) using variable replacement syntax. We introduce a field to [`Task Results`](./tasks.md#emitting-results) called `Value` whose value can be set to the variable `$(steps..results.)`. + +```yaml +apiVersion: tekton.dev/v1 +kind: Task +metadata: + name: task-fetching-results +spec: + results: + - name: git-url + description: "url of git repo" + value: $(steps.git-clone.results.url) + - name: registry-url + description: "url of docker registry" + value: $(steps.kaniko.results.url) + steps: + - name: git-clone + ref: + name: clone-step-action + - name: kaniko + ref: + name: kaniko-step-action +``` + +`Results` emitted to `$(step.results..path)` are not automatically available as `TaskRun Results`. The `Task` must explicitly fetch it from the underlying `Step` referencing `StepActions`. + +For example, lets assume that in the previous example, the "kaniko" `StepAction` also produced a `Result` named "digest". In that case, the `Task` should also fetch the "digest" from "kaniko" `Step`. + +```yaml +apiVersion: tekton.dev/v1 +kind: Task +metadata: + name: task-fetching-results +spec: + results: + - name: git-url + description: "url of git repo" + value: $(steps.git-clone.results.url) + - name: registry-url + description: "url of docker registry" + value: $(steps.kaniko.results.url) + - name: digest + description: "digest of the image" + value: $(steps.kaniko.results.digest) + steps: + - name: git-clone + ref: + name: clone-step-action + - name: kaniko + ref: + name: kaniko-step-action +``` + ### Declaring SecurityContext You can declare `securityContext` in a `StepAction`: @@ -159,7 +276,7 @@ spec: ``` -## Referencing a StepAction +### Referencing a StepAction `StepActions` can be referenced from the `Step` using the `ref` field, as follows: @@ -280,33 +397,7 @@ spec: onError: continue ``` -### Passing Params to StepAction - -A `StepAction` may require [params](#(declaring-parameters)). In this case, a `Task` needs to ensure that the `StepAction` has access to all the required `params`. -When referencing a `StepAction`, a `Step` can also provide it with `params`, just like how a `TaskRun` provides params to the underlying `Task`. - -```yaml -apiVersion: tekton.dev/v1 -kind: Task -metadata: - name: step-action -spec: - TaskSpec: - params: - - name: param-for-step-action - description: "this is a param that the step action needs." - steps: - - name: action-runner - ref: - name: step-action - params: - - name: step-action-param - value: $(params.param-for-step-action) -``` - -**Note:** If a `Step` declares `params` for an `inlined Step`, it will also lead to a validation error. This is because an `inlined Step` gets it's `params` from the `TaskRun`. - -### Specifying Remote StepActions +#### Specifying Remote StepActions A `ref` field may specify a `StepAction` in a remote location such as git. Support for specific types of remote will depend on the `Resolvers` your @@ -333,3 +424,13 @@ spec: ``` The default resolver type can be configured by the `default-resolver-type` field in the `config-defaults` ConfigMap (`alpha` feature). See [additional-configs.md](./additional-configs.md) for details. + +## Known Limitations + +### Cannot pass Step Results between Steps + +It's not currently possible to pass results produced by a `Step` into following `Steps`. We are working on this feature and will be made available soon. + +### Cannot extract Step Results via Sidecar logs + +Currently, we only support Step Results via `Termination Message` (the default method of result extraction). The ability to extract `Step` results from `sidecar-logs` is not yet available. We are working on enabling this soon. diff --git a/examples/v1/taskruns/no-ci/stepaction-results.yaml b/examples/v1/taskruns/no-ci/stepaction-results.yaml new file mode 100644 index 00000000000..f4a285d83db --- /dev/null +++ b/examples/v1/taskruns/no-ci/stepaction-results.yaml @@ -0,0 +1,24 @@ +apiVersion: tekton.dev/v1alpha1 +kind: StepAction +metadata: + name: step-action +spec: + image: alpine + results: + - name: result1 + script: | + echo "I am a Step Action!!!" >> $(step.results.result1.path) +--- +apiVersion: tekton.dev/v1 +kind: TaskRun +metadata: + name: step-action-run +spec: + TaskSpec: + results: + - name: step-result + value: $(steps.action-runner.results.result1) + steps: + - name: action-runner + ref: + name: step-action diff --git a/hack/ignored-openapi-violations.list b/hack/ignored-openapi-violations.list index 43afc5a82d9..ef619305725 100644 --- a/hack/ignored-openapi-violations.list +++ b/hack/ignored-openapi-violations.list @@ -49,3 +49,5 @@ API rule violation: list_type_missing,github.com/tektoncd/pipeline/pkg/apis/pipe API rule violation: list_type_missing,github.com/tektoncd/pipeline/pkg/apis/pipeline/v1alpha1,VerificationPolicySpec,Resources API rule violation: list_type_missing,github.com/tektoncd/pipeline/pkg/apis/pipeline/v1beta1,ParamSpec,Enum API rule violation: list_type_missing,github.com/tektoncd/pipeline/pkg/apis/pipeline/v1,ParamSpec,Enum +API rule violation: list_type_missing,github.com/tektoncd/pipeline/pkg/apis/pipeline/v1,StepState,Results +API rule violation: list_type_missing,github.com/tektoncd/pipeline/pkg/apis/pipeline/v1beta1,StepState,Results diff --git a/pkg/apis/pipeline/v1/openapi_generated.go b/pkg/apis/pipeline/v1/openapi_generated.go index e66aea8abf3..0fb74d29b15 100644 --- a/pkg/apis/pipeline/v1/openapi_generated.go +++ b/pkg/apis/pipeline/v1/openapi_generated.go @@ -3130,11 +3130,24 @@ func schema_pkg_apis_pipeline_v1_StepState(ref common.ReferenceCallback) common. Format: "", }, }, + "results": { + SchemaProps: spec.SchemaProps{ + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/tektoncd/pipeline/pkg/apis/pipeline/v1.TaskRunResult"), + }, + }, + }, + }, + }, }, }, }, Dependencies: []string{ - "k8s.io/api/core/v1.ContainerStateRunning", "k8s.io/api/core/v1.ContainerStateTerminated", "k8s.io/api/core/v1.ContainerStateWaiting"}, + "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1.TaskRunResult", "k8s.io/api/core/v1.ContainerStateRunning", "k8s.io/api/core/v1.ContainerStateTerminated", "k8s.io/api/core/v1.ContainerStateWaiting"}, } } @@ -3666,7 +3679,7 @@ func schema_pkg_apis_pipeline_v1_TaskRunResult(ref common.ReferenceCallback) com return common.OpenAPIDefinition{ Schema: spec.Schema{ SchemaProps: spec.SchemaProps{ - Description: "TaskRunResult used to describe the results of a task", + Description: "TaskRunStepResult is a type alias of TaskRunResult", Type: []string{"object"}, Properties: map[string]spec.Schema{ "name": { diff --git a/pkg/apis/pipeline/v1/result_types.go b/pkg/apis/pipeline/v1/result_types.go index 9727ae49a5b..6361d7a362d 100644 --- a/pkg/apis/pipeline/v1/result_types.go +++ b/pkg/apis/pipeline/v1/result_types.go @@ -72,6 +72,9 @@ type TaskRunResult struct { Value ResultValue `json:"value"` } +// TaskRunStepResult is a type alias of TaskRunResult +type TaskRunStepResult = TaskRunResult + // ResultValue is a type alias of ParamValue type ResultValue = ParamValue diff --git a/pkg/apis/pipeline/v1/swagger.json b/pkg/apis/pipeline/v1/swagger.json index ea46bf648ea..d99469bffd7 100644 --- a/pkg/apis/pipeline/v1/swagger.json +++ b/pkg/apis/pipeline/v1/swagger.json @@ -1595,6 +1595,13 @@ "name": { "type": "string" }, + "results": { + "type": "array", + "items": { + "default": {}, + "$ref": "#/definitions/v1.TaskRunResult" + } + }, "running": { "description": "Details about a running container", "$ref": "#/definitions/v1.ContainerStateRunning" @@ -1887,7 +1894,7 @@ } }, "v1.TaskRunResult": { - "description": "TaskRunResult used to describe the results of a task", + "description": "TaskRunStepResult is a type alias of TaskRunResult", "type": "object", "required": [ "name", diff --git a/pkg/apis/pipeline/v1/taskrun_types.go b/pkg/apis/pipeline/v1/taskrun_types.go index 0b8e9451975..ff78c122dfa 100644 --- a/pkg/apis/pipeline/v1/taskrun_types.go +++ b/pkg/apis/pipeline/v1/taskrun_types.go @@ -338,9 +338,10 @@ func (trs *TaskRunStatus) SetCondition(newCond *apis.Condition) { // StepState reports the results of running a step in a Task. type StepState struct { corev1.ContainerState `json:",inline"` - Name string `json:"name,omitempty"` - Container string `json:"container,omitempty"` - ImageID string `json:"imageID,omitempty"` + Name string `json:"name,omitempty"` + Container string `json:"container,omitempty"` + ImageID string `json:"imageID,omitempty"` + Results []TaskRunStepResult `json:"results,omitempty"` } // SidecarState reports the results of running a sidecar in a Task. diff --git a/pkg/apis/pipeline/v1/zz_generated.deepcopy.go b/pkg/apis/pipeline/v1/zz_generated.deepcopy.go index dd332d9188a..40fe4ba804d 100644 --- a/pkg/apis/pipeline/v1/zz_generated.deepcopy.go +++ b/pkg/apis/pipeline/v1/zz_generated.deepcopy.go @@ -1360,6 +1360,13 @@ func (in *StepResult) DeepCopy() *StepResult { func (in *StepState) DeepCopyInto(out *StepState) { *out = *in in.ContainerState.DeepCopyInto(&out.ContainerState) + if in.Results != nil { + in, out := &in.Results, &out.Results + *out = make([]TaskRunResult, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } return } diff --git a/pkg/apis/pipeline/v1beta1/openapi_generated.go b/pkg/apis/pipeline/v1beta1/openapi_generated.go index 182364822bc..d9eaff0ecc4 100644 --- a/pkg/apis/pipeline/v1beta1/openapi_generated.go +++ b/pkg/apis/pipeline/v1beta1/openapi_generated.go @@ -4030,11 +4030,24 @@ func schema_pkg_apis_pipeline_v1beta1_StepState(ref common.ReferenceCallback) co Format: "", }, }, + "results": { + SchemaProps: spec.SchemaProps{ + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/tektoncd/pipeline/pkg/apis/pipeline/v1beta1.TaskRunResult"), + }, + }, + }, + }, + }, }, }, }, Dependencies: []string{ - "k8s.io/api/core/v1.ContainerStateRunning", "k8s.io/api/core/v1.ContainerStateTerminated", "k8s.io/api/core/v1.ContainerStateWaiting"}, + "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1beta1.TaskRunResult", "k8s.io/api/core/v1.ContainerStateRunning", "k8s.io/api/core/v1.ContainerStateTerminated", "k8s.io/api/core/v1.ContainerStateWaiting"}, } } @@ -4928,7 +4941,7 @@ func schema_pkg_apis_pipeline_v1beta1_TaskRunResult(ref common.ReferenceCallback return common.OpenAPIDefinition{ Schema: spec.Schema{ SchemaProps: spec.SchemaProps{ - Description: "TaskRunResult used to describe the results of a task", + Description: "TaskRunStepResult is a type alias of TaskRunResult", Type: []string{"object"}, Properties: map[string]spec.Schema{ "name": { diff --git a/pkg/apis/pipeline/v1beta1/result_types.go b/pkg/apis/pipeline/v1beta1/result_types.go index 2f484f47ddd..ec3192d3925 100644 --- a/pkg/apis/pipeline/v1beta1/result_types.go +++ b/pkg/apis/pipeline/v1beta1/result_types.go @@ -52,6 +52,9 @@ type TaskRunResult struct { Value ResultValue `json:"value"` } +// TaskRunStepResult is a type alias of TaskRunResult +type TaskRunStepResult = TaskRunResult + // ResultValue is a type alias of ParamValue type ResultValue = ParamValue diff --git a/pkg/apis/pipeline/v1beta1/swagger.json b/pkg/apis/pipeline/v1beta1/swagger.json index e04ad18a4b3..63c4af3d15b 100644 --- a/pkg/apis/pipeline/v1beta1/swagger.json +++ b/pkg/apis/pipeline/v1beta1/swagger.json @@ -2220,6 +2220,13 @@ "name": { "type": "string" }, + "results": { + "type": "array", + "items": { + "default": {}, + "$ref": "#/definitions/v1beta1.TaskRunResult" + } + }, "running": { "description": "Details about a running container", "$ref": "#/definitions/v1.ContainerStateRunning" @@ -2704,7 +2711,7 @@ } }, "v1beta1.TaskRunResult": { - "description": "TaskRunResult used to describe the results of a task", + "description": "TaskRunStepResult is a type alias of TaskRunResult", "type": "object", "required": [ "name", diff --git a/pkg/apis/pipeline/v1beta1/taskrun_conversion.go b/pkg/apis/pipeline/v1beta1/taskrun_conversion.go index ed3fcc5856e..f2e847c6124 100644 --- a/pkg/apis/pipeline/v1beta1/taskrun_conversion.go +++ b/pkg/apis/pipeline/v1beta1/taskrun_conversion.go @@ -330,6 +330,12 @@ func (ss StepState) convertTo(ctx context.Context, sink *v1.StepState) { sink.Name = ss.Name sink.Container = ss.ContainerName sink.ImageID = ss.ImageID + sink.Results = nil + for _, r := range ss.Results { + new := v1.TaskRunStepResult{} + r.convertTo(ctx, &new) + sink.Results = append(sink.Results, new) + } } func (ss *StepState) convertFrom(ctx context.Context, source v1.StepState) { @@ -337,6 +343,12 @@ func (ss *StepState) convertFrom(ctx context.Context, source v1.StepState) { ss.Name = source.Name ss.ContainerName = source.Container ss.ImageID = source.ImageID + ss.Results = nil + for _, r := range source.Results { + new := TaskRunStepResult{} + new.convertFrom(ctx, r) + ss.Results = append(ss.Results, new) + } } func (trr TaskRunResult) convertTo(ctx context.Context, sink *v1.TaskRunResult) { diff --git a/pkg/apis/pipeline/v1beta1/taskrun_conversion_test.go b/pkg/apis/pipeline/v1beta1/taskrun_conversion_test.go index 13a870e2728..27da78412eb 100644 --- a/pkg/apis/pipeline/v1beta1/taskrun_conversion_test.go +++ b/pkg/apis/pipeline/v1beta1/taskrun_conversion_test.go @@ -102,6 +102,29 @@ func TestTaskRunConversion(t *testing.T) { }, }, }, + }, { + name: "taskrun with step Results in step state", + in: &v1beta1.TaskRun{ + ObjectMeta: metav1.ObjectMeta{ + Name: "foo", + Namespace: "bar", + }, + Spec: v1beta1.TaskRunSpec{}, + Status: v1beta1.TaskRunStatus{ + TaskRunStatusFields: v1beta1.TaskRunStatusFields{ + Steps: []v1beta1.StepState{{ + Results: []v1beta1.TaskRunStepResult{{ + Name: "foo", + Type: v1beta1.ResultsTypeString, + Value: v1beta1.ResultValue{ + Type: v1beta1.ParamTypeString, + StringVal: "bar", + }, + }}, + }}, + }, + }, + }, }, { name: "taskrun conversion all non deprecated fields", in: &v1beta1.TaskRun{ diff --git a/pkg/apis/pipeline/v1beta1/taskrun_types.go b/pkg/apis/pipeline/v1beta1/taskrun_types.go index 2b869121d2c..a12676acb0a 100644 --- a/pkg/apis/pipeline/v1beta1/taskrun_types.go +++ b/pkg/apis/pipeline/v1beta1/taskrun_types.go @@ -367,9 +367,10 @@ func (trs *TaskRunStatus) SetCondition(newCond *apis.Condition) { // StepState reports the results of running a step in a Task. type StepState struct { corev1.ContainerState `json:",inline"` - Name string `json:"name,omitempty"` - ContainerName string `json:"container,omitempty"` - ImageID string `json:"imageID,omitempty"` + Name string `json:"name,omitempty"` + ContainerName string `json:"container,omitempty"` + ImageID string `json:"imageID,omitempty"` + Results []TaskRunStepResult `json:"results,omitempty"` } // SidecarState reports the results of running a sidecar in a Task. diff --git a/pkg/apis/pipeline/v1beta1/zz_generated.deepcopy.go b/pkg/apis/pipeline/v1beta1/zz_generated.deepcopy.go index 1eb1695bbcd..2dd4fd8edb2 100644 --- a/pkg/apis/pipeline/v1beta1/zz_generated.deepcopy.go +++ b/pkg/apis/pipeline/v1beta1/zz_generated.deepcopy.go @@ -1810,6 +1810,13 @@ func (in *StepOutputConfig) DeepCopy() *StepOutputConfig { func (in *StepState) DeepCopyInto(out *StepState) { *out = *in in.ContainerState.DeepCopyInto(&out.ContainerState) + if in.Results != nil { + in, out := &in.Results, &out.Results + *out = make([]TaskRunResult, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } return } diff --git a/pkg/entrypoint/entrypointer.go b/pkg/entrypoint/entrypointer.go index ac7baf16dfb..c90b6d0960c 100644 --- a/pkg/entrypoint/entrypointer.go +++ b/pkg/entrypoint/entrypointer.go @@ -96,6 +96,8 @@ type Entrypointer struct { // PostWriter encapsulates writing files when complete. PostWriter PostWriter + // StepResults is the set of files that might contain step results + StepResults []string // Results is the set of files that might contain task results Results []string // Timeout is an optional user-specified duration within which the Step must complete @@ -147,6 +149,9 @@ func (e Entrypointer) Go() error { _ = logger.Sync() }() + if err := os.MkdirAll(filepath.Join(e.StepMetadataDir, "results"), os.ModePerm); err != nil { + return err + } for _, f := range e.WaitFiles { if err := e.Waiter.Wait(context.Background(), f, e.WaitFileContent, e.BreakpointOnFailure); err != nil { // An error happened while waiting, so we bail @@ -237,17 +242,30 @@ func (e Entrypointer) Go() error { if e.ResultsDirectory != "" { resultPath = e.ResultsDirectory } - if err := e.readResultsFromDisk(ctx, resultPath); err != nil { + if err := e.readResultsFromDisk(ctx, resultPath, result.TaskRunResultType); err != nil { logger.Fatalf("Error while handling results: %s", err) } } + if len(e.StepResults) >= 1 && e.StepResults[0] != "" { + stepResultPath := filepath.Join(e.StepMetadataDir, "results") + if e.ResultsDirectory != "" { + stepResultPath = e.ResultsDirectory + } + if err := e.readResultsFromDisk(ctx, stepResultPath, result.StepResultType); err != nil { + logger.Fatalf("Error while handling step results: %s", err) + } + } return err } -func (e Entrypointer) readResultsFromDisk(ctx context.Context, resultDir string) error { +func (e Entrypointer) readResultsFromDisk(ctx context.Context, resultDir string, resultType result.ResultType) error { output := []result.RunResult{} - for _, resultFile := range e.Results { + results := e.Results + if resultType == result.StepResultType { + results = e.StepResults + } + for _, resultFile := range results { if resultFile == "" { continue } @@ -261,9 +279,10 @@ func (e Entrypointer) readResultsFromDisk(ctx context.Context, resultDir string) output = append(output, result.RunResult{ Key: resultFile, Value: string(fileContents), - ResultType: result.TaskRunResultType, + ResultType: resultType, }) } + if e.SpireWorkloadAPI != nil { signed, err := e.SpireWorkloadAPI.Sign(ctx, output) if err != nil { diff --git a/pkg/entrypoint/entrypointer_test.go b/pkg/entrypoint/entrypointer_test.go index 84731daeb24..127d70b992d 100644 --- a/pkg/entrypoint/entrypointer_test.go +++ b/pkg/entrypoint/entrypointer_test.go @@ -262,11 +262,13 @@ func TestReadResultsFromDisk(t *testing.T) { desc string results []string resultContent []v1beta1.ResultValue + resultType result.ResultType want []result.RunResult }{{ desc: "read string result file", results: []string{"results"}, resultContent: []v1beta1.ResultValue{*v1beta1.NewStructuredValues("hello world")}, + resultType: result.TaskRunResultType, want: []result.RunResult{ {Value: `"hello world"`, ResultType: 1}}, @@ -274,6 +276,7 @@ func TestReadResultsFromDisk(t *testing.T) { desc: "read array result file", results: []string{"results"}, resultContent: []v1beta1.ResultValue{*v1beta1.NewStructuredValues("hello", "world")}, + resultType: result.TaskRunResultType, want: []result.RunResult{ {Value: `["hello","world"]`, ResultType: 1}}, @@ -281,12 +284,40 @@ func TestReadResultsFromDisk(t *testing.T) { desc: "read string and array result files", results: []string{"resultsArray", "resultsString"}, resultContent: []v1beta1.ResultValue{*v1beta1.NewStructuredValues("hello", "world"), *v1beta1.NewStructuredValues("hello world")}, + resultType: result.TaskRunResultType, want: []result.RunResult{ {Value: `["hello","world"]`, ResultType: 1}, {Value: `"hello world"`, ResultType: 1}, }, + }, { + desc: "read string step result file", + results: []string{"results"}, + resultContent: []v1beta1.ResultValue{*v1beta1.NewStructuredValues("hello world")}, + resultType: result.StepResultType, + want: []result.RunResult{ + {Value: `"hello world"`, + ResultType: 4}}, + }, { + desc: "read array step result file", + results: []string{"results"}, + resultContent: []v1beta1.ResultValue{*v1beta1.NewStructuredValues("hello", "world")}, + resultType: result.StepResultType, + want: []result.RunResult{ + {Value: `["hello","world"]`, + ResultType: 4}}, + }, { + desc: "read string and array step result files", + results: []string{"resultsArray", "resultsString"}, + resultContent: []v1beta1.ResultValue{*v1beta1.NewStructuredValues("hello", "world"), *v1beta1.NewStructuredValues("hello world")}, + resultType: result.StepResultType, + want: []result.RunResult{ + {Value: `["hello","world"]`, + ResultType: 4}, + {Value: `"hello world"`, + ResultType: 4}, + }, }, } { t.Run(c.desc, func(t *testing.T) { @@ -319,10 +350,11 @@ func TestReadResultsFromDisk(t *testing.T) { e := Entrypointer{ Results: resultsFilePath, + StepResults: resultsFilePath, TerminationPath: terminationPath, ResultExtractionMethod: config.ResultExtractionMethodTerminationMessage, } - if err := e.readResultsFromDisk(ctx, ""); err != nil { + if err := e.readResultsFromDisk(ctx, "", c.resultType); err != nil { t.Fatal(err) } msg, err := os.ReadFile(terminationPath) @@ -462,6 +494,12 @@ func TestEntrypointerResults(t *testing.T) { resultsToWrite: map[string]string{ "foo": "abc", }, + }, { + desc: "write single step result", + entrypoint: "echo", + resultsToWrite: map[string]string{ + "foo": "abc", + }, }, { desc: "write multiple result", entrypoint: "echo", @@ -555,6 +593,7 @@ func TestEntrypointerResults(t *testing.T) { Runner: fr, PostWriter: fpw, Results: results, + StepResults: results, ResultsDirectory: resultsDir, ResultExtractionMethod: config.ResultExtractionMethodTerminationMessage, TerminationPath: terminationPath, diff --git a/pkg/pod/entrypoint.go b/pkg/pod/entrypoint.go index d7cd3506405..72096adb72b 100644 --- a/pkg/pod/entrypoint.go +++ b/pkg/pod/entrypoint.go @@ -124,7 +124,7 @@ var ( // command, we must have fetched the image's ENTRYPOINT before calling this // method, using entrypoint_lookup.go. // Additionally, Step timeouts are added as entrypoint flag. -func orderContainers(commonExtraEntrypointArgs []string, steps []corev1.Container, taskSpec *v1.TaskSpec, breakpointConfig *v1.TaskRunDebug, waitForReadyAnnotation, enableKeepPodOnCancel bool) ([]corev1.Container, error) { +func orderContainers(ctx context.Context, commonExtraEntrypointArgs []string, steps []corev1.Container, taskSpec *v1.TaskSpec, breakpointConfig *v1.TaskRunDebug, waitForReadyAnnotation, enableKeepPodOnCancel bool) ([]corev1.Container, error) { if len(steps) == 0 { return nil, errors.New("No steps specified") } @@ -149,6 +149,7 @@ func orderContainers(commonExtraEntrypointArgs []string, steps []corev1.Containe "-termination_path", terminationPath, "-step_metadata_dir", filepath.Join(RunDir, idx, "status"), ) + argsForEntrypoint = append(argsForEntrypoint, commonExtraEntrypointArgs...) if taskSpec != nil { if taskSpec.Steps != nil && len(taskSpec.Steps) >= i+1 { @@ -168,6 +169,9 @@ func orderContainers(commonExtraEntrypointArgs []string, steps []corev1.Containe if taskSpec.Steps[i].StderrConfig != nil { argsForEntrypoint = append(argsForEntrypoint, "-stderr_path", taskSpec.Steps[i].StderrConfig.Path) } + // add step results + stepResultArgs := stepResultArgument(taskSpec.Steps[i].Results) + argsForEntrypoint = append(argsForEntrypoint, stepResultArgs...) } argsForEntrypoint = append(argsForEntrypoint, resultArgument(steps, taskSpec.Results)...) } @@ -199,6 +203,18 @@ func orderContainers(commonExtraEntrypointArgs []string, steps []corev1.Containe return steps, nil } +// stepResultArgument creates the cli arguments for step results to the entrypointer. +func stepResultArgument(stepResults []v1.StepResult) []string { + if len(stepResults) == 0 { + return nil + } + stepResultNames := []string{} + for _, r := range stepResults { + stepResultNames = append(stepResultNames, r.Name) + } + return []string{"-step_results", strings.Join(stepResultNames, ",")} +} + func resultArgument(steps []corev1.Container, results []v1.TaskResult) []string { if len(results) == 0 { return nil @@ -209,7 +225,9 @@ func resultArgument(steps []corev1.Container, results []v1.TaskResult) []string func collectResultsName(results []v1.TaskResult) string { var resultNames []string for _, r := range results { - resultNames = append(resultNames, r.Name) + if r.Value == nil { + resultNames = append(resultNames, r.Name) + } } return strings.Join(resultNames, ",") } @@ -335,7 +353,11 @@ func TrimSidecarPrefix(name string) string { return strings.TrimPrefix(name, sid // returns "step-unnamed-" if not specified func StepName(name string, i int) string { if name != "" { - return fmt.Sprintf("%s%s", stepPrefix, name) + return getContainerName(name) } return fmt.Sprintf("%sunnamed-%d", stepPrefix, i) } + +func getContainerName(name string) string { + return fmt.Sprintf("%s%s", stepPrefix, name) +} diff --git a/pkg/pod/entrypoint_test.go b/pkg/pod/entrypoint_test.go index 56d57c8a146..296bedc3cd8 100644 --- a/pkg/pod/entrypoint_test.go +++ b/pkg/pod/entrypoint_test.go @@ -95,7 +95,7 @@ func TestOrderContainers(t *testing.T) { }, TerminationMessagePath: "/tekton/termination", }} - got, err := orderContainers([]string{}, steps, nil, nil, true, false) + got, err := orderContainers(context.Background(), []string{}, steps, nil, nil, true, false) if err != nil { t.Fatalf("orderContainers: %v", err) } @@ -163,7 +163,7 @@ func TestOrderContainersWithResultsSidecarLogs(t *testing.T) { }, TerminationMessagePath: "/tekton/termination", }} - got, err := orderContainers([]string{"-dont_send_results_to_termination_path"}, steps, nil, nil, true, false) + got, err := orderContainers(context.Background(), []string{"-dont_send_results_to_termination_path"}, steps, nil, nil, true, false) if err != nil { t.Fatalf("orderContainers: %v", err) } @@ -209,7 +209,7 @@ func TestOrderContainersWithNoWait(t *testing.T) { VolumeMounts: []corev1.VolumeMount{volumeMount}, TerminationMessagePath: "/tekton/termination", }} - got, err := orderContainers([]string{}, steps, nil, nil, false, false) + got, err := orderContainers(context.Background(), []string{}, steps, nil, nil, false, false) if err != nil { t.Fatalf("orderContainers: %v", err) } @@ -245,7 +245,7 @@ func TestOrderContainersWithDebugOnFailure(t *testing.T) { OnFailure: "enabled", }, } - got, err := orderContainers([]string{}, steps, nil, taskRunDebugConfig, true, false) + got, err := orderContainers(context.Background(), []string{}, steps, nil, taskRunDebugConfig, true, false) if err != nil { t.Fatalf("orderContainers: %v", err) } @@ -273,7 +273,87 @@ func TestOrderContainersWithEnabelKeepPodOnCancel(t *testing.T) { VolumeMounts: []corev1.VolumeMount{downwardMount}, TerminationMessagePath: "/tekton/termination", }} - got, err := orderContainers([]string{}, steps, nil, nil, false, true) + got, err := orderContainers(context.Background(), []string{}, steps, nil, nil, false, true) + if err != nil { + t.Fatalf("orderContainers: %v", err) + } + if d := cmp.Diff(want, got); d != "" { + t.Errorf("Diff %s", diff.PrintWantGot(d)) + } +} + +func TestStepResultArgument(t *testing.T) { + for _, tc := range []struct { + name string + results []v1.StepResult + want []string + }{{ + name: "no step results", + want: nil, + }, { + name: "step results", + results: []v1.StepResult{{ + Name: "sum", + Description: "This is the sum result of the task", + }, { + Name: "sub", + Description: "This is the sub result of the task", + }}, + want: []string{"-step_results", "sum,sub"}, + }} { + t.Run(tc.name, func(t *testing.T) { + got := stepResultArgument(tc.results) + if d := cmp.Diff(tc.want, got); d != "" { + t.Errorf("Diff %s", diff.PrintWantGot(d)) + } + }) + } +} + +func TestEntryPointStepActionResults(t *testing.T) { + taskSpec := v1.TaskSpec{ + Steps: []v1.Step{{ + Results: []v1.StepResult{{ + Name: "sub", + Description: "This is a step result", + }}, + }}, + Results: []v1.TaskResult{{ + Name: "sum", + Description: "This is the sum result of the task", + }}, + } + + steps := []corev1.Container{{ + Name: "named-step", + Image: "step-1", + Command: []string{"cmd"}, + Args: []string{"arg1", "arg2"}, + }} + want := []corev1.Container{{ + Name: "named-step", + Image: "step-1", + Command: []string{entrypointBinary}, + Args: []string{ + "-wait_file", "/tekton/downward/ready", + "-wait_file_content", + "-post_file", "/tekton/run/0/out", + "-termination_path", "/tekton/termination", + "-step_metadata_dir", "/tekton/run/0/status", + "-step_results", "sub", + "-results", "sum", + "-entrypoint", "cmd", "--", + "arg1", "arg2", + }, + VolumeMounts: []corev1.VolumeMount{downwardMount}, + TerminationMessagePath: "/tekton/termination", + }} + ctx := config.ToContext(context.Background(), &config.Config{ + FeatureFlags: &config.FeatureFlags{ + EnableStepActions: true, + }, + }) + got, err := orderContainers(ctx, []string{}, steps, &taskSpec, nil, true, false) if err != nil { t.Fatalf("orderContainers: %v", err) } @@ -351,7 +431,7 @@ func TestEntryPointResults(t *testing.T) { }, TerminationMessagePath: "/tekton/termination", }} - got, err := orderContainers([]string{}, steps, &taskSpec, nil, true, false) + got, err := orderContainers(context.Background(), []string{}, steps, &taskSpec, nil, true, false) if err != nil { t.Fatalf("orderContainers: %v", err) } @@ -392,7 +472,7 @@ func TestEntryPointResultsSingleStep(t *testing.T) { VolumeMounts: []corev1.VolumeMount{downwardMount}, TerminationMessagePath: "/tekton/termination", }} - got, err := orderContainers([]string{}, steps, &taskSpec, nil, true, false) + got, err := orderContainers(context.Background(), []string{}, steps, &taskSpec, nil, true, false) if err != nil { t.Fatalf("orderContainers: %v", err) } @@ -429,7 +509,7 @@ func TestEntryPointSingleResultsSingleStep(t *testing.T) { VolumeMounts: []corev1.VolumeMount{downwardMount}, TerminationMessagePath: "/tekton/termination", }} - got, err := orderContainers([]string{}, steps, &taskSpec, nil, true, false) + got, err := orderContainers(context.Background(), []string{}, steps, &taskSpec, nil, true, false) if err != nil { t.Fatalf("orderContainers: %v", err) } @@ -500,7 +580,7 @@ func TestEntryPointOnError(t *testing.T) { err: errors.New("task step onError must be either \"continue\" or \"stopAndFail\" but it is set to an invalid value \"invalid-on-error\""), }} { t.Run(tc.desc, func(t *testing.T) { - got, err := orderContainers([]string{}, steps, &tc.taskSpec, nil, true, false) + got, err := orderContainers(context.Background(), []string{}, steps, &tc.taskSpec, nil, true, false) if len(tc.wantContainers) == 0 { if err == nil { t.Fatalf("expected an error for an invalid value for onError but received none") @@ -599,7 +679,7 @@ func TestEntryPointStepOutputConfigs(t *testing.T) { }, TerminationMessagePath: "/tekton/termination", }} - got, err := orderContainers([]string{}, steps, &taskSpec, nil, true, false) + got, err := orderContainers(context.Background(), []string{}, steps, &taskSpec, nil, true, false) if err != nil { t.Fatalf("orderContainers: %v", err) } diff --git a/pkg/pod/pod.go b/pkg/pod/pod.go index cfc7d69d683..04a0a2dec0e 100644 --- a/pkg/pod/pod.go +++ b/pkg/pod/pod.go @@ -239,9 +239,9 @@ func (b *Builder) Build(ctx context.Context, taskRun *v1.TaskRun, taskSpec v1.Ta readyImmediately := isPodReadyImmediately(*featureFlags, taskSpec.Sidecars) if alphaAPIEnabled { - stepContainers, err = orderContainers(commonExtraEntrypointArgs, stepContainers, &taskSpec, taskRun.Spec.Debug, !readyImmediately, enableKeepPodOnCancel) + stepContainers, err = orderContainers(ctx, commonExtraEntrypointArgs, stepContainers, &taskSpec, taskRun.Spec.Debug, !readyImmediately, enableKeepPodOnCancel) } else { - stepContainers, err = orderContainers(commonExtraEntrypointArgs, stepContainers, &taskSpec, nil, !readyImmediately, enableKeepPodOnCancel) + stepContainers, err = orderContainers(ctx, commonExtraEntrypointArgs, stepContainers, &taskSpec, nil, !readyImmediately, enableKeepPodOnCancel) } if err != nil { return nil, err diff --git a/pkg/pod/status.go b/pkg/pod/status.go index 7c52ca4ce32..848c72ba05e 100644 --- a/pkg/pod/status.go +++ b/pkg/pod/status.go @@ -184,9 +184,9 @@ func setTaskRunStatusBasedOnStepStatus(ctx context.Context, logger *zap.SugaredL if err != nil { merr = multierror.Append(merr, err) } - // populate task run CRD with results from sidecar logs - taskResults, _ := filterResults(sidecarLogResults, specResults) + // since sidecar logs does not support step results yet, it is empty for now. + taskResults, _, _ := filterResults(sidecarLogResults, specResults, []v1.StepResult{}) if tr.IsDone() { trs.Results = append(trs.Results, taskResults...) } @@ -195,6 +195,7 @@ func setTaskRunStatusBasedOnStepStatus(ctx context.Context, logger *zap.SugaredL for _, s := range stepStatuses { // Avoid changing the original value by modifying the pointer value. state := s.State.DeepCopy() + taskRunStepResults := []v1.TaskRunStepResult{} if state.Terminated != nil && len(state.Terminated.Message) != 0 { msg := state.Terminated.Message @@ -214,8 +215,35 @@ func setTaskRunStatusBasedOnStepStatus(ctx context.Context, logger *zap.SugaredL merr = multierror.Append(merr, err) } - taskResults, filteredResults := filterResults(results, specResults) + // Identify Step Results + stepResults := []v1.StepResult{} + if ts != nil { + for _, step := range ts.Steps { + if getContainerName(step.Name) == s.Name { + stepResults = append(stepResults, step.Results...) + } + } + } + taskResults, stepRunRes, filteredResults := filterResults(results, specResults, stepResults) if tr.IsDone() { + taskRunStepResults = append(taskRunStepResults, stepRunRes...) + // Identify StepResults needed by the Task Results + neededStepResults, err := findStepResultsFetchedByTask(s.Name, specResults) + if err != nil { + merr = multierror.Append(merr, err) + } + // Set TaskResults from StepResults + for _, r := range stepRunRes { + // this result was requested by the Task + if _, ok := neededStepResults[r.Name]; ok { + taskRunResult := v1.TaskRunResult{ + Name: neededStepResults[r.Name], + Type: r.Type, + Value: r.Value, + } + taskResults = append(taskResults, taskRunResult) + } + } trs.Results = append(trs.Results, taskResults...) } msg, err = createMessageFromResults(filteredResults) @@ -238,6 +266,7 @@ func setTaskRunStatusBasedOnStepStatus(ctx context.Context, logger *zap.SugaredL Name: trimStepPrefix(s.Name), Container: s.Name, ImageID: s.ImageID, + Results: taskRunStepResults, }) } @@ -266,16 +295,43 @@ func createMessageFromResults(results []result.RunResult) (string, error) { return string(bytes), nil } +// findStepResultsFetchedByTask fetches step results that the Task needs. +// It accepts a container name and the TaskResults as input and outputs +// a map with the name of the step result as the key and the name of the task result that is fetching it as value. +func findStepResultsFetchedByTask(containerName string, specResults []v1.TaskResult) (map[string]string, error) { + neededStepResults := map[string]string{} + for _, r := range specResults { + if r.Value != nil { + if r.Value.StringVal != "" { + sName, resultName, err := v1.ExtractStepResultName(r.Value.StringVal) + if err != nil { + return nil, err + } + // Only look at named results - referencing unnamed steps is unsupported. + if getContainerName(sName) == containerName { + neededStepResults[resultName] = r.Name + } + } + } + } + return neededStepResults, nil +} + // filterResults filters the RunResults and TaskResults based on the results declared in the task spec. // It returns a slice of any of the input results that are defined in the task spec, converted to TaskRunResults, // and a slice of any of the RunResults that don't represent internal values (i.e. those that should not be displayed in the TaskRun status. -func filterResults(results []result.RunResult, specResults []v1.TaskResult) ([]v1.TaskRunResult, []result.RunResult) { +func filterResults(results []result.RunResult, specResults []v1.TaskResult, stepResults []v1.StepResult) ([]v1.TaskRunResult, []v1.TaskRunStepResult, []result.RunResult) { var taskResults []v1.TaskRunResult + var taskRunStepResults []v1.TaskRunStepResult var filteredResults []result.RunResult neededTypes := make(map[string]v1.ResultsType) + neededStepTypes := make(map[string]v1.ResultsType) for _, r := range specResults { neededTypes[r.Name] = r.Type } + for _, r := range stepResults { + neededStepTypes[r.Name] = r.Type + } for _, r := range results { switch r.ResultType { case result.TaskRunResultType: @@ -300,6 +356,28 @@ func filterResults(results []result.RunResult, specResults []v1.TaskResult) ([]v } taskResults = append(taskResults, taskRunResult) filteredResults = append(filteredResults, r) + case result.StepResultType: + var taskRunStepResult v1.TaskRunStepResult + if neededStepTypes[r.Key] == v1.ResultsTypeString { + taskRunStepResult = v1.TaskRunStepResult{ + Name: r.Key, + Type: v1.ResultsTypeString, + Value: *v1.NewStructuredValues(r.Value), + } + } else { + v := v1.ResultValue{} + err := v.UnmarshalJSON([]byte(r.Value)) + if err != nil { + continue + } + taskRunStepResult = v1.TaskRunStepResult{ + Name: r.Key, + Type: v1.ResultsType(v.Type), + Value: v, + } + } + taskRunStepResults = append(taskRunStepResults, taskRunStepResult) + filteredResults = append(filteredResults, r) case result.InternalTektonResultType: // Internal messages are ignored because they're not used as external result continue @@ -307,8 +385,7 @@ func filterResults(results []result.RunResult, specResults []v1.TaskResult) ([]v filteredResults = append(filteredResults, r) } } - - return taskResults, filteredResults + return taskResults, taskRunStepResults, filteredResults } func removeDuplicateResults(taskRunResult []v1.TaskRunResult) []v1.TaskRunResult { diff --git a/pkg/pod/status_test.go b/pkg/pod/status_test.go index 77c0176399a..4496651b661 100644 --- a/pkg/pod/status_test.go +++ b/pkg/pod/status_test.go @@ -189,6 +189,249 @@ func TestSetTaskRunStatusBasedOnStepStatus_sidecar_logs(t *testing.T) { } } +func TestMakeTaskRunStatus_StepResults(t *testing.T) { + for _, c := range []struct { + desc string + podStatus corev1.PodStatus + pod corev1.Pod + tr v1.TaskRun + want v1.TaskRunStatus + }{{ + desc: "step results string type", + podStatus: corev1.PodStatus{ + Phase: corev1.PodSucceeded, + ContainerStatuses: []corev1.ContainerStatus{{ + Name: "step-one", + State: corev1.ContainerState{ + Terminated: &corev1.ContainerStateTerminated{ + Message: `[{"key":"uri","value":"https://foo.bar\n","type":4}]`, + }, + }, + }}, + }, + tr: v1.TaskRun{ + ObjectMeta: metav1.ObjectMeta{ + Name: "task-run", + Namespace: "foo", + }, + Spec: v1.TaskRunSpec{ + TaskSpec: &v1.TaskSpec{ + Results: []v1.TaskResult{{ + Name: "task-result", + Type: v1.ResultsTypeString, + Value: &v1.ParamValue{ + Type: v1.ParamTypeString, + StringVal: "$(steps.one.results.uri)", + }, + }}, + Steps: []v1.Step{{ + Name: "one", + Results: []v1.StepResult{{ + Name: "uri", + Type: v1.ResultsTypeString, + }}, + }}, + }, + }, + }, + want: v1.TaskRunStatus{ + Status: statusSuccess(), + TaskRunStatusFields: v1.TaskRunStatusFields{ + Steps: []v1.StepState{{ + ContainerState: corev1.ContainerState{ + Terminated: &corev1.ContainerStateTerminated{ + Message: `[{"key":"uri","value":"https://foo.bar\n","type":4}]`, + }}, + Name: "one", + Container: "step-one", + Results: []v1.TaskRunStepResult{{ + Name: "uri", + Type: v1.ResultsTypeString, + Value: *v1.NewStructuredValues("https://foo.bar\n"), + }}, + }}, + Sidecars: []v1.SidecarState{}, + Results: []v1.TaskRunResult{{ + Name: "task-result", + Type: v1.ResultsTypeString, + Value: *v1.NewStructuredValues("https://foo.bar\n"), + }}, + // We don't actually care about the time, just that it's not nil + CompletionTime: &metav1.Time{Time: time.Now()}, + }, + }, + }, { + desc: "step results array type", + podStatus: corev1.PodStatus{ + Phase: corev1.PodSucceeded, + ContainerStatuses: []corev1.ContainerStatus{{ + Name: "step-one", + State: corev1.ContainerState{ + Terminated: &corev1.ContainerStateTerminated{ + Message: `[{"key":"array","value":"[\"hello\",\"world\"]","type":4}]`, + }, + }, + }}, + }, + tr: v1.TaskRun{ + ObjectMeta: metav1.ObjectMeta{ + Name: "task-run", + Namespace: "foo", + }, + Spec: v1.TaskRunSpec{ + TaskSpec: &v1.TaskSpec{ + Results: []v1.TaskResult{{ + Name: "resultName", + Type: v1.ResultsTypeArray, + Value: &v1.ParamValue{ + Type: v1.ParamTypeString, + StringVal: "$(steps.one.results.array)", + }, + }}, + Steps: []v1.Step{{ + Name: "one", + Results: []v1.StepResult{{ + Name: "array", + Type: v1.ResultsTypeArray, + }}, + }}, + }, + }, + }, + want: v1.TaskRunStatus{ + Status: statusSuccess(), + TaskRunStatusFields: v1.TaskRunStatusFields{ + Steps: []v1.StepState{{ + ContainerState: corev1.ContainerState{ + Terminated: &corev1.ContainerStateTerminated{ + Message: `[{"key":"array","value":"[\"hello\",\"world\"]","type":4}]`, + }}, + Name: "one", + Container: "step-one", + Results: []v1.TaskRunStepResult{{ + Name: "array", + Type: v1.ResultsTypeArray, + Value: *v1.NewStructuredValues("hello", "world"), + }}, + }}, + Sidecars: []v1.SidecarState{}, + Results: []v1.TaskRunResult{{ + Name: "resultName", + Type: v1.ResultsTypeArray, + Value: *v1.NewStructuredValues("hello", "world"), + }}, + // We don't actually care about the time, just that it's not nil + CompletionTime: &metav1.Time{Time: time.Now()}, + }, + }, + }, { + desc: "filter task results from step results", + podStatus: corev1.PodStatus{ + Phase: corev1.PodSucceeded, + ContainerStatuses: []corev1.ContainerStatus{{ + Name: "step-one", + State: corev1.ContainerState{ + Terminated: &corev1.ContainerStateTerminated{ + Message: `[{"key":"digest","value":"sha256:1234","type":4},{"key":"resultName","value":"resultValue","type":4}]`, + }, + }, + }}, + }, + tr: v1.TaskRun{ + ObjectMeta: metav1.ObjectMeta{ + Name: "task-run", + Namespace: "foo", + }, + Spec: v1.TaskRunSpec{ + TaskSpec: &v1.TaskSpec{ + Results: []v1.TaskResult{{ + Name: "resultDigest", + Type: v1.ResultsTypeString, + Value: &v1.ParamValue{ + Type: v1.ParamTypeString, + StringVal: "$(steps.one.results.digest)", + }, + }}, + Steps: []v1.Step{{ + Name: "one", + Results: []v1.StepResult{{ + Name: "digest", + Type: v1.ResultsTypeString, + }, { + Name: "resultName", + Type: v1.ResultsTypeString, + }}, + }}, + }, + }, + }, + want: v1.TaskRunStatus{ + Status: statusSuccess(), + TaskRunStatusFields: v1.TaskRunStatusFields{ + Steps: []v1.StepState{{ + ContainerState: corev1.ContainerState{ + Terminated: &corev1.ContainerStateTerminated{ + Message: `[{"key":"digest","value":"sha256:1234","type":4},{"key":"resultName","value":"resultValue","type":4}]`, + }}, + Name: "one", + Container: "step-one", + Results: []v1.TaskRunStepResult{{ + Name: "digest", + Type: v1.ResultsTypeString, + Value: *v1.NewStructuredValues("sha256:1234"), + }, { + Name: "resultName", + Type: v1.ResultsTypeString, + Value: *v1.NewStructuredValues("resultValue"), + }}, + }}, + Sidecars: []v1.SidecarState{}, + Results: []v1.TaskRunResult{{ + Name: "resultDigest", + Type: v1.ResultsTypeString, + Value: *v1.NewStructuredValues("sha256:1234"), + }}, + // 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() + if cmp.Diff(c.pod, corev1.Pod{}) == "" { + c.pod = corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "pod", + Namespace: "foo", + CreationTimestamp: now, + }, + Status: c.podStatus, + } + } + + logger, _ := logging.NewLogger("", "status") + kubeclient := fakek8s.NewSimpleClientset() + got, err := MakeTaskRunStatus(context.Background(), logger, c.tr, &c.pod, kubeclient, c.tr.Spec.TaskSpec) + if err != nil { + t.Errorf("MakeTaskRunResult: %s", err) + } + + // Common traits, set for test case brevity. + c.want.PodName = "pod" + + ensureTimeNotNil := cmp.Comparer(func(x, y *metav1.Time) bool { + if x == nil { + return y == nil + } + return y != nil + }) + if d := cmp.Diff(c.want, got, ignoreVolatileTime, ensureTimeNotNil); d != "" { + t.Errorf("Diff %s", diff.PrintWantGot(d)) + } + }) + } +} + func TestMakeTaskRunStatus(t *testing.T) { for _, c := range []struct { desc string @@ -1276,6 +1519,11 @@ func TestMakeTaskRunStatus(t *testing.T) { // Common traits, set for test case brevity. c.want.PodName = "pod" c.want.StartTime = &metav1.Time{Time: startTime} + for i := range c.want.TaskRunStatusFields.Steps { + if c.want.TaskRunStatusFields.Steps[i].Results == nil { + c.want.TaskRunStatusFields.Steps[i].Results = []v1.TaskRunResult{} + } + } ensureTimeNotNil := cmp.Comparer(func(x, y *metav1.Time) bool { if x == nil { @@ -1554,6 +1802,11 @@ func TestMakeTaskRunStatusAlpha(t *testing.T) { // Common traits, set for test case brevity. c.want.PodName = "pod" c.want.StartTime = &metav1.Time{Time: startTime} + for i := range c.want.TaskRunStatusFields.Steps { + if c.want.TaskRunStatusFields.Steps[i].Results == nil { + c.want.TaskRunStatusFields.Steps[i].Results = []v1.TaskRunResult{} + } + } ensureTimeNotNil := cmp.Comparer(func(x, y *metav1.Time) bool { if x == nil { @@ -1632,24 +1885,28 @@ func TestMakeRunStatusJSONError(t *testing.T) { }}, Name: "non-json", Container: "step-non-json", + Results: []v1.TaskRunResult{}, ImageID: "image", }, { ContainerState: corev1.ContainerState{ Terminated: &corev1.ContainerStateTerminated{}}, Name: "after-non-json", Container: "step-after-non-json", + Results: []v1.TaskRunResult{}, ImageID: "image", }, { ContainerState: corev1.ContainerState{ Terminated: &corev1.ContainerStateTerminated{}}, Name: "this-step-might-panic", Container: "step-this-step-might-panic", + Results: []v1.TaskRunResult{}, ImageID: "image", }, { ContainerState: corev1.ContainerState{ Terminated: &corev1.ContainerStateTerminated{}}, Name: "foo", Container: "step-foo", + Results: []v1.TaskRunResult{}, ImageID: "image", }}, Sidecars: []v1.SidecarState{}, diff --git a/pkg/reconciler/taskrun/resources/apply.go b/pkg/reconciler/taskrun/resources/apply.go index 0af13115fda..40fac760cc2 100644 --- a/pkg/reconciler/taskrun/resources/apply.go +++ b/pkg/reconciler/taskrun/resources/apply.go @@ -257,9 +257,38 @@ func applyWorkspaceMountPath(variable string, spec *v1.TaskSpec, declaration v1. return ApplyReplacements(spec, stringReplacements, emptyArrayReplacements, map[string]map[string]string{}) } -// ApplyTaskResults applies the substitution from values in results which are referenced in spec as subitems +// ApplyResults applies the substitution from values in results and step results which are referenced in spec as subitems // of the replacementStr. -func ApplyTaskResults(spec *v1.TaskSpec) *v1.TaskSpec { +func ApplyResults(spec *v1.TaskSpec) *v1.TaskSpec { + // Apply all the Step Result replacements + for i := range spec.Steps { + stringReplacements := getStepResultReplacements(spec.Steps[i], i) + container.ApplyStepReplacements(&spec.Steps[i], stringReplacements, map[string][]string{}) + } + stringReplacements := getTaskResultReplacements(spec) + return ApplyReplacements(spec, stringReplacements, map[string][]string{}, map[string]map[string]string{}) +} + +// getStepResultReplacements creates all combinations of string replacements from Step Results. +func getStepResultReplacements(step v1.Step, idx int) map[string]string { + stringReplacements := map[string]string{} + + patterns := []string{ + "step.results.%s.path", + "step.results[%q].path", + "step.results['%s'].path", + } + stepName := pod.StepName(step.Name, idx) + for _, result := range step.Results { + for _, pattern := range patterns { + stringReplacements[fmt.Sprintf(pattern, result.Name)] = filepath.Join(pipeline.StepsDir, stepName, "results", result.Name) + } + } + return stringReplacements +} + +// getTaskResultReplacements creates all combinations of string replacements from TaskResults. +func getTaskResultReplacements(spec *v1.TaskSpec) map[string]string { stringReplacements := map[string]string{} patterns := []string{ @@ -273,7 +302,7 @@ func ApplyTaskResults(spec *v1.TaskSpec) *v1.TaskSpec { stringReplacements[fmt.Sprintf(pattern, result.Name)] = filepath.Join(pipeline.DefaultResultPath, result.Name) } } - return ApplyReplacements(spec, stringReplacements, map[string][]string{}, map[string]map[string]string{}) + return stringReplacements } // ApplyStepExitCodePath replaces the occurrences of exitCode path with the absolute tekton internal path diff --git a/pkg/reconciler/taskrun/resources/apply_test.go b/pkg/reconciler/taskrun/resources/apply_test.go index 4de5ced1daa..32b825be1ad 100644 --- a/pkg/reconciler/taskrun/resources/apply_test.go +++ b/pkg/reconciler/taskrun/resources/apply_test.go @@ -1516,7 +1516,51 @@ func TestTaskResults(t *testing.T) { spec.Steps[1].Script = "#!/usr/bin/env bash\ndate | tee /tekton/results/current-date-human-readable" spec.Steps[2].Script = "#!/usr/bin/env bash\ndate | tee /tekton/results/current-date-human-readable" }) - got := resources.ApplyTaskResults(ts) + got := resources.ApplyResults(ts) + if d := cmp.Diff(want, got); d != "" { + t.Errorf("ApplyTaskResults() got diff %s", diff.PrintWantGot(d)) + } +} + +func TestStepResults(t *testing.T) { + names.TestingSeed() + ts := &v1.TaskSpec{ + Steps: []v1.Step{{ + Name: "print-date-unix-timestamp", + Results: []v1.StepResult{{ + Name: "current.date.unix.timestamp", + Description: "The current date in unix timestamp format", + }}, + Image: "bash:latest", + Args: []string{ + "$(step.results[\"current.date.unix.timestamp\"].path)", + }, + Script: "#!/usr/bin/env bash\ndate +%s | tee $(step.results[\"current.date.unix.timestamp\"].path)", + }, { + Name: "print-date-human-readable", + Results: []v1.StepResult{{ + Name: "current-date-human-readable", + Description: "The current date in humand readable format", + }}, + Image: "bash:latest", + Script: "#!/usr/bin/env bash\ndate | tee $(step.results.current-date-human-readable.path)", + }, { + Name: "print-date-human-readable-again", + Image: "bash:latest", + Results: []v1.StepResult{{ + Name: "current-date-human-readable", + Description: "The current date in humand readable format", + }}, + Script: "#!/usr/bin/env bash\ndate | tee $(step.results['current-date-human-readable'].path)", + }}, + } + want := applyMutation(ts, func(spec *v1.TaskSpec) { + spec.Steps[0].Script = "#!/usr/bin/env bash\ndate +%s | tee /tekton/steps/step-print-date-unix-timestamp/results/current.date.unix.timestamp" + spec.Steps[0].Args[0] = "/tekton/steps/step-print-date-unix-timestamp/results/current.date.unix.timestamp" + spec.Steps[1].Script = "#!/usr/bin/env bash\ndate | tee /tekton/steps/step-print-date-human-readable/results/current-date-human-readable" + spec.Steps[2].Script = "#!/usr/bin/env bash\ndate | tee /tekton/steps/step-print-date-human-readable-again/results/current-date-human-readable" + }) + got := resources.ApplyResults(ts) if d := cmp.Diff(want, got); d != "" { t.Errorf("ApplyTaskResults() got diff %s", diff.PrintWantGot(d)) } diff --git a/pkg/reconciler/taskrun/resources/taskspec.go b/pkg/reconciler/taskrun/resources/taskspec.go index cf24b19b977..255dc399c58 100644 --- a/pkg/reconciler/taskrun/resources/taskspec.go +++ b/pkg/reconciler/taskrun/resources/taskspec.go @@ -131,6 +131,9 @@ func GetStepActionsData(ctx context.Context, taskSpec v1.TaskSpec, taskRun *v1.T if len(stepActionSpec.VolumeMounts) > 0 { s.VolumeMounts = stepActionSpec.VolumeMounts } + if len(stepActionSpec.Results) > 0 { + s.Results = stepActionSpec.Results + } if err := validateStepHasStepActionParameters(s.Params, stepActionSpec.Params); err != nil { return nil, err } diff --git a/pkg/reconciler/taskrun/resources/taskspec_test.go b/pkg/reconciler/taskrun/resources/taskspec_test.go index 33e11ae9e9b..1a3479a47ad 100644 --- a/pkg/reconciler/taskrun/resources/taskspec_test.go +++ b/pkg/reconciler/taskrun/resources/taskspec_test.go @@ -421,6 +421,41 @@ func TestGetStepActionsData(t *testing.T) { Value: "value1", }}, }}, + }, { + name: "step-action-with-step-result", + tr: &v1.TaskRun{ + ObjectMeta: metav1.ObjectMeta{ + Name: "mytaskrun", + Namespace: "default", + }, + Spec: v1.TaskRunSpec{ + TaskSpec: &v1.TaskSpec{ + Steps: []v1.Step{{ + Ref: &v1.Ref{ + Name: "stepActionWithScript", + }, + }}, + }, + }, + }, + stepAction: &v1alpha1.StepAction{ + ObjectMeta: metav1.ObjectMeta{ + Name: "stepActionWithScript", + Namespace: "default", + }, + Spec: v1alpha1.StepActionSpec{ + Image: "myimage", + Script: "ls", + Results: []v1.StepResult{{ + Name: "foo", + }}, + }, + }, + want: []v1.Step{{ + Image: "myimage", + Script: "ls", + Results: []v1.StepResult{{Name: "foo", Type: "string"}}, + }}, }, { name: "inline and ref StepAction", tr: &v1.TaskRun{ diff --git a/pkg/reconciler/taskrun/taskrun.go b/pkg/reconciler/taskrun/taskrun.go index a8463fe2b25..c43a0770268 100644 --- a/pkg/reconciler/taskrun/taskrun.go +++ b/pkg/reconciler/taskrun/taskrun.go @@ -847,7 +847,7 @@ func applyParamsContextsResultsAndWorkspaces(ctx context.Context, tr *v1.TaskRun ts = resources.ApplyContexts(ts, rtr.TaskName, tr) // Apply task result substitution - ts = resources.ApplyTaskResults(ts) + ts = resources.ApplyResults(ts) // Apply step exitCode path substitution ts = resources.ApplyStepExitCodePath(ts) diff --git a/pkg/reconciler/taskrun/taskrun_test.go b/pkg/reconciler/taskrun/taskrun_test.go index 5c870577793..3796d3757cf 100644 --- a/pkg/reconciler/taskrun/taskrun_test.go +++ b/pkg/reconciler/taskrun/taskrun_test.go @@ -3114,6 +3114,37 @@ spec: Args: []string{"taskrun string param", "taskrun", "array", "taskrun", "array", "param", "taskrun object param"}, Name: "step1", }}, + }, { + name: "step results", + taskRun: parse.MustParseV1TaskRun(t, ` +metadata: + name: taskrun-with-step-results + namespace: foo +spec: + taskSpec: + steps: + - ref: + name: stepAction + name: step1 +`), + stepAction: parse.MustParseV1alpha1StepAction(t, ` +metadata: + name: stepAction + namespace: foo +spec: + results: + - name: result + image: myImage + command: ["echo"] + args: ["hi", ">>", "$(step.results.result.path)"] +`), + want: []v1.Step{{ + Image: "myImage", + Command: []string{"echo"}, + Args: []string{"hi", ">>", "/tekton/steps/step-step1/results/result"}, + Name: "step1", + Results: []v1.StepResult{{Name: "result", Type: "string"}}, + }}, }, { name: "params from taskspec", taskRun: parse.MustParseV1TaskRun(t, ` @@ -3204,8 +3235,6 @@ spec: type: string default: key: "stepaction object param" - results: - - name: result image: myImage command: ["echo"] args: ["$(params.string-param)", "$(params.array-param[0])", "$(params.array-param[1])", "$(params.array-param[*])", "$(params.object-param.key)"] diff --git a/pkg/result/result.go b/pkg/result/result.go index cfcbc3e90a2..515fe9a6025 100644 --- a/pkg/result/result.go +++ b/pkg/result/result.go @@ -33,6 +33,8 @@ const ( InternalTektonResultType = 3 // UnknownResultType default unknown result type value UnknownResultType = 10 + // StepResultType default step result value + StepResultType ResultType = 4 ) // RunResult is used to write key/value pairs to TaskRun pod termination messages. @@ -80,6 +82,8 @@ func (r *ResultType) UnmarshalJSON(data []byte) error { } switch asString { + case "StepResult": + *r = StepResultType case "TaskRunResult": *r = TaskRunResultType case "InternalTektonResult": diff --git a/pkg/result/result_test.go b/pkg/result/result_test.go index ee2e5c366c4..41006aed079 100644 --- a/pkg/result/result_test.go +++ b/pkg/result/result_test.go @@ -33,6 +33,10 @@ func TestRunResult_UnmarshalJSON(t *testing.T) { name: "type defined as string - TaskRunResult", data: "{\"key\":\"resultName\",\"value\":\"resultValue\", \"type\": \"TaskRunResult\"}", pr: RunResult{Key: "resultName", Value: "resultValue", ResultType: TaskRunResultType}, + }, { + name: "type defined as string - StepResult", + data: "{\"key\":\"resultName\",\"value\":\"resultValue\", \"type\": \"StepResult\"}", + pr: RunResult{Key: "resultName", Value: "resultValue", ResultType: StepResultType}, }, { name: "type defined as string - InternalTektonResult", diff --git a/test/clients.go b/test/clients.go index b3bcec3bd19..0a0a4f21400 100644 --- a/test/clients.go +++ b/test/clients.go @@ -68,6 +68,7 @@ type clients struct { V1TaskClient v1.TaskInterface V1TaskRunClient v1.TaskRunInterface V1PipelineRunClient v1.PipelineRunInterface + V1alpha1StepActionClient v1alpha1.StepActionInterface } // newClients instantiates and returns several clientsets required for making requests to the @@ -109,5 +110,6 @@ func newClients(t *testing.T, configPath, clusterName, namespace string) *client c.V1TaskClient = cs.TektonV1().Tasks(namespace) c.V1TaskRunClient = cs.TektonV1().TaskRuns(namespace) c.V1PipelineRunClient = cs.TektonV1().PipelineRuns(namespace) + c.V1alpha1StepActionClient = cs.TektonV1alpha1().StepActions(namespace) return c } diff --git a/test/stepaction_results_test.go b/test/stepaction_results_test.go new file mode 100644 index 00000000000..5c3fafa50a2 --- /dev/null +++ b/test/stepaction_results_test.go @@ -0,0 +1,258 @@ +//go:build e2e +// +build e2e + +/* +Copyright 2022 The Tekton Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package test + +import ( + "context" + "fmt" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/tektoncd/pipeline/pkg/apis/config" + v1 "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1" + v1alpha1 "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1alpha1" + "github.com/tektoncd/pipeline/test/parse" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "knative.dev/pkg/system" + knativetest "knative.dev/pkg/test" +) + +var ( + ignoreProvenance = cmpopts.IgnoreFields(v1.TaskRunStatusFields{}, "Provenance") + requireEnableStepActionsGate = map[string]string{ + "enable-step-actions": "true", + } +) + +func TestStepResultsStepActions(t *testing.T) { + featureFlags := getFeatureFlagsBaseOnAPIFlag(t) + previousResultExtractionMethod := featureFlags.ResultExtractionMethod + + type tests struct { + name string + taskRunFunc func(*testing.T, string) (*v1.TaskRun, *v1.TaskRun) + stepActionFunc func(*testing.T, string) *v1alpha1.StepAction + } + + tds := []tests{{ + name: "step results", + taskRunFunc: getStepActionsTaskRun, + stepActionFunc: getStepAction, + }} + + for _, td := range tds { + td := td + t.Run(td.name, func(t *testing.T) { + ctx := context.Background() + ctx, cancel := context.WithCancel(ctx) + defer cancel() + + c, namespace := setUpStepActionsResults(ctx, t) + + // reset configmap + knativetest.CleanupOnInterrupt(func() { resetStepActionsResults(ctx, t, c, previousResultExtractionMethod) }, t.Logf) + defer resetSidecarLogs(ctx, t, c, previousResultExtractionMethod) + + knativetest.CleanupOnInterrupt(func() { tearDown(ctx, t, c, namespace) }, t.Logf) + defer tearDown(ctx, t, c, namespace) + + t.Logf("Setting up test resources for %q test in namespace %s", td.name, namespace) + taskRun, expectedResolvedTaskRun := td.taskRunFunc(t, namespace) + stepAction := td.stepActionFunc(t, namespace) + + trName := taskRun.Name + + _, err := c.V1alpha1StepActionClient.Create(ctx, stepAction, metav1.CreateOptions{}) + if err != nil { + t.Fatalf("Failed to create StepAction : %s", err) + } + + _, err = c.V1TaskRunClient.Create(ctx, taskRun, metav1.CreateOptions{}) + if err != nil { + t.Fatalf("Failed to create TaskRun `%s`: %s", trName, err) + } + + t.Logf("Waiting for TaskRun %s in namespace %s to complete", trName, namespace) + if err := WaitForTaskRunState(ctx, c, trName, TaskRunSucceed(trName), "TaskRunSuccess", v1Version); err != nil { + t.Fatalf("Error waiting for TaskRun %s to finish: %s", trName, err) + } + taskrun, _ := c.V1TaskRunClient.Get(ctx, trName, metav1.GetOptions{}) + d := cmp.Diff(expectedResolvedTaskRun, taskrun, + ignoreTypeMeta, + ignoreObjectMeta, + ignoreCondition, + ignoreTaskRunStatus, + ignoreContainerStates, + ignoreProvenance, + ignoreSidecarState, + ignoreStepState, + ) + if d != "" { + t.Fatalf(`The expected taskrun does not match created taskrun. Here is the diff: %v`, d) + } + + t.Logf("Successfully finished test %q", td.name) + }) + } +} + +func getStepAction(t *testing.T, namespace string) *v1alpha1.StepAction { + t.Helper() + return parse.MustParseV1alpha1StepAction(t, fmt.Sprintf(` +metadata: + name: step-action + namespace: %s +spec: + results: + - name: result1 + type: string + image: alpine + script: | + echo -n step-action >> $(step.results.result1.path) +`, namespace)) +} + +func getStepActionsTaskRun(t *testing.T, namespace string) (*v1.TaskRun, *v1.TaskRun) { + t.Helper() + taskRun := parse.MustParseV1TaskRun(t, fmt.Sprintf(` +metadata: + name: step-results-task-run + namespace: %s +spec: + taskSpec: + steps: + - name: step1 + image: alpine + script: | + echo -n inlined-step >> $(step.results.result1.path) + results: + - name: result1 + type: string + - name: step2 + ref: + name: step-action + results: + - name: inlined-step-result + type: string + value: $(steps.step1.results.result1) + - name: step-action-result + type: string + value: $(steps.step2.results.result1) +`, namespace)) + + expectedTaskRun := parse.MustParseV1TaskRun(t, fmt.Sprintf(` +metadata: + name: step-results-task-run + namespace: %s +spec: + serviceAccountName: default + timeout: 1h + taskSpec: + steps: + - name: step1 + image: alpine + script: | + echo -n inlined-step >> $(step.results.result1.path) + results: + - name: result1 + type: string + - name: step2 + ref: + name: step-action + results: + - name: inlined-step-result + type: string + value: $(steps.step1.results.result1) + - name: step-action-result + type: string + value: $(steps.step2.results.result1) +status: + conditions: + - type: "Succeeded" + status: "True" + reason: "Succeeded" + podName: step-results-task-run-pod + taskSpec: + steps: + - name: step1 + image: alpine + results: + - name: result1 + type: string + script: | + echo -n inlined-step >> /tekton/steps/step-step1/results/result1 + - name: step2 + image: alpine + results: + - name: result1 + type: string + script: | + echo -n step-action >> /tekton/steps/step-step2/results/result1 + results: + - name: inlined-step-result + type: string + value: $(steps.step1.results.result1) + - name: step-action-result + type: string + value: $(steps.step2.results.result1) + results: + - name: inlined-step-result + type: string + value: inlined-step + - name: step-action-result + type: string + value: step-action + steps: + - name: step1 + container: step-step1 + results: + - name: result1 + type: string + value: inlined-step + - name: step2 + container: step-step2 + results: + - name: result1 + type: string + value: step-action +`, namespace)) + return taskRun, expectedTaskRun +} + +func setUpStepActionsResults(ctx context.Context, t *testing.T) (*clients, string) { + t.Helper() + c, ns := setup(ctx, t, requireAllGates(requireEnableStepActionsGate)) + configMapData := map[string]string{ + "results-from": "termination-message", + } + + if err := updateConfigMap(ctx, c.KubeClient, system.Namespace(), config.GetFeatureFlagsConfigName(), configMapData); err != nil { + t.Fatal(err) + } + return c, ns +} + +func resetStepActionsResults(ctx context.Context, t *testing.T, c *clients, previousResultExtractionMethod string) { + t.Helper() + if err := updateConfigMap(ctx, c.KubeClient, system.Namespace(), config.GetFeatureFlagsConfigName(), map[string]string{"results-from": previousResultExtractionMethod}); err != nil { + t.Fatal(err) + } +}