From 6115e0383e0edd23c8111530de7f84f5637d0a12 Mon Sep 17 00:00:00 2001 From: Yongxuan Zhang Date: Wed, 11 May 2022 02:56:41 +0000 Subject: [PATCH] [TEP-0076] Add indexing into array for params This commit provides the indexing into array for params and gated by alpha feature flag. Before this commit we can only refer to the whole array param, with this feature we can refer to array's element such as $(params.param-name[0]). --- docs/variables.md | 6 + .../taskruns/alpha/param_array_indexing.yaml | 26 ++ pkg/reconciler/pipelinerun/pipelinerun.go | 2 +- pkg/reconciler/pipelinerun/resources/apply.go | 18 +- .../pipelinerun/resources/apply_test.go | 345 +++++++++++++++++- pkg/reconciler/taskrun/resources/apply.go | 16 +- .../taskrun/resources/apply_test.go | 253 ++++++++++++- pkg/reconciler/taskrun/taskrun.go | 2 +- 8 files changed, 661 insertions(+), 7 deletions(-) create mode 100644 examples/v1beta1/taskruns/alpha/param_array_indexing.yaml diff --git a/docs/variables.md b/docs/variables.md index ac1d23715e7..8df7706d4bd 100644 --- a/docs/variables.md +++ b/docs/variables.md @@ -19,6 +19,9 @@ For instructions on using variable substitutions see the relevant section of [th | `params.` | The value of the parameter at runtime. | | `params['']` | (see above) | | `params[""]` | (see above) | +| `params.[i]` | Get the i element of param array. This is alpha feature, set `enable-api-fields` to `alpha` to use it.| +| `params[''][i]` | (see above) | +| `params[""][i]` | (see above) | | `tasks..results.` | The value of the `Task's` result. Can alter `Task` execution order within a `Pipeline`.) | | `tasks..results['']` | (see above)) | | `tasks..results[""]` | (see above)) | @@ -38,6 +41,9 @@ For instructions on using variable substitutions see the relevant section of [th | `params.` | The value of the parameter at runtime. | | `params['']` | (see above) | | `params[""]` | (see above) | +| `params.[i]` | Get the i element of param array. This is alpha feature, set `enable-api-fields` to `alpha` to use it.| +| `params[''][i]` | (see above) | +| `params[""][i]` | (see above) | | `resources.inputs..path` | The path to the input resource's directory. | | `resources.outputs..path` | The path to the output resource's directory. | | `results..path` | The path to the file where the `Task` writes its results data. | diff --git a/examples/v1beta1/taskruns/alpha/param_array_indexing.yaml b/examples/v1beta1/taskruns/alpha/param_array_indexing.yaml new file mode 100644 index 00000000000..f0c1dfc8dc9 --- /dev/null +++ b/examples/v1beta1/taskruns/alpha/param_array_indexing.yaml @@ -0,0 +1,26 @@ +apiVersion: tekton.dev/v1beta1 +kind: TaskRun +metadata: + generateName: params-array-indexing- +spec: + params: + - name: array-to-echo + value: + - "foo" + - "bar" + taskSpec: + params: + - name: array-to-echo + type: array + steps: + # this step should echo "foo" + - name: echo-params-1 + image: bash:3.2 + args: [ + "echo", + "$(params.array-to-echo[0])", + ] + # this step should echo "bar" + - name: echo-params-2 + image: bash:3.2 + script: "echo $(params.array-to-echo[1])" diff --git a/pkg/reconciler/pipelinerun/pipelinerun.go b/pkg/reconciler/pipelinerun/pipelinerun.go index e8947b9b0a7..6c817d254e0 100644 --- a/pkg/reconciler/pipelinerun/pipelinerun.go +++ b/pkg/reconciler/pipelinerun/pipelinerun.go @@ -454,7 +454,7 @@ func (c *Reconciler) reconcile(ctx context.Context, pr *v1beta1.PipelineRun, get } // Apply parameter substitution from the PipelineRun - pipelineSpec = resources.ApplyParameters(pipelineSpec, pr) + pipelineSpec = resources.ApplyParameters(ctx, pipelineSpec, pr) pipelineSpec = resources.ApplyContexts(pipelineSpec, pipelineMeta.Name, pr) pipelineSpec = resources.ApplyWorkspaces(pipelineSpec, pr) diff --git a/pkg/reconciler/pipelinerun/resources/apply.go b/pkg/reconciler/pipelinerun/resources/apply.go index 4187b915a84..320bb5d1a7c 100644 --- a/pkg/reconciler/pipelinerun/resources/apply.go +++ b/pkg/reconciler/pipelinerun/resources/apply.go @@ -17,10 +17,12 @@ limitations under the License. package resources import ( + "context" "fmt" "strconv" "strings" + "github.com/tektoncd/pipeline/pkg/apis/config" "github.com/tektoncd/pipeline/pkg/apis/run/v1alpha1" "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1beta1" @@ -28,13 +30,14 @@ import ( ) // ApplyParameters applies the params from a PipelineRun.Params to a PipelineSpec. -func ApplyParameters(p *v1beta1.PipelineSpec, pr *v1beta1.PipelineRun) *v1beta1.PipelineSpec { +func ApplyParameters(ctx context.Context, p *v1beta1.PipelineSpec, pr *v1beta1.PipelineRun) *v1beta1.PipelineSpec { // This assumes that the PipelineRun inputs have been validated against what the Pipeline requests. // stringReplacements is used for standard single-string stringReplacements, while arrayReplacements contains arrays // that need to be further processed. stringReplacements := map[string]string{} arrayReplacements := map[string][]string{} + cfg := config.FromContextOrDefaults(ctx) patterns := []string{ "params.%s", @@ -51,6 +54,13 @@ func ApplyParameters(p *v1beta1.PipelineSpec, pr *v1beta1.PipelineRun) *v1beta1. } } else { for _, pattern := range patterns { + // array indexing for param is alpha feature + // TODO(#4723): Validate the array reference is not our of bound + if cfg.FeatureFlags.EnableAPIFields == config.AlphaAPIFields { + for i := 0; i < len(p.Default.ArrayVal); i++ { + stringReplacements[fmt.Sprintf(pattern+"[%d]", p.Name, i)] = p.Default.ArrayVal[i] + } + } arrayReplacements[fmt.Sprintf(pattern, p.Name)] = p.Default.ArrayVal } } @@ -64,6 +74,12 @@ func ApplyParameters(p *v1beta1.PipelineSpec, pr *v1beta1.PipelineRun) *v1beta1. } } else { for _, pattern := range patterns { + // array indexing for param is alpha feature + if cfg.FeatureFlags.EnableAPIFields == config.AlphaAPIFields { + for i := 0; i < len(p.Value.ArrayVal); i++ { + stringReplacements[fmt.Sprintf(pattern+"[%d]", p.Name, i)] = p.Value.ArrayVal[i] + } + } arrayReplacements[fmt.Sprintf(pattern, p.Name)] = p.Value.ArrayVal } } diff --git a/pkg/reconciler/pipelinerun/resources/apply_test.go b/pkg/reconciler/pipelinerun/resources/apply_test.go index 8dd15896080..5a1b3ba1247 100644 --- a/pkg/reconciler/pipelinerun/resources/apply_test.go +++ b/pkg/reconciler/pipelinerun/resources/apply_test.go @@ -17,10 +17,12 @@ limitations under the License. package resources import ( + "context" "testing" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" + "github.com/tektoncd/pipeline/pkg/apis/config" "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1alpha1" "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1beta1" resourcev1alpha1 "github.com/tektoncd/pipeline/pkg/apis/resource/v1alpha1" @@ -30,6 +32,7 @@ import ( ) func TestApplyParameters(t *testing.T) { + ctx := context.Background() for _, tt := range []struct { name string original v1beta1.PipelineSpec @@ -357,7 +360,347 @@ func TestApplyParameters(t *testing.T) { Params: tt.params, }, } - got := ApplyParameters(&tt.original, run) + got := ApplyParameters(ctx, &tt.original, run) + if d := cmp.Diff(&tt.expected, got); d != "" { + t.Errorf("ApplyParameters() got diff %s", diff.PrintWantGot(d)) + } + }) + } +} + +func TestApplyParameters_ArrayIndexing(t *testing.T) { + ctx := context.Background() + cfg := config.FromContextOrDefaults(ctx) + cfg.FeatureFlags.EnableAPIFields = config.AlphaAPIFields + ctx = config.ToContext(ctx, cfg) + for _, tt := range []struct { + name string + original v1beta1.PipelineSpec + params []v1beta1.Param + expected v1beta1.PipelineSpec + }{{ + name: "single parameter", + original: v1beta1.PipelineSpec{ + Params: []v1beta1.ParamSpec{ + {Name: "first-param", Type: v1beta1.ParamTypeArray, Default: v1beta1.NewArrayOrString("default-value", "default-value-again")}, + {Name: "second-param", Type: v1beta1.ParamTypeString}, + }, + Tasks: []v1beta1.PipelineTask{{ + Params: []v1beta1.Param{ + {Name: "first-task-first-param", Value: *v1beta1.NewArrayOrString("$(params.first-param[1])")}, + {Name: "first-task-second-param", Value: *v1beta1.NewArrayOrString("$(params.second-param[0])")}, + {Name: "first-task-third-param", Value: *v1beta1.NewArrayOrString("static value")}, + }, + }}, + }, + params: []v1beta1.Param{{Name: "second-param", Value: *v1beta1.NewArrayOrString("second-value", "second-value-again")}}, + expected: v1beta1.PipelineSpec{ + Params: []v1beta1.ParamSpec{ + {Name: "first-param", Type: v1beta1.ParamTypeArray, Default: v1beta1.NewArrayOrString("default-value", "default-value-again")}, + {Name: "second-param", Type: v1beta1.ParamTypeString}, + }, + Tasks: []v1beta1.PipelineTask{{ + Params: []v1beta1.Param{ + {Name: "first-task-first-param", Value: *v1beta1.NewArrayOrString("default-value-again")}, + {Name: "first-task-second-param", Value: *v1beta1.NewArrayOrString("second-value")}, + {Name: "first-task-third-param", Value: *v1beta1.NewArrayOrString("static value")}, + }, + }}, + }, + }, { + name: "single parameter with when expression", + original: v1beta1.PipelineSpec{ + Params: []v1beta1.ParamSpec{ + {Name: "first-param", Type: v1beta1.ParamTypeArray, Default: v1beta1.NewArrayOrString("default-value", "default-value-again")}, + {Name: "second-param", Type: v1beta1.ParamTypeString}, + }, + Tasks: []v1beta1.PipelineTask{{ + WhenExpressions: []v1beta1.WhenExpression{{ + Input: "$(params.first-param[1])", + Operator: selection.In, + Values: []string{"$(params.second-param[0])"}, + }}, + }}, + }, + params: []v1beta1.Param{{Name: "second-param", Value: *v1beta1.NewArrayOrString("second-value", "second-value-again")}}, + expected: v1beta1.PipelineSpec{ + Params: []v1beta1.ParamSpec{ + {Name: "first-param", Type: v1beta1.ParamTypeArray, Default: v1beta1.NewArrayOrString("default-value", "default-value-again")}, + {Name: "second-param", Type: v1beta1.ParamTypeString}, + }, + Tasks: []v1beta1.PipelineTask{{ + WhenExpressions: []v1beta1.WhenExpression{{ + Input: "default-value-again", + Operator: selection.In, + Values: []string{"second-value"}, + }}, + }}, + }, + }, { + name: "pipeline parameter nested inside task parameter", + original: v1beta1.PipelineSpec{ + Params: []v1beta1.ParamSpec{ + {Name: "first-param", Type: v1beta1.ParamTypeArray, Default: v1beta1.NewArrayOrString("default-value", "default-value-again")}, + {Name: "second-param", Type: v1beta1.ParamTypeArray, Default: v1beta1.NewArrayOrString("default-value", "default-value-again")}, + }, + Tasks: []v1beta1.PipelineTask{{ + Params: []v1beta1.Param{ + {Name: "first-task-first-param", Value: *v1beta1.NewArrayOrString("$(input.workspace.$(params.first-param[0]))")}, + {Name: "first-task-second-param", Value: *v1beta1.NewArrayOrString("$(input.workspace.$(params.second-param[1]))")}, + }, + }}, + }, + params: nil, // no parameter values. + expected: v1beta1.PipelineSpec{ + Params: []v1beta1.ParamSpec{ + {Name: "first-param", Type: v1beta1.ParamTypeArray, Default: v1beta1.NewArrayOrString("default-value", "default-value-again")}, + {Name: "second-param", Type: v1beta1.ParamTypeArray, Default: v1beta1.NewArrayOrString("default-value", "default-value-again")}, + }, + Tasks: []v1beta1.PipelineTask{{ + Params: []v1beta1.Param{ + {Name: "first-task-first-param", Value: *v1beta1.NewArrayOrString("$(input.workspace.default-value)")}, + {Name: "first-task-second-param", Value: *v1beta1.NewArrayOrString("$(input.workspace.default-value-again)")}, + }, + }}, + }, + }, { + name: "parameters in task condition", + original: v1beta1.PipelineSpec{ + Params: []v1beta1.ParamSpec{ + {Name: "first-param", Type: v1beta1.ParamTypeArray, Default: v1beta1.NewArrayOrString("default-value", "default-value-again")}, + {Name: "second-param", Type: v1beta1.ParamTypeArray}, + }, + Tasks: []v1beta1.PipelineTask{{ + Conditions: []v1beta1.PipelineTaskCondition{{ + Params: []v1beta1.Param{ + {Name: "cond-first-param", Value: *v1beta1.NewArrayOrString("$(params.first-param[0])")}, + {Name: "cond-second-param", Value: *v1beta1.NewArrayOrString("$(params.second-param[1])")}, + }, + }}, + }}, + }, + params: []v1beta1.Param{{Name: "second-param", Value: *v1beta1.NewArrayOrString("second-value", "second-value-again")}}, + expected: v1beta1.PipelineSpec{ + Params: []v1beta1.ParamSpec{ + {Name: "first-param", Type: v1beta1.ParamTypeArray, Default: v1beta1.NewArrayOrString("default-value", "default-value-again")}, + {Name: "second-param", Type: v1beta1.ParamTypeArray}, + }, + Tasks: []v1beta1.PipelineTask{{ + Conditions: []v1beta1.PipelineTaskCondition{{ + Params: []v1beta1.Param{ + {Name: "cond-first-param", Value: *v1beta1.NewArrayOrString("default-value")}, + {Name: "cond-second-param", Value: *v1beta1.NewArrayOrString("second-value-again")}, + }, + }}, + }}, + }, + }, { + name: "array parameter", + original: v1beta1.PipelineSpec{ + Params: []v1beta1.ParamSpec{ + {Name: "first-param", Type: v1beta1.ParamTypeArray, Default: v1beta1.NewArrayOrString("default", "array", "value")}, + {Name: "second-param", Type: v1beta1.ParamTypeArray}, + }, + Tasks: []v1beta1.PipelineTask{{ + Params: []v1beta1.Param{ + {Name: "first-task-first-param", Value: *v1beta1.NewArrayOrString("firstelement", "$(params.first-param)")}, + {Name: "first-task-second-param", Value: *v1beta1.NewArrayOrString("firstelement", "$(params.second-param[0])")}, + }, + }}, + }, + params: []v1beta1.Param{ + {Name: "second-param", Value: *v1beta1.NewArrayOrString("second-value", "array")}, + }, + expected: v1beta1.PipelineSpec{ + Params: []v1beta1.ParamSpec{ + {Name: "first-param", Type: v1beta1.ParamTypeArray, Default: v1beta1.NewArrayOrString("default", "array", "value")}, + {Name: "second-param", Type: v1beta1.ParamTypeArray}, + }, + Tasks: []v1beta1.PipelineTask{{ + Params: []v1beta1.Param{ + {Name: "first-task-first-param", Value: *v1beta1.NewArrayOrString("firstelement", "default", "array", "value")}, + {Name: "first-task-second-param", Value: *v1beta1.NewArrayOrString("firstelement", "second-value")}, + }, + }}, + }, + }, { + name: "parameter evaluation with final tasks", + original: v1beta1.PipelineSpec{ + Params: []v1beta1.ParamSpec{ + {Name: "first-param", Type: v1beta1.ParamTypeArray, Default: v1beta1.NewArrayOrString("default-value", "default-value-again")}, + {Name: "second-param", Type: v1beta1.ParamTypeArray}, + }, + Finally: []v1beta1.PipelineTask{{ + Params: []v1beta1.Param{ + {Name: "final-task-first-param", Value: *v1beta1.NewArrayOrString("$(params.first-param[0])")}, + {Name: "final-task-second-param", Value: *v1beta1.NewArrayOrString("$(params.second-param[1])")}, + }, + WhenExpressions: v1beta1.WhenExpressions{{ + Input: "$(params.first-param[0])", + Operator: selection.In, + Values: []string{"$(params.second-param[1])"}, + }}, + }}, + }, + params: []v1beta1.Param{{Name: "second-param", Value: *v1beta1.NewArrayOrString("second-value", "second-value-again")}}, + expected: v1beta1.PipelineSpec{ + Params: []v1beta1.ParamSpec{ + {Name: "first-param", Type: v1beta1.ParamTypeArray, Default: v1beta1.NewArrayOrString("default-value", "default-value-again")}, + {Name: "second-param", Type: v1beta1.ParamTypeArray}, + }, + Finally: []v1beta1.PipelineTask{{ + Params: []v1beta1.Param{ + {Name: "final-task-first-param", Value: *v1beta1.NewArrayOrString("default-value")}, + {Name: "final-task-second-param", Value: *v1beta1.NewArrayOrString("second-value-again")}, + }, + WhenExpressions: v1beta1.WhenExpressions{{ + Input: "default-value", + Operator: selection.In, + Values: []string{"second-value-again"}, + }}, + }}, + }, + }, { + name: "parameter evaluation with both tasks and final tasks", + original: v1beta1.PipelineSpec{ + Params: []v1beta1.ParamSpec{ + {Name: "first-param", Type: v1beta1.ParamTypeArray, Default: v1beta1.NewArrayOrString("default-value", "default-value-again")}, + {Name: "second-param", Type: v1beta1.ParamTypeArray}, + }, + Tasks: []v1beta1.PipelineTask{{ + Params: []v1beta1.Param{ + {Name: "final-task-first-param", Value: *v1beta1.NewArrayOrString("$(params.first-param[0])")}, + {Name: "final-task-second-param", Value: *v1beta1.NewArrayOrString("$(params.second-param[1])")}, + }, + }}, + Finally: []v1beta1.PipelineTask{{ + Params: []v1beta1.Param{ + {Name: "final-task-first-param", Value: *v1beta1.NewArrayOrString("$(params.first-param[0])")}, + {Name: "final-task-second-param", Value: *v1beta1.NewArrayOrString("$(params.second-param[1])")}, + }, + WhenExpressions: v1beta1.WhenExpressions{{ + Input: "$(params.first-param[0])", + Operator: selection.In, + Values: []string{"$(params.second-param[1])"}, + }}, + }}, + }, + params: []v1beta1.Param{{Name: "second-param", Value: *v1beta1.NewArrayOrString("second-value", "second-value-again")}}, + expected: v1beta1.PipelineSpec{ + Params: []v1beta1.ParamSpec{ + {Name: "first-param", Type: v1beta1.ParamTypeArray, Default: v1beta1.NewArrayOrString("default-value", "default-value-again")}, + {Name: "second-param", Type: v1beta1.ParamTypeArray}, + }, + Tasks: []v1beta1.PipelineTask{{ + Params: []v1beta1.Param{ + {Name: "final-task-first-param", Value: *v1beta1.NewArrayOrString("default-value")}, + {Name: "final-task-second-param", Value: *v1beta1.NewArrayOrString("second-value-again")}, + }, + }}, + Finally: []v1beta1.PipelineTask{{ + Params: []v1beta1.Param{ + {Name: "final-task-first-param", Value: *v1beta1.NewArrayOrString("default-value")}, + {Name: "final-task-second-param", Value: *v1beta1.NewArrayOrString("second-value-again")}, + }, + WhenExpressions: v1beta1.WhenExpressions{{ + Input: "default-value", + Operator: selection.In, + Values: []string{"second-value-again"}, + }}, + }}, + }, + }, { + name: "parameter references with bracket notation and special characters", + original: v1beta1.PipelineSpec{ + Params: []v1beta1.ParamSpec{ + {Name: "first.param", Type: v1beta1.ParamTypeArray, Default: v1beta1.NewArrayOrString("default-value", "default-value-again")}, + {Name: "second/param", Type: v1beta1.ParamTypeArray}, + {Name: "third.param", Type: v1beta1.ParamTypeArray, Default: v1beta1.NewArrayOrString("default-value", "default-value-again")}, + {Name: "fourth/param", Type: v1beta1.ParamTypeArray}, + }, + Tasks: []v1beta1.PipelineTask{{ + Params: []v1beta1.Param{ + {Name: "first-task-first-param", Value: *v1beta1.NewArrayOrString(`$(params["first.param"][0])`)}, + {Name: "first-task-second-param", Value: *v1beta1.NewArrayOrString(`$(params["second/param"][0])`)}, + {Name: "first-task-third-param", Value: *v1beta1.NewArrayOrString(`$(params['third.param'][1])`)}, + {Name: "first-task-fourth-param", Value: *v1beta1.NewArrayOrString(`$(params['fourth/param'][1])`)}, + {Name: "first-task-fifth-param", Value: *v1beta1.NewArrayOrString("static value")}, + }, + }}, + }, + params: []v1beta1.Param{ + {Name: "second/param", Value: *v1beta1.NewArrayOrString("second-value", "second-value-again")}, + {Name: "fourth/param", Value: *v1beta1.NewArrayOrString("fourth-value", "fourth-value-again")}, + }, + expected: v1beta1.PipelineSpec{ + Params: []v1beta1.ParamSpec{ + {Name: "first.param", Type: v1beta1.ParamTypeArray, Default: v1beta1.NewArrayOrString("default-value", "default-value-again")}, + {Name: "second/param", Type: v1beta1.ParamTypeArray}, + {Name: "third.param", Type: v1beta1.ParamTypeArray, Default: v1beta1.NewArrayOrString("default-value", "default-value-again")}, + {Name: "fourth/param", Type: v1beta1.ParamTypeArray}, + }, + Tasks: []v1beta1.PipelineTask{{ + Params: []v1beta1.Param{ + {Name: "first-task-first-param", Value: *v1beta1.NewArrayOrString("default-value")}, + {Name: "first-task-second-param", Value: *v1beta1.NewArrayOrString("second-value")}, + {Name: "first-task-third-param", Value: *v1beta1.NewArrayOrString("default-value-again")}, + {Name: "first-task-fourth-param", Value: *v1beta1.NewArrayOrString("fourth-value-again")}, + {Name: "first-task-fifth-param", Value: *v1beta1.NewArrayOrString("static value")}, + }, + }}, + }, + }, { + name: "single parameter in workspace subpath", + original: v1beta1.PipelineSpec{ + Params: []v1beta1.ParamSpec{ + {Name: "first-param", Type: v1beta1.ParamTypeArray, Default: v1beta1.NewArrayOrString("default-value", "default-value-again")}, + {Name: "second-param", Type: v1beta1.ParamTypeArray}, + }, + Tasks: []v1beta1.PipelineTask{{ + Params: []v1beta1.Param{ + {Name: "first-task-first-param", Value: *v1beta1.NewArrayOrString("$(params.first-param[0])")}, + {Name: "first-task-second-param", Value: *v1beta1.NewArrayOrString("static value")}, + }, + Workspaces: []v1beta1.WorkspacePipelineTaskBinding{ + { + Name: "first-workspace", + Workspace: "first-workspace", + SubPath: "$(params.second-param[1])", + }, + }, + }}, + }, + params: []v1beta1.Param{{Name: "second-param", Value: *v1beta1.NewArrayOrString("second-value", "second-value-again")}}, + expected: v1beta1.PipelineSpec{ + Params: []v1beta1.ParamSpec{ + {Name: "first-param", Type: v1beta1.ParamTypeArray, Default: v1beta1.NewArrayOrString("default-value", "default-value-again")}, + {Name: "second-param", Type: v1beta1.ParamTypeArray}, + }, + Tasks: []v1beta1.PipelineTask{{ + Params: []v1beta1.Param{ + {Name: "first-task-first-param", Value: *v1beta1.NewArrayOrString("default-value")}, + {Name: "first-task-second-param", Value: *v1beta1.NewArrayOrString("static value")}, + }, + Workspaces: []v1beta1.WorkspacePipelineTaskBinding{ + { + Name: "first-workspace", + Workspace: "first-workspace", + SubPath: "second-value-again", + }, + }, + }}, + }, + }, + } { + tt := tt // capture range variable + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + run := &v1beta1.PipelineRun{ + Spec: v1beta1.PipelineRunSpec{ + Params: tt.params, + }, + } + got := ApplyParameters(ctx, &tt.original, run) if d := cmp.Diff(&tt.expected, got); d != "" { t.Errorf("ApplyParameters() got diff %s", diff.PrintWantGot(d)) } diff --git a/pkg/reconciler/taskrun/resources/apply.go b/pkg/reconciler/taskrun/resources/apply.go index c038fbc3915..686d4561bed 100644 --- a/pkg/reconciler/taskrun/resources/apply.go +++ b/pkg/reconciler/taskrun/resources/apply.go @@ -33,13 +33,14 @@ import ( ) // ApplyParameters applies the params from a TaskRun.Input.Parameters to a TaskSpec -func ApplyParameters(spec *v1beta1.TaskSpec, tr *v1beta1.TaskRun, defaults ...v1beta1.ParamSpec) *v1beta1.TaskSpec { +func ApplyParameters(ctx context.Context, spec *v1beta1.TaskSpec, tr *v1beta1.TaskRun, defaults ...v1beta1.ParamSpec) *v1beta1.TaskSpec { // This assumes that the TaskRun inputs have been validated against what the Task requests. // stringReplacements is used for standard single-string stringReplacements, while arrayReplacements contains arrays // that need to be further processed. stringReplacements := map[string]string{} arrayReplacements := map[string][]string{} + cfg := config.FromContextOrDefaults(ctx) patterns := []string{ "params.%s", @@ -58,6 +59,13 @@ func ApplyParameters(spec *v1beta1.TaskSpec, tr *v1beta1.TaskRun, defaults ...v1 } } else { for _, pattern := range patterns { + // array indexing for param is alpha feature + // TODO(#4723): Validate the array reference is not our of bound + if cfg.FeatureFlags.EnableAPIFields == config.AlphaAPIFields { + for i := 0; i < len(p.Default.ArrayVal); i++ { + stringReplacements[fmt.Sprintf(pattern+"[%d]", p.Name, i)] = p.Default.ArrayVal[i] + } + } arrayReplacements[fmt.Sprintf(pattern, p.Name)] = p.Default.ArrayVal } } @@ -71,6 +79,12 @@ func ApplyParameters(spec *v1beta1.TaskSpec, tr *v1beta1.TaskRun, defaults ...v1 } } else { for _, pattern := range patterns { + // array indexing for param is alpha feature + if cfg.FeatureFlags.EnableAPIFields == config.AlphaAPIFields { + for i := 0; i < len(p.Value.ArrayVal); i++ { + stringReplacements[fmt.Sprintf(pattern+"[%d]", p.Name, i)] = p.Value.ArrayVal[i] + } + } arrayReplacements[fmt.Sprintf(pattern, p.Name)] = p.Value.ArrayVal } } diff --git a/pkg/reconciler/taskrun/resources/apply_test.go b/pkg/reconciler/taskrun/resources/apply_test.go index d7e3636e752..8e0cce8a844 100644 --- a/pkg/reconciler/taskrun/resources/apply_test.go +++ b/pkg/reconciler/taskrun/resources/apply_test.go @@ -222,6 +222,180 @@ var ( }, } + simpleTaskSpecArrayIndexing = &v1beta1.TaskSpec{ + Sidecars: []v1beta1.Sidecar{{ + Name: "foo", + Image: `$(params["myimage"][0])`, + Env: []corev1.EnvVar{{ + Name: "foo", + Value: "$(params['FOO'][1])", + }}, + }}, + StepTemplate: &v1beta1.StepTemplate{ + Env: []corev1.EnvVar{{ + Name: "template-var", + Value: `$(params["FOO"][1])`, + }}, + Image: "$(params.myimage[0])", + }, + Steps: []v1beta1.Step{{ + Name: "foo", + Image: "$(params.myimage[0])", + }, { + Name: "baz", + Image: "bat", + WorkingDir: "$(inputs.resources.workspace.path)", + Args: []string{"$(inputs.resources.workspace.url)"}, + }, { + Name: "qux", + Image: "$(params.something[0])", + Args: []string{"$(outputs.resources.imageToUse.url)"}, + }, { + Name: "foo", + Image: `$(params["myimage"][0])`, + }, { + Name: "baz", + Image: "$(params.somethingelse)", + WorkingDir: "$(inputs.resources.workspace.path)", + Args: []string{"$(inputs.resources.workspace.url)"}, + }, { + Name: "qux", + Image: "quux", + Args: []string{"$(outputs.resources.imageToUse.url)"}, + }, { + Name: "foo", + Image: "busybox:$(params.FOO[1])", + VolumeMounts: []corev1.VolumeMount{{ + Name: "$(params.FOO[1])", + MountPath: "path/to/$(params.FOO[1])", + SubPath: "sub/$(params.FOO[1])/path", + }}, + }, { + Name: "foo", + Image: "busybox:$(params.FOO[1])", + Env: []corev1.EnvVar{{ + Name: "foo", + Value: "value-$(params.FOO[1])", + }, { + Name: "bar", + ValueFrom: &corev1.EnvVarSource{ + ConfigMapKeyRef: &corev1.ConfigMapKeySelector{ + LocalObjectReference: corev1.LocalObjectReference{Name: "config-$(params.FOO[1])"}, + Key: "config-key-$(params.FOO[1])", + }, + }, + }, { + Name: "baz", + ValueFrom: &corev1.EnvVarSource{ + SecretKeyRef: &corev1.SecretKeySelector{ + LocalObjectReference: corev1.LocalObjectReference{Name: "secret-$(params.FOO[1])"}, + Key: "secret-key-$(params.FOO[1])", + }, + }, + }}, + EnvFrom: []corev1.EnvFromSource{{ + Prefix: "prefix-0-$(params.FOO[1])", + ConfigMapRef: &corev1.ConfigMapEnvSource{ + LocalObjectReference: corev1.LocalObjectReference{Name: "config-$(params.FOO[1])"}, + }, + }, { + Prefix: "prefix-1-$(params.FOO[1])", + SecretRef: &corev1.SecretEnvSource{ + LocalObjectReference: corev1.LocalObjectReference{Name: "secret-$(params.FOO[1])"}, + }, + }}, + }, { + Name: "outputs-resources-path-ab", + Image: "$(outputs.resources.imageToUse-ab.path)", + }, { + Name: "outputs-resources-path-re", + Image: "$(outputs.resources.imageToUse-re.path)", + }}, + Volumes: []corev1.Volume{{ + Name: "$(params.FOO[1])", + VolumeSource: corev1.VolumeSource{ + ConfigMap: &corev1.ConfigMapVolumeSource{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: "$(params.FOO[1])", + }, + Items: []corev1.KeyToPath{{ + Key: "$(params.FOO[1])", + Path: "$(params.FOO[1])", + }}, + }, + }, + }, { + Name: "some-secret", + VolumeSource: corev1.VolumeSource{ + Secret: &corev1.SecretVolumeSource{ + SecretName: "$(params.FOO[1])", + Items: []corev1.KeyToPath{{ + Key: "$(params.FOO[1])", + Path: "$(params.FOO[1])", + }}, + }, + }, + }, { + Name: "some-pvc", + VolumeSource: corev1.VolumeSource{ + PersistentVolumeClaim: &corev1.PersistentVolumeClaimVolumeSource{ + ClaimName: "$(params.FOO[1])", + }, + }, + }, { + Name: "some-projected-volumes", + VolumeSource: corev1.VolumeSource{ + Projected: &corev1.ProjectedVolumeSource{ + Sources: []corev1.VolumeProjection{{ + ConfigMap: &corev1.ConfigMapProjection{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: "$(params.FOO[1])", + }, + }, + Secret: &corev1.SecretProjection{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: "$(params.FOO[1])", + }, + }, + ServiceAccountToken: &corev1.ServiceAccountTokenProjection{ + Audience: "$(params.FOO[1])", + }, + }}, + }, + }, + }, { + Name: "some-csi", + VolumeSource: corev1.VolumeSource{ + CSI: &corev1.CSIVolumeSource{ + VolumeAttributes: map[string]string{ + "secretProviderClass": "$(params.FOO[1])", + }, + NodePublishSecretRef: &corev1.LocalObjectReference{ + Name: "$(params.FOO[1])", + }, + }, + }, + }}, + Resources: &v1beta1.TaskResources{ + Inputs: []v1beta1.TaskResource{{ + ResourceDeclaration: v1beta1.ResourceDeclaration{ + Name: "workspace", + }, + }}, + Outputs: []v1beta1.TaskResource{{ + ResourceDeclaration: v1beta1.ResourceDeclaration{ + Name: "imageToUse-ab", + TargetPath: "/foo/builtImage", + }, + }, { + ResourceDeclaration: v1beta1.ResourceDeclaration{ + Name: "imageToUse-re", + TargetPath: "foo/builtImage", + }, + }}, + }, + } + gcsTaskSpec = &v1beta1.TaskSpec{ Steps: []v1beta1.Step{{ Name: "foobar", @@ -496,9 +670,10 @@ func TestApplyArrayParameters(t *testing.T) { spec.Steps[1].Args = []string{"first", "second", "defaulted", "value!", "last"} }), }} + ctx := context.Background() for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got := resources.ApplyParameters(tt.args.ts, tt.args.tr, tt.args.dp...) + got := resources.ApplyParameters(ctx, tt.args.ts, tt.args.tr, tt.args.dp...) if d := cmp.Diff(tt.want, got); d != "" { t.Errorf("ApplyParameters() got diff %s", diff.PrintWantGot(d)) } @@ -569,7 +744,81 @@ func TestApplyParameters(t *testing.T) { spec.Sidecars[0].Image = "bar" spec.Sidecars[0].Env[0].Value = "world" }) - got := resources.ApplyParameters(simpleTaskSpec, tr, dp...) + ctx := context.Background() + got := resources.ApplyParameters(ctx, simpleTaskSpec, tr, dp...) + if d := cmp.Diff(want, got); d != "" { + t.Errorf("ApplyParameters() got diff %s", diff.PrintWantGot(d)) + } +} + +func TestApplyParameters_ArrayIndexing(t *testing.T) { + tr := &v1beta1.TaskRun{ + Spec: v1beta1.TaskRunSpec{ + Params: []v1beta1.Param{{ + Name: "myimage", + Value: *v1beta1.NewArrayOrString("bar", "foo"), + }, { + Name: "FOO", + Value: *v1beta1.NewArrayOrString("hello", "world"), + }}, + }, + } + dp := []v1beta1.ParamSpec{{ + Name: "something", + Default: v1beta1.NewArrayOrString("mydefault", "mydefault2"), + }, { + Name: "somethingelse", + Default: v1beta1.NewArrayOrString(""), + }} + want := applyMutation(simpleTaskSpec, func(spec *v1beta1.TaskSpec) { + spec.StepTemplate.Env[0].Value = "world" + spec.StepTemplate.Image = "bar" + + spec.Steps[0].Image = "bar" + spec.Steps[2].Image = "mydefault" + spec.Steps[3].Image = "bar" + spec.Steps[4].Image = "" + + spec.Steps[6].VolumeMounts[0].Name = "world" + spec.Steps[6].VolumeMounts[0].SubPath = "sub/world/path" + spec.Steps[6].VolumeMounts[0].MountPath = "path/to/world" + spec.Steps[6].Image = "busybox:world" + + spec.Steps[7].Env[0].Value = "value-world" + spec.Steps[7].Env[1].ValueFrom.ConfigMapKeyRef.LocalObjectReference.Name = "config-world" + spec.Steps[7].Env[1].ValueFrom.ConfigMapKeyRef.Key = "config-key-world" + spec.Steps[7].Env[2].ValueFrom.SecretKeyRef.LocalObjectReference.Name = "secret-world" + spec.Steps[7].Env[2].ValueFrom.SecretKeyRef.Key = "secret-key-world" + spec.Steps[7].EnvFrom[0].Prefix = "prefix-0-world" + spec.Steps[7].EnvFrom[0].ConfigMapRef.LocalObjectReference.Name = "config-world" + spec.Steps[7].EnvFrom[1].Prefix = "prefix-1-world" + spec.Steps[7].EnvFrom[1].SecretRef.LocalObjectReference.Name = "secret-world" + spec.Steps[7].Image = "busybox:world" + spec.Steps[8].Image = "$(outputs.resources.imageToUse-ab.path)" + spec.Steps[9].Image = "$(outputs.resources.imageToUse-re.path)" + + spec.Volumes[0].Name = "world" + spec.Volumes[0].VolumeSource.ConfigMap.LocalObjectReference.Name = "world" + spec.Volumes[0].VolumeSource.ConfigMap.Items[0].Key = "world" + spec.Volumes[0].VolumeSource.ConfigMap.Items[0].Path = "world" + spec.Volumes[1].VolumeSource.Secret.SecretName = "world" + spec.Volumes[1].VolumeSource.Secret.Items[0].Key = "world" + spec.Volumes[1].VolumeSource.Secret.Items[0].Path = "world" + spec.Volumes[2].VolumeSource.PersistentVolumeClaim.ClaimName = "world" + spec.Volumes[3].VolumeSource.Projected.Sources[0].ConfigMap.Name = "world" + spec.Volumes[3].VolumeSource.Projected.Sources[0].Secret.Name = "world" + spec.Volumes[3].VolumeSource.Projected.Sources[0].ServiceAccountToken.Audience = "world" + spec.Volumes[4].VolumeSource.CSI.VolumeAttributes["secretProviderClass"] = "world" + spec.Volumes[4].VolumeSource.CSI.NodePublishSecretRef.Name = "world" + + spec.Sidecars[0].Image = "bar" + spec.Sidecars[0].Env[0].Value = "world" + }) + ctx := context.Background() + cfg := config.FromContextOrDefaults(ctx) + cfg.FeatureFlags.EnableAPIFields = config.AlphaAPIFields + ctx = config.ToContext(ctx, cfg) + got := resources.ApplyParameters(ctx, simpleTaskSpecArrayIndexing, tr, dp...) if d := cmp.Diff(want, got); d != "" { t.Errorf("ApplyParameters() got diff %s", diff.PrintWantGot(d)) } diff --git a/pkg/reconciler/taskrun/taskrun.go b/pkg/reconciler/taskrun/taskrun.go index d1bea6bb0cc..7199e223b2e 100644 --- a/pkg/reconciler/taskrun/taskrun.go +++ b/pkg/reconciler/taskrun/taskrun.go @@ -646,7 +646,7 @@ func (c *Reconciler) createPod(ctx context.Context, tr *v1beta1.TaskRun, rtr *re defaults = append(defaults, ts.Params...) } // Apply parameter substitution from the taskrun. - ts = resources.ApplyParameters(ts, tr, defaults...) + ts = resources.ApplyParameters(ctx, ts, tr, defaults...) // Apply context substitution from the taskrun ts = resources.ApplyContexts(ts, rtr.TaskName, tr)