diff --git a/docs/variables.md b/docs/variables.md
index c627c0a73c5..f4967dde0b1 100644
--- a/docs/variables.md
+++ b/docs/variables.md
@@ -50,6 +50,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) |
| `resources.inputs..path` | The path to the input resource's directory. |
| `resources.outputs..path` | The path to the output resource's directory. |
| `results..path` | The path to the file where the `Task` writes its results data. |
diff --git a/examples/v1beta1/taskruns/alpha/param_array_indexing.yaml b/examples/v1beta1/taskruns/alpha/param_array_indexing.yaml
new file mode 100644
index 00000000000..505152346e2
--- /dev/null
+++ b/examples/v1beta1/taskruns/alpha/param_array_indexing.yaml
@@ -0,0 +1,37 @@
+apiVersion: tekton.dev/v1beta1
+kind: TaskRun
+metadata:
+ name: params-array-indexing
+spec:
+ params:
+ - name: array-to-echo
+ value:
+ - "foo"
+ - "bar"
+ taskSpec:
+ params:
+ - name: array-to-echo
+ type: array
+ steps:
+ # this step should echo "foo"
+ - name: echo-params-1
+ image: bash:3.2
+ args: [
+ "echo",
+ "$(params.array-to-echo[0])",
+ ]
+ # this step should echo "bar"
+ - name: echo-params-2
+ image: ubuntu
+ script: |
+ #!/bin/bash
+ VALUE=$(params.array-to-echo[1])
+ EXPECTED="bar"
+ 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
diff --git a/pkg/reconciler/taskrun/resources/apply.go b/pkg/reconciler/taskrun/resources/apply.go
index 597d827f22e..ab6c5bb4511 100644
--- a/pkg/reconciler/taskrun/resources/apply.go
+++ b/pkg/reconciler/taskrun/resources/apply.go
@@ -33,13 +33,14 @@ import (
)
// ApplyParameters applies the params from a TaskRun.Input.Parameters to a TaskSpec
-func ApplyParameters(spec *v1beta1.TaskSpec, tr *v1beta1.TaskRun, defaults ...v1beta1.ParamSpec) *v1beta1.TaskSpec {
+func ApplyParameters(ctx context.Context, spec *v1beta1.TaskSpec, tr *v1beta1.TaskRun, defaults ...v1beta1.ParamSpec) *v1beta1.TaskSpec {
// This assumes that the TaskRun inputs have been validated against what the Task requests.
// stringReplacements is used for standard single-string stringReplacements, while arrayReplacements contains arrays
// that need to be further processed.
stringReplacements := map[string]string{}
arrayReplacements := map[string][]string{}
+ cfg := config.FromContextOrDefaults(ctx)
patterns := []string{
"params.%s",
@@ -58,6 +59,12 @@ func ApplyParameters(spec *v1beta1.TaskSpec, tr *v1beta1.TaskRun, defaults ...v1
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(spec *v1beta1.TaskSpec, tr *v1beta1.TaskRun, defaults ...v1
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/taskrun/resources/apply_test.go b/pkg/reconciler/taskrun/resources/apply_test.go
index 2f8cf80249b..ab4d6634e6e 100644
--- a/pkg/reconciler/taskrun/resources/apply_test.go
+++ b/pkg/reconciler/taskrun/resources/apply_test.go
@@ -344,6 +344,180 @@ var (
}},
}
+ simpleTaskSpecArrayIndexing = &v1beta1.TaskSpec{
+ Sidecars: []v1beta1.Sidecar{{
+ Name: "foo",
+ Image: `$(params["myimage"][0])`,
+ Env: []corev1.EnvVar{{
+ Name: "foo",
+ Value: "$(params['FOO'][1])",
+ }},
+ }},
+ StepTemplate: &v1beta1.StepTemplate{
+ Env: []corev1.EnvVar{{
+ Name: "template-var",
+ Value: `$(params["FOO"][1])`,
+ }},
+ Image: "$(params.myimage[0])",
+ },
+ Steps: []v1beta1.Step{{
+ Name: "foo",
+ Image: "$(params.myimage[0])",
+ }, {
+ Name: "baz",
+ Image: "bat",
+ WorkingDir: "$(inputs.resources.workspace.path)",
+ Args: []string{"$(inputs.resources.workspace.url)"},
+ }, {
+ Name: "qux",
+ Image: "$(params.something[0])",
+ Args: []string{"$(outputs.resources.imageToUse.url)"},
+ }, {
+ Name: "foo",
+ Image: `$(params["myimage"][0])`,
+ }, {
+ Name: "baz",
+ Image: "$(params.somethingelse)",
+ WorkingDir: "$(inputs.resources.workspace.path)",
+ Args: []string{"$(inputs.resources.workspace.url)"},
+ }, {
+ Name: "qux",
+ Image: "quux",
+ Args: []string{"$(outputs.resources.imageToUse.url)"},
+ }, {
+ Name: "foo",
+ Image: "busybox:$(params.FOO[1])",
+ VolumeMounts: []corev1.VolumeMount{{
+ Name: "$(params.FOO[1])",
+ MountPath: "path/to/$(params.FOO[1])",
+ SubPath: "sub/$(params.FOO[1])/path",
+ }},
+ }, {
+ Name: "foo",
+ Image: "busybox:$(params.FOO[1])",
+ Env: []corev1.EnvVar{{
+ Name: "foo",
+ Value: "value-$(params.FOO[1])",
+ }, {
+ Name: "bar",
+ ValueFrom: &corev1.EnvVarSource{
+ ConfigMapKeyRef: &corev1.ConfigMapKeySelector{
+ LocalObjectReference: corev1.LocalObjectReference{Name: "config-$(params.FOO[1])"},
+ Key: "config-key-$(params.FOO[1])",
+ },
+ },
+ }, {
+ Name: "baz",
+ ValueFrom: &corev1.EnvVarSource{
+ SecretKeyRef: &corev1.SecretKeySelector{
+ LocalObjectReference: corev1.LocalObjectReference{Name: "secret-$(params.FOO[1])"},
+ Key: "secret-key-$(params.FOO[1])",
+ },
+ },
+ }},
+ EnvFrom: []corev1.EnvFromSource{{
+ Prefix: "prefix-0-$(params.FOO[1])",
+ ConfigMapRef: &corev1.ConfigMapEnvSource{
+ LocalObjectReference: corev1.LocalObjectReference{Name: "config-$(params.FOO[1])"},
+ },
+ }, {
+ Prefix: "prefix-1-$(params.FOO[1])",
+ SecretRef: &corev1.SecretEnvSource{
+ LocalObjectReference: corev1.LocalObjectReference{Name: "secret-$(params.FOO[1])"},
+ },
+ }},
+ }, {
+ Name: "outputs-resources-path-ab",
+ Image: "$(outputs.resources.imageToUse-ab.path)",
+ }, {
+ Name: "outputs-resources-path-re",
+ Image: "$(outputs.resources.imageToUse-re.path)",
+ }},
+ Volumes: []corev1.Volume{{
+ Name: "$(params.FOO[1])",
+ VolumeSource: corev1.VolumeSource{
+ ConfigMap: &corev1.ConfigMapVolumeSource{
+ LocalObjectReference: corev1.LocalObjectReference{
+ Name: "$(params.FOO[1])",
+ },
+ Items: []corev1.KeyToPath{{
+ Key: "$(params.FOO[1])",
+ Path: "$(params.FOO[1])",
+ }},
+ },
+ },
+ }, {
+ Name: "some-secret",
+ VolumeSource: corev1.VolumeSource{
+ Secret: &corev1.SecretVolumeSource{
+ SecretName: "$(params.FOO[1])",
+ Items: []corev1.KeyToPath{{
+ Key: "$(params.FOO[1])",
+ Path: "$(params.FOO[1])",
+ }},
+ },
+ },
+ }, {
+ Name: "some-pvc",
+ VolumeSource: corev1.VolumeSource{
+ PersistentVolumeClaim: &corev1.PersistentVolumeClaimVolumeSource{
+ ClaimName: "$(params.FOO[1])",
+ },
+ },
+ }, {
+ Name: "some-projected-volumes",
+ VolumeSource: corev1.VolumeSource{
+ Projected: &corev1.ProjectedVolumeSource{
+ Sources: []corev1.VolumeProjection{{
+ ConfigMap: &corev1.ConfigMapProjection{
+ LocalObjectReference: corev1.LocalObjectReference{
+ Name: "$(params.FOO[1])",
+ },
+ },
+ Secret: &corev1.SecretProjection{
+ LocalObjectReference: corev1.LocalObjectReference{
+ Name: "$(params.FOO[1])",
+ },
+ },
+ ServiceAccountToken: &corev1.ServiceAccountTokenProjection{
+ Audience: "$(params.FOO[1])",
+ },
+ }},
+ },
+ },
+ }, {
+ Name: "some-csi",
+ VolumeSource: corev1.VolumeSource{
+ CSI: &corev1.CSIVolumeSource{
+ VolumeAttributes: map[string]string{
+ "secretProviderClass": "$(params.FOO[1])",
+ },
+ NodePublishSecretRef: &corev1.LocalObjectReference{
+ Name: "$(params.FOO[1])",
+ },
+ },
+ },
+ }},
+ Resources: &v1beta1.TaskResources{
+ Inputs: []v1beta1.TaskResource{{
+ ResourceDeclaration: v1beta1.ResourceDeclaration{
+ Name: "workspace",
+ },
+ }},
+ Outputs: []v1beta1.TaskResource{{
+ ResourceDeclaration: v1beta1.ResourceDeclaration{
+ Name: "imageToUse-ab",
+ TargetPath: "/foo/builtImage",
+ },
+ }, {
+ ResourceDeclaration: v1beta1.ResourceDeclaration{
+ Name: "imageToUse-re",
+ TargetPath: "foo/builtImage",
+ },
+ }},
+ },
+ }
+
gcsTaskSpec = &v1beta1.TaskSpec{
Steps: []v1beta1.Step{{
Name: "foobar",
@@ -697,7 +871,7 @@ func TestApplyArrayParameters(t *testing.T) {
}}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
- got := resources.ApplyParameters(tt.args.ts, tt.args.tr, tt.args.dp...)
+ got := resources.ApplyParameters(context.Background(), tt.args.ts, tt.args.tr, tt.args.dp...)
if d := cmp.Diff(tt.want, got); d != "" {
t.Errorf("ApplyParameters() got diff %s", diff.PrintWantGot(d))
}
@@ -768,7 +942,80 @@ func TestApplyParameters(t *testing.T) {
spec.Sidecars[0].Image = "bar"
spec.Sidecars[0].Env[0].Value = "world"
})
- got := resources.ApplyParameters(simpleTaskSpec, tr, dp...)
+ got := resources.ApplyParameters(context.Background(), simpleTaskSpec, tr, dp...)
+ if d := cmp.Diff(want, got); d != "" {
+ t.Errorf("ApplyParameters() got diff %s", diff.PrintWantGot(d))
+ }
+}
+
+func TestApplyParameters_ArrayIndexing(t *testing.T) {
+ tr := &v1beta1.TaskRun{
+ Spec: v1beta1.TaskRunSpec{
+ Params: []v1beta1.Param{{
+ Name: "myimage",
+ Value: *v1beta1.NewArrayOrString("bar", "foo"),
+ }, {
+ Name: "FOO",
+ Value: *v1beta1.NewArrayOrString("hello", "world"),
+ }},
+ },
+ }
+ dp := []v1beta1.ParamSpec{{
+ Name: "something",
+ Default: v1beta1.NewArrayOrString("mydefault", "mydefault2"),
+ }, {
+ Name: "somethingelse",
+ Default: v1beta1.NewArrayOrString(""),
+ }}
+ want := applyMutation(simpleTaskSpec, func(spec *v1beta1.TaskSpec) {
+ spec.StepTemplate.Env[0].Value = "world"
+ spec.StepTemplate.Image = "bar"
+
+ spec.Steps[0].Image = "bar"
+ spec.Steps[2].Image = "mydefault"
+ spec.Steps[3].Image = "bar"
+ spec.Steps[4].Image = ""
+
+ spec.Steps[6].VolumeMounts[0].Name = "world"
+ spec.Steps[6].VolumeMounts[0].SubPath = "sub/world/path"
+ spec.Steps[6].VolumeMounts[0].MountPath = "path/to/world"
+ spec.Steps[6].Image = "busybox:world"
+
+ spec.Steps[7].Env[0].Value = "value-world"
+ spec.Steps[7].Env[1].ValueFrom.ConfigMapKeyRef.LocalObjectReference.Name = "config-world"
+ spec.Steps[7].Env[1].ValueFrom.ConfigMapKeyRef.Key = "config-key-world"
+ spec.Steps[7].Env[2].ValueFrom.SecretKeyRef.LocalObjectReference.Name = "secret-world"
+ spec.Steps[7].Env[2].ValueFrom.SecretKeyRef.Key = "secret-key-world"
+ spec.Steps[7].EnvFrom[0].Prefix = "prefix-0-world"
+ spec.Steps[7].EnvFrom[0].ConfigMapRef.LocalObjectReference.Name = "config-world"
+ spec.Steps[7].EnvFrom[1].Prefix = "prefix-1-world"
+ spec.Steps[7].EnvFrom[1].SecretRef.LocalObjectReference.Name = "secret-world"
+ spec.Steps[7].Image = "busybox:world"
+ spec.Steps[8].Image = "$(outputs.resources.imageToUse-ab.path)"
+ spec.Steps[9].Image = "$(outputs.resources.imageToUse-re.path)"
+
+ spec.Volumes[0].Name = "world"
+ spec.Volumes[0].VolumeSource.ConfigMap.LocalObjectReference.Name = "world"
+ spec.Volumes[0].VolumeSource.ConfigMap.Items[0].Key = "world"
+ spec.Volumes[0].VolumeSource.ConfigMap.Items[0].Path = "world"
+ spec.Volumes[1].VolumeSource.Secret.SecretName = "world"
+ spec.Volumes[1].VolumeSource.Secret.Items[0].Key = "world"
+ spec.Volumes[1].VolumeSource.Secret.Items[0].Path = "world"
+ spec.Volumes[2].VolumeSource.PersistentVolumeClaim.ClaimName = "world"
+ spec.Volumes[3].VolumeSource.Projected.Sources[0].ConfigMap.Name = "world"
+ spec.Volumes[3].VolumeSource.Projected.Sources[0].Secret.Name = "world"
+ spec.Volumes[3].VolumeSource.Projected.Sources[0].ServiceAccountToken.Audience = "world"
+ spec.Volumes[4].VolumeSource.CSI.VolumeAttributes["secretProviderClass"] = "world"
+ spec.Volumes[4].VolumeSource.CSI.NodePublishSecretRef.Name = "world"
+
+ spec.Sidecars[0].Image = "bar"
+ spec.Sidecars[0].Env[0].Value = "world"
+ })
+ ctx := context.Background()
+ cfg := config.FromContextOrDefaults(ctx)
+ cfg.FeatureFlags.EnableAPIFields = config.AlphaAPIFields
+ ctx = config.ToContext(ctx, cfg)
+ got := resources.ApplyParameters(ctx, simpleTaskSpecArrayIndexing, tr, dp...)
if d := cmp.Diff(want, got); d != "" {
t.Errorf("ApplyParameters() got diff %s", diff.PrintWantGot(d))
}
@@ -834,7 +1081,7 @@ func TestApplyObjectParameters(t *testing.T) {
spec.Volumes[3].VolumeSource.CSI.VolumeAttributes["secretProviderClass"] = "taskrun-value-for-key1"
spec.Volumes[3].VolumeSource.CSI.NodePublishSecretRef.Name = "taskrun-value-for-key1"
})
- got := resources.ApplyParameters(objectParamTaskSpec, tr, dp...)
+ got := resources.ApplyParameters(context.Background(), objectParamTaskSpec, tr, dp...)
if d := cmp.Diff(want, got); d != "" {
t.Errorf("ApplyParameters() got diff %s", diff.PrintWantGot(d))
}
diff --git a/pkg/reconciler/taskrun/taskrun.go b/pkg/reconciler/taskrun/taskrun.go
index 90f7beb56e6..9a903d7fdd2 100644
--- a/pkg/reconciler/taskrun/taskrun.go
+++ b/pkg/reconciler/taskrun/taskrun.go
@@ -372,6 +372,12 @@ func (c *Reconciler) prepare(ctx context.Context, tr *v1beta1.TaskRun) (*v1beta1
return nil, nil, controller.NewPermanentError(err)
}
+ if err := validateParamArrayIndex(ctx, tr.Spec.Params, rtr.TaskSpec); err != nil {
+ logger.Errorf("TaskRun %q Param references are invalid: %v", tr.Name, err)
+ tr.Status.MarkResourceFailed(podconvert.ReasonFailedValidation, err)
+ return nil, nil, controller.NewPermanentError(err)
+ }
+
if err := c.updateTaskRunWithDefaultWorkspaces(ctx, tr, taskSpec); err != nil {
logger.Errorf("Failed to update taskrun %s with default workspace: %v", tr.Name, err)
tr.Status.MarkResourceFailed(podconvert.ReasonFailedResolution, err)
@@ -418,7 +424,7 @@ func (c *Reconciler) reconcile(ctx context.Context, tr *v1beta1.TaskRun, rtr *re
logger := logging.FromContext(ctx)
recorder := controller.GetEventRecorder(ctx)
- ts := updateTaskSpecParamsContextsResults(tr, rtr)
+ ts := updateTaskSpecParamsContextsResults(ctx, tr, rtr)
tr.Status.TaskSpec = ts
// Get the TaskRun's Pod if it should have one. Otherwise, create the Pod.
@@ -740,14 +746,14 @@ func (c *Reconciler) createPod(ctx context.Context, ts *v1beta1.TaskSpec, tr *v1
return pod, err
}
-func updateTaskSpecParamsContextsResults(tr *v1beta1.TaskRun, rtr *resources.ResolvedTaskResources) *v1beta1.TaskSpec {
+func updateTaskSpecParamsContextsResults(ctx context.Context, tr *v1beta1.TaskRun, rtr *resources.ResolvedTaskResources) *v1beta1.TaskSpec {
ts := rtr.TaskSpec.DeepCopy()
var defaults []v1beta1.ParamSpec
if len(ts.Params) > 0 {
defaults = append(defaults, ts.Params...)
}
// Apply parameter substitution from the taskrun.
- ts = resources.ApplyParameters(ts, tr, defaults...)
+ ts = resources.ApplyParameters(ctx, ts, tr, defaults...)
// Apply context substitution from the taskrun
ts = resources.ApplyContexts(ts, rtr.TaskName, tr)
diff --git a/pkg/reconciler/taskrun/taskrun_test.go b/pkg/reconciler/taskrun/taskrun_test.go
index 878e9a7bf34..7e2b450e69f 100644
--- a/pkg/reconciler/taskrun/taskrun_test.go
+++ b/pkg/reconciler/taskrun/taskrun_test.go
@@ -2297,7 +2297,7 @@ spec:
Kind: "Task",
TaskSpec: &v1beta1.TaskSpec{Steps: simpleTask.Spec.Steps, Workspaces: simpleTask.Spec.Workspaces},
}
- taskSpec := updateTaskSpecParamsContextsResults(taskRun, rtr)
+ taskSpec := updateTaskSpecParamsContextsResults(context.Background(), taskRun, rtr)
pod, err := r.createPod(testAssets.Ctx, taskSpec, taskRun, rtr)
if err != nil {
@@ -2401,7 +2401,7 @@ spec:
TaskSpec: &v1beta1.TaskSpec{Steps: simpleTask.Spec.Steps, Workspaces: simpleTask.Spec.Workspaces},
}
- taskSpec := updateTaskSpecParamsContextsResults(taskRun, rtr)
+ taskSpec := updateTaskSpecParamsContextsResults(context.Background(), taskRun, rtr)
_, err := r.createPod(testAssets.Ctx, taskSpec, taskRun, rtr)
if err == nil || err.Error() != expectedError {
diff --git a/pkg/reconciler/taskrun/validate_resources.go b/pkg/reconciler/taskrun/validate_resources.go
index d6c65d6c180..e6ff5e9688f 100644
--- a/pkg/reconciler/taskrun/validate_resources.go
+++ b/pkg/reconciler/taskrun/validate_resources.go
@@ -19,17 +19,33 @@ package taskrun
import (
"context"
"fmt"
+ "regexp"
"github.com/tektoncd/pipeline/pkg/apis/config"
"github.com/tektoncd/pipeline/pkg/apis/pipeline/v1beta1"
resourcev1alpha1 "github.com/tektoncd/pipeline/pkg/apis/resource/v1alpha1"
"github.com/tektoncd/pipeline/pkg/list"
"github.com/tektoncd/pipeline/pkg/reconciler/taskrun/resources"
+ "github.com/tektoncd/pipeline/pkg/substitution"
"k8s.io/apimachinery/pkg/util/sets"
"github.com/hashicorp/go-multierror"
+ corev1 "k8s.io/api/core/v1"
)
+const (
+ // paramIndex will match all `[int]` for param expression
+ paramIndexing = `\$\(params(\.[_a-zA-Z0-9.-]+|\[\'[_a-zA-Z0-9.-]+\'\]|\[\"[_a-zA-Z0-9.-]+\"\])\[[0-9]+\]\)`
+ // paramIndex will match all `[int]` for param expression
+ paramIndex = `\[[0-9]+\]`
+)
+
+// paramIndexRegex will match all `[int]` for param expression
+var paramIndexingRegex = regexp.MustCompile(paramIndexing)
+
+// paramIndexRegex will match all `[int]` for param expression
+var paramIndexRegex = regexp.MustCompile(paramIndex)
+
func validateResources(requiredResources []v1beta1.TaskResource, providedResources map[string]*resourcev1alpha1.PipelineResource) error {
required := make([]string, 0, len(requiredResources))
optional := make([]string, 0, len(requiredResources))
@@ -353,3 +369,200 @@ func missingKeysofObjectResults(tr *v1beta1.TaskRun, specResults []v1beta1.TaskR
}
return findMissingKeys(neededKeys, providedKeys)
}
+
+func validateParamArrayIndex(ctx context.Context, params []v1beta1.Param, spec *v1beta1.TaskSpec) error {
+ cfg := config.FromContextOrDefaults(ctx)
+ if cfg.FeatureFlags.EnableAPIFields != config.AlphaAPIFields {
+ return nil
+ }
+
+ var defaults []v1beta1.ParamSpec
+ if len(spec.Params) > 0 {
+ defaults = append(defaults, spec.Params...)
+ }
+ // 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 pipeline
+ 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)
+ }
+ }
+ }
+ }
+
+ outofBoundParams := sets.String{}
+
+ // Validate array param in steps fields.
+ validateStepsParamArrayIndexing(spec.Steps, arrayParams, &outofBoundParams)
+
+ // Validate array param in StepTemplate fields.
+ validateStepsTemplateParamArrayIndexing(spec.StepTemplate, arrayParams, &outofBoundParams)
+
+ // Validate array param in build's volumes
+ validateVolumesParamArrayIndexing(spec.Volumes, arrayParams, &outofBoundParams)
+
+ for _, v := range spec.Workspaces {
+ extractParamIndex(v.MountPath, arrayParams, &outofBoundParams)
+ }
+
+ validateSidecarsParamArrayIndexing(spec.Sidecars, arrayParams, &outofBoundParams)
+
+ if outofBoundParams.Len() > 0 {
+ return fmt.Errorf("non-existent param references:%v", outofBoundParams.List())
+ }
+
+ return nil
+}
+
+func extractParamIndex(paramReference string, arrayParams map[string]int, outofBoundParams *sets.String) {
+ list := paramIndexingRegex.FindAllString(paramReference, -1)
+ 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)
+ }
+ }
+ }
+}
+
+func validateStepsParamArrayIndexing(steps []v1beta1.Step, arrayParams map[string]int, outofBoundParams *sets.String) {
+ for _, step := range steps {
+ extractParamIndex(step.Script, arrayParams, outofBoundParams)
+ container := step.ToK8sContainer()
+ validateContainerParamArrayIndexing(container, arrayParams, outofBoundParams)
+ }
+}
+
+func validateStepsTemplateParamArrayIndexing(stepTemplate *v1beta1.StepTemplate, arrayParams map[string]int, outofBoundParams *sets.String) {
+ if stepTemplate == nil {
+ return
+ }
+ container := stepTemplate.ToK8sContainer()
+ validateContainerParamArrayIndexing(container, arrayParams, outofBoundParams)
+}
+
+func validateSidecarsParamArrayIndexing(sidecars []v1beta1.Sidecar, arrayParams map[string]int, outofBoundParams *sets.String) {
+ for _, s := range sidecars {
+ extractParamIndex(s.Script, arrayParams, outofBoundParams)
+ container := s.ToK8sContainer()
+ validateContainerParamArrayIndexing(container, arrayParams, outofBoundParams)
+ }
+}
+
+func validateVolumesParamArrayIndexing(volumes []corev1.Volume, arrayParams map[string]int, outofBoundParams *sets.String) {
+ for i, v := range volumes {
+ extractParamIndex(v.Name, arrayParams, outofBoundParams)
+ if v.VolumeSource.ConfigMap != nil {
+ extractParamIndex(v.ConfigMap.Name, arrayParams, outofBoundParams)
+ for _, item := range v.ConfigMap.Items {
+ extractParamIndex(item.Key, arrayParams, outofBoundParams)
+ extractParamIndex(item.Path, arrayParams, outofBoundParams)
+ }
+ }
+ if v.VolumeSource.Secret != nil {
+ extractParamIndex(v.Secret.SecretName, arrayParams, outofBoundParams)
+ for _, item := range v.Secret.Items {
+ extractParamIndex(item.Key, arrayParams, outofBoundParams)
+ extractParamIndex(item.Path, arrayParams, outofBoundParams)
+ }
+ }
+ if v.PersistentVolumeClaim != nil {
+ extractParamIndex(v.PersistentVolumeClaim.ClaimName, arrayParams, outofBoundParams)
+ }
+ if v.Projected != nil {
+ for _, s := range volumes[i].Projected.Sources {
+ if s.ConfigMap != nil {
+ extractParamIndex(s.ConfigMap.Name, arrayParams, outofBoundParams)
+ }
+ if s.Secret != nil {
+ extractParamIndex(s.Secret.Name, arrayParams, outofBoundParams)
+ }
+ if s.ServiceAccountToken != nil {
+ extractParamIndex(s.ServiceAccountToken.Audience, arrayParams, outofBoundParams)
+ }
+ }
+ }
+ if v.CSI != nil {
+ if v.CSI.NodePublishSecretRef != nil {
+ extractParamIndex(v.CSI.NodePublishSecretRef.Name, arrayParams, outofBoundParams)
+ }
+ if v.CSI.VolumeAttributes != nil {
+ for _, value := range v.CSI.VolumeAttributes {
+ extractParamIndex(value, arrayParams, outofBoundParams)
+ }
+ }
+ }
+ }
+}
+
+func validateContainerParamArrayIndexing(c *corev1.Container, arrayParams map[string]int, outofBoundParams *sets.String) {
+ extractParamIndex(c.Name, arrayParams, outofBoundParams)
+ extractParamIndex(c.Image, arrayParams, outofBoundParams)
+ extractParamIndex(string(c.ImagePullPolicy), arrayParams, outofBoundParams)
+
+ for _, a := range c.Args {
+ extractParamIndex(a, arrayParams, outofBoundParams)
+ }
+
+ for ie, e := range c.Env {
+ extractParamIndex(e.Value, arrayParams, outofBoundParams)
+ if c.Env[ie].ValueFrom != nil {
+ if e.ValueFrom.SecretKeyRef != nil {
+ extractParamIndex(e.ValueFrom.SecretKeyRef.LocalObjectReference.Name, arrayParams, outofBoundParams)
+ extractParamIndex(e.ValueFrom.SecretKeyRef.Key, arrayParams, outofBoundParams)
+ }
+ if e.ValueFrom.ConfigMapKeyRef != nil {
+ extractParamIndex(e.ValueFrom.ConfigMapKeyRef.LocalObjectReference.Name, arrayParams, outofBoundParams)
+ extractParamIndex(e.ValueFrom.ConfigMapKeyRef.Key, arrayParams, outofBoundParams)
+ }
+ }
+ }
+
+ for _, e := range c.EnvFrom {
+ extractParamIndex(e.Prefix, arrayParams, outofBoundParams)
+ if e.ConfigMapRef != nil {
+ extractParamIndex(e.ConfigMapRef.LocalObjectReference.Name, arrayParams, outofBoundParams)
+ }
+ if e.SecretRef != nil {
+ extractParamIndex(e.SecretRef.LocalObjectReference.Name, arrayParams, outofBoundParams)
+
+ }
+ }
+
+ extractParamIndex(c.WorkingDir, arrayParams, outofBoundParams)
+ for _, cc := range c.Command {
+ extractParamIndex(cc, arrayParams, outofBoundParams)
+ }
+
+ for _, v := range c.VolumeMounts {
+ extractParamIndex(v.Name, arrayParams, outofBoundParams)
+ extractParamIndex(v.MountPath, arrayParams, outofBoundParams)
+ extractParamIndex(v.SubPath, arrayParams, outofBoundParams)
+ }
+}
diff --git a/pkg/reconciler/taskrun/validate_resources_test.go b/pkg/reconciler/taskrun/validate_resources_test.go
index 947fbe8be4c..9e50826dfb5 100644
--- a/pkg/reconciler/taskrun/validate_resources_test.go
+++ b/pkg/reconciler/taskrun/validate_resources_test.go
@@ -18,14 +18,19 @@ package taskrun
import (
"context"
+ "fmt"
+ "strings"
"testing"
+ v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+ "github.com/google/go-cmp/cmp"
"github.com/tektoncd/pipeline/pkg/apis/config"
"github.com/tektoncd/pipeline/pkg/apis/pipeline/v1beta1"
resourcev1alpha1 "github.com/tektoncd/pipeline/pkg/apis/resource/v1alpha1"
"github.com/tektoncd/pipeline/pkg/reconciler/taskrun/resources"
+ "github.com/tektoncd/pipeline/test/diff"
)
func TestValidateResolvedTaskResources_ValidResources(t *testing.T) {
@@ -824,3 +829,167 @@ func TestValidateResult(t *testing.T) {
}
}
+
+func TestValidateParamArrayIndex(t *testing.T) {
+ stepsInvalidReferences := []string{}
+ for i := 10; i <= 26; i++ {
+ stepsInvalidReferences = append(stepsInvalidReferences, fmt.Sprintf("$(params.array-params[%d])", i))
+ }
+ volumesInvalidReferences := []string{}
+ for i := 10; i <= 22; i++ {
+ volumesInvalidReferences = append(volumesInvalidReferences, fmt.Sprintf("$(params.array-params[%d])", i))
+ }
+
+ tcs := []struct {
+ name string
+ params []v1beta1.Param
+ taskspec *v1beta1.TaskSpec
+ expectedError error
+ }{{
+ name: "steps reference invalid",
+ params: []v1beta1.Param{{
+ Name: "array-params",
+ Value: *v1beta1.NewArrayOrString("bar", "foo"),
+ }},
+ taskspec: &v1beta1.TaskSpec{
+ Steps: []v1beta1.Step{{
+ Name: "$(params.array-params[10])",
+ Image: "$(params.array-params[11])",
+ Command: []string{"$(params.array-params[12])"},
+ Args: []string{"$(params.array-params[13])"},
+ Script: "echo $(params.array-params[14])",
+ Env: []v1.EnvVar{{
+ Value: "$(params.array-params[15])",
+ ValueFrom: &v1.EnvVarSource{
+ SecretKeyRef: &v1.SecretKeySelector{
+ Key: "$(params.array-params[16])",
+ LocalObjectReference: v1.LocalObjectReference{
+ Name: "$(params.array-params[17])",
+ },
+ },
+ ConfigMapKeyRef: &v1.ConfigMapKeySelector{
+ Key: "$(params.array-params[18])",
+ LocalObjectReference: v1.LocalObjectReference{
+ Name: "$(params.array-params[19])",
+ },
+ },
+ },
+ }},
+ EnvFrom: []v1.EnvFromSource{{
+ Prefix: "$(params.array-params[20])",
+ ConfigMapRef: &v1.ConfigMapEnvSource{
+ LocalObjectReference: v1.LocalObjectReference{
+ Name: "$(params.array-params[21])",
+ },
+ },
+ SecretRef: &v1.SecretEnvSource{
+ LocalObjectReference: v1.LocalObjectReference{
+ Name: "$(params.array-params[22])",
+ },
+ },
+ }},
+ WorkingDir: "$(params.array-params[23])",
+ VolumeMounts: []v1.VolumeMount{{
+ Name: "$(params.array-params[24])",
+ MountPath: "$(params.array-params[25])",
+ SubPath: "$(params.array-params[26])",
+ }},
+ }},
+ },
+ expectedError: fmt.Errorf("non-existent param references:[%v]", strings.Join(stepsInvalidReferences, " ")),
+ }, {
+ name: "volumes reference invalid",
+ params: []v1beta1.Param{{
+ Name: "array-params",
+ Value: *v1beta1.NewArrayOrString("bar", "foo"),
+ }},
+ taskspec: &v1beta1.TaskSpec{
+ Volumes: []v1.Volume{{
+ Name: "$(params.array-params[10])",
+ VolumeSource: v1.VolumeSource{
+ ConfigMap: &v1.ConfigMapVolumeSource{
+ LocalObjectReference: v1.LocalObjectReference{
+ Name: "$(params.array-params[11])",
+ },
+ Items: []v1.KeyToPath{{
+ Key: "$(params.array-params[12])",
+ Path: "$(params.array-params[13])",
+ },
+ },
+ },
+ Secret: &v1.SecretVolumeSource{
+ SecretName: "$(params.array-params[14])",
+ Items: []v1.KeyToPath{{
+ Key: "$(params.array-params[15])",
+ Path: "$(params.array-params[16])",
+ }},
+ },
+ PersistentVolumeClaim: &v1.PersistentVolumeClaimVolumeSource{
+ ClaimName: "$(params.array-params[17])",
+ },
+ Projected: &v1.ProjectedVolumeSource{
+ Sources: []v1.VolumeProjection{{
+ ConfigMap: &v1.ConfigMapProjection{
+ LocalObjectReference: v1.LocalObjectReference{
+ Name: "$(params.array-params[18])",
+ },
+ },
+ Secret: &v1.SecretProjection{
+ LocalObjectReference: v1.LocalObjectReference{
+ Name: "$(params.array-params[19])",
+ },
+ },
+ ServiceAccountToken: &v1.ServiceAccountTokenProjection{
+ Audience: "$(params.array-params[20])",
+ },
+ }},
+ },
+ CSI: &v1.CSIVolumeSource{
+ NodePublishSecretRef: &v1.LocalObjectReference{
+ Name: "$(params.array-params[21])",
+ },
+ VolumeAttributes: map[string]string{"key": "$(params.array-params[22])"},
+ },
+ },
+ },
+ },
+ },
+ expectedError: fmt.Errorf("non-existent param references:[%v]", strings.Join(volumesInvalidReferences, " ")),
+ }, {
+ name: "workspaces reference invalid",
+ params: []v1beta1.Param{{
+ Name: "array-params",
+ Value: *v1beta1.NewArrayOrString("bar", "foo"),
+ }},
+ taskspec: &v1beta1.TaskSpec{
+ Workspaces: []v1beta1.WorkspaceDeclaration{{
+ MountPath: "$(params.array-params[3])",
+ }},
+ },
+ expectedError: fmt.Errorf("non-existent param references:[%v]", "$(params.array-params[3])"),
+ }, {
+ name: "sidecar reference invalid",
+ params: []v1beta1.Param{{
+ Name: "array-params",
+ Value: *v1beta1.NewArrayOrString("bar", "foo"),
+ }},
+ taskspec: &v1beta1.TaskSpec{
+ Sidecars: []v1beta1.Sidecar{{
+ Script: "$(params.array-params[3])",
+ },
+ },
+ },
+ expectedError: fmt.Errorf("non-existent param references:[%v]", "$(params.array-params[3])"),
+ },
+ }
+ for _, tc := range tcs {
+ t.Run(tc.name, func(t *testing.T) {
+ ctx := config.ToContext(context.Background(), &config.Config{FeatureFlags: &config.FeatureFlags{EnableAPIFields: "alpha"}})
+ err := validateParamArrayIndex(ctx, tc.params, tc.taskspec)
+ if d := cmp.Diff(tc.expectedError.Error(), err.Error()); d != "" {
+ t.Errorf("validateParamArrayIndex() errors diff %s", diff.PrintWantGot(d))
+ }
+ })
+ }
+
+}