跳转至

集群状态生命周期: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.goupdateClusterInitStatus

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=maintenanceInitStatus=failed 时,展示为「初始化失败」而非「维护中」,帮助管理员快速区分人工维护和系统故障。

详情页

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

状态切换下拉

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

向后兼容

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

测试计划

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