add allOf support into CEL validator

preserve errors for additionalProperties and xvalidations so the API surface stays the same

Once this has had some soak time we can allow CRDs to use these abilities
This commit is contained in:
Alexander Zielenski 2024-04-17 19:06:01 -07:00
parent 44d05119e2
commit 74f8e4dd51
2 changed files with 586 additions and 47 deletions

View File

@ -53,9 +53,12 @@ import (
type Validator struct { type Validator struct {
Items *Validator Items *Validator
Properties map[string]Validator Properties map[string]Validator
AllOfValidators []*Validator
AdditionalProperties *Validator AdditionalProperties *Validator
Schema *schema.Structural
uncompiledRules []apiextensions.ValidationRule
compiledRules []CompilationResult compiledRules []CompilationResult
// Program compilation is pre-checked at CRD creation/update time, so we don't expect compilation to fail // Program compilation is pre-checked at CRD creation/update time, so we don't expect compilation to fail
@ -82,25 +85,37 @@ func NewValidator(s *schema.Structural, isResourceRoot bool, perCallLimit uint64
if !hasXValidations(s) { if !hasXValidations(s) {
return nil 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 // 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 // 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. // 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. // 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 { func validator(validationSchema, nodeSchema *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()) compilationSchema := *nodeSchema
compilationSchema.XValidations = validationSchema.XValidations
compiledRules, err := Compile(&compilationSchema, declType, perCallLimit, environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion(), true), StoredExpressionsEnvLoader())
var itemsValidator, additionalPropertiesValidator *Validator var itemsValidator, additionalPropertiesValidator *Validator
var propertiesValidators map[string]Validator var propertiesValidators map[string]Validator
if s.Items != nil { var allOfValidators []*Validator
itemsValidator = validator(s.Items, s.Items.XEmbeddedResource, declType.ElemType, perCallLimit)
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)) if len(validationSchema.Properties) > 0 {
for k, p := range s.Properties { propertiesValidators = make(map[string]Validator, len(validationSchema.Properties))
prop := p 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 var fieldType *cel.DeclType
if escapedPropName, ok := cel.Escape(k); ok { if escapedPropName, ok := cel.Escape(k); ok {
if f, ok := declType.Fields[escapedPropName]; ok { if f, ok := declType.Fields[escapedPropName]; ok {
@ -112,20 +127,32 @@ func validator(s *schema.Structural, isResourceRoot bool, declType *cel.DeclType
} else { } else {
// field may be absent from declType if the property name is unescapable, in which case we should convert // 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. // the field value type to a DeclType.
fieldType = model.SchemaDeclType(&prop, prop.XEmbeddedResource) fieldType = model.SchemaDeclType(&nodeProperty, nodeProperty.XEmbeddedResource)
if fieldType == nil { if fieldType == nil {
continue 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 propertiesValidators[k] = *p
} }
} }
} }
if s.AdditionalProperties != nil && s.AdditionalProperties.Structural != nil { if validationSchema.AdditionalProperties != nil && validationSchema.AdditionalProperties.Structural != nil &&
additionalPropertiesValidator = validator(s.AdditionalProperties.Structural, s.AdditionalProperties.Structural.XEmbeddedResource, declType.ElemType, perCallLimit) 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 activationFactory := validationActivationWithoutOldSelf
for _, rule := range compiledRules { for _, rule := range compiledRules {
if rule.UsesOldSelf { if rule.UsesOldSelf {
@ -136,12 +163,15 @@ func validator(s *schema.Structural, isResourceRoot bool, declType *cel.DeclType
return &Validator{ return &Validator{
compiledRules: compiledRules, compiledRules: compiledRules,
uncompiledRules: validationSchema.XValidations,
compilationErr: err, compilationErr: err,
isResourceRoot: isResourceRoot, isResourceRoot: isResourceRoot,
Items: itemsValidator, Items: itemsValidator,
AdditionalProperties: additionalPropertiesValidator, AdditionalProperties: additionalPropertiesValidator,
Properties: propertiesValidators, Properties: propertiesValidators,
AllOfValidators: allOfValidators,
celActivationFactory: activationFactory, 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. // 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 // 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 // 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{} opt := options{}
for _, o := range opts { for _, o := range opts {
o(&opt) 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 // 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} 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() t := time.Now()
defer func() { defer func() {
metrics.Metrics.ObserveEvaluation(time.Since(t)) 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 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 { if remainingBudget < 0 {
return errs, remainingBudget 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) { switch obj := obj.(type) {
case []interface{}: case []interface{}:
oldArray, _ := oldObj.([]interface{}) oldArray, _ := oldObj.([]interface{})
var arrayErrs field.ErrorList 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...) errs = append(errs, arrayErrs...)
return errs, remainingBudget return errs, remainingBudget
case map[string]interface{}: case map[string]interface{}:
oldMap, _ := oldObj.(map[string]interface{}) oldMap, _ := oldObj.(map[string]interface{})
var mapErrs field.ErrorList 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...) errs = append(errs, mapErrs...)
return errs, remainingBudget return errs, remainingBudget
} }
@ -268,7 +342,9 @@ func (s *Validator) validate(ctx context.Context, fldPath *field.Path, sts *sche
return errs, remainingBudget 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 // guard against oldObj being a non-nil interface with a nil value
if oldObj != nil { if oldObj != nil {
v := reflect.ValueOf(oldObj) 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) activation, optionalOldSelfActivation := s.celActivationFactory(sts, obj, oldObj)
for i, compiled := range s.compiledRules { for i, compiled := range s.compiledRules {
rule := sts.XValidations[i] rule := s.uncompiledRules[i]
if compiled.Error != nil { if compiled.Error != nil {
errs = append(errs, field.Invalid(fldPath, sts.Type, fmt.Sprintf("rule compile error: %v", compiled.Error))) errs = append(errs, field.Invalid(fldPath, sts.Type, fmt.Sprintf("rule compile error: %v", compiled.Error)))
continue continue
@ -720,7 +796,7 @@ func (a *validationActivation) Parent() interpreter.Activation {
return nil 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 remainingBudget = costBudget
if remainingBudget < 0 { if remainingBudget < 0 {
return errs, remainingBudget return errs, remainingBudget
@ -729,9 +805,9 @@ func (s *Validator) validateMap(ctx context.Context, fldPath *field.Path, sts *s
return nil, remainingBudget 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 { for k, v := range obj {
var oldV interface{} var oldV interface{}
if correlatable { if correlatable {
@ -739,25 +815,24 @@ func (s *Validator) validateMap(ctx context.Context, fldPath *field.Path, sts *s
} }
var err field.ErrorList 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...) errs = append(errs, err...)
if remainingBudget < 0 { if remainingBudget < 0 {
return errs, remainingBudget return errs, remainingBudget
} }
} }
} }
if s.Properties != nil && sts.Properties != nil { if s.Properties != nil {
for k, v := range obj { for k, v := range obj {
stsProp, stsOk := sts.Properties[k]
sub, ok := s.Properties[k] sub, ok := s.Properties[k]
if ok && stsOk { if ok {
var oldV interface{} var oldV interface{}
if correlatable { if correlatable {
oldV = oldObj[k] // +k8s:verify-mutation:reason=clone oldV = oldObj[k] // +k8s:verify-mutation:reason=clone
} }
var err field.ErrorList 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...) errs = append(errs, err...)
if remainingBudget < 0 { if remainingBudget < 0 {
return errs, remainingBudget return errs, remainingBudget
@ -769,19 +844,19 @@ func (s *Validator) validateMap(ctx context.Context, fldPath *field.Path, sts *s
return errs, remainingBudget 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 remainingBudget = costBudget
if remainingBudget < 0 { if remainingBudget < 0 {
return errs, remainingBudget 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 // 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 // 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 { for i := range obj {
var err field.ErrorList 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...) errs = append(errs, err...)
if remainingBudget < 0 { if remainingBudget < 0 {
return errs, remainingBudget return errs, remainingBudget

View File

@ -2059,7 +2059,7 @@ func TestValidationExpressions(t *testing.T) {
t.Run(testName, func(t *testing.T) { t.Run(testName, func(t *testing.T) {
t.Parallel() t.Parallel()
s := withRule(*tt.schema, validRule) 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 { if celValidator == nil {
t.Fatal("expected non nil validator") 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])?$"))`), 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"}, 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 { for _, tt := range tests {
@ -2304,7 +2768,7 @@ func TestValidationExpressionsAtSchemaLevels(t *testing.T) {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
t.Parallel() t.Parallel()
ctx := context.TODO() 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 { if celValidator == nil {
t.Fatal("expected non nil validator") t.Fatal("expected non nil validator")
} }
@ -2371,7 +2835,7 @@ func TestCELValidationLimit(t *testing.T) {
t.Run(validRule, func(t *testing.T) { t.Run(validRule, func(t *testing.T) {
t.Parallel() t.Parallel()
s := withRule(*tt.schema, validRule) 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 // test with cost budget exceeded
errs, _ := celValidator.Validate(ctx, field.NewPath("root"), &s, tt.obj, nil, 0) 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.Run(testName, func(t *testing.T) {
t.Parallel() t.Parallel()
s := withRule(*tt.schema, validRule) 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 { if celValidator == nil {
t.Fatal("expected non nil validator") t.Fatal("expected non nil validator")
} }
@ -2826,7 +3290,7 @@ func TestReasonAndFldPath(t *testing.T) {
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
ctx := context.TODO() 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 { if celValidator == nil {
t.Fatal("expected non nil validator") t.Fatal("expected non nil validator")
} }
@ -3985,7 +4449,7 @@ func TestOptionalOldSelf(t *testing.T) {
// t.Parallel() // t.Parallel()
ctx := context.TODO() 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 { if celValidator == nil {
t.Fatal("expected non nil validator") t.Fatal("expected non nil validator")
} }
@ -4139,7 +4603,7 @@ func TestOptionalOldSelfCheckForNull(t *testing.T) {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
ctx := context.TODO() 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 { if celValidator == nil {
t.Fatal("expected non nil validator") t.Fatal("expected non nil validator")
} }
@ -4225,7 +4689,7 @@ func TestOptionalOldSelfIsOptionalType(t *testing.T) {
tt.schema.XValidations[i].OptionalOldSelf = ptr.To(true) 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 { if celValidator == nil {
t.Fatal("expected non nil validator") t.Fatal("expected non nil validator")
} }