Skip to content

Commit 91bdb5c

Browse files
committed
pkg/test: add in-cluster/in-image testing support
Issue #435
1 parent 2a3e6bc commit 91bdb5c

File tree

7 files changed

+288
-18
lines changed

7 files changed

+288
-18
lines changed

Gopkg.lock

Lines changed: 0 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

commands/operator-sdk/cmd/build.go

Lines changed: 138 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,17 +15,31 @@
1515
package cmd
1616

1717
import (
18+
"bytes"
1819
"fmt"
20+
"io/ioutil"
21+
"log"
1922
"os"
2023
"os/exec"
2124

25+
"github.com/operator-framework/operator-sdk/commands/operator-sdk/cmd/cmdutil"
2226
cmdError "github.com/operator-framework/operator-sdk/commands/operator-sdk/error"
27+
"github.com/operator-framework/operator-sdk/pkg/generator"
2328

29+
"github.com/ghodss/yaml"
2430
"github.com/spf13/cobra"
2531
)
2632

33+
var (
34+
namespacedManBuild string
35+
globalManBuild string
36+
rbacManBuild string
37+
testLocationBuild string
38+
enableTests bool
39+
)
40+
2741
func NewBuildCmd() *cobra.Command {
28-
return &cobra.Command{
42+
buildCmd := &cobra.Command{
2943
Use: "build <image>",
3044
Short: "Compiles code and builds artifacts",
3145
Long: `The operator-sdk build command compiles the code, builds the executables,
@@ -42,6 +56,74 @@ For example:
4256
`,
4357
Run: buildFunc,
4458
}
59+
buildCmd.Flags().BoolVarP(&enableTests, "enable-tests", "e", false, "Enable in-cluster testing by adding test binary to the image")
60+
buildCmd.Flags().StringVarP(&testLocationBuild, "test-location", "t", "./test/e2e", "Location of tests")
61+
buildCmd.Flags().StringVarP(&namespacedManBuild, "namespaced", "n", "", "Path of namespaced resources for tests")
62+
buildCmd.Flags().StringVarP(&globalManBuild, "global", "g", "deploy/crd.yaml", "Path of global resources for tests")
63+
buildCmd.Flags().StringVarP(&rbacManBuild, "rbac", "r", "deploy/rbac.yaml", "Path of global resources for tests")
64+
return buildCmd
65+
}
66+
67+
func parseRoles(yamlFile []byte) ([]byte, error) {
68+
res := make([]byte, 0)
69+
yamlSplit := bytes.Split(yamlFile, []byte("\n---\n"))
70+
for _, yamlSpec := range yamlSplit {
71+
yamlMap := make(map[string]interface{})
72+
err := yaml.Unmarshal(yamlSpec, &yamlMap)
73+
if err != nil {
74+
return nil, err
75+
}
76+
if yamlMap["kind"].(string) == "Role" {
77+
ruleBytes, err := yaml.Marshal(yamlMap["rules"])
78+
if err != nil {
79+
return nil, err
80+
}
81+
res = append(res, ruleBytes...)
82+
}
83+
}
84+
return res, nil
85+
}
86+
87+
func verifyDeploymentImage(yamlFile []byte, imageName string) string {
88+
warningMessages := ""
89+
yamlSplit := bytes.Split(yamlFile, []byte("\n---\n"))
90+
for _, yamlSpec := range yamlSplit {
91+
yamlMap := make(map[string]interface{})
92+
err := yaml.Unmarshal(yamlSpec, &yamlMap)
93+
if err != nil {
94+
fmt.Printf("WARNING: Could not unmarshal yaml namespaced spec")
95+
return ""
96+
}
97+
if yamlMap["kind"].(string) == "Deployment" {
98+
// this is ugly and hacky; we should probably make this cleaner
99+
nestedMap, ok := yamlMap["spec"].(map[string]interface{})
100+
if !ok {
101+
continue
102+
}
103+
nestedMap, ok = nestedMap["template"].(map[string]interface{})
104+
if !ok {
105+
continue
106+
}
107+
nestedMap, ok = nestedMap["spec"].(map[string]interface{})
108+
if !ok {
109+
continue
110+
}
111+
containersArray, ok := nestedMap["containers"].([]interface{})
112+
if !ok {
113+
continue
114+
}
115+
for _, item := range containersArray {
116+
image, ok := item.(map[string]interface{})["image"].(string)
117+
if !ok {
118+
continue
119+
}
120+
if image != imageName {
121+
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)
122+
}
123+
}
124+
}
125+
}
126+
return warningMessages
45127
}
46128

