Repository: kubevirt/kubevirt
Pull request #14419: net, ctrl, vm: Consolidate hotplug / hotunplug logic
Author: login: orelmisan
Labels: name: size/L
name: sig/network
name: area/controller
name: release-note-none
description: Denotes a PR that doesn't merit a release note.
name: lgtm
description: Indicates that a PR is ready to be merged.
name: approved
description: Indicates a PR has been approved by an approver from all required OWNERS files.
name: dco-signoff: yes
description: Indicates the PR's author has DCO signed all their commits.
name: kind/cleanup
description: Categorizes issue or PR as related to cleaning up code, process, or technical debt.
name: sig/testing
Created: 2025-04-03T17:34:54Z | Merged: 2025-05-20T22:03:27Z

<!--  Thanks for sending a pull request!  Here are some tips for you:
1. Consider creating this PR as draft: https://github.com/kubevirt/kubevirt/blob/main/CONTRIBUTING.md#consider-opening-your-pull-request-as-draft
2. Follow the instructions for writing a release note from k8s: https://git.k8s.io/community/contributors/guide/release-notes.md
-->

### What this PR does
This is a follow-up to https://github.com/kubevirt/kubevirt/pull/13901 and https://github.com/kubevirt/kubevirt/pull/13844.

Currently, there are two unit test files testing the NIC hotplug / hotunplug logic:
- pkg/network/controllers/vm_test.go
- pkg/network/controllers/hotplug_test.go

Move tests testing lower level logic from `hotplug_test.go` to the higher level `vm_test.go` file that tests the network VM controller as a whole.

Move `ApplyDynamicIfaceRequestOnVMI` and `ClearDetachedInterfaces` closer to their only caller (network VM controller).

<!-- (optional, in `fixes #<issue number>(, fixes #<issue_number>, ...)` format, will close the issue(s) when PR gets merged)*: -->
Fixes #

### Why we need it and why it was done in this way
The following tradeoffs were made:

The following alternatives were considered:

Links to places where the discussion took place: <!-- optional: slack, other GH issue, mailinglist, ... -->

### Special notes for your reviewer

<!-- optional -->

### Checklist

This checklist is not enforcing, but it's a reminder of items that could be relevant to every PR.
Approvers are expected to review this list.

