From d3b8017e652115ac102a5d10fc455240c1331a49 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 | 3 + .../pipelinerun-param-array-indexing.yaml | 58 +++ pkg/apis/pipeline/v1beta1/param_types_test.go | 7 + pkg/reconciler/pipelinerun/pipelinerun.go | 9 + pkg/reconciler/pipelinerun/resources/apply.go | 13 + .../pipelinerun/resources/apply_test.go | 309 ++++++++++++++ .../pipelinerun/resources/validate_params.go | 108 +++++ .../resources/validate_params_test.go | 399 ++++++++++++++++++ pkg/substitution/substitution.go | 38 +- pkg/substitution/substitution_test.go | 95 +++++ 10 files changed, 1038 insertions(+), 1 deletion(-) create mode 100644 examples/v1beta1/pipelineruns/alpha/pipelinerun-param-array-indexing.yaml diff --git a/docs/variables.md b/docs/variables.md index acab2fc722f..c627c0a73c5 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-th 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)) | diff --git a/examples/v1beta1/pipelineruns/alpha/pipelinerun-param-array-indexing.yaml b/examples/v1beta1/pipelineruns/alpha/pipelinerun-param-array-indexing.yaml new file mode 100644 index 00000000000..ac8600ccc49 --- /dev/null +++ b/examples/v1beta1/pipelineruns/alpha/pipelinerun-param-array-indexing.yaml @@ -0,0 +1,58 @@ +apiVersion: tekton.dev/v1beta1 +kind: Pipeline +metadata: + name: deploy +spec: + params: + - name: environments + type: array + tasks: + - name: deploy + params: + - name: environment1 + value: '$(params.environments[0])' + - name: environment2 + value: '$(params.environments[1])' + taskSpec: + params: + - name: environment1 + type: string + - name: environment2 + type: string + steps: + # this step should echo "staging" + - name: echo-params-1 + image: bash:3.2 + args: [ + "echo", + "$(params.environment1)", + ] + # this step should echo "staging" + - name: echo-params-2 + image: ubuntu + script: | + #!/bin/bash + VALUE=$(params.environment2) + EXPECTED="qa" + 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 +--- +apiVersion: tekton.dev/v1beta1 +kind: PipelineRun +metadata: + name: deployrun +spec: + pipelineRef: + name: deploy + params: + - name: environments + value: + - 'staging' + - 'qa' + - 'prod' diff --git a/pkg/apis/pipeline/v1beta1/param_types_test.go b/pkg/apis/pipeline/v1beta1/param_types_test.go index 40676152371..2a8f0d0b2d7 100644 --- a/pkg/apis/pipeline/v1beta1/param_types_test.go +++ b/pkg/apis/pipeline/v1beta1/param_types_test.go @@ -230,6 +230,13 @@ func TestArrayOrString_ApplyReplacements(t *testing.T) { arrayReplacements: map[string][]string{"params.myarray": {"a", "b", "c"}}, }, expectedOutput: v1beta1.NewArrayOrString("a", "b", "c"), + }, { + name: "array indexing replacement on string val", + args: args{ + input: v1beta1.NewArrayOrString("$(params.myarray[0])"), + stringReplacements: map[string]string{"params.myarray[0]": "a", "params.myarray[1]": "b"}, + }, + expectedOutput: v1beta1.NewArrayOrString("a"), }, { name: "object replacement on string val", args: args{ diff --git a/pkg/reconciler/pipelinerun/pipelinerun.go b/pkg/reconciler/pipelinerun/pipelinerun.go index 19038cd6cb7..99f63a9758a 100644 --- a/pkg/reconciler/pipelinerun/pipelinerun.go +++ b/pkg/reconciler/pipelinerun/pipelinerun.go @@ -448,6 +448,15 @@ func (c *Reconciler) reconcile(ctx context.Context, pr *v1beta1.PipelineRun, get return controller.NewPermanentError(err) } + // Ensure that the array reference is not out of bound + if err := resources.ValidateParamArrayIndex(ctx, pipelineSpec, pr); err != nil { + // This Run has failed, so we need to mark it as failed and stop reconciling it + pr.Status.MarkFailed(ReasonObjectParameterMissKeys, + "PipelineRun %s/%s parameters is missing object keys required by Pipeline %s/%s's parameters: %s", + pr.Namespace, pr.Name, pr.Namespace, pipelineMeta.Name, err) + return controller.NewPermanentError(err) + } + // Ensure that the workspaces expected by the Pipeline are provided by the PipelineRun. if err := resources.ValidateWorkspaceBindings(pipelineSpec, pr); err != nil { pr.Status.MarkFailed(ReasonInvalidWorkspaceBinding, diff --git a/pkg/reconciler/pipelinerun/resources/apply.go b/pkg/reconciler/pipelinerun/resources/apply.go index 254207bb4f5..6f93cf454dd 100644 --- a/pkg/reconciler/pipelinerun/resources/apply.go +++ b/pkg/reconciler/pipelinerun/resources/apply.go @@ -39,6 +39,7 @@ func ApplyParameters(ctx context.Context, p *v1beta1.PipelineSpec, pr *v1beta1.P stringReplacements := map[string]string{} arrayReplacements := map[string][]string{} objectReplacements := map[string]map[string]string{} + cfg := config.FromContextOrDefaults(ctx) patterns := []string{ "params.%s", @@ -55,6 +56,12 @@ func ApplyParameters(ctx context.Context, p *v1beta1.PipelineSpec, pr *v1beta1.P switch p.Default.Type { case v1beta1.ParamTypeArray: for _, pattern := range patterns { + // array indexing for param is alpha feature + 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 } case v1beta1.ParamTypeObject: @@ -76,6 +83,12 @@ func ApplyParameters(ctx context.Context, p *v1beta1.PipelineSpec, pr *v1beta1.P switch p.Value.Type { case v1beta1.ParamTypeArray: 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 } case v1beta1.ParamTypeObject: diff --git a/pkg/reconciler/pipelinerun/resources/apply_test.go b/pkg/reconciler/pipelinerun/resources/apply_test.go index db96a859d64..fb5c95cad5b 100644 --- a/pkg/reconciler/pipelinerun/resources/apply_test.go +++ b/pkg/reconciler/pipelinerun/resources/apply_test.go @@ -954,6 +954,315 @@ func TestApplyParameters(t *testing.T) { } } +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: "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)) + } + }) + } +} + func TestApplyTaskResults_MinimalExpression(t *testing.T) { for _, tt := range []struct { name string diff --git a/pkg/reconciler/pipelinerun/resources/validate_params.go b/pkg/reconciler/pipelinerun/resources/validate_params.go index dca9cb6ae9c..a7a465161c0 100644 --- a/pkg/reconciler/pipelinerun/resources/validate_params.go +++ b/pkg/reconciler/pipelinerun/resources/validate_params.go @@ -17,11 +17,15 @@ limitations under the License. package resources import ( + "context" "fmt" + "github.com/tektoncd/pipeline/pkg/apis/config" "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1beta1" "github.com/tektoncd/pipeline/pkg/list" "github.com/tektoncd/pipeline/pkg/reconciler/taskrun" + "github.com/tektoncd/pipeline/pkg/substitution" + "k8s.io/apimachinery/pkg/util/sets" ) // ValidateParamTypesMatching validate that parameters in PipelineRun override corresponding parameters in Pipeline of the same type. @@ -84,3 +88,107 @@ func ValidateObjectParamRequiredKeys(pipelineParameters []v1beta1.ParamSpec, pip return nil } + +// ValidateParamArrayIndex validate if the array indexing param reference target is existent +func ValidateParamArrayIndex(ctx context.Context, p *v1beta1.PipelineSpec, pr *v1beta1.PipelineRun) error { + cfg := config.FromContextOrDefaults(ctx) + if cfg.FeatureFlags.EnableAPIFields != config.AlphaAPIFields { + return nil + } + + arrayParams := extractParamIndexes(p.Params, pr.Spec.Params) + + outofBoundParams := sets.String{} + + // collect all the references + for i := range p.Tasks { + findInvalidParamArrayReferences(p.Tasks[i].Params, arrayParams, &outofBoundParams) + findInvalidParamArrayReferences(p.Tasks[i].Matrix, arrayParams, &outofBoundParams) + for j := range p.Tasks[i].Workspaces { + findInvalidParamArrayReference(p.Tasks[i].Workspaces[j].SubPath, arrayParams, &outofBoundParams) + } + for _, wes := range p.Tasks[i].WhenExpressions { + findInvalidParamArrayReference(wes.Input, arrayParams, &outofBoundParams) + for _, v := range wes.Values { + findInvalidParamArrayReference(v, arrayParams, &outofBoundParams) + } + } + } + + for i := range p.Finally { + findInvalidParamArrayReferences(p.Finally[i].Params, arrayParams, &outofBoundParams) + findInvalidParamArrayReferences(p.Finally[i].Matrix, arrayParams, &outofBoundParams) + for _, wes := range p.Finally[i].WhenExpressions { + for _, v := range wes.Values { + findInvalidParamArrayReference(v, arrayParams, &outofBoundParams) + } + } + } + + if outofBoundParams.Len() > 0 { + return fmt.Errorf("non-existent param references:%v", outofBoundParams.List()) + } + return nil +} + +func extractParamIndexes(defaults []v1beta1.ParamSpec, params []v1beta1.Param) map[string]int { + // Collect all array params + arrayParams := make(map[string]int) + + patterns := []string{ + "$(params.%s)", + "$(params[%q])", + "$(params['%s'])", + } + + // Collect array params lengths from defaults + for _, p := range defaults { + if p.Default != nil { + if p.Default.Type == v1beta1.ParamTypeArray { + for _, pattern := range patterns { + for i := 0; i < len(p.Default.ArrayVal); i++ { + arrayParams[fmt.Sprintf(pattern, p.Name)] = len(p.Default.ArrayVal) + } + } + } + } + } + + // Collect array params lengths from pipelinerun or taskrun + for _, p := range params { + if p.Value.Type == v1beta1.ParamTypeArray { + for _, pattern := range patterns { + for i := 0; i < len(p.Value.ArrayVal); i++ { + arrayParams[fmt.Sprintf(pattern, p.Name)] = len(p.Value.ArrayVal) + } + } + } + } + return arrayParams +} + +func findInvalidParamArrayReferences(params []v1beta1.Param, arrayParams map[string]int, outofBoundParams *sets.String) { + for i := range params { + findInvalidParamArrayReference(params[i].Value.StringVal, arrayParams, outofBoundParams) + for _, v := range params[i].Value.ArrayVal { + findInvalidParamArrayReference(v, arrayParams, outofBoundParams) + } + for _, v := range params[i].Value.ObjectVal { + findInvalidParamArrayReference(v, arrayParams, outofBoundParams) + } + } +} + +func findInvalidParamArrayReference(paramReference string, arrayParams map[string]int, outofBoundParams *sets.String) { + list := substitution.ExtractParamsExpressions(paramReference) + for _, val := range list { + indexString := substitution.ExtractIndexString(paramReference) + idx, _ := substitution.ExtractIndex(indexString) + v := substitution.TrimArrayIndex(val) + if paramLength, ok := arrayParams[v]; ok { + if idx >= paramLength { + outofBoundParams.Insert(val) + } + } + } +} diff --git a/pkg/reconciler/pipelinerun/resources/validate_params_test.go b/pkg/reconciler/pipelinerun/resources/validate_params_test.go index 7e5fefbab3f..570bc4d0a7f 100644 --- a/pkg/reconciler/pipelinerun/resources/validate_params_test.go +++ b/pkg/reconciler/pipelinerun/resources/validate_params_test.go @@ -17,10 +17,16 @@ limitations under the License. package resources import ( + "context" + "fmt" "testing" + "github.com/google/go-cmp/cmp" + "github.com/tektoncd/pipeline/pkg/apis/config" "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1beta1" + "github.com/tektoncd/pipeline/test/diff" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/selection" ) func TestValidateParamTypesMatching_Valid(t *testing.T) { @@ -295,3 +301,396 @@ func TestValidateObjectParamRequiredKeys_Valid(t *testing.T) { }) } } + +func TestValidateParamArrayIndex_valid(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 + }{{ + 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")}}, + }, { + 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")}}, + }, { + 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. + }, { + 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")}, + }, + }, { + 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")}}, + }, { + 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")}}, + }, { + 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")}, + }, + }, { + 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")}}, + }, + } { + tt := tt // capture range variable + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + run := &v1beta1.PipelineRun{ + Spec: v1beta1.PipelineRunSpec{ + Params: tt.params, + }, + } + err := ValidateParamArrayIndex(ctx, &tt.original, run) + if err != nil { + t.Errorf("ValidateParamArrayIndex() got err %s", err) + } + }) + } +} + +func TestValidateParamArrayIndex_invalid(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 error + }{{ + name: "single parameter reference out of bound", + 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[2])")}, + {Name: "first-task-second-param", Value: *v1beta1.NewArrayOrString("$(params.second-param[2])")}, + {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: fmt.Errorf("non-existent param references:[$(params.first-param[2]) $(params.second-param[2])]"), + }, { + name: "single parameter reference with when expression out of bound", + 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[2])", + Operator: selection.In, + Values: []string{"$(params.second-param[2])"}, + }}, + }}, + }, + params: []v1beta1.Param{{Name: "second-param", Value: *v1beta1.NewArrayOrString("second-value", "second-value-again")}}, + expected: fmt.Errorf("non-existent param references:[$(params.first-param[2]) $(params.second-param[2])]"), + }, { + name: "pipeline parameter reference nested inside task parameter out of bound", + 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[2]))")}, + {Name: "first-task-second-param", Value: *v1beta1.NewArrayOrString("$(input.workspace.$(params.second-param[2]))")}, + }, + }}, + }, + params: nil, // no parameter values. + expected: fmt.Errorf("non-existent param references:[$(params.first-param[2]) $(params.second-param[2])]"), + }, { + name: "array parameter reference out of bound", + 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[3])")}, + {Name: "first-task-second-param", Value: *v1beta1.NewArrayOrString("firstelement", "$(params.second-param[4])")}, + }, + }}, + }, + params: []v1beta1.Param{ + {Name: "second-param", Value: *v1beta1.NewArrayOrString("second-value", "array")}, + }, + expected: fmt.Errorf("non-existent param references:[$(params.first-param[3]) $(params.second-param[4])]"), + }, { + name: "object parameter reference out of bound", + 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.NewObject(map[string]string{ + "val1": "$(params.first-param[4])", + "val2": "$(params.second-param[4])", + })}, + }, + }}, + }, + params: []v1beta1.Param{ + {Name: "second-param", Value: *v1beta1.NewArrayOrString("second-value", "array")}, + }, + expected: fmt.Errorf("non-existent param references:[$(params.first-param[4]) $(params.second-param[4])]"), + }, { + name: "parameter evaluation with final tasks reference out of bound", + 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[2])")}, + {Name: "final-task-second-param", Value: *v1beta1.NewArrayOrString("$(params.second-param[2])")}, + }, + 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: fmt.Errorf("non-existent param references:[$(params.first-param[2]) $(params.second-param[2])]"), + }, { + name: "parameter evaluation with both tasks and final tasks reference out of bound", + 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[2])")}, + {Name: "final-task-second-param", Value: *v1beta1.NewArrayOrString("$(params.second-param[2])")}, + }, + }}, + Finally: []v1beta1.PipelineTask{{ + Params: []v1beta1.Param{ + {Name: "final-task-first-param", Value: *v1beta1.NewArrayOrString("$(params.first-param[3])")}, + {Name: "final-task-second-param", Value: *v1beta1.NewArrayOrString("$(params.second-param[3])")}, + }, + WhenExpressions: v1beta1.WhenExpressions{{ + Input: "$(params.first-param[2])", + Operator: selection.In, + Values: []string{"$(params.second-param[2])"}, + }}, + }}, + }, + params: []v1beta1.Param{{Name: "second-param", Value: *v1beta1.NewArrayOrString("second-value", "second-value-again")}}, + expected: fmt.Errorf("non-existent param references:[$(params.first-param[2]) $(params.first-param[3]) $(params.second-param[2]) $(params.second-param[3])]"), + }, { + name: "parameter references with bracket notation and special characters reference out of bound", + 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"][2])`)}, + {Name: "first-task-second-param", Value: *v1beta1.NewArrayOrString(`$(params["second/param"][2])`)}, + {Name: "first-task-third-param", Value: *v1beta1.NewArrayOrString(`$(params['third.param'][2])`)}, + {Name: "first-task-fourth-param", Value: *v1beta1.NewArrayOrString(`$(params['fourth/param'][2])`)}, + {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: fmt.Errorf("non-existent param references:[$(params[\"first.param\"][2]) $(params[\"second/param\"][2]) $(params['fourth/param'][2]) $(params['third.param'][2])]"), + }, { + name: "single parameter in workspace subpath reference out of bound", + 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[2])")}, + {Name: "first-task-second-param", Value: *v1beta1.NewArrayOrString("static value")}, + }, + Workspaces: []v1beta1.WorkspacePipelineTaskBinding{ + { + Name: "first-workspace", + Workspace: "first-workspace", + SubPath: "$(params.second-param[3])", + }, + }, + }}, + }, + params: []v1beta1.Param{{Name: "second-param", Value: *v1beta1.NewArrayOrString("second-value", "second-value-again")}}, + expected: fmt.Errorf("non-existent param references:[$(params.first-param[2]) $(params.second-param[3])]"), + }, + } { + tt := tt // capture range variable + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + run := &v1beta1.PipelineRun{ + Spec: v1beta1.PipelineRunSpec{ + Params: tt.params, + }, + } + err := ValidateParamArrayIndex(ctx, &tt.original, run) + if d := cmp.Diff(tt.expected.Error(), err.Error()); d != "" { + t.Errorf("ValidateParamArrayIndex() errors diff %s", diff.PrintWantGot(d)) + } + }) + } +} diff --git a/pkg/substitution/substitution.go b/pkg/substitution/substitution.go index 45b65a0f96a..93cac332bc9 100644 --- a/pkg/substitution/substitution.go +++ b/pkg/substitution/substitution.go @@ -19,6 +19,7 @@ package substitution import ( "fmt" "regexp" + "strconv" "strings" "k8s.io/apimachinery/pkg/util/sets" @@ -30,8 +31,23 @@ const ( // braceMatchingRegex is a regex for parameter references including dot notation, bracket notation with single and double quotes. braceMatchingRegex = "(\\$(\\(%s(\\.(?P%s)|\\[\"(?P%s)\"\\]|\\['(?P%s)'\\])\\)))" + // arrayIndexing will match all `[int]` and `[*]` for parseExpression + arrayIndexing = `\[([0-9])*\*?\]` + // paramIndex will match all `$(params.paramName[int])` expressions + paramIndexing = `\$\(params(\.[_a-zA-Z0-9.-]+|\[\'[_a-zA-Z0-9.-\/]+\'\]|\[\"[_a-zA-Z0-9.-\/]+\"\])\[[0-9]+\]\)` + // intIndex will match all `[int]` expressions + intIndex = `\[[0-9]+\]` ) +// arrayIndexingRegex is used to match `[int]` and `[*]` +var arrayIndexingRegex = regexp.MustCompile(arrayIndexing) + +// paramIndexingRegex will match all `$(params.paramName[int])` expressions +var paramIndexingRegex = regexp.MustCompile(paramIndexing) + +// intIndexRegex will match all `[int]` for param expression +var intIndexRegex = regexp.MustCompile(intIndex) + // ValidateVariable makes sure all variables in the provided string are known func ValidateVariable(name, value, prefix, locationName, path string, vars sets.String) *apis.FieldError { if vs, present, _ := extractVariablesFromString(value, prefix); present { @@ -59,7 +75,7 @@ func ValidateVariableP(value, prefix string, vars sets.String) *apis.FieldError } for _, v := range vs { - v = strings.TrimSuffix(v, "[*]") + v = TrimArrayIndex(v) if !vars.Has(v) { return &apis.FieldError{ Message: fmt.Sprintf("non-existent variable in %q", value), @@ -325,3 +341,23 @@ func ApplyArrayReplacements(in string, stringReplacements map[string]string, arr // Otherwise return a size-1 array containing the input string with standard stringReplacements applied. return []string{ApplyReplacements(in, stringReplacements)} } + +// TrimArrayIndex replaces all `[i]` and `[*]` to "". +func TrimArrayIndex(s string) string { + return arrayIndexingRegex.ReplaceAllString(s, "") +} + +// ExtractParamsExpressions will find all `$(params.paramName[int])` expressions +func ExtractParamsExpressions(s string) []string { + return paramIndexingRegex.FindAllString(s, -1) +} + +// ExtractIndexString will find the leftmost match of `[int]` +func ExtractIndexString(s string) string { + return intIndexRegex.FindString(s) +} + +// ExtractIndex will extract int from `[int]` +func ExtractIndex(s string) (int, error) { + return strconv.Atoi(strings.TrimSuffix(strings.TrimPrefix(s, "["), "]")) +} diff --git a/pkg/substitution/substitution_test.go b/pkg/substitution/substitution_test.go index bced595a17b..ea8d1b9ae29 100644 --- a/pkg/substitution/substitution_test.go +++ b/pkg/substitution/substitution_test.go @@ -478,3 +478,98 @@ func TestApplyArrayReplacements(t *testing.T) { }) } } + +func TestExtractParamsExpressions(t *testing.T) { + tests := []struct { + name string + input string + want []string + }{{ + name: "normal string", + input: "hello world", + want: nil, + }, { + name: "param reference", + input: "$(params.paramName)", + want: nil, + }, { + name: "param star reference", + input: "$(params.paramName[*])", + want: nil, + }, { + name: "param index reference", + input: "$(params.paramName[1])", + want: []string{"$(params.paramName[1])"}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := substitution.ExtractParamsExpressions(tt.input) + if d := cmp.Diff(tt.want, got); d != "" { + t.Error(diff.PrintWantGot(d)) + } + }) + } +} + +func TestExtractIntIndex(t *testing.T) { + tests := []struct { + name string + input string + want string + }{{ + name: "normal string", + input: "hello world", + want: "", + }, { + name: "param reference", + input: "$(params.paramName)", + want: "", + }, { + name: "param star reference", + input: "$(params.paramName[*])", + want: "", + }, { + name: "param index reference", + input: "$(params.paramName[1])", + want: "[1]", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := substitution.ExtractIndexString(tt.input) + if d := cmp.Diff(tt.want, got); d != "" { + t.Error(diff.PrintWantGot(d)) + } + }) + } +} + +func TestTrimSquareBrackets(t *testing.T) { + tests := []struct { + name string + input string + want int + }{{ + name: "normal string", + input: "hello world", + want: 0, + }, { + name: "star in square bracket", + input: "[*]", + want: 0, + }, { + name: "index in square bracket", + input: "[1]", + want: 1, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, _ := substitution.ExtractIndex(tt.input) + if d := cmp.Diff(tt.want, got); d != "" { + t.Error(diff.PrintWantGot(d)) + } + }) + } +}