forked from EduCraft/curriculum-project-hub
feat: add deployable alpha silo
This commit is contained in:
Executable
+81
@@ -0,0 +1,81 @@
|
||||
#!/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"
|
||||
Reference in New Issue
Block a user