Skip to content

Commit

Permalink
[TEP-0144] Validate PipelineRun for Param Enum
Browse files Browse the repository at this point in the history
Part of [#7270][#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

[#7270]: #7270
[tep-0144]: https://github.com/tektoncd/community/blob/main/teps/0144-param-enum.md
  • Loading branch information
QuanZhang-William committed Nov 7, 2023
1 parent 515c4a3 commit 344c756
Show file tree
Hide file tree
Showing 8 changed files with 463 additions and 1 deletion.
3 changes: 3 additions & 0 deletions docs/pipeline-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -1997,6 +1997,9 @@ associated Pipeline is an invalid graph (a.k.a wrong order, cycle, …)</p>
</tr><tr><td><p>&#34;InvalidMatrixParameterTypes&#34;</p></td>
<td><p>ReasonInvalidMatrixParameterTypes indicates a matrix contains invalid parameter types</p>
</td>
</tr><tr><td><p>&#34;InvalidParamValue&#34;</p></td>
<td><p>PipelineRunReasonInvalidParamValue indicates that the PipelineRun Param input value is not allowed.</p>
</td>
</tr><tr><td><p>&#34;InvalidTaskResultReference&#34;</p></td>
<td><p>ReasonInvalidTaskResultReference indicates a task result was declared
but was not initialized by that task</p>
Expand Down
14 changes: 14 additions & 0 deletions docs/pipelineruns.md
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,20 @@ case is when your CI system autogenerates `PipelineRuns` and it has `Parameters`
provide to all `PipelineRuns`. Because you can pass in extra `Parameters`, you don't have to
go through the complexity of checking each `Pipeline` and providing only the required params.

#### Parameter Enums

> :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.

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

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.

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`.

You can also specify `Enum` for `PipelineRun` with an embedded `Pipeline`. The same param validation will be executed in this scenario.

See more details in [Param.Enum](./pipelines.md#param-enum).

#### Propagated Parameters

When using an inlined spec, parameters from the parent `PipelineRun` will be
Expand Down
65 changes: 64 additions & 1 deletion docs/pipelines.md
Original file line number Diff line number Diff line change
Expand Up @@ -276,7 +276,70 @@ spec:

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

Parameter declarations can include `enum` which is a predefine set of valid values that can be accepted by the `Pipeline`.
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`:

``` yaml
apiVersion: tekton.dev/v1
kind: Pipeline
metadata:
name: pipeline-param-enum
spec:
params:
- name: message
enum: ["v1", "v2"]
default: "v1"
tasks:
- name: task1
params:
- name: message
value: $(params.message)
steps:
- name: build
image: bash:3.2
script: |
echo "$(params.message)"
```

If the `Param` value passed in by `PipelineRun` is **NOT** in the predefined `enum` list, the `PipelineRun` will fail with reason `InvalidParamValue`.

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.

``` yaml
apiVersion: tekton.dev/v1
kind: Task
metadata:
name: param-enum-demo
spec:
params:
- name: message
type: string
enum: ["v1", "v2"]
steps:
- name: build
image: bash:latest
script: |
echo "$(params.message)"
```

``` yaml
apiVersion: tekton.dev/v1
kind: Pipeline
metadata:
name: pipeline-param-enum
spec:
params:
- name: message
enum: ["v2", "v3"]
tasks:
- name: task1
params:
- name: message
value: $(params.message)
taskRef:
name: param-enum-demo
```

See usage in this [example](../examples/v1/pipelineruns/alpha/param-enum.yaml)

## Adding `Tasks` to the `Pipeline`

Expand Down
33 changes: 33 additions & 0 deletions examples/v1/pipelineruns/alpha/param-enum.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
apiVersion: tekton.dev/v1
kind: Pipeline
metadata:
name: pipeline-param-enum
spec:
params:
- name: message
enum: ["v1", "v2", "v3"]
default: "v1"
tasks:
- name: task1
params:
- name: message
value: $(params.message)
taskSpec:
params:
- name: message
steps:
- name: build
image: bash:3.2
script: |
echo "$(params.message)"
---
apiVersion: tekton.dev/v1
kind: PipelineRun
metadata:
name: pipelinerun-param-enum
spec:
pipelineRef:
name: pipeline-param-enum
params:
- name: message
value: "v2"
2 changes: 2 additions & 0 deletions pkg/apis/pipeline/v1/pipelinerun_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -409,6 +409,8 @@ const (
PipelineRunReasonCreateRunFailed PipelineRunReason = "CreateRunFailed"
// ReasonCELEvaluationFailed indicates the pipeline fails the CEL evaluation
PipelineRunReasonCELEvaluationFailed PipelineRunReason = "CELEvaluationFailed"
// PipelineRunReasonInvalidParamValue indicates that the PipelineRun Param input value is not allowed.
PipelineRunReasonInvalidParamValue PipelineRunReason = "InvalidParamValue"
)

func (t PipelineRunReason) String() string {
Expand Down
21 changes: 21 additions & 0 deletions pkg/reconciler/pipelinerun/pipelinerun.go
Original file line number Diff line number Diff line change
Expand Up @@ -387,6 +387,17 @@ func (c *Reconciler) resolvePipelineState(
return nil, controller.NewPermanentError(err)
}
}

if config.FromContextOrDefaults(ctx).FeatureFlags.EnableParamEnum {
if len(resolvedTask.TaskRuns) > 0 && len(resolvedTask.TaskRuns[0].Status.Conditions) > 0 {
cond := resolvedTask.TaskRuns[0].Status.Conditions[0]
if cond.Status == corev1.ConditionFalse && cond.Reason == v1.TaskRunReasonInvalidParamValue {
pr.Status.MarkFailed(v1.PipelineRunReasonInvalidParamValue.String(),
"Invalid param value in the referenced Task from PipelineTask \"%s\": %s", resolvedTask.PipelineTask.Name, cond.Message)
return nil, controller.NewPermanentError(err)
}
}
}
pst = append(pst, resolvedTask)
}
return pst, nil
Expand Down Expand Up @@ -487,6 +498,16 @@ func (c *Reconciler) reconcile(ctx context.Context, pr *v1.PipelineRun, getPipel
return controller.NewPermanentError(err)
}

if config.FromContextOrDefaults(ctx).FeatureFlags.EnableParamEnum {
if err := taskrun.ValidateEnumParam(ctx, pr.Spec.Params, pipelineSpec.Params); err != nil {
logger.Errorf("PipelineRun %q Param Enum validation failed: %v", pr.Name, err)
pr.Status.MarkFailed(v1.PipelineRunReasonInvalidParamValue.String(),
"PipelineRun %s/%s parameters have invalid value: %s",
pr.Namespace, pr.Name, err)
return controller.NewPermanentError(err)
}
}

// Ensure that the keys of an object param declared in PipelineSpec are not missed in the PipelineRunSpec
if err = resources.ValidateObjectParamRequiredKeys(pipelineSpec.Params, pr.Spec.Params); err != nil {
// This Run has failed, so we need to mark it as failed and stop reconciling it
Expand Down
182 changes: 182 additions & 0 deletions pkg/reconciler/pipelinerun/pipelinerun_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4278,6 +4278,188 @@ spec:
checkPipelineRunConditionStatusAndReason(t, pipelineRun, corev1.ConditionFalse, string(v1.PipelineRunReasonCELEvaluationFailed))
}

func TestReconcile_Pipeline_Level_Enum_Pass(t *testing.T) {
ps := []*v1.Pipeline{parse.MustParseV1Pipeline(t, `
metadata:
name: test-pipeline-level-enum
namespace: foo
spec:
params:
- name: version
type: string
enum: ["v1", "v2"]
- name: tag
type: string
tasks:
- name: a-task
params:
- name: version
value: $(params.version)
- name: tag
value: $(params.tag)
taskSpec:
name: a-task
params:
- name: version
- name: tag
steps:
- name: s1
image: alpine
script: |
echo $(params.version) + $(params.tag)
`)}
prs := []*v1.PipelineRun{parse.MustParseV1PipelineRun(t, `
metadata:
name: test-pipeline-level-enum-run
namespace: foo
spec:
params:
- name: version
value: "v1"
- name: tag
value: "t1"
pipelineRef:
name: test-pipeline-level-enum
`)}
cms := []*corev1.ConfigMap{
{
ObjectMeta: metav1.ObjectMeta{Name: config.GetFeatureFlagsConfigName(), Namespace: system.Namespace()},
Data: map[string]string{
"enable-param-enum": "true",
},
},
}
d := test.Data{
PipelineRuns: prs,
Pipelines: ps,
ConfigMaps: cms,
}
prt := newPipelineRunTest(t, d)
defer prt.Cancel()
pipelineRun, _ := prt.reconcileRun("foo", "test-pipeline-level-enum-run", []string{}, false)
// PipelineRun in running status indicates the param enum has passed validation
checkPipelineRunConditionStatusAndReason(t, pipelineRun, corev1.ConditionUnknown, v1.PipelineRunReasonRunning.String())
}

func TestReconcile_Pipeline_Level_Enum_Failed(t *testing.T) {
ps := []*v1.Pipeline{parse.MustParseV1Pipeline(t, `
metadata:
name: test-pipeline-level-enum
namespace: foo
spec:
params:
- name: version
type: string
enum: ["v1", "v2"]
tasks:
- name: a-task
taskSpec:
name: a-task
params:
- name: version
steps:
- name: s1
image: alpine
script: |
echo $(params.version)
`)}
prs := []*v1.PipelineRun{parse.MustParseV1PipelineRun(t, `
metadata:
name: test-pipeline-level-enum-run
namespace: foo
spec:
params:
- name: version
value: "v3"
pipelineRef:
name: test-pipeline-level-enum
`)}
cms := []*corev1.ConfigMap{
{
ObjectMeta: metav1.ObjectMeta{Name: config.GetFeatureFlagsConfigName(), Namespace: system.Namespace()},
Data: map[string]string{
"enable-param-enum": "true",
},
},
}
d := test.Data{
PipelineRuns: prs,
Pipelines: ps,
ConfigMaps: cms,
}
prt := newPipelineRunTest(t, d)
defer prt.Cancel()
pipelineRun, _ := prt.reconcileRun("foo", "test-pipeline-level-enum-run", []string{}, true)
checkPipelineRunConditionStatusAndReason(t, pipelineRun, corev1.ConditionFalse, string(v1.PipelineRunReasonInvalidParamValue))
}

func TestReconcile_PipelineTask_Level_Enum_Failed(t *testing.T) {
ps := []*v1.Pipeline{parse.MustParseV1Pipeline(t, `
metadata:
name: test-pipelineTask-level-enum
namespace: foo
spec:
params:
- name: version
type: string
tasks:
- name: a-task
params:
- name: version
value: $(params.version)
taskSpec:
name: a-task
params:
- name: version
enum: ["v1", "v2"]
steps:
- name: s1
image: alpine
script: |
echo $(params.version)
`)}
prs := []*v1.PipelineRun{parse.MustParseV1PipelineRun(t, `
metadata:
name: test-pipelineTask-level-enum-run
namespace: foo
spec:
params:
- name: version
value: "v3"
pipelineRef:
name: test-pipelineTask-level-enum
`)}

trs := []*v1.TaskRun{mustParseTaskRunWithObjectMeta(t,
taskRunObjectMeta("test-pipelineTask-level-enum-a-task", "foo",
"test-pipelineTask-level-enum-run", "test-pipelineTask-level-enum", "a-task", true),
`
status:
conditions:
- status: "False"
type: Succeeded
reason: "InvalidParamValue"
`)}
cms := []*corev1.ConfigMap{
{
ObjectMeta: metav1.ObjectMeta{Name: config.GetFeatureFlagsConfigName(), Namespace: system.Namespace()},
Data: map[string]string{
"enable-param-enum": "true",
},
},
}
d := test.Data{
PipelineRuns: prs,
Pipelines: ps,
ConfigMaps: cms,
TaskRuns: trs,
}
prt := newPipelineRunTest(t, d)
defer prt.Cancel()
pipelineRun, _ := prt.reconcileRun("foo", "test-pipelineTask-level-enum-run", []string{}, true)
checkPipelineRunConditionStatusAndReason(t, pipelineRun, corev1.ConditionFalse, string(v1.PipelineRunReasonInvalidParamValue))
}

// TestReconcileWithAffinityAssistantStatefulSet tests that given a pipelineRun with workspaces,
// an Affinity Assistant StatefulSet is created for each PVC workspace and
// that the Affinity Assistant names is propagated to TaskRuns.
Expand Down
Loading

0 comments on commit 344c756

Please sign in to comment.