1
16
17 package roundtrip
18
19 import (
20 "bytes"
21 "encoding/hex"
22 "math/rand"
23 "reflect"
24 "strings"
25 "testing"
26
27
28 "github.com/golang/protobuf/proto"
29 "github.com/google/go-cmp/cmp"
30 fuzz "github.com/google/gofuzz"
31 flag "github.com/spf13/pflag"
32
33 apitesting "k8s.io/apimachinery/pkg/api/apitesting"
34 "k8s.io/apimachinery/pkg/api/apitesting/fuzzer"
35 apiequality "k8s.io/apimachinery/pkg/api/equality"
36 apimeta "k8s.io/apimachinery/pkg/api/meta"
37 metafuzzer "k8s.io/apimachinery/pkg/apis/meta/fuzzer"
38 "k8s.io/apimachinery/pkg/runtime"
39 "k8s.io/apimachinery/pkg/runtime/schema"
40 runtimeserializer "k8s.io/apimachinery/pkg/runtime/serializer"
41 "k8s.io/apimachinery/pkg/runtime/serializer/json"
42 "k8s.io/apimachinery/pkg/runtime/serializer/protobuf"
43 "k8s.io/apimachinery/pkg/util/dump"
44 "k8s.io/apimachinery/pkg/util/sets"
45 )
46
47 type InstallFunc func(scheme *runtime.Scheme)
48
49
50
51 func RoundTripTestForAPIGroup(t *testing.T, installFn InstallFunc, fuzzingFuncs fuzzer.FuzzerFuncs) {
52 scheme := runtime.NewScheme()
53 installFn(scheme)
54
55 RoundTripTestForScheme(t, scheme, fuzzingFuncs)
56 }
57
58
59 func RoundTripTestForScheme(t *testing.T, scheme *runtime.Scheme, fuzzingFuncs fuzzer.FuzzerFuncs) {
60 codecFactory := runtimeserializer.NewCodecFactory(scheme)
61 f := fuzzer.FuzzerFor(
62 fuzzer.MergeFuzzerFuncs(metafuzzer.Funcs, fuzzingFuncs),
63 rand.NewSource(rand.Int63()),
64 codecFactory,
65 )
66 RoundTripTypesWithoutProtobuf(t, scheme, codecFactory, f, nil)
67 }
68
69
70
71 func RoundTripProtobufTestForAPIGroup(t *testing.T, installFn InstallFunc, fuzzingFuncs fuzzer.FuzzerFuncs) {
72 scheme := runtime.NewScheme()
73 installFn(scheme)
74
75 RoundTripProtobufTestForScheme(t, scheme, fuzzingFuncs)
76 }
77
78
79 func RoundTripProtobufTestForScheme(t *testing.T, scheme *runtime.Scheme, fuzzingFuncs fuzzer.FuzzerFuncs) {
80 codecFactory := runtimeserializer.NewCodecFactory(scheme)
81 fuzzer := fuzzer.FuzzerFor(
82 fuzzer.MergeFuzzerFuncs(metafuzzer.Funcs, fuzzingFuncs),
83 rand.NewSource(rand.Int63()),
84 codecFactory,
85 )
86 RoundTripTypes(t, scheme, codecFactory, fuzzer, nil)
87 }
88
89 var FuzzIters = flag.Int("fuzz-iters", defaultFuzzIters, "How many fuzzing iterations to do.")
90
91
92
93 var globalNonRoundTrippableTypes = sets.NewString(
94 "ExportOptions",
95 "GetOptions",
96
97
98
99
100 "WatchEvent",
101
102 "ListOptions",
103
104 "DeleteOptions",
105 )
106
107
108
109
110
111
112
113
114
115
116
117
118
119 func GlobalNonRoundTrippableTypes() sets.String {
120 return sets.NewString(globalNonRoundTrippableTypes.List()...)
121 }
122
123
124
125 func RoundTripTypesWithoutProtobuf(t *testing.T, scheme *runtime.Scheme, codecFactory runtimeserializer.CodecFactory, fuzzer *fuzz.Fuzzer, nonRoundTrippableTypes map[schema.GroupVersionKind]bool) {
126 roundTripTypes(t, scheme, codecFactory, fuzzer, nonRoundTrippableTypes, true)
127 }
128
129 func RoundTripTypes(t *testing.T, scheme *runtime.Scheme, codecFactory runtimeserializer.CodecFactory, fuzzer *fuzz.Fuzzer, nonRoundTrippableTypes map[schema.GroupVersionKind]bool) {
130 roundTripTypes(t, scheme, codecFactory, fuzzer, nonRoundTrippableTypes, false)
131 }
132
133 func roundTripTypes(t *testing.T, scheme *runtime.Scheme, codecFactory runtimeserializer.CodecFactory, fuzzer *fuzz.Fuzzer, nonRoundTrippableTypes map[schema.GroupVersionKind]bool, skipProtobuf bool) {
134 for _, group := range groupsFromScheme(scheme) {
135 t.Logf("starting group %q", group)
136 internalVersion := schema.GroupVersion{Group: group, Version: runtime.APIVersionInternal}
137 internalKindToGoType := scheme.KnownTypes(internalVersion)
138
139 for kind := range internalKindToGoType {
140 if globalNonRoundTrippableTypes.Has(kind) {
141 continue
142 }
143
144 internalGVK := internalVersion.WithKind(kind)
145 roundTripSpecificKind(t, internalGVK, scheme, codecFactory, fuzzer, nonRoundTrippableTypes, skipProtobuf)
146 }
147
148 t.Logf("finished group %q", group)
149 }
150 }
151
152
153
154 func RoundTripExternalTypes(t *testing.T, scheme *runtime.Scheme, codecFactory runtimeserializer.CodecFactory, fuzzer *fuzz.Fuzzer, nonRoundTrippableTypes map[schema.GroupVersionKind]bool) {
155 kinds := scheme.AllKnownTypes()
156 for gvk := range kinds {
157 if gvk.Version == runtime.APIVersionInternal || globalNonRoundTrippableTypes.Has(gvk.Kind) {
158 continue
159 }
160 t.Run(gvk.Group+"."+gvk.Version+"."+gvk.Kind, func(t *testing.T) {
161 roundTripSpecificKind(t, gvk, scheme, codecFactory, fuzzer, nonRoundTrippableTypes, false)
162 })
163 }
164 }
165
166
167
168 func RoundTripExternalTypesWithoutProtobuf(t *testing.T, scheme *runtime.Scheme, codecFactory runtimeserializer.CodecFactory, fuzzer *fuzz.Fuzzer, nonRoundTrippableTypes map[schema.GroupVersionKind]bool) {
169 kinds := scheme.AllKnownTypes()
170 for gvk := range kinds {
171 if gvk.Version == runtime.APIVersionInternal || globalNonRoundTrippableTypes.Has(gvk.Kind) {
172 continue
173 }
174 t.Run(gvk.Group+"."+gvk.Version+"."+gvk.Kind, func(t *testing.T) {
175 roundTripSpecificKind(t, gvk, scheme, codecFactory, fuzzer, nonRoundTrippableTypes, true)
176 })
177 }
178 }
179
180 func RoundTripSpecificKindWithoutProtobuf(t *testing.T, gvk schema.GroupVersionKind, scheme *runtime.Scheme, codecFactory runtimeserializer.CodecFactory, fuzzer *fuzz.Fuzzer, nonRoundTrippableTypes map[schema.GroupVersionKind]bool) {
181 roundTripSpecificKind(t, gvk, scheme, codecFactory, fuzzer, nonRoundTrippableTypes, true)
182 }
183
184 func RoundTripSpecificKind(t *testing.T, gvk schema.GroupVersionKind, scheme *runtime.Scheme, codecFactory runtimeserializer.CodecFactory, fuzzer *fuzz.Fuzzer, nonRoundTrippableTypes map[schema.GroupVersionKind]bool) {
185 roundTripSpecificKind(t, gvk, scheme, codecFactory, fuzzer, nonRoundTrippableTypes, false)
186 }
187
188 func roundTripSpecificKind(t *testing.T, gvk schema.GroupVersionKind, scheme *runtime.Scheme, codecFactory runtimeserializer.CodecFactory, fuzzer *fuzz.Fuzzer, nonRoundTrippableTypes map[schema.GroupVersionKind]bool, skipProtobuf bool) {
189 if nonRoundTrippableTypes[gvk] {
190 t.Logf("skipping %v", gvk)
191 return
192 }
193
194
195 for i := 0; i < *FuzzIters; i++ {
196 if gvk.Version == runtime.APIVersionInternal {
197 roundTripToAllExternalVersions(t, scheme, codecFactory, fuzzer, gvk, nonRoundTrippableTypes, skipProtobuf)
198 } else {
199 roundTripOfExternalType(t, scheme, codecFactory, fuzzer, gvk, skipProtobuf)
200 }
201 if t.Failed() {
202 break
203 }
204 }
205 }
206
207
208
209 func fuzzInternalObject(t *testing.T, fuzzer *fuzz.Fuzzer, object runtime.Object) runtime.Object {
210 fuzzer.Fuzz(object)
211
212 j, err := apimeta.TypeAccessor(object)
213 if err != nil {
214 t.Fatalf("Unexpected error %v for %#v", err, object)
215 }
216 j.SetKind("")
217 j.SetAPIVersion("")
218
219 return object
220 }
221
222 func groupsFromScheme(scheme *runtime.Scheme) []string {
223 ret := sets.String{}
224 for gvk := range scheme.AllKnownTypes() {
225 ret.Insert(gvk.Group)
226 }
227 return ret.List()
228 }
229
230 func roundTripToAllExternalVersions(t *testing.T, scheme *runtime.Scheme, codecFactory runtimeserializer.CodecFactory, fuzzer *fuzz.Fuzzer, internalGVK schema.GroupVersionKind, nonRoundTrippableTypes map[schema.GroupVersionKind]bool, skipProtobuf bool) {
231 object, err := scheme.New(internalGVK)
232 if err != nil {
233 t.Fatalf("Couldn't make a %v? %v", internalGVK, err)
234 }
235 if _, err := apimeta.TypeAccessor(object); err != nil {
236 t.Fatalf("%q is not a TypeMeta and cannot be tested - add it to nonRoundTrippableInternalTypes: %v", internalGVK, err)
237 }
238
239 fuzzInternalObject(t, fuzzer, object)
240
241
242
243 for externalGVK, externalGoType := range scheme.AllKnownTypes() {
244 if externalGVK.Version == runtime.APIVersionInternal {
245 continue
246 }
247 if externalGVK.GroupKind() != internalGVK.GroupKind() {
248 continue
249 }
250 if nonRoundTrippableTypes[externalGVK] {
251 t.Logf("\tskipping %v %v", externalGVK, externalGoType)
252 continue
253 }
254 t.Logf("\tround tripping to %v %v", externalGVK, externalGoType)
255
256 roundTrip(t, scheme, apitesting.TestCodec(codecFactory, externalGVK.GroupVersion()), object)
257
258
259 if !skipProtobuf && externalGVK.Group != "kubeadm.k8s.io" {
260 s := protobuf.NewSerializer(scheme, scheme)
261 protobufCodec := codecFactory.CodecForVersions(s, s, externalGVK.GroupVersion(), nil)
262 roundTrip(t, scheme, protobufCodec, object)
263 }
264 }
265 }
266
267 func roundTripOfExternalType(t *testing.T, scheme *runtime.Scheme, codecFactory runtimeserializer.CodecFactory, fuzzer *fuzz.Fuzzer, externalGVK schema.GroupVersionKind, skipProtobuf bool) {
268 object, err := scheme.New(externalGVK)
269 if err != nil {
270 t.Fatalf("Couldn't make a %v? %v", externalGVK, err)
271 }
272 typeAcc, err := apimeta.TypeAccessor(object)
273 if err != nil {
274 t.Fatalf("%q is not a TypeMeta and cannot be tested - add it to nonRoundTrippableInternalTypes: %v", externalGVK, err)
275 }
276
277 fuzzInternalObject(t, fuzzer, object)
278
279 typeAcc.SetKind(externalGVK.Kind)
280 typeAcc.SetAPIVersion(externalGVK.GroupVersion().String())
281
282 roundTrip(t, scheme, json.NewSerializer(json.DefaultMetaFactory, scheme, scheme, false), object)
283
284
285 if !skipProtobuf {
286 roundTrip(t, scheme, protobuf.NewSerializer(scheme, scheme), object)
287 }
288 }
289
290
291
292
293
294
295
296
297
298
299
300
301 func roundTrip(t *testing.T, scheme *runtime.Scheme, codec runtime.Codec, object runtime.Object) {
302 original := object
303
304
305 object = object.DeepCopyObject()
306 name := reflect.TypeOf(object).Elem().Name()
307 if !apiequality.Semantic.DeepEqual(original, object) {
308 t.Errorf("%v: DeepCopy altered the object, diff: %v", name, cmp.Diff(original, object))
309 t.Errorf("%s", dump.Pretty(original))
310 t.Errorf("%s", dump.Pretty(object))
311 return
312 }
313
314
315 data, err := runtime.Encode(codec, object)
316 if err != nil {
317 if runtime.IsNotRegisteredError(err) {
318 t.Logf("%v: not registered: %v (%s)", name, err, dump.Pretty(object))
319 } else {
320 t.Errorf("%v: %v (%s)", name, err, dump.Pretty(object))
321 }
322 return
323 }
324
325
326
327
328 if !apiequality.Semantic.DeepEqual(original, object) {
329 t.Errorf("%v: encode altered the object, diff: %v", name, cmp.Diff(original, object))
330 return
331 }
332
333
334 secondData, err := runtime.Encode(codec, object)
335 if err != nil {
336 if runtime.IsNotRegisteredError(err) {
337 t.Logf("%v: not registered: %v (%s)", name, err, dump.Pretty(object))
338 } else {
339 t.Errorf("%v: %v (%s)", name, err, dump.Pretty(object))
340 }
341 return
342 }
343
344
345
346 if !bytes.Equal(data, secondData) {
347 t.Errorf("%v: serialization is not stable: %s", name, dump.Pretty(object))
348 }
349
350
351 obj2, err := runtime.Decode(codec, data)
352 if err != nil {
353 t.Errorf("%v: %v\nCodec: %#v\nData: %s\nSource: %#v", name, err, codec, dataAsString(data), dump.Pretty(object))
354 panic("failed")
355 }
356
357
358
359 if !apiequality.Semantic.DeepEqual(original, obj2) {
360 t.Errorf("%v: diff: %v\nCodec: %#v\nSource:\n\n%#v\n\nEncoded:\n\n%s\n\nFinal:\n\n%#v", name, cmp.Diff(original, obj2), codec, dump.Pretty(original), dataAsString(data), dump.Pretty(obj2))
361 return
362 }
363
364
365
366 obj3 := reflect.New(reflect.TypeOf(object).Elem()).Interface().(runtime.Object)
367 if err := runtime.DecodeInto(codec, data, obj3); err != nil {
368 t.Errorf("%v: %v", name, err)
369 return
370 }
371
372
373
374
375 intAndExt, err := internalAndExternalKind(scheme, object)
376 if err != nil {
377 t.Errorf("%v: %v", name, err)
378 return
379 }
380 if intAndExt {
381 typeAcc, err := apimeta.TypeAccessor(object)
382 if err != nil {
383 t.Fatalf("%v: error accessing TypeMeta: %v", name, err)
384 }
385 if len(typeAcc.GetAPIVersion()) == 0 {
386 typeAcc, err := apimeta.TypeAccessor(obj3)
387 if err != nil {
388 t.Fatalf("%v: error accessing TypeMeta: %v", name, err)
389 }
390 typeAcc.SetAPIVersion("")
391 typeAcc.SetKind("")
392 }
393 }
394
395
396
397 if !apiequality.Semantic.DeepEqual(object, obj3) {
398 t.Errorf("%v: diff: %v\nCodec: %#v", name, cmp.Diff(object, obj3), codec)
399 return
400 }
401
402
403
404
405 fuzzer.ValueFuzz(object)
406 if !apiequality.Semantic.DeepEqual(original, obj3) {
407 t.Errorf("%v: fuzzing a copy altered the original, diff: %v", name, cmp.Diff(original, obj3))
408 return
409 }
410 }
411
412 func internalAndExternalKind(scheme *runtime.Scheme, object runtime.Object) (bool, error) {
413 kinds, _, err := scheme.ObjectKinds(object)
414 if err != nil {
415 return false, err
416 }
417 internal, external := false, false
418 for _, k := range kinds {
419 if k.Version == runtime.APIVersionInternal {
420 internal = true
421 } else {
422 external = true
423 }
424 }
425 return internal && external, nil
426 }
427
428
429
430 func dataAsString(data []byte) string {
431 dataString := string(data)
432 if !strings.HasPrefix(dataString, "{") {
433 dataString = "\n" + hex.Dump(data)
434 proto.NewBuffer(make([]byte, 0, 1024)).DebugPrint("decoded object", data)
435 }
436 return dataString
437 }
438
View as plain text