Skip to content

Commit c9cfc74

Browse files
authored
Merge pull request kubernetes#124775 from benluddy/cbor-unstructuredlist
KEP-4222: Decode CBOR to UnstructuredList as UnstructuredJSONScheme does.
2 parents df35f17 + 7e6b866 commit c9cfc74

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)