Status: Draft (2026-10-17), Source + Read-only Measurement. Based on KubeVirt
v1.9.0(commit79d34c17). The webhook warning messages were read from the source and were not observed on an actual kubectl screen (no new VMs were created).
Reading KubeVirt Source Code 2: APIs and CRDs
Table of Contents
- 1. Intuition: Determined in Three Places
- 2. Types: types.go
- 3. Generated Code: Files Not to Edit Manually
- 4. Webhooks: Five Gates Through Which Requests Pass
- 5. Measurements
- 6. Limitations
- References
1. Intuition: Determined in Three Places
Three places are involved until a single line of VM YAML is accepted.
- Types —
types.go— The Go struct determines field names and shapes. - Schema — The OpenAPI schema generated from that struct is embedded in the CRD, allowing kube-apiserver to filter out malformed requests first.
- Webhooks — Requests that are structurally valid but semantically incorrect (such as mutually exclusive fields or disabled feature gates) are filtered out by virt-api webhooks. Instead of rejecting them, you can also attach warnings only .
2. Types: types.go
staging/src/kubevirt.io/api/core/v1/ Below, types.go is a single file spanning 4,090 lines. First, you only need to locate two structs.
VirtualMachineInstance(types.go:47) —Spec VirtualMachineInstanceSpecandStatus VirtualMachineInstanceStatus. The+genclient:noStatusmarker above is a generator instruction: "Do not create a client method that modifies status separately." The VMI status is shared among multiple virt-controllers and virt-handlers (Parts 3 and 4).VirtualMachine(:1942) —Spec VirtualMachineSpec(:2007) contains the template for the VMI (Template) and whether to keep it running (RunningorRunStrategy).
Running field has a Deprecated: comment attached (:2010). The reason it was kept without being deleted is shown in a single method: the one that folds the two fields into a single run strategy ( 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
}
Source: kubevirt/kubevirt v1.9.0 types.go lines 1961–1968 (Apache-2.0). Since the controller only calls this method, running: true immediately becomes Always inside the code. Five run strategy constants appear in the documentation, but a sixth one, WaitAsReceiver
(:1995, also exists in the code. This value is for VMs undergoing cross-cluster migration, and webhooks accept it only when the feature gate is enabled (Section 4 below).
The list of VMI phases (:1098) serves as a map when reading Parts 3 and 4. The comment attached to Scheduled states: "Here, virt-controller's responsibility ends and virt-handler takes over" (:1109).
3. Generated Code: Files Not to Edit Manually
make generate(Makefile:53) runs
hack/generate.sh inside the builder container. The generators sequentially called by this script are as follows.
| Generator | Input | Output | Measured Line Count |
|---|---|---|---|
swagger-doc |
types.go comments |
types_swagger_generated.go(API documentation descriptions) |
1,312 |
deepcopy-gen(generate.sh:27) |
+k8s:deepcopy-gen markers |
deepcopy_generated.go |
7,410 |
defaulter-gen |
Default functions | zz_generated.defaults.go |
— |
openapi-gen |
All types | client-go/api/openapi_generated.go |
40,507 |
client-gen |
+genclient markers |
client-go Typed client for |
— |
controller-gen crd(:139) |
+kubebuilder: markers |
CRD YAML → crd-validation-generator(:177) → virt-operator/…/validations_generated.go |
33,701 |
The last line is unique. KubeVirt does not keep CRD YAML in the repository; instead, it converts the schema generated by controller-gen into a Go string and embeds it inside virt-operator. When virt-operator installs them, it builds the CRDs programmatically via NewVirtualMachineCrd()
(crds.go:187) and attaches CRDsValidation["virtualmachine"]
(validations_generated.go:4444) as openAPIV3Schema . Therefore, a PR adding a single field to brings in hundreds of lines of generated files for a single line in types.go. Reviewers do not read the generated files; they only check whether rerunning make generate results in any differences (generate-verify). Manually edited generated files are rejected here.
4. Webhooks: Five Gates Through Which Requests Pass
Who Registers the Webhooks
Webhook configuration objects (ValidatingWebhookConfiguration) are also generated as code by virt-operator.
NewVirtAPIValidatingWebhookConfiguration()(webhooks.go:296inserts a webhook for each path and sets failurePolicy to Fail . The VM path is the constant VMValidatePath = "/virtualmachines-validate"
(:945). On the other side, virt-api registers an HTTP handler with the same constant (api.go:994). If the path name is changed on only one side, the webhook receives a 404, and due to the Fail policy, no one will be able to create VMs. This is why both files use the same constant.
VMsAdmitter.Admit() Admit() Line by Line
vms-admitter.go:87
's Admit() validates in this order:
- Checks whether the requested resource is truly
virtualmachinesand conforms to the schema (ValidateSchema). - ). If the VM is being deleted, it is allowed immediately to avoid competing with other deletions.
- Appliesinstancetypes and preferences to a copy, fills in default values, and validates the result. The comment notes: "Applied to a copy because we do not want to store this." In other words, validation is based on the "VMI that will actually run," but what goes into etcd remains exactly what the user sent.
- When creating (
Create) only: Checks whether fields with disabled feature gates were used and whether network configurations are correct. ValidateVirtualMachineSpec()— Templates, metadata, the entire VMI spec, DataVolume templates, run strategies, and live migration features.- Storage-side checks (
storageadmitters.Admit) and volume requests. - If it passes, warnings are collected and the request is allowed.
The final step is where spec.running warnings are emitted (: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.")
}
Source: kubevirt/kubevirt v1.9.0 vms-admitter.go lines 175–178 (Apache-2.0). Since it is a AdmissionResponse.Warnings rather than a rejection, the VM is created successfully, and kubectl displays the response header warnings as Warning: lines. warnDeprecatedAPIs()
(vmi-create-admitter.go:141) adds a warning when the spec uses a deprecated feature gate.
Run Strategy Validation
validateRunStrategy()(:231) checks three things. Using running and runStrategy together results in rejection; omitting both results in rejection with "RunStrategy must be specified"; and whether the
runStrategy value is one of the five allowed list items ( validRunStrategies(:53). WaitAsReceiver is not in this list and is only permitted when the DecentralizedLiveMigration feature gate is enabled.
Another Entrance Besides Webhooks: Subresources
virt-api is both a webhook server and an aggregated API server(v1.subresources.kubevirt.io). virtctl start does not modify the VM directly but calls
…/virtualmachines/<이름>/start . The handler StartVMRequestHandler()
(lifecycle.go:43) inspects the run strategy and, if it is
Halted , converts the VM's spec.runStrategy(or the legacy spec.running) into a JSON patch, or if it is Manual·RerunOnFailure , adds a start request to the status. Comments note that this has no meaning for strategies like Always that must already be running. Ultimately, subresources also only modify the VM object, and the actual creation of the VMI is handled by the VM controller in Part 3.
5. Measurements
Read from a small cluster running v1.9.0 (2026-10-17).
- Webhook configurations.
virt-api-validatorhas 23 validation webhooks,virt-api-mutatorhas 4 mutation webhooks, and all 27 arefailurePolicy: Fail. The VM webhooks usevirtualmachine-validator.kubevirt.io→/virtualmachines-validateand operationCREATE·UPDATE, matching the constants above. VMIs have separate create (/virtualmachineinstances-validate-create) and update (…-update) webhooks. CDI in the same cluster maintains four separate webhook configurations. - CRD Versions.
virtualmachines.kubevirt.ioBothvirtualmachineinstances.kubevirt.ioandv1·v1alpha3expose two versions, while the storage version (status.storedVersions) is av1single one. - Subresources.
/apis/subresources.kubevirt.io/v1exposes 31 subresources. The VM side includesstart·stop·restart·migrate, and the VMI side includesconsole·vnc·pause·freeze·addvolume. As seen in Part 2 "Until a Student VM Starts,"execdoes not exist. running: trueA VM Created via . A VM created by the lab backend still usesspec.running: true. That VM'sstatus.runStrategywasAlways. This is where the folding fromRunStrategy()extends all the way to status (recorded at vm.go:2478). Warnings received at creation time were not recorded by the backend, so they are unmeasured.
6. Limitations
- Warning messages were read from the source. Sending the same VM via server-side dry-run would allow viewing warnings without creating a VM, but we skipped this as we decided not to send any write requests to the cluster this time.
- The list of default values populated by mutating webhooks (
vm-mutator.go) was not traced. Most default values reside on the VMI side (vmi-mutator.go,pkg/defaults). - Generator versions and execution times are unmeasured(we did not run
make generate).
A Word from Titi. Don't be surprised that a PR adding a single field spans hundreds of lines. Only a few lines in types.go are actually written by humans; the rest are generated by machines. When reviewing, you only need to look at types.go, webhooks, and tests!
References
All accessed on 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 Documentation, Dynamic Admission Control (including warnings): https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/
- Sister series "Until a Student VM Starts" Part 2: Inside KubeVirt