#!/usr/bin/env bash # ===================================================================== # 使用 Podman 部署实验室预约平台 # # 用法: # ./scripts/podman-deploy.sh up # 构建镜像并按 数据库->后端->前端 顺序启动(默认) # ./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 # 输出镜像/容器/日志等诊断信息 # # 背景: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 对不存在容器的报错噪音。 # ===================================================================== set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" PROJECT_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)" cd "${PROJECT_DIR}" # ---------- 服务/容器名 ---------- MARIADB_CONTAINER="lab-mariadb" BACKEND_CONTAINER="lab-backend" FRONTEND_CONTAINER="lab-frontend" NETWORK_NAME="lab-reservation_lab-net" # ---------- 检查 podman ---------- if ! command -v podman >/dev/null 2>&1; then echo "错误:未找到 podman,请先安装:https://podman.io/docs/installation" >&2 exit 1 fi # ---------- 选择可用的 compose 命令 ---------- if podman compose version >/dev/null 2>&1; then COMPOSE=(podman compose) elif command -v podman-compose >/dev/null 2>&1; then COMPOSE=(podman-compose) else echo "错误:未找到 podman compose / podman-compose,请安装 podman-compose:" >&2 echo " pipx install podman-compose 或 pip3 install --user podman-compose" >&2 exit 1 fi # ---------- 读取 .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() { podman image inspect "$1" >/dev/null 2>&1 } container_exists() { podman inspect "$1" >/dev/null 2>&1 } # ---------- 构建镜像 ---------- build_images() { echo "==> 构建后端镜像 lab-backend:latest(首次构建需拉取 Maven 依赖,可能较慢)" podman build -t lab-backend:latest . echo "==> 构建前端镜像 lab-frontend:latest" podman build -t lab-frontend:latest ./frontend if ! image_exists lab-backend:latest || ! image_exists lab-frontend:latest; then echo "错误:镜像构建失败,请查看上方构建日志" >&2 exit 1 fi echo "==> 镜像构建完成" } # ---------- 启动单个服务(--no-deps 绕过 compose 依赖查找,失败则回退) ---------- start_service() { local service="$1" if ! "${COMPOSE[@]}" up -d --no-deps "${service}" 2>/dev/null; then "${COMPOSE[@]}" up -d "${service}" fi } # ---------- 等待 MySQL/MariaDB 就绪 ---------- wait_mariadb() { local tries=60 echo " 等待 MariaDB 就绪..." for _ in $(seq 1 "${tries}"); do if podman exec "${MARIADB_CONTAINER}" healthcheck.sh --connect --innodb_initialized >/dev/null 2>&1; then echo " MariaDB 已就绪" return 0 fi sleep 2 done echo " 警告:MariaDB 未在预期时间内就绪" >&2 return 1 } # ---------- 等待 HTTP 服务响应 ---------- wait_http() { local name="$1" url="$2" tries="${3:-60}" code echo " 等待 ${name} 响应..." for _ in $(seq 1 "${tries}"); do code="$(curl -s -o /dev/null -w '%{http_code}' "${url}" || true)" if [ -n "${code}" ] && [ "${code}" != "000" ]; then echo " ${name} 已就绪:${url}(HTTP ${code})" return 0 fi sleep 2 done echo " 警告:${name} 未在预期时间内响应:${url}" >&2 return 1 } # ---------- 打印部署结果 ---------- print_result() { echo echo "部署完成:" echo " 前端地址:http://localhost:${WEB_PORT}" echo " 后端地址:http://localhost:${BACKEND_PORT}" echo " 演示账号:admin/admin123、teacher1/123456、student1/123456" echo podman ps --filter "name=lab-" --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}" } # ---------- 诊断信息 ---------- diagnose() { echo "== podman 版本 ==" podman version --format '{{.Client.Version}} (server {{.Server.Version}})' 2>/dev/null || podman --version echo echo "== compose 命令 ==" echo "${COMPOSE[*]}" echo echo "== 相关镜像 ==" podman images --filter "reference=lab-*" --format "table {{.Repository}}\t{{.Tag}}\t{{.ID}}\t{{.Created}}" echo echo "== 相关容器 ==" podman ps -a --filter "name=lab-" --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}" echo for container in "${MARIADB_CONTAINER}" "${BACKEND_CONTAINER}" "${FRONTEND_CONTAINER}"; do if container_exists "${container}"; then echo "== ${container} 最近日志 ==" podman logs --tail 30 "${container}" 2>&1 || true echo fi done } # ---------- 停止并清理 ---------- down() { # 静默调用 compose down(不存在容器时的报错噪音直接忽略),再按容器名兜底清理 "${COMPOSE[@]}" down >/dev/null 2>&1 || true for container in "${FRONTEND_CONTAINER}" "${BACKEND_CONTAINER}" "${MARIADB_CONTAINER}"; do podman rm -f "${container}" >/dev/null 2>&1 || true done podman network rm "${NETWORK_NAME}" >/dev/null 2>&1 || true echo "已停止并删除容器(数据库数据卷保留)" } down_volumes() { down # 兼容历史配置:清理名为 *mariadb-data 的命名卷 local volumes volumes="$(podman volume ls --format '{{.Name}}' 2>/dev/null | grep -E 'mariadb-data$' || true)" if [ -n "${volumes}" ]; then echo "${volumes}" | while read -r volume; do 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}" echo "已删除数据库数据目录 ${absolute_dir},下次启动将重建演示数据" fi ;; "") echo "提示:数据目录 ${DATA_DIR} 无法解析,请手动检查" ;; *) echo "提示:数据目录 ${DATA_DIR} 不在项目目录内,出于安全考虑未自动删除,请手动处理" ;; esac } ACTION="${1:-up}" case "${ACTION}" in up) build_images echo "==> 1/3 启动数据库" start_service mariadb wait_mariadb || true echo "==> 2/3 启动后端" start_service backend if ! wait_http "后端" "http://localhost:${BACKEND_PORT}/api/labs" 60; then diagnose exit 1 fi echo "==> 3/3 启动前端" start_service frontend if ! wait_http "前端" "http://localhost:${WEB_PORT}/" 60; then diagnose exit 1 fi print_result ;; build) build_images ;; down) down ;; down-v) down_volumes ;; status) podman ps -a --filter "name=lab-" --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}" ;; logs) podman logs -f --tail=100 "${BACKEND_CONTAINER}" ;; diagnose) diagnose ;; *) echo "用法:$0 [up|build|down|down-v|status|logs|diagnose]" >&2 exit 1 ;; esac