Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

commands/.../new.go;pkg/scaffold: adding support for ClusterRole and ClusterRoleBinding #747

Merged
merged 7 commits into from
Nov 15, 2018
26 changes: 20 additions & 6 deletions commands/operator-sdk/cmd/new.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand All @@ -64,6 +65,7 @@ var (
projectName string
skipGit bool
generatePlaybook bool
isClusterScoped bool
)

const (
Expand Down Expand Up @@ -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{},
Expand Down Expand Up @@ -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,
},
Expand Down
22 changes: 17 additions & 5 deletions doc/ansible/user-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand All @@ -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
Expand Down
1 change: 1 addition & 0 deletions doc/sdk-cli-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
18 changes: 17 additions & 1 deletion doc/user-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -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].

Expand Down Expand Up @@ -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.
Expand Down
6 changes: 6 additions & 0 deletions pkg/scaffold/ansible/operator.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ import (

type Operator struct {
input.Input

IsClusterScoped bool
}

func (s *Operator) GetInput() (input.Input, error) {
Expand Down Expand Up @@ -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}}"
`
6 changes: 6 additions & 0 deletions pkg/scaffold/operator.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ const OperatorYamlFile = "operator.yaml"

type Operator struct {
input.Input

IsClusterScoped bool
}

func (s *Operator) GetInput() (input.Input, error) {
Expand Down Expand Up @@ -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:
Expand Down
49 changes: 49 additions & 0 deletions pkg/scaffold/operator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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"
`
4 changes: 3 additions & 1 deletion pkg/scaffold/role.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ const RoleYamlFile = "role.yaml"

type Role struct {
input.Input

IsClusterScoped bool
}

func (s *Role) GetInput() (input.Input, error) {
Expand Down Expand Up @@ -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}}
Expand Down
48 changes: 48 additions & 0 deletions pkg/scaffold/role_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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"
`
10 changes: 8 additions & 2 deletions pkg/scaffold/rolebinding.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ const RoleBindingYamlFile = "role_binding.yaml"

type RoleBinding struct {
input.Input

IsClusterScoped bool
}

func (s *RoleBinding) GetInput() (input.Input, error) {
Expand All @@ -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
`
28 changes: 28 additions & 0 deletions pkg/scaffold/rolebinding_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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
`