Skip to content

Commit e55063d

Browse files
benluddyk8s-publishing-bot
authored andcommitted
Automatically transcode RawExtension between unstructured protocols.
Kubernetes-commit: 4755e1f85979f4db11114261797e6da8b116dc10
1 parent e126c65 commit e55063d

File tree

7 files changed

+624
-5
lines changed

7 files changed

+624
-5
lines changed

pkg/runtime/extension.go

+95-5
Original file line numberDiff line numberDiff line change
@@ -18,16 +18,77 @@ package runtime
1818

1919
import (
2020
"bytes"
21-
"encoding/json"
2221
"errors"
22+
"fmt"
23+
24+
cbor "k8s.io/apimachinery/pkg/runtime/serializer/cbor/direct"
25+
"k8s.io/apimachinery/pkg/util/json"
2326
)
2427

28+
// RawExtension intentionally avoids implementing value.UnstructuredConverter for now because the
29+
// signature of ToUnstructured does not allow returning an error value in cases where the conversion
30+
// is not possible (content type is unrecognized or bytes don't match content type).
31+
func rawToUnstructured(raw []byte, contentType string) (interface{}, error) {
32+
switch contentType {
33+
case ContentTypeJSON:
34+
var u interface{}
35+
if err := json.Unmarshal(raw, &u); err != nil {
36+
return nil, fmt.Errorf("failed to parse RawExtension bytes as JSON: %w", err)
37+
}
38+
return u, nil
39+
case ContentTypeCBOR:
40+
var u interface{}
41+
if err := cbor.Unmarshal(raw, &u); err != nil {
42+
return nil, fmt.Errorf("failed to parse RawExtension bytes as CBOR: %w", err)
43+
}
44+
return u, nil
45+
default:
46+
return nil, fmt.Errorf("cannot convert RawExtension with unrecognized content type to unstructured")
47+
}
48+
}
49+
50+
func (re RawExtension) guessContentType() string {
51+
switch {
52+
case bytes.HasPrefix(re.Raw, cborSelfDescribed):
53+
return ContentTypeCBOR
54+
case len(re.Raw) > 0:
55+
switch re.Raw[0] {
56+
case '\t', '\r', '\n', ' ', '{', '[', 'n', 't', 'f', '"', '-', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9':
57+
// Prefixes for the four whitespace characters, objects, arrays, strings, numbers, true, false, and null.
58+
return ContentTypeJSON
59+
}
60+
}
61+
return ""
62+
}
63+
2564
func (re *RawExtension) UnmarshalJSON(in []byte) error {
2665
if re == nil {
2766
return errors.New("runtime.RawExtension: UnmarshalJSON on nil pointer")
2867
}
29-
if !bytes.Equal(in, []byte("null")) {
30-
re.Raw = append(re.Raw[0:0], in...)
68+
if bytes.Equal(in, []byte("null")) {
69+
return nil
70+
}
71+
re.Raw = append(re.Raw[0:0], in...)
72+
return nil
73+
}
74+
75+
var (
76+
cborNull = []byte{0xf6}
77+
cborSelfDescribed = []byte{0xd9, 0xd9, 0xf7}
78+
)
79+
80+
func (re *RawExtension) UnmarshalCBOR(in []byte) error {
81+
if re == nil {
82+
return errors.New("runtime.RawExtension: UnmarshalCBOR on nil pointer")
83+
}
84+
if !bytes.Equal(in, cborNull) {
85+
if !bytes.HasPrefix(in, cborSelfDescribed) {
86+
// The self-described CBOR tag doesn't change the interpretation of the data
87+
// item it encloses, but it is useful as a magic number. Its encoding is
88+
// also what is used to implement the CBOR RecognizingDecoder.
89+
re.Raw = append(re.Raw[:0], cborSelfDescribed...)
90+
}
91+
re.Raw = append(re.Raw, in...)
3192
}
3293
return nil
3394
}
@@ -46,6 +107,35 @@ func (re RawExtension) MarshalJSON() ([]byte, error) {
46107
}
47108
return []byte("null"), nil
48109
}
49-
// TODO: Check whether ContentType is actually JSON before returning it.
50-
return re.Raw, nil
110+
111+
contentType := re.guessContentType()
112+
if contentType == ContentTypeJSON {
113+
return re.Raw, nil
114+
}
115+
116+
u, err := rawToUnstructured(re.Raw, contentType)
117+
if err != nil {
118+
return nil, err
119+
}
120+
return json.Marshal(u)
121+
}
122+
123+
func (re RawExtension) MarshalCBOR() ([]byte, error) {
124+
if re.Raw == nil {
125+
if re.Object != nil {
126+
return cbor.Marshal(re.Object)
127+
}
128+
return cbor.Marshal(nil)
129+
}
130+
131+
contentType := re.guessContentType()
132+
if contentType == ContentTypeCBOR {
133+
return re.Raw, nil
134+
}
135+
136+
u, err := rawToUnstructured(re.Raw, contentType)
137+
if err != nil {
138+
return nil, err
139+
}
140+
return cbor.Marshal(u)
51141
}

pkg/runtime/extension_test.go

