90 lines
2.4 KiB
Bash
90 lines
2.4 KiB
Bash
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
|
|
CONTAINER="geointel"
|
|
BACKUP_DIR=""
|
|
|
|
usage() {
|
|
cat <<'EOF'
|
|
Usage: bash scripts/verify_release_backup.sh --backup-dir PATH [--container NAME]
|
|
|
|
Verifies checksums, manifest shape and PostgreSQL archive readability. This
|
|
command is read-only and never creates, restores or drops a database.
|
|
EOF
|
|
}
|
|
|
|
while [ "$#" -gt 0 ]; do
|
|
case "$1" in
|
|
--backup-dir) BACKUP_DIR="$2"; shift 2 ;;
|
|
--container) CONTAINER="$2"; shift 2 ;;
|
|
--help|-h) usage; exit 0 ;;
|
|
*) echo "Unknown argument: $1" >&2; usage >&2; exit 2 ;;
|
|
esac
|
|
done
|
|
|
|
if [ -z "$BACKUP_DIR" ]; then
|
|
usage >&2
|
|
exit 2
|
|
fi
|
|
for required in docker python3 sha256sum; do
|
|
command -v "$required" >/dev/null 2>&1 || {
|
|
echo "Missing required command: $required" >&2
|
|
exit 2
|
|
}
|
|
done
|
|
BACKUP_DIR="$(python3 -c 'import pathlib,sys; print(pathlib.Path(sys.argv[1]).expanduser().resolve())' "$BACKUP_DIR")"
|
|
for required_file in manifest.json database.dump database.list database-metadata.tsv table-counts.tsv CHECKSUMS.sha256; do
|
|
test -s "$BACKUP_DIR/$required_file" || {
|
|
echo "Missing or empty backup artifact: $required_file" >&2
|
|
exit 3
|
|
}
|
|
done
|
|
|
|
(
|
|
cd "$BACKUP_DIR"
|
|
sha256sum -c CHECKSUMS.sha256
|
|
)
|
|
|
|
python3 - "$BACKUP_DIR/manifest.json" <<'PY'
|
|
import json
|
|
import pathlib
|
|
import sys
|
|
|
|
payload = json.loads(pathlib.Path(sys.argv[1]).read_text(encoding="utf-8"))
|
|
required = {
|
|
"schema_version",
|
|
"release_id",
|
|
"created_at",
|
|
"read_only_source",
|
|
"database_name",
|
|
"database_user",
|
|
"image_id",
|
|
"git_commit",
|
|
}
|
|
missing = sorted(required - payload.keys())
|
|
if missing:
|
|
raise SystemExit(f"Backup manifest misses: {', '.join(missing)}")
|
|
if payload["schema_version"] != 1 or payload["read_only_source"] is not True:
|
|
raise SystemExit("Unsupported or unsafe backup manifest")
|
|
PY
|
|
|
|
if [ "$(docker inspect -f '{{.State.Running}}' "$CONTAINER" 2>/dev/null || true)" != "true" ]; then
|
|
echo "Container '$CONTAINER' is required to run pg_restore --list." >&2
|
|
exit 3
|
|
fi
|
|
TMP_LIST="$(mktemp)"
|
|
trap 'rm -f -- "$TMP_LIST"' EXIT
|
|
docker exec -i "$CONTAINER" pg_restore --list \
|
|
< "$BACKUP_DIR/database.dump" \
|
|
> "$TMP_LIST"
|
|
cmp -s "$TMP_LIST" "$BACKUP_DIR/database.list" || {
|
|
echo "The current pg_restore listing differs from the retained listing." >&2
|
|
exit 3
|
|
}
|
|
grep -q "TABLE DATA" "$TMP_LIST" || {
|
|
echo "Archive contains no table data entries." >&2
|
|
exit 3
|
|
}
|
|
|
|
echo "Release backup verified read-only: $BACKUP_DIR"
|