From 7e8e3388f49f07d13e4f43c44bce20b37c54f657 Mon Sep 17 00:00:00 2001 From: admin Date: Thu, 30 Jul 2026 15:55:26 -0700 Subject: [PATCH] postgresql.yaml: port dry-run fixes (bugs 1,2,4,5) - $$(...) escaping, ETCD3_HOSTS, post_init_wrapper.sh SUPERUSER role fix --- postgresql/postgresql.yaml | 83 +++++++++++++++++++++++++++++++++----- 1 file changed, 74 insertions(+), 9 deletions(-) diff --git a/postgresql/postgresql.yaml b/postgresql/postgresql.yaml index 3355f2a..b9fdb35 100644 --- a/postgresql/postgresql.yaml +++ b/postgresql/postgresql.yaml @@ -10,6 +10,47 @@ # read secrets from /run/secrets and export them as the plain env vars # Spilo's configure_spilo.py actually expects, before invoking the image's # real entrypoint chain (/launch.sh init). +# +# NOTE: command: blocks use $$(...) not $(...) — Compose's own variable +# interpolation parses $( as an attempted ${VAR} reference and fails with +# "invalid interpolation format" / "you may need to escape any $ with +# another $". $$ escapes to a literal $ for the shell at runtime. This +# applies uniformly to EVERY literal $ character anywhere in a command: +# block, including inside heredocs and nested quoting — Compose scans the +# raw text before any shell/heredoc logic ever runs, so quoting context +# doesn't exempt anything from this escaping requirement. (Confirmed the +# hard way in the pgha-test dry run — see "ADR-0001 Dry-Run Debugging Log" +# note — this file originally had the unescaped $(...) bug.) +# +# NOTE: ETCD3_HOSTS not ETCD_HOSTS — confirmed against zalando/spilo +# configure_spilo.py: PATRONI_DCS includes both "etcd" (legacy v2 API, +# python-etcd client) and "etcd3" (v3 API, python-etcd3 client) as +# distinct DCS backends selected by env var prefix. Our etcd containers +# (v3.5.9) have the v2 API disabled by default, so ETCD_HOSTS causes +# Patroni to hit /v2 endpoints that 404. ETCD3_HOSTS selects the correct +# v3-API client. (Also originally wrong in this file — fixed after dry run.) +# +# NOTE: patroni-0/patroni-1 override bootstrap.post_init via +# SPILO_CONFIGURATION (Spilo's documented, supported mechanism for +# overriding any generated Patroni config — configure_spilo.py deep-merges +# user-supplied SPILO_CONFIGURATION on top of its own generated config). +# This is needed because Spilo's own /scripts/post_init.sh hardcodes +# "ALTER VIEW ... OWNER TO postgres" with no way to parameterize that role +# name via env vars. Since our superuser is PGUSER_SUPERUSER=PGadmin (to +# match production), there is no role literally named "postgres" in the +# cloned data, so Spilo's unmodified script fails with +# 'ERROR: role "postgres" does not exist'. The override points +# bootstrap.post_init at our own wrapper script instead, which creates a +# harmless, idempotent "postgres" role (WITH SUPERUSER NOLOGIN — SUPERUSER +# is required because Spilo's own _zmon_schema.dump does +# "SET ROLE TO postgres; CREATE EXTENSION plpython3u", which needs real +# superuser privileges, not just role existence; NOLOGIN means it can never +# be used to establish a real connection, so this carries no security +# exposure), then execs Spilo's real, UNMODIFIED post_init.sh with all +# original arguments passed through — Zalando's script itself is never +# patched or forked. Both fixes (role existence + SUPERUSER) were found +# and validated end-to-end in the pgha-test dry run before being ported +# here — see the dry-run debugging note for full detail. # ───────────────────────────────────────────────────────────────────────── version: "3.6" @@ -88,7 +129,9 @@ services: # ── Patroni / Spilo data replicas (2 total, per user constraint) ──────── # Image tag pinned to 4.0-p3 (Zalando postgres-operator's own referenced # default at time of writing) — re-verify against current releases - # before actual deploy, as patch tags move. + # before actual deploy, as patch tags move. Decision (2026-07-30): staying + # on PostgreSQL 17 / this Spilo line for now; PG18 deferred as a separate + # future project pending app-compatibility checks. See ADR-0001 notes. patroni-0: image: ghcr.io/zalando/spilo-17:4.0-p3 hostname: patroni-0 @@ -96,17 +139,28 @@ services: - /bin/sh - -c - | - export PGPASSWORD_SUPERUSER="$(cat /run/secrets/postgresql_password)" - export PGPASSWORD_STANDBY="$(cat /run/secrets/postgresql_replication_password)" - export PATRONI_RESTAPI_PASSWORD="$(cat /run/secrets/postgresql_patroni_password)" + export PGPASSWORD_SUPERUSER="$$(cat /run/secrets/postgresql_password)" + export PGPASSWORD_STANDBY="$$(cat /run/secrets/postgresql_replication_password)" + export PATRONI_RESTAPI_PASSWORD="$$(cat /run/secrets/postgresql_patroni_password)" + mkdir -p /scripts + cat > /scripts/post_init_wrapper.sh <<'WRAP' + #!/bin/bash + set -e + psql -d "$$2" -v ON_ERROR_STOP=1 -c 'DO $$do$$ BEGIN IF NOT EXISTS (SELECT 1 FROM pg_catalog.pg_roles WHERE rolname = $$x$$postgres$$x$$) THEN CREATE ROLE postgres WITH SUPERUSER NOLOGIN; END IF; END $$do$$;' + exec /scripts/post_init.sh "$$@" + WRAP + chmod +x /scripts/post_init_wrapper.sh exec /bin/sh /launch.sh init environment: SCOPE: postgres-ha PATRONI_NAME: patroni-0 - ETCD_HOSTS: '"etcd-1:2379","etcd-2:2379","etcd-3:2379"' + ETCD3_HOSTS: '"etcd-1:2379","etcd-2:2379","etcd-3:2379"' PGUSER_STANDBY: standby PATRONI_RESTAPI_USERNAME: patroni PGROOT: /home/postgres/pgdata/pgroot + SPILO_CONFIGURATION: | + bootstrap: + post_init: /scripts/post_init_wrapper.sh "zalandos" # CLONE_* vars intentionally omitted here — enabled only at cutover # (Phase 3) to seed from the existing 41GB single instance via # pg_basebackup. See ADR-0001 note before cutover. @@ -130,17 +184,28 @@ services: - /bin/sh - -c - | - export PGPASSWORD_SUPERUSER="$(cat /run/secrets/postgresql_password)" - export PGPASSWORD_STANDBY="$(cat /run/secrets/postgresql_replication_password)" - export PATRONI_RESTAPI_PASSWORD="$(cat /run/secrets/postgresql_patroni_password)" + export PGPASSWORD_SUPERUSER="$$(cat /run/secrets/postgresql_password)" + export PGPASSWORD_STANDBY="$$(cat /run/secrets/postgresql_replication_password)" + export PATRONI_RESTAPI_PASSWORD="$$(cat /run/secrets/postgresql_patroni_password)" + mkdir -p /scripts + cat > /scripts/post_init_wrapper.sh <<'WRAP' + #!/bin/bash + set -e + psql -d "$$2" -v ON_ERROR_STOP=1 -c 'DO $$do$$ BEGIN IF NOT EXISTS (SELECT 1 FROM pg_catalog.pg_roles WHERE rolname = $$x$$postgres$$x$$) THEN CREATE ROLE postgres WITH SUPERUSER NOLOGIN; END IF; END $$do$$;' + exec /scripts/post_init.sh "$$@" + WRAP + chmod +x /scripts/post_init_wrapper.sh exec /bin/sh /launch.sh init environment: SCOPE: postgres-ha PATRONI_NAME: patroni-1 - ETCD_HOSTS: '"etcd-1:2379","etcd-2:2379","etcd-3:2379"' + ETCD3_HOSTS: '"etcd-1:2379","etcd-2:2379","etcd-3:2379"' PGUSER_STANDBY: standby PATRONI_RESTAPI_USERNAME: patroni PGROOT: /home/postgres/pgdata/pgroot + SPILO_CONFIGURATION: | + bootstrap: + post_init: /scripts/post_init_wrapper.sh "zalandos" secrets: - postgresql_password - postgresql_replication_password