Skip to content

Commit 417f13c

Browse files
authored
Submit RBAC credentials during initial Event processing (zalando#344)
* During initial Event processing submit the service account for pods and bind it to a cluster role that allows Patroni to successfully start. The cluster role is assumed to be created by the k8s cluster administrator.
1 parent 3a9378d commit 417f13c

File tree

10 files changed

+193
-72
lines changed

10 files changed

+193
-72
lines changed

docs/administrator.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -90,13 +90,13 @@ namespace. The operator performs **no** further syncing of this account.
9090

9191
## Role-based access control for the operator
9292

93-
The `manifests/operator-rbac.yaml` defines cluster roles and bindings needed
93+
The `manifests/operator-service-account-rbac.yaml` defines cluster roles and bindings needed
9494
for the operator to function under access control restrictions. To deploy the
9595
operator with this RBAC policy use:
9696

9797
```bash
9898
$ kubectl create -f manifests/configmap.yaml
99-
$ kubectl create -f manifests/operator-rbac.yaml
99+
$ kubectl create -f manifests/operator-service-account-rbac.yaml
100100
$ kubectl create -f manifests/postgres-operator.yaml
101101
$ kubectl create -f manifests/minimal-postgres-manifest.yaml
102102
```

docs/reference/operator_parameters.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -110,8 +110,10 @@ configuration they are grouped under the `kubernetes` key.
110110
* **pod_service_account_definition**
111111
The operator tries to create the pod Service Account in the namespace that
112112
doesn't define such an account using the YAML definition provided by this
113-
option. If not defined, a simple definition that contains only the name will
114-
be used. The default is empty.
113+
option. If not defined, a simple definition that contains only the name will be used. The default is empty.
114+
115+
* **pod_service_account_role_binding_definition**
116+
This definition must bind pod service account to a role with permission sufficient for the pods to start and for Patroni to access k8s endpoints; service account on its own lacks any such rights starting with k8s v1.8. If not excplicitly defined by the user, a simple definition that binds the account to the operator's own 'zalando-postgres-operator' cluster role will be used. The default is empty.
115117

116118
* **pod_terminate_grace_period**
117119
Patroni pods are [terminated

manifests/operator-service-account-rbac.yaml

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,21 @@ rules:
123123
verbs:
124124
- get
125125
- create
126+
- apiGroups:
127+
- "rbac.authorization.k8s.io"
128+
resources:
129+
- rolebindings
130+
verbs:
131+
- get
132+
- create
133+
- apiGroups:
134+
- "rbac.authorization.k8s.io"
135+
resources:
136+
- clusterroles
137+
verbs:
138+
- bind
139+
resourceNames:
140+
- zalando-postgres-operator
126141

127142
---
128143
apiVersion: rbac.authorization.k8s.io/v1

pkg/cluster/cluster.go

Lines changed: 6 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import (
2828
"github.com/zalando-incubator/postgres-operator/pkg/util/patroni"
2929
"github.com/zalando-incubator/postgres-operator/pkg/util/teams"
3030
"github.com/zalando-incubator/postgres-operator/pkg/util/users"
31+
rbacv1beta1 "k8s.io/client-go/pkg/apis/rbac/v1beta1"
3132
)
3233

3334
var (
@@ -39,10 +40,11 @@ var (
3940

4041
// Config contains operator-wide clients and configuration used from a cluster. TODO: remove struct duplication.
4142
type Config struct {
42-
OpConfig config.Config
43-
RestConfig *rest.Config
44-
InfrastructureRoles map[string]spec.PgUser // inherited from the controller
45-
PodServiceAccount *v1.ServiceAccount
43+
OpConfig config.Config
44+
RestConfig *rest.Config
45+
InfrastructureRoles map[string]spec.PgUser // inherited from the controller
46+
PodServiceAccount *v1.ServiceAccount
47+
PodServiceAccountRoleBinding *rbacv1beta1.RoleBinding
4648
}
4749

4850
type kubeResources struct {
@@ -199,39 +201,6 @@ func (c *Cluster) initUsers() error {
199201
return nil
200202
}
201203

202-
/*
203-
Ensures the service account required by StatefulSets to create pods exists in a namespace before a PG cluster is created there so that a user does not have to deploy the account manually.
204-
205-
The operator does not sync these accounts after creation.
206-
*/
207-
func (c *Cluster) createPodServiceAccounts() error {
208-
209-
podServiceAccountName := c.Config.OpConfig.PodServiceAccountName
210-
_, err := c.KubeClient.ServiceAccounts(c.Namespace).Get(podServiceAccountName, metav1.GetOptions{})
211-
212-
if err != nil {
213-
214-
c.setProcessName(fmt.Sprintf("creating pod service account in the namespace %v", c.Namespace))
215-
216-
c.logger.Infof("the pod service account %q cannot be retrieved in the namespace %q. Trying to deploy the account.", podServiceAccountName, c.Namespace)
217-
218-
// get a separate copy of service account
219-
// to prevent a race condition when setting a namespace for many clusters
220-
sa := *c.PodServiceAccount
221-
_, err = c.KubeClient.ServiceAccounts(c.Namespace).Create(&sa)
222-
if err != nil {
223-
return fmt.Errorf("cannot deploy the pod service account %q defined in the config map to the %q namespace: %v", podServiceAccountName, c.Namespace, err)
224-
}
225-
226-
c.logger.Infof("successfully deployed the pod service account %q to the %q namespace", podServiceAccountName, c.Namespace)
227-
228-
} else {
229-
c.logger.Infof("successfully found the service account %q used to create pods to the namespace %q", podServiceAccountName, c.Namespace)
230-
}
231-
232-
return nil
233-
}
234-
235204
// Create creates the new kubernetes objects associated with the cluster.
236205
func (c *Cluster) Create() error {
237206
c.mu.Lock()
@@ -298,11 +267,6 @@ func (c *Cluster) Create() error {
298267
}
299268
c.logger.Infof("pod disruption budget %q has been successfully created", util.NameFromMeta(pdb.ObjectMeta))
300269

301-
if err = c.createPodServiceAccounts(); err != nil {
302-
return fmt.Errorf("could not create pod service account %v : %v", c.OpConfig.PodServiceAccountName, err)
303-
}
304-
c.logger.Infof("pod service accounts have been successfully synced")
305-
306270
if c.Statefulset != nil {
307271
return fmt.Errorf("statefulset already exists in the cluster")
308272
}

pkg/controller/controller.go

Lines changed: 53 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
"k8s.io/apimachinery/pkg/types"
1111
"k8s.io/client-go/kubernetes/scheme"
1212
"k8s.io/client-go/pkg/api/v1"
13+
rbacv1beta1 "k8s.io/client-go/pkg/apis/rbac/v1beta1"
1314
"k8s.io/client-go/tools/cache"
1415

1516
"github.com/zalando-incubator/postgres-operator/pkg/apiserver"
@@ -52,7 +53,9 @@ type Controller struct {
5253

5354
workerLogs map[uint32]ringlog.RingLogger
5455

55-
PodServiceAccount *v1.ServiceAccount
56+
PodServiceAccount *v1.ServiceAccount
57+
PodServiceAccountRoleBinding *rbacv1beta1.RoleBinding
58+
namespacesWithDefinedRBAC sync.Map
5659
}
5760

5861
// NewController creates a new controller
@@ -162,6 +165,53 @@ func (c *Controller) initPodServiceAccount() {
162165
// actual service accounts are deployed at the time of Postgres/Spilo cluster creation
163166
}
164167

168+
func (c *Controller) initRoleBinding() {
169+
170+
// service account on its own lacks any rights starting with k8s v1.8
171+
// operator binds it to the cluster role with sufficient priviliges
172+
// we assume the role is created by the k8s administrator
173+
if c.opConfig.PodServiceAccountRoleBindingDefinition == "" {
174+
c.opConfig.PodServiceAccountRoleBindingDefinition = `
175+
{
176+
"apiVersion": "rbac.authorization.k8s.io/v1beta1",
177+
"kind": "RoleBinding",
178+
"metadata": {
179+
"name": "zalando-postgres-operator"
180+
},
181+
"roleRef": {
182+
"apiGroup": "rbac.authorization.k8s.io",
183+
"kind": "ClusterRole",
184+
"name": "zalando-postgres-operator"
185+
},
186+
"subjects": [
187+
{
188+
"kind": "ServiceAccount",
189+
"name": "operator"
190+
}
191+
]
192+
}`
193+
}
194+
c.logger.Info("Parse role bindings")
195+
// re-uses k8s internal parsing. See k8s client-go issue #193 for explanation
196+
decode := scheme.Codecs.UniversalDeserializer().Decode
197+
obj, groupVersionKind, err := decode([]byte(c.opConfig.PodServiceAccountRoleBindingDefinition), nil, nil)
198+
199+
switch {
200+
case err != nil:
201+
panic(fmt.Errorf("Unable to parse the definiton of the role binding for the pod service account definiton from the operator config map: %v", err))
202+
case groupVersionKind.Kind != "RoleBinding":
203+
panic(fmt.Errorf("role binding definiton in the operator config map defines another type of resource: %v", groupVersionKind.Kind))
204+
default:
205+
c.PodServiceAccountRoleBinding = obj.(*rbacv1beta1.RoleBinding)
206+
c.PodServiceAccountRoleBinding.Namespace = ""
207+
c.PodServiceAccountRoleBinding.Subjects[0].Name = c.PodServiceAccount.Name
208+
c.logger.Info("successfully parsed")
209+
210+
}
211+
212+
// actual roles bindings are deployed at the time of Postgres/Spilo cluster creation
213+
}
214+
165215
func (c *Controller) initController() {
166216
c.initClients()
167217

@@ -176,6 +226,8 @@ func (c *Controller) initController() {
176226
}
177227
} else {
178228
c.initOperatorConfig()
229+
c.initPodServiceAccount()
230+
c.initRoleBinding()
179231
}
180232

181233
c.modifyConfigFromEnvironment()

pkg/controller/operator_config.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,11 @@ import (
44
"encoding/json"
55
"fmt"
66

7+
"time"
8+
79
"github.com/zalando-incubator/postgres-operator/pkg/util/config"
810
"github.com/zalando-incubator/postgres-operator/pkg/util/constants"
911
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
10-
"time"
1112
)
1213

1314
func (c *Controller) readOperatorConfigurationFromCRD(configObjectNamespace, configObjectName string) (*config.OperatorConfiguration, error) {
@@ -49,6 +50,7 @@ func (c *Controller) importConfigurationFromCRD(fromCRD *config.OperatorConfigur
4950

5051
result.PodServiceAccountName = fromCRD.Kubernetes.PodServiceAccountName
5152
result.PodServiceAccountDefinition = fromCRD.Kubernetes.PodServiceAccountDefinition
53+
result.PodServiceAccountRoleBindingDefinition = fromCRD.Kubernetes.PodServiceAccountRoleBindingDefinition
5254
result.PodTerminateGracePeriod = time.Duration(fromCRD.Kubernetes.PodTerminateGracePeriod)
5355
result.WatchedNamespace = fromCRD.Kubernetes.WatchedNamespace
5456
result.PDBNameFormat = fromCRD.Kubernetes.PDBNameFormat

pkg/controller/postgresql.go

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import (
2020
"github.com/zalando-incubator/postgres-operator/pkg/spec"
2121
"github.com/zalando-incubator/postgres-operator/pkg/util"
2222
"github.com/zalando-incubator/postgres-operator/pkg/util/constants"
23+
"github.com/zalando-incubator/postgres-operator/pkg/util/k8sutil"
2324
"github.com/zalando-incubator/postgres-operator/pkg/util/ringlog"
2425
)
2526

@@ -179,6 +180,11 @@ func (c *Controller) processEvent(event spec.ClusterEvent) {
179180
c.warnOnDeprecatedPostgreSQLSpecParameters(&event.NewSpec.Spec)
180181
c.mergeDeprecatedPostgreSQLSpecParameters(&event.NewSpec.Spec)
181182
}
183+
184+
if err := c.submitRBACCredentials(event); err != nil {
185+
c.logger.Warnf("Pods and/or Patroni may misfunction due to the lack of permissions: %v", err)
186+
}
187+
182188
}
183189

184190
switch event.EventType {
@@ -457,3 +463,78 @@ func (c *Controller) postgresqlDelete(obj interface{}) {
457463

458464
c.queueClusterEvent(pg, nil, spec.EventDelete)
459465
}
466+
467+
/*
468+
Ensures the pod service account and role bindings exists in a namespace before a PG cluster is created there so that a user does not have to deploy these credentials manually.
469+
StatefulSets require the service account to create pods; Patroni requires relevant RBAC bindings to access endpoints.
470+
471+
The operator does not sync accounts/role bindings after creation.
472+
*/
473+
func (c *Controller) submitRBACCredentials(event spec.ClusterEvent) error {
474+
475+
namespace := event.NewSpec.GetNamespace()
476+
if _, ok := c.namespacesWithDefinedRBAC.Load(namespace); ok {
477+
return nil
478+
}
479+
480+
if err := c.createPodServiceAccount(namespace); err != nil {
481+
return fmt.Errorf("could not create pod service account %v : %v", c.opConfig.PodServiceAccountName, err)
482+
}
483+
484+
if err := c.createRoleBindings(namespace); err != nil {
485+
return fmt.Errorf("could not create role binding %v : %v", c.PodServiceAccountRoleBinding.Name, err)
486+
}
487+
488+
c.namespacesWithDefinedRBAC.Store(namespace, true)
489+
return nil
490+
}
491+
492+
func (c *Controller) createPodServiceAccount(namespace string) error {
493+
494+
podServiceAccountName := c.opConfig.PodServiceAccountName
495+
_, err := c.KubeClient.ServiceAccounts(namespace).Get(podServiceAccountName, metav1.GetOptions{})
496+
if k8sutil.ResourceNotFound(err) {
497+
498+
c.logger.Infof(fmt.Sprintf("creating pod service account in the namespace %v", namespace))
499+
500+
// get a separate copy of service account
501+
// to prevent a race condition when setting a namespace for many clusters
502+
sa := *c.PodServiceAccount
503+
if _, err = c.KubeClient.ServiceAccounts(namespace).Create(&sa); err != nil {
504+
return fmt.Errorf("cannot deploy the pod service account %v defined in the config map to the %v namespace: %v", podServiceAccountName, namespace, err)
505+
}
506+
507+
c.logger.Infof("successfully deployed the pod service account %v to the %v namespace", podServiceAccountName, namespace)
508+
} else if k8sutil.ResourceAlreadyExists(err) {
509+
return nil
510+
}
511+
512+
return err
513+
}
514+
515+
func (c *Controller) createRoleBindings(namespace string) error {
516+
517+
podServiceAccountName := c.opConfig.PodServiceAccountName
518+
podServiceAccountRoleBindingName := c.PodServiceAccountRoleBinding.Name
519+
520+
_, err := c.KubeClient.RoleBindings(namespace).Get(podServiceAccountRoleBindingName, metav1.GetOptions{})
521+
if k8sutil.ResourceNotFound(err) {
522+
523+
c.logger.Infof("Creating the role binding %v in the namespace %v", podServiceAccountRoleBindingName, namespace)
524+
525+
// get a separate copy of role binding
526+
// to prevent a race condition when setting a namespace for many clusters
527+
rb := *c.PodServiceAccountRoleBinding
528+
_, err = c.KubeClient.RoleBindings(namespace).Create(&rb)
529+
if err != nil {
530+
return fmt.Errorf("cannot bind the pod service account %q defined in the config map to the cluster role in the %q namespace: %v", podServiceAccountName, namespace, err)
531+
}
532+
533+
c.logger.Infof("successfully deployed the role binding for the pod service account %q to the %q namespace", podServiceAccountName, namespace)
534+
535+
} else if k8sutil.ResourceAlreadyExists(err) {
536+
return nil
537+
}
538+
539+
return err
540+
}

pkg/util/config/config.go

Lines changed: 14 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -75,19 +75,20 @@ type Config struct {
7575
// default name `operator` enables backward compatibility with the older ServiceAccountName field
7676
PodServiceAccountName string `name:"pod_service_account_name" default:"operator"`
7777
// value of this string must be valid JSON or YAML; see initPodServiceAccount
78-
PodServiceAccountDefinition string `name:"pod_service_account_definition" default:""`
79-
DbHostedZone string `name:"db_hosted_zone" default:"db.example.com"`
80-
AWSRegion string `name:"aws_region" default:"eu-central-1"`
81-
WALES3Bucket string `name:"wal_s3_bucket"`
82-
LogS3Bucket string `name:"log_s3_bucket"`
83-
KubeIAMRole string `name:"kube_iam_role"`
84-
DebugLogging bool `name:"debug_logging" default:"true"`
85-
EnableDBAccess bool `name:"enable_database_access" default:"true"`
86-
EnableTeamsAPI bool `name:"enable_teams_api" default:"true"`
87-
EnableTeamSuperuser bool `name:"enable_team_superuser" default:"false"`
88-
TeamAdminRole string `name:"team_admin_role" default:"admin"`
89-
EnableMasterLoadBalancer bool `name:"enable_master_load_balancer" default:"true"`
90-
EnableReplicaLoadBalancer bool `name:"enable_replica_load_balancer" default:"false"`
78+
PodServiceAccountDefinition string `name:"pod_service_account_definition" default:""`
79+
PodServiceAccountRoleBindingDefinition string `name:"pod_service_account_role_binding_definition" default:""`
80+
DbHostedZone string `name:"db_hosted_zone" default:"db.example.com"`
81+
AWSRegion string `name:"aws_region" default:"eu-central-1"`
82+
WALES3Bucket string `name:"wal_s3_bucket"`
83+
LogS3Bucket string `name:"log_s3_bucket"`
84+
KubeIAMRole string `name:"kube_iam_role"`
85+
DebugLogging bool `name:"debug_logging" default:"true"`
86+
EnableDBAccess bool `name:"enable_database_access" default:"true"`
87+
EnableTeamsAPI bool `name:"enable_teams_api" default:"true"`
88+
EnableTeamSuperuser bool `name:"enable_team_superuser" default:"false"`
89+
TeamAdminRole string `name:"team_admin_role" default:"admin"`
90+
EnableMasterLoadBalancer bool `name:"enable_master_load_balancer" default:"true"`
91+
EnableReplicaLoadBalancer bool `name:"enable_replica_load_balancer" default:"false"`
9192
// deprecated and kept for backward compatibility
9293
EnableLoadBalancer *bool `name:"enable_load_balancer"`
9394
MasterDNSNameFormat stringTemplate `name:"master_dns_name_format" default:"{cluster}.{team}.{hostedzone}"`

pkg/util/config/crd_config.go

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -31,17 +31,18 @@ type PostgresUsersConfiguration struct {
3131
type KubernetesMetaConfiguration struct {
3232
PodServiceAccountName string `json:"pod_service_account_name,omitempty"`
3333
// TODO: change it to the proper json
34-
PodServiceAccountDefinition string `json:"pod_service_account_definition,omitempty"`
35-
PodTerminateGracePeriod spec.Duration `json:"pod_terminate_grace_period,omitempty"`
36-
WatchedNamespace string `json:"watched_namespace,omitempty"`
37-
PDBNameFormat stringTemplate `json:"pdb_name_format,omitempty"`
38-
SecretNameTemplate stringTemplate `json:"secret_name_template,omitempty"`
39-
OAuthTokenSecretName spec.NamespacedName `json:"oauth_token_secret_name,omitempty"`
40-
InfrastructureRolesSecretName spec.NamespacedName `json:"infrastructure_roles_secret_name,omitempty"`
41-
PodRoleLabel string `json:"pod_role_label,omitempty"`
42-
ClusterLabels map[string]string `json:"cluster_labels,omitempty"`
43-
ClusterNameLabel string `json:"cluster_name_label,omitempty"`
44-
NodeReadinessLabel map[string]string `json:"node_readiness_label,omitempty"`
34+
PodServiceAccountDefinition string `json:"pod_service_account_definition,omitempty"`
35+
PodServiceAccountRoleBindingDefinition string `json:"pod_service_account_role_binding_definition,omitempty"`
36+
PodTerminateGracePeriod spec.Duration `json:"pod_terminate_grace_period,omitempty"`
37+
WatchedNamespace string `json:"watched_namespace,omitempty"`
38+
PDBNameFormat stringTemplate `json:"pdb_name_format,omitempty"`
39+
SecretNameTemplate stringTemplate `json:"secret_name_template,omitempty"`
40+
OAuthTokenSecretName spec.NamespacedName `json:"oauth_token_secret_name,omitempty"`
41+
InfrastructureRolesSecretName spec.NamespacedName `json:"infrastructure_roles_secret_name,omitempty"`
42+
PodRoleLabel string `json:"pod_role_label,omitempty"`
43+
ClusterLabels map[string]string `json:"cluster_labels,omitempty"`
44+
ClusterNameLabel string `json:"cluster_name_label,omitempty"`
45+
NodeReadinessLabel map[string]string `json:"node_readiness_label,omitempty"`
4546
// TODO: use a proper toleration structure?
4647
PodToleration map[string]string `json:"toleration,omitempty"`
4748
// TODO: use namespacedname

0 commit comments

Comments
 (0)