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  }