47129
const (
@@ -56,18 +138,73 @@ func buildFunc(cmd *cobra.Command, args []string) {
56138
}
57139

58140
bcmd := exec.Command(build)
141+
bcmd.Env = append(os.Environ(), fmt.Sprintf("TEST_LOCATION=%v", testLocationBuild))
142+
bcmd.Env = append(bcmd.Env, fmt.Sprintf("ENABLE_TESTS=%v", enableTests))
59143
o, err := bcmd.CombinedOutput()
60144
if err != nil {
61145
cmdError.ExitWithError(cmdError.ExitError, fmt.Errorf("failed to build: (%v)", string(o)))
62146
}
63147
fmt.Fprintln(os.Stdout, string(o))
64148

149+
namespacedRolesBytes := make([]byte, 0)
150+
genWarning := ""
65151
image := args[0]
152+
if enableTests {
153+
if namespacedManBuild == "" {
154+
os.Mkdir("deploy/test", os.FileMode(int(0775)))
155+
namespacedManBuild = "deploy/test/namespace-manifests.yaml"
156+
rbac, err := ioutil.ReadFile("deploy/rbac.yaml")
157+
if err != nil {
158+
log.Fatalf("could not find rbac manifest: %v", err)
159+
}
160+
operator, err := ioutil.ReadFile("deploy/operator.yaml")
161+
if err != nil {
162+
log.Fatalf("could not find operator manifest: %v", err)
163+
}
164+
combined := append(rbac, []byte("\n---\n")...)
165+
combined = append(combined, operator...)
166+
err = ioutil.WriteFile(namespacedManBuild, combined, os.FileMode(int(0664)))
167+
if err != nil {
168+
log.Fatalf("could not create temporary namespaced manifest file: %v", err)
169+
}
170+
defer func() {
171+
err := os.Remove(namespacedManBuild)
172+
if err != nil {
173+
log.Fatalf("could not delete temporary namespace manifest file")
174+
}
175+
}()
176+
}
177+
namespacedBytes, err := ioutil.ReadFile(namespacedManBuild)
178+
if err != nil {
179+
log.Fatalf("could not read rbac manifest: %v", err)
180+
}
181+
namespacedRolesBytes, err = parseRoles(namespacedBytes)
182+
if err != nil {
183+
log.Fatalf("could not parse namespaced manifest file for rbac roles: %v", err)
184+
}
185+
genWarning = verifyDeploymentImage(namespacedBytes, image)
186+
global, err := ioutil.ReadFile(globalManBuild)
187+
if err != nil {
188+
cmdError.ExitWithError(cmdError.ExitError, fmt.Errorf("failed to read global manifest: (%v)", err))
189+
}
190+
c := cmdutil.GetConfig()
191+
if err = generator.RenderTestYaml(c, string(global), string(namespacedRolesBytes), image); err != nil {
192+
cmdError.ExitWithError(cmdError.ExitError, fmt.Errorf("failed to generate deploy/test-gen.yaml: (%v)", err))
193+
}
194+
os.Link("tmp/build/dockerfiles/Dockerfile_Tests", "tmp/build/Dockerfile")
195+
} else {
196+
os.Link("tmp/build/dockerfiles/Dockerfile_Standard", "tmp/build/Dockerfile")
197+
}
198+
defer os.Remove("tmp/build/Dockerfile")
66199
dbcmd := exec.Command(dockerBuild)
67200
dbcmd.Env = append(os.Environ(), fmt.Sprintf("IMAGE=%v", image))
201+
dbcmd.Env = append(dbcmd.Env, fmt.Sprintf("NAMESPACEDMAN=%v", namespacedManBuild))
68202
o, err = dbcmd.CombinedOutput()
69203
if err != nil {
70204
cmdError.ExitWithError(cmdError.ExitError, fmt.Errorf("failed to output build image %v: (%v)", image, string(o)))
71205
}
72206
fmt.Fprintln(os.Stdout, string(o))
207+
if genWarning != "" {
208+
fmt.Printf("%s\n", genWarning)
209+
}
73210
}

