Files
lab/scripts/podman-deploy.sh

384 lines
13 KiB
Bash
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env bash
# =====================================================================
# 使用 Podman 部署实验室预约平台
#
# 用法:
# ./scripts/podman-deploy.sh up [--no-build] # 构建镜像并按 数据库->后端->前端 顺序启动
# ./scripts/podman-deploy.sh build # 只构建镜像
# ./scripts/podman-deploy.sh down # 停止并删除容器(保留数据库数据目录)
# ./scripts/podman-deploy.sh down-v # 停止并删除容器与数据目录(重建演示数据)
# ./scripts/podman-deploy.sh status # 查看容器状态
# ./scripts/podman-deploy.sh logs # 跟随查看后端日志
# ./scripts/podman-deploy.sh diagnose # 输出镜像/容器/日志等诊断信息
#
# 可选环境变量:
# CMD_TIMEOUT 单条 podman/compose 命令超时秒数(默认 60,防止卡死)
# BUILD_TIMEOUT 镜像构建超时秒数(默认 1800)
# READY_TRIES 等待就绪的轮询次数,每轮 2 秒(默认 60)
# DEBUG=1 输出每一步执行的命令(set -x)
#
# 背景:Podman 的 compose 实现(podman-compose)与 Docker Compose 在
# 「镜像名解析、depends_on 依赖查找」上存在差异,直接 up 常见报错:
# Error: short-name "lab-reservation_backend" did not resolve to an alias ...
# Error: "lab-backend" is not a valid container, cannot be used as a dependency
# 本脚本的处理方式:
# 1) 先用 podman build 显式构建 lab-backend / lab-frontend 镜像,避免短名拉取;
# 2) 再用 `up -d --no-deps` 逐个启动服务并等待就绪,绕过 compose 的依赖查找;
# 3) down 时直接按容器名清理,避免 podman-compose 对不存在容器的报错噪音;
# 4) 启动前生成去除 depends_on 的临时 compose 文件,避免旧版 podman-compose
# 在 `up` 时等待依赖健康检查而卡死;
# 5) 所有外部命令都带超时,任何一步卡住都会明确报出并退出,不会无限等待。
# =====================================================================
set -euo pipefail
if [ "${DEBUG:-0}" = "1" ]; then set -x; fi
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)"
cd "${PROJECT_DIR}"
# ---------- 可调参数 ----------
CMD_TIMEOUT="${CMD_TIMEOUT:-60}"
BUILD_TIMEOUT="${BUILD_TIMEOUT:-1800}"
READY_TRIES="${READY_TRIES:-60}"
# ---------- 服务/容器名 ----------
MARIADB_CONTAINER="lab-mariadb"
BACKEND_CONTAINER="lab-backend"
FRONTEND_CONTAINER="lab-frontend"
NETWORK_NAME="lab-reservation_lab-net"
# ---------- 日志与错误追踪 ----------
log() {
printf '[%s] %s\n' "$(date '+%H:%M:%S')" "$*"
}
# 出错时给出失败信息,便于定位卡住/失败的位置
on_error() {
local exit_code=$?
log "执行失败(退出码 ${exit_code}),出错位置约在脚本第 ${1:-?} 行"
log "可执行 '$0 diagnose' 查看容器与日志,或 DEBUG=1 $0 up 观察每一步命令"
exit "${exit_code}"
}
trap 'on_error ${LINENO}' ERR
# ---------- 带超时执行命令 ----------
# 说明:macOS 默认没有 timeout 命令,这里提供纯 bash 兜底实现。
# 返回 124 表示超时。
run_with_timeout() {
local seconds="$1"
shift
if command -v timeout >/dev/null 2>&1; then
timeout "${seconds}" "$@"
return $?
fi
if command -v gtimeout >/dev/null 2>&1; then
gtimeout "${seconds}" "$@"
return $?
fi
"$@" &
local pid=$!
local waited=0
while kill -0 "${pid}" 2>/dev/null; do
if [ "${waited}" -ge "${seconds}" ]; then
kill -TERM "${pid}" 2>/dev/null || true
sleep 2
kill -KILL "${pid}" 2>/dev/null || true
wait "${pid}" 2>/dev/null || true
return 124
fi
sleep 1
waited=$((waited + 1))
done
wait "${pid}"
}
# ---------- 检查 podman ----------
if ! command -v podman >/dev/null 2>&1; then
log "错误:未找到 podman,请先安装:https://podman.io/docs/installation"
exit 1
fi
# ---------- 选择可用的 compose 命令 ----------
COMPOSE=()
COMPOSE_FILE_ARGS=()
compose_available() {
if run_with_timeout 30 podman compose version >/dev/null 2>&1; then
COMPOSE=(podman compose)
return 0
fi
if command -v podman-compose >/dev/null 2>&1; then
COMPOSE=(podman-compose)
return 0
fi
return 1
}
if ! compose_available; then
log "错误:未找到可用的 podman compose / podman-compose,请安装 podman-compose:"
log " pipx install podman-compose 或 pip3 install --user podman-compose"
exit 1
fi
log "使用 compose 命令:${COMPOSE[*]}"
# ---------- 读取 .env 配置(不 source,避免值中含空格被当作命令执行) ----------
read_env() {
if [ -f .env ]; then
grep -E "^$1=" .env 2>/dev/null | tail -n 1 | cut -d= -f2- | tr -d '"' || true
fi
}
WEB_PORT="$(read_env WEB_PORT)"
BACKEND_PORT="$(read_env BACKEND_PORT)"
DATA_DIR_ENV="$(read_env DATA_DIR)"
WEB_PORT="${WEB_PORT:-8088}"
BACKEND_PORT="${BACKEND_PORT:-8080}"
DATA_DIR="${DATA_DIR:-${DATA_DIR_ENV:-./mariadb-data}}"
DATA_DIR="${DATA_DIR%/}"
# ---------- 通用检查函数(inspect 在 podman / docker 下语义一致) ----------
image_exists() {
run_with_timeout "${CMD_TIMEOUT}" podman image inspect "$1" >/dev/null 2>&1
}
container_exists() {
run_with_timeout "${CMD_TIMEOUT}" podman inspect "$1" >/dev/null 2>&1
}
# ---------- 构建镜像 ----------
build_images() {
log "构建后端镜像 lab-backend:latest(首次构建需拉取 Maven 依赖,可能较慢)"
if ! run_with_timeout "${BUILD_TIMEOUT}" podman build -t lab-backend:latest .; then
log "错误:后端镜像构建失败或超时(BUILD_TIMEOUT=${BUILD_TIMEOUT}s)"
exit 1
fi
log "构建前端镜像 lab-frontend:latest"
if ! run_with_timeout "${BUILD_TIMEOUT}" podman build -t lab-frontend:latest ./frontend; then
log "错误:前端镜像构建失败或超时(BUILD_TIMEOUT=${BUILD_TIMEOUT}s)"
exit 1
fi
if ! image_exists lab-backend:latest || ! image_exists lab-frontend:latest; then
log "错误:镜像未生成,请查看上方构建日志"
exit 1
fi
log "镜像构建完成"
}
# ---------- 生成 Podman 专用 compose 文件 ----------
# podman-compose 在 `up` 处理 depends_on 的 service_healthy 条件时可能长时间阻塞,
# 这里把 depends_on 段落剔除生成临时文件(服务启动顺序由脚本自己控制),
# 既保留 docker-compose.yml 作为唯一配置源,又规避 podman-compose 的兼容问题。
GENERATED_COMPOSE=".podman-compose.generated.yml"
prepare_compose_file() {
local base="docker-compose.yml"
if [ ! -f "${base}" ]; then
log "错误:未找到 ${base}"
exit 1
fi
awk '
/^ depends_on:[[:space:]]*$/ { skip=1; next }
skip && /^ / { next }
{ skip=0; print }
' "${base}" > "${GENERATED_COMPOSE}"
if ! grep -q "container_name: ${BACKEND_CONTAINER}" "${GENERATED_COMPOSE}"; then
log "错误:生成 ${GENERATED_COMPOSE} 失败,请检查 ${base} 格式"
exit 1
fi
log "已生成 ${GENERATED_COMPOSE}(已移除 depends_on,规避 podman-compose 依赖等待)"
COMPOSE_FILE_ARGS=(-f "${GENERATED_COMPOSE}")
}
# ---------- 启动单个服务 ----------
start_service() {
local service="$1"
log "启动服务:${service}"
run_with_timeout "${CMD_TIMEOUT}" "${COMPOSE[@]}" "${COMPOSE_FILE_ARGS[@]}" up -d "${service}"
}
# ---------- 等待 MySQL/MariaDB 就绪 ----------
wait_mariadb() {
local tries="${READY_TRIES}" waited=0
log "等待 MariaDB 就绪(最多 $((tries * 2)) 秒)..."
while [ "${waited}" -lt "${tries}" ]; do
if run_with_timeout 15 podman exec "${MARIADB_CONTAINER}" \
healthcheck.sh --connect --innodb_initialized >/dev/null 2>&1; then
log "MariaDB 已就绪"
return 0
fi
sleep 2
waited=$((waited + 1))
done
log "警告:MariaDB 未在预期时间内就绪,继续尝试启动后续服务"
return 1
}
# ---------- 等待 HTTP 服务响应 ----------
wait_http() {
local name="$1" url="$2" tries="${3:-${READY_TRIES}}" waited=0 code
log "等待 ${name} 响应(最多 $((tries * 2)) 秒):${url}"
while [ "${waited}" -lt "${tries}" ]; do
code="$(curl -s -o /dev/null -w '%{http_code}' --connect-timeout 3 --max-time 8 "${url}" || true)"
if [ -n "${code}" ] && [ "${code}" != "000" ]; then
log "${name} 已就绪:${url}(HTTP ${code})"
return 0
fi
sleep 2
waited=$((waited + 1))
done
log "警告:${name} 未在预期时间内响应:${url}"
return 1
}
# ---------- 打印部署结果 ----------
print_result() {
log "部署完成:"
echo " 前端地址:http://localhost:${WEB_PORT}"
echo " 后端地址:http://localhost:${BACKEND_PORT}"
echo " 演示账号:admin/admin123、teacher1/123456、student1/123456"
echo
show_status
}
# ---------- 查看状态 ----------
show_status() {
run_with_timeout "${CMD_TIMEOUT}" podman ps -a --filter "name=lab-" \
--format "table {{.Names}}\t{{.Status}}\t{{.Ports}}"
}
# ---------- 诊断信息 ----------
diagnose() {
log "== podman 版本 =="
run_with_timeout "${CMD_TIMEOUT}" podman version 2>&1 | head -5 || podman --version
echo
log "== compose 命令 =="
echo "${COMPOSE[*]}"
echo
log "== 相关镜像 =="
run_with_timeout "${CMD_TIMEOUT}" podman images --format \
"table {{.Repository}}\t{{.Tag}}\t{{.ID}}\t{{.Created}}" 2>/dev/null | grep -E "lab-|REPOSITORY" || true
echo
log "== 相关容器 =="
show_status || true
echo
for container in "${MARIADB_CONTAINER}" "${BACKEND_CONTAINER}" "${FRONTEND_CONTAINER}"; do
if container_exists "${container}"; then
log "== ${container} 最近日志 =="
run_with_timeout "${CMD_TIMEOUT}" podman logs --tail 30 "${container}" 2>&1 || true
echo
fi
done
}
# ---------- 停止并清理 ----------
down() {
# 静默调用 compose down(不存在容器时的报错噪音直接忽略),再按容器名兜底清理
local compose_args=()
if [ -f "${GENERATED_COMPOSE}" ]; then
compose_args=(-f "${GENERATED_COMPOSE}")
fi
run_with_timeout "${CMD_TIMEOUT}" "${COMPOSE[@]}" "${compose_args[@]}" down >/dev/null 2>&1 || true
for container in "${FRONTEND_CONTAINER}" "${BACKEND_CONTAINER}" "${MARIADB_CONTAINER}"; do
run_with_timeout "${CMD_TIMEOUT}" podman rm -f "${container}" >/dev/null 2>&1 || true
done
run_with_timeout "${CMD_TIMEOUT}" podman network rm "${NETWORK_NAME}" >/dev/null 2>&1 || true
rm -f "${GENERATED_COMPOSE}"
log "已停止并删除容器(数据库数据目录保留)"
}
down_volumes() {
down
# 兼容历史配置:清理名为 *mariadb-data 的命名卷
local volumes
volumes="$(run_with_timeout "${CMD_TIMEOUT}" podman volume ls --format '{{.Name}}' 2>/dev/null \
| grep -E 'mariadb-data$' || true)"
if [ -n "${volumes}" ]; then
echo "${volumes}" | while read -r volume; do
run_with_timeout "${CMD_TIMEOUT}" podman volume rm -f "${volume}" >/dev/null 2>&1 || true
done
fi
# 当前配置:数据库数据持久化在宿主机目录(DATA_DIR,默认 ./mariadb-data)
local absolute_dir
absolute_dir="$(cd "$(dirname "${DATA_DIR}")" 2>/dev/null && pwd)/$(basename "${DATA_DIR}")" || absolute_dir=""
case "${absolute_dir}" in
"${PROJECT_DIR}"/*)
if [ -d "${absolute_dir}" ]; then
rm -rf "${absolute_dir}"
log "已删除数据库数据目录 ${absolute_dir},下次启动将重建演示数据"
fi
;;
"")
log "提示:数据目录 ${DATA_DIR} 无法解析,请手动检查"
;;
*)
log "提示:数据目录 ${DATA_DIR} 不在项目目录内,出于安全考虑未自动删除,请手动处理"
;;
esac
}
# ---------- 启动流程 ----------
up() {
local skip_build="${1:-}"
if [ "${skip_build}" != "--no-build" ]; then
build_images
else
log "跳过镜像构建(--no-build)"
if ! image_exists lab-backend:latest || ! image_exists lab-frontend:latest; then
log "错误:本地缺少 lab-backend / lab-frontend 镜像,请先执行 '$0 build'"
exit 1
fi
fi
prepare_compose_file
log "1/3 启动数据库"
start_service mariadb
wait_mariadb || true
log "2/3 启动后端"
start_service backend
if ! wait_http "后端" "http://localhost:${BACKEND_PORT}/api/labs"; then
log "后端未就绪,输出诊断信息:"
diagnose
exit 1
fi
log "3/3 启动前端"
start_service frontend
if ! wait_http "前端" "http://localhost:${WEB_PORT}/"; then
log "前端未就绪,输出诊断信息:"
diagnose
exit 1
fi
print_result
}
ACTION="${1:-up}"
case "${ACTION}" in
up)
up "${2:-}"
;;
build)
build_images
;;
down)
down
;;
down-v)
down_volumes
;;
status)
show_status
;;
logs)
log "按 Ctrl+C 退出日志跟随"
podman logs -f --tail=100 "${BACKEND_CONTAINER}"
;;
diagnose)
diagnose
;;
*)
echo "用法:$0 [up [--no-build]|build|down|down-v|status|logs|diagnose]" >&2
exit 1
;;
esac