| @@ -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 { | ||||
|   | ||||
| @@ -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)) | ||||
|  | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
| @@ -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{ | ||||
|   | ||||
| @@ -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 { | ||||
|   | ||||
| @@ -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") { | ||||
|   | ||||
| @@ -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...) | ||||
| 		} | ||||
| 	} | ||||
|   | ||||
| @@ -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...) | ||||
| 	} | ||||
|  | ||||
|   | ||||
| @@ -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. | ||||
|   | ||||
| @@ -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) | ||||
| 		}) | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 Kubernetes Prow Robot
					Kubernetes Prow Robot