1. 页面概览
1.1 是什么
「部署与漂移」页(对应前端源码 action/web/src/views/DeploymentPage.vue,页面标题「Apollo 部署与漂移」)是 LightApollo 控制面的部署执行与状态一致性演示台。它把「把期望状态下发到 Agent」到「检测并收敛实际状态漂移」的整条闭环搬到了页面上:你可以在本页为某个已激活的期望状态在指定 Agent 上发起一次部署;在部署列表中观察部署逐批推进;由于页面是纯 Pull 演示(部署实际由 Spoke Agent 周期性拉取执行),本页以「推进」弹窗来模拟 Agent 上报组件状态,从而驱动后端的就绪门控与状态机向前走;当实际状态与期望声明出现偏差时,本页还提供漂移检测与收敛能力,把漂移作为结构化 diff 落库并展示,必要时触发自动调和。
在整个 Apollo 产品的流程中,本页处于「期望状态 → 部署执行 → 运行收敛」的中段:期望状态的创建、激活与审批在「期望状态」管理页(部署总览等相邻页面)完成,渠道/策略(含 drift_policy)在「制品与渠道」与 GitOps 页面配置,Spoke Agent 在「Spoke Agent」页登记并拉取;本页则是唯一能主动「发起一次部署」并把 Agent 组件状态喂回后端的控制点,漂移检测的输入(期望声明 vs 实际状态 JSON)也在本页手工构造。输入是用户选择的期望状态 + Agent 名称 + 部署策略;输出是部署记录(六种状态)与漂移事件(带路径级 diff),收敛后由 Agent 重新拉取使实际状态回到期望。
典型使用链路(演示视角的完整闭环):① 先在相邻页面激活一条期望状态并确认组件声明完整(depends_on 依赖成 DAG);② 在本页「发起部署」选该期望状态 + Agent 名(如 app-001)+ 策略,观察列表出现 deploying 记录且首批组件已放行;③ 点击该行进详情,逐批用「推进」把当前批次组件上报 ready,观察 next_batch 放行下一批,直到全部组件 ready、部署变 synced;④ 制造并检测漂移:在「漂移检测」填入与期望不一致的 actual_state JSON 点「检测漂移」,事件列表出现带路径级 diff 的事件、关联部署变 drifting;⑤ 若该期望状态关联的同步目标 drift_policy=auto_fix,后端会自动把部署置回 pending 触发重调和,否则在事件上点「收敛(reconcile)」并让 Spoke Agent 重新拉取,最终状态回到 synced。
1.2 核心价值/能力表
| 能力 | 说明 | 对应页面操作 |
|---|---|---|
| 发起部署 | 选择 active 期望状态 + Agent + 策略,创建一条部署记录并放行首批组件 | 「发起部署」表单 |
| 部署列表 | 集中查看部署的 ID、期望状态、版本、Agent、策略、状态与开始时间 | 页中部部署列表卡片 |
| 组件详情 | 展开单条部署查看每个组件的类型/状态/就绪详情/错误 | 点击部署行展开 |
| 模拟推进 | 以 ready/failed/deploying 三种上报驱动就绪门控,放行下一批或触发回滚 | 「推进」按钮 + 弹窗 |
| 手动 Sync | 对 failed/drifting/degraded 部署解除退避并打 sync 指令戳,强制 Agent 下一轮重调和 | 「手动 Sync」按钮 |
| 漂移检测 | 手工给 actual_state JSON,与期望声明做字段级 diff,落库并标记关联部署 | 「漂移检测(drift check)」表单 |
| 漂移收敛 | 指示 Agent 重新拉取应用,直到 diff 清空、部署恢复 synced | 漂移事件「收敛(reconcile)」 |
| 审计留痕 | 发起/推进/失败/同步/漂移检测/收敛均写审计日志 | 全部写操作 |
1.3 一句话总结
本页把「期望状态下发 → Agent 逐批就绪 → 漂移检测与收敛」的部署一致性闭环演示出来,并给出「发起部署、推进上报、手动 Sync、检测与收敛漂移」四类操作入口。
2. 访问入口
2.1 路由与菜单
| 项 | 值 |
|---|---|
| 路由 path | /apollo/deployments |
| 路由 name | ApolloDeployments |
| meta.title | Apollo 部署与漂移 |
| 侧边栏入口 | ApolloLayout 侧边栏「部署与漂移」,位于「部署总览」之后、「环境管理」之前 |
| 前端源码 | action/web/src/views/DeploymentPage.vue |
| API 客户端 | action/web/src/api/apolloClient.js |
| 路由注册 | action/web/src/router/index.js,/apollo 子路由 children 下,component: DeploymentPage,挂 ApolloLayout |
相邻页(侧边栏顺序):部署总览 overview.html(前)、环境管理 environments.html(后)、Spoke Agent agents.html(Agent 拉取/上报的另一面)。
2.2 认证与权限
- 路由
requiresAuth: true,meta 无白名单:未登录访问会被前端路由守卫拦到/apollo/login(登录态为独立的apollo_token,向后兼容回退读取旧aip_token)。 - 后端各端点走 protected 组鉴权,按权限点细粒度控制:部署读取类(列表/详情/组件)要求
PermDeploymentRead;部署写操作(发起/推进/手动 Sync)要求PermDeploymentExecute;漂移事件读取要求PermDriftRead;漂移检测与收敛要求PermDriftReconcile。 - 前端在权限点缺失时(403)由 apolloClient 响应拦截器统一
alert('无权限执行该操作'),页面 catch 兜底再弹错误条;401 时拦截器清除 token 并跳转登录页。
2.3 端口与 API 前缀
- Apollo 后端端口 18082;Vite 把
/apollo-api代理到 18082。 - 客户端 baseURL
/apollo-api/v1,请求自动附带Authorization: Bearer <apollo_token>;响应统一 envelope{code,message,data,request_id},拦截器在code===0时把response.data解包为业务数据,页面const { data } = await apiClient.get(...)直接拿业务数据。
3. 界面布局
┌───────────────────────────────────────────────────────────┐
│ Apollo 部署与漂移 [发起部署/收起表单] │
│ [操作结果提示条(v-if alert.message,可「关闭」)] │
├───────────────────────────────────────────────────────────┤
│ ┌────────────────────────────────────────────────────────┐ │
│ │ 发起部署(v-if showCreate,默认收起) │ │
│ │ 期望状态(须active)* | Agent 名称 * | 部署策略 │ │
│ │ [发起部署] [取消] │ │
│ └────────────────────────────────────────────────────────┘ │
├───────────────────────────────────────────────────────────┤
│ ┌ 部署列表(loading/空态/表格 三态卡片)─────────────────────┐ │
│ │ ID 期望状态 版本 Agent 策略 状态 开始时间 操作 │ │
│ │ (行点击展开;展开行内:组件推进状态表 + 推进进度/就绪门控) │ │
│ │ 操作列按状态:推进 / 收敛漂移 / 手动 Sync │ │
│ └────────────────────────────────────────────────────────┘ │
├───────────────────────────────────────────────────────────┤
│ ┌ 推进部署 #N(弹窗 modal)────────────────────────────────┐ │
│ │ 选择组件 | 上报状态 | 就绪快照(readiness JSON,选填) │ │
│ │ [推进(POST advance)] [取消] · [推进结果 code block] │ │
│ └────────────────────────────────────────────────────────┘ │
├───────────────────────────────────────────────────────────┤
│ ┌ 漂移检测(drift check)卡片──────────────────────────────┐ │
│ │ 期望状态 * | Agent 名称 * | 实际状态(actual_state JSON) │ │
│ │ [检测漂移] │ │
│ └────────────────────────────────────────────────────────┘ │
│ ┌ 漂移事件卡片 ────────────────────────────────────────────┐ │
│ │ 事件 #id · 期望状态 #id · Agent · has_drift · 时间 │ │
│ │ [收敛(reconcile)] │ │
│ │ diff JSON(code block) │ │
│ └────────────────────────────────────────────────────────┘ │
└───────────────────────────────────────────────────────────┘
| 板块 | 职责 |
|---|---|
| 页头 + 提示条 | 标题、发起部署表单开关按钮、全局操作结果提示(成功/失败/信息) |
| 发起部署卡片 | 收集期望状态/Agent/策略,创建部署(默认收起,点「发起部署」展开) |
| 部署列表卡片 | 六种状态展示、点击行展开组件级详情、按状态提供推进/收敛漂移/手动 Sync |
| 推进弹窗 | 模拟 Agent 上报单个组件状态(ready/failed/deploying + 可选 readiness),驱动就绪门控 |
| 漂移检测卡片 | 手工提供实际状态 JSON,触发字段级 diff 检测 |
| 漂移事件卡片 | 展示历史漂移事件(含 diff),对单条事件执行收敛 |
4. 交互元素
4.1 页头与操作结果提示条
| 控件 | 位置 | 含义 | 必填/默认/可用条件 | 操作效果 | 触发后端调用 | 边界与细节 |
|---|---|---|---|---|---|---|
| 按钮「发起部署」/「收起表单」 | 页头右侧 | 切换发起部署表单显隐 | 标签随状态切换:收起时显示「发起部署」,展开时显示「收起表单」 | 点击反转 showCreate,表单在卡片中展开/收起 | 无 | 纯前端显隐,不影响数据 |
| 提示条「关闭」 | 顶部提示条右侧 | 关闭当前操作结果提示 | 有 alert.message 才显示 | alert.message='' 清空提示 | 无 | 提示为单槽,新操作覆盖旧提示 |
提示条形态:<div class="alert" :class="alert.type">,类型有 alert-info(默认)、alert-success、alert-error。发起成功/未检测到漂移等成功场景用 success;失败、检测到漂移、收敛中这类场景用 error。每条关键路径文案在下文逐条列出(按钮文字、提示文案与源码逐字一致)。
4.2 发起部署表单(卡片「发起部署」)
打开方式:点页头「发起部署」按钮。表单字段表:
| 字段 | 类型 | 必填 | 校验/默认 | 保存逻辑 |
|---|---|---|---|---|
| 期望状态(须 active)* | 下拉 v-model.number=startForm.desired_state_id | 是 | 仅列 activeDesiredStates(desiredStates.filter(ds => ds.status==='active'));首项为禁用「请选择」(value null);选项文案 #{{id}} {{name}}(v{{version}});HTML required | 随请求体 desired_state_id 提交(number 保证为数字) |
| Agent 名称 * | 文本输入 | 是 | placeholder「如 app-001」;HTML required | 随请求体 agent_name 提交 |
| 部署策略 | 下拉 | 否 | 首项 value '' 文案「默认(default)」,其余为 policies 列表(value=策略名);空则回退 'default' | 随请求体 policy_name 提交(空串时前端补 'default') |
提交流程(提交按钮文字「发起部署」,busy 时变为「发起中...」并禁用;旁边「取消」按钮纯关闭表单不清数据):
| 控件 | 位置 | 含义 | 必填/默认/可用条件 | 操作效果 | 触发后端调用 | 边界与细节 |
|---|---|---|---|---|---|---|
| 提交按钮「发起部署」 | 表单底部 | 提交发起部署 | busy 时禁用并显示「发起中...」;期望状态/Agent 为空时 HTML required 拦提交,JS 层再校验兜底 | 成功提示 部署已发起 id=${data.id}(success),收起表单、清空三个字段并刷新部署列表;失败提示 发起部署失败:<错误详情>(error) | POST /deployments/start,body {desired_state_id, agent_name, policy_name} | 发起前校验源码文案为「期望状态与 Agent 名称必填」;后端校验不通过返回 400/404/409(详见 7.1/7.2) |
发起部署成功意味着:后端创建一条 pending→deploying 的部署记录 + 每组件一条 pending 的组件状态,并立即放行首批(无依赖层);记录 started_at;监控侧按组件声明探针登记健康检查(旁路,失败不阻断)。
4.3 部署列表卡片
加载三态:loading 时显示「加载中...」;deployments.length===0 时显示「暂无部署记录,请点击"发起部署"。」;有数据时渲染表格。列头依次为 ID / 期望状态 / 版本 / Agent / 策略 / 状态 / 开始时间 / 操作,单元格渲染:#{{dep.desired_state_id}}、{{dep.bundle_version}}、{{dep.agent_name}}、{{dep.policy_name || 'default'}}、状态徽标、formatTime(dep.started_at || dep.created_at)(时间去掉 T 截取前 19 位,空则 -)。
状态徽标六种(class status-<status>):pending(灰)、deploying(蓝)、synced(绿)、drifting(橙)、degraded(紫)、failed(红)。语义:pending 等待 Agent 拉取或调和;deploying 进行中(已放行批次组件未全部就绪);synced 已收敛终态;drifting 漂移检测标记;degraded 降级(健康劣化/拉取失败重试聚合);failed 失败终态。
| 控件 | 位置 | 含义 | 必填/默认/可用条件 | 操作效果 | 触发后端调用 | 边界与细节 |
|---|---|---|---|---|---|---|
| 部署行 | 列表 tbody | 单条部署 | 始终可点 | 行点击切换展开(同 id 再点折叠);展开异步并发拉详情与组件 | 展开:GET /deployments/:id + GET /deployments/:id/components | 加载失败提示「加载部署详情失败:...」,不阻断列表 |
| 「推进」 | 行操作列 | 打开推进弹窗 | 仅 status==='pending'|'deploying' 显示 | 打开推进弹窗(见 4.5) | 无(弹窗提交才调后端) | 若该行未展开,会先自动展开加载组件 |
| 「收敛漂移」 | 行操作列 | 对 drifting 部署收敛 | 仅 status==='drifting' 显示 | 点击抛 ReferenceError,无任何效果(前端缺陷,见 8 章) | 无 | 该按钮绑定的 openReconcile 函数在 <script setup> 中未定义 |
| 「手动 Sync」 | 行操作列 | 解除退避并强制重调和 | 仅 status==='failed'|'drifting'|'degraded' 显示;busy 时禁用 | 见 4.6 | POST /deployments/:id/sync | 高影响写操作,有原生 confirm 二次确认 |
busy 是页面级提交锁:任一写操作进行中,提交类按钮全部置灰(发起部署、推进、手动 Sync、检测漂移、收敛),避免并发提交制造混乱状态;操作完成(含失败)后统一复位。
4.4 部署详情展开区
点击部署行展开一个跨列 <tr>(class detail-row),内容包括:
- detail-meta 行:
id={{id}} · 期望状态 #{{desired_state_id}} · Agent {{agent_name}} · 状态 <徽标>;若deployment.error非空,追加红字错误:{{error}}。 - 组件推进状态表(子表,列头 组件/类型/状态/就绪详情/错误):每组件渲染
<strong>name</strong>、kind、组件状态徽标(comp-pending/comp-deploying/comp-ready/comp-failed)、就绪详情(cs.readiness非空显示<code>JSON.stringify(cs.readiness)</code>,否则-)、错误列(cs.error || '-')。 - 推进进度行(
.dag-legend,组件非空才显示):推进进度:{{readyCount}}/{{componentStates.length}} · 就绪门控:仅当依赖全部 ready 才放行下一批,其中readyCount为状态ready的组件数(computed)。
| 控件 | 位置 | 含义 | 必填/默认/可用条件 | 操作效果 | 触发后端调用 | 边界与细节 |
|---|---|---|---|---|---|---|
| 组件状态表 | 展开区 | 组件级推进视图 | 展开后异步加载 | 展示各组件类型/状态/readiness/error | GET /deployments/:id/components | 组件状态按部署唯一索引 (deployment_id,name) upsert,幂等 |
| 行(再次点击) | 列表 | 收起详情 | 已展开的同 id 行 | 折叠并清空 detailDeployment/componentStates | 无 | 「推进」成功后会再次调用 toggleDetail(id),同 id 会直接折叠——每推进一轮详情会自动收起,需重新点击行查看进度 |
4.5 「推进」按钮与推进弹窗(Agent 上报模拟)
行内「推进」(openAdvance)打开弹窗,标题 推进部署 #{{deployment.id}}(右上「关闭」可关)。弹窗字段与控件:
| 控件 | 位置 | 含义 | 必填/默认/可用条件 | 操作效果 | 触发后端调用 | 边界与细节 |
|---|---|---|---|---|---|---|
| 下拉「选择组件」 | 弹窗表单 | 本次上报哪个组件 | 必填;选项为该部署的 componentStates(按 name);首项禁用「请选择组件」 | 记录 advanceForm.name | 无 | 行未展开时组件异步加载,弹窗刚打开时选项可能为空,等详情加载完成再选 |
| 下拉「上报状态」 | 弹窗表单 | 该组件的模拟上报状态 | 默认 ready;三选项:ready(就绪,放行下一批)、failed(失败,触发自动回滚)、deploying(部署中) | 记录 advanceForm.status | 无 | 语义由后端编排消化 |
| 文本域「就绪快照(readiness JSON,选填)」 | 弹窗表单 | 附带 readiness JSON | 选填;placeholder {"ready":true};内容非空时前端 JSON.parse | 解析成功后并入该组件 readiness | 无 | 非法 JSON 抛异常进 catch,提示 推进失败:Unexpected token ...,请求不发出 |
| 按钮「推进(POST advance)」 | 弹窗底部 | 提交组件上报 | advanceForm.name 为空或 busy 时禁用;busy 文案「推进中...」 | 成功把返回结果 JSON 渲染到弹窗下方「推进结果」code block,并提示 推进完成:状态 ${status}(${ready_count}/${total_count});若有 next_batch 追加 ,放行下一批:${names.join(', ')};status==='failed' 时提示为 error 类型否则 success;随后刷新部署列表并(重新)展开该部署详情 | POST /deployments/:id/advance,body {deployment_id, agent_name, components:[{name, status, readiness?}]} | 失败提示「推进失败:...」;选择组件为空时提示「请选择组件」 |
| 按钮「取消」 | 弹窗底部 | 关闭弹窗 | 始终可用 | 关闭弹窗(不清结果) | 无 | 点遮罩(@click.self)也可关闭 |
前提提示:若当前部署行尚未展开,点「推进」会先异步加载组件,弹窗打开瞬间「选择组件」下拉可能是空的,需等详情加载完再选组件;已展开的行则可直接用上次加载的组件列表。
「推进」是把「就绪门控 + 超时 + 失败自动回滚」驱动起来的关键动作:上报 ready 的组件若声明了 readiness 探针,监控侧会执行探针,未通过则组件保持 deploying 并写错误(readiness 探针未通过(声明探针已由 monitoring 执行并落库));批次内全部 ready 才放行下一批(响应 next_batch);组件 deploying 超过策略就绪超时(默认 60s)被判 readiness 就绪超时(60s) 而置 failed;任一组件 failed → 见 5.4 失败与自动回滚。
4.6 「手动 Sync」按钮
列表行对 failed/drifting/degraded 状态显示「手动 Sync」。点击先弹原生 confirm:对部署 #{{id}}(Agent {{agent_name}},状态 {{status}})执行手动 Sync?\n将清除退避/失败状态并指示 Agent 强制重调和一轮。 取消则中止;确认后调 POST /deployments/:id/sync(body 无内容)。
| 控件 | 位置 | 含义 | 必填/默认/可用条件 | 操作效果 | 触发后端调用 | 边界与细节 |
|---|---|---|---|---|---|---|
| 「手动 Sync」 | 行操作列 | 解除退避/失败并强制重调和 | 仅 failed/drifting/degraded;busy 禁用 | 成功提示按响应分两种:带 note 时 Sync 成功(${note});否则 Sync 成功:${from_status||原状态} → ${status},已打 sync 指令戳;随后刷新部署列表 | POST /deployments/:id/sync | 失败提示「手动 Sync 失败:...」;synced 终态被后端拒绝(409,文案「部署已收敛(synced),无需手动 sync」),但按钮不向 synced 显示 |
4.7 漂移检测表单(drift check)
卡片标题 漂移检测(drift check)。表单字段:
| 字段 | 类型 | 必填 | 校验/默认 | 保存逻辑 |
|---|---|---|---|---|
| 期望状态 * | 下拉 v-model.number=driftCheck.desired_state_id | 是 | 注意与发起部署不同:此下拉列全部 desiredStates(不做 active 过滤),选项文案 #{{id}} {{name}}({{status}});首项禁用「请选择」;required | 随 desired_state_id 提交 |
| Agent 名称 * | 文本输入 | 是 | placeholder「如 drift-agent」;required | 随 agent_name 提交 |
| 实际状态(actual_state JSON) | 文本域 rows=4 | 否 | placeholder {"app":"demo-app","version":"9.9.9"};非空时前端 JSON.parse 为对象 | 有值则随 actual_state 提交(对象),留空不传(后端按空对象判定) |
提交按钮「检测漂移」(busy 时「检测中...」并禁用)。流程:空校验拦截(源码文案「期望状态与 Agent 名称必填」)→ POST /drift/check(body 含 desired_state_id/agent_name,actual_state 按解析结果附加)。结果按 has_drift 分两路提示:
- 检测到漂移(
has_drift=true):检测到漂移(${diff.length} 项 diff),请查看漂移事件列表进行收敛(error 型),随后刷新部署列表与漂移事件; - 未检测到漂移:
未检测到漂移(实际状态与期望一致)(success 型)。
非法 JSON 在前端 parse 即抛异常,走 catch 提示「漂移检测失败:Unexpected token ...」且不会发出请求。检测成功但后端报错时提示「漂移检测失败:<后端 message>」。
后端副作用:actual_state(缺省空对象)与期望声明(declaration JSON,含 policy.ignore_paths 忽略规则)做字段级 diff;有 diff 则写一条 drift_events(无 diff 不落库)并把该期望状态+Agent 关联的最近部署标记 drifting;随后按 drift_policy 评估 auto_fix(详见 5.4)。留空提交时因期望声明通常非空而实际为空对象,会产出大量 add 型 diff——基本必报漂移,这是演示语义而非缺陷,收敛要用漂移事件区的「收敛(reconcile)」配合 Agent 重拉。
4.8 漂移事件列表与「收敛(reconcile)」
卡片标题 漂移事件;无事件时显示「暂无漂移事件。」。每条 drift-item:
- 头部:
事件 #{{evt.id}}、期望状态 #{{evt.desired_state_id}}、Agent {{evt.agent_name || '-'}}、橙色徽标has_drift(后端只落 has_drift=true 的事件)、时间formatTime(evt.created_at)、右侧按钮「收敛(reconcile)」; - 正文 code block 渲染
JSON.stringify(evt.diff, null, 2)(diff 数组全文)。
| 控件 | 位置 | 含义 | 必填/默认/可用条件 | 操作效果 | 触发后端调用 | 边界与细节 |
|---|---|---|---|---|---|---|
| 「收敛(reconcile)」 | 漂移事件条目 | 对该事件收敛 | busy 时禁用 | 弹 confirm:对漂移事件 #{{id}}(期望状态 #{{ds_id}},Agent {{agent_name||'-'}})执行收敛?;确认后请求 | POST /drift/:id/reconcile,body 为空对象 {} | 结果按 has_drift 提示:有漂移 收敛中(reconciling=true,已指示 Agent 重新拉取应用,剩余 ${diff.length} 项漂移)(error 型);无漂移 收敛完成(部署已恢复 synced)(success 型);随后刷新列表与事件 |
后端收敛语义:收敛以空 actual_state 重新执行一次漂移判定——只要期望声明非空就必然仍有 diff,于是返回 reconciling=true(若有 gitops 关联且 drift_policy 允许,还可能再次触发 auto_fix 把部署置 pending),真正的收敛靠 Spoke Agent 下一轮拉取重应用实现;只有「收敛」请求能携带精确 actual_state 时才会判定已收敛并把关联部署置回 synced(当前页面固定传空体,页面上的「收敛完成」提示需 Agent 已实际收敛后才能出现)。收敛对象取该事件记录的 desired_state_id + agent_name 对:即使收敛的是较早的事件,也会针对这一对当前的实际/期望状态重新判定,与事件新旧无关。失败提示「收敛失败:...」;事件 id 不存在后端 404。
5. 后端关联
5.1 API 客户端
action/web/src/api/apolloClient.js:axios 实例 baseURL /apollo-api/v1、timeout 30000ms、请求拦截器对所有请求附加 Authorization: Bearer <token>(token 取 apollo_token,无则回退 aip_token)。响应拦截器:HTTP 2xx 且 code===0 时把 response.data 替换为业务数据本身(页面 const { data } = ... 直接得到业务对象/数组);HTTP 2xx 但 code!==0 拒绝;4xx/5xx 时提取 body.message || body.error 并把 message 别名写入 error.response.data.error 后 reject——401 清 token 并跳 /apollo/login(登录页自身 401 不跳,避免死循环),403 alert('无权限执行该操作');blob/arraybuffer 下载与无数字 code 的裸响应原样放行。页面各 catch 统一用 err.response?.data?.error || err.message 展示后端错误。
5.2 端点表
| 方法 | 路径 | 权限点 | 请求体/Query | 页面触发点 |
|---|---|---|---|---|
| POST | /deployments/start | deployment:execute | {desired_state_id, agent_name, policy_name} | 发起部署 |
| GET | /deployments | deployment:read | ?agent=(可选过滤) | 加载部署列表 |
| GET | /deployments/:id | deployment:read | - | 展开详情 |
| GET | /deployments/:id/components | deployment:read | - | 展开详情组件表 |
| POST | /deployments/:id/advance | deployment:execute | {deployment_id, agent_name, components:[{name,status,readiness?}]}(AgentReport) | 推进弹窗提交 |
| POST | /deployments/:id/sync | deployment:execute | 空 | 手动 Sync |
| GET | /desired-states | desired_state:read | - | 两个下拉的数据源(发起表单过滤 active) |
| GET | /policies | policy:read | - | 策略下拉数据源 |
| GET | /drift/events | drift:read | ?desired_state_id=&agent_name= | 漂移事件列表 |
| POST | /drift/check | drift:reconcile | {desired_state_id, agent_name, actual_state?} | 检测漂移 |
| POST | /drift/:id/reconcile | drift:reconcile | {}(可选 actual_state) | 收敛(reconcile) |
5.3 响应结构示例
统一 envelope 成功形态(拦截器已解包,业务代码只见 data):
POST /deployments/start 响应(原始 envelope 与解包后)
{ "code": 0, "message": "ok", "data": { "id": 23 }, "request_id": "req_b63f..." }
解包后业务代码 data.id === 23,页面提示 部署已发起 id=23。字段说明:id 新部署记录主键。创建类接口返回 HTTP 201。
部署记录(GET /deployments 列表项与 GET /deployments/:id 本体,字段即 hub.DeploymentRecord):
{
"id": 23,
"desired_state_id": 7,
"bundle_version": 3,
"agent_name": "app-001",
"policy_name": "default",
"status": "deploying",
"error": "",
"started_at": "2026-09-07T10:20:30+08:00",
"finished_at": null,
"auto_fix_count": 0,
"last_auto_fix_at": null,
"sync_requested_at": null,
"created_at": "2026-09-07T10:20:30+08:00",
"updated_at": "2026-09-07T10:20:31+08:00"
}
字段含义:id 主键;desired_state_id 所部署期望状态;bundle_version 期望状态当时的 bundle 版本;agent_name 目标 Agent;policy_name 使用的策略名(可空,空串时列表显示 default);status 六态之一 pending/deploying/synced/failed/drifting/degraded;error 失败原因(空串无);started_at 发起时间(列表「开始时间」优先取它);finished_at 终态完成时间(synced/failed 置位);auto_fix_count/last_auto_fix_at 漂移自动调和轮数与最近触发时间(防循环判据,页面不直接展示但参与 sync/auto_fix 判定);sync_requested_at 手动 sync 指令戳(非空表示待消费「立即重调和」指令,pull 消费后清空)。
组件状态(GET /deployments/:id/components 数组项,字段即 hub.ComponentState):
{
"id": 301,
"deployment_id": 23,
"name": "web",
"kind": "binary",
"status": "ready",
"readiness": { "ready": true },
"digest": "sha256:abc123",
"started_at": "2026-09-07T10:20:32+08:00",
"error": "",
"created_at": "2026-09-07T10:20:30+08:00",
"updated_at": "2026-09-07T10:20:40+08:00"
}
字段含义:name 组件名;kind 组件类型(默认 binary);status 组件状态 pending/deploying/ready/failed(对应徽标 comp-*);readiness 就绪详情 JSON(就绪快照/探针结果,页面 JSON.stringify 展示);digest 组件摘要;started_at 组件进入 deploying 的时间(就绪超时判据);error 组件错误(探针未通过/就绪超时/上报 failed 会写入)。
推进结果(POST /deployments/:id/advance,即 hub.AdvanceResult):
上报 web 为 ready、放行下一批时的响应示例
{
"deployment_id": 23,
"status": "deploying",
"next_batch": ["db"],
"ready_count": 1,
"total_count": 3,
"auto_rollback": null
}
字段含义:status 推进后部署状态(synced 表示已全部收敛终态;failed 表示本次推进触发失败);next_batch 本次放行的下一批组件名数组(空数组时提示串不追加「放行下一批」);ready_count/total_count 当前 ready 组件数/总组件数(页面提示 推进完成:状态 X(1/3));auto_rollback 非空(回滚草稿期望状态 id)表示本次失败已触发自动回滚。全部批次完成时 status=synced。
手动 Sync(POST /deployments/:id/sync)三种响应分支:
成功清退避分支 / 幂等分支 / 终态拒绝分支
{ "id": 23, "from_status": "drifting", "status": "pending", "sync_requested_at": "2026-09-07T11:00:00+08:00" }
成功分支:from_status 原状态(drifting/failed/degraded 之一);status 恒为 pending(清退避置回待拉取);sync_requested_at 打上的指令戳——Spoke 下轮拉取即使 digest 未变也强制重调和。页面提示 Sync 成功:drifting → pending,已打 sync 指令戳。
{ "id": 23, "from_status": "deploying", "status": "deploying", "sync_requested_at": null, "note": "部署已在进行中,无需重调和" }
幂等分支(部署已在 deploying):无退避可解除,不落新指令戳,sync_requested_at 返回 null,页面提示 Sync 成功(部署已在进行中,无需重调和)。
{ "code": "DEPLOYMENT_SYNC_INVALID", "error": "部署已收敛(synced),无需手动 sync" }
终态拒绝分支:HTTP 409;synced 已收敛不允许再 sync(列表行不向 synced 显示按钮,此分支多为列表未刷新的陈旧态触发)。
漂移检测/收敛结果(POST /drift/check、POST /drift/:id/reconcile,即 hub.DriftResult):
检测到漂移(含 auto_fix)示例
{
"has_drift": true,
"diff": [
{ "op": "replace", "path": "/version", "expected": "9.9.9", "actual": "8.8.8" },
{ "op": "add", "path": "/components/1", "expected": { "name": "db", "kind": "binary", "version": "2.0" } }
],
"reconciling": false,
"auto_fix_triggered": true,
"auto_fix_reason": "drift_policy=auto_fix 允许自动调和:部署已置 pending,Agent 下轮 poll 将重新应用期望状态"
}
字段含义:has_drift 忽略规则过滤后是否存在 diff;diff JSON Patch(RFC 6902)路径级 diff 数组,op ∈ add(期望有实际无,附 expected)/remove(实际有期望无)/replace(值不同,附 expected+actual),path 形如 /a/b/0/c(数组用下标),支持 */** 通配 ignore;reconciling check 端点恒 false,reconcile 且有漂移时为 true;auto_fix_triggered 本次是否触发自动调和;auto_fix_reason 触发/未触发原因文案。未检测到漂移时 {"has_drift":false,"reconciling":false,"diff":[]}(无 diff 不写事件、不动部署状态,页面提示「未检测到漂移」)。
漂移事件(GET /drift/events 数组项,即 hub.DriftEvent):
{
"id": 5,
"desired_state_id": 7,
"agent_name": "drift-agent",
"diff": [
{ "op": "replace", "path": "/version", "expected": "9.9.9", "actual": "8.8.8" }
],
"has_drift": true,
"created_at": "2026-09-07T10:30:00+08:00"
}
字段含义:id 事件主键(「收敛(reconcile)」按钮路径参数);desired_state_id/agent_name 检测对象;diff 结构化 diff JSON(页面 code block 全文展示);has_drift 恒为 true(无漂移不落库);created_at 检测时间。列表按 id 降序、未分页。
5.4 关键机制
部署锁与发起校验(StartDeployment)。发起部署时后端依次校验:agent 名非空(空 → 400 DEPLOYMENT_INVALID「agent 标识不能为空」);期望状态存在(无 → 404);期望状态必须已激活(status !== active → 400,文案含当前状态);期望状态必须声明了组件(无组件 → 400「期望状态无组件,无可部署内容」);组件依赖须构成合法 DAG(computeLevels Kahn 拓扑分层失败则拒绝)。随后在事务内做同一 Agent 部署锁:若该 Agent 已存在 status ∈ {pending, deploying} 的进行中部署则 409 DEPLOYMENT_CONFLICT(文案含进行中部署 id 与状态),保证同一 Agent 串行执行。事务内创建 DeploymentRecord(初始 pending、started_at=now)→ 立即置 deploying → 为每个组件建一条 ComponentState(pending)。事务提交后调用 gateAndRelease 放行首批(无依赖层)组件(置 deploying),并对监控侧回调登记带 deployment_id 的探针健康检查(G18 打点,旁路失败不阻断)。页面上「发起部署」成功即完成了上述整串动作。
DAG 分层与就绪门控(Advance)。期望状态组件靠 depends_on 构成 DAG,computeLevels 按依赖关系分出层级;就绪门控的规则是批次内组件全部 ready 才放行下一批(页面 legend 文案「就绪门控:仅当依赖全部 ready 才放行下一批」)。每次 advance 上报对应当前批次内一个组件:上报 ready → 组件置 ready;但若该组件声明了 readiness 探针且监控侧执行未通过,则组件保持 deploying 并写错误(readiness 探针未通过(声明探针已由 monitoring 执行并落库)/readiness 探针执行失败: ...)——就绪门控不放行,等待策略超时兜底。上报 deploying 仅刷新就绪快照(readiness)。上报对未知组件名直接忽略;组件状态按 (deployment_id,name) 唯一索引幂等 upsert(重复推进不产生重复记录)。当某层全部 ready 后 gateAndRelease 计算并放行下一层(响应 next_batch 返回放行的组件名);全部层完成 → 部署置 synced 终态(finished_at 记录),审计 DEPLOYMENT_SYNC。
就绪超时与失败自动回滚。组件进入 deploying 会记 started_at;advance 请求内会做超时检查——超过策略 readiness_timeout_sec(默认策略 60s)的 deploying 组件被置 failed 并写错误 readiness 就绪超时(60s)。另有一个后台超时收敛任务(server/deployment_timeout.go)周期性对滞留 deploying 的部署做兜底超时判定(探测 Agent 探活失败时本轮保守跳过)。任一组件的失败上报/超时都会把部署引向失败处理:部署置 failed + 记录 error、审计 DEPLOYMENT_FAILED(含 auto_rollback 标志);若策略 rules.auto_rollback 为 true(默认)则调用期望状态服务的 Rollback 生成一条回滚期望状态草稿(rollback_of 指向上一稳定版、bundle 版本号 +1,AdvanceResult.auto_rollback 返回草稿 id),让 Agent 按旧版本收敛回来。对已终态(synced/failed)的部署再提交 advance 不会重复推进,直接回当前结果(幂等)。
手动 Sync 与指令戳消费(F2 即时收敛载体)。handleDeploymentSync 先读部署:synced → 409 拒绝(DEPLOYMENT_SYNC_INVALID);否则调 RequestSync——状态门控只对 failed/drifting/degraded/pending 生效,把它们置回 pending、清除 error/finished_at(置 NULL)、打上 sync_requested_at 指令戳;对 deploying(已在收敛中)不落新指令、返回幂等 200(note「部署已在进行中,无需重调和」)。指令戳的消费在 Agent pull 端:Spoke 下轮拉取时若该 Agent 最近部署是 pending 且打了 sync_requested_at(或 auto_fix_count>0),pull 响应携带 reconcile_required=true,并原子执行 ConsumeSyncRequest(WHERE status=pending 单条 UPDATE,pending→deploying 并清指令戳)保证一次性语义——即使期望状态 digest 未变也强制 Agent 重新应用一轮,使实际状态收敛回期望。
漂移检测:结构化 diff 与落库(G3)。ComputeDrift 递归对比期望声明(declaration JSON)与实际状态(map/数组任意嵌套):期望有而实际无 → add(附 expected;期望值 null 视为"应删除"不产 add);实际有而期望无 → remove;均存在但值不同 → replace;数值类型统一归一为 float64 比较避免 int/float 误判。路径为 JSON Pointer 风格(/a/b/0/c),并支持期望声明 policy.ignore_paths 忽略规则(精确路径、单段 *、深通配 **,命中路径及其整棵子树不产 diff)。CheckAndRecord 只在 hasDrift=true 时写一条 drift_events(无 diff 不落库)并把该期望状态+Agent 关联的最近部署标记 drifting(若存在且非 drifting),审计按结果写 DRIFT_DETECTED(diff 计数)/DRIFT_CLEAN。
漂移自动调和(auto_fix,阶段 E 批4b)与防循环。漂移被检测到后,server 层注入的 apolloDriftAutoFix(通过最近 50 条 sync_history 的 OperationSummary 反查该期望状态关联的 gitops 同步目标)评估 drift_policy:仅 auto_fix 允许自动调和(alert 等策略仅记录)。通过后 hub 层再做两道防循环闸:单条部署 auto_fix_count 已达上限(默认 MaxRounds=3)不触发;距 last_auto_fix_at 小于防抖窗口(默认 5 分钟)不触发。放行则 RecordAutoFix 原子自增(WHERE auto_fix_count < max 条件更新防并发超限),把部署置 pending——Agent 下轮 pull 走与手动 sync 相同的 reconcile_required 消费路径重新应用。未触发原因会写进 auto_fix_reason 并随 DRIFT_AUTO_FIX 审计留痕。
收敛(Reconcile)语义。收敛本质是用(可选)actual_state 再跑一次 CheckAndRecord:仍有漂移 → 返回 reconciling=true(指示 Spoke 重拉应用,页面提示「收敛中」);无漂移 → 把关联部署置回 synced、清漂移(页面提示「收敛完成」)。本页收敛请求固定传空体 {},对非空期望声明等价于"当前无任何实际状态",必然再报 diff——页面上的「收敛完成」只有当 Agent 真正把实际状态拉回期望之后、再以一致实际状态收敛时才会出现,实际收敛执行者是 Spoke Agent 而非本页。
6. 权限与安全
- 认证分层:页面整体 requiresAuth;读取类走
deployment:read/drift:read(+下拉数据源的desired_state:read/policy:read),写操作全部走deployment:execute/drift:reconcile。developer/operator 具备执行与收敛类权限,viewer 仅读,平台/项目管理员为通配。 - 写操作防护:手动 Sync 与漂移收敛均为高影响写操作,前端用浏览器原生
confirm二次确认后才发请求;advance 写组件状态但幂等(唯一索引 upsert)。 - 幂等与终态保护:advance/sync 对终态部署幂等或 409 拒绝,不会重复改写;auto_fix 有轮数/防抖上限,不会无限重部署。
- 审计留痕:每次部署发起/推进/失败/手动 Sync、漂移检测与收敛均写审计日志(DEPLOYMENT_START / DEPLOYMENT_ADVANCE / DEPLOYMENT_SYNC / DEPLOYMENT_FAILED / DRIFT_DETECTED / DRIFT_CLEAN / DRIFT_AUTO_FIX / DRIFT_RECONCILE),可在审计日志页追溯。
- 错误提示口径:前端把所有 4xx/5xx 的服务端 message 直接透出到提示条,可能暴露内部校验细节;403 由拦截器统一 alert。
7. 常见问题与排错
- 发起部署提示「期望状态与 Agent 名称必填」:原因是两个必填项之一为空(未选期望状态/Agent 名为空)。处理:期望状态下拉只列 active 项,先在部署总览/期望状态管理页把目标期望状态激活,再回本页重新选择。
- 发起部署失败,提示 409
DEPLOYMENT_CONFLICT(「已有进行中部署...同一 agent 串行执行」):原因是对同一 Agent 发起了第二次部署,部署锁拦截(pending/deploying视为进行中)。处理:等该 Agent 部署推进到终态(synced/failed),或先对旧部署执行手动 Sync/推进完成后再发起。 - 部署一直停留在 deploying,推进不前进:原因有二——① 就绪门控:本批组件未全部 ready(有组件上报 deploying 或探针未通过);② 组件就绪超时(默认 60s)被判 failed 触发了回滚。处理:展开部署行看组件状态/错误列定位未就绪组件,点「推进」把它上报为 ready 放行下一批;若看到
readiness 就绪超时/readiness 探针未通过错误,检查探针声明与组件真实就绪情况。 - 点「推进」后提示
推进失败:Unexpected token ...:原因是「就绪快照(readiness JSON,选填)」填了非法 JSON,前端JSON.parse抛异常且请求未发出。处理:按 placeholder{"ready":true}修正引号/括号,或留空跳过;确认提示里没有「已推进」字样(本轮请求未提交,不会产生半途状态)。 - 漂移检测报
漂移检测失败:Unexpected token ...:原因是「实际状态(actual_state JSON)」不是合法 JSON,parse 抛异常、请求未发出。处理:按 placeholder{"app":"demo-app","version":"9.9.9"}修正;留空则提交空对象判定。 - 漂移检测总提示「检测到漂移(N 项 diff)」:原因是 actual_state 留空提交 → 后端按空对象与(通常非空的)期望声明比对,必产大量
add型 diff。处理:按期望声明的实际内容填写接近真实的 actual_state 再检;drifting的收敛请走漂移事件区「收敛(reconcile)」。 - 点「收敛(reconcile)」后提示
收敛中(...剩余 N 项漂移)而非收敛完成:原因是本页收敛固定传空 actual_state,重新判定必然仍有 diff,reconciling=true只是指示 Spoke 重新拉取。处理:这不是失败——等 Agent 下一轮拉取并应用期望状态后再查看漂移事件;若漂移持续存在且收敛反复触发,检查 auto_fix 轮数上限(3 轮)与防抖窗口(5 分钟),以及 Agent 上报链路。 - 部署列表里 drifting 行点「收敛漂移」没有反应(控制台 ReferenceError):原因是该按钮绑定的
openReconcile函数在<script setup>中未定义(前端缺陷,见 8 章)。处理:改用下方漂移事件列表里对应事件的「收敛(reconcile)」按钮执行收敛;该问题需由前端修复。 - 手动 Sync 提示失败且文案含「部署已收敛(synced),无需手动 sync」:原因是列表数据陈旧——该部署实际已 synced,仍显示旧按钮态。处理:刷新页面/重进列表后确认状态;synced 部署无需也不允许 sync。
- 打开推进弹窗后「选择组件」为空、无法推进:原因是在行未展开时直接点「推进」,组件列表是异步加载的,弹窗先于详情加载完成打开。处理:等几秒让组件加载完成再选,或先点该行展开详情等组件表渲染后,再点「推进」重开弹窗;若始终为空,检查该部署的
GET /deployments/:id/components是否返回空(理论上发起部署必含组件,为空属异常,可看后端日志)。
8. 已知缺陷与边界
| 缺陷/边界 | 说明 |
|---|---|
| drifting 行「收敛漂移」按钮失效 | DeploymentPage.vue 行内按钮 @click="openReconcile(dep)" 引用了未定义函数(script setup 只有 openAdvance/handleReconcile),点击抛 ReferenceError、不发起任何请求——收敛请走漂移事件列表的「收敛(reconcile)」按钮;需前端修复 |
| 漂移事件加载失败被静默吞掉 | fetchDriftEvents 的 catch 只把 driftEvents 置空数组,无提示条——漂移事件加载异常对用户不可见,易误判「无漂移事件」 |
| 推进成功自动收起详情 | advance 成功后调用 toggleDetail(id) 对已展开的同一行是折叠语义,详情每次推进后自动收起,多轮推进需每轮重新点击行展开查看进度(易用性) |
| 纯 Pull 演示 | 页面通过「推进」弹窗模拟 Agent 上报,真实收敛依赖 Spoke Agent 拉取;页面不轮询、不自动刷新,状态变化需手动操作触发 |
| 收敛/漂移检测传空状态必报漂移 | 漂移检测留空 actual_state 与收敛固定空体,对非空期望声明必然再检出差量;是演示语义,需配合 Agent 重拉收敛 |
| 全量无分页 | 部署列表、期望状态、策略、漂移事件均一次性全量加载(drift events 后端 limit=0),数据量大时页面临压力 |
| 漂移下拉不过滤状态 | 「漂移检测」期望状态下拉列全部状态(含 draft/deprecated/rollbacked),未像发起部署那样只列 active,可能误选已弃用状态 |
| 策略加载失败静默 | fetchPolicies 失败仅注释「不阻断」,无任何提示,策略下拉会只剩「默认(default)」 |
| 原生 confirm | 手动 Sync、收敛均用浏览器原生 confirm,同步阻塞且样式不统一;误确认后无撤销通道 |