Some checks failed
Release production image / production-image (push) Has been cancelled
82 lines
3.5 KiB
Bash
Executable File
82 lines
3.5 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
umask 077
|
|
|
|
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
|
COMPOSE_FILE="${ROOT_DIR}/compose.production.yml"
|
|
ENV_FILE="${ROOT_DIR}/.env"
|
|
BACKUP_DIR="${ROOT_DIR}/backups"
|
|
RETENTION_DAYS=7
|
|
SERVICE="conductor"
|
|
TIMESTAMP="$(date -u +%Y-%m-%dT%H-%M-%SZ)"
|
|
|
|
usage() {
|
|
printf '%s\n' "Usage: $0 [--compose-file PATH] [--env-file PATH] [--backup-dir PATH] [--retention-days N]" >&2
|
|
}
|
|
|
|
while [[ $# -gt 0 ]]; do
|
|
case "$1" in
|
|
--compose-file) COMPOSE_FILE="$2"; shift 2 ;;
|
|
--env-file) ENV_FILE="$2"; shift 2 ;;
|
|
--backup-dir) BACKUP_DIR="$2"; shift 2 ;;
|
|
--retention-days) RETENTION_DAYS="$2"; shift 2 ;;
|
|
-h|--help) usage; exit 0 ;;
|
|
*) echo "Unknown option: $1" >&2; usage; exit 1 ;;
|
|
esac
|
|
done
|
|
|
|
[[ -f "$COMPOSE_FILE" ]] || { echo "Compose file not found: $COMPOSE_FILE" >&2; exit 1; }
|
|
[[ -f "$ENV_FILE" ]] || { echo "Environment file not found: $ENV_FILE (it contains required recovery keys)." >&2; exit 1; }
|
|
[[ "$RETENTION_DAYS" =~ ^[0-9]+$ ]] || { echo "--retention-days must be a non-negative integer." >&2; exit 1; }
|
|
|
|
compose=(docker compose --env-file "$ENV_FILE" -f "$COMPOSE_FILE")
|
|
container_id="$("${compose[@]}" ps -q "$SERVICE")"
|
|
[[ -n "$container_id" ]] || { echo "Conductor container is not created." >&2; exit 1; }
|
|
[[ "$(docker inspect -f '{{.State.Running}}' "$container_id")" == "true" ]] || { echo "Conductor container is not running." >&2; exit 1; }
|
|
|
|
mkdir -p "$BACKUP_DIR"
|
|
chmod 700 "$BACKUP_DIR"
|
|
work_dir="$(mktemp -d)"
|
|
cleanup() { rm -rf -- "$work_dir"; }
|
|
trap cleanup EXIT
|
|
mkdir -p "$work_dir/database" "$work_dir/configuration"
|
|
|
|
staged="/data/.conductor-backup-${TIMESTAMP}.db"
|
|
echo "[*] Creating consistent online SQLite backup..." >&2
|
|
"${compose[@]}" exec -T "$SERVICE" node dist/scripts/backupDatabase.js "$staged" >&2
|
|
docker cp "${container_id}:${staged}" "$work_dir/database/conductor.db"
|
|
"${compose[@]}" exec -T "$SERVICE" node -e 'const fs=require("fs");fs.unlinkSync(process.argv[1])' "$staged"
|
|
|
|
cp "$ENV_FILE" "$work_dir/configuration/.env"
|
|
cp "$COMPOSE_FILE" "$work_dir/configuration/compose.production.yml"
|
|
image_ref="$(docker inspect -f '{{.Config.Image}}' "$container_id")"
|
|
image_id="$(docker inspect -f '{{.Image}}' "$container_id")"
|
|
cat > "$work_dir/manifest.json" <<EOF
|
|
{
|
|
"format": "conductor-backup-v1",
|
|
"createdAt": "$(date -u +%Y-%m-%dT%H:%M:%SZ)",
|
|
"image": "${image_ref}",
|
|
"imageId": "${image_id}",
|
|
"database": "database/conductor.db",
|
|
"environment": "configuration/.env"
|
|
}
|
|
EOF
|
|
(cd "$work_dir" && sha256sum database/conductor.db configuration/.env configuration/compose.production.yml manifest.json > SHA256SUMS)
|
|
|
|
archive="${BACKUP_DIR}/conductor_${TIMESTAMP}.tar.gz"
|
|
tar -czf "$archive" -C "$work_dir" database configuration manifest.json SHA256SUMS
|
|
chmod 600 "$archive"
|
|
sha256sum "$archive" > "${archive}.sha256"
|
|
chmod 600 "${archive}.sha256"
|
|
|
|
removed=0
|
|
while IFS= read -r -d '' old; do rm -f -- "$old" "${old}.sha256"; removed=$((removed + 1)); done < <(find "$BACKUP_DIR" -maxdepth 1 -name 'conductor_*.tar.gz' -mtime +"$RETENTION_DAYS" -print0)
|
|
size_bytes="$(stat -c '%s' "$archive")"
|
|
result="${BACKUP_DIR}/backup-result.json"
|
|
cat > "$result" <<EOF
|
|
{"script":"backup-conductor","status":"success","timestamp":"$(date -u +%Y-%m-%dT%H:%M:%SZ)","backup":{"file":"${archive}","sizeBytes":${size_bytes},"checksumFile":"${archive}.sha256"},"rotation":{"retentionDays":${RETENTION_DAYS},"filesRemoved":${removed}},"coverage":{"database":true,"encryptionKeys":true,"composeMetadata":true}}
|
|
EOF
|
|
chmod 600 "$result"
|
|
echo "[*] Backup complete: $archive" >&2
|
|
cat "$result"
|