diff --git a/.travis.yml b/.travis.yml index 95bab8d30a..9e0f9fd021 100644 --- a/.travis.yml +++ b/.travis.yml @@ -25,13 +25,14 @@ install: script: - make install +- go test ./commands/... - go test ./pkg/... - go test ./test/e2e/... - cd test/test-framework # test framework with defaults -- operator-sdk test -t . +- operator-sdk test local . # test operator-sdk test flags -- operator-sdk test -t . -g deploy/crd.yaml -n deploy/namespace-init.yaml -f "-parallel 1" -k $HOME/.kube/config +- operator-sdk test local . -g deploy/crd.yaml -n deploy/namespace-init.yaml -f "-parallel 1" -k $HOME/.kube/config # go back to project root - cd ../.. - go vet ./... diff --git a/commands/operator-sdk/cmd/build.go b/commands/operator-sdk/cmd/build.go index 8747b93a4d..1110106ff2 100644 --- a/commands/operator-sdk/cmd/build.go +++ b/commands/operator-sdk/cmd/build.go @@ -15,17 +15,30 @@ package cmd import ( + "bytes" + "errors" "fmt" + "io/ioutil" + "log" "os" "os/exec" + "github.com/operator-framework/operator-sdk/commands/operator-sdk/cmd/cmdutil" cmdError "github.com/operator-framework/operator-sdk/commands/operator-sdk/error" + "github.com/operator-framework/operator-sdk/pkg/generator" + "github.com/ghodss/yaml" "github.com/spf13/cobra" ) +var ( + namespacedManBuild string + testLocationBuild string + enableTests bool +) + func NewBuildCmd() *cobra.Command { - return &cobra.Command{ + buildCmd := &cobra.Command{ Use: "build ", Short: "Compiles code and builds artifacts", Long: `The operator-sdk build command compiles the code, builds the executables, @@ -42,12 +55,86 @@ For example: `, Run: buildFunc, } + buildCmd.Flags().BoolVarP(&enableTests, "enable-tests", "e", false, "Enable in-cluster testing by adding test binary to the image") + buildCmd.Flags().StringVarP(&testLocationBuild, "test-location", "t", "./test/e2e", "Location of tests") + buildCmd.Flags().StringVarP(&namespacedManBuild, "namespaced", "n", "deploy/operator.yaml", "Path of namespaced resources for tests") + return buildCmd +} + +/* + * verifyDeploymentImages checks image names of pod 0 in deployments found in the provided yaml file. + * This is done because e2e tests require a namespaced manifest file to configure a namespace with + * required resources. This function is intended to identify if a user used a different image name + * for their operator in the provided yaml, which would result in the testing of the wrong operator + * image. As it is possible for a namespaced yaml to have multiple deployments (such as the vault + * operator, which depends on the etcd-operator), this is just a warning, not a fatal error. + */ +func verifyDeploymentImage(yamlFile []byte, imageName string) error { + warningMessages := "" + yamlSplit := bytes.Split(yamlFile, []byte("\n---\n")) + for _, yamlSpec := range yamlSplit { + yamlMap := make(map[string]interface{}) + err := yaml.Unmarshal(yamlSpec, &yamlMap) + if err != nil { + log.Fatal("Could not unmarshal yaml namespaced spec") + } + kind, ok := yamlMap["kind"].(string) + if !ok { + log.Fatal("Yaml manifest file contains a 'kind' field that is not a string") + } + if kind == "Deployment" { + // this is ugly and hacky; we should probably make this cleaner + nestedMap, ok := yamlMap["spec"].(map[string]interface{}) + if !ok { + continue + } + nestedMap, ok = nestedMap["template"].(map[string]interface{}) + if !ok { + continue + } + nestedMap, ok = nestedMap["spec"].(map[string]interface{}) + if !ok { + continue + } + containersArray, ok := nestedMap["containers"].([]interface{}) + if !ok { + continue + } + for _, item := range containersArray { + image, ok := item.(map[string]interface{})["image"].(string) + if !ok { + continue + } + if image != imageName { + warningMessages = fmt.Sprintf("%s\nWARNING: Namespace manifest contains a deployment with image %v, which does not match the name of the image being built: %v", warningMessages, image, imageName) + } + } + } + } + if warningMessages == "" { + return nil + } + return errors.New(warningMessages) +} + +func renderTestManifest(image string) { + namespacedBytes, err := ioutil.ReadFile(namespacedManBuild) + if err != nil { + log.Fatalf("could not read namespaced manifest: %v", err) + } + if err = generator.RenderTestYaml(cmdutil.GetConfig(), image); err != nil { + log.Fatalf("failed to generate deploy/test-pod.yaml: (%v)", err) + } + err = verifyDeploymentImage(namespacedBytes, image) + // the error from verifyDeploymentImage is just a warning, not fatal error + if err != nil { + fmt.Printf("%v\n", err) + } } const ( - build = "./tmp/build/build.sh" - dockerBuild = "./tmp/build/docker_build.sh" - configYaml = "./config/config.yaml" + build = "./tmp/build/build.sh" + configYaml = "./config/config.yaml" ) func buildFunc(cmd *cobra.Command, args []string) { @@ -56,6 +143,8 @@ func buildFunc(cmd *cobra.Command, args []string) { } bcmd := exec.Command(build) + bcmd.Env = append(os.Environ(), fmt.Sprintf("TEST_LOCATION=%v", testLocationBuild)) + bcmd.Env = append(bcmd.Env, fmt.Sprintf("ENABLE_TESTS=%v", enableTests)) o, err := bcmd.CombinedOutput() if err != nil { cmdError.ExitWithError(cmdError.ExitError, fmt.Errorf("failed to build: (%v)", string(o))) @@ -63,11 +152,29 @@ func buildFunc(cmd *cobra.Command, args []string) { fmt.Fprintln(os.Stdout, string(o)) image := args[0] - dbcmd := exec.Command(dockerBuild) - dbcmd.Env = append(os.Environ(), fmt.Sprintf("IMAGE=%v", image)) + baseImageName := image + if enableTests { + baseImageName += "-intermediate" + } + dbcmd := exec.Command("docker", "build", ".", "-f", "tmp/build/Dockerfile", "-t", baseImageName) o, err = dbcmd.CombinedOutput() if err != nil { - cmdError.ExitWithError(cmdError.ExitError, fmt.Errorf("failed to output build image %v: (%v)", image, string(o))) + if enableTests { + cmdError.ExitWithError(cmdError.ExitError, fmt.Errorf("failed to build intermediate image for %s image: (%s)", image, string(o))) + } else { + cmdError.ExitWithError(cmdError.ExitError, fmt.Errorf("failed to output build image %s: (%s)", image, string(o))) + } } fmt.Fprintln(os.Stdout, string(o)) + + if enableTests { + testDbcmd := exec.Command("docker", "build", ".", "-f", "tmp/build/test-framework/Dockerfile", "-t", image, "--build-arg", "NAMESPACEDMAN="+namespacedManBuild, "--build-arg", "BASEIMAGE="+baseImageName) + o, err = testDbcmd.CombinedOutput() + if err != nil { + cmdError.ExitWithError(cmdError.ExitError, fmt.Errorf("failed to output build image %v: (%v)", image, string(o))) + } + fmt.Fprintln(os.Stdout, string(o)) + // create test-pod.yaml as well as check image name of deployments in namespaced manifest + renderTestManifest(image) + } } diff --git a/commands/operator-sdk/cmd/build_test.go b/commands/operator-sdk/cmd/build_test.go new file mode 100644 index 0000000000..054336db87 --- /dev/null +++ b/commands/operator-sdk/cmd/build_test.go @@ -0,0 +1,116 @@ +// 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 cmd + +import "testing" + +var memcachedNamespaceManExample = `apiVersion: v1 +kind: ServiceAccount +metadata: + name: memcached-operator + +--- + +kind: Role +apiVersion: rbac.authorization.k8s.io/v1beta1 +metadata: + name: memcached-operator +rules: +- apiGroups: + - cache.example.com + resources: + - "*" + verbs: + - "*" +- apiGroups: + - "" + resources: + - pods + - services + - endpoints + - persistentvolumeclaims + - events + - configmaps + - secrets + verbs: + - "*" +- apiGroups: + - apps + resources: + - deployments + - daemonsets + - replicasets + - statefulsets + verbs: + - "*" + +--- + +kind: RoleBinding +apiVersion: rbac.authorization.k8s.io/v1beta1 +metadata: + name: memcached-operator +subjects: +- kind: ServiceAccount + name: memcached-operator +roleRef: + kind: Role + name: memcached-operator + apiGroup: rbac.authorization.k8s.io + +--- + +apiVersion: apps/v1 +kind: Deployment +metadata: + name: memcached-operator +spec: + replicas: 1 + selector: + matchLabels: + name: memcached-operator + template: + metadata: + labels: + name: memcached-operator + spec: + serviceAccountName: memcached-operator + containers: + - name: memcached-operator + image: quay.io/coreos/operator-sdk-dev:test-framework-operator + ports: + - containerPort: 60000 + name: metrics + command: + - memcached-operator + imagePullPolicy: Always + env: + - name: WATCH_NAMESPACE + valueFrom: + fieldRef: + fieldPath: metadata.namespace + - name: OPERATOR_NAME + value: "memcached-operator" + +` + +func TestVerifyDeploymentImage(t *testing.T) { + if err := verifyDeploymentImage([]byte(memcachedNamespaceManExample), "quay.io/coreos/operator-sdk-dev:test-framework-operator"); err != nil { + t.Fatalf("verifyDeploymentImage incorrectly reported an error: %v", err) + } + if err := verifyDeploymentImage([]byte(memcachedNamespaceManExample), "different-image-name"); err == nil { + t.Fatal("verifyDeploymentImage did not report an error on an incorrect manifest") + } +} diff --git a/commands/operator-sdk/cmd/test.go b/commands/operator-sdk/cmd/test.go index c3a9dd91b3..9f7cea7e80 100644 --- a/commands/operator-sdk/cmd/test.go +++ b/commands/operator-sdk/cmd/test.go @@ -15,82 +15,20 @@ package cmd import ( - "io/ioutil" - "log" - "os" - "strings" - - "github.com/operator-framework/operator-sdk/pkg/test" + "github.com/operator-framework/operator-sdk/commands/operator-sdk/cmd/test" "github.com/spf13/cobra" ) -var ( - testLocation string - kubeconfig string - globalManifestPath string - namespacedManifestPath string - goTestFlags string -) - func NewTestCmd() *cobra.Command { testCmd := &cobra.Command{ - Use: "test --test-location [flags]", - Short: "Run End-To-End tests", - Run: testFunc, - } - defaultKubeConfig := "" - homedir, ok := os.LookupEnv("HOME") - if ok { - defaultKubeConfig = homedir + "/.kube/config" + Use: "test", + Short: "Tests the operator", + Long: `The test command has subcommands that can test the operator locally or from within a cluster. +`, } - testCmd.Flags().StringVarP(&testLocation, "test-location", "t", "", "Location of test files (e.g. ./test/e2e/)") - testCmd.MarkFlagRequired("test-location") - testCmd.Flags().StringVarP(&kubeconfig, "kubeconfig", "k", defaultKubeConfig, "Kubeconfig path") - testCmd.Flags().StringVarP(&globalManifestPath, "global-init", "g", "deploy/crd.yaml", "Path to manifest for Global resources (e.g. CRD manifest)") - testCmd.Flags().StringVarP(&namespacedManifestPath, "namespaced-init", "n", "", "Path to manifest for per-test, namespaced resources (e.g. RBAC and Operator manifest)") - testCmd.Flags().StringVarP(&goTestFlags, "go-test-flags", "f", "", "Additional flags to pass to go test") + testCmd.AddCommand(cmdtest.NewTestLocalCmd()) + testCmd.AddCommand(cmdtest.NewTestClusterCmd()) return testCmd } - -func testFunc(cmd *cobra.Command, args []string) { - // if no namespaced manifest path is given, combine deploy/sa.yaml, deploy/rbac.yaml and deploy/operator.yaml - if namespacedManifestPath == "" { - os.Mkdir("deploy/test", os.FileMode(int(0775))) - namespacedManifestPath = "deploy/test/namespace-manifests.yaml" - sa, err := ioutil.ReadFile("deploy/sa.yaml") - if err != nil { - log.Fatalf("could not find sa manifest: %v", err) - } - rbac, err := ioutil.ReadFile("deploy/rbac.yaml") - if err != nil { - log.Fatalf("could not find rbac manifest: %v", err) - } - operator, err := ioutil.ReadFile("deploy/operator.yaml") - if err != nil { - log.Fatalf("could not find operator manifest: %v", err) - } - combined := append(sa, []byte("\n---\n")...) - combined = append(combined, rbac...) - combined = append(combined, []byte("\n---\n")...) - combined = append(combined, operator...) - err = ioutil.WriteFile(namespacedManifestPath, combined, os.FileMode(int(0664))) - if err != nil { - log.Fatalf("could not create temporary namespaced manifest file: %v", err) - } - defer func() { - err := os.Remove(namespacedManifestPath) - if err != nil { - log.Fatalf("could not delete temporary namespace manifest file") - } - }() - } - testArgs := []string{"test", testLocation + "/..."} - testArgs = append(testArgs, "-"+test.KubeConfigFlag, kubeconfig) - testArgs = append(testArgs, "-"+test.NamespacedManPathFlag, namespacedManifestPath) - testArgs = append(testArgs, "-"+test.GlobalManPathFlag, globalManifestPath) - testArgs = append(testArgs, "-"+test.ProjRootFlag, mustGetwd()) - testArgs = append(testArgs, strings.Split(goTestFlags, " ")...) - execCmd(os.Stdout, "go", testArgs...) -} diff --git a/commands/operator-sdk/cmd/test/cluster.go b/commands/operator-sdk/cmd/test/cluster.go new file mode 100644 index 0000000000..7064bbc5c9 --- /dev/null +++ b/commands/operator-sdk/cmd/test/cluster.go @@ -0,0 +1,159 @@ +// 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 cmdtest + +import ( + "bytes" + "fmt" + "os" + "strings" + "time" + + "github.com/operator-framework/operator-sdk/pkg/test" + + "github.com/spf13/cobra" + v1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/wait" + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/tools/clientcmd" +) + +type testClusterConfig struct { + namespace string + kubeconfig string + imagePullPolicy string + serviceAccount string + pendingTimeout int +} + +var tcConfig testClusterConfig + +func NewTestClusterCmd() *cobra.Command { + testCmd := &cobra.Command{ + Use: "cluster [flags]", + Short: "Run End-To-End tests using image with embedded test binary", + RunE: testClusterFunc, + } + defaultKubeConfig := "" + homedir, ok := os.LookupEnv("HOME") + if ok { + defaultKubeConfig = homedir + "/.kube/config" + } + testCmd.Flags().StringVarP(&tcConfig.namespace, "namespace", "n", "default", "Namespace to run tests in") + testCmd.Flags().StringVarP(&tcConfig.kubeconfig, "kubeconfig", "k", defaultKubeConfig, "Kubeconfig path") + testCmd.Flags().StringVarP(&tcConfig.imagePullPolicy, "imagePullPolicy", "i", "Always", "Set test pod image pull policy. Allowed values: Always, Never") + testCmd.Flags().StringVarP(&tcConfig.serviceAccount, "serviceAccount", "s", "default", "Service account to run tests on") + testCmd.Flags().IntVarP(&tcConfig.pendingTimeout, "pendingTimeout", "p", 60, "Timeout for testing pod in pending state") + + return testCmd +} + +func testClusterFunc(cmd *cobra.Command, args []string) error { + // in main.go, we catch and print errors, so we don't want cobra to print the error itself + cmd.SilenceErrors = true + if len(args) != 1 { + return fmt.Errorf("operator-sdk test cluster requires exactly 1 argument") + } + var pullPolicy v1.PullPolicy + if strings.ToLower(tcConfig.imagePullPolicy) == "always" { + pullPolicy = v1.PullAlways + } else if strings.ToLower(tcConfig.imagePullPolicy) == "never" { + pullPolicy = v1.PullNever + } else { + return fmt.Errorf("invalid imagePullPolicy '%v'", tcConfig.imagePullPolicy) + } + // cobra prints its help message on error; we silence that here because any errors below + // are due to the test failing, not incorrect user input + cmd.SilenceUsage = true + testPod := &v1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "operator-test", + }, + Spec: v1.PodSpec{ + ServiceAccountName: tcConfig.serviceAccount, + RestartPolicy: v1.RestartPolicyNever, + Containers: []v1.Container{{ + Name: "operator-test", + Image: args[0], + ImagePullPolicy: pullPolicy, + Command: []string{"/go-test.sh"}, + Env: []v1.EnvVar{{ + Name: test.TestNamespaceEnv, + ValueFrom: &v1.EnvVarSource{FieldRef: &v1.ObjectFieldSelector{FieldPath: "metadata.namespace"}}, + }}, + }}, + }, + } + kubeconfig, err := clientcmd.BuildConfigFromFlags("", tcConfig.kubeconfig) + if err != nil { + return fmt.Errorf("failed to get kubeconfig: %v", err) + } + kubeclient, err := kubernetes.NewForConfig(kubeconfig) + if err != nil { + return fmt.Errorf("failed to create kubeclient: %v", err) + } + testPod, err = kubeclient.CoreV1().Pods(tcConfig.namespace).Create(testPod) + if err != nil { + return fmt.Errorf("failed to create test pod: %v", err) + } + defer func() { + err = kubeclient.CoreV1().Pods(tcConfig.namespace).Delete(testPod.Name, &metav1.DeleteOptions{}) + if err != nil { + fmt.Printf("Warning: failed to delete test pod") + } + }() + err = wait.Poll(time.Second*5, time.Second*time.Duration(tcConfig.pendingTimeout), func() (bool, error) { + testPod, err = kubeclient.CoreV1().Pods(tcConfig.namespace).Get(testPod.Name, metav1.GetOptions{}) + if err != nil { + return false, fmt.Errorf("failed to get test pod: %v", err) + } + if testPod.Status.Phase == v1.PodPending { + return false, nil + } + return true, nil + }) + if err != nil { + testPod, err = kubeclient.CoreV1().Pods(tcConfig.namespace).Get(testPod.Name, metav1.GetOptions{}) + if err != nil { + return fmt.Errorf("failed to get test pod: %v", err) + } + waitingState := testPod.Status.ContainerStatuses[0].State.Waiting + return fmt.Errorf("test pod stuck in 'Pending' phase for longer than %d seconds.\nMessage: %s\nReason: %s", tcConfig.pendingTimeout, waitingState.Message, waitingState.Reason) + } + for { + testPod, err = kubeclient.CoreV1().Pods(tcConfig.namespace).Get(testPod.Name, metav1.GetOptions{}) + if err != nil { + return fmt.Errorf("failed to get test pod: %v", err) + } + if testPod.Status.Phase != v1.PodSucceeded && testPod.Status.Phase != v1.PodFailed { + time.Sleep(time.Second * 5) + continue + } else if testPod.Status.Phase == v1.PodSucceeded { + fmt.Printf("Test Successfully Completed\n") + return nil + } else if testPod.Status.Phase == v1.PodFailed { + req := kubeclient.CoreV1().Pods(tcConfig.namespace).GetLogs(testPod.Name, &v1.PodLogOptions{}) + readCloser, err := req.Stream() + if err != nil { + return fmt.Errorf("test failed and failed to get error logs") + } + defer readCloser.Close() + buf := new(bytes.Buffer) + buf.ReadFrom(readCloser) + return fmt.Errorf("test failed:\n%s", buf.String()) + } + } +} diff --git a/commands/operator-sdk/cmd/test/local.go b/commands/operator-sdk/cmd/test/local.go new file mode 100644 index 0000000000..0e203487d8 --- /dev/null +++ b/commands/operator-sdk/cmd/test/local.go @@ -0,0 +1,116 @@ +// 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 cmdtest + +import ( + "fmt" + "io/ioutil" + "log" + "os" + "os/exec" + "strings" + + cmdError "github.com/operator-framework/operator-sdk/commands/operator-sdk/error" + "github.com/operator-framework/operator-sdk/pkg/test" + + "github.com/spf13/cobra" +) + +type testLocalConfig struct { + kubeconfig string + globalManPath string + namespacedManPath string + goTestFlags string +} + +var tlConfig testLocalConfig + +func NewTestLocalCmd() *cobra.Command { + testCmd := &cobra.Command{ + Use: "local [flags]", + Short: "Run End-To-End tests locally", + Run: testLocalFunc, + } + defaultKubeConfig := "" + homedir, ok := os.LookupEnv("HOME") + if ok { + defaultKubeConfig = homedir + "/.kube/config" + } + testCmd.Flags().StringVarP(&tlConfig.kubeconfig, "kubeconfig", "k", defaultKubeConfig, "Kubeconfig path") + testCmd.Flags().StringVarP(&tlConfig.globalManPath, "global-init", "g", "deploy/crd.yaml", "Path to manifest for Global resources (e.g. CRD manifest)") + testCmd.Flags().StringVarP(&tlConfig.namespacedManPath, "namespaced-init", "n", "", "Path to manifest for per-test, namespaced resources (e.g. RBAC and Operator manifest)") + testCmd.Flags().StringVarP(&tlConfig.goTestFlags, "go-test-flags", "f", "", "Additional flags to pass to go test") + + return testCmd +} + +func testLocalFunc(cmd *cobra.Command, args []string) { + if len(args) != 1 { + cmdError.ExitWithError(cmdError.ExitBadArgs, fmt.Errorf("operator-sdk test local requires exactly 1 argument")) + } + // if no namespaced manifest path is given, combine deploy/sa.yaml, deploy/rbac.yaml and deploy/operator.yaml + if tlConfig.namespacedManPath == "" { + os.Mkdir("deploy/test", os.FileMode(int(0775))) + tlConfig.namespacedManPath = "deploy/test/namespace-manifests.yaml" + sa, err := ioutil.ReadFile("deploy/sa.yaml") + if err != nil { + log.Fatalf("could not find sa manifest: %v", err) + } + rbac, err := ioutil.ReadFile("deploy/rbac.yaml") + if err != nil { + log.Fatalf("could not find rbac manifest: %v", err) + } + operator, err := ioutil.ReadFile("deploy/operator.yaml") + if err != nil { + log.Fatalf("could not find operator manifest: %v", err) + } + combined := append(sa, []byte("\n---\n")...) + combined = append(combined, rbac...) + combined = append(combined, []byte("\n---\n")...) + combined = append(combined, operator...) + err = ioutil.WriteFile(tlConfig.namespacedManPath, combined, os.FileMode(int(0664))) + if err != nil { + log.Fatalf("could not create temporary namespaced manifest file: %v", err) + } + defer func() { + err := os.Remove(tlConfig.namespacedManPath) + if err != nil { + log.Fatalf("could not delete temporary namespace manifest file") + } + }() + } + testArgs := []string{"test", args[0] + "/..."} + testArgs = append(testArgs, "-"+test.KubeConfigFlag, tlConfig.kubeconfig) + testArgs = append(testArgs, "-"+test.NamespacedManPathFlag, tlConfig.namespacedManPath) + testArgs = append(testArgs, "-"+test.GlobalManPathFlag, tlConfig.globalManPath) + testArgs = append(testArgs, "-"+test.ProjRootFlag, mustGetwd()) + testArgs = append(testArgs, strings.Split(tlConfig.goTestFlags, " ")...) + dc := exec.Command("go", testArgs...) + dc.Dir = mustGetwd() + dc.Stdout = os.Stdout + dc.Stderr = os.Stderr + err := dc.Run() + if err != nil { + cmdError.ExitWithError(cmdError.ExitError, fmt.Errorf("failed to exec `go %s`: %v", strings.Join(testArgs, " "), err)) + } +} + +func mustGetwd() string { + wd, err := os.Getwd() + if err != nil { + cmdError.ExitWithError(cmdError.ExitError, fmt.Errorf("failed to determine the full path of the current directory: %v", err)) + } + return wd +} diff --git a/pkg/generator/generator.go b/pkg/generator/generator.go index 3970466333..e8893cebd2 100644 --- a/pkg/generator/generator.go +++ b/pkg/generator/generator.go @@ -24,13 +24,15 @@ import ( "strings" "text/template" + "github.com/operator-framework/operator-sdk/pkg/test" + k8sutil "github.com/operator-framework/operator-sdk/pkg/util/k8sutil" ) const ( defaultDirFileMode = 0750 defaultFileMode = 0644 - defaultExecFileMode = 0744 + defaultExecFileMode = 0755 // dirs cmdDir = "cmd" deployDir = "deploy" @@ -39,6 +41,7 @@ const ( tmpDir = "tmp" buildDir = tmpDir + "/build" codegenDir = tmpDir + "/codegen" + dockerTestDir = buildDir + "/test-framework" pkgDir = "pkg" apisDir = pkgDir + "/apis" stubDir = pkgDir + "/stub" @@ -51,8 +54,9 @@ const ( register = "register.go" types = "types.go" build = "build.sh" - dockerBuild = "docker_build.sh" dockerfile = "Dockerfile" + testingDockerfile = "Dockerfile" + goTest = "go-test.sh" boilerplate = "boilerplate.go.txt" updateGenerated = "update-generated.sh" gopkgtoml = "Gopkg.toml" @@ -77,6 +81,7 @@ const ( operatorTmplName = "deploy/operator.yaml" rbacTmplName = "deploy/rbac.yaml" crTmplName = "deploy/cr.yaml" + testYamlName = "deploy/test-pod.yaml" saTmplName = "deploy/sa.yaml" pluralSuffix = "s" ) @@ -255,6 +260,15 @@ func renderDeployFiles(deployDir, projectName, apiVersion, kind string) error { return renderWriteFile(filepath.Join(deployDir, "operator.yaml"), operatorTmplName, operatorYamlTmpl, opTd) } +func RenderTestYaml(c *Config, image string) error { + opTd := tmplData{ + ProjectName: c.ProjectName, + Image: image, + TestNamespaceEnv: test.TestNamespaceEnv, + } + return renderWriteFile(filepath.Join(deployDir, "test-pod.yaml"), testYamlName, testYamlTmpl, opTd) +} + // RenderOlmCatalog generates catalog manifests "deploy/olm-catalog/*" // The current working directory must be the project repository root func RenderOlmCatalog(c *Config, image, version string) error { @@ -341,26 +355,23 @@ func renderBuildFiles(buildDir, repoPath, projectName string) error { return err } - buf = &bytes.Buffer{} - if err := renderDockerBuildFile(buf); err != nil { - return err + dTd := tmplData{ + ProjectName: projectName, } - if err := writeFileAndPrint(filepath.Join(buildDir, dockerBuild), buf.Bytes(), defaultExecFileMode); err != nil { + + if err := renderWriteFile(filepath.Join(buildDir, dockerfile), "tmp/build/Dockerfile", dockerFileTmpl, dTd); err != nil { return err } - dTd := tmplData{ - ProjectName: projectName, - } - if err := renderFile(buf, "tmp/build/Dockerfile", dockerFileTmpl, dTd); err != nil { + if err := renderWriteFile(filepath.Join(buildDir, "test-framework", testingDockerfile), "tmp/build/test-framework/Dockerfile", testingDockerFileTmpl, dTd); err != nil { return err } - return renderWriteFile(filepath.Join(buildDir, dockerfile), "tmp/build/Dockerfile", dockerFileTmpl, dTd) -} -func renderDockerBuildFile(w io.Writer) error { - _, err := w.Write([]byte(dockerBuildTmpl)) - return err + buf = &bytes.Buffer{} + if err := renderFile(buf, filepath.Join(buildDir, goTest), goTestScript, tmplData{}); err != nil { + return err + } + return writeFileAndPrint(filepath.Join(buildDir, goTest), buf.Bytes(), defaultExecFileMode) } func renderCodegenFiles(codegenDir, repoPath, apiDirName, version, projectName string) error { @@ -468,6 +479,9 @@ type tmplData struct { CRDVersion string CSVName string CatalogVersion string + + // for test framework + TestNamespaceEnv string } // Creates all the necesary directories for the generated files @@ -480,6 +494,7 @@ func (g *Generator) generateDirStructure() error { filepath.Join(g.projectName, olmCatalogDir), filepath.Join(g.projectName, buildDir), filepath.Join(g.projectName, codegenDir), + filepath.Join(g.projectName, dockerTestDir), filepath.Join(g.projectName, versionDir), filepath.Join(g.projectName, apisDir, apiDirName(g.apiVersion), version(g.apiVersion)), filepath.Join(g.projectName, stubDir), diff --git a/pkg/generator/generator_test.go b/pkg/generator/generator_test.go index 371821d3e6..6032ab2935 100644 --- a/pkg/generator/generator_test.go +++ b/pkg/generator/generator_test.go @@ -511,8 +511,13 @@ mkdir -p ${BIN_DIR} PROJECT_NAME="app-operator" REPO_PATH="github.com/example-inc/app-operator" BUILD_PATH="${REPO_PATH}/cmd/${PROJECT_NAME}" +TEST_PATH="${REPO_PATH}/${TEST_LOCATION}" echo "building "${PROJECT_NAME}"..." GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -o ${BIN_DIR}/${PROJECT_NAME} $BUILD_PATH +if $ENABLE_TESTS ; then + echo "building "${PROJECT_NAME}-test"..." + GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go test -c -o ${BIN_DIR}/${PROJECT_NAME}-test $TEST_PATH +fi ` const dockerFileExp = `FROM alpine:3.6 @@ -539,17 +544,6 @@ func TestGenBuild(t *testing.T) { t.Errorf("\nTest failed. Below is the diff of the expected vs actual results.\nRed text is missing and green text is extra.\n\n" + dmp.DiffPrettyText(diffs)) } - buf = &bytes.Buffer{} - if err := renderDockerBuildFile(buf); err != nil { - t.Error(err) - return - } - if dockerBuildTmpl != buf.String() { - dmp := diffmatchpatch.New() - diffs := dmp.DiffMain(dockerBuildTmpl, buf.String(), false) - t.Errorf("\nTest failed. Below is the diff of the expected vs actual results.\nRed text is missing and green text is extra.\n\n" + dmp.DiffPrettyText(diffs)) - } - buf = &bytes.Buffer{} dTd := tmplData{ ProjectName: appProjectName, diff --git a/pkg/generator/templates.go b/pkg/generator/templates.go index 9c377b8ba5..61d7b542eb 100644 --- a/pkg/generator/templates.go +++ b/pkg/generator/templates.go @@ -424,6 +424,24 @@ spec: version: {{.Version}} ` +const testYamlTmpl = `apiVersion: v1 +kind: Pod +metadata: + name: {{.ProjectName}}-test +spec: + restartPolicy: Never + containers: + - name: {{.ProjectName}}-test + image: {{.Image}} + imagePullPolicy: Always + command: ["/go-test.sh"] + env: + - name: {{.TestNamespaceEnv}} + valueFrom: + fieldRef: + fieldPath: metadata.namespace +` + const operatorYamlTmpl = `apiVersion: apps/v1 kind: Deployment metadata: @@ -548,21 +566,18 @@ mkdir -p ${BIN_DIR} PROJECT_NAME="{{.ProjectName}}" REPO_PATH="{{.RepoPath}}" BUILD_PATH="${REPO_PATH}/cmd/${PROJECT_NAME}" +TEST_PATH="${REPO_PATH}/${TEST_LOCATION}" echo "building "${PROJECT_NAME}"..." GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -o ${BIN_DIR}/${PROJECT_NAME} $BUILD_PATH -` - -const dockerBuildTmpl = `#!/usr/bin/env bash - -if ! which docker > /dev/null; then - echo "docker needs to be installed" - exit 1 +if $ENABLE_TESTS ; then + echo "building "${PROJECT_NAME}-test"..." + GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go test -c -o ${BIN_DIR}/${PROJECT_NAME}-test $TEST_PATH fi +` -: ${IMAGE:?"Need to set IMAGE, e.g. gcr.io//-operator"} +const goTestScript = `#!/bin/sh -echo "building container ${IMAGE}..." -docker build -t "${IMAGE}" -f tmp/build/Dockerfile . +memcached-operator-test -test.parallel=1 -test.failfast -root=/ -kubeconfig=incluster -namespacedMan=namespaced.yaml -test.v ` const dockerFileTmpl = `FROM alpine:3.6 @@ -573,6 +588,16 @@ USER {{.ProjectName}} ADD tmp/_output/bin/{{.ProjectName}} /usr/local/bin/{{.ProjectName}} ` +const testingDockerFileTmpl = `ARG BASEIMAGE + +FROM ${BASEIMAGE} + +ADD tmp/_output/bin/memcached-operator-test /usr/local/bin/memcached-operator-test +ARG NAMESPACEDMAN +ADD $NAMESPACEDMAN /namespaced.yaml +ADD tmp/build/go-test.sh /go-test.sh +` + // apiDocTmpl is the template for apis/../doc.go const apiDocTmpl = `// +k8s:deepcopy-gen=package // +groupName={{.GroupName}} diff --git a/pkg/test/framework.go b/pkg/test/framework.go index 2edb1b806f..97b1040951 100644 --- a/pkg/test/framework.go +++ b/pkg/test/framework.go @@ -17,6 +17,8 @@ package test import ( goctx "context" "fmt" + "net" + "os" "sync" "time" @@ -51,10 +53,30 @@ type Framework struct { DynamicClient dynclient.Client DynamicDecoder runtime.Decoder NamespacedManPath *string + InCluster bool } func setup(kubeconfigPath, namespacedManPath *string) error { - kubeconfig, err := clientcmd.BuildConfigFromFlags("", *kubeconfigPath) + var err error + var kubeconfig *rest.Config + inCluster := false + if *kubeconfigPath == "incluster" { + // Work around https://github.com/kubernetes/kubernetes/issues/40973 + if len(os.Getenv("KUBERNETES_SERVICE_HOST")) == 0 { + addrs, err := net.LookupHost("kubernetes.default.svc") + if err != nil { + return fmt.Errorf("failed to get service host: %v", err) + } + os.Setenv("KUBERNETES_SERVICE_HOST", addrs[0]) + } + if len(os.Getenv("KUBERNETES_SERVICE_PORT")) == 0 { + os.Setenv("KUBERNETES_SERVICE_PORT", "443") + } + kubeconfig, err = rest.InClusterConfig() + inCluster = true + } else { + kubeconfig, err = clientcmd.BuildConfigFromFlags("", *kubeconfigPath) + } if err != nil { return fmt.Errorf("failed to build the kubeconfig: %v", err) } @@ -82,6 +104,7 @@ func setup(kubeconfigPath, namespacedManPath *string) error { DynamicClient: dynClient, DynamicDecoder: dynDec, NamespacedManPath: namespacedManPath, + InCluster: inCluster, } return nil } @@ -114,7 +137,11 @@ func AddToFrameworkScheme(addToScheme addToSchemeFunc, obj runtime.Object) error Global.RestMapper.Reset() Global.DynamicClient, err = dynclient.New(Global.KubeConfig, dynclient.Options{Scheme: Global.Scheme, Mapper: Global.RestMapper}) err = wait.PollImmediate(time.Second, time.Second*10, func() (done bool, err error) { - err = Global.DynamicClient.List(goctx.TODO(), &dynclient.ListOptions{Namespace: "default"}, obj) + if Global.InCluster { + err = Global.DynamicClient.List(goctx.TODO(), &dynclient.ListOptions{Namespace: os.Getenv(TestNamespaceEnv)}, obj) + } else { + err = Global.DynamicClient.List(goctx.TODO(), &dynclient.ListOptions{Namespace: "default"}, obj) + } if err != nil { Global.RestMapper.Reset() return false, nil diff --git a/pkg/test/main_entry.go b/pkg/test/main_entry.go index 4c7a15fd40..608df58eb3 100644 --- a/pkg/test/main_entry.go +++ b/pkg/test/main_entry.go @@ -27,6 +27,7 @@ const ( KubeConfigFlag = "kubeconfig" NamespacedManPathFlag = "namespacedMan" GlobalManPathFlag = "globalMan" + TestNamespaceEnv = "TEST_NAMESPACE" ) func MainEntry(m *testing.M) { @@ -53,12 +54,14 @@ func MainEntry(m *testing.M) { os.Exit(exitCode) }() // create crd - globalYAML, err := ioutil.ReadFile(*globalManPath) - if err != nil { - log.Fatalf("failed to read global resource manifest: %v", err) - } - err = ctx.createFromYAML(globalYAML, true) - if err != nil { - log.Fatalf("failed to create resource(s) in global resource manifest: %v", err) + if !Global.InCluster { + globalYAML, err := ioutil.ReadFile(*globalManPath) + if err != nil { + log.Fatalf("failed to read global resource manifest: %v", err) + } + err = ctx.createFromYAML(globalYAML, true) + if err != nil { + log.Fatalf("failed to create resource(s) in global resource manifest: %v", err) + } } } diff --git a/pkg/test/resource_creator.go b/pkg/test/resource_creator.go index 5ed0cd24b9..940f603174 100644 --- a/pkg/test/resource_creator.go +++ b/pkg/test/resource_creator.go @@ -19,6 +19,7 @@ import ( goctx "context" "fmt" "io/ioutil" + "os" yaml "gopkg.in/yaml.v2" core "k8s.io/api/core/v1" @@ -30,6 +31,10 @@ func (ctx *TestCtx) GetNamespace() (string, error) { if ctx.Namespace != "" { return ctx.Namespace, nil } + if Global.InCluster { + ctx.Namespace = os.Getenv(TestNamespaceEnv) + return ctx.Namespace, nil + } // create namespace ctx.Namespace = ctx.GetID() namespaceObj := &core.Namespace{ObjectMeta: metav1.ObjectMeta{Name: ctx.Namespace}} diff --git a/test/e2e/incluster-test-code/main_test.go.tmpl b/test/e2e/incluster-test-code/main_test.go.tmpl new file mode 100644 index 0000000000..210d906294 --- /dev/null +++ b/test/e2e/incluster-test-code/main_test.go.tmpl @@ -0,0 +1,25 @@ +// 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/e2e/incluster-test-code/memcached_test.go.tmpl b/test/e2e/incluster-test-code/memcached_test.go.tmpl new file mode 100644 index 0000000000..294ff06bd2 --- /dev/null +++ b/test/e2e/incluster-test-code/memcached_test.go.tmpl @@ -0,0 +1,124 @@ +// 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" + + operator "github.com/example-inc/memcached-operator/pkg/apis/cache/v1alpha1" + framework "github.com/operator-framework/operator-sdk/pkg/test" + "github.com/operator-framework/operator-sdk/pkg/test/e2eutil" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" +) + +var ( + retryInterval = time.Second * 5 + timeout = time.Second * 30 +) + +func TestMemcached(t *testing.T) { + memcachedList := &operator.MemcachedList{ + TypeMeta: metav1.TypeMeta{ + Kind: "Memcached", + APIVersion: "cache.example.com/v1alpha1", + }, + } + err := framework.AddToFrameworkScheme(operator.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.TestCtx) error { + namespace, err := ctx.GetNamespace() + if err != nil { + return fmt.Errorf("could not get namespace: %v", err) + } + // create memcached custom resource + exampleMemcached := &operator.Memcached{ + TypeMeta: metav1.TypeMeta{ + Kind: "Memcached", + APIVersion: "cache.example.com/v1alpha1", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "example-memcached", + Namespace: namespace, + }, + Spec: operator.MemcachedSpec{ + Size: 3, + }, + } + err = f.DynamicClient.Create(goctx.TODO(), exampleMemcached) + if err != nil { + return err + } + ctx.AddFinalizerFn(func() error { + return f.DynamicClient.Delete(goctx.TODO(), exampleMemcached) + }) + // wait for example-memcached to reach 3 replicas + err = e2eutil.WaitForDeployment(t, f.KubeClient, namespace, "example-memcached", 3, retryInterval, timeout) + if err != nil { + return err + } + + err = f.DynamicClient.Get(goctx.TODO(), types.NamespacedName{Name: "example-memcached", Namespace: namespace}, exampleMemcached) + if err != nil { + return err + } + exampleMemcached.Spec.Size = 4 + err = f.DynamicClient.Update(goctx.TODO(), exampleMemcached) + if err != nil { + return err + } + + // wait for example-memcached to reach 4 replicas + return e2eutil.WaitForDeployment(t, f.KubeClient, namespace, "example-memcached", 4, retryInterval, timeout) +} + +func MemcachedCluster(t *testing.T) { + t.Parallel() + ctx := framework.NewTestCtx(t) + defer ctx.Cleanup(t) + err := ctx.InitializeClusterResources() + if err != nil { + t.Fatalf("failed to initialize cluster resources: %v", err) + } + t.Log("Initialized cluster resources") + namespace, err := ctx.GetNamespace() + if err != nil { + t.Fatal(err) + } + // get global framework variables + f := framework.Global + // wait for memcached-operator to be ready + err = e2eutil.WaitForDeployment(t, f.KubeClient, namespace, "memcached-operator", 1, retryInterval, timeout) + if err != nil { + t.Fatal(err) + } + + if err = memcachedScaleTest(t, f, ctx); err != nil { + t.Fatal(err) + } +} diff --git a/test/e2e/memcached_test.go b/test/e2e/memcached_test.go index cbcbd2467a..beaa95db3d 100644 --- a/test/e2e/memcached_test.go +++ b/test/e2e/memcached_test.go @@ -127,6 +127,33 @@ func TestMemcached(t *testing.T) { t.Fatalf("error: %v\nCommand Output: %s\n", err, string(cmdOut)) } + t.Log("Copying test files to ./test") + if err = os.MkdirAll("./test", os.FileMode(int(0755))); err != nil { + t.Fatalf("could not create test/e2e dir: %v", err) + } + cmdOut, err = exec.Command("cp", "-a", path.Join(gopath, "/src/github.com/operator-framework/operator-sdk/test/e2e/incluster-test-code"), "./test/e2e").CombinedOutput() + if err != nil { + t.Fatalf("could not copy tests to test/e2e: %v\nCommand Output:\n%v", err, string(cmdOut)) + } + // fix naming of files + cmdOut, err = exec.Command("mv", "test/e2e/main_test.go.tmpl", "test/e2e/main_test.go").CombinedOutput() + if err != nil { + t.Fatalf("could not rename test/e2e/main_test.go.tmpl: %v\nCommand Output:\n%v", err, string(cmdOut)) + } + cmdOut, err = exec.Command("mv", "test/e2e/memcached_test.go.tmpl", "test/e2e/memcached_test.go").CombinedOutput() + if err != nil { + t.Fatalf("could not rename test/e2e/memcached_test.go.tmpl: %v\nCommand Output:\n%v", err, string(cmdOut)) + } + t.Log("Pulling new dependencies with dep ensure") + cmdOut, err = exec.Command("dep", "ensure").CombinedOutput() + if err != nil { + t.Fatalf("dep ensure failed: %v\nCommand Output:\n%v", err, string(cmdOut)) + } + // use current operator-sdk code + os.RemoveAll("vendor/github.com/operator-framework/operator-sdk/pkg") + os.Symlink(path.Join(gopath, "/src/github.com/operator-framework/operator-sdk/pkg"), + "vendor/github.com/operator-framework/operator-sdk/pkg") + // create crd crdYAML, err := ioutil.ReadFile("deploy/crd.yaml") err = ctx.CreateFromYAML(crdYAML) @@ -135,9 +162,10 @@ func TestMemcached(t *testing.T) { } t.Log("Created crd") - // run both subtests + // run subtests t.Run("memcached-group", func(t *testing.T) { t.Run("Cluster", MemcachedCluster) + t.Run("ClusterTest", MemcachedClusterTest) t.Run("Local", MemcachedLocal) }) } @@ -238,20 +266,13 @@ func MemcachedCluster(t *testing.T) { f := framework.Global ctx := f.NewTestCtx(t) defer ctx.Cleanup(t) - local := *f.ImageName == "" - if local { - *f.ImageName = "quay.io/example/memcached-operator:v0.0.1" - } - t.Log("Building operator docker image") - cmdOut, err := exec.Command("operator-sdk", "build", *f.ImageName).CombinedOutput() + operatorYAML, err := ioutil.ReadFile("deploy/operator.yaml") if err != nil { - t.Fatalf("error: %v\nCommand Output: %s\n", err, string(cmdOut)) + t.Fatalf("could not read deploy/operator.yaml: %v", err) } - - operatorYAML, err := ioutil.ReadFile("deploy/operator.yaml") - operatorYAML = bytes.Replace(operatorYAML, []byte("REPLACE_IMAGE"), []byte(*f.ImageName), 1) - + local := *f.ImageName == "" if local { + *f.ImageName = "quay.io/example/memcached-operator:v0.0.1" if err != nil { t.Fatal(err) } @@ -260,7 +281,19 @@ func MemcachedCluster(t *testing.T) { if err != nil { t.Fatal(err) } - } else { + } + operatorYAML = bytes.Replace(operatorYAML, []byte("REPLACE_IMAGE"), []byte(*f.ImageName), 1) + err = ioutil.WriteFile("deploy/operator.yaml", operatorYAML, os.FileMode(0644)) + if err != nil { + t.Fatalf("failed to write deploy/operator.yaml: %v", err) + } + t.Log("Building operator docker image") + cmdOut, err := exec.Command("operator-sdk", "build", *f.ImageName, "-e", "-t", "./test/e2e", "-n", "deploy/operator.yaml").CombinedOutput() + if err != nil { + t.Fatalf("error: %v\nCommand Output: %s\n", err, string(cmdOut)) + } + + if !local { t.Log("Pushing docker image to repo") cmdOut, err = exec.Command("docker", "push", *f.ImageName).CombinedOutput() if err != nil { @@ -315,3 +348,39 @@ func MemcachedCluster(t *testing.T) { t.Fatal(err) } } + +func MemcachedClusterTest(t *testing.T) { + // get global framework variables + f := framework.Global + ctx := f.NewTestCtx(t) + defer ctx.Cleanup(t) + + // create sa + saYAML, err := ioutil.ReadFile("deploy/sa.yaml") + if err != nil { + t.Fatal(err) + } + err = ctx.CreateFromYAML(saYAML) + if err != nil { + t.Fatal(err) + } + t.Log("Created sa") + + // create rbac + rbacYAML, err := ioutil.ReadFile("deploy/rbac.yaml") + err = ctx.CreateFromYAML(rbacYAML) + if err != nil { + t.Fatalf("failed to create rbac: %v", err) + } + t.Log("Created rbac") + + namespace, err := ctx.GetNamespace() + if err != nil { + t.Fatalf("could not get namespace: %v", err) + } + cmdOut, err := exec.Command("operator-sdk", "test", "cluster", *f.ImageName, "-n", namespace, "-i", "Never", "-s", "memcached-operator").CombinedOutput() + if err != nil { + t.Fatalf("in-cluster test failed: %v\nCommand Output:\n%s", err, string(cmdOut)) + } + +} diff --git a/test/test-framework/memcached_test.go b/test/test-framework/memcached_test.go index 131b301a2d..c11370594d 100644 --- a/test/test-framework/memcached_test.go +++ b/test/test-framework/memcached_test.go @@ -74,6 +74,9 @@ func memcachedScaleTest(t *testing.T, f *framework.Framework, ctx *framework.Tes if err != nil { return err } + ctx.AddFinalizerFn(func() error { + return f.DynamicClient.Delete(goctx.TODO(), exampleMemcached) + }) // wait for example-memcached to reach 3 replicas err = e2eutil.WaitForDeployment(t, f.KubeClient, namespace, "example-memcached", 3, retryInterval, timeout) if err != nil {