mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-07-31 15:25:57 +00:00
Merge pull request #125629 from benluddy/cbor-rawextension
KEP-4222: Automatically transcode RawExtension between unstructured protocols.
This commit is contained in:
commit
2d877b5259
@ -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)
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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)},
|
||||
|
@ -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
|
||||
}
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
@ -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.
|
||||
|
Loading…
Reference in New Issue
Block a user