TT Lab
Blog

한국어 English 日本語

Get started
Blog

Blog

Reading KubeVirt Source Code 2: APIs and CRDs — Types, Generated Code, Webhooks

Status: Draft (2026-10-17), Source + Read-only Measurement. Based on KubeVirt v1.9.0(commit 79d34c17). 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

Three places are involved until a single line of VM YAML is accepted.

  1. Types — types.go — The Go struct determines field names and shapes.
  2. Schema — The OpenAPI schema generated from that struct is embedded in the CRD, allowing kube-apiserver to filter out malformed requests first.
  3. 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.

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

types.go 한 파일에서 생겨나는 것들 make generate → hack/generate.sh. 생성 파일은 손으로 고치지 않는다 staging/src/kubevirt.io/api/core/v1/types.go Go 구조체 + 주석 마커(+genclient, +kubebuilder…) · 4,090줄 deepcopy-gen deepcopy_generated.go 7,410줄 swagger-doc types_swagger_generated.go 1,312줄 openapi-gen openapi_generated.go 40,507줄 · client-go/api client-gen client-go 클라이언트 kubevirt.io/client-go controller-gen crd → crd-validation-generator controller-gen 결과는 virt-operator 의 validations_generated.go(33,701줄)로 들어가 설치 때 CRD 의 openAPIV3Schema 가 된다(4절 그림의 ③ 관문). CI 는 make generate 뒤 차이가 있으면 떨어뜨린다(generate-verify).
Figure: Generated files originating from a single types.go

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

VM 하나를 apply 하면 — API 서버 안의 관문 왼쪽이 kube-apiserver 의 순서, 오른쪽이 그 관문을 맡은 KubeVirt 코드 ① 인증 · 인가 RBAC ② 변형 웹훅 기본값 채우기 virt-api /virtualmachines-mutate VMsMutator.Mutate ③ 스키마 검증 CRD 의 openAPIV3Schema virt-operator CRD 를 설치 validations_generated.go ④ 검증 웹훅 허용 · 거절 · 경고 virt-api /virtualmachines-validate VMsAdmitter.Admit ⑤ etcd 에 저장 그 뒤 컨트롤러가 본다 ④ 가 돌려준 경고는 kubectl 에 「Warning:」 줄로 보인다 spec.running is deprecated, please use spec.runStrategy instead. 두 웹훅 모두 failurePolicy: Fail — virt-api 가 응답하지 않으면 VM 을 만들지도 고치지도 못한다(실측: 검증 23개 · 변형 4개 모두 Fail)
Figure: The gates an applied VM passes through inside kube-apiserver and their code

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:

  1. Checks whether the requested resource is truly virtualmachines and conforms to the schema (ValidateSchema).
  2. ). If the VM is being deleted, it is allowed immediately to avoid competing with other deletions.
  3. 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.
  4. When creating (Create) only: Checks whether fields with disabled feature gates were used and whether network configurations are correct.
  5. ValidateVirtualMachineSpec() — Templates, metadata, the entire VMI spec, DataVolume templates, run strategies, and live migration features.
  6. Storage-side checks (storageadmitters.Admit) and volume requests.
  7. 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).

6. Limitations

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.

Sign in to like

Comments

No comments yet.

Sign in to leave a comment