Skip to content

Commit de914d6

Browse files
committed
Support nondeterministic encode for the CBOR serializer.
1 parent e9cde03 commit de914d6

File tree

5 files changed

+169
-1
lines changed

5 files changed

+169
-1
lines changed

staging/src/k8s.io/apimachinery/pkg/api/apitesting/roundtrip/unstructured.go

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,23 @@ func RoundtripToUnstructured(t *testing.T, scheme *runtime.Scheme, funcs fuzzer.
132132
t.Fatalf("unstructured via json differed from unstructured via cbor: %v", cmp.Diff(uJSON, uCBOR))
133133
}
134134

135+
// original->CBOR(nondeterministic)->Unstructured
136+
buf.Reset()
137+
if err := cborSerializer.EncodeNondeterministic(item, &buf); err != nil {
138+
t.Fatalf("error encoding native to cbor: %v", err)
139+
}
140+
var uCBORNondeterministic runtime.Object = &unstructured.Unstructured{}
141+
uCBORNondeterministic, _, err = cborSerializer.Decode(buf.Bytes(), &gvk, uCBORNondeterministic)
142+
if err != nil {
143+
diag, _ := cbor.Diagnose(buf.Bytes())
144+
t.Fatalf("error decoding cbor to unstructured: %v, diag: %s", err, diag)
145+
}
146+
147+
// original->CBOR->Unstructured == original->CBOR(nondeterministic)->Unstructured
148+
if !apiequality.Semantic.DeepEqual(uCBOR, uCBORNondeterministic) {
149+
t.Fatalf("unstructured via nondeterministic cbor differed from unstructured via cbor: %v", cmp.Diff(uCBOR, uCBORNondeterministic))
150+
}
151+
135152
// original->JSON/CBOR->Unstructured == original->JSON/CBOR->Unstructured->JSON->Unstructured
136153
buf.Reset()
137154
if err := jsonSerializer.Encode(uJSON, &buf); err != nil {
@@ -161,6 +178,21 @@ func RoundtripToUnstructured(t *testing.T, scheme *runtime.Scheme, funcs fuzzer.
161178
t.Errorf("object changed during native-cbor-unstructured-cbor-unstructured roundtrip, diff: %s", cmp.Diff(uCBOR, uCBOR2))
162179
}
163180

181+
// original->JSON/CBOR->Unstructured->CBOR->Unstructured == original->JSON/CBOR->Unstructured->CBOR(nondeterministic)->Unstructured
182+
buf.Reset()
183+
if err := cborSerializer.EncodeNondeterministic(uCBOR, &buf); err != nil {
184+
t.Fatalf("error encoding unstructured to cbor: %v", err)
185+
}
186+
var uCBOR2Nondeterministic runtime.Object = &unstructured.Unstructured{}
187+
uCBOR2Nondeterministic, _, err = cborSerializer.Decode(buf.Bytes(), &gvk, uCBOR2Nondeterministic)
188+
if err != nil {
189+
diag, _ := cbor.Diagnose(buf.Bytes())
190+
t.Fatalf("error decoding cbor to unstructured: %v, diag: %s", err, diag)
191+
}
192+
if !apiequality.Semantic.DeepEqual(uCBOR, uCBOR2Nondeterministic) {
193+
t.Errorf("object changed during native-cbor-unstructured-cbor(nondeterministic)-unstructured roundtrip, diff: %s", cmp.Diff(uCBOR, uCBOR2Nondeterministic))
194+
}
195+
164196
// original->JSON/CBOR->Unstructured->JSON->final == original
165197
buf.Reset()
166198
if err := jsonSerializer.Encode(uJSON, &buf); err != nil {
@@ -187,6 +219,20 @@ func RoundtripToUnstructured(t *testing.T, scheme *runtime.Scheme, funcs fuzzer.
187219
if !apiequality.Semantic.DeepEqual(item, finalCBOR) {
188220
t.Errorf("object changed during native-cbor-unstructured-cbor-native roundtrip, diff: %s", cmp.Diff(item, finalCBOR))
189221
}
222+
223+
// original->JSON/CBOR->Unstructured->CBOR(nondeterministic)->final == original
224+
buf.Reset()
225+
if err := cborSerializer.EncodeNondeterministic(uCBOR, &buf); err != nil {
226+
t.Fatalf("error encoding unstructured to cbor: %v", err)
227+
}
228+
finalCBORNondeterministic, _, err := cborSerializer.Decode(buf.Bytes(), &gvk, nil)
229+
if err != nil {
230+
diag, _ := cbor.Diagnose(buf.Bytes())
231+
t.Fatalf("error decoding cbor to native: %v, diag: %s", err, diag)
232+
}
233+
if !apiequality.Semantic.DeepEqual(item, finalCBORNondeterministic) {
234+
t.Errorf("object changed during native-cbor-unstructured-cbor-native roundtrip, diff: %s", cmp.Diff(item, finalCBORNondeterministic))
235+
}
190236
}
191237
})
192238
}

