Files
Aleksei Sviridkin fa9f09e3d8 fix(vm-disk)!: refuse a golden-image clone CDI would leave unbound (#4796)
## What this PR does

A VM disk created from a golden image into a StorageClass other than the
image's one can hang forever: its temporary clone PVC stays unbound, and
the user sees nothing. With the default `csi-clone` strategy, CDI checks
the source and target classes for a common provisioner, a matching
volume mode and room to expand. It never compares class names. So a
target class backed by the same provisioner gets no host-assisted
fallback, and the clone is left to the CSI driver. LINSTOR creates it
next to the source data, so on a target class that does not allow remote
volume access from every node (a node-local class, for example) the PVC
may never bind.

The vm-disk chart now refuses such a disk at render time and tells the
user which StorageClass to set. The check follows CDI's own clone
planner. The effective strategy comes from the CDI override, or else
from the target StorageProfile. It has to be `csi-clone`, or `snapshot`
when CDI gets a VolumeSnapshotClass of the provisioner. Once the
snapshot path has such a class it runs the same checks as csi-clone, and
LINSTOR ships one. Both classes have to share a driver (the golden's is
read from its PV, as CDI does), the volume modes have to match, and the
target class has to allow expansion. On top of that the chart refuses
only a LINSTOR target whose `allowRemoteVolumeAccess`, read the way
linstor-csi reads it, is not true; without the parameter that means a
layer list with no DRBD or NVMe layer. A remote-accessible LINSTOR class
and other drivers render. When the provisioner or the volume mode
differs, CDI falls back to a host-assisted copy, which completes, so the
disk renders as before. A target class without expansion is left alone
too, see the follow-ups.

The class check compares nothing when a lookup it needs returns nothing:
under `helm template`, before the golden PVC exists, or without the CDI
resource. An empty `storageClass` is compared as the class CDI would
pick for a kubevirt DataVolume, which is the default virt class,
otherwise the Kubernetes default class. Existing disks are exempt,
because their DataVolume spec is immutable. A disk counts as existing
when its DataVolume or its PVC exists, since CDI can garbage-collect a
completed DataVolume. Under `snapshot`, a snapshot class that both
StorageProfiles name but that does not exist is refused too, for any
driver, because CDI then fails to plan the clone instead of falling
back.

This is a breaking change: a new disk in a LINSTOR class without remote
access from every node rendered before and is refused now, and so is one
whose StorageProfiles name a snapshot class that does not exist. The
first case can include a clone that would happen to work: on a class
with `Immediate` binding, which I have not verified, or when the node
the clone is placed on already holds a replica of the golden image. The
way out is the golden image's class or the `copy` clone strategy.

How this covers the three fix directions in the issue:

1. Omitting `cloneStrategyOverride` when it is empty already landed on
main in #4138: the template skips the field for an empty or null value,
the values schema accepts the empty string, and the chart tests pin
both. This PR does not touch it.
2. I kept `csi-clone` as the default on purpose. Same-class clones are
the common case, and `csi-clone` is the fast path for them. Changing the
default is a separate decision, tracked in #4850.
3. vm-disk refuses the mismatch where the clone can hang, as described
above. That is narrower than the issue proposed, which was to refuse
every mismatch: every case CDI falls back on completes, and a clone into
a remote-accessible LINSTOR class should complete too. The last part
comes from reading the CDI and linstor-csi sources, I have not run it on
a cluster.

Follow-ups this PR does not cover:

- When several classes claim the default, CDI picks the newest one. The
chart does not reproduce that tie-break and compares nothing in that
case.
- The chart does not compare sizes. A target class without volume
expansion is never refused for remote access, even when the disk is no
larger than the golden image and CDI would attempt the clone.
- Under `snapshot` on a cluster that does not serve the snapshot API,
StorageProfiles naming a snapshot class still make CDI fail, and the
chart does not look that class up.
- A disk cloned from another disk (`source.disk`) into another class of
the same provisioner can hang the same way, and nothing refuses it yet.
- Whether `csi-clone` should remain the platform default now that it can
be turned off: #4850.

Closes #2908

### Screenshots

Not a UI change.

### Downstream repositories

- [x] No downstream repository is affected by this change
- [ ] [cozystack/website](https://github.com/cozystack/website) -
follow-up:
- [ ]
[cozystack/terraform-provider-cozystack](https://github.com/cozystack/terraform-provider-cozystack)
- follow-up:
- [ ]
[cozystack/ansible-cozystack](https://github.com/cozystack/ansible-cozystack)
- follow-up:
- [ ] [cozystack/ccp](https://github.com/cozystack/ccp) - follow-up:
- [ ] [cozystack/talm](https://github.com/cozystack/talm) - follow-up:
- [ ] [cozystack/cozyhr](https://github.com/cozystack/cozyhr) -
follow-up:
- [ ] [cozystack/cozy-proxy](https://github.com/cozystack/cozy-proxy) -
follow-up:
- [ ]
[cozystack/cozystack-telemetry-server](https://github.com/cozystack/cozystack-telemetry-server)
- follow-up:
- [ ]
[cozystack/external-apps-example](https://github.com/cozystack/external-apps-example)
- follow-up:
- [ ] [cozystack/examples](https://github.com/cozystack/examples) -
follow-up:
- [ ] [cozystack/community](https://github.com/cozystack/community) -
follow-up:

No values, schema or ApplicationDefinition change. Re-check notes for
the next CDI and linstor-csi bumps sit next to the version pins in the
kubevirt-cdi-operator and linstor Makefiles. The new note in the vm-disk
README reaches the website through the regular reference-page
regeneration.

### Release note

```release-note
fix(vm-disk)!: a new disk from a golden image is now refused with a clear error when CDI would CSI-clone it into another LINSTOR StorageClass that does not allow remote volume access from every node. Before, such a disk could stay pending forever with no error. A clone that would have landed on a node already holding a replica of the golden image is refused as well; use the golden image's StorageClass or the copy clone strategy.
```

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **Bug Fixes**
* Prevented image-backed disks from being rendered when CDI’s selected
clone strategy cannot safely clone across differing storage classes,
including applicable LINSTOR remote-access and snapshot-class checks.
* Existing DataVolumes and same-namespace PVCs are not blocked by these
checks. Disk creation can proceed when required information is
unavailable or the selected strategy supports the operation.
* **Documentation**
* Clarified when image-backed disks may be refused and recommended using
the golden image’s storage class or the copy strategy as alternatives.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-10-12 17:24:29 +03:00
..
2025-09-18 16:27:38 +02:00
2024-10-16 18:35:30 +02:00
2025-09-24 12:32:37 +02:00

Virtual Machine Disk

A Virtual Machine Disk

storageClass and source are annotated as immutable in the chart schema — see docs/storage-immutability.md for the contract and which consumers enforce it. DataVolume.spec is immutable in CDI, so the chart also fails the release on an attempted storageClass/source edit of an existing disk rather than silently reusing the old spec; delete and recreate the disk to change either field.

Whether CDI has finished populating the disk shows up in the WorkloadsReady condition, not in Ready, which follows the Helm release and does not wait on the DataVolume: WorkloadsReady stays False while an import, clone or upload is in flight and when the DataVolume fails. Its message names the DataVolume and the phase it is in, followed by the error CDI reports for the pod populating the disk when there is one, such as a 404 or an unresolvable host on an HTTP source: CDI keeps retrying such an import, so its phase stays ImportInProgress, and the error is shown between retries rather than while a retry is running. kubectl get vmdisks shows the condition in the WORKLOADS column, and the console badge reads DataVolumeNotReady until it is True. A disk created before this behaviour was introduced keeps its old release settings, under which Ready may wait on the DataVolume, until its application is next updated. A source.upload disk reads False from the moment it is created until virtctl image-upload completes, because CDI reports waiting for the upload and receiving it as one phase. A disk counts as ready while it waits for its consumer with bindImmediately off on a WaitForFirstConsumer class, because that is a state such a disk is supposed to rest in.

bindImmediately is on by default, so a disk's volume is provisioned when the disk is created rather than deferred to the first consumer. That default exists because a VMDisk is a standalone object with a lifecycle of its own: on a WaitForFirstConsumer StorageClass a disk that no VM has attached yet would otherwise never populate at all. It costs something on a node-pinned class such as local, where the CDI worker pod becomes the first consumer and the volume lands where that pod was scheduled rather than where the VM will run, so a VM whose placement is constrained (a nodeSelector, a resource request only one node satisfies, or the Windows affinity applied by _cluster.scheduling.dedicatedNodesForWindowsVMs) can stay unschedulable against a node-affinity conflict, and storageClass is immutable so the only exit is deleting and recreating the disk. Turn bindImmediately off for a disk a VMInstance will consume on such a class: KubeVirt then renders a temporary launcher pod carrying the VM's own affinity and node selector, and the volume binds where the VM can actually run. The chart default replicated binds Immediate regardless, so neither setting changes anything there.

A disk created from source.image should use the StorageClass its golden image lives in, replicated in the default layout. When CDI clones with the csi-clone strategy, the platform default, into a different StorageClass of the same CSI driver with the same volume mode and room to expand, it does not fall back to a host-assisted copy and leaves the clone to the driver. The snapshot strategy does the same when it gets a VolumeSnapshotClass of that driver: the class both StorageProfiles name, or, when they do not name the same one, any class of the driver. LINSTOR creates that clone next to the source data, so on a target class that does not allow remote volume access from every node, such as a node-local class, the clone may never bind. The chart refuses such a new disk when the golden's driver and the target class's provisioner are both LINSTOR and the target's allowRemoteVolumeAccess is not a value linstor-csi reads as true (1, t or true, in any of the cases Go's strconv.ParseBool accepts); any other value is a list of rules, and the chart counts it as access from anywhere only when one rule names no fromSame label. Without that parameter it reads the target's layer list the way linstor-csi does, which allows remote access when it has a DRBD or NVMe layer, and a class that sets no layer list has DRBD. Under snapshot, it also refuses a disk whose golden and target StorageProfiles name the same VolumeSnapshotClass when that class does not exist and the golden's driver is the target class's provisioner, whatever the driver: CDI then fails to plan the clone instead of falling back. Both refusals compare only what they can look up: without the golden image's PVC, the cdi resource or the target's StorageClass nothing is compared. The remote access refusal also needs the target's StorageProfile with a claim property set and the golden's driver, taken from its bound PV or else from its StorageClass; when any of them is missing, the disk renders. Neither applies to a disk whose DataVolume or PVC already exists. Use the golden image's StorageClass instead, or ask the platform administrator to set cloneStrategyOverride: copy in the kubevirt-cdi package, which clones through a host-assisted copy in every case. The remote access refusal leaves alone a target class that allows remote volume access, a driver other than LINSTOR, a target class of another driver, a different volume mode, a class without volume expansion, snapshot that gets no VolumeSnapshotClass of the driver, and the copy strategy. An empty storageClass is compared as the class CDI would pick for it: the class annotated storageclass.kubevirt.io/is-default-virt-class, otherwise the class annotated storageclass.kubernetes.io/is-default-class; when several classes carry the annotation that decides, nothing is compared.

Parameters

Common parameters

Name Description Type Value
source The source image location used to create a disk. object {}
source.image Use image by name from default collection. *object null
source.image.name Name of the image to use. string ""
source.upload Upload local image. *object null
source.http Download image from an HTTP source. *object null
source.http.url URL to download the image. string ""
source.disk Clone an existing vm-disk. *object null
source.disk.name Name of the vm-disk to clone. string ""
optical Defines if disk should be considered optical. bool false
storage The size of the disk allocated for the virtual machine. quantity 5Gi
storageClass StorageClass used to store the data. string replicated
bindImmediately Provision the volume at disk creation instead of waiting for a consumer. bool true