#!/usr/bin/env bash set -Eeuo pipefail # shunfeng 主从增量发布编排器。 # 只处理 admin / gateway / auth / monitor / data;不处理 PDF、WS、中间件。 SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" cd "$SCRIPT_DIR" DEPLOY_ENV="${DEPLOY_ENV:-emp-shunfeng}" DEPLOY_HOME="${DEPLOY_HOME:-/home/emp-shunfeng}" RUNTIME_DIR="${RUNTIME_DIR:-$DEPLOY_HOME/runtime}" RELEASE_ENV_FILE="${RELEASE_ENV_FILE:-$RUNTIME_DIR/release.env}" IMAGE_TAR="${IMAGE_TAR:-images.tar}" SERVICE_FILE="${SERVICE_FILE:-services.txt}" MANIFEST_FILE="${MANIFEST_FILE:-manifest.txt}" NODE_HELPER="$SCRIPT_DIR/apply-node-update.sh" log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" } die() { echo "ERROR: $*" >&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 } manifest_value() { local key="$1" sed -n "s/^${key}=//p" "$MANIFEST_FILE" | head -1 } contains_service() { local target="$1" shift local service for service in "$@"; do [[ "$service" == "$target" ]] && return 0 done return 1 } if [[ -f "$RELEASE_ENV_FILE" ]]; then set -a # shellcheck disable=SC1090 . "$RELEASE_ENV_FILE" set +a fi PRIMARY_HOST="${PRIMARY_HOST:-192.168.0.3}" REMOTE_HOST="${REMOTE_HOST:-192.168.0.4}" REMOTE_USER="${REMOTE_USER:-root}" SSH_PORT="${SSH_PORT:-22}" REMOTE_DEPLOY_HOME="${REMOTE_DEPLOY_HOME:-/home/emp-shunfeng}" REMOTE_RUNTIME_DIR="${REMOTE_RUNTIME_DIR:-$REMOTE_DEPLOY_HOME/runtime}" SKIP_PRIMARY_HOST_CHECK="${SKIP_PRIMARY_HOST_CHECK:-0}" SKIP_REMOTE_SYNC="${SKIP_REMOTE_SYNC:-0}" BACKEND_COMPOSE_FILE="${BACKEND_COMPOSE_FILE:-$RUNTIME_DIR/docker-compose.yml}" BACKEND_ENV_FILE="${BACKEND_ENV_FILE:-$RUNTIME_DIR/.env}" SSH_OPTS=( -o StrictHostKeyChecking=accept-new -o ServerAliveInterval=15 -o ServerAliveCountMax=3 ) ssh_cmd() { if [[ -n "${DEPLOY_PASSWORD:-}" ]]; then need_cmd sshpass SSHPASS="$DEPLOY_PASSWORD" sshpass -e ssh -p "$SSH_PORT" "${SSH_OPTS[@]}" "$REMOTE_USER@$REMOTE_HOST" "$@" else ssh -p "$SSH_PORT" "${SSH_OPTS[@]}" "$REMOTE_USER@$REMOTE_HOST" "$@" fi } scp_file() { local source_file="$1" local target_file="$2" if [[ -n "${DEPLOY_PASSWORD:-}" ]]; then need_cmd sshpass SSHPASS="$DEPLOY_PASSWORD" sshpass -e scp -P "$SSH_PORT" "${SSH_OPTS[@]}" "$source_file" "$REMOTE_USER@$REMOTE_HOST:$target_file" else scp -P "$SSH_PORT" "${SSH_OPTS[@]}" "$source_file" "$REMOTE_USER@$REMOTE_HOST:$target_file" fi } ensure_primary_host() { [[ "$SKIP_PRIMARY_HOST_CHECK" == "1" ]] && return 0 local ips ip ips="$(hostname -I 2>/dev/null || true)" [[ -n "$ips" ]] || return 0 for ip in $ips; do [[ "$ip" == "$PRIMARY_HOST" ]] && return 0 done die "current host IP does not include $PRIMARY_HOST. Current IP: $ips" } node_action() { local action="$1" local role="$2" shift 2 DEPLOY_HOME="$DEPLOY_HOME" \ RUNTIME_DIR="$RUNTIME_DIR" \ bash "$NODE_HELPER" "$action" "$role" "$RUN_ID" "$IMAGE_TAG" "$@" } remote_action() { local action="$1" local image_tar="$2" shift 2 local service_args command_text printf -v service_args '%q ' "$@" command_text="DEPLOY_HOME=$(printf '%q' "$REMOTE_DEPLOY_HOME") RUNTIME_DIR=$(printf '%q' "$REMOTE_RUNTIME_DIR")" if [[ -n "$image_tar" ]]; then command_text+=" IMAGE_TAR=$(printf '%q' "$image_tar")" fi command_text+=" bash $(printf '%q' "$REMOTE_HELPER") $(printf '%q' "$action") sub $(printf '%q' "$RUN_ID") $(printf '%q' "$IMAGE_TAG") $service_args" ssh_cmd "bash -lc $(printf '%q' "$command_text")" } cleanup_remote_files() { [[ -n "${REMOTE_ARCHIVE:-}" && -n "${REMOTE_HELPER:-}" ]] || return 0 local cleanup_command cleanup_command="rm -f -- $(printf '%q' "$REMOTE_ARCHIVE") $(printf '%q' "$REMOTE_HELPER")" ssh_cmd "bash -lc $(printf '%q' "$cleanup_command")" >/dev/null 2>&1 || true } cleanup() { if [[ -n "${SHARED_IMAGE_ARCHIVE:-}" && -f "${SHARED_IMAGE_ARCHIVE:-}" ]]; then rm -f -- "$SHARED_IMAGE_ARCHIVE" fi } trap cleanup EXIT need_cmd docker need_cmd sed need_cmd hostname need_cmd mktemp [[ "$DEPLOY_ENV" == "emp-shunfeng" ]] || die "this package must use DEPLOY_ENV=emp-shunfeng" [[ -f "$IMAGE_TAR" ]] || die "image tar not found: $IMAGE_TAR" [[ -f "$SERVICE_FILE" ]] || die "service file not found: $SERVICE_FILE" [[ -f "$MANIFEST_FILE" ]] || die "manifest not found: $MANIFEST_FILE" [[ -f "$NODE_HELPER" ]] || die "node helper not found: $NODE_HELPER" [[ -f "$RELEASE_ENV_FILE" ]] || die "release config not found: $RELEASE_ENV_FILE" [[ -f "$BACKEND_COMPOSE_FILE" ]] || die "compose file not found: $BACKEND_COMPOSE_FILE" [[ -f "$BACKEND_ENV_FILE" ]] || die "env file not found: $BACKEND_ENV_FILE" PACKAGE_ENV="$(manifest_value deploy_env)" IMAGE_TAG="$(manifest_value image_tag)" [[ "$PACKAGE_ENV" == "emp-shunfeng" ]] || die "package env mismatch: $PACKAGE_ENV" [[ "$IMAGE_TAG" =~ ^[a-zA-Z0-9_.-]+$ ]] || die "invalid package image tag: $IMAGE_TAG" RUN_ID="${IMAGE_TAG}-$(date '+%Y%m%d%H%M%S')" REQUESTED_SERVICES=() declare -A SEEN_SERVICES=() while IFS= read -r raw_service || [[ -n "$raw_service" ]]; do [[ -n "$raw_service" ]] || continue service="$(normalize_service "$raw_service")" if [[ -z "${SEEN_SERVICES[$service]:-}" ]]; then REQUESTED_SERVICES+=("$service") SEEN_SERVICES[$service]=1 fi done < "$SERVICE_FILE" [[ "${#REQUESTED_SERVICES[@]}" -gt 0 ]] || die "no services in package" SHARED_SERVICES=() for service in "${REQUESTED_SERVICES[@]}"; do [[ "$service" == "data" ]] || SHARED_SERVICES+=("$service") done ensure_primary_host if [[ "${#SHARED_SERVICES[@]}" -gt 0 && "$SKIP_REMOTE_SYNC" != "1" ]]; then need_cmd ssh need_cmd scp remote_preflight="test -f $(printf '%q' "$REMOTE_RUNTIME_DIR/release.env") -a -f $(printf '%q' "$REMOTE_RUNTIME_DIR/docker-compose.yml") -a -f $(printf '%q' "$REMOTE_RUNTIME_DIR/.env")" ssh_cmd "bash -lc $(printf '%q' "$remote_preflight")" \ || die "sub server runtime is not ready: $REMOTE_RUNTIME_DIR" fi log "=========================================" log "EMP shunfeng image-package deployment" log "Release: $IMAGE_TAG" log "Master services: ${REQUESTED_SERVICES[*]}" if [[ "${#SHARED_SERVICES[@]}" -gt 0 ]]; then log "Sub services: ${SHARED_SERVICES[*]}" else log "Sub services: none" fi log "=========================================" log "[1/6] Snapshot current master images" node_action snapshot master "${REQUESTED_SERVICES[@]}" log "[2/6] Load release images on master" if ! docker load -i "$IMAGE_TAR"; then node_action restore-tags master "${REQUESTED_SERVICES[@]}" || true die "failed to load release images" fi for service in "${REQUESTED_SERVICES[@]}"; do if ! docker image inspect "emp-$service:$IMAGE_TAG" >/dev/null 2>&1; then node_action restore-tags master "${REQUESTED_SERVICES[@]}" || true die "release image not found after docker load: emp-$service:$IMAGE_TAG" fi done REMOTE_UPDATED=0 if [[ "${#SHARED_SERVICES[@]}" -gt 0 && "$SKIP_REMOTE_SYNC" != "1" ]]; then log "[3/6] Export and transfer shared images to sub server" SHARED_IMAGE_ARCHIVE="$(mktemp "/tmp/emp-shunfeng-${RUN_ID}-XXXXXX")" SHARED_IMAGES=() for service in "${SHARED_SERVICES[@]}"; do SHARED_IMAGES+=("emp-$service:$IMAGE_TAG" "emp-$service:latest") done if ! docker save -o "$SHARED_IMAGE_ARCHIVE" "${SHARED_IMAGES[@]}"; then node_action restore-tags master "${REQUESTED_SERVICES[@]}" || true die "failed to export shared images" fi REMOTE_ARCHIVE="/tmp/emp-shunfeng-${RUN_ID}.tar" REMOTE_HELPER="/tmp/emp-shunfeng-node-${RUN_ID}.sh" if ! scp_file "$SHARED_IMAGE_ARCHIVE" "$REMOTE_ARCHIVE" \ || ! scp_file "$NODE_HELPER" "$REMOTE_HELPER" \ || ! ssh_cmd "chmod 700 $(printf '%q' "$REMOTE_HELPER")"; then node_action restore-tags master "${REQUESTED_SERVICES[@]}" || true cleanup_remote_files die "failed to transfer release files to sub server" fi log "[4/6] Update and verify sub server" if ! remote_action apply "$REMOTE_ARCHIVE" "${SHARED_SERVICES[@]}"; then remote_action rollback "" "${SHARED_SERVICES[@]}" || true node_action restore-tags master "${REQUESTED_SERVICES[@]}" || true cleanup_remote_files die "sub server deployment failed; master containers were not changed" fi REMOTE_UPDATED=1 elif [[ "${#SHARED_SERVICES[@]}" -gt 0 ]]; then log "[3/6] Skip sub server by SKIP_REMOTE_SYNC=1" log "[4/6] Sub server not updated" else log "[3/6] No shared image to transfer" log "[4/6] Sub server not required" fi log "[5/6] Update and verify master server" if ! node_action deploy master "${REQUESTED_SERVICES[@]}"; then log "Master deployment failed, roll back master" node_action rollback master "${REQUESTED_SERVICES[@]}" || true if [[ "$REMOTE_UPDATED" == "1" ]]; then log "Roll back sub server to keep both nodes consistent" remote_action rollback "" "${SHARED_SERVICES[@]}" || true fi cleanup_remote_files die "master server deployment failed" fi log "[6/6] Cleanup temporary transfer files" cleanup_remote_files log "Deployment completed: $IMAGE_TAG" docker ps --filter 'name=emp-' --format 'table {{.Names}}\t{{.Image}}\t{{.Status}}'