Skip to content

Commit 7e6b866

Browse files
committed
Decode CBOR to UnstructuredList as UnstructuredJSONScheme does.
Decoding to map[string]interface{} and passing the result to UnstructuredList's SetUnstructuredContent method does not produce objects that are identical to those produced by UnstructuredJSONScheme's decode method. UnstructuredJSONScheme's decode: 1. removes the "items" key from the map in its Object field 2. sets "apiVersion" and "kind" (determined heuristically from the list's GVK) on elements of its Items slice that were not serialized with a nonempty string "apiVersion" and "kind" 3. returns a missing kind error if any element is missing "kind"
1 parent 51ad0bb commit 7e6b866

File tree

2 files changed

+222
-6
lines changed

2 files changed

+222
-6
lines changed

staging/src/k8s.io/apimachinery/pkg/runtime/serializer/cbor/cbor.go

Lines changed: 75 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,10 @@ import (
2222
"errors"
2323
"fmt"
2424
"io"
25+
"strings"
2526

2627
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
28+
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
2729
"k8s.io/apimachinery/pkg/runtime"
2830
"k8s.io/apimachinery/pkg/runtime/schema"
2931
"k8s.io/apimachinery/pkg/runtime/serializer/cbor/internal/modes"
@@ -137,16 +139,83 @@ func diagnose(data []byte) string {
137139
return diag
138140
}
139141

142+
// unmarshal unmarshals CBOR data from the provided byte slice into a Go object. If the decoder is
143+
// configured to report strict errors, the first error return value may be a non-nil strict decoding
144+
// error. If the last error return value is non-nil, then the unmarshal failed entirely and the
145+
// state of the destination object should not be relied on.
140146
func (s *serializer) unmarshal(data []byte, into interface{}) (strict, lax error) {
141147
if u, ok := into.(runtime.Unstructured); ok {
142148
var content map[string]interface{}
143149
defer func() {
144-
// TODO: The UnstructuredList implementation of SetUnstructuredContent is
145-
// not identical to what unstructuredJSONScheme does: (1) it retains the
146-
// "items" key in its Object field, and (2) it does not infer a singular
147-
// Kind from the list's Kind and populate omitted apiVersion/kind for all
148-
// entries in Items.
149-
u.SetUnstructuredContent(content)
150+
switch u := u.(type) {
151+
case *unstructured.UnstructuredList:
152+
// UnstructuredList's implementation of SetUnstructuredContent
153+
// produces different objects than those produced by a decode using
154+
// UnstructuredJSONScheme:
155+
//
156+
// 1. SetUnstructuredContent retains the "items" key in the list's
157+
// Object field. It is omitted from Object when decoding with
158+
// UnstructuredJSONScheme.
159+
// 2. SetUnstructuredContent does not populate "apiVersion" and
160+
// "kind" on each entry of its Items
161+
// field. UnstructuredJSONScheme does, inferring the singular
162+
// Kind from the list Kind.
163+
// 3. SetUnstructuredContent ignores entries of "items" that are
164+
// not JSON objects or are objects without
165+
// "kind". UnstructuredJSONScheme returns an error in either
166+
// case.
167+
//
168+
// UnstructuredJSONScheme's behavior is replicated here.
169+
var items []interface{}
170+
if uncast, present := content["items"]; present {
171+
var cast bool
172+
items, cast = uncast.([]interface{})
173+
if !cast {
174+
strict, lax = nil, fmt.Errorf("items field of UnstructuredList must be encoded as an array or null if present")
175+
return
176+
}
177+
}
178+
apiVersion, _ := content["apiVersion"].(string)
179+
kind, _ := content["kind"].(string)
180+
kind = strings.TrimSuffix(kind, "List")
181+
var unstructureds []unstructured.Unstructured
182+
if len(items) > 0 {
183+
unstructureds = make([]unstructured.Unstructured, len(items))
184+
}
185+
for i := range items {
186+
object, cast := items[i].(map[string]interface{})
187+
if !cast {
188+
strict, lax = nil, fmt.Errorf("elements of the items field of UnstructuredList must be encoded as a map")
189+
return
190+
}
191+
192+
// As in UnstructuredJSONScheme, only set the heuristic
193+
// singular GVK when both "apiVersion" and "kind" are either
194+
// missing, non-string, or empty.
195+
object["apiVersion"], _ = object["apiVersion"].(string)
196+
object["kind"], _ = object["kind"].(string)
197+
if object["apiVersion"] == "" && object["kind"] == "" {
198+
object["apiVersion"] = apiVersion
199+
object["kind"] = kind
200+
}
201+
202+
if object["kind"] == "" {
203+
strict, lax = nil, runtime.NewMissingKindErr(diagnose(data))
204+
return
205+
}
206+
if object["apiVersion"] == "" {
207+
strict, lax = nil, runtime.NewMissingVersionErr(diagnose(data))
208+
return
209+
}
210+
211+
unstructureds[i].Object = object
212+
}
213+
delete(content, "items")
214+
u.Object = content
215+
u.Items = unstructureds
216+
default:
217+
u.SetUnstructuredContent(content)
218+
}
150219
}()
151220
into = &content
152221
}

staging/src/k8s.io/apimachinery/pkg/runtime/serializer/cbor/cbor_test.go

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,33 @@ func TestEncode(t *testing.T) {
156156
}
157157
},
158158
},
159+
{
160+
name: "unstructuredlist",
161+
in: &unstructured.UnstructuredList{
162+
Object: map[string]interface{}{
163+
"apiVersion": "v",
164+
"kind": "kList",
165+
},
166+
Items: []unstructured.Unstructured{
167+
{Object: map[string]interface{}{"foo": int64(1)}},
168+
{Object: map[string]interface{}{"foo": int64(2)}},
169+
},
170+
},
171+
assertOnWriter: func() (io.Writer, func(t *testing.T)) {
172+
var b bytes.Buffer
173+
return &b, func(t *testing.T) {
174+
// {'kind': 'kList', 'items': [{'foo': 1}, {'foo': 2}], 'apiVersion': 'v'}
175+
if diff := cmp.Diff(b.Bytes(), []byte("\xd9\xd9\xf7\xa3\x44kind\x45kList\x45items\x82\xa1\x43foo\x01\xa1\x43foo\x02\x4aapiVersion\x41v")); diff != "" {
176+
t.Errorf("unexpected diff:\n%s", diff)
177+
}
178+
}
179+
},
180+
assertOnError: func(t *testing.T, err error) {
181+
if err != nil {
182+
t.Errorf("expected nil error, got: %v", err)
183+
}
184+
},
185+
},
159186
} {
160187
t.Run(tc.name, func(t *testing.T) {
161188
s := NewSerializer(nil, nil)
@@ -417,6 +444,126 @@ func TestDecode(t *testing.T) {
417444
}
418445
},
419446
},
447+
{
448+
name: "into unstructuredlist missing kind",
449+
data: []byte("\xa1\x6aapiVersion\x61v"),
450+
into: &unstructured.UnstructuredList{},
451+
expectedObj: nil,
452+
expectedGVK: &schema.GroupVersionKind{Version: "v"},
453+
assertOnError: func(t *testing.T, err error) {
454+
if !runtime.IsMissingKind(err) {
455+
t.Errorf("expected MissingKind, got: %v", err)
456+
}
457+
},
458+
},
459+
{
460+
name: "into unstructuredlist missing version",
461+
data: []byte("\xa1\x64kind\x65kList"),
462+
into: &unstructured.UnstructuredList{},
463+
expectedObj: nil,
464+
expectedGVK: &schema.GroupVersionKind{Kind: "kList"},
465+
assertOnError: func(t *testing.T, err error) {
466+
if !runtime.IsMissingVersion(err) {
467+
t.Errorf("expected MissingVersion, got: %v", err)
468+
}
469+
},
470+
},
471+
{
472+
name: "into unstructuredlist empty",
473+
data: []byte("\xa2\x6aapiVersion\x61v\x64kind\x65kList"),
474+
into: &unstructured.UnstructuredList{},
475+
expectedObj: &unstructured.UnstructuredList{Object: map[string]interface{}{
476+
"apiVersion": "v",
477+
"kind": "kList",
478+
}},
479+
expectedGVK: &schema.GroupVersionKind{Version: "v", Kind: "kList"},
480+
assertOnError: func(t *testing.T, err error) {
481+
if err != nil {
482+
t.Errorf("expected nil error, got: %v", err)
483+
}
484+
},
485+
},
486+
{
487+
name: "into unstructuredlist nonempty",
488+
data: []byte("\xa3\x6aapiVersion\x61v\x64kind\x65kList\x65items\x82\xa1\x63foo\x01\xa1\x63foo\x02"), // {"apiVersion": "v", "kind": "kList", "items": [{"foo": 1}, {"foo": 2}]}
489+
into: &unstructured.UnstructuredList{},
490+
expectedObj: &unstructured.UnstructuredList{
491+
Object: map[string]interface{}{
492+
"apiVersion": "v",
493+
"kind": "kList",
494+
},
495+
Items: []unstructured.Unstructured{
496+
{Object: map[string]interface{}{"apiVersion": "v", "kind": "k", "foo": int64(1)}},
497+
{Object: map[string]interface{}{"apiVersion": "v", "kind": "k", "foo": int64(2)}},
498+
},
499+
},
500+
expectedGVK: &schema.GroupVersionKind{Version: "v", Kind: "kList"},
501+
assertOnError: func(t *testing.T, err error) {
502+
if err != nil {
503+
t.Errorf("expected nil error, got: %v", err)
504+
}
505+
},
506+
},
507+
{
508+
name: "into unstructuredlist item gvk present",
509+
data: []byte("\xa3\x6aapiVersion\x61v\x64kind\x65kList\x65items\x81\xa2\x6aapiVersion\x62vv\x64kind\x62kk"), // {"apiVersion": "v", "kind": "kList", "items": [{"apiVersion": "vv", "kind": "kk"}]}
510+
into: &unstructured.UnstructuredList{},
511+
expectedObj: &unstructured.UnstructuredList{
512+
Object: map[string]interface{}{
513+
"apiVersion": "v",
514+
"kind": "kList",
515+
},
516+
Items: []unstructured.Unstructured{
517+
{Object: map[string]interface{}{"apiVersion": "vv", "kind": "kk"}},
518+
},
519+
},
520+
expectedGVK: &schema.GroupVersionKind{Version: "v", Kind: "kList"},
521+
assertOnError: func(t *testing.T, err error) {
522+
if err != nil {
523+
t.Errorf("expected nil error, got: %v", err)
524+
}
525+
},
526+
},
527+
{
528+
name: "into unstructuredlist item missing kind",
529+
data: []byte("\xa3\x6aapiVersion\x61v\x64kind\x65kList\x65items\x81\xa1\x6aapiVersion\x62vv"), // {"apiVersion": "v", "kind": "kList", "items": [{"apiVersion": "vv"}]}
530+
metaFactory: &defaultMetaFactory{},
531+
into: &unstructured.UnstructuredList{},
532+
expectedGVK: &schema.GroupVersionKind{Version: "v", Kind: "kList"},
533+
assertOnError: func(t *testing.T, err error) {
534+
if !runtime.IsMissingKind(err) {
535+
t.Errorf("expected MissingVersion, got: %v", err)
536+
}
537+
},
538+
},
539+
{
540+
name: "into unstructuredlist item missing version",
541+
data: []byte("\xa3\x6aapiVersion\x61v\x64kind\x65kList\x65items\x81\xa1\x64kind\x62kk"), // {"apiVersion": "v", "kind": "kList", "items": [{"kind": "kk"}]}
542+
metaFactory: &defaultMetaFactory{},
543+
into: &unstructured.UnstructuredList{},
544+
expectedGVK: &schema.GroupVersionKind{Version: "v", Kind: "kList"},
545+
assertOnError: func(t *testing.T, err error) {
546+
if !runtime.IsMissingVersion(err) {
547+
t.Errorf("expected MissingVersion, got: %v", err)
548+
}
549+
},
550+
},
551+
{
552+
name: "using unstructuredlist creater",
553+
data: []byte("\xa2\x6aapiVersion\x61v\x64kind\x65kList"),
554+
metaFactory: &defaultMetaFactory{},
555+
creater: stubCreater{obj: &unstructured.UnstructuredList{}},
556+
expectedObj: &unstructured.UnstructuredList{Object: map[string]interface{}{
557+
"apiVersion": "v",
558+
"kind": "kList",
559+
}},
560+
expectedGVK: &schema.GroupVersionKind{Version: "v", Kind: "kList"},
561+
assertOnError: func(t *testing.T, err error) {
562+
if err != nil {
563+
t.Errorf("expected nil error, got: %v", err)
564+
}
565+
},
566+
},
420567
} {
421568
t.Run(tc.name, func(t *testing.T) {
422569
s := newSerializer(tc.metaFactory, tc.creater, tc.typer, tc.options...)

0 commit comments

Comments
 (0)