pkg/generator/generator.go

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ const (
3939
tmpDir = "tmp"
4040
buildDir = tmpDir + "/build"
4141
codegenDir = tmpDir + "/codegen"
42+
dockerDir = buildDir + "/dockerfiles"
4243
pkgDir = "pkg"
4344
apisDir = pkgDir + "/apis"
4445
stubDir = pkgDir + "/stub"
@@ -52,7 +53,9 @@ const (
5253
types = "types.go"
5354
build = "build.sh"
5455
dockerBuild = "docker_build.sh"
55-
dockerfile = "Dockerfile"
56+
standardDockerfile = "Dockerfile_Standard"
57+
testingDockerfile = "Dockerfile_Testing"
58+
goTest = "go-test.sh"
5659
boilerplate = "boilerplate.go.txt"
5760
updateGenerated = "update-generated.sh"
5861
gopkgtoml = "Gopkg.toml"
@@ -76,6 +79,7 @@ const (
7679
operatorTmplName = "deploy/operator.yaml"
7780
rbacTmplName = "deploy/rbac.yaml"
7881
crTmplName = "deploy/cr.yaml"
82+
testYamlName = "deploy/test-gen.yaml"
7983
pluralSuffix = "s"
8084
)
8185

@@ -246,6 +250,16 @@ func renderDeployFiles(deployDir, projectName, apiVersion, kind string) error {
246250
return renderWriteFile(filepath.Join(deployDir, "operator.yaml"), operatorTmplName, operatorYamlTmpl, opTd)
247251
}
248252

253+
func RenderTestYaml(c *Config, globalManifest, extraRoles, image string) error {
254+
opTd := tmplData{
255+
GlobalManifest: globalManifest,
256+
ExtraRoles: extraRoles,
257+
ProjectName: c.ProjectName,
258+
Image: image,
259+
}
260+
return renderWriteFile(filepath.Join(deployDir, "test-gen.yaml"), testYamlName, testYamlTmpl, opTd)
261+
}
262+
249263
// RenderOlmCatalog generates catalog manifests "deploy/olm-catalog/*"
250264
// The current working directory must be the project repository root
251265
func RenderOlmCatalog(c *Config, image, version string) error {
@@ -343,10 +357,16 @@ func renderBuildFiles(buildDir, repoPath, projectName string) error {
343357
dTd := tmplData{
344358
ProjectName: projectName,
345359
}
346-
if err := renderFile(buf, "tmp/build/Dockerfile", dockerFileTmpl, dTd); err != nil {
360+
361+
if err := renderWriteFile(filepath.Join(buildDir, "dockerfiles", standardDockerfile), "tmp/build/dockerfiles/Dockerfile_Standard", standardDockerFileTmpl, dTd); err != nil {
362+
return err
363+
}
364+
365+
if err := renderWriteFile(filepath.Join(buildDir, "dockerfiles", testingDockerfile), "tmp/build/dockerfiles/Dockerfile_Testing", testingDockerFileTmpl, dTd); err != nil {
347366
return err
348367
}
349-
return renderWriteFile(filepath.Join(buildDir, dockerfile), "tmp/build/Dockerfile", dockerFileTmpl, dTd)
368+
369+
return renderWriteFile(filepath.Join(buildDir, goTest), "tmp/build/go-test.sh", goTestScript, tmplData{})
350370
}
351371

352372
func renderDockerBuildFile(w io.Writer) error {
@@ -459,6 +479,10 @@ type tmplData struct {
459479
CRDVersion string
460480
CSVName string
461481
CatalogVersion string
482+
483+
// global manifest used for testing
484+
GlobalManifest string
485+
ExtraRoles string
462486
}
463487

464488
// Creates all the necesary directories for the generated files
@@ -471,6 +495,7 @@ func (g *Generator) generateDirStructure() error {
471495
filepath.Join(g.projectName, olmCatalogDir),
472496
filepath.Join(g.projectName, buildDir),
473497
filepath.Join(g.projectName, codegenDir),
498+
filepath.Join(g.projectName, dockerDir),
474499
filepath.Join(g.projectName, versionDir),
475500
filepath.Join(g.projectName, apisDir, apiDirName(g.apiVersion), version(g.apiVersion)),
476501
filepath.Join(g.projectName, stubDir),

pkg/generator/templates.go

Lines changed: 78 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -424,6 +424,54 @@ spec:
424424
version: {{.Version}}
425425
`
426426

427+
const testYamlTmpl = `{{.GlobalManifest}}
428+
---
429+
kind: Role
430+
apiVersion: rbac.authorization.k8s.io/v1beta1
431+
metadata:
432+
name: {{.ProjectName}}-test
433+
rules:
434+
- apiGroups:
435+
- "rbac.authorization.k8s.io"
436+
resources:
437+
- "*"
438+
verbs:
439+
- "*"
440+
{{.ExtraRoles}}
441+
---
442+
kind: RoleBinding
443+
apiVersion: rbac.authorization.k8s.io/v1beta1
444+
metadata:
445+
name: default-account-{{.ProjectName}}-test
446+
subjects:
447+
- kind: ServiceAccount
448+
name: default
449+
roleRef:
450+
kind: Role
451+
name: {{.ProjectName}}-test
452+
apiGroup: rbac.authorization.k8s.io
453+
---
454+
apiVersion: v1
455+
kind: Pod
456+
metadata:
457+
name: {{.ProjectName}}-test
458+
spec:
459+
restartPolicy: Never
460+
containers:
461+
- name: {{.ProjectName}}-test
462+
image: {{.Image}}
463+
imagePullPolicy: Always
464+
command: ["/go-test.sh"]
465+
resources:
466+
requests:
467+
cpu: 1
468+
env:
469+
- name: TEST_NAMESPACE
470+
valueFrom:
471+
fieldRef:
472+
fieldPath: metadata.namespace
473+
`
474+
427475
const operatorYamlTmpl = `apiVersion: apps/v1
428476
kind: Deployment
429477
metadata:
@@ -541,8 +589,13 @@ mkdir -p ${BIN_DIR}
541589
PROJECT_NAME="{{.ProjectName}}"
542590
REPO_PATH="{{.RepoPath}}"
543591
BUILD_PATH="${REPO_PATH}/cmd/${PROJECT_NAME}"
592+
TEST_PATH="${REPO_PATH}/${TEST_LOCATION}"
544593
echo "building "${PROJECT_NAME}"..."
545594
GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -o ${BIN_DIR}/${PROJECT_NAME} $BUILD_PATH
595+
if $ENABLE_TESTS ; then
596+
echo "building "${PROJECT_NAME}-test"..."
597+
GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go test -c -o ${BIN_DIR}/${PROJECT_NAME}-test $TEST_PATH
598+
fi
546599
`
547600

548601
const dockerBuildTmpl = `#!/usr/bin/env bash
@@ -555,15 +608,38 @@ fi
555608
: ${IMAGE:?"Need to set IMAGE, e.g. gcr.io/<repo>/<your>-operator"}
556609
557610
echo "building container ${IMAGE}..."
558-
docker build -t "${IMAGE}" -f tmp/build/Dockerfile .
611+
docker build -t "${IMAGE}" -f tmp/build/Dockerfile . --build-arg NAMESPACEDMAN=$NAMESPACEDMAN
559612
`
560613

561-
const dockerFileTmpl = `FROM alpine:3.6
614+
const goTestScript = `#!/bin/sh
615+
616+
memcached-operator-test -test.parallel=1 -test.failfast -root=/ -kubeconfig=incluster -namespacedMan=namespaced.yaml -test.v
617+
`
618+
619+
const standardDockerFileTmpl = `FROM alpine:3.6
562620
563621
RUN adduser -D {{.ProjectName}}
564622
USER {{.ProjectName}}
565623
566624
ADD tmp/_output/bin/{{.ProjectName}} /usr/local/bin/{{.ProjectName}}
625+
626+
# just keep this to ignore warnings
627+
ARG NAMESPACEDMAN
628+
`
629+
630+
const testingDockerFileTmpl = `FROM alpine:3.6
631+
632+
RUN adduser -D {{.ProjectName}}
633+
USER {{.ProjectName}}
634+
635+
ADD tmp/_output/bin/{{.ProjectName}} /usr/local/bin/{{.ProjectName}}
636+
637+
# just keep this to ignore warnings
638+
ARG NAMESPACEDMAN
639+
640+
ADD tmp/_output/bin/{{.ProjectName}}-test /usr/local/bin/{{.ProjectName}}-test
641+
ADD $NAMESPACEDMAN /namespaced.yaml
642+
ADD tmp/build/go-test.sh /go-test.sh
567643
`
568644

569645
// apiDocTmpl is the template for apis/../doc.go

0 commit comments

Comments
 (0)