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)) + } + }) + } +}