mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-07-23 11:50:44 +00:00
apiextensions: add structural schema intermediate types
This commit is contained in:
parent
d8a9dfacbf
commit
f9dc278e75
@ -686,6 +686,10 @@ func (v *specStandardValidatorV3) validate(schema *apiextensions.JSONSchemaProps
|
||||
return allErrs
|
||||
}
|
||||
|
||||
//
|
||||
// WARNING: if anything new is allowed below, NewStructural must be adapted to support it.
|
||||
//
|
||||
|
||||
if schema.Default != nil {
|
||||
allErrs = append(allErrs, field.Forbidden(fldPath.Child("default"), "default is not supported"))
|
||||
}
|
||||
|
@ -0,0 +1,82 @@
|
||||
/*
|
||||
Copyright 2019 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package schema
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"k8s.io/apimachinery/pkg/util/validation/field"
|
||||
)
|
||||
|
||||
// validateStructuralCompleteness checks that every specified field or array in s is also specified
|
||||
// outside of value validation.
|
||||
func validateStructuralCompleteness(s *Structural, fldPath *field.Path) field.ErrorList {
|
||||
if s == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return validateValueValidationCompleteness(s.ValueValidation, s, fldPath, fldPath)
|
||||
}
|
||||
|
||||
func validateValueValidationCompleteness(v *ValueValidation, s *Structural, sPath, vPath *field.Path) field.ErrorList {
|
||||
if v == nil {
|
||||
return nil
|
||||
}
|
||||
if s == nil {
|
||||
return field.ErrorList{field.Required(sPath, fmt.Sprintf("because it is defined in %s", vPath.String()))}
|
||||
}
|
||||
|
||||
allErrs := field.ErrorList{}
|
||||
|
||||
allErrs = append(allErrs, validateNestedValueValidationCompleteness(v.Not, s, sPath, vPath.Child("not"))...)
|
||||
for i := range v.AllOf {
|
||||
allErrs = append(allErrs, validateNestedValueValidationCompleteness(&v.AllOf[i], s, sPath, vPath.Child("allOf").Index(i))...)
|
||||
}
|
||||
for i := range v.AnyOf {
|
||||
allErrs = append(allErrs, validateNestedValueValidationCompleteness(&v.AnyOf[i], s, sPath, vPath.Child("anyOf").Index(i))...)
|
||||
}
|
||||
for i := range v.OneOf {
|
||||
allErrs = append(allErrs, validateNestedValueValidationCompleteness(&v.OneOf[i], s, sPath, vPath.Child("oneOf").Index(i))...)
|
||||
}
|
||||
|
||||
return allErrs
|
||||
}
|
||||
|
||||
func validateNestedValueValidationCompleteness(v *NestedValueValidation, s *Structural, sPath, vPath *field.Path) field.ErrorList {
|
||||
if v == nil {
|
||||
return nil
|
||||
}
|
||||
if s == nil {
|
||||
return field.ErrorList{field.Required(sPath, fmt.Sprintf("because it is defined in %s", vPath.String()))}
|
||||
}
|
||||
|
||||
allErrs := field.ErrorList{}
|
||||
|
||||
allErrs = append(allErrs, validateValueValidationCompleteness(&v.ValueValidation, s, sPath, vPath)...)
|
||||
allErrs = append(allErrs, validateNestedValueValidationCompleteness(v.Items, s.Items, sPath.Child("items"), vPath.Child("items"))...)
|
||||
for k, vFld := range v.Properties {
|
||||
if sFld, ok := s.Properties[k]; !ok {
|
||||
allErrs = append(allErrs, field.Required(sPath.Child("properties").Key(k), fmt.Sprintf("because it is defined in %s", vPath.Child("properties").Key(k))))
|
||||
} else {
|
||||
allErrs = append(allErrs, validateNestedValueValidationCompleteness(&vFld, &sFld, sPath.Child("properties").Key(k), vPath.Child("properties").Key(k))...)
|
||||
}
|
||||
}
|
||||
|
||||
// don't check additionalProperties as this is not allowed (and checked during validation)
|
||||
|
||||
return allErrs
|
||||
}
|
@ -0,0 +1,276 @@
|
||||
/*
|
||||
Copyright 2019 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package schema
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions"
|
||||
)
|
||||
|
||||
// NewStructural converts an OpenAPI v3 schema into a structural schema. A pre-validated JSONSchemaProps will
|
||||
// not fail on NewStructural. This means that we require that:
|
||||
//
|
||||
// - items is not an array of schemas
|
||||
// - the following fields are not set:
|
||||
// - id
|
||||
// - schema
|
||||
// - $ref
|
||||
// - patternProperties
|
||||
// - dependencies
|
||||
// - additionalItems
|
||||
// - definitions.
|
||||
//
|
||||
// The follow fields are not preserved:
|
||||
// - externalDocs
|
||||
// - example.
|
||||
func NewStructural(s *apiextensions.JSONSchemaProps) (*Structural, error) {
|
||||
if s == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
if err := validateUnsupportedFields(s); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
vv, err := newValueValidation(s)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
g, err := newGenerics(s)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
x, err := newExtensions(s)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ss := &Structural{
|
||||
Generic: *g,
|
||||
Extensions: *x,
|
||||
ValueValidation: vv,
|
||||
}
|
||||
|
||||
if s.Items != nil {
|
||||
if len(s.Items.JSONSchemas) > 0 {
|
||||
// we validate that it is not an array
|
||||
return nil, fmt.Errorf("OpenAPIV3Schema 'items' must be a schema, but is an array")
|
||||
}
|
||||
item, err := NewStructural(s.Items.Schema)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ss.Items = item
|
||||
}
|
||||
|
||||
if len(s.Properties) > 0 {
|
||||
ss.Properties = make(map[string]Structural, len(s.Properties))
|
||||
for k, x := range s.Properties {
|
||||
fld, err := NewStructural(&x)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ss.Properties[k] = *fld
|
||||
}
|
||||
}
|
||||
|
||||
return ss, nil
|
||||
}
|
||||
|
||||
func newGenerics(s *apiextensions.JSONSchemaProps) (*Generic, error) {
|
||||
if s == nil {
|
||||
return nil, nil
|
||||
}
|
||||
g := &Generic{
|
||||
Type: s.Type,
|
||||
Description: s.Description,
|
||||
Title: s.Title,
|
||||
Nullable: s.Nullable,
|
||||
}
|
||||
if s.Default != nil {
|
||||
g.Default = JSON{interface{}(*s.Default)}
|
||||
}
|
||||
|
||||
if s.AdditionalProperties != nil {
|
||||
if s.AdditionalProperties.Schema != nil {
|
||||
ss, err := NewStructural(s.AdditionalProperties.Schema)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
g.AdditionalProperties = &StructuralOrBool{Structural: ss}
|
||||
} else {
|
||||
g.AdditionalProperties = &StructuralOrBool{Bool: s.AdditionalProperties.Allows}
|
||||
}
|
||||
}
|
||||
|
||||
return g, nil
|
||||
}
|
||||
|
||||
func newValueValidation(s *apiextensions.JSONSchemaProps) (*ValueValidation, error) {
|
||||
if s == nil {
|
||||
return nil, nil
|
||||
}
|
||||
not, err := newNestedValueValidation(s.Not)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
v := &ValueValidation{
|
||||
Format: s.Format,
|
||||
Maximum: s.Maximum,
|
||||
ExclusiveMaximum: s.ExclusiveMaximum,
|
||||
Minimum: s.Minimum,
|
||||
ExclusiveMinimum: s.ExclusiveMinimum,
|
||||
MaxLength: s.MaxLength,
|
||||
MinLength: s.MinLength,
|
||||
Pattern: s.Pattern,
|
||||
MaxItems: s.MaxItems,
|
||||
MinItems: s.MinItems,
|
||||
UniqueItems: s.UniqueItems,
|
||||
MultipleOf: s.MultipleOf,
|
||||
MaxProperties: s.MaxProperties,
|
||||
MinProperties: s.MinProperties,
|
||||
Required: s.Required,
|
||||
Not: not,
|
||||
}
|
||||
|
||||
for _, e := range s.Enum {
|
||||
v.Enum = append(v.Enum, JSON{e})
|
||||
}
|
||||
|
||||
for _, x := range s.AllOf {
|
||||
clause, err := newNestedValueValidation(&x)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
v.AllOf = append(v.AllOf, *clause)
|
||||
}
|
||||
|
||||
for _, x := range s.AnyOf {
|
||||
clause, err := newNestedValueValidation(&x)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
v.AnyOf = append(v.AnyOf, *clause)
|
||||
}
|
||||
|
||||
for _, x := range s.OneOf {
|
||||
clause, err := newNestedValueValidation(&x)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
v.OneOf = append(v.OneOf, *clause)
|
||||
}
|
||||
|
||||
return v, nil
|
||||
}
|
||||
|
||||
func newNestedValueValidation(s *apiextensions.JSONSchemaProps) (*NestedValueValidation, error) {
|
||||
if s == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
if err := validateUnsupportedFields(s); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
vv, err := newValueValidation(s)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
g, err := newGenerics(s)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
x, err := newExtensions(s)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
v := &NestedValueValidation{
|
||||
ValueValidation: *vv,
|
||||
ForbiddenGenerics: *g,
|
||||
ForbiddenExtensions: *x,
|
||||
}
|
||||
|
||||
if s.Items != nil {
|
||||
if len(s.Items.JSONSchemas) > 0 {
|
||||
// we validate that it is not an array
|
||||
return nil, fmt.Errorf("OpenAPIV3Schema 'items' must be a schema, but is an array")
|
||||
}
|
||||
nvv, err := newNestedValueValidation(s.Items.Schema)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
v.Items = nvv
|
||||
}
|
||||
if s.Properties != nil {
|
||||
v.Properties = make(map[string]NestedValueValidation, len(s.Properties))
|
||||
for k, x := range s.Properties {
|
||||
nvv, err := newNestedValueValidation(&x)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
v.Properties[k] = *nvv
|
||||
}
|
||||
}
|
||||
|
||||
return v, nil
|
||||
}
|
||||
|
||||
func newExtensions(s *apiextensions.JSONSchemaProps) (*Extensions, error) {
|
||||
if s == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
return &Extensions{
|
||||
XPreserveUnknownFields: s.XPreserveUnknownFields,
|
||||
XEmbeddedResource: s.XEmbeddedResource,
|
||||
XIntOrString: s.XIntOrString,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// validateUnsupportedFields checks that those fields rejected by validation are actually unset.
|
||||
func validateUnsupportedFields(s *apiextensions.JSONSchemaProps) error {
|
||||
if len(s.ID) > 0 {
|
||||
return fmt.Errorf("OpenAPIV3Schema 'id' is not supported")
|
||||
}
|
||||
if len(s.Schema) > 0 {
|
||||
return fmt.Errorf("OpenAPIV3Schema 'schema' is not supported")
|
||||
}
|
||||
if s.Ref != nil && len(*s.Ref) > 0 {
|
||||
return fmt.Errorf("OpenAPIV3Schema '$ref' is not supported")
|
||||
}
|
||||
if len(s.PatternProperties) > 0 {
|
||||
return fmt.Errorf("OpenAPIV3Schema 'patternProperties' is not supported")
|
||||
}
|
||||
if len(s.Dependencies) > 0 {
|
||||
return fmt.Errorf("OpenAPIV3Schema 'dependencies' is not supported")
|
||||
}
|
||||
if s.AdditionalItems != nil {
|
||||
return fmt.Errorf("OpenAPIV3Schema 'additionalItems' is not supported")
|
||||
}
|
||||
if len(s.Definitions) > 0 {
|
||||
return fmt.Errorf("OpenAPIV3Schema 'definitions' is not supported")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
@ -0,0 +1,160 @@
|
||||
/*
|
||||
Copyright 2019 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package schema
|
||||
|
||||
import (
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
)
|
||||
|
||||
// +k8s:deepcopy-gen=true
|
||||
|
||||
// Structural represents a structural schema.
|
||||
type Structural struct {
|
||||
Items *Structural
|
||||
Properties map[string]Structural
|
||||
|
||||
Generic
|
||||
Extensions
|
||||
|
||||
*ValueValidation
|
||||
}
|
||||
|
||||
// +k8s:deepcopy-gen=true
|
||||
|
||||
// StructuralOrBool is either a structural schema or a boolean.
|
||||
type StructuralOrBool struct {
|
||||
Structural *Structural
|
||||
Bool bool
|
||||
}
|
||||
|
||||
// +k8s:deepcopy-gen=true
|
||||
|
||||
// Generic contains the generic schema fields not allowed in value validation.
|
||||
type Generic struct {
|
||||
Description string
|
||||
// type specifies the type of a value.
|
||||
// It can be object, array, number, integer, boolean, string.
|
||||
// It is optional only if x-kubernetes-preserve-unknown-fields
|
||||
// or x-kubernetes-int-or-string is true.
|
||||
Type string
|
||||
Title string
|
||||
Default JSON
|
||||
AdditionalProperties *StructuralOrBool
|
||||
Nullable bool
|
||||
}
|
||||
|
||||
// +k8s:deepcopy-gen=true
|
||||
|
||||
// Extensions contains the Kubernetes OpenAPI v3 vendor extensions.
|
||||
type Extensions struct {
|
||||
// x-kubernetes-preserve-unknown-fields stops the API server
|
||||
// decoding step from pruning fields which are not specified
|
||||
// in the validation schema. This affects fields recursively,
|
||||
// but switches back to normal pruning behaviour if nested
|
||||
// properties or additionalProperties are specified in the schema.
|
||||
XPreserveUnknownFields bool
|
||||
|
||||
// x-kubernetes-embedded-resource defines that the value is an
|
||||
// embedded Kubernetes runtime.Object, with TypeMeta and
|
||||
// ObjectMeta. The type must be object. It is allowed to further
|
||||
// restrict the embedded object. Both ObjectMeta and TypeMeta
|
||||
// are validated automatically. x-kubernetes-preserve-unknown-fields
|
||||
// must be true.
|
||||
XEmbeddedResource bool
|
||||
|
||||
// x-kubernetes-int-or-string specifies that this value is
|
||||
// either an integer or a string. If this is true, an empty
|
||||
// type is allowed and type as child of anyOf is permitted
|
||||
// if following one of the following patterns:
|
||||
//
|
||||
// 1) anyOf:
|
||||
// - type: integer
|
||||
// - type: string
|
||||
// 2) allOf:
|
||||
// - anyOf:
|
||||
// - type: integer
|
||||
// - type: string
|
||||
// - ... zero or more
|
||||
XIntOrString bool
|
||||
}
|
||||
|
||||
// +k8s:deepcopy-gen=true
|
||||
|
||||
// ValueValidation contains all schema fields not contributing to the structure of the schema.
|
||||
type ValueValidation struct {
|
||||
Format string
|
||||
Maximum *float64
|
||||
ExclusiveMaximum bool
|
||||
Minimum *float64
|
||||
ExclusiveMinimum bool
|
||||
MaxLength *int64
|
||||
MinLength *int64
|
||||
Pattern string
|
||||
MaxItems *int64
|
||||
MinItems *int64
|
||||
UniqueItems bool
|
||||
MultipleOf *float64
|
||||
Enum []JSON
|
||||
MaxProperties *int64
|
||||
MinProperties *int64
|
||||
Required []string
|
||||
AllOf []NestedValueValidation
|
||||
OneOf []NestedValueValidation
|
||||
AnyOf []NestedValueValidation
|
||||
Not *NestedValueValidation
|
||||
}
|
||||
|
||||
// +k8s:deepcopy-gen=true
|
||||
|
||||
// NestedValueValidation contains value validations, items and properties usable when nested
|
||||
// under a logical junctor, and catch all structs for generic and vendor extensions schema fields.
|
||||
type NestedValueValidation struct {
|
||||
ValueValidation
|
||||
|
||||
Items *NestedValueValidation
|
||||
Properties map[string]NestedValueValidation
|
||||
|
||||
// Anything set in the following will make the scheme
|
||||
// non-structural, with the exception of these two patterns if
|
||||
// x-kubernetes-int-or-string is true:
|
||||
//
|
||||
// 1) anyOf:
|
||||
// - type: integer
|
||||
// - type: string
|
||||
// 2) allOf:
|
||||
// - anyOf:
|
||||
// - type: integer
|
||||
// - type: string
|
||||
// - ... zero or more
|
||||
ForbiddenGenerics Generic
|
||||
ForbiddenExtensions Extensions
|
||||
}
|
||||
|
||||
// JSON wraps an arbitrary JSON value to be able to implement deepcopy.
|
||||
type JSON struct {
|
||||
Object interface{}
|
||||
}
|
||||
|
||||
// DeepCopy creates a deep copy of the wrapped JSON value.
|
||||
func (j JSON) DeepCopy() JSON {
|
||||
return JSON{runtime.DeepCopyJSONValue(j.Object)}
|
||||
}
|
||||
|
||||
// DeepCopyInto creates a deep copy of the wrapped JSON value and stores it in into.
|
||||
func (j JSON) DeepCopyInto(into *JSON) {
|
||||
into.Object = runtime.DeepCopyJSONValue(j.Object)
|
||||
}
|
@ -0,0 +1,238 @@
|
||||
/*
|
||||
Copyright 2019 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package schema
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
|
||||
"k8s.io/apimachinery/pkg/util/validation/field"
|
||||
)
|
||||
|
||||
var intOrStringAnyOf = []NestedValueValidation{
|
||||
{ForbiddenGenerics: Generic{
|
||||
Type: "integer",
|
||||
}},
|
||||
{ForbiddenGenerics: Generic{
|
||||
Type: "string",
|
||||
}},
|
||||
}
|
||||
|
||||
type level int
|
||||
|
||||
const (
|
||||
rootLevel level = iota
|
||||
itemLevel
|
||||
fieldLevel
|
||||
)
|
||||
|
||||
// ValidateStructural checks that s is a structural schema with the invariants:
|
||||
//
|
||||
// * structurality: both `ForbiddenGenerics` and `ForbiddenExtensions` only have zero values, with the two exceptions for IntOrString.
|
||||
// * RawExtension: for every schema with `x-kubernetes-embedded-resource: true`, `x-kubernetes-preserve-unknown-fields: true` and `type: object` are set
|
||||
// * IntOrString: for `x-kubernetes-int-or-string: true` either `type` is empty under `anyOf` and `allOf` or the schema structure is one of these:
|
||||
//
|
||||
// 1) anyOf:
|
||||
// - type: integer
|
||||
// - type: string
|
||||
// 2) allOf:
|
||||
// - anyOf:
|
||||
// - type: integer
|
||||
// - type: string
|
||||
// - ... zero or more
|
||||
//
|
||||
// * every specified field or array in s is also specified outside of value validation.
|
||||
func ValidateStructural(s *Structural, fldPath *field.Path) field.ErrorList {
|
||||
allErrs := field.ErrorList{}
|
||||
|
||||
allErrs = append(allErrs, validateStructuralInvariants(s, rootLevel, fldPath)...)
|
||||
allErrs = append(allErrs, validateStructuralCompleteness(s, fldPath)...)
|
||||
|
||||
return allErrs
|
||||
}
|
||||
|
||||
// validateStructuralInvariants checks the invariants of a structural schema.
|
||||
func validateStructuralInvariants(s *Structural, lvl level, fldPath *field.Path) field.ErrorList {
|
||||
if s == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
allErrs := field.ErrorList{}
|
||||
|
||||
allErrs = append(allErrs, validateStructuralInvariants(s.Items, itemLevel, fldPath.Child("items"))...)
|
||||
for k, v := range s.Properties {
|
||||
allErrs = append(allErrs, validateStructuralInvariants(&v, fieldLevel, fldPath.Child("properties").Key(k))...)
|
||||
}
|
||||
allErrs = append(allErrs, validateGeneric(&s.Generic, fldPath)...)
|
||||
allErrs = append(allErrs, validateExtensions(&s.Extensions, fldPath)...)
|
||||
|
||||
// detect the two IntOrString exceptions:
|
||||
// 1) anyOf:
|
||||
// - type: integer
|
||||
// - type: string
|
||||
// 2) allOf:
|
||||
// - anyOf:
|
||||
// - type: integer
|
||||
// - type: string
|
||||
// - ... zero or more
|
||||
skipAnyOf := false
|
||||
skipFirstAllOfAnyOf := false
|
||||
if s.XIntOrString && s.ValueValidation != nil {
|
||||
if len(s.ValueValidation.AnyOf) == 2 && reflect.DeepEqual(s.ValueValidation.AnyOf, intOrStringAnyOf) {
|
||||
skipAnyOf = true
|
||||
} else if len(s.ValueValidation.AllOf) >= 1 && len(s.ValueValidation.AllOf[0].AnyOf) == 2 && reflect.DeepEqual(s.ValueValidation.AllOf[0].AnyOf, intOrStringAnyOf) {
|
||||
skipFirstAllOfAnyOf = true
|
||||
}
|
||||
}
|
||||
|
||||
allErrs = append(allErrs, validateValueValidation(s.ValueValidation, skipAnyOf, skipFirstAllOfAnyOf, fldPath)...)
|
||||
|
||||
if s.XEmbeddedResource && s.Type != "object" {
|
||||
if len(s.Type) == 0 {
|
||||
allErrs = append(allErrs, field.Required(fldPath.Child("type"), "must be object if x-kubernetes-embedded-resource is true"))
|
||||
} else {
|
||||
allErrs = append(allErrs, field.Invalid(fldPath.Child("type"), s.Type, "must be object if x-kubernetes-embedded-resource is true"))
|
||||
}
|
||||
} else if len(s.Type) == 0 && !s.Extensions.XIntOrString && !s.Extensions.XPreserveUnknownFields {
|
||||
switch lvl {
|
||||
case rootLevel:
|
||||
allErrs = append(allErrs, field.Required(fldPath.Child("type"), "must not be empty at the root"))
|
||||
case itemLevel:
|
||||
allErrs = append(allErrs, field.Required(fldPath.Child("type"), "must not be empty for specified array items"))
|
||||
case fieldLevel:
|
||||
allErrs = append(allErrs, field.Required(fldPath.Child("type"), "must not be empty for specified object fields"))
|
||||
}
|
||||
}
|
||||
|
||||
if lvl == rootLevel && len(s.Type) > 0 && s.Type != "object" {
|
||||
allErrs = append(allErrs, field.Invalid(fldPath.Child("type"), s.Type, "must be object at the root"))
|
||||
}
|
||||
|
||||
if s.XEmbeddedResource && !s.XPreserveUnknownFields && s.Properties == nil {
|
||||
allErrs = append(allErrs, field.Required(fldPath.Child("properties"), "must not be empty if x-kubernetes-embedded-resource is true without x-kubernetes-preserve-unknown-fields"))
|
||||
}
|
||||
|
||||
return allErrs
|
||||
}
|
||||
|
||||
// validateGeneric checks the generic fields of a structural schema.
|
||||
func validateGeneric(g *Generic, fldPath *field.Path) field.ErrorList {
|
||||
if g == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
allErrs := field.ErrorList{}
|
||||
|
||||
if g.AdditionalProperties != nil {
|
||||
if g.AdditionalProperties.Structural != nil {
|
||||
allErrs = append(allErrs, validateStructuralInvariants(g.AdditionalProperties.Structural, fieldLevel, fldPath.Child("additionalProperties"))...)
|
||||
}
|
||||
}
|
||||
|
||||
return allErrs
|
||||
}
|
||||
|
||||
// validateExtensions checks Kubernetes vendor extensions of a structural schema.
|
||||
func validateExtensions(x *Extensions, fldPath *field.Path) field.ErrorList {
|
||||
allErrs := field.ErrorList{}
|
||||
|
||||
if x.XIntOrString && x.XPreserveUnknownFields {
|
||||
allErrs = append(allErrs, field.Invalid(fldPath.Child("x-kubernetes-preserve-unknown-fields"), x.XPreserveUnknownFields, "must be false if x-kubernetes-int-or-string is true"))
|
||||
}
|
||||
if x.XIntOrString && x.XEmbeddedResource {
|
||||
allErrs = append(allErrs, field.Invalid(fldPath.Child("x-kubernetes-embedded-resource"), x.XEmbeddedResource, "must be false if x-kubernetes-int-or-string is true"))
|
||||
}
|
||||
|
||||
return allErrs
|
||||
}
|
||||
|
||||
// validateValueValidation checks the value validation in a structural schema.
|
||||
func validateValueValidation(v *ValueValidation, skipAnyOf, skipFirstAllOfAnyOf bool, fldPath *field.Path) field.ErrorList {
|
||||
if v == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
allErrs := field.ErrorList{}
|
||||
|
||||
if !skipAnyOf {
|
||||
for i := range v.AnyOf {
|
||||
allErrs = append(allErrs, validateNestedValueValidation(&v.AnyOf[i], false, false, fldPath.Child("anyOf").Index(i))...)
|
||||
}
|
||||
}
|
||||
|
||||
for i := range v.AllOf {
|
||||
skipAnyOf := false
|
||||
if skipFirstAllOfAnyOf && i == 0 {
|
||||
skipAnyOf = true
|
||||
}
|
||||
allErrs = append(allErrs, validateNestedValueValidation(&v.AllOf[i], skipAnyOf, false, fldPath.Child("allOf").Index(i))...)
|
||||
}
|
||||
|
||||
for i := range v.OneOf {
|
||||
allErrs = append(allErrs, validateNestedValueValidation(&v.OneOf[i], false, false, fldPath.Child("oneOf").Index(i))...)
|
||||
}
|
||||
|
||||
allErrs = append(allErrs, validateNestedValueValidation(v.Not, false, false, fldPath.Child("not"))...)
|
||||
|
||||
return allErrs
|
||||
}
|
||||
|
||||
// validateNestedValueValidation checks the nested value validation under a logic junctor in a structural schema.
|
||||
func validateNestedValueValidation(v *NestedValueValidation, skipAnyOf, skipAllOfAnyOf bool, fldPath *field.Path) field.ErrorList {
|
||||
if v == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
allErrs := field.ErrorList{}
|
||||
|
||||
allErrs = append(allErrs, validateValueValidation(&v.ValueValidation, skipAnyOf, skipAllOfAnyOf, fldPath)...)
|
||||
allErrs = append(allErrs, validateNestedValueValidation(v.Items, false, false, fldPath.Child("items"))...)
|
||||
|
||||
for k, fld := range v.Properties {
|
||||
allErrs = append(allErrs, validateNestedValueValidation(&fld, false, false, fldPath.Child("properties").Key(k))...)
|
||||
}
|
||||
|
||||
if len(v.ForbiddenGenerics.Type) > 0 {
|
||||
allErrs = append(allErrs, field.Forbidden(fldPath.Child("type"), "must be empty to be structural"))
|
||||
}
|
||||
if v.ForbiddenGenerics.AdditionalProperties != nil {
|
||||
allErrs = append(allErrs, field.Forbidden(fldPath.Child("additionalProperties"), "must be undefined to be structural"))
|
||||
}
|
||||
if v.ForbiddenGenerics.Default.Object != nil {
|
||||
allErrs = append(allErrs, field.Forbidden(fldPath.Child("default"), "must be undefined to be structural"))
|
||||
}
|
||||
if len(v.ForbiddenGenerics.Title) > 0 {
|
||||
allErrs = append(allErrs, field.Forbidden(fldPath.Child("title"), "must be empty to be structural"))
|
||||
}
|
||||
if len(v.ForbiddenGenerics.Description) > 0 {
|
||||
allErrs = append(allErrs, field.Forbidden(fldPath.Child("description"), "must be empty to be structural"))
|
||||
}
|
||||
if v.ForbiddenGenerics.Nullable {
|
||||
allErrs = append(allErrs, field.Forbidden(fldPath.Child("nullable"), "must be false to be structural"))
|
||||
}
|
||||
|
||||
if v.ForbiddenExtensions.XPreserveUnknownFields {
|
||||
allErrs = append(allErrs, field.Forbidden(fldPath.Child("x-kubernetes-preserve-unknown-fields"), "must be false to be structural"))
|
||||
}
|
||||
if v.ForbiddenExtensions.XEmbeddedResource {
|
||||
allErrs = append(allErrs, field.Forbidden(fldPath.Child("x-kubernetes-embedded-resource"), "must be false to be structural"))
|
||||
}
|
||||
if v.ForbiddenExtensions.XIntOrString {
|
||||
allErrs = append(allErrs, field.Forbidden(fldPath.Child("x-kubernetes-int-or-string"), "must be false to be structural"))
|
||||
}
|
||||
|
||||
return allErrs
|
||||
}
|
@ -0,0 +1,71 @@
|
||||
/*
|
||||
Copyright 2019 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package schema
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
fuzz "github.com/google/gofuzz"
|
||||
|
||||
"k8s.io/apimachinery/pkg/util/rand"
|
||||
)
|
||||
|
||||
func TestValidateNestedValueValidationComplete(t *testing.T) {
|
||||
fuzzer := fuzz.New()
|
||||
fuzzer.Funcs(
|
||||
func(s *JSON, c fuzz.Continue) {
|
||||
if c.RandBool() {
|
||||
s.Object = float64(42.0)
|
||||
}
|
||||
},
|
||||
func(s **StructuralOrBool, c fuzz.Continue) {
|
||||
if c.RandBool() {
|
||||
*s = &StructuralOrBool{}
|
||||
}
|
||||
},
|
||||
)
|
||||
fuzzer.NilChance(0)
|
||||
|
||||
// check that we didn't forget to check any forbidden generic field
|
||||
tt := reflect.TypeOf(Generic{})
|
||||
for i := 0; i < tt.NumField(); i++ {
|
||||
vv := &NestedValueValidation{}
|
||||
x := reflect.ValueOf(&vv.ForbiddenGenerics).Elem()
|
||||
i := rand.Intn(x.NumField())
|
||||
fuzzer.Fuzz(x.Field(i).Addr().Interface())
|
||||
|
||||
errs := validateNestedValueValidation(vv, false, false, nil)
|
||||
if len(errs) == 0 && !reflect.DeepEqual(vv.ForbiddenGenerics, Generic{}) {
|
||||
t.Errorf("expected ForbiddenGenerics validation errors for: %#v", vv)
|
||||
}
|
||||
}
|
||||
|
||||
// check that we didn't forget to check any forbidden extension field
|
||||
tt = reflect.TypeOf(Extensions{})
|
||||
for i := 0; i < tt.NumField(); i++ {
|
||||
vv := &NestedValueValidation{}
|
||||
x := reflect.ValueOf(&vv.ForbiddenExtensions).Elem()
|
||||
i := rand.Intn(x.NumField())
|
||||
fuzzer.Fuzz(x.Field(i).Addr().Interface())
|
||||
|
||||
errs := validateNestedValueValidation(vv, false, false, nil)
|
||||
if len(errs) == 0 && !reflect.DeepEqual(vv.ForbiddenExtensions, Extensions{}) {
|
||||
t.Errorf("expected ForbiddenExtensions validation errors for: %#v", vv)
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user