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:
		
							parent
							
								
									b8fba429df
								
							
						
					
					
						commit
						71b93b4cc2
					
				|  | @ -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, | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -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 | ||||||
|  | @ -57,4 +57,6 @@ spec: | ||||||
|         - name: PGOP_DB_HOSTED_ZONE |         - name: PGOP_DB_HOSTED_ZONE | ||||||
|           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" | ||||||
|  |  | ||||||
|  | @ -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 | ||||||
|  | } | ||||||
|  |  | ||||||
|  | @ -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 == "" { | ||||||
|  |  | ||||||
|  | @ -122,4 +122,4 @@ func (c *Cluster) syncStatefulSet() error { | ||||||
| 	c.logger.Infof("Pods have been recreated") | 	c.logger.Infof("Pods have been recreated") | ||||||
| 
 | 
 | ||||||
| 	return nil | 	return nil | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -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) | ||||||
|  |  | ||||||
|  | @ -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{ | ||||||
|  |  | ||||||
|  | @ -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 | ||||||
|  | } | ||||||
|  |  | ||||||
|  | @ -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) { | ||||||
|  |  | ||||||
|  | @ -37,4 +37,5 @@ type PgUser struct { | ||||||
| 	Name     string | 	Name     string | ||||||
| 	Password string | 	Password string | ||||||
| 	Flags    []string | 	Flags    []string | ||||||
|  | 	MemberOf string | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -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 { | ||||||
|  |  | ||||||
|  | @ -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" | ||||||
| ) | ) | ||||||
|  |  | ||||||
		Loading…
	
		Reference in New Issue