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 8d8d0944997..e5c851d650d 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 @@ -17,6 +17,7 @@ limitations under the License. package validation import ( + "context" "fmt" "reflect" "regexp" @@ -48,7 +49,8 @@ var ( ) // ValidateCustomResourceDefinition statically validates -func ValidateCustomResourceDefinition(obj *apiextensions.CustomResourceDefinition) field.ErrorList { +// context is passed for supporting context cancellation during cel validation when validating defaults +func ValidateCustomResourceDefinition(ctx context.Context, obj *apiextensions.CustomResourceDefinition) field.ErrorList { nameValidationFn := func(name string, prefix bool) []string { ret := genericvalidation.NameIsDNSSubdomain(name, prefix) requiredName := obj.Spec.Names.Plural + "." + obj.Spec.Group @@ -71,7 +73,7 @@ func ValidateCustomResourceDefinition(obj *apiextensions.CustomResourceDefinitio } allErrs := genericvalidation.ValidateObjectMeta(&obj.ObjectMeta, false, nameValidationFn, field.NewPath("metadata")) - allErrs = append(allErrs, validateCustomResourceDefinitionSpec(&obj.Spec, opts, field.NewPath("spec"))...) + allErrs = append(allErrs, validateCustomResourceDefinitionSpec(ctx, &obj.Spec, opts, field.NewPath("spec"))...) allErrs = append(allErrs, ValidateCustomResourceDefinitionStatus(&obj.Status, field.NewPath("status"))...) allErrs = append(allErrs, ValidateCustomResourceDefinitionStoredVersions(obj.Status.StoredVersions, obj.Spec.Versions, field.NewPath("status").Child("storedVersions"))...) allErrs = append(allErrs, validateAPIApproval(obj, nil)...) @@ -106,7 +108,8 @@ type validationOptions struct { } // ValidateCustomResourceDefinitionUpdate statically validates -func ValidateCustomResourceDefinitionUpdate(obj, oldObj *apiextensions.CustomResourceDefinition) field.ErrorList { +// context is passed for supporting context cancellation during cel validation when validating defaults +func ValidateCustomResourceDefinitionUpdate(ctx context.Context, obj, oldObj *apiextensions.CustomResourceDefinition) field.ErrorList { opts := validationOptions{ allowDefaults: true, requireRecognizedConversionReviewVersion: oldObj.Spec.Conversion == nil || hasValidConversionReviewVersionOrEmpty(oldObj.Spec.Conversion.ConversionReviewVersions), @@ -120,7 +123,7 @@ func ValidateCustomResourceDefinitionUpdate(obj, oldObj *apiextensions.CustomRes } allErrs := genericvalidation.ValidateObjectMetaUpdate(&obj.ObjectMeta, &oldObj.ObjectMeta, field.NewPath("metadata")) - allErrs = append(allErrs, validateCustomResourceDefinitionSpecUpdate(&obj.Spec, &oldObj.Spec, opts, field.NewPath("spec"))...) + allErrs = append(allErrs, validateCustomResourceDefinitionSpecUpdate(ctx, &obj.Spec, &oldObj.Spec, opts, field.NewPath("spec"))...) allErrs = append(allErrs, ValidateCustomResourceDefinitionStatus(&obj.Status, field.NewPath("status"))...) allErrs = append(allErrs, ValidateCustomResourceDefinitionStoredVersions(obj.Status.StoredVersions, obj.Spec.Versions, field.NewPath("status").Child("storedVersions"))...) allErrs = append(allErrs, validateAPIApproval(obj, oldObj)...) @@ -163,12 +166,13 @@ func ValidateUpdateCustomResourceDefinitionStatus(obj, oldObj *apiextensions.Cus } // validateCustomResourceDefinitionVersion statically validates. -func validateCustomResourceDefinitionVersion(version *apiextensions.CustomResourceDefinitionVersion, fldPath *field.Path, statusEnabled bool, opts validationOptions) field.ErrorList { +// context is passed for supporting context cancellation during cel validation when validating defaults +func validateCustomResourceDefinitionVersion(ctx context.Context, version *apiextensions.CustomResourceDefinitionVersion, fldPath *field.Path, statusEnabled bool, opts validationOptions) field.ErrorList { allErrs := field.ErrorList{} for _, err := range validateDeprecationWarning(version.Deprecated, version.DeprecationWarning) { allErrs = append(allErrs, field.Invalid(fldPath.Child("deprecationWarning"), version.DeprecationWarning, err)) } - allErrs = append(allErrs, validateCustomResourceDefinitionValidation(version.Schema, statusEnabled, opts, fldPath.Child("schema"))...) + allErrs = append(allErrs, validateCustomResourceDefinitionValidation(ctx, version.Schema, statusEnabled, opts, fldPath.Child("schema"))...) allErrs = append(allErrs, ValidateCustomResourceDefinitionSubresources(version.Subresources, fldPath.Child("subresources"))...) for i := range version.AdditionalPrinterColumns { allErrs = append(allErrs, ValidateCustomResourceColumnDefinition(&version.AdditionalPrinterColumns[i], fldPath.Child("additionalPrinterColumns").Index(i))...) @@ -206,7 +210,8 @@ func validateDeprecationWarning(deprecated bool, deprecationWarning *string) []s return errors } -func validateCustomResourceDefinitionSpec(spec *apiextensions.CustomResourceDefinitionSpec, opts validationOptions, fldPath *field.Path) field.ErrorList { +// context is passed for supporting context cancellation during cel validation when validating defaults +func validateCustomResourceDefinitionSpec(ctx context.Context, spec *apiextensions.CustomResourceDefinitionSpec, opts validationOptions, fldPath *field.Path) field.ErrorList { allErrs := field.ErrorList{} if len(spec.Group) == 0 { @@ -270,7 +275,7 @@ func validateCustomResourceDefinitionSpec(spec *apiextensions.CustomResourceDefi allErrs = append(allErrs, field.Invalid(fldPath.Child("versions").Index(i).Child("name"), spec.Versions[i].Name, strings.Join(errs, ","))) } subresources := getSubresourcesForVersion(spec, version.Name) - allErrs = append(allErrs, validateCustomResourceDefinitionVersion(&version, fldPath.Child("versions").Index(i), hasStatusEnabled(subresources), opts)...) + allErrs = append(allErrs, validateCustomResourceDefinitionVersion(ctx, &version, fldPath.Child("versions").Index(i), hasStatusEnabled(subresources), opts)...) } // The top-level and per-version fields are mutual exclusive @@ -325,7 +330,7 @@ func validateCustomResourceDefinitionSpec(spec *apiextensions.CustomResourceDefi } allErrs = append(allErrs, ValidateCustomResourceDefinitionNames(&spec.Names, fldPath.Child("names"))...) - allErrs = append(allErrs, validateCustomResourceDefinitionValidation(spec.Validation, hasAnyStatusEnabled(spec), opts, fldPath.Child("validation"))...) + allErrs = append(allErrs, validateCustomResourceDefinitionValidation(ctx, spec.Validation, hasAnyStatusEnabled(spec), opts, fldPath.Child("validation"))...) allErrs = append(allErrs, ValidateCustomResourceDefinitionSubresources(spec.Subresources, fldPath.Child("subresources"))...) for i := range spec.AdditionalPrinterColumns { @@ -448,8 +453,9 @@ func validateCustomResourceConversion(conversion *apiextensions.CustomResourceCo } // validateCustomResourceDefinitionSpecUpdate statically validates -func validateCustomResourceDefinitionSpecUpdate(spec, oldSpec *apiextensions.CustomResourceDefinitionSpec, opts validationOptions, fldPath *field.Path) field.ErrorList { - allErrs := validateCustomResourceDefinitionSpec(spec, opts, fldPath) +// context is passed for supporting context cancellation during cel validation when validating defaults +func validateCustomResourceDefinitionSpecUpdate(ctx context.Context, spec, oldSpec *apiextensions.CustomResourceDefinitionSpec, opts validationOptions, fldPath *field.Path) field.ErrorList { + allErrs := validateCustomResourceDefinitionSpec(ctx, spec, opts, fldPath) if opts.requireImmutableNames { // these effect the storage and cannot be changed therefore @@ -661,7 +667,8 @@ type specStandardValidator interface { } // validateCustomResourceDefinitionValidation statically validates -func validateCustomResourceDefinitionValidation(customResourceValidation *apiextensions.CustomResourceValidation, statusSubresourceEnabled bool, opts validationOptions, fldPath *field.Path) field.ErrorList { +// context is passed for supporting context cancellation during cel validation when validating defaults +func validateCustomResourceDefinitionValidation(ctx context.Context, customResourceValidation *apiextensions.CustomResourceValidation, statusSubresourceEnabled bool, opts validationOptions, fldPath *field.Path) field.ErrorList { allErrs := field.ErrorList{} if customResourceValidation == nil { @@ -717,7 +724,7 @@ func validateCustomResourceDefinitionValidation(customResourceValidation *apiext } } else if validationErrors := structuralschema.ValidateStructural(fldPath.Child("openAPIV3Schema"), ss); len(validationErrors) > 0 { allErrs = append(allErrs, validationErrors...) - } else if validationErrors, err := structuraldefaulting.ValidateDefaults(fldPath.Child("openAPIV3Schema"), ss, true, opts.requirePrunedDefaults); err != nil { + } else if validationErrors, err := structuraldefaulting.ValidateDefaults(ctx, fldPath.Child("openAPIV3Schema"), ss, true, opts.requirePrunedDefaults); err != nil { // this should never happen allErrs = append(allErrs, field.Invalid(fldPath.Child("openAPIV3Schema"), "", err.Error())) } else { diff --git a/staging/src/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/validation/validation_test.go b/staging/src/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/validation/validation_test.go index 4a4400b4c29..fdfb79c67b6 100644 --- a/staging/src/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/validation/validation_test.go +++ b/staging/src/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/validation/validation_test.go @@ -17,6 +17,7 @@ limitations under the License. package validation import ( + "context" "math/rand" "reflect" "strings" @@ -4072,7 +4073,8 @@ func TestValidateCustomResourceDefinition(t *testing.T) { if tc.resource.Spec.Conversion != nil && tc.resource.Spec.Conversion.Strategy == apiextensions.WebhookConverter && len(tc.resource.Spec.Conversion.ConversionReviewVersions) == 0 { tc.resource.Spec.Conversion.ConversionReviewVersions = []string{"v1beta1"} } - errs := ValidateCustomResourceDefinition(tc.resource) + ctx := context.TODO() + errs := ValidateCustomResourceDefinition(ctx, tc.resource) seenErrs := make([]bool, len(errs)) for _, expectedError := range tc.errors { @@ -6199,7 +6201,8 @@ func TestValidateCustomResourceDefinitionUpdate(t *testing.T) { for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { - errs := ValidateCustomResourceDefinitionUpdate(tc.resource, tc.old) + ctx := context.TODO() + errs := ValidateCustomResourceDefinitionUpdate(ctx, tc.resource, tc.old) seenErrs := make([]bool, len(errs)) for _, expectedError := range tc.errors { @@ -7965,7 +7968,8 @@ func TestValidateCustomResourceDefinitionValidation(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got := validateCustomResourceDefinitionValidation(&tt.input, tt.statusEnabled, tt.opts, field.NewPath("spec", "validation")) + ctx := context.TODO() + got := validateCustomResourceDefinitionValidation(ctx, &tt.input, tt.statusEnabled, tt.opts, field.NewPath("spec", "validation")) seenErrs := make([]bool, len(got)) 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 e405181ed56..0167c817a78 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 @@ -49,6 +49,10 @@ const ( // RuntimeCELCostBudget is the overall cost budget for runtime CEL validation cost per CustomResource // current RuntimeCELCostBudget gives roughly 1 seconds for CR validation RuntimeCELCostBudget = 20000000 + + // checkFrequency configures the number of iterations within a comprehension to evaluate + // before checking whether the function evaluation has been interrupted + checkFrequency = 100 ) // CompilationResult represents the cel compilation result for one rule @@ -153,7 +157,7 @@ func compileRule(rule apiextensions.ValidationRule, env *cel.Env, perCallLimit u } // TODO: Ideally we could configure the per expression limit at validation time and set it to the remaining overall budget, but we would either need a way to pass in a limit at evaluation time or move program creation to validation time - prog, err := env.Program(ast, cel.EvalOptions(cel.OptOptimize, cel.OptTrackCost), cel.CostLimit(perCallLimit)) + prog, err := env.Program(ast, cel.EvalOptions(cel.OptOptimize, cel.OptTrackCost), cel.CostLimit(perCallLimit), cel.InterruptCheckFrequency(checkFrequency)) if err != nil { compilationResult.Error = &Error{ErrorTypeInvalid, "program instantiation failed: " + err.Error()} return 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 5c1d1cba669..746b2807edb 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 @@ -17,6 +17,7 @@ limitations under the License. package cel import ( + "context" "fmt" "math" "strings" @@ -96,32 +97,33 @@ func validator(s *schema.Structural, isResourceRoot bool, perCallLimit uint64) * // Validate validates all x-kubernetes-validations rules in Validator against obj and returns any errors. // If the validation rules exceed the costBudget, subsequent evaluations will be skipped, the list of errs returned will not be empty, and a negative remainingBudget will be returned. // Most callers can ignore the returned remainingBudget value unless another validate call is going to be made -func (s *Validator) Validate(fldPath *field.Path, sts *schema.Structural, obj interface{}, costBudget int64) (errs field.ErrorList, remainingBudget int64) { +// context is passed for supporting context cancellation during cel validation +func (s *Validator) Validate(ctx context.Context, fldPath *field.Path, sts *schema.Structural, obj interface{}, costBudget int64) (errs field.ErrorList, remainingBudget int64) { remainingBudget = costBudget if s == nil || obj == nil { return nil, remainingBudget } - errs, remainingBudget = s.validateExpressions(fldPath, sts, obj, remainingBudget) + errs, remainingBudget = s.validateExpressions(ctx, fldPath, sts, obj, remainingBudget) if remainingBudget < 0 { return errs, remainingBudget } switch obj := obj.(type) { case []interface{}: var arrayErrs field.ErrorList - arrayErrs, remainingBudget = s.validateArray(fldPath, sts, obj, remainingBudget) + arrayErrs, remainingBudget = s.validateArray(ctx, fldPath, sts, obj, remainingBudget) errs = append(errs, arrayErrs...) return errs, remainingBudget case map[string]interface{}: var mapErrs field.ErrorList - mapErrs, remainingBudget = s.validateMap(fldPath, sts, obj, remainingBudget) + mapErrs, remainingBudget = s.validateMap(ctx, fldPath, sts, obj, remainingBudget) errs = append(errs, mapErrs...) return errs, remainingBudget } return errs, remainingBudget } -func (s *Validator) validateExpressions(fldPath *field.Path, sts *schema.Structural, obj interface{}, costBudget int64) (errs field.ErrorList, remainingBudget int64) { +func (s *Validator) validateExpressions(ctx context.Context, fldPath *field.Path, sts *schema.Structural, obj interface{}, costBudget int64) (errs field.ErrorList, remainingBudget int64) { remainingBudget = costBudget if obj == nil { // We only validate non-null values. Rules that need to check for the state of a nullable value or the presence of an optional @@ -159,7 +161,7 @@ func (s *Validator) validateExpressions(fldPath *field.Path, sts *schema.Structu errs = append(errs, field.InternalError(fldPath, fmt.Errorf("oldSelf validation not implemented"))) continue // todo: wire oldObj parameter } - evalResult, evalDetails, err := compiled.Program.Eval(activation) + evalResult, evalDetails, err := compiled.Program.ContextEval(ctx, activation) if evalDetails == nil { errs = append(errs, field.InternalError(fldPath, fmt.Errorf("runtime cost could not be calculated for validation rule: %v, no further validation rules will be run", ruleErrorString(rule)))) return errs, -1 @@ -230,7 +232,7 @@ func (a *validationActivation) Parent() interpreter.Activation { return nil } -func (s *Validator) validateMap(fldPath *field.Path, sts *schema.Structural, obj map[string]interface{}, costBudget int64) (errs field.ErrorList, remainingBudget int64) { +func (s *Validator) validateMap(ctx context.Context, fldPath *field.Path, sts *schema.Structural, obj map[string]interface{}, costBudget int64) (errs field.ErrorList, remainingBudget int64) { remainingBudget = costBudget if remainingBudget < 0 { return errs, remainingBudget @@ -242,7 +244,7 @@ func (s *Validator) validateMap(fldPath *field.Path, sts *schema.Structural, obj if s.AdditionalProperties != nil && sts.AdditionalProperties != nil && sts.AdditionalProperties.Structural != nil { for k, v := range obj { var err field.ErrorList - err, remainingBudget = s.AdditionalProperties.Validate(fldPath.Key(k), sts.AdditionalProperties.Structural, v, remainingBudget) + err, remainingBudget = s.AdditionalProperties.Validate(ctx, fldPath.Key(k), sts.AdditionalProperties.Structural, v, remainingBudget) errs = append(errs, err...) if remainingBudget < 0 { return errs, remainingBudget @@ -255,7 +257,7 @@ func (s *Validator) validateMap(fldPath *field.Path, sts *schema.Structural, obj sub, ok := s.Properties[k] if ok && stsOk { var err field.ErrorList - err, remainingBudget = sub.Validate(fldPath.Child(k), &stsProp, v, remainingBudget) + err, remainingBudget = sub.Validate(ctx, fldPath.Child(k), &stsProp, v, remainingBudget) errs = append(errs, err...) if remainingBudget < 0 { return errs, remainingBudget @@ -267,7 +269,7 @@ func (s *Validator) validateMap(fldPath *field.Path, sts *schema.Structural, obj return errs, remainingBudget } -func (s *Validator) validateArray(fldPath *field.Path, sts *schema.Structural, obj []interface{}, costBudget int64) (errs field.ErrorList, remainingBudget int64) { +func (s *Validator) validateArray(ctx context.Context, fldPath *field.Path, sts *schema.Structural, obj []interface{}, costBudget int64) (errs field.ErrorList, remainingBudget int64) { remainingBudget = costBudget if remainingBudget < 0 { return errs, remainingBudget @@ -275,7 +277,7 @@ func (s *Validator) validateArray(fldPath *field.Path, sts *schema.Structural, o if s.Items != nil && sts.Items != nil { for i := range obj { var err field.ErrorList - err, remainingBudget = s.Items.Validate(fldPath.Index(i), sts.Items, obj[i], remainingBudget) + err, remainingBudget = s.Items.Validate(ctx, fldPath.Index(i), sts.Items, obj[i], remainingBudget) errs = append(errs, err...) if remainingBudget < 0 { return errs, remainingBudget 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 382f97fc440..efc54a3e1cb 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 @@ -17,10 +17,12 @@ limitations under the License. package cel import ( + "context" "fmt" "math" "strings" "testing" + "time" apiextensions "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" "k8s.io/apiextensions-apiserver/pkg/apiserver/schema" @@ -1686,9 +1688,9 @@ func TestValidationExpressions(t *testing.T) { i := i t.Run(tests[i].name, func(t *testing.T) { t.Parallel() - // set costBudget to maxInt64 for current test tt := tests[i] - tt.costBudget = math.MaxInt64 + tt.costBudget = RuntimeCELCostBudget + ctx := context.TODO() for j := range tt.valid { validRule := tt.valid[j] t.Run(validRule, func(t *testing.T) { @@ -1698,13 +1700,13 @@ func TestValidationExpressions(t *testing.T) { if celValidator == nil { t.Fatal("expected non nil validator") } - errs, _ := celValidator.Validate(field.NewPath("root"), &s, tt.obj, tt.costBudget) + errs, _ := celValidator.Validate(ctx, field.NewPath("root"), &s, tt.obj, tt.costBudget) for _, err := range errs { t.Errorf("unexpected error: %v", err) } // test with cost budget exceeded - errs, _ = celValidator.Validate(field.NewPath("root"), &s, tt.obj, 0) + errs, _ = celValidator.Validate(ctx, field.NewPath("root"), &s, tt.obj, 0) var found bool for _, err := range errs { if err.Type == field.ErrorTypeInvalid && strings.Contains(err.Error(), "validation failed due to running out of cost budget, no further validation rules will be run") { @@ -1724,7 +1726,7 @@ func TestValidationExpressions(t *testing.T) { if celValidator == nil { t.Fatal("expected non nil validator") } - errs, _ = celValidator.Validate(field.NewPath("root"), &s, tt.obj, tt.costBudget) + errs, _ = celValidator.Validate(ctx, field.NewPath("root"), &s, tt.obj, tt.costBudget) 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 @@ -1743,7 +1745,7 @@ func TestValidationExpressions(t *testing.T) { if celValidator == nil { t.Fatal("expected non nil validator") } - errs, _ := celValidator.Validate(field.NewPath("root"), &s, tt.obj, tt.costBudget) + errs, _ := celValidator.Validate(ctx, field.NewPath("root"), &s, tt.obj, tt.costBudget) if len(errs) == 0 { t.Error("expected validation errors but got none") } @@ -1754,7 +1756,7 @@ func TestValidationExpressions(t *testing.T) { } // test with cost budget exceeded - errs, _ = celValidator.Validate(field.NewPath("root"), &s, tt.obj, 0) + errs, _ = celValidator.Validate(ctx, field.NewPath("root"), &s, tt.obj, 0) var found bool for _, err := range errs { if err.Type == field.ErrorTypeInvalid && strings.Contains(err.Error(), "validation failed due to running out of cost budget, no further validation rules will be run") { @@ -1773,6 +1775,148 @@ func TestValidationExpressions(t *testing.T) { } } +func TestCELValidationContextCancellation(t *testing.T) { + items := make([]interface{}, 1000) + for i := int64(0); i < 1000; i++ { + items[i] = i + } + tests := []struct { + name string + schema *schema.Structural + obj map[string]interface{} + rule string + }{ + {name: "test cel validation with context cancellation", + obj: map[string]interface{}{ + "array": items, + }, + schema: objectTypePtr(map[string]schema.Structural{ + "array": listType(&integerType), + }), + rule: "self.array.map(e, e * 20).filter(e, e > 50).exists(e, e == 60)", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctx := context.TODO() + s := withRule(*tt.schema, tt.rule) + celValidator := NewValidator(&s, PerCallLimit) + if celValidator == nil { + t.Fatal("expected non nil validator") + } + errs, _ := celValidator.Validate(ctx, field.NewPath("root"), &s, tt.obj, RuntimeCELCostBudget) + for _, err := range errs { + t.Errorf("unexpected error: %v", err) + } + + // test context cancellation + found := false + evalCtx, cancel := context.WithTimeout(ctx, time.Microsecond) + cancel() + errs, _ = celValidator.Validate(evalCtx, field.NewPath("root"), &s, tt.obj, RuntimeCELCostBudget) + for _, err := range errs { + if err.Type == field.ErrorTypeInvalid && strings.Contains(err.Error(), "operation interrupted") { + found = true + break + } + } + if !found { + t.Errorf("expect operation interrupted err but did not find") + } + }) + } +} + +func BenchmarkCELValidationWithContext(b *testing.B) { + items := make([]interface{}, 1000) + for i := int64(0); i < 1000; i++ { + items[i] = i + } + tests := []struct { + name string + schema *schema.Structural + obj map[string]interface{} + rule string + }{ + {name: "benchmark for cel validation with context", + obj: map[string]interface{}{ + "array": items, + }, + schema: objectTypePtr(map[string]schema.Structural{ + "array": listType(&integerType), + }), + rule: "self.array.map(e, e * 20).filter(e, e > 50).exists(e, e == 60)", + }, + } + + for _, tt := range tests { + b.Run(tt.name, func(b *testing.B) { + ctx := context.TODO() + s := withRule(*tt.schema, tt.rule) + celValidator := NewValidator(&s, 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, RuntimeCELCostBudget) + for _, err := range errs { + b.Fatalf("validation failed: %v", err) + } + } + }) + } +} + +func BenchmarkCELValidationWithCancelledContext(b *testing.B) { + items := make([]interface{}, 1000) + for i := int64(0); i < 1000; i++ { + items[i] = i + } + tests := []struct { + name string + schema *schema.Structural + obj map[string]interface{} + rule string + }{ + {name: "benchmark for cel validation with context", + obj: map[string]interface{}{ + "array": items, + }, + schema: objectTypePtr(map[string]schema.Structural{ + "array": listType(&integerType), + }), + rule: "self.array.map(e, e * 20).filter(e, e > 50).exists(e, e == 60)", + }, + } + + for _, tt := range tests { + b.Run(tt.name, func(b *testing.B) { + ctx := context.TODO() + s := withRule(*tt.schema, tt.rule) + celValidator := NewValidator(&s, 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, RuntimeCELCostBudget) + //found := false + //for _, err := range errs { + // if err.Type == field.ErrorTypeInvalid && strings.Contains(err.Error(), "operation interrupted") { + // found = true + // break + // } + //} + if len(errs) == 0 { + b.Errorf("expect operation interrupted err but did not find") + } + } + }) + } +} + func primitiveType(typ, format string) schema.Structural { result := schema.Structural{ Generic: schema.Generic{ 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 a181de0ddb4..431708ee14c 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 @@ -17,6 +17,7 @@ limitations under the License. package defaulting import ( + "context" "fmt" "reflect" @@ -33,7 +34,8 @@ import ( ) // ValidateDefaults checks that default values validate and are properly pruned. -func ValidateDefaults(pth *field.Path, s *structuralschema.Structural, isResourceRoot, requirePrunedDefaults bool) (field.ErrorList, error) { +// context is passed for supporting context cancellation during cel validation +func ValidateDefaults(ctx context.Context, pth *field.Path, s *structuralschema.Structural, isResourceRoot, requirePrunedDefaults bool) (field.ErrorList, error) { f := NewRootObjectFunc().WithTypeMeta(metav1.TypeMeta{APIVersion: "validation/v1", Kind: "Validation"}) if isResourceRoot { @@ -47,14 +49,15 @@ func ValidateDefaults(pth *field.Path, s *structuralschema.Structural, isResourc } } - allErr, error, _ := validate(pth, s, s, f, false, requirePrunedDefaults, cel.RuntimeCELCostBudget) + allErr, error, _ := validate(ctx, pth, s, s, f, false, requirePrunedDefaults, cel.RuntimeCELCostBudget) return allErr, error } // validate is the recursive step func for the validation. insideMeta is true if s specifies // TypeMeta or ObjectMeta. The SurroundingObjectFunc f is used to validate defaults of // TypeMeta or ObjectMeta fields. -func validate(pth *field.Path, s *structuralschema.Structural, rootSchema *structuralschema.Structural, f SurroundingObjectFunc, insideMeta, requirePrunedDefaults bool, costBudget int64) (allErrs field.ErrorList, error error, remainingCost int64) { +// context is passed for supporting context cancellation during cel validation +func validate(ctx context.Context, pth *field.Path, s *structuralschema.Structural, rootSchema *structuralschema.Structural, f SurroundingObjectFunc, insideMeta, requirePrunedDefaults bool, costBudget int64) (allErrs field.ErrorList, error error, remainingCost int64) { remainingCost = costBudget if s == nil { return nil, nil, remainingCost @@ -86,7 +89,7 @@ func validate(pth *field.Path, s *structuralschema.Structural, rootSchema *struc } 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, cel.PerCallLimit); celValidator != nil { - celErrs, rmCost := celValidator.Validate(pth.Child("default"), s, s.Default.Object, remainingCost) + celErrs, rmCost := celValidator.Validate(ctx, pth.Child("default"), s, s.Default.Object, remainingCost) remainingCost = rmCost allErrs = append(allErrs, celErrs...) if remainingCost < 0 { @@ -111,7 +114,7 @@ func validate(pth *field.Path, s *structuralschema.Structural, rootSchema *struc } 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, cel.PerCallLimit); celValidator != nil { - celErrs, rmCost := celValidator.Validate(pth.Child("default"), s, s.Default.Object, remainingCost) + celErrs, rmCost := celValidator.Validate(ctx, pth.Child("default"), s, s.Default.Object, remainingCost) remainingCost = rmCost allErrs = append(allErrs, celErrs...) if remainingCost < 0 { @@ -124,7 +127,7 @@ func validate(pth *field.Path, s *structuralschema.Structural, rootSchema *struc // do not follow additionalProperties because defaults are forbidden there if s.Items != nil { - errs, err, rCost := validate(pth.Child("items"), s.Items, rootSchema, f.Index(), insideMeta, requirePrunedDefaults, remainingCost) + errs, err, rCost := validate(ctx, pth.Child("items"), s.Items, rootSchema, f.Index(), insideMeta, requirePrunedDefaults, remainingCost) remainingCost = rCost allErrs = append(allErrs, errs...) if err != nil { @@ -140,7 +143,7 @@ func validate(pth *field.Path, s *structuralschema.Structural, rootSchema *struc if s.XEmbeddedResource && (k == "metadata" || k == "apiVersion" || k == "kind") { subInsideMeta = true } - errs, err, rCost := validate(pth.Child("properties").Key(k), &subSchema, rootSchema, f.Child(k), subInsideMeta, requirePrunedDefaults, remainingCost) + errs, err, rCost := validate(ctx, pth.Child("properties").Key(k), &subSchema, rootSchema, f.Child(k), subInsideMeta, requirePrunedDefaults, remainingCost) remainingCost = rCost allErrs = append(allErrs, errs...) if err != nil { diff --git a/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema/defaulting/validation_test.go b/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema/defaulting/validation_test.go index 3f29600f1ae..b3cb4d9cb19 100644 --- a/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema/defaulting/validation_test.go +++ b/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema/defaulting/validation_test.go @@ -17,6 +17,7 @@ limitations under the License. package defaulting import ( + "context" "strings" "testing" @@ -95,6 +96,7 @@ func TestDefaultValidationWithCostBudget(t *testing.T) { } for _, tt := range tests { + ctx := context.TODO() t.Run(tt.name, func(t *testing.T) { schema := tt.input.OpenAPIV3Schema ss, err := structuralschema.NewStructural(schema) @@ -105,7 +107,7 @@ func TestDefaultValidationWithCostBudget(t *testing.T) { f := NewRootObjectFunc().WithTypeMeta(metav1.TypeMeta{APIVersion: "validation/v1", Kind: "Validation"}) // cost budget is large enough to pass all validation rules - allErrs, err, _ := validate(field.NewPath("test"), ss, ss, f, false, false, 10) + allErrs, err, _ := validate(ctx, field.NewPath("test"), ss, ss, f, false, false, 10) if err != nil { t.Errorf("unexpected error: %v", err) } @@ -115,7 +117,7 @@ func TestDefaultValidationWithCostBudget(t *testing.T) { } // cost budget exceeded for the first validation rule - allErrs, err, _ = validate(field.NewPath("test"), ss, ss, f, false, false, 0) + allErrs, err, _ = validate(ctx, field.NewPath("test"), ss, ss, f, false, false, 0) meet := 0 for _, er := range allErrs { if er.Type == field.ErrorTypeInvalid && strings.Contains(er.Error(), "validation failed due to running out of cost budget, no further validation rules will be run") { @@ -130,7 +132,7 @@ func TestDefaultValidationWithCostBudget(t *testing.T) { } // cost budget exceeded for the last validation rule - allErrs, err, _ = validate(field.NewPath("test"), ss, ss, f, false, false, 9) + allErrs, err, _ = validate(ctx, field.NewPath("test"), ss, ss, f, false, false, 9) meet = 0 for _, er := range allErrs { if er.Type == field.ErrorTypeInvalid && strings.Contains(er.Error(), "validation failed due to running out of cost budget, no further validation rules will be run") { 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 0826212a8b1..d30c2ecd222 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 @@ -91,7 +91,7 @@ func (a statusStrategy) ValidateUpdate(ctx context.Context, obj, old runtime.Obj // validate x-kubernetes-validations rules if celValidator, ok := a.customResourceStrategy.celValidators[v]; ok { - err, _ := celValidator.Validate(nil, a.customResourceStrategy.structuralSchemas[v], u.Object, cel.RuntimeCELCostBudget) + err, _ := celValidator.Validate(ctx, nil, a.customResourceStrategy.structuralSchemas[v], u.Object, cel.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 4de94269dee..c59a6934fcc 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 @@ -174,7 +174,7 @@ func (a customResourceStrategy) Validate(ctx context.Context, obj runtime.Object // validate x-kubernetes-validations rules if celValidator, ok := a.celValidators[v]; ok { - err, _ := celValidator.Validate(nil, a.structuralSchemas[v], u.Object, cel.RuntimeCELCostBudget) + err, _ := celValidator.Validate(ctx, nil, a.structuralSchemas[v], u.Object, cel.RuntimeCELCostBudget) errs = append(errs, err...) } } @@ -227,7 +227,7 @@ func (a customResourceStrategy) ValidateUpdate(ctx context.Context, obj, old run // validate x-kubernetes-validations rules if celValidator, ok := a.celValidators[v]; ok { - err, _ := celValidator.Validate(nil, a.structuralSchemas[v], uNew.Object, cel.RuntimeCELCostBudget) + err, _ := celValidator.Validate(ctx, nil, a.structuralSchemas[v], uNew.Object, cel.RuntimeCELCostBudget) errs = append(errs, err...) } diff --git a/staging/src/k8s.io/apiextensions-apiserver/pkg/registry/customresourcedefinition/strategy.go b/staging/src/k8s.io/apiextensions-apiserver/pkg/registry/customresourcedefinition/strategy.go index 421c2119f60..391a923f6c3 100644 --- a/staging/src/k8s.io/apiextensions-apiserver/pkg/registry/customresourcedefinition/strategy.go +++ b/staging/src/k8s.io/apiextensions-apiserver/pkg/registry/customresourcedefinition/strategy.go @@ -115,7 +115,7 @@ func (strategy) PrepareForUpdate(ctx context.Context, obj, old runtime.Object) { // Validate validates a new CustomResourceDefinition. func (strategy) Validate(ctx context.Context, obj runtime.Object) field.ErrorList { - return validation.ValidateCustomResourceDefinition(obj.(*apiextensions.CustomResourceDefinition)) + return validation.ValidateCustomResourceDefinition(ctx, obj.(*apiextensions.CustomResourceDefinition)) } // WarningsOnCreate returns warnings for the creation of the given object. @@ -138,7 +138,7 @@ func (strategy) Canonicalize(obj runtime.Object) { // ValidateUpdate is the default update validation for an end user updating status. func (strategy) ValidateUpdate(ctx context.Context, obj, old runtime.Object) field.ErrorList { - return validation.ValidateCustomResourceDefinitionUpdate(obj.(*apiextensions.CustomResourceDefinition), old.(*apiextensions.CustomResourceDefinition)) + return validation.ValidateCustomResourceDefinitionUpdate(ctx, obj.(*apiextensions.CustomResourceDefinition), old.(*apiextensions.CustomResourceDefinition)) } // WarningsOnUpdate returns warnings for the given update. diff --git a/staging/src/k8s.io/apiextensions-apiserver/pkg/registry/customresourcedefinition/strategy_test.go b/staging/src/k8s.io/apiextensions-apiserver/pkg/registry/customresourcedefinition/strategy_test.go index bb9b84c5d20..2b981ddc809 100644 --- a/staging/src/k8s.io/apiextensions-apiserver/pkg/registry/customresourcedefinition/strategy_test.go +++ b/staging/src/k8s.io/apiextensions-apiserver/pkg/registry/customresourcedefinition/strategy_test.go @@ -17,6 +17,7 @@ limitations under the License. package customresourcedefinition import ( + "context" "testing" "github.com/google/go-cmp/cmp" @@ -182,10 +183,11 @@ func TestValidateAPIApproval(t *testing.T) { } var actual field.ErrorList + ctx := context.TODO() if oldCRD == nil { - actual = validation.ValidateCustomResourceDefinition(crd) + actual = validation.ValidateCustomResourceDefinition(ctx, crd) } else { - actual = validation.ValidateCustomResourceDefinitionUpdate(crd, oldCRD) + actual = validation.ValidateCustomResourceDefinitionUpdate(ctx, crd, oldCRD) } test.validateError(t, actual) })