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 [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

add pr validation
  • Loading branch information
QuanZhang-William committed Nov 6, 2023
1 parent 515c4a3 commit bf4a5eb
Show file tree
Hide file tree
Showing 8 changed files with 377 additions and 1 deletion.
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 `v3` 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
30 changes: 30 additions & 0 deletions examples/v1/pipelineruns/alpha/param-enum.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
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)
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
22 changes: 22 additions & 0 deletions pkg/reconciler/pipelinerun/pipelinerun.go
Original file line number Diff line number Diff line change
Expand Up @@ -387,6 +387,18 @@ func (c *Reconciler) resolvePipelineState(
return nil, controller.NewPermanentError(err)
}
}

cfg := config.FromContextOrDefaults(ctx)
if cfg.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 +499,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
119 changes: 119 additions & 0 deletions pkg/reconciler/pipelinerun/pipelinerun_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4278,6 +4278,125 @@ spec:
checkPipelineRunConditionStatusAndReason(t, pipelineRun, corev1.ConditionFalse, string(v1.PipelineRunReasonCELEvaluationFailed))
}

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
5 changes: 5 additions & 0 deletions pkg/reconciler/taskrun/taskrun.go
Original file line number Diff line number Diff line change
Expand Up @@ -417,7 +417,12 @@ func (c *Reconciler) prepare(ctx context.Context, tr *v1.TaskRun) (*v1.TaskSpec,
return nil, nil, controller.NewPermanentError(err)
}

<<<<<<< HEAD
if config.FromContextOrDefaults(ctx).FeatureFlags.EnableParamEnum {
=======
cfg := config.FromContextOrDefaults(ctx)
if cfg.FeatureFlags.EnableParamEnum {
>>>>>>> 8a5996bc5 (add pr validation)
if err := ValidateEnumParam(ctx, tr.Spec.Params, rtr.TaskSpec.Params); err != nil {
logger.Errorf("TaskRun %q Param Enum validation failed: %v", tr.Name, err)
tr.Status.MarkResourceFailed(v1.TaskRunReasonInvalidParamValue, err)
Expand Down
Loading

0 comments on commit bf4a5eb

Please sign in to comment.