forked from EduCraft/curriculum-project-hub
82 lines
3.2 KiB
Bash
Executable File
82 lines
3.2 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# Create a stopped-instance backup. Business data and recovery credentials are
|
|
# deliberately written to different root-owned destinations.
|
|
set -euo pipefail
|
|
|
|
INSTANCE_ID="${INSTANCE_ID:?INSTANCE_ID required}"
|
|
ENV_FILE="${ENV_FILE:?ENV_FILE required}"
|
|
KEYRING_FILE="${KEYRING_FILE:?KEYRING_FILE required}"
|
|
BUSINESS_BACKUP_DIR="${BUSINESS_BACKUP_DIR:?BUSINESS_BACKUP_DIR required}"
|
|
RECOVERY_BACKUP_DIR="${RECOVERY_BACKUP_DIR:?RECOVERY_BACKUP_DIR required}"
|
|
SERVICE_UNIT="cph-hub-$INSTANCE_ID.service"
|
|
|
|
[ "$(id -u)" -eq 0 ] || { echo "backup must run as root" >&2; exit 1; }
|
|
umask 077
|
|
|
|
validate_root_secret_file() {
|
|
local label="$1" path="$2" metadata
|
|
[[ "$path" = /* ]] || { echo "$label must be an absolute path" >&2; exit 1; }
|
|
[ ! -L "$path" ] || { echo "$label must not be a symlink: $path" >&2; exit 1; }
|
|
[ -f "$path" ] || { echo "$label must be a regular file: $path" >&2; exit 1; }
|
|
metadata="$(stat -c '%u:%g:%a' -- "$path")"
|
|
[ "$metadata" = "0:0:600" ] || {
|
|
echo "$label must be root:root mode 0600; found $metadata" >&2
|
|
exit 1
|
|
}
|
|
}
|
|
|
|
validate_root_secret_file "ENV_FILE" "$ENV_FILE"
|
|
validate_root_secret_file "KEYRING_FILE" "$KEYRING_FILE"
|
|
state="$(systemctl show --property=ActiveState --value "$SERVICE_UNIT")"
|
|
case "$state" in inactive|failed) ;; *) echo "$SERVICE_UNIT must be stopped; state=$state" >&2; exit 1;; esac
|
|
|
|
set -a
|
|
# shellcheck disable=SC1090
|
|
. "$ENV_FILE"
|
|
set +a
|
|
: "${DATABASE_URL:?DATABASE_URL missing from ENV_FILE}"
|
|
: "${HUB_PROJECT_WORKSPACE_ROOT:?HUB_PROJECT_WORKSPACE_ROOT missing from ENV_FILE}"
|
|
[ -d "$HUB_PROJECT_WORKSPACE_ROOT" ] || { echo "workspace root missing" >&2; exit 1; }
|
|
|
|
install -d -o root -g root -m 0700 "$BUSINESS_BACKUP_DIR" "$RECOVERY_BACKUP_DIR"
|
|
business_root="$(realpath -m "$BUSINESS_BACKUP_DIR")"
|
|
recovery_root="$(realpath -m "$RECOVERY_BACKUP_DIR")"
|
|
workspace_root="$(realpath -m "$HUB_PROJECT_WORKSPACE_ROOT")"
|
|
secret_root="$(realpath -m "$(dirname "$KEYRING_FILE")")"
|
|
paths_overlap() {
|
|
local left="$1" right="$2"
|
|
[ "$left" = "$right" ] || [[ "$left/" == "$right/"* ]] || [[ "$right/" == "$left/"* ]]
|
|
}
|
|
for pair in \
|
|
"$business_root|$recovery_root" \
|
|
"$business_root|$workspace_root" \
|
|
"$recovery_root|$workspace_root" \
|
|
"$recovery_root|$secret_root"; do
|
|
left="${pair%%|*}"; right="${pair#*|}"
|
|
if paths_overlap "$left" "$right"; then
|
|
echo "backup destinations must not overlap each other or live state: $left <> $right" >&2
|
|
exit 1
|
|
fi
|
|
done
|
|
stamp="$(date -u +%Y%m%dT%H%M%SZ)"
|
|
business="$business_root/$INSTANCE_ID-$stamp"
|
|
recovery="$recovery_root/$INSTANCE_ID-$stamp"
|
|
install -d -o root -g root -m 0700 "$business" "$recovery"
|
|
|
|
pg_dump --format=custom --file="$business/database.dump" "$DATABASE_URL"
|
|
tar --create --file="$business/workspaces.tar" --directory="$HUB_PROJECT_WORKSPACE_ROOT" .
|
|
cp --preserve=mode,ownership,timestamps "$KEYRING_FILE" "$recovery/secret-keyring.json"
|
|
cp --preserve=mode,ownership,timestamps "$ENV_FILE" "$recovery/platform.env"
|
|
chmod 0600 "$recovery/secret-keyring.json" "$recovery/platform.env"
|
|
|
|
(
|
|
cd "$business"
|
|
sha256sum database.dump workspaces.tar > SHA256SUMS
|
|
)
|
|
(
|
|
cd "$recovery"
|
|
sha256sum secret-keyring.json platform.env > SHA256SUMS
|
|
)
|
|
echo "business backup: $business"
|
|
echo "separate recovery backup: $recovery"
|