diff --git a/commands/operator-sdk/cmd/new.go b/commands/operator-sdk/cmd/new.go index af0b775e24b..e84c74587f3 100644 --- a/commands/operator-sdk/cmd/new.go +++ b/commands/operator-sdk/cmd/new.go @@ -53,6 +53,7 @@ generates a skeletal app-operator application in $GOPATH/src/github.com/example. 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. (Only used for --type ansible)") + newCmd.Flags().BoolVar(&isClusterScoped, "cluster-scoped", false, "Generate cluster-scoped resources instead of namespace-scoped") return newCmd } @@ -64,6 +65,7 @@ var ( projectName string skipGit bool generatePlaybook bool + isClusterScoped bool ) const ( @@ -131,9 +133,15 @@ func doScaffold() { &scaffold.Cmd{}, &scaffold.Dockerfile{}, &scaffold.ServiceAccount{}, - &scaffold.Role{}, - &scaffold.RoleBinding{}, - &scaffold.Operator{}, + &scaffold.Role{ + IsClusterScoped: isClusterScoped, + }, + &scaffold.RoleBinding{ + IsClusterScoped: isClusterScoped, + }, + &scaffold.Operator{ + IsClusterScoped: isClusterScoped, + }, &scaffold.Apis{}, &scaffold.Controller{}, &scaffold.Version{}, @@ -177,9 +185,15 @@ func doAnsibleScaffold() { }, galaxyInit, &scaffold.ServiceAccount{}, - &scaffold.Role{}, - &scaffold.RoleBinding{}, - &ansible.Operator{}, + &scaffold.Role{ + IsClusterScoped: isClusterScoped, + }, + &scaffold.RoleBinding{ + IsClusterScoped: isClusterScoped, + }, + &ansible.Operator{ + IsClusterScoped: isClusterScoped, + }, &scaffold.Crd{ Resource: resource, }, diff --git a/doc/ansible/user-guide.md b/doc/ansible/user-guide.md index c3291dd6205..ab37bd7110d 100644 --- a/doc/ansible/user-guide.md +++ b/doc/ansible/user-guide.md @@ -53,6 +53,15 @@ Memcached resource with APIVersion `cache.example.com/v1apha1` and Kind To learn more about the project directory structure, see [project layout][layout_doc] doc. +#### Operator scope + +A namespace-scoped operator (the default) watches and manages resources in a single namespace, whereas a cluster-scoped operator watches and manages resources cluster-wide. Namespace-scoped operators are preferred because of their flexibility. They enable decoupled upgrades, namespace isolation for failures and monitoring, and differing API definitions. However, there are use cases where a cluster-scoped operator may make sense. For example, the [cert-manager](https://github.com/jetstack/cert-manager) operator is often deployed with cluster-scoped permissions and watches so that it can manage issuing certificates for an entire cluster. + +If you'd like to create your memcached-operator project to be cluster-scoped use the following `operator-sdk new` command instead: +``` +$ operator-sdk new memcached-operator --cluster-scoped --api-version=cache.example.com/v1alpha1 --kind=Memcached --type=ansible +``` + ## Customize the operator logic For this example the memcached-operator will execute the following @@ -205,10 +214,17 @@ deployment image in this file needs to be modified from the placeholder $ sed -i 's|REPLACE_IMAGE|quay.io/example/memcached-operator:v0.0.1|g' deploy/operator.yaml ``` +If you created your operator using `--cluster-scoped=true`, update the service account namespace in the generated `ClusterRoleBinding` to match where you are deploying your operator. +``` +$ export OPERATOR_NAMESPACE=$(kubectl config view --minify -o jsonpath='{.contexts[0].context.namespace}') +$ sed -i "s|REPLACE_NAMESPACE|$OPERATOR_NAMESPACE|g" deploy/role_binding.yaml +``` + **Note** -If you are performing these steps on OSX, use the following command: +If you are performing these steps on OSX, use the following commands instead: ``` $ sed -i "" 's|REPLACE_IMAGE|quay.io/example/memcached-operator:v0.0.1|g' deploy/operator.yaml +$ sed -i "" "s|REPLACE_NAMESPACE|$OPERATOR_NAMESPACE|g" deploy/role_binding.yaml ``` Deploy the memcached-operator: @@ -220,10 +236,6 @@ $ kubectl create -f deploy/role_binding.yaml $ kubectl create -f deploy/operator.yaml ``` -**NOTE**: `deploy/rbac.yaml` creates a `ClusterRoleBinding` and assumes we are -working in namespace `default`. If you are working in a different namespace you -must modify this file before creating it. - Verify that the memcached-operator is up and running: ```sh diff --git a/doc/sdk-cli-reference.md b/doc/sdk-cli-reference.md index 368eb1acefc..f15ee589d0d 100644 --- a/doc/sdk-cli-reference.md +++ b/doc/sdk-cli-reference.md @@ -133,6 +133,7 @@ Scaffolds a new operator project. * `--type` Type of operator to initialize: "ansible" or "go" (default "go"). Also requires the following flags if `--type=ansible` * `--api-version` CRD APIVersion in the format `$GROUP_NAME/$VERSION` (e.g app.example.com/v1alpha1) * `--kind` CRD Kind. (e.g AppService) +* `--cluster-scoped` Initialize the operator to be cluster-scoped instead of namespace-scoped * `-h, --help` - help for new ### Example diff --git a/doc/user-guide.md b/doc/user-guide.md index 80236de2dcd..27dbb190f40 100644 --- a/doc/user-guide.md +++ b/doc/user-guide.md @@ -47,6 +47,15 @@ $ cd memcached-operator To learn about the project directory structure, see [project layout][layout_doc] doc. +#### Operator scope + +A namespace-scoped operator (the default) watches and manages resources in a single namespace, whereas a cluster-scoped operator watches and manages resources cluster-wide. Namespace-scoped operators are preferred because of their flexibility. They enable decoupled upgrades, namespace isolation for failures and monitoring, and differing API definitions. However, there are use cases where a cluster-scoped operator may make sense. For example, the [cert-manager](https://github.com/jetstack/cert-manager) operator is often deployed with cluster-scoped permissions and watches so that it can manage issuing certificates for an entire cluster. + +If you'd like to create your memcached-operator project to be cluster-scoped use the following `operator-sdk new` command instead: +``` +$ operator-sdk new memcached-operator --cluster-scoped +``` + ### Manager The main program for the operator `cmd/manager/main.go` initializes and runs the [Manager][manager_go_doc]. @@ -193,10 +202,17 @@ $ sed -i 's|REPLACE_IMAGE|quay.io/example/memcached-operator:v0.0.1|g' deploy/op $ docker push quay.io/example/memcached-operator:v0.0.1 ``` +If you created your operator using `--cluster-scoped=true`, update the service account namespace in the generated `ClusterRoleBinding` to match where you are deploying your operator. +``` +$ export OPERATOR_NAMESPACE=$(kubectl config view --minify -o jsonpath='{.contexts[0].context.namespace}') +$ sed -i "s|REPLACE_NAMESPACE|$OPERATOR_NAMESPACE|g" deploy/role_binding.yaml +``` + **Note** -If you are performing these steps on OSX, use the following command: +If you are performing these steps on OSX, use the following commands instead: ``` $ sed -i "" 's|REPLACE_IMAGE|quay.io/example/memcached-operator:v0.0.1|g' deploy/operator.yaml +$ sed -i "" "s|REPLACE_NAMESPACE|$OPERATOR_NAMESPACE|g" deploy/role_binding.yaml ``` The Deployment manifest is generated at `deploy/operator.yaml`. Be sure to update the deployment image as shown above since the default is just a placeholder. diff --git a/pkg/scaffold/ansible/operator.go b/pkg/scaffold/ansible/operator.go index c4031e7eb0d..869f67c8a09 100644 --- a/pkg/scaffold/ansible/operator.go +++ b/pkg/scaffold/ansible/operator.go @@ -23,6 +23,8 @@ import ( type Operator struct { input.Input + + IsClusterScoped bool } func (s *Operator) GetInput() (input.Input, error) { @@ -58,9 +60,13 @@ spec: imagePullPolicy: Always env: - name: WATCH_NAMESPACE + {{- if .IsClusterScoped }} + value: "" + {{- else }} valueFrom: fieldRef: fieldPath: metadata.namespace + {{- end}} - name: OPERATOR_NAME value: "{{.ProjectName}}" ` diff --git a/pkg/scaffold/operator.go b/pkg/scaffold/operator.go index f89eaf8d140..12c4bae2777 100644 --- a/pkg/scaffold/operator.go +++ b/pkg/scaffold/operator.go @@ -24,6 +24,8 @@ const OperatorYamlFile = "operator.yaml" type Operator struct { input.Input + + IsClusterScoped bool } func (s *Operator) GetInput() (input.Input, error) { @@ -61,9 +63,13 @@ spec: imagePullPolicy: Always env: - name: WATCH_NAMESPACE + {{- if .IsClusterScoped }} + value: "" + {{- else }} valueFrom: fieldRef: fieldPath: metadata.namespace + {{- end}} - name: POD_NAME valueFrom: fieldRef: diff --git a/pkg/scaffold/operator_test.go b/pkg/scaffold/operator_test.go index 8b5bd9757bd..c8f98f2ea94 100644 --- a/pkg/scaffold/operator_test.go +++ b/pkg/scaffold/operator_test.go @@ -33,6 +33,19 @@ func TestOperator(t *testing.T) { } } +func TestOperatorClusterScoped(t *testing.T) { + s, buf := setupScaffoldAndWriter() + err := s.Execute(appConfig, &Operator{IsClusterScoped: true}) + if err != nil { + t.Fatalf("failed to execute the scaffold: (%v)", err) + } + + if operatorClusterScopedExp != buf.String() { + diffs := testutil.Diff(operatorClusterScopedExp, buf.String()) + t.Fatalf("expected vs actual differs.\n%v", diffs) + } +} + const operatorExp = `apiVersion: apps/v1 kind: Deployment metadata: @@ -70,3 +83,39 @@ spec: - name: OPERATOR_NAME value: "app-operator" ` + +const operatorClusterScopedExp = `apiVersion: apps/v1 +kind: Deployment +metadata: + name: app-operator +spec: + replicas: 1 + selector: + matchLabels: + name: app-operator + template: + metadata: + labels: + name: app-operator + spec: + serviceAccountName: app-operator + containers: + - name: app-operator + # Replace this with the built image name + image: REPLACE_IMAGE + ports: + - containerPort: 60000 + name: metrics + command: + - app-operator + imagePullPolicy: Always + env: + - name: WATCH_NAMESPACE + value: "" + - name: POD_NAME + valueFrom: + fieldRef: + fieldPath: metadata.name + - name: OPERATOR_NAME + value: "app-operator" +` diff --git a/pkg/scaffold/role.go b/pkg/scaffold/role.go index 1cf9166761e..b77b24db2f7 100644 --- a/pkg/scaffold/role.go +++ b/pkg/scaffold/role.go @@ -34,6 +34,8 @@ const RoleYamlFile = "role.yaml" type Role struct { input.Input + + IsClusterScoped bool } func (s *Role) GetInput() (input.Input, error) { @@ -148,7 +150,7 @@ func UpdateRoleForResource(r *Resource, absProjectPath string) error { return nil } -const roleTemplate = `kind: Role +const roleTemplate = `kind: {{if .IsClusterScoped}}Cluster{{end}}Role apiVersion: rbac.authorization.k8s.io/v1 metadata: name: {{.ProjectName}} diff --git a/pkg/scaffold/role_test.go b/pkg/scaffold/role_test.go index 9b5c30d076e..30897f0f0c7 100644 --- a/pkg/scaffold/role_test.go +++ b/pkg/scaffold/role_test.go @@ -33,6 +33,19 @@ func TestRole(t *testing.T) { } } +func TestRoleClusterScoped(t *testing.T) { + s, buf := setupScaffoldAndWriter() + err := s.Execute(appConfig, &Role{IsClusterScoped: true}) + if err != nil { + t.Fatalf("failed to execute the scaffold: (%v)", err) + } + + if clusterroleExp != buf.String() { + diffs := testutil.Diff(clusterroleExp, buf.String()) + t.Fatalf("expected vs actual differs.\n%v", diffs) + } +} + const roleExp = `kind: Role apiVersion: rbac.authorization.k8s.io/v1 metadata: @@ -67,3 +80,38 @@ rules: - "get" - "create" ` + +const clusterroleExp = `kind: ClusterRole +apiVersion: rbac.authorization.k8s.io/v1 +metadata: + name: app-operator +rules: +- apiGroups: + - "" + resources: + - pods + - services + - endpoints + - persistentvolumeclaims + - events + - configmaps + - secrets + verbs: + - "*" +- apiGroups: + - apps + resources: + - deployments + - daemonsets + - replicasets + - statefulsets + verbs: + - "*" +- apiGroups: + - monitoring.coreos.com + resources: + - servicemonitors + verbs: + - "get" + - "create" +` diff --git a/pkg/scaffold/rolebinding.go b/pkg/scaffold/rolebinding.go index a9718354f37..96d85f82cbe 100644 --- a/pkg/scaffold/rolebinding.go +++ b/pkg/scaffold/rolebinding.go @@ -24,6 +24,8 @@ const RoleBindingYamlFile = "role_binding.yaml" type RoleBinding struct { input.Input + + IsClusterScoped bool } func (s *RoleBinding) GetInput() (input.Input, error) { @@ -34,15 +36,19 @@ func (s *RoleBinding) GetInput() (input.Input, error) { return s.Input, nil } -const roleBindingTemplate = `kind: RoleBinding +const roleBindingTemplate = `kind: {{if .IsClusterScoped}}Cluster{{end}}RoleBinding apiVersion: rbac.authorization.k8s.io/v1 metadata: name: {{.ProjectName}} subjects: - kind: ServiceAccount name: {{.ProjectName}} + {{- if .IsClusterScoped }} + # Replace this with the namespace the operator is deployed in. + namespace: REPLACE_NAMESPACE + {{- end }} roleRef: - kind: Role + kind: {{if .IsClusterScoped}}Cluster{{end}}Role name: {{.ProjectName}} apiGroup: rbac.authorization.k8s.io ` diff --git a/pkg/scaffold/rolebinding_test.go b/pkg/scaffold/rolebinding_test.go index cdc39ea322d..c8eefb4d4ec 100644 --- a/pkg/scaffold/rolebinding_test.go +++ b/pkg/scaffold/rolebinding_test.go @@ -33,6 +33,19 @@ func TestRoleBinding(t *testing.T) { } } +func TestRoleBindingClusterScoped(t *testing.T) { + s, buf := setupScaffoldAndWriter() + err := s.Execute(appConfig, &RoleBinding{IsClusterScoped: true}) + if err != nil { + t.Fatalf("failed to execute the scaffold: (%v)", err) + } + + if clusterrolebindingExp != buf.String() { + diffs := testutil.Diff(clusterrolebindingExp, buf.String()) + t.Fatalf("expected vs actual differs.\n%v", diffs) + } +} + const rolebindingExp = `kind: RoleBinding apiVersion: rbac.authorization.k8s.io/v1 metadata: @@ -45,3 +58,18 @@ roleRef: name: app-operator apiGroup: rbac.authorization.k8s.io ` + +const clusterrolebindingExp = `kind: ClusterRoleBinding +apiVersion: rbac.authorization.k8s.io/v1 +metadata: + name: app-operator +subjects: +- kind: ServiceAccount + name: app-operator + # Replace this with the namespace the operator is deployed in. + namespace: REPLACE_NAMESPACE +roleRef: + kind: ClusterRole + name: app-operator + apiGroup: rbac.authorization.k8s.io +`