diff --git a/pkg/cluster/connection_pooler.go b/pkg/cluster/connection_pooler.go index 9198f3c68..b128555c0 100644 --- a/pkg/cluster/connection_pooler.go +++ b/pkg/cluster/connection_pooler.go @@ -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 diff --git a/pkg/cluster/connection_pooler_test.go b/pkg/cluster/connection_pooler_test.go index a346201dc..fc527d96b 100644 --- a/pkg/cluster/connection_pooler_test.go +++ b/pkg/cluster/connection_pooler_test.go @@ -31,6 +31,7 @@ func newFakeK8sPoolerTestClient() (k8sutil.KubernetesClient, *fake.Clientset) { DeploymentsGetter: clientSet.AppsV1(), ServicesGetter: clientSet.CoreV1(), SecretsGetter: clientSet.CoreV1(), + ConfigMapsGetter: clientSet.CoreV1(), }, clientSet } diff --git a/pkg/cluster/pgbouncer_config_test.go b/pkg/cluster/pgbouncer_config_test.go index efe85b5b2..f44b0d6af 100644 --- a/pkg/cluster/pgbouncer_config_test.go +++ b/pkg/cluster/pgbouncer_config_test.go @@ -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) + } +}