-
Notifications
You must be signed in to change notification settings - Fork 1.8k
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
Changes from all commits
b25c0d1
c0a67a7
923505c
7cfeb77
afffc38
e401771
046725f
aa34ac0
becee99
2790687
49fc1c1
d6f22e3
732e049
2441947
73ea6e2
4f303c7
128f935
b4d18dc
695995d
ddcf878
9769960
3db2baf
07779a0
38ce471
e34fcaf
db5bee2
8d0f071
276a62f
fba6df7
901b22e
50eb65c
5102bb3
d722d6e
b7900f9
81be3d2
ad18ed6
49ab763
7947e47
3e1dfcb
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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, | ||
|
@@ -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") | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If the |
||
} | ||
if kind == "Deployment" { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe we should rather check the opposite:
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The |
||
// 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,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) | ||
} | ||
} |
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") | ||
} | ||
} |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.