diff --git a/pkg/master/thirdparty/thirdparty.go b/pkg/master/thirdparty/thirdparty.go index e1a463a660c..8e00569f996 100644 --- a/pkg/master/thirdparty/thirdparty.go +++ b/pkg/master/thirdparty/thirdparty.go @@ -261,7 +261,7 @@ func (m *ThirdPartyResourceServer) migrateThirdPartyResourceData(gvk schema.Grou 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}, apiextensionsserver.UnstructuredCopier{}, - customresource.NewStrategy(discoveryclient.NewUnstructuredObjectTyper(nil), true), + customresource.NewStrategy(discoveryclient.NewUnstructuredObjectTyper(nil), true, gvk), m.crdRESTOptionsGetter, ) diff --git a/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/BUILD b/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/BUILD index 20a8d2d02b2..4885fd6c081 100644 --- a/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/BUILD +++ b/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/BUILD @@ -42,6 +42,7 @@ go_library( "//vendor/k8s.io/apimachinery/pkg/runtime/schema:go_default_library", "//vendor/k8s.io/apimachinery/pkg/runtime/serializer:go_default_library", "//vendor/k8s.io/apimachinery/pkg/runtime/serializer/json:go_default_library", + "//vendor/k8s.io/apimachinery/pkg/runtime/serializer/versioning:go_default_library", "//vendor/k8s.io/apimachinery/pkg/types:go_default_library", "//vendor/k8s.io/apimachinery/pkg/util/runtime:go_default_library", "//vendor/k8s.io/apimachinery/pkg/util/wait:go_default_library", 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 8dfe59e105c..24778128fc9 100644 --- a/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/apiserver.go +++ b/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/apiserver.go @@ -56,6 +56,17 @@ var ( registry = registered.NewOrDie("") Scheme = runtime.NewScheme() Codecs = serializer.NewCodecFactory(Scheme) + + // if you modify this, make sure you update the crEncoder + unversionedVersion = schema.GroupVersion{Group: "", Version: "v1"} + unversionedTypes = []runtime.Object{ + &metav1.Status{}, + &metav1.WatchEvent{}, + &metav1.APIVersions{}, + &metav1.APIGroupList{}, + &metav1.APIGroup{}, + &metav1.APIResourceList{}, + } ) func init() { @@ -64,14 +75,7 @@ func init() { // we need to add the options to empty v1 metav1.AddToGroupVersion(Scheme, schema.GroupVersion{Group: "", Version: "v1"}) - unversioned := schema.GroupVersion{Group: "", Version: "v1"} - Scheme.AddUnversionedTypes(unversioned, - &metav1.Status{}, - &metav1.APIVersions{}, - &metav1.APIGroupList{}, - &metav1.APIGroup{}, - &metav1.APIResourceList{}, - ) + Scheme.AddUnversionedTypes(unversionedVersion, unversionedTypes...) } type Config struct { 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 1a251fda2e3..b505ef1c9cd 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 @@ -19,6 +19,7 @@ package apiserver import ( "bytes" "fmt" + "io" "net/http" "path" "sync" @@ -33,6 +34,7 @@ import ( "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/runtime/serializer/json" + "k8s.io/apimachinery/pkg/runtime/serializer/versioning" "k8s.io/apimachinery/pkg/types" utilruntime "k8s.io/apimachinery/pkg/util/runtime" "k8s.io/apiserver/pkg/admission" @@ -358,13 +360,36 @@ func (s UnstructuredNegotiatedSerializer) SupportedMediaTypes() []runtime.Serial } func (s UnstructuredNegotiatedSerializer) EncoderForVersion(serializer runtime.Encoder, gv runtime.GroupVersioner) runtime.Encoder { - return unstructured.UnstructuredJSONScheme + return versioning.NewDefaultingCodecForScheme(Scheme, crEncoderInstance, nil, gv, nil) } func (s UnstructuredNegotiatedSerializer) DecoderToVersion(serializer runtime.Decoder, gv runtime.GroupVersioner) runtime.Decoder { return unstructured.UnstructuredJSONScheme } +var crEncoderInstance = crEncoder{} + +// crEncoder *usually* encodes using the unstructured.UnstructuredJSONScheme, but if the type is Status or WatchEvent +// it will serialize them out using the converting codec. +type crEncoder struct{} + +func (crEncoder) Encode(obj runtime.Object, w io.Writer) error { + switch t := obj.(type) { + case *metav1.Status, *metav1.WatchEvent: + for _, info := range Codecs.SupportedMediaTypes() { + // we are always json + if info.MediaType == "application/json" { + return info.Serializer.Encode(obj, w) + } + } + + return fmt.Errorf("unable to find json serializer for %T", t) + + default: + return unstructured.UnstructuredJSONScheme.Encode(obj, w) + } +} + type UnstructuredCreator struct{} func (UnstructuredCreator) New(kind schema.GroupVersionKind) (runtime.Object, error) { diff --git a/staging/src/k8s.io/apiextensions-apiserver/test/integration/BUILD b/staging/src/k8s.io/apiextensions-apiserver/test/integration/BUILD index fda8c71fcbe..893ba587931 100644 --- a/staging/src/k8s.io/apiextensions-apiserver/test/integration/BUILD +++ b/staging/src/k8s.io/apiextensions-apiserver/test/integration/BUILD @@ -13,6 +13,7 @@ go_test( "basic_test.go", "finalization_test.go", "registration_test.go", + "validation_test.go", ], tags = [ "automanaged", diff --git a/staging/src/k8s.io/apiextensions-apiserver/test/integration/validation_test.go b/staging/src/k8s.io/apiextensions-apiserver/test/integration/validation_test.go new file mode 100644 index 00000000000..69ccb58aa4a --- /dev/null +++ b/staging/src/k8s.io/apiextensions-apiserver/test/integration/validation_test.go @@ -0,0 +1,81 @@ +/* +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 integration + +import ( + "strings" + "testing" + + apiextensionsv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1" + "k8s.io/apiextensions-apiserver/test/integration/testserver" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" +) + +func TestForProperValidationErrors(t *testing.T) { + stopCh, apiExtensionClient, clientPool, err := testserver.StartDefaultServer() + if err != nil { + t.Fatal(err) + } + defer close(stopCh) + + noxuDefinition := testserver.NewNoxuCustomResourceDefinition(apiextensionsv1beta1.NamespaceScoped) + noxuVersionClient, err := testserver.CreateNewCustomResourceDefinition(noxuDefinition, apiExtensionClient, clientPool) + if err != nil { + t.Fatal(err) + } + + ns := "not-the-default" + noxuResourceClient := NewNamespacedCustomResourceClient(ns, noxuVersionClient, noxuDefinition) + + tests := []struct { + name string + instanceFn func() *unstructured.Unstructured + expectedError string + }{ + { + name: "bad version", + instanceFn: func() *unstructured.Unstructured { + instance := testserver.NewNoxuInstance(ns, "foo") + instance.Object["apiVersion"] = "mygroup.example.com/v2" + return instance + }, + expectedError: "the API version in the data (mygroup.example.com/v2) does not match the expected API version (mygroup.example.com/v1beta1)", + }, + { + name: "bad kind", + instanceFn: func() *unstructured.Unstructured { + instance := testserver.NewNoxuInstance(ns, "foo") + instance.Object["kind"] = "SomethingElse" + return instance + }, + expectedError: `SomethingElse.mygroup.example.com "foo" is invalid: kind: Invalid value: "SomethingElse": must be WishIHadChosenNoxu`, + }, + } + + for _, tc := range tests { + _, err := noxuResourceClient.Create(tc.instanceFn()) + if err == nil { + t.Errorf("%v: expected %v", tc.name, tc.expectedError) + continue + } + // this only works when status errors contain the expect kind and version, so this effectively tests serializations too + if !strings.Contains(err.Error(), tc.expectedError) { + t.Errorf("%v: expected %v, got %v", tc.name, tc.expectedError, err) + continue + } + } +}