fix(infra): own pg_ident mappings in an app-specific moments.conf
Some checks failed
deploy / Build prerendered web (push) Successful in 6m59s
deploy / Build api + worker (static musl) (push) Failing after 11m22s
deploy / Deploy web to oolon (push) Successful in 28s
deploy / Deploy moments-api to nikola (push) Has been skipped
deploy / Deploy moments-worker to frootmig (push) Has been skipped
refresh / Rebuild prerendered web (push) Successful in 6m49s
refresh / Deploy refreshed web to oolon (push) Successful in 19s

Write the cert_cn -> role mappings to pg_ident.conf.d/moments.conf
instead of per-host <cert_cn>.conf files, so provisioning for other
apps that own drop-ins for the same hosts can never clobber or drop
moments access. db-perms.sh now also migrates any moments_ro/rw lines
found in legacy host-named files into moments.conf verbatim (deleting
legacy files left empty), corrects the standby hostname to
frankie.hanzalova.internal, and warns-and-continues on an unreachable
pg host instead of aborting the run.

Closes #3

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Dcsi987fK5ftZEP6w3HXEd
This commit is contained in:
2026-07-02 09:29:00 +03:00
parent fd33acd33a
commit 3d7cbd90d6
2 changed files with 59 additions and 12 deletions

View File

@@ -1,11 +1,16 @@
# moments — pg_ident.conf.d drop-in template
# Rendered by script/deploy.sh per host based on manifest.yml.
# Install path: /var/lib/pgsql/18/data/pg_ident.conf.d/{{HOST_FQDN}}.conf
# Maintained by script/db-perms.sh (idempotent; run from a workstation).
# Install path: /var/lib/pgsql/18/data/pg_ident.conf.d/moments.conf
#
# The file is named for the app (moments.conf), NOT the cert_cn host, so
# provisioning for other apps — which may own drop-ins for the same hosts —
# can never clobber or interleave with moments access.
#
# Apply with: sudo systemctl reload postgresql-18 — on BOTH magrathea
# (primary) and frankie (standby) so failover doesn't lock the app out.
#
# One line per (host, role) mapping. A host that runs both moments-api
# and moments-worker will have two lines (one for moments_ro, one for
# moments_rw).
# One line per (cert_cn host, role) mapping. A host that runs both
# moments-api and moments-worker will have two lines (one for moments_ro,
# one for moments_rw).
cert_cn {{HOST_FQDN}} {{DB_ROLE}}

View File

@@ -1,8 +1,14 @@
#!/usr/bin/env bash
#
# Idempotently add cert_cn → role mappings to pg_ident.conf.d on the moments
# postgres primary and standby, then reload postgres so the changes take
# effect. Re-running is a no-op (no duplicate lines, no spurious reload).
# Idempotently maintain the moments cert_cn → role mappings in an app-owned
# pg_ident.conf.d/moments.conf on the moments postgres primary and standby,
# then reload postgres so the changes take effect. Re-running is a no-op
# (no duplicate lines, no spurious reload).
#
# The file is named for the app, not the cert_cn host, so provisioning for
# other apps (which may own files for the same hosts) can never clobber or
# interleave with moments access. Any moments_* lines found in legacy
# host-named files are migrated here and removed from the legacy file.
#
# Run from a workstation with ssh access to both pg hosts. This script ssh's
# out; do NOT run it on magrathea/frankie directly.
@@ -14,32 +20,63 @@ worker_host=frootmig.kosherinata.internal
pg_hosts=(
magrathea.kosherinata.internal
frankie.kosherinata.internal
frankie.hanzalova.internal
)
# Each (cert_cn host, role) pair becomes one cert_cn line in
# pg_ident.conf.d/<cert_cn host>.conf on every pg host listed above.
# pg_ident.conf.d/moments.conf on every pg host listed above.
mapping_pairs=(
"$api_host" moments_ro
"$worker_host" moments_rw
)
ident_dir=/var/lib/pgsql/18/data/pg_ident.conf.d
app_file=moments.conf
failed=0
for pg_host in "${pg_hosts[@]}"; do
printf '==> %s\n' "$pg_host"
ssh -o BatchMode=yes "$pg_host" "sudo bash -s -- ${ident_dir@Q} ${mapping_pairs[@]@Q}" <<'REMOTE_EOF'
if ! ssh -o BatchMode=yes -o ConnectTimeout=5 "$pg_host" \
"sudo bash -s -- ${ident_dir@Q} ${app_file@Q} ${mapping_pairs[@]@Q}" <<'REMOTE_EOF'
set -euo pipefail
ident_dir="$1"; shift
file="${ident_dir}/$1"; shift
changed=0
# Migrate any moments_* lines out of legacy host-named files into the
# app-owned file, so other apps' provisioning (which may own files for the
# same hosts) can never clobber or drop moments access. Lines are preserved
# verbatim (this covers mappings beyond mapping_pairs, e.g. a workstation's).
# A legacy file left empty is removed.
moments_re='^cert_cn[[:space:]]+[^[:space:]]+[[:space:]]+moments_(ro|rw)[[:space:]]*$'
for legacy in "$ident_dir"/*.conf; do
[[ "$legacy" == "$file" ]] && continue
grep --extended-regexp --quiet "$moments_re" "$legacy" || continue
while read -r line; do
if [[ -f "$file" ]] && grep --fixed-strings --line-regexp --quiet -- "$line" "$file"; then
printf ' already in %s, dropping from %s: %s\n' "${file##*/}" "${legacy##*/}" "$line"
else
printf '%s\n' "$line" | sudo -u postgres tee --append "$file" >/dev/null
printf ' migrated from %s: %s\n' "${legacy##*/}" "$line"
fi
done < <(grep --extended-regexp "$moments_re" "$legacy")
remaining="$(grep --extended-regexp --invert-match "$moments_re" "$legacy" || true)"
if [[ -n "$remaining" ]]; then
printf '%s\n' "$remaining" | sudo -u postgres tee "$legacy" >/dev/null
else
rm "$legacy"
printf ' removed empty legacy file %s\n' "${legacy##*/}"
fi
changed=1
done
while [[ $# -gt 0 ]]; do
cert_cn_host="$1"
role="$2"
shift 2
line="cert_cn ${cert_cn_host} ${role}"
file="${ident_dir}/${cert_cn_host}.conf"
# The heredoc runs as root via sudo bash, so [[ -f ]] and grep are fine
# without dropping privs. tee --append runs as postgres so a newly-created
@@ -60,4 +97,9 @@ else
echo " no changes; reload skipped"
fi
REMOTE_EOF
then
echo " WARNING: ${pg_host} unreachable or failed — re-run when it is back"
failed=1
fi
done
exit "$failed"