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.
This commit is contained in:
Oleksii Kliukin 2017-04-12 16:21:13 +02:00 committed by Murat Kabilov
parent b8fba429df
commit 71b93b4cc2
13 changed files with 129 additions and 33 deletions

View File

@ -48,9 +48,9 @@ func ControllerConfig() *controller.Config {
restClient, err := k8sutil.KubernetesRestClient(restConfig) restClient, err := k8sutil.KubernetesRestClient(restConfig)
return &controller.Config{ return &controller.Config{
PodNamespace: podNamespace, //TODO: move to config.Config PodNamespace: podNamespace, //TODO: move to config.Config
KubeClient: client, KubeClient: client,
RestClient: restClient, RestClient: restClient,
} }
} }

View File

@ -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

View File

@ -58,3 +58,5 @@ spec:
value: "db.example.com" value: "db.example.com"
- name: PGOP_DNS_NAME_FORMAT - name: PGOP_DNS_NAME_FORMAT
value: "%s.%s.staging.%s" value: "%s.%s.staging.%s"
- name: PGOP_INFRASTRUCTURE_ROLES_SECRET_NAME
value: "postgresql-infrastructure-roles"

View File

@ -30,15 +30,17 @@ import (
var ( var (
alphaNumericRegexp = regexp.MustCompile("^[a-zA-Z][a-zA-Z0-9]*$") 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 //TODO: remove struct duplication
type Config struct { type Config struct {
KubeClient *kubernetes.Clientset //TODO: move clients to the better place? KubeClient *kubernetes.Clientset //TODO: move clients to the better place?
RestClient *rest.RESTClient RestClient *rest.RESTClient
EtcdClient etcdclient.KeysAPI EtcdClient etcdclient.KeysAPI
TeamsAPIClient *teams.TeamsAPI TeamsAPIClient *teams.TeamsAPI
OpConfig *config.Config OpConfig *config.Config
InfrastructureRoles map[string]spec.PgUser // inherited from the controller
} }
type kubeResources struct { type kubeResources struct {
@ -122,6 +124,11 @@ func (c *Cluster) SetStatus(status spec.PostgresStatus) {
func (c *Cluster) initUsers() error { func (c *Cluster) initUsers() error {
c.initSystemUsers() c.initSystemUsers()
if err := c.initInfrastructureRoles(); err != nil {
return fmt.Errorf("Can't init infrastructure roles: %s", err)
}
if err := c.initRobotUsers(); err != nil { if err := c.initRobotUsers(); err != nil {
return fmt.Errorf("Can't init robot users: %s", err) 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) return fmt.Errorf("Can't init human users: %s", err)
} }
c.logger.Debugf("Initialized users: %# v", util.Pretty(c.pgUsers))
return nil return nil
} }
@ -400,3 +409,14 @@ func (c *Cluster) initHumanUsers() error {
return nil 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
}

View File

@ -66,7 +66,9 @@ func (c *Cluster) createPgUser(user spec.PgUser) (isHuman bool, err error) {
if addLoginFlag { if addLoginFlag {
flags = append(flags, "LOGIN") flags = append(flags, "LOGIN")
} }
if !isHuman && user.MemberOf != "" {
flags = append(flags, fmt.Sprintf("IN ROLE \"%s\"", user.MemberOf))
}
userFlags := strings.Join(flags, " ") userFlags := strings.Join(flags, " ")
userPassword := fmt.Sprintf("ENCRYPTED PASSWORD '%s'", util.PGUserPassword(user)) userPassword := fmt.Sprintf("ENCRYPTED PASSWORD '%s'", util.PGUserPassword(user))
if user.Password == "" { if user.Password == "" {

View File

@ -18,7 +18,7 @@ import (
) )
func isValidUsername(username string) bool { func isValidUsername(username string) bool {
return alphaNumericRegexp.MatchString(username) return userRegexp.MatchString(username)
} }
func normalizeUserFlags(userFlags []string) (flags []string, err error) { func normalizeUserFlags(userFlags []string) (flags []string, err error) {
@ -218,8 +218,10 @@ func (c *Cluster) dnsName() string {
} }
func (c *Cluster) credentialSecretName(username string) 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, return fmt.Sprintf(constants.UserSecretTemplate,
username, strings.Replace(username, "_", "-", -1),
c.Metadata.Name, c.Metadata.Name,
constants.TPRName, constants.TPRName,
constants.TPRVendor) constants.TPRVendor)

View File

@ -17,11 +17,12 @@ import (
) )
type Config struct { type Config struct {
PodNamespace string PodNamespace string
KubeClient *kubernetes.Clientset KubeClient *kubernetes.Clientset
RestClient *rest.RESTClient RestClient *rest.RESTClient
EtcdClient etcdclient.KeysAPI EtcdClient etcdclient.KeysAPI
TeamsAPIClient *teams.TeamsAPI TeamsAPIClient *teams.TeamsAPI
InfrastructureRoles map[string]spec.PgUser
} }
type Controller struct { type Controller struct {
@ -74,6 +75,11 @@ func (c *Controller) initController() {
} }
c.TeamsAPIClient.RefreshTokenAction = c.getOAuthToken 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 // Postgresqls
clusterLw := &cache.ListWatch{ clusterLw := &cache.ListWatch{

View File

@ -8,17 +8,19 @@ import (
extv1beta "k8s.io/client-go/pkg/apis/extensions/v1beta1" 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/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/constants"
"github.bus.zalan.do/acid/postgres-operator/pkg/util/k8sutil" "github.bus.zalan.do/acid/postgres-operator/pkg/util/k8sutil"
) )
func (c *Controller) makeClusterConfig() cluster.Config { func (c *Controller) makeClusterConfig() cluster.Config {
return cluster.Config{ return cluster.Config{
KubeClient: c.KubeClient, KubeClient: c.KubeClient,
RestClient: c.RestClient, RestClient: c.RestClient,
EtcdClient: c.EtcdClient, EtcdClient: c.EtcdClient,
TeamsAPIClient: c.TeamsAPIClient, TeamsAPIClient: c.TeamsAPIClient,
OpConfig: c.opConfig, 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) 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
}

View File

@ -79,7 +79,7 @@ type PostgresSpec struct {
NumberOfInstances int32 `json:"numberOfInstances"` NumberOfInstances int32 `json:"numberOfInstances"`
Users map[string]UserFlags `json:"users"` Users map[string]UserFlags `json:"users"`
MaintenanceWindows []MaintenanceWindow `json:"maintenanceWindows,omitempty"` MaintenanceWindows []MaintenanceWindow `json:"maintenanceWindows,omitempty"`
ClusterName string `json:"-"` ClusterName string `json:"-"`
} }
type PostgresqlList struct { type PostgresqlList struct {
@ -193,7 +193,7 @@ type PostgresqlCopy Postgresql
func clusterName(clusterName string, teamName string) (string, error) { func clusterName(clusterName string, teamName string) (string, error) {
teamNameLen := len(teamName) teamNameLen := len(teamName)
if len(clusterName) < teamNameLen + 2 { if len(clusterName) < teamNameLen+2 {
return "", fmt.Errorf("Name is too short") return "", fmt.Errorf("Name is too short")
} }
if strings.ToLower(clusterName[:teamNameLen]) != strings.ToLower(teamName) { if strings.ToLower(clusterName[:teamNameLen]) != strings.ToLower(teamName) {

View File

@ -37,4 +37,5 @@ type PgUser struct {
Name string Name string
Password string Password string
Flags []string Flags []string
MemberOf string
} }

View File

@ -24,12 +24,13 @@ type Resources struct {
} }
type Auth struct { type Auth struct {
PamRoleName string `split_words:"true" default:"zalandos"` PamRoleName string `split_words:"true" default:"zalandos"`
PamConfiguration string `split_words:"true" default:"https://info.example.com/oauth2/tokeninfo?access_token= uid realm=/employees"` 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/"` TeamsAPIUrl string `envconfig:"teams_api_url" default:"https://teams.example.com/api/"`
OAuthTokenSecretName string `envconfig:"oauth_token_secret_name" default:"postgresql-operator"` OAuthTokenSecretName string `envconfig:"oauth_token_secret_name" default:"postgresql-operator"`
SuperUsername string `split_words:"true" default:"postgres"` InfrastructureRolesSecretName string `split_words:"true"`
ReplicationUsername string `split_words:"true" default:"replication"` SuperUsername string `split_words:"true" default:"postgres"`
ReplicationUsername string `split_words:"true" default:"replication"`
} }
type Config struct { type Config struct {

View File

@ -1,10 +1,10 @@
package teams package teams
import ( import (
"fmt"
"strings"
"encoding/json" "encoding/json"
"fmt"
"net/http" "net/http"
"strings"
"github.com/Sirupsen/logrus" "github.com/Sirupsen/logrus"
) )