From a7b8dd1a5c5eeca5cecac77132462263969ae69d Mon Sep 17 00:00:00 2001 From: Daniil Loktev Date: Mon, 6 Apr 2026 13:11:32 +0300 Subject: [PATCH 01/12] wip Signed-off-by: Daniil Loktev --- .../pkg/controller/kvbuilder/kvvm.go | 20 ++++++-- .../pkg/controller/kvbuilder/kvvm_utils.go | 12 ++++- .../pkg/controller/vm/internal/sync_kvvm.go | 46 +++++++++++++++++++ .../vmchange/comparator_pod_placement.go | 19 +++++++- 4 files changed, 89 insertions(+), 8 deletions(-) diff --git a/images/virtualization-artifact/pkg/controller/kvbuilder/kvvm.go b/images/virtualization-artifact/pkg/controller/kvbuilder/kvvm.go index 5ef04f9ed9..9e58030663 100644 --- a/images/virtualization-artifact/pkg/controller/kvbuilder/kvvm.go +++ b/images/virtualization-artifact/pkg/controller/kvbuilder/kvvm.go @@ -564,6 +564,15 @@ func (b *KVVM) ClearNetworkInterfaces() { b.Resource.Spec.Template.Spec.Domain.Devices.Interfaces = nil } +func (b *KVVM) SetNetworkInterfaceAbsent(name string) { + for i, iface := range b.Resource.Spec.Template.Spec.Domain.Devices.Interfaces { + if iface.Name == name { + b.Resource.Spec.Template.Spec.Domain.Devices.Interfaces[i].State = virtv1.InterfaceStateAbsent + return + } + } +} + func (b *KVVM) SetNetworkInterface(name, macAddress string, acpiIndex int) { net := virtv1.Network{ Name: name, @@ -590,15 +599,16 @@ func (b *KVVM) SetNetworkInterface(name, macAddress string, acpiIndex int) { iface.MacAddress = macAddress } - ifaceExists := false - for _, i := range b.Resource.Spec.Template.Spec.Domain.Devices.Interfaces { - if i.Name == name { - ifaceExists = true + updated := false + for i, existing := range b.Resource.Spec.Template.Spec.Domain.Devices.Interfaces { + if existing.Name == name { + b.Resource.Spec.Template.Spec.Domain.Devices.Interfaces[i] = iface + updated = true break } } - if !ifaceExists { + if !updated { b.Resource.Spec.Template.Spec.Domain.Devices.Interfaces = append(b.Resource.Spec.Template.Spec.Domain.Devices.Interfaces, iface) } } diff --git a/images/virtualization-artifact/pkg/controller/kvbuilder/kvvm_utils.go b/images/virtualization-artifact/pkg/controller/kvbuilder/kvvm_utils.go index 594a6d7bdb..732f502d68 100644 --- a/images/virtualization-artifact/pkg/controller/kvbuilder/kvvm_utils.go +++ b/images/virtualization-artifact/pkg/controller/kvbuilder/kvvm_utils.go @@ -355,7 +355,17 @@ func ApplyMigrationVolumes(kvvm *KVVM, vm *v1alpha2.VirtualMachine, vdsByName ma } func setNetwork(kvvm *KVVM, networkSpec network.InterfaceSpecList) { - kvvm.ClearNetworkInterfaces() + desiredByName := make(map[string]struct{}, len(networkSpec)) + for _, n := range networkSpec { + desiredByName[n.InterfaceName] = struct{}{} + } + + for _, iface := range kvvm.Resource.Spec.Template.Spec.Domain.Devices.Interfaces { + if _, wanted := desiredByName[iface.Name]; !wanted { + kvvm.SetNetworkInterfaceAbsent(iface.Name) + } + } + for _, n := range networkSpec { kvvm.SetNetworkInterface(n.InterfaceName, n.MAC, n.ID) } diff --git a/images/virtualization-artifact/pkg/controller/vm/internal/sync_kvvm.go b/images/virtualization-artifact/pkg/controller/vm/internal/sync_kvvm.go index 530551a938..3229bd7dec 100644 --- a/images/virtualization-artifact/pkg/controller/vm/internal/sync_kvvm.go +++ b/images/virtualization-artifact/pkg/controller/vm/internal/sync_kvvm.go @@ -658,6 +658,10 @@ func (h *SyncKvvmHandler) applyVMChangesToKVVM(ctx context.Context, s state.Virt return fmt.Errorf("unable to update KVVM using new VM spec: %w", err) } + if err := h.patchPodNetworkAnnotation(ctx, s); err != nil { + return fmt.Errorf("unable to patch pod network annotation: %w", err) + } + case vmchange.ActionNone: log.Info("No changes to underlying KVVM, update last-applied-spec annotation", "vm.name", current.GetName()) @@ -713,6 +717,48 @@ func (h *SyncKvvmHandler) isVMUnschedulable( return false } +func (h *SyncKvvmHandler) patchPodNetworkAnnotation(ctx context.Context, s state.VirtualMachineState) error { + log := logger.FromContext(ctx) + + pod, err := s.Pod(ctx) + if err != nil { + return err + } + if pod == nil { + return nil + } + + current := s.VirtualMachine().Current() + vmmacs, err := s.VirtualMachineMACAddresses(ctx) + if err != nil { + return err + } + + networkSpec := network.CreateNetworkSpec(current, vmmacs) + networkConfigStr, err := networkSpec.ToString() + if err != nil { + return fmt.Errorf("failed to serialize network spec: %w", err) + } + + currentAnnotation := pod.Annotations[annotations.AnnNetworksSpec] + if currentAnnotation == networkConfigStr { + return nil + } + + patch := client.MergeFrom(pod.DeepCopy()) + if pod.Annotations == nil { + pod.Annotations = make(map[string]string) + } + pod.Annotations[annotations.AnnNetworksSpec] = networkConfigStr + + if err := h.client.Patch(ctx, pod, patch); err != nil { + return fmt.Errorf("failed to patch pod %s network annotation: %w", pod.Name, err) + } + + log.Info("Patched pod network annotation", "pod", pod.Name, "networks", networkConfigStr) + return nil +} + // isPlacementPolicyChanged returns true if any of the Affinity, NodePlacement, or Toleration rules have changed. func (h *SyncKvvmHandler) isPlacementPolicyChanged(allChanges vmchange.SpecChanges) bool { for _, c := range allChanges.GetAll() { diff --git a/images/virtualization-artifact/pkg/controller/vmchange/comparator_pod_placement.go b/images/virtualization-artifact/pkg/controller/vmchange/comparator_pod_placement.go index 7a4b90c24a..e6e6eff9d5 100644 --- a/images/virtualization-artifact/pkg/controller/vmchange/comparator_pod_placement.go +++ b/images/virtualization-artifact/pkg/controller/vmchange/comparator_pod_placement.go @@ -89,10 +89,10 @@ func compareNetworks(current, desired *v1alpha2.VirtualMachineSpec) []FieldChang desiredValue := NewValue(desired.Networks, desired.Networks == nil, false) action := ActionRestart - // During upgrade from 1.6.0 to 1.7.0, network interface IDs are auto-populated for all existing VMs in the cluster. - // This allows avoiding a virtual machine restart during the version upgrade. if isOnlyNetworkIDAutofillChange(current.Networks, desired.Networks) { action = ActionNone + } else if isOnlyNonMainNetworksChanged(current.Networks, desired.Networks) { + action = ActionApplyImmediate } return compareValues( @@ -104,6 +104,21 @@ func compareNetworks(current, desired *v1alpha2.VirtualMachineSpec) []FieldChang ) } +func isOnlyNonMainNetworksChanged(current, desired []v1alpha2.NetworksSpec) bool { + currentMain := getMainNetwork(current) + desiredMain := getMainNetwork(desired) + return reflect.DeepEqual(currentMain, desiredMain) +} + +func getMainNetwork(networks []v1alpha2.NetworksSpec) *v1alpha2.NetworksSpec { + for i := range networks { + if networks[i].Type == v1alpha2.NetworksTypeMain { + return &networks[i] + } + } + return nil +} + func isOnlyNetworkIDAutofillChange(current, desired []v1alpha2.NetworksSpec) bool { if len(current) != len(desired) { return false From 4357da06727d6e36cd4ec955de2e3461d0127764 Mon Sep 17 00:00:00 2001 From: Daniil Loktev Date: Mon, 6 Apr 2026 15:45:27 +0300 Subject: [PATCH 02/12] wip Signed-off-by: Daniil Loktev --- build/components/versions.yml | 2 +- images/virt-artifact/werf.inc.yaml | 2 ++ images/virt-controller/werf.inc.yaml | 1 + 3 files changed, 4 insertions(+), 1 deletion(-) diff --git a/build/components/versions.yml b/build/components/versions.yml index f72a94e45d..d11de912ad 100644 --- a/build/components/versions.yml +++ b/build/components/versions.yml @@ -3,7 +3,7 @@ firmware: libvirt: v10.9.0 edk2: stable202411 core: - 3p-kubevirt: v1.6.2-v12n.21 + 3p-kubevirt: feat/core/network-hotplug-support 3p-containerized-data-importer: v1.60.3-v12n.17 distribution: 2.8.3 package: diff --git a/images/virt-artifact/werf.inc.yaml b/images/virt-artifact/werf.inc.yaml index f30560fba6..af8f96c8be 100644 --- a/images/virt-artifact/werf.inc.yaml +++ b/images/virt-artifact/werf.inc.yaml @@ -8,6 +8,7 @@ --- image: {{ .ModuleNamePrefix }}{{ .ImageName }}-src-artifact final: false +fromCacheVersion: "{{ now | date "Mon Jan 2 15:04:05 MST 2006" }}" fromImage: builder/src secrets: - id: SOURCE_REPO @@ -43,6 +44,7 @@ packages: image: {{ .ModuleNamePrefix }}{{ .ImageName }} final: false +fromCacheVersion: "{{ now | date "Mon Jan 2 15:04:05 MST 2006" }}" fromImage: {{ eq $.SVACE_ENABLED "false" | ternary "builder/golang-alt-1.25" "builder/golang-alt-svace-1.25" }} mount: - fromPath: ~/go-pkg-cache diff --git a/images/virt-controller/werf.inc.yaml b/images/virt-controller/werf.inc.yaml index c1f7113305..d74b7eaf76 100644 --- a/images/virt-controller/werf.inc.yaml +++ b/images/virt-controller/werf.inc.yaml @@ -1,5 +1,6 @@ --- image: {{ .ModuleNamePrefix }}{{ .ImageName }} +fromCacheVersion: "{{ now | date "Mon Jan 2 15:04:05 MST 2006" }}" fromImage: {{ .ModuleNamePrefix }}distroless git: {{- include "image mount points" . }} From c6eea115f150b18791a01b0a7140f25d4ea62cd5 Mon Sep 17 00:00:00 2001 From: Daniil Loktev Date: Mon, 6 Apr 2026 16:46:08 +0300 Subject: [PATCH 03/12] wip Signed-off-by: Daniil Loktev --- .../pkg/controller/kvbuilder/kvvm_test.go | 93 ++++++++++++++++ .../vmchange/comparator_pod_placement.go | 10 +- .../pkg/controller/vmchange/compare_test.go | 100 ++++++++++++++++++ 3 files changed, 202 insertions(+), 1 deletion(-) diff --git a/images/virtualization-artifact/pkg/controller/kvbuilder/kvvm_test.go b/images/virtualization-artifact/pkg/controller/kvbuilder/kvvm_test.go index 8a14050251..c67eff2afc 100644 --- a/images/virtualization-artifact/pkg/controller/kvbuilder/kvvm_test.go +++ b/images/virtualization-artifact/pkg/controller/kvbuilder/kvvm_test.go @@ -22,7 +22,9 @@ import ( corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/types" + virtv1 "kubevirt.io/api/core/v1" + "github.com/deckhouse/virtualization-controller/pkg/common/network" "github.com/deckhouse/virtualization/api/core/v1alpha2" ) @@ -145,3 +147,94 @@ func TestSetOsType(t *testing.T) { } }) } + +func newTestKVVM() *KVVM { + return NewEmptyKVVM(types.NamespacedName{Name: "test", Namespace: "default"}, KVVMOptions{ + EnableParavirtualization: true, + }) +} + +func TestSetNetworkInterfaceAbsent(t *testing.T) { + b := newTestKVVM() + b.SetNetworkInterface("default", "", 1) + b.SetNetworkInterface("veth_n12345678", "aa:bb:cc:dd:ee:ff", 2) + + b.SetNetworkInterfaceAbsent("veth_n12345678") + + for _, iface := range b.Resource.Spec.Template.Spec.Domain.Devices.Interfaces { + if iface.Name == "veth_n12345678" { + if iface.State != virtv1.InterfaceStateAbsent { + t.Errorf("expected State %q, got %q", virtv1.InterfaceStateAbsent, iface.State) + } + return + } + } + t.Error("interface veth_n12345678 not found") +} + +func TestSetNetworkInterfaceReplacesExisting(t *testing.T) { + b := newTestKVVM() + b.SetNetworkInterface("veth_n12345678", "aa:bb:cc:dd:ee:ff", 2) + b.SetNetworkInterfaceAbsent("veth_n12345678") + + b.SetNetworkInterface("veth_n12345678", "aa:bb:cc:dd:ee:ff", 2) + + for _, iface := range b.Resource.Spec.Template.Spec.Domain.Devices.Interfaces { + if iface.Name == "veth_n12345678" { + if iface.State != "" { + t.Errorf("expected empty State after re-add, got %q", iface.State) + } + return + } + } + t.Error("interface veth_n12345678 not found") +} + +func TestSetNetworkMarksRemovedAsAbsent(t *testing.T) { + b := newTestKVVM() + b.SetNetworkInterface("default", "", 1) + b.SetNetworkInterface("veth_n12345678", "aa:bb:cc:dd:ee:ff", 2) + + setNetwork(b, network.InterfaceSpecList{ + {InterfaceName: "default", MAC: "", ID: 1}, + }) + + found := false + for _, iface := range b.Resource.Spec.Template.Spec.Domain.Devices.Interfaces { + if iface.Name == "veth_n12345678" { + found = true + if iface.State != virtv1.InterfaceStateAbsent { + t.Errorf("removed interface should have State %q, got %q", virtv1.InterfaceStateAbsent, iface.State) + } + } + if iface.Name == "default" && iface.State != "" { + t.Errorf("kept interface should have empty State, got %q", iface.State) + } + } + if !found { + t.Error("removed interface should be retained with absent state, not deleted") + } +} + +func TestSetNetworkAddsNewInterface(t *testing.T) { + b := newTestKVVM() + b.SetNetworkInterface("default", "", 1) + + setNetwork(b, network.InterfaceSpecList{ + {InterfaceName: "default", MAC: "", ID: 1}, + {InterfaceName: "veth_n12345678", MAC: "aa:bb:cc:dd:ee:ff", ID: 2}, + }) + + found := false + for _, iface := range b.Resource.Spec.Template.Spec.Domain.Devices.Interfaces { + if iface.Name == "veth_n12345678" { + found = true + if iface.ACPIIndex != 2 { + t.Errorf("expected ACPIIndex 2, got %d", iface.ACPIIndex) + } + } + } + if !found { + t.Error("new interface should be added") + } +} diff --git a/images/virtualization-artifact/pkg/controller/vmchange/comparator_pod_placement.go b/images/virtualization-artifact/pkg/controller/vmchange/comparator_pod_placement.go index e6e6eff9d5..e1f58cf533 100644 --- a/images/virtualization-artifact/pkg/controller/vmchange/comparator_pod_placement.go +++ b/images/virtualization-artifact/pkg/controller/vmchange/comparator_pod_placement.go @@ -107,7 +107,15 @@ func compareNetworks(current, desired *v1alpha2.VirtualMachineSpec) []FieldChang func isOnlyNonMainNetworksChanged(current, desired []v1alpha2.NetworksSpec) bool { currentMain := getMainNetwork(current) desiredMain := getMainNetwork(desired) - return reflect.DeepEqual(currentMain, desiredMain) + currentHasMain := currentMain != nil || len(current) == 0 + desiredHasMain := desiredMain != nil || len(desired) == 0 + if !currentHasMain || !desiredHasMain { + return false + } + if currentMain == nil || desiredMain == nil { + return true + } + return reflect.DeepEqual(*currentMain, *desiredMain) } func getMainNetwork(networks []v1alpha2.NetworksSpec) *v1alpha2.NetworksSpec { diff --git a/images/virtualization-artifact/pkg/controller/vmchange/compare_test.go b/images/virtualization-artifact/pkg/controller/vmchange/compare_test.go index 2ab26e66a8..779c384924 100644 --- a/images/virtualization-artifact/pkg/controller/vmchange/compare_test.go +++ b/images/virtualization-artifact/pkg/controller/vmchange/compare_test.go @@ -369,6 +369,106 @@ networks: requirePathOperation("networks", ChangeReplace), ), }, + { + "apply immediate when adding non-main network to nil networks", + ``, + ` +networks: +- type: Main + id: 1 +- type: ClusterNetwork + name: additional + id: 2 +`, + assertChanges( + actionRequired(ActionApplyImmediate), + requirePathOperation("networks", ChangeAdd), + ), + }, + { + "apply immediate when adding non-main network to existing main", + ` +networks: +- type: Main + id: 1 +`, + ` +networks: +- type: Main + id: 1 +- type: ClusterNetwork + name: additional + id: 2 +`, + assertChanges( + actionRequired(ActionApplyImmediate), + requirePathOperation("networks", ChangeReplace), + ), + }, + { + "apply immediate when removing non-main network", + ` +networks: +- type: Main + id: 1 +- type: Network + name: net1 + id: 2 +`, + ` +networks: +- type: Main + id: 1 +`, + assertChanges( + actionRequired(ActionApplyImmediate), + requirePathOperation("networks", ChangeReplace), + ), + }, + { + "restart when main network is removed", + ` +networks: +- type: Main + id: 1 +- type: Network + name: net1 + id: 2 +`, + ` +networks: +- type: Network + name: net1 + id: 2 +`, + assertChanges( + actionRequired(ActionRestart), + requirePathOperation("networks", ChangeReplace), + ), + }, + { + "restart when main network id changes", + ` +networks: +- type: Main + id: 1 +- type: Network + name: net1 + id: 2 +`, + ` +networks: +- type: Main + id: 5 +- type: Network + name: net1 + id: 2 +`, + assertChanges( + actionRequired(ActionRestart), + requirePathOperation("networks", ChangeReplace), + ), + }, } for _, tt := range tests { From aeedb02c1f0a45955c799b18db79ecb47b87cf16 Mon Sep 17 00:00:00 2001 From: Daniil Loktev Date: Mon, 6 Apr 2026 17:47:08 +0300 Subject: [PATCH 04/12] wip Signed-off-by: Daniil Loktev --- .../pkg/controller/vm/internal/sync_kvvm.go | 25 ++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/images/virtualization-artifact/pkg/controller/vm/internal/sync_kvvm.go b/images/virtualization-artifact/pkg/controller/vm/internal/sync_kvvm.go index 3229bd7dec..3b6db2237c 100644 --- a/images/virtualization-artifact/pkg/controller/vm/internal/sync_kvvm.go +++ b/images/virtualization-artifact/pkg/controller/vm/internal/sync_kvvm.go @@ -745,17 +745,36 @@ func (h *SyncKvvmHandler) patchPodNetworkAnnotation(ctx context.Context, s state return nil } - patch := client.MergeFrom(pod.DeepCopy()) + podPatch := client.MergeFrom(pod.DeepCopy()) if pod.Annotations == nil { pod.Annotations = make(map[string]string) } pod.Annotations[annotations.AnnNetworksSpec] = networkConfigStr - if err := h.client.Patch(ctx, pod, patch); err != nil { + if err := h.client.Patch(ctx, pod, podPatch); err != nil { return fmt.Errorf("failed to patch pod %s network annotation: %w", pod.Name, err) } - log.Info("Patched pod network annotation", "pod", pod.Name, "networks", networkConfigStr) + + kvvmi, err := s.KVVMI(ctx) + if err != nil { + return err + } + if kvvmi == nil { + return nil + } + + vmiPatch := client.MergeFrom(kvvmi.DeepCopy()) + if kvvmi.Annotations == nil { + kvvmi.Annotations = make(map[string]string) + } + kvvmi.Annotations[annotations.AnnNetworksSpec] = networkConfigStr + + if err := h.client.Patch(ctx, kvvmi, vmiPatch); err != nil { + return fmt.Errorf("failed to patch VMI %s network annotation: %w", kvvmi.Name, err) + } + log.Info("Patched VMI network annotation", "vmi", kvvmi.Name, "networks", networkConfigStr) + return nil } From d16fed371c904130278f5d3800603824c2801d77 Mon Sep 17 00:00:00 2001 From: Daniil Loktev Date: Mon, 6 Apr 2026 18:17:48 +0300 Subject: [PATCH 05/12] wip Signed-off-by: Daniil Loktev --- .../pkg/controller/kvbuilder/kvvm_utils.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/images/virtualization-artifact/pkg/controller/kvbuilder/kvvm_utils.go b/images/virtualization-artifact/pkg/controller/kvbuilder/kvvm_utils.go index 732f502d68..6bfb27ca75 100644 --- a/images/virtualization-artifact/pkg/controller/kvbuilder/kvvm_utils.go +++ b/images/virtualization-artifact/pkg/controller/kvbuilder/kvvm_utils.go @@ -360,13 +360,18 @@ func setNetwork(kvvm *KVVM, networkSpec network.InterfaceSpecList) { desiredByName[n.InterfaceName] = struct{}{} } + existingByName := make(map[string]struct{}) for _, iface := range kvvm.Resource.Spec.Template.Spec.Domain.Devices.Interfaces { + existingByName[iface.Name] = struct{}{} if _, wanted := desiredByName[iface.Name]; !wanted { kvvm.SetNetworkInterfaceAbsent(iface.Name) } } for _, n := range networkSpec { + if _, exists := existingByName[n.InterfaceName]; exists { + continue + } kvvm.SetNetworkInterface(n.InterfaceName, n.MAC, n.ID) } } From e7371387793c24155ba4d1b62ba5b30078c66209 Mon Sep 17 00:00:00 2001 From: Daniil Loktev Date: Mon, 6 Apr 2026 18:54:55 +0300 Subject: [PATCH 06/12] wip Signed-off-by: Daniil Loktev --- .../pkg/controller/kvbuilder/kvvm_utils.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/images/virtualization-artifact/pkg/controller/kvbuilder/kvvm_utils.go b/images/virtualization-artifact/pkg/controller/kvbuilder/kvvm_utils.go index 6bfb27ca75..eb2ce6b4ca 100644 --- a/images/virtualization-artifact/pkg/controller/kvbuilder/kvvm_utils.go +++ b/images/virtualization-artifact/pkg/controller/kvbuilder/kvvm_utils.go @@ -372,6 +372,9 @@ func setNetwork(kvvm *KVVM, networkSpec network.InterfaceSpecList) { if _, exists := existingByName[n.InterfaceName]; exists { continue } + if n.InterfaceName == network.NameDefaultInterface { + continue + } kvvm.SetNetworkInterface(n.InterfaceName, n.MAC, n.ID) } } From afc481d7aecae9ffe191bc684da5dbe2c12978b8 Mon Sep 17 00:00:00 2001 From: Daniil Loktev Date: Mon, 6 Apr 2026 19:23:24 +0300 Subject: [PATCH 07/12] wip Signed-off-by: Daniil Loktev --- .../pkg/controller/kvbuilder/kvvm.go | 11 ++++++----- .../pkg/controller/kvbuilder/kvvm_utils.go | 3 --- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/images/virtualization-artifact/pkg/controller/kvbuilder/kvvm.go b/images/virtualization-artifact/pkg/controller/kvbuilder/kvvm.go index 9e58030663..8cd1e84a37 100644 --- a/images/virtualization-artifact/pkg/controller/kvbuilder/kvvm.go +++ b/images/virtualization-artifact/pkg/controller/kvbuilder/kvvm.go @@ -587,14 +587,15 @@ func (b *KVVM) SetNetworkInterface(name, macAddress string, acpiIndex int) { }, true, ) - devPreset := DeviceOptionsPresets.Find(b.opts.EnableParavirtualization) - iface := virtv1.Interface{ - Name: name, - Model: devPreset.InterfaceModel, - ACPIIndex: acpiIndex, + Name: name, } iface.Bridge = &virtv1.InterfaceBridge{} + if name != "default" { + devPreset := DeviceOptionsPresets.Find(b.opts.EnableParavirtualization) + iface.Model = devPreset.InterfaceModel + iface.ACPIIndex = acpiIndex + } if macAddress != "" { iface.MacAddress = macAddress } diff --git a/images/virtualization-artifact/pkg/controller/kvbuilder/kvvm_utils.go b/images/virtualization-artifact/pkg/controller/kvbuilder/kvvm_utils.go index eb2ce6b4ca..6bfb27ca75 100644 --- a/images/virtualization-artifact/pkg/controller/kvbuilder/kvvm_utils.go +++ b/images/virtualization-artifact/pkg/controller/kvbuilder/kvvm_utils.go @@ -372,9 +372,6 @@ func setNetwork(kvvm *KVVM, networkSpec network.InterfaceSpecList) { if _, exists := existingByName[n.InterfaceName]; exists { continue } - if n.InterfaceName == network.NameDefaultInterface { - continue - } kvvm.SetNetworkInterface(n.InterfaceName, n.MAC, n.ID) } } From 1bc8b68164a462921c5166ac4a6a54ba05134ce9 Mon Sep 17 00:00:00 2001 From: Daniil Loktev Date: Mon, 6 Apr 2026 19:53:43 +0300 Subject: [PATCH 08/12] wip Signed-off-by: Daniil Loktev --- .../pkg/controller/vm/internal/sync_kvvm.go | 33 ++++++++++++++++--- 1 file changed, 29 insertions(+), 4 deletions(-) diff --git a/images/virtualization-artifact/pkg/controller/vm/internal/sync_kvvm.go b/images/virtualization-artifact/pkg/controller/vm/internal/sync_kvvm.go index 3b6db2237c..f49ae126ea 100644 --- a/images/virtualization-artifact/pkg/controller/vm/internal/sync_kvvm.go +++ b/images/virtualization-artifact/pkg/controller/vm/internal/sync_kvvm.go @@ -654,14 +654,23 @@ func (h *SyncKvvmHandler) applyVMChangesToKVVM(ctx context.Context, s state.Virt h.recorder.Event(current, corev1.EventTypeNormal, v1alpha2.ReasonVMChangesApplied, message) log.Debug(message, "vm.name", current.GetName(), "changes", changes) - if err := h.updateKVVM(ctx, s); err != nil { - return fmt.Errorf("unable to update KVVM using new VM spec: %w", err) - } - if err := h.patchPodNetworkAnnotation(ctx, s); err != nil { return fmt.Errorf("unable to patch pod network annotation: %w", err) } + ready, err := h.isNetworkReadyOnPod(ctx, s) + if err != nil { + return fmt.Errorf("unable to check pod network status: %w", err) + } + if !ready { + log.Info("Waiting for SDN to configure network interfaces on the pod") + return nil + } + + if err := h.updateKVVM(ctx, s); err != nil { + return fmt.Errorf("unable to update KVVM using new VM spec: %w", err) + } + case vmchange.ActionNone: log.Info("No changes to underlying KVVM, update last-applied-spec annotation", "vm.name", current.GetName()) @@ -717,6 +726,22 @@ func (h *SyncKvvmHandler) isVMUnschedulable( return false } +func (h *SyncKvvmHandler) isNetworkReadyOnPod(ctx context.Context, s state.VirtualMachineState) (bool, error) { + pods, err := s.Pods(ctx) + if err != nil { + return false, err + } + if pods == nil || len(pods.Items) == 0 { + return false, nil + } + + errMsg, err := extractNetworkStatusFromPods(pods) + if err != nil { + return false, err + } + return errMsg == "", nil +} + func (h *SyncKvvmHandler) patchPodNetworkAnnotation(ctx context.Context, s state.VirtualMachineState) error { log := logger.FromContext(ctx) From b16d905f1a6e02021aa8efcd4741ea2224fbfc98 Mon Sep 17 00:00:00 2001 From: Daniil Loktev Date: Tue, 7 Apr 2026 16:44:20 +0300 Subject: [PATCH 09/12] wip Signed-off-by: Daniil Loktev --- .../pkg/common/network/types.go | 13 ++++--- .../pkg/controller/kvbuilder/kvvm.go | 3 +- .../pkg/controller/vm/internal/sync_kvvm.go | 39 ++++++++----------- .../vmchange/comparator_pod_placement.go | 17 +++----- 4 files changed, 33 insertions(+), 39 deletions(-) diff --git a/images/virtualization-artifact/pkg/common/network/types.go b/images/virtualization-artifact/pkg/common/network/types.go index 58d70914e5..2540b58db2 100644 --- a/images/virtualization-artifact/pkg/common/network/types.go +++ b/images/virtualization-artifact/pkg/common/network/types.go @@ -37,13 +37,16 @@ func HasMainNetworkStatus(networks []v1alpha2.NetworksStatus) bool { } func HasMainNetworkSpec(networks []v1alpha2.NetworksSpec) bool { - for _, network := range networks { - if network.Type == v1alpha2.NetworksTypeMain { - return true + return GetMainNetworkSpec(networks) != nil +} + +func GetMainNetworkSpec(networks []v1alpha2.NetworksSpec) *v1alpha2.NetworksSpec { + for i := range networks { + if networks[i].Type == v1alpha2.NetworksTypeMain { + return &networks[i] } } - - return false + return nil } type InterfaceSpec struct { diff --git a/images/virtualization-artifact/pkg/controller/kvbuilder/kvvm.go b/images/virtualization-artifact/pkg/controller/kvbuilder/kvvm.go index 8cd1e84a37..2e20bdb582 100644 --- a/images/virtualization-artifact/pkg/controller/kvbuilder/kvvm.go +++ b/images/virtualization-artifact/pkg/controller/kvbuilder/kvvm.go @@ -31,6 +31,7 @@ import ( "github.com/deckhouse/virtualization-controller/pkg/common" "github.com/deckhouse/virtualization-controller/pkg/common/array" + "github.com/deckhouse/virtualization-controller/pkg/common/network" "github.com/deckhouse/virtualization-controller/pkg/common/resource_builder" "github.com/deckhouse/virtualization-controller/pkg/common/vm" "github.com/deckhouse/virtualization/api/core/v1alpha2" @@ -591,7 +592,7 @@ func (b *KVVM) SetNetworkInterface(name, macAddress string, acpiIndex int) { Name: name, } iface.Bridge = &virtv1.InterfaceBridge{} - if name != "default" { + if name != network.NameDefaultInterface { devPreset := DeviceOptionsPresets.Find(b.opts.EnableParavirtualization) iface.Model = devPreset.InterfaceModel iface.ACPIIndex = acpiIndex diff --git a/images/virtualization-artifact/pkg/controller/vm/internal/sync_kvvm.go b/images/virtualization-artifact/pkg/controller/vm/internal/sync_kvvm.go index f49ae126ea..30ab380a79 100644 --- a/images/virtualization-artifact/pkg/controller/vm/internal/sync_kvvm.go +++ b/images/virtualization-artifact/pkg/controller/vm/internal/sync_kvvm.go @@ -734,7 +734,6 @@ func (h *SyncKvvmHandler) isNetworkReadyOnPod(ctx context.Context, s state.Virtu if pods == nil || len(pods.Items) == 0 { return false, nil } - errMsg, err := extractNetworkStatusFromPods(pods) if err != nil { return false, err @@ -759,24 +758,12 @@ func (h *SyncKvvmHandler) patchPodNetworkAnnotation(ctx context.Context, s state return err } - networkSpec := network.CreateNetworkSpec(current, vmmacs) - networkConfigStr, err := networkSpec.ToString() + networkConfigStr, err := network.CreateNetworkSpec(current, vmmacs).ToString() if err != nil { return fmt.Errorf("failed to serialize network spec: %w", err) } - currentAnnotation := pod.Annotations[annotations.AnnNetworksSpec] - if currentAnnotation == networkConfigStr { - return nil - } - - podPatch := client.MergeFrom(pod.DeepCopy()) - if pod.Annotations == nil { - pod.Annotations = make(map[string]string) - } - pod.Annotations[annotations.AnnNetworksSpec] = networkConfigStr - - if err := h.client.Patch(ctx, pod, podPatch); err != nil { + if err := h.patchAnnotationIfChanged(ctx, pod, annotations.AnnNetworksSpec, networkConfigStr); err != nil { return fmt.Errorf("failed to patch pod %s network annotation: %w", pod.Name, err) } log.Info("Patched pod network annotation", "pod", pod.Name, "networks", networkConfigStr) @@ -789,13 +776,7 @@ func (h *SyncKvvmHandler) patchPodNetworkAnnotation(ctx context.Context, s state return nil } - vmiPatch := client.MergeFrom(kvvmi.DeepCopy()) - if kvvmi.Annotations == nil { - kvvmi.Annotations = make(map[string]string) - } - kvvmi.Annotations[annotations.AnnNetworksSpec] = networkConfigStr - - if err := h.client.Patch(ctx, kvvmi, vmiPatch); err != nil { + if err := h.patchAnnotationIfChanged(ctx, kvvmi, annotations.AnnNetworksSpec, networkConfigStr); err != nil { return fmt.Errorf("failed to patch VMI %s network annotation: %w", kvvmi.Name, err) } log.Info("Patched VMI network annotation", "vmi", kvvmi.Name, "networks", networkConfigStr) @@ -803,6 +784,20 @@ func (h *SyncKvvmHandler) patchPodNetworkAnnotation(ctx context.Context, s state return nil } +func (h *SyncKvvmHandler) patchAnnotationIfChanged(ctx context.Context, obj client.Object, key, value string) error { + if obj.GetAnnotations()[key] == value { + return nil + } + patch := client.MergeFrom(obj.DeepCopyObject().(client.Object)) + anns := obj.GetAnnotations() + if anns == nil { + anns = make(map[string]string) + } + anns[key] = value + obj.SetAnnotations(anns) + return h.client.Patch(ctx, obj, patch) +} + // isPlacementPolicyChanged returns true if any of the Affinity, NodePlacement, or Toleration rules have changed. func (h *SyncKvvmHandler) isPlacementPolicyChanged(allChanges vmchange.SpecChanges) bool { for _, c := range allChanges.GetAll() { diff --git a/images/virtualization-artifact/pkg/controller/vmchange/comparator_pod_placement.go b/images/virtualization-artifact/pkg/controller/vmchange/comparator_pod_placement.go index e1f58cf533..b8f8de9b7c 100644 --- a/images/virtualization-artifact/pkg/controller/vmchange/comparator_pod_placement.go +++ b/images/virtualization-artifact/pkg/controller/vmchange/comparator_pod_placement.go @@ -19,6 +19,7 @@ package vmchange import ( "reflect" + "github.com/deckhouse/virtualization-controller/pkg/common/network" "github.com/deckhouse/virtualization/api/core/v1alpha2" ) @@ -104,9 +105,12 @@ func compareNetworks(current, desired *v1alpha2.VirtualMachineSpec) []FieldChang ) } +// isOnlyNonMainNetworksChanged returns true when the Main network is unchanged +// between current and desired (so only non-Main networks differ). +// Empty networks list is equivalent to having an implicit default Main. func isOnlyNonMainNetworksChanged(current, desired []v1alpha2.NetworksSpec) bool { - currentMain := getMainNetwork(current) - desiredMain := getMainNetwork(desired) + currentMain := network.GetMainNetworkSpec(current) + desiredMain := network.GetMainNetworkSpec(desired) currentHasMain := currentMain != nil || len(current) == 0 desiredHasMain := desiredMain != nil || len(desired) == 0 if !currentHasMain || !desiredHasMain { @@ -118,15 +122,6 @@ func isOnlyNonMainNetworksChanged(current, desired []v1alpha2.NetworksSpec) bool return reflect.DeepEqual(*currentMain, *desiredMain) } -func getMainNetwork(networks []v1alpha2.NetworksSpec) *v1alpha2.NetworksSpec { - for i := range networks { - if networks[i].Type == v1alpha2.NetworksTypeMain { - return &networks[i] - } - } - return nil -} - func isOnlyNetworkIDAutofillChange(current, desired []v1alpha2.NetworksSpec) bool { if len(current) != len(desired) { return false From 93695f2e1ca07c3862183309b4ac6d961b1c2db6 Mon Sep 17 00:00:00 2001 From: Daniil Loktev Date: Tue, 7 Apr 2026 17:27:49 +0300 Subject: [PATCH 10/12] wip Signed-off-by: Daniil Loktev --- .../pkg/controller/kvbuilder/kvvm_utils.go | 5 -- .../pkg/controller/vm/internal/sync_kvvm.go | 69 +++++++++---------- 2 files changed, 32 insertions(+), 42 deletions(-) diff --git a/images/virtualization-artifact/pkg/controller/kvbuilder/kvvm_utils.go b/images/virtualization-artifact/pkg/controller/kvbuilder/kvvm_utils.go index 6bfb27ca75..732f502d68 100644 --- a/images/virtualization-artifact/pkg/controller/kvbuilder/kvvm_utils.go +++ b/images/virtualization-artifact/pkg/controller/kvbuilder/kvvm_utils.go @@ -360,18 +360,13 @@ func setNetwork(kvvm *KVVM, networkSpec network.InterfaceSpecList) { desiredByName[n.InterfaceName] = struct{}{} } - existingByName := make(map[string]struct{}) for _, iface := range kvvm.Resource.Spec.Template.Spec.Domain.Devices.Interfaces { - existingByName[iface.Name] = struct{}{} if _, wanted := desiredByName[iface.Name]; !wanted { kvvm.SetNetworkInterfaceAbsent(iface.Name) } } for _, n := range networkSpec { - if _, exists := existingByName[n.InterfaceName]; exists { - continue - } kvvm.SetNetworkInterface(n.InterfaceName, n.MAC, n.ID) } } diff --git a/images/virtualization-artifact/pkg/controller/vm/internal/sync_kvvm.go b/images/virtualization-artifact/pkg/controller/vm/internal/sync_kvvm.go index 30ab380a79..1c49703f4e 100644 --- a/images/virtualization-artifact/pkg/controller/vm/internal/sync_kvvm.go +++ b/images/virtualization-artifact/pkg/controller/vm/internal/sync_kvvm.go @@ -654,17 +654,21 @@ func (h *SyncKvvmHandler) applyVMChangesToKVVM(ctx context.Context, s state.Virt h.recorder.Event(current, corev1.EventTypeNormal, v1alpha2.ReasonVMChangesApplied, message) log.Debug(message, "vm.name", current.GetName(), "changes", changes) - if err := h.patchPodNetworkAnnotation(ctx, s); err != nil { - return fmt.Errorf("unable to patch pod network annotation: %w", err) - } + if hasNetworkChange(changes) { + if err := h.patchPodNetworkAnnotation(ctx, s); err != nil { + return fmt.Errorf("unable to patch pod network annotation: %w", err) + } - ready, err := h.isNetworkReadyOnPod(ctx, s) - if err != nil { - return fmt.Errorf("unable to check pod network status: %w", err) - } - if !ready { - log.Info("Waiting for SDN to configure network interfaces on the pod") - return nil + ready, err := h.isNetworkReadyOnPod(ctx, s) + if err != nil { + return fmt.Errorf("unable to check pod network status: %w", err) + } + if !ready { + msg := "Waiting for SDN to configure network interfaces on the pod" + log.Info(msg) + h.recorder.Event(current, corev1.EventTypeNormal, v1alpha2.ReasonVMChangesApplied, msg) + return nil + } } if err := h.updateKVVM(ctx, s); err != nil { @@ -726,6 +730,15 @@ func (h *SyncKvvmHandler) isVMUnschedulable( return false } +func hasNetworkChange(changes vmchange.SpecChanges) bool { + for _, c := range changes.GetAll() { + if c.Path == "networks" { + return true + } + } + return false +} + func (h *SyncKvvmHandler) isNetworkReadyOnPod(ctx context.Context, s state.VirtualMachineState) (bool, error) { pods, err := s.Pods(ctx) if err != nil { @@ -763,41 +776,23 @@ func (h *SyncKvvmHandler) patchPodNetworkAnnotation(ctx context.Context, s state return fmt.Errorf("failed to serialize network spec: %w", err) } - if err := h.patchAnnotationIfChanged(ctx, pod, annotations.AnnNetworksSpec, networkConfigStr); err != nil { - return fmt.Errorf("failed to patch pod %s network annotation: %w", pod.Name, err) - } - log.Info("Patched pod network annotation", "pod", pod.Name, "networks", networkConfigStr) - - kvvmi, err := s.KVVMI(ctx) - if err != nil { - return err - } - if kvvmi == nil { + if pod.Annotations[annotations.AnnNetworksSpec] == networkConfigStr { return nil } - if err := h.patchAnnotationIfChanged(ctx, kvvmi, annotations.AnnNetworksSpec, networkConfigStr); err != nil { - return fmt.Errorf("failed to patch VMI %s network annotation: %w", kvvmi.Name, err) + patch := client.MergeFrom(pod.DeepCopy()) + if pod.Annotations == nil { + pod.Annotations = make(map[string]string) + } + pod.Annotations[annotations.AnnNetworksSpec] = networkConfigStr + if err := h.client.Patch(ctx, pod, patch); err != nil { + return fmt.Errorf("failed to patch pod %s network annotation: %w", pod.Name, err) } - log.Info("Patched VMI network annotation", "vmi", kvvmi.Name, "networks", networkConfigStr) + log.Info("Patched pod network annotation", "pod", pod.Name, "networks", networkConfigStr) return nil } -func (h *SyncKvvmHandler) patchAnnotationIfChanged(ctx context.Context, obj client.Object, key, value string) error { - if obj.GetAnnotations()[key] == value { - return nil - } - patch := client.MergeFrom(obj.DeepCopyObject().(client.Object)) - anns := obj.GetAnnotations() - if anns == nil { - anns = make(map[string]string) - } - anns[key] = value - obj.SetAnnotations(anns) - return h.client.Patch(ctx, obj, patch) -} - // isPlacementPolicyChanged returns true if any of the Affinity, NodePlacement, or Toleration rules have changed. func (h *SyncKvvmHandler) isPlacementPolicyChanged(allChanges vmchange.SpecChanges) bool { for _, c := range allChanges.GetAll() { From 745bc07d292718dce12e347e23e9a795c3ce5c01 Mon Sep 17 00:00:00 2001 From: Daniil Loktev Date: Wed, 8 Apr 2026 12:48:37 +0300 Subject: [PATCH 11/12] wip Signed-off-by: Daniil Loktev --- .../pkg/common/network/spec.go | 8 ++++++++ .../pkg/controller/kvbuilder/kvvm.go | 12 +++++------- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/images/virtualization-artifact/pkg/common/network/spec.go b/images/virtualization-artifact/pkg/common/network/spec.go index 4d1976cbc9..176fb7340f 100644 --- a/images/virtualization-artifact/pkg/common/network/spec.go +++ b/images/virtualization-artifact/pkg/common/network/spec.go @@ -30,6 +30,14 @@ func CreateNetworkSpec(vm *v1alpha2.VirtualMachine, vmmacs []*v1alpha2.VirtualMa macPool := NewMacAddressPool(vm, vmmacs) var specs InterfaceSpecList + if len(vm.Spec.Networks) == 0 { + specs = append(specs, createMainInterfaceSpec(v1alpha2.NetworksSpec{ + Type: v1alpha2.NetworksTypeMain, + ID: ptr.To(ReservedMainID), + })) + return specs + } + for _, net := range vm.Spec.Networks { if net.Type == v1alpha2.NetworksTypeMain { specs = append(specs, createMainInterfaceSpec(net)) diff --git a/images/virtualization-artifact/pkg/controller/kvbuilder/kvvm.go b/images/virtualization-artifact/pkg/controller/kvbuilder/kvvm.go index 2e20bdb582..9e58030663 100644 --- a/images/virtualization-artifact/pkg/controller/kvbuilder/kvvm.go +++ b/images/virtualization-artifact/pkg/controller/kvbuilder/kvvm.go @@ -31,7 +31,6 @@ import ( "github.com/deckhouse/virtualization-controller/pkg/common" "github.com/deckhouse/virtualization-controller/pkg/common/array" - "github.com/deckhouse/virtualization-controller/pkg/common/network" "github.com/deckhouse/virtualization-controller/pkg/common/resource_builder" "github.com/deckhouse/virtualization-controller/pkg/common/vm" "github.com/deckhouse/virtualization/api/core/v1alpha2" @@ -588,15 +587,14 @@ func (b *KVVM) SetNetworkInterface(name, macAddress string, acpiIndex int) { }, true, ) + devPreset := DeviceOptionsPresets.Find(b.opts.EnableParavirtualization) + iface := virtv1.Interface{ - Name: name, + Name: name, + Model: devPreset.InterfaceModel, + ACPIIndex: acpiIndex, } iface.Bridge = &virtv1.InterfaceBridge{} - if name != network.NameDefaultInterface { - devPreset := DeviceOptionsPresets.Find(b.opts.EnableParavirtualization) - iface.Model = devPreset.InterfaceModel - iface.ACPIIndex = acpiIndex - } if macAddress != "" { iface.MacAddress = macAddress } From 6e43337b54f290b42b305ca6da1c1f4ba12217f6 Mon Sep 17 00:00:00 2001 From: Daniil Loktev Date: Thu, 9 Apr 2026 10:31:55 +0300 Subject: [PATCH 12/12] wip Signed-off-by: Daniil Loktev --- images/virt-artifact/werf.inc.yaml | 2 -- images/virt-controller/werf.inc.yaml | 1 - 2 files changed, 3 deletions(-) diff --git a/images/virt-artifact/werf.inc.yaml b/images/virt-artifact/werf.inc.yaml index af8f96c8be..f30560fba6 100644 --- a/images/virt-artifact/werf.inc.yaml +++ b/images/virt-artifact/werf.inc.yaml @@ -8,7 +8,6 @@ --- image: {{ .ModuleNamePrefix }}{{ .ImageName }}-src-artifact final: false -fromCacheVersion: "{{ now | date "Mon Jan 2 15:04:05 MST 2006" }}" fromImage: builder/src secrets: - id: SOURCE_REPO @@ -44,7 +43,6 @@ packages: image: {{ .ModuleNamePrefix }}{{ .ImageName }} final: false -fromCacheVersion: "{{ now | date "Mon Jan 2 15:04:05 MST 2006" }}" fromImage: {{ eq $.SVACE_ENABLED "false" | ternary "builder/golang-alt-1.25" "builder/golang-alt-svace-1.25" }} mount: - fromPath: ~/go-pkg-cache diff --git a/images/virt-controller/werf.inc.yaml b/images/virt-controller/werf.inc.yaml index d74b7eaf76..c1f7113305 100644 --- a/images/virt-controller/werf.inc.yaml +++ b/images/virt-controller/werf.inc.yaml @@ -1,6 +1,5 @@ --- image: {{ .ModuleNamePrefix }}{{ .ImageName }} -fromCacheVersion: "{{ now | date "Mon Jan 2 15:04:05 MST 2006" }}" fromImage: {{ .ModuleNamePrefix }}distroless git: {{- include "image mount points" . }}