diff --git a/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema/cel/validation.go b/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema/cel/validation.go index d9b595805ba..a0341e75195 100644 --- a/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema/cel/validation.go +++ b/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema/cel/validation.go @@ -51,12 +51,15 @@ import ( // Validator parallels the structure of schema.Structural and includes the compiled CEL programs // for the x-kubernetes-validations of each schema node. type Validator struct { - Items *Validator - Properties map[string]Validator - + Items *Validator + Properties map[string]Validator + AllOfValidators []*Validator AdditionalProperties *Validator - compiledRules []CompilationResult + Schema *schema.Structural + + uncompiledRules []apiextensions.ValidationRule + compiledRules []CompilationResult // Program compilation is pre-checked at CRD creation/update time, so we don't expect compilation to fail // they are recompiled and added to this type, and it does, it is an internal bug. @@ -82,25 +85,37 @@ func NewValidator(s *schema.Structural, isResourceRoot bool, perCallLimit uint64 if !hasXValidations(s) { return nil } - return validator(s, isResourceRoot, model.SchemaDeclType(s, isResourceRoot), perCallLimit) + return validator(s, s, isResourceRoot, model.SchemaDeclType(s, isResourceRoot), perCallLimit) } // validator creates a Validator for all x-kubernetes-validations at the level of the provided schema and lower and // returns the Validator if any x-kubernetes-validations exist in the schema, or nil if no x-kubernetes-validations // exist. declType is expected to be a CEL DeclType corresponding to the structural schema. // perCallLimit was added for testing purpose only. Callers should always use const PerCallLimit from k8s.io/apiserver/pkg/apis/cel/config.go as input. -func validator(s *schema.Structural, isResourceRoot bool, declType *cel.DeclType, perCallLimit uint64) *Validator { - // strictCost is always true to enforce cost limits. - compiledRules, err := Compile(s, declType, perCallLimit, environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion(), true), StoredExpressionsEnvLoader()) +func validator(validationSchema, nodeSchema *schema.Structural, isResourceRoot bool, declType *cel.DeclType, perCallLimit uint64) *Validator { + + compilationSchema := *nodeSchema + compilationSchema.XValidations = validationSchema.XValidations + compiledRules, err := Compile(&compilationSchema, declType, perCallLimit, environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion(), true), StoredExpressionsEnvLoader()) + var itemsValidator, additionalPropertiesValidator *Validator var propertiesValidators map[string]Validator - if s.Items != nil { - itemsValidator = validator(s.Items, s.Items.XEmbeddedResource, declType.ElemType, perCallLimit) + var allOfValidators []*Validator + + if validationSchema.Items != nil && nodeSchema.Items != nil { + itemsValidator = validator(validationSchema.Items, nodeSchema.Items, nodeSchema.Items.XEmbeddedResource, declType.ElemType, perCallLimit) } - if len(s.Properties) > 0 { - propertiesValidators = make(map[string]Validator, len(s.Properties)) - for k, p := range s.Properties { - prop := p + + if len(validationSchema.Properties) > 0 { + propertiesValidators = make(map[string]Validator, len(validationSchema.Properties)) + for k, validationProperty := range validationSchema.Properties { + nodeProperty, ok := nodeSchema.Properties[k] + if !ok { + // Can only add value validations for fields that are on the + // structural spine of the schema. + continue + } + var fieldType *cel.DeclType if escapedPropName, ok := cel.Escape(k); ok { if f, ok := declType.Fields[escapedPropName]; ok { @@ -112,20 +127,32 @@ func validator(s *schema.Structural, isResourceRoot bool, declType *cel.DeclType } else { // field may be absent from declType if the property name is unescapable, in which case we should convert // the field value type to a DeclType. - fieldType = model.SchemaDeclType(&prop, prop.XEmbeddedResource) + fieldType = model.SchemaDeclType(&nodeProperty, nodeProperty.XEmbeddedResource) if fieldType == nil { continue } } - if p := validator(&prop, prop.XEmbeddedResource, fieldType, perCallLimit); p != nil { + if p := validator(&validationProperty, &nodeProperty, nodeProperty.XEmbeddedResource, fieldType, perCallLimit); p != nil { propertiesValidators[k] = *p } } } - if s.AdditionalProperties != nil && s.AdditionalProperties.Structural != nil { - additionalPropertiesValidator = validator(s.AdditionalProperties.Structural, s.AdditionalProperties.Structural.XEmbeddedResource, declType.ElemType, perCallLimit) + if validationSchema.AdditionalProperties != nil && validationSchema.AdditionalProperties.Structural != nil && + nodeSchema.AdditionalProperties != nil && nodeSchema.AdditionalProperties.Structural != nil { + additionalPropertiesValidator = validator(validationSchema.AdditionalProperties.Structural, nodeSchema.AdditionalProperties.Structural, nodeSchema.AdditionalProperties.Structural.XEmbeddedResource, declType.ElemType, perCallLimit) } - if len(compiledRules) > 0 || err != nil || itemsValidator != nil || additionalPropertiesValidator != nil || len(propertiesValidators) > 0 { + + if validationSchema.ValueValidation != nil && len(validationSchema.ValueValidation.AllOf) > 0 { + allOfValidators = make([]*Validator, 0, len(validationSchema.ValueValidation.AllOf)) + for _, allOf := range validationSchema.ValueValidation.AllOf { + allOfValidator := validator(nestedToStructural(&allOf), nodeSchema, isResourceRoot, declType, perCallLimit) + if allOfValidator != nil { + allOfValidators = append(allOfValidators, allOfValidator) + } + } + } + + if len(compiledRules) > 0 || err != nil || itemsValidator != nil || additionalPropertiesValidator != nil || len(propertiesValidators) > 0 || len(allOfValidators) > 0 { activationFactory := validationActivationWithoutOldSelf for _, rule := range compiledRules { if rule.UsesOldSelf { @@ -136,12 +163,15 @@ func validator(s *schema.Structural, isResourceRoot bool, declType *cel.DeclType return &Validator{ compiledRules: compiledRules, + uncompiledRules: validationSchema.XValidations, compilationErr: err, isResourceRoot: isResourceRoot, Items: itemsValidator, AdditionalProperties: additionalPropertiesValidator, Properties: propertiesValidators, + AllOfValidators: allOfValidators, celActivationFactory: activationFactory, + Schema: nodeSchema, } } @@ -164,13 +194,13 @@ func WithRatcheting(correlation *common.CorrelatedObject) Option { // 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 // context is passed for supporting context cancellation during cel validation -func (s *Validator) Validate(ctx context.Context, fldPath *field.Path, sts *schema.Structural, obj, oldObj interface{}, costBudget int64, opts ...Option) (errs field.ErrorList, remainingBudget int64) { +func (s *Validator) Validate(ctx context.Context, fldPath *field.Path, _ *schema.Structural, obj, oldObj interface{}, costBudget int64, opts ...Option) (errs field.ErrorList, remainingBudget int64) { opt := options{} for _, o := range opts { o(&opt) } - return s.validate(ctx, fldPath, sts, obj, oldObj, opt.ratchetingOptions, costBudget) + return s.validate(ctx, fldPath, obj, oldObj, opt.ratchetingOptions, costBudget) } // ratchetingOptions stores the current correlation object and the nearest @@ -234,7 +264,37 @@ func (r ratchetingOptions) index(idx int) ratchetingOptions { return ratchetingOptions{currentCorrelation: r.currentCorrelation.Index(idx), nearestParentCorrelation: r.currentCorrelation} } -func (s *Validator) validate(ctx context.Context, fldPath *field.Path, sts *schema.Structural, obj, oldObj interface{}, correlation ratchetingOptions, costBudget int64) (errs field.ErrorList, remainingBudget int64) { +func nestedToStructural(nested *schema.NestedValueValidation) *schema.Structural { + if nested == nil { + return nil + } + + structuralConversion := &schema.Structural{ + ValueValidation: &nested.ValueValidation, + ValidationExtensions: nested.ValidationExtensions, + Generic: nested.ForbiddenGenerics, + Extensions: nested.ForbiddenExtensions, + Items: nestedToStructural(nested.Items), + } + + if len(nested.Properties) > 0 { + structuralConversion.Properties = make(map[string]schema.Structural, len(nested.Properties)) + for k, v := range nested.Properties { + structuralConversion.Properties[k] = *nestedToStructural(&v) + } + } + + if nested.AdditionalProperties != nil { + structuralConversion.AdditionalProperties = &schema.StructuralOrBool{ + Structural: nestedToStructural(nested.AdditionalProperties), + } + } + + return structuralConversion + +} + +func (s *Validator) validate(ctx context.Context, fldPath *field.Path, obj, oldObj interface{}, correlation ratchetingOptions, costBudget int64) (errs field.ErrorList, remainingBudget int64) { t := time.Now() defer func() { metrics.Metrics.ObserveEvaluation(time.Since(t)) @@ -244,23 +304,37 @@ func (s *Validator) validate(ctx context.Context, fldPath *field.Path, sts *sche return nil, remainingBudget } - errs, remainingBudget = s.validateExpressions(ctx, fldPath, sts, obj, oldObj, correlation, remainingBudget) + errs, remainingBudget = s.validateExpressions(ctx, fldPath, obj, oldObj, correlation, remainingBudget) if remainingBudget < 0 { return errs, remainingBudget } + // If the schema has allOf, recurse through those elements to see if there + // are any validation rules that need to be evaluated. + for _, allOfValidator := range s.AllOfValidators { + var allOfErrs field.ErrorList + // Pass options with nil currentCorrelation to mirror schema ratcheting + // behavior which does not ratchet allOf errors. This may change in the + // future for allOf. + allOfErrs, remainingBudget = allOfValidator.validate(ctx, fldPath, obj, oldObj, ratchetingOptions{nearestParentCorrelation: correlation.nearestParentCorrelation}, remainingBudget) + errs = append(errs, allOfErrs...) + if remainingBudget < 0 { + return errs, remainingBudget + } + } + switch obj := obj.(type) { case []interface{}: oldArray, _ := oldObj.([]interface{}) var arrayErrs field.ErrorList - arrayErrs, remainingBudget = s.validateArray(ctx, fldPath, sts, obj, oldArray, correlation, remainingBudget) + arrayErrs, remainingBudget = s.validateArray(ctx, fldPath, obj, oldArray, correlation, remainingBudget) errs = append(errs, arrayErrs...) return errs, remainingBudget case map[string]interface{}: oldMap, _ := oldObj.(map[string]interface{}) var mapErrs field.ErrorList - mapErrs, remainingBudget = s.validateMap(ctx, fldPath, sts, obj, oldMap, correlation, remainingBudget) + mapErrs, remainingBudget = s.validateMap(ctx, fldPath, obj, oldMap, correlation, remainingBudget) errs = append(errs, mapErrs...) return errs, remainingBudget } @@ -268,7 +342,9 @@ func (s *Validator) validate(ctx context.Context, fldPath *field.Path, sts *sche return errs, remainingBudget } -func (s *Validator) validateExpressions(ctx context.Context, fldPath *field.Path, sts *schema.Structural, obj, oldObj interface{}, correlation ratchetingOptions, costBudget int64) (errs field.ErrorList, remainingBudget int64) { +func (s *Validator) validateExpressions(ctx context.Context, fldPath *field.Path, obj, oldObj interface{}, correlation ratchetingOptions, costBudget int64) (errs field.ErrorList, remainingBudget int64) { + sts := s.Schema + // guard against oldObj being a non-nil interface with a nil value if oldObj != nil { v := reflect.ValueOf(oldObj) @@ -303,7 +379,7 @@ func (s *Validator) validateExpressions(ctx context.Context, fldPath *field.Path } activation, optionalOldSelfActivation := s.celActivationFactory(sts, obj, oldObj) for i, compiled := range s.compiledRules { - rule := sts.XValidations[i] + rule := s.uncompiledRules[i] if compiled.Error != nil { errs = append(errs, field.Invalid(fldPath, sts.Type, fmt.Sprintf("rule compile error: %v", compiled.Error))) continue @@ -720,7 +796,7 @@ func (a *validationActivation) Parent() interpreter.Activation { return nil } -func (s *Validator) validateMap(ctx context.Context, fldPath *field.Path, sts *schema.Structural, obj, oldObj map[string]interface{}, correlation ratchetingOptions, costBudget int64) (errs field.ErrorList, remainingBudget int64) { +func (s *Validator) validateMap(ctx context.Context, fldPath *field.Path, obj, oldObj map[string]interface{}, correlation ratchetingOptions, costBudget int64) (errs field.ErrorList, remainingBudget int64) { remainingBudget = costBudget if remainingBudget < 0 { return errs, remainingBudget @@ -729,9 +805,9 @@ func (s *Validator) validateMap(ctx context.Context, fldPath *field.Path, sts *s return nil, remainingBudget } - correlatable := MapIsCorrelatable(sts.XMapType) + correlatable := MapIsCorrelatable(s.Schema.XMapType) - if s.AdditionalProperties != nil && sts.AdditionalProperties != nil && sts.AdditionalProperties.Structural != nil { + if s.AdditionalProperties != nil { for k, v := range obj { var oldV interface{} if correlatable { @@ -739,25 +815,24 @@ func (s *Validator) validateMap(ctx context.Context, fldPath *field.Path, sts *s } var err field.ErrorList - err, remainingBudget = s.AdditionalProperties.validate(ctx, fldPath.Key(k), sts.AdditionalProperties.Structural, v, oldV, correlation.key(k), remainingBudget) + err, remainingBudget = s.AdditionalProperties.validate(ctx, fldPath.Key(k), v, oldV, correlation.key(k), remainingBudget) errs = append(errs, err...) if remainingBudget < 0 { return errs, remainingBudget } } } - if s.Properties != nil && sts.Properties != nil { + if s.Properties != nil { for k, v := range obj { - stsProp, stsOk := sts.Properties[k] sub, ok := s.Properties[k] - if ok && stsOk { + if ok { var oldV interface{} if correlatable { oldV = oldObj[k] // +k8s:verify-mutation:reason=clone } var err field.ErrorList - err, remainingBudget = sub.validate(ctx, fldPath.Child(k), &stsProp, v, oldV, correlation.key(k), remainingBudget) + err, remainingBudget = sub.validate(ctx, fldPath.Child(k), v, oldV, correlation.key(k), remainingBudget) errs = append(errs, err...) if remainingBudget < 0 { return errs, remainingBudget @@ -769,19 +844,19 @@ func (s *Validator) validateMap(ctx context.Context, fldPath *field.Path, sts *s return errs, remainingBudget } -func (s *Validator) validateArray(ctx context.Context, fldPath *field.Path, sts *schema.Structural, obj, oldObj []interface{}, correlation ratchetingOptions, costBudget int64) (errs field.ErrorList, remainingBudget int64) { +func (s *Validator) validateArray(ctx context.Context, fldPath *field.Path, obj, oldObj []interface{}, correlation ratchetingOptions, costBudget int64) (errs field.ErrorList, remainingBudget int64) { remainingBudget = costBudget if remainingBudget < 0 { return errs, remainingBudget } - if s.Items != nil && sts.Items != nil { + if s.Items != nil { // only map-type lists support self-oldSelf correlation for cel rules. if this isn't a // map-type list, then makeMapList returns an implementation that always returns nil - correlatableOldItems := makeMapList(sts, oldObj) + correlatableOldItems := makeMapList(s.Schema, oldObj) for i := range obj { var err field.ErrorList - err, remainingBudget = s.Items.validate(ctx, fldPath.Index(i), sts.Items, obj[i], correlatableOldItems.Get(obj[i]), correlation.index(i), remainingBudget) + err, remainingBudget = s.Items.validate(ctx, fldPath.Index(i), obj[i], correlatableOldItems.Get(obj[i]), correlation.index(i), remainingBudget) errs = append(errs, err...) if remainingBudget < 0 { return errs, remainingBudget diff --git a/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema/cel/validation_test.go b/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema/cel/validation_test.go index 4c304a72843..b429905bf59 100644 --- a/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema/cel/validation_test.go +++ b/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema/cel/validation_test.go @@ -2059,7 +2059,7 @@ func TestValidationExpressions(t *testing.T) { t.Run(testName, func(t *testing.T) { t.Parallel() s := withRule(*tt.schema, validRule) - celValidator := validator(&s, tt.isRoot, model.SchemaDeclType(&s, tt.isRoot), celconfig.PerCallLimit) + celValidator := validator(&s, &s, tt.isRoot, model.SchemaDeclType(&s, tt.isRoot), celconfig.PerCallLimit) if celValidator == nil { t.Fatal("expected non nil validator") } @@ -2297,6 +2297,470 @@ func TestValidationExpressionsAtSchemaLevels(t *testing.T) { schema: genMatchSelectorSchema(`self.matchExpressions.all(rule, size(rule.key) <= 63 && rule.key.matches("^(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])?$"))`), errors: []string{"failed rule"}, }, + { + name: "allOf rule", + errors: []string{ + `key must be 'value' and key2 must be 'value2'`, + `key must not be equal to key2`, + }, + obj: map[string]interface{}{ + "key": "value", + "key2": "value", + }, + schema: &schema.Structural{ + Generic: schema.Generic{ + Type: "object", + }, + Properties: map[string]schema.Structural{ + "key": stringType, + "key2": stringType, + }, + ValueValidation: &schema.ValueValidation{ + AllOf: []schema.NestedValueValidation{ + { + ValidationExtensions: schema.ValidationExtensions{ + XValidations: []apiextensions.ValidationRule{ + { + Rule: "self.key == 'value' && self.key2 == 'value2'", + Message: "key must be 'value' and key2 must be 'value2'", + }, + }, + }, + }, + { + ValidationExtensions: schema.ValidationExtensions{ + XValidations: []apiextensions.ValidationRule{ + { + Rule: "self.key != self.key2", + Message: "key must not be equal to key2", + }, + }, + }, + }, + }, + }, + }, + }, + { + // anyOf is not suppored due to the fact that to properly implement it + // the SchemaValidator would need to call out to CEL. This test + // shows that a rule that would otherwise be an error has those + // fields ignored (CRD validation shoulds will throw error in this case) + name: "anyOf rule ignored", + obj: map[string]interface{}{ + "key": "value", + "key2": "value", + }, + schema: &schema.Structural{ + Generic: schema.Generic{ + Type: "object", + }, + Properties: map[string]schema.Structural{ + "key": stringType, + "key2": stringType, + }, + ValidationExtensions: schema.ValidationExtensions{ + XValidations: []apiextensions.ValidationRule{ + { + Rule: "true", + }, + }, + }, + ValueValidation: &schema.ValueValidation{ + AnyOf: []schema.NestedValueValidation{ + { + ValidationExtensions: schema.ValidationExtensions{ + XValidations: []apiextensions.ValidationRule{ + { + Rule: "self.key == 'value' && self.key2 == 'value2'", + Message: "key must be 'value' and key2 must be 'value2'", + }, + }, + }, + }, + { + ValidationExtensions: schema.ValidationExtensions{ + XValidations: []apiextensions.ValidationRule{ + { + Rule: "self.key != self.key2", + Message: "key must not be equal to key2", + }, + }, + }, + }, + }, + }, + }, + }, + { + // OneOf is not suppored due to the fact that to properly implement it + // the SchemaValidator would need to call out to CEL. This test + // shows that a rule that would otherwise be an error has those + // fields ignored (CRD validation shoulds will throw error in this case) + name: "oneOf rule ignored", + obj: map[string]interface{}{ + "key": "value", + "key2": "value", + }, + schema: &schema.Structural{ + Generic: schema.Generic{ + Type: "object", + }, + Properties: map[string]schema.Structural{ + "key": stringType, + "key2": stringType, + }, + ValidationExtensions: schema.ValidationExtensions{ + XValidations: []apiextensions.ValidationRule{ + { + Rule: "true", + }, + }, + }, + ValueValidation: &schema.ValueValidation{ + OneOf: []schema.NestedValueValidation{ + { + ValidationExtensions: schema.ValidationExtensions{ + XValidations: []apiextensions.ValidationRule{ + { + Rule: "self.key == 'value' && self.key2 == 'value2'", + Message: "key must be 'value' and key2 must be 'value2'", + }, + }, + }, + }, + { + ValidationExtensions: schema.ValidationExtensions{ + XValidations: []apiextensions.ValidationRule{ + { + Rule: "self.key != self.key2", + Message: "key must not be equal to key2", + }, + }, + }, + }, + }, + }, + }, + }, + { + // Not is not suppored due to the fact that to properly implement it + // the SchemaValidator would need to call out to CEL. This test + // shows that a rule that would otherwise be an error has those + // fields ignored (CRD validation shoulds will throw error in this case) + name: "not rule ignored", + obj: map[string]interface{}{ + "key": "value", + "key2": "value", + }, + schema: &schema.Structural{ + Generic: schema.Generic{ + Type: "object", + }, + Properties: map[string]schema.Structural{ + "key": stringType, + "key2": stringType, + }, + ValidationExtensions: schema.ValidationExtensions{ + XValidations: []apiextensions.ValidationRule{ + { + Rule: "true", + }, + }, + }, + ValueValidation: &schema.ValueValidation{ + Not: &schema.NestedValueValidation{ + ValidationExtensions: schema.ValidationExtensions{ + XValidations: []apiextensions.ValidationRule{ + { + Rule: "self.key == 'value' && self.key2 == 'value2'", + Message: "key must be 'value' and key2 must be 'value2'", + }, + }, + }, + }, + }, + }, + }, + { + name: "allOf.items", + obj: map[string]interface{}{ + "myList": []interface{}{"value", "value2"}, + }, + errors: []string{ + `must be value2 or not value`, + `len must be 5`, + }, + schema: &schema.Structural{ + Generic: schema.Generic{ + Type: "object", + }, + Properties: map[string]schema.Structural{ + "myList": { + Generic: schema.Generic{ + Type: "array", + }, + Items: &stringType, + ValueValidation: &schema.ValueValidation{ + AllOf: []schema.NestedValueValidation{ + { + Items: &schema.NestedValueValidation{ + ValidationExtensions: schema.ValidationExtensions{ + XValidations: []apiextensions.ValidationRule{ + { + Rule: "self.size() == 5", + Message: "len must be 5", + }, + }, + }, + }, + }, + { + Items: &schema.NestedValueValidation{ + ValidationExtensions: schema.ValidationExtensions{ + XValidations: []apiextensions.ValidationRule{ + { + Rule: `self == "value2" || self != "value"`, + Message: "must be value2 or not value", + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + { + name: "allOf.additionalProperties", + obj: map[string]interface{}{ + "myProperty": map[string]interface{}{ + "key": "value", + "key2": "value2", + }, + }, + errors: []string{ + `root.myProperty[key]: Invalid value: "string": must be value2 or not value`, + `root.myProperty[key2]: Invalid value: "string": len must be 5`, + }, + schema: &schema.Structural{ + Generic: schema.Generic{ + Type: "object", + }, + // need to put all tests in Properties due to WithTypeAndObjectMeta + // enforcing structs for all top level schemas + Properties: map[string]schema.Structural{ + "myProperty": { + Generic: schema.Generic{ + Type: "object", + }, + AdditionalProperties: &schema.StructuralOrBool{Structural: &stringType}, + ValueValidation: &schema.ValueValidation{ + AllOf: []schema.NestedValueValidation{ + { + AdditionalProperties: &schema.NestedValueValidation{ + ValidationExtensions: schema.ValidationExtensions{ + XValidations: []apiextensions.ValidationRule{ + { + Rule: `self == "value2" || self != "value"`, + Message: "must be value2 or not value", + }, + { + Rule: "self.size() == 5", + Message: "len must be 5", + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + { + name: "allOf.properties", + obj: map[string]interface{}{ + "key": "value", + "key2": "value2", + }, + errors: []string{ + `root.key: Invalid value: "string": must be value2 or not value`, + `root.key2: Invalid value: "string": len must be 5`, + }, + schema: &schema.Structural{ + Generic: schema.Generic{ + Type: "object", + }, + Properties: map[string]schema.Structural{ + "key": stringType, + "key2": stringType, + }, + ValueValidation: &schema.ValueValidation{ + AllOf: []schema.NestedValueValidation{ + { + Properties: map[string]schema.NestedValueValidation{ + "key": { + ValidationExtensions: schema.ValidationExtensions{ + XValidations: []apiextensions.ValidationRule{ + { + Rule: `self == "value2" || self != "value"`, + Message: "must be value2 or not value", + }, + }, + }, + }, + "key2": { + ValidationExtensions: schema.ValidationExtensions{ + XValidations: []apiextensions.ValidationRule{ + { + Rule: "self.size() == 5", + Message: "len must be 5", + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + { + name: "allOf.items.allOf", + obj: map[string]interface{}{"myList": []interface{}{"value", "value2"}}, + errors: []string{ + `must be value2 or not value`, + `len must be 5`, + }, + schema: &schema.Structural{ + Generic: schema.Generic{ + Type: "object", + }, + Properties: map[string]schema.Structural{ + "myList": { + Generic: schema.Generic{ + Type: "array", + }, + Items: &stringType, + ValueValidation: &schema.ValueValidation{ + AllOf: []schema.NestedValueValidation{ + { + Items: &schema.NestedValueValidation{ + ValueValidation: schema.ValueValidation{ + AllOf: []schema.NestedValueValidation{ + { + ValidationExtensions: schema.ValidationExtensions{ + XValidations: []apiextensions.ValidationRule{ + { + Rule: "self.size() == 5", + Message: "len must be 5", + }, + }, + }, + }, + }, + }, + }, + }, + { + Items: &schema.NestedValueValidation{ + ValueValidation: schema.ValueValidation{ + AllOf: []schema.NestedValueValidation{ + { + ValidationExtensions: schema.ValidationExtensions{ + XValidations: []apiextensions.ValidationRule{ + { + Rule: `self == "value2" || self != "value"`, + Message: "must be value2 or not value", + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + { + name: "properties.allOf.additionalProperties.allOf.properties", + obj: map[string]interface{}{ + "myProperty": map[string]interface{}{ + "randomKey": map[string]interface{}{ + "key": "value", + "key2": "value2", + }, + }, + }, + errors: []string{ + `must be value2 or not value`, + `len must be 5`, + }, + schema: &schema.Structural{ + Generic: schema.Generic{ + Type: "object", + }, + Properties: map[string]schema.Structural{ + "myProperty": { + Generic: schema.Generic{ + Type: "object", + }, + AdditionalProperties: &schema.StructuralOrBool{Structural: objectTypePtr(map[string]schema.Structural{ + "key": stringType, + "key2": stringType, + })}, + }, + }, + ValueValidation: &schema.ValueValidation{ + AllOf: []schema.NestedValueValidation{ + { + Properties: map[string]schema.NestedValueValidation{ + "myProperty": { + AdditionalProperties: &schema.NestedValueValidation{ + ValueValidation: schema.ValueValidation{ + AllOf: []schema.NestedValueValidation{ + { + Properties: map[string]schema.NestedValueValidation{ + "key": { + ValidationExtensions: schema.ValidationExtensions{ + XValidations: []apiextensions.ValidationRule{ + { + Rule: `self == "value2" || self != "value"`, + Message: "must be value2 or not value", + }, + }, + }, + }, + "key2": { + ValidationExtensions: schema.ValidationExtensions{ + XValidations: []apiextensions.ValidationRule{ + { + Rule: "self.size() == 5", + Message: "len must be 5", + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, } for _, tt := range tests { @@ -2304,7 +2768,7 @@ func TestValidationExpressionsAtSchemaLevels(t *testing.T) { t.Run(tt.name, func(t *testing.T) { t.Parallel() ctx := context.TODO() - celValidator := validator(tt.schema, true, model.SchemaDeclType(tt.schema, true), celconfig.PerCallLimit) + celValidator := validator(tt.schema, tt.schema, true, model.SchemaDeclType(tt.schema, true), celconfig.PerCallLimit) if celValidator == nil { t.Fatal("expected non nil validator") } @@ -2371,7 +2835,7 @@ func TestCELValidationLimit(t *testing.T) { t.Run(validRule, func(t *testing.T) { t.Parallel() s := withRule(*tt.schema, validRule) - celValidator := validator(&s, false, model.SchemaDeclType(&s, false), celconfig.PerCallLimit) + celValidator := validator(&s, &s, false, model.SchemaDeclType(&s, false), celconfig.PerCallLimit) // test with cost budget exceeded errs, _ := celValidator.Validate(ctx, field.NewPath("root"), &s, tt.obj, nil, 0) @@ -2505,7 +2969,7 @@ func TestCELMaxRecursionDepth(t *testing.T) { t.Run(testName, func(t *testing.T) { t.Parallel() s := withRule(*tt.schema, validRule) - celValidator := validator(&s, tt.isRoot, model.SchemaDeclType(&s, tt.isRoot), celconfig.PerCallLimit) + celValidator := validator(&s, &s, tt.isRoot, model.SchemaDeclType(&s, tt.isRoot), celconfig.PerCallLimit) if celValidator == nil { t.Fatal("expected non nil validator") } @@ -2826,7 +3290,7 @@ func TestReasonAndFldPath(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { ctx := context.TODO() - celValidator := validator(tt.schema, true, model.SchemaDeclType(tt.schema, true), celconfig.PerCallLimit) + celValidator := validator(tt.schema, tt.schema, true, model.SchemaDeclType(tt.schema, true), celconfig.PerCallLimit) if celValidator == nil { t.Fatal("expected non nil validator") } @@ -3985,7 +4449,7 @@ func TestOptionalOldSelf(t *testing.T) { // t.Parallel() ctx := context.TODO() - celValidator := validator(tt.schema, true, model.SchemaDeclType(tt.schema, false), celconfig.PerCallLimit) + celValidator := validator(tt.schema, tt.schema, true, model.SchemaDeclType(tt.schema, false), celconfig.PerCallLimit) if celValidator == nil { t.Fatal("expected non nil validator") } @@ -4139,7 +4603,7 @@ func TestOptionalOldSelfCheckForNull(t *testing.T) { t.Run(tt.name, func(t *testing.T) { ctx := context.TODO() - celValidator := validator(&tt.schema, false, model.SchemaDeclType(&tt.schema, false), celconfig.PerCallLimit) + celValidator := validator(&tt.schema, &tt.schema, false, model.SchemaDeclType(&tt.schema, false), celconfig.PerCallLimit) if celValidator == nil { t.Fatal("expected non nil validator") } @@ -4225,7 +4689,7 @@ func TestOptionalOldSelfIsOptionalType(t *testing.T) { tt.schema.XValidations[i].OptionalOldSelf = ptr.To(true) } - celValidator := validator(&tt.schema, false, model.SchemaDeclType(&tt.schema, false), celconfig.PerCallLimit) + celValidator := validator(&tt.schema, &tt.schema, false, model.SchemaDeclType(&tt.schema, false), celconfig.PerCallLimit) if celValidator == nil { t.Fatal("expected non nil validator") }