Files
deploy-test/00-init-tools.sh
2026-04-21 12:24:04 +07:00

479 lines
19 KiB
Bash
Executable File
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env bash
# =============================================================================
# 00-init-tools.sh — 安装并配置服务器基础工具环境
#
# 功能:
# 1. 安装 Go默认 1.22.5,可通过参数指定版本)
# 2. 配置 GOPROXY自动测速选最快节点
# 3. 安装 Node.js / npm前端依赖
# 4. 安装 Docker基础设施容器
# 5. 安装 etcdctl查看 Etcd 服务注册)
# 6. 安装 Nginx 并写入 PC/CMS/Build-CMS/Build-Down/OpenIM 反代(本机 HTTP :80外部 HTTPS 由 LB/CDN 终止)
# 7. 写入 /etc/profile.d/deploy-env.sh永久生效
#
# 用法:
# ./deploy-test/00-init-tools.sh # 安装全部(含 Nginx 反代)
# ./deploy-test/00-init-tools.sh go # 只安装/配置 Go
# ./deploy-test/00-init-tools.sh node # 只安装 Node.js
# ./deploy-test/00-init-tools.sh docker # 只安装 Docker
# ./deploy-test/00-init-tools.sh goproxy # 只配置 GOPROXY
# sudo ./deploy-test/00-init-tools.sh etcdctl # 只安装 etcdctl需 root
# sudo ./deploy-test/00-init-tools.sh nginx # 只安装 Nginx 反代(需 root
#
# 前置条件: root 或 sudo 权限Ubuntu/Debian 系统
#
# 下一步: ./deploy-test/01-init-env.sh
# =============================================================================
set -euo pipefail
source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/common.sh"
init_dirs
init_script_log
# ── 可调参数 ───────────────────────────────────────────────────────────────────
GO_VERSION="${GO_VERSION:-1.22.5}"
GO_ARCH="${GO_ARCH:-amd64}" # amd64 / arm64
NODE_VERSION="${NODE_VERSION:-20}" # Node.js LTS 大版本
ETCDCTL_VERSION="${ETCDCTL_VERSION:-3.5.17}"
PROFILE_FILE="/etc/profile.d/deploy-env.sh"
TARGET="${1:-all}"
header "步骤 0 — 初始化工具环境"
echo " 目标: ${TARGET} (Go=${GO_VERSION}, Node=${NODE_VERSION})"
# ──────────────────────────────────────────────────────────────────────────────
# 辅助函数
# ──────────────────────────────────────────────────────────────────────────────
# 检查命令是否存在(不触发 set -e
_has() { command -v "$1" &>/dev/null; }
# 追加到全局 profile幂等同一行不重复写
_append_profile() {
local line="$1"
grep -qxF "$line" "$PROFILE_FILE" 2>/dev/null || echo "$line" >> "$PROFILE_FILE"
}
# 测试 GOPROXY 节点延迟,返回 ms超时返回 9999
_probe_proxy() {
local url="${1%/}/github.com/gin-gonic/gin/@v/list"
local ms
ms=$(curl -s -o /dev/null -w "%{time_total}" --max-time 5 "$url" 2>/dev/null || echo "9.999")
echo "${ms/./}" | sed 's/^0*//' | awk '{printf "%d\n", $1 * 1000 / 1000 + 0}'
# 简单地把秒转毫秒并取整
python3 -c "print(int(float('${ms}') * 1000))" 2>/dev/null || echo 9999
}
# ──────────────────────────────────────────────────────────────────────────────
# 1. Go
# ──────────────────────────────────────────────────────────────────────────────
_install_go() {
step "安装 Go ${GO_VERSION} (linux/${GO_ARCH})"
local tarball="go${GO_VERSION}.linux-${GO_ARCH}.tar.gz"
local url="https://go.dev/dl/${tarball}"
local tmp="/tmp/${tarball}"
if _has go; then
local cur
cur=$(go version | awk '{print $3}' | sed 's/go//')
if [[ "$cur" == "$GO_VERSION" ]]; then
success " Go ${GO_VERSION} 已安装,跳过"
return 0
fi
warn " 当前 Go 版本 ${cur},将升级到 ${GO_VERSION}"
fi
info " 下载 ${url}"
curl -fL --progress-bar -o "$tmp" "$url" || {
# 备用镜像
warn " go.dev 下载失败,尝试备用镜像..."
curl -fL --progress-bar -o "$tmp" "https://golang.google.cn/dl/${tarball}" || {
error " 下载失败,请手动安装: https://go.dev/dl/"
return 1
}
}
info " 解压到 /usr/local/go"
rm -rf /usr/local/go
tar -C /usr/local -xzf "$tmp"
rm -f "$tmp"
# 写入 profile
[[ -f "$PROFILE_FILE" ]] || touch "$PROFILE_FILE"
_append_profile 'export PATH=$PATH:/usr/local/go/bin'
_append_profile 'export GOPATH=/root/go'
_append_profile 'export GOMODCACHE=/root/go/pkg/mod'
# 当前会话也生效
export PATH=$PATH:/usr/local/go/bin
export GOPATH=/root/go
export GOMODCACHE=/root/go/pkg/mod
success " Go $(go version | awk '{print $3}') 安装完成 → /usr/local/go/bin/go"
}
# ──────────────────────────────────────────────────────────────────────────────
# 2. GOPROXY — 自动选最快节点
# ──────────────────────────────────────────────────────────────────────────────
_config_goproxy() {
step "配置 GOPROXY自动测速"
if ! _has go; then
warn " go 未安装,跳过 GOPROXY 配置"
return 0
fi
declare -A PROXIES=(
["proxy.golang.org"]="https://proxy.golang.org"
["goproxy.cn"]="https://goproxy.cn"
["goproxy.io"]="https://goproxy.io"
)
local best_name="goproxy.io"
local best_ms=9999
info " 测速中..."
for name in "${!PROXIES[@]}"; do
local url="${PROXIES[$name]}"
local ms
ms=$(python3 -c "
import urllib.request, time, sys
try:
t = time.time()
urllib.request.urlopen('${url}/github.com/gin-gonic/gin/@v/list', timeout=5)
print(int((time.time()-t)*1000))
except:
print(9999)
" 2>/dev/null)
echo " ${name}: ${ms}ms"
if (( ms < best_ms )); then
best_ms=$ms
best_name=$name
best_url="${url}"
fi
done
# 构建代理列表:最快的放第一
local proxy_list="${best_url},https://proxy.golang.org,https://goproxy.cn,direct"
# 去重
proxy_list=$(echo "$proxy_list" | tr ',' '\n' | awk '!seen[$0]++' | tr '\n' ',' | sed 's/,$//')
go env -w GOPROXY="${proxy_list}"
go env -w GONOSUMDB="*"
go env -w GOFLAGS=""
# 同步写入 profilego env -w 已持久化到 GOENV这里写 profile 作为双保险)
_append_profile "export GOPROXY=${proxy_list}"
_append_profile 'export GONOSUMDB=*'
success " GOPROXY=${proxy_list}"
success " 最快节点: ${best_name} (${best_ms}ms)"
}
# ──────────────────────────────────────────────────────────────────────────────
# 3. Node.js
# ──────────────────────────────────────────────────────────────────────────────
_install_node() {
step "安装 Node.js ${NODE_VERSION}.x LTS"
if _has node; then
local cur
cur=$(node --version)
info " 当前 Node.js 版本: ${cur}"
# 检查大版本是否匹配
local major
major=$(echo "$cur" | sed 's/v//' | cut -d. -f1)
if (( major >= NODE_VERSION )); then
success " Node.js >= ${NODE_VERSION},跳过"
return 0
fi
warn " 版本过旧,将升级"
fi
if _has apt-get; then
info " 使用 NodeSource 安装 Node.js ${NODE_VERSION}.x"
curl -fsSL "https://deb.nodesource.com/setup_${NODE_VERSION}.x" | bash -
apt-get install -y nodejs
elif _has yum; then
curl -fsSL "https://rpm.nodesource.com/setup_${NODE_VERSION}.x" | bash -
yum install -y nodejs
else
warn " 未找到 apt-get/yum请手动安装 Node.js: https://nodejs.org/"
return 1
fi
# 安装常用全局包管理器
npm install -g pnpm yarn 2>/dev/null || true
_append_profile 'export PATH=$PATH:/usr/local/bin'
# 将 GitHub SSH 地址重写为 HTTPS服务器通常没有 GitHub SSH 密钥)
git config --global url."https://github.com/".insteadOf "git+ssh://git@github.com/" 2>/dev/null || true
git config --global url."https://github.com/".insteadOf "ssh://git@github.com/" 2>/dev/null || true
git config --global url."https://github.com/".insteadOf "git@github.com:" 2>/dev/null || true
success " Node.js $(node --version)npm $(npm --version) 安装完成"
_has pnpm && success " pnpm $(pnpm --version)"
_has yarn && success " yarn $(yarn --version)"
}
# ──────────────────────────────────────────────────────────────────────────────
# 4. Docker
# ──────────────────────────────────────────────────────────────────────────────
_install_docker() {
step "安装 Docker"
if _has docker; then
success " Docker $(docker --version | awk '{print $3}' | tr -d ',') 已安装,跳过"
return 0
fi
if _has apt-get; then
info " 使用官方脚本安装 Docker..."
curl -fsSL https://get.docker.com | sh
elif _has yum; then
yum install -y docker
systemctl enable docker
else
warn " 未找到 apt-get/yum请手动安装 Docker: https://docs.docker.com/engine/install/"
return 1
fi
# 启动服务
systemctl start docker 2>/dev/null || service docker start 2>/dev/null || true
success " Docker $(docker --version | awk '{print $3}' | tr -d ',') 安装完成"
}
# ──────────────────────────────────────────────────────────────────────────────
# 5. etcdctl
# ──────────────────────────────────────────────────────────────────────────────
_install_etcdctl() {
step "安装 etcdctl ${ETCDCTL_VERSION}"
if _has etcdctl; then
success " etcdctl 已安装: $(etcdctl version | head -1)"
return 0
fi
if [[ "$(id -u)" -ne 0 ]]; then
warn " etcdctl 安装需 root请执行: sudo $0 etcdctl"
return 0
fi
if _has apt-get; then
apt-get update -y
if apt-get install -y etcd-client; then
success " etcdctl 安装完成: $(etcdctl version | head -1)"
return 0
fi
warn " apt 安装 etcd-client 失败,尝试下载官方二进制"
elif _has dnf; then
if dnf install -y etcd; then
success " etcdctl 安装完成: $(etcdctl version | head -1)"
return 0
fi
warn " dnf 安装 etcd 失败,尝试下载官方二进制"
elif _has yum; then
if yum install -y etcd; then
success " etcdctl 安装完成: $(etcdctl version | head -1)"
return 0
fi
warn " yum 安装 etcd 失败,尝试下载官方二进制"
fi
local arch
case "$(uname -m)" in
x86_64|amd64) arch="amd64" ;;
aarch64|arm64) arch="arm64" ;;
*)
error " 不支持的架构: $(uname -m),请手动安装 etcdctl"
return 1
;;
esac
local tarball="etcd-v${ETCDCTL_VERSION}-linux-${arch}.tar.gz"
local url="https://github.com/etcd-io/etcd/releases/download/v${ETCDCTL_VERSION}/${tarball}"
local tmp="/tmp/${tarball}"
local out_dir="/tmp/etcd-v${ETCDCTL_VERSION}-linux-${arch}"
info " 下载 ${url}"
curl -fL --progress-bar -o "$tmp" "$url"
rm -rf "$out_dir"
tar -C /tmp -xzf "$tmp"
install -m 0755 "${out_dir}/etcdctl" /usr/local/bin/etcdctl
rm -rf "$tmp" "$out_dir"
success " etcdctl 安装完成: $(etcdctl version | head -1)"
}
# ──────────────────────────────────────────────────────────────────────────────
# 6. Nginx — PC / CMS / Build-CMS / Build-Down / OpenIM 统一入口(本机 HTTP :80
# ──────────────────────────────────────────────────────────────────────────────
_install_pc_nginx_proxy() {
step "安装 Nginx 并配置 OpenIM/PC/CMS/Build-CMS/Build-Down 反代"
if [[ "$(id -u)" -ne 0 ]]; then
error " Nginx 安装需 root请执行: sudo $0 nginx"
return 1
fi
local script_dir
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
local conf_src="${script_dir}/nginx/openim-pc-proxy.conf"
local conf_name="openim-pc-proxy.conf"
local proxy_domain="${PC_PROXY_DOMAIN:-pc-jack.imharry.work}"
if [[ -f "$ENV_FILE" ]]; then
# shellcheck source=/dev/null
source "$ENV_FILE"
proxy_domain="${PC_PROXY_DOMAIN:-$proxy_domain}"
fi
if [[ ! -f "$conf_src" ]]; then
error " 找不到配置: $conf_src"
return 1
fi
if ! _has nginx; then
if _has apt-get; then
apt-get update -y
apt-get install -y nginx
elif _has dnf; then
dnf install -y nginx
elif _has yum; then
yum install -y nginx
else
error " 未检测到 apt/dnf/yum请先手动安装 nginx"
return 1
fi
fi
if [[ -d /etc/nginx/sites-available ]]; then
install -m 0644 "$conf_src" "/etc/nginx/sites-available/${conf_name}"
mkdir -p /etc/nginx/sites-enabled
ln -sf "/etc/nginx/sites-available/${conf_name}" "/etc/nginx/sites-enabled/${conf_name}"
if [[ -f /etc/nginx/sites-enabled/default ]]; then
rm -f /etc/nginx/sites-enabled/default
info " 已移除 sites-enabled/default避免与 openim-pc-proxy 冲突"
fi
# 保留 sites-available/default 文件时,去掉 default_server防止日后误启用再次抢占 :80
if [[ -f /etc/nginx/sites-available/default ]]; then
sed -i.bak-openim \
-e 's/listen \[::\]:80 default_server;/listen [::]:80;/g' \
-e 's/listen 80 default_server;/listen 80;/g' \
/etc/nginx/sites-available/default 2>/dev/null || true
info " 已去除 sites-available/default 中的 default_server若存在"
fi
else
install -m 0644 "$conf_src" "/etc/nginx/conf.d/${conf_name}"
fi
nginx -t
systemctl enable nginx 2>/dev/null || true
systemctl restart nginx
local health_failed=0
local host
for host in \
"$proxy_domain" \
"cms-jack.imharry.work" \
"build-jack.imharry.work" \
"down-jack.imharry.work"
do
if curl -fsS --max-time 3 -H "Host: ${host}" http://127.0.0.1/nginx-health >/dev/null 2>&1; then
success " Host 路由已生效: ${host}"
else
error " Host 路由检查失败: ${host}"
health_failed=1
fi
done
success " Nginx 反代已启用(配置: $conf_src"
info " 本机 Nginx 仅监听 TCP 80curl -sS -H 'Host: ${proxy_domain}' http://127.0.0.1/nginx-health 应返回 ok"
info " 外部 HTTPS 可由 LB/CDN/其它网关终止后转发到本机 :80"
info " PC 入口: https://${proxy_domain}/"
info " CMS 入口: http://cms-jack.imharry.work/"
info " Build CMS 入口: http://build-jack.imharry.work/"
info " Build Down 入口: http://down-jack.imharry.work/"
if [[ "$health_failed" -ne 0 ]]; then
error " Nginx 已重启,但部分 Host 路由校验失败,请检查 server_name / 默认站点 / 其它占用 :80 的配置"
return 1
fi
}
# all 时非 root 则跳过(不中断 Go/Node/Docker/etcdctl
_run_nginx_if_root() {
if [[ "$(id -u)" -eq 0 ]]; then
_install_pc_nginx_proxy
else
warn " 当前非 root已跳过 Nginx。需要时在服务器上执行: sudo $0 nginx"
fi
}
# ──────────────────────────────────────────────────────────────────────────────
# 执行
# ──────────────────────────────────────────────────────────────────────────────
case "$TARGET" in
go)
_install_go
_config_goproxy
;;
goproxy)
_config_goproxy
;;
node)
_install_node
;;
docker)
_install_docker
;;
etcdctl)
_install_etcdctl
;;
nginx)
_install_pc_nginx_proxy
;;
all)
_install_go
_config_goproxy
_install_node
_install_docker
_install_etcdctl
_run_nginx_if_root
;;
*)
error "未知目标: $TARGET"
echo "用法: $0 [all|go|goproxy|node|docker|etcdctl|nginx]"
exit 1
;;
esac
# ──────────────────────────────────────────────────────────────────────────────
# 汇总
# ──────────────────────────────────────────────────────────────────────────────
echo ""
success "环境初始化完成!"
echo ""
echo -e "${BOLD}当前工具版本:${NC}"
_has go && echo " Go: $(go version | awk '{print $3, $4}')" || echo " Go: 未安装"
_has node && echo " Node: $(node --version)" || echo " Node: 未安装"
_has npm && echo " npm: $(npm --version)" || echo " npm: 未安装"
_has pnpm && echo " pnpm: $(pnpm --version)" || echo " pnpm: 未安装"
_has yarn && echo " yarn: $(yarn --version)" || echo " yarn: 未安装"
_has docker && echo " Docker: $(docker --version | awk '{print $3}' | tr -d ',')" || echo " Docker: 未安装"
_has etcdctl && echo " etcdctl: $(etcdctl version | head -1 | sed 's/^etcdctl version: //')" || echo " etcdctl: 未安装"
_has nginx && echo " Nginx: $(nginx -v 2>&1 | sed 's/^nginx version: //')" || echo " Nginx: 未安装"
echo ""
echo -e "${BOLD}GOPROXY 配置:${NC}"
_has go && go env GOPROXY || echo " (go 未安装)"
echo ""
echo -e "${YELLOW}注意: 新终端需执行以下命令使环境变量生效:${NC}"
echo -e " ${CYAN}source /etc/profile.d/deploy-env.sh${NC}"
echo -e " 或重新登录 SSH"
echo ""
echo -e "${BOLD}下一步:${NC}"
echo -e " ${CYAN}./deploy-test/01-init-env.sh${NC}"