状态:草稿 (2026-10-17),源码 + 只读实测。 基准为 KubeVirt
v1.9.0(提交79d34c17)。Webhook 警告文本是从源码中读取的,并非在实际 kubectl 界面中看到(未创建新 VM)。
KubeVirt 源码阅读 2:API 与 CRD
目录
1. 直觉:由三个地方决定
在 VM 的 YAML 一行被接受之前,有三个地方参与其中。
- 类型 —
types.go的 Go 结构体决定了字段的名称和形态。 - 架构 ——从该结构体生成的 OpenAPI 架构加载到 CRD 中,使 kube-apiserver 能够率先过滤掉形态错误的请求。
- Webhook ——形态正确但语义错误的请求(例如不能同时使用的两个字段、未开启的功能门槛)会被 virt-api 的 Webhook 过滤。也可以选择不拒绝请求,而只附加 警告 。
2. 类型:types.go
staging/src/kubevirt.io/api/core/v1/ 下面的 types.go 单个文件长达 4,090 行。首先只需找到两个结构体。
VirtualMachineInstance(types.go:47) —Spec VirtualMachineInstanceSpec与Status VirtualMachineInstanceStatus。上方的+genclient:noStatus标记是生成器的指令,意为“不要为单独修改 status 创建客户端方法”。VMI 的 status 由 virt-controller 和 virt-handler 多个组件共同分担(第 3、4 篇)。VirtualMachine(:1942) —Spec VirtualMachineSpec(:2007)中包含 VMI 的模板(Template)以及是否开启(Running或RunStrategy)。
Running 字段带有 Deprecated: 注释(:2010)。之所以没有直接删除并保留至今,原因可以从一个方法中看出。即将这两个字段折叠为单一运行策略的 RunStrategy()
(:1957)。
RunStrategy := RunStrategyHalted
if vm.Spec.Running != nil {
if (*vm.Spec.Running) == true {
RunStrategy = RunStrategyAlways
}
} else if vm.Spec.RunStrategy != nil {
RunStrategy = *vm.Spec.RunStrategy
}
来源:kubevirt/kubevirt v1.9.0 types.go 第 1961–1968 行 (Apache-2.0)。由于控制器仅调用此方法,因此 running: true 在代码中会直接变成 Always 。文档中提到了五种运行策略常量,但在代码中还有第六种 WaitAsReceiver
(:1995)。这是针对跨集群迁移接收端 VM 的值,只有在开启功能门槛时 Webhook 才会接受它(见下文第 4 节)。
VMI 阶段(phase)列表(:1098)将成为阅读第 3、4 篇时的地图。 Scheduled 附加的注释说明了“在此处 virt-controller 的责任结束,并由 virt-handler 接管”(:1109).
3. 生成代码:无需手动修改的文件
make generate(Makefile:53)会在构建容器内运行
hack/generate.sh 。该脚本按顺序调用的生成器如下。
| 生成器 | 输入 | 输出 | 实测行数 |
|---|---|---|---|
swagger-doc |
types.go 的注释 |
types_swagger_generated.go(API 文档说明) |
1,312 |
deepcopy-gen(generate.sh:27) |
+k8s:deepcopy-gen 标记 |
deepcopy_generated.go |
7,410 |
defaulter-gen |
默认值函数 | zz_generated.defaults.go |
— |
openapi-gen |
整个类型 | client-go/api/openapi_generated.go |
40,507 |
client-gen |
+genclient 标记 |
client-go 的类型化客户端 |
— |
controller-gen crd(:139) |
+kubebuilder: 标记 |
CRD YAML → crd-validation-generator(:177) → virt-operator/…/validations_generated.go |
33,701 |
最后一行很特别。KubeVirt 不会将 CRD YAML 保存在仓库中,而是将 controller-gen 生成的架构转换为 Go 字符串并放入 virt-operator 中。当 virt-operator 安装时,它会通过 NewVirtualMachineCrd()
(crds.go:187)在代码中创建 CRD,并将 CRDsValidation["virtualmachine"]
(validations_generated.go:4444)作为 openAPIV3Schema 附加。因此, 添加一个字段的 PR 会伴随 types.go 的一行改动带来数百行的生成文件。 审查者不会去阅读生成的文件,而只需检查 CI 是否重新运行了 make generate 并确认没有差异(generate-verify)。手动修改的生成文件在此会被剔除。
4. Webhook:请求经过的五道关卡
谁来注册 Webhook
Webhook 配置对象(ValidatingWebhookConfiguration)也由 virt-operator 通过代码生成。
NewVirtAPIValidatingWebhookConfiguration()(webhooks.go:296)为每个路径添加一个 Webhook,并将 failurePolicy 设为 Fail 。VM 的路径是常量 VMValidatePath = "/virtualmachines-validate"
(:945)。在另一端,virt-api 使用相同的常量挂载 HTTP 处理器(api.go:994)。如果仅在一端更改路径名称,Webhook 将收到 404,并且由于 Fail 策略,将导致任何人都无法创建 VM。这就是两个文件使用相同常量的原因。
VMsAdmitter.Admit() 逐行解析
vms-admitter.go:87
中的 Admit() 按以下顺序进行检查。
- 请求的资源是否确实是
virtualmachines,是否符合架构(ValidateSchema). - 如果是正在删除中的 VM,则直接放行。这是为了避免与其他删除操作产生竞争。
- 副本中应用 instancetype·preference 并填充默认值,然后对结果进行验证。注释中写道:“由于不想保存它,所以将其应用在副本上”。也就是说,验证基于“实际将要运行的 VMI”,但 etcd 中保存的仍然是用户发送的原始内容。
- 仅在创建时(
Create):是否使用了关闭功能门槛的字段,网络配置是否正确。 ValidateVirtualMachineSpec()——模板、元数据、VMI 整体规格、DataVolume 模板、运行策略、实时更新功能。- 存储方面的检查(
storageadmitters.Admit)与卷请求。 - 如果通过,则汇集警告并予以放行。
最后一个阶段是产生 spec.running 警告的地方(:175–178).
warnings := warnDeprecatedAPIs(&vm.Spec.Template.Spec, admitter.ClusterConfig)
if vm.Spec.Running != nil {
warnings = append(warnings, "spec.running is deprecated, please use spec.runStrategy instead.")
}
来源:kubevirt/kubevirt v1.9.0 vms-admitter.go 第 175–178 行 (Apache-2.0)。因为这是 AdmissionResponse.Warnings 警告 Warning: 而非拒绝,所以 VM 照常创建,kubectl 会在响应头中显示 warnDeprecatedAPIs()
(vmi-create-admitter.go:141)会在规格使用了已废弃的功能门槛状态时添加警告。
检查运行策略
validateRunStrategy()(:231)检查三项内容。如果同时使用 running 和 runStrategy 则予以拒绝,如果两者皆无则以 “RunStrategy must be specified” 拒绝,并且
runStrategy 的值必须是允许列表( validRunStrategies(:53)中的五种之一。 WaitAsReceiver 不在此列表中,只有在启用了 DecentralizedLiveMigration 功能门槛时才会单独通过。
除了 Webhook 的另一个入口:子资源
virt-api 既是 Webhook 服务器,也是 聚合 API 服务器(v1.subresources.kubevirt.io( virtctl start 不直接修改 VM,而是调用
…/virtualmachines/<이름>/start 。处理器 StartVMRequestHandler()
(lifecycle.go:43)会查看运行策略,如果是
Halted 则将 VM 的 spec.runStrategy(或旧的 spec.running)转换为 JSON Patch;如果是 Manual·RerunOnFailure 则将启动请求写入 status。注释中写道,对于像 Always 这样本来就必须处于开启状态的策略,这样做没有意义。归根结底,子资源也 只是修改了 VM 对象,而实际创建 VMI 的工作由第 3 篇中的 VM 控制器完成。
5. 实测
在安装了 v1.9.0 的小型集群中进行读取 (2026-10-17)。
- Webhook 配置。
virt-api-validator中有 23 个验证 Webhook,virt-api-mutator中有 4 个变更 Webhook,这 27 个全部都是failurePolicy: Fail。VM 的对应virtualmachine-validator.kubevirt.io→/virtualmachines-validate,操作为CREATE·UPDATE,与上述常量相同。VMI 的创建(/virtualmachineinstances-validate-create)和修改(…-update)是分开的。同一个集群中的 CDI 也单独配置了四个 Webhook。 - CRD 的版本。
virtualmachines.kubevirt.io和virtualmachineinstances.kubevirt.io都提供v1·v1alpha3两个版本,而存储版本(status.storedVersions)只有一个是v1。 - 子资源。
/apis/subresources.kubevirt.io/v1导出了 31 个。VM 侧有start·stop·restart·migrate等,VMI 侧有console·vnc·pause·freeze·addvolume等。正如在“学生 VM 启动过程”第 2 篇中所看到的,并没有exec。 running: true用 创建的 VM。实训后端创建的一个 VM 仍然在使用spec.running: true。该 VM 的status.runStrategy是Always。这正是上述RunStrategy()的折叠一直延续到了 status 中(写入 status 的位置是 vm.go:2478)。由于后端没有记录创建时收到的警告,因此属于 未实测。
6. 局限性
- 警告文本是从源码中读取的。如果将相同的 VM 发送到服务端的 dry-run,无需创建 VM 即可看到警告,但由于本次决定不向集群发送任何写入请求,因此未进行此操作。
- 变更 Webhook(
vm-mutator.go)填充的默认值列表未予跟进。绝大多数默认值都在 VMI 侧(vmi-mutator.go,pkg/defaults)中。 - 生成器的版本和运行时间属于 未实测(未运行
make generate)。
Titi 的寄语。 看到一个增加字段的 PR 包含数百行代码时,请不要惊讶。真正由人工编写的只有 types.go 的几行,其余都是由机器生成的。在进行代码审查时,只需关注 types.go、Webhook 以及测试即可!
参考
均于 2026-10-17 查阅。
types.go: https://github.com/kubevirt/kubevirt/blob/v1.9.0/staging/src/kubevirt.io/api/core/v1/types.govms-admitter.go: https://github.com/kubevirt/kubevirt/blob/v1.9.0/pkg/virt-api/webhooks/validating-webhook/admitters/vms-admitter.gohack/generate.sh: https://github.com/kubevirt/kubevirt/blob/v1.9.0/hack/generate.sh- Kubernetes 文档,动态准入控制(含警告): https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/
- 姊妹篇《学生 VM 启动过程》第 2 篇: KubeVirt 内部