mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-07-24 04:06:03 +00:00
Wire in request context
This commit is contained in:
parent
52ab081283
commit
b3851d4115
@ -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)
|
||||
})
|
||||
|
Loading…
Reference in New Issue
Block a user