Unverified Commit 8efea56a authored by Kubernetes Prow Robot's avatar Kubernetes Prow Robot Committed by GitHub

Merge pull request #77207 from sttts/sttts-structural-schema

apiextensions: implement structural schema condition
parents b27fe7f4 c836a251
......@@ -30,6 +30,9 @@ API rule violation: names_match,k8s.io/apiextensions-apiserver/pkg/apis/apiexten
API rule violation: names_match,k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1,JSON,Raw
API rule violation: names_match,k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1,JSONSchemaProps,Ref
API rule violation: names_match,k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1,JSONSchemaProps,Schema
API rule violation: names_match,k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1,JSONSchemaProps,XEmbeddedResource
API rule violation: names_match,k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1,JSONSchemaProps,XIntOrString
API rule violation: names_match,k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1,JSONSchemaProps,XPreserveUnknownFields
API rule violation: names_match,k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1,JSONSchemaPropsOrArray,JSONSchemas
API rule violation: names_match,k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1,JSONSchemaPropsOrArray,Schema
API rule violation: names_match,k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1,JSONSchemaPropsOrBool,Allows
......
......@@ -16808,6 +16808,18 @@
},
"uniqueItems": {
"type": "boolean"
},
"x-kubernetes-embedded-resource": {
"description": "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. kind, apiVersion and metadata are validated automatically. x-kubernetes-preserve-unknown-fields is allowed to be true, but does not have to be if the object is fully specified (up to kind, apiVersion, metadata).",
"type": "boolean"
},
"x-kubernetes-int-or-string": {
"description": "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:\n\n1) anyOf:\n - type: integer\n - type: string\n2) allOf:\n - anyOf:\n - type: integer\n - type: string\n - ... zero or more",
"type": "boolean"
},
"x-kubernetes-preserve-unknown-fields": {
"description": "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.",
"type": "boolean"
}
},
"type": "object"
......@@ -47,6 +47,7 @@ filegroup(
"//staging/src/k8s.io/apiextensions-apiserver/pkg/cmd/server:all-srcs",
"//staging/src/k8s.io/apiextensions-apiserver/pkg/controller/establish:all-srcs",
"//staging/src/k8s.io/apiextensions-apiserver/pkg/controller/finalizer:all-srcs",
"//staging/src/k8s.io/apiextensions-apiserver/pkg/controller/nonstructuralschema:all-srcs",
"//staging/src/k8s.io/apiextensions-apiserver/pkg/controller/openapi:all-srcs",
"//staging/src/k8s.io/apiextensions-apiserver/pkg/controller/status:all-srcs",
"//staging/src/k8s.io/apiextensions-apiserver/pkg/crdserverscheme:all-srcs",
......
......@@ -264,6 +264,22 @@ const (
// NamesAccepted means the names chosen for this CustomResourceDefinition do not conflict with others in
// the group and are therefore accepted.
NamesAccepted CustomResourceDefinitionConditionType = "NamesAccepted"
// NonStructuralSchema means that one or more OpenAPI schema is not structural.
//
// A schema is structural if it specifies types for all values, with the only exceptions of those with
// - x-kubernetes-int-or-string: true — for fields which can be integer or string
// - x-kubernetes-preserve-unknown-fields: true — for raw, unspecified JSON values
// and there is no type, additionalProperties, default, nullable or x-kubernetes-* vendor extenions
// specified under allOf, anyOf, oneOf or not.
//
// Non-structural schemas will not be allowed anymore in v1 API groups. Moreover, new features will not be
// available for non-structural CRDs:
// - pruning
// - defaulting
// - read-only
// - OpenAPI publishing
// - webhook conversion
NonStructuralSchema CustomResourceDefinitionConditionType = "NonStructuralSchema"
// Terminating means that the CustomResourceDefinition has been deleted and is cleaning up.
Terminating CustomResourceDefinitionConditionType = "Terminating"
)
......
......@@ -55,6 +55,36 @@ type JSONSchemaProps struct {
Definitions JSONSchemaDefinitions
ExternalDocs *ExternalDocumentation
Example *JSON
// 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
}
// JSON represents any valid JSON value.
......
......@@ -443,6 +443,37 @@ message JSONSchemaProps {
optional JSON example = 36;
optional bool nullable = 37;
// 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.
optional bool xKubernetesPreserveUnknownFields = 38;
// 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. kind, apiVersion and metadata
// are validated automatically. x-kubernetes-preserve-unknown-fields
// is allowed to be true, but does not have to be if the object
// is fully specified (up to kind, apiVersion, metadata).
optional bool xKubernetesEmbeddedResource = 39;
// 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
optional bool xKubernetesIntOrString = 40;
}
// JSONSchemaPropsOrArray represents a value that can either be a JSONSchemaProps
......
......@@ -279,6 +279,22 @@ const (
// NamesAccepted means the names chosen for this CustomResourceDefinition do not conflict with others in
// the group and are therefore accepted.
NamesAccepted CustomResourceDefinitionConditionType = "NamesAccepted"
// NonStructuralSchema means that one or more OpenAPI schema is not structural.
//
// A schema is structural if it specifies types for all values, with the only exceptions of those with
// - x-kubernetes-int-or-string: true — for fields which can be integer or string
// - x-kubernetes-preserve-unknown-fields: true — for raw, unspecified JSON values
// and there is no type, additionalProperties, default, nullable or x-kubernetes-* vendor extenions
// specified under allOf, anyOf, oneOf or not.
//
// Non-structural schemas will not be allowed anymore in v1 API groups. Moreover, new features will not be
// available for non-structural CRDs:
// - pruning
// - defaulting
// - read-only
// - OpenAPI publishing
// - webhook conversion
NonStructuralSchema CustomResourceDefinitionConditionType = "NonStructuralSchema"
// Terminating means that the CustomResourceDefinition has been deleted and is cleaning up.
Terminating CustomResourceDefinitionConditionType = "Terminating"
)
......
......@@ -55,6 +55,37 @@ type JSONSchemaProps struct {
ExternalDocs *ExternalDocumentation `json:"externalDocs,omitempty" protobuf:"bytes,35,opt,name=externalDocs"`
Example *JSON `json:"example,omitempty" protobuf:"bytes,36,opt,name=example"`
Nullable bool `json:"nullable,omitempty" protobuf:"bytes,37,opt,name=nullable"`
// 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 `json:"x-kubernetes-preserve-unknown-fields,omitempty" protobuf:"bytes,38,opt,name=xKubernetesPreserveUnknownFields"`
// 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. kind, apiVersion and metadata
// are validated automatically. x-kubernetes-preserve-unknown-fields
// is allowed to be true, but does not have to be if the object
// is fully specified (up to kind, apiVersion, metadata).
XEmbeddedResource bool `json:"x-kubernetes-embedded-resource,omitempty" protobuf:"bytes,39,opt,name=xKubernetesEmbeddedResource"`
// 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 `json:"x-kubernetes-int-or-string,omitempty" protobuf:"bytes,40,opt,name=xKubernetesIntOrString"`
}
// JSON represents any valid JSON value.
......
......@@ -938,6 +938,9 @@ func autoConvert_v1beta1_JSONSchemaProps_To_apiextensions_JSONSchemaProps(in *JS
out.Example = nil
}
out.Nullable = in.Nullable
out.XPreserveUnknownFields = in.XPreserveUnknownFields
out.XEmbeddedResource = in.XEmbeddedResource
out.XIntOrString = in.XIntOrString
return nil
}
......@@ -1120,6 +1123,9 @@ func autoConvert_apiextensions_JSONSchemaProps_To_v1beta1_JSONSchemaProps(in *ap
} else {
out.Example = nil
}
out.XPreserveUnknownFields = in.XPreserveUnknownFields
out.XEmbeddedResource = in.XEmbeddedResource
out.XIntOrString = in.XIntOrString
return nil
}
......
......@@ -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"))
}
......@@ -786,7 +790,7 @@ func validateSimpleJSONPath(s string, fldPath *field.Path) field.ErrorList {
return allErrs
}
var allowedFieldsAtRootSchema = []string{"Description", "Type", "Format", "Title", "Maximum", "ExclusiveMaximum", "Minimum", "ExclusiveMinimum", "MaxLength", "MinLength", "Pattern", "MaxItems", "MinItems", "UniqueItems", "MultipleOf", "Required", "Items", "Properties", "ExternalDocs", "Example"}
var allowedFieldsAtRootSchema = []string{"Description", "Type", "Format", "Title", "Maximum", "ExclusiveMaximum", "Minimum", "ExclusiveMinimum", "MaxLength", "MinLength", "Pattern", "MaxItems", "MinItems", "UniqueItems", "MultipleOf", "Required", "Items", "Properties", "ExternalDocs", "Example", "XPreserveUnknownFields"}
func allowedAtRootSchema(field string) bool {
for _, v := range allowedFieldsAtRootSchema {
......
......@@ -32,6 +32,7 @@ go_library(
"//staging/src/k8s.io/apiextensions-apiserver/pkg/client/listers/apiextensions/internalversion:go_default_library",
"//staging/src/k8s.io/apiextensions-apiserver/pkg/controller/establish:go_default_library",
"//staging/src/k8s.io/apiextensions-apiserver/pkg/controller/finalizer:go_default_library",
"//staging/src/k8s.io/apiextensions-apiserver/pkg/controller/nonstructuralschema:go_default_library",
"//staging/src/k8s.io/apiextensions-apiserver/pkg/controller/openapi:go_default_library",
"//staging/src/k8s.io/apiextensions-apiserver/pkg/controller/status:go_default_library",
"//staging/src/k8s.io/apiextensions-apiserver/pkg/crdserverscheme:go_default_library",
......@@ -119,6 +120,7 @@ filegroup(
srcs = [
":package-srcs",
"//staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/conversion:all-srcs",
"//staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema:all-srcs",
"//staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/validation:all-srcs",
],
tags = ["automanaged"],
......
......@@ -30,6 +30,7 @@ import (
internalinformers "k8s.io/apiextensions-apiserver/pkg/client/informers/internalversion"
"k8s.io/apiextensions-apiserver/pkg/controller/establish"
"k8s.io/apiextensions-apiserver/pkg/controller/finalizer"
"k8s.io/apiextensions-apiserver/pkg/controller/nonstructuralschema"
openapicontroller "k8s.io/apiextensions-apiserver/pkg/controller/openapi"
"k8s.io/apiextensions-apiserver/pkg/controller/status"
apiextensionsfeatures "k8s.io/apiextensions-apiserver/pkg/features"
......@@ -195,6 +196,7 @@ func (c completedConfig) New(delegationTarget genericapiserver.DelegationTarget)
crdController := NewDiscoveryController(s.Informers.Apiextensions().InternalVersion().CustomResourceDefinitions(), versionDiscoveryHandler, groupDiscoveryHandler)
namingController := status.NewNamingConditionController(s.Informers.Apiextensions().InternalVersion().CustomResourceDefinitions(), crdClient.Apiextensions())
nonStructuralSchemaController := nonstructuralschema.NewConditionController(s.Informers.Apiextensions().InternalVersion().CustomResourceDefinitions(), crdClient.Apiextensions())
finalizingController := finalizer.NewCRDFinalizer(
s.Informers.Apiextensions().InternalVersion().CustomResourceDefinitions(),
crdClient.Apiextensions(),
......@@ -217,6 +219,7 @@ func (c completedConfig) New(delegationTarget genericapiserver.DelegationTarget)
go crdController.Run(context.StopCh)
go namingController.Run(context.StopCh)
go establishingController.Run(context.StopCh)
go nonStructuralSchemaController.Run(5, context.StopCh)
go finalizingController.Run(5, context.StopCh)
return nil
})
......
load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test")
go_library(
name = "go_default_library",
srcs = [
"complete.go",
"convert.go",
"structural.go",
"validation.go",
"zz_generated.deepcopy.go",
],
importmap = "k8s.io/kubernetes/vendor/k8s.io/apiextensions-apiserver/pkg/apiserver/schema",
importpath = "k8s.io/apiextensions-apiserver/pkg/apiserver/schema",
visibility = ["//visibility:public"],
deps = [
"//staging/src/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions:go_default_library",
"//staging/src/k8s.io/apimachinery/pkg/runtime:go_default_library",
"//staging/src/k8s.io/apimachinery/pkg/util/validation/field:go_default_library",
],
)
filegroup(
name = "package-srcs",
srcs = glob(["**"]),
tags = ["automanaged"],
visibility = ["//visibility:private"],
)
filegroup(
name = "all-srcs",
srcs = [":package-srcs"],
tags = ["automanaged"],
visibility = ["//visibility:public"],
)
go_test(
name = "go_default_test",
srcs = ["validation_test.go"],
embed = [":go_default_library"],
deps = [
"//staging/src/k8s.io/apimachinery/pkg/util/rand:go_default_library",
"//vendor/github.com/google/gofuzz:go_default_library",
],
)
/*
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
}
/*
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
}
/*
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)
}
/*
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.
// * additionalProperties at the root is not allowed.
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, lvl, 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, lvl level, fldPath *field.Path) field.ErrorList {
if g == nil {
return nil
}
allErrs := field.ErrorList{}
if g.AdditionalProperties != nil {
if lvl == rootLevel {
allErrs = append(allErrs, field.Forbidden(fldPath.Child("additionalProperties"), "must not be used at the root"))
}
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
}
/*
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)
}
}
}
// +build !ignore_autogenerated
/*
Copyright 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.
*/
// Code generated by deepcopy-gen. DO NOT EDIT.
package schema
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *Extensions) DeepCopyInto(out *Extensions) {
*out = *in
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Extensions.
func (in *Extensions) DeepCopy() *Extensions {
if in == nil {
return nil
}
out := new(Extensions)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *Generic) DeepCopyInto(out *Generic) {
*out = *in
out.Default = in.Default.DeepCopy()
if in.AdditionalProperties != nil {
in, out := &in.AdditionalProperties, &out.AdditionalProperties
*out = new(StructuralOrBool)
(*in).DeepCopyInto(*out)
}
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Generic.
func (in *Generic) DeepCopy() *Generic {
if in == nil {
return nil
}
out := new(Generic)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *NestedValueValidation) DeepCopyInto(out *NestedValueValidation) {
*out = *in
in.ValueValidation.DeepCopyInto(&out.ValueValidation)
if in.Items != nil {
in, out := &in.Items, &out.Items
*out = new(NestedValueValidation)
(*in).DeepCopyInto(*out)
}
if in.Properties != nil {
in, out := &in.Properties, &out.Properties
*out = make(map[string]NestedValueValidation, len(*in))
for key, val := range *in {
(*out)[key] = *val.DeepCopy()
}
}
in.ForbiddenGenerics.DeepCopyInto(&out.ForbiddenGenerics)
out.ForbiddenExtensions = in.ForbiddenExtensions
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NestedValueValidation.
func (in *NestedValueValidation) DeepCopy() *NestedValueValidation {
if in == nil {
return nil
}
out := new(NestedValueValidation)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *Structural) DeepCopyInto(out *Structural) {
*out = *in
if in.Items != nil {
in, out := &in.Items, &out.Items
*out = new(Structural)
(*in).DeepCopyInto(*out)
}
if in.Properties != nil {
in, out := &in.Properties, &out.Properties
*out = make(map[string]Structural, len(*in))
for key, val := range *in {
(*out)[key] = *val.DeepCopy()
}
}
in.Generic.DeepCopyInto(&out.Generic)
out.Extensions = in.Extensions
if in.ValueValidation != nil {
in, out := &in.ValueValidation, &out.ValueValidation
*out = new(ValueValidation)
(*in).DeepCopyInto(*out)
}
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Structural.
func (in *Structural) DeepCopy() *Structural {
if in == nil {
return nil
}
out := new(Structural)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *StructuralOrBool) DeepCopyInto(out *StructuralOrBool) {
*out = *in
if in.Structural != nil {
in, out := &in.Structural, &out.Structural
*out = new(Structural)
(*in).DeepCopyInto(*out)
}
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new StructuralOrBool.
func (in *StructuralOrBool) DeepCopy() *StructuralOrBool {
if in == nil {
return nil
}
out := new(StructuralOrBool)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *ValueValidation) DeepCopyInto(out *ValueValidation) {
*out = *in
if in.Maximum != nil {
in, out := &in.Maximum, &out.Maximum
*out = new(float64)
**out = **in
}
if in.Minimum != nil {
in, out := &in.Minimum, &out.Minimum
*out = new(float64)
**out = **in
}
if in.MaxLength != nil {
in, out := &in.MaxLength, &out.MaxLength
*out = new(int64)
**out = **in
}
if in.MinLength != nil {
in, out := &in.MinLength, &out.MinLength
*out = new(int64)
**out = **in
}
if in.MaxItems != nil {
in, out := &in.MaxItems, &out.MaxItems
*out = new(int64)
**out = **in
}
if in.MinItems != nil {
in, out := &in.MinItems, &out.MinItems
*out = new(int64)
**out = **in
}
if in.MultipleOf != nil {
in, out := &in.MultipleOf, &out.MultipleOf
*out = new(float64)
**out = **in
}
if in.Enum != nil {
in, out := &in.Enum, &out.Enum
*out = make([]JSON, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
if in.MaxProperties != nil {
in, out := &in.MaxProperties, &out.MaxProperties
*out = new(int64)
**out = **in
}
if in.MinProperties != nil {
in, out := &in.MinProperties, &out.MinProperties
*out = new(int64)
**out = **in
}
if in.Required != nil {
in, out := &in.Required, &out.Required
*out = make([]string, len(*in))
copy(*out, *in)
}
if in.AllOf != nil {
in, out := &in.AllOf, &out.AllOf
*out = make([]NestedValueValidation, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
if in.OneOf != nil {
in, out := &in.OneOf, &out.OneOf
*out = make([]NestedValueValidation, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
if in.AnyOf != nil {
in, out := &in.AnyOf, &out.AnyOf
*out = make([]NestedValueValidation, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
if in.Not != nil {
in, out := &in.Not, &out.Not
*out = new(NestedValueValidation)
(*in).DeepCopyInto(*out)
}
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ValueValidation.
func (in *ValueValidation) DeepCopy() *ValueValidation {
if in == nil {
return nil
}
out := new(ValueValidation)
in.DeepCopyInto(out)
return out
}
......@@ -194,6 +194,16 @@ func ConvertJSONSchemaPropsWithPostProcess(in *apiextensions.JSONSchemaProps, ou
}
}
if in.XPreserveUnknownFields {
out.VendorExtensible.AddExtension("x-kubernetes-preserve-unknown-fields", true)
}
if in.XEmbeddedResource {
out.VendorExtensible.AddExtension("x-kubernetes-embedded-resource", true)
}
if in.XIntOrString {
out.VendorExtensible.AddExtension("x-kubernetes-int-or-string", true)
}
return nil
}
......
load("@io_bazel_rules_go//go:def.bzl", "go_library")
go_library(
name = "go_default_library",
srcs = ["nonstructuralschema_controller.go"],
importmap = "k8s.io/kubernetes/vendor/k8s.io/apiextensions-apiserver/pkg/controller/nonstructuralschema",
importpath = "k8s.io/apiextensions-apiserver/pkg/controller/nonstructuralschema",
visibility = ["//visibility:public"],
deps = [
"//staging/src/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions:go_default_library",
"//staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema:go_default_library",
"//staging/src/k8s.io/apiextensions-apiserver/pkg/client/clientset/internalclientset/typed/apiextensions/internalversion:go_default_library",
"//staging/src/k8s.io/apiextensions-apiserver/pkg/client/informers/internalversion/apiextensions/internalversion:go_default_library",
"//staging/src/k8s.io/apiextensions-apiserver/pkg/client/listers/apiextensions/internalversion:go_default_library",
"//staging/src/k8s.io/apimachinery/pkg/api/errors:go_default_library",
"//staging/src/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
"//staging/src/k8s.io/apimachinery/pkg/util/runtime:go_default_library",
"//staging/src/k8s.io/apimachinery/pkg/util/validation/field:go_default_library",
"//staging/src/k8s.io/apimachinery/pkg/util/wait:go_default_library",
"//staging/src/k8s.io/client-go/tools/cache:go_default_library",
"//staging/src/k8s.io/client-go/util/workqueue:go_default_library",
"//vendor/k8s.io/klog:go_default_library",
],
)
filegroup(
name = "package-srcs",
srcs = glob(["**"]),
tags = ["automanaged"],
visibility = ["//visibility:private"],
)
filegroup(
name = "all-srcs",
srcs = [":package-srcs"],
tags = ["automanaged"],
visibility = ["//visibility:public"],
)
/*
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 nonstructuralschema
import (
"fmt"
"sort"
"time"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
"k8s.io/apimachinery/pkg/util/validation/field"
"k8s.io/apimachinery/pkg/util/wait"
"k8s.io/client-go/tools/cache"
"k8s.io/client-go/util/workqueue"
"k8s.io/klog"
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions"
"k8s.io/apiextensions-apiserver/pkg/apiserver/schema"
client "k8s.io/apiextensions-apiserver/pkg/client/clientset/internalclientset/typed/apiextensions/internalversion"
informers "k8s.io/apiextensions-apiserver/pkg/client/informers/internalversion/apiextensions/internalversion"
listers "k8s.io/apiextensions-apiserver/pkg/client/listers/apiextensions/internalversion"
)
// ConditionController is maintaining the NonStructuralSchema condition.
type ConditionController struct {
crdClient client.CustomResourceDefinitionsGetter
crdLister listers.CustomResourceDefinitionLister
crdSynced cache.InformerSynced
// To allow injection for testing.
syncFn func(key string) error
queue workqueue.RateLimitingInterface
}
// NewConditionController constructs a non-structural schema condition controller.
func NewConditionController(
crdInformer informers.CustomResourceDefinitionInformer,
crdClient client.CustomResourceDefinitionsGetter,
) *ConditionController {
c := &ConditionController{
crdClient: crdClient,
crdLister: crdInformer.Lister(),
crdSynced: crdInformer.Informer().HasSynced,
queue: workqueue.NewNamedRateLimitingQueue(workqueue.DefaultControllerRateLimiter(), "non_structural_schema_condition_controller"),
}
crdInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{
AddFunc: c.addCustomResourceDefinition,
UpdateFunc: c.updateCustomResourceDefinition,
DeleteFunc: nil,
})
c.syncFn = c.sync
return c
}
func calculateCondition(in *apiextensions.CustomResourceDefinition) *apiextensions.CustomResourceDefinitionCondition {
cond := &apiextensions.CustomResourceDefinitionCondition{
Type: apiextensions.NonStructuralSchema,
Status: apiextensions.ConditionUnknown,
}
allErrs := field.ErrorList{}
if in.Spec.Validation != nil && in.Spec.Validation.OpenAPIV3Schema != nil {
s, err := schema.NewStructural(in.Spec.Validation.OpenAPIV3Schema)
if err != nil {
cond.Reason = "StructuralError"
cond.Message = fmt.Sprintf("failed to check global validation schema: %v", err)
return cond
}
pth := field.NewPath("spec", "validation", "openAPIV3Schema")
allErrs = append(allErrs, schema.ValidateStructural(s, pth)...)
}
for _, v := range in.Spec.Versions {
if v.Schema == nil || v.Schema.OpenAPIV3Schema == nil {
continue
}
s, err := schema.NewStructural(v.Schema.OpenAPIV3Schema)
if err != nil {
cond.Reason = "StructuralError"
cond.Message = fmt.Sprintf("failed to check validation schema for version %s: %v", v.Name, err)
return cond
}
pth := field.NewPath("spec", "version").Key(v.Name).Child("schema", "openAPIV3Schema")
allErrs = append(allErrs, schema.ValidateStructural(s, pth)...)
}
if len(allErrs) == 0 {
return nil
}
// sort error messages. Otherwise, the condition message will change every sync due to
// randomized map iteration.
sort.Slice(allErrs, func(i, j int) bool {
return allErrs[i].Error() < allErrs[j].Error()
})
cond.Status = apiextensions.ConditionTrue
cond.Reason = "Violations"
cond.Message = allErrs.ToAggregate().Error()
return cond
}
func (c *ConditionController) sync(key string) error {
inCustomResourceDefinition, err := c.crdLister.Get(key)
if apierrors.IsNotFound(err) {
return nil
}
if err != nil {
return err
}
// check old condition
cond := calculateCondition(inCustomResourceDefinition)
old := apiextensions.FindCRDCondition(inCustomResourceDefinition, apiextensions.NonStructuralSchema)
if cond == nil && old == nil {
return nil
}
if cond != nil && old != nil && old.Status == cond.Status && old.Reason == cond.Reason && old.Message == cond.Message {
return nil
}
// update condition
crd := inCustomResourceDefinition.DeepCopy()
if cond == nil {
apiextensions.RemoveCRDCondition(crd, apiextensions.NonStructuralSchema)
} else {
cond.LastTransitionTime = metav1.NewTime(time.Now())
apiextensions.SetCRDCondition(crd, *cond)
}
_, err = c.crdClient.CustomResourceDefinitions().UpdateStatus(crd)
if apierrors.IsNotFound(err) || apierrors.IsConflict(err) {
// deleted or changed in the meantime, we'll get called again
return nil
}
if err != nil {
return err
}
return nil
}
// Run starts the controller.
func (c *ConditionController) Run(threadiness int, stopCh <-chan struct{}) {
defer utilruntime.HandleCrash()
defer c.queue.ShutDown()
klog.Infof("Starting NonStructuralSchemaConditionController")
defer klog.Infof("Shutting down NonStructuralSchemaConditionController")
if !cache.WaitForCacheSync(stopCh, c.crdSynced) {
return
}
for i := 0; i < threadiness; i++ {
go wait.Until(c.runWorker, time.Second, stopCh)
}
<-stopCh
}
func (c *ConditionController) runWorker() {
for c.processNextWorkItem() {
}
}
// processNextWorkItem deals with one key off the queue. It returns false when it's time to quit.
func (c *ConditionController) processNextWorkItem() bool {
key, quit := c.queue.Get()
if quit {
return false
}
defer c.queue.Done(key)
err := c.syncFn(key.(string))
if err == nil {
c.queue.Forget(key)
return true
}
utilruntime.HandleError(fmt.Errorf("%v failed with: %v", key, err))
c.queue.AddRateLimited(key)
return true
}
func (c *ConditionController) enqueue(obj *apiextensions.CustomResourceDefinition) {
key, err := cache.DeletionHandlingMetaNamespaceKeyFunc(obj)
if err != nil {
utilruntime.HandleError(fmt.Errorf("Couldn't get key for object %#v: %v", obj, err))
return
}
c.queue.Add(key)
}
func (c *ConditionController) addCustomResourceDefinition(obj interface{}) {
castObj := obj.(*apiextensions.CustomResourceDefinition)
klog.V(4).Infof("Adding %s", castObj.Name)
c.enqueue(castObj)
}
func (c *ConditionController) updateCustomResourceDefinition(obj, _ interface{}) {
castObj := obj.(*apiextensions.CustomResourceDefinition)
klog.V(4).Infof("Updating %s", castObj.Name)
c.enqueue(castObj)
}
......@@ -26,6 +26,7 @@ go_test(
"//staging/src/k8s.io/api/autoscaling/v1:go_default_library",
"//staging/src/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1:go_default_library",
"//staging/src/k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset:go_default_library",
"//staging/src/k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/scheme:go_default_library",
"//staging/src/k8s.io/apiextensions-apiserver/pkg/cmd/server/options:go_default_library",
"//staging/src/k8s.io/apiextensions-apiserver/pkg/features:go_default_library",
"//staging/src/k8s.io/apiextensions-apiserver/test/integration/fixtures:go_default_library",
......@@ -40,6 +41,7 @@ go_test(
"//staging/src/k8s.io/apimachinery/pkg/types:go_default_library",
"//staging/src/k8s.io/apimachinery/pkg/util/json:go_default_library",
"//staging/src/k8s.io/apimachinery/pkg/util/wait:go_default_library",
"//staging/src/k8s.io/apimachinery/pkg/util/yaml:go_default_library",
"//staging/src/k8s.io/apimachinery/pkg/watch:go_default_library",
"//staging/src/k8s.io/apiserver/pkg/endpoints/request:go_default_library",
"//staging/src/k8s.io/apiserver/pkg/features:go_default_library",
......
......@@ -1066,6 +1066,7 @@ k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1
k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/validation
k8s.io/apiextensions-apiserver/pkg/apiserver
k8s.io/apiextensions-apiserver/pkg/apiserver/conversion
k8s.io/apiextensions-apiserver/pkg/apiserver/schema
k8s.io/apiextensions-apiserver/pkg/apiserver/validation
k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset
k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/scheme
......@@ -1087,6 +1088,7 @@ k8s.io/apiextensions-apiserver/pkg/cmd/server/options
k8s.io/apiextensions-apiserver/pkg/cmd/server/testing
k8s.io/apiextensions-apiserver/pkg/controller/establish
k8s.io/apiextensions-apiserver/pkg/controller/finalizer
k8s.io/apiextensions-apiserver/pkg/controller/nonstructuralschema
k8s.io/apiextensions-apiserver/pkg/controller/openapi
k8s.io/apiextensions-apiserver/pkg/controller/status
k8s.io/apiextensions-apiserver/pkg/crdserverscheme
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment