第 2 章:CRD 是长期维护的 API
声明一份能够拒绝坏输入的结构
GatewayInstance 的 CRD 位于 lab/crd.yaml。它使用 Namespaced 作用域,把实例和子资源放在同一命名空间。replicas 是整数并限定在零到三;message 是有长度限制的字符串,默认值为 gateway ready。用户输入在进入存储前经过结构验证,控制器不必把任意 JSON 都解释成合理配置。
限制副本数在教学项目中兼有实验隔离价值:一个拼写错误不会创建大量 Pod。但它不是平台配额。多个实例仍可能分别申请三个副本;真正的租户资源约束需要 ResourceQuota、策略和容量管理配合。API 字段的局部范围验证不能代替聚合资源预算。
message 的上限也有明确用途。它最终进入 Envoy 启动参数,过长内容会放大对象大小和进程参数,不适合用作大文件分发。本项目把字符串先交给 JSON 编码器,再放入 YAML 的标量位置,避免引号、换行或冒号破坏配置。输入验证与输出编码解决不同问题,不能因为长度受限就手工拼接不受转义的文本。
spec 与 status 的不同写入路径
CRD 开启 status 子资源。用户对主资源进行 Update 时,不应顺手改写控制器状态;控制器写 status 时,也不应覆盖用户最新 spec。API server 提供了子资源隔离,但这不自动解决语义竞态:状态写入者仍须知道自己处理的是哪一版意图。
假设控制器读到 generation 为七,创建了对应 Deployment。用户随后把 message 改为另一值,generation 变为八。控制器在写 status 前重新 GET 到最新对象,如果把八直接填入 observedGeneration,就会宣布尚未应用的配置已经处理。正确做法是携带本轮的七,检查当前意图仍与七一致;不一致时返回并重新协调。
随书代码采用后一种保守策略:任何中途 spec 变化都阻止本轮状态确认。真实 API 竞争测试在子资源处理后更新 spec,断言旧 observedGeneration 不会被错误推进,再由下一次协调应用新副本数。单靠 status 子资源权限无法捕获这个错误。
默认值也是 API 行为
创建时省略 message,API server 会填入默认值。控制器从服务器读取的是默认后的结构,因此不必维护另一套隐含默认值。这样做还意味着默认值的变化需要作为 API 变更评审:已存储对象、更新对象以及不同客户端的观察结果可能不同,不能假定改 YAML 后所有历史实例都会同步改变。
CRD 不应暴露无法稳定兑现的底层配置。例如直接接受一整段 DeploymentSpec,表面灵活,实际让用户与控制器同时控制大量字段。后续控制器升级可能覆盖用户设置,或者因未知字段而失去收敛能力。先定义业务意图,再为确有需求的可配置项设计稳定含义,更容易维持向后兼容。
status 采用保留未知字段的简化结构,便于实验集中研究协调逻辑。生产 API 更适合给条件、计数和引用建立明确 schema,使客户端和准入链能理解结构。本书不把教学 schema 当作成熟 API 规范,升级为严格状态 schema 时也应保留已有字段的可读性。
名称与身份分开处理
名称只在命名空间内唯一,并且删除后可以重用。UID 代表一次对象生命。外部登记使用 UID 派生命名,owner references 也绑定 UID,防止旧实例删除或清理时误操作后来出现的同名实例。把名称当成永久业务主键,是控制器与外部系统集成时最常见的隐患之一。
跨命名空间 owner reference 有限制,因此本项目不会让一个命名空间的实例直接拥有另一个命名空间的 Secret。需要共享资源时,应另行定义引用权限、消费者计数和删除策略。把引用写成一个字符串并不会让垃圾回收器理解业务关系。
观察准入结果而非只看 YAML
调试时先读取服务器实际返回的对象,确认默认值、generation、UID 与 resourceVersion。源码中的 YAML 是输入,服务器保存的对象才是协调起点。若准入 webhook 或策略改写了字段,控制器必须围绕最终对象工作,否则每轮都会与准入链互相改写。
练习可以从删除 message 默认值开始:先比较创建请求和服务器返回,再设计显式空字符串应该被接受、拒绝还是表示默认。不要急着加代码补空值;先把用户能依赖的 API 行为写清楚,然后再确定应该由 schema、默认规则还是控制器实现。
评审案例:零值不能悄悄变成默认值
Go 的整数零值容易诱使实现把读取不到 replicas 和用户显式写零合并处理。前者是结构缺失,后者是合法停止数据面的意图。如果程序简单判断零就改成一,用户永远无法缩容到零;如果把缺失当零,又可能在不完整对象上意外停止服务。
本项目让 schema 要求 replicas 存在,服务器验证成功后控制器才读取。这减小了缺失歧义,但未来 schema 升级仍要考虑历史对象。若把字段改为可选,应明确缺省与零的区别,并使用能够表达存在性的类型或读取返回值。
字段单位也应进入名字或说明。超时用秒还是毫秒,容量是字节还是字符,副本数是否允许负数,都影响客户端长期兼容。先把它们写成可验证的约束,再考虑界面上的默认提示。表单默认值不能替代服务器规则,因为用户可以通过命令行和其他客户端绕过表单。