This commit is contained in:
Mitch Murphy 2026-07-31 08:16:36 -04:00 committed by GitHub
commit 287d0d7976
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
20 changed files with 6791 additions and 47 deletions

View File

@ -899,6 +899,25 @@ spec:
super_username: super_username:
default: postgres default: postgres
type: string type: string
pattern: '^(\d+(e\d+)?|\d+(\.\d+)?(e\d+)?[EPTGMK]i?)$'
connection_pooler_generate_config:
type: boolean
default: false
connection_pooler_command:
type: array
items:
type: string
connection_pooler_args:
type: array
items:
type: string
connection_pooler_auth_type:
type: string
default: "scram-sha-256"
connection_pooler_config_path:
type: string
default: "/etc/pgbouncer/pgbouncer.ini"
patroni:
type: object type: object
workers: workers:
default: 8 default: 8

View File

@ -475,6 +475,15 @@ configConnectionPooler:
connection_pooler_default_memory_request: 100Mi connection_pooler_default_memory_request: 100Mi
connection_pooler_default_cpu_limit: "1" connection_pooler_default_cpu_limit: "1"
connection_pooler_default_memory_limit: 100Mi connection_pooler_default_memory_limit: 100Mi
# whether the operator should generate the pgbouncer.ini config map and
# override the pooler container command/args (needed for images without an
# entrypoint that renders the config, e.g. the Chainguard FIPS pgbouncer image)
connection_pooler_generate_config: false
# connection_pooler_command: []
# connection_pooler_args:
# - "/etc/pgbouncer/pgbouncer.ini"
connection_pooler_auth_type: "scram-sha-256"
connection_pooler_config_path: "/etc/pgbouncer/pgbouncer.ini"
configPatroni: configPatroni:
# enable Patroni DCS failsafe_mode feature # enable Patroni DCS failsafe_mode feature

View File

@ -0,0 +1,133 @@
<h1>Operator-generated PgBouncer config (Helm)</h1>
By default the connection pooler relies on the PgBouncer image's entrypoint to render `pgbouncer.ini` from environment variables (this is what the bundled `ghcr.io/zalando/postgres-operator/pgbouncer` image does). Some images — for example the **Chainguard FIPS PgBouncer** image — ship no such entrypoint.
When `connection_pooler_generate_config` is enabled, the operator renders the config itself instead of relying on the image. For every pooler it:
- renders `pgbouncer.ini` and stores it in a ConfigMap named `<pooler>-config` (e.g. `acid-minimal-cluster-pooler-config`);
- mounts that ConfigMap into the pooler container at `connection_pooler_config_path` using a `subPath`;
- overrides the container `command`/`args` (when set) so PgBouncer reads the mounted file;
- stamps the pod template with an `acid.zalan.do/pgbouncer-config-checksum` annotation, so the pooler restarts automatically when the rendered config changes.
The feature is **opt-in**; with the default `connection_pooler_generate_config: false` nothing changes for existing clusters.
## 1. Configure the operator via the Helm chart
These settings are operator-wide defaults and live under `configConnectionPooler` in the chart's `values.yaml`.
```yaml
configConnectionPooler:
# Point the pooler at an image whose entrypoint does NOT render pgbouncer.ini.
# Replace with your actual image reference.
connection_pooler_image: "cgr.dev/chainguard/pgbouncer-fips:latest"
# Let the operator render and own pgbouncer.ini.
connection_pooler_generate_config: true
# Optional: override the container entrypoint. Leave unset to keep the image's
# own entrypoint. Set it when the image has no entrypoint that starts pgbouncer.
connection_pooler_command:
- "pgbouncer"
# Args are applied only when generate_config is true. The default already points
# pgbouncer at the mounted config file, so you usually don't need to change it.
connection_pooler_args:
- "/etc/pgbouncer/pgbouncer.ini"
# Written into the generated pgbouncer.ini.
connection_pooler_auth_type: "scram-sha-256"
# Where the ConfigMap is mounted (and where args/command should point).
connection_pooler_config_path: "/etc/pgbouncer/pgbouncer.ini"
```
Install or upgrade the operator with these values:
```bash
helm upgrade --install postgres-operator ./charts/postgres-operator \
--namespace postgres-operator --create-namespace \
-f values-pooler.yaml
```
Or set individual values inline:
```bash
helm upgrade --install postgres-operator ./charts/postgres-operator \
--namespace postgres-operator --create-namespace \
--set configConnectionPooler.connection_pooler_generate_config=true \
--set configConnectionPooler.connection_pooler_image="cgr.dev/chainguard/pgbouncer-fips:latest"
```
| Value (under `configConnectionPooler`) | Default | Purpose |
|---|---|---|
| `connection_pooler_generate_config` | `false` | Master switch — render `pgbouncer.ini` into an operator-owned ConfigMap. |
| `connection_pooler_command` | _(unset)_ | Container `command` override; unset keeps the image entrypoint. Applied only when generating. |
| `connection_pooler_args` | `["/etc/pgbouncer/pgbouncer.ini"]` | Container `args`; applied only when generating. |
| `connection_pooler_auth_type` | `scram-sha-256` | `auth_type` written into the rendered config. |
| `connection_pooler_config_path` | `/etc/pgbouncer/pgbouncer.ini` | Mount path of the generated config. |
## 2. Enable the pooler on a Postgres cluster
The operator settings above only take effect for clusters that actually run a pooler. Enable it in the `postgresql` manifest:
```yaml
apiVersion: "acid.zalan.do/v1"
kind: postgresql
metadata:
name: acid-minimal-cluster
namespace: default
spec:
teamId: "acid"
postgresql:
version: "17"
numberOfInstances: 2
volume:
size: 1Gi
# Run a master connection pooler for this cluster.
enableConnectionPooler: true
# Optionally also pool replica connections:
# enableReplicaConnectionPooler: true
# Per-cluster pooler overrides are optional; defaults come from the operator config.
connectionPooler:
numberOfInstances: 2
mode: "transaction"
```
## 3. Verify
```bash
# The operator-owned config map for the master pooler:
kubectl get configmap acid-minimal-cluster-pooler-config -o yaml
# Inspect the rendered pgbouncer.ini:
kubectl get configmap acid-minimal-cluster-pooler-config \
-o jsonpath='{.data.pgbouncer\.ini}'
# Confirm the pooler pod mounts it and carries the checksum annotation:
kubectl get pod -l connection-pooler=acid-minimal-cluster-pooler \
-o jsonpath='{.items[0].metadata.annotations.acid\.zalan\.do/pgbouncer-config-checksum}'
```
A rendered config looks roughly like:
```ini
[databases]
* = host=acid-minimal-cluster port=5432
[pgbouncer]
pool_mode = transaction
auth_type = scram-sha-256
auth_file = /etc/pgbouncer/userlist.txt
auth_query = SELECT * FROM pooler.user_lookup($1)
server_tls_sslmode = require
default_pool_size = 15
max_db_connections = 30
```
When the cluster has TLS configured (`spec.tls`), the operator additionally renders `client_tls_sslmode`, `client_tls_key_file`, and `client_tls_cert_file`.
## Notes
- `connection_pooler_command` and `connection_pooler_args` are applied **only** when `connection_pooler_generate_config` is `true`. With generation off, the image entrypoint runs unchanged.
- Changing any input that affects the rendered config (mode, auth type, sizes, TLS) updates the ConfigMap and changes the checksum annotation, which rolls the pooler pods automatically.
- The same parameters are available on the `OperatorConfiguration` CRD as `connection_pooler_generate_config`, `connection_pooler_command`, `connection_pooler_args`, `connection_pooler_auth_type`, and `connection_pooler_config_path`.

View File

