diff --git a/config/config-feature-flags.yaml b/config/config-feature-flags.yaml index 9911dd3fdb3..83c8ee98e01 100644 --- a/config/config-feature-flags.yaml +++ b/config/config-feature-flags.yaml @@ -127,5 +127,4 @@ data: # This feature is in preview mode and not implemented yet. Please check #7259 for updates. enable-step-actions: "false" # Setting this flag to "true" will enable the built-in param input validation via param enum. - # NOTE (#7270): this feature is still under development and not yet functional. enable-param-enum: "false" diff --git a/docs/additional-configs.md b/docs/additional-configs.md index 11b95952590..16a48b36950 100644 --- a/docs/additional-configs.md +++ b/docs/additional-configs.md @@ -317,6 +317,7 @@ Features currently in "alpha" are: | [Coschedule](./affinityassistants.md) | [TEP-0135](https://github.com/tektoncd/community/blob/main/teps/0135-coscheduling-pipelinerun-pods.md) | N/A |`coschedule` | | [keep pod on cancel](./taskruns.md#cancelling-a-taskrun) | N/A | v0.52 | keep-pod-on-cancel | | [CEL in WhenExpression](./taskruns.md#cancelling-a-taskrun) | [TEP-0145](https://github.com/tektoncd/community/blob/main/teps/0145-cel-in-whenexpression.md) | N/A | enable-cel-in-whenexpression | +| [Param Enum](./taskruns.md#parameter-enums) | [TEP-0144](https://github.com/tektoncd/community/blob/main/teps/0144-param-enum.md) | N/A | `enable-param-enum` | ### Beta Features diff --git a/docs/pipeline-api.md b/docs/pipeline-api.md index d2f6a4d93b7..7496f17ad45 100644 --- a/docs/pipeline-api.md +++ b/docs/pipeline-api.md @@ -1997,6 +1997,9 @@ associated Pipeline is an invalid graph (a.k.a wrong order, cycle, …)

"InvalidMatrixParameterTypes"

ReasonInvalidMatrixParameterTypes indicates a matrix contains invalid parameter types

+

"InvalidParamValue"

+

PipelineRunReasonInvalidParamValue indicates that the PipelineRun Param input value is not allowed.

+

"InvalidTaskResultReference"

ReasonInvalidTaskResultReference indicates a task result was declared but was not initialized by that task

diff --git a/docs/pipelineruns.md b/docs/pipelineruns.md index a8ab9cf2187..43ef4d7675a 100644 --- a/docs/pipelineruns.md +++ b/docs/pipelineruns.md @@ -271,6 +271,18 @@ 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. + +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 diff --git a/docs/pipelines.md b/docs/pipelines.md index e734ed1de73..6f2dd5c0e7f 100644 --- a/docs/pipelines.md +++ b/docs/pipelines.md @@ -274,9 +274,70 @@ spec: #### Param enum > :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. +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`: -Parameter declarations can include `enum` which is a predefine set of valid values that can be accepted by the `Pipeline`. +``` 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` diff --git a/docs/taskruns.md b/docs/taskruns.md index ba56b68ed1f..942d507c653 100644 --- a/docs/taskruns.md +++ b/docs/taskruns.md @@ -406,8 +406,6 @@ go through the complexity of checking each `Task` and providing only the require > :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 `Task`, you can only provide `Parameter` values in the `TaskRun` that are predefined in the `Param.Enum` in the `Task`. The `TaskRun` will fail with reason `InvalidParamValue` otherwise. You can also specify `Enum` for [`TaskRun` with an embedded `Task`](#example-taskrun-with-an-embedded-task). The same param validation will be executed in this scenario. diff --git a/docs/tasks.md b/docs/tasks.md index d7a79f3feda..72e3f81f759 100644 --- a/docs/tasks.md +++ b/docs/tasks.md @@ -715,8 +715,6 @@ spec: #### Param enum > :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. - Parameter declarations can include `enum` which is a predefine set of valid values that can be accepted by the `Param`. For example, the valid/allowed values for `Param` "message" is bounded to `v1`, `v2` and `v3`: ``` yaml diff --git a/examples/v1/pipelineruns/alpha/param-enum.yaml b/examples/v1/pipelineruns/alpha/param-enum.yaml new file mode 100644 index 00000000000..58d5fda4281 --- /dev/null +++ b/examples/v1/pipelineruns/alpha/param-enum.yaml @@ -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" \ No newline at end of file diff --git a/pkg/apis/pipeline/v1/pipelinerun_types.go b/pkg/apis/pipeline/v1/pipelinerun_types.go index 88aad636b89..724b47130a7 100644 --- a/pkg/apis/pipeline/v1/pipelinerun_types.go +++ b/pkg/apis/pipeline/v1/pipelinerun_types.go @@ -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 { diff --git a/pkg/reconciler/pipelinerun/pipelinerun.go b/pkg/reconciler/pipelinerun/pipelinerun.go index aac9fba25ba..c0d6b3d4fb4 100644 --- a/pkg/reconciler/pipelinerun/pipelinerun.go +++ b/pkg/reconciler/pipelinerun/pipelinerun.go @@ -387,6 +387,19 @@ func (c *Reconciler) resolvePipelineState( return nil, controller.NewPermanentError(err) } } + + if config.FromContextOrDefaults(ctx).FeatureFlags.EnableParamEnum { + for _, tr := range resolvedTask.TaskRuns { + if len(tr.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 @@ -487,6 +500,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 diff --git a/pkg/reconciler/pipelinerun/pipelinerun_test.go b/pkg/reconciler/pipelinerun/pipelinerun_test.go index e9e50b529a4..9a86f1b2f52 100644 --- a/pkg/reconciler/pipelinerun/pipelinerun_test.go +++ b/pkg/reconciler/pipelinerun/pipelinerun_test.go @@ -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.