Skip to content

Commit

Permalink
[TEP-0075] Support Object Results substitution
Browse files Browse the repository at this point in the history
This is part of work in TEP-0075.
This commit provides the support to apply object results replacements.
Previous this commit we support emitting object results so users can
write object results to task level, but we cannot pass object results from
one task to another within one pipeline. This commit adds the support for this.
  • Loading branch information
Yongxuanzhang authored and tekton-robot committed Jul 8, 2022
1 parent f93ff6f commit dc9ea6c
Show file tree
Hide file tree
Showing 7 changed files with 185 additions and 5 deletions.
15 changes: 13 additions & 2 deletions docs/pipelines.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.<task-name>.results.<result-name>)`. Array `Results` is supported as alpha feature and
can be referer as `$(tasks.<task-name>.results.<result-name>[*])`.
can be referred as `$(tasks.<task-name>.results.<result-name>[*])`. Array indexing can be rererred
as `$(tasks.<task-name>.results.<result-name>[i])` where `i` is the index.
Object `Results` is supported as alpha feature and can be referred as
`$(tasks.<task-name>.results.<result-name>[*])`, object elements can be referred as
`$(tasks.<task-name>.results.<result-name>.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`,
Expand All @@ -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`,
Expand Down
3 changes: 3 additions & 0 deletions docs/variables.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@ For instructions on using variable substitutions see the relevant section of [th
| `tasks.<taskName>.results.<resultName>[*]` | The array value of the `Task's` result. Can alter `Task` execution order within a `Pipeline`. Cannot be used in `script`.) |
| `tasks.<taskName>.results['<resultName>'][*]` | (see above)) |
| `tasks.<taskName>.results["<resultName>"][*]` | (see above)) |
| `tasks.<taskName>.results.<resultName>.key` | The `key` value of the `Task's` object result. Can alter `Task` execution order within a `Pipeline`.) |
| `tasks.<taskName>.results['<resultName>'][key]` | (see above)) |
| `tasks.<taskName>.results["<resultName>"][key]` | (see above)) |
| `workspaces.<workspaceName>.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. |
Expand Down
67 changes: 67 additions & 0 deletions examples/v1beta1/pipelineruns/alpha/pipeline-object-results.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
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-object-foo
image: bash:latest
args: [
"echo",
"$(params.whole-object.foo)"
]
- name: print-object-hello
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
5 changes: 3 additions & 2 deletions pkg/reconciler/pipelinerun/resources/apply.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
71 changes: 71 additions & 0 deletions pkg/reconciler/pipelinerun/resources/apply_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{{
Expand Down
27 changes: 27 additions & 0 deletions pkg/reconciler/pipelinerun/resources/resultrefresolution.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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),
Expand All @@ -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),
}
}
2 changes: 1 addition & 1 deletion pkg/reconciler/taskrun/validate_resources.go
Original file line number Diff line number Diff line change
Expand Up @@ -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] {
Expand Down

0 comments on commit dc9ea6c

Please sign in to comment.