Skip to content

Commit

Permalink
Merge pull request #125790 from benluddy/cbor-fieldsv1
Browse files Browse the repository at this point in the history
KEP-4222: Support either JSON or CBOR in FieldsV1.

Kubernetes-commit: ccbbbc0f1f353e7dec5ce3dd83cccc0b7603d40a
  • Loading branch information
k8s-publishing-bot committed Jul 11, 2024
2 parents 6b362fa + 3da83fe commit 4524748
Show file tree
Hide file tree
Showing 5 changed files with 447 additions and 9 deletions.
83 changes: 82 additions & 1 deletion pkg/apis/meta/v1/helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,10 @@ import (

"k8s.io/apimachinery/pkg/fields"
"k8s.io/apimachinery/pkg/labels"
cbor "k8s.io/apimachinery/pkg/runtime/serializer/cbor/direct"
"k8s.io/apimachinery/pkg/selection"
"k8s.io/apimachinery/pkg/types"
utiljson "k8s.io/apimachinery/pkg/util/json"
)

// LabelSelectorAsSelector converts the LabelSelector api type into a struct that implements
Expand Down Expand Up @@ -280,13 +282,20 @@ func (f FieldsV1) MarshalJSON() ([]byte, error) {
if f.Raw == nil {
return []byte("null"), nil
}
if f.getContentType() == fieldsV1InvalidOrValidCBORObject {
var u map[string]interface{}
if err := cbor.Unmarshal(f.Raw, &u); err != nil {
return nil, fmt.Errorf("metav1.FieldsV1 cbor invalid: %w", err)
}
return utiljson.Marshal(u)
}
return f.Raw, nil
}

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

var _ json.Marshaler = FieldsV1{}
var _ json.Unmarshaler = &FieldsV1{}

func (f FieldsV1) MarshalCBOR() ([]byte, error) {
if f.Raw == nil {
return cbor.Marshal(nil)
}
if f.getContentType() == fieldsV1InvalidOrValidJSONObject {
var u map[string]interface{}
if err := utiljson.Unmarshal(f.Raw, &u); err != nil {
return nil, fmt.Errorf("metav1.FieldsV1 json invalid: %w", err)
}
return cbor.Marshal(u)
}
return f.Raw, nil
}

var cborNull = []byte{0xf6}

func (f *FieldsV1) UnmarshalCBOR(b []byte) error {
if f == nil {
return errors.New("metav1.FieldsV1: UnmarshalCBOR on nil pointer")
}
if !bytes.Equal(b, cborNull) {
f.Raw = append(f.Raw[0:0], b...)
}
return nil
}

const (
// fieldsV1InvalidOrEmpty indicates that a FieldsV1 either contains no raw bytes or its raw
// bytes don't represent an allowable value in any supported encoding.
fieldsV1InvalidOrEmpty = iota

// fieldsV1InvalidOrValidJSONObject indicates that a FieldV1 either contains raw bytes that
// are a valid JSON encoding of an allowable value or don't represent an allowable value in
// any supported encoding.
fieldsV1InvalidOrValidJSONObject

// fieldsV1InvalidOrValidCBORObject indicates that a FieldV1 either contains raw bytes that
// are a valid CBOR encoding of an allowable value or don't represent an allowable value in
// any supported encoding.
fieldsV1InvalidOrValidCBORObject
)

// getContentType returns one of fieldsV1InvalidOrEmpty, fieldsV1InvalidOrValidJSONObject,
// fieldsV1InvalidOrValidCBORObject based on the value of Raw.
//
// Raw can be encoded in JSON or CBOR and is only valid if it is empty, null, or an object (map)
// value. It is invalid if it contains a JSON string, number, boolean, or array. If Raw is nonempty
// and represents an allowable value, then the initial byte unambiguously distinguishes a
// JSON-encoded value from a CBOR-encoded value.
//
// A valid JSON-encoded value can begin with any of the four JSON whitespace characters, the first
// character 'n' of null, or '{' (0x09, 0x0a, 0x0d, 0x20, 0x6e, or 0x7b, respectively). A valid
// CBOR-encoded value can begin with the null simple value, an initial byte with major type "map",
// or, if a tag-enclosed map, an initial byte with major type "tag" (0xf6, 0xa0...0xbf, or
// 0xc6...0xdb). The two sets of valid initial bytes don't intersect.
func (f FieldsV1) getContentType() int {
if len(f.Raw) > 0 {
p := f.Raw[0]
switch p {
case 'n', '{', '\t', '\r', '\n', ' ':
return fieldsV1InvalidOrValidJSONObject
case 0xf6: // null
return fieldsV1InvalidOrValidCBORObject
default:
if p >= 0xa0 && p <= 0xbf /* map */ || p >= 0xc6 && p <= 0xdb /* tag */ {
return fieldsV1InvalidOrValidCBORObject
}
}
}
return fieldsV1InvalidOrEmpty
}
242 changes: 242 additions & 0 deletions pkg/apis/meta/v1/helpers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -249,3 +249,245 @@ func TestSetMetaDataLabel(t *testing.T) {
}
}
}

