mirror of
https://github.com/zalando/postgres-operator.git
synced 2026-10-18 05:35:07 +02:00
Create human users from teams api
This commit is contained in:
+7
-3
@@ -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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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"]
|
||||||
|
|||||||
@@ -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:
|
||||||
+80
-43
@@ -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)
|
||||||
|
|
||||||
|
|||||||
+4
-2
@@ -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()
|
||||||
|
|||||||
+59
-45
@@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"`
|
||||||
|
|||||||
@@ -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/"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user