staging/src/k8s.io/apimachinery/pkg/runtime/interfaces.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,19 @@ type Encoder interface {
6969
Identifier() Identifier
7070
}
7171

72+
// NondeterministicEncoder is implemented by Encoders that can serialize objects more efficiently in
73+
// cases where the output does not need to be deterministic.
74+
type NondeterministicEncoder interface {
75+
Encoder
76+
77+
// EncodeNondeterministic writes an object to the stream. Unlike the Encode method of
78+
// Encoder, EncodeNondeterministic does not guarantee that any two invocations will write
79+
// the same sequence of bytes to the io.Writer. Any differences will not be significant to a
80+
// generic decoder. For example, map entries and struct fields might be encoded in any
81+
// order.
82+
EncodeNondeterministic(Object, io.Writer) error
83+
}
84+
7285
// MemoryAllocator is responsible for allocating memory.
7386
// By encapsulating memory allocation into its own interface, we can reuse the memory
7487
// across many operations in places we know it can significantly improve the performance.

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

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,14 @@ func (mf *defaultMetaFactory) Interpret(data []byte) (*schema.GroupVersionKind,
5555

5656
type Serializer interface {
5757
runtime.Serializer
58+
runtime.NondeterministicEncoder
5859
recognizer.RecognizingDecoder
60+
61+
// NewSerializer returns a value of this interface type rather than exporting the serializer
62+
// type and returning one of those because the zero value of serializer isn't ready to
63+
// use. Users aren't intended to implement cbor.Serializer themselves, and this unexported
64+
// interface method is here to prevent that (https://go.dev/blog/module-compatibility).
65+
private()
5966
}
6067

6168
var _ Serializer = &serializer{}
@@ -79,6 +86,8 @@ type serializer struct {
7986
options options
8087
}
8188

89+
func (serializer) private() {}
90+
8291
func NewSerializer(creater runtime.ObjectCreater, typer runtime.ObjectTyper, options ...Option) Serializer {
8392
return newSerializer(&defaultMetaFactory{}, creater, typer, options...)
8493
}
@@ -117,6 +126,10 @@ func (s *serializer) Encode(obj runtime.Object, w io.Writer) error {
117126
return s.encode(modes.Encode, obj, w)
118127
}
119128

129+
func (s *serializer) EncodeNondeterministic(obj runtime.Object, w io.Writer) error {
130+
return s.encode(modes.EncodeNondeterministic, obj, w)
131+
}
132+
120133
func (s *serializer) encode(mode modes.EncMode, obj runtime.Object, w io.Writer) error {
121134
var v interface{} = obj
122135
if u, ok := obj.(runtime.Unstructured); ok {

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

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import (
2626
"errors"
2727
"io"
2828
"reflect"
29+
"strconv"
2930
"testing"
3031

3132
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -762,3 +763,98 @@ type stubMetaFactory struct {
762763
func (mf stubMetaFactory) Interpret([]byte) (*schema.GroupVersionKind, error) {
763764
return mf.gvk, mf.err
764765
}
766+
767+
type oneMapField struct {
768+
metav1.TypeMeta `json:",inline"`
769+
Map map[string]interface{} `json:"map"`
770+
}
771+
772+
func (o oneMapField) DeepCopyObject() runtime.Object {
773+
panic("unimplemented")
774+
}
775+
776+
func (o oneMapField) GetObjectKind() schema.ObjectKind {
777+
panic("unimplemented")
778+
}
779+
780+
type eightStringFields struct {
781+
metav1.TypeMeta `json:",inline"`
782+
A string `json:"1"`
783+
B string `json:"2"`
784+
C string `json:"3"`
785+
D string `json:"4"`
786+
E string `json:"5"`
787+
F string `json:"6"`
788+
G string `json:"7"`
789+
H string `json:"8"`
790+
}
791+
792+
func (o eightStringFields) DeepCopyObject() runtime.Object {
793+
panic("unimplemented")
794+
}
795+
796+
func (o eightStringFields) GetObjectKind() schema.ObjectKind {
797+
panic("unimplemented")
798+
}
799+
800+
// TestEncodeNondeterministic tests that repeated encodings of multi-field structs and maps do not
801+
// encode to precisely the same bytes when repeatedly encoded with EncodeNondeterministic. When
802+
// using EncodeNondeterministic, the order of items in CBOR maps should be intentionally shuffled to
803+
// prevent applications from inadvertently depending on encoding determinism. All permutations do
804+
// not necessarily have equal probability.
805+
func TestEncodeNondeterministic(t *testing.T) {
806+
for _, tc := range []struct {
807+
name string
808+
input runtime.Object
809+
}{
810+
{
811+
name: "map",
812+
input: func() runtime.Object {
813+
m := map[string]interface{}{}
814+
for i := 1; i <= 8; i++ {
815+
m[strconv.Itoa(i)] = strconv.Itoa(i)
816+
817+
}
818+
return oneMapField{Map: m}
819+
}(),
820+
},
821+
{
822+
name: "struct",
823+
input: eightStringFields{
824+
TypeMeta: metav1.TypeMeta{},
825+
A: "1",
826+
B: "2",
827+
C: "3",
828+
D: "4",
829+
E: "5",
830+
F: "6",
831+
G: "7",
832+
H: "8",
833+
},
834+
},
835+
} {
836+
t.Run(tc.name, func(t *testing.T) {
837+
var b bytes.Buffer
838+
e := NewSerializer(nil, nil)
839+
840+
if err := e.EncodeNondeterministic(tc.input, &b); err != nil {
841+
t.Fatal(err)
842+
}
843+
first := b.String()
844+
845+
const Trials = 128
846+
for trial := 0; trial < Trials; trial++ {
847+
b.Reset()
848+
if err := e.EncodeNondeterministic(tc.input, &b); err != nil {
849+
t.Fatal(err)
850+
}
851+
852+
if !bytes.Equal([]byte(first), b.Bytes()) {
853+
return
854+
}
855+
}
856+
t.Fatalf("nondeterministic encode produced the same bytes on %d consecutive calls: %s", Trials, first)
857+
})
858+
}
859+
860+
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ var Encode = EncMode{
105105
var EncodeNondeterministic = EncMode{
106106
delegate: func() cbor.UserBufferEncMode {
107107
opts := Encode.options()
108-
opts.Sort = cbor.SortNone // TODO: Use cbor.SortFastShuffle after bump to v2.7.0.
108+
opts.Sort = cbor.SortFastShuffle
109109
em, err := opts.UserBufferEncMode()
110110
if err != nil {
111111
panic(err)

0 commit comments

Comments
 (0)