Skip to content

Commit 4524748

Browse files
Merge pull request #125790 from benluddy/cbor-fieldsv1
KEP-4222: Support either JSON or CBOR in FieldsV1. Kubernetes-commit: ccbbbc0f1f353e7dec5ce3dd83cccc0b7603d40a
2 parents 6b362fa + 3da83fe commit 4524748

File tree

5 files changed

+447
-9
lines changed

5 files changed

+447
-9
lines changed

pkg/apis/meta/v1/helpers.go

Lines changed: 82 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,10 @@ import (
2424

2525
"k8s.io/apimachinery/pkg/fields"
2626
"k8s.io/apimachinery/pkg/labels"
27+
cbor "k8s.io/apimachinery/pkg/runtime/serializer/cbor/direct"
2728
"k8s.io/apimachinery/pkg/selection"
2829
"k8s.io/apimachinery/pkg/types"
30+
utiljson "k8s.io/apimachinery/pkg/util/json"
2931
)
3032

3133
// LabelSelectorAsSelector converts the LabelSelector api type into a struct that implements
@@ -280,13 +282,20 @@ func (f FieldsV1) MarshalJSON() ([]byte, error) {
280282
if f.Raw == nil {
281283
return []byte("null"), nil
282284
}
285+
if f.getContentType() == fieldsV1InvalidOrValidCBORObject {
286+
var u map[string]interface{}
287+
if err := cbor.Unmarshal(f.Raw, &u); err != nil {
288+
return nil, fmt.Errorf("metav1.FieldsV1 cbor invalid: %w", err)
289+
}
290+
return utiljson.Marshal(u)
291+
}
283292
return f.Raw, nil
284293
}
285294

