Unverified Commit 3193e78a authored by Kubernetes Prow Robot's avatar Kubernetes Prow Robot Committed by GitHub

Merge pull request #77333 from sttts/sttts-structural-crd-pruning

apiextensions: implement structural schema CRD pruning
parents b10e16e8 d10f8c1a
...@@ -16500,6 +16500,10 @@ ...@@ -16500,6 +16500,10 @@
"$ref": "#/definitions/io.k8s.apiextensions-apiserver.pkg.apis.apiextensions.v1beta1.CustomResourceDefinitionNames", "$ref": "#/definitions/io.k8s.apiextensions-apiserver.pkg.apis.apiextensions.v1beta1.CustomResourceDefinitionNames",
"description": "Names are the names used to describe this custom resource" "description": "Names are the names used to describe this custom resource"
}, },
"preserveUnknownFields": {
"description": "preserveUnknownFields disables pruning of object fields which are not specified in the OpenAPI schema. apiVersion, kind, metadata and known fields inside metadata are always preserved. Defaults to true in v1beta and will default to false in v1.",
"type": "boolean"
},
"scope": { "scope": {
"description": "Scope indicates whether this resource is cluster or namespace scoped. Default is namespaced", "description": "Scope indicates whether this resource is cluster or namespace scoped. Default is namespaced",
"type": "string" "type": "string"
...@@ -15,6 +15,7 @@ go_library( ...@@ -15,6 +15,7 @@ go_library(
"//staging/src/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
"//staging/src/k8s.io/apimachinery/pkg/runtime/serializer:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/runtime/serializer:go_default_library",
"//vendor/github.com/google/gofuzz:go_default_library", "//vendor/github.com/google/gofuzz:go_default_library",
"//vendor/k8s.io/utils/pointer:go_default_library",
], ],
) )
......
...@@ -25,6 +25,7 @@ import ( ...@@ -25,6 +25,7 @@ import (
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions" "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
runtimeserializer "k8s.io/apimachinery/pkg/runtime/serializer" runtimeserializer "k8s.io/apimachinery/pkg/runtime/serializer"
"k8s.io/utils/pointer"
) )
var swaggerMetadataDescriptions = metav1.ObjectMeta{}.SwaggerDoc() var swaggerMetadataDescriptions = metav1.ObjectMeta{}.SwaggerDoc()
...@@ -69,6 +70,9 @@ func Funcs(codecs runtimeserializer.CodecFactory) []interface{} { ...@@ -69,6 +70,9 @@ func Funcs(codecs runtimeserializer.CodecFactory) []interface{} {
if obj.Conversion.Strategy == apiextensions.WebhookConverter && len(obj.Conversion.ConversionReviewVersions) == 0 { if obj.Conversion.Strategy == apiextensions.WebhookConverter && len(obj.Conversion.ConversionReviewVersions) == 0 {
obj.Conversion.ConversionReviewVersions = []string{"v1beta1"} obj.Conversion.ConversionReviewVersions = []string{"v1beta1"}
} }
if obj.PreserveUnknownFields == nil {
obj.PreserveUnknownFields = pointer.BoolPtr(true)
}
}, },
func(obj *apiextensions.CustomResourceDefinition, c fuzz.Continue) { func(obj *apiextensions.CustomResourceDefinition, c fuzz.Continue) {
c.FuzzNoCustom(obj) c.FuzzNoCustom(obj)
......
...@@ -73,6 +73,12 @@ type CustomResourceDefinitionSpec struct { ...@@ -73,6 +73,12 @@ type CustomResourceDefinitionSpec struct {
// `conversion` defines conversion settings for the CRD. // `conversion` defines conversion settings for the CRD.
Conversion *CustomResourceConversion Conversion *CustomResourceConversion
// preserveUnknownFields disables pruning of object fields which are not
// specified in the OpenAPI schema. apiVersion, kind, metadata and known
// fields inside metadata are always preserved.
// Defaults to true in v1beta and will default to false in v1.
PreserveUnknownFields *bool
} }
// CustomResourceConversion describes how to convert different versions of a CR. // CustomResourceConversion describes how to convert different versions of a CR.
......
...@@ -72,6 +72,9 @@ func SetDefaults_CustomResourceDefinitionSpec(obj *CustomResourceDefinitionSpec) ...@@ -72,6 +72,9 @@ func SetDefaults_CustomResourceDefinitionSpec(obj *CustomResourceDefinitionSpec)
if obj.Conversion.Strategy == WebhookConverter && len(obj.Conversion.ConversionReviewVersions) == 0 { if obj.Conversion.Strategy == WebhookConverter && len(obj.Conversion.ConversionReviewVersions) == 0 {
obj.Conversion.ConversionReviewVersions = []string{SchemeGroupVersion.Version} obj.Conversion.ConversionReviewVersions = []string{SchemeGroupVersion.Version}
} }
if obj.PreserveUnknownFields == nil {
obj.PreserveUnknownFields = utilpointer.BoolPtr(true)
}
} }
// SetDefaults_ServiceReference sets defaults for Webhook's ServiceReference // SetDefaults_ServiceReference sets defaults for Webhook's ServiceReference
......
...@@ -247,6 +247,12 @@ message CustomResourceDefinitionSpec { ...@@ -247,6 +247,12 @@ message CustomResourceDefinitionSpec {
// `conversion` defines conversion settings for the CRD. // `conversion` defines conversion settings for the CRD.
// +optional // +optional
optional CustomResourceConversion conversion = 9; optional CustomResourceConversion conversion = 9;
// preserveUnknownFields disables pruning of object fields which are not
// specified in the OpenAPI schema. apiVersion, kind, metadata and known
// fields inside metadata are always preserved.
// Defaults to true in v1beta and will default to false in v1.
optional bool preserveUnknownFields = 10;
} }
// CustomResourceDefinitionStatus indicates the state of the CustomResourceDefinition // CustomResourceDefinitionStatus indicates the state of the CustomResourceDefinition
......
...@@ -78,6 +78,12 @@ type CustomResourceDefinitionSpec struct { ...@@ -78,6 +78,12 @@ type CustomResourceDefinitionSpec struct {
// `conversion` defines conversion settings for the CRD. // `conversion` defines conversion settings for the CRD.
// +optional // +optional
Conversion *CustomResourceConversion `json:"conversion,omitempty" protobuf:"bytes,9,opt,name=conversion"` Conversion *CustomResourceConversion `json:"conversion,omitempty" protobuf:"bytes,9,opt,name=conversion"`
// preserveUnknownFields disables pruning of object fields which are not
// specified in the OpenAPI schema. apiVersion, kind, metadata and known
// fields inside metadata are always preserved.
// Defaults to true in v1beta and will default to false in v1.
PreserveUnknownFields *bool `json:"preserveUnknownFields,omitempty" protobuf:"varint,10,opt,name=preserveUnknownFields"`
} }
// CustomResourceConversion describes how to convert different versions of a CR. // CustomResourceConversion describes how to convert different versions of a CR.
......
...@@ -504,6 +504,7 @@ func autoConvert_v1beta1_CustomResourceDefinitionSpec_To_apiextensions_CustomRes ...@@ -504,6 +504,7 @@ func autoConvert_v1beta1_CustomResourceDefinitionSpec_To_apiextensions_CustomRes
} else { } else {
out.Conversion = nil out.Conversion = nil
} }
out.PreserveUnknownFields = (*bool)(unsafe.Pointer(in.PreserveUnknownFields))
return nil return nil
} }
...@@ -550,6 +551,7 @@ func autoConvert_apiextensions_CustomResourceDefinitionSpec_To_v1beta1_CustomRes ...@@ -550,6 +551,7 @@ func autoConvert_apiextensions_CustomResourceDefinitionSpec_To_v1beta1_CustomRes
} else { } else {
out.Conversion = nil out.Conversion = nil
} }
out.PreserveUnknownFields = (*bool)(unsafe.Pointer(in.PreserveUnknownFields))
return nil return nil
} }
......
...@@ -283,6 +283,11 @@ func (in *CustomResourceDefinitionSpec) DeepCopyInto(out *CustomResourceDefiniti ...@@ -283,6 +283,11 @@ func (in *CustomResourceDefinitionSpec) DeepCopyInto(out *CustomResourceDefiniti
*out = new(CustomResourceConversion) *out = new(CustomResourceConversion)
(*in).DeepCopyInto(*out) (*in).DeepCopyInto(*out)
} }
if in.PreserveUnknownFields != nil {
in, out := &in.PreserveUnknownFields, &out.PreserveUnknownFields
*out = new(bool)
**out = **in
}
return return
} }
......
...@@ -14,6 +14,7 @@ go_library( ...@@ -14,6 +14,7 @@ go_library(
deps = [ deps = [
"//staging/src/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions:go_default_library", "//staging/src/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions:go_default_library",
"//staging/src/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1:go_default_library", "//staging/src/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1:go_default_library",
"//staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema:go_default_library",
"//staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/validation:go_default_library", "//staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/validation:go_default_library",
"//staging/src/k8s.io/apiextensions-apiserver/pkg/features:go_default_library", "//staging/src/k8s.io/apiextensions-apiserver/pkg/features:go_default_library",
"//staging/src/k8s.io/apimachinery/pkg/api/equality:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/api/equality:go_default_library",
......
...@@ -21,6 +21,7 @@ import ( ...@@ -21,6 +21,7 @@ import (
"reflect" "reflect"
"strings" "strings"
structuralschema "k8s.io/apiextensions-apiserver/pkg/apiserver/schema"
apiequality "k8s.io/apimachinery/pkg/api/equality" apiequality "k8s.io/apimachinery/pkg/api/equality"
genericvalidation "k8s.io/apimachinery/pkg/api/validation" genericvalidation "k8s.io/apimachinery/pkg/api/validation"
"k8s.io/apimachinery/pkg/util/sets" "k8s.io/apimachinery/pkg/util/sets"
...@@ -102,9 +103,9 @@ func ValidateUpdateCustomResourceDefinitionStatus(obj, oldObj *apiextensions.Cus ...@@ -102,9 +103,9 @@ func ValidateUpdateCustomResourceDefinitionStatus(obj, oldObj *apiextensions.Cus
} }
// ValidateCustomResourceDefinitionVersion statically validates. // ValidateCustomResourceDefinitionVersion statically validates.
func ValidateCustomResourceDefinitionVersion(version *apiextensions.CustomResourceDefinitionVersion, fldPath *field.Path, statusEnabled bool) field.ErrorList { func ValidateCustomResourceDefinitionVersion(version *apiextensions.CustomResourceDefinitionVersion, fldPath *field.Path, mustBeStructural, statusEnabled bool) field.ErrorList {
allErrs := field.ErrorList{} allErrs := field.ErrorList{}
allErrs = append(allErrs, ValidateCustomResourceDefinitionValidation(version.Schema, statusEnabled, fldPath.Child("schema"))...) allErrs = append(allErrs, ValidateCustomResourceDefinitionValidation(version.Schema, mustBeStructural, statusEnabled, fldPath.Child("schema"))...)
allErrs = append(allErrs, ValidateCustomResourceDefinitionSubresources(version.Subresources, fldPath.Child("subresources"))...) allErrs = append(allErrs, ValidateCustomResourceDefinitionSubresources(version.Subresources, fldPath.Child("subresources"))...)
for i := range version.AdditionalPrinterColumns { for i := range version.AdditionalPrinterColumns {
allErrs = append(allErrs, ValidateCustomResourceColumnDefinition(&version.AdditionalPrinterColumns[i], fldPath.Child("additionalPrinterColumns").Index(i))...) allErrs = append(allErrs, ValidateCustomResourceColumnDefinition(&version.AdditionalPrinterColumns[i], fldPath.Child("additionalPrinterColumns").Index(i))...)
...@@ -130,6 +131,20 @@ func validateCustomResourceDefinitionSpec(spec *apiextensions.CustomResourceDefi ...@@ -130,6 +131,20 @@ func validateCustomResourceDefinitionSpec(spec *apiextensions.CustomResourceDefi
allErrs = append(allErrs, validateEnumStrings(fldPath.Child("scope"), string(spec.Scope), []string{string(apiextensions.ClusterScoped), string(apiextensions.NamespaceScoped)}, true)...) allErrs = append(allErrs, validateEnumStrings(fldPath.Child("scope"), string(spec.Scope), []string{string(apiextensions.ClusterScoped), string(apiextensions.NamespaceScoped)}, true)...)
mustBeStructural := false
if spec.PreserveUnknownFields == nil || *spec.PreserveUnknownFields == false {
mustBeStructural = true
// check that either a global schema or versioned schemas are set
if spec.Validation == nil || spec.Validation.OpenAPIV3Schema == nil {
for i, v := range spec.Versions {
schemaPath := fldPath.Child("versions").Index(i).Child("schema", "openAPIV3Schema")
if v.Served && (v.Schema == nil || v.Schema.OpenAPIV3Schema == nil) {
allErrs = append(allErrs, field.Required(schemaPath, "because otherwise all fields are pruned"))
}
}
}
}
storageFlagCount := 0 storageFlagCount := 0
versionsMap := map[string]bool{} versionsMap := map[string]bool{}
uniqueNames := true uniqueNames := true
...@@ -146,7 +161,7 @@ func validateCustomResourceDefinitionSpec(spec *apiextensions.CustomResourceDefi ...@@ -146,7 +161,7 @@ func validateCustomResourceDefinitionSpec(spec *apiextensions.CustomResourceDefi
allErrs = append(allErrs, field.Invalid(fldPath.Child("versions").Index(i).Child("name"), spec.Versions[i].Name, strings.Join(errs, ","))) allErrs = append(allErrs, field.Invalid(fldPath.Child("versions").Index(i).Child("name"), spec.Versions[i].Name, strings.Join(errs, ",")))
} }
subresources := getSubresourcesForVersion(spec, version.Name) subresources := getSubresourcesForVersion(spec, version.Name)
allErrs = append(allErrs, ValidateCustomResourceDefinitionVersion(&version, fldPath.Child("versions").Index(i), hasStatusEnabled(subresources))...) allErrs = append(allErrs, ValidateCustomResourceDefinitionVersion(&version, fldPath.Child("versions").Index(i), mustBeStructural, hasStatusEnabled(subresources))...)
} }
// The top-level and per-version fields are mutual exclusive // The top-level and per-version fields are mutual exclusive
...@@ -201,7 +216,7 @@ func validateCustomResourceDefinitionSpec(spec *apiextensions.CustomResourceDefi ...@@ -201,7 +216,7 @@ func validateCustomResourceDefinitionSpec(spec *apiextensions.CustomResourceDefi
} }
allErrs = append(allErrs, ValidateCustomResourceDefinitionNames(&spec.Names, fldPath.Child("names"))...) allErrs = append(allErrs, ValidateCustomResourceDefinitionNames(&spec.Names, fldPath.Child("names"))...)
allErrs = append(allErrs, ValidateCustomResourceDefinitionValidation(spec.Validation, hasAnyStatusEnabled(spec), fldPath.Child("validation"))...) allErrs = append(allErrs, ValidateCustomResourceDefinitionValidation(spec.Validation, mustBeStructural, hasAnyStatusEnabled(spec), fldPath.Child("validation"))...)
allErrs = append(allErrs, ValidateCustomResourceDefinitionSubresources(spec.Subresources, fldPath.Child("subresources"))...) allErrs = append(allErrs, ValidateCustomResourceDefinitionSubresources(spec.Subresources, fldPath.Child("subresources"))...)
for i := range spec.AdditionalPrinterColumns { for i := range spec.AdditionalPrinterColumns {
...@@ -531,7 +546,7 @@ type specStandardValidator interface { ...@@ -531,7 +546,7 @@ type specStandardValidator interface {
} }
// ValidateCustomResourceDefinitionValidation statically validates // ValidateCustomResourceDefinitionValidation statically validates
func ValidateCustomResourceDefinitionValidation(customResourceValidation *apiextensions.CustomResourceValidation, statusSubresourceEnabled bool, fldPath *field.Path) field.ErrorList { func ValidateCustomResourceDefinitionValidation(customResourceValidation *apiextensions.CustomResourceValidation, mustBeStructural, statusSubresourceEnabled bool, fldPath *field.Path) field.ErrorList {
allErrs := field.ErrorList{} allErrs := field.ErrorList{}
if customResourceValidation == nil { if customResourceValidation == nil {
...@@ -573,6 +588,17 @@ func ValidateCustomResourceDefinitionValidation(customResourceValidation *apiext ...@@ -573,6 +588,17 @@ func ValidateCustomResourceDefinitionValidation(customResourceValidation *apiext
openAPIV3Schema := &specStandardValidatorV3{} openAPIV3Schema := &specStandardValidatorV3{}
allErrs = append(allErrs, ValidateCustomResourceDefinitionOpenAPISchema(schema, fldPath.Child("openAPIV3Schema"), openAPIV3Schema)...) allErrs = append(allErrs, ValidateCustomResourceDefinitionOpenAPISchema(schema, fldPath.Child("openAPIV3Schema"), openAPIV3Schema)...)
if mustBeStructural {
if ss, err := structuralschema.NewStructural(schema); err != nil {
// if the generic schema validation did its job, we should never get an error here. Hence, we hide it if there are validation errors already.
if len(allErrs) == 0 {
allErrs = append(allErrs, field.Invalid(fldPath.Child("openAPIV3Schema"), "", err.Error()))
}
} else {
allErrs = append(allErrs, structuralschema.ValidateStructural(ss, fldPath.Child("openAPIV3Schema"))...)
}
}
} }
// if validation passed otherwise, make sure we can actually construct a schema validator from this custom resource validation. // if validation passed otherwise, make sure we can actually construct a schema validator from this custom resource validation.
......
...@@ -201,6 +201,11 @@ func (in *CustomResourceDefinitionSpec) DeepCopyInto(out *CustomResourceDefiniti ...@@ -201,6 +201,11 @@ func (in *CustomResourceDefinitionSpec) DeepCopyInto(out *CustomResourceDefiniti
*out = new(CustomResourceConversion) *out = new(CustomResourceConversion)
(*in).DeepCopyInto(*out) (*in).DeepCopyInto(*out)
} }
if in.PreserveUnknownFields != nil {
in, out := &in.PreserveUnknownFields, &out.PreserveUnknownFields
*out = new(bool)
**out = **in
}
return return
} }
......
...@@ -23,6 +23,8 @@ go_library( ...@@ -23,6 +23,8 @@ go_library(
"//staging/src/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/install:go_default_library", "//staging/src/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/install:go_default_library",
"//staging/src/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1:go_default_library", "//staging/src/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1:go_default_library",
"//staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/conversion:go_default_library", "//staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/conversion:go_default_library",
"//staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema:go_default_library",
"//staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema/pruning:go_default_library",
"//staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/validation:go_default_library", "//staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/validation: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:go_default_library",
"//staging/src/k8s.io/apiextensions-apiserver/pkg/client/clientset/internalclientset:go_default_library", "//staging/src/k8s.io/apiextensions-apiserver/pkg/client/clientset/internalclientset:go_default_library",
......
...@@ -30,6 +30,8 @@ import ( ...@@ -30,6 +30,8 @@ import (
"github.com/go-openapi/validate" "github.com/go-openapi/validate"
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions" "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions"
"k8s.io/apiextensions-apiserver/pkg/apiserver/conversion" "k8s.io/apiextensions-apiserver/pkg/apiserver/conversion"
structuralschema "k8s.io/apiextensions-apiserver/pkg/apiserver/schema"
structuralpruning "k8s.io/apiextensions-apiserver/pkg/apiserver/schema/pruning"
apiservervalidation "k8s.io/apiextensions-apiserver/pkg/apiserver/validation" apiservervalidation "k8s.io/apiextensions-apiserver/pkg/apiserver/validation"
informers "k8s.io/apiextensions-apiserver/pkg/client/informers/internalversion/apiextensions/internalversion" informers "k8s.io/apiextensions-apiserver/pkg/client/informers/internalversion/apiextensions/internalversion"
listers "k8s.io/apiextensions-apiserver/pkg/client/listers/apiextensions/internalversion" listers "k8s.io/apiextensions-apiserver/pkg/client/listers/apiextensions/internalversion"
...@@ -39,6 +41,7 @@ import ( ...@@ -39,6 +41,7 @@ import (
apiextensionsfeatures "k8s.io/apiextensions-apiserver/pkg/features" apiextensionsfeatures "k8s.io/apiextensions-apiserver/pkg/features"
"k8s.io/apiextensions-apiserver/pkg/registry/customresource" "k8s.io/apiextensions-apiserver/pkg/registry/customresource"
"k8s.io/apiextensions-apiserver/pkg/registry/customresource/tableconvertor" "k8s.io/apiextensions-apiserver/pkg/registry/customresource/tableconvertor"
apiequality "k8s.io/apimachinery/pkg/api/equality" apiequality "k8s.io/apimachinery/pkg/api/equality"
apierrors "k8s.io/apimachinery/pkg/api/errors" apierrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/api/meta" "k8s.io/apimachinery/pkg/api/meta"
...@@ -520,6 +523,20 @@ func (r *crdHandler) getOrCreateServingInfoFor(crd *apiextensions.CustomResource ...@@ -520,6 +523,20 @@ func (r *crdHandler) getOrCreateServingInfoFor(crd *apiextensions.CustomResource
return nil, err return nil, err
} }
// Check for nil because we dereference this throughout the handler code.
// Note: we always default this to non-nil. But we should guard these dereferences any way.
if crd.Spec.PreserveUnknownFields == nil {
return nil, fmt.Errorf("unexpected nil spec.preserveUnknownFields in the CustomResourceDefinition")
}
var structuralSchema *structuralschema.Structural
if validationSchema != nil {
structuralSchema, err = structuralschema.NewStructural(validationSchema.OpenAPIV3Schema)
if *crd.Spec.PreserveUnknownFields == false && err != nil {
return nil, err // validation should avoid this
}
}
var statusSpec *apiextensions.CustomResourceSubresourceStatus var statusSpec *apiextensions.CustomResourceSubresourceStatus
var statusValidator *validate.SchemaValidator var statusValidator *validate.SchemaValidator
subresources, err := apiextensions.GetSubresourcesForVersion(crd, v.Name) subresources, err := apiextensions.GetSubresourcesForVersion(crd, v.Name)
...@@ -570,10 +587,13 @@ func (r *crdHandler) getOrCreateServingInfoFor(crd *apiextensions.CustomResource ...@@ -570,10 +587,13 @@ func (r *crdHandler) getOrCreateServingInfoFor(crd *apiextensions.CustomResource
scaleSpec, scaleSpec,
), ),
crdConversionRESTOptionsGetter{ crdConversionRESTOptionsGetter{
RESTOptionsGetter: r.restOptionsGetter, RESTOptionsGetter: r.restOptionsGetter,
converter: safeConverter, converter: safeConverter,
decoderVersion: schema.GroupVersion{Group: crd.Spec.Group, Version: v.Name}, decoderVersion: schema.GroupVersion{Group: crd.Spec.Group, Version: v.Name},
encoderVersion: schema.GroupVersion{Group: crd.Spec.Group, Version: storageVersion}, encoderVersion: schema.GroupVersion{Group: crd.Spec.Group, Version: storageVersion},
structuralSchema: structuralSchema,
structuralSchemaGK: kind.GroupKind(),
preserveUnknownFields: *crd.Spec.PreserveUnknownFields,
}, },
crd.Status.AcceptedNames.Categories, crd.Status.AcceptedNames.Categories,
table, table,
...@@ -595,7 +615,14 @@ func (r *crdHandler) getOrCreateServingInfoFor(crd *apiextensions.CustomResource ...@@ -595,7 +615,14 @@ func (r *crdHandler) getOrCreateServingInfoFor(crd *apiextensions.CustomResource
ClusterScoped: clusterScoped, ClusterScoped: clusterScoped,
SelfLinkPathPrefix: selfLinkPrefix, SelfLinkPathPrefix: selfLinkPrefix,
}, },
Serializer: unstructuredNegotiatedSerializer{typer: typer, creator: creator, converter: safeConverter}, Serializer: unstructuredNegotiatedSerializer{
typer: typer,
creator: creator,
converter: safeConverter,
structuralSchema: structuralSchema,
structuralSchemaGK: kind.GroupKind(),
preserveUnknownFields: *crd.Spec.PreserveUnknownFields,
},
ParameterCodec: parameterCodec, ParameterCodec: parameterCodec,
Creater: creator, Creater: creator,
...@@ -646,6 +673,13 @@ func (r *crdHandler) getOrCreateServingInfoFor(crd *apiextensions.CustomResource ...@@ -646,6 +673,13 @@ func (r *crdHandler) getOrCreateServingInfoFor(crd *apiextensions.CustomResource
// shallow copy // shallow copy
statusScope := *requestScopes[v.Name] statusScope := *requestScopes[v.Name]
statusScope.Subresource = "status" statusScope.Subresource = "status"
statusScope.Serializer = unstructuredNegotiatedSerializer{
typer: typer, creator: creator,
converter: safeConverter,
structuralSchema: structuralSchema,
structuralSchemaGK: kind.GroupKind(),
preserveUnknownFields: *crd.Spec.PreserveUnknownFields,
}
statusScope.Namer = handlers.ContextBasedNaming{ statusScope.Namer = handlers.ContextBasedNaming{
SelfLinker: meta.NewAccessor(), SelfLinker: meta.NewAccessor(),
ClusterScoped: clusterScoped, ClusterScoped: clusterScoped,
...@@ -680,6 +714,10 @@ type unstructuredNegotiatedSerializer struct { ...@@ -680,6 +714,10 @@ type unstructuredNegotiatedSerializer struct {
typer runtime.ObjectTyper typer runtime.ObjectTyper
creator runtime.ObjectCreater creator runtime.ObjectCreater
converter runtime.ObjectConvertor converter runtime.ObjectConvertor
structuralSchema *structuralschema.Structural
structuralSchemaGK schema.GroupKind
preserveUnknownFields bool
} }
func (s unstructuredNegotiatedSerializer) SupportedMediaTypes() []runtime.SerializerInfo { func (s unstructuredNegotiatedSerializer) SupportedMediaTypes() []runtime.SerializerInfo {
...@@ -712,7 +750,7 @@ func (s unstructuredNegotiatedSerializer) EncoderForVersion(encoder runtime.Enco ...@@ -712,7 +750,7 @@ func (s unstructuredNegotiatedSerializer) EncoderForVersion(encoder runtime.Enco
} }
func (s unstructuredNegotiatedSerializer) DecoderToVersion(decoder runtime.Decoder, gv runtime.GroupVersioner) runtime.Decoder { func (s unstructuredNegotiatedSerializer) DecoderToVersion(decoder runtime.Decoder, gv runtime.GroupVersioner) runtime.Decoder {
d := schemaCoercingDecoder{delegate: decoder, validator: unstructuredSchemaCoercer{}} d := schemaCoercingDecoder{delegate: decoder, validator: unstructuredSchemaCoercer{structuralSchema: s.structuralSchema, structuralSchemaGK: s.structuralSchemaGK, preserveUnknownFields: s.preserveUnknownFields}}
return versioning.NewDefaultingCodecForScheme(Scheme, nil, d, nil, gv) return versioning.NewDefaultingCodecForScheme(Scheme, nil, d, nil, gv)
} }
...@@ -801,19 +839,29 @@ func (in crdStorageMap) clone() crdStorageMap { ...@@ -801,19 +839,29 @@ func (in crdStorageMap) clone() crdStorageMap {
// provided custom converter and custom encoder and decoder version. // provided custom converter and custom encoder and decoder version.
type crdConversionRESTOptionsGetter struct { type crdConversionRESTOptionsGetter struct {
generic.RESTOptionsGetter generic.RESTOptionsGetter
converter runtime.ObjectConvertor converter runtime.ObjectConvertor
encoderVersion schema.GroupVersion encoderVersion schema.GroupVersion
decoderVersion schema.GroupVersion decoderVersion schema.GroupVersion
structuralSchema *structuralschema.Structural
structuralSchemaGK schema.GroupKind
preserveUnknownFields bool
} }
func (t crdConversionRESTOptionsGetter) GetRESTOptions(resource schema.GroupResource) (generic.RESTOptions, error) { func (t crdConversionRESTOptionsGetter) GetRESTOptions(resource schema.GroupResource) (generic.RESTOptions, error) {
ret, err := t.RESTOptionsGetter.GetRESTOptions(resource) ret, err := t.RESTOptionsGetter.GetRESTOptions(resource)
if err == nil { if err == nil {
d := schemaCoercingDecoder{delegate: ret.StorageConfig.Codec, validator: unstructuredSchemaCoercer{ d := schemaCoercingDecoder{delegate: ret.StorageConfig.Codec, validator: unstructuredSchemaCoercer{
// drop invalid fields while decoding old CRs (before we had any ObjectMeta validation) // drop invalid fields while decoding old CRs (before we haven't had any ObjectMeta validation)
dropInvalidMetadata: true, dropInvalidMetadata: true,
structuralSchema: t.structuralSchema,
structuralSchemaGK: t.structuralSchemaGK,
preserveUnknownFields: t.preserveUnknownFields,
}}
c := schemaCoercingConverter{delegate: t.converter, validator: unstructuredSchemaCoercer{
structuralSchema: t.structuralSchema,
structuralSchemaGK: t.structuralSchemaGK,
preserveUnknownFields: t.preserveUnknownFields,
}} }}
c := schemaCoercingConverter{delegate: t.converter, validator: unstructuredSchemaCoercer{}}
ret.StorageConfig.Codec = versioning.NewCodec( ret.StorageConfig.Codec = versioning.NewCodec(
ret.StorageConfig.Codec, ret.StorageConfig.Codec,
d, d,
...@@ -894,13 +942,17 @@ func (v schemaCoercingConverter) ConvertFieldLabel(gvk schema.GroupVersionKind, ...@@ -894,13 +942,17 @@ func (v schemaCoercingConverter) ConvertFieldLabel(gvk schema.GroupVersionKind,
return v.delegate.ConvertFieldLabel(gvk, label, value) return v.delegate.ConvertFieldLabel(gvk, label, value)
} }
// unstructuredSchemaCoercer does the validation for Unstructured that json.Unmarshal // unstructuredSchemaCoercer adds to unstructured unmarshalling what json.Unmarshal does
// does for native types. This includes: // in addition for native types when decoding into Golang structs:
// - validating and pruning ObjectMeta (here with optional error instead of pruning) //
// - TODO: application of an OpenAPI validator (against the whole object or a top-level field of it). // - validating and pruning ObjectMeta
// - TODO: optionally application of post-validation algorithms like defaulting and/or OpenAPI based pruning. // - generic pruning of unknown fields following a structural schema.
type unstructuredSchemaCoercer struct { type unstructuredSchemaCoercer struct {
dropInvalidMetadata bool dropInvalidMetadata bool
structuralSchema *structuralschema.Structural
structuralSchemaGK schema.GroupKind
preserveUnknownFields bool
} }
func (v *unstructuredSchemaCoercer) apply(u *unstructured.Unstructured) error { func (v *unstructuredSchemaCoercer) apply(u *unstructured.Unstructured) error {
...@@ -918,6 +970,15 @@ func (v *unstructuredSchemaCoercer) apply(u *unstructured.Unstructured) error { ...@@ -918,6 +970,15 @@ func (v *unstructuredSchemaCoercer) apply(u *unstructured.Unstructured) error {
return err return err
} }
// compare group and kind because also other object like DeleteCollection options pass through here
gv, err := schema.ParseGroupVersion(apiVersion)
if err != nil {
return err
}
if !v.preserveUnknownFields && gv.Group == v.structuralSchemaGK.Group && kind == v.structuralSchemaGK.Kind {
structuralpruning.Prune(u.Object, v.structuralSchema)
}
// restore meta fields, starting clean // restore meta fields, starting clean
if foundKind { if foundKind {
u.SetKind(kind) u.SetKind(kind)
......
...@@ -31,7 +31,10 @@ filegroup( ...@@ -31,7 +31,10 @@ filegroup(
filegroup( filegroup(
name = "all-srcs", name = "all-srcs",
srcs = [":package-srcs"], srcs = [
":package-srcs",
"//staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema/pruning:all-srcs",
],
tags = ["automanaged"], tags = ["automanaged"],
visibility = ["//visibility:public"], visibility = ["//visibility:public"],
) )
......
load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test")
go_library(
name = "go_default_library",
srcs = ["algorithm.go"],
importmap = "k8s.io/kubernetes/vendor/k8s.io/apiextensions-apiserver/pkg/apiserver/schema/pruning",
importpath = "k8s.io/apiextensions-apiserver/pkg/apiserver/schema/pruning",
visibility = ["//visibility:public"],
deps = ["//staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema:go_default_library"],
)
go_test(
name = "go_default_test",
srcs = ["algorithm_test.go"],
embed = [":go_default_library"],
deps = [
"//staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema:go_default_library",
"//staging/src/k8s.io/apimachinery/pkg/runtime:go_default_library",
"//staging/src/k8s.io/apimachinery/pkg/util/json: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 pruning
import (
"fmt"
structuralschema "k8s.io/apiextensions-apiserver/pkg/apiserver/schema"
)
// Prune removes object fields in obj which are not specified in s.
func Prune(obj map[string]interface{}, s *structuralschema.Structural) {
prune(obj, s)
}
func prune(x interface{}, s *structuralschema.Structural) {
if s != nil && s.XPreserveUnknownFields {
skipPrune(x, s)
return
}
switch x := x.(type) {
case map[string]interface{}:
if s == nil {
for k := range x {
delete(x, k)
}
return
}
for k, v := range x {
prop, ok := s.Properties[k]
if ok {
prune(v, &prop)
} else if s.AdditionalProperties != nil {
prune(v, s.AdditionalProperties.Structural)
} else {
delete(x, k)
}
fmt.Printf("deleting %q => %#v\n", k, x)
}
case []interface{}:
if s == nil {
for _, v := range x {
prune(v, nil)
}
return
}
for _, v := range x {
prune(v, s.Items)
}
default:
// scalars, do nothing
}
}
func skipPrune(x interface{}, s *structuralschema.Structural) {
if s == nil {
return
}
switch x := x.(type) {
case map[string]interface{}:
for k, v := range x {
if prop, ok := s.Properties[k]; ok {
prune(v, &prop)
} else {
skipPrune(v, nil)
}
}
case []interface{}:
for _, v := range x {
skipPrune(v, s.Items)
}
default:
// scalars, do nothing
}
}
/*
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 pruning
import (
"bytes"
"reflect"
"testing"
structuralschema "k8s.io/apiextensions-apiserver/pkg/apiserver/schema"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/util/json"
)
func TestPrune(t *testing.T) {
tests := []struct {
name string
json string
schema *structuralschema.Structural
expected string
}{
{"empty", "null", nil, "null"},
{"scalar", "4", &structuralschema.Structural{}, "4"},
{"scalar array", "[1,2]", &structuralschema.Structural{
Items: &structuralschema.Structural{},
}, "[1,2]"},
{"object array", `[{"a":1},{"b":1},{"a":1,"b":2,"c":3}]`, &structuralschema.Structural{
Items: &structuralschema.Structural{
Properties: map[string]structuralschema.Structural{
"a": {},
"c": {},
},
},
}, `[{"a":1},{},{"a":1,"c":3}]`},
{"object array with nil schema", `[{"a":1},{"b":1},{"a":1,"b":2,"c":3}]`, nil, `[{},{},{}]`},
{"object array object", `{"array":[{"a":1},{"b":1},{"a":1,"b":2,"c":3}],"unspecified":{"a":1},"specified":{"a":1,"b":2,"c":3}}`, &structuralschema.Structural{
Properties: map[string]structuralschema.Structural{
"array": {
Items: &structuralschema.Structural{
Properties: map[string]structuralschema.Structural{
"a": {},
"c": {},
},
},
},
"specified": {
Properties: map[string]structuralschema.Structural{
"a": {},
"c": {},
},
},
},
}, `{"array":[{"a":1},{},{"a":1,"c":3}],"specified":{"a":1,"c":3}}`},
{"nested x-kubernetes-preserve-unknown-fields", `
{
"unspecified":"bar",
"alpha": "abc",
"beta": 42.0,
"unspecifiedObject": {"unspecified": "bar"},
"pruning": {
"unspecified": "bar",
"unspecifiedObject": {"unspecified": "bar"},
"pruning": {"unspecified": "bar"},
"preserving": {"unspecified": "bar"}
},
"preserving": {
"unspecified": "bar",
"unspecifiedObject": {"unspecified": "bar"},
"pruning": {"unspecified": "bar"},
"preserving": {"unspecified": "bar"}
}
}
`, &structuralschema.Structural{
Generic: structuralschema.Generic{Type: "object"},
Extensions: structuralschema.Extensions{XPreserveUnknownFields: true},
Properties: map[string]structuralschema.Structural{
"alpha": {Generic: structuralschema.Generic{Type: "string"}},
"beta": {Generic: structuralschema.Generic{Type: "number"}},
"pruning": {
Generic: structuralschema.Generic{Type: "object"},
Properties: map[string]structuralschema.Structural{
"preserving": {
Generic: structuralschema.Generic{Type: "object"},
Extensions: structuralschema.Extensions{XPreserveUnknownFields: true},
},
"pruning": {
Generic: structuralschema.Generic{Type: "object"},
},
},
},
"preserving": {
Generic: structuralschema.Generic{Type: "object"},
Extensions: structuralschema.Extensions{XPreserveUnknownFields: true},
Properties: map[string]structuralschema.Structural{
"preserving": {
Generic: structuralschema.Generic{Type: "object"},
Extensions: structuralschema.Extensions{XPreserveUnknownFields: true},
},
"pruning": {
Generic: structuralschema.Generic{Type: "object"},
},
},
},
},
}, `
{
"unspecified":"bar",
"alpha": "abc",
"beta": 42.0,
"unspecifiedObject": {"unspecified": "bar"},
"pruning": {
"pruning": {},
"preserving": {"unspecified": "bar"}
},
"preserving": {
"unspecified": "bar",
"unspecifiedObject": {"unspecified": "bar"},
"pruning": {},
"preserving": {"unspecified": "bar"}
}
}
`},
{"additionalProperties with schema", `{"a":1,"b":1,"c":{"a":1,"b":2,"c":{"a":1}}}`, &structuralschema.Structural{
Properties: map[string]structuralschema.Structural{
"a": {},
"c": {
Generic: structuralschema.Generic{
AdditionalProperties: &structuralschema.StructuralOrBool{
Structural: &structuralschema.Structural{
Generic: structuralschema.Generic{
Type: "integer",
},
},
},
},
},
},
}, `{"a":1,"c":{"a":1,"b":2,"c":{}}}`},
{"additionalProperties with bool", `{"a":1,"b":1,"c":{"a":1,"b":2,"c":{"a":1}}}`, &structuralschema.Structural{
Properties: map[string]structuralschema.Structural{
"a": {},
"c": {
Generic: structuralschema.Generic{
AdditionalProperties: &structuralschema.StructuralOrBool{
Bool: false,
},
},
},
},
}, `{"a":1,"c":{"a":1,"b":2,"c":{}}}`},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var in interface{}
if err := json.Unmarshal([]byte(tt.json), &in); err != nil {
t.Fatal(err)
}
var expected interface{}
if err := json.Unmarshal([]byte(tt.expected), &expected); err != nil {
t.Fatal(err)
}
prune(in, tt.schema)
if !reflect.DeepEqual(in, expected) {
var buf bytes.Buffer
enc := json.NewEncoder(&buf)
enc.SetIndent("", " ")
err := enc.Encode(in)
if err != nil {
t.Fatalf("unexpected result mashalling error: %v", err)
}
t.Errorf("expected: %s\ngot: %s", tt.expected, buf.String())
}
})
}
}
const smallInstance = `
{
"unspecified":"bar",
"alpha": "abc",
"beta": 42.0,
"unspecifiedObject": {"unspecified": "bar"},
"pruning": {
"pruning": {},
"preserving": {"unspecified": "bar"}
},
"preserving": {
"unspecified": "bar",
"unspecifiedObject": {"unspecified": "bar"},
"pruning": {},
"preserving": {"unspecified": "bar"}
}
}
`
func BenchmarkPrune(b *testing.B) {
b.StopTimer()
b.ReportAllocs()
schema := &structuralschema.Structural{
Generic: structuralschema.Generic{Type: "object"},
Extensions: structuralschema.Extensions{XPreserveUnknownFields: true},
Properties: map[string]structuralschema.Structural{
"alpha": {Generic: structuralschema.Generic{Type: "string"}},
"beta": {Generic: structuralschema.Generic{Type: "number"}},
"pruning": {
Generic: structuralschema.Generic{Type: "object"},
Properties: map[string]structuralschema.Structural{
"preserving": {
Generic: structuralschema.Generic{Type: "object"},
Extensions: structuralschema.Extensions{XPreserveUnknownFields: true},
},
"pruning": {
Generic: structuralschema.Generic{Type: "object"},
},
},
},
"preserving": {
Generic: structuralschema.Generic{Type: "object"},
Extensions: structuralschema.Extensions{XPreserveUnknownFields: true},
Properties: map[string]structuralschema.Structural{
"preserving": {
Generic: structuralschema.Generic{Type: "object"},
Extensions: structuralschema.Extensions{XPreserveUnknownFields: true},
},
"pruning": {
Generic: structuralschema.Generic{Type: "object"},
},
},
},
},
}
var obj map[string]interface{}
err := json.Unmarshal([]byte(smallInstance), &obj)
if err != nil {
b.Fatal(err)
}
instances := make([]map[string]interface{}, 0, b.N)
for i := 0; i < b.N; i++ {
instances = append(instances, runtime.DeepCopyJSON(obj))
}
b.StartTimer()
for i := 0; i < b.N; i++ {
Prune(instances[i], schema)
}
}
func BenchmarkDeepCopy(b *testing.B) {
b.StopTimer()
b.ReportAllocs()
var obj map[string]interface{}
err := json.Unmarshal([]byte(smallInstance), &obj)
if err != nil {
b.Fatal(err)
}
instances := make([]map[string]interface{}, 0, b.N)
b.StartTimer()
for i := 0; i < b.N; i++ {
instances = append(instances, runtime.DeepCopyJSON(obj))
}
}
func BenchmarkUnmarshal(b *testing.B) {
b.StopTimer()
b.ReportAllocs()
instances := make([]map[string]interface{}, b.N)
b.StartTimer()
for i := 0; i < b.N; i++ {
err := json.Unmarshal([]byte(smallInstance), &instances[i])
if err != nil {
b.Fatal(err)
}
}
}
...@@ -14,6 +14,7 @@ go_test( ...@@ -14,6 +14,7 @@ go_test(
"change_test.go", "change_test.go",
"finalization_test.go", "finalization_test.go",
"objectmeta_test.go", "objectmeta_test.go",
"pruning_test.go",
"registration_test.go", "registration_test.go",
"subresources_test.go", "subresources_test.go",
"table_test.go", "table_test.go",
...@@ -54,6 +55,7 @@ go_test( ...@@ -54,6 +55,7 @@ go_test(
"//vendor/github.com/coreos/etcd/pkg/transport:go_default_library", "//vendor/github.com/coreos/etcd/pkg/transport:go_default_library",
"//vendor/github.com/stretchr/testify/assert:go_default_library", "//vendor/github.com/stretchr/testify/assert:go_default_library",
"//vendor/github.com/stretchr/testify/require:go_default_library", "//vendor/github.com/stretchr/testify/require:go_default_library",
"//vendor/k8s.io/utils/pointer:go_default_library",
"//vendor/sigs.k8s.io/yaml:go_default_library", "//vendor/sigs.k8s.io/yaml:go_default_library",
], ],
) )
......
...@@ -181,7 +181,7 @@ var _ = SIGDescribe("AdmissionWebhook", func() { ...@@ -181,7 +181,7 @@ var _ = SIGDescribe("AdmissionWebhook", func() {
defer testcrd.CleanUp() defer testcrd.CleanUp()
webhookCleanup := registerMutatingWebhookForCustomResource(f, context, testcrd) webhookCleanup := registerMutatingWebhookForCustomResource(f, context, testcrd)
defer webhookCleanup() defer webhookCleanup()
testMutatingCustomResourceWebhook(f, testcrd.Crd, testcrd.DynamicClients["v1"]) testMutatingCustomResourceWebhook(f, testcrd.Crd, testcrd.DynamicClients["v1"], false)
}) })
ginkgo.It("Should deny crd creation", func() { ginkgo.It("Should deny crd creation", func() {
...@@ -202,6 +202,35 @@ var _ = SIGDescribe("AdmissionWebhook", func() { ...@@ -202,6 +202,35 @@ var _ = SIGDescribe("AdmissionWebhook", func() {
testMultiVersionCustomResourceWebhook(f, testcrd) testMultiVersionCustomResourceWebhook(f, testcrd)
}) })
ginkgo.It("Should mutate custom resource with pruning", func() {
const prune = true
testcrd, err := createAdmissionWebhookMultiVersionTestCRDWithV1Storage(f, func(crd *apiextensionsv1beta1.CustomResourceDefinition) {
crd.Spec.PreserveUnknownFields = pointer.BoolPtr(false)
crd.Spec.Validation = &apiextensionsv1beta1.CustomResourceValidation{
OpenAPIV3Schema: &apiextensionsv1beta1.JSONSchemaProps{
Type: "object",
Properties: map[string]apiextensionsv1beta1.JSONSchemaProps{
"data": {
Type: "object",
Properties: map[string]apiextensionsv1beta1.JSONSchemaProps{
"mutation-start": {Type: "string"},
"mutation-stage-1": {Type: "string"},
// mutation-stage-2 is intentionally missing such that it is pruned
},
},
},
},
}
})
if err != nil {
return
}
defer testcrd.CleanUp()
webhookCleanup := registerMutatingWebhookForCustomResource(f, context, testcrd)
defer webhookCleanup()
testMutatingCustomResourceWebhook(f, testcrd.Crd, testcrd.DynamicClients["v1"], prune)
})
ginkgo.It("Should deny crd creation", func() { ginkgo.It("Should deny crd creation", func() {
crdWebhookCleanup := registerValidatingWebhookForCRD(f, context) crdWebhookCleanup := registerValidatingWebhookForCRD(f, context)
defer crdWebhookCleanup() defer crdWebhookCleanup()
...@@ -1329,7 +1358,7 @@ func testCustomResourceWebhook(f *framework.Framework, crd *apiextensionsv1beta1 ...@@ -1329,7 +1358,7 @@ func testCustomResourceWebhook(f *framework.Framework, crd *apiextensionsv1beta1
} }
} }
func testMutatingCustomResourceWebhook(f *framework.Framework, crd *apiextensionsv1beta1.CustomResourceDefinition, customResourceClient dynamic.ResourceInterface) { func testMutatingCustomResourceWebhook(f *framework.Framework, crd *apiextensionsv1beta1.CustomResourceDefinition, customResourceClient dynamic.ResourceInterface, prune bool) {
ginkgo.By("Creating a custom resource that should be mutated by the webhook") ginkgo.By("Creating a custom resource that should be mutated by the webhook")
crName := "cr-instance-1" crName := "cr-instance-1"
cr := &unstructured.Unstructured{ cr := &unstructured.Unstructured{
...@@ -1350,7 +1379,9 @@ func testMutatingCustomResourceWebhook(f *framework.Framework, crd *apiextension ...@@ -1350,7 +1379,9 @@ func testMutatingCustomResourceWebhook(f *framework.Framework, crd *apiextension
expectedCRData := map[string]interface{}{ expectedCRData := map[string]interface{}{
"mutation-start": "yes", "mutation-start": "yes",
"mutation-stage-1": "yes", "mutation-stage-1": "yes",
"mutation-stage-2": "yes", }
if !prune {
expectedCRData["mutation-stage-2"] = "yes"
} }
if !reflect.DeepEqual(expectedCRData, mutatedCR.Object["data"]) { if !reflect.DeepEqual(expectedCRData, mutatedCR.Object["data"]) {
framework.Failf("\nexpected %#v\n, got %#v\n", expectedCRData, mutatedCR.Object["data"]) framework.Failf("\nexpected %#v\n, got %#v\n", expectedCRData, mutatedCR.Object["data"])
...@@ -1571,9 +1602,9 @@ func testSlowWebhookTimeoutNoError(f *framework.Framework) { ...@@ -1571,9 +1602,9 @@ func testSlowWebhookTimeoutNoError(f *framework.Framework) {
// createAdmissionWebhookMultiVersionTestCRDWithV1Storage creates a new CRD specifically // createAdmissionWebhookMultiVersionTestCRDWithV1Storage creates a new CRD specifically
// for the admissin webhook calling test. // for the admissin webhook calling test.
func createAdmissionWebhookMultiVersionTestCRDWithV1Storage(f *framework.Framework) (*crd.TestCrd, error) { func createAdmissionWebhookMultiVersionTestCRDWithV1Storage(f *framework.Framework, opts ...crd.Option) (*crd.TestCrd, error) {
group := fmt.Sprintf("%s-multiversion-crd-test.k8s.io", f.BaseName) group := fmt.Sprintf("%s-multiversion-crd-test.k8s.io", f.BaseName)
return crd.CreateMultiVersionTestCRD(f, group, func(crd *apiextensionsv1beta1.CustomResourceDefinition) { return crd.CreateMultiVersionTestCRD(f, group, append([]crd.Option{func(crd *apiextensionsv1beta1.CustomResourceDefinition) {
crd.Spec.Versions = []apiextensionsv1beta1.CustomResourceDefinitionVersion{ crd.Spec.Versions = []apiextensionsv1beta1.CustomResourceDefinitionVersion{
{ {
Name: "v1", Name: "v1",
...@@ -1586,7 +1617,7 @@ func createAdmissionWebhookMultiVersionTestCRDWithV1Storage(f *framework.Framewo ...@@ -1586,7 +1617,7 @@ func createAdmissionWebhookMultiVersionTestCRDWithV1Storage(f *framework.Framewo
Storage: false, Storage: false,
}, },
} }
}) }}, opts...)...)
} }
// servedAPIVersions returns the API versions served by the CRD. // servedAPIVersions returns the API versions served by the CRD.
......
...@@ -113,6 +113,9 @@ var ( ...@@ -113,6 +113,9 @@ var (
gvr("", "v1", "nodes/proxy"): {"*": testSubresourceProxy}, gvr("", "v1", "nodes/proxy"): {"*": testSubresourceProxy},
gvr("", "v1", "pods/proxy"): {"*": testSubresourceProxy}, gvr("", "v1", "pods/proxy"): {"*": testSubresourceProxy},
gvr("", "v1", "services/proxy"): {"*": testSubresourceProxy}, gvr("", "v1", "services/proxy"): {"*": testSubresourceProxy},
gvr("random.numbers.com", "v1", "integers"): {"create": testPruningRandomNumbers},
gvr("custom.fancy.com", "v2", "pants"): {"create": testNoPruningCustomFancy},
} }
// admissionExemptResources lists objects which are exempt from admission validation/mutation, // admissionExemptResources lists objects which are exempt from admission validation/mutation,
...@@ -921,6 +924,46 @@ func testSubresourceProxy(c *testContext) { ...@@ -921,6 +924,46 @@ func testSubresourceProxy(c *testContext) {
} }
} }
func testPruningRandomNumbers(c *testContext) {
testResourceCreate(c)
cr2pant, err := c.client.Resource(c.gvr).Get("fortytwo", metav1.GetOptions{})
if err != nil {
c.t.Error(err)
return
}
foo, found, err := unstructured.NestedString(cr2pant.Object, "foo")
if err != nil {
c.t.Error(err)
return
}
if found {
c.t.Errorf("expected .foo to be pruned, but got: %s", foo)
}
}
func testNoPruningCustomFancy(c *testContext) {
testResourceCreate(c)
cr2pant, err := c.client.Resource(c.gvr).Get("cr2pant", metav1.GetOptions{})
if err != nil {
c.t.Error(err)
return
}
foo, _, err := unstructured.NestedString(cr2pant.Object, "foo")
if err != nil {
c.t.Error(err)
return
}
// check that no pruning took place
if expected, got := "test", foo; expected != got {
c.t.Errorf("expected /foo to be %q, got: %q", expected, got)
}
}
// //
// utility methods // utility methods
// //
......
...@@ -76,5 +76,6 @@ go_library( ...@@ -76,5 +76,6 @@ go_library(
"//test/integration/framework:go_default_library", "//test/integration/framework:go_default_library",
"//vendor/github.com/coreos/etcd/clientv3:go_default_library", "//vendor/github.com/coreos/etcd/clientv3:go_default_library",
"//vendor/github.com/coreos/etcd/clientv3/concurrency:go_default_library", "//vendor/github.com/coreos/etcd/clientv3/concurrency:go_default_library",
"//vendor/k8s.io/utils/pointer:go_default_library",
], ],
) )
...@@ -22,6 +22,7 @@ import ( ...@@ -22,6 +22,7 @@ import (
"k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/runtime/schema"
utilfeature "k8s.io/apiserver/pkg/util/feature" utilfeature "k8s.io/apiserver/pkg/util/feature"
"k8s.io/kubernetes/pkg/features" "k8s.io/kubernetes/pkg/features"
"k8s.io/utils/pointer"
) )
// GetEtcdStorageData returns etcd data for all persisted objects. // GetEtcdStorageData returns etcd data for all persisted objects.
...@@ -485,6 +486,10 @@ func GetEtcdStorageDataForNamespace(namespace string) map[schema.GroupVersionRes ...@@ -485,6 +486,10 @@ func GetEtcdStorageDataForNamespace(namespace string) map[schema.GroupVersionRes
ExpectedEtcdPath: "/registry/awesome.bears.com/pandas/cr4panda", ExpectedEtcdPath: "/registry/awesome.bears.com/pandas/cr4panda",
ExpectedGVK: gvkP("awesome.bears.com", "v1", "Panda"), ExpectedGVK: gvkP("awesome.bears.com", "v1", "Panda"),
}, },
gvr("random.numbers.com", "v1", "integers"): {
Stub: `{"kind": "Integer", "apiVersion": "random.numbers.com/v1", "metadata": {"name": "fortytwo"}, "value": 42, "garbage": "oiujnasdf"}`, // requires TypeMeta due to CRD scheme's UnstructuredObjectTyper
ExpectedEtcdPath: "/registry/random.numbers.com/integers/fortytwo",
},
// -- // --
// k8s.io/kubernetes/pkg/apis/auditregistration/v1alpha1 // k8s.io/kubernetes/pkg/apis/auditregistration/v1alpha1
...@@ -580,6 +585,32 @@ func GetCustomResourceDefinitionData() []*apiextensionsv1beta1.CustomResourceDef ...@@ -580,6 +585,32 @@ func GetCustomResourceDefinitionData() []*apiextensionsv1beta1.CustomResourceDef
}, },
}, },
}, },
// cluster scoped with legacy version field and pruning.
{
ObjectMeta: metav1.ObjectMeta{
Name: "integers.random.numbers.com",
},
Spec: apiextensionsv1beta1.CustomResourceDefinitionSpec{
Group: "random.numbers.com",
Version: "v1",
Scope: apiextensionsv1beta1.ClusterScoped,
Names: apiextensionsv1beta1.CustomResourceDefinitionNames{
Plural: "integers",
Kind: "Integer",
},
Validation: &apiextensionsv1beta1.CustomResourceValidation{
OpenAPIV3Schema: &apiextensionsv1beta1.JSONSchemaProps{
Type: "object",
Properties: map[string]apiextensionsv1beta1.JSONSchemaProps{
"value": {
Type: "number",
},
},
},
},
PreserveUnknownFields: pointer.BoolPtr(false),
},
},
// cluster scoped with versions field // cluster scoped with versions field
{ {
ObjectMeta: metav1.ObjectMeta{ ObjectMeta: metav1.ObjectMeta{
......
...@@ -1073,6 +1073,7 @@ k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/validation ...@@ -1073,6 +1073,7 @@ k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/validation
k8s.io/apiextensions-apiserver/pkg/apiserver k8s.io/apiextensions-apiserver/pkg/apiserver
k8s.io/apiextensions-apiserver/pkg/apiserver/conversion k8s.io/apiextensions-apiserver/pkg/apiserver/conversion
k8s.io/apiextensions-apiserver/pkg/apiserver/schema k8s.io/apiextensions-apiserver/pkg/apiserver/schema
k8s.io/apiextensions-apiserver/pkg/apiserver/schema/pruning
k8s.io/apiextensions-apiserver/pkg/apiserver/validation k8s.io/apiextensions-apiserver/pkg/apiserver/validation
k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset
k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/scheme k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/scheme
......
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