diff --git a/staging/src/k8s.io/apimachinery/pkg/runtime/serializer/versioning/BUILD b/staging/src/k8s.io/apimachinery/pkg/runtime/serializer/versioning/BUILD index 785dc5fcfe..9ba9d16f5a 100644 --- a/staging/src/k8s.io/apimachinery/pkg/runtime/serializer/versioning/BUILD +++ b/staging/src/k8s.io/apimachinery/pkg/runtime/serializer/versioning/BUILD @@ -8,12 +8,17 @@ load( go_test( name = "go_default_test", - srcs = ["versioning_test.go"], + srcs = [ + "versioning_test.go", + "versioning_unstructured_test.go", + ], embed = [":go_default_library"], deps = [ + "//staging/src/k8s.io/apimachinery/pkg/apis/meta/v1/unstructured:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/runtime:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/runtime/schema:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/util/diff:go_default_library", + "//vendor/github.com/stretchr/testify/assert:go_default_library", ], ) diff --git a/staging/src/k8s.io/apimachinery/pkg/runtime/serializer/versioning/versioning_unstructured_test.go b/staging/src/k8s.io/apimachinery/pkg/runtime/serializer/versioning/versioning_unstructured_test.go new file mode 100644 index 0000000000..e47a259f69 --- /dev/null +++ b/staging/src/k8s.io/apimachinery/pkg/runtime/serializer/versioning/versioning_unstructured_test.go @@ -0,0 +1,338 @@ +/* +Copyright 2015 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 versioning + +import ( + "fmt" + "io/ioutil" + "testing" + + "github.com/stretchr/testify/assert" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" +) + +func buildUnstructuredDecodable(gvk schema.GroupVersionKind) runtime.Object { + obj := &unstructured.Unstructured{} + obj.SetGroupVersionKind(gvk) + return obj +} + +func buildUnstructuredListDecodable(gvk schema.GroupVersionKind) runtime.Object { + obj := &unstructured.UnstructuredList{} + obj.SetGroupVersionKind(gvk) + return obj +} + +func TestEncodeUnstructured(t *testing.T) { + v1GVK := schema.GroupVersionKind{ + Group: "crispy", + Version: "v1", + Kind: "Noxu", + } + v2GVK := schema.GroupVersionKind{ + Group: "crispy", + Version: "v2", + Kind: "Noxu", + } + elseGVK := schema.GroupVersionKind{ + Group: "crispy2", + Version: "else", + Kind: "Noxu", + } + elseUnstructuredDecodable := buildUnstructuredDecodable(elseGVK) + elseUnstructuredDecodableList := buildUnstructuredListDecodable(elseGVK) + v1UnstructuredDecodable := buildUnstructuredDecodable(v1GVK) + v1UnstructuredDecodableList := buildUnstructuredListDecodable(v1GVK) + v2UnstructuredDecodable := buildUnstructuredDecodable(v2GVK) + + testCases := []struct { + name string + convertor runtime.ObjectConvertor + targetVersion runtime.GroupVersioner + outObj runtime.Object + typer runtime.ObjectTyper + + errFunc func(error) bool + expectedObj runtime.Object + }{ + { + name: "encode v1 unstructured with v2 encode version", + typer: &mockTyper{ + gvks: []schema.GroupVersionKind{v1GVK}, + }, + outObj: v1UnstructuredDecodable, + targetVersion: v2GVK.GroupVersion(), + convertor: &checkConvertor{ + obj: v2UnstructuredDecodable, + groupVersion: v2GVK.GroupVersion(), + }, + expectedObj: v2UnstructuredDecodable, + }, + { + name: "both typer and conversion are bypassed when unstructured gvk matches encode gvk", + typer: &mockTyper{ + err: fmt.Errorf("unexpected typer call"), + }, + outObj: v1UnstructuredDecodable, + targetVersion: v1GVK.GroupVersion(), + convertor: &checkConvertor{ + err: fmt.Errorf("unexpected conversion happened"), + }, + expectedObj: v1UnstructuredDecodable, + }, + { + name: "encode will fail when unstructured object's gvk and encode gvk mismatches", + outObj: elseUnstructuredDecodable, + targetVersion: v1GVK.GroupVersion(), + errFunc: func(err error) bool { + return assert.Equal(t, runtime.NewNotRegisteredGVKErrForTarget("noxu-scheme", elseGVK, v1GVK.GroupVersion()), err) + }, + }, + { + name: "encode with unstructured list's gvk regardless of its elements' gvk", + outObj: elseUnstructuredDecodableList, + targetVersion: elseGVK.GroupVersion(), + }, + { + name: "typer fail to recognize unstructured object gvk will fail the encoding", + outObj: elseUnstructuredDecodable, + targetVersion: v1GVK.GroupVersion(), + typer: &mockTyper{ + err: fmt.Errorf("invalid obj gvk"), + }, + }, + { + name: "encoding unstructured object without encode version will fallback to typer suggested version", + targetVersion: v1GVK.GroupVersion(), + convertor: &checkConvertor{ + obj: v1UnstructuredDecodableList, + groupVersion: v1GVK.GroupVersion(), + }, + outObj: elseUnstructuredDecodable, + typer: &mockTyper{ + gvks: []schema.GroupVersionKind{v1GVK}, + }, + }, + } + for _, testCase := range testCases { + serializer := &mockSerializer{} + codec := NewCodec(serializer, serializer, testCase.convertor, nil, testCase.typer, nil, testCase.targetVersion, nil, "noxu-scheme") + err := codec.Encode(testCase.outObj, ioutil.Discard) + if testCase.errFunc != nil { + if !testCase.errFunc(err) { + t.Errorf("%v: failed: %v", testCase.name, err) + } + return + } + assert.NoError(t, err) + assert.Equal(t, testCase.expectedObj, serializer.obj) + } +} + +type errNotRecognizedGVK struct { + failedGVK schema.GroupVersionKind + claimingGVKs []schema.GroupVersionKind +} + +func (e errNotRecognizedGVK) Error() string { + return fmt.Sprintf("unrecognized gvk %v, should be one of %v", e.failedGVK, e.claimingGVKs) +} + +type mockUnstructuredNopConvertor struct { + claimingGVKs []schema.GroupVersionKind +} + +func (c *mockUnstructuredNopConvertor) recognizeGVK(gvkToCheck schema.GroupVersionKind) error { + matched := false + for _, gvk := range c.claimingGVKs { + if gvk == gvkToCheck { + matched = true + } + } + if !matched { + return errNotRecognizedGVK{ + failedGVK: gvkToCheck, + claimingGVKs: c.claimingGVKs, + } + } + return nil +} + +func (c *mockUnstructuredNopConvertor) Convert(in, out, context interface{}) error { + inObj := in.(*unstructured.Unstructured) + outObj := out.(*unstructured.Unstructured) + if err := c.recognizeGVK(outObj.GroupVersionKind()); err != nil { + return err + } + outGVK := outObj.GetObjectKind().GroupVersionKind() + *outObj = *inObj.DeepCopy() + outObj.GetObjectKind().SetGroupVersionKind(outGVK) + return nil +} + +func (c *mockUnstructuredNopConvertor) ConvertToVersion(in runtime.Object, outVersion runtime.GroupVersioner) (runtime.Object, error) { + out := in.DeepCopyObject() + targetGVK, matched := outVersion.KindForGroupVersionKinds([]schema.GroupVersionKind{in.GetObjectKind().GroupVersionKind()}) + if !matched { + return nil, fmt.Errorf("attempt to convert to mismatched gv %v", outVersion) + } + if err := c.recognizeGVK(out.GetObjectKind().GroupVersionKind()); err != nil { + return nil, err + } + out.GetObjectKind().SetGroupVersionKind(targetGVK) + return out, nil +} + +func (c *mockUnstructuredNopConvertor) ConvertFieldLabel(gvk schema.GroupVersionKind, label, value string) (string, string, error) { + return "", "", fmt.Errorf("unexpected call to ConvertFieldLabel") +} + +func TestDecodeUnstructured(t *testing.T) { + internalGVK := schema.GroupVersionKind{ + Group: "crispy", + Version: runtime.APIVersionInternal, + Kind: "Noxu", + } + v1GVK := schema.GroupVersionKind{ + Group: "crispy", + Version: "v1", + Kind: "Noxu", + } + v2GVK := schema.GroupVersionKind{ + Group: "crispy", + Version: "v2", + Kind: "Noxu", + } + internalUnstructuredDecodable := buildUnstructuredDecodable(internalGVK) + v1UnstructuredDecodable := buildUnstructuredDecodable(v1GVK) + v2UnstructuredDecodable := buildUnstructuredDecodable(v2GVK) + + testCases := []struct { + name string + serializer runtime.Serializer + convertor runtime.ObjectConvertor + suggestedConvertVersion runtime.GroupVersioner + defaultGVK *schema.GroupVersionKind + intoObj runtime.Object + + errFunc func(error) bool + expectedGVKOfSerializedData *schema.GroupVersionKind + expectedOut runtime.Object + }{ + { + name: "decode v1 unstructured into non-nil v2 unstructured", + serializer: &mockSerializer{actual: &v1GVK, obj: v1UnstructuredDecodable}, + convertor: &mockUnstructuredNopConvertor{ + claimingGVKs: []schema.GroupVersionKind{ + v1GVK, v2GVK, + }, + }, + suggestedConvertVersion: v2GVK.GroupVersion(), + intoObj: v2UnstructuredDecodable, + expectedGVKOfSerializedData: &v1GVK, + expectedOut: v2UnstructuredDecodable, + }, + { + name: "decode v1 unstructured into nil object with v2 version", + serializer: &mockSerializer{actual: &v1GVK, obj: v1UnstructuredDecodable}, + convertor: &mockUnstructuredNopConvertor{ + claimingGVKs: []schema.GroupVersionKind{ + v1GVK, v2GVK, + }, + }, + suggestedConvertVersion: v2GVK.GroupVersion(), + intoObj: nil, + expectedGVKOfSerializedData: &v1GVK, + expectedOut: v2UnstructuredDecodable, + }, + { + name: "decode v1 unstructured into non-nil internal unstructured", + serializer: &mockSerializer{actual: &v1GVK, obj: v1UnstructuredDecodable}, + convertor: &mockUnstructuredNopConvertor{ + claimingGVKs: []schema.GroupVersionKind{ + v1GVK, v2GVK, + }, + }, + suggestedConvertVersion: internalGVK.GroupVersion(), + intoObj: internalUnstructuredDecodable, + errFunc: func(err error) bool { + notRecognized, ok := err.(errNotRecognizedGVK) + if !ok { + return false + } + return assert.Equal(t, notRecognized.failedGVK, internalGVK) + }, + }, + { + name: "decode v1 unstructured into nil object with internal version", + serializer: &mockSerializer{actual: &v1GVK, obj: v1UnstructuredDecodable}, + convertor: &mockUnstructuredNopConvertor{ + claimingGVKs: []schema.GroupVersionKind{ + v1GVK, v2GVK, + }, + }, + suggestedConvertVersion: internalGVK.GroupVersion(), + intoObj: nil, + errFunc: func(err error) bool { + notRecognized, ok := err.(errNotRecognizedGVK) + if !ok { + return false + } + return assert.Equal(t, notRecognized.failedGVK, internalGVK) + }, + }, + { + name: "skip conversion if serializer returns the same unstructured as into", + serializer: &mockSerializer{actual: &v1GVK, obj: v1UnstructuredDecodable}, + convertor: &checkConvertor{ + err: fmt.Errorf("unexpected conversion happened"), + }, + suggestedConvertVersion: internalGVK.GroupVersion(), + intoObj: v1UnstructuredDecodable, + expectedGVKOfSerializedData: &v1GVK, + expectedOut: v1UnstructuredDecodable, + }, + { + name: "invalid convert version makes decoding unstructured fail", + serializer: &mockSerializer{actual: &v1GVK, obj: v1UnstructuredDecodable}, + convertor: &checkConvertor{ + in: v1UnstructuredDecodable, + groupVersion: internalGVK.GroupVersion(), + err: fmt.Errorf("no matching decode version"), + }, + suggestedConvertVersion: internalGVK.GroupVersion(), + errFunc: func(err error) bool { + return assert.Equal(t, err, fmt.Errorf("no matching decode version")) + }, + }, + } + for _, testCase := range testCases { + codec := NewCodec(testCase.serializer, testCase.serializer, testCase.convertor, nil, nil, nil, nil, testCase.suggestedConvertVersion, "noxu-scheme") + actualObj, actualSerializedGVK, err := codec.Decode([]byte(`{}`), testCase.defaultGVK, testCase.intoObj) + if testCase.errFunc != nil { + if !testCase.errFunc(err) { + t.Errorf("%v: failed: %v", testCase.name, err) + } + return + } + assert.NoError(t, err) + assert.Equal(t, testCase.expectedOut, actualObj, "%v failed", testCase.name) + assert.Equal(t, testCase.expectedGVKOfSerializedData, actualSerializedGVK, "%v failed", testCase.name) + } +}