@ -1095,3 +1095,27 @@ operator being able to provide some reasonable defaults.
**connection_pooler_default_cpu_limit** **connection_pooler_default_cpu_limit**
**connection_pooler_default_memory_limit** **connection_pooler_default_memory_limit**
Default resource configuration for connection pooler deployment. Default resource configuration for connection pooler deployment.
* **connection_pooler_generate_config**
When `true`, the operator renders a `pgbouncer.ini` into an operator-owned
ConfigMap, mounts it into the pooler pod, and overrides the container
command/args. Use for pgbouncer images that do not ship an entrypoint that
renders the config (e.g. FIPS/distroless images). The default `false`
preserves the stock behavior of relying on the image entrypoint.
* **connection_pooler_command**
Container `command` override applied only when `connection_pooler_generate_config`
is enabled. Empty (default) keeps the image entrypoint.
* **connection_pooler_args**
Container `args` applied only when `connection_pooler_generate_config` is
enabled. The default `["/etc/pgbouncer/pgbouncer.ini"]` points pgbouncer at the
mounted config.
* **connection_pooler_auth_type**
`auth_type` written into the generated `pgbouncer.ini`. The default is
`scram-sha-256`.
* **connection_pooler_config_path**
Mount path of the generated `pgbouncer.ini` inside the pooler container. The
default is `/etc/pgbouncer/pgbouncer.ini`.

View File

@ -0,0 +1,986 @@
---
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
annotations:
controller-gen.kubebuilder.io/version: v0.17.3
labels:
app.kubernetes.io/name: postgres-operator
name: operatorconfigurations.acid.zalan.do
spec:
group: acid.zalan.do
names:
categories:
- all
kind: OperatorConfiguration
listKind: OperatorConfigurationList
plural: operatorconfigurations
shortNames:
- opconfig
singular: operatorconfiguration
scope: Namespaced
versions:
- additionalPrinterColumns:
- description: Spilo image to be used for Pods
jsonPath: .configuration.docker_image
name: Image
type: string
- description: Label for K8s resources created by operator
jsonPath: .configuration.kubernetes.cluster_name_label
name: Cluster-Label
type: string
- description: Name of service account to be used
jsonPath: .configuration.kubernetes.pod_service_account_name
name: Service-Account
type: string
- description: Minimum number of instances per Postgres cluster
jsonPath: .configuration.min_instances
name: Min-Instances
type: integer
- description: Age of the OperatorConfiguration resource
jsonPath: .metadata.creationTimestamp
name: Age
type: date
name: v1
schema:
openAPIV3Schema:
description: OperatorConfiguration defines the specification for the OperatorConfiguration.
properties:
apiVersion:
description: |-
APIVersion defines the versioned schema of this representation of an object.
Servers should convert recognized schemas to the latest internal value, and
may reject unrecognized values.
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources
type: string
configuration:
description: OperatorConfigurationData defines the operation config
properties:
aws_or_gcp:
description: AWSGCPConfiguration defines the configuration for AWS
properties:
additional_secret_mount:
type: string
additional_secret_mount_path:
type: string
aws_region:
default: eu-central-1
type: string
enable_ebs_gp3_migration:
type: boolean
enable_ebs_gp3_migration_max_size:
format: int64
type: integer
gcp_credentials:
type: string
kube_iam_role:
type: string
log_s3_bucket:
type: string
wal_az_storage_account:
type: string
wal_gs_bucket:
type: string
wal_s3_bucket:
type: string
type: object
connection_pooler:
description: ConnectionPoolerConfiguration defines default configuration
for connection pooler
properties:
connection_pooler_args:
default:
- /etc/pgbouncer/pgbouncer.ini
items:
type: string
type: array
connection_pooler_auth_type:
default: scram-sha-256
type: string
connection_pooler_command:
items:
type: string
type: array
connection_pooler_config_path:
default: /etc/pgbouncer/pgbouncer.ini
type: string
connection_pooler_default_cpu_limit:
pattern: ^(\d+m|\d+(\.\d{1,3})?)$
type: string
connection_pooler_default_cpu_request:
pattern: ^(\d+m|\d+(\.\d{1,3})?)$
type: string
connection_pooler_default_memory_limit:
pattern: ^(\d+(e\d+)?|\d+(\.\d+)?(e\d+)?[EPTGMK]i?)$
type: string
connection_pooler_default_memory_request:
pattern: ^(\d+(e\d+)?|\d+(\.\d+)?(e\d+)?[EPTGMK]i?)$
type: string
connection_pooler_generate_config:
default: false
type: boolean
connection_pooler_image:
default: ghcr.io/zalando/postgres-operator/pgbouncer:latest
type: string
connection_pooler_max_db_connections:
format: int32
type: integer
connection_pooler_mode:
default: transaction
enum:
- session
- transaction
type: string
connection_pooler_number_of_instances:
default: 2
format: int32
minimum: 1
type: integer
connection_pooler_schema:
default: pooler
type: string
connection_pooler_user:
default: pooler
type: string
type: object
crd_categories:
items:
type: string
type: array
debug:
description: OperatorDebugConfiguration defines options for the debug
mode
properties:
debug_logging:
default: true
type: boolean
enable_database_access:
default: true
type: boolean
type: object
docker_image:
default: ghcr.io/zalando/spilo-18:4.1-p1
type: string
enable_crd_registration:
default: true
type: boolean
enable_lazy_spilo_upgrade:
type: boolean
enable_maintenance_windows:
default: true
type: boolean
enable_pgversion_env_var:
default: true
type: boolean
enable_shm_volume:
default: true
type: boolean
enable_spilo_wal_path_compat:
type: boolean
enable_team_id_clustername_prefix:
type: boolean
etcd_host:
default: ""
type: string
ignore_instance_limits_annotation_key:
type: string
ignore_resources_limits_annotation_key:
type: string
kubernetes:
description: KubernetesMetaConfiguration defines k8s conf required
for all Postgres clusters and the operator itself
properties:
additional_pod_capabilities:
items:
type: string
type: array
cluster_domain:
default: cluster.local
type: string
cluster_labels:
additionalProperties:
type: string
default:
application: spilo
type: object
cluster_name_label:
default: cluster-name
type: string
custom_pod_annotations:
additionalProperties:
type: string
type: object
delete_annotation_date_key:
type: string
delete_annotation_name_key:
type: string
downscaler_annotations:
items:
type: string
type: array
enable_cross_namespace_secret:
type: boolean
enable_finalizers:
type: boolean
enable_init_containers:
default: true
type: boolean
enable_owner_references:
type: boolean
enable_persistent_volume_claim_deletion:
default: true
type: boolean
enable_pod_antiaffinity:
type: boolean
enable_pod_disruption_budget:
default: true
type: boolean
enable_readiness_probe:
type: boolean
enable_secrets_deletion:
default: true
type: boolean
enable_sidecars:
default: true
type: boolean
ignored_annotations:
items:
type: string
type: array
infrastructure_roles_secret_name:
description: |-
NamespacedName comprises a resource name, with a mandatory namespace,
rendered as "<namespace>/<name>". Being a type captures intent and
helps make sure that UIDs, namespaced names and non-namespaced names
do not get conflated in code. For most use cases, namespace and name
will already have been format validated at the API entry point, so we
don't do that here. Where that's not the case (e.g. in testing),
consider using NamespacedNameOrDie() in testing.go in this package.
from: https://github.com/kubernetes/apimachinery/blob/master/pkg/types/namespacedname.go
properties:
name:
type: string
namespace:
type: string
required:
- name
type: object
infrastructure_roles_secrets:
description: namespaced name of the secret containing infrastructure
roles names and passwords
items:
properties:
defaultrolevalue:
type: string
defaultuservalue:
type: string
details:
description: This field point out the detailed yaml definition
of the role, if exists
type: string
passwordkey:
type: string
rolekey:
type: string
secretname:
description: |-
Name of a secret which describes the role, and optionally name of a
configmap with an extra information
properties:
name:
type: string
namespace:
type: string
required:
- name
type: object
template:
type: boolean
userkey:
type: string
type: object
type: array
inherited_annotations:
items:
type: string
type: array
inherited_labels:
items:
type: string
type: array
liveness_probe:
description: |-
Probe describes a health check to be performed against a container to determine whether it is
alive or ready to receive traffic.
properties:
exec:
description: Exec specifies a command to execute in the container.
properties:
command:
description: |-
Command is the command line to execute inside the container, the working directory for the
command is root ('/') in the container's filesystem. The command is simply exec'd, it is
not run inside a shell, so traditional shell instructions ('|', etc) won't work. To use
a shell, you need to explicitly call out to that shell.
Exit status of 0 is treated as live/healthy and non-zero is unhealthy.
items:
type: string
type: array
x-kubernetes-list-type: atomic
type: object
failureThreshold:
description: |-
Minimum consecutive failures for the probe to be considered failed after having succeeded.
Defaults to 3. Minimum value is 1.
format: int32
type: integer
grpc:
description: GRPC specifies a GRPC HealthCheckRequest.
properties:
port:
description: Port number of the gRPC service. Number must
be in the range 1 to 65535.
format: int32
type: integer
service:
default: ""
description: |-
Service is the name of the service to place in the gRPC HealthCheckRequest
(see https://github.com/grpc/grpc/blob/master/doc/health-checking.md).
If this is not specified, the default behavior is defined by gRPC.
type: string
required:
- port
type: object
httpGet:
description: HTTPGet specifies an HTTP GET request to perform.
properties:
host:
description: |-
Host name to connect to, defaults to the pod IP. You probably want to set
"Host" in httpHeaders instead.
type: string
httpHeaders:
description: Custom headers to set in the request. HTTP
allows repeated headers.
items:
description: HTTPHeader describes a custom header to
be used in HTTP probes
properties:
name:
description: |-
The header field name.
This will be canonicalized upon output, so case-variant names will be understood as the same header.
type: string
value:
description: The header field value
type: string
required:
- name
- value
type: object
type: array
x-kubernetes-list-type: atomic
path:
description: Path to access on the HTTP server.
type: string
port:
anyOf:
- type: integer
- type: string
description: |-
Name or number of the port to access on the container.
Number must be in the range 1 to 65535.
Name must be an IANA_SVC_NAME.
x-kubernetes-int-or-string: true
scheme:
description: |-
Scheme to use for connecting to the host.
Defaults to HTTP.
type: string
required:
- port
type: object
initialDelaySeconds:
description: |-
Number of seconds after the container has started before liveness probes are initiated.
More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes
format: int32
type: integer
periodSeconds:
description: |-
How often (in seconds) to perform the probe.
Default to 10 seconds. Minimum value is 1.
format: int32
type: integer
successThreshold:
description: |-
Minimum consecutive successes for the probe to be considered successful after having failed.
Defaults to 1. Must be 1 for liveness and startup. Minimum value is 1.
format: int32
type: integer
tcpSocket:
description: TCPSocket specifies a connection to a TCP port.
properties:
host:
description: 'Optional: Host name to connect to, defaults
to the pod IP.'
type: string
port:
anyOf:
- type: integer
- type: string
description: |-
Number or name of the port to access on the container.
Number must be in the range 1 to 65535.
Name must be an IANA_SVC_NAME.
x-kubernetes-int-or-string: true
required:
- port
type: object
terminationGracePeriodSeconds:
description: |-
Optional duration in seconds the pod needs to terminate gracefully upon probe failure.
The grace period is the duration in seconds after the processes running in the pod are sent
a termination signal and the time when the processes are forcibly halted with a kill signal.
Set this value longer than the expected cleanup time for your process.
If this value is nil, the pod's terminationGracePeriodSeconds will be used. Otherwise, this
value overrides the value provided by the pod spec.
Value must be non-negative integer. The value zero indicates stop immediately via
the kill signal (no opportunity to shut down).
This is a beta field and requires enabling ProbeTerminationGracePeriod feature gate.
Minimum value is 1. spec.terminationGracePeriodSeconds is used if unset.
format: int64
type: integer
timeoutSeconds:
description: |-
Number of seconds after which the probe times out.
Defaults to 1 second. Minimum value is 1.
More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes
format: int32
type: integer
type: object
master_pod_move_timeout:
default: 20m
description: timeout for successful migration of master pods from
unschedulable node
type: string
node_readiness_label:
additionalProperties:
type: string
type: object
node_readiness_label_merge:
enum:
- AND
- OR
type: string
oauth_token_secret_name:
default: postgres-operator
description: namespaced name of the secret containing the OAuth2
token to pass to the teams API
properties:
name:
type: string
namespace:
type: string
required:
- name
type: object
pdb_master_label_selector:
default: true
type: boolean
pdb_name_format:
default: postgres-{cluster}-pdb
description: defines the template for PDB names
type: string
persistent_volume_claim_retention_policy:
additionalProperties:
type: string
type: object
pod_antiaffinity_preferred_during_scheduling:
type: boolean
pod_antiaffinity_topology_key:
default: kubernetes.io/hostname
type: string
pod_environment_configmap:
description: namespaced name of the ConfigMap with environment
variables to populate on every pod
properties:
name:
type: string
namespace:
type: string
required:
- name
type: object
pod_environment_secret:
type: string
pod_management_policy:
default: ordered_ready
enum:
- ordered_ready
- parallel
type: string
pod_priority_class_name:
type: string
pod_role_label:
default: spilo-role
type: string
pod_service_account_definition:
type: string
pod_service_account_name:
default: postgres-pod
type: string
pod_service_account_role_binding_definition:
type: string
pod_terminate_grace_period:
default: 5m
description: Postgres pods are terminated forcefully after this
timeout
type: string
secret_name_template:
default: '{username}.{cluster}.credentials.{tprkind}.{tprgroup}'
description: |-
template for database user secrets generated by the operator,
here username contains the namespace in the format namespace.username
if the user is in different namespace than cluster and cross namespace secrets
are enabled via `enable_cross_namespace_secret` flag in the configuration.
type: string
share_pgsocket_with_sidecars:
type: boolean
spilo_allow_privilege_escalation:
default: true
type: boolean
spilo_fsgroup:
format: int64
type: integer
spilo_privileged:
type: boolean
spilo_runasgroup:
format: int64
type: integer
spilo_runasuser:
format: int64
type: integer
storage_resize_mode:
default: pvc
enum:
- ebs
- mixed
- pvc
- "off"
type: string
toleration:
additionalProperties:
type: string
type: object
watched_namespace:
type: string
type: object
kubernetes_use_configmaps:
default: true
type: boolean
load_balancer:
description: LoadBalancerConfiguration defines the LB configuration
properties:
custom_service_annotations:
additionalProperties:
type: string
type: object
db_hosted_zone:
type: string
enable_master_load_balancer:
type: boolean
enable_master_node_port:
type: boolean
enable_master_pooler_load_balancer:
type: boolean
enable_master_pooler_node_port:
type: boolean
enable_replica_load_balancer:
type: boolean
enable_replica_node_port:
type: boolean
enable_replica_pooler_load_balancer:
type: boolean
enable_replica_pooler_node_port:
type: boolean
external_traffic_policy:
default: Cluster
enum:
- Cluster
- Local
type: string
master_dns_name_format:
default: '{cluster}.{namespace}.{hostedzone}'
description: defines the DNS name string template for the master
load balancer cluster
type: string
master_legacy_dns_name_format:
default: '{cluster}.{team}.{hostedzone}'
description: deprecated DNS template for master load balancer
using team name
type: string
replica_dns_name_format:
default: '{cluster}-repl.{namespace}.{hostedzone}'
description: defines the DNS name string template for the replica
load balancer cluster
type: string
replica_legacy_dns_name_format:
default: '{cluster}-repl.{team}.{hostedzone}'
description: deprecated DNS template for replica load balancer
using team name
type: string
type: object
logging_rest_api:
description: LoggingRESTAPIConfiguration defines Logging API conf
properties:
api_port:
default: 8080
type: integer
cluster_history_entries:
default: 1000
type: integer
ring_log_lines:
default: 100
type: integer
type: object
logical_backup:
description: OperatorLogicalBackupConfiguration defines configuration
for logical backup
properties:
logical_backup_azure_storage_account_key:
type: string
logical_backup_azure_storage_account_name:
type: string
logical_backup_azure_storage_container:
type: string
logical_backup_cpu_limit:
pattern: ^(\d+m|\d+(\.\d{1,3})?)$
type: string
logical_backup_cpu_request:
pattern: ^(\d+m|\d+(\.\d{1,3})?)$
type: string
logical_backup_cronjob_environment_secret:
type: string
logical_backup_docker_image:
default: ghcr.io/zalando/postgres-operator/logical-backup:v1.15.1
type: string
logical_backup_failed_jobs_history_limit:
default: 3
format: int32
minimum: 0
type: integer
logical_backup_google_application_credentials:
type: string
logical_backup_job_prefix:
default: logical-backup-
type: string
logical_backup_memory_limit:
pattern: ^(\d+(e\d+)?|\d+(\.\d+)?(e\d+)?[EPTGMK]i?)$
type: string
logical_backup_memory_request:
pattern: ^(\d+(e\d+)?|\d+(\.\d+)?(e\d+)?[EPTGMK]i?)$
type: string
logical_backup_provider:
default: s3
enum:
- az
- gcs
- s3
type: string
logical_backup_s3_access_key_id:
type: string
logical_backup_s3_bucket:
type: string
logical_backup_s3_bucket_prefix:
type: string
logical_backup_s3_endpoint:
type: string
logical_backup_s3_region:
type: string
logical_backup_s3_retention_time:
type: string
logical_backup_s3_secret_access_key:
type: string
logical_backup_s3_sse:
type: string
logical_backup_schedule:
default: 30 00 * * *
pattern: ^(\d+|\*)(/\d+)?(\s+(\d+|\*)(/\d+)?){4}$
type: string
logical_backup_successful_jobs_history_limit:
default: 3
format: int32
minimum: 0
type: integer
logical_backup_ttl_seconds_after_finished:
default: 86400
format: int32
minimum: 0
type: integer
type: object
maintenance_windows:
items:
pattern: ^\ *((Mon|Tue|Wed|Thu|Fri|Sat|Sun):(2[0-3]|[01]?\d):([0-5]?\d)|(2[0-3]|[01]?\d):([0-5]?\d))-((2[0-3]|[01]?\d):([0-5]?\d)|(2[0-3]|[01]?\d):([0-5]?\d))\
*$
type: string
type: array
major_version_upgrade:
description: MajorVersionUpgradeConfiguration defines how to execute
major version upgrades of Postgres.
properties:
major_version_upgrade_mode:
default: manual
enum:
- "off"
- manual
- full
type: string
major_version_upgrade_team_allow_list:
items:
type: string
type: array
minimal_major_version:
default: "14"
type: string
target_major_version:
default: "18"
type: string
type: object
max_instances:
default: -1
description: -1 = disabled
format: int32
minimum: -1
type: integer
min_instances:
default: -1
description: -1 = disabled
format: int32
minimum: -1
type: integer
patroni:
description: PatroniConfiguration defines configuration for Patroni
properties:
enable_patroni_failsafe_mode:
type: boolean
type: object
postgres_pod_resources:
description: PostgresPodResourcesDefaults defines the spec of default
resources
properties:
default_cpu_limit:
pattern: ^(\d+m|\d+(\.\d{1,3})?)$
type: string
default_cpu_request:
pattern: ^(\d+m|\d+(\.\d{1,3})?)$
type: string
default_memory_limit:
pattern: ^(\d+(e\d+)?|\d+(\.\d+)?(e\d+)?[EPTGMK]i?)$
type: string
default_memory_request:
pattern: ^(\d+(e\d+)?|\d+(\.\d+)?(e\d+)?[EPTGMK]i?)$
type: string
max_cpu_request:
pattern: ^(\d+m|\d+(\.\d{1,3})?)$
type: string
max_memory_request:
pattern: ^(\d+(e\d+)?|\d+(\.\d+)?(e\d+)?[EPTGMK]i?)$
type: string
min_cpu_limit:
pattern: ^(\d+m|\d+(\.\d{1,3})?)$
type: string
min_memory_limit:
pattern: ^(\d+(e\d+)?|\d+(\.\d+)?(e\d+)?[EPTGMK]i?)$
type: string
type: object
repair_period:
default: 5m
description: period between consecutive repair requests
type: string
resync_period:
default: 30m
description: period between consecutive sync requests
type: string
scalyr:
description: ScalyrConfiguration defines the configuration for ScalyrAPI
properties:
scalyr_api_key:
type: string
scalyr_cpu_limit:
default: "1"
pattern: ^(\d+m|\d+(\.\d{1,3})?)$
type: string
scalyr_cpu_request:
default: 100m
pattern: ^(\d+m|\d+(\.\d{1,3})?)$
type: string
scalyr_image:
type: string
scalyr_memory_limit:
default: 500Mi
pattern: ^(\d+(e\d+)?|\d+(\.\d+)?(e\d+)?[EPTGMK]i?)$
type: string
scalyr_memory_request:
default: 50Mi
pattern: ^(\d+(e\d+)?|\d+(\.\d+)?(e\d+)?[EPTGMK]i?)$
type: string
scalyr_server_url:
default: https://upload.eu.scalyr.com
type: string
type: object
set_memory_request_to_limit:
type: boolean
sidecar_docker_images:
additionalProperties:
type: string
type: object
sidecars:
type: object
x-kubernetes-preserve-unknown-fields: true
teams_api:
description: TeamsAPIConfiguration defines the configuration of TeamsAPI
properties:
enable_admin_role_for_users:
default: true
type: boolean
enable_postgres_team_crd:
default: true
type: boolean
enable_postgres_team_crd_superusers:
type: boolean
enable_team_member_deprecation:
type: boolean
enable_team_superuser:
type: boolean
enable_teams_api:
type: boolean
pam_configuration:
default: https://info.example.com/oauth2/tokeninfo?access_token=
uid realm=/employees
type: string
pam_role_name:
default: zalandos
type: string
postgres_superuser_teams:
items:
type: string
type: array
protected_role_names:
default:
- admin
- cron_admin
items:
type: string
type: array
role_deletion_suffix:
default: _deleted
type: string
team_admin_role:
default: admin
type: string
team_api_role_configuration:
additionalProperties:
type: string
default:
log_statement: all
type: object
teams_api_url:
default: https://teams.example.com/api/
type: string
type: object
timeouts:
description: OperatorTimeouts defines the timeout of ResourceCheck,
PodWait, ReadyWait
properties:
patroni_api_check_interval:
default: 1s
description: interval between consecutive attempts of operator
calling the Patroni API
type: string
patroni_api_check_timeout:
default: 5s
description: timeout when waiting for successful response from
Patroni API
type: string
pod_deletion_wait_timeout:
default: 10m
description: timeout when waiting for the Postgres pods to be
deleted
type: string
pod_label_wait_timeout:
default: 10m
description: timeout when waiting for pod role and cluster labels
type: string
ready_wait_interval:
default: 4s
description: interval between consecutive attempts waiting for
postgresql CRD to be created
type: string
ready_wait_timeout:
default: 30s
description: timeout for the complete postgres CRD creation
type: string
resource_check_interval:
default: 3s
description: interval to wait between consecutive attempts to
check for some K8s resources
type: string
resource_check_timeout:
default: 10m
description: timeout when waiting for the presence of a certain
K8s resource
type: string
type: object
users:
description: PostgresUsersConfiguration defines the system users of
Postgres.
properties:
additional_owner_roles:
items:
type: string
type: array
enable_password_rotation:
type: boolean
password_rotation_interval:
default: 90
format: int32
type: integer
password_rotation_user_retention:
default: 120
format: int32
type: integer
replication_username:
default: standby
type: string
super_username:
default: postgres
type: string
type: object
workers:
default: 8
format: int32
minimum: 1
type: integer
type: object
kind:
description: |-
Kind is a string value representing the REST resource this object represents.
Servers may infer this from the endpoint the client submits requests to.
Cannot be updated.
In CamelCase.
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
type: string
metadata:
type: object
required:
- configuration
- metadata
type: object
served: true
storage: true
subresources:
status: {}

View File

@ -0,0 +1,84 @@
---
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
annotations:
controller-gen.kubebuilder.io/version: v0.17.3
labels:
app.kubernetes.io/name: postgres-operator
name: postgresteams.acid.zalan.do
spec:
group: acid.zalan.do
names:
categories:
- all
kind: PostgresTeam
listKind: PostgresTeamList
plural: postgresteams
shortNames:
- pgteam
singular: postgresteam
scope: Namespaced
versions:
- name: v1
schema:
openAPIV3Schema:
description: PostgresTeam defines Custom Resource Definition Object for team
management.
properties:
apiVersion:
description: |-
APIVersion defines the versioned schema of this representation of an object.
Servers should convert recognized schemas to the latest internal value, and
may reject unrecognized values.
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources
type: string
kind:
description: |-
Kind is a string value representing the REST resource this object represents.
Servers may infer this from the endpoint the client submits requests to.
Cannot be updated.
In CamelCase.
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
type: string
metadata:
type: object
spec:
description: PostgresTeamSpec defines the specification for the PostgresTeam
TPR.
properties:
additionalMembers:
additionalProperties:
description: List of users who will also be added to the Postgres
cluster.
items:
type: string
type: array
description: Map for teamId and associated additional users
type: object
additionalSuperuserTeams:
additionalProperties:
description: List of teams to become Postgres superusers
items:
type: string
type: array
description: Map for teamId and associated additional superuser teams
type: object
additionalTeams:
additionalProperties:
description: List of teams whose members will also be added to the
Postgres cluster.
items:
type: string
type: array
description: Map for teamId and associated additional teams
type: object
type: object
required:
- metadata
- spec
type: object
served: true
storage: true
subresources:
status: {}

View File

@ -899,6 +899,25 @@ spec:
super_username: super_username:
default: postgres default: postgres
type: string type: string
pattern: '^(\d+(e\d+)?|\d+(\.\d+)?(e\d+)?[EPTGMK]i?)$'
connection_pooler_generate_config:
type: boolean
default: false
connection_pooler_command:
type: array
items:
type: string
connection_pooler_args:
type: array
items:
type: string
connection_pooler_auth_type:
type: string
default: "scram-sha-256"
connection_pooler_config_path:
type: string
default: "/etc/pgbouncer/pgbouncer.ini"
patroni:
type: object type: object
workers: workers:
default: 8 default: 8

View File

@ -233,5 +233,11 @@ configuration:
connection_pooler_number_of_instances: 2 connection_pooler_number_of_instances: 2
# connection_pooler_schema: "pooler" # connection_pooler_schema: "pooler"
# connection_pooler_user: "pooler" # connection_pooler_user: "pooler"
connection_pooler_generate_config: false
# connection_pooler_command: []
# connection_pooler_args:
# - "/etc/pgbouncer/pgbouncer.ini"
connection_pooler_auth_type: "scram-sha-256"
connection_pooler_config_path: "/etc/pgbouncer/pgbouncer.ini"
patroni: patroni:
enable_patroni_failsafe_mode: false enable_patroni_failsafe_mode: false

View File

@ -2180,9 +2180,6 @@ spec:
pattern: ^(\d+|\*)(/\d+)?(\s+(\d+|\*)(/\d+)?){4}$ pattern: ^(\d+|\*)(/\d+)?(\s+(\d+|\*)(/\d+)?){4}$
type: string type: string
maintenanceWindows: maintenanceWindows:
items:
pattern: '^\ *((Mon|Tue|Wed|Thu|Fri|Sat|Sun):(2[0-3]|[01]?\d):([0-5]?\d)|(2[0-3]|[01]?\d):([0-5]?\d))-((2[0-3]|[01]?\d):([0-5]?\d)|(2[0-3]|[01]?\d):([0-5]?\d))\ *$'
type: string
type: array type: array
masterNodePort: masterNodePort:
format: int32 format: int32
@ -2896,17 +2893,6 @@ spec:
format: int64 format: int64
type: integer type: integer
standby: standby:
anyOf:
- required:
- s3_wal_path
- required:
- gs_wal_path
- required:
- standby_host
not:
required:
- s3_wal_path
- gs_wal_path
description: |- description: |-
StandbyDescription contains remote primary config and/or s3/gs wal path. StandbyDescription contains remote primary config and/or s3/gs wal path.
standby_host can be specified alone or together with either s3_wal_path OR gs_wal_path (mutually exclusive). standby_host can be specified alone or together with either s3_wal_path OR gs_wal_path (mutually exclusive).

File diff suppressed because it is too large Load Diff

View File

@ -9,6 +9,7 @@ nav:
- Postgres Operator UI: 'operator-ui.md' - Postgres Operator UI: 'operator-ui.md'
- Admin guide: 'administrator.md' - Admin guide: 'administrator.md'
- User guide: 'user.md' - User guide: 'user.md'
- PgBouncer generated config: 'pgbouncer-generated-config.md'
- Developer guide: 'developer.md' - Developer guide: 'developer.md'
- Reference: - Reference:
- Config parameters: 'reference/operator_parameters.md' - Config parameters: 'reference/operator_parameters.md'

View File

@ -345,6 +345,15 @@ type ConnectionPoolerConfiguration struct {
DefaultCPULimit string `json:"connection_pooler_default_cpu_limit,omitempty"` DefaultCPULimit string `json:"connection_pooler_default_cpu_limit,omitempty"`
// +kubebuilder:validation:Pattern=`^(\d+(e\d+)?|\d+(\.\d+)?(e\d+)?[EPTGMK]i?)$` // +kubebuilder:validation:Pattern=`^(\d+(e\d+)?|\d+(\.\d+)?(e\d+)?[EPTGMK]i?)$`
DefaultMemoryLimit string `json:"connection_pooler_default_memory_limit,omitempty"` DefaultMemoryLimit string `json:"connection_pooler_default_memory_limit,omitempty"`
// +kubebuilder:default=false
GenerateConfig *bool `json:"connection_pooler_generate_config,omitempty"`
Command []string `json:"connection_pooler_command,omitempty"`
// +kubebuilder:default={"/etc/pgbouncer/pgbouncer.ini"}
Args []string `json:"connection_pooler_args,omitempty"`
// +kubebuilder:default="scram-sha-256"
AuthType string `json:"connection_pooler_auth_type,omitempty"`
// +kubebuilder:default="/etc/pgbouncer/pgbouncer.ini"
ConfigPath string `json:"connection_pooler_config_path,omitempty"`
} }
// OperatorLogicalBackupConfiguration defines configuration for logical backup // OperatorLogicalBackupConfiguration defines configuration for logical backup

View File

@ -142,6 +142,21 @@ func (in *ConnectionPoolerConfiguration) DeepCopyInto(out *ConnectionPoolerConfi
*out = new(int32) *out = new(int32)
**out = **in **out = **in
} }
if in.GenerateConfig != nil {
in, out := &in.GenerateConfig, &out.GenerateConfig
*out = new(bool)
**out = **in
}
if in.Command != nil {
in, out := &in.Command, &out.Command
*out = make([]string, len(*in))
copy(*out, *in)
}
if in.Args != nil {
in, out := &in.Args, &out.Args
*out = make([]string, len(*in))
copy(*out, *in)
}
return return
} }

View File

@ -34,6 +34,7 @@ type ConnectionPoolerObjects struct {
AuthSecret *v1.Secret AuthSecret *v1.Secret
Deployment *appsv1.Deployment Deployment *appsv1.Deployment
Service *v1.Service Service *v1.Service
ConfigMap *v1.ConfigMap
Name string Name string
ClusterName string ClusterName string
Namespace string Namespace string
@ -200,6 +201,52 @@ func (c *Cluster) generateConnectionPoolerAuthSecret(connectionPooler *Connectio
} }
} }
type connectionPoolerSizes struct {
maxDBConn int32
defaultSize int32
minSize int32
reserveSize int32
maxClientConn int32
}
// connectionPoolerSizes computes the pgBouncer pool sizing from the cluster
// spec and operator config. Shared by getConnectionPoolerEnvVars (stock image,
// via env vars) and generatePgBouncerIni (generated config).
func (c *Cluster) connectionPoolerSizes() connectionPoolerSizes {
spec := &c.Spec
connectionPoolerSpec := spec.ConnectionPooler
if connectionPoolerSpec == nil {
connectionPoolerSpec = &acidv1.ConnectionPooler{}
}
numberOfInstances := connectionPoolerSpec.NumberOfInstances
if numberOfInstances == nil {
numberOfInstances = util.CoalesceInt32(
c.OpConfig.ConnectionPooler.NumberOfInstances,
k8sutil.Int32ToPointer(1))
}
effectiveMaxDBConn := util.CoalesceInt32(
connectionPoolerSpec.MaxDBConnections,
c.OpConfig.ConnectionPooler.MaxDBConnections)
if effectiveMaxDBConn == nil {
effectiveMaxDBConn = k8sutil.Int32ToPointer(
constants.ConnectionPoolerMaxDBConnections)
}
maxDBConn := *effectiveMaxDBConn / *numberOfInstances
defaultSize := maxDBConn / 2
minSize := defaultSize / 2
return connectionPoolerSizes{
maxDBConn: maxDBConn,
defaultSize: defaultSize,
minSize: minSize,
reserveSize: minSize,
maxClientConn: constants.ConnectionPoolerMaxClientConnections,
}
}
// Generate pool size related environment variables. // Generate pool size related environment variables.
// //
// MAX_DB_CONN would specify the global maximum for connections to a target // MAX_DB_CONN would specify the global maximum for connections to a target
@ -220,7 +267,6 @@ func (c *Cluster) generateConnectionPoolerAuthSecret(connectionPooler *Connectio
// have to wait for spinning up a new connections. // have to wait for spinning up a new connections.
// //
// RESERVE_SIZE is how many additional connections to allow for a pooler. // RESERVE_SIZE is how many additional connections to allow for a pooler.
func (c *Cluster) getConnectionPoolerEnvVars() []v1.EnvVar { func (c *Cluster) getConnectionPoolerEnvVars() []v1.EnvVar {
spec := &c.Spec spec := &c.Spec
connectionPoolerSpec := spec.ConnectionPooler connectionPoolerSpec := spec.ConnectionPooler
@ -231,27 +277,7 @@ func (c *Cluster) getConnectionPoolerEnvVars() []v1.EnvVar {
connectionPoolerSpec.Mode, connectionPoolerSpec.Mode,
c.OpConfig.ConnectionPooler.Mode) c.OpConfig.ConnectionPooler.Mode)
numberOfInstances := connectionPoolerSpec.NumberOfInstances sizes := c.connectionPoolerSizes()
if numberOfInstances == nil {
numberOfInstances = util.CoalesceInt32(
c.OpConfig.ConnectionPooler.NumberOfInstances,
k8sutil.Int32ToPointer(1))
}
effectiveMaxDBConn := util.CoalesceInt32(
connectionPoolerSpec.MaxDBConnections,
c.OpConfig.ConnectionPooler.MaxDBConnections)
if effectiveMaxDBConn == nil {
effectiveMaxDBConn = k8sutil.Int32ToPointer(
constants.ConnectionPoolerMaxDBConnections)
}
maxDBConn := *effectiveMaxDBConn / *numberOfInstances
defaultSize := maxDBConn / 2
minSize := defaultSize / 2
reserveSize := minSize
return []v1.EnvVar{ return []v1.EnvVar{
{ {
@ -264,23 +290,23 @@ func (c *Cluster) getConnectionPoolerEnvVars() []v1.EnvVar {
}, },
{ {
Name: "CONNECTION_POOLER_DEFAULT_SIZE", Name: "CONNECTION_POOLER_DEFAULT_SIZE",
Value: fmt.Sprint(defaultSize), Value: fmt.Sprint(sizes.defaultSize),
}, },
{ {
Name: "CONNECTION_POOLER_MIN_SIZE", Name: "CONNECTION_POOLER_MIN_SIZE",
Value: fmt.Sprint(minSize), Value: fmt.Sprint(sizes.minSize),
}, },
{ {
Name: "CONNECTION_POOLER_RESERVE_SIZE", Name: "CONNECTION_POOLER_RESERVE_SIZE",
Value: fmt.Sprint(reserveSize), Value: fmt.Sprint(sizes.reserveSize),
}, },
{ {
Name: "CONNECTION_POOLER_MAX_CLIENT_CONN", Name: "CONNECTION_POOLER_MAX_CLIENT_CONN",
Value: fmt.Sprint(constants.ConnectionPoolerMaxClientConnections), Value: fmt.Sprint(sizes.maxClientConn),
}, },
{ {
Name: "CONNECTION_POOLER_MAX_DB_CONN", Name: "CONNECTION_POOLER_MAX_DB_CONN",
Value: fmt.Sprint(maxDBConn), Value: fmt.Sprint(sizes.maxDBConn),
}, },
} }
} }
@ -441,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 := c.connectionPoolerConfigMapName(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.Env = envVars
poolerContainer.VolumeMounts = volumeMounts poolerContainer.VolumeMounts = volumeMounts
tolerationsSpec := tolerations(&spec.Tolerations, c.OpConfig.PodToleration) tolerationsSpec := tolerations(&spec.Tolerations, c.OpConfig.PodToleration)
@ -458,11 +511,16 @@ func (c *Cluster) generateConnectionPoolerPodTemplate(role PostgresRole) (
securityContext.FSGroup = effectiveFSGroup securityContext.FSGroup = effectiveFSGroup
} }
podAnnotations, err := c.connectionPoolerPodAnnotations(role)
if err != nil {
return nil, err
}
podTemplate := &v1.PodTemplateSpec{ podTemplate := &v1.PodTemplateSpec{
ObjectMeta: metav1.ObjectMeta{ ObjectMeta: metav1.ObjectMeta{
Labels: c.connectionPoolerLabels(role, true).MatchLabels, Labels: c.connectionPoolerLabels(role, true).MatchLabels,
Namespace: c.Namespace, Namespace: c.Namespace,
Annotations: c.annotationsSet(c.generatePodAnnotations(spec)), Annotations: podAnnotations,
}, },
Spec: v1.PodSpec{ Spec: v1.PodSpec{
TerminationGracePeriodSeconds: &gracePeriod, TerminationGracePeriodSeconds: &gracePeriod,
@ -750,9 +808,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) 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].AuthSecret = nil
c.ConnectionPooler[role].Deployment = nil c.ConnectionPooler[role].Deployment = nil
c.ConnectionPooler[role].Service = nil c.ConnectionPooler[role].Service = nil
c.ConnectionPooler[role].ConfigMap = nil
return nil return nil
} }
@ -776,6 +853,40 @@ func (c *Cluster) deleteConnectionPoolerSecret() (err error) {
return nil 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 // Perform actual patching of a connection pooler deployment, assuming that all
// the check were already done before. // the check were already done before.
func updateConnectionPoolerDeployment(KubeClient k8sutil.KubernetesClient, newDeployment *appsv1.Deployment, doUpdate bool) (*appsv1.Deployment, error) { func updateConnectionPoolerDeployment(KubeClient k8sutil.KubernetesClient, newDeployment *appsv1.Deployment, doUpdate bool) (*appsv1.Deployment, error) {
@ -1123,6 +1234,14 @@ func (c *Cluster) syncConnectionPoolerWorker(oldSpec, newSpec *acidv1.Postgresql
c.ConnectionPooler[role].AuthSecret = authSecret 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 // next the pooler deployment
deployment, err = c.KubeClient. deployment, err = c.KubeClient.
Deployments(c.Namespace). Deployments(c.Namespace).
@ -1183,7 +1302,10 @@ func (c *Cluster) syncConnectionPoolerWorker(oldSpec, newSpec *acidv1.Postgresql
syncReason = append(syncReason, specReason...) 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{} deletedPodAnnotations := []string{}
if changed, reason := c.compareAnnotations(deployment.Spec.Template.Annotations, newPodAnnotations, &deletedPodAnnotations); changed { if changed, reason := c.compareAnnotations(deployment.Spec.Template.Annotations, newPodAnnotations, &deletedPodAnnotations); changed {
specSync = true specSync = true

View File

@ -31,6 +31,7 @@ func newFakeK8sPoolerTestClient() (k8sutil.KubernetesClient, *fake.Clientset) {
DeploymentsGetter: clientSet.AppsV1(), DeploymentsGetter: clientSet.AppsV1(),
ServicesGetter: clientSet.CoreV1(), ServicesGetter: clientSet.CoreV1(),
SecretsGetter: clientSet.CoreV1(), SecretsGetter: clientSet.CoreV1(),
ConfigMapsGetter: clientSet.CoreV1(),
}, clientSet }, clientSet
} }
@ -1155,6 +1156,34 @@ func TestConnectionPoolerServiceSpec(t *testing.T) {
} }
} }
func TestConnectionPoolerSizes(t *testing.T) {
maxDB := int32(60)
instances := int32(2)
cluster := New(
Config{OpConfig: config.Config{
ConnectionPooler: config.ConnectionPooler{
MaxDBConnections: &maxDB,
NumberOfInstances: &instances,
},
}},
k8sutil.NewMockKubernetesClient(), acidv1.Postgresql{}, logger, eventRecorder)
cluster.Spec = acidv1.PostgresSpec{ConnectionPooler: &acidv1.ConnectionPooler{}}
sizes := cluster.connectionPoolerSizes()
if sizes.maxDBConn != 30 {
t.Errorf("expected maxDBConn 30, got %d", sizes.maxDBConn)
}
if sizes.defaultSize != 15 {
t.Errorf("expected defaultSize 15, got %d", sizes.defaultSize)
}
if sizes.reserveSize != 7 {
t.Errorf("expected reserveSize 7, got %d", sizes.reserveSize)
}
if sizes.minSize != 7 {
t.Errorf("expected minSize 7, got %d", sizes.minSize)
}
}
func TestConnectionPoolerServiceType(t *testing.T) { func TestConnectionPoolerServiceType(t *testing.T) {
testName := "Test connection pooler service type selection" testName := "Test connection pooler service type selection"

View File

@ -0,0 +1,192 @@
package cluster
import (
"bytes"
"crypto/sha256"
"fmt"
"sort"
"strings"
"text/template"
acidv1 "github.com/zalando/postgres-operator/pkg/apis/acid.zalan.do/v1"
"github.com/zalando/postgres-operator/pkg/util"
v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
const (
pgBouncerConfigFileName = "pgbouncer.ini"
poolerConfigChecksumAnnotation = "acid.zalan.do/pgbouncer-config-checksum"
)
// FIPS-friendly pgbouncer.ini. Differs from the stock Zalando image template:
// no logfile/pidfile (distroless logs to stdout), auth_type is configurable,
// and TLS cert lines are emitted only when spec.TLS is set (the FIPS image does
// not run the openssl cert-generation step that the Zalando entrypoint does).
const pgBouncerConfigTemplateText = `# Generated by postgres-operator. Do not edit.
[databases]
* = host={{ .DBHost }} port={{ .DBPort }} auth_user={{ .User }}
postgres = host={{ .DBHost }} port={{ .DBPort }} auth_user={{ .User }}
[pgbouncer]
pool_mode = {{ .Mode }}
listen_port = {{ .ListenPort }}
listen_addr = *
admin_users = {{ .User }}
{{- if .StatsUsers }}
stats_users = {{ .StatsUsers }}
{{- end }}
auth_dbname = postgres
auth_file = /etc/pgbouncer/userlist.txt
auth_query = SELECT * FROM {{ .Schema }}.user_lookup($1)
auth_type = {{ .AuthType }}
server_tls_sslmode = require
{{- if .TLS }}
{{- if .TLSCAFile }}
server_tls_ca_file = {{ .TLSCAFile }}
{{- end }}
client_tls_sslmode = require
client_tls_key_file = {{ .TLSKeyFile }}
client_tls_cert_file = {{ .TLSCertFile }}
{{- end }}
log_connections = 0
log_disconnections = 0
max_prepared_statements = 200
default_pool_size = {{ .DefaultPoolSize }}
reserve_pool_size = {{ .ReservePoolSize }}
max_client_conn = {{ .MaxClientConn }}
max_db_connections = {{ .MaxDBConnections }}
idle_transaction_timeout = 600
server_login_retry = 5
ignore_startup_parameters = extra_float_digits,options
`
var pgBouncerConfigTemplate = template.Must(
template.New(pgBouncerConfigFileName).Parse(pgBouncerConfigTemplateText))
type pgBouncerConfigParams struct {
DBHost string
DBPort int32
ListenPort int32
User string
Schema string
Mode string
AuthType string
StatsUsers string
DefaultPoolSize int32
ReservePoolSize int32
MaxClientConn int32
MaxDBConnections int32
TLS bool
TLSCAFile string
TLSKeyFile string
TLSCertFile string
}
// generatePgBouncerIni renders the pgbouncer.ini for the given role from the
// cluster spec and operator config.
func (c *Cluster) generatePgBouncerIni(role PostgresRole) (string, error) {
spec := &c.Spec
connectionPoolerSpec := spec.ConnectionPooler
if connectionPoolerSpec == nil {
connectionPoolerSpec = &acidv1.ConnectionPooler{}
}
sizes := c.connectionPoolerSizes()
infraRolesList := make([]string, 0)
for infraRoleName := range c.InfrastructureRoles {
infraRolesList = append(infraRolesList, infraRoleName)
}
sort.Strings(infraRolesList) // deterministic output for stable checksums
params := pgBouncerConfigParams{
DBHost: c.serviceAddress(role),
DBPort: c.servicePort(role),
ListenPort: pgPort,
User: util.Coalesce(connectionPoolerSpec.User, c.OpConfig.ConnectionPooler.User),
Schema: util.Coalesce(connectionPoolerSpec.Schema, c.OpConfig.ConnectionPooler.Schema),
Mode: util.Coalesce(connectionPoolerSpec.Mode, c.OpConfig.ConnectionPooler.Mode),
AuthType: c.OpConfig.ConnectionPooler.AuthType,
StatsUsers: strings.Join(infraRolesList, ","),
DefaultPoolSize: sizes.defaultSize,
ReservePoolSize: sizes.reserveSize,
MaxClientConn: sizes.maxClientConn,
MaxDBConnections: sizes.maxDBConn,
}
if spec.TLS != nil && spec.TLS.SecretName != "" {
mountPath := "/tls"
params.TLS = true
params.TLSCertFile = ensurePath(spec.TLS.CertificateFile, mountPath, "tls.crt")
params.TLSKeyFile = ensurePath(spec.TLS.PrivateKeyFile, mountPath, "tls.key")
if spec.TLS.CAFile != "" {
mountPathCA := mountPath
if spec.TLS.CASecretName != "" {
mountPathCA = mountPath + "ca"
}
params.TLSCAFile = ensurePath(spec.TLS.CAFile, mountPathCA, "")
}
}
var buf bytes.Buffer
if err := pgBouncerConfigTemplate.Execute(&buf, params); err != nil {
return "", fmt.Errorf("could not render pgbouncer config: %v", err)
}
return buf.String(), nil
}
// connectionPoolerConfigChecksum returns the sha256 of the rendered config,
// used as a pod annotation so config changes roll the pooler pods.
func (c *Cluster) connectionPoolerConfigChecksum(role PostgresRole) (string, error) {
ini, err := c.generatePgBouncerIni(role)
if err != nil {
return "", err
}
sum := sha256.Sum256([]byte(ini))
return fmt.Sprintf("%x", sum), nil
}
// connectionPoolerConfigMapName returns the name of the operator-generated
// pgbouncer config map for the given role.
func (c *Cluster) connectionPoolerConfigMapName(role PostgresRole) string {
return fmt.Sprintf("%s-config", c.connectionPoolerName(role))
}
// generateConnectionPoolerConfigMap builds the operator-owned ConfigMap holding
// the rendered pgbouncer.ini for the given role.
func (c *Cluster) generateConnectionPoolerConfigMap(role PostgresRole) (*v1.ConfigMap, error) {
ini, err := c.generatePgBouncerIni(role)
if err != nil {
return nil, err
}
return &v1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Name: c.connectionPoolerConfigMapName(role),
Namespace: c.Namespace,
Labels: c.connectionPoolerLabels(role, true).MatchLabels,
Annotations: c.annotationsSet(nil),
OwnerReferences: c.ownerReferences(),
},
Data: map[string]string{
pgBouncerConfigFileName: ini,
},
}, 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
}

View File

@ -0,0 +1,245 @@
package cluster
import (
"context"
"strings"
"testing"
acidv1 "github.com/zalando/postgres-operator/pkg/apis/acid.zalan.do/v1"
"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"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
func newGenerateConfigCluster() *Cluster {
maxDB := int32(60)
instances := int32(2)
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"},
},
Resources: config.Resources{
EnableOwnerReferences: util.True(),
},
}},
k8sutil.NewMockKubernetesClient(), acidv1.Postgresql{}, logger, eventRecorder)
cluster.Name = "acid-test"
cluster.Namespace = "default"
cluster.Spec = acidv1.PostgresSpec{ConnectionPooler: &acidv1.ConnectionPooler{}}
return cluster
}
func TestGeneratePgBouncerIni(t *testing.T) {
cluster := newGenerateConfigCluster()
ini, err := cluster.generatePgBouncerIni(Master)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
for _, want := range []string{
"[databases]",
"[pgbouncer]",
"pool_mode = transaction",
"auth_type = scram-sha-256",
"auth_file = /etc/pgbouncer/userlist.txt",
"auth_query = SELECT * FROM pooler.user_lookup($1)",
"server_tls_sslmode = require",
"default_pool_size = 15",
"max_db_connections = 30",
} {
if !strings.Contains(ini, want) {
t.Errorf("rendered ini missing %q\n---\n%s", want, ini)
}
}
if strings.Contains(ini, "client_tls_cert_file") {
t.Errorf("did not expect client_tls_cert_file without spec.TLS\n%s", ini)
}
}
func TestGeneratePgBouncerIniWithTLS(t *testing.T) {
cluster := newGenerateConfigCluster()
cluster.Spec.TLS = &acidv1.TLSDescription{SecretName: "pg-tls"}
ini, err := cluster.generatePgBouncerIni(Master)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
for _, want := range []string{
"client_tls_sslmode = require",
"client_tls_key_file = /tls/tls.key",
"client_tls_cert_file = /tls/tls.crt",
} {
if !strings.Contains(ini, want) {
t.Errorf("rendered ini missing %q\n---\n%s", want, ini)
}
}
}
func TestConnectionPoolerConfigChecksumStability(t *testing.T) {
cluster := newGenerateConfigCluster()
sum1, err := cluster.connectionPoolerConfigChecksum(Master)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
sum2, err := cluster.connectionPoolerConfigChecksum(Master)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if sum1 != sum2 {
t.Errorf("checksum not stable: %q != %q", sum1, sum2)
}
cluster.OpConfig.ConnectionPooler.AuthType = "md5"
sum3, err := cluster.connectionPoolerConfigChecksum(Master)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if sum1 == sum3 {
t.Errorf("checksum should change when config changes")
}
}
func TestGenerateConnectionPoolerConfigMap(t *testing.T) {
cluster := newGenerateConfigCluster()
cm, err := cluster.generateConnectionPoolerConfigMap(Master)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if cm.Name != cluster.connectionPoolerName(Master)+"-config" {
t.Errorf("unexpected config map name %q", cm.Name)
}
if _, ok := cm.Data["pgbouncer.ini"]; !ok {
t.Errorf("config map missing pgbouncer.ini key, got %#v", cm.Data)
}
if len(cm.OwnerReferences) == 0 {
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")
}
}
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)
}
// drift -> update branch: change a config input that alters the rendered ini
cluster.OpConfig.ConnectionPooler.AuthType = "md5"
if err := cluster.syncConnectionPoolerConfigMap(Master); err != nil {
t.Fatalf("unexpected error on drift resync: %v", err)
}
cm, err = client.ConfigMaps("default").Get(context.TODO(), name, metav1.GetOptions{})
if err != nil {
t.Fatalf("config map not found after update: %v", err)
}
if !strings.Contains(cm.Data["pgbouncer.ini"], "auth_type = md5") {
t.Errorf("expected updated config map to contain auth_type = md5, got:\n%s", cm.Data["pgbouncer.ini"])
}
}

