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:
+34
-2
@@ -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_host(NEXUS_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_host(NEXUS_REDIS_URL 已保留向导密码)"
|
||||
else
|
||||
info "1Panel Redis 容器: $redis_host(NEXUS_REDIS_URL 占位,向导步骤 3 填密码后写入卷 .env)"
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
done <<<"$detect_out"
|
||||
|
||||
@@ -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
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user