From 32c527071f866dcedef534d3bd48ce336fea44df Mon Sep 17 00:00:00 2001 From: Valeriy Khorunzhin Date: Tue, 3 Mar 2026 10:58:58 +0300 Subject: [PATCH 1/5] fix Signed-off-by: Valeriy Khorunzhin --- .../pkg/controller/vmip/internal/attached_handler.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/images/virtualization-artifact/pkg/controller/vmip/internal/attached_handler.go b/images/virtualization-artifact/pkg/controller/vmip/internal/attached_handler.go index 0bd413ddb1..fb132b5c44 100644 --- a/images/virtualization-artifact/pkg/controller/vmip/internal/attached_handler.go +++ b/images/virtualization-artifact/pkg/controller/vmip/internal/attached_handler.go @@ -125,6 +125,10 @@ func (h *AttachedHandler) getAttachedVirtualMachine(ctx context.Context, vmip *v } if attachedVM != nil { + // A VM that is being deleted is not considered attached so the VMIP can be released and, if managed, removed (e.g. during cascade deletion). + if !attachedVM.GetDeletionTimestamp().IsZero() { + return nil, nil + } if network.HasMainNetworkStatus(attachedVM.Status.Networks) { return attachedVM, nil } From 46a9e8f4c998aa1853005b19f381d0b36df56668 Mon Sep 17 00:00:00 2001 From: Valeriy Khorunzhin Date: Wed, 4 Mar 2026 17:51:27 +0300 Subject: [PATCH 2/5] watch kvvm Signed-off-by: Valeriy Khorunzhin --- .../vmip/internal/attached_handler.go | 8 +- .../vmip/internal/watcher/kvvm_watcher.go | 95 +++++++++++++++++++ .../pkg/controller/vmip/vmip_reconciler.go | 1 + 3 files changed, 103 insertions(+), 1 deletion(-) create mode 100644 images/virtualization-artifact/pkg/controller/vmip/internal/watcher/kvvm_watcher.go diff --git a/images/virtualization-artifact/pkg/controller/vmip/internal/attached_handler.go b/images/virtualization-artifact/pkg/controller/vmip/internal/attached_handler.go index fb132b5c44..46e1b1a5ce 100644 --- a/images/virtualization-artifact/pkg/controller/vmip/internal/attached_handler.go +++ b/images/virtualization-artifact/pkg/controller/vmip/internal/attached_handler.go @@ -23,6 +23,7 @@ import ( corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" + virtv1 "kubevirt.io/api/core/v1" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/reconcile" @@ -125,8 +126,13 @@ func (h *AttachedHandler) getAttachedVirtualMachine(ctx context.Context, vmip *v } if attachedVM != nil { + kvvm, err := object.FetchObject(ctx, types.NamespacedName{Name: attachedVM.Name, Namespace: attachedVM.Namespace}, h.client, &virtv1.VirtualMachine{}) + if err != nil { + return nil, err + } + // A VM that is being deleted is not considered attached so the VMIP can be released and, if managed, removed (e.g. during cascade deletion). - if !attachedVM.GetDeletionTimestamp().IsZero() { + if !attachedVM.GetDeletionTimestamp().IsZero() && kvvm == nil { return nil, nil } if network.HasMainNetworkStatus(attachedVM.Status.Networks) { diff --git a/images/virtualization-artifact/pkg/controller/vmip/internal/watcher/kvvm_watcher.go b/images/virtualization-artifact/pkg/controller/vmip/internal/watcher/kvvm_watcher.go new file mode 100644 index 0000000000..fc17fdb30f --- /dev/null +++ b/images/virtualization-artifact/pkg/controller/vmip/internal/watcher/kvvm_watcher.go @@ -0,0 +1,95 @@ +/* +Copyright 2026 Flant JSC + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package watcher + +import ( + "context" + "fmt" + "strings" + + "k8s.io/apimachinery/pkg/types" + virtv1 "kubevirt.io/api/core/v1" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller" + "sigs.k8s.io/controller-runtime/pkg/event" + "sigs.k8s.io/controller-runtime/pkg/handler" + "sigs.k8s.io/controller-runtime/pkg/manager" + "sigs.k8s.io/controller-runtime/pkg/predicate" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + "sigs.k8s.io/controller-runtime/pkg/source" + + "github.com/deckhouse/deckhouse/pkg/log" + "github.com/deckhouse/virtualization-controller/pkg/controller/indexer" + "github.com/deckhouse/virtualization/api/core/v1alpha2" +) + +func NewKVVMWatcher(client client.Client) *KVVMWatcher { + return &KVVMWatcher{ + client: client, + logger: log.Default().With("watcher", strings.ToLower(v1alpha2.VirtualMachineKind)), + } +} + +type KVVMWatcher struct { + client client.Client + logger *log.Logger +} + +func (w *KVVMWatcher) Watch(mgr manager.Manager, ctr controller.Controller) error { + if err := ctr.Watch( + source.Kind( + mgr.GetCache(), + &virtv1.VirtualMachine{}, + handler.TypedEnqueueRequestsFromMapFunc(w.enqueueRequests), + predicate.TypedFuncs[*virtv1.VirtualMachine]{ + CreateFunc: func(tce event.TypedCreateEvent[*virtv1.VirtualMachine]) bool { return false }, + UpdateFunc: func(tue event.TypedUpdateEvent[*virtv1.VirtualMachine]) bool { return false }, + }, + ), + ); err != nil { + return fmt.Errorf("error setting watch on VirtualMachine: %w", err) + } + return nil +} + +func (w KVVMWatcher) enqueueRequests(ctx context.Context, kvvm *virtv1.VirtualMachine) []reconcile.Request { + var requests []reconcile.Request + + vmipNames := make(map[string]struct{}) + + vmips := &v1alpha2.VirtualMachineIPAddressList{} + err := w.client.List(ctx, vmips, client.InNamespace(kvvm.Namespace), &client.MatchingFields{ + indexer.IndexFieldVMIPByVM: kvvm.Name, + }) + if err != nil { + w.logger.Error(fmt.Sprintf("failed to list vmips: %s", err)) + return nil + } + + for _, vmip := range vmips.Items { + vmipNames[vmip.Name] = struct{}{} + } + + for vmipName := range vmipNames { + requests = append(requests, reconcile.Request{NamespacedName: types.NamespacedName{ + Namespace: kvvm.Namespace, + Name: vmipName, + }}) + } + + return requests +} diff --git a/images/virtualization-artifact/pkg/controller/vmip/vmip_reconciler.go b/images/virtualization-artifact/pkg/controller/vmip/vmip_reconciler.go index f05cec1d7b..daec935c8a 100644 --- a/images/virtualization-artifact/pkg/controller/vmip/vmip_reconciler.go +++ b/images/virtualization-artifact/pkg/controller/vmip/vmip_reconciler.go @@ -60,6 +60,7 @@ func (r *Reconciler) SetupController(_ context.Context, mgr manager.Manager, ctr watcher.NewVirtualMachineIPAddressWatcher(), watcher.NewVirtualMachineIPAddressLeaseWatcher(mgr.GetClient()), watcher.NewVirtualMachineWatcher(mgr.GetClient()), + watcher.NewKVVMWatcher(mgr.GetClient()), } { err := w.Watch(mgr, ctr) if err != nil { From 95c147f524c6c4551c17783a7b4af89e43918dfe Mon Sep 17 00:00:00 2001 From: Valeriy Khorunzhin Date: Wed, 4 Mar 2026 18:01:18 +0300 Subject: [PATCH 3/5] fix logger init Signed-off-by: Valeriy Khorunzhin --- .../pkg/controller/vmip/internal/watcher/kvvm_watcher.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/images/virtualization-artifact/pkg/controller/vmip/internal/watcher/kvvm_watcher.go b/images/virtualization-artifact/pkg/controller/vmip/internal/watcher/kvvm_watcher.go index fc17fdb30f..ed4f91bd64 100644 --- a/images/virtualization-artifact/pkg/controller/vmip/internal/watcher/kvvm_watcher.go +++ b/images/virtualization-artifact/pkg/controller/vmip/internal/watcher/kvvm_watcher.go @@ -40,7 +40,7 @@ import ( func NewKVVMWatcher(client client.Client) *KVVMWatcher { return &KVVMWatcher{ client: client, - logger: log.Default().With("watcher", strings.ToLower(v1alpha2.VirtualMachineKind)), + logger: log.Default().With("watcher", strings.ToLower(virtv1.VirtualMachineGroupVersionKind.Kind)), } } From 7a01a8e1a7bdc2be79001c9f0250472fb76dde66 Mon Sep 17 00:00:00 2001 From: Valeriy Khorunzhin Date: Wed, 4 Mar 2026 18:53:55 +0300 Subject: [PATCH 4/5] not kvvm Signed-off-by: Valeriy Khorunzhin --- .../vmip/internal/attached_handler.go | 8 +- .../vmip/internal/watcher/kvvm_watcher.go | 95 ------------------- .../pkg/controller/vmip/vmip_reconciler.go | 1 - 3 files changed, 1 insertion(+), 103 deletions(-) delete mode 100644 images/virtualization-artifact/pkg/controller/vmip/internal/watcher/kvvm_watcher.go diff --git a/images/virtualization-artifact/pkg/controller/vmip/internal/attached_handler.go b/images/virtualization-artifact/pkg/controller/vmip/internal/attached_handler.go index 46e1b1a5ce..4d67d7d9f6 100644 --- a/images/virtualization-artifact/pkg/controller/vmip/internal/attached_handler.go +++ b/images/virtualization-artifact/pkg/controller/vmip/internal/attached_handler.go @@ -23,7 +23,6 @@ import ( corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" - virtv1 "kubevirt.io/api/core/v1" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/reconcile" @@ -126,13 +125,8 @@ func (h *AttachedHandler) getAttachedVirtualMachine(ctx context.Context, vmip *v } if attachedVM != nil { - kvvm, err := object.FetchObject(ctx, types.NamespacedName{Name: attachedVM.Name, Namespace: attachedVM.Namespace}, h.client, &virtv1.VirtualMachine{}) - if err != nil { - return nil, err - } - // A VM that is being deleted is not considered attached so the VMIP can be released and, if managed, removed (e.g. during cascade deletion). - if !attachedVM.GetDeletionTimestamp().IsZero() && kvvm == nil { + if !attachedVM.GetDeletionTimestamp().IsZero() && len(attachedVM.Status.VirtualMachinePods) == 0 { return nil, nil } if network.HasMainNetworkStatus(attachedVM.Status.Networks) { diff --git a/images/virtualization-artifact/pkg/controller/vmip/internal/watcher/kvvm_watcher.go b/images/virtualization-artifact/pkg/controller/vmip/internal/watcher/kvvm_watcher.go deleted file mode 100644 index ed4f91bd64..0000000000 --- a/images/virtualization-artifact/pkg/controller/vmip/internal/watcher/kvvm_watcher.go +++ /dev/null @@ -1,95 +0,0 @@ -/* -Copyright 2026 Flant JSC - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package watcher - -import ( - "context" - "fmt" - "strings" - - "k8s.io/apimachinery/pkg/types" - virtv1 "kubevirt.io/api/core/v1" - "sigs.k8s.io/controller-runtime/pkg/client" - "sigs.k8s.io/controller-runtime/pkg/controller" - "sigs.k8s.io/controller-runtime/pkg/event" - "sigs.k8s.io/controller-runtime/pkg/handler" - "sigs.k8s.io/controller-runtime/pkg/manager" - "sigs.k8s.io/controller-runtime/pkg/predicate" - "sigs.k8s.io/controller-runtime/pkg/reconcile" - "sigs.k8s.io/controller-runtime/pkg/source" - - "github.com/deckhouse/deckhouse/pkg/log" - "github.com/deckhouse/virtualization-controller/pkg/controller/indexer" - "github.com/deckhouse/virtualization/api/core/v1alpha2" -) - -func NewKVVMWatcher(client client.Client) *KVVMWatcher { - return &KVVMWatcher{ - client: client, - logger: log.Default().With("watcher", strings.ToLower(virtv1.VirtualMachineGroupVersionKind.Kind)), - } -} - -type KVVMWatcher struct { - client client.Client - logger *log.Logger -} - -func (w *KVVMWatcher) Watch(mgr manager.Manager, ctr controller.Controller) error { - if err := ctr.Watch( - source.Kind( - mgr.GetCache(), - &virtv1.VirtualMachine{}, - handler.TypedEnqueueRequestsFromMapFunc(w.enqueueRequests), - predicate.TypedFuncs[*virtv1.VirtualMachine]{ - CreateFunc: func(tce event.TypedCreateEvent[*virtv1.VirtualMachine]) bool { return false }, - UpdateFunc: func(tue event.TypedUpdateEvent[*virtv1.VirtualMachine]) bool { return false }, - }, - ), - ); err != nil { - return fmt.Errorf("error setting watch on VirtualMachine: %w", err) - } - return nil -} - -func (w KVVMWatcher) enqueueRequests(ctx context.Context, kvvm *virtv1.VirtualMachine) []reconcile.Request { - var requests []reconcile.Request - - vmipNames := make(map[string]struct{}) - - vmips := &v1alpha2.VirtualMachineIPAddressList{} - err := w.client.List(ctx, vmips, client.InNamespace(kvvm.Namespace), &client.MatchingFields{ - indexer.IndexFieldVMIPByVM: kvvm.Name, - }) - if err != nil { - w.logger.Error(fmt.Sprintf("failed to list vmips: %s", err)) - return nil - } - - for _, vmip := range vmips.Items { - vmipNames[vmip.Name] = struct{}{} - } - - for vmipName := range vmipNames { - requests = append(requests, reconcile.Request{NamespacedName: types.NamespacedName{ - Namespace: kvvm.Namespace, - Name: vmipName, - }}) - } - - return requests -} diff --git a/images/virtualization-artifact/pkg/controller/vmip/vmip_reconciler.go b/images/virtualization-artifact/pkg/controller/vmip/vmip_reconciler.go index daec935c8a..f05cec1d7b 100644 --- a/images/virtualization-artifact/pkg/controller/vmip/vmip_reconciler.go +++ b/images/virtualization-artifact/pkg/controller/vmip/vmip_reconciler.go @@ -60,7 +60,6 @@ func (r *Reconciler) SetupController(_ context.Context, mgr manager.Manager, ctr watcher.NewVirtualMachineIPAddressWatcher(), watcher.NewVirtualMachineIPAddressLeaseWatcher(mgr.GetClient()), watcher.NewVirtualMachineWatcher(mgr.GetClient()), - watcher.NewKVVMWatcher(mgr.GetClient()), } { err := w.Watch(mgr, ctr) if err != nil { From 2deaefec90b18fcf20843b10a39a04afd4db48a4 Mon Sep 17 00:00:00 2001 From: Valeriy Khorunzhin Date: Thu, 5 Mar 2026 01:08:08 +0300 Subject: [PATCH 5/5] update vm watcher Signed-off-by: Valeriy Khorunzhin --- .../pkg/controller/vmip/internal/watcher/vm_watcher.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/images/virtualization-artifact/pkg/controller/vmip/internal/watcher/vm_watcher.go b/images/virtualization-artifact/pkg/controller/vmip/internal/watcher/vm_watcher.go index 49f9fb1231..ce3d7e9bf4 100644 --- a/images/virtualization-artifact/pkg/controller/vmip/internal/watcher/vm_watcher.go +++ b/images/virtualization-artifact/pkg/controller/vmip/internal/watcher/vm_watcher.go @@ -71,6 +71,10 @@ func (w VirtualMachineWatcher) Watch(mgr manager.Manager, ctr controller.Control return true } + if len(oldVM.Status.VirtualMachinePods) != len(newVM.Status.VirtualMachinePods) { + return true + } + return false }, },