knative.dev/pkg@v0.0.0-20260602142205-ac97e43f6622/apis/testing/roundtrip/roundtrip.go (about) 1 /* 2 Copyright 2020 The Knative 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 "context" 21 "math/rand" 22 "net/url" 23 "reflect" 24 "regexp" 25 "testing" 26 27 "github.com/google/go-cmp/cmp" 28 "github.com/google/go-cmp/cmp/cmpopts" 29 "k8s.io/apimachinery/pkg/api/apitesting/fuzzer" 30 "k8s.io/apimachinery/pkg/api/apitesting/roundtrip" 31 apiequality "k8s.io/apimachinery/pkg/api/equality" 32 apimeta "k8s.io/apimachinery/pkg/api/meta" 33 metafuzzer "k8s.io/apimachinery/pkg/apis/meta/fuzzer" 34 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 35 "k8s.io/apimachinery/pkg/runtime" 36 "k8s.io/apimachinery/pkg/runtime/schema" 37 "k8s.io/apimachinery/pkg/runtime/serializer" 38 "k8s.io/apimachinery/pkg/util/sets" 39 "sigs.k8s.io/randfill" 40 41 "knative.dev/pkg/apis" 42 ) 43 44 type convertibleObject interface { 45 runtime.Object 46 apis.Convertible 47 } 48 49 // globalNonRoundTrippableTypes are kinds that are effectively reserved 50 // across all GroupVersions. They don't roundtrip 51 // 52 // This list comes from k8s.io/apimachinery. We can drop this constant when 53 // the PR (https://github.com/kubernetes/kubernetes/pull/86959) merges and 54 // we bump to a version that has the change 55 var globalNonRoundTrippableTypes = sets.NewString( 56 "ExportOptions", 57 "GetOptions", 58 // WatchEvent does not include kind and version and can only be deserialized 59 // implicitly (if the caller expects the specific object). The watch call defines 60 // the schema by content type, rather than via kind/version included in each 61 // object. 62 "WatchEvent", 63 // ListOptions is now part of the meta group 64 "ListOptions", 65 // Delete options is only read in metav1 66 "DeleteOptions", 67 ) 68 69 var ( 70 metaV1Types map[reflect.Type]struct{} 71 metaV1ListType = reflect.TypeOf((*metav1.ListMetaAccessor)(nil)).Elem() 72 ) 73 74 func init() { 75 gv := schema.GroupVersion{Group: "roundtrip.group", Version: "v1"} 76 77 scheme := runtime.NewScheme() 78 79 metav1.AddToGroupVersion(scheme, gv) 80 metaV1Types = make(map[reflect.Type]struct{}) 81 82 // Build up a list of types to ignore 83 for _, t := range scheme.KnownTypes(gv) { 84 metaV1Types[t] = struct{}{} 85 } 86 } 87 88 // ExternalTypesViaJSON applies the round-trip test to all external round-trippable Kinds 89 // in the scheme. This is effectively testing the scenario: 90 // 91 // external -> json -> external 92 func ExternalTypesViaJSON(t *testing.T, scheme *runtime.Scheme, fuzzerFuncs fuzzer.FuzzerFuncs) { 93 codecFactory := serializer.NewCodecFactory(scheme) 94 95 f := fuzzer.FuzzerFor( 96 fuzzer.MergeFuzzerFuncs(metafuzzer.Funcs, fuzzerFuncs), 97 rand.NewSource(rand.Int63()), 98 codecFactory, 99 ) 100 101 f.SkipFieldsWithPattern(regexp.MustCompile("DeprecatedGeneration")) 102 103 kinds := scheme.AllKnownTypes() 104 for gvk := range kinds { 105 if gvk.Version == runtime.APIVersionInternal || globalNonRoundTrippableTypes.Has(gvk.Kind) { 106 continue 107 } 108 t.Run(gvk.Group+"."+gvk.Version+"."+gvk.Kind, func(t *testing.T) { 109 roundtrip.RoundTripSpecificKindWithoutProtobuf(t, gvk, scheme, codecFactory, f, nil) 110 }) 111 } 112 } 113 114 // ExternalTypesViaHub applies the round-trip test to all external round-trippable Kinds 115 // in the scheme. This is effectively testing the scenario: 116 // 117 // external version -> hub version -> external version 118 func ExternalTypesViaHub(t *testing.T, scheme, hubs *runtime.Scheme, fuzzerFuncs fuzzer.FuzzerFuncs) { 119 f := fuzzer.FuzzerFor( 120 fuzzer.MergeFuzzerFuncs(metafuzzer.Funcs, fuzzerFuncs), 121 rand.NewSource(rand.Int63()), 122 // This seems to be used for protobuf not json 123 serializer.NewCodecFactory(scheme), 124 ) 125 126 f.SkipFieldsWithPattern(regexp.MustCompile("DeprecatedGeneration")) 127 128 for gvk, objType := range scheme.AllKnownTypes() { 129 if gvk.Version == runtime.APIVersionInternal || 130 gvk.Group == "" || // K8s group 131 globalNonRoundTrippableTypes.Has(gvk.Kind) { 132 continue 133 } 134 135 if _, ok := metaV1Types[objType]; ok { 136 continue 137 } 138 139 if reflect.PointerTo(objType).AssignableTo(metaV1ListType) { 140 continue 141 } 142 143 if hubs.Recognizes(gvk) { 144 continue 145 } 146 147 t.Run(gvk.Group+"."+gvk.Version+"."+gvk.Kind, func(t *testing.T) { 148 for range *roundtrip.FuzzIters { 149 roundTripViaHub(t, gvk, scheme, hubs, f) 150 151 if t.Failed() { 152 break 153 } 154 } 155 }) 156 } 157 } 158 159 func roundTripViaHub(t *testing.T, gvk schema.GroupVersionKind, scheme, hubs *runtime.Scheme, f *randfill.Filler) { 160 ctx := context.Background() 161 162 hub, hubGVK := hubInstanceForGK(t, hubs, gvk.GroupKind()) 163 obj := objForGVK(t, gvk, scheme) 164 165 fuzzObject(t, f, gvk, obj) 166 167 original := obj 168 obj = obj.DeepCopyObject().(convertibleObject) 169 170 if !apiequality.Semantic.DeepEqual(original, obj) { 171 t.Error("DeepCopy modified the original object (DeepCopy should not have side-effects), diff:", diff(original, obj)) 172 return 173 } 174 175 if err := hub.ConvertFrom(ctx, obj); err != nil { 176 t.Errorf("Conversion to hub (%s) failed: %s", hubGVK, err) 177 } 178 179 if !apiequality.Semantic.DeepEqual(original, obj) { 180 t.Errorf("Conversion to hub (%s) modified the original object (ConvertFrom should not have side-effects), diff: %v", hubGVK, diff(original, obj)) 181 return 182 } 183 184 newObj := objForGVK(t, gvk, scheme) 185 if err := hub.ConvertTo(ctx, newObj); err != nil { 186 t.Errorf("Conversion from hub (%s) failed: %s", hubGVK, err) 187 t.Errorf("object: %#v", obj) 188 return 189 } 190 191 if !apiequality.Semantic.DeepEqual(obj, newObj) { 192 t.Errorf("round trip through hub (%s) produced a diff: %s", hubGVK, diff(original, newObj)) 193 return 194 } 195 } 196 197 func diff(obj1, obj2 interface{}) string { 198 // knative.dev/pkg/apis.URL is an alias to net.URL which embeds a 199 // url.Userinfo that has an unexported field 200 return cmp.Diff(obj1, obj2, cmpopts.IgnoreUnexported(url.Userinfo{})) 201 } 202 203 func objForGVK(t *testing.T, 204 gvk schema.GroupVersionKind, 205 scheme *runtime.Scheme, 206 ) convertibleObject { 207 t.Helper() 208 209 obj, err := scheme.New(gvk) 210 if err != nil { 211 t.Fatal("unable to create object instance for type", err) 212 } 213 214 objType, err := apimeta.TypeAccessor(obj) 215 if err != nil { 216 t.Fatalf("%q is not a TypeMeta and cannot be tested: %v", gvk, err) 217 } 218 objType.SetKind(gvk.Kind) 219 objType.SetAPIVersion(gvk.GroupVersion().String()) 220 return obj.(convertibleObject) 221 } 222 223 func fuzzObject(t *testing.T, fuzzer *randfill.Filler, gvk schema.GroupVersionKind, obj interface{}) { 224 fuzzer.Fill(obj) 225 226 objType, err := apimeta.TypeAccessor(obj) 227 if err != nil { 228 t.Fatalf("%q is not a TypeMeta and cannot be tested: %v", gvk, err) 229 } 230 objType.SetKind(gvk.Kind) 231 objType.SetAPIVersion(gvk.GroupVersion().String()) 232 } 233 234 func hubInstanceForGK(t *testing.T, 235 hubs *runtime.Scheme, 236 gk schema.GroupKind, 237 ) (apis.Convertible, schema.GroupVersionKind) { 238 t.Helper() 239 240 for hubGVK := range hubs.AllKnownTypes() { 241 if hubGVK.GroupKind() == gk { 242 obj, err := hubs.New(hubGVK) 243 if err != nil { 244 t.Fatal("error creating objects", err) 245 } 246 247 return obj.(apis.Convertible), hubGVK 248 } 249 } 250 251 t.Fatalf("hub type not found") 252 return nil, schema.GroupVersionKind{} 253 }