Add apply migration and rollback from backup
This commit is contained in:
721
scripts/apply-migrations.sh
Executable file
721
scripts/apply-migrations.sh
Executable file
@@ -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 "=========================================="
|
||||
426
scripts/rollback-database.sh
Executable file
426
scripts/rollback-database.sh
Executable file
@@ -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 "=========================================="
|
||||
Reference in New Issue
Block a user