Skip to content

Commit fe6d2a3

Browse files
authored
pkg/test: add in-cluster/in-image testing support (#469)
* pkg/test: add in-cluster/in-image testing support Issue #435 * *: change the way the dockerfiles are used * commands/.../test: create new test subcommands * .travis.yml: update travis.yml to match test subcommands * commands/.../test/cluster.go: return errors instead of cmderror The cluster test has defers that need to run on error, which would not run if cmdError is used due to its use of os.Exit(). Instead, we can just return an error, which allows the defer functions to run. * *: add tests for incluster test image and fix bugs This adds tests for the new incluster image testing mode as well as fixes some bugs that this test exposed * commands/.../test/cluster.go: handle pending phase Test could hang forever if the pod remaining in the 'Pending' phase, which would occur if it cannot pull the image. This sets a configurable pending timeout with a default of 60 seconds * .travis.yml: enable go testing of ./commands/... * pkg/generator: remove docker_build.sh generation * pkg/generator/generator.go: change defaultExecFileMode to 0755 * commands/.../test: use structs for test flags/vars
1 parent 46a1c52 commit fe6d2a3

File tree

16 files changed

+863
-136
lines changed

16 files changed

+863
-136
lines changed

.travis.yml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,13 +25,14 @@ install:
2525

2626
script:
2727
- make install
28+
- go test ./commands/...
2829
- go test ./pkg/...
2930
- go test ./test/e2e/...
3031
- cd test/test-framework
3132
# test framework with defaults
32-
- operator-sdk test -t .
33+
- operator-sdk test local .
3334
# test operator-sdk test flags
34-
- operator-sdk test -t . -g deploy/crd.yaml -n deploy/namespace-init.yaml -f "-parallel 1" -k $HOME/.kube/config
35+
- operator-sdk test local . -g deploy/crd.yaml -n deploy/namespace-init.yaml -f "-parallel 1" -k $HOME/.kube/config
3536
# go back to project root
3637
- cd ../..
3738
- go vet ./...

commands/operator-sdk/cmd/build.go

Lines changed: 114 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,17 +15,30 @@
1515
package cmd
1616

1717
import (
18+
"bytes"
19+
"errors"
1820
"fmt"
21+
"io/ioutil"
22+
"log"
1923
"os"
2024
"os/exec"
2125

26+
"github.com/operator-framework/operator-sdk/commands/operator-sdk/cmd/cmdutil"
2227
cmdError "github.com/operator-framework/operator-sdk/commands/operator-sdk/error"
28+
"github.com/operator-framework/operator-sdk/pkg/generator"
2329

30+
"github.com/ghodss/yaml"
2431
"github.com/spf13/cobra"
2532
)
2633

34+
var (
35+
namespacedManBuild string
36+
testLocationBuild string
37+
enableTests bool
38+
)
39+
2740
func NewBuildCmd() *cobra.Command {
28-
return &cobra.Command{
41+
buildCmd := &cobra.Command{
2942
Use: "build <image>",
3043
Short: "Compiles code and builds artifacts",
3144
Long: `The operator-sdk build command compiles the code, builds the executables,
@@ -42,12 +55,86 @@ For example:
4255
`,
4356
Run: buildFunc,
4457
}
58+
buildCmd.Flags().BoolVarP(&enableTests, "enable-tests", "e", false, "Enable in-cluster testing by adding test binary to the image")
59+
buildCmd.Flags().StringVarP(&testLocationBuild, "test-location", "t", "./test/e2e", "Location of tests")
60+
buildCmd.Flags().StringVarP(&namespacedManBuild, "namespaced", "n", "deploy/operator.yaml", "Path of namespaced resources for tests")
61+
return buildCmd
62+
}
63+
64+
/*
65+
* verifyDeploymentImages checks image names of pod 0 in deployments found in the provided yaml file.
66+
* This is done because e2e tests require a namespaced manifest file to configure a namespace with
67+
* required resources. This function is intended to identify if a user used a different image name
68+
* for their operator in the provided yaml, which would result in the testing of the wrong operator
69+
* image. As it is possible for a namespaced yaml to have multiple deployments (such as the vault
70+
* operator, which depends on the etcd-operator), this is just a warning, not a fatal error.
71+
*/
72+
func verifyDeploymentImage(yamlFile []byte, imageName string) error {
73+
warningMessages := ""
74+
yamlSplit := bytes.Split(yamlFile, []byte("\n---\n"))
75+
for _, yamlSpec := range yamlSplit {
76+
yamlMap := make(map[string]interface{})
77+
err := yaml.Unmarshal(yamlSpec, &yamlMap)
78+
if err != nil {
79+
log.Fatal("Could not unmarshal yaml namespaced spec")
80+
}
81+
kind, ok := yamlMap["kind"].(string)
82+
if !ok {
83+
log.Fatal("Yaml manifest file contains a 'kind' field that is not a string")
84+
}
85+
if kind == "Deployment" {
86+
// this is ugly and hacky; we should probably make this cleaner
87+
nestedMap, ok := yamlMap["spec"].(map[string]interface{})
88+
if !ok {
89+
continue
90+
}
91+
nestedMap, ok = nestedMap["template"].(map[string]interface{})
92+
if !ok {
93+
continue
94+
}
95+
nestedMap, ok = nestedMap["spec"].(map[string]interface{})
96+
if !ok {
97+
continue
98+
}
99+
containersArray, ok := nestedMap["containers"].([]interface{})
100+
if !ok {
101+
continue
102+
}
103+
for _, item := range containersArray {
104+
image, ok := item.(map[string]interface{})["image"].(string)
105+
if !ok {
106+
continue
107+
}
108+
if image != imageName {
109+
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)
110+
}
111+
}
112+
}
113+
}
114+
if warningMessages == "" {
115+
return nil
116+
}
117+
return errors.New(warningMessages)
118+
}
119+
120+
func renderTestManifest(image string) {
121+
namespacedBytes, err := ioutil.ReadFile(namespacedManBuild)
122+
if err != nil {
123+
log.Fatalf("could not read namespaced manifest: %v", err)
124+
}
125+
if err = generator.RenderTestYaml(cmdutil.GetConfig(), image); err != nil {
126+
log.Fatalf("failed to generate deploy/test-pod.yaml: (%v)", err)
127+
}
128+
err = verifyDeploymentImage(namespacedBytes, image)
129+
// the error from verifyDeploymentImage is just a warning, not fatal error
130+
if err != nil {
131+
fmt.Printf("%v\n", err)
132+
}
45133
}
46134

47135
const (
48-
build = "./tmp/build/build.sh"
49-
dockerBuild = "./tmp/build/docker_build.sh"
50-
configYaml = "./config/config.yaml"
136+
build = "./tmp/build/build.sh"
137+
configYaml = "./config/config.yaml"
51138
)
52139

53140
func buildFunc(cmd *cobra.Command, args []string) {
@@ -56,18 +143,38 @@ func buildFunc(cmd *cobra.Command, args []string) {
56143
}
57144

58145
bcmd := exec.Command(build)
146+
bcmd.Env = append(os.Environ(), fmt.Sprintf("TEST_LOCATION=%v", testLocationBuild))
147+
bcmd.Env = append(bcmd.Env, fmt.Sprintf("ENABLE_TESTS=%v", enableTests))
59148
o, err := bcmd.CombinedOutput()
60149
if err != nil {
61150
cmdError.ExitWithError(cmdError.ExitError, fmt.Errorf("failed to build: (%v)", string(o)))
62151
}
63152
fmt.Fprintln(os.Stdout, string(o))
64153

65154
image := args[0]
66-
dbcmd := exec.Command(dockerBuild)
67-
dbcmd.Env = append(os.Environ(), fmt.Sprintf("IMAGE=%v", image))
155+
baseImageName := image
156+
if enableTests {
157+
baseImageName += "-intermediate"
158+
}
159+
dbcmd := exec.Command("docker", "build", ".", "-f", "tmp/build/Dockerfile", "-t", baseImageName)
68160
o, err = dbcmd.CombinedOutput()
69161
if err != nil {
70-
cmdError.ExitWithError(cmdError.ExitError, fmt.Errorf("failed to output build image %v: (%v)", image, string(o)))
162+
if enableTests {
163+
cmdError.ExitWithError(cmdError.ExitError, fmt.Errorf("failed to build intermediate image for %s image: (%s)", image, string(o)))
164+
} else {
165+
cmdError.ExitWithError(cmdError.ExitError, fmt.Errorf("failed to output build image %s: (%s)", image, string(o)))
166+
}
71167
}
72168
fmt.Fprintln(os.Stdout, string(o))
169+
170+
if enableTests {
171+
testDbcmd := exec.Command("docker", "build", ".", "-f", "tmp/build/test-framework/Dockerfile", "-t", image, "--build-arg", "NAMESPACEDMAN="+namespacedManBuild, "--build-arg", "BASEIMAGE="+baseImageName)
172+
o, err = testDbcmd.CombinedOutput()
173+
if err != nil {
174+
cmdError.ExitWithError(cmdError.ExitError, fmt.Errorf("failed to output build image %v: (%v)", image, string(o)))
175+
}
176+
fmt.Fprintln(os.Stdout, string(o))
177+
// create test-pod.yaml as well as check image name of deployments in namespaced manifest
178+
renderTestManifest(image)
179+
}
73180
}
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
// Copyright 2018 The Operator-SDK Authors
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package cmd
16+
17+
import "testing"
18+
19+
var memcachedNamespaceManExample = `apiVersion: v1
20+
kind: ServiceAccount
21+
metadata:
22+
name: memcached-operator
23+
24+
---
25+
26+
kind: Role
27+
apiVersion: rbac.authorization.k8s.io/v1beta1
28+
metadata:
29+
name: memcached-operator
30+
rules:
31+
- apiGroups:
32+
- cache.example.com
33+
resources:
34+
- "*"
35+
verbs:
36+
- "*"
37+
- apiGroups:
38+
- ""
39+
resources:
40+
- pods
41+
- services
42+
- endpoints
43+
- persistentvolumeclaims
44+
- events
45+
- configmaps
46+
- secrets
47+
verbs:
48+
- "*"
49+
- apiGroups:
50+
- apps
51+
resources:
52+
- deployments
53+
- daemonsets
54+
- replicasets
55+
- statefulsets
56+
verbs:
57+
- "*"
58+
59+
---
60+
61+
kind: RoleBinding
62+
apiVersion: rbac.authorization.k8s.io/v1beta1
63+
metadata:
64+
name: memcached-operator
65+
subjects:
66+
- kind: ServiceAccount
67+
name: memcached-operator
68+
roleRef:
69+
kind: Role
70+
name: memcached-operator
71+
apiGroup: rbac.authorization.k8s.io
72+
73+
---
74+
75+
apiVersion: apps/v1
76+
kind: Deployment
77+
metadata:
78+
name: memcached-operator
79+
spec:
80+
replicas: 1
81+
selector:
82+
matchLabels:
83+
name: memcached-operator
84+
template:
85+
metadata:
86+
labels:
87+
name: memcached-operator
88+
spec:
89+
serviceAccountName: memcached-operator
90+
containers:
91+
- name: memcached-operator
92+
image: quay.io/coreos/operator-sdk-dev:test-framework-operator
93+
ports:
94+
- containerPort: 60000
95+
name: metrics
96+
command:
97+
- memcached-operator
98+
imagePullPolicy: Always
99+
env:
100+
- name: WATCH_NAMESPACE
101+
valueFrom:
102+
fieldRef:
103+
fieldPath: metadata.namespace
104+
- name: OPERATOR_NAME
105+
value: "memcached-operator"
106+
107+
`
108+
109+
func TestVerifyDeploymentImage(t *testing.T) {
110+
if err := verifyDeploymentImage([]byte(memcachedNamespaceManExample), "quay.io/coreos/operator-sdk-dev:test-framework-operator"); err != nil {
111+
t.Fatalf("verifyDeploymentImage incorrectly reported an error: %v", err)
112+
}
113+
if err := verifyDeploymentImage([]byte(memcachedNamespaceManExample), "different-image-name"); err == nil {
114+
t.Fatal("verifyDeploymentImage did not report an error on an incorrect manifest")
115+
}
116+
}

commands/operator-sdk/cmd/test.go

Lines changed: 7 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -15,82 +15,20 @@
1515
package cmd
1616

1717
import (
18-
"io/ioutil"
19-
"log"
20-
"os"
21-
"strings"
22-
23-
"github.com/operator-framework/operator-sdk/pkg/test"
18+
"github.com/operator-framework/operator-sdk/commands/operator-sdk/cmd/test"
2419

2520
"github.com/spf13/cobra"
2621
)
2722

28-
var (
29-
testLocation string
30-
kubeconfig string
31-
globalManifestPath string
32-
namespacedManifestPath string
33-
goTestFlags string
34-
)
35-
3623
func NewTestCmd() *cobra.Command {
3724
testCmd := &cobra.Command{
38-
Use: "test --test-location <path to tests directory> [flags]",
39-
Short: "Run End-To-End tests",
40-
Run: testFunc,
41-
}
42-
defaultKubeConfig := ""
43-
homedir, ok := os.LookupEnv("HOME")
44-
if ok {
45-
defaultKubeConfig = homedir + "/.kube/config"
25+
Use: "test",
26+
Short: "Tests the operator",
27+
Long: `The test command has subcommands that can test the operator locally or from within a cluster.
28+
`,
4629
}
47-
testCmd.Flags().StringVarP(&testLocation, "test-location", "t", "", "Location of test files (e.g. ./test/e2e/)")
48-
testCmd.MarkFlagRequired("test-location")
49-
testCmd.Flags().StringVarP(&kubeconfig, "kubeconfig", "k", defaultKubeConfig, "Kubeconfig path")
50-
testCmd.Flags().StringVarP(&globalManifestPath, "global-init", "g", "deploy/crd.yaml", "Path to manifest for Global resources (e.g. CRD manifest)")
51-
testCmd.Flags().StringVarP(&namespacedManifestPath, "namespaced-init", "n", "", "Path to manifest for per-test, namespaced resources (e.g. RBAC and Operator manifest)")
52-
testCmd.Flags().StringVarP(&goTestFlags, "go-test-flags", "f", "", "Additional flags to pass to go test")
5330

31+
testCmd.AddCommand(cmdtest.NewTestLocalCmd())
32+
testCmd.AddCommand(cmdtest.NewTestClusterCmd())
5433
return testCmd
5534
}
56-
57-
func testFunc(cmd *cobra.Command, args []string) {
58-
// if no namespaced manifest path is given, combine deploy/sa.yaml, deploy/rbac.yaml and deploy/operator.yaml
59-
if namespacedManifestPath == "" {
60-
os.Mkdir("deploy/test", os.FileMode(int(0775)))
61-
namespacedManifestPath = "deploy/test/namespace-manifests.yaml"
62-
sa, err := ioutil.ReadFile("deploy/sa.yaml")
63-
if err != nil {
64-
log.Fatalf("could not find sa manifest: %v", err)
65-
}
66-
rbac, err := ioutil.ReadFile("deploy/rbac.yaml")
67-
if err != nil {
68-
log.Fatalf("could not find rbac manifest: %v", err)
69-
}
70-
operator, err := ioutil.ReadFile("deploy/operator.yaml")
71-
if err != nil {
72-
log.Fatalf("could not find operator manifest: %v", err)
73-
}
74-
combined := append(sa, []byte("\n---\n")...)
75-
combined = append(combined, rbac...)
76-
combined = append(combined, []byte("\n---\n")...)
77-
combined = append(combined, operator...)
78-
err = ioutil.WriteFile(namespacedManifestPath, combined, os.FileMode(int(0664)))
79-
if err != nil {
80-
log.Fatalf("could not create temporary namespaced manifest file: %v", err)
81-
}
82-
defer func() {
83-
err := os.Remove(namespacedManifestPath)
84-
if err != nil {
85-
log.Fatalf("could not delete temporary namespace manifest file")
86-
}
87-
}()
88-
}
89-
testArgs := []string{"test", testLocation + "/..."}
90-
testArgs = append(testArgs, "-"+test.KubeConfigFlag, kubeconfig)
91-
testArgs = append(testArgs, "-"+test.NamespacedManPathFlag, namespacedManifestPath)
92-
testArgs = append(testArgs, "-"+test.GlobalManPathFlag, globalManifestPath)
93-
testArgs = append(testArgs, "-"+test.ProjRootFlag, mustGetwd())
94-
testArgs = append(testArgs, strings.Split(goTestFlags, " ")...)
95-
execCmd(os.Stdout, "go", testArgs...)
96-
}

0 commit comments

Comments
 (0)