From 7e0775e5ec083f3a90c318950c8ee1dd5eb47a98 Mon Sep 17 00:00:00 2001 From: "Dr. Stefan Schimanski" Date: Fri, 7 Sep 2018 11:59:17 +0200 Subject: [PATCH 1/6] apiextensions: add smoke test checking that patches apply to non-storage versions --- .../test/integration/BUILD | 1 + .../test/integration/versioning_test.go | 102 ++++++++++++++++++ 2 files changed, 103 insertions(+) diff --git a/staging/src/k8s.io/apiextensions-apiserver/test/integration/BUILD b/staging/src/k8s.io/apiextensions-apiserver/test/integration/BUILD index 6929e34e283..4ac71b28dba 100644 --- a/staging/src/k8s.io/apiextensions-apiserver/test/integration/BUILD +++ b/staging/src/k8s.io/apiextensions-apiserver/test/integration/BUILD @@ -45,6 +45,7 @@ go_test( "//vendor/github.com/coreos/etcd/clientv3:go_default_library", "//vendor/github.com/coreos/etcd/pkg/transport:go_default_library", "//vendor/github.com/ghodss/yaml:go_default_library", + "//vendor/github.com/stretchr/testify/assert:go_default_library", "//vendor/github.com/stretchr/testify/require:go_default_library", ], ) diff --git a/staging/src/k8s.io/apiextensions-apiserver/test/integration/versioning_test.go b/staging/src/k8s.io/apiextensions-apiserver/test/integration/versioning_test.go index 7d305b081fd..7a813ab49cf 100644 --- a/staging/src/k8s.io/apiextensions-apiserver/test/integration/versioning_test.go +++ b/staging/src/k8s.io/apiextensions-apiserver/test/integration/versioning_test.go @@ -17,14 +17,116 @@ limitations under the License. package integration import ( + "fmt" + "net/http" "reflect" "testing" + "time" + + "github.com/stretchr/testify/assert" + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/types" + "k8s.io/apimachinery/pkg/util/wait" apiextensionsv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1" "k8s.io/apiextensions-apiserver/test/integration/fixtures" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) +func TestInternalVersionIsHandlerVersion(t *testing.T) { + tearDown, apiExtensionClient, dynamicClient, err := fixtures.StartDefaultServerWithClients(t) + if err != nil { + t.Fatal(err) + } + defer tearDown() + + noxuDefinition := fixtures.NewMultipleVersionNoxuCRD(apiextensionsv1beta1.NamespaceScoped) + + assert.Equal(t, "v1beta1", noxuDefinition.Spec.Versions[0].Name) + assert.Equal(t, "v1beta2", noxuDefinition.Spec.Versions[1].Name) + assert.True(t, noxuDefinition.Spec.Versions[1].Storage) + + noxuDefinition, err = fixtures.CreateNewCustomResourceDefinition(noxuDefinition, apiExtensionClient, dynamicClient) + if err != nil { + t.Fatal(err) + } + + ns := "not-the-default" + + noxuNamespacedResourceClientV1beta1 := newNamespacedCustomResourceVersionedClient(ns, dynamicClient, noxuDefinition, "v1beta1") // use the non-storage version v1beta1 + + t.Logf("Creating foo") + noxuInstanceToCreate := fixtures.NewNoxuInstance(ns, "foo") + _, err = noxuNamespacedResourceClientV1beta1.Create(noxuInstanceToCreate, metav1.CreateOptions{}) + if err != nil { + t.Fatal(err) + } + + // update validation via update because the cache priming in CreateNewCustomResourceDefinition will fail otherwise + t.Logf("Updating CRD to validate apiVersion") + noxuDefinition, err = updateCustomResourceDefinitionWithRetry(apiExtensionClient, noxuDefinition.Name, func(crd *apiextensionsv1beta1.CustomResourceDefinition) { + crd.Spec.Validation = &apiextensionsv1beta1.CustomResourceValidation{ + OpenAPIV3Schema: &apiextensionsv1beta1.JSONSchemaProps{ + Properties: map[string]apiextensionsv1beta1.JSONSchemaProps{ + "apiVersion": { + Pattern: "^mygroup.example.com/v1beta1$", // this means we can only patch via the v1beta1 handler version + }, + }, + Required: []string{"apiVersion"}, + }, + } + }) + assert.NoError(t, err) + + time.Sleep(time.Second) + + // patches via handler version v1beta1 should succeed (validation allows that API version) + { + t.Logf("patch of handler version v1beta1 (non-storage version) should succeed") + i := 0 + err = wait.PollImmediate(time.Millisecond*100, wait.ForeverTestTimeout, func() (bool, error) { + patch := []byte(fmt.Sprintf(`{"i": %d}`, i)) + i++ + + _, err := noxuNamespacedResourceClientV1beta1.Patch("foo", types.MergePatchType, patch, metav1.UpdateOptions{}) + if err != nil { + // work around "grpc: the client connection is closing" error + // TODO: fix the grpc error + if err, ok := err.(*errors.StatusError); ok && err.Status().Code == http.StatusInternalServerError { + return false, nil + } + return false, err + } + return true, nil + }) + assert.NoError(t, err) + } + + // patches via handler version matching storage version should fail (validation does not allow that API version) + { + t.Logf("patch of handler version v1beta2 (storage version) should fail") + i := 0 + noxuNamespacedResourceClientV1beta2 := newNamespacedCustomResourceVersionedClient(ns, dynamicClient, noxuDefinition, "v1beta2") // use the storage version v1beta2 + err = wait.PollImmediate(time.Millisecond*100, wait.ForeverTestTimeout, func() (bool, error) { + patch := []byte(fmt.Sprintf(`{"i": %d}`, i)) + i++ + + _, err := noxuNamespacedResourceClientV1beta2.Patch("foo", types.MergePatchType, patch, metav1.UpdateOptions{}) + assert.NotNil(t, err) + + // work around "grpc: the client connection is closing" error + // TODO: fix the grpc error + if err, ok := err.(*errors.StatusError); ok && err.Status().Code == http.StatusInternalServerError { + return false, nil + } + + assert.Contains(t, err.Error(), "apiVersion") + return true, nil + }) + assert.NoError(t, err) + } +} + func TestVersionedNamspacedScopedCRD(t *testing.T) { tearDown, apiExtensionClient, dynamicClient, err := fixtures.StartDefaultServerWithClients(t) if err != nil { From 3332a0c972849d2fa76d354d4bb46a7b196368e4 Mon Sep 17 00:00:00 2001 From: Jordan Liggitt Date: Mon, 22 Oct 2018 14:20:22 -0400 Subject: [PATCH 2/6] Test custom resource scaling with multiple versions --- .../test/integration/subresources_test.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/staging/src/k8s.io/apiextensions-apiserver/test/integration/subresources_test.go b/staging/src/k8s.io/apiextensions-apiserver/test/integration/subresources_test.go index c8c8728ee52..3c0f8976456 100644 --- a/staging/src/k8s.io/apiextensions-apiserver/test/integration/subresources_test.go +++ b/staging/src/k8s.io/apiextensions-apiserver/test/integration/subresources_test.go @@ -51,6 +51,10 @@ func NewNoxuSubresourcesCRD(scope apiextensionsv1beta1.ResourceScope) *apiextens ShortNames: []string{"foo", "bar", "abc", "def"}, ListKind: "NoxuItemList", }, + Versions: []apiextensionsv1beta1.CustomResourceDefinitionVersion{ + {Name: "v1beta1", Served: true, Storage: false}, + {Name: "v1", Served: true, Storage: true}, + }, Scope: scope, Subresources: &apiextensionsv1beta1.CustomResourceSubresources{ Status: &apiextensionsv1beta1.CustomResourceSubresourceStatus{}, From 870d121d5e8033a72c62ef3a64939f0eacab6798 Mon Sep 17 00:00:00 2001 From: Jordan Liggitt Date: Mon, 22 Oct 2018 10:01:27 -0400 Subject: [PATCH 3/6] Inline patch#toUnversioned --- .../apiserver/pkg/endpoints/handlers/patch.go | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/patch.go b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/patch.go index d73c3fd573e..4163b18267a 100644 --- a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/patch.go +++ b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/patch.go @@ -242,11 +242,6 @@ type patcher struct { mechanism patchMechanism } -func (p *patcher) toUnversioned(versionedObj runtime.Object) (runtime.Object, error) { - gvk := p.kind.GroupKind().WithVersion(runtime.APIVersionInternal) - return p.unsafeConvertor.ConvertToVersion(versionedObj, gvk.GroupVersion()) -} - type patchMechanism interface { applyPatchToCurrentObject(currentObject runtime.Object) (runtime.Object, error) } @@ -321,12 +316,8 @@ func (p *smpPatcher) applyPatchToCurrentObject(currentObject runtime.Object) (ru return nil, err } // Convert the object back to unversioned (aka internal version). - unversionedObjToUpdate, err := p.toUnversioned(versionedObjToUpdate) - if err != nil { - return nil, err - } - - return unversionedObjToUpdate, nil + gvk := p.kind.GroupKind().WithVersion(runtime.APIVersionInternal) + return p.unsafeConvertor.ConvertToVersion(versionedObjToUpdate, gvk.GroupVersion()) } // strategicPatchObject applies a strategic merge patch of to From 0e9b06df0f21b421ff69fd455d4542883d61e8c3 Mon Sep 17 00:00:00 2001 From: Jordan Liggitt Date: Mon, 22 Oct 2018 10:14:52 -0400 Subject: [PATCH 4/6] Allow specifying the hub group-version for a handler --- .../pkg/apiserver/customresource_handler.go | 3 +++ .../apiserver/pkg/endpoints/handlers/create.go | 2 +- .../k8s.io/apiserver/pkg/endpoints/handlers/patch.go | 12 ++++++++---- .../k8s.io/apiserver/pkg/endpoints/handlers/rest.go | 3 +++ .../apiserver/pkg/endpoints/handlers/rest_test.go | 3 +++ .../apiserver/pkg/endpoints/handlers/update.go | 3 ++- .../src/k8s.io/apiserver/pkg/endpoints/installer.go | 2 ++ 7 files changed, 22 insertions(+), 6 deletions(-) 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 697c5e591a1..725fbfbc302 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 @@ -525,6 +525,9 @@ func (r *crdHandler) getOrCreateServingInfoFor(crd *apiextensions.CustomResource Resource: schema.GroupVersionResource{Group: crd.Spec.Group, Version: v.Name, Resource: crd.Status.AcceptedNames.Plural}, Kind: kind, + // a handler for a specific group-version of a custom resource uses that version as the in-memory representation + HubGroupVersion: kind.GroupVersion(), + MetaGroupVersion: metav1.SchemeGroupVersion, TableConvertor: storages[v.Name].CustomResource, diff --git a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/create.go b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/create.go index e40e4288ac5..62a80ad4945 100644 --- a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/create.go +++ b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/create.go @@ -77,7 +77,7 @@ func createHandler(r rest.NamedCreater, scope RequestScope, admit admission.Inte scope.err(err, w, req) return } - decoder := scope.Serializer.DecoderToVersion(s.Serializer, schema.GroupVersion{Group: gv.Group, Version: runtime.APIVersionInternal}) + decoder := scope.Serializer.DecoderToVersion(s.Serializer, scope.HubGroupVersion) body, err := readBody(req) if err != nil { diff --git a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/patch.go b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/patch.go index 4163b18267a..5c7ecb74e55 100644 --- a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/patch.go +++ b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/patch.go @@ -118,9 +118,10 @@ func PatchResource(r rest.Patcher, scope RequestScope, admit admission.Interface return } gv := scope.Kind.GroupVersion() + codec := runtime.NewCodec( scope.Serializer.EncoderForVersion(s.Serializer, gv), - scope.Serializer.DecoderToVersion(s.Serializer, schema.GroupVersion{Group: gv.Group, Version: runtime.APIVersionInternal}), + scope.Serializer.DecoderToVersion(s.Serializer, scope.HubGroupVersion), ) userInfo, _ := request.UserFrom(ctx) @@ -163,6 +164,8 @@ func PatchResource(r rest.Patcher, scope RequestScope, admit admission.Interface kind: scope.Kind, resource: scope.Resource, + hubGroupVersion: scope.HubGroupVersion, + createValidation: rest.AdmissionToValidateObjectFunc(admit, staticAdmissionAttributes), updateValidation: rest.AdmissionToValidateObjectUpdateFunc(admit, staticAdmissionAttributes), admissionCheck: admissionCheck, @@ -218,6 +221,8 @@ type patcher struct { resource schema.GroupVersionResource kind schema.GroupVersionKind + hubGroupVersion schema.GroupVersion + // Validation functions createValidation rest.ValidateObjectFunc updateValidation rest.ValidateObjectUpdateFunc @@ -315,9 +320,8 @@ func (p *smpPatcher) applyPatchToCurrentObject(currentObject runtime.Object) (ru if err := strategicPatchObject(p.defaulter, currentVersionedObject, p.patchJS, versionedObjToUpdate, p.schemaReferenceObj); err != nil { return nil, err } - // Convert the object back to unversioned (aka internal version). - gvk := p.kind.GroupKind().WithVersion(runtime.APIVersionInternal) - return p.unsafeConvertor.ConvertToVersion(versionedObjToUpdate, gvk.GroupVersion()) + // Convert the object back to the hub version + return p.unsafeConvertor.ConvertToVersion(versionedObjToUpdate, p.hubGroupVersion) } // strategicPatchObject applies a strategic merge patch of to diff --git a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/rest.go b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/rest.go index 8b3ca9d62bc..daa7c76cc85 100644 --- a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/rest.go +++ b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/rest.go @@ -65,6 +65,9 @@ type RequestScope struct { Subresource string MetaGroupVersion schema.GroupVersion + + // HubGroupVersion indicates what version objects read from etcd or incoming requests should be converted to for in-memory handling. + HubGroupVersion schema.GroupVersion } func (scope *RequestScope) err(err error, w http.ResponseWriter, req *http.Request) { diff --git a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/rest_test.go b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/rest_test.go index b190f8d299b..66d9e9c2cf8 100644 --- a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/rest_test.go +++ b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/rest_test.go @@ -367,6 +367,7 @@ func (tc *patchTestCase) Run(t *testing.T) { kind := examplev1.SchemeGroupVersion.WithKind("Pod") resource := examplev1.SchemeGroupVersion.WithResource("pods") schemaReferenceObj := &examplev1.Pod{} + hubVersion := example.SchemeGroupVersion for _, patchType := range []types.PatchType{types.JSONPatchType, types.MergePatchType, types.StrategicMergePatchType} { // This needs to be reset on each iteration. @@ -439,6 +440,8 @@ func (tc *patchTestCase) Run(t *testing.T) { kind: kind, resource: resource, + hubGroupVersion: hubVersion, + createValidation: rest.ValidateAllObjectFunc, updateValidation: admissionValidation, admissionCheck: admissionMutation, diff --git a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/update.go b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/update.go index 19d23e1f2eb..3c0139d3d9d 100644 --- a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/update.go +++ b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/update.go @@ -89,8 +89,9 @@ func UpdateResource(r rest.Updater, scope RequestScope, admit admission.Interfac } defaultGVK := scope.Kind original := r.New() + trace.Step("About to convert to expected version") - decoder := scope.Serializer.DecoderToVersion(s.Serializer, schema.GroupVersion{Group: defaultGVK.Group, Version: runtime.APIVersionInternal}) + decoder := scope.Serializer.DecoderToVersion(s.Serializer, scope.HubGroupVersion) obj, gvk, err := decoder.Decode(body, &defaultGVK, original) if err != nil { err = transformDecodeError(scope.Typer, err, original, gvk, body) diff --git a/staging/src/k8s.io/apiserver/pkg/endpoints/installer.go b/staging/src/k8s.io/apiserver/pkg/endpoints/installer.go index 19bb5e55adf..bbf905d0d8e 100644 --- a/staging/src/k8s.io/apiserver/pkg/endpoints/installer.go +++ b/staging/src/k8s.io/apiserver/pkg/endpoints/installer.go @@ -506,6 +506,8 @@ func (a *APIInstaller) registerResourceHandlers(path string, storage rest.Storag Subresource: subresource, Kind: fqKindToRegister, + HubGroupVersion: schema.GroupVersion{Group: fqKindToRegister.Group, Version: runtime.APIVersionInternal}, + MetaGroupVersion: metav1.SchemeGroupVersion, } if a.group.MetaGroupVersion != nil { From b51ac8f7d581ad368488e275af525c273e1d5603 Mon Sep 17 00:00:00 2001 From: Jordan Liggitt Date: Mon, 22 Oct 2018 14:07:57 -0400 Subject: [PATCH 5/6] Instantiate unstructured objects with versions in custom resource handler --- .../pkg/apiserver/customresource_handler.go | 1 + .../pkg/registry/customresource/etcd.go | 13 +++++++++---- .../pkg/registry/customresource/etcd_test.go | 1 + 3 files changed, 11 insertions(+), 4 deletions(-) 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 725fbfbc302..e8d175ad99d 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 @@ -477,6 +477,7 @@ func (r *crdHandler) getOrCreateServingInfoFor(crd *apiextensions.CustomResource storages[v.Name] = customresource.NewStorage( schema.GroupResource{Group: crd.Spec.Group, Resource: crd.Status.AcceptedNames.Plural}, + schema.GroupVersionKind{Group: crd.Spec.Group, Version: v.Name, Kind: crd.Status.AcceptedNames.Kind}, schema.GroupVersionKind{Group: crd.Spec.Group, Version: v.Name, Kind: crd.Status.AcceptedNames.ListKind}, customresource.NewStrategy( typer, diff --git a/staging/src/k8s.io/apiextensions-apiserver/pkg/registry/customresource/etcd.go b/staging/src/k8s.io/apiextensions-apiserver/pkg/registry/customresource/etcd.go index 0a94bc5c494..cb7c8805398 100644 --- a/staging/src/k8s.io/apiextensions-apiserver/pkg/registry/customresource/etcd.go +++ b/staging/src/k8s.io/apiextensions-apiserver/pkg/registry/customresource/etcd.go @@ -40,8 +40,8 @@ type CustomResourceStorage struct { Scale *ScaleREST } -func NewStorage(resource schema.GroupResource, listKind schema.GroupVersionKind, strategy customResourceStrategy, optsGetter generic.RESTOptionsGetter, categories []string, tableConvertor rest.TableConvertor) CustomResourceStorage { - customResourceREST, customResourceStatusREST := newREST(resource, listKind, strategy, optsGetter, categories, tableConvertor) +func NewStorage(resource schema.GroupResource, kind, listKind schema.GroupVersionKind, strategy customResourceStrategy, optsGetter generic.RESTOptionsGetter, categories []string, tableConvertor rest.TableConvertor) CustomResourceStorage { + customResourceREST, customResourceStatusREST := newREST(resource, kind, listKind, strategy, optsGetter, categories, tableConvertor) s := CustomResourceStorage{ CustomResource: customResourceREST, @@ -75,9 +75,14 @@ type REST struct { } // newREST returns a RESTStorage object that will work against API services. -func newREST(resource schema.GroupResource, listKind schema.GroupVersionKind, strategy customResourceStrategy, optsGetter generic.RESTOptionsGetter, categories []string, tableConvertor rest.TableConvertor) (*REST, *StatusREST) { +func newREST(resource schema.GroupResource, kind, listKind schema.GroupVersionKind, strategy customResourceStrategy, optsGetter generic.RESTOptionsGetter, categories []string, tableConvertor rest.TableConvertor) (*REST, *StatusREST) { store := &genericregistry.Store{ - NewFunc: func() runtime.Object { return &unstructured.Unstructured{} }, + NewFunc: func() runtime.Object { + // set the expected group/version/kind in the new object as a signal to the versioning decoder + ret := &unstructured.Unstructured{} + ret.SetGroupVersionKind(kind) + return ret + }, NewListFunc: func() runtime.Object { // lists are never stored, only manufactured, so stomp in the right kind ret := &unstructured.UnstructuredList{} diff --git a/staging/src/k8s.io/apiextensions-apiserver/pkg/registry/customresource/etcd_test.go b/staging/src/k8s.io/apiextensions-apiserver/pkg/registry/customresource/etcd_test.go index 0a6870dc05d..75339730e23 100644 --- a/staging/src/k8s.io/apiextensions-apiserver/pkg/registry/customresource/etcd_test.go +++ b/staging/src/k8s.io/apiextensions-apiserver/pkg/registry/customresource/etcd_test.go @@ -91,6 +91,7 @@ func newStorage(t *testing.T) (customresource.CustomResourceStorage, *etcdtestin storage := customresource.NewStorage( schema.GroupResource{Group: "mygroup.example.com", Resource: "noxus"}, + kind, schema.GroupVersionKind{Group: "mygroup.example.com", Version: "v1beta1", Kind: "NoxuItemList"}, customresource.NewStrategy( typer, From 1c5d3ab85e67f815e46006cc6a343983566837a3 Mon Sep 17 00:00:00 2001 From: Jordan Liggitt Date: Mon, 22 Oct 2018 16:04:06 -0400 Subject: [PATCH 6/6] Avoid short-circuiting conversion when decoding into opinionated unstructured objects --- .../pkg/runtime/serializer/versioning/versioning.go | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/staging/src/k8s.io/apimachinery/pkg/runtime/serializer/versioning/versioning.go b/staging/src/k8s.io/apimachinery/pkg/runtime/serializer/versioning/versioning.go index a5ae3ac4bb7..00184710760 100644 --- a/staging/src/k8s.io/apimachinery/pkg/runtime/serializer/versioning/versioning.go +++ b/staging/src/k8s.io/apimachinery/pkg/runtime/serializer/versioning/versioning.go @@ -18,6 +18,7 @@ package versioning import ( "io" + "reflect" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" @@ -90,7 +91,16 @@ func (c *codec) Decode(data []byte, defaultGVK *schema.GroupVersionKind, into ru into = versioned.Last() } - obj, gvk, err := c.decoder.Decode(data, defaultGVK, into) + // If the into object is unstructured and expresses an opinion about its group/version, + // create a new instance of the type so we always exercise the conversion path (skips short-circuiting on `into == obj`) + decodeInto := into + if into != nil { + if _, ok := into.(runtime.Unstructured); ok && !into.GetObjectKind().GroupVersionKind().GroupVersion().Empty() { + decodeInto = reflect.New(reflect.TypeOf(into).Elem()).Interface().(runtime.Object) + } + } + + obj, gvk, err := c.decoder.Decode(data, defaultGVK, decodeInto) if err != nil { return nil, gvk, err }