From 0fcc13874cbdaadafba43701f35bff5dfa746a3f Mon Sep 17 00:00:00 2001 From: Eric Stroczynski Date: Mon, 13 Jul 2020 13:23:33 -0700 Subject: [PATCH] This commit removes pkg/test, the `test local` subcommand, and internal SDK e2e tests from the test/test-framework sample project, as part of a larger effort to rely on envtest. *: remove references to pkg/test, `test local`, and any tests that were solely based on `test local`. --- Makefile | 7 +- cmd/operator-sdk/cli/legacy.go | 2 - cmd/operator-sdk/test/cmd.go | 31 -- cmd/operator-sdk/test/local.go | 364 ---------------- go.mod | 2 - go.sum | 3 +- hack/tests/e2e-ansible-molecule.sh | 10 +- hack/tests/scaffolding/e2e-go-scaffold.sh | 59 --- hack/tests/scaffolding/scaffold-memcached.go | 226 ---------- hack/tests/subcommand.sh | 55 --- pkg/test/client.go | 117 ------ pkg/test/context.go | 132 ------ pkg/test/context_test.go | 44 -- pkg/test/e2eutil/wait_util.go | 104 ----- pkg/test/framework.go | 298 -------------- pkg/test/main_entry.go | 56 --- pkg/test/resource_creator.go | 159 ------- pkg/test/resource_creator_test.go | 204 --------- test/test-framework/test/e2e/main_test.go | 25 -- .../test-framework/test/e2e/memcached_test.go | 125 ------ .../content/en/docs/ansible/testing-guide.md | 13 +- website/content/en/docs/cli/operator-sdk.md | 1 - .../content/en/docs/cli/operator-sdk_test.md | 23 -- .../en/docs/cli/operator-sdk_test_local.md | 39 -- .../testing/travis-build.md | 51 +-- .../en/docs/golang/legacy/e2e-tests.md | 388 ------------------ 26 files changed, 13 insertions(+), 2525 deletions(-) delete mode 100644 cmd/operator-sdk/test/cmd.go delete mode 100644 cmd/operator-sdk/test/local.go delete mode 100755 hack/tests/scaffolding/e2e-go-scaffold.sh delete mode 100644 hack/tests/scaffolding/scaffold-memcached.go delete mode 100755 hack/tests/subcommand.sh delete mode 100644 pkg/test/client.go delete mode 100644 pkg/test/context.go delete mode 100644 pkg/test/context_test.go delete mode 100644 pkg/test/e2eutil/wait_util.go delete mode 100755 pkg/test/framework.go delete mode 100644 pkg/test/main_entry.go delete mode 100644 pkg/test/resource_creator.go delete mode 100644 pkg/test/resource_creator_test.go delete mode 100644 test/test-framework/test/e2e/main_test.go delete mode 100644 test/test-framework/test/e2e/memcached_test.go delete mode 100644 website/content/en/docs/cli/operator-sdk_test.md delete mode 100644 website/content/en/docs/cli/operator-sdk_test_local.md delete mode 100644 website/content/en/docs/golang/legacy/e2e-tests.md diff --git a/Makefile b/Makefile index 3e9be6e2d5..08765f9121 100644 --- a/Makefile +++ b/Makefile @@ -266,15 +266,12 @@ test-links: test-ci: test-sanity test-unit install test-subcommand test-e2e ## Run the CI test suite # Subcommand tests. -.PHONY: test-subcommand test-subcommand-local test-subcommand-olm-install +.PHONY: test-subcommand test-subcommand-olm-install -test-subcommand: test-subcommand-local test-subcommand-olm-install +test-subcommand: test-subcommand-olm-install ./hack/tests/subcommand-bundle.sh ./hack/tests/subcommand-generate-csv.sh -test-subcommand-local: - ./hack/tests/subcommand.sh - test-subcommand-olm-install: ./hack/tests/subcommand-olm-install.sh diff --git a/cmd/operator-sdk/cli/legacy.go b/cmd/operator-sdk/cli/legacy.go index fa235adceb..2a07a9212d 100644 --- a/cmd/operator-sdk/cli/legacy.go +++ b/cmd/operator-sdk/cli/legacy.go @@ -29,7 +29,6 @@ import ( "github.com/operator-framework/operator-sdk/cmd/operator-sdk/new" "github.com/operator-framework/operator-sdk/cmd/operator-sdk/olm" "github.com/operator-framework/operator-sdk/cmd/operator-sdk/run" - "github.com/operator-framework/operator-sdk/cmd/operator-sdk/test" "github.com/operator-framework/operator-sdk/cmd/operator-sdk/version" "github.com/operator-framework/operator-sdk/internal/flags" "github.com/operator-framework/operator-sdk/internal/util/projutil" @@ -76,7 +75,6 @@ func GetCLIRoot() *cobra.Command { new.NewCmd(), olm.NewCmd(), run.NewCmdLegacy(), - test.NewCmd(), version.NewCmd(), ) diff --git a/cmd/operator-sdk/test/cmd.go b/cmd/operator-sdk/test/cmd.go deleted file mode 100644 index 808c704930..0000000000 --- a/cmd/operator-sdk/test/cmd.go +++ /dev/null @@ -1,31 +0,0 @@ -// Copyright 2018 The Operator-SDK 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 test - -import ( - "github.com/spf13/cobra" -) - -func NewCmd() *cobra.Command { - testCmd := &cobra.Command{ - Use: "test", - Short: "Tests the operator", - Long: `The test command has subcommands that can test the operator. -`, - } - - testCmd.AddCommand(newTestLocalCmd()) - return testCmd -} diff --git a/cmd/operator-sdk/test/local.go b/cmd/operator-sdk/test/local.go deleted file mode 100644 index cc1594e334..0000000000 --- a/cmd/operator-sdk/test/local.go +++ /dev/null @@ -1,364 +0,0 @@ -// Copyright 2018 The Operator-SDK 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 test - -import ( - "bytes" - "errors" - "fmt" - "io/ioutil" - "os" - "os/exec" - "path/filepath" - "strings" - - "github.com/operator-framework/operator-sdk/internal/scaffold" - "github.com/operator-framework/operator-sdk/internal/util/fileutil" - internalk8sutil "github.com/operator-framework/operator-sdk/internal/util/k8sutil" - "github.com/operator-framework/operator-sdk/internal/util/projutil" - "github.com/operator-framework/operator-sdk/pkg/k8sutil" - "github.com/operator-framework/operator-sdk/pkg/test" - - log "github.com/sirupsen/logrus" - "github.com/spf13/cobra" - appsv1 "k8s.io/api/apps/v1" - "k8s.io/apimachinery/pkg/runtime" - "k8s.io/apimachinery/pkg/runtime/serializer" - cgoscheme "k8s.io/client-go/kubernetes/scheme" - "sigs.k8s.io/yaml" -) - -var deployTestDir = filepath.Join(scaffold.DeployDir, "test") - -type testLocalConfig struct { - kubeconfig string - globalManPath string - namespacedManPath string - goTestFlags string - moleculeTestFlags string - // TODO: remove before 1.0.0 - // Namespace is deprecated - namespace string - operatorNamespace string - watchNamespace string - image string - localOperatorFlags string - upLocal bool - noSetup bool - debug bool - skipCleanupOnError bool -} - -var tlConfig testLocalConfig - -func newTestLocalCmd() *cobra.Command { - testCmd := &cobra.Command{ - Use: "local [flags]", - Short: "Run End-To-End tests locally", - RunE: testLocalFunc, - } - testCmd.Flags().StringVar(&tlConfig.kubeconfig, "kubeconfig", "", "Kubeconfig path") - testCmd.Flags().StringVar(&tlConfig.globalManPath, "global-manifest", "", - "Path to manifest for Global resources (e.g. CRD manifests)") - testCmd.Flags().StringVar(&tlConfig.namespacedManPath, "namespaced-manifest", "", - "Path to manifest for per-test, namespaced resources (e.g. RBAC and Operator manifest)") - testCmd.Flags().StringVar(&tlConfig.goTestFlags, "go-test-flags", "", - "Additional flags to pass to go test") - testCmd.Flags().StringVar(&tlConfig.moleculeTestFlags, "molecule-test-flags", "", - "Additional flags to pass to molecule test") - // TODO: remove before 1.0.0. Namespace is deprecated - testCmd.Flags().StringVar(&tlConfig.namespace, "namespace", "", - "(Deprecated: use --operator-namespace instead) If non-empty, single namespace to run tests in") - testCmd.Flags().StringVar(&tlConfig.operatorNamespace, "operator-namespace", "", - "Namespace where the operator will be deployed, CRs will be created and tests will be executed "+ - "(By default it will be in the default namespace defined in the kubeconfig)") - testCmd.Flags().StringVar(&tlConfig.watchNamespace, "watch-namespace", "", - "(only valid with --up-local) Namespace where the operator watches for changes."+ - " Set \"\" for AllNamespaces, set \"ns1,ns2\" for MultiNamespace"+ - "(if not set then watches Operator Namespace") - testCmd.Flags().BoolVar(&tlConfig.upLocal, "up-local", false, - "Enable running operator locally with go run instead of as an image in the cluster") - testCmd.Flags().BoolVar(&tlConfig.noSetup, "no-setup", false, "Disable test resource creation") - testCmd.Flags().BoolVar(&tlConfig.debug, "debug", false, "Enable debug-level logging") - testCmd.Flags().StringVar(&tlConfig.image, "image", "", - "Use a different operator image from the one specified in the namespaced manifest") - testCmd.Flags().StringVar(&tlConfig.localOperatorFlags, "local-operator-flags", "", - "The flags that the operator needs (while using --up-local). Example: \"--flag1 value1 --flag2=value2\"") - testCmd.Flags().BoolVar(&tlConfig.skipCleanupOnError, "skip-cleanup-error", false, - "If set as true, the cleanup function responsible to remove all artifacts "+ - "will be skipped if an error is faced.") - - return testCmd -} - -func testLocalFunc(cmd *cobra.Command, args []string) error { - //TODO: remove before 1.0.0 - // set --operator-namespace flag if the --namespace flag is set - // (only if --operator-namespace flag is not set) - if cmd.Flags().Changed("namespace") { - log.Info("--namespace is deprecated; use --operator-namespace instead.") - if !cmd.Flags().Changed("operator-namespace") { - err := cmd.Flags().Set("operator-namespace", tlConfig.namespace) - return err - } - } - switch t := projutil.GetOperatorType(); t { - case projutil.OperatorTypeGo: - return testLocalGoFunc(cmd, args) - case projutil.OperatorTypeAnsible: - return testLocalAnsibleFunc() - case projutil.OperatorTypeHelm: - return fmt.Errorf("`test local` for Helm operators is not implemented") - } - return projutil.ErrUnknownOperatorType{} -} - -func testLocalAnsibleFunc() error { - projutil.MustInProjectRoot() - testArgs := []string{} - if tlConfig.debug { - testArgs = append(testArgs, "--debug") - } - testArgs = append(testArgs, "test", "-s", "test-local") - - if tlConfig.moleculeTestFlags != "" { - testArgs = append(testArgs, strings.Split(tlConfig.moleculeTestFlags, " ")...) - } - - dc := exec.Command("molecule", testArgs...) - dc.Env = append(os.Environ(), fmt.Sprintf("%v=%v", test.TestOperatorNamespaceEnv, tlConfig.operatorNamespace)) - dc.Dir = projutil.MustGetwd() - if err := projutil.ExecCmd(dc); err != nil { - log.Fatal(err) - } - return nil -} - -func testLocalGoFunc(cmd *cobra.Command, args []string) error { - if len(args) != 1 { - return fmt.Errorf("command %s requires exactly one argument", cmd.CommandPath()) - } - if (tlConfig.noSetup && tlConfig.globalManPath != "") || - (tlConfig.noSetup && tlConfig.namespacedManPath != "") { - return fmt.Errorf("the global-manifest and namespaced-manifest flags cannot be enabled" + - " at the same time as the no-setup flag") - } - - if tlConfig.upLocal && tlConfig.operatorNamespace == "" { - return fmt.Errorf("must specify a namespace with operator-namespace flag to run in when --up-local flag is set") - } - if !tlConfig.upLocal && cmd.Flags().Changed("watch-namespace") { - return fmt.Errorf("--watch-namespace not valid without -up-local flag") - } - - log.Info("Testing operator locally.") - - // if no namespaced manifest path is given, combine deploy/service_account.yaml, deploy/role.yaml, - // deploy/role_binding.yaml and deploy/operator.yaml - if tlConfig.namespacedManPath == "" && !tlConfig.noSetup { - if !tlConfig.upLocal { - file, err := internalk8sutil.GenerateCombinedNamespacedManifest(scaffold.DeployDir) - if err != nil { - return err - } - tlConfig.namespacedManPath = file.Name() - } else { - file, err := ioutil.TempFile("", "empty.yaml") - if err != nil { - return fmt.Errorf("could not create empty manifest file: %v", err) - } - tlConfig.namespacedManPath = file.Name() - emptyBytes := []byte{} - if err := file.Chmod(os.FileMode(fileutil.DefaultFileMode)); err != nil { - return fmt.Errorf("could not chown temporary namespaced manifest file: %v", err) - } - if _, err := file.Write(emptyBytes); err != nil { - return fmt.Errorf("could not write temporary namespaced manifest file: %v", err) - } - if err := file.Close(); err != nil { - return err - } - } - defer func() { - err := os.Remove(tlConfig.namespacedManPath) - if err != nil { - log.Errorf("Could not delete temporary namespace manifest file: (%v)", err) - } - }() - } - if tlConfig.globalManPath == "" && !tlConfig.noSetup { - file, err := internalk8sutil.GenerateCombinedGlobalManifest(scaffold.CRDsDir) - if err != nil { - return err - } - tlConfig.globalManPath = file.Name() - defer func() { - err := os.Remove(tlConfig.globalManPath) - if err != nil { - log.Errorf("Could not delete global manifest file: (%v)", err) - } - }() - } - if tlConfig.noSetup { - err := os.MkdirAll(deployTestDir, os.FileMode(fileutil.DefaultDirFileMode)) - if err != nil { - return fmt.Errorf("could not create %s: %v", deployTestDir, err) - } - tlConfig.namespacedManPath = filepath.Join(deployTestDir, "empty.yaml") - tlConfig.globalManPath = filepath.Join(deployTestDir, "empty.yaml") - emptyBytes := []byte{} - err = ioutil.WriteFile(tlConfig.globalManPath, emptyBytes, os.FileMode(fileutil.DefaultFileMode)) - if err != nil { - return fmt.Errorf("could not create empty manifest file: %v", err) - } - defer func() { - err := os.Remove(tlConfig.globalManPath) - if err != nil { - log.Errorf("Could not delete empty manifest file: (%v)", err) - } - }() - } - if tlConfig.image != "" { - err := replaceImage(tlConfig.namespacedManPath, tlConfig.image) - if err != nil { - return fmt.Errorf("failed to overwrite operator image in the namespaced manifest: %v", err) - } - } - testArgs := []string{ - "-" + test.NamespacedManPathFlag, tlConfig.namespacedManPath, - "-" + test.GlobalManPathFlag, tlConfig.globalManPath, - "-" + test.ProjRootFlag, projutil.MustGetwd(), - } - if tlConfig.kubeconfig != "" { - testArgs = append(testArgs, "-"+test.KubeConfigFlag, tlConfig.kubeconfig) - } - // if we do the append using an empty go flags, it inserts an empty arg, which causes - // any later flags to be ignored - if tlConfig.goTestFlags != "" { - testArgs = append(testArgs, strings.Split(tlConfig.goTestFlags, " ")...) - } - if tlConfig.operatorNamespace != "" || tlConfig.noSetup { - testArgs = append(testArgs, "-parallel=1") - } - env := os.Environ() - if tlConfig.operatorNamespace != "" { - env = append( - env, - fmt.Sprintf("%v=%v", test.TestOperatorNamespaceEnv, tlConfig.operatorNamespace), - ) - } - - if cmd.Flags().Changed("watch-namespace") { - env = append( - env, - fmt.Sprintf("%v=%v", test.TestWatchNamespaceEnv, tlConfig.watchNamespace), - ) - } - - if tlConfig.upLocal { - env = append(env, fmt.Sprintf("%s=%s", k8sutil.ForceRunModeEnv, k8sutil.LocalRunMode)) - testArgs = append(testArgs, "-"+test.LocalOperatorFlag) - if tlConfig.localOperatorFlags != "" { - testArgs = append(testArgs, "-"+test.LocalOperatorArgs, tlConfig.localOperatorFlags) - } - } - testArgs = append(testArgs, fmt.Sprintf("-%s=%t", test.SkipCleanupOnErrorFlag, tlConfig.skipCleanupOnError)) - opts := projutil.GoTestOptions{ - GoCmdOptions: projutil.GoCmdOptions{ - PackagePath: args[0] + "/...", - Env: env, - Dir: projutil.MustGetwd(), - }, - TestBinaryArgs: testArgs, - } - if err := projutil.GoTest(opts); err != nil { - var exitErr *exec.ExitError - if errors.As(err, &exitErr) { - os.Exit(exitErr.ExitCode()) - } - log.Fatalf("Failed to build test binary: %v", err) - } - log.Info("Local operator test successfully completed.") - return nil -} - -// TODO: add support for multiple deployments and containers (user would have to -// provide extra information in that case) - -// replaceImage searches for a deployment and replaces the image in the container -// to the one specified in the function call. The function will fail if the -// number of deployments is not equal to one or if the deployment has multiple -// containers -func replaceImage(manifestPath, image string) error { - yamlFile, err := ioutil.ReadFile(manifestPath) - if err != nil { - return err - } - foundDeployment := false - newManifest := []byte{} - scanner := internalk8sutil.NewYAMLScanner(bytes.NewBuffer(yamlFile)) - for scanner.Scan() { - yamlSpec := scanner.Bytes() - - decoded := make(map[string]interface{}) - err = yaml.Unmarshal(yamlSpec, &decoded) - if err != nil { - return err - } - kind, ok := decoded["kind"].(string) - if !ok || kind != "Deployment" { - newManifest = internalk8sutil.CombineManifests(newManifest, yamlSpec) - continue - } - if foundDeployment { - return fmt.Errorf("cannot use `image` flag on namespaced manifest with more than 1 deployment") - } - foundDeployment = true - scheme := runtime.NewScheme() - // scheme for client go - if err := cgoscheme.AddToScheme(scheme); err != nil { - log.Fatalf("Failed to add client-go scheme to runtime client: (%v)", err) - } - dynamicDecoder := serializer.NewCodecFactory(scheme).UniversalDeserializer() - - obj, _, err := dynamicDecoder.Decode(yamlSpec, nil, nil) - if err != nil { - return err - } - var dep appsv1.Deployment - switch o := obj.(type) { - case *appsv1.Deployment: - dep = *o - default: - return fmt.Errorf("error in replaceImage switch case; could not convert runtime.Object" + - " to deployment") - } - if len(dep.Spec.Template.Spec.Containers) != 1 { - return fmt.Errorf("cannot use `image` flag on namespaced manifest containing more" + - " than 1 container in the operator deployment") - } - dep.Spec.Template.Spec.Containers[0].Image = image - updatedYamlSpec, err := yaml.Marshal(dep) - if err != nil { - return fmt.Errorf("failed to convert deployment object back to yaml: %v", err) - } - newManifest = internalk8sutil.CombineManifests(newManifest, updatedYamlSpec) - } - if err := scanner.Err(); err != nil { - return fmt.Errorf("failed to scan %s: %v", manifestPath, err) - } - - return ioutil.WriteFile(manifestPath, newManifest, fileutil.DefaultFileMode) -} diff --git a/go.mod b/go.mod index 78529af89a..a528b61d85 100644 --- a/go.mod +++ b/go.mod @@ -17,7 +17,6 @@ require ( github.com/onsi/gomega v1.9.0 github.com/operator-framework/api v0.3.8 github.com/operator-framework/operator-registry v1.12.6-0.20200611222234-275301b779f8 - github.com/pborman/uuid v1.2.0 github.com/prometheus/client_golang v1.5.1 github.com/prometheus/common v0.9.1 github.com/rogpeppe/go-internal v1.5.0 @@ -29,7 +28,6 @@ require ( github.com/spf13/viper v1.4.0 github.com/stretchr/testify v1.5.1 go.uber.org/zap v1.14.1 - golang.org/x/net v0.0.0-20200301022130-244492dfa37a golang.org/x/tools v0.0.0-20200403190813-44a64ad78b9b gomodules.xyz/jsonpatch/v3 v3.0.1 gopkg.in/yaml.v2 v2.2.8 diff --git a/go.sum b/go.sum index ec0127b5b5..92581414e6 100644 --- a/go.sum +++ b/go.sum @@ -1061,9 +1061,8 @@ golang.org/x/net v0.0.0-20191002035440-2ec189313ef0/go.mod h1:z5CRVTTTmAJ677TzLL golang.org/x/net v0.0.0-20191004110552-13f9640d40b9/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20191028085509-fe3aa8a45271/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20191112182307-2180aed22343/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b h1:0mm1VjtFUOIlE1SbDlwjYaDxZVDP2S5ou6y0gSgXHu8= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200301022130-244492dfa37a h1:GuSPYbZzB5/dcLNCwLQLsg3obCJtX9IJhpXkvY7kzk0= -golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20181106182150-f42d05182288/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= diff --git a/hack/tests/e2e-ansible-molecule.sh b/hack/tests/e2e-ansible-molecule.sh index d91ebf6576..e9a0a865f9 100755 --- a/hack/tests/e2e-ansible-molecule.sh +++ b/hack/tests/e2e-ansible-molecule.sh @@ -16,14 +16,6 @@ pip3 install --user ansible-lint yamllint pip3 install --user docker openshift jmespath ansible-galaxy collection install community.kubernetes -deploy_prereqs() { - header_text "Deploying resources" - kubectl create -f "$OPERATORDIR/deploy/service_account.yaml" - kubectl create -f "$OPERATORDIR/deploy/role.yaml" - kubectl create -f "$OPERATORDIR/deploy/role_binding.yaml" - kubectl create -f "$OPERATORDIR/deploy/crds/ansible.example.com_memcacheds_crd.yaml" -} - remove_prereqs() { header_text "Deleting resources" kubectl delete --wait=true --ignore-not-found=true --timeout=60s -f "$OPERATORDIR/deploy/crds/ansible.example.com_memcacheds_crd.yaml" @@ -56,7 +48,7 @@ header_text "Test local" pushd memcached-operator sed -i".bak" -E -e 's/(FROM quay.io\/operator-framework\/ansible-operator)(:.*)?/\1:dev/g' build/Dockerfile; rm -f build/Dockerfile.bak OPERATORDIR="$(pwd)" -TEST_CLUSTER_PORT=24443 operator-sdk test local --operator-namespace default +TEST_CLUSTER_PORT=24443 TEST_OPERATOR_NAMESPACE=default molecule test -s test-local remove_prereqs diff --git a/hack/tests/scaffolding/e2e-go-scaffold.sh b/hack/tests/scaffolding/e2e-go-scaffold.sh deleted file mode 100755 index c0286c2af4..0000000000 --- a/hack/tests/scaffolding/e2e-go-scaffold.sh +++ /dev/null @@ -1,59 +0,0 @@ -#!/usr/bin/env bash - -set -ex - -source hack/lib/test_lib.sh - -function download_old_sdk_binary() { - VERSION="v0.18.1" - echo "Getting SDK $VERSION release binary..." - BIN_URL="" - OS=$(go env GOOS) - if [ "$OS" = "darwin" ]; then - BIN_URL="https://github.com/operator-framework/operator-sdk/releases/download/$VERSION/operator-sdk-$VERSION-x86_64-apple-darwin" - elif [ "$OS" = "linux" ]; then - BIN_URL="https://github.com/operator-framework/operator-sdk/releases/download/$VERSION/operator-sdk-$VERSION-x86_64-linux-gnu" - else - echo "Failed to get SDK $VERSION release binary for $OS" - exit 1 - fi - - curl -o operator-sdk-old -L $BIN_URL - chmod +x ./operator-sdk-old -} - -ROOTDIR="$(pwd)" -BASEPROJECTDIR="$(mktemp -d)" -IMAGE_NAME="quay.io/example/memcached-operator:v0.0.1" - -# change IMAGE_NAME in case it is redefined by a flag -# parse "--image-name value" format -ORIG_ARGS=("$@") -while [[ $# -gt 0 ]] -do -key="$1" -case $key in - -image-name|--image-name) # "--image-name value" format - IMAGE_NAME="$2" - shift - ;; - -image-name=*|--image-name=*) # "--image-name=value" format - IMAGE_NAME="${key#*=}" - shift - ;; - *) # different arg/flag - shift - ;; -esac -done - -set -- "${ORIG_ARGS[@]}" - -go build -o $BASEPROJECTDIR/scaffold-memcached $ROOTDIR/hack/tests/scaffolding/scaffold-memcached.go - -pushd "$BASEPROJECTDIR" -# scaffold-memcached uses "operator-sdk-old new ..." to scaffold the legacy project -# since "new" is not present in the new CLI -download_old_sdk_binary -./scaffold-memcached --local-repo $ROOTDIR --image-name=$IMAGE_NAME --local-image $@ -popd diff --git a/hack/tests/scaffolding/scaffold-memcached.go b/hack/tests/scaffolding/scaffold-memcached.go deleted file mode 100644 index 33a7178f99..0000000000 --- a/hack/tests/scaffolding/scaffold-memcached.go +++ /dev/null @@ -1,226 +0,0 @@ -// Copyright 2019 The Operator-SDK 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 main - -import ( - "bytes" - "flag" - "fmt" - "io/ioutil" - "os" - "os/exec" - "path/filepath" - "regexp" - "strings" - - "github.com/operator-framework/operator-sdk/internal/util/fileutil" - - log "github.com/sirupsen/logrus" -) - -// TODO: Migrate most/all of the cli commands to the bash script instead of keeping them here - -const ( - sdkRepo = "github.com/operator-framework/operator-sdk" - operatorName = "memcached-operator" - testRepo = "github.com/example-inc/" + operatorName -) - -func main() { - localRepo := flag.String("local-repo", "", "Path to local SDK repository being tested."+ - " Only use when running e2e tests locally") - imageName := flag.String("image-name", "", "Name of image being used for tests") - noPull := flag.Bool("local-image", false, "Disable pulling images as image is local") - flag.Parse() - // get global framework variables - sdkTestE2EDir, err := os.Getwd() - if err != nil { - log.Fatal(err) - } - defer func() { - if err := os.Chdir(sdkTestE2EDir); err != nil { - log.Errorf("Failed to change back to original working directory: (%v)", err) - } - }() - localSDKPath := *localRepo - if localSDKPath == "" { - localSDKPath = sdkTestE2EDir - } - - // Post v0.18.1 "operator-sdk new" is not part of the new CLI - // so we use an older release binary that should already be present locally - // from hack/tests/scaffolding/e2e-go-scaffold.sh - log.Print("Creating new operator project") - cmdOut, err := exec.Command("./operator-sdk-old", - "new", - operatorName, - "--repo", testRepo, - "--skip-validation").CombinedOutput() - if err != nil { - log.Fatalf("Error: %v\nCommand Output: %s\n", err, string(cmdOut)) - } - - if err := os.Chdir(operatorName); err != nil { - log.Fatalf("Failed to change to %s directory: (%v)", operatorName, err) - } - - // Always use the local SDK path in the go.mod "replace" line. - modBytes, err := insertGoModReplaceDir(sdkRepo, localSDKPath) - if err != nil { - log.Fatalf("Failed to insert go.mod replace: %v", err) - } - log.Printf("go.mod: %v", string(modBytes)) - cmdOut, err = exec.Command("go", "build", "./...").CombinedOutput() - if err != nil { - log.Fatalf("Command \"go build ./...\" failed after modifying go.mod: %v\nCommand Output:\n%v", - err, string(cmdOut)) - } - - // Set replicas to 2 to test leader election. In production, this should - // almost always be set to 1, because there isn't generally value in having - // a hot spare operator process. - opYaml, err := ioutil.ReadFile("deploy/operator.yaml") - if err != nil { - log.Fatalf("Could not read deploy/operator.yaml: %v", err) - } - newOpYaml := bytes.Replace(opYaml, []byte("replicas: 1"), []byte("replicas: 2"), 1) - err = ioutil.WriteFile("deploy/operator.yaml", newOpYaml, 0644) - if err != nil { - log.Fatalf("Could not write deploy/operator.yaml: %v", err) - } - - cmd := exec.Command("operator-sdk", - "add", - "api", - "--api-version=cache.example.com/v1alpha1", - "--kind=Memcached") - cmd.Env = os.Environ() - cmdOut, err = cmd.CombinedOutput() - if err != nil { - log.Fatalf("Error: %v\nCommand Output: %s\n", err, string(cmdOut)) - } - cmdOut, err = exec.Command("operator-sdk", - "add", - "controller", - "--api-version=cache.example.com/v1alpha1", - "--kind=Memcached").CombinedOutput() - if err != nil { - log.Fatalf("Error: %v\nCommand Output: %s\n", err, string(cmdOut)) - } - - tmplFiles := map[string]string{ - filepath.Join(localSDKPath, "example/memcached-operator/memcached_controller.go.tmpl"): "pkg/controller/memcached/memcached_controller.go", - filepath.Join(localSDKPath, "test/e2e/_incluster-test-code/main_test.go"): "test/e2e/main_test.go", - filepath.Join(localSDKPath, "test/e2e/_incluster-test-code/memcached_test.go"): "test/e2e/memcached_test.go", - } - - for src, dst := range tmplFiles { - if err := os.MkdirAll(filepath.Dir(dst), fileutil.DefaultDirFileMode); err != nil { - log.Fatalf("Could not create template destination directory: %s", err) - } - cmdOut, err = exec.Command("cp", src, dst).CombinedOutput() - if err != nil { - log.Fatalf("Error: %v\nCommand Output: %s\n", err, string(cmdOut)) - } - } - - memcachedTypesFile, err := ioutil.ReadFile("pkg/apis/cache/v1alpha1/memcached_types.go") - if err != nil { - log.Fatalf("Could not read pkg/apis/cache/v1alpha1/memcached_types.go: %v", err) - } - - memcachedTypesFileLines := bytes.Split(memcachedTypesFile, []byte("\n")) - for lineNum, line := range memcachedTypesFileLines { - if strings.Contains(string(line), "type MemcachedSpec struct {") { - memcachedTypesFileLinesIntermediate := append(memcachedTypesFileLines[:lineNum+1], - []byte("\tSize int32 `json:\"size\"`")) - memcachedTypesFileLines = append(memcachedTypesFileLinesIntermediate, - memcachedTypesFileLines[lineNum+3:]...) - break - } - } - - for lineNum, line := range memcachedTypesFileLines { - if strings.Contains(string(line), "type MemcachedStatus struct {") { - memcachedTypesFileLinesIntermediate := append(memcachedTypesFileLines[:lineNum+1], - []byte("\tNodes []string `json:\"nodes\"`")) - memcachedTypesFileLines = append(memcachedTypesFileLinesIntermediate, - memcachedTypesFileLines[lineNum+3:]...) - break - } - } - - if err := os.Remove("pkg/apis/cache/v1alpha1/memcached_types.go"); err != nil { - log.Fatalf("Failed to remove old memcached_type.go file: (%v)", err) - } - err = ioutil.WriteFile("pkg/apis/cache/v1alpha1/memcached_types.go", bytes.Join(memcachedTypesFileLines, - []byte("\n")), fileutil.DefaultFileMode) - if err != nil { - log.Fatalf("Could not write to pkg/apis/cache/v1alpha1/memcached_types.go: %v", err) - } - - log.Print("Generating k8s") - cmdOut, err = exec.Command("operator-sdk", "generate", "k8s").CombinedOutput() - if err != nil { - log.Fatalf("Error: %v\nCommand Output: %s\n", err, string(cmdOut)) - } - - log.Print("Generating CRDs") - cmdOut, err = exec.Command("operator-sdk", "generate", "crds").CombinedOutput() - if err != nil { - log.Fatalf("Error: %v\nCommand Output: %s\n", err, string(cmdOut)) - } - - log.Print("Pulling new dependencies with go mod") - cmdOut, err = exec.Command("go", "build", "./...").CombinedOutput() - if err != nil { - log.Fatalf("Command \"go build ./...\" failed: %v\nCommand Output:\n%v", err, string(cmdOut)) - } - - operatorYAML, err := ioutil.ReadFile("deploy/operator.yaml") - if err != nil { - log.Fatalf("Could not read deploy/operator.yaml: %v", err) - } - if *imageName == "" { - *imageName = "quay.io/example/memcached-operator:v0.0.1" - } - if *noPull { - operatorYAML = bytes.Replace(operatorYAML, []byte("imagePullPolicy: Always"), []byte("imagePullPolicy: Never"), - 1) - } - operatorYAML = bytes.Replace(operatorYAML, []byte("REPLACE_IMAGE"), []byte(*imageName), 1) - err = ioutil.WriteFile("deploy/operator.yaml", operatorYAML, fileutil.DefaultFileMode) - if err != nil { - log.Fatalf("Failed to write to deploy/operator.yaml: %v", err) - } -} - -func insertGoModReplaceDir(repo, path string) ([]byte, error) { - modBytes, err := ioutil.ReadFile("go.mod") - if err != nil { - return nil, fmt.Errorf("failed to read go.mod: %w", err) - } - // Remove all replace lines in go.mod. - replaceRe := regexp.MustCompile(fmt.Sprintf("(replace )?%s =>.+", repo)) - modBytes = replaceRe.ReplaceAll(modBytes, nil) - // Append the desired replace to the end of go.mod's bytes. - sdkReplace := fmt.Sprintf("replace %s => %s", repo, path) - modBytes = append(modBytes, []byte("\n"+sdkReplace)...) - err = ioutil.WriteFile("go.mod", modBytes, fileutil.DefaultFileMode) - if err != nil { - return nil, fmt.Errorf("failed to write go.mod before replacing SDK repo: %w", err) - } - return modBytes, nil -} diff --git a/hack/tests/subcommand.sh b/hack/tests/subcommand.sh deleted file mode 100755 index dfe972e8c0..0000000000 --- a/hack/tests/subcommand.sh +++ /dev/null @@ -1,55 +0,0 @@ -#!/usr/bin/env bash -source hack/lib/test_lib.sh - -set -ex - -if [ -z "$KUBECONFIG" ]; then - KUBECONFIG=$HOME/.kube/config -fi - -pushd test/test-framework -# test building image with image-build-args having a space within quotes -operator-sdk build operator-sdk-dev --image-build-args="--label some.name=\"First Last\"" - -# test framework with defaults -operator-sdk test local ./test/e2e - -# test operator-sdk test flags -operator-sdk test local ./test/e2e --global-manifest deploy/crds/cache.example.com_memcacheds_crd.yaml --namespaced-manifest deploy/namespace-init.yaml --go-test-flags "-parallel 1" --kubeconfig $KUBECONFIG --image=quay.io/coreos/operator-sdk-dev:test-framework-operator-runtime - -# we use the test-memcached namespace for all future tests, so we only need to set this trap once -kubectl create namespace test-memcached -trap_add 'kubectl delete namespace test-memcached || true' EXIT -operator-sdk test local ./test/e2e --operator-namespace=test-memcached -kubectl delete namespace test-memcached - -# test operator in 'run local' mode -kubectl create namespace test-memcached -operator-sdk test local ./test/e2e --up-local --operator-namespace=test-memcached -kubectl delete namespace test-memcached - -# test operator in up local mode with --watch-namespace flag -kubectl create namespace test-memcached -operator-sdk test local ./test/e2e --up-local --operator-namespace=test-memcached --watch-namespace="" -kubectl delete namespace test-memcached - -# test operator in 'run local' mode with kubeconfig -kubectl create namespace test-memcached -operator-sdk test local ./test/e2e --up-local --operator-namespace=test-memcached --kubeconfig $KUBECONFIG -kubectl delete namespace test-memcached - -# test operator in no-setup mode -kubectl create namespace test-memcached -kubectl create -f deploy/crds/cache.example.com_memcacheds_crd.yaml -# this runs after the popd at the end, so it needs the path from the project root -trap_add 'kubectl delete -f test/test-framework/deploy/crds/cache.example.com_memcacheds_crd.yaml' EXIT -kubectl create -f deploy/crds/cache.example.com_memcachedrs_crd.yaml -# this runs after the popd at the end, so it needs the path from the project root -trap_add 'kubectl delete -f test/test-framework/deploy/crds/cache.example.com_memcachedrs_crd.yaml' EXIT -kubectl create -f deploy/service_account.yaml --namespace test-memcached -kubectl create -f deploy/role.yaml --namespace test-memcached -kubectl create -f deploy/role_binding.yaml --namespace test-memcached -kubectl create -f deploy/operator.yaml --namespace test-memcached -operator-sdk test local ./test/e2e --operator-namespace=test-memcached --no-setup -kubectl delete namespace test-memcached -popd diff --git a/pkg/test/client.go b/pkg/test/client.go deleted file mode 100644 index 0f2c98d4a0..0000000000 --- a/pkg/test/client.go +++ /dev/null @@ -1,117 +0,0 @@ -// Copyright 2018 The Operator-SDK 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 test - -import ( - goctx "context" - "fmt" - - apierrors "k8s.io/apimachinery/pkg/api/errors" - "k8s.io/apimachinery/pkg/runtime" - "k8s.io/apimachinery/pkg/util/wait" - "k8s.io/client-go/util/retry" - dynclient "sigs.k8s.io/controller-runtime/pkg/client" -) - -type frameworkClient struct { - dynclient.Client -} - -var _ FrameworkClient = &frameworkClient{} - -type FrameworkClient interface { - Get(gCtx goctx.Context, key dynclient.ObjectKey, obj runtime.Object) error - List(gCtx goctx.Context, list runtime.Object, opts ...dynclient.ListOption) error - Create(gCtx goctx.Context, obj runtime.Object, cleanupOptions *CleanupOptions) error - Delete(gCtx goctx.Context, obj runtime.Object, opts ...dynclient.DeleteOption) error - Update(gCtx goctx.Context, obj runtime.Object) error -} - -func retryOnAnyError(err error) bool { - return !apierrors.IsNotFound(err) -} - -// Create uses the dynamic client to create an object and then adds a -// cleanup function to delete it when Cleanup is called. In addition to -// the standard controller-runtime client options -func (f *frameworkClient) Create(gCtx goctx.Context, obj runtime.Object, cleanupOptions *CleanupOptions) error { - objCopy := obj.DeepCopyObject() - err := f.Client.Create(gCtx, obj) - if err != nil { - return err - } - // if no test context exists, cannot add finalizer function or print to testing log - if cleanupOptions == nil || cleanupOptions.TestContext == nil { - return nil - } - key, err := dynclient.ObjectKeyFromObject(objCopy) - // this function fails silently if t is nil - if cleanupOptions.TestContext.t != nil { - cleanupOptions.TestContext.t.Logf("resource type %+v with namespace/name (%+v) created\n", - objCopy.GetObjectKind().GroupVersionKind().Kind, key) - } - cleanupOptions.TestContext.AddCleanupFn(func() error { - err = retry.OnError(retry.DefaultRetry, retryOnAnyError, func() error { - return f.Client.Delete(gCtx, objCopy) - }) - if err != nil && !apierrors.IsNotFound(err) { - return err - } - if cleanupOptions.Timeout == 0 && cleanupOptions.RetryInterval != 0 { - return fmt.Errorf("retry interval is set but timeout is not; cannot poll for cleanup") - } else if cleanupOptions.Timeout != 0 && cleanupOptions.RetryInterval == 0 { - return fmt.Errorf("timeout is set but retry interval is not; cannot poll for cleanup") - } - if cleanupOptions.Timeout != 0 && cleanupOptions.RetryInterval != 0 { - return wait.PollImmediate(cleanupOptions.RetryInterval, cleanupOptions.Timeout, func() (bool, error) { - err = f.Client.Get(gCtx, key, objCopy) - if err != nil { - if apierrors.IsNotFound(err) { - if cleanupOptions.TestContext.t != nil { - cleanupOptions.TestContext.t.Logf("resource type %+v with namespace/name (%+v)"+ - " successfully deleted\n", objCopy.GetObjectKind().GroupVersionKind().Kind, key) - } - return true, nil - } - return false, fmt.Errorf("error encountered during deletion of resource type %v with"+ - " namespace/name (%+v): %v", objCopy.GetObjectKind().GroupVersionKind().Kind, key, err) - } - if cleanupOptions.TestContext.t != nil { - cleanupOptions.TestContext.t.Logf("waiting for deletion of resource type %+v with"+ - " namespace/name (%+v)\n", objCopy.GetObjectKind().GroupVersionKind().Kind, key) - } - return false, nil - }) - } - return nil - }) - return nil -} - -func (f *frameworkClient) Get(gCtx goctx.Context, key dynclient.ObjectKey, obj runtime.Object) error { - return f.Client.Get(gCtx, key, obj) -} - -func (f *frameworkClient) List(gCtx goctx.Context, list runtime.Object, opts ...dynclient.ListOption) error { - return f.Client.List(gCtx, list, opts...) -} - -func (f *frameworkClient) Delete(gCtx goctx.Context, obj runtime.Object, opts ...dynclient.DeleteOption) error { - return f.Client.Delete(gCtx, obj, opts...) -} - -func (f *frameworkClient) Update(gCtx goctx.Context, obj runtime.Object) error { - return f.Client.Update(gCtx, obj) -} diff --git a/pkg/test/context.go b/pkg/test/context.go deleted file mode 100644 index b637161c96..0000000000 --- a/pkg/test/context.go +++ /dev/null @@ -1,132 +0,0 @@ -// Copyright 2018 The Operator-SDK 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 test - -import ( - "os" - "testing" - "time" - - "github.com/pborman/uuid" - log "github.com/sirupsen/logrus" - "k8s.io/client-go/kubernetes" - "k8s.io/client-go/restmapper" -) - -type Context struct { - id string - cleanupFns []cleanupFn - // the namespace is deprecated - // todo: remove before 1.0.0 - // use operatorNamespace or watchNamespace instead - namespace string - operatorNamespace string - watchNamespace string - t *testing.T - - namespacedManPath string - client *frameworkClient - kubeclient kubernetes.Interface - restMapper *restmapper.DeferredDiscoveryRESTMapper - skipCleanupOnError bool -} - -// todo(camilamacedo86): Remove the following line just added for we are able to deprecated TestCtx -// need to be done before: 1.0.0 - -// Deprecated: TestCtx exists for historical compatibility. Use Context instead. -type TestCtx = Context //nolint:golint - -type CleanupOptions struct { - TestContext *Context - Timeout time.Duration - RetryInterval time.Duration -} - -type cleanupFn func() error - -func (f *Framework) newContext(t *testing.T) *Context { - - // Context is used among others for namespace names where '/' is forbidden and must be 63 characters or less - id := "osdk-e2e-" + uuid.New() - - var operatorNamespace string - _, ok := os.LookupEnv(TestOperatorNamespaceEnv) - if ok { - operatorNamespace = f.OperatorNamespace - } - - watchNamespace := operatorNamespace - ns, ok := os.LookupEnv(TestWatchNamespaceEnv) - if ok { - watchNamespace = ns - } - - return &Context{ - id: id, - t: t, - namespace: operatorNamespace, - operatorNamespace: operatorNamespace, - watchNamespace: watchNamespace, - namespacedManPath: *f.NamespacedManPath, - client: f.Client, - kubeclient: f.KubeClient, - restMapper: f.restMapper, - skipCleanupOnError: f.skipCleanupOnError, - } -} - -// Deprecated: NewTestCtx exists for historical compatibility. Use NewContext instead. -func NewTestCtx(t *testing.T) *TestCtx { - return Global.newContext(t) -} - -func NewContext(t *testing.T) *Context { - return Global.newContext(t) -} - -func (ctx *Context) GetID() string { - return ctx.id -} - -func (ctx *Context) Cleanup() { - if ctx.t != nil { - // The cleanup function will be skipped - if ctx.t.Failed() && ctx.skipCleanupOnError { - // Also, could we log the error here? - log.Info("Skipping cleanup function since --skip-cleanup-error is true") - return - } - } - failed := false - for i := len(ctx.cleanupFns) - 1; i >= 0; i-- { - err := ctx.cleanupFns[i]() - if err != nil { - failed = true - if ctx.t != nil { - ctx.t.Errorf("A cleanup function failed with error: (%v)\n", err) - } else { - log.Errorf("A cleanup function failed with error: (%v)", err) - } - } - } - if ctx.t == nil && failed { - log.Fatal("A cleanup function failed") - } -} - -func (ctx *Context) AddCleanupFn(fn cleanupFn) { - ctx.cleanupFns = append(ctx.cleanupFns, fn) -} diff --git a/pkg/test/context_test.go b/pkg/test/context_test.go deleted file mode 100644 index 2929c3d942..0000000000 --- a/pkg/test/context_test.go +++ /dev/null @@ -1,44 +0,0 @@ -// Copyright 2019 The Operator-SDK 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 test - -import ( - "strings" - "testing" - - "github.com/pborman/uuid" -) - -func TestNewContextCreatesID(t *testing.T) { - fakeNamespacedManPath := "fakePath" - Global = &Framework{ - NamespacedManPath: &fakeNamespacedManPath, - } - - ctx := NewContext(t) - - if strings.Index(ctx.GetID(), "osdk-e2e-") != 0 { - t.Error("ID should start with osdk-e2e-") - } - - idUUID := uuid.Parse(strings.Replace(ctx.GetID(), "osdk-e2e-", "", 1)) - if idUUID == nil { - t.Error("ID should end with a UUID") - } - - if len(ctx.GetID()) > 63 { - t.Error("ID should be no more than 63 characters long, so that it may be a valid namespace name") - } -} diff --git a/pkg/test/e2eutil/wait_util.go b/pkg/test/e2eutil/wait_util.go deleted file mode 100644 index 46c6e82c3c..0000000000 --- a/pkg/test/e2eutil/wait_util.go +++ /dev/null @@ -1,104 +0,0 @@ -// Copyright 2018 The Operator-SDK 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 e2eutil - -import ( - "context" - "testing" - "time" - - "github.com/operator-framework/operator-sdk/pkg/test" - - apierrors "k8s.io/apimachinery/pkg/api/errors" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/runtime" - "k8s.io/apimachinery/pkg/util/wait" - "k8s.io/client-go/kubernetes" - "sigs.k8s.io/controller-runtime/pkg/client" -) - -// WaitForDeployment checks to see if a given deployment has a certain number of available replicas after a specified -// amount of time. If the deployment does not have the required number of replicas after 5 * retries seconds, -// the function returns an error. This can be used in multiple ways, like verifying that a required resource is ready -// before trying to use it, or to test. Failure handling, like simulated in SimulatePodFail. -func WaitForDeployment(t *testing.T, kubeclient kubernetes.Interface, namespace, name string, replicas int, - retryInterval, timeout time.Duration) error { - return waitForDeployment(t, kubeclient, namespace, name, replicas, retryInterval, timeout, false) -} - -// WaitForOperatorDeployment has the same functionality as WaitForDeployment but will no wait for the deployment if the -// test was run with a locally run operator (--up-local flag) -func WaitForOperatorDeployment(t *testing.T, kubeclient kubernetes.Interface, namespace, name string, replicas int, - retryInterval, timeout time.Duration) error { - return waitForDeployment(t, kubeclient, namespace, name, replicas, retryInterval, timeout, true) -} - -func waitForDeployment(t *testing.T, kubeclient kubernetes.Interface, namespace, name string, replicas int, - retryInterval, timeout time.Duration, isOperator bool) error { - if isOperator && test.Global.LocalOperator { - t.Log("Operator is running locally; skip waitForDeployment") - return nil - } - err := wait.Poll(retryInterval, timeout, func() (done bool, err error) { - deployment, err := kubeclient.AppsV1().Deployments(namespace).Get(context.TODO(), name, metav1.GetOptions{}) - if err != nil { - if apierrors.IsNotFound(err) { - t.Logf("Waiting for availability of Deployment: %s in Namespace: %s \n", name, namespace) - return false, nil - } - return false, err - } - - if int(deployment.Status.AvailableReplicas) >= replicas { - return true, nil - } - t.Logf("Waiting for full availability of %s deployment (%d/%d)\n", name, - deployment.Status.AvailableReplicas, replicas) - return false, nil - }) - if err != nil { - return err - } - t.Logf("Deployment available (%d/%d)\n", replicas, replicas) - return nil -} - -func WaitForDeletion(t *testing.T, dynclient client.Client, obj runtime.Object, retryInterval, - timeout time.Duration) error { - key, err := client.ObjectKeyFromObject(obj) - if err != nil { - return err - } - - kind := obj.GetObjectKind().GroupVersionKind().Kind - ctx, cancel := context.WithTimeout(context.Background(), timeout) - defer cancel() - err = wait.Poll(retryInterval, timeout, func() (done bool, err error) { - err = dynclient.Get(ctx, key, obj) - if apierrors.IsNotFound(err) { - return true, nil - } - if err != nil { - return false, err - } - t.Logf("Waiting for %s %s to be deleted\n", kind, key) - return false, nil - }) - if err != nil { - return err - } - t.Logf("%s %s was deleted\n", kind, key) - return nil -} diff --git a/pkg/test/framework.go b/pkg/test/framework.go deleted file mode 100755 index f3a2d6bb78..0000000000 --- a/pkg/test/framework.go +++ /dev/null @@ -1,298 +0,0 @@ -// Copyright 2018 The Operator-SDK 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 test - -import ( - "bytes" - goctx "context" - "flag" - "fmt" - "io/ioutil" - "os" - "os/exec" - "path/filepath" - "strings" - "sync" - "testing" - "time" - - // Import all Kubernetes client auth plugins (e.g. Azure, GCP, OIDC, etc.) - _ "k8s.io/client-go/plugin/pkg/client/auth" - - "github.com/operator-framework/operator-sdk/internal/scaffold" - k8sInternal "github.com/operator-framework/operator-sdk/internal/util/k8sutil" - "github.com/operator-framework/operator-sdk/internal/util/projutil" - "github.com/operator-framework/operator-sdk/pkg/k8sutil" - - log "github.com/sirupsen/logrus" - extscheme "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/scheme" - "k8s.io/apimachinery/pkg/runtime" - "k8s.io/apimachinery/pkg/util/wait" - cached "k8s.io/client-go/discovery/cached" - "k8s.io/client-go/kubernetes" - cgoscheme "k8s.io/client-go/kubernetes/scheme" - "k8s.io/client-go/rest" - "k8s.io/client-go/restmapper" - "k8s.io/client-go/tools/clientcmd" - dynclient "sigs.k8s.io/controller-runtime/pkg/client" -) - -var ( - // Global framework struct - Global *Framework -) - -type Framework struct { - Client *frameworkClient - KubeConfig *rest.Config - KubeClient kubernetes.Interface - Scheme *runtime.Scheme - NamespacedManPath *string - OperatorNamespace string - WatchNamespace string - - restMapper *restmapper.DeferredDiscoveryRESTMapper - - projectRoot string - globalManPath string - localOperatorArgs string - kubeconfigPath string - schemeMutex sync.Mutex - LocalOperator bool - skipCleanupOnError bool -} - -type frameworkOpts struct { - projectRoot string - kubeconfigPath string - globalManPath string - namespacedManPath string - localOperatorArgs string - isLocalOperator bool - skipCleanupOnError bool -} - -const ( - ProjRootFlag = "root" - KubeConfigFlag = "kubeconfig" - NamespacedManPathFlag = "namespacedMan" - GlobalManPathFlag = "globalMan" - LocalOperatorFlag = "localOperator" - LocalOperatorArgs = "localOperatorArgs" - SkipCleanupOnErrorFlag = "skipCleanupOnError" - - TestOperatorNamespaceEnv = "TEST_OPERATOR_NAMESPACE" - TestWatchNamespaceEnv = "TEST_WATCH_NAMESPACE" -) - -func (opts *frameworkOpts) addToFlagSet(flagset *flag.FlagSet) { - flagset.StringVar(&opts.projectRoot, ProjRootFlag, "", "path to project root") - flagset.StringVar(&opts.namespacedManPath, NamespacedManPathFlag, "", "path to rbac manifest") - flagset.BoolVar(&opts.isLocalOperator, LocalOperatorFlag, false, - "enable if operator is running locally (not in cluster)") - flagset.StringVar(&opts.globalManPath, GlobalManPathFlag, "", "path to operator manifest") - flagset.StringVar(&opts.localOperatorArgs, LocalOperatorArgs, "", - "flags that the operator needs (while using --up-local). example: \"--flag1 value1 --flag2=value2\"") - flagset.BoolVar(&opts.skipCleanupOnError, SkipCleanupOnErrorFlag, false, - "If set as true, the cleanup function responsible to remove all artifacts "+ - "will be skipped if an error is faced.") -} - -func newFramework(opts *frameworkOpts) (*Framework, error) { - kubeconfig, kcNamespace, err := k8sInternal.GetKubeconfigAndNamespace(opts.kubeconfigPath) - if err != nil { - return nil, fmt.Errorf("failed to build the kubeconfig: %w", err) - } - - operatorNamespace := kcNamespace - ns, ok := os.LookupEnv(TestOperatorNamespaceEnv) - if ok && ns != "" { - operatorNamespace = ns - } - - kubeclient, err := kubernetes.NewForConfig(kubeconfig) - if err != nil { - return nil, fmt.Errorf("failed to build the kubeclient: %w", err) - } - - scheme := runtime.NewScheme() - if err := cgoscheme.AddToScheme(scheme); err != nil { - return nil, fmt.Errorf("failed to add cgo scheme to runtime scheme: %w", err) - } - if err := extscheme.AddToScheme(scheme); err != nil { - return nil, fmt.Errorf("failed to add api extensions scheme to runtime scheme: %w", err) - } - - cachedDiscoveryClient := cached.NewMemCacheClient(kubeclient.Discovery()) - restMapper := restmapper.NewDeferredDiscoveryRESTMapper(cachedDiscoveryClient) - restMapper.Reset() - - dynClient, err := dynclient.New(kubeconfig, dynclient.Options{Scheme: scheme, Mapper: restMapper}) - if err != nil { - return nil, fmt.Errorf("failed to build the dynamic client: %w", err) - } - framework := &Framework{ - Client: &frameworkClient{Client: dynClient}, - KubeConfig: kubeconfig, - KubeClient: kubeclient, - Scheme: scheme, - NamespacedManPath: &opts.namespacedManPath, - OperatorNamespace: operatorNamespace, - LocalOperator: opts.isLocalOperator, - - projectRoot: opts.projectRoot, - globalManPath: opts.globalManPath, - localOperatorArgs: opts.localOperatorArgs, - kubeconfigPath: opts.kubeconfigPath, - restMapper: restMapper, - skipCleanupOnError: opts.skipCleanupOnError, - } - return framework, nil -} - -type addToSchemeFunc func(*runtime.Scheme) error - -// AddToFrameworkScheme allows users to add the scheme for their custom resources -// to the framework's scheme for use with the dynamic client. The user provides -// the addToScheme function (located in the register.go file of their operator -// project) and the List struct for their custom resource. For example, for a -// memcached operator, the list stuct may look like: -// &MemcachedList{} -// The List object is needed because the CRD has not always been fully registered -// by the time this function is called. If the CRD takes more than 5 seconds to -// become ready, this function throws an error -func AddToFrameworkScheme(addToScheme addToSchemeFunc, obj runtime.Object) error { - return Global.addToScheme(addToScheme, obj) -} - -func (f *Framework) addToScheme(addToScheme addToSchemeFunc, obj runtime.Object) error { - f.schemeMutex.Lock() - defer f.schemeMutex.Unlock() - - err := addToScheme(f.Scheme) - if err != nil { - return err - } - f.restMapper.Reset() - dynClient, err := dynclient.New(f.KubeConfig, dynclient.Options{Scheme: f.Scheme, Mapper: f.restMapper}) - if err != nil { - return fmt.Errorf("failed to initialize new dynamic client: %w", err) - } - err = wait.PollImmediate(time.Second, time.Second*10, func() (done bool, err error) { - ns, ok := os.LookupEnv(TestOperatorNamespaceEnv) - if ok && ns != "" { - err = dynClient.List(goctx.TODO(), obj, dynclient.InNamespace(f.OperatorNamespace)) - } else { - err = dynClient.List(goctx.TODO(), obj, dynclient.InNamespace("default")) - } - if err != nil { - f.restMapper.Reset() - return false, nil - } - f.Client = &frameworkClient{Client: dynClient} - return true, nil - }) - if err != nil { - return fmt.Errorf("failed to build the dynamic client: %w", err) - } - return nil -} - -func (f *Framework) runM(m *testing.M) (int, error) { - // setup context to use when setting up crd - ctx := f.newContext(nil) - defer ctx.Cleanup() - - // go test always runs from the test directory; change to project root - err := os.Chdir(f.projectRoot) - if err != nil { - return 0, fmt.Errorf("failed to change directory to project root: %w", err) - } - - // create crd - globalYAML, err := ioutil.ReadFile(f.globalManPath) - if err != nil { - return 0, fmt.Errorf("failed to read global resource manifest: %w", err) - } - err = ctx.createFromYAML(globalYAML, true, &CleanupOptions{TestContext: ctx}) - if err != nil { - return 0, fmt.Errorf("failed to create resource(s) in global resource manifest: %w", err) - } - - if !f.LocalOperator { - return m.Run(), nil - } - - // start local operator before running tests - outBuf := &bytes.Buffer{} - localCmd, err := f.setupLocalCommand() - if err != nil { - return 0, fmt.Errorf("failed to setup local command: %w", err) - } - localCmd.Stdout = outBuf - localCmd.Stderr = outBuf - - err = localCmd.Start() - if err != nil { - return 0, fmt.Errorf("failed to run operator locally: %w", err) - } - log.Info("Started local operator") - - // run the tests - exitCode := m.Run() - - // kill the local operator and print its logs - err = localCmd.Process.Kill() - if err != nil { - log.Warn("Failed to stop local operator process") - } - fmt.Printf("\n------ Local operator output ------\n%s\n", outBuf.String()) - return exitCode, nil -} - -func (f *Framework) setupLocalCommand() (*exec.Cmd, error) { - projectName := filepath.Base(projutil.MustGetwd()) - outputBinName := filepath.Join(scaffold.BuildBinDir, projectName+"-local") - opts := projutil.GoCmdOptions{ - BinName: outputBinName, - PackagePath: filepath.Join(projutil.GetGoPkg(), filepath.ToSlash(scaffold.ManagerDir)), - } - if err := projutil.GoBuild(opts); err != nil { - return nil, fmt.Errorf("failed to build local operator binary: %w", err) - } - - args := []string{} - if f.localOperatorArgs != "" { - args = append(args, strings.Split(f.localOperatorArgs, " ")...) - } - - localCmd := exec.Command(outputBinName, args...) - - if f.kubeconfigPath != "" { - localCmd.Env = append(os.Environ(), fmt.Sprintf("%v=%v", k8sutil.KubeConfigEnvVar, f.kubeconfigPath)) - } else { - // we can hardcode index 0 as that is the highest priority kubeconfig to be loaded and will always - // be populated by NewDefaultClientConfigLoadingRules() - localCmd.Env = append(os.Environ(), fmt.Sprintf("%v=%v", k8sutil.KubeConfigEnvVar, - clientcmd.NewDefaultClientConfigLoadingRules().Precedence[0])) - } - watchNamespace := f.OperatorNamespace - ns, ok := os.LookupEnv(TestWatchNamespaceEnv) - if ok { - watchNamespace = ns - } - localCmd.Env = append(localCmd.Env, fmt.Sprintf("%v=%v", k8sutil.WatchNamespaceEnvVar, watchNamespace)) - return localCmd, nil -} diff --git a/pkg/test/main_entry.go b/pkg/test/main_entry.go deleted file mode 100644 index 0095a95484..0000000000 --- a/pkg/test/main_entry.go +++ /dev/null @@ -1,56 +0,0 @@ -// Copyright 2018 The Operator-SDK 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 test - -import ( - "flag" - "os" - "testing" - - log "github.com/sirupsen/logrus" -) - -func MainEntry(m *testing.M) { - fopts := &frameworkOpts{} - fopts.addToFlagSet(flag.CommandLine) - // controller-runtime registers the --kubeconfig flag in client config - // package: - // https://github.com/kubernetes-sigs/controller-runtime/blob/v0.5.2/pkg/client/config/config.go#L39 - // - // If this flag is not registered, do so. Otherwise retrieve its value. - kcFlag := flag.Lookup(KubeConfigFlag) - if kcFlag == nil { - flag.StringVar(&fopts.kubeconfigPath, KubeConfigFlag, "", "path to kubeconfig") - } - - flag.Parse() - - if kcFlag != nil { - fopts.kubeconfigPath = kcFlag.Value.String() - } - - f, err := newFramework(fopts) - if err != nil { - log.Fatalf("Failed to create framework: %v", err) - } - - Global = f - - exitCode, err := f.runM(m) - if err != nil { - log.Fatal(err) - } - os.Exit(exitCode) -} diff --git a/pkg/test/resource_creator.go b/pkg/test/resource_creator.go deleted file mode 100644 index dd045b95de..0000000000 --- a/pkg/test/resource_creator.go +++ /dev/null @@ -1,159 +0,0 @@ -// Copyright 2018 The Operator-SDK 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 test - -import ( - "bytes" - goctx "context" - "fmt" - "io/ioutil" - "os" - "time" - - "golang.org/x/net/context" - core "k8s.io/api/core/v1" - apierrors "k8s.io/apimachinery/pkg/api/errors" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "k8s.io/apimachinery/pkg/util/wait" - "sigs.k8s.io/yaml" - - "github.com/operator-framework/operator-sdk/internal/util/k8sutil" -) - -// TODO: remove before 1.0.0 -// Deprecated: GetNamespace() exists for historical compatibility. -// Use GetOperatorNamespace() or GetWatchNamespace() instead -func (ctx *Context) GetNamespace() (string, error) { - var err error - ctx.namespace, err = ctx.getNamespace(ctx.namespace) - return ctx.namespace, err -} - -// GetOperatorNamespace will return an Operator Namespace, -// if the flag --operator-namespace not be used (TestOpeatorNamespaceEnv not set) -// then it will create a new namespace with randon name and return that namespace -func (ctx *Context) GetOperatorNamespace() (string, error) { - var err error - ctx.operatorNamespace, err = ctx.getNamespace(ctx.operatorNamespace) - return ctx.operatorNamespace, err -} - -func (ctx *Context) getNamespace(ns string) (string, error) { - if ns != "" { - return ns, nil - } - // create namespace - ns = ctx.GetID() - namespaceObj := &core.Namespace{ObjectMeta: metav1.ObjectMeta{Name: ns}} - _, err := ctx.kubeclient.CoreV1().Namespaces().Create(context.TODO(), namespaceObj, metav1.CreateOptions{}) - if apierrors.IsAlreadyExists(err) { - return "", fmt.Errorf("namespace %s already exists: %w", ns, err) - } else if err != nil { - return "", err - } - ctx.AddCleanupFn(func() error { - gracePeriodSeconds := int64(0) - opts := metav1.DeleteOptions{GracePeriodSeconds: &gracePeriodSeconds} - return ctx.kubeclient.CoreV1().Namespaces().Delete(context.TODO(), ns, opts) - }) - return ns, nil -} - -// GetWatchNamespace will return the namespaces to operator -// watch for changes, if the flag --watch-namespaced not be used -// then it will return the Operator Namespace. -func (ctx *Context) GetWatchNamespace() (string, error) { - // if ctx.watchNamespace is already set and not ""; - // then return ctx.watchnamespace - if ctx.watchNamespace != "" { - return ctx.watchNamespace, nil - } - // if ctx.watchNamespace == ""; - // ensure it was set explicitly using TestWatchNamespaceEnv - if ns, ok := os.LookupEnv(TestWatchNamespaceEnv); ok { - return ns, nil - } - // get ctx.operatorNamespace (use ctx.GetOperatorNamespace() - // to make sure ctx.operatorNamespace is not "") - operatorNamespace, err := ctx.GetOperatorNamespace() - if err != nil { - return "", nil - } - ctx.watchNamespace = operatorNamespace - return ctx.watchNamespace, nil -} - -func (ctx *Context) createFromYAML(yamlFile []byte, skipIfExists bool, cleanupOptions *CleanupOptions) error { - operatorNamespace, err := ctx.GetOperatorNamespace() - if err != nil { - return err - } - scanner := k8sutil.NewYAMLScanner(bytes.NewBuffer(yamlFile)) - for scanner.Scan() { - yamlSpec := scanner.Bytes() - - obj := &unstructured.Unstructured{} - jsonSpec, err := yaml.YAMLToJSON(yamlSpec) - if err != nil { - return fmt.Errorf("could not convert yaml file to json: %w", err) - } - if err := obj.UnmarshalJSON(jsonSpec); err != nil { - return fmt.Errorf("failed to unmarshal object spec: %w", err) - } - obj.SetNamespace(operatorNamespace) - err = ctx.client.Create(goctx.TODO(), obj, cleanupOptions) - if skipIfExists && apierrors.IsAlreadyExists(err) { - continue - } - if err != nil { - _, restErr := ctx.restMapper.RESTMappings(obj.GetObjectKind().GroupVersionKind().GroupKind()) - if restErr == nil { - return err - } - // don't store error, as only error will be timeout. Error from runtime client will be easier for - // the user to understand than the timeout error, so just use that if we fail - _ = wait.PollImmediate(time.Second*1, time.Second*10, func() (bool, error) { - ctx.restMapper.Reset() - _, err := ctx.restMapper.RESTMappings(obj.GetObjectKind().GroupVersionKind().GroupKind()) - if err != nil { - return false, nil - } - return true, nil - }) - err = ctx.client.Create(goctx.TODO(), obj, cleanupOptions) - if skipIfExists && apierrors.IsAlreadyExists(err) { - continue - } - if err != nil { - return err - } - } - } - - if err := scanner.Err(); err != nil { - return fmt.Errorf("failed to scan manifest: %w", err) - } - return nil -} - -func (ctx *Context) InitializeClusterResources(cleanupOptions *CleanupOptions) error { - // create namespaced resources - namespacedYAML, err := ioutil.ReadFile(ctx.namespacedManPath) - if err != nil { - return fmt.Errorf("failed to read namespaced manifest: %w", err) - } - return ctx.createFromYAML(namespacedYAML, false, cleanupOptions) -} diff --git a/pkg/test/resource_creator_test.go b/pkg/test/resource_creator_test.go deleted file mode 100644 index 918b3a7e81..0000000000 --- a/pkg/test/resource_creator_test.go +++ /dev/null @@ -1,204 +0,0 @@ -// Copyright 2018 The Operator-SDK 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 test - -import ( - "os" - "testing" - - "k8s.io/client-go/kubernetes/fake" -) - -const ( - OperatorNamespaceEnv = "TEST_OPERATOR_NAMESPACE" - WatchNamespaceEnv = "TEST_WATCH_NAMESPACE" -) - -var fakeNamespacedManPath string = "fakePath" - -func TestGetOperatorNamespace(t *testing.T) { - Global = &Framework{ - NamespacedManPath: &fakeNamespacedManPath, - KubeClient: fake.NewSimpleClientset(), - } - - t.Run("should create a non-empty new Operator Namespace when OperatorNamespace env is not set", - func(t *testing.T) { - Global.OperatorNamespace = "" - ctx := NewContext(t) - operatorNamespace, err := ctx.GetOperatorNamespace() - assertNoError(t, err) - if len(operatorNamespace) <= 0 { - t.Errorf("Expected non-empty operatorNamespace") - } - }) - t.Run("should return Operator Namespace specified by OperatorNamespace Env", func(t *testing.T) { - operatorNamespace := "test-operator-namespae" - Global.OperatorNamespace = operatorNamespace - os.Setenv(TestOperatorNamespaceEnv, operatorNamespace) - defer func() { - Global.OperatorNamespace = "" - os.Unsetenv(OperatorNamespaceEnv) - }() - - ctx := NewContext(t) - got, err := ctx.GetOperatorNamespace() - assertNoError(t, err) - if got != operatorNamespace { - t.Errorf("Expected %v, got %v", operatorNamespace, got) - } - }) - t.Run("should return non-empty new Operator Namespace, when OperatorNamespace Env = \"\"", - func(t *testing.T) { - operatorNamespace := "" - Global.OperatorNamespace = operatorNamespace - os.Setenv(TestOperatorNamespaceEnv, operatorNamespace) - defer func() { - Global.OperatorNamespace = "" - os.Unsetenv(OperatorNamespaceEnv) - }() - - ctx := NewContext(t) - got, err := ctx.GetOperatorNamespace() - assertNoError(t, err) - if len(got) <= 0 { - t.Errorf("Expected non-empty operatorNamespace") - } - }) -} - -func TestGetWatchNamespace(t *testing.T) { - Global = &Framework{ - NamespacedManPath: &fakeNamespacedManPath, - KubeClient: fake.NewSimpleClientset(), - } - - t.Run("should return Watch Namespace (==Operator Namespace) WatchNamespace Env is not set", - func(t *testing.T) { - Global.WatchNamespace = "" - Global.OperatorNamespace = "" - ctx := NewContext(t) - watchNamespace, err := ctx.GetWatchNamespace() - assertNoError(t, err) - operatorNamespace, err := ctx.GetOperatorNamespace() - assertNoError(t, err) - if len(operatorNamespace) <= 0 { - t.Errorf("Expected non-empty operatorNamespace") - } - if watchNamespace != operatorNamespace { - t.Errorf("Expected watchNamespace: %s, got %s", operatorNamespace, watchNamespace) - } - }) - - t.Run("should return Watch Namespace (==WatchNamespace Env) WatchNamespace Env is set", - func(t *testing.T) { - watchNamespace := "test-watch-namespace" - Global.WatchNamespace = watchNamespace - Global.OperatorNamespace = "" - os.Setenv(WatchNamespaceEnv, watchNamespace) - defer func() { - Global.WatchNamespace = "" - defer os.Unsetenv(WatchNamespaceEnv) - }() - - ctx := NewContext(t) - got, err := ctx.GetWatchNamespace() - assertNoError(t, err) - if watchNamespace != got { - t.Errorf("Expected watchNamespace: %s, got %s", watchNamespace, got) - } - operatorNamespace, err := ctx.GetOperatorNamespace() - assertNoError(t, err) - if len(operatorNamespace) <= 0 { - t.Errorf("Expected non-empty operatorNamespace") - } - if watchNamespace == operatorNamespace { - t.Errorf("Expected operator-Namespace: %v, to be different than watch-Namespace: %v", - operatorNamespace, watchNamespace) - } - }) - - t.Run("should return Watch Namespace (==WatchNamespace Env) even when WatchNamespace Env is set to \"\"", - func(t *testing.T) { - watchNamespace := "" - Global.WatchNamespace = watchNamespace - Global.OperatorNamespace = "" - os.Setenv(WatchNamespaceEnv, watchNamespace) - defer func() { - Global.WatchNamespace = "" - defer os.Unsetenv(WatchNamespaceEnv) - }() - - ctx := NewContext(t) - - got, err := ctx.GetWatchNamespace() - assertNoError(t, err) - if watchNamespace != got { - t.Errorf("Expected watchNamespace: %s, got %s", watchNamespace, got) - } - operatorNamespace, err := ctx.GetOperatorNamespace() - assertNoError(t, err) - if len(operatorNamespace) <= 0 { - t.Errorf("Expected non-empty operatorNamespace") - } - if watchNamespace == operatorNamespace { - t.Errorf("Expected operator-Namespace: %v, to be different than watch-Namespace: %v", - operatorNamespace, watchNamespace) - } - }) - - t.Run("should return Watch Namespace and Operator Namespace when both Env are set through Env var", - func(t *testing.T) { - watchNamespace := "test-watch-namespace" - operatorNamespace := "test-operator-namespace" - Global.WatchNamespace = watchNamespace - Global.OperatorNamespace = operatorNamespace - os.Setenv(OperatorNamespaceEnv, operatorNamespace) - os.Setenv(WatchNamespaceEnv, watchNamespace) - defer func() { - Global.WatchNamespace = "" - Global.OperatorNamespace = "" - defer os.Unsetenv(OperatorNamespaceEnv) - defer os.Unsetenv(WatchNamespaceEnv) - }() - - ctx := NewContext(t) - gotWatchNamespace, err := ctx.GetWatchNamespace() - assertNoError(t, err) - if watchNamespace != gotWatchNamespace { - t.Errorf("Expected watchNamespace: %s, got %s", watchNamespace, gotWatchNamespace) - } - - gotOperatorNamespace, err := ctx.GetOperatorNamespace() - assertNoError(t, err) - if len(gotOperatorNamespace) <= 0 { - t.Errorf("Expected non-empty operatorNamespace") - } - if gotWatchNamespace == gotOperatorNamespace { - t.Errorf("Expected operator-Namespace: %v, to be different than watch-Namespace: %v", - operatorNamespace, watchNamespace) - } - if gotOperatorNamespace != operatorNamespace { - t.Errorf("Expected operatorNamespace: %s, got %s", operatorNamespace, gotOperatorNamespace) - } - }) -} - -func assertNoError(t *testing.T, err error) { - t.Helper() - if err != nil { - t.Errorf("Expected no error, %v", err) - } -} diff --git a/test/test-framework/test/e2e/main_test.go b/test/test-framework/test/e2e/main_test.go deleted file mode 100644 index 210d906294..0000000000 --- a/test/test-framework/test/e2e/main_test.go +++ /dev/null @@ -1,25 +0,0 @@ -// Copyright 2018 The Operator-SDK 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 e2e - -import ( - "testing" - - f "github.com/operator-framework/operator-sdk/pkg/test" -) - -func TestMain(m *testing.M) { - f.MainEntry(m) -} diff --git a/test/test-framework/test/e2e/memcached_test.go b/test/test-framework/test/e2e/memcached_test.go deleted file mode 100644 index 1ba93bb125..0000000000 --- a/test/test-framework/test/e2e/memcached_test.go +++ /dev/null @@ -1,125 +0,0 @@ -// Copyright 2018 The Operator-SDK 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 e2e - -import ( - goctx "context" - "fmt" - "testing" - "time" - - framework "github.com/operator-framework/operator-sdk/pkg/test" - "github.com/operator-framework/operator-sdk/pkg/test/e2eutil" - apis "github.com/operator-framework/operator-sdk/test/test-framework/pkg/apis" - operator "github.com/operator-framework/operator-sdk/test/test-framework/pkg/apis/cache/v1alpha1" - - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/types" - "k8s.io/client-go/util/retry" -) - -const ( - retryInterval = time.Second * 5 - timeout = time.Second * 90 - cleanupRetryInterval = time.Second * 1 - cleanupTimeout = time.Second * 5 -) - -func TestMemcached(t *testing.T) { - memcachedList := &operator.MemcachedList{} - err := framework.AddToFrameworkScheme(apis.AddToScheme, memcachedList) - if err != nil { - t.Fatalf("Failed to add custom resource scheme to framework: %v", err) - } - // run subtests - t.Run("memcached-group", func(t *testing.T) { - t.Run("Cluster", MemcachedCluster) - t.Run("Cluster2", MemcachedCluster) - }) -} - -func memcachedScaleTest(t *testing.T, f *framework.Framework, ctx *framework.Context, fromReplicas, toReplicas int) error { - namespace, err := ctx.GetOperatorNamespace() - if err != nil { - return fmt.Errorf("could not get namespace: %v", err) - } - // create memcached custom resource - exampleMemcached := &operator.Memcached{ - ObjectMeta: metav1.ObjectMeta{ - Name: "example-memcached", - Namespace: namespace, - }, - Spec: operator.MemcachedSpec{ - Size: int32(fromReplicas), - }, - } - key := types.NamespacedName{Name: exampleMemcached.GetName(), Namespace: exampleMemcached.GetNamespace()} - // use Context's create helper to create the object and add a cleanup function for the new object - err = f.Client.Create(goctx.TODO(), exampleMemcached, &framework.CleanupOptions{TestContext: ctx, Timeout: cleanupTimeout, RetryInterval: cleanupRetryInterval}) - if err != nil { - return fmt.Errorf("could not create %q: %v", key, err) - } - // wait for example-memcached to reach `fromReplicas` replicas - err = e2eutil.WaitForDeployment(t, f.KubeClient, namespace, "example-memcached", fromReplicas, retryInterval, timeout) - if err != nil { - return fmt.Errorf("failed waiting for %d deployment/%s replicas: %v", fromReplicas, key.Name, err) - } - - err = retry.RetryOnConflict(retry.DefaultBackoff, func() error { - err = f.Client.Get(goctx.TODO(), key, exampleMemcached) - if err != nil { - return fmt.Errorf("could not get memcached CR %q: %v", key, err) - } - - exampleMemcached.Spec.Size = int32(toReplicas) - t.Logf("Attempting memcached %q update, resourceVersion: %s", key, exampleMemcached.GetResourceVersion()) - return f.Client.Update(goctx.TODO(), exampleMemcached) - }) - if err != nil { - return fmt.Errorf("could not update memcached CR %q: %v", key, err) - } - - // wait for example-memcached to reach `toReplicas` replicas - if err := e2eutil.WaitForDeployment(t, f.KubeClient, key.Namespace, key.Name, toReplicas, retryInterval, timeout); err != nil { - return fmt.Errorf("failed waiting for %d deployment/%s replicas: %v", toReplicas, key.Name, err) - } - return nil -} - -func MemcachedCluster(t *testing.T) { - t.Parallel() - ctx := framework.NewContext(t) - defer ctx.Cleanup() - err := ctx.InitializeClusterResources(&framework.CleanupOptions{TestContext: ctx, Timeout: cleanupTimeout, RetryInterval: cleanupRetryInterval}) - if err != nil { - t.Fatalf("Failed to initialize cluster resources: %v", err) - } - t.Log("Initialized cluster resources") - namespace, err := ctx.GetOperatorNamespace() - if err != nil { - t.Fatal(err) - } - // get global framework variables - f := framework.Global - // wait for memcached-operator to be ready - err = e2eutil.WaitForOperatorDeployment(t, f.KubeClient, namespace, "memcached-operator", 1, retryInterval, timeout) - if err != nil { - t.Fatal(err) - } - - if err = memcachedScaleTest(t, f, ctx, 3, 4); err != nil { - t.Fatal(err) - } -} diff --git a/website/content/en/docs/ansible/testing-guide.md b/website/content/en/docs/ansible/testing-guide.md index f5be2eaa93..daa0510990 100644 --- a/website/content/en/docs/ansible/testing-guide.md +++ b/website/content/en/docs/ansible/testing-guide.md @@ -14,7 +14,7 @@ To begin, you sould have: - [Molecule](https://github.com/ansible/molecule) >= v3.0 - [Ansible](https://github.com/ansible/ansible) >= v2.9 - [The OpenShift Python client](https://github.com/openshift/openshift-restclient-python) >= v0.8 -- An initialized Ansible Operator project, with the molecule directory present. +- An initialized Ansible Operator project, with the molecule directory present. **NOTE** If you initialized a project with a previous version of operator-sdk, you can generate a new dummy project and copy in the `molecule` directory. Just be sure to generate the dummy project with the same `api-version` and `kind`, or some of the generated files will not work without modification. Your top-level project structure should look like this: @@ -143,9 +143,7 @@ cluster or external registry, and can run in CI environments that allow users to (such as Travis). It brings up a Kubernetes-in-docker cluster, builds your Operator, deploys it into the cluster, and then creates an instance of your CustomResource and runs your assertions to make sure the Operator responded properly. -You can run this scenario with -`molecule test -s local`, which is equivalent to `operator-sdk test local`, or with `molecule converge -s test-local`, which will leave the environment up -afterward. +You can run this scenario with `molecule test -s local`, or with `molecule converge -s test-local` which will leave the environment up afterward. The scenario has the following structure: @@ -189,11 +187,6 @@ will reset it. - `molecule test` performs a full loop, bringing a cluster up, preparing it, running your tasks, and tearing it down. - `molecule converge` is more useful for iterative development, as it leaves your environment up between runs. This can cause unexpected problems if you end up corrupting your environment during testing, but running `molecule destroy` will reset it. -## operator-sdk test local - -The `operator-sdk test local` command kicks off an end-to-end test of your Operator. It will run the -`test-local` molecule scenario described above. - ## Writing tests ### Adding a task @@ -242,7 +235,7 @@ The file should now look like this: ``` Now that we have a functional Operator, and an assertion of its behavior, we can verify that everything is working -by running `operator-sdk test local`. +by running `molecule test -s local`. #### The Ansible `assert` and `fail` modules These modules are handy for adding assertions and failure conditions to your Ansible Operator tests: diff --git a/website/content/en/docs/cli/operator-sdk.md b/website/content/en/docs/cli/operator-sdk.md index ce775fb2a4..e6423bd9f9 100644 --- a/website/content/en/docs/cli/operator-sdk.md +++ b/website/content/en/docs/cli/operator-sdk.md @@ -27,6 +27,5 @@ An SDK for building operators with ease * [operator-sdk new](../operator-sdk_new) - Creates a new operator application * [operator-sdk olm](../operator-sdk_olm) - Manage the Operator Lifecycle Manager installation in your cluster * [operator-sdk run](../operator-sdk_run) - Run an Operator in a variety of environments -* [operator-sdk test](../operator-sdk_test) - Tests the operator * [operator-sdk version](../operator-sdk_version) - Prints the version of operator-sdk diff --git a/website/content/en/docs/cli/operator-sdk_test.md b/website/content/en/docs/cli/operator-sdk_test.md deleted file mode 100644 index 20be25e70d..0000000000 --- a/website/content/en/docs/cli/operator-sdk_test.md +++ /dev/null @@ -1,23 +0,0 @@ ---- -title: "operator-sdk test" ---- -## operator-sdk test - -Tests the operator - -### Synopsis - -The test command has subcommands that can test the operator. - - -### Options - -``` - -h, --help help for test -``` - -### SEE ALSO - -* [operator-sdk](../operator-sdk) - An SDK for building operators with ease -* [operator-sdk test local](../operator-sdk_test_local) - Run End-To-End tests locally - diff --git a/website/content/en/docs/cli/operator-sdk_test_local.md b/website/content/en/docs/cli/operator-sdk_test_local.md deleted file mode 100644 index df8c625927..0000000000 --- a/website/content/en/docs/cli/operator-sdk_test_local.md +++ /dev/null @@ -1,39 +0,0 @@ ---- -title: "operator-sdk test local" ---- -## operator-sdk test local - -Run End-To-End tests locally - -### Synopsis - -Run End-To-End tests locally - -``` -operator-sdk test local [flags] -``` - -### Options - -``` - --debug Enable debug-level logging - --global-manifest string Path to manifest for Global resources (e.g. CRD manifests) - --go-test-flags string Additional flags to pass to go test - -h, --help help for local - --image string Use a different operator image from the one specified in the namespaced manifest - --kubeconfig string Kubeconfig path - --local-operator-flags string The flags that the operator needs (while using --up-local). Example: "--flag1 value1 --flag2=value2" - --molecule-test-flags string Additional flags to pass to molecule test - --namespace string (Deprecated: use --operator-namespace instead) If non-empty, single namespace to run tests in - --namespaced-manifest string Path to manifest for per-test, namespaced resources (e.g. RBAC and Operator manifest) - --no-setup Disable test resource creation - --operator-namespace string Namespace where the operator will be deployed, CRs will be created and tests will be executed (By default it will be in the default namespace defined in the kubeconfig) - --skip-cleanup-error If set as true, the cleanup function responsible to remove all artifacts will be skipped if an error is faced. - --up-local Enable running operator locally with go run instead of as an image in the cluster - --watch-namespace string (only valid with --up-local) Namespace where the operator watches for changes. Set "" for AllNamespaces, set "ns1,ns2" for MultiNamespace(if not set then watches Operator Namespace -``` - -### SEE ALSO - -* [operator-sdk test](../operator-sdk_test) - Tests the operator - diff --git a/website/content/en/docs/contribution-guidelines/testing/travis-build.md b/website/content/en/docs/contribution-guidelines/testing/travis-build.md index 981e9853dd..bd23ba4af3 100644 --- a/website/content/en/docs/contribution-guidelines/testing/travis-build.md +++ b/website/content/en/docs/contribution-guidelines/testing/travis-build.md @@ -35,53 +35,16 @@ The Go, Ansible, and Helm tests then differ in what tests they run. 3. Check that all error messages start with a lower case alphabetical character and do not end with punctuation, and log messages start with an upper case alphabetical character. 4. Make sure the repo is in a clean state (this is particularly useful for making sure `go.mod` and `go.sum` are up-to-date after running `make tidy`). 2. Run unit tests. - 1. Run `make test`. -3. Run [subcommand tests][subcommand]. - 1. Run `test local` with no flags enabled. - 2. Run `test local` with most configuration flags enabled. - 3. Run `test local` in single namespace mode. - 4. Run `test local` with `--up-local` flag. - 5. Run `test local` with both `--up-local` and `--kubeconfig` flags. - 6. Create all test resources with kubectl and run `test local` with `--no-setup` flag. - 7. Run `scorecard` subcommand and check that expected score matches actual score. - 8. Run `scorecard` subcommand with json output enabled and verify the output. -4. Run [go e2e tests][go-e2e]. - 1. Scaffold a project using `hack/tests/scaffolding/e2e-go-scaffold.sh` - 2. Build `memcached-operator` image to be used in tests - 3. Run scaffolded project e2e tests using `operator-sdk run local` - 1. Run cluster test (namespace is auto-generated and deleted by test framework). - 1. Deploy operator and required resources to the cluster. - 2. Run the leader election test. - 1. Verify that operator deployment is ready. - 2. Verify that leader configmap specifies 1 leader and that the memcached operator has 2 pods (configuration for this is done in step 4.1). - 3. Delete current leader and wait for memcached-operator deployment to become ready again. - 4. Verify that leader configmap specifies 1 leader and that the memcached-operator has 2 pods. - 5. Verify that the name of the new leader is different from the name of the old leader. - 3. Run the memcached scale test. - 1. Create memcached CR specifying a desired cluster size of 3 and wait until memcached cluster is of size 3. - 2. Increase desired cluster size to 4 and wait until memcached cluster is of size 4. - 4. Run the memcached metrics test. - 1. Make sure the metrics Service was created. - 2. Get metrics via proxy pod and make sure they are present. - 3. Perform linting of the existing metrics. - 5. Run the memcached custom resource metrics test. - 1. Make sure the metrics Service was created. - 2. Get metrics via proxy pod and make sure they are present. - 3. Perform linting of the existing metrics. - 4. Perform checks on each custom resource generated metric and makes sure the name, type, value, labels and metric are correct. - 2. Run local test (namespace is auto-generated and deleted by test framework). - 1. Start operator using the `run local` subcommand. - 2. Run memcached scale test (described in step 4.3.1.3) - 4. Run [TLS library tests][tls-tests]. - 1. This test runs multiple simple tests of the operator-sdk's TLS library. The tests run in parallel and each tests runs in its own namespace. + 1. Run `make test-unit`. +3. Run [go e2e tests][go-e2e]. + 1. Run `make test-e2e-go`. ### Ansible tests -1. Run [ansible molecule tests][ansible-molecule]. (`make test-e2e-ansible-molecule) +1. Run [ansible molecule tests][ansible-molecule]. (`make test-e2e-ansible-molecule`) 1. Create and configure a new ansible type memcached-operator. 2. Create cluster resources. - 3. Run `operator-sdk test local` to run ansible molecule tests - 4. Change directory to [`test/ansible`][ansible-test] and run `operator-sdk test local` + 4. Change directory to [`test/ansible`][ansible-test] and run `molecule test -s local` **NOTE**: All created resources, including the namespace, are deleted using a bash trap when the test finishes @@ -108,9 +71,7 @@ The Go, Ansible, and Helm tests then differ in what tests they run. [k8s-script]: https://github.com/operator-framework/operator-sdk/blob/master/hack/ci/setup-k8s.sh [kind]: https://kind.sigs.k8s.io/ [sanity]: https://github.com/operator-framework/operator-sdk/blob/master/hack/tests/sanity-check.sh -[subcommand]: https://github.com/operator-framework/operator-sdk/blob/master/hack/tests/subcommand.sh -[go-e2e]: https://github.com/operator-framework/operator-sdk/blob/master/hack/tests/e2e-go.sh -[tls-tests]: https://github.com/operator-framework/operator-sdk/blob/master/test/e2e/tls_util_test.go +[go-e2e]: https://github.com/operator-framework/operator-sdk/blob/master/test/e2e/e2e_suite.go [ansible-molecule]: https://github.com/operator-framework/operator-sdk/blob/master/hack/tests/e2e-ansible-molecule.sh [ansible-test]: https://github.com/operator-framework/operator-sdk/tree/master/test/ansible [helm-e2e]: https://github.com/operator-framework/operator-sdk/blob/master/hack/tests/e2e-helm.sh diff --git a/website/content/en/docs/golang/legacy/e2e-tests.md b/website/content/en/docs/golang/legacy/e2e-tests.md deleted file mode 100644 index e444968559..0000000000 --- a/website/content/en/docs/golang/legacy/e2e-tests.md +++ /dev/null @@ -1,388 +0,0 @@ ---- -title: Writing E2E Tests -linkTitle: Writing E2E Tests -weight: 11 ---- -# Using the Operator SDK's Test Framework to Write E2E Tests - -End-to-end tests are essential to ensure that an operator works -as intended in real-world scenarios. The Operator SDK includes a testing -framework to make writing tests simpler and quicker by removing boilerplate -code and providing common test utilities. The Operator SDK includes the -test framework as a library under `pkg/test` and the e2e tests are written -as standard go tests. - -## Components - -The test framework includes a few components. The most important to talk -about are Framework and Context. - -### Framework - -[Framework][framework-link] contains all global variables, such as the kubeconfig, kubeclient, -scheme, and dynamic client (provided via the controller-runtime project). -It is initialized by the MainEntry function and can be used anywhere in the tests. - -**Note:** several required arguments are initialized and added by `MainEntry()`. Do not attempt to -use `testing.M` directly. - -### Context - -[Context][context-link] is a local context that stores important information for each test, such -as the namespace for that test and the cleanup functions. By handling -namespace and resource initialization through Context, we can make sure that all -resources are properly handled and removed after the test finishes. - -## Walkthrough: Writing Tests - -In this section, we will be walking through writing the e2e tests of the sample -[memcached-operator][memcached-sample]. - -### Main Test - -The first step to writing a test is to create the `main_test.go` file. The `main_test.go` -file simply calls the test framework's main entry that sets up the framework and then -starts the tests. It should be pretty much identical for all operators. This is what it -looks like for the memcached-operator: - -```go -package e2e - -import ( - "testing" - - f "github.com/operator-framework/operator-sdk/pkg/test" -) - -func TestMain(m *testing.M) { - f.MainEntry(m) -} -``` - -### Individual Tests - -In this section, we will be designing a test based on the [memcached_test.go][memcached-test-link] file -from the [memcached-operator][memcached-sample] sample. - -#### 1. Import the framework - -Once MainEntry sets up the framework, it runs the remainder of the tests. First, make -sure to import `testing`, the operator-sdk test framework (`pkg/test`) as well as your operator's libraries: - -```go -import ( - "testing" - - cachev1alpha1 "github.com/operator-framework/operator-sdk-samples/go/memcached-operator/pkg/apis/cache/v1alpha1" - "github.com/operator-framework/operator-sdk-samples/go/memcached-operator/pkg/apis" - - framework "github.com/operator-framework/operator-sdk/pkg/test" -) -``` - -#### 2. Register types with framework scheme - -The next step is to register your operator's scheme with the framework's dynamic client. -To do this, pass the CRD's `AddToScheme` function and its List type object to the framework's -[AddToFrameworkScheme][scheme-link] function. For our example memcached-operator, it looks like this: - -```go -memcachedList := &cachev1alpha1.MemcachedList{} -err := framework.AddToFrameworkScheme(apis.AddToScheme, memcachedList) -if err != nil { - t.Fatalf("failed to add custom resource scheme to framework: %v", err) -} -``` - -We pass in the CR List object `memcachedList` as an argument to `AddToFrameworkScheme()` because -the framework needs to ensure that the dynamic client has the REST mappings to query the API -server for the CR type. The framework will keep polling the API server for the mappings and -timeout after 5 seconds, returning an error if the mappings were not discovered in that time. - -#### 3. Setup the test context and resources - -The next step is to create a Context for the current test and defer its cleanup function: - -```go -ctx := framework.NewContext(t) -defer ctx.Cleanup() -``` - -Now that there is a `Context`, the test's Kubernetes resources (specifically the test namespace, -Service Account, RBAC, and Operator deployment in `local` testing; just the Operator deployment -in `cluster` testing) can be initialized: - -```go -err := ctx.InitializeClusterResources(&framework.CleanupOptions{TestContext: ctx, Timeout: cleanupTimeout, RetryInterval: cleanupRetryInterval}) -if err != nil { - t.Fatalf("failed to initialize cluster resources: %v", err) -} -``` - -The `InitializeClusterResources` function uses the custom `Create` function in the framework client to create the resources provided -in your namespaced manifest. The custom `Create` function use the controller-runtime's client to create resources and then -creates a cleanup function that is called by `ctx.Cleanup` which deletes the resource and then waits for the resource to be -fully deleted before returning. This is configurable with `CleanupOptions`. For info on how to use `CleanupOptions` see -[this section](#how-to-use-cleanup). - -If you want to make sure the operator's deployment is fully ready before moving onto the next part of the -test, the `WaitForOperatorDeployment` function from [e2eutil][e2eutil-link] (in the sdk under `pkg/test/e2eutil`) can be used: - -```go -// get namespace -namespace, err := ctx.GetOperatorNamespace() -if err != nil { - t.Fatal(err) -} -// get global framework variables -f := framework.Global -// wait for memcached-operator to be ready -err = e2eutil.WaitForOperatorDeployment(t, f.KubeClient, namespace, "memcached-operator", 1, time.Second*5, time.Second*30) -if err != nil { - t.Fatal(err) -} -``` - -#### 4. Write the test specific code - -Since the controller-runtime's dynamic client uses go contexts, make sure to import the go context library. -In this example, we imported it as `goctx`: - -##### How to use the Framework Client `Create`'s `CleanupOptions` - -The test framework provides `Client`, which exposes most of the controller-runtime's client unmodified, but the `Create` -function has added functionality to create cleanup functions for these resources as well. To manage how cleanup -is handled, we use a `CleanupOptions` struct. Here are some examples of how to use it: - -```go -// Create with no cleanup -Create(goctx.TODO(), exampleMemcached, &framework.CleanupOptions{}) - -// Create with cleanup but no polling for resources to be deleted -Create(goctx.TODO(), exampleMemcached, &framework.CleanupOptions{TestContext: ctx}) - -// Create with cleanup and polling wait for resources to be deleted -Create(goctx.TODO(), exampleMemcached, &framework.CleanupOptions{TestContext: ctx, Timeout: timeout, RetryInterval: retryInterval}) -``` - -This is how we can create a custom memcached custom resource with a size of 3: - -```go -// create memcached custom resource -exampleMemcached := &cachev1alpha1.Memcached{ - ObjectMeta: metav1.ObjectMeta{ - Name: "example-memcached", - Namespace: namespace, - }, - Spec: cachev1alpha1.MemcachedSpec{ - Size: 3, - }, -} -err = f.Client.Create(goctx.TODO(), exampleMemcached, &framework.CleanupOptions{TestContext: ctx, Timeout: time.Second * 5, RetryInterval: time.Second * 1}) -if err != nil { - return err -} -``` - -Now we can check if the operator successfully worked. In the case of the memcached operator, it should have -created a deployment called "example-memcached" with 3 replicas. To check, we use the `WaitForDeployment` function, which -is the same as `WaitForOperatorDeployment` with the exception that `WaitForOperatorDeployment` will skip waiting -for the deployment if the test is run locally and the `--up-local` flag is set; the `WaitForDeployment` function always -waits for the deployment: - -```go -// wait for example-memcached to reach 3 replicas -err = e2eutil.WaitForDeployment(t, f.KubeClient, namespace, "example-memcached", 3, time.Second*5, time.Second*30) -if err != nil { - return err -} -``` - -We can also test that the deployment scales correctly when the CR is updated: - -```go -err = f.Client.Get(goctx.TODO(), types.NamespacedName{Name: "example-memcached", Namespace: namespace}, exampleMemcached) -if err != nil { - return err -} -exampleMemcached.Spec.Size = 4 -err = f.Client.Update(goctx.TODO(), exampleMemcached) -if err != nil { - return err -} - -// wait for example-memcached to reach 4 replicas -err = e2eutil.WaitForDeployment(t, f.KubeClient, namespace, "example-memcached", 4, time.Second*5, time.Second*30) -if err != nil { - return err -} -``` - -Once the end of the function is reached, the Context's cleanup -functions will automatically be run since they were deferred when the Context was created. - -## Running the Tests - -To make running the tests simpler, the `operator-sdk` CLI tool has a `test` subcommand that can configure -default test settings, such as locations of your global resource manifest file (by default -`deploy/crd.yaml`) and your namespaced resource manifest file (by default `deploy/service_account.yaml` concatenated with -`deploy/rbac.yaml` and `deploy/operator.yaml`), and allows the user to configure runtime options. - -To run the tests, run the `operator-sdk test local` command in your project root and pass the location of the tests -as an argument. You can use `--help` to view the other configuration options and use `--go-test-flags` to pass in arguments to `go test`. Here is an example command: - -```shell -$ operator-sdk test local ./test/e2e --go-test-flags "-v -parallel=2" -``` - -### Image Flag - -If you wish to specify a different operator image than specified in your `operator.yaml` file (or a user-specified -namespaced manifest file), you can use the `--image` flag: - -```shell -$ operator-sdk test local ./test/e2e --image quay.io/example/my-operator:v0.0.2 -``` - -### Namespace Flag - -If you wish to run all the tests in 1 namespace (which also forces `-parallel=1`), you can use the `--namespace` flag: - -```shell -$ kubectl create namespace operator-test -$ operator-sdk test local ./test/e2e --operator-namespace operator-test -``` - -### Up-Local Flag - -To run the operator itself locally during the tests instead of starting a deployment in the cluster, you can use the -`--up-local` flag. This mode will still create global resources, but by default will not create any in-cluster namespaced -resources unless the user specifies one through the `--namespaced-manifest` flag. - -**NOTE**: The `--up-local` flag requires the `--operator-namespace` flag and the command will NOT create the namespace. Then, be sure that you are specifying a valid namespace. - -```shell -$ kubectl create namespace operator-test -$ operator-sdk test local ./test/e2e --operator-namespace operator-test --up-local -``` - -### No-Setup Flag - -If you would prefer to create the resources yourself and skip resource creation, you can use the `--no-setup` flag: -```shell -$ kubectl create namespace operator-test -$ kubectl create -f deploy/crds/cache.example.com_memcacheds_crd.yaml -$ kubectl create -f deploy/service_account.yaml --namespace operator-test -$ kubectl create -f deploy/role.yaml --namespace operator-test -$ kubectl create -f deploy/role_binding.yaml --namespace operator-test -$ kubectl create -f deploy/operator.yaml --namespace operator-test -$ operator-sdk test local ./test/e2e --operator-namespace operator-test --no-setup -``` - -### Test Permissions - -Executing e2e tests requires the permission to access, create, and delete resources on your cluster. Depending on what kind of Kubernetes cluster -you are using, this may require some manual setup. For example, OpenShift users are not created with cluster-admin access by default, so you would have -to manually add permissions to access these resources. - -The simplest way to accomplish this is to bind the cluster-admin Cluster Role to the Service Account you will run the test under. -If you are unable or unwilling to grant such access, a more limited permission set can be created and bound to your Service Account. -A good place to start would be the Role bound to your operator itself, such as [this role for the memcached operator example][memcached-role]. -In addition, you might have to create a Cluster Role to allow your tests to create namespaces, like so: -``` -apiVersion: rbac.authorization.k8s.io/v1 -kind: ClusterRole -metadata: - name: testuser -rules: -- apiGroups: - - "" - resources: - - namespaces - verbs: - - create - - delete - - get - - list - - watch - - update -``` - -Note that this isn't an exhaustive permission set, and the e2e tests you write might require more or less access. - -For more documentation on the `operator-sdk test local` command, see the [SDK CLI Reference][cli-test-local] doc. - -### Skip-Cleanup-Error Flag - -If the tests encounter an error, it is possible to tell the framework not to delete the resources. This behavior is enabled with the `--skip-cleanup-error` flag: - -```shell -$ operator-sdk test local ./test/e2e --skip-cleanup-error -``` - -This is useful if after the error happens, you need to inspect the resources that were created in the test or if you have automated scripts that download all the logs from the pods at the end of the test run. - -**NOTE**: The created resources will be deleted if the tests pass. - -### Running Go Test Directly (Not Recommended) - -For advanced use cases, it is possible to run the tests via `go test` directly. As long as all flags defined -in [MainEntry][main-entry-link] are declared, the tests will run correctly. Running the tests directly with missing flags -will result in undefined behavior. This is an example `go test` equivalent to the `operator-sdk test local` example above: - -```shell -# Combine service_account, role, role_binding, and operator manifests into namespaced manifest -$ cp deploy/service_account.yaml deploy/namespace-init.yaml -$ echo -e "\n---\n" >> deploy/namespace-init.yaml -$ cat deploy/role.yaml >> deploy/namespace-init.yaml -$ echo -e "\n---\n" >> deploy/namespace-init.yaml -$ cat deploy/role_binding.yaml >> deploy/namespace-init.yaml -$ echo -e "\n---\n" >> deploy/namespace-init.yaml -$ cat deploy/operator.yaml >> deploy/namespace-init.yaml -# Run tests -$ go test ./test/e2e/... -root=$(pwd) -kubeconfig=$HOME/.kube/config -globalMan deploy/crds/cache.example.com_apps_crd.yaml -namespacedMan deploy/namespace-init.yaml -v -parallel=2 -``` - -## Manual Cleanup - -While the test framework provides utilities that allow the test to automatically be cleaned up when done, -it is possible that an error in the test code could cause a panic, which would stop the test -without running the deferred cleanup. To clean up manually, you should check what namespaces currently exist -in your cluster. You can do this with `kubectl`: - -```shell -$ kubectl get namespaces - -Example Output: -NAME STATUS AGE -default Active 2h -kube-public Active 2h -kube-system Active 2h -main-1534287036 Active 23s -memcached-memcached-group-cluster-1534287037 Active 22s -memcached-memcached-group-cluster2-1534287037 Active 22s -``` - -The names of the namespaces will be either start with `main` or with the name of the tests and the suffix will -be a Unix timestamp (number of seconds since January 1, 1970 00:00 UTC). Kubectl can be used to delete these -namespaces and the resources in those namespaces: - -```shell -$ kubectl delete namespace main-153428703 -``` - -Since the CRD is not namespaced, it must be deleted separately. Clean up the CRD created by the tests using the CRD manifest `deploy/crd.yaml`: - -```shell -$ kubectl delete -f deploy/crds/cache.example.com_memcacheds_crd.yaml -``` - -[memcached-sample]:https://github.com/operator-framework/operator-sdk-samples/tree/master/go/memcached-operator -[framework-link]:https://github.com/operator-framework/operator-sdk/blob/master/pkg/test/framework.go -[context-link]:https://github.com/operator-framework/operator-sdk/blob/master/pkg/test/context.go -[e2eutil-link]:https://github.com/operator-framework/operator-sdk/tree/master/pkg/test/e2eutil -[memcached-test-link]:https://github.com/operator-framework/operator-sdk-samples/blob/master/go/memcached-operator/test/e2e/memcached_test.go -[scheme-link]:https://github.com/operator-framework/operator-sdk/blob/v0.17.0/pkg/test/framework.go#L176 -[cli-test-local]: /docs/cli/operator-sdk_test_local -[main-entry-link]:https://github.com/operator-framework/operator-sdk/blob/v0.17.0/pkg/test/main_entry.go#L25 -[memcached-role]:https://github.com/operator-framework/operator-sdk-samples/blob/master/go/memcached-operator/deploy/role.yaml