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
|
return allErrs
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// WARNING: if anything new is allowed below, NewStructural must be adapted to support it.
|
||||||
|
//
|
||||||
|
|
||||||
if schema.Default != nil {
|
if schema.Default != nil {
|
||||||
allErrs = append(allErrs, field.Forbidden(fldPath.Child("default"), "default is not supported"))
|
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