From 5176e4158303f2aee799d78b0653c27a6f952f6a Mon Sep 17 00:00:00 2001 From: cryptooda Date: Tue, 18 Nov 2025 23:41:16 +0700 Subject: [PATCH] Add apply migration and rollback from backup --- scripts/apply-migrations.sh | 721 +++++++++++++++++++++++++++++++++++ scripts/rollback-database.sh | 426 +++++++++++++++++++++ 2 files changed, 1147 insertions(+) create mode 100755 scripts/apply-migrations.sh create mode 100755 scripts/rollback-database.sh diff --git a/scripts/apply-migrations.sh b/scripts/apply-migrations.sh new file mode 100755 index 00000000..2176dae3 --- /dev/null +++ b/scripts/apply-migrations.sh @@ -0,0 +1,721 @@ +#!/bin/bash + +# Apply Migrations Script (No Build, No Migration Creation) +# Usage: ./apply-migrations.sh [environment] +# Environments: Development, Sandbox, Production, 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/migration_${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/apply-migrations.sh +# └── src/ +# ├── Managing.Api/ +# ├── Managing.Infrastructure.Database/ +# └── Managing.Docker/ +PROJECT_ROOT_DIR="$(dirname "$SCRIPT_DIR")" # One level up from scripts/ +SRC_DIR="$PROJECT_ROOT_DIR/src" +DB_PROJECT_PATH="$SRC_DIR/Managing.Infrastructure.Database" +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 || warn "PostgreSQL CLI (psql) is not installed. Database connectivity checks will be skipped." +command -v pg_dump >/dev/null 2>&1 || warn "PostgreSQL pg_dump is not installed. Will use EF Core migration script for backup instead." + +# Create backup directory (with environment subfolder) +mkdir -p "$BACKUP_DIR" || error "Failed to create backup directory: $BACKUP_DIR" +log "Backup directory created/verified: $BACKUP_DIR" + +log "🚀 Starting migration application 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, password: $DB_PASSWORD)" +} + +# Helper function to get the first migration name +get_first_migration() { + local first_migration=$(cd "$DB_PROJECT_PATH" && dotnet ef migrations list --no-build --startup-project "$API_PROJECT_PATH" | head -1 | awk '{print $1}') + echo "$first_migration" +} + +# Helper function to test PostgreSQL connectivity +test_postgres_connectivity() { + if ! command -v psql >/dev/null 2>&1; then + warn "⚠️ psql not available, skipping PostgreSQL connectivity test" + return 0 + fi + + log "🔍 Testing PostgreSQL connectivity with psql..." + + # For remote servers or when target database might not exist, test with postgres database first + local test_database="$DB_NAME" + if [ "$TARGET_DB_EXISTS" = "false" ]; then + test_database="postgres" + log "🔍 Target database doesn't exist, testing connectivity with 'postgres' database..." + fi + + # Test basic connectivity + if PGPASSWORD="$DB_PASSWORD" psql -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d "$test_database" -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 "$test_database" -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" + + # Only check migrations if we're testing the actual target database + if [ "$test_database" = "$DB_NAME" ]; then + # Check if __EFMigrationsHistory table exists + if PGPASSWORD="$DB_PASSWORD" psql -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d "$DB_NAME" -c "\dt __EFMigrationsHistory" >/dev/null 2>&1; then + log "✅ EF Core migrations history table exists" + + # Count applied migrations + MIGRATION_COUNT=$(PGPASSWORD="$DB_PASSWORD" psql -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d "$DB_NAME" -t -c "SELECT COUNT(*) FROM \"__EFMigrationsHistory\";" 2>/dev/null | tr -d ' ') + log "📋 Applied migrations count: $MIGRATION_COUNT" + + # Show recent migrations + if [ "$MIGRATION_COUNT" -gt 0 ]; then + log "📋 Recent migrations:" + RECENT_MIGRATIONS=$(PGPASSWORD="$DB_PASSWORD" psql -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d "$DB_NAME" -t -c " + SELECT \"MigrationId\" FROM \"__EFMigrationsHistory\" + ORDER BY \"MigrationId\" DESC + LIMIT 5; + " 2>/dev/null | sed 's/^/ /') + echo "$RECENT_MIGRATIONS" + fi + else + warn "⚠️ EF Core migrations history table not found - database may be empty" + fi + else + log "📋 Connectivity test completed using 'postgres' database (target database will be created)" + fi + + return 0 + else + error "❌ PostgreSQL connectivity test failed" + error " Host: $DB_HOST, Port: $DB_PORT, Database: $test_database, User: $DB_USER" + return 1 + fi +} + +# --- Core Logic --- +# No global 'cd' needed here. All paths are now absolute. +# This makes the script much more robust to where it's executed from. + +# 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 and Create if Needed +log "🔧 Step 1: Checking database connection and creating database if needed..." + +# Log the environment and expected connection details (for user info, still relies on appsettings) +log "🔧 Using environment: $ENVIRONMENT" +log "📋 Connection details: $DB_HOST:$DB_PORT/$DB_NAME (user: $DB_USER)" + +# Initial connectivity check - test if we can reach the database server +log "🔍 Step 1a: Testing basic database server connectivity..." +if command -v psql >/dev/null 2>&1; then + # Test if we can connect to the postgres database (which should always exist) + log "🔍 Connecting to default 'postgres' database to verify server connectivity..." + if PGPASSWORD="$DB_PASSWORD" psql -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d postgres -c "SELECT 1;" >/dev/null 2>&1; then + log "✅ Database server connectivity test passed" + + # Check if our target database exists + log "🔍 Checking if target database '$DB_NAME' exists..." + DB_EXISTS=$(PGPASSWORD="$DB_PASSWORD" psql -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d postgres -t -c "SELECT 1 FROM pg_database WHERE datname = '$DB_NAME';" 2>/dev/null | tr -d ' ') + + if [ "$DB_EXISTS" = "1" ]; then + log "✅ Target database '$DB_NAME' exists" + TARGET_DB_EXISTS=true + else + log "⚠️ Target database '$DB_NAME' does not exist - will be created" + TARGET_DB_EXISTS=false + fi + + else + error "❌ Database server connectivity test failed" + error " Cannot reach PostgreSQL server at $DB_HOST:$DB_PORT with database 'postgres'" + error " Please verify:" + error " - Database server is running" + error " - Network connectivity to $DB_HOST:$DB_PORT" + error " - Credentials are correct (user: $DB_USER)" + error " - Firewall settings allow connections" + error " - The 'postgres' database exists (default PostgreSQL database)" + fi +else + # Fallback: try to connect using EF Core to test basic connectivity + log "🔄 psql not available, testing connectivity via EF Core..." + if (cd "$DB_PROJECT_PATH" && dotnet ef migrations list --no-build --startup-project "$API_PROJECT_PATH" --connection "$CONNECTION_STRING") >/dev/null 2>&1; then + log "✅ Database server connectivity test passed (via EF Core)" + TARGET_DB_EXISTS=true # Assume it exists if EF Core can connect + else + warn "⚠️ Could not verify database server connectivity (psql not available)" + warn " Proceeding with caution - connectivity will be tested during migration" + TARGET_DB_EXISTS=false # Assume it doesn't exist if EF Core can't connect + fi +fi + +log "🔍 Step 1b: Testing database connection and checking if database exists via EF CLI..." + +# Test connection by listing migrations. If it fails, the database likely doesn't exist or is inaccessible. +# Execute dotnet ef from DB_PROJECT_PATH for correct context, but pass API_PROJECT_PATH as startup. +# Since we assume projects are already built, we can safely use --no-build flag for faster execution +if (cd "$DB_PROJECT_PATH" && dotnet ef migrations list --no-build --startup-project "$API_PROJECT_PATH" --connection "$CONNECTION_STRING") >/dev/null 2>&1; then + log "✅ EF Core database connection successful and database appears to exist." + + # Now test with psql for additional verification (this will use postgres db if target doesn't exist) + test_postgres_connectivity + + # If psql connectivity test fails, stop the migration + if [ $? -ne 0 ]; then + error "❌ PostgreSQL connectivity test failed. Migration aborted for safety." + error " Please verify your database connection and try again." + fi + +else + # Database doesn't exist or connection failed + if [ "$TARGET_DB_EXISTS" = "false" ]; then + log "📝 Database '$DB_NAME' does not exist. Creating database and applying migrations..." + + # Test connectivity with postgres database first (since target doesn't exist) + test_postgres_connectivity + + # If connectivity test fails, stop the migration + if [ $? -ne 0 ]; then + error "❌ PostgreSQL connectivity test failed. Cannot proceed with database creation." + error " Please verify your connection settings and try again." + fi + + # Step 1: Create the database first + log "🔧 Step 1: Creating database '$DB_NAME'..." + if command -v psql >/dev/null 2>&1; then + 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'" + error " Please verify you have sufficient privileges to create databases." + fi + else + warn "⚠️ psql not available, attempting to create database via EF Core..." + # EF Core will attempt to create the database during update + fi + + # Step 2: Generate migration script for the new database + log "📝 Step 2: Generating migration script for new database..." + TEMP_MIGRATION_SCRIPT="$BACKUP_DIR/temp_migration_${ENVIRONMENT}_${TIMESTAMP}.sql" + + if (cd "$DB_PROJECT_PATH" && ASPNETCORE_ENVIRONMENT="$ENVIRONMENT" dotnet ef migrations script --idempotent --no-build --startup-project "$API_PROJECT_PATH" --output "$TEMP_MIGRATION_SCRIPT"); then + log "✅ Migration script generated successfully: $(basename "$TEMP_MIGRATION_SCRIPT")" + + # Step 3: Apply the migration script to the new database + log "🔧 Step 3: Applying migration script to new database..." + if command -v psql >/dev/null 2>&1; then + if PGPASSWORD="$DB_PASSWORD" psql -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d "$DB_NAME" -f "$TEMP_MIGRATION_SCRIPT" >/dev/null 2>&1; then + log "✅ Migration script applied successfully to new database" + else + error "❌ Failed to apply migration script to newly created database" + fi + else + # Fallback to EF Core database update + log "🔄 psql not available, using EF Core to apply migrations..." + if (cd "$DB_PROJECT_PATH" && dotnet ef database update --no-build --startup-project "$API_PROJECT_PATH" --connection "$CONNECTION_STRING"); then + log "✅ Database created and initialized successfully using EF Core" + else + ERROR_OUTPUT=$( (cd "$DB_PROJECT_PATH" && dotnet ef database update --no-build --startup-project "$API_PROJECT_PATH" --connection "$CONNECTION_STRING") 2>&1 || true ) + error "❌ Failed to create and initialize database using EF Core." + error " EF CLI Output: $ERROR_OUTPUT" + fi + fi + + # Clean up temporary migration script + rm -f "$TEMP_MIGRATION_SCRIPT" + + else + ERROR_OUTPUT=$( (cd "$DB_PROJECT_PATH" && ASPNETCORE_ENVIRONMENT="$ENVIRONMENT" dotnet ef migrations script --idempotent --no-build --startup-project "$API_PROJECT_PATH" --output "$TEMP_MIGRATION_SCRIPT") 2>&1 || true ) + error "❌ Failed to generate migration script." + error " EF CLI Output: $ERROR_OUTPUT" + fi + + else + warn "⚠️ Database connection failed but database may exist. Attempting to update existing database..." + + # Try to update the existing database + if (cd "$DB_PROJECT_PATH" && dotnet ef database update --no-build --startup-project "$API_PROJECT_PATH" --connection "$CONNECTION_STRING"); then + log "✅ Database updated successfully" + else + ERROR_OUTPUT=$( (cd "$DB_PROJECT_PATH" && dotnet ef database update --no-build --startup-project "$API_PROJECT_PATH" --connection "$CONNECTION_STRING") 2>&1 || true ) + error "❌ Failed to update database." + error " EF CLI Output: $ERROR_OUTPUT" + error " This usually means the connection string in your .NET project's appsettings.$ENVIRONMENT.json is incorrect," + error " or the database server is not running/accessible for environment '$ENVIRONMENT'." + fi + fi + + # Test connectivity after creation/update + test_postgres_connectivity + + # If connectivity test fails after creation, stop the migration + if [ $? -ne 0 ]; then + error "❌ PostgreSQL connectivity test failed after database creation. Migration aborted for safety." + error " Database may have been created but is not accessible. Please verify your connection settings." + fi +fi + +# Final verification of connection +log "🔍 Verifying database connection post-creation/update..." +if (cd "$DB_PROJECT_PATH" && dotnet ef migrations list --no-build --startup-project "$API_PROJECT_PATH" --connection "$CONNECTION_STRING") >/dev/null 2>&1; then + log "✅ Database connectivity verification passed." +else + ERROR_OUTPUT=$( (cd "$DB_PROJECT_PATH" && dotnet ef migrations list --no-build --startup-project "$API_PROJECT_PATH" --connection "$CONNECTION_STRING") 2>&1 || true ) + error "❌ Final database connectivity verification failed." + error " EF CLI Output: $ERROR_OUTPUT" + error " This is critical. Please review the previous error messages and your connection string for '$ENVIRONMENT'." +fi + +# Step 2: Create database backup (only if database exists) +log "📦 Step 2: Checking if database backup is needed..." + +# Check if the target database exists +DB_EXISTS=false +if PGPASSWORD="$DB_PASSWORD" psql -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d "postgres" -c "SELECT 1 FROM pg_database WHERE datname='$DB_NAME';" 2>/dev/null | grep -q "1 row"; then + DB_EXISTS=true + log "✅ Target database '$DB_NAME' exists" +else + log "ℹ️ Target database '$DB_NAME' does not exist - skipping backup" +fi + +# Ask user if they want to create a backup +CREATE_BACKUP=false +if [ "$DB_EXISTS" = "true" ]; then + echo "" + echo "==========================================" + echo "📦 DATABASE BACKUP" + echo "==========================================" + echo "Database: $DB_HOST:$DB_PORT/$DB_NAME" + echo "Environment: $ENVIRONMENT" + echo "" + echo "Would you like to create a backup before proceeding?" + echo "⚠️ It is highly recommended to create a backup for safety." + echo "==========================================" + echo "" + + read -p "🔧 Create database backup? (y/n, default: y): " create_backup + create_backup=${create_backup:-y} # Default to 'y' if user just presses Enter + + if [[ "$create_backup" =~ ^[Yy]$ ]]; then + log "✅ User chose to create backup - proceeding with backup" + CREATE_BACKUP=true + else + warn "⚠️ User chose to skip backup - proceeding without backup" + warn " This is not recommended. Proceed at your own risk!" + CREATE_BACKUP=false + fi +fi + +if [ "$DB_EXISTS" = "true" ] && [ "$CREATE_BACKUP" = "true" ]; then + # Define the actual backup file path (absolute) + BACKUP_FILE="$BACKUP_DIR/managing_${ENVIRONMENT}_backup_${TIMESTAMP}.sql" + # Backup file display path (relative to script execution) + BACKUP_FILE_DISPLAY="$BACKUP_DIR_NAME/$ENVIRONMENT/managing_${ENVIRONMENT}_backup_${TIMESTAMP}.sql" + +# Create backup with retry logic +BACKUP_SUCCESS=false +for attempt in 1 2 3; do + log "Backup attempt $attempt/3..." + + # Create real database backup using pg_dump + if command -v pg_dump >/dev/null 2>&1; then + 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 > "$BACKUP_FILE" 2>/dev/null; then + log "✅ Database backup created using pg_dump: $BACKUP_FILE_DISPLAY" + BACKUP_SUCCESS=true + break + else + # If pg_dump fails, fall back to EF Core migration script + warn "⚠️ pg_dump failed, falling back to EF Core migration script..." + + # Generate complete backup script (all migrations from beginning) + log "📋 Generating complete backup script (all migrations)..." + if (cd "$DB_PROJECT_PATH" && ASPNETCORE_ENVIRONMENT="$ENVIRONMENT" dotnet ef migrations script --idempotent --no-build --startup-project "$API_PROJECT_PATH" --output "$BACKUP_FILE"); then + log "✅ Complete EF Core Migration SQL Script generated: $BACKUP_FILE_DISPLAY" + BACKUP_SUCCESS=true + break + else + ERROR_OUTPUT=$( (cd "$DB_PROJECT_PATH" && ASPNETCORE_ENVIRONMENT="$ENVIRONMENT" dotnet ef migrations script --idempotent --no-build --startup-project "$API_PROJECT_PATH" --output "$BACKUP_FILE") 2>&1 || true) + if [ $attempt -lt 3 ]; then + warn "⚠️ Backup attempt $attempt failed. Retrying in 5 seconds..." + warn " EF CLI Output: $ERROR_OUTPUT" + sleep 5 + else + error "❌ Database backup failed after 3 attempts." + error " EF CLI Output: $ERROR_OUTPUT" + error " Migration aborted for safety reasons." + fi + fi + fi + else + # If pg_dump is not available, use EF Core migration script + warn "⚠️ pg_dump not available, using EF Core migration script for backup..." + + # Generate complete backup script (all migrations from beginning) + log "📋 Generating complete backup script (all migrations)..." + if (cd "$DB_PROJECT_PATH" && ASPNETCORE_ENVIRONMENT="$ENVIRONMENT" dotnet ef migrations script --idempotent --no-build --startup-project "$API_PROJECT_PATH" --output "$BACKUP_FILE"); then + log "✅ Complete EF Core Migration SQL Script generated: $BACKUP_FILE_DISPLAY" + BACKUP_SUCCESS=true + break + else + ERROR_OUTPUT=$( (cd "$DB_PROJECT_PATH" && ASPNETCORE_ENVIRONMENT="$ENVIRONMENT" dotnet ef migrations script --idempotent --no-build --startup-project "$API_PROJECT_PATH" --output "$BACKUP_FILE") 2>&1 || true) + if [ $attempt -lt 3 ]; then + warn "⚠️ Backup attempt $attempt failed. Retrying in 5 seconds..." + warn " EF CLI Output: $ERROR_OUTPUT" + sleep 5 + else + error "❌ Database backup failed after 3 attempts." + error " EF CLI Output: $ERROR_OUTPUT" + error " Migration aborted for safety reasons." + fi + fi + fi +done + + # Check if backup was successful before proceeding + if [ "$BACKUP_SUCCESS" != "true" ]; then + error "❌ Database backup failed. Migration aborted for safety." + error " Cannot proceed with migration without a valid backup." + error " Please resolve backup issues and try again." + fi +fi + +# Step 3: Run Migration (This effectively is a retry if previous "update" failed, or a final apply) +log "🔄 Step 3: Running database migration (final application of pending migrations)..." + +# Check if database exists and create it if needed before applying migrations +log "🔍 Step 3a: Ensuring target database exists..." +if [ "$TARGET_DB_EXISTS" = "false" ]; then + log "🔧 Creating database '$DB_NAME'..." + if command -v psql >/dev/null 2>&1; then + 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'" + error " Please verify you have sufficient privileges to create databases." + fi + else + error "❌ psql not available, cannot create database. Please create database '$DB_NAME' manually." + fi +fi + +# Generate migration script first (Microsoft recommended approach) +MIGRATION_SCRIPT="$BACKUP_DIR/migration_${ENVIRONMENT}_${TIMESTAMP}.sql" +log "📝 Step 3b: Generating migration script for pending migrations..." + +# Check if database is empty (no tables) to determine the best approach +log "🔍 Checking if database has existing tables..." +DB_HAS_TABLES=false +if command -v psql >/dev/null 2>&1; then + 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") + if [ "$TABLE_COUNT" -gt 0 ]; then + DB_HAS_TABLES=true + log "✅ Database has $TABLE_COUNT existing tables - using idempotent script generation" + else + log "⚠️ Database appears to be empty - using full migration script generation" + fi +else + log "⚠️ psql not available - assuming database has tables and using idempotent script generation" + DB_HAS_TABLES=true +fi + +# Generate migration script based on database state +if [ "$DB_HAS_TABLES" = "true" ]; then + # For databases with existing tables, generate a complete idempotent script + log "📝 Generating complete migration script (idempotent) for database with existing tables..." + if (cd "$DB_PROJECT_PATH" && ASPNETCORE_ENVIRONMENT="$ENVIRONMENT" dotnet ef migrations script --idempotent --no-build --startup-project "$API_PROJECT_PATH" --output "$MIGRATION_SCRIPT"); then + log "✅ Complete migration script generated (all migrations, idempotent): $(basename "$MIGRATION_SCRIPT")" + else + ERROR_OUTPUT=$( (cd "$DB_PROJECT_PATH" && ASPNETCORE_ENVIRONMENT="$ENVIRONMENT" dotnet ef migrations script --idempotent --no-build --startup-project "$API_PROJECT_PATH" --output "$MIGRATION_SCRIPT") 2>&1 || true ) + error "❌ Failed to generate complete migration script." + error " EF CLI Output: $ERROR_OUTPUT" + error " Check the .NET project logs for detailed errors." + if [ "$CREATE_BACKUP" = "true" ] && [ -n "$BACKUP_FILE_DISPLAY" ]; then + error " Backup script available at: $BACKUP_FILE_DISPLAY" + fi + fi +else + # Use full script generation for empty databases (generate script from the very beginning) + log "📝 Generating full migration script for empty database..." + if (cd "$DB_PROJECT_PATH" && ASPNETCORE_ENVIRONMENT="$ENVIRONMENT" dotnet ef migrations script --no-build --startup-project "$API_PROJECT_PATH" --output "$MIGRATION_SCRIPT"); then + log "✅ Complete migration script generated (all migrations): $(basename "$MIGRATION_SCRIPT")" + else + ERROR_OUTPUT=$( (cd "$DB_PROJECT_PATH" && ASPNETCORE_ENVIRONMENT="$ENVIRONMENT" dotnet ef migrations script --no-build --startup-project "$API_PROJECT_PATH" --output "$MIGRATION_SCRIPT") 2>&1 || true ) + error "❌ Failed to generate complete migration script." + error " EF CLI Output: $ERROR_OUTPUT" + error " Check the .NET project logs for detailed errors." + if [ "$CREATE_BACKUP" = "true" ] && [ -n "$BACKUP_FILE_DISPLAY" ]; then + error " Backup script available at: $BACKUP_FILE_DISPLAY" + fi + fi +fi + + # Show the migration script path to the user for review + echo "" + echo "==========================================" + echo "📋 MIGRATION SCRIPT READY FOR REVIEW" + echo "==========================================" + echo "Generated script: $MIGRATION_SCRIPT" + echo "Environment: $ENVIRONMENT" + echo "Database: $DB_HOST:$DB_PORT/$DB_NAME" + echo "" + + # Show a preview of the migration script content + if [ -f "$MIGRATION_SCRIPT" ]; then + SCRIPT_SIZE=$(wc -l < "$MIGRATION_SCRIPT") + echo "📄 Migration script contains $SCRIPT_SIZE lines" + + # Show last 20 lines as preview + echo "" + echo "📋 PREVIEW (last 20 lines):" + echo "----------------------------------------" + tail -20 "$MIGRATION_SCRIPT" | sed 's/^/ /' + if [ "$SCRIPT_SIZE" -gt 20 ]; then + echo " ... (showing last 20 lines of $SCRIPT_SIZE total)" + fi + echo "----------------------------------------" + echo "" + fi + + echo "⚠️ IMPORTANT: Please review the migration script before proceeding!" + echo " You can examine the full script with: cat $MIGRATION_SCRIPT" + echo " Or open it in your editor to review the changes." + echo "" + echo "==========================================" + echo "" + + # Ask for user confirmation + read -p "🔍 Have you reviewed the migration script and want to proceed? Type 'yes' to continue: " user_confirmation + + if [ "$user_confirmation" != "yes" ]; then + log "❌ Migration cancelled by user." + log " Migration script is available at: $(basename "$MIGRATION_SCRIPT")" + log " You can apply it manually later with:" + log " PGPASSWORD=\"$DB_PASSWORD\" psql -h \"$DB_HOST\" -p \"$DB_PORT\" -U \"$DB_USER\" -d \"$DB_NAME\" -f \"$MIGRATION_SCRIPT\"" + exit 0 + fi + + log "✅ User confirmed migration. Proceeding with database update..." + + # Apply the migration script using psql (recommended approach) + log "🔧 Step 3c: Applying migration script to database..." + if command -v psql >/dev/null 2>&1; then + if PGPASSWORD="$DB_PASSWORD" psql -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d "$DB_NAME" -f "$MIGRATION_SCRIPT" >/dev/null 2>&1; then + log "✅ Migration script applied successfully to database" + else + ERROR_OUTPUT=$( (PGPASSWORD="$DB_PASSWORD" psql -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d "$DB_NAME" -f "$MIGRATION_SCRIPT") 2>&1 || true ) + error "❌ Failed to apply migration script to database" + error " PSQL Output: $ERROR_OUTPUT" + error " Migration script available at: $(basename "$MIGRATION_SCRIPT")" + fi + else + # Fallback to EF Core database update if psql is not available + log "🔄 psql not available, falling back to EF Core database update..." + if (cd "$DB_PROJECT_PATH" && dotnet ef database update --no-build --startup-project "$API_PROJECT_PATH" --connection "$CONNECTION_STRING"); then + log "✅ Database migration completed successfully using EF Core." + else + ERROR_OUTPUT=$( (cd "$DB_PROJECT_PATH" && dotnet ef database update --no-build --startup-project "$API_PROJECT_PATH" --connection "$CONNECTION_STRING") 2>&1 || true ) + error "❌ Database migration failed during final update." + error " EF CLI Output: $ERROR_OUTPUT" + error " Check the .NET project logs for detailed errors." + if [ "$CREATE_BACKUP" = "true" ] && [ -n "$BACKUP_FILE_DISPLAY" ]; then + error " Backup script available at: $BACKUP_FILE_DISPLAY" + fi + fi + fi + + # Save a copy of the migration script for reference before cleaning up + MIGRATION_SCRIPT_COPY="$BACKUP_DIR/migration_${ENVIRONMENT}_${TIMESTAMP}_applied.sql" + if [ -f "$MIGRATION_SCRIPT" ]; then + cp "$MIGRATION_SCRIPT" "$MIGRATION_SCRIPT_COPY" + log "📝 Migration script saved for reference: $(basename "$MIGRATION_SCRIPT_COPY")" + fi + + # Clean up temporary migration script after successful application + rm -f "$MIGRATION_SCRIPT" + +# Step 4: Verify Migration +log "🔍 Step 4: Verifying migration status..." + +# List migrations to check applied status +MIGRATION_LIST_OUTPUT=$( (cd "$DB_PROJECT_PATH" && dotnet ef migrations list --no-build --startup-project "$API_PROJECT_PATH" --connection "$CONNECTION_STRING") 2>&1 ) +log "📋 Current migration status:\n$MIGRATION_LIST_OUTPUT" + +# Check if there are any pending migrations after update +PENDING_MIGRATIONS=$(echo "$MIGRATION_LIST_OUTPUT" | grep -c "\[ \]" || echo "0") +PENDING_MIGRATIONS=$(echo "$PENDING_MIGRATIONS" | tr -d '\n') # Remove any newlines +if [ "$PENDING_MIGRATIONS" -gt 0 ]; then + warn "⚠️ WARNING: $PENDING_MIGRATIONS pending migration(s) found after update." + warn " This indicates the 'dotnet ef database update' command may not have fully completed." +else + log "✅ All migrations appear to be applied successfully." +fi + +# --- Step 5: Cleanup Backups (keep only 5 dumps max) --- +log "🧹 Step 5: Cleaning up old backups..." + +# Keep only the last 5 backups for this environment (in the environment-specific subfolder) +ls -t "$BACKUP_DIR"/managing_${ENVIRONMENT}_backup_*.sql 2>/dev/null | tail -n +6 | xargs -r rm -f || true # Added -f for force removal + +log "✅ Kept last 5 backups for $ENVIRONMENT environment in $BACKUP_DIR_NAME/$ENVIRONMENT/" + +log "🎉 Migration application completed successfully for environment: $ENVIRONMENT!" +if [ "$CREATE_BACKUP" = "true" ] && [ -n "$BACKUP_FILE_DISPLAY" ]; then + log "📁 EF Core Migration SQL Script: $BACKUP_FILE_DISPLAY" +fi +log "📝 Full Log file: $LOG_FILE" + +echo "" +echo "==========================================" +echo "📋 MIGRATION SUMMARY" +echo "==========================================" +echo "Environment: $ENVIRONMENT" +echo "Timestamp: $TIMESTAMP" +echo "Status: ✅ SUCCESS" +if [ "$CREATE_BACKUP" = "true" ] && [ -n "$BACKUP_FILE_DISPLAY" ]; then + echo "EF Core SQL Backup: $BACKUP_FILE_DISPLAY" +else + echo "Database Backup: Skipped by user" +fi +echo "Log: $LOG_FILE" +echo "==========================================" diff --git a/scripts/rollback-database.sh b/scripts/rollback-database.sh new file mode 100755 index 00000000..44d2bc03 --- /dev/null +++ b/scripts/rollback-database.sh @@ -0,0 +1,426 @@ +#!/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 "=========================================="