374 lines
10 KiB
Bash
Executable file
374 lines
10 KiB
Bash
Executable file
#!/bin/bash
|
||
|
||
readonly BLUE_COLOR="blue"
|
||
readonly GREEN_COLOR="green"
|
||
readonly CORE_PROJECT_NAME="portfolio"
|
||
readonly DEPLOYMENT_LABEL="deployment.color"
|
||
readonly RETRY_MAX_ATTEMPTS="${RETRY_MAX_ATTEMPTS:-5}"
|
||
readonly RETRY_DELAY="${RETRY_DELAY:-5}"
|
||
|
||
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_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_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() {
|
||
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"
|
||
|
||
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 [[ -n "${DEPLOY_HOST}" ]]; then
|
||
ssh deploy -q -E /dev/null "$*"
|
||
else
|
||
bash -c -- "$*"
|
||
fi
|
||
}
|
||
|
||
require_var() {
|
||
local var_name=$1
|
||
local var_value=${!var_name:-}
|
||
|
||
if [ -z "$var_value" ]; then
|
||
echo "❌ Deployment Error: Required environment variable '${var_name}' is not set or is empty" >&2
|
||
echo " Please check your GitHub Actions secrets, workflow environment variables," >&2
|
||
echo " or deployment configuration to ensure '${var_name}' is properly defined." >&2
|
||
exit 1
|
||
fi
|
||
}
|
||
|
||
# Helper function for common error handling pattern
|
||
error_exit() {
|
||
echo "❌ ERROR: $1" >&2
|
||
exit 1
|
||
}
|
||
|
||
# Helper function for command execution with error handling
|
||
run_or_exit() {
|
||
local description="$1"
|
||
shift
|
||
|
||
if ! "$@"; then
|
||
error_exit "$description"
|
||
fi
|
||
}
|
||
|
||
get_current_color() {
|
||
local blue_count=$(count_color_containers "$BLUE_COLOR")
|
||
local green_count=$(count_color_containers "$GREEN_COLOR")
|
||
|
||
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
|
||
local project_name_blue=$(get_project_name "$BLUE_COLOR")
|
||
local project_name_green=$(get_project_name "$GREEN_COLOR")
|
||
local blue_newest=$(docker inspect --format='{{.Created}}' "$(docker ps -q --filter "label=com.docker.compose.project=$project_name_blue" | head -1)" 2>/dev/null || echo '1970-01-01')
|
||
local green_newest=$(docker inspect --format='{{.Created}}' "$(docker ps -q --filter "label=com.docker.compose.project=$project_name_green" | 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
|
||
}
|
||
|
||
get_deployment_state() {
|
||
local blue_count=$(count_color_containers "$BLUE_COLOR")
|
||
local green_count=$(count_color_containers "$GREEN_COLOR")
|
||
|
||
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
|
||
}
|
||
|
||
is_deployment_in_progress() {
|
||
local deployment_state=$(get_deployment_state)
|
||
[ "$deployment_state" = "both" ]
|
||
}
|
||
|
||
switch_color() {
|
||
[ "$1" = "$BLUE_COLOR" ] && echo "$GREEN_COLOR" || echo "$BLUE_COLOR"
|
||
}
|
||
|
||
get_project_name() {
|
||
local color=$1
|
||
local env_suffix=$([ "${PROD:-}" = "true" ] && echo "prod" || echo "staging")
|
||
echo "${CORE_PROJECT_NAME}-${env_suffix}-${color}"
|
||
}
|
||
|
||
get_compose_files() {
|
||
echo "-f docker-compose_web.yml"
|
||
}
|
||
|
||
refresh_proxy() {
|
||
echo "🔄 Refreshing proxy configuration..."
|
||
}
|
||
|
||
count_containers() {
|
||
local filters=$1
|
||
docker ps ${filters} -q 2>/dev/null | wc -l | tr -d '\n' || echo 0
|
||
}
|
||
|
||
count_color_containers() {
|
||
local color=$1
|
||
local project_name=$(get_project_name "$color")
|
||
docker ps --filter "label=com.docker.compose.project=$project_name" -q 2>/dev/null | wc -l
|
||
}
|
||
|
||
# Removed get_previous_release_path - no longer needed as we use direct container cleanup
|
||
|
||
cleanup_color_containers() {
|
||
local color=$1
|
||
local project_name=$(get_project_name "$color")
|
||
|
||
# Get container IDs using the same filter logic as get_current_color
|
||
local container_ids=$(run_on_target "docker ps --filter 'label=com.docker.compose.project=$project_name' -q 2>/dev/null")
|
||
|
||
if [ -n "$container_ids" ]; then
|
||
echo "🛑 Stopping $color containers from project: $project_name"
|
||
# Stop containers directly by ID, with timeout
|
||
run_on_target "echo '$container_ids' | xargs -r docker stop --timeout 10 2>/dev/null || true"
|
||
|
||
echo "🗑️ Removing $color containers from project: $project_name"
|
||
# Remove containers directly by ID
|
||
run_on_target "echo '$container_ids' | xargs -r docker rm -f 2>/dev/null || true"
|
||
else
|
||
echo "ℹ️ No $color containers found to clean up"
|
||
fi
|
||
}
|
||
|
||
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 "portfolio"
|
||
}
|
||
validate_deployment_env() {
|
||
require_var "REPO_PROJECT_PATH"
|
||
require_var "PROD"
|
||
require_var "DOMAIN"
|
||
|
||
export CURRENT_LINK_PATH="${REPO_PROJECT_PATH}/current"
|
||
export RELEASES_PATH="${REPO_PROJECT_PATH}/releases"
|
||
}
|
||
|
||
get_health_check_status() {
|
||
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
|
||
}
|
||
|
||
wait_for_healthy_containers() {
|
||
local project_name=$1
|
||
local service_name=$2
|
||
local expected_count=$3
|
||
local max_attempts=60
|
||
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=""
|
||
|
||
if [ -L "$CURRENT_LINK_PATH" ] && [ "$(readlink -f "$CURRENT_LINK_PATH")" = "$(realpath "$release")" ]; then
|
||
status=" [CURRENT]"
|
||
fi
|
||
|
||
if [ -f "${release}/.failed" ]; then
|
||
status="${status} [FAILED]"
|
||
fi
|
||
|
||
indent_output echo "- ${version}${status}"
|
||
done
|
||
else
|
||
indent_output echo "No releases found"
|
||
fi
|
||
}
|
||
|
||
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
|
||
}
|
||
|
||
prefix_output() {
|
||
local prefix=" "
|
||
|
||
if [ $# -lt 2 ]; then
|
||
echo "Error: prefix_output requires at least 2 arguments" >&2
|
||
return 1
|
||
fi
|
||
|
||
prefix="$1"
|
||
shift
|
||
|
||
"$@" 2>&1 | sed "s/^/${prefix}/"
|
||
|
||
return ${PIPESTATUS[0]}
|
||
}
|
||
|
||
indent_output() {
|
||
local indent=" "
|
||
|
||
if [[ "$1" =~ ^[[:space:]]+$ ]]; then
|
||
indent="$1"
|
||
shift
|
||
fi
|
||
|
||
prefix_output "$indent" "$@"
|
||
}
|
||
|
||
run_with_header() {
|
||
local header="$1"
|
||
shift
|
||
|
||
echo "$header"
|
||
indent_output " " "$@"
|
||
}
|
||
|
||
substitute_env_vars() {
|
||
local file_path="$1"
|
||
|
||
if [ ! -f "$file_path" ]; then
|
||
echo "❌ Error: File '$file_path' does not exist" >&2
|
||
return 1
|
||
fi
|
||
|
||
while IFS= read -r line; do
|
||
while [[ "$line" =~ \$\{([A-Za-z_][A-Za-z0-9_]*)\} ]]; do
|
||
local var_name="${BASH_REMATCH[1]}"
|
||
local var_value="${!var_name:-}"
|
||
line="${line//\$\{${var_name}\}/${var_value}}"
|
||
done
|
||
echo "$line"
|
||
done < "$file_path" > "$file_path.tmp" && mv "$file_path.tmp" "$file_path"
|
||
}
|
||
|
||
substitute_env_vars_remote() {
|
||
local file_path="$1"
|
||
|
||
if [[ -n "${DEPLOY_HOST}" ]]; then
|
||
run_on_target "$(declare -f substitute_env_vars); substitute_env_vars '$file_path'"
|
||
else
|
||
substitute_env_vars "$file_path"
|
||
fi
|
||
}
|