Merge d762e707f4 into a7aaad0a0b
This commit is contained in:
commit
287d0d7976
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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_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`.
|
||||||
|
|
|
||||||
|
|
@ -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:
|
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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
@ -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'
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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,
|
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
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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]")
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue