mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-08-14 06:15:45 +00:00
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:
parent
44d05119e2
commit
74f8e4dd51
@ -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
|
||||
|
@ -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")
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user