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" )