From 74b4bb588dacebef4e7e3a239092c7ddd6954ae4 Mon Sep 17 00:00:00 2001 From: Daniil Loktev Date: Wed, 1 Jul 2026 18:57:42 +0300 Subject: [PATCH 1/4] wip Signed-off-by: Daniil Loktev --- api/core/v1alpha2/vmbdacondition/condition.go | 2 + .../controller/vmbda/internal/life_cycle.go | 23 +++++++ .../vmbda/internal/life_cycle_test.go | 63 +++++++++++++++++++ 3 files changed, 88 insertions(+) create mode 100644 images/virtualization-artifact/pkg/controller/vmbda/internal/life_cycle_test.go diff --git a/api/core/v1alpha2/vmbdacondition/condition.go b/api/core/v1alpha2/vmbdacondition/condition.go index 19d30e18cd..4e664c79ea 100644 --- a/api/core/v1alpha2/vmbdacondition/condition.go +++ b/api/core/v1alpha2/vmbdacondition/condition.go @@ -65,6 +65,8 @@ const ( Conflict AttachedReason = "Conflict" // DeviceNotAvailableOnNode indicates that the block device's PersistentVolume is not available on the node where the virtual machine is running. DeviceNotAvailableOnNode AttachedReason = "DeviceNotAvailableOnNode" + // BlockedByMigration indicates that a ReadWriteOnce block device cannot be hot-plugged while the virtual machine is migrating. + BlockedByMigration AttachedReason = "BlockedByMigration" // CapacityAvailable signifies that the capacity not reached and attaching available. CapacityAvailable DiskAttachmentCapacityAvailableReason = "CapacityAvailable" diff --git a/images/virtualization-artifact/pkg/controller/vmbda/internal/life_cycle.go b/images/virtualization-artifact/pkg/controller/vmbda/internal/life_cycle.go index 6dfce1a459..dde49cdcb5 100644 --- a/images/virtualization-artifact/pkg/controller/vmbda/internal/life_cycle.go +++ b/images/virtualization-artifact/pkg/controller/vmbda/internal/life_cycle.go @@ -20,8 +20,10 @@ import ( "context" "errors" "fmt" + "slices" "time" + corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" virtv1 "kubevirt.io/api/core/v1" "sigs.k8s.io/controller-runtime/pkg/reconcile" @@ -31,6 +33,7 @@ import ( "github.com/deckhouse/virtualization-controller/pkg/logger" "github.com/deckhouse/virtualization/api/core/v1alpha2" "github.com/deckhouse/virtualization/api/core/v1alpha2/vmbdacondition" + "github.com/deckhouse/virtualization/api/core/v1alpha2/vmcondition" ) type LifeCycleHandler struct { @@ -253,6 +256,17 @@ func (h LifeCycleHandler) Handle(ctx context.Context, vmbda *v1alpha2.VirtualMac } if pvc != nil { + if isVirtualMachineMigrating(vm) && isReadWriteOnce(pvc) { + log.Info("Cannot hot-plug a ReadWriteOnce disk while the virtual machine is migrating") + + vmbda.Status.Phase = v1alpha2.BlockDeviceAttachmentPhasePending + cb. + Status(metav1.ConditionFalse). + Reason(vmbdacondition.BlockedByMigration). + Message("Cannot hot-plug a ReadWriteOnce disk while the virtual machine is migrating.") + return reconcile.Result{}, nil + } + available, err := h.attacher.IsPVAvailableOnVMNode(ctx, pvc, kvvmi) if err != nil { return reconcile.Result{}, err @@ -301,3 +315,12 @@ func (h LifeCycleHandler) Handle(ctx context.Context, vmbda *v1alpha2.VirtualMac return reconcile.Result{}, canErr } } + +func isVirtualMachineMigrating(vm *v1alpha2.VirtualMachine) bool { + migrating, _ := conditions.GetCondition(vmcondition.TypeMigrating, vm.Status.Conditions) + return migrating.Status == metav1.ConditionTrue +} + +func isReadWriteOnce(pvc *corev1.PersistentVolumeClaim) bool { + return !slices.Contains(pvc.Spec.AccessModes, corev1.ReadWriteMany) +} diff --git a/images/virtualization-artifact/pkg/controller/vmbda/internal/life_cycle_test.go b/images/virtualization-artifact/pkg/controller/vmbda/internal/life_cycle_test.go new file mode 100644 index 0000000000..685e23b674 --- /dev/null +++ b/images/virtualization-artifact/pkg/controller/vmbda/internal/life_cycle_test.go @@ -0,0 +1,63 @@ +/* +Copyright 2026 Flant JSC + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package internal + +import ( + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "github.com/deckhouse/virtualization/api/core/v1alpha2" + "github.com/deckhouse/virtualization/api/core/v1alpha2/vmcondition" +) + +var _ = Describe("LifeCycleHandler helpers", func() { + DescribeTable("isVirtualMachineMigrating", func(status metav1.ConditionStatus, hasCondition, expected bool) { + vm := &v1alpha2.VirtualMachine{} + if hasCondition { + vm.Status.Conditions = []metav1.Condition{ + { + Type: vmcondition.TypeMigrating.String(), + Status: status, + }, + } + } + Expect(isVirtualMachineMigrating(vm)).To(Equal(expected)) + }, + Entry("migrating condition is true", metav1.ConditionTrue, true, true), + Entry("migrating condition is false", metav1.ConditionFalse, true, false), + Entry("migrating condition is unknown", metav1.ConditionUnknown, true, false), + Entry("migrating condition is absent", metav1.ConditionTrue, false, false), + ) + + DescribeTable("isReadWriteOnce", func(accessModes []corev1.PersistentVolumeAccessMode, expected bool) { + pvc := &corev1.PersistentVolumeClaim{ + Spec: corev1.PersistentVolumeClaimSpec{ + AccessModes: accessModes, + }, + } + Expect(isReadWriteOnce(pvc)).To(Equal(expected)) + }, + Entry("ReadWriteOnce", []corev1.PersistentVolumeAccessMode{corev1.ReadWriteOnce}, true), + Entry("ReadWriteOncePod", []corev1.PersistentVolumeAccessMode{corev1.ReadWriteOncePod}, true), + Entry("ReadOnlyMany", []corev1.PersistentVolumeAccessMode{corev1.ReadOnlyMany}, true), + Entry("no access modes", []corev1.PersistentVolumeAccessMode{}, true), + Entry("ReadWriteMany", []corev1.PersistentVolumeAccessMode{corev1.ReadWriteMany}, false), + Entry("ReadWriteMany among others", []corev1.PersistentVolumeAccessMode{corev1.ReadWriteOnce, corev1.ReadWriteMany}, false), + ) +}) From 83fa9470ce01520dca2d2147d1550815a71b3638 Mon Sep 17 00:00:00 2001 From: Daniil Loktev Date: Thu, 2 Jul 2026 11:01:06 +0300 Subject: [PATCH 2/4] wip Signed-off-by: Daniil Loktev --- .../pkg/controller/vmbda/internal/life_cycle.go | 8 ++++---- .../pkg/controller/vmbda/internal/life_cycle_test.go | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/images/virtualization-artifact/pkg/controller/vmbda/internal/life_cycle.go b/images/virtualization-artifact/pkg/controller/vmbda/internal/life_cycle.go index dde49cdcb5..3e2f830a38 100644 --- a/images/virtualization-artifact/pkg/controller/vmbda/internal/life_cycle.go +++ b/images/virtualization-artifact/pkg/controller/vmbda/internal/life_cycle.go @@ -257,13 +257,13 @@ func (h LifeCycleHandler) Handle(ctx context.Context, vmbda *v1alpha2.VirtualMac if pvc != nil { if isVirtualMachineMigrating(vm) && isReadWriteOnce(pvc) { - log.Info("Cannot hot-plug a ReadWriteOnce disk while the virtual machine is migrating") + log.Info("Cannot hotplug a ReadWriteOnce disk while the virtual machine is migrating") vmbda.Status.Phase = v1alpha2.BlockDeviceAttachmentPhasePending cb. Status(metav1.ConditionFalse). Reason(vmbdacondition.BlockedByMigration). - Message("Cannot hot-plug a ReadWriteOnce disk while the virtual machine is migrating.") + Message("Cannot hotplug a ReadWriteOnce disk while the virtual machine is migrating.") return reconcile.Result{}, nil } @@ -317,8 +317,8 @@ func (h LifeCycleHandler) Handle(ctx context.Context, vmbda *v1alpha2.VirtualMac } func isVirtualMachineMigrating(vm *v1alpha2.VirtualMachine) bool { - migrating, _ := conditions.GetCondition(vmcondition.TypeMigrating, vm.Status.Conditions) - return migrating.Status == metav1.ConditionTrue + _, migrating := conditions.GetCondition(vmcondition.TypeMigrating, vm.Status.Conditions) + return migrating } func isReadWriteOnce(pvc *corev1.PersistentVolumeClaim) bool { diff --git a/images/virtualization-artifact/pkg/controller/vmbda/internal/life_cycle_test.go b/images/virtualization-artifact/pkg/controller/vmbda/internal/life_cycle_test.go index 685e23b674..e599b7c522 100644 --- a/images/virtualization-artifact/pkg/controller/vmbda/internal/life_cycle_test.go +++ b/images/virtualization-artifact/pkg/controller/vmbda/internal/life_cycle_test.go @@ -40,8 +40,8 @@ var _ = Describe("LifeCycleHandler helpers", func() { Expect(isVirtualMachineMigrating(vm)).To(Equal(expected)) }, Entry("migrating condition is true", metav1.ConditionTrue, true, true), - Entry("migrating condition is false", metav1.ConditionFalse, true, false), - Entry("migrating condition is unknown", metav1.ConditionUnknown, true, false), + Entry("migrating condition is false", metav1.ConditionFalse, true, true), + Entry("migrating condition is unknown", metav1.ConditionUnknown, true, true), Entry("migrating condition is absent", metav1.ConditionTrue, false, false), ) From dee4a53981d97881c1e1eb57da5390407f31b821 Mon Sep 17 00:00:00 2001 From: Daniil Loktev Date: Thu, 2 Jul 2026 11:22:46 +0300 Subject: [PATCH 3/4] wip Signed-off-by: Daniil Loktev --- .../pkg/controller/vmbda/internal/watcher/vm_watcher.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/images/virtualization-artifact/pkg/controller/vmbda/internal/watcher/vm_watcher.go b/images/virtualization-artifact/pkg/controller/vmbda/internal/watcher/vm_watcher.go index efca5f6e84..a08c556e8b 100644 --- a/images/virtualization-artifact/pkg/controller/vmbda/internal/watcher/vm_watcher.go +++ b/images/virtualization-artifact/pkg/controller/vmbda/internal/watcher/vm_watcher.go @@ -61,6 +61,12 @@ func (w VirtualMachineWatcher) Watch(mgr manager.Manager, ctr controller.Control return true } + _, oldMigrating := conditions.GetCondition(vmcondition.TypeMigrating, e.ObjectOld.Status.Conditions) + _, newMigrating := conditions.GetCondition(vmcondition.TypeMigrating, e.ObjectNew.Status.Conditions) + if oldMigrating != newMigrating { + return true + } + return w.hasBlockDeviceAttachmentChanges(e.ObjectOld, e.ObjectNew) }, }, From ba27620d6f611e63a7ed8a5c75dc0f50a6b2a8ed Mon Sep 17 00:00:00 2001 From: Daniil Loktev Date: Mon, 6 Jul 2026 17:37:16 +0300 Subject: [PATCH 4/4] forbid rwx as well Signed-off-by: Daniil Loktev --- api/core/v1alpha2/vmbdacondition/condition.go | 2 +- .../controller/vmbda/internal/life_cycle.go | 28 ++++++++----------- .../vmbda/internal/life_cycle_test.go | 17 ----------- 3 files changed, 12 insertions(+), 35 deletions(-) diff --git a/api/core/v1alpha2/vmbdacondition/condition.go b/api/core/v1alpha2/vmbdacondition/condition.go index 4e664c79ea..23500d3c5c 100644 --- a/api/core/v1alpha2/vmbdacondition/condition.go +++ b/api/core/v1alpha2/vmbdacondition/condition.go @@ -65,7 +65,7 @@ const ( Conflict AttachedReason = "Conflict" // DeviceNotAvailableOnNode indicates that the block device's PersistentVolume is not available on the node where the virtual machine is running. DeviceNotAvailableOnNode AttachedReason = "DeviceNotAvailableOnNode" - // BlockedByMigration indicates that a ReadWriteOnce block device cannot be hot-plugged while the virtual machine is migrating. + // BlockedByMigration indicates that a block device cannot be hot-plugged while the virtual machine is migrating. BlockedByMigration AttachedReason = "BlockedByMigration" // CapacityAvailable signifies that the capacity not reached and attaching available. diff --git a/images/virtualization-artifact/pkg/controller/vmbda/internal/life_cycle.go b/images/virtualization-artifact/pkg/controller/vmbda/internal/life_cycle.go index 3e2f830a38..105aa02728 100644 --- a/images/virtualization-artifact/pkg/controller/vmbda/internal/life_cycle.go +++ b/images/virtualization-artifact/pkg/controller/vmbda/internal/life_cycle.go @@ -20,10 +20,8 @@ import ( "context" "errors" "fmt" - "slices" "time" - corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" virtv1 "kubevirt.io/api/core/v1" "sigs.k8s.io/controller-runtime/pkg/reconcile" @@ -250,23 +248,23 @@ func (h LifeCycleHandler) Handle(ctx context.Context, vmbda *v1alpha2.VirtualMac } if ad.PVCName != "" { + if isVirtualMachineMigrating(vm) { + log.Info("Cannot hotplug a disk while the virtual machine is migrating") + + vmbda.Status.Phase = v1alpha2.BlockDeviceAttachmentPhasePending + cb. + Status(metav1.ConditionFalse). + Reason(vmbdacondition.BlockedByMigration). + Message("Cannot hotplug a disk while the virtual machine is migrating.") + return reconcile.Result{}, nil + } + pvc, err := h.attacher.GetPersistentVolumeClaim(ctx, ad) if err != nil { return reconcile.Result{}, err } if pvc != nil { - if isVirtualMachineMigrating(vm) && isReadWriteOnce(pvc) { - log.Info("Cannot hotplug a ReadWriteOnce disk while the virtual machine is migrating") - - vmbda.Status.Phase = v1alpha2.BlockDeviceAttachmentPhasePending - cb. - Status(metav1.ConditionFalse). - Reason(vmbdacondition.BlockedByMigration). - Message("Cannot hotplug a ReadWriteOnce disk while the virtual machine is migrating.") - return reconcile.Result{}, nil - } - available, err := h.attacher.IsPVAvailableOnVMNode(ctx, pvc, kvvmi) if err != nil { return reconcile.Result{}, err @@ -320,7 +318,3 @@ func isVirtualMachineMigrating(vm *v1alpha2.VirtualMachine) bool { _, migrating := conditions.GetCondition(vmcondition.TypeMigrating, vm.Status.Conditions) return migrating } - -func isReadWriteOnce(pvc *corev1.PersistentVolumeClaim) bool { - return !slices.Contains(pvc.Spec.AccessModes, corev1.ReadWriteMany) -} diff --git a/images/virtualization-artifact/pkg/controller/vmbda/internal/life_cycle_test.go b/images/virtualization-artifact/pkg/controller/vmbda/internal/life_cycle_test.go index e599b7c522..5a3c381da0 100644 --- a/images/virtualization-artifact/pkg/controller/vmbda/internal/life_cycle_test.go +++ b/images/virtualization-artifact/pkg/controller/vmbda/internal/life_cycle_test.go @@ -19,7 +19,6 @@ package internal import ( . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" - corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "github.com/deckhouse/virtualization/api/core/v1alpha2" @@ -44,20 +43,4 @@ var _ = Describe("LifeCycleHandler helpers", func() { Entry("migrating condition is unknown", metav1.ConditionUnknown, true, true), Entry("migrating condition is absent", metav1.ConditionTrue, false, false), ) - - DescribeTable("isReadWriteOnce", func(accessModes []corev1.PersistentVolumeAccessMode, expected bool) { - pvc := &corev1.PersistentVolumeClaim{ - Spec: corev1.PersistentVolumeClaimSpec{ - AccessModes: accessModes, - }, - } - Expect(isReadWriteOnce(pvc)).To(Equal(expected)) - }, - Entry("ReadWriteOnce", []corev1.PersistentVolumeAccessMode{corev1.ReadWriteOnce}, true), - Entry("ReadWriteOncePod", []corev1.PersistentVolumeAccessMode{corev1.ReadWriteOncePod}, true), - Entry("ReadOnlyMany", []corev1.PersistentVolumeAccessMode{corev1.ReadOnlyMany}, true), - Entry("no access modes", []corev1.PersistentVolumeAccessMode{}, true), - Entry("ReadWriteMany", []corev1.PersistentVolumeAccessMode{corev1.ReadWriteMany}, false), - Entry("ReadWriteMany among others", []corev1.PersistentVolumeAccessMode{corev1.ReadWriteOnce, corev1.ReadWriteMany}, false), - ) })