Compare commits
6
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d93e74bc77 | ||
|
|
4dd5cc07b4 | ||
|
|
c03c8cf9ce | ||
|
|
6038f81248 | ||
|
|
4dde725b56 | ||
|
|
bcad884069 |
@@ -0,0 +1,355 @@
|
|||||||
|
# 部署引擎设计
|
||||||
|
|
||||||
|
> *部署不是执行命令。*
|
||||||
|
>
|
||||||
|
> *部署是让一个未知环境变得可预测的过程。*
|
||||||
|
>
|
||||||
|
> *不理解环境的部署脚本,只能算是穿着风衣的命令。*
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 为什么需要部署引擎
|
||||||
|
|
||||||
|
`deploy.sh` 不是安装脚本。它是部署引擎。
|
||||||
|
|
||||||
|
安装脚本解决的是“怎么把文件放到正确的位置”。部署引擎解决的是“如何让一个未知环境进入可运行状态”。
|
||||||
|
|
||||||
|
用户的机器从来都不是一致的。在实际部署中,可能遇到:
|
||||||
|
|
||||||
|
- Docker 未安装
|
||||||
|
- Docker 已安装但未启动
|
||||||
|
- `daemon.json` 不存在
|
||||||
|
- `daemon.json` 存在但为空
|
||||||
|
- `daemon.json` 存在但格式非法
|
||||||
|
- `registry-mirrors` 不存在
|
||||||
|
- `registry-mirrors` 存在但全部不可用
|
||||||
|
- `registry-mirrors` 存在但部分不可用
|
||||||
|
- `jq` 未安装
|
||||||
|
- Git 未安装
|
||||||
|
- `frpc-console` 容器已存在
|
||||||
|
- `frpc-console` 容器正在运行
|
||||||
|
- 当前版本高于目标版本(降级)
|
||||||
|
- 当前版本低于目标版本(升级)
|
||||||
|
- LTS 与 Preview 通道混用
|
||||||
|
|
||||||
|
因此:**部署不是执行命令。部署首先是环境诊断。**
|
||||||
|
|
||||||
|
`deploy.sh` 的设计目标:
|
||||||
|
|
||||||
|
1. **探测** —— 了解当前环境的状态
|
||||||
|
2. **分析** —— 判断当前状态与目标状态之间的差距
|
||||||
|
3. **规划** —— 生成最小必要变更的执行计划
|
||||||
|
4. **确认** —— 让用户在执行前理解即将发生的变化
|
||||||
|
5. **执行** —— 按照计划执行变更
|
||||||
|
6. **验证** —— 确认变更生效且系统正常
|
||||||
|
7. **清理** —— 移除临时产物,保持系统整洁
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 部署生命周期
|
||||||
|
|
||||||
|
`deploy.sh` 的执行流程可以用以下状态流转图表示:
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────────┐
|
||||||
|
│ Environment Detection │
|
||||||
|
│ OS / ARCH / Git / Curl / Wget / Docker / jq / Container │
|
||||||
|
└─────────────────────────────────────────────────────────────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌─────────────────────────────────────────────────────────────────┐
|
||||||
|
│ State Analysis │
|
||||||
|
│ 镜像源状态 / 容器状态 / 版本状态 / 通道状态 │
|
||||||
|
└─────────────────────────────────────────────────────────────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌─────────────────────────────────────────────────────────────────┐
|
||||||
|
│ Deployment Planning │
|
||||||
|
│ 生成变更清单:镜像源配置 / 工具安装 / 代码拉取 / 镜像构建 │
|
||||||
|
└─────────────────────────────────────────────────────────────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌─────────────────────────────────────────────────────────────────┐
|
||||||
|
│ User Confirmation │
|
||||||
|
│ 展示计划 → 等待确认 → 允许自定义配置 │
|
||||||
|
└─────────────────────────────────────────────────────────────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌─────────────────────────────────────────────────────────────────┐
|
||||||
|
│ Execution │
|
||||||
|
│ 备份(降级时)→ 安装工具 → 拉取代码 → 配置镜像源 → 构建镜像 │
|
||||||
|
│ → 清理旧容器 → 启动新容器 → 写入版本 │
|
||||||
|
└─────────────────────────────────────────────────────────────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌─────────────────────────────────────────────────────────────────┐
|
||||||
|
│ Verification │
|
||||||
|
│ 镜像内容 / 容器状态 / 数据库可用性 / 版本一致性 / frpc PID │
|
||||||
|
└─────────────────────────────────────────────────────────────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌─────────────────────────────────────────────────────────────────┐
|
||||||
|
│ Cleanup │
|
||||||
|
│ 清理旧镜像(保留最近 N 个)/ 清理临时文件 │
|
||||||
|
└─────────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
每个阶段都是独立的、可观测的、可失败的。任何一个阶段失败,系统都不会进入下一个阶段。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 状态机设计
|
||||||
|
|
||||||
|
`deploy.sh` 的核心不是“执行命令”,而是“状态转换”。
|
||||||
|
|
||||||
|
以 Docker 镜像源检测为例:
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────────┐
|
||||||
|
│ 初始状态 │
|
||||||
|
│ daemon.json 存在性未知 │
|
||||||
|
└─────────────────────────────────────────────────────────────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌─────────────────────────────────────────────────────────────────┐
|
||||||
|
│ daemon.json 不存在 │
|
||||||
|
│ 状态: no_file │
|
||||||
|
│ 动作: 创建并写入 │
|
||||||
|
└─────────────────────────────────────────────────────────────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌─────────────────────────────────────────────────────────────────┐
|
||||||
|
│ daemon.json 存在但格式非法 │
|
||||||
|
│ 状态: (检测到非法) │
|
||||||
|
│ 动作: 备份 → 重建为 {} │
|
||||||
|
└─────────────────────────────────────────────────────────────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌─────────────────────────────────────────────────────────────────┐
|
||||||
|
│ daemon.json 存在但无 registry-mirrors │
|
||||||
|
│ 状态: no_key │
|
||||||
|
│ 动作: 写入可用镜像源 │
|
||||||
|
└─────────────────────────────────────────────────────────────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌─────────────────────────────────────────────────────────────────┐
|
||||||
|
│ registry-mirrors 存在且全部可用 │
|
||||||
|
│ 状态: has_valid_ordered │
|
||||||
|
│ 动作: 跳过(无变更) │
|
||||||
|
└─────────────────────────────────────────────────────────────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌─────────────────────────────────────────────────────────────────┐
|
||||||
|
│ registry-mirrors 存在但可用源排在不可用源之后 │
|
||||||
|
│ 状态: has_valid_reorder_needed │
|
||||||
|
│ 动作: 重排(可用源前置) │
|
||||||
|
└─────────────────────────────────────────────────────────────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌─────────────────────────────────────────────────────────────────┐
|
||||||
|
│ registry-mirrors 全部不可用 │
|
||||||
|
│ 状态: all_invalid │
|
||||||
|
│ 动作: 插入可用默认源(如存在) │
|
||||||
|
└─────────────────────────────────────────────────────────────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌─────────────────────────────────────────────────────────────────┐
|
||||||
|
│ 默认镜像源全部不可用 │
|
||||||
|
│ 状态: default_unavailable │
|
||||||
|
│ 动作: 跳过(提示用户手动配置) │
|
||||||
|
└─────────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
这个状态机的核心原则是:
|
||||||
|
|
||||||
|
> **只在需要时修改。只在确认后执行。每次修改都是可逆的或有备份的。**
|
||||||
|
|
||||||
|
所有状态都通过 `jq` 检测,所有修改都通过 `jq` 执行,所有原始配置都被备份。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 幂等设计
|
||||||
|
|
||||||
|
`deploy.sh` 是幂等的。多次执行的结果与一次执行相同。
|
||||||
|
|
||||||
|
幂等性通过以下方式保证:
|
||||||
|
|
||||||
|
### 1. 条件执行
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 只在容器存在时执行删除
|
||||||
|
if docker ps -a | grep -q frpc-console; then
|
||||||
|
docker stop frpc-console
|
||||||
|
docker rm frpc-console
|
||||||
|
fi
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 状态检测前置
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 检测到用户配置全部可用时跳过修改
|
||||||
|
if [ "$MIRROR_STATUS" = "has_valid_ordered" ]; then
|
||||||
|
WRITE_DAEMON=false
|
||||||
|
RESTART_DOCKER=false
|
||||||
|
fi
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 版本标记
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 每次构建使用唯一标签,不覆盖已有镜像
|
||||||
|
IMAGE_TAG="${SEMVER}-lts-$(date +%Y%m%d)"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. 增量修改而非覆盖
|
||||||
|
|
||||||
|
镜像源配置采用“插入到最前面”而非“替换整个文件”的策略,保留用户原有配置。
|
||||||
|
|
||||||
|
幂等性的意义在于:
|
||||||
|
|
||||||
|
> **用户可以在任何时候重新运行 `deploy.sh`,而不必担心系统被破坏。**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 事务完整性
|
||||||
|
|
||||||
|
`deploy.sh` 采用类似数据库事务的设计原则:
|
||||||
|
|
||||||
|
### 1. 变更前备份
|
||||||
|
|
||||||
|
降级操作会触发数据库备份:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
if [ "$TGT_SEMVER" \< "$CUR_SEMVER" ]; then
|
||||||
|
BACKUP_DIR="/opt/frpc-console-backups/${TIMESTAMP}_${CHANNEL}"
|
||||||
|
cp -r "$DEPLOY_DIR" "$BACKUP_DIR/frpc-console"
|
||||||
|
fi
|
||||||
|
```
|
||||||
|
|
||||||
|
修改 `daemon.json` 前也会备份原文件:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp "$DAEMON_JSON" "${DAEMON_JSON}.bak.$(date +%Y%m%d-%H%M%S)"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 变更后验证
|
||||||
|
|
||||||
|
每次写入后都验证目标状态是否达成:
|
||||||
|
|
||||||
|
- 写入镜像源后验证 JSON 有效性
|
||||||
|
- 启动容器后验证容器状态
|
||||||
|
- 写入版本后验证版本一致性
|
||||||
|
- 启动后验证 frpc 进程是否存在
|
||||||
|
|
||||||
|
### 3. 失败不继续
|
||||||
|
|
||||||
|
任何关键步骤失败都会中断部署,不会进入下一阶段:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
if [ $? -ne 0 ]; then
|
||||||
|
print_error "Docker 构建失败"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 验证不是可选项
|
||||||
|
|
||||||
|
大多数安装脚本在“安装完成”后即结束。`deploy.sh` 在“安装完成”后才开始验证。
|
||||||
|
|
||||||
|
验证链:
|
||||||
|
|
||||||
|
```
|
||||||
|
构建完成
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
验证镜像内容 ──→ 失败则退出
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
启动容器
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
验证容器状态 ──→ 失败则退出
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
验证数据库可用性
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
验证版本一致性 ──→ 警告但不退出
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
验证 frpc 子进程 ──→ 警告但不退出
|
||||||
|
```
|
||||||
|
|
||||||
|
验证的目的是区分“部署完成”和“部署成功”。
|
||||||
|
|
||||||
|
- **部署完成**:脚本执行完毕
|
||||||
|
- **部署成功**:所有组件正常运行
|
||||||
|
|
||||||
|
`deploy.sh` 提供明确的信号来区分这两种状态。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 边界条件处理
|
||||||
|
|
||||||
|
边界条件是部署脚本最大的不确定性来源。`deploy.sh` 在处理边界时遵循一个原则:**先假设它可能出问题,再设计应对方式。**
|
||||||
|
|
||||||
|
以下边界条件已被识别并处理:
|
||||||
|
|
||||||
|
### 已处理的边界
|
||||||
|
|
||||||
|
- `daemon.json` 不存在 → 创建
|
||||||
|
- `daemon.json` 为空 → 写入 `{}`
|
||||||
|
- `daemon.json` 格式非法 → 备份并重建
|
||||||
|
- `jq` 未安装 → 尝试安装,失败则降级处理
|
||||||
|
- `registry-mirrors` 不存在 → 写入可用源
|
||||||
|
- `registry-mirrors` 全部不可用 → 插入可用默认源
|
||||||
|
- 用户镜像源全部失效 → 插入可用默认源到最前面
|
||||||
|
- 默认镜像源全部不可用 → 跳过配置,提示用户手动处理
|
||||||
|
- 旧版本文件在根目录 → 自动迁移到 `data/`
|
||||||
|
- 容器已存在 → 停止并删除
|
||||||
|
- 容器正在运行 → 优雅停止
|
||||||
|
- 降级操作 → 自动备份,用户确认
|
||||||
|
- LTS ↔ Preview 通道切换 → 备份数据库,用户确认
|
||||||
|
- 旧镜像积累 → 保留最近 N 个,自动清理
|
||||||
|
|
||||||
|
### 处理原则
|
||||||
|
|
||||||
|
每新增一个边界条件,都是在扩展一个同一个决策表:当 `环境变量 X` 处于 `状态 Y` 时,引擎应该执行 `动作 Z`。这些条目不相互覆盖,而是叠加在同一个状态机之上,彼此通过前置条件互锁。
|
||||||
|
|
||||||
|
这种方式的优点是:边界条件越多,系统对环境的适应能力越强,但状态机的核心结构不需要跟着膨胀。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 与工程设计哲学的关系
|
||||||
|
|
||||||
|
`deployment-engine-design.md` 与 `engineering-philosophy.md` 的关系是:
|
||||||
|
|
||||||
|
| 文档 | 回答的问题 |
|
||||||
|
|:---|:---|
|
||||||
|
| `engineering-philosophy.md` | 为什么这样设计? |
|
||||||
|
| `deployment-engine-design.md` | 这些设计如何落地? |
|
||||||
|
| `deploy.sh` | 落地后的最终产物 |
|
||||||
|
|
||||||
|
三者形成一条完整的链路:
|
||||||
|
|
||||||
|
```
|
||||||
|
哲学 → 架构 → 部署引擎 → 代码
|
||||||
|
```
|
||||||
|
|
||||||
|
哲学文档定义“为什么”,部署引擎文档定义“如何实现”,`deploy.sh` 是“实现的最终形态”。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 结语
|
||||||
|
|
||||||
|
> `deploy.sh` 不是安装脚本。
|
||||||
|
>
|
||||||
|
> 它是 `frpc-console` 的部署引擎。
|
||||||
|
>
|
||||||
|
> 它负责:环境探测、状态分析、部署规划、用户确认、执行动作、结果验证、环境清理。
|
||||||
|
>
|
||||||
|
> 它的设计目标是:**在尽量不打扰用户已有环境的前提下,把部署成功率提高到接近 100%。**
|
||||||
|
|
||||||
|
这份文档记录了它为什么长成这样。
|
||||||
@@ -24,9 +24,33 @@ DEFAULT_DEPLOY_DIR="/opt/frpc-console"
|
|||||||
IMAGE_NAME="frpc-console"
|
IMAGE_NAME="frpc-console"
|
||||||
KEEP_IMAGES=3
|
KEEP_IMAGES=3
|
||||||
|
|
||||||
# ---------- 状态变量 ----------
|
# ---------- 镜像加速配置 ----------
|
||||||
|
DEFAULT_MIRRORS=(
|
||||||
|
"https://docker.1panel.live"
|
||||||
|
"https://docker.1panel.dev"
|
||||||
|
"https://docker.1ms.run"
|
||||||
|
)
|
||||||
|
DAEMON_JSON="/etc/docker/daemon.json"
|
||||||
|
MIRROR_CHECK_TIMEOUT=5
|
||||||
|
|
||||||
|
# ---------- 镜像加速状态变量 ----------
|
||||||
|
MIRROR_STATUS=""
|
||||||
|
DEFAULT_STATUS=""
|
||||||
|
WRITE_DAEMON=false
|
||||||
|
RESTART_DOCKER=false
|
||||||
|
REORDER=false
|
||||||
|
INSERT_DEFAULT=false
|
||||||
|
HAS_REGISTRY_MIRRORS_KEY=false
|
||||||
|
AVAILABLE_MIRRORS=()
|
||||||
|
USER_MIRRORS_AVAILABLE=()
|
||||||
|
USER_MIRRORS_UNAVAILABLE=()
|
||||||
|
USER_MIRRORS_ALL=()
|
||||||
|
MIRROR_DETECT_OUTPUT=""
|
||||||
|
|
||||||
|
# ---------- 其他状态变量 ----------
|
||||||
OS=""; OS_VERSION=""; ARCH=""
|
OS=""; OS_VERSION=""; ARCH=""
|
||||||
HAS_GIT=false; HAS_CURL=false; HAS_WGET=false; HAS_DOCKER=false
|
HAS_GIT=false; HAS_CURL=false; HAS_WGET=false; HAS_DOCKER=false
|
||||||
|
HAS_JQ=false
|
||||||
NEED_INSTALL_GIT=false; NEED_INSTALL_CURL=false; NEED_INSTALL_WGET=false
|
NEED_INSTALL_GIT=false; NEED_INSTALL_CURL=false; NEED_INSTALL_WGET=false
|
||||||
PORT=${DEFAULT_PORT}
|
PORT=${DEFAULT_PORT}
|
||||||
DEPLOY_DIR=${DEFAULT_DEPLOY_DIR}
|
DEPLOY_DIR=${DEFAULT_DEPLOY_DIR}
|
||||||
@@ -36,6 +60,7 @@ SKIP_CONFIRM=false; CHECK_ONLY=false; DRY_RUN=false
|
|||||||
CHANNEL=""; BRANCH=""; TARGET_VERSION=""; IMAGE_TAG=""
|
CHANNEL=""; BRANCH=""; TARGET_VERSION=""; IMAGE_TAG=""
|
||||||
CURRENT_VERSION=""
|
CURRENT_VERSION=""
|
||||||
|
|
||||||
|
# ---------- 打印函数 ----------
|
||||||
print_info() { echo -e "${BLUE}[INFO]${NC} $1"; }
|
print_info() { echo -e "${BLUE}[INFO]${NC} $1"; }
|
||||||
print_success() { echo -e "${GREEN}[✓]${NC} $1"; }
|
print_success() { echo -e "${GREEN}[✓]${NC} $1"; }
|
||||||
print_warn() { echo -e "${YELLOW}[⚠]${NC} $1"; }
|
print_warn() { echo -e "${YELLOW}[⚠]${NC} $1"; }
|
||||||
@@ -65,6 +90,7 @@ parse_args() {
|
|||||||
done
|
done
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# ---------- 权限检测 ----------
|
||||||
check_root() {
|
check_root() {
|
||||||
if [ "$EUID" -ne 0 ]; then
|
if [ "$EUID" -ne 0 ]; then
|
||||||
print_error "请使用 root 权限运行此脚本"
|
print_error "请使用 root 权限运行此脚本"
|
||||||
@@ -73,6 +99,7 @@ check_root() {
|
|||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# ---------- 系统检测 ----------
|
||||||
detect_os() {
|
detect_os() {
|
||||||
if [ -f /etc/os-release ]; then
|
if [ -f /etc/os-release ]; then
|
||||||
. /etc/os-release
|
. /etc/os-release
|
||||||
@@ -96,11 +123,13 @@ detect_arch() {
|
|||||||
esac
|
esac
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# ---------- 工具检测 ----------
|
||||||
check_tools() {
|
check_tools() {
|
||||||
command -v git &> /dev/null && HAS_GIT=true || NEED_INSTALL_GIT=true
|
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 curl &> /dev/null && HAS_CURL=true || NEED_INSTALL_CURL=true
|
||||||
command -v wget &> /dev/null && HAS_WGET=true || NEED_INSTALL_WGET=true
|
command -v wget &> /dev/null && HAS_WGET=true || NEED_INSTALL_WGET=true
|
||||||
command -v docker &> /dev/null && HAS_DOCKER=true
|
command -v docker &> /dev/null && HAS_DOCKER=true
|
||||||
|
command -v jq &> /dev/null && HAS_JQ=true || HAS_JQ=false
|
||||||
}
|
}
|
||||||
|
|
||||||
check_container() {
|
check_container() {
|
||||||
@@ -112,6 +141,444 @@ check_container() {
|
|||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# ---------- 镜像加速:连通性检测 ----------
|
||||||
|
check_mirror_available() {
|
||||||
|
local url="$1"
|
||||||
|
local http_code
|
||||||
|
http_code=$(curl -s --max-time "$MIRROR_CHECK_TIMEOUT" -o /dev/null -w "%{http_code}" \
|
||||||
|
"${url}/v2/" 2>/dev/null)
|
||||||
|
if [[ "$http_code" =~ ^2[0-9][0-9]$ ]] || [ "$http_code" = "401" ] || [ "$http_code" = "403" ]; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------- 镜像加速:安装 jq ----------
|
||||||
|
install_jq() {
|
||||||
|
print_info "尝试安装 jq..."
|
||||||
|
case $OS in
|
||||||
|
opensuse*|suse*|opensuse-tumbleweed|opensuse-slowroll|opensuse-leap)
|
||||||
|
zypper install -y jq 2>/dev/null || true
|
||||||
|
;;
|
||||||
|
ubuntu|debian|linuxmint)
|
||||||
|
apt update -qq 2>/dev/null && apt install -y jq 2>/dev/null || true
|
||||||
|
;;
|
||||||
|
centos|rhel|fedora|rocky|almalinux)
|
||||||
|
yum install -y jq 2>/dev/null || true
|
||||||
|
;;
|
||||||
|
alpine)
|
||||||
|
apk add jq 2>/dev/null || true
|
||||||
|
;;
|
||||||
|
arch|manjaro|endeavouros)
|
||||||
|
pacman -S --needed --noconfirm jq 2>/dev/null || true
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
return 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
command -v jq &> /dev/null && HAS_JQ=true || HAS_JQ=false
|
||||||
|
if [ "$HAS_JQ" = true ]; then
|
||||||
|
print_success "jq 安装成功"
|
||||||
|
return 0
|
||||||
|
else
|
||||||
|
print_warn "jq 安装失败,将使用 sed 模式(功能受限)"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------- 镜像加速:确保 daemon.json 存在且有效 ----------
|
||||||
|
ensure_daemon_json_valid() {
|
||||||
|
if [ -f "$DAEMON_JSON" ] && [ -s "$DAEMON_JSON" ]; then
|
||||||
|
if jq -e '.' "$DAEMON_JSON" >/dev/null 2>&1; then
|
||||||
|
return 0
|
||||||
|
else
|
||||||
|
local backup_path="${DAEMON_JSON}.bak.$(date +%Y%m%d-%H%M%S)"
|
||||||
|
cp "$DAEMON_JSON" "$backup_path"
|
||||||
|
print_warn "daemon.json 格式非法,已备份至: $backup_path"
|
||||||
|
echo '{}' > "$DAEMON_JSON"
|
||||||
|
print_info "已重建为有效的 JSON 文件"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
elif [ -f "$DAEMON_JSON" ] && [ ! -s "$DAEMON_JSON" ]; then
|
||||||
|
local backup_path="${DAEMON_JSON}.bak.$(date +%Y%m%d-%H%M%S)"
|
||||||
|
cp "$DAEMON_JSON" "$backup_path" 2>/dev/null || true
|
||||||
|
print_warn "daemon.json 为空文件,已备份至: $backup_path"
|
||||||
|
echo '{}' > "$DAEMON_JSON"
|
||||||
|
print_info "已重建为有效的 JSON 文件"
|
||||||
|
return 0
|
||||||
|
else
|
||||||
|
print_info "daemon.json 不存在,将创建"
|
||||||
|
echo '{}' > "$DAEMON_JSON"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------- 镜像加速:检测阶段 ----------
|
||||||
|
check_docker_mirrors() {
|
||||||
|
print_step "检测 Docker 镜像源配置..."
|
||||||
|
|
||||||
|
# 1. 检测三个默认地址的连通性
|
||||||
|
print_info "检测默认镜像源连通性 (超时 ${MIRROR_CHECK_TIMEOUT}s)..."
|
||||||
|
local default_available_count=0
|
||||||
|
for mirror in "${DEFAULT_MIRRORS[@]}"; do
|
||||||
|
if check_mirror_available "$mirror"; then
|
||||||
|
AVAILABLE_MIRRORS+=("$mirror")
|
||||||
|
default_available_count=$((default_available_count + 1))
|
||||||
|
print_info " ✅ $mirror 可用"
|
||||||
|
else
|
||||||
|
print_info " ❌ $mirror 不可用"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ ${#AVAILABLE_MIRRORS[@]} -eq 0 ]; then
|
||||||
|
DEFAULT_STATUS="default_unavailable"
|
||||||
|
print_warn "所有默认镜像源均不可用,将跳过镜像加速配置"
|
||||||
|
elif [ ${#AVAILABLE_MIRRORS[@]} -eq ${#DEFAULT_MIRRORS[@]} ]; then
|
||||||
|
DEFAULT_STATUS="default_all_available"
|
||||||
|
print_success "所有默认镜像源均可用"
|
||||||
|
else
|
||||||
|
DEFAULT_STATUS="default_partial"
|
||||||
|
print_info "部分默认镜像源可用 (${#AVAILABLE_MIRRORS[@]}/${#DEFAULT_MIRRORS[@]})"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 2. 检查 daemon.json
|
||||||
|
if [ ! -f "$DAEMON_JSON" ]; then
|
||||||
|
MIRROR_STATUS="no_file"
|
||||||
|
HAS_REGISTRY_MIRRORS_KEY=false
|
||||||
|
print_info "daemon.json 不存在"
|
||||||
|
generate_mirror_detect_output
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 3. 检测 jq 是否可解析
|
||||||
|
if ! jq -e '.' "$DAEMON_JSON" >/dev/null 2>&1 2>/dev/null; then
|
||||||
|
print_warn "daemon.json 格式不标准,将备份并重建"
|
||||||
|
local backup_path="${DAEMON_JSON}.bak.$(date +%Y%m%d-%H%M%S)"
|
||||||
|
cp "$DAEMON_JSON" "$backup_path"
|
||||||
|
print_info "已备份至: $backup_path"
|
||||||
|
echo '{}' > "$DAEMON_JSON"
|
||||||
|
print_warn "已重建 daemon.json,请确认是否需要将其他自定义配置迁回"
|
||||||
|
MIRROR_STATUS="no_file"
|
||||||
|
HAS_REGISTRY_MIRRORS_KEY=false
|
||||||
|
generate_mirror_detect_output
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 4. 检查是否存在 registry-mirrors 字段
|
||||||
|
local has_key
|
||||||
|
has_key=$(jq -e 'has("registry-mirrors")' "$DAEMON_JSON" 2>/dev/null)
|
||||||
|
if [ "$has_key" != "true" ]; then
|
||||||
|
HAS_REGISTRY_MIRRORS_KEY=false
|
||||||
|
MIRROR_STATUS="no_key"
|
||||||
|
print_info "daemon.json 中无 registry-mirrors 字段"
|
||||||
|
generate_mirror_detect_output
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
HAS_REGISTRY_MIRRORS_KEY=true
|
||||||
|
|
||||||
|
# 5. 读取 registry-mirrors 数组
|
||||||
|
local mirror_list
|
||||||
|
mirror_list=$(jq -r '."registry-mirrors" // [] | .[]' "$DAEMON_JSON" 2>/dev/null)
|
||||||
|
if [ -z "$mirror_list" ]; then
|
||||||
|
MIRROR_STATUS="empty_array"
|
||||||
|
print_info "registry-mirrors 为空数组"
|
||||||
|
generate_mirror_detect_output
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 6. 读取用户配置所有地址(保持顺序)
|
||||||
|
USER_MIRRORS_ALL=()
|
||||||
|
while IFS= read -r mirror; do
|
||||||
|
if [ -n "$mirror" ]; then
|
||||||
|
USER_MIRRORS_ALL+=("$mirror")
|
||||||
|
fi
|
||||||
|
done <<< "$mirror_list"
|
||||||
|
|
||||||
|
# 7. 检测用户配置中每个地址的连通性
|
||||||
|
print_info "检测用户配置的镜像源连通性..."
|
||||||
|
USER_MIRRORS_AVAILABLE=()
|
||||||
|
USER_MIRRORS_UNAVAILABLE=()
|
||||||
|
|
||||||
|
for mirror in "${USER_MIRRORS_ALL[@]}"; do
|
||||||
|
if check_mirror_available "$mirror"; then
|
||||||
|
USER_MIRRORS_AVAILABLE+=("$mirror")
|
||||||
|
print_info " ✅ $mirror (用户配置) 可用"
|
||||||
|
else
|
||||||
|
USER_MIRRORS_UNAVAILABLE+=("$mirror")
|
||||||
|
print_info " ❌ $mirror (用户配置) 不可用"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# 8. 判断状态
|
||||||
|
if [ ${#USER_MIRRORS_AVAILABLE[@]} -gt 0 ]; then
|
||||||
|
local need_reorder=false
|
||||||
|
local seen_unavailable=false
|
||||||
|
for mirror in "${USER_MIRRORS_ALL[@]}"; do
|
||||||
|
local is_avail=false
|
||||||
|
for a in "${USER_MIRRORS_AVAILABLE[@]}"; do
|
||||||
|
if [ "$a" = "$mirror" ]; then
|
||||||
|
is_avail=true
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
if [ "$is_avail" = true ] && [ "$seen_unavailable" = true ]; then
|
||||||
|
need_reorder=true
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
if [ "$is_avail" = false ]; then
|
||||||
|
seen_unavailable=true
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ "$need_reorder" = true ]; then
|
||||||
|
MIRROR_STATUS="has_valid_reorder_needed"
|
||||||
|
print_info "可用镜像源排在不可用镜像源之后,需要重排"
|
||||||
|
else
|
||||||
|
MIRROR_STATUS="has_valid_ordered"
|
||||||
|
print_info "用户镜像源配置正常,无需调整"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
MIRROR_STATUS="all_invalid"
|
||||||
|
print_warn "用户配置全部不可用"
|
||||||
|
fi
|
||||||
|
|
||||||
|
generate_mirror_detect_output
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------- 镜像加速:生成检测输出 ----------
|
||||||
|
generate_mirror_detect_output() {
|
||||||
|
MIRROR_DETECT_OUTPUT=""
|
||||||
|
|
||||||
|
if [ "$DEFAULT_STATUS" = "default_unavailable" ]; then
|
||||||
|
MIRROR_DETECT_OUTPUT="${MIRROR_DETECT_OUTPUT}${YELLOW}⚠ 所有默认镜像源均不可用,将跳过镜像加速配置${NC}\n"
|
||||||
|
MIRROR_DETECT_OUTPUT="${MIRROR_DETECT_OUTPUT}如果 Docker 构建失败,请检查网络或手动配置镜像源。\n"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
local avail_list=""
|
||||||
|
for m in "${AVAILABLE_MIRRORS[@]}"; do
|
||||||
|
avail_list="${avail_list} ✓ ${m}\n"
|
||||||
|
done
|
||||||
|
|
||||||
|
case "$MIRROR_STATUS" in
|
||||||
|
no_file|no_key|empty_array)
|
||||||
|
MIRROR_DETECT_OUTPUT="${MIRROR_DETECT_OUTPUT}${YELLOW}未检测到 registry-mirrors 配置。${NC}\n\n"
|
||||||
|
if [ ${#AVAILABLE_MIRRORS[@]} -gt 0 ]; then
|
||||||
|
MIRROR_DETECT_OUTPUT="${MIRROR_DETECT_OUTPUT}将写入以下可用镜像源:\n\n${avail_list}\n"
|
||||||
|
MIRROR_DETECT_OUTPUT="${MIRROR_DETECT_OUTPUT}写入位置:${DAEMON_JSON}\n"
|
||||||
|
MIRROR_DETECT_OUTPUT="${MIRROR_DETECT_OUTPUT}${YELLOW}Docker 将在配置完成后自动重启以应用配置。${NC}\n"
|
||||||
|
WRITE_DAEMON=true
|
||||||
|
RESTART_DOCKER=true
|
||||||
|
REORDER=false
|
||||||
|
INSERT_DEFAULT=true
|
||||||
|
else
|
||||||
|
MIRROR_DETECT_OUTPUT="${MIRROR_DETECT_OUTPUT}${RED}无可用的默认镜像源,将跳过配置。${NC}\n"
|
||||||
|
WRITE_DAEMON=false
|
||||||
|
RESTART_DOCKER=false
|
||||||
|
REORDER=false
|
||||||
|
INSERT_DEFAULT=false
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
|
||||||
|
has_valid_ordered)
|
||||||
|
MIRROR_DETECT_OUTPUT="${MIRROR_DETECT_OUTPUT}${GREEN}检测完成。用户已配置可用的镜像源:${NC}\n\n"
|
||||||
|
for m in "${USER_MIRRORS_AVAILABLE[@]}"; do
|
||||||
|
MIRROR_DETECT_OUTPUT="${MIRROR_DETECT_OUTPUT} ✓ ${m}\n"
|
||||||
|
done
|
||||||
|
if [ ${#USER_MIRRORS_UNAVAILABLE[@]} -gt 0 ]; then
|
||||||
|
MIRROR_DETECT_OUTPUT="${MIRROR_DETECT_OUTPUT}\n${YELLOW}不可用地址:${NC}\n"
|
||||||
|
for m in "${USER_MIRRORS_UNAVAILABLE[@]}"; do
|
||||||
|
MIRROR_DETECT_OUTPUT="${MIRROR_DETECT_OUTPUT} ✗ ${m}\n"
|
||||||
|
done
|
||||||
|
MIRROR_DETECT_OUTPUT="${MIRROR_DETECT_OUTPUT}\n可用地址已排在不可用地址之前,无需修改。\n"
|
||||||
|
else
|
||||||
|
MIRROR_DETECT_OUTPUT="${MIRROR_DETECT_OUTPUT}\n所有地址均可用,无需修改。\n"
|
||||||
|
fi
|
||||||
|
WRITE_DAEMON=false
|
||||||
|
RESTART_DOCKER=false
|
||||||
|
REORDER=false
|
||||||
|
INSERT_DEFAULT=false
|
||||||
|
;;
|
||||||
|
|
||||||
|
has_valid_reorder_needed)
|
||||||
|
MIRROR_DETECT_OUTPUT="${MIRROR_DETECT_OUTPUT}${YELLOW}检测完成。用户配置中存在不可用或顺序不当的镜像源。${NC}\n\n"
|
||||||
|
MIRROR_DETECT_OUTPUT="${MIRROR_DETECT_OUTPUT}当前配置顺序:\n"
|
||||||
|
local idx=1
|
||||||
|
for m in "${USER_MIRRORS_ALL[@]}"; do
|
||||||
|
local status=""
|
||||||
|
local found=false
|
||||||
|
for a in "${USER_MIRRORS_AVAILABLE[@]}"; do
|
||||||
|
if [ "$a" = "$m" ]; then
|
||||||
|
found=true
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
if [ "$found" = true ]; then
|
||||||
|
status="${GREEN}✅ 可用${NC}"
|
||||||
|
else
|
||||||
|
status="${RED}❌ 不可用${NC}"
|
||||||
|
fi
|
||||||
|
MIRROR_DETECT_OUTPUT="${MIRROR_DETECT_OUTPUT} ${idx}. ${m} ${status}\n"
|
||||||
|
idx=$((idx + 1))
|
||||||
|
done
|
||||||
|
MIRROR_DETECT_OUTPUT="${MIRROR_DETECT_OUTPUT}\n${YELLOW}将重新排序:可用地址前置,不可用地址后移。${NC}\n"
|
||||||
|
MIRROR_DETECT_OUTPUT="${MIRROR_DETECT_OUTPUT}\n新顺序:\n"
|
||||||
|
local new_idx=1
|
||||||
|
for m in "${USER_MIRRORS_AVAILABLE[@]}"; do
|
||||||
|
MIRROR_DETECT_OUTPUT="${MIRROR_DETECT_OUTPUT} ${new_idx}. ${m} ${GREEN}✅${NC}\n"
|
||||||
|
new_idx=$((new_idx + 1))
|
||||||
|
done
|
||||||
|
for m in "${USER_MIRRORS_UNAVAILABLE[@]}"; do
|
||||||
|
MIRROR_DETECT_OUTPUT="${MIRROR_DETECT_OUTPUT} ${new_idx}. ${m} ${RED}❌${NC}\n"
|
||||||
|
new_idx=$((new_idx + 1))
|
||||||
|
done
|
||||||
|
MIRROR_DETECT_OUTPUT="${MIRROR_DETECT_OUTPUT}\n${YELLOW}Docker 将在配置完成后自动重启以应用配置。${NC}\n"
|
||||||
|
WRITE_DAEMON=true
|
||||||
|
RESTART_DOCKER=true
|
||||||
|
REORDER=true
|
||||||
|
INSERT_DEFAULT=false
|
||||||
|
;;
|
||||||
|
|
||||||
|
all_invalid)
|
||||||
|
if [ ${#AVAILABLE_MIRRORS[@]} -gt 0 ]; then
|
||||||
|
MIRROR_DETECT_OUTPUT="${MIRROR_DETECT_OUTPUT}${YELLOW}检测完成。用户配置的镜像源全部不可用:${NC}\n\n"
|
||||||
|
for m in "${USER_MIRRORS_UNAVAILABLE[@]}"; do
|
||||||
|
MIRROR_DETECT_OUTPUT="${MIRROR_DETECT_OUTPUT} ✗ ${m}\n"
|
||||||
|
done
|
||||||
|
MIRROR_DETECT_OUTPUT="${MIRROR_DETECT_OUTPUT}\n${GREEN}检测到可用镜像源:${NC}\n\n"
|
||||||
|
for m in "${AVAILABLE_MIRRORS[@]}"; do
|
||||||
|
MIRROR_DETECT_OUTPUT="${MIRROR_DETECT_OUTPUT} ✓ ${m}\n"
|
||||||
|
done
|
||||||
|
MIRROR_DETECT_OUTPUT="${MIRROR_DETECT_OUTPUT}\n${YELLOW}将把可用地址插入到用户配置最前面。${NC}\n"
|
||||||
|
MIRROR_DETECT_OUTPUT="${MIRROR_DETECT_OUTPUT}${YELLOW}Docker 将在配置完成后自动重启以应用配置。${NC}\n"
|
||||||
|
WRITE_DAEMON=true
|
||||||
|
RESTART_DOCKER=true
|
||||||
|
REORDER=false
|
||||||
|
INSERT_DEFAULT=true
|
||||||
|
else
|
||||||
|
MIRROR_DETECT_OUTPUT="${MIRROR_DETECT_OUTPUT}${RED}检测完成。用户配置的镜像源全部不可用:${NC}\n\n"
|
||||||
|
for m in "${USER_MIRRORS_UNAVAILABLE[@]}"; do
|
||||||
|
MIRROR_DETECT_OUTPUT="${MIRROR_DETECT_OUTPUT} ✗ ${m}\n"
|
||||||
|
done
|
||||||
|
MIRROR_DETECT_OUTPUT="${MIRROR_DETECT_OUTPUT}\n${RED}默认镜像源也全部不可用。将跳过镜像加速配置。${NC}\n"
|
||||||
|
MIRROR_DETECT_OUTPUT="${MIRROR_DETECT_OUTPUT}${YELLOW}如果 Docker 构建失败,请检查网络连接,或手动配置 /etc/docker/daemon.json 中的 registry-mirrors。${NC}\n"
|
||||||
|
WRITE_DAEMON=false
|
||||||
|
RESTART_DOCKER=false
|
||||||
|
REORDER=false
|
||||||
|
INSERT_DEFAULT=false
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
print_title
|
||||||
|
print_subtitle "Docker 镜像源检测结果"
|
||||||
|
echo ""
|
||||||
|
echo -e "$MIRROR_DETECT_OUTPUT"
|
||||||
|
print_title
|
||||||
|
echo ""
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------- 镜像加速:配置执行阶段 ----------
|
||||||
|
configure_docker_mirrors() {
|
||||||
|
if [ "$WRITE_DAEMON" != true ]; then
|
||||||
|
print_info "无需修改 Docker 镜像源配置"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
print_step "配置 Docker 镜像源..."
|
||||||
|
|
||||||
|
if [ "$HAS_JQ" = false ]; then
|
||||||
|
print_warn "jq 未安装,尝试安装..."
|
||||||
|
if ! install_jq; then
|
||||||
|
print_error "jq 安装失败,无法配置镜像源"
|
||||||
|
print_error "请手动配置 /etc/docker/daemon.json 中的 registry-mirrors"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
local new_mirrors=()
|
||||||
|
if [ "$REORDER" = true ]; then
|
||||||
|
new_mirrors=("${USER_MIRRORS_AVAILABLE[@]}" "${USER_MIRRORS_UNAVAILABLE[@]}")
|
||||||
|
print_info "[反馈] 动作: 重排镜像源顺序"
|
||||||
|
elif [ "$INSERT_DEFAULT" = true ]; then
|
||||||
|
if [ "$MIRROR_STATUS" = "no_file" ] || [ "$MIRROR_STATUS" = "no_key" ] || [ "$MIRROR_STATUS" = "empty_array" ]; then
|
||||||
|
new_mirrors=("${AVAILABLE_MIRRORS[@]}")
|
||||||
|
print_info "[反馈] 动作: 写入可用默认镜像源 (无已有配置)"
|
||||||
|
else
|
||||||
|
new_mirrors=("${AVAILABLE_MIRRORS[@]}" "${USER_MIRRORS_UNAVAILABLE[@]}")
|
||||||
|
print_info "[反馈] 动作: 插入可用默认镜像源到最前面 (用户配置全部不可用)"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
print_info "无需修改镜像源配置"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
print_info "[反馈] 新镜像列表: ${new_mirrors[*]}"
|
||||||
|
|
||||||
|
if ! ensure_daemon_json_valid; then
|
||||||
|
print_error "无法创建或修复 daemon.json"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
local backup_path="${DAEMON_JSON}.bak.$(date +%Y%m%d-%H%M%S)"
|
||||||
|
cp "$DAEMON_JSON" "$backup_path"
|
||||||
|
print_info "[反馈] 已备份: $backup_path"
|
||||||
|
|
||||||
|
local json_mirrors
|
||||||
|
json_mirrors=$(printf '%s\n' "${new_mirrors[@]}" | jq -R . | jq -s . 2>/dev/null)
|
||||||
|
if [ -z "$json_mirrors" ] || [ "$json_mirrors" = "[]" ]; then
|
||||||
|
print_error "[反馈] 构建 JSON 镜像数组失败或为空"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
print_info "[反馈] json_mirrors: $json_mirrors"
|
||||||
|
|
||||||
|
print_info "[反馈] 执行 jq 写入操作..."
|
||||||
|
if ! jq --argjson mirrors "$json_mirrors" '."registry-mirrors" = $mirrors' \
|
||||||
|
"$DAEMON_JSON" > "${DAEMON_JSON}.tmp" 2> "${DAEMON_JSON}.error"; then
|
||||||
|
print_error "[反馈] jq 执行失败"
|
||||||
|
if [ -f "${DAEMON_JSON}.error" ]; then
|
||||||
|
print_error "[反馈] 错误详情:"
|
||||||
|
cat "${DAEMON_JSON}.error" | while read -r line; do
|
||||||
|
print_error " $line"
|
||||||
|
done
|
||||||
|
rm -f "${DAEMON_JSON}.error"
|
||||||
|
fi
|
||||||
|
rm -f "${DAEMON_JSON}.tmp"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ ! -f "${DAEMON_JSON}.tmp" ] || [ ! -s "${DAEMON_JSON}.tmp" ]; then
|
||||||
|
print_error "[反馈] 临时文件为空或不存在"
|
||||||
|
rm -f "${DAEMON_JSON}.tmp"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! jq -e '.' "${DAEMON_JSON}.tmp" >/dev/null 2>&1; then
|
||||||
|
print_error "[反馈] 生成的临时文件不是有效的 JSON"
|
||||||
|
rm -f "${DAEMON_JSON}.tmp"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
mv "${DAEMON_JSON}.tmp" "$DAEMON_JSON"
|
||||||
|
print_success "daemon.json 已更新"
|
||||||
|
|
||||||
|
if [ "$RESTART_DOCKER" = true ]; then
|
||||||
|
print_step "重启 Docker 服务..."
|
||||||
|
if systemctl restart docker 2>/dev/null; then
|
||||||
|
print_success "Docker 重启完成"
|
||||||
|
sleep 2
|
||||||
|
else
|
||||||
|
print_error "Docker 重启失败,请手动重启后重新运行脚本"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
print_success "镜像源配置完成"
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
# ---------- 版本管理 ----------
|
# ---------- 版本管理 ----------
|
||||||
read_version_from_remote() {
|
read_version_from_remote() {
|
||||||
local branch="$1"
|
local branch="$1"
|
||||||
@@ -244,6 +711,14 @@ print_environment_summary() {
|
|||||||
# ---------- 部署计划 ----------
|
# ---------- 部署计划 ----------
|
||||||
generate_plan() {
|
generate_plan() {
|
||||||
PLAN=""
|
PLAN=""
|
||||||
|
if [ "$WRITE_DAEMON" = true ]; then
|
||||||
|
PLAN="${PLAN} • 配置 Docker 镜像源"
|
||||||
|
if [ "$RESTART_DOCKER" = true ]; then
|
||||||
|
PLAN="${PLAN}(写入可用地址,并重启 Docker)\n"
|
||||||
|
else
|
||||||
|
PLAN="${PLAN}\n"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
if [ "$NEED_INSTALL_GIT" = true ] || [ "$NEED_INSTALL_CURL" = true ] || [ "$NEED_INSTALL_WGET" = true ]; then
|
if [ "$NEED_INSTALL_GIT" = true ] || [ "$NEED_INSTALL_CURL" = true ] || [ "$NEED_INSTALL_WGET" = true ]; then
|
||||||
local pkgs=""
|
local pkgs=""
|
||||||
[ "$NEED_INSTALL_GIT" = true ] && pkgs="${pkgs} git"
|
[ "$NEED_INSTALL_GIT" = true ] && pkgs="${pkgs} git"
|
||||||
@@ -252,12 +727,12 @@ generate_plan() {
|
|||||||
PLAN="${PLAN} • 安装必要工具:${pkgs}\n"
|
PLAN="${PLAN} • 安装必要工具:${pkgs}\n"
|
||||||
fi
|
fi
|
||||||
PLAN="${PLAN} • 拉取 frpc-console 源码 (${BRANCH} 分支)\n"
|
PLAN="${PLAN} • 拉取 frpc-console 源码 (${BRANCH} 分支)\n"
|
||||||
PLAN="${PLAN} • 构建 Docker 镜像: ${IMAGE_NAME}:${IMAGE_TAG}\n"
|
|
||||||
PLAN="${PLAN} • 目标版本: ${TARGET_VERSION}\n"
|
|
||||||
if [ "$CONTAINER_EXISTS" = true ]; then
|
if [ "$CONTAINER_EXISTS" = true ]; then
|
||||||
PLAN="${PLAN} • 停止并删除旧容器\n"
|
PLAN="${PLAN} • 停止并删除旧容器\n"
|
||||||
fi
|
fi
|
||||||
PLAN="${PLAN} • 启动 frpc-console 容器 (端口 ${PORT})"
|
PLAN="${PLAN} • 构建 Docker 镜像: ${IMAGE_NAME}:${IMAGE_TAG}\n"
|
||||||
|
PLAN="${PLAN} • 启动容器 (端口 ${PORT})\n"
|
||||||
|
PLAN="${PLAN} • 写入版本文件并验证"
|
||||||
}
|
}
|
||||||
|
|
||||||
print_deployment_plan() {
|
print_deployment_plan() {
|
||||||
@@ -384,14 +859,12 @@ do_deploy() {
|
|||||||
mkdir -p bin static
|
mkdir -p bin static
|
||||||
print_success "代码拉取完成 (分支: ${BRANCH})"
|
print_success "代码拉取完成 (分支: ${BRANCH})"
|
||||||
|
|
||||||
# ----- 停止旧容器 -----
|
# ============================================================
|
||||||
if [ "$CONTAINER_EXISTS" = true ]; then
|
# 镜像加速配置:拉取代码之后、docker build 之前执行
|
||||||
print_step "停止旧容器..."
|
# ============================================================
|
||||||
if [ "$CONTAINER_RUNNING" = true ]; then
|
if ! configure_docker_mirrors; then
|
||||||
docker stop frpc-console 2>/dev/null || true
|
print_error "镜像源配置失败,请检查后重试"
|
||||||
fi
|
exit 1
|
||||||
docker rm frpc-console 2>/dev/null || true
|
|
||||||
print_success "旧容器已清理"
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# ----- 准备部署目录 -----
|
# ----- 准备部署目录 -----
|
||||||
@@ -465,6 +938,23 @@ do_deploy() {
|
|||||||
print_info "已标记 ${IMAGE_NAME}:preview"
|
print_info "已标记 ${IMAGE_NAME}:preview"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# 强制清理旧容器(独立检测,不依赖之前缓存的变量)
|
||||||
|
# ============================================================
|
||||||
|
print_step "检查并清理旧容器..."
|
||||||
|
if docker ps -a --format '{{.Names}}' 2>/dev/null | grep -q "^frpc-console$"; then
|
||||||
|
print_info "检测到已存在的 frpc-console 容器"
|
||||||
|
if docker ps --format '{{.Names}}' 2>/dev/null | grep -q "^frpc-console$"; then
|
||||||
|
print_info "容器正在运行,停止中..."
|
||||||
|
docker stop frpc-console 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
print_info "删除旧容器..."
|
||||||
|
docker rm frpc-console 2>/dev/null || true
|
||||||
|
print_success "旧容器已清理"
|
||||||
|
else
|
||||||
|
print_info "未检测到旧容器"
|
||||||
|
fi
|
||||||
|
|
||||||
# ----- 启动新容器 -----
|
# ----- 启动新容器 -----
|
||||||
print_step "启动 frpc-console 容器..."
|
print_step "启动 frpc-console 容器..."
|
||||||
docker run -d \
|
docker run -d \
|
||||||
@@ -594,6 +1084,8 @@ main() {
|
|||||||
check_tools
|
check_tools
|
||||||
check_container
|
check_container
|
||||||
|
|
||||||
|
check_docker_mirrors
|
||||||
|
|
||||||
select_channel
|
select_channel
|
||||||
generate_target_version "$CHANNEL" "$BRANCH"
|
generate_target_version "$CHANNEL" "$BRANCH"
|
||||||
|
|
||||||
|
|||||||
@@ -21,6 +21,7 @@
|
|||||||
|---|---|
|
|---|---|
|
||||||
| [📖 工程设计哲学](./Docs/engineering-philosophy.md) | 为什么它会长成今天这样 |
|
| [📖 工程设计哲学](./Docs/engineering-philosophy.md) | 为什么它会长成今天这样 |
|
||||||
| [🔄 更新策略](./Docs/update-strategy.md) | Preview / LTS |
|
| [🔄 更新策略](./Docs/update-strategy.md) | Preview / LTS |
|
||||||
|
| [🚀 部署引擎设计](./Docs/deployment-engine-design.md) | deploy 的完整架构设计 |
|
||||||
| [🏗 架构设计](./Docs/architecture.md) | 整体架构设计思路 |
|
| [🏗 架构设计](./Docs/architecture.md) | 整体架构设计思路 |
|
||||||
| [⚙ 设计决策](./Docs/design-decisions.md) | 为什么没有 CI?为什么叫 Console? |
|
| [⚙ 设计决策](./Docs/design-decisions.md) | 为什么没有 CI?为什么叫 Console? |
|
||||||
| [🌐 边缘节点](./Docs/edge-node.md) | 为什么支持 BusyBox、ARMHF |
|
| [🌐 边缘节点](./Docs/edge-node.md) | 为什么支持 BusyBox、ARMHF |
|
||||||
|
|||||||
Reference in New Issue
Block a user