Files
managing-apps/scripts/rollback-database.sh

427 lines
17 KiB
Bash
Executable File

#!/bin/bash
# Database Rollback Script
# Usage: ./rollback-database.sh [environment]
# Environments: Development, SandboxRemote, ProductionRemote, Oda
set -e # Exit on any error
ENVIRONMENT=${1:-"Development"} # Default to Development for safer initial testing
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
BACKUP_DIR_NAME="backups" # Just the directory name
LOGS_DIR_NAME="logs" # Just the directory name
# Get the directory where the script is located
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )"
# Create logs directory first (before LOG_FILE is used)
LOGS_DIR="$SCRIPT_DIR/$LOGS_DIR_NAME"
mkdir -p "$LOGS_DIR" || { echo "Failed to create logs directory: $LOGS_DIR"; exit 1; }
LOG_FILE="$SCRIPT_DIR/logs/rollback_${ENVIRONMENT}_${TIMESTAMP}.log"
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# Logging function
log() {
echo -e "${GREEN}[$(date +'%Y-%m-%d %H:%M:%S')] $1${NC}" | tee -a "$LOG_FILE"
}
warn() {
echo -e "${YELLOW}[$(date +'%Y-%m-%d %H:%M:%S')] WARNING: $1${NC}" | tee -a "$LOG_FILE"
}
error() {
echo -e "${RED}[$(date +'%Y-%m-%d %H:%M:%S')] ERROR: $1${NC}" | tee -a "$LOG_FILE"
exit 1
}
info() {
echo -e "${BLUE}[$(date +'%Y-%m-%d %H:%M:%S')] INFO: $1${NC}" | tee -a "$LOG_FILE"
}
# --- Determine Base Paths ---
# Get the directory where the script is located
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )"
log "Script is located in: $SCRIPT_DIR"
# Define absolute paths for projects and common directories relative to the script
# Assuming the project structure is:
# your_repo/
# ├── scripts/rollback-database.sh
# └── src/
# ├── Managing.Api/
# └── Managing.Docker/
PROJECT_ROOT_DIR="$(dirname "$SCRIPT_DIR")" # One level up from scripts/
SRC_DIR="$PROJECT_ROOT_DIR/src"
API_PROJECT_PATH="$SRC_DIR/Managing.Api"
DOCKER_DIR="$SRC_DIR/Managing.Docker" # Adjust if your docker-compose files are elsewhere
# Define absolute path for backup directory with environment subfolder
BACKUP_DIR="$SCRIPT_DIR/$BACKUP_DIR_NAME/$ENVIRONMENT"
# --- Pre-checks and Setup ---
info "Pre-flight checks..."
command -v dotnet >/dev/null 2>&1 || error ".NET SDK is not installed. Please install .NET SDK to run this script."
command -v docker >/dev/null 2>&1 || warn "Docker is not installed. This is fine if not running Development or Oda environment with Docker."
command -v psql >/dev/null 2>&1 || error "PostgreSQL CLI (psql) is required for database rollback. Please install PostgreSQL client tools."
command -v pg_restore >/dev/null 2>&1 || warn "pg_restore not available. Will use psql for SQL script restoration."
# Create backup directory (with environment subfolder) - for storing rollback logs
mkdir -p "$BACKUP_DIR" || error "Failed to create backup directory: $BACKUP_DIR"
log "Backup directory created/verified: $BACKUP_DIR"
log "🔄 Starting database rollback for environment: $ENVIRONMENT"
# Validate environment
case $ENVIRONMENT in
"Development"|"SandboxRemote"|"ProductionRemote"|"Oda")
log "✅ Environment '$ENVIRONMENT' is valid"
;;
*)
error "❌ Invalid environment '$ENVIRONMENT'. Use: Development, SandboxRemote, ProductionRemote, or Oda"
;;
esac
# Helper function to start PostgreSQL for Development (if still using Docker Compose)
start_postgres_if_needed() {
if [ "$ENVIRONMENT" = "Development" ] || [ "$ENVIRONMENT" = "Oda" ]; then # Assuming Oda also uses local Docker
log "🔍 Checking if PostgreSQL is running for $ENVIRONMENT..."
if ! docker ps --filter "name=postgres" --format "{{.Names}}" | grep -q "postgres"; then
log "🐳 Starting PostgreSQL container for $ENVIRONMENT from $DOCKER_DIR..."
# Execute docker-compose from the DOCKER_DIR
(cd "$DOCKER_DIR" && docker-compose -f docker-compose.yml -f docker-compose.local.yml up -d postgres) || error "Failed to start PostgreSQL container."
log "⏳ Waiting for PostgreSQL to be ready (15 seconds)..."
sleep 15
else
log "✅ PostgreSQL container is already running."
fi
fi
}
# Helper function to extract connection details from appsettings
extract_connection_details() {
local appsettings_file="$API_PROJECT_PATH/appsettings.$ENVIRONMENT.json"
local default_appsettings="$API_PROJECT_PATH/appsettings.json"
# Try environment-specific file first, then default
if [ -f "$appsettings_file" ]; then
log "📋 Reading connection string from: appsettings.$ENVIRONMENT.json"
# Look for PostgreSql.ConnectionString first, then fallback to ConnectionString
CONNECTION_STRING=$(grep -A 3 '"PostgreSql"' "$appsettings_file" | grep -o '"ConnectionString": *"[^"]*"' | cut -d'"' -f4)
if [ -z "$CONNECTION_STRING" ]; then
CONNECTION_STRING=$(grep -o '"ConnectionString": *"[^"]*"' "$appsettings_file" | cut -d'"' -f4)
fi
elif [ -f "$default_appsettings" ]; then
log "📋 Reading connection string from: appsettings.json (default)"
# Look for PostgreSql.ConnectionString first, then fallback to ConnectionString
CONNECTION_STRING=$(grep -A 3 '"PostgreSql"' "$default_appsettings" | grep -o '"ConnectionString": *"[^"]*"' | cut -d'"' -f4)
if [ -z "$CONNECTION_STRING" ]; then
CONNECTION_STRING=$(grep -o '"ConnectionString": *"[^"]*"' "$default_appsettings" | cut -d'"' -f4)
fi
else
warn "⚠️ Could not find appsettings file for environment $ENVIRONMENT"
return 1
fi
if [ -z "$CONNECTION_STRING" ]; then
error "❌ Could not extract connection string from appsettings file"
return 1
fi
log "📋 Found connection string: $CONNECTION_STRING"
# Parse connection string
DB_HOST=$(echo "$CONNECTION_STRING" | grep -o 'Host=[^;]*' | cut -d'=' -f2)
DB_PORT=$(echo "$CONNECTION_STRING" | grep -o 'Port=[^;]*' | cut -d'=' -f2)
DB_NAME=$(echo "$CONNECTION_STRING" | grep -o 'Database=[^;]*' | cut -d'=' -f2)
DB_USER=$(echo "$CONNECTION_STRING" | grep -o 'Username=[^;]*' | cut -d'=' -f2)
DB_PASSWORD=$(echo "$CONNECTION_STRING" | grep -o 'Password=[^;]*' | cut -d'=' -f2)
# Set defaults if not found
DB_HOST=${DB_HOST:-"localhost"}
DB_PORT=${DB_PORT:-"5432"}
DB_NAME=${DB_NAME:-"postgres"}
DB_USER=${DB_USER:-"postgres"}
DB_PASSWORD=${DB_PASSWORD:-"postgres"}
log "📋 Extracted connection details: $DB_HOST:$DB_PORT/$DB_NAME (user: $DB_USER)"
}
# Helper function to test PostgreSQL connectivity
test_postgres_connectivity() {
log "🔍 Testing PostgreSQL connectivity with psql..."
# Test basic connectivity
if PGPASSWORD="$DB_PASSWORD" psql -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d "$DB_NAME" -c "SELECT version();" >/dev/null 2>&1; then
log "✅ PostgreSQL connectivity test passed"
# Get database info
log "📊 Database Information:"
DB_INFO=$(PGPASSWORD="$DB_PASSWORD" psql -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d "$DB_NAME" -t -c "
SELECT
'Database: ' || current_database() || ' (Size: ' || pg_size_pretty(pg_database_size(current_database())) || ')',
'PostgreSQL Version: ' || version(),
'Connection: ' || inet_server_addr() || ':' || inet_server_port()
" 2>/dev/null | tr '\n' ' ')
log " $DB_INFO"
return 0
else
error "❌ PostgreSQL connectivity test failed"
error " Host: $DB_HOST, Port: $DB_PORT, Database: $DB_NAME, User: $DB_USER"
return 1
fi
}
# --- Core Logic ---
# Set ASPNETCORE_ENVIRONMENT to load the correct appsettings
export ASPNETCORE_ENVIRONMENT="$ENVIRONMENT"
log "ASPNETCORE_ENVIRONMENT set to: $ASPNETCORE_ENVIRONMENT"
# If Development or Oda, start local PostgreSQL
start_postgres_if_needed
# Extract connection details from appsettings
extract_connection_details
# Step 1: Check Database Connection
log "🔧 Step 1: Checking database connection..."
# Test connectivity
test_postgres_connectivity
# Step 2: Find and list available backups
log "🔍 Step 2: Finding available backups..."
# Look for backup files in the environment-specific backup directory
BACKUP_FILES=$(ls -t "$BACKUP_DIR"/managing_${ENVIRONMENT}_backup_*.sql 2>/dev/null || true)
if [ -z "$BACKUP_FILES" ]; then
error "❌ No backup files found for environment '$ENVIRONMENT'"
error " Expected backup files in: $BACKUP_DIR/managing_${ENVIRONMENT}_backup_*.sql"
error " Please ensure backups exist before attempting rollback."
error " You can create a backup using: ./apply-migrations.sh $ENVIRONMENT"
fi
# Get the last 5 backups (most recent first)
RECENT_BACKUPS=$(echo "$BACKUP_FILES" | head -5)
BACKUP_COUNT=$(echo "$RECENT_BACKUPS" | wc -l | tr -d ' ')
log "✅ Found $BACKUP_COUNT backup(s) for environment '$ENVIRONMENT'"
# Display available backups
echo ""
echo "=========================================="
echo "📋 AVAILABLE BACKUPS FOR $ENVIRONMENT"
echo "=========================================="
echo "Last 5 backups (most recent first):"
echo ""
BACKUP_ARRAY=()
INDEX=1
echo "$RECENT_BACKUPS" | while read -r backup_file; do
if [ -f "$backup_file" ]; then
BACKUP_FILENAME=$(basename "$backup_file")
BACKUP_SIZE=$(ls -lh "$backup_file" | awk '{print $5}')
BACKUP_LINES=$(wc -l < "$backup_file")
BACKUP_TIMESTAMP=$(echo "$BACKUP_FILENAME" | sed "s/managing_${ENVIRONMENT}_backup_\(.*\)\.sql/\1/")
BACKUP_DATE=$(date -r "$backup_file" "+%Y-%m-%d %H:%M:%S")
echo "[$INDEX] $BACKUP_FILENAME"
echo " Date: $BACKUP_DATE"
echo " Size: $BACKUP_SIZE"
echo " Lines: $BACKUP_LINES"
echo ""
BACKUP_ARRAY+=("$backup_file")
((INDEX++))
fi
done
echo "=========================================="
echo ""
# Let user choose which backup to use
read -p "🔄 Enter the number of the backup to rollback to (1-$BACKUP_COUNT, or 'cancel' to abort): " user_choice
if [ "$user_choice" = "cancel" ]; then
log "❌ Rollback cancelled by user."
exit 0
fi
# Validate user choice
if ! [[ "$user_choice" =~ ^[0-9]+$ ]] || [ "$user_choice" -lt 1 ] || [ "$user_choice" -gt "$BACKUP_COUNT" ]; then
error "❌ Invalid choice '$user_choice'. Please enter a number between 1 and $BACKUP_COUNT, or 'cancel' to abort."
fi
# Get the selected backup file
SELECTED_BACKUP=$(echo "$RECENT_BACKUPS" | sed -n "${user_choice}p")
BACKUP_FILENAME=$(basename "$SELECTED_BACKUP")
BACKUP_TIMESTAMP=$(echo "$BACKUP_FILENAME" | sed "s/managing_${ENVIRONMENT}_backup_\(.*\)\.sql/\1/")
log "✅ Selected backup: $BACKUP_FILENAME"
log " Location: $SELECTED_BACKUP"
log " Timestamp: $BACKUP_TIMESTAMP"
# Get backup file info
if [ -f "$SELECTED_BACKUP" ]; then
BACKUP_SIZE=$(ls -lh "$SELECTED_BACKUP" | awk '{print $5}')
BACKUP_LINES=$(wc -l < "$SELECTED_BACKUP")
log "📄 Selected backup file details:"
log " Size: $BACKUP_SIZE"
log " Lines: $BACKUP_LINES"
else
error "❌ Selected backup file does not exist or is not readable: $SELECTED_BACKUP"
fi
# Step 3: Show backup preview and get user confirmation
echo ""
echo "=========================================="
echo "🔄 DATABASE ROLLBACK CONFIRMATION"
echo "=========================================="
echo "Environment: $ENVIRONMENT"
echo "Database: $DB_HOST:$DB_PORT/$DB_NAME"
echo "Selected Backup: $BACKUP_FILENAME"
echo "Backup Size: $BACKUP_SIZE"
echo "Backup Lines: $BACKUP_LINES"
echo ""
echo "⚠️ WARNING: This will DROP and RECREATE the database!"
echo " All current data will be lost and replaced with the backup."
echo " This action cannot be undone!"
echo ""
echo "📋 BACKUP PREVIEW (first 20 lines):"
echo "----------------------------------------"
head -20 "$SELECTED_BACKUP" | sed 's/^/ /'
if [ "$BACKUP_LINES" -gt 20 ]; then
echo " ... (showing first 20 lines of $BACKUP_LINES total)"
fi
echo "----------------------------------------"
echo ""
read -p "🔄 Are you sure you want to rollback to this backup? Type 'yes' to proceed: " user_confirmation
if [ "$user_confirmation" != "yes" ]; then
log "❌ Rollback cancelled by user."
exit 0
fi
log "✅ User confirmed rollback. Proceeding with database restoration..."
# Step 4: Create a final backup before rollback (safety measure)
log "📦 Step 4: Creating final backup before rollback..."
FINAL_BACKUP_FILE="$BACKUP_DIR/managing_${ENVIRONMENT}_pre_rollback_backup_${TIMESTAMP}.sql"
log "Creating final backup of current state..."
if PGPASSWORD="$DB_PASSWORD" pg_dump -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d "$DB_NAME" --no-password --verbose --clean --if-exists --create --format=plain > "$FINAL_BACKUP_FILE" 2>/dev/null; then
log "✅ Pre-rollback backup created: $(basename "$FINAL_BACKUP_FILE")"
else
warn "⚠️ Failed to create pre-rollback backup. Proceeding anyway..."
fi
# Step 5: Perform the rollback
log "🔄 Step 5: Performing database rollback..."
# Terminate active connections to the database (except our own)
log "🔌 Terminating active connections to database '$DB_NAME'..."
TERMINATE_QUERY="
SELECT pg_terminate_backend(pid)
FROM pg_stat_activity
WHERE datname = '$DB_NAME' AND pid <> pg_backend_pid();
"
if PGPASSWORD="$DB_PASSWORD" psql -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d "postgres" -c "$TERMINATE_QUERY" >/dev/null 2>&1; then
log "✅ Active connections terminated"
else
warn "⚠️ Could not terminate active connections. This may cause issues."
fi
# Drop and recreate the database
log "💥 Dropping and recreating database '$DB_NAME'..."
if PGPASSWORD="$DB_PASSWORD" psql -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d "postgres" -c "DROP DATABASE IF EXISTS \"$DB_NAME\";" >/dev/null 2>&1; then
log "✅ Database '$DB_NAME' dropped successfully"
else
error "❌ Failed to drop database '$DB_NAME'"
fi
if PGPASSWORD="$DB_PASSWORD" psql -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d "postgres" -c "CREATE DATABASE \"$DB_NAME\";" >/dev/null 2>&1; then
log "✅ Database '$DB_NAME' created successfully"
else
error "❌ Failed to create database '$DB_NAME'"
fi
# Restore from backup
log "📥 Restoring database from backup..."
if PGPASSWORD="$DB_PASSWORD" psql -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d "$DB_NAME" -f "$SELECTED_BACKUP" >/dev/null 2>&1; then
log "✅ Database successfully restored from backup"
else
ERROR_OUTPUT=$( (PGPASSWORD="$DB_PASSWORD" psql -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d "$DB_NAME" -f "$SELECTED_BACKUP") 2>&1 || true )
error "❌ Failed to restore database from backup"
error " PSQL Output: $ERROR_OUTPUT"
error " Backup file: $SELECTED_BACKUP"
error " Pre-rollback backup available at: $(basename "$FINAL_BACKUP_FILE")"
fi
# Step 6: Verify rollback
log "🔍 Step 6: Verifying rollback..."
# Test connectivity after restore
if test_postgres_connectivity; then
log "✅ Database connectivity verified after rollback"
# Get basic database stats
TABLE_COUNT=$(PGPASSWORD="$DB_PASSWORD" psql -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d "$DB_NAME" -t -c "SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = 'public';" 2>/dev/null | tr -d ' ' || echo "0")
log "📊 Post-rollback database stats:"
log " Tables: $TABLE_COUNT"
if [ "$TABLE_COUNT" -gt 0 ]; then
log " Sample tables:"
SAMPLE_TABLES=$(PGPASSWORD="$DB_PASSWORD" psql -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d "$DB_NAME" -t -c "
SELECT tablename FROM information_schema.tables
WHERE table_schema = 'public'
ORDER BY tablename
LIMIT 5;
" 2>/dev/null | sed 's/^/ /')
echo "$SAMPLE_TABLES"
fi
else
error "❌ Database connectivity test failed after rollback"
error " The rollback may have completed but the database is not accessible."
error " Pre-rollback backup available at: $(basename "$FINAL_BACKUP_FILE")"
fi
# --- Step 7: Cleanup old backups (keep only 5 rollbacks max) ---
log "🧹 Step 7: Cleaning up old rollback backups..."
# Keep only the last 5 pre-rollback backups for this environment
ls -t "$BACKUP_DIR"/managing_${ENVIRONMENT}_pre_rollback_backup_*.sql 2>/dev/null | tail -n +6 | xargs -r rm -f || true
log "✅ Kept last 5 pre-rollback backups for $ENVIRONMENT environment in $BACKUP_DIR_NAME/$ENVIRONMENT/"
# Success Summary
log "🎉 Database rollback completed successfully for environment: $ENVIRONMENT!"
log "📁 Restored from backup: $BACKUP_FILENAME"
if [ -f "$FINAL_BACKUP_FILE" ]; then
log "📁 Pre-rollback backup: $(basename "$FINAL_BACKUP_FILE")"
fi
log "📝 Full Log file: $LOG_FILE"
echo ""
echo "=========================================="
echo "📋 ROLLBACK SUMMARY"
echo "=========================================="
echo "Environment: $ENVIRONMENT"
echo "Timestamp: $TIMESTAMP"
echo "Status: ✅ SUCCESS"
echo "Restored from: $BACKUP_FILENAME"
if [ -f "$FINAL_BACKUP_FILE" ]; then
echo "Pre-rollback backup: $(basename "$FINAL_BACKUP_FILE")"
fi
echo "Database: $DB_HOST:$DB_PORT/$DB_NAME"
echo "Log: $LOG_FILE"
echo "=========================================="