diff --git a/staging/src/k8s.io/component-base/config/BUILD b/staging/src/k8s.io/component-base/config/BUILD index 7301ed294de..25c1c281586 100644 --- a/staging/src/k8s.io/component-base/config/BUILD +++ b/staging/src/k8s.io/component-base/config/BUILD @@ -24,6 +24,7 @@ filegroup( name = "all-srcs", srcs = [ ":package-srcs", + "//staging/src/k8s.io/component-base/config/testing:all-srcs", "//staging/src/k8s.io/component-base/config/v1alpha1:all-srcs", "//staging/src/k8s.io/component-base/config/validation:all-srcs", ], diff --git a/staging/src/k8s.io/component-base/config/testing/BUILD b/staging/src/k8s.io/component-base/config/testing/BUILD new file mode 100644 index 00000000000..9fa750a1e50 --- /dev/null +++ b/staging/src/k8s.io/component-base/config/testing/BUILD @@ -0,0 +1,44 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test") + +go_library( + name = "go_default_library", + srcs = [ + "apigroup.go", + "defaulting.go", + "helpers.go", + "roundtrip.go", + ], + importmap = "k8s.io/kubernetes/vendor/k8s.io/component-base/config/testing", + importpath = "k8s.io/component-base/config/testing", + visibility = ["//visibility:public"], + deps = [ + "//staging/src/k8s.io/apimachinery/pkg/api/apitesting/naming:go_default_library", + "//staging/src/k8s.io/apimachinery/pkg/runtime:go_default_library", + "//staging/src/k8s.io/apimachinery/pkg/runtime/schema:go_default_library", + "//staging/src/k8s.io/apimachinery/pkg/runtime/serializer:go_default_library", + "//staging/src/k8s.io/apimachinery/pkg/util/errors:go_default_library", + "//staging/src/k8s.io/apimachinery/pkg/util/runtime:go_default_library", + "//staging/src/k8s.io/apimachinery/pkg/util/sets:go_default_library", + "//vendor/github.com/google/go-cmp/cmp:go_default_library", + ], +) + +filegroup( + name = "package-srcs", + srcs = glob(["**"]), + tags = ["automanaged"], + visibility = ["//visibility:private"], +) + +filegroup( + name = "all-srcs", + srcs = [":package-srcs"], + tags = ["automanaged"], + visibility = ["//visibility:public"], +) + +go_test( + name = "go_default_test", + srcs = ["apigroup_test.go"], + embed = [":go_default_library"], +) diff --git a/staging/src/k8s.io/component-base/config/testing/apigroup.go b/staging/src/k8s.io/component-base/config/testing/apigroup.go new file mode 100644 index 00000000000..2759f6524da --- /dev/null +++ b/staging/src/k8s.io/component-base/config/testing/apigroup.go @@ -0,0 +1,207 @@ +/* +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 testing + +import ( + "fmt" + "reflect" + "regexp" + "strings" + + apinamingtest "k8s.io/apimachinery/pkg/api/apitesting/naming" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/util/errors" + "k8s.io/apimachinery/pkg/util/sets" +) + +// APIVersionRegexp is the regular expression that matches with valid apiversion +var APIVersionRegexp = regexp.MustCompile(`^v\d+((alpha|beta){1}\d+)?$`) + +// ComponentConfigPackage is used in APIGroup Testing +type ComponentConfigPackage struct { + ComponentName string + GroupName string + SchemeGroupVersion schema.GroupVersion + AddToScheme func(*runtime.Scheme) error + SkipTests sets.String + AllowedTags map[reflect.Type]bool + AllowedNonstandardJSONNames map[reflect.Type]string +} + +type testingFunc func(*runtime.Scheme, *ComponentConfigPackage) error + +const ( + verifyTagNaming = "verifyTagNaming" + verifyGroupNameSuffix = "verifyGroupNameSuffix" + verifyGroupNameMatch = "verifyGroupNameMatch" + verifyCorrectGroupName = "verifyCorrectGroupName" + verifyComponentConfigKindExists = "verifyComponentConfigKindExists" + verifyExternalAPIVersion = "verifyExternalAPIVersion" + verifyInternalAPIVersion = "verifyInternalAPIVersion" +) + +var testingFuncs = map[string]testingFunc{ + verifyTagNaming: verifyTagNamingFunc, + verifyGroupNameSuffix: verifyGroupNameSuffixFunc, + verifyGroupNameMatch: verifyGroupNameMatchFunc, + verifyCorrectGroupName: verifyCorrectGroupNameFunc, + verifyComponentConfigKindExists: verifyComponentConfigKindExistsFunc, +} + +// VerifyExternalTypePackage tests if external component config package is defined correctly +// Test tag naming (json name should match Go name) +// Test that GroupName has the k8s.io suffix +// Test that GroupName == SchemeGroupVersion.GroupName +// Test that the API version follows the right pattern and isn't internal +// Test that addKnownTypes and AddToScheme registers at least one type and doesn't error +// Test that the GroupName is named correctly (based on ComponentName), and there is a {Component}Configuration kind in the scheme +func VerifyExternalTypePackage(pkginfo *ComponentConfigPackage) error { + scheme, err := setup(pkginfo) + if err != nil { + return fmt.Errorf("test setup error: %v", err) + } + extraFns := map[string]testingFunc{ + verifyExternalAPIVersion: verifyExternalAPIVersionFunc, + } + return runFuncs(scheme, pkginfo, extraFns) +} + +// VerifyInternalTypePackage tests if internal component config package is defined correctly +// Test tag naming (no tags allowed) +// Test that GroupName has the k8s.io suffix +// Test that GroupName == SchemeGroupVersion.GroupName +// API version should be internal +// Test that addKnownTypes and AddToScheme registers at least one type and doesn't error +// Test that the GroupName is named correctly (based on ComponentName), and there is a {Component}Configuration kind in the scheme +func VerifyInternalTypePackage(pkginfo *ComponentConfigPackage) error { + scheme, err := setup(pkginfo) + if err != nil { + return fmt.Errorf("test setup error: %v", err) + } + extraFns := map[string]testingFunc{ + verifyInternalAPIVersion: verifyInternalAPIVersionFunc, + } + return runFuncs(scheme, pkginfo, extraFns) +} + +func setup(pkginfo *ComponentConfigPackage) (*runtime.Scheme, error) { + if len(pkginfo.ComponentName) == 0 || + len(pkginfo.GroupName) == 0 || + pkginfo.SchemeGroupVersion.Empty() || + pkginfo.AddToScheme == nil { + return nil, fmt.Errorf("invalid argument: not all parameters were passed correctly to the function") + } + + scheme := runtime.NewScheme() + if err := pkginfo.AddToScheme(scheme); err != nil { + return nil, fmt.Errorf("AddToScheme must not return an error: %v", err) + } + if len(scheme.AllKnownTypes()) == 0 { + return nil, fmt.Errorf("AddToScheme doesn't register any type") + } + return scheme, nil +} + +func runFuncs(scheme *runtime.Scheme, pkginfo *ComponentConfigPackage, extraFns map[string]testingFunc) error { + verifyFns := []testingFunc{} + for name, fn := range testingFuncs { + if pkginfo.SkipTests.Has(name) { + continue + } + verifyFns = append(verifyFns, fn) + } + for name, fn := range extraFns { + if pkginfo.SkipTests.Has(name) { + continue + } + verifyFns = append(verifyFns, fn) + } + errs := []error{} + for _, fn := range verifyFns { + if err := fn(scheme, pkginfo); err != nil { + errs = append(errs, err) + } + } + return errors.NewAggregate(errs) +} + +func verifyTagNamingFunc(scheme *runtime.Scheme, pkginfo *ComponentConfigPackage) error { + return apinamingtest.VerifyTagNaming(scheme, pkginfo.AllowedTags, pkginfo.AllowedNonstandardJSONNames) +} + +func verifyGroupNameSuffixFunc(scheme *runtime.Scheme, _ *ComponentConfigPackage) error { + return apinamingtest.VerifyGroupNames(scheme, sets.NewString()) +} + +func verifyGroupNameMatchFunc(_ *runtime.Scheme, pkginfo *ComponentConfigPackage) error { + if pkginfo.GroupName != pkginfo.SchemeGroupVersion.Group { + return fmt.Errorf("GroupName must be equal to SchemeGroupVersion.Group, GroupName: %v,SchemeGroupVersion.Group: %v", + pkginfo.GroupName, pkginfo.SchemeGroupVersion.Group) + } + return nil +} + +func verifyCorrectGroupNameFunc(_ *runtime.Scheme, pkginfo *ComponentConfigPackage) error { + desiredGroupName := fmt.Sprintf("%s.config.k8s.io", lowercaseWithoutDashes(pkginfo.ComponentName)) + if pkginfo.SchemeGroupVersion.Group != desiredGroupName { + return fmt.Errorf("got GroupName %q, want %q", pkginfo.SchemeGroupVersion.Group, desiredGroupName) + + } + return nil +} + +func verifyComponentConfigKindExistsFunc(scheme *runtime.Scheme, pkginfo *ComponentConfigPackage) error { + expectedKind := fmt.Sprintf("%sConfiguration", dashesToCapitalCase(pkginfo.ComponentName)) + expectedGVK := pkginfo.SchemeGroupVersion.WithKind(expectedKind) + if !scheme.Recognizes(expectedGVK) { + registeredKinds := sets.NewString() + for gvk := range scheme.AllKnownTypes() { + registeredKinds.Insert(gvk.Kind) + } + return fmt.Errorf("Kind %s not registered in the scheme, registered kinds are %v", expectedKind, registeredKinds.List()) + } + return nil +} + +func verifyExternalAPIVersionFunc(_ *runtime.Scheme, pkginfo *ComponentConfigPackage) error { + if !APIVersionRegexp.MatchString(pkginfo.SchemeGroupVersion.Version) { + return fmt.Errorf("invalid API version %q, must match %q", pkginfo.SchemeGroupVersion.Version, APIVersionRegexp.String()) + } + return nil +} + +func verifyInternalAPIVersionFunc(_ *runtime.Scheme, pkginfo *ComponentConfigPackage) error { + if pkginfo.SchemeGroupVersion.Version != runtime.APIVersionInternal { + return fmt.Errorf("internal API version must be %q, got %q", + runtime.APIVersionInternal, pkginfo.SchemeGroupVersion.Version) + } + return nil +} + +func lowercaseWithoutDashes(str string) string { + return strings.Replace(strings.ToLower(str), "-", "", -1) +} + +func dashesToCapitalCase(str string) string { + segments := strings.Split(str, "-") + result := "" + for _, segment := range segments { + result += strings.Title(segment) + } + return result +} diff --git a/staging/src/k8s.io/component-base/config/testing/apigroup_test.go b/staging/src/k8s.io/component-base/config/testing/apigroup_test.go new file mode 100644 index 00000000000..691d733f8f9 --- /dev/null +++ b/staging/src/k8s.io/component-base/config/testing/apigroup_test.go @@ -0,0 +1,73 @@ +/* +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 testing + +import ( + "testing" +) + +func TestAPIVersionRegexp(t *testing.T) { + testCases := []struct { + name string + apiversion string + expected bool + }{ + { + name: "v1", + apiversion: "v1", + expected: true, + }, + { + name: "v1alpha1", + apiversion: "v1alpha1", + expected: true, + }, + { + name: "v1beta1", + apiversion: "v1beta1", + expected: true, + }, + { + name: "doesn't start with v", + apiversion: "beta1", + expected: false, + }, + { + name: "doesn't end with digit", + apiversion: "v1alpha", + expected: false, + }, + { + name: "doesn't have digit after v", + apiversion: "valpha1", + expected: false, + }, + { + name: "both alpha beta", + apiversion: "v1alpha1beta1", + expected: false, + }, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + actual := APIVersionRegexp.MatchString(tc.apiversion) + if actual != tc.expected { + t.Errorf("APIVersionRegexp expected %v, got %v", tc.expected, actual) + } + }) + } +} diff --git a/staging/src/k8s.io/component-base/config/testing/defaulting.go b/staging/src/k8s.io/component-base/config/testing/defaulting.go new file mode 100644 index 00000000000..a8ffe263793 --- /dev/null +++ b/staging/src/k8s.io/component-base/config/testing/defaulting.go @@ -0,0 +1,51 @@ +/* +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 testing + +import ( + "fmt" + "path/filepath" + "testing" + + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/serializer" +) + +// DefaultingTest run defaulting tests for given scheme +func DefaultingTest(t *testing.T, scheme *runtime.Scheme, codecs serializer.CodecFactory) { + tc := GetDefaultingTestCases(scheme) + RunTestsOnYAMLData(t, scheme, tc, codecs) +} + +// GetDefaultingTestCases returns defaulting testcases for given scheme +func GetDefaultingTestCases(scheme *runtime.Scheme) []TestCase { + cases := []TestCase{} + for gvk := range scheme.AllKnownTypes() { + beforeDir := fmt.Sprintf("testdata/%s/before", gvk.Kind) + afterDir := fmt.Sprintf("testdata/%s/after", gvk.Kind) + filename := fmt.Sprintf("%s.yaml", gvk.Version) + + cases = append(cases, TestCase{ + name: fmt.Sprintf("default_%s", gvk.Version), + in: filepath.Join(beforeDir, filename), + inGVK: gvk, + out: filepath.Join(afterDir, filename), + outGV: gvk.GroupVersion(), + }) + } + return cases +} diff --git a/staging/src/k8s.io/component-base/config/testing/helpers.go b/staging/src/k8s.io/component-base/config/testing/helpers.go new file mode 100644 index 00000000000..9fa62bcd5e0 --- /dev/null +++ b/staging/src/k8s.io/component-base/config/testing/helpers.go @@ -0,0 +1,96 @@ +/* +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 testing + +import ( + "bytes" + "io/ioutil" + "os" + "testing" + + "github.com/google/go-cmp/cmp" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/runtime/serializer" +) + +// RunTestsOnYAMLData decodes the yaml file from specified path, encodes the object and matches +// with expected yaml in specified path +func RunTestsOnYAMLData(t *testing.T, scheme *runtime.Scheme, tests []TestCase, codecs serializer.CodecFactory) { + for _, rt := range tests { + t.Run(rt.name, func(t *testing.T) { + obj, err := decodeTestData(rt.in, scheme, rt.inGVK, codecs) + if err != nil { + t.Fatal(err) + } + + const mediaType = runtime.ContentTypeYAML + info, ok := runtime.SerializerInfoForMediaType(codecs.SupportedMediaTypes(), mediaType) + if !ok { + t.Errorf("unable to locate encoder -- %q is not a supported media type", mediaType) + } + + encoder := codecs.EncoderForVersion(info.Serializer, rt.outGV) + + actual, err := runtime.Encode(encoder, obj) + if err != nil { + t.Fatalf("failed to encode object: %v", err) + } + + expected, err := ioutil.ReadFile(rt.out) + if err != nil && !os.IsNotExist(err) { + t.Fatalf("couldn't read test data: %v", err) + } + + needsUpdate := false + if os.IsNotExist(err) { + needsUpdate = true + t.Error("couldn't find test data") + } else { + if !bytes.Equal(expected, actual) { + t.Errorf("Output does not match expected, diff (- want, + got):\n\tin: %s\n\tout: %s\n\tgroupversion: %s\n\tdiff: \n%s\n", + rt.in, rt.out, rt.outGV.String(), cmp.Diff(string(expected), string(actual))) + needsUpdate = true + } + } + if needsUpdate { + const updateEnvVar = "UPDATE_COMPONENTCONFIG_FIXTURE_DATA" + if os.Getenv(updateEnvVar) == "true" { + if err := ioutil.WriteFile(rt.out, actual, 0755); err != nil { + t.Fatal(err) + } + t.Logf("wrote expected test 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) + } + } + }) + } +} + +func decodeTestData(path string, scheme *runtime.Scheme, gvk schema.GroupVersionKind, codecs serializer.CodecFactory) (runtime.Object, error) { + content, err := ioutil.ReadFile(path) + if err != nil { + return nil, err + } + + obj, _, err := codecs.DecoderToVersion(codecs.UniversalDecoder(), gvk.GroupVersion()).Decode(content, &gvk, nil) + if err != nil { + return nil, err + } + return obj, nil +} diff --git a/staging/src/k8s.io/component-base/config/testing/roundtrip.go b/staging/src/k8s.io/component-base/config/testing/roundtrip.go new file mode 100644 index 00000000000..4bcdb6cff32 --- /dev/null +++ b/staging/src/k8s.io/component-base/config/testing/roundtrip.go @@ -0,0 +1,92 @@ +/* +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 testing + +import ( + "fmt" + "os" + "path/filepath" + "testing" + + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/runtime/serializer" + utilruntime "k8s.io/apimachinery/pkg/util/runtime" + "k8s.io/apimachinery/pkg/util/sets" +) + +// RoundTripTest runs roundtrip tests for given scheme +func RoundTripTest(t *testing.T, scheme *runtime.Scheme, codecs serializer.CodecFactory) { + tc := GetRoundtripTestCases(scheme, nil) + RunTestsOnYAMLData(t, scheme, tc, codecs) +} + +// TestCase defines a testcase for roundtrip and defaulting tests +type TestCase struct { + name, in, out string + inGVK schema.GroupVersionKind + outGV schema.GroupVersion +} + +// GetRoundtripTestCases returns the testcases for roundtrip testing for given scheme +func GetRoundtripTestCases(scheme *runtime.Scheme, disallowMarshalGroupVersions sets.String) []TestCase { + cases := []TestCase{} + versionsForKind := map[schema.GroupKind][]string{} + for gvk := range scheme.AllKnownTypes() { + versionsForKind[gvk.GroupKind()] = append(versionsForKind[gvk.GroupKind()], gvk.Version) + } + + for gk, versions := range versionsForKind { + for _, vin := range versions { + if vin == runtime.APIVersionInternal { + continue // Don't try to deserialize the internal version + } + for _, vout := range versions { + inGVK := schema.GroupVersionKind{Group: gk.Group, Version: vin, Kind: gk.Kind} + marshalGV := schema.GroupVersion{Group: gk.Group, Version: vout} + if disallowMarshalGroupVersions.Has(marshalGV.String()) { + continue // Don't marshal a gv that is blacklisted + } + testdir := filepath.Join("testdata", gk.Kind, fmt.Sprintf("%sTo%s", vin, vout)) + utilruntime.Must(filepath.Walk(testdir, func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + if info.IsDir() { + if info.Name() == fmt.Sprintf("%sTo%s", vin, vout) { + return nil + } + return filepath.SkipDir + } + if filepath.Ext(info.Name()) != ".yaml" { + return nil + } + cases = append(cases, TestCase{ + name: fmt.Sprintf("%sTo%s", vin, vout), + in: filepath.Join(testdir, info.Name()), + inGVK: inGVK, + out: filepath.Join(testdir, fmt.Sprintf("%s.after_roundtrip", info.Name())), + outGV: marshalGV, + }) + + return nil + })) + } + } + } + return cases +} diff --git a/staging/src/k8s.io/component-base/go.mod b/staging/src/k8s.io/component-base/go.mod index 6d550e6cf8d..ee4a35987cd 100644 --- a/staging/src/k8s.io/component-base/go.mod +++ b/staging/src/k8s.io/component-base/go.mod @@ -6,6 +6,7 @@ go 1.12 require ( github.com/blang/semver v3.5.0+incompatible + github.com/google/go-cmp v0.3.0 github.com/prometheus/client_golang v1.0.0 github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90 github.com/prometheus/common v0.4.1