- [ ] Design: A [design document](https://github.com/kubevirt/community/tree/main/design-proposals) was considered and is present (link) or not required
- [ ] PR: The PR description is expressive enough and will help future contributors
- [ ] Code: [Write code that humans can understand](https://en.wikiquote.org/wiki/Martin_Fowler#code-for-humans) and [Keep it simple](https://en.wikipedia.org/wiki/KISS_principle)
- [ ] Refactor: You have [left the code cleaner than you found it (Boy Scout Rule)](https://learning.oreilly.com/library/view/97-things-every/9780596809515/ch08.html)
- [ ] Upgrade: Impact of this change on upgrade flows was considered and addressed if required
- [ ] Testing: New code requires [new unit tests](https://github.com/kubevirt/kubevirt/blob/main/docs/reviewer-guide.md#when-is-a-pr-good-enough). New features and bug fixes require at least on e2e test
- [ ] Documentation: A [user-guide update](https://github.com/kubevirt/user-guide/) was considered and is present (link) or not required. You want a user-guide update if it's a user facing feature / API change.
- [ ] Community: Announcement to [kubevirt-dev](https://groups.google.com/g/kubevirt-dev/) was considered

### Release note
<!--  Write your release note:
1. Enter your extended release note in the below block. If the PR requires additional action from users switching to the new release, include the string "action required".
2. If no release note is required, just write "NONE".
-->
```release-note
NONE
```




Diff:
diff --git a/pkg/network/controllers/BUILD.bazel b/pkg/network/controllers/BUILD.bazel
index 49b5c574761f..326e63092478 100644
--- a/pkg/network/controllers/BUILD.bazel
+++ b/pkg/network/controllers/BUILD.bazel
@@ -3,7 +3,6 @@ load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test")
 go_library(
     name = "go_default_library",
     srcs = [
-        "hotplug.go",
         "vm.go",
         "vmi.go",
     ],
@@ -28,7 +27,6 @@ go_test(
     name = "go_default_test",
     srcs = [
         "controllers_suite_test.go",
-        "hotplug_test.go",
         "vm_test.go",
         "vmi_test.go",
     ],
diff --git a/pkg/network/controllers/hotplug.go b/pkg/network/controllers/hotplug.go
deleted file mode 100644
index c9ecc7568bc5..000000000000
--- a/pkg/network/controllers/hotplug.go
+++ /dev/null
@@ -1,76 +0,0 @@
-/*
- * 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 controllers
-
-import (
-	v1 "kubevirt.io/api/core/v1"
-
-	"kubevirt.io/kubevirt/pkg/network/vmispec"
-)
-
-func ApplyDynamicIfaceRequestOnVMI(
-	vm *v1.VirtualMachine,
-	vmi *v1.VirtualMachineInstance,
-	hasOrdinalIfaces bool,
-) *v1.VirtualMachineInstanceSpec {
-	vmiSpecCopy := vmi.Spec.DeepCopy()
-	vmiIndexedInterfaces := vmispec.IndexInterfaceSpecByName(vmiSpecCopy.Domain.Devices.Interfaces)
-	vmIndexedNetworks := vmispec.IndexNetworkSpecByName(vm.Spec.Template.Spec.Networks)
-	for _, vmIface := range vm.Spec.Template.Spec.Domain.Devices.Interfaces {
-		vmiIfaceCopy, existsInVMISpec := vmiIndexedInterfaces[vmIface.Name]
-
-		shouldHotplugIface := !existsInVMISpec &&
-			vmIface.State != v1.InterfaceStateAbsent &&
-			(vmIface.InterfaceBindingMethod.Bridge != nil || vmIface.InterfaceBindingMethod.SRIOV != nil)
-
-		shouldUpdateExistingIfaceState := existsInVMISpec &&
-			vmIface.State != vmiIfaceCopy.State &&
-			vmiIfaceCopy.State != v1.InterfaceStateAbsent
-
-		switch {
-		case shouldHotplugIface:
-			vmiSpecCopy.Networks = append(vmiSpecCopy.Networks, vmIndexedNetworks[vmIface.Name])
-			vmiSpecCopy.Domain.Devices.Interfaces = append(vmiSpecCopy.Domain.Devices.Interfaces, vmIface)
-
-		case shouldUpdateExistingIfaceState:
-			if !(hasOrdinalIfaces && vmIface.State == v1.InterfaceStateAbsent) {
-				vmiIface := vmispec.LookupInterfaceByName(vmiSpecCopy.Domain.Devices.Interfaces, vmIface.Name)
-				vmiIface.State = vmIface.State
-			}
-		}
-	}
-	return vmiSpecCopy
-}
-
-func ClearDetachedInterfaces(
-	specIfaces []v1.Interface,
-	specNets []v1.Network,
-	statusIfaces map[string]v1.VirtualMachineInstanceNetworkInterface,
-) ([]v1.Interface, []v1.Network) {
-	var ifaces []v1.Interface
-	for _, iface := range specIfaces {
-		if _, existInStatus := statusIfaces[iface.Name]; (existInStatus && iface.State == v1.InterfaceStateAbsent) ||
-			iface.State != v1.InterfaceStateAbsent {
-			ifaces = append(ifaces, iface)
-		}
-	}
-
-	return ifaces, vmispec.FilterNetworksByInterfaces(specNets, ifaces)
-}
diff --git a/pkg/network/controllers/hotplug_test.go b/pkg/network/controllers/hotplug_test.go
deleted file mode 100644
index 87c34e01d85a..000000000000
--- a/pkg/network/controllers/hotplug_test.go
+++ /dev/null
@@ -1,141 +0,0 @@
-/*
- * 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 controllers_test
-
-import (
-	. "github.com/onsi/ginkgo/v2"
-	. "github.com/onsi/gomega"
-
-	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
-
-	v1 "kubevirt.io/api/core/v1"
-
-	"kubevirt.io/kubevirt/pkg/libvmi"
-	"kubevirt.io/kubevirt/pkg/network/controllers"
-	"kubevirt.io/kubevirt/pkg/network/vmispec"
-)
-
-var _ = Describe("Network interface hot{un}plug", func() {
-	const (
-		testNetworkName1 = "testnet1"
-		testNetworkName2 = "testnet2"
-
-		ordinal = true
-	)
-
-	DescribeTable("apply dynamic interface request on VMI",
-		func(vmiForVM, currentVMI, expectedVMI *v1.VirtualMachineInstance, hasOrdinalIfaces bool) {
-			vm := virtualMachineFromVMI(currentVMI.Name, vmiForVM)
-			updatedVMI := controllers.ApplyDynamicIfaceRequestOnVMI(vm, currentVMI, hasOrdinalIfaces)
-			Expect(updatedVMI.Networks).To(Equal(expectedVMI.Spec.Networks))
-			Expect(updatedVMI.Domain.Devices.Interfaces).To(Equal(expectedVMI.Spec.Domain.Devices.Interfaces))
-		},
-		Entry("when an interface has to be hotplugged but it is absent",
-			libvmi.New(
-				libvmi.WithInterface(bridgeAbsentInterface(testNetworkName1)),
-				libvmi.WithNetwork(&v1.Network{Name: testNetworkName1}),
-			),
-			libvmi.New(),
-			libvmi.New(),
-			!ordinal),
-		Entry("when one interface has to be plugged and other hotunplugged",
-			libvmi.New(
-				libvmi.WithInterface(bridgeAbsentInterface(testNetworkName1)),
-				libvmi.WithNetwork(&v1.Network{Name: testNetworkName1}),
-				libvmi.WithInterface(bridgeInterface(testNetworkName2)),
-				libvmi.WithNetwork(&v1.Network{Name: testNetworkName2}),
-			),
-			libvmi.New(
-				libvmi.WithInterface(bridgeInterface(testNetworkName1)),
-				libvmi.WithNetwork(&v1.Network{Name: testNetworkName1}),
-			),
-			libvmi.New(
-				libvmi.WithInterface(bridgeAbsentInterface(testNetworkName1)),
-				libvmi.WithNetwork(&v1.Network{Name: testNetworkName1}),
-				libvmi.WithInterface(bridgeInterface(testNetworkName2)),
-				libvmi.WithNetwork(&v1.Network{Name: testNetworkName2}),
-			),
-			!ordinal),
-	)
-
-	DescribeTable("spec interfaces",
-		func(specIfaces []v1.Interface, statusIfaces []v1.VirtualMachineInstanceNetworkInterface,
-			expectedInterfaces []v1.Interface, expectedNetworks []v1.Network,
-		) {
-			var testNetworks []v1.Network
-			for _, iface := range specIfaces {
-				testNetworks = append(testNetworks, v1.Network{Name: iface.Name})
-			}
-			testStatusIfaces := vmispec.IndexInterfaceStatusByName(statusIfaces,
-				func(i v1.VirtualMachineInstanceNetworkInterface) bool { return true })
-
-			ifaces, networks := controllers.ClearDetachedInterfaces(specIfaces, testNetworks, testStatusIfaces)
-
-			Expect(ifaces).To(Equal(expectedInterfaces))
-			Expect(networks).To(Equal(expectedNetworks))
-		},
-		Entry("should remain, given non-absent interfaces, and no associated status ifaces (i.e.: plug pending)",
-			[]v1.Interface{{Name: "blue"}, {Name: "red"}},
-			[]v1.VirtualMachineInstanceNetworkInterface{},
-			[]v1.Interface{{Name: "blue"}, {Name: "red"}},
-			[]v1.Network{{Name: "blue"}, {Name: "red"}},
-		),
-		Entry("should remain, given non-absent interfaces, and associated status ifaces (i.e.: plugged iface)",
-			[]v1.Interface{{Name: "blue"}, {Name: "red"}},
-			[]v1.VirtualMachineInstanceNetworkInterface{{Name: "blue"}, {Name: "red"}},
-			[]v1.Interface{{Name: "blue"}, {Name: "red"}},
-			[]v1.Network{{Name: "blue"}, {Name: "red"}},
-		),
-		Entry("should remain, given status iface and no associated iface in spec",
-			[]v1.Interface{{Name: "blue"}},
-			[]v1.VirtualMachineInstanceNetworkInterface{{Name: "RED"}},
-			[]v1.Interface{{Name: "blue"}},
-			[]v1.Network{{Name: "blue"}},
-		),
-	)
-})
-
-func bridgeInterface(name string) v1.Interface {
-	return v1.Interface{Name: name, InterfaceBindingMethod: v1.InterfaceBindingMethod{Bridge: &v1.InterfaceBridge{}}}
-}
-
-func bridgeAbsentInterface(name string) v1.Interface {
-	iface := bridgeInterface(name)
-	iface.State = v1.InterfaceStateAbsent
-	return iface
-}
-
-func virtualMachineFromVMI(name string, vmi *v1.VirtualMachineInstance) *v1.VirtualMachine {
-	started := true
-	vm := &v1.VirtualMachine{
-		ObjectMeta: metav1.ObjectMeta{Name: name, Namespace: vmi.ObjectMeta.Namespace, ResourceVersion: "1", UID: "vm-uid"},
-		Spec: v1.VirtualMachineSpec{
-			Running: &started,
-			Template: &v1.VirtualMachineInstanceTemplateSpec{
-				ObjectMeta: metav1.ObjectMeta{
-					Name:   vmi.ObjectMeta.Name,
-					Labels: vmi.ObjectMeta.Labels,
-				},
-				Spec: vmi.Spec,
-			},
-		},
-	}
-	return vm
-}
diff --git a/pkg/network/controllers/vm.go b/pkg/network/controllers/vm.go
index 5a41f9674d50..d8fdc60934be 100644
--- a/pkg/network/controllers/vm.go
+++ b/pkg/network/controllers/vm.go
@@ -79,19 +79,19 @@ func (v *VMController) Sync(vm *v1.VirtualMachine, vmi *v1.VirtualMachineInstanc
 		func(ifaceStatus v1.VirtualMachineInstanceNetworkInterface) bool { return true },
 	)
 
-	ifaces, networks := ClearDetachedInterfaces(
+	ifaces, networks := clearDetachedInterfaces(
 		vmCopy.Spec.Template.Spec.Domain.Devices.Interfaces,
 		vmCopy.Spec.Template.Spec.Networks, indexedStatusIfaces,
 	)
 	vmCopy.Spec.Template.Spec.Domain.Devices.Interfaces = ifaces
 	vmCopy.Spec.Template.Spec.Networks = networks
 
-	ifaces, networks = ClearDetachedInterfaces(vmiCopy.Spec.Domain.Devices.Interfaces, vmiCopy.Spec.Networks, indexedStatusIfaces)
+	ifaces, networks = clearDetachedInterfaces(vmiCopy.Spec.Domain.Devices.Interfaces, vmiCopy.Spec.Networks, indexedStatusIfaces)
 	vmiCopy.Spec.Domain.Devices.Interfaces = ifaces
 	vmiCopy.Spec.Networks = networks
 
 	hasOrdinalIfaces := namescheme.HasOrdinalSecondaryIfaces(vmi.Spec.Networks, vmi.Status.Interfaces)
-	updatedVmiSpec := ApplyDynamicIfaceRequestOnVMI(vmCopy, vmiCopy, hasOrdinalIfaces)
+	updatedVmiSpec := applyDynamicIfaceRequestOnVMI(vmCopy, vmiCopy, hasOrdinalIfaces)
 	vmiCopy.Spec = *updatedVmiSpec
 
 	if err := v.vmiInterfacesPatch(&vmiCopy.Spec, vmi); err != nil {
@@ -123,3 +123,53 @@ func (v *VMController) vmiInterfacesPatch(newVmiSpec *v1.VirtualMachineInstanceS
 
 	return err
 }
+
+func applyDynamicIfaceRequestOnVMI(
+	vm *v1.VirtualMachine,
+	vmi *v1.VirtualMachineInstance,
+	hasOrdinalIfaces bool,
+) *v1.VirtualMachineInstanceSpec {
+	vmiSpecCopy := vmi.Spec.DeepCopy()
+	vmiIndexedInterfaces := vmispec.IndexInterfaceSpecByName(vmiSpecCopy.Domain.Devices.Interfaces)
+	vmIndexedNetworks := vmispec.IndexNetworkSpecByName(vm.Spec.Template.Spec.Networks)
+	for _, vmIface := range vm.Spec.Template.Spec.Domain.Devices.Interfaces {
+		vmiIfaceCopy, existsInVMISpec := vmiIndexedInterfaces[vmIface.Name]
+
+		shouldHotplugIface := !existsInVMISpec &&
+			vmIface.State != v1.InterfaceStateAbsent &&
+			(vmIface.InterfaceBindingMethod.Bridge != nil || vmIface.InterfaceBindingMethod.SRIOV != nil)
+
+		shouldUpdateExistingIfaceState := existsInVMISpec &&
+			vmIface.State != vmiIfaceCopy.State &&
+			vmiIfaceCopy.State != v1.InterfaceStateAbsent
+
+		switch {
+		case shouldHotplugIface:
+			vmiSpecCopy.Networks = append(vmiSpecCopy.Networks, vmIndexedNetworks[vmIface.Name])
+			vmiSpecCopy.Domain.Devices.Interfaces = append(vmiSpecCopy.Domain.Devices.Interfaces, vmIface)
+
+		case shouldUpdateExistingIfaceState:
+			if !(hasOrdinalIfaces && vmIface.State == v1.InterfaceStateAbsent) {
+				vmiIface := vmispec.LookupInterfaceByName(vmiSpecCopy.Domain.Devices.Interfaces, vmIface.Name)
+				vmiIface.State = vmIface.State
+			}
+		}
+	}
+	return vmiSpecCopy
+}
+
+func clearDetachedInterfaces(
+	specIfaces []v1.Interface,
+	specNets []v1.Network,
+	statusIfaces map[string]v1.VirtualMachineInstanceNetworkInterface,
+) ([]v1.Interface, []v1.Network) {
+	var ifaces []v1.Interface
+	for _, iface := range specIfaces {
+		if _, existInStatus := statusIfaces[iface.Name]; (existInStatus && iface.State == v1.InterfaceStateAbsent) ||
+			iface.State != v1.InterfaceStateAbsent {
+			ifaces = append(ifaces, iface)
+		}
+	}
+
+	return ifaces, vmispec.FilterNetworksByInterfaces(specNets, ifaces)
+}
diff --git a/pkg/network/controllers/vm_test.go b/pkg/network/controllers/vm_test.go
index 249d606bf6c8..b1da1cef927e 100644
--- a/pkg/network/controllers/vm_test.go
+++ b/pkg/network/controllers/vm_test.go
@@ -74,6 +74,19 @@ var _ = Describe("VM Network Controller", func() {
 				libvmi.WithNetwork(libvmi.MultusNetwork(secondaryNetName, nadName)),
 			),
 		),
+		Entry("there is an interface status that does not match a spec interface",
+			libvmi.NewVirtualMachine(libvmi.New(
+				libvmi.WithInterface(libvmi.InterfaceDeviceWithMasqueradeBinding()),
+				libvmi.WithNetwork(v1.DefaultPodNetwork()),
+			)),
+			libvmi.New(
+				libvmi.WithInterface(libvmi.InterfaceDeviceWithMasqueradeBinding()),
+				libvmi.WithNetwork(v1.DefaultPodNetwork()),
+				libvmistatus.WithStatus(libvmistatus.New(
+					libvmistatus.WithInterfaceStatus(v1.VirtualMachineInstanceNetworkInterface{Name: "DEFAULT"}),
+				)),
+			),
+		),
 	)
 
 	It("sync fails when VMI patch returns an error", func() {
@@ -156,6 +169,46 @@ var _ = Describe("VM Network Controller", func() {
 		}),
 	)
 
+	It("sync does not hotplug a new absent interface", func() {
+		clientset := fake.NewSimpleClientset()
+		c := controllers.NewVMController(clientset)
+		vmi := libvmi.New(
+			libvmi.WithInterface(libvmi.InterfaceDeviceWithMasqueradeBinding()),
+			libvmi.WithNetwork(v1.DefaultPodNetwork()),
+		)
+
+		originalVMI := vmi.DeepCopy()
+		vm := libvmi.NewVirtualMachine(originalVMI)
+
+		absentIfaceToPlug := v1.Interface{
+			Name:                   "absentIface",
+			State:                  v1.InterfaceStateAbsent,
+			InterfaceBindingMethod: v1.InterfaceBindingMethod{Bridge: &v1.InterfaceBridge{}},
+		}
+
+		vm = plugNetworkInterface(vm, absentIfaceToPlug)
+
+		// Simulate the existence of the VMI on the server (to allow the Sync to patch it).
+		_, err := clientset.KubevirtV1().VirtualMachineInstances(vmi.Namespace).Create(context.Background(), vmi, k8smetav1.CreateOptions{})
+		Expect(err).NotTo(HaveOccurred())
+
+		updatedVM, err := c.Sync(vm, vmi)
+		Expect(err).NotTo(HaveOccurred())
+
+		// Assert that the hotplugged interface and its matching network were cleared
+		Expect(updatedVM.Spec.Template.Spec.Networks).To(Equal(originalVMI.Spec.Networks))
+		Expect(updatedVM.Spec.Template.Spec.Domain.Devices.Interfaces).To(Equal(originalVMI.Spec.Domain.Devices.Interfaces))
+
+		// Assert that the hotplug haven't reached the VMI
+		updatedVMI, err := clientset.KubevirtV1().
+			VirtualMachineInstances(vmi.Namespace).
+			Get(context.Background(), vmi.Name, k8smetav1.GetOptions{})
+		Expect(err).NotTo(HaveOccurred())
+
+		Expect(updatedVMI.Spec.Networks).To(Equal(originalVMI.Spec.Networks))
+		Expect(updatedVMI.Spec.Domain.Devices.Interfaces).To(Equal(originalVMI.Spec.Domain.Devices.Interfaces))
+	})
+
 	DescribeTable("sync succeeds to mark an existing interface for hotunplug", func(currentIfaceState v1.InterfaceState) {
 		clientset := fake.NewSimpleClientset()
 		c := controllers.NewVMController(clientset)
@@ -469,6 +522,70 @@ var _ = Describe("VM Network Controller", func() {
 		Expect(iface).NotTo(BeNil())
 		Expect(iface.State).NotTo(Equal(v1.InterfaceStateAbsent))
 	})
+
+	It("Sync successfully performs hotplug and hotunplug at the same time", func() {
+		const (
+			netToAttachName = "attach-me"
+
+			netToDetachName    = "detach-me"
+			netToDetachNADName = "detach-me-nad"
+		)
+
+		clientset := fake.NewSimpleClientset()
+		c := controllers.NewVMController(clientset)
+
+		multusAndDomainInfoSource := vmispec.NewInfoSource(vmispec.InfoSourceMultusStatus, vmispec.InfoSourceDomain)
+
+		ifaceToDetach := libvmi.InterfaceDeviceWithBridgeBinding(netToDetachName)
+		netToDetach := libvmi.MultusNetwork(netToDetachName, netToDetachNADName)
+
+		vmi := libvmi.New(
+			libvmi.WithInterface(libvmi.InterfaceDeviceWithMasqueradeBinding()),
+			libvmi.WithInterface(ifaceToDetach),
+			libvmi.WithNetwork(v1.DefaultPodNetwork()),
+			libvmi.WithNetwork(netToDetach),
+			libvmistatus.WithStatus(
+				libvmistatus.New(
+					libvmistatus.WithInterfaceStatus(v1.VirtualMachineInstanceNetworkInterface{
+						Name:             defaultNetName,
+						PodInterfaceName: namescheme.PrimaryPodInterfaceName,
+						InfoSource:       vmispec.InfoSourceDomain,
+					}),
+					libvmistatus.WithInterfaceStatus(v1.VirtualMachineInstanceNetworkInterface{
+						Name:             netToDetachName,
+						PodInterfaceName: namescheme.GenerateHashedInterfaceName(netToDetachName),
+						InfoSource:       multusAndDomainInfoSource,
+					}),
+				),
+			),
+		)
+
+		vm := libvmi.NewVirtualMachine(vmi.DeepCopy())
+
+		// Mark the secondary interface for hotunplug
+		vm.Spec.Template.Spec.Domain.Devices.Interfaces[1].State = v1.InterfaceStateAbsent
+
+		vm = plugNetworkInterface(vm, libvmi.InterfaceDeviceWithBridgeBinding(netToAttachName))
+
+		// Simulate the existence of the VMI on the server (to allow the Sync to patch it).
+		_, err := clientset.KubevirtV1().VirtualMachineInstances(vmi.Namespace).Create(context.Background(), vmi, k8smetav1.CreateOptions{})
+		Expect(err).NotTo(HaveOccurred())
+
+		originalVM := vm.DeepCopy()
+		updatedVM, err := c.Sync(vm, vmi)
+		Expect(err).NotTo(HaveOccurred())
+
+		Expect(updatedVM).To(Equal(originalVM))
+
+		// Assert that hotplug and hotunplug had reached the VMI
+		updatedVMI, err := clientset.KubevirtV1().
+			VirtualMachineInstances(vmi.Namespace).
+			Get(context.Background(), vmi.Name, k8smetav1.GetOptions{})
+		Expect(err).NotTo(HaveOccurred())
+
+		Expect(updatedVMI.Spec.Networks).To(Equal(originalVM.Spec.Template.Spec.Networks))
+		Expect(updatedVMI.Spec.Domain.Devices.Interfaces).To(Equal(originalVM.Spec.Template.Spec.Domain.Devices.Interfaces))
+	})
 })
 
 type syncError interface {


Reviews:
user: login: EdDev
body: >  net, ctrl: Mv ClearDetachedInterfaces
> 
> This function is only used by the network VM controller.
> Move it closer to its caller.
> 
> Its unit tests are covered by the network VM controller's unit tests[1].
> 
> [1] https://github.com/kubevirt/kubevirt/blob/c6aeb68d1794b71026236f726c15aa9550cc6d64/pkg/network/controllers/vm_test.go#L111

Please clarify which test is covering this scenario:
> "should remain, given status iface and no associated iface in spec"


state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-06T09:00:05Z
user: login: RamLavi
body: Looks good overall, added some comments
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-07T08:15:49Z
user: login: orelmisan
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-21T07:36:27Z
user: login: orelmisan
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-21T07:40:12Z
user: login: orelmisan
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-21T07:40:56Z
user: login: EdDev
body: > The "should remain, given status iface and no associated iface in spec"
case's title is misleading, it actually tests that a spec interface will
not be cleared in case there is no matching interface status.
This scenario is covered by the hotplug scenario [1] as well.

I do not see what is misleading about it.
Given there is an interface reported in the status but not in the spec, it should do nothing.
The test describes it correctly, the implementation used it mixed with another scenario which may caused this confusion.

Still, the coverage of this case needs to be shown or explained why not.
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-21T18:04:36Z
user: login: ormergi
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-27T08:56:11Z
user: login: orelmisan
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-27T09:01:11Z
user: login: ormergi
body: Hi, thanks for this clean up, I like the idea hotplug/unplug is being tested by a higher abstraction layer.

One comment about the new tests, it seems the test calls the network controller Sync "statically", instead of having the controller actually running as we would with a real env.
This approach may result in some side-effects not being detected by the tests, vs. having a the controller actually running as it would in a living env.
Although I do understand changing the tests to work with a running controller require additional work. Pleas consider that for following PRs.
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-27T09:29:50Z
user: login: orelmisan
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-18T12:39:44Z
user: login: orelmisan
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-18T12:40:26Z
user: login: orelmisan
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-18T12:46:21Z
user: login: EdDev
body: Thanks!

/approve
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-18T14:05:50Z
user: login: oshoval
body: Thanks


state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-20T12:10:33Z
user: login: orelmisan
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-20T12:12:59Z
user: login: orelmisan
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-20T12:14:49Z
user: login: oshoval
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-20T12:16:44Z
user: login: oshoval
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-20T12:17:32Z
user: login: orelmisan
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-20T12:21:18Z
user: login: oshoval
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-20T12:22:38Z
user: login: orelmisan
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-20T12:35:22Z
user: login: oshoval
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-20T12:38:07Z
user: login: orelmisan
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-20T12:38:46Z
user: login: orelmisan
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-20T12:39:40Z
user: login: oshoval
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-20T12:41:01Z
user: login: oshoval
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-20T12:50:16Z
user: login: nirdothan
body: Thank you @orelmisan. Please see my comments below. 
state: APPROVED
author_association: MEMBER
submitted_at: 2025-05-20T12:59:05Z
user: login: orelmisan
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-20T13:00:53Z
user: login: orelmisan
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-20T13:02:04Z
user: login: orelmisan
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-20T15:10:39Z
user: login: orelmisan
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-20T15:11:05Z
user: login: orelmisan
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-20T15:12:03Z
user: login: orelmisan
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-20T15:13:01Z
user: login: oshoval
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-20T15:14:20Z

Review comments:
diff_hunk: @@ -156,6 +156,46 @@ var _ = Describe("VM Network Controller", func() {
 		}),
 	)
 
+	It("sync does not hotplug a new absent interface", func() {
+		clientset := fake.NewSimpleClientset()
path: pkg/network/controllers/vm_test.go
user: login: EdDev
body: Not directly related to this change: We need to remove this boilerplate preparations from the test body and move it to a setup fixture or/and helper. When reading the tests, these lines are unfocusing the reader from the main test logic.
created_at: 2025-04-06T07:21:46Z
author_association: MEMBER
diff_hunk: @@ -156,6 +156,46 @@ var _ = Describe("VM Network Controller", func() {
 		}),
 	)
 
+	It("sync does not hotplug a new absent interface", func() {
+		clientset := fake.NewSimpleClientset()
+		c := controllers.NewVMController(clientset)
+		vmi := libvmi.New(
+			libvmi.WithInterface(libvmi.InterfaceDeviceWithMasqueradeBinding()),
+			libvmi.WithNetwork(v1.DefaultPodNetwork()),
+		)
+
+		originalVMI := vmi.DeepCopy()
+		vm := libvmi.NewVirtualMachine(originalVMI)
+
+		absentIfaceToPlug := v1.Interface{
+			Name:                   "absentIface",
+			State:                  v1.InterfaceStateAbsent,
+			InterfaceBindingMethod: v1.InterfaceBindingMethod{Bridge: &v1.InterfaceBridge{}},
+		}
+
+		vm = plugNetworkInterface(vm, absentIfaceToPlug)
+
+		// Simulate the existence of the VMI on the server (to allow the Sync to patch it).
path: pkg/network/controllers/vm_test.go
user: login: RamLavi
body: general comment regarding the comments on this test: `//...` comments are doomed to get stale. 
Please consider removing when possible, or at least switch to "By" so they are exposed and document the test.
created_at: 2025-04-07T07:56:03Z
author_association: CONTRIBUTOR
diff_hunk: @@ -509,6 +509,70 @@ var _ = Describe("VM Network Controller", func() {
 		Expect(iface).NotTo(BeNil())
 		Expect(iface.State).NotTo(Equal(v1.InterfaceStateAbsent))
 	})
+
+	It("Sync successfully performs hotplug and hotunplug at the same time", func() {
+		const (
+			netToAttachName = "attach-me"
+
+			netToDetachName    = "detach-me"
+			netToDetachNADName = "detach-me-nad"
+		)
+
+		clientset := fake.NewSimpleClientset()
+		c := controllers.NewVMController(clientset)
+
+		multusAndDomainInfoSource := vmispec.NewInfoSource(vmispec.InfoSourceMultusStatus, vmispec.InfoSourceDomain)
+
+		ifaceToDetach := libvmi.InterfaceDeviceWithBridgeBinding(netToDetachName)
+		netToDetach := libvmi.MultusNetwork(netToDetachName, netToDetachNADName)
+
+		vmi := libvmi.New(
+			libvmi.WithInterface(libvmi.InterfaceDeviceWithMasqueradeBinding()),
+			libvmi.WithInterface(ifaceToDetach),
+			libvmi.WithNetwork(v1.DefaultPodNetwork()),
+			libvmi.WithNetwork(netToDetach),
+			libvmistatus.WithStatus(
path: pkg/network/controllers/vm_test.go
user: login: RamLavi
body: this was not in the original test, can you explain why it is needed?
created_at: 2025-04-07T08:10:59Z
author_association: CONTRIBUTOR
diff_hunk: @@ -156,6 +156,46 @@ var _ = Describe("VM Network Controller", func() {
 		}),
 	)
 
+	It("sync does not hotplug a new absent interface", func() {
+		clientset := fake.NewSimpleClientset()
+		c := controllers.NewVMController(clientset)
+		vmi := libvmi.New(
+			libvmi.WithInterface(libvmi.InterfaceDeviceWithMasqueradeBinding()),
+			libvmi.WithNetwork(v1.DefaultPodNetwork()),
+		)
+
+		originalVMI := vmi.DeepCopy()
+		vm := libvmi.NewVirtualMachine(originalVMI)
+
+		absentIfaceToPlug := v1.Interface{
+			Name:                   "absentIface",
+			State:                  v1.InterfaceStateAbsent,
+			InterfaceBindingMethod: v1.InterfaceBindingMethod{Bridge: &v1.InterfaceBridge{}},
+		}
+
+		vm = plugNetworkInterface(vm, absentIfaceToPlug)
+
+		// Simulate the existence of the VMI on the server (to allow the Sync to patch it).
path: pkg/network/controllers/vm_test.go
user: login: orelmisan
body: > general comment regarding the comments on this test: //... comments are doomed to get stale.

I tend to agree and suggest that we will remove them all in a follow up.

> Please consider removing when possible, or at least switch to "By" so they are exposed and document the test.

Could you please elaborate how is a `By` stanza different from a comment in a unit test?


created_at: 2025-04-21T07:36:27Z
author_association: MEMBER
diff_hunk: @@ -509,6 +509,70 @@ var _ = Describe("VM Network Controller", func() {
 		Expect(iface).NotTo(BeNil())
 		Expect(iface.State).NotTo(Equal(v1.InterfaceStateAbsent))
 	})
+
+	It("Sync successfully performs hotplug and hotunplug at the same time", func() {
+		const (
+			netToAttachName = "attach-me"
+
+			netToDetachName    = "detach-me"
+			netToDetachNADName = "detach-me-nad"
+		)
+
+		clientset := fake.NewSimpleClientset()
+		c := controllers.NewVMController(clientset)
+
+		multusAndDomainInfoSource := vmispec.NewInfoSource(vmispec.InfoSourceMultusStatus, vmispec.InfoSourceDomain)
+
+		ifaceToDetach := libvmi.InterfaceDeviceWithBridgeBinding(netToDetachName)
+		netToDetach := libvmi.MultusNetwork(netToDetachName, netToDetachNADName)
+
+		vmi := libvmi.New(
+			libvmi.WithInterface(libvmi.InterfaceDeviceWithMasqueradeBinding()),
+			libvmi.WithInterface(ifaceToDetach),
+			libvmi.WithNetwork(v1.DefaultPodNetwork()),
+			libvmi.WithNetwork(netToDetach),
+			libvmistatus.WithStatus(
path: pkg/network/controllers/vm_test.go
user: login: orelmisan
body: The original test only tested a small fraction of the logic.
The proposed test, tests the whole VM controller's logic in the scenario when one interface is hotplugged and another is hotunplugged.
In the test, we hotunplug a device that is currently plugged (has an interface status with certain info sources).
created_at: 2025-04-21T07:40:12Z
author_association: MEMBER
diff_hunk: @@ -156,6 +156,46 @@ var _ = Describe("VM Network Controller", func() {
 		}),
 	)
 
+	It("sync does not hotplug a new absent interface", func() {
+		clientset := fake.NewSimpleClientset()
path: pkg/network/controllers/vm_test.go
user: login: orelmisan
body: Agreed.
created_at: 2025-04-21T07:40:56Z
author_association: MEMBER
diff_hunk: @@ -156,6 +156,46 @@ var _ = Describe("VM Network Controller", func() {
 		}),
 	)
 
+	It("sync does not hotplug a new absent interface", func() {
+		clientset := fake.NewSimpleClientset()
+		c := controllers.NewVMController(clientset)
+		vmi := libvmi.New(
+			libvmi.WithInterface(libvmi.InterfaceDeviceWithMasqueradeBinding()),
+			libvmi.WithNetwork(v1.DefaultPodNetwork()),
+		)
+
+		originalVMI := vmi.DeepCopy()
+		vm := libvmi.NewVirtualMachine(originalVMI)
+
+		absentIfaceToPlug := v1.Interface{
+			Name:                   "absentIface",
+			State:                  v1.InterfaceStateAbsent,
+			InterfaceBindingMethod: v1.InterfaceBindingMethod{Bridge: &v1.InterfaceBridge{}},
+		}
+
+		vm = plugNetworkInterface(vm, absentIfaceToPlug)
+
+		// Simulate the existence of the VMI on the server (to allow the Sync to patch it).
path: pkg/network/controllers/vm_test.go
user: login: ormergi
body: Using By reflect in the test logs allowing to troubleshoot when there are failures.
For example having `By("Assert that the hotplugged interface and its matching network were cleared")`
result in Gingko to print the following line 
```
STEP: Assert that the hotplugged interface and its matching network were cleared
```

In e2e this is very useful.
created_at: 2025-04-27T08:56:11Z
author_association: CONTRIBUTOR
diff_hunk: @@ -156,6 +156,46 @@ var _ = Describe("VM Network Controller", func() {
 		}),
 	)
 
+	It("sync does not hotplug a new absent interface", func() {
+		clientset := fake.NewSimpleClientset()
+		c := controllers.NewVMController(clientset)
+		vmi := libvmi.New(
+			libvmi.WithInterface(libvmi.InterfaceDeviceWithMasqueradeBinding()),
+			libvmi.WithNetwork(v1.DefaultPodNetwork()),
+		)
+
+		originalVMI := vmi.DeepCopy()
+		vm := libvmi.NewVirtualMachine(originalVMI)
+
+		absentIfaceToPlug := v1.Interface{
+			Name:                   "absentIface",
+			State:                  v1.InterfaceStateAbsent,
+			InterfaceBindingMethod: v1.InterfaceBindingMethod{Bridge: &v1.InterfaceBridge{}},
+		}
+
+		vm = plugNetworkInterface(vm, absentIfaceToPlug)
+
+		// Simulate the existence of the VMI on the server (to allow the Sync to patch it).
path: pkg/network/controllers/vm_test.go
user: login: orelmisan
body: > In e2e this is very useful.

I agree.
But keep in mind that these are unit tests, which quickly run to completion.
Having them around is not much different from having comments - which can rot and are sometimes a maintenance burden.
created_at: 2025-04-27T09:01:11Z
author_association: MEMBER
diff_hunk: @@ -509,6 +509,70 @@ var _ = Describe("VM Network Controller", func() {
 		Expect(iface).NotTo(BeNil())
 		Expect(iface.State).NotTo(Equal(v1.InterfaceStateAbsent))
 	})
+
+	It("Sync successfully performs hotplug and hotunplug at the same time", func() {
+		const (
+			netToAttachName = "attach-me"
+
+			netToDetachName    = "detach-me"
+			netToDetachNADName = "detach-me-nad"
+		)
+
+		clientset := fake.NewSimpleClientset()
+		c := controllers.NewVMController(clientset)
+
+		multusAndDomainInfoSource := vmispec.NewInfoSource(vmispec.InfoSourceMultusStatus, vmispec.InfoSourceDomain)
+
+		ifaceToDetach := libvmi.InterfaceDeviceWithBridgeBinding(netToDetachName)
+		netToDetach := libvmi.MultusNetwork(netToDetachName, netToDetachNADName)
+
+		vmi := libvmi.New(
+			libvmi.WithInterface(libvmi.InterfaceDeviceWithMasqueradeBinding()),
+			libvmi.WithInterface(ifaceToDetach),
+			libvmi.WithNetwork(v1.DefaultPodNetwork()),
+			libvmi.WithNetwork(netToDetach),
+			libvmistatus.WithStatus(
+				libvmistatus.New(
+					libvmistatus.WithInterfaceStatus(v1.VirtualMachineInstanceNetworkInterface{
+						Name:             defaultNetName,
+						PodInterfaceName: namescheme.PrimaryPodInterfaceName,
+						InfoSource:       vmispec.InfoSourceDomain,
+					}),
+					libvmistatus.WithInterfaceStatus(v1.VirtualMachineInstanceNetworkInterface{
+						Name:             netToDetachName,
+						PodInterfaceName: namescheme.GenerateHashedInterfaceName(netToDetachName),
+						InfoSource:       multusAndDomainInfoSource,
+					}),
+				),
+			),
+		)
+
+		vm := libvmi.NewVirtualMachine(vmi.DeepCopy())
+
+		// Mark the secondary interface for hotunplug
+		vm.Spec.Template.Spec.Domain.Devices.Interfaces[1].State = v1.InterfaceStateAbsent
+
+		vm = plugNetworkInterface(vm, libvmi.InterfaceDeviceWithBridgeBinding(netToAttachName))
+
+		// Simulate the existence of the VMI on the server (to allow the Sync to patch it).
+		_, err := clientset.KubevirtV1().VirtualMachineInstances(vmi.Namespace).Create(context.Background(), vmi, k8smetav1.CreateOptions{})
path: pkg/network/controllers/vm_test.go
user: login: ormergi
body: I think the test missing the VirtualMachine creation as well, having the test simulate VM existence should prevent unwanted side effects.
created_at: 2025-04-27T09:10:36Z
author_association: CONTRIBUTOR
diff_hunk: @@ -509,6 +509,70 @@ var _ = Describe("VM Network Controller", func() {
 		Expect(iface).NotTo(BeNil())
 		Expect(iface.State).NotTo(Equal(v1.InterfaceStateAbsent))
 	})
+
+	It("Sync successfully performs hotplug and hotunplug at the same time", func() {
+		const (
+			netToAttachName = "attach-me"
+
+			netToDetachName    = "detach-me"
+			netToDetachNADName = "detach-me-nad"
+		)
+
+		clientset := fake.NewSimpleClientset()
+		c := controllers.NewVMController(clientset)
+
+		multusAndDomainInfoSource := vmispec.NewInfoSource(vmispec.InfoSourceMultusStatus, vmispec.InfoSourceDomain)
+
+		ifaceToDetach := libvmi.InterfaceDeviceWithBridgeBinding(netToDetachName)
+		netToDetach := libvmi.MultusNetwork(netToDetachName, netToDetachNADName)
+
+		vmi := libvmi.New(
+			libvmi.WithInterface(libvmi.InterfaceDeviceWithMasqueradeBinding()),
+			libvmi.WithInterface(ifaceToDetach),
+			libvmi.WithNetwork(v1.DefaultPodNetwork()),
+			libvmi.WithNetwork(netToDetach),
+			libvmistatus.WithStatus(
+				libvmistatus.New(
+					libvmistatus.WithInterfaceStatus(v1.VirtualMachineInstanceNetworkInterface{
+						Name:             defaultNetName,
+						PodInterfaceName: namescheme.PrimaryPodInterfaceName,
+						InfoSource:       vmispec.InfoSourceDomain,
+					}),
+					libvmistatus.WithInterfaceStatus(v1.VirtualMachineInstanceNetworkInterface{
+						Name:             netToDetachName,
+						PodInterfaceName: namescheme.GenerateHashedInterfaceName(netToDetachName),
+						InfoSource:       multusAndDomainInfoSource,
+					}),
+				),
+			),
+		)
+
+		vm := libvmi.NewVirtualMachine(vmi.DeepCopy())
+
+		// Mark the secondary interface for hotunplug
+		vm.Spec.Template.Spec.Domain.Devices.Interfaces[1].State = v1.InterfaceStateAbsent
+
+		vm = plugNetworkInterface(vm, libvmi.InterfaceDeviceWithBridgeBinding(netToAttachName))
+
+		// Simulate the existence of the VMI on the server (to allow the Sync to patch it).
+		_, err := clientset.KubevirtV1().VirtualMachineInstances(vmi.Namespace).Create(context.Background(), vmi, k8smetav1.CreateOptions{})
+		Expect(err).NotTo(HaveOccurred())
+
+		originalVM := vm.DeepCopy()
+		updatedVM, err := c.Sync(vm, vmi)
+		Expect(err).NotTo(HaveOccurred())
+
+		Expect(updatedVM).To(Equal(originalVM))
path: pkg/network/controllers/vm_test.go
user: login: ormergi
body: nit: I think adding a comment saying "the controller should not change the VM object" would be nice.
created_at: 2025-04-27T09:13:47Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,68 +0,0 @@
-/*
- * 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 controllers_test
-
-import (
-	. "github.com/onsi/ginkgo/v2"
-	. "github.com/onsi/gomega"
-
-	v1 "kubevirt.io/api/core/v1"
-
-	"kubevirt.io/kubevirt/pkg/network/controllers"
-	"kubevirt.io/kubevirt/pkg/network/vmispec"
-)
-
-var _ = Describe("Network interface hot{un}plug", func() {
-	DescribeTable("spec interfaces",
-		func(specIfaces []v1.Interface, statusIfaces []v1.VirtualMachineInstanceNetworkInterface,
-			expectedInterfaces []v1.Interface, expectedNetworks []v1.Network,
-		) {
-			var testNetworks []v1.Network
-			for _, iface := range specIfaces {
-				testNetworks = append(testNetworks, v1.Network{Name: iface.Name})
-			}
-			testStatusIfaces := vmispec.IndexInterfaceStatusByName(statusIfaces,
-				func(i v1.VirtualMachineInstanceNetworkInterface) bool { return true })
-
-			ifaces, networks := controllers.ClearDetachedInterfaces(specIfaces, testNetworks, testStatusIfaces)
-
-			Expect(ifaces).To(Equal(expectedInterfaces))
-			Expect(networks).To(Equal(expectedNetworks))
-		},
-		Entry("should remain, given non-absent interfaces, and no associated status ifaces (i.e.: plug pending)",
-			[]v1.Interface{{Name: "blue"}, {Name: "red"}},
-			[]v1.VirtualMachineInstanceNetworkInterface{},
-			[]v1.Interface{{Name: "blue"}, {Name: "red"}},
-			[]v1.Network{{Name: "blue"}, {Name: "red"}},
-		),
-		Entry("should remain, given non-absent interfaces, and associated status ifaces (i.e.: plugged iface)",
-			[]v1.Interface{{Name: "blue"}, {Name: "red"}},
-			[]v1.VirtualMachineInstanceNetworkInterface{{Name: "blue"}, {Name: "red"}},
-			[]v1.Interface{{Name: "blue"}, {Name: "red"}},
-			[]v1.Network{{Name: "blue"}, {Name: "red"}},
-		),
-		Entry("should remain, given status iface and no associated iface in spec",
-			[]v1.Interface{{Name: "blue"}},
-			[]v1.VirtualMachineInstanceNetworkInterface{{Name: "RED"}},
-			[]v1.Interface{{Name: "blue"}},
-			[]v1.Network{{Name: "blue"}},
-		),
path: pkg/network/controllers/hotplug_test.go
user: login: ormergi
body: The mentioned test [1] covers a successful scenario of hotplug, but it doesnt seem to cover these scenarios, when there are pending hotplug/unplug  and when a removed iface remain in status.
Please consider extending the [1] to add new one(s) to cover that.

[1] https://github.com/kubevirt/kubevirt/blob/c6aeb68d1794b71026236f726c15aa9550cc6d64/pkg/network/controllers/vm_test.go#L111
created_at: 2025-04-27T09:26:06Z
author_association: CONTRIBUTOR
diff_hunk: @@ -509,6 +509,70 @@ var _ = Describe("VM Network Controller", func() {
 		Expect(iface).NotTo(BeNil())
 		Expect(iface.State).NotTo(Equal(v1.InterfaceStateAbsent))
 	})
+
+	It("Sync successfully performs hotplug and hotunplug at the same time", func() {
+		const (
+			netToAttachName = "attach-me"
+
+			netToDetachName    = "detach-me"
+			netToDetachNADName = "detach-me-nad"
+		)
+
+		clientset := fake.NewSimpleClientset()
+		c := controllers.NewVMController(clientset)
+
+		multusAndDomainInfoSource := vmispec.NewInfoSource(vmispec.InfoSourceMultusStatus, vmispec.InfoSourceDomain)
+
+		ifaceToDetach := libvmi.InterfaceDeviceWithBridgeBinding(netToDetachName)
+		netToDetach := libvmi.MultusNetwork(netToDetachName, netToDetachNADName)
+
+		vmi := libvmi.New(
+			libvmi.WithInterface(libvmi.InterfaceDeviceWithMasqueradeBinding()),
+			libvmi.WithInterface(ifaceToDetach),
+			libvmi.WithNetwork(v1.DefaultPodNetwork()),
+			libvmi.WithNetwork(netToDetach),
+			libvmistatus.WithStatus(
+				libvmistatus.New(
+					libvmistatus.WithInterfaceStatus(v1.VirtualMachineInstanceNetworkInterface{
+						Name:             defaultNetName,
+						PodInterfaceName: namescheme.PrimaryPodInterfaceName,
+						InfoSource:       vmispec.InfoSourceDomain,
+					}),
+					libvmistatus.WithInterfaceStatus(v1.VirtualMachineInstanceNetworkInterface{
+						Name:             netToDetachName,
+						PodInterfaceName: namescheme.GenerateHashedInterfaceName(netToDetachName),
+						InfoSource:       multusAndDomainInfoSource,
+					}),
+				),
+			),
+		)
+
+		vm := libvmi.NewVirtualMachine(vmi.DeepCopy())
+
+		// Mark the secondary interface for hotunplug
+		vm.Spec.Template.Spec.Domain.Devices.Interfaces[1].State = v1.InterfaceStateAbsent
+
+		vm = plugNetworkInterface(vm, libvmi.InterfaceDeviceWithBridgeBinding(netToAttachName))
+
+		// Simulate the existence of the VMI on the server (to allow the Sync to patch it).
+		_, err := clientset.KubevirtV1().VirtualMachineInstances(vmi.Namespace).Create(context.Background(), vmi, k8smetav1.CreateOptions{})
path: pkg/network/controllers/vm_test.go
user: login: orelmisan
body: The VM objects are not "created" on the fake client all over the file.
This is because the network VM controller does not directly fetches / mutates the VirtualMachine object (currently it just patches the VMI).

created_at: 2025-05-18T12:39:44Z
author_association: MEMBER
diff_hunk: @@ -509,6 +509,70 @@ var _ = Describe("VM Network Controller", func() {
 		Expect(iface).NotTo(BeNil())
 		Expect(iface.State).NotTo(Equal(v1.InterfaceStateAbsent))
 	})
+
+	It("Sync successfully performs hotplug and hotunplug at the same time", func() {
+		const (
+			netToAttachName = "attach-me"
+
+			netToDetachName    = "detach-me"
+			netToDetachNADName = "detach-me-nad"
+		)
+
+		clientset := fake.NewSimpleClientset()
+		c := controllers.NewVMController(clientset)
+
+		multusAndDomainInfoSource := vmispec.NewInfoSource(vmispec.InfoSourceMultusStatus, vmispec.InfoSourceDomain)
+
+		ifaceToDetach := libvmi.InterfaceDeviceWithBridgeBinding(netToDetachName)
+		netToDetach := libvmi.MultusNetwork(netToDetachName, netToDetachNADName)
+
+		vmi := libvmi.New(
+			libvmi.WithInterface(libvmi.InterfaceDeviceWithMasqueradeBinding()),
+			libvmi.WithInterface(ifaceToDetach),
+			libvmi.WithNetwork(v1.DefaultPodNetwork()),
+			libvmi.WithNetwork(netToDetach),
+			libvmistatus.WithStatus(
+				libvmistatus.New(
+					libvmistatus.WithInterfaceStatus(v1.VirtualMachineInstanceNetworkInterface{
+						Name:             defaultNetName,
+						PodInterfaceName: namescheme.PrimaryPodInterfaceName,
+						InfoSource:       vmispec.InfoSourceDomain,
+					}),
+					libvmistatus.WithInterfaceStatus(v1.VirtualMachineInstanceNetworkInterface{
+						Name:             netToDetachName,
+						PodInterfaceName: namescheme.GenerateHashedInterfaceName(netToDetachName),
+						InfoSource:       multusAndDomainInfoSource,
+					}),
+				),
+			),
+		)
+
+		vm := libvmi.NewVirtualMachine(vmi.DeepCopy())
+
+		// Mark the secondary interface for hotunplug
+		vm.Spec.Template.Spec.Domain.Devices.Interfaces[1].State = v1.InterfaceStateAbsent
+
+		vm = plugNetworkInterface(vm, libvmi.InterfaceDeviceWithBridgeBinding(netToAttachName))
+
+		// Simulate the existence of the VMI on the server (to allow the Sync to patch it).
+		_, err := clientset.KubevirtV1().VirtualMachineInstances(vmi.Namespace).Create(context.Background(), vmi, k8smetav1.CreateOptions{})
+		Expect(err).NotTo(HaveOccurred())
+
+		originalVM := vm.DeepCopy()
+		updatedVM, err := c.Sync(vm, vmi)
+		Expect(err).NotTo(HaveOccurred())
+
+		Expect(updatedVM).To(Equal(originalVM))
path: pkg/network/controllers/vm_test.go
user: login: orelmisan
body: > nit: I think adding a comment saying "the controller should not change the VM object" would be nice.

Isn't it reflected with this assertion?

created_at: 2025-05-18T12:40:26Z
author_association: MEMBER
diff_hunk: @@ -1,68 +0,0 @@
-/*
- * 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 controllers_test
-
-import (
-	. "github.com/onsi/ginkgo/v2"
-	. "github.com/onsi/gomega"
-
-	v1 "kubevirt.io/api/core/v1"
-
-	"kubevirt.io/kubevirt/pkg/network/controllers"
-	"kubevirt.io/kubevirt/pkg/network/vmispec"
-)
-
-var _ = Describe("Network interface hot{un}plug", func() {
-	DescribeTable("spec interfaces",
-		func(specIfaces []v1.Interface, statusIfaces []v1.VirtualMachineInstanceNetworkInterface,
-			expectedInterfaces []v1.Interface, expectedNetworks []v1.Network,
-		) {
-			var testNetworks []v1.Network
-			for _, iface := range specIfaces {
-				testNetworks = append(testNetworks, v1.Network{Name: iface.Name})
-			}
-			testStatusIfaces := vmispec.IndexInterfaceStatusByName(statusIfaces,
-				func(i v1.VirtualMachineInstanceNetworkInterface) bool { return true })
-
-			ifaces, networks := controllers.ClearDetachedInterfaces(specIfaces, testNetworks, testStatusIfaces)
-
-			Expect(ifaces).To(Equal(expectedInterfaces))
-			Expect(networks).To(Equal(expectedNetworks))
-		},
-		Entry("should remain, given non-absent interfaces, and no associated status ifaces (i.e.: plug pending)",
-			[]v1.Interface{{Name: "blue"}, {Name: "red"}},
-			[]v1.VirtualMachineInstanceNetworkInterface{},
-			[]v1.Interface{{Name: "blue"}, {Name: "red"}},
-			[]v1.Network{{Name: "blue"}, {Name: "red"}},
-		),
-		Entry("should remain, given non-absent interfaces, and associated status ifaces (i.e.: plugged iface)",
-			[]v1.Interface{{Name: "blue"}, {Name: "red"}},
-			[]v1.VirtualMachineInstanceNetworkInterface{{Name: "blue"}, {Name: "red"}},
-			[]v1.Interface{{Name: "blue"}, {Name: "red"}},
-			[]v1.Network{{Name: "blue"}, {Name: "red"}},
-		),
-		Entry("should remain, given status iface and no associated iface in spec",
-			[]v1.Interface{{Name: "blue"}},
-			[]v1.VirtualMachineInstanceNetworkInterface{{Name: "RED"}},
-			[]v1.Interface{{Name: "blue"}},
-			[]v1.Network{{Name: "blue"}},
-		),
path: pkg/network/controllers/hotplug_test.go
user: login: orelmisan
body: The hotplug test has two interfaces without a matching interface status - Same as "should remain, given non-absent interfaces, and no associated status ifaces (i.e.: plug pending)".

The hotunplug test has a pending interface.
created_at: 2025-05-18T12:46:20Z
author_association: MEMBER
diff_hunk: @@ -156,6 +156,46 @@ var _ = Describe("VM Network Controller", func() {
 		}),
 	)
 
+	It("sync does not hotplug a new absent interface", func() {
+		clientset := fake.NewSimpleClientset()
path: pkg/network/controllers/vm_test.go
user: login: oshoval
body: worth meanwhile to have it on Context +  BeforeEach for this ?
not mandatory, but this what we are doing not once 

created_at: 2025-05-20T10:43:39Z
author_association: CONTRIBUTOR
diff_hunk: @@ -156,6 +156,46 @@ var _ = Describe("VM Network Controller", func() {
 		}),
 	)
 
+	It("sync does not hotplug a new absent interface", func() {
+		clientset := fake.NewSimpleClientset()
+		c := controllers.NewVMController(clientset)
+		vmi := libvmi.New(
+			libvmi.WithInterface(libvmi.InterfaceDeviceWithMasqueradeBinding()),
path: pkg/network/controllers/vm_test.go
user: login: oshoval
body: is it just me or we didnt have it on the previous test?
if it is not a must maybe worth to drop ?

created_at: 2025-05-20T10:51:05Z
author_association: CONTRIBUTOR
diff_hunk: @@ -509,6 +509,70 @@ var _ = Describe("VM Network Controller", func() {
 		Expect(iface).NotTo(BeNil())
 		Expect(iface.State).NotTo(Equal(v1.InterfaceStateAbsent))
 	})
+
+	It("Sync successfully performs hotplug and hotunplug at the same time", func() {
+		const (
+			netToAttachName = "attach-me"
path: pkg/network/controllers/vm_test.go
user: login: nirdothan
body: I like the names!
created_at: 2025-05-20T11:21:36Z
author_association: MEMBER
diff_hunk: @@ -509,6 +509,70 @@ var _ = Describe("VM Network Controller", func() {
 		Expect(iface).NotTo(BeNil())
 		Expect(iface.State).NotTo(Equal(v1.InterfaceStateAbsent))
 	})
+
+	It("Sync successfully performs hotplug and hotunplug at the same time", func() {
+		const (
+			netToAttachName = "attach-me"
+
+			netToDetachName    = "detach-me"
+			netToDetachNADName = "detach-me-nad"
+		)
+
+		clientset := fake.NewSimpleClientset()
+		c := controllers.NewVMController(clientset)
+
+		multusAndDomainInfoSource := vmispec.NewInfoSource(vmispec.InfoSourceMultusStatus, vmispec.InfoSourceDomain)
+
+		ifaceToDetach := libvmi.InterfaceDeviceWithBridgeBinding(netToDetachName)
+		netToDetach := libvmi.MultusNetwork(netToDetachName, netToDetachNADName)
+
+		vmi := libvmi.New(
+			libvmi.WithInterface(libvmi.InterfaceDeviceWithMasqueradeBinding()),
+			libvmi.WithInterface(ifaceToDetach),
+			libvmi.WithNetwork(v1.DefaultPodNetwork()),
+			libvmi.WithNetwork(netToDetach),
+			libvmistatus.WithStatus(
path: pkg/network/controllers/vm_test.go
user: login: oshoval
body: please consider a follow-up that use the fixture and split it to 2 tests, this one is too long
created_at: 2025-05-20T11:28:43Z
author_association: CONTRIBUTOR
diff_hunk: @@ -509,6 +509,70 @@ var _ = Describe("VM Network Controller", func() {
 		Expect(iface).NotTo(BeNil())
 		Expect(iface.State).NotTo(Equal(v1.InterfaceStateAbsent))
 	})
+
+	It("Sync successfully performs hotplug and hotunplug at the same time", func() {
+		const (
+			netToAttachName = "attach-me"
+
+			netToDetachName    = "detach-me"
+			netToDetachNADName = "detach-me-nad"
+		)
+
+		clientset := fake.NewSimpleClientset()
+		c := controllers.NewVMController(clientset)
+
+		multusAndDomainInfoSource := vmispec.NewInfoSource(vmispec.InfoSourceMultusStatus, vmispec.InfoSourceDomain)
+
+		ifaceToDetach := libvmi.InterfaceDeviceWithBridgeBinding(netToDetachName)
+		netToDetach := libvmi.MultusNetwork(netToDetachName, netToDetachNADName)
+
+		vmi := libvmi.New(
+			libvmi.WithInterface(libvmi.InterfaceDeviceWithMasqueradeBinding()),
+			libvmi.WithInterface(ifaceToDetach),
+			libvmi.WithNetwork(v1.DefaultPodNetwork()),
+			libvmi.WithNetwork(netToDetach),
+			libvmistatus.WithStatus(
+				libvmistatus.New(
+					libvmistatus.WithInterfaceStatus(v1.VirtualMachineInstanceNetworkInterface{
+						Name:             defaultNetName,
+						PodInterfaceName: namescheme.PrimaryPodInterfaceName,
+						InfoSource:       vmispec.InfoSourceDomain,
+					}),
+					libvmistatus.WithInterfaceStatus(v1.VirtualMachineInstanceNetworkInterface{
+						Name:             netToDetachName,
+						PodInterfaceName: namescheme.GenerateHashedInterfaceName(netToDetachName),
+						InfoSource:       multusAndDomainInfoSource,
path: pkg/network/controllers/vm_test.go
user: login: oshoval
body: on `updatedVM, err := c.Sync(vm, vmi)` we dont need infoSource right ?
if so, worth to remove please here and on L539
because it isn't required + this way the test can hide problems if something is changed and by chance this info mask a bug

created_at: 2025-05-20T11:35:31Z
author_association: CONTRIBUTOR
diff_hunk: @@ -156,6 +156,46 @@ var _ = Describe("VM Network Controller", func() {
 		}),
 	)
 
+	It("sync does not hotplug a new absent interface", func() {
+		clientset := fake.NewSimpleClientset()
+		c := controllers.NewVMController(clientset)
+		vmi := libvmi.New(
+			libvmi.WithInterface(libvmi.InterfaceDeviceWithMasqueradeBinding()),
path: pkg/network/controllers/vm_test.go
user: login: orelmisan
body: > is it just me or we didnt have it on the previous test? if it is not a must maybe worth to drop ?

I don't follow, could you please elaborate?
created_at: 2025-05-20T12:12:58Z
author_association: MEMBER
diff_hunk: @@ -509,6 +509,70 @@ var _ = Describe("VM Network Controller", func() {
 		Expect(iface).NotTo(BeNil())
 		Expect(iface.State).NotTo(Equal(v1.InterfaceStateAbsent))
 	})
+
+	It("Sync successfully performs hotplug and hotunplug at the same time", func() {
+		const (
+			netToAttachName = "attach-me"
+
+			netToDetachName    = "detach-me"
+			netToDetachNADName = "detach-me-nad"
+		)
+
+		clientset := fake.NewSimpleClientset()
+		c := controllers.NewVMController(clientset)
+
+		multusAndDomainInfoSource := vmispec.NewInfoSource(vmispec.InfoSourceMultusStatus, vmispec.InfoSourceDomain)
+
+		ifaceToDetach := libvmi.InterfaceDeviceWithBridgeBinding(netToDetachName)
+		netToDetach := libvmi.MultusNetwork(netToDetachName, netToDetachNADName)
+
+		vmi := libvmi.New(
+			libvmi.WithInterface(libvmi.InterfaceDeviceWithMasqueradeBinding()),
+			libvmi.WithInterface(ifaceToDetach),
+			libvmi.WithNetwork(v1.DefaultPodNetwork()),
+			libvmi.WithNetwork(netToDetach),
+			libvmistatus.WithStatus(
+				libvmistatus.New(
+					libvmistatus.WithInterfaceStatus(v1.VirtualMachineInstanceNetworkInterface{
+						Name:             defaultNetName,
+						PodInterfaceName: namescheme.PrimaryPodInterfaceName,
+						InfoSource:       vmispec.InfoSourceDomain,
+					}),
+					libvmistatus.WithInterfaceStatus(v1.VirtualMachineInstanceNetworkInterface{
+						Name:             netToDetachName,
+						PodInterfaceName: namescheme.GenerateHashedInterfaceName(netToDetachName),
+						InfoSource:       multusAndDomainInfoSource,
path: pkg/network/controllers/vm_test.go
user: login: orelmisan
body: This is how the VMI is configured in a hotunplug scenario.
created_at: 2025-05-20T12:14:49Z
author_association: MEMBER
diff_hunk: @@ -509,6 +509,70 @@ var _ = Describe("VM Network Controller", func() {
 		Expect(iface).NotTo(BeNil())
 		Expect(iface.State).NotTo(Equal(v1.InterfaceStateAbsent))
 	})
+
+	It("Sync successfully performs hotplug and hotunplug at the same time", func() {
+		const (
+			netToAttachName = "attach-me"
+
+			netToDetachName    = "detach-me"
+			netToDetachNADName = "detach-me-nad"
+		)
+
+		clientset := fake.NewSimpleClientset()
+		c := controllers.NewVMController(clientset)
+
+		multusAndDomainInfoSource := vmispec.NewInfoSource(vmispec.InfoSourceMultusStatus, vmispec.InfoSourceDomain)
+
+		ifaceToDetach := libvmi.InterfaceDeviceWithBridgeBinding(netToDetachName)
+		netToDetach := libvmi.MultusNetwork(netToDetachName, netToDetachNADName)
+
+		vmi := libvmi.New(
+			libvmi.WithInterface(libvmi.InterfaceDeviceWithMasqueradeBinding()),
+			libvmi.WithInterface(ifaceToDetach),
+			libvmi.WithNetwork(v1.DefaultPodNetwork()),
+			libvmi.WithNetwork(netToDetach),
+			libvmistatus.WithStatus(
+				libvmistatus.New(
+					libvmistatus.WithInterfaceStatus(v1.VirtualMachineInstanceNetworkInterface{
+						Name:             defaultNetName,
+						PodInterfaceName: namescheme.PrimaryPodInterfaceName,
+						InfoSource:       vmispec.InfoSourceDomain,
+					}),
+					libvmistatus.WithInterfaceStatus(v1.VirtualMachineInstanceNetworkInterface{
+						Name:             netToDetachName,
+						PodInterfaceName: namescheme.GenerateHashedInterfaceName(netToDetachName),
+						InfoSource:       multusAndDomainInfoSource,
path: pkg/network/controllers/vm_test.go
user: login: oshoval
body: ye i know but for this test those vars arent required right ?
this is the point
created_at: 2025-05-20T12:16:44Z
author_association: CONTRIBUTOR
diff_hunk: @@ -156,6 +156,46 @@ var _ = Describe("VM Network Controller", func() {
 		}),
 	)
 
+	It("sync does not hotplug a new absent interface", func() {
+		clientset := fake.NewSimpleClientset()
+		c := controllers.NewVMController(clientset)
+		vmi := libvmi.New(
+			libvmi.WithInterface(libvmi.InterfaceDeviceWithMasqueradeBinding()),
path: pkg/network/controllers/vm_test.go
user: login: oshoval
body: previous test didn't have the Masquerade interface, right ?
if so, might worth to drop it to have the test the minimal required
created_at: 2025-05-20T12:17:32Z
author_association: CONTRIBUTOR
diff_hunk: @@ -156,6 +156,46 @@ var _ = Describe("VM Network Controller", func() {
 		}),
 	)
 
+	It("sync does not hotplug a new absent interface", func() {
+		clientset := fake.NewSimpleClientset()
+		c := controllers.NewVMController(clientset)
+		vmi := libvmi.New(
+			libvmi.WithInterface(libvmi.InterfaceDeviceWithMasqueradeBinding()),
path: pkg/network/controllers/vm_test.go
user: login: orelmisan
body: > previous test didn't have the Masquerade interface, right ? if so, might worth to drop it to have the test the minimal required

What is the name of the test in question?
created_at: 2025-05-20T12:21:18Z
author_association: MEMBER
diff_hunk: @@ -156,6 +156,46 @@ var _ = Describe("VM Network Controller", func() {
 		}),
 	)
 
+	It("sync does not hotplug a new absent interface", func() {
+		clientset := fake.NewSimpleClientset()
+		c := controllers.NewVMController(clientset)
+		vmi := libvmi.New(
+			libvmi.WithInterface(libvmi.InterfaceDeviceWithMasqueradeBinding()),
path: pkg/network/controllers/vm_test.go
user: login: oshoval
body: the one on the first commit

https://github.com/kubevirt/kubevirt/pull/14419/commits/b9ba1c71fd24ea9e4b79d7684e3a9da962418234#diff-72336e7282e99e07104813732305eeabe4fd5e9b9c13e7c689bbaf78c1afbd8aL50

created_at: 2025-05-20T12:22:38Z
author_association: CONTRIBUTOR
diff_hunk: @@ -156,6 +156,46 @@ var _ = Describe("VM Network Controller", func() {
 		}),
 	)
 
+	It("sync does not hotplug a new absent interface", func() {
+		clientset := fake.NewSimpleClientset()
+		c := controllers.NewVMController(clientset)
+		vmi := libvmi.New(
+			libvmi.WithInterface(libvmi.InterfaceDeviceWithMasqueradeBinding()),
path: pkg/network/controllers/vm_test.go
user: login: orelmisan
body: It could be removed and libvmi.WithAutoAttachPodInterface(false) will be added.
Is it ok with you?
created_at: 2025-05-20T12:35:22Z
author_association: MEMBER
diff_hunk: @@ -156,6 +156,46 @@ var _ = Describe("VM Network Controller", func() {
 		}),
 	)
 
+	It("sync does not hotplug a new absent interface", func() {
+		clientset := fake.NewSimpleClientset()
+		c := controllers.NewVMController(clientset)
+		vmi := libvmi.New(
+			libvmi.WithInterface(libvmi.InterfaceDeviceWithMasqueradeBinding()),
path: pkg/network/controllers/vm_test.go
user: login: oshoval
body: default is auto attach, so i guess you just want it to be explicit ?
if so i would say just keep it as now, unless it is totally removed without even the line you just suggested
anyhow whatever you prefer

created_at: 2025-05-20T12:38:07Z
author_association: CONTRIBUTOR
diff_hunk: @@ -509,6 +509,70 @@ var _ = Describe("VM Network Controller", func() {
 		Expect(iface).NotTo(BeNil())
 		Expect(iface.State).NotTo(Equal(v1.InterfaceStateAbsent))
 	})
+
+	It("Sync successfully performs hotplug and hotunplug at the same time", func() {
+		const (
+			netToAttachName = "attach-me"
+
+			netToDetachName    = "detach-me"
+			netToDetachNADName = "detach-me-nad"
+		)
+
+		clientset := fake.NewSimpleClientset()
+		c := controllers.NewVMController(clientset)
+
+		multusAndDomainInfoSource := vmispec.NewInfoSource(vmispec.InfoSourceMultusStatus, vmispec.InfoSourceDomain)
+
+		ifaceToDetach := libvmi.InterfaceDeviceWithBridgeBinding(netToDetachName)
+		netToDetach := libvmi.MultusNetwork(netToDetachName, netToDetachNADName)
+
+		vmi := libvmi.New(
+			libvmi.WithInterface(libvmi.InterfaceDeviceWithMasqueradeBinding()),
+			libvmi.WithInterface(ifaceToDetach),
+			libvmi.WithNetwork(v1.DefaultPodNetwork()),
+			libvmi.WithNetwork(netToDetach),
+			libvmistatus.WithStatus(
+				libvmistatus.New(
+					libvmistatus.WithInterfaceStatus(v1.VirtualMachineInstanceNetworkInterface{
+						Name:             defaultNetName,
+						PodInterfaceName: namescheme.PrimaryPodInterfaceName,
+						InfoSource:       vmispec.InfoSourceDomain,
+					}),
+					libvmistatus.WithInterfaceStatus(v1.VirtualMachineInstanceNetworkInterface{
+						Name:             netToDetachName,
+						PodInterfaceName: namescheme.GenerateHashedInterfaceName(netToDetachName),
+						InfoSource:       multusAndDomainInfoSource,
path: pkg/network/controllers/vm_test.go
user: login: orelmisan
body: > ye i know but for this test those vars arent required right ?
this is the point

Define required.
I find it helps in understanding the tested scenario.
created_at: 2025-05-20T12:38:46Z
author_association: MEMBER
diff_hunk: @@ -156,6 +156,46 @@ var _ = Describe("VM Network Controller", func() {
 		}),
 	)
 
+	It("sync does not hotplug a new absent interface", func() {
+		clientset := fake.NewSimpleClientset()
+		c := controllers.NewVMController(clientset)
+		vmi := libvmi.New(
+			libvmi.WithInterface(libvmi.InterfaceDeviceWithMasqueradeBinding()),
path: pkg/network/controllers/vm_test.go
user: login: orelmisan
body: > default is auto attach, so i guess you just want it to be explicit ?

Yes.
created_at: 2025-05-20T12:39:39Z
author_association: MEMBER
diff_hunk: @@ -509,6 +509,70 @@ var _ = Describe("VM Network Controller", func() {
 		Expect(iface).NotTo(BeNil())
 		Expect(iface.State).NotTo(Equal(v1.InterfaceStateAbsent))
 	})
+
+	It("Sync successfully performs hotplug and hotunplug at the same time", func() {
+		const (
+			netToAttachName = "attach-me"
+
+			netToDetachName    = "detach-me"
+			netToDetachNADName = "detach-me-nad"
+		)
+
+		clientset := fake.NewSimpleClientset()
+		c := controllers.NewVMController(clientset)
+
+		multusAndDomainInfoSource := vmispec.NewInfoSource(vmispec.InfoSourceMultusStatus, vmispec.InfoSourceDomain)
+
+		ifaceToDetach := libvmi.InterfaceDeviceWithBridgeBinding(netToDetachName)
+		netToDetach := libvmi.MultusNetwork(netToDetachName, netToDetachNADName)
+
+		vmi := libvmi.New(
+			libvmi.WithInterface(libvmi.InterfaceDeviceWithMasqueradeBinding()),
+			libvmi.WithInterface(ifaceToDetach),
+			libvmi.WithNetwork(v1.DefaultPodNetwork()),
+			libvmi.WithNetwork(netToDetach),
+			libvmistatus.WithStatus(
+				libvmistatus.New(
+					libvmistatus.WithInterfaceStatus(v1.VirtualMachineInstanceNetworkInterface{
+						Name:             defaultNetName,
+						PodInterfaceName: namescheme.PrimaryPodInterfaceName,
+						InfoSource:       vmispec.InfoSourceDomain,
+					}),
+					libvmistatus.WithInterfaceStatus(v1.VirtualMachineInstanceNetworkInterface{
+						Name:             netToDetachName,
+						PodInterfaceName: namescheme.GenerateHashedInterfaceName(netToDetachName),
+						InfoSource:       multusAndDomainInfoSource,
path: pkg/network/controllers/vm_test.go
user: login: oshoval
body: required = test will fail without it

it is more explicit, but sometimes those details are just confusing
personally i prefer to just have the minimal info 

and it has a risk that it can mask a real bug, if the flow suddenly changes and those arguments
fit exactly a buggy case and make the test pass where it shouldnt

plus require to maintain it, without the test actually use it and verify the values make sense to the case
so it adds a layer of hardness

EDIT - it also misleads and defocus the reader to think those are important for the flow being tested


created_at: 2025-05-20T12:41:01Z
author_association: CONTRIBUTOR
diff_hunk: @@ -58,11 +58,5 @@ var _ = Describe("Network interface hot{un}plug", func() {
 			[]v1.Interface{{Name: "blue"}, {Name: "red"}},
 			[]v1.Network{{Name: "blue"}, {Name: "red"}},
 		),
-		Entry("should remain, given status iface and no associated iface in spec",
path: pkg/network/controllers/hotplug_test.go
user: login: nirdothan
body: I was hoping to `controllers.ClearDetachedInterfaces()` refactored  because it is very hard to follow.
While it's still here I'm having a hard time letting go of the direct tests that demonstrate its behavior.
Since "education" does not justify keeping redundant code, I won't block on this, however I do recommend that we take this as a follow up. 
created_at: 2025-05-20T12:50:11Z
author_association: MEMBER
diff_hunk: @@ -156,6 +156,46 @@ var _ = Describe("VM Network Controller", func() {
 		}),
 	)
 
+	It("sync does not hotplug a new absent interface", func() {
+		clientset := fake.NewSimpleClientset()
+		c := controllers.NewVMController(clientset)
+		vmi := libvmi.New(
+			libvmi.WithInterface(libvmi.InterfaceDeviceWithMasqueradeBinding()),
path: pkg/network/controllers/vm_test.go
user: login: oshoval
body: i think we miss by adding it explicitly,
reader shoiuldn't care about it,
auto atttach is a feature of kubevirt itself, not a package that we use,  where it worth to be explicit

but wont insist


created_at: 2025-05-20T12:50:16Z
author_association: CONTRIBUTOR
diff_hunk: @@ -74,6 +74,19 @@ var _ = Describe("VM Network Controller", func() {
 				libvmi.WithNetwork(libvmi.MultusNetwork(secondaryNetName, nadName)),
 			),
 		),
+		Entry("there is an interface status that does not match a spec interface",
+			libvmi.NewVirtualMachine(libvmi.New(
+				libvmi.WithInterface(libvmi.InterfaceDeviceWithMasqueradeBinding()),
+				libvmi.WithNetwork(v1.DefaultPodNetwork()),
+			)),
+			libvmi.New(
+				libvmi.WithInterface(libvmi.InterfaceDeviceWithMasqueradeBinding()),
+				libvmi.WithNetwork(v1.DefaultPodNetwork()),
+				libvmistatus.WithStatus(libvmistatus.New(
+					libvmistatus.WithInterfaceStatus(v1.VirtualMachineInstanceNetworkInterface{Name: "DEFAULT"}),
+				)),
path: pkg/network/controllers/vm_test.go
user: login: nirdothan
body: nit: Please consider changing the string into something that is significantly different from "default" in a different case.
created_at: 2025-05-20T12:55:56Z
author_association: MEMBER
diff_hunk: @@ -157,3 +157,19 @@ func applyDynamicIfaceRequestOnVMI(
 	}
 	return vmiSpecCopy
 }
+
+func clearDetachedInterfaces(
path: pkg/network/controllers/vm.go
user: login: nirdothan
body: As I wrote above: Let's refactor this function in a follow-up.
created_at: 2025-05-20T12:58:32Z
author_association: MEMBER
diff_hunk: @@ -58,11 +58,5 @@ var _ = Describe("Network interface hot{un}plug", func() {
 			[]v1.Interface{{Name: "blue"}, {Name: "red"}},
 			[]v1.Network{{Name: "blue"}, {Name: "red"}},
 		),
-		Entry("should remain, given status iface and no associated iface in spec",
path: pkg/network/controllers/hotplug_test.go
user: login: orelmisan
body: > I was hoping to `controllers.ClearDetachedInterfaces()` refactored because it is very hard to follow. While it's still here I'm having a hard time letting go of the direct tests that demonstrate its behavior. Since "education" does not justify keeping redundant code, I won't block on this, however I do recommend that we take this as a follow up.

The purpose of this PR is to ease refactoring the code in follow ups.
created_at: 2025-05-20T13:00:53Z
author_association: MEMBER
diff_hunk: @@ -74,6 +74,19 @@ var _ = Describe("VM Network Controller", func() {
 				libvmi.WithNetwork(libvmi.MultusNetwork(secondaryNetName, nadName)),
 			),
 		),
+		Entry("there is an interface status that does not match a spec interface",
+			libvmi.NewVirtualMachine(libvmi.New(
+				libvmi.WithInterface(libvmi.InterfaceDeviceWithMasqueradeBinding()),
+				libvmi.WithNetwork(v1.DefaultPodNetwork()),
+			)),
+			libvmi.New(
+				libvmi.WithInterface(libvmi.InterfaceDeviceWithMasqueradeBinding()),
+				libvmi.WithNetwork(v1.DefaultPodNetwork()),
+				libvmistatus.WithStatus(libvmistatus.New(
+					libvmistatus.WithInterfaceStatus(v1.VirtualMachineInstanceNetworkInterface{Name: "DEFAULT"}),
+				)),
path: pkg/network/controllers/vm_test.go
user: login: orelmisan
body: The original test did the same, I did not want to not divert from it.

created_at: 2025-05-20T13:02:04Z
author_association: MEMBER
diff_hunk: @@ -156,6 +156,46 @@ var _ = Describe("VM Network Controller", func() {
 		}),
 	)
 
+	It("sync does not hotplug a new absent interface", func() {
+		clientset := fake.NewSimpleClientset()
+		c := controllers.NewVMController(clientset)
+		vmi := libvmi.New(
+			libvmi.WithInterface(libvmi.InterfaceDeviceWithMasqueradeBinding()),
path: pkg/network/controllers/vm_test.go
user: login: orelmisan
body: I personally think its worth imitating reality in unit test.
So when people want to try a scenario they saw on unit test - it will resemble reality.
created_at: 2025-05-20T15:10:39Z
author_association: MEMBER
diff_hunk: @@ -509,6 +509,70 @@ var _ = Describe("VM Network Controller", func() {
 		Expect(iface).NotTo(BeNil())
 		Expect(iface.State).NotTo(Equal(v1.InterfaceStateAbsent))
 	})
+
+	It("Sync successfully performs hotplug and hotunplug at the same time", func() {
+		const (
+			netToAttachName = "attach-me"
+
+			netToDetachName    = "detach-me"
+			netToDetachNADName = "detach-me-nad"
+		)
+
+		clientset := fake.NewSimpleClientset()
+		c := controllers.NewVMController(clientset)
+
+		multusAndDomainInfoSource := vmispec.NewInfoSource(vmispec.InfoSourceMultusStatus, vmispec.InfoSourceDomain)
+
+		ifaceToDetach := libvmi.InterfaceDeviceWithBridgeBinding(netToDetachName)
+		netToDetach := libvmi.MultusNetwork(netToDetachName, netToDetachNADName)
+
+		vmi := libvmi.New(
+			libvmi.WithInterface(libvmi.InterfaceDeviceWithMasqueradeBinding()),
+			libvmi.WithInterface(ifaceToDetach),
+			libvmi.WithNetwork(v1.DefaultPodNetwork()),
+			libvmi.WithNetwork(netToDetach),
+			libvmistatus.WithStatus(
path: pkg/network/controllers/vm_test.go
user: login: orelmisan
body: There will be multiple follow ups.
This PR only brigs all the logic into one file.
created_at: 2025-05-20T15:11:05Z
author_association: MEMBER
diff_hunk: @@ -156,6 +156,46 @@ var _ = Describe("VM Network Controller", func() {
 		}),
 	)
 
+	It("sync does not hotplug a new absent interface", func() {
+		clientset := fake.NewSimpleClientset()
path: pkg/network/controllers/vm_test.go
user: login: orelmisan
body: I prefer to perform additional refactoring in follow-up PRs.
This PR has already became quite bit.
created_at: 2025-05-20T15:12:03Z
author_association: MEMBER
diff_hunk: @@ -509,6 +509,70 @@ var _ = Describe("VM Network Controller", func() {
 		Expect(iface).NotTo(BeNil())
 		Expect(iface.State).NotTo(Equal(v1.InterfaceStateAbsent))
 	})
+
+	It("Sync successfully performs hotplug and hotunplug at the same time", func() {
+		const (
+			netToAttachName = "attach-me"
+
+			netToDetachName    = "detach-me"
+			netToDetachNADName = "detach-me-nad"
+		)
+
+		clientset := fake.NewSimpleClientset()
+		c := controllers.NewVMController(clientset)
+
+		multusAndDomainInfoSource := vmispec.NewInfoSource(vmispec.InfoSourceMultusStatus, vmispec.InfoSourceDomain)
+
+		ifaceToDetach := libvmi.InterfaceDeviceWithBridgeBinding(netToDetachName)
+		netToDetach := libvmi.MultusNetwork(netToDetachName, netToDetachNADName)
+
+		vmi := libvmi.New(
+			libvmi.WithInterface(libvmi.InterfaceDeviceWithMasqueradeBinding()),
+			libvmi.WithInterface(ifaceToDetach),
+			libvmi.WithNetwork(v1.DefaultPodNetwork()),
+			libvmi.WithNetwork(netToDetach),
+			libvmistatus.WithStatus(
+				libvmistatus.New(
+					libvmistatus.WithInterfaceStatus(v1.VirtualMachineInstanceNetworkInterface{
+						Name:             defaultNetName,
+						PodInterfaceName: namescheme.PrimaryPodInterfaceName,
+						InfoSource:       vmispec.InfoSourceDomain,
+					}),
+					libvmistatus.WithInterfaceStatus(v1.VirtualMachineInstanceNetworkInterface{
+						Name:             netToDetachName,
+						PodInterfaceName: namescheme.GenerateHashedInterfaceName(netToDetachName),
+						InfoSource:       multusAndDomainInfoSource,
path: pkg/network/controllers/vm_test.go
user: login: orelmisan
body: Same answer as https://github.com/kubevirt/kubevirt/pull/14419#discussion_r2098236021.
created_at: 2025-05-20T15:13:00Z
author_association: MEMBER
diff_hunk: @@ -509,6 +509,70 @@ var _ = Describe("VM Network Controller", func() {
 		Expect(iface).NotTo(BeNil())
 		Expect(iface.State).NotTo(Equal(v1.InterfaceStateAbsent))
 	})
+
+	It("Sync successfully performs hotplug and hotunplug at the same time", func() {
+		const (
+			netToAttachName = "attach-me"
+
+			netToDetachName    = "detach-me"
+			netToDetachNADName = "detach-me-nad"
+		)
+
+		clientset := fake.NewSimpleClientset()
+		c := controllers.NewVMController(clientset)
+
+		multusAndDomainInfoSource := vmispec.NewInfoSource(vmispec.InfoSourceMultusStatus, vmispec.InfoSourceDomain)
+
+		ifaceToDetach := libvmi.InterfaceDeviceWithBridgeBinding(netToDetachName)
+		netToDetach := libvmi.MultusNetwork(netToDetachName, netToDetachNADName)
+
+		vmi := libvmi.New(
+			libvmi.WithInterface(libvmi.InterfaceDeviceWithMasqueradeBinding()),
+			libvmi.WithInterface(ifaceToDetach),
+			libvmi.WithNetwork(v1.DefaultPodNetwork()),
+			libvmi.WithNetwork(netToDetach),
+			libvmistatus.WithStatus(
+				libvmistatus.New(
+					libvmistatus.WithInterfaceStatus(v1.VirtualMachineInstanceNetworkInterface{
+						Name:             defaultNetName,
+						PodInterfaceName: namescheme.PrimaryPodInterfaceName,
+						InfoSource:       vmispec.InfoSourceDomain,
+					}),
+					libvmistatus.WithInterfaceStatus(v1.VirtualMachineInstanceNetworkInterface{
+						Name:             netToDetachName,
+						PodInterfaceName: namescheme.GenerateHashedInterfaceName(netToDetachName),
+						InfoSource:       multusAndDomainInfoSource,
path: pkg/network/controllers/vm_test.go
user: login: oshoval
body: not same case imho, but lets agree to disagree
hold can removed please, thanks

created_at: 2025-05-20T15:14:20Z
author_association: CONTRIBUTOR

PR comments:
user: login: orelmisan
created_at: 2025-04-03T17:35:09Z
body: /kind cleanup
author_association: MEMBER
user: login: orelmisan
created_at: 2025-04-03T17:35:20Z
body: /cc @nirdothan 
author_association: MEMBER
user: login: orelmisan
created_at: 2025-04-21T07:26:31Z
body: [Change](https://github.com/kubevirt/kubevirt/compare/d036d6c69b167683e7b43c5a9a06671b33ac19af..4f7d3b3a25be793887455e2274ba97cda1d731bb): Rebase.
author_association: MEMBER
user: login: orelmisan
created_at: 2025-04-21T07:47:03Z
body: [Change](https://github.com/kubevirt/kubevirt/compare/4f7d3b3a25be793887455e2274ba97cda1d731bb..ad5e0b2f57a1fb5d4e29a04de1a4bae344e01fcf): Updated commit message in order to address @EdDev's [comment](https://github.com/kubevirt/kubevirt/pull/14419#pullrequestreview-2745060489).
author_association: MEMBER
user: login: orelmisan
created_at: 2025-04-21T07:55:23Z
body: [Change](https://github.com/kubevirt/kubevirt/compare/ad5e0b2f57a1fb5d4e29a04de1a4bae344e01fcf..602f21c7c7483b4818e2745d2ff42f86232a52de): Fixed a typo in commit message.
author_association: MEMBER
user: login: orelmisan
created_at: 2025-04-21T19:16:12Z
body: > > The "should remain, given status iface and no associated iface in spec"
> > case's title is misleading, it actually tests that a spec interface will
> > not be cleared in case there is no matching interface status.
> > This scenario is covered by the hotplug scenario [1] as well.
> 
> I do not see what is misleading about it. Given there is an interface reported in the status but not in the spec, it should do nothing. The test describes it correctly, the implementation used it mixed with another scenario which may caused this confusion.
> 
> Still, the coverage of this case needs to be shown or explained why not.

The test's full name is `spec interfaces should remain, given status iface and no associated iface in spec`.
It is given an interface with a matching network, and an unrelated interface status.
The test asserts that the spec interface and its network are still there following the execution of the production logic.
The test does not assert about the interface status, so I think that the hotplug test covers it since it has no matching interface status for the hotplugged spec interface and network.
Also, the newly added hotplug and hotunplug scenario covers this case since it has a hotplugged interface without a matching interface status and there are additional unrelated interface statuses. 
author_association: MEMBER
user: login: EdDev
created_at: 2025-04-21T20:09:07Z
body: > and an unrelated interface status

The test name interprets it in the other direction: There is an interface on the status but not on the spec. For the interface that is specified on the spec, you can add the missing status and it won’t change the test (or what it aims to check).
author_association: MEMBER
user: login: ormergi
created_at: 2025-04-28T14:30:32Z
body: > One comment about the new tests, it seems the test calls the network controller Sync "statically", instead of having the controller actually running as we would with a real env. This approach may result in some side-effects not being detected by the tests, vs. having a the controller actually running as it would in a living env. Although I do understand changing the tests to work with a running controller require additional work. Pleas consider that for following PRs.

Following offline discussion with Orel, I checked the VM controller tests again, the controller seem to run statically as well so its not feasible to run the network VM controller tests differently.
Please ignore that comment.. 

author_association: CONTRIBUTOR
user: login: orelmisan
created_at: 2025-05-18T12:06:19Z
body: [Change](https://github.com/kubevirt/kubevirt/compare/602f21c7c7483b4818e2745d2ff42f86232a52de..3a133bc7c6955e4b52cb00287aba597ea397c84f): Rebase.
author_association: MEMBER
user: login: orelmisan
created_at: 2025-05-18T12:34:54Z
body: [Change](https://github.com/kubevirt/kubevirt/compare/3a133bc7c6955e4b52cb00287aba597ea397c84f..8a5786bde6b0e144e1a5ca693e4d1d0651ca87a9): Addressed review comments.
author_association: MEMBER
user: login: kubevirt-bot
created_at: 2025-05-18T12:34:56Z
body: @orelmisan: The following test **failed**, say `/retest` to rerun all failed tests or `/retest-required` to rerun all mandatory failed tests:

Test name | Commit | Details | Required | Rerun command
--- | --- | --- | --- | ---
pull-kubevirt-check-tests-for-flakes | 8a5786bde6b0e144e1a5ca693e4d1d0651ca87a9 | [link](https://prow.ci.kubevirt.io/view/gs/kubevirt-prow/pr-logs/pull/kubevirt_kubevirt/14419/pull-kubevirt-check-tests-for-flakes/1924081035818045440) | false | `/test pull-kubevirt-check-tests-for-flakes`

<details>

Instructions for interacting with me using PR comments are available [here](https://git.k8s.io/community/contributors/guide/pull-requests.md).  If you have questions or suggestions related to my behavior, please file an issue against the [kubernetes-sigs/prow](https://github.com/kubernetes-sigs/prow/issues/new?title=Prow%20issue:) repository. I understand the commands that are listed [here](https://go.k8s.io/bot-commands).
</details>
<!-- test report -->
author_association: CONTRIBUTOR
user: login: orelmisan
created_at: 2025-05-18T12:35:18Z
body: > > and an unrelated interface status
> 
> The test name interprets it in the other direction: There is an interface on the status but not on the spec. For the interface that is specified on the spec, you can add the missing status and it won’t change the test (or what it aims to check).

A test was added to cover this scenario.
author_association: MEMBER
user: login: kubevirt-bot
created_at: 2025-05-18T14:06:00Z
body: [APPROVALNOTIFIER] This PR is **APPROVED**

This pull-request has been approved by: *<a href="https://github.com/kubevirt/kubevirt/pull/14419#pullrequestreview-2848997514" title="Approved">EdDev</a>*

The full list of commands accepted by this bot can be found [here](https://prow.ci.kubevirt.io/command-help?repo=kubevirt%2Fkubevirt).

The pull request process is described [here](https://github.com/kubevirt/community/blob/main/docs/add-merge-automation-to-your-repository.md#example-workflow)

<details >
Needs approval from an approver in each of these files:

- ~~[pkg/network/OWNERS](https://github.com/kubevirt/kubevirt/blob/main/pkg/network/OWNERS)~~ [EdDev]

Approvers can indicate their approval by writing `/approve` in a comment
Approvers can cancel approval by writing `/approve cancel` in a comment
</details>
<!-- META={"approvers":[]} -->
author_association: CONTRIBUTOR
user: login: kubevirt-commenter-bot
created_at: 2025-05-20T12:59:11Z
body: Required labels detected, running phase 2 presubmits:
/test pull-kubevirt-e2e-windows2016
/test pull-kubevirt-e2e-kind-1.30-vgpu
/test pull-kubevirt-e2e-kind-sriov
/test pull-kubevirt-e2e-k8s-1.32-ipv6-sig-network
/test pull-kubevirt-e2e-k8s-1.30-sig-network
/test pull-kubevirt-e2e-k8s-1.30-sig-storage
/test pull-kubevirt-e2e-k8s-1.30-sig-compute
/test pull-kubevirt-e2e-k8s-1.30-sig-operator
/test pull-kubevirt-e2e-k8s-1.31-sig-network
/test pull-kubevirt-e2e-k8s-1.31-sig-storage
/test pull-kubevirt-e2e-k8s-1.31-sig-compute
/test pull-kubevirt-e2e-k8s-1.31-sig-operator

author_association: NONE
user: login: orelmisan
created_at: 2025-05-20T15:07:21Z
body: Lets wait for an agreement.

/hold
author_association: MEMBER
user: login: orelmisan
created_at: 2025-05-20T15:33:14Z
body: Removing the hold per @oshoval's agreement in https://github.com/kubevirt/kubevirt/pull/14419#discussion_r2098243723.
/hold cancel
author_association: MEMBER
user: login: kubevirt-commenter-bot
created_at: 2025-05-20T18:46:22Z
body: /retest-required
This bot automatically retries required jobs that failed/flaked on approved PRs.
Silence the bot with an `/lgtm cancel` or `/hold` comment for consistent failures.
author_association: NONE
user: login: kubevirt-commenter-bot
created_at: 2025-05-20T21:46:21Z
body: /retest-required
This bot automatically retries required jobs that failed/flaked on approved PRs.
Silence the bot with an `/lgtm cancel` or `/hold` comment for consistent failures.
author_association: NONE