diff --git a/postgresql/cutover/test/pgha-dryrun.yaml b/postgresql/cutover/test/pgha-dryrun.yaml index 4b0bbd7..1a252a0 100644 --- a/postgresql/cutover/test/pgha-dryrun.yaml +++ b/postgresql/cutover/test/pgha-dryrun.yaml @@ -16,8 +16,11 @@ # 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. Same -# reasoning applies to $$PGDATA below. +# 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. # # NOTE: ETCD3_HOSTS not ETCD_HOSTS — confirmed against zalando/spilo # configure_spilo.py: PATRONI_DCS includes both "etcd" (legacy v2 API, @@ -28,18 +31,28 @@ # v3-API client. # # NOTE: "legacy" needs a pg_hba.conf rule permitting REPLICATION-type -# connections, which is a distinct connection class from normal client -# connections in Postgres. The vanilla postgres:17 image's default -# pg_hba.conf allows normal client connections (pg_dumpall worked earlier) -# but NOT replication connections, which pg_basebackup (used by Patroni's -# CLONE_WITH_BASEBACKUP) requires. Fixed via a /docker-entrypoint-initdb.d/ -# hook script (the officially supported extension point, runs once right -# after initdb, before the server's final restart) that appends a -# permissive "trust" rule for replication connections. "trust" is -# acceptable ONLY because this container is fully disposable and exists -# solely to validate cutover mechanics, not auth. See the note -# "ADR-0001 Addendum — pg_hba.conf replication prerequisite discovered in -# dry run" for the real production prerequisite this exposes. +# connections (distinct from normal client connections), fixed via a +# /docker-entrypoint-initdb.d/ hook script (official extension point). +# "trust" is acceptable ONLY because this container is fully disposable. +# See note "ADR-0001 Addendum — pg_hba.conf replication prerequisite +# discovered in dry run" for the real production prerequisite this exposes. +# +# 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, NOLOGIN "postgres" role first, then execs Spilo's +# real, UNMODIFIED post_init.sh with all original arguments passed through +# — Zalando's script itself is never patched or forked. This affects real +# production too (superuser there is also PGadmin, not postgres) and must +# be carried into the real staging/production compose files as well. # ───────────────────────────────────────────────────────────────────────── version: "3.6" @@ -156,6 +169,14 @@ services: export PGPASSWORD_STANDBY="$$(cat /run/secrets/postgresql_replication_password)" export PATRONI_RESTAPI_PASSWORD="$$(cat /run/secrets/postgresql_patroni_password)" export CLONE_PASSWORD="$$(cat /run/secrets/postgresql_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; 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: pgha-test @@ -170,6 +191,9 @@ services: CLONE_HOST: postgresql CLONE_PORT: "5432" CLONE_USER: PGadmin + SPILO_CONFIGURATION: | + bootstrap: + post_init: /scripts/post_init_wrapper.sh "zalandos" secrets: - postgresql_password - postgresql_replication_password @@ -194,6 +218,14 @@ services: export PGPASSWORD_STANDBY="$$(cat /run/secrets/postgresql_replication_password)" export PATRONI_RESTAPI_PASSWORD="$$(cat /run/secrets/postgresql_patroni_password)" export CLONE_PASSWORD="$$(cat /run/secrets/postgresql_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; 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: pgha-test @@ -208,6 +240,9 @@ services: CLONE_HOST: postgresql CLONE_PORT: "5432" CLONE_USER: PGadmin + SPILO_CONFIGURATION: | + bootstrap: + post_init: /scripts/post_init_wrapper.sh "zalandos" secrets: - postgresql_password - postgresql_replication_password