# Premium 多组件、多集群升级编排设计（简化版）

## Metadata

| Item | Value |
| --- | --- |
| Status | Draft |
| First drafted | 2026-08-11 |
| Last updated | 2026-08-14 |
| Scope | Premium `SHARED_POOL` / `LOGICAL_CLUSTER` 的单集群或多集群指定组件、全组件升级；第一版不处理 TiCDC |
| Related implementation | `pkg/service/cloudconfig/change/config`、`pkg/service/approval`、`pkg/service/k8supgradeplan`、`nextgen_applier.go`、`resource_pool_applier.go` |

## Table of Contents

- [Introduction](#introduction)
- [Motivation or Background](#motivation-or-background)
  - [Current State](#current-state)
  - [Goals](#goals)
  - [Non-goals](#non-goals)
- [Detailed Design](#detailed-design)
  - [Key Decisions](#key-decisions)
  - [Architecture and Responsibility Boundaries](#architecture-and-responsibility-boundaries)
  - [Target Selection and Scope Resolution](#target-selection-and-scope-resolution)
  - [Fixed Component Order](#fixed-component-order)
  - [Approval Design](#approval-design)
  - [Managed ConfigurationChange](#managed-configurationchange)
  - [Conflict Detection](#conflict-detection)
  - [State Model](#state-model)
  - [Reconciliation Algorithm](#reconciliation-algorithm)
  - [Data Model](#data-model)
  - [API](#api)
  - [Failure Retry Cancellation and Rollback](#failure-retry-cancellation-and-rollback)
  - [Consistency and Idempotency](#consistency-and-idempotency)
  - [Code Changes](#code-changes)
  - [Observability and Audit](#observability-and-audit)
  - [Rollout Plan](#rollout-plan)
- [Test Design](#test-design)
  - [Functional Tests](#functional-tests)
  - [Scenario Tests](#scenario-tests)
  - [Compatibility Tests](#compatibility-tests)
- [Impacts & Risks](#impacts--risks)
  - [Impacts](#impacts)
  - [Risks and Mitigations](#risks-and-mitigations)
- [Unresolved Questions](#unresolved-questions)

## Introduction

本设计为 Premium 升级增加一个轻量的持久化编排层，支持在一个操作中选择一个或多个 Premium 集群，并升级指定组件或全部现存组件。一个 Upgrade Plan 的全部集群、组件、目标版本和执行顺序形成一份不可变清单，并只创建一张计划级审批单。

审批通过后，编排器按照服务端写死的 SOP 顺序，为每个可执行 Apply Group 即时创建一个受管 `ConfigurationChange`。一个 Apply Group 可以包含一个或多个用户选中的组件；TiFlash、PD 和 BR 按 SOP 作为原子 Apply Group，同组中被选中的组件合并到一次 Apply，但不自动加入未选择组件。该子 Change 复用现有 CloudConfig 的多组件变更构建、目标版本校验、冲突检查、组件专属 applier、结果检查和审计能力，但不再创建审批单，且自身 `approval_id` 为空。一个 Plan 只有 `premium_upgrade_plans.approval_id` 所指向的父审批具有授权语义。

第一版有意保持简单：不维护组件依赖图；SOP 原子组只控制所选组件的 Apply 聚合，不补齐组内或组外未选择组件；不检查未选择组件的版本兼容性；不提供用户可选的 Dependency Policy 或 Failure Policy，也不增加 Premium 专属资源锁表。

## Motivation or Background

### Current State

当前 Premium 升级已经支持在 `CreateConfigurationChangeReq.components[]` 中为 `LOGICAL_CLUSTER` 或 `SHARED_POOL` 组件指定目标版本：

- `ConfigurationChangeBuilder.AssembleComponentChange` 构建组件版本、镜像和相关配置模板变更；
- `NextgenApplier` 和 `ResourcePoolApplier` 根据组件调用 Config Center、Management Service 或 Cluster Service NG；
- 创建普通 `ConfigurationChange` 时会检查相同 CloudConfig locator 下是否已有未完成 Change；
- 普通 Change 从 `created` 开始，后台 watcher 会调用 `SendApproval()` 为它创建独立审批单；
- 当前 Change phase 只有 `created/approving/wait_for_apply/applying/applied/cancelled/withdrawn`，没有失败终态；`CheckResult()` 未完成时只返回 retriable error；
- 一个 `ConfigurationChange` 只有一个目标 locator、一套 phase 和一个整体结果。

现有能力可以支持“一个目标的一次多组件配置变更”，但不能支持“多个集群共用一张审批单，同时保留组件级顺序、结果和重试”。如果简单地为每个集群或组件调用现有公开 Create API，就会再次产生多个审批单。

### Goals

- 支持以下四种请求，并使用同一种内部模型：
  - 一个集群的指定组件；
  - 一个集群的全部现存组件；
  - 多个集群的相同指定组件集合；
  - 多个集群各自的全部现存组件。
- 一个 Upgrade Plan 无论包含多少集群和组件，都只创建一张审批单。
- 审批清单明确展示每个目标的组件、起止版本、实际 CloudConfig locator、执行顺序、审批路由和 apply strategy。
- 服务端维护一份固定组件顺序；部分组件升级只保留选中组件之间的相对顺序。
- Plan 按固定阶段顺序推进；同一阶段可以包含多个独立 Apply Group，每个 Apply Group 在每个实际 locator 上创建独立 ConfigurationChange，并允许受控并行。
- 每个 Apply Group 独立记录状态、关联一个受管 `ConfigurationChange`，并复用现有多组件 Apply 和 CheckResult。
- BR Apply Group 执行前检查同一 Shared Pool 下全部 Logical Cluster 的运行中 Restore/PITR 状态；存在冲突时等待并重试。
- 为受管 Change 增加明确的 `failed` 终态，由 Step 的超时和错误分类驱动，避免失败 Change 永久阻塞 locator。
- 当前目标某一步失败后停止该目标的剩余步骤；其他不受影响目标继续执行。
- 复用现有 ConfigurationChange 的未完成 Change 检查，避免增加重复的 Premium 资源锁表。
- 保持现有普通 ConfigurationChange API、审批流和历史记录兼容。
- 多副本 DevOps 下通过 Step CAS 防止同一个步骤被重复调度。

### Non-goals

- 第一版不读取或维护组件兼容性矩阵。
- 第一版不检查未选择组件的版本，也不根据目标版本自动加入原子组成员或其他前置阶段。
- 第一版不支持动态依赖 DAG、并行依赖分支或用户自定义升级顺序。
- 第一版不提供 `Dependency Policy`、`Failure Policy`、`Start Policy` 等用户配置。
- 第一版不建设通用工作流引擎。
- 第一版不提供自动回滚或自动降级；补偿操作必须创建新的 Upgrade Plan 并重新审批。
- 不承诺多集群原子性，允许部分目标成功、部分目标失败。
- 不新增 `premium_upgrade_step_dependencies`。
- 不新增 `premium_upgrade_resource_locks`。
- 不替代 Config Center、Management Service 或 Cluster Service NG 对实际运行状态的所有权。

## Detailed Design

### Key Decisions

| Area | Decision |
| --- | --- |
| Approval boundary | 一个 `PremiumUpgradePlan` 只创建一个父审批；Target、Step 和受管 ConfigurationChange 不创建子审批，子 Change 的 `approval_id` 为空 |
| Execution unit | 一个 Step 表示一个实际 locator 上的一次 Apply，可包含一个或多个组件；相同 Shared Pool locator/Apply Group 只生成一次 |
| Component order | 服务端代码中固定顺序；部分选择按相对顺序执行 |
| Dependency | 不构建 DAG；只根据固定 SOP 顺序和原子组执行，不增加 dependency 表 |
| Version validation | 只验证选中组件的目标版本可用；不检查未选择组件版本 |
| Failure behavior | 一个 locator 失败后停止受其影响 cluster 的剩余步骤；其他 cluster 继续 |
| Child change creation | 父审批通过且 Step 即将执行时即时创建，不在等待审批期间提前创建 |
| Conflict protection | 调度前复用严格的 `ClusterConfigurationChangeable`，创建时再次执行现有 scope-aware 校验；不增加 Premium 资源锁表 |
| Multi-replica claiming | 通过 `premium_upgrade_steps.version` 的 CAS 更新抢占 Step |
| Existing API | 普通 ConfigurationChange 的 Create/Approve/Apply/Cancel 行为保持不变 |

执行与审批关系如下：

```text
Premium Upgrade Plan
  └── one plan approval
        ├── selected cluster A
        │     ├── Step 1 -> managed ConfigurationChange -> apply
        │     └── Step 2 -> managed ConfigurationChange -> apply
        └── selected cluster B
              ├── Step 1 -> managed ConfigurationChange -> apply
              └── Step 2 -> managed ConfigurationChange -> apply
```

`premium_upgrade_plans.approval_id` 是唯一授权来源。受管 ConfigurationChange 的 `approval_id` 保持 `NULL`，通过 `premium_upgrade_step_id` 反向关联 Plan 和父审批，因此不会被现有 CloudConfig approval event handler 当作独立审批对象。

### Architecture and Responsibility Boundaries

```mermaid
flowchart TD
    U[User selects clusters and components] --> P[Preview]
    P --> R[Resolve actual locators, versions and fixed sequence]
    R --> C[Create plan, targets and steps]
    C --> A[Create one plan approval]
    A --> B{Approved?}
    B -- Pending --> A
    B -- Rejected --> X[Reject plan without creating child changes]
    B -- Approved --> O[Orchestrator chooses the current Apply Group sequence]
    O --> Q[CAS claim step]
    Q --> V[Read current state and check unfinished changes]
    V -- Conflict --> W[waiting_conflict and retry later]
    V -- BR group --> H{Any running Restore in the Shared Pool?}
    H -- Yes --> W
    H -- No --> M[Create one managed ConfigurationChange for this Apply Group]
    V -- Other group --> M
    M --> E[Internal Apply using existing applier]
    E --> K[Existing CheckResult]
    K -- Success --> N[Finish the current sequence and advance]
    K -- Failure --> F[Skip later steps for affected source clusters]
    N --> O
    F --> G[Other targets may continue]
```

| Component | Responsibility |
| --- | --- |
| Premium Upgrade controller/service | Preview、创建、查询、取消、重试和权限检查 |
| Planner | 解析目标 locator、实际组件、固定顺序、审批 owner、apply strategy 和不可变 manifest |
| Premium Upgrade reconciler | 处理父审批状态、选择下一 Step、CAS 抢占、冲突等待和聚合状态 |
| CloudConfig internal managed API | 构建并保存受管单组件或多组件 Change，跳过子审批，内部 Apply/Cancel |
| Existing CloudConfig actor/appliers | 组件参数构建、目标版本可用性校验、下游 Apply、CheckResult 和审计 |
| Approval Service | 只处理 Plan 的一张聚合审批单 |

### Target Selection and Scope Resolution

#### Cluster selection

前端提供两种发现方式，但最终都冻结为显式 cluster ID 列表：

1. 输入一个或多个精确 `cluster_id`；
2. 输入一个精确 `tenant_id`，分页列出该 tenant 下的 Premium logical clusters；用户可以保留已选项，再查询另一个 tenant ID。

Tenant ID 使用自由输入，不使用 tenant name，也不使用全量下拉框。第一版不增加 Environment、Project、region 或 tenant name 筛选。查询条件只用于发现；提交审批的是 Preview 返回的显式目标快照，不是一个会随数据变化的动态查询。

该交互有真实后端基础：现有 `GET /api/v1/devops/clusters` 已支持 `tenant_id`、`cluster_id`、`deploy_type` 和分页，并在 handler 中对 tenant-restricted API key 逐个检查 tenant scope；现有 `GET /api/v1/devops/cloudconfig/cluster-components/search` 也支持互斥的单个 `tenant_id` 或 `cluster_id`，tenant scope 已通过 Management Service 的 `ListClusters(TenantIDs, DeployType=NEXTGEN)` 枚举 logical clusters。具体见 `pkg/controller/cluster/cluster.go`、`pkg/controller/cloudconfig/cloudconfig_controller.go` 和 `pkg/service/cloudconfig/cluster_component.go`。

第一版 cluster picker 应优先复用 `/clusters?tenant_id=...&deploy_type=DEPLOY_TYPE_NEXTGEN` 和 `/clusters?cluster_id=...`；Premium Preview 再 batch resolve Shared Pool locator 和组件。只有现有 cluster response 无法满足 picker 所需字段时才增加专用 discovery endpoint。前端不能用 mock 数据假定专用接口已经存在。

#### Component scope

请求支持：

```json
{
  "scope": {
    "mode": "selected",
    "components": [
      {"cluster_type": "SHARED_POOL", "component": "tikv"},
      {"cluster_type": "LOGICAL_CLUSTER", "component": "tidb"},
      {"cluster_type": "LOGICAL_CLUSTER", "component": "tiproxy"}
    ]
  }
}
```

或：

```json
{
  "scope": {
    "mode": "all_present"
  }
}
```

- `selected`：组件以 `(cluster_type, component)` 唯一标识；所有目标使用相同组件集合。Preview 只返回用户明确选择且目标上存在的组件；同一 TiFlash、PD 或 BR 原子组中被选中的多个组件进入同一个 Step，但不会补齐未选择成员。不能只传无 cluster type 的 `tidb`，因为 Shared Pool 和 Logical Cluster 中可能存在同名组件。
- `all_present`：对每个目标读取实际配置，仅加入当前存在并受 Premium Upgrade 支持的组件。
- 不使用 `ClusterType.AvailableComponents()` 直接代表实际存在组件。
- 不为未选择组件创建 Step，也不读取它们的兼容性规则。

#### Locator normalization

Planner 必须在 Preview 阶段把用户选择解析为真实 CloudConfig locator：

- Premium Cluster 组件使用 `LOGICAL_CLUSTER/<logical_cluster_id>`；
- Resource Pool 组件使用 `SHARED_POOL/<actual_shared_pool_id>`；
- 不允许在受管路径中继续使用“传 Premium Cluster ID，由 `ResourcePoolApplier` 在 Apply 时转换”的旧兼容行为。

相同 Plan 中多个 Premium Cluster 如果解析到同一个 Shared Pool，则相同 `(cluster_type, cluster_id, apply_group)` 的物理步骤只保留一次，并在审批 manifest 中列出其影响的 Premium Cluster。这样同一个物理 locator Apply Group 不会被重复升级。

### Fixed Component Order

第一版在 Premium Upgrade Service 中维护固定顺序、Apply Group 和组件执行元数据。顺序直接来自《Upgrade Canary Internal Cluster from 2510 to 2603 SOP》的第一张表：

```go
type ComponentUpgradePolicy struct {
    Component    string
    ApplyGroup   string
    Priority     int
    GroupOrder   int
    AtomicGroup  bool
    ClusterType  cloudconfig.ClusterType
}
```

| Priority | Apply Group | Components | Behavior |
| --- | --- | --- | --- |
| 20 | `tiflash` | `tiflash_compute`, `tiflash_write` | 原子组，一个 managed ConfigurationChange、一次 Apply |
| 30 | `pd` | `pd`, `pd_tso`, `pd_scheduling` | 原子组，一个 managed ConfigurationChange、一次 Apply |
| 40 | `tikv_worker` | `tikv_worker` | 单组件 Apply |
| 50 | `coprocessor_worker` | `coprocessor_worker` | 单组件 Apply |
| 60 | `tikv` | `tikv` | 单组件 Apply |
| 70 | `tidb_worker` | `tidb_worker` | 单组件 Apply；不实现 SOP 中的一天等待 |
| 80 | `tidb` | `tidb` | 单组件 Apply |
| 90 | `br` | `classic_restore`, `tibr_autobackup`, `tibr_restore`, `tibrgc_t2_dfsgc`, `tibrgc_t3_archive` | 原子组；Apply 前检查运行中 Restore/PITR |
| 100 | `shared_pool_tiproxy` | `tiproxy` | 单组件 Change |
| 100 | `data_migration` | `data_migration` | 单组件 Change |
| 100 | `resource_manager` | `resource_manager` | 单组件 Change |
| 100 | `schema_manager` | `schema_manager` | 单组件 Change |
| 100 | `logical_cluster_tidb` | `tidb` | 单组件 Change |
| 100 | `logical_cluster_tiproxy` | `tiproxy` | 单组件 Change |
| 100 | `dumpling` | `dumpling` | 单组件 Change |

这 7 个 compatibility component 共享最后一个 sequence，但每个 component 都是独立 Apply Group、独立 ConfigurationChange。它们只共享 Upgrade Plan 和父审批，不合并成多组件 Change。`resource_manager` 和 `schema_manager` 因此也随最后阶段执行。TiCDC 不进入第一版：虽然 Config Center 能表示 `TICDC_NG/ticdc`，当前 DevOps 的 TiCDC NG component-level version、Dataflow NG version dispatch 和结果检查尚未形成完整闭环。

Priority 使用间隔值，便于以后插入新组件；Plan 中保存解析后的连续 `sequence`，而不是执行时重新读取 Priority。选择部分阶段时只保留相对顺序。

部分选择示例：

```text
Fixed order: TiFlash -> PD -> TiKV -> TiDB -> BR -> Compatibility
Selected:    PD, TiKV, BR, Compatibility
Resolved:    PD(1) -> TiKV(2) -> BR(3) -> Compatibility(4)
```

原子组只约束执行方式，不改变选择范围。例如只选择 `pd_tso` 时清单和 Change 都只包含 `pd_tso`；同时选择 `pd + pd_tso` 时，两者合并到一个 Step、一个 managed ConfigurationChange 和一次 Apply，未选择的 `pd_scheduling` 不会被加入。尾部 7 个 compatibility component 不是原子组；被选中的 component 分别创建单组件 Change，但都使用同一 sequence。`all_present` 加入目标上实际存在的全部受支持 component。

Planner 按 `(target locator, apply_group)` 聚合组件：

```go
type Step struct {
    Sequence   uint32
    ApplyGroup string
    Components []StepComponent
}
```

Plan 创建后，`sequence`、`apply_group`、组件数组、locator、目标版本和 apply mode 都冻结。`sequence` 表示 Plan 内的 Apply 阶段：同一 sequence 的不同 locator 可以并行，只有当前 sequence 的所有步骤达到终态后才能进入下一 sequence。失败步骤影响的 source clusters 会从后续阶段排除，其他 clusters 可以继续。后续代码顺序变化不得改变已经审批的 Plan。

### Approval Design

#### One plan, one approval

创建 Plan 时：

1. 完成所有目标和组件 Preview；
2. 保存 Plan、Target 和 Step；
3. 根据完整 manifest 创建一张聚合审批单；
4. 把返回的 `approval_id` 写入 `premium_upgrade_plans`；
5. 等待父审批结果，不创建任何子 ConfigurationChange。

审批清单至少包含：

| Field | Description |
| --- | --- |
| Plan | name、reason、applicant |
| Targets | 用户选中的 cluster IDs、tenant IDs，以及解析后的实际 locators |
| Components | 每个 locator/Apply Group 的组件数组及各自 from version、to version |
| Execution order | 每个目标冻结后的 `sequence` |
| Apply strategy | 组件对应的 apply mode/handler |
| Approval route | 根据完整 manifest 解析并冻结的审批节点 |

Target、Step 和 managed ConfigurationChange 不创建审批。审批拒绝或取消时，Plan 进入终态，且此时没有子 Change 需要撤回。

#### Approval routing

```text
premium_upgrade_plans.approval_id = authorization source
managed ConfigurationChange.approval_id = NULL
managed ConfigurationChange.premium_upgrade_step_id -> Step -> Plan -> parent approval
```

第一版不注册静态 Approval Template，也不复用只能表达单一 `cluster_id/tenant_id/high_risk_components` 的现有 `cloudconfig/modify-config-ng` 模板。Premium Upgrade Service 直接调用现有 `ApprovalService.CreateApproval()`：审批路由节点在 Preview/Create 时根据完整 manifest 动态计算；多个 owner node 使用现有 `ApproverNode.Parallel` 表达并行审批，每个节点保留配置的 `ANY`/`ALL`。最终解析出的节点快照写入 `approval_manifest`，dispatcher 直接使用这份快照创建唯一父审批。

审批节点沿用 Approval Service 的 `ApproverNode` 字段，并增加 `components`、`repositories` 和 `default` 作为 Premium Upgrade 路由条件：

```yaml
premiumUpgrade:
  approval:
    nodes:
      - name: Backup approval
        components: [SHARED_POOL/tibr_restore]
        email: premium-backup-owner@example.com
        method: ANY
      - name: Storage approval
        repositories:
          - gcr.io/pingcap-public/dbaas/pd
          - gcr.io/pingcap-public/dbaas/tikv
        userGroup: premium-storage-owners
        method: ANY
      - name: Platform approval
        default: true
        userGroup: premium-platform-owners
        method: ANY
        selfApprove: true
```

Preview 根据目标版本调用现有 CloudConfig `GetDefaultComponent`，取得并冻结目标 `base_image`。路由优先级为 component、目标 Base Repo、default node；没有 `components` 和 `repositories` 的节点也会被视为 default。节点支持 Approval Service 现有的 `email`、`emails`、`userGroup`、`userGroups`、`applicantLeader`、`selfApprove`、`fromParam`、`method`、`skipWhenEmpty` 和 `condition` 字段。`selfApprove: true` 会把申请人加入该节点；在 `method: ANY` 下，申请人或组内任一成员均可完成审批，在 `method: ALL` 下则需要节点内所有审批人完成审批。旧的 `defaultGroup/groups/repositoryGroups/componentGroups` 配置仍向后兼容，但不能与 `nodes` 混用。

审批事件通过唯一的 `premium_upgrade_plans.approval_id` 查找 Plan，只更新一次父状态。CloudConfig watcher 不能让 managed Change 独立处理该审批事件；保持子 Change 的 `approval_id=NULL` 可以直接避免现有 `handleApprovalEventForConfigurationChange()` 按父 approval ID 扫到全部子 Change。

现有 `ApprovalService.CreateApproval()` 没有业务幂等键，因此父审批创建采用持久化 outbox 风格状态，而不是把外部调用伪装成单个数据库事务：

1. Plan、Target、Step 在事务内写入，Plan 状态为 `approval_pending_create`；
2. 唯一的 approval dispatcher 用 CAS 把 Plan 改为 `approval_creating`；
3. 调用 `CreateApproval()`，成功后立即写入 `approval_id` 和 `pending_approval`；
4. 调用失败且明确未创建时退回 `approval_pending_create` 重试；
5. 进程如果在 Approval 已创建、`approval_id` 写回前退出，Plan 进入 `approval_reconcile_required`，禁止自动再次创建，由告警和人工根据 Plan detail/approval Parameters 中的 `plan_id` 关联后补写。

这避免静默创建两张审批单。后续如果 Approval Service 支持 `source_type + source_id` 唯一键，再把第 5 步改成自动幂等重放。

### Managed ConfigurationChange

#### Why it is still created

Step 不直接调用下游服务，而是复用现有 ConfigurationChange 作为执行记录和 adapter，以继续使用：

- `ConfigurationChangeBuilder`；
- 选中组件目标版本可用性校验；
- 未完成 Change 冲突检查；
- NextGen/ResourcePool component-specific applier；
- Apply mode；
- CheckResult；
- 配置历史和审计事件。

#### Creation timing

受管 Change 只能在父审批通过且 Step 即将执行时创建：

```text
plan approved
  -> step claimed
  -> state/conflict check
  -> create one managed ConfigurationChange
  -> apply immediately
```

不能在 Plan 提交审批时预创建所有子 Change，否则等待审批期间这些未完成 Change 会提前占用所有 locator，阻止正常运维操作。

#### Internal API

新增 CloudConfig service 内部接口，不暴露为普通 HTTP API：

```go
type CreateManagedConfigurationChangeParam struct {
    PlanID   uint64
    TargetID uint64
    StepID   uint64
    Change   api.CreateConfigurationChangeParam
}

CreateManagedConfigurationChange(
    ctx context.Context,
    param CreateManagedConfigurationChangeParam,
) (*ccent.ConfigurationChange, error)

ApplyManagedConfigurationChange(
    ctx context.Context,
    stepID uint64,
    changeID uint64,
    mode api.ApplyConfigurationChangeParam,
) error

FailManagedConfigurationChange(
    ctx context.Context,
    stepID uint64,
    changeID uint64,
) error

CancelManagedConfigurationChange(
    ctx context.Context,
    stepID uint64,
    changeID uint64,
) error
```

`CreateManagedConfigurationChange` 复用普通 Create 的 builder、`creatable` 和 `validateCreate`，但保存时直接设置：

```text
phase = wait_for_apply
approval_id = NULL
premium_upgrade_step_id = step.id
```

它不得调用公开的 `CreateConfigurationChange()`，因为普通 Create 会保存 `phase=created`，随后 watcher 调用 `SendApproval()` 创建子审批。

Premium scheduler 和 `ApplyManagedConfigurationChange` 在调用现有 actor `Apply()` 前共同保证：

- Change 的 `premium_upgrade_step_id` 与 Step 完全匹配，并通过 Step 关联 Target/Plan；
- Premium scheduler 只扫描已 approved/running 的父 Plan；
- Step 通过带父 Plan running 条件的 CAS 进入 `preparing`；
- scheduler 只选择 Plan 当前允许执行的最小 sequence；
- CloudConfig 内部方法校验 Step ID、Change phase 为 `wait_for_apply` 且没有子 `approval_id`。

公开 `ApplyConfigurationChange` 和 `CancelConfigurationChange` 必须拒绝 `premium_upgrade_step_id` 非空的 Change。只有 Premium Upgrade Service 能使用内部方法操作它们。

### Conflict Detection

第一版不增加 `premium_upgrade_resource_locks`，直接复用 ConfigurationChange Create 中已有的 scope-aware locator 冲突检查：

```text
ClusterConfigurationChangeableForChange(newChange)
  = 创建时执行现有 scope-aware 校验；TiProxy-only 与 non-TiProxy-only 可并存
```

创建受管 Change 前：

1. 使用规范化后的真实 locator 创建 managed Change；
2. managed Create 和普通 Create 共用 builder、`creatable`、`validateCreate`；
3. `creatable` 调用现有 `ClusterConfigurationChangeableForChange`；
4. 如果返回 `FAILED_PRECONDITION`，不创建 Change，把 Step 设为 `waiting_conflict`；
5. reconciler 稍后重试；创建成功后立即 Apply。

这使 Premium Upgrade 与普通人工 ConfigurationChange 使用同一个冲突事实来源，避免两套锁状态不一致。

BR Apply Group 额外执行运行时 Restore guard。该检查不增加持久化锁：

1. 根据 Target 的 tenant IDs 分页列出 Premium Logical Cluster；
2. 过滤出绑定当前 Shared Pool 的全部 cluster，并与 `source_cluster_ids` 合并；
3. 调用 Management Service `ListClusterCustomStatus` 批量查询 `RESTORE`、Serverless Restore 变体和 `PITR`；
4. 任一未过期状态存在时，Step 进入 `waiting_conflict` 并记录 cluster IDs；
5. 状态接口调用失败时 fail closed，同样进入 `waiting_conflict`，避免无法确认时继续升级；
6. 下次重试重新读取实时状态，全部消失后才创建 BR managed ConfigurationChange。

检查范围必须是 Shared Pool 下所有 Logical Cluster，而不只是审批单选择的 cluster；未选择的 cluster 也可能正在使用相同 BR 组件。SOP 中的 “One day idle” 不进入第一版状态机。

Create 前还会按解析后的 `(cluster_type, cluster_id)` 查询非终态 Premium Plan；发现重叠 locator 时拒绝新 Plan，避免两个已经审批的 Plan 长期互相等待。该查询是业务层保护，不是新的资源锁表；真正执行前仍以 ConfigurationChange 的未完成记录作为最终冲突事实来源。

现有“检查未完成 Change，再插入新 Change”没有数据库行锁或活动 locator 唯一约束，理论上存在并发竞态。这是 CloudConfig 的既有问题。第一版接受与现有 API 相同的行为；如果需要彻底解决，应在通用 CloudConfig 层增加原子 locator reservation，而不是恢复 Premium 专属资源锁表。

### State Model

#### Plan status

```text
draft
  -> approval_pending_create
  -> approval_creating
  -> pending_approval
  -> approved
  -> running
  -> completed | partial_failed | failed
  -> cancelling -> cancelled

approval_creating -> approval_reconcile_required
pending_approval -> rejected | cancelled
approved | running -> cancelling -> cancelled
```

- `completed`：所有非 noop Step 成功；
- `partial_failed`：至少一个目标完成、至少一个目标失败；
- `failed`：没有用户选择的 cluster 完成且至少一个 cluster 失败；
- `rejected`：父审批拒绝，未创建子 Change。
- `approval_reconcile_required`：Approval 可能已经创建但 ID 尚未写回，必须先恢复唯一关联，不能重新创建审批。

#### Target status

```text
pending
  -> running
  -> waiting_conflict
  -> running
  -> completed | failed | cancelled
```

Target 表示一个规范化后的实际 locator。它的 `waiting_conflict` 表示当前组件阶段暂时不能在该 locator 创建 ConfigurationChange，不表示 Plan 需要重新审批。

#### Step status

```text
pending
  -> preparing
  -> waiting_conflict -> preparing
  -> applying
  -> succeeded | failed

pending | waiting_conflict -> cancelled
pending -> skipped
```

- `skipped`：该步骤无需执行，或者同一目标的前序步骤失败；必须记录 `status_reason`。
- `preparing`：通过 CAS 抢占，但尚未开始外部 apply。
- `applying`：已创建并 Apply managed ConfigurationChange，等待其终态。

### Reconciliation Algorithm

每个 reconciliation tick：

```text
load latest plan
if plan approval is pending:
    query parent approval and return
if plan approval is creating/reconcile_required:
    run approval recovery rules and return
if rejected/cancelled:
    close plan and return
if not approved/running:
    return

active_sequence = the smallest sequence with non-terminal eligible steps

for each step in active_sequence up to server concurrency limit:
    refresh running managed ConfigurationChange, if any
    if current step is applying:
        if change applied:
            mark step succeeded
        else if managed change failed or the step deadline is exceeded:
            mark step failed
            mark affected source cluster IDs failed
            mark their later steps skipped
        continue

    CAS step to preparing using id + status + version
    if CAS lost:
        continue

    read current target/components state
    if every component in the apply group is already at approved desired state:
        mark step succeeded as noop
        continue

    if apply group is BR:
        list every logical cluster attached to the shared pool
        if any active Restore/PITR exists or status cannot be verified:
            mark step waiting_conflict and set next_retry_at
            continue

    run existing unfinished ConfigurationChange check
    if conflict:
        mark step waiting_conflict and set next_retry_at
        continue

    create one managed ConfigurationChange containing every component in the apply group
    link change id to step
    call internal managed Apply
    mark step applying

when every step in active_sequence is terminal:
    advance to the next sequence

aggregate execution targets, source clusters and plan status
```

固定失败行为：一个实际 locator 的 Step 失败后，其 `source_cluster_ids` 对应的用户选择 clusters 进入失败状态，后续阶段中只服务这些 clusters 的 Step 设为 `skipped`；不受影响的 clusters 继续。Shared Pool Step 失败会影响所有引用该 Shared Pool 的选中 clusters。第一版没有可配置 Failure Policy。

### Data Model

简化版新增三张表，并修改一张现有表：

| Change | Table | Purpose |
| --- | --- | --- |
| Add | `premium_upgrade_plans` | 一次用户操作和唯一父审批 |
| Add | `premium_upgrade_targets` | 审批内冻结的目标及实际 locator |
| Add | `premium_upgrade_steps` | Apply Group 顺序、多组件意图、执行状态和 managed Change 关联 |
| Alter | `cloudconfig_configuration_changes` | 标记并关联受 Plan 管理的子 Change |

明确不新增：

```text
premium_upgrade_step_dependencies
premium_upgrade_resource_locks
```

#### `premium_upgrade_plans`

| Field | Purpose |
| --- | --- |
| `id`, `name`, `reason` | 标识和用户意图 |
| `applicant`, `operator_entity` | 权限与审计 |
| `desired_version` | 所有选中组件的目标版本 |
| `scope_mode` | `selected` 或 `all_present` |
| `requested_cluster_ids` | 用户选择的 cluster ID 快照 |
| `approval_manifest`, `manifest_hash` | 完整不可变授权清单 |
| `approval_id`, `approval_status` | 唯一父审批；创建前允许为空 |
| `status`, `last_error` | 聚合状态 |
| `max_parallel_targets` | 服务端限制后的目标并发数，不作为用户策略 |
| `component_order_revision` | 创建时使用的固定顺序版本 |
| `idempotency_key` | 防止重复创建 Plan |
| `cancel_requested_at`, `started_at`, `finished_at` | 生命周期时间 |
| base timestamps and `version` | 乐观并发控制 |

推荐索引：

- `(applicant, idempotency_key)` 唯一；
- `approval_id` unique；MySQL 允许多个 `NULL`，创建成功后每个非空 ID 只能关联一个 Plan；
- `status`；
- `created_at`。

#### `premium_upgrade_targets`

Target 表示审批清单里的规范化执行目标。一个用户选择的 Premium Cluster 可能解析出一个 Logical Cluster target 和一个 Shared Pool target；相同 Shared Pool 在同一 Plan 中只保存一次。

| Field | Purpose |
| --- | --- |
| `id`, `plan_id` | 标识和父 Plan |
| `cluster_type`, `cluster_id` | 真实 CloudConfig locator |
| `source_cluster_ids` | 该执行目标影响的用户所选 Premium Cluster IDs |
| `cluster_name`, `tenant_ids` | 冻结展示和审计信息 |
| `shared_pool_id`, `logical_region`, `cloud_provider` | 路由信息 |
| `status`, `last_error` | 聚合结果 |
| lifecycle timestamps | 执行时间 |

唯一索引：

```text
(plan_id, cluster_type, cluster_id)
```

#### `premium_upgrade_steps`

| Field | Purpose |
| --- | --- |
| `id`, `plan_id`, `target_id` | 标识和父级 |
| `sequence` | Plan 内冻结的 Apply 阶段顺序；同一阶段在不同 locator 上使用相同值 |
| `apply_group` | 稳定的 Apply 单元名称，例如 `tiflash`、`pd`、`br` |
| `components` | JSON 数组；每项包含 `component`、`from_version`、`to_version`，是不可变多组件升级意图 |
| `apply_strategy`, `apply_mode` | 整个 Apply Group 的执行方法 |
| `configuration_change_id` | 即时创建的 managed Change；执行前为空 |
| `status`, `status_reason` | 编排状态 |
| `attempt_count`, `next_retry_at` | 冲突等待和重试 |
| `apply_deadline_at` | 服务端根据固定组件策略计算的本次执行截止时间 |
| `last_error_code`, `last_error_message` | 诊断信息 |
| `version` | CAS 抢占版本 |
| lifecycle timestamps | 执行时间 |

推荐唯一索引：

```text
(target_id, apply_group)
configuration_change_id UNIQUE
```

本项目生产数据库为 MySQL；nullable unique index 允许多个 `NULL`，因此执行前为空的 Step 可以共存，不使用 MySQL 不支持的 partial-index 语法。

推荐查询索引：

```text
(status, next_retry_at)
(plan_id, sequence, status)
(plan_id, target_id, sequence)
```

#### Changes to `cloudconfig_configuration_changes`

增加 nullable 字段：

```text
premium_upgrade_step_id UNIQUE
```

`premium_upgrade_step_id` 为空表示普通 unmanaged Change，继续使用原审批和公开 Apply/Cancel 行为；非空表示 managed Change：

- 创建时直接进入 `wait_for_apply`；
- `approval_id` 必须为 `NULL`；父审批通过 Step 反向关联；
- 不调用 `SendApproval()` / `ContinueIfApproved()`；
- 公开 Apply/Cancel 返回 permission/precondition error；
- 只有 Premium Upgrade Service 可通过内部接口执行。

另外给 `ConfigurationChangePhase` 增加 `failed`，并将其加入 finished values。该 phase 只表示一次 Change 执行已经确定失败，不等于自动回滚。普通 Change 可以在后续逐步复用该终态，第一版至少由 managed CheckResult 的硬错误/超时关闭受管 Change，确保现有冲突查询能够释放 locator。

### API

以下均为本设计需要新增的 API，不是当前已经存在的接口。现有 cluster-component search 只作为实现数据源之一。

#### Cluster discovery

优先复用：

```http
GET /api/v1/devops/clusters?tenant_id=123&deploy_type=DEPLOY_TYPE_NEXTGEN&page=1&per_page=50
GET /api/v1/devops/clusters?cluster_id=101234567890
```

约束：

- `tenant_id` 与 `cluster_id` 互斥，均为精确 ID；
- tenant 查询必须分页，只返回 Premium/NextGen logical cluster summary；
- picker 至少需要 `cluster_id`、tenant ID 和 cluster name；Shared Pool ID、logical region 由 Preview 解析，不要求 discovery response 暴露；
- 不接受 tenant name 或 Environment；
- 用户跨 tenant 选择时由前端保留已选 cluster IDs，再发起下一次 tenant 查询；最终由 Preview batch resolve 全量复核；
- 如果实现验证发现现有 `/clusters` response 或查询契约不足，再增加前述 `/premium-upgrades/clusters` adapter，但不应重复实现 Management Service 查询逻辑。

#### Preview

```http
POST /api/v1/devops/premiumUpgradePlans/preview
```

示例请求：

```json
{
  "cluster_ids": ["101234567890", "101234567891"],
  "scope": {
    "mode": "selected",
    "components": [
      {"cluster_type": "SHARED_POOL", "component": "tikv"},
      {"cluster_type": "LOGICAL_CLUSTER", "component": "tidb"},
      {"cluster_type": "LOGICAL_CLUSTER", "component": "tiproxy"}
    ]
  },
  "desired_version": "v8.5.3",
  "reason": "Production rollout"
}
```

Preview 返回：

- 用户选择 cluster 快照；
- 实际 `LOGICAL_CLUSTER` / `SHARED_POOL` locator；
- 每个选中组件的 from/to version；
- 固定相对顺序；
- apply strategy；
- approval route；
- 不可用组件或版本错误；
- `plan_hash`。

不接受以下字段：

```text
dependency_policy
auto_include_dependencies
failure_policy
start_policy
custom_component_order
```

#### Create and query

| Method | Path | Purpose |
| --- | --- | --- |
| `GET` | `/api/v1/devops/premiumUpgradePlans/componentPolicies` | 返回后端当前固定的 `(cluster_type, component, priority, apply_strategy, revision)`，供前端渲染真实组件选择项 |
| `POST` | `/api/v1/devops/premiumUpgradePlans` | 使用 `expected_manifest_hash` 创建 Plan/Targets/Steps，并异步创建一张父审批 |
| `GET` | `/api/v1/devops/premiumUpgradePlans` | 查询 Plan 列表 |
| `GET` | `/api/v1/devops/premiumUpgradePlans/{plan_id}` | 查询审批、目标、固定顺序、Step 和 managed Change 状态 |
| `POST` | `/api/v1/devops/premiumUpgradePlans/{plan_id}/cancel` | 停止新 Step；审批未完成时取消父审批 |
| `POST` | `/api/v1/devops/premiumUpgradePlans/{plan_id}/steps/{step_id}/retry` | 仅重试尚未创建 ConfigurationChange 的失败 Step，不创建新审批 |

Create 使用 `Idempotency-Key` 和 `expected_manifest_hash`。相同 key 和相同 intent 返回原 Plan；相同 key 但 intent 不同返回 conflict。

本设计统一采用资源风格 path。具体 controller 注册时必须放在带认证的 `/api/v1/devops` group，并在 Swagger 中生成以上契约。

### Failure Retry Cancellation and Rollback

#### Conflict waiting

其他 ConfigurationChange 占用 locator 时：

```text
step -> waiting_conflict
```

不创建 managed Change，不占用新的审批，也不把目标标记失败。后续按 `next_retry_at` 重试。

#### Apply failure

- managed Change 创建前的临时读取错误和临时 5xx：增加 `attempt_count`，稍后重新读取后重试；
- managed Change 已经创建后，不通过 retry endpoint 再次调用 Apply；reconciler 只恢复现有 Change、读取 observed state 并运行 CheckResult；
- 明确不支持、目标不存在或非重试 Apply 错误：Step `failed`；
- Step 失败后，同一 Target 的更高 sequence Step 全部 `skipped`；
- 其他 Target 继续；
- Plan 最终聚合为 `partial_failed` 或 `failed`。

Retry endpoint 只允许 `configuration_change_id IS NULL` 的失败 Step；它不改变已审批的 Apply Group、组件数组、locator、target version、sequence 或 apply mode。已经创建 Change 的 Step 如果失败，不原地复用或重放；需要新 Plan 重新审批。任何需要修改 intent 的恢复同样必须重新 Preview、创建新 Plan 并审批。

#### Cancellation

- `pending_approval`：取消唯一父审批，Plan `cancelled`；
- `approved/running`：停止抢占新 Step，把 pending/waiting Step 标记 cancelled；
- 已创建但尚未 applying 的 managed Change 通过内部 Cancel 关闭；
- applying Change 默认等待得到已知结果，除非具体 applier 提供安全取消；
- completed Step 不回滚。

第一版不自动回滚。反向版本变更必须是新的 Upgrade Plan。

### Consistency and Idempotency

#### Plan creation

使用一个数据库事务：

1. 校验 `expected_plan_hash`；
2. 插入 Plan，并设 `status=approval_pending_create`；
3. 插入规范化 Targets；
4. 插入已排序 Steps；
5. 提交后由 approval dispatcher CAS 抢占并创建父审批；
6. 如果创建结果不确定，进入 `approval_reconcile_required`，不得自动再建。

此阶段不创建 ConfigurationChange。

#### Step claiming

```sql
UPDATE premium_upgrade_steps
SET status = 'preparing',
    version = version + 1,
    attempt_count = attempt_count + 1
WHERE id = ?
  AND status IN ('pending', 'waiting_conflict')
  AND version = ?;
```

只有影响行数为 1 的 reconciler 获得该 Step。Plan/Target 的状态聚合也使用最新数据库状态，不能依赖进程内缓存。

#### External apply recovery

外部 Apply 不是数据库事务。进程可能在下游接受请求后、Step 写回 `applying` 前退出。因此重试前必须：

1. 根据 Step 查找已关联 managed Change；
2. 读取 ConfigurationChange phase；
3. 读取 Config Center/下游 observed state；
4. 已达到目标则标记成功；
5. 未达到则继续检查现有 Change，不能因为进程重启再次调用 Apply。

Change 创建与 Step 关联使用 `premium_upgrade_step_id UNIQUE` 实现幂等：reconciler 必须先按 Step ID 查询现有 Change，再尝试创建；唯一键冲突后重新加载现有 Change，不能生成第二个执行记录。

#### Managed failure closure

当前 `actorApplying.CheckResult()` 只区分“已完成”和“继续等待”，ConfigurationChange 也没有失败 phase。为保证 Plan 能进入失败终态并释放冲突，需要增加 managed result classifier：

- `CheckResult()` 返回 applied：Change `applied`、Step `succeeded`；
- retriable error 且未超过 `apply_deadline_at`：保持 `applying`；
- 明确不可重试错误，或超过 deadline：内部 `FailManagedConfigurationChange()` 把 Change 置为 `failed` 并写 `closed_at`；错误码和错误详情只写入 Step 的 `last_error_code/last_error_message`，Step 置为 `failed`；
- `failed` 加入 `ConfigurationChangePhaseFinishedValues`，因此不再被 unfinished Change 查询视为冲突。

错误分类和 deadline 是服务端固定策略，不暴露为 Failure Policy。没有这一改动，原有 actor 会让异常 managed Change 永久停在 `applying`，后续步骤和正常配置变更都无法继续。

#### Existing conflict race

第一版沿用 CloudConfig 当前的“查询未结束 Change，再插入”行为。若后续需要数据库级严格互斥，应给所有 ConfigurationChange 提供通用的原子 locator reservation；不要新增仅 Premium 使用的锁表。

### Code Changes

1. 新增 `entity/premiumupgrade`、`models/premiumupgrade` 和 `pkg/service/premiumupgrade/{api,repo,service}`。
2. 新增 `premium_upgrade_plans`、`premium_upgrade_targets`、`premium_upgrade_steps` migration；对已经执行过单组件 Step 迁移的环境追加 forward migration：把旧 `component/from_version/to_version` 回填成单成员 `apply_group/components[]`，移除旧唯一索引和旧列，再建立 `(target_id, apply_group)` 唯一索引。同一 target 可以在相同 sequence 保存多个单组件 Apply Group；已有 Plan 的执行意图不合并、不改变顺序。
3. 给 `cloudconfig_configuration_changes` 增加 nullable `premium_upgrade_step_id` 唯一索引；给 phase 增加 `failed` finished value。失败详情只保存在 Upgrade Step。
4. 在 Premium Upgrade Service 中增加固定 `ComponentUpgradePolicy`，包含 priority、cluster type、apply strategy 和 apply mode；增加启动校验以拒绝重复 component/priority。
5. 增加 Preview planner，解析真实 Shared Pool locator、实际组件、目标版本 Base Repo、版本可用性、相对 sequence、去重 Shared Pool 步骤和 plan hash。
6. 参考 `pkg/service/k8supgradeplan` 的 Plan 级审批事件模式，增加聚合审批 builder 和 approval dispatcher；审批组按 component override、目标 Base Repo、default group 解析，节点来自持久化 manifest，不信任前端拼装文本。
7. 重构 CloudConfig Create，使 builder、`creatable`、`validateCreate` 可被普通 Create 和 managed Create 复用。
8. 新增内部 `CreateManagedConfigurationChange`、`ApplyManagedConfigurationChange`、`FailManagedConfigurationChange` 和 `CancelManagedConfigurationChange`。
9. managed Create 直接保存 `wait_for_apply`，不进入普通 `created -> SendApproval` watcher。
10. 在公开 Apply/Cancel 中拒绝 `premium_upgrade_step_id` 非空的 Change。
11. 增加 Premium reconciler，使用 Step CAS、ConfigurationChange 事件和 cron fallback 推进状态。
12. managed Create 直接复用 `ClusterConfigurationChangeableForChange`；所有 Resource Pool managed Change 必须使用真实 Shared Pool ID。
13. 注册 DI、HTTP controller、event subscription 和 migration models。
14. 使用 `mockgen` 生成 repo/service 可复用 mocks；新测试优先使用 Ginkgo/Gomega。

### Observability and Audit

日志和审计字段：

```text
plan_id
target_id
step_id
configuration_change_id
approval_id
cluster_type
cluster_id
apply_group
sequence
attempt
```

推荐指标：

- `premium_upgrade_plans{status}`；
- `premium_upgrade_targets{status,cluster_type}`；
- `premium_upgrade_steps{status,apply_group}`；
- `premium_upgrade_approval_dispatch{status}` 和 reconcile-required count；
- approval wait、conflict wait、apply 和 total duration；
- CAS claim conflict count；
- apply attempts/failures；
- unmanaged ConfigurationChange conflict count；
- plans with partial failure。

Plan detail 是父审批的 detail link，完整展示所有目标和步骤。每个 Step 链接到现有 ConfigurationChange history。审计同时记录原 applicant 和实际执行的内部 reconciler identity。

### Rollout Plan

1. 落地三张新表和 CloudConfig nullable `premium_upgrade_step_id`，不启用 endpoint。
2. 增加 `failed` finished phase，只先用于 managed Change，并验证普通 Change 查询兼容。
3. 增加固定顺序 registry、Preview、单审批 manifest 和 approval dispatcher，在 dev 验证目标解析、审批内容和不确定结果恢复。
4. 增加 managed Change 内部创建/Apply/Fail 和公开接口保护。
5. 先启用一个目标、两个低风险组件，`max_parallel_targets=1`。
6. 验证部分组件的相对顺序以及未选择组件不被检查、不被创建 Step。
7. 验证一个目标的 `all_present`。
8. 验证多个独立目标共享一张审批，并逐步增加目标并发。
9. 验证多个 Premium Cluster 解析到同一 Shared Pool 时步骤去重和真实 locator 冲突检查。
10. 生产初期限制 target/step 数量；确认下游幂等性和 runbook 后再提高限制。

Feature gate 分别控制 Preview、Create 和 Dispatch。关闭 Dispatch 时保留查询能力，不修改已审批 manifest。

## Test Design

### Functional Tests

- `selected` 只生成用户选中的组件；TiFlash、PD、BR 原子组只聚合其中被选中的组件，不补齐组内组件或其他阶段。
- `all_present` 从目标实际 Configuration 解析组件，不使用 AvailableComponents 直接展开。
- 任一目标缺少 selected component 时 Preview 返回明确错误。
- 只检查选中组件的目标版本是否可用，不读取未选择组件版本。
- 固定 registry 无重复 component，Apply Group 的 priority/atomic 属性和组内顺序一致。
- 只选择 `pd_tso` 时只生成包含 `pd_tso` 的 Step；选择 `pd + pd_tso` 时两者进入同一个 Step，且不加入 `pd_scheduling`。
- 选择 `tiflash_compute/pd/tikv/tibr_restore` 时输出 `TiFlash -> PD -> TiKV -> BR`，每个原子组只生成一个 Step。
- Plan 保存连续 sequence；修改代码中的 priority 不影响历史 Plan。
- 多个 logical clusters 指向相同 Shared Pool 时，相同资源池步骤只生成一次。
- 每个物理 Resource Pool Step 使用 `SHARED_POOL/<actual_shared_pool_id>`。
- Preview/Create plan hash 一致；源状态变化后拒绝使用过期 hash 创建。
- 一个 Plan 只保留一个 approval ID；Target/Step 不创建审批。
- approval dispatcher 的 CAS 只允许一个 worker 发起创建；创建结果不确定时进入 `approval_reconcile_required`，不会自动再建。
- 恢复 `approval_reconcile_required` 时能根据 Approval Parameters 中的 `plan_id` 补回唯一关联。
- 父审批 pending/rejected 时不创建 managed ConfigurationChange。
- managed Create 复用 builder、creatable 和 selected component version validation。
- 一个多组件 Step 创建一个包含完整 `components[]` 的 managed ConfigurationChange。
- managed Change 创建后直接是 `wait_for_apply`，`approval_id` 为 null，并能经 Step 关联父 approval ID。
- managed Change 永不调用 `SendApproval()` 或 `ContinueIfApproved()`。
- 父审批结束事件不会被 CloudConfig handler 匹配到 managed Change。
- 公开 Apply/Cancel 拒绝 managed Change；内部接口校验 Plan/Target/Step 归属。
- 同 locator 存在任意未结束普通或 managed Change 时，严格 precheck 让 Step 进入 `waiting_conflict` 且不创建子 Change。
- 冲突结束后 Step 能重新 CAS 抢占并继续。
- BR Step 会查询 Shared Pool 下所有 Logical Cluster 的 Restore/PITR custom status；存在未过期状态或查询失败时进入 `waiting_conflict`。
- 同一 Step 并发创建 managed Change 时，`premium_upgrade_step_id UNIQUE` 保证最终只关联一个 Change。
- 同一 Step 被多个 reconciler 处理时只有一个 CAS 成功。
- 当前 sequence 所有可执行 Step 到达终态后才进入下一 sequence。
- Shared Pool 等实际 locator Step 失败后，按重叠的 `source_cluster_ids` 跳过关联目标的后续 Step，其他 clusters 继续。
- ConfigurationChange 已达到目标状态时重试识别为成功，不重复 Apply。
- CheckResult 硬错误或超过 apply deadline 时，managed Change 进入 `failed` finished phase，释放 locator 并让 Step 失败。
- Repository converter、唯一索引和状态聚合使用 Ginkgo/Gomega 表驱动测试。

### Scenario Tests

1. 一个集群同时选择 `tiflash_compute + tiflash_write`：验证一张审批、一个双组件 Step、一个 managed Change 和一次 Apply。
2. 一个集群只选择 `pd_tso`：验证 Preview、审批和 managed Change 都只包含 `pd_tso`，执行一次 Apply。
3. 多个集群选择相同组件：验证只有一张父审批，同一组件 sequence 的不同 locator 受控并行，完成后才进入下一 sequence。
4. 多个集群选择 `all_present`：验证每个目标根据实际存在组件生成不同清单，但同属一张审批。
5. 两个 Premium Cluster 共享一个 Shared Pool：验证 Resource Pool Step 去重并使用真实 Shared Pool locator。
6. 父审批拒绝：验证没有 managed ConfigurationChange 被创建，所有目标不执行。
7. 审批等待期间创建普通 ConfigurationChange：父审批通过后相应 Step 进入 `waiting_conflict`；普通 Change 结束后继续。
8. 中间组件 Apply 失败：受该 locator 影响的 source clusters 后续 Step skipped，其他 clusters 继续并最终形成 partial failure。
9. DevOps 在下游接受 Apply 后重启：恢复时读取 managed Change 和 observed state，不创建新审批、不重复 rollout。
10. 在 pending、waiting_conflict 和 applying 阶段分别取消，验证相应取消语义。
11. 禁用 event delivery：cron reconciler 仍能从数据库恢复 Plan。
12. Approval 创建成功但写回前模拟进程退出：Plan 停在 reconcile-required，不产生第二张审批。
13. managed Change 长时间 applying：达到 deadline 后进入 failed，后续普通 ConfigurationChange 不再被它阻塞。
14. BR 执行前，同一 Shared Pool 中未被审批单选择的 Logical Cluster 正在 Restore：Step 等待；Restore 结束后自动继续。
15. Restore status API 暂时失败：BR Step fail closed 并重试，不创建 managed Change。
16. 已运行旧版单组件 Step migration 的数据库升级后，现有行被回填成单成员 Apply Group，新 Step 可写入且旧 Plan 顺序不变。

### Compatibility Tests

- 现有普通单组件和多组件 ConfigurationChange Preview/Create/Approve/Apply 流程保持不变。
- `premium_upgrade_step_id` 为 null 的历史记录继续走原 actor 状态机。
- 普通 ConfigurationChange 的 `approval_id`、approval event、Apply/Cancel 语义不变。
- 原 `created` Change 仍由 watcher 创建自己的审批。
- managed Change 不进入 `created`，不会改变普通 watcher 查询语义。
- 现有 ConfigurationChange detail 和 audit 能显示 managed Change，并链接到父 Plan。
- Config Center component-level version/image/template 请求保持兼容。
- `NextgenApplier.CheckResult` 和 `ResourcePoolApplier.CheckResult` 同时覆盖单组件和多组件输入。
- ResourcePoolApplier 的旧 ID 转换行为继续服务 unmanaged Change；managed Change 不使用该兼容入口。
- Tenant/API key 必须对 Plan 中每一个用户选择目标有权限。
- 新表和 nullable 字段支持滚动升级；旧 binary 忽略新表，但回滚前必须关闭 Premium Dispatch。
- 第一版不支持 downgrade；非升级版本请求返回明确错误。

## Impacts & Risks

### Impacts

- 用户可以用一个审批单授权完整的多集群、多组件升级清单。
- 组件执行顺序统一由服务端决定，页面只展示解析结果，不让用户理解或配置依赖策略。
- 子 ConfigurationChange 继续提供现有配置历史、applier 和 CheckResult 能力，但不增加审批量。
- 等待父审批期间不创建子 Change，因此不会提前阻塞集群的其他正常变更。
- Plan、Target、Step 三张表足以提供组件级进度、失败定位和重试。
- 删除 dependency/lock 表降低第一版 schema、调度器和运维复杂度。

### Risks and Mitigations

| Risk | Mitigation |
| --- | --- |
| 固定组件顺序错误 | 上线前由组件 owner 确认；registry 启动校验；顺序 revision 冻结进 Plan |
| 未检查未选择组件导致版本组合不兼容 | 明确作为第一版限制；审批展示只保证选中组件；未来有权威兼容矩阵后另行设计 |
| Managed Change 意外创建子审批 | 创建时直接设为 `wait_for_apply`；单测保证不进入 watcher 的 created/approving 路径 |
| 父审批事件误触发全部子 Change | managed Change 的 `approval_id=NULL`；通过 Step 反向关联父审批 |
| Approval 成功但 ID 写回失败导致重复审批 | dispatcher CAS；不确定结果进入 reconcile-required 并告警，人工恢复唯一关联 |
| 用户手工 Apply managed Change 绕过顺序 | 公开 Apply/Cancel 检查 `premium_upgrade_step_id` 并拒绝；只允许内部接口 |
| 审批等待期间环境变化 | 子 Change 即时创建并读取当前状态；与已审批 from state 不一致时停止该 Step，需要新 Plan |
| 普通 Change 与 Plan 冲突 | 统一使用现有 unfinished Change 检查；Step 等待而不是覆盖 |
| managed Change 永久停留 applying | 增加 failed finished phase、固定 apply deadline 和错误分类，关闭 Change 后释放 locator |
| 两个并发请求同时通过现有冲突检查 | 第一版与现有 CloudConfig 风险一致；后续在通用 CloudConfig 层增加原子 locator reservation |
| 同一 Shared Pool 被多个输入集群重复升级 | Preview 解析真实 Shared Pool ID，并按实际 locator/Apply Group 去重 |
| BR 升级与 Restore 并发 | Apply 前批量检查 Shared Pool 下所有 Logical Cluster 的 Restore/PITR custom status；无法确认时 fail closed |
| 原子组中只有部分组件被用户选中 | Preview 和审批 manifest 只保留所选组件；同组中被选中的组件合并为一个 Change，未选择组件不加入 |
| 多副本重复调度同一 Step | 使用 status + version CAS；外部重试前读取 managed Change 和 observed state |
| 中途失败造成部分版本升级 | 停止该 Target 后续 Step，保留准确状态并告警；不自动回滚 |
| 聚合审批内容过大 | 列表展示摘要，Plan detail 展示完整 manifest；限制 Plan target/step 数量 |
| 静态审批模板不能表达动态多目标、多 owner | 不注册新模板；用持久化 manifest 构建动态 Parallel/ANY 节点快照并创建一张父审批 |

## Unresolved Questions

1. 每个组件对应的准确 `apply_strategy` 和 `ApplyMode` 是什么？
2. selected component 的 from version 在审批后发生变化时，是整张 Plan 停止，还是只停止对应 Target？本设计默认只停止对应 Target。
3. Premium 聚合审批的 owner 路由规则是什么：按 tenant、组件、实际 locator，还是固定运维组？不同 owner 处于同一并行层还是串行层？
4. 一个 Shared Pool Step 影响多个用户选择 Cluster 时，Plan detail 中的展示和 Target 状态聚合应采用怎样的 UI 文案？
5. 初始的 Plan target 数、Step 数、目标并发数、冲突等待时间、apply deadline 和最大重试次数是多少？
6. Config Center、Management Service 和 Cluster Service NG 对相同多组件 desired-state 请求是否都保证幂等？
7. 何时需要把现有 ConfigurationChange 冲突检查升级为数据库级原子 locator reservation？
