From 4755e1f85979f4db11114261797e6da8b116dc10 Mon Sep 17 00:00:00 2001 From: Ben Luddy Date: Fri, 14 Jun 2024 12:59:48 -0400 Subject: [PATCH] Automatically transcode RawExtension between unstructured protocols. --- pkg/api/testing/unstructured_test.go | 20 +- .../apimachinery/pkg/runtime/extension.go | 100 +++++++- .../pkg/runtime/extension_test.go | 151 ++++++++++++ .../pkg/runtime/serializer/cbor/cbor.go | 6 + .../pkg/runtime/serializer/cbor/cbor_test.go | 27 +++ .../pkg/runtime/serializer/cbor/raw.go | 218 ++++++++++++++++++ .../pkg/runtime/serializer/cbor/raw_test.go | 126 ++++++++++ .../k8s.io/apimachinery/pkg/runtime/types.go | 1 + 8 files changed, 625 insertions(+), 24 deletions(-) create mode 100644 staging/src/k8s.io/apimachinery/pkg/runtime/serializer/cbor/raw.go create mode 100644 staging/src/k8s.io/apimachinery/pkg/runtime/serializer/cbor/raw_test.go diff --git a/pkg/api/testing/unstructured_test.go b/pkg/api/testing/unstructured_test.go index 26c0d71106f..9995d9c3b1c 100644 --- a/pkg/api/testing/unstructured_test.go +++ b/pkg/api/testing/unstructured_test.go @@ -137,25 +137,7 @@ func TestRoundTrip(t *testing.T) { } func TestRoundtripToUnstructured(t *testing.T) { - skipped := sets.New( - // TODO: Support cross-protocol RawExtension roundtrips. - schema.GroupVersionKind{Version: "v1", Kind: "List"}, - schema.GroupVersionKind{Group: "apps", Version: "v1beta1", Kind: "ControllerRevision"}, - schema.GroupVersionKind{Group: "apps", Version: "v1beta1", Kind: "ControllerRevisionList"}, - schema.GroupVersionKind{Group: "apps", Version: "v1beta2", Kind: "ControllerRevision"}, - schema.GroupVersionKind{Group: "apps", Version: "v1beta2", Kind: "ControllerRevisionList"}, - schema.GroupVersionKind{Group: "apps", Version: "v1", Kind: "ControllerRevision"}, - schema.GroupVersionKind{Group: "apps", Version: "v1", Kind: "ControllerRevisionList"}, - schema.GroupVersionKind{Group: "admission.k8s.io", Version: "v1beta1", Kind: "AdmissionReview"}, - schema.GroupVersionKind{Group: "admission.k8s.io", Version: "v1", Kind: "AdmissionReview"}, - schema.GroupVersionKind{Group: "resource.k8s.io", Version: "v1alpha2", Kind: "ResourceClaim"}, - schema.GroupVersionKind{Group: "resource.k8s.io", Version: "v1alpha2", Kind: "ResourceClaimList"}, - schema.GroupVersionKind{Group: "resource.k8s.io", Version: "v1alpha2", Kind: "ResourceClaimParameters"}, - schema.GroupVersionKind{Group: "resource.k8s.io", Version: "v1alpha2", Kind: "ResourceClaimParametersList"}, - schema.GroupVersionKind{Group: "resource.k8s.io", Version: "v1alpha2", Kind: "ResourceClassParameters"}, - schema.GroupVersionKind{Group: "resource.k8s.io", Version: "v1alpha2", Kind: "ResourceClassParametersList"}, - ) - + skipped := sets.New[schema.GroupVersionKind]() for gvk := range legacyscheme.Scheme.AllKnownTypes() { if nonRoundTrippableTypes.Has(gvk.Kind) { skipped.Insert(gvk) diff --git a/staging/src/k8s.io/apimachinery/pkg/runtime/extension.go b/staging/src/k8s.io/apimachinery/pkg/runtime/extension.go index 9056397fa51..60c000bcb71 100644 --- a/staging/src/k8s.io/apimachinery/pkg/runtime/extension.go +++ b/staging/src/k8s.io/apimachinery/pkg/runtime/extension.go @@ -18,16 +18,77 @@ package runtime import ( "bytes" - "encoding/json" "errors" + "fmt" + + cbor "k8s.io/apimachinery/pkg/runtime/serializer/cbor/direct" + "k8s.io/apimachinery/pkg/util/json" ) +// RawExtension intentionally avoids implementing value.UnstructuredConverter for now because the +// signature of ToUnstructured does not allow returning an error value in cases where the conversion +// is not possible (content type is unrecognized or bytes don't match content type). +func rawToUnstructured(raw []byte, contentType string) (interface{}, error) { + switch contentType { + case ContentTypeJSON: + var u interface{} + if err := json.Unmarshal(raw, &u); err != nil { + return nil, fmt.Errorf("failed to parse RawExtension bytes as JSON: %w", err) + } + return u, nil + case ContentTypeCBOR: + var u interface{} + if err := cbor.Unmarshal(raw, &u); err != nil { + return nil, fmt.Errorf("failed to parse RawExtension bytes as CBOR: %w", err) + } + return u, nil + default: + return nil, fmt.Errorf("cannot convert RawExtension with unrecognized content type to unstructured") + } +} + +func (re RawExtension) guessContentType() string { + switch { + case bytes.HasPrefix(re.Raw, cborSelfDescribed): + return ContentTypeCBOR + case len(re.Raw) > 0: + switch re.Raw[0] { + case '\t', '\r', '\n', ' ', '{', '[', 'n', 't', 'f', '"', '-', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9': + // Prefixes for the four whitespace characters, objects, arrays, strings, numbers, true, false, and null. + return ContentTypeJSON + } + } + return "" +} + func (re *RawExtension) UnmarshalJSON(in []byte) error { if re == nil { return errors.New("runtime.RawExtension: UnmarshalJSON on nil pointer") } - if !bytes.Equal(in, []byte("null")) { - re.Raw = append(re.Raw[0:0], in...) + if bytes.Equal(in, []byte("null")) { + return nil + } + re.Raw = append(re.Raw[0:0], in...) + return nil +} + +var ( + cborNull = []byte{0xf6} + cborSelfDescribed = []byte{0xd9, 0xd9, 0xf7} +) + +func (re *RawExtension) UnmarshalCBOR(in []byte) error { + if re == nil { + return errors.New("runtime.RawExtension: UnmarshalCBOR on nil pointer") + } + if !bytes.Equal(in, cborNull) { + if !bytes.HasPrefix(in, cborSelfDescribed) { + // The self-described CBOR tag doesn't change the interpretation of the data + // item it encloses, but it is useful as a magic number. Its encoding is + // also what is used to implement the CBOR RecognizingDecoder. + re.Raw = append(re.Raw[:0], cborSelfDescribed...) + } + re.Raw = append(re.Raw, in...) } return nil } @@ -46,6 +107,35 @@ func (re RawExtension) MarshalJSON() ([]byte, error) { } return []byte("null"), nil } - // TODO: Check whether ContentType is actually JSON before returning it. - return re.Raw, nil + + contentType := re.guessContentType() + if contentType == ContentTypeJSON { + return re.Raw, nil + } + + u, err := rawToUnstructured(re.Raw, contentType) + if err != nil { + return nil, err + } + return json.Marshal(u) +} + +func (re RawExtension) MarshalCBOR() ([]byte, error) { + if re.Raw == nil { + if re.Object != nil { + return cbor.Marshal(re.Object) + } + return cbor.Marshal(nil) + } + + contentType := re.guessContentType() + if contentType == ContentTypeCBOR { + return re.Raw, nil + } + + u, err := rawToUnstructured(re.Raw, contentType) + if err != nil { + return nil, err + } + return cbor.Marshal(u) } diff --git a/staging/src/k8s.io/apimachinery/pkg/runtime/extension_test.go b/staging/src/k8s.io/apimachinery/pkg/runtime/extension_test.go index 5f9154ea6b9..3a296987b7b 100644 --- a/staging/src/k8s.io/apimachinery/pkg/runtime/extension_test.go +++ b/staging/src/k8s.io/apimachinery/pkg/runtime/extension_test.go @@ -23,6 +23,9 @@ import ( "testing" "k8s.io/apimachinery/pkg/runtime" + runtimetesting "k8s.io/apimachinery/pkg/runtime/testing" + + "github.com/google/go-cmp/cmp" ) func TestEmbeddedRawExtensionMarshal(t *testing.T) { @@ -111,3 +114,151 @@ func TestEmbeddedRawExtensionRoundTrip(t *testing.T) { } } } + +func TestRawExtensionMarshalUnstructured(t *testing.T) { + for _, tc := range []struct { + Name string + In runtime.RawExtension + WantCBOR []byte + ExpectedErrorCBOR string + WantJSON string + ExpectedErrorJSON string + }{ + { + Name: "nil bytes and nil object", + In: runtime.RawExtension{}, + WantCBOR: []byte{0xf6}, + WantJSON: "null", + }, + { + Name: "nil bytes and non-nil object", + In: runtime.RawExtension{Object: &runtimetesting.ExternalSimple{TestString: "foo"}}, + WantCBOR: []byte("\xa1\x4atestString\x43foo"), + WantJSON: `{"testString":"foo"}`, + }, + { + Name: "cbor bytes not enclosed in self-described tag", + In: runtime.RawExtension{Raw: []byte{0x43, 'f', 'o', 'o'}}, // 'foo' + ExpectedErrorCBOR: "cannot convert RawExtension with unrecognized content type to unstructured", + ExpectedErrorJSON: "cannot convert RawExtension with unrecognized content type to unstructured", + }, + { + Name: "cbor bytes enclosed in self-described tag", + In: runtime.RawExtension{Raw: []byte{0xd9, 0xd9, 0xf7, 0x43, 'f', 'o', 'o'}}, // 55799('foo') + WantCBOR: []byte{0xd9, 0xd9, 0xf7, 0x43, 'f', 'o', 'o'}, // 55799('foo') + WantJSON: `"foo"`, + }, + { + Name: "json bytes", + In: runtime.RawExtension{Raw: []byte(`"foo"`)}, + WantCBOR: []byte{0x43, 'f', 'o', 'o'}, + WantJSON: `"foo"`, + }, + { + Name: "ambiguous bytes not enclosed in self-described cbor tag", + In: runtime.RawExtension{Raw: []byte{'0'}}, // CBOR -17 / JSON 0 + WantCBOR: []byte{0x00}, + WantJSON: `0`, + }, + { + Name: "ambiguous bytes enclosed in self-described cbor tag", + In: runtime.RawExtension{Raw: []byte{0xd9, 0xd9, 0xf7, '0'}}, // 55799(-17) + WantCBOR: []byte{0xd9, 0xd9, 0xf7, '0'}, + WantJSON: `-17`, + }, + { + Name: "unrecognized bytes", + In: runtime.RawExtension{Raw: []byte{0xff}}, + ExpectedErrorCBOR: "cannot convert RawExtension with unrecognized content type to unstructured", + ExpectedErrorJSON: "cannot convert RawExtension with unrecognized content type to unstructured", + }, + { + Name: "invalid cbor with self-described cbor prefix", + In: runtime.RawExtension{Raw: []byte{0xd9, 0xd9, 0xf7, 0xff}}, + WantCBOR: []byte{0xd9, 0xd9, 0xf7, 0xff}, // verbatim + ExpectedErrorJSON: `failed to parse RawExtension bytes as CBOR: cbor: unexpected "break" code`, + }, + { + Name: "invalid json with json prefix", + In: runtime.RawExtension{Raw: []byte(`{{`)}, + ExpectedErrorCBOR: `failed to parse RawExtension bytes as JSON: invalid character '{' looking for beginning of object key string`, + WantJSON: `{{`, // verbatim + }, + } { + t.Run(tc.Name, func(t *testing.T) { + t.Run("CBOR", func(t *testing.T) { + got, err := tc.In.MarshalCBOR() + if err != nil { + if tc.ExpectedErrorCBOR == "" { + t.Fatalf("unexpected error: %v", err) + } + if msg := err.Error(); msg != tc.ExpectedErrorCBOR { + t.Fatalf("expected error %q but got %q", tc.ExpectedErrorCBOR, msg) + } + } + + if diff := cmp.Diff(tc.WantCBOR, got); diff != "" { + t.Errorf("unexpected diff:\n%s", diff) + } + }) + + t.Run("JSON", func(t *testing.T) { + got, err := tc.In.MarshalJSON() + if err != nil { + if tc.ExpectedErrorJSON == "" { + t.Fatalf("unexpected error: %v", err) + } + if msg := err.Error(); msg != tc.ExpectedErrorJSON { + t.Fatalf("expected error %q but got %q", tc.ExpectedErrorJSON, msg) + } + } + + if diff := cmp.Diff(tc.WantJSON, string(got)); diff != "" { + t.Errorf("unexpected diff:\n%s", diff) + } + }) + }) + } +} + +func TestRawExtensionUnmarshalCBOR(t *testing.T) { + for _, tc := range []struct { + Name string + In []byte + Want runtime.RawExtension + }{ + { + // From json.Unmarshaler: By convention, to approximate the behavior of + // Unmarshal itself, Unmarshalers implement UnmarshalJSON([]byte("null")) as + // a no-op. + Name: "no-op on null", + In: []byte{0xf6}, + Want: runtime.RawExtension{}, + }, + { + Name: "input copied verbatim", + In: []byte{0xd9, 0xd9, 0xf7, 0x5f, 0x41, 'f', 0x42, 'o', 'o', 0xff}, // 55799(_ 'f' 'oo') + Want: runtime.RawExtension{ + Raw: []byte{0xd9, 0xd9, 0xf7, 0x5f, 0x41, 'f', 0x42, 'o', 'o', 0xff}, // 55799(_ 'f' 'oo') + }, + }, + { + Name: "input enclosed in self-described tag if absent", + In: []byte{0x5f, 0x41, 'f', 0x42, 'o', 'o', 0xff}, // (_ 'f' 'oo') + Want: runtime.RawExtension{ + Raw: []byte{0xd9, 0xd9, 0xf7, 0x5f, 0x41, 'f', 0x42, 'o', 'o', 0xff}, // 55799(_ 'f' 'oo') + }, + }, + } { + t.Run(tc.Name, func(t *testing.T) { + var got runtime.RawExtension + if err := got.UnmarshalCBOR(tc.In); err != nil { + t.Fatalf("unexpected error: %v", err) + } + + if diff := cmp.Diff(tc.Want, got); diff != "" { + t.Errorf("unexpected diff:\n%s", diff) + } + }) + } +} diff --git a/staging/src/k8s.io/apimachinery/pkg/runtime/serializer/cbor/cbor.go b/staging/src/k8s.io/apimachinery/pkg/runtime/serializer/cbor/cbor.go index e36bca1721d..c664b5e6b9c 100644 --- a/staging/src/k8s.io/apimachinery/pkg/runtime/serializer/cbor/cbor.go +++ b/staging/src/k8s.io/apimachinery/pkg/runtime/serializer/cbor/cbor.go @@ -298,6 +298,12 @@ func (s *serializer) Decode(data []byte, gvk *schema.GroupVersionKind, into runt if err != nil { return nil, actual, err } + + // TODO: Make possible to disable this behavior. + if err := transcodeRawTypes(obj); err != nil { + return nil, actual, err + } + return obj, actual, strict } diff --git a/staging/src/k8s.io/apimachinery/pkg/runtime/serializer/cbor/cbor_test.go b/staging/src/k8s.io/apimachinery/pkg/runtime/serializer/cbor/cbor_test.go index 457af702d3e..b95e7a18aec 100644 --- a/staging/src/k8s.io/apimachinery/pkg/runtime/serializer/cbor/cbor_test.go +++ b/staging/src/k8s.io/apimachinery/pkg/runtime/serializer/cbor/cbor_test.go @@ -121,6 +121,18 @@ func (p *anyObject) UnmarshalCBOR(in []byte) error { return modes.Decode.Unmarshal(in, &p.Value) } +type structWithRawExtensionField struct { + Extension runtime.RawExtension `json:"extension"` +} + +func (p structWithRawExtensionField) GetObjectKind() schema.ObjectKind { + return schema.EmptyObjectKind +} + +func (structWithRawExtensionField) DeepCopyObject() runtime.Object { + panic("unimplemented") +} + func TestEncode(t *testing.T) { for _, tc := range []struct { name string @@ -264,6 +276,21 @@ func TestDecode(t *testing.T) { } }, }, + { + name: "rawextension transcoded", + data: []byte{0xa1, 0x49, 'e', 'x', 't', 'e', 'n', 's', 'i', 'o', 'n', 0xa1, 0x41, 'a', 0x01}, + gvk: &schema.GroupVersionKind{}, + metaFactory: stubMetaFactory{gvk: &schema.GroupVersionKind{}}, + typer: stubTyper{gvks: []schema.GroupVersionKind{{Group: "x", Version: "y", Kind: "z"}}}, + into: &structWithRawExtensionField{}, + expectedObj: &structWithRawExtensionField{Extension: runtime.RawExtension{Raw: []byte(`{"a":1}`)}}, + expectedGVK: &schema.GroupVersionKind{Group: "x", Version: "y", Kind: "z"}, + assertOnError: func(t *testing.T, err error) { + if err != nil { + t.Errorf("expected nil error, got: %v", err) + } + }, + }, { name: "strict mode strict error", options: []Option{Strict(true)}, diff --git a/staging/src/k8s.io/apimachinery/pkg/runtime/serializer/cbor/raw.go b/staging/src/k8s.io/apimachinery/pkg/runtime/serializer/cbor/raw.go new file mode 100644 index 00000000000..bfcc9997210 --- /dev/null +++ b/staging/src/k8s.io/apimachinery/pkg/runtime/serializer/cbor/raw.go @@ -0,0 +1,218 @@ +/* +Copyright 2024 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 cbor + +import ( + "fmt" + "reflect" + "sync" + + "k8s.io/apimachinery/pkg/runtime" +) + +var sharedTranscoders transcoders + +var rawTypeTranscodeFuncs = map[reflect.Type]func(reflect.Value) error{ + reflect.TypeFor[runtime.RawExtension](): func(rv reflect.Value) error { + if !rv.CanAddr() { + return nil + } + re := rv.Addr().Interface().(*runtime.RawExtension) + if re.Raw == nil { + // When Raw is nil it encodes to null. Don't change nil Raw values during + // transcoding, they would have unmarshalled from JSON as nil too. + return nil + } + j, err := re.MarshalJSON() + if err != nil { + return fmt.Errorf("failed to transcode RawExtension to JSON: %w", err) + } + re.Raw = j + return nil + }, +} + +func transcodeRawTypes(v interface{}) error { + if v == nil { + return nil + } + + rv := reflect.ValueOf(v) + return sharedTranscoders.getTranscoder(rv.Type()).fn(rv) +} + +type transcoder struct { + fn func(rv reflect.Value) error +} + +var noop = transcoder{ + fn: func(reflect.Value) error { + return nil + }, +} + +type transcoders struct { + lock sync.RWMutex + m map[reflect.Type]**transcoder +} + +func (ts *transcoders) getTranscoder(rt reflect.Type) transcoder { + ts.lock.RLock() + tpp, ok := ts.m[rt] + ts.lock.RUnlock() + if ok { + return **tpp + } + + ts.lock.Lock() + defer ts.lock.Unlock() + tp := ts.getTranscoderLocked(rt) + return *tp +} + +func (ts *transcoders) getTranscoderLocked(rt reflect.Type) *transcoder { + if tpp, ok := ts.m[rt]; ok { + // A transcoder for this type was cached while waiting to acquire the lock. + return *tpp + } + + // Cache the transcoder now, before populating fn, so that circular references between types + // don't overflow the call stack. + t := new(transcoder) + if ts.m == nil { + ts.m = make(map[reflect.Type]**transcoder) + } + ts.m[rt] = &t + + for rawType, fn := range rawTypeTranscodeFuncs { + if rt == rawType { + t = &transcoder{fn: fn} + return t + } + } + + switch rt.Kind() { + case reflect.Array: + te := ts.getTranscoderLocked(rt.Elem()) + rtlen := rt.Len() + if rtlen == 0 || te == &noop { + t = &noop + break + } + t.fn = func(rv reflect.Value) error { + for i := 0; i < rtlen; i++ { + if err := te.fn(rv.Index(i)); err != nil { + return err + } + } + return nil + } + case reflect.Interface: + // Any interface value might have a dynamic type involving RawExtension. It needs to + // be checked. + t.fn = func(rv reflect.Value) error { + if rv.IsNil() { + return nil + } + rv = rv.Elem() + // The interface element's type is dynamic so its transcoder can't be + // determined statically. + return ts.getTranscoder(rv.Type()).fn(rv) + } + case reflect.Map: + rtk := rt.Key() + tk := ts.getTranscoderLocked(rtk) + rte := rt.Elem() + te := ts.getTranscoderLocked(rte) + if tk == &noop && te == &noop { + t = &noop + break + } + t.fn = func(rv reflect.Value) error { + iter := rv.MapRange() + rvk := reflect.New(rtk).Elem() + rve := reflect.New(rte).Elem() + for iter.Next() { + rvk.SetIterKey(iter) + if err := tk.fn(rvk); err != nil { + return err + } + rve.SetIterValue(iter) + if err := te.fn(rve); err != nil { + return err + } + } + return nil + } + case reflect.Pointer: + te := ts.getTranscoderLocked(rt.Elem()) + if te == &noop { + t = &noop + break + } + t.fn = func(rv reflect.Value) error { + if rv.IsNil() { + return nil + } + return te.fn(rv.Elem()) + } + case reflect.Slice: + te := ts.getTranscoderLocked(rt.Elem()) + if te == &noop { + t = &noop + break + } + t.fn = func(rv reflect.Value) error { + for i := 0; i < rv.Len(); i++ { + if err := te.fn(rv.Index(i)); err != nil { + return err + } + } + return nil + } + case reflect.Struct: + type fieldTranscoder struct { + Index int + Transcoder *transcoder + } + var fieldTranscoders []fieldTranscoder + for i := 0; i < rt.NumField(); i++ { + f := rt.Field(i) + tf := ts.getTranscoderLocked(f.Type) + if tf == &noop { + continue + } + fieldTranscoders = append(fieldTranscoders, fieldTranscoder{Index: i, Transcoder: tf}) + } + if len(fieldTranscoders) == 0 { + t = &noop + break + } + t.fn = func(rv reflect.Value) error { + for _, ft := range fieldTranscoders { + if err := ft.Transcoder.fn(rv.Field(ft.Index)); err != nil { + return err + } + } + return nil + } + default: + t = &noop + } + + return t +} diff --git a/staging/src/k8s.io/apimachinery/pkg/runtime/serializer/cbor/raw_test.go b/staging/src/k8s.io/apimachinery/pkg/runtime/serializer/cbor/raw_test.go new file mode 100644 index 00000000000..ea362acc048 --- /dev/null +++ b/staging/src/k8s.io/apimachinery/pkg/runtime/serializer/cbor/raw_test.go @@ -0,0 +1,126 @@ +/* +Copyright 2024 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 cbor + +import ( + "fmt" + "testing" + + "k8s.io/apimachinery/pkg/runtime" + + "github.com/google/go-cmp/cmp" +) + +func TestTranscodeRawTypes(t *testing.T) { + for _, tc := range []struct { + In interface{} + Out interface{} + }{ + { + In: nil, + Out: nil, + }, + { + In: &runtime.RawExtension{Raw: []byte{0xd9, 0xd9, 0xf7, 0x07}}, + Out: &runtime.RawExtension{Raw: []byte(`7`)}, + }, + { + In: &runtime.RawExtension{}, + Out: &runtime.RawExtension{}, + }, + { + In: runtime.RawExtension{Raw: []byte{0xd9, 0xd9, 0xf7, 0x07}}, + Out: runtime.RawExtension{Raw: []byte{0xd9, 0xd9, 0xf7, 0x07}}, // not addressable + }, + { + In: &[...]runtime.RawExtension{{Raw: []byte{0xd9, 0xd9, 0xf7, 0x07}}, {Raw: []byte{0xd9, 0xd9, 0xf7, 0x08}}, {Raw: []byte{0xd9, 0xd9, 0xf7, 0x09}}}, + Out: &[...]runtime.RawExtension{{Raw: []byte(`7`)}, {Raw: []byte(`8`)}, {Raw: []byte(`9`)}}, + }, + { + In: &[0]runtime.RawExtension{}, + Out: &[0]runtime.RawExtension{}, + }, + { + In: &[]runtime.RawExtension{{Raw: []byte{0xd9, 0xd9, 0xf7, 0x07}}, {Raw: []byte{0xd9, 0xd9, 0xf7, 0x08}}, {Raw: []byte{0xd9, 0xd9, 0xf7, 0x09}}}, + Out: &[]runtime.RawExtension{{Raw: []byte(`7`)}, {Raw: []byte(`8`)}, {Raw: []byte(`9`)}}, + }, + { + In: &[]runtime.RawExtension{}, + Out: &[]runtime.RawExtension{}, + }, + { + In: &[]string{"foo"}, + Out: &[]string{"foo"}, + }, + { + In: (*runtime.RawExtension)(nil), + Out: (*runtime.RawExtension)(nil), + }, + { + In: &struct{ I fmt.Stringer }{I: &runtime.RawExtension{Raw: []byte{0xd9, 0xd9, 0xf7, 0x07}}}, + Out: &struct{ I fmt.Stringer }{I: &runtime.RawExtension{Raw: []byte(`7`)}}, + }, + { + In: &struct{ I fmt.Stringer }{I: nil}, + Out: &struct{ I fmt.Stringer }{I: nil}, + }, + { + In: &struct{ I int64 }{I: 7}, + Out: &struct{ I int64 }{I: 7}, + }, + { + In: &struct { + E runtime.RawExtension + I int64 + }{E: runtime.RawExtension{Raw: []byte{0xd9, 0xd9, 0xf7, 0x07}}, I: 7}, + Out: &struct { + E runtime.RawExtension + I int64 + }{E: runtime.RawExtension{Raw: []byte(`7`)}, I: 7}, + }, + { + In: &struct { + runtime.RawExtension + }{RawExtension: runtime.RawExtension{Raw: []byte{0xd9, 0xd9, 0xf7, 0x07}}}, + Out: &struct { + runtime.RawExtension + }{RawExtension: runtime.RawExtension{Raw: []byte(`7`)}}, + }, + { + In: &map[string]string{"hello": "world"}, + Out: &map[string]string{"hello": "world"}, + }, + { + In: &map[string]runtime.RawExtension{"hello": {Raw: []byte{0xd9, 0xd9, 0xf7, 0x07}}}, + Out: &map[string]runtime.RawExtension{"hello": {Raw: []byte{0xd9, 0xd9, 0xf7, 0x07}}}, // not addressable + }, + { + In: &map[string][]runtime.RawExtension{"hello": {{Raw: []byte{0xd9, 0xd9, 0xf7, 0x07}}}}, + Out: &map[string][]runtime.RawExtension{"hello": {{Raw: []byte(`7`)}}}, + }, + } { + t.Run(fmt.Sprintf("%#v", tc.In), func(t *testing.T) { + if err := transcodeRawTypes(tc.In); err != nil { + t.Fatalf("unexpected error: %v", err) + } + + if diff := cmp.Diff(tc.Out, tc.In); diff != "" { + t.Errorf("unexpected diff:\n%s", diff) + } + }) + } +} diff --git a/staging/src/k8s.io/apimachinery/pkg/runtime/types.go b/staging/src/k8s.io/apimachinery/pkg/runtime/types.go index ce77c7910a9..1680c149f95 100644 --- a/staging/src/k8s.io/apimachinery/pkg/runtime/types.go +++ b/staging/src/k8s.io/apimachinery/pkg/runtime/types.go @@ -46,6 +46,7 @@ const ( ContentTypeJSON string = "application/json" ContentTypeYAML string = "application/yaml" ContentTypeProtobuf string = "application/vnd.kubernetes.protobuf" + ContentTypeCBOR string = "application/cbor" ) // RawExtension is used to hold extensions in external versions.