diff --git a/config/config-feature-flags.yaml b/config/config-feature-flags.yaml index af73d4a005a..50eaa68519b 100644 --- a/config/config-feature-flags.yaml +++ b/config/config-feature-flags.yaml @@ -121,3 +121,6 @@ data: # Setting this flag to "true" will keep pod on cancellation # allowing examination of the logs on the pods from cancelled taskruns keep-pod-on-cancel: "false" + # Setting this flag to "true" will enable the CEL evaluation in WhenExpression + # This feature is in preview mode and not implemented yet. Please check #7244 for the updates. + enable-cel-in-whenexpression: "false" diff --git a/docs/pipeline-api.md b/docs/pipeline-api.md index 573745f4a61..9b73b9e7e40 100644 --- a/docs/pipeline-api.md +++ b/docs/pipeline-api.md @@ -5782,6 +5782,20 @@ k8s.io/apimachinery/pkg/selection.Operator It must be non-empty

+ + +cel
+ +string + + + +(Optional) +

CEL is a string of Common Language Expression, which can be used to conditionally execute +the task based on the result of the expression evaluation +More info about CEL syntax: https://github.com/google/cel-spec/blob/master/doc/langdef.md

+ +

WhenExpressions @@ -14549,6 +14563,20 @@ k8s.io/apimachinery/pkg/selection.Operator It must be non-empty

+ + +cel
+ +string + + + +(Optional) +

CEL is a string of Common Language Expression, which can be used to conditionally execute +the task based on the result of the expression evaluation +More info about CEL syntax: https://github.com/google/cel-spec/blob/master/doc/langdef.md

+ +

