From b8160c9d75ce2b8e2f3d771197e1742a292cb1bc Mon Sep 17 00:00:00 2001 From: Daniil Antoshin Date: Thu, 2 Jul 2026 12:02:39 +0200 Subject: [PATCH 1/2] fix(vm): resolve VMBDA-attached images during hotplug window The KVVM builder resolves block device images from maps built out of .spec/.status blockDeviceRefs, while the VMBDA-driven volume sync iterates the VMBDA list. During a hotplug attach KubeVirt adds the volume to KVVM before the device appears in .status.blockDeviceRefs, so the freshly attached image is missing from the maps and setBlockDeviceDisk aborts the whole reconcile with "image should exist in the cluster". Fill the builder maps with VMBDA-referenced images/disks not yet reflected in the refs. Scoped to the KVVM builder call site so block device readiness and finalizer logic are unaffected. Signed-off-by: Daniil Antoshin --- .../vm/internal/block_device_handler.go | 51 +++++++++++++++++++ .../pkg/controller/vm/internal/sync_kvvm.go | 6 +++ 2 files changed, 57 insertions(+) diff --git a/images/virtualization-artifact/pkg/controller/vm/internal/block_device_handler.go b/images/virtualization-artifact/pkg/controller/vm/internal/block_device_handler.go index 2af6194427..faea62522d 100644 --- a/images/virtualization-artifact/pkg/controller/vm/internal/block_device_handler.go +++ b/images/virtualization-artifact/pkg/controller/vm/internal/block_device_handler.go @@ -316,3 +316,54 @@ func (s *BlockDevicesState) Reload(ctx context.Context) error { s.VMBDAByBlockDeviceRef = vmbdaByRef return nil } + +// ResolveVMBDAAttachedDevices fills VIByName/CVIByName/VDByName with objects referenced +// by VMBDAs that are not yet reflected in the VM block device refs. +// +// During a hotplug attach KubeVirt adds the volume to KVVM before the device appears in +// .status.blockDeviceRefs. Reload builds the maps from those refs, so the freshly attached +// image is missing from them while the VMBDA-driven volume sync already tries to set its disk. +// Without this the KVVM builder fails to resolve the image and aborts the whole reconcile. +// +// Scoped to the KVVM builder on purpose: it must not be called from the block device status +// handler, whose readiness and finalizer logic operates on .spec/.status refs only. +func (s *BlockDevicesState) ResolveVMBDAAttachedDevices(ctx context.Context) error { + for ref := range s.VMBDAByBlockDeviceRef { + switch ref.Kind { + case v1alpha2.VMBDAObjectRefKindVirtualImage: + if _, ok := s.VIByName[ref.Name]; ok { + continue + } + vi, err := s.s.VirtualImage(ctx, ref.Name) + if err != nil { + return err + } + if vi != nil { + s.VIByName[ref.Name] = vi + } + case v1alpha2.VMBDAObjectRefKindClusterVirtualImage: + if _, ok := s.CVIByName[ref.Name]; ok { + continue + } + cvi, err := s.s.ClusterVirtualImage(ctx, ref.Name) + if err != nil { + return err + } + if cvi != nil { + s.CVIByName[ref.Name] = cvi + } + case v1alpha2.VMBDAObjectRefKindVirtualDisk: + if _, ok := s.VDByName[ref.Name]; ok { + continue + } + vd, err := s.s.VirtualDisk(ctx, ref.Name) + if err != nil { + return err + } + if vd != nil { + s.VDByName[ref.Name] = vd + } + } + } + return nil +} diff --git a/images/virtualization-artifact/pkg/controller/vm/internal/sync_kvvm.go b/images/virtualization-artifact/pkg/controller/vm/internal/sync_kvvm.go index a2de001ae5..373fce051d 100644 --- a/images/virtualization-artifact/pkg/controller/vm/internal/sync_kvvm.go +++ b/images/virtualization-artifact/pkg/controller/vm/internal/sync_kvvm.go @@ -525,6 +525,12 @@ func MakeKVVMFromVMSpec(ctx context.Context, s state.VirtualMachineState) (*virt if err != nil { return nil, fmt.Errorf("failed to reload blockdevice state for the virtual machine: %w", err) } + // Resolve images/disks attached via VMBDA but not yet reflected in block device refs, + // so the builder can set their disks during the hotplug attach window instead of aborting. + err = bdState.ResolveVMBDAAttachedDevices(ctx) + if err != nil { + return nil, fmt.Errorf("failed to resolve VMBDA-attached block devices for the virtual machine: %w", err) + } class, err := s.Class(ctx) if err != nil { return nil, err From 9fe0147f1500c6ff98604ff1e63498808a96b410 Mon Sep 17 00:00:00 2001 From: Daniil Antoshin Date: Thu, 2 Jul 2026 12:31:52 +0200 Subject: [PATCH 2/2] test(vm): cover VMBDA-attached device resolution during hotplug window Assert ResolveVMBDAAttachedDevices fills the builder maps with images and disks referenced by VMBDAs that are missing from the block device refs, and tolerates a VMBDA whose image does not exist. Signed-off-by: Daniil Antoshin --- .../vm/internal/block_devices_test.go | 63 +++++++++++++++++++ 1 file changed, 63 insertions(+) diff --git a/images/virtualization-artifact/pkg/controller/vm/internal/block_devices_test.go b/images/virtualization-artifact/pkg/controller/vm/internal/block_devices_test.go index 91794ff144..f1a9a62dc8 100644 --- a/images/virtualization-artifact/pkg/controller/vm/internal/block_devices_test.go +++ b/images/virtualization-artifact/pkg/controller/vm/internal/block_devices_test.go @@ -1582,6 +1582,69 @@ var _ = Describe("Capacity check", func() { }) }) +var _ = Describe("ResolveVMBDAAttachedDevices", func() { + var ctx context.Context + + BeforeEach(func() { + ctx = logger.ToContext(context.TODO(), slog.Default()) + }) + + It("fills the builder maps with VMBDA-attached devices missing from block device refs", func() { + const ns = "hotplug-race" + + vi := &v1alpha2.VirtualImage{ + ObjectMeta: metav1.ObjectMeta{Name: "vi-hotplug", Namespace: ns}, + Status: v1alpha2.VirtualImageStatus{Phase: v1alpha2.ImageReady}, + } + cvi := &v1alpha2.ClusterVirtualImage{ + ObjectMeta: metav1.ObjectMeta{Name: "cvi-hotplug"}, + Status: v1alpha2.ClusterVirtualImageStatus{Phase: v1alpha2.ImageReady}, + } + vd := &v1alpha2.VirtualDisk{ + ObjectMeta: metav1.ObjectMeta{Name: "vd-hotplug", Namespace: ns}, + } + + // Spec and Status BlockDeviceRefs are intentionally empty: this models the hotplug + // attach window where the volume is already added to KVVM but the device is not yet + // reflected in the VM block device refs. + vm := &v1alpha2.VirtualMachine{ + ObjectMeta: metav1.ObjectMeta{Name: "vm-hotplug-race", Namespace: ns}, + } + + newVMBDA := func(name string, kind v1alpha2.VMBDAObjectRefKind, refName string) *v1alpha2.VirtualMachineBlockDeviceAttachment { + return &v1alpha2.VirtualMachineBlockDeviceAttachment{ + ObjectMeta: metav1.ObjectMeta{Name: name, Namespace: ns}, + Spec: v1alpha2.VirtualMachineBlockDeviceAttachmentSpec{ + VirtualMachineName: vm.Name, + BlockDeviceRef: v1alpha2.VMBDAObjectRef{Kind: kind, Name: refName}, + }, + } + } + vmbdaVi := newVMBDA("vmbda-vi", v1alpha2.VMBDAObjectRefKindVirtualImage, vi.Name) + vmbdaCvi := newVMBDA("vmbda-cvi", v1alpha2.VMBDAObjectRefKindClusterVirtualImage, cvi.Name) + vmbdaVd := newVMBDA("vmbda-vd", v1alpha2.VMBDAObjectRefKindVirtualDisk, vd.Name) + // A VMBDA whose image does not exist: must be tolerated, not cause an error. + vmbdaMissing := newVMBDA("vmbda-missing", v1alpha2.VMBDAObjectRefKindClusterVirtualImage, "cvi-gone") + + _, _, vmState := setupEnvironment(vm, vi, cvi, vd, vmbdaVi, vmbdaCvi, vmbdaVd, vmbdaMissing) + + bdState := NewBlockDeviceState(vmState) + Expect(bdState.Reload(ctx)).To(Succeed()) + + // Reload builds the maps from block device refs only, which are empty here. + Expect(bdState.VIByName).To(BeEmpty()) + Expect(bdState.CVIByName).To(BeEmpty()) + Expect(bdState.VDByName).To(BeEmpty()) + + Expect(bdState.ResolveVMBDAAttachedDevices(ctx)).To(Succeed()) + + Expect(bdState.VIByName).To(HaveKey(vi.Name)) + Expect(bdState.CVIByName).To(HaveKey(cvi.Name)) + Expect(bdState.VDByName).To(HaveKey(vd.Name)) + Expect(bdState.CVIByName).NotTo(HaveKey("cvi-gone")) + }) +}) + func vmFactoryByVM(vm *v1alpha2.VirtualMachine) func() *v1alpha2.VirtualMachine { return func() *v1alpha2.VirtualMachine { return vm