feat(pooler): create/sync/delete generated config map in lifecycle

This commit is contained in:
Mitch Murphy 2026-06-05 14:41:11 -04:00
parent c5e0b43a36
commit e79c7d3ca3
3 changed files with 114 additions and 1 deletions

View File

@ -773,9 +773,28 @@ func (c *Cluster) deleteConnectionPooler(role PostgresRole) (err error) {
c.logger.Infof("connection pooler auth secret %s has been deleted for role %s", authSecret.Name, role)
}
// Repeat the same for the generated config map
configMap := c.ConnectionPooler[role].ConfigMap
if configMap == nil {
c.logger.Debug("no connection pooler config map object to delete")
} else {
err = c.KubeClient.
ConfigMaps(c.Namespace).
Delete(context.TODO(), configMap.Name, options)
if k8sutil.ResourceNotFound(err) {
c.logger.Debugf("connection pooler config map %s for role %s has already been deleted", configMap.Name, role)
} else if err != nil {
return fmt.Errorf("could not delete connection pooler config map: %v", err)
}
c.logger.Infof("connection pooler config map %s has been deleted for role %s", configMap.Name, role)
}
c.ConnectionPooler[role].AuthSecret = nil
c.ConnectionPooler[role].Deployment = nil
c.ConnectionPooler[role].Service = nil
c.ConnectionPooler[role].ConfigMap = nil
return nil
}
@ -799,6 +818,40 @@ func (c *Cluster) deleteConnectionPoolerSecret() (err error) {
return nil
}
// syncConnectionPoolerConfigMap reconciles the operator-generated pgbouncer
// config map for the given role: create if missing, update on drift.
func (c *Cluster) syncConnectionPoolerConfigMap(role PostgresRole) error {
desired, err := c.generateConnectionPoolerConfigMap(role)
if err != nil {
return fmt.Errorf("could not generate connection pooler config map: %v", err)
}
existing, err := c.KubeClient.ConfigMaps(c.Namespace).Get(context.TODO(), desired.Name, metav1.GetOptions{})
if k8sutil.ResourceNotFound(err) {
created, cErr := c.KubeClient.ConfigMaps(c.Namespace).Create(context.TODO(), desired, metav1.CreateOptions{})
if cErr != nil {
return fmt.Errorf("could not create connection pooler config map: %v", cErr)
}
c.ConnectionPooler[role].ConfigMap = created
return nil
} else if err != nil {
return fmt.Errorf("could not get connection pooler config map: %v", err)
}
if !reflect.DeepEqual(existing.Data, desired.Data) {
desired.ResourceVersion = existing.ResourceVersion
updated, uErr := c.KubeClient.ConfigMaps(c.Namespace).Update(context.TODO(), desired, metav1.UpdateOptions{})
if uErr != nil {
return fmt.Errorf("could not update connection pooler config map: %v", uErr)
}
c.ConnectionPooler[role].ConfigMap = updated
return nil
}
c.ConnectionPooler[role].ConfigMap = existing
return nil
}
// Perform actual patching of a connection pooler deployment, assuming that all
// the check were already done before.
func updateConnectionPoolerDeployment(KubeClient k8sutil.KubernetesClient, newDeployment *appsv1.Deployment, doUpdate bool) (*appsv1.Deployment, error) {
@ -1146,6 +1199,14 @@ func (c *Cluster) syncConnectionPoolerWorker(oldSpec, newSpec *acidv1.Postgresql
c.ConnectionPooler[role].AuthSecret = authSecret
}
// reconcile the generated pgbouncer config map before the deployment so the
// mounted config exists when pods start
if c.OpConfig.ConnectionPooler.GenerateConfig {
if cmErr := c.syncConnectionPoolerConfigMap(role); cmErr != nil {
return NoSync, cmErr
}
}
// next the pooler deployment
deployment, err = c.KubeClient.
Deployments(c.Namespace).
@ -1206,7 +1267,10 @@ func (c *Cluster) syncConnectionPoolerWorker(oldSpec, newSpec *acidv1.Postgresql
syncReason = append(syncReason, specReason...)
}
newPodAnnotations := c.annotationsSet(c.generatePodAnnotations(&c.Spec))
newPodAnnotations, annErr := c.connectionPoolerPodAnnotations(role)
if annErr != nil {
return nil, fmt.Errorf("could not generate pod annotations for connection pooler: %v", annErr)
}
deletedPodAnnotations := []string{}
if changed, reason := c.compareAnnotations(deployment.Spec.Template.Annotations, newPodAnnotations, &deletedPodAnnotations); changed {
specSync = true

View File

@ -31,6 +31,7 @@ func newFakeK8sPoolerTestClient() (k8sutil.KubernetesClient, *fake.Clientset) {
DeploymentsGetter: clientSet.AppsV1(),
ServicesGetter: clientSet.CoreV1(),
SecretsGetter: clientSet.CoreV1(),
ConfigMapsGetter: clientSet.CoreV1(),
}, clientSet
}

View File

@ -1,6 +1,7 @@
package cluster
import (
"context"
"strings"
"testing"
@ -9,6 +10,7 @@ import (
"github.com/zalando/postgres-operator/pkg/util/config"
"github.com/zalando/postgres-operator/pkg/util/k8sutil"
v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
func newGenerateConfigCluster() *Cluster {
@ -182,3 +184,49 @@ func TestPoolerPodTemplateGeneratedConfigOff(t *testing.T) {
t.Errorf("did not expect checksum annotation when GenerateConfig is off")
}
}
func TestSyncConnectionPoolerConfigMap(t *testing.T) {
client, _ := newFakeK8sPoolerTestClient()
maxDB := int32(60)
instances := int32(2)
pg := acidv1.Postgresql{
ObjectMeta: metav1.ObjectMeta{Name: "acid-test", Namespace: "default"},
Spec: acidv1.PostgresSpec{
EnableConnectionPooler: boolToPointer(true),
ConnectionPooler: &acidv1.ConnectionPooler{},
},
}
cluster := New(
Config{OpConfig: config.Config{
ConnectionPooler: config.ConnectionPooler{
User: "pooler", Schema: "pooler", Mode: "transaction",
MaxDBConnections: &maxDB, NumberOfInstances: &instances,
GenerateConfig: true, AuthType: "scram-sha-256",
ConfigPath: "/etc/pgbouncer/pgbouncer.ini",
Args: []string{"/etc/pgbouncer/pgbouncer.ini"},
},
}},
client, pg, logger, eventRecorder)
cluster.Name = "acid-test"
cluster.Namespace = "default"
cluster.Spec = pg.Spec
cluster.ConnectionPooler = map[PostgresRole]*ConnectionPoolerObjects{
Master: {Name: cluster.connectionPoolerName(Master), Namespace: "default", Role: Master},
}
if err := cluster.syncConnectionPoolerConfigMap(Master); err != nil {
t.Fatalf("unexpected error: %v", err)
}
name := cluster.connectionPoolerName(Master) + "-config"
cm, err := client.ConfigMaps("default").Get(context.TODO(), name, metav1.GetOptions{})
if err != nil {
t.Fatalf("config map not created: %v", err)
}
if _, ok := cm.Data["pgbouncer.ini"]; !ok {
t.Errorf("config map missing pgbouncer.ini")
}
if err := cluster.syncConnectionPoolerConfigMap(Master); err != nil {
t.Fatalf("unexpected error on resync: %v", err)
}
}