View File

@ -285,5 +285,18 @@ func (c *Controller) importConfigurationFromCRD(fromCRD *acidv1.OperatorConfigur
fromCRD.ConnectionPooler.MaxDBConnections, fromCRD.ConnectionPooler.MaxDBConnections,
k8sutil.Int32ToPointer(constants.ConnectionPoolerMaxDBConnections)) k8sutil.Int32ToPointer(constants.ConnectionPoolerMaxDBConnections))
if fromCRD.ConnectionPooler.GenerateConfig != nil {
result.ConnectionPooler.GenerateConfig = *fromCRD.ConnectionPooler.GenerateConfig
}
// Command is nil when not configured (keeps the image entrypoint)
result.ConnectionPooler.Command = fromCRD.ConnectionPooler.Command
result.ConnectionPooler.Args = util.CoalesceStrArr(
fromCRD.ConnectionPooler.Args,
[]string{"/etc/pgbouncer/pgbouncer.ini"})
result.ConnectionPooler.AuthType = util.Coalesce(
fromCRD.ConnectionPooler.AuthType, "scram-sha-256")
result.ConnectionPooler.ConfigPath = util.Coalesce(
fromCRD.ConnectionPooler.ConfigPath, "/etc/pgbouncer/pgbouncer.ini")
return result return result
} }

