Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

validate: Expose hooks to inject custom behavior during traversal #406

Merged
merged 6 commits into from
Jul 14, 2023
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 4 additions & 9 deletions pkg/validation/validate/object_validator.go
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ func (o *objectValidator) Validate(data interface{}) *Result {
// Cases: properties which are not regular properties and have not been matched by the PatternProperties validator
if o.AdditionalProperties != nil && o.AdditionalProperties.Schema != nil {
// AdditionalProperties as Schema
res.Merge(NewSchemaValidator(o.AdditionalProperties.Schema, o.Root, o.Path+"."+key, o.KnownFormats, o.Options.Options()...).Validate(value))
res.Merge(o.Options.subPropertyValidator(key, o.AdditionalProperties.Schema).Validate(value))
} else if regularProperty && !(matched || succeededOnce) {
// TODO: this is dead code since regularProperty=false here
res.AddErrors(errors.FailedAllPatternProperties(o.Path, o.In, key))
Expand All @@ -114,14 +114,9 @@ func (o *objectValidator) Validate(data interface{}) *Result {
// Property types:
// - regular Property
for pName, pSchema := range o.Properties {
rName := pName
if o.Path != "" {
rName = o.Path + "." + pName
}

// Recursively validates each property against its schema
if v, ok := val[pName]; ok {
r := NewSchemaValidator(&pSchema, o.Root, rName, o.KnownFormats, o.Options.Options()...).Validate(v)
r := o.Options.subPropertyValidator(pName, &pSchema).Validate(v)
res.Merge(r)
}
}
Expand All @@ -144,7 +139,7 @@ func (o *objectValidator) Validate(data interface{}) *Result {
if !regularProperty && (matched /*|| succeededOnce*/) {
for _, pName := range patterns {
if v, ok := o.PatternProperties[pName]; ok {
res.Merge(NewSchemaValidator(&v, o.Root, o.Path+"."+key, o.KnownFormats, o.Options.Options()...).Validate(value))
res.Merge(o.Options.subPropertyValidator(key, &v).Validate(value))
}
}
}
Expand All @@ -163,7 +158,7 @@ func (o *objectValidator) validatePatternProperty(key string, value interface{},
if match, _ := regexp.MatchString(k, key); match {
patterns = append(patterns, k)
matched = true
validator := NewSchemaValidator(&sch, o.Root, o.Path+"."+key, o.KnownFormats, o.Options.Options()...)
validator := o.Options.subPropertyValidator(key, &sch)

res := validator.Validate(value)
result.Merge(res)
Expand Down
107 changes: 107 additions & 0 deletions pkg/validation/validate/ratcheting.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
package validate

import (
"fmt"
"reflect"

"k8s.io/kube-openapi/pkg/validation/spec"
"k8s.io/kube-openapi/pkg/validation/strfmt"
)

type ratchetingSchemaValidator struct {
Schema *spec.Schema
Root interface{}
Path string
KnownFormats strfmt.Registry
Options []Option
}

func NewRatchetingSchemaValidator(schema *spec.Schema, rootSchema interface{}, root string, formats strfmt.Registry, options ...Option) *ratchetingSchemaValidator {
return &ratchetingSchemaValidator{
Schema: schema,
Root: rootSchema,
Path: root,
KnownFormats: formats,
Options: options,
}
}

func (r *ratchetingSchemaValidator) ValidateUpdate(old, new interface{}) *Result {
opts := append([]Option{
r.enableRatchetingOption(old),
}, r.Options...)

s := NewSchemaValidator(r.Schema, r.Root, r.Path, r.KnownFormats, opts...)

res := s.Validate(new)

if res.IsValid() {
return res
}

if reflect.DeepEqual(old, new) {
//!TODO: only consider errors with paths on "."
alexzielenski marked this conversation as resolved.
Show resolved Hide resolved
newRes := &Result{}
newRes.MergeAsWarnings(res)
return newRes
}

return res
}

func (r *ratchetingSchemaValidator) enableRatchetingOption(old interface{}) Option {
stub := &ratchetThunk{
oldValue: old,
ratchetingSchemaValidator: r,
}

return func(svo *SchemaValidatorOptions) {
svo.subIndexValidator = stub.SubIndexValidator
svo.subPropertyValidator = stub.SubPropertyValidator
}
}

type ratchetThunk struct {
*ratchetingSchemaValidator
oldValue interface{}
}

func (r ratchetThunk) Applies(value interface{}, kind reflect.Kind) bool {
return true
}

func (s ratchetThunk) SetPath(path string) {
s.ratchetingSchemaValidator.Path = path
}

// Validate validates the value.
func (r ratchetThunk) Validate(value interface{}) *Result {
return r.ValidateUpdate(r.oldValue, value)
}

func (r ratchetThunk) SubPropertyValidator(field string, sch *spec.Schema) valueValidator {
// Find correlated old value
if asMap, ok := r.oldValue.(map[string]interface{}); ok {
return ratchetThunk{
oldValue: asMap[field],
ratchetingSchemaValidator: NewRatchetingSchemaValidator(sch, r.Root, r.Path+"."+field, r.KnownFormats, r.Options...),
}
}

return NewSchemaValidator(sch, r.ratchetingSchemaValidator.Root, r.ratchetingSchemaValidator.Path+"."+field, r.ratchetingSchemaValidator.KnownFormats, r.ratchetingSchemaValidator.Options...)

}

func (r ratchetThunk) SubIndexValidator(index int, sch *spec.Schema) valueValidator {
//!TODO: implement slice ratcheting which considers the x-kubernetes extensions
// Some notes
// 1. Check if this schema uses map-keys
// 2. If it does not, use index
// 3. If it does, find other entry with map

// if the list is a set, just find the element in the old value which equals it
// if it exists. Sets can only be used on scalars so this is find

return NewSchemaValidator(sch, r.Root, fmt.Sprintf("%s[%d]", r.Path, index), r.KnownFormats, r.Options...)

}
121 changes: 121 additions & 0 deletions pkg/validation/validate/ratcheting_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
package validate_test

import (
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"k8s.io/kube-openapi/pkg/validation/spec"
"k8s.io/kube-openapi/pkg/validation/strfmt"
"k8s.io/kube-openapi/pkg/validation/validate"
)

func ptr[T any](v T) *T {
return &v
}

var zeroIntSchema *spec.Schema = &spec.Schema{
SchemaProps: spec.SchemaProps{
Type: spec.StringOrArray{"number"},
Minimum: ptr(float64(0)),
Maximum: ptr(float64(0)),
},
}

var smallIntSchema *spec.Schema = &spec.Schema{
SchemaProps: spec.SchemaProps{
Type: spec.StringOrArray{"number"},
Maximum: ptr(float64(50)),
},
}

var mediumIntSchema *spec.Schema = &spec.Schema{
SchemaProps: spec.SchemaProps{
Type: spec.StringOrArray{"number"},
Minimum: ptr(float64(50)),
Maximum: ptr(float64(10000)),
},
}

var largeIntSchema *spec.Schema = &spec.Schema{
SchemaProps: spec.SchemaProps{
Type: spec.StringOrArray{"number"},
Minimum: ptr(float64(10000)),
},
}

func TestScalarRatcheting(t *testing.T) {
validator := validate.NewRatchetingSchemaValidator(mediumIntSchema, nil, "", strfmt.Default)
require.True(t, validator.ValidateUpdate(1, 1).IsValid())
require.False(t, validator.ValidateUpdate(1, 2).IsValid())
}

var objectSchema *spec.Schema = &spec.Schema{
SchemaProps: spec.SchemaProps{
Type: spec.StringOrArray{"object"},
Properties: map[string]spec.Schema{
"zero": *zeroIntSchema,
"small": *smallIntSchema,
"medium": *mediumIntSchema,
"large": *largeIntSchema,
},
},
}

var objectObjectSchema *spec.Schema = &spec.Schema{
SchemaProps: spec.SchemaProps{
Type: spec.StringOrArray{"object"},
Properties: map[string]spec.Schema{
"nested": *objectSchema,
},
},
}

// Shows scalar fields of objects can be ratcheted
func TestObjectScalarFieldsRatcheting(t *testing.T) {
validator := validate.NewRatchetingSchemaValidator(objectSchema, nil, "", strfmt.Default)
assert.True(t, validator.ValidateUpdate(map[string]interface{}{
"small": 500,
}, map[string]interface{}{
"small": 500,
}).IsValid())
assert.True(t, validator.ValidateUpdate(map[string]interface{}{
"small": 501,
}, map[string]interface{}{
"small": 501,
"medium": 500,
}).IsValid())
assert.False(t, validator.ValidateUpdate(map[string]interface{}{
"small": 500,
}, map[string]interface{}{
"small": 501,
}).IsValid())
}

// Shows schemas with object fields which themselves are ratcheted can be ratcheted
func TestObjectObjectFieldsRatcheting(t *testing.T) {
validator := validate.NewRatchetingSchemaValidator(objectObjectSchema, nil, "", strfmt.Default)
assert.True(t, validator.ValidateUpdate(map[string]interface{}{
"nested": map[string]interface{}{
"small": 500,
}}, map[string]interface{}{
"nested": map[string]interface{}{
"small": 500,
}}).IsValid())
assert.True(t, validator.ValidateUpdate(map[string]interface{}{
"nested": map[string]interface{}{
"small": 501,
}}, map[string]interface{}{
"nested": map[string]interface{}{
"small": 501,
"medium": 500,
}}).IsValid())
assert.False(t, validator.ValidateUpdate(map[string]interface{}{
"nested": map[string]interface{}{
"small": 500,
}}, map[string]interface{}{
"nested": map[string]interface{}{
"small": 501,
}}).IsValid())
}

14 changes: 14 additions & 0 deletions pkg/validation/validate/schema.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,12 @@ func NewSchemaValidator(schema *spec.Schema, rootSchema interface{}, root string
for _, o := range options {
o(&s.Options)
}

if s.Options.subIndexValidator == nil {
s.Options.subPropertyValidator = s.SubPropertyValidator
alexzielenski marked this conversation as resolved.
Show resolved Hide resolved
s.Options.subIndexValidator = s.SubIndexValidator
}

s.validators = []valueValidator{
s.typeValidator(),
s.schemaPropsValidator(),
Expand All @@ -91,6 +97,14 @@ func NewSchemaValidator(schema *spec.Schema, rootSchema interface{}, root string
return &s
}

func (s *SchemaValidator) SubPropertyValidator(field string, sch *spec.Schema) valueValidator {
alexzielenski marked this conversation as resolved.
Show resolved Hide resolved
return NewSchemaValidator(sch, s.Root, s.Path+"."+field, s.KnownFormats, s.Options.Options()...)
}

func (s *SchemaValidator) SubIndexValidator(index int, sch *spec.Schema) valueValidator {
return NewSchemaValidator(sch, s.Root, fmt.Sprintf("%s[%d]", s.Path, index), s.KnownFormats, s.Options.Options()...)
}

// SetPath sets the path for this schema validator
func (s *SchemaValidator) SetPath(path string) {
s.Path = path
Expand Down
6 changes: 6 additions & 0 deletions pkg/validation/validate/schema_option.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,15 @@

package validate

import (
"k8s.io/kube-openapi/pkg/validation/spec"
)

// SchemaValidatorOptions defines optional rules for schema validation
type SchemaValidatorOptions struct {
validationRulesEnabled bool
subIndexValidator func(index int, sch *spec.Schema) valueValidator
subPropertyValidator func(field string, sch *spec.Schema) valueValidator
}

// Option sets optional rules for schema validation
Expand Down
6 changes: 3 additions & 3 deletions pkg/validation/validate/slice_validator.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ func (s *schemaSliceValidator) Validate(data interface{}) *Result {
size := val.Len()

if s.Items != nil && s.Items.Schema != nil {
validator := NewSchemaValidator(s.Items.Schema, s.Root, s.Path, s.KnownFormats, s.Options.Options()...)
validator := s.Options.subIndexValidator(0, s.Items.Schema)
for i := 0; i < size; i++ {
validator.SetPath(fmt.Sprintf("%s[%d]", s.Path, i))
value := val.Index(i)
Expand All @@ -66,7 +66,7 @@ func (s *schemaSliceValidator) Validate(data interface{}) *Result {
if s.Items != nil && len(s.Items.Schemas) > 0 {
itemsSize = len(s.Items.Schemas)
for i := 0; i < itemsSize; i++ {
validator := NewSchemaValidator(&s.Items.Schemas[i], s.Root, fmt.Sprintf("%s[%d]", s.Path, i), s.KnownFormats, s.Options.Options()...)
validator := s.Options.subIndexValidator(i, &s.Items.Schemas[i])
if val.Len() <= i {
break
}
Expand All @@ -79,7 +79,7 @@ func (s *schemaSliceValidator) Validate(data interface{}) *Result {
}
if s.AdditionalItems.Schema != nil {
for i := itemsSize; i < size-itemsSize+1; i++ {
validator := NewSchemaValidator(s.AdditionalItems.Schema, s.Root, fmt.Sprintf("%s[%d]", s.Path, i), s.KnownFormats, s.Options.Options()...)
validator := s.Options.subIndexValidator(i, s.AdditionalItems.Schema)
result.Merge(validator.Validate(val.Index(i).Interface()))
}
}
Expand Down