两处都是「看起来成功、实际给了错误信息/留下凭据」的问题。
A. 历史条数假数据(行为变化)
旧逻辑:ADMIN_TOK=$(grep ... || true) 取不到时仍发一个空的 `Bearer `
请求;第 1 步把 history-server 改成 fail-closed 后它必然回 401 JSON,
而 `grep -o '"url"' | wc -l` 对错误响应数出 0,再被 HIST=${HIST:-0}
兜一层,于是对外打印「历史条数 0」—— 一个假数据,看起来像历史被清空。
现在抽成 human_history_count(),三条路径分开:
· 未配置 NAVI_ADMIN_TOKEN → 「未知(未配置…跳过探测)」,不发任何请求
· 非 200(401/502/连不上的 000) → 「未知(HTTP xxx)」,不数 body
· 只有 200 才数 '"url"' 出现次数
⚠️ 行为变化:token 取不到时历史条数显示「未知」而不是「0」;
真实的 0 条(200 + 空列表)仍然显示 0。仍旧绝不阻断升级。
B. 明文令牌的 env 文件残留
/tmp/navi-host-<stamp>.env 含 NAVI_ADMIN_TOKEN / NAVI_SYNC_TOKEN 明文,
chmod 600 只防别的用户读,不防它一直躺在 /tmp。现在:
· 统一为 $ENV_FILE(含 .t / .new 中间文件),不再散落字面量路径
· 新增 cleanup_env_file(),同时删三个文件
· 注册 trap ... EXIT(任何 exit 都删)+ fail() 回滚路径显式调用
+ 成功收尾显式调用(三处)
删它不影响已起容器:docker run 已在启动时把值注入容器 env。
顺带确认(未改动,仅补注释):重置门户口令后打印一次 $NEWPW 是有意设计
——那是管理员唯一一次拿到它的机会(数据卷只存哈希),注释已写明
「只打印这一次」,且不写日志文件、不进任何 env。
新增 deploy/zd-host_test.sh(bash deploy/zd-host_test.sh,退出码 0=全绿):
1. bash -n 语法检查;
2. 静态护栏:轮换只打印 ${#VAR} 长度、无 set -x、无 HIST=${HIST:-0}、
无「grep url | wc -l || echo 0」旧写法、必须校验 HTTP 状态码、
env 文件在 fail() 与 EXIT trap 与成功路径上都有删除、中间文件也删;
3. 用 BEGIN/END 标记把 human_history_count 原样抠出来,配 stub curl 单测
空 token / 401 / 502 / 连接失败 / 200 三条 / 200 空列表 六种情形,
并断言空 token 时根本没发请求、有 token 时带了 Authorization 头。
反向验证:把历史条数换回旧写法并删掉三处 env 文件清理后,
测试从 26 通过 / 0 失败变成 10 通过 / 16 失败(列出「仍有 HIST=${HIST:-0}」
「fail() 回滚路径没有删 env 文件」「空 token 时仍然调用了 curl」等)。
另外用第 1 步的真实 history-server + 模拟两层鉴权的反代做了端到端复核,
旧写法在 401/连接失败下都报 0,新写法分别报「未知(HTTP 401)」「未知(HTTP 000)」,
200 + 3 条报 3,200 + 空列表报 0。
335 lines
17 KiB
Bash
335 lines
17 KiB
Bash
#!/usr/bin/env bash
|
||
# HomeDash host 模式部署(切换成"单容器即入口"之后使用)
|
||
#
|
||
# 为什么不能用 deploy/zd.sh:
|
||
# zd.sh 靠"改门户 nginx 的上游端口 + 双端口轮换"实现零停机,前提是入口
|
||
# (portal-nginx)独立于应用容器。切换之后入口就是 navi 容器本身,
|
||
# host 网络下端口即入口,没有可轮换的第二组端口 —— 那套脚本会失效。
|
||
#
|
||
# 本脚本的策略(用户已明确接受数秒闪断):
|
||
# 构建 → 上传 → 停旧容器 → 起新容器(host 网络)→ 全面校验 → 改名
|
||
# 任一步失败:删掉新容器、把旧容器改名回去并启动(自动回滚)
|
||
#
|
||
# 用法: deploy/zd-host.sh <version>
|
||
# 环境变量: NAVI_HOST / NAVI_SSH_PASS / NAVI_DATA_DIR / NAVI_CERT_DIR / NAVI_BASE_IMAGE
|
||
set -euo pipefail
|
||
|
||
VER="${1:?用法: deploy/zd-host.sh <version>}"
|
||
TAG="homedash:ui-${VER}"
|
||
STAMP="$(date +%Y%m%d%H%M%S)"
|
||
|
||
HOST="${NAVI_HOST:-}" # 必填:NAVI_HOST=user@host(仓库不预置任何真实地址)
|
||
|
||
# 必填校验要在赋值之后(set -u 下提前引用会直接 unbound variable)
|
||
[ -n "$HOST" ] || { echo "!! 请指定目标机:NAVI_HOST=user@host bash $0 <版本>" >&2; exit 2; }
|
||
SSH_PASS="${NAVI_SSH_PASS:-}"
|
||
DATA_DIR="${NAVI_DATA_DIR:-/vol1/docker/navi-data}"
|
||
CERT_DIR="${NAVI_CERT_DIR:-/vol1/docker/cert}"
|
||
BASE_IMAGE="${NAVI_BASE_IMAGE:-jianf-navi:latest}"
|
||
|
||
SRC="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||
CTX=/tmp/homedash-ctx
|
||
TAR="/tmp/homedash-${VER}.tar.gz"
|
||
REMOTE_TAR="/tmp/homedash-${VER}-${STAMP}.tar.gz"
|
||
|
||
SSH_OPTS=(-o StrictHostKeyChecking=no)
|
||
if [ -n "$SSH_PASS" ]; then
|
||
SSH=(sshpass -p "$SSH_PASS" ssh "${SSH_OPTS[@]}" "$HOST")
|
||
SCP=(sshpass -p "$SSH_PASS" scp -q "${SSH_OPTS[@]}")
|
||
else
|
||
SSH=(ssh "${SSH_OPTS[@]}" "$HOST")
|
||
SCP=(scp -q "${SSH_OPTS[@]}")
|
||
fi
|
||
|
||
echo "== 1/3 本地构建 ${TAG}(源码 ${SRC})=="
|
||
cd "$SRC"
|
||
node --check web/navi.js
|
||
(cd admin-server && gofmt -l . && go test ./... >/dev/null)
|
||
(cd history-server && gofmt -l . && go test ./... >/dev/null)
|
||
rm -rf "$CTX"; mkdir -p "$CTX/runtime" "$CTX/web/extension" "$CTX/admin" "$CTX/portal" "$CTX/ext"
|
||
(cd history-server && CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -trimpath -ldflags='-s -w' -o "$CTX/runtime/history-server" .)
|
||
(cd admin-server && CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -trimpath -ldflags='-s -w' -o "$CTX/runtime/admin-server" .)
|
||
# 扩展包必须从当前源码重打:不能继承基镜像里的旧包。
|
||
# 实测踩坑(2026-10-02):增量 Dockerfile 只 COPY install.html,zip/xpi 全部
|
||
# 来自基镜像,导致 chrome background 的修复升级后仍不生效(包是 3 周前的)。
|
||
(cd extension/chrome && zip -qr "$CTX/ext/navi-chrome.zip" .)
|
||
(cd extension/firefox && zip -qr "$CTX/ext/navi-firefox.xpi" .)
|
||
cp nginx.conf supervisord.conf searxng-settings.yml conf.example.yml "$CTX/"
|
||
cp deploy/cert-agent.sh "$CTX/cert-agent.sh"
|
||
cp admin/index.html "$CTX/admin/index.html"
|
||
cp -a portal/. "$CTX/portal/"
|
||
cp web/extension/install.html "$CTX/web/extension/install.html"
|
||
cp web/navi.js web/navi.css web/patch-index.py "$CTX/web/"
|
||
cat > "$CTX/Dockerfile" <<EOF
|
||
FROM ${BASE_IMAGE}
|
||
COPY runtime/history-server /usr/local/bin/history-server
|
||
COPY runtime/admin-server /usr/local/bin/admin-server
|
||
COPY nginx.conf /etc/nginx/nginx.conf
|
||
COPY supervisord.conf /etc/supervisor/conf.d/supervisord.conf
|
||
COPY searxng-settings.yml /etc/searxng/settings.yml
|
||
COPY conf.example.yml /app/defaults/conf.yml
|
||
COPY admin/index.html /app/admin/index.html
|
||
COPY portal /app/portal
|
||
# 站点图标:纸飞机(覆盖 Dashy 自带的那套,双保险)
|
||
COPY portal/favicon.svg /app/dashy/favicon.svg
|
||
COPY portal/favicon.ico /app/dashy/favicon.ico
|
||
COPY portal/apple-touch-icon.png /app/dashy/apple-touch-icon.png
|
||
COPY portal/web-icons/ /app/dashy/web-icons/
|
||
# /icons/favicon.ico 才是 Dashy 前端实际用的那个:它的 JS 在运行时把
|
||
# <link rel=icon> 改写成 /icons/favicon.ico(实测:服务端注入的 /favicon.svg
|
||
# 会被它覆盖),而镜像里从来没有 /icons/ 这个目录 —— 所以标签栏一直拿不到图标,
|
||
# 请求落到 try_files 兜底的 index.html(content-type: text/html),浏览器直接忽略。
|
||
# 静态改 index.html 治不了本,必须把这个路径补上。
|
||
COPY portal/favicon.ico /app/dashy/icons/favicon.ico
|
||
COPY portal/favicon.svg /app/dashy/icons/favicon.svg
|
||
COPY web/extension/install.html /app/dashy/extension/install.html
|
||
COPY ext/navi-chrome.zip /app/dashy/extension/navi-chrome.zip
|
||
COPY ext/navi-firefox.xpi /app/dashy/extension/navi-firefox.xpi
|
||
COPY web/navi.js /app/dashy/navi.js
|
||
COPY web/navi.css /app/dashy/navi.css
|
||
COPY web/patch-index.py /tmp/patch-index.py
|
||
COPY cert-agent.sh /usr/local/bin/cert-agent
|
||
RUN chmod +x /usr/local/bin/cert-agent
|
||
RUN python3 /tmp/patch-index.py /app/dashy/index.html
|
||
HEALTHCHECK --interval=30s --timeout=5s --start-period=45s --retries=3 \\
|
||
CMD curl -fs http://localhost:18080/healthz || exit 1
|
||
EOF
|
||
DOCKER_BUILDKIT=0 docker build -q -t "$TAG" "$CTX"
|
||
docker save "$TAG" | gzip -1 > "$TAR"
|
||
echo " 镜像与包就绪: $(ls -sh "$TAR" | awk '{print $1}')"
|
||
|
||
echo "== 2/3 上传 =="
|
||
"${SCP[@]}" "$TAR" "$HOST:$REMOTE_TAR"
|
||
|
||
echo "== 3/3 远端切换(接受数秒闪断,失败自动回滚)=="
|
||
"${SSH[@]}" "TAG='$TAG' VER='$VER' STAMP='$STAMP' REMOTE_TAR='$REMOTE_TAR' \
|
||
DATA_DIR='$DATA_DIR' CERT_DIR='$CERT_DIR' \
|
||
NAVI_COOKIE_DOMAIN='${NAVI_COOKIE_DOMAIN:-}' NAVI_HOME_URL='${NAVI_HOME_URL:-}' \
|
||
NAVI_ROTATE_ADMIN_TOKEN='${NAVI_ROTATE_ADMIN_TOKEN:-}' \
|
||
NAVI_ROTATE_SYNC_TOKEN='${NAVI_ROTATE_SYNC_TOKEN:-}' bash -s" <<'REMOTE'
|
||
set -euo pipefail
|
||
|
||
# 前置检查:必须已经是 host 模式的单容器入口,否则用错脚本会出事
|
||
NET=$(docker inspect navi --format '{{.HostConfig.NetworkMode}}' 2>/dev/null || echo none)
|
||
if [ "$NET" != "host" ]; then
|
||
echo "!! navi 目前是 '$NET' 网络:本脚本只用于切换后的 host 模式单容器" >&2
|
||
echo " 切换前请用 deploy/zd.sh;或先执行正式切换。" >&2
|
||
exit 1
|
||
fi
|
||
|
||
OLD_ID=$(docker inspect navi --format '{{.Id}}')
|
||
|
||
# 该文件含**明文令牌**(NAVI_ADMIN_TOKEN / NAVI_SYNC_TOKEN 等)。chmod 600 只防别的
|
||
# 用户读,不能防它一直躺在 /tmp 里,所以成功、失败回滚、以及任何 exit 都要删。
|
||
# 删它不影响已起的容器:docker run 已在启动时把值注入容器 env(--env-file 只读一次)。
|
||
ENV_FILE="/tmp/navi-host-${STAMP}.env"
|
||
cleanup_env_file() {
|
||
rm -f "$ENV_FILE" "$ENV_FILE.t" "$ENV_FILE.new" 2>/dev/null || true
|
||
}
|
||
trap cleanup_env_file EXIT
|
||
|
||
docker inspect navi --format '{{range .Config.Env}}{{println .}}{{end}}' \
|
||
| grep -E '^(NAVI_ADMIN_TOKEN|NAVI_SYNC_TOKEN|SEARXNG_BASE_URL|PORTAL_|DEBIAN_FRONTEND|CERT_)' > "$ENV_FILE" || true
|
||
chmod 600 "$ENV_FILE"
|
||
|
||
# ① 不再把明文门户口令带进新容器(docker inspect 就能看到它)。
|
||
# 实际生效的是数据卷里的加盐哈希,所以丢掉 env 不影响登录;
|
||
# 稍后会在新容器里写入一个强度更高的随机口令并打印一次。
|
||
CLEAN_PORTAL_PW=0
|
||
if grep -q '^PORTAL_PASSWORD=' "$ENV_FILE"; then
|
||
grep -v '^PORTAL_PASSWORD=' "$ENV_FILE" > "$ENV_FILE.new" && mv "$ENV_FILE.new" "$ENV_FILE"
|
||
CLEAN_PORTAL_PW=1
|
||
fi
|
||
|
||
# ③ 令牌轮换:渗透报告证明旧 NAVI_ADMIN_TOKEN / NAVI_SYNC_TOKEN 已泄露
|
||
# (/api/sync-info 在公网明文回显过 syncToken),必须支持在部署时换新值。
|
||
# 以前本脚本只从旧容器继承 env,导致轮换无处落地。
|
||
# 用法:NAVI_ROTATE_ADMIN_TOKEN=<新值> NAVI_ROTATE_SYNC_TOKEN=<新值> bash deploy/zd-host.sh <ver>
|
||
# 只打印长度,不打印值。
|
||
if [ -n "${NAVI_ROTATE_ADMIN_TOKEN:-}" ]; then
|
||
if [ "${#NAVI_ROTATE_ADMIN_TOKEN}" -lt 32 ]; then
|
||
echo "!! NAVI_ROTATE_ADMIN_TOKEN 太短(${#NAVI_ROTATE_ADMIN_TOKEN} 字符),至少 32" >&2
|
||
exit 2
|
||
fi
|
||
grep -v '^NAVI_ADMIN_TOKEN=' "$ENV_FILE" > "$ENV_FILE.t" || true
|
||
echo "NAVI_ADMIN_TOKEN=${NAVI_ROTATE_ADMIN_TOKEN}" >> "$ENV_FILE.t"
|
||
mv "$ENV_FILE.t" "$ENV_FILE"
|
||
echo " 已轮换 NAVI_ADMIN_TOKEN(新值长度 ${#NAVI_ROTATE_ADMIN_TOKEN})"
|
||
fi
|
||
if [ -n "${NAVI_ROTATE_SYNC_TOKEN:-}" ]; then
|
||
if [ "${#NAVI_ROTATE_SYNC_TOKEN}" -lt 32 ]; then
|
||
echo "!! NAVI_ROTATE_SYNC_TOKEN 太短(${#NAVI_ROTATE_SYNC_TOKEN} 字符),至少 32" >&2
|
||
exit 2
|
||
fi
|
||
grep -v '^NAVI_SYNC_TOKEN=' "$ENV_FILE" > "$ENV_FILE.t" || true
|
||
echo "NAVI_SYNC_TOKEN=${NAVI_ROTATE_SYNC_TOKEN}" >> "$ENV_FILE.t"
|
||
mv "$ENV_FILE.t" "$ENV_FILE"
|
||
echo " 已轮换 NAVI_SYNC_TOKEN(新值长度 ${#NAVI_ROTATE_SYNC_TOKEN})"
|
||
fi
|
||
chmod 600 "$ENV_FILE"
|
||
|
||
# ② 跨子域单点登录所需变量:新版已移除代码里的写死默认值,
|
||
# 不显式提供的话登录 cookie 会退化成 host-only(登录后换子域就掉线)。
|
||
if ! grep -q '^PORTAL_COOKIE_DOMAIN=' "$ENV_FILE"; then
|
||
if [ -n "${NAVI_COOKIE_DOMAIN:-}" ]; then
|
||
echo "PORTAL_COOKIE_DOMAIN=${NAVI_COOKIE_DOMAIN}" >> "$ENV_FILE"
|
||
else
|
||
echo "!! 缺少 PORTAL_COOKIE_DOMAIN:新版已删除代码默认值,直接升级会让跨子域单点登录失效。" >&2
|
||
echo " 执行方式:NAVI_COOKIE_DOMAIN=.你的域名 NAVI_HOME_URL=https://你的门户/ NAVI_SSH_PASS=... bash deploy/zd-host.sh $VER" >&2
|
||
exit 2
|
||
fi
|
||
fi
|
||
if ! grep -q '^PORTAL_HOME_URL=' "$ENV_FILE" && [ -n "${NAVI_HOME_URL:-}" ]; then
|
||
echo "PORTAL_HOME_URL=${NAVI_HOME_URL}" >> "$ENV_FILE"
|
||
fi
|
||
OLD_IMAGE=$(docker inspect navi --format '{{.Config.Image}}')
|
||
|
||
PREV="navi-prev-${STAMP}"
|
||
fail() {
|
||
rc=$?
|
||
echo "!! 失败(rc=$rc)→ 回滚到 ${OLD_IMAGE}" >&2
|
||
cleanup_env_file
|
||
docker rm -f navi-next >/dev/null 2>&1 || true
|
||
if ! docker inspect "$PREV" >/dev/null 2>&1; then
|
||
# 还没改名就把原容器起回来
|
||
docker start navi >/dev/null 2>&1 || true
|
||
else
|
||
docker rename "$PREV" navi >/dev/null 2>&1 || true
|
||
docker start navi >/dev/null 2>&1 || true
|
||
fi
|
||
sleep 2
|
||
echo " 回滚后状态: $(docker inspect navi --format '{{.State.Status}} {{.Config.Image}}' 2>/dev/null)" >&2
|
||
exit $rc
|
||
}
|
||
trap fail ERR
|
||
|
||
gzip -dc "$REMOTE_TAR" | docker load >/dev/null
|
||
|
||
echo " 停旧容器(闪断开始)"
|
||
docker stop navi >/dev/null
|
||
docker rename navi "$PREV"
|
||
|
||
docker run -d --name navi-next --restart unless-stopped --network host \
|
||
-v "${DATA_DIR}:/app/data" -v "${CERT_DIR}:/etc/nginx/certs" \
|
||
--env-file "$ENV_FILE" "homedash:ui-${VER}" >/dev/null
|
||
|
||
w200() { for _ in $(seq 1 60); do [ "$(curl -s -o /dev/null -w '%{http_code}' -m 3 "$1" 2>/dev/null || true)" = "200" ] && { echo " ready $1"; return 0; }; sleep 2; done; echo " TIMEOUT $1" >&2; return 1; }
|
||
w200 "http://127.0.0.1:18080/healthz"
|
||
w200 "http://127.0.0.1:18080/conf.yml"
|
||
w200 "http://127.0.0.1:18080/admin"
|
||
|
||
# 入口自检:探测域名不写死(脱敏时曾把真实域名换成占位符,导致自检去探测
|
||
# 一个路由表里不存在的域名而误判失败)。默认取路由表第一条的 host,
|
||
# 也可用 NAVI_CHECK_HOST 指定。apply 触发的 reload 是异步的,先等就绪。
|
||
CHECK_HOST="${NAVI_CHECK_HOST:-}"
|
||
if [ -z "$CHECK_HOST" ]; then
|
||
# 只用无引号的字符类提取域名,避免 tr/sed 的引号嵌套把远端脚本打断
|
||
CHECK_HOST=$(grep -m1 'host:' "$DATA_DIR/gateway.yml" 2>/dev/null | grep -oE '[A-Za-z0-9][A-Za-z0-9.-]*\.[A-Za-z]{2,}' | head -1 || true)
|
||
fi
|
||
[ -n "$CHECK_HOST" ] || { echo "!! 无法确定自检域名:请传 NAVI_CHECK_HOST=<某个已配置的域名>" >&2; false; }
|
||
echo " 自检探测域名: $CHECK_HOST"
|
||
|
||
for _w in $(seq 1 20); do
|
||
_c=$(curl -sk -o /dev/null -w '%{http_code}' -m 3 --resolve "$CHECK_HOST:3080:127.0.0.1" \
|
||
"https://$CHECK_HOST:3080/" 2>/dev/null || echo 000)
|
||
[ "$_c" != "000" ] && break
|
||
sleep 0.5
|
||
done
|
||
|
||
# 已配置的域名必须可服务:门户可能是 200,受保护的是 302 —— 两者都算正常;
|
||
# 000(没监听)/404(不在路由表)/5xx(上游或配置坏了)都算失败。
|
||
CODE=$(curl -sk -o /dev/null -w '%{http_code}' -m 8 --resolve "$CHECK_HOST:3080:127.0.0.1" "https://$CHECK_HOST:3080/")
|
||
case "$CODE" in
|
||
200|302) echo " 已配置域名 $CHECK_HOST → $CODE" ;;
|
||
*) echo "!! 已配置域名 $CHECK_HOST 应 200/302,实际 $CODE" >&2; false ;;
|
||
esac
|
||
CODE=$(curl -sk -o /dev/null -w '%{http_code}' -m 8 --resolve nobody.invalid:3080:127.0.0.1 https://nobody.invalid:3080/)
|
||
[ "$CODE" = "404" ] || { echo "!! 未知域名应 404,实际 $CODE" >&2; false; }
|
||
CODE=$(curl -s -o /dev/null -w '%{http_code}' -m 8 -H "Host: $CHECK_HOST" http://127.0.0.1:3081/)
|
||
[ "$CODE" = "302" ] || { echo "!! HTTP 入口应 302 跳转,实际 $CODE" >&2; false; }
|
||
echo " 入口自检通过(3080/3081)"
|
||
|
||
# 让容器按当前路由表重新生成一次网关配置:镜像里的门户模板/错误页可能已更新,
|
||
# 而线上 gateway.conf 是上一次生成的(本轮真实事故:门户页占位符裸露)。
|
||
ADMIN_TOK=$(grep '^NAVI_ADMIN_TOKEN=' "$ENV_FILE" | cut -d= -f2- || true)
|
||
if [ -n "$ADMIN_TOK" ]; then
|
||
APPLY=$(curl -s -X POST -H "Authorization: Bearer $ADMIN_TOK" http://127.0.0.1:3005/api/gateway/apply || true)
|
||
echo " 已重新生成网关配置: ${APPLY}"
|
||
case "$APPLY" in *applied*) ;; *) echo "!! 网关配置重新生成失败" >&2; false ;; esac
|
||
|
||
# ③ 旧容器用过明文 PORTAL_PASSWORD 时,换成强随机口令并只打印一次
|
||
# (失败不影响升级:可在后台「统一登录账号」自行设置)
|
||
# 只清理明文 env(生效凭据来自数据卷 portal-auth.json,删掉 env 不影响登录)。
|
||
# 重置口令会造成"所有受保护域名都要用新口令登录",属高风险操作,
|
||
# 因此必须显式开启 NAVI_ROTATE_PORTAL_PASSWORD=1 才执行。
|
||
if [ "${CLEAN_PORTAL_PW:-0}" = "1" ] && [ "${NAVI_ROTATE_PORTAL_PASSWORD:-0}" != "1" ]; then
|
||
echo " 已丢弃旧容器 env 里的明文 PORTAL_PASSWORD;门户口令保持不变(如确需更换,后台可改,或传 NAVI_ROTATE_PORTAL_PASSWORD=1)"
|
||
fi
|
||
if [ "${CLEAN_PORTAL_PW:-0}" = "1" ] && [ "${NAVI_ROTATE_PORTAL_PASSWORD:-0}" = "1" ]; then
|
||
NEWPW="$( (openssl rand -base64 24 2>/dev/null || head -c 24 /dev/urandom | base64) | tr -d '/+=' | cut -c1-20)"
|
||
RESP=$(curl -s -X POST -H "Authorization: Bearer $ADMIN_TOK" -H 'Content-Type: application/json' \
|
||
-d "{\"username\":\"admin\",\"password\":\"$NEWPW\"}" http://127.0.0.1:3005/api/portal-auth || true)
|
||
case "$RESP" in *error*|*Error*) echo " ! 门户口令重置失败(可在后台改):$RESP" >&2 ;;
|
||
# 这里**有意**把新口令打印一次:这是管理员唯一能拿到它的机会(数据卷里只存哈希)。
|
||
# 打印到此为止 —— 不写日志文件、不写进任何 env/注释,也不在任何其它路径回显。
|
||
*) echo " 已清除明文 env 并重置门户口令 → admin / $NEWPW(请立即记录,只打印这一次)" ;; esac
|
||
fi
|
||
fi
|
||
|
||
# 历史条数只用于人工对比,不应成为升级的阻断点。
|
||
# 2026-10-22:管理 API 改为默认拒绝后,/admin/api/* 需要登录会话;
|
||
# 这里用管理员 token 访问(v107+ 支持 Bearer),拿不到就不显示而不报错。
|
||
#
|
||
# 2026-10-22 审计补修:以前取不到 token 时仍会发一个空的 `Bearer ` 请求,
|
||
# history-server fail-closed 回 401 JSON,`grep -o '"url"' | wc -l` 数出 0,
|
||
# 于是对外**打印「历史条数 0」——一个假数据**(看起来像历史被清空了)。
|
||
# 现在三条路径分开:无 token → 「未知(跳过)」;非 200 → 「未知(HTTP xxx)」;
|
||
# 只有 200 才数 body。仍然绝不阻断升级。
|
||
# >>> human_history_count BEGIN(deploy/zd-host_test.sh 会抽取这一段做单测,改动请保留标记)
|
||
human_history_count() {
|
||
# $1 = 管理员 token(可空)
|
||
local tok="$1"
|
||
if [ -z "$tok" ]; then
|
||
echo "未知(未配置 NAVI_ADMIN_TOKEN,跳过探测)"
|
||
return 0
|
||
fi
|
||
local tmp code body n
|
||
tmp=$(mktemp 2>/dev/null || echo "/tmp/navi-hist-${STAMP}")
|
||
# curl 连不上时仍会把 -w 的 000 写出来并返回非 0,所以先 `|| true` 再归一化,
|
||
# 不能 `|| echo 000`(那会拼成 "000\n000" 打进提示里)。
|
||
code=$(curl -s -m 8 -o "$tmp" -w '%{http_code}' \
|
||
-H "Authorization: Bearer ${tok}" \
|
||
"http://127.0.0.1:18080/admin/api/history" 2>/dev/null || true)
|
||
code=$(printf '%s' "$code" | tr -d '[:space:]')
|
||
[ -n "$code" ] || code=000
|
||
if [ "$code" != "200" ]; then
|
||
rm -f "$tmp"
|
||
echo "未知(HTTP ${code};接口要求管理员 token,失败不影响升级)"
|
||
return 0
|
||
fi
|
||
body=$(cat "$tmp" 2>/dev/null || true)
|
||
rm -f "$tmp"
|
||
n=$(printf '%s' "$body" | grep -o '"url"' | wc -l || true)
|
||
echo "${n:-0}"
|
||
}
|
||
# <<< human_history_count END
|
||
HIST=$(human_history_count "${ADMIN_TOK:-}")
|
||
echo " 历史条数 ${HIST}(同步接口只累加不删除;与切换前对比由人工确认)"
|
||
|
||
docker rename navi-next navi
|
||
trap - ERR
|
||
docker rm -f "$PREV" >/dev/null 2>&1 || true
|
||
rm -f "$REMOTE_TAR"
|
||
cleanup_env_file # 明文令牌的 /tmp env 文件必须删(成功路径)
|
||
# 收尾断言:名字必须回到 navi。中途异常中断曾导致容器留在 navi-next,
|
||
# 而脚本"看起来"跑完了(本轮真实事故),所以这里显式检查。
|
||
if ! docker inspect navi >/dev/null 2>&1; then
|
||
if docker inspect navi-next >/dev/null 2>&1; then
|
||
echo "!! 容器仍名为 navi-next(切换未收尾):已核验可直接 docker rename navi-next navi" >&2
|
||
fi
|
||
false
|
||
fi
|
||
echo "DEPLOY_OK tag=homedash:ui-${VER} mode=host health=$(docker inspect navi --format '{{if .State.Health}}{{.State.Health.Status}}{{else}}{{.State.Status}}{{end}}')"
|
||
echo "闪断结束(旧容器已删除;镜像 ${OLD_IMAGE} 仍可用于手动回滚)"
|
||
REMOTE
|