mcpField
K
k8s-crd-from-typed-schema

k8s-crd-from-typed-schema

github.com/mizchi云与 DevOps41648 次安装1 周前更新
代码仓库

关于 k8s-crd-from-typed-schema

帮助开发者将类型化 schema 转换为 Kubernetes CRD 的实用技能,涵盖 Structural Schema 适配、CRD 包装和验证。重点说明容易遗漏的字段限制、status 子资源与版本配置问题。

主要功能

  1. 01适配 JSON Schema 到 Structural Schema
  2. 02处理不兼容 schema 特性
  3. 03生成 CRD 外层结构
  4. 04计算资源复数名称
  5. 05使用 kubectl 服务端 dry-run 验证
  6. 06说明 metadata 与 status 陷阱

适用场景

  1. 01从 zod 等类型定义生成 CRD
  2. 02排查 kubectl 拒绝 CRD 的错误
  3. 03避免 operator 因 status 更新循环
  4. 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 调用都会:

  1. 增加 .metadata.generation。
  2. 唤醒监听器。
  3. Operator 再次协调 → 再次修补状态。
  4. 回到第 1 步。

症状:Operator 看起来运行正常,但日志显示 reconcile loop 每隔几百毫秒就触发一次,永不停歇。CPU 使用率缓慢攀升。Apiserver 的 QPS 配额不断消耗。

修复方法:在每个已提供的版本中始终包含 subresources: { status: {} }。

陷阱 3:判别联合生成的 oneOf

z.discriminatedUnion(...) 和 Type.Union(...) 会转换为 oneOf。结构化 Schema 会拒绝在 x-kubernetes-validations(CEL)之外使用此字段。有两种修复方法:

  1. 宽松绕过方式:将该节点替换为 x-kubernetes-preserve-unknown-fields: true(会失去验证)。
  2. 严格 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 包作为技能制品发布。

评论

支持 Markdown