1.脚本及tar包在同目录操作----
1.1 .主页资源下载离线tar包至服务器

2.

vim deploy-k8s.sh
#!/bin/bash
#===============================================================================
# K8s v1.30.14 All-in-One 离线部署脚本 (证书100年版)
#===============================================================================
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}"
# ==========================================================

# ========================= 颜色定义 =========================
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
CYAN='\033[0;36m'
WHITE='\033[1;37m'
DIM='\033[2m'
BOLD='\033[1m'
NC='\033[0m'

# ========================= 日志 & 装饰函数 =========================
log_info() {
    printf "\r\033[K"
    echo -e "${GREEN}${BOLD}[INFO]${NC}  ${DIM}$(date '+%H:%M:%S')${NC} $1"
}
log_warn() {
    printf "\r\033[K"
    echo -e "${YELLOW}${BOLD}[WARN]${NC}  ${DIM}$(date '+%H:%M:%S')${NC} $1"
}
log_error() {
    printf "\r\033[K"
    echo -e "${RED}${BOLD}[ERROR]${NC} ${DIM}$(date '+%H:%M:%S')${NC} $1"
}

# ★ 炫彩标题框(自适应宽度)★
log_step() {
    local title="$1"
    local term_cols
    term_cols=$(tput cols 2>/dev/null || echo 80)
    local width=$((term_cols - 4))
    [[ $width -gt 54 ]] && width=54
    [[ $width -lt 40 ]] && width=40
    local inner=$((width - 2))
    local 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=""
    for ((i=0; i<lpad; i++)); do lp+=" "; done
    for ((i=0; i<rpad; i++)); do rp+=" "; done

    local -a rainbow=(
        '\033[38;5;196m' '\033[38;5;202m' '\033[38;5;208m' '\033[38;5;214m'
        '\033[38;5;220m' '\033[38;5;226m' '\033[38;5;190m' '\033[38;5;154m'
        '\033[38;5;118m' '\033[38;5;82m'  '\033[38;5;46m'  '\033[38;5;47m'
        '\033[38;5;48m'  '\033[38;5;49m'  '\033[38;5;50m'  '\033[38;5;51m'
        '\033[38;5;45m'  '\033[38;5;39m'  '\033[38;5;33m'  '\033[38;5;27m'
        '\033[38;5;57m'  '\033[38;5;93m'  '\033[38;5;129m' '\033[38;5;165m'
        '\033[38;5;201m' '\033[38;5;200m' '\033[38;5;199m' '\033[38;5;198m'
        '\033[38;5;197m' '\033[38;5;196m'
    )
    local rc=${#rainbow[@]}

    echo ""
    local border_top="╔"
    for ((i=0; i<inner; i++)); do border_top+="═"; done
    border_top+="╗"
    for ((i=0; i<${#border_top}; i++)); do
        printf "%b%s%b" "${rainbow[$((i % rc))]}" "${border_top:$i:1}" "$NC"
        sleep 0.004
    done
    echo ""
    printf "%b%s%b" "${rainbow[0]}" "║" "$NC"
    printf "%s" "$lp"
    printf "\033[1;97m%s\033[0m" "$title"
    printf "%s" "$rp"
    printf "%b%s%b\n" "${rainbow[$(( (inner + 1) % rc ))]}" "║" "$NC"
    local border_bot="╚"
    for ((i=0; i<inner; i++)); do border_bot+="═"; done
    border_bot+="╝"
    for ((i=0; i<${#border_bot}; i++)); do
        local ci=$(( (rc - 1 - (i % rc)) ))
        printf "%b%s%b" "${rainbow[$ci]}" "${border_bot:$i:1}" "$NC"
        sleep 0.004
    done
    echo ""
}

# ★ 烟花特效加载动画 ★
run_with_spinner() {
    local msg="$1"; shift
    local -a fw_frames=(
        '    ·    ' '   ···   ' '  ··*··  ' ' ··***·· '
        '··*****··' '·*******·' ' ***·*** ' '  *·*·*  '
        '   ··*   ' '    ·    ' '         ' '    ✦    '
        '   ✦✦   ' '  ✦*✦✦  ' ' ✦✦***✦✦ ' '✦*****✦✦'
        '✦*******✦' ' ***·*** ' '  *·*·*  ' '   ··*   '
        '    ·    ' '         ' '    ·    ' '   ···   '
        '  ··⁺··  ' ' ··⁺*⁺·· ' '··⁺***⁺··' '·⁺*****⁺·'
        ' ⁺*****⁺ ' ' ***·*** ' '  *·*·*  ' '   ··⁺   '
    )
    local fc=${#fw_frames[@]}
    local -a fw_colors=(
        '\033[38;5;226m' '\033[38;5;220m' '\033[38;5;214m' '\033[38;5;208m'
        '\033[38;5;202m' '\033[38;5;196m' '\033[38;5;201m' '\033[38;5;213m'
        '\033[38;5;231m' '\033[38;5;226m' '\033[38;5;220m' '\033[38;5;214m'
        '\033[38;5;51m'  '\033[38;5;45m'  '\033[38;5;39m'  '\033[38;5;33m'
    )
    local cc=${#fw_colors[@]}
    (
        local i=0
        while true; do
            local fi=$((i % fc))
            local ci=$((i % cc))
            printf "\r${fw_colors[$ci]}${BOLD}%s${NC} ${WHITE}%s${NC}" "${fw_frames[$fi]}" "$msg"
            sleep 0.06
            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 filled=$((pct / 2))
    local empty=$((50 - 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
    printf "\r  ${YELLOW}[${YELLOW}%s${DIM}%s${NC}${YELLOW}]${NC} ${YELLOW}%3d%%${NC}  %s" \
        "$bar_filled" "$bar_empty" "$pct" "$desc"
}

# =====================================================================
#                     ↓↓↓ 业务逻辑 ↓↓↓
# =====================================================================

#===============================================================================
# 第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

if [[ -z "$OS_FAMILY" ]]; then
    log_error "无法确定系统族,请手动指定"; exit 1
fi

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' )"

#===============================================================================
# 第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"
    log_info "解压完成"
    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
    if [[ ! -f "$OFFLINE_DIR/MANIFEST.txt" ]]; then
        log_error "解压后未找到 MANIFEST.txt,离线包可能不完整"
        exit 1
    fi
    log_info "解压完成 → $OFFLINE_DIR"
fi

log_info "离线包内容:"
ls -la "$OFFLINE_DIR/"

#===============================================================================
# 第2步:前置检查
#===============================================================================
log_step "第2步:前置检查"

if [[ $EUID -ne 0 ]]; then
    log_error "请以 root 用户运行此脚本"; exit 1
fi

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步:安装系统依赖包"

if [[ "$OS_FAMILY" == "rpm" ]]; then
    if [[ -d "$OFFLINE_DIR/rpms" ]] && ls "$OFFLINE_DIR/rpms"/*.rpm &>/dev/null; then
        run_with_spinner "正在安装 RPM 依赖包..." \
            bash -c 'rpm -ivh --force --nodeps "$OFFLINE_DIR"/rpms/*.rpm 2>&1 | tail -5'
        log_info "RPM 依赖安装完成 ✓"
    else
        log_warn "rpms/ 目录为空或不存在,跳过 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
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"
            log_error "请确保离线包中包含 debs/$DEB_VER/ 目录"
            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
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
    if [[ -d "$OFFLINE_DIR/rpms" ]] && ls "$OFFLINE_DIR/rpms"/*.rpm &>/dev/null; then
        set +e
        rpm -ivh --force --nodeps "$OFFLINE_DIR"/rpms/*.rpm 2>/dev/null
        _rpm_rc=$?
        set -e
        if [[ $_rpm_rc -eq 0 ]]; then
            INSTALLED_FROM="rpms"
            log_info "RPM 安装完成"
        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
            _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 "二进制安装完成"
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"

mkdir -p /opt/cni/bin
if [[ -d "$OFFLINE_DIR/cni-plugins" ]] && ls "$OFFLINE_DIR/cni-plugins"/* &>/dev/null; then
    cp -f "$OFFLINE_DIR/cni-plugins"/* /opt/cni/bin/
    chmod +x /opt/cni/bin/*
    log_info "CNI plugins 已安装: $(ls /opt/cni/bin/ | wc -l) 个"
else
    log_warn "cni-plugins 目录为空,跳过"
fi

#===============================================================================
# 第7步:配置 containerd
#===============================================================================
log_step "第7步:配置 containerd"

mkdir -p /etc/containerd

_CONTAINERD_BIN="$(command -v containerd 2>/dev/null || echo /usr/local/bin/containerd)"
if [[ ! -x "$_CONTAINERD_BIN" ]]; then
    log_error "containerd 二进制未找到或不可执行"
    ls -la /usr/local/bin/containerd* 2>/dev/null || echo "  /usr/local/bin/ 下无 containerd"
    exit 1
fi
log_info "containerd 路径: $_CONTAINERD_BIN ($("$_CONTAINERD_BIN" --version 2>&1 || echo 'unknown'))"

set +e
"$_CONTAINERD_BIN" config default > /etc/containerd/config.toml 2>/tmp/containerd-config.err
_crd_rc=$?
set -e
if [[ $_crd_rc -ne 0 ]]; then
    log_error "containerd config default 失败 (rc=$_crd_rc):"
    cat /tmp/containerd-config.err
    exit 1
fi

sed -i "s|sandbox_image = .*|sandbox_image = \"${IMAGE_REPO}/pause:3.9\"|" /etc/containerd/config.toml
sed -i 's/SystemdCgroup = false/SystemdCgroup = true/' /etc/containerd/config.toml
sed -i 's/snapshotter = "native"/snapshotter = "overlayfs"/' /etc/containerd/config.toml 2>/dev/null || true
log_info "config.toml 已修改 (sandbox_image/systemd cgroup/overlayfs)"

systemctl daemon-reload
systemctl enable containerd
run_with_spinner "正在启动 containerd..." bash -c 'systemctl restart containerd && sleep 3'

if systemctl is-active --quiet containerd; then
    log_info "containerd 运行正常 ✓"
    crictl info > /dev/null 2>&1 && log_info "crictl 连接正常 ✓"
else
    log_error "containerd 启动失败!"
    journalctl -u containerd --no-pager -n 20
    exit 1
fi

#===============================================================================
# 第8步:导入容器镜像
#===============================================================================
log_step "第8步:导入容器镜像"

IMAGE_DIR="$OFFLINE_DIR/images"
IMPORTED=0; SKIPPED=0; FAILED=0

if [[ -d "$IMAGE_DIR" ]] && ls "$IMAGE_DIR"/*.tar &>/dev/null; then
    _IMG_TOTAL=0
    for tar_file in "$IMAGE_DIR"/*.tar; do
        fn="$(basename "$tar_file")"
        echo "$fn" | grep -qE "_sha256_" || _IMG_TOTAL=$((_IMG_TOTAL + 1))
    done
    _IMG_CUR=0
    for tar_file in "$IMAGE_DIR"/*.tar; do
        filename="$(basename "$tar_file")"
        if echo "$filename" | grep -qE "_sha256_"; then
            SKIPPED=$((SKIPPED + 1)); continue
        fi
        _IMG_CUR=$((_IMG_CUR + 1))
        show_progress $_IMG_CUR $_IMG_TOTAL "导入 $filename"
        if ctr -n k8s.io images import "$tar_file" > /dev/null 2>&1; then
            IMPORTED=$((IMPORTED + 1))
        else
            FAILED=$((FAILED + 1))
        fi
    done
    echo ""
else
    log_warn "images 目录为空或不存在"
fi
log_info "导入完成: 成功=$IMPORTED  跳过=$SKIPPED  失败=$FAILED"

log_info "当前 k8s 镜像列表:"
ctr -n k8s.io images ls -q 2>/dev/null | grep -v "^sha256:" | sort | while read -r img; do
    echo "    $img"
done

#===============================================================================
# 第9步:创建 kubelet drop-in
#===============================================================================
log_step "第9步:创建 kubelet drop-in"

mkdir -p /etc/systemd/system/kubelet.service.d
if [[ "$OS_FAMILY" == "rpm" ]]; then
    KUBELET_ENV_FILE="/etc/sysconfig/kubelet"
else
    KUBELET_ENV_FILE="/etc/default/kubelet"
fi
touch "$KUBELET_ENV_FILE"

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=-${KUBELET_ENV_FILE}
ExecStart=
ExecStart=/usr/bin/kubelet \$KUBELET_KUBECONFIG_ARGS \$KUBELET_CONFIG_ARGS \$KUBELET_KUBEADM_ARGS \$KUBELET_EXTRA_ARGS
EOF

systemctl daemon-reload
systemctl enable kubelet
log_info "kubelet drop-in 已创建 (EnvFile=$KUBELET_ENV_FILE) ✓"

#===============================================================================
# 第10步:清理旧状态
#===============================================================================
log_step "第10步:清理旧状态"

run_with_spinner "正在清理旧集群状态..." bash -c '
    kubeadm reset -f --cri-socket "'"$CRI_SOCKET"'" 2>/dev/null || true
    rm -rf /var/lib/etcd/* /etc/kubernetes/manifests /etc/kubernetes/pki
    rm -rf /var/lib/kubelet/config.yaml /var/lib/kubelet/kubeadm-flags.env
    rm -rf /etc/cni/net.d/* "$HOME/.kube"
    echo 1 > /proc/sys/net/ipv4/ip_forward
    systemctl restart containerd; sleep 2
    systemctl restart kubelet 2>/dev/null || true; sleep 3
'
log_info "清理完成 ✓"

#===============================================================================
# 第10.5步:生成 kubeadm 配置文件(证书有效期 100 年)
#===============================================================================
log_step "第10.5步:生成 kubeadm 配置"

cat > /tmp/kubeadm-config.yaml <<EOF
apiVersion: kubeadm.k8s.io/v1beta3
kind: ClusterConfiguration
kubernetesVersion: "${K8S_VERSION}"
imageRepository: "${IMAGE_REPO}"
networking:
  serviceSubnet: "${SERVICE_CIDR}"
  podSubnet: "${POD_CIDR}"
---
apiVersion: kubeadm.k8s.io/v1beta3
kind: InitConfiguration
localAPIEndpoint:
  advertiseAddress: "${MASTER_IP}"
nodeRegistration:
  criSocket: "${CRI_SOCKET}"
ignorePreflightErrors:
  - all
EOF

log_info "kubeadm 配置文件已生成: /tmp/kubeadm-config.yaml"
#===============================================================================
# 第11步:kubeadm init
#===============================================================================
log_step "第11步:kubeadm init"

log_info "参数:"
echo "    K8s版本:      $K8S_VERSION"
echo "    镜像仓库:     $IMAGE_REPO"
echo "    Service CIDR: $SERVICE_CIDR"
echo "    Pod CIDR:     $POD_CIDR"
echo "    CRI Socket:   $CRI_SOCKET"
echo "    OS:           $OS_PRETTY ($OS_FAMILY)"
echo "    CNI模式:      $CNI_MODE"
echo "    证书有效期:   100年 (876000h)"
echo ""

_INIT_LOG="/tmp/kubeadm-init.log"

_sp_frames=('⠋' '' '⠹' '' '⠼' '⠴' '⠦' '' '⠇' '⠏')
(
    _i=0
    while true; do
        printf "\r${CYAN}${BOLD}%s${NC} ${WHITE}kubeadm init 执行中(预计 1~3 分钟)...${NC}" "${_sp_frames[$((_i % ${#_sp_frames[@]}))]}"
        sleep 0.08
        _i=$((_i + 1))
    done
) &
_SP_PID=$!
disown $_SP_PID 2>/dev/null || true

set +e
kubeadm init \
    --config=/tmp/kubeadm-config.yaml \
    > "$_INIT_LOG" 2>&1
_INIT_RC=$?
set -e

kill $_SP_PID 2>/dev/null || true
wait $_SP_PID 2>/dev/null || true
printf "\r\033[K"

if [[ $_INIT_RC -ne 0 ]]; then
    log_error "kubeadm init 失败 (exit code=$_INIT_RC),最后 30 行日志:"
    tail -30 "$_INIT_LOG" | sed 's/^/    /'
    log_error "完整日志: $_INIT_LOG"
    exit 1
fi

grep -E "(Your Kubernetes control-plane|kubeadm join|To start using)" "$_INIT_LOG" | sed 's/^/    /' || true
log_info "kubeadm init 成功 ✓ (完整日志: $_INIT_LOG)"

#===============================================================================
# 第11.5步:重签证书为100年有效期
#===============================================================================
log_step "第11.5步:重签证书 → 100年有效期"

set +e
PKI_DIR="/etc/kubernetes/pki"
DAYS=36500

mkdir -p /tmp/pki-backup
cp -a "$PKI_DIR"/* /tmp/pki-backup/ 2>/dev/null || true
log_info "原始证书已备份到 /tmp/pki-backup/"

# 通用重签函数:从原证书提取SAN,用CA重签为100年
renew_leaf() {
    local cert="$1" key="$2" ca_cert="$3" ca_key="$4"
    local name
    name=$(basename "$cert")

    if [[ ! -f "$cert" || ! -f "$key" || ! -f "$ca_cert" || ! -f "$ca_key" ]]; then
        log_warn "跳过 $name (文件缺失)"
        return 0
    fi

    # 1) 提取CN
    local cn
    cn=$(openssl x509 -in "$cert" -noout -subject -nameopt multiline 2>/dev/null | grep commonName | sed 's/.*= //')
    [[ -z "$cn" ]] && cn="unknown"

    # 2) 提取SAN(关键:去掉前导空格,保留原始逗号分隔格式)
    local san=""
    san=$(openssl x509 -in "$cert" -noout -ext subjectAltName 2>/dev/null | tail -n +2 | sed 's/^[[:space:]]*//' | sed 's/IP Address:/IP:/g')

    # 3) 写扩展配置文件
    local ext="/tmp/_ext_${name}.cnf"
    cat > "$ext" <<EXTEOF
basicConstraints = CA:FALSE
keyUsage = digitalSignature, keyEncipherment
extendedKeyUsage = clientAuth, serverAuth
EXTEOF
    if [[ -n "$san" ]]; then
        echo "subjectAltName = ${san}" >> "$ext"
    fi

    # 4) 生成CSR并重签
    local csr="/tmp/_csr_${name}.pem"
    openssl req -new -key "$key" -subj "/CN=${cn}" -out "$csr" 2>/dev/null

    local errfile="/tmp/_err_${name}.txt"
        if openssl x509 -req -in "$csr" \
            -CA "$ca_cert" -CAkey "$ca_key" -CAcreateserial \
            -days "$DAYS" -extfile "$ext" \
            -out "$cert" 2>"$errfile"; then
    log_info "✓ $name → 100年 (CN=$cn)"
    else
        log_warn "重签失败 $name: $(cat "$errfile")"
        cp -f "/tmp/pki-backup/$name" "$cert" 2>/dev/null || true
    fi

    rm -f "$csr" "$ext" "$errfile"
}

log_info "── 重签叶子证书 ──"
renew_leaf "$PKI_DIR/apiserver.crt"                "$PKI_DIR/apiserver.key"                "$PKI_DIR/ca.crt"             "$PKI_DIR/ca.key"
renew_leaf "$PKI_DIR/apiserver-kubelet-client.crt" "$PKI_DIR/apiserver-kubelet-client.key" "$PKI_DIR/ca.crt"             "$PKI_DIR/ca.key"
renew_leaf "$PKI_DIR/apiserver-etcd-client.crt"    "$PKI_DIR/apiserver-etcd-client.key"    "$PKI_DIR/etcd/ca.crt"        "$PKI_DIR/etcd/ca.key"
renew_leaf "$PKI_DIR/front-proxy-client.crt"       "$PKI_DIR/front-proxy-client.key"       "$PKI_DIR/front-proxy-ca.crt" "$PKI_DIR/front-proxy-ca.key"
renew_leaf "$PKI_DIR/etcd/server.crt"              "$PKI_DIR/etcd/server.key"              "$PKI_DIR/etcd/ca.crt"        "$PKI_DIR/etcd/ca.key"
renew_leaf "$PKI_DIR/etcd/peer.crt"                "$PKI_DIR/etcd/peer.key"                "$PKI_DIR/etcd/ca.crt"        "$PKI_DIR/etcd/ca.key"
renew_leaf "$PKI_DIR/etcd/healthcheck-client.crt"  "$PKI_DIR/etcd/healthcheck-client.key"  "$PKI_DIR/etcd/ca.crt"        "$PKI_DIR/etcd/ca.key"

# 更新 kubeconfig
# ... 前面 renew_leaf 和 PKI 叶子证书重签保持不变 ...

# 更新 kubeconfig
log_info "── 更新 kubeconfig ──"
rm -f /etc/kubernetes/admin.conf /etc/kubernetes/controller-manager.conf \
      /etc/kubernetes/scheduler.conf /etc/kubernetes/kubelet.conf \
      /etc/kubernetes/super-admin.conf "$HOME/.kube/config" 2>/dev/null || true
kubeadm init phase kubeconfig all --config=/tmp/kubeadm-config.yaml >/dev/null 2>&1 || true
mkdir -p "$HOME/.kube"
cp -f /etc/kubernetes/admin.conf "$HOME/.kube/config" 2>/dev/null || true
chown "$(id -u):$(id -g)" "$HOME/.kube/config" 2>/dev/null || true
export KUBECONFIG=/etc/kubernetes/admin.conf
log_info "kubeconfig 已重新生成 ✓"

# ========== 新增:重签 kubeconfig 内嵌客户端证书为100年 ==========
log_info "── 重签 kubeconfig 内嵌客户端证书 ──"

renew_kubeconfig_cert() {
    local conf_path="$1" cn="$2" org="$3" ca_cert="$4" ca_key="$5"
    local conf_name
    conf_name=$(basename "$conf_path")

    if [[ ! -f "$conf_path" || ! -f "$ca_cert" || ! -f "$ca_key" ]]; then
        log_warn "跳过 $conf_name (文件缺失)"
        return 0
    fi

    # 生成新密钥和证书
    local tmp_key="/tmp/_kc_${cn}.key"
    local tmp_csr="/tmp/_kc_${cn}.csr"
    local tmp_crt="/tmp/_kc_${cn}.crt"

    openssl genrsa -out "$tmp_key" 2048 2>/dev/null
    openssl req -new -key "$tmp_key" -subj "/O=${org}/CN=${cn}" -out "$tmp_csr" 2>/dev/null
    openssl x509 -req -in "$tmp_csr" \
        -CA "$ca_cert" -CAkey "$ca_key" -CAcreateserial \
        -days "$DAYS" \
        -out "$tmp_crt" 2>/dev/null

    # Base64 编码并嵌入 kubeconfig
    local cert_b64 key_b64
    cert_b64=$(base64 -w0 "$tmp_crt")
    key_b64=$(base64 -w0 "$tmp_key")

    kubectl --kubeconfig="$conf_path" config set users."${cn}".client-certificate-data "${cert_b64}" 2>/dev/null
    kubectl --kubeconfig="$conf_path" config set users."${cn}".client-key-data "${key_b64}" 2>/dev/null

    log_info "✓ $conf_name → 100年 (CN=$cn)"

    rm -f "$tmp_key" "$tmp_csr" "$tmp_crt"
}

renew_kubeconfig_cert "/etc/kubernetes/admin.conf"              "kubernetes-admin"              "system:masters"                "$PKI_DIR/ca.crt" "$PKI_DIR/ca.key"
renew_kubeconfig_cert "/etc/kubernetes/controller-manager.conf" "system:kube-controller-manager" "system:kube-controller-manager" "$PKI_DIR/ca.crt" "$PKI_DIR/ca.key"
renew_kubeconfig_cert "/etc/kubernetes/scheduler.conf"          "system:kube-scheduler"          "system:kube-scheduler"          "$PKI_DIR/ca.crt" "$PKI_DIR/ca.key"
renew_kubeconfig_cert "/etc/kubernetes/super-admin.conf"        "kubernetes-super-admin"         "system:masters"                "$PKI_DIR/ca.crt" "$PKI_DIR/ca.key"

# 同步更新 ~/.kube/config(admin.conf 的副本)
cp -f /etc/kubernetes/admin.conf "$HOME/.kube/config" 2>/dev/null || true
log_info "kubeconfig 内嵌证书重签完成 ✓"
# ========== 新增结束 ==========

# 重启控制平面
log_info "── 重启控制平面 ──"
# ... 后面重启和验证逻辑保持不变 ...
# 重启控制平面
log_info "── 重启控制平面 ──"
if command -v crictl &>/dev/null; then
    crictl stopp "$(crictl pods -q --namespace kube-system 2>/dev/null)" 2>/dev/null || true
    crictl rmp   "$(crictl pods -q --namespace kube-system 2>/dev/null)" 2>/dev/null || true
fi
sleep 5

run_with_spinner "等待 API Server 就绪..." bash -c '
    export KUBECONFIG=/etc/kubernetes/admin.conf
    for i in $(seq 1 90); do
        kubectl get --raw /healthz &>/dev/null && exit 0
        sleep 2
    done
    exit 1
' || { log_error "API Server 未就绪"; exit 1; }

log_info "控制平面已重启 ✓"

# 验证
log_info "── 证书到期时间 ──"
for crt in ca.crt apiserver.crt apiserver-kubelet-client.crt apiserver-etcd-client.crt \
           front-proxy-ca.crt front-proxy-client.crt \
           etcd/ca.crt etcd/server.crt etcd/peer.crt etcd/healthcheck-client.crt; do
    exp=$(openssl x509 -in "$PKI_DIR/$crt" -noout -enddate 2>/dev/null | cut -d= -f2)
    printf "    %-38s %s\n" "$crt" "$exp"
done
echo ""
log_info "所有证书重签完成 ✓"

set -e
#===============================================================================
# 第12步:配置 kubectl
#===============================================================================
log_step "第12步:配置 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

cat > /etc/profile.d/kubectl.sh <<'EOF'
export KUBECONFIG=/etc/kubernetes/admin.conf
alias k='kubectl'
source <(kubectl completion bash) 2>/dev/null || true
complete -o default -F __start_kubectl k 2>/dev/null || true
EOF
chmod +x /etc/profile.d/kubectl.sh
log_info "kubectl 配置完成 ✓"

#===============================================================================
# ★★★ 第13步:CNI 自动选型 & 部署网络 ★★★
#===============================================================================
log_step "第13步:CNI 自动选型 & 部署网络"

if [[ -t 0 && "$CNI_MODE" == "auto" ]]; then
    echo ""
    echo -e "${CYAN}${BOLD}请选择 CNI 网络插件:${NC}"
    echo -e "  ${WHITE}1)${NC} Auto    - 自动评分选型(推荐)"
    echo -e "  ${WHITE}2)${NC} Calico  - 功能全面,支持 NetworkPolicy/eBPF/BGP"
    echo -e "  ${WHITE}3)${NC} Flannel - 轻量简单,适合小规模集群"
    echo ""
    read -r -p "$(echo -e "${GREEN}${BOLD}请输入选项 [1/2/3] (默认 1):${NC} ")" _cni_choice
    case "$_cni_choice" in
        2|calico|Calico)   CNI_MODE="calico" ;;
        3|flannel|Flannel) CNI_MODE="flannel" ;;
        *)                 CNI_MODE="auto" ;;
    esac
    log_info "用户选择: CNI_MODE=${CNI_MODE}"
    echo ""
fi

choose_cni() {
    local FLANNEL_SCORE=0 CALICO_SCORE=0
    local -a REASONS=()
    local NODE_COUNT=1
    local TOTAL_CPU TOTAL_MEM_KI KERNEL_MAJOR KERNEL_MINOR
    TOTAL_CPU=$(nproc)
    TOTAL_MEM_KI=$(free -k 2>/dev/null | awk '/^Mem:/{print int($2/1024/1024)}')
    [[ -z "$TOTAL_MEM_KI" || ! "$TOTAL_MEM_KI" =~ ^[0-9]+$ ]] && TOTAL_MEM_KI=16
    KERNEL_MAJOR=$(uname -r | awk -F. '{print $1}')
    KERNEL_MINOR=$(uname -r | awk -F. '{print $2}')
    local HAS_BGP_PEER="no"
    pgrep -x "frr\|bird\|bgpd" &>/dev/null && HAS_BGP_PEER="yes"
    local NETPOL_COUNT=0
    NETPOL_COUNT=$(kubectl get networkpolicy -A --no-headers 2>/dev/null | wc -l || echo 0)
    local L2_REACHABLE="no"
    local GW_NET MY_NET
    GW_NET=$(ip route show default 2>/dev/null | awk '{print $3}' | cut -d. -f1-3)
    MY_NET=$(hostname -I | awk '{print $1}' | cut -d. -f1-3)
    [[ "$GW_NET" == "$MY_NET" ]] && L2_REACHABLE="yes"
    local CLOUD_PROVIDER="bare-metal"
    curl -s --connect-timeout 2 http://100.100.100.200/latest/meta-data/ &>/dev/null && CLOUD_PROVIDER="aliyun"
    curl -s --connect-timeout 2 http://169.254.169.254/latest/meta-data/ &>/dev/null && CLOUD_PROVIDER="aws/gcp/openstack"

    log_info "── CNI 选型检测数据 ──"
    log_info "  节点数: ${NODE_COUNT} | CPU: ${TOTAL_CPU} | MEM: ${TOTAL_MEM_KI}Gi"
    log_info "  内核: ${KERNEL_MAJOR}.${KERNEL_MINOR} | BGP: ${HAS_BGP_PEER} | L2: ${L2_REACHABLE}"
    log_info "  云平台: ${CLOUD_PROVIDER} | NetPol数: ${NETPOL_COUNT}"
    echo ""

    if [ "$NODE_COUNT" -le 5 ]; then
        ((FLANNEL_SCORE += 3)); REASONS+=("节点数 ≤5,Flannel 足够轻量")
    elif [ "$NODE_COUNT" -le 20 ]; then
        ((FLANNEL_SCORE += 1)); ((CALICO_SCORE += 1)); REASONS+=("节点数 ${NODE_COUNT},两者皆可")
    else
        ((CALICO_SCORE += 3)); REASONS+=("节点数 ${NODE_COUNT} > 20,Calico 更适合大规模")
    fi
    [ "$NETPOL_COUNT" -gt 0 ] && { ((CALICO_SCORE += 5)); REASONS+=("已有 NetworkPolicy,Calico 原生支持"); }
    [[ "$TOTAL_MEM_KI" -le 8 && "$TOTAL_CPU" -le 4 ]] && { ((FLANNEL_SCORE += 2)); REASONS+=("资源受限(CPU≤4,MEM≤8Gi),Flannel 更轻量"); }
    [ "$KERNEL_MAJOR" -ge 5 ] && [ "$KERNEL_MINOR" -ge 4 ] 2>/dev/null && { ((CALICO_SCORE += 1)); REASONS+=("内核 ≥5.4,Calico 可启用 eBPF"); }
    [ "$HAS_BGP_PEER" = "yes" ] && { ((CALICO_SCORE += 4)); REASONS+=("检测到 BGP 环境,Calico 原生 BGP 路由最优"); }
    [ "$L2_REACHABLE" = "yes" ] && { ((FLANNEL_SCORE += 1)); REASONS+=("节点二层可达,Flannel host-gw 零封装高性能"); }
    [ "$CLOUD_PROVIDER" != "bare-metal" ] && { ((FLANNEL_SCORE += 1)); REASONS+=("云环境(${CLOUD_PROVIDER}),Flannel 通常足够"); }
    ((FLANNEL_SCORE += 1)); REASONS+=("Flannel 运维复杂度更低")

    echo ""
    echo "┌─────────────────────────────────────────┐"
    echo "│         CNI 选型评分结果                │"
    echo "├─────────────────────────────────────────┤"
    printf "│  Flannel 得分: %-23s│\n" "$FLANNEL_SCORE"
    printf "│  Calico  得分: %-23s│\n" "$CALICO_SCORE"
    echo "└─────────────────────────────────────────┘"
    echo ""
    log_info "决策依据:"
    for r in "${REASONS[@]}"; do echo "  • $r"; done
    echo ""

    if [ "$CALICO_SCORE" -gt "$FLANNEL_SCORE" ]; then CHOSEN_CNI="calico"
    elif [ "$FLANNEL_SCORE" -gt "$CALICO_SCORE" ]; then CHOSEN_CNI="flannel"
    else CHOSEN_CNI="calico"; log_warn "评分持平,默认选择 Calico(功能更全面)"
    fi
    log_info "✅ CNI 选型结果: ${CHOSEN_CNI^^}"
}

case "$CNI_MODE" in
    calico)  CHOSEN_CNI="calico";  log_info "CNI_MODE=calico,强制使用 Calico" ;;
    flannel) CHOSEN_CNI="flannel"; log_info "CNI_MODE=flannel,强制使用 Flannel" ;;
    auto|*)  choose_cni ;;
esac

CALICO_YAML="$OFFLINE_DIR/configs/calico.yaml"
FLANNEL_YAML="$OFFLINE_DIR/configs/flannel.yaml"

if [[ "$CHOSEN_CNI" == "calico" && ! -f "$CALICO_YAML" ]]; then
    if [[ -f "$FLANNEL_YAML" ]]; then
        log_warn "离线包中无 calico.yaml,fallback 到 Flannel"; CHOSEN_CNI="flannel"
    else log_error "离线包中既无 calico.yaml 也无 flannel.yaml!"; exit 1; fi
fi
if [[ "$CHOSEN_CNI" == "flannel" && ! -f "$FLANNEL_YAML" ]]; then
    if [[ -f "$CALICO_YAML" ]]; then
        log_warn "离线包中无 flannel.yaml,fallback 到 Calico"; CHOSEN_CNI="calico"
    else log_error "离线包中既无 flannel.yaml 也无 calico.yaml!"; exit 1; fi
fi

case "$CHOSEN_CNI" in
    calico)
        log_info "部署 Calico 网络..."
        kubectl apply -f "$CALICO_YAML"
        KERNEL_VER=$(uname -r | cut -d. -f1-2 | tr -d '.')
        if [ "$KERNEL_VER" -lt 54 ]; then
            log_warn "内核版本低于 5.4 ($(uname -r)),禁用 Calico eBPF"
            WAIT_DS=0
            while [[ $WAIT_DS -lt 60 ]]; do
                kubectl get daemonset calico-node -n kube-system &>/dev/null && break
                sleep 3; WAIT_DS=$((WAIT_DS + 3))
            done
            if kubectl get daemonset calico-node -n kube-system &>/dev/null; then
                kubectl set env daemonset/calico-node -n kube-system FELIX_BPFENABLED=false
                kubectl delete pod -n kube-system -l k8s-app=calico-node
                log_info "Calico eBPF 已禁用,Pod 已重启 ✓"
            else log_warn "calico-node DaemonSet 尚未创建,eBPF 禁用跳过"; fi
        else log_info "内核版本 $(uname -r) >= 5.4,保留 Calico eBPF 默认配置"; fi
        ;;
    flannel)
        log_info "部署 Flannel 网络..."
        kubectl apply -f "$FLANNEL_YAML"
        ;;
esac

log_info "等待 ${CHOSEN_CNI^^} & CoreDNS 就绪(最多 180 秒)..."
WAIT=0; MAX_WAIT=180
while [[ $WAIT -lt $MAX_WAIT ]]; do
    READY="$(kubectl get pods -A --field-selector=status.phase=Running 2>/dev/null | grep -cE '(coredns|calico|flannel)' || true)"
    TOTAL="$(kubectl get pods -A 2>/dev/null | grep -cE '(coredns|calico|flannel)' || true)"
    if [[ "$READY" -ge 3 && "$READY" == "$TOTAL" ]]; then
        printf "\r\033[K"
        log_info "${CHOSEN_CNI^^} & CoreDNS 全部就绪 ✓ (${WAIT}s)"
        break
    fi
    sleep 5; WAIT=$((WAIT + 5))
    show_progress $WAIT $MAX_WAIT "Running: ${READY}/${TOTAL}"
done
echo ""
[[ $WAIT -ge $MAX_WAIT ]] && log_warn "超时!部分 Pod 可能仍在启动中,请手动检查"

#===============================================================================
# 第14步:验证集群
#===============================================================================
log_step "第14步:验证集群"

echo ""
log_info "--- 节点状态 ---"
kubectl get nodes -o wide 2>/dev/null || true
echo ""
log_info "--- 全部 Pod ---"
kubectl get pods -A -o wide 2>/dev/null || true
echo ""
log_info "--- 集群信息 ---"
kubectl cluster-info 2>/dev/null || true
echo ""
log_info "--- API Server 连通性 ---"
kubectl get --raw /healthz 2>/dev/null && log_info "API Server 健康 ✓" || log_warn "API Server 异常"

#===============================================================================
# 第15步:验证证书有效期
#===============================================================================
log_step "第15步:验证证书有效期(100年)"

log_info "证书过期时间:"
kubeadm certs check-expiration 2>/dev/null | sed 's/^/    /' || true
echo ""
log_info "CA 证书详情:"
openssl x509 -in /etc/kubernetes/pki/ca.crt -noout -subject -enddate 2>/dev/null | sed 's/^/    /' || true

#===============================================================================
# 完成
#===============================================================================
log_step "🎉 K8s $K8S_VERSION 部署完成"

JOIN_CMD="$(kubeadm token create --print-join-command 2>/dev/null || echo "N/A")"
cat <<EOF

  ┌─────────────────────────────────────────────────┐
  │          部 署 信 息 汇 总                        │
  ├─────────────────────────────────────────────────┤
  │  K8s 版本:     $K8S_VERSION
  │  操作系统:     $OS_PRETTY
  │  系统族:       $OS_FAMILY ($OS_DISTRO)
  │  Master IP:    $MASTER_IP
  │  Pod CIDR:     $POD_CIDR
  │  Service CIDR: $SERVICE_CIDR
  │  网络插件:     ${CHOSEN_CNI^^} (选型模式: $CNI_MODE)
  │  容器运行时:   containerd
  │  证书有效期:   100年 (876000h)
  ├─────────────────────────────────────────────────┤
  │  kubectl 使用:
  │    export KUBECONFIG=/etc/kubernetes/admin.conf
  │    kubectl get nodes -o wide
  │    kubectl get pods -A
  ├─────────────────────────────────────────────────┤
  │  添加 Worker 节点:
  │    $JOIN_CMD
  └─────────────────────────────────────────────────┘

EOF
log_info "部署日志已保存至 /tmp/kubeadm-init.log"
echo "============================================================"
bash deploy-k8s.sh

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