#!/bin/bash # Common library for deployment scripts # Source this file in other scripts: source "${SCRIPT_DIR}/common-lib.sh" # Common constants readonly BLUE_COLOR="blue" readonly GREEN_COLOR="green" readonly CORE_PROJECT_NAME="pkmntrade-club" readonly DEPLOYMENT_LABEL="deployment.color" readonly RETRY_MAX_ATTEMPTS="${RETRY_MAX_ATTEMPTS:-5}" readonly RETRY_DELAY="${RETRY_DELAY:-5}" # Dry run helper function # Usage: execute_if_not_dry "description" command [args...] execute_if_not_dry() { local description="$1" shift if [ "$DRY_RUN" = true ]; then indent_output echo "[DRY RUN] Would execute: $description" indent_output echo " Command: $*" else "$@" fi } # Execute with error handling # Usage: execute_or_fail "description" command [args...] execute_or_fail() { local description="$1" shift if [ "$DRY_RUN" = true ]; then indent_output echo "[DRY RUN] Would execute: $description" indent_output echo " Command: $*" else if ! "$@"; then echo "❌ Error: Failed to $description" exit 1 fi fi } # Execute with warning on failure (non-critical operations) # Usage: execute_or_warn "description" command [args...] execute_or_warn() { local description="$1" shift if [ "$DRY_RUN" = true ]; then indent_output echo "[DRY RUN] Would execute: $description" indent_output echo " Command: $*" else if ! "$@"; then echo "⚠️ Warning: Failed to $description (continuing anyway)" fi fi } # Retry a command with exponential backoff retry() { local max_attempts=$RETRY_MAX_ATTEMPTS local delay=$RETRY_DELAY local attempt=1 local exit_code=0 until "$@"; do exit_code=$? if [ "$attempt" -ge "$max_attempts" ]; then echo "❌ Command failed after $max_attempts attempts: $*" >&2 return $exit_code fi echo "⚠️ Attempt $attempt failed, retrying in ${delay}s..." >&2 sleep "$delay" # Exponential backoff delay=$((delay * 2)) attempt=$((attempt + 1)) done if [ $attempt -gt 1 ]; then echo "✅ Command succeeded after $attempt attempts" fi return 0 } run_on_target() { # if DEPLOY_HOST is set, we are not on remote if [[ -n "${DEPLOY_HOST}" ]]; then ssh deploy "$*" else bash -c -- "$*" fi } # Function to check if a variable is set require_var() { local var_name=$1 local var_value=${!var_name} if [ -z "$var_value" ]; then echo "Error: ${var_name} not set" >&2 exit 1 fi } # Function to get deployment color based on running containers get_current_color() { local blue_count=$(docker ps --filter "label=${DEPLOYMENT_LABEL}=${BLUE_COLOR}" -q 2>/dev/null | wc -l) local green_count=$(docker ps --filter "label=${DEPLOYMENT_LABEL}=${GREEN_COLOR}" -q 2>/dev/null | wc -l) if [ "$blue_count" -gt 0 ] && [ "$green_count" -eq 0 ]; then echo "$BLUE_COLOR" elif [ "$green_count" -gt 0 ] && [ "$blue_count" -eq 0 ]; then echo "$GREEN_COLOR" elif [ "$blue_count" -gt 0 ] && [ "$green_count" -gt 0 ]; then # Both colors running - return the newer one local blue_newest=$(docker inspect --format='{{.Created}}' "$(docker ps -q --filter "label=${DEPLOYMENT_LABEL}=${BLUE_COLOR}" | head -1)" 2>/dev/null || echo '1970-01-01') local green_newest=$(docker inspect --format='{{.Created}}' "$(docker ps -q --filter "label=${DEPLOYMENT_LABEL}=${GREEN_COLOR}" | head -1)" 2>/dev/null || echo '1970-01-01') if [[ "$blue_newest" > "$green_newest" ]]; then echo "$BLUE_COLOR" else echo "$GREEN_COLOR" fi else echo "none" fi } # Function to get deployment state (none, blue, green, both) get_deployment_state() { local blue_count=$(docker ps --filter "label=${DEPLOYMENT_LABEL}=${BLUE_COLOR}" -q 2>/dev/null | wc -l) local green_count=$(docker ps --filter "label=${DEPLOYMENT_LABEL}=${GREEN_COLOR}" -q 2>/dev/null | wc -l) if [ "$blue_count" -gt 0 ] && [ "$green_count" -gt 0 ]; then echo "both" elif [ "$blue_count" -gt 0 ]; then echo "$BLUE_COLOR" elif [ "$green_count" -gt 0 ]; then echo "$GREEN_COLOR" else echo "none" fi } # Function to check if deployment is in progress is_deployment_in_progress() { local blue_count=$(docker ps --filter "label=${DEPLOYMENT_LABEL}=${BLUE_COLOR}" -q 2>/dev/null | wc -l) local green_count=$(docker ps --filter "label=${DEPLOYMENT_LABEL}=${GREEN_COLOR}" -q 2>/dev/null | wc -l) if [ "$blue_count" -gt 0 ] && [ "$green_count" -gt 0 ]; then return 0 # true - deployment in progress else return 1 # false - no deployment in progress fi } # Function to switch color switch_color() { local current=$1 if [ "$current" = "$BLUE_COLOR" ]; then echo "$GREEN_COLOR" else echo "$BLUE_COLOR" fi } # Function to get project name for a color get_project_name() { local color=$1 echo "${CORE_PROJECT_NAME}-${color}" } # Function to get compose files based on PROD setting get_compose_files() { # Always use the same docker-compose file for both staging and production echo "-f docker-compose_web.yml" } # Function to refresh gatekeepers refresh_gatekeepers() { echo "🔄 Refreshing gatekeepers..." docker kill -s SIGHUP ${CORE_PROJECT_NAME}-gatekeeper-manager-1 2>/dev/null || true } # Function to count containers by filter count_containers() { local filters=$1 docker ps ${filters} -q 2>/dev/null | wc -l | tr -d '\n' || echo 0 } get_previous_release_path() { local current_link_path=$1 local previous_release_path=$(run_on_target "cat '${current_link_path}/.previous_version'") echo "${previous_release_path}" } # Function to stop and remove the previous release's containers for a color cleanup_color_containers() { local color=$1 local project_name=$(get_project_name "$color") # Use CLEANUP_RELEASE_PATH if set, otherwise default to the previous release. # This is crucial for rollbacks to use the correct compose file for cleanup. local release_path=${CLEANUP_RELEASE_PATH:-$(get_previous_release_path "${CURRENT_LINK_PATH}")} echo "🛑 Stopping $color containers from release: ${release_path}" run_on_target "cd '${release_path}' && docker compose -p '${project_name}' stop --timeout 30 2>/dev/null || true" echo "🗑️ Removing $color containers from release: ${release_path}" run_on_target "cd '${release_path}' && docker compose -p '${project_name}' down --remove-orphans 2>/dev/null || true" } # Function to wait with countdown wait_with_countdown() { local seconds=$1 local message=$2 echo -n "$message" for ((i=seconds; i>0; i--)); do echo -n " $i" sleep 1 done echo " done!" } get_web_service_name() { echo "web" # hardcoded for now } # Standard environment validation validate_deployment_env() { require_var "REPO_PROJECT_PATH" require_var "PROD" require_var "REPLICA_COUNT" if [ "$PROD" = "true" ]; then require_var "PRODUCTION_DOMAIN" else require_var "STAGING_DOMAIN" fi # Set derived variables export CURRENT_LINK_PATH="${REPO_PROJECT_PATH}/current" export RELEASES_PATH="${REPO_PROJECT_PATH}/releases" export REPLICA_COUNT="${REPLICA_COUNT}" } get_health_check_status() { # TODO: instead get the health check status from gatus container's api local statuses=$(docker ps --format '{{.Names}} {{.Status}}') local unhealthy_count=0 local IFS=$'\n' for status in $statuses; do local name=$(echo $status | cut -d' ' -f1) local status=$(echo $status | cut -d' ' -f2-) if [[ "$status" == *"unhealthy"* ]]; then unhealthy_count=$((unhealthy_count + 1)) echo "❌ Unhealthy: $name [$status]" else echo "✅ Healthy: $name [$status]" fi done return $unhealthy_count } # Function to wait for containers to be healthy wait_for_healthy_containers() { local project_name=$1 local service_name=$2 local expected_count=$3 local max_attempts=60 # 5 minutes with 5-second intervals local attempt=0 echo "⏳ Waiting for $service_name containers to be healthy..." while [ $attempt -lt $max_attempts ]; do healthy_count=$(count_containers "--filter label=com.docker.compose.project=${project_name} --filter label=com.docker.compose.service=${service_name} --filter health=healthy") if [[ "$healthy_count" -eq "$expected_count" ]]; then echo "✅ All $service_name containers are healthy ($healthy_count/$expected_count)" return 0 fi echo "⏳ Healthy containers: $healthy_count/$expected_count (attempt $((attempt+1))/$max_attempts)" sleep 5 attempt=$((attempt + 1)) done echo "❌ Timeout waiting for $service_name containers to be healthy" return 1 } list_releases() { local REPO_PROJECT_PATH=$1 local RELEASES_PATH="${REPO_PROJECT_PATH}/releases" local CURRENT_LINK_PATH="${REPO_PROJECT_PATH}/current" echo "📋 Available releases:" if [ -d "$RELEASES_PATH" ]; then for release in $(ls -dt ${RELEASES_PATH}/*); do version=$(basename "$release") status="" # Check if it's current if [ -L "$CURRENT_LINK_PATH" ] && [ "$(readlink -f "$CURRENT_LINK_PATH")" = "$(realpath "$release")" ]; then status=" [CURRENT]" fi # Check if it failed if [ -f "${release}/.failed" ]; then status="${status} [FAILED]" fi indent_output echo "- ${version}${status}" done else indent_output echo "No releases found" fi } # Function to get image tag from deployment get_deployment_image_tag() { local color=$1 local container=$(docker ps --filter "label=com.docker.compose.project=${CORE_PROJECT_NAME}-${color}" --format '{{.Names}}'| head -1) if [ -n "$container" ]; then docker inspect "${container}" --format '{{index .Config.Labels "deployment.image_tag"}}' else echo "unknown" fi } # Function to run a command and prefix its output # Usage: prefix_output "PREFIX" command [args...] # Example: prefix_output " | " docker ps # Example: prefix_output " => " docker compose ps prefix_output() { local prefix=" " if [ $# -lt 2 ]; then echo "Error: prefix_output requires at least 2 arguments" >&2 return 1 fi prefix="$1" shift # Run the command and prefix each line of output "$@" 2>&1 | sed "s/^/${prefix}/" # Return the exit code of the original command (not sed) return ${PIPESTATUS[0]} } # Function to run a command and indent its output # Usage: indent_output [INDENT_STRING] command [args...] # Example: indent_output docker ps # Uses default 2 spaces # Example: indent_output " " docker ps # Uses 4 spaces indent_output() { local indent=" " # Default to 2 spaces # Check if first argument looks like an indent string (starts with spaces or tabs) if [[ "$1" =~ ^[[:space:]]+$ ]]; then indent="$1" shift fi # Use prefix_output with the indent string prefix_output "$indent" "$@" } # Function to run command with header and indented output # Usage: run_with_header "HEADER" command [args...] # Example: run_with_header "Docker Containers:" docker ps run_with_header() { local header="$1" shift echo "$header" indent_output " " "$@" }