Skip to content

Commit 6833e8d

Browse files
[TEP-0144] Validate TaskRun 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 TaskRun 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 8fd372f commit 6833e8d

15 files changed

+382
-1
lines changed

docs/taskruns.md

+10
Original file line numberDiff line numberDiff line change
@@ -402,6 +402,16 @@ case is when your CI system autogenerates `TaskRuns` and it has `Parameters` it
402402
provide to all `TaskRuns`. Because you can pass in extra `Parameters`, you don't have to
403403
go through the complexity of checking each `Task` and providing only the required params.
404404

405+
#### Parameter Enums
406+
407+
> :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.
408+
409+
> :seedling: This feature is WIP and not yet supported/implemented. Documentation to be completed.
410+
411+
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.
412+
413+
See more details in [Param.Enum](./tasks.md#param-enum).
414+
405415
### Specifying `Resource` limits
406416

407417
Each Step in a Task can specify its resource requirements. See

docs/tasks.md

+22-1
Original file line numberDiff line numberDiff line change
@@ -717,7 +717,28 @@ spec:
717717

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

720-
Parameter declarations can include `enum` which is a predefine set of valid values that can be accepted by the `Task`.
720+
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`:
721+
722+
``` yaml
723+
apiVersion: tekton.dev/v1
724+
kind: Task
725+
metadata:
726+
name: param-enum-demo
727+
spec:
728+
params:
729+
- name: message
730+
type: string
731+
enum: ["v1", "v2", "v3"]
732+
steps:
733+
- name: build
734+
image: bash:latest
735+
script: |
736+
echo "$(params.message)"
737+
```
738+
739+
If the `Param` value passed in by `TaskRuns` is **NOT** in the predefined `enum` list, the `TaskRuns` will fail with reason `InvalidParamValue`.
740+
741+
See usage in this [example](../examples/v1/taskruns/alpha/param-enum.yaml)
721742

722743
### Specifying `Workspaces`
723744

+25
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
apiVersion: tekton.dev/v1
2+
kind: Task
3+
metadata:
4+
name: task-param-enum
5+
spec:
6+
params:
7+
- name: message
8+
enum: ["v1", "v2", "v3"]
9+
default: "v1"
10+
steps:
11+
- name: build
12+
image: bash:3.2
13+
script: |
14+
echo "$(params.message)"
15+
---
16+
apiVersion: tekton.dev/v1
17+
kind: TaskRun
18+
metadata:
19+
name: taskrun-param-enum
20+
spec:
21+
taskRef:
22+
name: task-param-enum
23+
params:
24+
- name: message
25+
value: "v1"

pkg/apis/pipeline/v1/param_types.go

+6
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import (
2626
"github.com/tektoncd/pipeline/pkg/substitution"
2727
corev1 "k8s.io/api/core/v1"
2828
"k8s.io/apimachinery/pkg/util/sets"
29+
"k8s.io/utils/strings/slices"
2930
"knative.dev/pkg/apis"
3031
)
3132

@@ -161,6 +162,11 @@ func (ps ParamSpecs) validateParamEnums(ctx context.Context) *apis.FieldError {
161162
for dup := range findDups(p.Enum) {
162163
errs = errs.Also(apis.ErrGeneric(fmt.Sprintf("parameter enum value %v appears more than once", dup), "").ViaKey(p.Name))
163164
}
165+
if p.Default != nil && p.Default.StringVal != "" {
166+
if !slices.Contains(p.Enum, p.Default.StringVal) {
167+
errs = errs.Also(apis.ErrGeneric(fmt.Sprintf("param default value %v not in the enum list", p.Default.StringVal), "").ViaKey(p.Name))
168+
}
169+
}
164170
}
165171
return errs
166172
}

pkg/apis/pipeline/v1/task_validation_test.go

+15
Original file line numberDiff line numberDiff line change
@@ -2300,6 +2300,21 @@ func TestParamEnum_Failure(t *testing.T) {
23002300
configMap map[string]string
23012301
expectedErr error
23022302
}{{
2303+
name: "param default val not in enum list - failure",
2304+
params: []v1.ParamSpec{{
2305+
Name: "param1",
2306+
Type: v1.ParamTypeString,
2307+
Default: &v1.ParamValue{
2308+
Type: v1.ParamTypeString,
2309+
StringVal: "v4",
2310+
},
2311+
Enum: []string{"v1", "v2"},
2312+
}},
2313+
configMap: map[string]string{
2314+
"enable-param-enum": "true",
2315+
},
2316+
expectedErr: fmt.Errorf("param default value v4 not in the enum list: params[param1]"),
2317+
}, {
23032318
name: "param enum with array type - failure",
23042319
params: []v1.ParamSpec{{
23052320
Name: "param1",

pkg/apis/pipeline/v1/taskrun_types.go

+2
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,8 @@ const (
185185
TaskRunReasonResultLargerThanAllowedLimit TaskRunReason = "TaskRunResultLargerThanAllowedLimit"
186186
// TaskRunReasonStopSidecarFailed indicates that the sidecar is not properly stopped.
187187
TaskRunReasonStopSidecarFailed = "TaskRunStopSidecarFailed"
188+
// TaskRunReasonInvalidParamValue indicates that the TaskRun Param input value is not allowed.
189+
TaskRunReasonInvalidParamValue = "InvalidParamValue"
188190
)
189191

190192
func (t TaskRunReason) String() string {

pkg/apis/pipeline/v1beta1/param_types.go

+6
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import (
2626
"github.com/tektoncd/pipeline/pkg/substitution"
2727
corev1 "k8s.io/api/core/v1"
2828
"k8s.io/apimachinery/pkg/util/sets"
29+
"k8s.io/utils/strings/slices"
2930
"knative.dev/pkg/apis"
3031
)
3132

@@ -152,6 +153,11 @@ func (ps ParamSpecs) validateParamEnums(ctx context.Context) *apis.FieldError {
152153
for dup := range findDups(p.Enum) {
153154
errs = errs.Also(apis.ErrGeneric(fmt.Sprintf("parameter enum value %v appears more than once", dup), "").ViaKey(p.Name))
154155
}
156+
if p.Default != nil && p.Default.StringVal != "" {
157+
if !slices.Contains(p.Enum, p.Default.StringVal) {
158+
errs = errs.Also(apis.ErrGeneric(fmt.Sprintf("param default value %v not in the enum list", p.Default.StringVal), "").ViaKey(p.Name))
159+
}
160+
}
155161
}
156162
return errs
157163
}

pkg/apis/pipeline/v1beta1/task_validation_test.go

+15
Original file line numberDiff line numberDiff line change
@@ -2137,6 +2137,21 @@ func TestParamEnum_Failure(t *testing.T) {
21372137
configMap map[string]string
21382138
expectedErr error
21392139
}{{
2140+
name: "param default val not in enum list - failure",
2141+
params: []v1beta1.ParamSpec{{
2142+
Name: "param1",
2143+
Type: v1beta1.ParamTypeString,
2144+
Default: &v1beta1.ParamValue{
2145+
Type: v1beta1.ParamTypeString,
2146+
StringVal: "v4",
2147+
},
2148+
Enum: []string{"v1", "v2"},
2149+
}},
2150+
configMap: map[string]string{
2151+
"enable-param-enum": "true",
2152+
},
2153+
expectedErr: fmt.Errorf("param default value v4 not in the enum list: params[param1]"),
2154+
}, {
21402155
name: "param enum with array type - failure",
21412156
params: []v1beta1.ParamSpec{{
21422157
Name: "param1",

pkg/reconciler/taskrun/taskrun.go

+6
Original file line numberDiff line numberDiff line change
@@ -404,6 +404,12 @@ func (c *Reconciler) prepare(ctx context.Context, tr *v1.TaskRun) (*v1.TaskSpec,
404404
return nil, nil, controller.NewPermanentError(err)
405405
}
406406

407+
if err := ValidateEnumParam(ctx, tr.Spec.Params, rtr.TaskSpec.Params); err != nil {
408+
logger.Errorf("TaskRun %q Param Enum validation failed: %v", tr.Name, err)
409+
tr.Status.MarkResourceFailed(v1.TaskRunReasonInvalidParamValue, err)
410+
return nil, nil, controller.NewPermanentError(err)
411+
}
412+
407413
if err := resources.ValidateParamArrayIndex(rtr.TaskSpec, tr.Spec.Params); err != nil {
408414
logger.Errorf("TaskRun %q Param references are invalid: %v", tr.Name, err)
409415
tr.Status.MarkResourceFailed(podconvert.ReasonFailedValidation, err)

pkg/reconciler/taskrun/taskrun_test.go

+108
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,24 @@ var (
145145
Steps: []v1.Step{simpleStep},
146146
},
147147
}
148+
simpleTaskWithParamEnum = &v1.Task{
149+
ObjectMeta: objectMeta("test-task-param-enum", "foo"),
150+
TypeMeta: metav1.TypeMeta{
151+
APIVersion: "tekton.dev/v1",
152+
Kind: "Task",
153+
},
154+
Spec: v1.TaskSpec{
155+
Params: []v1.ParamSpec{{
156+
Name: "param1",
157+
Enum: []string{"v1", "v2"},
158+
}, {
159+
Name: "param2",
160+
Enum: []string{"v1", "v2"},
161+
Default: &v1.ParamValue{Type: v1.ParamTypeString, StringVal: "v1"},
162+
}},
163+
Steps: []v1.Step{simpleStep},
164+
},
165+
}
148166
resultsTask = &v1.Task{
149167
ObjectMeta: objectMeta("test-results-task", "foo"),
150168
Spec: v1.TaskSpec{
@@ -4999,6 +5017,96 @@ status:
49995017
}
50005018
}
50015019

5020+
func TestReconcile_TaskRunWithParam_Enum_valid(t *testing.T) {
5021+
taskRunWithParamValid := parse.MustParseV1TaskRun(t, `
5022+
metadata:
5023+
name: test-taskrun-with-param-enum-valid
5024+
namespace: foo
5025+
spec:
5026+
params:
5027+
- name: param1
5028+
value: v1
5029+
taskRef:
5030+
name: test-task-param-enum
5031+
`)
5032+
5033+
d := test.Data{
5034+
TaskRuns: []*v1.TaskRun{taskRunWithParamValid},
5035+
Tasks: []*v1.Task{simpleTaskWithParamEnum},
5036+
ConfigMaps: []*corev1.ConfigMap{{
5037+
ObjectMeta: metav1.ObjectMeta{Namespace: system.Namespace(), Name: config.GetFeatureFlagsConfigName()},
5038+
Data: map[string]string{
5039+
"enable-param-enum": "true",
5040+
},
5041+
}},
5042+
}
5043+
5044+
testAssets, cancel := getTaskRunController(t, d)
5045+
defer cancel()
5046+
createServiceAccount(t, testAssets, taskRunWithParamValid.Spec.ServiceAccountName, taskRunWithParamValid.Namespace)
5047+
5048+
// Reconcile the TaskRun
5049+
if err := testAssets.Controller.Reconciler.Reconcile(testAssets.Ctx, getRunName(taskRunWithParamValid)); err == nil {
5050+
t.Error("wanted a wrapped requeue error, but got nil.")
5051+
} else if ok, _ := controller.IsRequeueKey(err); !ok {
5052+
t.Errorf("expected no error. Got error %v", err)
5053+
}
5054+
5055+
tr, err := testAssets.Clients.Pipeline.TektonV1().TaskRuns(taskRunWithParamValid.Namespace).Get(testAssets.Ctx, taskRunWithParamValid.Name, metav1.GetOptions{})
5056+
if err != nil {
5057+
t.Fatalf("getting updated taskrun: %v", err)
5058+
}
5059+
condition := tr.Status.GetCondition(apis.ConditionSucceeded)
5060+
if condition.Type != apis.ConditionSucceeded || condition.Reason == string(corev1.ConditionFalse) {
5061+
t.Errorf("Expected TaskRun to succeed but it did not. Final conditions were:\n%#v", tr.Status.Conditions)
5062+
}
5063+
}
5064+
5065+
func TestReconcile_TaskRunWithParam_Enum_invalid(t *testing.T) {
5066+
taskRunWithParamInvalid := parse.MustParseV1TaskRun(t, `
5067+
metadata:
5068+
name: test-taskrun-with-param-enum-invalid
5069+
namespace: foo
5070+
spec:
5071+
params:
5072+
- name: param1
5073+
value: invalid
5074+
taskRef:
5075+
name: test-task-param-enum
5076+
`)
5077+
5078+
d := test.Data{
5079+
TaskRuns: []*v1.TaskRun{taskRunWithParamInvalid},
5080+
Tasks: []*v1.Task{simpleTaskWithParamEnum},
5081+
ConfigMaps: []*corev1.ConfigMap{{
5082+
ObjectMeta: metav1.ObjectMeta{Namespace: system.Namespace(), Name: config.GetFeatureFlagsConfigName()},
5083+
Data: map[string]string{
5084+
"enable-param-enum": "true",
5085+
},
5086+
}},
5087+
}
5088+
5089+
expectedErr := fmt.Errorf("1 error occurred:\n\t* param `param1` value: invalid is not in the enum list")
5090+
expectedFailureReason := "InvalidParamValue"
5091+
testAssets, cancel := getTaskRunController(t, d)
5092+
defer cancel()
5093+
createServiceAccount(t, testAssets, taskRunWithParamInvalid.Spec.ServiceAccountName, taskRunWithParamInvalid.Namespace)
5094+
5095+
// Reconcile the TaskRun
5096+
err := testAssets.Controller.Reconciler.Reconcile(testAssets.Ctx, getRunName(taskRunWithParamInvalid))
5097+
if d := cmp.Diff(expectedErr.Error(), strings.TrimSuffix(err.Error(), "\n\n")); d != "" {
5098+
t.Errorf("Expected: %v, but Got: %v", expectedErr, err)
5099+
}
5100+
tr, err := testAssets.Clients.Pipeline.TektonV1().TaskRuns(taskRunWithParamInvalid.Namespace).Get(testAssets.Ctx, taskRunWithParamInvalid.Name, metav1.GetOptions{})
5101+
if err != nil {
5102+
t.Fatalf("Error getting updated taskrun: %v", err)
5103+
}
5104+
condition := tr.Status.GetCondition(apis.ConditionSucceeded)
5105+
if condition.Type != apis.ConditionSucceeded || condition.Status != corev1.ConditionFalse || condition.Reason != expectedFailureReason {
5106+
t.Errorf("Expected TaskRun to fail with reason \"%s\" but it did not. Final conditions were:\n%#v", expectedFailureReason, tr.Status.Conditions)
5107+
}
5108+
}
5109+
50025110
func TestReconcile_validateTaskRunResults_valid(t *testing.T) {
50035111
taskRunResultsTypeMatched := parse.MustParseV1TaskRun(t, `
50045112
metadata:

pkg/reconciler/taskrun/validate_taskrun.go

+25
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import (
2828
"github.com/tektoncd/pipeline/pkg/reconciler/taskrun/resources"
2929

3030
"k8s.io/apimachinery/pkg/util/sets"
31+
"k8s.io/utils/strings/slices"
3132
)
3233

3334
// validateParams validates that all Pipeline Task, Matrix.Params and Matrix.Include parameters all have values, match the specified
@@ -174,6 +175,30 @@ func ValidateResolvedTask(ctx context.Context, params []v1.Param, matrix *v1.Mat
174175
return nil
175176
}
176177

178+
// ValidateEnumParam validates the param values are in the defined enum list in the corresponding paramSpecs if provided.
179+
// An validation error is returned otherwise.
180+
func ValidateEnumParam(ctx context.Context, params []v1.Param, paramSpecs v1.ParamSpecs) error {
181+
paramSpecNameToEnum := map[string][]string{}
182+
for _, ps := range paramSpecs {
183+
if len(ps.Enum) == 0 {
184+
continue
185+
}
186+
paramSpecNameToEnum[ps.Name] = ps.Enum
187+
}
188+
189+
for _, p := range params {
190+
// skip validation for and non-string typed and optional params (using default value)
191+
// the default value of param is validated at validation webhook dryrun
192+
if p.Value.Type != v1.ParamTypeString || p.Value.StringVal == "" {
193+
continue
194+
}
195+
if !slices.Contains(paramSpecNameToEnum[p.Name], p.Value.StringVal) {
196+
return fmt.Errorf("param `%s` value: %s is not in the enum list", p.Name, p.Value.StringVal)
197+
}
198+
}
199+
return nil
200+
}
201+
177202
func validateTaskSpecRequestResources(taskSpec *v1.TaskSpec) error {
178203
if taskSpec != nil {
179204
for _, step := range taskSpec.Steps {

0 commit comments

Comments
 (0)