github.com/spotmaxtech/k8s-apimachinery-v0260@v0.0.1/pkg/api/apitesting/roundtrip/compatibility.go (about) 1 /* 2 Copyright 2019 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 gojson "encoding/json" 22 "io/ioutil" 23 "os" 24 "os/exec" 25 "path/filepath" 26 "reflect" 27 "sort" 28 "strings" 29 "testing" 30 31 "github.com/google/go-cmp/cmp" 32 33 apiequality "github.com/spotmaxtech/k8s-apimachinery-v0260/pkg/api/equality" 34 "github.com/spotmaxtech/k8s-apimachinery-v0260/pkg/runtime" 35 "github.com/spotmaxtech/k8s-apimachinery-v0260/pkg/runtime/schema" 36 "github.com/spotmaxtech/k8s-apimachinery-v0260/pkg/runtime/serializer/json" 37 "github.com/spotmaxtech/k8s-apimachinery-v0260/pkg/runtime/serializer/protobuf" 38 "github.com/spotmaxtech/k8s-apimachinery-v0260/pkg/util/sets" 39 ) 40 41 // CompatibilityTestOptions holds configuration for running a compatibility test using in-memory objects 42 // and serialized files on disk representing the current code and serialized data from previous versions. 43 // 44 // Example use: `NewCompatibilityTestOptions(scheme).Complete(t).Run(t)` 45 type CompatibilityTestOptions struct { 46 // Scheme is used to create new objects for filling, decoding, and for constructing serializers. 47 // Required. 48 Scheme *runtime.Scheme 49 50 // TestDataDir points to a directory containing compatibility test data. 51 // Complete() populates this with "testdata" if unset. 52 TestDataDir string 53 54 // TestDataDirCurrentVersion points to a directory containing compatibility test data for the current version. 55 // Complete() populates this with "<TestDataDir>/HEAD" if unset. 56 // Within this directory, `<group>.<version>.<kind>.[json|yaml|pb]` files are required to exist, and are: 57 // * verified to match serialized FilledObjects[GVK] 58 // * verified to decode without error 59 // * verified to round-trip byte-for-byte when re-encoded 60 // * verified to be semantically equal when decoded into memory 61 TestDataDirCurrentVersion string 62 63 // TestDataDirsPreviousVersions is a list of directories containing compatibility test data for previous versions. 64 // Complete() populates this with "<TestDataDir>/v*" directories if nil. 65 // Within these directories, `<group>.<version>.<kind>.[json|yaml|pb]` files are optional. If present, they are: 66 // * verified to decode without error 67 // * verified to round-trip byte-for-byte when re-encoded (or to match a `<group>.<version>.<kind>.[json|yaml|pb].after_roundtrip.[json|yaml|pb]` file if it exists) 68 // * verified to be semantically equal when decoded into memory 69 TestDataDirsPreviousVersions []string 70 71 // Kinds is a list of fully qualified kinds to test. 72 // Complete() populates this with Scheme.AllKnownTypes() if unset. 73 Kinds []schema.GroupVersionKind 74 75 // FilledObjects is an optional set of pre-filled objects to use for verifying HEAD fixtures. 76 // Complete() populates this with the result of CompatibilityTestObject(Kinds[*], Scheme, FillFuncs) for any missing kinds. 77 // Objects must deterministically populate every field and be identical on every invocation. 78 FilledObjects map[schema.GroupVersionKind]runtime.Object 79 80 // FillFuncs is an optional map of custom functions to use to fill instances of particular types. 81 FillFuncs map[reflect.Type]FillFunc 82 83 JSON runtime.Serializer 84 YAML runtime.Serializer 85 Proto runtime.Serializer 86 } 87 88 // FillFunc is a function that populates all serializable fields in obj. 89 // s and i are string and integer values relevant to the object being populated 90 // (for example, the json key or protobuf tag containing the object) 91 // that can be used when filling the object to make the object content identifiable 92 type FillFunc func(s string, i int, obj interface{}) 93 94 func NewCompatibilityTestOptions(scheme *runtime.Scheme) *CompatibilityTestOptions { 95 return &CompatibilityTestOptions{Scheme: scheme} 96 } 97 98 // coreKinds includes kinds that typically only need to be tested in a single API group 99 var coreKinds = sets.NewString( 100 "CreateOptions", "UpdateOptions", "PatchOptions", "DeleteOptions", 101 "GetOptions", "ListOptions", "ExportOptions", 102 "WatchEvent", 103 ) 104 105 func (c *CompatibilityTestOptions) Complete(t *testing.T) *CompatibilityTestOptions { 106 t.Helper() 107 108 // Verify scheme 109 if c.Scheme == nil { 110 t.Fatal("scheme is required") 111 } 112 113 // Populate testdata dirs 114 if c.TestDataDir == "" { 115 c.TestDataDir = "testdata" 116 } 117 if c.TestDataDirCurrentVersion == "" { 118 c.TestDataDirCurrentVersion = filepath.Join(c.TestDataDir, "HEAD") 119 } 120 if c.TestDataDirsPreviousVersions == nil { 121 dirs, err := filepath.Glob(filepath.Join(c.TestDataDir, "v*")) 122 if err != nil { 123 t.Fatal(err) 124 } 125 sort.Strings(dirs) 126 c.TestDataDirsPreviousVersions = dirs 127 } 128 129 // Populate kinds 130 if len(c.Kinds) == 0 { 131 gvks := []schema.GroupVersionKind{} 132 for gvk := range c.Scheme.AllKnownTypes() { 133 if gvk.Version == "" || gvk.Version == runtime.APIVersionInternal { 134 // only test external types 135 continue 136 } 137 if strings.HasSuffix(gvk.Kind, "List") { 138 // omit list types 139 continue 140 } 141 if gvk.Group != "" && coreKinds.Has(gvk.Kind) { 142 // only test options types in the core API group 143 continue 144 } 145 gvks = append(gvks, gvk) 146 } 147 c.Kinds = gvks 148 } 149 150 // Sort kinds to get deterministic test order 151 sort.Slice(c.Kinds, func(i, j int) bool { 152 if c.Kinds[i].Group != c.Kinds[j].Group { 153 return c.Kinds[i].Group < c.Kinds[j].Group 154 } 155 if c.Kinds[i].Version != c.Kinds[j].Version { 156 return c.Kinds[i].Version < c.Kinds[j].Version 157 } 158 if c.Kinds[i].Kind != c.Kinds[j].Kind { 159 return c.Kinds[i].Kind < c.Kinds[j].Kind 160 } 161 return false 162 }) 163 164 // Fill any missing objects 165 if c.FilledObjects == nil { 166 c.FilledObjects = map[schema.GroupVersionKind]runtime.Object{} 167 } 168 fillFuncs := defaultFillFuncs() 169 for k, v := range c.FillFuncs { 170 fillFuncs[k] = v 171 } 172 for _, gvk := range c.Kinds { 173 if _, ok := c.FilledObjects[gvk]; ok { 174 continue 175 } 176 obj, err := CompatibilityTestObject(c.Scheme, gvk, fillFuncs) 177 if err != nil { 178 t.Fatal(err) 179 } 180 c.FilledObjects[gvk] = obj 181 } 182 183 if c.JSON == nil { 184 c.JSON = json.NewSerializer(json.DefaultMetaFactory, c.Scheme, c.Scheme, true) 185 } 186 if c.YAML == nil { 187 c.YAML = json.NewYAMLSerializer(json.DefaultMetaFactory, c.Scheme, c.Scheme) 188 } 189 if c.Proto == nil { 190 c.Proto = protobuf.NewSerializer(c.Scheme, c.Scheme) 191 } 192 193 return c 194 } 195 196 func (c *CompatibilityTestOptions) Run(t *testing.T) { 197 usedHEADFixtures := sets.NewString() 198 199 for _, gvk := range c.Kinds { 200 t.Run(makeName(gvk), func(t *testing.T) { 201 202 t.Run("HEAD", func(t *testing.T) { 203 c.runCurrentVersionTest(t, gvk, usedHEADFixtures) 204 }) 205 206 for _, previousVersionDir := range c.TestDataDirsPreviousVersions { 207 t.Run(filepath.Base(previousVersionDir), func(t *testing.T) { 208 c.runPreviousVersionTest(t, gvk, previousVersionDir, nil) 209 }) 210 } 211 212 }) 213 } 214 215 // Check for unused HEAD fixtures 216 t.Run("unused_fixtures", func(t *testing.T) { 217 files, err := os.ReadDir(c.TestDataDirCurrentVersion) 218 if err != nil { 219 t.Fatal(err) 220 } 221 allFixtures := sets.NewString() 222 for _, file := range files { 223 allFixtures.Insert(file.Name()) 224 } 225 226 if unused := allFixtures.Difference(usedHEADFixtures); len(unused) > 0 { 227 t.Fatalf("remove unused fixtures from %s:\n%s", c.TestDataDirCurrentVersion, strings.Join(unused.List(), "\n")) 228 } 229 }) 230 } 231 232 func (c *CompatibilityTestOptions) runCurrentVersionTest(t *testing.T, gvk schema.GroupVersionKind, usedFiles sets.String) { 233 expectedObject := c.FilledObjects[gvk] 234 expectedJSON, expectedYAML, expectedProto := c.encode(t, expectedObject) 235 236 actualJSON, actualYAML, actualProto, err := read(c.TestDataDirCurrentVersion, gvk, "", usedFiles) 237 if err != nil && !os.IsNotExist(err) { 238 t.Fatal(err) 239 } 240 241 needsUpdate := false 242 if os.IsNotExist(err) { 243 t.Errorf("current version compatibility files did not exist: %v", err) 244 needsUpdate = true 245 } else { 246 if !bytes.Equal(expectedJSON, actualJSON) { 247 t.Errorf("json differs") 248 t.Log(cmp.Diff(string(actualJSON), string(expectedJSON))) 249 needsUpdate = true 250 } 251 252 if !bytes.Equal(expectedYAML, actualYAML) { 253 t.Errorf("yaml differs") 254 t.Log(cmp.Diff(string(actualYAML), string(expectedYAML))) 255 needsUpdate = true 256 } 257 258 if !bytes.Equal(expectedProto, actualProto) { 259 t.Errorf("proto differs") 260 needsUpdate = true 261 t.Log(cmp.Diff(dumpProto(t, actualProto[4:]), dumpProto(t, expectedProto[4:]))) 262 // t.Logf("json (for locating the offending field based on surrounding data): %s", string(expectedJSON)) 263 } 264 } 265 266 if needsUpdate { 267 const updateEnvVar = "UPDATE_COMPATIBILITY_FIXTURE_DATA" 268 if os.Getenv(updateEnvVar) == "true" { 269 writeFile(t, c.TestDataDirCurrentVersion, gvk, "", "json", expectedJSON) 270 writeFile(t, c.TestDataDirCurrentVersion, gvk, "", "yaml", expectedYAML) 271 writeFile(t, c.TestDataDirCurrentVersion, gvk, "", "pb", expectedProto) 272 t.Logf("wrote expected compatibility data... verify, commit, and rerun tests") 273 } else { 274 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) 275 } 276 return 277 } 278 279 emptyObj, err := c.Scheme.New(gvk) 280 if err != nil { 281 t.Fatal(err) 282 } 283 { 284 // compact before decoding since embedded RawExtension fields retain indenting 285 compacted := &bytes.Buffer{} 286 if err := gojson.Compact(compacted, actualJSON); err != nil { 287 t.Error(err) 288 } 289 290 jsonDecoded := emptyObj.DeepCopyObject() 291 jsonDecoded, _, err = c.JSON.Decode(compacted.Bytes(), &gvk, jsonDecoded) 292 if err != nil { 293 t.Error(err) 294 } else if !apiequality.Semantic.DeepEqual(expectedObject, jsonDecoded) { 295 t.Errorf("expected and decoded json objects differed:\n%s", cmp.Diff(expectedObject, jsonDecoded)) 296 } 297 } 298 { 299 yamlDecoded := emptyObj.DeepCopyObject() 300 yamlDecoded, _, err = c.YAML.Decode(actualYAML, &gvk, yamlDecoded) 301 if err != nil { 302 t.Error(err) 303 } else if !apiequality.Semantic.DeepEqual(expectedObject, yamlDecoded) { 304 t.Errorf("expected and decoded yaml objects differed:\n%s", cmp.Diff(expectedObject, yamlDecoded)) 305 } 306 } 307 { 308 protoDecoded := emptyObj.DeepCopyObject() 309 protoDecoded, _, err = c.Proto.Decode(actualProto, &gvk, protoDecoded) 310 if err != nil { 311 t.Error(err) 312 } else if !apiequality.Semantic.DeepEqual(expectedObject, protoDecoded) { 313 t.Errorf("expected and decoded proto objects differed:\n%s", cmp.Diff(expectedObject, protoDecoded)) 314 } 315 } 316 } 317 318 func (c *CompatibilityTestOptions) encode(t *testing.T, obj runtime.Object) (json, yaml, proto []byte) { 319 jsonBytes := bytes.NewBuffer(nil) 320 if err := c.JSON.Encode(obj, jsonBytes); err != nil { 321 t.Fatalf("error encoding json: %v", err) 322 } 323 yamlBytes := bytes.NewBuffer(nil) 324 if err := c.YAML.Encode(obj, yamlBytes); err != nil { 325 t.Fatalf("error encoding yaml: %v", err) 326 } 327 protoBytes := bytes.NewBuffer(nil) 328 if err := c.Proto.Encode(obj, protoBytes); err != nil { 329 t.Fatalf("error encoding proto: %v", err) 330 } 331 return jsonBytes.Bytes(), yamlBytes.Bytes(), protoBytes.Bytes() 332 } 333 334 func read(dir string, gvk schema.GroupVersionKind, suffix string, usedFiles sets.String) (json, yaml, proto []byte, err error) { 335 jsonFilename := makeName(gvk) + suffix + ".json" 336 actualJSON, jsonErr := ioutil.ReadFile(filepath.Join(dir, jsonFilename)) 337 yamlFilename := makeName(gvk) + suffix + ".yaml" 338 actualYAML, yamlErr := ioutil.ReadFile(filepath.Join(dir, yamlFilename)) 339 protoFilename := makeName(gvk) + suffix + ".pb" 340 actualProto, protoErr := ioutil.ReadFile(filepath.Join(dir, protoFilename)) 341 if usedFiles != nil { 342 usedFiles.Insert(jsonFilename) 343 usedFiles.Insert(yamlFilename) 344 usedFiles.Insert(protoFilename) 345 } 346 if jsonErr != nil { 347 return actualJSON, actualYAML, actualProto, jsonErr 348 } 349 if yamlErr != nil { 350 return actualJSON, actualYAML, actualProto, yamlErr 351 } 352 if protoErr != nil { 353 return actualJSON, actualYAML, actualProto, protoErr 354 } 355 return actualJSON, actualYAML, actualProto, nil 356 } 357 358 func writeFile(t *testing.T, dir string, gvk schema.GroupVersionKind, suffix, extension string, data []byte) { 359 if err := os.MkdirAll(dir, os.FileMode(0755)); err != nil { 360 t.Fatal("error making directory", err) 361 } 362 if err := ioutil.WriteFile(filepath.Join(dir, makeName(gvk)+suffix+"."+extension), data, os.FileMode(0644)); err != nil { 363 t.Fatalf("error writing %s: %v", extension, err) 364 } 365 } 366 367 func (c *CompatibilityTestOptions) runPreviousVersionTest(t *testing.T, gvk schema.GroupVersionKind, previousVersionDir string, usedFiles sets.String) { 368 jsonBeforeRoundTrip, yamlBeforeRoundTrip, protoBeforeRoundTrip, err := read(previousVersionDir, gvk, "", usedFiles) 369 if os.IsNotExist(err) || (len(jsonBeforeRoundTrip) == 0 && len(yamlBeforeRoundTrip) == 0 && len(protoBeforeRoundTrip) == 0) { 370 t.SkipNow() 371 return 372 } 373 if err != nil { 374 t.Fatal(err) 375 } 376 377 emptyObj, err := c.Scheme.New(gvk) 378 if err != nil { 379 t.Fatal(err) 380 } 381 382 // compact before decoding since embedded RawExtension fields retain indenting 383 compacted := &bytes.Buffer{} 384 if err := gojson.Compact(compacted, jsonBeforeRoundTrip); err != nil { 385 t.Fatal(err) 386 } 387 388 jsonDecoded := emptyObj.DeepCopyObject() 389 jsonDecoded, _, err = c.JSON.Decode(compacted.Bytes(), &gvk, jsonDecoded) 390 if err != nil { 391 t.Fatal(err) 392 } 393 jsonBytes := bytes.NewBuffer(nil) 394 if err := c.JSON.Encode(jsonDecoded, jsonBytes); err != nil { 395 t.Fatalf("error encoding json: %v", err) 396 } 397 jsonAfterRoundTrip := jsonBytes.Bytes() 398 399 yamlDecoded := emptyObj.DeepCopyObject() 400 yamlDecoded, _, err = c.YAML.Decode(yamlBeforeRoundTrip, &gvk, yamlDecoded) 401 if err != nil { 402 t.Fatal(err) 403 } else if !apiequality.Semantic.DeepEqual(jsonDecoded, yamlDecoded) { 404 t.Errorf("decoded json and yaml objects differ:\n%s", cmp.Diff(jsonDecoded, yamlDecoded)) 405 } 406 yamlBytes := bytes.NewBuffer(nil) 407 if err := c.YAML.Encode(yamlDecoded, yamlBytes); err != nil { 408 t.Fatalf("error encoding yaml: %v", err) 409 } 410 yamlAfterRoundTrip := yamlBytes.Bytes() 411 412 protoDecoded := emptyObj.DeepCopyObject() 413 protoDecoded, _, err = c.Proto.Decode(protoBeforeRoundTrip, &gvk, protoDecoded) 414 if err != nil { 415 t.Fatal(err) 416 } else if !apiequality.Semantic.DeepEqual(jsonDecoded, protoDecoded) { 417 t.Errorf("decoded json and proto objects differ:\n%s", cmp.Diff(jsonDecoded, protoDecoded)) 418 } 419 protoBytes := bytes.NewBuffer(nil) 420 if err := c.Proto.Encode(protoDecoded, protoBytes); err != nil { 421 t.Fatalf("error encoding proto: %v", err) 422 } 423 protoAfterRoundTrip := protoBytes.Bytes() 424 425 expectedJSONAfterRoundTrip, expectedYAMLAfterRoundTrip, expectedProtoAfterRoundTrip, _ := read(previousVersionDir, gvk, ".after_roundtrip", usedFiles) 426 if len(expectedJSONAfterRoundTrip) == 0 { 427 expectedJSONAfterRoundTrip = jsonBeforeRoundTrip 428 } 429 if len(expectedYAMLAfterRoundTrip) == 0 { 430 expectedYAMLAfterRoundTrip = yamlBeforeRoundTrip 431 } 432 if len(expectedProtoAfterRoundTrip) == 0 { 433 expectedProtoAfterRoundTrip = protoBeforeRoundTrip 434 } 435 436 jsonNeedsUpdate := false 437 yamlNeedsUpdate := false 438 protoNeedsUpdate := false 439 440 if !bytes.Equal(expectedJSONAfterRoundTrip, jsonAfterRoundTrip) { 441 t.Errorf("json differs") 442 t.Log(cmp.Diff(string(expectedJSONAfterRoundTrip), string(jsonAfterRoundTrip))) 443 jsonNeedsUpdate = true 444 } 445 446 if !bytes.Equal(expectedYAMLAfterRoundTrip, yamlAfterRoundTrip) { 447 t.Errorf("yaml differs") 448 t.Log(cmp.Diff(string(expectedYAMLAfterRoundTrip), string(yamlAfterRoundTrip))) 449 yamlNeedsUpdate = true 450 } 451 452 if !bytes.Equal(expectedProtoAfterRoundTrip, protoAfterRoundTrip) { 453 t.Errorf("proto differs") 454 protoNeedsUpdate = true 455 t.Log(cmp.Diff(dumpProto(t, expectedProtoAfterRoundTrip[4:]), dumpProto(t, protoAfterRoundTrip[4:]))) 456 // t.Logf("json (for locating the offending field based on surrounding data): %s", string(expectedJSON)) 457 } 458 459 if jsonNeedsUpdate || yamlNeedsUpdate || protoNeedsUpdate { 460 const updateEnvVar = "UPDATE_COMPATIBILITY_FIXTURE_DATA" 461 if os.Getenv(updateEnvVar) == "true" { 462 if jsonNeedsUpdate { 463 writeFile(t, previousVersionDir, gvk, ".after_roundtrip", "json", jsonAfterRoundTrip) 464 } 465 if yamlNeedsUpdate { 466 writeFile(t, previousVersionDir, gvk, ".after_roundtrip", "yaml", yamlAfterRoundTrip) 467 } 468 if protoNeedsUpdate { 469 writeFile(t, previousVersionDir, gvk, ".after_roundtrip", "pb", protoAfterRoundTrip) 470 } 471 t.Logf("wrote expected compatibility data... verify, commit, and rerun tests") 472 } else { 473 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) 474 } 475 return 476 } 477 } 478 479 func makeName(gvk schema.GroupVersionKind) string { 480 g := gvk.Group 481 if g == "" { 482 g = "core" 483 } 484 return g + "." + gvk.Version + "." + gvk.Kind 485 } 486 487 func dumpProto(t *testing.T, data []byte) string { 488 t.Helper() 489 protoc, err := exec.LookPath("protoc") 490 if err != nil { 491 t.Log(err) 492 return "" 493 } 494 cmd := exec.Command(protoc, "--decode_raw") 495 cmd.Stdin = bytes.NewBuffer(data) 496 d, err := cmd.CombinedOutput() 497 if err != nil { 498 t.Log(err) 499 return "" 500 } 501 return string(d) 502 }