release: harden plugin deployment and recovery
This commit is contained in:
+24
-5
@@ -24,7 +24,7 @@ docker compose -f docker-compose.yml up -d
|
||||
```sh
|
||||
RELEASE_SHA=COMMIT_SHA_40_HEX
|
||||
curl -fsSL "https://git.awaioi.com/awaioi/sub2api-add/raw/commit/${RELEASE_SHA}/deploy/install.sh" \
|
||||
| sudo env PLUGIN_REF=v1.1.0 PLUGIN_COMMIT_SHA="$RELEASE_SHA" bash -s -- --plugin all
|
||||
| sudo env PLUGIN_REF=v1.1.1 PLUGIN_COMMIT_SHA="$RELEASE_SHA" bash -s -- --plugin all
|
||||
```
|
||||
|
||||
生产安装必须固定 `PLUGIN_COMMIT_SHA`,并从受信任的发布记录复制
|
||||
@@ -42,7 +42,7 @@ curl -fsSL https://git.awaioi.com/awaioi/sub2api-add/raw/branch/main/deploy/inst
|
||||
```sh
|
||||
RELEASE_SHA=COMMIT_SHA_40_HEX
|
||||
curl -fsSL "https://git.awaioi.com/awaioi/sub2api-add/raw/commit/${RELEASE_SHA}/deploy/install.sh" \
|
||||
| sudo env PLUGIN_REF=v1.1.0 PLUGIN_COMMIT_SHA="$RELEASE_SHA" bash -s -- --plugin plugin-admin
|
||||
| sudo env PLUGIN_REF=v1.1.1 PLUGIN_COMMIT_SHA="$RELEASE_SHA" bash -s -- --plugin plugin-admin
|
||||
```
|
||||
|
||||
安装订阅插件:
|
||||
@@ -50,7 +50,7 @@ curl -fsSL "https://git.awaioi.com/awaioi/sub2api-add/raw/commit/${RELEASE_SHA}/
|
||||
```sh
|
||||
RELEASE_SHA=COMMIT_SHA_40_HEX
|
||||
curl -fsSL "https://git.awaioi.com/awaioi/sub2api-add/raw/commit/${RELEASE_SHA}/deploy/install.sh" \
|
||||
| sudo env PLUGIN_REF=v1.1.0 PLUGIN_COMMIT_SHA="$RELEASE_SHA" bash -s -- --plugin subscription-admin
|
||||
| sudo env PLUGIN_REF=v1.1.1 PLUGIN_COMMIT_SHA="$RELEASE_SHA" bash -s -- --plugin subscription-admin
|
||||
```
|
||||
|
||||
默认安装位置:
|
||||
@@ -85,11 +85,28 @@ sudo systemctl restart sub2api-plugin-admin sub2api-subscription-admin
|
||||
与 `127.0.0.1:8091`。
|
||||
2. 登录 plugin-admin,上传业务插件包,或在插件市场选择目录版本。
|
||||
3. 控制面完成签名、哈希和 Core 兼容性校验后,仅将包登记为“已入库,待启用”,不会启动进程。
|
||||
4. 配置业务插件的 loopback `service_url`,再点击启用;健康检查通过后才算安装完成。
|
||||
4. 对 external 业务插件,先由部署者启动对应后端,再配置 loopback `service_url`;
|
||||
对 command 插件,点击启用时控制面才会启动进程。两种模式都必须通过健康和
|
||||
就绪检查后才算安装完成。
|
||||
5. 预览、确认并应用插件声明的管理员菜单。
|
||||
|
||||
订阅插件是可选项;未安装或未应用菜单时,Core 管理员菜单不会出现“订阅管理”。
|
||||
|
||||
## 版本回滚
|
||||
|
||||
Plugin Admin 会在升级时保留旧 revision。生产回滚建议在插件详情的“版本”页
|
||||
选择目标 revision;也可以调用下面的受保护 API(需要 Plugin Admin 会话的
|
||||
CSRF token 和唯一 `Idempotency-Key`):
|
||||
|
||||
```text
|
||||
POST /api/plugins/{plugin_id}/rollback
|
||||
{"revision":"<retained revision id>"}
|
||||
```
|
||||
|
||||
控制面会先启动并探测目标 revision,健康后再停止当前版本并原子切换注册表。
|
||||
探测失败不会替换当前活动版本。external 插件回滚前必须先确保目标版本的
|
||||
独立服务已经监听 `service_url`;command 插件由控制面负责启动和停止。
|
||||
|
||||
## 一键卸载
|
||||
|
||||
默认卸载服务和二进制,但保留配置与插件数据,便于重新安装:
|
||||
@@ -121,4 +138,6 @@ sudo ./deploy/uninstall.sh --plugin subscription-admin
|
||||
```
|
||||
|
||||
再次运行安装脚本会重新构建并重启选定插件。安装脚本发现源码有未提交修改
|
||||
时会中止,避免升级覆盖本地改动。
|
||||
时会中止,避免升级覆盖本地改动。该脚本从 pinned commit 构建控制面和示例
|
||||
业务后端;业务 `.s2plugin` 的生产签名由 `plugins/subscription-admin/package.sh`
|
||||
和受信发布者密钥负责,Plugin Admin 不以 `.s2plugin` 归档交付。
|
||||
|
||||
+13
-1
@@ -3,7 +3,7 @@ set -Eeuo pipefail
|
||||
|
||||
REPO_URL=${PLUGIN_REPO_URL:-https://git.awaioi.com/awaioi/sub2api-add.git}
|
||||
SOURCE_DIR=${PLUGIN_SOURCE_DIR:-/opt/sub2api-add}
|
||||
REF=${PLUGIN_REF:-v1.1.0}
|
||||
REF=${PLUGIN_REF:-v1.1.1}
|
||||
EXPECTED_SHA=${PLUGIN_COMMIT_SHA:-}
|
||||
ALLOW_MUTABLE_REF=${PLUGIN_ALLOW_MUTABLE_REF:-false}
|
||||
DEPLOY_ENV=${PLUGIN_ENV:-production}
|
||||
@@ -22,10 +22,18 @@ validate_path_components() {
|
||||
[[ "$component" != "." && "$component" != ".." ]] || die "$label 不能包含 . 或 .. 路径组件"
|
||||
current="$current/$component"
|
||||
[[ ! -L "$current" ]] || die "$label 的路径组件不能是符号链接:$current"
|
||||
[[ ! -e "$current" || -d "$current" ]] || die "$label 的路径组件不是目录:$current"
|
||||
done
|
||||
}
|
||||
|
||||
ensure_real_parent() {
|
||||
local path=$1 label=$2 parent
|
||||
parent=$(dirname -- "$path")
|
||||
[[ -d "$parent" && ! -L "$parent" ]] || die "$label 的父目录必须是已存在的真实目录:$parent"
|
||||
}
|
||||
|
||||
validate_path_components "$SOURCE_DIR" PLUGIN_SOURCE_DIR
|
||||
ensure_real_parent "$SOURCE_DIR" PLUGIN_SOURCE_DIR
|
||||
if [[ "$SOURCE_DIR" != */sub2api-add && "${PLUGIN_ALLOW_CUSTOM_PATHS:-false}" != "true" ]]; then
|
||||
die "PLUGIN_SOURCE_DIR 必须以 sub2api-add 结尾;如确需自定义路径请显式设置 PLUGIN_ALLOW_CUSTOM_PATHS=true"
|
||||
fi
|
||||
@@ -57,6 +65,10 @@ if [[ -e "$SOURCE_DIR" && ! -d "$SOURCE_DIR/.git" ]]; then
|
||||
die "$SOURCE_DIR 已存在但不是本插件仓库;请设置 PLUGIN_SOURCE_DIR"
|
||||
fi
|
||||
|
||||
if [[ ! -e "$SOURCE_DIR" ]]; then
|
||||
ensure_real_parent "$SOURCE_DIR" PLUGIN_SOURCE_DIR
|
||||
fi
|
||||
|
||||
if [[ -d "$SOURCE_DIR/.git" ]]; then
|
||||
if [[ -n "$(git -C "$SOURCE_DIR" status --porcelain)" ]]; then
|
||||
die "$SOURCE_DIR 有未提交修改,先清理后再升级"
|
||||
|
||||
+47
-5
@@ -22,12 +22,40 @@ validate_managed_root() {
|
||||
[[ "$component" != "." && "$component" != ".." ]] || die "$label 不能包含 . 或 .. 路径组件"
|
||||
current="$current/$component"
|
||||
[[ ! -L "$current" ]] || die "$label 的路径组件不能是符号链接:$current"
|
||||
[[ ! -e "$current" || -d "$current" ]] || die "$label 的路径组件不是目录:$current"
|
||||
done
|
||||
if [[ "$value" != */sub2api-add && "${PLUGIN_ALLOW_CUSTOM_PATHS:-false}" != "true" ]]; then
|
||||
die "$label 必须位于受管的 sub2api-add 目录;如确需自定义路径请显式设置 PLUGIN_ALLOW_CUSTOM_PATHS=true"
|
||||
fi
|
||||
}
|
||||
|
||||
ensure_real_parent() {
|
||||
local path=$1 label=$2 parent component current="" parts=()
|
||||
parent=$(dirname -- "$path")
|
||||
IFS='/' read -r -a parts <<< "${parent#/}"
|
||||
for component in "${parts[@]}"; do
|
||||
[[ -z "$component" ]] && continue
|
||||
current="$current/$component"
|
||||
[[ -d "$current" && ! -L "$current" ]] || die "$label 的父目录必须是已存在的真实目录:$current"
|
||||
done
|
||||
}
|
||||
|
||||
ensure_real_dir() {
|
||||
local path=$1 label=$2 component current="" parts=()
|
||||
[[ -n "$path" && "$path" = /* ]] || die "$label 路径无效"
|
||||
IFS='/' read -r -a parts <<< "${path#/}"
|
||||
for component in "${parts[@]}"; do
|
||||
[[ -z "$component" ]] && continue
|
||||
current="$current/$component"
|
||||
[[ -d "$current" && ! -L "$current" ]] || die "$label 必须是已存在的真实目录:$current"
|
||||
done
|
||||
}
|
||||
|
||||
ensure_regular_target() {
|
||||
local path=$1 label=$2
|
||||
[[ ! -L "$path" ]] || die "$label 不能是符号链接:$path"
|
||||
}
|
||||
|
||||
die() { printf '错误:%s\n' "$*" >&2; exit 1; }
|
||||
[[ $EUID -eq 0 ]] || die "请使用 root 或 sudo 运行"
|
||||
command -v systemctl >/dev/null 2>&1 || die "缺少 systemd/systemctl"
|
||||
@@ -52,6 +80,11 @@ validate_managed_root "$ETC_DIR" PLUGIN_ETC_DIR
|
||||
validate_managed_root "$VAR_DIR" PLUGIN_VAR_DIR
|
||||
validate_managed_root "$PREFIX" PLUGIN_INSTALL_PREFIX
|
||||
validate_managed_root "$SOURCE_DIR" PLUGIN_SOURCE_DIR
|
||||
ensure_real_parent "$ETC_DIR" PLUGIN_ETC_DIR
|
||||
ensure_real_parent "$VAR_DIR" PLUGIN_VAR_DIR
|
||||
ensure_real_parent "$PREFIX" PLUGIN_INSTALL_PREFIX
|
||||
ensure_real_parent "$SOURCE_DIR" PLUGIN_SOURCE_DIR
|
||||
ensure_real_dir /etc/systemd/system SYSTEMD_UNIT_DIR
|
||||
if $PURGE && [[ "${PLUGIN_PURGE_SOURCE:-true}" == "true" && "$SOURCE_DIR" != "$PREFIX" && "${PLUGIN_ALLOW_CUSTOM_PATHS:-false}" != "true" ]]; then
|
||||
die "为避免误删,--purge 默认要求 PLUGIN_SOURCE_DIR 与 PLUGIN_INSTALL_PREFIX 相同"
|
||||
fi
|
||||
@@ -64,28 +97,37 @@ fi
|
||||
|
||||
for plugin in "${PLUGINS[@]}"; do
|
||||
unit="sub2api-$plugin.service"
|
||||
unit_path="/etc/systemd/system/$unit"
|
||||
if [[ -e "$unit_path" || -L "$unit_path" ]]; then
|
||||
ensure_regular_target "$unit_path" SYSTEMD_UNIT
|
||||
fi
|
||||
systemctl disable --now "$unit" 2>/dev/null || true
|
||||
rm -f -- "/etc/systemd/system/$unit"
|
||||
rm -f -- "$unit_path"
|
||||
# Keep the checked-out source clean so a later install can fast-forward it.
|
||||
# Only generated binaries are removed unless --purge is explicitly used.
|
||||
rm -f -- "$PREFIX/$plugin/bin/$plugin"
|
||||
if [[ -e "$PREFIX/$plugin/bin/$plugin" || -L "$PREFIX/$plugin/bin/$plugin" ]]; then
|
||||
ensure_real_parent "$PREFIX/$plugin/bin/$plugin" PLUGIN_INSTALL_PREFIX
|
||||
rm -f -- "$PREFIX/$plugin/bin/$plugin"
|
||||
fi
|
||||
if $PURGE; then
|
||||
plugin_root="$PREFIX/$plugin"
|
||||
if [[ -d "$plugin_root" && ! -L "$plugin_root" ]]; then
|
||||
ensure_real_parent "$plugin_root" PLUGIN_INSTALL_PREFIX
|
||||
rm -rf -- "${plugin_root:?}"
|
||||
fi
|
||||
rm -f -- "$ETC_DIR/$plugin.env"
|
||||
plugin_data="$VAR_DIR/$plugin"
|
||||
if [[ -d "$plugin_data" && ! -L "$plugin_data" ]]; then
|
||||
ensure_real_parent "$plugin_data" PLUGIN_VAR_DIR
|
||||
rm -rf -- "${plugin_data:?}"
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
systemctl daemon-reload
|
||||
if $PURGE && [[ -d "$ETC_DIR" ]]; then rmdir "$ETC_DIR" 2>/dev/null || true; fi
|
||||
if $PURGE && [[ -d "$VAR_DIR" ]]; then rmdir "$VAR_DIR" 2>/dev/null || true; fi
|
||||
if $PURGE && [[ "${PLUGIN_PURGE_SOURCE:-true}" == "true" && "$SOURCE_DIR" == "$PREFIX" && -d "$SOURCE_DIR" && ! -L "$SOURCE_DIR" ]]; then rm -rf -- "$SOURCE_DIR"; fi
|
||||
if $PURGE && [[ -d "$ETC_DIR" && ! -L "$ETC_DIR" ]]; then rmdir "$ETC_DIR" 2>/dev/null || true; fi
|
||||
if $PURGE && [[ -d "$VAR_DIR" && ! -L "$VAR_DIR" ]]; then rmdir "$VAR_DIR" 2>/dev/null || true; fi
|
||||
if $PURGE && [[ "${PLUGIN_PURGE_SOURCE:-true}" == "true" && "$SOURCE_DIR" == "$PREFIX" && -d "$SOURCE_DIR" && ! -L "$SOURCE_DIR" ]]; then ensure_real_parent "$SOURCE_DIR" PLUGIN_SOURCE_DIR; rm -rf -- "$SOURCE_DIR"; fi
|
||||
|
||||
if $PURGE; then
|
||||
userdel "$RUN_USER" 2>/dev/null || true
|
||||
|
||||
Reference in New Issue
Block a user