feat: complete unified plugin admin v1.1.0
This commit is contained in:
+24
-8
@@ -22,22 +22,35 @@ docker compose -f docker-compose.yml up -d
|
||||
在一台已安装 Go 1.23+、Git 和 systemd 的 Linux 主机上:
|
||||
|
||||
```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
|
||||
```
|
||||
|
||||
生产安装必须固定 `PLUGIN_COMMIT_SHA`,并从受信任的发布记录复制
|
||||
`RELEASE_SHA`。tag 只是选择版本,不是完整性证明。开发环境如需使用
|
||||
可变分支或未 pin 的 tag,必须显式设置对应的开发开关。
|
||||
|
||||
```sh
|
||||
export PLUGIN_ENV=development PLUGIN_REF=main PLUGIN_ALLOW_MUTABLE_REF=true
|
||||
curl -fsSL https://git.awaioi.com/awaioi/sub2api-add/raw/branch/main/deploy/install.sh \
|
||||
| sudo bash -s -- --plugin all
|
||||
| sudo -E bash -s -- --plugin all
|
||||
```
|
||||
|
||||
只安装控制面:
|
||||
|
||||
```sh
|
||||
curl -fsSL https://git.awaioi.com/awaioi/sub2api-add/raw/branch/main/deploy/install.sh \
|
||||
| sudo bash -s -- --plugin plugin-admin
|
||||
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
|
||||
```
|
||||
|
||||
安装订阅插件:
|
||||
|
||||
```sh
|
||||
curl -fsSL https://git.awaioi.com/awaioi/sub2api-add/raw/branch/main/deploy/install.sh \
|
||||
| sudo bash -s -- --plugin subscription-admin
|
||||
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
|
||||
```
|
||||
|
||||
默认安装位置:
|
||||
@@ -63,7 +76,8 @@ sudo systemctl restart sub2api-plugin-admin sub2api-subscription-admin
|
||||
`PLUGIN_CONFIG_KEY` 和受信发布者公钥。插件市场默认读取
|
||||
`/var/lib/sub2api-add/plugin-admin/marketplace/index.json`;可通过
|
||||
`PLUGIN_MARKETPLACE_INDEX` 指向受控本地索引或 HTTPS 索引,并用
|
||||
`PLUGIN_MARKETPLACE_ALLOWED_HOSTS` 限定远程索引和包的精确主机。
|
||||
`PLUGIN_MARKETPLACE_ALLOWED_HOSTS` 限定远程索引和包的精确主机。生产远程索引
|
||||
还必须设置 `PLUGIN_MARKETPLACE_INDEX_SHA256`,把目录内容固定到已审核的摘要。
|
||||
|
||||
## 菜单接入
|
||||
|
||||
@@ -81,14 +95,16 @@ sudo systemctl restart sub2api-plugin-admin sub2api-subscription-admin
|
||||
默认卸载服务和二进制,但保留配置与插件数据,便于重新安装:
|
||||
|
||||
```sh
|
||||
curl -fsSL https://git.awaioi.com/awaioi/sub2api-add/raw/branch/main/deploy/uninstall.sh \
|
||||
RELEASE_SHA=COMMIT_SHA_40_HEX
|
||||
curl -fsSL "https://git.awaioi.com/awaioi/sub2api-add/raw/commit/${RELEASE_SHA}/deploy/uninstall.sh" \
|
||||
| sudo bash -s -- --plugin all
|
||||
```
|
||||
|
||||
确认同时删除插件配置、插件数据和源码:
|
||||
|
||||
```sh
|
||||
curl -fsSL https://git.awaioi.com/awaioi/sub2api-add/raw/branch/main/deploy/uninstall.sh \
|
||||
RELEASE_SHA=COMMIT_SHA_40_HEX
|
||||
curl -fsSL "https://git.awaioi.com/awaioi/sub2api-add/raw/commit/${RELEASE_SHA}/deploy/uninstall.sh" \
|
||||
| sudo bash -s -- --plugin all --purge --yes
|
||||
```
|
||||
|
||||
|
||||
+63
-3
@@ -3,12 +3,56 @@ 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:-main}
|
||||
REF=${PLUGIN_REF:-v1.1.0}
|
||||
EXPECTED_SHA=${PLUGIN_COMMIT_SHA:-}
|
||||
ALLOW_MUTABLE_REF=${PLUGIN_ALLOW_MUTABLE_REF:-false}
|
||||
DEPLOY_ENV=${PLUGIN_ENV:-production}
|
||||
ALLOW_UNPINNED_TAG=${PLUGIN_ALLOW_UNPINNED_TAG:-false}
|
||||
|
||||
die() { printf '错误:%s\n' "$*" >&2; exit 1; }
|
||||
|
||||
validate_path_components() {
|
||||
local value=$1 label=$2 component current=""
|
||||
[[ -n "$value" && "$value" = /* && "$value" != "/" ]] || die "$label 必须是非根绝对路径"
|
||||
[[ "$value" != *$'\n'* && "$value" != *$'\r'* ]] || die "$label 包含非法换行"
|
||||
local parts=()
|
||||
IFS='/' read -r -a parts <<< "${value#/}"
|
||||
for component in "${parts[@]}"; do
|
||||
[[ -z "$component" ]] && continue
|
||||
[[ "$component" != "." && "$component" != ".." ]] || die "$label 不能包含 . 或 .. 路径组件"
|
||||
current="$current/$component"
|
||||
[[ ! -L "$current" ]] || die "$label 的路径组件不能是符号链接:$current"
|
||||
done
|
||||
}
|
||||
|
||||
validate_path_components "$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
|
||||
[[ $EUID -eq 0 ]] || die "请使用 root 或 sudo 运行"
|
||||
command -v git >/dev/null 2>&1 || die "缺少 git"
|
||||
|
||||
is_sha() { [[ "$1" =~ ^[0-9a-fA-F]{40}$ ]]; }
|
||||
is_release_tag() { [[ "$1" =~ ^v[0-9]+\.[0-9]+\.[0-9]+([.-][0-9A-Za-z.-]+)?$ ]]; }
|
||||
[[ "$DEPLOY_ENV" == "development" || "$DEPLOY_ENV" == "production" ]] || die "PLUGIN_ENV 必须是 development 或 production"
|
||||
if [[ -n "$EXPECTED_SHA" ]] && ! is_sha "$EXPECTED_SHA"; then
|
||||
die "PLUGIN_COMMIT_SHA 必须是 40 位 commit SHA"
|
||||
fi
|
||||
if [[ "$ALLOW_MUTABLE_REF" == "true" && "$DEPLOY_ENV" != "development" ]]; then
|
||||
die "PLUGIN_ALLOW_MUTABLE_REF=true 仅允许在 PLUGIN_ENV=development 使用"
|
||||
fi
|
||||
if ! is_sha "$REF" && ! is_release_tag "$REF" && [[ "$ALLOW_MUTABLE_REF" != "true" ]]; then
|
||||
die "默认只接受不可变 commit SHA 或版本 tag;开发环境显式设置 PLUGIN_ALLOW_MUTABLE_REF=true 才能使用分支"
|
||||
fi
|
||||
if [[ -z "$EXPECTED_SHA" ]] && is_release_tag "$REF"; then
|
||||
if [[ "$DEPLOY_ENV" != "development" || "$ALLOW_UNPINNED_TAG" != "true" ]]; then
|
||||
die "使用版本 tag 时必须设置 PLUGIN_COMMIT_SHA;仅开发环境显式设置 PLUGIN_ALLOW_UNPINNED_TAG=true 才能跳过 pin"
|
||||
fi
|
||||
fi
|
||||
if [[ "$ALLOW_UNPINNED_TAG" == "true" && "$DEPLOY_ENV" != "development" ]]; then
|
||||
die "PLUGIN_ALLOW_UNPINNED_TAG=true 仅允许在 PLUGIN_ENV=development 使用"
|
||||
fi
|
||||
|
||||
if [[ -e "$SOURCE_DIR" && ! -d "$SOURCE_DIR/.git" ]]; then
|
||||
die "$SOURCE_DIR 已存在但不是本插件仓库;请设置 PLUGIN_SOURCE_DIR"
|
||||
fi
|
||||
@@ -17,10 +61,26 @@ if [[ -d "$SOURCE_DIR/.git" ]]; then
|
||||
if [[ -n "$(git -C "$SOURCE_DIR" status --porcelain)" ]]; then
|
||||
die "$SOURCE_DIR 有未提交修改,先清理后再升级"
|
||||
fi
|
||||
git -C "$SOURCE_DIR" fetch --depth 1 origin "$REF"
|
||||
git -C "$SOURCE_DIR" fetch --depth 1 --no-tags origin "$REF"
|
||||
git -C "$SOURCE_DIR" checkout --force FETCH_HEAD
|
||||
else
|
||||
git clone --depth 1 --branch "$REF" "$REPO_URL" "$SOURCE_DIR"
|
||||
if is_sha "$REF"; then
|
||||
git clone --depth 1 --no-tags "$REPO_URL" "$SOURCE_DIR"
|
||||
git -C "$SOURCE_DIR" fetch --depth 1 --no-tags origin "$REF"
|
||||
git -C "$SOURCE_DIR" checkout --force FETCH_HEAD
|
||||
else
|
||||
git clone --depth 1 --no-tags --branch "$REF" "$REPO_URL" "$SOURCE_DIR"
|
||||
fi
|
||||
fi
|
||||
|
||||
RESOLVED_SHA=$(git -C "$SOURCE_DIR" rev-parse HEAD)
|
||||
NORMALIZED_RESOLVED_SHA=$(printf '%s' "$RESOLVED_SHA" | tr '[:upper:]' '[:lower:]')
|
||||
NORMALIZED_EXPECTED_SHA=$(printf '%s' "$EXPECTED_SHA" | tr '[:upper:]' '[:lower:]')
|
||||
if [[ -n "$EXPECTED_SHA" && "$NORMALIZED_RESOLVED_SHA" != "$NORMALIZED_EXPECTED_SHA" ]]; then
|
||||
die "检出的 commit 与 PLUGIN_COMMIT_SHA 不一致:$RESOLVED_SHA"
|
||||
fi
|
||||
if ! is_sha "$REF" && ! is_release_tag "$REF" && [[ "$ALLOW_MUTABLE_REF" == "true" ]]; then
|
||||
printf '%s\n' '警告:当前使用可变 Git ref,仅限开发环境。' >&2
|
||||
fi
|
||||
|
||||
exec "$SOURCE_DIR/scripts/install-local.sh" "$@"
|
||||
|
||||
+39
-6
@@ -10,8 +10,27 @@ SELECTION=all
|
||||
PURGE=false
|
||||
YES=false
|
||||
|
||||
validate_managed_root() {
|
||||
local value=$1 label=$2 component current=""
|
||||
[[ -n "$value" && "$value" = /* ]] || die "$label 必须是绝对路径"
|
||||
[[ "$value" != *$'\n'* && "$value" != *$'\r'* ]] || die "$label 包含非法换行"
|
||||
[[ "$value" != "/" ]] || die "$label 不允许指向根目录"
|
||||
local parts=()
|
||||
IFS='/' read -r -a parts <<< "${value#/}"
|
||||
for component in "${parts[@]}"; do
|
||||
[[ -z "$component" ]] && continue
|
||||
[[ "$component" != "." && "$component" != ".." ]] || die "$label 不能包含 . 或 .. 路径组件"
|
||||
current="$current/$component"
|
||||
[[ ! -L "$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
|
||||
}
|
||||
|
||||
die() { printf '错误:%s\n' "$*" >&2; exit 1; }
|
||||
[[ $EUID -eq 0 ]] || die "请使用 root 或 sudo 运行"
|
||||
command -v systemctl >/dev/null 2>&1 || die "缺少 systemd/systemctl"
|
||||
|
||||
while (($#)); do
|
||||
case "$1" in
|
||||
@@ -29,6 +48,14 @@ case "$SELECTION" in
|
||||
*) die "插件必须是 all、plugin-admin 或 subscription-admin" ;;
|
||||
esac
|
||||
|
||||
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
|
||||
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
|
||||
|
||||
if $PURGE && ! $YES; then
|
||||
[[ -t 0 ]] || die "非交互环境执行 --purge 时必须同时提供 --yes"
|
||||
read -r -p "将删除插件配置和数据,继续?[y/N] " answer
|
||||
@@ -38,21 +65,27 @@ fi
|
||||
for plugin in "${PLUGINS[@]}"; do
|
||||
unit="sub2api-$plugin.service"
|
||||
systemctl disable --now "$unit" 2>/dev/null || true
|
||||
rm -f "/etc/systemd/system/$unit"
|
||||
rm -f -- "/etc/systemd/system/$unit"
|
||||
# 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"
|
||||
rm -f -- "$PREFIX/$plugin/bin/$plugin"
|
||||
if $PURGE; then
|
||||
rm -rf "$PREFIX/$plugin"
|
||||
rm -f "$ETC_DIR/$plugin.env"
|
||||
rm -rf "$VAR_DIR/$plugin"
|
||||
plugin_root="$PREFIX/$plugin"
|
||||
if [[ -d "$plugin_root" && ! -L "$plugin_root" ]]; then
|
||||
rm -rf -- "${plugin_root:?}"
|
||||
fi
|
||||
rm -f -- "$ETC_DIR/$plugin.env"
|
||||
plugin_data="$VAR_DIR/$plugin"
|
||||
if [[ -d "$plugin_data" && ! -L "$plugin_data" ]]; then
|
||||
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 && [[ -d "$SOURCE_DIR" ]]; then rm -rf "$SOURCE_DIR"; 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; then
|
||||
userdel "$RUN_USER" 2>/dev/null || true
|
||||
|
||||
Reference in New Issue
Block a user