diff --git a/images/virtualization-artifact/pkg/builder/vmbda/vd.go b/images/virtualization-artifact/pkg/builder/vmbda/vmbda.go similarity index 100% rename from images/virtualization-artifact/pkg/builder/vmbda/vd.go rename to images/virtualization-artifact/pkg/builder/vmbda/vmbda.go diff --git a/images/virtualization-artifact/pkg/controller/vmbda/internal/interfaces.go b/images/virtualization-artifact/pkg/controller/vmbda/internal/interfaces.go new file mode 100644 index 0000000000..dc7b5adb3e --- /dev/null +++ b/images/virtualization-artifact/pkg/controller/vmbda/internal/interfaces.go @@ -0,0 +1,33 @@ +/* +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 ( + "context" + + virtv1 "kubevirt.io/api/core/v1" + + "github.com/deckhouse/virtualization/api/core/v1alpha2" +) + +//go:generate go tool moq -rm -out mock.go . AttachmentService + +type AttachmentService interface { + GetVirtualMachine(ctx context.Context, name, namespace string) (*v1alpha2.VirtualMachine, error) + GetKVVMI(ctx context.Context, vm *v1alpha2.VirtualMachine) (*virtv1.VirtualMachineInstance, error) + GetKVVM(ctx context.Context, vm *v1alpha2.VirtualMachine) (*virtv1.VirtualMachine, error) +} diff --git a/images/virtualization-artifact/pkg/controller/vmbda/internal/mock.go b/images/virtualization-artifact/pkg/controller/vmbda/internal/mock.go new file mode 100644 index 0000000000..8f0e6440c4 --- /dev/null +++ b/images/virtualization-artifact/pkg/controller/vmbda/internal/mock.go @@ -0,0 +1,189 @@ +// Code generated by moq; DO NOT EDIT. +// github.com/matryer/moq + +package internal + +import ( + "context" + "github.com/deckhouse/virtualization/api/core/v1alpha2" + virtv1 "kubevirt.io/api/core/v1" + "sync" +) + +// Ensure, that AttachmentServiceMock does implement AttachmentService. +// If this is not the case, regenerate this file with moq. +var _ AttachmentService = &AttachmentServiceMock{} + +// AttachmentServiceMock is a mock implementation of AttachmentService. +// +// func TestSomethingThatUsesAttachmentService(t *testing.T) { +// +// // make and configure a mocked AttachmentService +// mockedAttachmentService := &AttachmentServiceMock{ +// GetKVVMFunc: func(ctx context.Context, vm *v1alpha2.VirtualMachine) (*virtv1.VirtualMachine, error) { +// panic("mock out the GetKVVM method") +// }, +// GetKVVMIFunc: func(ctx context.Context, vm *v1alpha2.VirtualMachine) (*virtv1.VirtualMachineInstance, error) { +// panic("mock out the GetKVVMI method") +// }, +// GetVirtualMachineFunc: func(ctx context.Context, name string, namespace string) (*v1alpha2.VirtualMachine, error) { +// panic("mock out the GetVirtualMachine method") +// }, +// } +// +// // use mockedAttachmentService in code that requires AttachmentService +// // and then make assertions. +// +// } +type AttachmentServiceMock struct { + // GetKVVMFunc mocks the GetKVVM method. + GetKVVMFunc func(ctx context.Context, vm *v1alpha2.VirtualMachine) (*virtv1.VirtualMachine, error) + + // GetKVVMIFunc mocks the GetKVVMI method. + GetKVVMIFunc func(ctx context.Context, vm *v1alpha2.VirtualMachine) (*virtv1.VirtualMachineInstance, error) + + // GetVirtualMachineFunc mocks the GetVirtualMachine method. + GetVirtualMachineFunc func(ctx context.Context, name string, namespace string) (*v1alpha2.VirtualMachine, error) + + // calls tracks calls to the methods. + calls struct { + // GetKVVM holds details about calls to the GetKVVM method. + GetKVVM []struct { + // Ctx is the ctx argument value. + Ctx context.Context + // VM is the vm argument value. + VM *v1alpha2.VirtualMachine + } + // GetKVVMI holds details about calls to the GetKVVMI method. + GetKVVMI []struct { + // Ctx is the ctx argument value. + Ctx context.Context + // VM is the vm argument value. + VM *v1alpha2.VirtualMachine + } + // GetVirtualMachine holds details about calls to the GetVirtualMachine method. + GetVirtualMachine []struct { + // Ctx is the ctx argument value. + Ctx context.Context + // Name is the name argument value. + Name string + // Namespace is the namespace argument value. + Namespace string + } + } + lockGetKVVM sync.RWMutex + lockGetKVVMI sync.RWMutex + lockGetVirtualMachine sync.RWMutex +} + +// GetKVVM calls GetKVVMFunc. +func (mock *AttachmentServiceMock) GetKVVM(ctx context.Context, vm *v1alpha2.VirtualMachine) (*virtv1.VirtualMachine, error) { + if mock.GetKVVMFunc == nil { + panic("AttachmentServiceMock.GetKVVMFunc: method is nil but AttachmentService.GetKVVM was just called") + } + callInfo := struct { + Ctx context.Context + VM *v1alpha2.VirtualMachine + }{ + Ctx: ctx, + VM: vm, + } + mock.lockGetKVVM.Lock() + mock.calls.GetKVVM = append(mock.calls.GetKVVM, callInfo) + mock.lockGetKVVM.Unlock() + return mock.GetKVVMFunc(ctx, vm) +} + +// GetKVVMCalls gets all the calls that were made to GetKVVM. +// Check the length with: +// +// len(mockedAttachmentService.GetKVVMCalls()) +func (mock *AttachmentServiceMock) GetKVVMCalls() []struct { + Ctx context.Context + VM *v1alpha2.VirtualMachine +} { + var calls []struct { + Ctx context.Context + VM *v1alpha2.VirtualMachine + } + mock.lockGetKVVM.RLock() + calls = mock.calls.GetKVVM + mock.lockGetKVVM.RUnlock() + return calls +} + +// GetKVVMI calls GetKVVMIFunc. +func (mock *AttachmentServiceMock) GetKVVMI(ctx context.Context, vm *v1alpha2.VirtualMachine) (*virtv1.VirtualMachineInstance, error) { + if mock.GetKVVMIFunc == nil { + panic("AttachmentServiceMock.GetKVVMIFunc: method is nil but AttachmentService.GetKVVMI was just called") + } + callInfo := struct { + Ctx context.Context + VM *v1alpha2.VirtualMachine + }{ + Ctx: ctx, + VM: vm, + } + mock.lockGetKVVMI.Lock() + mock.calls.GetKVVMI = append(mock.calls.GetKVVMI, callInfo) + mock.lockGetKVVMI.Unlock() + return mock.GetKVVMIFunc(ctx, vm) +} + +// GetKVVMICalls gets all the calls that were made to GetKVVMI. +// Check the length with: +// +// len(mockedAttachmentService.GetKVVMICalls()) +func (mock *AttachmentServiceMock) GetKVVMICalls() []struct { + Ctx context.Context + VM *v1alpha2.VirtualMachine +} { + var calls []struct { + Ctx context.Context + VM *v1alpha2.VirtualMachine + } + mock.lockGetKVVMI.RLock() + calls = mock.calls.GetKVVMI + mock.lockGetKVVMI.RUnlock() + return calls +} + +// GetVirtualMachine calls GetVirtualMachineFunc. +func (mock *AttachmentServiceMock) GetVirtualMachine(ctx context.Context, name string, namespace string) (*v1alpha2.VirtualMachine, error) { + if mock.GetVirtualMachineFunc == nil { + panic("AttachmentServiceMock.GetVirtualMachineFunc: method is nil but AttachmentService.GetVirtualMachine was just called") + } + callInfo := struct { + Ctx context.Context + Name string + Namespace string + }{ + Ctx: ctx, + Name: name, + Namespace: namespace, + } + mock.lockGetVirtualMachine.Lock() + mock.calls.GetVirtualMachine = append(mock.calls.GetVirtualMachine, callInfo) + mock.lockGetVirtualMachine.Unlock() + return mock.GetVirtualMachineFunc(ctx, name, namespace) +} + +// GetVirtualMachineCalls gets all the calls that were made to GetVirtualMachine. +// Check the length with: +// +// len(mockedAttachmentService.GetVirtualMachineCalls()) +func (mock *AttachmentServiceMock) GetVirtualMachineCalls() []struct { + Ctx context.Context + Name string + Namespace string +} { + var calls []struct { + Ctx context.Context + Name string + Namespace string + } + mock.lockGetVirtualMachine.RLock() + calls = mock.calls.GetVirtualMachine + mock.lockGetVirtualMachine.RUnlock() + return calls +} diff --git a/images/virtualization-artifact/pkg/controller/vmbda/internal/suite_test.go b/images/virtualization-artifact/pkg/controller/vmbda/internal/suite_test.go new file mode 100644 index 0000000000..b5fafd16d8 --- /dev/null +++ b/images/virtualization-artifact/pkg/controller/vmbda/internal/suite_test.go @@ -0,0 +1,29 @@ +/* +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 ( + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +func TestVirtualMachineBlockDeviceAttachment(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "VirtualMachineBlockDeviceAttachment Handlers Suite") +} diff --git a/images/virtualization-artifact/pkg/controller/vmbda/internal/virtual_machine_ready.go b/images/virtualization-artifact/pkg/controller/vmbda/internal/virtual_machine_ready.go index 280c090882..2cd8f710cc 100644 --- a/images/virtualization-artifact/pkg/controller/vmbda/internal/virtual_machine_ready.go +++ b/images/virtualization-artifact/pkg/controller/vmbda/internal/virtual_machine_ready.go @@ -25,16 +25,15 @@ import ( "sigs.k8s.io/controller-runtime/pkg/reconcile" "github.com/deckhouse/virtualization-controller/pkg/controller/conditions" - "github.com/deckhouse/virtualization-controller/pkg/controller/service" "github.com/deckhouse/virtualization/api/core/v1alpha2" "github.com/deckhouse/virtualization/api/core/v1alpha2/vmbdacondition" ) type VirtualMachineReadyHandler struct { - attachment *service.AttachmentService + attachment AttachmentService } -func NewVirtualMachineReadyHandler(attachment *service.AttachmentService) *VirtualMachineReadyHandler { +func NewVirtualMachineReadyHandler(attachment AttachmentService) *VirtualMachineReadyHandler { return &VirtualMachineReadyHandler{ attachment: attachment, } @@ -72,7 +71,7 @@ func (h VirtualMachineReadyHandler) Handle(ctx context.Context, vmbda *v1alpha2. } switch vm.Status.Phase { - case v1alpha2.MachineRunning: + case v1alpha2.MachineRunning, v1alpha2.MachineMigrating: // OK. case v1alpha2.MachineStopping, v1alpha2.MachineStopped, v1alpha2.MachineStarting: vmbda.Status.Phase = v1alpha2.BlockDeviceAttachmentPhasePending diff --git a/images/virtualization-artifact/pkg/controller/vmbda/internal/virtual_machine_ready_test.go b/images/virtualization-artifact/pkg/controller/vmbda/internal/virtual_machine_ready_test.go new file mode 100644 index 0000000000..24e37c65f9 --- /dev/null +++ b/images/virtualization-artifact/pkg/controller/vmbda/internal/virtual_machine_ready_test.go @@ -0,0 +1,208 @@ +/* +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 ( + "context" + "errors" + "time" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/utils/ptr" + virtv1 "kubevirt.io/api/core/v1" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + + vmbdaBuilder "github.com/deckhouse/virtualization-controller/pkg/builder/vmbda" + "github.com/deckhouse/virtualization-controller/pkg/controller/conditions" + "github.com/deckhouse/virtualization/api/core/v1alpha2" + "github.com/deckhouse/virtualization/api/core/v1alpha2/vmbdacondition" +) + +var _ = Describe("VirtualMachineReadyHandler Handle", func() { + var ( + vmbda *v1alpha2.VirtualMachineBlockDeviceAttachment + attachmentServiceMock AttachmentServiceMock + ) + + BeforeEach(func() { + vmbda = vmbdaBuilder.NewEmpty("vmbda", "default") + attachmentServiceMock = AttachmentServiceMock{ + GetVirtualMachineFunc: func(_ context.Context, _, _ string) (*v1alpha2.VirtualMachine, error) { + return nil, nil + }, + GetKVVMIFunc: func(_ context.Context, _ *v1alpha2.VirtualMachine) (*virtv1.VirtualMachineInstance, error) { + return nil, nil + }, + GetKVVMFunc: func(_ context.Context, _ *v1alpha2.VirtualMachine) (*virtv1.VirtualMachine, error) { + return nil, nil + }, + } + }) + + It("should set VirtualMachineReady condition reason to unknown if deletion timestamp is not nil", func() { + vmbda.DeletionTimestamp = ptr.To(metav1.Time{Time: time.Now()}) + result, err := NewVirtualMachineReadyHandler(&attachmentServiceMock).Handle(context.Background(), vmbda) + Expect(result).To(Equal(reconcile.Result{})) + Expect(err).NotTo(HaveOccurred()) + + vmReadyCondition, ok := conditions.GetCondition(vmbdacondition.VirtualMachineReadyType, vmbda.Status.Conditions) + Expect(ok).To(BeTrue()) + Expect(vmReadyCondition.Reason).To(Equal(conditions.ReasonUnknown.String())) + }) + + It("should set VirtualMachineReady condition to false if virtual machine not found", func() { + result, err := NewVirtualMachineReadyHandler(&attachmentServiceMock).Handle(context.Background(), vmbda) + Expect(result).To(Equal(reconcile.Result{})) + Expect(err).NotTo(HaveOccurred()) + + vmReadyCondition, ok := conditions.GetCondition(vmbdacondition.VirtualMachineReadyType, vmbda.Status.Conditions) + Expect(ok).To(BeTrue()) + Expect(vmReadyCondition.Status).To(Equal(metav1.ConditionFalse)) + Expect(vmReadyCondition.Reason).To(Equal(vmbdacondition.VirtualMachineNotReady.String())) + }) + + DescribeTable("should set VirtualMachineReady condition based on vm phase if kvvm and kvvmi exist", func(phase v1alpha2.MachinePhase, expectedStatus metav1.ConditionStatus, expectedReason string) { + attachmentServiceMock.GetVirtualMachineFunc = func(_ context.Context, _, _ string) (*v1alpha2.VirtualMachine, error) { + return &v1alpha2.VirtualMachine{ + Status: v1alpha2.VirtualMachineStatus{ + Phase: phase, + }, + }, nil + } + attachmentServiceMock.GetKVVMFunc = func(_ context.Context, _ *v1alpha2.VirtualMachine) (*virtv1.VirtualMachine, error) { + return &virtv1.VirtualMachine{}, nil + } + attachmentServiceMock.GetKVVMIFunc = func(ctx context.Context, _ *v1alpha2.VirtualMachine) (*virtv1.VirtualMachineInstance, error) { + return &virtv1.VirtualMachineInstance{}, nil + } + + result, err := NewVirtualMachineReadyHandler(&attachmentServiceMock).Handle(context.Background(), vmbda) + Expect(result).To(Equal(reconcile.Result{})) + Expect(err).NotTo(HaveOccurred()) + + vmReadyCondition, ok := conditions.GetCondition(vmbdacondition.VirtualMachineReadyType, vmbda.Status.Conditions) + Expect(ok).To(BeTrue()) + Expect(vmReadyCondition.Status).To(Equal(expectedStatus)) + Expect(vmReadyCondition.Reason).To(Equal(expectedReason)) + }, + Entry("Running", v1alpha2.MachineRunning, metav1.ConditionTrue, vmbdacondition.VirtualMachineReady.String()), + Entry("Migrating", v1alpha2.MachineMigrating, metav1.ConditionTrue, vmbdacondition.VirtualMachineReady.String()), + Entry("Stopping", v1alpha2.MachineStopping, metav1.ConditionFalse, vmbdacondition.NotAttached.String()), + Entry("Stopped", v1alpha2.MachineStopped, metav1.ConditionFalse, vmbdacondition.NotAttached.String()), + Entry("Starting", v1alpha2.MachineStarting, metav1.ConditionFalse, vmbdacondition.NotAttached.String()), + Entry("Degraded", v1alpha2.MachineDegraded, metav1.ConditionFalse, vmbdacondition.VirtualMachineNotReady.String()), + Entry("Terminating", v1alpha2.MachineTerminating, metav1.ConditionFalse, vmbdacondition.VirtualMachineNotReady.String()), + Entry("Pause", v1alpha2.MachinePause, metav1.ConditionFalse, vmbdacondition.VirtualMachineNotReady.String()), + Entry("Pending", v1alpha2.MachinePending, metav1.ConditionFalse, vmbdacondition.VirtualMachineNotReady.String()), + ) + + It("should set VirtualMachineReady condition to false if kvvm is not found", func() { + attachmentServiceMock.GetVirtualMachineFunc = func(_ context.Context, _, _ string) (*v1alpha2.VirtualMachine, error) { + return &v1alpha2.VirtualMachine{ + Status: v1alpha2.VirtualMachineStatus{ + Phase: v1alpha2.MachineRunning, + }, + }, nil + } + attachmentServiceMock.GetKVVMFunc = func(_ context.Context, _ *v1alpha2.VirtualMachine) (*virtv1.VirtualMachine, error) { + return nil, nil + } + + result, err := NewVirtualMachineReadyHandler(&attachmentServiceMock).Handle(context.Background(), vmbda) + Expect(result).To(Equal(reconcile.Result{})) + Expect(err).NotTo(HaveOccurred()) + + vmReadyCondition, ok := conditions.GetCondition(vmbdacondition.VirtualMachineReadyType, vmbda.Status.Conditions) + Expect(ok).To(BeTrue()) + Expect(vmReadyCondition.Status).To(Equal(metav1.ConditionFalse)) + Expect(vmReadyCondition.Reason).To(Equal(vmbdacondition.VirtualMachineNotReady.String())) + }) + + It("should set VirtualMachineReady condition to false if kvvmi is not found", func() { + attachmentServiceMock.GetVirtualMachineFunc = func(_ context.Context, _, _ string) (*v1alpha2.VirtualMachine, error) { + return &v1alpha2.VirtualMachine{ + Status: v1alpha2.VirtualMachineStatus{ + Phase: v1alpha2.MachineRunning, + }, + }, nil + } + attachmentServiceMock.GetKVVMFunc = func(_ context.Context, _ *v1alpha2.VirtualMachine) (*virtv1.VirtualMachine, error) { + return &virtv1.VirtualMachine{}, nil + } + attachmentServiceMock.GetKVVMIFunc = func(ctx context.Context, _ *v1alpha2.VirtualMachine) (*virtv1.VirtualMachineInstance, error) { + return nil, nil + } + + result, err := NewVirtualMachineReadyHandler(&attachmentServiceMock).Handle(context.Background(), vmbda) + Expect(result).To(Equal(reconcile.Result{})) + Expect(err).NotTo(HaveOccurred()) + + vmReadyCondition, ok := conditions.GetCondition(vmbdacondition.VirtualMachineReadyType, vmbda.Status.Conditions) + Expect(ok).To(BeTrue()) + Expect(vmReadyCondition.Status).To(Equal(metav1.ConditionFalse)) + Expect(vmReadyCondition.Reason).To(Equal(vmbdacondition.VirtualMachineNotReady.String())) + }) + + It("should return error if getting virtual machine fails", func() { + attachmentServiceMock.GetVirtualMachineFunc = func(_ context.Context, _, _ string) (*v1alpha2.VirtualMachine, error) { + return nil, errors.New("test error") + } + + result, err := NewVirtualMachineReadyHandler(&attachmentServiceMock).Handle(context.Background(), vmbda) + Expect(result).To(Equal(reconcile.Result{})) + Expect(err).To(HaveOccurred()) + }) + + It("should return error if getting kvvm fails", func() { + attachmentServiceMock.GetVirtualMachineFunc = func(_ context.Context, _, _ string) (*v1alpha2.VirtualMachine, error) { + return &v1alpha2.VirtualMachine{ + Status: v1alpha2.VirtualMachineStatus{ + Phase: v1alpha2.MachineRunning, + }, + }, nil + } + attachmentServiceMock.GetKVVMFunc = func(_ context.Context, _ *v1alpha2.VirtualMachine) (*virtv1.VirtualMachine, error) { + return nil, errors.New("test error") + } + + result, err := NewVirtualMachineReadyHandler(&attachmentServiceMock).Handle(context.Background(), vmbda) + Expect(result).To(Equal(reconcile.Result{})) + Expect(err).To(HaveOccurred()) + }) + + It("should return error if getting kvvmi fails", func() { + attachmentServiceMock.GetVirtualMachineFunc = func(_ context.Context, _, _ string) (*v1alpha2.VirtualMachine, error) { + return &v1alpha2.VirtualMachine{ + Status: v1alpha2.VirtualMachineStatus{ + Phase: v1alpha2.MachineRunning, + }, + }, nil + } + attachmentServiceMock.GetKVVMFunc = func(_ context.Context, _ *v1alpha2.VirtualMachine) (*virtv1.VirtualMachine, error) { + return &virtv1.VirtualMachine{}, nil + } + attachmentServiceMock.GetKVVMIFunc = func(_ context.Context, _ *v1alpha2.VirtualMachine) (*virtv1.VirtualMachineInstance, error) { + return nil, errors.New("test error") + } + + result, err := NewVirtualMachineReadyHandler(&attachmentServiceMock).Handle(context.Background(), vmbda) + Expect(result).To(Equal(reconcile.Result{})) + Expect(err).To(HaveOccurred()) + }) +})