286295
// UnmarshalJSON implements json.Unmarshaler
287296
func (f *FieldsV1) UnmarshalJSON(b []byte) error {
288297
if f == nil {
289-
return errors.New("metav1.Fields: UnmarshalJSON on nil pointer")
298+
return errors.New("metav1.FieldsV1: UnmarshalJSON on nil pointer")
290299
}
291300
if !bytes.Equal(b, []byte("null")) {
292301
f.Raw = append(f.Raw[0:0], b...)
@@ -296,3 +305,75 @@ func (f *FieldsV1) UnmarshalJSON(b []byte) error {
296305

297306
var _ json.Marshaler = FieldsV1{}
298307
var _ json.Unmarshaler = &FieldsV1{}
308+
309+
func (f FieldsV1) MarshalCBOR() ([]byte, error) {
310+
if f.Raw == nil {
311+
return cbor.Marshal(nil)
312+
}
313+
if f.getContentType() == fieldsV1InvalidOrValidJSONObject {
314+
var u map[string]interface{}
315+
if err := utiljson.Unmarshal(f.Raw, &u); err != nil {
316+
return nil, fmt.Errorf("metav1.FieldsV1 json invalid: %w", err)
317+
}
318+
return cbor.Marshal(u)
319+
}
320+
return f.Raw, nil
321+
}
322+
323+
var cborNull = []byte{0xf6}
324+
325+
func (f *FieldsV1) UnmarshalCBOR(b []byte) error {
326+
if f == nil {
327+
return errors.New("metav1.FieldsV1: UnmarshalCBOR on nil pointer")
328+
}
329+
if !bytes.Equal(b, cborNull) {
330+
f.Raw = append(f.Raw[0:0], b...)
331+
}
332+
return nil
333+
}
334+
335+
const (
336+
// fieldsV1InvalidOrEmpty indicates that a FieldsV1 either contains no raw bytes or its raw
337+
// bytes don't represent an allowable value in any supported encoding.
338+
fieldsV1InvalidOrEmpty = iota
339+
340+
// fieldsV1InvalidOrValidJSONObject indicates that a FieldV1 either contains raw bytes that
341+
// are a valid JSON encoding of an allowable value or don't represent an allowable value in
342+
// any supported encoding.
343+
fieldsV1InvalidOrValidJSONObject
344+
345+
// fieldsV1InvalidOrValidCBORObject indicates that a FieldV1 either contains raw bytes that
346+
// are a valid CBOR encoding of an allowable value or don't represent an allowable value in
347+
// any supported encoding.
348+
fieldsV1InvalidOrValidCBORObject
349+
)
350+
351+
// getContentType returns one of fieldsV1InvalidOrEmpty, fieldsV1InvalidOrValidJSONObject,
352+
// fieldsV1InvalidOrValidCBORObject based on the value of Raw.
353+
//
354+
// Raw can be encoded in JSON or CBOR and is only valid if it is empty, null, or an object (map)
355+
// value. It is invalid if it contains a JSON string, number, boolean, or array. If Raw is nonempty
356+
// and represents an allowable value, then the initial byte unambiguously distinguishes a
357+
// JSON-encoded value from a CBOR-encoded value.
358+
//
359+
// A valid JSON-encoded value can begin with any of the four JSON whitespace characters, the first
360+
// character 'n' of null, or '{' (0x09, 0x0a, 0x0d, 0x20, 0x6e, or 0x7b, respectively). A valid
361+
// CBOR-encoded value can begin with the null simple value, an initial byte with major type "map",
362+
// or, if a tag-enclosed map, an initial byte with major type "tag" (0xf6, 0xa0...0xbf, or
363+
// 0xc6...0xdb). The two sets of valid initial bytes don't intersect.
364+
func (f FieldsV1) getContentType() int {
365+
if len(f.Raw) > 0 {
366+
p := f.Raw[0]
367+
switch p {
368+
case 'n', '{', '\t', '\r', '\n', ' ':
369+
return fieldsV1InvalidOrValidJSONObject
370+
case 0xf6: // null
371+
return fieldsV1InvalidOrValidCBORObject
372+
default:
373+
if p >= 0xa0 && p <= 0xbf /* map */ || p >= 0xc6 && p <= 0xdb /* tag */ {
374+
return fieldsV1InvalidOrValidCBORObject
375+
}
376+
}
377+
}
378+
return fieldsV1InvalidOrEmpty
379+
}

pkg/apis/meta/v1/helpers_test.go

Lines changed: 242 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -249,3 +249,245 @@ func TestSetMetaDataLabel(t *testing.T) {
249249
}
250250
}
251251
}
252+
253+
func TestFieldsV1MarshalJSON(t *testing.T) {
254+
for _, tc := range []struct {
255+
Name string
256+
FieldsV1 FieldsV1
257+
Want []byte
258+
Error string
259+
}{
260+
{
261+
Name: "nil encodes as json null",
262+
FieldsV1: FieldsV1{},
263+
Want: []byte(`null`),
264+
},
265+
{
266+
Name: "empty invalid json is returned as-is",
267+
FieldsV1: FieldsV1{Raw: []byte{}},
268+
Want: []byte{},
269+
},
270+
{
271+
Name: "cbor null is transcoded to json null",
272+
FieldsV1: FieldsV1{Raw: []byte{0xf6}}, // null
273+
Want: []byte(`null`),
274+
},
275+
{
276+
Name: "valid non-map cbor and valid non-object json is returned as-is",
277+
FieldsV1: FieldsV1{Raw: []byte{0x30}},
278+
Want: []byte{0x30}, // Valid CBOR encoding of -17 and JSON encoding of 0!
279+
},
280+
{
281+
Name: "self-described cbor map is transcoded to json map",
282+
FieldsV1: FieldsV1{Raw: []byte{0xd9, 0xd9, 0xf7, 0xa1, 0x43, 'f', 'o', 'o', 0x43, 'b', 'a', 'r'}}, // 55799({"foo":"bar"})
283+
Want: []byte(`{"foo":"bar"}`),
284+
},
285+
{
286+
Name: "json object is returned as-is",
287+
FieldsV1: FieldsV1{Raw: []byte(" \t\r\n{\"foo\":\"bar\"}")},
288+
Want: []byte(" \t\r\n{\"foo\":\"bar\"}"),
289+
},
290+
{
291+
Name: "invalid json is returned as-is",
292+
FieldsV1: FieldsV1{Raw: []byte(`{{`)},
293+
Want: []byte(`{{`),
294+
},
295+
{
296+
Name: "invalid cbor fails to transcode to json",
297+
FieldsV1: FieldsV1{Raw: []byte{0xa1}},
298+
Error: "metav1.FieldsV1 cbor invalid: unexpected EOF",
299+
},
300+
} {
301+
t.Run(tc.Name, func(t *testing.T) {
302+
got, err := tc.FieldsV1.MarshalJSON()
303+
if err != nil {
304+
if tc.Error == "" {
305+
t.Fatalf("unexpected error: %v", err)
306+
}
307+
if msg := err.Error(); msg != tc.Error {
308+
t.Fatalf("expected error %q, got %q", tc.Error, msg)
309+
}
310+
} else if tc.Error != "" {
311+
t.Fatalf("expected error %q, got nil", tc.Error)
312+
}
313+
if diff := cmp.Diff(tc.Want, got); diff != "" {
314+
t.Errorf("unexpected diff:\n%s", diff)
315+
}
316+
})
317+
}
318+
}
319+
320+
func TestFieldsV1MarshalCBOR(t *testing.T) {
321+
for _, tc := range []struct {
322+
Name string
323+
FieldsV1 FieldsV1
324+
Want []byte
325+
Error string
326+
}{
327+
{
328+
Name: "nil encodes as cbor null",
329+
FieldsV1: FieldsV1{},
330+
Want: []byte{0xf6}, // null
331+
},
332+
{
333+
Name: "empty invalid cbor is returned as-is",
334+
FieldsV1: FieldsV1{Raw: []byte{}},
335+
Want: []byte{},
336+
},
337+
{
338+
Name: "json null is transcoded to cbor null",
339+
FieldsV1: FieldsV1{Raw: []byte(`null`)},
340+
Want: []byte{0xf6}, // null
341+
},
342+
{
343+
Name: "valid non-map cbor and valid non-object json is returned as-is",
344+
FieldsV1: FieldsV1{Raw: []byte{0x30}},
345+
Want: []byte{0x30}, // Valid CBOR encoding of -17 and JSON encoding of 0!
346+
},
347+
{
348+
Name: "json object is transcoded to cbor map",
349+
FieldsV1: FieldsV1{Raw: []byte(" \t\r\n{\"foo\":\"bar\"}")},
350+
Want: []byte{0xa1, 0x43, 'f', 'o', 'o', 0x43, 'b', 'a', 'r'},
351+
},
352+
{
353+
Name: "self-described cbor map is returned as-is",
354+
FieldsV1: FieldsV1{Raw: []byte{0xd9, 0xd9, 0xf7, 0xa1, 0x43, 'f', 'o', 'o', 0x43, 'b', 'a', 'r'}}, // 55799({"foo":"bar"})
355+
Want: []byte{0xd9, 0xd9, 0xf7, 0xa1, 0x43, 'f', 'o', 'o', 0x43, 'b', 'a', 'r'}, // 55799({"foo":"bar"})
356+
},
357+
{
358+
Name: "invalid json fails to transcode to cbor",
359+
FieldsV1: FieldsV1{Raw: []byte(`{{`)},
360+
Error: "metav1.FieldsV1 json invalid: invalid character '{' looking for beginning of object key string",
361+
},
362+
{
363+
Name: "invalid cbor is returned as-is",
364+
FieldsV1: FieldsV1{Raw: []byte{0xa1}},
365+
Want: []byte{0xa1},
366+
},
367+
} {
368+
t.Run(tc.Name, func(t *testing.T) {
369+
got, err := tc.FieldsV1.MarshalCBOR()
370+
if err != nil {
371+
if tc.Error == "" {
372+
t.Fatalf("unexpected error: %v", err)
373+
}
374+
if msg := err.Error(); msg != tc.Error {
375+
t.Fatalf("expected error %q, got %q", tc.Error, msg)
376+
}
377+
} else if tc.Error != "" {
378+
t.Fatalf("expected error %q, got nil", tc.Error)
379+
}
380+
381+
if diff := cmp.Diff(tc.Want, got); diff != "" {
382+
t.Errorf("unexpected diff:\n%s", diff)
383+
}
384+
})
385+
}
386+
}
387+
388+
func TestFieldsV1UnmarshalJSON(t *testing.T) {
389+
for _, tc := range []struct {
390+
Name string
391+
JSON []byte
392+
Into *FieldsV1
393+
Want *FieldsV1
394+
Error string
395+
}{
396+
{
397+
Name: "nil receiver returns error",
398+
Into: nil,
399+
Error: "metav1.FieldsV1: UnmarshalJSON on nil pointer",
400+
},
401+
{
402+
Name: "json null does not modify receiver", // conventional for json.Unmarshaler
403+
JSON: []byte(`null`),
404+
Into: &FieldsV1{Raw: []byte(`unmodified`)},
405+
Want: &FieldsV1{Raw: []byte(`unmodified`)},
406+
},
407+
{
408+
Name: "valid input is copied verbatim",
409+
JSON: []byte("{\"foo\":\"bar\"} \t\r\n"),
410+
Into: &FieldsV1{},
411+
Want: &FieldsV1{Raw: []byte("{\"foo\":\"bar\"} \t\r\n")},
412+
},
413+
{
414+
Name: "invalid input is copied verbatim",
415+
JSON: []byte("{{"),
416+
Into: &FieldsV1{},
417+
Want: &FieldsV1{Raw: []byte("{{")},
418+
},
419+
} {
420+
t.Run(tc.Name, func(t *testing.T) {
421+
got := tc.Into.DeepCopy()
422+
err := got.UnmarshalJSON(tc.JSON)
423+
if err != nil {
424+
if tc.Error == "" {
425+
t.Fatalf("unexpected error: %v", err)
426+
}
427+
if msg := err.Error(); msg != tc.Error {
428+
t.Fatalf("expected error %q, got %q", tc.Error, msg)
429+
}
430+
} else if tc.Error != "" {
431+
t.Fatalf("expected error %q, got nil", tc.Error)
432+
}
433+
434+
if diff := cmp.Diff(tc.Want, got); diff != "" {
435+
t.Errorf("unexpected diff:\n%s", diff)
436+
}
437+
})
438+
}
439+
}
440+
441+
func TestFieldsV1UnmarshalCBOR(t *testing.T) {
442+
for _, tc := range []struct {
443+
Name string
444+
CBOR []byte
445+
Into *FieldsV1
446+
Want *FieldsV1
447+
Error string
448+
}{
449+
{
450+
Name: "nil receiver returns error",
451+
Into: nil,
452+
Want: nil,
453+
Error: "metav1.FieldsV1: UnmarshalCBOR on nil pointer",
454+
},
455+
{
456+
Name: "cbor null does not modify receiver",
457+
CBOR: []byte{0xf6},
458+
Into: &FieldsV1{Raw: []byte(`unmodified`)},
459+
Want: &FieldsV1{Raw: []byte(`unmodified`)},
460+
},
461+
{
462+
Name: "valid input is copied verbatim",
463+
CBOR: []byte{0xa1, 0x43, 'f', 'o', 'o', 0x43, 'b', 'a', 'r'},
464+
Into: &FieldsV1{},
465+
Want: &FieldsV1{Raw: []byte{0xa1, 0x43, 'f', 'o', 'o', 0x43, 'b', 'a', 'r'}},
466+
},
467+
{
468+
Name: "invalid input is copied verbatim",
469+
CBOR: []byte{0xff}, // UnmarshalCBOR should never be called with malformed input, testing anyway.
470+
Into: &FieldsV1{},
471+
Want: &FieldsV1{Raw: []byte{0xff}},
472+
},
473+
} {
474+
t.Run(tc.Name, func(t *testing.T) {
475+
got := tc.Into.DeepCopy()
476+
err := got.UnmarshalCBOR(tc.CBOR)
477+
if err != nil {
478+
if tc.Error == "" {
479+
t.Fatalf("unexpected error: %v", err)
480+
}
481+
if msg := err.Error(); msg != tc.Error {
482+
t.Fatalf("expected error %q, got %q", tc.Error, msg)
483+
}
484+
} else if tc.Error != "" {
485+
t.Fatalf("expected error %q, got nil", tc.Error)
486+
}
487+
488+
if diff := cmp.Diff(tc.Want, got); diff != "" {
489+
t.Errorf("unexpected diff:\n%s", diff)
490+
}
491+
})
492+
}
493+
}

0 commit comments

Comments
 (0)