deploy_agent.sh 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376
  1. #!/bin/bash
  2. # ============ 强制要求 Bash 执行 ============
  3. if [ -z "$BASH_VERSION" ]; then
  4. echo "****************************************************************"
  5. echo "* 错误:此脚本必须使用 bash 执行,不支持 sh/dash!"
  6. echo "* 请使用以下任一方式运行:"
  7. echo "* 1. 赋予执行权限后: ./deploy_agent.sh"
  8. echo "* 2. 显式指定 bash: bash deploy_agent.sh"
  9. echo "****************************************************************"
  10. exit 1
  11. fi
  12. # ==========================================
  13. # ================= 配置区域 =================
  14. SOURCE_DIR="/home/lq/lq_workspace/LQAgentServer/source/LQAgentWritePlatform"
  15. DOCKER_APP_DIR="/home/lq/lq_workspace/LQAgentServer/app/LqAgentWritePlatform/docker"
  16. COMPOSE_FILE="docker-compose.yml"
  17. IMAGE_NAME="lq_agent_write_platform_server_dev"
  18. GIT_USER="WangXuMing"
  19. GIT_PASS="123456"
  20. PROXY_HOST="127.0.0.1"
  21. PROXY_PORT="7890"
  22. # ================= 辅助函数 =================
  23. log_info() {
  24. echo -e "\033[32m[INFO] $(date '+%Y-%m-%d %H:%M:%S') - $1\033[0m"
  25. }
  26. log_error() {
  27. echo -e "\033[31m[ERROR] $(date '+%Y-%m-%d %H:%M:%S') - $1\033[0m"
  28. }
  29. log_warn() {
  30. echo -e "\033[33m[WARN] $(date '+%Y-%m-%d %H:%M:%S') - $1\033[0m"
  31. }
  32. check_status() {
  33. if [ $? -ne 0 ]; then
  34. log_error "$1 执行失败,脚本终止。"
  35. exit 1
  36. fi
  37. }
  38. ask_use_proxy() {
  39. USE_PROXY=false
  40. PROXY_URL=""
  41. if [ ! -t 0 ]; then
  42. log_info "非交互终端,默认不使用代理"
  43. return
  44. fi
  45. echo ""
  46. echo "============================================"
  47. echo " 是否使用代理加速依赖下载?"
  48. echo " 默认: 不使用代理(直接走清华镜像源)"
  49. echo "============================================"
  50. read -p "使用代理 [y/N]: " proxy_choice
  51. case "$proxy_choice" in
  52. [yY][eE][sS]|[yY])
  53. USE_PROXY=true
  54. if [ -z "$PROXY_HOST" ]; then
  55. PROXY_HOST=$(hostname -I | awk '{print $1}')
  56. fi
  57. PROXY_URL="http://${PROXY_HOST}:${PROXY_PORT}"
  58. log_info "已启用代理: ${PROXY_URL}"
  59. if ! curl -s --connect-timeout 2 "http://${PROXY_HOST}:${PROXY_PORT}" > /dev/null 2>&1; then
  60. echo ""
  61. echo "[WARN] 代理端口 ${PROXY_HOST}:${PROXY_PORT} 未响应,可能尚未启动"
  62. echo " 请在另一个终端执行: clashctl on"
  63. echo ""
  64. read -p "确认代理已开启后按回车继续,或 Ctrl+C 退出: "
  65. fi
  66. ;;
  67. *)
  68. log_info "不使用代理,走默认镜像源"
  69. ;;
  70. esac
  71. }
  72. compare_versions() {
  73. v1=$(echo "$1" | sed 's/v//')
  74. v2=$(echo "$2" | sed 's/v//')
  75. if [ "$(echo "$v1 == $v2" | bc)" -eq 1 ]; then
  76. return 0
  77. elif [ "$(echo "$v1 > $v2" | bc)" -eq 1 ]; then
  78. return 1
  79. else
  80. return 2
  81. fi
  82. }
  83. # ================= 步骤 1: Git 拉取代码 =================
  84. log_info "步骤 1: 进入源码目录并拉取最新代码..."
  85. if [ ! -d "$SOURCE_DIR" ]; then
  86. log_error "源码目录不存在: $SOURCE_DIR"
  87. exit 1
  88. fi
  89. if [ ! -x "$SOURCE_DIR" ]; then
  90. log_error "源码目录无进入权限!正在修复..."
  91. sudo chmod +x "$SOURCE_DIR"
  92. sudo chown -R lq:lq "$SOURCE_DIR"
  93. fi
  94. cd "$SOURCE_DIR" || {
  95. log_error "进入源码目录失败!路径:$SOURCE_DIR"
  96. exit 1
  97. }
  98. check_status "进入源码目录"
  99. if [ ! -d ".git" ]; then
  100. log_error "当前目录不是 Git 仓库!路径:$SOURCE_DIR"
  101. exit 1
  102. fi
  103. log_info "检查本地是否存在可能与远程冲突的已修改文件..."
  104. HAS_CONFLICT_FILES=$(git status --porcelain | grep -v "^??")
  105. if [ -n "$HAS_CONFLICT_FILES" ]; then
  106. log_info "发现以下文件存在本地修改(将被远程最新代码覆盖):"
  107. echo "$HAS_CONFLICT_FILES" | awk '{print " - " $2}'
  108. log_info "正在强制丢弃本地修改,确保同步远程最新代码..."
  109. git checkout -- .
  110. log_info "本地冲突文件修改已丢弃,准备拉取远程最新代码..."
  111. else
  112. log_info "本地无可能冲突的已修改文件,直接拉取远程最新代码..."
  113. fi
  114. ORIGIN_URL=$(git remote get-url origin 2>/dev/null)
  115. if [ $? -ne 0 ]; then
  116. log_error "获取 Git 远程地址失败!请检查 remote 配置"
  117. exit 1
  118. fi
  119. CLEAN_URL=${ORIGIN_URL#*://}
  120. AUTH_URL="http://${GIT_USER}:${GIT_PASS}@${CLEAN_URL}"
  121. UPSTREAM_URL=$(git remote get-url upstream 2>/dev/null)
  122. if [ $? -ne 0 ]; then
  123. log_warn "未配置 upstream 远程,503 时无法切换备用源"
  124. UPSTREAM_AVAILABLE=0
  125. else
  126. UPSTREAM_CLEAN_URL=${UPSTREAM_URL#*://}
  127. UPSTREAM_AUTH_URL="http://${GIT_USER}:${GIT_PASS}@${UPSTREAM_CLEAN_URL}"
  128. UPSTREAM_AVAILABLE=1
  129. fi
  130. MAX_RETRIES=3
  131. COUNT=0
  132. GIT_SUCCESS=0
  133. CURRENT_AUTH_URL="$AUTH_URL"
  134. while [ $COUNT -lt $MAX_RETRIES ]; do
  135. log_info "正在执行 Git Pull (第 $((COUNT+1)) 次尝试) - 强制拉取 dev 分支最新代码..."
  136. log_info "当前使用远程地址:${CURRENT_AUTH_URL}"
  137. PULL_OUTPUT=$(git pull "$CURRENT_AUTH_URL" dev --force --allow-unrelated-histories 2>&1)
  138. PULL_EXIT_CODE=$?
  139. if [ $PULL_EXIT_CODE -eq 0 ]; then
  140. GIT_SUCCESS=1
  141. LATEST_COMMIT=$(git log -1 --format="%h - %s ")
  142. log_info "Git Pull 成功!当前部署提交版本:$LATEST_COMMIT"
  143. break
  144. else
  145. if echo "$PULL_OUTPUT" | grep -qiE "503 Service Unavailable|503 Unavailable|returned error: 503" && [ $UPSTREAM_AVAILABLE -eq 1 ]; then
  146. log_error "Git Pull 失败:当前远程(origin)返回 503 不可达,切换到备用远程(upstream)重试..."
  147. log_error "错误详情:$PULL_OUTPUT"
  148. CURRENT_AUTH_URL="$UPSTREAM_AUTH_URL"
  149. COUNT=$((COUNT+1))
  150. sleep 3
  151. elif echo "$PULL_OUTPUT" | grep -qiE "503 Service Unavailable|503 Unavailable|returned error: 503" && [ $UPSTREAM_AVAILABLE -eq 0 ]; then
  152. log_error "Git Pull 失败:远程返回 503 不可达,但未配置 upstream 备用源,无法切换..."
  153. log_error "错误详情:$PULL_OUTPUT"
  154. COUNT=$((COUNT+1))
  155. sleep 3
  156. else
  157. log_error "Git Pull 失败(非 503 错误),准备重试..."
  158. log_error "错误详情:$PULL_OUTPUT"
  159. COUNT=$((COUNT+1))
  160. sleep 3
  161. fi
  162. fi
  163. done
  164. if [ $GIT_SUCCESS -eq 0 ]; then
  165. log_error "Git Pull 已重试 $MAX_RETRIES 次,全部失败!"
  166. exit 1
  167. fi
  168. # ================= 步骤 2: 关闭当前容器 =================
  169. log_info "步骤 2: 关闭正在运行的容器..."
  170. if [ ! -d "$DOCKER_APP_DIR" ]; then
  171. log_error "Docker 运行目录不存在: $DOCKER_APP_DIR"
  172. exit 1
  173. fi
  174. cd "$DOCKER_APP_DIR"
  175. check_status "进入 Docker 运行目录"
  176. docker compose down
  177. if [ $? -ne 0 ]; then
  178. log_error "警告: Docker Compose Down 返回非零状态,尝试继续..."
  179. fi
  180. # ================= 步骤 3: 获取当前版本并计算新版本号 =================
  181. log_info "步骤 3: 查找当前运行版本并计算新版本号..."
  182. CURRENT_CONFIG_TAG=$(grep -m 1 "image: ${IMAGE_NAME}:" "$DOCKER_APP_DIR/$COMPOSE_FILE" | sed "s|.*image: ${IMAGE_NAME}:||")
  183. if [ -z "$CURRENT_CONFIG_TAG" ]; then
  184. CURRENT_CONFIG_TAG="v0.01"
  185. log_warn "未在配置文件中找到版本号,使用默认版本: $CURRENT_CONFIG_TAG"
  186. else
  187. log_info "当前配置文件中的版本: $CURRENT_CONFIG_TAG"
  188. fi
  189. VERSION_NUM=$(echo "$CURRENT_CONFIG_TAG" | sed 's/v//')
  190. NEW_VERSION_NUM=$(echo "$VERSION_NUM" | awk '{printf "%.2f", $1 + 0.01}')
  191. NEW_TAG="v$NEW_VERSION_NUM"
  192. log_info "计算出的新版本号为: $NEW_TAG"
  193. # ================= 步骤 4: 清理旧镜像 =================
  194. log_info "步骤 4: 清理旧镜像(保留最新版本,删除上上次版本)..."
  195. HISTORY_IMAGES=$(docker images --filter "reference=${IMAGE_NAME}:*" --format "{{.Tag}} {{.ID}} {{.CreatedAt}}" | sort -r)
  196. if [ -n "$HISTORY_IMAGES" ]; then
  197. mapfile -t IMAGE_ARRAY <<< "$HISTORY_IMAGES"
  198. log_info "发现 ${#IMAGE_ARRAY[@]} 个历史镜像版本:"
  199. for ((i=0; i<${#IMAGE_ARRAY[@]}; i++)); do
  200. TAG=$(echo "${IMAGE_ARRAY[$i]}" | awk '{print $1}')
  201. IMAGE_ID=$(echo "${IMAGE_ARRAY[$i]}" | awk '{print $2}')
  202. CREATED=$(echo "${IMAGE_ARRAY[$i]}" | awk '{$1=$2=""; print $0}' | sed 's/^ //')
  203. if [ "$TAG" = "$CURRENT_CONFIG_TAG" ]; then
  204. log_info " [$i] $TAG - $IMAGE_ID (当前运行版本) - $CREATED"
  205. CURRENT_INDEX=$i
  206. else
  207. log_info " [$i] $TAG - $IMAGE_ID - $CREATED"
  208. fi
  209. done
  210. if [ ${#IMAGE_ARRAY[@]} -gt 1 ]; then
  211. SECOND_TAG=$(echo "${IMAGE_ARRAY[1]}" | awk '{print $1}')
  212. SECOND_ID=$(echo "${IMAGE_ARRAY[1]}" | awk '{print $2}')
  213. if [ "$SECOND_TAG" != "$NEW_TAG" ] && [ "$SECOND_TAG" != "$CURRENT_CONFIG_TAG" ]; then
  214. log_info "正在删除上上次版本镜像: $SECOND_TAG ($SECOND_ID)"
  215. docker rmi -f "$SECOND_ID" 2>/dev/null
  216. if [ $? -eq 0 ]; then
  217. log_info "成功删除上上次版本镜像: $SECOND_TAG"
  218. else
  219. log_warn "删除镜像 $SECOND_TAG 失败(可能已被删除或正在使用),跳过..."
  220. fi
  221. else
  222. log_info "跳过删除 $SECOND_TAG,因为它是当前运行版本或将要构建的新版本"
  223. fi
  224. for ((i=2; i<${#IMAGE_ARRAY[@]}; i++)); do
  225. OLD_TAG=$(echo "${IMAGE_ARRAY[$i]}" | awk '{print $1}')
  226. OLD_ID=$(echo "${IMAGE_ARRAY[$i]}" | awk '{print $2}')
  227. if [ "$OLD_TAG" != "$NEW_TAG" ] && [ "$OLD_TAG" != "$CURRENT_CONFIG_TAG" ]; then
  228. log_info "删除更旧的镜像: $OLD_TAG ($OLD_ID)"
  229. docker rmi -f "$OLD_ID" 2>/dev/null
  230. fi
  231. done
  232. else
  233. log_info "只有1个历史镜像,无需清理"
  234. fi
  235. else
  236. log_info "未找到历史镜像"
  237. fi
  238. # ================= 步骤 5: 构建新镜像 =================
  239. BASE_IMAGE_NAME="lq-agent-base"
  240. BASE_IMAGE_TAG="latest"
  241. REQUIREMENTS_FILE="$SOURCE_DIR/requirements.txt"
  242. BASE_HASH_FILE="$DOCKER_APP_DIR/.base_image_req_hash"
  243. NEED_REBUILD_BASE=false
  244. cd "$SOURCE_DIR"
  245. check_status "返回源码目录"
  246. ask_use_proxy
  247. if ! docker images --format "{{.Repository}}:{{.Tag}}" | grep -q "^${BASE_IMAGE_NAME}:${BASE_IMAGE_TAG}$"; then
  248. log_info "步骤 5a: base 镜像不存在,首次构建 ${BASE_IMAGE_NAME}:${BASE_IMAGE_TAG}(包含所有依赖,仅此一次)..."
  249. NEED_REBUILD_BASE=true
  250. else
  251. CURRENT_HASH=$(md5sum "$REQUIREMENTS_FILE" | awk '{print $1}')
  252. if [ -f "$BASE_HASH_FILE" ]; then
  253. STORED_HASH=$(cat "$BASE_HASH_FILE")
  254. if [ "$CURRENT_HASH" != "$STORED_HASH" ]; then
  255. log_info "步骤 5a: requirements.txt 已变化,重建 base 镜像..."
  256. NEED_REBUILD_BASE=true
  257. else
  258. log_info "步骤 5a: base 镜像已存在且依赖未变化,跳过重建"
  259. fi
  260. else
  261. log_info "步骤 5a: 未找到依赖哈希记录,首次记录并确保 base 镜像一致..."
  262. NEED_REBUILD_BASE=true
  263. fi
  264. fi
  265. if [ "$NEED_REBUILD_BASE" = true ]; then
  266. if [ "$USE_PROXY" = true ]; then
  267. log_info "步骤 5a: 使用代理构建 base 镜像 (${PROXY_URL})..."
  268. docker build -f Dockerfile.base \
  269. --network host \
  270. --build-arg HTTP_PROXY="${PROXY_URL}" \
  271. --build-arg HTTPS_PROXY="${PROXY_URL}" \
  272. --build-arg http_proxy="${PROXY_URL}" \
  273. --build-arg https_proxy="${PROXY_URL}" \
  274. -t "${BASE_IMAGE_NAME}:${BASE_IMAGE_TAG}" .
  275. else
  276. log_info "步骤 5a: 不使用代理,使用默认镜像源构建 base 镜像..."
  277. docker build -f Dockerfile.base -t "${BASE_IMAGE_NAME}:${BASE_IMAGE_TAG}" .
  278. fi
  279. check_status "base 镜像构建"
  280. md5sum "$REQUIREMENTS_FILE" | awk '{print $1}' > "$BASE_HASH_FILE"
  281. log_info "base 镜像构建成功: ${BASE_IMAGE_NAME}:${BASE_IMAGE_TAG}"
  282. fi
  283. log_info "步骤 5b: 构建应用镜像 $IMAGE_NAME:$NEW_TAG ..."
  284. docker build -t "${IMAGE_NAME}:${NEW_TAG}" .
  285. check_status "应用镜像构建"
  286. log_info "应用镜像构建成功: ${IMAGE_NAME}:${NEW_TAG}"
  287. # ================= 步骤 6: 修改 docker-compose.yml 版本号 =================
  288. log_info "步骤 6: 更新 docker-compose.yml 中的版本号..."
  289. cd "$DOCKER_APP_DIR"
  290. check_status "进入 Docker 运行目录"
  291. if [ ! -f "$COMPOSE_FILE" ]; then
  292. log_error "找不到配置文件: $COMPOSE_FILE"
  293. exit 1
  294. fi
  295. sed -i "s|image: ${IMAGE_NAME}:.*|image: ${IMAGE_NAME}:${NEW_TAG}|" "$COMPOSE_FILE"
  296. check_status "修改 docker-compose.yml"
  297. MATCH_LINE=$(grep "image: ${IMAGE_NAME}:" "$COMPOSE_FILE" | tr '\n' '; ')
  298. log_info "配置文件已更新: $MATCH_LINE"
  299. # ================= 步骤 7: 启动容器 =================
  300. log_info "步骤 7: 启动 Docker Compose..."
  301. docker compose up -d
  302. check_status "启动容器"
  303. # ================= 步骤 8: 显示当前保留的镜像 =================
  304. log_info "步骤 8: 当前保留的镜像版本列表:"
  305. docker images --filter "reference=${IMAGE_NAME}:*" --format "table {{.Tag}}\t{{.ID}}\t{{.Size}}\t{{.CreatedAt}}"
  306. log_info "===================================================="
  307. log_info " 开发版部署成功!"
  308. log_info " 当前运行端口: 8003"
  309. log_info " 部署版本: $NEW_TAG"
  310. log_info " 保留镜像: 最新版本 + 前一个版本"
  311. log_info "===================================================="