You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

279 lines
9.2 KiB

  1. #!/usr/bin/env bash
  2. set -Eeuo pipefail
  3. # shunfeng 主从增量发布编排器。
  4. # 只处理 admin / gateway / auth / monitor / data;不处理 PDF、WS、中间件。
  5. SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
  6. cd "$SCRIPT_DIR"
  7. DEPLOY_ENV="${DEPLOY_ENV:-emp-shunfeng}"
  8. DEPLOY_HOME="${DEPLOY_HOME:-/home/emp-shunfeng}"
  9. RUNTIME_DIR="${RUNTIME_DIR:-$DEPLOY_HOME/runtime}"
  10. RELEASE_ENV_FILE="${RELEASE_ENV_FILE:-$RUNTIME_DIR/release.env}"
  11. IMAGE_TAR="${IMAGE_TAR:-images.tar}"
  12. SERVICE_FILE="${SERVICE_FILE:-services.txt}"
  13. MANIFEST_FILE="${MANIFEST_FILE:-manifest.txt}"
  14. NODE_HELPER="$SCRIPT_DIR/apply-node-update.sh"
  15. log() {
  16. echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*"
  17. }
  18. die() {
  19. echo "ERROR: $*" >&2
  20. exit 1
  21. }
  22. need_cmd() {
  23. command -v "$1" >/dev/null 2>&1 || die "missing command: $1"
  24. }
  25. normalize_service() {
  26. case "$1" in
  27. admin|emp-admin) echo "admin" ;;
  28. gateway|emp-gateway) echo "gateway" ;;
  29. auth|emp-auth) echo "auth" ;;
  30. monitor|emp-monitor) echo "monitor" ;;
  31. data|emp-data) echo "data" ;;
  32. *) die "unsupported service: $1" ;;
  33. esac
  34. }
  35. manifest_value() {
  36. local key="$1"
  37. sed -n "s/^${key}=//p" "$MANIFEST_FILE" | head -1
  38. }
  39. contains_service() {
  40. local target="$1"
  41. shift
  42. local service
  43. for service in "$@"; do
  44. [[ "$service" == "$target" ]] && return 0
  45. done
  46. return 1
  47. }
  48. if [[ -f "$RELEASE_ENV_FILE" ]]; then
  49. set -a
  50. # shellcheck disable=SC1090
  51. . "$RELEASE_ENV_FILE"
  52. set +a
  53. fi
  54. PRIMARY_HOST="${PRIMARY_HOST:-192.168.0.3}"
  55. REMOTE_HOST="${REMOTE_HOST:-192.168.0.4}"
  56. REMOTE_USER="${REMOTE_USER:-root}"
  57. SSH_PORT="${SSH_PORT:-22}"
  58. REMOTE_DEPLOY_HOME="${REMOTE_DEPLOY_HOME:-/home/emp-shunfeng}"
  59. REMOTE_RUNTIME_DIR="${REMOTE_RUNTIME_DIR:-$REMOTE_DEPLOY_HOME/runtime}"
  60. SKIP_PRIMARY_HOST_CHECK="${SKIP_PRIMARY_HOST_CHECK:-0}"
  61. SKIP_REMOTE_SYNC="${SKIP_REMOTE_SYNC:-0}"
  62. BACKEND_COMPOSE_FILE="${BACKEND_COMPOSE_FILE:-$RUNTIME_DIR/docker-compose.yml}"
  63. BACKEND_ENV_FILE="${BACKEND_ENV_FILE:-$RUNTIME_DIR/.env}"
  64. SSH_OPTS=(
  65. -o StrictHostKeyChecking=accept-new
  66. -o ServerAliveInterval=15
  67. -o ServerAliveCountMax=3
  68. )
  69. ssh_cmd() {
  70. if [[ -n "${DEPLOY_PASSWORD:-}" ]]; then
  71. need_cmd sshpass
  72. SSHPASS="$DEPLOY_PASSWORD" sshpass -e ssh -p "$SSH_PORT" "${SSH_OPTS[@]}" "$REMOTE_USER@$REMOTE_HOST" "$@"
  73. else
  74. ssh -p "$SSH_PORT" "${SSH_OPTS[@]}" "$REMOTE_USER@$REMOTE_HOST" "$@"
  75. fi
  76. }
  77. scp_file() {
  78. local source_file="$1"
  79. local target_file="$2"
  80. if [[ -n "${DEPLOY_PASSWORD:-}" ]]; then
  81. need_cmd sshpass
  82. SSHPASS="$DEPLOY_PASSWORD" sshpass -e scp -P "$SSH_PORT" "${SSH_OPTS[@]}" "$source_file" "$REMOTE_USER@$REMOTE_HOST:$target_file"
  83. else
  84. scp -P "$SSH_PORT" "${SSH_OPTS[@]}" "$source_file" "$REMOTE_USER@$REMOTE_HOST:$target_file"
  85. fi
  86. }
  87. ensure_primary_host() {
  88. [[ "$SKIP_PRIMARY_HOST_CHECK" == "1" ]] && return 0
  89. local ips ip
  90. ips="$(hostname -I 2>/dev/null || true)"
  91. [[ -n "$ips" ]] || return 0
  92. for ip in $ips; do
  93. [[ "$ip" == "$PRIMARY_HOST" ]] && return 0
  94. done
  95. die "current host IP does not include $PRIMARY_HOST. Current IP: $ips"
  96. }
  97. node_action() {
  98. local action="$1"
  99. local role="$2"
  100. shift 2
  101. DEPLOY_HOME="$DEPLOY_HOME" \
  102. RUNTIME_DIR="$RUNTIME_DIR" \
  103. bash "$NODE_HELPER" "$action" "$role" "$RUN_ID" "$IMAGE_TAG" "$@"
  104. }
  105. remote_action() {
  106. local action="$1"
  107. local image_tar="$2"
  108. shift 2
  109. local service_args command_text
  110. printf -v service_args '%q ' "$@"
  111. command_text="DEPLOY_HOME=$(printf '%q' "$REMOTE_DEPLOY_HOME") RUNTIME_DIR=$(printf '%q' "$REMOTE_RUNTIME_DIR")"
  112. if [[ -n "$image_tar" ]]; then
  113. command_text+=" IMAGE_TAR=$(printf '%q' "$image_tar")"
  114. fi
  115. command_text+=" bash $(printf '%q' "$REMOTE_HELPER") $(printf '%q' "$action") sub $(printf '%q' "$RUN_ID") $(printf '%q' "$IMAGE_TAG") $service_args"
  116. ssh_cmd "bash -lc $(printf '%q' "$command_text")"
  117. }
  118. cleanup_remote_files() {
  119. [[ -n "${REMOTE_ARCHIVE:-}" && -n "${REMOTE_HELPER:-}" ]] || return 0
  120. local cleanup_command
  121. cleanup_command="rm -f -- $(printf '%q' "$REMOTE_ARCHIVE") $(printf '%q' "$REMOTE_HELPER")"
  122. ssh_cmd "bash -lc $(printf '%q' "$cleanup_command")" >/dev/null 2>&1 || true
  123. }
  124. cleanup() {
  125. if [[ -n "${SHARED_IMAGE_ARCHIVE:-}" && -f "${SHARED_IMAGE_ARCHIVE:-}" ]]; then
  126. rm -f -- "$SHARED_IMAGE_ARCHIVE"
  127. fi
  128. }
  129. trap cleanup EXIT
  130. need_cmd docker
  131. need_cmd sed
  132. need_cmd hostname
  133. need_cmd mktemp
  134. [[ "$DEPLOY_ENV" == "emp-shunfeng" ]] || die "this package must use DEPLOY_ENV=emp-shunfeng"
  135. [[ -f "$IMAGE_TAR" ]] || die "image tar not found: $IMAGE_TAR"
  136. [[ -f "$SERVICE_FILE" ]] || die "service file not found: $SERVICE_FILE"
  137. [[ -f "$MANIFEST_FILE" ]] || die "manifest not found: $MANIFEST_FILE"
  138. [[ -f "$NODE_HELPER" ]] || die "node helper not found: $NODE_HELPER"
  139. [[ -f "$RELEASE_ENV_FILE" ]] || die "release config not found: $RELEASE_ENV_FILE"
  140. [[ -f "$BACKEND_COMPOSE_FILE" ]] || die "compose file not found: $BACKEND_COMPOSE_FILE"
  141. [[ -f "$BACKEND_ENV_FILE" ]] || die "env file not found: $BACKEND_ENV_FILE"
  142. PACKAGE_ENV="$(manifest_value deploy_env)"
  143. IMAGE_TAG="$(manifest_value image_tag)"
  144. [[ "$PACKAGE_ENV" == "emp-shunfeng" ]] || die "package env mismatch: $PACKAGE_ENV"
  145. [[ "$IMAGE_TAG" =~ ^[a-zA-Z0-9_.-]+$ ]] || die "invalid package image tag: $IMAGE_TAG"
  146. RUN_ID="${IMAGE_TAG}-$(date '+%Y%m%d%H%M%S')"
  147. REQUESTED_SERVICES=()
  148. declare -A SEEN_SERVICES=()
  149. while IFS= read -r raw_service || [[ -n "$raw_service" ]]; do
  150. [[ -n "$raw_service" ]] || continue
  151. service="$(normalize_service "$raw_service")"
  152. if [[ -z "${SEEN_SERVICES[$service]:-}" ]]; then
  153. REQUESTED_SERVICES+=("$service")
  154. SEEN_SERVICES[$service]=1
  155. fi
  156. done < "$SERVICE_FILE"
  157. [[ "${#REQUESTED_SERVICES[@]}" -gt 0 ]] || die "no services in package"
  158. SHARED_SERVICES=()
  159. for service in "${REQUESTED_SERVICES[@]}"; do
  160. [[ "$service" == "data" ]] || SHARED_SERVICES+=("$service")
  161. done
  162. ensure_primary_host
  163. if [[ "${#SHARED_SERVICES[@]}" -gt 0 && "$SKIP_REMOTE_SYNC" != "1" ]]; then
  164. need_cmd ssh
  165. need_cmd scp
  166. 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")"
  167. ssh_cmd "bash -lc $(printf '%q' "$remote_preflight")" \
  168. || die "sub server runtime is not ready: $REMOTE_RUNTIME_DIR"
  169. fi
  170. log "========================================="
  171. log "EMP shunfeng image-package deployment"
  172. log "Release: $IMAGE_TAG"
  173. log "Master services: ${REQUESTED_SERVICES[*]}"
  174. if [[ "${#SHARED_SERVICES[@]}" -gt 0 ]]; then
  175. log "Sub services: ${SHARED_SERVICES[*]}"
  176. else
  177. log "Sub services: none"
  178. fi
  179. log "========================================="
  180. log "[1/6] Snapshot current master images"
  181. node_action snapshot master "${REQUESTED_SERVICES[@]}"
  182. log "[2/6] Load release images on master"
  183. if ! docker load -i "$IMAGE_TAR"; then
  184. node_action restore-tags master "${REQUESTED_SERVICES[@]}" || true
  185. die "failed to load release images"
  186. fi
  187. for service in "${REQUESTED_SERVICES[@]}"; do
  188. if ! docker image inspect "emp-$service:$IMAGE_TAG" >/dev/null 2>&1; then
  189. node_action restore-tags master "${REQUESTED_SERVICES[@]}" || true
  190. die "release image not found after docker load: emp-$service:$IMAGE_TAG"
  191. fi
  192. done
  193. REMOTE_UPDATED=0
  194. if [[ "${#SHARED_SERVICES[@]}" -gt 0 && "$SKIP_REMOTE_SYNC" != "1" ]]; then
  195. log "[3/6] Export and transfer shared images to sub server"
  196. SHARED_IMAGE_ARCHIVE="$(mktemp "/tmp/emp-shunfeng-${RUN_ID}-XXXXXX")"
  197. SHARED_IMAGES=()
  198. for service in "${SHARED_SERVICES[@]}"; do
  199. SHARED_IMAGES+=("emp-$service:$IMAGE_TAG" "emp-$service:latest")
  200. done
  201. if ! docker save -o "$SHARED_IMAGE_ARCHIVE" "${SHARED_IMAGES[@]}"; then
  202. node_action restore-tags master "${REQUESTED_SERVICES[@]}" || true
  203. die "failed to export shared images"
  204. fi
  205. REMOTE_ARCHIVE="/tmp/emp-shunfeng-${RUN_ID}.tar"
  206. REMOTE_HELPER="/tmp/emp-shunfeng-node-${RUN_ID}.sh"
  207. if ! scp_file "$SHARED_IMAGE_ARCHIVE" "$REMOTE_ARCHIVE" \
  208. || ! scp_file "$NODE_HELPER" "$REMOTE_HELPER" \
  209. || ! ssh_cmd "chmod 700 $(printf '%q' "$REMOTE_HELPER")"; then
  210. node_action restore-tags master "${REQUESTED_SERVICES[@]}" || true
  211. cleanup_remote_files
  212. die "failed to transfer release files to sub server"
  213. fi
  214. log "[4/6] Update and verify sub server"
  215. if ! remote_action apply "$REMOTE_ARCHIVE" "${SHARED_SERVICES[@]}"; then
  216. remote_action rollback "" "${SHARED_SERVICES[@]}" || true
  217. node_action restore-tags master "${REQUESTED_SERVICES[@]}" || true
  218. cleanup_remote_files
  219. die "sub server deployment failed; master containers were not changed"
  220. fi
  221. REMOTE_UPDATED=1
  222. elif [[ "${#SHARED_SERVICES[@]}" -gt 0 ]]; then
  223. log "[3/6] Skip sub server by SKIP_REMOTE_SYNC=1"
  224. log "[4/6] Sub server not updated"
  225. else
  226. log "[3/6] No shared image to transfer"
  227. log "[4/6] Sub server not required"
  228. fi
  229. log "[5/6] Update and verify master server"
  230. if ! node_action deploy master "${REQUESTED_SERVICES[@]}"; then
  231. log "Master deployment failed, roll back master"
  232. node_action rollback master "${REQUESTED_SERVICES[@]}" || true
  233. if [[ "$REMOTE_UPDATED" == "1" ]]; then
  234. log "Roll back sub server to keep both nodes consistent"
  235. remote_action rollback "" "${SHARED_SERVICES[@]}" || true
  236. fi
  237. cleanup_remote_files
  238. die "master server deployment failed"
  239. fi
  240. log "[6/6] Cleanup temporary transfer files"
  241. cleanup_remote_files
  242. log "Deployment completed: $IMAGE_TAG"
  243. docker ps --filter 'name=emp-' --format 'table {{.Names}}\t{{.Image}}\t{{.Status}}'