Merge d762e707f4 into a7aaad0a0b
This commit is contained in:
commit
287d0d7976
|
|
@ -899,6 +899,25 @@ spec:
|
|||
super_username:
|
||||
default: postgres
|
||||
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
|
||||
workers:
|
||||
default: 8
|
||||
|
|
|
|||
|
|
@ -475,6 +475,15 @@ configConnectionPooler:
|
|||
connection_pooler_default_memory_request: 100Mi
|
||||
connection_pooler_default_cpu_limit: "1"
|
||||
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:
|
||||
# enable Patroni DCS failsafe_mode feature
|
||||
|
|
|
|||
|
|
@ -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`.
|
||||
|
|
@ -1095,3 +1095,27 @@ operator being able to provide some reasonable defaults.
|
|||
**connection_pooler_default_cpu_limit**
|
||||
**connection_pooler_default_memory_limit**
|
||||
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`.
|
||||
|
|
|
|||
|
|
@ -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: {}
|
||||
|
|
@ -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: {}
|
||||
|
|
@ -899,6 +899,25 @@ spec:
|
|||
super_username:
|
||||
default: postgres
|
||||
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
|
||||
workers:
|
||||
default: 8
|
||||
|
|
|
|||
|
|
@ -233,5 +233,11 @@ configuration:
|
|||
connection_pooler_number_of_instances: 2
|
||||
# connection_pooler_schema: "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:
|
||||
enable_patroni_failsafe_mode: false
|
||||
|
|
|
|||
|
|
@ -2180,9 +2180,6 @@ spec:
|
|||
pattern: ^(\d+|\*)(/\d+)?(\s+(\d+|\*)(/\d+)?){4}$
|
||||
type: string
|
||||
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
|
||||
masterNodePort:
|
||||
format: int32
|
||||
|
|
@ -2896,17 +2893,6 @@ spec:
|
|||
format: int64
|
||||
type: integer
|
||||
standby:
|
||||
anyOf:
|
||||
- required:
|
||||
- s3_wal_path
|
||||
- required:
|
||||
- gs_wal_path
|
||||
- required:
|
||||
- standby_host
|
||||
not:
|
||||
required:
|
||||
- s3_wal_path
|
||||
- gs_wal_path
|
||||
description: |-
|
||||
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).
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -9,6 +9,7 @@ nav:
|
|||
- Postgres Operator UI: 'operator-ui.md'
|
||||
- Admin guide: 'administrator.md'
|
||||
- User guide: 'user.md'
|
||||
- PgBouncer generated config: 'pgbouncer-generated-config.md'
|
||||
- Developer guide: 'developer.md'
|
||||
- Reference:
|
||||
- Config parameters: 'reference/operator_parameters.md'
|
||||
|
|
|
|||
|
|
@ -345,6 +345,15 @@ type ConnectionPoolerConfiguration struct {
|
|||
DefaultCPULimit string `json:"connection_pooler_default_cpu_limit,omitempty"`
|
||||
// +kubebuilder:validation:Pattern=`^(\d+(e\d+)?|\d+(\.\d+)?(e\d+)?[EPTGMK]i?)$`
|
||||
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
|
||||
|
|
|
|||
|
|
@ -142,6 +142,21 @@ func (in *ConnectionPoolerConfiguration) DeepCopyInto(out *ConnectionPoolerConfi
|
|||
*out = new(int32)
|
||||
**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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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.
|
||||
//
|
||||
// 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.
|
||||
//
|
||||
// RESERVE_SIZE is how many additional connections to allow for a pooler.
|
||||
|
||||
func (c *Cluster) getConnectionPoolerEnvVars() []v1.EnvVar {
|
||||
spec := &c.Spec
|
||||
connectionPoolerSpec := spec.ConnectionPooler
|
||||
|
|
@ -231,27 +277,7 @@ func (c *Cluster) getConnectionPoolerEnvVars() []v1.EnvVar {
|
|||
connectionPoolerSpec.Mode,
|
||||
c.OpConfig.ConnectionPooler.Mode)
|
||||
|
||||
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
|
||||
reserveSize := minSize
|
||||
sizes := c.connectionPoolerSizes()
|
||||
|
||||
return []v1.EnvVar{
|
||||
{
|
||||
|
|
@ -264,23 +290,23 @@ func (c *Cluster) getConnectionPoolerEnvVars() []v1.EnvVar {
|
|||
},
|
||||
{
|
||||
Name: "CONNECTION_POOLER_DEFAULT_SIZE",
|
||||
Value: fmt.Sprint(defaultSize),
|
||||
Value: fmt.Sprint(sizes.defaultSize),
|
||||
},
|
||||
{
|
||||
Name: "CONNECTION_POOLER_MIN_SIZE",
|
||||
Value: fmt.Sprint(minSize),
|
||||
Value: fmt.Sprint(sizes.minSize),
|
||||
},
|
||||
{
|
||||
Name: "CONNECTION_POOLER_RESERVE_SIZE",
|
||||
Value: fmt.Sprint(reserveSize),
|
||||
Value: fmt.Sprint(sizes.reserveSize),
|
||||
},
|
||||
{
|
||||
Name: "CONNECTION_POOLER_MAX_CLIENT_CONN",
|
||||
Value: fmt.Sprint(constants.ConnectionPoolerMaxClientConnections),
|
||||
Value: fmt.Sprint(sizes.maxClientConn),
|
||||
},
|
||||
{
|
||||
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.VolumeMounts = volumeMounts
|
||||
tolerationsSpec := tolerations(&spec.Tolerations, c.OpConfig.PodToleration)
|
||||
|
|
@ -458,11 +511,16 @@ func (c *Cluster) generateConnectionPoolerPodTemplate(role PostgresRole) (
|
|||
securityContext.FSGroup = effectiveFSGroup
|
||||
}
|
||||
|
||||
podAnnotations, err := c.connectionPoolerPodAnnotations(role)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
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,
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
|
|
@ -776,6 +853,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) {
|
||||
|
|
@ -1123,6 +1234,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).
|
||||
|
|
@ -1183,7 +1302,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
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ func newFakeK8sPoolerTestClient() (k8sutil.KubernetesClient, *fake.Clientset) {
|
|||
DeploymentsGetter: clientSet.AppsV1(),
|
||||
ServicesGetter: clientSet.CoreV1(),
|
||||
SecretsGetter: clientSet.CoreV1(),
|
||||
ConfigMapsGetter: clientSet.CoreV1(),
|
||||
}, 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) {
|
||||
testName := "Test connection pooler service type selection"
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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"])
|
||||
}
|
||||
}
|
||||
|
|
@ -285,5 +285,18 @@ func (c *Controller) importConfigurationFromCRD(fromCRD *acidv1.OperatorConfigur
|
|||
fromCRD.ConnectionPooler.MaxDBConnections,
|
||||
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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -165,6 +165,12 @@ type ConnectionPooler struct {
|
|||
ConnectionPoolerDefaultMemoryRequest string `name:"connection_pooler_default_memory_request"`
|
||||
ConnectionPoolerDefaultCPULimit string `name:"connection_pooler_default_cpu_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
|
||||
|
|
|
|||
|
|
@ -6,6 +6,8 @@ import (
|
|||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
|
|
@ -43,10 +45,6 @@ func int32Ptr(i int32) *int32 {
|
|||
return &i
|
||||
}
|
||||
|
||||
func boolPtr(b bool) *bool {
|
||||
return &b
|
||||
}
|
||||
|
||||
var validateTests = []struct {
|
||||
description string
|
||||
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]")
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue