diff --git a/README.md b/README.md index 9bd663c60..a76905303 100644 --- a/README.md +++ b/README.md @@ -87,7 +87,16 @@ By default, the operator watches the namespace it is deployed to. You can change Note that for an operator to manage pods in the watched namespace, the operator's service account (as specified in the operator deployment manifest) has to have appropriate privileges to access the watched namespace. The operator may not be able to function in the case it watches all namespaces but lacks access rights to any of them (except Kubernetes system namespaces like `kube-system`). The reason is that for multiple namespaces operations such as '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 Kubernetes API (e.g. when using Kubernetes-native configuration of Patroni). +The watched namespace also needs to have a (possibly different) service account in the case database pods need to talk to the Kubernetes API (e.g. when using Kubernetes-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. ### Create ConfigMap diff --git a/manifests/configmap.yaml b/manifests/configmap.yaml index 7d5c742c8..f3b79e67b 100644 --- a/manifests/configmap.yaml +++ b/manifests/configmap.yaml @@ -7,7 +7,6 @@ data: # if neither is set or evaluates to the empty string, listen to the operator's own namespace # if set to the "*", listen to all namespaces # watched_namespace: development - service_account_name: operator cluster_labels: application:spilo cluster_name_label: version pod_role_label: spilo-role diff --git a/pkg/cluster/cluster.go b/pkg/cluster/cluster.go index bfb8e35d7..b6251eafc 100644 --- a/pkg/cluster/cluster.go +++ b/pkg/cluster/cluster.go @@ -42,6 +42,7 @@ type Config struct { OpConfig config.Config RestConfig *rest.Config InfrastructureRoles map[string]spec.PgUser // inherited from the controller + PodServiceAccount *v1.ServiceAccount } type kubeResources struct { @@ -194,6 +195,39 @@ func (c *Cluster) initUsers() error { return nil } +/* + 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. + + The operator does not sync these accounts after creation. +*/ +func (c *Cluster) createPodServiceAccounts() error { + + podServiceAccountName := c.Config.OpConfig.PodServiceAccountName + _, err := c.KubeClient.ServiceAccounts(c.Namespace).Get(podServiceAccountName, metav1.GetOptions{}) + + if err != nil { + + c.setProcessName(fmt.Sprintf("creating pod service account in the namespace %v", c.Namespace)) + + c.logger.Infof("the pod service account %q cannot be retrieved in the namespace %q. Trying to deploy the account.", podServiceAccountName, c.Namespace) + + // 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(c.Namespace).Create(&sa) + if err != nil { + return fmt.Errorf("cannot deploy the pod service account %q defined in the config map to the %q namespace: %v", podServiceAccountName, c.Namespace, err) + } + + c.logger.Infof("successfully deployed the pod service account %q to the %q namespace", podServiceAccountName, c.Namespace) + + } else { + c.logger.Infof("successfully found the service account %q used to create pods to the namespace %q", podServiceAccountName, c.Namespace) + } + + return nil +} + // Create creates the new kubernetes objects associated with the cluster. func (c *Cluster) Create() error { c.mu.Lock() @@ -256,6 +290,11 @@ func (c *Cluster) Create() error { } c.logger.Infof("pod disruption budget %q has been successfully created", util.NameFromMeta(pdb.ObjectMeta)) + if err = c.createPodServiceAccounts(); err != nil { + return fmt.Errorf("could not create pod service account %v : %v", c.OpConfig.PodServiceAccountName, err) + } + c.logger.Infof("pod service accounts have been successfully synced") + if c.Statefulset != nil { return fmt.Errorf("statefulset already exists in the cluster") } diff --git a/pkg/cluster/k8sres.go b/pkg/cluster/k8sres.go index cf16bb39a..2b1460643 100644 --- a/pkg/cluster/k8sres.go +++ b/pkg/cluster/k8sres.go @@ -435,7 +435,7 @@ func (c *Cluster) generatePodTemplate( terminateGracePeriodSeconds := int64(c.OpConfig.PodTerminateGracePeriod.Seconds()) podSpec := v1.PodSpec{ - ServiceAccountName: c.OpConfig.ServiceAccountName, + ServiceAccountName: c.OpConfig.PodServiceAccountName, TerminationGracePeriodSeconds: &terminateGracePeriodSeconds, Containers: []v1.Container{container}, Tolerations: c.tolerations(tolerationsSpec), diff --git a/pkg/controller/controller.go b/pkg/controller/controller.go index d19da5b84..7b309a547 100644 --- a/pkg/controller/controller.go +++ b/pkg/controller/controller.go @@ -8,6 +8,7 @@ import ( "github.com/Sirupsen/logrus" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/kubernetes/scheme" "k8s.io/client-go/pkg/api/v1" "k8s.io/client-go/tools/cache" @@ -50,6 +51,8 @@ type Controller struct { lastClusterSyncTime int64 workerLogs map[uint32]ringlog.RingLogger + + PodServiceAccount *v1.ServiceAccount } // NewController creates a new controller @@ -113,11 +116,46 @@ func (c *Controller) initOperatorConfig() { if scalyrAPIKey != "" { c.opConfig.ScalyrAPIKey = scalyrAPIKey } + +} + +func (c *Controller) initPodServiceAccount() { + + if c.opConfig.PodServiceAccountDefinition == "" { + c.opConfig.PodServiceAccountDefinition = ` + { "apiVersion": "v1", + "kind": "ServiceAccount", + "metadata": { + "name": "operator" + } + }` + } + + // 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.PodServiceAccountDefinition), nil, nil) + + switch { + case err != nil: + panic(fmt.Errorf("Unable to parse pod service account definiton from the operator config map: %v", err)) + case groupVersionKind.Kind != "ServiceAccount": + panic(fmt.Errorf("pod service account definiton in the operator config map 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.PodServiceAccount.Name = c.opConfig.PodServiceAccountName + } + c.PodServiceAccount.Namespace = "" + } + + // actual service accounts are deployed at the time of Postgres/Spilo cluster creation } func (c *Controller) initController() { c.initClients() c.initOperatorConfig() + c.initPodServiceAccount() c.initSharedInformers() diff --git a/pkg/controller/util.go b/pkg/controller/util.go index 69124c111..5e46e93eb 100644 --- a/pkg/controller/util.go +++ b/pkg/controller/util.go @@ -26,6 +26,7 @@ func (c *Controller) makeClusterConfig() cluster.Config { RestConfig: c.config.RestConfig, OpConfig: config.Copy(c.opConfig), InfrastructureRoles: infrastructureRoles, + PodServiceAccount: c.PodServiceAccount, } } diff --git a/pkg/util/config/config.go b/pkg/util/config/config.go index 0e653ce0b..b101c6f08 100644 --- a/pkg/util/config/config.go +++ b/pkg/util/config/config.go @@ -67,21 +67,25 @@ type Config struct { Resources Auth Scalyr - 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:"etcd-client.default.svc.cluster.local:2379"` - DockerImage string `name:"docker_image" default:"registry.opensource.zalan.do/acid/spiloprivate-9.6:1.2-p4"` - ServiceAccountName string `name:"service_account_name" default:"operator"` - DbHostedZone string `name:"db_hosted_zone" default:"db.example.com"` - EtcdScope string `name:"etcd_scope" default:"service"` - WALES3Bucket string `name:"wal_s3_bucket"` - KubeIAMRole string `name:"kube_iam_role"` - DebugLogging bool `name:"debug_logging" default:"true"` - EnableDBAccess bool `name:"enable_database_access" default:"true"` - EnableTeamsAPI bool `name:"enable_teams_api" default:"true"` - EnableTeamSuperuser bool `name:"enable_team_superuser" default:"false"` - TeamAdminRole string `name:"team_admin_role" default:"admin"` - EnableMasterLoadBalancer bool `name:"enable_master_load_balancer" default:"true"` - EnableReplicaLoadBalancer bool `name:"enable_replica_load_balancer" default:"false"` + + 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:"etcd-client.default.svc.cluster.local:2379"` + DockerImage string `name:"docker_image" default:"registry.opensource.zalan.do/acid/spiloprivate-9.6:1.2-p4"` + // default name `operator` enables backward compatibility with the older ServiceAccountName field + PodServiceAccountName string `name:"pod_service_account_name" default:"operator"` + // value of this string must be valid JSON or YAML; see initPodServiceAccount + PodServiceAccountDefinition string `name:"pod_service_account_definition" default:""` + DbHostedZone string `name:"db_hosted_zone" default:"db.example.com"` + EtcdScope string `name:"etcd_scope" default:"service"` + WALES3Bucket string `name:"wal_s3_bucket"` + KubeIAMRole string `name:"kube_iam_role"` + DebugLogging bool `name:"debug_logging" default:"true"` + EnableDBAccess bool `name:"enable_database_access" default:"true"` + EnableTeamsAPI bool `name:"enable_teams_api" default:"true"` + EnableTeamSuperuser bool `name:"enable_team_superuser" default:"false"` + TeamAdminRole string `name:"team_admin_role" default:"admin"` + EnableMasterLoadBalancer bool `name:"enable_master_load_balancer" default:"true"` + EnableReplicaLoadBalancer bool `name:"enable_replica_load_balancer" default:"false"` // deprecated and kept for backward compatibility EnableLoadBalancer *bool `name:"enable_load_balancer" default:"true"` MasterDNSNameFormat stringTemplate `name:"master_dns_name_format" default:"{cluster}.{team}.{hostedzone}"`