跳转至

集群状态生命周期:Status 与 InitStatus 联动

背景

当前集群有两个独立的状态字段:

字段 类型 值 谁控制
Status 运维状态 active / inactive / maintenance 管理员手动切换
InitStatus 生命周期 "" / pending / running / completed / failed 系统自动

问题:创建集群时 autoTest 通过即设 Status = active,不等待 InitStatus。导致一个初始化失败(无 Operator、无法承载租户)的集群在列表页显示为「运行中」,误导管理员。

列表页只展示 Status,不展示 InitStatus,加剧了信息断层。

设计原则

  1. 不合并两个字段 — 它们代表正交维度(运维状态 vs 生命周期),合并会丢失表达力
  2. InitStatus 联动 Status — 初始化结果自动影响运维状态,无需管理员手动干预
  3. 管理员操作优先 — 管理员主动设置的 Status 不被系统自动回退覆盖
  4. 列表页单字段可读 — 只看 Status 列就能判断集群是否可用

状态模型

Status 新增值:initializing

Status 枚举值:
  active         — 就绪,可承载租户
  initializing   — 创建后正在初始化(新增)
  maintenance    — 维护中(管理员手动 或 初始化失败自动)
  inactive       — 已停用(管理员手动)

对应数据库 check 约束和 Go binding 的 oneof 需要新增 initializing。

状态转换图

                          ┌──── 管理员手动 ────┐
                          ▼                    │
创建 ──→ initializing ──→ active ←──────→ maintenance
              │              │                 ▲
              │              └──→ inactive      │
              │                                │
              └──── init failed ───────────────┘

重新初始化:
  maintenance(init_failed) ──→ initializing ──→ active / maintenance

完整组合矩阵

Status InitStatus 列表展示 含义 可执行操作
initializing running 🔄 初始化中 刚创建,正在部署 Operator 查看进度
initializing pending 🔄 初始化中 任务排队中 查看进度
active completed 🟢 运行中 就绪,可承载租户 切换状态、创建租户
active "" 🟢 运行中 未开启 autoInit 或历史集群 切换状态
maintenance failed 🟠 维护中 初始化失败,需排查 重新初始化、查看日志
maintenance "" 🟠 维护中 管理员手动维护 恢复为运行中
inactive 任意 ⚫ 已停用 管理员手动下线 恢复为运行中

不合法组合(应杜绝)

Status InitStatus 为什么不合法
active running 还在初始化,不应标记为可用
active failed 初始化失败,不应标记为可用
active pending 还未初始化,不应标记为可用
inactive running 已停用的集群不应在初始化

涉及文件

文件 变更类型 说明
internal/shared/db/models/k8s_cluster.go 修改 Status 枚举新增 initializing
internal/admin/api/dto/cluster_dto.go 修改 binding 新增 initializing;响应新增 initStatus 展示
internal/admin/api/handlers/cluster_handler.go 修改 创建逻辑调整初始 Status
internal/admin/service/cluster_service.go 修改 创建流程 Status 初始化逻辑
internal/admin/service/cluster_init_executor.go 修改 updateClusterInitStatus 联动 Status
数据库迁移 新增 status 字段 check 约束新增 initializing

实现细节

1. 创建集群时的 Status 初始化

当前逻辑(cluster_service.go 创建流程):

autoTest=true  → 测试通过 → Status = "active"
autoTest=false → Status = "active"(默认)

新逻辑:

autoInit=true  → Status = "initializing"(无论 autoTest 结果如何)
autoInit=false → 走当前逻辑(autoTest 决定 active/inactive)

// cluster_service.go CreateCluster 方法中
func (s *ClusterService) CreateCluster(ctx context.Context, req *dto.CreateClusterRequest, adminID uint) (*dto.ClusterResponse, error) {
    // ... 创建集群记录 ...

    // 确定初始 Status
    if autoInit {
        cluster.Status = "initializing"
        cluster.InitStatus = "pending"
    } else if autoTest {
        // autoTest 通过则 active,失败则 inactive(当前逻辑不变)
    }

    // ... 保存并启动异步任务 ...
}

2. InitStatus 联动 Status

修改 cluster_init_executor.go 的 updateClusterInitStatus:

func (e *ClusterInitExecutor) updateClusterInitStatus(ctx context.Context, clusterID uint, initStatus string) {
    cluster, err := e.clusterRepo.GetByID(ctx, clusterID)
    if err != nil {
        logger.GetLogger().Warn("更新集群状态失败:获取集群失败",
            zap.Uint("cluster_id", clusterID), zap.Error(err))
        return
    }

    cluster.InitStatus = initStatus

    // 联动 Status(仅在系统可控状态下联动,不覆盖管理员操作)
    switch initStatus {
    case "running":
        // 仅当 Status 为 initializing 或 pending 时联动
        // 如果管理员已手动设为 maintenance/inactive,不干预
        if cluster.Status == "initializing" || cluster.Status == "" {
            cluster.Status = "initializing"
        }
    case "completed":
        // 初始化完成 → 升级为 active
        // 除非管理员已手动设为 inactive(尊重管理员意图)
        if cluster.Status != "inactive" {
            cluster.Status = "active"
        }
    case "failed":
        // 初始化失败 → 降级为 maintenance
        // 除非管理员已手动设为 inactive
        if cluster.Status != "inactive" {
            cluster.Status = "maintenance"
        }
    }

    if err := e.clusterRepo.Update(ctx, cluster); err != nil {
        logger.GetLogger().Warn("更新集群状态失败",
            zap.Uint("cluster_id", clusterID),
            zap.String("initStatus", initStatus),
            zap.String("status", cluster.Status),
            zap.Error(err))
    }
}

