#!/usr/bin/env bash # Weekly restore test — proves backups are actually restorable, not just # byte-streams that look like backups. Restores latest backup into a # temporary DB inside the same Postgres container, runs a schema check, # then drops the temp DB. # # Cron: 30 4 * * 0 root /opt/bmm-ops/restore-test.sh (Sundays 04:30 UTC) set -uo pipefail BACKUP_DIR="/var/backups/bmm" LOG_FILE="/var/log/bmm-backup.log" NOTIFY="/opt/bmm-ops/notify.sh" PG_USER="bmm" CONTAINER="bmm-postgres" TEMP_DB="bmm_restore_test_$(date +%s)" TS=$(date -u +%Y-%m-%dT%H:%M:%SZ) log() { echo "[${TS}] restore-test: $*" >> "$LOG_FILE"; } fail() { log "FAIL: $*" docker exec "$CONTAINER" psql -U "$PG_USER" -d postgres -c "DROP DATABASE IF EXISTS ${TEMP_DB}" >/dev/null 2>&1 "$NOTIFY" "restore-test-failed" "$*" exit 1 } # Find newest backup LATEST=$(ls -t "${BACKUP_DIR}"/bmm-*.sql.gz 2>/dev/null | head -1) if [ -z "$LATEST" ] || [ ! -f "$LATEST" ]; then fail "no backup found in ${BACKUP_DIR}" fi log "testing restore from: $LATEST" # Create temp DB if ! docker exec "$CONTAINER" psql -U "$PG_USER" -d postgres -c "CREATE DATABASE ${TEMP_DB}" >/dev/null 2>&1; then fail "could not create temp DB ${TEMP_DB}" fi # Restore — pipe through container stdin if ! gunzip -c "$LATEST" | docker exec -i "$CONTAINER" psql -U "$PG_USER" -d "$TEMP_DB" >/dev/null 2>>"$LOG_FILE"; then fail "psql restore failed for $LATEST" fi # Schema sanity — expect the core tables to exist (adjust if schema evolves) EXPECTED_TABLES="users sessions oauth_tokens mcp_servers" for tbl in $EXPECTED_TABLES; do COUNT=$(docker exec "$CONTAINER" psql -U "$PG_USER" -d "$TEMP_DB" -tAc "SELECT count(*) FROM information_schema.tables WHERE table_name='${tbl}'" 2>>"$LOG_FILE") if [ "$COUNT" != "1" ]; then fail "restored DB missing expected table: ${tbl}" fi done # Drop temp DB docker exec "$CONTAINER" psql -U "$PG_USER" -d postgres -c "DROP DATABASE ${TEMP_DB}" >/dev/null 2>&1 log "ok — $LATEST restores cleanly, schema validates" exit 0