mirror of
https://github.com/zalando/postgres-operator.git
synced 2026-10-06 08:31:55 +02:00
Merge branch 'master' into fix/set-password-encryption-default-to-scram-sha-256
This commit is contained in:
+93
-54
@@ -3,6 +3,7 @@ package cluster
|
||||
// Postgres CustomResourceDefinition object i.e. Spilo
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
@@ -32,6 +33,7 @@ import (
|
||||
v1 "k8s.io/api/core/v1"
|
||||
policyv1 "k8s.io/api/policy/v1"
|
||||
rbacv1 "k8s.io/api/rbac/v1"
|
||||
"k8s.io/apimachinery/pkg/api/equality"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"k8s.io/client-go/rest"
|
||||
@@ -70,7 +72,7 @@ type kubeResources struct {
|
||||
CriticalOpPodDisruptionBudget *policyv1.PodDisruptionBudget
|
||||
LogicalBackupJob *batchv1.CronJob
|
||||
Streams map[string]*zalandov1.FabricEventStream
|
||||
//Pods are treated separately
|
||||
// Pods are treated separately
|
||||
}
|
||||
|
||||
// Cluster describes postgresql cluster
|
||||
@@ -95,7 +97,7 @@ type Cluster struct {
|
||||
|
||||
teamsAPIClient teams.Interface
|
||||
oauthTokenGetter OAuthTokenGetter
|
||||
KubeClient k8sutil.KubernetesClient //TODO: move clients to the better place?
|
||||
KubeClient k8sutil.KubernetesClient // TODO: move clients to the better place?
|
||||
currentProcess Process
|
||||
processMu sync.RWMutex // protects the current operation for reporting, no need to hold the master mutex
|
||||
specMu sync.RWMutex // protects the spec for reporting, no need to hold the master mutex
|
||||
@@ -149,7 +151,8 @@ func New(cfg Config, kubeClient k8sutil.KubernetesClient, pgSpec acidv1.Postgres
|
||||
PatroniEndpoints: make(map[string]*v1.Endpoints),
|
||||
PatroniConfigMaps: make(map[string]*v1.ConfigMap),
|
||||
VolumeClaims: make(map[types.UID]*v1.PersistentVolumeClaim),
|
||||
Streams: make(map[string]*zalandov1.FabricEventStream)},
|
||||
Streams: make(map[string]*zalandov1.FabricEventStream),
|
||||
},
|
||||
userSyncStrategy: users.DefaultUserSyncStrategy{
|
||||
PasswordEncryption: passwordEncryption,
|
||||
RoleDeletionSuffix: cfg.OpConfig.RoleDeletionSuffix,
|
||||
@@ -271,25 +274,29 @@ func (c *Cluster) Create() (err error) {
|
||||
)
|
||||
|
||||
defer func() {
|
||||
var (
|
||||
pgUpdatedStatus *acidv1.Postgresql
|
||||
errStatus error
|
||||
)
|
||||
if err == nil {
|
||||
pgUpdatedStatus, errStatus = c.KubeClient.SetPostgresCRDStatus(c.clusterName(), acidv1.ClusterStatusRunning) //TODO: are you sure it's running?
|
||||
} else {
|
||||
currentStatus := c.Status.DeepCopy()
|
||||
pg := c.Postgresql.DeepCopy()
|
||||
pg.Status.PostgresClusterStatus = acidv1.ClusterStatusRunning
|
||||
|
||||
if err != nil {
|
||||
c.logger.Warningf("cluster created failed: %v", err)
|
||||
pgUpdatedStatus, errStatus = c.KubeClient.SetPostgresCRDStatus(c.clusterName(), acidv1.ClusterStatusAddFailed)
|
||||
pg.Status.PostgresClusterStatus = acidv1.ClusterStatusAddFailed
|
||||
}
|
||||
if errStatus != nil {
|
||||
c.logger.Warningf("could not set cluster status: %v", errStatus)
|
||||
}
|
||||
if pgUpdatedStatus != nil {
|
||||
|
||||
if !equality.Semantic.DeepEqual(currentStatus, pg.Status) {
|
||||
pgUpdatedStatus, err := c.KubeClient.SetPostgresCRDStatus(c.clusterName(), pg)
|
||||
if err != nil {
|
||||
c.logger.Warningf("could not set cluster status: %v", err)
|
||||
return
|
||||
}
|
||||
c.setSpec(pgUpdatedStatus)
|
||||
}
|
||||
}()
|
||||
|
||||
pgCreateStatus, err = c.KubeClient.SetPostgresCRDStatus(c.clusterName(), acidv1.ClusterStatusCreating)
|
||||
pg := c.Postgresql.DeepCopy()
|
||||
pg.Status.PostgresClusterStatus = acidv1.ClusterStatusCreating
|
||||
|
||||
pgCreateStatus, err = c.KubeClient.SetPostgresCRDStatus(c.clusterName(), pg)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not set cluster status: %v", err)
|
||||
}
|
||||
@@ -380,7 +387,7 @@ func (c *Cluster) Create() (err error) {
|
||||
|
||||
// create database objects unless we are running without pods or disabled
|
||||
// that feature explicitly
|
||||
if !(c.databaseAccessDisabled() || c.getNumberOfInstances(&c.Spec) <= 0 || c.Spec.StandbyCluster != nil) {
|
||||
if !(c.databaseAccessDisabled() || c.getNumberOfInstances(&c.Spec) <= 0 || isStandbyCluster(&c.Spec)) {
|
||||
c.logger.Infof("Create roles")
|
||||
if err = c.createRoles(); err != nil {
|
||||
return fmt.Errorf("could not create users: %v", err)
|
||||
@@ -440,7 +447,7 @@ func (c *Cluster) compareStatefulSetWith(statefulSet *appsv1.StatefulSet) *compa
|
||||
var match, needsRollUpdate, needsReplace bool
|
||||
|
||||
match = true
|
||||
//TODO: improve me
|
||||
// TODO: improve me
|
||||
if *c.Statefulset.Spec.Replicas != *statefulSet.Spec.Replicas {
|
||||
match = false
|
||||
reasons = append(reasons, "new statefulset's number of replicas does not match the current one")
|
||||
@@ -499,6 +506,11 @@ func (c *Cluster) compareStatefulSetWith(statefulSet *appsv1.StatefulSet) *compa
|
||||
needsRollUpdate = true
|
||||
reasons = append(reasons, "new statefulset's pod affinity does not match the current one")
|
||||
}
|
||||
if !reflect.DeepEqual(c.Statefulset.Spec.Template.Spec.TopologySpreadConstraints, statefulSet.Spec.Template.Spec.TopologySpreadConstraints) {
|
||||
needsReplace = true
|
||||
needsRollUpdate = true
|
||||
reasons = append(reasons, "new statefulset's pod topologySpreadConstraints does not match the current one")
|
||||
}
|
||||
if len(c.Statefulset.Spec.Template.Spec.Tolerations) != len(statefulSet.Spec.Template.Spec.Tolerations) {
|
||||
needsReplace = true
|
||||
needsRollUpdate = true
|
||||
@@ -613,6 +625,8 @@ func (c *Cluster) compareContainers(description string, setA, setB []v1.Containe
|
||||
func(a, b v1.Container) bool { return a.Name != b.Name }),
|
||||
newCheck("new %s's %s (index %d) readiness probe does not match the current one",
|
||||
func(a, b v1.Container) bool { return !reflect.DeepEqual(a.ReadinessProbe, b.ReadinessProbe) }),
|
||||
newCheck("new %s's %s (index %d) liveness probe does not match the current one",
|
||||
func(a, b v1.Container) bool { return !reflect.DeepEqual(a.LivenessProbe, b.LivenessProbe) }),
|
||||
newCheck("new %s's %s (index %d) ports do not match the current one",
|
||||
func(a, b v1.Container) bool { return !comparePorts(a.Ports, b.Ports) }),
|
||||
newCheck("new %s's %s (index %d) resources do not match the current ones",
|
||||
@@ -672,7 +686,6 @@ func compareResourcesAssumeFirstNotNil(a *v1.ResourceRequirements, b *v1.Resourc
|
||||
}
|
||||
}
|
||||
return true
|
||||
|
||||
}
|
||||
|
||||
func compareEnv(a, b []v1.EnvVar) bool {
|
||||
@@ -707,9 +720,7 @@ func compareEnv(a, b []v1.EnvVar) bool {
|
||||
}
|
||||
|
||||
func compareSpiloConfiguration(configa, configb string) bool {
|
||||
var (
|
||||
oa, ob spiloConfiguration
|
||||
)
|
||||
var oa, ob spiloConfiguration
|
||||
|
||||
var err error
|
||||
err = json.Unmarshal([]byte(configa), &oa)
|
||||
@@ -818,7 +829,6 @@ func (c *Cluster) compareAnnotations(old, new map[string]string, removedList *[]
|
||||
}
|
||||
|
||||
return reason != "", reason
|
||||
|
||||
}
|
||||
|
||||
func (c *Cluster) compareServices(old, new *v1.Service) (bool, string) {
|
||||
@@ -895,7 +905,7 @@ func (c *Cluster) compareLogicalBackupJob(cur, new *batchv1.CronJob) *compareLog
|
||||
}
|
||||
|
||||
func (c *Cluster) comparePodDisruptionBudget(cur, new *policyv1.PodDisruptionBudget) (bool, string) {
|
||||
//TODO: improve comparison
|
||||
// TODO: improve comparison
|
||||
if !reflect.DeepEqual(new.Spec, cur.Spec) {
|
||||
return false, "new PDB's spec does not match the current one"
|
||||
}
|
||||
@@ -944,8 +954,17 @@ func (c *Cluster) removeFinalizer() error {
|
||||
}
|
||||
|
||||
c.logger.Infof("removing finalizer %s", finalizerName)
|
||||
finalizers := util.RemoveString(c.ObjectMeta.Finalizers, finalizerName)
|
||||
newSpec, err := c.KubeClient.SetFinalizer(c.clusterName(), c.DeepCopy(), finalizers)
|
||||
|
||||
// Fetch the latest version of the object to avoid resourceVersion conflicts
|
||||
clusterName := c.clusterName()
|
||||
latestPg, err := c.KubeClient.PostgresqlsGetter.Postgresqls(clusterName.Namespace).Get(
|
||||
context.TODO(), clusterName.Name, metav1.GetOptions{})
|
||||
if err != nil {
|
||||
return fmt.Errorf("error fetching latest postgresql for finalizer removal: %v", err)
|
||||
}
|
||||
|
||||
finalizers := util.RemoveString(latestPg.ObjectMeta.Finalizers, finalizerName)
|
||||
newSpec, err := c.KubeClient.SetFinalizer(clusterName, latestPg, finalizers)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error removing finalizer: %v", err)
|
||||
}
|
||||
@@ -977,28 +996,33 @@ func (c *Cluster) Update(oldSpec, newSpec *acidv1.Postgresql) error {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
c.KubeClient.SetPostgresCRDStatus(c.clusterName(), acidv1.ClusterStatusUpdating)
|
||||
newSpec.Status.PostgresClusterStatus = acidv1.ClusterStatusUpdating
|
||||
|
||||
if !isInMaintenanceWindow(newSpec.Spec.MaintenanceWindows) {
|
||||
newSpec, err := c.KubeClient.SetPostgresCRDStatus(c.clusterName(), newSpec)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not set cluster status to updating: %w", err)
|
||||
}
|
||||
|
||||
if !c.isInMaintenanceWindow(newSpec.Spec.MaintenanceWindows) {
|
||||
// do not apply any major version related changes yet
|
||||
newSpec.Spec.PostgresqlParam.PgVersion = oldSpec.Spec.PostgresqlParam.PgVersion
|
||||
}
|
||||
c.setSpec(newSpec)
|
||||
|
||||
defer func() {
|
||||
var (
|
||||
pgUpdatedStatus *acidv1.Postgresql
|
||||
err error
|
||||
)
|
||||
currentStatus := newSpec.Status.DeepCopy()
|
||||
newSpec.Status.PostgresClusterStatus = acidv1.ClusterStatusRunning
|
||||
|
||||
if updateFailed {
|
||||
pgUpdatedStatus, err = c.KubeClient.SetPostgresCRDStatus(c.clusterName(), acidv1.ClusterStatusUpdateFailed)
|
||||
} else {
|
||||
pgUpdatedStatus, err = c.KubeClient.SetPostgresCRDStatus(c.clusterName(), acidv1.ClusterStatusRunning)
|
||||
newSpec.Status.PostgresClusterStatus = acidv1.ClusterStatusUpdateFailed
|
||||
}
|
||||
if err != nil {
|
||||
c.logger.Warningf("could not set cluster status: %v", err)
|
||||
}
|
||||
if pgUpdatedStatus != nil {
|
||||
|
||||
if !equality.Semantic.DeepEqual(currentStatus, newSpec.Status) {
|
||||
pgUpdatedStatus, err := c.KubeClient.SetPostgresCRDStatus(c.clusterName(), newSpec)
|
||||
if err != nil {
|
||||
c.logger.Warningf("could not set cluster status: %v", err)
|
||||
return
|
||||
}
|
||||
c.setSpec(pgUpdatedStatus)
|
||||
}
|
||||
}()
|
||||
@@ -1063,7 +1087,7 @@ func (c *Cluster) Update(oldSpec, newSpec *acidv1.Postgresql) error {
|
||||
}
|
||||
|
||||
c.logger.Debug("syncing secrets")
|
||||
//TODO: mind the secrets of the deleted/new users
|
||||
// TODO: mind the secrets of the deleted/new users
|
||||
if err := c.syncSecrets(); err != nil {
|
||||
c.logger.Errorf("could not sync secrets: %v", err)
|
||||
updateFailed = true
|
||||
@@ -1101,7 +1125,6 @@ func (c *Cluster) Update(oldSpec, newSpec *acidv1.Postgresql) error {
|
||||
|
||||
// logical backup job
|
||||
func() {
|
||||
|
||||
// create if it did not exist
|
||||
if !oldSpec.Spec.EnableLogicalBackup && newSpec.Spec.EnableLogicalBackup {
|
||||
c.logger.Debug("creating backup cron job")
|
||||
@@ -1129,7 +1152,6 @@ func (c *Cluster) Update(oldSpec, newSpec *acidv1.Postgresql) error {
|
||||
updateFailed = true
|
||||
}
|
||||
}
|
||||
|
||||
}()
|
||||
|
||||
// Roles and Databases
|
||||
@@ -1206,7 +1228,7 @@ func syncResources(a, b *v1.ResourceRequirements) bool {
|
||||
// before the pods, it will be re-created by the current master pod and will remain, obstructing the
|
||||
// creation of the new cluster with the same name. Therefore, the endpoints should be deleted last.
|
||||
func (c *Cluster) Delete() error {
|
||||
var anyErrors = false
|
||||
anyErrors := false
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
c.eventRecorder.Event(c.GetReference(), v1.EventTypeNormal, "Delete", "Started deletion of cluster resources")
|
||||
@@ -1283,7 +1305,7 @@ func (c *Cluster) Delete() error {
|
||||
// If we are done deleting our various resources we remove the finalizer to let K8S finally delete the Postgres CR
|
||||
if anyErrors {
|
||||
c.eventRecorder.Event(c.GetReference(), v1.EventTypeWarning, "Delete", "some resources could be successfully deleted yet")
|
||||
return fmt.Errorf("some error(s) occured when deleting resources, NOT removing finalizer yet")
|
||||
return fmt.Errorf("some error(s) occurred when deleting resources, NOT removing finalizer yet")
|
||||
}
|
||||
if err := c.removeFinalizer(); err != nil {
|
||||
return fmt.Errorf("done cleaning up, but error when removing finalizer: %v", err)
|
||||
@@ -1297,7 +1319,6 @@ func (c *Cluster) NeedsRepair() (bool, acidv1.PostgresStatus) {
|
||||
c.specMu.RLock()
|
||||
defer c.specMu.RUnlock()
|
||||
return !c.Status.Success(), c.Status
|
||||
|
||||
}
|
||||
|
||||
// ReceivePodEvent is called back by the controller in order to add the cluster's pod event to the queue.
|
||||
@@ -1406,7 +1427,6 @@ func (c *Cluster) initSystemUsers() {
|
||||
}
|
||||
|
||||
func (c *Cluster) initPreparedDatabaseRoles() error {
|
||||
|
||||
if c.Spec.PreparedDatabases != nil && len(c.Spec.PreparedDatabases) == 0 { // TODO: add option to disable creating such a default DB
|
||||
c.Spec.PreparedDatabases = map[string]acidv1.PreparedDatabase{strings.Replace(c.Name, "-", "_", -1): {}}
|
||||
}
|
||||
@@ -1472,10 +1492,9 @@ func (c *Cluster) initPreparedDatabaseRoles() error {
|
||||
}
|
||||
|
||||
func (c *Cluster) initDefaultRoles(defaultRoles map[string]string, admin, prefix, searchPath, secretNamespace string) error {
|
||||
|
||||
for defaultRole, inherits := range defaultRoles {
|
||||
namespace := c.Namespace
|
||||
//if namespaced secrets are allowed
|
||||
// if namespaced secrets are allowed
|
||||
if secretNamespace != "" {
|
||||
if c.Config.OpConfig.EnableCrossNamespaceSecret {
|
||||
namespace = secretNamespace
|
||||
@@ -1543,7 +1562,7 @@ func (c *Cluster) initRobotUsers() error {
|
||||
}
|
||||
}
|
||||
|
||||
//if namespaced secrets are allowed
|
||||
// if namespaced secrets are allowed
|
||||
if c.Config.OpConfig.EnableCrossNamespaceSecret {
|
||||
if strings.Contains(username, ".") {
|
||||
splits := strings.Split(username, ".")
|
||||
@@ -1594,7 +1613,6 @@ func (c *Cluster) initAdditionalOwnerRoles() {
|
||||
|
||||
func (c *Cluster) initTeamMembers(teamID string, isPostgresSuperuserTeam bool) error {
|
||||
teamMembers, err := c.getTeamMembers(teamID)
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not get list of team members for team %q: %v", teamID, err)
|
||||
}
|
||||
@@ -1633,7 +1651,6 @@ func (c *Cluster) initTeamMembers(teamID string, isPostgresSuperuserTeam bool) e
|
||||
}
|
||||
|
||||
func (c *Cluster) initHumanUsers() error {
|
||||
|
||||
var clusterIsOwnedBySuperuserTeam bool
|
||||
superuserTeams := []string{}
|
||||
|
||||
@@ -1767,10 +1784,27 @@ func (c *Cluster) GetStatus() *ClusterStatus {
|
||||
}
|
||||
|
||||
func (c *Cluster) GetSwitchoverSchedule() string {
|
||||
now := time.Now().UTC()
|
||||
return c.getSwitchoverScheduleAtTime(now)
|
||||
}
|
||||
|
||||
func (c *Cluster) getSwitchoverScheduleAtTime(now time.Time) string {
|
||||
var possibleSwitchover, schedule time.Time
|
||||
|
||||
now := time.Now().UTC()
|
||||
for _, window := range c.Spec.MaintenanceWindows {
|
||||
maintenanceWindows := c.Spec.MaintenanceWindows
|
||||
if len(maintenanceWindows) == 0 {
|
||||
maintenanceWindows = make([]acidv1.MaintenanceWindow, 0, len(c.OpConfig.MaintenanceWindows))
|
||||
for _, windowStr := range c.OpConfig.MaintenanceWindows {
|
||||
var window acidv1.MaintenanceWindow
|
||||
if err := window.UnmarshalJSON([]byte(windowStr)); err != nil {
|
||||
c.logger.Errorf("could not parse default maintenance window %q: %v", windowStr, err)
|
||||
continue
|
||||
}
|
||||
maintenanceWindows = append(maintenanceWindows, window)
|
||||
}
|
||||
}
|
||||
|
||||
for _, window := range maintenanceWindows {
|
||||
// in the best case it is possible today
|
||||
possibleSwitchover = time.Date(now.Year(), now.Month(), now.Day(), window.StartTime.Hour(), window.StartTime.Minute(), 0, 0, time.UTC)
|
||||
if window.Everyday {
|
||||
@@ -1788,10 +1822,15 @@ func (c *Cluster) GetSwitchoverSchedule() string {
|
||||
}
|
||||
}
|
||||
|
||||
if (schedule == time.Time{}) || possibleSwitchover.Before(schedule) {
|
||||
if (schedule.Equal(time.Time{})) || possibleSwitchover.Before(schedule) {
|
||||
schedule = possibleSwitchover
|
||||
}
|
||||
}
|
||||
|
||||
if schedule.IsZero() {
|
||||
return ""
|
||||
}
|
||||
|
||||
return schedule.Format("2006-01-02T15:04+00")
|
||||
}
|
||||
|
||||
|
||||
+51
-41
@@ -680,8 +680,7 @@ func TestServiceAnnotations(t *testing.T) {
|
||||
operatorAnnotations: make(map[string]string),
|
||||
serviceAnnotations: make(map[string]string),
|
||||
expect: map[string]string{
|
||||
"external-dns.alpha.kubernetes.io/hostname": "acid-test-stg.test.db.example.com,test-stg.acid.db.example.com",
|
||||
"service.beta.kubernetes.io/aws-load-balancer-connection-idle-timeout": "3600",
|
||||
"external-dns.alpha.kubernetes.io/hostname": "acid-test-stg.test.db.example.com,test-stg.acid.db.example.com",
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -702,8 +701,7 @@ func TestServiceAnnotations(t *testing.T) {
|
||||
operatorAnnotations: make(map[string]string),
|
||||
serviceAnnotations: make(map[string]string),
|
||||
expect: map[string]string{
|
||||
"external-dns.alpha.kubernetes.io/hostname": "acid-test-stg.test.db.example.com,test-stg.acid.db.example.com",
|
||||
"service.beta.kubernetes.io/aws-load-balancer-connection-idle-timeout": "3600",
|
||||
"external-dns.alpha.kubernetes.io/hostname": "acid-test-stg.test.db.example.com,test-stg.acid.db.example.com",
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -714,8 +712,7 @@ func TestServiceAnnotations(t *testing.T) {
|
||||
operatorAnnotations: make(map[string]string),
|
||||
serviceAnnotations: map[string]string{"foo": "bar"},
|
||||
expect: map[string]string{
|
||||
"external-dns.alpha.kubernetes.io/hostname": "acid-test-stg.test.db.example.com,test-stg.acid.db.example.com",
|
||||
"service.beta.kubernetes.io/aws-load-balancer-connection-idle-timeout": "3600",
|
||||
"external-dns.alpha.kubernetes.io/hostname": "acid-test-stg.test.db.example.com,test-stg.acid.db.example.com",
|
||||
"foo": "bar",
|
||||
},
|
||||
},
|
||||
@@ -737,8 +734,7 @@ func TestServiceAnnotations(t *testing.T) {
|
||||
operatorAnnotations: map[string]string{"foo": "bar"},
|
||||
serviceAnnotations: make(map[string]string),
|
||||
expect: map[string]string{
|
||||
"external-dns.alpha.kubernetes.io/hostname": "acid-test-stg.test.db.example.com,test-stg.acid.db.example.com",
|
||||
"service.beta.kubernetes.io/aws-load-balancer-connection-idle-timeout": "3600",
|
||||
"external-dns.alpha.kubernetes.io/hostname": "acid-test-stg.test.db.example.com,test-stg.acid.db.example.com",
|
||||
"foo": "bar",
|
||||
},
|
||||
},
|
||||
@@ -780,8 +776,7 @@ func TestServiceAnnotations(t *testing.T) {
|
||||
"external-dns.alpha.kubernetes.io/hostname": "wrong.external-dns-name.example.com",
|
||||
},
|
||||
expect: map[string]string{
|
||||
"external-dns.alpha.kubernetes.io/hostname": "acid-test-stg.test.db.example.com,test-stg.acid.db.example.com",
|
||||
"service.beta.kubernetes.io/aws-load-balancer-connection-idle-timeout": "3600",
|
||||
"external-dns.alpha.kubernetes.io/hostname": "acid-test-stg.test.db.example.com,test-stg.acid.db.example.com",
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -792,8 +787,7 @@ func TestServiceAnnotations(t *testing.T) {
|
||||
serviceAnnotations: make(map[string]string),
|
||||
operatorAnnotations: make(map[string]string),
|
||||
expect: map[string]string{
|
||||
"external-dns.alpha.kubernetes.io/hostname": "acid-test-stg.test.db.example.com,test-stg.acid.db.example.com",
|
||||
"service.beta.kubernetes.io/aws-load-balancer-connection-idle-timeout": "3600",
|
||||
"external-dns.alpha.kubernetes.io/hostname": "acid-test-stg.test.db.example.com,test-stg.acid.db.example.com",
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -835,8 +829,7 @@ func TestServiceAnnotations(t *testing.T) {
|
||||
operatorAnnotations: make(map[string]string),
|
||||
serviceAnnotations: make(map[string]string),
|
||||
expect: map[string]string{
|
||||
"external-dns.alpha.kubernetes.io/hostname": "acid-test-stg-repl.test.db.example.com,test-stg-repl.acid.db.example.com",
|
||||
"service.beta.kubernetes.io/aws-load-balancer-connection-idle-timeout": "3600",
|
||||
"external-dns.alpha.kubernetes.io/hostname": "acid-test-stg-repl.test.db.example.com,test-stg-repl.acid.db.example.com",
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -857,8 +850,7 @@ func TestServiceAnnotations(t *testing.T) {
|
||||
operatorAnnotations: make(map[string]string),
|
||||
serviceAnnotations: make(map[string]string),
|
||||
expect: map[string]string{
|
||||
"external-dns.alpha.kubernetes.io/hostname": "acid-test-stg-repl.test.db.example.com,test-stg-repl.acid.db.example.com",
|
||||
"service.beta.kubernetes.io/aws-load-balancer-connection-idle-timeout": "3600",
|
||||
"external-dns.alpha.kubernetes.io/hostname": "acid-test-stg-repl.test.db.example.com,test-stg-repl.acid.db.example.com",
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -869,8 +861,7 @@ func TestServiceAnnotations(t *testing.T) {
|
||||
operatorAnnotations: make(map[string]string),
|
||||
serviceAnnotations: map[string]string{"foo": "bar"},
|
||||
expect: map[string]string{
|
||||
"external-dns.alpha.kubernetes.io/hostname": "acid-test-stg-repl.test.db.example.com,test-stg-repl.acid.db.example.com",
|
||||
"service.beta.kubernetes.io/aws-load-balancer-connection-idle-timeout": "3600",
|
||||
"external-dns.alpha.kubernetes.io/hostname": "acid-test-stg-repl.test.db.example.com,test-stg-repl.acid.db.example.com",
|
||||
"foo": "bar",
|
||||
},
|
||||
},
|
||||
@@ -892,8 +883,7 @@ func TestServiceAnnotations(t *testing.T) {
|
||||
operatorAnnotations: map[string]string{"foo": "bar"},
|
||||
serviceAnnotations: make(map[string]string),
|
||||
expect: map[string]string{
|
||||
"external-dns.alpha.kubernetes.io/hostname": "acid-test-stg-repl.test.db.example.com,test-stg-repl.acid.db.example.com",
|
||||
"service.beta.kubernetes.io/aws-load-balancer-connection-idle-timeout": "3600",
|
||||
"external-dns.alpha.kubernetes.io/hostname": "acid-test-stg-repl.test.db.example.com,test-stg-repl.acid.db.example.com",
|
||||
"foo": "bar",
|
||||
},
|
||||
},
|
||||
@@ -935,8 +925,7 @@ func TestServiceAnnotations(t *testing.T) {
|
||||
"external-dns.alpha.kubernetes.io/hostname": "wrong.external-dns-name.example.com",
|
||||
},
|
||||
expect: map[string]string{
|
||||
"external-dns.alpha.kubernetes.io/hostname": "acid-test-stg-repl.test.db.example.com,test-stg-repl.acid.db.example.com",
|
||||
"service.beta.kubernetes.io/aws-load-balancer-connection-idle-timeout": "3600",
|
||||
"external-dns.alpha.kubernetes.io/hostname": "acid-test-stg-repl.test.db.example.com,test-stg-repl.acid.db.example.com",
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -947,8 +936,7 @@ func TestServiceAnnotations(t *testing.T) {
|
||||
serviceAnnotations: make(map[string]string),
|
||||
operatorAnnotations: make(map[string]string),
|
||||
expect: map[string]string{
|
||||
"external-dns.alpha.kubernetes.io/hostname": "acid-test-stg-repl.test.db.example.com,test-stg-repl.acid.db.example.com",
|
||||
"service.beta.kubernetes.io/aws-load-balancer-connection-idle-timeout": "3600",
|
||||
"external-dns.alpha.kubernetes.io/hostname": "acid-test-stg-repl.test.db.example.com,test-stg-repl.acid.db.example.com",
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -1377,7 +1365,6 @@ func TestCompareServices(t *testing.T) {
|
||||
serviceWithOwnerReference := newService(
|
||||
map[string]string{
|
||||
constants.ZalandoDNSNameAnnotation: "clstr.acid.zalan.do",
|
||||
constants.ElbTimeoutAnnotationName: constants.ElbTimeoutAnnotationValue,
|
||||
},
|
||||
v1.ServiceTypeClusterIP,
|
||||
[]string{"128.141.0.0/16", "137.138.0.0/16"},
|
||||
@@ -1406,7 +1393,6 @@ func TestCompareServices(t *testing.T) {
|
||||
current: newService(
|
||||
map[string]string{
|
||||
constants.ZalandoDNSNameAnnotation: "clstr.acid.zalan.do",
|
||||
constants.ElbTimeoutAnnotationName: constants.ElbTimeoutAnnotationValue,
|
||||
},
|
||||
v1.ServiceTypeClusterIP,
|
||||
[]string{"128.141.0.0/16", "137.138.0.0/16"},
|
||||
@@ -1414,7 +1400,6 @@ func TestCompareServices(t *testing.T) {
|
||||
new: newService(
|
||||
map[string]string{
|
||||
constants.ZalandoDNSNameAnnotation: "clstr.acid.zalan.do",
|
||||
constants.ElbTimeoutAnnotationName: constants.ElbTimeoutAnnotationValue,
|
||||
},
|
||||
v1.ServiceTypeClusterIP,
|
||||
[]string{"128.141.0.0/16", "137.138.0.0/16"},
|
||||
@@ -1426,7 +1411,6 @@ func TestCompareServices(t *testing.T) {
|
||||
current: newService(
|
||||
map[string]string{
|
||||
constants.ZalandoDNSNameAnnotation: "clstr.acid.zalan.do",
|
||||
constants.ElbTimeoutAnnotationName: constants.ElbTimeoutAnnotationValue,
|
||||
},
|
||||
v1.ServiceTypeClusterIP,
|
||||
[]string{"128.141.0.0/16", "137.138.0.0/16"},
|
||||
@@ -1434,7 +1418,6 @@ func TestCompareServices(t *testing.T) {
|
||||
new: newService(
|
||||
map[string]string{
|
||||
constants.ZalandoDNSNameAnnotation: "clstr.acid.zalan.do",
|
||||
constants.ElbTimeoutAnnotationName: constants.ElbTimeoutAnnotationValue,
|
||||
},
|
||||
v1.ServiceTypeLoadBalancer,
|
||||
[]string{"128.141.0.0/16", "137.138.0.0/16"},
|
||||
@@ -1447,7 +1430,6 @@ func TestCompareServices(t *testing.T) {
|
||||
current: newService(
|
||||
map[string]string{
|
||||
constants.ZalandoDNSNameAnnotation: "clstr.acid.zalan.do",
|
||||
constants.ElbTimeoutAnnotationName: constants.ElbTimeoutAnnotationValue,
|
||||
},
|
||||
v1.ServiceTypeLoadBalancer,
|
||||
[]string{"128.141.0.0/16", "137.138.0.0/16"},
|
||||
@@ -1455,7 +1437,6 @@ func TestCompareServices(t *testing.T) {
|
||||
new: newService(
|
||||
map[string]string{
|
||||
constants.ZalandoDNSNameAnnotation: "clstr.acid.zalan.do",
|
||||
constants.ElbTimeoutAnnotationName: constants.ElbTimeoutAnnotationValue,
|
||||
},
|
||||
v1.ServiceTypeLoadBalancer,
|
||||
[]string{"185.249.56.0/22"},
|
||||
@@ -1468,7 +1449,6 @@ func TestCompareServices(t *testing.T) {
|
||||
current: newService(
|
||||
map[string]string{
|
||||
constants.ZalandoDNSNameAnnotation: "clstr.acid.zalan.do",
|
||||
constants.ElbTimeoutAnnotationName: constants.ElbTimeoutAnnotationValue,
|
||||
},
|
||||
v1.ServiceTypeLoadBalancer,
|
||||
[]string{"128.141.0.0/16", "137.138.0.0/16"},
|
||||
@@ -1476,7 +1456,6 @@ func TestCompareServices(t *testing.T) {
|
||||
new: newService(
|
||||
map[string]string{
|
||||
constants.ZalandoDNSNameAnnotation: "clstr.acid.zalan.do",
|
||||
constants.ElbTimeoutAnnotationName: constants.ElbTimeoutAnnotationValue,
|
||||
},
|
||||
v1.ServiceTypeLoadBalancer,
|
||||
[]string{},
|
||||
@@ -1489,7 +1468,6 @@ func TestCompareServices(t *testing.T) {
|
||||
current: newService(
|
||||
map[string]string{
|
||||
constants.ZalandoDNSNameAnnotation: "clstr.acid.zalan.do",
|
||||
constants.ElbTimeoutAnnotationName: constants.ElbTimeoutAnnotationValue,
|
||||
},
|
||||
v1.ServiceTypeClusterIP,
|
||||
[]string{"128.141.0.0/16", "137.138.0.0/16"},
|
||||
@@ -1603,8 +1581,8 @@ func newCronJob(image, schedule string, vars []v1.EnvVar, mounts []v1.VolumeMoun
|
||||
|
||||
func TestCompareLogicalBackupJob(t *testing.T) {
|
||||
|
||||
img1 := "registry.opensource.zalan.do/acid/logical-backup:v1.0"
|
||||
img2 := "registry.opensource.zalan.do/acid/logical-backup:v2.0"
|
||||
img1 := "ghcr.io/zalando/postgres-operator/logical-backup:v1.14.0"
|
||||
img2 := "ghcr.io/zalando/postgres-operator/logical-backup:v1.15.1"
|
||||
|
||||
clientSet := fake.NewSimpleClientset()
|
||||
acidClientSet := fakeacidv1.NewSimpleClientset()
|
||||
@@ -2116,7 +2094,7 @@ func TestCompareVolumeMounts(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestGetSwitchoverSchedule(t *testing.T) {
|
||||
now := time.Now()
|
||||
now, _ := time.Parse(time.RFC3339, "2025-11-11T12:35:00Z")
|
||||
|
||||
futureTimeStart := now.Add(1 * time.Hour)
|
||||
futureWindowTimeStart := futureTimeStart.Format("15:04")
|
||||
@@ -2125,10 +2103,13 @@ func TestGetSwitchoverSchedule(t *testing.T) {
|
||||
pastWindowTimeStart := pastTimeStart.Format("15:04")
|
||||
pastWindowTimeEnd := now.Add(-1 * time.Hour).Format("15:04")
|
||||
|
||||
defaultWindowStr := fmt.Sprintf("%s-%s", futureWindowTimeStart, futureWindowTimeEnd)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
windows []acidv1.MaintenanceWindow
|
||||
expected string
|
||||
name string
|
||||
windows []acidv1.MaintenanceWindow
|
||||
defaultWindows []string
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "everyday maintenance windows is later today",
|
||||
@@ -2190,12 +2171,41 @@ func TestGetSwitchoverSchedule(t *testing.T) {
|
||||
},
|
||||
expected: pastTimeStart.AddDate(0, 0, 1).Format("2006-01-02T15:04+00"),
|
||||
},
|
||||
{
|
||||
name: "fallback to operator default window when spec is empty",
|
||||
windows: []acidv1.MaintenanceWindow{},
|
||||
defaultWindows: []string{defaultWindowStr},
|
||||
expected: futureTimeStart.Format("2006-01-02T15:04+00"),
|
||||
},
|
||||
{
|
||||
name: "no windows defined returns empty string",
|
||||
windows: []acidv1.MaintenanceWindow{},
|
||||
defaultWindows: nil,
|
||||
expected: "",
|
||||
},
|
||||
{
|
||||
name: "choose the earliest window from multiple in spec",
|
||||
windows: []acidv1.MaintenanceWindow{
|
||||
{
|
||||
Weekday: now.AddDate(0, 0, 2).Weekday(),
|
||||
StartTime: mustParseTime(futureWindowTimeStart),
|
||||
EndTime: mustParseTime(futureWindowTimeEnd),
|
||||
},
|
||||
{
|
||||
Weekday: now.AddDate(0, 0, 1).Weekday(),
|
||||
StartTime: mustParseTime(pastWindowTimeStart),
|
||||
EndTime: mustParseTime(pastWindowTimeEnd),
|
||||
},
|
||||
},
|
||||
expected: pastTimeStart.AddDate(0, 0, 1).Format("2006-01-02T15:04+00"),
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
cluster.Spec.MaintenanceWindows = tt.windows
|
||||
schedule := cluster.GetSwitchoverSchedule()
|
||||
cluster.OpConfig.MaintenanceWindows = tt.defaultWindows
|
||||
schedule := cluster.getSwitchoverScheduleAtTime(now)
|
||||
if schedule != tt.expected {
|
||||
t.Errorf("Expected GetSwitchoverSchedule to return %s, returned: %s", tt.expected, schedule)
|
||||
}
|
||||
|
||||
@@ -31,6 +31,7 @@ var poolerRunAsGroup = int64(101)
|
||||
|
||||
// ConnectionPoolerObjects K8s objects that are belong to connection pooler
|
||||
type ConnectionPoolerObjects struct {
|
||||
AuthSecret *v1.Secret
|
||||
Deployment *appsv1.Deployment
|
||||
Service *v1.Service
|
||||
Name string
|
||||
@@ -167,6 +168,38 @@ func (c *Cluster) createConnectionPooler(LookupFunction InstallFunction) (SyncRe
|
||||
return reason, nil
|
||||
}
|
||||
|
||||
func (c *Cluster) generateUserlist() string {
|
||||
var sb strings.Builder
|
||||
|
||||
poolerAdminUser := c.systemUsers[constants.ConnectionPoolerUserKeyName]
|
||||
fmt.Fprintf(&sb, "\"%s\" \"%s\"\n", poolerAdminUser.Name, poolerAdminUser.Password)
|
||||
|
||||
for roleName, infraRole := range c.InfrastructureRoles {
|
||||
if infraRole.Password != "" {
|
||||
fmt.Fprintf(&sb, "\"%s\" \"%s\"\n", roleName, infraRole.Password)
|
||||
}
|
||||
}
|
||||
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
func (c *Cluster) generateConnectionPoolerAuthSecret(connectionPooler *ConnectionPoolerObjects) *v1.Secret {
|
||||
return &v1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Labels: c.connectionPoolerLabels(connectionPooler.Role, true).MatchLabels,
|
||||
Name: fmt.Sprintf("%s-u", connectionPooler.Name),
|
||||
Namespace: connectionPooler.Namespace,
|
||||
Annotations: c.annotationsSet(nil),
|
||||
OwnerReferences: c.ownerReferences(),
|
||||
},
|
||||
Type: v1.SecretTypeOpaque,
|
||||
// Secret data must be bytes. Kubernetes handles the encoding.
|
||||
StringData: map[string]string{
|
||||
"userlist.txt": c.generateUserlist(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Generate pool size related environment variables.
|
||||
//
|
||||
// MAX_DB_CONN would specify the global maximum for connections to a target
|
||||
@@ -320,6 +353,18 @@ func (c *Cluster) generateConnectionPoolerPodTemplate(role PostgresRole) (
|
||||
}
|
||||
envVars = append(envVars, c.getConnectionPoolerEnvVars()...)
|
||||
|
||||
infraRolesList := make([]string, 0)
|
||||
for infraRoleName := range c.InfrastructureRoles {
|
||||
infraRolesList = append(infraRolesList, infraRoleName)
|
||||
}
|
||||
|
||||
if len(infraRolesList) > 0 {
|
||||
envVars = append(envVars, v1.EnvVar{
|
||||
Name: "INFRASTRUCTURE_ROLES",
|
||||
Value: strings.Join(infraRolesList, ","),
|
||||
})
|
||||
}
|
||||
|
||||
poolerContainer := v1.Container{
|
||||
Name: connectionPoolerContainer,
|
||||
Image: effectiveDockerImage,
|
||||
@@ -343,12 +388,29 @@ func (c *Cluster) generateConnectionPoolerPodTemplate(role PostgresRole) (
|
||||
},
|
||||
}
|
||||
|
||||
var poolerVolumes []v1.Volume
|
||||
var volumeMounts []v1.VolumeMount
|
||||
|
||||
// mount secret volume with userlist.txt for pgBouncer to authenticate users
|
||||
poolerVolumes = append(poolerVolumes, v1.Volume{
|
||||
Name: fmt.Sprintf("%s-u", c.connectionPoolerName(role)),
|
||||
VolumeSource: v1.VolumeSource{
|
||||
Secret: &v1.SecretVolumeSource{
|
||||
SecretName: fmt.Sprintf("%s-u", c.connectionPoolerName(role)),
|
||||
},
|
||||
},
|
||||
})
|
||||
volumeMounts = append(volumeMounts, v1.VolumeMount{
|
||||
Name: fmt.Sprintf("%s-u", c.connectionPoolerName(role)),
|
||||
MountPath: "/etc/pgbouncer/userlist.txt",
|
||||
SubPath: "userlist.txt",
|
||||
ReadOnly: true,
|
||||
})
|
||||
|
||||
// If the cluster has custom TLS certificates configured, we do the following:
|
||||
// 1. Add environment variables to tell pgBouncer where to find the TLS certificates
|
||||
// 2. Reference the secret in a volume
|
||||
// 3. Mount the volume to the container at /tls
|
||||
var poolerVolumes []v1.Volume
|
||||
var volumeMounts []v1.VolumeMount
|
||||
if spec.TLS != nil && spec.TLS.SecretName != "" {
|
||||
getPoolerTLSEnv := func(k string) string {
|
||||
keyName := ""
|
||||
@@ -533,10 +595,6 @@ func (c *Cluster) generatePoolerServiceAnnotations(role PostgresRole, spec *acid
|
||||
annotations := c.getCustomServiceAnnotations(role, spec)
|
||||
|
||||
if c.shouldCreateLoadBalancerForPoolerService(role, spec) {
|
||||
// set ELB Timeout annotation with default value
|
||||
if _, ok := annotations[constants.ElbTimeoutAnnotationName]; !ok {
|
||||
annotations[constants.ElbTimeoutAnnotationName] = constants.ElbTimeoutAnnotationValue
|
||||
}
|
||||
// -repl suffix will be added by replicaDNSName
|
||||
clusterNameWithPoolerSuffix := c.connectionPoolerName(Master)
|
||||
if role == Master {
|
||||
@@ -639,12 +697,31 @@ func (c *Cluster) deleteConnectionPooler(role PostgresRole) (err error) {
|
||||
c.logger.Infof("connection pooler service %s has been deleted for role %s", service.Name, role)
|
||||
}
|
||||
|
||||
// Repeat the same for the auth secret
|
||||
authSecret := c.ConnectionPooler[role].AuthSecret
|
||||
if authSecret == nil {
|
||||
c.logger.Debug("no connection pooler auth secret to delete")
|
||||
} else {
|
||||
err := c.KubeClient.
|
||||
Secrets(c.Namespace).
|
||||
Delete(context.TODO(), authSecret.Name, metav1.DeleteOptions{})
|
||||
|
||||
if k8sutil.ResourceNotFound(err) {
|
||||
c.logger.Debugf("connection pooler auth secret %s for role %s has already been deleted", authSecret.Name, role)
|
||||
} else if err != nil {
|
||||
return fmt.Errorf("could not delete connection pooler auth secret: %v", err)
|
||||
}
|
||||
|
||||
c.logger.Infof("connection pooler auth secret %s has been deleted for role %s", authSecret.Name, role)
|
||||
}
|
||||
|
||||
c.ConnectionPooler[role].AuthSecret = nil
|
||||
c.ConnectionPooler[role].Deployment = nil
|
||||
c.ConnectionPooler[role].Service = nil
|
||||
return nil
|
||||
}
|
||||
|
||||
// delete connection pooler
|
||||
// delete connection pooler secret
|
||||
func (c *Cluster) deleteConnectionPoolerSecret() (err error) {
|
||||
// Repeat the same for the secret object
|
||||
secretName := c.credentialSecretName(c.OpConfig.ConnectionPooler.User)
|
||||
@@ -660,6 +737,7 @@ func (c *Cluster) deleteConnectionPoolerSecret() (err error) {
|
||||
return fmt.Errorf("could not delete pooler secret: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -912,7 +990,7 @@ func (c *Cluster) syncConnectionPooler(oldSpec, newSpec *acidv1.Postgresql, Look
|
||||
|
||||
// in this case also do not forget to install lookup function
|
||||
// skip installation in standby clusters, since they are read-only
|
||||
if !c.ConnectionPooler[role].LookupFunction && c.Spec.StandbyCluster == nil {
|
||||
if !c.ConnectionPooler[role].LookupFunction && !isStandbyCluster(&newSpec.Spec) {
|
||||
connectionPooler := c.Spec.ConnectionPooler
|
||||
specSchema := ""
|
||||
specUser := ""
|
||||
@@ -975,11 +1053,42 @@ func (c *Cluster) syncConnectionPoolerWorker(oldSpec, newSpec *acidv1.Postgresql
|
||||
pods []v1.Pod
|
||||
service *v1.Service
|
||||
newService *v1.Service
|
||||
authSecret *v1.Secret
|
||||
newAuthSecret *v1.Secret
|
||||
err error
|
||||
)
|
||||
|
||||
updatedPodAnnotations := map[string]*string{}
|
||||
syncReason := make([]string, 0)
|
||||
|
||||
// create extra secret for connection pooler authentication
|
||||
newAuthSecret = c.generateConnectionPoolerAuthSecret(c.ConnectionPooler[role])
|
||||
if authSecret, err = c.KubeClient.Secrets(c.Namespace).Get(context.TODO(), fmt.Sprintf("%s-u", c.connectionPoolerName(role)), metav1.GetOptions{}); err == nil {
|
||||
c.ConnectionPooler[role].AuthSecret = authSecret
|
||||
// make sure existing annotations are preserved
|
||||
newAuthSecret.Annotations = c.annotationsSet(authSecret.Annotations)
|
||||
authSecret, err = c.KubeClient.Secrets(authSecret.Namespace).Update(context.TODO(), newAuthSecret, metav1.UpdateOptions{})
|
||||
if err != nil {
|
||||
return NoSync, fmt.Errorf("could not update connection pooler auth secret: %v", err)
|
||||
}
|
||||
c.ConnectionPooler[role].AuthSecret = authSecret
|
||||
} else if !k8sutil.ResourceNotFound(err) {
|
||||
return NoSync, fmt.Errorf("could not get auth secret for connection pooler to sync: %v", err)
|
||||
}
|
||||
|
||||
if k8sutil.ResourceNotFound(err) {
|
||||
c.logger.Warningf("auth secret %s for connection pooler is not found, create it", fmt.Sprintf("%s-u", c.connectionPoolerName(role)))
|
||||
authSecret, err = c.KubeClient.
|
||||
Secrets(newAuthSecret.Namespace).
|
||||
Create(context.TODO(), newAuthSecret, metav1.CreateOptions{})
|
||||
|
||||
if err != nil {
|
||||
return NoSync, err
|
||||
}
|
||||
c.ConnectionPooler[role].AuthSecret = authSecret
|
||||
}
|
||||
|
||||
// next the pooler deployment
|
||||
deployment, err = c.KubeClient.
|
||||
Deployments(c.Namespace).
|
||||
Get(context.TODO(), c.connectionPoolerName(role), metav1.GetOptions{})
|
||||
|
||||
@@ -30,6 +30,7 @@ func newFakeK8sPoolerTestClient() (k8sutil.KubernetesClient, *fake.Clientset) {
|
||||
StatefulSetsGetter: clientSet.AppsV1(),
|
||||
DeploymentsGetter: clientSet.AppsV1(),
|
||||
ServicesGetter: clientSet.CoreV1(),
|
||||
SecretsGetter: clientSet.CoreV1(),
|
||||
}, clientSet
|
||||
}
|
||||
|
||||
@@ -803,6 +804,7 @@ func TestConnectionPoolerDeploymentSpec(t *testing.T) {
|
||||
}
|
||||
cluster.ConnectionPooler = map[PostgresRole]*ConnectionPoolerObjects{
|
||||
Master: {
|
||||
AuthSecret: nil,
|
||||
Deployment: nil,
|
||||
Service: nil,
|
||||
LookupFunction: true,
|
||||
@@ -1019,6 +1021,7 @@ func TestPoolerTLS(t *testing.T) {
|
||||
// create pooler resources
|
||||
cluster.ConnectionPooler = map[PostgresRole]*ConnectionPoolerObjects{}
|
||||
cluster.ConnectionPooler[Master] = &ConnectionPoolerObjects{
|
||||
AuthSecret: nil,
|
||||
Deployment: nil,
|
||||
Service: nil,
|
||||
Name: cluster.connectionPoolerName(Master),
|
||||
@@ -1089,12 +1092,14 @@ func TestConnectionPoolerServiceSpec(t *testing.T) {
|
||||
}
|
||||
cluster.ConnectionPooler = map[PostgresRole]*ConnectionPoolerObjects{
|
||||
Master: {
|
||||
AuthSecret: nil,
|
||||
Deployment: nil,
|
||||
Service: nil,
|
||||
LookupFunction: false,
|
||||
Role: Master,
|
||||
},
|
||||
Replica: {
|
||||
AuthSecret: nil,
|
||||
Deployment: nil,
|
||||
Service: nil,
|
||||
LookupFunction: false,
|
||||
|
||||
+17
-3
@@ -281,9 +281,23 @@ func findUsersFromRotation(rotatedUsers []string, db *sql.DB) (map[string]string
|
||||
return extraUsers, nil
|
||||
}
|
||||
|
||||
func (c *Cluster) cleanupRotatedUsers(rotatedUsers []string, db *sql.DB) error {
|
||||
func (c *Cluster) cleanupRotatedUsers(rotatedUsers []string) error {
|
||||
c.setProcessName("checking for rotated users to remove from the database due to configured retention")
|
||||
extraUsers, err := findUsersFromRotation(rotatedUsers, db)
|
||||
|
||||
err := c.initDbConn()
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not init db connection: %v", err)
|
||||
}
|
||||
defer func() {
|
||||
if c.connectionIsClosed() {
|
||||
return
|
||||
}
|
||||
if err := c.closeDbConn(); err != nil {
|
||||
c.logger.Errorf("could not close database connection after removing users exceeding configured retention interval: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
extraUsers, err := findUsersFromRotation(rotatedUsers, c.pgDb)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error when querying for deprecated users from password rotation: %v", err)
|
||||
}
|
||||
@@ -304,7 +318,7 @@ func (c *Cluster) cleanupRotatedUsers(rotatedUsers []string, db *sql.DB) error {
|
||||
}
|
||||
if retentionDate.After(userCreationDate) {
|
||||
c.logger.Infof("dropping user %q due to configured days in password_rotation_user_retention", rotatedUser)
|
||||
if err = users.DropPgUser(rotatedUser, db); err != nil {
|
||||
if err = users.DropPgUser(rotatedUser, c.pgDb); err != nil {
|
||||
c.logger.Errorf("could not drop role %q: %v", rotatedUser, err)
|
||||
continue
|
||||
}
|
||||
|
||||
+100
-62
@@ -313,6 +313,14 @@ func (c *Cluster) generateResourceRequirements(
|
||||
specLimits := acidv1.ResourceDescription{}
|
||||
result := v1.ResourceRequirements{}
|
||||
|
||||
enforceThresholds := true
|
||||
resourcesLimitAnnotationKey := c.OpConfig.IgnoreResourcesLimitsAnnotationKey
|
||||
if resourcesLimitAnnotationKey != "" {
|
||||
if value, exists := c.ObjectMeta.Annotations[resourcesLimitAnnotationKey]; exists && value == "true" {
|
||||
enforceThresholds = false
|
||||
}
|
||||
}
|
||||
|
||||
if resources != nil {
|
||||
specRequests = resources.ResourceRequests
|
||||
specLimits = resources.ResourceLimits
|
||||
@@ -329,7 +337,7 @@ func (c *Cluster) generateResourceRequirements(
|
||||
}
|
||||
|
||||
// enforce minimum cpu and memory limits for Postgres containers only
|
||||
if containerName == constants.PostgresContainerName {
|
||||
if containerName == constants.PostgresContainerName && enforceThresholds {
|
||||
if err = c.enforceMinResourceLimits(&result); err != nil {
|
||||
return nil, fmt.Errorf("could not enforce minimum resource limits: %v", err)
|
||||
}
|
||||
@@ -344,7 +352,7 @@ func (c *Cluster) generateResourceRequirements(
|
||||
}
|
||||
|
||||
// enforce maximum cpu and memory requests for Postgres containers only
|
||||
if containerName == constants.PostgresContainerName {
|
||||
if containerName == constants.PostgresContainerName && enforceThresholds {
|
||||
if err = c.enforceMaxResourceRequests(&result); err != nil {
|
||||
return nil, fmt.Errorf("could not enforce maximum resource requests: %v", err)
|
||||
}
|
||||
@@ -412,7 +420,7 @@ PatroniInitDBParams:
|
||||
}
|
||||
|
||||
if patroni.MaximumLagOnFailover >= 0 {
|
||||
config.Bootstrap.DCS.MaximumLagOnFailover = patroni.MaximumLagOnFailover
|
||||
config.Bootstrap.DCS.MaximumLagOnFailover = float32(patroni.MaximumLagOnFailover)
|
||||
}
|
||||
if patroni.LoopWait != 0 {
|
||||
config.Bootstrap.DCS.LoopWait = patroni.LoopWait
|
||||
@@ -523,13 +531,14 @@ func (c *Cluster) nodeAffinity(nodeReadinessLabel map[string]string, nodeAffinit
|
||||
},
|
||||
}
|
||||
} else {
|
||||
if c.OpConfig.NodeReadinessLabelMerge == "OR" {
|
||||
switch c.OpConfig.NodeReadinessLabelMerge {
|
||||
case "OR":
|
||||
manifestTerms := nodeAffinityCopy.RequiredDuringSchedulingIgnoredDuringExecution.NodeSelectorTerms
|
||||
manifestTerms = append(manifestTerms, nodeReadinessSelectorTerm)
|
||||
nodeAffinityCopy.RequiredDuringSchedulingIgnoredDuringExecution = &v1.NodeSelector{
|
||||
NodeSelectorTerms: manifestTerms,
|
||||
}
|
||||
} else if c.OpConfig.NodeReadinessLabelMerge == "AND" {
|
||||
case "AND":
|
||||
for i, nodeSelectorTerm := range nodeAffinityCopy.RequiredDuringSchedulingIgnoredDuringExecution.NodeSelectorTerms {
|
||||
manifestExpressions := nodeSelectorTerm.MatchExpressions
|
||||
manifestExpressions = append(manifestExpressions, matchExpressions...)
|
||||
@@ -603,6 +612,13 @@ func generatePodAntiAffinity(podAffinityTerm v1.PodAffinityTerm, preferredDuring
|
||||
return podAntiAffinity
|
||||
}
|
||||
|
||||
func generateTopologySpreadConstraints(labels labels.Set, topologySpreadConstraints []v1.TopologySpreadConstraint) []v1.TopologySpreadConstraint {
|
||||
for _, topologySpreadConstraint := range topologySpreadConstraints {
|
||||
topologySpreadConstraint.LabelSelector = &metav1.LabelSelector{MatchLabels: labels}
|
||||
}
|
||||
return topologySpreadConstraints
|
||||
}
|
||||
|
||||
func tolerations(tolerationsSpec *[]v1.Toleration, podToleration map[string]string) []v1.Toleration {
|
||||
// allow to override tolerations by postgresql manifest
|
||||
if len(*tolerationsSpec) > 0 {
|
||||
@@ -808,10 +824,8 @@ func (c *Cluster) generatePodTemplate(
|
||||
initContainers []v1.Container,
|
||||
sidecarContainers []v1.Container,
|
||||
sharePgSocketWithSidecars *bool,
|
||||
topologySpreadConstraintsSpec []v1.TopologySpreadConstraint,
|
||||
tolerationsSpec *[]v1.Toleration,
|
||||
spiloRunAsUser *int64,
|
||||
spiloRunAsGroup *int64,
|
||||
spiloFSGroup *int64,
|
||||
nodeAffinity *v1.Affinity,
|
||||
schedulerName *string,
|
||||
terminateGracePeriod int64,
|
||||
@@ -830,18 +844,22 @@ func (c *Cluster) generatePodTemplate(
|
||||
terminateGracePeriodSeconds := terminateGracePeriod
|
||||
containers := []v1.Container{*spiloContainer}
|
||||
containers = append(containers, sidecarContainers...)
|
||||
securityContext := v1.PodSecurityContext{}
|
||||
|
||||
if spiloRunAsUser != nil {
|
||||
securityContext.RunAsUser = spiloRunAsUser
|
||||
securityContext := v1.PodSecurityContext{
|
||||
RunAsUser: c.OpConfig.Resources.SpiloRunAsUser,
|
||||
RunAsGroup: c.OpConfig.Resources.SpiloRunAsGroup,
|
||||
FSGroup: c.OpConfig.Resources.SpiloFSGroup,
|
||||
}
|
||||
|
||||
if spiloRunAsGroup != nil {
|
||||
securityContext.RunAsGroup = spiloRunAsGroup
|
||||
if c.Spec.SpiloRunAsUser != nil {
|
||||
securityContext.RunAsUser = c.Spec.SpiloRunAsUser
|
||||
}
|
||||
|
||||
if spiloFSGroup != nil {
|
||||
securityContext.FSGroup = spiloFSGroup
|
||||
if c.Spec.SpiloRunAsGroup != nil {
|
||||
securityContext.RunAsGroup = c.Spec.SpiloRunAsGroup
|
||||
}
|
||||
|
||||
if c.Spec.SpiloFSGroup != nil {
|
||||
securityContext.FSGroup = c.Spec.SpiloFSGroup
|
||||
}
|
||||
|
||||
podSpec := v1.PodSpec{
|
||||
@@ -877,6 +895,8 @@ func (c *Cluster) generatePodTemplate(
|
||||
podSpec.PriorityClassName = priorityClassName
|
||||
}
|
||||
|
||||
podSpec.TopologySpreadConstraints = generateTopologySpreadConstraints(labels, topologySpreadConstraintsSpec)
|
||||
|
||||
if sharePgSocketWithSidecars != nil && *sharePgSocketWithSidecars {
|
||||
addVarRunVolume(&podSpec)
|
||||
}
|
||||
@@ -1279,6 +1299,19 @@ func generateSpiloReadinessProbe() *v1.Probe {
|
||||
}
|
||||
}
|
||||
|
||||
func generateSpiloLivenessProbe(probe, defaultProbe *v1.Probe) *v1.Probe {
|
||||
|
||||
if probe != nil {
|
||||
return probe
|
||||
}
|
||||
|
||||
if defaultProbe != nil {
|
||||
return defaultProbe
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Cluster) generateStatefulSet(spec *acidv1.PostgresSpec) (*appsv1.StatefulSet, error) {
|
||||
|
||||
var (
|
||||
@@ -1297,11 +1330,14 @@ func (c *Cluster) generateStatefulSet(spec *acidv1.PostgresSpec) (*appsv1.Statef
|
||||
return nil, fmt.Errorf("could not generate resource requirements: %v", err)
|
||||
}
|
||||
|
||||
if spec.InitContainers != nil && len(spec.InitContainers) > 0 {
|
||||
if len(spec.InitContainers) > 0 {
|
||||
if c.OpConfig.EnableInitContainers != nil && !(*c.OpConfig.EnableInitContainers) {
|
||||
c.logger.Warningf("initContainers specified but disabled in configuration - next statefulset creation would fail")
|
||||
}
|
||||
initContainers = spec.InitContainers
|
||||
if err := c.validateContainers(initContainers); err != nil {
|
||||
return nil, fmt.Errorf("invalid init containers: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// backward compatible check for InitContainers
|
||||
@@ -1340,22 +1376,6 @@ func (c *Cluster) generateStatefulSet(spec *acidv1.PostgresSpec) (*appsv1.Statef
|
||||
// pickup the docker image for the spilo container
|
||||
effectiveDockerImage := util.Coalesce(spec.DockerImage, c.OpConfig.DockerImage)
|
||||
|
||||
// determine the User, Group and FSGroup for the spilo pod
|
||||
effectiveRunAsUser := c.OpConfig.Resources.SpiloRunAsUser
|
||||
if spec.SpiloRunAsUser != nil {
|
||||
effectiveRunAsUser = spec.SpiloRunAsUser
|
||||
}
|
||||
|
||||
effectiveRunAsGroup := c.OpConfig.Resources.SpiloRunAsGroup
|
||||
if spec.SpiloRunAsGroup != nil {
|
||||
effectiveRunAsGroup = spec.SpiloRunAsGroup
|
||||
}
|
||||
|
||||
effectiveFSGroup := c.OpConfig.Resources.SpiloFSGroup
|
||||
if spec.SpiloFSGroup != nil {
|
||||
effectiveFSGroup = spec.SpiloFSGroup
|
||||
}
|
||||
|
||||
volumeMounts := generateVolumeMounts(spec.Volume)
|
||||
|
||||
// configure TLS with a custom secret volume
|
||||
@@ -1398,9 +1418,11 @@ func (c *Cluster) generateStatefulSet(spec *acidv1.PostgresSpec) (*appsv1.Statef
|
||||
spiloContainer.ReadinessProbe = generateSpiloReadinessProbe()
|
||||
}
|
||||
|
||||
spiloContainer.LivenessProbe = generateSpiloLivenessProbe(spec.LivenessProbe, c.OpConfig.LivenessProbe)
|
||||
|
||||
// generate container specs for sidecars specified in the cluster manifest
|
||||
clusterSpecificSidecars := []v1.Container{}
|
||||
if spec.Sidecars != nil && len(spec.Sidecars) > 0 {
|
||||
if len(spec.Sidecars) > 0 {
|
||||
// warn if sidecars are defined, but globally disabled (does not apply to globally defined sidecars)
|
||||
if c.OpConfig.EnableSidecars != nil && !(*c.OpConfig.EnableSidecars) {
|
||||
c.logger.Warningf("sidecars specified but disabled in configuration - next statefulset creation would fail")
|
||||
@@ -1454,6 +1476,10 @@ func (c *Cluster) generateStatefulSet(spec *acidv1.PostgresSpec) (*appsv1.Statef
|
||||
|
||||
sidecarContainers = patchSidecarContainers(sidecarContainers, volumeMounts, c.OpConfig.SuperUsername, c.credentialSecretName(c.OpConfig.SuperUsername))
|
||||
|
||||
if err := c.validateContainers(sidecarContainers); err != nil {
|
||||
return nil, fmt.Errorf("invalid sidecar containers: %v", err)
|
||||
}
|
||||
|
||||
tolerationSpec := tolerations(&spec.Tolerations, c.OpConfig.PodToleration)
|
||||
effectivePodPriorityClassName := util.Coalesce(spec.PodPriorityClassName, c.OpConfig.PodPriorityClassName)
|
||||
|
||||
@@ -1468,10 +1494,8 @@ func (c *Cluster) generateStatefulSet(spec *acidv1.PostgresSpec) (*appsv1.Statef
|
||||
initContainers,
|
||||
sidecarContainers,
|
||||
c.OpConfig.SharePgSocketWithSidecars,
|
||||
spec.TopologySpreadConstraints,
|
||||
&tolerationSpec,
|
||||
effectiveRunAsUser,
|
||||
effectiveRunAsGroup,
|
||||
effectiveFSGroup,
|
||||
c.nodeAffinity(c.OpConfig.NodeReadinessLabel, spec.NodeAffinity),
|
||||
spec.SchedulerName,
|
||||
int64(c.OpConfig.PodTerminateGracePeriod.Seconds()),
|
||||
@@ -1504,11 +1528,12 @@ func (c *Cluster) generateStatefulSet(spec *acidv1.PostgresSpec) (*appsv1.Statef
|
||||
updateStrategy := appsv1.StatefulSetUpdateStrategy{Type: appsv1.OnDeleteStatefulSetStrategyType}
|
||||
|
||||
var podManagementPolicy appsv1.PodManagementPolicyType
|
||||
if c.OpConfig.PodManagementPolicy == "ordered_ready" {
|
||||
switch c.OpConfig.PodManagementPolicy {
|
||||
case "ordered_ready":
|
||||
podManagementPolicy = appsv1.OrderedReadyPodManagement
|
||||
} else if c.OpConfig.PodManagementPolicy == "parallel" {
|
||||
case "parallel":
|
||||
podManagementPolicy = appsv1.ParallelPodManagement
|
||||
} else {
|
||||
default:
|
||||
return nil, fmt.Errorf("could not set the pod management policy to the unknown value: %v", c.OpConfig.PodManagementPolicy)
|
||||
}
|
||||
|
||||
@@ -1674,7 +1699,7 @@ func (c *Cluster) getNumberOfInstances(spec *acidv1.PostgresSpec) int32 {
|
||||
}
|
||||
}
|
||||
|
||||
if spec.StandbyCluster != nil {
|
||||
if isStandbyCluster(spec) {
|
||||
if newcur == 1 {
|
||||
min = newcur
|
||||
max = newcur
|
||||
@@ -2030,11 +2055,6 @@ func (c *Cluster) generateServiceAnnotations(role PostgresRole, spec *acidv1.Pos
|
||||
if c.shouldCreateLoadBalancerForService(role, spec) {
|
||||
dnsName := c.dnsName(role)
|
||||
|
||||
// Just set ELB Timeout annotation with default value, if it does not
|
||||
// have a custom value
|
||||
if _, ok := annotations[constants.ElbTimeoutAnnotationName]; !ok {
|
||||
annotations[constants.ElbTimeoutAnnotationName] = constants.ElbTimeoutAnnotationValue
|
||||
}
|
||||
// External DNS name annotation is not customizable
|
||||
annotations[constants.ZalandoDNSNameAnnotation] = dnsName
|
||||
}
|
||||
@@ -2190,23 +2210,29 @@ func (c *Cluster) generateStandbyEnvironment(description *acidv1.StandbyDescript
|
||||
Value: description.StandbyPort,
|
||||
})
|
||||
}
|
||||
} else {
|
||||
c.logger.Info("standby cluster streaming from WAL location")
|
||||
if description.S3WalPath != "" {
|
||||
if description.StandbyPrimarySlotName != "" {
|
||||
result = append(result, v1.EnvVar{
|
||||
Name: "STANDBY_WALE_S3_PREFIX",
|
||||
Value: description.S3WalPath,
|
||||
Name: "STANDBY_PRIMARY_SLOT_NAME",
|
||||
Value: description.StandbyPrimarySlotName,
|
||||
})
|
||||
} else if description.GSWalPath != "" {
|
||||
result = append(result, v1.EnvVar{
|
||||
Name: "STANDBY_WALE_GS_PREFIX",
|
||||
Value: description.GSWalPath,
|
||||
})
|
||||
} else {
|
||||
c.logger.Error("no WAL path specified in standby section")
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
// WAL archive can be specified with or without standby_host
|
||||
if description.S3WalPath != "" {
|
||||
c.logger.Info("standby cluster using S3 WAL archive")
|
||||
result = append(result, v1.EnvVar{
|
||||
Name: "STANDBY_WALE_S3_PREFIX",
|
||||
Value: description.S3WalPath,
|
||||
})
|
||||
result = append(result, v1.EnvVar{Name: "STANDBY_METHOD", Value: "STANDBY_WITH_WALE"})
|
||||
result = append(result, v1.EnvVar{Name: "STANDBY_WAL_BUCKET_SCOPE_PREFIX", Value: ""})
|
||||
} else if description.GSWalPath != "" {
|
||||
c.logger.Info("standby cluster using GCS WAL archive")
|
||||
result = append(result, v1.EnvVar{
|
||||
Name: "STANDBY_WALE_GS_PREFIX",
|
||||
Value: description.GSWalPath,
|
||||
})
|
||||
result = append(result, v1.EnvVar{Name: "STANDBY_METHOD", Value: "STANDBY_WITH_WALE"})
|
||||
result = append(result, v1.EnvVar{Name: "STANDBY_WAL_BUCKET_SCOPE_PREFIX", Value: ""})
|
||||
}
|
||||
@@ -2346,6 +2372,8 @@ func (c *Cluster) generateLogicalBackupJob() (*batchv1.CronJob, error) {
|
||||
|
||||
tolerationsSpec := tolerations(&spec.Tolerations, c.OpConfig.PodToleration)
|
||||
|
||||
topologySpreadConstraintsSpec := generateTopologySpreadConstraints(labels, spec.TopologySpreadConstraints)
|
||||
|
||||
// re-use the method that generates DB pod templates
|
||||
if podTemplate, err = c.generatePodTemplate(
|
||||
c.Namespace,
|
||||
@@ -2355,10 +2383,8 @@ func (c *Cluster) generateLogicalBackupJob() (*batchv1.CronJob, error) {
|
||||
[]v1.Container{},
|
||||
[]v1.Container{},
|
||||
util.False(),
|
||||
topologySpreadConstraintsSpec,
|
||||
&tolerationsSpec,
|
||||
nil,
|
||||
nil,
|
||||
nil,
|
||||
c.nodeAffinity(c.OpConfig.NodeReadinessLabel, nil),
|
||||
nil,
|
||||
int64(c.OpConfig.PodTerminateGracePeriod.Seconds()),
|
||||
@@ -2590,3 +2616,15 @@ func ensurePath(file string, defaultDir string, defaultFile string) string {
|
||||
}
|
||||
return file
|
||||
}
|
||||
|
||||
func (c *Cluster) validateContainers(containers []v1.Container) error {
|
||||
for i, container := range containers {
|
||||
if container.Name == "" {
|
||||
return fmt.Errorf("container[%d]: name is required", i)
|
||||
}
|
||||
if container.Image == "" {
|
||||
return fmt.Errorf("container '%v': image is required", container.Name)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
+372
-31
@@ -72,18 +72,18 @@ func TestGenerateSpiloJSONConfiguration(t *testing.T) {
|
||||
}{
|
||||
{
|
||||
subtest: "Patroni default configuration",
|
||||
pgParam: &acidv1.PostgresqlParam{PgVersion: "17"},
|
||||
pgParam: &acidv1.PostgresqlParam{PgVersion: "18"},
|
||||
patroni: &acidv1.Patroni{},
|
||||
opConfig: &config.Config{
|
||||
Auth: config.Auth{
|
||||
PamRoleName: "zalandos",
|
||||
},
|
||||
},
|
||||
result: `{"postgresql":{"bin_dir":"/usr/lib/postgresql/17/bin"},"bootstrap":{"initdb":[{"auth-host":"scram-sha-256"},{"auth-local":"trust"}],"dcs":{}}}`,
|
||||
result: `{"postgresql":{"bin_dir":"/usr/lib/postgresql/18/bin"},"bootstrap":{"initdb":[{"auth-host":"scram-sha-256"},{"auth-local":"trust"}],"dcs":{}}}`,
|
||||
},
|
||||
{
|
||||
subtest: "Patroni configured",
|
||||
pgParam: &acidv1.PostgresqlParam{PgVersion: "17"},
|
||||
pgParam: &acidv1.PostgresqlParam{PgVersion: "18"},
|
||||
patroni: &acidv1.Patroni{
|
||||
InitDB: map[string]string{
|
||||
"encoding": "UTF8",
|
||||
@@ -102,38 +102,38 @@ func TestGenerateSpiloJSONConfiguration(t *testing.T) {
|
||||
FailsafeMode: util.True(),
|
||||
},
|
||||
opConfig: &config.Config{},
|
||||
result: `{"postgresql":{"bin_dir":"/usr/lib/postgresql/17/bin","pg_hba":["hostssl all all 0.0.0.0/0 scram-sha-256","host all all 0.0.0.0/0 scram-sha-256"]},"bootstrap":{"initdb":[{"auth-host":"scram-sha-256"},{"auth-local":"trust"},"data-checksums",{"encoding":"UTF8"},{"locale":"en_US.UTF-8"}],"dcs":{"ttl":30,"loop_wait":10,"retry_timeout":10,"maximum_lag_on_failover":33554432,"synchronous_mode":true,"synchronous_mode_strict":true,"synchronous_node_count":1,"slots":{"permanent_logical_1":{"database":"foo","plugin":"pgoutput","type":"logical"}},"failsafe_mode":true}}}`,
|
||||
result: `{"postgresql":{"bin_dir":"/usr/lib/postgresql/18/bin","pg_hba":["hostssl all all 0.0.0.0/0 scram-sha-256","host all all 0.0.0.0/0 scram-sha-256"]},"bootstrap":{"initdb":[{"auth-host":"scram-sha-256"},{"auth-local":"trust"},"data-checksums",{"encoding":"UTF8"},{"locale":"en_US.UTF-8"}],"dcs":{"ttl":30,"loop_wait":10,"retry_timeout":10,"maximum_lag_on_failover":33554432,"synchronous_mode":true,"synchronous_mode_strict":true,"synchronous_node_count":1,"slots":{"permanent_logical_1":{"database":"foo","plugin":"pgoutput","type":"logical"}},"failsafe_mode":true}}}`,
|
||||
},
|
||||
{
|
||||
subtest: "Patroni failsafe_mode configured globally",
|
||||
pgParam: &acidv1.PostgresqlParam{PgVersion: "17"},
|
||||
pgParam: &acidv1.PostgresqlParam{PgVersion: "18"},
|
||||
patroni: &acidv1.Patroni{},
|
||||
opConfig: &config.Config{
|
||||
EnablePatroniFailsafeMode: util.True(),
|
||||
},
|
||||
result: `{"postgresql":{"bin_dir":"/usr/lib/postgresql/17/bin"},"bootstrap":{"initdb":[{"auth-host":"scram-sha-256"},{"auth-local":"trust"}],"dcs":{"failsafe_mode":true}}}`,
|
||||
result: `{"postgresql":{"bin_dir":"/usr/lib/postgresql/18/bin"},"bootstrap":{"initdb":[{"auth-host":"scram-sha-256"},{"auth-local":"trust"}],"dcs":{"failsafe_mode":true}}}`,
|
||||
},
|
||||
{
|
||||
subtest: "Patroni failsafe_mode configured globally, disabled for cluster",
|
||||
pgParam: &acidv1.PostgresqlParam{PgVersion: "17"},
|
||||
pgParam: &acidv1.PostgresqlParam{PgVersion: "18"},
|
||||
patroni: &acidv1.Patroni{
|
||||
FailsafeMode: util.False(),
|
||||
},
|
||||
opConfig: &config.Config{
|
||||
EnablePatroniFailsafeMode: util.True(),
|
||||
},
|
||||
result: `{"postgresql":{"bin_dir":"/usr/lib/postgresql/17/bin"},"bootstrap":{"initdb":[{"auth-host":"scram-sha-256"},{"auth-local":"trust"}],"dcs":{"failsafe_mode":false}}}`,
|
||||
result: `{"postgresql":{"bin_dir":"/usr/lib/postgresql/18/bin"},"bootstrap":{"initdb":[{"auth-host":"scram-sha-256"},{"auth-local":"trust"}],"dcs":{"failsafe_mode":false}}}`,
|
||||
},
|
||||
{
|
||||
subtest: "Patroni failsafe_mode disabled globally, configured for cluster",
|
||||
pgParam: &acidv1.PostgresqlParam{PgVersion: "17"},
|
||||
pgParam: &acidv1.PostgresqlParam{PgVersion: "18"},
|
||||
patroni: &acidv1.Patroni{
|
||||
FailsafeMode: util.True(),
|
||||
},
|
||||
opConfig: &config.Config{
|
||||
EnablePatroniFailsafeMode: util.False(),
|
||||
},
|
||||
result: `{"postgresql":{"bin_dir":"/usr/lib/postgresql/17/bin"},"bootstrap":{"initdb":[{"auth-host":"scram-sha-256"},{"auth-local":"trust"}],"dcs":{"failsafe_mode":true}}}`,
|
||||
result: `{"postgresql":{"bin_dir":"/usr/lib/postgresql/18/bin"},"bootstrap":{"initdb":[{"auth-host":"scram-sha-256"},{"auth-local":"trust"}],"dcs":{"failsafe_mode":true}}}`,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
@@ -164,15 +164,15 @@ func TestExtractPgVersionFromBinPath(t *testing.T) {
|
||||
},
|
||||
{
|
||||
subTest: "test current bin path against hard coded template",
|
||||
binPath: "/usr/lib/postgresql/17/bin",
|
||||
binPath: "/usr/lib/postgresql/18/bin",
|
||||
template: pgBinariesLocationTemplate,
|
||||
expected: "17",
|
||||
expected: "18",
|
||||
},
|
||||
{
|
||||
subTest: "test alternative bin path against a matching template",
|
||||
binPath: "/usr/pgsql-17/bin",
|
||||
binPath: "/usr/pgsql-18/bin",
|
||||
template: "/usr/pgsql-%v/bin",
|
||||
expected: "17",
|
||||
expected: "18",
|
||||
},
|
||||
}
|
||||
|
||||
@@ -1370,7 +1370,33 @@ func TestStandbyEnv(t *testing.T) {
|
||||
envLen: 2,
|
||||
},
|
||||
{
|
||||
subTest: "from remote primary - ignore WAL path",
|
||||
subTest: "from remote primary with S3 WAL path",
|
||||
standbyOpts: &acidv1.StandbyDescription{
|
||||
S3WalPath: "s3://some/path/",
|
||||
StandbyHost: "remote-primary",
|
||||
},
|
||||
env: v1.EnvVar{
|
||||
Name: "STANDBY_HOST",
|
||||
Value: "remote-primary",
|
||||
},
|
||||
envPos: 0,
|
||||
envLen: 4,
|
||||
},
|
||||
{
|
||||
subTest: "verify S3 WAL env with standby host",
|
||||
standbyOpts: &acidv1.StandbyDescription{
|
||||
S3WalPath: "s3://some/path/",
|
||||
StandbyHost: "remote-primary",
|
||||
},
|
||||
env: v1.EnvVar{
|
||||
Name: "STANDBY_WALE_S3_PREFIX",
|
||||
Value: "s3://some/path/",
|
||||
},
|
||||
envPos: 1,
|
||||
envLen: 4,
|
||||
},
|
||||
{
|
||||
subTest: "from remote primary with GCS WAL path",
|
||||
standbyOpts: &acidv1.StandbyDescription{
|
||||
GSWalPath: "gs://some/path/",
|
||||
StandbyHost: "remote-primary",
|
||||
@@ -1380,7 +1406,20 @@ func TestStandbyEnv(t *testing.T) {
|
||||
Value: "remote-primary",
|
||||
},
|
||||
envPos: 0,
|
||||
envLen: 1,
|
||||
envLen: 4,
|
||||
},
|
||||
{
|
||||
subTest: "from remote primary with slot name",
|
||||
standbyOpts: &acidv1.StandbyDescription{
|
||||
StandbyHost: "remote-primary",
|
||||
StandbyPrimarySlotName: "my_slot",
|
||||
},
|
||||
env: v1.EnvVar{
|
||||
Name: "STANDBY_PRIMARY_SLOT_NAME",
|
||||
Value: "my_slot",
|
||||
},
|
||||
envPos: 1,
|
||||
envLen: 2,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -1935,7 +1974,8 @@ func TestAdditionalVolume(t *testing.T) {
|
||||
AdditionalVolumes: additionalVolumes,
|
||||
Sidecars: []acidv1.Sidecar{
|
||||
{
|
||||
Name: sidecarName,
|
||||
Name: sidecarName,
|
||||
DockerImage: "test-image",
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -2148,7 +2188,7 @@ func TestSidecars(t *testing.T) {
|
||||
|
||||
spec = acidv1.PostgresSpec{
|
||||
PostgresqlParam: acidv1.PostgresqlParam{
|
||||
PgVersion: "17",
|
||||
PgVersion: "18",
|
||||
Parameters: map[string]string{
|
||||
"max_connections": "100",
|
||||
},
|
||||
@@ -2163,10 +2203,12 @@ func TestSidecars(t *testing.T) {
|
||||
},
|
||||
Sidecars: []acidv1.Sidecar{
|
||||
{
|
||||
Name: "cluster-specific-sidecar",
|
||||
Name: "cluster-specific-sidecar",
|
||||
DockerImage: "test-image",
|
||||
},
|
||||
{
|
||||
Name: "cluster-specific-sidecar-with-resources",
|
||||
Name: "cluster-specific-sidecar-with-resources",
|
||||
DockerImage: "test-image",
|
||||
Resources: &acidv1.Resources{
|
||||
ResourceRequests: acidv1.ResourceDescription{CPU: k8sutil.StringToPointer("210m"), Memory: k8sutil.StringToPointer("0.8Gi")},
|
||||
ResourceLimits: acidv1.ResourceDescription{CPU: k8sutil.StringToPointer("510m"), Memory: k8sutil.StringToPointer("1.4Gi")},
|
||||
@@ -2201,7 +2243,8 @@ func TestSidecars(t *testing.T) {
|
||||
},
|
||||
SidecarContainers: []v1.Container{
|
||||
{
|
||||
Name: "global-sidecar",
|
||||
Name: "global-sidecar",
|
||||
Image: "test-image",
|
||||
},
|
||||
// will be replaced by a cluster specific sidecar with the same name
|
||||
{
|
||||
@@ -2271,6 +2314,7 @@ func TestSidecars(t *testing.T) {
|
||||
// cluster specific sidecar
|
||||
assert.Contains(t, s.Spec.Template.Spec.Containers, v1.Container{
|
||||
Name: "cluster-specific-sidecar",
|
||||
Image: "test-image",
|
||||
Env: env,
|
||||
Resources: generateKubernetesResources("200m", "500m", "0.7Gi", "1.3Gi"),
|
||||
ImagePullPolicy: v1.PullIfNotPresent,
|
||||
@@ -2297,6 +2341,7 @@ func TestSidecars(t *testing.T) {
|
||||
// global sidecar
|
||||
assert.Contains(t, s.Spec.Template.Spec.Containers, v1.Container{
|
||||
Name: "global-sidecar",
|
||||
Image: "test-image",
|
||||
Env: env,
|
||||
VolumeMounts: mounts,
|
||||
})
|
||||
@@ -2325,6 +2370,180 @@ func TestSidecars(t *testing.T) {
|
||||
|
||||
}
|
||||
|
||||
func TestContainerValidation(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
spec acidv1.PostgresSpec
|
||||
clusterConfig Config
|
||||
expectedError string
|
||||
}{
|
||||
{
|
||||
name: "init container without image",
|
||||
spec: acidv1.PostgresSpec{
|
||||
PostgresqlParam: acidv1.PostgresqlParam{
|
||||
PgVersion: "18",
|
||||
},
|
||||
TeamID: "myapp",
|
||||
NumberOfInstances: 1,
|
||||
Volume: acidv1.Volume{
|
||||
Size: "1G",
|
||||
},
|
||||
InitContainers: []v1.Container{
|
||||
{
|
||||
Name: "invalid-initcontainer",
|
||||
},
|
||||
},
|
||||
},
|
||||
clusterConfig: Config{
|
||||
OpConfig: config.Config{
|
||||
PodManagementPolicy: "ordered_ready",
|
||||
ProtectedRoles: []string{"admin"},
|
||||
Auth: config.Auth{
|
||||
SuperUsername: superUserName,
|
||||
ReplicationUsername: replicationUserName,
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedError: "image is required",
|
||||
},
|
||||
{
|
||||
name: "sidecar without name",
|
||||
spec: acidv1.PostgresSpec{
|
||||
PostgresqlParam: acidv1.PostgresqlParam{
|
||||
PgVersion: "18",
|
||||
},
|
||||
TeamID: "myapp",
|
||||
NumberOfInstances: 1,
|
||||
Volume: acidv1.Volume{
|
||||
Size: "1G",
|
||||
},
|
||||
},
|
||||
clusterConfig: Config{
|
||||
OpConfig: config.Config{
|
||||
PodManagementPolicy: "ordered_ready",
|
||||
ProtectedRoles: []string{"admin"},
|
||||
Auth: config.Auth{
|
||||
SuperUsername: superUserName,
|
||||
ReplicationUsername: replicationUserName,
|
||||
},
|
||||
SidecarContainers: []v1.Container{
|
||||
{
|
||||
Image: "test-image",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedError: "name is required",
|
||||
},
|
||||
{
|
||||
name: "sidecar without image",
|
||||
spec: acidv1.PostgresSpec{
|
||||
PostgresqlParam: acidv1.PostgresqlParam{
|
||||
PgVersion: "18",
|
||||
},
|
||||
TeamID: "myapp",
|
||||
NumberOfInstances: 1,
|
||||
Volume: acidv1.Volume{
|
||||
Size: "1G",
|
||||
},
|
||||
Sidecars: []acidv1.Sidecar{
|
||||
{
|
||||
Name: "invalid-sidecar",
|
||||
},
|
||||
},
|
||||
},
|
||||
clusterConfig: Config{
|
||||
OpConfig: config.Config{
|
||||
PodManagementPolicy: "ordered_ready",
|
||||
ProtectedRoles: []string{"admin"},
|
||||
Auth: config.Auth{
|
||||
SuperUsername: superUserName,
|
||||
ReplicationUsername: replicationUserName,
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedError: "image is required",
|
||||
},
|
||||
{
|
||||
name: "valid containers pass validation",
|
||||
spec: acidv1.PostgresSpec{
|
||||
PostgresqlParam: acidv1.PostgresqlParam{
|
||||
PgVersion: "18",
|
||||
},
|
||||
TeamID: "myapp",
|
||||
NumberOfInstances: 1,
|
||||
Volume: acidv1.Volume{
|
||||
Size: "1G",
|
||||
},
|
||||
Sidecars: []acidv1.Sidecar{
|
||||
{
|
||||
Name: "valid-sidecar",
|
||||
DockerImage: "busybox:latest",
|
||||
},
|
||||
},
|
||||
InitContainers: []v1.Container{
|
||||
{
|
||||
Name: "valid-initcontainer",
|
||||
Image: "alpine:latest",
|
||||
},
|
||||
},
|
||||
},
|
||||
clusterConfig: Config{
|
||||
OpConfig: config.Config{
|
||||
PodManagementPolicy: "ordered_ready",
|
||||
ProtectedRoles: []string{"admin"},
|
||||
Auth: config.Auth{
|
||||
SuperUsername: superUserName,
|
||||
ReplicationUsername: replicationUserName,
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedError: "",
|
||||
},
|
||||
{
|
||||
name: "multiple invalid sidecars",
|
||||
spec: acidv1.PostgresSpec{
|
||||
Sidecars: []acidv1.Sidecar{
|
||||
{
|
||||
Name: "sidecar1",
|
||||
},
|
||||
{
|
||||
Name: "sidecar2",
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedError: "image is required",
|
||||
},
|
||||
{
|
||||
name: "empty container name and image",
|
||||
spec: acidv1.PostgresSpec{
|
||||
InitContainers: []v1.Container{
|
||||
{
|
||||
Name: "",
|
||||
Image: "",
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedError: "name is required",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
cluster := New(tc.clusterConfig, k8sutil.KubernetesClient{}, acidv1.Postgresql{}, logger, eventRecorder)
|
||||
|
||||
_, err := cluster.generateStatefulSet(&tc.spec)
|
||||
|
||||
if tc.expectedError != "" {
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), tc.expectedError)
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGeneratePodDisruptionBudget(t *testing.T) {
|
||||
testName := "Test PodDisruptionBudget spec generation"
|
||||
|
||||
@@ -2618,7 +2837,8 @@ func TestGenerateService(t *testing.T) {
|
||||
Name: "cluster-specific-sidecar",
|
||||
},
|
||||
{
|
||||
Name: "cluster-specific-sidecar-with-resources",
|
||||
Name: "cluster-specific-sidecar-with-resources",
|
||||
DockerImage: "test-image",
|
||||
Resources: &acidv1.Resources{
|
||||
ResourceRequests: acidv1.ResourceDescription{CPU: k8sutil.StringToPointer("210m"), Memory: k8sutil.StringToPointer("0.8Gi")},
|
||||
ResourceLimits: acidv1.ResourceDescription{CPU: k8sutil.StringToPointer("510m"), Memory: k8sutil.StringToPointer("1.4Gi")},
|
||||
@@ -2747,6 +2967,7 @@ func newLBFakeClient() (k8sutil.KubernetesClient, *fake.Clientset) {
|
||||
DeploymentsGetter: clientSet.AppsV1(),
|
||||
PodsGetter: clientSet.CoreV1(),
|
||||
ServicesGetter: clientSet.CoreV1(),
|
||||
SecretsGetter: clientSet.CoreV1(),
|
||||
}, clientSet
|
||||
}
|
||||
|
||||
@@ -2928,6 +3149,7 @@ func TestGenerateResourceRequirements(t *testing.T) {
|
||||
namespace := "default"
|
||||
clusterNameLabel := "cluster-name"
|
||||
sidecarName := "postgres-exporter"
|
||||
dockerImage := "test-image"
|
||||
|
||||
// enforceMinResourceLimits will be called 2 times emitting 4 events (2x cpu, 2x memory raise)
|
||||
// enforceMaxResourceRequests will be called 4 times emitting 6 events (2x cpu, 4x memory cap)
|
||||
@@ -2948,6 +3170,9 @@ func TestGenerateResourceRequirements(t *testing.T) {
|
||||
PodRoleLabel: "spilo-role",
|
||||
}
|
||||
|
||||
configWithEnabledIgnoreResourcesLimits := configResources
|
||||
configWithEnabledIgnoreResourcesLimits.IgnoreResourcesLimitsAnnotationKey = "zalando.org/ignore-resources-limits"
|
||||
|
||||
tests := []struct {
|
||||
subTest string
|
||||
config config.Config
|
||||
@@ -2993,7 +3218,8 @@ func TestGenerateResourceRequirements(t *testing.T) {
|
||||
Spec: acidv1.PostgresSpec{
|
||||
Sidecars: []acidv1.Sidecar{
|
||||
{
|
||||
Name: sidecarName,
|
||||
Name: sidecarName,
|
||||
DockerImage: dockerImage,
|
||||
},
|
||||
},
|
||||
TeamID: "acid",
|
||||
@@ -3232,7 +3458,8 @@ func TestGenerateResourceRequirements(t *testing.T) {
|
||||
Spec: acidv1.PostgresSpec{
|
||||
Sidecars: []acidv1.Sidecar{
|
||||
{
|
||||
Name: sidecarName,
|
||||
Name: sidecarName,
|
||||
DockerImage: dockerImage,
|
||||
Resources: &acidv1.Resources{
|
||||
ResourceRequests: acidv1.ResourceDescription{CPU: k8sutil.StringToPointer("10m"), Memory: k8sutil.StringToPointer("10Mi")},
|
||||
ResourceLimits: acidv1.ResourceDescription{CPU: k8sutil.StringToPointer("100m"), Memory: k8sutil.StringToPointer("100Mi")},
|
||||
@@ -3281,14 +3508,15 @@ func TestGenerateResourceRequirements(t *testing.T) {
|
||||
{
|
||||
subTest: "test enforcing min cpu and memory limit",
|
||||
config: config.Config{
|
||||
Resources: configResources,
|
||||
Resources: configWithEnabledIgnoreResourcesLimits,
|
||||
PodManagementPolicy: "ordered_ready",
|
||||
SetMemoryRequestToLimit: false,
|
||||
},
|
||||
pgSpec: acidv1.Postgresql{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: clusterName,
|
||||
Namespace: namespace,
|
||||
Name: clusterName,
|
||||
Namespace: namespace,
|
||||
Annotations: map[string]string{"zalando.org/ignore-resources-limits": "false"},
|
||||
},
|
||||
Spec: acidv1.PostgresSpec{
|
||||
Resources: &acidv1.Resources{
|
||||
@@ -3306,6 +3534,35 @@ func TestGenerateResourceRequirements(t *testing.T) {
|
||||
ResourceLimits: acidv1.ResourceDescription{CPU: k8sutil.StringToPointer("250m"), Memory: k8sutil.StringToPointer("250Mi")},
|
||||
},
|
||||
},
|
||||
{
|
||||
subTest: "ingnore min cpu and memory limit threshold",
|
||||
config: config.Config{
|
||||
Resources: configWithEnabledIgnoreResourcesLimits,
|
||||
PodManagementPolicy: "ordered_ready",
|
||||
SetMemoryRequestToLimit: false,
|
||||
},
|
||||
pgSpec: acidv1.Postgresql{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: clusterName,
|
||||
Namespace: namespace,
|
||||
Annotations: map[string]string{"zalando.org/ignore-resources-limits": "true"},
|
||||
},
|
||||
Spec: acidv1.PostgresSpec{
|
||||
Resources: &acidv1.Resources{
|
||||
ResourceRequests: acidv1.ResourceDescription{CPU: k8sutil.StringToPointer("100m"), Memory: k8sutil.StringToPointer("100Mi")},
|
||||
ResourceLimits: acidv1.ResourceDescription{CPU: k8sutil.StringToPointer("200m"), Memory: k8sutil.StringToPointer("200Mi")},
|
||||
},
|
||||
TeamID: "acid",
|
||||
Volume: acidv1.Volume{
|
||||
Size: "1G",
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedResources: acidv1.Resources{
|
||||
ResourceRequests: acidv1.ResourceDescription{CPU: k8sutil.StringToPointer("100m"), Memory: k8sutil.StringToPointer("100Mi")},
|
||||
ResourceLimits: acidv1.ResourceDescription{CPU: k8sutil.StringToPointer("200m"), Memory: k8sutil.StringToPointer("200Mi")},
|
||||
},
|
||||
},
|
||||
{
|
||||
subTest: "test min cpu and memory limit are not enforced on sidecar",
|
||||
config: config.Config{
|
||||
@@ -3321,7 +3578,8 @@ func TestGenerateResourceRequirements(t *testing.T) {
|
||||
Spec: acidv1.PostgresSpec{
|
||||
Sidecars: []acidv1.Sidecar{
|
||||
{
|
||||
Name: sidecarName,
|
||||
Name: sidecarName,
|
||||
DockerImage: dockerImage,
|
||||
Resources: &acidv1.Resources{
|
||||
ResourceRequests: acidv1.ResourceDescription{CPU: k8sutil.StringToPointer("10m"), Memory: k8sutil.StringToPointer("10Mi")},
|
||||
ResourceLimits: acidv1.ResourceDescription{CPU: k8sutil.StringToPointer("100m"), Memory: k8sutil.StringToPointer("100Mi")},
|
||||
@@ -3342,14 +3600,15 @@ func TestGenerateResourceRequirements(t *testing.T) {
|
||||
{
|
||||
subTest: "test enforcing max cpu and memory requests",
|
||||
config: config.Config{
|
||||
Resources: configResources,
|
||||
Resources: configWithEnabledIgnoreResourcesLimits,
|
||||
PodManagementPolicy: "ordered_ready",
|
||||
SetMemoryRequestToLimit: false,
|
||||
},
|
||||
pgSpec: acidv1.Postgresql{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: clusterName,
|
||||
Namespace: namespace,
|
||||
Name: clusterName,
|
||||
Namespace: namespace,
|
||||
Annotations: map[string]string{"zalando.org/ignore-resources-limits": "yes"},
|
||||
},
|
||||
Spec: acidv1.PostgresSpec{
|
||||
Resources: &acidv1.Resources{
|
||||
@@ -3367,6 +3626,35 @@ func TestGenerateResourceRequirements(t *testing.T) {
|
||||
ResourceLimits: acidv1.ResourceDescription{CPU: k8sutil.StringToPointer("2"), Memory: k8sutil.StringToPointer("4Gi")},
|
||||
},
|
||||
},
|
||||
{
|
||||
subTest: "ignore max cpu and memory requests limit",
|
||||
config: config.Config{
|
||||
Resources: configWithEnabledIgnoreResourcesLimits,
|
||||
PodManagementPolicy: "ordered_ready",
|
||||
SetMemoryRequestToLimit: false,
|
||||
},
|
||||
pgSpec: acidv1.Postgresql{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: clusterName,
|
||||
Namespace: namespace,
|
||||
Annotations: map[string]string{"zalando.org/ignore-resources-limits": "true"},
|
||||
},
|
||||
Spec: acidv1.PostgresSpec{
|
||||
Resources: &acidv1.Resources{
|
||||
ResourceRequests: acidv1.ResourceDescription{CPU: k8sutil.StringToPointer("1"), Memory: k8sutil.StringToPointer("2Gi")},
|
||||
ResourceLimits: acidv1.ResourceDescription{CPU: k8sutil.StringToPointer("2"), Memory: k8sutil.StringToPointer("4Gi")},
|
||||
},
|
||||
TeamID: "acid",
|
||||
Volume: acidv1.Volume{
|
||||
Size: "1G",
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedResources: acidv1.Resources{
|
||||
ResourceRequests: acidv1.ResourceDescription{CPU: k8sutil.StringToPointer("1"), Memory: k8sutil.StringToPointer("2Gi")},
|
||||
ResourceLimits: acidv1.ResourceDescription{CPU: k8sutil.StringToPointer("2"), Memory: k8sutil.StringToPointer("4Gi")},
|
||||
},
|
||||
},
|
||||
{
|
||||
subTest: "test SetMemoryRequestToLimit flag but raise only until max memory request",
|
||||
config: config.Config{
|
||||
@@ -3984,3 +4272,56 @@ func TestGenerateCapabilities(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestTopologySpreadConstraints(t *testing.T) {
|
||||
clusterName := "acid-test-cluster"
|
||||
namespace := "default"
|
||||
labelSelector := &metav1.LabelSelector{
|
||||
MatchLabels: cluster.labelsSet(true),
|
||||
}
|
||||
|
||||
pg := acidv1.Postgresql{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: clusterName,
|
||||
Namespace: namespace,
|
||||
},
|
||||
Spec: acidv1.PostgresSpec{
|
||||
NumberOfInstances: 1,
|
||||
Resources: &acidv1.Resources{
|
||||
ResourceRequests: acidv1.ResourceDescription{CPU: k8sutil.StringToPointer("1"), Memory: k8sutil.StringToPointer("10")},
|
||||
ResourceLimits: acidv1.ResourceDescription{CPU: k8sutil.StringToPointer("1"), Memory: k8sutil.StringToPointer("10")},
|
||||
},
|
||||
Volume: acidv1.Volume{
|
||||
Size: "1G",
|
||||
},
|
||||
TopologySpreadConstraints: []v1.TopologySpreadConstraint{
|
||||
{
|
||||
MaxSkew: 1,
|
||||
TopologyKey: "topology.kubernetes.io/zone",
|
||||
WhenUnsatisfiable: v1.DoNotSchedule,
|
||||
LabelSelector: labelSelector,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
cluster := New(
|
||||
Config{
|
||||
OpConfig: config.Config{
|
||||
PodManagementPolicy: "ordered_ready",
|
||||
},
|
||||
}, k8sutil.KubernetesClient{}, acidv1.Postgresql{}, logger, eventRecorder)
|
||||
cluster.Name = clusterName
|
||||
cluster.Namespace = namespace
|
||||
cluster.labelsSet(true)
|
||||
|
||||
s, err := cluster.generateStatefulSet(&pg.Spec)
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, s.Spec.Template.Spec.TopologySpreadConstraints, v1.TopologySpreadConstraint{
|
||||
MaxSkew: int32(1),
|
||||
TopologyKey: "topology.kubernetes.io/zone",
|
||||
WhenUnsatisfiable: v1.DoNotSchedule,
|
||||
LabelSelector: labelSelector,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/Masterminds/semver"
|
||||
@@ -14,16 +15,6 @@ import (
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
)
|
||||
|
||||
// VersionMap Map of version numbers
|
||||
var VersionMap = map[string]int{
|
||||
"12": 120000,
|
||||
"13": 130000,
|
||||
"14": 140000,
|
||||
"15": 150000,
|
||||
"16": 160000,
|
||||
"17": 170000,
|
||||
}
|
||||
|
||||
const (
|
||||
majorVersionUpgradeSuccessAnnotation = "last-major-upgrade-success"
|
||||
majorVersionUpgradeFailureAnnotation = "last-major-upgrade-failure"
|
||||
@@ -31,21 +22,22 @@ const (
|
||||
|
||||
// IsBiggerPostgresVersion Compare two Postgres version numbers
|
||||
func IsBiggerPostgresVersion(old string, new string) bool {
|
||||
oldN := VersionMap[old]
|
||||
newN := VersionMap[new]
|
||||
oldN, _ := strconv.Atoi(old)
|
||||
newN, _ := strconv.Atoi(new)
|
||||
return newN > oldN
|
||||
}
|
||||
|
||||
// GetDesiredMajorVersionAsInt Convert string to comparable integer of PG version
|
||||
func (c *Cluster) GetDesiredMajorVersionAsInt() int {
|
||||
return VersionMap[c.GetDesiredMajorVersion()]
|
||||
version, _ := strconv.Atoi(c.GetDesiredMajorVersion())
|
||||
return version * 10000
|
||||
}
|
||||
|
||||
// GetDesiredMajorVersion returns major version to use, incl. potential auto upgrade
|
||||
func (c *Cluster) GetDesiredMajorVersion() string {
|
||||
|
||||
if c.Config.OpConfig.MajorVersionUpgradeMode == "full" {
|
||||
// e.g. current is 13, minimal is 13 allowing 13 to 17 clusters, everything below is upgraded
|
||||
// e.g. current is 14, minimal is 14 allowing 14 to 18 clusters, everything below is upgraded
|
||||
if IsBiggerPostgresVersion(c.Spec.PgVersion, c.Config.OpConfig.MinimalMajorVersion) {
|
||||
c.logger.Infof("overwriting configured major version %s to %s", c.Spec.PgVersion, c.Config.OpConfig.TargetMajorVersion)
|
||||
return c.Config.OpConfig.TargetMajorVersion
|
||||
@@ -198,7 +190,7 @@ func (c *Cluster) majorVersionUpgrade() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
if !isInMaintenanceWindow(c.Spec.MaintenanceWindows) {
|
||||
if !c.isInMaintenanceWindow(c.Spec.MaintenanceWindows) {
|
||||
c.logger.Infof("skipping major version upgrade, not in maintenance window")
|
||||
return nil
|
||||
}
|
||||
@@ -237,7 +229,7 @@ func (c *Cluster) majorVersionUpgrade() error {
|
||||
|
||||
isUpgradeSuccess := true
|
||||
numberOfPods := len(pods)
|
||||
if allRunning && masterPod != nil {
|
||||
if allRunning {
|
||||
c.logger.Infof("healthy cluster ready to upgrade, current: %d desired: %d", c.currentMajorVersion, desiredVersion)
|
||||
if c.currentMajorVersion < desiredVersion {
|
||||
defer func() error {
|
||||
@@ -276,6 +268,10 @@ func (c *Cluster) majorVersionUpgrade() error {
|
||||
if err != nil {
|
||||
isUpgradeSuccess = false
|
||||
c.annotatePostgresResource(isUpgradeSuccess)
|
||||
c.logger.Errorf("upgrade action triggered but command failed: %v", err)
|
||||
if strings.TrimSpace(scriptErrMsg) == "" {
|
||||
scriptErrMsg = err.Error()
|
||||
}
|
||||
c.eventRecorder.Eventf(c.GetReference(), v1.EventTypeWarning, "Major Version Upgrade", "upgrade from %d to %d FAILED: %v", c.currentMajorVersion, desiredVersion, scriptErrMsg)
|
||||
return fmt.Errorf("%s", scriptErrMsg)
|
||||
}
|
||||
|
||||
+39
-4
@@ -280,7 +280,7 @@ func (c *Cluster) MigrateMasterPod(podName spec.NamespacedName) error {
|
||||
}
|
||||
|
||||
scheduleSwitchover := false
|
||||
if !isInMaintenanceWindow(c.Spec.MaintenanceWindows) {
|
||||
if !c.isInMaintenanceWindow(c.Spec.MaintenanceWindows) {
|
||||
c.logger.Infof("postponing switchover, not in maintenance window")
|
||||
scheduleSwitchover = true
|
||||
}
|
||||
@@ -376,6 +376,36 @@ func (c *Cluster) getPatroniMemberData(pod *v1.Pod) (patroni.MemberData, error)
|
||||
return memberData, nil
|
||||
}
|
||||
|
||||
// podIsNotRunning returns true if a pod is known to be in a non-running state,
|
||||
// e.g. stuck in CreateContainerConfigError, CrashLoopBackOff, ImagePullBackOff, etc.
|
||||
// Pods with no status information are not considered non-running, as they may
|
||||
// simply not have reported status yet.
|
||||
func podIsNotRunning(pod *v1.Pod) bool {
|
||||
if pod.Status.Phase == "" {
|
||||
// No status reported yet — don't treat as non-running
|
||||
return false
|
||||
}
|
||||
if pod.Status.Phase != v1.PodRunning {
|
||||
return true
|
||||
}
|
||||
for _, cs := range pod.Status.ContainerStatuses {
|
||||
if cs.State.Waiting != nil || cs.State.Terminated != nil {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// allPodsRunning returns true only if every pod in the list is in a healthy running state.
|
||||
func (c *Cluster) allPodsRunning(pods []v1.Pod) bool {
|
||||
for i := range pods {
|
||||
if podIsNotRunning(&pods[i]) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (c *Cluster) recreatePod(podName spec.NamespacedName) (*v1.Pod, error) {
|
||||
stopCh := make(chan struct{})
|
||||
ch := c.registerPodSubscriber(podName)
|
||||
@@ -432,9 +462,10 @@ func (c *Cluster) recreatePods(pods []v1.Pod, switchoverCandidates []spec.Namesp
|
||||
}
|
||||
|
||||
newRole := PostgresRole(newPod.Labels[c.OpConfig.PodRoleLabel])
|
||||
if newRole == Replica {
|
||||
switch newRole {
|
||||
case Replica:
|
||||
replicas = append(replicas, util.NameFromMeta(pod.ObjectMeta))
|
||||
} else if newRole == Master {
|
||||
case Master:
|
||||
newMasterPod = newPod
|
||||
}
|
||||
}
|
||||
@@ -443,7 +474,8 @@ func (c *Cluster) recreatePods(pods []v1.Pod, switchoverCandidates []spec.Namesp
|
||||
// switchover if
|
||||
// 1. we have not observed a new master pod when re-creating former replicas
|
||||
// 2. we know possible switchover targets even when no replicas were recreated
|
||||
if newMasterPod == nil && len(replicas) > 0 {
|
||||
// 3. the master pod is actually running (can't switchover a dead master)
|
||||
if newMasterPod == nil && len(replicas) > 0 && !podIsNotRunning(masterPod) {
|
||||
masterCandidate, err := c.getSwitchoverCandidate(masterPod)
|
||||
if err != nil {
|
||||
// do not recreate master now so it will keep the update flag and switchover will be retried on next sync
|
||||
@@ -454,6 +486,9 @@ func (c *Cluster) recreatePods(pods []v1.Pod, switchoverCandidates []spec.Namesp
|
||||
}
|
||||
} else if newMasterPod == nil && len(replicas) == 0 {
|
||||
c.logger.Warningf("cannot perform switch over before re-creating the pod: no replicas")
|
||||
} else if podIsNotRunning(masterPod) {
|
||||
c.logger.Warningf("master pod %q is not running, skipping switchover and recreating directly",
|
||||
util.NameFromMeta(masterPod.ObjectMeta))
|
||||
}
|
||||
c.logger.Infof("recreating old master pod %q", util.NameFromMeta(masterPod.ObjectMeta))
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ import (
|
||||
"github.com/zalando/postgres-operator/pkg/util/config"
|
||||
"github.com/zalando/postgres-operator/pkg/util/k8sutil"
|
||||
"github.com/zalando/postgres-operator/pkg/util/patroni"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
)
|
||||
|
||||
func TestGetSwitchoverCandidate(t *testing.T) {
|
||||
@@ -112,3 +113,302 @@ func TestGetSwitchoverCandidate(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestPodIsNotRunning(t *testing.T) {
|
||||
tests := []struct {
|
||||
subtest string
|
||||
pod v1.Pod
|
||||
expected bool
|
||||
}{
|
||||
{
|
||||
subtest: "pod with no status reported yet",
|
||||
pod: v1.Pod{
|
||||
Status: v1.PodStatus{},
|
||||
},
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
subtest: "pod running with all containers ready",
|
||||
pod: v1.Pod{
|
||||
Status: v1.PodStatus{
|
||||
Phase: v1.PodRunning,
|
||||
ContainerStatuses: []v1.ContainerStatus{
|
||||
{
|
||||
State: v1.ContainerState{
|
||||
Running: &v1.ContainerStateRunning{},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
subtest: "pod in pending phase",
|
||||
pod: v1.Pod{
|
||||
Status: v1.PodStatus{
|
||||
Phase: v1.PodPending,
|
||||
},
|
||||
},
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
subtest: "pod running but container in CreateContainerConfigError",
|
||||
pod: v1.Pod{
|
||||
Status: v1.PodStatus{
|
||||
Phase: v1.PodRunning,
|
||||
ContainerStatuses: []v1.ContainerStatus{
|
||||
{
|
||||
State: v1.ContainerState{
|
||||
Waiting: &v1.ContainerStateWaiting{
|
||||
Reason: "CreateContainerConfigError",
|
||||
Message: `secret "some-secret" not found`,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
subtest: "pod running but container in CrashLoopBackOff",
|
||||
pod: v1.Pod{
|
||||
Status: v1.PodStatus{
|
||||
Phase: v1.PodRunning,
|
||||
ContainerStatuses: []v1.ContainerStatus{
|
||||
{
|
||||
State: v1.ContainerState{
|
||||
Waiting: &v1.ContainerStateWaiting{
|
||||
Reason: "CrashLoopBackOff",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
subtest: "pod running but container terminated",
|
||||
pod: v1.Pod{
|
||||
Status: v1.PodStatus{
|
||||
Phase: v1.PodRunning,
|
||||
ContainerStatuses: []v1.ContainerStatus{
|
||||
{
|
||||
State: v1.ContainerState{
|
||||
Terminated: &v1.ContainerStateTerminated{
|
||||
ExitCode: 137,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
subtest: "pod running with mixed container states - one healthy one broken",
|
||||
pod: v1.Pod{
|
||||
Status: v1.PodStatus{
|
||||
Phase: v1.PodRunning,
|
||||
ContainerStatuses: []v1.ContainerStatus{
|
||||
{
|
||||
State: v1.ContainerState{
|
||||
Running: &v1.ContainerStateRunning{},
|
||||
},
|
||||
},
|
||||
{
|
||||
State: v1.ContainerState{
|
||||
Waiting: &v1.ContainerStateWaiting{
|
||||
Reason: "CreateContainerConfigError",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
subtest: "pod in failed phase",
|
||||
pod: v1.Pod{
|
||||
Status: v1.PodStatus{
|
||||
Phase: v1.PodFailed,
|
||||
},
|
||||
},
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
subtest: "pod running with multiple healthy containers",
|
||||
pod: v1.Pod{
|
||||
Status: v1.PodStatus{
|
||||
Phase: v1.PodRunning,
|
||||
ContainerStatuses: []v1.ContainerStatus{
|
||||
{
|
||||
State: v1.ContainerState{
|
||||
Running: &v1.ContainerStateRunning{},
|
||||
},
|
||||
},
|
||||
{
|
||||
State: v1.ContainerState{
|
||||
Running: &v1.ContainerStateRunning{},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
subtest: "pod running with ImagePullBackOff",
|
||||
pod: v1.Pod{
|
||||
Status: v1.PodStatus{
|
||||
Phase: v1.PodRunning,
|
||||
ContainerStatuses: []v1.ContainerStatus{
|
||||
{
|
||||
State: v1.ContainerState{
|
||||
Waiting: &v1.ContainerStateWaiting{
|
||||
Reason: "ImagePullBackOff",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.subtest, func(t *testing.T) {
|
||||
result := podIsNotRunning(&tt.pod)
|
||||
if result != tt.expected {
|
||||
t.Errorf("podIsNotRunning() = %v, expected %v", result, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAllPodsRunning(t *testing.T) {
|
||||
client, _ := newFakeK8sSyncClient()
|
||||
|
||||
var cluster = New(
|
||||
Config{
|
||||
OpConfig: config.Config{
|
||||
Resources: config.Resources{
|
||||
ClusterLabels: map[string]string{"application": "spilo"},
|
||||
ClusterNameLabel: "cluster-name",
|
||||
PodRoleLabel: "spilo-role",
|
||||
},
|
||||
},
|
||||
}, client, acidv1.Postgresql{}, logger, eventRecorder)
|
||||
|
||||
tests := []struct {
|
||||
subtest string
|
||||
pods []v1.Pod
|
||||
expected bool
|
||||
}{
|
||||
{
|
||||
subtest: "all pods running",
|
||||
pods: []v1.Pod{
|
||||
{
|
||||
Status: v1.PodStatus{
|
||||
Phase: v1.PodRunning,
|
||||
ContainerStatuses: []v1.ContainerStatus{
|
||||
{State: v1.ContainerState{Running: &v1.ContainerStateRunning{}}},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Status: v1.PodStatus{
|
||||
Phase: v1.PodRunning,
|
||||
ContainerStatuses: []v1.ContainerStatus{
|
||||
{State: v1.ContainerState{Running: &v1.ContainerStateRunning{}}},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
subtest: "one pod not running",
|
||||
pods: []v1.Pod{
|
||||
{
|
||||
Status: v1.PodStatus{
|
||||
Phase: v1.PodRunning,
|
||||
ContainerStatuses: []v1.ContainerStatus{
|
||||
{State: v1.ContainerState{Running: &v1.ContainerStateRunning{}}},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Status: v1.PodStatus{
|
||||
Phase: v1.PodRunning,
|
||||
ContainerStatuses: []v1.ContainerStatus{
|
||||
{
|
||||
State: v1.ContainerState{
|
||||
Waiting: &v1.ContainerStateWaiting{
|
||||
Reason: "CreateContainerConfigError",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
subtest: "all pods not running",
|
||||
pods: []v1.Pod{
|
||||
{
|
||||
Status: v1.PodStatus{
|
||||
Phase: v1.PodPending,
|
||||
},
|
||||
},
|
||||
{
|
||||
Status: v1.PodStatus{
|
||||
Phase: v1.PodRunning,
|
||||
ContainerStatuses: []v1.ContainerStatus{
|
||||
{
|
||||
State: v1.ContainerState{
|
||||
Waiting: &v1.ContainerStateWaiting{
|
||||
Reason: "CrashLoopBackOff",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
subtest: "empty pod list",
|
||||
pods: []v1.Pod{},
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
subtest: "pods with no status reported yet",
|
||||
pods: []v1.Pod{
|
||||
{
|
||||
Status: v1.PodStatus{},
|
||||
},
|
||||
{
|
||||
Status: v1.PodStatus{},
|
||||
},
|
||||
},
|
||||
expected: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.subtest, func(t *testing.T) {
|
||||
result := cluster.allPodsRunning(tt.pods)
|
||||
if result != tt.expected {
|
||||
t.Errorf("allPodsRunning() = %v, expected %v", result, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -94,12 +94,12 @@ func (c *Cluster) listResources() error {
|
||||
func (c *Cluster) createStatefulSet() (*appsv1.StatefulSet, error) {
|
||||
c.setProcessName("creating statefulset")
|
||||
// check if it's allowed that spec contains initContainers
|
||||
if c.Spec.InitContainers != nil && len(c.Spec.InitContainers) > 0 &&
|
||||
if len(c.Spec.InitContainers) > 0 &&
|
||||
c.OpConfig.EnableInitContainers != nil && !(*c.OpConfig.EnableInitContainers) {
|
||||
return nil, fmt.Errorf("initContainers specified but disabled in configuration")
|
||||
}
|
||||
// check if it's allowed that spec contains sidecars
|
||||
if c.Spec.Sidecars != nil && len(c.Spec.Sidecars) > 0 &&
|
||||
if len(c.Spec.Sidecars) > 0 &&
|
||||
c.OpConfig.EnableSidecars != nil && !(*c.OpConfig.EnableSidecars) {
|
||||
return nil, fmt.Errorf("sidecar containers specified but disabled in configuration")
|
||||
}
|
||||
|
||||
+147
-69
@@ -20,6 +20,7 @@ import (
|
||||
batchv1 "k8s.io/api/batch/v1"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
policyv1 "k8s.io/api/policy/v1"
|
||||
"k8s.io/apimachinery/pkg/api/equality"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
)
|
||||
@@ -40,23 +41,28 @@ func (c *Cluster) Sync(newSpec *acidv1.Postgresql) error {
|
||||
defer c.mu.Unlock()
|
||||
|
||||
oldSpec := c.Postgresql
|
||||
|
||||
if !c.isInMaintenanceWindow(newSpec.Spec.MaintenanceWindows) {
|
||||
// do not apply any major version related changes yet
|
||||
newSpec.Spec.PostgresqlParam.PgVersion = oldSpec.Spec.PostgresqlParam.PgVersion
|
||||
}
|
||||
|
||||
c.setSpec(newSpec)
|
||||
|
||||
defer func() {
|
||||
var (
|
||||
pgUpdatedStatus *acidv1.Postgresql
|
||||
errStatus error
|
||||
)
|
||||
if err != nil {
|
||||
c.logger.Warningf("error while syncing cluster state: %v", err)
|
||||
pgUpdatedStatus, errStatus = c.KubeClient.SetPostgresCRDStatus(c.clusterName(), acidv1.ClusterStatusSyncFailed)
|
||||
newSpec.Status.PostgresClusterStatus = acidv1.ClusterStatusSyncFailed
|
||||
} else if !c.Status.Running() {
|
||||
pgUpdatedStatus, errStatus = c.KubeClient.SetPostgresCRDStatus(c.clusterName(), acidv1.ClusterStatusRunning)
|
||||
newSpec.Status.PostgresClusterStatus = acidv1.ClusterStatusRunning
|
||||
}
|
||||
if errStatus != nil {
|
||||
c.logger.Warningf("could not set cluster status: %v", errStatus)
|
||||
}
|
||||
if pgUpdatedStatus != nil {
|
||||
|
||||
if !equality.Semantic.DeepEqual(oldSpec.Status, newSpec.Status) {
|
||||
pgUpdatedStatus, err := c.KubeClient.SetPostgresCRDStatus(c.clusterName(), newSpec)
|
||||
if err != nil {
|
||||
c.logger.Warningf("could not set cluster status: %v", err)
|
||||
return
|
||||
}
|
||||
c.setSpec(pgUpdatedStatus)
|
||||
}
|
||||
}()
|
||||
@@ -97,11 +103,6 @@ func (c *Cluster) Sync(newSpec *acidv1.Postgresql) error {
|
||||
}
|
||||
}
|
||||
|
||||
if !isInMaintenanceWindow(newSpec.Spec.MaintenanceWindows) {
|
||||
// do not apply any major version related changes yet
|
||||
newSpec.Spec.PostgresqlParam.PgVersion = oldSpec.Spec.PostgresqlParam.PgVersion
|
||||
}
|
||||
|
||||
if err = c.syncStatefulSet(); err != nil {
|
||||
if !k8sutil.ResourceAlreadyExists(err) {
|
||||
err = fmt.Errorf("could not sync statefulsets: %v", err)
|
||||
@@ -718,14 +719,26 @@ func (c *Cluster) syncStatefulSet() error {
|
||||
if configPatched, restartPrimaryFirst, restartWait, err = c.syncPatroniConfig(pods, c.Spec.Patroni, requiredPgParameters); err != nil {
|
||||
c.logger.Warningf("Patroni config updated? %v - errors during config sync: %v", configPatched, err)
|
||||
postponeReasons = append(postponeReasons, "errors during Patroni config sync")
|
||||
isSafeToRecreatePods = false
|
||||
// Only mark unsafe if all pods are running. If some pods are not running,
|
||||
// Patroni API errors are expected and should not block pod recreation,
|
||||
// which is the only way to fix non-running pods.
|
||||
if c.allPodsRunning(pods) {
|
||||
isSafeToRecreatePods = false
|
||||
} else {
|
||||
c.logger.Warningf("ignoring Patroni config sync errors because some pods are not running")
|
||||
}
|
||||
}
|
||||
|
||||
// restart Postgres where it is still pending
|
||||
if err = c.restartInstances(pods, restartWait, restartPrimaryFirst); err != nil {
|
||||
c.logger.Errorf("errors while restarting Postgres in pods via Patroni API: %v", err)
|
||||
postponeReasons = append(postponeReasons, "errors while restarting Postgres via Patroni API")
|
||||
isSafeToRecreatePods = false
|
||||
// Same logic: don't let unreachable non-running pods block recreation.
|
||||
if c.allPodsRunning(pods) {
|
||||
isSafeToRecreatePods = false
|
||||
} else {
|
||||
c.logger.Warningf("ignoring Patroni restart errors because some pods are not running")
|
||||
}
|
||||
}
|
||||
|
||||
// if we get here we also need to re-create the pods (either leftovers from the old
|
||||
@@ -1030,6 +1043,23 @@ func (c *Cluster) syncStandbyClusterConfiguration() error {
|
||||
standbyOptionsToSet["create_replica_methods"] = []string{"bootstrap_standby_with_wale", "basebackup_fast_xlog"}
|
||||
standbyOptionsToSet["restore_command"] = "envdir \"/run/etc/wal-e.d/env-standby\" /scripts/restore_command.sh \"%f\" \"%p\""
|
||||
|
||||
if c.Spec.StandbyCluster.StandbyHost != "" {
|
||||
standbyOptionsToSet["host"] = c.Spec.StandbyCluster.StandbyHost
|
||||
} else {
|
||||
standbyOptionsToSet["host"] = nil
|
||||
}
|
||||
|
||||
if c.Spec.StandbyCluster.StandbyPort != "" {
|
||||
standbyOptionsToSet["port"] = c.Spec.StandbyCluster.StandbyPort
|
||||
} else {
|
||||
standbyOptionsToSet["port"] = nil
|
||||
}
|
||||
|
||||
if c.Spec.StandbyCluster.StandbyPrimarySlotName != "" {
|
||||
standbyOptionsToSet["primary_slot_name"] = c.Spec.StandbyCluster.StandbyPrimarySlotName
|
||||
} else {
|
||||
standbyOptionsToSet["primary_slot_name"] = nil
|
||||
}
|
||||
} else {
|
||||
c.logger.Infof("promoting standby cluster and detach from source")
|
||||
standbyOptionsToSet = nil
|
||||
@@ -1059,40 +1089,45 @@ func (c *Cluster) syncStandbyClusterConfiguration() error {
|
||||
func (c *Cluster) syncSecrets() error {
|
||||
c.logger.Debug("syncing secrets")
|
||||
c.setProcessName("syncing secrets")
|
||||
errors := make([]string, 0)
|
||||
generatedSecrets := c.generateUserSecrets()
|
||||
retentionUsers := make([]string, 0)
|
||||
currentTime := time.Now()
|
||||
|
||||
for secretUsername, generatedSecret := range generatedSecrets {
|
||||
secret, err := c.KubeClient.Secrets(generatedSecret.Namespace).Create(context.TODO(), generatedSecret, metav1.CreateOptions{})
|
||||
pgUserDegraded := false
|
||||
createdSecret, err := c.KubeClient.Secrets(generatedSecret.Namespace).Create(context.TODO(), generatedSecret, metav1.CreateOptions{})
|
||||
if err == nil {
|
||||
c.Secrets[secret.UID] = secret
|
||||
c.logger.Infof("created new secret %s, namespace: %s, uid: %s", util.NameFromMeta(secret.ObjectMeta), generatedSecret.Namespace, secret.UID)
|
||||
c.Secrets[createdSecret.UID] = createdSecret
|
||||
c.logger.Infof("created new secret %s, namespace: %s, uid: %s", util.NameFromMeta(createdSecret.ObjectMeta), generatedSecret.Namespace, createdSecret.UID)
|
||||
continue
|
||||
}
|
||||
if k8sutil.ResourceAlreadyExists(err) {
|
||||
if err = c.updateSecret(secretUsername, generatedSecret, &retentionUsers, currentTime); err != nil {
|
||||
c.logger.Warningf("syncing secret %s failed: %v", util.NameFromMeta(secret.ObjectMeta), err)
|
||||
updatedSecret, err := c.updateSecret(secretUsername, generatedSecret, &retentionUsers, currentTime)
|
||||
if err == nil {
|
||||
c.Secrets[updatedSecret.UID] = updatedSecret
|
||||
continue
|
||||
}
|
||||
errors = append(errors, fmt.Sprintf("syncing secret %s failed: %v", util.NameFromMeta(generatedSecret.ObjectMeta), err))
|
||||
pgUserDegraded = true
|
||||
} else {
|
||||
return fmt.Errorf("could not create secret for user %s: in namespace %s: %v", secretUsername, generatedSecret.Namespace, err)
|
||||
errors = append(errors, fmt.Sprintf("could not create secret for user %s: in namespace %s: %v", secretUsername, generatedSecret.Namespace, err))
|
||||
pgUserDegraded = true
|
||||
}
|
||||
c.updatePgUser(secretUsername, pgUserDegraded)
|
||||
}
|
||||
|
||||
// remove rotation users that exceed the retention interval
|
||||
if len(retentionUsers) > 0 {
|
||||
err := c.initDbConn()
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not init db connection: %v", err)
|
||||
}
|
||||
if err = c.cleanupRotatedUsers(retentionUsers, c.pgDb); err != nil {
|
||||
return fmt.Errorf("error removing users exceeding configured retention interval: %v", err)
|
||||
}
|
||||
if err := c.closeDbConn(); err != nil {
|
||||
c.logger.Errorf("could not close database connection after removing users exceeding configured retention interval: %v", err)
|
||||
if err := c.cleanupRotatedUsers(retentionUsers); err != nil {
|
||||
errors = append(errors, fmt.Sprintf("error removing users exceeding configured retention interval: %v", err))
|
||||
}
|
||||
}
|
||||
|
||||
if len(errors) > 0 {
|
||||
return fmt.Errorf("%v", strings.Join(errors, `', '`))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1105,7 +1140,7 @@ func (c *Cluster) updateSecret(
|
||||
secretUsername string,
|
||||
generatedSecret *v1.Secret,
|
||||
retentionUsers *[]string,
|
||||
currentTime time.Time) error {
|
||||
currentTime time.Time) (*v1.Secret, error) {
|
||||
var (
|
||||
secret *v1.Secret
|
||||
err error
|
||||
@@ -1115,20 +1150,21 @@ func (c *Cluster) updateSecret(
|
||||
|
||||
// get the secret first
|
||||
if secret, err = c.KubeClient.Secrets(generatedSecret.Namespace).Get(context.TODO(), generatedSecret.Name, metav1.GetOptions{}); err != nil {
|
||||
return fmt.Errorf("could not get current secret: %v", err)
|
||||
return generatedSecret, fmt.Errorf("could not get current secret: %v", err)
|
||||
}
|
||||
c.Secrets[secret.UID] = secret
|
||||
|
||||
// fetch user map to update later
|
||||
var userMap map[string]spec.PgUser
|
||||
var userKey string
|
||||
if secretUsername == c.systemUsers[constants.SuperuserKeyName].Name {
|
||||
switch secretUsername {
|
||||
case c.systemUsers[constants.SuperuserKeyName].Name:
|
||||
userKey = constants.SuperuserKeyName
|
||||
userMap = c.systemUsers
|
||||
} else if secretUsername == c.systemUsers[constants.ReplicationUserKeyName].Name {
|
||||
case c.systemUsers[constants.ReplicationUserKeyName].Name:
|
||||
userKey = constants.ReplicationUserKeyName
|
||||
userMap = c.systemUsers
|
||||
} else {
|
||||
default:
|
||||
userKey = secretUsername
|
||||
userMap = c.pgUsers
|
||||
}
|
||||
@@ -1151,37 +1187,15 @@ func (c *Cluster) updateSecret(
|
||||
pwdUser := userMap[userKey]
|
||||
secretName := util.NameFromMeta(secret.ObjectMeta)
|
||||
|
||||
// if password rotation is enabled update password and username if rotation interval has been passed
|
||||
// rotation can be enabled globally or via the manifest (excluding the Postgres superuser)
|
||||
rotationEnabledInManifest := secretUsername != constants.SuperuserKeyName &&
|
||||
(slices.Contains(c.Spec.UsersWithSecretRotation, secretUsername) ||
|
||||
slices.Contains(c.Spec.UsersWithInPlaceSecretRotation, secretUsername))
|
||||
|
||||
// globally enabled rotation is only allowed for manifest and bootstrapped roles
|
||||
allowedRoleTypes := []spec.RoleOrigin{spec.RoleOriginManifest, spec.RoleOriginBootstrap}
|
||||
rotationAllowed := !pwdUser.IsDbOwner && slices.Contains(allowedRoleTypes, pwdUser.Origin) && c.Spec.StandbyCluster == nil
|
||||
|
||||
// users can ignore any kind of rotation
|
||||
isIgnoringRotation := slices.Contains(c.Spec.UsersIgnoringSecretRotation, secretUsername)
|
||||
|
||||
if ((c.OpConfig.EnablePasswordRotation && rotationAllowed) || rotationEnabledInManifest) && !isIgnoringRotation {
|
||||
updateSecretMsg, err = c.rotatePasswordInSecret(secret, secretUsername, pwdUser.Origin, currentTime, retentionUsers)
|
||||
// do not perform any rotation of reset for standby clusters
|
||||
if !isStandbyCluster(&c.Spec) {
|
||||
updateSecretMsg, err = c.checkForPasswordRotation(secret, secretUsername, pwdUser, retentionUsers, currentTime)
|
||||
if err != nil {
|
||||
c.logger.Warnf("password rotation failed for user %s: %v", secretUsername, err)
|
||||
return nil, fmt.Errorf("error while checking for password rotation: %v", err)
|
||||
}
|
||||
if updateSecretMsg != "" {
|
||||
updateSecret = true
|
||||
}
|
||||
} else {
|
||||
// username might not match if password rotation has been disabled again
|
||||
if secretUsername != string(secret.Data["username"]) {
|
||||
*retentionUsers = append(*retentionUsers, secretUsername)
|
||||
secret.Data["username"] = []byte(secretUsername)
|
||||
secret.Data["password"] = []byte(util.RandomPassword(constants.PasswordLength))
|
||||
secret.Data["nextRotation"] = []byte{}
|
||||
updateSecret = true
|
||||
updateSecretMsg = fmt.Sprintf("secret %s does not contain the role %s - updating username and resetting password", secretName, secretUsername)
|
||||
}
|
||||
}
|
||||
|
||||
// if this secret belongs to the infrastructure role and the password has changed - replace it in the secret
|
||||
@@ -1210,24 +1224,71 @@ func (c *Cluster) updateSecret(
|
||||
if updateSecret {
|
||||
c.logger.Infof("%s", updateSecretMsg)
|
||||
if secret, err = c.KubeClient.Secrets(secret.Namespace).Update(context.TODO(), secret, metav1.UpdateOptions{}); err != nil {
|
||||
return fmt.Errorf("could not update secret %s: %v", secretName, err)
|
||||
return nil, fmt.Errorf("could not update secret: %v", err)
|
||||
}
|
||||
c.Secrets[secret.UID] = secret
|
||||
}
|
||||
|
||||
if changed, _ := c.compareAnnotations(secret.Annotations, generatedSecret.Annotations, nil); changed {
|
||||
patchData, err := metaAnnotationsPatch(generatedSecret.Annotations)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not form patch for secret %q annotations: %v", secret.Name, err)
|
||||
return nil, fmt.Errorf("could not form patch for secret annotations: %v", err)
|
||||
}
|
||||
secret, err = c.KubeClient.Secrets(secret.Namespace).Patch(context.TODO(), secret.Name, types.MergePatchType, []byte(patchData), metav1.PatchOptions{})
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not patch annotations for secret %q: %v", secret.Name, err)
|
||||
return nil, fmt.Errorf("could not patch annotations for secret: %v", err)
|
||||
}
|
||||
c.Secrets[secret.UID] = secret
|
||||
}
|
||||
|
||||
return nil
|
||||
return secret, nil
|
||||
}
|
||||
|
||||
func (c *Cluster) checkForPasswordRotation(
|
||||
secret *v1.Secret,
|
||||
secretUsername string,
|
||||
pwdUser spec.PgUser,
|
||||
retentionUsers *[]string,
|
||||
currentTime time.Time) (string, error) {
|
||||
|
||||
var (
|
||||
passwordRotationMsg string
|
||||
err error
|
||||
)
|
||||
|
||||
// if password rotation is enabled update password and username if rotation interval has been passed
|
||||
// rotation can be enabled globally or via the manifest (excluding the Postgres superuser)
|
||||
rotationEnabledInManifest := secretUsername != constants.SuperuserKeyName &&
|
||||
(slices.Contains(c.Spec.UsersWithSecretRotation, secretUsername) ||
|
||||
slices.Contains(c.Spec.UsersWithInPlaceSecretRotation, secretUsername))
|
||||
|
||||
// globally enabled rotation is only allowed for manifest and bootstrapped roles
|
||||
allowedRoleTypes := []spec.RoleOrigin{spec.RoleOriginManifest, spec.RoleOriginBootstrap}
|
||||
rotationAllowed := !pwdUser.IsDbOwner && slices.Contains(allowedRoleTypes, pwdUser.Origin)
|
||||
|
||||
// users can ignore any kind of rotation
|
||||
isIgnoringRotation := slices.Contains(c.Spec.UsersIgnoringSecretRotation, secretUsername)
|
||||
|
||||
if ((c.OpConfig.EnablePasswordRotation && rotationAllowed) || rotationEnabledInManifest) && !isIgnoringRotation {
|
||||
passwordRotationMsg, err = c.rotatePasswordInSecret(secret, secretUsername, pwdUser.Origin, currentTime, retentionUsers)
|
||||
if err != nil {
|
||||
c.logger.Warnf("password rotation failed for user %s: %v", secretUsername, err)
|
||||
}
|
||||
} else {
|
||||
// username might not match if password rotation has been disabled again
|
||||
usernameFromSecret := string(secret.Data["username"])
|
||||
if secretUsername != usernameFromSecret {
|
||||
// handle edge case when manifest user conflicts with a user from prepared databases
|
||||
if strings.Replace(usernameFromSecret, "-", "_", -1) == strings.Replace(secretUsername, "-", "_", -1) {
|
||||
return "", fmt.Errorf("could not update secret because of user name mismatch: expected: %s, got: %s", secretUsername, usernameFromSecret)
|
||||
}
|
||||
*retentionUsers = append(*retentionUsers, secretUsername)
|
||||
secret.Data["username"] = []byte(secretUsername)
|
||||
secret.Data["password"] = []byte(util.RandomPassword(constants.PasswordLength))
|
||||
secret.Data["nextRotation"] = []byte{}
|
||||
passwordRotationMsg = fmt.Sprintf("secret does not contain the role %s - updating username and resetting password", secretUsername)
|
||||
}
|
||||
}
|
||||
|
||||
return passwordRotationMsg, nil
|
||||
}
|
||||
|
||||
func (c *Cluster) rotatePasswordInSecret(
|
||||
@@ -1333,6 +1394,23 @@ func (c *Cluster) rotatePasswordInSecret(
|
||||
return updateSecretMsg, nil
|
||||
}
|
||||
|
||||
func (c *Cluster) updatePgUser(secretUsername string, degraded bool) {
|
||||
for key, pgUser := range c.pgUsers {
|
||||
if pgUser.Name == secretUsername {
|
||||
pgUser.Degraded = degraded
|
||||
c.pgUsers[key] = pgUser
|
||||
return
|
||||
}
|
||||
}
|
||||
for key, pgUser := range c.systemUsers {
|
||||
if pgUser.Name == secretUsername {
|
||||
pgUser.Degraded = degraded
|
||||
c.systemUsers[key] = pgUser
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Cluster) syncRoles() (err error) {
|
||||
c.setProcessName("syncing roles")
|
||||
|
||||
|
||||
+107
-2
@@ -12,9 +12,12 @@ import (
|
||||
|
||||
v1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
k8stesting "k8s.io/client-go/testing"
|
||||
|
||||
"github.com/golang/mock/gomock"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/zalando/postgres-operator/mocks"
|
||||
@@ -50,6 +53,16 @@ func newFakeK8sSyncClient() (k8sutil.KubernetesClient, *fake.Clientset) {
|
||||
}
|
||||
|
||||
func newFakeK8sSyncSecretsClient() (k8sutil.KubernetesClient, *fake.Clientset) {
|
||||
// add a reactor that checks namespace existence before creating secrets
|
||||
clientSet.PrependReactor("create", "secrets", func(action k8stesting.Action) (bool, runtime.Object, error) {
|
||||
createAction := action.(k8stesting.CreateAction)
|
||||
secret := createAction.GetObject().(*v1.Secret)
|
||||
if secret.Namespace != "default" {
|
||||
return true, nil, errors.New("namespace does not exist")
|
||||
}
|
||||
return false, nil, nil
|
||||
})
|
||||
|
||||
return k8sutil.KubernetesClient{
|
||||
SecretsGetter: clientSet.CoreV1(),
|
||||
}, clientSet
|
||||
@@ -788,6 +801,41 @@ func TestSyncStandbyClusterConfiguration(t *testing.T) {
|
||||
// this should update the Patroni config again
|
||||
err = cluster.syncStandbyClusterConfiguration()
|
||||
assert.NoError(t, err)
|
||||
|
||||
// test with standby_host, standby_port and standby_primary_slot_name
|
||||
cluster.Spec.StandbyCluster = &acidv1.StandbyDescription{
|
||||
StandbyHost: "remote-primary.example.com",
|
||||
StandbyPort: "5433",
|
||||
StandbyPrimarySlotName: "standby_slot",
|
||||
}
|
||||
cluster.syncStatefulSet()
|
||||
updatedSts4 := cluster.Statefulset
|
||||
|
||||
// check that pods have all three STANDBY_* environment variables
|
||||
assert.Contains(t, updatedSts4.Spec.Template.Spec.Containers[0].Env, v1.EnvVar{Name: "STANDBY_HOST", Value: "remote-primary.example.com"})
|
||||
assert.Contains(t, updatedSts4.Spec.Template.Spec.Containers[0].Env, v1.EnvVar{Name: "STANDBY_PORT", Value: "5433"})
|
||||
assert.Contains(t, updatedSts4.Spec.Template.Spec.Containers[0].Env, v1.EnvVar{Name: "STANDBY_PRIMARY_SLOT_NAME", Value: "standby_slot"})
|
||||
|
||||
// this should update the Patroni config with host, port and primary_slot_name
|
||||
err = cluster.syncStandbyClusterConfiguration()
|
||||
assert.NoError(t, err)
|
||||
|
||||
// test property deletion: remove standby_primary_slot_name
|
||||
cluster.Spec.StandbyCluster = &acidv1.StandbyDescription{
|
||||
StandbyHost: "remote-primary.example.com",
|
||||
StandbyPort: "5433",
|
||||
}
|
||||
cluster.syncStatefulSet()
|
||||
updatedSts5 := cluster.Statefulset
|
||||
|
||||
// check that STANDBY_PRIMARY_SLOT_NAME is not present
|
||||
assert.Contains(t, updatedSts5.Spec.Template.Spec.Containers[0].Env, v1.EnvVar{Name: "STANDBY_HOST", Value: "remote-primary.example.com"})
|
||||
assert.Contains(t, updatedSts5.Spec.Template.Spec.Containers[0].Env, v1.EnvVar{Name: "STANDBY_PORT", Value: "5433"})
|
||||
assert.NotContains(t, updatedSts5.Spec.Template.Spec.Containers[0].Env, v1.EnvVar{Name: "STANDBY_PRIMARY_SLOT_NAME", Value: "standby_slot"})
|
||||
|
||||
// this should update the Patroni config and set primary_slot_name to nil
|
||||
err = cluster.syncStandbyClusterConfiguration()
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestUpdateSecret(t *testing.T) {
|
||||
@@ -810,7 +858,7 @@ func TestUpdateSecret(t *testing.T) {
|
||||
},
|
||||
Spec: acidv1.PostgresSpec{
|
||||
Databases: map[string]string{dbname: dbowner},
|
||||
Users: map[string]acidv1.UserFlags{appUser: {}, "bar": {}, dbowner: {}},
|
||||
Users: map[string]acidv1.UserFlags{appUser: {}, "bar": {}, dbowner: {}, "not-exist.test_user": {}},
|
||||
UsersIgnoringSecretRotation: []string{"bar"},
|
||||
UsersWithInPlaceSecretRotation: []string{dbowner},
|
||||
Streams: []acidv1.Stream{
|
||||
@@ -842,6 +890,7 @@ func TestUpdateSecret(t *testing.T) {
|
||||
PasswordRotationInterval: 1,
|
||||
PasswordRotationUserRetention: 3,
|
||||
},
|
||||
EnableCrossNamespaceSecret: true,
|
||||
Resources: config.Resources{
|
||||
ClusterLabels: map[string]string{"application": "spilo"},
|
||||
ClusterNameLabel: "cluster-name",
|
||||
@@ -864,7 +913,9 @@ func TestUpdateSecret(t *testing.T) {
|
||||
|
||||
allUsers := make(map[string]spec.PgUser)
|
||||
for _, pgUser := range cluster.pgUsers {
|
||||
allUsers[pgUser.Name] = pgUser
|
||||
if !pgUser.Degraded {
|
||||
allUsers[pgUser.Name] = pgUser
|
||||
}
|
||||
}
|
||||
for _, systemUser := range cluster.systemUsers {
|
||||
allUsers[systemUser.Name] = systemUser
|
||||
@@ -948,3 +999,57 @@ func TestUpdateSecret(t *testing.T) {
|
||||
t.Errorf("%s: updated secret does not contain expected username: expected %s, got %s", testName, appUser, currentUsername)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateSecretNameConflict(t *testing.T) {
|
||||
client, _ := newFakeK8sSyncSecretsClient()
|
||||
|
||||
clusterName := "acid-test-cluster"
|
||||
namespace := "default"
|
||||
secretTemplate := config.StringTemplate("{username}.{cluster}.credentials")
|
||||
|
||||
// define manifest user that has the same name as a prepared database owner user except for dashes vs underscores
|
||||
// because of this the operator cannot create both secrets because underscores are not allowed in k8s secret names
|
||||
pg := acidv1.Postgresql{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: clusterName,
|
||||
Namespace: namespace,
|
||||
},
|
||||
Spec: acidv1.PostgresSpec{
|
||||
PreparedDatabases: map[string]acidv1.PreparedDatabase{"prepared": {DefaultUsers: true}},
|
||||
Users: map[string]acidv1.UserFlags{"prepared-owner-user": {}},
|
||||
Volume: acidv1.Volume{
|
||||
Size: "1Gi",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
var cluster = New(
|
||||
Config{
|
||||
OpConfig: config.Config{
|
||||
Auth: config.Auth{
|
||||
SuperUsername: "postgres",
|
||||
ReplicationUsername: "standby",
|
||||
SecretNameTemplate: secretTemplate,
|
||||
},
|
||||
Resources: config.Resources{
|
||||
ClusterLabels: map[string]string{"application": "spilo"},
|
||||
ClusterNameLabel: "cluster-name",
|
||||
},
|
||||
},
|
||||
}, client, pg, logger, eventRecorder)
|
||||
|
||||
cluster.Name = clusterName
|
||||
cluster.Namespace = namespace
|
||||
cluster.pgUsers = map[string]spec.PgUser{}
|
||||
|
||||
// init all users
|
||||
cluster.initUsers()
|
||||
// create secrets and fail because of user name mismatch
|
||||
// prepared-owner-user from manifest vs prepared_owner_user from prepared database
|
||||
err := cluster.syncSecrets()
|
||||
assert.Error(t, err)
|
||||
|
||||
// the order of secrets to sync is not deterministic, check only first part of the error message
|
||||
expectedError := fmt.Sprintf("syncing secret %s failed: error while checking for password rotation: could not update secret because of user name mismatch", "default/prepared-owner-user.acid-test-cluster.credentials")
|
||||
assert.Contains(t, err.Error(), expectedError)
|
||||
}
|
||||
|
||||
+29
-3
@@ -8,6 +8,7 @@ import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"reflect"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -663,15 +664,40 @@ func parseResourceRequirements(resourcesRequirement v1.ResourceRequirements) (ac
|
||||
return resources, nil
|
||||
}
|
||||
|
||||
func isInMaintenanceWindow(specMaintenanceWindows []acidv1.MaintenanceWindow) bool {
|
||||
if len(specMaintenanceWindows) == 0 {
|
||||
func isStandbyCluster(spec *acidv1.PostgresSpec) bool {
|
||||
for _, env := range spec.Env {
|
||||
hasStandbyEnv, _ := regexp.MatchString(`^STANDBY_WALE_(S3|GS|GSC|SWIFT)_PREFIX$`, env.Name)
|
||||
if hasStandbyEnv && env.Value != "" {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return spec.StandbyCluster != nil
|
||||
}
|
||||
|
||||
func (c *Cluster) isInMaintenanceWindow(specMaintenanceWindows []acidv1.MaintenanceWindow) bool {
|
||||
ignoreMaintenanceWindows := c.OpConfig.EnableMaintenanceWindows != nil && !*c.OpConfig.EnableMaintenanceWindows
|
||||
noWindowsDefined := len(specMaintenanceWindows) == 0 && len(c.OpConfig.MaintenanceWindows) == 0
|
||||
if noWindowsDefined || ignoreMaintenanceWindows {
|
||||
return true
|
||||
}
|
||||
now := time.Now()
|
||||
currentDay := now.Weekday()
|
||||
currentTime := now.Format("15:04")
|
||||
|
||||
for _, window := range specMaintenanceWindows {
|
||||
maintenanceWindows := specMaintenanceWindows
|
||||
if len(maintenanceWindows) == 0 {
|
||||
maintenanceWindows = make([]acidv1.MaintenanceWindow, 0, len(c.OpConfig.MaintenanceWindows))
|
||||
for _, windowStr := range c.OpConfig.MaintenanceWindows {
|
||||
var window acidv1.MaintenanceWindow
|
||||
if err := window.UnmarshalJSON([]byte(windowStr)); err != nil {
|
||||
c.logger.Errorf("could not parse default maintenance window %q: %v", windowStr, err)
|
||||
continue
|
||||
}
|
||||
maintenanceWindows = append(maintenanceWindows, window)
|
||||
}
|
||||
}
|
||||
|
||||
for _, window := range maintenanceWindows {
|
||||
startTime := window.StartTime.Format("15:04")
|
||||
endTime := window.EndTime.Format("15:04")
|
||||
|
||||
|
||||
+79
-11
@@ -288,6 +288,12 @@ func newInheritedAnnotationsCluster(client k8sutil.KubernetesClient) (*Cluster,
|
||||
},
|
||||
}
|
||||
|
||||
// add postgresql cluster to fake client
|
||||
_, err := client.PostgresqlsGetter.Postgresqls(namespace).Create(context.TODO(), &pg, metav1.CreateOptions{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
cluster := New(
|
||||
Config{
|
||||
OpConfig: config.Config{
|
||||
@@ -321,7 +327,7 @@ func newInheritedAnnotationsCluster(client k8sutil.KubernetesClient) (*Cluster,
|
||||
}, client, pg, logger, eventRecorder)
|
||||
cluster.Name = clusterName
|
||||
cluster.Namespace = namespace
|
||||
_, err := cluster.createStatefulSet()
|
||||
_, err = cluster.createStatefulSet()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -651,6 +657,23 @@ func Test_trimCronjobName(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestIsInMaintenanceWindow(t *testing.T) {
|
||||
cluster := New(
|
||||
Config{
|
||||
OpConfig: config.Config{
|
||||
EnableMaintenanceWindows: util.True(),
|
||||
Resources: config.Resources{
|
||||
ClusterLabels: map[string]string{"application": "spilo"},
|
||||
ClusterNameLabel: "cluster-name",
|
||||
DefaultCPURequest: "300m",
|
||||
DefaultCPULimit: "300m",
|
||||
DefaultMemoryRequest: "300Mi",
|
||||
DefaultMemoryLimit: "300Mi",
|
||||
},
|
||||
},
|
||||
}, k8sutil.KubernetesClient{}, acidv1.Postgresql{}, logger, eventRecorder)
|
||||
cluster.Name = clusterName
|
||||
cluster.Namespace = namespace
|
||||
|
||||
now := time.Now()
|
||||
futureTimeStart := now.Add(1 * time.Hour)
|
||||
futureTimeStartFormatted := futureTimeStart.Format("15:04")
|
||||
@@ -658,14 +681,31 @@ func TestIsInMaintenanceWindow(t *testing.T) {
|
||||
futureTimeEndFormatted := futureTimeEnd.Format("15:04")
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
windows []acidv1.MaintenanceWindow
|
||||
expected bool
|
||||
name string
|
||||
windows []acidv1.MaintenanceWindow
|
||||
configWindows []string
|
||||
windowsFlag bool
|
||||
expected bool
|
||||
}{
|
||||
{
|
||||
name: "no maintenance windows",
|
||||
windows: nil,
|
||||
expected: true,
|
||||
name: "no maintenance windows",
|
||||
windows: nil,
|
||||
configWindows: nil,
|
||||
windowsFlag: true,
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "maintenance windows diabled",
|
||||
windows: []acidv1.MaintenanceWindow{
|
||||
{
|
||||
Everyday: true,
|
||||
StartTime: mustParseTime("00:00"),
|
||||
EndTime: mustParseTime("23:59"),
|
||||
},
|
||||
},
|
||||
configWindows: nil,
|
||||
windowsFlag: false,
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "maintenance windows with everyday",
|
||||
@@ -676,7 +716,9 @@ func TestIsInMaintenanceWindow(t *testing.T) {
|
||||
EndTime: mustParseTime("23:59"),
|
||||
},
|
||||
},
|
||||
expected: true,
|
||||
configWindows: nil,
|
||||
windowsFlag: true,
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "maintenance windows with weekday",
|
||||
@@ -687,7 +729,9 @@ func TestIsInMaintenanceWindow(t *testing.T) {
|
||||
EndTime: mustParseTime("23:59"),
|
||||
},
|
||||
},
|
||||
expected: true,
|
||||
configWindows: nil,
|
||||
windowsFlag: true,
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "maintenance windows with future interval time",
|
||||
@@ -698,14 +742,38 @@ func TestIsInMaintenanceWindow(t *testing.T) {
|
||||
EndTime: mustParseTime(futureTimeEndFormatted),
|
||||
},
|
||||
},
|
||||
expected: false,
|
||||
windowsFlag: true,
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "global maintenance windows with future interval time",
|
||||
windows: nil,
|
||||
configWindows: []string{fmt.Sprintf("%s-%s", futureTimeStartFormatted, futureTimeEndFormatted)},
|
||||
windowsFlag: true,
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "global maintenance windows all day",
|
||||
windows: nil,
|
||||
configWindows: []string{"00:00-02:00", "02:00-23:59"},
|
||||
windowsFlag: true,
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "global maintenance windows ignored",
|
||||
windows: nil,
|
||||
configWindows: []string{"00:00-02:00", "02:00-23:59"},
|
||||
windowsFlag: false,
|
||||
expected: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
cluster.OpConfig.EnableMaintenanceWindows = &tt.windowsFlag
|
||||
cluster.OpConfig.MaintenanceWindows = tt.configWindows
|
||||
cluster.Spec.MaintenanceWindows = tt.windows
|
||||
if isInMaintenanceWindow(cluster.Spec.MaintenanceWindows) != tt.expected {
|
||||
if cluster.isInMaintenanceWindow(cluster.Spec.MaintenanceWindows) != tt.expected {
|
||||
t.Errorf("Expected isInMaintenanceWindow to return %t", tt.expected)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -39,7 +39,7 @@ func (c *Cluster) syncVolumes() error {
|
||||
} else {
|
||||
err = c.syncUnderlyingEBSVolume()
|
||||
if err != nil {
|
||||
c.logger.Errorf("errors occured during EBS volume adjustments: %v", err)
|
||||
c.logger.Errorf("errors occurred during EBS volume adjustments: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user