Skip to content

Commit 344c756

Browse files
[TEP-0144] Validate PipelineRun for Param Enum
Part of [tektoncd#7270][tektoncd#7270]. In [TEP-0144][tep-0144] we proposed a new `enum` field to support built-in param input validation. This commit adds validation logic for PipelineRun against Param Enum /kind feature [tektoncd#7270]: tektoncd#7270 [tep-0144]: https://github.com/tektoncd/community/blob/main/teps/0144-param-enum.md
1 parent 515c4a3 commit 344c756

File tree

8 files changed

+463
-1
lines changed

8 files changed

+463
-1
lines changed

docs/pipeline-api.md

+3
Original file line numberDiff line numberDiff line change
@@ -1997,6 +1997,9 @@ associated Pipeline is an invalid graph (a.k.a wrong order, cycle, …)</p>
19971997
</tr><tr><td><p>&#34;InvalidMatrixParameterTypes&#34;</p></td>
19981998
<td><p>ReasonInvalidMatrixParameterTypes indicates a matrix contains invalid parameter types</p>
19991999
</td>
2000+
</tr><tr><td><p>&#34;InvalidParamValue&#34;</p></td>
2001+
<td><p>PipelineRunReasonInvalidParamValue indicates that the PipelineRun Param input value is not allowed.</p>
2002+
</td>
20002003
</tr><tr><td><p>&#34;InvalidTaskResultReference&#34;</p></td>
20012004
<td><p>ReasonInvalidTaskResultReference indicates a task result was declared
20022005
but was not initialized by that task</p>

docs/pipelineruns.md

+14
Original file line numberDiff line numberDiff line change
@@ -271,6 +271,20 @@ case is when your CI system autogenerates `PipelineRuns` and it has `Parameters`
271271
provide to all `PipelineRuns`. Because you can pass in extra `Parameters`, you don't have to
272272
go through the complexity of checking each `Pipeline` and providing only the required params.
273273

274+
#### Parameter Enums
275+
276+
> :seedling: **Specifying `enum` is an [alpha](additional-configs.md#alpha-features) feature.** The `enable-param-enum` feature flag must be set to `"true"` to enable this feature.
277+
278+
> :seedling: This feature is WIP and not yet supported/implemented. Documentation to be completed.
279+
280+
If a `Parameter` is guarded by `Enum` in the `Pipeline`, you can only provide `Parameter` values in the `PipelineRun` that are predefined in the `Param.Enum` in the `Pipeline`. The `PipelineRun` will fail with reason `InvalidParamValue` otherwise.
281+
282+
Tekton will also the validate the `param` values passed to any referenced `Tasks` (vis `taskRef`) if `Enum` is specified for the `Task`. The `PipelineRun` will fail with reason `InvalidParamValue` if `Enum` validation is failed for any of the `PipelineTask`.
283+
284+
You can also specify `Enum` for `PipelineRun` with an embedded `Pipeline`. The same param validation will be executed in this scenario.
285+
286+
See more details in [Param.Enum](./pipelines.md#param-enum).
287+
274288
#### Propagated Parameters
275289

276290
When using an inlined spec, parameters from the parent `PipelineRun` will be

docs/pipelines.md

+64-1
Original file line numberDiff line numberDiff line change
@@ -276,7 +276,70 @@ spec:
276276

277277
> :seedling: This feature is WIP and not yet supported/implemented. Documentation to be completed.
278278

279-
Parameter declarations can include `enum` which is a predefine set of valid values that can be accepted by the `Pipeline`.
279+
Parameter declarations can include `enum` which is a predefine set of valid values that can be accepted by the `Pipeline` `Param`. For example, the valid/allowed values for `Param` "message" is bounded to `v1` and `v2`:
280+
281+
``` yaml
282+
apiVersion: tekton.dev/v1
283+
kind: Pipeline
284+
metadata:
285+
name: pipeline-param-enum
286+
spec:
287+
params:
288+
- name: message
289+
enum: ["v1", "v2"]
290+
default: "v1"
291+
tasks:
292+
- name: task1
293+
params:
294+
- name: message
295+
value: $(params.message)
296+
steps:
297+
- name: build
298+
image: bash:3.2
299+
script: |
300+
echo "$(params.message)"
301+
```
302+
303+
If the `Param` value passed in by `PipelineRun` is **NOT** in the predefined `enum` list, the `PipelineRun` will fail with reason `InvalidParamValue`.
304+
305+
If a `PipelineTask` references a `Task` with `enum`, Tekton validates the **intersection** of enum specified in the referenced `Task` and the enum specified in the Pipeline `spec.params`. In the example below, the referenced `Task` accepts `v1` and `v2` as valid values, and the `Pipeline` accepts `v2` and `v3` as valid values. Only passing `v2` in the `PipelineRun` will lead to a sucessful execution.
306+
307+
``` yaml
308+
apiVersion: tekton.dev/v1
309+
kind: Task
310+
metadata:
311+
name: param-enum-demo
312+
spec:
313+
params:
314+
- name: message
315+
type: string
316+
enum: ["v1", "v2"]
317+
steps:
318+
- name: build
319+
image: bash:latest
320+
script: |
321+
echo "$(params.message)"
322+
```
323+
324+
``` yaml
325+
apiVersion: tekton.dev/v1
326+
kind: Pipeline
327+
metadata:
328+
name: pipeline-param-enum
329+
spec:
330+
params:
331+
- name: message
332+
enum: ["v2", "v3"]
333+
tasks:
334+
- name: task1
335+
params:
336+
- name: message
337+
value: $(params.message)
338+
taskRef:
339+
name: param-enum-demo
340+
```
341+
342+
See usage in this [example](../examples/v1/pipelineruns/alpha/param-enum.yaml)
280343

281344
## Adding `Tasks` to the `Pipeline`
282345

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
apiVersion: tekton.dev/v1
2+
kind: Pipeline
3+
metadata:
4+
name: pipeline-param-enum
5+
spec:
6+
params:
7+
- name: message
8+
enum: ["v1", "v2", "v3"]
9+
default: "v1"
10+
tasks:
11+
- name: task1
12+
params:
13+
- name: message
14+
value: $(params.message)
15+
taskSpec:
16+
params:
17+
- name: message
18+
steps:
19+
- name: build
20+
image: bash:3.2
21+
script: |
22+
echo "$(params.message)"
23+
---
24+
apiVersion: tekton.dev/v1
25+
kind: PipelineRun
26+
metadata:
27+
name: pipelinerun-param-enum
28+
spec:
29+
pipelineRef:
30+
name: pipeline-param-enum
31+
params:
32+
- name: message
33+
value: "v2"

pkg/apis/pipeline/v1/pipelinerun_types.go

+2
Original file line numberDiff line numberDiff line change
@@ -409,6 +409,8 @@ const (
409409
PipelineRunReasonCreateRunFailed PipelineRunReason = "CreateRunFailed"
410410
// ReasonCELEvaluationFailed indicates the pipeline fails the CEL evaluation
411411
PipelineRunReasonCELEvaluationFailed PipelineRunReason = "CELEvaluationFailed"
412+
// PipelineRunReasonInvalidParamValue indicates that the PipelineRun Param input value is not allowed.
413+
PipelineRunReasonInvalidParamValue PipelineRunReason = "InvalidParamValue"
412414
)
413415

414416
func (t PipelineRunReason) String() string {

pkg/reconciler/pipelinerun/pipelinerun.go

+21
Original file line numberDiff line numberDiff line change
@@ -387,6 +387,17 @@ func (c *Reconciler) resolvePipelineState(
387387
return nil, controller.NewPermanentError(err)
388388
}
389389
}
390+
391+
if config.FromContextOrDefaults(ctx).FeatureFlags.EnableParamEnum {
392+
if len(resolvedTask.TaskRuns) > 0 && len(resolvedTask.TaskRuns[0].Status.Conditions) > 0 {
393+
cond := resolvedTask.TaskRuns[0].Status.Conditions[0]
394+
if cond.Status == corev1.ConditionFalse && cond.Reason == v1.TaskRunReasonInvalidParamValue {
395+
pr.Status.MarkFailed(v1.PipelineRunReasonInvalidParamValue.String(),
396+
"Invalid param value in the referenced Task from PipelineTask \"%s\": %s", resolvedTask.PipelineTask.Name, cond.Message)
397+
return nil, controller.NewPermanentError(err)
398+
}
399+
}
400+
}
390401
pst = append(pst, resolvedTask)
391402
}
392403
return pst, nil
@@ -487,6 +498,16 @@ func (c *Reconciler) reconcile(ctx context.Context, pr *v1.PipelineRun, getPipel
487498
return controller.NewPermanentError(err)
488499
}
489500

501+
if config.FromContextOrDefaults(ctx).FeatureFlags.EnableParamEnum {
502+
if err := taskrun.ValidateEnumParam(ctx, pr.Spec.Params, pipelineSpec.Params); err != nil {
503+
logger.Errorf("PipelineRun %q Param Enum validation failed: %v", pr.Name, err)
504+
pr.Status.MarkFailed(v1.PipelineRunReasonInvalidParamValue.String(),
505+
"PipelineRun %s/%s parameters have invalid value: %s",
506+
pr.Namespace, pr.Name, err)
507+
return controller.NewPermanentError(err)
508+
}
509+
}
510+
490511
// Ensure that the keys of an object param declared in PipelineSpec are not missed in the PipelineRunSpec
491512
if err = resources.ValidateObjectParamRequiredKeys(pipelineSpec.Params, pr.Spec.Params); err != nil {
492513
// This Run has failed, so we need to mark it as failed and stop reconciling it

pkg/reconciler/pipelinerun/pipelinerun_test.go

+182
Original file line numberDiff line numberDiff line change
@@ -4278,6 +4278,188 @@ spec:
42784278
checkPipelineRunConditionStatusAndReason(t, pipelineRun, corev1.ConditionFalse, string(v1.PipelineRunReasonCELEvaluationFailed))
42794279
}
42804280

4281+
func TestReconcile_Pipeline_Level_Enum_Pass(t *testing.T) {
4282+
ps := []*v1.Pipeline{parse.MustParseV1Pipeline(t, `
4283+
metadata:
4284+
name: test-pipeline-level-enum
4285+
namespace: foo
4286+
spec:
4287+
params:
4288+
- name: version
4289+
type: string
4290+
enum: ["v1", "v2"]
4291+
- name: tag
4292+
type: string
4293+
tasks:
4294+
- name: a-task
4295+
params:
4296+
- name: version
4297+
value: $(params.version)
4298+
- name: tag
4299+
value: $(params.tag)
4300+
taskSpec:
4301+
name: a-task
4302+
params:
4303+
- name: version
4304+
- name: tag
4305+
steps:
4306+
- name: s1
4307+
image: alpine
4308+
script: |
4309+
echo $(params.version) + $(params.tag)
4310+
`)}
4311+
prs := []*v1.PipelineRun{parse.MustParseV1PipelineRun(t, `
4312+
metadata:
4313+
name: test-pipeline-level-enum-run
4314+
namespace: foo
4315+
spec:
4316+
params:
4317+
- name: version
4318+
value: "v1"
4319+
- name: tag
4320+
value: "t1"
4321+
pipelineRef:
4322+
name: test-pipeline-level-enum
4323+
`)}
4324+
cms := []*corev1.ConfigMap{
4325+
{
4326+
ObjectMeta: metav1.ObjectMeta{Name: config.GetFeatureFlagsConfigName(), Namespace: system.Namespace()},
4327+
Data: map[string]string{
4328+
"enable-param-enum": "true",
4329+
},
4330+
},
4331+
}
4332+
d := test.Data{
4333+
PipelineRuns: prs,
4334+
Pipelines: ps,
4335+
ConfigMaps: cms,
4336+
}
4337+
prt := newPipelineRunTest(t, d)
4338+
defer prt.Cancel()
4339+
pipelineRun, _ := prt.reconcileRun("foo", "test-pipeline-level-enum-run", []string{}, false)
4340+
// PipelineRun in running status indicates the param enum has passed validation
4341+
checkPipelineRunConditionStatusAndReason(t, pipelineRun, corev1.ConditionUnknown, v1.PipelineRunReasonRunning.String())
4342+
}
4343+
4344+
func TestReconcile_Pipeline_Level_Enum_Failed(t *testing.T) {
4345+
ps := []*v1.Pipeline{parse.MustParseV1Pipeline(t, `
4346+
metadata:
4347+
name: test-pipeline-level-enum
4348+
namespace: foo
4349+
spec:
4350+
params:
4351+
- name: version
4352+
type: string
4353+
enum: ["v1", "v2"]
4354+
tasks:
4355+
- name: a-task
4356+
taskSpec:
4357+
name: a-task
4358+
params:
4359+
- name: version
4360+
steps:
4361+
- name: s1
4362+
image: alpine
4363+
script: |
4364+
echo $(params.version)
4365+
`)}
4366+
prs := []*v1.PipelineRun{parse.MustParseV1PipelineRun(t, `
4367+
metadata:
4368+
name: test-pipeline-level-enum-run
4369+
namespace: foo
4370+
spec:
4371+
params:
4372+
- name: version
4373+
value: "v3"
4374+
pipelineRef:
4375+
name: test-pipeline-level-enum
4376+
`)}
4377+
cms := []*corev1.ConfigMap{
4378+
{
4379+
ObjectMeta: metav1.ObjectMeta{Name: config.GetFeatureFlagsConfigName(), Namespace: system.Namespace()},
4380+
Data: map[string]string{
4381+
"enable-param-enum": "true",
4382+
},
4383+
},
4384+
}
4385+
d := test.Data{
4386+
PipelineRuns: prs,
4387+
Pipelines: ps,
4388+
ConfigMaps: cms,
4389+
}
4390+
prt := newPipelineRunTest(t, d)
4391+
defer prt.Cancel()
4392+
pipelineRun, _ := prt.reconcileRun("foo", "test-pipeline-level-enum-run", []string{}, true)
4393+
checkPipelineRunConditionStatusAndReason(t, pipelineRun, corev1.ConditionFalse, string(v1.PipelineRunReasonInvalidParamValue))
4394+
}
4395+
4396+
func TestReconcile_PipelineTask_Level_Enum_Failed(t *testing.T) {
4397+
ps := []*v1.Pipeline{parse.MustParseV1Pipeline(t, `
4398+
metadata:
4399+
name: test-pipelineTask-level-enum
4400+
namespace: foo
4401+
spec:
4402+
params:
4403+
- name: version
4404+
type: string
4405+
tasks:
4406+
- name: a-task
4407+
params:
4408+
- name: version
4409+
value: $(params.version)
4410+
taskSpec:
4411+
name: a-task
4412+
params:
4413+
- name: version
4414+
enum: ["v1", "v2"]
4415+
steps:
4416+
- name: s1
4417+
image: alpine
4418+
script: |
4419+
echo $(params.version)
4420+
`)}
4421+
prs := []*v1.PipelineRun{parse.MustParseV1PipelineRun(t, `
4422+
metadata:
4423+
name: test-pipelineTask-level-enum-run
4424+
namespace: foo
4425+
spec:
4426+
params:
4427+
- name: version
4428+
value: "v3"
4429+
pipelineRef:
4430+
name: test-pipelineTask-level-enum
4431+
`)}
4432+
4433+
trs := []*v1.TaskRun{mustParseTaskRunWithObjectMeta(t,
4434+
taskRunObjectMeta("test-pipelineTask-level-enum-a-task", "foo",
4435+
"test-pipelineTask-level-enum-run", "test-pipelineTask-level-enum", "a-task", true),
4436+
`
4437+
status:
4438+
conditions:
4439+
- status: "False"
4440+
type: Succeeded
4441+
reason: "InvalidParamValue"
4442+
`)}
4443+
cms := []*corev1.ConfigMap{
4444+
{
4445+
ObjectMeta: metav1.ObjectMeta{Name: config.GetFeatureFlagsConfigName(), Namespace: system.Namespace()},
4446+
Data: map[string]string{
4447+
"enable-param-enum": "true",
4448+
},
4449+
},
4450+
}
4451+
d := test.Data{
4452+
PipelineRuns: prs,
4453+
Pipelines: ps,
4454+
ConfigMaps: cms,
4455+
TaskRuns: trs,
4456+
}
4457+
prt := newPipelineRunTest(t, d)
4458+
defer prt.Cancel()
4459+
pipelineRun, _ := prt.reconcileRun("foo", "test-pipelineTask-level-enum-run", []string{}, true)
4460+
checkPipelineRunConditionStatusAndReason(t, pipelineRun, corev1.ConditionFalse, string(v1.PipelineRunReasonInvalidParamValue))
4461+
}
4462+
42814463
// TestReconcileWithAffinityAssistantStatefulSet tests that given a pipelineRun with workspaces,
42824464
// an Affinity Assistant StatefulSet is created for each PVC workspace and
42834465
// that the Affinity Assistant names is propagated to TaskRuns.

0 commit comments

Comments
 (0)