1346 lines
45 KiB
Bash
1346 lines
45 KiB
Bash
#!/usr/bin/env bash
|
||
# Nexus 6.0 — 1Panel + Docker 一键安装 / 升级(参数已全部内置,无需 export)
|
||
#
|
||
# 新机 1Panel 终端一条命令:
|
||
# curl -fsSL "http://66.154.115.8:3000/admin/Nexus/raw/branch/main/deploy/nexus-1panel.sh" | bash -s install
|
||
#
|
||
# 已 clone 到 /opt/nexus:
|
||
# cd /opt/nexus && bash deploy/nexus-1panel.sh install --skip-clone
|
||
#
|
||
# 其他:
|
||
# bash deploy/nexus-1panel.sh install --profile 4c16g
|
||
# bash deploy/nexus-1panel.sh upgrade
|
||
# bash deploy/nexus-1panel.sh check
|
||
# bash deploy/nexus-1panel.sh ports
|
||
#
|
||
# 公共仓库匿名 clone,无需 Gitea 令牌。
|
||
# 仅私人仓库可选: deploy/nexus-1panel.secrets.sh 或 /root/.nexus-deploy.env
|
||
# 或 clone 时 URL 带令牌,脚本会从 git remote 自动解析
|
||
|
||
set -euo pipefail
|
||
|
||
# ═══════════════════════════════════════════════════════════════════
|
||
# 内置路径/域名(无敏感信息,可提交 Git)
|
||
# ═══════════════════════════════════════════════════════════════════
|
||
NEXUS_ROOT="${NEXUS_ROOT:-/opt/nexus}"
|
||
NEXUS_GITEA_HOST="${NEXUS_GITEA_HOST:-66.154.115.8:3000}"
|
||
NEXUS_GITEA_REPO="${NEXUS_GITEA_REPO:-admin/Nexus.git}"
|
||
NEXUS_DOMAIN="${NEXUS_DOMAIN:-api.synaglobal.vip}"
|
||
NEXUS_PROFILE="${NEXUS_PROFILE:-2c8g}"
|
||
NEXUS_FROM_ENV="${NEXUS_FROM_ENV:-/www/wwwroot/api.synaglobal.vip/.env}"
|
||
NEXUS_BACKUP_DIR="${NEXUS_BACKUP_DIR:-/var/backups/nexus}"
|
||
NEXUS_DEPLOY_ENV="${NEXUS_DEPLOY_ENV:-/root/.nexus-deploy.env}"
|
||
NEXUS_PUBLISH_PORT="${NEXUS_PUBLISH_PORT:-8600}"
|
||
GIT_BRANCH="${NEXUS_GIT_BRANCH:-main}"
|
||
|
||
COMPOSE_FILE="docker/docker-compose.prod.yml"
|
||
COMPOSE_1PANEL="docker/docker-compose.1panel.yml"
|
||
ENV_PROD="docker/.env.prod"
|
||
HOST_PROFILE_FILE="docker/.host-profile"
|
||
|
||
RED='\033[0;31m'
|
||
GREEN='\033[0;32m'
|
||
YELLOW='\033[1;33m'
|
||
CYAN='\033[0;36m'
|
||
NC='\033[0m'
|
||
|
||
info() { echo -e "${GREEN}[INFO]${NC} $*"; }
|
||
warn() { echo -e "${YELLOW}[WARN]${NC} $*"; }
|
||
error() { echo -e "${RED}[ERROR]${NC} $*" >&2; }
|
||
step() { echo -e "${CYAN}[STEP]${NC} $*"; }
|
||
|
||
SKIP_CLONE=false
|
||
SKIP_GIT=false
|
||
FRESH_INSTALL=false
|
||
REUSE_SECRETS=false
|
||
FROM_ENV="${NEXUS_FROM_ENV}"
|
||
NO_BACKUP=false
|
||
REQUIRE_BACKUP=false
|
||
PRUNE_IMAGES=false
|
||
NO_CACHE=false
|
||
CHECK_ONLY=false
|
||
DRY_RUN=false
|
||
CUSTOM_CPUS=""
|
||
CUSTOM_MEM_GB=""
|
||
|
||
usage() {
|
||
cat <<'EOF'
|
||
Nexus 1Panel 一键脚本
|
||
|
||
bash deploy/nexus-1panel.sh install [选项] 克隆/配置/启动
|
||
bash deploy/nexus-1panel.sh upgrade [选项] 拉代码+备份+重建
|
||
bash deploy/nexus-1panel.sh check 检查远程是否有更新
|
||
bash deploy/nexus-1panel.sh ports 检查 Gitea/8600/80/443 与防火墙
|
||
bash deploy/nexus-1panel.sh init-token <令牌> 覆盖内置 Gitea 令牌(一般不需要)
|
||
|
||
资源档位(三选一):
|
||
--profile 2c8g|4c16g|1c4g 预设(默认 2c8g = 2核8G)
|
||
--cpus N --mem-gb N 自动匹配最近预设
|
||
|
||
安装选项:
|
||
--skip-clone 已在 /opt/nexus,仅 pull
|
||
--fresh 全新安装(本机自动生成唯一密钥 + /app/install.html)
|
||
--from-env PATH 从旧 .env 迁机导入密钥
|
||
--reuse-secrets 使用 secrets 文件中的 NEXUS 密钥(勿用于新机)
|
||
--domain NAME 覆盖域名
|
||
|
||
升级选项:
|
||
--no-backup --require-backup --prune --no-cache --dry-run --check(仅查 Git,不重建)
|
||
|
||
Git: 公共仓库匿名 clone(无需令牌);私人仓库见 init-token
|
||
EOF
|
||
exit 0
|
||
}
|
||
|
||
script_dir() {
|
||
dirname "$(readlink -f "${BASH_SOURCE[0]}")"
|
||
}
|
||
|
||
secrets_file_path() {
|
||
echo "$(script_dir)/nexus-1panel.secrets.sh"
|
||
}
|
||
|
||
load_token_from_git_remote() {
|
||
local root="${1:-$NEXUS_ROOT}"
|
||
[[ -n "${NEXUS_GITEA_TOKEN:-}" ]] && return 0
|
||
[[ -d "${root}/.git" ]] || return 0
|
||
local url
|
||
url="$(git -C "$root" remote get-url origin 2>/dev/null || true)"
|
||
if [[ "$url" =~ http://[^:]+:([^@]+)@ ]]; then
|
||
NEXUS_GITEA_TOKEN="${BASH_REMATCH[1]}"
|
||
info "已从 git remote 解析 Gitea 令牌"
|
||
fi
|
||
}
|
||
|
||
load_secrets() {
|
||
local secrets_file
|
||
secrets_file="$(secrets_file_path)"
|
||
if [[ -f "$secrets_file" ]]; then
|
||
# shellcheck disable=SC1090
|
||
set -a
|
||
source "$secrets_file"
|
||
set +a
|
||
info "已加载 $secrets_file"
|
||
fi
|
||
if [[ -f "$NEXUS_DEPLOY_ENV" ]]; then
|
||
# shellcheck disable=SC1090
|
||
set -a
|
||
source "$NEXUS_DEPLOY_ENV"
|
||
set +a
|
||
info "已加载 $NEXUS_DEPLOY_ENV"
|
||
fi
|
||
load_token_from_git_remote "$NEXUS_ROOT"
|
||
if [[ "$FRESH_INSTALL" == true ]]; then
|
||
unset NEXUS_SECRET_KEY NEXUS_API_KEY NEXUS_ENCRYPTION_KEY
|
||
info "全新安装:忽略 secrets 中的 NEXUS 密钥,将为本机自动生成"
|
||
fi
|
||
}
|
||
|
||
resolve_profile() {
|
||
if [[ -n "$CUSTOM_CPUS" && -n "$CUSTOM_MEM_GB" ]]; then
|
||
local c m
|
||
c="$CUSTOM_CPUS"
|
||
m="$CUSTOM_MEM_GB"
|
||
if [[ "$c" -le 1 && "$m" -le 4 ]]; then
|
||
NEXUS_PROFILE="1c4g"
|
||
elif [[ "$c" -le 2 && "$m" -le 8 ]]; then
|
||
NEXUS_PROFILE="2c8g"
|
||
else
|
||
NEXUS_PROFILE="4c16g"
|
||
fi
|
||
info "规格 ${c}C${m}G → 档位 ${NEXUS_PROFILE}"
|
||
return 0
|
||
fi
|
||
case "$NEXUS_PROFILE" in
|
||
1c4g|2c8g|4c16g) ;;
|
||
*)
|
||
error "未知档位: $NEXUS_PROFILE(可用: 1c4g, 2c8g, 4c16g)"
|
||
exit 1
|
||
;;
|
||
esac
|
||
}
|
||
|
||
profile_env_file() {
|
||
local root="$1"
|
||
echo "$root/docker/profiles/${NEXUS_PROFILE}.env"
|
||
}
|
||
|
||
save_host_profile() {
|
||
local root="$1"
|
||
echo "$NEXUS_PROFILE" >"$root/$HOST_PROFILE_FILE"
|
||
}
|
||
|
||
load_saved_profile() {
|
||
local root="$1"
|
||
if [[ -f "$root/$HOST_PROFILE_FILE" ]]; then
|
||
NEXUS_PROFILE="$(tr -d '\r\n' <"$root/$HOST_PROFILE_FILE")"
|
||
info "使用已保存档位: $NEXUS_PROFILE"
|
||
fi
|
||
}
|
||
|
||
apply_pool_to_env_prod() {
|
||
local root="$1" prof="$2"
|
||
local env_file="$root/$ENV_PROD"
|
||
[[ -f "$env_file" ]] || return 0
|
||
local pool overflow
|
||
pool="$(grep -E '^NEXUS_DB_POOL_SIZE=' "$prof" | cut -d= -f2-)"
|
||
overflow="$(grep -E '^NEXUS_DB_MAX_OVERFLOW=' "$prof" | cut -d= -f2-)"
|
||
[[ -n "$pool" ]] && sed -i "s|^NEXUS_DB_POOL_SIZE=.*|NEXUS_DB_POOL_SIZE=${pool}|" "$env_file"
|
||
[[ -n "$overflow" ]] && sed -i "s|^NEXUS_DB_MAX_OVERFLOW=.*|NEXUS_DB_MAX_OVERFLOW=${overflow}|" "$env_file"
|
||
}
|
||
|
||
has_1panel_network() {
|
||
docker network inspect 1panel-network >/dev/null 2>&1 \
|
||
|| sudo docker network inspect 1panel-network >/dev/null 2>&1
|
||
}
|
||
|
||
upsert_env_var() {
|
||
local file="$1" key="$2" val="$3"
|
||
if grep -qE "^${key}=" "$file" 2>/dev/null; then
|
||
sed -i "s|^${key}=.*|${key}=${val}|" "$file"
|
||
else
|
||
echo "${key}=${val}" >>"$file"
|
||
fi
|
||
}
|
||
|
||
migrate_redis_url_host() {
|
||
local old_url="$1" new_host="$2"
|
||
python3 - "$old_url" "$new_host" <<'PY'
|
||
import sys
|
||
from urllib.parse import quote, unquote, urlparse
|
||
|
||
old_url, new_host = sys.argv[1], sys.argv[2]
|
||
u = urlparse(old_url)
|
||
if u.scheme != "redis":
|
||
sys.exit(0)
|
||
password = unquote(u.password or "")
|
||
if not password and u.username:
|
||
password = unquote(u.username)
|
||
if password and new_host:
|
||
print(f"redis://:{quote(password, safe='')}@{new_host}:6379/0")
|
||
PY
|
||
}
|
||
|
||
# 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="" migrated="" 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" ]]; then
|
||
if [[ "${vol_url,,}" == *"${host_lc}"* ]]; then
|
||
echo "$vol_url"
|
||
return 0
|
||
fi
|
||
if [[ "$vol_url" == *"@"* ]]; then
|
||
migrated="$(migrate_redis_url_host "$vol_url" "$redis_host" 2>/dev/null || true)"
|
||
if [[ -n "$migrated" ]]; then
|
||
echo "$migrated"
|
||
return 0
|
||
fi
|
||
fi
|
||
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" == *"@"* ]]; then
|
||
if [[ "${existing,,}" == *"${host_lc}"* ]]; then
|
||
echo "$existing"
|
||
return 0
|
||
fi
|
||
migrated="$(migrate_redis_url_host "$existing" "$redis_host" 2>/dev/null || true)"
|
||
if [[ -n "$migrated" ]]; then
|
||
echo "$migrated"
|
||
return 0
|
||
fi
|
||
fi
|
||
fi
|
||
echo "redis://${redis_host}:6379/0"
|
||
}
|
||
|
||
nexus_container_name() {
|
||
docker ps --format '{{.Names}}' | grep -E 'nexus-prod-nexus|nexus-nexus-1' | head -1 || true
|
||
}
|
||
|
||
nexus_publish_port() {
|
||
local root="$1"
|
||
local env_file="$root/$ENV_PROD"
|
||
local port
|
||
port="$(grep -E '^NEXUS_PUBLISH_PORT=' "$env_file" 2>/dev/null | cut -d= -f2- | tr -d '"' || true)"
|
||
echo "${port:-8600}"
|
||
}
|
||
|
||
nexus_container_has_install_lock() {
|
||
local cname="$1"
|
||
docker exec "$cname" test -f /app/.install_locked 2>/dev/null \
|
||
|| docker exec "$cname" test -f /var/lib/nexus/.install_locked 2>/dev/null
|
||
}
|
||
|
||
restore_install_lock_in_container() {
|
||
local cname="$1"
|
||
[[ -n "$cname" ]] || return 0
|
||
docker exec "$cname" sh -c '
|
||
if [ ! -f /app/.install_locked ] && [ -f /var/lib/nexus/.install_locked ]; then
|
||
cp /var/lib/nexus/.install_locked /app/.install_locked
|
||
chmod 600 /app/.install_locked
|
||
fi
|
||
' 2>/dev/null || true
|
||
}
|
||
|
||
nexus_on_1panel_network() {
|
||
local cname="$1"
|
||
[[ -n "$cname" ]] || return 1
|
||
docker inspect "$cname" --format '{{range $k, $v := .NetworkSettings.Networks}}{{$k}} {{end}}' 2>/dev/null \
|
||
| grep -qw '1panel-network'
|
||
}
|
||
|
||
verify_nexus_1panel_network() {
|
||
local root="$1"
|
||
if ! has_1panel_network; then
|
||
return 0
|
||
fi
|
||
local cname
|
||
cname="$(nexus_container_name)"
|
||
[[ -n "$cname" ]] || return 0
|
||
restore_install_lock_in_container "$cname"
|
||
if nexus_on_1panel_network "$cname"; then
|
||
info "已验证 Nexus 接入 1panel-network"
|
||
return 0
|
||
fi
|
||
error "Nexus 容器 ${cname} 未接入 1panel-network,无法解析 1Panel MySQL/Redis 主机名"
|
||
warn "正在叠加 docker-compose.1panel.yml 重新创建容器..."
|
||
compose_cmd "$root" up -d --no-build --remove-orphans
|
||
sleep 2
|
||
cname="$(nexus_container_name)"
|
||
restore_install_lock_in_container "$cname"
|
||
if nexus_on_1panel_network "$cname"; then
|
||
info "已修复:Nexus 已接入 1panel-network"
|
||
return 0
|
||
fi
|
||
error "修复失败:Nexus 仍未接入 1panel-network"
|
||
return 1
|
||
}
|
||
|
||
set_install_complete() {
|
||
local root="$1"
|
||
local env_file="$root/$ENV_PROD"
|
||
local cname
|
||
[[ -f "$env_file" ]] || return 0
|
||
cname="$(nexus_container_name)"
|
||
[[ -n "$cname" ]] || return 0
|
||
restore_install_lock_in_container "$cname"
|
||
if nexus_container_has_install_lock "$cname"; then
|
||
archive_install_wizard_in_container "$cname"
|
||
upsert_env_var "$env_file" "NEXUS_INSTALL_WIZARD_PENDING" "0"
|
||
info "安装已完成:NEXUS_INSTALL_WIZARD_PENDING=0"
|
||
fi
|
||
}
|
||
|
||
sync_env_prod_to_volume() {
|
||
local root="$1"
|
||
local env_file="$root/$ENV_PROD"
|
||
local state_vol cname
|
||
state_vol="$(docker volume ls -q --filter name=nexus-state 2>/dev/null | head -1 || true)"
|
||
[[ -n "$state_vol" && -f "$env_file" ]] || return 0
|
||
cname="$(nexus_container_name)"
|
||
[[ -n "$cname" ]] || return 0
|
||
restore_install_lock_in_container "$cname"
|
||
nexus_container_has_install_lock "$cname" || return 0
|
||
python3 - "$env_file" "$state_vol" <<'PY'
|
||
import re
|
||
import subprocess
|
||
import sys
|
||
from pathlib import Path
|
||
|
||
env_file = Path(sys.argv[1])
|
||
vol = sys.argv[2]
|
||
keys = (
|
||
"NEXUS_SECRET_KEY",
|
||
"NEXUS_API_KEY",
|
||
"NEXUS_ENCRYPTION_KEY",
|
||
"NEXUS_REDIS_URL",
|
||
"NEXUS_DATABASE_URL",
|
||
"NEXUS_TELEGRAM_BOT_TOKEN",
|
||
"NEXUS_TELEGRAM_CHAT_ID",
|
||
)
|
||
prod = {}
|
||
for line in env_file.read_text(encoding="utf-8").splitlines():
|
||
m = re.match(r"^([A-Z0-9_]+)=(.*)$", line.strip())
|
||
if m and m.group(1) in keys and m.group(2).strip():
|
||
prod[m.group(1)] = m.group(2).strip().strip('"')
|
||
if not prod:
|
||
sys.exit(0)
|
||
cur = subprocess.run(
|
||
["docker", "run", "--rm", "-v", f"{vol}:/data", "alpine", "cat", "/data/.env"],
|
||
capture_output=True,
|
||
text=True,
|
||
)
|
||
text = cur.stdout if cur.returncode == 0 else ""
|
||
if not text.strip():
|
||
sys.exit(0)
|
||
lines = text.splitlines()
|
||
out = []
|
||
seen = set()
|
||
for line in lines:
|
||
key = line.split("=", 1)[0].strip() if "=" in line else ""
|
||
if key in prod:
|
||
out.append(f'{key}={prod[key]}')
|
||
seen.add(key)
|
||
else:
|
||
out.append(line)
|
||
for key, val in prod.items():
|
||
if key not in seen:
|
||
out.append(f'{key}={val}')
|
||
new_text = "\n".join(out).rstrip() + "\n"
|
||
subprocess.run(
|
||
["docker", "run", "--rm", "-i", "-v", f"{vol}:/data", "alpine", "sh", "-c", "cat > /data/.env && chmod 600 /data/.env"],
|
||
input=new_text,
|
||
text=True,
|
||
check=True,
|
||
)
|
||
print("synced")
|
||
PY
|
||
info "已将 docker/.env.prod 密钥/URL 同步至 nexus-state 卷"
|
||
}
|
||
|
||
tag_rollback_image() {
|
||
local root="$1" old_id
|
||
old_id="$(docker ps --filter "name=nexus-prod-nexus" --format '{{.Image}}' 2>/dev/null | head -1 || true)"
|
||
if [[ -z "$old_id" ]]; then
|
||
old_id="$(compose_cmd "$root" images -q nexus 2>/dev/null | head -1 || true)"
|
||
fi
|
||
if [[ -n "$old_id" ]]; then
|
||
docker tag "$old_id" "nexus-prod-nexus:rollback" 2>/dev/null || true
|
||
info "已标记回滚镜像 nexus-prod-nexus:rollback"
|
||
fi
|
||
}
|
||
|
||
gate_new_image_import() {
|
||
local root="$1" img
|
||
img="$(compose_cmd "$root" images -q nexus 2>/dev/null | head -1 || true)"
|
||
if [[ -z "$img" ]]; then
|
||
error "镜像门控失败:未找到 nexus 镜像"
|
||
return 1
|
||
fi
|
||
if ! docker run --rm --entrypoint python3 "$img" -c "import server.main" >/dev/null 2>&1; then
|
||
error "镜像门控失败:import server.main"
|
||
return 1
|
||
fi
|
||
info "镜像门控通过 (import server.main)"
|
||
return 0
|
||
}
|
||
|
||
rollback_compose_upgrade() {
|
||
local root="$1"
|
||
if docker image inspect nexus-prod-nexus:rollback >/dev/null 2>&1; then
|
||
warn "健康检查失败,回滚至上一镜像..."
|
||
docker tag nexus-prod-nexus:rollback nexus-prod-nexus:latest 2>/dev/null || true
|
||
compose_cmd "$root" up -d --no-build --remove-orphans
|
||
if ! verify_nexus_1panel_network "$root"; then
|
||
error "回滚后仍未接入 1panel-network"
|
||
return 1
|
||
fi
|
||
return 0
|
||
fi
|
||
warn "无回滚镜像 nexus-prod-nexus:rollback,跳过自动回滚"
|
||
return 1
|
||
}
|
||
|
||
write_1panel_hosts_volume() {
|
||
local mysql_host="${1:-}" redis_host="${2:-}"
|
||
[[ -n "$mysql_host" || -n "$redis_host" ]] || return 0
|
||
local vol json
|
||
vol="$(docker volume ls -q --filter name=nexus-web-data 2>/dev/null | head -1 || true)"
|
||
if [[ -z "$vol" ]]; then
|
||
warn "未找到 nexus-web-data 卷,跳过写入 1panel-hosts.json"
|
||
return 0
|
||
fi
|
||
json="{"
|
||
if [[ -n "$mysql_host" ]]; then
|
||
json+="\"db_host\":\"${mysql_host}\""
|
||
[[ -n "$redis_host" ]] && json+=","
|
||
fi
|
||
if [[ -n "$redis_host" ]]; then
|
||
json+="\"redis_host\":\"${redis_host}\""
|
||
fi
|
||
json+="}"
|
||
if printf '%s' "$json" | docker run --rm -i -v "${vol}:/data" alpine:3.19 \
|
||
sh -c 'cat > /data/1panel-hosts.json && chmod 644 /data/1panel-hosts.json'; then
|
||
info "已写入卷 ${vol} → web/data/1panel-hosts.json"
|
||
else
|
||
warn "写入 1panel-hosts.json 失败(向导仍可读 .env.prod 环境变量)"
|
||
fi
|
||
}
|
||
|
||
sync_1panel_service_hosts() {
|
||
local root="$1"
|
||
local env_file="$root/$ENV_PROD"
|
||
local detect="$root/deploy/detect-1panel-services.sh"
|
||
[[ -f "$env_file" ]] || return 0
|
||
if ! has_1panel_network; then
|
||
warn "未找到 1panel-network — 请先在 1Panel 应用商店安装 MySQL/Redis"
|
||
return 0
|
||
fi
|
||
if [[ ! -x "$detect" ]]; then
|
||
chmod +x "$detect" 2>/dev/null || true
|
||
fi
|
||
if [[ ! -x "$detect" ]]; then
|
||
warn "未找到 detect-1panel-services.sh,跳过 1Panel 容器名探测"
|
||
return 0
|
||
fi
|
||
local line mysql_host="" redis_host="" detect_out
|
||
if ! detect_out="$("$detect" 2>&1)"; then
|
||
warn "1Panel 容器探测: ${detect_out:-无 MySQL/Redis 容器运行}"
|
||
return 0
|
||
fi
|
||
while IFS= read -r line; do
|
||
case "$line" in
|
||
NEXUS_1PANEL_DB_HOST=*)
|
||
mysql_host="${line#NEXUS_1PANEL_DB_HOST=}"
|
||
upsert_env_var "$env_file" NEXUS_1PANEL_DB_HOST "$mysql_host"
|
||
info "1Panel MySQL 容器: $mysql_host(安装向导将预填为数据库主机)"
|
||
;;
|
||
NEXUS_1PANEL_REDIS_HOST=*)
|
||
redis_host="${line#NEXUS_1PANEL_REDIS_HOST=}"
|
||
upsert_env_var "$env_file" NEXUS_1PANEL_REDIS_HOST "$redis_host"
|
||
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"
|
||
write_1panel_hosts_volume "$mysql_host" "$redis_host"
|
||
}
|
||
|
||
compose_cmd() {
|
||
local root="$1"
|
||
shift
|
||
local prof
|
||
prof="$(profile_env_file "$root")"
|
||
if [[ ! -f "$prof" ]]; then
|
||
error "缺少档位文件: $prof"
|
||
exit 1
|
||
fi
|
||
cd "$root"
|
||
local -a compose_args=(-f "$COMPOSE_FILE")
|
||
if has_1panel_network && [[ -f "$COMPOSE_1PANEL" ]]; then
|
||
compose_args+=(-f "$COMPOSE_1PANEL")
|
||
fi
|
||
docker compose "${compose_args[@]}" --env-file "$ENV_PROD" --env-file "$prof" "$@"
|
||
}
|
||
|
||
rand_secret() {
|
||
openssl rand -hex 24 2>/dev/null || head -c 24 /dev/urandom | od -An -tx1 | tr -d ' \n'
|
||
}
|
||
|
||
rand_pass() {
|
||
openssl rand -hex 16 2>/dev/null || head -c 16 /dev/urandom | od -An -tx1 | tr -d ' \n'
|
||
}
|
||
|
||
read_env_val() {
|
||
local file="$1" key="$2"
|
||
grep -E "^${key}=" "$file" 2>/dev/null | head -1 | cut -d= -f2- | tr -d '\r"' | sed 's/^[[:space:]]*//;s/[[:space:]]*$//'
|
||
}
|
||
|
||
parse_gitea_endpoint() {
|
||
local hp="${NEXUS_GITEA_HOST}"
|
||
GITEA_HOST="${hp%%:*}"
|
||
GITEA_PORT="${hp##*:}"
|
||
if [[ "$GITEA_HOST" == "$GITEA_PORT" || -z "$GITEA_PORT" ]]; then
|
||
GITEA_PORT=3000
|
||
fi
|
||
}
|
||
|
||
nexus_publish_port() {
|
||
local root="${1:-}"
|
||
local p="${NEXUS_PUBLISH_PORT:-8600}"
|
||
if [[ -n "$root" && -f "$root/$ENV_PROD" ]]; then
|
||
local v
|
||
v="$(read_env_val "$root/$ENV_PROD" NEXUS_PUBLISH_PORT)"
|
||
[[ -n "$v" ]] && p="$v"
|
||
fi
|
||
echo "$p"
|
||
}
|
||
|
||
tcp_reachable() {
|
||
local host="$1" port="$2" t="${3:-3}"
|
||
if command -v nc >/dev/null 2>&1; then
|
||
nc -z -w "$t" "$host" "$port" 2>/dev/null
|
||
return $?
|
||
fi
|
||
if command -v timeout >/dev/null 2>&1; then
|
||
timeout "$t" bash -c "exec 3<>/dev/tcp/${host}/${port}" 2>/dev/null \
|
||
&& { exec 3<&- 3>&-; return 0; }
|
||
else
|
||
bash -c "exec 3<>/dev/tcp/${host}/${port}" 2>/dev/null \
|
||
&& { exec 3<&- 3>&-; return 0; }
|
||
fi
|
||
return 1
|
||
}
|
||
|
||
local_port_listening() {
|
||
local port="$1"
|
||
if command -v ss >/dev/null 2>&1; then
|
||
ss -tlnH 2>/dev/null | grep -qE ":${port}[[:space:]]"
|
||
return $?
|
||
fi
|
||
if command -v netstat >/dev/null 2>&1; then
|
||
netstat -tln 2>/dev/null | grep -qE ":${port}[[:space:]]"
|
||
return $?
|
||
fi
|
||
curl -sf --max-time 2 "http://127.0.0.1:${port}/health" >/dev/null 2>&1
|
||
}
|
||
|
||
firewall_may_block_port() {
|
||
local port="$1"
|
||
if command -v ufw >/dev/null 2>&1; then
|
||
local st
|
||
st="$(ufw status 2>/dev/null | head -1 || true)"
|
||
if [[ "$st" == *"active"* ]]; then
|
||
ufw status 2>/dev/null | grep -qE "${port}/tcp[[:space:]]+ALLOW" && return 1
|
||
return 0
|
||
fi
|
||
fi
|
||
if command -v firewall-cmd >/dev/null 2>&1 && firewall-cmd --state 2>/dev/null | grep -qi running; then
|
||
firewall-cmd --list-ports 2>/dev/null | grep -q "${port}/tcp" && return 1
|
||
firewall-cmd --list-services 2>/dev/null | grep -qE '^(http|https)$' \
|
||
&& [[ "$port" == "80" || "$port" == "443" ]] && return 1
|
||
return 0
|
||
fi
|
||
return 1
|
||
}
|
||
|
||
remind_firewall() {
|
||
local port="$1" label="$2"
|
||
if firewall_may_block_port "$port"; then
|
||
warn "防火墙可能未放行 ${port}/tcp(${label})"
|
||
warn " 本机: ufw allow ${port}/tcp 或 1Panel → 主机 → 防火墙"
|
||
warn " 云厂商控制台: 安全组入站放行 ${port}/tcp"
|
||
fi
|
||
}
|
||
|
||
check_ports_preflight() {
|
||
local root="${1:-$NEXUS_ROOT}"
|
||
local nport issues=0
|
||
parse_gitea_endpoint
|
||
nport="$(nexus_publish_port "$root")"
|
||
|
||
step "检查端口与防火墙..."
|
||
if tcp_reachable "$GITEA_HOST" "$GITEA_PORT"; then
|
||
info "Gitea ${GITEA_HOST}:${GITEA_PORT} — 可达(git 拉取)"
|
||
else
|
||
warn "Gitea ${GITEA_HOST}:${GITEA_PORT} — 无法连接,clone/pull 会失败"
|
||
warn " 请确认 Gitea 在线,且本机出站与该端口未被防火墙/安全组拦截"
|
||
issues=$((issues + 1))
|
||
fi
|
||
|
||
if local_port_listening "$nport"; then
|
||
info "127.0.0.1:${nport} — 已有监听(可能为旧 Nexus 实例)"
|
||
else
|
||
info "127.0.0.1:${nport} — 尚未监听(安装后将由 Compose 绑定)"
|
||
fi
|
||
|
||
for spec in "80:HTTP" "443:HTTPS"; do
|
||
local p="${spec%%:*}" label="${spec##*:}"
|
||
if local_port_listening "$p"; then
|
||
info "本机 :${p} — 已监听(${label} / 1Panel)"
|
||
else
|
||
warn "本机 :${p} — 未监听,外网 ${label} 暂不可用"
|
||
warn " 安装完成后: 1Panel → 网站 → 反代 http://127.0.0.1:${nport} 并申请 SSL"
|
||
issues=$((issues + 1))
|
||
fi
|
||
remind_firewall "$p" "${label} 公网"
|
||
done
|
||
|
||
remind_firewall 22 "SSH(远程维护)"
|
||
|
||
if [[ "$issues" -gt 0 ]]; then
|
||
warn "共 ${issues} 项端口提醒(可继续安装,但请先处理 Gitea 不可达问题)"
|
||
else
|
||
info "端口预检通过"
|
||
fi
|
||
echo ""
|
||
}
|
||
|
||
check_ports_post() {
|
||
local root="$1"
|
||
local nport health_ext
|
||
nport="$(nexus_publish_port "$root")"
|
||
|
||
step "检查服务端口..."
|
||
if local_port_listening "$nport"; then
|
||
info "Nexus 127.0.0.1:${nport} — 已监听"
|
||
else
|
||
warn "127.0.0.1:${nport} — 未监听,请执行: cd $root && docker compose ... ps"
|
||
fi
|
||
|
||
if local_port_listening 443; then
|
||
health_ext="$(curl -sk --max-time 8 -o /dev/null -w '%{http_code}' "https://${NEXUS_DOMAIN}/health" 2>/dev/null || echo "000")"
|
||
if [[ "$health_ext" == "200" ]]; then
|
||
info "https://${NEXUS_DOMAIN}/health — 外网可达 (HTTP 200)"
|
||
else
|
||
warn "https://${NEXUS_DOMAIN}/health — 返回 HTTP ${health_ext}"
|
||
warn " 若外网打不开: 1Panel 反代是否指向 127.0.0.1:${nport},安全组是否放行 443"
|
||
fi
|
||
else
|
||
warn "本机 443 未监听 — 请在 1Panel 创建站点并启用 HTTPS,否则浏览器无法访问 ${NEXUS_DOMAIN}"
|
||
remind_firewall 443 "HTTPS"
|
||
fi
|
||
|
||
if ! local_port_listening 80; then
|
||
info "本机 80 未监听(仅 HTTPS 时正常)"
|
||
fi
|
||
echo ""
|
||
}
|
||
|
||
cmd_ports() {
|
||
require_root
|
||
check_ports_preflight "$NEXUS_ROOT"
|
||
if [[ -f "$NEXUS_ROOT/$ENV_PROD" ]]; then
|
||
check_ports_post "$NEXUS_ROOT"
|
||
fi
|
||
}
|
||
|
||
require_root() {
|
||
if [[ "$(id -u)" -ne 0 ]]; then
|
||
error "请切换到 root 执行(su -),不要用 sudo 包一层"
|
||
exit 1
|
||
fi
|
||
}
|
||
|
||
require_token() {
|
||
if [[ -n "${NEXUS_GITEA_TOKEN:-}" ]]; then
|
||
return 0
|
||
fi
|
||
info "未配置 Gitea 令牌,按公共仓库匿名 clone/pull"
|
||
}
|
||
|
||
git_remote_url() {
|
||
if [[ -n "${NEXUS_GITEA_TOKEN:-}" ]]; then
|
||
echo "http://admin:${NEXUS_GITEA_TOKEN}@${NEXUS_GITEA_HOST}/${NEXUS_GITEA_REPO}"
|
||
else
|
||
echo "http://${NEXUS_GITEA_HOST}/${NEXUS_GITEA_REPO}"
|
||
fi
|
||
}
|
||
|
||
ensure_packages() {
|
||
if command -v apt-get >/dev/null 2>&1; then
|
||
export DEBIAN_FRONTEND=noninteractive
|
||
apt-get update -qq
|
||
apt-get install -y -qq git curl openssl ca-certificates >/dev/null 2>&1 || true
|
||
fi
|
||
}
|
||
|
||
ensure_docker() {
|
||
if docker compose version >/dev/null 2>&1; then
|
||
info "Docker Compose: $(docker compose version --short 2>/dev/null || docker compose version)"
|
||
return 0
|
||
fi
|
||
error "未检测到 docker compose。请在 1Panel → 容器 安装 Docker。"
|
||
exit 1
|
||
}
|
||
|
||
clone_or_pull() {
|
||
local url
|
||
url="$(git_remote_url)"
|
||
|
||
if [[ "$SKIP_CLONE" == true ]]; then
|
||
if [[ ! -d "$NEXUS_ROOT/.git" ]]; then
|
||
error "--skip-clone 但 $NEXUS_ROOT 不是 git 仓库"
|
||
exit 1
|
||
fi
|
||
info "在 $NEXUS_ROOT 执行 git pull..."
|
||
git -C "$NEXUS_ROOT" remote set-url origin "$url" 2>/dev/null || true
|
||
git -C "$NEXUS_ROOT" fetch origin "$GIT_BRANCH" --prune
|
||
git -C "$NEXUS_ROOT" reset --hard "origin/${GIT_BRANCH}"
|
||
return 0
|
||
fi
|
||
|
||
mkdir -p "$(dirname "$NEXUS_ROOT")"
|
||
if [[ -d "$NEXUS_ROOT/.git" ]]; then
|
||
info "仓库已存在,同步 origin/${GIT_BRANCH}..."
|
||
git -C "$NEXUS_ROOT" remote set-url origin "$url" 2>/dev/null || true
|
||
git -C "$NEXUS_ROOT" fetch origin "$GIT_BRANCH" --prune
|
||
git -C "$NEXUS_ROOT" reset --hard "origin/${GIT_BRANCH}"
|
||
return 0
|
||
fi
|
||
|
||
if [[ -d "$NEXUS_ROOT" ]] && [[ -n "$(ls -A "$NEXUS_ROOT" 2>/dev/null)" ]]; then
|
||
error "目录非空且不是 git 仓库: $NEXUS_ROOT"
|
||
exit 1
|
||
fi
|
||
|
||
rm -rf "$NEXUS_ROOT"
|
||
info "克隆 Nexus → $NEXUS_ROOT ..."
|
||
git clone "$url" "$NEXUS_ROOT"
|
||
git -C "$NEXUS_ROOT" checkout "$GIT_BRANCH" 2>/dev/null || true
|
||
}
|
||
|
||
write_temp_secrets_env() {
|
||
local f
|
||
f="$(mktemp /tmp/nexus-secrets-import.XXXXXX.env)"
|
||
{
|
||
printf 'NEXUS_SECRET_KEY=%s\n' "$NEXUS_SECRET_KEY"
|
||
printf 'NEXUS_API_KEY=%s\n' "$NEXUS_API_KEY"
|
||
printf 'NEXUS_ENCRYPTION_KEY=%s\n' "$NEXUS_ENCRYPTION_KEY"
|
||
} >"$f"
|
||
chmod 600 "$f"
|
||
echo "$f"
|
||
}
|
||
|
||
write_env_prod() {
|
||
local root="$1"
|
||
local env_file="$root/$ENV_PROD"
|
||
local example="$root/docker/.env.prod.example"
|
||
local gen="$root/scripts/generate_nexus_secrets.py"
|
||
local prof backup tmp
|
||
|
||
if [[ ! -f "$example" ]]; then
|
||
error "缺少 $example"
|
||
exit 1
|
||
fi
|
||
if [[ ! -f "$gen" ]]; then
|
||
error "缺少 $gen"
|
||
exit 1
|
||
fi
|
||
|
||
if [[ -f "$env_file" ]]; then
|
||
warn "$env_file 已存在,仅更新域名与连接池"
|
||
sed -i "s|^NEXUS_CORS_ORIGINS=.*|NEXUS_CORS_ORIGINS=https://${NEXUS_DOMAIN}|" "$env_file"
|
||
sed -i "s|^NEXUS_API_BASE_URL=.*|NEXUS_API_BASE_URL=https://${NEXUS_DOMAIN}|" "$env_file"
|
||
apply_pool_to_env_prod "$root" "$(profile_env_file "$root")"
|
||
chmod 600 "$env_file"
|
||
return 0
|
||
fi
|
||
|
||
prof="$(profile_env_file "$root")"
|
||
backup="/root/.nexus-install-secrets-$(date +%Y%m%d%H%M%S).env"
|
||
|
||
if [[ -n "$FROM_ENV" && -f "$FROM_ENV" ]]; then
|
||
info "迁机:从 $FROM_ENV 导入密钥..."
|
||
python3 "$gen" write-prod \
|
||
--template "$example" \
|
||
--output "$env_file" \
|
||
--domain "$NEXUS_DOMAIN" \
|
||
--profile-env "$prof" \
|
||
--from-env "$FROM_ENV" \
|
||
--backup "$backup"
|
||
elif [[ "$FRESH_INSTALL" == true ]]; then
|
||
info "全新安装:为本机生成唯一 NEXUS 密钥(MySQL/Redis 请自行安装)..."
|
||
python3 "$gen" write-prod \
|
||
--template "$example" \
|
||
--output "$env_file" \
|
||
--domain "$NEXUS_DOMAIN" \
|
||
--profile-env "$prof" \
|
||
--fresh \
|
||
--wizard-pending \
|
||
--backup "$backup"
|
||
info "密钥备份: $backup(仅本机,勿复制到其他服务器)"
|
||
info "完成后访问 https://${NEXUS_DOMAIN}/app/install.html"
|
||
elif [[ "$REUSE_SECRETS" == true && -n "${NEXUS_SECRET_KEY:-}" && -n "${NEXUS_API_KEY:-}" && -n "${NEXUS_ENCRYPTION_KEY:-}" ]]; then
|
||
warn "使用 secrets 中的 NEXUS 密钥(仅迁机/复用,新机勿用 --reuse-secrets)"
|
||
tmp="$(write_temp_secrets_env)"
|
||
python3 "$gen" write-prod \
|
||
--template "$example" \
|
||
--output "$env_file" \
|
||
--domain "$NEXUS_DOMAIN" \
|
||
--profile-env "$prof" \
|
||
--from-env "$tmp" \
|
||
--backup "$backup"
|
||
rm -f "$tmp"
|
||
else
|
||
info "未迁机导入:为本机生成唯一密钥..."
|
||
python3 "$gen" write-prod \
|
||
--template "$example" \
|
||
--output "$env_file" \
|
||
--domain "$NEXUS_DOMAIN" \
|
||
--profile-env "$prof" \
|
||
--fresh \
|
||
--wizard-pending \
|
||
--backup "$backup"
|
||
info "密钥备份: $backup"
|
||
if [[ "$FRESH_INSTALL" != true ]]; then
|
||
info "完成后访问 https://${NEXUS_DOMAIN}/app/install.html"
|
||
fi
|
||
fi
|
||
|
||
chmod 600 "$env_file"
|
||
info "已写入 $env_file"
|
||
}
|
||
|
||
purge_legacy_bundled_services() {
|
||
local root="$1"
|
||
local s
|
||
for s in \
|
||
"$root/deploy/uninstall-mysql-compose.sh" \
|
||
"$root/deploy/uninstall-redis-compose.sh"; do
|
||
if [[ -x "$s" ]]; then
|
||
step "清理遗留 Compose 内置服务: $(basename "$s")"
|
||
NEXUS_ROOT="$root" bash "$s" || true
|
||
fi
|
||
done
|
||
}
|
||
|
||
reset_nexus_state_volume_for_fresh() {
|
||
local vol
|
||
vol="$(docker volume ls -q --filter name=nexus-state 2>/dev/null | head -1 || true)"
|
||
[[ -n "$vol" ]] || return 0
|
||
warn "全新安装:清除 nexus-state 卷内旧 .env / .install_locked"
|
||
docker run --rm -v "${vol}:/data" alpine:3.19 \
|
||
sh -c 'rm -f /data/.env /data/.install_locked' 2>/dev/null || true
|
||
}
|
||
|
||
restore_install_wizard_html_in_container() {
|
||
local cname="$1"
|
||
[[ -n "$cname" ]] || return 0
|
||
docker exec "$cname" sh -c '
|
||
rm -f /app/.install_locked
|
||
if [ -f /app/web/app/install.html.bak ] && [ ! -f /app/web/app/install.html ]; then
|
||
mv /app/web/app/install.html.bak /app/web/app/install.html
|
||
fi
|
||
' 2>/dev/null || true
|
||
}
|
||
|
||
compose_up() {
|
||
local root="$1" cname
|
||
save_host_profile "$root"
|
||
purge_legacy_bundled_services "$root"
|
||
sync_1panel_service_hosts "$root"
|
||
if [[ "$FRESH_INSTALL" == true ]]; then
|
||
reset_nexus_state_volume_for_fresh
|
||
fi
|
||
if has_1panel_network; then
|
||
info "已接入 1panel-network(MySQL/Redis 使用 1Panel 容器名互联)"
|
||
fi
|
||
info "档位: ${NEXUS_PROFILE}(仅 Nexus 容器;MySQL/Redis 请自行安装)"
|
||
info "构建并启动(首次约 10~20 分钟)..."
|
||
compose_cmd "$root" up -d --build --remove-orphans
|
||
verify_nexus_1panel_network "$root" || return 1
|
||
if [[ "$FRESH_INSTALL" == true ]]; then
|
||
cname="$(nexus_container_name)"
|
||
restore_install_wizard_html_in_container "$cname"
|
||
fi
|
||
}
|
||
|
||
archive_install_wizard_in_container() {
|
||
local cname="$1"
|
||
[[ -n "$cname" ]] || return 0
|
||
docker exec "$cname" sh -c '
|
||
if [ -f /app/web/app/install.html.bak ]; then
|
||
rm -f /app/web/app/install.html
|
||
elif [ -f /app/web/app/install.html ]; then
|
||
mv /app/web/app/install.html /app/web/app/install.html.bak
|
||
fi
|
||
' 2>/dev/null || true
|
||
}
|
||
|
||
verify_install_wizard() {
|
||
local root="$1"
|
||
local port="${2:-$(nexus_publish_port "$root")}"
|
||
local code cname
|
||
cname="$(nexus_container_name)"
|
||
if [[ -n "$cname" ]] && nexus_container_has_install_lock "$cname"; then
|
||
archive_install_wizard_in_container "$cname"
|
||
code=$(curl -s -o /dev/null -w '%{http_code}' "http://127.0.0.1:${port}/app/install.html" 2>/dev/null || echo "000")
|
||
if [[ "$code" == "404" ]]; then
|
||
info " /app/install.html 已归档 → HTTP 404"
|
||
return 0
|
||
fi
|
||
warn " 已锁定但 /app/install.html → HTTP $code"
|
||
return 1
|
||
fi
|
||
code=$(curl -s -o /dev/null -w '%{http_code}' "http://127.0.0.1:${port}/app/install.html" 2>/dev/null || echo "000")
|
||
if [[ "$code" == "200" ]]; then
|
||
info " /app/install.html → HTTP 200"
|
||
return 0
|
||
fi
|
||
warn " /app/install.html → HTTP $code(尝试同步静态文件…)"
|
||
if [[ -x "$root/deploy/sync-install-wizard-to-container.sh" ]]; then
|
||
NEXUS_PUBLISH_PORT="$port" NEXUS_ROOT="$root" bash "$root/deploy/sync-install-wizard-to-container.sh" || true
|
||
code=$(curl -s -o /dev/null -w '%{http_code}' "http://127.0.0.1:${port}/app/install.html" 2>/dev/null || echo "000")
|
||
if [[ "$code" == "200" ]]; then
|
||
info " /app/install.html → HTTP 200(已同步)"
|
||
return 0
|
||
fi
|
||
fi
|
||
warn " 请重建镜像: cd $root && docker compose -f docker/docker-compose.prod.yml --env-file docker/.env.prod build --no-cache nexus"
|
||
return 1
|
||
}
|
||
|
||
verify_health() {
|
||
local root="$1" title="${2:-安装}"
|
||
local i health spa port
|
||
port="$(nexus_publish_port "$root")"
|
||
info "等待服务就绪 (端口 ${port})..."
|
||
for i in $(seq 1 60); do
|
||
health=$(curl -sf "http://127.0.0.1:${port}/health" 2>/dev/null || true)
|
||
if [[ "$health" == "ok" ]]; then
|
||
spa=$(curl -s -o /dev/null -w '%{http_code}' "http://127.0.0.1:${port}/app/" 2>/dev/null || echo "000")
|
||
verify_install_wizard "$root" "$port" || true
|
||
echo ""
|
||
info "═══ ${title}完成 ═══"
|
||
info " 档位: ${NEXUS_PROFILE}"
|
||
info " /health → ok"
|
||
info " /app/ → HTTP $spa"
|
||
info " 目录: $root"
|
||
if [[ -d "$root/.git" ]]; then
|
||
info " 版本: $(git -C "$root" log -1 --oneline)"
|
||
fi
|
||
echo ""
|
||
info "1Panel → 网站 → 反代 http://127.0.0.1:${port} 域名: ${NEXUS_DOMAIN}"
|
||
info "配置示例: $root/deploy/1panel/openresty-nexus.conf.example"
|
||
if [[ "$FRESH_INSTALL" == true ]] || ! grep -qE '^NEXUS_SECRET_KEY=.+$' "$root/$ENV_PROD" 2>/dev/null; then
|
||
info "浏览器: https://${NEXUS_DOMAIN}/app/install.html"
|
||
else
|
||
info "浏览器: https://${NEXUS_DOMAIN}/app/"
|
||
fi
|
||
echo ""
|
||
NEXUS_ROOT="$root" bash "$root/deploy/install-nx-cli.sh" 2>/dev/null || true
|
||
info "运维菜单: nx 或 nexus-update"
|
||
check_ports_post "$root"
|
||
return 0
|
||
fi
|
||
sleep 5
|
||
done
|
||
error "健康检查超时"
|
||
check_ports_post "$root" || true
|
||
compose_cmd "$root" logs --tail=80 nexus || true
|
||
return 1
|
||
}
|
||
|
||
ensure_repo() {
|
||
if [[ ! -d "$NEXUS_ROOT/.git" ]]; then
|
||
error "未找到 $NEXUS_ROOT,请先: bash $0 install"
|
||
exit 1
|
||
fi
|
||
if [[ ! -f "$NEXUS_ROOT/$ENV_PROD" ]]; then
|
||
error "缺少 $NEXUS_ROOT/$ENV_PROD"
|
||
exit 1
|
||
fi
|
||
}
|
||
|
||
git_fetch() {
|
||
local url
|
||
url="$(git_remote_url)"
|
||
git -C "$NEXUS_ROOT" remote set-url origin "$url" 2>/dev/null || true
|
||
git -C "$NEXUS_ROOT" fetch origin "$GIT_BRANCH" --prune
|
||
}
|
||
|
||
commits_behind() {
|
||
git -C "$NEXUS_ROOT" rev-list "HEAD..origin/${GIT_BRANCH}" --count 2>/dev/null || echo "0"
|
||
}
|
||
|
||
show_pending() {
|
||
local behind
|
||
behind="$(commits_behind)"
|
||
info "当前: $(git -C "$NEXUS_ROOT" log -1 --oneline)"
|
||
info "远程: $(git -C "$NEXUS_ROOT" log -1 --oneline "origin/${GIT_BRANCH}")"
|
||
if [[ "$behind" == "0" ]]; then
|
||
info "已是最新。"
|
||
return 1
|
||
fi
|
||
warn "落后 ${behind} 个提交:"
|
||
git -C "$NEXUS_ROOT" log --oneline "HEAD..origin/${GIT_BRANCH}"
|
||
return 0
|
||
}
|
||
|
||
backup_mysql() {
|
||
local root="$1" ts file dump_sh
|
||
if [[ "$NO_BACKUP" == true ]]; then
|
||
warn "已跳过备份 (--no-backup)"
|
||
return 0
|
||
fi
|
||
dump_sh="${root}/deploy/mysql_dump_to_file.sh"
|
||
if [[ ! -f "$dump_sh" ]]; then
|
||
warn "缺少 $dump_sh,跳过 MySQL 备份"
|
||
return 0
|
||
fi
|
||
mkdir -p "$NEXUS_BACKUP_DIR"
|
||
ts="$(date +%Y%m%d-%H%M%S)"
|
||
file="${NEXUS_BACKUP_DIR}/nexus-before-upgrade-${ts}.sql"
|
||
step "备份 MySQL → $file"
|
||
if ! NEXUS_ROOT="$root" bash "$dump_sh" --output "$file" --root "$root"; then
|
||
if [[ "$REQUIRE_BACKUP" == true ]]; then
|
||
error "mysqldump 失败,已中止(--require-backup)"
|
||
rm -f "$file"
|
||
exit 1
|
||
fi
|
||
warn "mysqldump 失败,继续升级(加 --require-backup 可在备份失败时中止)"
|
||
rm -f "$file"
|
||
return 0
|
||
fi
|
||
gzip -f "$file"
|
||
info "备份: ${file}.gz"
|
||
}
|
||
|
||
git_upgrade() {
|
||
local before after
|
||
before="$(git -C "$NEXUS_ROOT" rev-parse HEAD)"
|
||
step "同步 origin/${GIT_BRANCH}"
|
||
git -C "$NEXUS_ROOT" reset --hard "origin/${GIT_BRANCH}"
|
||
after="$(git -C "$NEXUS_ROOT" rev-parse HEAD)"
|
||
info "代码: ${before:0:7} → ${after:0:7}"
|
||
}
|
||
|
||
cmd_init_token() {
|
||
require_root
|
||
local tok="${1:-}"
|
||
if [[ -z "$tok" ]]; then
|
||
error "用法: $0 init-token <Gitea令牌>"
|
||
exit 1
|
||
fi
|
||
printf 'NEXUS_GITEA_TOKEN=%s\n' "$tok" >"$NEXUS_DEPLOY_ENV"
|
||
chmod 600 "$NEXUS_DEPLOY_ENV"
|
||
info "已写入 $NEXUS_DEPLOY_ENV"
|
||
}
|
||
|
||
cmd_install() {
|
||
echo ""
|
||
echo "=========================================="
|
||
echo " Nexus 1Panel 一键安装"
|
||
echo " 域名: ${NEXUS_DOMAIN} 档位: ${NEXUS_PROFILE}"
|
||
echo "=========================================="
|
||
echo ""
|
||
|
||
require_root
|
||
require_token
|
||
resolve_profile
|
||
ensure_packages
|
||
ensure_docker
|
||
check_ports_preflight "$NEXUS_ROOT"
|
||
clone_or_pull
|
||
NEXUS_ROOT="$NEXUS_ROOT" bash "$NEXUS_ROOT/deploy/install-nx-cli.sh" 2>/dev/null || true
|
||
write_env_prod "$NEXUS_ROOT"
|
||
compose_up "$NEXUS_ROOT"
|
||
verify_health "$NEXUS_ROOT" "安装" || exit 1
|
||
}
|
||
|
||
cmd_upgrade() {
|
||
echo ""
|
||
echo "=========================================="
|
||
echo " Nexus 1Panel 一键升级"
|
||
echo "=========================================="
|
||
echo ""
|
||
|
||
require_root
|
||
require_token
|
||
ensure_docker
|
||
ensure_repo
|
||
load_saved_profile "$NEXUS_ROOT"
|
||
resolve_profile
|
||
check_ports_preflight "$NEXUS_ROOT"
|
||
if [[ "$SKIP_GIT" == true ]]; then
|
||
info "跳过 Git 同步(本机 rsync 已推送代码)"
|
||
else
|
||
git_fetch
|
||
show_pending || true
|
||
fi
|
||
|
||
if [[ "$CHECK_ONLY" == true ]]; then
|
||
exit 0
|
||
fi
|
||
if [[ "$DRY_RUN" == true ]]; then
|
||
info "dry-run 结束。"
|
||
exit 0
|
||
fi
|
||
|
||
if [[ "$SKIP_GIT" == true ]]; then
|
||
info "本机直传模式:跳过 git reset,仍备份并重建镜像"
|
||
if [[ "$NO_BACKUP" != true ]]; then
|
||
backup_mysql "$NEXUS_ROOT"
|
||
fi
|
||
else
|
||
local behind
|
||
behind="$(commits_behind)"
|
||
if [[ "$behind" != "0" ]]; then
|
||
backup_mysql "$NEXUS_ROOT"
|
||
git_upgrade
|
||
else
|
||
info "Git 已最新,仍重建 Docker 镜像与容器(避免旧 entrypoint 缓存)"
|
||
if [[ "$NO_BACKUP" != true ]]; then
|
||
backup_mysql "$NEXUS_ROOT"
|
||
fi
|
||
fi
|
||
fi
|
||
|
||
apply_pool_to_env_prod "$NEXUS_ROOT" "$(profile_env_file "$NEXUS_ROOT")"
|
||
upsert_env_var "$NEXUS_ROOT/$ENV_PROD" "NEXUS_HOST_ROOT" "$NEXUS_ROOT"
|
||
save_host_profile "$NEXUS_ROOT"
|
||
purge_legacy_bundled_services "$NEXUS_ROOT"
|
||
sync_1panel_service_hosts "$NEXUS_ROOT"
|
||
sync_env_prod_to_volume "$NEXUS_ROOT"
|
||
tag_rollback_image "$NEXUS_ROOT"
|
||
if [[ "$NO_CACHE" == true ]]; then
|
||
step "无缓存重建 Nexus 镜像..."
|
||
compose_cmd "$NEXUS_ROOT" build --no-cache nexus
|
||
else
|
||
step "重建 Nexus 镜像..."
|
||
compose_cmd "$NEXUS_ROOT" build nexus
|
||
fi
|
||
if ! gate_new_image_import "$NEXUS_ROOT"; then
|
||
exit 1
|
||
fi
|
||
step "重建容器(--remove-orphans 清理旧 mysql/redis)..."
|
||
compose_cmd "$NEXUS_ROOT" up -d --no-build --remove-orphans
|
||
if ! verify_nexus_1panel_network "$NEXUS_ROOT"; then
|
||
rollback_compose_upgrade "$NEXUS_ROOT" || true
|
||
exit 1
|
||
fi
|
||
local port wiz_code i health
|
||
port="$(nexus_publish_port "$NEXUS_ROOT")"
|
||
for i in $(seq 1 30); do
|
||
health="$(curl -sf "http://127.0.0.1:${port}/health" 2>/dev/null || true)"
|
||
[[ "$health" == "ok" ]] && break
|
||
sleep 2
|
||
done
|
||
cname="$(nexus_container_name)"
|
||
if [[ -n "$cname" ]] && nexus_container_has_install_lock "$cname"; then
|
||
archive_install_wizard_in_container "$cname"
|
||
info "安装已锁定,跳过 install.html 热同步"
|
||
else
|
||
wiz_code="$(curl -s -o /dev/null -w '%{http_code}' "http://127.0.0.1:${port}/app/install.html" 2>/dev/null || echo "000")"
|
||
if [[ "$wiz_code" != "200" && -x "$NEXUS_ROOT/deploy/sync-install-wizard-to-container.sh" ]]; then
|
||
NEXUS_PUBLISH_PORT="$port" NEXUS_ROOT="$NEXUS_ROOT" bash "$NEXUS_ROOT/deploy/sync-install-wizard-to-container.sh"
|
||
fi
|
||
fi
|
||
set_install_complete "$NEXUS_ROOT"
|
||
if [[ "$PRUNE_IMAGES" == true ]]; then
|
||
docker image prune -f >/dev/null 2>&1 || true
|
||
fi
|
||
NEXUS_ROOT="$NEXUS_ROOT" bash "$NEXUS_ROOT/deploy/install-nx-cli.sh" 2>/dev/null || true
|
||
if [[ -x "$NEXUS_ROOT/deploy/sync_webapp_to_container.sh" ]]; then
|
||
step "同步 Git web/app 进容器(消除镜像 vite 缓存 hash 漂移)..."
|
||
NEXUS_PUBLISH_PORT="$port" NEXUS_ROOT="$NEXUS_ROOT" bash "$NEXUS_ROOT/deploy/sync_webapp_to_container.sh" || {
|
||
warn "web/app 同步失败,继续健康检查(请手动执行 sync_webapp_to_container.sh)"
|
||
}
|
||
fi
|
||
if ! verify_health "$NEXUS_ROOT" "升级"; then
|
||
rollback_compose_upgrade "$NEXUS_ROOT" || true
|
||
exit 1
|
||
fi
|
||
# shellcheck source=./detect_deploy_runtime.sh
|
||
if [[ -f "$NEXUS_ROOT/deploy/detect_deploy_runtime.sh" ]]; then
|
||
# shellcheck disable=SC1091
|
||
source "$NEXUS_ROOT/deploy/detect_deploy_runtime.sh"
|
||
if [[ -t 0 ]]; then
|
||
prompt_ops_cron_if_missing "$NEXUS_ROOT"
|
||
else
|
||
suggest_ops_cron "$NEXUS_ROOT"
|
||
fi
|
||
fi
|
||
local vcname
|
||
vcname="$(nexus_container_name)"
|
||
if [[ -n "$vcname" ]] && nexus_container_has_install_lock "$vcname" \
|
||
&& [[ -x "$NEXUS_ROOT/deploy/verify-1panel-install-wizard.sh" ]]; then
|
||
step "升级后验收..."
|
||
NEXUS_ROOT="$NEXUS_ROOT" bash "$NEXUS_ROOT/deploy/verify-1panel-install-wizard.sh" || \
|
||
warn "验收脚本报告 FAIL — 见上方输出"
|
||
fi
|
||
}
|
||
|
||
cmd_check() {
|
||
require_root
|
||
require_token
|
||
ensure_repo
|
||
git_fetch
|
||
show_pending || true
|
||
}
|
||
|
||
parse_common_install() {
|
||
while [[ $# -gt 0 ]]; do
|
||
case "$1" in
|
||
-h|--help) usage ;;
|
||
--skip-clone) SKIP_CLONE=true; shift ;;
|
||
--fresh) FRESH_INSTALL=true; shift ;;
|
||
--reuse-secrets) REUSE_SECRETS=true; shift ;;
|
||
--profile) NEXUS_PROFILE="$2"; shift 2 ;;
|
||
--cpus) CUSTOM_CPUS="$2"; shift 2 ;;
|
||
--mem-gb|--memory-gb) CUSTOM_MEM_GB="$2"; shift 2 ;;
|
||
--domain) NEXUS_DOMAIN="$2"; shift 2 ;;
|
||
--from-env) FROM_ENV="$2"; shift 2 ;;
|
||
--root) NEXUS_ROOT="$2"; shift 2 ;;
|
||
--no-backup) NO_BACKUP=true; shift ;;
|
||
--prune|--prune-images) PRUNE_IMAGES=true; shift ;;
|
||
--dry-run) DRY_RUN=true; shift ;;
|
||
*)
|
||
error "未知参数: $1"
|
||
usage
|
||
;;
|
||
esac
|
||
done
|
||
}
|
||
|
||
parse_common_upgrade() {
|
||
while [[ $# -gt 0 ]]; do
|
||
case "$1" in
|
||
-h|--help) usage ;;
|
||
--skip-git) SKIP_GIT=true; shift ;;
|
||
--profile) NEXUS_PROFILE="$2"; shift 2 ;;
|
||
--cpus) CUSTOM_CPUS="$2"; shift 2 ;;
|
||
--mem-gb|--memory-gb) CUSTOM_MEM_GB="$2"; shift 2 ;;
|
||
--root) NEXUS_ROOT="$2"; shift 2 ;;
|
||
--branch) GIT_BRANCH="$2"; shift 2 ;;
|
||
--no-backup) NO_BACKUP=true; shift ;;
|
||
--require-backup) REQUIRE_BACKUP=true; shift ;;
|
||
--prune|--prune-images) PRUNE_IMAGES=true; shift ;;
|
||
--no-cache|--rebuild) NO_CACHE=true; shift ;;
|
||
--dry-run) DRY_RUN=true; shift ;;
|
||
--check) CHECK_ONLY=true; shift ;;
|
||
*)
|
||
error "未知参数: $1"
|
||
usage
|
||
;;
|
||
esac
|
||
done
|
||
}
|
||
|
||
main() {
|
||
local cmd="${1:-install}"
|
||
shift || true
|
||
|
||
load_secrets
|
||
|
||
case "$cmd" in
|
||
install)
|
||
parse_common_install "$@"
|
||
cmd_install
|
||
;;
|
||
upgrade)
|
||
parse_common_upgrade "$@"
|
||
cmd_upgrade
|
||
;;
|
||
check)
|
||
CHECK_ONLY=true
|
||
parse_common_upgrade "$@"
|
||
cmd_check
|
||
;;
|
||
init-token)
|
||
cmd_init_token "${1:-}"
|
||
;;
|
||
ports)
|
||
cmd_ports
|
||
;;
|
||
-h|--help|help)
|
||
usage
|
||
;;
|
||
*)
|
||
error "未知子命令: $cmd"
|
||
usage
|
||
;;
|
||
esac
|
||
}
|
||
|
||
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
|
||
main "$@"
|
||
fi
|