Skip to content

Commit afacd43

Browse files
committed
[TEP-0076] Add array support for emitting results
This commit provides support for emitting array results via changing TaskRunResult value from string to ArrayorString and add ResultValue for PipelineResourceResult. Previous to this commit we can only emit string type result.
1 parent 1c8447d commit afacd43

22 files changed

+574
-79
lines changed

docs/tasks.md

Lines changed: 38 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -330,7 +330,7 @@ steps:
330330
echo "I am supposed to sleep for 60 seconds!"
331331
sleep 60
332332
timeout: 5s
333-
```
333+
```
334334

335335
#### Specifying `onError` for a `step`
336336

@@ -450,7 +450,7 @@ Parameter names:
450450

451451
For example, `foo.Is-Bar_` is a valid parameter name, but `barIsBa$` or `0banana` are not.
452452

453-
> NOTE:
453+
> NOTE:
454454
> 1. Parameter names are **case insensitive**. For example, `APPLE` and `apple` will be treated as equal. If they appear in the same TaskSpec's params, it will be rejected as invalid.
455455
> 2. If a parameter name contains dots (.), it must be referenced by using the [bracket notation](#substituting-parameters-and-resources) with either single or double quotes i.e. `$(params['foo.bar'])`, `$(params["foo.bar"])`. See the following example for more information.
456456

@@ -684,6 +684,30 @@ or [at the `Pipeline` level](./pipelines.md#configuring-execution-results-at-the
684684
**Note:** The maximum size of a `Task's` results is limited by the container termination message feature of Kubernetes,
685685
as results are passed back to the controller via this mechanism. At present, the limit is
686686
["4096 bytes"](https://github.com/kubernetes/kubernetes/blob/96e13de777a9eb57f87889072b68ac40467209ac/pkg/kubelet/container/runtime.go#L632).
687+
688+
**Note:** The result type currently support `string` and `array` (`array` is alpha gated feature), you can write `array` results via JSON escaped format. In the example below, the task specifies one files in the `results` field and write `array` to the file. And `array` is currently supported in Task level not in Pipeline level.
689+
690+
```
691+
kind: Task
692+
apiVersion: tekton.dev/v1beta1
693+
metadata:
694+
name: write-array
695+
annotations:
696+
description: |
697+
A simple task that writes array
698+
spec:
699+
results:
700+
- name: array-results
701+
type: array
702+
description: The array results
703+
steps:
704+
- name: write-array
705+
image: bash:latest
706+
script: |
707+
#!/usr/bin/env bash
708+
echo -n "[\"hello\",\"world\"]" | tee $(results.array-results.path)
709+
```
710+
687711
Results are written to the termination message encoded as JSON objects and Tekton uses those objects
688712
to pass additional information to the controller. As such, `Task` results are best suited for holding
689713
small amounts of data, such as commit SHAs, branch names, ephemeral namespaces, and so on.
@@ -1181,10 +1205,10 @@ log into the `Pod` and add a `Step` that pauses the `Task` at the desired stage.
11811205

11821206
### Running Step Containers as a Non Root User
11831207

1184-
All steps that do not require to be run as a root user should make use of TaskRun features to
1185-
designate the container for a step runs as a user without root permissions. As a best practice,
1186-
running containers as non root should be built into the container image to avoid any possibility
1187-
of the container being run as root. However, as a further measure of enforcing this practice,
1208+
All steps that do not require to be run as a root user should make use of TaskRun features to
1209+
designate the container for a step runs as a user without root permissions. As a best practice,
1210+
running containers as non root should be built into the container image to avoid any possibility
1211+
of the container being run as root. However, as a further measure of enforcing this practice,
11881212
steps can make use of a `securityContext` to specify how the container should run.
11891213

11901214
An example of running Task steps as a non root user is shown below:
@@ -1228,17 +1252,17 @@ spec:
12281252
runAsUser: 1001
12291253
```
12301254

1231-
In the example above, the step `show-user-2000` specifies via a `securityContext` that the container
1232-
for the step should run as user 2000. A `securityContext` must still be specified via a TaskRun `podTemplate`
1233-
for this TaskRun to run in a Kubernetes environment that enforces running containers as non root as a requirement.
1255+
In the example above, the step `show-user-2000` specifies via a `securityContext` that the container
1256+
for the step should run as user 2000. A `securityContext` must still be specified via a TaskRun `podTemplate`
1257+
for this TaskRun to run in a Kubernetes environment that enforces running containers as non root as a requirement.
12341258

1235-
The `runAsNonRoot` property specified via the `podTemplate` above validates that steps part of this TaskRun are
1236-
running as non root users and will fail to start any step container that attempts to run as root. Only specifying
1237-
`runAsNonRoot: true` will not actually run containers as non root as the property simply validates that steps are not
1259+
The `runAsNonRoot` property specified via the `podTemplate` above validates that steps part of this TaskRun are
1260+
running as non root users and will fail to start any step container that attempts to run as root. Only specifying
1261+
`runAsNonRoot: true` will not actually run containers as non root as the property simply validates that steps are not
12381262
running as root. It is the `runAsUser` property that is actually used to set the non root user ID for the container.
12391263

1240-
If a step defines its own `securityContext`, it will be applied for the step container over the `securityContext`
1241-
specified at the pod level via the TaskRun `podTemplate`.
1264+
If a step defines its own `securityContext`, it will be applied for the step container over the `securityContext`
1265+
specified at the pod level via the TaskRun `podTemplate`.
12421266

12431267
More information about Pod and Container Security Contexts can be found via the [Kubernetes website](https://kubernetes.io/docs/tasks/configure-pod-container/security-context/#set-the-security-context-for-a-pod).
12441268

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
kind: Task
2+
apiVersion: tekton.dev/v1beta1
3+
metadata:
4+
name: write-array
5+
annotations:
6+
description: |
7+
A simple task that writes array
8+
spec:
9+
results:
10+
- name: array-results
11+
type: array
12+
description: The array results
13+
steps:
14+
- name: write-array
15+
image: bash:latest
16+
script: |
17+
#!/usr/bin/env bash
18+
echo -n "[\"hello\",\"world\"]" | tee $(results.array-results.path)
19+
- name: check-results-array
20+
image: ubuntu
21+
script: |
22+
#!/bin/bash
23+
VALUE=$(cat $(results.array-results.path))
24+
EXPECTED=[\"hello\",\"world\"]
25+
diff=$(diff <(printf "%s\n" "${VALUE[@]}") <(printf "%s\n" "${EXPECTED[@]}"))
26+
if [[ -z "$diff" ]]; then
27+
echo "TRUE"
28+
else
29+
echo "FALSE"
30+
fi
31+
---
32+
kind: TaskRun
33+
apiVersion: tekton.dev/v1beta1
34+
metadata:
35+
name: write-array-tr
36+
spec:
37+
taskRef:
38+
name: write-array
39+
kind: task

pkg/apis/pipeline/v1beta1/openapi_generated.go

Lines changed: 4 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pkg/apis/pipeline/v1beta1/param_types.go

Lines changed: 35 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -141,17 +141,43 @@ type ArrayOrString struct {
141141

142142
// UnmarshalJSON implements the json.Unmarshaller interface.
143143
func (arrayOrString *ArrayOrString) UnmarshalJSON(value []byte) error {
144-
switch value[0] {
145-
case '[':
146-
arrayOrString.Type = ParamTypeArray
147-
return json.Unmarshal(value, &arrayOrString.ArrayVal)
148-
case '{':
149-
arrayOrString.Type = ParamTypeObject
150-
return json.Unmarshal(value, &arrayOrString.ObjectVal)
151-
default:
144+
// ArrayOrString is used for Results Value as well, the results can be any kind of
145+
// data so we need to check if it is empty.
146+
if len(value) == 0 {
152147
arrayOrString.Type = ParamTypeString
153-
return json.Unmarshal(value, &arrayOrString.StringVal)
148+
return nil
149+
}
150+
if value[0] == '[' {
151+
// We're trying to Unmarshal to []string, but for cases like []int or other types
152+
// of nested array which we don't support yet, we should continue and Unmarshal
153+
// it to String. If the Type being set doesn't match what it actually should be,
154+
// it will be captured by validation in reconciler.
155+
// if failed to unmarshal to array, we will convert the value to string and marshal it to string
156+
var a []string
157+
if err := json.Unmarshal(value, &a); err == nil {
158+
arrayOrString.Type = ParamTypeArray
159+
arrayOrString.ArrayVal = a
160+
return nil
161+
}
162+
}
163+
if value[0] == '{' {
164+
// if failed to unmarshal to map, we will convert the value to string and marshal it to string
165+
var m map[string]string
166+
if err := json.Unmarshal(value, &m); err == nil {
167+
arrayOrString.Type = ParamTypeObject
168+
arrayOrString.ObjectVal = m
169+
return nil
170+
}
171+
}
172+
173+
// By default we unmarshal to string
174+
arrayOrString.Type = ParamTypeString
175+
if err := json.Unmarshal(value, &arrayOrString.StringVal); err == nil {
176+
return nil
154177
}
178+
arrayOrString.StringVal = string(value)
179+
180+
return nil
155181
}
156182

157183
// MarshalJSON implements the json.Marshaller interface.

pkg/apis/pipeline/v1beta1/param_types_test.go

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -227,6 +227,10 @@ func TestArrayOrString_UnmarshalJSON(t *testing.T) {
227227
input map[string]interface{}
228228
result v1beta1.ArrayOrString
229229
}{
230+
{
231+
input: map[string]interface{}{"val": 123},
232+
result: *v1beta1.NewArrayOrString("123"),
233+
},
230234
{
231235
input: map[string]interface{}{"val": "123"},
232236
result: *v1beta1.NewArrayOrString("123"),
@@ -282,6 +286,49 @@ func TestArrayOrString_UnmarshalJSON(t *testing.T) {
282286
}
283287
}
284288

289+
func TestArrayOrString_UnmarshalJSON_Directly(t *testing.T) {
290+
cases := []struct {
291+
desc string
292+
input string
293+
expected v1beta1.ArrayOrString
294+
}{
295+
{desc: "empty value", input: ``, expected: *v1beta1.NewArrayOrString("")},
296+
{desc: "int value", input: `1`, expected: *v1beta1.NewArrayOrString("1")},
297+
{desc: "int array", input: `[1,2,3]`, expected: *v1beta1.NewArrayOrString("[1,2,3]")},
298+
{desc: "nested array", input: `[1,\"2\",3]`, expected: *v1beta1.NewArrayOrString(`[1,\"2\",3]`)},
299+
{desc: "string value", input: `hello`, expected: *v1beta1.NewArrayOrString("hello")},
300+
{desc: "array value", input: `["hello","world"]`, expected: *v1beta1.NewArrayOrString("hello", "world")},
301+
{desc: "object value", input: `{"hello":"world"}`, expected: *v1beta1.NewObject(map[string]string{"hello": "world"})},
302+
}
303+
304+
for _, c := range cases {
305+
aos := v1beta1.ArrayOrString{}
306+
if err := aos.UnmarshalJSON([]byte(c.input)); err != nil {
307+
t.Errorf("Failed to unmarshal input '%v': %v", c.input, err)
308+
}
309+
if !reflect.DeepEqual(aos, c.expected) {
310+
t.Errorf("Failed to unmarshal input '%v': expected %+v, got %+v", c.input, c.expected, aos)
311+
}
312+
}
313+
}
314+
315+
func TestArrayOrString_UnmarshalJSON_Error(t *testing.T) {
316+
cases := []struct {
317+
desc string
318+
input string
319+
}{
320+
{desc: "empty value", input: "{\"val\": }"},
321+
{desc: "wrong beginning value", input: "{\"val\": @}"},
322+
}
323+
324+
for _, c := range cases {
325+
var result ArrayOrStringHolder
326+
if err := json.Unmarshal([]byte(c.input), &result); err == nil {
327+
t.Errorf("Should return err but got nil '%v'", c.input)
328+
}
329+
}
330+
}
331+
285332
func TestArrayOrString_MarshalJSON(t *testing.T) {
286333
cases := []struct {
287334
input v1beta1.ArrayOrString

pkg/apis/pipeline/v1beta1/result_types.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ type TaskRunResult struct {
3939
Type ResultsType `json:"type,omitempty"`
4040

4141
// Value the given value of the result
42-
Value string `json:"value"`
42+
Value ArrayOrString `json:"value"`
4343
}
4444

4545
// ResultsType indicates the type of a result;
@@ -54,7 +54,9 @@ type ResultsType string
5454
// Valid ResultsType:
5555
const (
5656
ResultsTypeString ResultsType = "string"
57+
ResultsTypeArray ResultsType = "array"
58+
ResultsTypeObject ResultsType = "object"
5759
)
5860

5961
// AllResultsTypes can be used for ResultsTypes validation.
60-
var AllResultsTypes = []ResultsType{ResultsTypeString}
62+
var AllResultsTypes = []ResultsType{ResultsTypeString, ResultsTypeArray, ResultsTypeObject}

pkg/apis/pipeline/v1beta1/result_validation.go

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -17,22 +17,21 @@ import (
1717
"context"
1818
"fmt"
1919

20+
"github.com/tektoncd/pipeline/pkg/apis/config"
2021
"knative.dev/pkg/apis"
2122
)
2223

2324
// Validate implements apis.Validatable
24-
func (tr TaskResult) Validate(_ context.Context) *apis.FieldError {
25+
func (tr TaskResult) Validate(ctx context.Context) (errs *apis.FieldError) {
2526
if !resultNameFormatRegex.MatchString(tr.Name) {
2627
return apis.ErrInvalidKeyName(tr.Name, "name", fmt.Sprintf("Name must consist of alphanumeric characters, '-', '_', and must start and end with an alphanumeric character (e.g. 'MyName', or 'my-name', or 'my_name', regex used for validation is '%s')", ResultNameFormat))
2728
}
28-
// Validate the result type
29-
validType := false
30-
for _, allowedType := range AllResultsTypes {
31-
if tr.Type == allowedType {
32-
validType = true
33-
}
29+
// Array and Object is alpha feature
30+
if tr.Type == ResultsTypeArray || tr.Type == ResultsTypeObject {
31+
return errs.Also(ValidateEnabledAPIFields(ctx, "results type", config.AlphaAPIFields))
3432
}
35-
if !validType {
33+
34+
if tr.Type != ResultsTypeString {
3635
return apis.ErrInvalidValue(tr.Type, "type", fmt.Sprintf("type must be string"))
3736
}
3837

pkg/apis/pipeline/v1beta1/swagger.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2605,8 +2605,8 @@
26052605
},
26062606
"value": {
26072607
"description": "Value the given value of the result",
2608-
"type": "string",
2609-
"default": ""
2608+
"default": {},
2609+
"$ref": "#/definitions/v1beta1.ArrayOrString"
26102610
}
26112611
}
26122612
},

pkg/apis/pipeline/v1beta1/task_validation_test.go

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -305,7 +305,7 @@ func TestTaskSpecValidate(t *testing.T) {
305305
}},
306306
},
307307
}, {
308-
name: "valid result type",
308+
name: "valid result type string",
309309
fields: fields{
310310
Steps: []v1beta1.Step{{
311311
Image: "my-image",
@@ -317,6 +317,32 @@ func TestTaskSpecValidate(t *testing.T) {
317317
Description: "my great result",
318318
}},
319319
},
320+
}, {
321+
name: "valid result type array",
322+
fields: fields{
323+
Steps: []v1beta1.Step{{
324+
Image: "my-image",
325+
Args: []string{"arg"},
326+
}},
327+
Results: []v1beta1.TaskResult{{
328+
Name: "MY-RESULT",
329+
Type: v1beta1.ResultsTypeArray,
330+
Description: "my great result",
331+
}},
332+
},
333+
}, {
334+
name: "valid result type object",
335+
fields: fields{
336+
Steps: []v1beta1.Step{{
337+
Image: "my-image",
338+
Args: []string{"arg"},
339+
}},
340+
Results: []v1beta1.TaskResult{{
341+
Name: "MY-RESULT",
342+
Type: v1beta1.ResultsTypeObject,
343+
Description: "my great result",
344+
}},
345+
},
320346
}, {
321347
name: "valid task name context",
322348
fields: fields{

pkg/apis/pipeline/v1beta1/zz_generated.deepcopy.go

Lines changed: 4 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)