Skip to content

Commit

Permalink
Merge pull request #125678 from benluddy/cbor-nondeterministic-encode
Browse files Browse the repository at this point in the history
KEP-4222:  Support nondeterministic encode for the CBOR serializer.

Kubernetes-commit: a73a27771566ce19d20854f3e963628eddb066d3
  • Loading branch information
k8s-publishing-bot committed Sep 26, 2024
2 parents 7f7bf11 + 2a1df23 commit dc03077
Show file tree
Hide file tree
Showing 5 changed files with 169 additions and 1 deletion.
46 changes: 46 additions & 0 deletions pkg/api/apitesting/roundtrip/unstructured.go
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,23 @@ func RoundtripToUnstructured(t *testing.T, scheme *runtime.Scheme, funcs fuzzer.
t.Fatalf("unstructured via json differed from unstructured via cbor: %v", cmp.Diff(uJSON, uCBOR))
}

// original->CBOR(nondeterministic)->Unstructured
buf.Reset()
if err := cborSerializer.EncodeNondeterministic(item, &buf); err != nil {
t.Fatalf("error encoding native to cbor: %v", err)
}
var uCBORNondeterministic runtime.Object = &unstructured.Unstructured{}
uCBORNondeterministic, _, err = cborSerializer.Decode(buf.Bytes(), &gvk, uCBORNondeterministic)
if err != nil {
diag, _ := cbor.Diagnose(buf.Bytes())
t.Fatalf("error decoding cbor to unstructured: %v, diag: %s", err, diag)
}

// original->CBOR->Unstructured == original->CBOR(nondeterministic)->Unstructured
if !apiequality.Semantic.DeepEqual(uCBOR, uCBORNondeterministic) {
t.Fatalf("unstructured via nondeterministic cbor differed from unstructured via cbor: %v", cmp.Diff(uCBOR, uCBORNondeterministic))
}

// original->JSON/CBOR->Unstructured == original->JSON/CBOR->Unstructured->JSON->Unstructured
buf.Reset()
if err := jsonSerializer.Encode(uJSON, &buf); err != nil {
Expand Down Expand Up @@ -161,6 +178,21 @@ func RoundtripToUnstructured(t *testing.T, scheme *runtime.Scheme, funcs fuzzer.
t.Errorf("object changed during native-cbor-unstructured-cbor-unstructured roundtrip, diff: %s", cmp.Diff(uCBOR, uCBOR2))
}

// original->JSON/CBOR->Unstructured->CBOR->Unstructured == original->JSON/CBOR->Unstructured->CBOR(nondeterministic)->Unstructured
buf.Reset()
if err := cborSerializer.EncodeNondeterministic(uCBOR, &buf); err != nil {
t.Fatalf("error encoding unstructured to cbor: %v", err)
}
var uCBOR2Nondeterministic runtime.Object = &unstructured.Unstructured{}
uCBOR2Nondeterministic, _, err = cborSerializer.Decode(buf.Bytes(), &gvk, uCBOR2Nondeterministic)
if err != nil {
diag, _ := cbor.Diagnose(buf.Bytes())
t.Fatalf("error decoding cbor to unstructured: %v, diag: %s", err, diag)
}
if !apiequality.Semantic.DeepEqual(uCBOR, uCBOR2Nondeterministic) {
t.Errorf("object changed during native-cbor-unstructured-cbor(nondeterministic)-unstructured roundtrip, diff: %s", cmp.Diff(uCBOR, uCBOR2Nondeterministic))
}

// original->JSON/CBOR->Unstructured->JSON->final == original
buf.Reset()
if err := jsonSerializer.Encode(uJSON, &buf); err != nil {
Expand All @@ -187,6 +219,20 @@ func RoundtripToUnstructured(t *testing.T, scheme *runtime.Scheme, funcs fuzzer.
if !apiequality.Semantic.DeepEqual(item, finalCBOR) {
t.Errorf("object changed during native-cbor-unstructured-cbor-native roundtrip, diff: %s", cmp.Diff(item, finalCBOR))
}

// original->JSON/CBOR->Unstructured->CBOR(nondeterministic)->final == original
buf.Reset()
if err := cborSerializer.EncodeNondeterministic(uCBOR, &buf); err != nil {
t.Fatalf("error encoding unstructured to cbor: %v", err)
}
finalCBORNondeterministic, _, err := cborSerializer.Decode(buf.Bytes(), &gvk, nil)
if err != nil {
diag, _ := cbor.Diagnose(buf.Bytes())
t.Fatalf("error decoding cbor to native: %v, diag: %s", err, diag)
}
if !apiequality.Semantic.DeepEqual(item, finalCBORNondeterministic) {
t.Errorf("object changed during native-cbor-unstructured-cbor-native roundtrip, diff: %s", cmp.Diff(item, finalCBORNondeterministic))
}
}
})
}
Expand Down
13 changes: 13 additions & 0 deletions pkg/runtime/interfaces.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,19 @@ type Encoder interface {
Identifier() Identifier
}

// NondeterministicEncoder is implemented by Encoders that can serialize objects more efficiently in
// cases where the output does not need to be deterministic.
type NondeterministicEncoder interface {
Encoder

// EncodeNondeterministic writes an object to the stream. Unlike the Encode method of
// Encoder, EncodeNondeterministic does not guarantee that any two invocations will write
// the same sequence of bytes to the io.Writer. Any differences will not be significant to a
// generic decoder. For example, map entries and struct fields might be encoded in any
// order.
EncodeNondeterministic(Object, io.Writer) error
}

