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