railiance-apps/tools/apps-pg-restore-drill.sh

101 lines
3.4 KiB
Bash
Raw Normal View History

#!/usr/bin/env bash
# Isolated apps-pg restore drill for vergabe_db (RAILIANCE-WP-0013-T04).
# Restores a local pg_dump into a temporary database on the apps-pg cluster.
set -euo pipefail
APPS_PG_NAMESPACE="${APPS_PG_NAMESPACE:-databases}"
APPS_PG_CLUSTER="${APPS_PG_CLUSTER:-apps-pg}"
APPS_PG_POD="${APPS_PG_POD:-}"
SOURCE_DB="${APPS_PG_DATABASE:-vergabe_db}"
BACKUP_DIR="${BACKUP_DIR:-$HOME/.cache/railiance/backups/apps-pg}"
BACKUP_FILE="${APPS_PG_BACKUP_FILE:-}"
DRILL_DB="${APPS_PG_RESTORE_DB:-vergabe_restore_drill}"
EVIDENCE_DIR="${EVIDENCE_DIR:-$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)/docs/evidence}"
if [[ -z "$APPS_PG_POD" ]]; then
APPS_PG_POD="$(
kubectl get pods -n "$APPS_PG_NAMESPACE" \
-l "cnpg.io/cluster=${APPS_PG_CLUSTER},role=primary" \
-o jsonpath='{.items[0].metadata.name}' 2>/dev/null || true
)"
fi
if [[ -z "$APPS_PG_POD" ]]; then
echo "ERROR: primary pod for ${APPS_PG_CLUSTER} not found" >&2
exit 1
fi
if [[ -z "$BACKUP_FILE" ]]; then
BACKUP_FILE="$(
find "$BACKUP_DIR" -maxdepth 1 -name "apps-pg-${SOURCE_DB}-*.dump" -type f 2>/dev/null \
| sort -r | head -1
)"
fi
if [[ -z "$BACKUP_FILE" || ! -f "$BACKUP_FILE" ]]; then
echo "ERROR: no backup file found for ${SOURCE_DB} under ${BACKUP_DIR}" >&2
echo "Run: make apps-pg-backup-dry-run" >&2
exit 1
fi
psql_exec() {
kubectl exec -n "$APPS_PG_NAMESPACE" "$APPS_PG_POD" -c postgres -- \
psql -U postgres -v ON_ERROR_STOP=1 "$@"
}
count_tables() {
local db="$1"
psql_exec -d "$db" -Atc \
"SELECT COALESCE(SUM(n_live_tup),0)::bigint FROM pg_stat_user_tables;"
}
echo "apps-pg restore drill"
echo " cluster: ${APPS_PG_CLUSTER}"
echo " pod: ${APPS_PG_POD}"
echo " source: ${SOURCE_DB}"
echo " backup: ${BACKUP_FILE}"
echo " drill db: ${DRILL_DB}"
echo
psql_exec -d postgres -c "DROP DATABASE IF EXISTS \"${DRILL_DB}\";"
psql_exec -d postgres -c "CREATE DATABASE \"${DRILL_DB}\";"
echo "ok: created drill database ${DRILL_DB}"
kubectl exec -i -n "$APPS_PG_NAMESPACE" "$APPS_PG_POD" -c postgres -- \
pg_restore -U postgres -d "$DRILL_DB" --no-owner --no-acl --clean --if-exists \
< "$BACKUP_FILE"
echo "ok: pg_restore completed"
PROD_ROWS="$(count_tables "$SOURCE_DB")"
DRILL_ROWS="$(count_tables "$DRILL_DB")"
echo "row estimate — production ${SOURCE_DB}: ${PROD_ROWS}"
echo "row estimate — drill ${DRILL_DB}: ${DRILL_ROWS}"
if [[ "$PROD_ROWS" != "$DRILL_ROWS" ]]; then
echo "ERROR: row-count mismatch between production and restore drill" >&2
exit 1
fi
echo "ok: row-count gate passed"
TS="$(date -u +%Y%m%dT%H%M%SZ)"
mkdir -p "$EVIDENCE_DIR"
EVIDENCE_FILE="${EVIDENCE_DIR}/apps-pg-restore-drill-${TS}.json"
python3 - <<PY
import json, pathlib
payload = {
"captured_at": "${TS}",
"workplan": "RAILIANCE-WP-0013",
"cluster": "${APPS_PG_CLUSTER}",
"source_database": "${SOURCE_DB}",
"restore_database": "${DRILL_DB}",
"backup_file": "$(basename "$BACKUP_FILE")",
"production_row_estimate": int("${PROD_ROWS}"),
"restored_row_estimate": int("${DRILL_ROWS}"),
"row_count_gate": "pass",
"notes": "Isolated restore on live CNPG primary; drill DB dropped after evidence write.",
}
path = pathlib.Path("${EVIDENCE_FILE}")
path.write_text(json.dumps(payload, indent=2) + "\\n", encoding="utf-8")
print(f"ok: evidence {path}")
PY
psql_exec -d postgres -c "DROP DATABASE IF EXISTS \"${DRILL_DB}\";"
echo "ok: dropped drill database ${DRILL_DB}"