github.com/spotmaxtech/k8s-apimachinery-v0260@v0.0.1/pkg/api/apitesting/roundtrip/roundtrip.go (about) 1 /* 2 Copyright 2017 The Kubernetes Authors. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 package roundtrip 18 19 import ( 20 "bytes" 21 "encoding/hex" 22 "math/rand" 23 "reflect" 24 "strings" 25 "testing" 26 27 "github.com/davecgh/go-spew/spew" 28 //nolint:staticcheck //iccheck // SA1019 Keep using deprecated module; it still seems to be maintained and the api of the recommended replacement differs 29 "github.com/golang/protobuf/proto" 30 fuzz "github.com/google/gofuzz" 31 flag "github.com/spf13/pflag" 32 33 apitesting "github.com/spotmaxtech/k8s-apimachinery-v0260/pkg/api/apitesting" 34 "github.com/spotmaxtech/k8s-apimachinery-v0260/pkg/api/apitesting/fuzzer" 35 apiequality "github.com/spotmaxtech/k8s-apimachinery-v0260/pkg/api/equality" 36 apimeta "github.com/spotmaxtech/k8s-apimachinery-v0260/pkg/api/meta" 37 metafuzzer "github.com/spotmaxtech/k8s-apimachinery-v0260/pkg/apis/meta/fuzzer" 38 "github.com/spotmaxtech/k8s-apimachinery-v0260/pkg/runtime" 39 "github.com/spotmaxtech/k8s-apimachinery-v0260/pkg/runtime/schema" 40 runtimeserializer "github.com/spotmaxtech/k8s-apimachinery-v0260/pkg/runtime/serializer" 41 "github.com/spotmaxtech/k8s-apimachinery-v0260/pkg/runtime/serializer/json" 42 "github.com/spotmaxtech/k8s-apimachinery-v0260/pkg/runtime/serializer/protobuf" 43 "github.com/spotmaxtech/k8s-apimachinery-v0260/pkg/util/diff" 44 "github.com/spotmaxtech/k8s-apimachinery-v0260/pkg/util/sets" 45 ) 46 47 type InstallFunc func(scheme *runtime.Scheme) 48 49 // RoundTripTestForAPIGroup is convenient to call from your install package to make sure that a "bare" install of your group provides 50 // enough information to round trip 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 // RoundTripTestForScheme is convenient to call if you already have a scheme and want to make sure that its well-formed 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 // RoundTripProtobufTestForAPIGroup is convenient to call from your install package to make sure that a "bare" install of your group provides 70 // enough information to round trip 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 // RoundTripProtobufTestForScheme is convenient to call if you already have a scheme and want to make sure that its well-formed 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 // globalNonRoundTrippableTypes are kinds that are effectively reserved across all GroupVersions 92 // They don't roundtrip 93 var globalNonRoundTrippableTypes = sets.NewString( 94 "ExportOptions", 95 "GetOptions", 96 // WatchEvent does not include kind and version and can only be deserialized 97 // implicitly (if the caller expects the specific object). The watch call defines 98 // the schema by content type, rather than via kind/version included in each 99 // object. 100 "WatchEvent", 101 // ListOptions is now part of the meta group 102 "ListOptions", 103 // Delete options is only read in metav1 104 "DeleteOptions", 105 ) 106 107 // GlobalNonRoundTrippableTypes returns the kinds that are effectively reserved across all GroupVersions. 108 // They don't roundtrip and thus can be excluded in any custom/downstream roundtrip tests 109 // 110 // kinds := scheme.AllKnownTypes() 111 // for gvk := range kinds { 112 // if roundtrip.GlobalNonRoundTrippableTypes().Has(gvk.Kind) { 113 // continue 114 // } 115 // t.Run(gvk.Group+"."+gvk.Version+"."+gvk.Kind, func(t *testing.T) { 116 // // roundtrip test 117 // }) 118 // } 119 func GlobalNonRoundTrippableTypes() sets.String { 120 return sets.NewString(globalNonRoundTrippableTypes.List()...) 121 } 122 123 // RoundTripTypesWithoutProtobuf applies the round-trip test to all round-trippable Kinds 124 // in the scheme. It will skip all the GroupVersionKinds in the skip list. 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 // RoundTripExternalTypes applies the round-trip test to all external round-trippable Kinds 153 // in the scheme. It will skip all the GroupVersionKinds in the nonRoundTripExternalTypes list . 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 // RoundTripExternalTypesWithoutProtobuf applies the round-trip test to all external round-trippable Kinds 167 // in the scheme. It will skip all the GroupVersionKinds in the nonRoundTripExternalTypes list. 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 // Try a few times, since runTest uses random values. 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 // fuzzInternalObject fuzzes an arbitrary runtime object using the appropriate 208 // fuzzer registered with the apitesting package. 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 // find all potential serializations in the scheme. 242 // TODO fix this up to handle kinds that cross registered with different names. 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 // TODO remove this hack after we're past the intermediate steps 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 // TODO remove this hack after we're past the intermediate steps 285 if !skipProtobuf { 286 roundTrip(t, scheme, protobuf.NewSerializer(scheme, scheme), object) 287 } 288 } 289 290 // roundTrip applies a single round-trip test to the given runtime object 291 // using the given codec. The round-trip test ensures that an object can be 292 // deep-copied, converted, marshaled and back without loss of data. 293 // 294 // For internal types this means 295 // 296 // internal -> external -> json/protobuf -> external -> internal. 297 // 298 // For external types this means 299 // 300 // external -> json/protobuf -> external. 301 func roundTrip(t *testing.T, scheme *runtime.Scheme, codec runtime.Codec, object runtime.Object) { 302 printer := spew.ConfigState{DisableMethods: true} 303 original := object 304 305 // deep copy the original object 306 object = object.DeepCopyObject() 307 name := reflect.TypeOf(object).Elem().Name() 308 if !apiequality.Semantic.DeepEqual(original, object) { 309 t.Errorf("%v: DeepCopy altered the object, diff: %v", name, diff.ObjectReflectDiff(original, object)) 310 t.Errorf("%s", spew.Sdump(original)) 311 t.Errorf("%s", spew.Sdump(object)) 312 return 313 } 314 315 // encode (serialize) the deep copy using the provided codec 316 data, err := runtime.Encode(codec, object) 317 if err != nil { 318 if runtime.IsNotRegisteredError(err) { 319 t.Logf("%v: not registered: %v (%s)", name, err, printer.Sprintf("%#v", object)) 320 } else { 321 t.Errorf("%v: %v (%s)", name, err, printer.Sprintf("%#v", object)) 322 } 323 return 324 } 325 326 // ensure that the deep copy is equal to the original; neither the deep 327 // copy or conversion should alter the object 328 // TODO eliminate this global 329 if !apiequality.Semantic.DeepEqual(original, object) { 330 t.Errorf("%v: encode altered the object, diff: %v", name, diff.ObjectReflectDiff(original, object)) 331 return 332 } 333 334 // encode (serialize) a second time to verify that it was not varying 335 secondData, err := runtime.Encode(codec, object) 336 if err != nil { 337 if runtime.IsNotRegisteredError(err) { 338 t.Logf("%v: not registered: %v (%s)", name, err, printer.Sprintf("%#v", object)) 339 } else { 340 t.Errorf("%v: %v (%s)", name, err, printer.Sprintf("%#v", object)) 341 } 342 return 343 } 344 345 // serialization to the wire must be stable to ensure that we don't write twice to the DB 346 // when the object hasn't changed. 347 if !bytes.Equal(data, secondData) { 348 t.Errorf("%v: serialization is not stable: %s", name, printer.Sprintf("%#v", object)) 349 } 350 351 // decode (deserialize) the encoded data back into an object 352 obj2, err := runtime.Decode(codec, data) 353 if err != nil { 354 t.Errorf("%v: %v\nCodec: %#v\nData: %s\nSource: %#v", name, err, codec, dataAsString(data), printer.Sprintf("%#v", object)) 355 panic("failed") 356 } 357 358 // ensure that the object produced from decoding the encoded data is equal 359 // to the original object 360 if !apiequality.Semantic.DeepEqual(original, obj2) { 361 t.Errorf("%v: diff: %v\nCodec: %#v\nSource:\n\n%#v\n\nEncoded:\n\n%s\n\nFinal:\n\n%#v", name, diff.ObjectReflectDiff(original, obj2), codec, printer.Sprintf("%#v", original), dataAsString(data), printer.Sprintf("%#v", obj2)) 362 return 363 } 364 365 // decode the encoded data into a new object (instead of letting the codec 366 // create a new object) 367 obj3 := reflect.New(reflect.TypeOf(object).Elem()).Interface().(runtime.Object) 368 if err := runtime.DecodeInto(codec, data, obj3); err != nil { 369 t.Errorf("%v: %v", name, err) 370 return 371 } 372 373 // special case for kinds which are internal and external at the same time (many in meta.k8s.io are). For those 374 // runtime.DecodeInto above will return the external variant and set the APIVersion and kind, while the input 375 // object might be internal. Hence, we clear those values for obj3 for that case to correctly compare. 376 intAndExt, err := internalAndExternalKind(scheme, object) 377 if err != nil { 378 t.Errorf("%v: %v", name, err) 379 return 380 } 381 if intAndExt { 382 typeAcc, err := apimeta.TypeAccessor(object) 383 if err != nil { 384 t.Fatalf("%v: error accessing TypeMeta: %v", name, err) 385 } 386 if len(typeAcc.GetAPIVersion()) == 0 { 387 typeAcc, err := apimeta.TypeAccessor(obj3) 388 if err != nil { 389 t.Fatalf("%v: error accessing TypeMeta: %v", name, err) 390 } 391 typeAcc.SetAPIVersion("") 392 typeAcc.SetKind("") 393 } 394 } 395 396 // ensure that the new runtime object is equal to the original after being 397 // decoded into 398 if !apiequality.Semantic.DeepEqual(object, obj3) { 399 t.Errorf("%v: diff: %v\nCodec: %#v", name, diff.ObjectReflectDiff(object, obj3), codec) 400 return 401 } 402 403 // do structure-preserving fuzzing of the deep-copied object. If it shares anything with the original, 404 // the deep-copy was actually only a shallow copy. Then original and obj3 will be different after fuzzing. 405 // NOTE: we use the encoding+decoding here as an alternative, guaranteed deep-copy to compare against. 406 fuzzer.ValueFuzz(object) 407 if !apiequality.Semantic.DeepEqual(original, obj3) { 408 t.Errorf("%v: fuzzing a copy altered the original, diff: %v", name, diff.ObjectReflectDiff(original, obj3)) 409 return 410 } 411 } 412 413 func internalAndExternalKind(scheme *runtime.Scheme, object runtime.Object) (bool, error) { 414 kinds, _, err := scheme.ObjectKinds(object) 415 if err != nil { 416 return false, err 417 } 418 internal, external := false, false 419 for _, k := range kinds { 420 if k.Version == runtime.APIVersionInternal { 421 internal = true 422 } else { 423 external = true 424 } 425 } 426 return internal && external, nil 427 } 428 429 // dataAsString returns the given byte array as a string; handles detecting 430 // protocol buffers. 431 func dataAsString(data []byte) string { 432 dataString := string(data) 433 if !strings.HasPrefix(dataString, "{") { 434 dataString = "\n" + hex.Dump(data) 435 proto.NewBuffer(make([]byte, 0, 1024)).DebugPrint("decoded object", data) 436 } 437 return dataString 438 }