+151
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,9 @@ import (
2323
"testing"
2424

2525
"k8s.io/apimachinery/pkg/runtime"
26+
runtimetesting "k8s.io/apimachinery/pkg/runtime/testing"
27+
28+
"github.com/google/go-cmp/cmp"
2629
)
2730

2831
func TestEmbeddedRawExtensionMarshal(t *testing.T) {
@@ -111,3 +114,151 @@ func TestEmbeddedRawExtensionRoundTrip(t *testing.T) {
111114
}
112115
}
113116
}
117+
118+
func TestRawExtensionMarshalUnstructured(t *testing.T) {
119+
for _, tc := range []struct {
120+
Name string
121+
In runtime.RawExtension
122+
WantCBOR []byte
123+
ExpectedErrorCBOR string
124+
WantJSON string
125+
ExpectedErrorJSON string
126+
}{
127+
{
128+
Name: "nil bytes and nil object",
129+
In: runtime.RawExtension{},
130+
WantCBOR: []byte{0xf6},
131+
WantJSON: "null",
132+
},
133+
{
134+
Name: "nil bytes and non-nil object",
135+
In: runtime.RawExtension{Object: &runtimetesting.ExternalSimple{TestString: "foo"}},
136+
WantCBOR: []byte("\xa1\x4atestString\x43foo"),
137+
WantJSON: `{"testString":"foo"}`,
138+
},
139+
{
140+
Name: "cbor bytes not enclosed in self-described tag",
141+
In: runtime.RawExtension{Raw: []byte{0x43, 'f', 'o', 'o'}}, // 'foo'
142+
ExpectedErrorCBOR: "cannot convert RawExtension with unrecognized content type to unstructured",
143+
ExpectedErrorJSON: "cannot convert RawExtension with unrecognized content type to unstructured",
144+
},
145+
{
146+
Name: "cbor bytes enclosed in self-described tag",
147+
In: runtime.RawExtension{Raw: []byte{0xd9, 0xd9, 0xf7, 0x43, 'f', 'o', 'o'}}, // 55799('foo')
148+
WantCBOR: []byte{0xd9, 0xd9, 0xf7, 0x43, 'f', 'o', 'o'}, // 55799('foo')
149+
WantJSON: `"foo"`,
150+
},
151+
{
152+
Name: "json bytes",
153+
In: runtime.RawExtension{Raw: []byte(`"foo"`)},
154+
WantCBOR: []byte{0x43, 'f', 'o', 'o'},
155+
WantJSON: `"foo"`,
156+
},
157+
{
158+
Name: "ambiguous bytes not enclosed in self-described cbor tag",
159+
In: runtime.RawExtension{Raw: []byte{'0'}}, // CBOR -17 / JSON 0
160+
WantCBOR: []byte{0x00},
161+
WantJSON: `0`,
162+
},
163+
{
164+
Name: "ambiguous bytes enclosed in self-described cbor tag",
165+
In: runtime.RawExtension{Raw: []byte{0xd9, 0xd9, 0xf7, '0'}}, // 55799(-17)
166+
WantCBOR: []byte{0xd9, 0xd9, 0xf7, '0'},
167+
WantJSON: `-17`,
168+
},
169+
{
170+
Name: "unrecognized bytes",
171+
In: runtime.RawExtension{Raw: []byte{0xff}},
172+
ExpectedErrorCBOR: "cannot convert RawExtension with unrecognized content type to unstructured",
173+
ExpectedErrorJSON: "cannot convert RawExtension with unrecognized content type to unstructured",
174+
},
175+
{
176+
Name: "invalid cbor with self-described cbor prefix",
177+
In: runtime.RawExtension{Raw: []byte{0xd9, 0xd9, 0xf7, 0xff}},
178+
WantCBOR: []byte{0xd9, 0xd9, 0xf7, 0xff}, // verbatim
179+
ExpectedErrorJSON: `failed to parse RawExtension bytes as CBOR: cbor: unexpected "break" code`,
180+
},
181+
{
182+
Name: "invalid json with json prefix",
183+
In: runtime.RawExtension{Raw: []byte(`{{`)},
184+
ExpectedErrorCBOR: `failed to parse RawExtension bytes as JSON: invalid character '{' looking for beginning of object key string`,
185+
WantJSON: `{{`, // verbatim
186+
},
187+
} {
188+
t.Run(tc.Name, func(t *testing.T) {
189+
t.Run("CBOR", func(t *testing.T) {
190+
got, err := tc.In.MarshalCBOR()
191+
if err != nil {
192+
if tc.ExpectedErrorCBOR == "" {
193+
t.Fatalf("unexpected error: %v", err)
194+
}
195+
if msg := err.Error(); msg != tc.ExpectedErrorCBOR {
196+
t.Fatalf("expected error %q but got %q", tc.ExpectedErrorCBOR, msg)
197+
}
198+
}
199+
200+
if diff := cmp.Diff(tc.WantCBOR, got); diff != "" {
201+
t.Errorf("unexpected diff:\n%s", diff)
202+
}
203+
})
204+
205+
t.Run("JSON", func(t *testing.T) {
206+
got, err := tc.In.MarshalJSON()
207+
if err != nil {
208+
if tc.ExpectedErrorJSON == "" {
209+
t.Fatalf("unexpected error: %v", err)
210+
}
211+
if msg := err.Error(); msg != tc.ExpectedErrorJSON {
212+
t.Fatalf("expected error %q but got %q", tc.ExpectedErrorJSON, msg)
213+
}
214+
}
215+
216+
if diff := cmp.Diff(tc.WantJSON, string(got)); diff != "" {
217+
t.Errorf("unexpected diff:\n%s", diff)
218+
}
219+
})
220+
})
221+
}
222+
}
223+
224+
func TestRawExtensionUnmarshalCBOR(t *testing.T) {
225+
for _, tc := range []struct {
226+
Name string
227+
In []byte
228+
Want runtime.RawExtension
229+
}{
230+
{
231+
// From json.Unmarshaler: By convention, to approximate the behavior of
232+
// Unmarshal itself, Unmarshalers implement UnmarshalJSON([]byte("null")) as
233+
// a no-op.
234+
Name: "no-op on null",
235+
In: []byte{0xf6},
236+
Want: runtime.RawExtension{},
237+
},
238+
{
239+
Name: "input copied verbatim",
240+
In: []byte{0xd9, 0xd9, 0xf7, 0x5f, 0x41, 'f', 0x42, 'o', 'o', 0xff}, // 55799(_ 'f' 'oo')
241+
Want: runtime.RawExtension{
242+
Raw: []byte{0xd9, 0xd9, 0xf7, 0x5f, 0x41, 'f', 0x42, 'o', 'o', 0xff}, // 55799(_ 'f' 'oo')
243+
},
244+
},
245+
{
246+
Name: "input enclosed in self-described tag if absent",
247+
In: []byte{0x5f, 0x41, 'f', 0x42, 'o', 'o', 0xff}, // (_ 'f' 'oo')
248+
Want: runtime.RawExtension{
249+
Raw: []byte{0xd9, 0xd9, 0xf7, 0x5f, 0x41, 'f', 0x42, 'o', 'o', 0xff}, // 55799(_ 'f' 'oo')
250+
},
251+
},
252+
} {
253+
t.Run(tc.Name, func(t *testing.T) {
254+
var got runtime.RawExtension
255+
if err := got.UnmarshalCBOR(tc.In); err != nil {
256+
t.Fatalf("unexpected error: %v", err)
257+
}
258+
259+
if diff := cmp.Diff(tc.Want, got); diff != "" {
260+
t.Errorf("unexpected diff:\n%s", diff)
261+
}
262+
})
263+
}
264+
}

