mirror of
https://github.com/mulgadc/spinifex.git
synced 2026-10-19 10:52:04 +00:00
step 8
This commit is contained in:
@@ -29,11 +29,14 @@ INSTALL_BINARIES="bin/rds-agent:/usr/local/bin/rds-agent"
|
||||
INSTALL_FILES="\
|
||||
scripts/images/common/rds-datadir:/usr/local/sbin/rds-datadir \
|
||||
scripts/images/rds-mariadb/rds-datadir.initd:/etc/init.d/rds-datadir \
|
||||
scripts/images/rds-mariadb/rds-init:/usr/local/sbin/rds-init \
|
||||
scripts/images/rds-mariadb/rds-init.initd:/etc/init.d/rds-init \
|
||||
scripts/images/rds-mariadb/rds-agent.initd:/etc/init.d/rds-agent \
|
||||
scripts/images/common/mulga-mgmt-net.initd:/etc/init.d/mulga-mgmt-net \
|
||||
scripts/images/common/mulga-mgmt-net-routes.initd:/etc/init.d/mulga-mgmt-net-routes \
|
||||
scripts/images/common/mulga-mgmt-net.sh:/usr/local/sbin/mulga-mgmt-net \
|
||||
scripts/images/rds-mariadb/zz-rds-include.cnf:/etc/my.cnf.d/zz-rds-include.cnf"
|
||||
scripts/images/rds-mariadb/zz-rds-include.cnf:/etc/my.cnf.d/zz-rds-include.cnf \
|
||||
scripts/images/rds-mariadb/mariadb.confd:/etc/conf.d/mariadb"
|
||||
|
||||
# Each service's own depend() asserts the ordering; this lists it to match.
|
||||
# mulga-mgmt-net is NOT here: setup.sh adds it to the boot runlevel, since a
|
||||
@@ -41,10 +44,10 @@ scripts/images/rds-mariadb/zz-rds-include.cnf:/etc/my.cnf.d/zz-rds-include.cnf"
|
||||
# route-enforcement second pass is the opposite — it has to follow cloud-init, so
|
||||
# the default runlevel is exactly where it belongs.
|
||||
#
|
||||
# mariadb is deliberately absent while this image has no bootstrap: the packaged
|
||||
# service only warns about an empty datadir instead of refusing, so an enabled
|
||||
# engine would launch mysqld_safe against an unbootstrapped data volume.
|
||||
ENABLE_SERVICES="rds-datadir rds-agent mulga-mgmt-net-routes"
|
||||
# The packaged mariadb service only warns about an empty datadir instead of
|
||||
# refusing, so it is not ordering that keeps it off an unbootstrapped volume:
|
||||
# /etc/conf.d/mariadb makes it need rds-init, which fails closed.
|
||||
ENABLE_SERVICES="rds-datadir rds-agent rds-init mariadb mulga-mgmt-net-routes"
|
||||
|
||||
SETUP_SCRIPT="scripts/images/rds-mariadb/setup.sh"
|
||||
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
# /etc/conf.d/mariadb — Spinifex RDS overrides for the stock Alpine mariadb
|
||||
# service. The package ships no conf.d file of its own, so this adds one rather
|
||||
# than replacing one.
|
||||
|
||||
# The packaged service's start_pre only prints "Datadir is empty or invalid" and
|
||||
# starts anyway, so an rds-init that failed — an unreachable gateway, a data
|
||||
# volume that did not attach, a datadir stamped for another engine — would be
|
||||
# answered by launching mysqld_safe against whatever is mounted. OpenRC reads
|
||||
# rc_need from here, so the engine depends on the bootstrap without the packaged
|
||||
# init script being patched.
|
||||
rc_need="rds-init"
|
||||
|
||||
# Everything else the service resolves from the configuration itself: datadir,
|
||||
# socket and port come from the platform drop-in rds-init writes onto the data
|
||||
# volume, which the packaged !includedir reads last.
|
||||
@@ -7,8 +7,8 @@ set -eu
|
||||
# the control plane launches instances with. A drift between them launches the
|
||||
# wrong image, refuses the right one, or reports a version the VM is not running.
|
||||
#
|
||||
# This preset carries no bootstrap yet, so the datadir stamp and the agent's
|
||||
# layout table are checked by the postgres preset's copy of this test only.
|
||||
# The agent's layout table is checked by the postgres preset's copy of this test
|
||||
# only, until this engine has a guest implementation to key it by.
|
||||
|
||||
SCRIPT_DIR=$(CDPATH='' cd -- "$(dirname -- "$0")" && pwd)
|
||||
REPO_ROOT=$(CDPATH='' cd -- "${SCRIPT_DIR}/../../.." && pwd)
|
||||
@@ -32,6 +32,11 @@ if ! grep -q "Name: *\"${ENGINE}\"" "${ENGINE_GO}"; then
|
||||
fail "the control plane has no engine named ${ENGINE}"
|
||||
fi
|
||||
|
||||
if ! grep -q "^ENGINE=\"${ENGINE}\"\$" "${SCRIPT_DIR}/rds-init"; then
|
||||
fail "rds-init does not stamp the data volume '${ENGINE}'"
|
||||
grep '^ENGINE=' "${SCRIPT_DIR}/rds-init" >&2
|
||||
fi
|
||||
|
||||
# The tag is what an instance's recorded EngineVersion resolves an AMI by, so it
|
||||
# has to be the same series the control plane pins and the packages install.
|
||||
version=$(sed -n 's/^[[:space:]]*MajorVersion: *"\([^"]*\)".*/\1/p' "${ENGINE_GO}")
|
||||
|
||||
@@ -0,0 +1,846 @@
|
||||
#!/bin/sh
|
||||
# rds-init — MariaDB bootstrap for the spinifex-rds-mariadb AMI.
|
||||
#
|
||||
# A boot oneshot ordered before the mariadb service. This is the MariaDB half of
|
||||
# the engine seam: mariadb-install-db, the master grant, the datadir and the 3306
|
||||
# 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 user 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 3306>
|
||||
# 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, one
|
||||
# name = 'value' per line
|
||||
# /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. A datadir holding the mysql
|
||||
# system database means skip mariadb-install-db 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
|
||||
|
||||
# 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 MariaDB half of the seam, so it is the same constant.
|
||||
ENGINE="mariadb"
|
||||
|
||||
MARIADB_BIN="${RDS_MARIADB_BIN:-/usr/bin}"
|
||||
ENGINE_USER="${RDS_ENGINE_USER:-mysql}"
|
||||
|
||||
# Mount point of the data volume, and the datadir one level inside it. The
|
||||
# datadir is deliberately not the mount point itself: a failed bootstrap clears
|
||||
# it, and the include directory, the engine stamp and the completion receipt all
|
||||
# have to outlive that. It also keeps lost+found and the include directory out of
|
||||
# a directory whose every subdirectory MariaDB lists as a database.
|
||||
DATA_MOUNT="${RDS_DATA_MOUNT:-/var/lib/mysql}"
|
||||
DATADIR="${RDS_DATADIR:-${DATA_MOUNT}/data}"
|
||||
|
||||
# Read through the image's baked /etc/my.cnf.d/zz-rds-include.cnf. Generated
|
||||
# configuration lives here rather than under /etc because a class change is a VM
|
||||
# replacement — fresh boot volume, same data volume — and parameters under /etc
|
||||
# would be discarded by it while the API still reported the customer's values.
|
||||
CONF_DIR="${RDS_CONF_DIR:-${DATA_MOUNT}/conf.d}"
|
||||
PARAM_FILE="${CONF_DIR}/10-rds-parameters.cnf"
|
||||
# What the server actually started on. The agent computes pending-restart by
|
||||
# comparing the installed file against this one, so writing it here — before the
|
||||
# engine starts — is what makes the two equal by construction on every boot.
|
||||
# Deliberately not a .cnf name: !includedir reads *.cnf and this is comparison
|
||||
# material, not a second set of settings.
|
||||
SERVING_FILE="${CONF_DIR}/10-rds-parameters.serving"
|
||||
PLATFORM_FILE="${CONF_DIR}/90-rds-init.cnf"
|
||||
|
||||
# Written between mariadb-install-db and a working master user. 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 user was never created.
|
||||
BOOTSTRAP_SENTINEL="${DATADIR}/rds-bootstrap-incomplete"
|
||||
|
||||
# Proof that the master user was durably applied, written once the datadir and
|
||||
# the user are both done. Deliberately outside DATADIR, which
|
||||
# clear_unfinished_datadir rm -rf's.
|
||||
RECEIPT_DIR="${RDS_RECEIPT_DIR:-${DATA_MOUNT}/.spinifex-rds/bootstrap}"
|
||||
RECEIPT_FILE="${RECEIPT_DIR}/receipt.env"
|
||||
|
||||
# Which engine's data this volume holds. Outside DATADIR 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/mysqld}"
|
||||
SOCKET="${SOCKET_DIR}/mysqld.sock"
|
||||
TLS_DIR="${RDS_TLS_DIR:-${SOCKET_DIR}/tls}"
|
||||
LOG_DIR="${RDS_LOG_DIR:-/var/log/mysql}"
|
||||
|
||||
# The bootstrap server listens on a unix socket in this directory and nowhere
|
||||
# else. It is 0700 and mysql-owned, so only mysql and root can reach the short
|
||||
# window before the master user exists.
|
||||
BOOTSTRAP_DIR="${RDS_BOOTSTRAP_DIR:-/run/rds-init}"
|
||||
BOOTSTRAP_SOCKET="${BOOTSTRAP_DIR}/mysqld.sock"
|
||||
BOOTSTRAP_PID="${BOOTSTRAP_DIR}/mysqld.pid"
|
||||
|
||||
# secure_file_priv names the one directory FILE-privileged statements may read
|
||||
# and write. It must be a real directory: the empty string means no restriction
|
||||
# at all. Root-owned and empty, on the boot volume, so it stays empty.
|
||||
SECURE_FILE_DIR="${RDS_SECURE_FILE_DIR:-/var/lib/mysql-files}"
|
||||
|
||||
MOUNTS_FILE="${RDS_MOUNTS_FILE:-/proc/mounts}"
|
||||
|
||||
# How long the private bootstrap server gets to open its socket, as probes one
|
||||
# BOOTSTRAP_POLL apart. InnoDB creates its redo log and system tablespace on this
|
||||
# first start, so it is not instant.
|
||||
BOOTSTRAP_PROBES="${RDS_BOOTSTRAP_PROBES:-60}"
|
||||
BOOTSTRAP_POLL="${RDS_BOOTSTRAP_POLL:-1}"
|
||||
|
||||
# The server truncates a longer name, so the database created would not be the
|
||||
# one the customer asked for, and MariaDB maps the name onto a directory name.
|
||||
MAX_DB_NAME_LEN=64
|
||||
MAX_USERNAME_LEN=80
|
||||
|
||||
log() { echo "[rds-init] $*"; }
|
||||
fail() { echo "[rds-init] ERROR: $*" >&2; exit 1; }
|
||||
|
||||
# Between these two, the datadir carries a mysql system database but no usable
|
||||
# master user. _server_pid is the private bootstrap server, if one is running.
|
||||
_created_datadir=0
|
||||
_bootstrapped=0
|
||||
_server_pid=""
|
||||
|
||||
# Runs from a trap rather than a statement, so neither `set -e` on an unguarded
|
||||
# step between mariadb-install-db and a working master user nor a signal can skip
|
||||
# it — either would leave a healthy-looking engine with no usable master user,
|
||||
# unrepairable.
|
||||
clear_unfinished_datadir() {
|
||||
[ "${_created_datadir}" = "1" ] || return 0
|
||||
[ "${_bootstrapped}" = "1" ] && return 0
|
||||
# A server left holding a datadir that is about to be deleted keeps its
|
||||
# socket and pidfile and blocks the mariadb service for the life of the boot.
|
||||
kill_bootstrap_server
|
||||
# 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 -- "${DATADIR}" && [ ! -e "${DATADIR}" ]; then
|
||||
echo "[rds-init] ERROR: bootstrap did not complete; datadir ${DATADIR} cleared" >&2
|
||||
return 0
|
||||
fi
|
||||
echo "[rds-init] ERROR: bootstrap did not complete and datadir ${DATADIR} could not be cleared; recover it out of band before retrying" >&2
|
||||
}
|
||||
|
||||
kill_bootstrap_server() {
|
||||
[ -n "${_server_pid}" ] || return 0
|
||||
kill -TERM "${_server_pid}" 2>/dev/null || true
|
||||
_server_pid=""
|
||||
}
|
||||
|
||||
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:-3306}"
|
||||
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 name is interpolated into CREATE USER by the shell — the mariadb client
|
||||
# has no identifier quoting of its own — so the rule the control plane
|
||||
# applies is enforced here rather than trusted.
|
||||
case "${RDS_MASTER_USERNAME}" in
|
||||
[!A-Za-z]* | *[!A-Za-z0-9_]*)
|
||||
fail "RDS_MASTER_USERNAME ${RDS_MASTER_USERNAME} must begin with a letter and hold only letters, digits and underscores; the control plane should have rejected it"
|
||||
;;
|
||||
esac
|
||||
if [ "${#RDS_MASTER_USERNAME}" -gt "${MAX_USERNAME_LEN}" ]; then
|
||||
fail "RDS_MASTER_USERNAME is longer than the ${MAX_USERNAME_LEN} characters MariaDB allows; the control plane should have rejected it"
|
||||
fi
|
||||
|
||||
# root is the superuser mariadb-install-db creates and rds-init keeps for
|
||||
# itself, mysql is the unix_socket account beside it, mariadb.sys owns the sys
|
||||
# schema, rdsadmin is reserved as on AWS, and PUBLIC is a role name the server
|
||||
# refuses to create a user under. Matched the same way the control plane
|
||||
# matches them: anything it refuses must be refused here too, or the name
|
||||
# reaches mariadb-install-db, fails at CREATE USER and spends the one-shot
|
||||
# password on the way.
|
||||
_master_lower=$(printf '%s' "${RDS_MASTER_USERNAME}" | tr '[:upper:]' '[:lower:]')
|
||||
case "${_master_lower}" in
|
||||
root | mysql | mariadb.sys | rdsadmin | public | mysql.* | mariadb.*)
|
||||
fail "RDS_MASTER_USERNAME ${RDS_MASTER_USERNAME} is reserved by the platform; the control plane should have rejected it"
|
||||
;;
|
||||
esac
|
||||
|
||||
# The same rule the control plane applies. Here it is the barrier rather than
|
||||
# defence in depth: the mariadb client has no identifier interpolation, so the
|
||||
# name is built into CREATE DATABASE by the shell, and MariaDB maps a database
|
||||
# name onto a directory name — which makes /, \, . and a trailing space
|
||||
# structurally dangerous rather than merely awkward.
|
||||
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 MariaDB 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
|
||||
|
||||
# The form the password takes inside the SQL, computed once beside it. A
|
||||
# statement echoed on failure carries that form and not the raw secret, so
|
||||
# redacting only the raw one would put a trivially unescaped password on the
|
||||
# console for exactly the passwords that needed escaping.
|
||||
RDS_MASTER_PASSWORD_SQL=""
|
||||
if [ -n "${RDS_MASTER_PASSWORD}" ]; then
|
||||
RDS_MASTER_PASSWORD_SQL=$(sql_literal "${RDS_MASTER_PASSWORD}")
|
||||
fi
|
||||
|
||||
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)"
|
||||
}
|
||||
|
||||
# True when the datadir holds anything at all. A failed enumeration is neither
|
||||
# "empty" nor "has content": it is a refusal, because both callers below decide
|
||||
# whether something may be created or deleted from the answer.
|
||||
datadir_has_content() {
|
||||
[ -d "${DATADIR}" ] || return 1
|
||||
_contents=$(ls -A -- "${DATADIR}" 2>/dev/null) ||
|
||||
fail "could not inspect datadir ${DATADIR}; refusing to decide whether it is ours"
|
||||
[ -n "${_contents}" ]
|
||||
}
|
||||
|
||||
# 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 "${ENGINE_USER}" -g "${ENGINE_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 "${ENGINE_USER}:${ENGINE_USER}" "${_tmp}"
|
||||
chmod 0600 "${_tmp}"
|
||||
|
||||
# Renamed rather than written in place, so a crash leaves either the old
|
||||
# stamp or none — and none is decided on the datadir, 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 run mariadb-install-db
|
||||
# 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 MariaDB volume in the field predates the stamp, so unlike PostgreSQL
|
||||
# there is nothing to backfill onto: an unstamped datadir with content in it
|
||||
# is by definition not ours, and stamping it would turn the check into a
|
||||
# rubber stamp on exactly the volume it exists to refuse.
|
||||
if datadir_has_content; then
|
||||
fail "${DATADIR} holds data but the volume carries no ${ENGINE} engine stamp; refusing to touch it. Recover the volume out of band."
|
||||
fi
|
||||
|
||||
log "data volume carries no engine stamp — recording ${ENGINE}"
|
||||
write_engine_stamp
|
||||
}
|
||||
|
||||
# Creates the system tables in an empty datadir. Root authenticates through the
|
||||
# unix_socket plugin — the direct analogue of PostgreSQL's peer auth, and the
|
||||
# reason no bootstrap password is needed.
|
||||
initialise_datadir() {
|
||||
# An empty datadir in `attach` mode means the data volume did not come back.
|
||||
# Initialising there would report success and hand the customer an empty
|
||||
# database in place of theirs.
|
||||
if [ "${RDS_MODE}" = "attach" ]; then
|
||||
fail "datadir ${DATADIR} holds no MariaDB system database but the bootstrap config is Mode=attach — the existing data volume is missing; refusing to create an empty database"
|
||||
fi
|
||||
[ -n "${RDS_MASTER_PASSWORD}" ] || fail "Mode=${RDS_MODE:-unset} carries no master password; cannot initialise the datadir"
|
||||
|
||||
install -d -m 0700 -o "${ENGINE_USER}" -g "${ENGINE_USER}" "${DATADIR}"
|
||||
|
||||
# Whether the datadir was empty beforehand gates the failure cleanup below.
|
||||
# A torn write can leave the mysql system database missing over intact user
|
||||
# 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 -- "${DATADIR}" 2>/dev/null); then
|
||||
[ -z "${_contents}" ] && _was_empty=1
|
||||
else
|
||||
fail "could not inspect datadir ${DATADIR}; refusing to decide whether failure cleanup is safe"
|
||||
fi
|
||||
|
||||
log "initialising the MariaDB datadir at ${DATADIR}"
|
||||
# --no-defaults: the installed configuration includes the customer's own
|
||||
# parameter file, and a static value this server will not start under would
|
||||
# spend the one-shot master password on a datadir that never gets one. The
|
||||
# real server starts on those parameters, where a rollback exists for them.
|
||||
if ! "${MARIADB_BIN}/mariadb-install-db" --no-defaults \
|
||||
--user="${ENGINE_USER}" --datadir="${DATADIR}" \
|
||||
--auth-root-authentication-method=socket --skip-test-db; then
|
||||
# A half-written datadir carries a mysql directory and would look
|
||||
# initialised forever after, but only what this invocation created may be
|
||||
# cleared.
|
||||
if [ "${_was_empty}" = "1" ]; then
|
||||
if rm -rf -- "${DATADIR}" && [ ! -e "${DATADIR}" ]; then
|
||||
fail "mariadb-install-db failed; datadir cleared"
|
||||
fi
|
||||
fail "mariadb-install-db failed and datadir ${DATADIR} could not be cleared; recover it out of band before retrying"
|
||||
fi
|
||||
fail "mariadb-install-db failed and ${DATADIR} was not empty beforehand — refusing to clear it, since the contents may be a datadir whose system tables were lost; recover the data volume out of band"
|
||||
fi
|
||||
|
||||
# Only a datadir 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 user exists.
|
||||
printf '%s\n' \
|
||||
'rds-init created this datadir and did not finish bootstrapping the master user.' \
|
||||
'While this file exists rds-init refuses to start the engine: the datadir carries no' \
|
||||
'usable master user, so it must not be attached to. Recover out of band.' \
|
||||
> "${BOOTSTRAP_SENTINEL}"
|
||||
chown "${ENGINE_USER}:${ENGINE_USER}" "${BOOTSTRAP_SENTINEL}"
|
||||
chmod 0600 "${BOOTSTRAP_SENTINEL}"
|
||||
}
|
||||
|
||||
# The resolved parameter group, prefixed with the group header. MariaDB parses
|
||||
# each file in an include directory on its own and treats a setting before any
|
||||
# group as a fatal error, so the header belongs to whoever writes the file rather
|
||||
# than to the engine-neutral renderer that produced its body.
|
||||
install_parameters() {
|
||||
if [ -f "${HANDOFF_DIR}/parameters.conf" ]; then
|
||||
_tmp="${CONF_DIR}/.10-rds-parameters.cnf.new"
|
||||
{ printf '[mysqld]\n'; cat "${HANDOFF_DIR}/parameters.conf"; } > "${_tmp}" ||
|
||||
fail "the resolved parameter group could not be written to ${PARAM_FILE}"
|
||||
chown "${ENGINE_USER}:${ENGINE_USER}" "${_tmp}"
|
||||
chmod 0600 "${_tmp}"
|
||||
mv -f "${_tmp}" "${PARAM_FILE}"
|
||||
record_serving_parameters
|
||||
return 0
|
||||
fi
|
||||
# A handoff without parameters keeps the previous include: dropping it would
|
||||
# silently reset the instance to engine defaults.
|
||||
if [ -f "${PARAM_FILE}" ]; then
|
||||
log "warning: no parameters in the handoff — keeping the parameters already installed"
|
||||
record_serving_parameters
|
||||
fi
|
||||
}
|
||||
|
||||
# The engine is about to start on whatever is installed, so the two are equal by
|
||||
# construction here. The agent refreshes this copy only after a restart it
|
||||
# performs itself: a live SET GLOBAL apply deliberately leaves it alone, since
|
||||
# that value was adopted without a restart, which is what "not pending" means.
|
||||
record_serving_parameters() {
|
||||
install -m 0600 -o "${ENGINE_USER}" -g "${ENGINE_USER}" "${PARAM_FILE}" "${SERVING_FILE}" ||
|
||||
fail "the serving copy of the parameters could not be written to ${SERVING_FILE}"
|
||||
}
|
||||
|
||||
# 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 "${ENGINE_USER}" -g "${ENGINE_USER}" "${TLS_DIR}"
|
||||
install -m 0600 -o "${ENGINE_USER}" -g "${ENGINE_USER}" "${HANDOFF_DIR}/server.crt" "${TLS_DIR}/server.crt"
|
||||
install -m 0600 -o "${ENGINE_USER}" -g "${ENGINE_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 --ssl-verify-server-cert.
|
||||
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 the include directory and MariaDB takes the
|
||||
# last occurrence of a setting, so a resolved parameter can never move the
|
||||
# endpoint's port, redirect the datadir or turn off TLS.
|
||||
#
|
||||
# innodb_buffer_pool_chunk_size and innodb_buffer_pool_instances are deliberately
|
||||
# absent although they are platform-owned: 11.8 removed the second outright, so
|
||||
# naming it is a startup failure rather than an override, and the first is
|
||||
# deprecated with no effect. The granularity they used to pin is fixed by the
|
||||
# server itself in this series.
|
||||
write_platform_config() {
|
||||
_ssl="$1"
|
||||
_tmp="${CONF_DIR}/.90-rds-init.cnf.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.
|
||||
[mysqld]
|
||||
port = ${RDS_PORT}
|
||||
datadir = ${DATADIR}
|
||||
socket = ${SOCKET}
|
||||
bind_address = 0.0.0.0
|
||||
|
||||
# InnoDB is the only storage engine the snapshot guarantee holds for: it keeps a
|
||||
# redo log and recovers from a crash-consistent snapshot, and Aria and MyISAM do
|
||||
# not. An explicit ENGINE= clause still works, which keeps the exception
|
||||
# deliberate and per-table rather than account-wide and invisible.
|
||||
default_storage_engine = InnoDB
|
||||
|
||||
# Point-in-time recovery is not offered, so the binary log is off: it would cost
|
||||
# write throughput and disk for a capability nothing reads.
|
||||
skip-log-bin
|
||||
|
||||
# Defence in depth behind the master grant, which withholds FILE: an empty
|
||||
# secure_file_priv would mean no restriction at all, so it names a root-owned
|
||||
# directory that stays empty.
|
||||
secure_file_priv = ${SECURE_FILE_DIR}
|
||||
skip-symbolic-links
|
||||
EOF
|
||||
if [ "${_ssl}" = "on" ]; then
|
||||
# Offered, not enforced, matching the PostgreSQL image's pg_hba: clients
|
||||
# opt in. require_secure_transport is deliberately absent until enforcement
|
||||
# lands for both engines at once. No ssl_ca, because client certificates
|
||||
# are not required and it would only gate those.
|
||||
cat >> "${_tmp}" <<EOF
|
||||
ssl_cert = ${TLS_DIR}/server.crt
|
||||
ssl_key = ${TLS_DIR}/server.key
|
||||
EOF
|
||||
fi
|
||||
chown "${ENGINE_USER}:${ENGINE_USER}" "${_tmp}"
|
||||
chmod 0600 "${_tmp}"
|
||||
mv -f "${_tmp}" "${PLATFORM_FILE}"
|
||||
}
|
||||
|
||||
# Everything refreshed on every boot: the resolved parameters and their serving
|
||||
# copy, the serving cert and the platform's own settings.
|
||||
install_runtime_config() {
|
||||
install -d -m 0750 -o "${ENGINE_USER}" -g "${ENGINE_USER}" "${CONF_DIR}"
|
||||
install -d -m 0755 -o "${ENGINE_USER}" -g "${ENGINE_USER}" "${SOCKET_DIR}"
|
||||
install -d -m 0750 -o "${ENGINE_USER}" -g "${ENGINE_USER}" "${LOG_DIR}"
|
||||
install -d -m 0700 "${SECURE_FILE_DIR}"
|
||||
|
||||
install_parameters
|
||||
install_tls
|
||||
write_platform_config "${SSL_MODE}"
|
||||
}
|
||||
|
||||
# Brings the server up on a private socket with no TCP listener at all, so the
|
||||
# master password can be set before the engine is reachable. --no-defaults for
|
||||
# the same reason mariadb-install-db has it, and because the password below is
|
||||
# escaped for a known sql_mode rather than the customer's.
|
||||
start_bootstrap_server() {
|
||||
install -d -m 0700 -o "${ENGINE_USER}" -g "${ENGINE_USER}" "${BOOTSTRAP_DIR}"
|
||||
|
||||
"${MARIADB_BIN}/mariadbd" --no-defaults \
|
||||
--user="${ENGINE_USER}" --datadir="${DATADIR}" \
|
||||
--skip-networking --socket="${BOOTSTRAP_SOCKET}" \
|
||||
--pid-file="${BOOTSTRAP_PID}" --log-error="${LOG_DIR}/rds-init.log" &
|
||||
_server_pid=$!
|
||||
|
||||
_probes=0
|
||||
while ! bootstrap_admin ping > /dev/null 2>&1; do
|
||||
# The server writes why it gave up to its error log, which is on the boot
|
||||
# volume and readable after the fact; the console only gets the refusal.
|
||||
if ! kill -0 "${_server_pid}" 2>/dev/null; then
|
||||
_server_pid=""
|
||||
fail "the bootstrap server exited before it opened ${BOOTSTRAP_SOCKET}; see ${LOG_DIR}/rds-init.log"
|
||||
fi
|
||||
if [ "${_probes}" -ge "${BOOTSTRAP_PROBES}" ]; then
|
||||
fail "the bootstrap server did not open ${BOOTSTRAP_SOCKET} after ${BOOTSTRAP_PROBES} probes ${BOOTSTRAP_POLL}s apart; see ${LOG_DIR}/rds-init.log"
|
||||
fi
|
||||
sleep "${BOOTSTRAP_POLL}"
|
||||
_probes=$((_probes + 1))
|
||||
done
|
||||
}
|
||||
|
||||
bootstrap_admin() {
|
||||
"${MARIADB_BIN}/mariadb-admin" --no-defaults \
|
||||
--socket="${BOOTSTRAP_SOCKET}" --user=root "$@"
|
||||
}
|
||||
|
||||
# Returns the shutdown's status, then the server's own: a caller reading this
|
||||
# must be able to tell "it refused to stop" from "it stopped badly", and both
|
||||
# from a server still holding the datadir.
|
||||
stop_bootstrap_server() {
|
||||
[ -n "${_server_pid}" ] || return 0
|
||||
bootstrap_admin shutdown || return 1
|
||||
|
||||
# `wait` rather than a kill -0 poll: the server is this shell's own child, so
|
||||
# until it is reaped it stays a zombie that kill -0 reports as alive.
|
||||
_wait_rc=0
|
||||
wait "${_server_pid}" || _wait_rc=$?
|
||||
_server_pid=""
|
||||
return "${_wait_rc}"
|
||||
}
|
||||
|
||||
# Prepended to every bootstrap script. The two logs that would copy the password
|
||||
# out of the statement are turned off — global rather than session variables, and
|
||||
# safe to set globally because nothing but rds-init can reach this server. The
|
||||
# mode is pinned because the password is escaped below for a mode that has
|
||||
# NO_BACKSLASH_ESCAPES off.
|
||||
BOOTSTRAP_SQL_GUARD="SET GLOBAL general_log = 0;
|
||||
SET GLOBAL slow_query_log = 0;
|
||||
SET SESSION sql_mode = 'NO_ENGINE_SUBSTITUTION';"
|
||||
|
||||
# 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
|
||||
}
|
||||
|
||||
# A value safe to interpolate into a single-quoted SQL literal. The quote is
|
||||
# doubled, which holds in every sql_mode, and the backslash is doubled, which is
|
||||
# why the guard above pins one where backslash escapes are on. The secret reaches
|
||||
# sed on stdin: an argv would be readable by every process in the guest.
|
||||
sql_literal() {
|
||||
printf '%s' "$1" | sed -e 's/\\/\\\\/g' -e "s/'/''/g"
|
||||
}
|
||||
|
||||
# The script rides stdin and never an argv, and both streams are redacted before
|
||||
# they reach the caller: the client echoes the statement it failed on, and the
|
||||
# initd sends everything here to /dev/console, which the host captures off ttyS0.
|
||||
mariadb_bootstrap() {
|
||||
# A pipeline's status is its last command's and POSIX sh has no pipefail, so
|
||||
# the client's own status leaves the subshell through the filesystem. The
|
||||
# guard shifts the client's reported line numbers by the lines it adds.
|
||||
_status_file="${BOOTSTRAP_DIR}/client.status"
|
||||
rm -f "${_status_file}"
|
||||
{
|
||||
_client_rc=0
|
||||
# --batch stops at the first error and exits non-zero, so a failed
|
||||
# statement cannot leave the rest of the script running.
|
||||
{ printf '%s\n' "${BOOTSTRAP_SQL_GUARD}"; cat; } |
|
||||
"${MARIADB_BIN}/mariadb" --no-defaults --batch --skip-column-names \
|
||||
--socket="${BOOTSTRAP_SOCKET}" --user=root ||
|
||||
_client_rc=$?
|
||||
printf '%s\n' "${_client_rc}" > "${_status_file}"
|
||||
} 2>&1 | redact_stream "${RDS_MASTER_PASSWORD}" | redact_stream "${RDS_MASTER_PASSWORD_SQL}"
|
||||
|
||||
# Every way of not recovering the client's status is a failed bootstrap
|
||||
# rather than a silent success, and each says so in its own words: blaming
|
||||
# the client for a status file that could not be written would send an
|
||||
# operator looking at a MariaDB 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.
|
||||
_client_status=1
|
||||
if [ -f "${_status_file}" ]; then
|
||||
_client_status=$(cat "${_status_file}" 2>/dev/null) || _client_status=""
|
||||
case "${_client_status}" in
|
||||
'' | *[!0-9]*)
|
||||
echo "[rds-init] ERROR: the mariadb client ran but its exit status could not be read back from ${_status_file} (got '${_client_status}'); treating the bootstrap as failed" >&2
|
||||
_client_status=1
|
||||
;;
|
||||
esac
|
||||
rm -f "${_status_file}"
|
||||
else
|
||||
echo "[rds-init] ERROR: the mariadb client ran but its exit status was never recorded at ${_status_file}; treating the bootstrap as failed" >&2
|
||||
fi
|
||||
return "${_client_status}"
|
||||
}
|
||||
|
||||
# 11.8 creates no anonymous account and --skip-test-db leaves no test database,
|
||||
# so on a current image both of these are no-ops. They run anyway: the properties
|
||||
# are asserted by the image tests, and an engine or packaging change that
|
||||
# reintroduced either would otherwise reach a customer's instance.
|
||||
remove_insecure_defaults() {
|
||||
mariadb_bootstrap <<'SQL'
|
||||
DELETE FROM mysql.global_priv WHERE User = '';
|
||||
DROP DATABASE IF EXISTS test;
|
||||
FLUSH PRIVILEGES;
|
||||
SQL
|
||||
}
|
||||
|
||||
# The master user is administrative but deliberately holds none of the
|
||||
# privileges that reach outside the database: FILE is SELECT INTO OUTFILE and
|
||||
# LOAD_FILE against any path as the mysql OS user, INSERT on mysql.* is native
|
||||
# code execution through CREATE FUNCTION ... SONAME, SUPER and its 11.8 split-out
|
||||
# successors reach SET GLOBAL and plugin loading, and SHUTDOWN stops the server
|
||||
# underneath the control plane's lifecycle. Master credentials must be a database
|
||||
# privilege, not a shell in the DB VM.
|
||||
apply_master_user() {
|
||||
log "creating the master user ${RDS_MASTER_USERNAME}"
|
||||
# Enumerated rather than GRANT ALL followed by revocations: a MariaDB release
|
||||
# that adds a privilege would otherwise hand it to the master silently.
|
||||
# '%' because the customer reaches the instance over its customer ENI.
|
||||
mariadb_bootstrap <<SQL
|
||||
CREATE USER IF NOT EXISTS '${RDS_MASTER_USERNAME}'@'%' IDENTIFIED BY '${RDS_MASTER_PASSWORD_SQL}';
|
||||
ALTER USER '${RDS_MASTER_USERNAME}'@'%' IDENTIFIED BY '${RDS_MASTER_PASSWORD_SQL}';
|
||||
GRANT SELECT, INSERT, UPDATE, DELETE, CREATE, DROP, RELOAD, PROCESS,
|
||||
REFERENCES, INDEX, ALTER, SHOW DATABASES, CREATE TEMPORARY TABLES,
|
||||
LOCK TABLES, EXECUTE, REPLICATION SLAVE, REPLICATION CLIENT,
|
||||
CREATE VIEW, SHOW VIEW, CREATE ROUTINE, ALTER ROUTINE,
|
||||
CREATE USER, EVENT, TRIGGER
|
||||
ON *.* TO '${RDS_MASTER_USERNAME}'@'%' WITH GRANT OPTION;
|
||||
SQL
|
||||
}
|
||||
|
||||
# The resolved value of one setting in the installed parameter file, or nothing
|
||||
# when the group does not carry it.
|
||||
resolved_parameter() {
|
||||
[ -f "${PARAM_FILE}" ] || return 0
|
||||
sed -n "s/^[[:space:]]*$1[[:space:]]*=[[:space:]]*//p" "${PARAM_FILE}" |
|
||||
tail -n 1 | tr -d "\"'[:space:]"
|
||||
}
|
||||
|
||||
# The bootstrap server runs on --no-defaults, so it is not carrying the character
|
||||
# set the real server will start with — and a database keeps the character set it
|
||||
# was created under for good. Both values therefore come from the parameter file
|
||||
# rather than from the server's compiled default.
|
||||
database_charset_clause() {
|
||||
_charset=$(resolved_parameter character_set_server)
|
||||
_collation=$(resolved_parameter collation_server)
|
||||
if [ -z "${_charset}" ]; then
|
||||
_charset="utf8mb4"
|
||||
_collation=""
|
||||
fi
|
||||
# Interpolated into CREATE DATABASE below, and read out of a file rather than
|
||||
# validated as an identifier on the way in.
|
||||
case "${_charset}${_collation}" in
|
||||
*[!A-Za-z0-9_]*)
|
||||
fail "the parameter group's character_set_server or collation_server is not a bare identifier; refusing to interpolate it into CREATE DATABASE"
|
||||
;;
|
||||
esac
|
||||
if [ -n "${_collation}" ]; then
|
||||
printf 'CHARACTER SET %s COLLATE %s' "${_charset}" "${_collation}"
|
||||
return 0
|
||||
fi
|
||||
printf 'CHARACTER SET %s' "${_charset}"
|
||||
}
|
||||
|
||||
# As on AWS, an instance created without a DBName gets no initial database. The
|
||||
# master reaches it through the global grant above, so nothing further is granted
|
||||
# on it here.
|
||||
create_database() {
|
||||
log "creating database ${RDS_DB_NAME}"
|
||||
_charset_clause=$(database_charset_clause)
|
||||
mariadb_bootstrap <<SQL
|
||||
CREATE DATABASE IF NOT EXISTS \`${RDS_DB_NAME}\` ${_charset_clause};
|
||||
SQL
|
||||
}
|
||||
|
||||
# Runs on a freshly initialised datadir only.
|
||||
bootstrap_master() {
|
||||
log "starting bootstrap server on ${BOOTSTRAP_DIR}"
|
||||
start_bootstrap_server
|
||||
|
||||
_rc=0
|
||||
remove_insecure_defaults || _rc=$?
|
||||
if [ "${_rc}" -eq 0 ]; then
|
||||
apply_master_user || _rc=$?
|
||||
fi
|
||||
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=$?
|
||||
|
||||
# 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 user/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 user exists from here, so the datadir is complete and nothing
|
||||
# may clear it — including on the stop failure reported below.
|
||||
mark_bootstrap_complete
|
||||
|
||||
# A server still holding the datadir blocks the mariadb 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 ${DATADIR}; the mariadb 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 user 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 "${ENGINE_USER}" -g "${ENGINE_USER}" "${RECEIPT_DIR}" ||
|
||||
fail "the master user exists but the bootstrap receipt directory ${RECEIPT_DIR} could not be created"
|
||||
|
||||
_tmp="${RECEIPT_FILE}.new"
|
||||
{
|
||||
echo "# Written by rds-init once the master user 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 user exists but the bootstrap receipt ${RECEIPT_FILE} could not be written"
|
||||
chown "${ENGINE_USER}:${ENGINE_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 user exists but the bootstrap receipt ${RECEIPT_FILE} could not be installed"
|
||||
sync "${RECEIPT_DIR}" 2>/dev/null || sync
|
||||
}
|
||||
|
||||
# A system database with no sentinel and no receipt is ambiguous from the datadir
|
||||
# alone: either an instance that bootstrapped before receipts existed, which must
|
||||
# attach, or one that applied the master user 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 user cannot be proven. Recover the volume out of band, or delete and recreate the instance."
|
||||
}
|
||||
|
||||
# Retires the guards armed at initialisation, in an order where no signal in
|
||||
# between can clear a datadir that now carries a usable master user.
|
||||
mark_bootstrap_complete() {
|
||||
_bootstrapped=1
|
||||
rm -f "${BOOTSTRAP_SENTINEL}" ||
|
||||
fail "the master user exists but ${BOOTSTRAP_SENTINEL} could not be removed; the next boot will refuse to start until it is deleted"
|
||||
trap - EXIT INT TERM HUP
|
||||
}
|
||||
|
||||
# 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 [ -d "${DATADIR}/mysql" ]; then
|
||||
# The engine would start cleanly on such a datadir and pass every health
|
||||
# probe with no master user 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 ${DATADIR} carries ${BOOTSTRAP_SENTINEL}: an earlier bootstrap was interrupted before the master user existed; recover the data volume out of band rather than starting the engine"
|
||||
fi
|
||||
require_bootstrap_receipt
|
||||
log "datadir ${DATADIR} is already initialised — attaching"
|
||||
install_runtime_config
|
||||
else
|
||||
initialise_datadir
|
||||
install_runtime_config
|
||||
bootstrap_master
|
||||
fi
|
||||
|
||||
consume_secrets
|
||||
log "bootstrap complete; handing off to the mariadb service"
|
||||
@@ -0,0 +1,35 @@
|
||||
#!/sbin/openrc-run
|
||||
# rds-init — boot oneshot that bootstraps or attaches the MariaDB datadir before
|
||||
# the engine starts.
|
||||
#
|
||||
# `before mariadb` orders the two, and /etc/conf.d/mariadb makes the engine need
|
||||
# this oneshot: Alpine's packaged mariadb service only prints a warning when the
|
||||
# datadir is empty and starts anyway, so ordering alone would let mysqld_safe
|
||||
# launch against an unbootstrapped data volume. `after rds-agent`, not `need`, so
|
||||
# a failed fetch fails this oneshot visibly rather than skipping it.
|
||||
|
||||
description="Spinifex RDS MariaDB bootstrap"
|
||||
|
||||
AGENT_ENV="/etc/spinifex-rds/agent.env"
|
||||
|
||||
depend() {
|
||||
need net
|
||||
after rds-agent rds-datadir
|
||||
before mariadb
|
||||
}
|
||||
|
||||
start_pre() {
|
||||
if [ -f "${AGENT_ENV}" ]; then
|
||||
# shellcheck disable=SC1090
|
||||
. "${AGENT_ENV}"
|
||||
export RDS_HANDOFF_DIR RDS_DATA_MOUNT
|
||||
fi
|
||||
}
|
||||
|
||||
start() {
|
||||
ebegin "Bootstrapping the RDS MariaDB instance"
|
||||
# The VM has no operator login, so the host-side serial log is the only view
|
||||
# of a bootstrap that failed before the engine came up.
|
||||
/usr/local/sbin/rds-init > /dev/console 2>&1
|
||||
eend $?
|
||||
}
|
||||
Executable
+1036
File diff suppressed because it is too large
Load Diff
@@ -29,16 +29,40 @@ assert_line() {
|
||||
|
||||
capture_dependencies rds-agent.initd > "${WORK}/agent"
|
||||
capture_dependencies rds-datadir.initd > "${WORK}/datadir"
|
||||
capture_dependencies rds-init.initd > "${WORK}/init"
|
||||
|
||||
assert_line "${WORK}/agent" "need net"
|
||||
assert_line "${WORK}/agent" "after localmount"
|
||||
assert_line "${WORK}/agent" "before rds-datadir rds-init mariadb"
|
||||
assert_line "${WORK}/datadir" "after localmount rds-agent"
|
||||
assert_line "${WORK}/datadir" "before rds-init mariadb"
|
||||
assert_line "${WORK}/init" "need net"
|
||||
assert_line "${WORK}/init" "after rds-agent rds-datadir"
|
||||
assert_line "${WORK}/init" "before mariadb"
|
||||
|
||||
if grep -Fq "before rds-agent" "${WORK}/datadir"; then
|
||||
echo "FAIL: rds-datadir still starts before the bootstrap handoff" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Ordering alone is not enough for this engine: Alpine's mariadb service only
|
||||
# warns about an empty datadir and starts anyway, so a failed rds-init would be
|
||||
# answered by launching mysqld_safe against an unbootstrapped volume. OpenRC
|
||||
# reads the dependency out of the service's conf.d file.
|
||||
grep -Fq 'rc_need="rds-init"' "${SCRIPT_DIR}/mariadb.confd" || {
|
||||
echo "FAIL: /etc/conf.d/mariadb does not make the engine need rds-init" >&2
|
||||
exit 1
|
||||
}
|
||||
grep -Fq "mariadb.confd:/etc/conf.d/mariadb" "${SCRIPT_DIR}/manifest.conf" || {
|
||||
echo "FAIL: manifest.conf does not install mariadb.confd" >&2
|
||||
exit 1
|
||||
}
|
||||
for svc in rds-init mariadb; do
|
||||
grep -qE "^ENABLE_SERVICES=.*[\" ]${svc}[\" ]" "${SCRIPT_DIR}/manifest.conf" || {
|
||||
echo "FAIL: manifest.conf does not enable ${svc}" >&2
|
||||
grep '^ENABLE_SERVICES' "${SCRIPT_DIR}/manifest.conf" >&2
|
||||
exit 1
|
||||
}
|
||||
done
|
||||
|
||||
echo "rds-openrc: dependency order tests passed"
|
||||
|
||||
@@ -5,9 +5,10 @@ set -eu
|
||||
# the libguestfs appliance under build-system-image.sh, after packages and
|
||||
# INSTALL_FILES are placed.
|
||||
|
||||
# INSTALL_FILES land 0644; OpenRC requires 0755 on init scripts, and rds-datadir
|
||||
# is executed directly by its service.
|
||||
chmod 0755 /etc/init.d/rds-datadir /etc/init.d/rds-agent /usr/local/sbin/rds-datadir
|
||||
# INSTALL_FILES land 0644; OpenRC requires 0755 on init scripts, and rds-init
|
||||
# and rds-datadir are executed directly by their services.
|
||||
chmod 0755 /etc/init.d/rds-datadir /etc/init.d/rds-init /etc/init.d/rds-agent \
|
||||
/usr/local/sbin/rds-datadir /usr/local/sbin/rds-init
|
||||
chmod 0755 /etc/init.d/mulga-mgmt-net /etc/init.d/mulga-mgmt-net-routes \
|
||||
/usr/local/sbin/mulga-mgmt-net
|
||||
|
||||
@@ -61,6 +62,15 @@ if grep -rEq '^[[:space:]]*skip[-_]networking' /etc/my.cnf /etc/my.cnf.d; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# The packaged service starts on a warning when the datadir is empty, so the
|
||||
# dependency is what keeps it off an unbootstrapped volume. Asserted here, since
|
||||
# an image whose engine can come up beside a failed bootstrap is the one failure
|
||||
# this preset must not have.
|
||||
grep -q '^rc_need="rds-init"' /etc/conf.d/mariadb || {
|
||||
echo "[rds-mariadb-setup] /etc/conf.d/mariadb does not make the engine need rds-init"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Bind /dev/console to the serial port so userspace boot output reaches ttyS0,
|
||||
# which the orchestrator captures host-side. Linux makes the last console= the
|
||||
# controlling one, and stock Alpine lists tty0 last — reorder so ttyS0 wins.
|
||||
|
||||
Reference in New Issue
Block a user