所属产品:LightApollo · 信息截止:2026-09-07(deep-rev)
1. 页面概览
1.1 是什么
「Git 仓库」是 LightApollo 的 GitOps 配置同步管理页(对应前端源码 action/web/src/views/apollo/GitReposPage.vue,对齐 TAD-02 GitOps)。它的职责是:把「仓库」里声明的配置,按「同步目标(sync target)」的定义同步成 LightApollo 的期望状态(DesiredState),落到指定环境,并检测实际状态与期望之间的漂移。页面以仓库为入口,一张表管仓库、一层展开管仓库下的同步目标,目标行再挂出同步 / 回滚 / 暂停 / 恢复 / 历史 / 漂移等操作。
需要先说清一个贯穿全局的实现前提:这里的「Git 仓库」是本地目录模拟。TAD-02 采用自研轻量实现,仓库 url 指向的是后端所在机器的磁盘目录(如 C:/configs/app-config),后端直接读目录里的文件,不真正走 git 协议拉取、也没有远端凭据交换;「commit」是由目录内容算出来的模拟 SHA(见 5.4)。因此本页所说的「同步」本质是「读目录 → 解析声明 → 在 hub 建期望状态」,与真实 GitOps 的 git 语义有差距,操作与演示不受影响。
页面在 LightApollo 发布链中的位置是「配置入口」:同步目标把仓库内 git_path 前缀目录下的配置文件(支持 k8s_manifest / helm / kustomize 三种声明类型)解析为期望状态,经过创建(draft)→(视同步策略)激活(active)后,期望状态就成为「部署与漂移」页的发布对象;目标环境正是「环境管理」页登记的环境(target_environment_id)。漂移策略(alert / auto_fix)决定发现差异后是仅告警还是自动调和。一句话:本页把「仓库内声明 → 目标环境的期望状态」这段 GitOps 链路做成可点按钮的界面,并留下同步历史与漂移事件供审计回溯。
本页涉及两种「状态」不要混淆:一是仓库/同步目标的登记状态(status:仓库 active/paused/error,目标 active/syncing/paused/error,徽标展示在列表);二是每次同步产出的同步历史行(SyncHistory:running → success/failed)。登记状态决定「同步引擎放不放行」,历史行则记录「某一次同步做成/做败了什么」。后面第 4、5 章所有按钮的成功与失败,最终都能落到这两类状态上被看到。
三种同步策略(sync_policy)的实际差异在 5.4 讲透,这里先给一句话印象:manual 策略只在被点「同步/刷新」时才跑一次;auto 策略除被手动/webhook 触发外,同步出的新声明还会走自动激活与安全门禁;scheduled 策略额外依赖目标上的 cron 定时表达式(sync_cron)到点自动触发。仓库级「同步间隔」字段(sync_interval_seconds)目前仅是记录,尚未接成真正的轮询触发(见 8 章)——不要指望填了它就会自动同步。
1.2 核心价值表
| 能力 | 说明 | 对应页面操作 |
|---|---|---|
| 仓库列表 | 展示登记的全部 Git 仓库:ID/名称/URL/默认分支/状态/同步间隔/最后同步时间 | 进入页面自动加载 |
| 仓库管理 | 新建/编辑/删除仓库,登记本地目录路径与认证方式等元数据 | 页头「新建仓库」与行操作「编辑」「删除」 |
| 测试连接 | 校验仓库 url 指向的本地目录存在且可读 | 行操作「测试连接」 |
| 刷新同步 | 对仓库下全部 active 同步目标各触发一次手动同步 | 行操作「刷新」 |
| 同步目标管理 | 行展开查看某仓库的同步目标,含新建/编辑/删除 | 「▸ 同步目标」行展开 + 子区按钮 |
| 手动同步 / 回滚 | 立即同步一次声明到目标环境;按最近一次同步结果回滚 | 目标行「同步」「回滚」 |
| 暂停 / 恢复 | 暂停后同步引擎拒绝该目标的同步(自动/webhook 都不再触发) | 目标行「暂停 / 恢复」 |
| 历史与漂移 | 弹窗查看同步历史(含 commit 与操作摘要)与漂移事件(期望/实际 diff) | 目标行「历史」「漂移」 |
1.3 一句话总结
Git 仓库页把仓库内 k8s_manifest / helm / kustomize 配置同步为指定环境的期望状态,并对每次同步留痕、对差异做漂移管理。
2. 访问入口
2.1 路由与菜单
| 项 | 值 |
|---|---|
| 路由 path | /apollo/git-repos |
| 路由 name | ApolloGitRepos |
| meta.title | Apollo Git 仓库 |
| 侧边栏位置 | ApolloLayout 侧边栏「Git 仓库」(位于「环境管理」之后、「制品管理」之前) |
| 前端源码 | action/web/src/views/apollo/GitReposPage.vue |
| API 客户端 | action/web/src/api/apolloClient.js |
| 路由注册 | action/web/src/router/index.js:import 于第 23 行,children 注册于第 427~432 行 |
2.2 认证与权限
- 路由
meta: { title: 'Apollo Git 仓库', requiresAuth: true }:未登录访问/apollo/git-repos被前端路由守卫拦截并跳转/apollo/login;守卫对/apollo前缀优先使用apollo_token,缺失时回退aip_token。 - 后端按权限点鉴权,分两个资源域:Git 仓库(
git-repo:read/write/execute)、同步目标(sync-target:read/write/execute),权限点常量见access/permissions.go的PermGitRepoRead/Write/Execute、PermSyncTargetRead/Write/Execute。读写分离:看仓库/目标/历史/漂移列表只需对应Read;新建/编辑/删除要对应Write;同步/回滚/暂停/恢复/刷新/测试连接等动作要对应Execute。没有 execute 权限的用户点这些按钮会弹「无权限执行该操作」类错误(apolloClient 对 403 的统一提示),操作不写库、不产生历史行。 - 端点归属两套路由组:
/projects/:pid/git-repos与/projects/:pid/sync-targets(带项目前缀,走项目成员校验),/git-repos/:id、/sync-targets/:id、/sync-history/:id等(不带项目前缀,仅做 token + 权限点校验)。本页固定用pid=1(default 项目,PROJECT_ID 常量)。 - 401:拦截器清除本地 token 并跳登录页(已在登录页则只报错)。403:
projectScopeAccessGuard对/projects/:pid/...做成员归属校验:super admin 直通,非 super 须是项目成员,否则 403AUTHZ_ERROR;执行类端点命中策略 deny 也会以 403/AUTHZ_ERROR语义返回(见 5.4 安全门禁 deny 分支)。
2.3 端口与 API 前缀
- Apollo 后端端口 18082;Vite 将
/apollo-api前缀代理到 18082。 - 客户端
apolloClient.js:baseURL: '/apollo-api/v1'、timeout 30000 ms、请求/响应拦截器行为同环境管理页(解包{code:0,data}信封)。 - 完整请求示例:
GET /apollo-api/v1/projects/1/git-repos、POST /apollo-api/v1/sync-targets/3/sync。
3. 界面布局
┌───────────────────────────────────────────────────────────────┐
│ Apollo Git 仓库与同步管理 [ 新建仓库 ] │
│ [ 说明 alert:Git 仓库为"本地目录"模拟… ] │
│ [ 操作结果提示条(可关闭)] │
│ [ 仓库列表卡片 ] │
│ ▸同步目标 ID 名称 URL(本地目录) 默认分支 状态 同步间隔 最后同步 操作 │
│ 操作列:测试连接 / 刷新 / 编辑 / 删除 │
│ ┌─ 同步目标子区(仓库行展开,每仓库一块)───────────────────────┐ │
│ │ 同步目标(N)— 仓库 {name} [ 新建同步目标 ] │ │
│ │ ID Git路径 配置类型 同步策略 漂移策略 目标环境 状态 操作 │ │
│ │ 操作列:同步/回滚/暂停|恢复/历史/漂移/编辑/删除 │ │
│ └──────────────────────────────────────────────────────────────┘ │
│ [ 新建/编辑仓库弹窗 ] [ 新建/编辑同步目标弹窗 ] │
│ [ 同步历史弹窗 ] [ 漂移事件弹窗 ] │
└───────────────────────────────────────────────────────────────┘
| 板块 | 职责 |
|---|---|
| 说明 alert | 常驻说明本页为本地目录模拟与同步机制,帮助操作者理解 url 语义 |
| 操作结果提示条 | 展示成功/失败/信息提示,右侧「关闭」可收起 |
| 仓库列表卡片 | 加载中「加载中...」;空数据「暂无 Git 仓库,点击"新建仓库"添加(项目 #1,url 填本地目录路径)。」;有数据渲染表格,首列是展开按钮 |
| 同步目标子区 | 点「▸ 同步目标」展开;每仓库一块,含标题行与操作列,展开结果有前端缓存 |
| 弹窗区 | 仓库表单、同步目标表单、同步历史(只读表格)、漂移事件(只读表格) |
页面横向较宽:仓库表一共 8 列(含最左侧展开钮占位与最右操作列),首列收起时只有展开符号、无其余文案;同步目标子区使用缩进块嵌套在仓库行内,看起来像「表套表」。两级表的行高与列宽由仓库行的展开状态决定——展开后该仓库行下会插入一块独立区域,不改变其它仓库行的排版,因此仓库很多时页面会很长(无分页,见 8 章)。
进入页面的初始请求序列(见 4.14):并行发起两个只读请求——拉仓库列表(渲染主体)+ 拉环境列表(仅备用,喂给同步目标弹窗的下拉),两者互不阻塞;仓库列表失败会红条提示且整页无数据,环境列表失败则静默降级。首屏没有任何写请求。
4. 交互元素
4.1 说明 alert 与全局 busy
顶部常驻一条 alert alert-info,文字为:「Git 仓库为"本地目录"模拟(url 指向磁盘目录,TAD-02 自研轻量实现);同步目标将仓库内 git_path 目录下的配置(k8s_manifest / helm / kustomize)同步到目标环境,并按漂移策略检测差异。」这条提示不可关闭,目的是让每个进入页面的人先知道 url 填的是目录。
页面互斥位 busy 比较特殊:它存的是字符串操作键而不是布尔位(空闲为 '',进行中为 saveRepo / test-{id} / sync-{id} 等),所有按钮都按 !!busy 禁用,所以任意一个操作进行中都会锁住全页按钮。多按钮连点被结构性地禁止;某一请求挂起(30s 超时)期间页面不可操作,超时后恢复。
4.2 仓库列表与状态徽标
| 控件 | 含义 | 边界与细节 |
|---|---|---|
| 「▸ / ▾ 同步目标」链接按钮 | 展开/收起该仓库的同步目标子区(title 提示「展开同步目标/收起同步目标」) | 首次展开去拉取该仓库同步目标;再次进入直接用缓存不再请求(见 4.7) |
| 状态徽标 | 仓库状态:active→绿、paused→灰、error→红,未知回退灰并显示 unknown | 徽标文案取 repo.status || 'unknown' |
| 同步间隔列 | formatInterval(sync_interval_seconds):秒转中文 | 小于 60 显示「N 秒」;小于 3600 显示「N 分钟」;否则「N.N 小时」;空或 ≤0 显示 - |
| 最后同步列 | formatTime(repo.last_sync_at):T 换空格、去 Z、截秒 | 空显示 - |
| URL 列 | 等宽字体 + 超长省略(url-cell,max-width 260px),悬停看全 | url 是本地目录路径,填错是测试连接失败的主因 |
仓库行的操作列四按钮:测试连接 / 刷新 / 编辑 / 删除(见 4.4~4.6)。
仓库表各列取值:ID 纯数字;名称列无特殊样式、非等宽;URL(本地目录)列等宽字体加省略;默认分支列为 repo.default_branch || '-';状态徽标见上表;同步间隔由 formatInterval 换算;最后同步为 formatTime。仓库表不带分页与搜索,列表一次性渲染;repo.name 在删除确认与操作提示里会原样拼接进文案,特殊字符会影响可读性但不影响功能。
仓库列表本身没有「刷新列表」按钮——唯一的主动刷新手段是执行一次仓库级写操作(新建/编辑/删除/刷新),成功后自动重新拉取。若别人在后端/其它页面改了仓库,本页停留期间不会感知,需通过上述任一操作或整页重进触发重新加载。这一「无轮询、写后刷新」约定是全页一致的设计(见 4.14)。
4.3 新建 / 编辑仓库弹窗
弹窗标题区分模式:新建为「新建仓库(项目 #1)」,编辑为「编辑仓库 #{id}(项目 #1)」。提交创建走 POST /projects/1/git-repos(成功提示「仓库创建成功(id=N)」),编辑走 PUT /git-repos/{id}(成功提示「仓库 #{id} 更新成功」),成功后关闭弹窗并刷新仓库列表。
表单字段表
| 字段 | 类型 | 必填 | 校验规则 | 默认 | 保存逻辑 |
|---|---|---|---|---|---|
| 名称(name) | 文本 | 是 | required + 前端非空校验,空则提示「名称(name)与 URL 必填」 | 空 | trim 后提交;项目内唯一(后端按项目判重) |
| URL(本地目录路径) | 文本 | 是 | 同上 | 空 | trim 后提交;placeholder「如 C:/configs/app-config 或 ./repos/app-config」,输入框下方有灰字说明「TAD-02 自研轻量实现:url 指向本地磁盘目录,由后端直接读取。」 |
| 认证方式(auth_type) | 下拉 | 否 | 三选项 | none | 选项:无需认证(none)/ SSH Key(ssh_key)/ 账号密码(basic)——仅记录,本地目录模拟不真正鉴权(见 8 章) |
| 默认分支(default_branch) | 文本 | 否 | 无 | 留空则保存时按 main 提交 | placeholder「如 main」 |
| 同步间隔(sync_interval_seconds) | 数字 | 否 | min=0;仅当填了正整数才进 payload | 留空 | placeholder「留空则不按周期自动同步」;>0 才带上,否则省略(后端 0=仅 webhook 语义) |
编辑态预填当前值(数字字段转字符串回显);删除与取消照旧。注意一个不对称:新建/编辑都能改 url(即换目录),编辑仓库与编辑同步目标不同——仓库的归属不可变,但 url 可换。
提交细节(与 handleSaveRepo / buildRepoPayload 逐字对应):点保存先走前端校验,name 与 url 任一为空立即红条「名称(name)与 URL 必填」并不发请求;通过后把按钮置 busy(saveRepo),先 trim 再组包。auth_type 恒带默认 none;default_branch 前端兜底补 main(repoForm.default_branch || 'main')——因此即便用户留空,发给后端的请求体里 default_branch 也是 main,与后端 defaultString(req.DefaultBranch, "main") 双保险一致;sync_interval_seconds 只有 Number(...) > 0 才写进请求体,填 0 或不填都等价于「后端字段保持 0(0=不按周期)」。后端 name 判空/trim、url 判空/trim 有同名校验(name 不能为空/url 不能为空),与前端提示文案不同源但语义一致。名称冲突(同项目同 name,唯一索引 idx_git_repo_project_name)在创建时以 409/冲突语义报错并红条提示;创建响应体直接是仓库对象(okStatus(201, repo)),前端用 data?.id || data?.repo?.id 双形态兜底取 id。
编辑仓库的局部更新语义:后端 PUT /git-repos/:id 用指针字段,只有请求体里出现的字段才会被更新(handleUpdateGitRepo 逐字段判 != nil)。前端编辑态把 5 个表单字段全部回填进 repoForm,因此点击保存会把当前表单值整体提交——不存在「只改一项」的顾虑;但其它字段(如 status、webhook_secret)不会被本页表单改到。编辑仓库与编辑同步目标类似但更宽松:仓库编辑允许改 name/url/auth_type/default_branch/sync_interval_seconds,且不校验 name 是否与其它仓库撞名之外的约束(url 指向的目录暂不要求此刻可读)。
4.4 测试连接
| 控件 | 触发后端调用 | 成功判定 | 细节 |
|---|---|---|---|
| 按钮「测试连接」 | POST /git-repos/{id}/test-connection | success===true / ok===true / connected===true | 后端返回 {id, url, connected:true} 或直接报错(目录不存在/不可读);前端提示「仓库 "{名称}" 连接测试:{message}」,message 缺失按判定回退「连接成功/连接失败」 |
后端 TestConnection 调用 checkReadableDir 校验 url 指向的目录:目录不存在(404)、不是目录、不可读都会报错并在提示条展示。操作期间按钮禁用;成功后不会自动刷新列表(无数据变化)。
「测试连接」本质是只读探活,不做目录快照也不写任何历史;它在新建仓库前最有用——先在表单里把 url 填好保存,再用测试连接验证目标目录真实存在可读,避免把错误路径带进后续所有同步。该按钮不校验目录内是否有符合约定的声明文件,目录存在即可读即返回 connected:true——所以「连接成功」不代表「同步一定能成功」(同步还依赖文件命名约定与声明合法性,见 5.4)。
4.5 刷新(触发同步)
| 控件 | 触发后端调用 | 细节 |
|---|---|---|
| 按钮「刷新」 | POST /git-repos/{id}/refresh | 后端对仓库下全部 active 同步目标各触发一次手动同步(trigger=manual);响应为 {repo_id, triggered:[...], failed:[...]}。前端提示「仓库 "{名称}" 已触发刷新同步(...)」,消息优先取 data.message,其次 状态:{data.status},都没有显示「刷新成功」,随后刷新仓库列表 |
刷新不等同于把仓库状态刷新为同步完成,而是「按一下,把该仓库所有活跃目标都同步一遍」的批量入口;单个目标想精细操作应去子区用「同步」。若仓库没有 active 同步目标,triggered 为空数组,前端仍提示刷新成功——这是正常现象,不是失败。
后端逐目标语义(对照 handleRefreshGitRepo):先按仓库 id 解析全部同步目标(ResolveSyncTargetsForRepo → ListByRepo),然后逐个串行执行 Sync(trigger=manual),仅跳过 status 非 active 的目标。某个目标失败不会中断后面的目标:失败项进 failed:[{target_id,error}],成功项进 triggered:[{target_id,history_id,status}]。注意这与子区「同步」不同:刷新不管目标的 sync_policy,manual/auto/scheduled 一律触发,只认 status=active。每次刷新后端都会写一条审计事件(action=GIT_REPO_REFRESH,payload 含 triggered/failed 计数)。前端拿到响应后只取 data.message 或 data.status 拼提示语,不会逐个展示 triggered/failed 明细——想确认每个目标本次结果应去对应目标的「历史」弹窗。
4.6 删除仓库
| 控件 | 触发后端调用 | 细节 |
|---|---|---|
| 按钮「删除」(危险) | DELETE /git-repos/{id} | 二次确认:「确定删除仓库 "{名称}"(#{id})吗?关联的同步目标可能一并删除。」;成功后提示「仓库已删除」并收起展开区、刷新列表 |
后端删除是事务级级联:先删该仓库每个同步目标的全部同步历史,再删这些同步目标,最后删仓库,返回 {id, deleted:true}。所以确认框那句「关联的同步目标可能一并删除」不只是提醒——它实际删了目标及其全部历史,是整条 GitOps 链路的物理清理,不可恢复。删除后仓库名可复用。
4.7 同步目标子区(行展开)
点首列「▸ 同步目标」展开子区。加载该仓库的同步目标用的是项目级接口:GET /projects/1/sync-targets 拉全部目标,前端按 git_repo_id 过滤(field(t,'git_repo_id') 做字符串比较),因此展开结果的正确性依赖接口返回完整列表。展开结果缓存在 syncMap[repo.id],首次展开拉取后,再次展开/收起不重新请求;但新建/同步/回滚/暂停/恢复/删除目标后都会以 force 语义强制重新拉取该仓库的目标列表(见 4.9/4.10),让缓存保持新鲜。
子区标题「同步目标(N)— 仓库 {name}」,右上角「新建同步目标」。列表列为 ID / Git 路径 / 配置类型 / 同步策略 / 漂移策略 / 目标环境 / 状态 / 操作。
| 列 | 取值规则 |
|---|---|
| Git 路径 | t.git_path || '-'(等宽字体) |
| 配置类型 | 中文标签:K8s Manifest(k8s_manifest)/ Helm Chart(helm)/ Kustomize(kustomize) |
| 同步策略 | manual→「手动」、auto→「自动」、scheduled→「定时({cron 或 -})」 |
| 漂移策略 | alert→「通知」、auto_fix→「自动修复」,未识别原样显示 |
| 目标环境 | 优先在已加载环境下拉里找 id:找到显示「{name}(#{id})」,找不到显示「#{id}」(纯数字兜底) |
| 状态 | t.status || 'unknown' 徽标:active→绿、syncing→蓝、paused→灰、error→红 |
目标行操作列七个按钮:同步 / 回滚 / 暂停(或恢复)/ 历史 / 漂移 / 编辑 / 删除。「暂停/恢复」按钮文案由当前状态决定:paused 显示「恢复」,否则显示「暂停」。
子区缓存与强制刷新约定:展开目标列表的请求 key 是 syncMap[repo.id](数组缓存)。页面初次进入时不会预拉任何目标列表——只有某仓库首次被展开才发它的第一个请求;之后该仓库的目标列表只在「新建/编辑/同步/回滚/暂停/恢复/删除」任一成功或失败触发 force 重拉时更新(见 4.14)。删除仓库成功后,对应展开区被强制收起、缓存清掉,避免悬空展示已删除仓库的目标。若某仓库从未展开过,页面关闭前不会产生该仓库目标的任何请求。
子区列宽与整行状态:目标行「状态」徽标取值 t.status || 'unknown',与仓库徽标用同一套样式但配色含义不同(目标多了 syncing 蓝);「Git 路径」列等宽字体 + 省略,与仓库 URL 列一致。目标行没有单独的空态占位——空列表时子区只显示标题行(同步目标(0)— 仓库 {name}),下方不渲染表格,也没有「暂无」提示文案。新建同步目标成功后新目标会出现在该仓库子区(按 force 重拉)。
4.8 新建 / 编辑同步目标弹窗
弹窗标题区分模式:「新建同步目标」/「编辑同步目标 #{id}」。创建提交 POST /projects/1/sync-targets(成功提示「同步目标创建成功(id=N)」),编辑提交 PUT /sync-targets/{id}(成功提示「同步目标 #{id} 更新成功」),成功后关闭弹窗并强制刷新所属仓库的目标列表。
表单字段表
| 字段 | 类型 | 必填 | 默认 | 校验规则 | 保存逻辑 |
|---|---|---|---|---|---|
| Git 仓库(git_repo_id) | 下拉 | 是 | 新建时=所属仓库 id;编辑时=当前值 | 编辑态禁用(不可迁移仓库);无值且 git_path 空会拦 | 原样提交 |
| Git 路径(git_path) | 文本 | 是 | 空 | 前端非空校验,空则提示「Git 仓库与 git_path 必填」 | trim 后提交;placeholder「如 manifests/prod 或 charts/app」 |
| 配置类型(config_type) | 下拉 | 是 | k8s_manifest | 选项固定三个 | 原样提交 |
| 同步策略(sync_policy) | 下拉 | 是 | manual | manual/auto/scheduled | 选 scheduled 才显示并提交 sync_cron |
| 漂移策略(drift_policy) | 下拉 | 否 | alert | alert / auto_fix;后端只认这两个值,旧值不识别 | 空则按 alert 提交 |
| 目标环境(target_environment_id)* | 下拉或数字输入 | 是 | 有环境下拉时取第一个环境 id | 空则提示「目标环境(target_environment_id)必填」 | Number() 强制转数字后提交 |
| 定时表达式(sync_cron) | 文本 | 仅 scheduled | 空 | 选 scheduled 才出现;填了才提交 | placeholder「如 */30 * * * *(每 30 分钟)」 |
目标环境下拉的数据源是进入页面时并行拉取的 GET /projects/1/environments;该请求失败不阻塞页面(弹窗自动降级成数字输入框,placeholder「环境 ID(环境列表加载失败时手输)」)。下拉选项文案「{环境名}(#{id})」。此处的必填校验注意顺序:先查 git_repo_id/git_path,再查 target_environment_id,两条提示文案不同。
默认值语义(对照 openCreateTarget / openEditTarget):新建目标时 git_repo_id 默认填所属仓库的 id(因为新建入口在某个仓库的子区里,仓库是已知的);config_type 默认 k8s_manifest、sync_policy 默认 manual、drift_policy 默认 alert、sync_cron 空。target_environment_id 默认取环境列表第一项(environments[0]?.id)——若进入页面时环境列表已加载成功,新建弹窗打开即有默认目标环境;若环境列表为空或加载失败,该默认值为 null,必须手选/手输,否则前端校验直接红条拦截。编辑目标时一律从行对象回填(git_path/config_type/sync_policy/sync_cron/drift_policy 都取当前值),目标环境用 field(t,'target_environment_id','target_environment') 双形态兜底取值。创建成功后前端提示取 data?.id || data?.sync_target?.id 双形态 id。
提交细节:保存按钮 busy 键为 saveTarget。前端校验顺序为 ①git_repo_id 与 git_path 任一空 → 红条「Git 仓库与 git_path 必填」;②target_environment_id 空(含 '')→ 红条「目标环境(target_environment_id)必填」。通过后组包:config_type、sync_policy、drift_policy 原样提交;target_environment_id 经 Number() 转数字;sync_cron 仅当 sync_policy=scheduled 且填写才带上(scheduled 未填 cron 会触发后端/调度告警,见 7 章第 5 条)。创建走 POST /projects/1/sync-targets,后端会校验 git_repo_id 属于当前项目(不属于则 400)与 git_path 非空,config_type/sync_policy/drift_policy 缺省分别为 k8s_manifest/auto/alert——注意后端 sync_policy 缺省是 auto,而前端新建默认是 manual,两者不一致时以后端为准的前提是前端每次都显式提交 manual(确实如此,所以实际生效的是 manual,除非后端直连调用未带该字段)。
4.9 同步与回滚
| 控件 | 触发后端调用 | 二次确认 | 细节 | |
|---|---|---|---|---|
| 按钮「同步」 | POST /sync-targets/{id}/sync | 无 | 结果提示「同步目标 #{id} 同步成功:commit {sha} | {summary}」(失败则「同步失败」,红条) |
| 按钮「回滚」 | POST /sync-targets/{id}/rollback | 有:「确定对同步目标 #{id} 执行回滚吗?将恢复上一次成功同步的配置。」 | 提示「同步目标 #{id} 回滚成功:commit ... | ...」 |
两者共用 parseSyncResult(data) 解析响应:识别 data.history/sync/record 嵌套或平铺记录,从记录里取 status / git_commit_sha / operation_summary / summary / message,再按 status 白名单(success/succeeded/synced/completed/ok)判定成功与否。但后端真实响应嵌套在 sync_history(回滚为 rollback_history)键下,不在前端识别列表内,导致 commit_sha 与 summary 恒取不到,提示永远显示「commit -」;状态判定读的是顶层 data.status(存在),所以成功/失败红绿提示仍是准的——这是本页最值得注意的前端缺陷(见第 8 章 d)。同步/回滚成功后都会强制刷新所属仓库目标列表。
点「同步」后端做了什么(对照 handleSyncTargetSync + SyncEngine.Sync):先校验同步目标存在且 status=active(非 active 返回 400「sync target %d 状态非 active」)→ 校验所属仓库存在且 active → 立即落一条 status=running 的同步历史 → 快照仓库目录 → 解析声明 → 逐条登记期望状态(draft)/幂等复用 → auto 策略走自动激活与门禁 → 回写历史 success/failed + commit + OperationSummary → 刷新仓库 last_sync_at。同步是同步阻塞执行:页面按钮会一直处于忙碌直到后端完成整条链(一个请求内跑完),响应里的 sync_history 就是本次完整结果;历史弹窗里能看到 running 行只在极短窗口存在,正常情况打开时已是 success/failed。
失败时的双重留痕:无论哪一步失败,历史行都会被置为 failed(含错误信息)并记录 finished_at,同时后端返回错误给页面红条展示——所以一次失败同步会在「提示条」和「历史弹窗」两处都留下线索,历史弹窗的 failed 行信息更完整(含失败原因)。门禁 deny 类错误(403 AUTHZ_ERROR)会原样透传到前端(后端 fail 闭包保留 ierr 的错误码与 HTTP 状态,避免折叠成 500),因此红条里的文案能区分「策略拒绝」与「系统故障」。
4.10 暂停 / 恢复
| 控件 | 触发后端调用 | 细节 |
|---|---|---|
| 按钮「暂停」/「恢复」 | POST /sync-targets/{id}/pause / .../resume | 文案随 t.status === 'paused' 切换;响应 {id, status: 'paused'|'active'};提示「同步目标 #{id} 已暂停(状态:paused)」(或「已恢复(状态:active)」),随后强制刷新目标列表 |
暂停是同步引擎层面的闸门:暂停后目标 status 为 paused,手动 sync、auto/webhook 触发都不会再执行(引擎按状态校验),列表徽标变灰。
4.11 同步历史弹窗
行内「历史」打开,标题「同步历史:#{id}」,弹窗内「加载中...」/「暂无同步历史。」两态。接口 GET /sync-targets/{id}/history 直接返回数组(后端已按最新在前反转)。表格列:触发类型 / 状态 / Commit / 提交信息 / 操作摘要 / 时间,映射 h.trigger_type / h.status / h.git_commit_sha / h.git_commit_msg / h.operation_summary / formatTime(h.created_at),空值显示 -。状态徽标:success→绿、failed/error→红、running/pending→蓝。触发类型取值见 5.4(manual/webhook/scheduled/rollback 等)。只读弹窗,无任何写操作。
4.12 漂移事件弹窗
行内「漂移」打开,标题「漂移事件:#{id}」,空态提示「暂无漂移,目标环境与配置一致。」接口 GET /sync-targets/{id}/drift 返回 {sync_target_id, count, events},前端做双形态兼容(数组直用,{events:[...]} 取 events)。表格列:资源 / 类型 / 期望/实际 / 状态 / 时间,字段用别名兜底读取(resource_name/name/resource、resource_kind/kind/config_type、desired/expected、actual/current、status/drift_status、detected_at/created_at),「期望/实际」两行灰字展示,都没有则回退展示 message/description/detail 或整个对象。状态徽标:drifted/detected→红、resolved/clean→绿、ignored→灰,未知回退红并显示 drifted。只读弹窗。
4.13 删除同步目标
| 控件 | 触发后端调用 | 细节 |
|---|---|---|
| 按钮「删除」(危险) | DELETE /sync-targets/{id} | 二次确认:「确定删除同步目标 #{id} 吗?」;成功提示「同步目标已删除」,强制刷新所属仓库目标列表 |
后端删除目标事务内级联清理其全部同步历史后删目标(返回 {id,deleted:true}),同步历史随之消失。删除单个目标不影响仓库与其它目标。
4.14 操作结果提示与自动刷新约定
- 错误信息兜底链:
err.response.data.message → err.response.data.error → err.message → '未知错误';成功/失败提示在顶部 alert 区分绿/红/蓝。 - 刷新约定:进入页面并行拉仓库列表 + 环境下拉数据(
fetchEnvironments失败静默,仅影响同步目标弹窗降级);页面无轮询。 - 仓库写操作(新建/编辑/删除/刷新)成功后刷新仓库列表;同步目标写操作(新建/编辑/同步/回滚/暂停/恢复/删除)成功后按所属仓库强制刷新该目标列表;「历史」「漂移」弹窗数据在打开时拉取,不随后台变化自动更新,重新打开即刷新。
5. 后端关联
5.1 API 客户端
与其它 Apollo 页共用 action/web/src/api/apolloClient.js:baseURL '/apollo-api/v1'、timeout 30000、请求拦截器附 apollo_token(回退 aip_token)Bearer 头;响应拦截器解包 {code:0,...} 信封,401 清 token 跳登录、403 统一 alert。后端成功信封形态:{code:0, message:"ok", data, request_id}。
5.2 端点表
页面实际用到(前缀 /api/v1,经 Vite /apollo-api 到 18082;括号为 server.go 权限点):
| 方法 | 路径 | 请求体 | 页面触发点 |
|---|---|---|---|
| GET | /projects/:pid/git-repos | - | 加载仓库列表(PermGitRepoRead) |
| POST | /projects/:pid/git-repos | {name, url, auth_type?, default_branch?, sync_interval_seconds?, ...} | 新建仓库(PermGitRepoWrite,201) |
| PUT | /git-repos/:id | {name?, url?, auth_type?, default_branch?, sync_interval_seconds?, ...} | 编辑仓库(PermGitRepoWrite) |
| DELETE | /git-repos/:id | - | 删除仓库(PermGitRepoWrite) |
| POST | /git-repos/:id/test-connection | - | 测试连接(PermGitRepoExecute) |
| POST | /git-repos/:id/refresh | - | 刷新同步(PermGitRepoExecute) |
| GET | /projects/:pid/sync-targets | - | 展开子区拉目标列表(PermSyncTargetRead) |
| POST | /projects/:pid/sync-targets | {git_repo_id, git_path, config_type?, target_environment_id, sync_policy?, sync_cron?, drift_policy?, ...} | 新建同步目标(PermSyncTargetWrite,201) |
| PUT | /sync-targets/:id | {git_path?, config_type?, target_environment_id?, sync_policy?, sync_cron?, drift_policy?, ...} | 编辑同步目标(PermSyncTargetWrite) |
| DELETE | /sync-targets/:id | - | 删除同步目标(PermSyncTargetWrite) |
| POST | /sync-targets/:id/sync | - | 手动同步(PermSyncTargetExecute) |
| POST | /sync-targets/:id/rollback | - | 回滚(PermSyncTargetExecute) |
| POST | /sync-targets/:id/pause、/resume | - | 暂停/恢复(PermSyncTargetExecute) |
| GET | /sync-targets/:id/history | - | 历史弹窗(PermSyncTargetRead) |
| GET | /sync-targets/:id/drift | - | 漂移弹窗(PermSyncTargetRead) |
| GET | /projects/:pid/environments | - | 同步目标弹窗的目标环境下拉(PermEnvRead) |
后端另有 GET /sync-history/:id(单条历史详情,PermSyncTargetRead)与公开 Webhook 端点(见 5.4)本页未直接用。环境页的 resources/applications 同理不在本页。
5.3 响应结构示例
新建仓库成功(201,信封解包后的 data):
{
"id": 3,
"project_id": 1,
"name": "app-config-repo",
"url": "C:/configs/app-config",
"auth_type": "none",
"default_branch": "main",
"sync_interval_seconds": 0,
"status": "active",
"webhook_secret": "",
"last_sync_at": null,
"created_at": "2026-09-07T09:00:00Z",
"updated_at": "2026-09-07T09:00:00Z"
}
字段说明:url 是本地目录路径;auth_type 后端枚举为 none/ssh/https_token(与前端下拉值不完全一致,见 8 章);sync_interval_seconds=0 表示不按周期轮询(注释语义 0=仅 webhook);last_sync_at 每次成功同步后被刷新;webhook_secret 为可选的 Webhook 验签密钥。列表接口 GET /projects/1/git-repos 返回该对象的数组。
手动同步成功响应(注意嵌套键):
{
"sync_target_id": 9,
"status": "success",
"sync_history": {
"id": 15,
"sync_target_id": 9,
"trigger_type": "manual",
"git_commit_sha": "3f1c2a9d4b07",
"git_commit_msg": "gitops sync 2026-09-07T10:00:00+08:00",
"status": "success",
"operation_summary": "[{\"name\":\"web-app\",\"app\":\"web-app\",\"version\":\"v1.2.0\",\"desired_state_id\":41,\"status\":\"created\"}]",
"started_at": "2026-09-07T02:00:00Z",
"finished_at": "2026-09-07T02:00:01Z",
"created_at": "2026-09-07T02:00:01Z"
}
}
字段说明:status 为同步结果(success/failed);sync_history 为完整历史行,含本次 commit(16 位模拟 SHA)、提交信息与 operation_summary(本次同步逐声明的结果项数组,元素含 name/app/version/desired_state_id/status=created|reused)。回滚响应结构对称:{sync_target_id, status, rollback_history, based_on_history_id}。前端 parseSyncResult 只识别 data.history/sync/record,不读 sync_history/rollback_history,因此 commit 与 summary 提示恒为 -(缺陷 d)。 而同步历史弹窗直接渲染 history 行(h.git_commit_sha 等),不受此缺陷影响。
测试连接成功 / 刷新同步响应:
{ "id": 3, "url": "C:/configs/app-config", "connected": true }
{
"repo_id": 3,
"triggered": [{"target_id": 9, "history_id": 15, "status": "success"}],
"failed": []
}
刷新同步响应中 triggered 列出被触发的目标与各自的 history_id/status,failed 列出失败项(含 error);没有 active 目标时两者都为空数组。漂移响应为 {sync_target_id, count, events},events 为 hub 漂移事件对象数组(含资源名/类型/期望/实际/状态/时间等,字段见 4.12 的别名兜底)。
5.4 关键机制
本地目录模拟与 commit 语义。仓库不含真实 git 逻辑:localrepo 用 checkReadableDir 校验目录存在且可读;每次同步前对目录做 Snapshot(列出条目),生成的 commit 是 SimulateCommitSHA 按内容树与时间戳算出的 16 位十六进制串,提交信息固定形如 gitops sync <RFC3339>。目录另有 fsnotify Watch 变更监听 + PollChanges 轮询兜底。因此「测试连接」「同步」「commit」全部围绕磁盘目录展开,url 写错或目录权限不足是测试连接失败的根因。
同步引擎主链(SyncEngine.Sync)。一次手动/自动同步按序执行:① 校验同步目标与所属仓库状态(非 active 拒绝);② 写一条 status=running 的同步历史;③ 对仓库目录做 Snapshot,ParseManifests 在 git_path 前缀目录下找声明文件——文件命名约定为 desired-state.yaml/.yml、declaration.yaml/.yml,或顶层 kind: DesiredState 的 yaml;④ 逐声明在 hub 创建期望状态(draft),同版本已存在时按幂等复用(409 冲突视为已存在而复用);⑤ 若同步策略为 auto 且本次新建了 draft 且内容已具备过审条件,执行安全门 securityGate.EvaluateDeployment——deny 会使整次同步失败(403 AUTHZ_ERROR),soft 的审批门 pending 则放行不阻塞;⑥ Activate 把 draft 置为 active 期望状态;⑦ 完成历史(status=success,写入 OperationSummary 与 commit),刷新仓库 last_sync_at。任一步失败会留下 status=failed 的历史行并终止。也就是说,页面上的一次「同步」可能创建出多个期望状态(每个声明一个),它们会出现在「部署与漂移」页。
回滚语义。POST /sync-targets/:id/rollback 先取该目标最近一条同步历史(ListByTarget 按 id 升序,末条即最近一次),交给 SyncEngine.Rollback 基于该历史的 OperationSummary 构造「回到上一次声明版本」的新 draft 并激活,落一条 trigger=rollback 的新历史返回。回滚不是把状态字段改回去,而是「又做了一次新同步」,因此历史里能看到链条式的新行。无任何历史可回滚时后端返回 400 语义错误(页面会走 catch 分支红条提示)。
调度与触发方式。同步触发类型(trigger_type)包括 manual(手动/刷新按钮)、webhook(仓库 Webhook 回调)、scheduled(cron 定时)、rollback(回滚)、drift_fix(漂移自动修复)等,历史弹窗的「触发类型」列即展示该字段。scheduled 策略的目标由 StartScheduler 用 robfig/cron 注册定时任务(表达式来自 sync_cron,为空或非法会被跳过并告警);repo 级 sync_interval_seconds 注释语义为轮询间隔(0=仅 webhook)。Webhook 端点是公开端点(无需登录):POST /api/v1/webhooks/git/:repo_id 触发该仓库全部 active 且 auto/scheduled 策略的目标(trigger=webhook);当仓库配置了 webhook_secret 时强制要求请求头 X-LightApollo-Signature 携带 HMAC-SHA256(body, secret) 的 hex,缺失或不匹配返回 401——未配置 secret 的仓库不校验(显式降级)。
漂移与调和。漂移检测作用于「期望状态(hub)vs 实际状态」:GET /sync-targets/:id/drift 从该目标同步历史的 OperationSummary 里收集期望状态 id,逐 id 查询 hub 漂移事件仓储聚合返回。漂移策略 alert(通知)只告警不动作;auto_fix(自动修复)会触发漂移自动调和,把实际状态拉回期望。历史 OperationSummary 为空(从未成功同步)的目标不会有漂移事件来源。
6. 权限与安全
- 认证分层:列表/详情/历史/漂移读操作要求
PermGitRepoRead/PermSyncTargetRead;仓库与目标的写操作要求对应Write;同步/回滚/暂停/恢复/刷新/测试连接等执行类一律要求PermSyncTargetExecute/PermGitRepoExecute。 - 删除即级联:删除仓库(DELETE /git-repos/:id)事务内级联删除各目标的历史、目标与仓库;删除目标也先删其全部历史。前端均有 confirm 二次确认;这是整条 GitOps 链路的物理清理,谨慎操作。
- auto_fix 双刃:漂移策略选 auto_fix 后后端会自动发起调和(重放期望状态),生产环境改动前建议先看漂移弹窗里期望/实际差异,确认无误再启用。
- Webhook 公开端点风险:webhook 无需登录即可命中同步,是刻意设计的开放入口;配置
webhook_secret后强制 HMAC 验签可堵住伪装触发,未配 secret 属显式降级(来源与处理逻辑见 gitops_handlers.go handleGitWebhook)。 - 项目级隔离:项目作用域资源经
projectScopeAccessGuard校验成员归属;页面固定 project 1(default 项目)。
7. 常见问题与排错
- 测试连接提示失败:原因是仓库 url 指向的本地目录不存在、不是目录或后端进程不可读(
checkReadableDir失败,错误含 NOT_FOUND/非目录/不可读等语义)。处理:核对 url 填的是磁盘绝对或相对路径、目录确实存在且对后端进程可读;修改后在编辑弹窗保存再测。 - 点「同步」红条失败,历史里留下 failed 行:原因是 git_path 下没有匹配命名的声明文件(
desired-state.yaml/.yml、declaration.yaml/.yml或kind: DesiredState),或声明内容校验失败、或安全门 deny。处理:核对 git_path 前缀与文件命名约定;看同步历史弹窗对应行的「操作摘要/错误」;安全门 deny 需调整部署安全策略后再同步。 - 同步成功提示但 commit 显示
-:原因是后端把结果嵌套在sync_history(回滚为rollback_history)下,前端parseSyncResult未识别该键(缺陷 d)。处理:commit 与 summary 需到「历史」弹窗查看真实值;成功/失败红绿提示不受影响。 - 点「刷新」提示成功但什么都没发生:原因是仓库没有 active 的同步目标(
triggered与failed都为空)或目标不是 active。处理:先展开子区确认存在 active 目标;逐个目标用行内「同步」验证。 - 选了 scheduled 策略但从不自动同步:原因是
sync_cron留空或 cron 表达式非法,StartScheduler会跳过并告警。处理:编辑同步目标,在出现的「定时表达式」输入框填写合法 cron(如*/30 * * * *,5 段式),保存后确认。 - 漂移弹窗提示「暂无漂移」,但实际环境有差异:原因是该目标没有成功同步过(OperationSummary 为空,无从收集期望状态 id),或 drift 事件的字段名与前端别名不匹配。处理:先执行一次成功同步,再重开「漂移」弹窗;仍空则核对仓库目录与目标环境实际状态。
- 历史弹窗全空/只有一条:原因是该目标从未同步过,或同步历史被级联删除(删目标/删仓库)。处理:先手动同步一次再看历史;确认不是刚删过目标。
- 目标环境列显示纯数字 #N:原因是页面上拉取的环境列表不包含该 id(环境被删或环境列表接口失败)。处理:到环境管理页确认该环境是否存在;此显示属兜底,不阻断其它操作。
8. 已知缺陷与边界
| 缺陷/边界 | 说明 | 位置 |
|---|---|---|
d. 同步/回滚提示 commit 恒为 - | 后端把结果放 sync_history/rollback_history,前端 parseSyncResult 只识别 history/sync/record,commit_sha 与 summary 取不到 | GitReposPage.vue 的 parseSyncResult;gitops_handlers handleSyncTargetSync/Rollback |
| Git 仓库为本地目录模拟 | url 指向磁盘目录,无真实 git 拉取/远端凭据;commit 为内容模拟 SHA | TAD-02 localrepo 自研实现 |
| 认证方式仅记录不鉴权 | 前端下拉值 ssh_key/basic 与后端枚举 ssh/https_token 不一致;本地目录模拟下 auth 不参与连接 | GitReposPage AUTH_TYPES vs gitops models |
| 漂移策略仅识 alert/auto_fix | 后端只认这两个值,reject/remediate 等旧值不被识别 | DRIFT_POLICIES 注释(对齐 gitops/models.go) |
| 项目 id 写死为 1 | 仓库/目标/环境下拉都固定请求 /projects/1/... | GitReposPage.vue PROJECT_ID 常量 |
| 展开结果有前端缓存 | 首次展开后复用缓存,外部改动需靠本页写操作触发强制刷新 | syncMap 缓存逻辑 |
| 同步目标列表全量拉取后前端过滤 | 子区用项目级 /sync-targets 全量列表按 git_repo_id 过滤,仓库多时列表大 | loadSyncTargets |
| 编辑同步目标不可迁移仓库 | git_repo_id 下拉在编辑态 disabled | targetModal template |
| 无分页/搜索 | 仓库与目标均全量一次性渲染 | fetchRepos/loadSyncTargets |
| 历史/漂移无自动更新 | 弹窗打开时拉取,后台变化需重开弹窗刷新 | openHistory/openDrift |