用户要求:备案号与域名都应当可配置,仓库内容要脱敏。 功能(不只是替换字面量): - 被代理页面注入的备案号改为读后台「站点信息」的 icp/gongan,**未配置就不注入**; 原先写死具体备案号,等于每个部署者都会把别人的备案信息印到自己页面上 - 注入内容做 nginx 安全处理(去引号/反斜杠/换行/美元符),并有测试覆盖 - auth.go:PORTAL_COOKIE_DOMAIN / PORTAL_HOME_URL 不再预置域名, cookie 默认 host-only、首页默认相对路径 "/" - gateway.go:默认 loginUrl 不再预置域名(受保护路由缺配置会明确报错,不静默生成跳不出去的登录页) - 首装脚本在给了 NAVI_DOMAIN 时自动注入 PORTAL_COOKIE_DOMAIN/PORTAL_HOME_URL, 避免"去掉写死默认值"导致跨子域单点登录失效 脱敏:内网 IP(10.0.0.10)与真实备案号清零;真实域名仅剩门户页内容 8 处(待定); 测试夹具/示例/错误页/脚本/后台占位提示全部通用化。 README:新增「跨子域单点登录需要的两个环境变量」;环境变量表改为 NAVI_HOST 必填。 测试:新增备案号注入三条(用配置、未配置不注入、危险字符清理)。
160 lines
7.6 KiB
Bash
160 lines
7.6 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)"
|
||
|
||
[ -n "$HOST" ] || { echo "!! 请指定目标机:NAVI_HOST=user@host bash $0 ..." >&2; exit 2; }
|
||
HOST="${NAVI_HOST:-}" # 必填:NAVI_HOST=user@host(仓库不预置任何真实地址)
|
||
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"
|
||
(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" .)
|
||
cp nginx.conf supervisord.conf searxng-settings.yml conf.example.yml "$CTX/"
|
||
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
|
||
COPY web/extension/install.html /app/dashy/extension/install.html
|
||
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
|
||
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' 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}}')
|
||
docker inspect navi --format '{{range .Config.Env}}{{println .}}{{end}}' \
|
||
| grep -E '^(NAVI_ADMIN_TOKEN|NAVI_SYNC_TOKEN|SEARXNG_BASE_URL|PORTAL_|DEBIAN_FRONTEND)' > "/tmp/navi-host-${STAMP}.env" || true
|
||
chmod 600 "/tmp/navi-host-${STAMP}.env"
|
||
OLD_IMAGE=$(docker inspect navi --format '{{.Config.Image}}')
|
||
|
||
PREV="navi-prev-${STAMP}"
|
||
fail() {
|
||
rc=$?
|
||
echo "!! 失败(rc=$rc)→ 回滚到 ${OLD_IMAGE}" >&2
|
||
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:ro" \
|
||
--env-file "/tmp/navi-host-${STAMP}.env" "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"
|
||
|
||
# 入口自检:受保护域名应 302、公开域名应非 5xx、未知域名应 404(自定义页)
|
||
CODE=$(curl -sk -o /dev/null -w '%{http_code}' -m 8 --resolve dashy.example.com:3080:127.0.0.1 https://dashy.example.com:3080/conf.yml)
|
||
[ "$CODE" = "302" ] || { echo "!! 受保护域名应 302,实际 $CODE" >&2; false; }
|
||
CODE=$(curl -sk -o /dev/null -w '%{http_code}' -m 8 --resolve nobody.test:3080:127.0.0.1 https://nobody.test:3080/)
|
||
[ "$CODE" = "404" ] || { echo "!! 未知域名应 404,实际 $CODE" >&2; false; }
|
||
CODE=$(curl -s -o /dev/null -w '%{http_code}' -m 8 -H 'Host: dashy.example.com' 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=' "/tmp/navi-host-${STAMP}.env" | 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
|
||
fi
|
||
|
||
HIST=$(curl -s "http://127.0.0.1:18080/admin/api/history" | grep -o '"url"' | wc -l)
|
||
echo " 历史条数 ${HIST}(同步接口只累加不删除;与切换前对比由人工确认)"
|
||
|
||
docker rename navi-next navi
|
||
trap - ERR
|
||
docker rm -f "$PREV" >/dev/null 2>&1 || true
|
||
rm -f "$REMOTE_TAR"
|
||
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
|