#!/bin/bash # # Production Rollback Script # Rolls back to a previous container image tag # # Usage: ./rollback-production.sh [OPTIONS] # # Options: # --domain DOMAIN Override domain name (default: michaelschiemer.de) # --vault-password-file FILE Specify vault password file # --force Force rollback without confirmation # --help Show this help message set -euo pipefail # Script directory SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" INFRA_DIR="${SCRIPT_DIR}/infrastructure" # Default values DEFAULT_DOMAIN="michaelschiemer.de" ENVIRONMENT="production" # Initialize variables ROLLBACK_TAG="" DOMAIN_NAME="$DEFAULT_DOMAIN" VAULT_PASSWORD_FILE="" FORCE="false" EXTRA_VARS="" # 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 functions log_info() { echo -e "${BLUE}[INFO]${NC} $1" >&2 } log_success() { echo -e "${GREEN}[SUCCESS]${NC} $1" >&2 } log_warning() { echo -e "${YELLOW}[WARNING]${NC} $1" >&2 } log_error() { echo -e "${RED}[ERROR]${NC} $1" >&2 } # Help function show_help() { cat << EOF Production Rollback Script for Custom PHP Framework USAGE: $0 [OPTIONS] ARGUMENTS: ROLLBACK_TAG Container image tag to rollback to (required) Must NOT be 'latest' for production rollbacks OPTIONS: --domain DOMAIN Override domain name (default: $DEFAULT_DOMAIN) --vault-password-file FILE Specify vault password file path --force Force rollback without confirmation prompt --help Show this help message EXAMPLES: # Rollback to version 1.2.2 $0 1.2.2 # Rollback with custom domain $0 1.2.2 --domain staging.michaelschiemer.de # Force rollback without confirmation $0 1.2.2 --force ENVIRONMENT VARIABLES: ANSIBLE_VAULT_PASSWORD_FILE Vault password file (overrides --vault-password-file) ROLLBACK_TAG Image tag to rollback to (overrides first argument) DOMAIN_NAME Domain name (overrides --domain) REQUIREMENTS: - Ansible 2.9+ - community.docker collection - SSH access to production server - Vault password file or ANSIBLE_VAULT_PASSWORD_FILE environment variable WARNING: This will immediately rollback your production application. Make sure the target image tag exists and is functional. EOF } # Parse command line arguments parse_args() { if [[ $# -eq 0 ]]; then log_error "No arguments provided" show_help exit 1 fi while [[ $# -gt 0 ]]; do case $1 in --help|-h) show_help exit 0 ;; --domain) if [[ -z "${2:-}" ]] || [[ "$2" =~ ^-- ]]; then log_error "--domain requires a domain name" exit 1 fi DOMAIN_NAME="$2" shift 2 ;; --vault-password-file) if [[ -z "${2:-}" ]] || [[ "$2" =~ ^-- ]]; then log_error "--vault-password-file requires a file path" exit 1 fi VAULT_PASSWORD_FILE="$2" shift 2 ;; --force) FORCE="true" shift ;; -*) log_error "Unknown option: $1" show_help exit 1 ;; *) if [[ -z "$ROLLBACK_TAG" ]]; then ROLLBACK_TAG="$1" else log_error "Multiple positional arguments provided. Only ROLLBACK_TAG is expected." show_help exit 1 fi shift ;; esac done } # Validate environment and requirements validate_environment() { log_info "Validating rollback environment..." # Check for required ROLLBACK_TAG if [[ -z "$ROLLBACK_TAG" ]]; then if [[ -n "${ROLLBACK_TAG:-}" ]]; then ROLLBACK_TAG="$ROLLBACK_TAG" else log_error "ROLLBACK_TAG is required" show_help exit 1 fi fi # Validate rollback tag for production if [[ "$ROLLBACK_TAG" == "latest" ]]; then log_error "Production rollbacks cannot use 'latest' tag" exit 1 fi # Override with environment variables if set DOMAIN_NAME="${DOMAIN_NAME:-$DEFAULT_DOMAIN}" # Check if ansible is available if ! command -v ansible-playbook &> /dev/null; then log_error "ansible-playbook not found. Please install Ansible." exit 1 fi # Check vault password file if [[ -n "${ANSIBLE_VAULT_PASSWORD_FILE:-}" ]]; then VAULT_PASSWORD_FILE="$ANSIBLE_VAULT_PASSWORD_FILE" fi if [[ -z "$VAULT_PASSWORD_FILE" ]]; then log_warning "No vault password file specified. Ansible will prompt for vault password." elif [[ ! -f "$VAULT_PASSWORD_FILE" ]]; then log_error "Vault password file not found: $VAULT_PASSWORD_FILE" exit 1 fi # Check infrastructure directory if [[ ! -d "$INFRA_DIR" ]]; then log_error "Infrastructure directory not found: $INFRA_DIR" exit 1 fi # Check inventory file local inventory_file="${INFRA_DIR}/inventories/production/hosts.yml" if [[ ! -f "$inventory_file" ]]; then log_error "Production inventory not found: $inventory_file" exit 1 fi # Check rollback playbook file local playbook_file="${INFRA_DIR}/playbooks/rollback.yml" if [[ ! -f "$playbook_file" ]]; then log_error "Rollback playbook not found: $playbook_file" exit 1 fi log_success "Environment validation complete" } # Get current deployment info get_current_deployment() { log_info "Checking current deployment..." # Try to get current deployment info via ansible local current_tag_cmd="ansible production -i ${INFRA_DIR}/inventories/production/hosts.yml -m shell -a 'cat /var/www/html/.last_successful_release 2>/dev/null || echo unknown'" if [[ -n "$VAULT_PASSWORD_FILE" ]]; then current_tag_cmd+=" --vault-password-file $VAULT_PASSWORD_FILE" fi local current_tag current_tag=$(eval "$current_tag_cmd" 2>/dev/null | grep -v "SUCCESS" | tail -1 || echo "unknown") if [[ "$current_tag" != "unknown" ]]; then log_info "Current deployment: $current_tag" if [[ "$current_tag" == "$ROLLBACK_TAG" ]]; then log_warning "Already running version $ROLLBACK_TAG" if [[ "$FORCE" == "false" ]]; then read -p "Do you want to force redeploy the same version? (y/N): " -n 1 -r echo if [[ ! $REPLY =~ ^[Yy]$ ]]; then log_info "Rollback cancelled" exit 0 fi fi fi else log_warning "Could not determine current deployment version" fi } # Confirm rollback action confirm_rollback() { if [[ "$FORCE" == "true" ]]; then log_warning "Force mode enabled, skipping confirmation" return fi log_warning "You are about to rollback production to: $ROLLBACK_TAG" log_warning "Domain: $DOMAIN_NAME" log_warning "This will immediately affect live users!" echo read -p "Are you sure you want to continue? (y/N): " -n 1 -r echo if [[ ! $REPLY =~ ^[Yy]$ ]]; then log_info "Rollback cancelled" exit 0 fi } # Build extra variables for ansible build_extra_vars() { EXTRA_VARS="-e ROLLBACK_TAG=$ROLLBACK_TAG" EXTRA_VARS+=" -e DOMAIN_NAME=$DOMAIN_NAME" EXTRA_VARS+=" -e ENV=$ENVIRONMENT" log_info "Rollback configuration:" log_info " Rollback Tag: $ROLLBACK_TAG" log_info " Domain: $DOMAIN_NAME" log_info " Environment: $ENVIRONMENT" } # Execute rollback run_rollback() { log_info "Starting production rollback..." local ansible_cmd="ansible-playbook" local inventory="${INFRA_DIR}/inventories/production/hosts.yml" local playbook="${INFRA_DIR}/playbooks/rollback.yml" # Build ansible command local cmd="$ansible_cmd -i $inventory $playbook $EXTRA_VARS" # Add vault password file if specified if [[ -n "$VAULT_PASSWORD_FILE" ]]; then cmd+=" --vault-password-file $VAULT_PASSWORD_FILE" fi # Change to infrastructure directory cd "$INFRA_DIR" log_info "Executing: $cmd" # Run rollback if eval "$cmd"; then log_success "Rollback completed successfully!" log_success "Application is available at: https://$DOMAIN_NAME" return 0 else log_error "Rollback failed!" return 1 fi } # Cleanup function cleanup() { local exit_code=$? if [[ $exit_code -ne 0 ]]; then log_error "Rollback failed with exit code: $exit_code" log_info "Check the logs above for details" log_info "You may need to manually check the server state" fi exit $exit_code } # Main execution main() { # Set trap for cleanup trap cleanup EXIT # Parse command line arguments parse_args "$@" # Validate environment validate_environment # Get current deployment info get_current_deployment # Confirm rollback action confirm_rollback # Build extra variables build_extra_vars # Run rollback run_rollback log_success "Production rollback completed successfully!" } # Execute main function if script is run directly if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then main "$@" fi