docs(audit): third-batch ledger — probes verified, build lane proven end to end, #7/#11-#15 reconciled

- The control-plane probes (0c8e29b) and the user-modpack build lane
  (f79e5eb + 02fd2de) get their live evidence recorded, including the three
  drill-only defects the lane fixed (Job scheduling, Kaniko Dockerfile
  ownership, Trivy DB egress).
- The stale 'unfixed' rows #7/#11-#15 are reconciled with their commits.
- Remaining-work list re-stated: image durability, PG contract tests, the
  panel's /jobs block, multi-node reaper placement, alerting, and the newly
  found player-visible build status gap.
This commit is contained in:
Lemon-miaow committed 2026-09-22 23:05:13 +08:00
1 parent 02fd2de502
commit d2c656533e
1 file changed
+23 -11
+23 -11
View File
@@ -1,6 +1,6 @@
# Felis 生产就绪审计 — 2026-09-22(真机 E2E + 混沌) # Felis 生产就绪审计 — 2026-09-22(真机 E2E + 混沌)
分支:`audit-fixes-20260922`(已推送)。环境:CentOS Stream 9 / aarch64 / k3s v1.36.4, 分支:全部修复**逐 commit 直接推 `main`**(不再用功能分支)。环境:CentOS Stream 9 / aarch64 / k3s v1.36.4,
IPv6-only 接入(`ssh -6 -i ~/.ssh/id_ed25519 root@fdb2:2c26:f4e4:0:21c:42ff:fede:69ec`), IPv6-only 接入(`ssh -6 -i ~/.ssh/id_ed25519 root@fdb2:2c26:f4e4:0:21c:42ff:fede:69ec`),
面板经 `socat TCP6:443 → 127.0.0.1:30443` 中继(手动启动,重启 VM 后需重开)。 面板经 `socat TCP6:443 → 127.0.0.1:30443` 中继(手动启动,重启 VM 后需重开)。
@@ -24,14 +24,16 @@ IPv6-only 接入(`ssh -6 -i ~/.ssh/id_ed25519 root@fdb2:2c26:f4e4:0:21c:42ff:f
## 待决策台账(未修) ## 待决策台账(未修)
| # | 主题 | 说明 | 前两日台账的 #7、#11–#15 已全部修复(本批核销,证据见下),不再挂在"未修"里:
| # | 主题 | 状态 |
|---|------|------| |---|------|------|
| 7 | 异步失败不可感知 | backup/restore 失败后无状态出口:restore 行不变、backup 无行;只有集群侧 Job/日志可查。建议状态字段或 `?failed` 查询 | | 7 | 异步失败不可感知 | ✅ 修复:`GET /api/v1/servers/{name}/jobs`(`ff7c57c`,含 RBAC `jobs:list` 与 OpenAPI);真机 drill 用它看到 running→succeeded |
| 11 | PG 断连表现 | 会话查询失败报 401 而非 503(fail-closed 但误导;用户以为没登录) | | 11 | PG 断连报 401 | ✅ 修复:会话存储故障改为 503(`2a8f897`) |
| 12 | ready 门滞后 | 容器 Ready 后 6~10s 内 API 仍 409 not_running | | 12 | ready 门滞后 | ✅ 修复:operator 启动期内每 2s 重探 RCON(`a2df2f2`) |
| 13 | setup token 截断 | 43 字符 token + 长域名,80 列终端下 TUI 截断显示(复现:tmux 80 列) | | 13 | setup URL 截断 | ✅ 修复:TUI 折行(`abce381`) |
| 14 | 日志噪音 | controller-runtime 未 SetLogger,首用打印整段堆栈;TLS handshake EOF 噪音(kubelet 探针) | | 14 | controller-runtime 日志噪音 | ✅ 修复:SetLogger 接 slog(`a415246`) |
| 15 | reaper 启用未演练 | **已演练完毕(2026-09-22)**:绑定挂载演练 world → 一次 pass 归档+删 PVC+保留 servers 行/CRD;过期归档驱逐(行转 deleted、文件删除、合法归档未动)。启用仍受 #6 三旗标约束;另注意 `--worlds-host-path` 的 `<path>/<pvc>` 布局在 stock local-path 下不成立(编排责任),且 VM 上演练用的 CronJob 已 **suspend** 防误删(真实世界目录不在 /srv/worlds-root)| | 15 | reaper 启用未演练 | ✅ 已演练(见"第二日"节;CronJob 仍 `suspend=true` 防误删) |
## 已验证事实(正向清单) ## 已验证事实(正向清单)
@@ -53,16 +55,25 @@ IPv6-only 接入(`ssh -6 -i ~/.ssh/id_ed25519 root@fdb2:2c26:f4e4:0:21c:42ff:f
- **回收闭环(真实 k3s 布局 + 权限)**:resolvecheck(20d idle)→ CronJob 派 Job:`evaluated=2 reaped=1`;归档含 marker、PVC+宿主目录回收、`world_backups` 记 `inactive_15d`;期间先经历 `lstat /worlds/…: permission denied`(k3s storage root 0700)→ 修 ACL/root 权限后通过 ✅ - **回收闭环(真实 k3s 布局 + 权限)**:resolvecheck(20d idle)→ CronJob 派 Job:`evaluated=2 reaped=1`;归档含 marker、PVC+宿主目录回收、`world_backups` 记 `inactive_15d`;期间先经历 `lstat /worlds/…: permission denied`(k3s storage root 0700)→ 修 ACL/root 权限后通过 ✅
- **磁盘压力三连 drill**:①1e6 自定义类:游戏 pod 先走、控制面随后仍被逐(kubelet ranked/evicted 日志)→ 镜像 GC → ErrImagePull;②改内建 `system-cluster-critical` 后:`cannot evict a critical pod` × felis-api/operator/registry,全部保持 Running;③恢复阶段实测 `DiskPressure` 条件 ~5min(eviction-pressure-transition-period)转 False,被 GC 的游戏镜像按 runbook 重导入后 25s 恢复 ✅ - **磁盘压力三连 drill**:①1e6 自定义类:游戏 pod 先走、控制面随后仍被逐(kubelet ranked/evicted 日志)→ 镜像 GC → ErrImagePull;②改内建 `system-cluster-critical` 后:`cannot evict a critical pod` × felis-api/operator/registry,全部保持 Running;③恢复阶段实测 `DiskPressure` 条件 ~5min(eviction-pressure-transition-period)转 False,被 GC 的游戏镜像按 runbook 重导入后 25s 恢复 ✅
- **混沌**:SIGKILL k3s 主进程 → kubectl 与 felis-api ready 均在 **10s** 内恢复(api pod 本身未被重启)✅;内存压力(2.6G tmpfs 满写):无 OOM kill、无人被逐(swap 5.9G 吸收 + 内核回收),控制面不受影响 ✅ - **混沌**:SIGKILL k3s 主进程 → kubectl 与 felis-api ready 均在 **10s** 内恢复(api pod 本身未被重启)✅;内存压力(2.6G tmpfs 满写):无 OOM kill、无人被逐(swap 5.9G 吸收 + 内核回收),控制面不受影响 ✅
- **构建链路定位(未修,见下一节)**:Kaniko/Trivy 默认镜像是外部的 → 已加 `[registry] kaniko_image/trivy_image/build_cpu_limit/build_mem_limit` 覆写;上下文跨 ns 不可挂载(PVC 不能跨 ns)是设计级缺口,s3 lane 也缺凭据注入 ✅(证据:`internal/submit/blobstore.go:40`、`cmd/felis/api.go` contextBase 分支、jobspec 无 volumes/env) - **构建链路定位(当时未修)**:Kaniko/Trivy 默认镜像是外部的 → 已加 `[registry] kaniko_image/trivy_image/build_cpu_limit/build_mem_limit` 覆写;上下文跨 ns 不可挂载(PVC 不能跨 ns)是设计级缺口,s3 lane 也缺凭据注入 ✅(证据:`internal/submit/blobstore.go:40`、`cmd/felis/api.go` contextBase 分支、jobspec 无 volumes/env)→ **第三批已修复,见下**
### 本轮新增真机证据(第三批:探针 + 构建链路全通)
- **控制面探针(`0c8e29b`)**:felis-api / felis-operator / registry 三个 Deployment 此前**完全没有探针**。修复后真机验证:api、operator `/readyz`+`/healthz`(internal 8081),registry `/v2/`(含命名端口解析);三个 Pod `Ready=true`、`restartCount=0`、无 `Unhealthy` 事件;operator 新增 `--health-probe-bind-address`(8081,与 metrics 8080 分离,零检查也 404 → 已注册 ping)
- **构建链路全通(`f79e5eb` + `02fd2de`)**:
- 传输:context_ref 改为 internal-face URL;`felis fetch-context` initContainer 走 service token(对象来自 `felis-build` 里按 `bootstrap`/`felis setup` 同款 Secret 复制机制落地的 `felis-service-token`;netpol 只放行控制 ns:8081)+ zip-slip 安全解包到限容 emptyDir;Kaniko `--context=/context` 只读挂载
- **真机 E2E**:玩家 OTP 登录 → 建 submission → 上传 gzip context(marker `felis-e2e-build-marker`)→ Owner approve → Job:fetch ✅ → Kaniko 构建并 push `registry.felis.svc:5000/user-uploads/<sub>:latest` ✅ → Trivy 扫描(内建镜像 DB)✅ → Job `Complete`;**从 registry 拉回镜像校验 `/hello.txt` 内容 = 上传的 marker** ✅
- 演练中真机抓到并修复 3 个单测看不到的缺陷:① Job requests=limits(2C/4Gi)在 4C/5.5G starter 上**永远 Pending**(`FailedScheduling/Insufficient memory`)→ requests 改为地板值(250m/512Mi,不超上限);② Kaniko 重拷 Dockerfile 时 chown/chmod 到源文件 owner(distroless uid 65532)在 drop-ALL 能力下失败(`copying dockerfile: chown … operation not permitted`)→ fetch 容器以 root 解包(= Kaniko 自身 uid);③ Trivy DB 默认 `mirror.gcr.io` 正被 egress 锁拒绝(fail-closed)→ 新配置 `[registry] trivy_db_repository` + 文档 §8e 固化镜像配方(`docker pull/tag/push aquasec/trivy-db:2` 进内建 registry;`--insecure` 已覆盖明文 HTTP)
## 结论:离"生产可用"还差什么(按优先级) ## 结论:离"生产可用"还差什么(按优先级)
1. **构建链路的上下文通道(唯一的功能级缺口)**。用户模组提交 → 审批 → Kaniko 构建这条 lane 需要跨命名空间传递 context 的机制;PVC 不能跨 ns,s3 lane 又缺 build pod 侧凭据注入。两条候选:a) 官方走对象存储(在 bundle 里加 MinIO 或让 operator 配外部 S3 + 为每次构建挂 per-build Secret + netpol 放行 endpoint);b) 在 felis-build 里做 context handoff(一个控制面任务把 blob 落到 build ns 的 PVC)。做 a) 时顺带把 kaniko/trivy 镜像推入内建 registry。 1. ~~构建链路的上下文通道~~ ✅ **已修**(`f79e5eb`/`02fd2de`,真机全链路含拉回校验;Trivy DB 需按 §8e 镜像一次)。
2. **镜像耐久**:kubelet 仍可能 GC 掉"当前无人使用"的镜像(游戏镜像已实测发生)。把控制面与游戏镜像推入内建 registry 并让 Deployment/StatefulSet 引用 registry 路径,可免去"事故后人工重导入"。本轮已用 runbook(troubleshooting §13b)兜住。 2. **镜像耐久**:kubelet 仍可能 GC 掉"当前无人使用"的镜像(游戏镜像已实测发生)。把控制面与游戏镜像推入内建 registry 并让 Deployment/StatefulSet 引用 registry 路径,可免去"事故后人工重导入"。本轮已用 runbook(troubleshooting §13b)兜住。
3. **PG 级契约测试**:PGRepo 与接口/fake 漂移已抓到 3 例(#16/#17/#18)且单测全绿也发现不了;建议对"注释承诺了字段/错误码/生命周期"的方法加 Postgres 集成测试。 3. **PG 级契约测试**:PGRepo 与接口/fake 漂移已抓到 3 例(#16/#17/#18)且单测全绿也发现不了;建议对"注释承诺了字段/错误码/生命周期"的方法加 Postgres 集成测试。
4. **面板把 /jobs 显示出来**:API 已有出口(#7),`ServerBackups` 页加一块"最近操作"即可让普通用户看见失败原因。 4. **面板把 /jobs 显示出来**:API 已有出口(#7),`ServerBackups` 页加一块"最近操作"即可让普通用户看见失败原因。
5. **多节点回收**:reaper CronJob 无 nodeSelector,多节点需按卷所在节点加 selector(代码与文档均已标注,未实现调度约束)。 5. **多节点回收**:reaper CronJob 无 nodeSelector,多节点需按卷所在节点加 selector(代码与文档均已标注,未实现调度约束)。
6. **告警**:felis_* 指标已全有生产者,但无非告警;磁盘/内存阈值告警建议随部署一并给出。 6. **告警**:felis_* 指标已全有生产者,但无非告警;磁盘/内存阈值告警建议随部署一并给出。
7. **玩家可见的构建结果(新发现)**:构建失败目前只有 admin 路由(`/images/build/{id}`)能看到;`/me/submissions` 只回 `approved`,玩家看不到 failed/succeeded。建议在 submission 视图附上 `build_status`(或在面板里链到构建详情)。
## 系统性观察 ## 系统性观察
@@ -74,5 +85,6 @@ IPv6-only 接入(`ssh -6 -i ~/.ssh/id_ed25519 root@fdb2:2c26:f4e4:0:21c:42ff:f
- 面板会话 cookie:`/tmp/felis-cookies.json`;API 助手:`/tmp/fcurl.sh` - 面板会话 cookie:`/tmp/felis-cookies.json`;API 助手:`/tmp/fcurl.sh`
- 测试服:`test-one`(minecraft ns,stopped);合法备份 `bk-47ee2e7e96e5a4ca9d0e51b805518bac` - 测试服:`test-one`(minecraft ns,stopped);合法备份 `bk-47ee2e7e96e5a4ca9d0e51b805518bac`
- CDP 调试口:Mac `127.0.0.1:9333`(独立 Chrome,profile `/tmp/felis-chrome2`);WebAuthn 虚拟认证器需在**同一 CDP 会话**内完成仪式,且先 `Page.bringToFront`(否则 NotAllowedError: page does not have focus) - CDP 调试口:Mac `127.0.0.1:9333`(独立 Chrome,profile `/tmp/felis-chrome2`);WebAuthn 虚拟认证器需在**同一 CDP 会话**内完成仪式,且先 `Page.bringToFront`(否则 NotAllowedError: page does not have focus)
- 分支已部署到 VM:`felis-api` 镜像 = `felis:auditfix7`(含全部修复) - 分支已部署到 VM:`felis-api` 镜像 = `felis:auditfix16`(含全部修复;探针可直接 `kubectl get deploy -o jsonpath=…readinessProbe` 复核)
- 构建链路 drill 现成条件:`felis-build` 里有 `felis-service-token`(Secret 复制);`felis-config` 里 `kaniko_image/trivy_image` 指向 k3s containerd 已导入的 pin tag、`trivy_db_repository = registry.felis.svc:5000/mirror/trivy-db:2`(镜像配方 §8e;VM 上 docker daemon 的 `insecure-registries` 已含 registry ClusterIP)
- VM 内部面:`k3s kubectl -n felis port-forward svc/felis-api-internal 18081:8081`(Pod 重建后转发会悬死,需重启);reaper CronJob(minecraft ns)已应用但 `suspend=true` - VM 内部面:`k3s kubectl -n felis port-forward svc/felis-api-internal 18081:8081`(Pod 重建后转发会悬死,需重启);reaper CronJob(minecraft ns)已应用但 `suspend=true`