mirror of
https://github.com/zalando/postgres-operator.git
synced 2026-10-09 22:30:14 +02:00
Merge branch 'master' into feature/infrastructure-roles-extension
This commit is contained in:
@@ -971,6 +971,9 @@ var OperatorConfigCRDResourceValidation = apiextv1beta1.CustomResourceValidation
|
||||
"pod_environment_configmap": {
|
||||
Type: "string",
|
||||
},
|
||||
"pod_environment_secret": {
|
||||
Type: "string",
|
||||
},
|
||||
"pod_management_policy": {
|
||||
Type: "string",
|
||||
Enum: []apiextv1beta1.JSON{
|
||||
|
||||
@@ -112,8 +112,9 @@ func (p *Postgresql) UnmarshalJSON(data []byte) error {
|
||||
|
||||
if clusterName, err := extractClusterName(tmp2.ObjectMeta.Name, tmp2.Spec.TeamID); err != nil {
|
||||
tmp2.Error = err.Error()
|
||||
tmp2.Status.PostgresClusterStatus = ClusterStatusInvalid
|
||||
} else if err := validateCloneClusterDescription(&tmp2.Spec.Clone); err != nil {
|
||||
tmp2.Status = PostgresStatus{PostgresClusterStatus: ClusterStatusInvalid}
|
||||
} else if err := validateCloneClusterDescription(tmp2.Spec.Clone); err != nil {
|
||||
|
||||
tmp2.Error = err.Error()
|
||||
tmp2.Status.PostgresClusterStatus = ClusterStatusInvalid
|
||||
} else {
|
||||
|
||||
@@ -71,6 +71,7 @@ type KubernetesMetaConfiguration struct {
|
||||
// TODO: use a proper toleration structure?
|
||||
PodToleration map[string]string `json:"toleration,omitempty"`
|
||||
PodEnvironmentConfigMap spec.NamespacedName `json:"pod_environment_configmap,omitempty"`
|
||||
PodEnvironmentSecret string `json:"pod_environment_secret,omitempty"`
|
||||
PodPriorityClassName string `json:"pod_priority_class_name,omitempty"`
|
||||
MasterPodMoveTimeout Duration `json:"master_pod_move_timeout,omitempty"`
|
||||
EnablePodAntiAffinity bool `json:"enable_pod_antiaffinity,omitempty"`
|
||||
|
||||
@@ -53,7 +53,7 @@ type PostgresSpec struct {
|
||||
NumberOfInstances int32 `json:"numberOfInstances"`
|
||||
Users map[string]UserFlags `json:"users"`
|
||||
MaintenanceWindows []MaintenanceWindow `json:"maintenanceWindows,omitempty"`
|
||||
Clone CloneDescription `json:"clone"`
|
||||
Clone *CloneDescription `json:"clone"`
|
||||
ClusterName string `json:"-"`
|
||||
Databases map[string]string `json:"databases,omitempty"`
|
||||
PreparedDatabases map[string]PreparedDatabase `json:"preparedDatabases,omitempty"`
|
||||
|
||||
@@ -72,7 +72,7 @@ func extractClusterName(clusterName string, teamName string) (string, error) {
|
||||
|
||||
func validateCloneClusterDescription(clone *CloneDescription) error {
|
||||
// when cloning from the basebackup (no end timestamp) check that the cluster name is a valid service name
|
||||
if clone.ClusterName != "" && clone.EndTimestamp == "" {
|
||||
if clone != nil && clone.ClusterName != "" && clone.EndTimestamp == "" {
|
||||
if !serviceNameRegex.MatchString(clone.ClusterName) {
|
||||
return fmt.Errorf("clone cluster name must confirm to DNS-1035, regex used for validation is %q",
|
||||
serviceNameRegexString)
|
||||
|
||||
@@ -163,7 +163,7 @@ var unmarshalCluster = []struct {
|
||||
"kind": "Postgresql","apiVersion": "acid.zalan.do/v1",
|
||||
"metadata": {"name": "acid-testcluster1"}, "spec": {"teamId": 100}}`), &tmp).Error(),
|
||||
},
|
||||
marshal: []byte(`{"kind":"Postgresql","apiVersion":"acid.zalan.do/v1","metadata":{"name":"acid-testcluster1","creationTimestamp":null},"spec":{"postgresql":{"version":"","parameters":null},"volume":{"size":"","storageClass":""},"patroni":{"initdb":null,"pg_hba":null,"ttl":0,"loop_wait":0,"retry_timeout":0,"maximum_lag_on_failover":0,"slots":null},"resources":{"requests":{"cpu":"","memory":""},"limits":{"cpu":"","memory":""}},"teamId":"","allowedSourceRanges":null,"numberOfInstances":0,"users":null,"clone":{}},"status":"Invalid"}`),
|
||||
marshal: []byte(`{"kind":"Postgresql","apiVersion":"acid.zalan.do/v1","metadata":{"name":"acid-testcluster1","creationTimestamp":null},"spec":{"postgresql":{"version":"","parameters":null},"volume":{"size":"","storageClass":""},"patroni":{"initdb":null,"pg_hba":null,"ttl":0,"loop_wait":0,"retry_timeout":0,"maximum_lag_on_failover":0,"slots":null},"resources":{"requests":{"cpu":"","memory":""},"limits":{"cpu":"","memory":""}},"teamId":"","allowedSourceRanges":null,"numberOfInstances":0,"users":null,"clone":null},"status":"Invalid"}`),
|
||||
err: nil},
|
||||
{
|
||||
about: "example with /status subresource",
|
||||
@@ -184,7 +184,7 @@ var unmarshalCluster = []struct {
|
||||
"kind": "Postgresql","apiVersion": "acid.zalan.do/v1",
|
||||
"metadata": {"name": "acid-testcluster1"}, "spec": {"teamId": 100}}`), &tmp).Error(),
|
||||
},
|
||||
marshal: []byte(`{"kind":"Postgresql","apiVersion":"acid.zalan.do/v1","metadata":{"name":"acid-testcluster1","creationTimestamp":null},"spec":{"postgresql":{"version":"","parameters":null},"volume":{"size":"","storageClass":""},"patroni":{"initdb":null,"pg_hba":null,"ttl":0,"loop_wait":0,"retry_timeout":0,"maximum_lag_on_failover":0,"slots":null},"resources":{"requests":{"cpu":"","memory":""},"limits":{"cpu":"","memory":""}},"teamId":"","allowedSourceRanges":null,"numberOfInstances":0,"users":null,"clone":{}},"status":{"PostgresClusterStatus":"Invalid"}}`),
|
||||
marshal: []byte(`{"kind":"Postgresql","apiVersion":"acid.zalan.do/v1","metadata":{"name":"acid-testcluster1","creationTimestamp":null},"spec":{"postgresql":{"version":"","parameters":null},"volume":{"size":"","storageClass":""},"patroni":{"initdb":null,"pg_hba":null,"ttl":0,"loop_wait":0,"retry_timeout":0,"maximum_lag_on_failover":0,"slots":null},"resources":{"requests":{"cpu":"","memory":""},"limits":{"cpu":"","memory":""}},"teamId":"","allowedSourceRanges":null,"numberOfInstances":0,"users":null,"clone":null},"status":{"PostgresClusterStatus":"Invalid"}}`),
|
||||
err: nil},
|
||||
{
|
||||
about: "example with detailed input manifest and deprecated pod_priority_class_name -> podPriorityClassName",
|
||||
@@ -327,7 +327,7 @@ var unmarshalCluster = []struct {
|
||||
EndTime: mustParseTime("05:15"),
|
||||
},
|
||||
},
|
||||
Clone: CloneDescription{
|
||||
Clone: &CloneDescription{
|
||||
ClusterName: "acid-batman",
|
||||
},
|
||||
ClusterName: "testcluster1",
|
||||
@@ -351,7 +351,7 @@ var unmarshalCluster = []struct {
|
||||
Status: PostgresStatus{PostgresClusterStatus: ClusterStatusInvalid},
|
||||
Error: errors.New("name must match {TEAM}-{NAME} format").Error(),
|
||||
},
|
||||
marshal: []byte(`{"kind":"Postgresql","apiVersion":"acid.zalan.do/v1","metadata":{"name":"teapot-testcluster1","creationTimestamp":null},"spec":{"postgresql":{"version":"","parameters":null},"volume":{"size":"","storageClass":""},"patroni":{"initdb":null,"pg_hba":null,"ttl":0,"loop_wait":0,"retry_timeout":0,"maximum_lag_on_failover":0,"slots":null} ,"resources":{"requests":{"cpu":"","memory":""},"limits":{"cpu":"","memory":""}},"teamId":"acid","allowedSourceRanges":null,"numberOfInstances":0,"users":null,"clone":{}},"status":{"PostgresClusterStatus":"Invalid"}}`),
|
||||
marshal: []byte(`{"kind":"Postgresql","apiVersion":"acid.zalan.do/v1","metadata":{"name":"teapot-testcluster1","creationTimestamp":null},"spec":{"postgresql":{"version":"","parameters":null},"volume":{"size":"","storageClass":""},"patroni":{"initdb":null,"pg_hba":null,"ttl":0,"loop_wait":0,"retry_timeout":0,"maximum_lag_on_failover":0,"slots":null} ,"resources":{"requests":{"cpu":"","memory":""},"limits":{"cpu":"","memory":""}},"teamId":"acid","allowedSourceRanges":null,"numberOfInstances":0,"users":null,"clone":null},"status":{"PostgresClusterStatus":"Invalid"}}`),
|
||||
err: nil},
|
||||
{
|
||||
about: "example with clone",
|
||||
@@ -366,7 +366,7 @@ var unmarshalCluster = []struct {
|
||||
},
|
||||
Spec: PostgresSpec{
|
||||
TeamID: "acid",
|
||||
Clone: CloneDescription{
|
||||
Clone: &CloneDescription{
|
||||
ClusterName: "team-batman",
|
||||
},
|
||||
ClusterName: "testcluster1",
|
||||
@@ -405,7 +405,7 @@ var unmarshalCluster = []struct {
|
||||
err: errors.New("unexpected end of JSON input")},
|
||||
{
|
||||
about: "expect error on JSON with field's value malformatted",
|
||||
in: []byte(`{"kind":"Postgresql","apiVersion":"acid.zalan.do/v1","metadata":{"name":"acid-testcluster","creationTimestamp":qaz},"spec":{"postgresql":{"version":"","parameters":null},"volume":{"size":"","storageClass":""},"patroni":{"initdb":null,"pg_hba":null,"ttl":0,"loop_wait":0,"retry_timeout":0,"maximum_lag_on_failover":0,"slots":null},"resources":{"requests":{"cpu":"","memory":""},"limits":{"cpu":"","memory":""}},"teamId":"acid","allowedSourceRanges":null,"numberOfInstances":0,"users":null,"clone":{}},"status":{"PostgresClusterStatus":"Invalid"}}`),
|
||||
in: []byte(`{"kind":"Postgresql","apiVersion":"acid.zalan.do/v1","metadata":{"name":"acid-testcluster","creationTimestamp":qaz},"spec":{"postgresql":{"version":"","parameters":null},"volume":{"size":"","storageClass":""},"patroni":{"initdb":null,"pg_hba":null,"ttl":0,"loop_wait":0,"retry_timeout":0,"maximum_lag_on_failover":0,"slots":null},"resources":{"requests":{"cpu":"","memory":""},"limits":{"cpu":"","memory":""}},"teamId":"acid","allowedSourceRanges":null,"numberOfInstances":0,"users":null,"clone":null},"status":{"PostgresClusterStatus":"Invalid"}}`),
|
||||
out: Postgresql{},
|
||||
marshal: []byte{},
|
||||
err: errors.New("invalid character 'q' looking for beginning of value"),
|
||||
|
||||
@@ -579,7 +579,11 @@ func (in *PostgresSpec) DeepCopyInto(out *PostgresSpec) {
|
||||
(*in)[i].DeepCopyInto(&(*out)[i])
|
||||
}
|
||||
}
|
||||
in.Clone.DeepCopyInto(&out.Clone)
|
||||
if in.Clone != nil {
|
||||
in, out := &in.Clone, &out.Clone
|
||||
*out = new(CloneDescription)
|
||||
(*in).DeepCopyInto(*out)
|
||||
}
|
||||
if in.Databases != nil {
|
||||
in, out := &in.Databases, &out.Databases
|
||||
*out = make(map[string]string, len(*in))
|
||||
|
||||
@@ -124,6 +124,10 @@ func New(cfg Config, kubeClient k8sutil.KubernetesClient, pgSpec acidv1.Postgres
|
||||
|
||||
return fmt.Sprintf("%s-%s", e.PodName, e.ResourceVersion), nil
|
||||
})
|
||||
password_encryption, ok := pgSpec.Spec.PostgresqlParam.Parameters["password_encryption"]
|
||||
if !ok {
|
||||
password_encryption = "md5"
|
||||
}
|
||||
|
||||
cluster := &Cluster{
|
||||
Config: cfg,
|
||||
@@ -135,7 +139,7 @@ func New(cfg Config, kubeClient k8sutil.KubernetesClient, pgSpec acidv1.Postgres
|
||||
Secrets: make(map[types.UID]*v1.Secret),
|
||||
Services: make(map[PostgresRole]*v1.Service),
|
||||
Endpoints: make(map[PostgresRole]*v1.Endpoints)},
|
||||
userSyncStrategy: users.DefaultUserSyncStrategy{},
|
||||
userSyncStrategy: users.DefaultUserSyncStrategy{password_encryption},
|
||||
deleteOptions: metav1.DeleteOptions{PropagationPolicy: &deletePropagationPolicy},
|
||||
podEventsQueue: podEventsQueue,
|
||||
KubeClient: kubeClient,
|
||||
@@ -797,10 +801,8 @@ func (c *Cluster) Delete() {
|
||||
c.logger.Warningf("could not delete statefulset: %v", err)
|
||||
}
|
||||
|
||||
for _, obj := range c.Secrets {
|
||||
if err := c.deleteSecret(obj); err != nil {
|
||||
c.logger.Warningf("could not delete secret: %v", err)
|
||||
}
|
||||
if err := c.deleteSecrets(); err != nil {
|
||||
c.logger.Warningf("could not delete secrets: %v", err)
|
||||
}
|
||||
|
||||
if err := c.deletePodDisruptionBudget(); err != nil {
|
||||
|
||||
+112
-51
@@ -7,6 +7,7 @@ import (
|
||||
"path"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
|
||||
@@ -20,7 +21,6 @@ import (
|
||||
|
||||
acidv1 "github.com/zalando/postgres-operator/pkg/apis/acid.zalan.do/v1"
|
||||
"github.com/zalando/postgres-operator/pkg/spec"
|
||||
pkgspec "github.com/zalando/postgres-operator/pkg/spec"
|
||||
"github.com/zalando/postgres-operator/pkg/util"
|
||||
"github.com/zalando/postgres-operator/pkg/util/config"
|
||||
"github.com/zalando/postgres-operator/pkg/util/constants"
|
||||
@@ -715,6 +715,30 @@ func (c *Cluster) generateSpiloPodEnvVars(uid types.UID, spiloConfiguration stri
|
||||
envVars = append(envVars, v1.EnvVar{Name: "SPILO_CONFIGURATION", Value: spiloConfiguration})
|
||||
}
|
||||
|
||||
if c.patroniUsesKubernetes() {
|
||||
envVars = append(envVars, v1.EnvVar{Name: "DCS_ENABLE_KUBERNETES_API", Value: "true"})
|
||||
} else {
|
||||
envVars = append(envVars, v1.EnvVar{Name: "ETCD_HOST", Value: c.OpConfig.EtcdHost})
|
||||
}
|
||||
|
||||
if c.patroniKubernetesUseConfigMaps() {
|
||||
envVars = append(envVars, v1.EnvVar{Name: "KUBERNETES_USE_CONFIGMAPS", Value: "true"})
|
||||
}
|
||||
|
||||
if cloneDescription != nil && cloneDescription.ClusterName != "" {
|
||||
envVars = append(envVars, c.generateCloneEnvironment(cloneDescription)...)
|
||||
}
|
||||
|
||||
if c.Spec.StandbyCluster != nil {
|
||||
envVars = append(envVars, c.generateStandbyEnvironment(standbyDescription)...)
|
||||
}
|
||||
|
||||
// add vars taken from pod_environment_configmap and pod_environment_secret first
|
||||
// (to allow them to override the globals set in the operator config)
|
||||
if len(customPodEnvVarsList) > 0 {
|
||||
envVars = append(envVars, customPodEnvVarsList...)
|
||||
}
|
||||
|
||||
if c.OpConfig.WALES3Bucket != "" {
|
||||
envVars = append(envVars, v1.EnvVar{Name: "WAL_S3_BUCKET", Value: c.OpConfig.WALES3Bucket})
|
||||
envVars = append(envVars, v1.EnvVar{Name: "WAL_BUCKET_SCOPE_SUFFIX", Value: getBucketScopeSuffix(string(uid))})
|
||||
@@ -737,28 +761,6 @@ func (c *Cluster) generateSpiloPodEnvVars(uid types.UID, spiloConfiguration stri
|
||||
envVars = append(envVars, v1.EnvVar{Name: "LOG_BUCKET_SCOPE_PREFIX", Value: ""})
|
||||
}
|
||||
|
||||
if c.patroniUsesKubernetes() {
|
||||
envVars = append(envVars, v1.EnvVar{Name: "DCS_ENABLE_KUBERNETES_API", Value: "true"})
|
||||
} else {
|
||||
envVars = append(envVars, v1.EnvVar{Name: "ETCD_HOST", Value: c.OpConfig.EtcdHost})
|
||||
}
|
||||
|
||||
if c.patroniKubernetesUseConfigMaps() {
|
||||
envVars = append(envVars, v1.EnvVar{Name: "KUBERNETES_USE_CONFIGMAPS", Value: "true"})
|
||||
}
|
||||
|
||||
if cloneDescription.ClusterName != "" {
|
||||
envVars = append(envVars, c.generateCloneEnvironment(cloneDescription)...)
|
||||
}
|
||||
|
||||
if c.Spec.StandbyCluster != nil {
|
||||
envVars = append(envVars, c.generateStandbyEnvironment(standbyDescription)...)
|
||||
}
|
||||
|
||||
if len(customPodEnvVarsList) > 0 {
|
||||
envVars = append(envVars, customPodEnvVarsList...)
|
||||
}
|
||||
|
||||
return envVars
|
||||
}
|
||||
|
||||
@@ -777,13 +779,81 @@ func deduplicateEnvVars(input []v1.EnvVar, containerName string, logger *logrus.
|
||||
result = append(result, input[i])
|
||||
} else if names[va.Name] == 1 {
|
||||
names[va.Name]++
|
||||
logger.Warningf("variable %q is defined in %q more than once, the subsequent definitions are ignored",
|
||||
va.Name, containerName)
|
||||
|
||||
// Some variables (those to configure the WAL_ and LOG_ shipping) may be overwritten, only log as info
|
||||
if strings.HasPrefix(va.Name, "WAL_") || strings.HasPrefix(va.Name, "LOG_") {
|
||||
logger.Infof("global variable %q has been overwritten by configmap/secret for container %q",
|
||||
va.Name, containerName)
|
||||
} else {
|
||||
logger.Warningf("variable %q is defined in %q more than once, the subsequent definitions are ignored",
|
||||
va.Name, containerName)
|
||||
}
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// Return list of variables the pod recieved from the configured ConfigMap
|
||||
func (c *Cluster) getPodEnvironmentConfigMapVariables() ([]v1.EnvVar, error) {
|
||||
configMapPodEnvVarsList := make([]v1.EnvVar, 0)
|
||||
|
||||
if c.OpConfig.PodEnvironmentConfigMap.Name == "" {
|
||||
return configMapPodEnvVarsList, nil
|
||||
}
|
||||
|
||||
cm, err := c.KubeClient.ConfigMaps(c.OpConfig.PodEnvironmentConfigMap.Namespace).Get(
|
||||
context.TODO(),
|
||||
c.OpConfig.PodEnvironmentConfigMap.Name,
|
||||
metav1.GetOptions{})
|
||||
if err != nil {
|
||||
// if not found, try again using the cluster's namespace if it's different (old behavior)
|
||||
if k8sutil.ResourceNotFound(err) && c.Namespace != c.OpConfig.PodEnvironmentConfigMap.Namespace {
|
||||
cm, err = c.KubeClient.ConfigMaps(c.Namespace).Get(
|
||||
context.TODO(),
|
||||
c.OpConfig.PodEnvironmentConfigMap.Name,
|
||||
metav1.GetOptions{})
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not read PodEnvironmentConfigMap: %v", err)
|
||||
}
|
||||
}
|
||||
for k, v := range cm.Data {
|
||||
configMapPodEnvVarsList = append(configMapPodEnvVarsList, v1.EnvVar{Name: k, Value: v})
|
||||
}
|
||||
return configMapPodEnvVarsList, nil
|
||||
}
|
||||
|
||||
// Return list of variables the pod recieved from the configured Secret
|
||||
func (c *Cluster) getPodEnvironmentSecretVariables() ([]v1.EnvVar, error) {
|
||||
secretPodEnvVarsList := make([]v1.EnvVar, 0)
|
||||
|
||||
if c.OpConfig.PodEnvironmentSecret == "" {
|
||||
return secretPodEnvVarsList, nil
|
||||
}
|
||||
|
||||
secret, err := c.KubeClient.Secrets(c.OpConfig.PodEnvironmentSecret).Get(
|
||||
context.TODO(),
|
||||
c.OpConfig.PodEnvironmentSecret,
|
||||
metav1.GetOptions{})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not read Secret PodEnvironmentSecretName: %v", err)
|
||||
}
|
||||
|
||||
for k := range secret.Data {
|
||||
secretPodEnvVarsList = append(secretPodEnvVarsList,
|
||||
v1.EnvVar{Name: k, ValueFrom: &v1.EnvVarSource{
|
||||
SecretKeyRef: &v1.SecretKeySelector{
|
||||
LocalObjectReference: v1.LocalObjectReference{
|
||||
Name: c.OpConfig.PodEnvironmentSecret,
|
||||
},
|
||||
Key: k,
|
||||
},
|
||||
}})
|
||||
}
|
||||
|
||||
return secretPodEnvVarsList, nil
|
||||
}
|
||||
|
||||
func getSidecarContainer(sidecar acidv1.Sidecar, index int, resources *v1.ResourceRequirements) *v1.Container {
|
||||
name := sidecar.Name
|
||||
if name == "" {
|
||||
@@ -943,32 +1013,23 @@ func (c *Cluster) generateStatefulSet(spec *acidv1.PostgresSpec) (*appsv1.Statef
|
||||
initContainers = spec.InitContainers
|
||||
}
|
||||
|
||||
customPodEnvVarsList := make([]v1.EnvVar, 0)
|
||||
|
||||
if c.OpConfig.PodEnvironmentConfigMap != (pkgspec.NamespacedName{}) {
|
||||
var cm *v1.ConfigMap
|
||||
cm, err = c.KubeClient.ConfigMaps(c.OpConfig.PodEnvironmentConfigMap.Namespace).Get(
|
||||
context.TODO(),
|
||||
c.OpConfig.PodEnvironmentConfigMap.Name,
|
||||
metav1.GetOptions{})
|
||||
if err != nil {
|
||||
// if not found, try again using the cluster's namespace if it's different (old behavior)
|
||||
if k8sutil.ResourceNotFound(err) && c.Namespace != c.OpConfig.PodEnvironmentConfigMap.Namespace {
|
||||
cm, err = c.KubeClient.ConfigMaps(c.Namespace).Get(
|
||||
context.TODO(),
|
||||
c.OpConfig.PodEnvironmentConfigMap.Name,
|
||||
metav1.GetOptions{})
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not read PodEnvironmentConfigMap: %v", err)
|
||||
}
|
||||
}
|
||||
for k, v := range cm.Data {
|
||||
customPodEnvVarsList = append(customPodEnvVarsList, v1.EnvVar{Name: k, Value: v})
|
||||
}
|
||||
sort.Slice(customPodEnvVarsList,
|
||||
func(i, j int) bool { return customPodEnvVarsList[i].Name < customPodEnvVarsList[j].Name })
|
||||
// fetch env vars from custom ConfigMap
|
||||
configMapEnvVarsList, err := c.getPodEnvironmentConfigMapVariables()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// fetch env vars from custom ConfigMap
|
||||
secretEnvVarsList, err := c.getPodEnvironmentSecretVariables()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// concat all custom pod env vars and sort them
|
||||
customPodEnvVarsList := append(configMapEnvVarsList, secretEnvVarsList...)
|
||||
sort.Slice(customPodEnvVarsList,
|
||||
func(i, j int) bool { return customPodEnvVarsList[i].Name < customPodEnvVarsList[j].Name })
|
||||
|
||||
if spec.StandbyCluster != nil && spec.StandbyCluster.S3WalPath == "" {
|
||||
return nil, fmt.Errorf("s3_wal_path is empty for standby cluster")
|
||||
}
|
||||
@@ -1004,7 +1065,7 @@ func (c *Cluster) generateStatefulSet(spec *acidv1.PostgresSpec) (*appsv1.Statef
|
||||
spiloEnvVars := c.generateSpiloPodEnvVars(
|
||||
c.Postgresql.GetUID(),
|
||||
spiloConfiguration,
|
||||
&spec.Clone,
|
||||
spec.Clone,
|
||||
spec.StandbyCluster,
|
||||
customPodEnvVarsList,
|
||||
)
|
||||
|
||||
+223
-7
@@ -1,15 +1,18 @@
|
||||
package cluster
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"sort"
|
||||
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
acidv1 "github.com/zalando/postgres-operator/pkg/apis/acid.zalan.do/v1"
|
||||
"github.com/zalando/postgres-operator/pkg/spec"
|
||||
"github.com/zalando/postgres-operator/pkg/util"
|
||||
"github.com/zalando/postgres-operator/pkg/util/config"
|
||||
"github.com/zalando/postgres-operator/pkg/util/constants"
|
||||
@@ -22,6 +25,7 @@ import (
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"k8s.io/apimachinery/pkg/util/intstr"
|
||||
v1core "k8s.io/client-go/kubernetes/typed/core/v1"
|
||||
)
|
||||
|
||||
// For testing purposes
|
||||
@@ -116,17 +120,17 @@ func TestGenerateSpiloPodEnvVars(t *testing.T) {
|
||||
|
||||
expectedValuesGSBucket := []ExpectedValue{
|
||||
ExpectedValue{
|
||||
envIndex: 14,
|
||||
envIndex: 15,
|
||||
envVarConstant: "WAL_GS_BUCKET",
|
||||
envVarValue: "wale-gs-bucket",
|
||||
},
|
||||
ExpectedValue{
|
||||
envIndex: 15,
|
||||
envIndex: 16,
|
||||
envVarConstant: "WAL_BUCKET_SCOPE_SUFFIX",
|
||||
envVarValue: "/SomeUUID",
|
||||
},
|
||||
ExpectedValue{
|
||||
envIndex: 16,
|
||||
envIndex: 17,
|
||||
envVarConstant: "WAL_BUCKET_SCOPE_PREFIX",
|
||||
envVarValue: "",
|
||||
},
|
||||
@@ -134,22 +138,22 @@ func TestGenerateSpiloPodEnvVars(t *testing.T) {
|
||||
|
||||
expectedValuesGCPCreds := []ExpectedValue{
|
||||
ExpectedValue{
|
||||
envIndex: 14,
|
||||
envIndex: 15,
|
||||
envVarConstant: "WAL_GS_BUCKET",
|
||||
envVarValue: "wale-gs-bucket",
|
||||
},
|
||||
ExpectedValue{
|
||||
envIndex: 15,
|
||||
envIndex: 16,
|
||||
envVarConstant: "WAL_BUCKET_SCOPE_SUFFIX",
|
||||
envVarValue: "/SomeUUID",
|
||||
},
|
||||
ExpectedValue{
|
||||
envIndex: 16,
|
||||
envIndex: 17,
|
||||
envVarConstant: "WAL_BUCKET_SCOPE_PREFIX",
|
||||
envVarValue: "",
|
||||
},
|
||||
ExpectedValue{
|
||||
envIndex: 17,
|
||||
envIndex: 18,
|
||||
envVarConstant: "GOOGLE_APPLICATION_CREDENTIALS",
|
||||
envVarValue: "some_path_to_credentials",
|
||||
},
|
||||
@@ -713,6 +717,218 @@ func TestSecretVolume(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
const (
|
||||
testPodEnvironmentConfigMapName = "pod_env_cm"
|
||||
testPodEnvironmentSecretName = "pod_env_sc"
|
||||
)
|
||||
|
||||
type mockSecret struct {
|
||||
v1core.SecretInterface
|
||||
}
|
||||
|
||||
type mockConfigMap struct {
|
||||
v1core.ConfigMapInterface
|
||||
}
|
||||
|
||||
func (c *mockSecret) Get(ctx context.Context, name string, options metav1.GetOptions) (*v1.Secret, error) {
|
||||
if name != testPodEnvironmentSecretName {
|
||||
return nil, fmt.Errorf("Secret PodEnvironmentSecret not found")
|
||||
}
|
||||
secret := &v1.Secret{}
|
||||
secret.Name = testPodEnvironmentSecretName
|
||||
secret.Data = map[string][]byte{
|
||||
"minio_access_key": []byte("alpha"),
|
||||
"minio_secret_key": []byte("beta"),
|
||||
}
|
||||
return secret, nil
|
||||
}
|
||||
|
||||
func (c *mockConfigMap) Get(ctx context.Context, name string, options metav1.GetOptions) (*v1.ConfigMap, error) {
|
||||
if name != testPodEnvironmentConfigMapName {
|
||||
return nil, fmt.Errorf("NotFound")
|
||||
}
|
||||
configmap := &v1.ConfigMap{}
|
||||
configmap.Name = testPodEnvironmentConfigMapName
|
||||
configmap.Data = map[string]string{
|
||||
"foo1": "bar1",
|
||||
"foo2": "bar2",
|
||||
}
|
||||
return configmap, nil
|
||||
}
|
||||
|
||||
type MockSecretGetter struct {
|
||||
}
|
||||
|
||||
type MockConfigMapsGetter struct {
|
||||
}
|
||||
|
||||
func (c *MockSecretGetter) Secrets(namespace string) v1core.SecretInterface {
|
||||
return &mockSecret{}
|
||||
}
|
||||
|
||||
func (c *MockConfigMapsGetter) ConfigMaps(namespace string) v1core.ConfigMapInterface {
|
||||
return &mockConfigMap{}
|
||||
}
|
||||
|
||||
func newMockKubernetesClient() k8sutil.KubernetesClient {
|
||||
return k8sutil.KubernetesClient{
|
||||
SecretsGetter: &MockSecretGetter{},
|
||||
ConfigMapsGetter: &MockConfigMapsGetter{},
|
||||
}
|
||||
}
|
||||
func newMockCluster(opConfig config.Config) *Cluster {
|
||||
cluster := &Cluster{
|
||||
Config: Config{OpConfig: opConfig},
|
||||
KubeClient: newMockKubernetesClient(),
|
||||
}
|
||||
return cluster
|
||||
}
|
||||
|
||||
func TestPodEnvironmentConfigMapVariables(t *testing.T) {
|
||||
testName := "TestPodEnvironmentConfigMapVariables"
|
||||
tests := []struct {
|
||||
subTest string
|
||||
opConfig config.Config
|
||||
envVars []v1.EnvVar
|
||||
err error
|
||||
}{
|
||||
{
|
||||
subTest: "no PodEnvironmentConfigMap",
|
||||
envVars: []v1.EnvVar{},
|
||||
},
|
||||
{
|
||||
subTest: "missing PodEnvironmentConfigMap",
|
||||
opConfig: config.Config{
|
||||
Resources: config.Resources{
|
||||
PodEnvironmentConfigMap: spec.NamespacedName{
|
||||
Name: "idonotexist",
|
||||
},
|
||||
},
|
||||
},
|
||||
err: fmt.Errorf("could not read PodEnvironmentConfigMap: NotFound"),
|
||||
},
|
||||
{
|
||||
subTest: "simple PodEnvironmentConfigMap",
|
||||
opConfig: config.Config{
|
||||
Resources: config.Resources{
|
||||
PodEnvironmentConfigMap: spec.NamespacedName{
|
||||
Name: testPodEnvironmentConfigMapName,
|
||||
},
|
||||
},
|
||||
},
|
||||
envVars: []v1.EnvVar{
|
||||
{
|
||||
Name: "foo1",
|
||||
Value: "bar1",
|
||||
},
|
||||
{
|
||||
Name: "foo2",
|
||||
Value: "bar2",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
c := newMockCluster(tt.opConfig)
|
||||
vars, err := c.getPodEnvironmentConfigMapVariables()
|
||||
sort.Slice(vars, func(i, j int) bool { return vars[i].Name < vars[j].Name })
|
||||
if !reflect.DeepEqual(vars, tt.envVars) {
|
||||
t.Errorf("%s %s: expected `%v` but got `%v`",
|
||||
testName, tt.subTest, tt.envVars, vars)
|
||||
}
|
||||
if tt.err != nil {
|
||||
if err.Error() != tt.err.Error() {
|
||||
t.Errorf("%s %s: expected error `%v` but got `%v`",
|
||||
testName, tt.subTest, tt.err, err)
|
||||
}
|
||||
} else {
|
||||
if err != nil {
|
||||
t.Errorf("%s %s: expected no error but got error: `%v`",
|
||||
testName, tt.subTest, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Test if the keys of an existing secret are properly referenced
|
||||
func TestPodEnvironmentSecretVariables(t *testing.T) {
|
||||
testName := "TestPodEnvironmentSecretVariables"
|
||||
tests := []struct {
|
||||
subTest string
|
||||
opConfig config.Config
|
||||
envVars []v1.EnvVar
|
||||
err error
|
||||
}{
|
||||
{
|
||||
subTest: "No PodEnvironmentSecret configured",
|
||||
envVars: []v1.EnvVar{},
|
||||
},
|
||||
{
|
||||
subTest: "Secret referenced by PodEnvironmentSecret does not exist",
|
||||
opConfig: config.Config{
|
||||
Resources: config.Resources{
|
||||
PodEnvironmentSecret: "idonotexist",
|
||||
},
|
||||
},
|
||||
err: fmt.Errorf("could not read Secret PodEnvironmentSecretName: Secret PodEnvironmentSecret not found"),
|
||||
},
|
||||
{
|
||||
subTest: "Pod environment vars reference all keys from secret configured by PodEnvironmentSecret",
|
||||
opConfig: config.Config{
|
||||
Resources: config.Resources{
|
||||
PodEnvironmentSecret: testPodEnvironmentSecretName,
|
||||
},
|
||||
},
|
||||
envVars: []v1.EnvVar{
|
||||
{
|
||||
Name: "minio_access_key",
|
||||
ValueFrom: &v1.EnvVarSource{
|
||||
SecretKeyRef: &v1.SecretKeySelector{
|
||||
LocalObjectReference: v1.LocalObjectReference{
|
||||
Name: testPodEnvironmentSecretName,
|
||||
},
|
||||
Key: "minio_access_key",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "minio_secret_key",
|
||||
ValueFrom: &v1.EnvVarSource{
|
||||
SecretKeyRef: &v1.SecretKeySelector{
|
||||
LocalObjectReference: v1.LocalObjectReference{
|
||||
Name: testPodEnvironmentSecretName,
|
||||
},
|
||||
Key: "minio_secret_key",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
c := newMockCluster(tt.opConfig)
|
||||
vars, err := c.getPodEnvironmentSecretVariables()
|
||||
sort.Slice(vars, func(i, j int) bool { return vars[i].Name < vars[j].Name })
|
||||
if !reflect.DeepEqual(vars, tt.envVars) {
|
||||
t.Errorf("%s %s: expected `%v` but got `%v`",
|
||||
testName, tt.subTest, tt.envVars, vars)
|
||||
}
|
||||
if tt.err != nil {
|
||||
if err.Error() != tt.err.Error() {
|
||||
t.Errorf("%s %s: expected error `%v` but got `%v`",
|
||||
testName, tt.subTest, tt.err, err)
|
||||
}
|
||||
} else {
|
||||
if err != nil {
|
||||
t.Errorf("%s %s: expected no error but got error: `%v`",
|
||||
testName, tt.subTest, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func testResources(cluster *Cluster, podSpec *v1.PodTemplateSpec) error {
|
||||
cpuReq := podSpec.Spec.Containers[0].Resources.Requests["cpu"]
|
||||
if cpuReq.String() != cluster.OpConfig.ConnectionPooler.ConnectionPoolerDefaultCPURequest {
|
||||
|
||||
@@ -725,17 +725,26 @@ func (c *Cluster) deleteEndpoint(role PostgresRole) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Cluster) deleteSecret(secret *v1.Secret) error {
|
||||
c.setProcessName("deleting secret %q", util.NameFromMeta(secret.ObjectMeta))
|
||||
c.logger.Debugf("deleting secret %q", util.NameFromMeta(secret.ObjectMeta))
|
||||
err := c.KubeClient.Secrets(secret.Namespace).Delete(context.TODO(), secret.Name, c.deleteOptions)
|
||||
if err != nil {
|
||||
return err
|
||||
func (c *Cluster) deleteSecrets() error {
|
||||
c.setProcessName("deleting secrets")
|
||||
var errors []string
|
||||
errorCount := 0
|
||||
for uid, secret := range c.Secrets {
|
||||
c.logger.Debugf("deleting secret %q", util.NameFromMeta(secret.ObjectMeta))
|
||||
err := c.KubeClient.Secrets(secret.Namespace).Delete(context.TODO(), secret.Name, c.deleteOptions)
|
||||
if err != nil {
|
||||
errors = append(errors, fmt.Sprintf("could not delete secret %q: %v", util.NameFromMeta(secret.ObjectMeta), err))
|
||||
errorCount++
|
||||
}
|
||||
c.logger.Infof("secret %q has been deleted", util.NameFromMeta(secret.ObjectMeta))
|
||||
c.Secrets[uid] = nil
|
||||
}
|
||||
c.logger.Infof("secret %q has been deleted", util.NameFromMeta(secret.ObjectMeta))
|
||||
delete(c.Secrets, secret.UID)
|
||||
|
||||
return err
|
||||
if errorCount > 0 {
|
||||
return fmt.Errorf("could not delete all secrets: %v", errors)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Cluster) createRoles() (err error) {
|
||||
|
||||
+31
-28
@@ -63,23 +63,26 @@ func noEmptySync(cluster *Cluster, err error, reason SyncReason) error {
|
||||
|
||||
func TestConnectionPoolerSynchronization(t *testing.T) {
|
||||
testName := "Test connection pooler synchronization"
|
||||
var cluster = New(
|
||||
Config{
|
||||
OpConfig: config.Config{
|
||||
ProtectedRoles: []string{"admin"},
|
||||
Auth: config.Auth{
|
||||
SuperUsername: superUserName,
|
||||
ReplicationUsername: replicationUserName,
|
||||
newCluster := func() *Cluster {
|
||||
return New(
|
||||
Config{
|
||||
OpConfig: config.Config{
|
||||
ProtectedRoles: []string{"admin"},
|
||||
Auth: config.Auth{
|
||||
SuperUsername: superUserName,
|
||||
ReplicationUsername: replicationUserName,
|
||||
},
|
||||
ConnectionPooler: config.ConnectionPooler{
|
||||
ConnectionPoolerDefaultCPURequest: "100m",
|
||||
ConnectionPoolerDefaultCPULimit: "100m",
|
||||
ConnectionPoolerDefaultMemoryRequest: "100Mi",
|
||||
ConnectionPoolerDefaultMemoryLimit: "100Mi",
|
||||
NumberOfInstances: int32ToPointer(1),
|
||||
},
|
||||
},
|
||||
ConnectionPooler: config.ConnectionPooler{
|
||||
ConnectionPoolerDefaultCPURequest: "100m",
|
||||
ConnectionPoolerDefaultCPULimit: "100m",
|
||||
ConnectionPoolerDefaultMemoryRequest: "100Mi",
|
||||
ConnectionPoolerDefaultMemoryLimit: "100Mi",
|
||||
NumberOfInstances: int32ToPointer(1),
|
||||
},
|
||||
},
|
||||
}, k8sutil.KubernetesClient{}, acidv1.Postgresql{}, logger, eventRecorder)
|
||||
}, k8sutil.KubernetesClient{}, acidv1.Postgresql{}, logger, eventRecorder)
|
||||
}
|
||||
cluster := newCluster()
|
||||
|
||||
cluster.Statefulset = &appsv1.StatefulSet{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
@@ -87,20 +90,20 @@ func TestConnectionPoolerSynchronization(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
clusterMissingObjects := *cluster
|
||||
clusterMissingObjects := newCluster()
|
||||
clusterMissingObjects.KubeClient = k8sutil.ClientMissingObjects()
|
||||
|
||||
clusterMock := *cluster
|
||||
clusterMock := newCluster()
|
||||
clusterMock.KubeClient = k8sutil.NewMockKubernetesClient()
|
||||
|
||||
clusterDirtyMock := *cluster
|
||||
clusterDirtyMock := newCluster()
|
||||
clusterDirtyMock.KubeClient = k8sutil.NewMockKubernetesClient()
|
||||
clusterDirtyMock.ConnectionPooler = &ConnectionPoolerObjects{
|
||||
Deployment: &appsv1.Deployment{},
|
||||
Service: &v1.Service{},
|
||||
}
|
||||
|
||||
clusterNewDefaultsMock := *cluster
|
||||
clusterNewDefaultsMock := newCluster()
|
||||
clusterNewDefaultsMock.KubeClient = k8sutil.NewMockKubernetesClient()
|
||||
|
||||
tests := []struct {
|
||||
@@ -124,7 +127,7 @@ func TestConnectionPoolerSynchronization(t *testing.T) {
|
||||
ConnectionPooler: &acidv1.ConnectionPooler{},
|
||||
},
|
||||
},
|
||||
cluster: &clusterMissingObjects,
|
||||
cluster: clusterMissingObjects,
|
||||
defaultImage: "pooler:1.0",
|
||||
defaultInstances: 1,
|
||||
check: objectsAreSaved,
|
||||
@@ -139,7 +142,7 @@ func TestConnectionPoolerSynchronization(t *testing.T) {
|
||||
EnableConnectionPooler: boolToPointer(true),
|
||||
},
|
||||
},
|
||||
cluster: &clusterMissingObjects,
|
||||
cluster: clusterMissingObjects,
|
||||
defaultImage: "pooler:1.0",
|
||||
defaultInstances: 1,
|
||||
check: objectsAreSaved,
|
||||
@@ -154,7 +157,7 @@ func TestConnectionPoolerSynchronization(t *testing.T) {
|
||||
ConnectionPooler: &acidv1.ConnectionPooler{},
|
||||
},
|
||||
},
|
||||
cluster: &clusterMissingObjects,
|
||||
cluster: clusterMissingObjects,
|
||||
defaultImage: "pooler:1.0",
|
||||
defaultInstances: 1,
|
||||
check: objectsAreSaved,
|
||||
@@ -169,7 +172,7 @@ func TestConnectionPoolerSynchronization(t *testing.T) {
|
||||
newSpec: &acidv1.Postgresql{
|
||||
Spec: acidv1.PostgresSpec{},
|
||||
},
|
||||
cluster: &clusterMock,
|
||||
cluster: clusterMock,
|
||||
defaultImage: "pooler:1.0",
|
||||
defaultInstances: 1,
|
||||
check: objectsAreDeleted,
|
||||
@@ -182,7 +185,7 @@ func TestConnectionPoolerSynchronization(t *testing.T) {
|
||||
newSpec: &acidv1.Postgresql{
|
||||
Spec: acidv1.PostgresSpec{},
|
||||
},
|
||||
cluster: &clusterDirtyMock,
|
||||
cluster: clusterDirtyMock,
|
||||
defaultImage: "pooler:1.0",
|
||||
defaultInstances: 1,
|
||||
check: objectsAreDeleted,
|
||||
@@ -203,7 +206,7 @@ func TestConnectionPoolerSynchronization(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
cluster: &clusterMock,
|
||||
cluster: clusterMock,
|
||||
defaultImage: "pooler:1.0",
|
||||
defaultInstances: 1,
|
||||
check: deploymentUpdated,
|
||||
@@ -220,7 +223,7 @@ func TestConnectionPoolerSynchronization(t *testing.T) {
|
||||
ConnectionPooler: &acidv1.ConnectionPooler{},
|
||||
},
|
||||
},
|
||||
cluster: &clusterNewDefaultsMock,
|
||||
cluster: clusterNewDefaultsMock,
|
||||
defaultImage: "pooler:2.0",
|
||||
defaultInstances: 2,
|
||||
check: deploymentUpdated,
|
||||
@@ -239,7 +242,7 @@ func TestConnectionPoolerSynchronization(t *testing.T) {
|
||||
ConnectionPooler: &acidv1.ConnectionPooler{},
|
||||
},
|
||||
},
|
||||
cluster: &clusterMock,
|
||||
cluster: clusterMock,
|
||||
defaultImage: "pooler:1.0",
|
||||
defaultInstances: 1,
|
||||
check: noEmptySync,
|
||||
|
||||
@@ -58,6 +58,7 @@ func (c *Controller) importConfigurationFromCRD(fromCRD *acidv1.OperatorConfigur
|
||||
result.PodServiceAccountDefinition = fromCRD.Kubernetes.PodServiceAccountDefinition
|
||||
result.PodServiceAccountRoleBindingDefinition = fromCRD.Kubernetes.PodServiceAccountRoleBindingDefinition
|
||||
result.PodEnvironmentConfigMap = fromCRD.Kubernetes.PodEnvironmentConfigMap
|
||||
result.PodEnvironmentSecret = fromCRD.Kubernetes.PodEnvironmentSecret
|
||||
result.PodTerminateGracePeriod = util.CoalesceDuration(time.Duration(fromCRD.Kubernetes.PodTerminateGracePeriod), "5m")
|
||||
result.SpiloPrivileged = fromCRD.Kubernetes.SpiloPrivileged
|
||||
result.SpiloFSGroup = fromCRD.Kubernetes.SpiloFSGroup
|
||||
|
||||
@@ -45,6 +45,7 @@ type Resources struct {
|
||||
MinCPULimit string `name:"min_cpu_limit" default:"250m"`
|
||||
MinMemoryLimit string `name:"min_memory_limit" default:"250Mi"`
|
||||
PodEnvironmentConfigMap spec.NamespacedName `name:"pod_environment_configmap"`
|
||||
PodEnvironmentSecret string `name:"pod_environment_secret"`
|
||||
NodeReadinessLabel map[string]string `name:"node_readiness_label" default:""`
|
||||
MaxInstances int32 `name:"max_instances" default:"-1"`
|
||||
MinInstances int32 `name:"min_instances" default:"-1"`
|
||||
|
||||
@@ -28,6 +28,7 @@ const (
|
||||
// an existing roles of another role membership, nor it removes the already assigned flag
|
||||
// (except for the NOLOGIN). TODO: process other NOflags, i.e. NOSUPERUSER correctly.
|
||||
type DefaultUserSyncStrategy struct {
|
||||
PasswordEncryption string
|
||||
}
|
||||
|
||||
// ProduceSyncRequests figures out the types of changes that need to happen with the given users.
|
||||
@@ -45,7 +46,7 @@ func (strategy DefaultUserSyncStrategy) ProduceSyncRequests(dbUsers spec.PgUserM
|
||||
}
|
||||
} else {
|
||||
r := spec.PgSyncUserRequest{}
|
||||
newMD5Password := util.PGUserPassword(newUser)
|
||||
newMD5Password := util.NewEncryptor(strategy.PasswordEncryption).PGUserPassword(newUser)
|
||||
|
||||
if dbUser.Password != newMD5Password {
|
||||
r.User.Password = newMD5Password
|
||||
@@ -140,7 +141,7 @@ func (strategy DefaultUserSyncStrategy) createPgUser(user spec.PgUser, db *sql.D
|
||||
if user.Password == "" {
|
||||
userPassword = "PASSWORD NULL"
|
||||
} else {
|
||||
userPassword = fmt.Sprintf(passwordTemplate, util.PGUserPassword(user))
|
||||
userPassword = fmt.Sprintf(passwordTemplate, util.NewEncryptor(strategy.PasswordEncryption).PGUserPassword(user))
|
||||
}
|
||||
query := fmt.Sprintf(createUserSQL, user.Name, strings.Join(userFlags, " "), userPassword)
|
||||
|
||||
@@ -155,7 +156,7 @@ func (strategy DefaultUserSyncStrategy) alterPgUser(user spec.PgUser, db *sql.DB
|
||||
var resultStmt []string
|
||||
|
||||
if user.Password != "" || len(user.Flags) > 0 {
|
||||
alterStmt := produceAlterStmt(user)
|
||||
alterStmt := produceAlterStmt(user, strategy.PasswordEncryption)
|
||||
resultStmt = append(resultStmt, alterStmt)
|
||||
}
|
||||
if len(user.MemberOf) > 0 {
|
||||
@@ -174,14 +175,14 @@ func (strategy DefaultUserSyncStrategy) alterPgUser(user spec.PgUser, db *sql.DB
|
||||
return nil
|
||||
}
|
||||
|
||||
func produceAlterStmt(user spec.PgUser) string {
|
||||
func produceAlterStmt(user spec.PgUser, encryption string) string {
|
||||
// ALTER ROLE ... LOGIN ENCRYPTED PASSWORD ..
|
||||
result := make([]string, 0)
|
||||
password := user.Password
|
||||
flags := user.Flags
|
||||
|
||||
if password != "" {
|
||||
result = append(result, fmt.Sprintf(passwordTemplate, util.PGUserPassword(user)))
|
||||
result = append(result, fmt.Sprintf(passwordTemplate, util.NewEncryptor(encryption).PGUserPassword(user)))
|
||||
}
|
||||
if len(flags) != 0 {
|
||||
result = append(result, strings.Join(flags, " "))
|
||||
|
||||
+57
-4
@@ -1,8 +1,11 @@
|
||||
package util
|
||||
|
||||
import (
|
||||
"crypto/hmac"
|
||||
"crypto/md5" // #nosec we need it to for PostgreSQL md5 passwords
|
||||
cryptoRand "crypto/rand"
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"math/big"
|
||||
@@ -16,10 +19,14 @@ import (
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
|
||||
"github.com/zalando/postgres-operator/pkg/spec"
|
||||
"golang.org/x/crypto/pbkdf2"
|
||||
)
|
||||
|
||||
const (
|
||||
md5prefix = "md5"
|
||||
md5prefix = "md5"
|
||||
scramsha256prefix = "SCRAM-SHA-256"
|
||||
saltlength = 16
|
||||
iterations = 4096
|
||||
)
|
||||
|
||||
var passwordChars = []byte("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789")
|
||||
@@ -61,16 +68,62 @@ func NameFromMeta(meta metav1.ObjectMeta) spec.NamespacedName {
|
||||
}
|
||||
}
|
||||
|
||||
// PGUserPassword is used to generate md5 password hash for a given user. It does nothing for already hashed passwords.
|
||||
func PGUserPassword(user spec.PgUser) string {
|
||||
if (len(user.Password) == md5.Size*2+len(md5prefix) && user.Password[:3] == md5prefix) || user.Password == "" {
|
||||
type Hasher func(user spec.PgUser) string
|
||||
type Random func(n int) string
|
||||
|
||||
type Encryptor struct {
|
||||
encrypt Hasher
|
||||
random Random
|
||||
}
|
||||
|
||||
func NewEncryptor(encryption string) *Encryptor {
|
||||
e := Encryptor{random: RandomPassword}
|
||||
m := map[string]Hasher{
|
||||
"md5": e.PGUserPasswordMD5,
|
||||
"scram-sha-256": e.PGUserPasswordScramSHA256,
|
||||
}
|
||||
hasher, ok := m[encryption]
|
||||
if !ok {
|
||||
hasher = e.PGUserPasswordMD5
|
||||
}
|
||||
e.encrypt = hasher
|
||||
return &e
|
||||
}
|
||||
|
||||
func (e *Encryptor) PGUserPassword(user spec.PgUser) string {
|
||||
if (len(user.Password) == md5.Size*2+len(md5prefix) && user.Password[:3] == md5prefix) ||
|
||||
(len(user.Password) > len(scramsha256prefix) && user.Password[:len(scramsha256prefix)] == scramsha256prefix) || user.Password == "" {
|
||||
// Avoid processing already encrypted or empty passwords
|
||||
return user.Password
|
||||
}
|
||||
return e.encrypt(user)
|
||||
}
|
||||
|
||||
func (e *Encryptor) PGUserPasswordMD5(user spec.PgUser) string {
|
||||
s := md5.Sum([]byte(user.Password + user.Name)) // #nosec, using md5 since PostgreSQL uses it for hashing passwords.
|
||||
return md5prefix + hex.EncodeToString(s[:])
|
||||
}
|
||||
|
||||
func (e *Encryptor) PGUserPasswordScramSHA256(user spec.PgUser) string {
|
||||
salt := []byte(e.random(saltlength))
|
||||
key := pbkdf2.Key([]byte(user.Password), salt, iterations, 32, sha256.New)
|
||||
mac := hmac.New(sha256.New, key)
|
||||
mac.Write([]byte("Server Key"))
|
||||
serverKey := mac.Sum(nil)
|
||||
mac = hmac.New(sha256.New, key)
|
||||
mac.Write([]byte("Client Key"))
|
||||
clientKey := mac.Sum(nil)
|
||||
storedKey := sha256.Sum256(clientKey)
|
||||
pass := fmt.Sprintf("%s$%v:%s$%s:%s",
|
||||
scramsha256prefix,
|
||||
iterations,
|
||||
base64.StdEncoding.EncodeToString(salt),
|
||||
base64.StdEncoding.EncodeToString(storedKey[:]),
|
||||
base64.StdEncoding.EncodeToString(serverKey),
|
||||
)
|
||||
return pass
|
||||
}
|
||||
|
||||
// Diff returns diffs between 2 objects
|
||||
func Diff(a, b interface{}) []string {
|
||||
return pretty.Diff(a, b)
|
||||
|
||||
+21
-7
@@ -12,20 +12,27 @@ import (
|
||||
)
|
||||
|
||||
var pgUsers = []struct {
|
||||
in spec.PgUser
|
||||
out string
|
||||
in spec.PgUser
|
||||
outmd5 string
|
||||
outscramsha256 string
|
||||
}{{spec.PgUser{
|
||||
Name: "test",
|
||||
Password: "password",
|
||||
Flags: []string{},
|
||||
MemberOf: []string{}},
|
||||
"md587f77988ccb5aa917c93201ba314fcd4"},
|
||||
"md587f77988ccb5aa917c93201ba314fcd4", "SCRAM-SHA-256$4096:c2FsdA==$lF4cRm/Jky763CN4HtxdHnjV4Q8AWTNlKvGmEFFU8IQ=:ub8OgRsftnk2ccDMOt7ffHXNcikRkQkq1lh4xaAqrSw="},
|
||||
{spec.PgUser{
|
||||
Name: "test",
|
||||
Password: "md592f413f3974bdf3799bb6fecb5f9f2c6",
|
||||
Flags: []string{},
|
||||
MemberOf: []string{}},
|
||||
"md592f413f3974bdf3799bb6fecb5f9f2c6"}}
|
||||
"md592f413f3974bdf3799bb6fecb5f9f2c6", "md592f413f3974bdf3799bb6fecb5f9f2c6"},
|
||||
{spec.PgUser{
|
||||
Name: "test",
|
||||
Password: "SCRAM-SHA-256$4096:S1ByZWhvYVV5VDlJNGZoVw==$ozLevu5k0pAQYRrSY+vZhetO6+/oB+qZvuutOdXR94U=:yADwhy0LGloXzh5RaVwLMFyUokwI17VkHVfKVuHu0Zs=",
|
||||
Flags: []string{},
|
||||
MemberOf: []string{}},
|
||||
"SCRAM-SHA-256$4096:S1ByZWhvYVV5VDlJNGZoVw==$ozLevu5k0pAQYRrSY+vZhetO6+/oB+qZvuutOdXR94U=:yADwhy0LGloXzh5RaVwLMFyUokwI17VkHVfKVuHu0Zs=", "SCRAM-SHA-256$4096:S1ByZWhvYVV5VDlJNGZoVw==$ozLevu5k0pAQYRrSY+vZhetO6+/oB+qZvuutOdXR94U=:yADwhy0LGloXzh5RaVwLMFyUokwI17VkHVfKVuHu0Zs="}}
|
||||
|
||||
var prettyDiffTest = []struct {
|
||||
inA interface{}
|
||||
@@ -107,9 +114,16 @@ func TestNameFromMeta(t *testing.T) {
|
||||
|
||||
func TestPGUserPassword(t *testing.T) {
|
||||
for _, tt := range pgUsers {
|
||||
pwd := PGUserPassword(tt.in)
|
||||
if pwd != tt.out {
|
||||
t.Errorf("PgUserPassword expected: %q, got: %q", tt.out, pwd)
|
||||
e := NewEncryptor("md5")
|
||||
pwd := e.PGUserPassword(tt.in)
|
||||
if pwd != tt.outmd5 {
|
||||
t.Errorf("PgUserPassword expected: %q, got: %q", tt.outmd5, pwd)
|
||||
}
|
||||
e = NewEncryptor("scram-sha-256")
|
||||
e.random = func(n int) string { return "salt" }
|
||||
pwd = e.PGUserPassword(tt.in)
|
||||
if pwd != tt.outscramsha256 {
|
||||
t.Errorf("PgUserPassword expected: %q, got: %q", tt.outscramsha256, pwd)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user