Status: Draft (2025-10-17), Code + Read-Only Measurements. Based on CDI
v1.66.0(commit938af7c5) and KubeVirtv1.9.0. The measurements are from DataVolumes, PVCs, CDIConfigs, and StorageProfiles that have already completed importing, as well as from inside the pods of VMs using those PVCs.
Reading KubeVirt Source Code 6: CDI
Table of Contents
- 1. Intuition: Attaching a "Populating Method" to a PVC
- 2. Diagram: The Path of Importing
- 3. Code Paths
- 4. Measurements
- 5. Limitations
- References
1. Intuition: Attaching a "Populating Method" to a PVC
A PVC is an empty container. Since a VM needs a disk with an OS installed, someone must download an image and pour it into the container. DataVolume is a CRD that records "create a PVC like this and populate it from this source (HTTP, registry, another PVC, upload)" in a single object, and CDI is a bundle of controllers and worker pods that perform this task. Its storage and lifecycle are separate from KubeVirt, and it connects to VMs via dataVolumeTemplates . The VM controller creates the DataVolume (handleDataVolumes, vm.go:512), and the VMI controller does not create pods until the DataVolume is ready (areDataVolumesReady,
datavolumes.go:116). This corresponds to "Delaying pod creation while DataVolume populates" seen in Part 3.
2. Diagram: The Path of Importing
3. Code Paths
3.1 DataVolume Controller: Determining the Shape and Size of the PVC
The import DataVolume is handled by pkg/controller/datavolume/import-controller.go of syncImport()
(:195. The common part syncDvPvcState()(controller-base.go:486draws the spec of the PVC to be created, where two fields diverge (renderPvcSpec,
util.go:108).
spec.pvccopies as written.spec.storagefills empty access modes, volume modes, and storage classes with StorageProfile (pvcFromStorage, :118), and the size is inflated by the filesystem headroom.(renderPvcSpecVolumeSize, :308).
Next, it decides whether to use a volume populator (shouldUseCDIPopulator,
controller-base.go:1316). If the storage class belongs to a CSI driver, it uses one, which can be turned off with the DataVolume annotation cdi.kubevirt.io/storage.usePopulator . When used, it creates a CR containing the source
VolumeImportSource (reconcileVolumeImportSourceCR, import-controller.go:348), and makes the dataSourceRef of the target PVC point to it.
3.2 Populator: Pouring into the Twin PVC and Transferring the PV
Under Kubernetes' volume populator specification, nobody binds a PVC with dataSourceRef until the populator fills it. CDI's import populator does not touch the target PVC directly, but instead creates a twin with the same spec ( twin with the same spec(createPVCPrime,
populator-base.go:194). The name is prime-<목표 PVC UID> , annotated for immediate binding, and labeled to be excluded from backups.
PVC' is watched by the legacy import controller (pkg/controller/import-controller.go). It reads the annotation to create the importer pod
(createImporterPod, :531), and passes the pod status back to the annotations of PVC'. The populator side ( reconcileTargetPVC()
(import-populator.go:154) copies those annotations to the target PVC to report progress. Once PVC' finishes, it cc.Rebind(...)
(:206) to
change the PV's claimRef to the target PVC and bind it. Since the image-containing PV changes its owner entirely, no data copying occurs. Thanks to this design, the target PVC goes through only two states from start to finish: 「not bound yet → bound fully filled」, leaving no window for anyone to attach a half-full disk in between.
3.3 Importer: Step-by-Step Processing and Resizing
The core of the importer pod is the state machine of pkg/importer/data-processor.go (ProcessDataWithPause,
:241). Info(View Source Format) →
Transfer…(Receive) → Convert(qemu-img convert as raw) → Resize → Complete , and step functions return the next step.
The size is determined by resize()(:294).
For a filesystem volume, ResizeImage()(:331) expands the image to the smaller of the 「requested size」 and the 「available space」, where the available space is getUsableSpace()
(:396) →
util.GetUsableSpace() . For a block volume, it uses the raw device size as is.
3.4 Two Size Calculation Functions
pkg/util/util.go are inverse functions of each other.
func GetRequiredSpace(filesystemOverhead float64, requestedSpace int64) int64 {
alignedSize := RoundUp(requestedSpace, DefaultAlignBlockSize)
spaceWithOverhead := int64(math.Ceil(float64(alignedSize) * (1 + filesystemOverhead)))
return spaceWithOverhead
}
Source: containerized-data-importer v1.66.0 pkg/util/util.go , comments removed from lines 232–241 (Apache-2.0).
GetRequiredSpace, :232 is used when creating PVC requests
(via the DataVolume controller InflateSizeWithOverhead ), GetUsableSpace, :223
is used when determining the image size (importer). To summarize:
- PVC Request = ⌈ 1MiB Alignment(Request) × (1 + Overhead) ⌉
- Image Size = 1MiB Floor( ⌈ min(Free Space, PVC Request) ÷ (1 + Overhead) ⌉ )
For the overhead, CDIConfig's status.filesystemOverhead uses the storage class-specific value if available, and falls back to the global value otherwise (GetFilesystemOverheadForStorageClass, common/util.go:629). The global default is "0.06"(common.go:226), and the alignment unit DefaultAlignBlockSize is 1MiB (util.go:32). Block mode is not inflated (InflateSizeWithOverhead, :1572).
As a result, if you request 10Gi with spec.storage , the guest gets a 10GiB disk, and the PVC is larger by that margin. Conversely, if you request 10Gi with spec.pvc , the PVC is exactly 10Gi and the disk becomes nearly 6% smaller. Since the official documentation only states to "request a PVC large enough to hold both the image and filesystem metadata," a PR adding this calculation to the documentation (#4318, document PR by the author, currently under review) is open.
3.5 StorageProfile and Clone Strategy
StorageProfile is an object created by CDI for each storage class, populating two fields.
- claimPropertySets — Access modes and volume modes suitable for this class. Used to fill in the blanks left by
spec.storage(3.1). The source is a table found by the provisioner name (CapabilitiesByProvisionerKey(storagecapabilities.go:39), and it is empty if the provisioner is not in the table. If an access mode is not specified inspec.storagefor an empty class, a PVC cannot be created. - cloneStrategy —
reconcileCloneStrategy()(storageprofile-controller.go:247) is chosen in the order of: user-defined value → storage class annotationcdi.kubevirt.io/clone-strategy→ recommended value from the provisioner table → "snapshot if VolumeSnapshotClass exists, otherwise copy (host-assisted copy)".
When a DataVolume to clone a PVC arrives, clone/planner.go 's ChooseStrategy()
(:157) →
computeStrategyForSourcePVC()(:297) starts from the default snapshot(:323), sequentially reflects global overrides and the StorageProfile, and falls back to host-assisted copy if snapshot is chosen but no matching VolumeSnapshotClass exists (:354). What the documentation calls
smart cloneis this snapshot path (planSnapshotFromPVC, :670: snapshot → temporary PVC restore → Rebind), and csi-clone is the CSI driver's volume cloning (planCSIClone, :739),
copy is a host-assisted copy where pods are spun up for both the source and target to stream bytes (planHostAssistedFromPVC, :526).
4. Measurements
Read from a small cluster running v1.66.0 (2026-10-17). The target is a single root DataVolume populated with a registry image, and a VM using it.
Size. The DataVolume used access mode RWO, requested spec.storage in 10Gi, and was on a shared storage CSI class.
| What | Value | Calculation |
|---|---|---|
| DataVolume Request | 10Gi = 10,737,418,240 B | — |
PVC spec.resources.requests.storage |
11,381,663,335 B | ⌈10,737,418,240 × 1.06⌉ = 11,381,663,335 ✓ |
PVC status.capacity.storage |
11,381,663,335 B | Equal |
Guest Disk (qemu-img info, raw) |
10,737,418,240 B = 10 GiB | ⌈11,381,663,335 ÷ 1.06⌉ → 1MiB Floor = 10GiB ✓ |
| Actual Size Occupied by Disk File | 867MiB | Sparse file |
VMI's status.volumeStatus[].persistentVolumeClaimInfo.filesystemOverhead also had "0.06" written on it. Here is the answer to the question left open in Part 3 of "Until Student VMs Boot up" ("According to CDI docs it's 10Gi, design docs say about 9.4GiB — Unmeasured"). Using spec.storage results in 10GiB . 9.4GiB corresponds to the size when given 10Gi with spec.pvc (10Gi ÷ 1.06 → 1MiB Floor ≈ 9.43GiB).
Trace of the Filler. The target PVC's annotation still had cdi.kubevirt.io/storage.usePopulator: "true", cdi.kubevirt.io/storage.populator.pvcPrime: prime-<UID>,
cdi.kubevirt.io/storage.populator.progress: 100.0%, cdi.kubevirt.io/storage.pod.phase: Succeeded, cdi.kubevirt.io/storage.contentType: kubevirt
. This is the result of the filler copying PVC''s annotations to the target in Section 3.2. PVC' and the importer pod had already been deleted.
Configuration. CDIConfig's status.filesystemOverhead had global 0.06and all four storage classes set to 0.06 , and the feature gate
HonorWaitForFirstConsumer was enabled.
StorageProfile. Two CSI classes on shared storage had their claimPropertySets populated with ReadWriteMany·Filesystem (drivers present in the provisioner table). Two node-local classes had empty claimPropertySets because they were provisioners not found in the table. All four classes had cloneStrategy as copy , and there was not a single VolumeSnapshotClass in the cluster. In other words, cloning a PVC in that small cluster performs a full byte-by-byte copyinstead of a smart clone. This is the exact reason why Part 4 of "Until Student VMs Boot up" chose the write layer instead of PVC cloning for each student.
5. Limitations
- Since the importer pod is deleted after completion, step-by-step logs (Info → … → Resize) could not be observed. While they could be viewed by remeasuring with a new DataVolume tagged with annotation
cdi.kubevirt.io/storage.pod.retainAfterCompletion, it is left unmeasuredas we decided not to create a new one. - Cases where "the available space is smaller than the request" (where the provider provisions a smaller volume than requested) were not measured. Since shared storage often does not enforce PVC sizes, this did not occur in the measured cluster.
- Replication (snapshot, csi-clone, copy) was only reviewed in the code and not executed. Block mode is also unmeasured.
A word from Titi.
spec.storageandspec.pvcmight look like they differ by only one character, but the disk size differs by 6%. If you want to receive the guest disk as exactly 10GiB, usespec.storage, and keep in mind that the PVC will be slightly larger!
References
All accessed on 2026-10-17.
- CDI v1.66.0 source: https://github.com/kubevirt/containerized-data-importer/tree/v1.66.0
- DataVolume documentation: https://github.com/kubevirt/containerized-data-importer/blob/v1.66.0/doc/datavolumes.md
- CDIConfig documentation (filesystemOverhead): https://github.com/kubevirt/containerized-data-importer/blob/v1.66.0/doc/cdi-config.md
- PR #4318 adding size calculation to documentation (under review): https://github.com/kubevirt/containerized-data-importer/pull/4318
- Kubernetes volume populator: https://kubernetes.io/docs/concepts/storage/persistent-volumes/#volume-populators-and-data-sources
- Sister series "Until the Student VM Starts" Part 3: A Disk is a PVC, Part 4: Overlay from a PVC Perspective