把校验甩给 API Server
目标
故意提交违反规则的资源,亲自收集 API 服务器拒绝了什么以及如何拒绝,并确认默认值注入、pruning 和 CEL 验证都能由同一个 schema 完成。
为什么重要
把验证移入 schema 的真正收益,不只是“拒绝”,而是**“在拒绝时告诉用户哪里不对、为什么不对”**。加入 enum 后,拒绝消息会列出允许值;加入 maximum 后,上限会直接出现在消息中。用户无需翻阅 Wiki,只读错误就能修复。pruning 起初会让人困惑——拼错的字段不会报错,而是悄悄消失。但这正是强制“schema 即契约”的机制。因此,只有确实需要接收任意键时,才应仅在对应位置例外开放。最后,CEL 改变了做法。过去为了实现“prod 至少两个副本”这类跨字段约束,需要运行验证 Webhook 服务器、管理证书,还要承担 Webhook 故障拖垮集群的风险。现在只需在 schema 中写一行。
步骤
开始前准备:每个实验都会启动新的实验 Pod,因此上一实验创建的集群状态不会保留。如果 kubectl get crd webservices.apps.labhub.io 没有结果,请重新把上一实验的 CRD 写入 /root/crd/crd.yaml 并应用,同时执行 kubectl create ns crd-lab。本实验要求的 schema 为:spec.required: [image];replicas(integer,minimum 1,maximum 10,default 1);tier(string,enum [dev, stage, prod],default dev)。第 6、7 步会在此基础上增加 spec.extra 和 CEL 规则。
- 在
/root/crd/validate/no-image.yaml中编写 WebService:metadata.name: no-image,命名空间为crd-lab,且spec中没有image。尝试应用,并将包含标准错误的失败输出保存到/root/crd/validate/out/err-required.txt。输出应包含spec.image和必填值相关消息,且no-image不得留在集群中。 - 在
/root/crd/validate/bad-type.yaml中写入metadata.name: bad-type、spec.image: nginx:1.27、spec.replicas: "three",尝试应用,并把失败输出保存到/root/crd/validate/out/err-type.txt。 - 在
/root/crd/validate/bad-tier.yaml中写入metadata.name: bad-tier、spec.image: nginx:1.27、spec.tier: qa,尝试应用,并把失败输出保存到/root/crd/validate/out/err-enum.txt。输出中应同时显示允许值。 - 在
/root/crd/validate/too-many.yaml中写入metadata.name: too-many、spec.image: nginx:1.27、spec.replicas: 50,尝试应用,并把失败输出保存到/root/crd/validate/out/err-range.txt。 - 在
/root/crd/validate/defaulted.yaml中只写metadata.name: defaulted和spec.image。绝对不要写spec.replicas和spec.tier。应用后确认已保存对象中自动填入replicas: 1、tier: dev。 - 在
/root/crd/crd.yaml的 v1 schema 中添加properties.spec.properties.extra,设置type: object和x-kubernetes-preserve-unknown-fields: true,然后重新应用 CRD。接着,在/root/crd/validate/pruned.yaml中加入metadata.name: pruned、spec.image和 schema 中不存在的spec.bogus: anything并应用(已保存对象中的bogus应消失);再在/root/crd/validate/preserved.yaml中加入metadata.name: preserved、spec.image、spec.extra.custom: kept并应用(该值应保留)。 - 在 v1 schema 的
properties.spec直属位置添加x-kubernetes-validations数组。第一条规则的rule为self.tier != 'prod' || self.replicas >= 2,message为prod 계층은 복제본이 2개 이상이어야 합니다。重新应用后,在/root/crd/validate/cel-violation.yaml中加入metadata.name: cel-violation、spec.image、spec.tier: prod、spec.replicas: 1,尝试应用,并把失败输出保存到/root/crd/validate/out/err-cel.txt。输出中必须原样包含上述 message。 - 创建
/root/crd/validate/out/matrix.json。顶层键为cases,数组中每个元素包含name、expected(rejected或accepted)、actual、rule四个键。加入 5 个拒绝案例(no-image/required、bad-type/type、bad-tier/enum、too-many/maximum、cel-violation/cel)和 3 个通过案例(defaulted/default、pruned/pruning、preserved/preserve-unknown-fields);所有案例的expected与actual都必须相同。
参考
- 每个实验都会启动新的实验 Pod,因此上一实验的集群状态不会保留。但只要把声明保存为文件,就能在任意 Pod 中重建相同状态——这正是声明式方法的实际优势。
- 要把应用失败输出保存到文件,必须像
kubectl apply -f 파일 > 출력파일 2>&1一样同时捕获标准错误。拒绝消息不在标准输出中。 - 可以复制
/opt/lab/fixtures/crd/sample-cr.yaml后只修改值,快速创建各个案例。 - CEL 规则在默认值填充后执行,因此
self.tier和self.replicas始终存在。 - 常见错误 1:第 5 步在清单中写入
replicas。这样无法区分该值是默认填充还是手动填写,评分会失败。 - 常见错误 2:第 6 步把
x-kubernetes-preserve-unknown-fields设置在整个 spec 上。这样会关闭整个 spec 的 pruning,bogus也会保留。只能设置在extra下。 - 常见错误 3:第 8 步把预期值直接复制为
actual。必须查询集群并填写实际结果,评分器会与集群对照。
观察缺少必填字段时如何被拒绝
在 /root/crd/validate/no-image.yaml 中编写 WebService:metadata.name: no-image,命名空间为 crd-lab,且 spec 中没有 image。尝试应用,并将包含标准错误的失败输出保存到 /root/crd/validate/out/err-required.txt。输出应包含 spec.image 和必填值相关消息,且 no-image 不得留在集群中。
这是故意制造失败的步骤。拒绝消息会写入标准错误,因此保存到文件时必须同时捕获标准错误。被拒绝的资源不得留在集群中。
观察类型不匹配时如何被拒绝
在 /root/crd/validate/bad-type.yaml 中写入 metadata.name: bad-type、spec.image: nginx:1.27、spec.replicas: "three",尝试应用,并把失败输出保存到 /root/crd/validate/out/err-type.txt。
YAML 中用引号包围数字后,它会成为字符串。请保存 schema 期待 integer 时产生的消息。
观察枚举违规及允许值提示
在 /root/crd/validate/bad-tier.yaml 中写入 metadata.name: bad-tier、spec.image: nginx:1.27、spec.tier: qa,尝试应用,并把失败输出保存到 /root/crd/validate/out/err-enum.txt。输出中应同时显示允许值。
enum 违规消息不仅拒绝请求,还会提示可用选项。输出中必须包含该列表。
观察超出范围时如何被拒绝
在 /root/crd/validate/too-many.yaml 中写入 metadata.name: too-many、spec.image: nginx:1.27、spec.replicas: 50,尝试应用,并把失败输出保存到 /root/crd/validate/out/err-range.txt。
请提供超过 maximum 的值。消息中会直接提到上限。
确认未填写字段会被注入默认值
在 /root/crd/validate/defaulted.yaml 中只写 metadata.name: defaulted 和 spec.image。绝对不要写 spec.replicas 和 spec.tier。应用后确认已保存对象中自动填入 replicas: 1、tier: dev。
若在清单中填写值,就无法确认是否由默认机制注入。请留空这两个字段,然后重新读取已保存对象进行比较。
观察未知字段的剪除与例外保留
在 /root/crd/crd.yaml 的 v1 schema 中添加 properties.spec.properties.extra,设置 type: object 和 x-kubernetes-preserve-unknown-fields: true,然后重新应用 CRD。接着,在 /root/crd/validate/pruned.yaml 中加入 metadata.name: pruned、spec.image 和 schema 中不存在的 spec.bogus: anything 并应用(已保存对象中的 bogus 应消失);再在 /root/crd/validate/preserved.yaml 中加入 metadata.name: preserved、spec.image、spec.extra.custom: kept 并应用(该值应保留)。
schema 中不存在的字段会在保存前被剪除。需要接收任意键的位置,应先在 schema 中定义为对象,再标记为保留未知字段。该标记是以 x- 开头的扩展键。
使用 CEL 表达跨字段约束
在 v1 schema 的 properties.spec 直属位置添加 x-kubernetes-validations 数组。第一条规则的 rule 为 self.tier != 'prod' || self.replicas >= 2,message 为 prod 계층은 복제본이 2개 이상이어야 합니다。重新应用后,在 /root/crd/validate/cel-violation.yaml 中加入 metadata.name: cel-violation、spec.image、spec.tier: prod、spec.replicas: 1,尝试应用,并把失败输出保存到 /root/crd/validate/out/err-cel.txt。输出中必须原样包含上述 message。
这条规则无法只查看一个字段来判断。请在 spec 对象层级放置规则数组,并在规则中用 self 指代当前对象。message 是用户会看到的语句,因此会原样出现在错误中。
建立验证矩阵并与实际结果对照
创建 /root/crd/validate/out/matrix.json。顶层键为 cases,数组中每个元素包含 name、expected(rejected 或 accepted)、actual、rule 四个键。加入 5 个拒绝案例(no-image/required、bad-type/type、bad-tier/enum、too-many/maximum、cel-violation/cel)和 3 个通过案例(defaulted/default、pruned/pruning、preserved/preserve-unknown-fields);所有案例的 expected 与 actual 都必须相同。
把之前步骤创建的案例整理成表。每个案例写明名称、预期、实际结果及触发的规则,预期与实际不能有任何一项不同。