1. 页面概览
1.1 是什么
「配置管理」页(对应前端源码 action/web/src/views/apollo/ConfigsPage.vue,页面标题「Apollo 配置管理」)是 LightApollo 的配置中心操作台(对齐 TAD-07)。页面用三个 Tab 拆成三块能力:配置模板(templates)声明 raw_yaml / kustomize / helm 三类配置模板并支持「按环境渲染」与「两环境差异对比」;密钥(secrets)以 AES-256-GCM 加密创建与管理 Sealed-Secret(列表只展示元数据、不落明文,可导出 sealed-secret YAML 入 Git 交付);加密配置(encryption)读写项目级加密主密钥配置。整页回答的是「部署时应用的配置从哪来、怎么证明各环境一致、敏感值怎么安全落库」:先在 Git 仓库登记模板与路径 → 选环境渲染出最终 YAML → 对比两环境差异确认收敛 → 把密码等敏感值加密成 Sealed-Secret 交付。 理解本页的钥匙是「模板声明 → 环境渲染 → 缓存比对」三段式。配置模板是声明式资源(git 仓库 + git_path 定位基线文件),本身不是最终配置;最终配置由后端渲染引擎把基线 + 覆盖 + 变量替换 + schema 校验合成出来(见 5.4),并写入 environment_configs 渲染缓存。因此本页的「渲染」和「对比」都是对同一份模板声明按不同环境即时求值:渲染弹窗选一个 environment_id 看该环境的产物 YAML;对比弹窗选 env_a/env_b 两个环境,后端分别渲染后做路径级 diff(add 绿 / remove 红 / change 橙)——「两环境配置是否漂移」用这个操作一眼可辨。渲染缓存同时被后续部署消费,模板更新后重渲染即可让新环境生效。 密钥能力走内置 AES-256-GCM 加密(自研轻量,不依赖外部 sealed-secrets 插件或 KMS,适配 Windows 单机模式):主密钥由后端自动生成并持久化在 encryption_configs,前端在任何列表/详情/导出里都拿不到明文——明文只在新密钥创建时输入一次即被加密成 AES256:<iv hex>:<ciphertext base64> 落库;「查看」导出的 sealed-secret YAML(kind: EncryptedSecret,apiVersion: apollo.zytech.io/v1)可安全提交 Git,由部署侧解密注入。项目 id 暂用常量 1(PROJECT_ID=1),本页所有列表都挂在项目 1 下。 典型使用链路(演示视角的完整闭环):① 先在「Git 仓库」页登记仓库、在「环境管理」页建好 dev/prod 等环境;② 本页 Tab1「新建配置模板」绑定仓库 + 路径(如 manifests/prod)创建模板;③ 行内「渲染」选 prod 环境,得到渲染结果 YAML(观察 ${ENVIRONMENT}/${PROJECT_ID} 等变量是否被替换);④ 行内「对比」选 dev vs prod,看差异行判断两环境期望是否收敛;⑤ Tab2「新建密钥」输入明文(如 db-password)加密落库,「查看」导出 sealed-secret YAML 供入 Git;⑥ Tab3 保存加密配置确认主密钥就绪。配置产物随后由「部署与漂移」/「Spoke Agent」链路消费。 直观示例(理解变量与 diff 的最小场景):设 dev/prod 环境名分别为 dev/prod,模板 git_path 指向仓库目录、其下 base.yaml 内容含 image: demo-web:${ENVIRONMENT} 与 replicas: 1。对 prod 环境渲染时,内置变量把 ${ENVIRONMENT} 替换为 prod,产物即 image: demo-web:prod(replicas 等无变量键保持不变);若模板带 schema {"required":["replicas"]} 而 base 恰好缺 replicas,渲染报 配置缺少必填字段: replicas 并拒绝写缓存。随后对 dev/prod 两环境「对比」,仅 image 路径值不同 → diff 输出单条 change(old demo-web:dev、new demo-web:prod);两环境产物完全相同时 diff 返回空数组,界面显示「两环境配置一致,无差异」。演示「环境收敛」通常就用这种「环境相关变量 + 个别 key 差异」来体现。
1.2 核心价值/能力表
| 能力 | 说明 | 对应页面操作 |
|---|---|---|
| 配置模板管理 | raw_yaml / kustomize / helm 三类模板声明,绑定 Git 仓库与路径 | Tab「配置模板」新建/编辑/删除 |
| 按环境渲染 | 选目标环境渲染出最终 YAML(Base+Overlay 合并 + 变量替换 + schema 校验 + 写缓存) | 模板行「渲染」 |
| 两环境差异对比 | env_a vs env_b 渲染产物做路径级 diff(add/remove/change + 旧值/新值) | 模板行「对比」 |
| Sealed-Secret 密钥 | AES-256-GCM 加密存储,列表仅展示元数据,明文一次性使用 | Tab「密钥」新建/查看/轮换/删除 |
| sealed-secret YAML 导出 | 导出 kind: EncryptedSecret 的 YAML,可安全入 Git 交付 | 密钥行「查看」→「复制」 |
| 主密钥轮换 | 换新项目主密钥并原子重加密项目内全部密文 | 密钥行「轮换」 |
| 加密配置读写 | 项目加密主密钥配置展示与保存(内置 AES-256-GCM 主密钥自动生成) | Tab「加密配置」「保存加密配置」 |
1.3 一句话总结
本页把「模板声明 → 环境渲染 → 差异对比」与「AES-256-GCM 加密密钥管理」集中在同一操作台,为部署环节提供可追溯、可审计、可安全交付的配置下发能力。
2. 访问入口
2.1 路由与菜单
| 项 | 值 |
|---|---|
| 路由 path | /apollo/configs |
| 路由 name | ApolloConfigs |
| meta.title | Apollo 配置管理 |
| 侧边栏入口 | ApolloLayout 侧边栏「配置管理」,位于「安全合规」之后、「制品与渠道」之前 |
| 前端源码 | action/web/src/views/apollo/ConfigsPage.vue |
| API 客户端 | action/web/src/api/apolloClient.js |
| 路由注册 | action/web/src/router/index.js,/apollo 子路由 children 下,component: ApolloConfigsPage(import 的 ConfigsPage),挂 ApolloLayout |
相邻页(侧边栏顺序):安全合规 security.md(前,安全策略门禁与扫描)、制品与渠道 bundles.md(后)、环境管理 environments.md(渲染/对比的环境下拉数据来源)、Git 仓库 git-repos.md(模板下拉的仓库数据来源)。
2.2 认证与权限
- 路由
requiresAuth: true:未登录访问被前端路由守卫拦到/apollo/login(登录态为独立apollo_token,2026-09-12 修订后仅认apollo_token,不再回退读取旧aip_token)。 - 后端配置端点全部注册在 protected 组,按权限点鉴权:列表/详情/sealed-yaml 读取用
config:read(PermConfigRead)、模板/密钥的写与删除用config:write(PermConfigWrite)、渲染/对比/轮换/解密用config:execute(PermConfigExecute)。403 时 apolloClient 统一alert('无权限执行该操作')。 - 前端 401(apollo_token 失效)由响应拦截器清 token 并跳转
/apollo/login。 - 与安全合规页的
security:*权限点互相独立:被授予config:*而缺security:*的主体可进本页管理配置,反之亦然;无任一权限点则登录后也进不了这两个菜单对应的页面数据。
2.3 端口与 API 前缀
- Apollo 后端端口 18082。Vite 开发代理把
/apollo-api前缀 rewrite 为/api转发(后端路由注册在/api/v1),客户端 baseURL/apollo-api/v1。 - 统一响应 envelope
{code,message,data,request_id}:拦截器在code===0时把response.data解包为业务数据;data可能是数组或{items,...}对象,页面用toList健壮解析。本页全部接口都走 envelope,无公开端点(对比安全合规页的 agent pull/report 裸响应场景)。 - 后端未初始化加密主密钥时,
GET /projects/:id/encryption-config返回 404(envelope),提示信息见 5.3。
3. 界面布局
┌──────────────────────────────────────────────────────────────┐
│ Apollo 配置管理 [新建配置模板]/[新建密钥] │
│ alert-info:配置中心(TAD-07):配置模板声明…密钥以 AES-256-GCM…│
│ [操作结果提示条(v-if alert.message,可「关闭」)] │
├──────────────────────────────────────────────────────────────┤
│ Tab:配置模板 | 密钥 | 加密配置 │
├──────────────────────────────────────────────────────────────┤
│ ┌ Tab1 配置模板 ────────────────────────────────────────────┐ │
│ │ 配置模板(n):ID/名称/类型徽标/Git仓库/Git路径/描述/操作 │ │
│ │ 操作:渲染 | 对比 | 编辑 | 删除 │ │
│ └────────────────────────────────────────────────────────────┘ │
│ ┌ Tab2 密钥 ────────────────────────────────────────────────┐ │
│ │ alert-info:密钥以 AES-256-GCM 加密存储,列表仅展示元数据… │ │
│ │ 密钥(n):ID/名称/加密方式徽标/密钥ID/创建时间/操作 │ │
│ │ 操作:查看 | 轮换 | 删除 │ │
│ └────────────────────────────────────────────────────────────┘ │
│ ┌ Tab3 加密配置 ────────────────────────────────────────────┐ │
│ │ 加密配置(项目 #1):alert-info + provider 下拉 + 更新时间 │ │
│ │ 说明(description)textarea + [保存加密配置] │ │
│ └────────────────────────────────────────────────────────────┘ │
├──────────────────────────────────────────────────────────────┤
│ 弹窗层(modal-overlay,@click.self 可关): │
│ ┌ 新建/编辑配置模板弹窗:名称/类型/Git仓库/路径/Schema/描述 ─┐ │
│ ┌ 渲染弹窗:目标环境 select → [开始渲染] → 渲染结果 YAML ────┐ │
│ ┌ 对比弹窗:环境A/环境B select → [开始对比] → diff 表 ──────┐ │
│ ┌ 新建密钥弹窗:名称 + 明文 → [创建(加密落库)] ────────────┐ │
│ ┌ Sealed Secret YAML 弹窗:YAML code block + [复制] ────────┐ │
└──────────────────────────────────────────────────────────────┘
各板块职责:
| 板块 | 职责 |
|---|---|
| 页头 + 顶部 alert-info | 标题「Apollo 配置管理」、按当前 Tab 显示「新建配置模板」/「新建密钥」(加密配置 Tab 无页头新建按钮)、TAD-07 说明、全局操作结果提示 |
| Tab 切换条 | 配置模板 / 密钥 / 加密配置 三面板切换;页头按钮随 Tab 变化 |
| 配置模板卡片 | 项目 #1 的模板表:行内渲染/对比/编辑/删除 |
| 密钥卡片 | 元数据表(明文不回显),行内查看 sealed-yaml/轮换/删除 |
| 加密配置卡片 | 项目加密主密钥 provider/更新时间/说明 的表单 |
| 配置模板弹窗 | 新建/编辑模板字段(含 schema JSON 可选) |
| 渲染弹窗 | 选目标环境 → 展示该环境渲染产物(code block) |
| 对比弹窗 | 选 env_a/env_b → 差异表(或两文本对比兜底) |
| 新建密钥弹窗 | 名称 + 明文值(一次性输入) |
| Sealed Secret YAML 弹窗 | 密钥 sealed-secret YAML 展示与复制 |
4. 交互元素
4.1 页头、顶部说明与操作结果提示条
| 控件 | 位置 | 含义 | 必填/默认/可用条件 | 操作效果 | 触发后端调用 | 边界与细节 |
|---|---|---|---|---|---|---|
| 页面标题 | 页头 h2 | 页面身份标识 | 常驻 | Apollo 配置管理 | 无 | 与源码 h2 逐字一致 |
| 「新建配置模板」 | 页头(仅 templates Tab) | 打开新建模板弹窗 | 当前 Tab=配置模板 | openCreateTemplate 预填 git_repo_id 为第一个仓库 | 无(弹窗) | 其他 Tab 下该按钮替换为「新建密钥」或隐藏 |
| 「新建密钥」 | 页头(仅 secrets Tab) | 打开新建密钥弹窗 | 当前 Tab=密钥 | openCreateSecret 清空表单 | 无(弹窗) | - |
| 顶部 alert-info | 标题下 | TAD-07 说明 | 常驻 | 只读文案 | 无 | 文字与源码逐字一致(见附录 A) |
| 操作结果提示条 | alert-info 下 | 展示最近一次操作结果 | 有 alert.message 才显示 | 成功 success / 失败 error | 无 | 单槽覆盖;「关闭」清空 |
页面状态用一组 ref 维护:loading(templates/secrets/encryption 三个布尔位,其中 encryption 初始 true)、busy(全局互斥操作键,非空则所有按钮禁用)、alert、三张表(templates/secrets/gitRepos/environments)、五个弹窗状态(templateModal/renderModal/diffModal/secretModal/sealedModal)、encConfig/encForm。busy 全局互斥:任一写操作(保存模板/渲染/对比/保存密钥/轮换/删除/保存加密配置)进行中,其余全部操作按钮同时禁用。
4.2 配置模板表与「新建/编辑配置模板」弹窗
卡片标题 配置模板({{templates.length}})。加载中显示 加载中...;空态 暂无配置模板,点击"新建配置模板"添加(项目 #1)。;加载失败提示 加载配置模板失败:{msg}。表格列:ID / 名称 / 类型 / Git 仓库 / Git 路径 / 描述 / 操作。类型徽标 typeLabel/typeClass:raw_yaml 灰 / kustomize 蓝 / helm 橙(长名 raw_yaml(原始 YAML) 只取括号前段)。Git 仓库列 repoLabel(git_repo_id) 用 gitRepos 查找显示 {name}(#{id}),查不到只显 #{id}。行内操作:渲染 / 对比 / 编辑 / 删除,全部 :disabled="!!busy"。 新建/编辑弹窗头:新建配置模板 / 编辑配置模板 #{{id}}。字段:
| 字段 | 控件/校验 | 说明 |
|---|---|---|
| 名称(name)* | input required | placeholder 如 app-config;保存前 trim 判空 |
| 类型(type)* | select | raw_yaml(原始 YAML)/ kustomize / helm(Helm Chart);默认 raw_yaml |
| Git 仓库(git_repo_id)* | select / number | gitRepos 有数据显示下拉 {name}(#{id});仓库列表加载失败时降级为 number 输入框(placeholder 仓库 ID(仓库列表加载失败时手输));默认选第一个仓库;保存 Number(git_repo_id) |
| Git 路径(git_path)* | input required | placeholder 如 manifests/prod 或 charts/app |
| Schema(schema JSON,可选) | textarea rows=3 | placeholder {"required":["replicas"],"properties":{"replicas":{"type":"integer"}}} 或留空;非空先 JSON.parse(语法错则 showAlert('schema 不是合法 JSON:{e.message}','alert-error') 并中止);muted 配置渲染后按此 Schema 校验;留空则不校验。 |
| 描述(description) | input | placeholder 如 生产环境应用配置模板;trim 非空才带 |
提交(handleSaveTemplate):先本地校验 名称、git_repo_id、git_path 非空,缺失提示 模板名称、Git 仓库(git_repo_id)与 git_path 必填;新建成功 配置模板创建成功(id=..)、编辑成功 配置模板 #id 更新成功,随后关闭弹窗并刷新模板表;失败 保存配置模板失败:{msg}(如项目内同名 409)。删除走 confirm 确定删除配置模板 "{name}"(#{id})吗?,成功 配置模板已删除,失败 删除配置模板失败:{msg}。
4.3 「渲染」弹窗
行内「渲染」打开 渲染配置模板 #{{id}}({{name}}) 弹窗。未出结果时显示目标环境选择:环境列表有数据显示下拉 {name}(#{id}),openRender 时默认选第一个环境;环境列表加载失败降级为 number 输入(placeholder 环境 ID(环境列表加载失败时手输))。底部按钮:取消 / 开始渲染(busy 或弹窗 busy 时禁用,进行中 渲染中...)。
| 控件 | 操作效果 | 触发后端调用 | 边界与细节 |
|---|---|---|---|
| 「开始渲染」 | 先校验 environment_id 已选,缺失提示 请选择目标环境(environment_id);成功后 m.result 填充渲染产物并展示,失败提示 渲染模板 #{id} 失败:{msg} | POST /config-templates/{id}/render?environment_id={envId} | 后端返回 {template_id, environment_id, rendered}(rendered 即 YAML 文本);前端 field() 候选键首位即 rendered(ConfigsPage.vue:724)——已修复(提交 412e9c48):现直接命中 rendered 展示 YAML 文本,不再兜底 stringify 整个 JSON |
结果区(renderModal.result 非空):顶部 muted 渲染时间:{{formatTime(renderedAt) || '-'}},下方 pre.code-block 展示渲染产物。弹窗标题下可点「关闭」或点遮罩关。渲染会写 environment_configs 缓存(见 5.4),是模板更新的正式生效路径——改模板后需重新渲染对应环境,缓存里的渲染结果才会刷新。边界:后端 render 响应不含 rendered_at 字段,renderedAt 取不到值,故「渲染时间」恒显示 -(见第 8 章)。
4.4 「对比」弹窗
行内「对比」打开 对比配置模板 #{{id}}({{name}}) 弹窗。未出结果时选择两环境(各自下拉,默认分别选第一、第二个环境,只有一个环境时两者同值);环境列表加载失败同样降级手输。按钮:取消 / 开始对比(进行中 对比中...)。
| 控件 | 操作效果 | 触发后端调用 | 边界与细节 |
|---|---|---|---|
| 「开始对比」 | 先校验 env_a/env_b 均非空,缺失提示 请选择对比环境(env_a / env_b);成功后按 parseDiffResponse 解析展示;失败 对比模板 #{id} 失败:{msg} | POST /config-templates/{id}/diff?env_a={a}&env_b={b} | 后端返回 {template_id, env_a, env_b, diff:[{type,path,old?,new?}]};前端找 items/diff/changes/lines 命中 diff → 渲染差异表;两文本形态(before/after 或 env_a/env_b)作兜底并排展示 |
结果区三种形态:① 差异表(result.items.length>0):列类型(add 新增绿 / remove 删除红 / change 变更橙徽标,别名 added/removed/modified/modify 兼容)/ 路径 / 旧值 / 新值,空值以 - 兜底、对象值 JSON 化;② 两文本并排(.diff-texts,键名如 环境 A(before)/环境 B(after));③ 都无 → 两环境配置一致,无差异。
4.5 「新建密钥」弹窗与密钥表
新建密钥弹窗头 新建密钥(项目 #1)。字段:名称(name)\* placeholder 如 db-password / api-key;明文值(plaintext)\* textarea rows=3 placeholder 输入密钥明文,后端加密后落库,列表与详情均不返回明文;muted 明文仅用于本次加密,后端以 AES-256-GCM 加密后存储,不会以明文返回。 提交按钮:busy==='saveSecret' 显示 加密保存中...,否则 创建(加密落库);取消按钮关弹窗。保存前校验名称与明文均非空,缺失提示 密钥名称(name)与明文值(plaintext)必填;成功提示 密钥创建成功(id=..)(已加密落库),关弹窗并刷新密钥表;失败 保存密钥失败:{msg}(同名 409 等)。明文由前端经 body {name, plaintext} 一次性提交,此后页面无任何地方可取回明文。 密钥表(Tab2)标题 密钥({{secrets.length}}),上方常驻 alert-info:密钥以 AES-256-GCM 加密存储,列表仅展示元数据(名称 / 加密方式 / 密钥 ID / 创建时间),不返回明文;可通过"查看"获取 sealed-secret YAML(kind: EncryptedSecret)用于交付或导入。 表列:ID / 名称 / 加密方式 / 密钥 ID / 创建时间 / 操作。加密方式徽标:aes-256-gcm/aes256_gcm/aes_gcm 绿、vault/cloud_kms 蓝、未识别灰(encLabel 取 ENC_PROVIDERS 匹配项括号前段)。行内操作:
| 控件 | 操作效果 | 触发后端调用 | 边界与细节 |
|---|---|---|---|
| 「查看」 | 打开 Sealed Secret YAML 弹窗并加载 YAML | GET /secrets/{id}/sealed-yaml | 见 4.6;失败 获取密钥 #{id} sealed YAML 失败:{msg} |
| 「轮换」 | 先 confirm(文案见附录 A)再调接口,成功 密钥 #id 轮换成功(新 key_id:..) 并刷新表;失败 轮换密钥 #{id} 失败:{msg} | POST /secrets/{id}/rotate | 单条轮换语义上是项目级:换新主密钥并原子重加密项目内全部密文(见 5.4),返回的 meta.key_id 即新主密钥标识 |
| 「删除」 | 先 confirm(文案见附录 A)再调接口,成功 密钥已删除 并刷新;失败 删除密钥失败:{msg} | DELETE /secrets/{id} | 删除不可恢复(密文无备份) |
4.6 Sealed Secret YAML 弹窗
「查看」打开 Sealed Secret YAML:{{name}}(#{{secretId}}) 弹窗,加载中显示 加载中...。正文上方 muted:该 YAML 为加密后的 sealed-secret(kind: EncryptedSecret),可安全交付/入库,不含明文。 正文 pre.code-block 展示 YAML 全文。底部两个按钮:「关闭」与「复制」——复制成功后提示 sealed-secret YAML 已复制(navigator.clipboard),失败 复制失败,请手动选择复制。 解析边界:后端返回 {filename:"sealed-secret.yaml", content:"<yaml 文本>"},前端 field(data,'sealed_yaml','yaml','content','manifest') 命中 content → 正常展示 YAML;字符串响应直接展示;都没有则把整个 data JSON 化(同第 8 章「sealed-yaml 解析兜底脆弱」边界,当前后端形态正常)。
4.7 Tab3:加密配置
卡片标题 加密配置(项目 #1)。内容:alert-info 内置 AES-256-GCM 主密钥由后端自动生成并持久化(适配 Windows 单机模式);加密后的敏感信息可安全存放(类似 Sealed Secrets),Agent 部署时解密注入应用环境变量或 K8s Secret。 + 表单:加密提供方(provider)下拉(AES-256-GCM(内置,主密钥自动生成)/ AES-GCM(兼容别名)/ HashiCorp Vault(可选)/ 云 KMS(可选))、更新时间(updated_at)只读框、说明(description) textarea、保存加密配置 提交按钮(busy==='saveEnc' 显示 保存中...)。加载中显示 加载中...;加载失败独立提示并给「重试」。 已修复(提交 412e9c48):onMounted 现同时调用 fetchSecrets() 与 fetchEncryptionConfig()(ConfigsPage.vue:894-899),loading.encryption 会被正常翻 false——「加密配置」Tab 打开即拉取并渲染表单,不再恒显示 加载中...;「密钥」Tab 也会在挂载时拉取真实列表。保存路径:handleSaveEncryption 提交 {provider,description},后端 PUT handler 现解析该 body 并持久化 provider/description(见 5.2/5.4),成功提示 加密配置已保存(provider:{响应 provider}) 的 provider 取自落库响应。演示配置能力可直接在页面操作。 > 注意:provider 下拉的 Vault/云 KMS 仅为可选展示项,内置加密恒走 AES-256-GCM、不接线外部 KMS(见第 8 章)。
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 解包为业务数据;HTTP 2xx 但 code!==0 拒绝;4xx/5xx 提取 body.message || body.error 并把 message 别名写入 error.response.data.error 后 reject——401 清 token 并跳 /apollo/login,403 alert('无权限执行该操作')。页面 catch 统一以 err.response?.data?.message || err.response?.data?.error || err.message(errMsg)展示后端错误。渲染/对比/查看接口的失败会走 catch 展示,密钥轮换/删除等均同此规则。
5.2 端点表
后端路由见 action/products/apollo/server/server.go(config 段约 964-981 行),全部带权限中间件。前端统一用 apolloClient,URL 前缀 /apollo-api/v1;下表写后端实际路径(前缀 /api/v1)。
| 方法 | 路径 | 权限 | 请求体/Query | 页面触发点 |
|---|---|---|---|---|
| GET/POST | /projects/:id/config-templates | config:read / write | POST body {name,type,git_repo_id,git_path,description?,schema?} | 模板表加载 / 新建模板 |
| GET/PUT/DELETE | /config-templates/:id | read / write / write | PUT body 同上 | 编辑/删除 |
| POST | /config-templates/:id/render | config:execute | Query environment_id | 渲染弹窗「开始渲染」 |
| POST | /config-templates/:id/diff | config:execute | Query env_a、env_b | 对比弹窗「开始对比」 |
| GET/POST | /projects/:id/secrets | config:read / write | POST body {name,plaintext,description?} | 密钥表加载 / 新建密钥 |
| GET/PUT/DELETE | /secrets/:id | read / write / write | PUT body {description?} | (页面当前只用到详情外的列表/删除;编辑走重建) |
| POST | /secrets/:id/rotate | config:execute | - | 密钥行「轮换」 |
| POST | /secrets/:id/decrypt | config:execute | - | 页面未暴露(脚本/后端链路使用) |
| POST | /projects/:id/secrets/rotate-batch | config:execute | - | 页面未暴露(脚本使用,项目级批量轮换) |
| GET | /secrets/:id/sealed-yaml | config:read | - | 密钥行「查看」 |
| GET/PUT | /projects/:id/encryption-config | config:read / write | PUT body {provider?,description?}(现解析并持久化) | Tab3 加载/保存(挂载即拉取,见 4.7) |
| GET | /projects/:id/git-repos | git-repo 读 | - | 模板弹窗仓库下拉(失败降级手输) |
| GET | /projects/:id/environments | env 读 | - | 渲染/对比弹窗环境下拉(失败降级手输) |
5.3 响应结构示例
以下示例取自后端 handler 构造与 configmgr 包模型(config_handlers.go、configmgr/),页面看到的实际 JSON 即 envelope 解包后的 data。 GET /projects/1/config-templates(数组,单项 ConfigTemplate):
[
{
"id": 3,
"project_id": 1,
"name": "app-config",
"type": "raw_yaml",
"git_repo_id": 2,
"git_path": "manifests/prod",
"schema": { "required": ["replicas"], "properties": { "replicas": { "type": "integer" } } },
"description": "生产环境应用配置模板",
"created_at": "2026-09-07T04:00:00+08:00",
"updated_at": "2026-09-07T04:00:00+08:00"
}
]
字段:type 枚举 raw_yaml/kustomize/helm;git_repo_id 定位 Git 仓库;git_path 仓库内相对路径(目录或单文件);schema(omitempty,未填不出现)渲染后校验;git_repo_id/git_path 页面新建设计为必填(后端 handleCreateConfigTemplate 对 name 空等有参数校验)。 POST /config-templates/3/render?environment_id=5(envelope 解包后):
{
"template_id": 3,
"environment_id": 5,
"rendered": "replicas: 3\nimage: demo-web:1.2.0\nENVIRONMENT: prod\nPROJECT_ID: 1\n"
}
rendered 是该环境的最终 YAML 文本(字符串)。已修复(提交 412e9c48):页面 field() 候选键现以 rendered 打头(ConfigsPage.vue:724),渲染结果直接展示 YAML 文本,不再兜底 stringify(data) 输出完整 JSON wrapper。残留边界:后端 render 响应不含 rendered_at,故「渲染时间」恒显示 -(见第 8 章)。真实产物可直接 curl 本接口取 rendered 字段观察。 POST /config-templates/3/diff?env_a=4&env_b=5(envelope 解包后):
{
"template_id": 3,
"env_a": 4,
"env_b": 5,
"diff": [
{ "type": "change", "path": "replicas", "old": 1, "new": 3 },
{ "type": "add", "path": "ENVIRONMENT", "new": "prod" },
{ "type": "remove", "path": "debug", "old": true }
]
}
diff 数组元素 DiffLine{type,path,old?,new?}(configmgr/render.go),type 为 add/remove/change,old/new omitempty。前端 parseDiffResponse 的键顺序 items→diff→changes→lines,当前命中 diff 走差异表渲染。 GET /projects/1/secrets(数组,单项 SecretMeta):
[
{
"id": 7,
"name": "db-password",
"encryption_method": "aes256_gcm",
"key_id": "proj-1",
"description": "",
"created_by": "admin",
"created_at": "2026-09-07T04:05:00+08:00",
"updated_at": "2026-09-07T04:05:00+08:00"
}
]
encryption_method 恒 aes256_gcm(当前实现);key_id 主密钥标识(初值 proj-1,轮换后 proj-1-rot-<ts%1000000>);不含明文与密文——元数据是密钥唯一可见形态。新建密钥 POST 返回 201 + 同形态 meta(okStatus(http.StatusCreated, meta))。 GET /secrets/7/sealed-yaml(envelope 解包后):
{
"filename": "sealed-secret.yaml",
"content": "apiVersion: apollo.zytech.io/v1\nkind: EncryptedSecret\nmetadata:\n name: db-password\nspec:\n encryptionMethod: aes256_gcm\n keyId: proj-1\n data: AES256:<iv hex>:<ciphertext base64>\n"
}
content 为可入 Git 交付的 sealed-secret YAML(kind: EncryptedSecret,不含明文;data 即 AES-256-GCM 密文格式 AES256:<iv hex>:<ciphertext base64>)。 GET /projects/1/encryption-config(envelope 解包后;未初始化时 404):
{
"project_id": 1,
"provider": "aes256_gcm",
"description": "生产主密钥",
"created_at": "2026-09-07T04:05:00+08:00",
"updated_at": "2026-09-07T04:05:00+08:00"
}
provider 默认 aes256_gcm(内置);description 为说明文本(可空,PUT 保存后回传)。 GET /projects/1/encryption-config(404,主密钥未初始化):失败 envelope 不含 data 键(与成功形态唯一结构差异),错误码 40401、HTTP 404:
{
"code": 40401,
"message": "项目加密主密钥未初始化,请先调用 PUT /projects/:id/encryption-config",
"request_id": "req_1720000000000a1b2"
}
未初始化(项目从未创建密钥且从未 PUT)时后端返回上述 404,前端若直连会走 errMsg 展示 message。PUT /projects/:id/encryption-config 的响应同 GET(幂等确保主密钥存在,首次 PUT 后 GET 即 200);已修复(提交 412e9c48):handler 现解析可选 body updateEncryptionConfigRequest{Provider *string; Description *string} 并持久化,description 亦已进入模型(见 5.4)。request_id 由中间件生成(req_ + unix 毫秒 + 4 位 hex),同时出现在响应头 X-Request-ID,可作为排错时的请求追踪标识。
5.4 关键机制
渲染引擎:模板类型只是登记语义,基线文件如何被发现(configmgr/render.go)。raw_yaml / kustomize / helm 三类模板类型对齐 gitops.ConfigType 语义,仅表示「这份配置在仓库里的组织形态」,引擎不执行真实的 Kustomize build 或 helm template——无论登记哪一类型,基线读取都走同一套 readBaseFile:git_path 指向 YAML 单文件时直接读该文件;指向目录(或留空)时按 base.yaml → config.yaml → base.yml → config.yml 顺序尝试目录下首个存在者,均不存在则按空 base 处理。仓库快照来自 gitops.LocalRepo{Path: git_repo.url}.Snapshot(直接读仓库本地目录,不发起 clone):仓库记录不存在或快照失败时不中断渲染,改按空 base 继续,并在输出 YAML 顶部插入 # 注释行(如 # 提示: git 仓库 2 不存在,已使用空 base 渲染、# 提示: git 仓库快照失败(open xxx: no such file...),已使用空 base 渲染)——当「渲染成功但内容不是期望配置」时,先看产物首行是不是这条提示注释。base 文件内容必须是合法 YAML,解析失败直接报 解析 base YAML 失败(<git_path>): <err>;overlay 解析失败同理报 解析 overlay YAML 失败(<overlay_path>): <err>,这两种失败都会让接口整体报错、页面走 4.3 的失败提示。 Overlay 覆盖、变量替换与 diff 的路径语义。引擎在 base map 上做自研 deepMerge 合入 overlay:两个键都是 map 时递归合并、标量覆盖、list 整段替换(非 JSON Merge Patch、不做逐元素合并);overlay 文件来自 environment_configs.overlay_path(「模板 × 环境」绑定行上的可选覆盖路径,仓库内相对定位,文件优先按仓库根、其次按 git_path 目录相对解析,目录形态则读其下 base.yaml/config.yaml);绑定行不存在时首次渲染会经 cacheRendered 自动创建该行、overlay_path 留空。变量替换把对象字符串值中的 ${KEY} / $KEY 按变量表替换(正则 \$\{...\}|\$...),并递归进入嵌套 map 与数组元素内的字符串;内置变量 ENVIRONMENT、ENVIRONMENT_NAME 注入当前环境名(envRepo 为空时跳过)、PROJECT_ID 注入模板归属项目;未定义变量保留原样。diff(diffMaps)对两环境渲染 map 做点分路径级递归比对:仅 B 有 → add(带 new)、仅 A 有 → remove(带 old)、同路径值不同 → change(同时带 old/new),嵌套对象继续下钻到叶子路径(如 resources.cpu);两环境一致时输出空数组 [](非 null,保证结构恒定)。渲染与对比都只读不写 Git 仓库,所有写操作落在数据库 environment_configs 缓存行上。 Schema 校验与渲染缓存。模板带 schema 时,渲染完成后按 {required:[],properties:{}} 结构递归校验(嵌套 Schema 声明了约束但实际值不是对象时视为结构不匹配):required 逐层查键存在,缺失字段以点分路径列表报 配置缺少必填字段: <路径1>, <路径2>;properties 值非对象时报 配置字段 <路径> 期望为对象,实际为非对象值。校验失败渲染整体报错且不写缓存。通过后 map→YAML 输出,并写/更新 environment_configs 行的 rendered_config(JSON 缓存)与 rendered_at(cacheRendered)。同一环境多次渲染结果一致可复现;模板/环境变更后需重新点「渲染」才刷新缓存——部署消费的正是该缓存行(见第 7 章问题 10)。 Sealed-Secret 加密与主密钥生命周期(configmgr/secret.go)。主密钥按项目一份:EnsureEncryptionConfig 幂等确保存在(无则生成 32 字节 crypto/rand key,base64 存 encryption_configs.key_material,key_id 初值 proj-<pid>),适配 Windows 单机模式、不依赖外部 KMS。Encrypt 用 AES-256-GCM + 随机 12 字节 IV 加密明文,密文格式 AES256:<iv hex>:<ciphertext base64>(ciphertext 含 GCM tag),明文不落库、密文不通过 JSON 回传(前端只在创建时拿 meta)。ToSealedSecretYAML 把密文导出为 apiVersion: apollo.zytech.io/v1 / kind: EncryptedSecret 的 YAML(spec 含 encryptionMethod/keyId/data),供交付入 Git,部署侧按需解密注入。 轮换的三阶段原子语义。POST /secrets/:id/rotate 表面按单条密钥发起,实际 Rotate 验证目标存在后调 rotateProjectLocked(与批量轮换 rotate-batch 共用):阶段一用旧主密钥在内存解密项目内全部密文(单条失败记入失败清单不中断);阶段二内存生成新主密钥(不落库)统一重加密;阶段三逐条落库新密文行(记录旧密文/旧 key_id 快照),全部成功后最后才提交主密钥覆盖 encryption_configs(key_id 变 proj-<pid>-rot-<ts%1000000>);任一步失败整体回滚(旧 key 与旧密文完整保留),目标密文在失败清单中则返回错误且主密钥不切换。为什么是项目级:项目所有密钥共用一把主密钥,切换主密钥后其余密文仍用旧 key 会永久不可解,故轮换必须连带重加密项目全部密文——页面上对任意一条密钥点「轮换」,实际影响范围是项目内全部密钥(这是 2026-08-11 数据可用性修复后的设计,页面 confirm 只提示单条,见第 8 章边界)。rotate-batch 是同一流程的显式批量入口(结果含 total/rotated/failed/failures),页面未暴露。 加密配置保存的语义。PUT /projects/:id/encryption-config handler 先调 EnsureEncryptionConfig(确保主密钥存在),已修复(提交 412e9c48):现解析可选 body updateEncryptionConfigRequest{Provider *string; Description *string}(io.ReadAll + json.Unmarshal)并经 cfgEncCfgRepo.Update 持久化 provider/description——保存后再 GET 能读到更新值。前端「保存加密配置」提示的 provider 取自响应(落库值)。遗留边界:provider 下拉的 vault/cloud_kms 只是可选展示项,实际加密恒走内置 AES-256-GCM、当前实现不接线真实 KMS(见第 8 章)。GET 未初始化返回 404 提示先 PUT。 密钥同名与不可变性:项目内密钥名唯一(重名 409,页面提示 保存密钥失败:{msg});密文内容创建后不可原地修改(PUT /secrets/:id 仅允许更新 description),改密文须删除重建;解密接口 POST /secrets/:id/decrypt(config:execute)每次调用校验项目作用域并落审计 CONFIG_SECRET_DECRYPT(页面未暴露,供脚本/部署链路使用)。
6. 权限与安全
- 认证分层:全部端点走独立
apollo_token(Bearer),按config:read/write/execute鉴权(读取含 sealed-yaml,写含新建/删除,execute 含渲染/对比/轮换/解密),403 由拦截器统一alert('无权限执行该操作')。 - 明文隔离:明文只在新建密钥时经 HTTPS 提交一次;列表/详情/导出/轮换任一返回路径都不含明文与密文(sealed-yaml 的密文是加密态、可直接交付)。删除密钥不可恢复,删除前有 confirm。
- 写操作防护:删除模板/删除密钥/轮换均有 confirm;
busy全局互斥防并发写;重名创建 409 被后端拒绝。解密/轮换为高敏操作,均落审计(CONFIG_SECRET_ROTATE/CONFIG_SECRET_DECRYPT 等)可在「审计日志」页查询。 - 渲染缓存一致性:模板更新后需重新渲染目标环境才刷新
environment_configs缓存,避免旧配置被部署消费。 - 环境/仓库下拉的降级安全:git-repos/environments 请求失败被静默降级为手输数字 id——数据来自用户输入但只用于资源定位(不存在越权读取面),供弹窗可用性兜底。
7. 常见问题与排错
以下问题均能从 ConfigsPage.vue 的 alert/confirm 文案、后端 handler/渲染与密钥服务代码复现。 1. 「渲染」结果不是 YAML,而是一大坨 JSON(历史现象):已修复(提交 412e9c48)——前端 field() 候选键现以 rendered 打头(ConfigsPage.vue:724),渲染结果直接展示 YAML 文本,不再兜底 stringify 整个响应。仅「渲染时间」仍恒为 -(后端响应不含 rendered_at,见第 8 章)。若仍看到 JSON,多为响应被网关/代理改写,可用 curl 调 POST /apollo-api/v1/config-templates/{id}/render?environment_id={env} 核对 rendered 字段。 2. 渲染「成功」但结果为空或内容不符:最常见原因是仓库缺失/快照失败被后端按空 base 渲染——输出首行会出现 # 提示: git 仓库 N 不存在/快照失败(...),已使用空 base 渲染 注释;base 文件本身不是合法 YAML 则报 解析 base YAML 失败(<git_path>): <err>;模板带 schema 时产物缺 required 键报 配置缺少必填字段: ...。处理:先看渲染产物首行是否带 # 提示: 注释;到「Git 仓库」页确认仓库已登记且本地目录可读;确认 git_path 目录下确有 base.yaml/config.yaml(或直接指向 YAML 单文件)且语法合法;带 schema 的模板核对产物含全部必填键后再重新渲染。 3. 新建模板的 Git 仓库下拉是空的、只能手输数字:原因是 GET /projects/1/git-repos 失败被静默降级(gitRepos=[]),弹窗改用 number 输入。处理:先到「Git 仓库」页确认已登记仓库;打开浏览器 Network 看 git-repos 请求为何失败(404/权限),修复后刷新重进。 4. 「对比」提示两环境配置一致但明明有差异 / 差异不直观:原因是 diff 基于两环境的渲染产物求差——若两环境都没有触发变量差异(如模板没用 ${ENVIRONMENT} 且 overlay 相同),产物相同则无 diff;若响应不是列表形态,前端会回退两文本并排。处理:确认模板在 git_path 引用了环境相关变量或 overlay,再对 env_a/env_b 重试;diff 类型含义见 4.4。 5. 密钥轮换后其它密钥也能正常「查看」,但再轮换报错:单条轮换是项目级操作,两次连续轮换按「新 key 加密→再换新 key 解密」应幂等可用;报错多为并发轮换冲突(busy 互斥被绕过,脚本同时调 rotate-batch)或主密钥提交失败回滚。处理:等上一次轮换结束后再操作;单条轮换接口幂等可重复调用(Rotate 注释),失败先查审计 CONFIG_SECRET_ROTATE。 6. 「查看」sealed YAML 弹窗内容不是 YAML 而是一串 JSON:后端返回 {filename, content},前端 field() 候选键顺序 sealed_yaml→yaml→content→manifest,content 在候选内故当前应正常;若仍异常,多半是后端返回了别的形态(如字符串被 axios 解析为对象)。处理:核对 GET /apollo-api/v1/secrets/{id}/sealed-yaml 响应;复制用弹窗「复制」按钮(clipboard 失败会提示手动复制)。 7. 新建密钥提示 保存密钥失败:{msg} 且 msg 含 409:原因是项目内已有同名密钥(密钥名唯一约束)。处理:改名或用「查看」确认既有密钥是否可复用;密文不可原地改,需改内容只能删除重建。 8. 加密配置 Tab 一直「加载中...」,表单不出现(历史现象):已修复(提交 412e9c48)——onMounted 现调用 fetchEncryptionConfig()(ConfigsPage.vue:894-899),Tab 打开即拉取并渲染表单;加载失败会独立提示并给「重试」。若仍卡加载,先看提示条错误(多为权限/网络),主密钥是否就绪可用脚本 GET /apollo-api/v1/projects/1/encryption-config 判断(404 表示未初始化,需先 PUT)。 9. 环境列表为空时渲染/对比弹窗手输 id,提交后 404/500:原因是输入的环境 id 不属于项目 #1 或不存在;对比时 env_a=env_b 也合法但结果必然「无差异」。处理:先在「环境管理」页登记环境,回到本页重进弹窗选下拉;确认 id 拼写无误。 10. 模板改了但部署拿到的配置没变:原因是渲染缓存(environment_configs.rendered_config)只在「渲染」时刷新,仅保存模板不重渲染则缓存保持旧值。处理:改模板后对目标环境重新点一次「渲染」,确认成功后再去部署/Agent 链路消费。
8. 已知缺陷与边界
| 缺陷/边界 | 说明 |
|---|---|
| 密钥/加密配置 Tab 永不加载 | 已修复(提交 412e9c48):onMounted 现调用 fetchSecrets() 与 fetchEncryptionConfig()(ConfigsPage.vue:894-899),「密钥」Tab 挂载即拉取真实列表、「加密配置」Tab 打开即渲染表单,不再恒显示空态/「加载中...」 |
渲染时间恒 - | 后端 render 响应不含 rendered_at 字段,前端 m.renderedAt = field(o,'rendered_at','rendered_at_time') 取不到值(ConfigsPage.vue:726),结果区「渲染时间」恒显示 -。属后端契约边界(渲染缓存行有 rendered_at,但 render 接口未回传);前端渲染产物 YAML 已正常展示(提交 412e9c48) |
| 加密配置 PUT 不生效 | 已修复(提交 412e9c48):后端 handleUpdateEncryptionConfig 现解析可选 body updateEncryptionConfigRequest{Provider *string; Description *string} 并经 cfgEncCfgRepo.Update 持久化,description 亦已进入模型;保存后 GET 可读到更新值 |
| 应用语义与文案边界 | 轮换 confirm 只提示当前密钥「将生成新的 key_id 并重新加密」,实际影响项目内全部密钥(单条轮换复用批量轮换实现),建议文案明示范围;删除密钥/模板不可恢复,无回收站 |
| PROJECT_ID 硬编码 | 三 Tab 全部列表/写操作以 PROJECT_ID=1 常量拼 URL;后续才支持 URL 参数 /project_id |
| vault/cloud_kms 未接线 | ENC_PROVIDERS 含 Vault/云 KMS 仅可选展示,实际加密恒走内置 AES-256-GCM;密钥表 encryption_method 恒 aes256_gcm |
| 手输 id 降级 | git-repos/environments 下拉失败时弹窗降级为数字输入,无输入校验(后端以 id 不存在报错兜底) |
| sealed-yaml 解析兜底脆弱 | openSealedYaml 依赖 content 等键命中,未命中时把整个对象 JSON 化展示(当前后端形态正常,属兜底路径) |
| 渲染/对比无语法高亮 | 渲染结果与 diff 值以 pre/纯文本展示,无 YAML 高亮与行内导航;diff 对象值 JSON 化展示 |
附录 A:页面文案速查(与 ConfigsPage.vue 逐字一致)
| 场景 | 源码取值/文案 |
|---|---|
| 页面标题 | Apollo 配置管理 |
| 顶部说明 | 配置中心(TAD-07):配置模板声明(raw_yaml / kustomize / helm)+ 按环境渲染与差异对比;密钥以 AES-256-GCM 加密存储(列表仅展示元数据,不落明文);加密配置内置主密钥自动生成。 |
| Tab | 配置模板 / 密钥 / 加密配置 |
| 模板按钮 | 新建配置模板 / 创建 / 保存 / 取消;必填校验 模板名称、Git 仓库(git_repo_id)与 git_path 必填;成功 配置模板创建成功(id=..) / 配置模板 #{id} 更新成功;失败 保存配置模板失败:{msg} |
| 模板行操作 | 渲染 / 对比 / 编辑 / 删除;confirm 确定删除配置模板 "{name}"(#{id})吗?;成功 配置模板已删除 |
| 模板类型下拉 | raw_yaml(原始 YAML) / kustomize / helm(Helm Chart) |
| Schema 校验 | schema 不是合法 JSON:{e.message};muted 配置渲染后按此 Schema 校验;留空则不校验。 |
| 渲染弹窗 | 标题 渲染配置模板 #{id}({name});环境标签 目标环境(environment_id)*;按钮 开始渲染 / 渲染中... / 取消 / 关闭;校验 请选择目标环境(environment_id);失败 渲染模板 #{id} 失败:{msg};时间 渲染时间: |
| 对比弹窗 | 标题 对比配置模板 #{id}({name});环境标签 环境 A(env_a)* / 环境 B(env_b)*;按钮 开始对比 / 对比中...;校验 请选择对比环境(env_a / env_b);失败 对比模板 #{id} 失败:{msg};一致 两环境配置一致,无差异。 |
| 新建密钥弹窗 | 标题 新建密钥(项目 #1);字段 名称(name)* / 明文值(plaintext)*;校验 密钥名称(name)与明文值(plaintext)必填;按钮 创建(加密落库) / 加密保存中...;成功 密钥创建成功(id=..)(已加密落库);muted 明文仅用于本次加密,后端以 AES-256-GCM 加密后存储,不会以明文返回。 |
| 密钥表说明 | 密钥以 AES-256-GCM 加密存储,列表仅展示元数据(名称 / 加密方式 / 密钥 ID / 创建时间),不返回明文;可通过"查看"获取 sealed-secret YAML(kind: EncryptedSecret)用于交付或导入。 |
| 密钥行操作 | 查看 / 轮换 / 删除;轮换 confirm 确定轮换密钥 "{name}"(#{id})吗?将生成新的 key_id 并重新加密。;删除 confirm 确定删除密钥 "{name}"(#{id})吗?该操作不可恢复。 |
| 查看弹窗 | 标题 Sealed Secret YAML:{name}(#{id});muted 该 YAML 为加密后的 sealed-secret(kind: EncryptedSecret),可安全交付/入库,不含明文。;按钮 关闭 / 复制;复制成功 sealed-secret YAML 已复制 |
| 加密配置 | 卡标题 加密配置(项目 #1);alert-info 内置 AES-256-GCM 主密钥由后端自动生成并持久化(适配 Windows 单机模式);加密后的敏感信息可安全存放(类似 Sealed Secrets),Agent 部署时解密注入应用环境变量或 K8s Secret。;字段 加密提供方(provider) / 更新时间(updated_at) / 说明(description);按钮 保存加密配置 / 保存中...;成功 加密配置已保存(provider:{resp provider}) |
| 加密提供方下拉 | AES-256-GCM(内置,主密钥自动生成) / AES-GCM(兼容别名) / HashiCorp Vault(可选) / 云 KMS(可选) |
| 空态 | 模板 暂无配置模板,点击"新建配置模板"添加(项目 #1)。;密钥 暂无密钥,点击"新建密钥"添加(项目 #1)。 |
| 下拉降级 placeholder | 仓库 仓库 ID(仓库列表加载失败时手输);环境 环境 ID(环境列表加载失败时手输) |