策略测试 — 把通过和拒绝一起钉住
一句话总结
策略测试的最小单位是输入样本、预期判定、预期消息三栏的表格,如果这张表里没有拒绝样本,这个测试就什么也守护不了。
为什么需要它
策略即代码,并不是比喻。它要放进仓库,要经过评审,要部署,要被修改。但它与应用代码有一点不同:策略出故障最常见的方式不是错误,而是沉默。如果匹配范围出现偏差,什么都看不到,策略就会悄悄地全部放行。界面上什么都没有发生,仪表板上也没有红线。所以“运行良好的策略”和“已经死掉的策略”,从表面上无法区分。
还有第二种力量叠加在上面。策略只会朝着越来越宽松的方向被修改。有人被拦住了来咨询,就放开一个条件,再多加一个例外命名空间。每次修改在当时都是合理的。半年过去,没有人知道这个策略当初到底想拦住什么。回归测试正是在这个地方钉下的钉子。如果测试中有拒绝样本,那么在把规则改宽松的那天,测试就会亮起红灯。
工作原理
基于表格(黄金用例)的测试。一个测试由三栏构成。
| 栏 | 内容 |
|---|---|
| 输入样本 | 实际可能送到集群的清单片段。包括应该通过的和应该被拦住的 |
| 预期判定 | pass / fail / skip |
| 预期消息 | 拒绝时人会收到的句子 |
很多团队会漏掉第三栏,而消息也是契约。拒绝消息是策略向开发者说话的唯一通道,这句话一变,抓取它来做自动化的流水线也会跟着变。把消息放进测试,“不知道为什么被拦住”的咨询,在评审阶段就能暴露出来。
样本最好每次只让一个地方不同。复制通过样本,只修改一个违规字段,做成多对这样的样本,这样测试一旦失败,从文件名上就能直接读出是什么变了。
运行测试的三个位置。
- 上传到集群之前,离线引擎。只用策略文件和样本文件来运行判定。不需要网络,也不需要集群,所以在 CI 中跑得最快。Kyverno CLI 的
kyverno apply和kyverno test、Conftest、OPA 都属于这一类。 - 上传到集群之前,服务端 dry-run。以
?dryRun=All发送的请求,除了保存步骤之外,原样经过正常路径。文档写明,此时所有相关的准入控制器都会执行,验证准入看到的是变形完成之后的对象,默认值会被填充,schema 验证也会发生。所以这不是“模拟”,而是真实的判定。离线引擎看不到的东西(与其他策略的相互作用、变形之后的形状),只有在这里才能看到。 - 上传之后。用后台扫描和报告遍历已经存在的资源。因为准入只看今后要进入的请求。
服务端 dry-run 还有一个配套的事实。会触发带有副作用的准入控制器的请求,在 dry-run 时反而会让它失败。所以 Webhook 必须把配置对象的 sideEffects 声明为 None 或 NoneOnDryRun,才能在 dry-run 路径上正常评估。内置的准入插件全都支持 dry-run。
假通过的常见原因。测试亮绿灯,而集群却没有拦截,原因通常是以下三者之一。
- 策略根本没有看那个资源。匹配范围是
Deployment,样本却是Pod,或者 apiVersion 不同。判定是skip,而测试如果不把skip算作失败,就会亮绿灯。把 skip 与 pass 区分开来统计,是第一道防线。 - 直接相信了退出码。实际上确实存在工具找到了违规、却以 0 结束的情况。本实验环境中的
kyverno json scan就是这样。这样一来,流水线就永远亮绿灯。 - 样本中一个拒绝都没有。只收集了通过样本的测试,即使把策略整个删掉,也仍然亮绿灯。打开测试文件时,如果没有一个 fail 预期,这个测试就等于没有。
策略仓库的 CI,通常是这样的顺序。
1) 정책 파일 스키마·문법 검사
2) 오프라인 엔진으로 골든 케이스 표 실행 (pass·fail·메시지)
3) skip 이 0 인지 확인 — 매치가 어긋나지 않았다는 증거
4) 거부 기대 건수가 0 이 아닌지 확인
5) 스테이징 클러스터에 서버 dry-run 으로 대표 표본 몇 개
6) 배포
第 3 项和第 4 项,是这份清单中最常被漏掉、最悄无声息地背叛你的。
在现场相遇的样子
第一,亮着绿灯却在生产中拦不住的策略。样本的 apiVersion 低了一个版本,全部是 skip,这样的案例很常见。这是因为只看了测试输出汇总行中的 pass 数,没有看 skip 数。在汇总中强制 skip 为 0 的一行,就能彻底堵住这类事故。
第二,把规则改宽松的那一天。“请把这个命名空间排除掉”反复出现,终于有一天,例外条件变得太宽,连原本要拦住的东西也通过了。那一刻,如果测试中有拒绝样本,PR 上就会亮红灯,如果没有,半年后只能通过事故才会知道。
第三,消息被悄悄改掉的那一天。重构策略时顺手润色了拒绝消息的措辞,结果抓取那个字符串来生成 Slack 告警的流水线,悄无声息地停了。如果把消息固定为预期值,做重构的人当场就会知道。
第四,这个环境坦率的局限。实验 Pod 中没有 conftest 和 opa。取而代之的是有 kyverno CLI,可以运行形态相同的基于表格的测试,而且有 kwok 启动的真实 API 服务器,所以服务端 dry-run 也能真正生效。工具名称不同,学到的东西是一样的。
参考文档
- kyverno apply 与测试套件:https://kyverno.io/docs/kyverno-cli/usage/apply/
- 策略报告:https://kyverno.io/docs/policy-reports/
- Kubernetes API 概念中的 dry-run: https://kubernetes.io/docs/reference/using-api/api-concepts/
- 动态准入控制与 sideEffects: https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/
下一项实验要做什么
从头开始为一个策略制作测试套件。把通过样本和拒绝样本成对放置,把预期判定和预期消息写进表格,然后亲自编写以上面第二个位置(服务端 dry-run)作为判定引擎的运行器。把策略根本不看的资源当作拒绝样本,亲手制造假通过,再加上能发现它的检查。接着把规则放宽一格,看到拒绝样本通过了,确认回归测试抓住了这个变更。最后确定退出码约定,使没有任何样本的测试不会亮绿灯。