Files
cozystack/packages/apps/vm-disk/tests/dv_source_test.yaml
T
Myasnikov Daniil f73729937c docs(vm-disk): record what immediate binding costs on a node-pinned class
Requesting immediate binding for every source, not just `upload`, is
deliberate: a VMDisk is a standalone object populated before any VM
consumes it, so on a WaitForFirstConsumer StorageClass a disk with no VM
attached would never populate at all.

What that costs was recorded nowhere an operator reads. On a node-pinned
class the CDI worker pod chooses the node, so the volume binds where the
importer landed rather than where the VM will run, and a VM constrained
elsewhere -- a nodeSelector, a request only one node satisfies, or the
Windows affinity from _cluster.scheduling.dedicatedNodesForWindowsVMs --
stays unschedulable against a node-affinity conflict. One disk is enough
for that; it is not a multi-disk-only case. storageClass is immutable,
so the exit is delete and recreate. The chart default `replicated` binds
Immediate regardless and does not move.

Also pinned in tests, in both directions the annotation is emitted: per
source type (blank, image, disk) so a regression to the old upload-only
condition reddens, and on the existing-DataVolume path, where `helm
upgrade` retrofits the annotation onto disks created before it was
unconditional -- inert for a volume that has already bound, and now a
recorded decision rather than an unasserted side effect.

Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
Assisted-By: LLM
2026-09-05 14:51:05 +05:00

175 lines
6.5 KiB
YAML

suite: vm-disk DataVolume source rendering on initial creation
# When no DataVolume exists yet, dv.yaml builds spec.source from .Values.source.
# This pins the refactor that routes every source type through the shared
# $desiredSource variable (used both for rendering and for the immutability
# comparison), so a regression there is caught without a live cluster.
release:
name: test-vm-disk
namespace: tenant-test
templates:
- templates/dv.yaml
tests:
- it: records an empty storage-class annotation when created with an empty storageClass
# Provenance is recorded even for "" (cluster default), so a later change to a
# concrete class is caught. Annotation-key presence (not its value)
# distinguishes a recorded empty class from a legacy disk with no annotation.
set:
storageClass: ""
asserts:
- equal:
path: metadata.annotations["vm-disk.cozystack.io/storage-class"]
value: ""
- it: treats an explicit null source as blank without crashing
# A direct HelmRelease consumer may write source: null; every .Values.source
# access must tolerate it (default dict) rather than crash hasKey on nil.
set:
source: null
asserts:
- notFailedTemplate: {}
- equal:
path: spec.source.blank
value: {}
- it: renders a blank source by default and records it in the source annotation
asserts:
- equal:
path: spec.source.blank
value: {}
- equal:
path: spec.storage.storageClassName
value: replicated
- equal:
path: metadata.annotations["vm-disk.cozystack.io/source"]
value: '{}'
# Immediate binding is requested for every source type, not only upload.
# A standalone VMDisk has no consumer to wait for, so on a
# WaitForFirstConsumer class the volume would otherwise never populate;
# the placement trade-off that buys is in README.md. Pinned per source so
# a regression to the old upload-only condition reddens here.
- equal:
path: metadata.annotations["cdi.kubevirt.io/storage.bind.immediate.requested"]
value: "true"
- it: renders an http source from values and records it in the source annotation
set:
source:
http:
url: https://example.com/img.qcow2
asserts:
- equal:
path: spec.source.http.url
value: https://example.com/img.qcow2
# The create-path annotation write is the linchpin of the immutability
# guard: later renders compare the operator's input against it, so a
# regression here would silently degrade every new disk to the lossy
# spec-compare fallback.
- equal:
path: metadata.annotations["vm-disk.cozystack.io/source"]
value: '{"http":{"url":"https://example.com/img.qcow2"}}'
# storageClass provenance is recorded at creation too, so a later clear-to-empty is caught.
- equal:
path: metadata.annotations["vm-disk.cozystack.io/storage-class"]
value: replicated
- it: records a query-string http URL with json-escaped special chars in the annotation
# Load-bearing round-trip: the source guard is a byte-for-byte string compare,
# so toJson's HTML-escaping of & < > (common in presigned S3 URLs) must be
# recorded verbatim and recompute identically. Locks the escaping so a future
# toJson change trips here instead of deadlocking every &-URL disk in prod.
set:
source:
http:
url: https://s3.example.com/x?a=1&b=2&c=<>
asserts:
- equal:
path: spec.source.http.url
value: https://s3.example.com/x?a=1&b=2&c=<>
- equal:
path: metadata.annotations["vm-disk.cozystack.io/source"]
value: '{"http":{"url":"https://s3.example.com/x?a=1\u0026b=2\u0026c=\u003c\u003e"}}'
- it: renders an image source as a cozy-public pvc clone
set:
source:
image:
name: ubuntu
asserts:
- equal:
path: spec.source.pvc.name
value: vm-default-images-ubuntu
- equal:
path: spec.source.pvc.namespace
value: cozy-public
- equal:
path: metadata.annotations["vm-disk.cozystack.io/source"]
value: '{"image":{"name":"ubuntu"}}'
# Immediate binding is requested for every source type, not only upload.
# A standalone VMDisk has no consumer to wait for, so on a
# WaitForFirstConsumer class the volume would otherwise never populate;
# the placement trade-off that buys is in README.md. Pinned per source so
# a regression to the old upload-only condition reddens here.
- equal:
path: metadata.annotations["cdi.kubevirt.io/storage.bind.immediate.requested"]
value: "true"
- it: renders a disk source as an in-namespace pvc clone
set:
source:
disk:
name: other
asserts:
- equal:
path: spec.source.pvc.name
value: vm-disk-other
- equal:
path: spec.source.pvc.namespace
value: tenant-test
- equal:
path: metadata.annotations["vm-disk.cozystack.io/source"]
value: '{"disk":{"name":"other"}}'
# Immediate binding is requested for every source type, not only upload.
# A standalone VMDisk has no consumer to wait for, so on a
# WaitForFirstConsumer class the volume would otherwise never populate;
# the placement trade-off that buys is in README.md. Pinned per source so
# a regression to the old upload-only condition reddens here.
- equal:
path: metadata.annotations["cdi.kubevirt.io/storage.bind.immediate.requested"]
value: "true"
- it: renders an upload source and requests immediate bind
set:
source:
upload: {}
asserts:
- equal:
path: spec.source.upload
value: {}
- equal:
path: metadata.annotations["cdi.kubevirt.io/storage.bind.immediate.requested"]
value: "true"
- equal:
path: metadata.annotations["vm-disk.cozystack.io/source"]
value: '{"upload":{}}'
- it: rejects more than one source type
set:
source:
http:
url: https://example.com/img.qcow2
image:
name: ubuntu
asserts:
- failedTemplate:
errorMessage: Exactly one type of source is expected!
- it: rejects an unrecognized single source type with a clear message
set:
source:
snapshot:
name: snap1
asserts:
- failedTemplate:
errorPattern: 'Unknown source type'