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/apiextensions-apiserver/pkg/apis/apiextensions/validation/validation.go b/staging/src/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/validation/validation.go index ab69389e675..2ef5e7a1fc5 100644 --- a/staging/src/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/validation/validation.go +++ b/staging/src/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/validation/validation.go @@ -35,6 +35,7 @@ import ( "k8s.io/apimachinery/pkg/util/sets" utilvalidation "k8s.io/apimachinery/pkg/util/validation" "k8s.io/apimachinery/pkg/util/validation/field" + celconfig "k8s.io/apiserver/pkg/apis/cel" apiservercel "k8s.io/apiserver/pkg/cel" "k8s.io/apiserver/pkg/util/webhook" @@ -1026,7 +1027,7 @@ func ValidateCustomResourceDefinitionOpenAPISchema(schema *apiextensions.JSONSch } else if typeInfo == nil { allErrs.CELErrors = append(allErrs.CELErrors, field.InternalError(fldPath.Child("x-kubernetes-validations"), fmt.Errorf("internal error: failed to retrieve type information for x-kubernetes-validations"))) } else { - compResults, err := cel.Compile(typeInfo.Schema, typeInfo.DeclType, cel.PerCallLimit) + compResults, err := cel.Compile(typeInfo.Schema, typeInfo.DeclType, celconfig.PerCallLimit) if err != nil { allErrs.CELErrors = append(allErrs.CELErrors, field.InternalError(fldPath.Child("x-kubernetes-validations"), err)) } else { diff --git a/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema/cel/celcoststability_test.go b/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema/cel/celcoststability_test.go index 22e9aa6e060..656f8898a66 100644 --- a/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema/cel/celcoststability_test.go +++ b/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema/cel/celcoststability_test.go @@ -24,6 +24,7 @@ import ( "k8s.io/apiextensions-apiserver/pkg/apiserver/schema" "k8s.io/apimachinery/pkg/util/validation/field" + celconfig "k8s.io/apiserver/pkg/apis/cel" ) func TestCelCostStability(t *testing.T) { @@ -1096,16 +1097,16 @@ func TestCelCostStability(t *testing.T) { t.Run(testName, func(t *testing.T) { t.Parallel() s := withRule(*tt.schema, validRule) - celValidator := NewValidator(&s, true, PerCallLimit) + celValidator := NewValidator(&s, true, celconfig.PerCallLimit) if celValidator == nil { t.Fatal("expected non nil validator") } ctx := context.TODO() - errs, remainingBudegt := celValidator.Validate(ctx, field.NewPath("root"), &s, tt.obj, nil, RuntimeCELCostBudget) + errs, remainingBudegt := celValidator.Validate(ctx, field.NewPath("root"), &s, tt.obj, nil, celconfig.RuntimeCELCostBudget) for _, err := range errs { t.Errorf("unexpected error: %v", err) } - rtCost := RuntimeCELCostBudget - remainingBudegt + rtCost := celconfig.RuntimeCELCostBudget - remainingBudegt if rtCost != expectedCost { t.Fatalf("runtime cost %d does not match expected runtime cost %d", rtCost, expectedCost) } diff --git a/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema/cel/compilation.go b/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema/cel/compilation.go index 177671f098d..1f0c9861d1f 100644 --- a/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema/cel/compilation.go +++ b/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema/cel/compilation.go @@ -27,6 +27,7 @@ import ( apiextensions "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" "k8s.io/apiextensions-apiserver/pkg/apiserver/schema" + celconfig "k8s.io/apiserver/pkg/apis/cel" apiservercel "k8s.io/apiserver/pkg/cel" "k8s.io/apiserver/pkg/cel/library" "k8s.io/apiserver/pkg/cel/metrics" @@ -40,22 +41,6 @@ const ( // OldScopedVarName is the variable name assigned to the existing value of the locally scoped data element of a // CEL validation expression. OldScopedVarName = "oldSelf" - - // 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 CustomResource - // current RuntimeCELCostBudget gives roughly 1 seconds for CR 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 ) // CompilationResult represents the cel compilation result for one rule @@ -103,7 +88,7 @@ func getBaseEnv() (*cel.Env, error) { // - nil Program, non-nil Error: Compilation resulted in an error // - nil Program, nil Error: The provided rule was empty so compilation was not attempted // -// perCallLimit was added for testing purpose only. Callers should always use const PerCallLimit as input. +// 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 Compile(s *schema.Structural, declType *apiservercel.DeclType, perCallLimit uint64) ([]CompilationResult, error) { t := time.Now() defer func() { @@ -195,7 +180,7 @@ func compileRule(rule apiextensions.ValidationRule, env *cel.Env, perCallLimit u cel.CostLimit(perCallLimit), cel.CostTracking(estimator), cel.OptimizeRegex(library.ExtensionLibRegexOptimizations...), - cel.InterruptCheckFrequency(checkFrequency), + cel.InterruptCheckFrequency(celconfig.CheckFrequency), ) if err != nil { compilationResult.Error = &apiservercel.Error{Type: apiservercel.ErrorTypeInvalid, Detail: "program instantiation failed: " + err.Error()} @@ -274,5 +259,5 @@ func (c *sizeEstimator) EstimateCallCost(function, overloadID string, target *ch // this function. func maxCardinality(minSize int64) uint64 { sz := minSize + 1 // assume at least one comma between elements - return uint64(maxRequestSizeBytes / sz) + return uint64(celconfig.MaxRequestSizeBytes / sz) } diff --git a/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema/cel/compilation_test.go b/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema/cel/compilation_test.go index f9f4f567503..0634d463baf 100644 --- a/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema/cel/compilation_test.go +++ b/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema/cel/compilation_test.go @@ -27,6 +27,7 @@ import ( apiextensions "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" "k8s.io/apiextensions-apiserver/pkg/apiserver/schema" "k8s.io/apiextensions-apiserver/pkg/apiserver/schema/cel/model" + celconfig "k8s.io/apiserver/pkg/apis/cel" ) const ( @@ -645,7 +646,7 @@ func TestCelCompilation(t *testing.T) { for _, tt := range cases { t.Run(tt.name, func(t *testing.T) { - compilationResults, err := Compile(&tt.input, model.SchemaDeclType(&tt.input, false), PerCallLimit) + compilationResults, err := Compile(&tt.input, model.SchemaDeclType(&tt.input, false), celconfig.PerCallLimit) if err != nil { t.Errorf("Expected no error, but got: %v", err) } @@ -1081,7 +1082,7 @@ func genMapWithCustomItemRule(item *schema.Structural, rule string) func(maxProp // if expectedCostExceedsLimit is non-zero. Typically, only expectedCost or expectedCostExceedsLimit is non-zero, not both. func schemaChecker(schema *schema.Structural, expectedCost uint64, expectedCostExceedsLimit uint64, t *testing.T) func(t *testing.T) { return func(t *testing.T) { - compilationResults, err := Compile(schema, model.SchemaDeclType(schema, false), PerCallLimit) + compilationResults, err := Compile(schema, model.SchemaDeclType(schema, false), celconfig.PerCallLimit) if err != nil { t.Errorf("Expected no error, got: %v", err) } diff --git a/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema/cel/validation.go b/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema/cel/validation.go index 2a039b6b58e..dfa5822245c 100644 --- a/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema/cel/validation.go +++ b/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema/cel/validation.go @@ -64,7 +64,7 @@ type Validator struct { // of the Structural schema and returns a custom resource validator that contains nested // validators for all items, properties and additionalProperties that transitively contain validator rules. // Returns nil if there are no validator rules in the Structural schema. May return a validator containing only errors. -// Adding perCallLimit as input arg for testing purpose only. Callers should always use const PerCallLimit as input +// Adding perCallLimit as input arg for testing purpose only. Callers should always use const PerCallLimit from k8s.io/apiserver/pkg/apis/cel/config.go as input func NewValidator(s *schema.Structural, isResourceRoot bool, perCallLimit uint64) *Validator { if !hasXValidations(s) { return nil @@ -75,6 +75,7 @@ func NewValidator(s *schema.Structural, isResourceRoot bool, perCallLimit uint64 // validator creates a Validator for all x-kubernetes-validations at the level of the provided schema and lower and // returns the Validator if any x-kubernetes-validations exist in the schema, or nil if no x-kubernetes-validations // exist. declType is expected to be a CEL DeclType corresponding to the structural schema. +// 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 validator(s *schema.Structural, isResourceRoot bool, declType *cel.DeclType, perCallLimit uint64) *Validator { compiledRules, err := Compile(s, declType, perCallLimit) var itemsValidator, additionalPropertiesValidator *Validator diff --git a/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema/cel/validation_test.go b/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema/cel/validation_test.go index b2d1fb07580..e1c885d9b7e 100644 --- a/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema/cel/validation_test.go +++ b/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema/cel/validation_test.go @@ -30,6 +30,7 @@ import ( "k8s.io/apiextensions-apiserver/pkg/apiserver/schema" "k8s.io/apiextensions-apiserver/pkg/apiserver/schema/cel/model" "k8s.io/apimachinery/pkg/util/validation/field" + celconfig "k8s.io/apiserver/pkg/apis/cel" ) // TestValidationExpressions tests CEL integration with custom resource values and OpenAPIv3. @@ -1766,7 +1767,7 @@ func TestValidationExpressions(t *testing.T) { t.Run(tests[i].name, func(t *testing.T) { t.Parallel() tt := tests[i] - tt.costBudget = RuntimeCELCostBudget + tt.costBudget = celconfig.RuntimeCELCostBudget ctx := context.TODO() for j := range tt.valid { validRule := tt.valid[j] @@ -1777,7 +1778,7 @@ func TestValidationExpressions(t *testing.T) { t.Run(testName, func(t *testing.T) { t.Parallel() s := withRule(*tt.schema, validRule) - celValidator := validator(&s, tt.isRoot, model.SchemaDeclType(&s, tt.isRoot), PerCallLimit) + celValidator := validator(&s, tt.isRoot, model.SchemaDeclType(&s, tt.isRoot), celconfig.PerCallLimit) if celValidator == nil { t.Fatal("expected non nil validator") } @@ -1801,7 +1802,7 @@ func TestValidationExpressions(t *testing.T) { } t.Run(testName, func(t *testing.T) { s := withRule(*tt.schema, rule) - celValidator := NewValidator(&s, true, PerCallLimit) + celValidator := NewValidator(&s, true, celconfig.PerCallLimit) if celValidator == nil { t.Fatal("expected non nil validator") } @@ -2015,7 +2016,7 @@ func TestValidationExpressionsAtSchemaLevels(t *testing.T) { t.Run(tt.name, func(t *testing.T) { t.Parallel() ctx := context.TODO() - celValidator := validator(tt.schema, true, model.SchemaDeclType(tt.schema, true), PerCallLimit) + celValidator := validator(tt.schema, true, model.SchemaDeclType(tt.schema, true), celconfig.PerCallLimit) if celValidator == nil { t.Fatal("expected non nil validator") } @@ -2082,7 +2083,7 @@ func TestCELValidationLimit(t *testing.T) { t.Run(validRule, func(t *testing.T) { t.Parallel() s := withRule(*tt.schema, validRule) - celValidator := validator(&s, false, model.SchemaDeclType(&s, false), PerCallLimit) + celValidator := validator(&s, false, model.SchemaDeclType(&s, false), celconfig.PerCallLimit) // test with cost budget exceeded errs, _ := celValidator.Validate(ctx, field.NewPath("root"), &s, tt.obj, nil, 0) @@ -2107,7 +2108,7 @@ func TestCELValidationLimit(t *testing.T) { if celValidator == nil { t.Fatal("expected non nil validator") } - errs, _ = celValidator.Validate(ctx, field.NewPath("root"), &s, tt.obj, nil, RuntimeCELCostBudget) + errs, _ = celValidator.Validate(ctx, field.NewPath("root"), &s, tt.obj, nil, celconfig.RuntimeCELCostBudget) for _, err := range errs { if err.Type == field.ErrorTypeInvalid && strings.Contains(err.Error(), "no further validation rules will be run due to call cost exceeds limit for rule") { found = true @@ -2150,11 +2151,11 @@ func TestCELValidationContextCancellation(t *testing.T) { t.Run(tt.name, func(t *testing.T) { ctx := context.TODO() s := withRule(*tt.schema, tt.rule) - celValidator := NewValidator(&s, true, PerCallLimit) + celValidator := NewValidator(&s, true, celconfig.PerCallLimit) if celValidator == nil { t.Fatal("expected non nil validator") } - errs, _ := celValidator.Validate(ctx, field.NewPath("root"), &s, tt.obj, nil, RuntimeCELCostBudget) + errs, _ := celValidator.Validate(ctx, field.NewPath("root"), &s, tt.obj, nil, celconfig.RuntimeCELCostBudget) for _, err := range errs { t.Errorf("unexpected error: %v", err) } @@ -2163,7 +2164,7 @@ func TestCELValidationContextCancellation(t *testing.T) { found := false evalCtx, cancel := context.WithTimeout(ctx, time.Microsecond) cancel() - errs, _ = celValidator.Validate(evalCtx, field.NewPath("root"), &s, tt.obj, nil, RuntimeCELCostBudget) + errs, _ = celValidator.Validate(evalCtx, field.NewPath("root"), &s, tt.obj, nil, celconfig.RuntimeCELCostBudget) for _, err := range errs { if err.Type == field.ErrorTypeInvalid && strings.Contains(err.Error(), "operation interrupted") { found = true @@ -2208,7 +2209,7 @@ func TestCELMaxRecursionDepth(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - tt.costBudget = RuntimeCELCostBudget + tt.costBudget = celconfig.RuntimeCELCostBudget ctx := context.TODO() for j := range tt.valid { validRule := tt.valid[j] @@ -2216,7 +2217,7 @@ func TestCELMaxRecursionDepth(t *testing.T) { t.Run(testName, func(t *testing.T) { t.Parallel() s := withRule(*tt.schema, validRule) - celValidator := validator(&s, tt.isRoot, model.SchemaDeclType(&s, tt.isRoot), PerCallLimit) + celValidator := validator(&s, tt.isRoot, model.SchemaDeclType(&s, tt.isRoot), celconfig.PerCallLimit) if celValidator == nil { t.Fatal("expected non nil validator") } @@ -2240,7 +2241,7 @@ func TestCELMaxRecursionDepth(t *testing.T) { } t.Run(testName, func(t *testing.T) { s := withRule(*tt.schema, rule) - celValidator := NewValidator(&s, true, PerCallLimit) + celValidator := NewValidator(&s, true, celconfig.PerCallLimit) if celValidator == nil { t.Fatal("expected non nil validator") } @@ -2285,12 +2286,12 @@ func BenchmarkCELValidationWithContext(b *testing.B) { b.Run(tt.name, func(b *testing.B) { ctx := context.TODO() s := withRule(*tt.schema, tt.rule) - celValidator := NewValidator(&s, true, PerCallLimit) + celValidator := NewValidator(&s, true, celconfig.PerCallLimit) if celValidator == nil { b.Fatal("expected non nil validator") } for i := 0; i < b.N; i++ { - errs, _ := celValidator.Validate(ctx, field.NewPath("root"), &s, tt.obj, nil, RuntimeCELCostBudget) + errs, _ := celValidator.Validate(ctx, field.NewPath("root"), &s, tt.obj, nil, celconfig.RuntimeCELCostBudget) for _, err := range errs { b.Fatalf("validation failed: %v", err) } @@ -2325,14 +2326,14 @@ func BenchmarkCELValidationWithCancelledContext(b *testing.B) { b.Run(tt.name, func(b *testing.B) { ctx := context.TODO() s := withRule(*tt.schema, tt.rule) - celValidator := NewValidator(&s, true, PerCallLimit) + celValidator := NewValidator(&s, true, celconfig.PerCallLimit) if celValidator == nil { b.Fatal("expected non nil validator") } for i := 0; i < b.N; i++ { evalCtx, cancel := context.WithTimeout(ctx, time.Microsecond) cancel() - errs, _ := celValidator.Validate(evalCtx, field.NewPath("root"), &s, tt.obj, nil, RuntimeCELCostBudget) + errs, _ := celValidator.Validate(evalCtx, field.NewPath("root"), &s, tt.obj, nil, celconfig.RuntimeCELCostBudget) //found := false //for _, err := range errs { // if err.Type == field.ErrorTypeInvalid && strings.Contains(err.Error(), "operation interrupted") { @@ -2379,7 +2380,7 @@ func BenchmarkCELValidationWithAndWithoutOldSelfReference(b *testing.B) { }, }, } - validator := NewValidator(s, true, PerCallLimit) + validator := NewValidator(s, true, celconfig.PerCallLimit) if validator == nil { b.Fatal("expected non nil validator") } @@ -2390,7 +2391,7 @@ func BenchmarkCELValidationWithAndWithoutOldSelfReference(b *testing.B) { b.ReportAllocs() b.ResetTimer() for i := 0; i < b.N; i++ { - errs, _ := validator.Validate(ctx, root, s, obj, obj, RuntimeCELCostBudget) + errs, _ := validator.Validate(ctx, root, s, obj, obj, celconfig.RuntimeCELCostBudget) for _, err := range errs { b.Errorf("unexpected error: %v", err) } diff --git a/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema/defaulting/validation.go b/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema/defaulting/validation.go index f89500b60bd..cd7c6e07558 100644 --- a/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema/defaulting/validation.go +++ b/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema/defaulting/validation.go @@ -32,6 +32,7 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/util/validation/field" + celconfig "k8s.io/apiserver/pkg/apis/cel" ) // ValidateDefaults checks that default values validate and are properly pruned. @@ -50,7 +51,7 @@ func ValidateDefaults(ctx context.Context, pth *field.Path, s *structuralschema. } } - allErr, error, _ := validate(ctx, pth, s, s, f, false, requirePrunedDefaults, cel.RuntimeCELCostBudget) + allErr, error, _ := validate(ctx, pth, s, s, f, false, requirePrunedDefaults, celconfig.RuntimeCELCostBudget) return allErr, error } @@ -91,7 +92,7 @@ func validate(ctx context.Context, pth *field.Path, s *structuralschema.Structur allErrs = append(allErrs, field.Invalid(pth.Child("default"), s.Default.Object, fmt.Sprintf("must result in valid metadata: %v", errs.ToAggregate()))) } else if errs := apiservervalidation.ValidateCustomResource(pth.Child("default"), s.Default.Object, validator); len(errs) > 0 { allErrs = append(allErrs, errs...) - } else if celValidator := cel.NewValidator(s, isResourceRoot, cel.PerCallLimit); celValidator != nil { + } else if celValidator := cel.NewValidator(s, isResourceRoot, celconfig.PerCallLimit); celValidator != nil { celErrs, rmCost := celValidator.Validate(ctx, pth.Child("default"), s, s.Default.Object, s.Default.Object, remainingCost) remainingCost = rmCost allErrs = append(allErrs, celErrs...) @@ -116,7 +117,7 @@ func validate(ctx context.Context, pth *field.Path, s *structuralschema.Structur allErrs = append(allErrs, errs...) } else if errs := apiservervalidation.ValidateCustomResource(pth.Child("default"), s.Default.Object, validator); len(errs) > 0 { allErrs = append(allErrs, errs...) - } else if celValidator := cel.NewValidator(s, isResourceRoot, cel.PerCallLimit); celValidator != nil { + } else if celValidator := cel.NewValidator(s, isResourceRoot, celconfig.PerCallLimit); celValidator != nil { celErrs, rmCost := celValidator.Validate(ctx, pth.Child("default"), s, s.Default.Object, s.Default.Object, remainingCost) remainingCost = rmCost allErrs = append(allErrs, celErrs...) diff --git a/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/validation/validation_test.go b/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/validation/validation_test.go index 49b1cab79d2..7bbeba3eae5 100644 --- a/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/validation/validation_test.go +++ b/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/validation/validation_test.go @@ -42,6 +42,7 @@ import ( "k8s.io/apimachinery/pkg/runtime/serializer" "k8s.io/apimachinery/pkg/util/json" "k8s.io/apimachinery/pkg/util/sets" + celconfig "k8s.io/apiserver/pkg/apis/cel" ) // TestRoundTrip checks the conversion to go-openapi types. @@ -608,7 +609,7 @@ func TestValidateCustomResource(t *testing.T) { if err != nil { t.Fatal(err) } - celValidator := cel.NewValidator(structural, false, cel.PerCallLimit) + celValidator := cel.NewValidator(structural, false, celconfig.PerCallLimit) for i, obj := range tt.objects { var oldObject interface{} if len(tt.oldObjects) == len(tt.objects) { @@ -617,14 +618,14 @@ func TestValidateCustomResource(t *testing.T) { if errs := ValidateCustomResource(nil, obj, validator); len(errs) > 0 { t.Errorf("unexpected validation error for %v: %v", obj, errs) } - errs, _ := celValidator.Validate(context.TODO(), nil, structural, obj, oldObject, cel.RuntimeCELCostBudget) + errs, _ := celValidator.Validate(context.TODO(), nil, structural, obj, oldObject, celconfig.RuntimeCELCostBudget) if len(errs) > 0 { t.Errorf(errs.ToAggregate().Error()) } } for i, failingObject := range tt.failingObjects { errs := ValidateCustomResource(nil, failingObject.object, validator) - celErrs, _ := celValidator.Validate(context.TODO(), nil, structural, failingObject.object, failingObject.oldObject, cel.RuntimeCELCostBudget) + celErrs, _ := celValidator.Validate(context.TODO(), nil, structural, failingObject.object, failingObject.oldObject, celconfig.RuntimeCELCostBudget) errs = append(errs, celErrs...) if len(errs) == 0 { t.Errorf("missing error for %v", failingObject.object) diff --git a/staging/src/k8s.io/apiextensions-apiserver/pkg/registry/customresource/status_strategy.go b/staging/src/k8s.io/apiextensions-apiserver/pkg/registry/customresource/status_strategy.go index efd9b2dff55..8d3796ac9af 100644 --- a/staging/src/k8s.io/apiextensions-apiserver/pkg/registry/customresource/status_strategy.go +++ b/staging/src/k8s.io/apiextensions-apiserver/pkg/registry/customresource/status_strategy.go @@ -21,11 +21,11 @@ import ( "sigs.k8s.io/structured-merge-diff/v4/fieldpath" - "k8s.io/apiextensions-apiserver/pkg/apiserver/schema/cel" structurallisttype "k8s.io/apiextensions-apiserver/pkg/apiserver/schema/listtype" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/util/validation/field" + celconfig "k8s.io/apiserver/pkg/apis/cel" ) type statusStrategy struct { @@ -113,7 +113,7 @@ func (a statusStrategy) ValidateUpdate(ctx context.Context, obj, old runtime.Obj if has, err := hasBlockingErr(errs); has { errs = append(errs, err) } else { - err, _ := celValidator.Validate(ctx, nil, a.customResourceStrategy.structuralSchemas[v], uNew.Object, oldObject, cel.RuntimeCELCostBudget) + err, _ := celValidator.Validate(ctx, nil, a.customResourceStrategy.structuralSchemas[v], uNew.Object, oldObject, celconfig.RuntimeCELCostBudget) errs = append(errs, err...) } } diff --git a/staging/src/k8s.io/apiextensions-apiserver/pkg/registry/customresource/strategy.go b/staging/src/k8s.io/apiextensions-apiserver/pkg/registry/customresource/strategy.go index 926806af7fc..92a688a56d7 100644 --- a/staging/src/k8s.io/apiextensions-apiserver/pkg/registry/customresource/strategy.go +++ b/staging/src/k8s.io/apiextensions-apiserver/pkg/registry/customresource/strategy.go @@ -35,6 +35,7 @@ import ( "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/util/validation/field" + celconfig "k8s.io/apiserver/pkg/apis/cel" "k8s.io/apiserver/pkg/features" apiserverstorage "k8s.io/apiserver/pkg/storage" "k8s.io/apiserver/pkg/storage/names" @@ -61,7 +62,7 @@ func NewStrategy(typer runtime.ObjectTyper, namespaceScoped bool, kind schema.Gr celValidators := map[string]*cel.Validator{} if utilfeature.DefaultFeatureGate.Enabled(features.CustomResourceValidationExpressions) { for name, s := range structuralSchemas { - v := cel.NewValidator(s, true, cel.PerCallLimit) // CEL programs are compiled and cached here + v := cel.NewValidator(s, true, celconfig.PerCallLimit) // CEL programs are compiled and cached here if v != nil { celValidators[name] = v } @@ -178,7 +179,7 @@ func (a customResourceStrategy) Validate(ctx context.Context, obj runtime.Object if has, err := hasBlockingErr(errs); has { errs = append(errs, err) } else { - err, _ := celValidator.Validate(ctx, nil, a.structuralSchemas[v], u.Object, nil, cel.RuntimeCELCostBudget) + err, _ := celValidator.Validate(ctx, nil, a.structuralSchemas[v], u.Object, nil, celconfig.RuntimeCELCostBudget) errs = append(errs, err...) } } @@ -235,7 +236,7 @@ func (a customResourceStrategy) ValidateUpdate(ctx context.Context, obj, old run if has, err := hasBlockingErr(errs); has { errs = append(errs, err) } else { - err, _ := celValidator.Validate(ctx, nil, a.structuralSchemas[v], uNew.Object, uOld.Object, cel.RuntimeCELCostBudget) + err, _ := celValidator.Validate(ctx, nil, a.structuralSchemas[v], uNew.Object, uOld.Object, celconfig.RuntimeCELCostBudget) errs = append(errs, err...) } } 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 ff1566d62ae..9c72319a6ff 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 @@ -33,6 +33,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" @@ -459,7 +460,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..e5e1160f4c7 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 @@ -32,6 +32,7 @@ import ( "k8s.io/apiserver/pkg/admission" "k8s.io/apiserver/pkg/admission/plugin/cel" "k8s.io/apiserver/pkg/admission/plugin/webhook/generic" + celconfig "k8s.io/apiserver/pkg/apis/cel" ) var _ cel.Filter = &fakeCelFilter{} @@ -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, 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..4e66868c14e --- /dev/null +++ b/staging/src/k8s.io/apiserver/pkg/apis/cel/config.go @@ -0,0 +1,36 @@ +/* +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 + +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 + // Note that even if server_run_options.go becomes configurable in the future, this cost constant should be fixed and it should be the max allowed request size for the server + MaxRequestSizeBytes = int64(3 * 1024 * 1024) +) diff --git a/staging/src/k8s.io/apiserver/pkg/cel/limits.go b/staging/src/k8s.io/apiserver/pkg/cel/limits.go index 7bdb958d05c..65c6ad5f3a3 100644 --- a/staging/src/k8s.io/apiserver/pkg/cel/limits.go +++ b/staging/src/k8s.io/apiserver/pkg/cel/limits.go @@ -16,9 +16,11 @@ limitations under the License. package cel +import celconfig "k8s.io/apiserver/pkg/apis/cel" + const ( // DefaultMaxRequestSizeBytes is the size of the largest request that will be accepted - DefaultMaxRequestSizeBytes = int64(3 * 1024 * 1024) + DefaultMaxRequestSizeBytes = celconfig.MaxRequestSizeBytes // MaxDurationSizeJSON // OpenAPI duration strings follow RFC 3339, section 5.6 - see the comment on maxDatetimeSizeJSON 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