From 4f0b1707868d8311d31d6633cfaca3eefce705c5 Mon Sep 17 00:00:00 2001 From: Lionel Villard Date: Fri, 23 Oct 2020 06:52:37 -0400 Subject: [PATCH] DeepCopy the entire set of conditions (#128) (#1645) * DeepCopy the entire set of conditions * remove unused function * add fuzzing testing --- go.mod | 1 + .../apis/sources/v1alpha1/kafka_conversion.go | 19 +- .../pkg/apis/sources/v1alpha1/kafka_types.go | 6 + .../apis/sources/v1alpha1/roundtrip_test.go | 63 ++ .../sources/v1alpha1/zz_generated.deepcopy.go | 5 + .../source/pkg/apis/sources/v1beta1/fuzzer.go | 45 ++ .../apis/sources/v1beta1/roundtrip_test.go | 38 ++ test/lib/listers.go | 5 - .../apimachinery/pkg/api/apitesting/codec.go | 116 ++++ .../api/apitesting/roundtrip/compatibility.go | 537 ++++++++++++++++++ .../pkg/api/apitesting/roundtrip/roundtrip.go | 438 ++++++++++++++ .../pkg/apis/meta/fuzzer/fuzzer.go | 331 +++++++++++ .../pkg/apis/testing/roundtrip/roundtrip.go | 256 +++++++++ vendor/modules.txt | 5 + 14 files changed, 1856 insertions(+), 9 deletions(-) create mode 100644 kafka/source/pkg/apis/sources/v1alpha1/roundtrip_test.go create mode 100644 kafka/source/pkg/apis/sources/v1beta1/fuzzer.go create mode 100644 kafka/source/pkg/apis/sources/v1beta1/roundtrip_test.go create mode 100644 vendor/k8s.io/apimachinery/pkg/api/apitesting/codec.go create mode 100644 vendor/k8s.io/apimachinery/pkg/api/apitesting/roundtrip/compatibility.go create mode 100644 vendor/k8s.io/apimachinery/pkg/api/apitesting/roundtrip/roundtrip.go create mode 100644 vendor/k8s.io/apimachinery/pkg/apis/meta/fuzzer/fuzzer.go create mode 100644 vendor/knative.dev/pkg/apis/testing/roundtrip/roundtrip.go diff --git a/go.mod b/go.mod index 001e5c88d9..1d4863df3c 100644 --- a/go.mod +++ b/go.mod @@ -9,6 +9,7 @@ require ( github.com/davecgh/go-spew v1.1.1 github.com/golang/protobuf v1.4.2 github.com/google/go-cmp v0.5.2 + github.com/google/gofuzz v1.1.0 github.com/google/uuid v1.1.1 github.com/gorilla/websocket v1.4.2 github.com/influxdata/tdigest v0.0.1 // indirect diff --git a/kafka/source/pkg/apis/sources/v1alpha1/kafka_conversion.go b/kafka/source/pkg/apis/sources/v1alpha1/kafka_conversion.go index 92237db24e..e55488a0a7 100644 --- a/kafka/source/pkg/apis/sources/v1alpha1/kafka_conversion.go +++ b/kafka/source/pkg/apis/sources/v1alpha1/kafka_conversion.go @@ -44,12 +44,17 @@ func (source *KafkaSource) ConvertTo(ctx context.Context, obj apis.Convertible) Topics: source.Spec.Topics, ConsumerGroup: source.Spec.ConsumerGroup, } - sink.Status.Status = source.Status.Status - source.Status.Status.ConvertTo(ctx, &sink.Status.Status) + source.Status.Status.DeepCopyInto(&sink.Status.Status) + // Optionals if source.Spec.Sink != nil { sink.Spec.Sink = *source.Spec.Sink.DeepCopy() } + + if source.Spec.CloudEventOverrides != nil { + sink.Spec.CloudEventOverrides = source.Spec.CloudEventOverrides.DeepCopy() + } + if source.Status.SinkURI != nil { sink.Status.SinkURI = source.Status.SinkURI.DeepCopy() } @@ -83,12 +88,18 @@ func (sink *KafkaSource) ConvertFrom(ctx context.Context, obj apis.Convertible) if reflect.DeepEqual(*sink.Spec.Sink, duckv1.Destination{}) { sink.Spec.Sink = nil } - sink.Status.Status = source.Status.Status - source.Status.Status.ConvertTo(ctx, &source.Status.Status) + + source.Status.Status.DeepCopyInto(&sink.Status.Status) + // Optionals if source.Status.SinkURI != nil { sink.Status.SinkURI = source.Status.SinkURI.DeepCopy() } + + if source.Spec.CloudEventOverrides != nil { + sink.Spec.CloudEventOverrides = source.Spec.CloudEventOverrides.DeepCopy() + } + if source.Status.CloudEventAttributes != nil { sink.Status.CloudEventAttributes = make([]duckv1.CloudEventAttributes, len(source.Status.CloudEventAttributes)) copy(sink.Status.CloudEventAttributes, source.Status.CloudEventAttributes) diff --git a/kafka/source/pkg/apis/sources/v1alpha1/kafka_types.go b/kafka/source/pkg/apis/sources/v1alpha1/kafka_types.go index 909c1dc404..5cf4041836 100644 --- a/kafka/source/pkg/apis/sources/v1alpha1/kafka_types.go +++ b/kafka/source/pkg/apis/sources/v1alpha1/kafka_types.go @@ -89,6 +89,12 @@ type KafkaSourceSpec struct { // Resource limits and Request specifications of the Receive Adapter Deployment // Deprecated: v1beta1 drops this field. Resources KafkaResourceSpec `json:"resources,omitempty"` + + // CloudEventOverrides defines overrides to control the output format and + // modifications of the event sent to the sink. + // +optional + // Needed for supporting round-tripping + CloudEventOverrides *duckv1.CloudEventOverrides `json:"ceOverrides,omitempty"` } const ( diff --git a/kafka/source/pkg/apis/sources/v1alpha1/roundtrip_test.go b/kafka/source/pkg/apis/sources/v1alpha1/roundtrip_test.go new file mode 100644 index 0000000000..d9a94cf3ff --- /dev/null +++ b/kafka/source/pkg/apis/sources/v1alpha1/roundtrip_test.go @@ -0,0 +1,63 @@ +/* +Copyright 2020 The Knative Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1alpha1 + +import ( + "testing" + + "k8s.io/apimachinery/pkg/api/apitesting/fuzzer" + "k8s.io/apimachinery/pkg/runtime" + utilruntime "k8s.io/apimachinery/pkg/util/runtime" + pkgfuzzer "knative.dev/pkg/apis/testing/fuzzer" + "knative.dev/pkg/apis/testing/roundtrip" + + "knative.dev/eventing-contrib/kafka/source/pkg/apis/sources/v1beta1" +) + +func TestSourcesRoundTripTypesToJSON(t *testing.T) { + scheme := runtime.NewScheme() + utilruntime.Must(AddToScheme(scheme)) + + fuzzerFuncs := fuzzer.MergeFuzzerFuncs( + pkgfuzzer.Funcs, + v1beta1.FuzzerFuncs, + ) + roundtrip.ExternalTypesViaJSON(t, scheme, fuzzerFuncs) +} + +func TestSourceRoundTripTypesToAlphaHub(t *testing.T) { + scheme := runtime.NewScheme() + + sb := runtime.SchemeBuilder{ + AddToScheme, + v1beta1.AddToScheme, + } + + utilruntime.Must(sb.AddToScheme(scheme)) + + hubs := runtime.NewScheme() + hubs.AddKnownTypes(SchemeGroupVersion, + &KafkaSource{}, + ) + + fuzzerFuncs := fuzzer.MergeFuzzerFuncs( + pkgfuzzer.Funcs, + v1beta1.FuzzerFuncs, + ) + + roundtrip.ExternalTypesViaHub(t, scheme, hubs, fuzzerFuncs) +} diff --git a/kafka/source/pkg/apis/sources/v1alpha1/zz_generated.deepcopy.go b/kafka/source/pkg/apis/sources/v1alpha1/zz_generated.deepcopy.go index 382c218ba8..c1ef4f7c35 100644 --- a/kafka/source/pkg/apis/sources/v1alpha1/zz_generated.deepcopy.go +++ b/kafka/source/pkg/apis/sources/v1alpha1/zz_generated.deepcopy.go @@ -151,6 +151,11 @@ func (in *KafkaSourceSpec) DeepCopyInto(out *KafkaSourceSpec) { (*in).DeepCopyInto(*out) } out.Resources = in.Resources + if in.CloudEventOverrides != nil { + in, out := &in.CloudEventOverrides, &out.CloudEventOverrides + *out = new(v1.CloudEventOverrides) + (*in).DeepCopyInto(*out) + } return } diff --git a/kafka/source/pkg/apis/sources/v1beta1/fuzzer.go b/kafka/source/pkg/apis/sources/v1beta1/fuzzer.go new file mode 100644 index 0000000000..52b8cc35b4 --- /dev/null +++ b/kafka/source/pkg/apis/sources/v1beta1/fuzzer.go @@ -0,0 +1,45 @@ +/* +Copyright 2020 The Knative Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1beta1 + +import ( + fuzz "github.com/google/gofuzz" + "k8s.io/apimachinery/pkg/api/apitesting/fuzzer" + "k8s.io/apimachinery/pkg/runtime/serializer" + pkgfuzzer "knative.dev/pkg/apis/testing/fuzzer" +) + +// FuzzerFuncs includes fuzzing funcs for sources.knative.dev v1beta1 types +// +// For other examples see +// https://github.com/kubernetes/apimachinery/blob/master/pkg/apis/meta/fuzzer/fuzzer.go +var FuzzerFuncs = fuzzer.MergeFuzzerFuncs( + func(codecs serializer.CodecFactory) []interface{} { + return []interface{}{ + func(s *KafkaSourceStatus, c fuzz.Continue) { + c.FuzzNoCustom(s) // fuzz the status object + + // Clear the random fuzzed condition + s.Status.SetConditions(nil) + + // Fuzz the known conditions except their type value + s.InitializeConditions() + pkgfuzzer.FuzzConditions(&s.Status, c) + }, + } + }, +) diff --git a/kafka/source/pkg/apis/sources/v1beta1/roundtrip_test.go b/kafka/source/pkg/apis/sources/v1beta1/roundtrip_test.go new file mode 100644 index 0000000000..4d505c122f --- /dev/null +++ b/kafka/source/pkg/apis/sources/v1beta1/roundtrip_test.go @@ -0,0 +1,38 @@ +/* +Copyright 2020 The Knative Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1beta1 + +import ( + "testing" + + "k8s.io/apimachinery/pkg/api/apitesting/fuzzer" + "k8s.io/apimachinery/pkg/runtime" + utilruntime "k8s.io/apimachinery/pkg/util/runtime" + pkgfuzzer "knative.dev/pkg/apis/testing/fuzzer" + "knative.dev/pkg/apis/testing/roundtrip" +) + +func TestSourcesRoundTripTypesToJSON(t *testing.T) { + scheme := runtime.NewScheme() + utilruntime.Must(AddToScheme(scheme)) + + fuzzerFuncs := fuzzer.MergeFuzzerFuncs( + pkgfuzzer.Funcs, + FuzzerFuncs, + ) + roundtrip.ExternalTypesViaJSON(t, scheme, fuzzerFuncs) +} diff --git a/test/lib/listers.go b/test/lib/listers.go index d209b566a8..5cd8ab254d 100644 --- a/test/lib/listers.go +++ b/test/lib/listers.go @@ -19,7 +19,6 @@ package lib import ( "k8s.io/apimachinery/pkg/runtime" fakekubeclientset "k8s.io/client-go/kubernetes/fake" - "k8s.io/client-go/tools/cache" fakeeventingclientset "knative.dev/eventing/pkg/client/clientset/versioned/fake" "knative.dev/pkg/reconciler/testing" ) @@ -58,10 +57,6 @@ func NewListers(objs []runtime.Object) Listers { return ls } -func (l Listers) indexerFor(obj runtime.Object) cache.Indexer { - return l.sorter.IndexerForObjectType(obj) -} - func (l Listers) GetKubeObjects() []runtime.Object { return l.sorter.ObjectsForSchemeFunc(fakekubeclientset.AddToScheme) } diff --git a/vendor/k8s.io/apimachinery/pkg/api/apitesting/codec.go b/vendor/k8s.io/apimachinery/pkg/api/apitesting/codec.go new file mode 100644 index 0000000000..542b0aa275 --- /dev/null +++ b/vendor/k8s.io/apimachinery/pkg/api/apitesting/codec.go @@ -0,0 +1,116 @@ +/* +Copyright 2017 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package apitesting + +import ( + "fmt" + "mime" + "os" + + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + runtimeserializer "k8s.io/apimachinery/pkg/runtime/serializer" + "k8s.io/apimachinery/pkg/runtime/serializer/recognizer" +) + +var ( + testCodecMediaType string + testStorageCodecMediaType string +) + +// TestCodec returns the codec for the API version to test against, as set by the +// KUBE_TEST_API_TYPE env var. +func TestCodec(codecs runtimeserializer.CodecFactory, gvs ...schema.GroupVersion) runtime.Codec { + if len(testCodecMediaType) != 0 { + serializerInfo, ok := runtime.SerializerInfoForMediaType(codecs.SupportedMediaTypes(), testCodecMediaType) + if !ok { + panic(fmt.Sprintf("no serializer for %s", testCodecMediaType)) + } + return codecs.CodecForVersions(serializerInfo.Serializer, codecs.UniversalDeserializer(), schema.GroupVersions(gvs), nil) + } + return codecs.LegacyCodec(gvs...) +} + +// TestStorageCodec returns the codec for the API version to test against used in storage, as set by the +// KUBE_TEST_API_STORAGE_TYPE env var. +func TestStorageCodec(codecs runtimeserializer.CodecFactory, gvs ...schema.GroupVersion) runtime.Codec { + if len(testStorageCodecMediaType) != 0 { + serializerInfo, ok := runtime.SerializerInfoForMediaType(codecs.SupportedMediaTypes(), testStorageCodecMediaType) + if !ok { + panic(fmt.Sprintf("no serializer for %s", testStorageCodecMediaType)) + } + + // etcd2 only supports string data - we must wrap any result before returning + // TODO: remove for etcd3 / make parameterizable + serializer := serializerInfo.Serializer + if !serializerInfo.EncodesAsText { + serializer = runtime.NewBase64Serializer(serializer, serializer) + } + + decoder := recognizer.NewDecoder(serializer, codecs.UniversalDeserializer()) + return codecs.CodecForVersions(serializer, decoder, schema.GroupVersions(gvs), nil) + + } + return codecs.LegacyCodec(gvs...) +} + +func init() { + var err error + if apiMediaType := os.Getenv("KUBE_TEST_API_TYPE"); len(apiMediaType) > 0 { + testCodecMediaType, _, err = mime.ParseMediaType(apiMediaType) + if err != nil { + panic(err) + } + } + + if storageMediaType := os.Getenv("KUBE_TEST_API_STORAGE_TYPE"); len(storageMediaType) > 0 { + testStorageCodecMediaType, _, err = mime.ParseMediaType(storageMediaType) + if err != nil { + panic(err) + } + } +} + +// InstallOrDieFunc mirrors install functions that require success +type InstallOrDieFunc func(scheme *runtime.Scheme) + +// SchemeForInstallOrDie builds a simple test scheme and codecfactory pair for easy unit testing from higher level install methods +func SchemeForInstallOrDie(installFns ...InstallOrDieFunc) (*runtime.Scheme, runtimeserializer.CodecFactory) { + scheme := runtime.NewScheme() + codecFactory := runtimeserializer.NewCodecFactory(scheme) + for _, installFn := range installFns { + installFn(scheme) + } + + return scheme, codecFactory +} + +// InstallFunc mirrors install functions that can return an error +type InstallFunc func(scheme *runtime.Scheme) error + +// SchemeForOrDie builds a simple test scheme and codecfactory pair for easy unit testing from the bare registration methods. +func SchemeForOrDie(installFns ...InstallFunc) (*runtime.Scheme, runtimeserializer.CodecFactory) { + scheme := runtime.NewScheme() + codecFactory := runtimeserializer.NewCodecFactory(scheme) + for _, installFn := range installFns { + if err := installFn(scheme); err != nil { + panic(err) + } + } + + return scheme, codecFactory +} diff --git a/vendor/k8s.io/apimachinery/pkg/api/apitesting/roundtrip/compatibility.go b/vendor/k8s.io/apimachinery/pkg/api/apitesting/roundtrip/compatibility.go new file mode 100644 index 0000000000..c65525330f --- /dev/null +++ b/vendor/k8s.io/apimachinery/pkg/api/apitesting/roundtrip/compatibility.go @@ -0,0 +1,537 @@ +/* +Copyright 2019 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package roundtrip + +import ( + "bytes" + "io/ioutil" + "os" + "os/exec" + "path/filepath" + "sort" + "strconv" + "strings" + "testing" + "time" + + "github.com/google/go-cmp/cmp" + fuzz "github.com/google/gofuzz" + + apiequality "k8s.io/apimachinery/pkg/api/equality" + apimeta "k8s.io/apimachinery/pkg/api/meta" + genericfuzzer "k8s.io/apimachinery/pkg/apis/meta/fuzzer" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/runtime/serializer" + "k8s.io/apimachinery/pkg/runtime/serializer/json" + "k8s.io/apimachinery/pkg/runtime/serializer/protobuf" + "k8s.io/apimachinery/pkg/util/intstr" + "k8s.io/apimachinery/pkg/util/sets" +) + +// CompatibilityTestOptions holds configuration for running a compatibility test using in-memory objects +// and serialized files on disk representing the current code and serialized data from previous versions. +// +// Example use: `NewCompatibilityTestOptions(scheme).Complete(t).Run(t)` +type CompatibilityTestOptions struct { + // Scheme is used to create new objects for fuzzing, decoding, and for constructing serializers. + // Required. + Scheme *runtime.Scheme + + // TestDataDir points to a directory containing compatibility test data. + // Complete() populates this with "testdata" if unset. + TestDataDir string + + // TestDataDirCurrentVersion points to a directory containing compatibility test data for the current version. + // Complete() populates this with "/HEAD" if unset. + // Within this directory, `...[json|yaml|pb]` files are required to exist, and are: + // * verified to match serialized FuzzedObjects[GVK] + // * verified to decode without error + // * verified to round-trip byte-for-byte when re-encoded + // * verified to be semantically equal when decoded into memory + TestDataDirCurrentVersion string + + // TestDataDirsPreviousVersions is a list of directories containing compatibility test data for previous versions. + // Complete() populates this with "/v*" directories if nil. + // Within these directories, `...[json|yaml|pb]` files are optional. If present, they are: + // * verified to decode without error + // * verified to round-trip byte-for-byte when re-encoded (or to match a `...[json|yaml|pb].after_roundtrip.[json|yaml|pb]` file if it exists) + // * verified to be semantically equal when decoded into memory + TestDataDirsPreviousVersions []string + + // Kinds is a list of fully qualified kinds to test. + // Complete() populates this with Scheme.AllKnownTypes() if unset. + Kinds []schema.GroupVersionKind + + // FuzzedObjects is an optional set of fuzzed objects to use for verifying HEAD fixtures. + // Complete() populates this with the result of CompatibilityTestObject(Kinds[*], Scheme, FuzzFuncs) for any missing kinds. + // Objects must be deterministically fuzzed and identical on every invocation. + FuzzedObjects map[schema.GroupVersionKind]runtime.Object + + // FuzzFuncs is an optional set of custom fuzzing functions to use to construct FuzzedObjects. + // They *must* not use any random source other than the passed-in fuzzer. + FuzzFuncs []interface{} + + JSON runtime.Serializer + YAML runtime.Serializer + Proto runtime.Serializer +} + +func NewCompatibilityTestOptions(scheme *runtime.Scheme) *CompatibilityTestOptions { + return &CompatibilityTestOptions{Scheme: scheme} +} + +// coreKinds includes kinds that typically only need to be tested in a single API group +var coreKinds = sets.NewString( + "CreateOptions", "UpdateOptions", "PatchOptions", "DeleteOptions", + "GetOptions", "ListOptions", "ExportOptions", + "WatchEvent", +) + +func (c *CompatibilityTestOptions) Complete(t *testing.T) *CompatibilityTestOptions { + t.Helper() + + // Verify scheme + if c.Scheme == nil { + t.Fatal("scheme is required") + } + + // Populate testdata dirs + if c.TestDataDir == "" { + c.TestDataDir = "testdata" + } + if c.TestDataDirCurrentVersion == "" { + c.TestDataDirCurrentVersion = filepath.Join(c.TestDataDir, "HEAD") + } + if c.TestDataDirsPreviousVersions == nil { + dirs, err := filepath.Glob(filepath.Join(c.TestDataDir, "v*")) + if err != nil { + t.Fatal(err) + } + sort.Strings(dirs) + c.TestDataDirsPreviousVersions = dirs + } + + // Populate kinds + if len(c.Kinds) == 0 { + gvks := []schema.GroupVersionKind{} + for gvk := range c.Scheme.AllKnownTypes() { + if gvk.Version == "" || gvk.Version == runtime.APIVersionInternal { + // only test external types + continue + } + if strings.HasSuffix(gvk.Kind, "List") { + // omit list types + continue + } + if gvk.Group != "" && coreKinds.Has(gvk.Kind) { + // only test options types in the core API group + continue + } + gvks = append(gvks, gvk) + } + c.Kinds = gvks + } + + // Sort kinds to get deterministic test order + sort.Slice(c.Kinds, func(i, j int) bool { + if c.Kinds[i].Group != c.Kinds[j].Group { + return c.Kinds[i].Group < c.Kinds[j].Group + } + if c.Kinds[i].Version != c.Kinds[j].Version { + return c.Kinds[i].Version < c.Kinds[j].Version + } + if c.Kinds[i].Kind != c.Kinds[j].Kind { + return c.Kinds[i].Kind < c.Kinds[j].Kind + } + return false + }) + + // Fuzz any missing objects + if c.FuzzedObjects == nil { + c.FuzzedObjects = map[schema.GroupVersionKind]runtime.Object{} + } + for _, gvk := range c.Kinds { + if _, ok := c.FuzzedObjects[gvk]; ok { + continue + } + obj, err := CompatibilityTestObject(c.Scheme, gvk, c.FuzzFuncs) + if err != nil { + t.Fatal(err) + } + c.FuzzedObjects[gvk] = obj + } + + if c.JSON == nil { + c.JSON = json.NewSerializer(json.DefaultMetaFactory, c.Scheme, c.Scheme, true) + } + if c.YAML == nil { + c.YAML = json.NewYAMLSerializer(json.DefaultMetaFactory, c.Scheme, c.Scheme) + } + if c.Proto == nil { + c.Proto = protobuf.NewSerializer(c.Scheme, c.Scheme) + } + + return c +} + +// CompatibilityTestObject returns a deterministically fuzzed object for the specified GVK +func CompatibilityTestObject(scheme *runtime.Scheme, gvk schema.GroupVersionKind, fuzzFuncs []interface{}) (runtime.Object, error) { + // Construct the object + obj, err := scheme.New(gvk) + if err != nil { + return nil, err + } + + // Fuzz it + CompatibilityTestFuzzer(scheme, fuzzFuncs).Fuzz(obj) + + // Set the kind and apiVersion + if typeAcc, err := apimeta.TypeAccessor(obj); err != nil { + return nil, err + } else { + typeAcc.SetKind(gvk.Kind) + typeAcc.SetAPIVersion(gvk.GroupVersion().String()) + } + + return obj, nil +} + +// CompatibilityTestFuzzer returns a fuzzer for the given scheme: +// - fixed seed (deterministic output that lets us generate the same fixtures on every run) +// - 0 nil chance (populate all fields) +// - 1 numelements (populate and bound all lists) +// - 20 max depth (don't recurse infinitely) +// - meta fuzzing functions added +// - custom fuzzing functions to make strings and managedFields more readable in fixtures +func CompatibilityTestFuzzer(scheme *runtime.Scheme, fuzzFuncs []interface{}) *fuzz.Fuzzer { + fuzzer := fuzz.NewWithSeed(0).NilChance(0).NumElements(1, 1).MaxDepth(20) + fuzzer = fuzzer.Funcs(genericfuzzer.Funcs(serializer.NewCodecFactory(scheme))...) + fuzzString := 1 + fuzzIntOrString := 1 + fuzzMicroTime := int64(1) + fuzzer.Funcs( + // avoid crazy strings + func(s *string, c fuzz.Continue) { + fuzzString++ + *s = strconv.Itoa(fuzzString) + }, + func(i **intstr.IntOrString, c fuzz.Continue) { + fuzzIntOrString++ + tmp := intstr.FromInt(fuzzIntOrString) + _ = tmp + *i = &tmp + }, + func(t **metav1.MicroTime, c fuzz.Continue) { + if t != nil && *t != nil { + // use type-defined fuzzing for non-nil objects + (*t).Fuzz(c) + return + } + fuzzMicroTime++ + tmp := metav1.NewMicroTime(time.Unix(fuzzMicroTime, 0)) + *t = &tmp + }, + // limit managed fields to two levels + func(f *[]metav1.ManagedFieldsEntry, c fuzz.Continue) { + field := metav1.ManagedFieldsEntry{} + c.Fuzz(&field) + if field.FieldsV1 != nil { + field.FieldsV1.Raw = []byte("{}") + } + *f = []metav1.ManagedFieldsEntry{field} + }, + func(r *runtime.RawExtension, c fuzz.Continue) { + // generate a raw object in normalized form + // TODO: test non-normalized round-tripping... YAMLToJSON normalizes and makes exact comparisons fail + r.Raw = []byte(`{"apiVersion":"example.com/v1","kind":"CustomType","spec":{"replicas":1},"status":{"available":1}}`) + }, + ) + fuzzer.Funcs(fuzzFuncs...) + return fuzzer +} + +func (c *CompatibilityTestOptions) Run(t *testing.T) { + for _, gvk := range c.Kinds { + t.Run(makeName(gvk), func(t *testing.T) { + + t.Run("HEAD", func(t *testing.T) { + c.runCurrentVersionTest(t, gvk) + }) + + for _, previousVersionDir := range c.TestDataDirsPreviousVersions { + t.Run(filepath.Base(previousVersionDir), func(t *testing.T) { + c.runPreviousVersionTest(t, gvk, previousVersionDir) + }) + } + + }) + } +} + +func (c *CompatibilityTestOptions) runCurrentVersionTest(t *testing.T, gvk schema.GroupVersionKind) { + expectedObject := c.FuzzedObjects[gvk] + expectedJSON, expectedYAML, expectedProto := c.encode(t, expectedObject) + + actualJSON, actualYAML, actualProto, err := read(c.TestDataDirCurrentVersion, gvk, "") + if err != nil && !os.IsNotExist(err) { + t.Fatal(err) + } + + needsUpdate := false + if os.IsNotExist(err) { + t.Errorf("current version compatibility files did not exist: %v", err) + needsUpdate = true + } else { + if !bytes.Equal(expectedJSON, actualJSON) { + t.Errorf("json differs") + t.Log(cmp.Diff(string(actualJSON), string(expectedJSON))) + needsUpdate = true + } + + if !bytes.Equal(expectedYAML, actualYAML) { + t.Errorf("yaml differs") + t.Log(cmp.Diff(string(actualYAML), string(expectedYAML))) + needsUpdate = true + } + + if !bytes.Equal(expectedProto, actualProto) { + t.Errorf("proto differs") + needsUpdate = true + t.Log(cmp.Diff(dumpProto(t, actualProto[4:]), dumpProto(t, expectedProto[4:]))) + // t.Logf("json (for locating the offending field based on surrounding data): %s", string(expectedJSON)) + } + } + + if needsUpdate { + const updateEnvVar = "UPDATE_COMPATIBILITY_FIXTURE_DATA" + if os.Getenv(updateEnvVar) == "true" { + writeFile(t, c.TestDataDirCurrentVersion, gvk, "", "json", expectedJSON) + writeFile(t, c.TestDataDirCurrentVersion, gvk, "", "yaml", expectedYAML) + writeFile(t, c.TestDataDirCurrentVersion, gvk, "", "pb", expectedProto) + t.Logf("wrote expected compatibility data... verify, commit, and rerun tests") + } else { + t.Logf("if the diff is expected because of a new type or a new field, re-run with %s=true to update the compatibility data", updateEnvVar) + } + return + } + + emptyObj, err := c.Scheme.New(gvk) + if err != nil { + t.Fatal(err) + } + { + jsonDecoded := emptyObj.DeepCopyObject() + jsonDecoded, _, err = c.JSON.Decode(actualJSON, &gvk, jsonDecoded) + if err != nil { + t.Error(err) + } else if !apiequality.Semantic.DeepEqual(expectedObject, jsonDecoded) { + t.Errorf("expected and decoded json objects differed:\n%s", cmp.Diff(expectedObject, jsonDecoded)) + } + } + { + yamlDecoded := emptyObj.DeepCopyObject() + yamlDecoded, _, err = c.YAML.Decode(actualYAML, &gvk, yamlDecoded) + if err != nil { + t.Error(err) + } else if !apiequality.Semantic.DeepEqual(expectedObject, yamlDecoded) { + t.Errorf("expected and decoded yaml objects differed:\n%s", cmp.Diff(expectedObject, yamlDecoded)) + } + } + { + protoDecoded := emptyObj.DeepCopyObject() + protoDecoded, _, err = c.Proto.Decode(actualProto, &gvk, protoDecoded) + if err != nil { + t.Error(err) + } else if !apiequality.Semantic.DeepEqual(expectedObject, protoDecoded) { + t.Errorf("expected and decoded proto objects differed:\n%s", cmp.Diff(expectedObject, protoDecoded)) + } + } +} + +func (c *CompatibilityTestOptions) encode(t *testing.T, obj runtime.Object) (json, yaml, proto []byte) { + jsonBytes := bytes.NewBuffer(nil) + if err := c.JSON.Encode(obj, jsonBytes); err != nil { + t.Fatalf("error encoding json: %v", err) + } + yamlBytes := bytes.NewBuffer(nil) + if err := c.YAML.Encode(obj, yamlBytes); err != nil { + t.Fatalf("error encoding yaml: %v", err) + } + protoBytes := bytes.NewBuffer(nil) + if err := c.Proto.Encode(obj, protoBytes); err != nil { + t.Fatalf("error encoding proto: %v", err) + } + return jsonBytes.Bytes(), yamlBytes.Bytes(), protoBytes.Bytes() +} + +func read(dir string, gvk schema.GroupVersionKind, suffix string) (json, yaml, proto []byte, err error) { + actualJSON, jsonErr := ioutil.ReadFile(filepath.Join(dir, makeName(gvk)+suffix+".json")) + actualYAML, yamlErr := ioutil.ReadFile(filepath.Join(dir, makeName(gvk)+suffix+".yaml")) + actualProto, protoErr := ioutil.ReadFile(filepath.Join(dir, makeName(gvk)+suffix+".pb")) + if jsonErr != nil { + return actualJSON, actualYAML, actualProto, jsonErr + } + if yamlErr != nil { + return actualJSON, actualYAML, actualProto, yamlErr + } + if protoErr != nil { + return actualJSON, actualYAML, actualProto, protoErr + } + return actualJSON, actualYAML, actualProto, nil +} + +func writeFile(t *testing.T, dir string, gvk schema.GroupVersionKind, suffix, extension string, data []byte) { + if err := os.MkdirAll(dir, os.FileMode(0755)); err != nil { + t.Fatal("error making directory", err) + } + if err := ioutil.WriteFile(filepath.Join(dir, makeName(gvk)+suffix+"."+extension), data, os.FileMode(0644)); err != nil { + t.Fatalf("error writing %s: %v", extension, err) + } +} + +func (c *CompatibilityTestOptions) runPreviousVersionTest(t *testing.T, gvk schema.GroupVersionKind, previousVersionDir string) { + jsonBeforeRoundTrip, yamlBeforeRoundTrip, protoBeforeRoundTrip, err := read(previousVersionDir, gvk, "") + if os.IsNotExist(err) || (len(jsonBeforeRoundTrip) == 0 && len(yamlBeforeRoundTrip) == 0 && len(protoBeforeRoundTrip) == 0) { + t.SkipNow() + return + } + if err != nil { + t.Fatal(err) + } + + emptyObj, err := c.Scheme.New(gvk) + if err != nil { + t.Fatal(err) + } + + jsonDecoded := emptyObj.DeepCopyObject() + jsonDecoded, _, err = c.JSON.Decode(jsonBeforeRoundTrip, &gvk, jsonDecoded) + if err != nil { + t.Fatal(err) + } + jsonBytes := bytes.NewBuffer(nil) + if err := c.JSON.Encode(jsonDecoded, jsonBytes); err != nil { + t.Fatalf("error encoding json: %v", err) + } + jsonAfterRoundTrip := jsonBytes.Bytes() + + yamlDecoded := emptyObj.DeepCopyObject() + yamlDecoded, _, err = c.YAML.Decode(yamlBeforeRoundTrip, &gvk, yamlDecoded) + if err != nil { + t.Fatal(err) + } else if !apiequality.Semantic.DeepEqual(jsonDecoded, yamlDecoded) { + t.Errorf("decoded json and yaml objects differ:\n%s", cmp.Diff(jsonDecoded, yamlDecoded)) + } + yamlBytes := bytes.NewBuffer(nil) + if err := c.YAML.Encode(yamlDecoded, yamlBytes); err != nil { + t.Fatalf("error encoding yaml: %v", err) + } + yamlAfterRoundTrip := yamlBytes.Bytes() + + protoDecoded := emptyObj.DeepCopyObject() + protoDecoded, _, err = c.Proto.Decode(protoBeforeRoundTrip, &gvk, protoDecoded) + if err != nil { + t.Fatal(err) + } else if !apiequality.Semantic.DeepEqual(jsonDecoded, protoDecoded) { + t.Errorf("decoded json and proto objects differ:\n%s", cmp.Diff(jsonDecoded, protoDecoded)) + } + protoBytes := bytes.NewBuffer(nil) + if err := c.Proto.Encode(protoDecoded, protoBytes); err != nil { + t.Fatalf("error encoding proto: %v", err) + } + protoAfterRoundTrip := protoBytes.Bytes() + + expectedJSONAfterRoundTrip, expectedYAMLAfterRoundTrip, expectedProtoAfterRoundTrip, _ := read(previousVersionDir, gvk, ".after_roundtrip") + if len(expectedJSONAfterRoundTrip) == 0 { + expectedJSONAfterRoundTrip = jsonBeforeRoundTrip + } + if len(expectedYAMLAfterRoundTrip) == 0 { + expectedYAMLAfterRoundTrip = yamlBeforeRoundTrip + } + if len(expectedProtoAfterRoundTrip) == 0 { + expectedProtoAfterRoundTrip = protoBeforeRoundTrip + } + + jsonNeedsUpdate := false + yamlNeedsUpdate := false + protoNeedsUpdate := false + + if !bytes.Equal(expectedJSONAfterRoundTrip, jsonAfterRoundTrip) { + t.Errorf("json differs") + t.Log(cmp.Diff(string(expectedJSONAfterRoundTrip), string(jsonAfterRoundTrip))) + jsonNeedsUpdate = true + } + + if !bytes.Equal(expectedYAMLAfterRoundTrip, yamlAfterRoundTrip) { + t.Errorf("yaml differs") + t.Log(cmp.Diff(string(expectedYAMLAfterRoundTrip), string(yamlAfterRoundTrip))) + yamlNeedsUpdate = true + } + + if !bytes.Equal(expectedProtoAfterRoundTrip, protoAfterRoundTrip) { + t.Errorf("proto differs") + protoNeedsUpdate = true + t.Log(cmp.Diff(dumpProto(t, expectedProtoAfterRoundTrip[4:]), dumpProto(t, protoAfterRoundTrip[4:]))) + // t.Logf("json (for locating the offending field based on surrounding data): %s", string(expectedJSON)) + } + + if jsonNeedsUpdate || yamlNeedsUpdate || protoNeedsUpdate { + const updateEnvVar = "UPDATE_COMPATIBILITY_FIXTURE_DATA" + if os.Getenv(updateEnvVar) == "true" { + if jsonNeedsUpdate { + writeFile(t, previousVersionDir, gvk, ".after_roundtrip", "json", jsonAfterRoundTrip) + } + if yamlNeedsUpdate { + writeFile(t, previousVersionDir, gvk, ".after_roundtrip", "yaml", yamlAfterRoundTrip) + } + if protoNeedsUpdate { + writeFile(t, previousVersionDir, gvk, ".after_roundtrip", "pb", protoAfterRoundTrip) + } + t.Logf("wrote expected compatibility data... verify, commit, and rerun tests") + } else { + t.Logf("if the diff is expected because of a new type or a new field, re-run with %s=true to update the compatibility data", updateEnvVar) + } + return + } +} + +func makeName(gvk schema.GroupVersionKind) string { + g := gvk.Group + if g == "" { + g = "core" + } + return g + "." + gvk.Version + "." + gvk.Kind +} + +func dumpProto(t *testing.T, data []byte) string { + t.Helper() + protoc, err := exec.LookPath("protoc") + if err != nil { + t.Log(err) + return "" + } + cmd := exec.Command(protoc, "--decode_raw") + cmd.Stdin = bytes.NewBuffer(data) + d, err := cmd.CombinedOutput() + if err != nil { + t.Log(err) + return "" + } + return string(d) +} diff --git a/vendor/k8s.io/apimachinery/pkg/api/apitesting/roundtrip/roundtrip.go b/vendor/k8s.io/apimachinery/pkg/api/apitesting/roundtrip/roundtrip.go new file mode 100644 index 0000000000..f7961043ac --- /dev/null +++ b/vendor/k8s.io/apimachinery/pkg/api/apitesting/roundtrip/roundtrip.go @@ -0,0 +1,438 @@ +/* +Copyright 2017 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package roundtrip + +import ( + "bytes" + "encoding/hex" + "math/rand" + "reflect" + "strings" + "testing" + + "github.com/davecgh/go-spew/spew" + "github.com/golang/protobuf/proto" + fuzz "github.com/google/gofuzz" + flag "github.com/spf13/pflag" + + apitesting "k8s.io/apimachinery/pkg/api/apitesting" + "k8s.io/apimachinery/pkg/api/apitesting/fuzzer" + apiequality "k8s.io/apimachinery/pkg/api/equality" + apimeta "k8s.io/apimachinery/pkg/api/meta" + metafuzzer "k8s.io/apimachinery/pkg/apis/meta/fuzzer" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + runtimeserializer "k8s.io/apimachinery/pkg/runtime/serializer" + "k8s.io/apimachinery/pkg/runtime/serializer/json" + "k8s.io/apimachinery/pkg/runtime/serializer/protobuf" + "k8s.io/apimachinery/pkg/util/diff" + "k8s.io/apimachinery/pkg/util/sets" +) + +type InstallFunc func(scheme *runtime.Scheme) + +// RoundTripTestForAPIGroup is convenient to call from your install package to make sure that a "bare" install of your group provides +// enough information to round trip +func RoundTripTestForAPIGroup(t *testing.T, installFn InstallFunc, fuzzingFuncs fuzzer.FuzzerFuncs) { + scheme := runtime.NewScheme() + installFn(scheme) + + RoundTripTestForScheme(t, scheme, fuzzingFuncs) +} + +// RoundTripTestForScheme is convenient to call if you already have a scheme and want to make sure that its well-formed +func RoundTripTestForScheme(t *testing.T, scheme *runtime.Scheme, fuzzingFuncs fuzzer.FuzzerFuncs) { + codecFactory := runtimeserializer.NewCodecFactory(scheme) + f := fuzzer.FuzzerFor( + fuzzer.MergeFuzzerFuncs(metafuzzer.Funcs, fuzzingFuncs), + rand.NewSource(rand.Int63()), + codecFactory, + ) + RoundTripTypesWithoutProtobuf(t, scheme, codecFactory, f, nil) +} + +// RoundTripProtobufTestForAPIGroup is convenient to call from your install package to make sure that a "bare" install of your group provides +// enough information to round trip +func RoundTripProtobufTestForAPIGroup(t *testing.T, installFn InstallFunc, fuzzingFuncs fuzzer.FuzzerFuncs) { + scheme := runtime.NewScheme() + installFn(scheme) + + RoundTripProtobufTestForScheme(t, scheme, fuzzingFuncs) +} + +// RoundTripProtobufTestForScheme is convenient to call if you already have a scheme and want to make sure that its well-formed +func RoundTripProtobufTestForScheme(t *testing.T, scheme *runtime.Scheme, fuzzingFuncs fuzzer.FuzzerFuncs) { + codecFactory := runtimeserializer.NewCodecFactory(scheme) + fuzzer := fuzzer.FuzzerFor( + fuzzer.MergeFuzzerFuncs(metafuzzer.Funcs, fuzzingFuncs), + rand.NewSource(rand.Int63()), + codecFactory, + ) + RoundTripTypes(t, scheme, codecFactory, fuzzer, nil) +} + +var FuzzIters = flag.Int("fuzz-iters", 20, "How many fuzzing iterations to do.") + +// globalNonRoundTrippableTypes are kinds that are effectively reserved across all GroupVersions +// They don't roundtrip +var globalNonRoundTrippableTypes = sets.NewString( + "ExportOptions", + "GetOptions", + // WatchEvent does not include kind and version and can only be deserialized + // implicitly (if the caller expects the specific object). The watch call defines + // the schema by content type, rather than via kind/version included in each + // object. + "WatchEvent", + // ListOptions is now part of the meta group + "ListOptions", + // Delete options is only read in metav1 + "DeleteOptions", +) + +// GlobalNonRoundTrippableTypes returns the kinds that are effectively reserved across all GroupVersions. +// They don't roundtrip and thus can be excluded in any custom/downstream roundtrip tests +// +// kinds := scheme.AllKnownTypes() +// for gvk := range kinds { +// if roundtrip.GlobalNonRoundTrippableTypes().Has(gvk.Kind) { +// continue +// } +// t.Run(gvk.Group+"."+gvk.Version+"."+gvk.Kind, func(t *testing.T) { +// // roundtrip test +// }) +// } +// +func GlobalNonRoundTrippableTypes() sets.String { + return sets.NewString(globalNonRoundTrippableTypes.List()...) +} + +// RoundTripTypesWithoutProtobuf applies the round-trip test to all round-trippable Kinds +// in the scheme. It will skip all the GroupVersionKinds in the skip list. +func RoundTripTypesWithoutProtobuf(t *testing.T, scheme *runtime.Scheme, codecFactory runtimeserializer.CodecFactory, fuzzer *fuzz.Fuzzer, nonRoundTrippableTypes map[schema.GroupVersionKind]bool) { + roundTripTypes(t, scheme, codecFactory, fuzzer, nonRoundTrippableTypes, true) +} + +func RoundTripTypes(t *testing.T, scheme *runtime.Scheme, codecFactory runtimeserializer.CodecFactory, fuzzer *fuzz.Fuzzer, nonRoundTrippableTypes map[schema.GroupVersionKind]bool) { + roundTripTypes(t, scheme, codecFactory, fuzzer, nonRoundTrippableTypes, false) +} + +func roundTripTypes(t *testing.T, scheme *runtime.Scheme, codecFactory runtimeserializer.CodecFactory, fuzzer *fuzz.Fuzzer, nonRoundTrippableTypes map[schema.GroupVersionKind]bool, skipProtobuf bool) { + for _, group := range groupsFromScheme(scheme) { + t.Logf("starting group %q", group) + internalVersion := schema.GroupVersion{Group: group, Version: runtime.APIVersionInternal} + internalKindToGoType := scheme.KnownTypes(internalVersion) + + for kind := range internalKindToGoType { + if globalNonRoundTrippableTypes.Has(kind) { + continue + } + + internalGVK := internalVersion.WithKind(kind) + roundTripSpecificKind(t, internalGVK, scheme, codecFactory, fuzzer, nonRoundTrippableTypes, skipProtobuf) + } + + t.Logf("finished group %q", group) + } +} + +// RoundTripExternalTypes applies the round-trip test to all external round-trippable Kinds +// in the scheme. It will skip all the GroupVersionKinds in the nonRoundTripExternalTypes list . +func RoundTripExternalTypes(t *testing.T, scheme *runtime.Scheme, codecFactory runtimeserializer.CodecFactory, fuzzer *fuzz.Fuzzer, nonRoundTrippableTypes map[schema.GroupVersionKind]bool) { + kinds := scheme.AllKnownTypes() + for gvk := range kinds { + if gvk.Version == runtime.APIVersionInternal || globalNonRoundTrippableTypes.Has(gvk.Kind) { + continue + } + t.Run(gvk.Group+"."+gvk.Version+"."+gvk.Kind, func(t *testing.T) { + roundTripSpecificKind(t, gvk, scheme, codecFactory, fuzzer, nonRoundTrippableTypes, false) + }) + } +} + +// RoundTripExternalTypesWithoutProtobuf applies the round-trip test to all external round-trippable Kinds +// in the scheme. It will skip all the GroupVersionKinds in the nonRoundTripExternalTypes list. +func RoundTripExternalTypesWithoutProtobuf(t *testing.T, scheme *runtime.Scheme, codecFactory runtimeserializer.CodecFactory, fuzzer *fuzz.Fuzzer, nonRoundTrippableTypes map[schema.GroupVersionKind]bool) { + kinds := scheme.AllKnownTypes() + for gvk := range kinds { + if gvk.Version == runtime.APIVersionInternal || globalNonRoundTrippableTypes.Has(gvk.Kind) { + continue + } + t.Run(gvk.Group+"."+gvk.Version+"."+gvk.Kind, func(t *testing.T) { + roundTripSpecificKind(t, gvk, scheme, codecFactory, fuzzer, nonRoundTrippableTypes, true) + }) + } +} + +func RoundTripSpecificKindWithoutProtobuf(t *testing.T, gvk schema.GroupVersionKind, scheme *runtime.Scheme, codecFactory runtimeserializer.CodecFactory, fuzzer *fuzz.Fuzzer, nonRoundTrippableTypes map[schema.GroupVersionKind]bool) { + roundTripSpecificKind(t, gvk, scheme, codecFactory, fuzzer, nonRoundTrippableTypes, true) +} + +func RoundTripSpecificKind(t *testing.T, gvk schema.GroupVersionKind, scheme *runtime.Scheme, codecFactory runtimeserializer.CodecFactory, fuzzer *fuzz.Fuzzer, nonRoundTrippableTypes map[schema.GroupVersionKind]bool) { + roundTripSpecificKind(t, gvk, scheme, codecFactory, fuzzer, nonRoundTrippableTypes, false) +} + +func roundTripSpecificKind(t *testing.T, gvk schema.GroupVersionKind, scheme *runtime.Scheme, codecFactory runtimeserializer.CodecFactory, fuzzer *fuzz.Fuzzer, nonRoundTrippableTypes map[schema.GroupVersionKind]bool, skipProtobuf bool) { + if nonRoundTrippableTypes[gvk] { + t.Logf("skipping %v", gvk) + return + } + + // Try a few times, since runTest uses random values. + for i := 0; i < *FuzzIters; i++ { + if gvk.Version == runtime.APIVersionInternal { + roundTripToAllExternalVersions(t, scheme, codecFactory, fuzzer, gvk, nonRoundTrippableTypes, skipProtobuf) + } else { + roundTripOfExternalType(t, scheme, codecFactory, fuzzer, gvk, skipProtobuf) + } + if t.Failed() { + break + } + } +} + +// fuzzInternalObject fuzzes an arbitrary runtime object using the appropriate +// fuzzer registered with the apitesting package. +func fuzzInternalObject(t *testing.T, fuzzer *fuzz.Fuzzer, object runtime.Object) runtime.Object { + fuzzer.Fuzz(object) + + j, err := apimeta.TypeAccessor(object) + if err != nil { + t.Fatalf("Unexpected error %v for %#v", err, object) + } + j.SetKind("") + j.SetAPIVersion("") + + return object +} + +func groupsFromScheme(scheme *runtime.Scheme) []string { + ret := sets.String{} + for gvk := range scheme.AllKnownTypes() { + ret.Insert(gvk.Group) + } + return ret.List() +} + +func roundTripToAllExternalVersions(t *testing.T, scheme *runtime.Scheme, codecFactory runtimeserializer.CodecFactory, fuzzer *fuzz.Fuzzer, internalGVK schema.GroupVersionKind, nonRoundTrippableTypes map[schema.GroupVersionKind]bool, skipProtobuf bool) { + object, err := scheme.New(internalGVK) + if err != nil { + t.Fatalf("Couldn't make a %v? %v", internalGVK, err) + } + if _, err := apimeta.TypeAccessor(object); err != nil { + t.Fatalf("%q is not a TypeMeta and cannot be tested - add it to nonRoundTrippableInternalTypes: %v", internalGVK, err) + } + + fuzzInternalObject(t, fuzzer, object) + + // find all potential serializations in the scheme. + // TODO fix this up to handle kinds that cross registered with different names. + for externalGVK, externalGoType := range scheme.AllKnownTypes() { + if externalGVK.Version == runtime.APIVersionInternal { + continue + } + if externalGVK.GroupKind() != internalGVK.GroupKind() { + continue + } + if nonRoundTrippableTypes[externalGVK] { + t.Logf("\tskipping %v %v", externalGVK, externalGoType) + continue + } + t.Logf("\tround tripping to %v %v", externalGVK, externalGoType) + + roundTrip(t, scheme, apitesting.TestCodec(codecFactory, externalGVK.GroupVersion()), object) + + // TODO remove this hack after we're past the intermediate steps + if !skipProtobuf && externalGVK.Group != "kubeadm.k8s.io" { + s := protobuf.NewSerializer(scheme, scheme) + protobufCodec := codecFactory.CodecForVersions(s, s, externalGVK.GroupVersion(), nil) + roundTrip(t, scheme, protobufCodec, object) + } + } +} + +func roundTripOfExternalType(t *testing.T, scheme *runtime.Scheme, codecFactory runtimeserializer.CodecFactory, fuzzer *fuzz.Fuzzer, externalGVK schema.GroupVersionKind, skipProtobuf bool) { + object, err := scheme.New(externalGVK) + if err != nil { + t.Fatalf("Couldn't make a %v? %v", externalGVK, err) + } + typeAcc, err := apimeta.TypeAccessor(object) + if err != nil { + t.Fatalf("%q is not a TypeMeta and cannot be tested - add it to nonRoundTrippableInternalTypes: %v", externalGVK, err) + } + + fuzzInternalObject(t, fuzzer, object) + + typeAcc.SetKind(externalGVK.Kind) + typeAcc.SetAPIVersion(externalGVK.GroupVersion().String()) + + roundTrip(t, scheme, json.NewSerializer(json.DefaultMetaFactory, scheme, scheme, false), object) + + // TODO remove this hack after we're past the intermediate steps + if !skipProtobuf { + roundTrip(t, scheme, protobuf.NewSerializer(scheme, scheme), object) + } +} + +// roundTrip applies a single round-trip test to the given runtime object +// using the given codec. The round-trip test ensures that an object can be +// deep-copied, converted, marshaled and back without loss of data. +// +// For internal types this means +// +// internal -> external -> json/protobuf -> external -> internal. +// +// For external types this means +// +// external -> json/protobuf -> external. +func roundTrip(t *testing.T, scheme *runtime.Scheme, codec runtime.Codec, object runtime.Object) { + printer := spew.ConfigState{DisableMethods: true} + original := object + + // deep copy the original object + object = object.DeepCopyObject() + name := reflect.TypeOf(object).Elem().Name() + if !apiequality.Semantic.DeepEqual(original, object) { + t.Errorf("%v: DeepCopy altered the object, diff: %v", name, diff.ObjectReflectDiff(original, object)) + t.Errorf("%s", spew.Sdump(original)) + t.Errorf("%s", spew.Sdump(object)) + return + } + + // encode (serialize) the deep copy using the provided codec + data, err := runtime.Encode(codec, object) + if err != nil { + if runtime.IsNotRegisteredError(err) { + t.Logf("%v: not registered: %v (%s)", name, err, printer.Sprintf("%#v", object)) + } else { + t.Errorf("%v: %v (%s)", name, err, printer.Sprintf("%#v", object)) + } + return + } + + // ensure that the deep copy is equal to the original; neither the deep + // copy or conversion should alter the object + // TODO eliminate this global + if !apiequality.Semantic.DeepEqual(original, object) { + t.Errorf("%v: encode altered the object, diff: %v", name, diff.ObjectReflectDiff(original, object)) + return + } + + // encode (serialize) a second time to verify that it was not varying + secondData, err := runtime.Encode(codec, object) + if err != nil { + if runtime.IsNotRegisteredError(err) { + t.Logf("%v: not registered: %v (%s)", name, err, printer.Sprintf("%#v", object)) + } else { + t.Errorf("%v: %v (%s)", name, err, printer.Sprintf("%#v", object)) + } + return + } + + // serialization to the wire must be stable to ensure that we don't write twice to the DB + // when the object hasn't changed. + if !bytes.Equal(data, secondData) { + t.Errorf("%v: serialization is not stable: %s", name, printer.Sprintf("%#v", object)) + } + + // decode (deserialize) the encoded data back into an object + obj2, err := runtime.Decode(codec, data) + if err != nil { + t.Errorf("%v: %v\nCodec: %#v\nData: %s\nSource: %#v", name, err, codec, dataAsString(data), printer.Sprintf("%#v", object)) + panic("failed") + } + + // ensure that the object produced from decoding the encoded data is equal + // to the original object + if !apiequality.Semantic.DeepEqual(original, obj2) { + 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)) + return + } + + // decode the encoded data into a new object (instead of letting the codec + // create a new object) + obj3 := reflect.New(reflect.TypeOf(object).Elem()).Interface().(runtime.Object) + if err := runtime.DecodeInto(codec, data, obj3); err != nil { + t.Errorf("%v: %v", name, err) + return + } + + // special case for kinds which are internal and external at the same time (many in meta.k8s.io are). For those + // runtime.DecodeInto above will return the external variant and set the APIVersion and kind, while the input + // object might be internal. Hence, we clear those values for obj3 for that case to correctly compare. + intAndExt, err := internalAndExternalKind(scheme, object) + if err != nil { + t.Errorf("%v: %v", name, err) + return + } + if intAndExt { + typeAcc, err := apimeta.TypeAccessor(object) + if err != nil { + t.Fatalf("%v: error accessing TypeMeta: %v", name, err) + } + if len(typeAcc.GetAPIVersion()) == 0 { + typeAcc, err := apimeta.TypeAccessor(obj3) + if err != nil { + t.Fatalf("%v: error accessing TypeMeta: %v", name, err) + } + typeAcc.SetAPIVersion("") + typeAcc.SetKind("") + } + } + + // ensure that the new runtime object is equal to the original after being + // decoded into + if !apiequality.Semantic.DeepEqual(object, obj3) { + t.Errorf("%v: diff: %v\nCodec: %#v", name, diff.ObjectReflectDiff(object, obj3), codec) + return + } + + // do structure-preserving fuzzing of the deep-copied object. If it shares anything with the original, + // the deep-copy was actually only a shallow copy. Then original and obj3 will be different after fuzzing. + // NOTE: we use the encoding+decoding here as an alternative, guaranteed deep-copy to compare against. + fuzzer.ValueFuzz(object) + if !apiequality.Semantic.DeepEqual(original, obj3) { + t.Errorf("%v: fuzzing a copy altered the original, diff: %v", name, diff.ObjectReflectDiff(original, obj3)) + return + } +} + +func internalAndExternalKind(scheme *runtime.Scheme, object runtime.Object) (bool, error) { + kinds, _, err := scheme.ObjectKinds(object) + if err != nil { + return false, err + } + internal, external := false, false + for _, k := range kinds { + if k.Version == runtime.APIVersionInternal { + internal = true + } else { + external = true + } + } + return internal && external, nil +} + +// dataAsString returns the given byte array as a string; handles detecting +// protocol buffers. +func dataAsString(data []byte) string { + dataString := string(data) + if !strings.HasPrefix(dataString, "{") { + dataString = "\n" + hex.Dump(data) + proto.NewBuffer(make([]byte, 0, 1024)).DebugPrint("decoded object", data) + } + return dataString +} diff --git a/vendor/k8s.io/apimachinery/pkg/apis/meta/fuzzer/fuzzer.go b/vendor/k8s.io/apimachinery/pkg/apis/meta/fuzzer/fuzzer.go new file mode 100644 index 0000000000..68cf673b76 --- /dev/null +++ b/vendor/k8s.io/apimachinery/pkg/apis/meta/fuzzer/fuzzer.go @@ -0,0 +1,331 @@ +/* +Copyright 2017 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package fuzzer + +import ( + "fmt" + "math/rand" + "sort" + "strconv" + "strings" + + fuzz "github.com/google/gofuzz" + + apitesting "k8s.io/apimachinery/pkg/api/apitesting" + "k8s.io/apimachinery/pkg/api/apitesting/fuzzer" + "k8s.io/apimachinery/pkg/api/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + metav1beta1 "k8s.io/apimachinery/pkg/apis/meta/v1beta1" + "k8s.io/apimachinery/pkg/runtime" + runtimeserializer "k8s.io/apimachinery/pkg/runtime/serializer" + "k8s.io/apimachinery/pkg/types" +) + +func genericFuzzerFuncs(codecs runtimeserializer.CodecFactory) []interface{} { + return []interface{}{ + func(q *resource.Quantity, c fuzz.Continue) { + *q = *resource.NewQuantity(c.Int63n(1000), resource.DecimalExponent) + }, + func(j *int, c fuzz.Continue) { + *j = int(c.Int31()) + }, + func(j **int, c fuzz.Continue) { + if c.RandBool() { + i := int(c.Int31()) + *j = &i + } else { + *j = nil + } + }, + func(j *runtime.TypeMeta, c fuzz.Continue) { + // We have to customize the randomization of TypeMetas because their + // APIVersion and Kind must remain blank in memory. + j.APIVersion = "" + j.Kind = "" + }, + func(j *runtime.Object, c fuzz.Continue) { + // TODO: uncomment when round trip starts from a versioned object + if true { //c.RandBool() { + *j = &runtime.Unknown{ + // We do not set TypeMeta here because it is not carried through a round trip + Raw: []byte(`{"apiVersion":"unknown.group/unknown","kind":"Something","someKey":"someValue"}`), + ContentType: runtime.ContentTypeJSON, + } + } else { + types := []runtime.Object{&metav1.Status{}, &metav1.APIGroup{}} + t := types[c.Rand.Intn(len(types))] + c.Fuzz(t) + *j = t + } + }, + func(r *runtime.RawExtension, c fuzz.Continue) { + // Pick an arbitrary type and fuzz it + types := []runtime.Object{&metav1.Status{}, &metav1.APIGroup{}} + obj := types[c.Rand.Intn(len(types))] + c.Fuzz(obj) + + // Find a codec for converting the object to raw bytes. This is necessary for the + // api version and kind to be correctly set be serialization. + var codec = apitesting.TestCodec(codecs, metav1.SchemeGroupVersion) + + // Convert the object to raw bytes + bytes, err := runtime.Encode(codec, obj) + if err != nil { + panic(fmt.Sprintf("Failed to encode object: %v", err)) + } + + // strip trailing newlines which do not survive roundtrips + for len(bytes) >= 1 && bytes[len(bytes)-1] == 10 { + bytes = bytes[:len(bytes)-1] + } + + // Set the bytes field on the RawExtension + r.Raw = bytes + }, + } +} + +// taken from gofuzz internals for RandString +type charRange struct { + first, last rune +} + +func (c *charRange) choose(r *rand.Rand) rune { + count := int64(c.last - c.first + 1) + ch := c.first + rune(r.Int63n(count)) + + return ch +} + +// randomLabelPart produces a valid random label value or name-part +// of a label key. +func randomLabelPart(c fuzz.Continue, canBeEmpty bool) string { + validStartEnd := []charRange{{'0', '9'}, {'a', 'z'}, {'A', 'Z'}} + validMiddle := []charRange{{'0', '9'}, {'a', 'z'}, {'A', 'Z'}, + {'.', '.'}, {'-', '-'}, {'_', '_'}} + + partLen := c.Rand.Intn(64) // len is [0, 63] + if !canBeEmpty { + partLen = c.Rand.Intn(63) + 1 // len is [1, 63] + } + + runes := make([]rune, partLen) + if partLen == 0 { + return string(runes) + } + + runes[0] = validStartEnd[c.Rand.Intn(len(validStartEnd))].choose(c.Rand) + for i := range runes[1:] { + runes[i+1] = validMiddle[c.Rand.Intn(len(validMiddle))].choose(c.Rand) + } + runes[len(runes)-1] = validStartEnd[c.Rand.Intn(len(validStartEnd))].choose(c.Rand) + + return string(runes) +} + +func randomDNSLabel(c fuzz.Continue) string { + validStartEnd := []charRange{{'0', '9'}, {'a', 'z'}} + validMiddle := []charRange{{'0', '9'}, {'a', 'z'}, {'-', '-'}} + + partLen := c.Rand.Intn(63) + 1 // len is [1, 63] + runes := make([]rune, partLen) + + runes[0] = validStartEnd[c.Rand.Intn(len(validStartEnd))].choose(c.Rand) + for i := range runes[1:] { + runes[i+1] = validMiddle[c.Rand.Intn(len(validMiddle))].choose(c.Rand) + } + runes[len(runes)-1] = validStartEnd[c.Rand.Intn(len(validStartEnd))].choose(c.Rand) + + return string(runes) +} + +func randomLabelKey(c fuzz.Continue) string { + namePart := randomLabelPart(c, false) + prefixPart := "" + + usePrefix := c.RandBool() + if usePrefix { + // we can fit, with dots, at most 3 labels in the 253 allotted characters + prefixPartsLen := c.Rand.Intn(2) + 1 + prefixParts := make([]string, prefixPartsLen) + for i := range prefixParts { + prefixParts[i] = randomDNSLabel(c) + } + prefixPart = strings.Join(prefixParts, ".") + "/" + } + + return prefixPart + namePart +} + +func v1FuzzerFuncs(codecs runtimeserializer.CodecFactory) []interface{} { + + return []interface{}{ + func(j *metav1.TypeMeta, c fuzz.Continue) { + // We have to customize the randomization of TypeMetas because their + // APIVersion and Kind must remain blank in memory. + j.APIVersion = "" + j.Kind = "" + }, + func(j *metav1.ObjectMeta, c fuzz.Continue) { + c.FuzzNoCustom(j) + + j.ResourceVersion = strconv.FormatUint(c.RandUint64(), 10) + j.UID = types.UID(c.RandString()) + + var sec, nsec int64 + c.Fuzz(&sec) + c.Fuzz(&nsec) + j.CreationTimestamp = metav1.Unix(sec, nsec).Rfc3339Copy() + + if j.DeletionTimestamp != nil { + c.Fuzz(&sec) + c.Fuzz(&nsec) + t := metav1.Unix(sec, nsec).Rfc3339Copy() + j.DeletionTimestamp = &t + } + + if len(j.Labels) == 0 { + j.Labels = nil + } else { + delete(j.Labels, "") + } + if len(j.Annotations) == 0 { + j.Annotations = nil + } else { + delete(j.Annotations, "") + } + if len(j.OwnerReferences) == 0 { + j.OwnerReferences = nil + } + if len(j.Finalizers) == 0 { + j.Finalizers = nil + } + }, + func(j *metav1.ListMeta, c fuzz.Continue) { + j.ResourceVersion = strconv.FormatUint(c.RandUint64(), 10) + j.SelfLink = c.RandString() + }, + func(j *metav1.LabelSelector, c fuzz.Continue) { + c.FuzzNoCustom(j) + // we can't have an entirely empty selector, so force + // use of MatchExpression if necessary + if len(j.MatchLabels) == 0 && len(j.MatchExpressions) == 0 { + j.MatchExpressions = make([]metav1.LabelSelectorRequirement, c.Rand.Intn(2)+1) + } + + if j.MatchLabels != nil { + fuzzedMatchLabels := make(map[string]string, len(j.MatchLabels)) + for i := 0; i < len(j.MatchLabels); i++ { + fuzzedMatchLabels[randomLabelKey(c)] = randomLabelPart(c, true) + } + j.MatchLabels = fuzzedMatchLabels + } + + validOperators := []metav1.LabelSelectorOperator{ + metav1.LabelSelectorOpIn, + metav1.LabelSelectorOpNotIn, + metav1.LabelSelectorOpExists, + metav1.LabelSelectorOpDoesNotExist, + } + + if j.MatchExpressions != nil { + // NB: the label selector parser code sorts match expressions by key, and sorts the values, + // so we need to make sure ours are sorted as well here to preserve round-trip comparison. + // In practice, not sorting doesn't hurt anything... + + for i := range j.MatchExpressions { + req := metav1.LabelSelectorRequirement{} + c.Fuzz(&req) + req.Key = randomLabelKey(c) + req.Operator = validOperators[c.Rand.Intn(len(validOperators))] + if req.Operator == metav1.LabelSelectorOpIn || req.Operator == metav1.LabelSelectorOpNotIn { + if len(req.Values) == 0 { + // we must have some values here, so randomly choose a short length + req.Values = make([]string, c.Rand.Intn(2)+1) + } + for i := range req.Values { + req.Values[i] = randomLabelPart(c, true) + } + sort.Strings(req.Values) + } else { + req.Values = nil + } + j.MatchExpressions[i] = req + } + + sort.Slice(j.MatchExpressions, func(a, b int) bool { return j.MatchExpressions[a].Key < j.MatchExpressions[b].Key }) + } + }, + func(j *metav1.ManagedFieldsEntry, c fuzz.Continue) { + c.FuzzNoCustom(j) + j.FieldsV1 = nil + }, + } +} + +func v1beta1FuzzerFuncs(codecs runtimeserializer.CodecFactory) []interface{} { + return []interface{}{ + func(r *metav1beta1.TableOptions, c fuzz.Continue) { + c.FuzzNoCustom(r) + // NoHeaders is not serialized to the wire but is allowed within the versioned + // type because we don't use meta internal types in the client and API server. + r.NoHeaders = false + }, + func(r *metav1beta1.TableRow, c fuzz.Continue) { + c.Fuzz(&r.Object) + c.Fuzz(&r.Conditions) + if len(r.Conditions) == 0 { + r.Conditions = nil + } + n := c.Intn(10) + if n > 0 { + r.Cells = make([]interface{}, n) + } + for i := range r.Cells { + t := c.Intn(6) + switch t { + case 0: + r.Cells[i] = c.RandString() + case 1: + r.Cells[i] = c.Int63() + case 2: + r.Cells[i] = c.RandBool() + case 3: + x := map[string]interface{}{} + for j := c.Intn(10) + 1; j >= 0; j-- { + x[c.RandString()] = c.RandString() + } + r.Cells[i] = x + case 4: + x := make([]interface{}, c.Intn(10)) + for i := range x { + x[i] = c.Int63() + } + r.Cells[i] = x + default: + r.Cells[i] = nil + } + } + }, + } +} + +var Funcs = fuzzer.MergeFuzzerFuncs( + genericFuzzerFuncs, + v1FuzzerFuncs, + v1beta1FuzzerFuncs, +) diff --git a/vendor/knative.dev/pkg/apis/testing/roundtrip/roundtrip.go b/vendor/knative.dev/pkg/apis/testing/roundtrip/roundtrip.go new file mode 100644 index 0000000000..1bf34e8c23 --- /dev/null +++ b/vendor/knative.dev/pkg/apis/testing/roundtrip/roundtrip.go @@ -0,0 +1,256 @@ +/* +Copyright 2020 The Knative Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package roundtrip + +import ( + "context" + "math/rand" + "net/url" + "reflect" + "regexp" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + fuzz "github.com/google/gofuzz" + "k8s.io/apimachinery/pkg/api/apitesting/fuzzer" + "k8s.io/apimachinery/pkg/api/apitesting/roundtrip" + apiequality "k8s.io/apimachinery/pkg/api/equality" + apimeta "k8s.io/apimachinery/pkg/api/meta" + metafuzzer "k8s.io/apimachinery/pkg/apis/meta/fuzzer" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/runtime/serializer" + "k8s.io/apimachinery/pkg/util/sets" + "knative.dev/pkg/apis" +) + +type convertibleObject interface { + runtime.Object + apis.Convertible +} + +// globalNonRoundTrippableTypes are kinds that are effectively reserved +// across all GroupVersions. They don't roundtrip +// +// This list comes from k8s.io/apimachinery. We can drop this constant when +// the PR (https://github.com/kubernetes/kubernetes/pull/86959) merges and +// we bump to a version that has the change +var globalNonRoundTrippableTypes = sets.NewString( + "ExportOptions", + "GetOptions", + // WatchEvent does not include kind and version and can only be deserialized + // implicitly (if the caller expects the specific object). The watch call defines + // the schema by content type, rather than via kind/version included in each + // object. + "WatchEvent", + // ListOptions is now part of the meta group + "ListOptions", + // Delete options is only read in metav1 + "DeleteOptions", +) + +var ( + metaV1Types map[reflect.Type]struct{} + metaV1ListType = reflect.TypeOf((*metav1.ListMetaAccessor)(nil)).Elem() +) + +func init() { + gv := schema.GroupVersion{Group: "roundtrip.group", Version: "v1"} + + scheme := runtime.NewScheme() + + metav1.AddToGroupVersion(scheme, gv) + metaV1Types = make(map[reflect.Type]struct{}) + + // Build up a list of types to ignore + for _, t := range scheme.KnownTypes(gv) { + metaV1Types[t] = struct{}{} + } +} + +// ExternalTypesViaJSON applies the round-trip test to all external round-trippable Kinds +// in the scheme. This is effectively testing the scenario: +// +// external -> json -> external +// +func ExternalTypesViaJSON(t *testing.T, scheme *runtime.Scheme, fuzzerFuncs fuzzer.FuzzerFuncs) { + codecFactory := serializer.NewCodecFactory(scheme) + + f := fuzzer.FuzzerFor( + fuzzer.MergeFuzzerFuncs(metafuzzer.Funcs, fuzzerFuncs), + rand.NewSource(rand.Int63()), + codecFactory, + ) + + f.SkipFieldsWithPattern(regexp.MustCompile("DeprecatedGeneration")) + + kinds := scheme.AllKnownTypes() + for gvk := range kinds { + if gvk.Version == runtime.APIVersionInternal || globalNonRoundTrippableTypes.Has(gvk.Kind) { + continue + } + t.Run(gvk.Group+"."+gvk.Version+"."+gvk.Kind, func(t *testing.T) { + roundtrip.RoundTripSpecificKindWithoutProtobuf(t, gvk, scheme, codecFactory, f, nil) + }) + } +} + +// ExternalTypesViaHub applies the round-trip test to all external round-trippable Kinds +// in the scheme. This is effectively testing the scenario: +// +// external version -> hub version -> external version +// +func ExternalTypesViaHub(t *testing.T, scheme, hubs *runtime.Scheme, fuzzerFuncs fuzzer.FuzzerFuncs) { + f := fuzzer.FuzzerFor( + fuzzer.MergeFuzzerFuncs(metafuzzer.Funcs, fuzzerFuncs), + rand.NewSource(rand.Int63()), + // This seems to be used for protobuf not json + serializer.NewCodecFactory(scheme), + ) + + f.SkipFieldsWithPattern(regexp.MustCompile("DeprecatedGeneration")) + + for gvk, objType := range scheme.AllKnownTypes() { + if gvk.Version == runtime.APIVersionInternal || + gvk.Group == "" || // K8s group + globalNonRoundTrippableTypes.Has(gvk.Kind) { + continue + } + + if _, ok := metaV1Types[objType]; ok { + continue + } + + if reflect.PtrTo(objType).AssignableTo(metaV1ListType) { + continue + } + + if hubs.Recognizes(gvk) { + continue + } + + t.Run(gvk.Group+"."+gvk.Version+"."+gvk.Kind, func(t *testing.T) { + for i := 0; i < *roundtrip.FuzzIters; i++ { + roundTripViaHub(t, gvk, scheme, hubs, f) + + if t.Failed() { + break + } + } + }) + } +} + +func roundTripViaHub(t *testing.T, gvk schema.GroupVersionKind, scheme, hubs *runtime.Scheme, f *fuzz.Fuzzer) { + ctx := context.Background() + + hub, hubGVK := hubInstanceForGK(t, hubs, gvk.GroupKind()) + obj := objForGVK(t, gvk, scheme) + + fuzzObject(t, f, gvk, obj) + + original := obj + obj = obj.DeepCopyObject().(convertibleObject) + + if !apiequality.Semantic.DeepEqual(original, obj) { + t.Errorf("DeepCopy modified the original object (DeepCopy should not have side-effects), diff: %v", diff(original, obj)) + return + } + + if err := hub.ConvertFrom(ctx, obj); err != nil { + t.Errorf("Conversion to hub (%s) failed: %s", hubGVK, err) + } + + if !apiequality.Semantic.DeepEqual(original, obj) { + t.Errorf("Conversion to hub (%s) modified the original object (ConvertFrom should not have side-effects), diff: %v", hubGVK, diff(original, obj)) + return + } + + newObj := objForGVK(t, gvk, scheme) + if err := hub.ConvertTo(ctx, newObj); err != nil { + t.Errorf("Conversion from hub (%s) failed: %s", hubGVK, err) + t.Errorf("object: %#v", obj) + return + } + + if !apiequality.Semantic.DeepEqual(obj, newObj) { + t.Errorf("round trip through hub (%s) produced a diff: %s", hubGVK, diff(original, newObj)) + return + } +} + +func diff(obj1, obj2 interface{}) string { + // knative.dev/pkg/apis.URL is an alias to net.URL which embeds a + // url.Userinfo that has an unexported field + return cmp.Diff(obj1, obj2, cmpopts.IgnoreUnexported(url.Userinfo{})) +} + +func objForGVK(t *testing.T, + gvk schema.GroupVersionKind, + scheme *runtime.Scheme, +) convertibleObject { + + t.Helper() + + obj, err := scheme.New(gvk) + if err != nil { + t.Fatalf("unable to create object instance for type %s", err) + } + + objType, err := apimeta.TypeAccessor(obj) + if err != nil { + t.Fatalf("%q is not a TypeMeta and cannot be tested: %v", gvk, err) + } + objType.SetKind(gvk.Kind) + objType.SetAPIVersion(gvk.GroupVersion().String()) + return obj.(convertibleObject) +} + +func fuzzObject(t *testing.T, fuzzer *fuzz.Fuzzer, gvk schema.GroupVersionKind, obj interface{}) { + fuzzer.Fuzz(obj) + + objType, err := apimeta.TypeAccessor(obj) + if err != nil { + t.Fatalf("%q is not a TypeMeta and cannot be tested: %v", gvk, err) + } + objType.SetKind(gvk.Kind) + objType.SetAPIVersion(gvk.GroupVersion().String()) +} + +func hubInstanceForGK(t *testing.T, + hubs *runtime.Scheme, + gk schema.GroupKind, +) (apis.Convertible, schema.GroupVersionKind) { + + t.Helper() + + for hubGVK := range hubs.AllKnownTypes() { + if hubGVK.GroupKind() == gk { + obj, err := hubs.New(hubGVK) + if err != nil { + t.Fatalf("error creating objects %s", err) + } + + return obj.(apis.Convertible), hubGVK + } + } + + t.Fatalf("hub type not found") + return nil, schema.GroupVersionKind{} +} diff --git a/vendor/modules.txt b/vendor/modules.txt index 3eb44705bf..cac7917808 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -170,6 +170,7 @@ github.com/google/go-github/v27/github # github.com/google/go-querystring v1.0.0 github.com/google/go-querystring/query # github.com/google/gofuzz v1.1.0 +## explicit github.com/google/gofuzz # github.com/google/mako v0.0.0-20190821191249-122f8dcef9e3 github.com/google/mako/clients/proto/analyzers/threshold_analyzer_go_proto @@ -722,12 +723,15 @@ k8s.io/apiextensions-apiserver/pkg/client/listers/apiextensions/v1 k8s.io/apiextensions-apiserver/pkg/client/listers/apiextensions/v1beta1 # k8s.io/apimachinery v0.18.8 => k8s.io/apimachinery v0.18.8 ## explicit +k8s.io/apimachinery/pkg/api/apitesting k8s.io/apimachinery/pkg/api/apitesting/fuzzer +k8s.io/apimachinery/pkg/api/apitesting/roundtrip k8s.io/apimachinery/pkg/api/equality k8s.io/apimachinery/pkg/api/errors k8s.io/apimachinery/pkg/api/meta k8s.io/apimachinery/pkg/api/resource k8s.io/apimachinery/pkg/api/validation +k8s.io/apimachinery/pkg/apis/meta/fuzzer k8s.io/apimachinery/pkg/apis/meta/internalversion k8s.io/apimachinery/pkg/apis/meta/v1 k8s.io/apimachinery/pkg/apis/meta/v1/unstructured @@ -1182,6 +1186,7 @@ knative.dev/pkg/apis/duck/v1alpha1 knative.dev/pkg/apis/duck/v1beta1 knative.dev/pkg/apis/testing knative.dev/pkg/apis/testing/fuzzer +knative.dev/pkg/apis/testing/roundtrip knative.dev/pkg/changeset knative.dev/pkg/client/injection/apiextensions/client knative.dev/pkg/client/injection/apiextensions/informers/apiextensions/v1/customresourcedefinition