用户反馈:部署时报错只说「未找到 kubectl,请先安装」「kubectl 无法连接集群,
请检查 kubeconfig」,对新人等于没说 —— 不知道是没装工具、网络不通、凭据过期
还是 RBAC 没权限,也不知道下一步查什么。
原实现只有两行:
command -v kubectl || die "未找到 kubectl,请先安装"
kubectl version --request-timeout=5s || die "kubectl 无法连接集群,请检查 kubeconfig"
改为三级预检 + 一级软检查:
【0.1 kubectl 是否存在】
按 CentOS/TencentOS、Ubuntu/Debian、macOS 分别给安装命令;
额外给「机器没有外网」的替代路径(控制台下载 / 内网跳板机 / 只 scp 二进制)。
【0.2 kubeconfig 文件是否存在】← 新增,堵一个误判
KUBECONFIG 指向不存在的文件时,kubectl 会忽略它并退回默认规则,
最终报出 "Authentication required" / "connection refused" 这类误导性错误,
让人白查凭据和网络。现在单独判一次,直接给出查找路径 + 各云控制台下载入口。
支持 KUBECONFIG 用 : 分隔多路径,任一存在即通过。
【0.3 能否连上 API Server】按错误类型分类,各给专属修法
· context 不存在 → get-contexts / use-context / 重新下载
· 401 认证失败 → 云厂商 token 有效期几小时,重新下载;自建查客户端证书
· 403 RBAC 拒绝 → auth whoami / auth can-i --list,列出部署所需的具体资源
· x509 证书问题 → 确认证书过期;可信内网可临时 insecure-skip-tls-verify
· 网络不通 → curl -k <API>/version、VPN/专线、安全组放行 6443、换跳板机
· 其他 → 原样打印 kubectl 错误 + 通用排查清单
【0.4 部署权限软检查】auth can-i 非 yes 时只告警不阻断
(首次部署时目标命名空间可能还不存在,硬拦会误伤正常流程)
失败时统一打印环境信息(kubeconfig 路径 / context / API Server / 当前用户),
一眼看出连的是哪个集群。连接超时可用 KUBECTL_TIMEOUT=15s 放宽(默认 8s)。
README 同步新增「前置条件」章节:预检项对照表 + 错误分类表 + 失败输出示例。
=== 验证 ===
从真实 install.sh 抽出预检段,用假 kubectl 精确模拟 8 种场景(两个脚本各跑一遍):
A 没有 kubectl → 退出 1,打印各系统安装命令 ✅
B API Server 超时 → 退出 1,提示 VPN/专线/安全组 ✅
C 没有 context → 退出 1,提示 get-contexts/use-context ✅
D 401 凭据过期 → 退出 1,提示重新下载 kubeconfig ✅
E 403 权限不足 → 退出 1,列出所需资源权限 ✅
F x509 证书过期 → 退出 1,提示证书/insecure 排障 ✅
G 正常环境(真实集群) → 通过预检,退出 0 ✅
H kubeconfig 文件不存在 → 退出 1,提示下载位置(不误报成 401)✅
结果:阻断 7/7、正常通过 1/1;bash -n 语法检查通过
⚠️ 踩坑记录:kubeconfig 多路径判断最初写成
while IFS= read -r _p; do ...; done < <(printf "%s" "$PATHS" | tr ":" "\n")
printf 输出没有结尾换行 → read 带着数据返回非 0(EOF)→ 循环体一次都不执行,
导致所有场景都误报「找不到 kubeconfig」。改用 IFS=":" + for 拆分。
498 lines
24 KiB
Bash
Executable File
498 lines
24 KiB
Bash
Executable File
#!/usr/bin/env bash
|
||
# ============================================================================
|
||
# lokistack 一键部署
|
||
#
|
||
# 自动适配:阿里云 ACK / 腾讯云 TKE / AWS EKS / 华为云 CCE / GCP GKE / 自建 K8s
|
||
# 命名空间固定:log
|
||
# 幂等:重复执行不覆盖密钥、不重建数据卷
|
||
#
|
||
# 用法:
|
||
# bash kubernetes/install.sh # 自动探测
|
||
# WITH_NODEPORT=1 bash kubernetes/install.sh # 同时开 NodePort(配 ingress)
|
||
# STORAGE_CLASS=local-path bash kubernetes/install.sh # 手动指定 StorageClass
|
||
# ============================================================================
|
||
set -euo pipefail
|
||
|
||
NS="log"
|
||
WITH_NODEPORT="${WITH_NODEPORT:-0}"
|
||
STORAGE_CLASS="${STORAGE_CLASS:-}"
|
||
DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||
|
||
info() { printf '\033[1;36m==>\033[0m %s\n' "$*"; }
|
||
ok() { printf '\033[1;32m✅\033[0m %s\n' "$*"; }
|
||
warn() { printf '\033[1;33m⚠️ \033[0m %s\n' "$*"; }
|
||
die() { printf '\033[1;31m❌\033[0m %s\n' "$*" >&2; exit 1; }
|
||
|
||
# ============================================================================
|
||
# 0) 前置检查
|
||
#
|
||
# 分三级:kubectl 是否存在 → 能否连上集群 → 有没有部署权限。
|
||
# 设计原则:每级失败都要说清「是哪一类问题 + 下一步具体做什么」。
|
||
# 只丢一句「请检查 kubeconfig」等于没说 —— 新人根本不知道该查什么,
|
||
# 也不知道是没装工具、网络不通、凭据过期、还是 RBAC 没权限。
|
||
# ============================================================================
|
||
KUBECTL_TIMEOUT="${KUBECTL_TIMEOUT:-8s}"
|
||
|
||
# 当前 context / server / user:失败时一起打印,一眼看出连的是哪个集群
|
||
k8s_env_info() {
|
||
local ctx server user kc
|
||
ctx=$(kubectl config current-context 2>/dev/null || echo "(无)")
|
||
server=$(kubectl config view --minify -o jsonpath='{.clusters[0].cluster.server}' 2>/dev/null || true)
|
||
user=$(kubectl config view --minify -o jsonpath='{.users[0].name}' 2>/dev/null || true)
|
||
kc="${KUBECONFIG:-$HOME/.kube/config}"
|
||
printf ' kubeconfig : %s\n' "$kc" >&2
|
||
printf ' 当前 context : %s\n' "$ctx" >&2
|
||
printf ' API Server : %s\n' "${server:-(未知)}" >&2
|
||
printf ' 当前用户 : %s\n' "${user:-(未知)}" >&2
|
||
}
|
||
|
||
# 统一失败出口:一句话结论 + 环境信息 + 可执行的处理步骤
|
||
k8s_die() {
|
||
printf '\n\033[1;31m❌ %s\033[0m\n\n' "$1" >&2
|
||
k8s_env_info
|
||
printf '\n%b\n\n' "$2" >&2
|
||
exit 1
|
||
}
|
||
|
||
# ── 0.1 kubectl 是否存在 ──────────────────────────────────────────────
|
||
if ! command -v kubectl >/dev/null 2>&1; then
|
||
printf '\n\033[1;31m❌ 找不到 kubectl 命令\033[0m\n\n' >&2
|
||
cat >&2 <<'KUBECTL_INSTALL_HELP'
|
||
本脚本通过 kubectl 操作 K8s 集群,需要先安装这个命令行工具。
|
||
|
||
── 安装方式 ───────────────────────────────────────────────────────
|
||
通用 Linux(官方二进制,最省事):
|
||
curl -LO "https://dl.k8s.io/release/$(curl -Ls https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl"
|
||
sudo install -o root -g root -m 0755 kubectl /usr/local/bin/kubectl
|
||
|
||
CentOS / TencentOS / RHEL : sudo yum install -y kubectl
|
||
Ubuntu / Debian : sudo apt-get update && sudo apt-get install -y kubectl
|
||
macOS : brew install kubectl
|
||
|
||
── 装完自检 ───────────────────────────────────────────────────────
|
||
kubectl version --client
|
||
|
||
── 这台机器没有外网怎么办 ─────────────────────────────────────────
|
||
· 云控制台「集群 → 连接信息」一般可直接下载 kubectl 与 kubeconfig
|
||
· 或者在内网跳板机 / 能访问集群的机器上执行本脚本
|
||
· 只需把 kubectl 二进制 scp 过来即可,不依赖 Docker / Jenkins
|
||
KUBECTL_INSTALL_HELP
|
||
exit 1
|
||
fi
|
||
|
||
# ── 0.2 kubeconfig 文件是否存在 ────────────────────────────────────────
|
||
# 必须先单独判一次:KUBECONFIG 指向不存在的文件时,kubectl 会忽略它并
|
||
# 退回默认规则,最终报出的往往是 "Authentication required"/"connection refused"
|
||
# 这类**误导性**错误,让人以为是凭据或网络问题,实际只是配置没放对位置。
|
||
_KC_PATHS="${KUBECONFIG:-$HOME/.kube/config}"
|
||
# KUBECONFIG 允许用 ':' 分隔多个路径,任意一个存在即算通过。
|
||
# 注意不要写成 while read 循环:printf 输出没有结尾换行时,read 会
|
||
# 带着数据返回非 0(EOF),`while read` 的循环体一次都不会执行 —— 踩过。
|
||
_kc_found=0
|
||
_OLD_IFS="${IFS}"
|
||
IFS=':'
|
||
for _p in ${_KC_PATHS}; do
|
||
if [ -n "${_p}" ] && [ -f "${_p}" ]; then
|
||
_kc_found=1
|
||
break
|
||
fi
|
||
done
|
||
IFS="${_OLD_IFS}"
|
||
|
||
if [ "${_kc_found}" -eq 0 ]; then
|
||
k8s_die "找不到 kubeconfig 文件(不知道该连哪个集群)" \
|
||
" 查找位置: ${_KC_PATHS}
|
||
(KUBECONFIG 环境变量未设置时,默认只找 ~/.kube/config)
|
||
|
||
── 怎么修 ─────────────────────────────────────────────────────────
|
||
1) 从云控制台下载 kubeconfig:
|
||
腾讯云 TKE : 集群 → 基本信息 → 连接信息 → 下载 kubeconfig
|
||
阿里云 ACK : 集群 → 集群信息 → 连接信息 → 复制/下载 kubeconfig
|
||
AWS EKS : aws eks update-kubeconfig --name <集群名> --region <区域>
|
||
2) 放到默认位置(推荐):
|
||
mkdir -p ~/.kube && mv <下载的文件> ~/.kube/config && chmod 600 ~/.kube/config
|
||
3) 或者用环境变量指向任意路径:
|
||
export KUBECONFIG=/path/to/kubeconfig
|
||
4) 确认生效: kubectl config get-contexts
|
||
5) 重新执行本脚本"
|
||
|
||
fi
|
||
|
||
# ── 0.3 能否连上集群(探测一次,按错误类型分别给建议)────────────────
|
||
if ! PROBE_OUT="$(kubectl version --request-timeout="${KUBECTL_TIMEOUT}" 2>&1)"; then
|
||
case "${PROBE_OUT}" in
|
||
*"does not exist"*|*"no context exists"*|*"current-context is not set"*|*"context was not found"*)
|
||
k8s_die "kubeconfig 里没有可用的 context(集群配置不对)" \
|
||
" ── 怎么修 ─────────────────────────────────────────────────────────
|
||
1) 确认 kubeconfig 在哪: echo \$KUBECONFIG ls -l ~/.kube/config
|
||
2) 看有哪些 context: kubectl config get-contexts
|
||
3) 切到正确的那一个: kubectl config use-context <名字>
|
||
4) 完全没有配置就下载: 腾讯云 → 集群 → 基本信息 → 连接信息 → 下载 kubeconfig
|
||
放到 ~/.kube/config,或 export KUBECONFIG=/path/to/config
|
||
5) 重新执行本脚本" ;;
|
||
|
||
*"Unauthorized"*|*"Authentication required"*|*"credentials"*|*"invalid bearer token"*)
|
||
k8s_die "集群认证失败(凭据无效或已过期)" \
|
||
" ── 怎么修 ─────────────────────────────────────────────────────────
|
||
· 云厂商 kubeconfig 里一般是**临时 token**,有效期几小时,过期就 401
|
||
→ 回控制台重新下载 kubeconfig,覆盖 ~/.kube/config
|
||
· 自建集群看客户端证书是否过期:
|
||
kubectl config view --minify --raw | grep client-certificate-data
|
||
· 验证当前身份: kubectl auth whoami (旧版用 kubectl get ns 试)" ;;
|
||
|
||
*"Forbidden"*|*"(403)"*)
|
||
k8s_die "已经连上集群,但当前账号没有权限(RBAC 拒绝)" \
|
||
" ── 怎么修 ─────────────────────────────────────────────────────────
|
||
· 确认身份: kubectl auth whoami
|
||
· 看能干什么: kubectl auth can-i --list -n ${NS}
|
||
· 部署本平台需要在 ${NS} 命名空间具备:
|
||
deployments.apps / services / persistentvolumeclaims / configmaps / secrets 的 create、get
|
||
若该命名空间还不存在,还需要集群级 create namespaces
|
||
· 找集群管理员绑定具备上述权限的 Role,或直接用管理员 kubeconfig 执行本脚本" ;;
|
||
|
||
*"x509"*|*"certificate"*|*"tls:"*|*"TLS"*)
|
||
k8s_die "集群证书校验失败(证书过期 / 域名不匹配 / 自签证书)" \
|
||
" ── 怎么修 ─────────────────────────────────────────────────────────
|
||
· 云托管集群极少出现,多见于自建集群;先确认 API Server 证书是否过期
|
||
· 可信内网内临时跳过校验(仅用于排障,不要长期使用):
|
||
kubectl --insecure-skip-tls-verify=true get ns
|
||
· 检查 kubeconfig 里 server 的地址/域名是否在证书 SAN 内" ;;
|
||
|
||
*"connection refused"*|*"no such host"*|*"i/o timeout"*|*"context deadline exceeded"*|*"network is unreachable"*|*"Unable to connect to the server"*|*"timeout"*)
|
||
k8s_die "连不上集群 API Server(网络不通)" \
|
||
" 实际报错: $(printf '%s' "${PROBE_OUT}" | tail -1)
|
||
|
||
── 按顺序排查 ───────────────────────────────────────────────────────
|
||
1) 地址对不对?用上面打印的 API Server 测一下:
|
||
curl -k --max-time 5 <API Server>/version
|
||
2) 要不要 VPN / 专线?云上 API Server 常是**内网地址**(10.x / 172.16~31.x / 192.168.x),
|
||
本机不在同一 VPC 时,必须先连 VPN、走专线,或把本机 IP 加进安全组白名单
|
||
3) 安全组 / 防火墙:云厂商默认只放行运维网段访问 6443 端口
|
||
4) 集群本身是否正常:控制台看集群状态是否「运行中」
|
||
5) 换台机器试:在能访问集群的跳板机上执行本脚本,可排除本机网络问题" ;;
|
||
|
||
*)
|
||
k8s_die "kubectl 无法连接集群" \
|
||
" 实际报错:
|
||
$(printf '%s\n' "${PROBE_OUT}" | sed 's/^/ /')
|
||
|
||
── 通用排查 ─────────────────────────────────────────────────────────
|
||
· kubectl config get-contexts # 当前用的是哪个 context
|
||
· kubectl cluster-info # API Server 是否可达
|
||
· echo \$KUBECONFIG # 确认读到的是哪个 kubeconfig
|
||
· 容器/精简镜像里执行时,注意镜像内可能没有 ca-certificates 与 kubectl" ;;
|
||
|
||
esac
|
||
fi
|
||
|
||
# ── 0.4 部署权限预检(只告警,不阻断)────────────────────────────────
|
||
# 说明:第一次部署时目标命名空间可能还不存在,can-i 对命名空间级资源会返回 no,
|
||
# 为避免误拦正常流程这里只提示;真正的拒绝会在 apply 阶段以 Forbidden 暴露。
|
||
if [ "$(kubectl auth can-i create deployments.apps -n "${NS}" --request-timeout="${KUBECTL_TIMEOUT}" 2>/dev/null || echo unknown)" != "yes" ]; then
|
||
warn "当前账号在命名空间 ${NS} 未确认具备部署权限(kubectl auth can-i 非 yes)"
|
||
warn "若随后 apply 报 Forbidden:请改用管理员 kubeconfig,或让管理员绑定 Role 后再执行"
|
||
fi
|
||
|
||
# ============================================================================
|
||
# 0.1) 云平台识别(信息展示 + 后续诊断用)
|
||
# ============================================================================
|
||
detect_cloud() {
|
||
local pid=""
|
||
pid=$(kubectl get nodes -o jsonpath='{.items[0].spec.providerID}' 2>/dev/null || true)
|
||
case "${pid}" in
|
||
qcloud://*) echo "腾讯云 TKE" ;;
|
||
aws://*) echo "AWS EKS" ;;
|
||
aliyun://*) echo "阿里云 ACK" ;;
|
||
huaweicloud*|huawei*) echo "华为云 CCE" ;;
|
||
gce://*) echo "GCP GKE" ;;
|
||
azure://*) echo "Azure AKS" ;;
|
||
k3s://*) echo "K3s(自建)" ;;
|
||
*) echo "自建 / 其他 K8s" ;;
|
||
esac
|
||
}
|
||
|
||
# ============================================================================
|
||
# 0.2) StorageClass 探测
|
||
# 优先级:显式 STORAGE_CLASS > default 注解 > 唯一 SC > 失败
|
||
# 注意:结果写入全局变量(RESOLVED_SC / SC_ERROR),不要用 $(...) 调用,
|
||
# 否则子 shell 里的赋值无法回传
|
||
# ============================================================================
|
||
RESOLVED_SC=""
|
||
SC_ERROR=""
|
||
resolve_storage_class() {
|
||
local sc="" all="" count=0
|
||
RESOLVED_SC=""
|
||
SC_ERROR=""
|
||
|
||
# 1) 显式指定
|
||
if [ -n "${STORAGE_CLASS}" ]; then
|
||
if ! kubectl get sc "${STORAGE_CLASS}" >/dev/null 2>&1; then
|
||
SC_ERROR="指定的 StorageClass '${STORAGE_CLASS}' 不存在。现有:$(kubectl get sc -o jsonpath='{range .items[*]}{.metadata.name}{" "}{end}' 2>/dev/null)"
|
||
return 1
|
||
fi
|
||
RESOLVED_SC="${STORAGE_CLASS}"; return 0
|
||
fi
|
||
|
||
# 2) 带 default 注解的 SC
|
||
sc=$(kubectl get sc -o jsonpath='{range .items[?(@.metadata.annotations.storageclass\.kubernetes\.io/is-default-class=="true")]}{.metadata.name}{"\n"}{end}' 2>/dev/null | head -1 || true)
|
||
if [ -n "${sc}" ]; then RESOLVED_SC="${sc}"; return 0; fi
|
||
|
||
# 3) 集群里只有一个 SC → 直接用它
|
||
all=$(kubectl get sc -o jsonpath='{range .items[*]}{.metadata.name}{"\n"}{end}' 2>/dev/null || true)
|
||
count=$(printf '%s' "${all}" | grep -c . || true)
|
||
if [ "${count}" = "1" ]; then RESOLVED_SC="${all}"; return 0; fi
|
||
|
||
# 4) 无法确定
|
||
return 1
|
||
}
|
||
|
||
# ============================================================================
|
||
# 0.3) 自建集群兜底:无任何 StorageClass 时,创建「静态 local PV」
|
||
#
|
||
# 设计取舍:
|
||
# - 零外部依赖:不拉 local-path-provisioner 镜像(国内网络常常拉不到)
|
||
# - hostPath + DirectoryOrCreate:kubelet 自动建目录,无需预先 mkdir
|
||
# - nodeAffinity 锁节点:Pod 永远调度到数据所在节点
|
||
# - WaitForFirstConsumer:先定 Pod 再绑 PV,避免跨节点错绑
|
||
# - reclaimPolicy: Retain:误删 PVC 不丢数据
|
||
#
|
||
# 代价:数据固定在一台节点,节点损毁需从备份恢复(README 有 CSI 备份说明)
|
||
# ============================================================================
|
||
LOCAL_SC_NAME="lokistack-local"
|
||
LOCAL_PV_NAME="lokistack-data-pv"
|
||
LOCAL_PV_SIZE="${PV_SIZE:-30Gi}"
|
||
LOCAL_PV_PATH="${PV_PATH:-/var/lib/lokistack-data}"
|
||
|
||
pick_local_node() {
|
||
if [ -n "${NODE_NAME:-}" ]; then echo "${NODE_NAME}"; return 0; fi
|
||
kubectl get nodes --no-headers \
|
||
-o custom-columns=NAME:.metadata.name,READY:.status.conditions[-1].status 2>/dev/null \
|
||
| awk '$2=="True"{print $1; exit}'
|
||
}
|
||
|
||
create_local_storage() {
|
||
local node
|
||
node="$(pick_local_node)"
|
||
[ -n "${node}" ] || { warn "找不到 Ready 节点,无法创建本地存储"; return 1; }
|
||
|
||
info "创建节点本地存储:节点=${node} 路径=${LOCAL_PV_PATH} 容量=${LOCAL_PV_SIZE}"
|
||
|
||
cat <<EOF | kubectl apply -f - >/dev/null
|
||
apiVersion: storage.k8s.io/v1
|
||
kind: StorageClass
|
||
metadata:
|
||
name: ${LOCAL_SC_NAME}
|
||
provisioner: kubernetes.io/no-provisioner
|
||
volumeBindingMode: WaitForFirstConsumer
|
||
reclaimPolicy: Retain
|
||
EOF
|
||
|
||
cat <<EOF | kubectl apply -f - >/dev/null
|
||
apiVersion: v1
|
||
kind: PersistentVolume
|
||
metadata:
|
||
name: ${LOCAL_PV_NAME}
|
||
labels:
|
||
app: lokistack
|
||
spec:
|
||
capacity:
|
||
storage: ${LOCAL_PV_SIZE}
|
||
accessModes:
|
||
- ReadWriteOnce
|
||
persistentVolumeReclaimPolicy: Retain
|
||
storageClassName: ${LOCAL_SC_NAME}
|
||
hostPath:
|
||
path: ${LOCAL_PV_PATH}
|
||
type: DirectoryOrCreate
|
||
nodeAffinity:
|
||
required:
|
||
nodeSelectorTerms:
|
||
- matchExpressions:
|
||
- key: kubernetes.io/hostname
|
||
operator: In
|
||
values:
|
||
- ${node}
|
||
EOF
|
||
|
||
ok "已创建 StorageClass ${LOCAL_SC_NAME} + PV ${LOCAL_PV_NAME}(节点 ${node})"
|
||
warn "注意:数据固定在该节点 ${LOCAL_PV_PATH},Pod 不会漂移到其他节点"
|
||
RESOLVED_SC="${LOCAL_SC_NAME}"
|
||
return 0
|
||
}
|
||
|
||
CLOUD="$(detect_cloud)"
|
||
info "lokistack 部署开始(namespace: ${NS})"
|
||
info "集群: $(kubectl config current-context 2>/dev/null || echo unknown)"
|
||
info "平台: ${CLOUD}"
|
||
|
||
# LOCAL_STORAGE 三态:auto(默认,无 SC 时自动建)/ off(只提示)/ force(强制建,测试用)
|
||
LOCAL_STORAGE="${LOCAL_STORAGE:-auto}"
|
||
|
||
if [ "${LOCAL_STORAGE}" = "force" ]; then
|
||
warn "LOCAL_STORAGE=force:强制使用节点本地存储"
|
||
create_local_storage || die "创建节点本地存储失败"
|
||
elif ! resolve_storage_class; then
|
||
if [ -n "${SC_ERROR}" ]; then
|
||
die "${SC_ERROR}"
|
||
fi
|
||
echo ""
|
||
if [ "${LOCAL_STORAGE}" = "auto" ]; then
|
||
warn "集群没有可用 StorageClass(自建集群常见)→ 自动启用节点本地存储"
|
||
create_local_storage || die "创建节点本地存储失败"
|
||
else
|
||
warn "未找到可用的 StorageClass,且 LOCAL_STORAGE=off"
|
||
echo ""
|
||
echo " 现有 StorageClass:"
|
||
kubectl get sc 2>/dev/null | sed 's/^/ /' || echo " (无)"
|
||
echo ""
|
||
echo " 解决办法(选一个):"
|
||
echo " 1) 手动指定(推荐,若已有可用 SC):"
|
||
echo " STORAGE_CLASS=<sc名称> bash kubernetes/install.sh"
|
||
echo ""
|
||
echo " 2) 标记某个 SC 为默认:"
|
||
echo " kubectl patch sc <sc名称> -p '{\"metadata\":{\"annotations\":{\"storageclass.kubernetes.io/is-default-class\":\"true\"}}}'"
|
||
echo ""
|
||
echo " 3) 自建集群安装 local-path-provisioner(动态供给,支持多 PVC):"
|
||
echo " kubectl apply -f https://raw.githubusercontent.com/rancher/local-path-provisioner/master/deploy/local-path-storage.yaml"
|
||
echo " STORAGE_CLASS=local-path bash kubernetes/install.sh"
|
||
echo ""
|
||
echo " 4) 让脚本用节点本地存储(零依赖):"
|
||
echo " 去掉 LOCAL_STORAGE=off 重跑即可"
|
||
echo ""
|
||
die "StorageClass 未就绪,部署中止(避免 PVC 永久 Pending)"
|
||
fi
|
||
else
|
||
ok "StorageClass: ${RESOLVED_SC}"
|
||
fi
|
||
|
||
# ============================================================================
|
||
# 1) 命名空间
|
||
# ============================================================================
|
||
kubectl apply -f "${DIR}/namespace.yaml"
|
||
|
||
# ============================================================================
|
||
# 2) Secret(幂等:已存在则跳过,绝不覆盖)
|
||
# ============================================================================
|
||
rand_hex() {
|
||
if command -v openssl >/dev/null 2>&1; then
|
||
openssl rand -hex 32
|
||
elif [ -c /dev/urandom ]; then
|
||
head -c 32 /dev/urandom | od -An -tx1 | tr -d ' \n'
|
||
else
|
||
die "无法生成随机密钥(缺 openssl 且无 /dev/urandom)"
|
||
fi
|
||
}
|
||
|
||
if kubectl -n "${NS}" get secret lokistack-secrets >/dev/null 2>&1; then
|
||
info "Secret lokistack-secrets 已存在,跳过(不覆盖现有密钥)"
|
||
else
|
||
info "生成随机密钥 JWT_SECRET / ENCRYPTION_KEY"
|
||
kubectl -n "${NS}" create secret generic lokistack-secrets \
|
||
--from-literal=JWT_SECRET="$(rand_hex)" \
|
||
--from-literal=ENCRYPTION_KEY="$(rand_hex)" >/dev/null
|
||
ok "Secret 已创建"
|
||
fi
|
||
|
||
# ============================================================================
|
||
# 3) 数据卷(显式写入探测到的 StorageClass)
|
||
# ============================================================================
|
||
RENDER_DIR="$(mktemp -d)"
|
||
trap 'rm -rf "${RENDER_DIR}"' EXIT
|
||
|
||
awk -v sc="${RESOLVED_SC}" '
|
||
/^spec:/ && !done { print; print " storageClassName: " sc; done=1; next }
|
||
{ print }
|
||
' "${DIR}/pvc.yaml" > "${RENDER_DIR}/pvc.yaml"
|
||
kubectl apply -f "${RENDER_DIR}/pvc.yaml"
|
||
|
||
# ============================================================================
|
||
# 4) 工作负载 + 服务
|
||
# ============================================================================
|
||
kubectl apply -f "${DIR}/deployment.yaml"
|
||
kubectl apply -f "${DIR}/service.yaml"
|
||
|
||
if [ "${WITH_NODEPORT}" = "1" ]; then
|
||
kubectl apply -f "${DIR}/service-nodeport.yaml"
|
||
ok "NodePort Service 已创建(30080)"
|
||
fi
|
||
|
||
# ============================================================================
|
||
# 5) 等待就绪(失败时给出按云定制的诊断)
|
||
# ============================================================================
|
||
info "等待 Deployment 就绪(最多 180 秒)..."
|
||
if ! kubectl -n "${NS}" rollout status deployment/lokistack --timeout=180s; then
|
||
echo ""
|
||
warn "部署未在 180 秒内就绪,开始诊断:"
|
||
echo ""
|
||
kubectl -n "${NS}" get po -l app=lokistack -o wide 2>/dev/null | sed 's/^/ /'
|
||
|
||
REASON=$(kubectl -n "${NS}" get po -l app=lokistack -o jsonpath='{.items[0].status.containerStatuses[0].state.waiting.reason}' 2>/dev/null || true)
|
||
echo ""
|
||
case "${REASON}" in
|
||
ImagePullBackOff|ErrImagePull)
|
||
warn "镜像拉取失败(${REASON})"
|
||
echo " → 当前镜像: $(grep -E '^\s+image:' "${DIR}/deployment.yaml" | head -1 | awk '{print $2}')"
|
||
echo " → 该节点拉不到 Docker Hub。请改为目标云可拉的仓库后重跑:"
|
||
echo " 腾讯云 TCR : ccr.ccs.tencentyun.com/<ns>/lokistack:latest"
|
||
echo " 阿里云 ACR : registry.cn-hangzhou.aliyuncs.com/<ns>/lokistack:latest"
|
||
echo " 华为云 SWR : swr.cn-north-4.myhuaweicloud.com/<ns>/lokistack:latest"
|
||
echo " 自建 Harbor: harbor.your.domain/library/lokistack:latest"
|
||
;;
|
||
CreateContainerConfigError)
|
||
warn "容器配置缺失(通常是 Secret 不存在)"
|
||
echo " → 检查: kubectl -n ${NS} get secret lokistack-secrets"
|
||
;;
|
||
"")
|
||
PVC_PHASE=$(kubectl -n "${NS}" get pvc lokistack-data -o jsonpath='{.status.phase}' 2>/dev/null || echo unknown)
|
||
if [ "${PVC_PHASE}" != "Bound" ]; then
|
||
warn "PVC 未绑定(phase=${PVC_PHASE})"
|
||
echo " → 检查 StorageClass: kubectl -n ${NS} describe pvc lokistack-data"
|
||
echo " → ${CLOUD} 建议的 SC:"
|
||
case "${CLOUD}" in
|
||
腾讯云*) echo " cbs" ;;
|
||
AWS*) echo " gp3 / gp2" ;;
|
||
阿里云*) echo " alicloud-disk-essd" ;;
|
||
华为云*) echo " csi-disk" ;;
|
||
*) echo " local-path(需装 local-path-provisioner)" ;;
|
||
esac
|
||
fi
|
||
;;
|
||
esac
|
||
|
||
echo ""
|
||
kubectl -n "${NS}" describe po -l app=lokistack 2>/dev/null | tail -15 | sed 's/^/ /'
|
||
die "部署未完成,请按上面提示处理"
|
||
fi
|
||
ok "Deployment 已就绪"
|
||
|
||
# ============================================================================
|
||
# 6) 输出访问信息
|
||
# ============================================================================
|
||
POD=$(kubectl -n "${NS}" get po -l app=lokistack -o jsonpath='{.items[0].metadata.name}' 2>/dev/null || echo "-")
|
||
POD_IP=$(kubectl -n "${NS}" get po -l app=lokistack -o jsonpath='{.items[0].status.podIP}' 2>/dev/null || echo "-")
|
||
NODE=$(kubectl -n "${NS}" get po -l app=lokistack -o jsonpath='{.items[0].spec.nodeName}' 2>/dev/null || echo "-")
|
||
|
||
echo ""
|
||
echo "════════════════════════════════════════════════════════"
|
||
ok "lokistack 部署完成"
|
||
echo "════════════════════════════════════════════════════════"
|
||
echo " 平台 : ${CLOUD}"
|
||
echo " Namespace : ${NS}"
|
||
echo " 存储类 : ${RESOLVED_SC}"
|
||
echo " Pod : ${POD} (node=${NODE} ip=${POD_IP})"
|
||
echo " Service : lokistack.${NS}.svc.cluster.local:8080"
|
||
[ "${WITH_NODEPORT}" = "1" ] && echo " NodePort : <任意节点IP>:30080"
|
||
echo ""
|
||
echo " 初始账号 : admin / admin123 ← 首次登录请立即改密"
|
||
echo ""
|
||
echo " 本地访问 : kubectl -n ${NS} port-forward svc/lokistack 8080:8080"
|
||
echo " 浏览器打开 http://localhost:8080"
|
||
echo ""
|
||
echo " 配 Ingress(可选,先跑 WITH_NODEPORT=1 bash install.sh):"
|
||
case "${CLOUD}" in
|
||
腾讯云*) echo " kubectl apply -f ${DIR}/examples/ingress-tke.yaml # qcloud class" ;;
|
||
阿里云*) echo " kubectl apply -f ${DIR}/examples/ingress.yaml # alb class" ;;
|
||
*) echo " 参照 ${DIR}/examples/ 改 ingressClassName 与注解" ;;
|
||
esac
|
||
echo "════════════════════════════════════════════════════════"
|