diff --git a/cmd/main.go b/cmd/main.go index e95ad3563..5f0e83db4 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -9,7 +9,9 @@ import ( "syscall" "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/teams" ) var ( @@ -43,10 +45,12 @@ func ControllerConfig() *controller.Config { restClient, err := k8sutil.KubernetesRestClient(restConfig) + teamsApi := teams.NewTeamsAPI(constants.TeamsAPIUrl) return &controller.Config{ - Namespace: Namespace, - KubeClient: client, - RestClient: restClient, + Namespace: Namespace, + KubeClient: client, + RestClient: restClient, + TeamsAPIClient: teamsApi, } } diff --git a/docker/Dockerfile b/docker/Dockerfile index c55a82abd..552d4f621 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -1,6 +1,9 @@ FROM alpine MAINTAINER Team ACID @ Zalando +# We need root certificates for dealing with teams api over https +RUN apk --no-cache add ca-certificates + COPY build/* / ENTRYPOINT ["/postgres-operator"] diff --git a/manifests/platform-credentials.yaml b/manifests/platform-credentials.yaml new file mode 100644 index 000000000..44ecf7f24 --- /dev/null +++ b/manifests/platform-credentials.yaml @@ -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: diff --git a/pkg/cluster/cluster.go b/pkg/cluster/cluster.go index 85ceeca70..5c687e117 100644 --- a/pkg/cluster/cluster.go +++ b/pkg/cluster/cluster.go @@ -21,10 +21,11 @@ import ( "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/retryutil" + "github.bus.zalan.do/acid/postgres-operator/pkg/util/teams" ) var ( - superUsername = "superuser" + superuserName = "postgres" replicationUsername = "replication" alphaNumericRegexp = regexp.MustCompile("^[a-zA-Z0-9]*$") @@ -32,10 +33,11 @@ var ( //TODO: remove struct duplication type Config struct { - Namespace string - KubeClient *kubernetes.Clientset //TODO: move clients to the better place? - RestClient *rest.RESTClient - EtcdClient etcdclient.KeysAPI + Namespace string + KubeClient *kubernetes.Clientset //TODO: move clients to the better place? + RestClient *rest.RESTClient + EtcdClient etcdclient.KeysAPI + TeamsAPIClient *teams.TeamsAPI } type pgUser struct { @@ -45,15 +47,15 @@ type pgUser struct { } type Cluster struct { - logger *logrus.Entry config Config + logger *logrus.Entry etcdHost string dockerImage string cluster *spec.Postgresql pgUsers map[string]pgUser - pgDb *sql.DB - mu sync.Mutex + pgDb *sql.DB + mu sync.Mutex } func New(cfg Config, spec *spec.Postgresql) *Cluster { @@ -71,6 +73,37 @@ func New(cfg Config, spec *spec.Postgresql) *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 { return labels.Set{ "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( "%s.%s.credentials.%s.%s", - userName, + username, (*c.cluster).Metadata.Name, constants.TPRName, constants.TPRVendor) @@ -91,7 +124,7 @@ func isValidUsername(username string) bool { 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) 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{} for k := range uniqueFlags { flags = append(flags, k) @@ -122,8 +151,8 @@ func (c *Cluster) init() { users := (*c.cluster.Spec).Users c.pgUsers = make(map[string]pgUser, len(users)+2) // + [superuser and replication] - c.pgUsers[superUsername] = pgUser{ - name: superUsername, + c.pgUsers[superuserName] = pgUser{ + name: superuserName, password: util.RandomPassword(constants.PasswordLength), } @@ -132,27 +161,35 @@ func (c *Cluster) init() { password: util.RandomPassword(constants.PasswordLength), } - for userName, userFlags := range users { - if !isValidUsername(userName) { - c.logger.Warningf("Invalid '%s' username", userName) + for username, userFlags := range users { + if !isValidUsername(username) { + c.logger.Warningf("Invalid username: '%s'", username) continue } - flags, err := validateUserFlags(userFlags) + flags, err := normalizeUserFlags(userFlags) 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{ - name: userName, + c.pgUsers[username] = pgUser{ + name: username, password: util.RandomPassword(constants.PasswordLength), 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) waitPodsDestroy() error { +func (c *Cluster) waitPodDelete() error { ls := c.labelsSet() listOptions := v1.ListOptions{ @@ -227,7 +264,7 @@ func (c *Cluster) waitPodLabelsReady() error { } func (c *Cluster) Create() error { - c.createEndPoint() + c.createEndpoint() c.createService() c.applySecrets() c.createStatefulSet() @@ -268,7 +305,7 @@ func (c *Cluster) waitClusterReady() error { func (c *Cluster) Delete() error { clusterName := (*c.cluster).Metadata.Name - nameSpace := c.config.Namespace + namespace := c.config.Namespace orphanDependents := false deleteOptions := &v1.DeleteOptions{ OrphanDependents: &orphanDependents, @@ -280,54 +317,54 @@ func (c *Cluster) Delete() error { kubeClient := c.config.KubeClient - podList, err := kubeClient.Pods(nameSpace).List(listOptions) + podList, err := kubeClient.Pods(namespace).List(listOptions) if err != nil { 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 { 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 { - err = kubeClient.Pods(nameSpace).Delete(pod.Name, deleteOptions) + err = kubeClient.Pods(namespace).Delete(pod.Name, deleteOptions) 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 { return fmt.Errorf("Can't get list of the services: %s", err) } for _, service := range serviceList.Items { - err = kubeClient.Services(nameSpace).Delete(service.Name, deleteOptions) + err = kubeClient.Services(namespace).Delete(service.Name, deleteOptions) 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 { return err } for _, secret := range secretsList.Items { - err = kubeClient.Secrets(nameSpace).Delete(secret.Name, deleteOptions) + err = kubeClient.Secrets(namespace).Delete(secret.Name, deleteOptions) 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) diff --git a/pkg/cluster/pg.go b/pkg/cluster/pg.go index a99ff1bfd..408aa4953 100644 --- a/pkg/cluster/pg.go +++ b/pkg/cluster/pg.go @@ -10,14 +10,16 @@ import ( func (c *Cluster) pgConnectionString() string { 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, + superuserName, strings.Replace(password, "$", "\\$", -1)) } func (c *Cluster) initDbConn() error { + //TODO: concurrent safe? if c.pgDb == nil { c.mu.Lock() defer c.mu.Unlock() diff --git a/pkg/cluster/resources.go b/pkg/cluster/resources.go index 8ac6b857d..10bc6f95c 100644 --- a/pkg/cluster/resources.go +++ b/pkg/cluster/resources.go @@ -1,26 +1,23 @@ package cluster import ( + "fmt" + "strings" + "k8s.io/client-go/pkg/api/resource" "k8s.io/client-go/pkg/api/v1" "k8s.io/client-go/pkg/apis/apps/v1beta1" "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/k8sutil" - "strings" ) var createUserSQL = `DO $$ BEGIN - SET local synchronous_commit = 'local'; - PERFORM * FROM pg_authid WHERE rolname = '%s'; - IF FOUND THEN - ALTER ROLE "%s" WITH %s PASSWORD '%s'; - ELSE - CREATE ROLE "%s" WITH %s PASSWORD '%s'; - END IF; + SET LOCAL synchronous_commit = 'local'; + CREATE ROLE "%s" %s PASSWORD %s; END; $$` @@ -63,7 +60,7 @@ func (c *Cluster) createStatefulSet() { ValueFrom: &v1.EnvVarSource{ SecretKeyRef: &v1.SecretKeySelector{ LocalObjectReference: v1.LocalObjectReference{ - Name: c.credentialSecretName("superuser"), + Name: c.credentialSecretName(superuserName), }, Key: "password", }, @@ -74,15 +71,15 @@ func (c *Cluster) createStatefulSet() { ValueFrom: &v1.EnvVarSource{ SecretKeyRef: &v1.SecretKeySelector{ LocalObjectReference: v1.LocalObjectReference{ - Name: c.credentialSecretName("replication"), + Name: c.credentialSecretName(replicationUsername), }, Key: "password", }, }, }, { - 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 + Name: "PAM_OAUTH2", //TODO: get from the operator tpr spec + Value: constants.PamConfiguration, //space before uid is obligatory }, { Name: "SPILO_CONFIGURATION", //TODO: get from the operator tpr spec @@ -93,10 +90,16 @@ bootstrap: initdb: - auth-host: md5 - auth-local: trust + users: + %s: + password: NULL + options: + - createdb + - nologin pg_hba: - hostnossl all all all reject - 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() { - //TODO: do not override current secrets - 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{ ObjectMeta: v1.ObjectMeta{ - Name: c.credentialSecretName(userName), + Name: c.credentialSecretName(username), Labels: c.labelsSet(), }, Type: v1.SecretTypeOpaque, @@ -192,32 +202,24 @@ func (c *Cluster) applySecrets() { if k8sutil.IsKubernetesResourceAlreadyExistError(err) { 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 { c.logger.Errorf("Can't get current secret: %s", err) } user := pgUser user.password = string(curSecrets.Data["password"]) - c.pgUsers[userName] = user - c.logger.Infof("Password fetched for user '%s' from the secrets", userName) + c.pgUsers[username] = user + c.logger.Infof("Password fetched for user '%s' from the secrets", username) 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 { if err != nil { - c.logger.Errorf("Error while creating secret: %+v", err) + c.logger.Errorf("Error while creating secret: %s", err) } 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() { @@ -245,11 +247,11 @@ func (c *Cluster) createService() { if err != nil { c.logger.Errorf("Error while creating service: %+v", err) } 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 _, err := c.config.KubeClient.Endpoints(c.config.Namespace).Get(clusterName) @@ -258,18 +260,18 @@ func (c *Cluster) createEndPoint() { return } - endPoint := v1.Endpoints{ + endpoint := v1.Endpoints{ ObjectMeta: v1.ObjectMeta{ Name: clusterName, 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 { c.logger.Errorf("Error while creating endpoint: %+v", err) } 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 == "" { userType = "human" - flags = append(flags, fmt.Sprintf("IN ROLE '%s'", constants.PamRoleName)) + flags = append(flags, fmt.Sprintf("IN ROLE \"%s\"", constants.PamRoleName)) } else { userType = "app" } + addLoginFlag := true + for _, v := range flags { + if v == "NOLOGIN" { + addLoginFlag = false + break + } + } + if addLoginFlag { + flags = append(flags, "LOGIN") + } + userFlags := strings.Join(flags, " ") - query := fmt.Sprintf(createUserSQL, - user.name, - user.name, userFlags, user.password, - user.name, userFlags, user.password) + userPassword := fmt.Sprintf("'%s'", user.password) + if user.password == "" { + userPassword = "NULL" + } + query := fmt.Sprintf(createUserSQL, user.name, userFlags, userPassword) _, err := c.pgDb.Query(query) if err != nil { c.logger.Errorf("Can't create %s user '%s': %s", user.name, err) } 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 { - for userName, user := range c.pgUsers { - if userName == superUsername || userName == replicationUsername { + for username, user := range c.pgUsers { + if username == superuserName || username == replicationUsername { continue } diff --git a/pkg/controller/controller.go b/pkg/controller/controller.go index fc0488af9..2a058e5c2 100644 --- a/pkg/controller/controller.go +++ b/pkg/controller/controller.go @@ -15,20 +15,22 @@ import ( "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" "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/teams" ) type Config struct { - Namespace string - KubeClient *kubernetes.Clientset - RestClient *rest.RESTClient - EtcdClient etcdclient.KeysAPI + Namespace string + KubeClient *kubernetes.Clientset + RestClient *rest.RESTClient + EtcdClient etcdclient.KeysAPI + TeamsAPIClient *teams.TeamsAPI } type Controller struct { - config Config - + config Config logger *logrus.Entry events chan *Event clusters map[string]*cluster.Cluster @@ -60,7 +62,6 @@ func (c *Controller) Run(stopCh <-chan struct{}, wg *sync.WaitGroup) { err := c.initEtcdClient() if err != nil { c.logger.Errorf("Can't get etcd client: %s", err) - return } @@ -76,9 +77,10 @@ func (c *Controller) watchTpr(stopCh <-chan struct{}) { } func (c *Controller) createTPR() error { + TPRName := fmt.Sprintf("%s.%s", constants.TPRName, constants.TPRVendor) tpr := &v1beta1extensions.ThirdPartyResource{ ObjectMeta: v1.ObjectMeta{ - Name: fmt.Sprintf("%s.%s", constants.TPRName, constants.TPRVendor), + Name: TPRName, }, Versions: []v1beta1extensions.APIVersion{ {Name: constants.TPRApiVersion}, @@ -87,15 +89,14 @@ func (c *Controller) createTPR() error { } _, err := c.config.KubeClient.ExtensionsV1beta1().ThirdPartyResources().Create(tpr) - if err != nil { if !k8sutil.IsKubernetesResourceAlreadyExistError(err) { return err } else { - c.logger.Info("ThirdPartyResource is already registered") + c.logger.Infof("ThirdPartyResource '%s' is already registered", TPRName) } } else { - c.logger.Info("ThirdPartyResource has been registered") + c.logger.Infof("ThirdPartyResource '%s' has been registered", TPRName) } restClient := c.config.RestClient @@ -105,10 +106,11 @@ func (c *Controller) createTPR() error { func (c *Controller) makeClusterConfig() cluster.Config { return cluster.Config{ - Namespace: c.config.Namespace, - KubeClient: c.config.KubeClient, - RestClient: c.config.RestClient, - EtcdClient: c.config.EtcdClient, + Namespace: c.config.Namespace, + KubeClient: c.config.KubeClient, + RestClient: c.config.RestClient, + 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.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{}) { @@ -177,12 +179,12 @@ func (c *Controller) clusterDelete(obj interface{}) { cluster := cluster.New(c.makeClusterConfig(), pg) err := cluster.Delete() 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 } close(c.stopChMap[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)) } diff --git a/pkg/spec/postgresql.go b/pkg/spec/postgresql.go index 42e0b9bd4..a63b03b46 100644 --- a/pkg/spec/postgresql.go +++ b/pkg/spec/postgresql.go @@ -3,9 +3,9 @@ package spec import ( "encoding/json" - "k8s.io/client-go/pkg/api" "k8s.io/client-go/pkg/api/meta" "k8s.io/client-go/pkg/api/unversioned" + "k8s.io/client-go/pkg/api/v1" ) type MaintenanceWindow struct { @@ -55,7 +55,7 @@ type PostgresSpec struct { NumberOfInstances int32 `json:"numberOfInstances"` Users map[string]UserFlags `json:"users"` MaintenanceWindows []string `json:"maintenanceWindows,omitempty"` - PamUsersSecret string `json:"pamUsersSecret",omitempty` + PamUsersSecret string `json:"pamUsersSecret,omitempty"` EtcdHost string DockerImage string @@ -81,7 +81,7 @@ type PostgresStatus struct { // PostgreSQL Third Party (resource) Object type Postgresql struct { unversioned.TypeMeta `json:",inline"` - Metadata api.ObjectMeta `json:"metadata"` + Metadata v1.ObjectMeta `json:"metadata"` Spec *PostgresSpec `json:"spec"` Status *PostgresStatus `json:"status"` diff --git a/pkg/util/constants/constants.go b/pkg/util/constants/constants.go index 47ea4394e..b2d58093e 100644 --- a/pkg/util/constants/constants.go +++ b/pkg/util/constants/constants.go @@ -15,9 +15,12 @@ const ( ResourceName = TPRName + "s" ResyncPeriod = 5 * time.Minute - EtcdHost = "etcd-client.default.svc.cluster.local:2379" //TODO: move to the operator spec - SpiloImage = "registry.opensource.zalan.do/acid/spilo-9.6:1.2-p11" - PamRoleName = "zalandos" + //TODO: move to the operator spec + EtcdHost = "etcd-client.default.svc.cluster.local:2379" + 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 + TeamsAPIUrl = "https://teams.example.com/api/" ) diff --git a/pkg/util/teams/teams.go b/pkg/util/teams/teams.go new file mode 100644 index 000000000..77e3d1acb --- /dev/null +++ b/pkg/util/teams/teams.go @@ -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 +} diff --git a/pkg/util/util.go b/pkg/util/util.go index 4681b15b9..6410d4ad7 100644 --- a/pkg/util/util.go +++ b/pkg/util/util.go @@ -1,8 +1,11 @@ package util import ( + "fmt" "math/rand" "time" + + "k8s.io/client-go/pkg/api/v1" ) var passwordChars = []byte("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789") @@ -19,3 +22,16 @@ func RandomPassword(n int) string { 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) +}