View File

@ -165,6 +165,12 @@ type ConnectionPooler struct {
ConnectionPoolerDefaultMemoryRequest string `name:"connection_pooler_default_memory_request"` ConnectionPoolerDefaultMemoryRequest string `name:"connection_pooler_default_memory_request"`
ConnectionPoolerDefaultCPULimit string `name:"connection_pooler_default_cpu_limit"` ConnectionPoolerDefaultCPULimit string `name:"connection_pooler_default_cpu_limit"`
ConnectionPoolerDefaultMemoryLimit string `name:"connection_pooler_default_memory_limit"` ConnectionPoolerDefaultMemoryLimit string `name:"connection_pooler_default_memory_limit"`
GenerateConfig bool `name:"connection_pooler_generate_config" default:"false"`
// Command is nil when not configured (keeps the image entrypoint).
Command []string `name:"connection_pooler_command"`
Args []string `name:"connection_pooler_args" default:"/etc/pgbouncer/pgbouncer.ini"`
AuthType string `name:"connection_pooler_auth_type" default:"scram-sha-256"`
ConfigPath string `name:"connection_pooler_config_path" default:"/etc/pgbouncer/pgbouncer.ini"`
} }
// Config describes operator config // Config describes operator config

View File

@ -6,6 +6,8 @@ import (
"reflect" "reflect"
"strings" "strings"
"testing" "testing"
"github.com/stretchr/testify/assert"
) )
func TestMain(m *testing.M) { func TestMain(m *testing.M) {
@ -43,10 +45,6 @@ func int32Ptr(i int32) *int32 {
return &i return &i
} }
func boolPtr(b bool) *bool {
return &b
}
var validateTests = []struct { var validateTests = []struct {
description string description string
cfg Config cfg Config
@ -335,3 +333,27 @@ func TestNewFromMap(t *testing.T) {
}) })
} }
} }
func TestConnectionPoolerGenerateConfigDefaults(t *testing.T) {
cfg := NewFromMap(map[string]string{})
assert.Equal(t, false, cfg.ConnectionPooler.GenerateConfig, "expected GenerateConfig default false")
assert.Equal(t, "scram-sha-256", cfg.ConnectionPooler.AuthType, "expected AuthType scram-sha-256")
assert.Equal(t, "/etc/pgbouncer/pgbouncer.ini", cfg.ConnectionPooler.ConfigPath, "expected ConfigPath /etc/pgbouncer/pgbouncer.ini")
assert.Equal(t, []string{"/etc/pgbouncer/pgbouncer.ini"}, cfg.ConnectionPooler.Args, "expected Args [/etc/pgbouncer/pgbouncer.ini]")
cfg2 := NewFromMap(map[string]string{
"connection_pooler_generate_config": "true",
"connection_pooler_auth_type": "md5",
"connection_pooler_args": "/custom/pgbouncer.ini",
"connection_pooler_config_path": "/custom/pgbouncer.ini",
"connection_pooler_command": "/usr/bin/pgbouncer",
})
if !cfg2.ConnectionPooler.GenerateConfig {
assert.Equal(t, true, cfg2.ConnectionPooler.GenerateConfig, "expected GenerateConfig true")
}
assert.Equal(t, "md5", cfg2.ConnectionPooler.AuthType, "expected AuthType md5")
assert.Equal(t, []string{"/custom/pgbouncer.ini"}, cfg2.ConnectionPooler.Args, "expected Args [/custom/pgbouncer.ini]")
assert.Equal(t, "/custom/pgbouncer.ini", cfg2.ConnectionPooler.ConfigPath, "expected ConfigPath /custom/pgbouncer.ini")
assert.Equal(t, []string{"/usr/bin/pgbouncer"}, cfg2.ConnectionPooler.Command, "expected Command [/usr/bin/pgbouncer]")
}