func TestFieldsV1MarshalJSON(t *testing.T) {
for _, tc := range []struct {
Name string
FieldsV1 FieldsV1
Want []byte
Error string
}{
{
Name: "nil encodes as json null",
FieldsV1: FieldsV1{},
Want: []byte(`null`),
},
{
Name: "empty invalid json is returned as-is",
FieldsV1: FieldsV1{Raw: []byte{}},
Want: []byte{},
},
{
Name: "cbor null is transcoded to json null",
FieldsV1: FieldsV1{Raw: []byte{0xf6}}, // null
Want: []byte(`null`),
},
{
Name: "valid non-map cbor and valid non-object json is returned as-is",
FieldsV1: FieldsV1{Raw: []byte{0x30}},
Want: []byte{0x30}, // Valid CBOR encoding of -17 and JSON encoding of 0!
},
{
Name: "self-described cbor map is transcoded to json map",
FieldsV1: FieldsV1{Raw: []byte{0xd9, 0xd9, 0xf7, 0xa1, 0x43, 'f', 'o', 'o', 0x43, 'b', 'a', 'r'}}, // 55799({"foo":"bar"})
Want: []byte(`{"foo":"bar"}`),
},
{
Name: "json object is returned as-is",
FieldsV1: FieldsV1{Raw: []byte(" \t\r\n{\"foo\":\"bar\"}")},
Want: []byte(" \t\r\n{\"foo\":\"bar\"}"),
},
{
Name: "invalid json is returned as-is",
FieldsV1: FieldsV1{Raw: []byte(`{{`)},
Want: []byte(`{{`),
},
{
Name: "invalid cbor fails to transcode to json",
FieldsV1: FieldsV1{Raw: []byte{0xa1}},
Error: "metav1.FieldsV1 cbor invalid: unexpected EOF",
},
} {
t.Run(tc.Name, func(t *testing.T) {
got, err := tc.FieldsV1.MarshalJSON()
if err != nil {
if tc.Error == "" {
t.Fatalf("unexpected error: %v", err)
}
if msg := err.Error(); msg != tc.Error {
t.Fatalf("expected error %q, got %q", tc.Error, msg)
}
} else if tc.Error != "" {
t.Fatalf("expected error %q, got nil", tc.Error)
}
if diff := cmp.Diff(tc.Want, got); diff != "" {
t.Errorf("unexpected diff:\n%s", diff)
}
})
}
}

func TestFieldsV1MarshalCBOR(t *testing.T) {
for _, tc := range []struct {
Name string
FieldsV1 FieldsV1
Want []byte
Error string
}{
{
Name: "nil encodes as cbor null",
FieldsV1: FieldsV1{},
Want: []byte{0xf6}, // null
},
{
Name: "empty invalid cbor is returned as-is",
FieldsV1: FieldsV1{Raw: []byte{}},
Want: []byte{},
},
{
Name: "json null is transcoded to cbor null",
FieldsV1: FieldsV1{Raw: []byte(`null`)},
Want: []byte{0xf6}, // null
},
{
Name: "valid non-map cbor and valid non-object json is returned as-is",
FieldsV1: FieldsV1{Raw: []byte{0x30}},
Want: []byte{0x30}, // Valid CBOR encoding of -17 and JSON encoding of 0!
},
{
Name: "json object is transcoded to cbor map",
FieldsV1: FieldsV1{Raw: []byte(" \t\r\n{\"foo\":\"bar\"}")},
Want: []byte{0xa1, 0x43, 'f', 'o', 'o', 0x43, 'b', 'a', 'r'},
},
{
Name: "self-described cbor map is returned as-is",
FieldsV1: FieldsV1{Raw: []byte{0xd9, 0xd9, 0xf7, 0xa1, 0x43, 'f', 'o', 'o', 0x43, 'b', 'a', 'r'}}, // 55799({"foo":"bar"})
Want: []byte{0xd9, 0xd9, 0xf7, 0xa1, 0x43, 'f', 'o', 'o', 0x43, 'b', 'a', 'r'}, // 55799({"foo":"bar"})
},
{
Name: "invalid json fails to transcode to cbor",
FieldsV1: FieldsV1{Raw: []byte(`{{`)},
Error: "metav1.FieldsV1 json invalid: invalid character '{' looking for beginning of object key string",
},
{
Name: "invalid cbor is returned as-is",
FieldsV1: FieldsV1{Raw: []byte{0xa1}},
Want: []byte{0xa1},
},
} {
t.Run(tc.Name, func(t *testing.T) {
got, err := tc.FieldsV1.MarshalCBOR()
if err != nil {
if tc.Error == "" {
t.Fatalf("unexpected error: %v", err)
}
if msg := err.Error(); msg != tc.Error {
t.Fatalf("expected error %q, got %q", tc.Error, msg)
}
} else if tc.Error != "" {
t.Fatalf("expected error %q, got nil", tc.Error)
}

if diff := cmp.Diff(tc.Want, got); diff != "" {
t.Errorf("unexpected diff:\n%s", diff)
}
})
}
}

