From 8641fbd9de8f33f03a614ae0cc688abe1345c442 Mon Sep 17 00:00:00 2001 From: Mitch Murphy Date: Fri, 5 Jun 2026 14:28:29 -0400 Subject: [PATCH] feat(pooler): mount generated config and override command/args --- pkg/cluster/connection_pooler.go | 35 +++++++++++++++++- pkg/cluster/pgbouncer_config.go | 17 +++++++++ pkg/cluster/pgbouncer_config_test.go | 55 ++++++++++++++++++++++++++++ 3 files changed, 106 insertions(+), 1 deletion(-) diff --git a/pkg/cluster/connection_pooler.go b/pkg/cluster/connection_pooler.go index ca740e12c..9f0525ef1 100644 --- a/pkg/cluster/connection_pooler.go +++ b/pkg/cluster/connection_pooler.go @@ -34,6 +34,7 @@ type ConnectionPoolerObjects struct { AuthSecret *v1.Secret Deployment *appsv1.Deployment Service *v1.Service + ConfigMap *v1.ConfigMap Name string ClusterName string Namespace string @@ -466,6 +467,33 @@ func (c *Cluster) generateConnectionPoolerPodTemplate(role PostgresRole) ( } } + // When enabled, mount an operator-generated pgbouncer.ini and override the + // container command/args (e.g. for images like the Chainguard FIPS pgbouncer + // whose entrypoint is the bare binary with no config-rendering wrapper). + if c.OpConfig.ConnectionPooler.GenerateConfig { + configVolumeName := fmt.Sprintf("%s-config", c.connectionPoolerName(role)) + poolerVolumes = append(poolerVolumes, v1.Volume{ + Name: configVolumeName, + VolumeSource: v1.VolumeSource{ + ConfigMap: &v1.ConfigMapVolumeSource{ + LocalObjectReference: v1.LocalObjectReference{ + Name: configVolumeName, + }, + }, + }, + }) + volumeMounts = append(volumeMounts, v1.VolumeMount{ + Name: configVolumeName, + MountPath: c.OpConfig.ConnectionPooler.ConfigPath, + SubPath: pgBouncerConfigFileName, + ReadOnly: true, + }) + if len(c.OpConfig.ConnectionPooler.Command) > 0 { + poolerContainer.Command = c.OpConfig.ConnectionPooler.Command + } + poolerContainer.Args = c.OpConfig.ConnectionPooler.Args + } + poolerContainer.Env = envVars poolerContainer.VolumeMounts = volumeMounts tolerationsSpec := tolerations(&spec.Tolerations, c.OpConfig.PodToleration) @@ -483,11 +511,16 @@ func (c *Cluster) generateConnectionPoolerPodTemplate(role PostgresRole) ( securityContext.FSGroup = effectiveFSGroup } + podAnnotations, annErr := c.connectionPoolerPodAnnotations(role) + if annErr != nil { + return nil, annErr + } + podTemplate := &v1.PodTemplateSpec{ ObjectMeta: metav1.ObjectMeta{ Labels: c.connectionPoolerLabels(role, true).MatchLabels, Namespace: c.Namespace, - Annotations: c.annotationsSet(c.generatePodAnnotations(spec)), + Annotations: podAnnotations, }, Spec: v1.PodSpec{ TerminationGracePeriodSeconds: &gracePeriod, diff --git a/pkg/cluster/pgbouncer_config.go b/pkg/cluster/pgbouncer_config.go index e6172ef74..eef6e0859 100644 --- a/pkg/cluster/pgbouncer_config.go +++ b/pkg/cluster/pgbouncer_config.go @@ -167,3 +167,20 @@ func (c *Cluster) generateConnectionPoolerConfigMap(role PostgresRole) (*v1.Conf }, }, nil } + +// connectionPoolerPodAnnotations returns the pooler pod annotations, adding the +// config checksum when generated config is enabled so config changes roll pods. +func (c *Cluster) connectionPoolerPodAnnotations(role PostgresRole) (map[string]string, error) { + annotations := c.annotationsSet(c.generatePodAnnotations(&c.Spec)) + if c.OpConfig.ConnectionPooler.GenerateConfig { + checksum, err := c.connectionPoolerConfigChecksum(role) + if err != nil { + return nil, err + } + if annotations == nil { + annotations = map[string]string{} + } + annotations[poolerConfigChecksumAnnotation] = checksum + } + return annotations, nil +} diff --git a/pkg/cluster/pgbouncer_config_test.go b/pkg/cluster/pgbouncer_config_test.go index fd68c9a5b..efe85b5b2 100644 --- a/pkg/cluster/pgbouncer_config_test.go +++ b/pkg/cluster/pgbouncer_config_test.go @@ -8,6 +8,7 @@ import ( "github.com/zalando/postgres-operator/pkg/util" "github.com/zalando/postgres-operator/pkg/util/config" "github.com/zalando/postgres-operator/pkg/util/k8sutil" + v1 "k8s.io/api/core/v1" ) func newGenerateConfigCluster() *Cluster { @@ -127,3 +128,57 @@ func TestGenerateConnectionPoolerConfigMap(t *testing.T) { t.Errorf("config map should have owner references") } } + +func findVolumeMount(mounts []v1.VolumeMount, path string) *v1.VolumeMount { + for i := range mounts { + if mounts[i].MountPath == path { + return &mounts[i] + } + } + return nil +} + +func TestPoolerPodTemplateGeneratedConfigOn(t *testing.T) { + cluster := newGenerateConfigCluster() + + tmpl, err := cluster.generateConnectionPoolerPodTemplate(Master) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + container := tmpl.Spec.Containers[0] + + mount := findVolumeMount(container.VolumeMounts, "/etc/pgbouncer/pgbouncer.ini") + if mount == nil { + t.Fatalf("expected a volume mount at /etc/pgbouncer/pgbouncer.ini") + } + if mount.SubPath != "pgbouncer.ini" { + t.Errorf("expected subPath pgbouncer.ini, got %q", mount.SubPath) + } + if len(container.Args) != 1 || container.Args[0] != "/etc/pgbouncer/pgbouncer.ini" { + t.Errorf("expected args [/etc/pgbouncer/pgbouncer.ini], got %#v", container.Args) + } + if _, ok := tmpl.Annotations[poolerConfigChecksumAnnotation]; !ok { + t.Errorf("expected checksum annotation on pod template") + } +} + +func TestPoolerPodTemplateGeneratedConfigOff(t *testing.T) { + cluster := newGenerateConfigCluster() + cluster.OpConfig.ConnectionPooler.GenerateConfig = false + + tmpl, err := cluster.generateConnectionPoolerPodTemplate(Master) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + container := tmpl.Spec.Containers[0] + + if findVolumeMount(container.VolumeMounts, "/etc/pgbouncer/pgbouncer.ini") != nil { + t.Errorf("did not expect config mount when GenerateConfig is off") + } + if len(container.Args) != 0 { + t.Errorf("did not expect args when GenerateConfig is off, got %#v", container.Args) + } + if _, ok := tmpl.Annotations[poolerConfigChecksumAnnotation]; ok { + t.Errorf("did not expect checksum annotation when GenerateConfig is off") + } +}