From 3f39fb9f8c4498ca8e75c38f0e4cf66eff35573f Mon Sep 17 00:00:00 2001 From: Dylan Murray Date: Tue, 11 Sep 2018 15:53:07 -0400 Subject: [PATCH 01/16] Add --type flag to new command Break out Ansible Operator logic from main func More updates Finish work on --type ansible Add build command Add Ansible Galaxy init Add --generate-playbook flag small updates Fix build command to detect cmd folder move stuff around so we can clean up tmp Make default not generate a playbook Add removing of temp init dir Fix generator test Add test for ansibleOperator yaml generation Add more tests and fix whitespace on Dockerfile --- commands/operator-sdk/cmd/build.go | 32 +++- commands/operator-sdk/cmd/new.go | 33 +++-- pkg/generator/generator.go | 225 ++++++++++++++++++++++++++--- pkg/generator/generator_test.go | 95 ++++++++++++ pkg/generator/templates.go | 39 ++++- 5 files changed, 383 insertions(+), 41 deletions(-) diff --git a/commands/operator-sdk/cmd/build.go b/commands/operator-sdk/cmd/build.go index b12c1446a5..a2be512a08 100644 --- a/commands/operator-sdk/cmd/build.go +++ b/commands/operator-sdk/cmd/build.go @@ -22,6 +22,7 @@ import ( "log" "os" "os/exec" + "strings" "github.com/operator-framework/operator-sdk/commands/operator-sdk/cmd/cmdutil" cmdError "github.com/operator-framework/operator-sdk/commands/operator-sdk/error" @@ -135,6 +136,7 @@ func renderTestManifest(image string) { const ( build = "./tmp/build/build.sh" configYaml = "./config/config.yaml" + mainGo = "./cmd/%s/main.go" ) func buildFunc(cmd *cobra.Command, args []string) { @@ -142,14 +144,15 @@ func buildFunc(cmd *cobra.Command, args []string) { cmdError.ExitWithError(cmdError.ExitBadArgs, fmt.Errorf("build command needs exactly 1 argument")) } - 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))) + // Don't need to buld go code if Ansible Operator + if buildCmd() { + bcmd := exec.Command(build) + 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)) } - fmt.Fprintln(os.Stdout, string(o)) image := args[0] baseImageName := image @@ -157,7 +160,7 @@ func buildFunc(cmd *cobra.Command, args []string) { baseImageName += "-intermediate" } dbcmd := exec.Command("docker", "build", ".", "-f", "tmp/build/Dockerfile", "-t", baseImageName) - o, err = dbcmd.CombinedOutput() + o, err := dbcmd.CombinedOutput() if err != nil { if enableTests { cmdError.ExitWithError(cmdError.ExitError, fmt.Errorf("failed to build intermediate image for %s image: (%s)", image, string(o))) @@ -178,3 +181,16 @@ func buildFunc(cmd *cobra.Command, args []string) { renderTestManifest(image) } } + +func buildCmd() bool { + dir, err := os.Getwd() + if err != nil { + cmdError.ExitWithError(cmdError.ExitError, fmt.Errorf("failed to get current working dir: %v", err)) + } + dirSplit := strings.Split(dir, "/") + projectName := dirSplit[len(dirSplit)-1] + if _, err = os.Stat(fmt.Sprintf(mainGo, projectName)); err == nil { + return true + } + return false +} diff --git a/commands/operator-sdk/cmd/new.go b/commands/operator-sdk/cmd/new.go index 6f4ed8bb2a..b20ee13160 100644 --- a/commands/operator-sdk/cmd/new.go +++ b/commands/operator-sdk/cmd/new.go @@ -53,23 +53,29 @@ generates a skeletal app-operator application in $GOPATH/src/github.com/example. newCmd.MarkFlagRequired("api-version") newCmd.Flags().StringVar(&kind, "kind", "", "Kubernetes CustomResourceDefintion kind. (e.g AppService)") newCmd.MarkFlagRequired("kind") + newCmd.Flags().StringVar(&operatorType, "type", "go", "Type of operator to initialize (e.g \"ansible\")") newCmd.Flags().BoolVar(&skipGit, "skip-git-init", false, "Do not init the directory as a git repository") + newCmd.Flags().BoolVar(&generatePlaybook, "generate-playbook", false, "Generate a playbook skeleton in watches.yaml. (Only used for --type ansible)") return newCmd } var ( - apiVersion string - kind string - projectName string - skipGit bool + apiVersion string + kind string + operatorType string + projectName string + skipGit bool + generatePlaybook bool ) const ( - gopath = "GOPATH" - src = "src" - dep = "dep" - ensureCmd = "ensure" + gopath = "GOPATH" + src = "src" + dep = "dep" + ensureCmd = "ensure" + goOperatorType = "go" + ansibleOperatorType = "ansible" ) func newFunc(cmd *cobra.Command, args []string) { @@ -79,13 +85,15 @@ func newFunc(cmd *cobra.Command, args []string) { parse(args) mustBeNewProject() verifyFlags() - g := generator.NewGenerator(apiVersion, kind, projectName, repoPath()) + g := generator.NewGenerator(apiVersion, kind, operatorType, projectName, repoPath(), generatePlaybook) err := g.Render() if err != nil { cmdError.ExitWithError(cmdError.ExitError, fmt.Errorf("failed to create project %v: %v", projectName, err)) } - pullDep() - generate.K8sCodegen(projectName) + if operatorType == goOperatorType { + pullDep() + generate.K8sCodegen(projectName) + } initGit() } @@ -136,6 +144,9 @@ func verifyFlags() { if len(kind) == 0 { cmdError.ExitWithError(cmdError.ExitBadArgs, errors.New("--kind must not have empty value")) } + if operatorType != goOperatorType && operatorType != ansibleOperatorType { + cmdError.ExitWithError(cmdError.ExitBadArgs, errors.New("--type can only be `go` or `ansible`")) + } kindFirstLetter := string(kind[0]) if kindFirstLetter != strings.ToUpper(kindFirstLetter) { cmdError.ExitWithError(cmdError.ExitBadArgs, errors.New("--kind must start with an uppercase letter")) diff --git a/pkg/generator/generator.go b/pkg/generator/generator.go index e8893cebd2..1606d2b96b 100644 --- a/pkg/generator/generator.go +++ b/pkg/generator/generator.go @@ -16,10 +16,12 @@ package generator import ( "bytes" + "errors" "fmt" "io" "io/ioutil" "os" + "os/exec" "path/filepath" "strings" "text/template" @@ -33,15 +35,22 @@ const ( defaultDirFileMode = 0750 defaultFileMode = 0644 defaultExecFileMode = 0755 + + // types + goOperatorType = "go" + ansibleOperatorType = "ansible" + // dirs cmdDir = "cmd" deployDir = "deploy" + rolesDir = "roles" olmCatalogDir = deployDir + "/olm-catalog" configDir = "config" tmpDir = "tmp" buildDir = tmpDir + "/build" codegenDir = tmpDir + "/codegen" dockerTestDir = buildDir + "/test-framework" + initDir = tmpDir + "/init" pkgDir = "pkg" apisDir = pkgDir + "/apis" stubDir = pkgDir + "/stub" @@ -59,6 +68,7 @@ const ( goTest = "go-test.sh" boilerplate = "boilerplate.go.txt" updateGenerated = "update-generated.sh" + galaxyInit = "galaxy-init.sh" gopkgtoml = "Gopkg.toml" gopkglock = "Gopkg.lock" config = "config.yaml" @@ -70,6 +80,11 @@ const ( crdYaml = "crd.yaml" gitignore = ".gitignore" versionfile = "version.go" + watches = "watches.yaml" + playbook = "playbook.yaml" + + // commands + galaxyInitCmd = initDir + "/" + galaxyInit // sdkImport is the operator-sdk import path. sdkImport = "github.com/operator-framework/operator-sdk/pkg/sdk" @@ -90,6 +105,10 @@ type Generator struct { // apiVersion is the kubernetes apiVersion that has the format of $GROUP_NAME/$VERSION. apiVersion string kind string + // operatorType is the type of operator to initialize + operatorType string + // generatePlaybook is a switch to set playbook instead of role in watches.yaml for Ansible Operator + generatePlaybook bool // projectName is name of the new operator application // and is also the name of the base directory. projectName string @@ -98,8 +117,15 @@ type Generator struct { } // NewGenerator creates a new scaffold Generator. -func NewGenerator(apiVersion, kind, projectName, repoPath string) *Generator { - return &Generator{apiVersion: apiVersion, kind: kind, projectName: projectName, repoPath: repoPath} +func NewGenerator(apiVersion, kind, operatorType, projectName, repoPath string, generatePlaybook bool) *Generator { + return &Generator{ + apiVersion: apiVersion, + kind: kind, + operatorType: operatorType, + generatePlaybook: generatePlaybook, + projectName: projectName, + repoPath: repoPath, + } } // Render generates the default project structure: @@ -118,8 +144,108 @@ func NewGenerator(apiVersion, kind, projectName, repoPath string) *Generator { // │ | ├── build // │ | └── codegen // │ └── version +// +// Render generates the following project structure for Operator type Ansible +// ├── +// │ ├── Dockerfile +// │ ├── roles +// │ │ └── +// │ ├── watches.yaml +// │ ├── deploy +// │ │ ├── -CRD.yaml +// │ │ ├── rbac.yaml +// │ │ ├── operator.yaml +// │ ├ └── cr.yaml + func (g *Generator) Render() error { - if err := g.generateDirStructure(); err != nil { + switch g.operatorType { + case ansibleOperatorType: + if err := g.renderAnsibleOperator(); err != nil { + return err + } + case goOperatorType: + if err := g.renderGoOperator(); err != nil { + return err + } + default: + return errors.New(fmt.Sprintf("unexpected operator type [%v]", g.operatorType)) + } + return nil +} + +func (g *Generator) renderAnsibleOperator() error { + if err := g.generateAnsibleDirStructure(); err != nil { + return err + } + if err := g.renderTmp(); err != nil { + return err + } + if err := g.renderRole(); err != nil { + return err + } + if err := g.renderWatches(); err != nil { + return err + } + if err := g.renderPlaybook(); err != nil { + return err + } + return g.renderDeploy() +} + +func (g *Generator) renderRole() error { + fmt.Printf("Rendering Ansible Galaxy role [%v/%v/%v]...\n", g.projectName, rolesDir, g.kind) + agcmd := exec.Command(filepath.Join(g.projectName, galaxyInitCmd)) + _, err := agcmd.CombinedOutput() + if err != nil { + return err + } + // Clean up tmp/init + fmt.Printf("Cleaning up %v\n", filepath.Join(g.projectName, initDir)) + err = os.RemoveAll(filepath.Join(g.projectName, initDir)) + if err != nil { + return err + } + return nil +} + +func (g *Generator) renderPlaybook() error { + if !g.generatePlaybook { + return nil + } + buf := &bytes.Buffer{} + bTd := tmplData{ + Kind: g.kind, + } + + if err := renderFile(buf, playbook, playbookTmpl, bTd); err != nil { + return err + } + if err := writeFileAndPrint(filepath.Join(g.projectName, playbook), buf.Bytes(), defaultFileMode); err != nil { + return err + } + return nil +} + +func (g *Generator) renderWatches() error { + buf := &bytes.Buffer{} + bTd := tmplData{ + GroupName: groupName(g.apiVersion), + Version: version(g.apiVersion), + Kind: g.kind, + GeneratePlaybook: g.generatePlaybook, + } + + if err := renderFile(buf, watches, watchesTmpl, bTd); err != nil { + return err + } + if err := writeFileAndPrint(filepath.Join(g.projectName, watches), buf.Bytes(), defaultFileMode); err != nil { + return err + } + return nil +} + +func (g *Generator) renderGoOperator() error { + if err := g.generateGoDirStructure(); err != nil { return err } @@ -203,7 +329,7 @@ func renderConfigFiles(configDir, apiVersion, kind, projectName string) error { func (g *Generator) renderDeploy() error { dp := filepath.Join(g.projectName, deployDir) - return renderDeployFiles(dp, g.projectName, g.apiVersion, g.kind) + return renderDeployFiles(dp, g.projectName, g.apiVersion, g.kind, g.operatorType) } func renderRBAC(deployDir, projectName, groupName string) error { @@ -215,7 +341,7 @@ func renderRBAC(deployDir, projectName, groupName string) error { return renderWriteFile(filepath.Join(deployDir, rbacYaml), rbacTmplName, rbacYamlTmpl, td) } -func renderDeployFiles(deployDir, projectName, apiVersion, kind string) error { +func renderDeployFiles(deployDir, projectName, apiVersion, kind, operatorType string) error { rbacTd := tmplData{ ProjectName: projectName, GroupName: groupName(apiVersion), @@ -256,6 +382,7 @@ func renderDeployFiles(deployDir, projectName, apiVersion, kind string) error { MetricsPort: k8sutil.PrometheusMetricsPort, MetricsPortName: k8sutil.PrometheusMetricsPortName, OperatorNameEnv: k8sutil.OperatorNameEnvVar, + IsGoOperator: isGoOperator(operatorType), } return renderWriteFile(filepath.Join(deployDir, "operator.yaml"), operatorTmplName, operatorYamlTmpl, opTd) } @@ -324,13 +451,21 @@ func getCSVName(name, version string) string { } func (g *Generator) renderTmp() error { - bDir := filepath.Join(g.projectName, buildDir) - if err := renderBuildFiles(bDir, g.repoPath, g.projectName); err != nil { - return err + switch g.operatorType { + case goOperatorType: + cDir := filepath.Join(g.projectName, codegenDir) + if err := renderCodegenFiles(cDir, g.repoPath, apiDirName(g.apiVersion), version(g.apiVersion), g.projectName); err != nil { + return err + } + case ansibleOperatorType: + iDir := filepath.Join(g.projectName, initDir) + if err := renderInitFiles(iDir, g.repoPath, g.projectName, g.kind); err != nil { + return err + } } - cDir := filepath.Join(g.projectName, codegenDir) - return renderCodegenFiles(cDir, g.repoPath, apiDirName(g.apiVersion), version(g.apiVersion), g.projectName) + bDir := filepath.Join(g.projectName, buildDir) + return renderBuildFiles(bDir, g.repoPath, g.projectName, g.operatorType, g.generatePlaybook) } func (g *Generator) renderVersion() error { @@ -341,25 +476,33 @@ func (g *Generator) renderVersion() error { return renderWriteFile(filepath.Join(g.projectName, versionDir, versionfile), "version/version.go", versionTmpl, td) } -func renderBuildFiles(buildDir, repoPath, projectName string) error { +func renderBuildFiles(buildDir, repoPath, projectName, operatorType string, generatePlaybook bool) error { buf := &bytes.Buffer{} bTd := tmplData{ ProjectName: projectName, RepoPath: repoPath, } - if err := renderFile(buf, "tmp/build/build.sh", buildTmpl, bTd); err != nil { - return err - } - if err := writeFileAndPrint(filepath.Join(buildDir, build), buf.Bytes(), defaultExecFileMode); err != nil { - return err + var dockerFileTmplName string + switch operatorType { + case goOperatorType: + dockerFileTmplName = dockerFileTmpl + if err := renderFile(buf, "tmp/build/build.sh", buildTmpl, bTd); err != nil { + return err + } + if err := writeFileAndPrint(filepath.Join(buildDir, build), buf.Bytes(), defaultExecFileMode); err != nil { + return err + } + case ansibleOperatorType: + dockerFileTmplName = dockerFileAnsibleTmpl } dTd := tmplData{ - ProjectName: projectName, + ProjectName: projectName, + GeneratePlaybook: generatePlaybook, } - if err := renderWriteFile(filepath.Join(buildDir, dockerfile), "tmp/build/Dockerfile", dockerFileTmpl, dTd); err != nil { + if err := renderWriteFile(filepath.Join(buildDir, dockerfile), "tmp/build/Dockerfile", dockerFileTmplName, dTd); err != nil { return err } @@ -374,6 +517,18 @@ func renderBuildFiles(buildDir, repoPath, projectName string) error { return writeFileAndPrint(filepath.Join(buildDir, goTest), buf.Bytes(), defaultExecFileMode) } +func renderInitFiles(initDir, repoPath, projectName, kind string) error { + buf := &bytes.Buffer{} + iTd := tmplData{ + Kind: kind, + Name: projectName, + } + if err := renderFile(buf, galaxyInitCmd, galaxyInitTmpl, iTd); err != nil { + return err + } + return writeFileAndPrint(filepath.Join(initDir, galaxyInit), buf.Bytes(), defaultExecFileMode) +} + func renderCodegenFiles(codegenDir, repoPath, apiDirName, version, projectName string) error { bTd := tmplData{ ProjectName: projectName, @@ -482,10 +637,14 @@ type tmplData struct { // for test framework TestNamespaceEnv string + + // Ansible Operator specific vars + GeneratePlaybook bool + IsGoOperator bool } -// Creates all the necesary directories for the generated files -func (g *Generator) generateDirStructure() error { +// Creates all the necesary directories for the generated files for Go Operator +func (g *Generator) generateGoDirStructure() error { dirsToCreate := []string{ g.projectName, filepath.Join(g.projectName, cmdDir, g.projectName), @@ -509,6 +668,25 @@ func (g *Generator) generateDirStructure() error { return nil } +// Creates all the necesary directories for the generated files for Ansible Operator +func (g *Generator) generateAnsibleDirStructure() error { + dirsToCreate := []string{ + g.projectName, + filepath.Join(g.projectName, deployDir), + filepath.Join(g.projectName, initDir), + filepath.Join(g.projectName, buildDir), + filepath.Join(g.projectName, rolesDir), + } + + for _, dir := range dirsToCreate { + if err := os.MkdirAll(dir, defaultDirFileMode); err != nil { + return err + } + } + + return nil +} + // Renders a file given a template, and fills in template fields according to values passed in the tmplData struct func renderFile(w io.Writer, fileLoc string, fileTmpl string, info tmplData) error { t := template.New(fileLoc) @@ -566,3 +744,10 @@ func renderWriteFile(filePath string, fileLoc string, fileTmpl string, info tmpl return nil } + +func isGoOperator(operatorType string) bool { + if operatorType == "go" { + return true + } + return false +} diff --git a/pkg/generator/generator_test.go b/pkg/generator/generator_test.go index 6032ab2935..b5a619ff06 100644 --- a/pkg/generator/generator_test.go +++ b/pkg/generator/generator_test.go @@ -218,6 +218,36 @@ spec: value: "app-operator" ` +const ansibleOperatorYamlExp = `apiVersion: apps/v1 +kind: Deployment +metadata: + name: app-operator +spec: + replicas: 1 + selector: + matchLabels: + name: app-operator + template: + metadata: + labels: + name: app-operator + spec: + containers: + - name: app-operator + image: quay.io/example-inc/app-operator:0.0.1 + ports: + - containerPort: 60000 + name: metrics + imagePullPolicy: Always + env: + - name: WATCH_NAMESPACE + valueFrom: + fieldRef: + fieldPath: metadata.namespace + - name: OPERATOR_NAME + value: "app-operator" +` + const rbacYamlExp = `kind: Role apiVersion: rbac.authorization.k8s.io/v1beta1 metadata: @@ -297,6 +327,7 @@ func TestGenDeploy(t *testing.T) { MetricsPort: k8sutil.PrometheusMetricsPort, MetricsPortName: k8sutil.PrometheusMetricsPortName, OperatorNameEnv: k8sutil.OperatorNameEnvVar, + IsGoOperator: true, } if err := renderFile(buf, operatorTmplName, operatorYamlTmpl, td); err != nil { t.Error(err) @@ -307,6 +338,25 @@ func TestGenDeploy(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)) } + // Test Ansible Operator + buf = &bytes.Buffer{} + td = tmplData{ + ProjectName: appProjectName, + Image: appImage, + MetricsPort: k8sutil.PrometheusMetricsPort, + MetricsPortName: k8sutil.PrometheusMetricsPortName, + OperatorNameEnv: k8sutil.OperatorNameEnvVar, + IsGoOperator: false, + } + if err := renderFile(buf, operatorTmplName, operatorYamlTmpl, td); err != nil { + t.Error(err) + } + if ansibleOperatorYamlExp != buf.String() { + dmp := diffmatchpatch.New() + diffs := dmp.DiffMain(ansibleOperatorYamlExp, 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{} if err := renderFile(buf, rbacTmplName, rbacYamlTmpl, tmplData{ProjectName: appProjectName, GroupName: appGroupName}); err != nil { t.Error(err) @@ -528,6 +578,19 @@ USER app-operator ADD tmp/_output/bin/app-operator /usr/local/bin/app-operator ` +const dockerFileAnsibleNoPlaybookExp = `FROM quay.io/water-hole/ansible-operator + +COPY roles/ ${HOME}/roles/ +COPY watches.yaml ${HOME}/watches.yaml +` + +const dockerFileAnsiblePlaybookExp = `FROM quay.io/water-hole/ansible-operator + +COPY roles/ ${HOME}/roles/ +COPY playbook.yaml ${HOME}/playbook.yaml +COPY watches.yaml ${HOME}/watches.yaml +` + func TestGenBuild(t *testing.T) { buf := &bytes.Buffer{} bTd := tmplData{ @@ -557,6 +620,38 @@ func TestGenBuild(t *testing.T) { diffs := dmp.DiffMain(dockerFileExp, 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)) } + + // Test Ansible Operator Dockerfile with no playbook + buf = &bytes.Buffer{} + dTd = tmplData{ + ProjectName: appProjectName, + GeneratePlaybook: false, + } + if err := renderFile(buf, "tmp/build/Dockerfile", dockerFileAnsibleTmpl, dTd); err != nil { + t.Error(err) + return + } + if dockerFileAnsibleNoPlaybookExp != buf.String() { + dmp := diffmatchpatch.New() + diffs := dmp.DiffMain(dockerFileAnsibleNoPlaybookExp, 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)) + } + + // Test Ansible Operator Dockerfile with playbook generation + buf = &bytes.Buffer{} + dTd = tmplData{ + ProjectName: appProjectName, + GeneratePlaybook: true, + } + if err := renderFile(buf, "tmp/build/Dockerfile", dockerFileAnsibleTmpl, dTd); err != nil { + t.Error(err) + return + } + if dockerFileAnsiblePlaybookExp != buf.String() { + dmp := diffmatchpatch.New() + diffs := dmp.DiffMain(dockerFileAnsiblePlaybookExp, 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)) + } } func TestWriteFileAndPrint(t *testing.T) { diff --git a/pkg/generator/templates.go b/pkg/generator/templates.go index 54f4c197d8..5f21c1f17e 100644 --- a/pkg/generator/templates.go +++ b/pkg/generator/templates.go @@ -463,9 +463,9 @@ spec: ports: - containerPort: {{.MetricsPort}} name: {{.MetricsPortName}} - command: +{{ if .IsGoOperator }} command: - {{.ProjectName}} - imagePullPolicy: Always +{{ end }} imagePullPolicy: Always env: - name: WATCH_NAMESPACE valueFrom: @@ -598,6 +598,41 @@ ADD $NAMESPACEDMAN /namespaced.yaml ADD tmp/build/go-test.sh /go-test.sh ` +// Ansible Operator files +const dockerFileAnsibleTmpl = `FROM quay.io/water-hole/ansible-operator + +COPY roles/ ${HOME}/roles/ +{{- if .GeneratePlaybook }} +COPY playbook.yaml ${HOME}/playbook.yaml{{ end }} +COPY watches.yaml ${HOME}/watches.yaml +` + +const watchesTmpl = `--- +- version: {{.Version}} + group: {{.GroupName}} + kind: {{.Kind}} +{{ if .GeneratePlaybook }} playbook: /opt/ansible/playbook.yaml{{ else }} role: {{.Kind}}{{ end }} +` + +const playbookTmpl = `- hosts: localhost + gather_facts: no + tasks: + - import_role: + name: "{{.Kind}}" +` + +const galaxyInitTmpl = `#!/usr/bin/env bash + +if ! which ansible-galaxy > /dev/null; then + echo "ansible needs to be installed" + exit 1 +fi + +echo "Initializing role skeleton..." +ansible-galaxy init --init-path={{.Name}}/roles/ {{.Kind}} +>>>>>>> Add --type flag to new command +` + // apiDocTmpl is the template for apis/../doc.go const apiDocTmpl = `// +k8s:deepcopy-gen=package // +groupName={{.GroupName}} From c2270343be203d78835e97790442a8e031c61577 Mon Sep 17 00:00:00 2001 From: John Kim Date: Thu, 20 Sep 2018 17:14:47 -0400 Subject: [PATCH 02/16] add generate crd CLI option --- commands/operator-sdk/cmd/generate.go | 1 + commands/operator-sdk/cmd/generate/crd.go | 68 +++++++++++++++++++++++ pkg/generator/generator.go | 12 ++++ 3 files changed, 81 insertions(+) create mode 100644 commands/operator-sdk/cmd/generate/crd.go diff --git a/commands/operator-sdk/cmd/generate.go b/commands/operator-sdk/cmd/generate.go index 73511baa1a..e164a919c3 100644 --- a/commands/operator-sdk/cmd/generate.go +++ b/commands/operator-sdk/cmd/generate.go @@ -29,5 +29,6 @@ func NewGenerateCmd() *cobra.Command { } cmd.AddCommand(generate.NewGenerateK8SCmd()) cmd.AddCommand(generate.NewGenerateOlmCatalogCmd()) + cmd.AddCommand(generate.NewGenerateCrdCmd()) return cmd } diff --git a/commands/operator-sdk/cmd/generate/crd.go b/commands/operator-sdk/cmd/generate/crd.go new file mode 100644 index 0000000000..70078e7232 --- /dev/null +++ b/commands/operator-sdk/cmd/generate/crd.go @@ -0,0 +1,68 @@ +// 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 generate + +import ( + "errors" + "fmt" + "os" + + cmdError "github.com/operator-framework/operator-sdk/commands/operator-sdk/error" + "github.com/operator-framework/operator-sdk/pkg/generator" + "github.com/spf13/cobra" +) + +var ( + apiVersion string + kind string +) + +func NewGenerateCrdCmd() *cobra.Command { + crdCmd := &cobra.Command{ + Use: "crd", + Short: "Generates a custom resource definition", + Long: `generates a custom resource definition (CRD) file for the . +`, + Run: crdFunc, + } + crdCmd.Flags().StringVar(&apiVersion, "api-version", "", "Kubernetes apiVersion and has a format of $GROUP_NAME/$VERSION (e.g app.example.com/v1alpha1)") + crdCmd.MarkFlagRequired("api-version") + crdCmd.Flags().StringVar(&kind, "kind", "", "Kubernetes CustomResourceDefintion kind. (e.g AppService)") + crdCmd.MarkFlagRequired("kind") + return crdCmd +} + +func crdFunc(cmd *cobra.Command, args []string) { + if len(args) != 0 { + cmdError.ExitWithError(cmdError.ExitBadArgs, errors.New("crd command doesn't accept any arguments.")) + } + verifyCrdFlags() + + fmt.Fprintln(os.Stdout, "Generating custom resource definition (CRD) file") + + // Generate CRD file + if err := generator.RenderDeployCrdFile(apiVersion, kind); err != nil { + cmdError.ExitWithError(cmdError.ExitError, fmt.Errorf("failed to generate CRD file: (%v)", err)) + } +} + +func verifyCrdFlags() { + if len(apiVersion) == 0 { + cmdError.ExitWithError(cmdError.ExitBadArgs, errors.New("--api-version must not have empty value")) + } + if len(kind) == 0 { + cmdError.ExitWithError(cmdError.ExitBadArgs, errors.New("--kind must not have empty value")) + } +} diff --git a/pkg/generator/generator.go b/pkg/generator/generator.go index 1606d2b96b..4adfef4df8 100644 --- a/pkg/generator/generator.go +++ b/pkg/generator/generator.go @@ -341,6 +341,18 @@ func renderRBAC(deployDir, projectName, groupName string) error { return renderWriteFile(filepath.Join(deployDir, rbacYaml), rbacTmplName, rbacYamlTmpl, td) } +func RenderDeployCrdFile(apiVersion, kind string) error { + crdTd := tmplData{ + Kind: kind, + KindSingular: strings.ToLower(kind), + KindPlural: toPlural(strings.ToLower(kind)), + GroupName: groupName(apiVersion), + Version: version(apiVersion), + } + crdFilePath := filepath.Join(deployDir, strings.ToLower(kind) + "_crd.yaml") + return renderWriteFile(crdFilePath, crdFilePath, crdYamlTmpl, crdTd) +} + func renderDeployFiles(deployDir, projectName, apiVersion, kind, operatorType string) error { rbacTd := tmplData{ ProjectName: projectName, From e99de5eac62bf37dd5d8f65b14b1391dd0f5d273 Mon Sep 17 00:00:00 2001 From: Dylan Murray Date: Fri, 21 Sep 2018 11:48:37 -0400 Subject: [PATCH 03/16] Small updates based on feedback --- commands/operator-sdk/cmd/new.go | 5 ++++- pkg/generator/generator.go | 5 ++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/commands/operator-sdk/cmd/new.go b/commands/operator-sdk/cmd/new.go index b20ee13160..24160374e1 100644 --- a/commands/operator-sdk/cmd/new.go +++ b/commands/operator-sdk/cmd/new.go @@ -55,7 +55,7 @@ generates a skeletal app-operator application in $GOPATH/src/github.com/example. newCmd.MarkFlagRequired("kind") newCmd.Flags().StringVar(&operatorType, "type", "go", "Type of operator to initialize (e.g \"ansible\")") newCmd.Flags().BoolVar(&skipGit, "skip-git-init", false, "Do not init the directory as a git repository") - newCmd.Flags().BoolVar(&generatePlaybook, "generate-playbook", false, "Generate a playbook skeleton in watches.yaml. (Only used for --type ansible)") + newCmd.Flags().BoolVar(&generatePlaybook, "generate-playbook", false, "Generate a playbook skeleton. (Only used for --type ansible)") return newCmd } @@ -147,6 +147,9 @@ func verifyFlags() { if operatorType != goOperatorType && operatorType != ansibleOperatorType { cmdError.ExitWithError(cmdError.ExitBadArgs, errors.New("--type can only be `go` or `ansible`")) } + if operatorType != ansibleOperatorType && generatePlaybook { + cmdError.ExitWithError(cmdError.ExitBadArgs, errors.New("--generate-playbook can only be used with --type `ansible`")) + } kindFirstLetter := string(kind[0]) if kindFirstLetter != strings.ToUpper(kindFirstLetter) { cmdError.ExitWithError(cmdError.ExitBadArgs, errors.New("--kind must start with an uppercase letter")) diff --git a/pkg/generator/generator.go b/pkg/generator/generator.go index 4adfef4df8..561bf315bc 100644 --- a/pkg/generator/generator.go +++ b/pkg/generator/generator.go @@ -16,7 +16,6 @@ package generator import ( "bytes" - "errors" "fmt" "io" "io/ioutil" @@ -168,7 +167,7 @@ func (g *Generator) Render() error { return err } default: - return errors.New(fmt.Sprintf("unexpected operator type [%v]", g.operatorType)) + return fmt.Errorf("unexpected operator type [%v]", g.operatorType) } return nil } @@ -349,7 +348,7 @@ func RenderDeployCrdFile(apiVersion, kind string) error { GroupName: groupName(apiVersion), Version: version(apiVersion), } - crdFilePath := filepath.Join(deployDir, strings.ToLower(kind) + "_crd.yaml") + crdFilePath := filepath.Join(deployDir, strings.ToLower(kind)+"_crd.yaml") return renderWriteFile(crdFilePath, crdFilePath, crdYamlTmpl, crdTd) } From 7cadceaa13207fc7e111b87fad748847f6efa0cb Mon Sep 17 00:00:00 2001 From: John Kim Date: Fri, 21 Sep 2018 16:46:48 -0400 Subject: [PATCH 04/16] verify deploy sub-directory existance and also generate cr.yaml --- commands/operator-sdk/cmd/generate/crd.go | 54 ++++++++++++++++++++--- pkg/generator/generator.go | 13 +++++- pkg/generator/generator_test.go | 1 + 3 files changed, 61 insertions(+), 7 deletions(-) diff --git a/commands/operator-sdk/cmd/generate/crd.go b/commands/operator-sdk/cmd/generate/crd.go index 70078e7232..fd5b3d4ae6 100644 --- a/commands/operator-sdk/cmd/generate/crd.go +++ b/commands/operator-sdk/cmd/generate/crd.go @@ -18,6 +18,8 @@ import ( "errors" "fmt" "os" + "path/filepath" + "strings" cmdError "github.com/operator-framework/operator-sdk/commands/operator-sdk/error" "github.com/operator-framework/operator-sdk/pkg/generator" @@ -29,11 +31,22 @@ var ( kind string ) +const ( + goDir = "GOPATH" + deployCrdDir = "deploy" +) + func NewGenerateCrdCmd() *cobra.Command { crdCmd := &cobra.Command{ Use: "crd", - Short: "Generates a custom resource definition", - Long: `generates a custom resource definition (CRD) file for the . + Short: "Generates a custom resource definition (CRD) and the custom resource (CR) files", + Long: `The operator-sdk generate command will create a custom resource definition (CRD) and the custom resource (CR) files for the specified api-version and kind. + +Generated CRD filename: /deploy/_crd.yaml +Generated CR filename: /deploy/_cr.yaml + + /deploy path must already exist + --api-version and --kind are required flags to generate the new operator application. `, Run: crdFunc, } @@ -49,12 +62,14 @@ func crdFunc(cmd *cobra.Command, args []string) { cmdError.ExitWithError(cmdError.ExitBadArgs, errors.New("crd command doesn't accept any arguments.")) } verifyCrdFlags() + verifyCrdDeployPath() fmt.Fprintln(os.Stdout, "Generating custom resource definition (CRD) file") - // Generate CRD file - if err := generator.RenderDeployCrdFile(apiVersion, kind); err != nil { - cmdError.ExitWithError(cmdError.ExitError, fmt.Errorf("failed to generate CRD file: (%v)", err)) + // generate CRD file + wd, _ := os.Getwd() + if err := generator.RenderDeployCrdFiles(filepath.Join(wd, deployCrdDir), apiVersion, kind); err != nil { + cmdError.ExitWithError(cmdError.ExitError, fmt.Errorf("failed to generate CRD and CR files: (%v)", err)) } } @@ -65,4 +80,33 @@ func verifyCrdFlags() { if len(kind) == 0 { cmdError.ExitWithError(cmdError.ExitBadArgs, errors.New("--kind must not have empty value")) } + kindFirstLetter := string(kind[0]) + if kindFirstLetter != strings.ToUpper(kindFirstLetter) { + cmdError.ExitWithError(cmdError.ExitBadArgs, errors.New("--kind must start with an uppercase letter")) + } + if strings.Count(apiVersion, "/") != 1 { + cmdError.ExitWithError(cmdError.ExitBadArgs, fmt.Errorf("api-version has wrong format (%v); format must be $GROUP_NAME/$VERSION (e.g app.example.com/v1alpha1)", apiVersion)) + } +} + +// verifyCrdDeployPath checks if the path /deploy sub-directory is exists, and that is rooted under $GOPATH +func verifyCrdDeployPath() { + // check if $GOPATH env exists + gp := os.Getenv(goDir) + if len(gp) == 0 { + cmdError.ExitWithError(cmdError.ExitError, fmt.Errorf("$GOPATH env not set")) + } + 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)) + } + // check if this project's repository path is rooted under $GOPATH + if !strings.HasPrefix(wd, gp) { + cmdError.ExitWithError(cmdError.ExitError, fmt.Errorf("project's repository path (%v) is not rooted under GOPATH (%v)", wd, gp)) + } + // check if the deploy sub-directory exist + _, err = os.Stat(filepath.Join(wd, deployCrdDir)) + if err != nil { + cmdError.ExitWithError(cmdError.ExitError, fmt.Errorf("the path (./%v) does not exist. run this command in your project directory", deployCrdDir)) + } } diff --git a/pkg/generator/generator.go b/pkg/generator/generator.go index 561bf315bc..df5ddedfd2 100644 --- a/pkg/generator/generator.go +++ b/pkg/generator/generator.go @@ -340,7 +340,16 @@ func renderRBAC(deployDir, projectName, groupName string) error { return renderWriteFile(filepath.Join(deployDir, rbacYaml), rbacTmplName, rbacYamlTmpl, td) } -func RenderDeployCrdFile(apiVersion, kind string) error { +func RenderDeployCrdFiles(deployPath, apiVersion, kind string) error { + crTd := tmplData{ + APIVersion: apiVersion, + Kind: kind, + } + crFilePath := filepath.Join(deployPath, strings.ToLower(kind)+"_cr.yaml") + if err := renderWriteFile(crFilePath, crFilePath, crYamlTmpl, crTd); err != nil { + return err + } + crdTd := tmplData{ Kind: kind, KindSingular: strings.ToLower(kind), @@ -348,7 +357,7 @@ func RenderDeployCrdFile(apiVersion, kind string) error { GroupName: groupName(apiVersion), Version: version(apiVersion), } - crdFilePath := filepath.Join(deployDir, strings.ToLower(kind)+"_crd.yaml") + crdFilePath := filepath.Join(deployPath, strings.ToLower(kind)+"_crd.yaml") return renderWriteFile(crdFilePath, crdFilePath, crdYamlTmpl, crdTd) } diff --git a/pkg/generator/generator_test.go b/pkg/generator/generator_test.go index b5a619ff06..1167625ae1 100644 --- a/pkg/generator/generator_test.go +++ b/pkg/generator/generator_test.go @@ -232,6 +232,7 @@ spec: labels: name: app-operator spec: + serviceAccountName: app-operator containers: - name: app-operator image: quay.io/example-inc/app-operator:0.0.1 From 1df6e55b30cc0602fdce8b2ed7f42daae6ba2bf6 Mon Sep 17 00:00:00 2001 From: Dylan Murray Date: Tue, 25 Sep 2018 11:09:56 -0400 Subject: [PATCH 05/16] Fix role template path --- pkg/generator/templates.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/generator/templates.go b/pkg/generator/templates.go index 5f21c1f17e..36dc22ac50 100644 --- a/pkg/generator/templates.go +++ b/pkg/generator/templates.go @@ -611,7 +611,7 @@ const watchesTmpl = `--- - version: {{.Version}} group: {{.GroupName}} kind: {{.Kind}} -{{ if .GeneratePlaybook }} playbook: /opt/ansible/playbook.yaml{{ else }} role: {{.Kind}}{{ end }} +{{ if .GeneratePlaybook }} playbook: /opt/ansible/playbook.yaml{{ else }} role: /opt/ansible/roles/{{.Kind}}{{ end }} ` const playbookTmpl = `- hosts: localhost From 1f68aedd7de2fb7f43e2cef114d2b365e97f3707 Mon Sep 17 00:00:00 2001 From: John Kim Date: Wed, 26 Sep 2018 22:11:05 -0400 Subject: [PATCH 06/16] pass envs to build cmd --- commands/operator-sdk/cmd/build.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/commands/operator-sdk/cmd/build.go b/commands/operator-sdk/cmd/build.go index a2be512a08..799dd3e2ae 100644 --- a/commands/operator-sdk/cmd/build.go +++ b/commands/operator-sdk/cmd/build.go @@ -147,6 +147,8 @@ func buildFunc(cmd *cobra.Command, args []string) { // Don't need to buld go code if Ansible Operator if buildCmd() { 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))) From b10bc859e2bdb1d9d85dfe312a82ad26d110df21 Mon Sep 17 00:00:00 2001 From: Dylan Murray Date: Thu, 27 Sep 2018 10:17:53 -0400 Subject: [PATCH 07/16] Fix merge conflict in galaxy script --- pkg/generator/templates.go | 1 - 1 file changed, 1 deletion(-) diff --git a/pkg/generator/templates.go b/pkg/generator/templates.go index 36dc22ac50..61c72a02e3 100644 --- a/pkg/generator/templates.go +++ b/pkg/generator/templates.go @@ -630,7 +630,6 @@ fi echo "Initializing role skeleton..." ansible-galaxy init --init-path={{.Name}}/roles/ {{.Kind}} ->>>>>>> Add --type flag to new command ` // apiDocTmpl is the template for apis/../doc.go From 15fe320729039c867bc3cb58f7e34bbb3b58f1f4 Mon Sep 17 00:00:00 2001 From: Dylan Murray Date: Fri, 28 Sep 2018 16:53:26 -0400 Subject: [PATCH 08/16] Nit update --- commands/operator-sdk/cmd/generate/crd.go | 1 + 1 file changed, 1 insertion(+) diff --git a/commands/operator-sdk/cmd/generate/crd.go b/commands/operator-sdk/cmd/generate/crd.go index fd5b3d4ae6..84b39a11cc 100644 --- a/commands/operator-sdk/cmd/generate/crd.go +++ b/commands/operator-sdk/cmd/generate/crd.go @@ -23,6 +23,7 @@ import ( cmdError "github.com/operator-framework/operator-sdk/commands/operator-sdk/error" "github.com/operator-framework/operator-sdk/pkg/generator" + "github.com/spf13/cobra" ) From 6ba4d7e11a67a293d33c519d1ee978a170b92e05 Mon Sep 17 00:00:00 2001 From: Dylan Murray Date: Tue, 2 Oct 2018 08:47:05 -0400 Subject: [PATCH 09/16] Nit updates based on feedback --- commands/operator-sdk/cmd/build.go | 4 ++-- commands/operator-sdk/cmd/generate/crd.go | 5 ++++- pkg/generator/generator.go | 3 ++- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/commands/operator-sdk/cmd/build.go b/commands/operator-sdk/cmd/build.go index 799dd3e2ae..11269d671f 100644 --- a/commands/operator-sdk/cmd/build.go +++ b/commands/operator-sdk/cmd/build.go @@ -145,7 +145,7 @@ func buildFunc(cmd *cobra.Command, args []string) { } // Don't need to buld go code if Ansible Operator - if buildCmd() { + if mainExists() { 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)) @@ -184,7 +184,7 @@ func buildFunc(cmd *cobra.Command, args []string) { } } -func buildCmd() bool { +func mainExists() bool { dir, err := os.Getwd() if err != nil { cmdError.ExitWithError(cmdError.ExitError, fmt.Errorf("failed to get current working dir: %v", err)) diff --git a/commands/operator-sdk/cmd/generate/crd.go b/commands/operator-sdk/cmd/generate/crd.go index 84b39a11cc..5f75419ba2 100644 --- a/commands/operator-sdk/cmd/generate/crd.go +++ b/commands/operator-sdk/cmd/generate/crd.go @@ -68,7 +68,10 @@ func crdFunc(cmd *cobra.Command, args []string) { fmt.Fprintln(os.Stdout, "Generating custom resource definition (CRD) file") // generate CRD file - wd, _ := os.Getwd() + wd, err := os.Getwd() + if err != nil { + cmdError.ExitWithError(cmdError.ExitError, err) + } if err := generator.RenderDeployCrdFiles(filepath.Join(wd, deployCrdDir), apiVersion, kind); err != nil { cmdError.ExitWithError(cmdError.ExitError, fmt.Errorf("failed to generate CRD and CR files: (%v)", err)) } diff --git a/pkg/generator/generator.go b/pkg/generator/generator.go index df5ddedfd2..fccd4ff798 100644 --- a/pkg/generator/generator.go +++ b/pkg/generator/generator.go @@ -194,8 +194,9 @@ func (g *Generator) renderAnsibleOperator() error { func (g *Generator) renderRole() error { fmt.Printf("Rendering Ansible Galaxy role [%v/%v/%v]...\n", g.projectName, rolesDir, g.kind) agcmd := exec.Command(filepath.Join(g.projectName, galaxyInitCmd)) - _, err := agcmd.CombinedOutput() + output, err := agcmd.CombinedOutput() if err != nil { + fmt.Printf("Rendering Ansible Galaxy role failed: [%v], Output: %s", err.Error(), string(output)) return err } // Clean up tmp/init From 249d7b120d362717b977fed92eae2415f67d5776 Mon Sep 17 00:00:00 2001 From: Shawn Hurley Date: Thu, 27 Sep 2018 08:05:16 -0400 Subject: [PATCH 10/16] pkg/ansible: Adding ansible operator controller and events package (#473) --- Gopkg.lock | 133 +++++++++++++++++++- pkg/ansible/controller/controller.go | 91 ++++++++++++++ pkg/ansible/controller/reconcile.go | 173 +++++++++++++++++++++++++++ pkg/ansible/controller/source.go | 78 ++++++++++++ pkg/ansible/controller/types.go | 125 +++++++++++++++++++ pkg/ansible/events/log_events.go | 73 +++++++++++ 6 files changed, 667 insertions(+), 6 deletions(-) create mode 100644 pkg/ansible/controller/controller.go create mode 100644 pkg/ansible/controller/reconcile.go create mode 100644 pkg/ansible/controller/source.go create mode 100644 pkg/ansible/controller/types.go create mode 100644 pkg/ansible/events/log_events.go diff --git a/Gopkg.lock b/Gopkg.lock index f2ba034310..bf1abbdbd4 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -25,6 +25,22 @@ revision = "0ca9ea5df5451ffdf184b4428c902747c2c11cd7" version = "v1.0.0" +[[projects]] + branch = "master" + digest = "1:65587005c6fa4293c0b8a2e457e689df7fda48cc5e1f5449ea2c1e7784551558" + name = "github.com/go-logr/logr" + packages = ["."] + pruneopts = "" + revision = "9fb12b3b21c5415d16ac18dc5cd42c1cfdd40c4e" + +[[projects]] + branch = "master" + digest = "1:ce43ad4015e7cdad3f0e8f2c8339439dd4470859a828d2a6988b0f713699e94a" + name = "github.com/go-logr/zapr" + packages = ["."] + pruneopts = "" + revision = "7536572e8d55209135cd5e7ccf7fce43dca217ab" + [[projects]] digest = "1:6e73003ecd35f4487a5e88270d3ca0a81bc80dc88053ac7e4dcfec5fba30d918" name = "github.com/gogo/protobuf" @@ -44,6 +60,14 @@ pruneopts = "" revision = "23def4e6c14b4da8ac2ed8007337bc5eb5007998" +[[projects]] + branch = "master" + digest = "1:9854532d7b2fee9414d4fcd8d8bccd6b1c1e1663d8ec0337af63a19aaf4a778e" + name = "github.com/golang/groupcache" + packages = ["lru"] + pruneopts = "" + revision = "6f2cf27854a4a29e3811b0371547be335d411b8b" + [[projects]] digest = "1:3dd078fda7500c341bc26cfbc6c6a34614f295a2457149fc1045cab767cbcf18" name = "github.com/golang/protobuf" @@ -74,6 +98,14 @@ pruneopts = "" revision = "24818f796faf91cd76ec7bddd72458fbced7a6c1" +[[projects]] + digest = "1:5247b135b5492aa232a731acdcb52b08f32b874cb398f21ab460396eadbe866b" + name = "github.com/google/uuid" + packages = ["."] + pruneopts = "" + revision = "d460ce9f8df2e77fb1ba55ca87fafed96c607494" + version = "v1.0.0" + [[projects]] digest = "1:16b2837c8b3cf045fa2cdc82af0cf78b19582701394484ae76b2c3bc3c99ad73" name = "github.com/googleapis/gnostic" @@ -132,6 +164,14 @@ revision = "1624edc4454b8682399def8740d46db5e4362ba4" version = "v1.1.5" +[[projects]] + branch = "master" + digest = "1:58050e2bc9621cc6b68c1da3e4a0d1c40ad1f89062b9855c26521fd42a97a106" + name = "github.com/mattbaird/jsonpatch" + packages = ["."] + pruneopts = "" + revision = "81af80346b1a01caae0cbc27fd3c1ba5b11e189f" + [[projects]] digest = "1:63722a4b1e1717be7b98fc686e0b30d5e7f734b9e93d7dee86293b6deab7ea28" name = "github.com/matttproud/golang_protobuf_extensions" @@ -164,6 +204,14 @@ pruneopts = "" revision = "cca7078d478f8520f85629ad7c68962d31ed7682" +[[projects]] + digest = "1:a5484d4fa43127138ae6e7b2299a6a52ae006c7f803d98d717f60abf3e97192e" + name = "github.com/pborman/uuid" + packages = ["."] + pruneopts = "" + revision = "adf5a7427709b9deb95d29d3fa8a2bf9cfd388f1" + version = "v1.2" + [[projects]] branch = "master" digest = "1:c24598ffeadd2762552269271b3b1510df2d83ee6696c1e543a0ff653af494bc" @@ -213,7 +261,7 @@ [[projects]] branch = "master" - digest = "1:e04aaa0e8f8da0ed3d6c0700bd77eda52a47f38510063209d72d62f0ef807d5e" + digest = "1:5a57ea878c9a40657ebe7916eca6bea7c76808f5acb68fd42ea5e204dd35f6f7" name = "github.com/prometheus/procfs" packages = [ ".", @@ -222,7 +270,7 @@ "xfs", ] pruneopts = "" - revision = "05ee40e3a273f7245e8777337fc7b46e533a9a92" + revision = "418d78d0b9a7b7de3a6bbc8a23def624cc977bb2" [[projects]] digest = "1:3962f553b77bf6c03fc07cd687a22dd3b00fe11aa14d31194f5505f5bb65cdc8" @@ -256,6 +304,37 @@ revision = "9a97c102cda95a86cec2345a6f09f55a939babf5" version = "v1.0.2" +[[projects]] + digest = "1:74f86c458e82e1c4efbab95233e0cf51b7cc02dc03193be9f62cd81224e10401" + name = "go.uber.org/atomic" + packages = ["."] + pruneopts = "" + revision = "1ea20fb1cbb1cc08cbd0d913a96dead89aa18289" + version = "v1.3.2" + +[[projects]] + digest = "1:22c7effcb4da0eacb2bb1940ee173fac010e9ef3c691f5de4b524d538bd980f5" + name = "go.uber.org/multierr" + packages = ["."] + pruneopts = "" + revision = "3c4937480c32f4c13a875a1829af76c98ca3d40a" + version = "v1.1.0" + +[[projects]] + digest = "1:246f378f80fba6fcf0f191c486b6613265abd2bc0f2fa55a36b928c67352021e" + name = "go.uber.org/zap" + packages = [ + ".", + "buffer", + "internal/bufferpool", + "internal/color", + "internal/exit", + "zapcore", + ] + pruneopts = "" + revision = "ff33455a0e382e8a81d14dd7c922020b6b5e7982" + version = "v1.9.1" + [[projects]] branch = "master" digest = "1:61a86f0be8b466d6e3fbdabb155aaa4006137cb5e3fd3b949329d103fa0ceb0f" @@ -266,7 +345,7 @@ [[projects]] branch = "master" - digest = "1:fbdbb6cf8db3278412c9425ad78b26bb8eb788181f26a3ffb3e4f216b314f86a" + digest = "1:6846191f608c0dd6109f37ca46b784f9630191ff13f86ae974135c05a4c92971" name = "golang.org/x/net" packages = [ "context", @@ -278,18 +357,18 @@ "idna", ] pruneopts = "" - revision = "26e67e76b6c3f6ce91f7c52def5af501b4e0f3a2" + revision = "f04abc6bdfa7a0171a8a0c9fd2ada9391044d056" [[projects]] branch = "master" - digest = "1:ed900376500543ca05f2a2383e1f541b4606f19cd22f34acb81b17a0b90c7f3e" + digest = "1:18ebd6e65d5223778b5fc92bbf2afffb54c6ac3889bbc362df692b965f932fae" name = "golang.org/x/sys" packages = [ "unix", "windows", ] pruneopts = "" - revision = "d0be0721c37eeb5299f245a996a483160fc36940" + revision = "b09afc3d579e346c4a7e4705953acaf6f9e551bd" [[projects]] digest = "1:5acd3512b047305d49e8763eef7ba423901e85d5dd2fd1e71778a0ea8de10bd4" @@ -342,6 +421,7 @@ digest = "1:2fe7efa9ea3052443378383d27c15ba088d03babe69a89815ce7fe9ec1d9aeb4" name = "k8s.io/api" packages = [ + "admission/v1beta1", "admissionregistration/v1alpha1", "admissionregistration/v1beta1", "apps/v1", @@ -423,16 +503,20 @@ "pkg/util/httpstream", "pkg/util/intstr", "pkg/util/json", + "pkg/util/mergepatch", "pkg/util/net", "pkg/util/proxy", "pkg/util/runtime", "pkg/util/sets", + "pkg/util/strategicpatch", + "pkg/util/uuid", "pkg/util/validation", "pkg/util/validation/field", "pkg/util/wait", "pkg/util/yaml", "pkg/version", "pkg/watch", + "third_party/forked/golang/json", "third_party/forked/golang/netutil", "third_party/forked/golang/reflect", ] @@ -492,8 +576,11 @@ "tools/clientcmd/api", "tools/clientcmd/api/latest", "tools/clientcmd/api/v1", + "tools/leaderelection", + "tools/leaderelection/resourcelock", "tools/metrics", "tools/pager", + "tools/record", "tools/reference", "transport", "util/buffer", @@ -509,12 +596,40 @@ revision = "1f13a808da65775f22cbf47862c4e5898d8f4ca1" version = "kubernetes-1.11.2" +[[projects]] + branch = "master" + digest = "1:951bc2047eea6d316a17850244274554f26fd59189360e45f4056b424dadf2c1" + name = "k8s.io/kube-openapi" + packages = ["pkg/util/proto"] + pruneopts = "" + revision = "e3762e86a74c878ffed47484592986685639c2cd" + [[projects]] digest = "1:207e862b55f9399362c667ce30f1ac8b2180d282bc7bb5749fc81122944aa2b5" name = "sigs.k8s.io/controller-runtime" packages = [ + "pkg/cache", + "pkg/cache/internal", "pkg/client", "pkg/client/apiutil", + "pkg/controller", + "pkg/event", + "pkg/handler", + "pkg/internal/controller", + "pkg/internal/recorder", + "pkg/leaderelection", + "pkg/manager", + "pkg/patch", + "pkg/predicate", + "pkg/reconcile", + "pkg/recorder", + "pkg/runtime/inject", + "pkg/runtime/log", + "pkg/source", + "pkg/source/internal", + "pkg/webhook/admission", + "pkg/webhook/admission/types", + "pkg/webhook/types", ] pruneopts = "" revision = "79f06014d49b565e2d980fcd9519b33874ddb8d6" @@ -558,6 +673,12 @@ "k8s.io/client-go/transport", "k8s.io/client-go/util/workqueue", "sigs.k8s.io/controller-runtime/pkg/client", + "sigs.k8s.io/controller-runtime/pkg/controller", + "sigs.k8s.io/controller-runtime/pkg/event", + "sigs.k8s.io/controller-runtime/pkg/handler", + "sigs.k8s.io/controller-runtime/pkg/manager", + "sigs.k8s.io/controller-runtime/pkg/reconcile", + "sigs.k8s.io/controller-runtime/pkg/source", ] solver-name = "gps-cdcl" solver-version = 1 diff --git a/pkg/ansible/controller/controller.go b/pkg/ansible/controller/controller.go new file mode 100644 index 0000000000..51b1b14eff --- /dev/null +++ b/pkg/ansible/controller/controller.go @@ -0,0 +1,91 @@ +// 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 controller + +import ( + "fmt" + "log" + "strings" + "time" + + "github.com/operator-framework/operator-sdk/pkg/ansible/events" + "github.com/operator-framework/operator-sdk/pkg/ansible/runner" + + "github.com/sirupsen/logrus" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime/schema" + "sigs.k8s.io/controller-runtime/pkg/controller" + crthandler "sigs.k8s.io/controller-runtime/pkg/handler" + "sigs.k8s.io/controller-runtime/pkg/manager" + "sigs.k8s.io/controller-runtime/pkg/source" +) + +// Options - options for your controller +type Options struct { + EventHandlers []events.EventHandler + LoggingLevel events.LogLevel + Runner runner.Runner + Namespace string + GVK schema.GroupVersionKind + // StopChannel is used to deal with the bug: + // https://github.com/kubernetes-sigs/controller-runtime/issues/103 + StopChannel <-chan struct{} +} + +// Add - Creates a new ansible operator controller and adds it to the manager +func Add(mgr manager.Manager, options Options) { + logrus.Infof("Watching %s/%v, %s, %s", options.GVK.Group, options.GVK.Version, options.GVK.Kind, options.Namespace) + if options.EventHandlers == nil { + options.EventHandlers = []events.EventHandler{} + } + eventHandlers := append(options.EventHandlers, events.NewLoggingEventHandler(options.LoggingLevel)) + + aor := &AnsibleOperatorReconciler{ + Client: mgr.GetClient(), + GVK: options.GVK, + Runner: options.Runner, + EventHandlers: eventHandlers, + } + + // Register the GVK with the schema + mgr.GetScheme().AddKnownTypeWithName(options.GVK, &unstructured.Unstructured{}) + metav1.AddToGroupVersion(mgr.GetScheme(), schema.GroupVersion{ + Group: options.GVK.Group, + Version: options.GVK.Version, + }) + + //Create new controller runtime controller and set the controller to watch GVK. + c, err := controller.New(fmt.Sprintf("%v-controller", strings.ToLower(options.GVK.Kind)), mgr, controller.Options{ + Reconciler: aor, + }) + if err != nil { + log.Fatal(err) + } + u := &unstructured.Unstructured{} + u.SetGroupVersionKind(options.GVK) + if err := c.Watch(&source.Kind{Type: u}, &crthandler.EnqueueRequestForObject{}); err != nil { + log.Fatal(err) + } + + r := NewReconcileLoop(time.Minute*1, options.GVK, mgr.GetClient()) + r.Stop = options.StopChannel + cs := &source.Channel{Source: r.Source} + cs.InjectStopChannel(options.StopChannel) + if err := c.Watch(cs, &crthandler.EnqueueRequestForObject{}); err != nil { + log.Fatal(err) + } + r.Start() +} diff --git a/pkg/ansible/controller/reconcile.go b/pkg/ansible/controller/reconcile.go new file mode 100644 index 0000000000..0db01ab103 --- /dev/null +++ b/pkg/ansible/controller/reconcile.go @@ -0,0 +1,173 @@ +// 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 controller + +import ( + "context" + "encoding/json" + "errors" + "os" + + "github.com/operator-framework/operator-sdk/pkg/ansible/events" + "github.com/operator-framework/operator-sdk/pkg/ansible/proxy/kubeconfig" + "github.com/operator-framework/operator-sdk/pkg/ansible/runner" + "github.com/operator-framework/operator-sdk/pkg/ansible/runner/eventapi" + + "github.com/sirupsen/logrus" + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime/schema" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/reconcile" +) + +// AnsibleOperatorReconciler - object to reconcile runner requests +type AnsibleOperatorReconciler struct { + GVK schema.GroupVersionKind + Runner runner.Runner + Client client.Client + EventHandlers []events.EventHandler +} + +// Reconcile - handle the event. +func (r *AnsibleOperatorReconciler) Reconcile(request reconcile.Request) (reconcile.Result, error) { + u := &unstructured.Unstructured{} + u.SetGroupVersionKind(r.GVK) + err := r.Client.Get(context.TODO(), request.NamespacedName, u) + if apierrors.IsNotFound(err) { + return reconcile.Result{}, nil + } + if err != nil { + return reconcile.Result{}, err + } + + deleted := u.GetDeletionTimestamp() != nil + finalizer, finalizerExists := r.Runner.GetFinalizer() + pendingFinalizers := u.GetFinalizers() + // If the resource is being deleted we don't want to add the finalizer again + if finalizerExists && !deleted && !contains(pendingFinalizers, finalizer) { + logrus.Debugf("Adding finalizer %s to resource", finalizer) + finalizers := append(pendingFinalizers, finalizer) + u.SetFinalizers(finalizers) + err := r.Client.Update(context.TODO(), u) + return reconcile.Result{}, err + } + if !contains(pendingFinalizers, finalizer) && deleted { + logrus.Info("Resource is terminated, skipping reconcilation") + return reconcile.Result{}, nil + } + + s := u.Object["spec"] + _, ok := s.(map[string]interface{}) + if !ok { + logrus.Warnf("spec was not found") + u.Object["spec"] = map[string]interface{}{} + r.Client.Update(context.TODO(), u) + return reconcile.Result{Requeue: true}, nil + } + ownerRef := metav1.OwnerReference{ + APIVersion: u.GetAPIVersion(), + Kind: u.GetKind(), + Name: u.GetName(), + UID: u.GetUID(), + } + + kc, err := kubeconfig.Create(ownerRef, "http://localhost:8888", u.GetNamespace()) + if err != nil { + return reconcile.Result{}, err + } + defer os.Remove(kc.Name()) + eventChan, err := r.Runner.Run(u, kc.Name()) + if err != nil { + return reconcile.Result{}, err + } + + // iterate events from ansible, looking for the final one + statusEvent := eventapi.StatusJobEvent{} + for event := range eventChan { + for _, eHandler := range r.EventHandlers { + go eHandler.Handle(u, event) + } + if event.Event == "playbook_on_stats" { + // convert to StatusJobEvent; would love a better way to do this + data, err := json.Marshal(event) + if err != nil { + return reconcile.Result{}, err + } + err = json.Unmarshal(data, &statusEvent) + if err != nil { + return reconcile.Result{}, err + } + } + } + if statusEvent.Event == "" { + err := errors.New("did not receive playbook_on_stats event") + logrus.Error(err.Error()) + return reconcile.Result{}, err + } + + // We only want to update the CustomResource once, so we'll track changes and do it at the end + var needsUpdate bool + runSuccessful := true + for _, count := range statusEvent.EventData.Failures { + if count > 0 { + runSuccessful = false + break + } + } + // The finalizer has run successfully, time to remove it + if deleted && finalizerExists && runSuccessful { + finalizers := []string{} + for _, pendingFinalizer := range pendingFinalizers { + if pendingFinalizer != finalizer { + finalizers = append(finalizers, pendingFinalizer) + } + } + u.SetFinalizers(finalizers) + needsUpdate = true + } + + statusMap, ok := u.Object["status"].(map[string]interface{}) + if !ok { + u.Object["status"] = ResourceStatus{ + Status: NewStatusFromStatusJobEvent(statusEvent), + } + logrus.Infof("adding status for the first time") + needsUpdate = true + } else { + // Need to conver the map[string]interface into a resource status. + if update, status := UpdateResourceStatus(statusMap, statusEvent); update { + u.Object["status"] = status + needsUpdate = true + } + } + if needsUpdate { + err = r.Client.Update(context.TODO(), u) + } + if !runSuccessful { + return reconcile.Result{Requeue: true}, err + } + return reconcile.Result{}, err +} + +func contains(l []string, s string) bool { + for _, elem := range l { + if elem == s { + return true + } + } + return false +} diff --git a/pkg/ansible/controller/source.go b/pkg/ansible/controller/source.go new file mode 100644 index 0000000000..f8556d7d86 --- /dev/null +++ b/pkg/ansible/controller/source.go @@ -0,0 +1,78 @@ +// 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 controller + +import ( + "context" + "time" + + "github.com/sirupsen/logrus" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime/schema" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/event" +) + +// ReconcileLoop - new loop +type ReconcileLoop struct { + Source chan event.GenericEvent + Stop <-chan struct{} + GVK schema.GroupVersionKind + Interval time.Duration + Client client.Client +} + +// NewReconcileLoop - loop for a GVK. +// The reconcilation loop is needed because the resync period +// for the informer is not suitable for this use case. +func NewReconcileLoop(interval time.Duration, gvk schema.GroupVersionKind, c client.Client) ReconcileLoop { + s := make(chan event.GenericEvent, 1025) + return ReconcileLoop{ + Source: s, + GVK: gvk, + Interval: interval, + Client: c, + } +} + +// Start - start the reconcile loop +func (r *ReconcileLoop) Start() { + go func() { + ticker := time.NewTicker(r.Interval) + defer ticker.Stop() + for { + select { + case <-ticker.C: + // List all object for the GVK + ul := &unstructured.UnstructuredList{} + ul.SetGroupVersionKind(r.GVK) + err := r.Client.List(context.Background(), nil, ul) + if err != nil { + logrus.Warningf("unable to list resources for GV: %v during reconcilation", r.GVK) + continue + } + for _, u := range ul.Items { + e := event.GenericEvent{ + Meta: &u, + Object: &u, + } + r.Source <- e + } + case <-r.Stop: + return + } + } + }() +} diff --git a/pkg/ansible/controller/types.go b/pkg/ansible/controller/types.go new file mode 100644 index 0000000000..1dd148bbd2 --- /dev/null +++ b/pkg/ansible/controller/types.go @@ -0,0 +1,125 @@ +// 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 controller + +import ( + "github.com/operator-framework/operator-sdk/pkg/ansible/runner/eventapi" +) + +const ( + host = "localhost" +) + +type Status struct { + Ok int `json:"ok"` + Changed int `json:"changed"` + Skipped int `json:"skipped"` + Failures int `json:"failures"` + TimeOfCompletion eventapi.EventTime `json:"completion"` +} + +func NewStatusFromStatusJobEvent(je eventapi.StatusJobEvent) Status { + // ok events. + o := 0 + changed := 0 + skipped := 0 + failures := 0 + if v, ok := je.EventData.Changed[host]; ok { + changed = v + } + if v, ok := je.EventData.Ok[host]; ok { + o = v + } + if v, ok := je.EventData.Skipped[host]; ok { + skipped = v + } + if v, ok := je.EventData.Failures[host]; ok { + failures = v + } + return Status{ + Ok: o, + Changed: changed, + Skipped: skipped, + Failures: failures, + TimeOfCompletion: je.Created, + } +} + +func IsStatusEqual(s1, s2 Status) bool { + return (s1.Ok == s2.Ok && s1.Changed == s2.Changed && s1.Skipped == s2.Skipped && s1.Failures == s2.Failures) +} + +func NewStatusFromMap(sm map[string]interface{}) Status { + //Create Old top level status + // ok events. + o := 0 + changed := 0 + skipped := 0 + failures := 0 + e := eventapi.EventTime{} + if v, ok := sm["changed"]; ok { + changed = int(v.(int64)) + } + if v, ok := sm["ok"]; ok { + o = int(v.(int64)) + } + if v, ok := sm["skipped"]; ok { + skipped = int(v.(int64)) + } + if v, ok := sm["failures"]; ok { + failures = int(v.(int64)) + } + if v, ok := sm["completion"]; ok { + s := v.(string) + e.UnmarshalJSON([]byte(s)) + } + return Status{ + Ok: o, + Changed: changed, + Skipped: skipped, + Failures: failures, + TimeOfCompletion: e, + } +} + +type ResourceStatus struct { + Status `json:",inline"` + FailureMessage string `json:"reason,omitempty"` + History []Status `json:"history,omitempty"` +} + +func UpdateResourceStatus(sm map[string]interface{}, je eventapi.StatusJobEvent) (bool, ResourceStatus) { + newStatus := NewStatusFromStatusJobEvent(je) + oldStatus := NewStatusFromMap(sm) + // Don't update the status if new status and old status are equal. + if IsStatusEqual(newStatus, oldStatus) { + return false, ResourceStatus{} + } + + history := []Status{} + h, ok := sm["history"] + if ok { + hi := h.([]interface{}) + for _, m := range hi { + ma := m.(map[string]interface{}) + history = append(history, NewStatusFromMap(ma)) + } + } + history = append(history, oldStatus) + return true, ResourceStatus{ + Status: newStatus, + History: history, + } +} diff --git a/pkg/ansible/events/log_events.go b/pkg/ansible/events/log_events.go new file mode 100644 index 0000000000..37422c106b --- /dev/null +++ b/pkg/ansible/events/log_events.go @@ -0,0 +1,73 @@ +// 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 events + +import ( + "github.com/operator-framework/operator-sdk/pkg/ansible/runner/eventapi" + "github.com/sirupsen/logrus" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" +) + +// LogLevel - Levelt for the logging to take place. +type LogLevel int + +const ( + // Tasks - only log the high level tasks. + Tasks LogLevel = iota + + // Everything - log every event. + Everything + + // Nothing - this will log nothing. + Nothing +) + +// EventHandler - knows how to handle job events. +type EventHandler interface { + Handle(*unstructured.Unstructured, eventapi.JobEvent) +} + +type loggingEventHandler struct { + LogLevel LogLevel +} + +func (l loggingEventHandler) Handle(u *unstructured.Unstructured, e eventapi.JobEvent) { + log := logrus.WithFields(logrus.Fields{ + "component": "logging_event_handler", + "name": u.GetName(), + "namespace": u.GetNamespace(), + "gvk": u.GroupVersionKind().String(), + "event_type": e.Event, + }) + t, ok := e.EventData["task"] + if ok { + log = log.WithField("task", t) + } + switch l.LogLevel { + case Everything: + log.Infof("event: %#v", e.EventData) + case Tasks: + if ok { + log.Infof("event: %#v", e.EventData) + } + } +} + +// NewLoggingEventHandler - Creates a Logging Event Handler to log events. +func NewLoggingEventHandler(l LogLevel) EventHandler { + return loggingEventHandler{ + LogLevel: l, + } +} From 02f115548745bb4808822aa948a627414259c781 Mon Sep 17 00:00:00 2001 From: John Kim Date: Thu, 27 Sep 2018 16:20:33 -0400 Subject: [PATCH 11/16] make ansible task log outputs more readable (#545) --- pkg/ansible/events/log_events.go | 38 ++++++++++++++++++++++++++------ 1 file changed, 31 insertions(+), 7 deletions(-) diff --git a/pkg/ansible/events/log_events.go b/pkg/ansible/events/log_events.go index 37422c106b..629dbb0b29 100644 --- a/pkg/ansible/events/log_events.go +++ b/pkg/ansible/events/log_events.go @@ -32,6 +32,15 @@ const ( // Nothing - this will log nothing. Nothing + + // Ansible Events + EventPlaybookOnTaskStart = "playbook_on_task_start" + EventRunnerOnOk = "runner_on_ok" + EventRunnerOnFailed = "runner_on_failed" + + // Ansible Task Actions + TaskActionSetFact = "set_fact" + TaskActionDebug = "debug" ) // EventHandler - knows how to handle job events. @@ -51,17 +60,32 @@ func (l loggingEventHandler) Handle(u *unstructured.Unstructured, e eventapi.Job "gvk": u.GroupVersionKind().String(), "event_type": e.Event, }) + if l.LogLevel == Nothing { + return + } + // log only the following for the 'Tasks' LogLevel t, ok := e.EventData["task"] if ok { - log = log.WithField("task", t) + setFactAction := e.EventData["task_action"] == TaskActionSetFact + debugAction := e.EventData["task_action"] == TaskActionDebug + + if e.Event == EventPlaybookOnTaskStart && !setFactAction && !debugAction { + log.Infof("[playbook task]: %s", e.EventData["name"]) + return + } + if e.Event == EventRunnerOnOk && debugAction { + log.Infof("[playbook debug]: %v", e.EventData["task_args"]) + return + } + if e.Event == EventRunnerOnFailed { + log.Errorf("[failed]: [playbook task] '%s' failed with task_args - %v", + t, e.EventData["task_args"]) + return + } } - switch l.LogLevel { - case Everything: + // log everything else for the 'Everything' LogLevel + if l.LogLevel == Everything { log.Infof("event: %#v", e.EventData) - case Tasks: - if ok { - log.Infof("event: %#v", e.EventData) - } } } From 20c47916d04c4dfcaca1230d9a3afe88ba16adeb Mon Sep 17 00:00:00 2001 From: Alex Pavel Date: Fri, 28 Sep 2018 16:42:14 -0700 Subject: [PATCH 12/16] commands,pkg/test: support single namespace mode for local test (#546) * commands,pkg/test: support single namespace mode for local test This commit brings support for running the tests in a single namespace when using `test local`, which was previously only used by `test cluster` (where it was a requirement to properly function) * doc: update docs with new test local --namespace flag * pkg/test/resource_creator.go: check namespace env value * pkg/test: change handling of TestNamespaceEnv --- .travis.yml | 4 ++++ commands/operator-sdk/cmd/test/local.go | 12 +++++++++++- doc/sdk-cli-reference.md | 1 + doc/test-framework/writing-e2e-tests.md | 8 ++++++++ hack/ci/setup-openshift.sh | 3 +++ pkg/test/framework.go | 22 +++++++++++++++------- pkg/test/main_entry.go | 6 ++++-- pkg/test/resource_creator.go | 5 ++--- 8 files changed, 48 insertions(+), 13 deletions(-) diff --git a/.travis.yml b/.travis.yml index dcf6ba8e4e..7ebe32c2b0 100644 --- a/.travis.yml +++ b/.travis.yml @@ -33,6 +33,10 @@ script: - operator-sdk test local . # test operator-sdk test flags - operator-sdk test local . --global-manifest deploy/crd.yaml --namespaced-manifest deploy/namespace-init.yaml --go-test-flags "-parallel 1" --kubeconfig $HOME/.kube/config +# test operator-sdk test local single namespace mode +- kubectl create namespace test-memcached +- operator-sdk test local . --namespace=test-memcached +- kubectl delete namespace test-memcached # go back to project root - cd ../.. - go vet ./... diff --git a/commands/operator-sdk/cmd/test/local.go b/commands/operator-sdk/cmd/test/local.go index 14191ba2f4..45b61f084f 100644 --- a/commands/operator-sdk/cmd/test/local.go +++ b/commands/operator-sdk/cmd/test/local.go @@ -33,6 +33,7 @@ type testLocalConfig struct { globalManPath string namespacedManPath string goTestFlags string + namespace string } var tlConfig testLocalConfig @@ -52,6 +53,7 @@ func NewTestLocalCmd() *cobra.Command { testCmd.Flags().StringVar(&tlConfig.globalManPath, "global-manifest", "deploy/crd.yaml", "Path to manifest for Global resources (e.g. CRD manifest)") testCmd.Flags().StringVar(&tlConfig.namespacedManPath, "namespaced-manifest", "", "Path to manifest for per-test, namespaced resources (e.g. RBAC and Operator manifest)") testCmd.Flags().StringVar(&tlConfig.goTestFlags, "go-test-flags", "", "Additional flags to pass to go test") + testCmd.Flags().StringVar(&tlConfig.namespace, "namespace", "", "If non-empty, single namespace to run tests in") return testCmd } @@ -96,8 +98,16 @@ func testLocalFunc(cmd *cobra.Command, args []string) { 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, " ")...) + // if we do the append using an empty go flags, it inserts an empty arg, which causes + // any later flags to be ignored + if tlConfig.goTestFlags != "" { + testArgs = append(testArgs, strings.Split(tlConfig.goTestFlags, " ")...) + } + if tlConfig.namespace != "" { + testArgs = append(testArgs, "-"+test.SingleNamespaceFlag, "-parallel=1") + } dc := exec.Command("go", testArgs...) + dc.Env = append(os.Environ(), fmt.Sprintf("%v=%v", test.TestNamespaceEnv, tlConfig.namespace)) dc.Dir = mustGetwd() dc.Stdout = os.Stdout dc.Stderr = os.Stderr diff --git a/doc/sdk-cli-reference.md b/doc/sdk-cli-reference.md index f61b4f87b6..6ef5eac68d 100644 --- a/doc/sdk-cli-reference.md +++ b/doc/sdk-cli-reference.md @@ -175,6 +175,7 @@ Runs the tests locally * `--kubeconfig` string - location of kubeconfig for kubernetes cluster (default "~/.kube/config") * `--global-manifest` string - path to manifest for global resources (default "deploy/crd.yaml) * `--namespaced-manifest` string - path to manifest for per-test, namespaced resources (default: combines deploy/sa.yaml, deploy/rbac.yaml, and deploy/operator.yaml) +* `--namespace` string - if non-empty, single namespace to run tests in (e.g. "operator-test") (default: "") * `--go-test-flags` string - extra arguments to pass to `go test` (e.g. -f "-v -parallel=2") * `-h, --help` - help for local diff --git a/doc/test-framework/writing-e2e-tests.md b/doc/test-framework/writing-e2e-tests.md index a08e4356c2..3c343adeda 100644 --- a/doc/test-framework/writing-e2e-tests.md +++ b/doc/test-framework/writing-e2e-tests.md @@ -207,6 +207,13 @@ as an argument. You can use `--help` to view the other configuration options and $ operator-sdk test local ./test/e2e --go-test-flags "-v -parallel=2" ``` +If you wish to run all the tests in 1 namespace (which also forces `-parallel=1`), you can use the `--namespace` flag: + +```shell +$ kubectl create namespace operator-test +$ operator-sdk test local ./test/e2e --namespace operator-test +``` + For more documentation on the `operator-sdk test local` command, see the [SDK CLI Reference][sdk-cli-ref] doc. For advanced use cases, it is possible to run the tests via `go test` directly. As long as all flags defined @@ -258,6 +265,7 @@ Test Successfully Completed ``` The `test cluster` command will deploy a test pod in the given namespace that will run the e2e tests packaged in the image. +The tests run sequentially in the namespace (`-parallel=1`), the same as running `operator-sdk test local --namespace `. The command will wait until the tests succeed (pod phase=`Succeeded`) or fail (pod phase=`Failed`). If the tests fail, the command will output the test pod logs which should be the standard go test error logs. diff --git a/hack/ci/setup-openshift.sh b/hack/ci/setup-openshift.sh index 39425c50d0..843b6a9b94 100755 --- a/hack/ci/setup-openshift.sh +++ b/hack/ci/setup-openshift.sh @@ -10,3 +10,6 @@ tar xvzOf oc.tar.gz openshift-origin-client-tools-v3.10.0-dd10d17-linux-64bit/oc oc cluster up # Become cluster admin oc login -u system:admin + +# kubectl is needed for the single namespace local test +curl -Lo kubectl https://storage.googleapis.com/kubernetes-release/release/v1.10.1/bin/linux/amd64/kubectl && chmod +x kubectl && sudo mv kubectl /usr/local/bin/ diff --git a/pkg/test/framework.go b/pkg/test/framework.go index 6e8000ff9c..937667ca30 100644 --- a/pkg/test/framework.go +++ b/pkg/test/framework.go @@ -52,13 +52,13 @@ type Framework struct { DynamicClient dynclient.Client DynamicDecoder runtime.Decoder NamespacedManPath *string - InCluster bool + SingleNamespace bool + Namespace string } -func setup(kubeconfigPath, namespacedManPath *string) error { +func setup(kubeconfigPath, namespacedManPath *string, singleNamespace *bool) error { 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 { @@ -72,7 +72,7 @@ func setup(kubeconfigPath, namespacedManPath *string) error { os.Setenv("KUBERNETES_SERVICE_PORT", "443") } kubeconfig, err = rest.InClusterConfig() - inCluster = true + *singleNamespace = true } else { kubeconfig, err = clientcmd.BuildConfigFromFlags("", *kubeconfigPath) } @@ -95,6 +95,13 @@ func setup(kubeconfigPath, namespacedManPath *string) error { return fmt.Errorf("failed to build the dynamic client: %v", err) } dynDec := serializer.NewCodecFactory(scheme).UniversalDeserializer() + namespace := "" + if *singleNamespace { + namespace = os.Getenv(TestNamespaceEnv) + if len(namespace) == 0 { + return fmt.Errorf("namespace set in %s cannot be empty", TestNamespaceEnv) + } + } Global = &Framework{ KubeConfig: kubeconfig, KubeClient: kubeclient, @@ -103,7 +110,8 @@ func setup(kubeconfigPath, namespacedManPath *string) error { DynamicClient: dynClient, DynamicDecoder: dynDec, NamespacedManPath: namespacedManPath, - InCluster: inCluster, + SingleNamespace: *singleNamespace, + Namespace: namespace, } return nil } @@ -136,8 +144,8 @@ 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) { - if Global.InCluster { - err = Global.DynamicClient.List(goctx.TODO(), &dynclient.ListOptions{Namespace: os.Getenv(TestNamespaceEnv)}, obj) + if Global.SingleNamespace { + err = Global.DynamicClient.List(goctx.TODO(), &dynclient.ListOptions{Namespace: Global.Namespace}, obj) } else { err = Global.DynamicClient.List(goctx.TODO(), &dynclient.ListOptions{Namespace: "default"}, obj) } diff --git a/pkg/test/main_entry.go b/pkg/test/main_entry.go index 608df58eb3..4e39032cb2 100644 --- a/pkg/test/main_entry.go +++ b/pkg/test/main_entry.go @@ -27,6 +27,7 @@ const ( KubeConfigFlag = "kubeconfig" NamespacedManPathFlag = "namespacedMan" GlobalManPathFlag = "globalMan" + SingleNamespaceFlag = "singleNamespace" TestNamespaceEnv = "TEST_NAMESPACE" ) @@ -35,13 +36,14 @@ func MainEntry(m *testing.M) { kubeconfigPath := flag.String(KubeConfigFlag, "", "path to kubeconfig") globalManPath := flag.String(GlobalManPathFlag, "", "path to operator manifest") namespacedManPath := flag.String(NamespacedManPathFlag, "", "path to rbac manifest") + singleNamespace := flag.Bool(SingleNamespaceFlag, false, "enable single namespace mode") flag.Parse() // go test always runs from the test directory; change to project root err := os.Chdir(*projRoot) if err != nil { log.Fatalf("failed to change directory to project root: %v", err) } - if err := setup(kubeconfigPath, namespacedManPath); err != nil { + if err := setup(kubeconfigPath, namespacedManPath, singleNamespace); err != nil { log.Fatalf("failed to set up framework: %v", err) } // setup context to use when setting up crd @@ -54,7 +56,7 @@ func MainEntry(m *testing.M) { os.Exit(exitCode) }() // create crd - if !Global.InCluster { + if *kubeconfigPath != "incluster" { globalYAML, err := ioutil.ReadFile(*globalManPath) if err != nil { log.Fatalf("failed to read global resource manifest: %v", err) diff --git a/pkg/test/resource_creator.go b/pkg/test/resource_creator.go index 940f603174..3a693a9321 100644 --- a/pkg/test/resource_creator.go +++ b/pkg/test/resource_creator.go @@ -19,7 +19,6 @@ import ( goctx "context" "fmt" "io/ioutil" - "os" yaml "gopkg.in/yaml.v2" core "k8s.io/api/core/v1" @@ -31,8 +30,8 @@ func (ctx *TestCtx) GetNamespace() (string, error) { if ctx.Namespace != "" { return ctx.Namespace, nil } - if Global.InCluster { - ctx.Namespace = os.Getenv(TestNamespaceEnv) + if Global.SingleNamespace { + ctx.Namespace = Global.Namespace return ctx.Namespace, nil } // create namespace From 4c72ad87e786d641ab70f647b98eee585967d209 Mon Sep 17 00:00:00 2001 From: Alex Pavel Date: Mon, 1 Oct 2018 11:11:49 -0700 Subject: [PATCH 13/16] test/e2e/memcached_test.go: fix e2e testing on local machines (#548) PR #525 changed the way that dep worked to fix dependency issues when doing e2e tests on travis. The PR breaks e2e testing on local machines though, as the tests will just use the master branch instead of a local branch. This commit symlinks the local sdk into vendor on non-travis tests. The tests will still fail on local machines if there is a dependency change, but we are unable to fix this on local machines unless `dep` adds local repo support. --- test/e2e/memcached_test.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/test/e2e/memcached_test.go b/test/e2e/memcached_test.go index fe35ef50cc..ae34e73bab 100644 --- a/test/e2e/memcached_test.go +++ b/test/e2e/memcached_test.go @@ -147,6 +147,12 @@ func TestMemcached(t *testing.T) { if err != nil { t.Fatalf("dep ensure failed: %v\nCommand Output:\n%v", err, string(cmdOut)) } + // link local sdk to vendor if not in travis + if prSlug == "" { + 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") From 096ae3a648d838356042a242232cfaa5d1d21a68 Mon Sep 17 00:00:00 2001 From: John Kim Date: Tue, 2 Oct 2018 11:09:41 -0400 Subject: [PATCH 14/16] update as suggested in comments --- commands/operator-sdk/cmd/generate/crd.go | 15 +++------------ pkg/generator/generator.go | 6 ++++-- 2 files changed, 7 insertions(+), 14 deletions(-) diff --git a/commands/operator-sdk/cmd/generate/crd.go b/commands/operator-sdk/cmd/generate/crd.go index 5f75419ba2..b9f1c0e899 100644 --- a/commands/operator-sdk/cmd/generate/crd.go +++ b/commands/operator-sdk/cmd/generate/crd.go @@ -43,8 +43,8 @@ func NewGenerateCrdCmd() *cobra.Command { Short: "Generates a custom resource definition (CRD) and the custom resource (CR) files", Long: `The operator-sdk generate command will create a custom resource definition (CRD) and the custom resource (CR) files for the specified api-version and kind. -Generated CRD filename: /deploy/_crd.yaml -Generated CR filename: /deploy/_cr.yaml +Generated CRD filename: /deploy/___crd.yaml +Generated CR filename: /deploy/___cr.yaml /deploy path must already exist --api-version and --kind are required flags to generate the new operator application. @@ -67,7 +67,7 @@ func crdFunc(cmd *cobra.Command, args []string) { fmt.Fprintln(os.Stdout, "Generating custom resource definition (CRD) file") - // generate CRD file + // generate CR/CRD file wd, err := os.Getwd() if err != nil { cmdError.ExitWithError(cmdError.ExitError, err) @@ -95,19 +95,10 @@ func verifyCrdFlags() { // verifyCrdDeployPath checks if the path /deploy sub-directory is exists, and that is rooted under $GOPATH func verifyCrdDeployPath() { - // check if $GOPATH env exists - gp := os.Getenv(goDir) - if len(gp) == 0 { - cmdError.ExitWithError(cmdError.ExitError, fmt.Errorf("$GOPATH env not set")) - } 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)) } - // check if this project's repository path is rooted under $GOPATH - if !strings.HasPrefix(wd, gp) { - cmdError.ExitWithError(cmdError.ExitError, fmt.Errorf("project's repository path (%v) is not rooted under GOPATH (%v)", wd, gp)) - } // check if the deploy sub-directory exist _, err = os.Stat(filepath.Join(wd, deployCrdDir)) if err != nil { diff --git a/pkg/generator/generator.go b/pkg/generator/generator.go index fccd4ff798..dc898d9e45 100644 --- a/pkg/generator/generator.go +++ b/pkg/generator/generator.go @@ -346,7 +346,8 @@ func RenderDeployCrdFiles(deployPath, apiVersion, kind string) error { APIVersion: apiVersion, Kind: kind, } - crFilePath := filepath.Join(deployPath, strings.ToLower(kind)+"_cr.yaml") + crFilePath := filepath.Join(deployPath, + groupName(apiVersion)+"_"+version(apiVersion)+"_"+strings.ToLower(kind)+"_cr.yaml") if err := renderWriteFile(crFilePath, crFilePath, crYamlTmpl, crTd); err != nil { return err } @@ -358,7 +359,8 @@ func RenderDeployCrdFiles(deployPath, apiVersion, kind string) error { GroupName: groupName(apiVersion), Version: version(apiVersion), } - crdFilePath := filepath.Join(deployPath, strings.ToLower(kind)+"_crd.yaml") + crdFilePath := filepath.Join(deployPath, + groupName(apiVersion)+"_"+version(apiVersion)+"_"+strings.ToLower(kind)+"_crd.yaml") return renderWriteFile(crdFilePath, crdFilePath, crdYamlTmpl, crdTd) } From a47f90fa2d0305a445c43fc25018fd5577b73c9f Mon Sep 17 00:00:00 2001 From: Dylan Murray Date: Tue, 2 Oct 2018 11:36:54 -0400 Subject: [PATCH 15/16] Do not generate service account file for Ansible Operator --- pkg/generator/generator.go | 13 ++++++++----- pkg/generator/templates.go | 3 ++- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/pkg/generator/generator.go b/pkg/generator/generator.go index dc898d9e45..a10eb5999a 100644 --- a/pkg/generator/generator.go +++ b/pkg/generator/generator.go @@ -392,11 +392,14 @@ func renderDeployFiles(deployDir, projectName, apiVersion, kind, operatorType st return err } - saTd := tmplData{ - ProjectName: projectName, - } - if err := renderWriteFile(filepath.Join(deployDir, saYaml), saTmplName, saYamlTmpl, saTd); err != nil { - return err + // Service account file is only needed for Go operator + if operatorType == goOperatorType { + saTd := tmplData{ + ProjectName: projectName, + } + if err := renderWriteFile(filepath.Join(deployDir, saYaml), saTmplName, saYamlTmpl, saTd); err != nil { + return err + } } opTd := tmplData{ diff --git a/pkg/generator/templates.go b/pkg/generator/templates.go index 61c72a02e3..5e4126849c 100644 --- a/pkg/generator/templates.go +++ b/pkg/generator/templates.go @@ -456,7 +456,8 @@ spec: labels: name: {{.ProjectName}} spec: - serviceAccountName: {{.ProjectName}} +{{- if .IsGoOperator }} + serviceAccountName: {{.ProjectName}}{{ end }} containers: - name: {{.ProjectName}} image: {{.Image}} From 1962e6b55e28aaf55e6103adbef8b097d3874c9f Mon Sep 17 00:00:00 2001 From: Dylan Murray Date: Tue, 2 Oct 2018 12:45:01 -0400 Subject: [PATCH 16/16] Update test --- pkg/generator/generator_test.go | 1 - 1 file changed, 1 deletion(-) diff --git a/pkg/generator/generator_test.go b/pkg/generator/generator_test.go index 1167625ae1..b5a619ff06 100644 --- a/pkg/generator/generator_test.go +++ b/pkg/generator/generator_test.go @@ -232,7 +232,6 @@ spec: labels: name: app-operator spec: - serviceAccountName: app-operator containers: - name: app-operator image: quay.io/example-inc/app-operator:0.0.1