func TestFieldsV1UnmarshalJSON(t *testing.T) {
for _, tc := range []struct {
Name string
JSON []byte
Into *FieldsV1
Want *FieldsV1
Error string
}{
{
Name: "nil receiver returns error",
Into: nil,
Error: "metav1.FieldsV1: UnmarshalJSON on nil pointer",
},
{
Name: "json null does not modify receiver", // conventional for json.Unmarshaler
JSON: []byte(`null`),
Into: &FieldsV1{Raw: []byte(`unmodified`)},
Want: &FieldsV1{Raw: []byte(`unmodified`)},
},
{
Name: "valid input is copied verbatim",
JSON: []byte("{\"foo\":\"bar\"} \t\r\n"),
Into: &FieldsV1{},
Want: &FieldsV1{Raw: []byte("{\"foo\":\"bar\"} \t\r\n")},
},
{
Name: "invalid input is copied verbatim",
JSON: []byte("{{"),
Into: &FieldsV1{},
Want: &FieldsV1{Raw: []byte("{{")},
},
} {
t.Run(tc.Name, func(t *testing.T) {
got := tc.Into.DeepCopy()
err := got.UnmarshalJSON(tc.JSON)
if err != nil {
if tc.Error == "" {
t.Fatalf("unexpected error: %v", err)
}
if msg := err.Error(); msg != tc.Error {
t.Fatalf("expected error %q, got %q", tc.Error, msg)
}
} else if tc.Error != "" {
t.Fatalf("expected error %q, got nil", tc.Error)
}

if diff := cmp.Diff(tc.Want, got); diff != "" {
t.Errorf("unexpected diff:\n%s", diff)
}
})
}
}

func TestFieldsV1UnmarshalCBOR(t *testing.T) {
for _, tc := range []struct {
Name string
CBOR []byte
Into *FieldsV1
Want *FieldsV1
Error string
}{
{
Name: "nil receiver returns error",
Into: nil,
Want: nil,
Error: "metav1.FieldsV1: UnmarshalCBOR on nil pointer",
},
{
Name: "cbor null does not modify receiver",
CBOR: []byte{0xf6},
Into: &FieldsV1{Raw: []byte(`unmodified`)},
Want: &FieldsV1{Raw: []byte(`unmodified`)},
},
{
Name: "valid input is copied verbatim",
CBOR: []byte{0xa1, 0x43, 'f', 'o', 'o', 0x43, 'b', 'a', 'r'},
Into: &FieldsV1{},
Want: &FieldsV1{Raw: []byte{0xa1, 0x43, 'f', 'o', 'o', 0x43, 'b', 'a', 'r'}},
},
{
Name: "invalid input is copied verbatim",
CBOR: []byte{0xff}, // UnmarshalCBOR should never be called with malformed input, testing anyway.
Into: &FieldsV1{},
Want: &FieldsV1{Raw: []byte{0xff}},
},
} {
t.Run(tc.Name, func(t *testing.T) {
got := tc.Into.DeepCopy()
err := got.UnmarshalCBOR(tc.CBOR)
if err != nil {
if tc.Error == "" {
t.Fatalf("unexpected error: %v", err)
}
if msg := err.Error(); msg != tc.Error {
t.Fatalf("expected error %q, got %q", tc.Error, msg)
}
} else if tc.Error != "" {
t.Fatalf("expected error %q, got nil", tc.Error)
}

if diff := cmp.Diff(tc.Want, got); diff != "" {
t.Errorf("unexpected diff:\n%s", diff)
}
})
}
}
Loading

0 comments on commit 4524748

Please sign in to comment.