// MemoryAllocator is responsible for allocating memory.
// By encapsulating memory allocation into its own interface, we can reuse the memory
// across many operations in places we know it can significantly improve the performance.
Expand Down
13 changes: 13 additions & 0 deletions pkg/runtime/serializer/cbor/cbor.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,14 @@ func (mf *defaultMetaFactory) Interpret(data []byte) (*schema.GroupVersionKind,

type Serializer interface {
runtime.Serializer
runtime.NondeterministicEncoder
recognizer.RecognizingDecoder

// NewSerializer returns a value of this interface type rather than exporting the serializer
// type and returning one of those because the zero value of serializer isn't ready to
// use. Users aren't intended to implement cbor.Serializer themselves, and this unexported
// interface method is here to prevent that (https://go.dev/blog/module-compatibility).
private()
}

var _ Serializer = &serializer{}
Expand All @@ -79,6 +86,8 @@ type serializer struct {
options options
}

func (serializer) private() {}

func NewSerializer(creater runtime.ObjectCreater, typer runtime.ObjectTyper, options ...Option) Serializer {
return newSerializer(&defaultMetaFactory{}, creater, typer, options...)
}
Expand Down Expand Up @@ -117,6 +126,10 @@ func (s *serializer) Encode(obj runtime.Object, w io.Writer) error {
return s.encode(modes.Encode, obj, w)
}

func (s *serializer) EncodeNondeterministic(obj runtime.Object, w io.Writer) error {
return s.encode(modes.EncodeNondeterministic, obj, w)
}

func (s *serializer) encode(mode modes.EncMode, obj runtime.Object, w io.Writer) error {
var v interface{} = obj
if u, ok := obj.(runtime.Unstructured); ok {
Expand Down
96 changes: 96 additions & 0 deletions pkg/runtime/serializer/cbor/cbor_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import (
"errors"
"io"
"reflect"
"strconv"
"testing"

metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
Expand Down Expand Up @@ -762,3 +763,98 @@ type stubMetaFactory struct {
func (mf stubMetaFactory) Interpret([]byte) (*schema.GroupVersionKind, error) {
return mf.gvk, mf.err
}

type oneMapField struct {
metav1.TypeMeta `json:",inline"`
Map map[string]interface{} `json:"map"`
}

func (o oneMapField) DeepCopyObject() runtime.Object {
panic("unimplemented")
}

func (o oneMapField) GetObjectKind() schema.ObjectKind {
panic("unimplemented")
}

type eightStringFields struct {
metav1.TypeMeta `json:",inline"`
A string `json:"1"`
B string `json:"2"`
C string `json:"3"`
D string `json:"4"`
E string `json:"5"`
F string `json:"6"`
G string `json:"7"`
H string `json:"8"`
}

func (o eightStringFields) DeepCopyObject() runtime.Object {
panic("unimplemented")
}

func (o eightStringFields) GetObjectKind() schema.ObjectKind {
panic("unimplemented")
}

// TestEncodeNondeterministic tests that repeated encodings of multi-field structs and maps do not
// encode to precisely the same bytes when repeatedly encoded with EncodeNondeterministic. When
// using EncodeNondeterministic, the order of items in CBOR maps should be intentionally shuffled to
// prevent applications from inadvertently depending on encoding determinism. All permutations do
// not necessarily have equal probability.
func TestEncodeNondeterministic(t *testing.T) {
for _, tc := range []struct {
name string
input runtime.Object
}{
{
name: "map",
input: func() runtime.Object {
m := map[string]interface{}{}
for i := 1; i <= 8; i++ {
m[strconv.Itoa(i)] = strconv.Itoa(i)

}
return oneMapField{Map: m}
}(),
},
{
name: "struct",
input: eightStringFields{
TypeMeta: metav1.TypeMeta{},
A: "1",
B: "2",
C: "3",
D: "4",
E: "5",
F: "6",
G: "7",
H: "8",
},
},
} {
t.Run(tc.name, func(t *testing.T) {
var b bytes.Buffer
e := NewSerializer(nil, nil)

if err := e.EncodeNondeterministic(tc.input, &b); err != nil {
t.Fatal(err)
}
first := b.String()

const Trials = 128
for trial := 0; trial < Trials; trial++ {
b.Reset()
if err := e.EncodeNondeterministic(tc.input, &b); err != nil {
t.Fatal(err)
}

if !bytes.Equal([]byte(first), b.Bytes()) {
return
}
}
t.Fatalf("nondeterministic encode produced the same bytes on %d consecutive calls: %s", Trials, first)
})
}

}
2 changes: 1 addition & 1 deletion pkg/runtime/serializer/cbor/internal/modes/encode.go
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ var Encode = EncMode{
var EncodeNondeterministic = EncMode{
delegate: func() cbor.UserBufferEncMode {
opts := Encode.options()
opts.Sort = cbor.SortNone // TODO: Use cbor.SortFastShuffle after bump to v2.7.0.
opts.Sort = cbor.SortFastShuffle
em, err := opts.UserBufferEncMode()
if err != nil {
panic(err)
Expand Down

0 comments on commit dc03077

Please sign in to comment.