* This file is part of the KubeVirt project
*
* 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.
*
* Copyright The KubeVirt Authors.
*
*/
package synchronization
import (
"context"
"crypto/tls"
"encoding/json"
"fmt"
"net"
"os"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
"go.uber.org/mock/gomock"
k8sv1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes/fake"
"k8s.io/client-go/tools/cache"
virtv1 "kubevirt.io/api/core/v1"
clientgoapi "kubevirt.io/client-go/api"
"kubevirt.io/client-go/kubecli"
kubevirtfake "kubevirt.io/client-go/kubevirt/fake"
"kubevirt.io/kubevirt/pkg/certificates"
kvcontroller "kubevirt.io/kubevirt/pkg/controller"
virtcontroller "kubevirt.io/kubevirt/pkg/controller"
"kubevirt.io/kubevirt/pkg/libvmi"
"kubevirt.io/kubevirt/pkg/pointer"
syncv1 "kubevirt.io/kubevirt/pkg/synchronizer-com/synchronization/v1"
"kubevirt.io/kubevirt/pkg/testutils"
)
const (
testMigrationID = "testMigrationID"
testMigrationUID = "testMigrationUID"
targetTestMigrationName = "targetMigrationName"
targetTestMigrationUID = "targetMigrationUID"
sourceTestMigrationName = "sourceMigrationName"
sourceTestMigrationUID = "sourceMigrationUID"
uniqueMigration = "uniqueMigrationID"
targetNamespace = "target-namespace"
sourcePodName = "sourcePod"
targetPodName = "targetPod"
remoteHostURL = "localhost:9186"
)
var _ = Describe("VMI status synchronization controller", func() {
var controller *SynchronizationController
var mockQueue *testutils.MockWorkQueue[string]
var virtClient *kubecli.MockKubevirtClient
var virtfakeClient *kubevirtfake.Clientset
var vmiInformer cache.SharedIndexInformer
var migrationInformer cache.SharedIndexInformer
var tlsConfig *tls.Config
addVMI := func(vmi *virtv1.VirtualMachineInstance) {
controller.vmiInformer.GetStore().Add(vmi)
key, err := virtcontroller.KeyFunc(vmi)
Expect(err).To(Not(HaveOccurred()))
controller.queue.Add(key)
}
BeforeEach(func() {
ctrl := gomock.NewController(GinkgoT())
k8sfakeClient := fake.NewSimpleClientset()
virtfakeClient = kubevirtfake.NewSimpleClientset()
virtClient = kubecli.NewMockKubevirtClient(ctrl)
virtClient.EXPECT().CoreV1().Return(k8sfakeClient.CoreV1()).AnyTimes()
virtClient.EXPECT().VirtualMachineInstance(metav1.NamespaceDefault).Return(virtfakeClient.KubevirtV1().VirtualMachineInstances(metav1.NamespaceDefault)).AnyTimes()
virtClient.EXPECT().VirtualMachineInstance(targetNamespace).Return(virtfakeClient.KubevirtV1().VirtualMachineInstances(targetNamespace)).AnyTimes()
virtClient.EXPECT().VirtualMachineInstanceMigration(k8sv1.NamespaceDefault).Return(virtfakeClient.KubevirtV1().VirtualMachineInstanceMigrations(k8sv1.NamespaceDefault)).AnyTimes()
virtClient.EXPECT().VirtualMachineInstanceMigration(targetNamespace).Return(virtfakeClient.KubevirtV1().VirtualMachineInstanceMigrations(targetNamespace)).AnyTimes()
vmiInformer, _ = testutils.NewFakeInformerWithIndexersFor(&virtv1.VirtualMachineInstance{}, kvcontroller.GetVMIInformerIndexers())
migrationInformer, _ = testutils.NewFakeInformerFor(&virtv1.VirtualMachineInstanceMigration{})
tmpDir, err := os.MkdirTemp("", "synchronizationcontrollertest")
Expect(err).ToNot(HaveOccurred())
store, err := certificates.GenerateSelfSignedCert(tmpDir, "test", "test")
Expect(err).ToNot(HaveOccurred())
tlsConfig = &tls.Config{
InsecureSkipVerify: true,
MinVersion: tls.VersionTLS12,
GetCertificate: func(info *tls.ClientHelloInfo) (certificate *tls.Certificate, e error) {
return store.Current()
},
}
controller, err = NewSynchronizationController(virtClient, vmiInformer, migrationInformer, tlsConfig, tlsConfig, "0.0.0.0", 9185, "")
Expect(err).ToNot(HaveOccurred())
mockQueue = testutils.NewMockWorkQueue(controller.queue)
controller.queue = mockQueue
})
Context("controller", func() {
It("Should not do anything if not migrating", func() {
vmi := clientgoapi.NewMinimalVMI("testvmi")
vmi.Status.Phase = virtv1.Running
addVMI(vmi)
controller.Execute()
})
})
Context("grpc SyncSourceMigrationStatus", func() {
var (
vmi *virtv1.VirtualMachineInstance
migration *virtv1.VirtualMachineInstanceMigration
)
BeforeEach(func() {
vmi = libvmi.New(libvmi.WithNamespace(k8sv1.NamespaceDefault))
err := controller.vmiInformer.GetStore().Add(vmi)
Expect(err).ToNot(HaveOccurred())
migration = createTargetMigration(testMigrationID, vmi.Name, k8sv1.NamespaceDefault)
err = controller.migrationInformer.GetStore().Add(migration)
Expect(err).ToNot(HaveOccurred())
vmi, err = controller.client.VirtualMachineInstance(vmi.Namespace).Create(context.Background(), vmi, metav1.CreateOptions{})
Expect(err).ToNot(HaveOccurred())
foundMig, err := controller.findTargetMigrationFromMigrationID(migration.Spec.Receive.MigrationID)
Expect(foundMig).To(Equal(migration))
})
It("should return proper error if no source migration state passed in", func() {
request := &syncv1.VMIStatusRequest{
MigrationID: testMigrationID,
}
resp, err := controller.SyncSourceMigrationStatus(context.TODO(), request)
Expect(err).To(HaveOccurred())
Expect(resp.Message).To(Equal("must pass source status"))
})
It("should return proper error if no source migration can be found", func() {
request := &syncv1.VMIStatusRequest{
MigrationID: testMigrationID,
VmiStatus: &syncv1.VMIStatus{
VmiStatusJson: []byte{1, 2, 3},
},
}
controller.migrationInformer.GetStore().Delete(migration)
resp, err := controller.SyncSourceMigrationStatus(context.TODO(), request)
Expect(err).To(HaveOccurred())
Expect(resp.Message).To(Equal(fmt.Sprintf(sourceUnableToLocateVMIMigrationIDErrorMsg, testMigrationID)))
})
It("should return proper error if no matching vmi for source migration can be found", func() {
request := &syncv1.VMIStatusRequest{
MigrationID: testMigrationID,
VmiStatus: &syncv1.VMIStatus{
VmiStatusJson: []byte{1, 2, 3},
},
}
controller.vmiInformer.GetStore().Delete(vmi)
resp, err := controller.SyncSourceMigrationStatus(context.TODO(), request)
Expect(err).To(HaveOccurred())
Expect(resp.Message).To(Equal(fmt.Sprintf(sourceUnableToLocateVMIMigrationIDErrorMsgVMI, testMigrationID, fmt.Sprintf("%s/%s", vmi.Namespace, vmi.Name))))
})
It("should return a proper error when the source request vmi status json is invalid", func() {
request := &syncv1.VMIStatusRequest{
MigrationID: testMigrationID,
VmiStatus: &syncv1.VMIStatus{
VmiStatusJson: []byte{1, 2, 3},
},
}
resp, err := controller.SyncSourceMigrationStatus(context.TODO(), request)
Expect(err).To(HaveOccurred())
Expect(err.Error()).To(ContainSubstring("invalid character"))
Expect(resp.Message).To(ContainSubstring(fmt.Sprintf("unable to unmarshal vmistatus for migrationID %s", request.MigrationID)))
})
DescribeTable("should update the VMI if the remote source migration state is different", func(vmiStatus, remoteVMIStatus *virtv1.VirtualMachineInstanceStatus, expectedMessage string, deleteVMI bool) {
if vmiStatus != nil {
vmi.Status = *vmiStatus
}
vmi, err := controller.client.VirtualMachineInstance(vmi.Namespace).Update(context.Background(), vmi, metav1.UpdateOptions{})
Expect(err).ToNot(HaveOccurred())
if deleteVMI {
controller.client.VirtualMachineInstance(vmi.Namespace).Delete(context.Background(), vmi.Name, metav1.DeleteOptions{})
}
vmiStatusJson, err := json.Marshal(remoteVMIStatus)
Expect(err).ToNot(HaveOccurred())
request := &syncv1.VMIStatusRequest{
MigrationID: testMigrationID,
VmiStatus: &syncv1.VMIStatus{
VmiStatusJson: vmiStatusJson,
},
}
_, err = controller.SyncSourceMigrationStatus(context.TODO(), request)
if expectedMessage != "" {
Expect(err).To(HaveOccurred())
Expect(err.Error()).To(ContainSubstring(expectedMessage))
} else {
Expect(err).ToNot(HaveOccurred())
}
},
Entry("no vmi status and no source status", nil, nil, "must pass source status", false),
Entry("no vmi status and source status", nil, &virtv1.VirtualMachineInstanceStatus{
MigrationState: &virtv1.VirtualMachineInstanceMigrationState{
TargetState: &virtv1.VirtualMachineInstanceMigrationTargetState{
VirtualMachineInstanceCommonMigrationState: virtv1.VirtualMachineInstanceCommonMigrationState{
Node: "node1",
},
},
SourceState: &virtv1.VirtualMachineInstanceMigrationSourceState{
VirtualMachineInstanceCommonMigrationState: virtv1.VirtualMachineInstanceCommonMigrationState{
Node: "node2",
},
},
},
}, "", false),
Entry("no vmi status and source status, but vmi not found", nil, &virtv1.VirtualMachineInstanceStatus{
MigrationState: &virtv1.VirtualMachineInstanceMigrationState{
TargetState: &virtv1.VirtualMachineInstanceMigrationTargetState{
VirtualMachineInstanceCommonMigrationState: virtv1.VirtualMachineInstanceCommonMigrationState{
Node: "node1",
},
},
SourceState: &virtv1.VirtualMachineInstanceMigrationSourceState{
VirtualMachineInstanceCommonMigrationState: virtv1.VirtualMachineInstanceCommonMigrationState{
Node: "node2",
},
},
},
}, "not found", true),
Entry("sources are different", &virtv1.VirtualMachineInstanceStatus{
MigrationState: &virtv1.VirtualMachineInstanceMigrationState{
TargetState: &virtv1.VirtualMachineInstanceMigrationTargetState{
VirtualMachineInstanceCommonMigrationState: virtv1.VirtualMachineInstanceCommonMigrationState{
Node: "node1",
},
},
SourceState: &virtv1.VirtualMachineInstanceMigrationSourceState{
VirtualMachineInstanceCommonMigrationState: virtv1.VirtualMachineInstanceCommonMigrationState{
Node: "node3",
},
},
},
}, &virtv1.VirtualMachineInstanceStatus{
MigrationState: &virtv1.VirtualMachineInstanceMigrationState{
TargetState: &virtv1.VirtualMachineInstanceMigrationTargetState{
VirtualMachineInstanceCommonMigrationState: virtv1.VirtualMachineInstanceCommonMigrationState{
Node: "node1",
},
},
SourceState: &virtv1.VirtualMachineInstanceMigrationSourceState{
VirtualMachineInstanceCommonMigrationState: virtv1.VirtualMachineInstanceCommonMigrationState{
Node: "node2",
},
},
},
}, "", false),
)
})
Context("grpc SyncTargetMigrationStatus", func() {
var (
vmi *virtv1.VirtualMachineInstance
migration *virtv1.VirtualMachineInstanceMigration
)
BeforeEach(func() {
vmi = libvmi.New(libvmi.WithNamespace(k8sv1.NamespaceDefault))
err := controller.vmiInformer.GetStore().Add(vmi)
Expect(err).ToNot(HaveOccurred())
migration = createSourceMigration(testMigrationID, vmi.Name, "", k8sv1.NamespaceDefault)
err = controller.migrationInformer.GetStore().Add(migration)
Expect(err).ToNot(HaveOccurred())
vmi, err = controller.client.VirtualMachineInstance(vmi.Namespace).Create(context.Background(), vmi, metav1.CreateOptions{})
Expect(err).ToNot(HaveOccurred())
foundMig, err := controller.findSourceMigrationFromMigrationID(migration.Spec.SendTo.MigrationID)
Expect(foundMig).To(Equal(migration))
})
It("should return proper error if no target migration state passed in", func() {
request := &syncv1.VMIStatusRequest{
MigrationID: testMigrationID,
}
resp, err := controller.SyncTargetMigrationStatus(context.TODO(), request)
Expect(err).To(HaveOccurred())
Expect(resp.Message).To(Equal("must pass target status"))
})
It("should return proper error if no target migration can be found", func() {
request := &syncv1.VMIStatusRequest{
MigrationID: testMigrationID,
VmiStatus: &syncv1.VMIStatus{
VmiStatusJson: []byte{1, 2, 3},
},
}
controller.migrationInformer.GetStore().Delete(migration)
resp, err := controller.SyncTargetMigrationStatus(context.TODO(), request)
Expect(err).To(HaveOccurred())
Expect(resp.Message).To(Equal(fmt.Sprintf(targetUnableToLocateVMIMigrationIDErrorMsg, testMigrationID)))
})
It("should return proper error if no matching vmi for target migration can be found", func() {
request := &syncv1.VMIStatusRequest{
MigrationID: testMigrationID,
VmiStatus: &syncv1.VMIStatus{
VmiStatusJson: []byte{1, 2, 3},
},
}
controller.vmiInformer.GetStore().Delete(vmi)
resp, err := controller.SyncTargetMigrationStatus(context.TODO(), request)
Expect(err).To(HaveOccurred())
Expect(resp.Message).To(Equal(fmt.Sprintf(targetUnableToLocateVMIMigrationIDErrorMsgVMI, testMigrationID, fmt.Sprintf("%s/%s", vmi.Namespace, vmi.Name))))
})
It("should return a proper error when the target request vmi status json is invalid", func() {
request := &syncv1.VMIStatusRequest{
MigrationID: testMigrationID,
VmiStatus: &syncv1.VMIStatus{
VmiStatusJson: []byte{1, 2, 3},
},
}
resp, err := controller.SyncTargetMigrationStatus(context.TODO(), request)
Expect(err).To(HaveOccurred())
Expect(err.Error()).To(ContainSubstring("invalid character"))
Expect(resp.Message).To(ContainSubstring(fmt.Sprintf("unable to unmarshal vmistatus for migrationID %s", request.MigrationID)))
})
DescribeTable("should update the VMI if the remote target migration state is different", func(vmiStatus, remoteVMIStatus *virtv1.VirtualMachineInstanceStatus, expectedMessage string, deleteVMI bool) {
if vmiStatus != nil {
vmi.Status = *vmiStatus
}
vmi, err := controller.client.VirtualMachineInstance(vmi.Namespace).Update(context.Background(), vmi, metav1.UpdateOptions{})
Expect(err).ToNot(HaveOccurred())
if deleteVMI {
controller.client.VirtualMachineInstance(vmi.Namespace).Delete(context.Background(), vmi.Name, metav1.DeleteOptions{})
}
vmiStatusJson, err := json.Marshal(remoteVMIStatus)
Expect(err).ToNot(HaveOccurred())
request := &syncv1.VMIStatusRequest{
MigrationID: testMigrationID,
VmiStatus: &syncv1.VMIStatus{
VmiStatusJson: vmiStatusJson,
},
}
_, err = controller.SyncTargetMigrationStatus(context.TODO(), request)
if expectedMessage != "" {
Expect(err).To(HaveOccurred())
Expect(err.Error()).To(ContainSubstring(expectedMessage))
} else {
Expect(err).ToNot(HaveOccurred())
}
},
Entry("no vmi status and no remote target status", nil, nil, "must pass target status", false),
Entry("no vmi status and remote target status", nil, &virtv1.VirtualMachineInstanceStatus{
MigrationState: &virtv1.VirtualMachineInstanceMigrationState{
TargetState: &virtv1.VirtualMachineInstanceMigrationTargetState{
VirtualMachineInstanceCommonMigrationState: virtv1.VirtualMachineInstanceCommonMigrationState{
Node: "node1",
},
},
SourceState: &virtv1.VirtualMachineInstanceMigrationSourceState{
VirtualMachineInstanceCommonMigrationState: virtv1.VirtualMachineInstanceCommonMigrationState{
Node: "node2",
},
},
},
}, "", false),
Entry("no vmi status and target status, but vmi not found, patch will fail", nil, &virtv1.VirtualMachineInstanceStatus{
MigrationState: &virtv1.VirtualMachineInstanceMigrationState{
TargetState: &virtv1.VirtualMachineInstanceMigrationTargetState{
VirtualMachineInstanceCommonMigrationState: virtv1.VirtualMachineInstanceCommonMigrationState{
Node: "node1",
},
},
SourceState: &virtv1.VirtualMachineInstanceMigrationSourceState{
VirtualMachineInstanceCommonMigrationState: virtv1.VirtualMachineInstanceCommonMigrationState{
Node: "node2",
},
},
},
}, "not found", true),
Entry("target are different", &virtv1.VirtualMachineInstanceStatus{
MigrationState: &virtv1.VirtualMachineInstanceMigrationState{
TargetState: &virtv1.VirtualMachineInstanceMigrationTargetState{
VirtualMachineInstanceCommonMigrationState: virtv1.VirtualMachineInstanceCommonMigrationState{
Node: "node3",
},
},
SourceState: &virtv1.VirtualMachineInstanceMigrationSourceState{
VirtualMachineInstanceCommonMigrationState: virtv1.VirtualMachineInstanceCommonMigrationState{
Node: "node1",
},
},
},
}, &virtv1.VirtualMachineInstanceStatus{
MigrationState: &virtv1.VirtualMachineInstanceMigrationState{
TargetState: &virtv1.VirtualMachineInstanceMigrationTargetState{
VirtualMachineInstanceCommonMigrationState: virtv1.VirtualMachineInstanceCommonMigrationState{
Node: "node2",
},
},
SourceState: &virtv1.VirtualMachineInstanceMigrationSourceState{
VirtualMachineInstanceCommonMigrationState: virtv1.VirtualMachineInstanceCommonMigrationState{
Node: "node1",
},
},
},
}, "", false),
)
})
verifySource := func(controller *SynchronizationController, vmi *virtv1.VirtualMachineInstance, url string) {
updatedSourceVMI, err := controller.client.VirtualMachineInstance(vmi.Namespace).Get(context.Background(), vmi.Name, metav1.GetOptions{})
Expect(err).ToNot(HaveOccurred())
Expect(updatedSourceVMI).ToNot(BeNil())
Expect(updatedSourceVMI.Status.MigrationState).ToNot(BeNil())
Expect(updatedSourceVMI.Status.MigrationState.TargetState).ToNot(BeNil())
Expect(updatedSourceVMI.Status.MigrationState.TargetState.MigrationUID).To(BeEquivalentTo(targetTestMigrationUID))
}
verifyTarget := func(controller *SynchronizationController, vmi *virtv1.VirtualMachineInstance, url string) {
updatedTargetVMI, err := controller.client.VirtualMachineInstance(vmi.Namespace).Get(context.Background(), vmi.Name, metav1.GetOptions{})
Expect(err).ToNot(HaveOccurred())
Expect(updatedTargetVMI).ToNot(BeNil())
Expect(updatedTargetVMI.Status.MigrationState).ToNot(BeNil())
Expect(updatedTargetVMI.Status.MigrationState.SourceState).ToNot(BeNil())
Expect(updatedTargetVMI.Status.MigrationState.SourceState.MigrationUID).To(BeEquivalentTo(sourceTestMigrationUID))
}
Context("target controller is same as source", func() {
var (
err error
targetVMI, sourceVMI *virtv1.VirtualMachineInstance
localTCPConn net.Listener
done chan struct{}
sourceMigration, targetMigration *virtv1.VirtualMachineInstanceMigration
)
BeforeEach(func() {
localTCPConn, err = controller.createTcpListener()
Expect(err).ToNot(HaveOccurred())
done = make(chan struct{})
go func() {
defer close(done)
controller.grpcServer.Serve(localTCPConn)
}()
targetVMI = libvmi.New(libvmi.WithNamespace(targetNamespace))
targetVMI.Status.MigrationState = &virtv1.VirtualMachineInstanceMigrationState{}
err = vmiInformer.GetStore().Add(targetVMI)
Expect(err).ToNot(HaveOccurred())
_, err = virtClient.VirtualMachineInstance(targetVMI.Namespace).Create(context.TODO(), targetVMI, metav1.CreateOptions{})
Expect(err).ToNot(HaveOccurred())
targetMigration = createTargetMigration(testMigrationID, targetVMI.Name, targetVMI.Namespace)
err = migrationInformer.GetStore().Add(targetMigration)
Expect(err).ToNot(HaveOccurred())
_, err = virtfakeClient.KubevirtV1().VirtualMachineInstanceMigrations(targetVMI.Namespace).Create(context.TODO(), targetMigration, metav1.CreateOptions{})
Expect(err).ToNot(HaveOccurred())
sourceVMI = libvmi.New(libvmi.WithNamespace(k8sv1.NamespaceDefault))
sourceVMI.Status.MigrationState = &virtv1.VirtualMachineInstanceMigrationState{}
err = vmiInformer.GetStore().Add(sourceVMI)
Expect(err).ToNot(HaveOccurred())
_, err = virtClient.VirtualMachineInstance(sourceVMI.Namespace).Create(context.TODO(), sourceVMI, metav1.CreateOptions{})
Expect(err).ToNot(HaveOccurred())
remoteURL, err := controller.getLocalSynchronizationAddress()
sourceMigration = createSourceMigration(testMigrationID, sourceVMI.Name, remoteURL, sourceVMI.Namespace)
err = migrationInformer.GetStore().Add(sourceMigration)
Expect(err).ToNot(HaveOccurred())
_, err = virtfakeClient.KubevirtV1().VirtualMachineInstanceMigrations(k8sv1.NamespaceDefault).Create(context.TODO(), sourceMigration, metav1.CreateOptions{})
Expect(err).ToNot(HaveOccurred())
})
AfterEach(func() {
controller.closeConnections()
if done != nil {
<-done
}
})
Context("handleSourceState", func() {
It("should not do anything if source doesn't have source migration", func() {
err := controller.handleSourceState(sourceVMI, sourceMigration)
Expect(err).ToNot(HaveOccurred())
sourceVMI.Status.MigrationState = nil
err = controller.handleSourceState(sourceVMI, sourceMigration)
Expect(err).ToNot(HaveOccurred())
})
It("should not do anything if source sync addres is not set", func() {
sourceVMI.Status.MigrationState.SourceState = &virtv1.VirtualMachineInstanceMigrationSourceState{}
err := controller.handleSourceState(sourceVMI, sourceMigration)
Expect(err).ToNot(HaveOccurred())
})
It("should error and fail to update target VMI if target VMI doesn't exist", func() {
sourceVMI.Status.MigrationState.SourceState = &virtv1.VirtualMachineInstanceMigrationSourceState{
VirtualMachineInstanceCommonMigrationState: virtv1.VirtualMachineInstanceCommonMigrationState{
SyncAddress: pointer.P(localTCPConn.Addr().String()),
MigrationUID: sourceTestMigrationUID,
},
}
sourceVMI.Status.MigrationState.TargetState = &virtv1.VirtualMachineInstanceMigrationTargetState{
VirtualMachineInstanceCommonMigrationState: virtv1.VirtualMachineInstanceCommonMigrationState{
SyncAddress: pointer.P(localTCPConn.Addr().String()),
MigrationUID: targetTestMigrationUID,
},
}
err = controller.vmiInformer.GetStore().Delete(targetVMI)
Expect(err).ToNot(HaveOccurred())
err = controller.handleSourceState(sourceVMI, sourceMigration)
Expect(err).To(HaveOccurred())
Expect(err.Error()).To(ContainSubstring(fmt.Sprintf(sourceUnableToLocateVMIMigrationIDErrorMsg, testMigrationID)))
})
It("should update target VMI source migration from source VMI", func() {
sourceVMI.Status.MigrationState.SourceState = &virtv1.VirtualMachineInstanceMigrationSourceState{
VirtualMachineInstanceCommonMigrationState: virtv1.VirtualMachineInstanceCommonMigrationState{
SyncAddress: pointer.P(localTCPConn.Addr().String()),
MigrationUID: sourceTestMigrationUID,
},
}
sourceVMI.Status.MigrationState.TargetState = &virtv1.VirtualMachineInstanceMigrationTargetState{
VirtualMachineInstanceCommonMigrationState: virtv1.VirtualMachineInstanceCommonMigrationState{
SyncAddress: pointer.P(localTCPConn.Addr().String()),
MigrationUID: targetTestMigrationUID,
},
}
err := controller.handleSourceState(sourceVMI, sourceMigration)
Expect(err).ToNot(HaveOccurred())
verifyTarget(controller, targetVMI, localTCPConn.Addr().String())
})
})
Context("handleTargetState", func() {
It("should not do anything if target doesn't have target migration", func() {
err := controller.handleTargetState(targetVMI, targetMigration)
Expect(err).ToNot(HaveOccurred())
targetVMI.Status.MigrationState = nil
err = controller.handleTargetState(targetVMI, targetMigration)
Expect(err).ToNot(HaveOccurred())
})
It("should not do anything if target sync addres is not set", func() {
targetVMI.Status.MigrationState.TargetState = &virtv1.VirtualMachineInstanceMigrationTargetState{}
err := controller.handleTargetState(targetVMI, targetMigration)
Expect(err).ToNot(HaveOccurred())
})
It("should error and fail to update target VMI if source VMI doesn't exist", func() {
targetVMI.Status.MigrationState.TargetState = &virtv1.VirtualMachineInstanceMigrationTargetState{
VirtualMachineInstanceCommonMigrationState: virtv1.VirtualMachineInstanceCommonMigrationState{
SyncAddress: pointer.P(localTCPConn.Addr().String()),
MigrationUID: targetTestMigrationUID,
},
}
targetVMI.Status.MigrationState.SourceState = &virtv1.VirtualMachineInstanceMigrationSourceState{
VirtualMachineInstanceCommonMigrationState: virtv1.VirtualMachineInstanceCommonMigrationState{
SyncAddress: pointer.P(localTCPConn.Addr().String()),
MigrationUID: sourceTestMigrationUID,
},
}
err = controller.vmiInformer.GetStore().Delete(sourceVMI)
Expect(err).ToNot(HaveOccurred())
err = controller.handleTargetState(targetVMI, targetMigration)
Expect(err).To(HaveOccurred())
Expect(err.Error()).To(ContainSubstring(fmt.Sprintf(targetUnableToLocateVMIMigrationIDErrorMsg, testMigrationID)))
})
It("should update source VMI target migration from target VMI", func() {
targetVMI.Status.MigrationState.TargetState = &virtv1.VirtualMachineInstanceMigrationTargetState{
VirtualMachineInstanceCommonMigrationState: virtv1.VirtualMachineInstanceCommonMigrationState{
SyncAddress: pointer.P(localTCPConn.Addr().String()),
MigrationUID: targetTestMigrationUID,
},
}
targetVMI.Status.MigrationState.SourceState = &virtv1.VirtualMachineInstanceMigrationSourceState{
VirtualMachineInstanceCommonMigrationState: virtv1.VirtualMachineInstanceCommonMigrationState{
SyncAddress: pointer.P(localTCPConn.Addr().String()),
MigrationUID: sourceTestMigrationUID,
},
}
err := controller.handleTargetState(targetVMI, targetMigration)
Expect(err).ToNot(HaveOccurred())
verifySource(controller, sourceVMI, localTCPConn.Addr().String())
})
})
It("should properly update both source and target if both handlers are called", func() {
targetVMI.Status.MigrationState.TargetState = &virtv1.VirtualMachineInstanceMigrationTargetState{
VirtualMachineInstanceCommonMigrationState: virtv1.VirtualMachineInstanceCommonMigrationState{
MigrationUID: targetTestMigrationUID,
},
}
targetVMI.Status.MigrationState.SourceState = &virtv1.VirtualMachineInstanceMigrationSourceState{
VirtualMachineInstanceCommonMigrationState: virtv1.VirtualMachineInstanceCommonMigrationState{
SyncAddress: pointer.P(localTCPConn.Addr().String()),
},
}
sourceVMI.Status.MigrationState.SourceState = &virtv1.VirtualMachineInstanceMigrationSourceState{
VirtualMachineInstanceCommonMigrationState: virtv1.VirtualMachineInstanceCommonMigrationState{
MigrationUID: sourceTestMigrationUID,
},
}
sourceVMI.Status.MigrationState.TargetState = &virtv1.VirtualMachineInstanceMigrationTargetState{
VirtualMachineInstanceCommonMigrationState: virtv1.VirtualMachineInstanceCommonMigrationState{
SyncAddress: pointer.P(localTCPConn.Addr().String()),
},
}
err = controller.vmiInformer.GetStore().Update(targetVMI)
Expect(err).ToNot(HaveOccurred())
_, err = controller.client.VirtualMachineInstance(targetVMI.Namespace).Update(context.Background(), targetVMI, metav1.UpdateOptions{})
Expect(err).ToNot(HaveOccurred())
_, err = controller.client.VirtualMachineInstance(sourceVMI.Namespace).Update(context.Background(), sourceVMI, metav1.UpdateOptions{})
Expect(err).ToNot(HaveOccurred())
err = controller.vmiInformer.GetStore().Update(sourceVMI)
Expect(err).ToNot(HaveOccurred())
sourceKey, err := kvcontroller.KeyFunc(sourceVMI)
Expect(err).ToNot(HaveOccurred())
err = controller.execute(sourceKey)
Expect(err).ToNot(HaveOccurred())
targetKey, err := kvcontroller.KeyFunc(targetVMI)
Expect(err).ToNot(HaveOccurred())
err = controller.execute(targetKey)
Expect(err).ToNot(HaveOccurred())
verifyTarget(controller, targetVMI, localTCPConn.Addr().String())
verifySource(controller, sourceVMI, localTCPConn.Addr().String())
})
})
Context("target controller different than source", func() {
var (
err error
remoteController *SynchronizationController
targetVMI, sourceVMI *virtv1.VirtualMachineInstance
targetMigration, sourceMigration *virtv1.VirtualMachineInstanceMigration
remoteURL, localURL string
controllerDone, remoteControllerDone chan struct{}
)
BeforeEach(func() {
remoteMigrationInformer, _ := testutils.NewFakeInformerFor(&virtv1.VirtualMachineInstanceMigration{})
remoteController, err = NewSynchronizationController(virtClient, vmiInformer, remoteMigrationInformer, tlsConfig, tlsConfig, "0.0.0.0", 9186, "")
Expect(err).ToNot(HaveOccurred())
remoteTCPConn, err := remoteController.createTcpListener()
Expect(err).ToNot(HaveOccurred())
remoteControllerDone = make(chan struct{})
go func() {
defer close(remoteControllerDone)
remoteController.grpcServer.Serve(remoteTCPConn)
}()
localTCPConn, err := controller.createTcpListener()
Expect(err).ToNot(HaveOccurred())
controllerDone = make(chan struct{})
go func() {
defer close(controllerDone)
controller.grpcServer.Serve(localTCPConn)
}()
targetVMI = libvmi.New(libvmi.WithNamespace(targetNamespace))
targetVMI.Status.MigrationState = &virtv1.VirtualMachineInstanceMigrationState{}
err = vmiInformer.GetStore().Add(targetVMI)
Expect(err).ToNot(HaveOccurred())
_, err = virtClient.VirtualMachineInstance(targetVMI.Namespace).Create(context.TODO(), targetVMI, metav1.CreateOptions{})
Expect(err).ToNot(HaveOccurred())
targetMigration = createTargetMigration(testMigrationID, targetVMI.Name, targetVMI.Namespace)
err = remoteMigrationInformer.GetStore().Add(targetMigration)
Expect(err).ToNot(HaveOccurred())
_, err = virtfakeClient.KubevirtV1().VirtualMachineInstanceMigrations(targetVMI.Namespace).Create(context.TODO(), targetMigration, metav1.CreateOptions{})
Expect(err).ToNot(HaveOccurred())
sourceVMI = libvmi.New(libvmi.WithNamespace(k8sv1.NamespaceDefault))
sourceVMI.Status.MigrationState = &virtv1.VirtualMachineInstanceMigrationState{}
err = vmiInformer.GetStore().Add(sourceVMI)
Expect(err).ToNot(HaveOccurred())
sourceVMI, err = virtClient.VirtualMachineInstance(sourceVMI.Namespace).Create(context.TODO(), sourceVMI, metav1.CreateOptions{})
Expect(err).ToNot(HaveOccurred())
remoteURL, err = remoteController.getLocalSynchronizationAddress()
sourceMigration = createSourceMigration(testMigrationID, sourceVMI.Name, remoteURL, sourceVMI.Namespace)
err = migrationInformer.GetStore().Add(sourceMigration)
Expect(err).ToNot(HaveOccurred())
_, err = virtfakeClient.KubevirtV1().VirtualMachineInstanceMigrations(sourceVMI.Namespace).Create(context.TODO(), sourceMigration, metav1.CreateOptions{})
Expect(err).ToNot(HaveOccurred())
localURL, err = controller.getLocalSynchronizationAddress()
Expect(err).ToNot(HaveOccurred())
})
AfterEach(func() {
remoteController.closeConnections()
controller.closeConnections()
if remoteControllerDone != nil {
<-remoteControllerDone
}
if controllerDone != nil {
<-controllerDone
}
})
It("should properly update both source and target if both handlers are called", func() {
targetVMI.Status.MigrationState.TargetState = &virtv1.VirtualMachineInstanceMigrationTargetState{
VirtualMachineInstanceCommonMigrationState: virtv1.VirtualMachineInstanceCommonMigrationState{
MigrationUID: targetTestMigrationUID,
},
}
targetVMI.Status.MigrationState.SourceState = &virtv1.VirtualMachineInstanceMigrationSourceState{
VirtualMachineInstanceCommonMigrationState: virtv1.VirtualMachineInstanceCommonMigrationState{
SyncAddress: pointer.P(localURL),
},
}
sourceVMI.Status.MigrationState.SourceState = &virtv1.VirtualMachineInstanceMigrationSourceState{
VirtualMachineInstanceCommonMigrationState: virtv1.VirtualMachineInstanceCommonMigrationState{
MigrationUID: sourceTestMigrationUID,
},
}
sourceVMI.Status.MigrationState.TargetState = &virtv1.VirtualMachineInstanceMigrationTargetState{
VirtualMachineInstanceCommonMigrationState: virtv1.VirtualMachineInstanceCommonMigrationState{
SyncAddress: pointer.P(remoteURL),
},
}
By("setting up the clients and informers")
err = remoteController.vmiInformer.GetStore().Update(targetVMI)
Expect(err).ToNot(HaveOccurred())
_, err = remoteController.client.VirtualMachineInstance(targetVMI.Namespace).Update(context.Background(), targetVMI, metav1.UpdateOptions{})
Expect(err).ToNot(HaveOccurred())
err = controller.vmiInformer.GetStore().Update(sourceVMI)
Expect(err).ToNot(HaveOccurred())
_, err = controller.client.VirtualMachineInstance(sourceVMI.Namespace).Update(context.Background(), sourceVMI, metav1.UpdateOptions{})
Expect(err).ToNot(HaveOccurred())
By("calling execute on the source")
sourceKey, err := kvcontroller.KeyFunc(sourceVMI)
Expect(err).ToNot(HaveOccurred())
err = controller.execute(sourceKey)
Expect(err).ToNot(HaveOccurred())
By("calling execute on the target")
targetKey, err := kvcontroller.KeyFunc(targetVMI)
Expect(err).ToNot(HaveOccurred())
err = remoteController.execute(targetKey)
Expect(err).ToNot(HaveOccurred())
By("verifying the result")
verifySource(controller, sourceVMI, remoteURL)
verifyTarget(remoteController, targetVMI, localURL)
By("updating the source state with a new sourcePodName")
sourceVMI, err = controller.client.VirtualMachineInstance(sourceVMI.Namespace).Get(context.Background(), sourceVMI.Name, metav1.GetOptions{})
Expect(err).ToNot(HaveOccurred())
sourceVMI.Status.MigrationState.SourceState.Pod = sourcePodName
err = controller.vmiInformer.GetStore().Update(sourceVMI)
Expect(err).ToNot(HaveOccurred())
_, err = controller.client.VirtualMachineInstance(sourceVMI.Namespace).Update(context.Background(), sourceVMI, metav1.UpdateOptions{})
Expect(err).ToNot(HaveOccurred())
By("updating the remote informer with the remote client, we can properly patch")
updatedTargetVMI, err := remoteController.client.VirtualMachineInstance(targetVMI.Namespace).Get(context.Background(), targetVMI.Name, metav1.GetOptions{})
Expect(err).ToNot(HaveOccurred())
remoteController.vmiInformer.GetStore().Update(updatedTargetVMI)
By("calling execute on the source")
err = controller.execute(sourceKey)
Expect(err).ToNot(HaveOccurred())
By("verifying the result")
verifyTarget(controller, targetVMI, localURL)
updatedTargetVMI, err = remoteController.client.VirtualMachineInstance(targetVMI.Namespace).Get(context.Background(), targetVMI.Name, metav1.GetOptions{})
Expect(err).ToNot(HaveOccurred())
remoteController.vmiInformer.GetStore().Update(updatedTargetVMI)
Expect(err).ToNot(HaveOccurred())
Expect(updatedTargetVMI).ToNot(BeNil())
Expect(updatedTargetVMI.Status.MigrationState).ToNot(BeNil())
Expect(updatedTargetVMI.Status.MigrationState.SourceState).ToNot(BeNil())
Expect(updatedTargetVMI.Status.MigrationState.SourceState.MigrationUID).To(BeEquivalentTo(sourceTestMigrationUID))
Expect(updatedTargetVMI.Status.MigrationState.SourceState.SyncAddress).ToNot(BeNil())
Expect(*updatedTargetVMI.Status.MigrationState.SourceState.SyncAddress).To(Equal(localURL))
Expect(updatedTargetVMI.Status.MigrationState.SourceState.Pod).To(Equal(sourcePodName))
By("updating the target state")
targetVMI.Status.MigrationState.TargetState.Pod = targetPodName
err = remoteController.vmiInformer.GetStore().Update(targetVMI)
Expect(err).ToNot(HaveOccurred())
By("calling execute on the target")
err = remoteController.execute(targetKey)
Expect(err).ToNot(HaveOccurred())
updatedSourceVMI, err := controller.client.VirtualMachineInstance(sourceVMI.Namespace).Get(context.Background(), sourceVMI.Name, metav1.GetOptions{})
Expect(err).ToNot(HaveOccurred())
Expect(updatedSourceVMI).ToNot(BeNil())
Expect(updatedSourceVMI.Status.MigrationState).ToNot(BeNil())
Expect(updatedSourceVMI.Status.MigrationState.TargetState).ToNot(BeNil())
Expect(updatedSourceVMI.Status.MigrationState.TargetState.MigrationUID).To(BeEquivalentTo(targetTestMigrationUID))
Expect(updatedSourceVMI.Status.MigrationState.TargetState.SyncAddress).ToNot(BeNil())
Expect(*updatedSourceVMI.Status.MigrationState.TargetState.SyncAddress).To(Equal(remoteURL))
Expect(updatedSourceVMI.Status.MigrationState.TargetState.Pod).To(Equal(targetPodName))
})
It("should not rebuild anything without migrations", func() {
objs := remoteController.migrationInformer.GetStore().List()
for _, obj := range objs {
remoteController.migrationInformer.GetStore().Delete(obj)
}
err := remoteController.rebuildConnectionsAndUpdateSyncAddress()
Expect(err).ToNot(HaveOccurred())
remoteController.syncOutboundConnectionMap.Range(func(key, value any) bool {
Fail(fmt.Sprintf("found object in outbound map %v, %v", key, value))
return true
})
})
It("should not rebuild anything if migrations are not ongoing", func() {
objs := remoteController.migrationInformer.GetStore().List()
for _, obj := range objs {
migration, ok := obj.(*virtv1.VirtualMachineInstanceMigration)
Expect(ok).To(BeTrue())
migration.Status.Phase = virtv1.MigrationFailed
remoteController.migrationInformer.GetStore().Update(migration)
}
err := remoteController.rebuildConnectionsAndUpdateSyncAddress()
Expect(err).ToNot(HaveOccurred())
remoteController.syncOutboundConnectionMap.Range(func(key, value any) bool {
Fail(fmt.Sprintf("found object in outbound map %v, %v", key, value))
return true
})
})
It("should not rebuild anything if migration references missing VMI", func() {
objs := remoteController.vmiInformer.GetStore().List()
for _, obj := range objs {
remoteController.vmiInformer.GetStore().Delete(obj)
}
err := remoteController.rebuildConnectionsAndUpdateSyncAddress()
Expect(err).ToNot(HaveOccurred())
remoteController.syncOutboundConnectionMap.Range(func(key, value any) bool {
Fail(fmt.Sprintf("found object in outbound map %v, %v", key, value))
return true
})
})
It("should not rebuild anything if VMI doesn't have sync address", func() {
err := remoteController.rebuildConnectionsAndUpdateSyncAddress()
Expect(err).ToNot(HaveOccurred())
remoteController.syncOutboundConnectionMap.Range(func(key, value any) bool {
Fail(fmt.Sprintf("found object in outbound map %v, %v", key, value))
return true
})
err = controller.rebuildConnectionsAndUpdateSyncAddress()
Expect(err).ToNot(HaveOccurred())
controller.syncReceivingConnectionMap.Range(func(key, value any) bool {
Fail(fmt.Sprintf("found object in outbound map %v, %v", key, value))
return true
})
})
It("should properly rebuild target", func() {
remoteURL, err := remoteController.getLocalSynchronizationAddress()
Expect(err).ToNot(HaveOccurred())
targetVMI.Status.MigrationState.SourceState = &virtv1.VirtualMachineInstanceMigrationSourceState{
VirtualMachineInstanceCommonMigrationState: virtv1.VirtualMachineInstanceCommonMigrationState{
SyncAddress: pointer.P(remoteURL),
},
}
targetVMI.Status.MigrationState.TargetState = &virtv1.VirtualMachineInstanceMigrationTargetState{
VirtualMachineInstanceCommonMigrationState: virtv1.VirtualMachineInstanceCommonMigrationState{
MigrationUID: targetTestMigrationUID,
},
}
_, err = remoteController.client.VirtualMachineInstance(targetVMI.Namespace).Update(context.Background(), targetVMI, metav1.UpdateOptions{})
Expect(err).ToNot(HaveOccurred())
err = remoteController.vmiInformer.GetStore().Update(targetVMI)
Expect(err).ToNot(HaveOccurred())
err = remoteController.rebuildConnectionsAndUpdateSyncAddress()
Expect(err).ToNot(HaveOccurred())
count := 0
remoteController.syncReceivingConnectionMap.Range(func(key, value any) bool {
count++
Expect(key).To(Equal(testMigrationID))
conn, ok := value.(*SynchronizationConnection)
Expect(ok).To(BeTrue())
Expect(conn.migrationID).To(Equal(testMigrationID))
Expect(conn.grpcClientConnection.CanonicalTarget()).To(ContainSubstring(remoteURL))
return true
})
Expect(count).To(Equal(1), "there should be one item in the map")
})
It("should properly rebuild source", func() {
remoteURL, err := controller.getLocalSynchronizationAddress()
Expect(err).ToNot(HaveOccurred())
sourceVMI.Status.MigrationState.TargetState = &virtv1.VirtualMachineInstanceMigrationTargetState{
VirtualMachineInstanceCommonMigrationState: virtv1.VirtualMachineInstanceCommonMigrationState{
SyncAddress: pointer.P(remoteURL),
},
}
sourceVMI.Status.MigrationState.SourceState = &virtv1.VirtualMachineInstanceMigrationSourceState{
VirtualMachineInstanceCommonMigrationState: virtv1.VirtualMachineInstanceCommonMigrationState{
MigrationUID: sourceTestMigrationUID,
},
}
_, err = controller.client.VirtualMachineInstance(sourceVMI.Namespace).Update(context.Background(), sourceVMI, metav1.UpdateOptions{})
Expect(err).ToNot(HaveOccurred())
err = controller.vmiInformer.GetStore().Update(sourceVMI)
Expect(err).ToNot(HaveOccurred())
err = controller.rebuildConnectionsAndUpdateSyncAddress()
Expect(err).ToNot(HaveOccurred())
count := 0
controller.syncOutboundConnectionMap.Range(func(key, value any) bool {
count++
Expect(key).To(Equal(testMigrationID))
conn, ok := value.(*SynchronizationConnection)
Expect(ok).To(BeTrue())
Expect(conn.migrationID).To(Equal(testMigrationID))
Expect(conn.grpcClientConnection.CanonicalTarget()).To(ContainSubstring(remoteURL))
return true
})
Expect(count).To(Equal(1), "there should be one item in the map")
})
It("should cancel remote migration", func() {
targetVMI.Status.MigrationState.TargetState = &virtv1.VirtualMachineInstanceMigrationTargetState{
VirtualMachineInstanceCommonMigrationState: virtv1.VirtualMachineInstanceCommonMigrationState{
MigrationUID: targetTestMigrationUID,
},
}
targetVMI.Status.MigrationState.SourceState = &virtv1.VirtualMachineInstanceMigrationSourceState{
VirtualMachineInstanceCommonMigrationState: virtv1.VirtualMachineInstanceCommonMigrationState{
SyncAddress: pointer.P(localURL),
},
}
sourceVMI.Status.MigrationState.SourceState = &virtv1.VirtualMachineInstanceMigrationSourceState{
VirtualMachineInstanceCommonMigrationState: virtv1.VirtualMachineInstanceCommonMigrationState{
MigrationUID: sourceTestMigrationUID,
},
}
sourceVMI.Status.MigrationState.TargetState = &virtv1.VirtualMachineInstanceMigrationTargetState{
VirtualMachineInstanceCommonMigrationState: virtv1.VirtualMachineInstanceCommonMigrationState{
SyncAddress: pointer.P(remoteURL),
},
}
By("setting up the clients and informers")
err = remoteController.vmiInformer.GetStore().Update(targetVMI)
Expect(err).ToNot(HaveOccurred())
_, err = remoteController.client.VirtualMachineInstance(targetVMI.Namespace).Update(context.Background(), targetVMI, metav1.UpdateOptions{})
Expect(err).ToNot(HaveOccurred())
err = controller.vmiInformer.GetStore().Update(sourceVMI)
Expect(err).ToNot(HaveOccurred())
_, err = controller.client.VirtualMachineInstance(sourceVMI.Namespace).Update(context.Background(), sourceVMI, metav1.UpdateOptions{})
Expect(err).ToNot(HaveOccurred())
targetMigration.DeletionTimestamp = pointer.P(metav1.Now())
err = remoteController.migrationInformer.GetStore().Update(targetMigration)
Expect(err).ToNot(HaveOccurred())
res, err := controller.CancelMigration(context.Background(), &syncv1.MigrationCancelRequest{
MigrationUID: targetTestMigrationUID,
})
Expect(err).ToNot(HaveOccurred())
Expect(res.Message).To(Equal("migration canceled"))
})
})
It("should return nil, nil if invalid resource type passed into index function", func() {
res, err := indexByMigrationUID("invalid")
Expect(res).To(BeNil())
Expect(err).ToNot(HaveOccurred())
res, err = indexByActiveVmiName("invalid")
Expect(res).To(BeNil())
Expect(err).ToNot(HaveOccurred())
res, err = indexBySourceMigrationID("invalid")
Expect(res).To(BeNil())
Expect(err).ToNot(HaveOccurred())
res, err = indexByTargetMigrationID("invalid")
Expect(res).To(BeNil())
Expect(err).ToNot(HaveOccurred())
})
It("should properly close connections", func() {
testConnection := &SynchronizationConnection{
migrationID: testMigrationID,
}
controller.syncOutboundConnectionMap.Store(testMigrationID, testConnection)
controller.syncOutboundConnectionMap.Store("invalidID", "invalidConnection")
_, loaded := controller.syncOutboundConnectionMap.Load("invalidID")
Expect(loaded).To(BeTrue())
_, loaded = controller.syncOutboundConnectionMap.Load(testMigrationID)
Expect(loaded).To(BeTrue())
controller.closeConnectionForMigrationID(controller.syncOutboundConnectionMap, testMigrationID)
controller.closeConnectionForMigrationID(controller.syncOutboundConnectionMap, "invalidID")
controller.closeConnectionForMigrationID(controller.syncOutboundConnectionMap, "unknownID")
_, loaded = controller.syncOutboundConnectionMap.Load("invalidID")
Expect(loaded).To(BeFalse())
_, loaded = controller.syncOutboundConnectionMap.Load(testMigrationID)
Expect(loaded).To(BeFalse())
_, loaded = controller.syncOutboundConnectionMap.Load("unknownID")
Expect(loaded).To(BeFalse())
})
Context("resource handler functions", func() {
var (
sourceMigration, targetMigration *virtv1.VirtualMachineInstanceMigration
)
BeforeEach(func() {
sourceMigration = createSourceMigration(testMigrationID, "test-vmi", "", k8sv1.NamespaceDefault)
targetMigration = createTargetMigration(testMigrationID, "test-vmi", k8sv1.NamespaceDefault)
Expect(controller.queue.Len()).To(Equal(0))
})
It("should add proper key to queue when adding/updating new VMIM", func() {
controller.addMigrationFunc(sourceMigration)
Expect(controller.queue.Len()).To(Equal(1))
key := fmt.Sprintf("%s/%s", sourceMigration.Namespace, sourceMigration.Spec.VMIName)
res, shutdown := controller.queue.Get()
Expect(shutdown).To(BeFalse())
Expect(res).To(Equal(key))
controller.queue.Done(key)
Expect(controller.queue.Len()).To(Equal(0))
controller.updateMigrationFunc(nil, sourceMigration)
Expect(controller.queue.Len()).To(Equal(1))
key = fmt.Sprintf("%s/%s", sourceMigration.Namespace, sourceMigration.Spec.VMIName)
res, shutdown = controller.queue.Get()
Expect(shutdown).To(BeFalse())
Expect(res).To(Equal(key))
controller.queue.Done(key)
Expect(controller.queue.Len()).To(Equal(0))
})
It("should close existing connections when deleting vmim", func() {
testConnection := &SynchronizationConnection{
migrationID: testMigrationID,
}
controller.syncOutboundConnectionMap.Store(testMigrationID, testConnection)
controller.deleteMigrationFunc(sourceMigration)
Expect(controller.queue.Len()).To(Equal(1))
key := fmt.Sprintf("%s/%s", sourceMigration.Namespace, sourceMigration.Spec.VMIName)
res, shutdown := controller.queue.Get()
Expect(shutdown).To(BeFalse())
Expect(res).To(Equal(key))
controller.queue.Done(key)
Expect(controller.queue.Len()).To(Equal(0))
_, loaded := controller.syncOutboundConnectionMap.Load(testMigrationID)
Expect(loaded).To(BeFalse())
controller.syncReceivingConnectionMap.Store(testMigrationID, testConnection)
controller.deleteMigrationFunc(targetMigration)
Expect(controller.queue.Len()).To(Equal(1))
key = fmt.Sprintf("%s/%s", targetMigration.Namespace, targetMigration.Spec.VMIName)
res, shutdown = controller.queue.Get()
Expect(shutdown).To(BeFalse())
Expect(res).To(Equal(key))
controller.queue.Done(key)
Expect(controller.queue.Len()).To(Equal(0))
_, loaded = controller.syncReceivingConnectionMap.Load(testMigrationID)
Expect(loaded).To(BeFalse())
})
})
It("should be able to find the VMI from the migration", func() {
vmi := libvmi.New(libvmi.WithNamespace(k8sv1.NamespaceDefault))
err := controller.vmiInformer.GetStore().Add(vmi)
Expect(err).ToNot(HaveOccurred())
migration := createTargetMigration(testMigrationID, vmi.Name, vmi.Namespace)
res, err := controller.getVMIFromMigration(migration)
Expect(err).ToNot(HaveOccurred())
Expect(res).To(BeEquivalentTo(vmi))
migration = createTargetMigration(testMigrationID, "unknown", vmi.Namespace)
res, err = controller.getVMIFromMigration(migration)
Expect(err).ToNot(HaveOccurred())
Expect(res).To(BeNil())
})
Context("getMergedTargetMigratedVolumes", func() {
It("should merge destination PVC volume mode and access modes from remote when local destination lacks them", func() {
volumeModeBlock := k8sv1.PersistentVolumeBlock
vmiVolumes := createVmiVolumes(
createVmiVolume(
withVolumeName("volume1"),
withSourcePVC("source-pvc-1", nil, nil),
withDestinationPVC("dest-pvc-1", nil, nil),
),
)
remoteVolumes := createRemoteVolumes(
createRemoteVolume(
withVolumeName("volume1"),
withDestinationPVC("dest-pvc-1", &volumeModeBlock, []k8sv1.PersistentVolumeAccessMode{k8sv1.ReadWriteMany}),
),
)
result := getMergedTargetMigratedVolumes(vmiVolumes, remoteVolumes)
verifyVolumes(result, 1, []volumeVerificationOption{
verifyVolumeName("volume1"),
verifySourcePVC("source-pvc-1", nil, nil),
verifyDestinationPVC("dest-pvc-1", &volumeModeBlock, []k8sv1.PersistentVolumeAccessMode{k8sv1.ReadWriteMany}),
})
})
It("should include VMI volumes that don't have matches in remote", func() {
vmiVolumes := createVmiVolumes(
createVmiVolume(
withVolumeName("volume1"),
withSourcePVC("source-pvc-1", nil, nil),
withDestinationPVC("dest-pvc-1", nil, nil),
),
createVmiVolume(
withVolumeName("volume2"),
withSourcePVC("source-pvc-2", nil, nil),
withDestinationPVC("dest-pvc-2", nil, nil),
),
)
remoteVolumes := createRemoteVolumes(
createRemoteVolume(
withVolumeName("volume1"),
withDestinationPVC("dest-pvc-1-new", nil, nil),
),
)
result := getMergedTargetMigratedVolumes(vmiVolumes, remoteVolumes)
verifyVolumes(result, 2,
[]volumeVerificationOption{
verifyVolumeName("volume1"),
verifyDestinationPVC("dest-pvc-1-new", nil, nil),
},
[]volumeVerificationOption{
verifyVolumeName("volume2"),
verifyDestinationPVC("dest-pvc-2", nil, nil),
},
)
})
DescribeTable("should handle empty volume lists",
func(vmiVolumes []virtv1.StorageMigratedVolumeInfo, remoteVolumes []virtv1.StorageMigratedVolumeInfo, expectedLen int, description string) {
result := getMergedTargetMigratedVolumes(vmiVolumes, remoteVolumes)
Expect(result).To(HaveLen(expectedLen), description)
if expectedLen > 0 {
verifyVolume(result[0],
verifyVolumeName("volume1"),
verifySourcePVC("source-pvc-1", nil, nil),
verifyDestinationPVC("dest-pvc-1", nil, nil),
)
}
},
Entry("empty VMI volumes list", createVmiVolumes(), createRemoteVolumes(
createRemoteVolume(
withVolumeName("volume1"),
withDestinationPVC("dest-pvc-1", nil, nil),
),
), 0, "should return empty when VMI volumes is empty"),
Entry("empty remote volumes list", createVmiVolumes(
createVmiVolume(
withVolumeName("volume1"),
withSourcePVC("source-pvc-1", nil, nil),
withDestinationPVC("dest-pvc-1", nil, nil),
),
), createRemoteVolumes(), 1, "should return VMI volumes when remote is empty"),
)
DescribeTable("should handle nil PVC info",
func(vmiVolumes []virtv1.StorageMigratedVolumeInfo, remoteVolumes []virtv1.StorageMigratedVolumeInfo, expectNilSource, expectNilDest bool, destClaimName string) {
result := getMergedTargetMigratedVolumes(vmiVolumes, remoteVolumes)
opts := []volumeVerificationOption{verifyVolumeName("volume1")}
if expectNilSource {
opts = append(opts, verifySourceIsNil())
} else {
opts = append(opts, verifySourcePVC("source-pvc-1", nil, nil))
}
if expectNilDest {
opts = append(opts, verifyDestinationIsNil())
} else {
opts = append(opts, verifyDestinationPVC(destClaimName, nil, nil))
}
verifyVolumes(result, 1, opts)
},
Entry("nil SourcePVCInfo in VMI volumes", createVmiVolumes(
createVmiVolume(
withVolumeName("volume1"),
withNilSourcePVC(),
withDestinationPVC("dest-pvc-1", nil, nil),
),
), createRemoteVolumes(
createRemoteVolume(
withVolumeName("volume1"),
withDestinationPVC("dest-pvc-1-new", nil, nil),
),
), true, false, "dest-pvc-1-new"),
Entry("nil DestinationPVCInfo in remote volumes", createVmiVolumes(
createVmiVolume(
withVolumeName("volume1"),
withSourcePVC("source-pvc-1", nil, nil),
withDestinationPVC("dest-pvc-1", nil, nil),
),
), createRemoteVolumes(
createRemoteVolume(
withVolumeName("volume1"),
withNilDestinationPVC(),
),
), false, true, ""),
)
It("should preserve local source PVC info when merging destination from remote", func() {
volumeModeFS := k8sv1.PersistentVolumeFilesystem
volumeModeBlock := k8sv1.PersistentVolumeBlock
vmiVolumes := createVmiVolumes(
createVmiVolume(
withVolumeName("volume1"),
withSourcePVC("source-pvc-1", &volumeModeFS, []k8sv1.PersistentVolumeAccessMode{k8sv1.ReadWriteOnce}),
withDestinationPVC("dest-pvc-1", nil, nil),
),
)
remoteVolumes := createRemoteVolumes(
createRemoteVolume(
withVolumeName("volume1"),
withDestinationPVC("dest-pvc-1", &volumeModeBlock, []k8sv1.PersistentVolumeAccessMode{k8sv1.ReadWriteMany}),
),
)
result := getMergedTargetMigratedVolumes(vmiVolumes, remoteVolumes)
verifyVolumes(result, 1, []volumeVerificationOption{
verifySourcePVC("source-pvc-1", &volumeModeFS, []k8sv1.PersistentVolumeAccessMode{k8sv1.ReadWriteOnce}),
verifyDestinationPVC("dest-pvc-1", &volumeModeBlock, []k8sv1.PersistentVolumeAccessMode{k8sv1.ReadWriteMany}),
})
})
It("should merge multiple volumes with different volume mode and access mode combinations", func() {
volumeModeFS := k8sv1.PersistentVolumeFilesystem
volumeModeBlock := k8sv1.PersistentVolumeBlock
vmiVolumes := createVmiVolumes(
createVmiVolume(
withVolumeName("volume1"),
withSourcePVC("source-pvc-1", nil, nil),
withDestinationPVC("dest-pvc-1", nil, nil),
),
createVmiVolume(
withVolumeName("volume2"),
withSourcePVC("source-pvc-2", nil, nil),
withDestinationPVC("dest-pvc-2", &volumeModeFS, []k8sv1.PersistentVolumeAccessMode{k8sv1.ReadWriteOnce}),
),
)
remoteVolumes := createRemoteVolumes(
createRemoteVolume(
withVolumeName("volume1"),
withDestinationPVC("dest-pvc-1", &volumeModeBlock, []k8sv1.PersistentVolumeAccessMode{k8sv1.ReadWriteMany}),
),
createRemoteVolume(
withVolumeName("volume2"),
withDestinationPVC("dest-pvc-2", &volumeModeBlock, []k8sv1.PersistentVolumeAccessMode{k8sv1.ReadOnlyMany}),
),
)
result := getMergedTargetMigratedVolumes(vmiVolumes, remoteVolumes)
verifyVolumes(result, 2,
[]volumeVerificationOption{
verifyVolumeName("volume1"),
verifyDestinationPVC("dest-pvc-1", &volumeModeBlock, []k8sv1.PersistentVolumeAccessMode{k8sv1.ReadWriteMany}),
},
[]volumeVerificationOption{
verifyVolumeName("volume2"),
verifyDestinationPVC("dest-pvc-2", &volumeModeBlock, []k8sv1.PersistentVolumeAccessMode{k8sv1.ReadOnlyMany}),
},
)
})
It("should handle volumes with empty volume names", func() {
vmiVolumes := createVmiVolumes(
createVmiVolume(
withVolumeName(""),
withSourcePVC("source-pvc-1", nil, nil),
withDestinationPVC("dest-pvc-1", nil, nil),
),
)
remoteVolumes := createRemoteVolumes(
createRemoteVolume(
withVolumeName(""),
withDestinationPVC("dest-pvc-1-new", nil, nil),
),
)
result := getMergedTargetMigratedVolumes(vmiVolumes, remoteVolumes)
verifyVolumes(result, 1, []volumeVerificationOption{
verifyVolumeName(""),
})
})
})
Context("getMergedSourceMigratedVolumes", func() {
It("should use VMI SourcePVCInfo when present, even if remote has volume mode and access modes", func() {
volumeModeBlock := k8sv1.PersistentVolumeBlock
vmiVolumes := createVmiVolumes(
createVmiVolume(
withVolumeName("volume1"),
withSourcePVC("source-pvc-1-vmi", nil, nil),
withDestinationPVC("dest-pvc-1", nil, nil),
),
)
remoteVolumes := createRemoteVolumes(
createRemoteVolume(
withVolumeName("volume1"),
withSourcePVC("source-pvc-1-remote", &volumeModeBlock, []k8sv1.PersistentVolumeAccessMode{k8sv1.ReadWriteMany}),
),
)
result := getMergedSourceMigratedVolumes(vmiVolumes, remoteVolumes)
verifyVolumes(result, 1, []volumeVerificationOption{
verifyVolumeName("volume1"),
verifySourcePVC("source-pvc-1-vmi", nil, nil),
verifySourceVolumeModeIsNil(),
verifySourceAccessModesIsNil(),
verifyDestinationPVC("dest-pvc-1", nil, nil),
})
})
It("should use remote SourcePVCInfo with volume mode and access modes when VMI SourcePVCInfo is nil", func() {
volumeModeBlock := k8sv1.PersistentVolumeBlock
vmiVolumes := createVmiVolumes(
createVmiVolume(
withVolumeName("volume1"),
withNilSourcePVC(),
withDestinationPVC("dest-pvc-1", nil, nil),
),
)
remoteVolumes := createRemoteVolumes(
createRemoteVolume(
withVolumeName("volume1"),
withSourcePVC("source-pvc-1-remote", &volumeModeBlock, []k8sv1.PersistentVolumeAccessMode{k8sv1.ReadWriteMany}),
),
)
result := getMergedSourceMigratedVolumes(vmiVolumes, remoteVolumes)
verifyVolumes(result, 1, []volumeVerificationOption{
verifyVolumeName("volume1"),
verifySourcePVC("source-pvc-1-remote", &volumeModeBlock, []k8sv1.PersistentVolumeAccessMode{k8sv1.ReadWriteMany}),
verifyDestinationPVC("dest-pvc-1", nil, nil),
})
})
It("should include remote volumes with volume mode and access modes that don't have matches in VMI", func() {
volumeModeBlock := k8sv1.PersistentVolumeBlock
vmiVolumes := createVmiVolumes(
createVmiVolume(
withVolumeName("volume1"),
withSourcePVC("source-pvc-1", nil, nil),
withDestinationPVC("dest-pvc-1", nil, nil),
),
)
remoteVolumes := createRemoteVolumes(
createRemoteVolume(
withVolumeName("volume1"),
withSourcePVC("source-pvc-1-remote", nil, nil),
),
createRemoteVolume(
withVolumeName("volume2"),
withSourcePVC("source-pvc-2", &volumeModeBlock, []k8sv1.PersistentVolumeAccessMode{k8sv1.ReadWriteMany}),
),
)
result := getMergedSourceMigratedVolumes(vmiVolumes, remoteVolumes)
verifyVolumes(result, 2,
[]volumeVerificationOption{
verifyVolumeName("volume1"),
verifySourcePVC("source-pvc-1", nil, nil),
},
[]volumeVerificationOption{
verifyVolumeName("volume2"),
verifySourcePVC("source-pvc-2", &volumeModeBlock, []k8sv1.PersistentVolumeAccessMode{k8sv1.ReadWriteMany}),
},
)
})
DescribeTable("should handle empty volume lists",
func(vmiVolumes []virtv1.StorageMigratedVolumeInfo, remoteVolumes []virtv1.StorageMigratedVolumeInfo, expectedLen int, description string) {
result := getMergedSourceMigratedVolumes(vmiVolumes, remoteVolumes)
Expect(result).To(HaveLen(expectedLen), description)
if expectedLen > 0 {
verifyVolume(result[0],
verifyVolumeName("volume1"),
verifySourcePVC("source-pvc-1", nil, nil),
)
}
},
Entry("empty VMI volumes list", createVmiVolumes(), createRemoteVolumes(
createRemoteVolume(
withVolumeName("volume1"),
withSourcePVC("source-pvc-1", nil, nil),
),
), 1, "should return remote volumes when VMI volumes is empty"),
Entry("empty remote volumes list", createVmiVolumes(
createVmiVolume(
withVolumeName("volume1"),
withSourcePVC("source-pvc-1", nil, nil),
withDestinationPVC("dest-pvc-1", nil, nil),
),
), createRemoteVolumes(), 0, "should return empty when remote volumes is empty"),
)
DescribeTable("should handle nil PVC info",
func(vmiVolumes []virtv1.StorageMigratedVolumeInfo, remoteVolumes []virtv1.StorageMigratedVolumeInfo, expectNilSource, expectNilDest bool, sourceClaimName string) {
result := getMergedSourceMigratedVolumes(vmiVolumes, remoteVolumes)
opts := []volumeVerificationOption{verifyVolumeName("volume1")}
if expectNilSource {
opts = append(opts, verifySourceIsNil())
} else {
opts = append(opts, verifySourcePVC(sourceClaimName, nil, nil))
}
if expectNilDest {
opts = append(opts, verifyDestinationIsNil())
} else {
opts = append(opts, verifyDestinationPVC("dest-pvc-1", nil, nil))
}
verifyVolumes(result, 1, opts)
},
Entry("nil SourcePVCInfo in both VMI and remote volumes", createVmiVolumes(
createVmiVolume(
withVolumeName("volume1"),
withNilSourcePVC(),
withDestinationPVC("dest-pvc-1", nil, nil),
),
), createRemoteVolumes(
createRemoteVolume(
withVolumeName("volume1"),
withNilSourcePVC(),
),
), true, false, ""),
Entry("nil DestinationPVCInfo in VMI volumes", createVmiVolumes(
createVmiVolume(
withVolumeName("volume1"),
withSourcePVC("source-pvc-1", nil, nil),
withNilDestinationPVC(),
),
), createRemoteVolumes(
createRemoteVolume(
withVolumeName("volume1"),
withSourcePVC("source-pvc-1-remote", nil, nil),
),
), false, true, "source-pvc-1"),
)
It("should preserve VMI SourcePVCInfo volume mode and access modes when VMI has them set", func() {
volumeModeFS := k8sv1.PersistentVolumeFilesystem
volumeModeBlock := k8sv1.PersistentVolumeBlock
vmiVolumes := createVmiVolumes(
createVmiVolume(
withVolumeName("volume1"),
withSourcePVC("source-pvc-1", &volumeModeFS, []k8sv1.PersistentVolumeAccessMode{k8sv1.ReadWriteOnce}),
withDestinationPVC("dest-pvc-1", nil, nil),
),
)
remoteVolumes := createRemoteVolumes(
createRemoteVolume(
withVolumeName("volume1"),
withSourcePVC("source-pvc-1-remote", &volumeModeBlock, []k8sv1.PersistentVolumeAccessMode{k8sv1.ReadWriteMany}),
),
)
result := getMergedSourceMigratedVolumes(vmiVolumes, remoteVolumes)
verifyVolumes(result, 1, []volumeVerificationOption{
verifyVolumeName("volume1"),
verifySourcePVC("source-pvc-1", &volumeModeFS, []k8sv1.PersistentVolumeAccessMode{k8sv1.ReadWriteOnce}),
})
})
It("should ensure returned volumes have valid structure", func() {
vmiVolumes := createVmiVolumes(
createVmiVolume(
withVolumeName("volume1"),
withSourcePVC("source-pvc-1", nil, []k8sv1.PersistentVolumeAccessMode{k8sv1.ReadWriteOnce}),
withDestinationPVC("dest-pvc-1", nil, []k8sv1.PersistentVolumeAccessMode{k8sv1.ReadWriteMany}),
),
)
remoteVolumes := createRemoteVolumes(
createRemoteVolume(
withVolumeName("volume1"),
withSourcePVC("source-pvc-1-remote", nil, []k8sv1.PersistentVolumeAccessMode{k8sv1.ReadOnlyMany}),
),
)
result := getMergedSourceMigratedVolumes(vmiVolumes, remoteVolumes)
verifyVolumes(result, 1, []volumeVerificationOption{
verifySourcePVC("source-pvc-1", nil, []k8sv1.PersistentVolumeAccessMode{k8sv1.ReadWriteOnce}),
verifyDestinationPVC("dest-pvc-1", nil, []k8sv1.PersistentVolumeAccessMode{k8sv1.ReadWriteMany}),
})
})
It("should handle multiple volumes with various combinations", func() {
vmiVolumes := createVmiVolumes(
createVmiVolume(
withVolumeName("volume1"),
withSourcePVC("source-pvc-1", nil, nil),
withDestinationPVC("dest-pvc-1", nil, nil),
),
createVmiVolume(
withVolumeName("volume2"),
withNilSourcePVC(),
withDestinationPVC("dest-pvc-2", nil, nil),
),
)
remoteVolumes := createRemoteVolumes(
createRemoteVolume(
withVolumeName("volume1"),
withSourcePVC("source-pvc-1-remote", nil, nil),
),
createRemoteVolume(
withVolumeName("volume2"),
withSourcePVC("source-pvc-2-remote", nil, nil),
),
createRemoteVolume(
withVolumeName("volume3"),
withSourcePVC("source-pvc-3", nil, nil),
),
)
result := getMergedSourceMigratedVolumes(vmiVolumes, remoteVolumes)
verifyVolumes(result, 3,
[]volumeVerificationOption{
verifyVolumeName("volume1"),
verifySourcePVC("source-pvc-1", nil, nil),
},
[]volumeVerificationOption{
verifyVolumeName("volume2"),
verifySourcePVC("source-pvc-2-remote", nil, nil),
},
[]volumeVerificationOption{
verifyVolumeName("volume3"),
verifySourcePVC("source-pvc-3", nil, nil),
},
)
})
It("should handle volumes with empty volume names", func() {
vmiVolumes := createVmiVolumes(
createVmiVolume(
withVolumeName(""),
withSourcePVC("source-pvc-1", nil, nil),
withDestinationPVC("dest-pvc-1", nil, nil),
),
)
remoteVolumes := createRemoteVolumes(
createRemoteVolume(
withVolumeName(""),
withSourcePVC("source-pvc-1-remote", nil, nil),
),
)
result := getMergedSourceMigratedVolumes(vmiVolumes, remoteVolumes)
verifyVolumes(result, 1, []volumeVerificationOption{
verifyVolumeName(""),
})
})
})
})
func createSourceMigration(migrationID, vmiName, connectionURL, namespace string) *virtv1.VirtualMachineInstanceMigration {
return &virtv1.VirtualMachineInstanceMigration{
ObjectMeta: metav1.ObjectMeta{
Name: sourceTestMigrationName,
Namespace: namespace,
UID: sourceTestMigrationUID,
},
Spec: virtv1.VirtualMachineInstanceMigrationSpec{
SendTo: &virtv1.VirtualMachineInstanceMigrationSource{
MigrationID: migrationID,
ConnectURL: connectionURL,
},
VMIName: vmiName,
},
}
}
func createTargetMigration(migrationID, vmiName, namespace string) *virtv1.VirtualMachineInstanceMigration {
return &virtv1.VirtualMachineInstanceMigration{
ObjectMeta: metav1.ObjectMeta{
Name: targetTestMigrationName,
Namespace: namespace,
UID: targetTestMigrationUID,
},
Spec: virtv1.VirtualMachineInstanceMigrationSpec{
Receive: &virtv1.VirtualMachineInstanceMigrationTarget{
MigrationID: migrationID,
},
VMIName: vmiName,
},
}
}
func createLegacyMigration() *virtv1.VirtualMachineInstanceMigration {
return &virtv1.VirtualMachineInstanceMigration{
ObjectMeta: metav1.ObjectMeta{
Name: testMigrationID,
Namespace: k8sv1.NamespaceDefault,
UID: testMigrationUID,
},
}
}
type volumeOption func(*virtv1.StorageMigratedVolumeInfo)
func withVolumeName(name string) volumeOption {
return func(v *virtv1.StorageMigratedVolumeInfo) {
v.VolumeName = name
}
}
func withSourcePVC(claimName string, volumeMode *k8sv1.PersistentVolumeMode, accessModes []k8sv1.PersistentVolumeAccessMode) volumeOption {
return func(v *virtv1.StorageMigratedVolumeInfo) {
v.SourcePVCInfo = &virtv1.PersistentVolumeClaimInfo{
ClaimName: claimName,
VolumeMode: volumeMode,
AccessModes: accessModes,
}
}
}
func withNilSourcePVC() volumeOption {
return func(v *virtv1.StorageMigratedVolumeInfo) {
v.SourcePVCInfo = nil
}
}
func withDestinationPVC(claimName string, volumeMode *k8sv1.PersistentVolumeMode, accessModes []k8sv1.PersistentVolumeAccessMode) volumeOption {
return func(v *virtv1.StorageMigratedVolumeInfo) {
v.DestinationPVCInfo = &virtv1.PersistentVolumeClaimInfo{
ClaimName: claimName,
VolumeMode: volumeMode,
AccessModes: accessModes,
}
}
}
func withNilDestinationPVC() volumeOption {
return func(v *virtv1.StorageMigratedVolumeInfo) {
v.DestinationPVCInfo = nil
}
}
func createVmiVolume(opts ...volumeOption) virtv1.StorageMigratedVolumeInfo {
volume := virtv1.StorageMigratedVolumeInfo{}
for _, opt := range opts {
opt(&volume)
}
return volume
}
func createVmiVolumes(volumes ...virtv1.StorageMigratedVolumeInfo) []virtv1.StorageMigratedVolumeInfo {
return volumes
}
func createRemoteVolume(opts ...volumeOption) virtv1.StorageMigratedVolumeInfo {
return createVmiVolume(opts...)
}
func createRemoteVolumes(volumes ...virtv1.StorageMigratedVolumeInfo) []virtv1.StorageMigratedVolumeInfo {
return volumes
}
type volumeVerificationOption func(*volumeVerification)
type volumeVerification struct {
volumeName string
sourceClaimName string
sourceVolumeMode *k8sv1.PersistentVolumeMode
sourceAccessModes []k8sv1.PersistentVolumeAccessMode
sourceShouldBeNil bool
sourceVolumeModeShouldNil bool
sourceAccessModesShouldNil bool
destClaimName string
destVolumeMode *k8sv1.PersistentVolumeMode
destAccessModes []k8sv1.PersistentVolumeAccessMode
destShouldBeNil bool
destVolumeModeShouldNil bool
destAccessModesShouldNil bool
}
func verifyVolumeName(name string) volumeVerificationOption {
return func(v *volumeVerification) {
v.volumeName = name
}
}
func verifySourcePVC(claimName string, volumeMode *k8sv1.PersistentVolumeMode, accessModes []k8sv1.PersistentVolumeAccessMode) volumeVerificationOption {
return func(v *volumeVerification) {
v.sourceClaimName = claimName
v.sourceVolumeMode = volumeMode
v.sourceAccessModes = accessModes
v.sourceShouldBeNil = false
}
}
func verifySourceVolumeModeIsNil() volumeVerificationOption {
return func(v *volumeVerification) {
v.sourceVolumeModeShouldNil = true
}
}
func verifySourceAccessModesIsNil() volumeVerificationOption {
return func(v *volumeVerification) {
v.sourceAccessModesShouldNil = true
}
}
func verifySourceIsNil() volumeVerificationOption {
return func(v *volumeVerification) {
v.sourceShouldBeNil = true
}
}
func verifyDestinationPVC(claimName string, volumeMode *k8sv1.PersistentVolumeMode, accessModes []k8sv1.PersistentVolumeAccessMode) volumeVerificationOption {
return func(v *volumeVerification) {
v.destClaimName = claimName
v.destVolumeMode = volumeMode
v.destAccessModes = accessModes
v.destShouldBeNil = false
}
}
func verifyDestinationVolumeModeIsNil() volumeVerificationOption {
return func(v *volumeVerification) {
v.destVolumeModeShouldNil = true
}
}
func verifyDestinationAccessModesIsNil() volumeVerificationOption {
return func(v *volumeVerification) {
v.destAccessModesShouldNil = true
}
}
func verifyDestinationIsNil() volumeVerificationOption {
return func(v *volumeVerification) {
v.destShouldBeNil = true
}
}
func verifyVolume(volume virtv1.StorageMigratedVolumeInfo, opts ...volumeVerificationOption) {
verification := &volumeVerification{}
for _, opt := range opts {
opt(verification)
}
if verification.volumeName != "" {
Expect(volume.VolumeName).To(Equal(verification.volumeName))
}
if verification.sourceShouldBeNil {
Expect(volume.SourcePVCInfo).To(BeNil())
} else if verification.sourceClaimName != "" {
Expect(volume.SourcePVCInfo).ToNot(BeNil())
Expect(volume.SourcePVCInfo.ClaimName).To(Equal(verification.sourceClaimName))
if verification.sourceVolumeModeShouldNil {
Expect(volume.SourcePVCInfo.VolumeMode).To(BeNil())
} else if verification.sourceVolumeMode != nil {
Expect(volume.SourcePVCInfo.VolumeMode).ToNot(BeNil())
Expect(*volume.SourcePVCInfo.VolumeMode).To(Equal(*verification.sourceVolumeMode))
}
if verification.sourceAccessModesShouldNil {
Expect(volume.SourcePVCInfo.AccessModes).To(BeNil())
} else if verification.sourceAccessModes != nil {
Expect(volume.SourcePVCInfo.AccessModes).To(Equal(verification.sourceAccessModes))
}
}
if verification.destShouldBeNil {
Expect(volume.DestinationPVCInfo).To(BeNil())
} else if verification.destClaimName != "" {
Expect(volume.DestinationPVCInfo).ToNot(BeNil())
Expect(volume.DestinationPVCInfo.ClaimName).To(Equal(verification.destClaimName))
if verification.destVolumeModeShouldNil {
Expect(volume.DestinationPVCInfo.VolumeMode).To(BeNil())
} else if verification.destVolumeMode != nil {
Expect(volume.DestinationPVCInfo.VolumeMode).ToNot(BeNil())
Expect(*volume.DestinationPVCInfo.VolumeMode).To(Equal(*verification.destVolumeMode))
}
if verification.destAccessModesShouldNil {
Expect(volume.DestinationPVCInfo.AccessModes).To(BeNil())
} else if verification.destAccessModes != nil {
Expect(volume.DestinationPVCInfo.AccessModes).To(Equal(verification.destAccessModes))
}
}
}
func verifyVolumes(result []virtv1.StorageMigratedVolumeInfo, expectedLen int, verifications ...[]volumeVerificationOption) {
Expect(result).To(HaveLen(expectedLen))
for i, verifyOpts := range verifications {
if i < len(result) {
verifyVolume(result[i], verifyOpts...)
}
}
}