Create human users from teams api

This commit is contained in:
Murat Kabilov 2017-02-16 15:01:13 +01:00
parent 58506634c4
commit 6e2d64bd50
11 changed files with 287 additions and 117 deletions

View File

@ -9,7 +9,9 @@ import (
"syscall" "syscall"
"github.bus.zalan.do/acid/postgres-operator/pkg/controller" "github.bus.zalan.do/acid/postgres-operator/pkg/controller"
"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"
"github.bus.zalan.do/acid/postgres-operator/pkg/util/teams"
) )
var ( var (
@ -43,10 +45,12 @@ func ControllerConfig() *controller.Config {
restClient, err := k8sutil.KubernetesRestClient(restConfig) restClient, err := k8sutil.KubernetesRestClient(restConfig)
teamsApi := teams.NewTeamsAPI(constants.TeamsAPIUrl)
return &controller.Config{ return &controller.Config{
Namespace: Namespace, Namespace: Namespace,
KubeClient: client, KubeClient: client,
RestClient: restClient, RestClient: restClient,
TeamsAPIClient: teamsApi,
} }
} }

View File

@ -1,6 +1,9 @@
FROM alpine FROM alpine
MAINTAINER Team ACID @ Zalando <team-acid@zalando.de> MAINTAINER Team ACID @ Zalando <team-acid@zalando.de>
# We need root certificates for dealing with teams api over https
RUN apk --no-cache add ca-certificates
COPY build/* / COPY build/* /
ENTRYPOINT ["/postgres-operator"] ENTRYPOINT ["/postgres-operator"]

View File

@ -0,0 +1,13 @@
apiVersion: "zalando.org/v1"
kind: PlatformCredentialsSet
metadata:
name: postgresql-operator
spec:
application: postgresql-operator
tokens:
read-only:
privileges:
cluster-registry-rw:
privileges:
cluster-rw:
privileges:

View File

@ -21,10 +21,11 @@ import (
"github.bus.zalan.do/acid/postgres-operator/pkg/util" "github.bus.zalan.do/acid/postgres-operator/pkg/util"
"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/retryutil" "github.bus.zalan.do/acid/postgres-operator/pkg/util/retryutil"
"github.bus.zalan.do/acid/postgres-operator/pkg/util/teams"
) )
var ( var (
superUsername = "superuser" superuserName = "postgres"
replicationUsername = "replication" replicationUsername = "replication"
alphaNumericRegexp = regexp.MustCompile("^[a-zA-Z0-9]*$") alphaNumericRegexp = regexp.MustCompile("^[a-zA-Z0-9]*$")
@ -32,10 +33,11 @@ var (
//TODO: remove struct duplication //TODO: remove struct duplication
type Config struct { type Config struct {
Namespace string Namespace string
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
} }
type pgUser struct { type pgUser struct {
@ -45,15 +47,15 @@ type pgUser struct {
} }
type Cluster struct { type Cluster struct {
logger *logrus.Entry
config Config config Config
logger *logrus.Entry
etcdHost string etcdHost string
dockerImage string dockerImage string
cluster *spec.Postgresql cluster *spec.Postgresql
pgUsers map[string]pgUser pgUsers map[string]pgUser
pgDb *sql.DB pgDb *sql.DB
mu sync.Mutex mu sync.Mutex
} }
func New(cfg Config, spec *spec.Postgresql) *Cluster { func New(cfg Config, spec *spec.Postgresql) *Cluster {
@ -71,6 +73,37 @@ func New(cfg Config, spec *spec.Postgresql) *Cluster {
return cluster return cluster
} }
func (c *Cluster) getReadonlyToken() (string, error) {
credentialsSecret, err := c.config.KubeClient.Secrets(c.config.Namespace).Get("postgresql-operator")
if err != nil {
return "", fmt.Errorf("Can't get credentials secret: %s", err)
}
data := credentialsSecret.Data
if string(data["read-only-token-type"]) != "Bearer" {
return "", fmt.Errorf("Wrong token type: %s", data["read-only-token-type"])
}
return string(data["read-only-token-secret"]), nil
}
func (c *Cluster) getTeamMembers() ([]string, error) {
token, err := c.getReadonlyToken()
if err != nil {
return nil, fmt.Errorf("Can't get oauth token: %s", err)
}
c.config.TeamsAPIClient.OauthToken = token
teamInfo, err := c.config.TeamsAPIClient.TeamInfo((*c.cluster.Spec).TeamId)
if err != nil {
return nil, fmt.Errorf("Can't get team info: %s", err)
}
return teamInfo.Members, nil
}
func (c *Cluster) labelsSet() labels.Set { func (c *Cluster) labelsSet() labels.Set {
return labels.Set{ return labels.Set{
"application": "spilo", "application": "spilo",
@ -78,10 +111,10 @@ func (c *Cluster) labelsSet() labels.Set {
} }
} }
func (c *Cluster) credentialSecretName(userName string) string { func (c *Cluster) credentialSecretName(username string) string {
return fmt.Sprintf( return fmt.Sprintf(
"%s.%s.credentials.%s.%s", "%s.%s.credentials.%s.%s",
userName, username,
(*c.cluster).Metadata.Name, (*c.cluster).Metadata.Name,
constants.TPRName, constants.TPRName,
constants.TPRVendor) constants.TPRVendor)
@ -91,7 +124,7 @@ func isValidUsername(username string) bool {
return alphaNumericRegexp.MatchString(username) return alphaNumericRegexp.MatchString(username)
} }
func validateUserFlags(userFlags []string) (flags []string, err error) { func normalizeUserFlags(userFlags []string) (flags []string, err error) {
uniqueFlags := make(map[string]bool) uniqueFlags := make(map[string]bool)
for _, flag := range userFlags { for _, flag := range userFlags {
@ -106,10 +139,6 @@ func validateUserFlags(userFlags []string) (flags []string, err error) {
} }
} }
if _, ok := uniqueFlags["NOLOGIN"]; !ok {
uniqueFlags["LOGIN"] = true
}
flags = []string{} flags = []string{}
for k := range uniqueFlags { for k := range uniqueFlags {
flags = append(flags, k) flags = append(flags, k)
@ -122,8 +151,8 @@ func (c *Cluster) init() {
users := (*c.cluster.Spec).Users users := (*c.cluster.Spec).Users
c.pgUsers = make(map[string]pgUser, len(users)+2) // + [superuser and replication] c.pgUsers = make(map[string]pgUser, len(users)+2) // + [superuser and replication]
c.pgUsers[superUsername] = pgUser{ c.pgUsers[superuserName] = pgUser{
name: superUsername, name: superuserName,
password: util.RandomPassword(constants.PasswordLength), password: util.RandomPassword(constants.PasswordLength),
} }
@ -132,27 +161,35 @@ func (c *Cluster) init() {
password: util.RandomPassword(constants.PasswordLength), password: util.RandomPassword(constants.PasswordLength),
} }
for userName, userFlags := range users { for username, userFlags := range users {
if !isValidUsername(userName) { if !isValidUsername(username) {
c.logger.Warningf("Invalid '%s' username", userName) c.logger.Warningf("Invalid username: '%s'", username)
continue continue
} }
flags, err := validateUserFlags(userFlags) flags, err := normalizeUserFlags(userFlags)
if err != nil { if err != nil {
c.logger.Warningf("Invalid flags for user '%s': %s", userName, err) c.logger.Warningf("Invalid flags for user '%s': %s", username, err)
} }
c.pgUsers[userName] = pgUser{ c.pgUsers[username] = pgUser{
name: userName, name: username,
password: util.RandomPassword(constants.PasswordLength), password: util.RandomPassword(constants.PasswordLength),
flags: flags, flags: flags,
} }
} }
teamMembers, err := c.getTeamMembers()
if err != nil {
c.logger.Errorf("Can't get list of team members: %s", err)
} else {
for _, username := range teamMembers {
c.pgUsers[username] = pgUser{name: username}
}
}
} }
func (c *Cluster) waitPodDelete() error {
func (c *Cluster) waitPodsDestroy() error {
ls := c.labelsSet() ls := c.labelsSet()
listOptions := v1.ListOptions{ listOptions := v1.ListOptions{
@ -227,7 +264,7 @@ func (c *Cluster) waitPodLabelsReady() error {
} }
func (c *Cluster) Create() error { func (c *Cluster) Create() error {
c.createEndPoint() c.createEndpoint()
c.createService() c.createService()
c.applySecrets() c.applySecrets()
c.createStatefulSet() c.createStatefulSet()
@ -268,7 +305,7 @@ func (c *Cluster) waitClusterReady() error {
func (c *Cluster) Delete() error { func (c *Cluster) Delete() error {
clusterName := (*c.cluster).Metadata.Name clusterName := (*c.cluster).Metadata.Name
nameSpace := c.config.Namespace namespace := c.config.Namespace
orphanDependents := false orphanDependents := false
deleteOptions := &v1.DeleteOptions{ deleteOptions := &v1.DeleteOptions{
OrphanDependents: &orphanDependents, OrphanDependents: &orphanDependents,
@ -280,54 +317,54 @@ func (c *Cluster) Delete() error {
kubeClient := c.config.KubeClient kubeClient := c.config.KubeClient
podList, err := kubeClient.Pods(nameSpace).List(listOptions) podList, err := kubeClient.Pods(namespace).List(listOptions)
if err != nil { if err != nil {
return fmt.Errorf("Can't get list of pods: %s", err) return fmt.Errorf("Can't get list of pods: %s", err)
} }
err = kubeClient.StatefulSets(nameSpace).Delete(clusterName, deleteOptions) err = kubeClient.StatefulSets(namespace).Delete(clusterName, deleteOptions)
if err != nil { if err != nil {
return fmt.Errorf("Can't delete statefulset: %s", err) return fmt.Errorf("Can't delete statefulset: %s", err)
} }
c.logger.Infof("StatefulSet %s.%s has been deleted", nameSpace, clusterName) c.logger.Infof("Statefulset '%s' has been deleted", util.FullObjectName(namespace, clusterName))
for _, pod := range podList.Items { for _, pod := range podList.Items {
err = kubeClient.Pods(nameSpace).Delete(pod.Name, deleteOptions) err = kubeClient.Pods(namespace).Delete(pod.Name, deleteOptions)
if err != nil { if err != nil {
return fmt.Errorf("Error while deleting pod %s.%s: %s", pod.Name, pod.Namespace, err) return fmt.Errorf("Error while deleting pod '%s': %s", util.FullObjectName(pod.Namespace, pod.Name), err)
} }
c.logger.Infof("Pod %s.%s has been deleted", pod.Name, pod.Namespace) c.logger.Infof("Pod '%s' has been deleted", util.FullObjectName(pod.Namespace, pod.Name))
} }
serviceList, err := kubeClient.Services(nameSpace).List(listOptions) serviceList, err := kubeClient.Services(namespace).List(listOptions)
if err != nil { if err != nil {
return fmt.Errorf("Can't get list of the services: %s", err) return fmt.Errorf("Can't get list of the services: %s", err)
} }
for _, service := range serviceList.Items { for _, service := range serviceList.Items {
err = kubeClient.Services(nameSpace).Delete(service.Name, deleteOptions) err = kubeClient.Services(namespace).Delete(service.Name, deleteOptions)
if err != nil { if err != nil {
return fmt.Errorf("Can't delete service %s.%s: %s", service.Name, service.Namespace, err) return fmt.Errorf("Can't delete service '%s': %s", util.FullObjectName(service.Namespace, service.Name), err)
} }
c.logger.Infof("Service %s.%s has been deleted", service.Name, service.Namespace) c.logger.Infof("Service '%s' has been deleted", util.FullObjectName(service.Namespace, service.Name))
} }
secretsList, err := kubeClient.Secrets(nameSpace).List(listOptions) secretsList, err := kubeClient.Secrets(namespace).List(listOptions)
if err != nil { if err != nil {
return err return err
} }
for _, secret := range secretsList.Items { for _, secret := range secretsList.Items {
err = kubeClient.Secrets(nameSpace).Delete(secret.Name, deleteOptions) err = kubeClient.Secrets(namespace).Delete(secret.Name, deleteOptions)
if err != nil { if err != nil {
return fmt.Errorf("Can't delete secret %s.%s: %s", secret.Name, secret.Namespace, err) return fmt.Errorf("Can't delete secret '%s': %s", util.FullObjectName(secret.Namespace, secret.Name), err)
} }
c.logger.Infof("Secret %s.%s has been deleted", secret.Name, secret.Namespace) c.logger.Infof("Secret '%s' has been deleted", util.FullObjectName(secret.Namespace, secret.Name))
} }
c.waitPodsDestroy() c.waitPodDelete()
etcdKey := fmt.Sprintf("/service/%s", clusterName) etcdKey := fmt.Sprintf("/service/%s", clusterName)

View File

@ -10,14 +10,16 @@ import (
func (c *Cluster) pgConnectionString() string { func (c *Cluster) pgConnectionString() string {
hostname := fmt.Sprintf("%s.%s.svc.cluster.local", (*c.cluster).Metadata.Name, (*c.cluster).Metadata.Namespace) hostname := fmt.Sprintf("%s.%s.svc.cluster.local", (*c.cluster).Metadata.Name, (*c.cluster).Metadata.Namespace)
password := c.pgUsers[superUsername].password password := c.pgUsers[superuserName].password
return fmt.Sprintf("host='%s' dbname=postgres sslmode=require user=postgres password='%s'", return fmt.Sprintf("host='%s' dbname=postgres sslmode=require user='%s' password='%s'",
hostname, hostname,
superuserName,
strings.Replace(password, "$", "\\$", -1)) strings.Replace(password, "$", "\\$", -1))
} }
func (c *Cluster) initDbConn() error { func (c *Cluster) initDbConn() error {
//TODO: concurrent safe?
if c.pgDb == nil { if c.pgDb == nil {
c.mu.Lock() c.mu.Lock()
defer c.mu.Unlock() defer c.mu.Unlock()

View File

@ -1,26 +1,23 @@
package cluster package cluster
import ( import (
"fmt"
"strings"
"k8s.io/client-go/pkg/api/resource" "k8s.io/client-go/pkg/api/resource"
"k8s.io/client-go/pkg/api/v1" "k8s.io/client-go/pkg/api/v1"
"k8s.io/client-go/pkg/apis/apps/v1beta1" "k8s.io/client-go/pkg/apis/apps/v1beta1"
"k8s.io/client-go/pkg/util/intstr" "k8s.io/client-go/pkg/util/intstr"
"fmt" "github.bus.zalan.do/acid/postgres-operator/pkg/util"
"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"
"strings"
) )
var createUserSQL = `DO $$ var createUserSQL = `DO $$
BEGIN BEGIN
SET local synchronous_commit = 'local'; SET LOCAL synchronous_commit = 'local';
PERFORM * FROM pg_authid WHERE rolname = '%s'; CREATE ROLE "%s" %s PASSWORD %s;
IF FOUND THEN
ALTER ROLE "%s" WITH %s PASSWORD '%s';
ELSE
CREATE ROLE "%s" WITH %s PASSWORD '%s';
END IF;
END; END;
$$` $$`
@ -63,7 +60,7 @@ func (c *Cluster) createStatefulSet() {
ValueFrom: &v1.EnvVarSource{ ValueFrom: &v1.EnvVarSource{
SecretKeyRef: &v1.SecretKeySelector{ SecretKeyRef: &v1.SecretKeySelector{
LocalObjectReference: v1.LocalObjectReference{ LocalObjectReference: v1.LocalObjectReference{
Name: c.credentialSecretName("superuser"), Name: c.credentialSecretName(superuserName),
}, },
Key: "password", Key: "password",
}, },
@ -74,15 +71,15 @@ func (c *Cluster) createStatefulSet() {
ValueFrom: &v1.EnvVarSource{ ValueFrom: &v1.EnvVarSource{
SecretKeyRef: &v1.SecretKeySelector{ SecretKeyRef: &v1.SecretKeySelector{
LocalObjectReference: v1.LocalObjectReference{ LocalObjectReference: v1.LocalObjectReference{
Name: c.credentialSecretName("replication"), Name: c.credentialSecretName(replicationUsername),
}, },
Key: "password", Key: "password",
}, },
}, },
}, },
{ {
Name: "PAM_OAUTH2", //TODO: get from the operator tpr spec Name: "PAM_OAUTH2", //TODO: get from the operator tpr spec
Value: "https://info.example.com/oauth2/tokeninfo?access_token= uid realm=/employees", //space before uid is obligatory Value: constants.PamConfiguration, //space before uid is obligatory
}, },
{ {
Name: "SPILO_CONFIGURATION", //TODO: get from the operator tpr spec Name: "SPILO_CONFIGURATION", //TODO: get from the operator tpr spec
@ -93,10 +90,16 @@ bootstrap:
initdb: initdb:
- auth-host: md5 - auth-host: md5
- auth-local: trust - auth-local: trust
users:
%s:
password: NULL
options:
- createdb
- nologin
pg_hba: pg_hba:
- hostnossl all all all reject - hostnossl all all all reject
- hostssl all +%s all pam - hostssl all +%s all pam
- hostssl all all all md5`, (*c.cluster.Spec).Version, constants.PamRoleName), - hostssl all all all md5`, (*c.cluster.Spec).Version, constants.PamRoleName, constants.PamRoleName),
}, },
} }
@ -169,17 +172,24 @@ bootstrap:
}, },
} }
c.config.KubeClient.StatefulSets(c.config.Namespace).Create(statefulSet) _, err := c.config.KubeClient.StatefulSets(c.config.Namespace).Create(statefulSet)
if err != nil {
c.logger.Errorf("Can't create statefulset: %s", err)
} else {
c.logger.Infof("Statefulset has been created: '%s'", util.FullObjectNameFromMeta(statefulSet.ObjectMeta))
}
} }
func (c *Cluster) applySecrets() { func (c *Cluster) applySecrets() {
//TODO: do not override current secrets
var err error var err error
for userName, pgUser := range c.pgUsers { for username, pgUser := range c.pgUsers {
//Skip users with no password i.e. human users (they'll be authenticated using pam)
if pgUser.password == "" {
continue
}
secret := v1.Secret{ secret := v1.Secret{
ObjectMeta: v1.ObjectMeta{ ObjectMeta: v1.ObjectMeta{
Name: c.credentialSecretName(userName), Name: c.credentialSecretName(username),
Labels: c.labelsSet(), Labels: c.labelsSet(),
}, },
Type: v1.SecretTypeOpaque, Type: v1.SecretTypeOpaque,
@ -192,32 +202,24 @@ func (c *Cluster) applySecrets() {
if k8sutil.IsKubernetesResourceAlreadyExistError(err) { if k8sutil.IsKubernetesResourceAlreadyExistError(err) {
c.logger.Infof("Skipping update of '%s'", secret.Name) c.logger.Infof("Skipping update of '%s'", secret.Name)
curSecrets, err := c.config.KubeClient.Secrets(c.config.Namespace).Get(c.credentialSecretName(userName)) curSecrets, err := c.config.KubeClient.Secrets(c.config.Namespace).Get(c.credentialSecretName(username))
if err != nil { if err != nil {
c.logger.Errorf("Can't get current secret: %s", err) c.logger.Errorf("Can't get current secret: %s", err)
} }
user := pgUser user := pgUser
user.password = string(curSecrets.Data["password"]) user.password = string(curSecrets.Data["password"])
c.pgUsers[userName] = user c.pgUsers[username] = user
c.logger.Infof("Password fetched for user '%s' from the secrets", userName) c.logger.Infof("Password fetched for user '%s' from the secrets", username)
continue continue
//_, err = c.config.KubeClient.Secrets(c.config.Namespace).Update(&secret)
//if err != nil {
// c.logger.Errorf("Error while updating secret: %+v", err)
//} else {
// c.logger.Infof("Secret updated: %+v", secret)
//}
} else { } else {
if err != nil { if err != nil {
c.logger.Errorf("Error while creating secret: %+v", err) c.logger.Errorf("Error while creating secret: %s", err)
} else { } else {
c.logger.Infof("Secret created: %s", secret.Name) c.logger.Infof("Secret created: '%s'", util.FullObjectNameFromMeta(secret.ObjectMeta))
} }
} }
} }
//TODO: remove secrets of the deleted users
} }
func (c *Cluster) createService() { func (c *Cluster) createService() {
@ -245,11 +247,11 @@ func (c *Cluster) createService() {
if err != nil { if err != nil {
c.logger.Errorf("Error while creating service: %+v", err) c.logger.Errorf("Error while creating service: %+v", err)
} else { } else {
c.logger.Infof("Service created: %s", service.Name) c.logger.Infof("Service created: '%s'", util.FullObjectNameFromMeta(service.ObjectMeta))
} }
} }
func (c *Cluster) createEndPoint() { func (c *Cluster) createEndpoint() {
clusterName := (*c.cluster).Metadata.Name clusterName := (*c.cluster).Metadata.Name
_, err := c.config.KubeClient.Endpoints(c.config.Namespace).Get(clusterName) _, err := c.config.KubeClient.Endpoints(c.config.Namespace).Get(clusterName)
@ -258,18 +260,18 @@ func (c *Cluster) createEndPoint() {
return return
} }
endPoint := v1.Endpoints{ endpoint := v1.Endpoints{
ObjectMeta: v1.ObjectMeta{ ObjectMeta: v1.ObjectMeta{
Name: clusterName, Name: clusterName,
Labels: c.labelsSet(), Labels: c.labelsSet(),
}, },
} }
_, err = c.config.KubeClient.Endpoints(c.config.Namespace).Create(&endPoint) _, err = c.config.KubeClient.Endpoints(c.config.Namespace).Create(&endpoint)
if err != nil { if err != nil {
c.logger.Errorf("Error while creating endpoint: %+v", err) c.logger.Errorf("Error while creating endpoint: %+v", err)
} else { } else {
c.logger.Infof("Endpoint created: %s", endPoint.Name) c.logger.Infof("Endpoint created: %s", endpoint.Name)
} }
} }
@ -279,28 +281,40 @@ func (c *Cluster) createUser(user pgUser) {
if user.password == "" { if user.password == "" {
userType = "human" userType = "human"
flags = append(flags, fmt.Sprintf("IN ROLE '%s'", constants.PamRoleName)) flags = append(flags, fmt.Sprintf("IN ROLE \"%s\"", constants.PamRoleName))
} else { } else {
userType = "app" userType = "app"
} }
addLoginFlag := true
for _, v := range flags {
if v == "NOLOGIN" {
addLoginFlag = false
break
}
}
if addLoginFlag {
flags = append(flags, "LOGIN")
}
userFlags := strings.Join(flags, " ") userFlags := strings.Join(flags, " ")
query := fmt.Sprintf(createUserSQL, userPassword := fmt.Sprintf("'%s'", user.password)
user.name, if user.password == "" {
user.name, userFlags, user.password, userPassword = "NULL"
user.name, userFlags, user.password) }
query := fmt.Sprintf(createUserSQL, user.name, userFlags, userPassword)
_, err := c.pgDb.Query(query) _, err := c.pgDb.Query(query)
if err != nil { if err != nil {
c.logger.Errorf("Can't create %s user '%s': %s", user.name, err) c.logger.Errorf("Can't create %s user '%s': %s", user.name, err)
} else { } else {
c.logger.Infof("%s user '%s' with flags %s has been created", userType, user.name, flags) c.logger.Infof("Created %s user '%s' with %s flags", userType, user.name, flags)
} }
} }
func (c *Cluster) createUsers() error { func (c *Cluster) createUsers() error {
for userName, user := range c.pgUsers { for username, user := range c.pgUsers {
if userName == superUsername || userName == replicationUsername { if username == superuserName || username == replicationUsername {
continue continue
} }

View File

@ -15,20 +15,22 @@ import (
"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/spec"
"github.bus.zalan.do/acid/postgres-operator/pkg/util"
"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"
"github.bus.zalan.do/acid/postgres-operator/pkg/util/teams"
) )
type Config struct { type Config struct {
Namespace string Namespace string
KubeClient *kubernetes.Clientset KubeClient *kubernetes.Clientset
RestClient *rest.RESTClient RestClient *rest.RESTClient
EtcdClient etcdclient.KeysAPI EtcdClient etcdclient.KeysAPI
TeamsAPIClient *teams.TeamsAPI
} }
type Controller struct { type Controller struct {
config Config config Config
logger *logrus.Entry logger *logrus.Entry
events chan *Event events chan *Event
clusters map[string]*cluster.Cluster clusters map[string]*cluster.Cluster
@ -60,7 +62,6 @@ func (c *Controller) Run(stopCh <-chan struct{}, wg *sync.WaitGroup) {
err := c.initEtcdClient() err := c.initEtcdClient()
if err != nil { if err != nil {
c.logger.Errorf("Can't get etcd client: %s", err) c.logger.Errorf("Can't get etcd client: %s", err)
return return
} }
@ -76,9 +77,10 @@ func (c *Controller) watchTpr(stopCh <-chan struct{}) {
} }
func (c *Controller) createTPR() error { func (c *Controller) createTPR() error {
TPRName := fmt.Sprintf("%s.%s", constants.TPRName, constants.TPRVendor)
tpr := &v1beta1extensions.ThirdPartyResource{ tpr := &v1beta1extensions.ThirdPartyResource{
ObjectMeta: v1.ObjectMeta{ ObjectMeta: v1.ObjectMeta{
Name: fmt.Sprintf("%s.%s", constants.TPRName, constants.TPRVendor), Name: TPRName,
}, },
Versions: []v1beta1extensions.APIVersion{ Versions: []v1beta1extensions.APIVersion{
{Name: constants.TPRApiVersion}, {Name: constants.TPRApiVersion},
@ -87,15 +89,14 @@ func (c *Controller) createTPR() error {
} }
_, err := c.config.KubeClient.ExtensionsV1beta1().ThirdPartyResources().Create(tpr) _, err := c.config.KubeClient.ExtensionsV1beta1().ThirdPartyResources().Create(tpr)
if err != nil { if err != nil {
if !k8sutil.IsKubernetesResourceAlreadyExistError(err) { if !k8sutil.IsKubernetesResourceAlreadyExistError(err) {
return err return err
} else { } else {
c.logger.Info("ThirdPartyResource is already registered") c.logger.Infof("ThirdPartyResource '%s' is already registered", TPRName)
} }
} else { } else {
c.logger.Info("ThirdPartyResource has been registered") c.logger.Infof("ThirdPartyResource '%s' has been registered", TPRName)
} }
restClient := c.config.RestClient restClient := c.config.RestClient
@ -105,10 +106,11 @@ func (c *Controller) createTPR() error {
func (c *Controller) makeClusterConfig() cluster.Config { func (c *Controller) makeClusterConfig() cluster.Config {
return cluster.Config{ return cluster.Config{
Namespace: c.config.Namespace, Namespace: c.config.Namespace,
KubeClient: c.config.KubeClient, KubeClient: c.config.KubeClient,
RestClient: c.config.RestClient, RestClient: c.config.RestClient,
EtcdClient: c.config.EtcdClient, EtcdClient: c.config.EtcdClient,
TeamsAPIClient: c.config.TeamsAPIClient,
} }
} }
@ -150,7 +152,7 @@ func (c *Controller) clusterAdd(obj interface{}) {
c.stopChMap[clusterName] = make(chan struct{}) c.stopChMap[clusterName] = make(chan struct{})
c.clusters[clusterName] = cl c.clusters[clusterName] = cl
c.logger.Infof("Postgresql cluster %s.%s has been created", clusterName, (*pg).Metadata.Namespace) c.logger.Infof("Postgresql cluster '%s' has been created", util.FullObjectNameFromMeta((*pg).Metadata))
} }
func (c *Controller) clusterUpdate(prev, cur interface{}) { func (c *Controller) clusterUpdate(prev, cur interface{}) {
@ -177,12 +179,12 @@ func (c *Controller) clusterDelete(obj interface{}) {
cluster := cluster.New(c.makeClusterConfig(), pg) cluster := cluster.New(c.makeClusterConfig(), pg)
err := cluster.Delete() err := cluster.Delete()
if err != nil { if err != nil {
c.logger.Errorf("Can't delete cluster '%s.%s': %s", clusterName, (*pg).Metadata.Namespace, err) c.logger.Errorf("Can't delete cluster '%s': %s", util.FullObjectNameFromMeta((*pg).Metadata), err)
return return
} }
close(c.stopChMap[clusterName]) close(c.stopChMap[clusterName])
delete(c.clusters, clusterName) delete(c.clusters, clusterName)
c.logger.Infof("Cluster delete: %s", (*pg).Metadata.Name) c.logger.Infof("Cluster has been deleted: '%s'", util.FullObjectNameFromMeta((*pg).Metadata))
} }

View File

@ -3,9 +3,9 @@ package spec
import ( import (
"encoding/json" "encoding/json"
"k8s.io/client-go/pkg/api"
"k8s.io/client-go/pkg/api/meta" "k8s.io/client-go/pkg/api/meta"
"k8s.io/client-go/pkg/api/unversioned" "k8s.io/client-go/pkg/api/unversioned"
"k8s.io/client-go/pkg/api/v1"
) )
type MaintenanceWindow struct { type MaintenanceWindow struct {
@ -55,7 +55,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 []string `json:"maintenanceWindows,omitempty"` MaintenanceWindows []string `json:"maintenanceWindows,omitempty"`
PamUsersSecret string `json:"pamUsersSecret",omitempty` PamUsersSecret string `json:"pamUsersSecret,omitempty"`
EtcdHost string EtcdHost string
DockerImage string DockerImage string
@ -81,7 +81,7 @@ type PostgresStatus struct {
// PostgreSQL Third Party (resource) Object // PostgreSQL Third Party (resource) Object
type Postgresql struct { type Postgresql struct {
unversioned.TypeMeta `json:",inline"` unversioned.TypeMeta `json:",inline"`
Metadata api.ObjectMeta `json:"metadata"` Metadata v1.ObjectMeta `json:"metadata"`
Spec *PostgresSpec `json:"spec"` Spec *PostgresSpec `json:"spec"`
Status *PostgresStatus `json:"status"` Status *PostgresStatus `json:"status"`

View File

@ -15,9 +15,12 @@ const (
ResourceName = TPRName + "s" ResourceName = TPRName + "s"
ResyncPeriod = 5 * time.Minute ResyncPeriod = 5 * time.Minute
EtcdHost = "etcd-client.default.svc.cluster.local:2379" //TODO: move to the operator spec //TODO: move to the operator spec
SpiloImage = "registry.opensource.zalan.do/acid/spilo-9.6:1.2-p11" EtcdHost = "etcd-client.default.svc.cluster.local:2379"
PamRoleName = "zalandos" SpiloImage = "registry.opensource.zalan.do/acid/spilo-9.6:1.2-p12"
PamRoleName = "zalandos"
PamConfiguration = "https://info.example.com/oauth2/tokeninfo?access_token= uid realm=/employees"
PasswordLength = 64 PasswordLength = 64
TeamsAPIUrl = "https://teams.example.com/api/"
) )

76
pkg/util/teams/teams.go Normal file
View File

@ -0,0 +1,76 @@
package teams
import (
"encoding/json"
"fmt"
"net/http"
"strings"
)
type InfrastructureAccount struct {
Id string `json:"id"`
Name string `json:"name"`
Provider string `json:"provider"`
Type string `json:"type"`
Description string `json:"description"`
Owner string `json:"owner"`
OwnerDn string `json:"owner_dn"`
Disabled bool `json:"disabled"`
}
type Team struct {
Dn string `json:"dn"`
Id string `json:"id"`
TeamName string `json:"id_name"`
TeamId string `json:"team_id"`
Type string `json:"official"`
FullName string `json:"name"`
Aliases []string `json:"alias"`
Mails []string `json:"mail"`
Members []string `json:"member"`
CostCenter string `json:"cost_center"`
DeliveryLead string `json:"delivery_lead"`
ParentTeamId string `json:"parent_team_id"`
InfrastructureAccounts []InfrastructureAccount `json:"infrastructure-accounts"`
}
type TeamsAPI struct {
url string
httpClient *http.Client
OauthToken string
}
func NewTeamsAPI(url string) *TeamsAPI {
t := TeamsAPI{
url: strings.TrimRight(url, "/"),
httpClient: &http.Client{},
}
return &t
}
func (t *TeamsAPI) TeamInfo(teamId string) (*Team, error) {
url := fmt.Sprintf("%s/teams/%s", t.url, teamId)
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return nil, err
}
req.Header.Add("Authorization", "Bearer "+t.OauthToken)
resp, err := t.httpClient.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
teamInfo := &Team{}
d := json.NewDecoder(resp.Body)
err = d.Decode(teamInfo)
if err != nil {
return nil, err
}
return teamInfo, nil
}

View File

@ -1,8 +1,11 @@
package util package util
import ( import (
"fmt"
"math/rand" "math/rand"
"time" "time"
"k8s.io/client-go/pkg/api/v1"
) )
var passwordChars = []byte("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789") var passwordChars = []byte("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789")
@ -19,3 +22,16 @@ func RandomPassword(n int) string {
return string(b) return string(b)
} }
func FullObjectNameFromMeta(meta v1.ObjectMeta) string {
return FullObjectName(meta.Namespace, meta.Name)
}
//TODO: Remove in favour of FullObjectNameFromMeta
func FullObjectName(ns, name string) string {
if ns == "" {
ns = "default"
}
return fmt.Sprintf("%s / %s", ns, name)
}