294 lines
9.3 KiB
Bash
Executable file
294 lines
9.3 KiB
Bash
Executable file
#!/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] <backend_name>
|
||
|
||
Commands:
|
||
add <backend_name> - Add backend configuration (uses \$DOMAIN and \$RELEASE_TYPE env vars)
|
||
remove <backend_name> - 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 "$@"
|