pkg/runtime/serializer/cbor/cbor.go

+6
Original file line numberDiff line numberDiff line change
@@ -298,6 +298,12 @@ func (s *serializer) Decode(data []byte, gvk *schema.GroupVersionKind, into runt
298298
if err != nil {
299299
return nil, actual, err
300300
}
301+
302+
// TODO: Make possible to disable this behavior.
303+
if err := transcodeRawTypes(obj); err != nil {
304+
return nil, actual, err
305+
}
306+
301307
return obj, actual, strict
302308
}
303309

pkg/runtime/serializer/cbor/cbor_test.go

+27
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,18 @@ func (p *anyObject) UnmarshalCBOR(in []byte) error {
121121
return modes.Decode.Unmarshal(in, &p.Value)
122122
}
123123

124+
type structWithRawExtensionField struct {
125+
Extension runtime.RawExtension `json:"extension"`
126+
}
127+
128+
func (p structWithRawExtensionField) GetObjectKind() schema.ObjectKind {
129+
return schema.EmptyObjectKind
130+
}
131+
132+
func (structWithRawExtensionField) DeepCopyObject() runtime.Object {
133+
panic("unimplemented")
134+
}
135+
124136
func TestEncode(t *testing.T) {
125137
for _, tc := range []struct {
126138
name string
@@ -264,6 +276,21 @@ func TestDecode(t *testing.T) {
264276
}
265277
},
266278
},
279+
{
280+
name: "rawextension transcoded",
281+
data: []byte{0xa1, 0x49, 'e', 'x', 't', 'e', 'n', 's', 'i', 'o', 'n', 0xa1, 0x41, 'a', 0x01},
282+
gvk: &schema.GroupVersionKind{},
283+
metaFactory: stubMetaFactory{gvk: &schema.GroupVersionKind{}},
284+
typer: stubTyper{gvks: []schema.GroupVersionKind{{Group: "x", Version: "y", Kind: "z"}}},
285+
into: &structWithRawExtensionField{},
286+
expectedObj: &structWithRawExtensionField{Extension: runtime.RawExtension{Raw: []byte(`{"a":1}`)}},
287+
expectedGVK: &schema.GroupVersionKind{Group: "x", Version: "y", Kind: "z"},
288+
assertOnError: func(t *testing.T, err error) {
289+
if err != nil {
290+
t.Errorf("expected nil error, got: %v", err)
291+
}
292+
},
293+
},
267294
{
268295
name: "strict mode strict error",
269296
options: []Option{Strict(true)},

0 commit comments

Comments
 (0)