diff --git a/charts/postgres-operator/crds/operatorconfigurations.yaml b/charts/postgres-operator/crds/operatorconfigurations.yaml index 9725c2708..e5dc266c3 100644 --- a/charts/postgres-operator/crds/operatorconfigurations.yaml +++ b/charts/postgres-operator/crds/operatorconfigurations.yaml @@ -148,6 +148,8 @@ spec: type: string pod_service_account_name: type: string + pod_service_account_role_definition: + type: string pod_service_account_role_binding_definition: type: string pod_terminate_grace_period: diff --git a/charts/postgres-operator/templates/clusterrole.yaml b/charts/postgres-operator/templates/clusterrole.yaml index f7fe1634c..58feedf10 100644 --- a/charts/postgres-operator/templates/clusterrole.yaml +++ b/charts/postgres-operator/templates/clusterrole.yaml @@ -9,6 +9,7 @@ metadata: app.kubernetes.io/managed-by: {{ .Release.Service }} app.kubernetes.io/instance: {{ .Release.Name }} rules: +# all verbs allowed for custom operator resources - apiGroups: - acid.zalan.do resources: @@ -16,7 +17,15 @@ rules: - postgresqls/status - operatorconfigurations verbs: - - "*" + - create + - delete + - deletecollection + - get + - list + - patch + - update + - watch +# to create or get/update CRDs when starting up - apiGroups: - apiextensions.k8s.io resources: @@ -26,12 +35,14 @@ rules: - get - patch - update +# to read configuration from ConfigMaps - apiGroups: - "" resources: - configmaps verbs: - get +# to manage endpoints which are also used by Patroni - apiGroups: - "" resources: @@ -43,7 +54,9 @@ rules: - get - list - patch - - watch # needed if zalando-postgres-operator account is used for pods as well + - update + - watch +# to CRUD secrets for database access - apiGroups: - "" resources: @@ -53,6 +66,7 @@ rules: - update - delete - get +# to check nodes for node readiness label - apiGroups: - "" resources: @@ -61,6 +75,7 @@ rules: - get - list - watch +# to read or delete existing PVCs. Creation via StatefulSet - apiGroups: - "" resources: @@ -69,6 +84,7 @@ rules: - delete - get - list + # to read existing PVs. Creation should be done via dynamic provisioning - apiGroups: - "" resources: @@ -77,6 +93,7 @@ rules: - get - list - update # only for resizing AWS volumes +# to watch Spilo pods and do rolling updates. Creation via StatefulSet - apiGroups: - "" resources: @@ -85,14 +102,17 @@ rules: - delete - get - list - - watch - patch + - update + - watch +# to resize the filesystem in Spilo pods when increasing volume size - apiGroups: - "" resources: - pods/exec verbs: - create +# to CRUD services to point to Postgres cluster instances - apiGroups: - "" resources: @@ -102,6 +122,7 @@ rules: - delete - get - patch +# to CRUD the StatefulSet which controls the Postgres cluster instances - apiGroups: - apps resources: @@ -112,12 +133,26 @@ rules: - get - list - patch +# to CRUD cron jobs for logical backups +- apiGroups: + - batch + resources: + - cronjobs + verbs: + - create + - delete + - get + - list + - patch + - update +# to get namespaces operator resources can run in - apiGroups: - "" resources: - namespaces verbs: - get +# to define PDBs. Update happens via delete/create - apiGroups: - policy resources: @@ -126,6 +161,7 @@ rules: - create - delete - get +# to create ServiceAccounts in each namespace the operator watches - apiGroups: - "" resources: @@ -133,30 +169,22 @@ rules: verbs: - get - create +# to create roles and role bindings to the pod service account - apiGroups: - - "rbac.authorization.k8s.io" + - rbac.authorization.k8s.io resources: - rolebindings + - roles verbs: - get - create +# needed to grant to namespaced roles when pods run in privileged mode - apiGroups: - - "rbac.authorization.k8s.io" + - extensions resources: - - clusterroles - verbs: - - bind + - podsecuritypolicies resourceNames: - - {{ include "postgres-operator.serviceAccountName" . }} -- apiGroups: - - batch - resources: - - cronjobs # enables logical backups + - privileged verbs: - - create - - delete - - get - - list - - patch - - update + - use {{ end }} diff --git a/charts/postgres-operator/templates/clusterrolebinding.yaml b/charts/postgres-operator/templates/clusterrolebinding.yaml index bfa21b42f..dbf65d00e 100644 --- a/charts/postgres-operator/templates/clusterrolebinding.yaml +++ b/charts/postgres-operator/templates/clusterrolebinding.yaml @@ -14,8 +14,6 @@ roleRef: name: {{ include "postgres-operator.serviceAccountName" . }} subjects: - kind: ServiceAccount -# note: the cluster role binding needs to be defined -# for every namespace the operator service account lives in. name: {{ include "postgres-operator.serviceAccountName" . }} namespace: {{ .Release.Namespace }} {{ end }} diff --git a/charts/postgres-operator/templates/configmap.yaml b/charts/postgres-operator/templates/configmap.yaml index 95eeb9546..0b976294e 100644 --- a/charts/postgres-operator/templates/configmap.yaml +++ b/charts/postgres-operator/templates/configmap.yaml @@ -9,7 +9,6 @@ metadata: app.kubernetes.io/managed-by: {{ .Release.Service }} app.kubernetes.io/instance: {{ .Release.Name }} data: - pod_service_account_name: {{ include "postgres-operator.serviceAccountName" . }} {{ toYaml .Values.configGeneral | indent 2 }} {{ toYaml .Values.configUsers | indent 2 }} {{ toYaml .Values.configKubernetes | indent 2 }} diff --git a/charts/postgres-operator/templates/operatorconfiguration.yaml b/charts/postgres-operator/templates/operatorconfiguration.yaml index 6a301c1fb..06e9c7605 100644 --- a/charts/postgres-operator/templates/operatorconfiguration.yaml +++ b/charts/postgres-operator/templates/operatorconfiguration.yaml @@ -14,7 +14,6 @@ configuration: {{ toYaml .Values.configUsers | indent 4 }} kubernetes: oauth_token_secret_name: {{ template "postgres-operator.fullname" . }} - pod_service_account_name: {{ include "postgres-operator.serviceAccountName" . }} {{ toYaml .Values.configKubernetes | indent 4 }} postgres_pod_resources: {{ toYaml .Values.configPostgresPodResources | indent 4 }} diff --git a/charts/postgres-operator/values-crd.yaml b/charts/postgres-operator/values-crd.yaml index 1f9b5e495..b4e53918a 100644 --- a/charts/postgres-operator/values-crd.yaml +++ b/charts/postgres-operator/values-crd.yaml @@ -100,6 +100,17 @@ configKubernetes: pod_management_policy: "ordered_ready" # label assigned to the Postgres pods (and services/endpoints) pod_role_label: spilo-role + # service account definition as JSON/YAML string to be used by postgres cluster pods + # pod_service_account_definition: "" + + # name of service account to be used by postgres cluster pods + pod_service_account_name: "postgres-pod" + # role definition as JSON/YAML string to be used by postgres cluster pods + # pod_service_account_role_definition: "" + + # role binding definition as JSON/YAML string to be used by pod service account + # pod_service_account_role_binding_definition: "" + # Postgres pods are terminated forcefully after this timeout pod_terminate_grace_period: 5m # template for database user secrets generated by the operator diff --git a/charts/postgres-operator/values.yaml b/charts/postgres-operator/values.yaml index 1be5851d2..5a83399c7 100644 --- a/charts/postgres-operator/values.yaml +++ b/charts/postgres-operator/values.yaml @@ -93,6 +93,17 @@ configKubernetes: pod_management_policy: "ordered_ready" # label assigned to the Postgres pods (and services/endpoints) pod_role_label: spilo-role + # service account definition as JSON/YAML string to be used by postgres cluster pods + # pod_service_account_definition: "" + + # name of service account to be used by postgres cluster pods + pod_service_account_name: "postgres-pod" + # role definition as JSON/YAML string to be used by postgres cluster pods + # pod_service_account_role_definition: "" + + # role binding definition as JSON/YAML string to be used by pod service account + # pod_service_account_role_binding_definition: "" + # Postgres pods are terminated forcefully after this timeout pod_terminate_grace_period: 5m # template for database user secrets generated by the operator diff --git a/docs/administrator.md b/docs/administrator.md index 2e86193c0..5aee6f309 100644 --- a/docs/administrator.md +++ b/docs/administrator.md @@ -47,6 +47,12 @@ patching the CRD manifest: zk8 patch crd postgresqls.acid.zalan.do -p '{"spec":{"validation": null}}' ``` +## Non-default cluster domain + +If your cluster uses a DNS domain other than the default `cluster.local`, this +needs to be set in the operator configuration (`cluster_domain` variable). This +is used by the operator to connect to the clusters after creation. + ## Namespaces ### Select the namespace to deploy to @@ -89,30 +95,6 @@ lacks access rights to any of them (except K8s system namespaces like 'list pods' execute at the cluster scope and fail at the first violation of access rights. -The watched namespace also needs to have a (possibly different) service account -in the case database pods need to talk to the K8s API (e.g. when using -K8s-native configuration of Patroni). The operator checks that the -`pod_service_account_name` exists in the target namespace, and, if not, deploys -there the `pod_service_account_definition` from the operator -[`Config`](../pkg/util/config/config.go) with the default value of: - -```yaml -apiVersion: v1 -kind: ServiceAccount -metadata: - name: operator -``` - -In this definition, the operator overwrites the account's name to match -`pod_service_account_name` and the `default` namespace to match the target -namespace. The operator performs **no** further syncing of this account. - -## Non-default cluster domain - -If your cluster uses a DNS domain other than the default `cluster.local`, this -needs to be set in the operator configuration (`cluster_domain` variable). This -is used by the operator to connect to the clusters after creation. - ## Role-based access control for the operator The manifest [`operator-service-account-rbac.yaml`](../manifests/operator-service-account-rbac.yaml) @@ -127,14 +109,19 @@ kubectl create -f manifests/postgres-operator.yaml kubectl create -f manifests/minimal-postgres-manifest.yaml ``` -### Service account and cluster roles +### Namespaced service account and roles -Note that the service account is named `zalando-postgres-operator`. You may have -to change the `service_account_name` in the operator ConfigMap and -`serviceAccountName` in the `postgres-operator` deployment appropriately. This -is done intentionally to avoid breaking those setups that already work with the -default `operator` account. In the future the operator should ideally be run -under the `zalando-postgres-operator` service account. +For each namespace the operator watches it creates (or reads) a service account +to be used by the Postgres Pods when a new cluster is deployed. This service +account is bound to a namespaced Role via RoleBinding, which are also created +(or read) by the operator. The name and definitions of these resources can be +[configured](reference/operator_parameters.md#kubernetes-resources). +Note, that the operator performs **no** further syncing of them. + +Until v1.3.1, RoleBindings pointed to the operator ClusterRole by default. This +can still be configured but is not recommended as the Postgres Pods should only +run with the least privileges required for Patroni to work. By default, the +namespaced RBAC resources are named `postgres-pod`. ### Give K8s users access to create/list `postgresqls` diff --git a/docs/reference/operator_parameters.md b/docs/reference/operator_parameters.md index e3893ea31..a0a45cb91 100644 --- a/docs/reference/operator_parameters.md +++ b/docs/reference/operator_parameters.md @@ -152,21 +152,29 @@ configuration they are grouped under the `kubernetes` key. service account used by Patroni running on individual Pods to communicate with the operator. Required even if native Kubernetes support in Patroni is not used, because Patroni keeps pod labels in sync with the instance role. - The default is `operator`. + The default is `postgres-pod`. * **pod_service_account_definition** - The operator tries to create the pod Service Account in the namespace that - doesn't define such an account using the YAML definition provided by this - option. If not defined, a simple definition that contains only the name will - be used. The default is empty. + on Postgres cluster creation the operator tries to create the service account + for the Postgres pods if it does not exist in the namespace. The internal + default service account definition (defines only the name) can be overwritten + with this parameter. Make sure to provide a valid YAML or JSON string. The + default is empty. + +* **pod_service_account_role_definition** + operator will try to create a role in the namespace to be used by pod service + account. The internal default definition contains permissions to manage pods + and endpoints necessary for Patroni to work. Therefore, when overwriting the + definition with this parameter make sure to provide sufficient access rights + in a valid YAML/JSON string. The default is empty. * **pod_service_account_role_binding_definition** - 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 explicitly 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. + the created service account and role are referenced with a role binding. When + overwriting its definition with this parameters using a valid YAML/JSON string + check that the specified service account and role either exist in the K8s + cluster or will be created by the operator. While it's possible to also + reference cluster roles, the binding itself can only be of kind `RoleBinding`, + not `ClusterRoleBinding`. The default is empty. * **pod_terminate_grace_period** Postgres pods are [terminated forcefully](https://kubernetes.io/docs/concepts/workloads/pods/pod/#termination-of-pods) diff --git a/manifests/configmap.yaml b/manifests/configmap.yaml index d26c83edf..02cc0d6cb 100644 --- a/manifests/configmap.yaml +++ b/manifests/configmap.yaml @@ -63,7 +63,10 @@ data: pod_label_wait_timeout: 10m pod_management_policy: "ordered_ready" pod_role_label: spilo-role - pod_service_account_name: "zalando-postgres-operator" + # pod_service_account_definition: "" + pod_service_account_name: "postgres-pod" + # pod_service_account_role_definition: "" + # pod_service_account_role_binding_definition: '{"apiVersion":"rbac.authorization.k8s.io/v1","kind":"RoleBinding","metadata":{"name":"postgres-pod"},"roleRef":{"apiGroup":"rbac.authorization.k8s.io","kind": "ClusterRole","name": "postgres-pod"},"subjects":[{"kind": "ServiceAccount","name": "postgres-pod"}]}' pod_terminate_grace_period: 5m # postgres_superuser_teams: "postgres_superusers" # protected_role_names: "admin" diff --git a/manifests/operator-service-account-rbac.yaml b/manifests/operator-service-account-rbac.yaml index 5e43cc03b..86eef4ffe 100644 --- a/manifests/operator-service-account-rbac.yaml +++ b/manifests/operator-service-account-rbac.yaml @@ -1,14 +1,14 @@ apiVersion: v1 kind: ServiceAccount metadata: - name: zalando-postgres-operator + name: postgres-operator namespace: default --- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: - name: zalando-postgres-operator + name: postgres-operator rules: # all verbs allowed for custom operator resources - apiGroups: @@ -18,7 +18,14 @@ rules: - postgresqls/status - operatorconfigurations verbs: - - "*" + - create + - delete + - deletecollection + - get + - list + - patch + - update + - watch # to create or get/update CRDs when starting up - apiGroups: - apiextensions.k8s.io @@ -48,7 +55,8 @@ rules: - get - list - patch - - watch # needed if zalando-postgres-operator account is used for pods as well + - update + - watch # to CRUD secrets for database access - apiGroups: - "" @@ -95,8 +103,9 @@ rules: - delete - get - list - - watch - patch + - update + - watch # to resize the filesystem in Spilo pods when increasing volume size - apiGroups: - "" @@ -126,6 +135,18 @@ rules: - get - list - patch +# to CRUD cron jobs for logical backups +- apiGroups: + - batch + resources: + - cronjobs + verbs: + - create + - delete + - get + - list + - patch + - update # to get namespaces operator resources can run in - apiGroups: - "" @@ -150,39 +171,35 @@ rules: verbs: - get - create -# to create role bindings to the operator service account +# to create roles and role bindings to the pod service account - apiGroups: - - "rbac.authorization.k8s.io" + - rbac.authorization.k8s.io resources: - rolebindings + - roles verbs: - get - create -# to CRUD cron jobs for logical backups +# needed to grant to namespaced roles when pods run in privileged mode - apiGroups: - - batch + - extensions resources: - - cronjobs + - podsecuritypolicies + resourceNames: + - privileged verbs: - - create - - delete - - get - - list - - patch - - update + - use --- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRoleBinding metadata: - name: zalando-postgres-operator + name: postgres-operator roleRef: apiGroup: rbac.authorization.k8s.io kind: ClusterRole - name: zalando-postgres-operator + name: postgres-operator subjects: - kind: ServiceAccount -# note: the cluster role binding needs to be defined -# for every namespace the operator service account lives in. - name: zalando-postgres-operator + name: postgres-operator namespace: default diff --git a/manifests/operatorconfiguration.crd.yaml b/manifests/operatorconfiguration.crd.yaml index 7bd5c529c..c4bc8d458 100644 --- a/manifests/operatorconfiguration.crd.yaml +++ b/manifests/operatorconfiguration.crd.yaml @@ -124,6 +124,8 @@ spec: type: string pod_service_account_name: type: string + pod_service_account_role_definition: + type: string pod_service_account_role_binding_definition: type: string pod_terminate_grace_period: diff --git a/manifests/postgres-operator.yaml b/manifests/postgres-operator.yaml index a06abfc68..e3bc3e3e4 100644 --- a/manifests/postgres-operator.yaml +++ b/manifests/postgres-operator.yaml @@ -12,7 +12,7 @@ spec: labels: name: postgres-operator spec: - serviceAccountName: zalando-postgres-operator + serviceAccountName: postgres-operator containers: - name: postgres-operator image: registry.opensource.zalan.do/acid/postgres-operator:v1.3.1 diff --git a/manifests/postgres-pod-rbac.yaml b/manifests/postgres-pod-rbac.yaml new file mode 100644 index 000000000..9536acbb7 --- /dev/null +++ b/manifests/postgres-pod-rbac.yaml @@ -0,0 +1,49 @@ +apiVersion: v1 +kind: ServiceAccount +metadata: + name: postgres-pod + namespace: default + +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: postgres-pod +rules: +- apiGroups: + - "" + resources: + - endpoints + verbs: + - create + - delete + - deletecollection + - get + - list + - patch + - update + - watch +- apiGroups: + - "" + resources: + - pods + verbs: + - get + - list + - patch + - update + - watch +- apiGroups: + - "" + resources: + - services + verbs: + - create +- apiGroups: + - extensions + resources: + - podsecuritypolicies + resourceNames: + - privileged + verbs: + - use diff --git a/manifests/postgresql-operator-default-configuration.yaml b/manifests/postgresql-operator-default-configuration.yaml index efd1a5396..6955d0c78 100644 --- a/manifests/postgresql-operator-default-configuration.yaml +++ b/manifests/postgresql-operator-default-configuration.yaml @@ -45,7 +45,8 @@ configuration: # pod_priority_class_name: "" pod_role_label: spilo-role # pod_service_account_definition: "" - pod_service_account_name: zalando-postgres-operator + pod_service_account_name: postgres-pod + # pod_service_account_role_definition: "" # pod_service_account_role_binding_definition: "" pod_terminate_grace_period: 5m secret_name_template: "{username}.{cluster}.credentials.{tprkind}.{tprgroup}" diff --git a/manifests/user-facing-clusterroles.yaml b/manifests/user-facing-clusterroles.yaml index 800aafdb9..64392a19a 100644 --- a/manifests/user-facing-clusterroles.yaml +++ b/manifests/user-facing-clusterroles.yaml @@ -11,7 +11,14 @@ rules: - postgresqls - postgresqls/status verbs: - - "*" + - create + - delete + - deletecollection + - get + - list + - patch + - update + - watch --- apiVersion: rbac.authorization.k8s.io/v1 @@ -48,4 +55,3 @@ rules: - get - list - watch - diff --git a/pkg/apis/acid.zalan.do/v1/crds.go b/pkg/apis/acid.zalan.do/v1/crds.go index 4cfc9a9e6..721f11158 100644 --- a/pkg/apis/acid.zalan.do/v1/crds.go +++ b/pkg/apis/acid.zalan.do/v1/crds.go @@ -771,6 +771,9 @@ var OperatorConfigCRDResourceValidation = apiextv1beta1.CustomResourceValidation "pod_service_account_name": { Type: "string", }, + "pod_service_account_role_definition": { + Type: "string", + }, "pod_service_account_role_binding_definition": { Type: "string", }, diff --git a/pkg/apis/acid.zalan.do/v1/operator_configuration_type.go b/pkg/apis/acid.zalan.do/v1/operator_configuration_type.go index ded5261fb..e126b6fb6 100644 --- a/pkg/apis/acid.zalan.do/v1/operator_configuration_type.go +++ b/pkg/apis/acid.zalan.do/v1/operator_configuration_type.go @@ -43,6 +43,7 @@ type KubernetesMetaConfiguration struct { PodServiceAccountName string `json:"pod_service_account_name,omitempty"` // TODO: change it to the proper json PodServiceAccountDefinition string `json:"pod_service_account_definition,omitempty"` + PodServiceAccountRoleDefinition string `json:"pod_service_account_role_definition,omitempty"` PodServiceAccountRoleBindingDefinition string `json:"pod_service_account_role_binding_definition,omitempty"` PodTerminateGracePeriod Duration `json:"pod_terminate_grace_period,omitempty"` SpiloPrivileged bool `json:"spilo_privileged,omitempty"` diff --git a/pkg/cluster/cluster.go b/pkg/cluster/cluster.go index 91e7a5195..87ff123a1 100644 --- a/pkg/cluster/cluster.go +++ b/pkg/cluster/cluster.go @@ -45,6 +45,7 @@ type Config struct { RestConfig *rest.Config InfrastructureRoles map[string]spec.PgUser // inherited from the controller PodServiceAccount *v1.ServiceAccount + PodServiceAccountRole *rbacv1.Role PodServiceAccountRoleBinding *rbacv1.RoleBinding } diff --git a/pkg/controller/controller.go b/pkg/controller/controller.go index f67d99c1d..989a66f96 100644 --- a/pkg/controller/controller.go +++ b/pkg/controller/controller.go @@ -57,6 +57,7 @@ type Controller struct { workerLogs map[uint32]ringlog.RingLogger PodServiceAccount *v1.ServiceAccount + PodServiceAccountRole *rbacv1.Role PodServiceAccountRoleBinding *rbacv1.RoleBinding } @@ -161,11 +162,12 @@ func (c *Controller) initPodServiceAccount() { if c.opConfig.PodServiceAccountDefinition == "" { c.opConfig.PodServiceAccountDefinition = ` - { "apiVersion": "v1", - "kind": "ServiceAccount", - "metadata": { - "name": "operator" - } + { + "apiVersion": "v1", + "kind": "ServiceAccount", + "metadata": { + "name": "postgres-pod" + } }` } @@ -175,13 +177,13 @@ func (c *Controller) initPodServiceAccount() { switch { case err != nil: - panic(fmt.Errorf("Unable to parse pod service account definition from the operator config map: %v", err)) + panic(fmt.Errorf("Unable to parse pod service account definition from the operator configuration: %v", err)) case groupVersionKind.Kind != "ServiceAccount": - panic(fmt.Errorf("pod service account definition in the operator config map defines another type of resource: %v", groupVersionKind.Kind)) + panic(fmt.Errorf("pod service account definition in the operator configuration defines another type of resource: %v", groupVersionKind.Kind)) default: c.PodServiceAccount = obj.(*v1.ServiceAccount) if c.PodServiceAccount.Name != c.opConfig.PodServiceAccountName { - c.logger.Warnf("in the operator config map, the pod service account name %v does not match the name %v given in the account definition; using the former for consistency", c.opConfig.PodServiceAccountName, c.PodServiceAccount.Name) + c.logger.Warnf("in the operator configuration, the pod service account name %v does not match the name %v given in the account definition; using the former for consistency", c.opConfig.PodServiceAccountName, c.PodServiceAccount.Name) c.PodServiceAccount.Name = c.opConfig.PodServiceAccountName } c.PodServiceAccount.Namespace = "" @@ -190,6 +192,100 @@ func (c *Controller) initPodServiceAccount() { // actual service accounts are deployed at the time of Postgres/Spilo cluster creation } +func (c *Controller) initRole() { + + // service account on its own lacks any rights starting with k8s v1.8 + // operator binds it to the namespaced role with sufficient privileges + if c.opConfig.PodServiceAccountRoleDefinition == "" { + c.opConfig.PodServiceAccountRoleDefinition = fmt.Sprintf(` + { + "apiVersion": "rbac.authorization.k8s.io/v1", + "kind": "Role", + "metadata": { + "name": "%s" + }, + "rules": [ + { + "apiGroups": [ + "" + ], + "resources": [ + "endpoints" + ], + "verbs": [ + "create", + "delete", + "deletecollection", + "get", + "list", + "patch", + "update", + "watch" + ] + }, + { + "apiGroups": [ + "" + ], + "resources": [ + "pods" + ], + "verbs": [ + "get", + "list", + "patch", + "update", + "watch" + ] + }, + { + "apiGroups": [ + "" + ], + "resources": [ + "services" + ], + "verbs": [ + "create" + ] + }, + { + "apiGroups": [ + "extensions" + ], + "resources": [ + "podsecuritypolicies" + ], + "resourceNames": [ + "privileged" + ], + "verbs": [ + "use" + ] + } + ] + }`, c.PodServiceAccount.Name) + } + c.logger.Info("Parse roles") + // re-uses k8s internal parsing. See k8s client-go issue #193 for explanation + decode := scheme.Codecs.UniversalDeserializer().Decode + obj, groupVersionKind, err := decode([]byte(c.opConfig.PodServiceAccountRoleDefinition), nil, nil) + + switch { + case err != nil: + panic(fmt.Errorf("unable to parse the role definition from the operator configuration: %v", err)) + case groupVersionKind.Kind != "Role": + panic(fmt.Errorf("role definition in the operator configuration defines another type of resource: %v", groupVersionKind.Kind)) + default: + c.PodServiceAccountRole = obj.(*rbacv1.Role) + c.PodServiceAccountRole.Namespace = "" + c.logger.Info("successfully parsed") + + } + + // actual roles bindings are deployed at the time of Postgres/Spilo cluster creation +} + func (c *Controller) initRoleBinding() { // service account on its own lacks any rights starting with k8s v1.8 @@ -205,7 +301,7 @@ func (c *Controller) initRoleBinding() { }, "roleRef": { "apiGroup": "rbac.authorization.k8s.io", - "kind": "ClusterRole", + "kind": "Role", "name": "%s" }, "subjects": [ @@ -223,9 +319,9 @@ func (c *Controller) initRoleBinding() { switch { case err != nil: - panic(fmt.Errorf("Unable to parse the definition of the role binding for the pod service account definition from the operator config map: %v", err)) + panic(fmt.Errorf("unable to parse the role binding definition from the operator configuration: %v", err)) case groupVersionKind.Kind != "RoleBinding": - panic(fmt.Errorf("role binding definition in the operator config map defines another type of resource: %v", groupVersionKind.Kind)) + panic(fmt.Errorf("role binding definition in the operator configuration defines another type of resource: %v", groupVersionKind.Kind)) default: c.PodServiceAccountRoleBinding = obj.(*rbacv1.RoleBinding) c.PodServiceAccountRoleBinding.Namespace = "" @@ -253,6 +349,10 @@ func (c *Controller) initController() { } c.initPodServiceAccount() c.initRoleBinding() + // init role only if binding references a role + if c.PodServiceAccountRoleBinding != nil && c.PodServiceAccountRoleBinding.RoleRef.Kind == "Role" { + c.initRole() + } c.modifyConfigFromEnvironment() diff --git a/pkg/controller/operator_config.go b/pkg/controller/operator_config.go index c6f10faa0..4b18a0902 100644 --- a/pkg/controller/operator_config.go +++ b/pkg/controller/operator_config.go @@ -45,6 +45,7 @@ func (c *Controller) importConfigurationFromCRD(fromCRD *acidv1.OperatorConfigur result.CustomPodAnnotations = fromCRD.Kubernetes.CustomPodAnnotations result.PodServiceAccountName = fromCRD.Kubernetes.PodServiceAccountName result.PodServiceAccountDefinition = fromCRD.Kubernetes.PodServiceAccountDefinition + result.PodServiceAccountRoleDefinition = fromCRD.Kubernetes.PodServiceAccountRoleDefinition result.PodServiceAccountRoleBindingDefinition = fromCRD.Kubernetes.PodServiceAccountRoleBindingDefinition result.PodEnvironmentConfigMap = fromCRD.Kubernetes.PodEnvironmentConfigMap result.PodTerminateGracePeriod = time.Duration(fromCRD.Kubernetes.PodTerminateGracePeriod) diff --git a/pkg/controller/postgresql.go b/pkg/controller/postgresql.go index 8e8f9ae85..2d07c5067 100644 --- a/pkg/controller/postgresql.go +++ b/pkg/controller/postgresql.go @@ -493,23 +493,31 @@ func (c *Controller) postgresqlDelete(obj interface{}) { } /* - Ensures the pod service account and role bindings exists in a namespace + Ensures the pod service account, role 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. StatefulSets require the service account to create pods; Patroni requires relevant RBAC bindings to access endpoints. - The operator does not sync accounts/role bindings after creation. + The operator does not sync accounts/roles/role bindings after creation. */ func (c *Controller) submitRBACCredentials(event ClusterEvent) error { namespace := event.NewSpec.GetNamespace() if err := c.createPodServiceAccount(namespace); err != nil { - return fmt.Errorf("could not create pod service account %v : %v", c.opConfig.PodServiceAccountName, err) + return fmt.Errorf("could not create pod service account %q : %v", c.opConfig.PodServiceAccountName, err) + } + + // create role only if binding references a role + // if not role is empty and we rely on an existing cluster role + if c.PodServiceAccountRole != nil { + if err := c.createRole(namespace); err != nil { + return fmt.Errorf("could not create role %q : %v", c.PodServiceAccountRole.Name, err) + } } if err := c.createRoleBindings(namespace); err != nil { - return fmt.Errorf("could not create role binding %v : %v", c.PodServiceAccountRoleBinding.Name, err) + return fmt.Errorf("could not create role binding %q : %v", c.PodServiceAccountRoleBinding.Name, err) } return nil } @@ -517,19 +525,39 @@ func (c *Controller) submitRBACCredentials(event ClusterEvent) error { func (c *Controller) createPodServiceAccount(namespace string) error { podServiceAccountName := c.opConfig.PodServiceAccountName + // get a separate copy of service account + // to prevent a race condition when setting a namespace for many clusters + sa := *c.PodServiceAccount + _, err := c.KubeClient.ServiceAccounts(namespace).Get(podServiceAccountName, metav1.GetOptions{}) if k8sutil.ResourceNotFound(err) { - - c.logger.Infof(fmt.Sprintf("creating pod service account in the namespace %v", namespace)) - - // get a separate copy of service account - // to prevent a race condition when setting a namespace for many clusters - sa := *c.PodServiceAccount + c.logger.Infof(fmt.Sprintf("creating pod service account %q in the %q namespace", podServiceAccountName, namespace)) if _, err = c.KubeClient.ServiceAccounts(namespace).Create(&sa); err != nil { - return fmt.Errorf("cannot deploy the pod service account %v defined in the config map to the %v namespace: %v", podServiceAccountName, namespace, err) + return fmt.Errorf("cannot deploy the pod service account %q defined in the configuration to the %q namespace: %v", podServiceAccountName, namespace, err) } + c.logger.Infof("successfully deployed the pod service account %q to the %q namespace", podServiceAccountName, namespace) + } else if k8sutil.ResourceAlreadyExists(err) { + return nil + } - c.logger.Infof("successfully deployed the pod service account %v to the %v namespace", podServiceAccountName, namespace) + return err +} + +func (c *Controller) createRole(namespace string) error { + + podServiceAccountRoleName := c.PodServiceAccountRole.Name + // get a separate copy of the role + // to prevent a race condition when setting a namespace for many clusters + role := *c.PodServiceAccountRole + + _, err := c.KubeClient.Roles(namespace).Get(podServiceAccountRoleName, metav1.GetOptions{}) + if k8sutil.ResourceNotFound(err) { + c.logger.Infof("creating role %q in the %q namespace", podServiceAccountRoleName, namespace) + _, err = c.KubeClient.Roles(namespace).Create(&role) + if err != nil { + return fmt.Errorf("cannot create role %q in the %q namespace: %v", podServiceAccountRoleName, namespace, err) + } + c.logger.Infof("successfully deployed role %q to the %q namespace", podServiceAccountRoleName, namespace) } else if k8sutil.ResourceAlreadyExists(err) { return nil } @@ -541,22 +569,18 @@ func (c *Controller) createRoleBindings(namespace string) error { podServiceAccountName := c.opConfig.PodServiceAccountName podServiceAccountRoleBindingName := c.PodServiceAccountRoleBinding.Name + // get a separate copy of role binding + // to prevent a race condition when setting a namespace for many clusters + rb := *c.PodServiceAccountRoleBinding _, err := c.KubeClient.RoleBindings(namespace).Get(podServiceAccountRoleBindingName, metav1.GetOptions{}) if k8sutil.ResourceNotFound(err) { - - c.logger.Infof("Creating the role binding %v in the namespace %v", podServiceAccountRoleBindingName, namespace) - - // get a separate copy of role binding - // to prevent a race condition when setting a namespace for many clusters - rb := *c.PodServiceAccountRoleBinding + c.logger.Infof("creating the role binding %q in the %q namespace", podServiceAccountRoleBindingName, namespace) _, err = c.KubeClient.RoleBindings(namespace).Create(&rb) if err != nil { - 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) + return fmt.Errorf("cannot bind the pod service account %q defined in the configuration to the cluster role in the %q namespace: %v", podServiceAccountName, namespace, err) } - c.logger.Infof("successfully deployed the role binding for the pod service account %q to the %q namespace", podServiceAccountName, namespace) - } else if k8sutil.ResourceAlreadyExists(err) { return nil } diff --git a/pkg/util/config/config.go b/pkg/util/config/config.go index e4e429abb..5894c01fa 100644 --- a/pkg/util/config/config.go +++ b/pkg/util/config/config.go @@ -91,14 +91,14 @@ type Config struct { Scalyr LogicalBackup - WatchedNamespace string `name:"watched_namespace"` // special values: "*" means 'watch all namespaces', the empty string "" means 'watch a namespace where operator is deployed to' - EtcdHost string `name:"etcd_host" default:""` // special values: the empty string "" means Patroni will use K8s as a DCS - DockerImage string `name:"docker_image" default:"registry.opensource.zalan.do/acid/spilo-cdp-12:1.6-p16"` - Sidecars map[string]string `name:"sidecar_docker_images"` - // default name `operator` enables backward compatibility with the older ServiceAccountName field - PodServiceAccountName string `name:"pod_service_account_name" default:"operator"` + WatchedNamespace string `name:"watched_namespace"` // special values: "*" means 'watch all namespaces', the empty string "" means 'watch a namespace where operator is deployed to' + EtcdHost string `name:"etcd_host" default:""` // special values: the empty string "" means Patroni will use K8s as a DCS + DockerImage string `name:"docker_image" default:"registry.opensource.zalan.do/acid/spilo-cdp-12:1.6-p16"` + Sidecars map[string]string `name:"sidecar_docker_images"` + PodServiceAccountName string `name:"pod_service_account_name" default:"postgres-pod"` // value of this string must be valid JSON or YAML; see initPodServiceAccount PodServiceAccountDefinition string `name:"pod_service_account_definition" default:""` + PodServiceAccountRoleDefinition string `name:"pod_service_account_role_definition" default:""` PodServiceAccountRoleBindingDefinition string `name:"pod_service_account_role_binding_definition" default:""` MasterPodMoveTimeout time.Duration `name:"master_pod_move_timeout" default:"20m"` DbHostedZone string `name:"db_hosted_zone" default:"db.example.com"` diff --git a/pkg/util/k8sutil/k8sutil.go b/pkg/util/k8sutil/k8sutil.go index 509b12c19..94d1f5ac2 100644 --- a/pkg/util/k8sutil/k8sutil.go +++ b/pkg/util/k8sutil/k8sutil.go @@ -39,6 +39,7 @@ type KubernetesClient struct { corev1.NamespacesGetter corev1.ServiceAccountsGetter appsv1.StatefulSetsGetter + rbacv1.RolesGetter rbacv1.RoleBindingsGetter policyv1beta1.PodDisruptionBudgetsGetter apiextbeta1.CustomResourceDefinitionsGetter @@ -103,6 +104,7 @@ func NewFromConfig(cfg *rest.Config) (KubernetesClient, error) { kubeClient.StatefulSetsGetter = client.AppsV1() kubeClient.PodDisruptionBudgetsGetter = client.PolicyV1beta1() kubeClient.RESTClient = client.CoreV1().RESTClient() + kubeClient.RolesGetter = client.RbacV1() kubeClient.RoleBindingsGetter = client.RbacV1() kubeClient.CronJobsGetter = client.BatchV1beta1()