Wire in request context

This commit is contained in:
cici37 2022-03-10 13:49:01 -08:00
parent 52ab081283
commit b3851d4115
11 changed files with 220 additions and 52 deletions

View File

@ -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 {

View File

@ -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))

View File

@ -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

View File

@ -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

View File

@ -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{

View File

@ -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 {

View File

@ -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") {

View File

@ -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...)
}
}

View File

@ -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...)
}

View File

@ -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.

View File

@ -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)
})