Repository: kubernetes-sigs/cluster-api-provider-kubevirt (go)

Issue:
VM deletion looks for resource in wrong namespace
**What steps did you take and what happened:**

When deleting a cluster that was spawned in an external infrastructure cluster, the [`pkg/kubevirt/machine.go Machine#Delete`](https://github.pie.apple.com/kubernetes-sigs/cluster-api-provider-kubevirt/blob/41c77a793ccbcaa21a05d9b76fd2d4f02c4934f5/pkg/kubevirt/machine.go#L254) method looks for the Virtual Machine to delete in the correct cluster, but in the same namespace name than where the `KubeVirtMachine` resource is in the management cluster:

```go
namespacedName := types.NamespacedName{Namespace: m.machineContext.KubevirtMachine.Namespace, Name: ...
```

Unless the same namespace name is used, then the VM failed to be found and the VM is not deleted as expected.


**What did you expect to happen:**

The logic should look in the correct namespace for the VM; which is already provided when creating the struct in the [NewMachine](https://github.pie.apple.com/kubernetes-sigs/cluster-api-provider-kubevirt/blob/41c77a793ccbcaa21a05d9b76fd2d4f02c4934f5/pkg/kubevirt/machine.go#L50) function.


**Anything else you would like to add:**

The [`pkg/kubevirt/machine.go Machine#Delete`](https://github.pie.apple.com/kubernetes-sigs/cluster-api-provider-kubevirt/blob/41c77a793ccbcaa21a05d9b76fd2d4f02c4934f5/pkg/kubevirt/machine.go#L254) also currently silences any other error that might prevent the VM to be found. In this case, the `vm` will most likely not be properly populated and the following call to delete it will fail, but with an unclear error. Instead, we should also directly fail when the `Get` of the VM fails with a non not-found error.

**Environment:**

- Cluster-api version: **v1beta1**
- Cluster-api-provider-kubevirt version: **v1alpha1**
- Kubernetes version: (use `kubectl version`): **v1.20.15**
- KubeVirt version: **v1**
- OS (e.g. from `/etc/os-release`): **CentOS 7**

/kind bug


Pull request:
Fix external deletion
**What this PR does / why we need it**: 

This PR fixes the deletion of a tenant-cluster spawned in an external infrastructure cluster. Before this change, unless the tenant cluster was in the namespace with the same name than the namespace where its specification has been created in the management cluster, then the logic that looks for the VMs will not find them and not delete them.

**Which issue this PR fixes**: fixes #150

**Special notes for your reviewer**:

Please note that this PR contains two commits to help with the review:

1. The first commit includes only changes to `pkg/kubevirt/machine_test.go` to allow specifying a different namespace and to add missing tests of the deletion when the tenant-cluster is spawned internally in the management cluster.
2. The second commit includes the fix of the issue and the new tests to validate working with external tenant clusters.

**Release notes**:

```release-note
Fixes deletion of VirtualMachine of tenant clusters created in external infrastructure cluster.
```


Patch:
diff --git a/pkg/kubevirt/machine.go b/pkg/kubevirt/machine.go
index bb52062..38bcc44 100644
--- a/pkg/kubevirt/machine.go
+++ b/pkg/kubevirt/machine.go
@@ -252,13 +252,14 @@ func (m *Machine) GenerateProviderID() (string, error) {
 
 // Delete deletes VM for this machine.
 func (m *Machine) Delete() error {
-	namespacedName := types.NamespacedName{Namespace: m.machineContext.KubevirtMachine.Namespace, Name: m.machineContext.KubevirtMachine.Name}
+	namespacedName := types.NamespacedName{Namespace: m.namespace, Name: m.machineContext.KubevirtMachine.Name}
 	vm := &kubevirtv1.VirtualMachine{}
 	if err := m.client.Get(m.machineContext.Context, namespacedName, vm); err != nil {
 		if apierrors.IsNotFound(err) {
 			m.machineContext.Logger.Info("VM does not exist, nothing to do.")
 			return nil
 		}
+		return errors.Wrapf(err, "failed to retrieve VM to delete")
 	}
 
 	if err := m.client.Delete(gocontext.Background(), vm); err != nil {


Test patch:
diff --git a/pkg/kubevirt/machine_test.go b/pkg/kubevirt/machine_test.go
index 35853f1..1352171 100644
--- a/pkg/kubevirt/machine_test.go
+++ b/pkg/kubevirt/machine_test.go
@@ -52,9 +52,6 @@ var (
 	kubevirtMachine     = testing.NewKubevirtMachine(kubevirtMachineName, machineName)
 	machine             = testing.NewMachine(clusterName, machineName, kubevirtMachine)
 
-	virtualMachineInstance = testing.NewVirtualMachineInstance(kubevirtMachine)
-	virtualMachine         = testing.NewVirtualMachine(virtualMachineInstance)
-
 	bootstrapDataSecret = testing.NewBootstrapDataSecret([]byte(fmt.Sprintf("#cloud-config\n\n%s\n", sshKey)))
 
 	logger = zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true)).WithName("machine_test")
@@ -65,6 +62,9 @@ var (
 
 var _ = Describe("Without KubeVirt VM running", func() {
 	var machineContext *context.MachineContext
+	namespace := kubevirtMachine.Namespace
+	virtualMachineInstance := testing.NewVirtualMachineInstance(kubevirtMachine)
+	virtualMachine := testing.NewVirtualMachine(virtualMachineInstance)
 
 	BeforeEach(func() {
 		machineContext = &context.MachineContext{
@@ -92,7 +92,7 @@ var _ = Describe("Without KubeVirt VM running", func() {
 	AfterEach(func() {})
 
 	It("NewMachine should have client and machineContext set, but vmiInstance equal nil", func() {
-		externalMachine, err := defaultTestMachine(machineContext, fakeClient, fakeVMCommandExecutor, []byte{})
+		externalMachine, err := defaultTestMachine(machineContext, namespace, fakeClient, fakeVMCommandExecutor, []byte{})
 		Expect(err).NotTo(HaveOccurred())
 		Expect(externalMachine.client).To(Equal(fakeClient))
 		Expect(externalMachine.machineContext).To(Equal(machineContext))
@@ -100,37 +100,37 @@ var _ = Describe("Without KubeVirt VM running", func() {
 	})
 
 	It("Exists should return false", func() {
-		externalMachine, err := defaultTestMachine(machineContext, fakeClient, fakeVMCommandExecutor, []byte{})
+		externalMachine, err := defaultTestMachine(machineContext, namespace, fakeClient, fakeVMCommandExecutor, []byte{})
 		Expect(err).NotTo(HaveOccurred())
 		Expect(externalMachine.Exists()).To(BeFalse())
 	})
 
 	It("Address should return ''", func() {
-		externalMachine, err := defaultTestMachine(machineContext, fakeClient, fakeVMCommandExecutor, []byte{})
+		externalMachine, err := defaultTestMachine(machineContext, namespace, fakeClient, fakeVMCommandExecutor, []byte{})
 		Expect(err).NotTo(HaveOccurred())
 		Expect(externalMachine.Address()).To(Equal(""))
 	})
 
 	It("IsReady should return false", func() {
-		externalMachine, err := defaultTestMachine(machineContext, fakeClient, fakeVMCommandExecutor, []byte{})
+		externalMachine, err := defaultTestMachine(machineContext, namespace, fakeClient, fakeVMCommandExecutor, []byte{})
 		Expect(err).NotTo(HaveOccurred())
 		Expect(externalMachine.IsReady()).To(BeFalse())
 	})
 
 	It("IsBootstrapped should return false", func() {
-		externalMachine, err := defaultTestMachine(machineContext, fakeClient, fakeVMCommandExecutor, []byte{})
+		externalMachine, err := defaultTestMachine(machineContext, namespace, fakeClient, fakeVMCommandExecutor, []byte{})
 		Expect(err).NotTo(HaveOccurred())
 		Expect(externalMachine.IsBootstrapped()).To(BeFalse())
 	})
 
 	It("SupportsCheckingIsBootstrapped should return false", func() {
-		externalMachine, err := defaultTestMachine(machineContext, fakeClient, fakeVMCommandExecutor, []byte{})
+		externalMachine, err := defaultTestMachine(machineContext, namespace, fakeClient, fakeVMCommandExecutor, []byte{})
 		Expect(err).NotTo(HaveOccurred())
 		Expect(externalMachine.SupportsCheckingIsBootstrapped()).To(BeFalse())
 	})
 
 	It("GenerateProviderID should fail", func() {
-		externalMachine, err := defaultTestMachine(machineContext, fakeClient, fakeVMCommandExecutor, []byte{})
+		externalMachine, err := defaultTestMachine(machineContext, namespace, fakeClient, fakeVMCommandExecutor, []byte{})
 		Expect(err).NotTo(HaveOccurred())
 		providerId, err := externalMachine.GenerateProviderID()
 		Expect(err).To(HaveOccurred())
@@ -138,36 +138,192 @@ var _ = Describe("Without KubeVirt VM running", func() {
 	})
 
 	It("Create should create VM, but not VMI", func() {
-		externalMachine, err := defaultTestMachine(machineContext, fakeClient, fakeVMCommandExecutor, []byte{})
+		externalMachine, err := defaultTestMachine(machineContext, namespace, fakeClient, fakeVMCommandExecutor, []byte{})
 		Expect(err).NotTo(HaveOccurred())
 
 		// read the vm before creation
-		validateVMNotExist(fakeClient, machineContext)
+		validateVMNotExist(virtualMachine, fakeClient, machineContext)
 
 		err = externalMachine.Create(machineContext.Context)
 		Expect(err).NotTo(HaveOccurred())
 
 		// read the vm before creation
-		validateVMExist(fakeClient, machineContext)
+		validateVMExist(virtualMachine, fakeClient, machineContext)
 	})
 
 	It("Create should create VM if it doesn't exist", func() {
-		externalMachine, err := defaultTestMachine(machineContext, fakeClient, fakeVMCommandExecutor, []byte{})
+		externalMachine, err := defaultTestMachine(machineContext, namespace, fakeClient, fakeVMCommandExecutor, []byte{})
 		Expect(err).NotTo(HaveOccurred())
 
 		// read the vm before creation
-		validateVMNotExist(fakeClient, machineContext)
+		validateVMNotExist(virtualMachine, fakeClient, machineContext)
 
 		err = externalMachine.Create(machineContext.Context)
 		Expect(err).NotTo(HaveOccurred())
 
 		// read the new created vm
-		validateVMExist(fakeClient, machineContext)
+		validateVMExist(virtualMachine, fakeClient, machineContext)
+	})
+
+	It("Delete should be lenient if VM doesn't exist", func() {
+		externalMachine, err := defaultTestMachine(machineContext, namespace, fakeClient, fakeVMCommandExecutor, []byte{})
+		Expect(err).NotTo(HaveOccurred())
+		validateVMNotExist(virtualMachine, fakeClient, machineContext)
+
+		err = externalMachine.Delete()
+		Expect(err).NotTo(HaveOccurred())
 	})
 })
 
 var _ = Describe("With KubeVirt VM running", func() {
 	var machineContext *context.MachineContext
+	namespace := kubevirtMachine.Namespace
+	virtualMachineInstance := testing.NewVirtualMachineInstance(kubevirtMachine)
+	virtualMachine := testing.NewVirtualMachine(virtualMachineInstance)
+
+	BeforeEach(func() {
+		machineContext = &context.MachineContext{
+			Context:             gocontext.TODO(),
+			Cluster:             cluster,
+			KubevirtCluster:     kubevirtCluster,
+			Machine:             machine,
+			KubevirtMachine:     kubevirtMachine,
+			BootstrapDataSecret: bootstrapDataSecret,
+			Logger:              logger,
+		}
+
+		virtualMachineInstance.Status.Conditions = []kubevirtv1.VirtualMachineInstanceCondition{
+			{
+				Type:   kubevirtv1.VirtualMachineInstanceReady,
+				Status: corev1.ConditionTrue,
+			},
+		}
+		objects := []client.Object{
+			cluster,
+			kubevirtCluster,
+			machine,
+			kubevirtMachine,
+			virtualMachineInstance,
+			virtualMachine,
+		}
+
+		fakeClient = fake.NewClientBuilder().WithScheme(setupScheme()).WithObjects(objects...).Build()
+
+		fakeVMCommandExecutor = FakeVMCommandExecutor{true}
+	})
+
+	AfterEach(func() {})
+
+	It("NewMachine should have all client, machineContext and vmiInstance NOT nil", func() {
+		externalMachine, err := defaultTestMachine(machineContext, namespace, fakeClient, fakeVMCommandExecutor, []byte(sshKey))
+		Expect(err).NotTo(HaveOccurred())
+		Expect(externalMachine.client).ToNot(BeNil())
+		Expect(externalMachine.machineContext).To(Equal(machineContext))
+		Expect(externalMachine.vmiInstance).ToNot(BeNil())
+	})
+
+	It("Exists should return true", func() {
+		externalMachine, err := defaultTestMachine(machineContext, namespace, fakeClient, fakeVMCommandExecutor, []byte(sshKey))
+		Expect(err).NotTo(HaveOccurred())
+		Expect(externalMachine.Exists()).To(BeTrue())
+	})
+
+	It("Address should return non-empty IP", func() {
+		externalMachine, err := defaultTestMachine(machineContext, namespace, fakeClient, fakeVMCommandExecutor, []byte(sshKey))
+		Expect(err).NotTo(HaveOccurred())
+		Expect(externalMachine.Address()).To(Equal(virtualMachineInstance.Status.Interfaces[0].IP))
+	})
+
+	It("IsReady should return true", func() {
+		externalMachine, err := defaultTestMachine(machineContext, namespace, fakeClient, fakeVMCommandExecutor, []byte(sshKey))
+		Expect(err).NotTo(HaveOccurred())
+		Expect(externalMachine.IsReady()).To(BeTrue())
+	})
+
+	It("IsBootstrapped should return true", func() {
+		externalMachine, err := defaultTestMachine(machineContext, namespace, fakeClient, fakeVMCommandExecutor, []byte(sshKey))
+		Expect(err).NotTo(HaveOccurred())
+		Expect(externalMachine.IsBootstrapped()).To(BeTrue())
+	})
+
+	It("SupportsCheckingIsBootstrapped should return true", func() {
+		externalMachine, err := defaultTestMachine(machineContext, namespace, fakeClient, fakeVMCommandExecutor, []byte(sshKey))
+		Expect(err).NotTo(HaveOccurred())
+		Expect(externalMachine.SupportsCheckingIsBootstrapped()).To(BeTrue())
+	})
+
+	It("GenerateProviderID should succeed", func() {
+		expectedProviderId := fmt.Sprintf("kubevirt://%s", kubevirtMachineName)
+
+		externalMachine, err := defaultTestMachine(machineContext, namespace, fakeClient, fakeVMCommandExecutor, []byte(sshKey))
+		Expect(err).NotTo(HaveOccurred())
+		providerId, err := externalMachine.GenerateProviderID()
+		Expect(err).ToNot(HaveOccurred())
+		Expect(providerId).To(Equal(expectedProviderId))
+	})
+
+	It("Delete should succeed", func() {
+		externalMachine, err := defaultTestMachine(machineContext, namespace, fakeClient, fakeVMCommandExecutor, []byte{})
+		Expect(err).NotTo(HaveOccurred())
+		validateVMExist(virtualMachine, fakeClient, machineContext)
+
+		err = externalMachine.Delete()
+		Expect(err).NotTo(HaveOccurred())
+		validateVMNotExist(virtualMachine, fakeClient, machineContext)
+	})
+})
+
+var _ = Describe("util functions", func() {
+	var machineContext *context.MachineContext
+
+	BeforeEach(func() {
+		machineContext = &context.MachineContext{
+			Context:             gocontext.TODO(),
+			Cluster:             cluster,
+			KubevirtCluster:     kubevirtCluster,
+			Machine:             machine,
+			KubevirtMachine:     kubevirtMachine.DeepCopy(),
+			BootstrapDataSecret: bootstrapDataSecret,
+			Logger:              logger,
+		}
+	})
+
+	It("GenerateProviderID should succeed", func() {
+		dataVolumeTemplates := []kubevirtv1.DataVolumeTemplateSpec{
+			{
+				ObjectMeta: metav1.ObjectMeta{
+					Labels:      map[string]string{"my": "label"},
+					Annotations: map[string]string{"my": "annotation"},
+					Name:        "dv1",
+				},
+			},
+		}
+		volumes := []kubevirtv1.Volume{
+			{
+				Name: "test1",
+				VolumeSource: kubevirtv1.VolumeSource{
+					DataVolume: &kubevirtv1.DataVolumeSource{
+						Name: "dv1",
+					},
+				},
+			},
+		}
+
+		machineContext.KubevirtMachine.Spec.VirtualMachineTemplate.Spec.DataVolumeTemplates = dataVolumeTemplates
+		machineContext.KubevirtMachine.Spec.VirtualMachineTemplate.Spec.Template.Spec.Volumes = volumes
+
+		newVM := newVirtualMachineFromKubevirtMachine(machineContext, "default")
+
+		Expect(newVM.Spec.DataVolumeTemplates[0].ObjectMeta.Name).To(Equal(kubevirtMachineName + "-dv1"))
+		Expect(newVM.Spec.Template.Spec.Volumes[0].VolumeSource.DataVolume.Name).To(Equal(kubevirtMachineName + "-dv1"))
+	})
+})
+
+var _ = Describe("With KubeVirt VM running externally", func() {
+	var machineContext *context.MachineContext
+	namespace := "external"
+	virtualMachineInstance := testing.NewExternalVirtualMachineInstance(kubevirtMachine, namespace)
+	virtualMachine := testing.NewVirtualMachine(virtualMachineInstance)
 
 	BeforeEach(func() {
 		machineContext = &context.MachineContext{
@@ -203,7 +359,7 @@ var _ = Describe("With KubeVirt VM running", func() {
 	AfterEach(func() {})
 
 	It("NewMachine should have all client, machineContext and vmiInstance NOT nil", func() {
-		externalMachine, err := defaultTestMachine(machineContext, fakeClient, fakeVMCommandExecutor, []byte(sshKey))
+		externalMachine, err := defaultTestMachine(machineContext, namespace, fakeClient, fakeVMCommandExecutor, []byte(sshKey))
 		Expect(err).NotTo(HaveOccurred())
 		Expect(externalMachine.client).ToNot(BeNil())
 		Expect(externalMachine.machineContext).To(Equal(machineContext))
@@ -211,31 +367,31 @@ var _ = Describe("With KubeVirt VM running", func() {
 	})
 
 	It("Exists should return true", func() {
-		externalMachine, err := defaultTestMachine(machineContext, fakeClient, fakeVMCommandExecutor, []byte(sshKey))
+		externalMachine, err := defaultTestMachine(machineContext, namespace, fakeClient, fakeVMCommandExecutor, []byte(sshKey))
 		Expect(err).NotTo(HaveOccurred())
 		Expect(externalMachine.Exists()).To(BeTrue())
 	})
 
 	It("Address should return non-empty IP", func() {
-		externalMachine, err := defaultTestMachine(machineContext, fakeClient, fakeVMCommandExecutor, []byte(sshKey))
+		externalMachine, err := defaultTestMachine(machineContext, namespace, fakeClient, fakeVMCommandExecutor, []byte(sshKey))
 		Expect(err).NotTo(HaveOccurred())
 		Expect(externalMachine.Address()).To(Equal(virtualMachineInstance.Status.Interfaces[0].IP))
 	})
 
 	It("IsReady should return true", func() {
-		externalMachine, err := defaultTestMachine(machineContext, fakeClient, fakeVMCommandExecutor, []byte(sshKey))
+		externalMachine, err := defaultTestMachine(machineContext, namespace, fakeClient, fakeVMCommandExecutor, []byte(sshKey))
 		Expect(err).NotTo(HaveOccurred())
 		Expect(externalMachine.IsReady()).To(BeTrue())
 	})
 
 	It("IsBootstrapped should return true", func() {
-		externalMachine, err := defaultTestMachine(machineContext, fakeClient, fakeVMCommandExecutor, []byte(sshKey))
+		externalMachine, err := defaultTestMachine(machineContext, namespace, fakeClient, fakeVMCommandExecutor, []byte(sshKey))
 		Expect(err).NotTo(HaveOccurred())
 		Expect(externalMachine.IsBootstrapped()).To(BeTrue())
 	})
 
 	It("SupportsCheckingIsBootstrapped should return true", func() {
-		externalMachine, err := defaultTestMachine(machineContext, fakeClient, fakeVMCommandExecutor, []byte(sshKey))
+		externalMachine, err := defaultTestMachine(machineContext, namespace, fakeClient, fakeVMCommandExecutor, []byte(sshKey))
 		Expect(err).NotTo(HaveOccurred())
 		Expect(externalMachine.SupportsCheckingIsBootstrapped()).To(BeTrue())
 	})
@@ -243,13 +399,22 @@ var _ = Describe("With KubeVirt VM running", func() {
 	It("GenerateProviderID should succeed", func() {
 		expectedProviderId := fmt.Sprintf("kubevirt://%s", kubevirtMachineName)
 
-		externalMachine, err := defaultTestMachine(machineContext, fakeClient, fakeVMCommandExecutor, []byte(sshKey))
+		externalMachine, err := defaultTestMachine(machineContext, namespace, fakeClient, fakeVMCommandExecutor, []byte(sshKey))
 		Expect(err).NotTo(HaveOccurred())
 		providerId, err := externalMachine.GenerateProviderID()
 		Expect(err).ToNot(HaveOccurred())
 		Expect(providerId).To(Equal(expectedProviderId))
 	})
 
+	It("Delete should succeed", func() {
+		externalMachine, err := defaultTestMachine(machineContext, namespace, fakeClient, fakeVMCommandExecutor, []byte{})
+		Expect(err).NotTo(HaveOccurred())
+		validateVMExist(virtualMachine, fakeClient, machineContext)
+
+		err = externalMachine.Delete()
+		Expect(err).NotTo(HaveOccurred())
+		validateVMNotExist(virtualMachine, fakeClient, machineContext)
+	})
 })
 
 var _ = Describe("util functions", func() {
@@ -298,23 +463,23 @@ var _ = Describe("util functions", func() {
 	})
 })
 
-func validateVMNotExist(fakeClient client.Client, machineContext *context.MachineContext) {
+func validateVMNotExist(expected *kubevirtv1.VirtualMachine, fakeClient client.Client, machineContext *context.MachineContext) {
 	vm := &kubevirtv1.VirtualMachine{}
-	key := client.ObjectKey{Name: virtualMachineInstance.Name, Namespace: virtualMachineInstance.Namespace}
+	key := client.ObjectKey{Name: expected.Name, Namespace: expected.Namespace}
 
 	err := fakeClient.Get(machineContext.Context, key, vm)
 	ExpectWithOffset(1, err).To(HaveOccurred())
 	ExpectWithOffset(1, apierrors.IsNotFound(err)).To(BeTrue())
 }
 
-func validateVMExist(fakeClient client.Client, machineContext *context.MachineContext) {
+func validateVMExist(expected *kubevirtv1.VirtualMachine, fakeClient client.Client, machineContext *context.MachineContext) {
 	vm := &kubevirtv1.VirtualMachine{}
-	key := client.ObjectKey{Name: virtualMachineInstance.Name, Namespace: virtualMachineInstance.Namespace}
+	key := client.ObjectKey{Name: expected.Name, Namespace: expected.Namespace}
 
 	err := fakeClient.Get(machineContext.Context, key, vm)
 	ExpectWithOffset(1, err).ToNot(HaveOccurred())
-	Expect(vm.Name).To(Equal(virtualMachineInstance.Name))
-	Expect(vm.Namespace).To(Equal(virtualMachineInstance.Namespace))
+	Expect(vm.Name).To(Equal(expected.Name))
+	Expect(vm.Namespace).To(Equal(expected.Namespace))
 }
 
 func setupScheme() *runtime.Scheme {
@@ -354,9 +519,9 @@ func (e FakeVMCommandExecutor) ExecuteCommand(command string) (string, error) {
 	}
 }
 
-func defaultTestMachine(ctx *context.MachineContext, client client.Client, vmExecutor FakeVMCommandExecutor, sshPubKey []byte) (*Machine, error) {
+func defaultTestMachine(ctx *context.MachineContext, namespace string, client client.Client, vmExecutor FakeVMCommandExecutor, sshPubKey []byte) (*Machine, error) {
 
-	machine, err := NewMachine(ctx, client, ctx.Cluster.Namespace, &ssh.ClusterNodeSshKeys{PublicKey: sshPubKey})
+	machine, err := NewMachine(ctx, client, namespace, &ssh.ClusterNodeSshKeys{PublicKey: sshPubKey})
 
 	machine.getCommandExecutor = func(fake string, fakeKeys *ssh.ClusterNodeSshKeys) ssh.VMCommandExecutor {
 		return vmExecutor
diff --git a/pkg/testing/common.go b/pkg/testing/common.go
index c107b4f..692c05d 100644
--- a/pkg/testing/common.go
+++ b/pkg/testing/common.go
@@ -114,6 +114,26 @@ func NewVirtualMachineInstance(kubevirtMachine *infrav1.KubevirtMachine) *kubevi
 	}
 }
 
+// NewExternalVirtualMachineInstance instantiates a new external VirtualMachineInstance; i.e. one in a specified
+// namespace that might differ from the kubevirtMachine one.
+func NewExternalVirtualMachineInstance(kubevirtMachine *infrav1.KubevirtMachine, namespace string) *kubevirtv1.VirtualMachineInstance {
+	return &kubevirtv1.VirtualMachineInstance{
+		TypeMeta: metav1.TypeMeta{
+			Kind:       "VirtualMachineInstance",
+			APIVersion: "kubevirt.io/v1",
+		},
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      kubevirtMachine.Name,
+			Namespace: namespace,
+		},
+		Status: kubevirtv1.VirtualMachineInstanceStatus{
+			Interfaces: []kubevirtv1.VirtualMachineInstanceNetworkInterface{
+				{IP: "1.1.1.1"},
+			},
+		},
+	}
+}
+
 func NewVirtualMachine(vmi *kubevirtv1.VirtualMachineInstance) *kubevirtv1.VirtualMachine {
 	return &kubevirtv1.VirtualMachine{
 		TypeMeta: metav1.TypeMeta{