mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-08-04 01:40:07 +00:00
Merge pull request #125678 from benluddy/cbor-nondeterministic-encode
KEP-4222: Support nondeterministic encode for the CBOR serializer.
This commit is contained in:
commit
a73a277715
@ -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))
|
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
|
// original->JSON/CBOR->Unstructured == original->JSON/CBOR->Unstructured->JSON->Unstructured
|
||||||
buf.Reset()
|
buf.Reset()
|
||||||
if err := jsonSerializer.Encode(uJSON, &buf); err != nil {
|
if err := jsonSerializer.Encode(uJSON, &buf); err != nil {
|
||||||
@ -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))
|
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
|
// original->JSON/CBOR->Unstructured->JSON->final == original
|
||||||
buf.Reset()
|
buf.Reset()
|
||||||
if err := jsonSerializer.Encode(uJSON, &buf); err != nil {
|
if err := jsonSerializer.Encode(uJSON, &buf); err != nil {
|
||||||
@ -187,6 +219,20 @@ func RoundtripToUnstructured(t *testing.T, scheme *runtime.Scheme, funcs fuzzer.
|
|||||||
if !apiequality.Semantic.DeepEqual(item, finalCBOR) {
|
if !apiequality.Semantic.DeepEqual(item, finalCBOR) {
|
||||||
t.Errorf("object changed during native-cbor-unstructured-cbor-native roundtrip, diff: %s", cmp.Diff(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))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -69,6 +69,19 @@ type Encoder interface {
|
|||||||
Identifier() Identifier
|
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.
|
// MemoryAllocator is responsible for allocating memory.
|
||||||
// By encapsulating memory allocation into its own interface, we can reuse the 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.
|
// across many operations in places we know it can significantly improve the performance.
|
||||||
|
@ -55,7 +55,14 @@ func (mf *defaultMetaFactory) Interpret(data []byte) (*schema.GroupVersionKind,
|
|||||||
|
|
||||||
type Serializer interface {
|
type Serializer interface {
|
||||||
runtime.Serializer
|
runtime.Serializer
|
||||||
|
runtime.NondeterministicEncoder
|
||||||
recognizer.RecognizingDecoder
|
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{}
|
var _ Serializer = &serializer{}
|
||||||
@ -79,6 +86,8 @@ type serializer struct {
|
|||||||
options options
|
options options
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (serializer) private() {}
|
||||||
|
|
||||||
func NewSerializer(creater runtime.ObjectCreater, typer runtime.ObjectTyper, options ...Option) Serializer {
|
func NewSerializer(creater runtime.ObjectCreater, typer runtime.ObjectTyper, options ...Option) Serializer {
|
||||||
return newSerializer(&defaultMetaFactory{}, creater, typer, options...)
|
return newSerializer(&defaultMetaFactory{}, creater, typer, options...)
|
||||||
}
|
}
|
||||||
@ -117,6 +126,10 @@ func (s *serializer) Encode(obj runtime.Object, w io.Writer) error {
|
|||||||
return s.encode(modes.Encode, obj, w)
|
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 {
|
func (s *serializer) encode(mode modes.EncMode, obj runtime.Object, w io.Writer) error {
|
||||||
var v interface{} = obj
|
var v interface{} = obj
|
||||||
if u, ok := obj.(runtime.Unstructured); ok {
|
if u, ok := obj.(runtime.Unstructured); ok {
|
||||||
|
@ -26,6 +26,7 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"io"
|
"io"
|
||||||
"reflect"
|
"reflect"
|
||||||
|
"strconv"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
@ -762,3 +763,98 @@ type stubMetaFactory struct {
|
|||||||
func (mf stubMetaFactory) Interpret([]byte) (*schema.GroupVersionKind, error) {
|
func (mf stubMetaFactory) Interpret([]byte) (*schema.GroupVersionKind, error) {
|
||||||
return mf.gvk, mf.err
|
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)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
@ -105,7 +105,7 @@ var Encode = EncMode{
|
|||||||
var EncodeNondeterministic = EncMode{
|
var EncodeNondeterministic = EncMode{
|
||||||
delegate: func() cbor.UserBufferEncMode {
|
delegate: func() cbor.UserBufferEncMode {
|
||||||
opts := Encode.options()
|
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()
|
em, err := opts.UserBufferEncMode()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
|
Loading…
Reference in New Issue
Block a user