diff --git a/docs/pipelines.md b/docs/pipelines.md index 80df548c3c5..b442c83638b 100644 --- a/docs/pipelines.md +++ b/docs/pipelines.md @@ -909,9 +909,14 @@ Sharing `Results` between `Tasks` in a `Pipeline` happens via [variable substitution](variables.md#variables-available-in-a-pipeline) - one `Task` emits a `Result` and another receives it as a `Parameter` with a variable such as `$(tasks..results.)`. Array `Results` is supported as alpha feature and -can be referer as `$(tasks..results.[*])`. +can be referred as `$(tasks..results.[*])`. Array indexing can be rererred +as `$(tasks..results.[i])` where `i` is the index. +Object `Results` is supported as alpha feature and can be referred as +`$(tasks..results.[*])`, object elements can be referred as +`$(tasks..results..key)`. -**Note:** Array `Result` cannot be used in `script`. +**Note:** Whole Array and Object `Results` cannot be referred in `script` and `args`. +**Note:** `Matrix` does not support `object` and `array` results. When one `Task` receives the `Results` of another, there is a dependency created between those two `Tasks`. In order for the receiving `Task` to get data from another `Task's` `Result`, @@ -928,6 +933,12 @@ params: value: "$(tasks.checkout-source.results.commit)" - name: array-params value: "$(tasks.checkout-source.results.array-results[*])" + - name: array-indexing-params + value: "$(tasks.checkout-source.results.array-results[1])" + - name: object-params + value: "$(tasks.checkout-source.results.object-results[*])" + - name: object-element-params + value: "$(tasks.checkout-source.results.object-results.objectkey)" ``` **Note:** If `checkout-source` exits successfully without initializing `commit` `Result`, diff --git a/docs/variables.md b/docs/variables.md index 3f99ca7e389..acab2fc722f 100644 --- a/docs/variables.md +++ b/docs/variables.md @@ -28,6 +28,9 @@ For instructions on using variable substitutions see the relevant section of [th | `tasks..results.[*]` | The array value of the `Task's` result. Can alter `Task` execution order within a `Pipeline`. Cannot be used in `script`.) | | `tasks..results[''][*]` | (see above)) | | `tasks..results[""][*]` | (see above)) | +| `tasks..results..key` | The `key` value of the `Task's` object result. Can alter `Task` execution order within a `Pipeline`.) | +| `tasks..results[''][key]` | (see above)) | +| `tasks..results[""][key]` | (see above)) | | `workspaces..bound` | Whether a `Workspace` has been bound or not. "false" if the `Workspace` declaration has `optional: true` and the Workspace binding was omitted by the PipelineRun. | | `context.pipelineRun.name` | The name of the `PipelineRun` that this `Pipeline` is running in. | | `context.pipelineRun.namespace` | The namespace of the `PipelineRun` that this `Pipeline` is running in. | diff --git a/examples/v1beta1/pipelineruns/alpha/pipeline-object-results.yaml b/examples/v1beta1/pipelineruns/alpha/pipeline-object-results.yaml new file mode 100644 index 00000000000..372b028425d --- /dev/null +++ b/examples/v1beta1/pipelineruns/alpha/pipeline-object-results.yaml @@ -0,0 +1,71 @@ +apiVersion: tekton.dev/v1beta1 +kind: PipelineRun +metadata: + name: pipelinerun-object-results +spec: + pipelineSpec: + tasks: + - name: task1 + taskSpec: + results: + - name: object-results + type: object + description: The object results + properties: + foo: { + type: string + } + hello: { + type: string + } + steps: + - name: write-object + image: bash:latest + script: | + #!/usr/bin/env bash + echo -n "{\"foo\":\"bar\",\"hello\":\"world\"}" | tee $(results.object-results.path) + - name: task2 + params: + - name: whole-object + value: "$(tasks.task1.results.object-results[*])" + - name: object-element + value: "$(tasks.task1.results.object-results.hello)" + taskSpec: + params: + - name: whole-object + type: object + properties: + foo: { + type: string + } + hello: { + type: string + } + default: { + hello: "", + foo: "", + } + - name: object-element + type: string + default: "defaultparam1" + steps: + - name: print-whole-object + image: bash:latest + args: [ + "echo", + "$(params.whole-object.foo)" + ] + - name: print-object-element + image: ubuntu + script: | + #!/bin/bash + VALUE=$(params.object-element) + EXPECTED="world" + diff=$(diff <(printf "%s\n" "${VALUE[@]}") <(printf "%s\n" "${EXPECTED[@]}")) + if [[ -z "$diff" ]]; then + echo "Get expected: ${VALUE}" + exit 0 + else + echo "Want: ${EXPECTED} Got: ${VALUE}" + exit 1 + fi diff --git a/pkg/reconciler/pipelinerun/resources/apply.go b/pkg/reconciler/pipelinerun/resources/apply.go index 7532a2f1ba3..254207bb4f5 100644 --- a/pkg/reconciler/pipelinerun/resources/apply.go +++ b/pkg/reconciler/pipelinerun/resources/apply.go @@ -123,11 +123,12 @@ func ApplyPipelineTaskContexts(pt *v1beta1.PipelineTask) *v1beta1.PipelineTask { func ApplyTaskResults(targets PipelineRunState, resolvedResultRefs ResolvedResultRefs) { stringReplacements := resolvedResultRefs.getStringReplacements() arrayReplacements := resolvedResultRefs.getArrayReplacements() + objectReplacements := resolvedResultRefs.getObjectReplacements() for _, resolvedPipelineRunTask := range targets { if resolvedPipelineRunTask.PipelineTask != nil { pipelineTask := resolvedPipelineRunTask.PipelineTask.DeepCopy() - pipelineTask.Params = replaceParamValues(pipelineTask.Params, stringReplacements, arrayReplacements, nil) - pipelineTask.Matrix = replaceParamValues(pipelineTask.Matrix, stringReplacements, arrayReplacements, nil) + pipelineTask.Params = replaceParamValues(pipelineTask.Params, stringReplacements, arrayReplacements, objectReplacements) + pipelineTask.Matrix = replaceParamValues(pipelineTask.Matrix, stringReplacements, nil, nil) pipelineTask.WhenExpressions = pipelineTask.WhenExpressions.ReplaceWhenExpressionsVariables(stringReplacements, arrayReplacements) resolvedPipelineRunTask.PipelineTask = pipelineTask } diff --git a/pkg/reconciler/pipelinerun/resources/apply_test.go b/pkg/reconciler/pipelinerun/resources/apply_test.go index 6771d936f66..db96a859d64 100644 --- a/pkg/reconciler/pipelinerun/resources/apply_test.go +++ b/pkg/reconciler/pipelinerun/resources/apply_test.go @@ -1083,6 +1083,77 @@ func TestApplyTaskResults_MinimalExpression(t *testing.T) { }}, }, }}, + }, { + name: "Test object result as a whole substitution - params", + resolvedResultRefs: ResolvedResultRefs{{ + Value: *v1beta1.NewObject(map[string]string{ + "key1": "val1", + "key2": "val2", + }), + ResultReference: v1beta1.ResultRef{ + PipelineTask: "aTask", + Result: "resultName", + }, + FromTaskRun: "aTaskRun", + }}, + targets: PipelineRunState{{ + PipelineTask: &v1beta1.PipelineTask{ + Name: "bTask", + TaskRef: &v1beta1.TaskRef{Name: "bTask"}, + Params: []v1beta1.Param{{ + Name: "bParam", + Value: *v1beta1.NewArrayOrString(`$(tasks.aTask.results.resultName[*])`), + }}, + }, + }}, + want: PipelineRunState{{ + PipelineTask: &v1beta1.PipelineTask{ + Name: "bTask", + TaskRef: &v1beta1.TaskRef{Name: "bTask"}, + Params: []v1beta1.Param{{ + Name: "bParam", + // index validation is done in ResolveResultRefs() before ApplyTaskResults() + Value: *v1beta1.NewObject(map[string]string{ + "key1": "val1", + "key2": "val2", + }), + }}, + }, + }}, + }, { + name: "Test object result element substitution - params", + resolvedResultRefs: ResolvedResultRefs{{ + Value: *v1beta1.NewObject(map[string]string{ + "key1": "val1", + "key2": "val2", + }), + ResultReference: v1beta1.ResultRef{ + PipelineTask: "aTask", + Result: "resultName", + }, + FromTaskRun: "aTaskRun", + }}, + targets: PipelineRunState{{ + PipelineTask: &v1beta1.PipelineTask{ + Name: "bTask", + TaskRef: &v1beta1.TaskRef{Name: "bTask"}, + Params: []v1beta1.Param{{ + Name: "bParam", + Value: *v1beta1.NewArrayOrString(`$(tasks.aTask.results.resultName.key1)`), + }}, + }, + }}, + want: PipelineRunState{{ + PipelineTask: &v1beta1.PipelineTask{ + Name: "bTask", + TaskRef: &v1beta1.TaskRef{Name: "bTask"}, + Params: []v1beta1.Param{{ + Name: "bParam", + // index validation is done in ResolveResultRefs() before ApplyTaskResults() + Value: *v1beta1.NewArrayOrString("val1"), + }}, + }, + }}, }, { name: "Test result substitution on minimal variable substitution expression - matrix", resolvedResultRefs: ResolvedResultRefs{{ diff --git a/pkg/reconciler/pipelinerun/resources/resultrefresolution.go b/pkg/reconciler/pipelinerun/resources/resultrefresolution.go index 69014de59ce..73d41f64c81 100644 --- a/pkg/reconciler/pipelinerun/resources/resultrefresolution.go +++ b/pkg/reconciler/pipelinerun/resources/resultrefresolution.go @@ -206,6 +206,13 @@ func (rs ResolvedResultRefs) getStringReplacements() map[string]string { replacements[target] = r.Value.ArrayVal[i] } } + case v1beta1.ParamTypeObject: + for key, element := range r.Value.ObjectVal { + for _, target := range r.getReplaceTargetfromObjectKey(key) { + replacements[target] = element + } + } + default: for _, target := range r.getReplaceTarget() { replacements[target] = r.Value.StringVal @@ -227,6 +234,18 @@ func (rs ResolvedResultRefs) getArrayReplacements() map[string][]string { return replacements } +func (rs ResolvedResultRefs) getObjectReplacements() map[string]map[string]string { + replacements := map[string]map[string]string{} + for _, r := range rs { + if r.Value.Type == v1beta1.ParamType(v1beta1.ResultsTypeObject) { + for _, target := range r.getReplaceTarget() { + replacements[target] = r.Value.ObjectVal + } + } + } + return replacements +} + func (r *ResolvedResultRef) getReplaceTarget() []string { return []string{ fmt.Sprintf("%s.%s.%s.%s", v1beta1.ResultTaskPart, r.ResultReference.PipelineTask, v1beta1.ResultResultPart, r.ResultReference.Result), @@ -242,3 +261,11 @@ func (r *ResolvedResultRef) getReplaceTargetfromArrayIndex(idx int) []string { fmt.Sprintf("%s.%s.%s['%s'][%d]", v1beta1.ResultTaskPart, r.ResultReference.PipelineTask, v1beta1.ResultResultPart, r.ResultReference.Result, idx), } } + +func (r *ResolvedResultRef) getReplaceTargetfromObjectKey(key string) []string { + return []string{ + fmt.Sprintf("%s.%s.%s.%s.%s", v1beta1.ResultTaskPart, r.ResultReference.PipelineTask, v1beta1.ResultResultPart, r.ResultReference.Result, key), + fmt.Sprintf("%s.%s.%s[%q][%s]", v1beta1.ResultTaskPart, r.ResultReference.PipelineTask, v1beta1.ResultResultPart, r.ResultReference.Result, key), + fmt.Sprintf("%s.%s.%s['%s'][%s]", v1beta1.ResultTaskPart, r.ResultReference.PipelineTask, v1beta1.ResultResultPart, r.ResultReference.Result, key), + } +} diff --git a/pkg/reconciler/taskrun/validate_resources.go b/pkg/reconciler/taskrun/validate_resources.go index a25c2517940..d6c65d6c180 100644 --- a/pkg/reconciler/taskrun/validate_resources.go +++ b/pkg/reconciler/taskrun/validate_resources.go @@ -146,7 +146,7 @@ func wrongTypeParamsNames(params []v1beta1.Param, matrix []v1beta1.Param, needed // to pass array result to array param, yet in yaml format this will be // unmarshalled to string for ArrayOrString. So we need to check and skip this validation. // Please refer issue #4879 for more details and examples. - if param.Value.Type == v1beta1.ParamTypeString && (neededParamsTypes[param.Name] == v1beta1.ParamTypeArray || (neededParamsTypes[param.Name] == v1beta1.ParamTypeObject)) && v1beta1.VariableSubstitutionRegex.MatchString(param.Value.StringVal) { + if param.Value.Type == v1beta1.ParamTypeString && (neededParamsTypes[param.Name] == v1beta1.ParamTypeArray || neededParamsTypes[param.Name] == v1beta1.ParamTypeObject) && v1beta1.VariableSubstitutionRegex.MatchString(param.Value.StringVal) { continue } if param.Value.Type != neededParamsTypes[param.Name] {