#!/usr/bin/env bash set -Eeuo pipefail # 在 shunfeng 单个节点上执行镜像快照、服务更新、健康检查或回滚。 # 本脚本由 apply-update.sh 在主机本地调用,并临时复制到从机调用。 ACTION="${1:-}" DEPLOY_ROLE="${2:-}" RUN_ID="${3:-}" IMAGE_TAG="${4:-}" shift $(( $# >= 4 ? 4 : $# )) REQUESTED_SERVICES=("$@") DEPLOY_HOME="${DEPLOY_HOME:-/home/emp-shunfeng}" RUNTIME_DIR="${RUNTIME_DIR:-$DEPLOY_HOME/runtime}" RELEASE_ENV_FILE="${RELEASE_ENV_FILE:-$RUNTIME_DIR/release.env}" log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] [$DEPLOY_ROLE] $*" } die() { echo "ERROR: [$DEPLOY_ROLE] $*" >&2 exit 1 } need_cmd() { command -v "$1" >/dev/null 2>&1 || die "missing command: $1" } normalize_service() { case "$1" in admin|emp-admin) echo "admin" ;; gateway|emp-gateway) echo "gateway" ;; auth|emp-auth) echo "auth" ;; monitor|emp-monitor) echo "monitor" ;; data|emp-data) echo "data" ;; *) die "unsupported service: $1" ;; esac } service_allowed_on_role() { local service="$1" if [[ "$DEPLOY_ROLE" == "master" ]]; then return 0 fi [[ "$DEPLOY_ROLE" == "sub" && "$service" != "data" ]] } image_name() { echo "emp-$1" } container_name() { echo "emp-$1" } if [[ -f "$RELEASE_ENV_FILE" ]]; then set -a # shellcheck disable=SC1090 . "$RELEASE_ENV_FILE" set +a fi BACKEND_PROJECT_NAME="${BACKEND_PROJECT_NAME:-emp_server}" BACKEND_COMPOSE_FILE="${BACKEND_COMPOSE_FILE:-$RUNTIME_DIR/docker-compose.yml}" BACKEND_ENV_FILE="${BACKEND_ENV_FILE:-$RUNTIME_DIR/.env}" ADMIN_HOST_PORT="${ADMIN_HOST_PORT:-4080}" ADMIN_CONTAINER_PORT="${ADMIN_CONTAINER_PORT:-80}" EMP_ADMIN_LOG_DIR="${EMP_ADMIN_LOG_DIR:-/data/emp/logs/emp-admin}" DOCKER_LOG_MAX_SIZE="${DOCKER_LOG_MAX_SIZE:-200m}" DOCKER_LOG_MAX_FILE="${DOCKER_LOG_MAX_FILE:-5}" HEALTH_RETRIES="${HEALTH_RETRIES:-30}" HEALTH_INTERVAL_SECONDS="${HEALTH_INTERVAL_SECONDS:-2}" NACOS_CONTAINER_NAME="${NACOS_CONTAINER_NAME:-emp-nacos}" REQUIRE_EXISTING_CONTAINERS="${REQUIRE_EXISTING_CONTAINERS:-1}" IMAGE_TAR="${IMAGE_TAR:-}" [[ "$ACTION" =~ ^(snapshot|restore-tags|deploy|apply|rollback)$ ]] || die "invalid action: $ACTION" [[ "$DEPLOY_ROLE" == "master" || "$DEPLOY_ROLE" == "sub" ]] || die "invalid role: $DEPLOY_ROLE" [[ "$RUN_ID" =~ ^[a-zA-Z0-9_.-]+$ ]] || die "invalid run id: $RUN_ID" [[ "$IMAGE_TAG" =~ ^[a-zA-Z0-9_.-]+$ ]] || die "invalid image tag: $IMAGE_TAG" [[ "${#REQUESTED_SERVICES[@]}" -gt 0 ]] || die "no services specified" ACTIVE_SERVICES=() declare -A SEEN_SERVICES=() for raw_service in "${REQUESTED_SERVICES[@]}"; do service="$(normalize_service "$raw_service")" if service_allowed_on_role "$service" && [[ -z "${SEEN_SERVICES[$service]:-}" ]]; then ACTIVE_SERVICES+=("$service") SEEN_SERVICES[$service]=1 fi done [[ "${#ACTIVE_SERVICES[@]}" -gt 0 ]] || die "no services apply to role $DEPLOY_ROLE" need_cmd docker if docker compose version >/dev/null 2>&1; then COMPOSE_CMD=(docker compose) elif command -v docker-compose >/dev/null 2>&1; then COMPOSE_CMD=(docker-compose) else die "missing docker compose" fi snapshot_images() { local service container image image_id rollback_image for service in "${ACTIVE_SERVICES[@]}"; do container="$(container_name "$service")" image="$(image_name "$service")" if ! docker container inspect "$container" >/dev/null 2>&1; then if [[ "$REQUIRE_EXISTING_CONTAINERS" == "1" ]]; then die "existing container is required for rollback: $container" fi log "No existing container to snapshot: $container" continue fi image_id="$(docker container inspect --format '{{.Image}}' "$container")" rollback_image="$image:rollback-$RUN_ID" docker image tag "$image_id" "$rollback_image" log "Rollback image saved: $rollback_image" done } restore_latest_tags() { local service image rollback_image for service in "${ACTIVE_SERVICES[@]}"; do image="$(image_name "$service")" rollback_image="$image:rollback-$RUN_ID" if docker image inspect "$rollback_image" >/dev/null 2>&1; then docker image tag "$rollback_image" "$image:latest" log "Latest tag restored: $image:latest" fi done } ensure_nacos() { local needs_backend=0 service for service in "${ACTIVE_SERVICES[@]}"; do [[ "$service" == "admin" ]] || needs_backend=1 done [[ "$needs_backend" == "1" ]] || return 0 if [[ "$(docker container inspect --format '{{.State.Running}}' "$NACOS_CONTAINER_NAME" 2>/dev/null || true)" != "true" ]]; then die "Nacos container is not running: $NACOS_CONTAINER_NAME" fi } run_admin() { local tag="$1" local image="emp-admin:$tag" docker image inspect "$image" >/dev/null 2>&1 || die "image not found: $image" docker stop emp-admin >/dev/null 2>&1 || true docker rm emp-admin >/dev/null 2>&1 || true docker run -d \ --name emp-admin \ -p "$ADMIN_HOST_PORT:$ADMIN_CONTAINER_PORT" \ -v "$EMP_ADMIN_LOG_DIR:/var/log/nginx" \ --log-opt "max-size=$DOCKER_LOG_MAX_SIZE" \ --log-opt "max-file=$DOCKER_LOG_MAX_FILE" \ --restart unless-stopped \ "$image" >/dev/null log "Admin started: $image" } run_backend_services() { local tag="$1" local compose_services=() local service image for service in "${ACTIVE_SERVICES[@]}"; do [[ "$service" == "admin" ]] && continue image="$(image_name "$service"):$tag" docker image inspect "$image" >/dev/null 2>&1 || die "image not found: $image" compose_services+=("emp-$service") done [[ "${#compose_services[@]}" -gt 0 ]] || return 0 [[ -f "$BACKEND_COMPOSE_FILE" ]] || die "compose file not found: $BACKEND_COMPOSE_FILE" [[ -f "$BACKEND_ENV_FILE" ]] || die "env file not found: $BACKEND_ENV_FILE" IMAGE_TAG="$tag" "${COMPOSE_CMD[@]}" \ --env-file "$BACKEND_ENV_FILE" \ -f "$BACKEND_COMPOSE_FILE" \ -p "$BACKEND_PROJECT_NAME" \ up -d --no-deps --no-build --force-recreate "${compose_services[@]}" } http_ok() { local url="$1" if command -v curl >/dev/null 2>&1; then curl -fsS --connect-timeout 2 --max-time 5 "$url" >/dev/null elif command -v wget >/dev/null 2>&1; then wget -q -T 5 -O /dev/null "$url" else die "curl or wget is required for health checks" fi } health_url() { case "$1" in admin) echo "http://127.0.0.1:$ADMIN_HOST_PORT/" ;; gateway) echo "http://127.0.0.1:9000/api/health" ;; auth) echo "http://127.0.0.1:9001/auth/health" ;; monitor) echo "http://127.0.0.1:9002/health" ;; data) echo "http://127.0.0.1:9003/health" ;; esac } wait_for_service() { local service="$1" local container url attempt container="$(container_name "$service")" url="$(health_url "$service")" for ((attempt = 1; attempt <= HEALTH_RETRIES; attempt += 1)); do if [[ "$(docker container inspect --format '{{.State.Running}}' "$container" 2>/dev/null || true)" == "true" ]] \ && http_ok "$url"; then log "Health check passed: $service ($url)" return 0 fi sleep "$HEALTH_INTERVAL_SECONDS" done log "Health check failed: $service ($url)" docker logs --tail=80 "$container" 2>/dev/null || true return 1 } health_check() { local service for service in "${ACTIVE_SERVICES[@]}"; do wait_for_service "$service" || return 1 done } deploy_services() { local tag="$1" local service ensure_nacos for service in "${ACTIVE_SERVICES[@]}"; do if [[ "$service" == "admin" ]]; then run_admin "$tag" || return 1 fi done run_backend_services "$tag" || return 1 health_check } rollback_services() { log "Rolling back services: ${ACTIVE_SERVICES[*]}" restore_latest_tags deploy_services latest } case "$ACTION" in snapshot) snapshot_images ;; restore-tags) restore_latest_tags ;; deploy) deploy_services "$IMAGE_TAG" ;; rollback) rollback_services ;; apply) [[ -n "$IMAGE_TAR" && -f "$IMAGE_TAR" ]] || die "image tar not found: $IMAGE_TAR" snapshot_images log "Load images: $IMAGE_TAR" docker load -i "$IMAGE_TAR" if ! deploy_services "$IMAGE_TAG"; then log "Deployment failed, start rollback" rollback_services || true exit 1 fi if [[ "$DEPLOY_ROLE" == "sub" ]]; then docker stop emp-data >/dev/null 2>&1 || true docker rm emp-data >/dev/null 2>&1 || true fi ;; esac