From 4e5f9bc31d0cb1cede32c84053be4596749cfc52 Mon Sep 17 00:00:00 2001 From: James Munnelly Date: Thu, 27 Feb 2020 18:14:49 +0000 Subject: [PATCH] Add integration testing framework and a basic conversion test Signed-off-by: James Munnelly --- BUILD.bazel | 1 + deploy/charts/cert-manager/BUILD.bazel | 5 +- deploy/charts/cert-manager/crds/BUILD.bazel | 13 ++ hack/update-crds.sh | 2 +- test/integration/BUILD.bazel | 17 ++ test/integration/conversion/BUILD.bazel | 30 ++++ .../integration/conversion/conversion_test.go | 91 +++++++++++ test/integration/framework/BUILD.bazel | 44 ++++++ test/integration/framework/apiserver.go | 149 ++++++++++++++++++ test/integration/framework/paths.go | 70 ++++++++ 10 files changed, 420 insertions(+), 2 deletions(-) create mode 100644 deploy/charts/cert-manager/crds/BUILD.bazel create mode 100644 test/integration/BUILD.bazel create mode 100644 test/integration/conversion/BUILD.bazel create mode 100644 test/integration/conversion/conversion_test.go create mode 100644 test/integration/framework/BUILD.bazel create mode 100644 test/integration/framework/apiserver.go create mode 100644 test/integration/framework/paths.go diff --git a/BUILD.bazel b/BUILD.bazel index c62f253d6..3e497219d 100644 --- a/BUILD.bazel +++ b/BUILD.bazel @@ -81,6 +81,7 @@ filegroup( "//pkg/webhook:all-srcs", "//test/acme/dns:all-srcs", "//test/e2e:all-srcs", + "//test/integration:all-srcs", "//test/unit/gen:all-srcs", "//test/unit/listers:all-srcs", ], diff --git a/deploy/charts/cert-manager/BUILD.bazel b/deploy/charts/cert-manager/BUILD.bazel index b27505298..611e596df 100644 --- a/deploy/charts/cert-manager/BUILD.bazel +++ b/deploy/charts/cert-manager/BUILD.bazel @@ -44,6 +44,9 @@ filegroup( filegroup( name = "all-srcs", - srcs = [":package-srcs"], + srcs = [ + ":package-srcs", + "//deploy/charts/cert-manager/crds:all-srcs", + ], tags = ["automanaged"], ) diff --git a/deploy/charts/cert-manager/crds/BUILD.bazel b/deploy/charts/cert-manager/crds/BUILD.bazel new file mode 100644 index 000000000..6df04e38c --- /dev/null +++ b/deploy/charts/cert-manager/crds/BUILD.bazel @@ -0,0 +1,13 @@ +filegroup( + name = "package-srcs", + srcs = glob(["**"]), + tags = ["automanaged"], + visibility = ["//visibility:private"], +) + +filegroup( + name = "all-srcs", + srcs = [":package-srcs"], + tags = ["automanaged"], + visibility = ["//visibility:public"], +) diff --git a/hack/update-crds.sh b/hack/update-crds.sh index 484aa0962..7f20c097f 100755 --- a/hack/update-crds.sh +++ b/hack/update-crds.sh @@ -47,7 +47,7 @@ cd "${REPO_ROOT}" out="./deploy/manifests/00-crds.yaml" rm "$out" || true touch "$out" -for file in $(find "./deploy/charts/cert-manager/crds" -type f | sort -V); do +for file in $(find "./deploy/charts/cert-manager/crds" -name '*.yaml' -type f | sort -V); do # concatenate all files while removing blank (^$) lines < "$file" sed '/^$$/d' >> "$out" printf -- "---\n" >> "$out" diff --git a/test/integration/BUILD.bazel b/test/integration/BUILD.bazel new file mode 100644 index 000000000..831825721 --- /dev/null +++ b/test/integration/BUILD.bazel @@ -0,0 +1,17 @@ +filegroup( + name = "package-srcs", + srcs = glob(["**"]), + tags = ["automanaged"], + visibility = ["//visibility:private"], +) + +filegroup( + name = "all-srcs", + srcs = [ + ":package-srcs", + "//test/integration/conversion:all-srcs", + "//test/integration/framework:all-srcs", + ], + tags = ["automanaged"], + visibility = ["//visibility:public"], +) diff --git a/test/integration/conversion/BUILD.bazel b/test/integration/conversion/BUILD.bazel new file mode 100644 index 000000000..31f7a1478 --- /dev/null +++ b/test/integration/conversion/BUILD.bazel @@ -0,0 +1,30 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_test") + +go_test( + name = "go_default_test", + srcs = ["conversion_test.go"], + deps = [ + "//pkg/api:go_default_library", + "//pkg/apis/certmanager/v1alpha2:go_default_library", + "//pkg/apis/certmanager/v1alpha3:go_default_library", + "//pkg/apis/meta/v1:go_default_library", + "//test/integration/framework:go_default_library", + "@io_k8s_apimachinery//pkg/apis/meta/v1:go_default_library", + "@io_k8s_apimachinery//pkg/runtime:go_default_library", + "@io_k8s_sigs_controller_runtime//pkg/client: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"], +) diff --git a/test/integration/conversion/conversion_test.go b/test/integration/conversion/conversion_test.go new file mode 100644 index 000000000..187e34872 --- /dev/null +++ b/test/integration/conversion/conversion_test.go @@ -0,0 +1,91 @@ +/* +Copyright 2020 The Jetstack cert-manager contributors. + +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 conversion + +import ( + "context" + "testing" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/jetstack/cert-manager/pkg/api" + "github.com/jetstack/cert-manager/pkg/apis/certmanager/v1alpha2" + "github.com/jetstack/cert-manager/pkg/apis/certmanager/v1alpha3" + cmmeta "github.com/jetstack/cert-manager/pkg/apis/meta/v1" + "github.com/jetstack/cert-manager/test/integration/framework" +) + +func TestConversion(t *testing.T) { + tests := map[string]struct { + input runtime.Object + output runtime.Object + }{ + "should convert Certificates from v1alpha2 to v1alpha3": { + input: &v1alpha2.Certificate{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test", + Namespace: "default", + }, + Spec: v1alpha2.CertificateSpec{ + SecretName: "something", + IssuerRef: cmmeta.ObjectReference{ + Name: "issuername", + }, + }, + }, + output: &v1alpha3.Certificate{}, + }, + "should convert CertificateRequest from v1alpha2 to v1alpha3": { + input: &v1alpha2.CertificateRequest{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test", + Namespace: "default", + }, + Spec: v1alpha2.CertificateRequestSpec{ + // validating webhook isn't currently configured in test + // environment so this passes validation. + CSRPEM: []byte("a"), + IssuerRef: cmmeta.ObjectReference{ + Name: "issuername", + }, + }, + }, + output: &v1alpha3.CertificateRequest{}, + }, + } + for name, test := range tests { + t.Run(name, func(t *testing.T) { + config, stop := framework.RunControlPlane(t) + defer stop() + + cl, err := client.New(config, client.Options{Scheme: api.Scheme}) + if err != nil { + t.Fatal(err) + } + + if err := cl.Create(context.Background(), test.input); err != nil { + t.Fatal(err) + } + meta := test.input.(metav1.ObjectMetaAccessor) + if err := cl.Get(context.Background(), client.ObjectKey{Name: meta.GetObjectMeta().GetName(), Namespace: meta.GetObjectMeta().GetNamespace()}, test.output); err != nil { + t.Errorf("failed to fetch object in expected API version: %v", err) + } + }) + } +} diff --git a/test/integration/framework/BUILD.bazel b/test/integration/framework/BUILD.bazel new file mode 100644 index 000000000..33619fa94 --- /dev/null +++ b/test/integration/framework/BUILD.bazel @@ -0,0 +1,44 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library") + +go_library( + name = "go_default_library", + srcs = [ + "apiserver.go", + "paths.go", + ], + data = [ + "//deploy/charts/cert-manager/crds:all-srcs", + "//hack/bin:com_coreos_etcd", + "//hack/bin:io_kubernetes_kube-apiserver", + "//hack/bin:kubectl", + ], + importpath = "github.com/jetstack/cert-manager/test/integration/framework", + visibility = ["//visibility:public"], + deps = [ + "//cmd/webhook/app/testing:go_default_library", + "@io_k8s_apiextensions_apiserver//pkg/apis/apiextensions:go_default_library", + "@io_k8s_apiextensions_apiserver//pkg/apis/apiextensions/install:go_default_library", + "@io_k8s_apiextensions_apiserver//pkg/apis/apiextensions/v1beta1:go_default_library", + "@io_k8s_apimachinery//pkg/apis/meta/v1:go_default_library", + "@io_k8s_apimachinery//pkg/runtime:go_default_library", + "@io_k8s_apimachinery//pkg/runtime/serializer/json:go_default_library", + "@io_k8s_apimachinery//pkg/runtime/serializer/versioning:go_default_library", + "@io_k8s_apimachinery//pkg/util/runtime:go_default_library", + "@io_k8s_client_go//rest:go_default_library", + "@io_k8s_sigs_controller_runtime//pkg/envtest: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"], +) diff --git a/test/integration/framework/apiserver.go b/test/integration/framework/apiserver.go new file mode 100644 index 000000000..b9622ceb0 --- /dev/null +++ b/test/integration/framework/apiserver.go @@ -0,0 +1,149 @@ +/* +Copyright 2020 The Jetstack cert-manager contributors. + +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 framework + +import ( + "fmt" + "io/ioutil" + "os" + "path/filepath" + "testing" + + "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions" + apiextensionsinstall "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/install" + "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + jsonserializer "k8s.io/apimachinery/pkg/runtime/serializer/json" + "k8s.io/apimachinery/pkg/runtime/serializer/versioning" + utilruntime "k8s.io/apimachinery/pkg/util/runtime" + "k8s.io/client-go/rest" + "sigs.k8s.io/controller-runtime/pkg/envtest" + + webhooktesting "github.com/jetstack/cert-manager/cmd/webhook/app/testing" +) + +func init() { + // Set environment variables for controller-runtime's envtest package. + // This is done once as we cannot scope environment variables to a single + // invocation of RunControlPlane due to envtest's design. + setUpEnvTestEnv() +} + +type StopFunc func() + +func RunControlPlane(t *testing.T) (*rest.Config, StopFunc) { + webhookOpts, stopWebhook := webhooktesting.StartWebhookServer(t, []string{}) + crdsDir, err := getCRDsPath() + if err != nil { + t.Fatalf("error determining CRD directory path: %v", err) + } + crds := readCustomResourcesAtPath(t, crdsDir) + for _, crd := range crds { + t.Logf("Found CRD with name %q", crd.Name) + } + patchCRDConversion(crds, webhookOpts.URL, webhookOpts.CAPEM) + // environment variables + env := &envtest.Environment{ + AttachControlPlaneOutput: true, + CRDs: crds, + } + config, err := env.Start() + if err != nil { + t.Fatalf("failed to start control plane: %v", err) + } + // TODO: configure Validating and Mutating webhook + return config, func() { + defer stopWebhook() + if err := env.Stop(); err != nil { + t.Logf("failed to shut down control plane, not failing test: %v", err) + } + } +} + +var ( + internalScheme = runtime.NewScheme() +) + +func init() { + utilruntime.Must(metav1.AddMetaToScheme(internalScheme)) + apiextensionsinstall.Install(internalScheme) +} + +func patchCRDConversion(crds []*v1beta1.CustomResourceDefinition, url string, caPEM []byte) { + for _, crd := range crds { + if crd.Spec.Conversion == nil { + continue + } + if crd.Spec.Conversion.WebhookClientConfig == nil { + continue + } + if crd.Spec.Conversion.WebhookClientConfig.Service == nil { + continue + } + path := "" + if crd.Spec.Conversion.WebhookClientConfig.Service.Path != nil { + path = *crd.Spec.Conversion.WebhookClientConfig.Service.Path + } + url := fmt.Sprintf("%s%s", url, path) + crd.Spec.Conversion.WebhookClientConfig.URL = &url + crd.Spec.Conversion.WebhookClientConfig.CABundle = caPEM + crd.Spec.Conversion.WebhookClientConfig.Service = nil + } +} + +func readCustomResourcesAtPath(t *testing.T, path string) []*v1beta1.CustomResourceDefinition { + serializer := jsonserializer.NewSerializerWithOptions(jsonserializer.DefaultMetaFactory, internalScheme, internalScheme, jsonserializer.SerializerOptions{ + Yaml: true, + }) + converter := runtime.UnsafeObjectConvertor(internalScheme) + codec := versioning.NewCodec(serializer, serializer, converter, internalScheme, internalScheme, internalScheme, runtime.InternalGroupVersioner, runtime.InternalGroupVersioner, internalScheme.Name()) + + var crds []*v1beta1.CustomResourceDefinition + if err := filepath.Walk(path, func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + if filepath.Ext(path) != ".yaml" { + return nil + } + crd, err := readCRDAtPath(codec, converter, path) + if err != nil { + return err + } + crds = append(crds, crd) + return nil + }); err != nil { + t.Fatal(err) + } + return crds +} + +func readCRDAtPath(codec runtime.Codec, converter runtime.ObjectConvertor, path string) (*v1beta1.CustomResourceDefinition, error) { + data, err := ioutil.ReadFile(path) + if err != nil { + return nil, err + } + + internalCRD := &apiextensions.CustomResourceDefinition{} + if _, _, err := codec.Decode(data, nil, internalCRD); err != nil { + return nil, err + } + + output := &v1beta1.CustomResourceDefinition{} + return output, converter.Convert(internalCRD, output, nil) +} diff --git a/test/integration/framework/paths.go b/test/integration/framework/paths.go new file mode 100644 index 000000000..84330b1bd --- /dev/null +++ b/test/integration/framework/paths.go @@ -0,0 +1,70 @@ +/* +Copyright 2020 The Jetstack cert-manager contributors. + +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 framework + +import ( + "fmt" + "os" + "os/exec" + "path/filepath" +) + +// setEnvTestEnv configures environment variables for controller-runtime's +// 'envtest' package. +func setUpEnvTestEnv() { + maybeSetEnv("TEST_ASSET_ETCD", "etcd", "hack", "bin", "etcd") + maybeSetEnv("TEST_ASSET_KUBE_APISERVER", "kube-apiserver", "hack", "bin", "kube-apiserver") + maybeSetEnv("TEST_ASSET_KUBECTL", "kubectl", "hack", "bin", "kubectl") +} + +func maybeSetEnv(key, bin string, path ...string) { + if os.Getenv(key) != "" { + return + } + p, err := getPath(bin, path...) + if err != nil { + panic(fmt.Sprintf(`Failed to find integration test dependency %q. +Either re-run this test using "bazel test //test/integration/{name}" or set the %s environment variable.`, bin, key)) + } + os.Setenv(key, p) +} + +// getCRDsPath returns a path to a directory containing cert-manager CRDs. +func getCRDsPath() (string, error) { + bazelPath := filepath.Join(os.Getenv("RUNFILES_DIR"), "com_github_jetstack_cert_manager", "deploy", "charts", "cert-manager", "crds") + d, err := os.Stat(bazelPath) + if err == os.ErrNotExist { + return filepath.Join(filepath.Dir(os.Getenv("GOMOD")), "deploy", "charts", "cert-manager", "crds"), nil + } + if err != nil { + return "", err + } + if m := d.Mode(); !m.IsDir() { + return "", fmt.Errorf("directory containing CRDs is not a directory") + } + return bazelPath, nil +} + +func getPath(name string, path ...string) (string, error) { + bazelPath := filepath.Join(append([]string{os.Getenv("RUNFILES_DIR"), "com_github_jetstack_cert_manager"}, path...)...) + p, err := exec.LookPath(bazelPath) + if err == nil { + return p, nil + } + + return exec.LookPath(name) +}