Skip to content

Commit

Permalink
Decode CBOR to UnstructuredList as UnstructuredJSONScheme does.
Browse files Browse the repository at this point in the history
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"
  • Loading branch information
benluddy committed May 13, 2024
1 parent 51ad0bb commit 7e6b866
Show file tree
Hide file tree
Showing 2 changed files with 222 additions and 6 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,10 @@ import (
"errors"
"fmt"
"io"
"strings"

metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/runtime/serializer/cbor/internal/modes"
Expand Down Expand Up @@ -137,16 +139,83 @@ func diagnose(data []byte) string {
return diag
}

// unmarshal unmarshals CBOR data from the provided byte slice into a Go object. If the decoder is
// configured to report strict errors, the first error return value may be a non-nil strict decoding
// error. If the last error return value is non-nil, then the unmarshal failed entirely and the
// state of the destination object should not be relied on.
func (s *serializer) unmarshal(data []byte, into interface{}) (strict, lax error) {
if u, ok := into.(runtime.Unstructured); ok {
var content map[string]interface{}
defer func() {
// TODO: The UnstructuredList implementation of SetUnstructuredContent is
// not identical to what unstructuredJSONScheme does: (1) it retains the
// "items" key in its Object field, and (2) it does not infer a singular
// Kind from the list's Kind and populate omitted apiVersion/kind for all
// entries in Items.
u.SetUnstructuredContent(content)
switch u := u.(type) {
case *unstructured.UnstructuredList:
// UnstructuredList's implementation of SetUnstructuredContent
// produces different objects than those produced by a decode using
// UnstructuredJSONScheme:
//
// 1. SetUnstructuredContent retains the "items" key in the list's
// Object field. It is omitted from Object when decoding with
// UnstructuredJSONScheme.
// 2. SetUnstructuredContent does not populate "apiVersion" and
// "kind" on each entry of its Items
// field. UnstructuredJSONScheme does, inferring the singular
// Kind from the list Kind.
// 3. SetUnstructuredContent ignores entries of "items" that are
// not JSON objects or are objects without
// "kind". UnstructuredJSONScheme returns an error in either
// case.
//
// UnstructuredJSONScheme's behavior is replicated here.
var items []interface{}
if uncast, present := content["items"]; present {
var cast bool
items, cast = uncast.([]interface{})
if !cast {
strict, lax = nil, fmt.Errorf("items field of UnstructuredList must be encoded as an array or null if present")
return
}
}
apiVersion, _ := content["apiVersion"].(string)
kind, _ := content["kind"].(string)
kind = strings.TrimSuffix(kind, "List")
var unstructureds []unstructured.Unstructured
if len(items) > 0 {
unstructureds = make([]unstructured.Unstructured, len(items))
}
for i := range items {
object, cast := items[i].(map[string]interface{})
if !cast {
strict, lax = nil, fmt.Errorf("elements of the items field of UnstructuredList must be encoded as a map")
return
}

// As in UnstructuredJSONScheme, only set the heuristic
// singular GVK when both "apiVersion" and "kind" are either
// missing, non-string, or empty.
object["apiVersion"], _ = object["apiVersion"].(string)
object["kind"], _ = object["kind"].(string)
if object["apiVersion"] == "" && object["kind"] == "" {
object["apiVersion"] = apiVersion
object["kind"] = kind
}

if object["kind"] == "" {
strict, lax = nil, runtime.NewMissingKindErr(diagnose(data))
return
}
if object["apiVersion"] == "" {
strict, lax = nil, runtime.NewMissingVersionErr(diagnose(data))
return
}

unstructureds[i].Object = object
}
delete(content, "items")
u.Object = content
u.Items = unstructureds
default:
u.SetUnstructuredContent(content)
}
}()
into = &content
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,33 @@ func TestEncode(t *testing.T) {
}
},
},
{
name: "unstructuredlist",
in: &unstructured.UnstructuredList{
Object: map[string]interface{}{
"apiVersion": "v",
"kind": "kList",
},
Items: []unstructured.Unstructured{
{Object: map[string]interface{}{"foo": int64(1)}},
{Object: map[string]interface{}{"foo": int64(2)}},
},
},
assertOnWriter: func() (io.Writer, func(t *testing.T)) {
var b bytes.Buffer
return &b, func(t *testing.T) {
// {'kind': 'kList', 'items': [{'foo': 1}, {'foo': 2}], 'apiVersion': 'v'}
if diff := cmp.Diff(b.Bytes(), []byte("\xd9\xd9\xf7\xa3\x44kind\x45kList\x45items\x82\xa1\x43foo\x01\xa1\x43foo\x02\x4aapiVersion\x41v")); diff != "" {
t.Errorf("unexpected diff:\n%s", diff)
}
}
},
assertOnError: func(t *testing.T, err error) {
if err != nil {
t.Errorf("expected nil error, got: %v", err)
}
},
},
} {
t.Run(tc.name, func(t *testing.T) {
s := NewSerializer(nil, nil)
Expand Down Expand Up @@ -417,6 +444,126 @@ func TestDecode(t *testing.T) {
}
},
},
{
name: "into unstructuredlist missing kind",
data: []byte("\xa1\x6aapiVersion\x61v"),
into: &unstructured.UnstructuredList{},
expectedObj: nil,
expectedGVK: &schema.GroupVersionKind{Version: "v"},
assertOnError: func(t *testing.T, err error) {
if !runtime.IsMissingKind(err) {
t.Errorf("expected MissingKind, got: %v", err)
}
},
},
{
name: "into unstructuredlist missing version",
data: []byte("\xa1\x64kind\x65kList"),
into: &unstructured.UnstructuredList{},
expectedObj: nil,
expectedGVK: &schema.GroupVersionKind{Kind: "kList"},
assertOnError: func(t *testing.T, err error) {
if !runtime.IsMissingVersion(err) {
t.Errorf("expected MissingVersion, got: %v", err)
}
},
},
{
name: "into unstructuredlist empty",
data: []byte("\xa2\x6aapiVersion\x61v\x64kind\x65kList"),
into: &unstructured.UnstructuredList{},
expectedObj: &unstructured.UnstructuredList{Object: map[string]interface{}{
"apiVersion": "v",
"kind": "kList",
}},
expectedGVK: &schema.GroupVersionKind{Version: "v", Kind: "kList"},
assertOnError: func(t *testing.T, err error) {
if err != nil {
t.Errorf("expected nil error, got: %v", err)
}
},
},
{
name: "into unstructuredlist nonempty",
data: []byte("\xa3\x6aapiVersion\x61v\x64kind\x65kList\x65items\x82\xa1\x63foo\x01\xa1\x63foo\x02"), // {"apiVersion": "v", "kind": "kList", "items": [{"foo": 1}, {"foo": 2}]}
into: &unstructured.UnstructuredList{},
expectedObj: &unstructured.UnstructuredList{
Object: map[string]interface{}{
"apiVersion": "v",
"kind": "kList",
},
Items: []unstructured.Unstructured{
{Object: map[string]interface{}{"apiVersion": "v", "kind": "k", "foo": int64(1)}},
{Object: map[string]interface{}{"apiVersion": "v", "kind": "k", "foo": int64(2)}},
},
},
expectedGVK: &schema.GroupVersionKind{Version: "v", Kind: "kList"},
assertOnError: func(t *testing.T, err error) {
if err != nil {
t.Errorf("expected nil error, got: %v", err)
}
},
},
{
name: "into unstructuredlist item gvk present",
data: []byte("\xa3\x6aapiVersion\x61v\x64kind\x65kList\x65items\x81\xa2\x6aapiVersion\x62vv\x64kind\x62kk"), // {"apiVersion": "v", "kind": "kList", "items": [{"apiVersion": "vv", "kind": "kk"}]}
into: &unstructured.UnstructuredList{},
expectedObj: &unstructured.UnstructuredList{
Object: map[string]interface{}{
"apiVersion": "v",
"kind": "kList",
},
Items: []unstructured.Unstructured{
{Object: map[string]interface{}{"apiVersion": "vv", "kind": "kk"}},
},
},
expectedGVK: &schema.GroupVersionKind{Version: "v", Kind: "kList"},
assertOnError: func(t *testing.T, err error) {
if err != nil {
t.Errorf("expected nil error, got: %v", err)
}
},
},
{
name: "into unstructuredlist item missing kind",
data: []byte("\xa3\x6aapiVersion\x61v\x64kind\x65kList\x65items\x81\xa1\x6aapiVersion\x62vv"), // {"apiVersion": "v", "kind": "kList", "items": [{"apiVersion": "vv"}]}
metaFactory: &defaultMetaFactory{},
into: &unstructured.UnstructuredList{},
expectedGVK: &schema.GroupVersionKind{Version: "v", Kind: "kList"},
assertOnError: func(t *testing.T, err error) {
if !runtime.IsMissingKind(err) {
t.Errorf("expected MissingVersion, got: %v", err)
}
},
},
{
name: "into unstructuredlist item missing version",
data: []byte("\xa3\x6aapiVersion\x61v\x64kind\x65kList\x65items\x81\xa1\x64kind\x62kk"), // {"apiVersion": "v", "kind": "kList", "items": [{"kind": "kk"}]}
metaFactory: &defaultMetaFactory{},
into: &unstructured.UnstructuredList{},
expectedGVK: &schema.GroupVersionKind{Version: "v", Kind: "kList"},
assertOnError: func(t *testing.T, err error) {
if !runtime.IsMissingVersion(err) {
t.Errorf("expected MissingVersion, got: %v", err)
}
},
},
{
name: "using unstructuredlist creater",
data: []byte("\xa2\x6aapiVersion\x61v\x64kind\x65kList"),
metaFactory: &defaultMetaFactory{},
creater: stubCreater{obj: &unstructured.UnstructuredList{}},
expectedObj: &unstructured.UnstructuredList{Object: map[string]interface{}{
"apiVersion": "v",
"kind": "kList",
}},
expectedGVK: &schema.GroupVersionKind{Version: "v", Kind: "kList"},
assertOnError: func(t *testing.T, err error) {
if err != nil {
t.Errorf("expected nil error, got: %v", err)
}
},
},
} {
t.Run(tc.name, func(t *testing.T) {
s := newSerializer(tc.metaFactory, tc.creater, tc.typer, tc.options...)
Expand Down

0 comments on commit 7e6b866

Please sign in to comment.