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  }