diff --git a/pkg/apis/admissionregistration/validation/validation.go b/pkg/apis/admissionregistration/validation/validation.go index 753ba80c775..e29926b447e 100644 --- a/pkg/apis/admissionregistration/validation/validation.go +++ b/pkg/apis/admissionregistration/validation/validation.go @@ -21,8 +21,6 @@ import ( "regexp" "strings" - "k8s.io/apiserver/pkg/admission/plugin/validatingadmissionpolicy" - genericvalidation "k8s.io/apimachinery/pkg/api/validation" "k8s.io/apimachinery/pkg/api/validation/path" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -31,6 +29,8 @@ import ( utilvalidation "k8s.io/apimachinery/pkg/util/validation" "k8s.io/apimachinery/pkg/util/validation/field" plugincel "k8s.io/apiserver/pkg/admission/plugin/cel" + "k8s.io/apiserver/pkg/admission/plugin/validatingadmissionpolicy" + celconfig "k8s.io/apiserver/pkg/apis/cel" "k8s.io/apiserver/pkg/cel" "k8s.io/apiserver/pkg/util/webhook" @@ -740,7 +740,7 @@ func validateValidation(v *admissionregistration.Validation, paramKind *admissio Expression: trimmedExpression, Message: v.Message, Reason: v.Reason, - }, plugincel.OptionalVariableDeclarations{HasParams: paramKind != nil, HasAuthorizer: true}) + }, plugincel.OptionalVariableDeclarations{HasParams: paramKind != nil, HasAuthorizer: true}, celconfig.PerCallLimit) if result.Error != nil { switch result.Error.Type { case cel.ErrorTypeRequired: diff --git a/staging/src/k8s.io/apiserver/pkg/admission/plugin/cel/compile.go b/staging/src/k8s.io/apiserver/pkg/admission/plugin/cel/compile.go index ff1eddf70a7..b91907d37dd 100644 --- a/staging/src/k8s.io/apiserver/pkg/admission/plugin/cel/compile.go +++ b/staging/src/k8s.io/apiserver/pkg/admission/plugin/cel/compile.go @@ -18,6 +18,7 @@ package cel import ( "fmt" + celconfig "k8s.io/apiserver/pkg/apis/cel" "sync" "github.com/google/cel-go/cel" @@ -33,8 +34,6 @@ const ( RequestVarName = "request" AuthorizerVarName = "authorizer" RequestResourceAuthorizerVarName = "authorizer.requestResource" - - checkFrequency = 100 ) var ( @@ -190,7 +189,8 @@ type CompilationResult struct { } // CompileCELExpression returns a compiled CEL expression. -func CompileCELExpression(expressionAccessor ExpressionAccessor, optionalVars OptionalVariableDeclarations) CompilationResult { +// perCallLimit was added for testing purpose only. Callers should always use const PerCallLimit from k8s.io/apiserver/pkg/apis/cel/config.go as input. +func CompileCELExpression(expressionAccessor ExpressionAccessor, optionalVars OptionalVariableDeclarations, perCallLimit uint64) CompilationResult { var env *cel.Env envs, err := getEnvs() if err != nil { @@ -245,9 +245,10 @@ func CompileCELExpression(expressionAccessor ExpressionAccessor, optionalVars Op } } prog, err := env.Program(ast, - cel.EvalOptions(cel.OptOptimize), + cel.EvalOptions(cel.OptOptimize, cel.OptTrackCost), cel.OptimizeRegex(library.ExtensionLibRegexOptimizations...), - cel.InterruptCheckFrequency(checkFrequency), + cel.InterruptCheckFrequency(celconfig.CheckFrequency), + cel.CostLimit(perCallLimit), ) if err != nil { return CompilationResult{ diff --git a/staging/src/k8s.io/apiserver/pkg/admission/plugin/cel/compile_test.go b/staging/src/k8s.io/apiserver/pkg/admission/plugin/cel/compile_test.go index 8d58a378ae0..216c067e789 100644 --- a/staging/src/k8s.io/apiserver/pkg/admission/plugin/cel/compile_test.go +++ b/staging/src/k8s.io/apiserver/pkg/admission/plugin/cel/compile_test.go @@ -19,6 +19,8 @@ package cel import ( "strings" "testing" + + celconfig "k8s.io/apiserver/pkg/apis/cel" ) func TestCompileValidatingPolicyExpression(t *testing.T) { @@ -120,7 +122,7 @@ func TestCompileValidatingPolicyExpression(t *testing.T) { for _, expr := range tc.expressions { result := CompileCELExpression(&fakeExpressionAccessor{ expr, - }, OptionalVariableDeclarations{HasParams: tc.hasParams, HasAuthorizer: true}) + }, OptionalVariableDeclarations{HasParams: tc.hasParams, HasAuthorizer: true}, celconfig.PerCallLimit) if result.Error != nil { t.Errorf("Unexpected error: %v", result.Error) } @@ -128,7 +130,7 @@ func TestCompileValidatingPolicyExpression(t *testing.T) { for expr, expectErr := range tc.errorExpressions { result := CompileCELExpression(&fakeExpressionAccessor{ expr, - }, OptionalVariableDeclarations{HasParams: tc.hasParams, HasAuthorizer: tc.hasAuthorizer}) + }, OptionalVariableDeclarations{HasParams: tc.hasParams, HasAuthorizer: tc.hasAuthorizer}, celconfig.PerCallLimit) if result.Error == nil { t.Errorf("Expected expression '%s' to contain '%v' but got no error", expr, expectErr) continue diff --git a/staging/src/k8s.io/apiserver/pkg/admission/plugin/cel/filter.go b/staging/src/k8s.io/apiserver/pkg/admission/plugin/cel/filter.go index fde22295958..b9d898c2bdc 100644 --- a/staging/src/k8s.io/apiserver/pkg/admission/plugin/cel/filter.go +++ b/staging/src/k8s.io/apiserver/pkg/admission/plugin/cel/filter.go @@ -19,6 +19,7 @@ package cel import ( "errors" "fmt" + "math" "reflect" "time" @@ -74,13 +75,14 @@ func (a *evaluationActivation) Parent() interpreter.Activation { } // Compile compiles the cel expressions defined in the ExpressionAccessors into a Filter -func (c *filterCompiler) Compile(expressionAccessors []ExpressionAccessor, options OptionalVariableDeclarations) Filter { +// perCallLimit was added for testing purpose only. Callers should always use const PerCallLimit from k8s.io/apiserver/pkg/apis/cel/config.go as input. +func (c *filterCompiler) Compile(expressionAccessors []ExpressionAccessor, options OptionalVariableDeclarations, perCallLimit uint64) Filter { if len(expressionAccessors) == 0 { return nil } compilationResults := make([]CompilationResult, len(expressionAccessors)) for i, expressionAccessor := range expressionAccessors { - compilationResults[i] = CompileCELExpression(expressionAccessor, options) + compilationResults[i] = CompileCELExpression(expressionAccessor, options, perCallLimit) } return NewFilter(compilationResults) } @@ -120,7 +122,8 @@ func objectToResolveVal(r runtime.Object) (interface{}, error) { // ForInput evaluates the compiled CEL expressions converting them into CELEvaluations // errors per evaluation are returned on the Evaluation object -func (f *filter) ForInput(versionedAttr *generic.VersionedAttributes, request *admissionv1.AdmissionRequest, inputs OptionalVariableBindings) ([]EvaluationResult, error) { +// runtimeCELCostBudget was added for testing purpose only. Callers should always use const RuntimeCELCostBudget from k8s.io/apiserver/pkg/apis/cel/config.go as input. +func (f *filter) ForInput(versionedAttr *generic.VersionedAttributes, request *admissionv1.AdmissionRequest, inputs OptionalVariableBindings, runtimeCELCostBudget int64) ([]EvaluationResult, error) { // TODO: replace unstructured with ref.Val for CEL variables when native type support is available evaluations := make([]EvaluationResult, len(f.compilationResults)) var err error @@ -159,6 +162,7 @@ func (f *filter) ForInput(versionedAttr *generic.VersionedAttributes, request *a requestResourceAuthorizer: requestResourceAuthorizerVal, } + remainingBudget := runtimeCELCostBudget for i, compilationResult := range f.compilationResults { var evaluation = &evaluations[i] evaluation.ExpressionAccessor = compilationResult.ExpressionAccessor @@ -171,9 +175,22 @@ func (f *filter) ForInput(versionedAttr *generic.VersionedAttributes, request *a continue } t1 := time.Now() - evalResult, _, err := compilationResult.Program.Eval(va) + evalResult, evalDetails, err := compilationResult.Program.Eval(va) elapsed := time.Since(t1) evaluation.Elapsed = elapsed + if evalDetails == nil { + return nil, errors.New(fmt.Sprintf("runtime cost could not be calculated for expression: %v, no further expression will be run", compilationResult.ExpressionAccessor.GetExpression())) + } else { + rtCost := evalDetails.ActualCost() + if rtCost == nil { + return nil, errors.New(fmt.Sprintf("runtime cost could not be calculated for expression: %v, no further expression will be run", compilationResult.ExpressionAccessor.GetExpression())) + } else { + if *rtCost > math.MaxInt64 || int64(*rtCost) > remainingBudget { + return nil, errors.New(fmt.Sprintf("validation failed due to running out of cost budget, no further validation rules will be run")) + } + remainingBudget -= int64(*rtCost) + } + } if err != nil { evaluation.Error = errors.New(fmt.Sprintf("expression '%v' resulted in error: %v", compilationResult.ExpressionAccessor.GetExpression(), err)) } else { diff --git a/staging/src/k8s.io/apiserver/pkg/admission/plugin/cel/filter_test.go b/staging/src/k8s.io/apiserver/pkg/admission/plugin/cel/filter_test.go index 5fe00ffa7d7..bc9b63590bd 100644 --- a/staging/src/k8s.io/apiserver/pkg/admission/plugin/cel/filter_test.go +++ b/staging/src/k8s.io/apiserver/pkg/admission/plugin/cel/filter_test.go @@ -27,6 +27,7 @@ import ( celtypes "github.com/google/cel-go/common/types" "github.com/stretchr/testify/require" + celconfig "k8s.io/apiserver/pkg/apis/cel" "k8s.io/apiserver/pkg/authentication/user" "k8s.io/apiserver/pkg/authorization/authorizer" apiservercel "k8s.io/apiserver/pkg/cel" @@ -87,7 +88,7 @@ func TestCompile(t *testing.T) { for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { var c filterCompiler - e := c.Compile(tc.validation, OptionalVariableDeclarations{HasParams: false, HasAuthorizer: false}) + e := c.Compile(tc.validation, OptionalVariableDeclarations{HasParams: false, HasAuthorizer: false}, celconfig.PerCallLimit) if e == nil { t.Fatalf("unexpected nil validator") } @@ -144,13 +145,14 @@ func TestFilter(t *testing.T) { var nilUnstructured *unstructured.Unstructured cases := []struct { - name string - attributes admission.Attributes - params runtime.Object - validations []ExpressionAccessor - results []EvaluationResult - hasParamKind bool - authorizer authorizer.Authorizer + name string + attributes admission.Attributes + params runtime.Object + validations []ExpressionAccessor + results []EvaluationResult + hasParamKind bool + authorizer authorizer.Authorizer + testPerCallLimit uint64 }{ { name: "valid syntax for object", @@ -616,12 +618,32 @@ func TestFilter(t *testing.T) { APIVersion: "*", }), }, + { + name: "test perCallLimit exceed", + validations: []ExpressionAccessor{ + &condition{ + Expression: "object.subsets.size() < params.spec.testSize", + }, + }, + attributes: newValidAttribute(nil, false), + results: []EvaluationResult{ + { + Error: errors.New(fmt.Sprintf("operation cancelled: actual cost limit exceeded")), + }, + }, + hasParamKind: true, + params: crdParams, + testPerCallLimit: 1, + }, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { c := filterCompiler{} - f := c.Compile(tc.validations, OptionalVariableDeclarations{HasParams: tc.hasParamKind, HasAuthorizer: tc.authorizer != nil}) + if tc.testPerCallLimit == 0 { + tc.testPerCallLimit = celconfig.PerCallLimit + } + f := c.Compile(tc.validations, OptionalVariableDeclarations{HasParams: tc.hasParamKind, HasAuthorizer: tc.authorizer != nil}, tc.testPerCallLimit) if f == nil { t.Fatalf("unexpected nil validator") } @@ -635,7 +657,7 @@ func TestFilter(t *testing.T) { } optionalVars := OptionalVariableBindings{VersionedParams: tc.params, Authorizer: tc.authorizer} - evalResults, err := f.ForInput(versionedAttr, CreateAdmissionRequest(versionedAttr.Attributes), optionalVars) + evalResults, err := f.ForInput(versionedAttr, CreateAdmissionRequest(versionedAttr.Attributes), optionalVars, celconfig.RuntimeCELCostBudget) if err != nil { t.Fatalf("unexpected error: %v", err) } @@ -652,6 +674,111 @@ func TestFilter(t *testing.T) { } } +func TestRuntimeCELCostBudget(t *testing.T) { + configMapParams := &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: "foo", + }, + Data: map[string]string{ + "fakeString": "fake", + }, + } + + cases := []struct { + name string + attributes admission.Attributes + params runtime.Object + validations []ExpressionAccessor + hasParamKind bool + authorizer authorizer.Authorizer + testRuntimeCELCostBudget int64 + exceedBudget bool + }{ + { + name: "expression exceed RuntimeCELCostBudget at fist expression", + validations: []ExpressionAccessor{ + &condition{ + Expression: "has(object.subsets) && object.subsets.size() < 2", + }, + &condition{ + Expression: "has(object.subsets)", + }, + }, + attributes: newValidAttribute(nil, false), + hasParamKind: false, + testRuntimeCELCostBudget: 1, + exceedBudget: true, + }, + { + name: "expression exceed RuntimeCELCostBudget at last expression", + validations: []ExpressionAccessor{ + &condition{ + Expression: "has(object.subsets) && object.subsets.size() < 2", + }, + &condition{ + Expression: "object.subsets.size() > 2", + }, + }, + attributes: newValidAttribute(nil, false), + hasParamKind: true, + params: configMapParams, + testRuntimeCELCostBudget: 5, + exceedBudget: true, + }, + { + name: "test RuntimeCELCostBudge is not exceed", + validations: []ExpressionAccessor{ + &condition{ + Expression: "oldObject != null", + }, + &condition{ + Expression: "object.subsets.size() > 2", + }, + }, + attributes: newValidAttribute(nil, false), + hasParamKind: true, + params: configMapParams, + exceedBudget: false, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + c := filterCompiler{} + f := c.Compile(tc.validations, OptionalVariableDeclarations{HasParams: tc.hasParamKind, HasAuthorizer: false}, celconfig.PerCallLimit) + if f == nil { + t.Fatalf("unexpected nil validator") + } + validations := tc.validations + CompilationResults := f.(*filter).compilationResults + require.Equal(t, len(validations), len(CompilationResults)) + + versionedAttr, err := generic.NewVersionedAttributes(tc.attributes, tc.attributes.GetKind(), newObjectInterfacesForTest()) + if err != nil { + t.Fatalf("unexpected error on conversion: %v", err) + } + + if tc.testRuntimeCELCostBudget == 0 { + tc.testRuntimeCELCostBudget = celconfig.RuntimeCELCostBudget + } + optionalVars := OptionalVariableBindings{VersionedParams: tc.params, Authorizer: tc.authorizer} + evalResults, err := f.ForInput(versionedAttr, CreateAdmissionRequest(versionedAttr.Attributes), optionalVars, tc.testRuntimeCELCostBudget) + if tc.exceedBudget && err == nil { + t.Errorf("Expected RuntimeCELCostBudge to be exceeded but got nil") + } + if tc.exceedBudget && !strings.Contains(err.Error(), "validation failed due to running out of cost budget, no further validation rules will be run") { + t.Errorf("Expected RuntimeCELCostBudge exceeded error but got: %v", err) + } + if err != nil && !tc.exceedBudget { + t.Fatalf("unexpected error: %v", err) + } + if tc.exceedBudget && len(evalResults) != 0 { + t.Fatalf("unexpected result returned: %v", evalResults) + } + }) + } +} + // newObjectInterfacesForTest returns an ObjectInterfaces appropriate for test cases in this file. func newObjectInterfacesForTest() admission.ObjectInterfaces { scheme := runtime.NewScheme() diff --git a/staging/src/k8s.io/apiserver/pkg/admission/plugin/cel/interface.go b/staging/src/k8s.io/apiserver/pkg/admission/plugin/cel/interface.go index 65f20141672..6bd6645ca40 100644 --- a/staging/src/k8s.io/apiserver/pkg/admission/plugin/cel/interface.go +++ b/staging/src/k8s.io/apiserver/pkg/admission/plugin/cel/interface.go @@ -65,7 +65,8 @@ type OptionalVariableDeclarations struct { // FilterCompiler contains a function to assist with converting types and values to/from CEL-typed values. type FilterCompiler interface { // Compile is used for the cel expression compilation - Compile(expressions []ExpressionAccessor, optionalDecls OptionalVariableDeclarations) Filter + // perCallLimit was added for testing purpose only. Callers should always use const PerCallLimit from k8s.io/apiserver/pkg/apis/cel/config.go as input. + Compile(expressions []ExpressionAccessor, optionalDecls OptionalVariableDeclarations, perCallLimit uint64) Filter } // OptionalVariableBindings provides expression bindings for optional CEL variables. @@ -84,7 +85,8 @@ type OptionalVariableBindings struct { // by the underlying CEL code (which is indicated by the match criteria of a policy definition). type Filter interface { // ForInput converts compiled CEL-typed values into evaluated CEL-typed values - ForInput(versionedAttr *generic.VersionedAttributes, request *v1.AdmissionRequest, optionalVars OptionalVariableBindings) ([]EvaluationResult, error) + // runtimeCELCostBudget was added for testing purpose only. Callers should always use const RuntimeCELCostBudget from k8s.io/apiserver/pkg/apis/cel/config.go as input. + ForInput(versionedAttr *generic.VersionedAttributes, request *v1.AdmissionRequest, optionalVars OptionalVariableBindings, runtimeCELCostBudget int64) ([]EvaluationResult, error) // CompilationErrors returns a list of errors from the compilation of the evaluator CompilationErrors() []error diff --git a/staging/src/k8s.io/apiserver/pkg/admission/plugin/validatingadmissionpolicy/admission_test.go b/staging/src/k8s.io/apiserver/pkg/admission/plugin/validatingadmissionpolicy/admission_test.go index 637fcc6930e..6a3c2395e3c 100644 --- a/staging/src/k8s.io/apiserver/pkg/admission/plugin/validatingadmissionpolicy/admission_test.go +++ b/staging/src/k8s.io/apiserver/pkg/admission/plugin/validatingadmissionpolicy/admission_test.go @@ -172,6 +172,7 @@ func (f *fakeCompiler) HasSynced() bool { func (f *fakeCompiler) Compile( expressions []cel.ExpressionAccessor, options cel.OptionalVariableDeclarations, + perCallLimit uint64, ) cel.Filter { key := expressions[0].GetExpression() if fun, ok := f.CompileFuncs[key]; ok { @@ -208,7 +209,7 @@ type fakeFilter struct { keyId string } -func (f *fakeFilter) ForInput(versionedAttr *whgeneric.VersionedAttributes, request *admissionv1.AdmissionRequest, inputs cel.OptionalVariableBindings) ([]cel.EvaluationResult, error) { +func (f *fakeFilter) ForInput(versionedAttr *whgeneric.VersionedAttributes, request *admissionv1.AdmissionRequest, inputs cel.OptionalVariableBindings, runtimeCELCostBudget int64) ([]cel.EvaluationResult, error) { return []cel.EvaluationResult{}, nil } @@ -220,10 +221,10 @@ var _ Validator = &fakeValidator{} type fakeValidator struct { *fakeFilter - ValidateFunc func(versionedAttr *whgeneric.VersionedAttributes, versionedParams runtime.Object) []PolicyDecision + ValidateFunc func(versionedAttr *whgeneric.VersionedAttributes, versionedParams runtime.Object, runtimeCELCostBudget int64) []PolicyDecision } -func (f *fakeValidator) RegisterDefinition(definition *v1alpha1.ValidatingAdmissionPolicy, validateFunc func(versionedAttr *whgeneric.VersionedAttributes, versionedParams runtime.Object) []PolicyDecision) { +func (f *fakeValidator) RegisterDefinition(definition *v1alpha1.ValidatingAdmissionPolicy, validateFunc func(versionedAttr *whgeneric.VersionedAttributes, versionedParams runtime.Object, runtimeCELCostBudget int64) []PolicyDecision) { //Key must be something that we can decipher from the inputs to Validate so using message which will be on the validationCondition object of evalResult validateKey := definition.Spec.Validations[0].Expression if validatorMap == nil { @@ -234,8 +235,8 @@ func (f *fakeValidator) RegisterDefinition(definition *v1alpha1.ValidatingAdmiss validatorMap[validateKey] = f } -func (f *fakeValidator) Validate(versionedAttr *whgeneric.VersionedAttributes, versionedParams runtime.Object) []PolicyDecision { - return f.ValidateFunc(versionedAttr, versionedParams) +func (f *fakeValidator) Validate(versionedAttr *whgeneric.VersionedAttributes, versionedParams runtime.Object, runtimeCELCostBudget int64) []PolicyDecision { + return f.ValidateFunc(versionedAttr, versionedParams, runtimeCELCostBudget) } var _ Matcher = &fakeMatcher{} @@ -715,7 +716,7 @@ func TestBasicPolicyDefinitionFailure(t *testing.T) { } }) - validator.RegisterDefinition(denyPolicy, func(versionedAttr *whgeneric.VersionedAttributes, versionedParams runtime.Object) []PolicyDecision { + validator.RegisterDefinition(denyPolicy, func(versionedAttr *whgeneric.VersionedAttributes, versionedParams runtime.Object, runtimeCELCostBudget int64) []PolicyDecision { return []PolicyDecision{ { Action: ActionDeny, @@ -775,7 +776,7 @@ func TestDefinitionDoesntMatch(t *testing.T) { } }) - validator.RegisterDefinition(denyPolicy, func(versionedAttr *whgeneric.VersionedAttributes, versionedParams runtime.Object) []PolicyDecision { + validator.RegisterDefinition(denyPolicy, func(versionedAttr *whgeneric.VersionedAttributes, versionedParams runtime.Object, runtimeCELCostBudget int64) []PolicyDecision { return []PolicyDecision{ { Action: ActionDeny, @@ -886,7 +887,7 @@ func TestReconfigureBinding(t *testing.T) { } }) - validator.RegisterDefinition(denyPolicy, func(versionedAttr *whgeneric.VersionedAttributes, versionedParams runtime.Object) []PolicyDecision { + validator.RegisterDefinition(denyPolicy, func(versionedAttr *whgeneric.VersionedAttributes, versionedParams runtime.Object, runtimeCELCostBudget int64) []PolicyDecision { return []PolicyDecision{ { Action: ActionDeny, @@ -993,7 +994,7 @@ func TestRemoveDefinition(t *testing.T) { } }) - validator.RegisterDefinition(denyPolicy, func(versionedAttr *whgeneric.VersionedAttributes, versionedParams runtime.Object) []PolicyDecision { + validator.RegisterDefinition(denyPolicy, func(versionedAttr *whgeneric.VersionedAttributes, versionedParams runtime.Object, runtimeCELCostBudget int64) []PolicyDecision { return []PolicyDecision{ { Action: ActionDeny, @@ -1060,7 +1061,7 @@ func TestRemoveBinding(t *testing.T) { } }) - validator.RegisterDefinition(denyPolicy, func(versionedAttr *whgeneric.VersionedAttributes, versionedParams runtime.Object) []PolicyDecision { + validator.RegisterDefinition(denyPolicy, func(versionedAttr *whgeneric.VersionedAttributes, versionedParams runtime.Object, runtimeCELCostBudget int64) []PolicyDecision { return []PolicyDecision{ { Action: ActionDeny, @@ -1168,7 +1169,7 @@ func TestInvalidParamSourceInstanceName(t *testing.T) { } }) - validator.RegisterDefinition(denyPolicy, func(versionedAttr *whgeneric.VersionedAttributes, versionedParams runtime.Object) []PolicyDecision { + validator.RegisterDefinition(denyPolicy, func(versionedAttr *whgeneric.VersionedAttributes, versionedParams runtime.Object, runtimeCELCostBudget int64) []PolicyDecision { return []PolicyDecision{ { Action: ActionDeny, @@ -1234,7 +1235,7 @@ func TestEmptyParamSource(t *testing.T) { } }) - validator.RegisterDefinition(denyPolicy, func(versionedAttr *whgeneric.VersionedAttributes, versionedParams runtime.Object) []PolicyDecision { + validator.RegisterDefinition(denyPolicy, func(versionedAttr *whgeneric.VersionedAttributes, versionedParams runtime.Object, runtimeCELCostBudget int64) []PolicyDecision { return []PolicyDecision{ { Action: ActionDeny, @@ -1334,7 +1335,7 @@ func TestMultiplePoliciesSharedParamType(t *testing.T) { } }) - validator1.RegisterDefinition(&policy1, func(versionedAttr *whgeneric.VersionedAttributes, versionedParams runtime.Object) []PolicyDecision { + validator1.RegisterDefinition(&policy1, func(versionedAttr *whgeneric.VersionedAttributes, versionedParams runtime.Object, runtimeCELCostBudget int64) []PolicyDecision { evaluations1.Add(1) return []PolicyDecision{ { @@ -1351,7 +1352,7 @@ func TestMultiplePoliciesSharedParamType(t *testing.T) { } }) - validator2.RegisterDefinition(&policy2, func(versionedAttr *whgeneric.VersionedAttributes, versionedParams runtime.Object) []PolicyDecision { + validator2.RegisterDefinition(&policy2, func(versionedAttr *whgeneric.VersionedAttributes, versionedParams runtime.Object, runtimeCELCostBudget int64) []PolicyDecision { evaluations2.Add(1) return []PolicyDecision{ { @@ -1459,7 +1460,7 @@ func TestNativeTypeParam(t *testing.T) { } }) - validator.RegisterDefinition(&nativeTypeParamPolicy, func(versionedAttr *whgeneric.VersionedAttributes, versionedParams runtime.Object) []PolicyDecision { + validator.RegisterDefinition(&nativeTypeParamPolicy, func(versionedAttr *whgeneric.VersionedAttributes, versionedParams runtime.Object, runtimeCELCostBudget int64) []PolicyDecision { evaluations.Add(1) if _, ok := versionedParams.(*v1.ConfigMap); ok { return []PolicyDecision{ diff --git a/staging/src/k8s.io/apiserver/pkg/admission/plugin/validatingadmissionpolicy/controller.go b/staging/src/k8s.io/apiserver/pkg/admission/plugin/validatingadmissionpolicy/controller.go index 800e823ab2d..52ef2fb0085 100644 --- a/staging/src/k8s.io/apiserver/pkg/admission/plugin/validatingadmissionpolicy/controller.go +++ b/staging/src/k8s.io/apiserver/pkg/admission/plugin/validatingadmissionpolicy/controller.go @@ -20,6 +20,7 @@ import ( "context" "errors" "fmt" + celconfig "k8s.io/apiserver/pkg/apis/cel" "sync" "sync/atomic" "time" @@ -320,7 +321,7 @@ func (c *celAdmissionController) Validate( versionedAttr = va } - decisions := bindingInfo.validator.Validate(versionedAttr, param) + decisions := bindingInfo.validator.Validate(versionedAttr, param, celconfig.RuntimeCELCostBudget) for _, decision := range decisions { switch decision.Action { diff --git a/staging/src/k8s.io/apiserver/pkg/admission/plugin/validatingadmissionpolicy/controller_reconcile.go b/staging/src/k8s.io/apiserver/pkg/admission/plugin/validatingadmissionpolicy/controller_reconcile.go index ecf2a715fc1..fbb76eda44e 100644 --- a/staging/src/k8s.io/apiserver/pkg/admission/plugin/validatingadmissionpolicy/controller_reconcile.go +++ b/staging/src/k8s.io/apiserver/pkg/admission/plugin/validatingadmissionpolicy/controller_reconcile.go @@ -32,6 +32,7 @@ import ( celmetrics "k8s.io/apiserver/pkg/admission/cel" "k8s.io/apiserver/pkg/admission/plugin/cel" "k8s.io/apiserver/pkg/admission/plugin/validatingadmissionpolicy/internal/generic" + celconfig "k8s.io/apiserver/pkg/apis/cel" "k8s.io/apiserver/pkg/authorization/authorizer" "k8s.io/client-go/dynamic" "k8s.io/client-go/dynamic/dynamicinformer" @@ -446,7 +447,7 @@ func (c *policyController) latestPolicyData() []policyData { } optionalVars := cel.OptionalVariableDeclarations{HasParams: hasParam, HasAuthorizer: true} bindingInfo.validator = c.newValidator( - c.filterCompiler.Compile(convertv1alpha1Validations(definitionInfo.lastReconciledValue.Spec.Validations), optionalVars), + c.filterCompiler.Compile(convertv1alpha1Validations(definitionInfo.lastReconciledValue.Spec.Validations), optionalVars, celconfig.PerCallLimit), convertv1alpha1FailurePolicyTypeTov1FailurePolicyType(definitionInfo.lastReconciledValue.Spec.FailurePolicy), c.authz, ) diff --git a/staging/src/k8s.io/apiserver/pkg/admission/plugin/validatingadmissionpolicy/interface.go b/staging/src/k8s.io/apiserver/pkg/admission/plugin/validatingadmissionpolicy/interface.go index 9847e4856b7..23ec19cd397 100644 --- a/staging/src/k8s.io/apiserver/pkg/admission/plugin/validatingadmissionpolicy/interface.go +++ b/staging/src/k8s.io/apiserver/pkg/admission/plugin/validatingadmissionpolicy/interface.go @@ -55,5 +55,6 @@ type Matcher interface { // Validator is contains logic for converting ValidationEvaluation to PolicyDecisions type Validator interface { // Validate is used to take cel evaluations and convert into decisions - Validate(versionedAttr *generic.VersionedAttributes, versionedParams runtime.Object) []PolicyDecision + // runtimeCELCostBudget was added for testing purpose only. Callers should always use const RuntimeCELCostBudget from k8s.io/apiserver/pkg/apis/cel/config.go as input. + Validate(versionedAttr *generic.VersionedAttributes, versionedParams runtime.Object, runtimeCELCostBudget int64) []PolicyDecision } diff --git a/staging/src/k8s.io/apiserver/pkg/admission/plugin/validatingadmissionpolicy/validator.go b/staging/src/k8s.io/apiserver/pkg/admission/plugin/validatingadmissionpolicy/validator.go index 385d1618e57..39efdd61daf 100644 --- a/staging/src/k8s.io/apiserver/pkg/admission/plugin/validatingadmissionpolicy/validator.go +++ b/staging/src/k8s.io/apiserver/pkg/admission/plugin/validatingadmissionpolicy/validator.go @@ -54,7 +54,8 @@ func policyDecisionActionForError(f v1.FailurePolicyType) PolicyDecisionAction { } // Validate takes a list of Evaluation and a failure policy and converts them into actionable PolicyDecisions -func (v *validator) Validate(versionedAttr *generic.VersionedAttributes, versionedParams runtime.Object) []PolicyDecision { +// runtimeCELCostBudget was added for testing purpose only. Callers should always use const RuntimeCELCostBudget from k8s.io/apiserver/pkg/apis/cel/config.go as input. +func (v *validator) Validate(versionedAttr *generic.VersionedAttributes, versionedParams runtime.Object, runtimeCELCostBudget int64) []PolicyDecision { var f v1.FailurePolicyType if v.failPolicy == nil { f = v1.Fail @@ -63,7 +64,7 @@ func (v *validator) Validate(versionedAttr *generic.VersionedAttributes, version } optionalVars := cel.OptionalVariableBindings{VersionedParams: versionedParams, Authorizer: v.authorizer} - evalResults, err := v.filter.ForInput(versionedAttr, cel.CreateAdmissionRequest(versionedAttr.Attributes), optionalVars) + evalResults, err := v.filter.ForInput(versionedAttr, cel.CreateAdmissionRequest(versionedAttr.Attributes), optionalVars, runtimeCELCostBudget) if err != nil { return []PolicyDecision{ { diff --git a/staging/src/k8s.io/apiserver/pkg/admission/plugin/validatingadmissionpolicy/validator_test.go b/staging/src/k8s.io/apiserver/pkg/admission/plugin/validatingadmissionpolicy/validator_test.go index 06d70329535..2daf6230949 100644 --- a/staging/src/k8s.io/apiserver/pkg/admission/plugin/validatingadmissionpolicy/validator_test.go +++ b/staging/src/k8s.io/apiserver/pkg/admission/plugin/validatingadmissionpolicy/validator_test.go @@ -28,6 +28,7 @@ import ( admissionv1 "k8s.io/api/admission/v1" v1 "k8s.io/api/admissionregistration/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + celconfig "k8s.io/apiserver/pkg/apis/cel" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apiserver/pkg/admission" "k8s.io/apiserver/pkg/admission/plugin/cel" @@ -41,7 +42,7 @@ type fakeCelFilter struct { throwError bool } -func (f *fakeCelFilter) ForInput(*generic.VersionedAttributes, *admissionv1.AdmissionRequest, cel.OptionalVariableBindings) ([]cel.EvaluationResult, error) { +func (f *fakeCelFilter) ForInput(*generic.VersionedAttributes, *admissionv1.AdmissionRequest, cel.OptionalVariableBindings, runtimeCELCostBudget int64) ([]cel.EvaluationResult, error) { if f.throwError { return nil, errors.New("test error") } @@ -465,7 +466,7 @@ func TestValidate(t *testing.T) { }, } - policyResults := v.Validate(fakeVersionedAttr, nil) + policyResults := v.Validate(fakeVersionedAttr, nil, celconfig.RuntimeCELCostBudget) require.Equal(t, len(policyResults), len(tc.policyDecision)) diff --git a/staging/src/k8s.io/apiserver/pkg/apis/cel/config.go b/staging/src/k8s.io/apiserver/pkg/apis/cel/config.go new file mode 100644 index 00000000000..34f5f037b85 --- /dev/null +++ b/staging/src/k8s.io/apiserver/pkg/apis/cel/config.go @@ -0,0 +1,37 @@ +/* +Copyright 2023 The Kubernetes 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 + + http://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. +*/ + +package cel + +import apiservercel "k8s.io/apiserver/pkg/cel" + +const ( + // PerCallLimit specify the actual cost limit per CEL validation call + // current PerCallLimit gives roughly 0.1 second for each expression validation call + PerCallLimit = 1000000 + + // RuntimeCELCostBudget is the overall cost budget for runtime CEL validation cost per ValidatingAdmissionPolicyBinding or CustomResource + // current RuntimeCELCostBudget gives roughly 1 seconds for the validation + RuntimeCELCostBudget = 10000000 + + // CheckFrequency configures the number of iterations within a comprehension to evaluate + // before checking whether the function evaluation has been interrupted + CheckFrequency = 100 + + // MaxRequestSizeBytes is the maximum size of a request to the API server + // TODO(DangerOnTheRanger): wire in MaxRequestBodyBytes from apiserver/pkg/server/options/server_run_options.go to make this configurable + MaxRequestSizeBytes = apiservercel.DefaultMaxRequestSizeBytes +) diff --git a/vendor/modules.txt b/vendor/modules.txt index 4c1c913ed14..fc9f4b144e4 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -1474,6 +1474,7 @@ k8s.io/apiserver/pkg/apis/audit k8s.io/apiserver/pkg/apis/audit/install k8s.io/apiserver/pkg/apis/audit/v1 k8s.io/apiserver/pkg/apis/audit/validation +k8s.io/apiserver/pkg/apis/cel k8s.io/apiserver/pkg/apis/config k8s.io/apiserver/pkg/apis/config/v1 k8s.io/apiserver/pkg/apis/config/validation