pkmntrade.club/server/scripts/manage.sh
badbl0cks 30ce126a07
feat(deploy): implement blue-green deployment strategy
This commit replaces the previous deployment mechanism with a blue-green strategy to lay the groundwork for zero-downtime deployments.
Key changes:
Introduces a deploy-blue-green.sh script to manage "blue" and "green" container sets, creating versioned releases.
Updates the Anubis gatekeeper template to dynamically route traffic based on the active deployment color, allowing for seamless traffic switching.
Modifies Docker Compose files to include color-specific labels and environment variables.
Adapts the GitHub Actions workflow to execute the new blue-green deployment process.
Removes the old, now-obsolete deployment and health check scripts.
Note: Automated rollback on health check failure is not yet implemented. Downgrades can be performed manually by switching the active color.
2025-06-12 16:58:55 -07:00

377 lines
No EOL
14 KiB
Bash
Executable file
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/bin/bash
set -euo pipefail
# Manage deployment releases
# Usage: ./manage.sh [--dry-run] COMMAND [ARGS]
# Source common functions
SCRIPT_DIR="$(cd "$(dirname "$(realpath "${BASH_SOURCE[0]}")")" && pwd)"
source "${SCRIPT_DIR}/common-lib.sh"
# Global variables
DRY_RUN=false
COMMAND=""
ARGS=()
# Parse global options
while [[ $# -gt 0 ]]; do
case "$1" in
--dry-run)
DRY_RUN=true
shift
;;
-*)
echo "Error: Unknown option: $1"
echo "Usage: $0 [--dry-run] COMMAND [ARGS]"
exit 1
;;
*)
# First non-option argument is the command
if [ -z "$COMMAND" ]; then
COMMAND="$1"
else
# Rest are command arguments
ARGS+=("$1")
fi
shift
;;
esac
done
if [ -z "$COMMAND" ]; then
echo "Error: No command specified"
echo "Usage: $0 [--dry-run] COMMAND [ARGS]"
echo "Commands:"
indent_output echo "status - Show deployment status"
indent_output echo "list - List all releases"
indent_output echo "version - Show current release"
indent_output echo "switch VERSION - Switch to a specific release version"
indent_output echo "cleanup [KEEP] - Clean up old releases (default: keep 5)"
echo ""
echo "Global options:"
indent_output echo "--dry-run - Show what would happen without making changes"
exit 1
fi
REPO_PROJECT_PATH="$(realpath "${SCRIPT_DIR}/../../../")"
CURRENT_LINK_PATH="${REPO_PROJECT_PATH}/current"
RELEASES_PATH="${REPO_PROJECT_PATH}/releases"
# Announce dry-run mode if active
if [ "$DRY_RUN" = true ]; then
echo "🔍 DRY RUN MODE - No changes will be made"
echo ""
fi
case "$COMMAND" in
status)
echo "🔍 Deployment Status"
echo "===================="
# Check if deployment is downgraded
if [ -d "$RELEASES_PATH" ] && [ -L "$CURRENT_LINK_PATH" ]; then
CURRENT_RELEASE_DIR_NAME=$(basename "$(readlink -f "$CURRENT_LINK_PATH")")
# Find the latest release by modification time.
LATEST_RELEASE_DIR_NAME=$(find "$RELEASES_PATH" -maxdepth 1 -mindepth 1 -type d -printf '%T@ %f\n' | sort -nr | head -n 1 | cut -d' ' -f2-)
if [ -n "$LATEST_RELEASE_DIR_NAME" ]; then
if [ "$CURRENT_RELEASE_DIR_NAME" == "$LATEST_RELEASE_DIR_NAME" ]; then
echo "✅ Deployment is on the latest release (${LATEST_RELEASE_DIR_NAME})."
else
echo "⚠️ Deployment is downgraded."
indent_output echo "Current: ${CURRENT_RELEASE_DIR_NAME}"
indent_output echo "Latest: ${LATEST_RELEASE_DIR_NAME}"
fi
else
# This case happens if RELEASES_PATH is empty
echo " No releases found in ${RELEASES_PATH}."
fi
elif [ ! -L "$CURRENT_LINK_PATH" ]; then
echo " No current deployment symlink found."
else # RELEASES_PATH does not exist
echo " Releases directory not found at ${RELEASES_PATH}."
fi
echo "" # Add a newline for spacing
# Get current state
CURRENT_STATE=$(get_deployment_state)
if [ "$CURRENT_STATE" = "both" ]; then
echo "🟡 Deployment State: both"
elif [ "$CURRENT_STATE" = "blue" ]; then
echo "🔵 Deployment State: blue"
elif [ "$CURRENT_STATE" = "green" ]; then
echo "🟢 Deployment State: green"
else
indent_output echo "Deployment State: none"
fi
echo "⚙️ Core Containers:"
indent_output docker ps --filter 'label=deployment.core=true' --format 'table {{.Names}}\t{{.Status}}\t{{.CreatedAt}}'
# Show containers by color with image info
echo "🔵 Blue Containers:"
BLUE_COUNT=$(count_containers "--filter label=deployment.color=blue")
# make sure BLUE_COUNT is a number
BLUE_COUNT=$(echo "$BLUE_COUNT" | tr -d '\n')
if [ "$BLUE_COUNT" -gt 0 ]; then
BLUE_IMAGE=$(get_deployment_image_tag "blue")
indent_output echo "Image: ${BLUE_IMAGE}"
indent_output docker ps --filter 'label=deployment.color=blue' --format 'table {{.Names}}\t{{.Status}}\t{{.CreatedAt}}'
else
indent_output echo "No blue containers running"
fi
echo "🟢 Green Containers:"
GREEN_COUNT=$(count_containers "--filter label=deployment.color=green")
if [ "$GREEN_COUNT" -gt 0 ]; then
GREEN_IMAGE=$(get_deployment_image_tag "green")
indent_output echo "Image: ${GREEN_IMAGE}"
indent_output docker ps --filter 'label=deployment.color=green' --format 'table {{.Names}}\t{{.Status}}\t{{.CreatedAt}}'
else
indent_output echo "No green containers running"
fi
list_releases "${REPO_PROJECT_PATH}"
# Health check summary
echo "❤️ Health Check Summary:"
case "$CURRENT_STATE" in
"both")
indent_output echo "⚠️ WARNING: Both blue and green containers are running!"
indent_output echo "This might indicate an incomplete deployment."
;;
"none")
indent_output echo "⚠️ WARNING: No web containers are running!"
;;
*)
if [ "$CURRENT_STATE" = "blue" ]; then
indent_output echo "🔵 System is running on blue deployment"
else
indent_output echo "🟢 System is running on green deployment"
fi
indent_output echo "❤️ Overall Healthcheck:"
indent_output " " get_health_check_status
;;
esac
# Show resource usage
echo "📊 Resource Usage:"
indent_output docker stats --no-stream --format 'table {{.Name}}\t{{.CPUPerc}}\t{{.MemPerc}}\t{{.MemUsage}}\t{{.NetIO}}'
# Show deployment images
echo "📦 Deployment Images:"
indent_output docker images 'badbl0cks/pkmntrade-club' --format 'table {{.Tag}}\t{{.ID}}\t{{.CreatedAt}}\t{{.Size}}' | grep -E '^TAG|sha-.{7} ' || indent_output echo "No deployment images found"
;;
list)
list_releases "${REPO_PROJECT_PATH}"
;;
version)
if [ -L "$CURRENT_LINK_PATH" ]; then
current_version=$(basename "$(readlink -f "$CURRENT_LINK_PATH")")
echo "📌 Current version: ${current_version}"
else
echo "❌ No current deployment found"
fi
;;
switch)
if [ ${#ARGS[@]} -lt 1 ]; then
echo "Error: VERSION required for switch"
echo "Usage: $0 [--dry-run] switch VERSION"
exit 1
fi
TARGET_VERSION="${ARGS[0]}"
TARGET_PATH="${RELEASES_PATH}/${TARGET_VERSION}"
# Validate target version exists
if [ ! -d "$TARGET_PATH" ]; then
echo "❌ Error: Version ${TARGET_VERSION} not found"
echo "Available releases:"
list_releases "${REPO_PROJECT_PATH}"
exit 1
fi
# Get current version if exists
CURRENT_VERSION="none"
CURRENT_VERSION_PATH=""
if [ -L "$CURRENT_LINK_PATH" ]; then
CURRENT_VERSION_PATH=$(readlink -f "$CURRENT_LINK_PATH")
CURRENT_VERSION=$(basename "$CURRENT_VERSION_PATH")
fi
# Edge case: trying to switch to the same version
if [ "$CURRENT_VERSION" == "$TARGET_VERSION" ]; then
echo "✅ Already on version ${TARGET_VERSION}. No action taken."
exit 0
fi
CURRENT_COLOR=$(get_current_color)
NEW_COLOR=$(switch_color "$CURRENT_COLOR")
echo "🔄 Switch Plan:"
indent_output echo "Current version: ${CURRENT_VERSION}"
if [ "$CURRENT_VERSION" != "none" ]; then
indent_output echo "Current path: ${CURRENT_VERSION_PATH}"
indent_output echo "Current color: ${CURRENT_COLOR}"
fi
indent_output echo "Target version: ${TARGET_VERSION}"
indent_output echo "Target path: ${TARGET_PATH}"
indent_output echo "Target color: ${NEW_COLOR}"
# Verify target release has necessary files
echo "📋 Checking target release integrity..."
MISSING_FILES=()
for file in "docker-compose_web.yml" "docker-compose_core.yml" ".env"; do
if [ ! -f "${TARGET_PATH}/${file}" ]; then
MISSING_FILES+=("$file")
fi
done
if [ ${#MISSING_FILES[@]} -gt 0 ]; then
echo "❌ Error: Target release is missing required files:"
printf " - %s\n" "${MISSING_FILES[@]}"
exit 1
fi
# Get compose files based on environment
COMPOSE_FILES=$(get_compose_files)
echo "🛑 Stopping current containers..."
if [ -d "$CURRENT_VERSION_PATH" ]; then
(
cd "$CURRENT_VERSION_PATH" || exit 1
WEB_PROJECT_NAME=$(get_project_name "$CURRENT_COLOR")
indent_output echo "Stopping web containers for project: ${WEB_PROJECT_NAME}..."
execute_or_warn "stop web containers" docker compose ${COMPOSE_FILES} -p "${WEB_PROJECT_NAME}" down
indent_output echo "Stopping core services for project: ${CORE_PROJECT_NAME}..."
execute_or_warn "stop core services" docker compose -f "docker-compose_core.yml" -p "${CORE_PROJECT_NAME}" down
)
else
indent_output echo "No current deployment to stop"
fi
echo "📝 Updating deployment metadata for ${TARGET_VERSION}..."
execute_or_fail "update .deployment_color to ${NEW_COLOR}" \
bash -c "echo '${NEW_COLOR}' > '${TARGET_PATH}/.deployment_color'"
execute_or_fail "update DEPLOYMENT_COLOR in .env" \
bash -c "sed -i 's/^DEPLOYMENT_COLOR=.*/DEPLOYMENT_COLOR=${NEW_COLOR}/' '${TARGET_PATH}/.env'"
# Update symlink
echo "🔗 Updating deployment symlink..."
execute_or_fail "update symlink from $CURRENT_LINK_PATH to $TARGET_PATH" \
ln -sfn "$TARGET_PATH" "$CURRENT_LINK_PATH"
# Start containers
echo "🚀 Starting containers from ${TARGET_VERSION}..."
(
cd "$TARGET_PATH" || exit 1
TARGET_WEB_PROJECT_NAME=$(get_project_name "$NEW_COLOR")
indent_output echo "Starting core services for project: ${CORE_PROJECT_NAME}..."
execute_or_fail "start core services" \
docker compose -f "docker-compose_core.yml" -p "${CORE_PROJECT_NAME}" up -d
indent_output echo "Starting web containers for project: ${TARGET_WEB_PROJECT_NAME}..."
execute_or_fail "start web containers" \
docker compose ${COMPOSE_FILES} -p "${TARGET_WEB_PROJECT_NAME}" up -d
)
if [ "$DRY_RUN" = true ]; then
echo ""
echo "✅ Dry run completed - no changes made"
else
echo "✅ Switch completed to version: ${TARGET_VERSION}"
echo "Run '$0 status' to verify deployment health"
fi
;;
cleanup)
# Parse cleanup arguments
KEEP_COUNT=5
for arg in "${ARGS[@]}"; do
if [[ "$arg" =~ ^[0-9]+$ ]]; then
KEEP_COUNT="$arg"
else
echo "Error: Invalid argument for cleanup: $arg"
echo "Usage: $0 [--dry-run] cleanup [KEEP_COUNT]"
exit 1
fi
done
echo "🗑️ Cleaning up old releases (keeping last ${KEEP_COUNT} and current)"
if [ ! -L "$CURRENT_LINK_PATH" ]; then
echo "❌ No current deployment symlink found. Aborting cleanup."
exit 1
fi
CURRENT_RELEASE_DIR_NAME=$(basename "$(readlink -f "$CURRENT_LINK_PATH")")
echo "📌 Current release: ${CURRENT_RELEASE_DIR_NAME}"
if [ -d "$RELEASES_PATH" ]; then
cd "$RELEASES_PATH"
# Get a list of inactive release directories, sorted by modification time (newest first).
INACTIVE_RELEASES=$(find . -maxdepth 1 -mindepth 1 -type d \
-not -name "$CURRENT_RELEASE_DIR_NAME" \
-printf '%T@ %f\n' | sort -nr | cut -d' ' -f2-)
if [ -z "$INACTIVE_RELEASES" ]; then
echo "No inactive releases found to clean up."
exit 0
fi
# Count total inactive releases
TOTAL_INACTIVE=$(echo "$INACTIVE_RELEASES" | wc -l | xargs)
echo "📊 Found ${TOTAL_INACTIVE} inactive release(s)"
# Identify releases to delete by skipping the KEEP_COUNT newest ones.
RELEASES_TO_DELETE=$(echo "$INACTIVE_RELEASES" | tail -n +$((KEEP_COUNT + 1)))
if [ -n "$RELEASES_TO_DELETE" ]; then
DELETE_COUNT=$(echo "$RELEASES_TO_DELETE" | wc -l | xargs)
echo "🗑️ The following ${DELETE_COUNT} old release(s) will be deleted:"
# Show releases with their sizes
while IFS= read -r release; do
if [ -d "$release" ]; then
SIZE=$(du -sh "$release" 2>/dev/null | cut -f1)
indent_output echo "- $release (Size: $SIZE)"
fi
done <<< "$RELEASES_TO_DELETE"
# Delete the releases
echo ""
while IFS= read -r release; do
execute_if_not_dry "delete release $release" rm -rf "$release"
done <<< "$RELEASES_TO_DELETE"
if [ "$DRY_RUN" = true ]; then
echo ""
echo "✅ Dry run completed - no releases were deleted"
else
echo "✅ Cleanup completed - deleted ${DELETE_COUNT} release(s)"
fi
else
KEPT_COUNT=$(echo "$INACTIVE_RELEASES" | wc -l | tr -d '\n')
echo "No old releases to delete. Found ${KEPT_COUNT} inactive release(s), which is within the retention count of ${KEEP_COUNT}."
fi
else
echo "No releases directory found"
fi
;;
*)
echo "Error: Unknown command: $COMMAND"
exit 1
;;
esac