mirror of
https://github.com/mulgadc/spinifex.git
synced 2026-10-15 17:11:30 +00:00
513 lines
24 KiB
Bash
513 lines
24 KiB
Bash
#!/bin/sh
|
|
# rds-init — PostgreSQL bootstrap for the spinifex-rds-postgres AMI.
|
|
#
|
|
# A boot oneshot ordered before the postgresql service. This is the PostgreSQL
|
|
# half of the engine seam: initdb, ALTER ROLE, the datadir and the 5432 default
|
|
# live here and nowhere else in the platform.
|
|
#
|
|
# Handoff contract, written by rds-agent into a root-owned tmpfs directory:
|
|
#
|
|
# /run/spinifex-rds/bootstrap.env 0600 KEY=value shell fragment:
|
|
# RDS_MODE=initialize|attach
|
|
# RDS_DB_INSTANCE_IDENTIFIER=<the DB instance this VM backs>
|
|
# RDS_MASTER_USERNAME=<master role name>
|
|
# RDS_MASTER_PASSWORD=<present only when RDS_MODE=initialize>
|
|
# RDS_DB_NAME=<initial database; optional, as on AWS>
|
|
# RDS_PORT=<listen port; defaults to 5432>
|
|
# RDS_DATA_VOLUME_ID / RDS_DATA_VOLUME_SERIAL=<expected volume identity>
|
|
# RDS_VM_GENERATION / RDS_FORMAT_AUTHORIZED=<create-only format grant>
|
|
# RDS_BOOTSTRAP_PENDING / RDS_PAYLOAD_ID=<the control plane is still
|
|
# waiting for this payload to be applied>
|
|
# /run/spinifex-rds/parameters.conf 0600 resolved parameter-group values in
|
|
# postgresql.conf syntax
|
|
# /run/spinifex-rds/server.crt 0600 serving cert, cluster-CA-signed
|
|
# /run/spinifex-rds/server.key 0600 its private key
|
|
#
|
|
# Nothing there survives a reboot: the next boot re-fetches and the cert is
|
|
# re-minted rather than persisted.
|
|
#
|
|
# rds-init keys off the datadir, never off RDS_MODE. An initialised datadir
|
|
# means skip initdb and only refresh config, so a replaced VM comes back on its
|
|
# existing data volume without re-bootstrapping. RDS_BOOTSTRAP_PENDING is not an
|
|
# exception: it is a control-plane-resolved precondition compared against
|
|
# on-disk evidence, the same shape as RDS_FORMAT_AUTHORIZED.
|
|
set -eu
|
|
|
|
PG_VERSION="${RDS_PG_VERSION:-18}"
|
|
PG_BIN="${RDS_PG_BIN:-/usr/libexec/postgresql${PG_VERSION}}"
|
|
PG_USER="${RDS_PG_USER:-postgres}"
|
|
|
|
# Mount point of the data volume, and the datadir one level inside it. The
|
|
# datadir is deliberately not the mount point itself: initdb refuses a non-empty
|
|
# directory and a freshly formatted ext4 volume already holds lost+found.
|
|
DATA_MOUNT="${RDS_DATA_MOUNT:-/var/lib/postgresql}"
|
|
PGDATA="${RDS_PGDATA:-${DATA_MOUNT}/${PG_VERSION}/data}"
|
|
|
|
# Written between initdb and a working master role. It lives on the data volume
|
|
# because no trap survives a crash or a hard kill, and it is what stops the next
|
|
# boot attaching to a datadir whose master role was never created.
|
|
BOOTSTRAP_SENTINEL="${PGDATA}/rds-bootstrap-incomplete"
|
|
|
|
# Proof that the master role was durably applied, written once initdb and the
|
|
# role are both done. Deliberately outside PGDATA, which clear_unfinished_datadir
|
|
# rm -rf's: no initdb-adjacent path can touch it and PGDATA stays a stock datadir.
|
|
RECEIPT_DIR="${RDS_RECEIPT_DIR:-${DATA_MOUNT}/.spinifex-rds/bootstrap}"
|
|
RECEIPT_FILE="${RECEIPT_DIR}/receipt.env"
|
|
|
|
HANDOFF_DIR="${RDS_HANDOFF_DIR:-/run/spinifex-rds}"
|
|
ENV_FILE="${HANDOFF_DIR}/bootstrap.env"
|
|
SOCKET_DIR="${RDS_SOCKET_DIR:-/run/postgresql}"
|
|
TLS_DIR="${RDS_TLS_DIR:-${SOCKET_DIR}/tls}"
|
|
LOG_DIR="${RDS_LOG_DIR:-/var/log/postgresql}"
|
|
|
|
# The bootstrap server listens on a unix socket in this directory and nowhere
|
|
# else. It is 0700 and postgres-owned, so only postgres and root can reach the
|
|
# short trust-authenticated window before the master password exists.
|
|
BOOTSTRAP_DIR="${RDS_BOOTSTRAP_DIR:-/run/rds-init}"
|
|
|
|
MOUNTS_FILE="${RDS_MOUNTS_FILE:-/proc/mounts}"
|
|
|
|
# The group role the master's administrative privileges hang off. Named as on
|
|
# AWS, so a customer's `GRANT rds_superuser TO ...` works here too.
|
|
MASTER_GROUP_ROLE="rds_superuser"
|
|
|
|
log() { echo "[rds-init] $*"; }
|
|
fail() { echo "[rds-init] ERROR: $*" >&2; exit 1; }
|
|
|
|
# Between these two, the datadir carries PG_VERSION but no usable master role.
|
|
_created_datadir=0
|
|
_bootstrapped=0
|
|
|
|
# Runs from a trap rather than a statement, so neither `set -e` on an unguarded
|
|
# step between initdb and a working master role nor a signal can skip it — either
|
|
# would leave a healthy-looking engine with no usable master role, unrepairable.
|
|
clear_unfinished_datadir() {
|
|
[ "${_created_datadir}" = "1" ] || return 0
|
|
[ "${_bootstrapped}" = "1" ] && return 0
|
|
# The receipt is installed before the traps retire, so a sweep in between
|
|
# would leave one vouching for a datadir that is about to be deleted. The
|
|
# agent would acknowledge it and the control plane would destroy the only
|
|
# copy of the password this instance can ever be initialised with.
|
|
rm -f -- "${RECEIPT_FILE}"
|
|
if rm -rf -- "${PGDATA}" && [ ! -e "${PGDATA}" ]; then
|
|
echo "[rds-init] ERROR: bootstrap did not complete; datadir ${PGDATA} cleared" >&2
|
|
return 0
|
|
fi
|
|
echo "[rds-init] ERROR: bootstrap did not complete and datadir ${PGDATA} could not be cleared; recover it out of band before retrying" >&2
|
|
}
|
|
|
|
# The server refuses to run as root.
|
|
as_pg() { su "${PG_USER}" -c "$1"; }
|
|
|
|
load_handoff() {
|
|
[ -f "${ENV_FILE}" ] || fail "no bootstrap handoff at ${ENV_FILE} — rds-agent has not delivered the bootstrap config"
|
|
|
|
# shellcheck disable=SC1090
|
|
. "${ENV_FILE}"
|
|
|
|
RDS_MODE="${RDS_MODE:-}"
|
|
RDS_DB_INSTANCE_IDENTIFIER="${RDS_DB_INSTANCE_IDENTIFIER:-}"
|
|
RDS_MASTER_USERNAME="${RDS_MASTER_USERNAME:-}"
|
|
RDS_MASTER_PASSWORD="${RDS_MASTER_PASSWORD:-}"
|
|
RDS_DB_NAME="${RDS_DB_NAME:-}"
|
|
RDS_PORT="${RDS_PORT:-5432}"
|
|
RDS_VM_GENERATION="${RDS_VM_GENERATION:-}"
|
|
# Absent from an agent that predates the receipt protocol, which reads as
|
|
# "nothing is pending" and leaves every path below unchanged.
|
|
RDS_BOOTSTRAP_PENDING="${RDS_BOOTSTRAP_PENDING:-0}"
|
|
RDS_PAYLOAD_ID="${RDS_PAYLOAD_ID:-}"
|
|
|
|
[ -n "${RDS_MASTER_USERNAME}" ] || fail "bootstrap config has no RDS_MASTER_USERNAME"
|
|
|
|
# The master role is created NOSUPERUSER, so a master named after the
|
|
# bootstrap superuser would leave the cluster without one and strip rds-agent
|
|
# of the privileged SQL it reaches over peer auth — unrepairable, since the
|
|
# datadir bootstraps once.
|
|
#
|
|
# The same set the control plane reserves, matched the same way: anything it
|
|
# refuses must be refused here too, or the name reaches initdb, fails at role
|
|
# creation and spends the one-shot password on the way.
|
|
_master_lower=$(printf '%s' "${RDS_MASTER_USERNAME}" | tr '[:upper:]' '[:lower:]')
|
|
case "${_master_lower}" in
|
|
"${PG_USER}" | "${MASTER_GROUP_ROLE}" | rdsadmin | pg_*)
|
|
fail "RDS_MASTER_USERNAME ${RDS_MASTER_USERNAME} is reserved by the platform; the control plane should have rejected it"
|
|
;;
|
|
esac
|
|
|
|
case "${RDS_PORT}" in
|
|
'' | *[!0-9]*) fail "bootstrap config has a non-numeric RDS_PORT: ${RDS_PORT}" ;;
|
|
esac
|
|
|
|
log "bootstrap config loaded (mode=${RDS_MODE:-unset}, port=${RDS_PORT})"
|
|
}
|
|
|
|
# Every replace boots a fresh clone of this AMI, so a datadir off the data volume
|
|
# disappears at the next class change or recovery — silently, as the engine looks
|
|
# fine.
|
|
require_data_volume() {
|
|
if awk -v mnt="${DATA_MOUNT}" '$2 == mnt { found = 1 } END { exit !found }' "${MOUNTS_FILE}"; then
|
|
return 0
|
|
fi
|
|
[ "${RDS_ALLOW_LOCAL_DATADIR:-0}" = "1" ] ||
|
|
fail "${DATA_MOUNT} is not a mount point — the data volume is not attached; refusing to initialise on the boot volume"
|
|
log "warning: ${DATA_MOUNT} is not a mount point; data will not survive a VM replace (RDS_ALLOW_LOCAL_DATADIR=1)"
|
|
}
|
|
|
|
# Runs initdb into an empty datadir and lays down the cluster config written
|
|
# once: the include_dir hook and pg_hba.
|
|
initialise_cluster() {
|
|
# An empty datadir in `attach` mode means the data volume did not come back.
|
|
# Creating a cluster there would report success and hand the customer an
|
|
# empty database in place of theirs.
|
|
if [ "${RDS_MODE}" = "attach" ]; then
|
|
fail "datadir ${PGDATA} is empty but the bootstrap config is Mode=attach — the existing data volume is missing; refusing to create an empty cluster"
|
|
fi
|
|
[ -n "${RDS_MASTER_PASSWORD}" ] || fail "Mode=${RDS_MODE:-unset} carries no master password; cannot initialise the cluster"
|
|
|
|
install -d -m 0700 -o "${PG_USER}" -g "${PG_USER}" "$(dirname "${PGDATA}")" "${PGDATA}"
|
|
|
|
# Whether the datadir was empty beforehand gates the failure cleanup below.
|
|
# A torn write can leave a missing PG_VERSION over intact data, and deleting
|
|
# then would destroy it in response to the one signal that it is still there.
|
|
_was_empty=0
|
|
if _contents=$(ls -A -- "${PGDATA}" 2>/dev/null); then
|
|
[ -z "${_contents}" ] && _was_empty=1
|
|
else
|
|
fail "could not inspect datadir ${PGDATA}; refusing to decide whether failure cleanup is safe"
|
|
fi
|
|
|
|
log "initialising PostgreSQL ${PG_VERSION} cluster at ${PGDATA}"
|
|
# ICU collation, not libc: musl has no locale support, so a libc-provider
|
|
# cluster would silently behave as C. Local peer auth keeps root/postgres
|
|
# administration working once the bootstrap server is gone.
|
|
if ! as_pg "'${PG_BIN}/initdb' --pgdata='${PGDATA}' -E UTF-8 \
|
|
--locale-provider=icu --icu-locale=en-001-x-icu --data-checksums \
|
|
--auth-local=peer --auth-host=scram-sha-256"; then
|
|
# A half-written datadir carries PG_VERSION and would look initialised
|
|
# forever after, but only what this invocation created may be cleared.
|
|
if [ "${_was_empty}" = "1" ]; then
|
|
if rm -rf -- "${PGDATA}" && [ ! -e "${PGDATA}" ]; then
|
|
fail "initdb failed; datadir cleared"
|
|
fi
|
|
fail "initdb failed and datadir ${PGDATA} could not be cleared; recover it out of band before retrying"
|
|
fi
|
|
fail "initdb failed and ${PGDATA} was not empty beforehand — refusing to clear it, since the contents may be a datadir whose PG_VERSION was lost; recover the data volume out of band"
|
|
fi
|
|
|
|
# Only a datadir initdb built over an empty directory may be swept later.
|
|
_created_datadir="${_was_empty}"
|
|
trap clear_unfinished_datadir EXIT
|
|
# An EXIT trap does not run on a signal in a POSIX shell, so the same sweep
|
|
# is armed for the signals a shutdown, an ACPI powerdown or a host-side
|
|
# force-stop delivers. EXIT is disarmed first so the sweep runs once.
|
|
trap 'trap - EXIT; clear_unfinished_datadir; exit 1' INT TERM HUP
|
|
|
|
# No trap survives a crash or a SIGKILL, so the incomplete state is also
|
|
# recorded on the volume itself. Cleared once the master role exists.
|
|
printf '%s\n' \
|
|
'rds-init created this datadir and did not finish bootstrapping the master role.' \
|
|
'While this file exists rds-init refuses to start the engine: the datadir carries no' \
|
|
'usable master role, so it must not be attached to. Recover out of band.' \
|
|
> "${BOOTSTRAP_SENTINEL}"
|
|
chown "${PG_USER}:${PG_USER}" "${BOOTSTRAP_SENTINEL}"
|
|
chmod 0600 "${BOOTSTRAP_SENTINEL}"
|
|
|
|
cat >> "${PGDATA}/pg_hba.conf" <<'EOF'
|
|
|
|
# Managed by rds-init. Clients reach the instance over its customer ENI, gated
|
|
# by the DB security group as on AWS RDS, and authenticate with scram-sha-256.
|
|
# TLS is offered, not enforced — clients opt in with sslmode=require and up.
|
|
host all all 0.0.0.0/0 scram-sha-256
|
|
host all all ::/0 scram-sha-256
|
|
EOF
|
|
}
|
|
|
|
# Parameters live beside the data rather than in /etc, since a class change boots
|
|
# a fresh root volume and would quietly revert them to engine defaults.
|
|
install_parameters() {
|
|
if [ -f "${HANDOFF_DIR}/parameters.conf" ]; then
|
|
install -m 0600 -o "${PG_USER}" -g "${PG_USER}" \
|
|
"${HANDOFF_DIR}/parameters.conf" "${PGDATA}/conf.d/10-rds-parameters.conf"
|
|
return 0
|
|
fi
|
|
# A handoff without parameters keeps the previous include: dropping it would
|
|
# silently reset the instance to engine defaults.
|
|
if [ -f "${PGDATA}/conf.d/10-rds-parameters.conf" ]; then
|
|
log "warning: no parameters in the handoff — keeping the parameters already installed"
|
|
fi
|
|
}
|
|
|
|
# The key stays on tmpfs: it is re-minted every fetch, so persisting it would
|
|
# spread a private key into every snapshot.
|
|
install_tls() {
|
|
if [ -f "${HANDOFF_DIR}/server.crt" ] && [ -f "${HANDOFF_DIR}/server.key" ]; then
|
|
install -d -m 0700 -o "${PG_USER}" -g "${PG_USER}" "${TLS_DIR}"
|
|
install -m 0600 -o "${PG_USER}" -g "${PG_USER}" "${HANDOFF_DIR}/server.crt" "${TLS_DIR}/server.crt"
|
|
install -m 0600 -o "${PG_USER}" -g "${PG_USER}" "${HANDOFF_DIR}/server.key" "${TLS_DIR}/server.key"
|
|
rm -f "${HANDOFF_DIR}/server.crt" "${HANDOFF_DIR}/server.key"
|
|
SSL_MODE="on"
|
|
return 0
|
|
fi
|
|
# A re-run after the cert was consumed must not turn TLS off underneath
|
|
# clients connecting with sslmode=require.
|
|
if [ -f "${TLS_DIR}/server.crt" ] && [ -f "${TLS_DIR}/server.key" ]; then
|
|
SSL_MODE="on"
|
|
return 0
|
|
fi
|
|
log "warning: no serving cert in the handoff — the engine will start without TLS"
|
|
SSL_MODE="off"
|
|
}
|
|
|
|
# The 90- prefix sorts last within include_dir and the last setting wins, so a
|
|
# resolved parameter can never move the endpoint's port or turn off TLS.
|
|
write_rds_conf() {
|
|
_ssl="$1"
|
|
_tmp="${PGDATA}/conf.d/.90-rds-init.conf.new"
|
|
cat > "${_tmp}" <<EOF
|
|
# Managed by rds-init — regenerated on every boot from the bootstrap config.
|
|
# Edits here are lost; instance parameters belong in the parameter group.
|
|
listen_addresses = '*'
|
|
port = ${RDS_PORT}
|
|
unix_socket_directories = '${SOCKET_DIR}'
|
|
ssl = ${_ssl}
|
|
EOF
|
|
if [ "${_ssl}" = "on" ]; then
|
|
cat >> "${_tmp}" <<EOF
|
|
ssl_cert_file = '${TLS_DIR}/server.crt'
|
|
ssl_key_file = '${TLS_DIR}/server.key'
|
|
EOF
|
|
fi
|
|
chown "${PG_USER}:${PG_USER}" "${_tmp}"
|
|
chmod 0600 "${_tmp}"
|
|
mv -f "${_tmp}" "${PGDATA}/conf.d/90-rds-init.conf"
|
|
}
|
|
|
|
# Everything refreshed on every boot: the include hook, the resolved parameters,
|
|
# the serving cert and the platform's own connectivity settings.
|
|
install_runtime_config() {
|
|
install -d -m 0700 -o "${PG_USER}" -g "${PG_USER}" "${PGDATA}/conf.d"
|
|
install -d -m 1775 -o "${PG_USER}" -g "${PG_USER}" "${SOCKET_DIR}"
|
|
install -d -m 0755 -o "${PG_USER}" -g "${PG_USER}" "${LOG_DIR}"
|
|
|
|
# postgresql.conf is on the data volume, so the hook is asserted every boot
|
|
# rather than only at initdb — a restored volume may predate it.
|
|
if ! grep -q "^include_dir = 'conf.d'" "${PGDATA}/postgresql.conf"; then
|
|
printf "\n# Managed by rds-init: parameter-group values (10-) then platform settings (90-).\ninclude_dir = 'conf.d'\n" \
|
|
>> "${PGDATA}/postgresql.conf"
|
|
fi
|
|
|
|
install_parameters
|
|
install_tls
|
|
write_rds_conf "${SSL_MODE}"
|
|
}
|
|
|
|
# Brings the cluster up on a private socket with trust auth and no TLS, so the
|
|
# master password can be set before the engine is reachable. Never listens on TCP.
|
|
start_bootstrap_server() {
|
|
install -d -m 0700 -o "${PG_USER}" -g "${PG_USER}" "${BOOTSTRAP_DIR}"
|
|
printf 'local all all trust\n' > "${BOOTSTRAP_DIR}/pg_hba.conf"
|
|
chown "${PG_USER}:${PG_USER}" "${BOOTSTRAP_DIR}/pg_hba.conf"
|
|
chmod 0600 "${BOOTSTRAP_DIR}/pg_hba.conf"
|
|
|
|
as_pg "'${PG_BIN}/pg_ctl' --pgdata='${PGDATA}' --wait --timeout=60 \
|
|
--log='${LOG_DIR}/rds-init.log' \
|
|
-o \"-c listen_addresses='' -c unix_socket_directories='${BOOTSTRAP_DIR}' \
|
|
-c unix_socket_permissions=0700 -c hba_file='${BOOTSTRAP_DIR}/pg_hba.conf' -c ssl=off\" \
|
|
start"
|
|
}
|
|
|
|
# The trust-authenticated pg_hba is removed by the caller, not here: a caller
|
|
# reading this function's status must be reading pg_ctl's.
|
|
stop_bootstrap_server() {
|
|
as_pg "'${PG_BIN}/pg_ctl' --pgdata='${PGDATA}' --wait --timeout=60 --mode=fast stop"
|
|
}
|
|
|
|
# Identifiers and the password ride the environment and are re-quoted by psql,
|
|
# never reaching a shell word or argv.
|
|
psql_bootstrap() {
|
|
RDS_MASTER_USERNAME="${RDS_MASTER_USERNAME}" \
|
|
RDS_MASTER_PASSWORD="${RDS_MASTER_PASSWORD}" \
|
|
RDS_DB_NAME="${RDS_DB_NAME}" \
|
|
RDS_GROUP_ROLE="${MASTER_GROUP_ROLE}" \
|
|
"${PG_BIN}/psql" --no-psqlrc --quiet --no-align --tuples-only \
|
|
-v ON_ERROR_STOP=1 \
|
|
-h "${BOOTSTRAP_DIR}" -p "${RDS_PORT}" -U "${PG_USER}" -d postgres -f -
|
|
}
|
|
|
|
# Runs on a freshly initialised cluster only.
|
|
bootstrap_master() {
|
|
log "starting bootstrap server on ${BOOTSTRAP_DIR}"
|
|
start_bootstrap_server
|
|
|
|
_rc=0
|
|
apply_master_role || _rc=$?
|
|
if [ "${_rc}" -eq 0 ] && [ -n "${RDS_DB_NAME}" ]; then
|
|
create_database || _rc=$?
|
|
fi
|
|
|
|
# `set -e` is suspended for a function called from an AND-OR list, so the
|
|
# stop status is captured rather than propagated. A failed stop must also not
|
|
# abort before the bootstrap check below, which names the real cause.
|
|
_stop_rc=0
|
|
stop_bootstrap_server || _stop_rc=$?
|
|
rm -f "${BOOTSTRAP_DIR}/pg_hba.conf"
|
|
|
|
# The datadir is swept by the traps, which also cover the steps here that
|
|
# abort under `set -e` before this check is ever reached.
|
|
if [ "${_rc}" -ne 0 ]; then
|
|
fail "bootstrapping the master role/database failed (rc=${_rc})"
|
|
fi
|
|
|
|
# Proof written while the traps are still armed, so a receipt that cannot be
|
|
# made durable clears the datadir instead of leaving one it cannot vouch for.
|
|
write_completion_receipt
|
|
|
|
# The master role exists from here, so the datadir is complete and nothing
|
|
# may clear it — including on the stop failure reported below.
|
|
mark_bootstrap_complete
|
|
|
|
# A postmaster still holding the datadir blocks the postgresql service for
|
|
# the life of the boot, so this is a failure and not a warning.
|
|
if [ "${_stop_rc}" -ne 0 ]; then
|
|
fail "the bootstrap server did not stop (rc=${_stop_rc}) and still holds ${PGDATA}; the postgresql service cannot start until it is gone"
|
|
fi
|
|
}
|
|
|
|
# The value of one receipt key. Never sourced: the receipt is on the data volume,
|
|
# so a restored volume can carry one written by another instance, and executing
|
|
# it would run whatever that instance's control plane put there.
|
|
receipt_value() {
|
|
[ -f "${RECEIPT_FILE}" ] || return 0
|
|
sed -n "s/^$1=//p" "${RECEIPT_FILE}" 2>/dev/null | head -n 1
|
|
}
|
|
|
|
# Written between the master role and the retirement of the traps, so a receipt
|
|
# that cannot be made durable still clears the datadir rather than leaving a
|
|
# bootstrapped one nobody can prove.
|
|
#
|
|
# Fatal on failure, the mirror image of the sentinel removal below: a healthy
|
|
# engine whose receipt never landed would come up available while the agent
|
|
# blocked forever on a receipt that will never appear, leaving a working database
|
|
# that can never accept a password change or a parameter reload.
|
|
write_completion_receipt() {
|
|
install -d -m 0700 -o "${PG_USER}" -g "${PG_USER}" "${RECEIPT_DIR}" ||
|
|
fail "the master role exists but the bootstrap receipt directory ${RECEIPT_DIR} could not be created"
|
|
|
|
_tmp="${RECEIPT_FILE}.new"
|
|
{
|
|
echo "# Written by rds-init once the master role was applied. Holds no secrets."
|
|
echo "RDS_RECEIPT_PAYLOAD_ID=${RDS_PAYLOAD_ID}"
|
|
echo "RDS_RECEIPT_DB_INSTANCE_IDENTIFIER=${RDS_DB_INSTANCE_IDENTIFIER}"
|
|
echo "RDS_RECEIPT_VM_GENERATION=${RDS_VM_GENERATION}"
|
|
echo "RDS_RECEIPT_COMPLETED_AT=$(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
|
} > "${_tmp}" ||
|
|
fail "the master role exists but the bootstrap receipt ${RECEIPT_FILE} could not be written"
|
|
chown "${PG_USER}:${PG_USER}" "${_tmp}"
|
|
chmod 0600 "${_tmp}"
|
|
|
|
# A receipt still in the page cache is lost by exactly the crash it exists to
|
|
# survive, so it is on disk before it is renamed into place.
|
|
sync "${_tmp}" 2>/dev/null || sync
|
|
mv -f "${_tmp}" "${RECEIPT_FILE}" ||
|
|
fail "the master role exists but the bootstrap receipt ${RECEIPT_FILE} could not be installed"
|
|
sync "${RECEIPT_DIR}" 2>/dev/null || sync
|
|
}
|
|
|
|
# PG_VERSION with no sentinel and no receipt is ambiguous from the datadir alone:
|
|
# either a legacy instance that bootstrapped before receipts existed, which must
|
|
# attach, or one that applied the master role and crashed before the receipt was
|
|
# durable, which must not. Only the control plane's view of whether the payload
|
|
# is still staged separates the two, and that is what RDS_BOOTSTRAP_PENDING says.
|
|
require_bootstrap_receipt() {
|
|
[ "${RDS_BOOTSTRAP_PENDING}" = "1" ] || return 0
|
|
if [ -n "${RDS_PAYLOAD_ID}" ] &&
|
|
[ "$(receipt_value RDS_RECEIPT_PAYLOAD_ID)" = "${RDS_PAYLOAD_ID}" ] &&
|
|
[ "$(receipt_value RDS_RECEIPT_DB_INSTANCE_IDENTIFIER)" = "${RDS_DB_INSTANCE_IDENTIFIER}" ]; then
|
|
return 0
|
|
fi
|
|
fail "the control plane has no record of a completed bootstrap for this datadir; refusing to attach to a datadir whose master role cannot be proven. Recover the volume out of band, or delete and recreate the instance."
|
|
}
|
|
|
|
# Retires the guards armed at initdb, in an order where no signal in between can
|
|
# clear a datadir that now carries a usable master role.
|
|
mark_bootstrap_complete() {
|
|
_bootstrapped=1
|
|
rm -f "${BOOTSTRAP_SENTINEL}" ||
|
|
fail "the master role exists but ${BOOTSTRAP_SENTINEL} could not be removed; the next boot will refuse to start until it is deleted"
|
|
trap - EXIT INT TERM HUP
|
|
}
|
|
|
|
# The master role is administrative but deliberately not a PostgreSQL SUPERUSER,
|
|
# as on AWS: a superuser reaches outside the database entirely — COPY FROM
|
|
# PROGRAM, pg_read_file/pg_write_file, untrusted languages — so master
|
|
# credentials would be a shell in the DB VM rather than a database privilege.
|
|
# The cluster's own superuser stays the bootstrap role, which only rds-init and
|
|
# rds-agent reach, over the unix socket under peer auth.
|
|
apply_master_role() {
|
|
log "creating the master role ${RDS_MASTER_USERNAME}"
|
|
# PG16 hardened CREATEROLE: it can neither grant itself SUPERUSER nor take
|
|
# membership in a predefined role it does not administer, so CREATEROLE here
|
|
# is not a route back to the privileges above.
|
|
psql_bootstrap <<'SQL'
|
|
\getenv master RDS_MASTER_USERNAME
|
|
\getenv password RDS_MASTER_PASSWORD
|
|
\getenv grp RDS_GROUP_ROLE
|
|
SELECT NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = :'grp') AS create_group \gset
|
|
\if :create_group
|
|
CREATE ROLE :"grp" NOLOGIN;
|
|
\endif
|
|
GRANT pg_monitor, pg_signal_backend, pg_checkpoint TO :"grp";
|
|
SELECT NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = :'master') AS create_role \gset
|
|
\if :create_role
|
|
CREATE ROLE :"master" WITH LOGIN NOSUPERUSER CREATEDB CREATEROLE;
|
|
\endif
|
|
ALTER ROLE :"master" WITH LOGIN NOSUPERUSER CREATEDB CREATEROLE PASSWORD :'password';
|
|
GRANT :"grp" TO :"master" WITH ADMIN OPTION;
|
|
SQL
|
|
}
|
|
|
|
# As on AWS, an instance created without a DBName gets no initial database.
|
|
create_database() {
|
|
log "creating database ${RDS_DB_NAME}"
|
|
psql_bootstrap <<'SQL'
|
|
\getenv master RDS_MASTER_USERNAME
|
|
\getenv dbname RDS_DB_NAME
|
|
SELECT NOT EXISTS (SELECT 1 FROM pg_database WHERE datname = :'dbname') AS create_db \gset
|
|
\if :create_db
|
|
CREATE DATABASE :"dbname" OWNER :"master";
|
|
\endif
|
|
SQL
|
|
}
|
|
|
|
# Drops the master password from the handoff once applied, so it is not left
|
|
# readable for the life of the boot. The rest is tmpfs and goes at reboot.
|
|
consume_secrets() {
|
|
[ -f "${ENV_FILE}" ] || return 0
|
|
_tmp="${ENV_FILE}.new"
|
|
install -m 0600 /dev/null "${_tmp}"
|
|
grep -v '^RDS_MASTER_PASSWORD=' "${ENV_FILE}" >> "${_tmp}" || true
|
|
mv -f "${_tmp}" "${ENV_FILE}"
|
|
}
|
|
|
|
load_handoff
|
|
require_data_volume
|
|
|
|
if [ -s "${PGDATA}/PG_VERSION" ]; then
|
|
# The engine would start cleanly on such a datadir and pass every health
|
|
# probe with no master role to log in as. The sentinel says the bootstrap did
|
|
# not finish, not that the volume is empty, so nothing here clears it.
|
|
if [ -e "${BOOTSTRAP_SENTINEL}" ]; then
|
|
fail "datadir ${PGDATA} carries ${BOOTSTRAP_SENTINEL}: an earlier bootstrap was interrupted before the master role existed; recover the data volume out of band rather than starting the engine"
|
|
fi
|
|
require_bootstrap_receipt
|
|
log "datadir ${PGDATA} is already initialised — attaching"
|
|
install_runtime_config
|
|
else
|
|
initialise_cluster
|
|
install_runtime_config
|
|
bootstrap_master
|
|
fi
|
|
|
|
consume_secrets
|
|
log "bootstrap complete; handing off to the postgresql service"
|