fix(install): 1Panel Redis 无账号与步骤 4 URL 自愈
Nexus CI/CD / test (push) Waiting to run
Nexus CI/CD / deploy (push) Blocked by required conditions
Nexus Pre-commit Checks / quick-check (push) Waiting to run

确立 Redis 仅密码认证(:pass@/default),隐藏误导性用户名字段;
connection-check 自动修正错误 NEXUS_REDIS_URL,并更新运维文档与验收脚本。
This commit is contained in:
Nexus Deploy
2026-06-06 07:51:52 +08:00
parent 3b2856f388
commit b11360015d
7 changed files with 290 additions and 28 deletions
+108 -6
View File
@@ -12,7 +12,7 @@ import logging
import subprocess
from pathlib import Path
from datetime import datetime, timezone
from urllib.parse import quote_plus, urlparse
from urllib.parse import quote_plus, unquote_plus, urlparse
from fastapi import APIRouter, HTTPException
from pydantic import BaseModel
@@ -338,13 +338,28 @@ def _test_redis_url(redis_url: str) -> tuple[bool, str]:
return False, f"✗ Redis 连接失败: {e}"
def _is_docker_install_mode() -> bool:
return os.environ.get("NEXUS_DOCKER_WRITE_ENV") == "1"
def _sanitize_redis_request(req: InitDbRequest) -> InitDbRequest:
"""1Panel/Docker: Redis has no account — ignore username (incl. mistaken root)."""
user = (req.redis_user or "").strip()
if _is_docker_install_mode():
if user:
return req.model_copy(update={"redis_user": ""})
return req
if user.lower() == "root":
return req.model_copy(update={"redis_user": ""})
return req
def _redis_url_candidates(req: InitDbRequest) -> list[tuple[str, str]]:
"""Try order for 1Panel Redis (default user vs :pass@ vs explicit user)."""
"""Auth variants: 1Panel tries :pass@ then default only (no root)."""
password = (req.redis_pass or "").strip()
if not password:
return [("无密码", _build_redis_url(req))]
user = (req.redis_user or "").strip()
seen: set[str] = set()
out: list[tuple[str, str]] = []
@@ -355,7 +370,13 @@ def _redis_url_candidates(req: InitDbRequest) -> list[tuple[str, str]]:
seen.add(url)
out.append((label, url))
if user:
if _is_docker_install_mode():
add("仅密码 (:pass@)", req.model_copy(update={"redis_user": ""}))
add("用户 default", req.model_copy(update={"redis_user": "default"}))
return out
user = (req.redis_user or "").strip()
if user and user.lower() != "root":
add(f"用户 {user}", req)
add("仅密码 (:pass@)", req.model_copy(update={"redis_user": ""}))
if user.lower() != "default":
@@ -363,6 +384,67 @@ def _redis_url_candidates(req: InitDbRequest) -> list[tuple[str, str]]:
return out
def _init_db_request_from_redis_url(redis_url: str) -> InitDbRequest | None:
"""Parse NEXUS_REDIS_URL into InitDbRequest fields for re-probe / repair."""
if not redis_url.strip():
return None
parsed = urlparse(redis_url)
if not parsed.hostname:
return None
user = unquote_plus(parsed.username or "")
password = unquote_plus(parsed.password or "")
if not password and user:
# Legacy bug: redis://pass@host (password stored as username)
password = user
user = ""
db = (parsed.path or "/0").lstrip("/") or "0"
return InitDbRequest(
db_pass=".",
redis_host=parsed.hostname,
redis_port=str(parsed.port or 6379),
redis_db=db,
redis_user=user,
redis_pass=password,
)
def _mask_redis_url(redis_url: str) -> str:
parsed = urlparse(redis_url)
if not parsed.password:
return redis_url
host = parsed.hostname or ""
port = parsed.port
user = parsed.username or ""
if user:
netloc = f"{user}:***@{host}"
else:
netloc = f":***@{host}"
if port:
netloc = f"{netloc}:{port}"
path = parsed.path or "/0"
return f"redis://{netloc}{path}"
def _patch_env_redis_url(new_url: str) -> None:
"""Update only NEXUS_REDIS_URL in existing .env (install step 4 repair)."""
if not ENV_FILE.is_file():
return
lines = read_utf8_text(ENV_FILE).splitlines()
new_line = f"NEXUS_REDIS_URL={_escape_env_value(new_url)}"
replaced = False
out: list[str] = []
for line in lines:
if line.strip().startswith("NEXUS_REDIS_URL="):
out.append(new_line)
replaced = True
else:
out.append(line)
if not replaced:
out.append(new_line)
write_utf8_lf(ENV_FILE, "\n".join(out) + "\n")
logger.info("Patched NEXUS_REDIS_URL in %s", ENV_FILE)
def _resolve_redis_url(req: InitDbRequest) -> tuple[bool, str, str | None]:
"""Pick first working Redis URL among 1Panel-compatible auth variants."""
last_msg = "✗ 未配置 Redis"
@@ -753,6 +835,7 @@ async def env_check():
async def _credential_checks(req: InitDbRequest) -> tuple[list[dict], bool, str | None]:
"""Verify MySQL + Redis with credentials from install step 3 (TCP + AUTH)."""
req = _sanitize_redis_request(req)
db_url = _make_db_url(req)
mysql_ok, mysql_msg = await _test_mysql_url(db_url)
redis_ok, redis_msg, redis_url = _resolve_redis_url(req)
@@ -775,7 +858,7 @@ async def test_credentials(req: InitDbRequest):
checks, all_pass, redis_url = await _credential_checks(req)
payload: dict = {"checks": checks, "all_pass": all_pass}
if redis_url:
payload["resolved_redis_url"] = redis_url
payload["resolved_redis_url"] = _mask_redis_url(redis_url)
return payload
@@ -792,11 +875,30 @@ async def connection_check():
mysql_ok, mysql_msg = await _test_mysql_url(db_url)
redis_ok, redis_msg = _test_redis_url(redis_url)
redis_repaired = False
resolved_display: str | None = None
if not redis_ok and redis_url:
req_from_env = _init_db_request_from_redis_url(redis_url)
if req_from_env:
ok, msg, resolved = _resolve_redis_url(_sanitize_redis_request(req_from_env))
if ok and resolved:
if resolved != redis_url and not _is_locked():
_patch_env_redis_url(resolved)
redis_repaired = True
redis_ok, redis_msg = True, msg
resolved_display = _mask_redis_url(resolved)
checks = [
{"name": "MySQL", "required": "可连接", "current": mysql_msg, "pass": mysql_ok},
{"name": "Redis", "required": "可连接", "current": redis_msg, "pass": redis_ok},
]
return {"checks": checks, "all_pass": mysql_ok and redis_ok}
payload: dict = {"checks": checks, "all_pass": mysql_ok and redis_ok}
if redis_repaired:
payload["redis_repaired"] = True
if resolved_display:
payload["resolved_redis_url"] = resolved_display
return payload
@router.post("/init-db")