diff --git a/staging/src/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1/BUILD b/staging/src/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1/BUILD index 421014e87c8..93a07cfe7a1 100644 --- a/staging/src/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1/BUILD +++ b/staging/src/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1/BUILD @@ -24,6 +24,7 @@ go_library( ], deps = [ "//vendor/github.com/gogo/protobuf/proto:go_default_library", + "//vendor/github.com/gogo/protobuf/sortkeys:go_default_library", "//vendor/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions:go_default_library", "//vendor/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library", "//vendor/k8s.io/apimachinery/pkg/conversion:go_default_library", diff --git a/staging/src/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/validation/BUILD b/staging/src/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/validation/BUILD index 53c042aeaf6..201115a4ef4 100644 --- a/staging/src/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/validation/BUILD +++ b/staging/src/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/validation/BUILD @@ -10,10 +10,13 @@ go_library( name = "go_default_library", srcs = ["validation.go"], deps = [ + "//vendor/github.com/go-openapi/spec:go_default_library", "//vendor/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions:go_default_library", + "//vendor/k8s.io/apiextensions-apiserver/pkg/features:go_default_library", "//vendor/k8s.io/apimachinery/pkg/api/validation:go_default_library", "//vendor/k8s.io/apimachinery/pkg/util/validation:go_default_library", "//vendor/k8s.io/apimachinery/pkg/util/validation/field:go_default_library", + "//vendor/k8s.io/apiserver/pkg/util/feature:go_default_library", ], ) diff --git a/staging/src/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/validation/validation.go b/staging/src/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/validation/validation.go index 6623b0478a8..f064b99333c 100644 --- a/staging/src/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/validation/validation.go +++ b/staging/src/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/validation/validation.go @@ -20,10 +20,15 @@ import ( "fmt" "strings" - "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions" + "github.com/go-openapi/spec" + genericvalidation "k8s.io/apimachinery/pkg/api/validation" validationutil "k8s.io/apimachinery/pkg/util/validation" "k8s.io/apimachinery/pkg/util/validation/field" + utilfeature "k8s.io/apiserver/pkg/util/feature" + + "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions" + apiextensionsfeatures "k8s.io/apiextensions-apiserver/pkg/features" ) // ValidateCustomResourceDefinition statically validates @@ -100,6 +105,12 @@ func ValidateCustomResourceDefinitionSpec(spec *apiextensions.CustomResourceDefi allErrs = append(allErrs, ValidateCustomResourceDefinitionNames(&spec.Names, fldPath.Child("names"))...) + if utilfeature.DefaultFeatureGate.Enabled(apiextensionsfeatures.CustomResourceValidation) { + allErrs = append(allErrs, ValidateCustomResourceDefinitionValidation(spec.Validation, fldPath.Child("validation"))...) + } else if spec.Validation != nil { + allErrs = append(allErrs, field.Forbidden(fldPath.Child("validation"), "disabled by feature-gate")) + } + return allErrs } @@ -158,3 +169,162 @@ func ValidateCustomResourceDefinitionNames(names *apiextensions.CustomResourceDe return allErrs } + +// specStandardValidator applies validations for different OpenAPI specfication versions. +type specStandardValidator interface { + validate(spec *apiextensions.JSONSchemaProps, fldPath *field.Path) field.ErrorList +} + +// ValidateCustomResourceDefinitionValidation statically validates +func ValidateCustomResourceDefinitionValidation(customResourceValidation *apiextensions.CustomResourceValidation, fldPath *field.Path) field.ErrorList { + allErrs := field.ErrorList{} + + if customResourceValidation == nil { + return allErrs + } + + if customResourceValidation.OpenAPIV3Schema != nil { + openAPIV3Schema := &specStandardValidatorV3{} + allErrs = append(allErrs, ValidateCustomResourceDefinitionOpenAPISchema(customResourceValidation.OpenAPIV3Schema, fldPath.Child("openAPIV3Schema"), openAPIV3Schema)...) + } + + return allErrs +} + +// ValidateCustomResourceDefinitionOpenAPISchema statically validates +func ValidateCustomResourceDefinitionOpenAPISchema(schema *apiextensions.JSONSchemaProps, fldPath *field.Path, ssv specStandardValidator) field.ErrorList { + allErrs := field.ErrorList{} + + if schema == nil { + return allErrs + } + + allErrs = append(allErrs, ssv.validate(schema, fldPath)...) + + if schema.UniqueItems == true { + allErrs = append(allErrs, field.Forbidden(fldPath.Child("uniqueItems"), "uniqueItems cannot be set to true since the runtime complexity becomes quadratic")) + } + + // additionalProperties contradicts Kubernetes API convention to ignore unknown fields + if schema.AdditionalProperties != nil { + if schema.AdditionalProperties.Allows == false { + allErrs = append(allErrs, field.Forbidden(fldPath.Child("additionalProperties"), "additionalProperties cannot be set to false")) + } + allErrs = append(allErrs, ValidateCustomResourceDefinitionOpenAPISchema(schema.AdditionalProperties.Schema, fldPath.Child("additionalProperties"), ssv)...) + } + + if schema.Ref != nil { + openapiRef, err := spec.NewRef(*schema.Ref) + if err != nil { + allErrs = append(allErrs, field.Invalid(fldPath.Child("ref"), *schema.Ref, err.Error())) + } + + if !openapiRef.IsValidURI() { + allErrs = append(allErrs, field.Invalid(fldPath.Child("ref"), *schema.Ref, "ref does not point to a valid URI")) + } + } + + if schema.AdditionalItems != nil { + allErrs = append(allErrs, ValidateCustomResourceDefinitionOpenAPISchema(schema.AdditionalItems.Schema, fldPath.Child("additionalItems"), ssv)...) + } + + allErrs = append(allErrs, ValidateCustomResourceDefinitionOpenAPISchema(schema.Not, fldPath.Child("not"), ssv)...) + + if len(schema.AllOf) != 0 { + for _, jsonSchema := range schema.AllOf { + allErrs = append(allErrs, ValidateCustomResourceDefinitionOpenAPISchema(&jsonSchema, fldPath.Child("allOf"), ssv)...) + } + } + + if len(schema.OneOf) != 0 { + for _, jsonSchema := range schema.OneOf { + allErrs = append(allErrs, ValidateCustomResourceDefinitionOpenAPISchema(&jsonSchema, fldPath.Child("oneOf"), ssv)...) + } + } + + if len(schema.AnyOf) != 0 { + for _, jsonSchema := range schema.AnyOf { + allErrs = append(allErrs, ValidateCustomResourceDefinitionOpenAPISchema(&jsonSchema, fldPath.Child("anyOf"), ssv)...) + } + } + + if len(schema.Properties) != 0 { + for property, jsonSchema := range schema.Properties { + allErrs = append(allErrs, ValidateCustomResourceDefinitionOpenAPISchema(&jsonSchema, fldPath.Child("properties").Key(property), ssv)...) + } + } + + if len(schema.PatternProperties) != 0 { + for property, jsonSchema := range schema.PatternProperties { + allErrs = append(allErrs, ValidateCustomResourceDefinitionOpenAPISchema(&jsonSchema, fldPath.Child("patternProperties").Key(property), ssv)...) + } + } + + if len(schema.Definitions) != 0 { + for definition, jsonSchema := range schema.Definitions { + allErrs = append(allErrs, ValidateCustomResourceDefinitionOpenAPISchema(&jsonSchema, fldPath.Child("definitions").Key(definition), ssv)...) + } + } + + if schema.Items != nil { + allErrs = append(allErrs, ValidateCustomResourceDefinitionOpenAPISchema(schema.Items.Schema, fldPath.Child("items"), ssv)...) + if len(schema.Items.JSONSchemas) != 0 { + for _, jsonSchema := range schema.Items.JSONSchemas { + allErrs = append(allErrs, ValidateCustomResourceDefinitionOpenAPISchema(&jsonSchema, fldPath.Child("items"), ssv)...) + } + } + } + + if schema.Dependencies != nil { + for dependency, jsonSchemaPropsOrStringArray := range schema.Dependencies { + allErrs = append(allErrs, ValidateCustomResourceDefinitionOpenAPISchema(jsonSchemaPropsOrStringArray.Schema, fldPath.Child("dependencies").Key(dependency), ssv)...) + } + } + + return allErrs +} + +type specStandardValidatorV3 struct{} + +// validate validates against OpenAPI Schema v3. +func (v *specStandardValidatorV3) validate(schema *apiextensions.JSONSchemaProps, fldPath *field.Path) field.ErrorList { + allErrs := field.ErrorList{} + + if schema == nil { + return allErrs + } + + if schema.Default != nil { + allErrs = append(allErrs, field.Forbidden(fldPath.Child("default"), "default is not supported")) + } + + if schema.ID != "" { + allErrs = append(allErrs, field.Forbidden(fldPath.Child("id"), "id is not supported")) + } + + if schema.AdditionalItems != nil { + allErrs = append(allErrs, field.Forbidden(fldPath.Child("additionalItems"), "additionalItems is not supported")) + } + + if len(schema.PatternProperties) != 0 { + allErrs = append(allErrs, field.Forbidden(fldPath.Child("patternProperties"), "patternProperties is not supported")) + } + + if len(schema.Definitions) != 0 { + allErrs = append(allErrs, field.Forbidden(fldPath.Child("definitions"), "definitions is not supported")) + } + + if schema.Dependencies != nil { + allErrs = append(allErrs, field.Forbidden(fldPath.Child("dependencies"), "dependencies is not supported")) + } + + if schema.Type == "null" { + allErrs = append(allErrs, field.Forbidden(fldPath.Child("type"), "type cannot be set to null")) + } + + if schema.Items != nil && len(schema.Items.JSONSchemas) != 0 { + allErrs = append(allErrs, field.Forbidden(fldPath.Child("items"), "items must be a schema object and not an array")) + } + + return allErrs +} diff --git a/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/BUILD b/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/BUILD index e14bf33317e..eb29291c7d4 100644 --- a/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/BUILD +++ b/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/BUILD @@ -14,10 +14,14 @@ go_library( "customresource_handler.go", ], deps = [ + "//vendor/github.com/go-openapi/spec:go_default_library", + "//vendor/github.com/go-openapi/strfmt:go_default_library", + "//vendor/github.com/go-openapi/validate:go_default_library", "//vendor/github.com/golang/glog:go_default_library", "//vendor/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions:go_default_library", "//vendor/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/install:go_default_library", "//vendor/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1:go_default_library", + "//vendor/k8s.io/apiextensions-apiserver/pkg/apiserver/validation:go_default_library", "//vendor/k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset:go_default_library", "//vendor/k8s.io/apiextensions-apiserver/pkg/client/clientset/internalclientset:go_default_library", "//vendor/k8s.io/apiextensions-apiserver/pkg/client/informers/externalversions:go_default_library", @@ -68,6 +72,9 @@ filegroup( filegroup( name = "all-srcs", - srcs = [":package-srcs"], + srcs = [ + ":package-srcs", + "//staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/validation:all-srcs", + ], tags = ["automanaged"], ) diff --git a/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/apiserver.go b/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/apiserver.go index e178bd140a6..1d1409d9a4d 100644 --- a/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/apiserver.go +++ b/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/apiserver.go @@ -171,6 +171,7 @@ func (c completedConfig) New(delegationTarget genericapiserver.DelegationTarget) groupDiscoveryHandler, s.GenericAPIServer.RequestContextMapper(), s.Informers.Apiextensions().InternalVersion().CustomResourceDefinitions().Lister(), + s.Informers.Apiextensions().InternalVersion().CustomResourceDefinitions(), delegateHandler, c.CRDRESTOptionsGetter, c.GenericConfig.AdmissionControl, diff --git a/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/customresource_handler.go b/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/customresource_handler.go index 4c0002b6cee..a64f0260efe 100644 --- a/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/customresource_handler.go +++ b/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/customresource_handler.go @@ -26,6 +26,11 @@ import ( "sync/atomic" "time" + openapispec "github.com/go-openapi/spec" + "github.com/go-openapi/strfmt" + "github.com/go-openapi/validate" + "github.com/golang/glog" + apierrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -44,8 +49,11 @@ import ( genericregistry "k8s.io/apiserver/pkg/registry/generic/registry" "k8s.io/apiserver/pkg/storage/storagebackend" "k8s.io/client-go/discovery" + cache "k8s.io/client-go/tools/cache" "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions" + apiservervalidation "k8s.io/apiextensions-apiserver/pkg/apiserver/validation" + informers "k8s.io/apiextensions-apiserver/pkg/client/informers/internalversion/apiextensions/internalversion" listers "k8s.io/apiextensions-apiserver/pkg/client/listers/apiextensions/internalversion" "k8s.io/apiextensions-apiserver/pkg/controller/finalizer" "k8s.io/apiextensions-apiserver/pkg/registry/customresource" @@ -84,6 +92,7 @@ func NewCustomResourceDefinitionHandler( groupDiscoveryHandler *groupDiscoveryHandler, requestContextMapper apirequest.RequestContextMapper, crdLister listers.CustomResourceDefinitionLister, + crdInformer informers.CustomResourceDefinitionInformer, delegate http.Handler, restOptionsGetter generic.RESTOptionsGetter, admission admission.Interface) *crdHandler { @@ -98,6 +107,10 @@ func NewCustomResourceDefinitionHandler( admission: admission, } + crdInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{ + UpdateFunc: ret.updateCustomResourceDefinition, + }) + ret.customStorage.Store(crdStorageMap{}) return ret } @@ -155,7 +168,12 @@ func (r *crdHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) { terminating := apiextensions.IsCRDConditionTrue(crd, apiextensions.Terminating) - crdInfo := r.getServingInfoFor(crd) + crdInfo, err := r.getServingInfoFor(crd) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + storage := crdInfo.storage requestScope := crdInfo.requestScope minRequestTimeout := 1 * time.Minute @@ -250,15 +268,18 @@ func (r *crdHandler) removeDeadStorage() { // GetCustomResourceListerCollectionDeleter returns the ListerCollectionDeleter for // the given uid, or nil if one does not exist. func (r *crdHandler) GetCustomResourceListerCollectionDeleter(crd *apiextensions.CustomResourceDefinition) finalizer.ListerCollectionDeleter { - info := r.getServingInfoFor(crd) + info, err := r.getServingInfoFor(crd) + if err != nil { + utilruntime.HandleError(err) + } return info.storage } -func (r *crdHandler) getServingInfoFor(crd *apiextensions.CustomResourceDefinition) *crdInfo { +func (r *crdHandler) getServingInfoFor(crd *apiextensions.CustomResourceDefinition) (*crdInfo, error) { storageMap := r.customStorage.Load().(crdStorageMap) ret, ok := storageMap[crd.UID] if ok { - return ret + return ret, nil } r.customStorageLock.Lock() @@ -266,7 +287,7 @@ func (r *crdHandler) getServingInfoFor(crd *apiextensions.CustomResourceDefiniti ret, ok = storageMap[crd.UID] if ok { - return ret + return ret, nil } // In addition to Unstructured objects (Custom Resources), we also may sometimes need to @@ -287,6 +308,17 @@ func (r *crdHandler) getServingInfoFor(crd *apiextensions.CustomResourceDefiniti unstructuredTyper: discovery.NewUnstructuredObjectTyper(nil), } creator := unstructuredCreator{} + + // convert CRD schema to openapi schema + openapiSchema := &openapispec.Schema{} + if err := apiservervalidation.ConvertToOpenAPITypes(crd, openapiSchema); err != nil { + return nil, err + } + if err := openapispec.ExpandSchema(openapiSchema, nil, nil); err != nil { + return nil, err + } + validator := validate.NewSchemaValidator(openapiSchema, nil, "", strfmt.Default) + storage := customresource.NewREST( schema.GroupResource{Group: crd.Spec.Group, Resource: crd.Spec.Names.Plural}, schema.GroupVersionKind{Group: crd.Spec.Group, Version: crd.Spec.Version, Kind: crd.Spec.Names.ListKind}, @@ -295,6 +327,7 @@ func (r *crdHandler) getServingInfoFor(crd *apiextensions.CustomResourceDefiniti typer, crd.Spec.Scope == apiextensions.NamespaceScoped, kind, + validator, ), r.restOptionsGetter, ) @@ -354,7 +387,27 @@ func (r *crdHandler) getServingInfoFor(crd *apiextensions.CustomResourceDefiniti storageMap2[crd.UID] = ret r.customStorage.Store(storageMap2) - return ret + return ret, nil +} + +func (c *crdHandler) updateCustomResourceDefinition(oldObj, _ interface{}) { + oldCRD := oldObj.(*apiextensions.CustomResourceDefinition) + glog.V(4).Infof("Updating customresourcedefinition %s", oldCRD.Name) + + c.customStorageLock.Lock() + defer c.customStorageLock.Unlock() + + storageMap := c.customStorage.Load().(crdStorageMap) + storageMap2 := make(crdStorageMap, len(storageMap)) + + // Copy because we cannot write to storageMap without a race + // as it is used without locking elsewhere + for k, v := range storageMap { + storageMap2[k] = v + } + + delete(storageMap2, oldCRD.UID) + c.customStorage.Store(storageMap2) } type unstructuredNegotiatedSerializer struct { diff --git a/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/validation/BUILD b/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/validation/BUILD new file mode 100644 index 00000000000..b64622758b0 --- /dev/null +++ b/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/validation/BUILD @@ -0,0 +1,32 @@ +package(default_visibility = ["//visibility:public"]) + +licenses(["notice"]) + +load( + "@io_bazel_rules_go//go:def.bzl", + "go_library", +) + +go_library( + name = "go_default_library", + srcs = ["validation.go"], + tags = ["automanaged"], + deps = [ + "//vendor/github.com/go-openapi/spec:go_default_library", + "//vendor/github.com/go-openapi/validate:go_default_library", + "//vendor/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions:go_default_library", + ], +) + +filegroup( + name = "package-srcs", + srcs = glob(["**"]), + tags = ["automanaged"], + visibility = ["//visibility:private"], +) + +filegroup( + name = "all-srcs", + srcs = [":package-srcs"], + tags = ["automanaged"], +) diff --git a/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/validation/validation.go b/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/validation/validation.go new file mode 100644 index 00000000000..4c5e62851e1 --- /dev/null +++ b/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/validation/validation.go @@ -0,0 +1,217 @@ +/* +Copyright 2017 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 validation + +import ( + "github.com/go-openapi/spec" + "github.com/go-openapi/validate" + + "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions" +) + +// ValidateCustomResource validates the Custom Resource against the schema in the CustomResourceDefinition. +// CustomResource is a JSON data structure. +func ValidateCustomResource(customResource interface{}, validator *validate.SchemaValidator) error { + result := validator.Validate(customResource) + if result.AsError() != nil { + return result.AsError() + } + return nil +} + +// ConvertToOpenAPITypes is used to convert internal types to go-openapi types. +func ConvertToOpenAPITypes(in *apiextensions.CustomResourceDefinition, out *spec.Schema) error { + if in.Spec.Validation != nil { + if err := convertJSONSchemaProps(in.Spec.Validation.OpenAPIV3Schema, out); err != nil { + return err + } + } + + return nil +} + +func convertJSONSchemaProps(in *apiextensions.JSONSchemaProps, out *spec.Schema) error { + if in == nil { + return nil + } + + out.ID = in.ID + out.Schema = spec.SchemaURL(in.Schema) + out.Description = in.Description + if in.Type != "" { + out.Type = spec.StringOrArray([]string{in.Type}) + } + out.Format = in.Format + out.Title = in.Title + out.ExclusiveMaximum = in.ExclusiveMaximum + out.Minimum = in.Minimum + out.ExclusiveMinimum = in.ExclusiveMinimum + out.MaxLength = in.MaxLength + out.MinLength = in.MinLength + out.Pattern = in.Pattern + out.MaxItems = in.MaxItems + out.MinItems = in.MinItems + out.UniqueItems = in.UniqueItems + out.MultipleOf = in.MultipleOf + out.MaxProperties = in.MaxProperties + out.MinProperties = in.MinProperties + out.Required = in.Required + + if in.Default != nil { + out.Default = *(in.Default) + } + if in.Example != nil { + out.Example = *(in.Example) + } + + out.Enum = make([]interface{}, len(in.Enum)) + for k, v := range in.Enum { + out.Enum[k] = v + } + + if err := convertJSONSchemaPropsOrArray(in.Items, out.Items); err != nil { + return err + } + if err := convertSliceOfJSONSchemaProps(&in.AllOf, &out.AllOf); err != nil { + return err + } + if err := convertSliceOfJSONSchemaProps(&in.OneOf, &out.OneOf); err != nil { + return err + } + if err := convertSliceOfJSONSchemaProps(&in.AnyOf, &out.AnyOf); err != nil { + return err + } + if err := convertJSONSchemaProps(in.Not, out.Not); err != nil { + return err + } + + var err error + out.Properties, err = convertMapOfJSONSchemaProps(in.Properties) + if err != nil { + return err + } + + out.PatternProperties, err = convertMapOfJSONSchemaProps(in.PatternProperties) + if err != nil { + return err + } + + if in.Ref != nil { + out.Ref, err = spec.NewRef(*in.Ref) + if err != nil { + return err + } + } + + if err := convertJSONSchemaPropsorBool(in.AdditionalProperties, out.AdditionalProperties); err != nil { + return err + } + + if err := convertJSONSchemaPropsorBool(in.AdditionalItems, out.AdditionalItems); err != nil { + return err + } + + if err := convertJSONSchemaDependencies(in.Dependencies, out.Dependencies); err != nil { + return err + } + + out.Definitions, err = convertMapOfJSONSchemaProps(in.Definitions) + if err != nil { + return err + } + + if in.ExternalDocs != nil { + out.ExternalDocs = &spec.ExternalDocumentation{} + out.ExternalDocs.Description = in.ExternalDocs.Description + out.ExternalDocs.URL = in.ExternalDocs.URL + } + + return nil +} + +func convertSliceOfJSONSchemaProps(in *[]apiextensions.JSONSchemaProps, out *[]spec.Schema) error { + if in != nil { + for _, jsonSchemaProps := range *in { + schema := spec.Schema{} + if err := convertJSONSchemaProps(&jsonSchemaProps, &schema); err != nil { + return err + } + *out = append(*out, schema) + } + } + return nil +} + +func convertMapOfJSONSchemaProps(in map[string]apiextensions.JSONSchemaProps) (map[string]spec.Schema, error) { + out := make(map[string]spec.Schema) + if len(in) != 0 { + for k, jsonSchemaProps := range in { + schema := spec.Schema{} + if err := convertJSONSchemaProps(&jsonSchemaProps, &schema); err != nil { + return nil, err + } + out[k] = schema + } + } + return out, nil +} + +func convertJSONSchemaPropsOrArray(in *apiextensions.JSONSchemaPropsOrArray, out *spec.SchemaOrArray) error { + if in != nil { + out.Schema = &spec.Schema{} + if err := convertJSONSchemaProps(in.Schema, out.Schema); err != nil { + return err + } + } + return nil +} + +func convertJSONSchemaPropsorBool(in *apiextensions.JSONSchemaPropsOrBool, out *spec.SchemaOrBool) error { + if in != nil { + out = &spec.SchemaOrBool{} + out.Allows = in.Allows + out.Schema = &spec.Schema{} + if err := convertJSONSchemaProps(in.Schema, out.Schema); err != nil { + return err + } + } + return nil +} + +func convertJSONSchemaPropsOrStringArray(in *apiextensions.JSONSchemaPropsOrStringArray, out *spec.SchemaOrStringArray) error { + if in != nil { + out.Property = in.Property + out.Schema = &spec.Schema{} + if err := convertJSONSchemaProps(in.Schema, out.Schema); err != nil { + return err + } + } + return nil +} + +func convertJSONSchemaDependencies(in apiextensions.JSONSchemaDependencies, out spec.Dependencies) error { + if in != nil { + for k, v := range in { + schemaOrArray := spec.SchemaOrStringArray{} + if err := convertJSONSchemaPropsOrStringArray(&v, &schemaOrArray); err != nil { + return err + } + out[k] = schemaOrArray + } + } + return nil +} diff --git a/staging/src/k8s.io/apiextensions-apiserver/pkg/registry/customresource/BUILD b/staging/src/k8s.io/apiextensions-apiserver/pkg/registry/customresource/BUILD index e61b8f824c5..90eb4120ecf 100644 --- a/staging/src/k8s.io/apiextensions-apiserver/pkg/registry/customresource/BUILD +++ b/staging/src/k8s.io/apiextensions-apiserver/pkg/registry/customresource/BUILD @@ -12,6 +12,8 @@ go_library( "strategy.go", ], deps = [ + "//vendor/github.com/go-openapi/validate:go_default_library", + "//vendor/k8s.io/apiextensions-apiserver/pkg/apiserver/validation:go_default_library", "//vendor/k8s.io/apimachinery/pkg/api/meta:go_default_library", "//vendor/k8s.io/apimachinery/pkg/api/validation:go_default_library", "//vendor/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library", diff --git a/staging/src/k8s.io/apiextensions-apiserver/pkg/registry/customresource/strategy.go b/staging/src/k8s.io/apiextensions-apiserver/pkg/registry/customresource/strategy.go index 6b97d07531a..18c6c2b7454 100644 --- a/staging/src/k8s.io/apiextensions-apiserver/pkg/registry/customresource/strategy.go +++ b/staging/src/k8s.io/apiextensions-apiserver/pkg/registry/customresource/strategy.go @@ -19,9 +19,12 @@ package customresource import ( "fmt" + "github.com/go-openapi/validate" + "k8s.io/apimachinery/pkg/api/meta" "k8s.io/apimachinery/pkg/api/validation" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/fields" "k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/runtime" @@ -30,6 +33,8 @@ import ( genericapirequest "k8s.io/apiserver/pkg/endpoints/request" "k8s.io/apiserver/pkg/storage" "k8s.io/apiserver/pkg/storage/names" + + apiservervalidation "k8s.io/apiextensions-apiserver/pkg/apiserver/validation" ) type customResourceDefinitionStorageStrategy struct { @@ -40,7 +45,7 @@ type customResourceDefinitionStorageStrategy struct { validator customResourceValidator } -func NewStrategy(typer runtime.ObjectTyper, namespaceScoped bool, kind schema.GroupVersionKind) customResourceDefinitionStorageStrategy { +func NewStrategy(typer runtime.ObjectTyper, namespaceScoped bool, kind schema.GroupVersionKind, validator *validate.SchemaValidator) customResourceDefinitionStorageStrategy { return customResourceDefinitionStorageStrategy{ ObjectTyper: typer, NameGenerator: names.SimpleNameGenerator, @@ -48,6 +53,7 @@ func NewStrategy(typer runtime.ObjectTyper, namespaceScoped bool, kind schema.Gr validator: customResourceValidator{ namespaceScoped: namespaceScoped, kind: kind, + validator: validator, }, } } @@ -113,6 +119,7 @@ func (a customResourceDefinitionStorageStrategy) MatchCustomResourceDefinitionSt type customResourceValidator struct { namespaceScoped bool kind schema.GroupVersionKind + validator *validate.SchemaValidator } func (a customResourceValidator) Validate(ctx genericapirequest.Context, obj runtime.Object) field.ErrorList { @@ -131,6 +138,17 @@ func (a customResourceValidator) Validate(ctx genericapirequest.Context, obj run return field.ErrorList{field.Invalid(field.NewPath("apiVersion"), typeAccessor.GetKind(), fmt.Sprintf("must be %v", a.kind.Group+"/"+a.kind.Version))} } + customResourceObject, ok := obj.(*unstructured.Unstructured) + // this will never happen. + if !ok { + return field.ErrorList{field.Invalid(field.NewPath(""), customResourceObject, fmt.Sprintf("has type %T. Must be a pointer to an Unstructured type", customResourceObject))} + } + + customResource := customResourceObject.UnstructuredContent() + if err = apiservervalidation.ValidateCustomResource(customResource, a.validator); err != nil { + return field.ErrorList{field.Invalid(field.NewPath(""), customResource, err.Error())} + } + return validation.ValidateObjectMetaAccessor(accessor, a.namespaceScoped, validation.NameIsDNSSubdomain, field.NewPath("metadata")) } @@ -154,5 +172,16 @@ func (a customResourceValidator) ValidateUpdate(ctx genericapirequest.Context, o return field.ErrorList{field.Invalid(field.NewPath("apiVersion"), typeAccessor.GetKind(), fmt.Sprintf("must be %v", a.kind.Group+"/"+a.kind.Version))} } + customResourceObject, ok := obj.(*unstructured.Unstructured) + // this will never happen. + if !ok { + return field.ErrorList{field.Invalid(field.NewPath(""), customResourceObject, fmt.Sprintf("has type %T. Must be a pointer to an Unstructured type", customResourceObject))} + } + + customResource := customResourceObject.UnstructuredContent() + if err = apiservervalidation.ValidateCustomResource(customResource, a.validator); err != nil { + return field.ErrorList{field.Invalid(field.NewPath(""), customResource, err.Error())} + } + return validation.ValidateObjectMetaAccessorUpdate(objAccessor, oldAccessor, field.NewPath("metadata")) }