#!/bin/bash set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" if [ -f "${SCRIPT_DIR}/common-lib.sh" ]; then source "${SCRIPT_DIR}/common-lib.sh" else echo "❌ ERROR: common-lib.sh not found at ${SCRIPT_DIR}/common-lib.sh" exit 1 fi # Validate common required environment variables require_var "DEPLOY_HOST" readonly HAPROXY_BASE_DIR="/srv/haproxy" readonly HAPROXY_CONFIGS_DIR="${HAPROXY_BASE_DIR}/configs" readonly HAPROXY_COMPOSE_FILE="${HAPROXY_BASE_DIR}/docker-compose.yml" readonly HAPROXY_MAIN_CONFIG="${HAPROXY_CONFIGS_DIR}/00-haproxy.cfg" readonly BACKEND_TEMPLATE_PATH="${SCRIPT_DIR}/../haproxy.backend.cfg" readonly HAPROXY_COMPOSE_TEMPLATE="${SCRIPT_DIR}/../docker-compose-haproxy.yml" show_usage() { cat << EOF Usage: $0 [add|remove] Commands: add - Add backend configuration (uses \$DOMAIN and \$RELEASE_TYPE env vars) remove - Remove backend configuration Environment Variables: DOMAIN - Domain name for the backend (required for add command) RELEASE_TYPE - Release type (e.g., prod, staging) (required for add command) Examples: DOMAIN=badblocks.dev RELEASE_TYPE=prod $0 add portfolio-prod $0 remove portfolio-staging EOF } setup_infrastructure_dirs_and_certs() { local certs_dir="${HAPROXY_BASE_DIR}/certs" echo "🏗️ Setting up HAProxy directories..." run_or_exit "Failed to setup HAProxy directories" run_on_target "mkdir -p '${HAPROXY_BASE_DIR}' '${HAPROXY_CONFIGS_DIR}' && chmod 755 '${HAPROXY_BASE_DIR}' '${HAPROXY_CONFIGS_DIR}'" # Add certificate setup if environment variables are set if [[ -n "${CF_PEM_CERT:-}" ]] && [[ -n "${CF_PEM_CA:-}" ]]; then echo "🏗️ Setting up HAProxy certificates..." run_or_exit "Failed to setup HAProxy directories" run_on_target "mkdir -p '${certs_dir}' && chmod 755 '${certs_dir}'" run_or_exit "Failed to install crt-${DOMAIN}.pem certificate file" run_on_target "cat > '${certs_dir}/crt-${DOMAIN}.pem' << 'EOF' $CF_PEM_CERT EOF chmod 644 '${certs_dir}/crt-${DOMAIN}.pem'" run_or_exit "Failed to install Cloudflare CA file" run_on_target "cat > '${certs_dir}/ca.pem' << 'EOF' $CF_PEM_CA EOF chmod 644 '${certs_dir}/ca.pem'" fi } init_haproxy_infrastructure() { echo "🚀 Initializing HAProxy infrastructure..." setup_infrastructure_dirs_and_certs # Check both config files in parallel and prepare uploads local main_config_missing=false local compose_config_missing=false local upload_commands=() if ! run_on_target "test -f '${HAPROXY_MAIN_CONFIG}'"; then main_config_missing=true upload_commands+=("scp '${SCRIPT_DIR}/../haproxy.cfg' deploy:'${HAPROXY_MAIN_CONFIG}'") echo "📋 Main HAProxy config needs creation" else echo "✅ Main config already exists" fi if ! run_on_target "test -f '${HAPROXY_COMPOSE_FILE}'"; then [ ! -f "$HAPROXY_COMPOSE_TEMPLATE" ] && error_exit "HAProxy compose template not found at $HAPROXY_COMPOSE_TEMPLATE" compose_config_missing=true upload_commands+=("scp '${HAPROXY_COMPOSE_TEMPLATE}' deploy:'${HAPROXY_COMPOSE_FILE}'") echo "🐳 Docker-compose config needs creation" else echo "✅ Docker-compose config already exists" fi # Upload missing config files in parallel if any are needed if [ ${#upload_commands[@]} -gt 0 ]; then echo "📤 Uploading configuration files..." for cmd in "${upload_commands[@]}"; do eval "$cmd" || error_exit "Failed to upload config file: $cmd" done & wait echo "✅ Configuration files uploaded" fi # Start HAProxy service (creates network automatically via compose) if ! is_haproxy_running; then echo "▶️ Starting HAProxy service..." if run_on_target "cd '${HAPROXY_BASE_DIR}' && docker compose up -d"; then echo "✅ HAProxy containers started" echo "⏳ Waiting for HAProxy to be healthy..." if wait_for_haproxy_healthy; then echo "✅ HAProxy is healthy and ready" else echo "❌ ERROR: HAProxy failed to become healthy" return 1 fi else echo "❌ ERROR: Failed to start HAProxy containers" exit 1 fi else echo "✅ HAProxy is already running" fi echo "✅ HAProxy infrastructure ready" } wait_for_haproxy_healthy() { local attempts=0 local max_attempts=30 while [ $attempts -lt $max_attempts ]; do attempts=$((attempts + 1)) # Get status in single call and parse more efficiently local container_info container_info=$(run_on_target "cd '${HAPROXY_BASE_DIR}' && docker compose ps haproxy --format '{{.Status}}'") # Check if container is healthy or running (single grep operation) if echo "$container_info" | grep -qE "(health: healthy|Up.*[0-9])"; then return 0 fi [ $attempts -lt $max_attempts ] && sleep 2 done echo "❌ ERROR: HAProxy failed to become healthy after $max_attempts attempts" echo " Last status: $container_info" return 1 } is_haproxy_running() { run_on_target "cd '${HAPROXY_BASE_DIR}' && docker compose ps haproxy --format '{{.Status}}' | grep -q 'Up'" } is_haproxy_infrastructure_ready() { if ! run_on_target "test -d '${HAPROXY_BASE_DIR}'"; then return 1 fi if ! run_on_target "test -f '${HAPROXY_MAIN_CONFIG}'"; then return 1 fi if ! run_on_target "test -f '${HAPROXY_COMPOSE_FILE}'"; then return 1 fi if ! is_haproxy_running; then return 1 fi return 0 } add_backend() { local backend_name="$1" [[ -z "$backend_name" ]] && { show_usage error_exit "add command requires backend_name" } [[ -z "${DOMAIN:-}" ]] && { show_usage error_exit "DOMAIN environment variable is required for add command" } [[ -z "${RELEASE_TYPE:-}" ]] && { show_usage error_exit "RELEASE_TYPE environment variable is required for add command" } echo "➕ Adding backend: $backend_name" # TODO: MAKE THIS IDEMPOTENT AND REMOVE THE is_haproxy_infrastructure_ready CHECK if ! is_haproxy_infrastructure_ready; then echo "🔧 Initializing HAProxy infrastructure..." init_haproxy_infrastructure fi local backend_config_path="${HAPROXY_CONFIGS_DIR}/${backend_name}.cfg" [ ! -f "$BACKEND_TEMPLATE_PATH" ] && error_exit "Backend template not found at $BACKEND_TEMPLATE_PATH" local temp_config="/tmp/${backend_name}.cfg" echo "📝 Generating backend config from template..." cp "$BACKEND_TEMPLATE_PATH" "$temp_config" substitute_env_vars "$temp_config" echo "✅ Backend config generated" echo "📤 Uploading backend config..." scp "$temp_config" deploy:"$backend_config_path" || { rm -f "$temp_config"; error_exit "Failed to upload backend config"; } echo "✅ Backend config uploaded" rm -f "$temp_config" echo "🔄 Reloading HAProxy..." reload_haproxy } remove_backend() { local backend_name="$1" [ -z "$backend_name" ] && { show_usage; error_exit "remove command requires backend_name"; } echo "➖ Removing backend: $backend_name" if ! is_haproxy_infrastructure_ready; then echo "🔧 Initializing HAProxy infrastructure..." init_haproxy_infrastructure fi local backend_config_path="${HAPROXY_CONFIGS_DIR}/${backend_name}.cfg" if run_on_target "test -f '${backend_config_path}'"; then run_on_target "rm -f '${backend_config_path}'" echo "✅ Backend configuration removed" reload_haproxy else echo "✅ Backend configuration already absent" fi } reload_haproxy() { if ! is_haproxy_infrastructure_ready; then echo "🔧 Infrastructure not ready, initializing..." init_haproxy_infrastructure return 0 # Infrastructure init already handles health check fi run_or_exit "Failed to send reload signal to HAProxy" run_on_target "cd '${HAPROXY_BASE_DIR}' && docker kill -s HUP haproxy" # Brief pause for reload to take effect, then single health check sleep 1 if wait_for_haproxy_healthy; then echo "✅ HAProxy configuration reloaded" else error_exit "HAProxy became unhealthy after reload" fi } validate_backend_name() { local backend_name="$1" [ -z "$backend_name" ] && error_exit "Backend name cannot be empty" [[ "$backend_name" =~ ^[a-zA-Z0-9][a-zA-Z0-9_-]*$ ]] 2>/dev/null || error_exit "Invalid backend name '$backend_name'. Can only contain alphanumeric characters, hyphens, and underscores." } main() { local command="${1:-}" case "$command" in "add") local backend_name="${2:-}" validate_backend_name "$backend_name" add_backend "$backend_name" ;; "remove") local backend_name="${2:-}" validate_backend_name "$backend_name" remove_backend "$backend_name" ;; "help"|"-h"|"--help") show_usage ;; *) show_usage error_exit "Unknown command '$command'" ;; esac echo "✅ HAProxy configuration completed" } [[ "${BASH_SOURCE[0]}" == "${0}" ]] && main "$@"