Add PostgreSQL deployment tooling and release validation

This commit is contained in:
Lyncoln
2026-09-17 11:10:52 -04:00
parent bfd2e1e7b2
commit 71f5537782
8 changed files with 896 additions and 0 deletions
+572
View File
@@ -0,0 +1,572 @@
#!/usr/bin/env bash
set -Eeuo pipefail
log() {
printf '[postgres-ha-bootstrap] %s\n' "$*" >&2
}
die() {
log "ERROR: $*"
exit 1
}
require_var() {
local name="$1"
[[ -n "${!name:-}" ]] || die "Required environment variable $name is not set"
}
require_file() {
local path="$1"
[[ -r "$path" ]] || die "Required file is not readable: $path"
}
as_postgres() {
runuser -u postgres -- "$@"
}
# PostgreSQL replication slot names are intentionally restricted here
# so values can safely be used in SQL and configuration.
validate_slot_name() {
[[ "$1" =~ ^[a-z0-9_]+$ ]] ||
die "Invalid replication slot name: $1"
}
install_secret_for_postgres() {
local source="$1"
local destination="$2"
require_file "$source"
install \
-o postgres \
-g postgres \
-m 0600 \
"$source" \
"$destination"
}
pgdata_is_initialized() {
[[ -s "$PGDATA/PG_VERSION" ]] &&
[[ -s "$PGDATA/global/pg_control" ]]
}
pgdata_is_empty() {
[[ ! -d "$PGDATA" ]] && return 0
local first
first="$(find "$PGDATA" -mindepth 1 -maxdepth 1 -print -quit 2>/dev/null || true)"
[[ -z "$first" ]]
}
marker_path() {
printf '%s/%s\n' \
"$PGDATA" \
"${POSTGRES_BOOTSTRAP_MARKER:-.loxthelion-ha-initialized}"
}
write_managed_config() {
local config="$PGDATA/postgres-ha.conf"
cat > "$config" <<EOF_CONFIG
# Managed by postgres-ha-bootstrap.
# Do not place node-role-specific settings in this file.
listen_addresses = '*'
wal_level = '${POSTGRES_WAL_LEVEL:-replica}'
max_wal_senders = ${POSTGRES_MAX_WAL_SENDERS:-10}
max_replication_slots = ${POSTGRES_MAX_REPLICATION_SLOTS:-10}
hot_standby = ${POSTGRES_HOT_STANDBY:-on}
password_encryption = 'scram-sha-256'
EOF_CONFIG
chown postgres:postgres "$config"
chmod 0600 "$config"
if ! grep -Eq \
"^[[:space:]]*include_if_exists[[:space:]]*=[[:space:]]*'postgres-ha\.conf'" \
"$PGDATA/postgresql.conf"; then
cat >> "$PGDATA/postgresql.conf" <<'EOF_CONFIG'
# Loxthelion PostgreSQL HA configuration
include_if_exists = 'postgres-ha.conf'
EOF_CONFIG
fi
}
write_managed_hba() {
local hba="$PGDATA/pg_hba.conf"
local tmp
tmp="$(mktemp)"
# Remove a previous block generated by this script.
awk '
/^# BEGIN LOXTHELION POSTGRES HA$/ { skip=1; next }
/^# END LOXTHELION POSTGRES HA$/ { skip=0; next }
!skip { print }
' "$hba" > "$tmp"
cat >> "$tmp" <<EOF_HBA
# BEGIN LOXTHELION POSTGRES HA
host replication ${POSTGRES_REPLICATION_USER} ${POSTGRES_HA_CIDR} scram-sha-256
host postgres ${POSTGRES_REPLICATION_USER} ${POSTGRES_HA_CIDR} scram-sha-256
# END LOXTHELION POSTGRES HA
EOF_HBA
cat "$tmp" > "$hba"
rm -f "$tmp"
chown postgres:postgres "$hba"
chmod 0600 "$hba"
}
create_persistent_pgpass() {
require_var POSTGRES_PRIMARY_HOST
require_var POSTGRES_PRIMARY_PORT
require_var POSTGRES_REPLICATION_USER
require_var POSTGRES_REPLICATION_PASSWORD_FILE
require_file "$POSTGRES_REPLICATION_PASSWORD_FILE"
local password
password="$(cat "$POSTGRES_REPLICATION_PASSWORD_FILE")"
mkdir -p /var/lib/postgresql
chown postgres:postgres /var/lib/postgresql
if [[ -n "${POSTGRES_REPLICATION_PEERS:-}" ]]; then
local endpoints hosts ports host port
endpoints=$(mc_peer_endpoints "$POSTGRES_REPLICATION_PEERS" "$POSTGRES_NODE_NAME") || die "Invalid replication peer map"
hosts=${endpoints%|*}; ports=${endpoints#*|}
[[ "$password" != *$'\n'* && "$password" != *$'\r'* ]] || die "Invalid multiline replication password"
password=${password//\\/\\\\}; password=${password//:/\\:}
(umask 077; : > /var/lib/postgresql/.pgpass)
while [[ -n "$hosts" ]]; do
host=${hosts%%,*}; port=${ports%%,*}
printf '%s:%s:*:%s:%s\n' "$host" "$port" "$POSTGRES_REPLICATION_USER" "$password" >> /var/lib/postgresql/.pgpass
[[ "$hosts" == *,* ]] || break
hosts=${hosts#*,}; ports=${ports#*,}
done
else
printf '%s:%s:*:%s:%s\n' \
"$POSTGRES_PRIMARY_HOST" \
"$POSTGRES_PRIMARY_PORT" \
"$POSTGRES_REPLICATION_USER" \
"$password" \
> /var/lib/postgresql/.pgpass
fi
chown postgres:postgres /var/lib/postgresql/.pgpass
chmod 0600 /var/lib/postgresql/.pgpass
}
grant_rewind_privileges() {
require_var POSTGRES_USER
require_var POSTGRES_REPLICATION_USER
log "Ensuring pg_rewind privileges for $POSTGRES_REPLICATION_USER"
as_postgres psql \
--username="$POSTGRES_USER" \
--dbname=postgres \
--set=ON_ERROR_STOP=1 \
--set=repl_user="$POSTGRES_REPLICATION_USER" <<'SQL'
GRANT EXECUTE
ON FUNCTION pg_catalog.pg_ls_dir(text, boolean, boolean)
TO :"repl_user";
GRANT EXECUTE
ON FUNCTION pg_catalog.pg_stat_file(text, boolean)
TO :"repl_user";
GRANT EXECUTE
ON FUNCTION pg_catalog.pg_read_binary_file(text)
TO :"repl_user";
GRANT EXECUTE
ON FUNCTION pg_catalog.pg_read_binary_file(text, bigint, bigint, boolean)
TO :"repl_user";
SQL
}
ensure_local_ha_slots() {
[[ -n "${POSTGRES_HA_SLOTS:-}" ]] || {
log "POSTGRES_HA_SLOTS is empty; no local HA slots requested"
return 0
}
local raw_slot slot slot_type
while IFS= read -r raw_slot; do
slot="${raw_slot//[[:space:]]/}"
[[ -n "$slot" ]] || continue
validate_slot_name "$slot"
slot_type="$(
as_postgres psql \
--username="$POSTGRES_USER" \
--dbname=postgres \
--tuples-only \
--no-align \
--set=ON_ERROR_STOP=1 \
--set=slot="$slot" <<'SQL'
SELECT slot_type
FROM pg_replication_slots
WHERE slot_name = :'slot';
SQL
)"
slot_type="${slot_type//[[:space:]]/}"
case "$slot_type" in
physical)
log "Physical HA slot already exists: $slot"
;;
"")
log "Creating physical HA slot: $slot"
as_postgres psql \
--username="$POSTGRES_USER" \
--dbname=postgres \
--set=ON_ERROR_STOP=1 \
--set=slot="$slot" <<'SQL' >/dev/null
SELECT pg_create_physical_replication_slot(:'slot');
SQL
;;
*)
die "HA slot $slot exists but is type $slot_type"
;;
esac
done < <(
printf '%s\n' "$POSTGRES_HA_SLOTS" |
tr ',' '\n'
)
}
bootstrap_primary() {
require_var POSTGRES_DB
require_var POSTGRES_USER
require_var POSTGRES_REPLICATION_USER
require_var POSTGRES_HA_CIDR
require_var POSTGRES_PASSWORD_FILE
require_var POSTGRES_REPLICATION_PASSWORD_FILE
require_file "$POSTGRES_PASSWORD_FILE"
require_file "$POSTGRES_REPLICATION_PASSWORD_FILE"
pgdata_is_empty ||
die "primary bootstrap requires an empty PGDATA"
log "Initializing fresh PostgreSQL primary"
mkdir -p "$PGDATA"
chown -R postgres:postgres /var/lib/postgresql
chmod 0700 "$PGDATA"
local pwfile
pwfile="/tmp/postgres-bootstrap-password"
install_secret_for_postgres \
"$POSTGRES_PASSWORD_FILE" \
"$pwfile"
as_postgres initdb \
-D "$PGDATA" \
--username="$POSTGRES_USER" \
--pwfile="$pwfile" \
--auth-host=scram-sha-256 \
--auth-local=trust \
--data-checksums
rm -f "$pwfile"
write_managed_config
write_managed_hba
log "Starting PostgreSQL temporarily for initial role/database setup"
as_postgres pg_ctl \
-D "$PGDATA" \
-w \
start
trap 'as_postgres pg_ctl -D "$PGDATA" -m fast -w stop >/dev/null 2>&1 || true' RETURN
local repl_password
repl_password="$(cat "$POSTGRES_REPLICATION_PASSWORD_FILE")"
# psql variables keep the password out of SQL construction itself.
as_postgres psql \
--username="$POSTGRES_USER" \
--dbname=postgres \
-v ON_ERROR_STOP=1 \
--set=repl_user="$POSTGRES_REPLICATION_USER" \
--set=repl_password="$repl_password" <<'SQL'
SELECT format(
'CREATE ROLE %I WITH LOGIN REPLICATION PASSWORD %L',
:'repl_user',
:'repl_password'
)
WHERE NOT EXISTS (
SELECT 1
FROM pg_roles
WHERE rolname = :'repl_user'
)
\gexec
SQL
grant_rewind_privileges
ensure_local_ha_slots
if ! as_postgres psql \
--username="$POSTGRES_USER" \
--dbname=postgres \
--tuples-only \
--no-align \
--set=ON_ERROR_STOP=1 \
--set=db="$POSTGRES_DB" <<'SQL' | grep -qx 1
SELECT 1
FROM pg_database
WHERE datname = :'db';
SQL
then
as_postgres createdb \
--username="$POSTGRES_USER" \
--owner="$POSTGRES_USER" \
"$POSTGRES_DB"
fi
as_postgres pg_ctl \
-D "$PGDATA" \
-m fast \
-w \
stop
trap - RETURN
touch "$(marker_path)"
chown postgres:postgres "$(marker_path)"
chmod 0600 "$(marker_path)"
log "Fresh primary bootstrap complete"
}
ensure_remote_replication_slot() {
validate_slot_name "$POSTGRES_REPLICATION_SLOT"
local slot_type
slot_type="$(
as_postgres env \
PGPASSFILE=/var/lib/postgresql/.pgpass \
psql \
--host="$POSTGRES_PRIMARY_HOST" \
--port="$POSTGRES_PRIMARY_PORT" \
--username="$POSTGRES_REPLICATION_USER" \
--dbname=postgres \
--tuples-only \
--no-align \
--set=ON_ERROR_STOP=1 \
--command="
SELECT slot_type
FROM pg_replication_slots
WHERE slot_name = '$POSTGRES_REPLICATION_SLOT';
" |
tr -d '[:space:]'
)"
case "$slot_type" in
physical)
log "Replication slot already exists: $POSTGRES_REPLICATION_SLOT"
;;
"")
log "Creating physical replication slot: $POSTGRES_REPLICATION_SLOT"
as_postgres env \
PGPASSFILE=/var/lib/postgresql/.pgpass \
psql \
--host="$POSTGRES_PRIMARY_HOST" \
--port="$POSTGRES_PRIMARY_PORT" \
--username="$POSTGRES_REPLICATION_USER" \
--dbname=postgres \
--set=ON_ERROR_STOP=1 \
--command="
SELECT pg_create_physical_replication_slot(
'$POSTGRES_REPLICATION_SLOT'
);
"
;;
*)
die "Slot $POSTGRES_REPLICATION_SLOT exists but is not physical"
;;
esac
}
bootstrap_standby() {
require_var POSTGRES_PRIMARY_HOST
require_var POSTGRES_PRIMARY_PORT
require_var POSTGRES_REPLICATION_USER
require_var POSTGRES_REPLICATION_SLOT
require_var POSTGRES_APPLICATION_NAME
require_var POSTGRES_REPLICATION_PASSWORD_FILE
pgdata_is_empty ||
die "standby bootstrap requires an empty PGDATA"
validate_slot_name "$POSTGRES_REPLICATION_SLOT"
log "Preparing fresh standby from $POSTGRES_PRIMARY_HOST"
mkdir -p "$PGDATA"
chmod 0700 "$PGDATA"
chown -R postgres:postgres /var/lib/postgresql
create_persistent_pgpass
ensure_remote_replication_slot
log "Running pg_basebackup"
as_postgres env \
PGPASSFILE=/var/lib/postgresql/.pgpass \
pg_basebackup \
--dbname="host=$POSTGRES_PRIMARY_HOST port=$POSTGRES_PRIMARY_PORT user=$POSTGRES_REPLICATION_USER application_name=$POSTGRES_APPLICATION_NAME passfile=/var/lib/postgresql/.pgpass" \
--pgdata="$PGDATA" \
--format=plain \
--wal-method=stream \
--slot="$POSTGRES_REPLICATION_SLOT" \
--write-recovery-conf \
--progress
[[ -e "$PGDATA/standby.signal" ]] ||
die "pg_basebackup completed without standby.signal"
pgdata_is_initialized ||
die "pg_basebackup did not produce a valid PostgreSQL data directory"
touch "$(marker_path)"
chown postgres:postgres "$(marker_path)"
chmod 0600 "$(marker_path)"
log "Fresh standby bootstrap complete"
}
adopt_existing() {
pgdata_is_initialized ||
die "POSTGRES_HA_MODE=existing requires a valid existing PostgreSQL cluster"
log "Existing PostgreSQL cluster detected"
write_managed_config
# HBA management requires these only when supplied.
if [[ -n "${POSTGRES_REPLICATION_USER:-}" &&
-n "${POSTGRES_HA_CIDR:-}" ]]; then
write_managed_hba
fi
touch "$(marker_path)"
chown postgres:postgres "$(marker_path)"
chmod 0600 "$(marker_path)"
log "Existing cluster adopted; no initdb or basebackup performed"
}
validate_bootstrapped_cluster() {
pgdata_is_initialized ||
die "Bootstrap marker exists but PGDATA is not a valid PostgreSQL cluster"
local version
version="$(cat "$PGDATA/PG_VERSION")"
[[ "$version" == "18" ]] ||
die "Expected PostgreSQL major version 18, found $version"
as_postgres pg_controldata "$PGDATA" >/dev/null ||
die "pg_controldata validation failed"
local pgdata_mode
pgdata_mode="$(stat -c '%a' "$PGDATA")"
[[ "$pgdata_mode" == "700" || "$pgdata_mode" == "750" ]] ||
die "PGDATA has invalid permissions: $pgdata_mode (expected 700 or 750)"
[[ "$(stat -c '%U' "$PGDATA")" == "postgres" ]] ||
die "PGDATA is not owned by postgres"
log "Existing bootstrapped PostgreSQL 18 cluster validated"
}
main() {
require_var PGDATA
if [[ -n "${POSTGRES_REPLICATION_PEERS:-}" ]]; then
require_var POSTGRES_NODE_NAME
require_var POSTGRES_APPLICATION_NAME
require_var POSTGRES_REPLICATION_USER
require_var POSTGRES_REPLICATION_SLOT
require_file /usr/local/libexec/postgres-ha-peer-config.sh
. /usr/local/libexec/postgres-ha-peer-config.sh
mc_peer_conninfo "$POSTGRES_REPLICATION_PEERS" "$POSTGRES_NODE_NAME" "$POSTGRES_REPLICATION_USER" "$POSTGRES_APPLICATION_NAME" >/dev/null || die "Invalid native peer configuration"
validate_slot_name "$POSTGRES_REPLICATION_SLOT"
# Basebackup slot creation still uses one explicitly selected stable source.
if [[ "${POSTGRES_HA_MODE:-}" == standby && ! -e "$(marker_path)" ]]; then
[[ ",$POSTGRES_REPLICATION_PEERS," == *"=$POSTGRES_PRIMARY_HOST:$POSTGRES_PRIMARY_PORT,"* ]] || die "Bootstrap source must be a mapped stable peer"
fi
fi
local marker
marker="$(marker_path)"
log "Node: ${POSTGRES_NODE_NAME:-unknown}"
log "Mode: ${POSTGRES_HA_MODE:-unset}"
log "PGDATA: $PGDATA"
if [[ -e "$marker" ]]; then
validate_bootstrapped_cluster
else
case "${POSTGRES_HA_MODE:-}" in
primary)
bootstrap_primary
;;
standby)
bootstrap_standby
;;
existing)
adopt_existing
;;
*)
die "POSTGRES_HA_MODE must be primary, standby, or existing"
;;
esac
fi
[[ "$#" -gt 0 ]] ||
die "No long-running container command supplied"
log "Bootstrap gate passed; executing: $*"
if [[ -n "${POSTGRES_REPLICATION_PEERS:-}" ]]; then
create_persistent_pgpass
if [[ -e "$PGDATA/standby.signal" ]]; then
[[ ! -e "$PGDATA/postmaster.pid" ]] || die "Refusing native peer rewrite with a postmaster PID file"
mc_peer_write_recovery "$PGDATA" "$POSTGRES_REPLICATION_PEERS" "$POSTGRES_NODE_NAME" "$POSTGRES_REPLICATION_USER" "$POSTGRES_APPLICATION_NAME" "$POSTGRES_REPLICATION_SLOT" || die "Native peer recovery configuration failed"
chown postgres:postgres "$PGDATA/postgresql.auto.conf" "$PGDATA/standby.signal"
fi
fi
exec "$@"
}
main "$@"
+54
View File
@@ -0,0 +1,54 @@
# POSIX-compatible shared normalizer. Also mounted beside the bootstrap script.
# Inputs are data, never evaluated shell. IPv4/DNS endpoints only in API 1.
mc_peer_endpoints()
{
LC_ALL=C awk -v peers="$1" -v self="$2" -v select_node="${3:-}" '
BEGIN {
n=split(peers, entries, ","); self=tolower(self); select_node=tolower(select_node);
if(n<2 || n>32 || self=="") exit 1;
for(i=1;i<=n;i++) {
if(split(entries[i],pair,"=")!=2 || pair[1]!~/^[A-Za-z0-9_][A-Za-z0-9_.-]*$/) exit 1;
node=tolower(pair[1]); if(seen[node]++) exit 1;
if(split(pair[2],addr,":")!=2 || addr[1]!~/^[A-Za-z0-9][A-Za-z0-9.-]*$/ || addr[2]!~/^[0-9]+$/ || addr[2]+0<1 || addr[2]+0>65535) exit 1;
endpoint=tolower(addr[1]) ":" (addr[2]+0); if(addresses[endpoint]++) exit 1;
if(node==self) { own++; continue; }
if(select_node!="" && node!=select_node) continue;
hosts=hosts (count?",":"") addr[1]; ports=ports (count?",":"") (addr[2]+0); count++;
}
if(own!=1 || count<1 || (select_node!="" && count!=1)) exit 1;
print hosts "|" ports;
}'
}
mc_peer_conninfo()
{
# peers, self, replication user, application name
case "$3" in ''|*[!a-zA-Z0-9_.-]*) return 1;; esac
case "$4" in ''|*[!a-zA-Z0-9_.-]*) return 1;; esac
mc_peer_pair=$(mc_peer_endpoints "$1" "$2") || return 1
printf 'host=%s port=%s user=%s application_name=%s target_session_attrs=primary connect_timeout=2\n' \
"${mc_peer_pair%|*}" "${mc_peer_pair#*|}" "$3" "$4"
}
mc_peer_write_recovery()
(
# PGDATA, peers, self, user, application, slot. No credentials in output.
set -eu
data=$1
conn=$(mc_peer_conninfo "$2" "$3" "$4" "$5") || exit 1
case "$6" in ''|*[!a-z0-9_]*) exit 1;; esac
[ ${#6} -le 63 ] || exit 1
[ -d "$data" ] && [ -f "$data/postgresql.auto.conf" ] && [ ! -L "$data/postgresql.auto.conf" ] || exit 1
[ ! -L "$data/standby.signal" ] || exit 1
umask 077
tmp=$(mktemp "$data/.peer-recovery.XXXXXXXX")
trap 'rm -f "$tmp"' EXIT HUP INT TERM
sed -e '/^[[:space:]]*primary_conninfo[[:space:]]*=/d' \
-e '/^[[:space:]]*primary_slot_name[[:space:]]*=/d' "$data/postgresql.auto.conf" > "$tmp"
printf "primary_conninfo = '%s'\nprimary_slot_name = '%s'\n" "$conn" "$6" >> "$tmp"
chmod 600 "$tmp"
mv -f "$tmp" "$data/postgresql.auto.conf"
[ ! -L "$data/standby.signal" ] || exit 1
touch "$data/standby.signal"
chmod 600 "$data/standby.signal"
)