diff --git a/pkg/cluster/cluster.go b/pkg/cluster/cluster.go index 2662cd521581fde55d62ee2a1ba21f8b9d134e13..057ae257b76ed226b94c17de775e28b45a10713c 100644 --- a/pkg/cluster/cluster.go +++ b/pkg/cluster/cluster.go @@ -17,6 +17,7 @@ import ( "k8s.io/client-go/pkg/api/v1" "k8s.io/client-go/pkg/apis/apps/v1beta1" policybeta1 "k8s.io/client-go/pkg/apis/policy/v1beta1" + rbac "k8s.io/client-go/pkg/apis/rbac/v1beta1" "k8s.io/client-go/rest" "k8s.io/client-go/tools/cache" @@ -39,10 +40,11 @@ var ( // Config contains operator-wide clients and configuration used from a cluster. TODO: remove struct duplication. type Config struct { - OpConfig config.Config - RestConfig *rest.Config - InfrastructureRoles map[string]spec.PgUser // inherited from the controller - PodServiceAccount *v1.ServiceAccount + OpConfig config.Config + RestConfig *rest.Config + InfrastructureRoles map[string]spec.PgUser // inherited from the controller + PodServiceAccount *v1.ServiceAccount + PodServiceAccountRoleBinding *rbac.RoleBinding } type kubeResources struct { @@ -200,7 +202,7 @@ func (c *Cluster) initUsers() error { } /* - 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. + Ensures the service account required by StatefulSets to create pods exists in a namespace and has correct access rights before a PG cluster is created there so that a user does not have to deploy and configure the account manually. The operator does not sync these accounts after creation. */ @@ -229,7 +231,31 @@ func (c *Cluster) createPodServiceAccounts() error { c.logger.Infof("successfully found the service account %q used to create pods to the namespace %q", podServiceAccountName, c.Namespace) } + podServiceAccountRoleBindingName := c.Config.PodServiceAccountRoleBinding.Name + _, err = c.KubeClient.RoleBindings(c.Namespace).Get(podServiceAccountRoleBindingName, metav1.GetOptions{}) + + if err != nil { + + c.setProcessName(fmt.Sprintf("binding the pod service account in the namespace %v to the cluster role", c.Namespace)) + + c.logger.Infof("the role binding %q for the pod service account %q cannot be retrieved in the namespace %q. Trying to deploy the role binding.", podServiceAccountRoleBindingName, podServiceAccountName, c.Namespace) + + // 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(c.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, c.Namespace, err) + } + + c.logger.Infof("successfully deployed the role binding for the pod service account %q to the %q namespace", podServiceAccountName, c.Namespace) + + } else { + c.logger.Infof("successfully found the role binding %q for the service account %q used to create pods to the namespace %q", podServiceAccountRoleBindingName, podServiceAccountName, c.Namespace) + } + return nil + } // Create creates the new kubernetes objects associated with the cluster. diff --git a/pkg/controller/controller.go b/pkg/controller/controller.go index 84a07811b1f99e10fbd2235a66ad12aaf92c8482..fae670b74317a055bf6c630659ceefde69d816ce 100644 --- a/pkg/controller/controller.go +++ b/pkg/controller/controller.go @@ -10,6 +10,7 @@ import ( "k8s.io/apimachinery/pkg/types" "k8s.io/client-go/kubernetes/scheme" "k8s.io/client-go/pkg/api/v1" + rbac "k8s.io/client-go/pkg/apis/rbac/v1beta1" "k8s.io/client-go/tools/cache" "github.com/zalando-incubator/postgres-operator/pkg/apiserver" @@ -52,7 +53,8 @@ type Controller struct { workerLogs map[uint32]ringlog.RingLogger - PodServiceAccount *v1.ServiceAccount + PodServiceAccount *v1.ServiceAccount + PodServiceAccountRoleBinding *rbac.RoleBinding } // NewController creates a new controller @@ -158,7 +160,49 @@ func (c *Controller) initPodServiceAccount() { c.PodServiceAccount.Namespace = "" } - // actual service accounts are deployed at the time of Postgres/Spilo cluster creation + // service account on its own lacks any rights starting with k8s v1.8 + // operator binds it to the cluster role with sufficient priviliges + // we assume the role is created by the k8s administrator + if c.opConfig.PodServiceAccountRoleBindingDefinition == "" { + c.opConfig.PodServiceAccountRoleBindingDefinition = ` + { + "apiVersion": "rbac.authorization.k8s.io/v1beta1", + "kind": "RoleBinding", + "metadata": { + "name": "zalando-postgres-operator" + }, + "roleRef": { + "apiGroup": "rbac.authorization.k8s.io", + "kind": "ClusterRole", + "name": "zalando-postgres-operator" + }, + "subjects": [ + { + "kind": "ServiceAccount", + "name": "operator" + } + ] + }` + } + c.logger.Info("Parse role bindings") + // re-uses k8s internal parsing. See k8s client-go issue #193 for explanation + obj, groupVersionKind, err = decode([]byte(c.opConfig.PodServiceAccountRoleBindingDefinition), nil, nil) + + switch { + case err != nil: + 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)) + case groupVersionKind.Kind != "RoleBinding": + panic(fmt.Errorf("role binding definiton in the operator config map defines another type of resource: %v", groupVersionKind.Kind)) + default: + c.PodServiceAccountRoleBinding = obj.(*rbac.RoleBinding) + c.PodServiceAccountRoleBinding.Namespace = "" + c.PodServiceAccountRoleBinding.Subjects[0].Name = c.PodServiceAccount.Name + c.logger.Info("successfully parsed") + + } + + // actual service accounts and roles bindings are deployed at the time of Postgres/Spilo cluster creation + } func (c *Controller) initController() { diff --git a/pkg/util/config/config.go b/pkg/util/config/config.go index 2bce980b1741c08d79c7afd03f4832cb8b99c84d..d8ac0e3d07d2b9e7ae7632f7732af7dd912bd92a 100644 --- a/pkg/util/config/config.go +++ b/pkg/util/config/config.go @@ -74,19 +74,20 @@ type Config struct { // 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"` - LogS3Bucket string `name:"log_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"` + PodServiceAccountDefinition string `name:"pod_service_account_definition" default:""` + PodServiceAccountRoleBindingDefinition string `name:"pod_service_account_role_binding" default:""` + DbHostedZone string `name:"db_hosted_zone" default:"db.example.com"` + EtcdScope string `name:"etcd_scope" default:"service"` + WALES3Bucket string `name:"wal_s3_bucket"` + LogS3Bucket string `name:"log_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"` MasterDNSNameFormat stringTemplate `name:"master_dns_name_format" default:"{cluster}.{team}.{hostedzone}"` diff --git a/pkg/util/k8sutil/k8sutil.go b/pkg/util/k8sutil/k8sutil.go index 142d4f822d1e06da85f370135cd79b3adb26a67b..d1f79c059b6aefbb69d5c128114894f9a9d7b8f7 100644 --- a/pkg/util/k8sutil/k8sutil.go +++ b/pkg/util/k8sutil/k8sutil.go @@ -4,6 +4,7 @@ import ( "fmt" "reflect" + "github.com/zalando-incubator/postgres-operator/pkg/util/constants" apiextclient "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset" apiextbeta1 "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/typed/apiextensions/v1beta1" apierrors "k8s.io/apimachinery/pkg/api/errors" @@ -13,13 +14,12 @@ import ( "k8s.io/client-go/kubernetes/typed/apps/v1beta1" v1core "k8s.io/client-go/kubernetes/typed/core/v1" policyv1beta1 "k8s.io/client-go/kubernetes/typed/policy/v1beta1" + rbacv1beta1 "k8s.io/client-go/kubernetes/typed/rbac/v1beta1" "k8s.io/client-go/pkg/api" "k8s.io/client-go/pkg/api/v1" policybeta1 "k8s.io/client-go/pkg/apis/policy/v1beta1" "k8s.io/client-go/rest" "k8s.io/client-go/tools/clientcmd" - - "github.com/zalando-incubator/postgres-operator/pkg/util/constants" ) // KubernetesClient describes getters for Kubernetes objects @@ -35,6 +35,8 @@ type KubernetesClient struct { v1core.NamespacesGetter v1core.ServiceAccountsGetter v1beta1.StatefulSetsGetter + + rbacv1beta1.RoleBindingsGetter policyv1beta1.PodDisruptionBudgetsGetter apiextbeta1.CustomResourceDefinitionsGetter @@ -83,6 +85,7 @@ func NewFromConfig(cfg *rest.Config) (KubernetesClient, error) { kubeClient.StatefulSetsGetter = client.AppsV1beta1() kubeClient.PodDisruptionBudgetsGetter = client.PolicyV1beta1() kubeClient.RESTClient = client.CoreV1().RESTClient() + kubeClient.RoleBindingsGetter = client.RbacV1beta1() cfg2 := *cfg cfg2.GroupVersion = &schema.GroupVersion{