Merge pull request #277 from zalando-incubator/automatically-deploy-service-account

Deploy service account for pod creation on demand
This commit is contained in:
zerg-junior 2018-04-26 14:44:37 +02:00 committed by GitHub
commit 8f08bef67c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 108 additions and 18 deletions

View File

@ -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. 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 ### Create ConfigMap

View File

@ -7,7 +7,6 @@ data:
# if neither is set or evaluates to the empty string, listen to the operator's own namespace # 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 # if set to the "*", listen to all namespaces
# watched_namespace: development # watched_namespace: development
service_account_name: operator
cluster_labels: application:spilo cluster_labels: application:spilo
cluster_name_label: version cluster_name_label: version
pod_role_label: spilo-role pod_role_label: spilo-role

View File

@ -42,6 +42,7 @@ type Config struct {
OpConfig config.Config OpConfig config.Config
RestConfig *rest.Config RestConfig *rest.Config
InfrastructureRoles map[string]spec.PgUser // inherited from the controller InfrastructureRoles map[string]spec.PgUser // inherited from the controller
PodServiceAccount *v1.ServiceAccount
} }
type kubeResources struct { type kubeResources struct {
@ -194,6 +195,39 @@ func (c *Cluster) initUsers() error {
return nil 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. // Create creates the new kubernetes objects associated with the cluster.
func (c *Cluster) Create() error { func (c *Cluster) Create() error {
c.mu.Lock() 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)) 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 { if c.Statefulset != nil {
return fmt.Errorf("statefulset already exists in the cluster") return fmt.Errorf("statefulset already exists in the cluster")
} }

View File

@ -435,7 +435,7 @@ func (c *Cluster) generatePodTemplate(
terminateGracePeriodSeconds := int64(c.OpConfig.PodTerminateGracePeriod.Seconds()) terminateGracePeriodSeconds := int64(c.OpConfig.PodTerminateGracePeriod.Seconds())
podSpec := v1.PodSpec{ podSpec := v1.PodSpec{
ServiceAccountName: c.OpConfig.ServiceAccountName, ServiceAccountName: c.OpConfig.PodServiceAccountName,
TerminationGracePeriodSeconds: &terminateGracePeriodSeconds, TerminationGracePeriodSeconds: &terminateGracePeriodSeconds,
Containers: []v1.Container{container}, Containers: []v1.Container{container},
Tolerations: c.tolerations(tolerationsSpec), Tolerations: c.tolerations(tolerationsSpec),

View File

@ -8,6 +8,7 @@ import (
"github.com/Sirupsen/logrus" "github.com/Sirupsen/logrus"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/types"
"k8s.io/client-go/kubernetes/scheme"
"k8s.io/client-go/pkg/api/v1" "k8s.io/client-go/pkg/api/v1"
"k8s.io/client-go/tools/cache" "k8s.io/client-go/tools/cache"
@ -50,6 +51,8 @@ type Controller struct {
lastClusterSyncTime int64 lastClusterSyncTime int64
workerLogs map[uint32]ringlog.RingLogger workerLogs map[uint32]ringlog.RingLogger
PodServiceAccount *v1.ServiceAccount
} }
// NewController creates a new controller // NewController creates a new controller
@ -113,11 +116,46 @@ func (c *Controller) initOperatorConfig() {
if scalyrAPIKey != "" { if scalyrAPIKey != "" {
c.opConfig.ScalyrAPIKey = 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() { func (c *Controller) initController() {
c.initClients() c.initClients()
c.initOperatorConfig() c.initOperatorConfig()
c.initPodServiceAccount()
c.initSharedInformers() c.initSharedInformers()

View File

@ -26,6 +26,7 @@ func (c *Controller) makeClusterConfig() cluster.Config {
RestConfig: c.config.RestConfig, RestConfig: c.config.RestConfig,
OpConfig: config.Copy(c.opConfig), OpConfig: config.Copy(c.opConfig),
InfrastructureRoles: infrastructureRoles, InfrastructureRoles: infrastructureRoles,
PodServiceAccount: c.PodServiceAccount,
} }
} }

View File

@ -67,21 +67,25 @@ type Config struct {
Resources Resources
Auth Auth
Scalyr 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"` WatchedNamespace string `name:"watched_namespace"` // special values: "*" means 'watch all namespaces', the empty string "" means 'watch a namespace where operator is deployed to'
DockerImage string `name:"docker_image" default:"registry.opensource.zalan.do/acid/spiloprivate-9.6:1.2-p4"` EtcdHost string `name:"etcd_host" default:"etcd-client.default.svc.cluster.local:2379"`
ServiceAccountName string `name:"service_account_name" default:"operator"` DockerImage string `name:"docker_image" default:"registry.opensource.zalan.do/acid/spiloprivate-9.6:1.2-p4"`
DbHostedZone string `name:"db_hosted_zone" default:"db.example.com"` // default name `operator` enables backward compatibility with the older ServiceAccountName field
EtcdScope string `name:"etcd_scope" default:"service"` PodServiceAccountName string `name:"pod_service_account_name" default:"operator"`
WALES3Bucket string `name:"wal_s3_bucket"` // value of this string must be valid JSON or YAML; see initPodServiceAccount
KubeIAMRole string `name:"kube_iam_role"` PodServiceAccountDefinition string `name:"pod_service_account_definition" default:""`
DebugLogging bool `name:"debug_logging" default:"true"` DbHostedZone string `name:"db_hosted_zone" default:"db.example.com"`
EnableDBAccess bool `name:"enable_database_access" default:"true"` EtcdScope string `name:"etcd_scope" default:"service"`
EnableTeamsAPI bool `name:"enable_teams_api" default:"true"` WALES3Bucket string `name:"wal_s3_bucket"`
EnableTeamSuperuser bool `name:"enable_team_superuser" default:"false"` KubeIAMRole string `name:"kube_iam_role"`
TeamAdminRole string `name:"team_admin_role" default:"admin"` DebugLogging bool `name:"debug_logging" default:"true"`
EnableMasterLoadBalancer bool `name:"enable_master_load_balancer" default:"true"` EnableDBAccess bool `name:"enable_database_access" default:"true"`
EnableReplicaLoadBalancer bool `name:"enable_replica_load_balancer" default:"false"` 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 // deprecated and kept for backward compatibility
EnableLoadBalancer *bool `name:"enable_load_balancer" default:"true"` EnableLoadBalancer *bool `name:"enable_load_balancer" default:"true"`
MasterDNSNameFormat stringTemplate `name:"master_dns_name_format" default:"{cluster}.{team}.{hostedzone}"` MasterDNSNameFormat stringTemplate `name:"master_dns_name_format" default:"{cluster}.{team}.{hostedzone}"`