Skip to content

pkg/test: add in-cluster/in-image testing support #469

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 39 commits into from
Sep 25, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
b25c0d1
pkg/test: add in-cluster/in-image testing support
AlexNPavel Aug 31, 2018
c0a67a7
*: change the way the dockerfiles are used
AlexNPavel Sep 11, 2018
923505c
commands/.../build.go: remove unused flag
AlexNPavel Sep 12, 2018
7cfeb77
commands/.../build.go: split some test stuff into new function
AlexNPavel Sep 12, 2018
afffc38
commands,templates: change test manifest handling
AlexNPavel Sep 12, 2018
e401771
commands/.../test: create new test subcommands
AlexNPavel Sep 13, 2018
046725f
.travis.yml: update travis.yml to match test subcommands
AlexNPavel Sep 13, 2018
aa34ac0
commands/.../test/cluster.go: fix error capitalization
AlexNPavel Sep 13, 2018
becee99
commands/.../test/*: fix subcommands
AlexNPavel Sep 13, 2018
2790687
commands/.../test/*: make the tests work and improve output
AlexNPavel Sep 13, 2018
49fc1c1
commands/.../test/cluster.go: return errors instead of cmderror
AlexNPavel Sep 13, 2018
d6f22e3
*: add tests for incluster test image and fix bugs
AlexNPavel Sep 13, 2018
732e049
Merge branch 'master' into incluster-test
AlexNPavel Sep 13, 2018
2441947
commands/.../test/cluster.go: uncomment deferred cleanup
AlexNPavel Sep 14, 2018
73ea6e2
commands/.../build.go: shorten code a bit
AlexNPavel Sep 14, 2018
4f303c7
commands/.../build.go: more code shortening
AlexNPavel Sep 14, 2018
128f935
commands/.../build.go: change intermediateImageName to baseImageName
AlexNPavel Sep 14, 2018
b4d18dc
commands/.../test/cluster.go: clean up error messages
AlexNPavel Sep 14, 2018
695995d
commands/.../cmd/test: more error message improvements
AlexNPavel Sep 17, 2018
ddcf878
commands/.../test: changes suggested by shawn-hurley
AlexNPavel Sep 19, 2018
9769960
Merge branch 'master' of github.com:operator-framework/operator-sdk i…
AlexNPavel Sep 19, 2018
3db2baf
commands/.../test/cluster.go: change imagePullPolicyFlag
AlexNPavel Sep 19, 2018
07779a0
test/e2e/memcached_test.go: update e2e tests
AlexNPavel Sep 19, 2018
38ce471
commands/.../test/cluster.go: handle pending phase
AlexNPavel Sep 19, 2018
e34fcaf
commands/.../build.go: suggestions from LiliC
AlexNPavel Sep 20, 2018
db5bee2
commands/.../build.go: add comment to verifyDeploymentImage
AlexNPavel Sep 20, 2018
8d0f071
Merge branch 'master' into incluster-test
AlexNPavel Sep 21, 2018
276a62f
commands/.../test/cluster.go: add flag for service account
AlexNPavel Sep 21, 2018
fba6df7
commands/.../build{,_test}.go: add unit test for verifyDeploymentImage
AlexNPavel Sep 21, 2018
901b22e
.travis.yml: enable go testing of ./commands/...
AlexNPavel Sep 21, 2018
50eb65c
commands/.../build.go: make unmarshalling error fatal
AlexNPavel Sep 21, 2018
5102bb3
commands,pkg: improvements suggested by haseeb
AlexNPavel Sep 24, 2018
d722d6e
pkg/generator: remove docker_build.sh generation
AlexNPavel Sep 24, 2018
b7900f9
Merge branch 'master' into incluster-test
AlexNPavel Sep 24, 2018
81be3d2
pkg/generator/generator.go: change defaultExecFileMode to 0755
AlexNPavel Sep 24, 2018
ad18ed6
commands/.../test/cluster: support lowercase pull policy
AlexNPavel Sep 24, 2018
49ab763
commands/.../test: use structs for test flags/vars
AlexNPavel Sep 24, 2018
7947e47
commands/.../build: add nil error check
AlexNPavel Sep 24, 2018
3e1dfcb
test/e2e/memcached_test.go: check error from readfile
AlexNPavel Sep 25, 2018
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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 ./...
Expand Down
121 changes: 114 additions & 7 deletions commands/operator-sdk/cmd/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 <image>",
Short: "Compiles code and builds artifacts",
Long: `The operator-sdk build command compiles the code, builds the executables,
Expand All @@ -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")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are keeping the default false for now and plan on changing it? I think eventually this should be a default of true? Thoughts?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For now, we will keep it as default false. The SDK does not create any default tests for operators and the build would fail in that case. This is why we need a user to confirm that they have tests that they want to include. The test binary is also 34 MB, which may be undesirable to include by default.

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")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could be more specific where this error occurs, something like: "log.Fatalf(Deployment verification failed: manifest file contains a 'kind' field that is not a string: %v, yamlMap["kind"])"

Do we need to do a log.Fatal here? Maybe just returning the error and handling it elsewhere would be better? If this error occurs then we should regenerate the manifest file? Just a suggestion.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the kind field is not a string, wouldn't that cause a crash later on when the test tries to decode it? I feel it's best to treat that as a fatal situation. Also, the namespaced manifest is not generated by the sdk. The initial operator.yaml file is generated on operator-sdk new ..., but the user modifies that and can also provide a different manifest file if they need more than just the operator deployment for their tests. It's probably best to fail and notify the user of the issue with their manifest.

}
if kind == "Deployment" {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we should rather check the opposite:

if kind != "Deployment" {
  return fmt.Sprintf("Deployment verification failed: Deployment manifest was not kind Deployment but: %s", kind)
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The verifyDeploymentImage function checks the entire namespaced manifest file, and while the most basic case would be just a single deployment for the operator, it could have multiple deployments or other objects, such as configmaps. We use kind=='Deployment' to ignore other resources and just check deployments.

// 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) {
Expand All @@ -56,18 +143,38 @@ 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)))
}
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)
}
}
116 changes: 116 additions & 0 deletions commands/operator-sdk/cmd/build_test.go
Original file line number Diff line number Diff line change
@@ -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")
}
}
76 changes: 7 additions & 69 deletions commands/operator-sdk/cmd/test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 <path to tests directory> [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...)
}
Loading