fix(install): 安装向导 Redis 密码不被 nx update/Compose 覆盖

Compose 不再注入 NEXUS_REDIS_URL;nx update 保留卷内带密码 URL;步骤 4 可从 install state 修复无密码 URL。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Nexus Deploy
2026-06-06 11:34:26 +08:00
parent beb802802a
commit 850038a3ec
5 changed files with 102 additions and 5 deletions
+34 -2
View File
@@ -200,6 +200,32 @@ upsert_env_var() {
fi
}
# Prefer wizard-written redis://:pass@host from nexus-state or .env.prod; avoid clobbering on nx update.
resolve_nexus_redis_url() {
local redis_host="$1"
local env_file="$2"
local vol_url="" existing="" host_lc="${redis_host,,}"
local state_vol
state_vol="$(docker volume ls -q --filter name=nexus-state 2>/dev/null | head -1 || true)"
if [[ -n "$state_vol" ]]; then
vol_url="$(docker run --rm -v "${state_vol}:/data" alpine sh -c \
"grep '^NEXUS_REDIS_URL=' /data/.env 2>/dev/null | cut -d= -f2- | tr -d '\"'" 2>/dev/null || true)"
if [[ -n "$vol_url" && "${vol_url,,}" == *"${host_lc}"* ]]; then
echo "$vol_url"
return 0
fi
fi
if [[ -f "$env_file" ]]; then
existing="$(grep '^NEXUS_REDIS_URL=' "$env_file" 2>/dev/null | cut -d= -f2- | tr -d '"' || true)"
if [[ -n "$existing" && "$existing" == *"@"* && "${existing,,}" == *"${host_lc}"* ]]; then
echo "$existing"
return 0
fi
fi
echo "redis://${redis_host}:6379/0"
}
write_1panel_hosts_volume() {
local mysql_host="${1:-}" redis_host="${2:-}"
[[ -n "$mysql_host" || -n "$redis_host" ]] || return 0
@@ -257,8 +283,14 @@ sync_1panel_service_hosts() {
NEXUS_1PANEL_REDIS_HOST=*)
redis_host="${line#NEXUS_1PANEL_REDIS_HOST=}"
upsert_env_var "$env_file" NEXUS_1PANEL_REDIS_HOST "$redis_host"
upsert_env_var "$env_file" "NEXUS_REDIS_URL" "redis://${redis_host}:6379/0"
info "1Panel Redis 容器: $redis_hostNEXUS_REDIS_URL 已同步)"
local redis_url
redis_url="$(resolve_nexus_redis_url "$redis_host" "$env_file")"
upsert_env_var "$env_file" "NEXUS_REDIS_URL" "$redis_url"
if [[ "$redis_url" == *"@"* ]]; then
info "1Panel Redis 容器: $redis_hostNEXUS_REDIS_URL 已保留向导密码)"
else
info "1Panel Redis 容器: $redis_hostNEXUS_REDIS_URL 占位,向导步骤 3 填密码后写入卷 .env)"
fi
;;
esac
done <<<"$detect_out"
+1 -2
View File
@@ -26,8 +26,7 @@ services:
NEXUS_HOST: "0.0.0.0"
NEXUS_PORT: "8600"
NEXUS_DEPLOY_PATH: /app
# 1Panel: nx update 写入 redis://1Panel-redis-xxxx:6379/0;非 1Panel 回退 host.docker.internal
NEXUS_REDIS_URL: ${NEXUS_REDIS_URL:-redis://${NEXUS_1PANEL_REDIS_HOST:-host.docker.internal}:6379/0}
# Redis URL 仅来自 /app/.env(安装向导 + entrypoint export_app_env),勿在此注入以免覆盖密码
NEXUS_1PANEL_DB_HOST: ${NEXUS_1PANEL_DB_HOST:-}
NEXUS_1PANEL_REDIS_HOST: ${NEXUS_1PANEL_REDIS_HOST:-}
NEXUS_CORS_ORIGINS: ${NEXUS_CORS_ORIGINS:?set NEXUS_CORS_ORIGINS}
@@ -0,0 +1,23 @@
# 2026-06-06 — 安装向导 Redis 持久化与 nx update 防覆盖
## 摘要
修复 1Panel Docker 场景下安装向导写入带密码 Redis URL 后,`nx update` 与 Compose 环境变量覆盖导致步骤 4 检测失败的问题。
## 动机
- `nexus-1panel.sh` 同步 `NEXUS_REDIS_URL=redis://容器:6379/0`(无密码)覆盖 `docker/.env.prod`
- Compose 注入 `NEXUS_REDIS_URL` 优先级高于 `/app/.env`,步骤 4 无法从无密码 URL 自愈
## 涉及文件
- `docker/docker-compose.prod.yml` — 移除 `NEXUS_REDIS_URL` 环境变量注入
- `deploy/nexus-1panel.sh``resolve_nexus_redis_url` 优先保留卷/已有带密码 URL
- `server/api/install.py``init-db` 暂存 `redis_pass``connection-check``_redis_repair_request` 修复无密码 URL
## 验证
```bash
.venv/bin/pytest tests/test_security_unit.py -q -k redis
bash deploy/verify-1panel-install-wizard.sh
```
+27 -1
View File
@@ -408,6 +408,27 @@ def _init_db_request_from_redis_url(redis_url: str) -> InitDbRequest | None:
)
def _redis_repair_request(redis_url: str) -> InitDbRequest | None:
"""Build InitDbRequest for connection-check repair (URL + install state password)."""
req = _init_db_request_from_redis_url(redis_url)
if not req:
return None
if (req.redis_pass or "").strip():
return req
if not STATE_FILE.is_file():
return req
import json
try:
state = json.loads(read_utf8_text(STATE_FILE))
except (json.JSONDecodeError, OSError):
return req
saved_pass = (state.get("redis_pass") or "").strip()
if saved_pass:
return req.model_copy(update={"redis_pass": saved_pass})
return req
def _mask_redis_url(redis_url: str) -> str:
parsed = urlparse(redis_url)
if not parsed.password:
@@ -957,7 +978,7 @@ async def connection_check():
resolved_display: str | None = None
if not redis_ok and redis_url:
req_from_env = _init_db_request_from_redis_url(redis_url)
req_from_env = _redis_repair_request(redis_url)
if req_from_env:
ok, msg, resolved = _resolve_redis_url(_sanitize_redis_request(req_from_env))
if ok and resolved:
@@ -1124,6 +1145,11 @@ async def init_db(req: InitDbRequest):
"install_dir": install_dir,
"site_url": site_url,
"api_port": req.api_port,
"redis_host": req.redis_host,
"redis_port": req.redis_port,
"redis_db": req.redis_db,
# Step 4 connection-check repair only; removed when install locks
"redis_pass": req.redis_pass,
"guardian_results": guardian_results,
"docker_mode": _is_docker_install_mode(),
"step": 4,
+17
View File
@@ -296,6 +296,23 @@ def test_configure_guardian_docker_mode_skips_supervisor(monkeypatch):
assert bare_results == docker_results
def test_redis_repair_request_uses_install_state_password(tmp_path, monkeypatch):
from server.api import install as install_api
from server.api.install import _redis_repair_request
state_file = tmp_path / ".install_state.json"
state_file.write_text(
'{"redis_pass":"secret-from-state","step":4}',
encoding="utf-8",
)
monkeypatch.setattr(install_api, "STATE_FILE", state_file)
req = _redis_repair_request("redis://1Panel-redis-x:6379/0")
assert req is not None
assert req.redis_pass == "secret-from-state"
assert req.redis_host == "1panel-redis-x"
def test_init_db_request_from_redis_url_legacy_and_colon_pass():
from server.api.install import _init_db_request_from_redis_url