diff --git a/scripts/refresh-staging-db.sh b/scripts/refresh-staging-db.sh new file mode 100755 index 0000000..e4af838 --- /dev/null +++ b/scripts/refresh-staging-db.sh @@ -0,0 +1,355 @@ +#!/bin/bash +set -e + +# Staging Database Refresh Script for MotoVaultPro +# Copies production database to staging (non-interactive) +# Usage: ./scripts/refresh-staging-db.sh [options] +# +# Prerequisites: +# SSH key-based access from staging act_runner to production act_runner. +# +# On STAGING (as act_runner): +# ssh-keygen -t ed25519 -N "" -f ~/.ssh/id_ed25519 +# cat ~/.ssh/id_ed25519.pub # Copy this output +# +# On PRODUCTION (as root or sudo): +# sudo -u act_runner mkdir -p ~/.ssh +# sudo chmod 700 /home/act_runner/.ssh +# sudo -u act_runner touch ~/.ssh/authorized_keys +# sudo chmod 600 /home/act_runner/.ssh/authorized_keys +# echo "PASTE_PUBLIC_KEY_HERE" | sudo tee -a /home/act_runner/.ssh/authorized_keys +# +# Verify from STAGING: +# ssh act_runner@172.30.1.36 echo "SSH OK" + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +# Configuration +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +TIMESTAMP=$(date +%Y%m%d_%H%M%S) + +# Server configuration +PRODUCTION_HOST="172.30.1.36" +PRODUCTION_USER="act_runner" +PRODUCTION_CONTAINER="mvp-postgres" + +STAGING_CONTAINER="mvp-postgres-staging" +STAGING_BACKEND_CONTAINER="mvp-backend-staging" + +# Database configuration +DATABASE_NAME="motovaultpro" +DATABASE_USER="postgres" + +# Paths +BACKUP_DIR="/tmp/mvp-db-refresh" +STAGING_BACKUP="${BACKUP_DIR}/staging_backup_${TIMESTAMP}.sql.gz" +PRODUCTION_DUMP="${BACKUP_DIR}/production_dump_${TIMESTAMP}.sql" + +# Options +DRY_RUN=false +SKIP_BACKUP=false +KEEP_DUMP=false + +# Function to print colored output +print_info() { + echo -e "${GREEN}[INFO]${NC} $1" +} + +print_warn() { + echo -e "${YELLOW}[WARN]${NC} $1" +} + +print_error() { + echo -e "${RED}[ERROR]${NC} $1" +} + +print_step() { + echo -e "${BLUE}[STEP]${NC} $1" +} + +print_dry() { + echo -e "${YELLOW}[DRY-RUN]${NC} Would: $1" +} + +# Function to show usage +show_usage() { + cat << EOF +Staging Database Refresh Script for MotoVaultPro + +Copies the production database to staging. This script must be run on the +staging server and requires SSH access to the production server. + +Usage: $0 [options] + +Options: + -h, --help Show this help message + --dry-run Show what would happen without making changes + --skip-backup Skip staging backup (faster, less safe) + --keep-dump Keep the production dump file after import + +Prerequisites: + SSH key-based access from staging to production. + + # On STAGING (as act_runner): + ssh-keygen -t ed25519 -N "" -f ~/.ssh/id_ed25519 + cat ~/.ssh/id_ed25519.pub # Copy this output + + # On PRODUCTION (as root or sudo): + sudo -u act_runner mkdir -p ~/.ssh + sudo chmod 700 /home/act_runner/.ssh + sudo -u act_runner touch ~/.ssh/authorized_keys + sudo chmod 600 /home/act_runner/.ssh/authorized_keys + echo "PASTE_KEY" | sudo tee -a /home/act_runner/.ssh/authorized_keys + + # Verify from STAGING: + ssh ${PRODUCTION_USER}@${PRODUCTION_HOST} echo "SSH OK" + +Examples: + # Preview what would happen + $0 --dry-run + + # Full refresh (recommended) + $0 + + # Quick refresh without staging backup + $0 --skip-backup + +EOF + exit 0 +} + +# Parse command line arguments +while [[ $# -gt 0 ]]; do + case $1 in + -h|--help) + show_usage + ;; + --dry-run) + DRY_RUN=true + shift + ;; + --skip-backup) + SKIP_BACKUP=true + shift + ;; + --keep-dump) + KEEP_DUMP=true + shift + ;; + *) + print_error "Unknown option: $1" + show_usage + ;; + esac +done + +# Cleanup function +cleanup() { + local exit_code=$? + if [ "$KEEP_DUMP" = false ] && [ -f "$PRODUCTION_DUMP" ]; then + rm -f "$PRODUCTION_DUMP" + fi + if [ $exit_code -ne 0 ]; then + print_error "Script failed with exit code $exit_code" + if [ -f "$STAGING_BACKUP" ]; then + print_warn "Staging backup available at: $STAGING_BACKUP" + print_warn "To restore: gunzip -c $STAGING_BACKUP | docker exec -i $STAGING_CONTAINER psql -U $DATABASE_USER -d $DATABASE_NAME" + fi + fi + exit $exit_code +} +trap cleanup EXIT + +# Header +echo "" +echo -e "${BLUE}========================================${NC}" +echo -e "${BLUE} MotoVaultPro Staging Database Refresh${NC}" +echo -e "${BLUE}========================================${NC}" +echo "" + +if [ "$DRY_RUN" = true ]; then + print_warn "DRY RUN MODE - No changes will be made" + echo "" +fi + +# Step 1: Validate prerequisites +print_step "1/8 Validating prerequisites..." + +# Check SSH to production +print_info "Testing SSH connection to production..." +if [ "$DRY_RUN" = true ]; then + print_dry "ssh ${PRODUCTION_USER}@${PRODUCTION_HOST} echo 'OK'" +else + if ! ssh -o BatchMode=yes -o ConnectTimeout=5 "${PRODUCTION_USER}@${PRODUCTION_HOST}" "echo 'OK'" > /dev/null 2>&1; then + print_error "Cannot SSH to production server (${PRODUCTION_USER}@${PRODUCTION_HOST})" + print_error "Ensure SSH key is set up. See script header for full instructions." + print_error "Quick check: Does ~/.ssh/id_ed25519 exist? Is your public key in production's authorized_keys?" + exit 1 + fi + print_info "SSH connection OK" +fi + +# Check production container +print_info "Checking production PostgreSQL container..." +if [ "$DRY_RUN" = true ]; then + print_dry "ssh ${PRODUCTION_USER}@${PRODUCTION_HOST} docker ps --filter name=${PRODUCTION_CONTAINER}" +else + if ! ssh "${PRODUCTION_USER}@${PRODUCTION_HOST}" "docker ps --format '{{.Names}}' | grep -q '^${PRODUCTION_CONTAINER}$'"; then + print_error "Production PostgreSQL container '${PRODUCTION_CONTAINER}' is not running" + exit 1 + fi + print_info "Production container OK" +fi + +# Check staging container +print_info "Checking staging PostgreSQL container..." +if [ "$DRY_RUN" = true ]; then + print_dry "docker ps --filter name=${STAGING_CONTAINER}" +else + if ! docker ps --format '{{.Names}}' | grep -q "^${STAGING_CONTAINER}$"; then + print_error "Staging PostgreSQL container '${STAGING_CONTAINER}' is not running" + exit 1 + fi + print_info "Staging container OK" +fi + +# Create backup directory +if [ "$DRY_RUN" = false ]; then + mkdir -p "$BACKUP_DIR" +fi + +# Step 2: Backup staging database +print_step "2/8 Backing up staging database..." +if [ "$SKIP_BACKUP" = true ]; then + print_warn "Skipping staging backup (--skip-backup)" +elif [ "$DRY_RUN" = true ]; then + print_dry "docker exec ${STAGING_CONTAINER} pg_dump -U ${DATABASE_USER} -d ${DATABASE_NAME} | gzip > ${STAGING_BACKUP}" +else + docker exec "$STAGING_CONTAINER" pg_dump -U "$DATABASE_USER" -d "$DATABASE_NAME" 2>/dev/null | gzip > "$STAGING_BACKUP" + BACKUP_SIZE=$(du -h "$STAGING_BACKUP" | cut -f1) + print_info "Staging backup created: $STAGING_BACKUP ($BACKUP_SIZE)" +fi + +# Step 3: Export production database via SSH +print_step "3/8 Exporting production database..." +if [ "$DRY_RUN" = true ]; then + print_dry "ssh ${PRODUCTION_USER}@${PRODUCTION_HOST} docker exec ${PRODUCTION_CONTAINER} pg_dump -U ${DATABASE_USER} -d ${DATABASE_NAME} > ${PRODUCTION_DUMP}" +else + print_info "Streaming production database (this may take a while)..." + ssh "${PRODUCTION_USER}@${PRODUCTION_HOST}" "docker exec ${PRODUCTION_CONTAINER} pg_dump -U ${DATABASE_USER} -d ${DATABASE_NAME}" > "$PRODUCTION_DUMP" + DUMP_SIZE=$(du -h "$PRODUCTION_DUMP" | cut -f1) + print_info "Production dump received: $PRODUCTION_DUMP ($DUMP_SIZE)" +fi + +# Step 4: Stop staging backend +print_step "4/8 Stopping staging backend..." +if [ "$DRY_RUN" = true ]; then + print_dry "docker stop ${STAGING_BACKEND_CONTAINER}" +else + if docker ps --format '{{.Names}}' | grep -q "^${STAGING_BACKEND_CONTAINER}$"; then + docker stop "$STAGING_BACKEND_CONTAINER" > /dev/null + print_info "Staging backend stopped" + else + print_warn "Staging backend not running, skipping stop" + fi +fi + +# Step 5: Drop and recreate staging database +print_step "5/8 Recreating staging database..." +if [ "$DRY_RUN" = true ]; then + print_dry "docker exec ${STAGING_CONTAINER} psql -U ${DATABASE_USER} -c 'DROP DATABASE IF EXISTS ${DATABASE_NAME}'" + print_dry "docker exec ${STAGING_CONTAINER} psql -U ${DATABASE_USER} -c 'CREATE DATABASE ${DATABASE_NAME}'" +else + # Terminate existing connections + docker exec "$STAGING_CONTAINER" psql -U "$DATABASE_USER" -c "SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname = '${DATABASE_NAME}' AND pid <> pg_backend_pid();" > /dev/null 2>&1 || true + + # Drop and create + docker exec "$STAGING_CONTAINER" psql -U "$DATABASE_USER" -c "DROP DATABASE IF EXISTS ${DATABASE_NAME}" > /dev/null + docker exec "$STAGING_CONTAINER" psql -U "$DATABASE_USER" -c "CREATE DATABASE ${DATABASE_NAME}" > /dev/null + print_info "Staging database recreated" +fi + +# Step 6: Import production dump +print_step "6/8 Importing production data..." +if [ "$DRY_RUN" = true ]; then + print_dry "docker exec -i ${STAGING_CONTAINER} psql -U ${DATABASE_USER} -d ${DATABASE_NAME} < ${PRODUCTION_DUMP}" +else + print_info "Importing data (this may take a while)..." + # Import with error suppression for non-critical issues (like role doesn't exist) + docker exec -i "$STAGING_CONTAINER" psql -U "$DATABASE_USER" -d "$DATABASE_NAME" < "$PRODUCTION_DUMP" 2>&1 | grep -v "^ERROR: role" || true + print_info "Import complete" +fi + +# Step 7: Restart staging backend +print_step "7/8 Restarting staging backend..." +if [ "$DRY_RUN" = true ]; then + print_dry "docker start ${STAGING_BACKEND_CONTAINER}" +else + docker start "$STAGING_BACKEND_CONTAINER" > /dev/null 2>&1 || true + print_info "Staging backend started" + + # Wait for backend to be healthy + print_info "Waiting for backend to be ready..." + for i in {1..30}; do + if docker exec "$STAGING_BACKEND_CONTAINER" curl -sf http://localhost:3000/api/health > /dev/null 2>&1; then + print_info "Backend is healthy" + break + fi + if [ $i -eq 30 ]; then + print_warn "Backend health check timed out (may still be starting)" + fi + sleep 2 + done +fi + +# Step 8: Verify and cleanup +print_step "8/8 Verifying refresh..." +if [ "$DRY_RUN" = true ]; then + print_dry "docker exec ${STAGING_CONTAINER} psql -U ${DATABASE_USER} -d ${DATABASE_NAME} -c 'SELECT COUNT(*) FROM information_schema.tables'" +else + # Get table count + TABLE_COUNT=$(docker exec "$STAGING_CONTAINER" psql -U "$DATABASE_USER" -d "$DATABASE_NAME" -tAc "SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = 'public'") + + # Get row counts for key tables + print_info "Database statistics:" + echo "" + docker exec "$STAGING_CONTAINER" psql -U "$DATABASE_USER" -d "$DATABASE_NAME" -c " + SELECT + schemaname || '.' || relname AS table, + n_live_tup AS rows + FROM pg_stat_user_tables + ORDER BY n_live_tup DESC + LIMIT 10; + " 2>/dev/null || true + echo "" + + # Cleanup dump file + if [ "$KEEP_DUMP" = false ]; then + rm -f "$PRODUCTION_DUMP" + print_info "Production dump cleaned up" + else + print_info "Production dump kept at: $PRODUCTION_DUMP" + fi +fi + +# Summary +echo "" +echo -e "${GREEN}========================================${NC}" +echo -e "${GREEN} Staging Database Refresh Complete!${NC}" +echo -e "${GREEN}========================================${NC}" +echo "" +if [ "$DRY_RUN" = true ]; then + print_warn "This was a dry run. No changes were made." +else + print_info "Production data has been copied to staging" + print_info "Tables: $TABLE_COUNT" + if [ "$SKIP_BACKUP" = false ] && [ -f "$STAGING_BACKUP" ]; then + print_info "Previous staging backup: $STAGING_BACKUP" + fi +fi +echo ""