...

Source file src/k8s.io/apimachinery/pkg/api/apitesting/roundtrip/roundtrip.go

Documentation: k8s.io/apimachinery/pkg/api/apitesting/roundtrip

     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  	//nolint:staticcheck //iccheck // SA1019 Keep using deprecated module; it still seems to be maintained and the api of the recommended replacement differs
    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  // 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  	original := object
   303  
   304  	// deep copy the original object
   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  	// encode (serialize) the deep copy using the provided codec
   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  	// ensure that the deep copy is equal to the original; neither the deep
   326  	// copy or conversion should alter the object
   327  	// TODO eliminate this global
   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  	// encode (serialize) a second time to verify that it was not varying
   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  	// serialization to the wire must be stable to ensure that we don't write twice to the DB
   345  	// when the object hasn't changed.
   346  	if !bytes.Equal(data, secondData) {
   347  		t.Errorf("%v: serialization is not stable: %s", name, dump.Pretty(object))
   348  	}
   349  
   350  	// decode (deserialize) the encoded data back into an object
   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  	// ensure that the object produced from decoding the encoded data is equal
   358  	// to the original object
   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  	// decode the encoded data into a new object (instead of letting the codec
   365  	// create a new object)
   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  	// special case for kinds which are internal and external at the same time (many in meta.k8s.io are). For those
   373  	// runtime.DecodeInto above will return the external variant and set the APIVersion and kind, while the input
   374  	// object might be internal. Hence, we clear those values for obj3 for that case to correctly compare.
   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  	// ensure that the new runtime object is equal to the original after being
   396  	// decoded into
   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  	// do structure-preserving fuzzing of the deep-copied object. If it shares anything with the original,
   403  	// the deep-copy was actually only a shallow copy. Then original and obj3 will be different after fuzzing.
   404  	// NOTE: we use the encoding+decoding here as an alternative, guaranteed deep-copy to compare against.
   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  // dataAsString returns the given byte array as a string; handles detecting
   429  // protocol buffers.
   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