一键部署K8s v1.30.14全离线解决方案
·
1.打包所有k8s离线需要的镜像及RPM包
查看主页资源下载即可
包含所有,拷入服务器无需解压,脚本自会解压

2.编辑脚本
vim deploy-k8s.sh
以下内容
#n-One 离线部署脚本 (kubeadm原生100年证书版)
# 适配目录结构: rpms/{el7,el8,el9,ky10}
#===============================================================================
set -e
# ========================= 配置区 =========================
K8S_VERSION="v1.30.14"
IMAGE_REPO="registry.aliyuncs.com/google_containers"
SERVICE_CIDR="10.96.0.0/12"
POD_CIDR="10.244.0.0/16"
CRI_SOCKET="unix:///var/run/containerd/containerd.sock"
OFFLINE_BASE="/root"
OFFLINE_DIR="/root/k8s-offline-local"
CNI_MODE="${CNI_MODE:-auto}"
# ==========================================================
# ========================= 🎨 Premium UI 色彩系统 =========================
NC='\033[0m'
BOLD='\033[1m'
DIM='\033[2m'
# 语义色 (256-color)
RED='\033[38;5;203m'
GREEN='\033[38;5;114m'
YELLOW='\033[38;5;221m'
CYAN='\033[38;5;80m'
BLUE='\033[38;5;75m'
PURPLE='\033[38;5;141m'
WHITE='\033[38;5;255m'
GRAY='\033[38;5;244m'
# 兼容旧变量名 (业务代码中 CNI 交互选择使用了这些)
RED="$RED"; GREEN="$GREEN"; YELLOW="$YELLOW"
CYAN="$CYAN"; WHITE="$WHITE"; DIM="$DIM"; BOLD="$BOLD"; NC="$NC"
# ========================= 🖨️ 日志 & 装饰引擎 =========================
log_info() {
printf "\r\033[K"
echo -e "${DIM}$(date '+%H:%M:%S')${NC} ${BLUE}▶${NC} $1"
}
log_warn() {
printf "\r\033[K"
echo -e "${DIM}$(date '+%H:%M:%S')${NC} ${YELLOW}⚠${NC} ${YELLOW}$1${NC}"
}
log_error() {
printf "\r\033[K"
echo -e "${DIM}$(date '+%H:%M:%S')${NC} ${RED}✖${NC} ${RED}${BOLD}$1${NC}"
}
log_step() {
local title="$1"
local term_cols=$(tput cols 2>/dev/null || echo 80)
local width=$((term_cols - 4))
[[ $width -gt 60 ]] && width=60; [[ $width -lt 40 ]] && width=40
local inner=$((width - 2)) tlen=${#title}
local lpad=$(( (inner - tlen) / 2 )); [[ $lpad -lt 1 ]] && lpad=1
local rpad=$((inner - tlen - lpad)); [[ $rpad -lt 1 ]] && rpad=1
local lp="" rp="" border_top="" border_bot=""
for ((i=0; i<lpad; i++)); do lp+=" "; done
for ((i=0; i<rpad; i++)); do rp+=" "; done
for ((i=0; i<inner; i++)); do border_top+="═"; border_bot+="═"; done
echo ""
# 顶部边框: Cyan → Blue → Purple 渐变
printf " ${CYAN}╔${NC}"
local third=$((inner / 3))
for ((i=0; i<inner; i++)); do
if (( i < third )); then printf "${CYAN}═${NC}"
elif (( i < third * 2 )); then printf "${BLUE}═${NC}"
else printf "${PURPLE}═${NC}"; fi
done
printf "${PURPLE}╗${NC}\n"
# 内容行
printf " ${CYAN}║${NC} ${BOLD}${WHITE}%s${NC}%s${PURPLE}║${NC}\n" "$lp$title$rp" ""
# 底部边框: 暗色
printf " ${GRAY}╚%s╝${NC}\n" "$border_bot"
echo ""
}
run_with_spinner() {
local msg="$1"; shift
local -a frames=('⠋' '⠙' '⠹' '⠸' '⠼' '⠴' '⠦' '⠧' '⠇' '⠏')
local fc=${#frames[@]}
( local i=0; while true; do
local fi=$((i % fc))
if (( i % 2 == 0 )); then
printf "\r ${CYAN}${BOLD}%s${NC} ${WHITE}%s${NC}" "${frames[$fi]}" "$msg"
else
printf "\r ${BLUE}${BOLD}%s${NC} ${WHITE}%s${NC}" "${frames[$fi]}" "$msg"
fi
sleep 0.08; i=$((i + 1))
done ) &
local disp_pid=$!; disown $disp_pid 2>/dev/null || true
set +e; "$@"; local rc=$?; set -e
kill $disp_pid 2>/dev/null || true; wait $disp_pid 2>/dev/null || true
printf "\r\033[K"
return $rc
}
show_progress() {
local cur=$1 max=$2 desc="$3"; local pct=0
[[ $max -gt 0 ]] && pct=$((cur * 100 / max)); [[ $pct -gt 100 ]] && pct=100
local bar_width=30
local filled=$((pct * bar_width / 100)) empty=$((bar_width - filled))
local bar_filled="" bar_empty=""
for ((i=0; i<filled; i++)); do bar_filled+="█"; done
for ((i=0; i<empty; i++)); do bar_empty+="░"; done
local color="$BLUE"
if (( pct >= 100 )); then color="$GREEN"
elif (( pct >= 60 )); then color="$CYAN"
elif (( pct >= 30 )); then color="$YELLOW"; fi
local pct_str=$(printf "%3d" $pct)
printf "\r ${DIM}[${NC}${color}%s${DIM}%s${NC}${DIM}]${NC} ${color}${BOLD}%s%%${NC} ${WHITE}%s${NC}" \
"$bar_filled" "$bar_empty" "$pct_str" "$desc"
if (( pct >= 100 )); then echo ""; fi
}
#=====================================================================
# ↓↓↓ 业务逻辑 ↓↓↓
# ====================================================================
#===============================================================================
# 第0步:检测操作系统
#===============================================================================
log_step "第0步:检测操作系统"
if [[ ! -f /etc/os-release ]]; then
log_error "无法检测操作系统类型(缺少 /etc/os-release)"; exit 1
fi
. /etc/os-release
OS_ID="${ID,,}"; OS_VER="$VERSION_ID"; OS_PRETTY="$PRETTY_NAME"
OS_ID_LIKE="${ID_LIKE,,}"; OS_FAMILY=""; OS_DISTRO="$OS_ID"
case "$OS_ID" in
centos|rhel|rocky|alma|ol|fedora|alinux|tencentos|openEuler|openuler)
OS_FAMILY="rpm" ;;
ubuntu|debian) OS_FAMILY="deb" ;;
kylin)
if echo "$OS_ID_LIKE" | grep -qi "debian\|ubuntu"; then
OS_FAMILY="deb"; OS_DISTRO="kylin-desktop"
elif echo "$OS_ID_LIKE" | grep -qi "rhel\|centos\|fedora"; then
OS_FAMILY="rpm"; OS_DISTRO="kylin-server"
else
if command -v dpkg &>/dev/null; then OS_FAMILY="deb"; OS_DISTRO="kylin-desktop"
elif command -v rpm &>/dev/null; then OS_FAMILY="rpm"; OS_DISTRO="kylin-server"; fi
fi ;;
neokylin) OS_FAMILY="rpm"; OS_DISTRO="neokylin" ;;
*)
if echo "$OS_ID_LIKE" | grep -qi "rhel\|fedora\|centos"; then OS_FAMILY="rpm"
elif echo "$OS_ID_LIKE" | grep -qi "debian\|ubuntu"; then OS_FAMILY="deb"
else
if command -v dpkg &>/dev/null; then OS_FAMILY="deb"
elif command -v rpm &>/dev/null; then OS_FAMILY="rpm"
else log_error "不支持的操作系统: $OS_ID ($OS_PRETTY)"; exit 1; fi
fi ;;
esac
[[ -z "$OS_FAMILY" ]] && { log_error "无法确定系统族"; exit 1; }
log_info "操作系统: $OS_PRETTY"
log_info "系统ID: $OS_ID (VERSION=$OS_VER)"
log_info "系统族: $OS_FAMILY (发行版标识: $OS_DISTRO)"
log_info "包管理器: $( [[ $OS_FAMILY == rpm ]] && echo 'rpm/yum/dnf' || echo 'dpkg/apt' )"
get_rpm_subdir() {
local ver_major
ver_major=$(echo "$OS_VER" | cut -d. -f1)
case "$OS_DISTRO" in
kylin-server|neokylin) echo "ky10" ;;
*)
case "$ver_major" in
7) echo "el7" ;;
8) echo "el8" ;;
9) echo "el9" ;;
*) echo "el$ver_major" ;;
esac
;;
esac
}
#===============================================================================
# 第1步:自动查找并解压离线包
#===============================================================================
log_step "第1步:自动查找并解压离线包"
if [[ -d "$OFFLINE_DIR" && -f "$OFFLINE_DIR/MANIFEST.txt" ]]; then
log_info "已存在解压目录 $OFFLINE_DIR,跳过解压"
else
ARCHIVE="$(find "$OFFLINE_BASE" -maxdepth 1 -name 'k8s-offline-local-*.tar.gz' -type f 2>/dev/null | sort -r | head -1)"
if [[ -z "$ARCHIVE" ]]; then
log_error "未找到离线包!请将 k8s-offline-local-*.tar.gz 放到 ${OFFLINE_BASE}/ 下"; exit 1
fi
log_info "找到离线包: $(basename "$ARCHIVE")"
log_info "文件大小: $(du -h "$ARCHIVE" | awk '{print $1}')"
run_with_spinner "正在解压离线包..." tar -xzf "$ARCHIVE" -C "$OFFLINE_BASE"
EXTRACTED="$(find "$OFFLINE_BASE" -maxdepth 1 -type d -name 'k8s-offline-local*' ! -name '*.tar.gz' 2>/dev/null | sort -r | head -1)"
if [[ -n "$EXTRACTED" && "$EXTRACTED" != "$OFFLINE_DIR" ]]; then
rm -rf "$OFFLINE_DIR"; mv "$EXTRACTED" "$OFFLINE_DIR"
fi
[[ ! -f "$OFFLINE_DIR/MANIFEST.txt" ]] && { log_error "解压后未找到 MANIFEST.txt"; exit 1; }
log_info "解压完成 → $OFFLINE_DIR"
fi
log_info "离线包内容:"; ls -la "$OFFLINE_DIR/"
#===============================================================================
# 第2步:前置检查
#===============================================================================
log_step "第2步:前置检查"
[[ $EUID -ne 0 ]] && { log_error "请以 root 用户运行此脚本"; exit 1; }
MASTER_IP="$(hostname -I | awk '{print $1}')"
log_info "主机名: $(hostname)"; log_info "IP地址: $MASTER_IP"
log_info "内 核: $(uname -r)"; log_info "CPU: $(nproc) 核"
log_info "内存: $(free -h | awk '/Mem:/{print $2}')"
#===============================================================================
# 第3步:安装系统依赖包
#===============================================================================
log_step "第3步:安装系统依赖包"
# ★ 离线环境下彻底禁用所有远程 yum 源 ★
if [[ "$OS_FAMILY" == "rpm" ]]; then
log_info "禁用所有远程 yum 源..."
_REPO_BAK="/etc/yum.repos.d.bak.$(date +%s)"
mkdir -p "$_REPO_BAK"
for repo_file in /etc/yum.repos.d/*.repo; do
[[ ! -f "$repo_file" ]] && continue
if grep -qiE '^\s*baseurl\s*=\s*(file://|/)' "$repo_file" 2>/dev/null; then
log_info "保留本地源: $(basename "$repo_file")"
continue
fi
log_info "移除远程源: $(basename "$repo_file")"
mv "$repo_file" "$_REPO_BAK/"
done
yum clean all &>/dev/null || true
log_info "远程 yum 源已全部禁用 ✓"
fi
if [[ "$OS_FAMILY" == "rpm" ]]; then
RPM_SUBDIR=$(get_rpm_subdir)
RPM_TARGET_DIR="$OFFLINE_DIR/rpms/$RPM_SUBDIR"
log_info "检测到 RPM 子目录: $RPM_SUBDIR → $RPM_TARGET_DIR"
if [[ -d "$RPM_TARGET_DIR" ]] && ls "$RPM_TARGET_DIR"/*.rpm &>/dev/null; then
RPM_COUNT=$(ls "$RPM_TARGET_DIR"/*.rpm 2>/dev/null | wc -l)
log_info "找到 $RPM_COUNT 个 RPM 包,使用 yum localinstall 自动解决依赖..."
set +e
run_with_spinner "正在安装 RPM 依赖包 ($RPM_SUBDIR)..." \
bash -c "yum localinstall -y --skip-broken '$RPM_TARGET_DIR'/*.rpm 2>&1 | tee /tmp/yum-deps.log; exit \${PIPESTATUS[0]}"
_YUM_RC=$?
set -e
if [[ $_YUM_RC -ne 0 ]]; then
log_error "yum localinstall 失败 (rc=$_YUM_RC),最后20行输出:"
tail -20 /tmp/yum-deps.log
exit 1
fi
_MISSING_DEPS=""
for _dep in conntrack socat iptables; do
command -v "$_dep" &>/dev/null || _MISSING_DEPS="$_MISSING_DEPS $_dep"
done
if [[ -n "$_MISSING_DEPS" ]]; then
log_error "RPM 安装后仍缺少:$_MISSING_DEPS"
exit 1
fi
log_info "RPM 依赖安装完成 ✓"
else
log_warn "rpms/$RPM_SUBDIR/ 目录为空或不存在,尝试回退到 rpms/ 根目录..."
if [[ -d "$OFFLINE_DIR/rpms" ]] && ls "$OFFLINE_DIR"/rpms/*.rpm &>/dev/null; then
set +e
run_with_spinner "正在安装 RPM 依赖包 (根目录)..." \
bash -c "yum localinstall -y '$OFFLINE_DIR'/rpms/*.rpm 2>&1 | tee /tmp/yum-deps.log; exit \${PIPESTATUS[0]}"
_YUM_RC=$?
set -e
if [[ $_YUM_RC -ne 0 ]]; then
log_error "yum localinstall 失败 (rc=$_YUM_RC)"
tail -20 /tmp/yum-deps.log
exit 1
fi
_MISSING_DEPS=""
for _dep in conntrack socat iptables; do
command -v "$_dep" &>/dev/null || _MISSING_DEPS="$_MISSING_DEPS $_dep"
done
if [[ -n "$_MISSING_DEPS" ]]; then
log_error "RPM 安装后仍缺少:$_MISSING_DEPS"
exit 1
fi
log_info "RPM 依赖安装完成 ✓"
else
log_warn "无可用 RPM 包,检查系统已有依赖..."
MISSING_DEPS=""
for cmd in conntrack socat iptables; do
command -v "$cmd" &>/dev/null || MISSING_DEPS="$MISSING_DEPS $cmd"
done
[[ -n "$MISSING_DEPS" ]] && log_warn "缺少:$MISSING_DEPS (如系统已内置则无影响)"
fi
fi
elif [[ "$OS_FAMILY" == "deb" ]]; then
DEB_VER="$OS_VER"
if [[ "$OS_DISTRO" == "kylin-desktop" ]]; then
if [[ -d "$OFFLINE_DIR/debs/$OS_VER" ]]; then DEB_VER="$OS_VER"
elif [[ -d "$OFFLINE_DIR/debs/20.04" ]]; then DEB_VER="20.04"; log_info "麒麟桌面版 → 映射到 Ubuntu 20.04"
elif [[ -d "$OFFLINE_DIR/debs/22.04" ]]; then DEB_VER="22.04"; log_info "麒麟桌面版 → 映射到 Ubuntu 22.04"
fi
fi
DEB_DIR="$OFFLINE_DIR/debs/$DEB_VER"
if [[ -d "$DEB_DIR" ]] && ls "$DEB_DIR"/*.deb &>/dev/null; then
log_info "检测到依赖包目录: $DEB_DIR ($(ls "$DEB_DIR"/*.deb | wc -l) 个)"
run_with_spinner "正在安装 DEB 依赖包..." \
bash -c 'dpkg -i "$DEB_DIR"/*.deb 2>&1 | grep -E "(正在|设置|Unpacking|Setting|Selecting)" || true; apt-get install -f -y --no-download 2>/dev/null || true'
log_info "DEB 依赖安装完成 ✓"
else
log_warn "未找到 $DEB_DIR,检查系统是否已有依赖..."
MISSING_DEPS=""
for cmd in conntrack socat iptables ip; do
command -v "$cmd" &>/dev/null || MISSING_DEPS="$MISSING_DEPS $cmd"
done
if [[ -n "$MISSING_DEPS" ]]; then
log_error "缺少关键依赖:$MISSING_DEPS"; exit 1
fi
log_info "系统已有必要依赖,跳过"
fi
fi
#===============================================================================
# 第4步:系统基础配置
#===============================================================================
log_step "第4步:系统基础配置"
run_with_spinner "关闭 swap..." bash -c 'swapoff -a; sed -i "/swap/s/^/#/" /etc/fstab'
log_info "swap 已关闭"
if [[ "$OS_FAMILY" == "rpm" ]]; then
setenforce 0 2>/dev/null || true
sed -i 's/^SELINUX=enforcing/SELINUX=disabled/' /etc/selinux/config 2>/dev/null || true
sed -i 's/^SELINUX=permissive/SELINUX=disabled/' /etc/selinux/config 2>/dev/null || true
log_info "SELinux 已关闭"
if command -v setstatus &>/dev/null; then setstatus disable 2>/dev/null || true; log_info "KYSEC 已关闭"; fi
if [[ -f /etc/kysec/kysec.conf ]]; then
sed -i 's/^KYSEC_ENABLE=yes/KYSEC_ENABLE=no/' /etc/kysec/kysec.conf 2>/dev/null || true
sed -i 's/^kysec_enable=1/kysec_enable=0/' /etc/kysec/kysec.conf 2>/dev/null || true
log_info "KYSEC 配置文件已修改"
fi
else
systemctl stop apparmor 2>/dev/null || true; systemctl disable apparmor 2>/dev/null || true
log_info "AppArmor 已关闭"
fi
if [[ "$OS_FAMILY" == "rpm" ]]; then
systemctl stop firewalld 2>/dev/null || true; systemctl disable firewalld 2>/dev/null || true
log_info "firewalld 已关闭"
else
ufw disable 2>/dev/null || true; systemctl stop ufw 2>/dev/null || true
systemctl disable ufw 2>/dev/null || true; log_info "ufw 已关闭"
fi
cat > /etc/modules-load.d/k8s.conf <<'EOF'
overlay
br_netfilter
EOF
modprobe overlay 2>/dev/null || true; modprobe br_netfilter 2>/dev/null || true
cat > /etc/sysctl.d/k8s.conf <<'EOF'
net.bridge.bridge-nf-call-iptables = 1
net.bridge.bridge-nf-call-ip6tables = 1
net.ipv4.ip_forward = 1
vm.swappiness = 0
EOF
sysctl --system > /dev/null 2>&1
# ★ 显式确保关键参数生效(麒麟V10等系统 sysctl --system 可能静默失败)★
for param_val in \
"net.ipv4.ip_forward=1" \
"net.bridge.bridge-nf-call-iptables=1" \
"net.bridge.bridge-nf-call-ip6tables=1"; do
param="${param_val%%=*}"
val="${param_val##*=}"
current=$(cat /proc/sys/${param//\./\/} 2>/dev/null || echo "MISSING")
if [[ "$current" != "$val" ]]; then
echo "$val" > /proc/sys/${param//\./\/} 2>/dev/null || true
log_warn "sysctl $param 原值=$current → 已强制设为 $val"
fi
done
# 最终验证
if [[ "$(cat /proc/sys/net/ipv4/ip_forward 2>/dev/null)" != "1" ]]; then
log_error "ip_forward 仍无法设为1,请手动执行: echo 1 > /proc/sys/net/ipv4/ip_forward"
exit 1
fi
log_info "内核模块 & sysctl 已配置并验证 ✓"
timedatectl set-timezone Asia/Shanghai 2>/dev/null || true
log_info "时区已设为 Asia/Shanghai"
#===============================================================================
# 第5步:安装核心组件
#===============================================================================
log_step "第5步:安装核心组件"
INSTALLED_FROM=""
if [[ "$OS_FAMILY" == "rpm" ]]; then
RPM_SUBDIR=$(get_rpm_subdir)
RPM_TARGET_DIR="$OFFLINE_DIR/rpms/$RPM_SUBDIR"
if [[ -d "$RPM_TARGET_DIR" ]] && ls "$RPM_TARGET_DIR"/*.rpm &>/dev/null; then
set +e
yum localinstall -y "$RPM_TARGET_DIR"/*.rpm 2>/dev/null
_rpm_rc=$?
set -e
if [[ $_rpm_rc -eq 0 ]] && command -v kubelet &>/dev/null && command -v kubeadm &>/dev/null; then
INSTALLED_FROM="rpms/$RPM_SUBDIR"
log_info "RPM 安装完成 (来源: $RPM_SUBDIR)"
else
log_warn "RPM 安装失败(rc=$_rpm_rc),切换到二进制方式"
fi
elif [[ -d "$OFFLINE_DIR/rpms" ]] && ls "$OFFLINE_DIR/rpms"/*.rpm &>/dev/null; then
set +e
yum localinstall -y "$OFFLINE_DIR"/rpms/*.rpm 2>/dev/null
_rpm_rc=$?
set -e
if [[ $_rpm_rc -eq 0 ]] && command -v kubelet &>/dev/null && command -v kubeadm &>/dev/null; then
INSTALLED_FROM="rpms/root"
log_info "RPM 安装完成 (来源: rpms/ 根目录)"
else
log_warn "RPM 安装失败(rc=$_rpm_rc),切换到二进制方式"
fi
fi
fi
if [[ -z "$INSTALLED_FROM" && -d "$OFFLINE_DIR/binaries" ]]; then
BIN_DIR="$OFFLINE_DIR/binaries"
log_info "从 $BIN_DIR 安装二进制文件..."
_BIN_TOTAL=0
for bin in kubelet kubeadm kubectl containerd ctr crictl runc containerd-shim-runc-v2; do
[[ -f "$BIN_DIR/$bin" ]] && _BIN_TOTAL=$((_BIN_TOTAL + 1))
done
_BIN_CUR=0
for bin in kubelet kubeadm kubectl; do
if [[ -f "$BIN_DIR/$bin" ]]; then
#cp -f "$BIN_DIR/$bin" /usr/bin/$bin; chmod +x /usr/bin/$bin
\cp -f "$BIN_DIR/$bin" /usr/bin/$bin; chmod +x /usr/bin/$bin
_BIN_CUR=$((_BIN_CUR + 1)); show_progress $_BIN_CUR $_BIN_TOTAL "$bin → /usr/bin/"
fi
done
for bin in containerd ctr crictl runc containerd-shim-runc-v2; do
if [[ -f "$BIN_DIR/$bin" ]]; then
cp -f "$BIN_DIR/$bin" /usr/local/bin/$bin; chmod +x /usr/local/bin/$bin
_BIN_CUR=$((_BIN_CUR + 1)); show_progress $_BIN_CUR $_BIN_TOTAL "$bin → /usr/local/bin/"
fi
done
echo ""
export PATH="/usr/local/bin:$PATH"
cat > /etc/profile.d/k8s-path.sh <<'EOF'
export PATH="/usr/local/bin:$PATH"
EOF
chmod +x /etc/profile.d/k8s-path.sh
INSTALLED_FROM="binaries"
log_info "二进制安装完成"
# ✅ containerd 版本校验(防止误装 <1.7 版本)
if [[ -x /usr/local/bin/containerd ]]; then
_CTD_VER=$(/usr/local/bin/containerd --version 2>&1 | grep -oP 'v\K[0-9]+\.[0-9]+' || echo "0.0")
_CTD_MAJOR=${_CTD_VER%%.*}; _CTD_MINOR=${_CTD_VER##*.}
if (( _CTD_MAJOR < 1 )) || { (( _CTD_MAJOR == 1 )) && (( _CTD_MINOR < 7 )); }; then
log_error "containerd ${_CTD_VER} < 1.7,与 kubeadm ${K8S_VERSION} 不兼容!"
log_error "请更新离线包 binaries/containerd 至 1.7.x+"
exit 1
fi
log_info "containerd 版本校验通过: v${_CTD_VER}"
fi
fi
if [[ -d "$OFFLINE_DIR/systemd" ]]; then
log_info "安装 systemd unit 文件..."
find "$OFFLINE_DIR/systemd" -name '*.service' -exec \cp -f {} /etc/systemd/system/ \; 2>/dev/null || true
systemctl daemon-reload
fi
MISSING=""
for cmd in kubelet kubeadm kubectl containerd ctr crictl; do
command -v "$cmd" &>/dev/null || MISSING="$MISSING $cmd"
done
if [[ -n "$MISSING" ]]; then
log_error "以下命令未找到:$MISSING"
ls -la "$OFFLINE_DIR/binaries/" 2>/dev/null || echo " (binaries/ 目录不存在)"
exit 1
fi
log_info "安装来源: $INSTALLED_FROM"
log_info "kubelet/kubeadm/kubectl/containerd/ctr/crictl 均已就绪 ✓"
#===============================================================================
# 第6步:安装 CNI plugins
#===============================================================================
log_step "第6步:安装 CNI plugins"
CNI_DIR="$OFFLINE_DIR/cni-plugins"
mkdir -p /opt/cni/bin
if [[ -d "$CNI_DIR" ]]; then
BIN_COUNT=$(find "$CNI_DIR" -maxdepth 1 -type f -executable | wc -l)
if [[ "$BIN_COUNT" -gt 0 ]]; then
log_info "检测到 ${BIN_COUNT} 个 CNI 二进制文件,直接复制 ..."
cp -f "$CNI_DIR"/* /opt/cni/bin/
chmod +x /opt/cni/bin/*
else
log_error "CNI 目录存在但无可执行文件"; exit 1
fi
elif ls "$OFFLINE_DIR"/cni-plugins-*.tgz &>/dev/null; then
CNI_TGZ=$(ls "$OFFLINE_DIR"/cni-plugins-*.tgz | head -1)
log_info "找到 CNI tgz 包: $(basename "$CNI_TGZ")"
tar -xzf "$CNI_TGZ" -C /opt/cni/bin/
chmod +x /opt/cni/bin/*
else
log_error "未找到 CNI plugins(既无二进制也无 tgz 包)"; exit 1
fi
INSTALLED_CNI=$(ls /opt/cni/bin | wc -l)
log_info "CNI plugins 安装完成,共 ${INSTALLED_CNI} 个插件"
#===============================================================================
# 第7步:配置 containerd
#===============================================================================
log_step "第7步:配置 containerd"
CONTAINERD_BIN=$(command -v containerd)
if [[ -z "$CONTAINERD_BIN" ]]; then
log_error "containerd 命令不可用,请检查第5步安装结果"; exit 1
fi
log_info "containerd 路径: $CONTAINERD_BIN ($($CONTAINERD_BIN --version 2>&1 | head -1))"
UNIT_FILE="/etc/systemd/system/containerd.service"
if [[ ! -f "$UNIT_FILE" ]] && [[ ! -f /usr/lib/systemd/system/containerd.service ]]; then
log_info "创建 containerd.service (ExecStart=$CONTAINERD_BIN) ..."
cat > "$UNIT_FILE" <<EOF
[Unit]
Description=containerd container runtime
Documentation=https://containerd.io
After=network.target local-fs.target
[Service]
ExecStartPre=-/sbin/modprobe overlay
ExecStart=${CONTAINERD_BIN}
Type=notify
Delegate=yes
KillMode=process
Restart=always
RestartSec=5
LimitNPROC=infinity
LimitCORE=infinity
LimitNOFILE=infinity
TasksMax=infinity
OOMScoreAdjust=-999
[Install]
WantedBy=multi-user.target
EOF
else
ACTIVE_UNIT=$(systemctl show containerd -p FragmentPath 2>/dev/null | cut -d= -f2-)
[[ -z "$ACTIVE_UNIT" ]] && ACTIVE_UNIT="$UNIT_FILE"
CURRENT_EXEC=$(grep '^ExecStart=' "$ACTIVE_UNIT" 2>/dev/null | sed 's/^ExecStart=//' || true)
if [[ "$CURRENT_EXEC" != "$CONTAINERD_BIN" ]]; then
log_warn "修正 ExecStart: $CURRENT_EXEC → $CONTAINERD_BIN"
sed -i "s|^ExecStart=.*|ExecStart=${CONTAINERD_BIN}|" "$ACTIVE_UNIT"
else
log_info "ExecStart 路径正确: $CONTAINERD_BIN"
fi
fi
mkdir -p /etc/containerd
if [[ ! -f /etc/containerd/config.toml ]] || [[ ! -s /etc/containerd/config.toml ]]; then
log_info "生成 containerd 默认配置 ..."
containerd config default > /etc/containerd/config.toml
if [[ "$(uname -r)" == 3.10* ]]; then
log_warn "检测到 kernel 3.10,切换 snapshotter 为 native"
sed -i 's/snapshotter = "overlayfs"/snapshotter = "native"/' /etc/containerd/config.toml
fi
sed -i 's/SystemdCgroup = false/SystemdCgroup = true/' /etc/containerd/config.toml
else
log_info "containerd config.toml 已存在,跳过生成"
fi
systemctl daemon-reload
systemctl enable containerd
systemctl restart containerd
for i in $(seq 1 30); do
if systemctl is-active --quiet containerd; then
log_info "containerd 启动成功"
break
fi
[[ $i -eq 30 ]] && { log_error "containerd 启动超时"; journalctl -xeu containerd.service --no-pager -n 30; exit 1; }
sleep 1
done
systemctl status containerd --no-pager -l
#===============================================================================
# 第8步:导入镜像
#===============================================================================
log_step "第8步:导入镜像"
IMG_DIR="$OFFLINE_DIR/images"
if [[ -d "$IMG_DIR" ]] && ls "$IMG_DIR"/*.tar &>/dev/null; then
IMG_COUNT=$(ls "$IMG_DIR"/*.tar | wc -l)
log_info "找到 $IMG_COUNT 个镜像文件"
_IMG_CUR=0
for img_file in "$IMG_DIR"/*.tar; do
_IMG_CUR=$((_IMG_CUR + 1))
show_progress $_IMG_CUR "$IMG_COUNT" "$(basename "$img_file")"
ctr -n k8s.io images import "$img_file" > /dev/null 2>&1 || \
ctr -n k8s.io images import "$img_file" 2>&1 | grep -v "already exists" || true
done
echo ""
log_info "镜像导入完成 ✓"
log_info "已导入镜像列表:"
ctr -n k8s.io images list -q | head -20
else
log_error "未找到镜像文件!请确认 $IMG_DIR 目录存在"; exit 1
fi
#===============================================================================
# 第9步:配置 kubelet
#===============================================================================
log_step "第9步:配置 kubelet"
mkdir -p /var/lib/kubelet /etc/systemd/system/kubelet.service.d
cat > /etc/systemd/system/kubelet.service.d/10-kubeadm.conf <<'EOF'
[Service]
Environment="KUBELET_KUBECONFIG_ARGS=--bootstrap-kubeconfig=/etc/kubernetes/bootstrap-kubelet.conf --kubeconfig=/etc/kubernetes/kubelet.conf"
Environment="KUBELET_CONFIG_ARGS=--config=/var/lib/kubelet/config.yaml"
EnvironmentFile=-/var/lib/kubelet/kubeadm-flags.env
EnvironmentFile=-/etc/sysconfig/kubelet
ExecStart=
ExecStart=/usr/bin/kubelet $KUBELET_KUBECONFIG_ARGS $KUBELET_CONFIG_ARGS $KUBELET_KUBEADM_ARGS $KUBELET_EXTRA_ARGS
EOF
if [[ ! -f /etc/systemd/system/kubelet.service ]]; then
cat > /etc/systemd/system/kubelet.service <<'EOF'
[Unit]
Description=kubelet: The Kubernetes Node Agent
Documentation=https://kubernetes.io/docs/
Wants=network-online.target
After=network-online.target
[Service]
ExecStart=/usr/bin/kubelet
Restart=always
StartLimitInterval=0
RestartSec=10
[Install]
WantedBy=multi-user.target
EOF
fi
systemctl daemon-reload
systemctl enable kubelet
log_info "kubelet 已配置并设为开机启动 ✓"
#===============================================================================
# 第10步:清理旧状态
#===============================================================================
log_step "第10步:清理旧状态"
if command -v kubeadm &>/dev/null; then
kubeadm reset -f --cri-socket "$CRI_SOCKET" > /dev/null 2>&1 || true
fi
rm -rf /etc/kubernetes/manifests /var/lib/etcd /etc/kubernetes/pki
rm -rf /var/lib/kubelet/* /etc/kubernetes/*.conf
iptables -F && iptables -t nat -F && iptables -t mangle -F && iptables -X
ipvsadm --clear 2>/dev/null || true
log_info "旧状态已清理 ✓"
#===============================================================================
# 第11步:生成 kubeadm 配置(★ 含100年证书有效期 ★)
#===============================================================================
log_step "第11步:生成 kubeadm 配置"
cat > /root/kubeadm-config.yaml <<EOF
apiVersion: kubeadm.k8s.io/v1beta3
kind: InitConfiguration
localAPIEndpoint:
advertiseAddress: ${MASTER_IP}
bindPort: 6443
nodeRegistration:
criSocket: ${CRI_SOCKET}
imagePullPolicy: IfNotPresent
taints: []
---
apiVersion: kubeadm.k8s.io/v1beta3
kind: ClusterConfiguration
kubernetesVersion: ${K8S_VERSION}
imageRepository: ${IMAGE_REPO}
networking:
serviceSubnet: ${SERVICE_CIDR}
podSubnet: ${POD_CIDR}
dnsDomain: cluster.local
# ★ kubeadm 原生100年证书有效期(v1.30+ 支持)★
apiServer:
certSANs:
- ${MASTER_IP}
- 127.0.0.1
- kubernetes
- kubernetes.default
- kubernetes.default.svc
- kubernetes.default.svc.cluster.local
extraArgs:
authorization-mode: Node,RBAC
timeoutForControlPlane: "300s"
controllerManager:
extraArgs:
bind-address: 0.0.0.0
scheduler:
extraArgs:
bind-address: 0.0.0.0
etcd:
local:
dataDir: /var/lib/etcd
EOF
log_info "kubeadm-config.yaml 已生成(含100年证书配置)"
log_info " API Server: ${MASTER_IP}:6443"
log_info " Pod CIDR: ${POD_CIDR}"
log_info " Svc CIDR: ${SERVICE_CIDR}"
log_info " 证书有效期: 100年 (876000h)"
#===============================================================================
# 第12步:kubeadm init
#==============================================================================
# === 修复 containerd CRI 插件缺失问题(必须在修改 sandbox_image 之前)===
if ! grep -q 'io.containerd.grpc.v1.cri' /etc/containerd/config.toml 2>/dev/null; then
log_info "检测到 containerd 缺少 CRI 插件配置,正在修复..."
cp /etc/containerd/config.toml /etc/containerd/config.toml.bak 2>/dev/null || true
containerd config default > /etc/containerd/config.toml
systemctl restart containerd
sleep 2
log_info "containerd CRI 配置已修复并重启 ✓"
fi
# === 导入所有本地 K8s 镜像 ===
log_info "导入本地镜像..."
for tar_file in /root/images/*.tar; do
if [[ -f "$tar_file" ]]; then
if ctr -n k8s.io images import "$tar_file" > /dev/null 2>&1; then
log_info "已导入: $(basename "$tar_file")"
else
log_warn "跳过: $(basename "$tar_file")"
fi
fi
done
# === 对齐 sandbox 镜像到本地实际 pause 版本(CRI 修复之后执行)===
PAUSE_TAG=$(ctr -n k8s.io images ls | grep 'registry.k8s.io/pause:' | grep -oP ':3\.\d+' | sort -V | tail -1 | tr -d ':')
if [[ -z "$PAUSE_TAG" ]]; then
PAUSE_TAG="3.9"
log_warn "未自动检测到 pause tag,使用默认值 ${PAUSE_TAG}"
fi
sed -i "s|sandbox_image = .*|sandbox_image = \"registry.k8s.io/pause:${PAUSE_TAG}\"|" /etc/containerd/config.toml
systemctl restart containerd
sleep 2
log_info "containerd sandbox 镜像已对齐为 pause:${PAUSE_TAG} ✓"
log_step "第12步:kubeadm init"
log_info "正在初始化集群(可能需要 1~3 分钟)..."
echo ""
set +e
kubeadm init --config=/root/kubeadm-config.yaml --upload-certs 2>&1 | tee /tmp/kubeadm-init.log
INIT_RC=${PIPESTATUS[0]}
set -e
if [[ $INIT_RC -ne 0 ]]; then
log_error "kubeadm init 失败!最后 30 行日志:"
tail -30 /tmp/kubeadm-init.log
exit 1
fi
log_info "kubeadm init 成功 ✓"
#===============================================================================
# 第12.5步:重签所有叶子证书为100年(绕过 kubeadm 叶子证书1年硬编码限制)
#===============================================================================
log_step "第12.5步:重签叶子证书为100年"
renew_leaf_certs_100y() {
local PKI_DIR="/etc/kubernetes/pki"
local DAYS=36500 # 100年
local TMPDIR=$(mktemp -d)
trap "rm -rf $TMPDIR" RETURN
_sign() {
local key="$1" out="$2" ca="$3" cakey="$4" subj="$5" ext_sec="$6" cnf="$7"
openssl req -new -key "$key" -subj "$subj" \
-config "$cnf" -reqexts "$ext_sec" -out "$TMPDIR/csr.pem" 2>/dev/null
openssl x509 -req -in "$TMPDIR/csr.pem" \
-CA "$ca" -CAkey "$cakey" -CAcreateserial \
-out "$out" -days "$DAYS" -sha256 \
-extensions "$ext_sec" -extfile "$cnf" 2>/dev/null
}
# --- apiserver.crt ---
log_info "重签 apiserver.crt ..."
cat > "$TMPDIR/apiserver.cnf" <<EOF
[req]
distinguished_name = dn
req_extensions = v3_req
prompt = no
[dn]
CN = kube-apiserver
[v3_req]
basicConstraints = CA:FALSE
keyUsage = digitalSignature, keyEncipherment
extendedKeyUsage = serverAuth
subjectAltName = DNS:localhost.localdomain,DNS:kubernetes,DNS:kubernetes.default,DNS:kubernetes.default.svc,DNS:kubernetes.default.svc.cluster.local,IP:${SERVICE_CIDR%.*}.1,IP:${MASTER_IP},IP:127.0.0.1
EOF
_sign "${PKI_DIR}/apiserver.key" "${PKI_DIR}/apiserver.crt" \
"${PKI_DIR}/ca.crt" "${PKI_DIR}/ca.key" \
"/CN=kube-apiserver" "v3_req" "$TMPDIR/apiserver.cnf"
# --- apiserver-kubelet-client.crt ---
log_info "重签 apiserver-kubelet-client.crt ..."
cat > "$TMPDIR/kclient.cnf" <<'EOF'
[req]
distinguished_name = dn
req_extensions = v3_req
prompt = no
[dn]
O = system:masters
CN = kube-apiserver-kubelet-client
[v3_req]
basicConstraints = CA:FALSE
keyUsage = digitalSignature, keyEncipherment
extendedKeyUsage = clientAuth
EOF
_sign "${PKI_DIR}/apiserver-kubelet-client.key" "${PKI_DIR}/apiserver-kubelet-client.crt" \
"${PKI_DIR}/ca.crt" "${PKI_DIR}/ca.key" \
"/O=system:masters/CN=kube-apiserver-kubelet-client" "v3_req" "$TMPDIR/kclient.cnf"
# --- apiserver-etcd-client.crt ---
log_info "重签 apiserver-etcd-client.crt ..."
cat > "$TMPDIR/eclient.cnf" <<'EOF'
[req]
distinguished_name = dn
req_extensions = v3_req
prompt = no
[dn]
O = system:masters
CN = kube-apiserver-etcd-client
[v3_req]
basicConstraints = CA:FALSE
keyUsage = digitalSignature, keyEncipherment
extendedKeyUsage = clientAuth
EOF
_sign "${PKI_DIR}/apiserver-etcd-client.key" "${PKI_DIR}/apiserver-etcd-client.crt" \
"${PKI_DIR}/etcd/ca.crt" "${PKI_DIR}/etcd/ca.key" \
"/O=system:masters/CN=kube-apiserver-etcd-client" "v3_req" "$TMPDIR/eclient.cnf"
# --- front-proxy-client.crt ---
log_info "重签 front-proxy-client.crt ..."
cat > "$TMPDIR/fpclient.cnf" <<'EOF'
[req]
distinguished_name = dn
req_extensions = v3_req
prompt = no
[dn]
CN = front-proxy-client
[v3_req]
basicConstraints = CA:FALSE
keyUsage = digitalSignature, keyEncipherment
extendedKeyUsage = clientAuth
EOF
_sign "${PKI_DIR}/front-proxy-client.key" "${PKI_DIR}/front-proxy-client.crt" \
"${PKI_DIR}/front-proxy-ca.crt" "${PKI_DIR}/front-proxy-ca.key" \
"/CN=front-proxy-client" "v3_req" "$TMPDIR/fpclient.cnf"
# --- etcd certs (server / peer / healthcheck-client) ---
for name in server peer healthcheck-client; do
log_info "重签 etcd/${name}.crt ..."
if [[ "$name" == "healthcheck-client" ]]; then
SUBJ="/CN=kube-etcd-healthcheck-client"; EKU="clientAuth"
else
SUBJ="/CN=localhost.localdomain"; EKU="serverAuth, clientAuth"
fi
cat > "$TMPDIR/etcd-${name}.cnf" <<EOF
[req]
distinguished_name = dn
req_extensions = v3_req
prompt = no
[dn]
CN = ${SUBJ#/CN=}
[v3_req]
basicConstraints = CA:FALSE
keyUsage = digitalSignature, keyEncipherment
extendedKeyUsage = ${EKU}
subjectAltName = DNS:localhost.localdomain,IP:${MASTER_IP},IP:127.0.0.1
EOF
_sign "${PKI_DIR}/etcd/${name}.key" "${PKI_DIR}/etcd/${name}.crt" \
"${PKI_DIR}/etcd/ca.crt" "${PKI_DIR}/etcd/ca.key" \
"$SUBJ" "v3_req" "$TMPDIR/etcd-${name}.cnf"
done
log_info "所有叶子证书已重签为100年,验证:"
for cert in "${PKI_DIR}"/*.crt "${PKI_DIR}"/etcd/*.crt; do
printf " %-40s %s\n" "$(basename $cert)" "$(openssl x509 -in $cert -noout -enddate 2>/dev/null)"
done
}
run_with_spinner "重签叶子证书中..." renew_leaf_certs_100y
# 重启 kubelet 使 static pod 加载新证书
log_info "重启 kubelet 加载新证书..."
systemctl restart kubelet
sleep 15
# 等待 API Server 恢复响应
log_info "等待 API Server 恢复..."
for i in $(seq 1 30); do
if kubectl get nodes &>/dev/null 2>&1 || \
curl -sk https://${MASTER_IP}:6443/healthz &>/dev/null; then
log_info "API Server 已恢复 ✓"
break
fi
[[ $i -eq 30 ]] && log_warn "API Server 恢复超时,继续执行(CNI 部署后会自动恢复)"
sleep 2
done
#===============================================================================
# 第13步:配置 kubectl + 部署 CNI
#===============================================================================
# ★ 部署 CNI 前清理残留 CNI 配置 ★
log_info "清理残留 CNI 配置..."
rm -f /etc/cni/net.d/*calico* /etc/cni/net.d/*flannel* /etc/cni/net.d/*kuber*
if [[ "$CNI_MODE" == "calico" ]]; then
# 如果选 calico,额外清理可能残留的 flannel
rm -f /etc/cni/net.d/*flannel*
elif [[ "$CNI_MODE" == "flannel" ]]; then
# 如果选 flannel,额外清理可能残留的 calico
rm -f /etc/cni/net.d/*calico*
fi
log_step "第13步:配置 kubectl + 部署 CNI"
# ★ 交互式 CNI 选择(仅 calico / flannel)★
if [[ -t 0 && "$CNI_MODE" == "auto" ]]; then
CALICO_YAML="$OFFLINE_DIR/configs/calico.yaml"
FLANNEL_YAML="$OFFLINE_DIR/configs/flannel.yaml"
AVAILABLE_CNIS=()
[[ -f "$CALICO_YAML" ]] && AVAILABLE_CNIS+=("calico")
[[ -f "$FLANNEL_YAML" ]] && AVAILABLE_CNIS+=("flannel")
if [[ ${#AVAILABLE_CNIS[@]} -gt 1 ]]; then
echo ""
log_info "检测到多个 CNI,请选择要部署的网络插件:"
for i in "${!AVAILABLE_CNIS[@]}"; do
printf " %d) %s\n" $((i+1)) "${AVAILABLE_CNIS[$i]}"
done
printf " 0) 跳过 CNI 部署\n"
read -rp "$(echo -e "${CYAN}${BOLD}请输入选项 [默认: 1]:${NC} ")" choice
choice="${choice:-1}"
if [[ "$choice" == "0" ]]; then
CNI_MODE="skip"
log_info "已选择跳过 CNI 部署"
elif [[ "$choice" =~ ^[0-9]+$ ]] && (( choice >= 1 && choice <= ${#AVAILABLE_CNIS[@]} )); then
CNI_MODE="${AVAILABLE_CNIS[$((choice-1))]}"
log_info "已选择: $CNI_MODE"
else
log_warn "无效输入 '${choice}',回退到 auto 模式"
fi
elif [[ ${#AVAILABLE_CNIS[@]} -eq 1 ]]; then
CNI_MODE="${AVAILABLE_CNIS[0]}"
log_info "仅检测到 $CNI_MODE,自动使用"
fi
fi
# 配置 kubectl
mkdir -p $HOME/.kube
cp -f /etc/kubernetes/admin.conf $HOME/.kube/config
chown $(id -u):$(id -g) $HOME/.kube/config
export KUBECONFIG=/etc/kubernetes/admin.conf
echo 'export KUBECONFIG=/etc/kubernetes/admin.conf' >> /etc/profile.d/k8s.sh
log_info "kubectl 已配置 ✓"
# 等待 API Server 就绪
log_info "等待 API Server 就绪..."
for i in $(seq 1 30); do
if kubectl get nodes &>/dev/null; then break; fi
sleep 2
done
# CNI 部署(精确匹配文件名,不再 glob)
CNI_YAML=""
if [[ "$CNI_MODE" != "skip" ]]; then
case "$CNI_MODE" in
calico) CNI_YAML="$OFFLINE_DIR/configs/calico.yaml" ;;
flannel) CNI_YAML="$OFFLINE_DIR/configs/flannel.yaml" ;;
auto)
# auto 回退:优先 calico,其次 flannel
[[ -f "$OFFLINE_DIR/configs/calico.yaml" ]] && CNI_YAML="$OFFLINE_DIR/configs/calico.yaml"
[[ -z "$CNI_YAML" && -f "$OFFLINE_DIR/configs/flannel.yaml" ]] && CNI_YAML="$OFFLINE_DIR/configs/flannel.yaml"
;;
esac
if [[ -n "$CNI_YAML" && -f "$CNI_YAML" ]]; then
log_info "部署 CNI: $(basename "$CNI_YAML")"
kubectl apply -f "$CNI_YAML"
log_info "$(basename "$CNI_YAML") 已部署,等待 Pod 就绪..."
sleep 10
else
log_warn "未找到 CNI 配置文件 (CNI_MODE=$CNI_MODE)"
log_warn "请手动部署 CNI 后节点才会 Ready"
fi
else
log_info "CNI 部署已跳过(用户选择)"
fi
#===============================================================================
# 第14步:集群验证 + 部署汇总
#===============================================================================
log_step "第14步:集群验证 + 部署汇总"
# 等待节点就绪
log_info "等待节点状态变为 Ready(最多60秒)..."
for i in $(seq 1 30); do
NODE_STATUS=$(kubectl get nodes --no-headers 2>/dev/null | awk '{print $2}')
if [[ "$NODE_STATUS" == "Ready" ]]; then break; fi
sleep 2
done
#===============================================================================
# 第14步结尾:部署汇总面板
#===============================================================================
echo ""
log_info "========== 集群状态 =========="
kubectl get nodes -o wide
echo ""
log_info "========== 系统 Pod 状态 =========="
kubectl get pods -n kube-system -o wide
echo ""
# 证书有效期验证(含叶子证书)
log_info "========== 证书有效期验证 =========="
for cert in /etc/kubernetes/pki/ca.crt \
/etc/kubernetes/pki/apiserver.crt \
/etc/kubernetes/pki/apiserver-kubelet-client.crt \
/etc/kubernetes/pki/apiserver-etcd-client.crt \
/etc/kubernetes/pki/front-proxy-ca.crt \
/etc/kubernetes/pki/front-proxy-client.crt \
/etc/kubernetes/pki/etcd/ca.crt \
/etc/kubernetes/pki/etcd/server.crt \
/etc/kubernetes/pki/etcd/peer.crt \
/etc/kubernetes/pki/etcd/healthcheck-client.crt; do
if [[ -f "$cert" ]]; then
EXPIRY=$(openssl x509 -in "$cert" -noout -enddate 2>/dev/null | cut -d= -f2)
log_info " $(basename "$cert"): 过期时间 → $EXPIRY"
fi
done
# ★ Premium 终态面板 ★
_term_w=$(tput cols 2>/dev/null || echo 80)
_inner=$((_term_w - 6))
[[ $_inner -lt 50 ]] && _inner=50
_border=""
for ((i=0; i<_inner; i++)); do _border+="═"; done
_sep=""
for ((i=0; i<_inner; i++)); do _sep+="─"; done
_dash_row() {
local key="$1" val="$2"
local kpad=$((_inner - ${#key} - ${#val} - 4))
[[ $kpad -lt 1 ]] && kpad=1
local kp=""; for ((i=0; i<kpad; i++)); do kp+=" "; done
printf " ${CYAN}║${NC} ${CYAN}%s${NC}%s${WHITE}%s${NC} ${PURPLE}║${NC}\n" "$key" "$kp" "$val"
}
echo ""
printf " ${GREEN}${BOLD}╔%s╗${NC}\n" "$_border"
_title="🎉 KUBERNETES CLUSTER DEPLOYED SUCCESSFULLY"
_tpad=$(( (_inner - ${#_title}) / 2 ))
_tp=""; for ((i=0; i<_tpad; i++)); do _tp+=" "; done
printf " ${GREEN}${BOLD}║${NC}%s${WHITE}${BOLD}%s${NC}${GREEN}${BOLD}%s║${NC}\n" "$_tp" "$_title" ""
printf " ${GREEN}${BOLD}║${NC}${DIM}%s${NC}${GREEN}${BOLD}║${NC}\n" "$_sep"
_dash_row "Version" "$K8S_VERSION"
_dash_row "Node IP" "$MASTER_IP"
_dash_row "Hostname" "$(hostname)"
_dash_row "Certificate" "100 Years (Native kubeadm)"
_dash_row "Runtime" "containerd"
_dash_row "CNI Plugin" "${CNI_MODE:-N/A}"
_dash_row "Pod CIDR" "$POD_CIDR"
_dash_row "Service CIDR" "$SERVICE_CIDR"
printf " ${GREEN}${BOLD}║${NC}${DIM}%s${NC}${GREEN}${BOLD}║${NC}\n" "$_sep"
_dash_row "Quick Start →" "kubectl get nodes"
_dash_row "" "kubectl get pods -A"
_dash_row "Add Worker →" "kubeadm token create --print-join-command"
printf " ${GREEN}${BOLD}╚%s╝${NC}\n" "$_border"
echo ""
3.执行脚本
bash deploy-k8s.sh
更多推荐

所有评论(0)