Skip to content

Commit 650f3c3

Browse files
committed
[TEP-0076]Support Array Results substitution
This is part of work in TEP-0076. This commit provides the support to apply array results replacements. Previous this commit we support emitting array results so users can write array results to task level, but we cannot pass array results from tasks within one pipeline. This commit adds the support for this.
1 parent df074f2 commit 650f3c3

File tree

14 files changed

+255
-32
lines changed

14 files changed

+255
-32
lines changed

docs/pipelines.md

Lines changed: 28 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ A `Pipeline` definition supports the following fields:
9595
a `Task` requires.
9696
- [`from`](#using-the-from-field) - Indicates the data for a [`PipelineResource`](resources.md)
9797
originates from the output of a previous `Task`.
98-
- [`runAfter`](#using-the-runafter-field) - Indicates that a `Task` should execute after one or more other
98+
- [`runAfter`](#using-the-runafter-field) - Indicates that a `Task` should execute after one or more other
9999
`Tasks` without output linking.
100100
- [`retries`](#using-the-retries-field) - Specifies the number of times to retry the execution of a `Task` after
101101
a failure. Does not apply to execution cancellations.
@@ -109,7 +109,7 @@ A `Pipeline` definition supports the following fields:
109109
- [`results`](#emitting-results-from-a-pipeline) - Specifies the location to which the `Pipeline` emits its execution
110110
results.
111111
- [`description`](#adding-a-description) - Holds an informative description of the `Pipeline` object.
112-
- [`finally`](#adding-finally-to-the-pipeline) - Specifies one or more `Tasks` to be executed in parallel after
112+
- [`finally`](#adding-finally-to-the-pipeline) - Specifies one or more `Tasks` to be executed in parallel after
113113
all other tasks have completed.
114114
- [`name`](#adding-finally-to-the-pipeline) - the name of this `Task` within the context of this `Pipeline`.
115115
- [`taskRef`](#adding-finally-to-the-pipeline) - a reference to a `Task` definition.
@@ -174,7 +174,7 @@ spec:
174174
workspace: pipeline-ws1
175175
```
176176

177-
For simplicity you can also map the name of the `Workspace` in `PipelineTask` to match with
177+
For simplicity you can also map the name of the `Workspace` in `PipelineTask` to match with
178178
the `Workspace` from the `Pipeline`.
179179
For example:
180180

@@ -191,12 +191,12 @@ spec:
191191
taskRef:
192192
name: gen-code # gen-code expects a Workspace named "source"
193193
workspaces:
194-
- name: source # <- mapping workspace name
194+
- name: source # <- mapping workspace name
195195
- name: commit
196196
taskRef:
197197
name: commit # commit expects a Workspace named "source"
198198
workspaces:
199-
- name: source # <- mapping workspace name
199+
- name: source # <- mapping workspace name
200200
runAfter:
201201
- gen-code
202202
```
@@ -402,7 +402,7 @@ spec:
402402
`"true"` in the `feature-flags` configmap, see [`install.md`](./install.md#customizing-the-pipelines-controller-behavior)**
403403

404404
You may also specify your `Task` reference using a `Tekton Bundle`. A `Tekton Bundle` is an OCI artifact that
405-
contains Tekton resources like `Tasks` which can be referenced within a `taskRef`.
405+
contains Tekton resources like `Tasks` which can be referenced within a `taskRef`.
406406

407407
There is currently a hard limit of 20 objects in a bundle.
408408

@@ -628,7 +628,7 @@ To guard a `Task` and its dependent Tasks:
628628

629629
##### Cascade `when` expressions to the specific dependent `Tasks`
630630

631-
Pick and choose which specific dependent `Tasks` to guard as well, and cascade the `when` expressions to those `Tasks`.
631+
Pick and choose which specific dependent `Tasks` to guard as well, and cascade the `when` expressions to those `Tasks`.
632632

633633
Taking the use case below, a user who wants to guard `manual-approval` and its dependent `Tasks`:
634634

@@ -689,12 +689,12 @@ tasks:
689689
value: $(tasks.manual-approval.results.approver)
690690
taskRef:
691691
name: slack-msg
692-
```
692+
```
693693

694694
##### Compose using Pipelines in Pipelines
695695

696-
Compose a set of `Tasks` as a unit of execution using `Pipelines` in `Pipelines`, which allows for guarding a `Task` and
697-
its dependent `Tasks` (as a sub-`Pipeline`) using `when` expressions.
696+
Compose a set of `Tasks` as a unit of execution using `Pipelines` in `Pipelines`, which allows for guarding a `Task` and
697+
its dependent `Tasks` (as a sub-`Pipeline`) using `when` expressions.
698698

699699
**Note:** `Pipelines` in `Pipelines` is an [experimental feature](https://github.com/tektoncd/experimental/tree/main/pipelines-in-pipelines)
700700

@@ -742,7 +742,7 @@ tasks:
742742
value: $(tasks.manual-approval.results.approver)
743743
taskRef:
744744
name: slack-msg
745-
745+
746746
---
747747
## main pipeline
748748
tasks:
@@ -765,12 +765,12 @@ tasks:
765765

766766
When `when` expressions evaluate to `False`, the `Task` will be skipped and:
767767
- The ordering-dependent `Tasks` will be executed
768-
- The resource-dependent `Tasks` (and their dependencies) will be skipped because of missing `Results` from the skipped
769-
parent `Task`. When we add support for [default `Results`](https://github.com/tektoncd/community/pull/240), then the
770-
resource-dependent `Tasks` may be executed if the default `Results` from the skipped parent `Task` are specified. In
768+
- The resource-dependent `Tasks` (and their dependencies) will be skipped because of missing `Results` from the skipped
769+
parent `Task`. When we add support for [default `Results`](https://github.com/tektoncd/community/pull/240), then the
770+
resource-dependent `Tasks` may be executed if the default `Results` from the skipped parent `Task` are specified. In
771771
addition, if a resource-dependent `Task` needs a file from a guarded parent `Task` in a shared `Workspace`, make sure
772-
to handle the execution of the child `Task` in case the expected file is missing from the `Workspace` because the
773-
guarded parent `Task` is skipped.
772+
to handle the execution of the child `Task` in case the expected file is missing from the `Workspace` because the
773+
guarded parent `Task` is skipped.
774774

775775
On the other hand, the rest of the `Pipeline` will continue executing.
776776

@@ -823,12 +823,12 @@ tasks:
823823
name: slack-msg
824824
```
825825

826-
If `manual-approval` is skipped, execution of its dependent `Tasks` (`slack-msg`, `build-image` and `deploy-image`)
826+
If `manual-approval` is skipped, execution of its dependent `Tasks` (`slack-msg`, `build-image` and `deploy-image`)
827827
would be unblocked regardless:
828828
- `build-image` and `deploy-image` should be executed successfully
829829
- `slack-msg` will be skipped because it is missing the `approver` `Result` from `manual-approval`
830830
- dependents of `slack-msg` would have been skipped too if it had any of them
831-
- if `manual-approval` specifies a default `approver` `Result`, such as "None", then `slack-msg` would be executed
831+
- if `manual-approval` specifies a default `approver` `Result`, such as "None", then `slack-msg` would be executed
832832
([supporting default `Results` is in progress](https://github.com/tektoncd/community/pull/240))
833833

834834
### Configuring the failure timeout
@@ -908,7 +908,10 @@ Tasks can emit [`Results`](tasks.md#emitting-results) when they execute. A Pipel
908908
Sharing `Results` between `Tasks` in a `Pipeline` happens via
909909
[variable substitution](variables.md#variables-available-in-a-pipeline) - one `Task` emits
910910
a `Result` and another receives it as a `Parameter` with a variable such as
911-
`$(tasks.<task-name>.results.<result-name>)`.
911+
`$(tasks.<task-name>.results.<result-name>)`. Array `Results` is supported as alpha feature and
912+
can be referer as `$(tasks.<task-name>.results.<result-name>[*])`.
913+
914+
**Note:** Array `Result` cannot be used in `script`.
912915

913916
When one `Task` receives the `Results` of another, there is a dependency created between those
914917
two `Tasks`. In order for the receiving `Task` to get data from another `Task's` `Result`,
@@ -923,6 +926,8 @@ before this one.
923926
params:
924927
- name: foo
925928
value: "$(tasks.checkout-source.results.commit)"
929+
- name: array-params
930+
value: "$(tasks.checkout-source.results.array-results[*])"
926931
```
927932

928933
**Note:** If `checkout-source` exits successfully without initializing `commit` `Result`,
@@ -944,7 +949,7 @@ when:
944949

945950
For an end-to-end example, see [`Task` `Results` in a `PipelineRun`](../examples/v1beta1/pipelineruns/task_results_example.yaml).
946951

947-
Note that `when` expressions are whitespace-sensitive. In particular, when producing results intended for inputs to `when`
952+
Note that `when` expressions are whitespace-sensitive. In particular, when producing results intended for inputs to `when`
948953
expressions that may include newlines at their close (e.g. `cat`, `jq`), you may wish to truncate them.
949954

950955
```yaml
@@ -1009,9 +1014,9 @@ without getting stuck in an infinite loop.
10091014
This is done using:
10101015
- _resource dependencies_:
10111016
- [`from`](#using-the-from-field) clauses on the [`PipelineResources`](resources.md) used by each `Task`
1012-
- [`results`](#emitting-results-from-a-pipeline) of one `Task` being passed into `params` or `when` expressions of
1017+
- [`results`](#emitting-results-from-a-pipeline) of one `Task` being passed into `params` or `when` expressions of
10131018
another
1014-
1019+
10151020
- _ordering dependencies_:
10161021
- [`runAfter`](#using-the-runafter-field) clauses on the corresponding `Tasks`
10171022

@@ -1197,7 +1202,7 @@ spec:
11971202
value: "someURL"
11981203
matrix:
11991204
- name: slack-channel
1200-
value:
1205+
value:
12011206
- "foo"
12021207
- "bar"
12031208
```

docs/variables.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,9 @@ For instructions on using variable substitutions see the relevant section of [th
2525
| `tasks.<taskName>.results.<resultName>[i]` | The ith value of the `Task's` array result. Can alter `Task` execution order within a `Pipeline`.) |
2626
| `tasks.<taskName>.results['<resultName>'][i]` | (see above)) |
2727
| `tasks.<taskName>.results["<resultName>"][i]` | (see above)) |
28+
| `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`.) |
29+
| `tasks.<taskName>.results['<resultName>'][*]` | (see above)) |
30+
| `tasks.<taskName>.results["<resultName>"][*]` | (see above)) |
2831
| `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. |
2932
| `context.pipelineRun.name` | The name of the `PipelineRun` that this `Pipeline` is running in. |
3033
| `context.pipelineRun.namespace` | The namespace of the `PipelineRun` that this `Pipeline` is running in. |
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
apiVersion: tekton.dev/v1beta1
2+
kind: PipelineRun
3+
metadata:
4+
name: pipelinerun-array-results
5+
spec:
6+
pipelineSpec:
7+
tasks:
8+
- name: task1
9+
taskSpec:
10+
results:
11+
- name: array-results
12+
type: array
13+
description: The array results
14+
steps:
15+
- name: write-array
16+
image: bash:latest
17+
script: |
18+
#!/usr/bin/env bash
19+
echo -n "[\"1\",\"2\",\"3\"]" | tee $(results.array-results.path)
20+
- name: task2
21+
params:
22+
- name: foo
23+
value: "$(tasks.task1.results.array-results[*])"
24+
- name: bar
25+
value: "$(tasks.task1.results.array-results[2])"
26+
taskSpec:
27+
params:
28+
- name: foo
29+
type: array
30+
default:
31+
- "defaultparam1"
32+
- "defaultparam2"
33+
- name: bar
34+
type: string
35+
default: "defaultparam1"
36+
steps:
37+
- name: print-foo
38+
image: bash:latest
39+
args: [
40+
"echo",
41+
"$(params.foo[*])"
42+
]
43+
- name: print-bar
44+
image: ubuntu
45+
script: |
46+
#!/bin/bash
47+
VALUE=$(params.bar)
48+
EXPECTED=3
49+
diff=$(diff <(printf "%s\n" "${VALUE[@]}") <(printf "%s\n" "${EXPECTED[@]}"))
50+
if [[ -z "$diff" ]]; then
51+
echo "Get expected: ${VALUE}"
52+
exit 0
53+
else
54+
echo "Want: ${EXPECTED} Got: ${VALUE}"
55+
exit 1
56+
fi

pkg/apis/pipeline/v1beta1/param_types.go

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -234,7 +234,6 @@ func (arrayOrString *ArrayOrString) applyOrCorrect(stringReplacements map[string
234234
if _, ok := stringReplacements[trimedStringVal]; ok {
235235
arrayOrString.StringVal = substitution.ApplyReplacements(arrayOrString.StringVal, stringReplacements)
236236
}
237-
238237
// if the stringVal is a reference to an array param, we need to change the type other than apply replacement
239238
if _, ok := arrayReplacements[trimedStringVal]; ok {
240239
arrayOrString.StringVal = ""

pkg/apis/pipeline/v1beta1/result_types.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ limitations under the License.
1313

1414
package v1beta1
1515

16+
import "strings"
17+
1618
// TaskResult used to describe the results of a task
1719
type TaskResult struct {
1820
// Name the given name
@@ -60,3 +62,8 @@ const (
6062

6163
// AllResultsTypes can be used for ResultsTypes validation.
6264
var AllResultsTypes = []ResultsType{ResultsTypeString, ResultsTypeArray, ResultsTypeObject}
65+
66+
// ResultsArrayReference returns the reference of the result. e.g. results.resultname[*] from $(results.resultname[*])
67+
func ResultsArrayReference(a string) string {
68+
return strings.TrimSuffix(strings.TrimSuffix(strings.TrimPrefix(a, "$("), ")"), "[*]")
69+
}

pkg/apis/pipeline/v1beta1/resultref.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,8 @@ const (
5353
ResultNameFormat = `^([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9]$`
5454
)
5555

56-
var variableSubstitutionRegex = regexp.MustCompile(variableSubstitutionFormat)
56+
// VariableSubstitutionRegex is a regex to find all result matching substitutions
57+
var VariableSubstitutionRegex = regexp.MustCompile(variableSubstitutionFormat)
5758
var exactVariableSubstitutionRegex = regexp.MustCompile(exactVariableSubstitutionFormat)
5859
var resultNameFormatRegex = regexp.MustCompile(ResultNameFormat)
5960
var arrayIndexingRegex = regexp.MustCompile(arrayIndexing)
@@ -129,7 +130,7 @@ func GetVarSubstitutionExpressionsForPipelineResult(result PipelineResult) ([]st
129130
}
130131

131132
func validateString(value string) []string {
132-
expressions := variableSubstitutionRegex.FindAllString(value, -1)
133+
expressions := VariableSubstitutionRegex.FindAllString(value, -1)
133134
if expressions == nil {
134135
return nil
135136
}

pkg/apis/pipeline/v1beta1/when_types.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,9 +62,12 @@ func (we *WhenExpression) applyReplacements(replacements map[string]string, arra
6262
for _, val := range we.Values {
6363
// arrayReplacements holds a list of array parameters with a pattern - params.arrayParam1
6464
// array params are referenced using $(params.arrayParam1[*])
65+
// array results are referenced using $(results.resultname[*])
6566
// check if the param exist in the arrayReplacements to replace it with a list of values
6667
if _, ok := arrayReplacements[fmt.Sprintf("%s.%s", ParamsPrefix, ArrayReference(val))]; ok {
6768
replacedValues = append(replacedValues, substitution.ApplyArrayReplacements(val, replacements, arrayReplacements)...)
69+
} else if _, ok := arrayReplacements[ResultsArrayReference(val)]; ok {
70+
replacedValues = append(replacedValues, substitution.ApplyArrayReplacements(val, replacements, arrayReplacements)...)
6871
} else {
6972
replacedValues = append(replacedValues, substitution.ApplyReplacements(val, replacements))
7073
}

pkg/apis/pipeline/v1beta1/when_types_test.go

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -248,6 +248,43 @@ func TestApplyReplacements(t *testing.T) {
248248
Operator: selection.In,
249249
Values: []string{"barfoo"},
250250
},
251+
}, {
252+
name: "replace array results variables",
253+
original: &WhenExpression{
254+
Input: "$(tasks.foo.results.bar)",
255+
Operator: selection.In,
256+
Values: []string{"$(tasks.aTask.results.aResult[*])"},
257+
},
258+
replacements: map[string]string{
259+
"tasks.foo.results.bar": "foobar",
260+
},
261+
arrayReplacements: map[string][]string{
262+
"tasks.aTask.results.aResult": {"dev", "stage"},
263+
},
264+
expected: &WhenExpression{
265+
Input: "foobar",
266+
Operator: selection.In,
267+
Values: []string{"dev", "stage"},
268+
},
269+
}, {
270+
name: "invaliad array results replacements",
271+
original: &WhenExpression{
272+
Input: "$(tasks.foo.results.bar)",
273+
Operator: selection.In,
274+
Values: []string{"$(tasks.aTask.results.aResult[invalid])"},
275+
},
276+
replacements: map[string]string{
277+
"tasks.foo.results.bar": "foobar",
278+
"tasks.aTask.results.aResult[*]": "barfoo",
279+
},
280+
arrayReplacements: map[string][]string{
281+
"tasks.aTask.results.aResult[*]": {"dev", "stage"},
282+
},
283+
expected: &WhenExpression{
284+
Input: "foobar",
285+
Operator: selection.In,
286+
Values: []string{"$(tasks.aTask.results.aResult[invalid])"},
287+
},
251288
}, {
252289
name: "replace array params",
253290
original: &WhenExpression{

0 commit comments

Comments
 (0)