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.
This commit is contained in:
parent
a58a0e642a
commit
30ce126a07
19 changed files with 1166 additions and 591 deletions
|
|
@ -53,6 +53,8 @@ services:
|
|||
done
|
||||
env_file:
|
||||
- .env
|
||||
labels:
|
||||
- "deployment.core=true"
|
||||
loba:
|
||||
image: haproxy:3.1
|
||||
stop_signal: SIGTERM
|
||||
|
|
@ -64,11 +66,14 @@ services:
|
|||
volumes:
|
||||
- ./haproxy.cfg:/usr/local/etc/haproxy/haproxy.cfg
|
||||
- ./certs:/certs
|
||||
labels:
|
||||
- "deployment.core=true"
|
||||
feedback:
|
||||
restart: always
|
||||
image: getfider/fider:stable
|
||||
labels:
|
||||
- "enable_gatekeeper=true"
|
||||
- "deployment.core=true"
|
||||
env_file:
|
||||
- .env
|
||||
# cadvisor:
|
||||
|
|
@ -91,6 +96,8 @@ services:
|
|||
timeout: 5s
|
||||
retries: 5
|
||||
start_period: 10s
|
||||
labels:
|
||||
- "deployment.core=true"
|
||||
dockergen-health:
|
||||
image: nginxproxy/docker-gen:latest
|
||||
command: -wait 15s -watch /gatus/config.template.yaml /gatus/config.yaml
|
||||
|
|
@ -98,6 +105,8 @@ services:
|
|||
volumes:
|
||||
- /var/run/docker.sock:/tmp/docker.sock:ro
|
||||
- ./gatus:/gatus
|
||||
labels:
|
||||
- "deployment.core=true"
|
||||
dockergen-gatekeeper:
|
||||
image: nginxproxy/docker-gen:latest
|
||||
command: -wait 15s -watch /gatekeeper/gatekeepers.template.yml /gatekeeper/gatekeepers.yml -notify-sighup pkmntrade-club-gatekeeper-manager-1
|
||||
|
|
@ -105,6 +114,8 @@ services:
|
|||
volumes:
|
||||
- /var/run/docker.sock:/tmp/docker.sock:ro
|
||||
- ./:/gatekeeper
|
||||
labels:
|
||||
- "deployment.core=true"
|
||||
gatekeeper-manager:
|
||||
image: docker:latest
|
||||
restart: always
|
||||
|
|
@ -115,6 +126,8 @@ services:
|
|||
environment:
|
||||
- REFRESH_INTERVAL=60
|
||||
entrypoint: ["/bin/sh", "-c"]
|
||||
labels:
|
||||
- "deployment.core=true"
|
||||
command:
|
||||
- |
|
||||
set -eu -o pipefail
|
||||
|
|
@ -239,7 +252,7 @@ services:
|
|||
echo "$(date +'%Y-%m-%d %H:%M:%S') [INFO]: Periodic healthcheck and refresh triggered."
|
||||
|
||||
if [ ! -f "$$COMPOSE_FILE_PATH" ]; then
|
||||
echo "$(date +'%Y-%m-%d %H:%M:%S') [ERROR]: Gatekeepers.yml has not been generated after $$REFRESH_INTERVAL seconds. Please check dockergen-gatekeeper is running correctly. Exiting."
|
||||
echo "$(date +'%Y-%m-%d %H:%M:%S') [ERROR]: gatekeepers.yml has not been generated after $$REFRESH_INTERVAL seconds. Please check dockergen-gatekeeper is running correctly. Exiting."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
|
|
@ -254,9 +267,21 @@ services:
|
|||
restart: always
|
||||
labels:
|
||||
- "enable_gatekeeper=true"
|
||||
- "deployment.core=true"
|
||||
# healthcheck:
|
||||
# test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
|
||||
# interval: 10s
|
||||
# timeout: 5s
|
||||
# retries: 5
|
||||
# start_period: 10s
|
||||
env_file:
|
||||
- .env
|
||||
environment:
|
||||
- GATUS_DELAY_START_SECONDS=30
|
||||
volumes:
|
||||
- ./gatus:/config
|
||||
- ./gatus:/config
|
||||
|
||||
networks:
|
||||
default:
|
||||
name: pkmntrade-club_network
|
||||
external: true
|
||||
|
|
@ -1,32 +0,0 @@
|
|||
x-common: &common
|
||||
image: badbl0cks/pkmntrade-club:staging
|
||||
restart: always
|
||||
env_file:
|
||||
- .env
|
||||
services:
|
||||
web-staging:
|
||||
<<: *common
|
||||
environment:
|
||||
- DEBUG=False
|
||||
- DISABLE_SIGNUPS=True
|
||||
- PUBLIC_HOST=staging.pkmntrade.club
|
||||
- ALLOWED_HOSTS=staging.pkmntrade.club,127.0.0.1
|
||||
labels:
|
||||
- "enable_gatekeeper=true"
|
||||
deploy:
|
||||
mode: replicated
|
||||
replicas: 2
|
||||
# healthcheck:
|
||||
# test: ["CMD", "curl", "-f", "http://127.0.0.1:8000/health"]
|
||||
# interval: 30s
|
||||
# timeout: 10s
|
||||
# retries: 3
|
||||
# start_period: 30s
|
||||
celery-staging:
|
||||
<<: *common
|
||||
environment:
|
||||
- DEBUG=False
|
||||
- DISABLE_SIGNUPS=True
|
||||
- PUBLIC_HOST=staging.pkmntrade.club
|
||||
- ALLOWED_HOSTS=staging.pkmntrade.club,127.0.0.1
|
||||
command: ["celery", "-A", "pkmntrade_club.django_project", "worker", "-l", "INFO", "-B", "-E"]
|
||||
|
|
@ -1,4 +1,7 @@
|
|||
x-common: &common
|
||||
image: badbl0cks/pkmntrade-club:${IMAGE_TAG:-stable}
|
||||
#image: ghcr.io/xe/x/httpdebug
|
||||
#entrypoint: ["/ko-app/httpdebug", "--bind", ":8000"]
|
||||
restart: always
|
||||
env_file:
|
||||
- .env
|
||||
|
|
@ -6,31 +9,42 @@ x-common: &common
|
|||
services:
|
||||
web:
|
||||
<<: *common
|
||||
image: ghcr.io/xe/x/httpdebug
|
||||
entrypoint: ["/ko-app/httpdebug", "--bind", ":8000"]
|
||||
#image: badbl0cks/pkmntrade-club:stable
|
||||
environment:
|
||||
- DEBUG=False
|
||||
- DISABLE_SIGNUPS=True
|
||||
- PUBLIC_HOST=${DOMAIN_NAME}
|
||||
- ALLOWED_HOSTS=${DOMAIN_NAME},127.0.0.1
|
||||
- DEPLOYMENT_COLOR=${DEPLOYMENT_COLOR:-blue}
|
||||
labels:
|
||||
- "enable_gatekeeper=true"
|
||||
- "deployment.color=${DEPLOYMENT_COLOR:-blue}"
|
||||
- "deployment.image_tag=${IMAGE_TAG:-stable}"
|
||||
deploy:
|
||||
mode: replicated
|
||||
replicas: ${REPLICA_COUNT}
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://127.0.0.1:8000/health/"]
|
||||
interval: 5s
|
||||
timeout: 3s
|
||||
retries: 2
|
||||
start_period: 60s
|
||||
stop_grace_period: 200s # 20s buffer + 180s workers-kill-timeout
|
||||
|
||||
celery:
|
||||
<<: *common
|
||||
environment:
|
||||
- DEBUG=False
|
||||
- DISABLE_SIGNUPS=True
|
||||
- PUBLIC_HOST=pkmntrade.club
|
||||
- ALLOWED_HOSTS=pkmntrade.club,127.0.0.1
|
||||
- DEPLOYMENT_COLOR=${DEPLOYMENT_COLOR:-blue}
|
||||
labels:
|
||||
- "enable_gatekeeper=true"
|
||||
deploy:
|
||||
mode: replicated
|
||||
replicas: 4
|
||||
# healthcheck:
|
||||
# test: ["CMD", "curl", "-f", "http://127.0.0.1:8000/health"]
|
||||
# interval: 30s
|
||||
# timeout: 10s
|
||||
# retries: 3
|
||||
# start_period: 30s
|
||||
# celery:
|
||||
# <<: *common
|
||||
# image: badbl0cks/pkmntrade-club:stable
|
||||
# environment:
|
||||
# - DEBUG=False
|
||||
# - DISABLE_SIGNUPS=True
|
||||
# - PUBLIC_HOST=pkmntrade.club
|
||||
# - ALLOWED_HOSTS=pkmntrade.club,127.0.0.1
|
||||
# command: ["celery", "-A", "pkmntrade_club.django_project", "worker", "-l", "INFO", "-B", "-E"]
|
||||
- "deployment.color=${DEPLOYMENT_COLOR:-blue}"
|
||||
- "deployment.image_tag=${IMAGE_TAG:-stable}"
|
||||
command: ["celery", "-A", "pkmntrade_club.django_project", "worker", "-l", "INFO", "-B", "-E"]
|
||||
stop_grace_period: 200s # match our longest stop_grace_period (currently web service is 200s)
|
||||
|
||||
networks:
|
||||
default:
|
||||
name: pkmntrade-club_network
|
||||
external: true
|
||||
|
|
@ -1,23 +1,52 @@
|
|||
services:
|
||||
{{ $all_containers := whereLabelValueMatches . "enable_gatekeeper" "true" }}
|
||||
{{ $all_containers = sortObjectsByKeysAsc $all_containers "Name" }}
|
||||
|
||||
# During deployment, both blue and green containers might exist
|
||||
# So we generate gatekeepers for ALL containers with deployment.color label
|
||||
{{ $color_containers := whereLabelExists $all_containers "deployment.color" }}
|
||||
{{ $color_containers = sortObjectsByKeysAsc $color_containers "Name" }}
|
||||
|
||||
{{ range $container := $all_containers }}
|
||||
{{ range $container := $color_containers }}
|
||||
{{ $serviceLabel := index $container.Labels "com.docker.compose.service" }}
|
||||
{{ $containerNumber := index $container.Labels "com.docker.compose.container-number" }}
|
||||
{{ $deploymentColor := index $container.Labels "deployment.color" }}
|
||||
{{ $port := "" }}
|
||||
{{ if eq $serviceLabel "web" }}
|
||||
{{ $port = ":8000" }}
|
||||
{{ end }}
|
||||
{{ if eq $serviceLabel "web-staging" }}
|
||||
{{ $port = ":8000" }}
|
||||
gatekeeper-{{ $serviceLabel }}-{{ $deploymentColor }}-{{ $containerNumber }}:
|
||||
image: ghcr.io/techarohq/anubis:latest
|
||||
container_name: pkmntrade-club-gatekeeper-{{ $serviceLabel }}-{{ $deploymentColor }}-{{ $containerNumber }}
|
||||
env_file:
|
||||
- .env
|
||||
environment:
|
||||
- TARGET=http://{{ $container.Name }}{{ $port }}
|
||||
- DEPLOYMENT_COLOR={{ $deploymentColor }}
|
||||
- TARGET_HOST=${DOMAIN_NAME}
|
||||
labels:
|
||||
- gatekeeper=true
|
||||
- deployment.color={{ $deploymentColor }}
|
||||
networks:
|
||||
default:
|
||||
aliases:
|
||||
- pkmntrade-club-gatekeeper-{{ $serviceLabel }}
|
||||
- gatekeeper-{{ $serviceLabel }}
|
||||
{{ end }}
|
||||
|
||||
# Always include non-color-specific services
|
||||
{{ $static_containers := whereLabelValueMatches . "enable_gatekeeper" "true" }}
|
||||
{{ $static_containers = whereLabelDoesNotExist $static_containers "deployment.color" }}
|
||||
{{ range $container := $static_containers }}
|
||||
{{ $serviceLabel := index $container.Labels "com.docker.compose.service" }}
|
||||
{{ $containerNumber := index $container.Labels "com.docker.compose.container-number" }}
|
||||
{{ $port := "" }}
|
||||
{{ if eq $serviceLabel "feedback" }}
|
||||
{{ $port = ":3000" }}
|
||||
{{ end }}
|
||||
{{ if eq $serviceLabel "health" }}
|
||||
{{ $port = ":8080" }}
|
||||
{{ end }}
|
||||
{{ if or (eq $serviceLabel "feedback") (eq $serviceLabel "health") }}
|
||||
gatekeeper-{{ $serviceLabel }}-{{ $containerNumber }}:
|
||||
image: ghcr.io/techarohq/anubis:latest
|
||||
container_name: pkmntrade-club-gatekeeper-{{ $serviceLabel }}-{{ $containerNumber }}
|
||||
|
|
@ -25,12 +54,6 @@ services:
|
|||
- .env
|
||||
environment:
|
||||
- TARGET=http://{{ $container.Name }}{{ $port }}
|
||||
{{ if eq $serviceLabel "web" }}
|
||||
- TARGET_HOST=pkmntrade.club # pass this host to django, which checks it with ALLOWED_HOSTS
|
||||
{{ end }}
|
||||
{{ if eq $serviceLabel "web-staging" }}
|
||||
- TARGET_HOST=staging.pkmntrade.club # pass this host to django, which checks it with ALLOWED_HOSTS
|
||||
{{ end }}
|
||||
labels:
|
||||
- gatekeeper=true
|
||||
networks:
|
||||
|
|
@ -39,7 +62,9 @@ services:
|
|||
- pkmntrade-club-gatekeeper-{{ $serviceLabel }}
|
||||
- gatekeeper-{{ $serviceLabel }}
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
|
||||
networks:
|
||||
default:
|
||||
name: pkmntrade-club_default
|
||||
name: pkmntrade-club_network
|
||||
external: true
|
||||
|
|
|
|||
|
|
@ -92,20 +92,15 @@ endpoints:
|
|||
- type: email
|
||||
{{ $all_containers := . }}
|
||||
{{ $web_containers := list }}
|
||||
{{ $web_staging_containers := list }}
|
||||
|
||||
{{ range $container := $all_containers }}
|
||||
{{ $serviceLabel := index $container.Labels "com.docker.compose.service" }}
|
||||
{{ if eq $serviceLabel "web" }}
|
||||
{{ $web_containers = append $web_containers $container }}
|
||||
{{ end }}
|
||||
{{ if eq $serviceLabel "web-staging" }}
|
||||
{{ $web_staging_containers = append $web_staging_containers $container }}
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
|
||||
{{ $web_containers = sortObjectsByKeysAsc $web_containers "Name" }}
|
||||
{{ $web_staging_containers = sortObjectsByKeysAsc $web_staging_containers "Name" }}
|
||||
|
||||
{{ range $container := $web_containers }}
|
||||
{{ $containerNumber := index $container.Labels "com.docker.compose.container-number" }}
|
||||
|
|
@ -113,7 +108,7 @@ endpoints:
|
|||
group: Main
|
||||
url: "http://{{ $container.Name }}:8000/health/"
|
||||
headers:
|
||||
Host: "pkmntrade.club"
|
||||
Host: "${DOMAIN_NAME}"
|
||||
interval: 60s
|
||||
conditions:
|
||||
- "[STATUS] == 200"
|
||||
|
|
@ -122,21 +117,6 @@ endpoints:
|
|||
- type: email
|
||||
{{ end }}
|
||||
|
||||
{{ range $container := $web_staging_containers }}
|
||||
{{ $containerNumber := index $container.Labels "com.docker.compose.container-number" }}
|
||||
- name: "Web Worker {{ $containerNumber }}"
|
||||
group: Staging
|
||||
url: "http://{{ $container.Name }}:8000/health/"
|
||||
headers:
|
||||
Host: "staging.pkmntrade.club"
|
||||
interval: 60s
|
||||
conditions:
|
||||
- "[STATUS] == 200"
|
||||
# - "[BODY] == OK/HEALTHY"
|
||||
alerts:
|
||||
- type: email
|
||||
{{ end }}
|
||||
|
||||
alerting:
|
||||
email:
|
||||
from: "${GATUS_SMTP_FROM}"
|
||||
|
|
|
|||
|
|
@ -25,32 +25,27 @@ frontend haproxy_entrypoint
|
|||
bind :443 ssl crt /certs/crt.pem verify required ca-file /certs/ca.pem
|
||||
use_backend %[req.hdr(host),lower,word(1,:)] # strip out port from host
|
||||
|
||||
frontend checks
|
||||
frontend healthchecks
|
||||
bind :80
|
||||
default_backend basic_check
|
||||
default_backend basic_loba_check
|
||||
|
||||
backend basic_check
|
||||
backend basic_loba_check
|
||||
http-request return status 200 content-type "text/plain" lf-string "OK/HEALTHY"
|
||||
|
||||
backend pkmntrade.club
|
||||
backend "${DOMAIN_NAME}"
|
||||
balance leastconn
|
||||
http-request set-header Host pkmntrade.club
|
||||
server-template gatekeeper-web- 4 gatekeeper-web:8000 check resolvers docker_resolver init-addr libc,none
|
||||
http-request set-header Host "${DOMAIN_NAME}"
|
||||
server-template gatekeeper-web- "${REPLICA_COUNT}" gatekeeper-web:8000 check resolvers docker_resolver init-addr none
|
||||
|
||||
backend staging.pkmntrade.club
|
||||
backend "feedback.${BASE_DOMAIN_NAME}"
|
||||
balance leastconn
|
||||
http-request set-header Host staging.pkmntrade.club
|
||||
server-template gatekeeper-web-staging- 4 gatekeeper-web-staging:8000 check resolvers docker_resolver init-addr libc,none
|
||||
http-request set-header Host feedback."${BASE_DOMAIN_NAME}"
|
||||
server-template gatekeeper-feedback- 1 gatekeeper-feedback:8000 check resolvers docker_resolver init-addr none
|
||||
|
||||
backend feedback.pkmntrade.club
|
||||
backend "health.${BASE_DOMAIN_NAME}"
|
||||
balance leastconn
|
||||
http-request set-header Host feedback.pkmntrade.club
|
||||
server-template gatekeeper-feedback- 4 gatekeeper-feedback:8000 check resolvers docker_resolver init-addr libc,none
|
||||
|
||||
backend health.pkmntrade.club
|
||||
balance leastconn
|
||||
http-request set-header Host health.pkmntrade.club
|
||||
server-template gatekeeper-health- 4 gatekeeper-health:8000 check resolvers docker_resolver init-addr libc,none
|
||||
http-request set-header Host health."${BASE_DOMAIN_NAME}"
|
||||
server-template gatekeeper-health- 1 gatekeeper-health:8000 check resolvers docker_resolver init-addr none
|
||||
|
||||
#EOF - trailing newline required
|
||||
|
||||
|
|
|
|||
382
server/scripts/common-lib.sh
Executable file
382
server/scripts/common-lib.sh
Executable file
|
|
@ -0,0 +1,382 @@
|
|||
#!/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 " " "$@"
|
||||
}
|
||||
377
server/scripts/manage.sh
Executable file
377
server/scripts/manage.sh
Executable file
|
|
@ -0,0 +1,377 @@
|
|||
#!/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
|
||||
Loading…
Add table
Add a link
Reference in a new issue