restore-drill.sh 3.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109
  1. #!/bin/sh
  2. # briven — restore drill. pulls the most recent backup off-site, restores
  3. # it into a throwaway postgres database, runs sanity counts, then drops
  4. # everything. intended to run monthly on the same kvm that takes the
  5. # nightly backup so we never discover at 3am that a backup is unusable.
  6. #
  7. # the drill exercises the full path: download → checksum → restore →
  8. # query → drop. failure on ANY step exits non-zero so the cron's discord
  9. # webhook fires.
  10. #
  11. # required env:
  12. # BRIVEN_BACKUP_B2_KEY_ID
  13. # BRIVEN_BACKUP_B2_APP_KEY
  14. # BRIVEN_BACKUP_B2_BUCKET
  15. # BRIVEN_DRILL_PG_HOST — host running a restore-only postgres
  16. # BRIVEN_DRILL_PG_PORT — default 5432
  17. # BRIVEN_DRILL_PG_USER — superuser that can CREATE/DROP DATABASE
  18. # BRIVEN_DRILL_PG_PASSWORD
  19. #
  20. # optional env:
  21. # BRIVEN_BACKUP_PREFIX — default: prod
  22. #
  23. # the script picks the lexicographically-latest dated folder under the
  24. # prefix, which works because pg-dump.sh names folders with the timestamp.
  25. set -eu
  26. prefix=${BRIVEN_BACKUP_PREFIX:-prod}
  27. ts=$(date -u +%Y%m%d-%H%M%S)
  28. work=/tmp/briven-restore-drill-$ts
  29. trap 'rm -rf "$work"' EXIT
  30. mkdir -p "$work"
  31. export B2_APPLICATION_KEY_ID="$BRIVEN_BACKUP_B2_KEY_ID"
  32. export B2_APPLICATION_KEY="$BRIVEN_BACKUP_B2_APP_KEY"
  33. echo "[restore-drill $ts] picking latest backup folder"
  34. latest=$(b2 ls "$BRIVEN_BACKUP_B2_BUCKET/$prefix/" --json \
  35. | jq -r '.[].fileName' \
  36. | sed 's|/$||' \
  37. | sort \
  38. | tail -n 1)
  39. if [ -z "$latest" ]; then
  40. echo "[restore-drill $ts] no backups under prefix=$prefix — drill cannot proceed" >&2
  41. exit 1
  42. fi
  43. echo "[restore-drill $ts] latest=$latest"
  44. echo "[restore-drill $ts] downloading"
  45. b2 sync "b2://$BRIVEN_BACKUP_B2_BUCKET/$latest" "$work"
  46. echo "[restore-drill $ts] checksum verify"
  47. ( cd "$work" && sha256sum -c sha256sums.txt ) || {
  48. echo "[restore-drill $ts] checksum mismatch — refusing to restore" >&2
  49. exit 2
  50. }
  51. restore_db="briven_drill_$(date -u +%s)"
  52. export PGHOST="$BRIVEN_DRILL_PG_HOST"
  53. export PGPORT="${BRIVEN_DRILL_PG_PORT:-5432}"
  54. export PGUSER="$BRIVEN_DRILL_PG_USER"
  55. export PGPASSWORD="$BRIVEN_DRILL_PG_PASSWORD"
  56. echo "[restore-drill $ts] CREATE DATABASE $restore_db"
  57. psql -d postgres -c "CREATE DATABASE $restore_db" >/dev/null
  58. # Trap the drop so a partial restore still cleans up.
  59. trap '
  60. rm -rf "$work";
  61. psql -d postgres -c "DROP DATABASE IF EXISTS $restore_db" >/dev/null 2>&1 || true
  62. ' EXIT
  63. echo "[restore-drill $ts] restoring control.dump → $restore_db"
  64. pg_restore --no-owner --no-privileges --dbname="$restore_db" "$work/control.dump"
  65. echo "[restore-drill $ts] sanity counts on the meta-db"
  66. # These tables exist on every shipped revision of the control schema. If
  67. # any of them are zero, the dump didn't capture the rows we expect —
  68. # fail the drill loudly so j gets paged.
  69. fail=0
  70. for t in users projects deployments organizations subscriptions; do
  71. n=$(psql -d "$restore_db" -tAc "SELECT count(*) FROM $t" 2>/dev/null || echo "ERROR")
  72. echo "[restore-drill $ts] $t = $n"
  73. if [ "$n" = "ERROR" ]; then
  74. echo "[restore-drill $ts] $t MISSING — drill failed" >&2
  75. fail=1
  76. fi
  77. done
  78. echo "[restore-drill $ts] restoring data.dump → $restore_db"
  79. pg_restore --no-owner --no-privileges --dbname="$restore_db" "$work/data.dump"
  80. # At least one project schema must be present in the data dump (else
  81. # we restored an empty cluster and the drill is meaningless).
  82. schemas=$(psql -d "$restore_db" -tAc \
  83. "SELECT count(*) FROM information_schema.schemata WHERE schema_name LIKE 'proj_%'")
  84. echo "[restore-drill $ts] data-plane project schemas restored: $schemas"
  85. if [ "$schemas" -lt 1 ]; then
  86. echo "[restore-drill $ts] no proj_* schemas — drill failed" >&2
  87. fail=1
  88. fi
  89. if [ "$fail" -ne 0 ]; then
  90. echo "[restore-drill $ts] FAILED" >&2
  91. exit 3
  92. fi
  93. echo "[restore-drill $ts] OK — backup taken at $latest restores cleanly"