Files
spinifex/scripts/images/rds-postgres/rds-init
T
Josh Siviour d52245001e feat: Ochre vector store (tenant pgvector, embeddings, ingestion, operator teardown) (#788)
* feat: add /v1/embeddings gateway adapter

* feat: Ochre vector store pgvector backend, KV index registry, orchestration

Adds handlers_ochrevector: the VectorBackend interface + pgx/v5 impl
with per-account schema/role isolation enforced via SET LOCAL ROLE +
search_path inside every transaction, the KV index registry
(CREATING/READY/DELETING/STALE), and the create/delete/list/reconcile
orchestration tying them together with crash-safe rollback and
re-entrant recovery of stuck records.

* feat: singleton appliance-ensure state machine for ochre vector store

Adds the encrypted control-plane credential store and re-entrant
Ensure state machine for the platform pgvector appliance (D2): a
fixed-key KV claim mirroring Registry.Reserve elects exactly one
winner to launch the appliance via an injected ApplianceLauncher,
losers wait or resume a stale crashed provision, and the master
password is generated once, stored only AES-256-GCM encrypted, and
decrypted solely inside Connect to build a pgxBackend.

* feat: Ochre vector store async ingestion (chunker, job store, worker)

Adds Stage 3: a deterministic recursive chunk splitter, a per-account
ingestion job-record store mirroring the index registry, the
transactional per-document replace on the backend, and the
IngestService worker that lists a predastore source, chunks and
embeds each object, and replaces its rows idempotently.

* feat: Ochre vector store similarity query and metadata filter grammar

Adds VectorBackend.Query (cosine kNN, k default/cap, iterative HNSW
scan under a filter) and a Bedrock-KB-shaped metadata Filter AST that
compiles to parameterized SQL over jsonb, closing off arbitrary SQL by
allowlisting jsonb keys and always binding values as params. Also
fixes RunJob reporting READY on a job that ingested nothing regardless
of failure cause, and stamps SourceSpec.Metadata onto ingested rows so
the new filters have data to match against.

* feat: Ochre vector store request/response API, VectorService, and CLI

Adds the daemon-facing VectorService interface (index CRUD, ingest,
describe job, query), its server implementation over the existing
registry/backend/embedder, a NATS-forwarding client, and the
`spx admin ochre vector` operator CLI. Query resolves and embeds
against each index's own pinned model; ingest stamps the same
model/dimension onto every source spec server-side. accountID is
always taken from the caller context, never the request payload.

Daemon subscription wiring and manifest registration are a later
change; this is pure request/response plumbing with no construction
into daemon.go or awsgw.go.

* feat: wire Ochre vector store VectorService into the daemon

Constructs the platform Postgres appliance and VectorService behind a new
OchreVector config section that defaults off, so existing daemons are
unaffected. Adds a real RDS-backed ApplianceLauncher that provisions the
appliance over rds.CreateDBInstance/DescribeDBInstances, registers the six
ochre.vector.* NATS subjects mirroring the bedrock endpoint service pattern,
and degrades to a disabled (nil) service on any boot-time appliance failure
rather than blocking daemon startup.

* fix: fix Ochre vector store boot ordering and self-register subjects

startOchreVector ran before subscribeAll, so its appliance launch fired
rds.CreateDBInstance/DescribeDBInstances requests with no responder yet
registered, timing out the launch on a first-enable boot. It also ran
synchronously, blocking daemon startup up to the appliance launch timeout.

Move it to a background goroutine started after subscribeAll returns, and
have it register its own ochre.vector.* subjects once the VectorService is
actually constructed, via a new registerNatsSubs helper shared with
subscribeAll's boot-time table. Guard d.natsSubscriptions with d.mu, since
this background registration can now race the shutdown path's
unsubscribe-all sweep.

* feat: bake pgvector into the shared rds-postgres image

* fix: reach the ochre appliance over a routed daemon port on its ENI IP

The daemon dialled the RDS appliance over rec.Endpoint, a vanity spx3.net
hostname the control-plane host can neither resolve nor route to, so a healthy
appliance was never reachable and the vector store disabled itself.

Resolve the appliance's customer-ENI private IP read-only via
ec2.DescribeNetworkInterfaces filtered on the spinifex:rds-db-instance tag, dial
that IP for the DSN instead of the hostname, and give the daemon its own routed
ENI in the appliance's subnet bound to an OVS host port so the IP is reachable.
Retry appliance startup with bounded backoff instead of hard-disabling on the
first transient.

* fix: install pgvector at bootstrap for the Ochre vector appliance

The daemon connects to the appliance as its NOSUPERUSER master, and the
image's pgvector is not a trusted extension, so CREATE EXTENSION vector
failed with permission denied. rds-init now installs it via the bootstrap
superuser, gated on the appliance's fixed master name so tenant instances
are unaffected.

* fix: grant the Ochre appliance master CREATE on its database

The daemon's EnsureAccount runs CREATE SCHEMA per account as the NOSUPERUSER
master, which does not own the postgres database, so bootstrap now grants it
CREATE on the database alongside the pgvector install. Both are gated to the
appliance master, so tenant instances are unaffected.

* fix: make the Ochre appliance's pgvector reachable under per-account isolation

The daemon connects as a NOSUPERUSER master and runs every account operation
under SET ROLE with a search_path pinned to the account schema. Two gaps blocked
that against a real non-superuser Postgres: the master could not SET ROLE into
the roles it creates, and the account role could not see the vector type.

Bootstrap now grants the appliance master createrole_self_grant and installs
pgvector into a read-only extensions schema granted to PUBLIC; the backend
carries that schema on each account search_path so the vector type resolves
without weakening per-account write isolation. All appliance-scoped.

* fix: prune stale VPC host port before installing the daemon appliance port

A replaced Ochre appliance is handed the same subnet IP, so a new daemon ENI
mints a fresh vhp- port at an address a superseded port still holds. The old
port survives at the OS/OVS layer even after its ENI record is wiped, so two
interfaces claim one address and the kernel can route to the dead one
(EHOSTUNREACH). installVPCHostPort now prunes any other vhp- port holding the
target address before installing.

* fix: gate VM restore's stale-socket kill on backing-store readiness

On daemon start, a live VM whose NBD sockets fail to dial was force-killed as an
orphan. While predastore/viperblock is still coming up after a restart those
sockets are transiently unreachable, so a healthy VM (the Ochre appliance was
the observed victim) was destroyed and its RDS record flipped to failed. The
kill now only fires when the backing store is confirmed up; otherwise the live
QEMU is kept and QMP reconnected. Genuine orphan reaping under a healthy store
is unchanged.

* fix: drive ochre ingest jobs from a daemon-side sweep scheduler

* feat: add operator teardown verb for the ochre platform appliance

Adds Appliance.Teardown to remove the singleton platform Postgres
appliance (RDS instance, host port, KV record), an admin-only NATS
subject and daemon handler to drive it, and `spx admin ochre appliance
teardown --confirm` to invoke it without exposing the action through
the tenant awsgw surface.

* fix: register ochre appliance teardown before Connect so it survives a broken appliance

* fix: purge ochre index and job metadata on appliance teardown

* test: mark ochre in-package test files for the external-test gate
2026-08-13 15:54:48 +10:00

696 lines
33 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.
#
# The data volume also carries an engine stamp, written before the datadir is
# inspected and compared on every boot, so another engine's volume is refused
# rather than initialised over.
#
# 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}"
# The engine this image bakes, stamped onto the data volume and compared against
# it on every boot. Held here rather than read from /etc/spinifex-rds/engine:
# this script is the PostgreSQL half of the seam, so it is the same constant.
ENGINE="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"
# Which engine's data this volume holds. Outside PGDATA for the same reason the
# receipt is, and beside it rather than inside the bootstrap directory: it
# outlives any one bootstrap and is read before the datadir is ever inspected.
ENGINE_STAMP="${RDS_ENGINE_STAMP:-${DATA_MOUNT}/.spinifex-rds/engine}"
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"
# The Ochre vector store appliance is the one platform (non-tenant) consumer of
# this image, provisioned with this fixed master name. Tenant instances take a
# different master, so the pgvector install below is scoped to the appliance.
OCHRE_APPLIANCE_MASTER="ochre_vector_admin"
# NAMEDATALEN-1: the server truncates a longer identifier, so the database
# created would not be the one the customer asked for.
MAX_DB_NAME_LEN=63
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
# Scopes the platform pgvector install to the appliance; a tenant master
# takes a different name and skips it.
RDS_IS_OCHRE_APPLIANCE=0
[ "${_master_lower}" = "${OCHRE_APPLIANCE_MASTER}" ] && RDS_IS_OCHRE_APPLIANCE=1
# The same rule the control plane applies, for the same reason the reserved
# name is re-checked: an unusable name reaches initdb, fails at CREATE
# DATABASE and spends the one-shot password on the way. psql quotes the
# identifier, so here this is defence in depth rather than the barrier.
case "${RDS_DB_NAME}" in
'') ;;
[!A-Za-z]* | *[!A-Za-z0-9_]*)
fail "RDS_DB_NAME ${RDS_DB_NAME} must begin with a letter and hold only letters, digits and underscores; the control plane should have rejected it"
;;
esac
if [ "${#RDS_DB_NAME}" -gt "${MAX_DB_NAME_LEN}" ]; then
fail "RDS_DB_NAME is longer than the ${MAX_DB_NAME_LEN} characters PostgreSQL allows; the control plane should have rejected it"
fi
case "${RDS_PORT}" in
'' | *[!0-9]*) fail "bootstrap config has a non-numeric RDS_PORT: ${RDS_PORT}" ;;
esac
# redact_stream is line-oriented, so a password spanning a line boundary
# would match neither half and reach /dev/console in the clear. The control
# plane rejects one; refusing here too keeps that an asserted property of
# the handoff rather than a silent dependency on another service.
case "${RDS_MASTER_PASSWORD}" in
*'
'*) fail "the master password in the bootstrap config spans more than one line; the control plane should have rejected it" ;;
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)"
}
# Written before the datadir is inspected, so a volume this image goes on to
# initialise carries the stamp from the first boot onwards. Fatal on failure:
# an unstamped volume is one the next boot cannot tell from another engine's.
write_engine_stamp() {
_stamp_dir=$(dirname "${ENGINE_STAMP}")
install -d -m 0700 -o "${PG_USER}" -g "${PG_USER}" "${_stamp_dir}" ||
fail "the datadir engine stamp directory ${_stamp_dir} could not be created; refusing to use a data volume whose engine cannot be recorded"
_tmp="${ENGINE_STAMP}.new"
printf '%s\n' "${ENGINE}" > "${_tmp}" ||
fail "the datadir engine stamp ${ENGINE_STAMP} could not be written; refusing to use a data volume whose engine cannot be recorded"
chown "${PG_USER}:${PG_USER}" "${_tmp}"
chmod 0600 "${_tmp}"
# Renamed rather than written in place, so a crash leaves either the old
# stamp or none — and none is backfilled, where a torn one would be fatal.
sync "${_tmp}" 2>/dev/null || sync
mv -f "${_tmp}" "${ENGINE_STAMP}" ||
fail "the datadir engine stamp ${ENGINE_STAMP} could not be installed; refusing to use a data volume whose engine cannot be recorded"
sync "${_stamp_dir}" 2>/dev/null || sync
}
# The volume mounts cleanly whichever engine wrote it, and another engine's
# datadir reads as uninitialised here: rds-init would initdb beside the
# customer's data and serve an empty database that passes every health probe.
require_engine_stamp() {
if [ -e "${ENGINE_STAMP}" ]; then
_stamped=""
_stamp_read=0
_stamped=$(head -n 1 -- "${ENGINE_STAMP}") && _stamp_read=1
[ "${_stamp_read}" = "1" ] ||
fail "the datadir engine stamp ${ENGINE_STAMP} exists but could not be read; refusing to attach to a data volume whose engine cannot be established"
if [ "${_stamped}" != "${ENGINE}" ]; then
fail "the data volume holds a '${_stamped}' datadir but this image runs ${ENGINE}; refusing to touch it. Attach it to a '${_stamped}' instance, or recover the volume out of band."
fi
log "data volume is stamped ${ENGINE}"
return 0
fi
# No stamp is either a volume predating it or one about to be initialised.
# Both are ours here — PostgreSQL had unstamped volumes in the field, so a
# non-empty datadir is backfilled rather than refused.
log "data volume carries no engine stamp — recording ${ENGINE}"
write_engine_stamp
}
# 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"
}
# Prepended to every bootstrap script. psql interpolates the password into the
# ALTER ROLE before the server sees it, and these three are what would then write
# it to the log: log_min_error_statement does so on any failure at its own
# default. All are SUSET and set for this session only, so nothing the customer
# puts in the parameter group overrides them.
BOOTSTRAP_LOG_GUARD="SET log_statement = 'none';
SET log_min_duration_statement = -1;
SET log_min_error_statement = 'panic';"
# Replaces every occurrence of the secret with a placeholder. Parameter
# expansion rather than sed, whose script would carry the secret in an argv any
# process on the guest can read.
redact_stream() {
_secret="$1"
while IFS= read -r _line || [ -n "${_line}" ]; do
_out=""
while [ -n "${_secret}" ]; do
case "${_line}" in
*"${_secret}"*)
_out="${_out}${_line%%"${_secret}"*}[REDACTED]"
_line="${_line#*"${_secret}"}"
;;
*) break ;;
esac
done
printf '%s\n' "${_out}${_line}"
done
}
# Identifiers and the password ride the environment and are re-quoted by psql,
# never reaching a shell word or argv. Both streams are redacted before they
# reach the caller: psql echoes the statement it failed on, and the initd sends
# everything here to /dev/console, which the host captures off ttyS0.
psql_bootstrap() {
# A pipeline's status is its last command's and POSIX sh has no pipefail, so
# psql's own status leaves the subshell through the filesystem. The guard
# shifts psql's <stdin> line numbers by the number of lines it adds.
_status_file="${BOOTSTRAP_DIR}/psql.status"
rm -f "${_status_file}"
{
_psql_rc=0
{ printf '%s\n' "${BOOTSTRAP_LOG_GUARD}"; cat; } |
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 - ||
_psql_rc=$?
printf '%s\n' "${_psql_rc}" > "${_status_file}"
} 2>&1 | redact_stream "${RDS_MASTER_PASSWORD}"
# Every way of not recovering psql's status is a failed bootstrap rather
# than a silent success, and each says so in its own words: blaming psql for
# a status file that could not be written would send an operator looking at
# a PostgreSQL that did exactly what it was asked.
#
# A torn write leaves the file created but empty, so the value is checked
# before it is returned: `return ""` is fatal in the guest's ash, and would
# kill rds-init here — past the sweep, before the diagnostics.
_psql_status=1
if [ -f "${_status_file}" ]; then
_psql_status=$(cat "${_status_file}" 2>/dev/null) || _psql_status=""
case "${_psql_status}" in
'' | *[!0-9]*)
echo "[rds-init] ERROR: psql ran but its exit status could not be read back from ${_status_file} (got '${_psql_status}'); treating the bootstrap as failed" >&2
_psql_status=1
;;
esac
rm -f "${_status_file}"
else
echo "[rds-init] ERROR: psql ran but its exit status was never recorded at ${_status_file}; treating the bootstrap as failed" >&2
fi
return "${_psql_status}"
}
# 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
if [ "${_rc}" -eq 0 ]; then
create_platform_extensions || _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
}
# pgvector is baked in but left uninstalled so tenant databases stay free of it.
# The appliance master is NOSUPERUSER, so the bootstrap superuser installs the
# extension into a read-only extensions schema and grants the master the database
# and role privileges its daemon needs to create a per-account schema and role.
# Scoped to the appliance master alone; tenant masters skip this entirely.
create_platform_extensions() {
[ "${RDS_IS_OCHRE_APPLIANCE:-0}" = "1" ] || return 0
log "configuring pgvector and platform privileges for the Ochre vector appliance"
psql_bootstrap <<SQL
CREATE SCHEMA IF NOT EXISTS extensions;
CREATE EXTENSION IF NOT EXISTS vector SCHEMA extensions;
GRANT USAGE ON SCHEMA extensions TO PUBLIC;
GRANT CREATE ON DATABASE postgres TO "${OCHRE_APPLIANCE_MASTER}";
ALTER ROLE "${OCHRE_APPLIANCE_MASTER}" SET createrole_self_grant = 'set, inherit';
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
require_engine_stamp
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"