187 lines
5.8 KiB
Bash
Executable File
187 lines
5.8 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
set -Eeuo pipefail
|
|
|
|
APP_DIR="${APP_DIR:-/opt/proxpanel}"
|
|
BACKUP_ROOT="${BACKUP_ROOT:-/opt/proxpanel-backups}"
|
|
GREEN_DIR="${GREEN_DIR:-/opt/proxpanel-green}"
|
|
GREEN_PROJECT="${GREEN_PROJECT:-proxpanel_green}"
|
|
GREEN_FRONTEND_PORT="${GREEN_FRONTEND_PORT:-18081}"
|
|
GREEN_BACKEND_PORT="${GREEN_BACKEND_PORT:-18080}"
|
|
NGINX_SITE="${NGINX_SITE:-/etc/nginx/sites-available/proxpanel.conf}"
|
|
|
|
usage() {
|
|
cat <<'EOF'
|
|
Usage:
|
|
sudo bash infra/deploy/rollback-blue-green.sh --backup-dir /opt/proxpanel-backups/<timestamp> [--cutover]
|
|
|
|
Behavior:
|
|
1) Stages backup app/env into /opt/proxpanel-green
|
|
2) Boots green stack on ports 18081/18080
|
|
3) Restores DB dump from selected backup into green postgres
|
|
4) Verifies green API health
|
|
5) If --cutover is provided, switches Nginx upstream to green frontend port
|
|
|
|
Notes:
|
|
- This script requires backup directory format created by deployment steps:
|
|
<backup-dir>/app/
|
|
<backup-dir>/env.production.bak (or .env.production.bak)
|
|
<backup-dir>/db_pre_*.sql
|
|
EOF
|
|
}
|
|
|
|
log() {
|
|
printf '\n[%s] %s\n' "$(date -u +'%Y-%m-%d %H:%M:%S UTC')" "$*"
|
|
}
|
|
|
|
die() {
|
|
printf '\n[ERROR] %s\n' "$*" >&2
|
|
exit 1
|
|
}
|
|
|
|
require_root() {
|
|
[[ "${EUID:-$(id -u)}" -eq 0 ]] || die "Run as root (or with sudo)."
|
|
}
|
|
|
|
parse_args() {
|
|
BACKUP_DIR=""
|
|
CUTOVER="false"
|
|
while [[ $# -gt 0 ]]; do
|
|
case "$1" in
|
|
--backup-dir)
|
|
BACKUP_DIR="${2:-}"
|
|
shift 2
|
|
;;
|
|
--cutover)
|
|
CUTOVER="true"
|
|
shift
|
|
;;
|
|
-h|--help)
|
|
usage
|
|
exit 0
|
|
;;
|
|
*)
|
|
die "Unknown argument: $1"
|
|
;;
|
|
esac
|
|
done
|
|
|
|
[[ -n "$BACKUP_DIR" ]] || die "--backup-dir is required"
|
|
}
|
|
|
|
pick_db_dump() {
|
|
find "$BACKUP_DIR" -maxdepth 1 -type f -name 'db_pre*.sql' | sort | tail -n 1
|
|
}
|
|
|
|
pick_env_backup() {
|
|
if [[ -f "$BACKUP_DIR/env.production.bak" ]]; then
|
|
printf '%s' "$BACKUP_DIR/env.production.bak"
|
|
return
|
|
fi
|
|
if [[ -f "$BACKUP_DIR/.env.production.bak" ]]; then
|
|
printf '%s' "$BACKUP_DIR/.env.production.bak"
|
|
return
|
|
fi
|
|
printf ''
|
|
}
|
|
|
|
wait_for_green_health() {
|
|
local i
|
|
for i in $(seq 1 90); do
|
|
if curl -fsS "http://127.0.0.1:${GREEN_BACKEND_PORT}/api/health" >/dev/null 2>&1; then
|
|
return
|
|
fi
|
|
sleep 2
|
|
done
|
|
die "Green backend health check failed."
|
|
}
|
|
|
|
main() {
|
|
require_root
|
|
parse_args "$@"
|
|
|
|
[[ -d "$BACKUP_DIR/app" ]] || die "Missing backup app directory: $BACKUP_DIR/app"
|
|
local env_backup
|
|
env_backup="$(pick_env_backup)"
|
|
[[ -n "$env_backup" ]] || die "Missing env backup in $BACKUP_DIR (.env.production.bak or env.production.bak)"
|
|
local db_dump
|
|
db_dump="$(pick_db_dump)"
|
|
[[ -n "$db_dump" ]] || die "Missing DB dump in backup dir: $BACKUP_DIR"
|
|
|
|
log "Preparing green staging directory at $GREEN_DIR"
|
|
rm -rf "$GREEN_DIR"
|
|
mkdir -p "$GREEN_DIR"
|
|
rsync -a "$BACKUP_DIR/app/" "$GREEN_DIR/"
|
|
cp "$env_backup" "$GREEN_DIR/.env.production"
|
|
|
|
# Override ports for green stack and keep CORS aligned to production hostname.
|
|
if grep -q '^FRONTEND_PORT=' "$GREEN_DIR/.env.production"; then
|
|
sed -i "s/^FRONTEND_PORT=.*/FRONTEND_PORT=${GREEN_FRONTEND_PORT}/" "$GREEN_DIR/.env.production"
|
|
else
|
|
echo "FRONTEND_PORT=${GREEN_FRONTEND_PORT}" >> "$GREEN_DIR/.env.production"
|
|
fi
|
|
if grep -q '^BACKEND_PORT=' "$GREEN_DIR/.env.production"; then
|
|
sed -i "s/^BACKEND_PORT=.*/BACKEND_PORT=${GREEN_BACKEND_PORT}/" "$GREEN_DIR/.env.production"
|
|
else
|
|
echo "BACKEND_PORT=${GREEN_BACKEND_PORT}" >> "$GREEN_DIR/.env.production"
|
|
fi
|
|
|
|
# Remove fixed container_name lines for project-isolated blue/green operation.
|
|
local compose_green
|
|
compose_green="$GREEN_DIR/infra/deploy/docker-compose.green.yml"
|
|
awk '!/^[[:space:]]*container_name:/' "$GREEN_DIR/infra/deploy/docker-compose.production.yml" > "$compose_green"
|
|
|
|
log "Stopping previous green project (if any)"
|
|
(
|
|
cd "$GREEN_DIR"
|
|
docker compose -p "$GREEN_PROJECT" --env-file .env.production -f "$compose_green" down -v || true
|
|
)
|
|
|
|
log "Starting green stack on ${GREEN_FRONTEND_PORT}/${GREEN_BACKEND_PORT}"
|
|
(
|
|
cd "$GREEN_DIR"
|
|
docker compose -p "$GREEN_PROJECT" --env-file .env.production -f "$compose_green" up -d --build
|
|
)
|
|
|
|
log "Restoring backup DB into green postgres"
|
|
local green_postgres_id green_pg_user green_pg_db
|
|
green_postgres_id="$(
|
|
cd "$GREEN_DIR" && docker compose -p "$GREEN_PROJECT" --env-file .env.production -f "$compose_green" ps -q postgres
|
|
)"
|
|
[[ -n "$green_postgres_id" ]] || die "Could not locate green postgres container."
|
|
|
|
# shellcheck disable=SC1090
|
|
source "$GREEN_DIR/.env.production"
|
|
green_pg_user="${POSTGRES_USER:-proxpanel}"
|
|
green_pg_db="${POSTGRES_DB:-proxpanel}"
|
|
|
|
docker exec "$green_postgres_id" psql -U "$green_pg_user" -d "$green_pg_db" -v ON_ERROR_STOP=1 \
|
|
-c 'DROP SCHEMA public CASCADE; CREATE SCHEMA public;'
|
|
|
|
cat "$db_dump" | docker exec -i "$green_postgres_id" psql -U "$green_pg_user" -d "$green_pg_db" -v ON_ERROR_STOP=1 >/dev/null
|
|
|
|
log "Validating green health"
|
|
wait_for_green_health
|
|
curl -fsS "http://127.0.0.1:${GREEN_BACKEND_PORT}/api/health" >/dev/null
|
|
|
|
if [[ "$CUTOVER" == "true" ]]; then
|
|
log "Cutting over Nginx upstream to green frontend port ${GREEN_FRONTEND_PORT}"
|
|
[[ -f "$NGINX_SITE" ]] || die "Missing nginx site config: $NGINX_SITE"
|
|
cp "$NGINX_SITE" "${NGINX_SITE}.pre-green.$(date -u +%Y%m%d-%H%M%S).bak"
|
|
sed -i -E "s#proxy_pass http://127\\.0\\.0\\.1:[0-9]+;#proxy_pass http://127.0.0.1:${GREEN_FRONTEND_PORT};#g" "$NGINX_SITE"
|
|
nginx -t
|
|
systemctl reload nginx
|
|
log "Nginx cutover complete."
|
|
fi
|
|
|
|
log "Blue/green rollback prep complete."
|
|
printf '\nGreen frontend: http://127.0.0.1:%s\n' "$GREEN_FRONTEND_PORT"
|
|
printf 'Green backend : http://127.0.0.1:%s/api/health\n' "$GREEN_BACKEND_PORT"
|
|
if [[ "$CUTOVER" == "true" ]]; then
|
|
printf 'Traffic switched to green.\n'
|
|
else
|
|
printf 'Traffic still on blue. Re-run with --cutover when ready.\n'
|
|
fi
|
|
}
|
|
|
|
main "$@"
|