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
+14
View File
@@ -21,3 +21,17 @@ __pycache__/
# Development candidate files
*.new
# Node-local PostgreSQL configuration / secrets
quadlet/postgres.env
**/secrets/
*.secret
*.key
# Pacemaker live/runtime snapshots
pacemaker/cib.xml
pacemaker/current-cluster-state.txt
pacemaker/constraints.txt
pacemaker/pcs-config.txt
pacemaker/postgresql-ha.txt
pacemaker/postgresql-ha-clone.txt
+33
View File
@@ -0,0 +1,33 @@
0675ddf1cadf497393404864370519c861ba4619787b086cf369e5b0113d6a44 ocf/loxthelion/ipvs-sync
5aa02fa065d1dba55486838fa4f0cd8eba982edaaf73cdc7d818223a09cbadc6 ocf/loxthelion/managed-container
10932919c4339b553c7c9dbf30b45a917c51c0a08f362e12678003d58b12fa82 ocf/loxthelion/managed-container.d/applications/postgresql/auth.sh
61d64b4a3a15c354f101ef34c5dc0425723f4204207d359b5377d2b9f04f6b23 ocf/loxthelion/managed-container.d/applications/postgresql/demote.sh
38323b217204132e572a9b1cfec8072773e502e49d6f0f96dfa2e565d332bbd1 ocf/loxthelion/managed-container.d/applications/postgresql/native_peers.sh
625e3b4fc6775164d351c98d1ef0e2c54cb2042c8a86015aa68649cb9c339260 ocf/loxthelion/managed-container.d/applications/postgresql/notify.sh
957a5a72ed277d0216e070abb80733483be3d421b9fa488e27fdd75bb2919548 ocf/loxthelion/managed-container.d/applications/postgresql/peer_config.sh
5091bc554915d1450fe939f94c0b0a370fecdb2ae5db9c4f0f55c340c0145519 ocf/loxthelion/managed-container.d/applications/postgresql/plugin.sh
c1d878dabf22ad3928d66a5542bda3c150e5e9b039926629e7c5fd3401d29be4 ocf/loxthelion/managed-container.d/applications/postgresql/postgres.sh
3a6c92e8fe7d0df2573b19be22dd707b89b1eaa8147bdc07ef84dcded51aaa31 ocf/loxthelion/managed-container.d/applications/postgresql/postgres.sh.pre-score-rejoin-reconcile-20260917-103742
d7547d66e4960a6aadfdd5cb1270431009453c8b769d0fa21f0b06d7fe7df686 ocf/loxthelion/managed-container.d/applications/postgresql/postgres.sh.pre-streaming-rejoin-marker-fix-20260917-103610
f482853a0c7992981e59b4ad78d1ee69851d5d60fdec1f695673228017dd8244 ocf/loxthelion/managed-container.d/applications/postgresql/promote.sh
5166fc3370f49ab14cd975ded4b03a74b9bd7dbff5762b41a15552c42f934ea7 ocf/loxthelion/managed-container.d/applications/postgresql/promotion_state.py
b0ebad749ed736f08ec748a693f12e8dc2541430e0d3db75bbb6b2b7e517f84b ocf/loxthelion/managed-container.d/applications/postgresql/rejoin.sh
0d3017fdca92b2dc5dba406065bc269622ce5140e3fce4a959ca95ce4a78e755 ocf/loxthelion/managed-container.d/applications/postgresql/replication.sh
d7d2fd7ab651d1f2f026bdb11d62e2c0c6ce6ee3128c9814d09550cf4304b3f2 ocf/loxthelion/managed-container.d/common.sh
2c897c3a21d924a1a865494cb5c76a3405e2de7fee55e41ccd7bee2511726927 ocf/loxthelion/managed-container.d/common.sh.pre-named-proxy-rc-20260917-100736
3a27ac9c63c61310da8b8711fa2c4400b37ea8eae2ee2a79e28bc63180b06c7c ocf/loxthelion/managed-container.d/container.sh
70431e0d390a3612339c7b5887286c1be4d6b305cbe8898e7895a211c26b6087 ocf/loxthelion/managed-container.d/engine.sh
6c92c095ce10ac95d843fdc1c49e31c19491ca22fdac960affd8649606d76d82 ocf/loxthelion/managed-container.d/lifecycles/systemd.sh
259e8cafa5b6d3fb54896ab37ff3917942c62c51eaa6e9bfa8ccf3afc6a7327b ocf/loxthelion/managed-container.d/proxy.sh
d5ca2aceddd5c8c5467e0997b099118279b9f54b96b8275e3a33182e1b992093 ocf/loxthelion/managed-container.d/proxy.sh.pre-named-proxy-rc-20260917-100736
f82e2571d309bce8e4d7e6aee6b61b68feb7d99dc85bb8b688b6123c13ccf6a7 ocf/loxthelion/managed-container.d/runtimes/podman.sh
8679baffebc77614fd80888d344ca55e11a13f84de6f096a2399c22a49c8cc6a ocf/loxthelion/managed-container.pre-entrypoint-selftest-20260917-105341
01eb9b161c8e3db289f09bf63953be779921e15bdf8b1a4d8ce7e60032b826e4 ocf/loxthelion/managed-container.pre-named-proxy-rc-20260917-100736
62ec256e108f40f91d626ea71e8945862aedb347e576a00e6bd1e15f9f363e70 ocf/loxthelion/managed-container.pre-proxy-load-fix-20260917-101231
4a99efea3d4a185e0630db89cfacc28aa8777bad9c747794ae69f70d87cd0327 pacemaker/README.md
a42bb59a058ffeb59c1baa96e403856910bbbaea98f9104ff93daf4b5970c780 postgres-host/postgres-ha-bootstrap
957a5a72ed277d0216e070abb80733483be3d421b9fa488e27fdd75bb2919548 postgres-host/postgres-ha-peer-config.sh
deb09e3ae40629f43b262ba4f9a7ebf59a1dca3da9d8c94478f670aea28bd0e1 quadlet/postgres.container
04a125068677bbee6e71d313ca9d95d35faa32851b79de781cd7b9f466561445 quadlet/postgres.env.example
761e4d5166e26507f34b3276258bd012ddbd7c667020a9797542e08b8976891a tests/run-all.sh
5e5d4fdd2b6c7e0b8b146de7aad06c1f96b221a10fec525b00d40e0d85709fb0 tests/test-entrypoint-structure.sh
+24
View File
@@ -0,0 +1,24 @@
# Pacemaker integration
The files generated by `pcs config`, `crm_mon`, and `cibadmin --query`
are intentionally not tracked here.
Those exports contain point-in-time cluster state, transient node
attributes, operation history, and temporary constraints, so they are
diagnostic snapshots rather than canonical configuration.
The `managed-container` OCF resource agent itself is under:
ocf/loxthelion/
Current PostgreSQL production design:
- promotable clone
- clone-max=3
- clone-node-max=1
- promoted-max=1
- promoted-node-max=1
- notify=true
- target-role=Promoted
- PostgreSQL VIP colocated with the Promoted instance
- promotion score controlled by the resource agent
+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"
)
+28
View File
@@ -0,0 +1,28 @@
[Unit]
Description=PostgreSQL HA Maintenance Container
[Container]
Volume=/home/david/.local/libexec/postgres-ha-peer-config.sh:/usr/local/libexec/postgres-ha-peer-config.sh:ro,Z
ContainerName=postgres
Image=docker.io/library/postgres@sha256:4ef4dbc939d61acea57712655ddb4b4ab27419c913f94cca0cd57cb3ea3c2280
Entrypoint=/usr/local/sbin/postgres-ha-bootstrap
Exec=sleep infinity
RunInit=true
StopSignal=SIGTERM
StopTimeout=5
Network=postgres.network
PublishPort=5432:5432
Volume=postgres-data:/var/lib/postgresql:Z
Volume=/home/david/.local/libexec/postgres-ha-bootstrap:/usr/local/sbin/postgres-ha-bootstrap:ro,Z
Volume=/home/david/.config/postgres-ha/secrets:/run/secrets/postgres-ha:ro,Z
EnvironmentFile=/home/david/.config/postgres-ha/postgres.env
[Service]
SuccessExitStatus=143
Restart=on-abnormal
TimeoutStartSec=0
+25
View File
@@ -0,0 +1,25 @@
PGDATA=/var/lib/postgresql/18/docker
# Initial bootstrap role. Pacemaker/RA controls promotion afterward.
POSTGRES_HA_MODE=standby
POSTGRES_REPLICATION_USER=replicator
# Node-specific values:
POSTGRES_NODE_NAME=CHANGE_ME
POSTGRES_APPLICATION_NAME=CHANGE_ME
POSTGRES_REPLICATION_SLOT=ha_pg_CHANGE_ME
# Stable cluster peer map:
POSTGRES_REPLICATION_PEERS=david-prodesk=192.168.1.4:5432,david-macbookpro=192.168.1.5:5432,david-eve=192.168.1.3:5432
# Initial/bootstrap upstream. The RA later determines the elected primary.
POSTGRES_PRIMARY_HOST=192.168.1.4
POSTGRES_PRIMARY_PORT=5432
POSTGRES_HA_SLOTS=ha_pg_david_macbookpro,ha_pg_david_eve,ha_pg_david_prodesk
# Secret contents are never stored here.
POSTGRES_REPLICATION_PASSWORD_FILE=/run/secrets/postgres-ha/replication_password
POSTGRES_HA_REPLICATION_NETWORKS=192.168.1.3/32,192.168.1.4/32,192.168.1.5/32,10.89.0.0/16
+146
View File
@@ -0,0 +1,146 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT="$(
cd "$(dirname "${BASH_SOURCE[0]}")/.." &&
pwd
)"
cd "$ROOT"
echo "========================================"
echo " managed-container release test suite"
echo "========================================"
echo
echo ">>> Entrypoint / shell structure"
./tests/test-entrypoint-structure.sh
echo
echo ">>> Python syntax"
while IFS= read -r -d '' file; do
python3 -m py_compile "$file"
done < <(
find ocf \
-type f \
-name '*.py' \
-print0
)
echo "PYTHON_SYNTAX_OK=1"
echo
echo ">>> Host helper shell syntax"
for file in postgres-host/*; do
[[ -f "$file" ]] || continue
case "$file" in
*.sh|*/postgres-ha-bootstrap)
bash -n "$file"
;;
esac
done
echo "HOST_HELPER_SYNTAX_OK=1"
echo
echo ">>> Secret-content guard"
python3 - <<'PY_SECRET_GUARD'
from pathlib import Path
import re
import sys
roots = [Path("quadlet"), Path("postgres-host")]
credential_key = re.compile(
r'^[A-Z0-9_]*(?:PASSWORD|PASSWD|SECRET|TOKEN)[A-Z0-9_]*$'
)
private_key = re.compile(
r'-----BEGIN (?:OPENSSH|RSA|EC|DSA) PRIVATE KEY-----'
)
errors = []
for root in roots:
if not root.exists():
continue
for path in root.rglob("*"):
if not path.is_file():
continue
try:
text = path.read_text(errors="replace")
except Exception as exc:
errors.append(f"{path}: unable to read: {exc}")
continue
if private_key.search(text):
errors.append(f"{path}: contains private-key material")
for lineno, raw in enumerate(text.splitlines(), 1):
line = raw.strip()
if not line or line.startswith("#"):
continue
# Only inspect configuration-style assignments whose KEY itself
# is uppercase and explicitly credential-related.
m = re.match(
r'^([A-Z][A-Z0-9_]*)\s*=\s*(.*)$',
line
)
if not m:
continue
key, value = m.groups()
if not credential_key.match(key):
continue
value = value.strip()
# Empty values are not embedded credentials.
if not value:
continue
# References, secret-file paths, templates and placeholders
# are acceptable.
safe_prefixes = (
"$",
"${",
"/run/secrets/",
"/etc/",
"<",
"CHANGE_ME",
)
unquoted = value.strip("'\"")
if value.startswith(safe_prefixes) or unquoted.startswith(safe_prefixes):
continue
errors.append(
f"{path}:{lineno}: possible literal credential assignment: {key}=..."
)
if errors:
for error in errors:
print(error, file=sys.stderr)
print(
"ERROR: possible embedded credential material found",
file=sys.stderr
)
sys.exit(1)
print("SECRET_GUARD_OK=1")
PY_SECRET_GUARD
echo
echo "ALL_TESTS_PASS=1"