diff --git a/.travis.yml b/.travis.yml index 9305e8cc329..05ab8ea4422 100644 --- a/.travis.yml +++ b/.travis.yml @@ -10,18 +10,6 @@ go: jobs: include: - - before_script: hack/ci/setup-minikube.sh - env: - - CLUSTER=minikube - - CHANGE_MINIKUBE_NONE_USER=true - script: test/test-go.sh - name: Go on minikube - - before_script: hack/ci/setup-minikube.sh - env: - - CLUSTER=minikube - - CHANGE_MINIKUBE_NONE_USER=true - script: test/test-ansible.sh - name: Ansible on minikube - before_script: hack/ci/setup-openshift.sh env: CLUSTER=openshift script: test/test-go.sh diff --git a/Gopkg.lock b/Gopkg.lock index 55e8e3cec2e..49fe38f74bc 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -261,15 +261,16 @@ version = "v2.0.1" [[projects]] - digest = "1:4142d94383572e74b42352273652c62afec5b23f325222ed09198f46009022d1" + digest = "1:f3e56d302f80d760e718743f89f4e7eaae532d4218ba330e979bd051f78de141" name = "github.com/prometheus/client_golang" packages = [ "prometheus", + "prometheus/internal", "prometheus/promhttp", ] pruneopts = "" - revision = "c5b7fccd204277076155f10851dad72b76a49317" - version = "v0.8.0" + revision = "1cafe34db7fdec6022e17e00e1c1ea501022f3e4" + version = "v0.9.0" [[projects]] branch = "master" @@ -281,7 +282,7 @@ [[projects]] branch = "master" - digest = "1:f477ef7b65d94fb17574fc6548cef0c99a69c1634ea3b6da248b63a61ebe0498" + digest = "1:d1b5970f2a453e7c4be08117fb683b5d096bad9d17f119a6e58d4c561ca205dd" name = "github.com/prometheus/common" packages = [ "expfmt", @@ -289,7 +290,7 @@ "model", ] pruneopts = "" - revision = "c7de2306084e37d54b8be01f3541a8464345e9a5" + revision = "bcb74de08d37a417cb6789eec1d6c810040f0470" [[projects]] branch = "master" @@ -380,11 +381,11 @@ [[projects]] branch = "master" - digest = "1:6a29a2cd3888fd1aeba990cfffebe3524b79d717a0abe52ee7d695c51ff75154" + digest = "1:78f41d38365ccef743e54ed854a2faf73313ba0750c621116a8eeb0395590bd0" name = "golang.org/x/crypto" packages = ["ssh/terminal"] pruneopts = "" - revision = "a92615f3c49003920a58dedcf32cf55022cefb8d" + revision = "0c41d7ab0a0ee717d4590a44bcb987dfd9e183eb" [[projects]] branch = "master" @@ -446,7 +447,7 @@ [[projects]] branch = "master" - digest = "1:4e6c1bdaf0b67e43450fd0c0f29ee3807f8f6d385e555c7e769e5dda95238b10" + digest = "1:19e6467b9934614872b092d019b757fbc89de35cf473d71828d604dbd9f9c172" name = "golang.org/x/tools" packages = [ "go/ast/astutil", @@ -455,7 +456,7 @@ "internal/gopathwalk", ] pruneopts = "" - revision = "19e2aca3fdf9724feacb8701307db6fb35055030" + revision = "5e66757b835f155f7e50931f54c9f6af8af86f75" [[projects]] digest = "1:75fb3fcfc73a8c723efde7777b40e8e8ff9babf30d8c56160d01beffea8a95a6" diff --git a/Makefile b/Makefile index 51ba488de45..37d5aaad390 100644 --- a/Makefile +++ b/Makefile @@ -9,7 +9,7 @@ else Q = @ endif -VERSION = $(shell git describe --dirty --tags) +VERSION = $(shell git describe --dirty --tags --always) REPO = github.com/operator-framework/operator-sdk BUILD_PATH = $(REPO)/commands/operator-sdk PKGS = $(shell go list ./... | grep -v /vendor/) diff --git a/README.md b/README.md index ec66ccef244..1230960e464 100644 --- a/README.md +++ b/README.md @@ -99,7 +99,7 @@ $ kubectl delete -f deploy/app_v1alpha1_appservice_cr.yaml $ kubectl delete -f deploy/operator.yaml $ kubectl delete -f deploy/role.yaml $ kubectl delete -f deploy/role_binding.yaml -$ kubectl delete -f deploy/app_v1alpha1_appservice_crd.yaml +$ kubectl delete -f deploy/crds/app_v1alpha1_appservice_crd.yaml ``` ## User Guide diff --git a/commands/operator-sdk/cmd/add.go b/commands/operator-sdk/cmd/add.go index 363bf2277ba..f4f87be3ea1 100644 --- a/commands/operator-sdk/cmd/add.go +++ b/commands/operator-sdk/cmd/add.go @@ -29,5 +29,6 @@ func NewAddCmd() *cobra.Command { upCmd.AddCommand(add.NewApiCmd()) upCmd.AddCommand(add.NewControllerCmd()) + upCmd.AddCommand(add.NewAddCrdCmd()) return upCmd } diff --git a/commands/operator-sdk/cmd/add/api.go b/commands/operator-sdk/cmd/add/api.go index 9a20ce4cc94..34dcde12d53 100644 --- a/commands/operator-sdk/cmd/add/api.go +++ b/commands/operator-sdk/cmd/add/api.go @@ -15,6 +15,7 @@ package add import ( + "encoding/json" "errors" "fmt" "io/ioutil" @@ -71,14 +72,16 @@ Example: func apiRun(cmd *cobra.Command, args []string) { // Create and validate new resource + cmdutil.MustInProjectRoot() r, err := scaffold.NewResource(apiVersion, kind) if err != nil { log.Fatal(err) } absProjectPath := cmdutil.MustGetwd() + cfg := &input.Config{ - Repo: cmdutil.MustInProjectRoot(), + Repo: cmdutil.CheckAndGetCurrPkg(), AbsProjectPath: absProjectPath, } @@ -146,7 +149,13 @@ func updateRoleForResource(r *scaffold.Resource, absProjectPath string) error { role.Rules = append(role.Rules, *pr) } // update role.yaml - data, err := yaml.Marshal(&role) + d, err := json.Marshal(&role) + if err != nil { + return fmt.Errorf("failed to marshal role(%+v): %v", role, err) + } + m := &map[string]interface{}{} + err = yaml.Unmarshal(d, m) + data, err := yaml.Marshal(m) if err != nil { return fmt.Errorf("failed to marshal role(%+v): %v", role, err) } diff --git a/commands/operator-sdk/cmd/add/controller.go b/commands/operator-sdk/cmd/add/controller.go index 928fb4264ca..dd52233d807 100644 --- a/commands/operator-sdk/cmd/add/controller.go +++ b/commands/operator-sdk/cmd/add/controller.go @@ -57,6 +57,7 @@ Example: } func controllerRun(cmd *cobra.Command, args []string) { + cmdutil.MustInProjectRoot() // Create and validate new resource r, err := scaffold.NewResource(apiVersion, kind) if err != nil { @@ -64,7 +65,7 @@ func controllerRun(cmd *cobra.Command, args []string) { } cfg := &input.Config{ - Repo: cmdutil.MustInProjectRoot(), + Repo: cmdutil.CheckAndGetCurrPkg(), AbsProjectPath: cmdutil.MustGetwd(), } diff --git a/commands/operator-sdk/cmd/generate/crd.go b/commands/operator-sdk/cmd/add/crd.go similarity index 53% rename from commands/operator-sdk/cmd/generate/crd.go rename to commands/operator-sdk/cmd/add/crd.go index b9f1c0e8997..e56319534e6 100644 --- a/commands/operator-sdk/cmd/generate/crd.go +++ b/commands/operator-sdk/cmd/add/crd.go @@ -12,39 +12,36 @@ // See the License for the specific language governing permissions and // limitations under the License. -package generate +package add import ( - "errors" "fmt" + "log" "os" "path/filepath" "strings" - cmdError "github.com/operator-framework/operator-sdk/commands/operator-sdk/error" - "github.com/operator-framework/operator-sdk/pkg/generator" + "github.com/operator-framework/operator-sdk/commands/operator-sdk/cmd/cmdutil" + "github.com/operator-framework/operator-sdk/pkg/scaffold" + "github.com/operator-framework/operator-sdk/pkg/scaffold/input" "github.com/spf13/cobra" ) -var ( - apiVersion string - kind string -) - const ( goDir = "GOPATH" deployCrdDir = "deploy" ) -func NewGenerateCrdCmd() *cobra.Command { +// NewAddCrdCmd - add crd command +func NewAddCrdCmd() *cobra.Command { crdCmd := &cobra.Command{ Use: "crd", - 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. + Short: "Adds a custom resource definition (CRD) and the custom resource (CR) files", + Long: `The operator-sdk add crd 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/crds/___crd.yaml +Generated CR filename: /deploy/crds/___cr.yaml /deploy path must already exist --api-version and --kind are required flags to generate the new operator application. @@ -59,8 +56,11 @@ Generated CR filename: /deploy/___cr.yaml } func crdFunc(cmd *cobra.Command, args []string) { + cfg := &input.Config{ + AbsProjectPath: cmdutil.MustGetwd(), + } if len(args) != 0 { - cmdError.ExitWithError(cmdError.ExitBadArgs, errors.New("crd command doesn't accept any arguments.")) + log.Fatal("crd command doesn't accept any arguments") } verifyCrdFlags() verifyCrdDeployPath() @@ -68,28 +68,39 @@ func crdFunc(cmd *cobra.Command, args []string) { fmt.Fprintln(os.Stdout, "Generating custom resource definition (CRD) file") // generate CR/CRD file - wd, err := os.Getwd() + resource, err := scaffold.NewResource(apiVersion, kind) if err != nil { - cmdError.ExitWithError(cmdError.ExitError, err) + log.Fatalf("%v", 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)) + s := scaffold.Scaffold{} + err = s.Execute(cfg, + &scaffold.Crd{Resource: resource}, + &scaffold.Cr{Resource: resource}, + ) + + if err != nil { + log.Fatalf("add scaffold failed: (%v)", err) + } + + // update deploy/role.yaml for the given resource r. + if err := updateRoleForResource(resource, cfg.AbsProjectPath); err != nil { + log.Fatalf("failed to update the RBAC manifest for the resource (%v, %v): %v", resource.APIVersion, resource.Kind, err) } } func verifyCrdFlags() { if len(apiVersion) == 0 { - cmdError.ExitWithError(cmdError.ExitBadArgs, errors.New("--api-version must not have empty value")) + log.Fatal("--api-version must not have empty value") } if len(kind) == 0 { - cmdError.ExitWithError(cmdError.ExitBadArgs, errors.New("--kind must not have empty value")) + log.Fatal("--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")) + log.Fatal("--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)) + log.Fatalf("api-version has wrong format (%v); format must be $GROUP_NAME/$VERSION (e.g app.example.com/v1alpha1)", apiVersion) } } @@ -97,11 +108,11 @@ func verifyCrdFlags() { func verifyCrdDeployPath() { 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)) + log.Fatalf("failed to determine the full path of the current directory: %v", err) } // 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)) + log.Fatalf("the path (./%v) does not exist. run this command in your project directory", deployCrdDir) } } diff --git a/commands/operator-sdk/cmd/build.go b/commands/operator-sdk/cmd/build.go index 74d8d5ba060..247e26bc384 100644 --- a/commands/operator-sdk/cmd/build.go +++ b/commands/operator-sdk/cmd/build.go @@ -23,10 +23,6 @@ import ( "os" "os/exec" "path/filepath" - "strings" - - "github.com/operator-framework/operator-sdk/commands/operator-sdk/cmd/cmdutil" - cmdError "github.com/operator-framework/operator-sdk/commands/operator-sdk/error" "github.com/operator-framework/operator-sdk/commands/operator-sdk/cmd/cmdutil" "github.com/operator-framework/operator-sdk/pkg/scaffold" @@ -137,9 +133,7 @@ func verifyTestManifest(image string) { } const ( - build = "./build/build.sh" - configYaml = "./config/config.yaml" - mainGo = "./cmd/manager/main.go" + mainGo = "./cmd/manager/main.go" ) func buildFunc(cmd *cobra.Command, args []string) { @@ -197,7 +191,7 @@ func buildFunc(cmd *cobra.Command, args []string) { absProjectPath := cmdutil.MustGetwd() cfg := &input.Config{ - Repo: cmdutil.MustInProjectRoot(), + Repo: cmdutil.CheckAndGetCurrPkg(), AbsProjectPath: absProjectPath, ProjectName: filepath.Base(wd), } @@ -230,16 +224,3 @@ func mainExists() bool { } return false } - -func mainExists() 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/cmdutil/util.go b/commands/operator-sdk/cmd/cmdutil/util.go index 2958c2fa7da..25b4e5f6a61 100644 --- a/commands/operator-sdk/cmd/cmdutil/util.go +++ b/commands/operator-sdk/cmd/cmdutil/util.go @@ -25,9 +25,8 @@ import ( type OperatorType int const ( - configYaml = "./config/config.yaml" - gopkgToml = "./Gopkg.toml" - tmpDockerfile = "./tmp/build/Dockerfile" + gopkgToml = "./Gopkg.toml" + buildDockerfile = "./build/Dockerfile" ) const ( // OperatorTypeGo - golang type of operator. @@ -47,14 +46,13 @@ const ( // MustInProjectRoot checks if the current dir is the project root and returns the current repo's import path // e.g github.com/example-inc/app-operator -func MustInProjectRoot() string { - // if the current directory has the "./cmd/manager/main.go" file, then it is safe to say +func MustInProjectRoot() { + // if the current directory has the "./build/dockerfile" file, then it is safe to say // we are at the project root. - _, err := os.Stat(tmpDockerfile) + _, err := os.Stat(buildDockerfile) if err != nil && os.IsNotExist(err) { log.Fatalf("must run command in project root dir: %v", err) } - return CheckAndGetCurrPkg() } func MustGetwd() string { diff --git a/commands/operator-sdk/cmd/generate.go b/commands/operator-sdk/cmd/generate.go index 0a75b7949ec..39923f518f0 100644 --- a/commands/operator-sdk/cmd/generate.go +++ b/commands/operator-sdk/cmd/generate.go @@ -24,14 +24,8 @@ func NewGenerateCmd() *cobra.Command { cmd := &cobra.Command{ Use: "generate ", Short: "Invokes specific generator", - Long: `The operator-sdk generate command invokes specific generator to generate code as needed. -`, + Long: `The operator-sdk generate command invokes specific generator to generate code as needed.`, } cmd.AddCommand(generate.NewGenerateK8SCmd()) -<<<<<<< HEAD - cmd.AddCommand(generate.NewGenerateOlmCatalogCmd()) - cmd.AddCommand(generate.NewGenerateCrdCmd()) -======= ->>>>>>> pkg/generator: remove generator return cmd } diff --git a/commands/operator-sdk/cmd/generate/k8s.go b/commands/operator-sdk/cmd/generate/k8s.go index 3568877e609..7e49d6d55da 100644 --- a/commands/operator-sdk/cmd/generate/k8s.go +++ b/commands/operator-sdk/cmd/generate/k8s.go @@ -27,10 +27,6 @@ import ( "github.com/spf13/cobra" ) -const ( - k8sGenerated = "./tmp/codegen/update-generated.sh" -) - func NewGenerateK8SCmd() *cobra.Command { return &cobra.Command{ Use: "k8s", @@ -51,7 +47,8 @@ func k8sFunc(cmd *cobra.Command, args []string) { // K8sCodegen performs deepcopy code-generation for all custom resources under pkg/apis func K8sCodegen() { - repoPkg := cmdutil.MustInProjectRoot() + cmdutil.MustInProjectRoot() + repoPkg := cmdutil.CheckAndGetCurrPkg() outputPkg := filepath.Join(repoPkg, "pkg/generated") apisPkg := filepath.Join(repoPkg, "pkg/apis") groupVersions, err := parseGroupVersions() diff --git a/commands/operator-sdk/cmd/new.go b/commands/operator-sdk/cmd/new.go index 6148c011d84..a7dafac138c 100644 --- a/commands/operator-sdk/cmd/new.go +++ b/commands/operator-sdk/cmd/new.go @@ -15,8 +15,10 @@ package cmd import ( + "encoding/json" "errors" "fmt" + "io/ioutil" "log" "os" "os/exec" @@ -24,12 +26,14 @@ import ( "strings" "github.com/operator-framework/operator-sdk/commands/operator-sdk/cmd/cmdutil" - cmdError "github.com/operator-framework/operator-sdk/commands/operator-sdk/error" - "github.com/operator-framework/operator-sdk/pkg/generator" "github.com/operator-framework/operator-sdk/pkg/scaffold" + "github.com/operator-framework/operator-sdk/pkg/scaffold/ansible" "github.com/operator-framework/operator-sdk/pkg/scaffold/input" "github.com/spf13/cobra" + yaml "gopkg.in/yaml.v2" + rbacv1 "k8s.io/api/rbac/v1" + cgoscheme "k8s.io/client-go/kubernetes/scheme" ) func NewNewCmd() *cobra.Command { @@ -75,27 +79,21 @@ const ( ensureCmd = "ensure" goOperatorType = "go" ansibleOperatorType = "ansible" - - defaultDirFileMode = 0750 - defaultFileMode = 0644 - defaultExecFileMode = 0755 ) func newFunc(cmd *cobra.Command, args []string) { if len(args) != 1 { - cmdError.ExitWithError(cmdError.ExitBadArgs, fmt.Errorf("new command needs 1 argument")) + log.Fatal("new command needs 1 argument") } parse(args) mustBeNewProject() verifyFlags() - 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)) - } - if operatorType == goOperatorType { + switch operatorType { + case goOperatorType: doScaffold() pullDep() + case ansibleOperatorType: + doAnsibleScaffold() } initGit() } @@ -151,19 +149,95 @@ func doScaffold() { } } +func doAnsibleScaffold() { + cfg := &input.Config{ + AbsProjectPath: filepath.Join(cmdutil.MustGetwd(), projectName), + ProjectName: projectName, + } + + resource, err := scaffold.NewResource(apiVersion, kind) + if err != nil { + log.Fatal("Invalid apiVersion and kind.") + } + + s := &scaffold.Scaffold{} + tmpdir, err := ioutil.TempDir("", "osdk") + if err != nil { + log.Fatal("unable to get temp directory") + } + + galaxyInit := &ansible.GalaxyInit{ + Resource: *resource, + Dir: tmpdir, + } + + err = s.Execute(cfg, + &ansible.Dockerfile{ + GeneratePlaybook: generatePlaybook, + }, + &ansible.WatchesYAML{ + Resource: *resource, + GeneratePlaybook: generatePlaybook, + }, + galaxyInit, + &scaffold.Role{}, + &scaffold.RoleBinding{}, + &ansible.Operator{}, + &scaffold.Crd{ + Resource: resource, + }, + &scaffold.Cr{ + Resource: resource, + }, + ) + if err != nil { + log.Fatalf("new scaffold failed: (%v)", err) + } + + // Decide on playbook. + if generatePlaybook { + err := s.Execute(cfg, + &ansible.Playbook{ + Resource: *resource, + }, + ) + if err != nil { + log.Fatalf("new scaffold failed: (%v)", err) + } + } + + // Run galaxy init. + cmd := exec.Command(filepath.Join(galaxyInit.AbsProjectPath, galaxyInit.Path)) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + cmd.Run() + // Delete Galxy INIT + // Mac OS tmp directory is /var/folders/_c/..... this means we have to make sure that we get the top level directory to remove + // everything. + tmpDirectorySlice := strings.Split(os.TempDir(), "/") + if err = os.RemoveAll(filepath.Join(galaxyInit.AbsProjectPath, tmpDirectorySlice[1])); err != nil { + log.Fatalf("failed to remove the galaxy init script") + } + + // update deploy/role.yaml for the given resource r. + if err := updateRoleForResource(resource, cfg.AbsProjectPath); err != nil { + log.Fatalf("failed to update the RBAC manifest for the resource (%v, %v): %v", resource.APIVersion, resource.Kind, err) + } +} + // repoPath checks if this project's repository path is rooted under $GOPATH and returns project's repository path. // repoPath field on generator is used primarily in generation of Go operator. For Ansible we will set it to cwd func repoPath() string { // We only care about GOPATH constraint checks if we are a Go operator - wd := mustGetwd() + wd := cmdutil.MustGetwd() if operatorType == goOperatorType { gp := os.Getenv(gopath) if len(gp) == 0 { - cmdError.ExitWithError(cmdError.ExitError, fmt.Errorf("$GOPATH env not set")) + log.Fatal("$GOPATH env not set") } // 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)) + log.Fatalf("project's repository path (%v) is not rooted under GOPATH (%v)", wd, gp) } // compute the repo path by stripping "$GOPATH/src/" from the path of the current directory. rp := filepath.Join(string(wd[len(filepath.Join(gp, src)):]), projectName) @@ -175,35 +249,32 @@ func repoPath() string { func verifyFlags() { if operatorType != goOperatorType && operatorType != ansibleOperatorType { - cmdError.ExitWithError(cmdError.ExitBadArgs, errors.New("--type can only be `go` or `ansible`")) + log.Fatal("--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`")) + log.Fatal("--generate-playbook can only be used with --type `ansible`") } + if operatorType == goOperatorType && (len(apiVersion) != 0 || len(kind) != 0) { + log.Fatal(`go type operator does not use --api-version or --kind. Please see "operator-sdk add" command after running new.`) + } + if operatorType != goOperatorType { if len(apiVersion) == 0 { - cmdError.ExitWithError(cmdError.ExitBadArgs, errors.New("--api-version must not have empty value")) + log.Fatal("--api-version must not have empty value") } if len(kind) == 0 { - cmdError.ExitWithError(cmdError.ExitBadArgs, errors.New("--kind must not have empty value")) + log.Fatal("--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")) + log.Fatal("--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)) + log.Fatalf("api-version has wrong format (%v); format must be $GROUP_NAME/$VERSION (e.g app.example.com/v1alpha1)", apiVersion) } } } -func mustGetwd() string { - wd, err := os.Getwd() - if err != nil { - log.Fatalf("failed to determine the full path of the current directory: %v", err) - } -} - func execCmd(stdout *os.File, cmd string, args ...string) { dc := exec.Command(cmd, args...) dc.Dir = filepath.Join(cmdutil.MustGetwd(), projectName) @@ -235,3 +306,66 @@ func initGit() { execCmd(os.Stdout, "git", "commit", "-q", "-m", "INITIAL COMMIT") fmt.Fprintln(os.Stdout, "Run git init done") } + +// Copied from add/api.go command +func updateRoleForResource(r *scaffold.Resource, absProjectPath string) error { + // append rbac rule to deploy/role.yaml + roleFilePath := filepath.Join(absProjectPath, "deploy", "role.yaml") + roleYAML, err := ioutil.ReadFile(roleFilePath) + if err != nil { + return fmt.Errorf("failed to read role manifest %v: %v", roleFilePath, err) + } + obj, _, err := cgoscheme.Codecs.UniversalDeserializer().Decode(roleYAML, nil, nil) + if err != nil { + return fmt.Errorf("failed to decode role manifest %v: %v", roleFilePath, err) + } + switch role := obj.(type) { + // TODO: handle cluster roles for operators to watch every namespace. + case *rbacv1.Role: + pr := &rbacv1.PolicyRule{} + apiGroupFound := false + for i := range role.Rules { + if role.Rules[i].APIGroups[0] == r.FullGroup { + apiGroupFound = true + pr = &role.Rules[i] + break + } + } + // check if the resource already exists + for _, resource := range pr.Resources { + if resource == r.Resource { + log.Printf("deploy/role.yaml RBAC rules already up to date for the resource (%v, %v)", r.APIVersion, r.Kind) + return nil + } + } + + pr.Resources = append(pr.Resources, r.Resource) + // create a new apiGroup if not found. + if !apiGroupFound { + pr.APIGroups = []string{r.FullGroup} + // Using "*" to allow access to the resource and all its subresources e.g "memcacheds" and "memcacheds/finalizers" + // https://kubernetes.io/docs/reference/access-authn-authz/admission-controllers/#ownerreferencespermissionenforcement + pr.Resources = []string{"*"} + pr.Verbs = []string{"*"} + role.Rules = append(role.Rules, *pr) + } + // update role.yaml + d, err := json.Marshal(&role) + if err != nil { + return fmt.Errorf("failed to marshal role(%+v): %v", role, err) + } + m := &map[string]interface{}{} + err = yaml.Unmarshal(d, m) + data, err := yaml.Marshal(m) + if err != nil { + return fmt.Errorf("failed to marshal role(%+v): %v", role, err) + } + if err := ioutil.WriteFile(roleFilePath, data, cmdutil.DefaultFileMode); err != nil { + return fmt.Errorf("failed to update %v: %v", roleFilePath, err) + } + default: + return errors.New("failed to parse role.yaml as a role") + } + // not reachable + return nil +} diff --git a/commands/operator-sdk/cmd/test/local.go b/commands/operator-sdk/cmd/test/local.go index 10d90ee41fb..7fccf00b64b 100644 --- a/commands/operator-sdk/cmd/test/local.go +++ b/commands/operator-sdk/cmd/test/local.go @@ -20,8 +20,6 @@ import ( "log" "os" "os/exec" - "strings" - "path/filepath" "strings" diff --git a/commands/operator-sdk/cmd/up/local.go b/commands/operator-sdk/cmd/up/local.go index dc5827623a3..7edf1065d60 100644 --- a/commands/operator-sdk/cmd/up/local.go +++ b/commands/operator-sdk/cmd/up/local.go @@ -27,7 +27,6 @@ import ( "syscall" "github.com/operator-framework/operator-sdk/commands/operator-sdk/cmd/cmdutil" - cmdError "github.com/operator-framework/operator-sdk/commands/operator-sdk/error" ansibleOperator "github.com/operator-framework/operator-sdk/pkg/ansible/operator" proxy "github.com/operator-framework/operator-sdk/pkg/ansible/proxy" "github.com/operator-framework/operator-sdk/pkg/util/k8sutil" @@ -75,7 +74,7 @@ func upLocalFunc(cmd *cobra.Command, args []string) { case cmdutil.OperatorTypeAnsible: upLocalAnsible() default: - cmdError.ExitWithError(cmdError.ExitError, fmt.Errorf("failed to determine operator type")) + log.Fatal("failed to determine operator type") } } diff --git a/doc/ansible/project_layout.md b/doc/ansible/project_layout.md index b16eddcabf3..3826905befc 100644 --- a/doc/ansible/project_layout.md +++ b/doc/ansible/project_layout.md @@ -8,5 +8,5 @@ After creating a new operator project using | :--- | :--- | | deploy | Contains a generic set of kubernetes manifests for deploying this operator on a kubernetes cluster. | | roles/ | Contains an Ansible Role initialized using [Ansible Galaxy](https://docs.ansible.com/ansible/latest/reference_appendices/galaxy.html) | -| tmp | Contains scripts that the operator-sdk uses for build and initialization. | +| build | Contains scripts that the operator-sdk uses for build and initialization. | | watches.yaml | Contains Group, Version, Kind, and Ansible invocation method. | diff --git a/doc/user-guide.md b/doc/user-guide.md index b201c3f46f4..5c8f4e7d87f 100644 --- a/doc/user-guide.md +++ b/doc/user-guide.md @@ -1,7 +1,6 @@ # User Guide This guide walks through an example of building a simple memcached-operator using the operator-sdk CLI tool and controller-runtime library API. - To learn how to use Ansible to create a Memcached operator, see [Ansible Operator User Guide][ansible_user_guide]. The rest of this document will show how to program an operator in Go. @@ -73,9 +72,7 @@ $ operator-sdk add api --api-version=cache.example.com/v1alpha1 --kind=Memcached ``` This will scaffold the Memcached resource API under `pkg/apis/cache/v1alpha1/...`. - ### Define the spec and status - Modify the spec and status of the `Memcached` Custom Resource(CR) at `pkg/apis/cache/v1alpha1/memcached_types.go`: ```Go @@ -316,8 +313,6 @@ $ kubectl delete -f deploy/crds/cache_v1alpha1_memcached_cr.yaml $ kubectl delete -f deploy/operator.yaml ``` -<<<<<<< HEAD - ## Advanced Topics ### Adding 3rd Party Resources To Your Operator To add a resource to an operator, you must add it to a scheme. By creating an `AddToScheme` method or reusing one you can easily add a resource to your scheme. An [example][deployments_register] shows that you define a function and then use the [runtime][runtime_package] package to create a `SchemeBuilder` @@ -356,9 +351,7 @@ func main() { ``` [memcached_handler]: ../example/memcached-operator/handler.go.tmpl -======= [memcached_controller]: ../example/memcached-operator/memcached_controller.go.tmpl ->>>>>>> *: update user-guide and memcached-operator example (#551) [layout_doc]:./project_layout.md [ansible_user_guide]:./ansible/user-guide.md [dep_tool]:https://golang.github.io/dep/docs/installation.html diff --git a/example/memcached-operator/memcached_controller.go.tmpl b/example/memcached-operator/memcached_controller.go.tmpl new file mode 100644 index 00000000000..0d8d8437090 --- /dev/null +++ b/example/memcached-operator/memcached_controller.go.tmpl @@ -0,0 +1,218 @@ +package memcached + +import ( + "context" + "log" + "reflect" + + cachev1alpha1 "github.com/example-inc/memcached-operator/pkg/apis/cache/v1alpha1" + + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + "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" +) + +/** +* USER ACTION REQUIRED: This is a scaffold file intended for the user to modify with their own Controller +* business logic. Delete these comments after modifying this file.* + */ + +// Add creates a new Memcached Controller and adds it to the Manager. The Manager will set fields on the Controller +// and Start it when the Manager is Started. +func Add(mgr manager.Manager) error { + return add(mgr, newReconciler(mgr)) +} + +// newReconciler returns a new reconcile.Reconciler +func newReconciler(mgr manager.Manager) reconcile.Reconciler { + return &ReconcileMemcached{client: mgr.GetClient(), scheme: mgr.GetScheme()} +} + +// add adds a new Controller to mgr with r as the reconcile.Reconciler +func add(mgr manager.Manager, r reconcile.Reconciler) error { + // Create a new controller + c, err := controller.New("memcached-controller", mgr, controller.Options{Reconciler: r}) + if err != nil { + return err + } + + // Watch for changes to primary resource Memcached + err = c.Watch(&source.Kind{Type: &cachev1alpha1.Memcached{}}, &handler.EnqueueRequestForObject{}) + if err != nil { + return err + } + + // TODO(user): Modify this to be the types you create that are owned by the primary resource + // Watch for changes to secondary resource Pods and requeue the owner Memcached + err = c.Watch(&source.Kind{Type: &appsv1.Deployment{}}, &handler.EnqueueRequestForOwner{ + IsController: true, + OwnerType: &cachev1alpha1.Memcached{}, + }) + if err != nil { + return err + } + + return nil +} + +var _ reconcile.Reconciler = &ReconcileMemcached{} + +// ReconcileMemcached reconciles a Memcached object +type ReconcileMemcached struct { + // TODO: Clarify the split client + // This client, initialized using mgr.Client() above, is a split client + // that reads objects from the cache and writes to the apiserver + client client.Client + scheme *runtime.Scheme +} + +// Reconcile reads that state of the cluster for a Memcached object and makes changes based on the state read +// and what is in the Memcached.Spec +// TODO(user): Modify this Reconcile function to implement your Controller logic. This example creates +// a Memcached Deployment for each Memcached CR +// Note: +// The Controller will requeue the Request to be processed again if the returned error is non-nil or +// Result.Requeue is true, otherwise upon completion it will remove the work from the queue. +func (r *ReconcileMemcached) Reconcile(request reconcile.Request) (reconcile.Result, error) { + log.Printf("Reconciling Memcached %s/%s\n", request.Namespace, request.Name) + + // Fetch the Memcached instance + memcached := &cachev1alpha1.Memcached{} + err := r.client.Get(context.TODO(), request.NamespacedName, memcached) + if err != nil { + if errors.IsNotFound(err) { + // Request object not found, could have been deleted after reconcile request. + // Owned objects are automatically garbage collected. For additional cleanup logic use finalizers. + // Return and don't requeue + log.Printf("Memcached %s/%s not found. Ignoring since object must be deleted\n", request.Namespace, request.Name) + return reconcile.Result{}, nil + } + // Error reading the object - requeue the request. + log.Printf("Failed to get Memcached: %v", err) + return reconcile.Result{}, err + } + + // Check if the deployment already exists, if not create a new one + found := &appsv1.Deployment{} + err = r.client.Get(context.TODO(), types.NamespacedName{Name: memcached.Name, Namespace: memcached.Namespace}, found) + if err != nil && errors.IsNotFound(err) { + // Define a new deployment + dep := r.deploymentForMemcached(memcached) + log.Printf("Creating a new Deployment %s/%s\n", dep.Namespace, dep.Name) + err = r.client.Create(context.TODO(), dep) + if err != nil { + log.Printf("Failed to create new Deployment: %v\n", err) + return reconcile.Result{}, err + } + // Deployment created successfully - return and requeue + return reconcile.Result{Requeue: true}, nil + } else if err != nil { + log.Printf("Failed to get Deployment: %v\n", err) + return reconcile.Result{}, err + } + + // Ensure the deployment size is the same as the spec + size := memcached.Spec.Size + if *found.Spec.Replicas != size { + found.Spec.Replicas = &size + err = r.client.Update(context.TODO(), found) + if err != nil { + log.Printf("Failed to update Deployment: %v\n", err) + return reconcile.Result{}, err + } + // Spec updated - return and requeue + return reconcile.Result{Requeue: true}, nil + } + + // Update the Memcached status with the pod names + // List the pods for this memcached's deployment + podList := &corev1.PodList{} + labelSelector := labels.SelectorFromSet(labelsForMemcached(memcached.Name)) + listOps := &client.ListOptions{Namespace: memcached.Namespace, LabelSelector: labelSelector} + err = r.client.List(context.TODO(), listOps, podList) + if err != nil { + log.Printf("Failed to list pods: %v", err) + return reconcile.Result{}, err + } + podNames := getPodNames(podList.Items) + + // Update status.Nodes if needed + if !reflect.DeepEqual(podNames, memcached.Status.Nodes) { + memcached.Status.Nodes = podNames + err := r.client.Update(context.TODO(), memcached) + if err != nil { + log.Printf("failed to update memcached status: %v", err) + return reconcile.Result{}, err + } + } + + return reconcile.Result{}, nil +} + +// deploymentForMemcached returns a memcached Deployment object +func (r *ReconcileMemcached) deploymentForMemcached(m *cachev1alpha1.Memcached) *appsv1.Deployment { + ls := labelsForMemcached(m.Name) + replicas := m.Spec.Size + + dep := &appsv1.Deployment{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "apps/v1", + Kind: "Deployment", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: m.Name, + Namespace: m.Namespace, + }, + Spec: appsv1.DeploymentSpec{ + Replicas: &replicas, + Selector: &metav1.LabelSelector{ + MatchLabels: ls, + }, + Template: corev1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{ + Labels: ls, + }, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{{ + Image: "memcached:1.4.36-alpine", + Name: "memcached", + Command: []string{"memcached", "-m=64", "-o", "modern", "-v"}, + Ports: []corev1.ContainerPort{{ + ContainerPort: 11211, + Name: "memcached", + }}, + }}, + }, + }, + }, + } + // Set Memcached instance as the owner and controller + controllerutil.SetControllerReference(m, dep, r.scheme) + return dep +} + +// labelsForMemcached returns the labels for selecting the resources +// belonging to the given memcached CR name. +func labelsForMemcached(name string) map[string]string { + return map[string]string{"app": "memcached", "memcached_cr": name} +} + +// getPodNames returns the pod names of the array of pods passed in +func getPodNames(pods []corev1.Pod) []string { + var podNames []string + for _, pod := range pods { + podNames = append(podNames, pod.Name) + } + return podNames +} diff --git a/pkg/ansible/proxy/proxy.go b/pkg/ansible/proxy/proxy.go index 58596f22fb2..57a01eeb26f 100644 --- a/pkg/ansible/proxy/proxy.go +++ b/pkg/ansible/proxy/proxy.go @@ -51,7 +51,7 @@ func InjectOwnerReferenceHandler(h http.Handler) http.Handler { authString, err := base64.StdEncoding.DecodeString(user) if err != nil { m := "could not base64 decode username" - logrus.Errorf("%s: %s", err.Error()) + logrus.Errorf("%s", err.Error()) http.Error(w, m, http.StatusBadRequest) return } @@ -63,7 +63,7 @@ func InjectOwnerReferenceHandler(h http.Handler) http.Handler { body, err := ioutil.ReadAll(req.Body) if err != nil { m := "could not read request body" - logrus.Errorf("%s: %s", err.Error()) + logrus.Errorf("%s", err.Error()) http.Error(w, m, http.StatusInternalServerError) return } @@ -71,7 +71,7 @@ func InjectOwnerReferenceHandler(h http.Handler) http.Handler { err = json.Unmarshal(body, data) if err != nil { m := "could not deserialize request body" - logrus.Errorf("%s: %s", err.Error()) + logrus.Errorf("%s", err.Error()) http.Error(w, m, http.StatusBadRequest) return } @@ -79,7 +79,7 @@ func InjectOwnerReferenceHandler(h http.Handler) http.Handler { newBody, err := json.Marshal(data.Object) if err != nil { m := "could not serialize body" - logrus.Errorf("%s: %s", err.Error()) + logrus.Errorf("%s", err.Error()) http.Error(w, m, http.StatusInternalServerError) return } diff --git a/pkg/ansible/runner/eventapi/eventapi.go b/pkg/ansible/runner/eventapi/eventapi.go index a28e22f3ff4..f8e0bf69fb6 100644 --- a/pkg/ansible/runner/eventapi/eventapi.go +++ b/pkg/ansible/runner/eventapi/eventapi.go @@ -122,7 +122,7 @@ func (e *EventReceiver) handleEvents(w http.ResponseWriter, r *http.Request) { if strings.Split(ct, ";")[0] != "application/json" { e.logger.WithFields(logrus.Fields{ "code": "415", - }).Info("wrong content type: %s", ct) + }).Infof("wrong content type: %s", ct) w.WriteHeader(http.StatusUnsupportedMediaType) w.Write([]byte("The content-type must be \"application/json\"")) return diff --git a/pkg/scaffold/ansible/dockerfile.go b/pkg/scaffold/ansible/dockerfile.go index fef137466e0..9d5c8af6a9d 100644 --- a/pkg/scaffold/ansible/dockerfile.go +++ b/pkg/scaffold/ansible/dockerfile.go @@ -1,3 +1,17 @@ +// 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 ansible import ( @@ -9,6 +23,8 @@ import ( //Dockerfile - docker file for creating image type Dockerfile struct { input.Input + + GeneratePlaybook bool } // GetInput - gets the input diff --git a/pkg/scaffold/ansible/galaxy_init.go b/pkg/scaffold/ansible/galaxy_init.go index 41ce2ac9811..e30bd8c57a6 100644 --- a/pkg/scaffold/ansible/galaxy_init.go +++ b/pkg/scaffold/ansible/galaxy_init.go @@ -1,25 +1,45 @@ +// 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 ansible import ( "io/ioutil" "path/filepath" + "github.com/operator-framework/operator-sdk/pkg/scaffold" "github.com/operator-framework/operator-sdk/pkg/scaffold/input" ) // GalaxyInit - wrapper type GalaxyInit struct { input.Input + Resource scaffold.Resource + Dir string } // GetInput - get input func (g *GalaxyInit) GetInput() (input.Input, error) { - if g.Path == "" { + if g.Dir == "" { dir, err := ioutil.TempDir("", "osdk") if err != nil { - return g.Input, err + return input.Input{}, err } - g.Path = filepath.Join(dir, "galaxy_init.sh") + g.Dir = dir + } + if g.Path == "" { + g.Path = filepath.Join(g.Dir, "galaxy_init.sh") } g.TemplateBody = galaxyInitTmpl g.IsExec = true @@ -34,5 +54,5 @@ if ! which ansible-galaxy > /dev/null; then fi echo "Initializing role skeleton..." -ansible-galaxy init --init-path={{.Name}}/roles/ {{.Kind}} +ansible-galaxy init --init-path={{.Input.AbsProjectPath}}/roles/ {{.Resource.Kind}} ` diff --git a/pkg/scaffold/ansible/operator.go b/pkg/scaffold/ansible/operator.go new file mode 100644 index 00000000000..e4d210a6523 --- /dev/null +++ b/pkg/scaffold/ansible/operator.go @@ -0,0 +1,64 @@ +// 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 ansible + +import ( + "path/filepath" + + "github.com/operator-framework/operator-sdk/pkg/scaffold/input" +) + +type Operator struct { + input.Input +} + +func (s *Operator) GetInput() (input.Input, error) { + if s.Path == "" { + s.Path = filepath.Join("deploy", "operator.yaml") + } + s.TemplateBody = operatorTemplate + return s.Input, nil +} + +const operatorTemplate = `apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{.ProjectName}} +spec: + replicas: 1 + selector: + matchLabels: + name: {{.ProjectName}} + template: + metadata: + labels: + name: {{.ProjectName}} + spec: + containers: + - name: {{.ProjectName}} + # Replace this with the built image name + image: REPLACE_IMAGE + ports: + - containerPort: 60000 + name: metrics + imagePullPolicy: Always + env: + - name: WATCH_NAMESPACE + valueFrom: + fieldRef: + fieldPath: metadata.namespace + - name: OPERATOR_NAME + value: "{{.ProjectName}}" +` diff --git a/pkg/scaffold/ansible/playbook.go b/pkg/scaffold/ansible/playbook.go index f61376cefaf..d1609cbdfe7 100644 --- a/pkg/scaffold/ansible/playbook.go +++ b/pkg/scaffold/ansible/playbook.go @@ -1,10 +1,28 @@ +// 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 ansible -import "github.com/operator-framework/operator-sdk/pkg/scaffold/input" +import ( + "github.com/operator-framework/operator-sdk/pkg/scaffold" + "github.com/operator-framework/operator-sdk/pkg/scaffold/input" +) // Playbook - the playbook tmpl wrapper type Playbook struct { input.Input + Resource scaffold.Resource } // GetInput - gets the input @@ -20,5 +38,5 @@ const playbookTmpl = `- hosts: localhost gather_facts: no tasks: - import_role: - name: "{{.Kind}}" + name: "{{.Resource.Kind}}" ` diff --git a/pkg/scaffold/ansible/watches_yaml.go b/pkg/scaffold/ansible/watches_yaml.go index e022d99b359..0f5c7c45d5d 100644 --- a/pkg/scaffold/ansible/watches_yaml.go +++ b/pkg/scaffold/ansible/watches_yaml.go @@ -1,6 +1,23 @@ +// 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 ansible -import "github.com/operator-framework/operator-sdk/pkg/scaffold/input" +import ( + "github.com/operator-framework/operator-sdk/pkg/scaffold" + "github.com/operator-framework/operator-sdk/pkg/scaffold/input" +) const ( watchesFile = "watches.yaml" @@ -9,6 +26,9 @@ const ( // WatchesYAML - watches yaml input wrapper type WatchesYAML struct { input.Input + + Resource scaffold.Resource + GeneratePlaybook bool } // GetInput - gets the input @@ -21,8 +41,8 @@ func (s *WatchesYAML) GetInput() (input.Input, error) { } const watchesYAMLTmpl = `--- -- version: {{.Version}} - group: {{.GroupName}} - kind: {{.Kind}} -{{ if .GeneratePlaybook }} playbook: /opt/ansible/playbook.yaml{{ else }} role: /opt/ansible/roles/{{.Kind}}{{ end }} +- version: {{.Resource.Version}} + group: {{.Resource.FullGroup}} + kind: {{.Resource.Kind}} +{{ if .GeneratePlaybook }} playbook: /opt/ansible/playbook.yaml{{ else }} role: /opt/ansible/roles/{{.Resource.Kind}}{{ end }} ` diff --git a/pkg/scaffold/cmd_test.go b/pkg/scaffold/cmd_test.go index 39d7a9acb1a..a44aa29285f 100644 --- a/pkg/scaffold/cmd_test.go +++ b/pkg/scaffold/cmd_test.go @@ -27,9 +27,11 @@ func TestCmd(t *testing.T) { t.Fatalf("failed to execute the scaffold: (%v)", err) } - if cmdExp != buf.String() { + tmpl := buf.String() + + if cmdExp != tmpl { dmp := diffmatchpatch.New() - diffs := diffmatchpatch.New().DiffMain(cmdExp, buf.String(), false) + diffs := diffmatchpatch.New().DiffMain(cmdExp, tmpl, false) t.Fatalf("expected vs actual differs. Red text is missing and green text is extra.\n%v", dmp.DiffPrettyText(diffs)) } } @@ -43,7 +45,6 @@ import ( "github.com/example-inc/app-operator/pkg/apis" "github.com/example-inc/app-operator/pkg/controller" - k8sutil "github.com/operator-framework/operator-sdk/pkg/util/k8sutil" sdkVersion "github.com/operator-framework/operator-sdk/version" _ "k8s.io/client-go/plugin/pkg/client/auth/gcp" diff --git a/pkg/scaffold/controller_kind_test.go b/pkg/scaffold/controller_kind_test.go index c95ff4d2a93..88ec015f9dd 100644 --- a/pkg/scaffold/controller_kind_test.go +++ b/pkg/scaffold/controller_kind_test.go @@ -31,9 +31,11 @@ func TestControllerKind(t *testing.T) { t.Fatalf("failed to execute the scaffold: (%v)", err) } - if controllerKindExp != buf.String() { + tmplOUt := buf.String() + + if controllerKindExp != tmplOUt { dmp := diffmatchpatch.New() - diffs := diffmatchpatch.New().DiffMain(controllerKindExp, buf.String(), false) + diffs := diffmatchpatch.New().DiffMain(controllerKindExp, tmplOUt, true) t.Fatalf("expected vs actual differs. Red text is missing and green text is extra.\n%v", dmp.DiffPrettyText(diffs)) } } @@ -45,7 +47,6 @@ import ( "log" appv1alpha1 "github.com/example-inc/app-operator/pkg/apis/app/v1alpha1" - corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" diff --git a/pkg/scaffold/gitignore.go b/pkg/scaffold/gitignore.go index 41444cb7850..3976d5890f3 100644 --- a/pkg/scaffold/gitignore.go +++ b/pkg/scaffold/gitignore.go @@ -105,10 +105,6 @@ Session.vim tags ### VisualStudioCode ### .vscode/* -!.vscode/settings.json -!.vscode/tasks.json -!.vscode/launch.json -!.vscode/extensions.json .history # End of https://www.gitignore.io/api/go,vim,emacs,visualstudiocode ` diff --git a/pkg/scaffold/gitignore_test.go b/pkg/scaffold/gitignore_test.go index 865c54f5427..ef6536bbdaa 100644 --- a/pkg/scaffold/gitignore_test.go +++ b/pkg/scaffold/gitignore_test.go @@ -109,10 +109,6 @@ Session.vim tags ### VisualStudioCode ### .vscode/* -!.vscode/settings.json -!.vscode/tasks.json -!.vscode/launch.json -!.vscode/extensions.json .history # End of https://www.gitignore.io/api/go,vim,emacs,visualstudiocode ` diff --git a/test/ansible-operator/Dockerfile b/test/ansible-operator/Dockerfile index b4a2c70ccc2..1d5639ca771 100644 --- a/test/ansible-operator/Dockerfile +++ b/test/ansible-operator/Dockerfile @@ -1,6 +1,7 @@ FROM ansible/ansible-runner RUN pip install --upgrade setuptools +RUN pip install urllib3==1.23 RUN pip install openshift ansible-runner-http RUN echo "localhost ansible_connection=local" > /etc/ansible/hosts \ diff --git a/test/ansible-operator/cmd/ansible-operator/main.go b/test/ansible-operator/cmd/ansible-operator/main.go index b06b328b001..1d85310ab4a 100644 --- a/test/ansible-operator/cmd/ansible-operator/main.go +++ b/test/ansible-operator/cmd/ansible-operator/main.go @@ -24,6 +24,7 @@ import ( "github.com/operator-framework/operator-sdk/pkg/ansible/controller" proxy "github.com/operator-framework/operator-sdk/pkg/ansible/proxy" "github.com/operator-framework/operator-sdk/pkg/ansible/runner" + "github.com/operator-framework/operator-sdk/pkg/util/k8sutil" sdkVersion "github.com/operator-framework/operator-sdk/version" "sigs.k8s.io/controller-runtime/pkg/client/config" "sigs.k8s.io/controller-runtime/pkg/manager" @@ -43,7 +44,14 @@ func main() { flag.Parse() logf.SetLogger(logf.ZapLogger(false)) - mgr, err := manager.New(config.GetConfigOrDie(), manager.Options{}) + namespace, err := k8sutil.GetWatchNamespace() + if err != nil { + log.Fatalf("failed to get watch namespace: %v", err) + } + + mgr, err := manager.New(config.GetConfigOrDie(), manager.Options{ + Namespace: namespace, + }) if err != nil { log.Fatal(err) } diff --git a/test/test-ansible.sh b/test/test-ansible.sh index 4077f590e41..8a9f6635cb9 100755 --- a/test/test-ansible.sh +++ b/test/test-ansible.sh @@ -1,6 +1,8 @@ #!/usr/bin/env bash -set -ev +DEST_IMAGE="quay.io/example/memcached-operator:v0.0.2" + +set -evx # switch to the "default" namespace if on openshift, to match the minikube test if which oc 2>/dev/null; then oc project default; fi @@ -20,28 +22,39 @@ cp -a ansible-memcached/memfin memcached-operator/roles/ cat ansible-memcached/watches-finalizer.yaml >> memcached-operator/watches.yaml pushd memcached-operator -operator-sdk build quay.io/example/memcached-operator:v0.0.2 -sed -i 's|REPLACE_IMAGE|quay.io/example/memcached-operator:v0.0.2|g' deploy/operator.yaml +operator-sdk build $DEST_IMAGE +sed -i "s|REPLACE_IMAGE|$DEST_IMAGE|g" deploy/operator.yaml sed -i 's|Always|Never|g' deploy/operator.yaml # deploy the operator -kubectl create -f deploy/rbac.yaml -kubectl create -f deploy/crd.yaml +kubectl create -f deploy/role.yaml +kubectl create -f deploy/role_binding.yaml +kubectl create -f deploy/crds/ansible_v1alpha1_memcached_crd.yaml kubectl create -f deploy/operator.yaml # wait for operator pod to run -kubectl rollout status deployment/memcached-operator -kubectl logs deployment/memcached-operator +if ! timeout 1m kubectl rollout status deployment/memcached-operator; +then + kubectl logs deployment/memcached-operator + exit 1 +fi # create CR -kubectl create -f deploy/cr.yaml -until kubectl get deployment -l app=memcached | grep memcached; do sleep 1; done +kubectl create -f deploy/crds/ansible_v1alpha1_memcached_cr.yaml +if ! timeout 20s bash -c -- 'until kubectl get deployment -l app=memcached | grep memcached; do sleep 1; done'; +then + kubectl logs deployment/memcached-operator + exit 1 +fi memcached_deployment=$(kubectl get deployment -l app=memcached -o jsonpath="{..metadata.name}") -kubectl rollout status deployment/${memcached_deployment} -kubectl logs deployment/${memcached_deployment} +if ! timeout 1m kubectl rollout status deployment/${memcached_deployment}; +then + kubectl logs deployment/${memcached_deployment} + exit 1 +fi # Test finalizer -kubectl delete -f deploy/cr.yaml --wait=true +kubectl delete -f deploy/crds/ansible_v1alpha1_memcached_cr.yaml --wait=true kubectl logs deployment/memcached-operator | grep "this is a finalizer" popd diff --git a/test/test-framework/deploy/namespace-init.yaml b/test/test-framework/deploy/namespace-init.yaml index 9e5dfedc612..864c7fcd989 100644 --- a/test/test-framework/deploy/namespace-init.yaml +++ b/test/test-framework/deploy/namespace-init.yaml @@ -24,17 +24,6 @@ rules: - replicasets - statefulsets verbs: - - "*" - ---- - -kind: RoleBinding -apiVersion: rbac.authorization.k8s.io/v1beta1 -metadata: - name: memcached-operator -subjects: -- kind: ServiceAccount - name: memcached-operator - '*' - apiGroups: - cache.example.com @@ -42,9 +31,7 @@ subjects: - '*' verbs: - '*' - --- - kind: RoleBinding apiVersion: rbac.authorization.k8s.io/v1 metadata: @@ -56,9 +43,7 @@ roleRef: kind: Role name: memcached-operator apiGroup: rbac.authorization.k8s.io - --- - apiVersion: apps/v1 kind: Deployment metadata: diff --git a/test/test-framework/deploy/operator.yaml b/test/test-framework/deploy/operator.yaml index 6552adc6e36..0f8437b93a2 100644 --- a/test/test-framework/deploy/operator.yaml +++ b/test/test-framework/deploy/operator.yaml @@ -12,7 +12,6 @@ spec: labels: name: memcached-operator spec: - serviceAccountName: memcached-operator containers: - name: memcached-operator # Replace this with the built image name diff --git a/test/test-go.sh b/test/test-go.sh index 472cf1b38ce..2141f8e1255 100755 --- a/test/test-go.sh +++ b/test/test-go.sh @@ -1,5 +1,5 @@ #!/usr/bin/env bash -set -e +set -ev go test ./commands/... go test ./pkg/...