Configure pg_hba in the local postgresql configuration of Patroni. (#361)

Previously, the operator put pg_hba into the bootstrap/pg_hba key of
Patroni. That had 2 adverse effects:
 - pg_hba.conf was shadowed by Spilo default section in the local
   postgresql configuration
 - when updating pg_hba in the cluster manifest, the updated lines were
   not propagated to DCS, since the key was defined in the boostrap
   section of Patroni.

Include some minor refactoring, moving methods to unexported when
possible and commenting out usage of md5, so that gosec won't complain.

Per https://github.com/zalando-incubator/postgres-operator/issues/330

Review by @zerg-junior
This commit is contained in:
Oleksii Kliukin 2018-08-08 11:01:26 +02:00 committed by GitHub
parent 199aa6508c
commit e933908084
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 55 additions and 52 deletions

View File

@ -119,7 +119,7 @@ func New(cfg Config, kubeClient k8sutil.KubernetesClient, pgSpec spec.Postgresql
} }
cluster.logger = logger.WithField("pkg", "cluster").WithField("cluster-name", cluster.clusterName()) cluster.logger = logger.WithField("pkg", "cluster").WithField("cluster-name", cluster.clusterName())
cluster.teamsAPIClient = teams.NewTeamsAPI(cfg.OpConfig.TeamsAPIUrl, logger) cluster.teamsAPIClient = teams.NewTeamsAPI(cfg.OpConfig.TeamsAPIUrl, logger)
cluster.oauthTokenGetter = NewSecretOauthTokenGetter(&kubeClient, cfg.OpConfig.OAuthTokenSecretName) cluster.oauthTokenGetter = newSecretOauthTokenGetter(&kubeClient, cfg.OpConfig.OAuthTokenSecretName)
cluster.patroni = patroni.New(cluster.logger) cluster.patroni = patroni.New(cluster.logger)
return cluster return cluster
@ -404,15 +404,15 @@ func (c *Cluster) compareStatefulSetWith(statefulSet *v1beta1.StatefulSet) *comp
return &compareStatefulsetResult{match: match, reasons: reasons, rollingUpdate: needsRollUpdate, replace: needsReplace} return &compareStatefulsetResult{match: match, reasons: reasons, rollingUpdate: needsRollUpdate, replace: needsReplace}
} }
type ContainerCondition func(a, b v1.Container) bool type containerCondition func(a, b v1.Container) bool
type ContainerCheck struct { type containerCheck struct {
condition ContainerCondition condition containerCondition
reason string reason string
} }
func NewCheck(msg string, cond ContainerCondition) ContainerCheck { func newCheck(msg string, cond containerCondition) containerCheck {
return ContainerCheck{reason: msg, condition: cond} return containerCheck{reason: msg, condition: cond}
} }
// compareContainers: compare containers from two stateful sets // compareContainers: compare containers from two stateful sets
@ -422,18 +422,18 @@ func NewCheck(msg string, cond ContainerCondition) ContainerCheck {
func (c *Cluster) compareContainers(setA, setB *v1beta1.StatefulSet) (bool, []string) { func (c *Cluster) compareContainers(setA, setB *v1beta1.StatefulSet) (bool, []string) {
reasons := make([]string, 0) reasons := make([]string, 0)
needsRollUpdate := false needsRollUpdate := false
checks := []ContainerCheck{ checks := []containerCheck{
NewCheck("new statefulset's container %d name doesn't match the current one", newCheck("new statefulset's container %d name doesn't match the current one",
func(a, b v1.Container) bool { return a.Name != b.Name }), func(a, b v1.Container) bool { return a.Name != b.Name }),
NewCheck("new statefulset's container %d image doesn't match the current one", newCheck("new statefulset's container %d image doesn't match the current one",
func(a, b v1.Container) bool { return a.Image != b.Image }), func(a, b v1.Container) bool { return a.Image != b.Image }),
NewCheck("new statefulset's container %d ports don't match the current one", newCheck("new statefulset's container %d ports don't match the current one",
func(a, b v1.Container) bool { return !reflect.DeepEqual(a.Ports, b.Ports) }), func(a, b v1.Container) bool { return !reflect.DeepEqual(a.Ports, b.Ports) }),
NewCheck("new statefulset's container %d resources don't match the current ones", newCheck("new statefulset's container %d resources don't match the current ones",
func(a, b v1.Container) bool { return !compareResources(&a.Resources, &b.Resources) }), func(a, b v1.Container) bool { return !compareResources(&a.Resources, &b.Resources) }),
NewCheck("new statefulset's container %d environment doesn't match the current one", newCheck("new statefulset's container %d environment doesn't match the current one",
func(a, b v1.Container) bool { return !reflect.DeepEqual(a.Env, b.Env) }), func(a, b v1.Container) bool { return !reflect.DeepEqual(a.Env, b.Env) }),
NewCheck("new statefulset's container %d environment sources don't match the current one", newCheck("new statefulset's container %d environment sources don't match the current one",
func(a, b v1.Container) bool { return !reflect.DeepEqual(a.EnvFrom, b.EnvFrom) }), func(a, b v1.Container) bool { return !reflect.DeepEqual(a.EnvFrom, b.EnvFrom) }),
} }
@ -630,6 +630,7 @@ func (c *Cluster) Delete() {
} }
} }
//NeedsRepair returns true if the cluster should be included in the repair scan (based on its in-memory status).
func (c *Cluster) NeedsRepair() (bool, spec.PostgresStatus) { func (c *Cluster) NeedsRepair() (bool, spec.PostgresStatus) {
c.specMu.RLock() c.specMu.RLock()
defer c.specMu.RUnlock() defer c.specMu.RUnlock()
@ -905,9 +906,9 @@ func (c *Cluster) shouldDeleteSecret(secret *v1.Secret) (delete bool, userName s
type simpleActionWithResult func() error type simpleActionWithResult func() error
type ClusterObjectGet func(name string) (spec.NamespacedName, error) type clusterObjectGet func(name string) (spec.NamespacedName, error)
type ClusterObjectDelete func(name string) error type clusterObjectDelete func(name string) error
func (c *Cluster) deletePatroniClusterObjects() error { func (c *Cluster) deletePatroniClusterObjects() error {
// TODO: figure out how to remove leftover patroni objects in other cases // TODO: figure out how to remove leftover patroni objects in other cases
@ -924,8 +925,8 @@ func (c *Cluster) deletePatroniClusterObjects() error {
} }
func (c *Cluster) deleteClusterObject( func (c *Cluster) deleteClusterObject(
get ClusterObjectGet, get clusterObjectGet,
del ClusterObjectDelete, del clusterObjectDelete,
objType string) error { objType string) error {
for _, suffix := range patroniObjectSuffixes { for _, suffix := range patroniObjectSuffixes {
name := fmt.Sprintf("%s-%s", c.Name, suffix) name := fmt.Sprintf("%s-%s", c.Name, suffix)

View File

@ -25,6 +25,7 @@ const (
pgBinariesLocationTemplate = "/usr/lib/postgresql/%s/bin" pgBinariesLocationTemplate = "/usr/lib/postgresql/%s/bin"
patroniPGBinariesParameterName = "bin_dir" patroniPGBinariesParameterName = "bin_dir"
patroniPGParametersParameterName = "parameters" patroniPGParametersParameterName = "parameters"
patroniPGHBAConfParameterName = "pg_hba"
localHost = "127.0.0.1/32" localHost = "127.0.0.1/32"
) )
@ -44,7 +45,6 @@ type patroniDCS struct {
type pgBootstrap struct { type pgBootstrap struct {
Initdb []interface{} `json:"initdb"` Initdb []interface{} `json:"initdb"`
Users map[string]pgUser `json:"users"` Users map[string]pgUser `json:"users"`
PgHBA []string `json:"pg_hba"`
DCS patroniDCS `json:"dcs,omitempty"` DCS patroniDCS `json:"dcs,omitempty"`
} }
@ -202,19 +202,6 @@ PatroniInitDBParams:
config.Bootstrap.Initdb = append(config.Bootstrap.Initdb, map[string]string{k: v}) config.Bootstrap.Initdb = append(config.Bootstrap.Initdb, map[string]string{k: v})
} }
// pg_hba parameters in the manifest replace the default ones. We cannot
// reasonably merge them automatically, because pg_hba parsing stops on
// a first successfully matched rule.
if len(patroni.PgHba) > 0 {
config.Bootstrap.PgHBA = patroni.PgHba
} else {
config.Bootstrap.PgHBA = []string{
"hostnossl all all all reject",
fmt.Sprintf("hostssl all +%s all pam", pamRoleName),
"hostssl all all all md5",
}
}
if patroni.MaximumLagOnFailover >= 0 { if patroni.MaximumLagOnFailover >= 0 {
config.Bootstrap.DCS.MaximumLagOnFailover = patroni.MaximumLagOnFailover config.Bootstrap.DCS.MaximumLagOnFailover = patroni.MaximumLagOnFailover
} }
@ -231,23 +218,23 @@ PatroniInitDBParams:
config.PgLocalConfiguration = make(map[string]interface{}) config.PgLocalConfiguration = make(map[string]interface{})
config.PgLocalConfiguration[patroniPGBinariesParameterName] = fmt.Sprintf(pgBinariesLocationTemplate, pg.PgVersion) config.PgLocalConfiguration[patroniPGBinariesParameterName] = fmt.Sprintf(pgBinariesLocationTemplate, pg.PgVersion)
if len(pg.Parameters) > 0 { if len(pg.Parameters) > 0 {
localParameters := make(map[string]string) local, bootstrap := getLocalAndBoostrapPostgreSQLParameters(pg.Parameters)
bootstrapParameters := make(map[string]string)
for param, val := range pg.Parameters { if len(local) > 0 {
if isBootstrapOnlyParameter(param) { config.PgLocalConfiguration[patroniPGParametersParameterName] = local
bootstrapParameters[param] = val
} else {
localParameters[param] = val
} }
} if len(bootstrap) > 0 {
if len(localParameters) > 0 {
config.PgLocalConfiguration[patroniPGParametersParameterName] = localParameters
}
if len(bootstrapParameters) > 0 {
config.Bootstrap.DCS.PGBootstrapConfiguration = make(map[string]interface{}) config.Bootstrap.DCS.PGBootstrapConfiguration = make(map[string]interface{})
config.Bootstrap.DCS.PGBootstrapConfiguration[patroniPGParametersParameterName] = bootstrapParameters config.Bootstrap.DCS.PGBootstrapConfiguration[patroniPGParametersParameterName] = bootstrap
} }
} }
// Patroni gives us a choice of writing pg_hba.conf to either the bootstrap section or to the local postgresql one.
// We choose the local one, because we need Patroni to change pg_hba.conf in PostgreSQL after the user changes the
// relevant section in the manifest.
if len(patroni.PgHba) > 0 {
config.PgLocalConfiguration[patroniPGHBAConfParameterName] = patroni.PgHba
}
config.Bootstrap.Users = map[string]pgUser{ config.Bootstrap.Users = map[string]pgUser{
pamRoleName: { pamRoleName: {
Password: "", Password: "",
@ -262,6 +249,19 @@ PatroniInitDBParams:
return string(result) return string(result)
} }
func getLocalAndBoostrapPostgreSQLParameters(parameters map[string]string) (local, bootstrap map[string]string) {
local = make(map[string]string)
bootstrap = make(map[string]string)
for param, val := range parameters {
if isBootstrapOnlyParameter(param) {
bootstrap[param] = val
} else {
local[param] = val
}
}
return
}
func nodeAffinity(nodeReadinessLabel map[string]string) *v1.Affinity { func nodeAffinity(nodeReadinessLabel map[string]string) *v1.Affinity {
matchExpressions := make([]v1.NodeSelectorRequirement, 0) matchExpressions := make([]v1.NodeSelectorRequirement, 0)
if len(nodeReadinessLabel) == 0 { if len(nodeReadinessLabel) == 0 {
@ -736,7 +736,7 @@ func (c *Cluster) generateStatefulSet(spec *spec.PostgresSpec) (*v1beta1.Statefu
Name: c.statefulSetName(), Name: c.statefulSetName(),
Namespace: c.Namespace, Namespace: c.Namespace,
Labels: c.labelsSet(true), Labels: c.labelsSet(true),
Annotations: map[string]string{RollingUpdateStatefulsetAnnotationKey: "false"}, Annotations: map[string]string{rollingUpdateStatefulsetAnnotationKey: "false"},
}, },
Spec: v1beta1.StatefulSetSpec{ Spec: v1beta1.StatefulSetSpec{
Replicas: &numberOfInstances, Replicas: &numberOfInstances,

View File

@ -18,7 +18,7 @@ import (
) )
const ( const (
RollingUpdateStatefulsetAnnotationKey = "zalando-postgres-operator-rolling-update-required" rollingUpdateStatefulsetAnnotationKey = "zalando-postgres-operator-rolling-update-required"
) )
func (c *Cluster) listResources() error { func (c *Cluster) listResources() error {
@ -140,7 +140,7 @@ func (c *Cluster) setRollingUpdateFlagForStatefulSet(sset *v1beta1.StatefulSet,
if anno == nil { if anno == nil {
anno = make(map[string]string) anno = make(map[string]string)
} }
anno[RollingUpdateStatefulsetAnnotationKey] = strconv.FormatBool(val) anno[rollingUpdateStatefulsetAnnotationKey] = strconv.FormatBool(val)
sset.SetAnnotations(anno) sset.SetAnnotations(anno)
} }
@ -162,12 +162,12 @@ func (c *Cluster) getRollingUpdateFlagFromStatefulSet(sset *v1beta1.StatefulSet,
anno := sset.GetAnnotations() anno := sset.GetAnnotations()
flag = defaultValue flag = defaultValue
stringFlag, exists := anno[RollingUpdateStatefulsetAnnotationKey] stringFlag, exists := anno[rollingUpdateStatefulsetAnnotationKey]
if exists { if exists {
var err error var err error
if flag, err = strconv.ParseBool(stringFlag); err != nil { if flag, err = strconv.ParseBool(stringFlag); err != nil {
c.logger.Warnf("error when parsing %q annotation for the statefulset %q: expected boolean value, got %q\n", c.logger.Warnf("error when parsing %q annotation for the statefulset %q: expected boolean value, got %q\n",
RollingUpdateStatefulsetAnnotationKey, rollingUpdateStatefulsetAnnotationKey,
types.NamespacedName{Namespace: sset.Namespace, Name: sset.Name}, types.NamespacedName{Namespace: sset.Namespace, Name: sset.Name},
stringFlag) stringFlag)
flag = defaultValue flag = defaultValue

View File

@ -35,7 +35,7 @@ type SecretOauthTokenGetter struct {
OAuthTokenSecretName spec.NamespacedName OAuthTokenSecretName spec.NamespacedName
} }
func NewSecretOauthTokenGetter(kubeClient *k8sutil.KubernetesClient, func newSecretOauthTokenGetter(kubeClient *k8sutil.KubernetesClient,
OAuthTokenSecretName spec.NamespacedName) *SecretOauthTokenGetter { OAuthTokenSecretName spec.NamespacedName) *SecretOauthTokenGetter {
return &SecretOauthTokenGetter{kubeClient, OAuthTokenSecretName} return &SecretOauthTokenGetter{kubeClient, OAuthTokenSecretName}
} }

View File

@ -71,6 +71,7 @@ type Sidecar struct {
Env []v1.EnvVar `json:"env,omitempty"` Env []v1.EnvVar `json:"env,omitempty"`
} }
// UserFlags defines flags (such as superuser, nologin) that could be assigned to individual users
type UserFlags []string type UserFlags []string
// PostgresStatus contains status of the PostgreSQL cluster (running, creation failed etc.) // PostgresStatus contains status of the PostgreSQL cluster (running, creation failed etc.)

View File

@ -1,7 +1,7 @@
package util package util
import ( import (
"crypto/md5" "crypto/md5" // #nosec we need it to for PostgreSQL md5 passwords
"encoding/hex" "encoding/hex"
"math/rand" "math/rand"
"regexp" "regexp"
@ -48,7 +48,7 @@ func PGUserPassword(user spec.PgUser) string {
// Avoid processing already encrypted or empty passwords // Avoid processing already encrypted or empty passwords
return user.Password return user.Password
} }
s := md5.Sum([]byte(user.Password + user.Name)) s := md5.Sum([]byte(user.Password + user.Name)) // #nosec, using md5 since PostgreSQL uses it for hashing passwords.
return md5prefix + hex.EncodeToString(s[:]) return md5prefix + hex.EncodeToString(s[:])
} }
@ -120,6 +120,7 @@ func MapContains(haystack, needle map[string]string) bool {
return true return true
} }
// Coalesce returns the first argument if it is not null, otherwise the second one.
func Coalesce(val, defaultVal string) string { func Coalesce(val, defaultVal string) string {
if val == "" { if val == "" {
return defaultVal return defaultVal