关于 k8s-crd-from-typed-schema
帮助开发者将类型化 schema 转换为 Kubernetes CRD 的实用技能,涵盖 Structural Schema 适配、CRD 包装和验证。重点说明容易遗漏的字段限制、status 子资源与版本配置问题。
主要功能
- 01适配 JSON Schema 到 Structural Schema
- 02处理不兼容 schema 特性
- 03生成 CRD 外层结构
- 04计算资源复数名称
- 05使用 kubectl 服务端 dry-run 验证
- 06说明 metadata 与 status 陷阱
适用场景
- 01从 zod 等类型定义生成 CRD
- 02排查 kubectl 拒绝 CRD 的错误
- 03避免 operator 因 status 更新循环
- 04在集群注册前验证 CR 和 schema
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ mizchi/ skills --skill k8s-crd-from-typed-schema安装后在 Claude Code 中输入/k8s-crd-from-typed-schema确认已加载。
附带文件README.md
详细介绍
从类型化 Schema 生成 k8s CRD
从类型化 Schema(zod / TypeBox / Valibot / 手写 JSON Schema)生成 CRD 看起来很简单——转换为 OpenAPI v3,嵌入为 openAPIV3Schema,就完成了。事实并非如此。CRD 使用的是 OpenAPI v3 子集以及 k8s 专用扩展(“结构化 Schema”),而且常见的 JSON Schema 功能中有好几种会悄然无法兼容。此外,还有两个与 Schema 无关的陷阱(status 子资源、禁止使用 metadata),只有在尝试使用 CRD 时才会显现。
这份技能清单列出了让 kubectl apply -f crd.yaml 成功、并确保操作器正常工作的必修项。
适用场景
- 构建操作器 / 控制器并编写自己的 CRD。
- 从类型化源生成 CRD,而不是手动编写 YAML。
- 遇到
kubectl错误,例如unknown field "$schema"/must not have "oneOf"/must not specify anything other than name and generateName/cannot have both "additionalProperties" and "properties"。 - 操作器首次运行正常,但首次写入状态后就陷入无限循环,不断协调。
工作流
1. 将类型化 Schema 转换为 JSON Schema(OpenAPI v3 方言)
对于 zod:
import { zodToJsonSchema } from 'zod-to-json-schema';
const json = zodToJsonSchema(specSchema, {
target: 'openApi3',
$refStrategy: 'none', // CRDs forbid $refs across the document
});
必须使用 $refStrategy: 'none'——k8s 的结构化 Schema 不会解析跨文档 $ref。请将所有内容内联。
对于 TypeBox:直接传入 Schema,然后运行下面的适配器。
2. 运行结构化 Schema 适配器
结构化 Schema 方言会拒绝多种 JSON Schema 功能。请移除或改写它们:
| JSON Schema 功能 | 处理方法 | 原因 |
|---|---|---|
$schema 元关键字 | 移除 | kubectl 会拒绝未知根键 |
oneOf / anyOf / allOf | 将该节点替换为 { x-kubernetes-preserve-unknown-fields: true } | 结构化 Schema 禁止在 x-kubernetes-validations 之外使用 Schema 形式的联合类型 |
元组 items(Schema 数组) | 合并为单个 Schema(使用 items[0]) | k8s 只支持单个项目 Schema |
除 uri / date-time 以外的 format | 移除 format 键 | apiserver 只支持有限的白名单;未知格式会导致验证失败 |
与 properties 并列的 additionalProperties: false | 移除 additionalProperties 键 | 结构化 Schema 禁止两者同时存在;声明了 properties 本身就表示对象封闭 |
空叶节点 {}(例如由 z.unknown() 生成) | 替换为 { x-kubernetes-preserve-unknown-fields: true } | 结构化 Schema 要求每个节点声明 type 或使用逃逸机制 |
此技能中的 examples/adapter.ts 提供了一个参考适配器(TypeScript,约 40 行),也可以从 k1c/src/cli/export-crds.ts 复制。
3. 将 spec Schema 封装在 CRD 外层结构中
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
name: <plural>.<group> # e.g. r2buckets.cloudflare.k1c.io
spec:
group: <group>
scope: Namespaced
names:
kind: <Kind>
singular: <kind>
plural: <plural>
listKind: <Kind>List
versions:
- name: v1alpha1
served: true
storage: true
schema:
openAPIV3Schema:
type: object
properties:
apiVersion: { type: string }
kind: { type: string }
spec: <your adapted schema here>
status:
type: object
x-kubernetes-preserve-unknown-fields: true
required: [spec]
subresources:
status: {} # <-- mandatory if your operator writes status
4. 正确计算复数形式
spec.names.plural 会出现在 kubectl URL 中(/apis/<group>/<version>/<plural>)。不要使用 kind.toLowerCase() + 's'——请处理常规名词变复数的情况:
function pluralize(kind: string): string {
const lower = kind.toLowerCase();
if (lower.endsWith('s') || lower.endsWith('x') || lower.endsWith('z')) return `${lower}es`;
if (lower.endsWith('y')) return `${lower.slice(0, -1)}ies`;
return `${lower}s`;
}
这与 Cloudflare 及大多数 k8s API 作者采用的词形变化规则一致。非规则 Kind(Endpoints、Quota → quotas)需要手动覆盖映射。
5. 使用 kubectl 的 dry-run 模式验证
kubectl apply --dry-run=server -f crd.yaml
kubectl apply --dry-run=server -f sample-cr.yaml # after CRD is registered
服务端 dry-run 可以捕获本地 dry-run 检测不到的结构化 Schema 违规(例如,additionalProperties 与 properties 同时存在的问题只有在服务端才会检查)。
易踩的坑
易踩的坑 1:在 openAPIV3Schema.properties 中声明 metadata
# ❌ kubectl rejects this:
openAPIV3Schema:
type: object
properties:
metadata: { type: object, properties: { name: { type: string } } }
spec: ...
# error: must not specify anything other than name and generateName for metadata
修复方法:完全不要列出 metadata。apiserver 会自动应用标准的 ObjectMeta Schema。只声明 apiVersion / kind / spec / status。
陷阱 2:缺少 subresources.status: {} 会导致无限协调循环
如果没有这一项,每次 kubectl patch --subresource=status 调用都会:
- 增加
.metadata.generation。 - 唤醒监听器。
- Operator 再次协调 → 再次修补状态。
- 回到第 1 步。
症状:Operator 看起来运行正常,但日志显示 reconcile loop 每隔几百毫秒就触发一次,永不停歇。CPU 使用率缓慢攀升。Apiserver 的 QPS 配额不断消耗。
修复方法:在每个已提供的版本中始终包含 subresources: { status: {} }。
陷阱 3:判别联合生成的 oneOf
z.discriminatedUnion(...) 和 Type.Union(...) 会转换为 oneOf。结构化 Schema 会拒绝在 x-kubernetes-validations(CEL)之外使用此字段。有两种修复方法:
- 宽松绕过方式:将该节点替换为
x-kubernetes-preserve-unknown-fields: true(会失去验证)。 - 严格 CEL 方式:生成
x-kubernetes-validations: [{ rule: "self.type == 'a' && has(self.payload)" }]。只有 apiserver ≥ 1.25 支持此方式。
第 2 步中的适配器默认选择方式 1。只有在验证至关重要时,才升级为方式 2。
陷阱 4:结构化 Schema 要求每个节点都包含 type
z.unknown() 会转换为空的 {} JSON Schema。结构化 Schema 会拒绝不含 type(或 enum,或绕过字段)的节点。在适配过程中,为每个空叶节点添加 x-kubernetes-preserve-unknown-fields: true。
陷阱 5:只能有一个 Schema 作为存储版本
所有已提供的版本中,每个 CRD 的 versions[N].storage: true 必须且只能有一项。存在多个 storage: true 时,apiserver 会在注册时拒绝该 CRD。从 v1beta1 添加 v1 时,只将其中一个版本切换为存储版本。
无需真实集群即可验证
在 CI 中启动 kind,并运行:
kubectl apply -f crd.yaml # registers the CRD
kubectl apply --dry-run=server -f every-example.yaml # validates each CR against the new schema
这正是 k1c 在 .github/workflows/k8s-validate.yml 中执行的操作——可查看该工作流作为参考任务。
为什么这很重要
CRD 是人类、apiserver 和 Operator 之间的契约。结构化 Schema 规则并非随意制定,而是为了让 apiserver 能够执行服务端验证、默认值设置和字段裁剪。但这些规则并未集中记录在一个地方:官方结构化 Schema 文档介绍了 Schema 方言,子资源文档介绍了 /status,而对 metadata 的禁止规定只会以 apiserver 运行时错误的形式出现。此技能将这四项内容整合到了一个工作流中。
相关技能
cloudflare-deploy— 如果你在 Cloudflare (CF) 上操作,可了解 Cloudflare 专属的资源结构(与 CRD 生成无关)。apm-usage— 了解如何将生成的 CRD 包作为技能制品发布。