联动规则总结:

InitStatus 变更 当前 Status 联动后 Status 说明
→ running initializing initializing 保持
→ running maintenance maintenance 管理员已介入,不覆盖
→ completed initializing active 初始化成功,升级
→ completed maintenance active 重新初始化成功
→ completed inactive inactive 尊重管理员停用意图
→ failed initializing maintenance 初始化失败,降级
→ failed active maintenance 重新初始化失败,降级
→ failed inactive inactive 尊重管理员停用意图

3. 管理员手动切换 Status 的约束

管理员通过 API 切换 Status 时,新增校验:

// cluster_handler.go UpdateClusterStatus
func (h *ClusterHandler) UpdateClusterStatus(c *gin.Context) {
    // ... 参数解析 ...

    // 不允许手动设为 initializing(系统专用状态)
    if req.Status == "initializing" {
        c.JSON(400, gin.H{"error": "initializing 为系统状态,不可手动设置"})
        return
    }

    // 如果集群正在初始化中,警告但不阻止
    // (管理员可能需要在初始化卡住时手动切换到 maintenance)
    cluster, _ := h.service.GetCluster(ctx, clusterID)
    if cluster.InitStatus == "running" && req.Status == "active" {
        c.JSON(400, gin.H{"error": "集群正在初始化中,初始化完成后将自动切换为运行中"})
        return
    }

    // ... 执行更新 ...
}

4. 重新初始化支持

初始化失败后,管理员应能触发重新初始化。当前 initCluster API 需要支持:

// 重新初始化前置校验
if cluster.InitStatus == "running" {
    return errors.New("集群正在初始化中,请等待完成或取消")
}

// 重置状态
cluster.Status = "initializing"
cluster.InitStatus = "pending"
// 启动新的初始化任务...

5. DTO 和 API 响应

ClusterResponse 和列表 API 中补充 initStatus 字段(当前已有但列表未展示):

// cluster_dto.go - ListClustersResponse 中的 ClusterResponse 已包含 InitStatus
// 无需后端改动,由前端决定是否展示

6. 数据库迁移

-- 新增 initializing 到 status 枚举
-- 如果使用 check 约束:
ALTER TABLE k8s_clusters DROP CONSTRAINT IF EXISTS k8s_clusters_status_check;
ALTER TABLE k8s_clusters ADD CONSTRAINT k8s_clusters_status_check
    CHECK (status IN ('active', 'inactive', 'maintenance', 'initializing'));

-- 历史数据修复:将 status=active 但 init_status=failed 的记录修正
UPDATE k8s_clusters
SET status = 'maintenance'
WHERE status = 'active' AND init_status = 'failed';

-- 将 status=active 但 init_status=running 的记录修正
UPDATE k8s_clusters
SET status = 'initializing'
WHERE status = 'active' AND init_status IN ('running', 'pending');

7. binding 校验更新

// cluster_dto.go

// CreateClusterRequest - Status 字段(一般不在创建时指定,由系统决定)
// 无需改动

// UpdateClusterRequest
Status string `json:"status" binding:"omitempty,oneof=active inactive maintenance"`
// 注意:不包含 initializing,管理员不可手动设置

// UpdateClusterStatusRequest
Status string `json:"status" binding:"required,oneof=active inactive maintenance"`
// 同上,不包含 initializing

// ListClustersRequest
Status string `form:"status" binding:"omitempty,oneof=active inactive maintenance initializing"`
// 查询过滤包含 initializing,以便管理员筛选

前端适配

列表页

在集群列表表格中,Status 列的展示需要适配新状态:

Status 值 展示 颜色
active 运行中 绿色
initializing 初始化中 蓝色(带 loading 动画)
maintenance 维护中 橙色
inactive 已停用 灰色

可选:当 Status=maintenance 且 InitStatus=failed 时,展示为「初始化失败」而非「维护中」,帮助管理员快速区分人工维护和系统故障。

详情页

已有初始化进度组件,无需改动。

状态切换下拉

移除 initializing 选项(系统专用),其余不变。

向后兼容

  1. 历史集群(InitStatus 为空):Status 不受影响,行为与当前一致
  2. API 消费者:initializing 是新增值,不破坏现有 active/inactive/maintenance 的消费逻辑。但如果消费者对 Status 做了穷举匹配(switch/case without default),需要适配
  3. 数据库:需要迁移脚本扩展 check 约束 + 修复历史脏数据

测试计划

场景 预期 Status 预期 InitStatus
创建集群(autoInit=true) initializing pending → running
初始化成功 active completed
初始化失败 maintenance failed
初始化失败后管理员手动切 inactive inactive failed
重新初始化 initializing pending → running
重新初始化成功 active completed
创建集群(autoInit=false, autoTest=true) active ""
管理员尝试手动设 initializing 400 错误 —
初始化中管理员尝试设 active 400 错误 —