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

更多推荐