WhenExpressions diff --git a/pkg/apis/config/feature_flags.go b/pkg/apis/config/feature_flags.go index 5ad99f228ee..cb165ddddaf 100644 --- a/pkg/apis/config/feature_flags.go +++ b/pkg/apis/config/feature_flags.go @@ -92,6 +92,10 @@ const ( KeepPodOnCancel = "keep-pod-on-cancel" // DefaultEnableKeepPodOnCancel is the default value for "keep-pod-on-cancel" DefaultEnableKeepPodOnCancel = false + // EnableCELInWhenExpression is the flag to enabled CEL in WhenExpression + EnableCELInWhenExpression = "enable-cel-in-whenexpression" + // DefaultEnableCELInWhenExpression is the default value for EnableCelInWhenExpression + DefaultEnableCELInWhenExpression = false disableAffinityAssistantKey = "disable-affinity-assistant" disableCredsInitKey = "disable-creds-init" @@ -140,6 +144,7 @@ type FeatureFlags struct { MaxResultSize int SetSecurityContext bool Coschedule string + EnableCELInWhenExpression bool } // GetFeatureFlagsConfigName returns the name of the configmap containing all @@ -209,10 +214,12 @@ func NewFeatureFlagsFromMap(cfgMap map[string]string) (*FeatureFlags, error) { if err := setFeature(setSecurityContextKey, DefaultSetSecurityContext, &tc.SetSecurityContext); err != nil { return nil, err } - if err := setCoschedule(cfgMap, DefaultCoschedule, tc.DisableAffinityAssistant, &tc.Coschedule); err != nil { return nil, err } + if err := setFeature(EnableCELInWhenExpression, DefaultEnableCELInWhenExpression, &tc.EnableCELInWhenExpression); err != nil { + return nil, err + } // Given that they are alpha features, Tekton Bundles and Custom Tasks should be switched on if // enable-api-fields is "alpha". If enable-api-fields is not "alpha" then fall back to the value of // each feature's individual flag. diff --git a/pkg/apis/config/feature_flags_test.go b/pkg/apis/config/feature_flags_test.go index a6f4e60bb1a..65756040698 100644 --- a/pkg/apis/config/feature_flags_test.go +++ b/pkg/apis/config/feature_flags_test.go @@ -73,6 +73,7 @@ func TestNewFeatureFlagsFromConfigMap(t *testing.T) { MaxResultSize: 4096, SetSecurityContext: true, Coschedule: config.CoscheduleDisabled, + EnableCELInWhenExpression: true, }, fileName: "feature-flags-all-flags-set", }, @@ -269,6 +270,9 @@ func TestNewFeatureFlagsConfigMapErrors(t *testing.T) { }, { fileName: "feature-flags-invalid-disable-affinity-assistant", want: `failed parsing feature flags config "truee": strconv.ParseBool: parsing "truee": invalid syntax`, + }, { + fileName: "feature-flags-invalid-enable-cel-in-whenexpression", + want: `failed parsing feature flags config "invalid": strconv.ParseBool: parsing "invalid": invalid syntax`, }} { t.Run(tc.fileName, func(t *testing.T) { cm := test.ConfigMapFromTestFile(t, tc.fileName) diff --git a/pkg/apis/config/testdata/feature-flags-all-flags-set.yaml b/pkg/apis/config/testdata/feature-flags-all-flags-set.yaml index 9e01abd1944..63015ec07c1 100644 --- a/pkg/apis/config/testdata/feature-flags-all-flags-set.yaml +++ b/pkg/apis/config/testdata/feature-flags-all-flags-set.yaml @@ -32,3 +32,4 @@ data: enable-provenance-in-status: "false" set-security-context: "true" keep-pod-on-cancel: "true" + enable-cel-in-whenexpression: "true" diff --git a/pkg/apis/config/testdata/feature-flags-invalid-enable-cel-in-whenexpression.yaml b/pkg/apis/config/testdata/feature-flags-invalid-enable-cel-in-whenexpression.yaml new file mode 100644 index 00000000000..fca3049351f --- /dev/null +++ b/pkg/apis/config/testdata/feature-flags-invalid-enable-cel-in-whenexpression.yaml @@ -0,0 +1,21 @@ +# Copyright 2023 The Tekton Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +apiVersion: v1 +kind: ConfigMap +metadata: + name: feature-flags + namespace: tekton-pipelines +data: + enable-cel-in-whenexpression: "invalid" diff --git a/pkg/apis/pipeline/v1/openapi_generated.go b/pkg/apis/pipeline/v1/openapi_generated.go index b5a14a692f1..6425ec1de3b 100644 --- a/pkg/apis/pipeline/v1/openapi_generated.go +++ b/pkg/apis/pipeline/v1/openapi_generated.go @@ -4289,7 +4289,6 @@ func schema_pkg_apis_pipeline_v1_WhenExpression(ref common.ReferenceCallback) co "input": { SchemaProps: spec.SchemaProps{ Description: "Input is the string for guard checking which can be a static input or an output from a parent Task", - Default: "", Type: []string{"string"}, Format: "", }, @@ -4297,7 +4296,6 @@ func schema_pkg_apis_pipeline_v1_WhenExpression(ref common.ReferenceCallback) co "operator": { SchemaProps: spec.SchemaProps{ Description: "Operator that represents an Input's relationship to the values", - Default: "", Type: []string{"string"}, Format: "", }, @@ -4322,8 +4320,14 @@ func schema_pkg_apis_pipeline_v1_WhenExpression(ref common.ReferenceCallback) co }, }, }, + "cel": { + SchemaProps: spec.SchemaProps{ + Description: "CEL is a string of Common Language Expression, which can be used to conditionally execute the task based on the result of the expression evaluation More info about CEL syntax: https://github.com/google/cel-spec/blob/master/doc/langdef.md", + Type: []string{"string"}, + Format: "", + }, + }, }, - Required: []string{"input", "operator", "values"}, }, }, } diff --git a/pkg/apis/pipeline/v1/pipeline_validation.go b/pkg/apis/pipeline/v1/pipeline_validation.go index 7b2de9f77ff..f20247bc6b1 100644 --- a/pkg/apis/pipeline/v1/pipeline_validation.go +++ b/pkg/apis/pipeline/v1/pipeline_validation.go @@ -88,7 +88,7 @@ func (ps *PipelineSpec) Validate(ctx context.Context) (errs *apis.FieldError) { errs = errs.Also(validatePipelineResults(ps.Results, ps.Tasks, ps.Finally)) errs = errs.Also(validateTasksAndFinallySection(ps)) errs = errs.Also(validateFinalTasks(ps.Tasks, ps.Finally)) - errs = errs.Also(validateWhenExpressions(ps.Tasks, ps.Finally)) + errs = errs.Also(validateWhenExpressions(ctx, ps.Tasks, ps.Finally)) errs = errs.Also(validateMatrix(ctx, ps.Tasks).ViaField("tasks")) errs = errs.Also(validateMatrix(ctx, ps.Finally).ViaField("finally")) return errs @@ -745,12 +745,12 @@ func validateResultsVariablesExpressionsInFinally(expressions []string, pipeline return errs } -func validateWhenExpressions(tasks []PipelineTask, finalTasks []PipelineTask) (errs *apis.FieldError) { +func validateWhenExpressions(ctx context.Context, tasks []PipelineTask, finalTasks []PipelineTask) (errs *apis.FieldError) { for i, t := range tasks { - errs = errs.Also(t.When.validate().ViaFieldIndex("tasks", i)) + errs = errs.Also(t.When.validate(ctx).ViaFieldIndex("tasks", i)) } for i, t := range finalTasks { - errs = errs.Also(t.When.validate().ViaFieldIndex("finally", i)) + errs = errs.Also(t.When.validate(ctx).ViaFieldIndex("finally", i)) } return errs } diff --git a/pkg/apis/pipeline/v1/swagger.json b/pkg/apis/pipeline/v1/swagger.json index bb031c72730..76d325bfff0 100644 --- a/pkg/apis/pipeline/v1/swagger.json +++ b/pkg/apis/pipeline/v1/swagger.json @@ -2220,21 +2220,18 @@ "v1.WhenExpression": { "description": "WhenExpression allows a PipelineTask to declare expressions to be evaluated before the Task is run to determine whether the Task should be executed or skipped", "type": "object", - "required": [ - "input", - "operator", - "values" - ], "properties": { + "cel": { + "description": "CEL is a string of Common Language Expression, which can be used to conditionally execute the task based on the result of the expression evaluation More info about CEL syntax: https://github.com/google/cel-spec/blob/master/doc/langdef.md", + "type": "string" + }, "input": { "description": "Input is the string for guard checking which can be a static input or an output from a parent Task", - "type": "string", - "default": "" + "type": "string" }, "operator": { "description": "Operator that represents an Input's relationship to the values", - "type": "string", - "default": "" + "type": "string" }, "values": { "description": "Values is an array of strings, which is compared against the input, for guard checking It must be non-empty", diff --git a/pkg/apis/pipeline/v1/when_types.go b/pkg/apis/pipeline/v1/when_types.go index 58af7273d2b..45a8bdbd98c 100644 --- a/pkg/apis/pipeline/v1/when_types.go +++ b/pkg/apis/pipeline/v1/when_types.go @@ -27,15 +27,21 @@ import ( // to determine whether the Task should be executed or skipped type WhenExpression struct { // Input is the string for guard checking which can be a static input or an output from a parent Task - Input string `json:"input"` + Input string `json:"input,omitempty"` // Operator that represents an Input's relationship to the values - Operator selection.Operator `json:"operator"` + Operator selection.Operator `json:"operator,omitempty"` // Values is an array of strings, which is compared against the input, for guard checking // It must be non-empty // +listType=atomic - Values []string `json:"values"` + Values []string `json:"values,omitempty"` + + // CEL is a string of Common Language Expression, which can be used to conditionally execute + // the task based on the result of the expression evaluation + // More info about CEL syntax: https://github.com/google/cel-spec/blob/master/doc/langdef.md + // +optional + CEL string `json:"cel,omitempty"` } func (we *WhenExpression) isInputInValues() bool { diff --git a/pkg/apis/pipeline/v1/when_validation.go b/pkg/apis/pipeline/v1/when_validation.go index f445d13ed6a..fb8a92487d9 100644 --- a/pkg/apis/pipeline/v1/when_validation.go +++ b/pkg/apis/pipeline/v1/when_validation.go @@ -17,11 +17,13 @@ limitations under the License. package v1 import ( + "context" "fmt" "strings" // TODO(#7244): Pull the cel-go library for now, the following PR will use the library. _ "github.com/google/cel-go/cel" + "github.com/tektoncd/pipeline/pkg/apis/config" "k8s.io/apimachinery/pkg/api/equality" "k8s.io/apimachinery/pkg/selection" "k8s.io/apimachinery/pkg/util/sets" @@ -33,18 +35,28 @@ var validWhenOperators = []string{ string(selection.NotIn), } -func (wes WhenExpressions) validate() *apis.FieldError { - return wes.validateWhenExpressionsFields().ViaField("when") +func (wes WhenExpressions) validate(ctx context.Context) *apis.FieldError { + return wes.validateWhenExpressionsFields(ctx).ViaField("when") } -func (wes WhenExpressions) validateWhenExpressionsFields() (errs *apis.FieldError) { +func (wes WhenExpressions) validateWhenExpressionsFields(ctx context.Context) (errs *apis.FieldError) { for idx, we := range wes { - errs = errs.Also(we.validateWhenExpressionFields().ViaIndex(idx)) + errs = errs.Also(we.validateWhenExpressionFields(ctx).ViaIndex(idx)) } return errs } -func (we *WhenExpression) validateWhenExpressionFields() *apis.FieldError { +func (we *WhenExpression) validateWhenExpressionFields(ctx context.Context) *apis.FieldError { + if we.CEL != "" { + if !config.FromContextOrDefaults(ctx).FeatureFlags.EnableCELInWhenExpression { + return apis.ErrGeneric("feature flag %s should be set to true to use CEL: %s in WhenExpression", config.EnableCELInWhenExpression, we.CEL) + } + if we.Input != "" || we.Operator != "" || len(we.Values) != 0 { + return apis.ErrGeneric(fmt.Sprintf("cel and input+operator+values cannot be set in one WhenExpression: %v", we)) + } + return nil + } + if equality.Semantic.DeepEqual(we, &WhenExpression{}) || we == nil { return apis.ErrMissingField(apis.CurrentField) } diff --git a/pkg/apis/pipeline/v1/when_validation_test.go b/pkg/apis/pipeline/v1/when_validation_test.go index 8cb2a891356..c602e0c1c7d 100644 --- a/pkg/apis/pipeline/v1/when_validation_test.go +++ b/pkg/apis/pipeline/v1/when_validation_test.go @@ -17,8 +17,10 @@ limitations under the License. package v1 import ( + "context" "testing" + "github.com/tektoncd/pipeline/pkg/apis/config" "k8s.io/apimachinery/pkg/selection" ) @@ -54,7 +56,7 @@ func TestWhenExpressions_Valid(t *testing.T) { }} for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := tt.wes.validate(); err != nil { + if err := tt.wes.validate(context.Background()); err != nil { t.Errorf("WhenExpressions.validate() returned an error for valid when expressions: %s", tt.wes) } }) @@ -97,9 +99,68 @@ func TestWhenExpressions_Invalid(t *testing.T) { }} for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := tt.wes.validate(); err == nil { + if err := tt.wes.validate(context.Background()); err == nil { t.Errorf("WhenExpressions.validate() did not return error for invalid when expressions: %s, %s", tt.wes, err) } }) } } + +func TestCELinWhenExpressions_Valid(t *testing.T) { + ctx := config.ToContext(context.Background(), &config.Config{ + FeatureFlags: &config.FeatureFlags{ + EnableCELInWhenExpression: true, + }, + }) + tests := []struct { + name string + wes WhenExpressions + }{{ + name: "valid cel", + wes: []WhenExpression{{ + CEL: " 'foo' == 'foo' ", + }}, + }} + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := tt.wes.validate(ctx); err != nil { + t.Errorf("WhenExpressions.validate() returned an error: %s for valid when expressions: %s", err, tt.wes) + } + }) + } +} + +func TestCELWhenExpressions_Invalid(t *testing.T) { + tests := []struct { + name string + wes WhenExpressions + enableFeatureFlag bool + }{{ + name: "feature flag not set", + wes: []WhenExpression{{ + CEL: " 'foo' == 'foo' ", + }}, + enableFeatureFlag: false, + }, { + name: "CEL should not coexist with input+operator+values", + wes: []WhenExpression{{ + CEL: "'foo' != 'foo'", + Input: "foo", + Operator: selection.In, + Values: []string{"foo"}, + }}, + enableFeatureFlag: true, + }} + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctx := config.ToContext(context.Background(), &config.Config{ + FeatureFlags: &config.FeatureFlags{ + EnableCELInWhenExpression: tt.enableFeatureFlag, + }, + }) + if err := tt.wes.validate(ctx); err == nil { + t.Errorf("WhenExpressions.validate() did not return error for invalid when expressions: %s", tt.wes) + } + }) + } +} diff --git a/pkg/apis/pipeline/v1beta1/openapi_generated.go b/pkg/apis/pipeline/v1beta1/openapi_generated.go index 74a6be3b64f..eba5a872419 100644 --- a/pkg/apis/pipeline/v1beta1/openapi_generated.go +++ b/pkg/apis/pipeline/v1beta1/openapi_generated.go @@ -5693,7 +5693,6 @@ func schema_pkg_apis_pipeline_v1beta1_WhenExpression(ref common.ReferenceCallbac "input": { SchemaProps: spec.SchemaProps{ Description: "Input is the string for guard checking which can be a static input or an output from a parent Task", - Default: "", Type: []string{"string"}, Format: "", }, @@ -5701,7 +5700,6 @@ func schema_pkg_apis_pipeline_v1beta1_WhenExpression(ref common.ReferenceCallbac "operator": { SchemaProps: spec.SchemaProps{ Description: "Operator that represents an Input's relationship to the values", - Default: "", Type: []string{"string"}, Format: "", }, @@ -5726,8 +5724,14 @@ func schema_pkg_apis_pipeline_v1beta1_WhenExpression(ref common.ReferenceCallbac }, }, }, + "cel": { + SchemaProps: spec.SchemaProps{ + Description: "CEL is a string of Common Language Expression, which can be used to conditionally execute the task based on the result of the expression evaluation More info about CEL syntax: https://github.com/google/cel-spec/blob/master/doc/langdef.md", + Type: []string{"string"}, + Format: "", + }, + }, }, - Required: []string{"input", "operator", "values"}, }, }, } diff --git a/pkg/apis/pipeline/v1beta1/pipeline_conversion.go b/pkg/apis/pipeline/v1beta1/pipeline_conversion.go index 9f5c75a839f..b80b2ad74b7 100644 --- a/pkg/apis/pipeline/v1beta1/pipeline_conversion.go +++ b/pkg/apis/pipeline/v1beta1/pipeline_conversion.go @@ -262,12 +262,14 @@ func (we WhenExpression) convertTo(ctx context.Context, sink *v1.WhenExpression) sink.Input = we.Input sink.Operator = we.Operator sink.Values = we.Values + sink.CEL = we.CEL } func (we *WhenExpression) convertFrom(ctx context.Context, source v1.WhenExpression) { we.Input = source.Input we.Operator = source.Operator we.Values = source.Values + we.CEL = source.CEL } func (m *Matrix) convertTo(ctx context.Context, sink *v1.Matrix) { diff --git a/pkg/apis/pipeline/v1beta1/pipeline_conversion_test.go b/pkg/apis/pipeline/v1beta1/pipeline_conversion_test.go index e9ba9cc9032..3b5939ccce3 100644 --- a/pkg/apis/pipeline/v1beta1/pipeline_conversion_test.go +++ b/pkg/apis/pipeline/v1beta1/pipeline_conversion_test.go @@ -62,6 +62,9 @@ func TestPipelineConversion(t *testing.T) { Name: "foo", OnError: v1beta1.PipelineTaskContinue, TaskRef: &v1beta1.TaskRef{Name: "example.com/my-foo-task"}, + WhenExpressions: v1beta1.WhenExpressions{{ + CEL: "'$(params.param-1)'=='foo'", + }}, }}, Params: []v1beta1.ParamSpec{{ Name: "param-1", diff --git a/pkg/apis/pipeline/v1beta1/pipeline_validation.go b/pkg/apis/pipeline/v1beta1/pipeline_validation.go index 2a0cbd6d2b6..118fff45a15 100644 --- a/pkg/apis/pipeline/v1beta1/pipeline_validation.go +++ b/pkg/apis/pipeline/v1beta1/pipeline_validation.go @@ -86,7 +86,7 @@ func (ps *PipelineSpec) Validate(ctx context.Context) (errs *apis.FieldError) { errs = errs.Also(validatePipelineResults(ps.Results, ps.Tasks, ps.Finally)) errs = errs.Also(validateTasksAndFinallySection(ps)) errs = errs.Also(validateFinalTasks(ps.Tasks, ps.Finally)) - errs = errs.Also(validateWhenExpressions(ps.Tasks, ps.Finally)) + errs = errs.Also(validateWhenExpressions(ctx, ps.Tasks, ps.Finally)) errs = errs.Also(validateMatrix(ctx, ps.Tasks).ViaField("tasks")) errs = errs.Also(validateMatrix(ctx, ps.Finally).ViaField("finally")) return errs @@ -707,12 +707,12 @@ func validateResultsVariablesExpressionsInFinally(expressions []string, pipeline return errs } -func validateWhenExpressions(tasks []PipelineTask, finalTasks []PipelineTask) (errs *apis.FieldError) { +func validateWhenExpressions(ctx context.Context, tasks []PipelineTask, finalTasks []PipelineTask) (errs *apis.FieldError) { for i, t := range tasks { - errs = errs.Also(t.WhenExpressions.validate().ViaFieldIndex("tasks", i)) + errs = errs.Also(t.WhenExpressions.validate(ctx).ViaFieldIndex("tasks", i)) } for i, t := range finalTasks { - errs = errs.Also(t.WhenExpressions.validate().ViaFieldIndex("finally", i)) + errs = errs.Also(t.WhenExpressions.validate(ctx).ViaFieldIndex("finally", i)) } return errs } diff --git a/pkg/apis/pipeline/v1beta1/swagger.json b/pkg/apis/pipeline/v1beta1/swagger.json index 97f789dbcbf..4ff7d3d43ab 100644 --- a/pkg/apis/pipeline/v1beta1/swagger.json +++ b/pkg/apis/pipeline/v1beta1/swagger.json @@ -3111,21 +3111,18 @@ "v1beta1.WhenExpression": { "description": "WhenExpression allows a PipelineTask to declare expressions to be evaluated before the Task is run to determine whether the Task should be executed or skipped", "type": "object", - "required": [ - "input", - "operator", - "values" - ], "properties": { + "cel": { + "description": "CEL is a string of Common Language Expression, which can be used to conditionally execute the task based on the result of the expression evaluation More info about CEL syntax: https://github.com/google/cel-spec/blob/master/doc/langdef.md", + "type": "string" + }, "input": { "description": "Input is the string for guard checking which can be a static input or an output from a parent Task", - "type": "string", - "default": "" + "type": "string" }, "operator": { "description": "Operator that represents an Input's relationship to the values", - "type": "string", - "default": "" + "type": "string" }, "values": { "description": "Values is an array of strings, which is compared against the input, for guard checking It must be non-empty", diff --git a/pkg/apis/pipeline/v1beta1/when_types.go b/pkg/apis/pipeline/v1beta1/when_types.go index e98eff147c7..76a78ea0ea1 100644 --- a/pkg/apis/pipeline/v1beta1/when_types.go +++ b/pkg/apis/pipeline/v1beta1/when_types.go @@ -27,15 +27,21 @@ import ( // to determine whether the Task should be executed or skipped type WhenExpression struct { // Input is the string for guard checking which can be a static input or an output from a parent Task - Input string `json:"input"` + Input string `json:"input,omitempty"` // Operator that represents an Input's relationship to the values - Operator selection.Operator `json:"operator"` + Operator selection.Operator `json:"operator,omitempty"` // Values is an array of strings, which is compared against the input, for guard checking // It must be non-empty // +listType=atomic - Values []string `json:"values"` + Values []string `json:"values,omitempty"` + + // CEL is a string of Common Language Expression, which can be used to conditionally execute + // the task based on the result of the expression evaluation + // More info about CEL syntax: https://github.com/google/cel-spec/blob/master/doc/langdef.md + // +optional + CEL string `json:"cel,omitempty"` } func (we *WhenExpression) isInputInValues() bool { diff --git a/pkg/apis/pipeline/v1beta1/when_validation.go b/pkg/apis/pipeline/v1beta1/when_validation.go index 17bb55c56cf..8279950afab 100644 --- a/pkg/apis/pipeline/v1beta1/when_validation.go +++ b/pkg/apis/pipeline/v1beta1/when_validation.go @@ -17,9 +17,11 @@ limitations under the License. package v1beta1 import ( + "context" "fmt" "strings" + "github.com/tektoncd/pipeline/pkg/apis/config" "k8s.io/apimachinery/pkg/api/equality" "k8s.io/apimachinery/pkg/selection" "k8s.io/apimachinery/pkg/util/sets" @@ -31,18 +33,28 @@ var validWhenOperators = []string{ string(selection.NotIn), } -func (wes WhenExpressions) validate() *apis.FieldError { - return wes.validateWhenExpressionsFields().ViaField("when") +func (wes WhenExpressions) validate(ctx context.Context) *apis.FieldError { + return wes.validateWhenExpressionsFields(ctx).ViaField("when") } -func (wes WhenExpressions) validateWhenExpressionsFields() (errs *apis.FieldError) { +func (wes WhenExpressions) validateWhenExpressionsFields(ctx context.Context) (errs *apis.FieldError) { for idx, we := range wes { - errs = errs.Also(we.validateWhenExpressionFields().ViaIndex(idx)) + errs = errs.Also(we.validateWhenExpressionFields(ctx).ViaIndex(idx)) } return errs } -func (we *WhenExpression) validateWhenExpressionFields() *apis.FieldError { +func (we *WhenExpression) validateWhenExpressionFields(ctx context.Context) *apis.FieldError { + if we.CEL != "" { + if !config.FromContextOrDefaults(ctx).FeatureFlags.EnableCELInWhenExpression { + return apis.ErrGeneric("feature flag %s should be set to true to use CEL: %s in WhenExpression", config.EnableCELInWhenExpression, we.CEL) + } + if we.Input != "" || we.Operator != "" || len(we.Values) != 0 { + return apis.ErrGeneric(fmt.Sprintf("cel and input+operator+values cannot be set in one WhenExpression: %v", we)) + } + return nil + } + if equality.Semantic.DeepEqual(we, &WhenExpression{}) || we == nil { return apis.ErrMissingField(apis.CurrentField) } diff --git a/pkg/apis/pipeline/v1beta1/when_validation_test.go b/pkg/apis/pipeline/v1beta1/when_validation_test.go index ac23b41d402..e02f99a9b7a 100644 --- a/pkg/apis/pipeline/v1beta1/when_validation_test.go +++ b/pkg/apis/pipeline/v1beta1/when_validation_test.go @@ -17,8 +17,10 @@ limitations under the License. package v1beta1 import ( + "context" "testing" + "github.com/tektoncd/pipeline/pkg/apis/config" "k8s.io/apimachinery/pkg/selection" ) @@ -54,7 +56,7 @@ func TestWhenExpressions_Valid(t *testing.T) { }} for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := tt.wes.validate(); err != nil { + if err := tt.wes.validate(context.Background()); err != nil { t.Errorf("WhenExpressions.validate() returned an error for valid when expressions: %s", tt.wes) } }) @@ -97,9 +99,68 @@ func TestWhenExpressions_Invalid(t *testing.T) { }} for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := tt.wes.validate(); err == nil { + if err := tt.wes.validate(context.Background()); err == nil { t.Errorf("WhenExpressions.validate() did not return error for invalid when expressions: %s, %s", tt.wes, err) } }) } } + +func TestCELinWhenExpressions_Valid(t *testing.T) { + ctx := config.ToContext(context.Background(), &config.Config{ + FeatureFlags: &config.FeatureFlags{ + EnableCELInWhenExpression: true, + }, + }) + tests := []struct { + name string + wes WhenExpressions + }{{ + name: "valid cel", + wes: []WhenExpression{{ + CEL: " 'foo' == 'foo' ", + }}, + }} + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := tt.wes.validate(ctx); err != nil { + t.Errorf("WhenExpressions.validate() returned an error: %s for valid when expressions: %s", err, tt.wes) + } + }) + } +} + +func TestCELWhenExpressions_Invalid(t *testing.T) { + tests := []struct { + name string + wes WhenExpressions + enableFeatureFlag bool + }{{ + name: "feature flag not set", + wes: []WhenExpression{{ + CEL: " 'foo' == 'foo' ", + }}, + enableFeatureFlag: false, + }, { + name: "CEL should not coexist with input+operator+values", + wes: []WhenExpression{{ + CEL: "'foo' != 'foo'", + Input: "foo", + Operator: selection.In, + Values: []string{"foo"}, + }}, + enableFeatureFlag: true, + }} + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctx := config.ToContext(context.Background(), &config.Config{ + FeatureFlags: &config.FeatureFlags{ + EnableCELInWhenExpression: tt.enableFeatureFlag, + }, + }) + if err := tt.wes.validate(ctx); err == nil { + t.Errorf("WhenExpressions.validate() did not return error for invalid when expressions: %s", tt.wes) + } + }) + } +}