Files
frpc-console/deploy.sh
T

745 lines
23 KiB
Bash
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.
#!/bin/bash
# ============================================================
# frpc-console 一键部署脚本 (Docker 优先)
# 用法:
# curl -sSL .../deploy.sh | sudo bash
# curl -sSL .../deploy.sh | sudo bash -s -- --channel preview
# ============================================================
set -e
# ---------- 颜色检测 ----------
if [ -t 1 ]; then
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
CYAN='\033[0;36m'
MAGENTA='\033[0;35m'
NC='\033[0m'
else
RED=''; GREEN=''; YELLOW=''; BLUE=''; CYAN=''; MAGENTA=''; NC=''
fi
# ---------- 配置 ----------
REPO_URL="https://git.whitetop.xyz/lxh2875931338/frpc-console.git"
BRANCH="main"
WORK_DIR="/tmp/frpc-console-build"
DEFAULT_PORT=9300
DEFAULT_DEPLOY_DIR="/opt/frpc-console"
IMAGE_NAME="frpc-console"
# ---------- 状态变量 ----------
OS=""
OS_VERSION=""
ARCH=""
HAS_GIT=false
HAS_CURL=false
HAS_WGET=false
HAS_DOCKER=false
NEED_INSTALL_GIT=false
NEED_INSTALL_CURL=false
NEED_INSTALL_WGET=false
PORT=${DEFAULT_PORT}
DEPLOY_DIR=${DEFAULT_DEPLOY_DIR}
DATA_DIR="${DEPLOY_DIR}/data"
CONTAINER_EXISTS=false
CONTAINER_RUNNING=false
SKIP_CONFIRM=false
CHECK_ONLY=false
DRY_RUN=false
CHANNEL=""
CURRENT_VERSION=""
TARGET_VERSION=""
IMAGE_TAG=""
# ---------- 打印函数 ----------
print_info() { echo -e "${BLUE}[INFO]${NC} $1"; }
print_success() { echo -e "${GREEN}[✓]${NC} $1"; }
print_warn() { echo -e "${YELLOW}[⚠]${NC} $1"; }
print_error() { echo -e "${RED}[✗]${NC} $1"; }
print_step() { echo -e "\n${CYAN}${NC} $1"; }
print_title() { echo -e "\n${MAGENTA}════════════════════════════════════════════════════════${NC}"; }
print_subtitle() { echo -e "${MAGENTA} $1${NC}"; }
# ---------- 解析命令行参数 ----------
parse_args() {
for arg in "$@"; do
case $arg in
--yes|-y)
SKIP_CONFIRM=true
;;
--check)
CHECK_ONLY=true
;;
--dry-run)
DRY_RUN=true
;;
--channel)
shift
CHANNEL="$1"
;;
--channel=*)
CHANNEL="${arg#*=}"
;;
--help|-h)
echo "用法: ./deploy.sh [选项]"
echo ""
echo "选项:"
echo " --channel lts 使用 LTS 通道 (main 分支)"
echo " --channel preview 使用 Preview 通道 (test 分支)"
echo " --yes, -y 跳过所有确认提示"
echo " --check 只检测环境,不执行部署"
echo " --dry-run 显示将执行的操作,不实际执行"
echo " --help, -h 显示帮助信息"
echo ""
echo "示例:"
echo " ./deploy.sh # 交互式选择通道"
echo " ./deploy.sh --channel lts # 部署 LTS 版本"
echo " ./deploy.sh --channel preview # 部署 Preview 版本"
echo " ./deploy.sh --yes # 无人值守部署"
exit 0
;;
*)
print_error "未知选项: $arg"
echo "使用 --help 查看帮助"
exit 1
;;
esac
done
}
# ---------- 检查 root ----------
check_root() {
if [ "$EUID" -ne 0 ]; then
print_error "请使用 root 权限运行此脚本"
echo " sudo bash deploy.sh"
exit 1
fi
}
# ---------- 检测操作系统 ----------
detect_os() {
if [ -f /etc/os-release ]; then
. /etc/os-release
OS=$ID
OS_VERSION=$VERSION_ID
else
print_error "无法识别操作系统"
exit 1
fi
}
# ---------- 检测 CPU 架构 ----------
detect_arch() {
ARCH=$(uname -m)
case $ARCH in
x86_64|amd64|aarch64|arm64|armv7l|armhf)
print_success "CPU 架构: $ARCH"
;;
*)
print_error "不支持的 CPU 架构: $ARCH"
exit 1
;;
esac
}
# ---------- 检测工具 ----------
check_tools() {
command -v git &> /dev/null && HAS_GIT=true || NEED_INSTALL_GIT=true
command -v curl &> /dev/null && HAS_CURL=true || NEED_INSTALL_CURL=true
command -v wget &> /dev/null && HAS_WGET=true || NEED_INSTALL_WGET=true
command -v docker &> /dev/null && HAS_DOCKER=true
}
# ---------- 检查容器状态 ----------
check_container() {
if docker ps -a --format '{{.Names}}' 2>/dev/null | grep -q "^frpc-console$"; then
CONTAINER_EXISTS=true
if docker ps --format '{{.Names}}' 2>/dev/null | grep -q "^frpc-console$"; then
CONTAINER_RUNNING=true
fi
fi
}
# ---------- 版本管理函数 ----------
# 直接从远程仓库读取 version.ini
read_version_from_remote() {
local branch="$1"
local version_url="https://git.whitetop.xyz/lxh2875931338/frpc-console/raw/${branch}/version.ini"
local content
content=$(curl -sSL "$version_url" 2>/dev/null)
if [ -z "$content" ]; then
print_error "无法从远程读取 version.ini: $version_url"
return 1
fi
SEMVER=$(echo "$content" | grep -v "^[;#]" | head -1 | awk '{print $1}')
LTS_DATE=$(echo "$content" | grep -v "^[;#]" | head -1 | awk '{print $2}')
echo "$SEMVER $LTS_DATE"
}
get_version_type() {
local semver="$1"
local minor="${semver#*.}"
if [ "$minor" = "0" ] || [ "$minor" = "5" ]; then
echo "lts"
else
echo "preview"
fi
}
generate_target_version() {
local channel="$1"
local branch="${2:-$BRANCH}"
local semver_lts_date
semver_lts_date=$(read_version_from_remote "$branch")
if [ $? -ne 0 ] || [ -z "$semver_lts_date" ]; then
print_error "读取版本信息失败"
exit 1
fi
SEMVER=$(echo "$semver_lts_date" | awk '{print $1}')
LTS_DATE=$(echo "$semver_lts_date" | awk '{print $2}')
if [ -z "$SEMVER" ]; then
print_error "无法从 version.ini 读取语义版本"
exit 1
fi
local vtype
vtype=$(get_version_type "$SEMVER")
if [ "$channel" = "lts" ] || [ "$vtype" = "lts" ]; then
if [ -z "$LTS_DATE" ]; then
LTS_DATE=$(date +%Y%m%d)
print_warn "LTS 版本未指定日期,使用今天: $LTS_DATE"
fi
TARGET_VERSION="${SEMVER}-lts-${LTS_DATE}"
IMAGE_TAG="${SEMVER}-lts-${LTS_DATE}"
else
TIMESTAMP=$(date +%Y%m%d-%H%M%S)
TARGET_VERSION="${SEMVER}-preview-${TIMESTAMP}"
IMAGE_TAG="preview-${TIMESTAMP}"
fi
echo "$TARGET_VERSION $IMAGE_TAG"
}
get_current_version() {
local version_file="${DEPLOY_DIR}/version.ini"
if [ -f "$version_file" ]; then
grep -v "^[;#]" "$version_file" | head -1 | tr -d '\n'
else
if [ "$CONTAINER_EXISTS" = true ]; then
docker exec frpc-console cat /app/version.ini 2>/dev/null | grep -v "^[;#]" | head -1 | tr -d '\n' || echo "0.0.0-dev"
else
echo "0.0.0-dev"
fi
fi
}
extract_semver() {
local version="$1"
echo "$version" | sed -E 's/^([0-9]+\.[0-9]+).*$/\1/'
}
extract_timestamp() {
local version="$1"
echo "$version" | sed -E 's/^[0-9]+\.[0-9]+-(lts|preview)-//'
}
compare_version() {
local current="$1"
local target="$2"
local cur_semver=$(extract_semver "$current")
local tgt_semver=$(extract_semver "$target")
if [ "$tgt_semver" -gt "$cur_semver" ] 2>/dev/null; then
return 1
fi
if [ "$tgt_semver" -lt "$cur_semver" ] 2>/dev/null; then
return 2
fi
local cur_ts=$(extract_timestamp "$current")
local tgt_ts=$(extract_timestamp "$target")
if [ -z "$cur_ts" ]; then
cur_ts="0"
fi
if [ -z "$tgt_ts" ]; then
tgt_ts="0"
fi
if [ "$tgt_ts" -gt "$cur_ts" ] 2>/dev/null; then
return 1
elif [ "$tgt_ts" -eq "$cur_ts" ] 2>/dev/null; then
return 0
else
return 2
fi
}
# ---------- 通道选择(含分支切换)----------
select_channel() {
if [ -n "$CHANNEL" ]; then
if [ "$CHANNEL" != "lts" ] && [ "$CHANNEL" != "preview" ]; then
print_error "无效通道: $CHANNEL (仅支持 lts / preview)"
exit 1
fi
if [ "$CHANNEL" = "preview" ]; then
BRANCH="test"
else
BRANCH="main"
fi
print_info "使用命令行参数: ${CHANNEL} 通道 (${BRANCH} 分支)"
return
fi
echo ""
echo -e "${CYAN}请选择部署通道:${NC}"
echo " 1. LTS (稳定版,生产推荐) [默认] → main 分支"
echo " 2. Preview (技术预览版,包含新特性) → test 分支"
echo ""
read -p "请选择 [1]: " CHANNEL_INPUT </dev/tty
CHANNEL_INPUT=${CHANNEL_INPUT:-1}
case $CHANNEL_INPUT in
1|"")
CHANNEL="lts"
BRANCH="main"
;;
2)
CHANNEL="preview"
BRANCH="test"
;;
*)
print_error "无效选择"
exit 1
;;
esac
print_info "已选择: ${CHANNEL} 通道 (${BRANCH} 分支)"
}
# ---------- 检测结果汇总 ----------
print_environment_summary() {
print_title
print_subtitle "环境检测结果"
echo ""
echo -e " ${CYAN}操作系统:${NC} $OS $OS_VERSION"
echo -e " ${CYAN}CPU 架构:${NC} $ARCH"
echo ""
echo " ${CYAN}必要工具:${NC}"
if [ "$HAS_GIT" = true ]; then
echo -e " git ✅ 已安装 ($(git --version | awk '{print $3}'))"
else
echo " git ❌ 未安装 (将自动安装)"
fi
if [ "$HAS_CURL" = true ]; then
echo " curl ✅ 已安装"
else
echo " curl ❌ 未安装 (将自动安装)"
fi
if [ "$HAS_WGET" = true ]; then
echo " wget ✅ 已安装"
else
echo " wget ❌ 未安装 (将自动安装)"
fi
echo ""
echo " ${CYAN}Docker 环境:${NC}"
if [ "$HAS_DOCKER" = true ]; then
echo -e " docker ✅ 已安装 ($(docker --version | awk '{print $3}' | tr -d ','))"
else
echo " docker ❌ 未安装"
print_error "Docker 未安装,请先安装 Docker"
echo ""
echo " 快速安装:"
echo " curl -fsSL https://get.docker.com | bash"
echo " systemctl enable --now docker"
echo ""
exit 1
fi
if [ "$CONTAINER_EXISTS" = true ]; then
echo ""
echo " ${CYAN}容器状态:${NC}"
if [ "$CONTAINER_RUNNING" = true ]; then
echo -e " frpc-console ✅ 运行中"
else
echo -e " frpc-console ⏸️ 已停止"
fi
CURRENT_VERSION=$(get_current_version)
echo -e " ${CYAN}当前版本:${NC} $CURRENT_VERSION"
fi
echo ""
}
# ---------- 生成操作列表 ----------
generate_plan() {
PLAN=""
if [ "$NEED_INSTALL_GIT" = true ] || [ "$NEED_INSTALL_CURL" = true ] || [ "$NEED_INSTALL_WGET" = true ]; then
local pkgs=""
[ "$NEED_INSTALL_GIT" = true ] && pkgs="${pkgs} git"
[ "$NEED_INSTALL_CURL" = true ] && pkgs="${pkgs} curl"
[ "$NEED_INSTALL_WGET" = true ] && pkgs="${pkgs} wget"
PLAN="${PLAN} • 安装必要工具:${pkgs}\n"
fi
PLAN="${PLAN} • 拉取 frpc-console 源码 (${BRANCH} 分支)\n"
PLAN="${PLAN} • 构建 Docker 镜像: ${IMAGE_NAME}:${IMAGE_TAG}\n"
PLAN="${PLAN} • 目标版本: ${TARGET_VERSION}\n"
if [ "$CONTAINER_EXISTS" = true ]; then
PLAN="${PLAN} • 停止并删除旧容器\n"
fi
PLAN="${PLAN} • 启动 frpc-console 容器 (端口 ${PORT})"
}
# ---------- 展示部署计划 ----------
print_deployment_plan() {
print_title
print_subtitle "部署计划"
echo ""
echo -e " ${CYAN}将执行以下操作:${NC}"
echo -e "$(echo -e "$PLAN")"
echo ""
echo -e " ${CYAN}配置信息:${NC}"
echo -e " ────────────────────────────────────"
echo -e " 监听端口 : ${PORT}"
echo -e " 部署目录 : ${DEPLOY_DIR}"
echo -e " 目标版本 : ${TARGET_VERSION}"
echo -e " 镜像标签 : ${IMAGE_TAG}"
echo -e " 源码分支 : ${BRANCH}"
echo -e " ────────────────────────────────────"
if [ "$CONTAINER_EXISTS" = true ]; then
echo ""
echo -e " ${YELLOW}⚠ 检测到已存在的 frpc-console 容器${NC}"
echo -e " 当前版本: ${CURRENT_VERSION}"
echo -e " 目标版本: ${TARGET_VERSION}"
fi
echo ""
}
# ---------- 确认部署 ----------
confirm_deploy() {
if [ "$SKIP_CONFIRM" = true ]; then
echo -e "${GREEN}▶ 已启用 --yes,自动确认${NC}"
return 0
fi
echo -e -n "${CYAN}确认执行? 输入 Y 继续,输入 n 自定义配置 [Y/n]: ${NC}"
read -r CONFIRM </dev/tty
case $CONFIRM in
n|N) return 1 ;;
*) return 0 ;;
esac
}
# ---------- 自定义配置 ----------
custom_config() {
print_title
print_subtitle "自定义配置"
echo ""
read -p "请输入监听端口 [${DEFAULT_PORT}]: " INPUT_PORT </dev/tty
PORT=${INPUT_PORT:-$DEFAULT_PORT}
read -p "请输入部署目录 [${DEFAULT_DEPLOY_DIR}]: " INPUT_DEPLOY_DIR </dev/tty
DEPLOY_DIR=${INPUT_DEPLOY_DIR:-$DEFAULT_DEPLOY_DIR}
DATA_DIR="${DEPLOY_DIR}/data"
echo ""
echo -e " ${CYAN}更新后的配置:${NC}"
echo -e " ────────────────────────────────────"
echo -e " 监听端口 : ${PORT}"
echo -e " 部署目录 : ${DEPLOY_DIR}"
echo -e " 数据目录 : ${DATA_DIR}"
echo -e " ────────────────────────────────────"
echo ""
}
# ---------- 执行部署 ----------
do_deploy() {
print_title
print_subtitle "开始部署"
echo ""
# ----- 事务前钩子:备份数据库(仅降级时备份) -----
BACKUP_DIR=""
# 如果是首次部署,跳过版本比对(因为版本文件不存在)
if [ -n "$CURRENT_VERSION" ] && [ "$CURRENT_VERSION" != "0.0.0-dev" ] && [ "$CURRENT_VERSION" != "(首次部署)" ]; then
compare_version "$CURRENT_VERSION" "$TARGET_VERSION"
COMPARE_RESULT=$?
if [ $COMPARE_RESULT -eq 2 ]; then
print_warn "检测到降级操作: $CURRENT_VERSION$TARGET_VERSION"
echo ""
echo -e "${YELLOW}⚠ 降级可能导致数据不兼容${NC}"
echo " 当前版本: $CURRENT_VERSION"
echo " 目标版本: $TARGET_VERSION"
echo ""
echo -e "${CYAN}将备份完整数据目录到:${NC}"
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
BACKUP_DIR="/opt/frpc-console-backups/${TIMESTAMP}_${CHANNEL}"
echo " $BACKUP_DIR"
echo ""
read -p "确认执行降级? [y/N]: " CONFIRM_DOWNGRADE </dev/tty
if [[ ! "$CONFIRM_DOWNGRADE" =~ ^[Yy]$ ]]; then
print_info "已取消降级操作"
exit 0
fi
print_step "执行降级前备份..."
mkdir -p "$BACKUP_DIR"
if [ -d "$DEPLOY_DIR" ]; then
cp -r "$DEPLOY_DIR" "$BACKUP_DIR/frpc-console"
print_success "备份完成: $BACKUP_DIR"
else
print_warn "部署目录不存在,跳过备份"
fi
fi
else
print_info "首次部署,跳过版本比对"
fi
# ----- 安装必要工具(必须在拉取代码之前!)-----
if [ "$NEED_INSTALL_GIT" = true ] || [ "$NEED_INSTALL_CURL" = true ] || [ "$NEED_INSTALL_WGET" = true ]; then
print_step "安装必要工具..."
local pkgs=""
[ "$NEED_INSTALL_GIT" = true ] && pkgs="${pkgs} git"
[ "$NEED_INSTALL_CURL" = true ] && pkgs="${pkgs} curl"
[ "$NEED_INSTALL_WGET" = true ] && pkgs="${pkgs} wget"
case $OS in
opensuse*|suse*|opensuse-tumbleweed|opensuse-slowroll|opensuse-leap)
if [ "$NEED_INSTALL_GIT" = true ]; then
zypper install -y git-core
pkgs=$(echo "$pkgs" | sed -E 's/(^| )git( |$)/ /g' | sed 's/ */ /g' | sed 's/^ //;s/ $//')
fi
if [ -n "$pkgs" ]; then
zypper install -y $pkgs
fi
;;
ubuntu|debian|linuxmint)
apt update -qq && apt install -y $pkgs
;;
centos|rhel|fedora|rocky|almalinux)
yum install -y $pkgs
;;
alpine)
apk add $pkgs
;;
arch|manjaro|endeavouros)
pacman -S --needed --noconfirm $pkgs
;;
*)
print_error "无法识别包管理器,请手动安装: $pkgs"
exit 1
;;
esac
print_success "必要工具安装完成"
fi
# ----- 拉取代码(现在 git 已安装)-----
print_step "拉取代码..."
rm -rf "$WORK_DIR"
git clone --depth 1 --branch "$BRANCH" "$REPO_URL" "$WORK_DIR"
cd "$WORK_DIR"
mkdir -p bin static
print_success "代码拉取完成 (分支: ${BRANCH})"
# ----- 停止旧容器(拉取代码之后)-----
if [ "$CONTAINER_EXISTS" = true ]; then
print_step "停止旧容器..."
if [ "$CONTAINER_RUNNING" = true ]; then
docker stop frpc-console 2>/dev/null || true
fi
docker rm frpc-console 2>/dev/null || true
print_success "旧容器已清理"
fi
# ----- 准备部署目录 -----
print_step "准备部署目录..."
mkdir -p "$DEPLOY_DIR"
mkdir -p "$DATA_DIR"
# ----- 构建 Docker 镜像 -----
print_step "构建 Docker 镜像..."
docker build -t "${IMAGE_NAME}:${IMAGE_TAG}" \
--build-arg VERSION="${TARGET_VERSION}" \
--build-arg IMAGE_TAG="${IMAGE_TAG}" \
.
print_success "Docker 镜像构建完成: ${IMAGE_NAME}:${IMAGE_TAG}"
if [ "$CHANNEL" = "lts" ]; then
docker tag "${IMAGE_NAME}:${IMAGE_TAG}" "${IMAGE_NAME}:lts"
print_info "已标记 ${IMAGE_NAME}:lts"
else
docker tag "${IMAGE_NAME}:${IMAGE_TAG}" "${IMAGE_NAME}:preview"
print_info "已标记 ${IMAGE_NAME}:preview"
fi
# ----- 启动新容器 -----
print_step "启动 frpc-console 容器..."
docker run -d \
--name frpc-console \
--restart=always \
--network host \
-v ${DEPLOY_DIR}:/app \
-e PORT=${PORT} \
-e TZ=Asia/Shanghai \
${IMAGE_NAME}:${IMAGE_TAG}
if docker ps | grep -q frpc-console; then
print_success "容器启动成功!"
else
print_error "容器启动失败,请检查日志: docker logs frpc-console"
exit 1
fi
# ----- 检查 frpc 子进程 -----
print_step "检查 frpc 状态..."
sleep 3
if docker ps --format '{{.Names}}' | grep -q "^frpc-console$"; then
if docker exec frpc-console ps aux 2>/dev/null | grep -q "[f]rpc -c"; then
print_success "frpc 进程运行正常"
else
print_warn "frpc 进程未运行(可能配置为空,请在 WebUI 中导入 TOML"
fi
else
print_error "frpc-console 容器未运行,请检查日志"
exit 1
fi
# ----- 事务后钩子:检测数据库 -----
print_step "验证数据库状态..."
if docker exec frpc-console sqlite3 /app/frpc-console.db "SELECT COUNT(*) FROM users;" 2>/dev/null | grep -q "^[0-9]"; then
print_success "数据库可用"
else
print_warn "数据库为空或不可用,请通过 WebUI 注册管理员账户"
fi
# ----- 验证版本写入 -----
print_step "验证版本..."
sleep 1
CONTAINER_VERSION=$(docker exec frpc-console cat /app/version.ini 2>/dev/null | grep -v "^[;#]" | head -1 | tr -d '\n' || echo "unknown")
echo -e " 容器版本: ${CONTAINER_VERSION}"
echo -e " 目标版本: ${TARGET_VERSION}"
if [ "$CONTAINER_VERSION" = "$TARGET_VERSION" ]; then
print_success "版本写入成功"
else
print_warn "版本不匹配,请检查"
fi
if [ -n "$BACKUP_DIR" ]; then
echo ""
print_warn "降级前数据已备份到: $BACKUP_DIR"
echo " 如需回退,请恢复: cp -r $BACKUP_DIR/frpc-console/* $DEPLOY_DIR/"
fi
# ----- 清理临时文件 -----
print_step "清理临时文件..."
rm -rf "$WORK_DIR"
print_success "临时文件已清理"
# ----- 输出信息 -----
print_title
print_success "frpc-console 部署完成!"
print_title
echo ""
echo -e " ${CYAN}📍 访问地址:${NC} http://$(hostname -I | awk '{print $1}'):${PORT}"
echo -e " ${CYAN}📂 数据目录:${NC} ${DEPLOY_DIR}"
echo -e " ${CYAN}📦 备份目录:${NC} /tmp/frpc-console/db-backups"
echo -e " ${CYAN}🐳 容器名称:${NC} frpc-console"
echo -e " ${CYAN}🏷️ 版本:${NC} ${TARGET_VERSION}"
echo -e " ${CYAN}🌿 分支:${NC} ${BRANCH}"
echo ""
echo -e " ${CYAN}常用命令:${NC}"
echo " docker logs frpc-console # 查看日志"
echo " docker restart frpc-console # 重启服务"
echo " docker stop frpc-console # 停止服务"
echo " docker start frpc-console # 启动服务"
echo " cat ${DEPLOY_DIR}/version.ini # 查看当前版本"
echo ""
echo -e " ${YELLOW}首次访问需要注册管理员账户${NC}"
echo ""
print_title
}
# ---------- 主流程 ----------
main() {
clear 2>/dev/null || true
parse_args "$@"
check_root
print_step "检测环境..."
detect_os
detect_arch
check_tools
check_container
select_channel
# 直接从远程仓库读取版本信息,不依赖本地文件
generate_target_version "$CHANNEL" "$BRANCH"
if [ "$CONTAINER_EXISTS" = true ]; then
CURRENT_VERSION=$(get_current_version)
else
CURRENT_VERSION="(首次部署)"
fi
print_environment_summary
if [ "$CHECK_ONLY" = true ]; then
print_info "环境检测完成(--check 模式,不执行部署)"
exit 0
fi
if [ "$HAS_DOCKER" = false ]; then
print_error "Docker 未安装,请先安装 Docker"
echo ""
echo " 快速安装:"
echo " curl -fsSL https://get.docker.com | bash"
echo " systemctl enable --now docker"
echo ""
exit 1
fi
generate_plan
print_deployment_plan
if ! confirm_deploy; then
custom_config
generate_plan
print_deployment_plan
if ! confirm_deploy; then
print_info "已取消部署"
exit 0
fi
fi
if [ "$DRY_RUN" = true ]; then
print_info "演练模式(--dry-run),不实际执行部署"
exit 0
fi
do_deploy
}
main "$@"