apiextensions: wire pruning into handler

This commit is contained in:
Dr. Stefan Schimanski 2019-05-02 12:03:42 +02:00
parent 52577aa908
commit 70ee02725f

View File

@ -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 (
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
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)
@ -574,6 +591,8 @@ func (r *crdHandler) getOrCreateServingInfoFor(crd *apiextensions.CustomResource
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,
preserveUnknownFields: *crd.Spec.PreserveUnknownFields,
}, },
crd.Status.AcceptedNames.Categories, crd.Status.AcceptedNames.Categories,
table, table,
@ -595,7 +614,7 @@ 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, preserveUnknownFields: *crd.Spec.PreserveUnknownFields},
ParameterCodec: parameterCodec, ParameterCodec: parameterCodec,
Creater: creator, Creater: creator,
@ -646,6 +665,7 @@ 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, preserveUnknownFields: *crd.Spec.PreserveUnknownFields}
statusScope.Namer = handlers.ContextBasedNaming{ statusScope.Namer = handlers.ContextBasedNaming{
SelfLinker: meta.NewAccessor(), SelfLinker: meta.NewAccessor(),
ClusterScoped: clusterScoped, ClusterScoped: clusterScoped,
@ -680,6 +700,9 @@ type unstructuredNegotiatedSerializer struct {
typer runtime.ObjectTyper typer runtime.ObjectTyper
creator runtime.ObjectCreater creator runtime.ObjectCreater
converter runtime.ObjectConvertor converter runtime.ObjectConvertor
structuralSchema *structuralschema.Structural
preserveUnknownFields bool
} }
func (s unstructuredNegotiatedSerializer) SupportedMediaTypes() []runtime.SerializerInfo { func (s unstructuredNegotiatedSerializer) SupportedMediaTypes() []runtime.SerializerInfo {
@ -712,7 +735,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, preserveUnknownFields: s.preserveUnknownFields}}
return versioning.NewDefaultingCodecForScheme(Scheme, nil, d, nil, gv) return versioning.NewDefaultingCodecForScheme(Scheme, nil, d, nil, gv)
} }
@ -804,16 +827,20 @@ type crdConversionRESTOptionsGetter struct {
converter runtime.ObjectConvertor converter runtime.ObjectConvertor
encoderVersion schema.GroupVersion encoderVersion schema.GroupVersion
decoderVersion schema.GroupVersion decoderVersion schema.GroupVersion
structuralSchema *structuralschema.Structural
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,
preserveUnknownFields: t.preserveUnknownFields,
}} }}
c := schemaCoercingConverter{delegate: t.converter, validator: unstructuredSchemaCoercer{}} c := schemaCoercingConverter{delegate: t.converter, validator: unstructuredSchemaCoercer{structuralSchema: t.structuralSchema, preserveUnknownFields: t.preserveUnknownFields}}
ret.StorageConfig.Codec = versioning.NewCodec( ret.StorageConfig.Codec = versioning.NewCodec(
ret.StorageConfig.Codec, ret.StorageConfig.Codec,
d, d,
@ -894,13 +921,15 @@ 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
preserveUnknownFields bool
} }
func (v *unstructuredSchemaCoercer) apply(u *unstructured.Unstructured) error { func (v *unstructuredSchemaCoercer) apply(u *unstructured.Unstructured) error {
@ -918,6 +947,10 @@ func (v *unstructuredSchemaCoercer) apply(u *unstructured.Unstructured) error {
return err return err
} }
if !v.preserveUnknownFields {
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)