From 71b93b4cc24ebfbec6f240976042652646f76407 Mon Sep 17 00:00:00 2001 From: Oleksii Kliukin Date: Wed, 12 Apr 2017 16:21:13 +0200 Subject: [PATCH] Feature/infrastructure roles (#91) * Add infrastructure roles configured globally. Those are the roles defined in the operator itself. The operator's configuration refers to the secret containing role names, passwords and membership information. While they are referred to as roles, in reality those are users. In addition, improve the regex to filter out invalid users and make sure user secret names are compatible with DNS name spec. Add an example manifest for the infrastructure roles. --- cmd/main.go | 6 +-- manifests/infrastructure-roles.yaml | 15 ++++++++ manifests/postgres-operator.yaml | 4 +- pkg/cluster/cluster.go | 30 ++++++++++++--- pkg/cluster/pg.go | 4 +- pkg/cluster/sync.go | 2 +- pkg/cluster/util.go | 6 ++- pkg/controller/controller.go | 16 +++++--- pkg/controller/util.go | 57 ++++++++++++++++++++++++++--- pkg/spec/postgresql.go | 4 +- pkg/spec/types.go | 1 + pkg/util/config/config.go | 13 ++++--- pkg/util/teams/teams.go | 4 +- 13 files changed, 129 insertions(+), 33 deletions(-) create mode 100644 manifests/infrastructure-roles.yaml diff --git a/cmd/main.go b/cmd/main.go index c3fe633b0..ee4db960d 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -48,9 +48,9 @@ func ControllerConfig() *controller.Config { restClient, err := k8sutil.KubernetesRestClient(restConfig) return &controller.Config{ - PodNamespace: podNamespace, //TODO: move to config.Config - KubeClient: client, - RestClient: restClient, + PodNamespace: podNamespace, //TODO: move to config.Config + KubeClient: client, + RestClient: restClient, } } diff --git a/manifests/infrastructure-roles.yaml b/manifests/infrastructure-roles.yaml new file mode 100644 index 000000000..e2c6e885b --- /dev/null +++ b/manifests/infrastructure-roles.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +data: + # robot_zmon_acid_monitoring + user1: cm9ib3Rfem1vbl9hY2lkX21vbml0b3Jpbmc= + # robot_zmon + inrole1: cm9ib3Rfem1vbg== + # testuser + user2: dGVzdHVzZXI= + # foobar + password2: Zm9vYmFy +kind: Secret +metadata: + name: postgresql-infrastructure-roles + namespace: default +type: Opaque diff --git a/manifests/postgres-operator.yaml b/manifests/postgres-operator.yaml index 6daf66efd..1aeca020f 100644 --- a/manifests/postgres-operator.yaml +++ b/manifests/postgres-operator.yaml @@ -57,4 +57,6 @@ spec: - name: PGOP_DB_HOSTED_ZONE value: "db.example.com" - name: PGOP_DNS_NAME_FORMAT - value: "%s.%s.staging.%s" \ No newline at end of file + value: "%s.%s.staging.%s" + - name: PGOP_INFRASTRUCTURE_ROLES_SECRET_NAME + value: "postgresql-infrastructure-roles" diff --git a/pkg/cluster/cluster.go b/pkg/cluster/cluster.go index 15a06b01a..7eb5c920b 100644 --- a/pkg/cluster/cluster.go +++ b/pkg/cluster/cluster.go @@ -30,15 +30,17 @@ import ( var ( alphaNumericRegexp = regexp.MustCompile("^[a-zA-Z][a-zA-Z0-9]*$") + userRegexp = regexp.MustCompile(`^[a-z0-9]([-_a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-_a-z0-9]*[a-z0-9])?)*$`) ) //TODO: remove struct duplication type Config struct { - KubeClient *kubernetes.Clientset //TODO: move clients to the better place? - RestClient *rest.RESTClient - EtcdClient etcdclient.KeysAPI - TeamsAPIClient *teams.TeamsAPI - OpConfig *config.Config + KubeClient *kubernetes.Clientset //TODO: move clients to the better place? + RestClient *rest.RESTClient + EtcdClient etcdclient.KeysAPI + TeamsAPIClient *teams.TeamsAPI + OpConfig *config.Config + InfrastructureRoles map[string]spec.PgUser // inherited from the controller } type kubeResources struct { @@ -122,6 +124,11 @@ func (c *Cluster) SetStatus(status spec.PostgresStatus) { func (c *Cluster) initUsers() error { c.initSystemUsers() + + if err := c.initInfrastructureRoles(); err != nil { + return fmt.Errorf("Can't init infrastructure roles: %s", err) + } + if err := c.initRobotUsers(); err != nil { return fmt.Errorf("Can't init robot users: %s", err) } @@ -130,6 +137,8 @@ func (c *Cluster) initUsers() error { return fmt.Errorf("Can't init human users: %s", err) } + c.logger.Debugf("Initialized users: %# v", util.Pretty(c.pgUsers)) + return nil } @@ -400,3 +409,14 @@ func (c *Cluster) initHumanUsers() error { return nil } + +func (c *Cluster) initInfrastructureRoles() error { + // add infrastucture roles from the operator's definition + for username, data := range c.InfrastructureRoles { + if !isValidUsername(username) { + return fmt.Errorf("Invalid username: '%s'", username) + } + c.pgUsers[username] = data + } + return nil +} diff --git a/pkg/cluster/pg.go b/pkg/cluster/pg.go index 22abcb48d..fc159ed87 100644 --- a/pkg/cluster/pg.go +++ b/pkg/cluster/pg.go @@ -66,7 +66,9 @@ func (c *Cluster) createPgUser(user spec.PgUser) (isHuman bool, err error) { if addLoginFlag { flags = append(flags, "LOGIN") } - + if !isHuman && user.MemberOf != "" { + flags = append(flags, fmt.Sprintf("IN ROLE \"%s\"", user.MemberOf)) + } userFlags := strings.Join(flags, " ") userPassword := fmt.Sprintf("ENCRYPTED PASSWORD '%s'", util.PGUserPassword(user)) if user.Password == "" { diff --git a/pkg/cluster/sync.go b/pkg/cluster/sync.go index bd32fe627..505ac5f01 100644 --- a/pkg/cluster/sync.go +++ b/pkg/cluster/sync.go @@ -122,4 +122,4 @@ func (c *Cluster) syncStatefulSet() error { c.logger.Infof("Pods have been recreated") return nil -} \ No newline at end of file +} diff --git a/pkg/cluster/util.go b/pkg/cluster/util.go index 5a3c2c6f7..17dca2cd1 100644 --- a/pkg/cluster/util.go +++ b/pkg/cluster/util.go @@ -18,7 +18,7 @@ import ( ) func isValidUsername(username string) bool { - return alphaNumericRegexp.MatchString(username) + return userRegexp.MatchString(username) } func normalizeUserFlags(userFlags []string) (flags []string, err error) { @@ -218,8 +218,10 @@ func (c *Cluster) dnsName() string { } func (c *Cluster) credentialSecretName(username string) string { + // secret must consist of lower case alphanumeric characters, '-' or '.', + // and must start and end with an alphanumeric character return fmt.Sprintf(constants.UserSecretTemplate, - username, + strings.Replace(username, "_", "-", -1), c.Metadata.Name, constants.TPRName, constants.TPRVendor) diff --git a/pkg/controller/controller.go b/pkg/controller/controller.go index e1366f06f..83305e356 100644 --- a/pkg/controller/controller.go +++ b/pkg/controller/controller.go @@ -17,11 +17,12 @@ import ( ) type Config struct { - PodNamespace string - KubeClient *kubernetes.Clientset - RestClient *rest.RESTClient - EtcdClient etcdclient.KeysAPI - TeamsAPIClient *teams.TeamsAPI + PodNamespace string + KubeClient *kubernetes.Clientset + RestClient *rest.RESTClient + EtcdClient etcdclient.KeysAPI + TeamsAPIClient *teams.TeamsAPI + InfrastructureRoles map[string]spec.PgUser } type Controller struct { @@ -74,6 +75,11 @@ func (c *Controller) initController() { } c.TeamsAPIClient.RefreshTokenAction = c.getOAuthToken + if infraRoles, err := c.getInfrastructureRoles(); err != nil { + c.logger.Warningf("Can't get infrastructure roles: %s", err) + } else { + c.InfrastructureRoles = infraRoles + } // Postgresqls clusterLw := &cache.ListWatch{ diff --git a/pkg/controller/util.go b/pkg/controller/util.go index 3b9469354..70617fa34 100644 --- a/pkg/controller/util.go +++ b/pkg/controller/util.go @@ -8,17 +8,19 @@ import ( extv1beta "k8s.io/client-go/pkg/apis/extensions/v1beta1" "github.bus.zalan.do/acid/postgres-operator/pkg/cluster" + "github.bus.zalan.do/acid/postgres-operator/pkg/spec" "github.bus.zalan.do/acid/postgres-operator/pkg/util/constants" "github.bus.zalan.do/acid/postgres-operator/pkg/util/k8sutil" ) func (c *Controller) makeClusterConfig() cluster.Config { return cluster.Config{ - KubeClient: c.KubeClient, - RestClient: c.RestClient, - EtcdClient: c.EtcdClient, - TeamsAPIClient: c.TeamsAPIClient, - OpConfig: c.opConfig, + KubeClient: c.KubeClient, + RestClient: c.RestClient, + EtcdClient: c.EtcdClient, + TeamsAPIClient: c.TeamsAPIClient, + OpConfig: c.opConfig, + InfrastructureRoles: c.InfrastructureRoles, } } @@ -71,3 +73,48 @@ func (c *Controller) createTPR() error { return k8sutil.WaitTPRReady(restClient, c.opConfig.TPR.ReadyWaitInterval, c.opConfig.TPR.ReadyWaitTimeout, c.PodNamespace) } + +func (c *Controller) getInfrastructureRoles() (result map[string]spec.PgUser, err error) { + if c.opConfig.InfrastructureRolesSecretName == "" { + // we don't have infrastructure roles defined, bail out + return nil, nil + } + infraRolesSecret, err := c.KubeClient.Secrets(api.NamespaceDefault).Get(c.opConfig.InfrastructureRolesSecretName) + if err != nil { + c.logger.Debugf("Infrastructure roles secret name: %s", c.opConfig.InfrastructureRolesSecretName) + return nil, fmt.Errorf("Can't get infrastructure roles Secret: %s", err) + } + data := infraRolesSecret.Data + result = make(map[string]spec.PgUser) +Users: + // in worst case we would have one line per user + for i := 1; i <= len(data); i++ { + properties := []string{"user", "password", "inrole"} + t := spec.PgUser{} + for _, p := range properties { + key := fmt.Sprintf("%s%d", p, i) + if val, present := data[key]; !present { + if p == "user" { + // exit when the user name with the next sequence id is absent + break Users + } + } else { + s := string(val) + switch p { + case "user": + t.Name = s + case "password": + t.Password = s + case "inrole": + t.MemberOf = s + default: + c.logger.Warnf("Unknown key %s", p) + } + } + } + if t.Name != "" { + result[t.Name] = t + } + } + return result, nil +} diff --git a/pkg/spec/postgresql.go b/pkg/spec/postgresql.go index de06ce77d..4d85c3157 100644 --- a/pkg/spec/postgresql.go +++ b/pkg/spec/postgresql.go @@ -79,7 +79,7 @@ type PostgresSpec struct { NumberOfInstances int32 `json:"numberOfInstances"` Users map[string]UserFlags `json:"users"` MaintenanceWindows []MaintenanceWindow `json:"maintenanceWindows,omitempty"` - ClusterName string `json:"-"` + ClusterName string `json:"-"` } type PostgresqlList struct { @@ -193,7 +193,7 @@ type PostgresqlCopy Postgresql func clusterName(clusterName string, teamName string) (string, error) { teamNameLen := len(teamName) - if len(clusterName) < teamNameLen + 2 { + if len(clusterName) < teamNameLen+2 { return "", fmt.Errorf("Name is too short") } if strings.ToLower(clusterName[:teamNameLen]) != strings.ToLower(teamName) { diff --git a/pkg/spec/types.go b/pkg/spec/types.go index 656e9fa5b..59babbe23 100644 --- a/pkg/spec/types.go +++ b/pkg/spec/types.go @@ -37,4 +37,5 @@ type PgUser struct { Name string Password string Flags []string + MemberOf string } diff --git a/pkg/util/config/config.go b/pkg/util/config/config.go index 63821bcc5..90ca7c895 100644 --- a/pkg/util/config/config.go +++ b/pkg/util/config/config.go @@ -24,12 +24,13 @@ type Resources struct { } type Auth struct { - PamRoleName string `split_words:"true" default:"zalandos"` - PamConfiguration string `split_words:"true" default:"https://info.example.com/oauth2/tokeninfo?access_token= uid realm=/employees"` - TeamsAPIUrl string `envconfig:"teams_api_url" default:"https://teams.example.com/api/"` - OAuthTokenSecretName string `envconfig:"oauth_token_secret_name" default:"postgresql-operator"` - SuperUsername string `split_words:"true" default:"postgres"` - ReplicationUsername string `split_words:"true" default:"replication"` + PamRoleName string `split_words:"true" default:"zalandos"` + PamConfiguration string `split_words:"true" default:"https://info.example.com/oauth2/tokeninfo?access_token= uid realm=/employees"` + TeamsAPIUrl string `envconfig:"teams_api_url" default:"https://teams.example.com/api/"` + OAuthTokenSecretName string `envconfig:"oauth_token_secret_name" default:"postgresql-operator"` + InfrastructureRolesSecretName string `split_words:"true"` + SuperUsername string `split_words:"true" default:"postgres"` + ReplicationUsername string `split_words:"true" default:"replication"` } type Config struct { diff --git a/pkg/util/teams/teams.go b/pkg/util/teams/teams.go index 788cdd372..1b6d54f28 100644 --- a/pkg/util/teams/teams.go +++ b/pkg/util/teams/teams.go @@ -1,10 +1,10 @@ package teams import ( - "fmt" - "strings" "encoding/json" + "fmt" "net/http" + "strings" "github.com/Sirupsen/logrus" )