From a2b41011e05e3bca02995be902f21294dfa0d5f5 Mon Sep 17 00:00:00 2001 From: Valeriy Khorunzhin Date: Mon, 24 Mar 2025 11:06:23 +0300 Subject: [PATCH 01/11] feat Signed-off-by: Valeriy Khorunzhin fix merge conflicts Signed-off-by: Valeriy Khorunzhin fix Signed-off-by: Valeriy Khorunzhin refactoring Signed-off-by: Valeriy Khorunzhin remove adding finalizers in deleteion handlers Signed-off-by: Valeriy Khorunzhin suite_test Signed-off-by: Valeriy Khorunzhin refactoring tests Signed-off-by: Valeriy Khorunzhin use test context Signed-off-by: Valeriy Khorunzhin refactoring Signed-off-by: Valeriy Khorunzhin resolve Signed-off-by: Valeriy Khorunzhin remove useless Signed-off-by: Valeriy Khorunzhin fix vi lifecycle Signed-off-by: Valeriy Khorunzhin fix tests Signed-off-by: Valeriy Khorunzhin refactoring Signed-off-by: Valeriy Khorunzhin refactoring Signed-off-by: Valeriy Khorunzhin refactoring Signed-off-by: Valeriy Khorunzhin fmt Signed-off-by: Valeriy Khorunzhin clear legacy watchers Signed-off-by: Valeriy Khorunzhin refactoring Signed-off-by: Valeriy Khorunzhin fix empty ns Signed-off-by: Valeriy Khorunzhin vi watchers reshuffle Signed-off-by: Valeriy Khorunzhin refactorig cvi watchers Signed-off-by: Valeriy Khorunzhin fix Signed-off-by: Valeriy Khorunzhin oops Signed-off-by: Valeriy Khorunzhin fix Signed-off-by: Valeriy Khorunzhin fix phase check Signed-off-by: Valeriy Khorunzhin vmbda Signed-off-by: Valeriy Khorunzhin ignore stopped Signed-off-by: Valeriy Khorunzhin fix watchers Signed-off-by: Valeriy Khorunzhin fix panic Signed-off-by: Valeriy Khorunzhin fix vmbda termination Signed-off-by: Valeriy Khorunzhin refactoring Signed-off-by: Valeriy Khorunzhin refactoring Signed-off-by: Valeriy Khorunzhin refactoring Signed-off-by: Valeriy Khorunzhin refactoring Signed-off-by: Valeriy Khorunzhin up go version dockerfile Signed-off-by: Valeriy Khorunzhin remove unused Signed-off-by: Valeriy Khorunzhin fix deletion check Signed-off-by: Valeriy Khorunzhin why we trigger Signed-off-by: Valeriy Khorunzhin inuse handlers refactoring Signed-off-by: Valeriy Khorunzhin remove attache handlers Signed-off-by: Valeriy Khorunzhin fix generation check Signed-off-by: Valeriy Khorunzhin logic fix Signed-off-by: Valeriy Khorunzhin fix linter Signed-off-by: Valeriy Khorunzhin fix linter Signed-off-by: Valeriy Khorunzhin fix logic Signed-off-by: Valeriy Khorunzhin fix typo Signed-off-by: Valeriy Khorunzhin reuse SetPhaseCondition Signed-off-by: Valeriy Khorunzhin remove unused check Signed-off-by: Valeriy Khorunzhin remove unused Signed-off-by: Valeriy Khorunzhin add comment to condition file Signed-off-by: Valeriy Khorunzhin refactoring Signed-off-by: Valeriy Khorunzhin concrete Signed-off-by: Valeriy Khorunzhin vi with new design Signed-off-by: Valeriy Khorunzhin cvi new design Signed-off-by: Valeriy Khorunzhin fix test data Signed-off-by: Valeriy Khorunzhin fix copyright Signed-off-by: Valeriy Khorunzhin refactoring Signed-off-by: Valeriy Khorunzhin do not requeue Signed-off-by: Valeriy Khorunzhin refactoring Signed-off-by: Valeriy Khorunzhin fix namespaces order in status Signed-off-by: Valeriy Khorunzhin optimization Signed-off-by: Valeriy Khorunzhin refactoring Signed-off-by: Valeriy Khorunzhin reuse existed diskService Signed-off-by: Valeriy Khorunzhin change builder digest Signed-off-by: Valeriy Khorunzhin empty empty fix after rebase Signed-off-by: Valeriy Khorunzhin --- api/core/v1alpha2/cvicondition/condition.go | 23 + api/core/v1alpha2/vicondition/condition.go | 23 + .../pkg/builder/vmbda/option.go | 45 ++ .../pkg/builder/vmbda/vmbda.go | 51 +++ .../pkg/controller/cvi/cvi_controller.go | 2 +- .../pkg/controller/cvi/cvi_reconciler.go | 21 +- .../pkg/controller/cvi/internal/attachee.go | 87 ---- .../pkg/controller/cvi/internal/deletion.go | 8 + .../pkg/controller/cvi/internal/inuse.go | 238 ++++++++++ .../pkg/controller/cvi/internal/inuse_test.go | 370 ++++++++++++++++ .../pkg/controller/cvi/internal/life_cycle.go | 9 + .../cvi/internal/source/object_ref.go | 5 +- .../pkg/controller/cvi/internal/suite_test.go | 29 ++ .../cvi/internal/watcher/cvi_watcher.go | 124 ++++++ .../cvi/internal/watcher/vd_watcher.go | 130 ++++++ .../cvi/internal/watcher/vi_watcher.go | 124 ++++++ .../pkg/controller/dvcr_data_source.go | 27 +- .../pkg/controller/indexer/cvi_indexer.go | 42 ++ .../pkg/controller/indexer/indexer.go | 14 + .../pkg/controller/indexer/vd_indexer.go | 42 ++ .../pkg/controller/indexer/vi_indexer.go | 42 ++ .../pkg/controller/vd/internal/life_cycle.go | 35 +- .../controller/vd/internal/life_cycle_test.go | 4 +- .../pkg/controller/vd/internal/source/http.go | 2 +- .../vd/internal/source/object_ref_cvi.go | 10 +- .../vd/internal/source/object_ref_vi.go | 7 +- .../controller/vd/internal/source/registry.go | 2 +- .../controller/vd/internal/source/sources.go | 2 +- .../controller/vd/internal/source/upload.go | 2 +- .../pkg/controller/vd/vd_controller.go | 2 +- .../pkg/controller/vi/internal/attachee.go | 93 ---- .../pkg/controller/vi/internal/deletion.go | 8 + .../pkg/controller/vi/internal/interfaces.go | 1 - .../pkg/controller/vi/internal/inuse.go | 242 +++++++++++ .../pkg/controller/vi/internal/inuse_test.go | 405 ++++++++++++++++++ .../pkg/controller/vi/internal/life_cycle.go | 40 +- .../controller/vi/internal/life_cycle_test.go | 4 +- .../pkg/controller/vi/internal/mock.go | 50 --- .../pkg/controller/vi/internal/source/http.go | 2 +- .../vi/internal/source/object_ref.go | 6 +- .../vi/internal/source/object_ref_vd.go | 2 +- .../internal/source/object_ref_vi_on_pvc.go | 2 +- .../controller/vi/internal/source/registry.go | 2 +- .../controller/vi/internal/source/sources.go | 2 +- .../controller/vi/internal/source/upload.go | 2 +- .../vi/internal/watcher/cvi_watcher.go | 126 ++++++ .../vi/internal/watcher/vd_watcher.go | 134 ++++++ .../vi/internal/watcher/vi_watcher.go | 128 ++++++ .../pkg/controller/vi/vi_controller.go | 4 +- .../pkg/controller/vi/vi_reconciler.go | 3 + .../vmbda/internal/block_device_ready.go | 10 +- .../pkg/controller/watchers/cvi_enqueuer.go | 9 - 52 files changed, 2495 insertions(+), 302 deletions(-) create mode 100644 images/virtualization-artifact/pkg/builder/vmbda/option.go create mode 100644 images/virtualization-artifact/pkg/builder/vmbda/vmbda.go delete mode 100644 images/virtualization-artifact/pkg/controller/cvi/internal/attachee.go create mode 100644 images/virtualization-artifact/pkg/controller/cvi/internal/inuse.go create mode 100644 images/virtualization-artifact/pkg/controller/cvi/internal/inuse_test.go create mode 100644 images/virtualization-artifact/pkg/controller/cvi/internal/suite_test.go create mode 100644 images/virtualization-artifact/pkg/controller/cvi/internal/watcher/cvi_watcher.go create mode 100644 images/virtualization-artifact/pkg/controller/cvi/internal/watcher/vd_watcher.go create mode 100644 images/virtualization-artifact/pkg/controller/cvi/internal/watcher/vi_watcher.go delete mode 100644 images/virtualization-artifact/pkg/controller/vi/internal/attachee.go create mode 100644 images/virtualization-artifact/pkg/controller/vi/internal/inuse.go create mode 100644 images/virtualization-artifact/pkg/controller/vi/internal/inuse_test.go create mode 100644 images/virtualization-artifact/pkg/controller/vi/internal/watcher/cvi_watcher.go create mode 100644 images/virtualization-artifact/pkg/controller/vi/internal/watcher/vd_watcher.go create mode 100644 images/virtualization-artifact/pkg/controller/vi/internal/watcher/vi_watcher.go diff --git a/api/core/v1alpha2/cvicondition/condition.go b/api/core/v1alpha2/cvicondition/condition.go index 1d55753286..4e7f6ae82f 100644 --- a/api/core/v1alpha2/cvicondition/condition.go +++ b/api/core/v1alpha2/cvicondition/condition.go @@ -28,6 +28,8 @@ const ( DatasourceReadyType Type = "DatasourceReady" // ReadyType indicates whether the import process succeeded and the `ClusterVirtualImage` is ready for use. ReadyType Type = "Ready" + // InUseType indicates that the `ClusterVirtualImage` is used by other resources and cannot be deleted now. + InUseType Type = "InUse" ) type ( @@ -35,6 +37,8 @@ type ( DatasourceReadyReason string // ReadyReason represents the various reasons for the Ready condition type. ReadyReason string + // InUseReason represents the various reasons for the InUseType condition type. + InUseReason string ) func (s DatasourceReadyReason) String() string { @@ -45,6 +49,10 @@ func (s ReadyReason) String() string { return string(s) } +func (s InUseReason) String() string { + return string(s) +} + const ( // DatasourceReady indicates that the datasource is ready for use, allowing the import process to start. DatasourceReady DatasourceReadyReason = "DatasourceReady" @@ -73,4 +81,19 @@ const ( ProvisioningFailed ReadyReason = "ProvisioningFailed" // Ready indicates that the import process is complete and the `ClusterVirtualImage` is ready for use. Ready ReadyReason = "Ready" + + /* + A ClusterVirtualImage can be considered in use if it meets the following two criteria: + 1) Provisioning of the ClusterVirtualImage must be completed. The ReadyCondition must be True. + 2) The ClusterVirtualImage must be used in one of the following ways: + - Be attached to one or more VirtualMachines (all VirtualMachine phases except Stopped) + - Be attached via a VirtualMachineBlockDeviceAttachment (any VMBDA phases) + - Be used for provisioning VirtualImage (phases: Pending, Provisioning, Failed) + - Be used for provisioning ClusterVirtualImage (phases: Pending, Provisioning, Failed) + - Be used for provisioning VirtualDisk (phases: Pending, Provisioning, WaitForFirstConsumer, Failed) + */ + // InUse indicates that the `ClusterVirtualImage` is used by other resources and cannot be deleted now. + InUse InUseReason = "InUse" + // NotInUse indicates that the `ClusterVirtualImage` is not used by other resources and can be deleted now. + NotInUse InUseReason = "NotInUse" ) diff --git a/api/core/v1alpha2/vicondition/condition.go b/api/core/v1alpha2/vicondition/condition.go index 86dbd2d0af..f1751bac33 100644 --- a/api/core/v1alpha2/vicondition/condition.go +++ b/api/core/v1alpha2/vicondition/condition.go @@ -30,6 +30,8 @@ const ( ReadyType Type = "Ready" // StorageClassReadyType indicates whether the storageClass ready. StorageClassReadyType Type = "StorageClassReady" + // InUseType indicates that the `VirtualImage` is used by other resources and cannot be deleted now. + InUseType Type = "InUse" ) type ( @@ -39,6 +41,8 @@ type ( ReadyReason string // StorageClassReadyReason represents the various reasons for the StorageClassReady condition type. StorageClassReadyReason string + // InUseReason represents the various reasons for the InUseType condition type. + InUseReason string ) func (s DatasourceReadyReason) String() string { @@ -53,6 +57,10 @@ func (s StorageClassReadyReason) String() string { return string(s) } +func (s InUseReason) String() string { + return string(s) +} + const ( // DatasourceReady indicates that the datasource is ready for use, allowing the import process to start. DatasourceReady DatasourceReadyReason = "DatasourceReady" @@ -99,4 +107,19 @@ const ( StorageClassNotFound StorageClassReadyReason = "StorageClassNotFound" // DVCRTypeUsed indicates that the DVCR provisioning chosen. DVCRTypeUsed StorageClassReadyReason = "DVCRTypeUsed" + + /* + A VirtualImage can be considered in use if it meets the following two criteria: + 1) Provisioning of the VirtualImage must be completed. The ReadyCondition must be True or have the Reason PVCLost. + 2) The VirtualImage must be used in one of the following ways: + - Be attached to one or more VirtualMachines (all VirtualMachine phases except Stopped) + - Be attached via a VirtualMachineBlockDeviceAttachment (any VMBDA phases) + - Be used for provisioning VirtualImage (phases: Pending, Provisioning, Failed) + - Be used for provisioning ClusterVirtualImage (phases: Pending, Provisioning, Failed) + - Be used for provisioning VirtualDisk (phases: Pending, Provisioning, WaitForFirstConsumer, Failed) + */ + // InUse indicates that the `VirtualImage` is used by other resources and cannot be deleted now. + InUse InUseReason = "InUse" + // InUse indicates that the `VirtualImage` is not used by other resources and can be deleted now. + NotInUse InUseReason = "NotInUse" ) diff --git a/images/virtualization-artifact/pkg/builder/vmbda/option.go b/images/virtualization-artifact/pkg/builder/vmbda/option.go new file mode 100644 index 0000000000..e8b34067cc --- /dev/null +++ b/images/virtualization-artifact/pkg/builder/vmbda/option.go @@ -0,0 +1,45 @@ +/* +Copyright 2025 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 vmbda + +import ( + "github.com/deckhouse/virtualization-controller/pkg/builder/meta" + "github.com/deckhouse/virtualization/api/core/v1alpha2" +) + +type Option func(vd *v1alpha2.VirtualMachineBlockDeviceAttachment) + +var ( + WithName = meta.WithName[*v1alpha2.VirtualMachineBlockDeviceAttachment] + WithNamespace = meta.WithNamespace[*v1alpha2.VirtualMachineBlockDeviceAttachment] + WithLabel = meta.WithLabel[*v1alpha2.VirtualMachineBlockDeviceAttachment] + WithLabels = meta.WithLabels[*v1alpha2.VirtualMachineBlockDeviceAttachment] + WithAnnotation = meta.WithAnnotation[*v1alpha2.VirtualMachineBlockDeviceAttachment] + WithAnnotations = meta.WithAnnotations[*v1alpha2.VirtualMachineBlockDeviceAttachment] +) + +func WithBlockDeviceRef(bdRef v1alpha2.VMBDAObjectRef) func(vmbda *v1alpha2.VirtualMachineBlockDeviceAttachment) { + return func(vmbda *v1alpha2.VirtualMachineBlockDeviceAttachment) { + vmbda.Spec.BlockDeviceRef = bdRef + } +} + +func WithVMName(vmName string) func(vmbda *v1alpha2.VirtualMachineBlockDeviceAttachment) { + return func(vmbda *v1alpha2.VirtualMachineBlockDeviceAttachment) { + vmbda.Spec.VirtualMachineName = vmName + } +} diff --git a/images/virtualization-artifact/pkg/builder/vmbda/vmbda.go b/images/virtualization-artifact/pkg/builder/vmbda/vmbda.go new file mode 100644 index 0000000000..760eceb69f --- /dev/null +++ b/images/virtualization-artifact/pkg/builder/vmbda/vmbda.go @@ -0,0 +1,51 @@ +/* +Copyright 2025 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 vmbda + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "github.com/deckhouse/virtualization/api/core/v1alpha2" +) + +func New(options ...Option) *v1alpha2.VirtualMachineBlockDeviceAttachment { + vmbda := NewEmpty("", "") + ApplyOptions(vmbda, options) + return vmbda +} + +func ApplyOptions(vmbda *v1alpha2.VirtualMachineBlockDeviceAttachment, opts []Option) { + if vmbda == nil { + return + } + for _, opt := range opts { + opt(vmbda) + } +} + +func NewEmpty(name, namespace string) *v1alpha2.VirtualMachineBlockDeviceAttachment { + return &v1alpha2.VirtualMachineBlockDeviceAttachment{ + TypeMeta: metav1.TypeMeta{ + APIVersion: v1alpha2.SchemeGroupVersion.String(), + Kind: v1alpha2.VirtualMachineBlockDeviceAttachmentKind, + }, + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + }, + } +} diff --git a/images/virtualization-artifact/pkg/controller/cvi/cvi_controller.go b/images/virtualization-artifact/pkg/controller/cvi/cvi_controller.go index c7f4bbb370..a220401558 100644 --- a/images/virtualization-artifact/pkg/controller/cvi/cvi_controller.go +++ b/images/virtualization-artifact/pkg/controller/cvi/cvi_controller.go @@ -76,8 +76,8 @@ func NewController( mgr.GetClient(), internal.NewDatasourceReadyHandler(sources), internal.NewLifeCycleHandler(sources, mgr.GetClient()), + internal.NewInUseHandler(mgr.GetClient()), internal.NewDeletionHandler(sources), - internal.NewAttacheeHandler(mgr.GetClient()), ) cviController, err := controller.New(ControllerName, mgr, controller.Options{ diff --git a/images/virtualization-artifact/pkg/controller/cvi/cvi_reconciler.go b/images/virtualization-artifact/pkg/controller/cvi/cvi_reconciler.go index 7898b2ac50..609afde5e0 100644 --- a/images/virtualization-artifact/pkg/controller/cvi/cvi_reconciler.go +++ b/images/virtualization-artifact/pkg/controller/cvi/cvi_reconciler.go @@ -32,7 +32,6 @@ import ( "github.com/deckhouse/virtualization-controller/pkg/controller/cvi/internal/watcher" "github.com/deckhouse/virtualization-controller/pkg/controller/reconciler" - "github.com/deckhouse/virtualization-controller/pkg/controller/watchers" virtv2 "github.com/deckhouse/virtualization/api/core/v1alpha2" ) @@ -109,16 +108,16 @@ func (r *Reconciler) SetupController(_ context.Context, mgr manager.Manager, ctr } } - cviFromVIEnqueuer := watchers.NewClusterVirtualImageRequestEnqueuer(mgr.GetClient(), &virtv2.VirtualImage{}, virtv2.ClusterVirtualImageObjectRefKindVirtualImage) - viWatcher := watchers.NewObjectRefWatcher(watchers.NewVirtualImageFilter(), cviFromVIEnqueuer) - if err := viWatcher.Run(mgr, ctr); err != nil { - return fmt.Errorf("error setting watch on VIs: %w", err) - } - - cviFromCVIEnqueuer := watchers.NewClusterVirtualImageRequestEnqueuer(mgr.GetClient(), &virtv2.ClusterVirtualImage{}, virtv2.ClusterVirtualImageObjectRefKindClusterVirtualImage) - cviWatcher := watchers.NewObjectRefWatcher(watchers.NewClusterVirtualImageFilter(), cviFromCVIEnqueuer) - if err := cviWatcher.Run(mgr, ctr); err != nil { - return fmt.Errorf("error setting watch on CVIs: %w", err) + for _, w := range []Watcher{ + watcher.NewClusterVirtualImageWatcher(mgr.GetClient()), + watcher.NewVirtualImageWatcher(mgr.GetClient()), + watcher.NewVirtualDiskWatcher(mgr.GetClient()), + watcher.NewVirtualDiskSnapshotWatcher(mgr.GetClient()), + } { + err := w.Watch(mgr, ctr) + if err != nil { + return fmt.Errorf("error setting watcher: %w", err) + } } return nil diff --git a/images/virtualization-artifact/pkg/controller/cvi/internal/attachee.go b/images/virtualization-artifact/pkg/controller/cvi/internal/attachee.go deleted file mode 100644 index 6668de0150..0000000000 --- a/images/virtualization-artifact/pkg/controller/cvi/internal/attachee.go +++ /dev/null @@ -1,87 +0,0 @@ -/* -Copyright 2024 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 internal - -import ( - "context" - "fmt" - - "sigs.k8s.io/controller-runtime/pkg/client" - "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" - "sigs.k8s.io/controller-runtime/pkg/reconcile" - - "github.com/deckhouse/virtualization-controller/pkg/logger" - virtv2 "github.com/deckhouse/virtualization/api/core/v1alpha2" -) - -type AttacheeHandler struct { - client client.Client -} - -func NewAttacheeHandler(client client.Client) *AttacheeHandler { - return &AttacheeHandler{ - client: client, - } -} - -func (h AttacheeHandler) Handle(ctx context.Context, cvi *virtv2.ClusterVirtualImage) (reconcile.Result, error) { - log := logger.FromContext(ctx).With(logger.SlogHandler("attachee")) - - hasAttachedVM, err := h.hasAttachedVM(ctx, cvi) - if err != nil { - return reconcile.Result{}, err - } - - switch { - case !hasAttachedVM: - log.Debug("Allow cluster virtual image deletion") - controllerutil.RemoveFinalizer(cvi, virtv2.FinalizerCVIProtection) - case cvi.DeletionTimestamp == nil: - log.Debug("Protect cluster virtual image from deletion") - controllerutil.AddFinalizer(cvi, virtv2.FinalizerCVIProtection) - default: - log.Debug("Cluster virtual image deletion is delayed: it's protected by virtual machines") - } - - return reconcile.Result{}, nil -} - -func (h AttacheeHandler) hasAttachedVM(ctx context.Context, cvi client.Object) (bool, error) { - var vms virtv2.VirtualMachineList - err := h.client.List(ctx, &vms, &client.ListOptions{}) - if err != nil { - return false, fmt.Errorf("error getting virtual machines: %w", err) - } - - for _, vm := range vms.Items { - if h.isCVIAttachedToVM(cvi.GetName(), vm) { - return true, nil - } - } - - return false, nil -} - -func (h AttacheeHandler) isCVIAttachedToVM(cviName string, vm virtv2.VirtualMachine) bool { - for _, bda := range vm.Status.BlockDeviceRefs { - if bda.Kind == virtv2.ClusterImageDevice && bda.Name == cviName { - return true - } - } - - return false -} diff --git a/images/virtualization-artifact/pkg/controller/cvi/internal/deletion.go b/images/virtualization-artifact/pkg/controller/cvi/internal/deletion.go index a3f90a5303..c84a933a46 100644 --- a/images/virtualization-artifact/pkg/controller/cvi/internal/deletion.go +++ b/images/virtualization-artifact/pkg/controller/cvi/internal/deletion.go @@ -20,12 +20,15 @@ import ( "context" "time" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" "sigs.k8s.io/controller-runtime/pkg/reconcile" + "github.com/deckhouse/virtualization-controller/pkg/controller/conditions" "github.com/deckhouse/virtualization-controller/pkg/controller/cvi/internal/source" "github.com/deckhouse/virtualization-controller/pkg/logger" virtv2 "github.com/deckhouse/virtualization/api/core/v1alpha2" + "github.com/deckhouse/virtualization/api/core/v1alpha2/cvicondition" ) const deletionHandlerName = "DeletionHandler" @@ -44,6 +47,11 @@ func (h DeletionHandler) Handle(ctx context.Context, cvi *virtv2.ClusterVirtualI log := logger.FromContext(ctx).With(logger.SlogHandler(deletionHandlerName)) if cvi.DeletionTimestamp != nil { + inUseCondition, _ := conditions.GetCondition(cvicondition.InUseType, cvi.Status.Conditions) + if inUseCondition.Status != metav1.ConditionFalse || !conditions.IsLastUpdated(inUseCondition, cvi) { + return reconcile.Result{}, nil + } + requeue, err := h.sources.CleanUp(ctx, cvi) if err != nil { return reconcile.Result{}, err diff --git a/images/virtualization-artifact/pkg/controller/cvi/internal/inuse.go b/images/virtualization-artifact/pkg/controller/cvi/internal/inuse.go new file mode 100644 index 0000000000..bbd10ea77a --- /dev/null +++ b/images/virtualization-artifact/pkg/controller/cvi/internal/inuse.go @@ -0,0 +1,238 @@ +/* +Copyright 2025 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 internal + +import ( + "context" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + + "github.com/deckhouse/virtualization-controller/pkg/controller/conditions" + "github.com/deckhouse/virtualization-controller/pkg/controller/indexer" + virtv2 "github.com/deckhouse/virtualization/api/core/v1alpha2" + "github.com/deckhouse/virtualization/api/core/v1alpha2/cvicondition" +) + +type InUseHandler struct { + client client.Client +} + +func NewInUseHandler(client client.Client) *InUseHandler { + return &InUseHandler{ + client: client, + } +} + +func (h InUseHandler) Handle(ctx context.Context, cvi *virtv2.ClusterVirtualImage) (reconcile.Result, error) { + cb := conditions.NewConditionBuilder(cvicondition.InUse).Generation(cvi.Generation) + readyCondition, _ := conditions.GetCondition(cvicondition.ReadyType, cvi.Status.Conditions) + if readyCondition.Status == metav1.ConditionFalse && conditions.IsLastUpdated(readyCondition, cvi) { + cb. + Status(metav1.ConditionFalse). + Reason(cvicondition.NotInUse). + Message("") + conditions.SetCondition(cb, &cvi.Status.Conditions) + return reconcile.Result{}, nil + } + if readyCondition.Status == metav1.ConditionUnknown || !conditions.IsLastUpdated(readyCondition, cvi) { + cb. + Status(metav1.ConditionUnknown). + Reason(conditions.ReasonUnknown). + Message("") + conditions.SetCondition(cb, &cvi.Status.Conditions) + return reconcile.Result{}, nil + } + + vms, err := h.listVMsUsingImage(ctx, cvi) + if err != nil { + return reconcile.Result{}, err + } + + vmbdas, err := h.listVMBDAsUsingImage(ctx, cvi) + if err != nil { + return reconcile.Result{}, err + } + + vds, err := h.listVDsUsingImage(ctx, cvi) + if err != nil { + return reconcile.Result{}, err + } + + vis, err := h.listVIsUsingImage(ctx, cvi) + if err != nil { + return reconcile.Result{}, err + } + + cvis, err := h.listCVIsUsingImage(ctx, cvi) + if err != nil { + return reconcile.Result{}, err + } + + consumerCount := len(vms) + len(vds) + len(vis) + len(cvis) + + if consumerCount > 0 { + cvi.Status.UsedInNamespaces = h.extractNamespacesFromObjects(vms, vmbdas, vds, vis) + cb. + Status(metav1.ConditionTrue). + Reason(cvicondition.InUse). + Message("") + } else { + cb. + Status(metav1.ConditionFalse). + Reason(cvicondition.NotInUse). + Message("") + } + + conditions.SetCondition(cb, &cvi.Status.Conditions) + return reconcile.Result{}, nil +} + +func (h InUseHandler) listVMsUsingImage(ctx context.Context, cvi *virtv2.ClusterVirtualImage) ([]client.Object, error) { + var vms virtv2.VirtualMachineList + err := h.client.List(ctx, &vms) + if err != nil { + return []client.Object{}, err + } + + var vmsUsingImage []client.Object + for _, vm := range vms.Items { + if vm.Status.Phase == virtv2.MachineStopped { + continue + } + + for _, bd := range vm.Status.BlockDeviceRefs { + if bd.Kind == virtv2.ClusterVirtualImageKind && bd.Name == cvi.Name { + vmsUsingImage = append(vmsUsingImage, &vm) + } + } + } + + return vmsUsingImage, nil +} + +func (h InUseHandler) listVMBDAsUsingImage(ctx context.Context, cvi *virtv2.ClusterVirtualImage) ([]client.Object, error) { + var vmbdas virtv2.VirtualMachineBlockDeviceAttachmentList + err := h.client.List(ctx, &vmbdas) + if err != nil { + return []client.Object{}, err + } + + var vmbdasUsedImage []client.Object + for _, vmbda := range vmbdas.Items { + if vmbda.Spec.BlockDeviceRef.Kind == virtv2.ClusterVirtualImageKind && vmbda.Spec.BlockDeviceRef.Name == cvi.Name { + vmbdasUsedImage = append(vmbdasUsedImage, &vmbda) + } + } + + return vmbdasUsedImage, nil +} + +func (h InUseHandler) listVDsUsingImage(ctx context.Context, cvi *virtv2.ClusterVirtualImage) ([]client.Object, error) { + var vds virtv2.VirtualDiskList + err := h.client.List(ctx, &vds, client.MatchingFields{ + indexer.IndexFieldVDByCVIDataSource: cvi.GetName(), + }) + if err != nil { + return []client.Object{}, err + } + + var vdsNotReady []client.Object + for _, vd := range vds.Items { + phase := vd.Status.Phase + isProvisioning := (phase == virtv2.DiskPending) || + (phase == virtv2.DiskProvisioning) || + (phase == virtv2.DiskWaitForFirstConsumer) || + (phase == virtv2.DiskFailed) + + if isProvisioning { + vdsNotReady = append(vdsNotReady, &vd) + } + } + + return vdsNotReady, nil +} + +func (h InUseHandler) listVIsUsingImage(ctx context.Context, cvi *virtv2.ClusterVirtualImage) ([]client.Object, error) { + var vis virtv2.VirtualImageList + err := h.client.List(ctx, &vis, client.MatchingFields{ + indexer.IndexFieldVIByCVIDataSource: cvi.GetName(), + }) + if err != nil { + return []client.Object{}, err + } + + var visNotReady []client.Object + for _, vi := range vis.Items { + phase := vi.Status.Phase + isProvisioning := (phase == virtv2.ImagePending) || (phase == virtv2.ImageProvisioning) || (phase == virtv2.ImageFailed) + + if isProvisioning { + visNotReady = append(visNotReady, &vi) + } + } + + return visNotReady, nil +} + +func (h InUseHandler) listCVIsUsingImage(ctx context.Context, cvi *virtv2.ClusterVirtualImage) ([]client.Object, error) { + var cvis virtv2.ClusterVirtualImageList + err := h.client.List(ctx, &cvis, client.MatchingFields{ + indexer.IndexFieldCVIByCVIDataSource: cvi.GetName(), + }) + if err != nil { + return []client.Object{}, err + } + + var cvisNotReady []client.Object + for _, cviItem := range cvis.Items { + phase := cviItem.Status.Phase + isProvisioning := (phase == virtv2.ImagePending) || (phase == virtv2.ImageProvisioning) || (phase == virtv2.ImageFailed) + + if isProvisioning { + cvisNotReady = append(cvisNotReady, &cviItem) + } + } + + return cvisNotReady, nil +} + +func (h InUseHandler) extractNamespacesFromObjects(vms, vmbdas, vds, vis []client.Object) []string { + var objects []client.Object + objects = append(objects, vms...) + objects = append(objects, vmbdas...) + objects = append(objects, vds...) + objects = append(objects, vis...) + + var namespaces []string + namespacesMap := make(map[string]struct{}) + for _, obj := range objects { + namespace := obj.GetNamespace() + if namespace == "" { + namespace = "default" + } + + _, ok := namespacesMap[namespace] + if !ok { + namespaces = append(namespaces, namespace) + namespacesMap[namespace] = struct{}{} + } + } + + return namespaces +} diff --git a/images/virtualization-artifact/pkg/controller/cvi/internal/inuse_test.go b/images/virtualization-artifact/pkg/controller/cvi/internal/inuse_test.go new file mode 100644 index 0000000000..d55bac0917 --- /dev/null +++ b/images/virtualization-artifact/pkg/controller/cvi/internal/inuse_test.go @@ -0,0 +1,370 @@ +/* +Copyright 2025 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 internal + +import ( + "time" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/utils/ptr" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + + cvibuilder "github.com/deckhouse/virtualization-controller/pkg/builder/cvi" + vdbuilder "github.com/deckhouse/virtualization-controller/pkg/builder/vd" + vibuilder "github.com/deckhouse/virtualization-controller/pkg/builder/vi" + vmbdabuilder "github.com/deckhouse/virtualization-controller/pkg/builder/vmbda" + "github.com/deckhouse/virtualization-controller/pkg/common/testutil" + "github.com/deckhouse/virtualization-controller/pkg/controller/conditions" + virtv2 "github.com/deckhouse/virtualization/api/core/v1alpha2" + "github.com/deckhouse/virtualization/api/core/v1alpha2/cvicondition" +) + +var _ = DescribeTable("InUseHandler Handle", func(args inUseHandlerTestArgs) { + cvi := &virtv2.ClusterVirtualImage{ + ObjectMeta: metav1.ObjectMeta{ + Name: args.CVIName, + DeletionTimestamp: args.DeletionTimestamp, + }, + Status: virtv2.ClusterVirtualImageStatus{ + Conditions: []metav1.Condition{ + { + Type: cvicondition.ReadyType.String(), + Reason: cvicondition.Ready.String(), + Status: metav1.ConditionTrue, + }, + }, + }, + } + + var objects []client.Object + for _, vm := range args.VMs { + objects = append(objects, &vm) + } + + for _, vmbda := range args.VMBDAs { + objects = append(objects, &vmbda) + } + + for _, vd := range args.VDs { + objects = append(objects, &vd) + } + + for _, vi := range args.VIs { + objects = append(objects, &vi) + } + + for _, cvi := range args.CVIs { + objects = append(objects, &cvi) + } + + fakeClient, err := testutil.NewFakeClientWithObjects(objects...) + Expect(err).ShouldNot(HaveOccurred()) + handler := NewInUseHandler(fakeClient) + + result, err := handler.Handle(testutil.ContextBackgroundWithNoOpLogger(), cvi) + Expect(err).To(BeNil()) + Expect(result).To(Equal(reconcile.Result{})) + inUseCondition, _ := conditions.GetCondition(cvicondition.InUseType, cvi.Status.Conditions) + Expect(inUseCondition.Status).To(Equal(args.ExpectedConditionStatus)) + Expect(inUseCondition.Reason).To(Equal(args.ExpectedConditionReason)) +}, + Entry("deletionTimestamp not exists", inUseHandlerTestArgs{ + VMs: []virtv2.VirtualMachine{ + generateVMForInUseTest("name", "ns", []virtv2.BlockDeviceStatusRef{}), + }, + CVIName: "test", + ExpectedConditionStatus: metav1.ConditionFalse, + ExpectedConditionReason: cvicondition.NotInUse.String(), + }), + Entry("has VirtualMachine but with no deleted CVI", inUseHandlerTestArgs{ + CVIName: "test", + DeletionTimestamp: ptr.To(metav1.Time{Time: time.Now()}), + VMs: []virtv2.VirtualMachine{ + generateVMForInUseTest("name", "ns2", []virtv2.BlockDeviceStatusRef{}), + }, + ExpectedConditionStatus: metav1.ConditionFalse, + ExpectedConditionReason: cvicondition.NotInUse.String(), + }), + Entry("has 1 VirtualMachine with connected terminating CVI", inUseHandlerTestArgs{ + CVIName: "test", + DeletionTimestamp: ptr.To(metav1.Time{Time: time.Now()}), + VMs: []virtv2.VirtualMachine{ + generateVMForInUseTest("name", "ns", []virtv2.BlockDeviceStatusRef{ + { + Kind: virtv2.ClusterVirtualImageKind, + Name: "test", + }, + }), + }, + ExpectedConditionStatus: metav1.ConditionTrue, + ExpectedConditionReason: cvicondition.InUse.String(), + }), + Entry("has 5 VirtualMachines with connected terminating CVI", inUseHandlerTestArgs{ + CVIName: "test", + DeletionTimestamp: ptr.To(metav1.Time{Time: time.Now()}), + VMs: []virtv2.VirtualMachine{ + generateVMForInUseTest("name", "ns", []virtv2.BlockDeviceStatusRef{ + { + Kind: virtv2.ClusterVirtualImageKind, + Name: "test", + }, + }), + generateVMForInUseTest("name2", "ns", []virtv2.BlockDeviceStatusRef{ + { + Kind: virtv2.ClusterVirtualImageKind, + Name: "test", + }, + }), + generateVMForInUseTest("name3", "ns", []virtv2.BlockDeviceStatusRef{ + { + Kind: virtv2.ClusterVirtualImageKind, + Name: "test", + }, + }), + generateVMForInUseTest("name4", "ns", []virtv2.BlockDeviceStatusRef{ + { + Kind: virtv2.ClusterVirtualImageKind, + Name: "test", + }, + }), + generateVMForInUseTest("name5", "ns", []virtv2.BlockDeviceStatusRef{ + { + Kind: virtv2.ClusterVirtualImageKind, + Name: "test", + }, + }), + { + ObjectMeta: metav1.ObjectMeta{ + Name: "test-vm-stopped", + Namespace: "ns", + }, + TypeMeta: metav1.TypeMeta{ + Kind: virtv2.VirtualMachineKind, + }, + Status: virtv2.VirtualMachineStatus{ + Phase: virtv2.MachineStopped, + BlockDeviceRefs: []virtv2.BlockDeviceStatusRef{ + { + Kind: virtv2.ClusterVirtualImageKind, + Name: "test", + }, + }, + }, + }, + }, + ExpectedConditionStatus: metav1.ConditionTrue, + ExpectedConditionReason: cvicondition.InUse.String(), + }), + Entry("has 5 VirtualMachines with connected terminating CVI, 1 VMBDA, 4 VD, 2 VI, 1 CVI", inUseHandlerTestArgs{ + CVIName: "test", + DeletionTimestamp: ptr.To(metav1.Time{Time: time.Now()}), + VMs: []virtv2.VirtualMachine{ + generateVMForInUseTest("name", "ns", []virtv2.BlockDeviceStatusRef{ + { + Kind: virtv2.ClusterVirtualImageKind, + Name: "test", + }, + }), + generateVMForInUseTest("name2", "ns", []virtv2.BlockDeviceStatusRef{ + { + Kind: virtv2.ClusterVirtualImageKind, + Name: "test", + }, + }), + generateVMForInUseTest("name3", "ns", []virtv2.BlockDeviceStatusRef{ + { + Kind: virtv2.ClusterVirtualImageKind, + Name: "test", + }, + }), + generateVMForInUseTest("name4", "ns", []virtv2.BlockDeviceStatusRef{ + { + Kind: virtv2.ClusterVirtualImageKind, + Name: "test", + }, + }), + generateVMForInUseTest("name5", "ns", []virtv2.BlockDeviceStatusRef{ + { + Kind: virtv2.ClusterVirtualImageKind, + Name: "test", + }, + }), + }, + VDs: []virtv2.VirtualDisk{ + generateVDForInUseTest("test", "ns1", virtv2.VirtualDiskDataSource{ + Type: virtv2.DataSourceTypeObjectRef, + ObjectRef: &virtv2.VirtualDiskObjectRef{ + Kind: virtv2.ClusterVirtualImageKind, + Name: "test", + }, + }), + generateVDForInUseTest("test2", "ns2", virtv2.VirtualDiskDataSource{ + Type: virtv2.DataSourceTypeObjectRef, + ObjectRef: &virtv2.VirtualDiskObjectRef{ + Kind: virtv2.ClusterVirtualImageKind, + Name: "test", + }, + }), + generateVDForInUseTest("test3", "ns3", virtv2.VirtualDiskDataSource{ + Type: virtv2.DataSourceTypeObjectRef, + ObjectRef: &virtv2.VirtualDiskObjectRef{ + Kind: virtv2.ClusterVirtualImageKind, + Name: "test", + }, + }), + generateVDForInUseTest("test4", "ns4", virtv2.VirtualDiskDataSource{ + Type: virtv2.DataSourceTypeObjectRef, + ObjectRef: &virtv2.VirtualDiskObjectRef{ + Kind: virtv2.ClusterVirtualImageKind, + Name: "test", + }, + }), + }, + VIs: []virtv2.VirtualImage{ + generateVIForInUseTest("test", "ns", virtv2.VirtualImageDataSource{ + Type: virtv2.DataSourceTypeObjectRef, + ObjectRef: &virtv2.VirtualImageObjectRef{ + Kind: virtv2.ClusterVirtualImageKind, + Name: "test", + }, + }), + generateVIForInUseTest("test2", "ns5", virtv2.VirtualImageDataSource{ + Type: virtv2.DataSourceTypeObjectRef, + ObjectRef: &virtv2.VirtualImageObjectRef{ + Kind: virtv2.ClusterVirtualImageKind, + Name: "test", + }, + }), + }, + CVIs: []virtv2.ClusterVirtualImage{ + generateCVIForInUseTest("test2", virtv2.ClusterVirtualImageDataSource{ + Type: virtv2.DataSourceTypeObjectRef, + ObjectRef: &virtv2.ClusterVirtualImageObjectRef{ + Kind: virtv2.ClusterVirtualImageKind, + Name: "test", + }, + }), + *cvibuilder.New( + cvibuilder.WithName("test322"), + cvibuilder.WithPhase(virtv2.ImageReady), + cvibuilder.WithCondition(metav1.Condition{ + Status: metav1.ConditionTrue, + Reason: cvicondition.ReadyType.String(), + }), + cvibuilder.WithDatasource(virtv2.ClusterVirtualImageDataSource{ + Type: virtv2.DataSourceTypeObjectRef, + ObjectRef: &virtv2.ClusterVirtualImageObjectRef{ + Kind: virtv2.ClusterVirtualImageKind, + Name: "test", + }, + }), + ), + }, + VMBDAs: []virtv2.VirtualMachineBlockDeviceAttachment{ + generateVMBDAForInUseTest( + "test", + "ns", + virtv2.VMBDAObjectRef{ + Kind: virtv2.ClusterVirtualImageKind, + Name: "test", + }, + "vm", + ), + }, + ExpectedConditionStatus: metav1.ConditionTrue, + ExpectedConditionReason: cvicondition.InUse.String(), + }), + Entry("has 1 CVI", inUseHandlerTestArgs{ + CVIName: "test", + DeletionTimestamp: ptr.To(metav1.Time{Time: time.Now()}), + CVIs: []virtv2.ClusterVirtualImage{ + generateCVIForInUseTest("test2", virtv2.ClusterVirtualImageDataSource{ + Type: virtv2.DataSourceTypeObjectRef, + ObjectRef: &virtv2.ClusterVirtualImageObjectRef{ + Kind: virtv2.ClusterVirtualImageKind, + Name: "test", + }, + }), + }, + ExpectedConditionStatus: metav1.ConditionTrue, + ExpectedConditionReason: cvicondition.InUse.String(), + }), +) + +type inUseHandlerTestArgs struct { + CVIName string + DeletionTimestamp *metav1.Time + VMs []virtv2.VirtualMachine + VMBDAs []virtv2.VirtualMachineBlockDeviceAttachment + VDs []virtv2.VirtualDisk + VIs []virtv2.VirtualImage + CVIs []virtv2.ClusterVirtualImage + ExpectedConditionReason string + ExpectedConditionStatus metav1.ConditionStatus +} + +func generateVMForInUseTest(name, namespace string, blockDeviceRefs []virtv2.BlockDeviceStatusRef) virtv2.VirtualMachine { + return virtv2.VirtualMachine{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + }, + TypeMeta: metav1.TypeMeta{ + APIVersion: virtv2.SchemeGroupVersion.String(), + Kind: "VirtualMachine", + }, + Status: virtv2.VirtualMachineStatus{ + BlockDeviceRefs: blockDeviceRefs, + }, + } +} + +func generateVIForInUseTest(name, namespace string, datasource virtv2.VirtualImageDataSource) virtv2.VirtualImage { + return *vibuilder.New( + vibuilder.WithName(name), + vibuilder.WithNamespace(namespace), + vibuilder.WithDatasource(datasource), + ) +} + +func generateCVIForInUseTest(name string, datasource virtv2.ClusterVirtualImageDataSource) virtv2.ClusterVirtualImage { + return *cvibuilder.New( + cvibuilder.WithName(name), + cvibuilder.WithDatasource(datasource), + cvibuilder.WithPhase(virtv2.ImagePending), + ) +} + +func generateVDForInUseTest(name, namespace string, datasource virtv2.VirtualDiskDataSource) virtv2.VirtualDisk { + return *vdbuilder.New( + vdbuilder.WithName(name), + vdbuilder.WithNamespace(namespace), + vdbuilder.WithDatasource(&datasource), + ) +} + +func generateVMBDAForInUseTest(name, namespace string, bdRef virtv2.VMBDAObjectRef, vmName string) virtv2.VirtualMachineBlockDeviceAttachment { + return *vmbdabuilder.New( + vmbdabuilder.WithName(name), + vmbdabuilder.WithNamespace(namespace), + vmbdabuilder.WithBlockDeviceRef(bdRef), + vmbdabuilder.WithVMName(vmName), + ) +} diff --git a/images/virtualization-artifact/pkg/controller/cvi/internal/life_cycle.go b/images/virtualization-artifact/pkg/controller/cvi/internal/life_cycle.go index d3c281ef5d..72d0a0a854 100644 --- a/images/virtualization-artifact/pkg/controller/cvi/internal/life_cycle.go +++ b/images/virtualization-artifact/pkg/controller/cvi/internal/life_cycle.go @@ -55,6 +55,15 @@ func (h LifeCycleHandler) Handle(ctx context.Context, cvi *virtv2.ClusterVirtual } if cvi.DeletionTimestamp != nil { + // It is necessary to update this condition in order to use this image as a data source. + if readyCondition.Status == metav1.ConditionTrue { + cb := conditions.NewConditionBuilder(cvicondition.ReadyType).Generation(cvi.Generation). + Status(metav1.ConditionTrue). + Reason(cvicondition.Ready). + Message("") + conditions.SetCondition(cb, &cvi.Status.Conditions) + } + cvi.Status.Phase = virtv2.ImageTerminating return reconcile.Result{}, nil } diff --git a/images/virtualization-artifact/pkg/controller/cvi/internal/source/object_ref.go b/images/virtualization-artifact/pkg/controller/cvi/internal/source/object_ref.go index 983b8c074a..31cbe635f8 100644 --- a/images/virtualization-artifact/pkg/controller/cvi/internal/source/object_ref.go +++ b/images/virtualization-artifact/pkg/controller/cvi/internal/source/object_ref.go @@ -43,6 +43,7 @@ import ( "github.com/deckhouse/virtualization-controller/pkg/logger" virtv2 "github.com/deckhouse/virtualization/api/core/v1alpha2" "github.com/deckhouse/virtualization/api/core/v1alpha2/cvicondition" + "github.com/deckhouse/virtualization/api/core/v1alpha2/vicondition" ) type ObjectRefDataSource struct { @@ -333,7 +334,9 @@ func (ds ObjectRefDataSource) Validate(ctx context.Context, cvi *virtv2.ClusterV } if vi.Spec.Storage == virtv2.StorageKubernetes || vi.Spec.Storage == virtv2.StoragePersistentVolumeClaim { - if vi.Status.Phase != virtv2.ImageReady { + readyCondition, _ := conditions.GetCondition(vicondition.ReadyType, vi.Status.Conditions) + + if readyCondition.Status != metav1.ConditionTrue || !conditions.IsLastUpdated(readyCondition, vi) { return NewImageNotReadyError(cvi.Spec.DataSource.ObjectRef.Name) } return nil diff --git a/images/virtualization-artifact/pkg/controller/cvi/internal/suite_test.go b/images/virtualization-artifact/pkg/controller/cvi/internal/suite_test.go new file mode 100644 index 0000000000..68c60dea80 --- /dev/null +++ b/images/virtualization-artifact/pkg/controller/cvi/internal/suite_test.go @@ -0,0 +1,29 @@ +/* +Copyright 2024 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 internal + +import ( + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +func TestHandlers(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Handlers") +} diff --git a/images/virtualization-artifact/pkg/controller/cvi/internal/watcher/cvi_watcher.go b/images/virtualization-artifact/pkg/controller/cvi/internal/watcher/cvi_watcher.go new file mode 100644 index 0000000000..31b9a03817 --- /dev/null +++ b/images/virtualization-artifact/pkg/controller/cvi/internal/watcher/cvi_watcher.go @@ -0,0 +1,124 @@ +/* +Copyright 2025 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" + + "k8s.io/apimachinery/pkg/types" + "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/virtualization-controller/pkg/controller/conditions" + "github.com/deckhouse/virtualization-controller/pkg/logger" + virtv2 "github.com/deckhouse/virtualization/api/core/v1alpha2" + "github.com/deckhouse/virtualization/api/core/v1alpha2/cvicondition" +) + +type ClusterVirtualImageWatcher struct { + client client.Client +} + +func NewClusterVirtualImageWatcher(client client.Client) *ClusterVirtualImageWatcher { + return &ClusterVirtualImageWatcher{ + client: client, + } +} + +func (w ClusterVirtualImageWatcher) Watch(mgr manager.Manager, ctr controller.Controller) error { + return ctr.Watch( + source.Kind(mgr.GetCache(), &virtv2.ClusterVirtualImage{}), + handler.EnqueueRequestsFromMapFunc(w.enqueueRequests), + predicate.Funcs{ + CreateFunc: func(e event.CreateEvent) bool { + return true + }, + DeleteFunc: func(e event.DeleteEvent) bool { + return true + }, + UpdateFunc: w.filterUpdateEvents, + }, + ) +} + +func (w ClusterVirtualImageWatcher) enqueueRequests(ctx context.Context, obj client.Object) (requests []reconcile.Request) { + var cviList virtv2.ClusterVirtualImageList + err := w.client.List(ctx, &cviList) + if err != nil { + logger.FromContext(ctx).Error(fmt.Sprintf("failed to list cvi: %s", err)) + return + } + + // We need to trigger reconcile for the cvi resources that use changed image as a datasource so they can continue provisioning. + for _, cvi := range cviList.Items { + if cvi.Spec.DataSource.Type != virtv2.DataSourceTypeObjectRef || cvi.Spec.DataSource.ObjectRef == nil { + continue + } + + if cvi.Spec.DataSource.ObjectRef.Kind != virtv2.ClusterVirtualImageKind || cvi.Spec.DataSource.ObjectRef.Name != obj.GetName() { + continue + } + + requests = append(requests, reconcile.Request{ + NamespacedName: types.NamespacedName{ + Name: cvi.Name, + }, + }) + } + + cvi, ok := obj.(*virtv2.ClusterVirtualImage) + if ok && cvi.Spec.DataSource.Type == virtv2.DataSourceTypeObjectRef { + if cvi.Spec.DataSource.ObjectRef != nil && cvi.Spec.DataSource.ObjectRef.Kind == virtv2.ClusterVirtualImageKind { + // Need to trigger reconcile for update InUse condition. + requests = append(requests, reconcile.Request{ + NamespacedName: types.NamespacedName{ + Name: cvi.Spec.DataSource.ObjectRef.Name, + }, + }) + } + } + + return +} + +func (w ClusterVirtualImageWatcher) filterUpdateEvents(e event.UpdateEvent) bool { + oldCVI, ok := e.ObjectOld.(*virtv2.ClusterVirtualImage) + if !ok { + return false + } + + newCVI, ok := e.ObjectNew.(*virtv2.ClusterVirtualImage) + if !ok { + return false + } + + oldReadyCondition, _ := conditions.GetCondition(cvicondition.ReadyType, oldCVI.Status.Conditions) + newReadyCondition, _ := conditions.GetCondition(cvicondition.ReadyType, newCVI.Status.Conditions) + + if oldCVI.Status.Phase != newCVI.Status.Phase || oldReadyCondition.Status != newReadyCondition.Status { + return true + } + + return false +} diff --git a/images/virtualization-artifact/pkg/controller/cvi/internal/watcher/vd_watcher.go b/images/virtualization-artifact/pkg/controller/cvi/internal/watcher/vd_watcher.go new file mode 100644 index 0000000000..79985805b1 --- /dev/null +++ b/images/virtualization-artifact/pkg/controller/cvi/internal/watcher/vd_watcher.go @@ -0,0 +1,130 @@ +/* +Copyright 2025 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" + + "k8s.io/apimachinery/pkg/types" + "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/virtualization-controller/pkg/controller/conditions" + "github.com/deckhouse/virtualization-controller/pkg/logger" + virtv2 "github.com/deckhouse/virtualization/api/core/v1alpha2" + "github.com/deckhouse/virtualization/api/core/v1alpha2/vdcondition" +) + +type VirtualDiskWatcher struct { + client client.Client +} + +func NewVirtualDiskWatcher(client client.Client) *VirtualDiskWatcher { + return &VirtualDiskWatcher{ + client: client, + } +} + +func (w VirtualDiskWatcher) Watch(mgr manager.Manager, ctr controller.Controller) error { + return ctr.Watch( + source.Kind(mgr.GetCache(), &virtv2.VirtualDisk{}), + handler.EnqueueRequestsFromMapFunc(w.enqueueRequests), + predicate.Funcs{ + CreateFunc: func(e event.CreateEvent) bool { + return true + }, + DeleteFunc: func(e event.DeleteEvent) bool { + return true + }, + UpdateFunc: w.filterUpdateEvents, + }, + ) +} + +func (w VirtualDiskWatcher) enqueueRequests(ctx context.Context, obj client.Object) (requests []reconcile.Request) { + var cviList virtv2.ClusterVirtualImageList + err := w.client.List(ctx, &cviList) + if err != nil { + logger.FromContext(ctx).Error(fmt.Sprintf("failed to list cvi: %s", err)) + return + } + + // We need to trigger reconcile for the cvi resources that use changed disk as a datasource so they can continue provisioning. + for _, cvi := range cviList.Items { + if cvi.Spec.DataSource.Type != virtv2.DataSourceTypeObjectRef || cvi.Spec.DataSource.ObjectRef == nil { + continue + } + + if cvi.Spec.DataSource.ObjectRef.Kind != virtv2.VirtualDiskKind || cvi.Spec.DataSource.ObjectRef.Name != obj.GetName() { + continue + } + + requests = append(requests, reconcile.Request{ + NamespacedName: types.NamespacedName{ + Name: cvi.Name, + }, + }) + } + + vd, ok := obj.(*virtv2.VirtualDisk) + if ok && vd.Spec.DataSource != nil && vd.Spec.DataSource.Type == virtv2.DataSourceTypeObjectRef { + if vd.Spec.DataSource.ObjectRef != nil && vd.Spec.DataSource.ObjectRef.Kind == virtv2.ClusterVirtualImageKind { + // Need to trigger reconcile for update InUse condition. + requests = append(requests, reconcile.Request{ + NamespacedName: types.NamespacedName{ + Name: vd.Spec.DataSource.ObjectRef.Name, + }, + }) + } + } + + return +} + +func (w VirtualDiskWatcher) filterUpdateEvents(e event.UpdateEvent) bool { + oldVD, ok := e.ObjectOld.(*virtv2.VirtualDisk) + if !ok { + return false + } + + newVD, ok := e.ObjectNew.(*virtv2.VirtualDisk) + if !ok { + return false + } + + oldInUseCondition, _ := conditions.GetCondition(vdcondition.InUseType, oldVD.Status.Conditions) + newInUseCondition, _ := conditions.GetCondition(vdcondition.InUseType, newVD.Status.Conditions) + + oldReadyCondition, _ := conditions.GetCondition(vdcondition.ReadyType, oldVD.Status.Conditions) + newReadyCondition, _ := conditions.GetCondition(vdcondition.ReadyType, newVD.Status.Conditions) + + if oldVD.Status.Phase != newVD.Status.Phase || + len(oldVD.Status.AttachedToVirtualMachines) != len(newVD.Status.AttachedToVirtualMachines) || + oldInUseCondition.Status != newInUseCondition.Status || + oldReadyCondition.Status != newReadyCondition.Status { + return true + } + + return false +} diff --git a/images/virtualization-artifact/pkg/controller/cvi/internal/watcher/vi_watcher.go b/images/virtualization-artifact/pkg/controller/cvi/internal/watcher/vi_watcher.go new file mode 100644 index 0000000000..3144deb1f8 --- /dev/null +++ b/images/virtualization-artifact/pkg/controller/cvi/internal/watcher/vi_watcher.go @@ -0,0 +1,124 @@ +/* +Copyright 2025 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" + + "k8s.io/apimachinery/pkg/types" + "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/virtualization-controller/pkg/controller/conditions" + "github.com/deckhouse/virtualization-controller/pkg/logger" + virtv2 "github.com/deckhouse/virtualization/api/core/v1alpha2" + "github.com/deckhouse/virtualization/api/core/v1alpha2/vicondition" +) + +type VirtualImageWatcher struct { + client client.Client +} + +func NewVirtualImageWatcher(client client.Client) *VirtualImageWatcher { + return &VirtualImageWatcher{ + client: client, + } +} + +func (w VirtualImageWatcher) Watch(mgr manager.Manager, ctr controller.Controller) error { + return ctr.Watch( + source.Kind(mgr.GetCache(), &virtv2.VirtualImage{}), + handler.EnqueueRequestsFromMapFunc(w.enqueueRequests), + predicate.Funcs{ + CreateFunc: func(e event.CreateEvent) bool { + return true + }, + DeleteFunc: func(e event.DeleteEvent) bool { + return true + }, + UpdateFunc: w.filterUpdateEvents, + }, + ) +} + +func (w VirtualImageWatcher) enqueueRequests(ctx context.Context, obj client.Object) (requests []reconcile.Request) { + var cviList virtv2.ClusterVirtualImageList + err := w.client.List(ctx, &cviList) + if err != nil { + logger.FromContext(ctx).Error(fmt.Sprintf("failed to list cvi: %s", err)) + return + } + + // We need to trigger reconcile for the cvi resources that use changed image as a datasource so they can continue provisioning. + for _, cvi := range cviList.Items { + if cvi.Spec.DataSource.Type != virtv2.DataSourceTypeObjectRef || cvi.Spec.DataSource.ObjectRef == nil { + continue + } + + if cvi.Spec.DataSource.ObjectRef.Kind != virtv2.VirtualImageKind || cvi.Spec.DataSource.ObjectRef.Name != obj.GetName() { + continue + } + + requests = append(requests, reconcile.Request{ + NamespacedName: types.NamespacedName{ + Name: cvi.Name, + }, + }) + } + + vi, ok := obj.(*virtv2.VirtualImage) + if ok && vi.Spec.DataSource.Type == virtv2.DataSourceTypeObjectRef { + if vi.Spec.DataSource.ObjectRef != nil && vi.Spec.DataSource.ObjectRef.Kind == virtv2.ClusterVirtualImageKind { + // Need to trigger reconcile for update InUse condition. + requests = append(requests, reconcile.Request{ + NamespacedName: types.NamespacedName{ + Name: vi.Spec.DataSource.ObjectRef.Name, + }, + }) + } + } + + return +} + +func (w VirtualImageWatcher) filterUpdateEvents(e event.UpdateEvent) bool { + oldVI, ok := e.ObjectOld.(*virtv2.VirtualImage) + if !ok { + return false + } + + newVI, ok := e.ObjectNew.(*virtv2.VirtualImage) + if !ok { + return false + } + + oldReadyCondition, _ := conditions.GetCondition(vicondition.ReadyType, oldVI.Status.Conditions) + newReadyCondition, _ := conditions.GetCondition(vicondition.ReadyType, newVI.Status.Conditions) + + if oldVI.Status.Phase != newVI.Status.Phase || oldReadyCondition.Status != newReadyCondition.Status { + return true + } + + return false +} diff --git a/images/virtualization-artifact/pkg/controller/dvcr_data_source.go b/images/virtualization-artifact/pkg/controller/dvcr_data_source.go index d74970833f..c613ba2b74 100644 --- a/images/virtualization-artifact/pkg/controller/dvcr_data_source.go +++ b/images/virtualization-artifact/pkg/controller/dvcr_data_source.go @@ -26,7 +26,10 @@ import ( "github.com/deckhouse/virtualization-controller/pkg/common/imageformat" "github.com/deckhouse/virtualization-controller/pkg/common/object" + "github.com/deckhouse/virtualization-controller/pkg/controller/conditions" virtv2 "github.com/deckhouse/virtualization/api/core/v1alpha2" + "github.com/deckhouse/virtualization/api/core/v1alpha2/cvicondition" + "github.com/deckhouse/virtualization/api/core/v1alpha2/vicondition" ) type DVCRDataSource struct { @@ -56,11 +59,13 @@ func NewDVCRDataSourcesForCVMI(ctx context.Context, ds virtv2.ClusterVirtualImag } if vmi != nil { + readyCondition, _ := conditions.GetCondition(vicondition.ReadyType, vmi.Status.Conditions) + dsDVCR.uid = vmi.UID dsDVCR.size = vmi.Status.Size dsDVCR.format = vmi.Status.Format dsDVCR.meta = vmi.GetObjectMeta() - dsDVCR.isReady = vmi.Status.Phase == virtv2.ImageReady + dsDVCR.isReady = readyCondition.Status == metav1.ConditionTrue && conditions.IsLastUpdated(readyCondition, vmi) dsDVCR.target = vmi.Status.Target.RegistryURL } } @@ -73,11 +78,13 @@ func NewDVCRDataSourcesForCVMI(ctx context.Context, ds virtv2.ClusterVirtualImag } if cvmi != nil { + readyCondition, _ := conditions.GetCondition(cvicondition.ReadyType, cvmi.Status.Conditions) + dsDVCR.uid = cvmi.UID dsDVCR.size = cvmi.Status.Size dsDVCR.meta = cvmi.GetObjectMeta() dsDVCR.format = cvmi.Status.Format - dsDVCR.isReady = cvmi.Status.Phase == virtv2.ImageReady + dsDVCR.isReady = readyCondition.Status == metav1.ConditionTrue && conditions.IsLastUpdated(readyCondition, cvmi) dsDVCR.target = cvmi.Status.Target.RegistryURL } } @@ -108,11 +115,13 @@ func NewDVCRDataSourcesForVMI(ctx context.Context, ds virtv2.VirtualImageDataSou return DVCRDataSource{}, fmt.Errorf("the DVCR not used for virtual images with storage type '%s'", vmi.Spec.Storage) } + readyCondition, _ := conditions.GetCondition(vicondition.ReadyType, vmi.Status.Conditions) + dsDVCR.uid = vmi.UID dsDVCR.size = vmi.Status.Size dsDVCR.format = vmi.Status.Format dsDVCR.meta = vmi.GetObjectMeta() - dsDVCR.isReady = vmi.Status.Phase == virtv2.ImageReady + dsDVCR.isReady = readyCondition.Status == metav1.ConditionTrue && conditions.IsLastUpdated(readyCondition, vmi) dsDVCR.target = vmi.Status.Target.RegistryURL } } @@ -125,11 +134,13 @@ func NewDVCRDataSourcesForVMI(ctx context.Context, ds virtv2.VirtualImageDataSou } if cvmi != nil { + readyCondition, _ := conditions.GetCondition(vicondition.ReadyType, cvmi.Status.Conditions) + dsDVCR.uid = cvmi.UID dsDVCR.size = cvmi.Status.Size dsDVCR.meta = cvmi.GetObjectMeta() dsDVCR.format = cvmi.Status.Format - dsDVCR.isReady = cvmi.Status.Phase == virtv2.ImageReady + dsDVCR.isReady = readyCondition.Status == metav1.ConditionTrue && conditions.IsLastUpdated(readyCondition, cvmi) dsDVCR.target = cvmi.Status.Target.RegistryURL } } @@ -156,11 +167,13 @@ func NewDVCRDataSourcesForVMD(ctx context.Context, ds *virtv2.VirtualDiskDataSou } if vmi != nil { + readyCondition, _ := conditions.GetCondition(vicondition.ReadyType, vmi.Status.Conditions) + dsDVCR.uid = vmi.UID dsDVCR.size = vmi.Status.Size dsDVCR.format = vmi.Status.Format dsDVCR.meta = vmi.GetObjectMeta() - dsDVCR.isReady = vmi.Status.Phase == virtv2.ImageReady + dsDVCR.isReady = readyCondition.Status == metav1.ConditionTrue && conditions.IsLastUpdated(readyCondition, vmi) dsDVCR.target = vmi.Status.Target.RegistryURL } } @@ -173,11 +186,13 @@ func NewDVCRDataSourcesForVMD(ctx context.Context, ds *virtv2.VirtualDiskDataSou } if cvmi != nil { + readyCondition, _ := conditions.GetCondition(vicondition.ReadyType, cvmi.Status.Conditions) + dsDVCR.uid = cvmi.UID dsDVCR.size = cvmi.Status.Size dsDVCR.meta = cvmi.GetObjectMeta() dsDVCR.format = cvmi.Status.Format - dsDVCR.isReady = cvmi.Status.Phase == virtv2.ImageReady + dsDVCR.isReady = readyCondition.Status == metav1.ConditionTrue && conditions.IsLastUpdated(readyCondition, cvmi) dsDVCR.target = cvmi.Status.Target.RegistryURL } } diff --git a/images/virtualization-artifact/pkg/controller/indexer/cvi_indexer.go b/images/virtualization-artifact/pkg/controller/indexer/cvi_indexer.go index d0c764fce3..603158761f 100644 --- a/images/virtualization-artifact/pkg/controller/indexer/cvi_indexer.go +++ b/images/virtualization-artifact/pkg/controller/indexer/cvi_indexer.go @@ -23,6 +23,48 @@ import ( virtv2 "github.com/deckhouse/virtualization/api/core/v1alpha2" ) +func IndexCVIByCVIDataSource() (obj client.Object, field string, extractValue client.IndexerFunc) { + return &virtv2.ClusterVirtualImage{}, IndexFieldCVIByCVIDataSource, IndexCVIByCVIDataSourceIndexerFunc +} + +func IndexCVIByCVIDataSourceIndexerFunc(object client.Object) []string { + cvi, ok := object.(*virtv2.ClusterVirtualImage) + if !ok || cvi == nil { + return nil + } + + if cvi.Spec.DataSource.Type != virtv2.DataSourceTypeObjectRef { + return nil + } + + if cvi.Spec.DataSource.ObjectRef == nil || cvi.Spec.DataSource.ObjectRef.Kind != virtv2.ClusterVirtualImageKind { + return nil + } + + return []string{cvi.Spec.DataSource.ObjectRef.Name} +} + +func IndexCVIByVIDataSource() (obj client.Object, field string, extractValue client.IndexerFunc) { + return &virtv2.ClusterVirtualImage{}, IndexFieldCVIByVIDataSource, IndexCVIByVIDataSourceIndexerFunc +} + +func IndexCVIByVIDataSourceIndexerFunc(object client.Object) []string { + cvi, ok := object.(*virtv2.ClusterVirtualImage) + if !ok || cvi == nil { + return nil + } + + if cvi.Spec.DataSource.Type != virtv2.DataSourceTypeObjectRef { + return nil + } + + if cvi.Spec.DataSource.ObjectRef == nil || cvi.Spec.DataSource.ObjectRef.Kind != virtv2.VirtualImageKind { + return nil + } + + return []string{cvi.Spec.DataSource.ObjectRef.Name} +} + func IndexCVIByVDSnapshot() (obj client.Object, field string, extractValue client.IndexerFunc) { return &virtv2.ClusterVirtualImage{}, IndexFieldCVIByVDSnapshot, func(object client.Object) []string { cvi, ok := object.(*virtv2.ClusterVirtualImage) diff --git a/images/virtualization-artifact/pkg/controller/indexer/indexer.go b/images/virtualization-artifact/pkg/controller/indexer/indexer.go index bc24be429f..1c263baca9 100644 --- a/images/virtualization-artifact/pkg/controller/indexer/indexer.go +++ b/images/virtualization-artifact/pkg/controller/indexer/indexer.go @@ -53,6 +53,14 @@ const ( IndexFieldVMBDAByVM = "spec.virtualMachineName" + IndexFieldVDByCVIDataSource = "vd,spec.DataSource.ObjectRef.Name,.Kind=ClusterVirtualImage" + IndexFieldVIByCVIDataSource = "vi,spec.DataSource.ObjectRef.Name,.Kind=ClusterVirtualImage" + IndexFieldCVIByCVIDataSource = "cvi,spec.DataSource.ObjectRef.Name,.Kind=ClusterVirtualImage" + + IndexFieldVDByVIDataSource = "vd,spec.DataSource.ObjectRef.Name,.Kind=VirtualImage" + IndexFieldVIByVIDataSource = "vi,spec.DataSource.ObjectRef.Name,.Kind=VirtualImage" + IndexFieldCVIByVIDataSource = "cvi,spec.DataSource.ObjectRef.Name,.Kind=VirtualImage" + IndexFieldVMMACByVM = "status.virtualMachine,Kind=VirtualMachineMACAddress" IndexFieldVMMACByAddress = "spec.address|status.address" @@ -76,6 +84,12 @@ var IndexGetters = []IndexGetter{ IndexCVIByVDSnapshot, IndexVMIPByAddress, IndexVMBDAByVM, + IndexVDByCVIDataSource, + IndexVIByCVIDataSource, + IndexCVIByCVIDataSource, + IndexVDByVIDataSource, + IndexVIByVIDataSource, + IndexCVIByVIDataSource, IndexVMMACByVM, IndexVMMACByAddress, IndexVMMACLeaseByVMMAC, diff --git a/images/virtualization-artifact/pkg/controller/indexer/vd_indexer.go b/images/virtualization-artifact/pkg/controller/indexer/vd_indexer.go index 0a4e96d6e5..80362fb9ed 100644 --- a/images/virtualization-artifact/pkg/controller/indexer/vd_indexer.go +++ b/images/virtualization-artifact/pkg/controller/indexer/vd_indexer.go @@ -58,3 +58,45 @@ func IndexVDByStorageClass() (obj client.Object, field string, extractValue clie } } } + +func IndexVDByCVIDataSource() (obj client.Object, field string, extractValue client.IndexerFunc) { + return &virtv2.VirtualDisk{}, IndexFieldVDByCVIDataSource, IndexVDByCVIDataSourceIndexerFunc +} + +func IndexVDByCVIDataSourceIndexerFunc(object client.Object) []string { + vd, ok := object.(*virtv2.VirtualDisk) + if !ok || vd == nil { + return nil + } + + if vd.Spec.DataSource == nil || vd.Spec.DataSource.Type != virtv2.DataSourceTypeObjectRef { + return nil + } + + if vd.Spec.DataSource.ObjectRef == nil || vd.Spec.DataSource.ObjectRef.Kind != virtv2.ClusterVirtualImageKind { + return nil + } + + return []string{vd.Spec.DataSource.ObjectRef.Name} +} + +func IndexVDByVIDataSource() (obj client.Object, field string, extractValue client.IndexerFunc) { + return &virtv2.VirtualDisk{}, IndexFieldVDByVIDataSource, IndexVDByVIDataSourceIndexerFunc +} + +func IndexVDByVIDataSourceIndexerFunc(object client.Object) []string { + vd, ok := object.(*virtv2.VirtualDisk) + if !ok || vd == nil { + return nil + } + + if vd.Spec.DataSource == nil || vd.Spec.DataSource.Type != virtv2.DataSourceTypeObjectRef { + return nil + } + + if vd.Spec.DataSource.ObjectRef == nil || vd.Spec.DataSource.ObjectRef.Kind != virtv2.VirtualImageKind { + return nil + } + + return []string{vd.Spec.DataSource.ObjectRef.Name} +} diff --git a/images/virtualization-artifact/pkg/controller/indexer/vi_indexer.go b/images/virtualization-artifact/pkg/controller/indexer/vi_indexer.go index a00f0c3ed2..9e3d719be0 100644 --- a/images/virtualization-artifact/pkg/controller/indexer/vi_indexer.go +++ b/images/virtualization-artifact/pkg/controller/indexer/vi_indexer.go @@ -58,3 +58,45 @@ func IndexVIByStorageClass() (obj client.Object, field string, extractValue clie } } } + +func IndexVIByCVIDataSource() (obj client.Object, field string, extractValue client.IndexerFunc) { + return &virtv2.VirtualImage{}, IndexFieldVIByCVIDataSource, IndexVIByCVIDataSourceIndexerFunc +} + +func IndexVIByCVIDataSourceIndexerFunc(object client.Object) []string { + vi, ok := object.(*virtv2.VirtualImage) + if !ok || vi == nil { + return nil + } + + if vi.Spec.DataSource.Type != virtv2.DataSourceTypeObjectRef { + return nil + } + + if vi.Spec.DataSource.ObjectRef == nil || vi.Spec.DataSource.ObjectRef.Kind != virtv2.ClusterVirtualImageKind { + return nil + } + + return []string{vi.Spec.DataSource.ObjectRef.Name} +} + +func IndexVIByVIDataSource() (obj client.Object, field string, extractValue client.IndexerFunc) { + return &virtv2.VirtualImage{}, IndexFieldVIByVIDataSource, IndexVIByVIDataSourceIndexerFunc +} + +func IndexVIByVIDataSourceIndexerFunc(object client.Object) []string { + vi, ok := object.(*virtv2.VirtualImage) + if !ok || vi == nil { + return nil + } + + if vi.Spec.DataSource.Type != virtv2.DataSourceTypeObjectRef { + return nil + } + + if vi.Spec.DataSource.ObjectRef == nil || vi.Spec.DataSource.ObjectRef.Kind != virtv2.VirtualImageKind { + return nil + } + + return []string{vi.Spec.DataSource.ObjectRef.Name} +} diff --git a/images/virtualization-artifact/pkg/controller/vd/internal/life_cycle.go b/images/virtualization-artifact/pkg/controller/vd/internal/life_cycle.go index ad0d964ed1..4ce87295a8 100644 --- a/images/virtualization-artifact/pkg/controller/vd/internal/life_cycle.go +++ b/images/virtualization-artifact/pkg/controller/vd/internal/life_cycle.go @@ -25,7 +25,9 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/reconcile" + "github.com/deckhouse/virtualization-controller/pkg/common/annotations" "github.com/deckhouse/virtualization-controller/pkg/controller/conditions" + "github.com/deckhouse/virtualization-controller/pkg/controller/supplements" "github.com/deckhouse/virtualization-controller/pkg/controller/vd/internal/source" "github.com/deckhouse/virtualization-controller/pkg/eventrecord" virtv2 "github.com/deckhouse/virtualization/api/core/v1alpha2" @@ -33,18 +35,20 @@ import ( ) type LifeCycleHandler struct { - client client.Client - blank source.Handler - sources Sources - recorder eventrecord.EventRecorderLogger + client client.Client + blank source.Handler + sources Sources + recorder eventrecord.EventRecorderLogger + diskService DiskService } -func NewLifeCycleHandler(recorder eventrecord.EventRecorderLogger, blank source.Handler, sources Sources, client client.Client) *LifeCycleHandler { +func NewLifeCycleHandler(recorder eventrecord.EventRecorderLogger, blank source.Handler, sources Sources, client client.Client, diskService DiskService) *LifeCycleHandler { return &LifeCycleHandler{ - client: client, - blank: blank, - sources: sources, - recorder: recorder, + client: client, + blank: blank, + sources: sources, + recorder: recorder, + diskService: diskService, } } @@ -62,6 +66,19 @@ func (h LifeCycleHandler) Handle(ctx context.Context, vd *virtv2.VirtualDisk) (r if vd.DeletionTimestamp != nil { vd.Status.Phase = virtv2.DiskTerminating + if readyCondition.Status == metav1.ConditionTrue { + cb := conditions.NewConditionBuilder(vdcondition.ReadyType).Generation(vd.Generation) + + supgen := supplements.NewGenerator(annotations.VDShortName, vd.Name, vd.Namespace, vd.UID) + pvc, err := h.diskService.GetPersistentVolumeClaim(ctx, supgen) + if err != nil { + return reconcile.Result{}, err + } + + source.SetPhaseConditionForFinishedDisk(pvc, cb, &vd.Status.Phase, supgen) + + conditions.SetCondition(cb, &vd.Status.Conditions) + } return reconcile.Result{}, nil } diff --git a/images/virtualization-artifact/pkg/controller/vd/internal/life_cycle_test.go b/images/virtualization-artifact/pkg/controller/vd/internal/life_cycle_test.go index 607116f502..5ec8892edd 100644 --- a/images/virtualization-artifact/pkg/controller/vd/internal/life_cycle_test.go +++ b/images/virtualization-artifact/pkg/controller/vd/internal/life_cycle_test.go @@ -86,7 +86,7 @@ var _ = Describe("LifeCycleHandler Run", func() { recorder := &eventrecord.EventRecorderLoggerMock{ EventFunc: func(_ client.Object, _, _, _ string) {}, } - handler := NewLifeCycleHandler(recorder, nil, &sourcesMock, nil) + handler := NewLifeCycleHandler(recorder, nil, &sourcesMock, nil, nil) ctx := logger.ToContext(context.TODO(), slog.Default()) @@ -188,7 +188,7 @@ var _ = Describe("LifeCycleHandler Run", func() { EventFunc: func(_ client.Object, _, _, _ string) {}, } - handler := NewLifeCycleHandler(recorder, nil, &sourcesMock, nil) + handler := NewLifeCycleHandler(recorder, nil, &sourcesMock, nil, nil) ctx := logger.ToContext(context.TODO(), slog.Default()) diff --git a/images/virtualization-artifact/pkg/controller/vd/internal/source/http.go b/images/virtualization-artifact/pkg/controller/vd/internal/source/http.go index 5ad8cea6c1..330bb4674d 100644 --- a/images/virtualization-artifact/pkg/controller/vd/internal/source/http.go +++ b/images/virtualization-artifact/pkg/controller/vd/internal/source/http.go @@ -117,7 +117,7 @@ func (ds HTTPDataSource) Sync(ctx context.Context, vd *virtv2.VirtualDisk) (reco case IsDiskProvisioningFinished(condition): log.Debug("Disk provisioning finished: clean up") - setPhaseConditionForFinishedDisk(pvc, cb, &vd.Status.Phase, supgen) + SetPhaseConditionForFinishedDisk(pvc, cb, &vd.Status.Phase, supgen) // Protect Ready Disk and underlying PVC. err = ds.diskService.Protect(ctx, vd, nil, pvc) diff --git a/images/virtualization-artifact/pkg/controller/vd/internal/source/object_ref_cvi.go b/images/virtualization-artifact/pkg/controller/vd/internal/source/object_ref_cvi.go index a5fc7fa1f8..afd9a58a4a 100644 --- a/images/virtualization-artifact/pkg/controller/vd/internal/source/object_ref_cvi.go +++ b/images/virtualization-artifact/pkg/controller/vd/internal/source/object_ref_cvi.go @@ -22,6 +22,7 @@ import ( "fmt" corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" cdiv1 "kubevirt.io/containerized-data-importer-api/pkg/apis/core/v1beta1" "sigs.k8s.io/controller-runtime/pkg/client" @@ -34,6 +35,7 @@ import ( "github.com/deckhouse/virtualization-controller/pkg/controller/supplements" "github.com/deckhouse/virtualization-controller/pkg/controller/vd/internal/source/step" virtv2 "github.com/deckhouse/virtualization/api/core/v1alpha2" + "github.com/deckhouse/virtualization/api/core/v1alpha2/cvicondition" "github.com/deckhouse/virtualization/api/core/v1alpha2/vdcondition" ) @@ -93,11 +95,13 @@ func (ds ObjectRefClusterVirtualImage) Validate(ctx context.Context, vd *virtv2. return fmt.Errorf("fetch vi %q: %w", cviRefKey, err) } - if cviRef == nil { - return NewClusterImageNotFoundError(vd.Spec.DataSource.ObjectRef.Name) + if cviRef == nil || cviRef.Status.Target.RegistryURL == "" { + return NewClusterImageNotReadyError(vd.Spec.DataSource.ObjectRef.Name) } - if cviRef.Status.Phase != virtv2.ImageReady || cviRef.Status.Target.RegistryURL == "" { + cviRefReady, _ := conditions.GetCondition(cvicondition.ReadyType, cviRef.Status.Conditions) + + if cviRefReady.Status != metav1.ConditionTrue { return NewClusterImageNotReadyError(vd.Spec.DataSource.ObjectRef.Name) } diff --git a/images/virtualization-artifact/pkg/controller/vd/internal/source/object_ref_vi.go b/images/virtualization-artifact/pkg/controller/vd/internal/source/object_ref_vi.go index 5d8d372cf2..3c1bed9a12 100644 --- a/images/virtualization-artifact/pkg/controller/vd/internal/source/object_ref_vi.go +++ b/images/virtualization-artifact/pkg/controller/vd/internal/source/object_ref_vi.go @@ -22,6 +22,7 @@ import ( "fmt" corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" cdiv1 "kubevirt.io/containerized-data-importer-api/pkg/apis/core/v1beta1" "sigs.k8s.io/controller-runtime/pkg/client" @@ -35,6 +36,7 @@ import ( "github.com/deckhouse/virtualization-controller/pkg/controller/vd/internal/source/step" virtv2 "github.com/deckhouse/virtualization/api/core/v1alpha2" "github.com/deckhouse/virtualization/api/core/v1alpha2/vdcondition" + "github.com/deckhouse/virtualization/api/core/v1alpha2/vicondition" ) type ObjectRefVirtualImage struct { @@ -94,10 +96,11 @@ func (ds ObjectRefVirtualImage) Validate(ctx context.Context, vd *virtv2.Virtual } if viRef == nil { - return NewImageNotFoundError(vd.Spec.DataSource.ObjectRef.Name) + return NewImageNotReadyError(vd.Spec.DataSource.ObjectRef.Name) } - if viRef.Status.Phase != virtv2.ImageReady { + viRefReady, _ := conditions.GetCondition(vicondition.ReadyType, viRef.Status.Conditions) + if viRefReady.Status != metav1.ConditionTrue { return NewImageNotReadyError(vd.Spec.DataSource.ObjectRef.Name) } diff --git a/images/virtualization-artifact/pkg/controller/vd/internal/source/registry.go b/images/virtualization-artifact/pkg/controller/vd/internal/source/registry.go index 5a005aea53..5e0c26c631 100644 --- a/images/virtualization-artifact/pkg/controller/vd/internal/source/registry.go +++ b/images/virtualization-artifact/pkg/controller/vd/internal/source/registry.go @@ -118,7 +118,7 @@ func (ds RegistryDataSource) Sync(ctx context.Context, vd *virtv2.VirtualDisk) ( case IsDiskProvisioningFinished(condition): log.Debug("Disk provisioning finished: clean up") - setPhaseConditionForFinishedDisk(pvc, cb, &vd.Status.Phase, supgen) + SetPhaseConditionForFinishedDisk(pvc, cb, &vd.Status.Phase, supgen) // Protect Ready Disk and underlying PVC. err = ds.diskService.Protect(ctx, vd, nil, pvc) diff --git a/images/virtualization-artifact/pkg/controller/vd/internal/source/sources.go b/images/virtualization-artifact/pkg/controller/vd/internal/source/sources.go index 1bf78398d6..fe5354b90d 100644 --- a/images/virtualization-artifact/pkg/controller/vd/internal/source/sources.go +++ b/images/virtualization-artifact/pkg/controller/vd/internal/source/sources.go @@ -98,7 +98,7 @@ func IsDiskProvisioningFinished(c metav1.Condition) bool { return c.Reason == vdcondition.Ready.String() || c.Reason == vdcondition.Lost.String() } -func setPhaseConditionForFinishedDisk( +func SetPhaseConditionForFinishedDisk( pvc *corev1.PersistentVolumeClaim, cb *conditions.ConditionBuilder, phase *virtv2.DiskPhase, diff --git a/images/virtualization-artifact/pkg/controller/vd/internal/source/upload.go b/images/virtualization-artifact/pkg/controller/vd/internal/source/upload.go index db5b5f51c6..af9541e459 100644 --- a/images/virtualization-artifact/pkg/controller/vd/internal/source/upload.go +++ b/images/virtualization-artifact/pkg/controller/vd/internal/source/upload.go @@ -125,7 +125,7 @@ func (ds UploadDataSource) Sync(ctx context.Context, vd *virtv2.VirtualDisk) (re case IsDiskProvisioningFinished(condition): log.Debug("Disk provisioning finished: clean up") - setPhaseConditionForFinishedDisk(pvc, cb, &vd.Status.Phase, supgen) + SetPhaseConditionForFinishedDisk(pvc, cb, &vd.Status.Phase, supgen) // Protect Ready Disk and underlying PVC. err = ds.diskService.Protect(ctx, vd, nil, pvc) diff --git a/images/virtualization-artifact/pkg/controller/vd/vd_controller.go b/images/virtualization-artifact/pkg/controller/vd/vd_controller.go index e28a49dd60..bf2fdea233 100644 --- a/images/virtualization-artifact/pkg/controller/vd/vd_controller.go +++ b/images/virtualization-artifact/pkg/controller/vd/vd_controller.go @@ -81,7 +81,7 @@ func NewController( mgr.GetClient(), internal.NewStorageClassReadyHandler(scService), internal.NewDatasourceReadyHandler(recorder, blank, sources), - internal.NewLifeCycleHandler(recorder, blank, sources, mgr.GetClient()), + internal.NewLifeCycleHandler(recorder, blank, sources, mgr.GetClient(), disk), internal.NewSnapshottingHandler(disk), internal.NewResizingHandler(recorder, disk), internal.NewDeletionHandler(sources), diff --git a/images/virtualization-artifact/pkg/controller/vi/internal/attachee.go b/images/virtualization-artifact/pkg/controller/vi/internal/attachee.go deleted file mode 100644 index f62972a6ee..0000000000 --- a/images/virtualization-artifact/pkg/controller/vi/internal/attachee.go +++ /dev/null @@ -1,93 +0,0 @@ -/* -Copyright 2024 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 internal - -import ( - "context" - "fmt" - - "sigs.k8s.io/controller-runtime/pkg/client" - "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" - "sigs.k8s.io/controller-runtime/pkg/reconcile" - - "github.com/deckhouse/virtualization-controller/pkg/logger" - virtv2 "github.com/deckhouse/virtualization/api/core/v1alpha2" -) - -type AttacheeHandler struct { - client client.Client -} - -func NewAttacheeHandler(client client.Client) *AttacheeHandler { - return &AttacheeHandler{ - client: client, - } -} - -func (h AttacheeHandler) Handle(ctx context.Context, vi *virtv2.VirtualImage) (reconcile.Result, error) { - log := logger.FromContext(ctx).With(logger.SlogHandler("attachee")) - - hasAttachedVM, err := h.hasAttachedVM(ctx, vi) - if err != nil { - return reconcile.Result{}, err - } - - switch { - case !hasAttachedVM: - log.Debug("Allow virtual image deletion") - controllerutil.RemoveFinalizer(vi, virtv2.FinalizerVIProtection) - case vi.DeletionTimestamp == nil: - log.Debug("Protect virtual image from deletion") - controllerutil.AddFinalizer(vi, virtv2.FinalizerVIProtection) - default: - log.Debug("Virtual image deletion is delayed: it's protected by virtual machines") - } - - return reconcile.Result{}, nil -} - -func (h AttacheeHandler) Name() string { - return "AttacheeHandler" -} - -func (h AttacheeHandler) hasAttachedVM(ctx context.Context, vi client.Object) (bool, error) { - var vms virtv2.VirtualMachineList - err := h.client.List(ctx, &vms, &client.ListOptions{ - Namespace: vi.GetNamespace(), - }) - if err != nil { - return false, fmt.Errorf("error getting virtual machines: %w", err) - } - - for _, vm := range vms.Items { - if h.isVIAttachedToVM(vi.GetName(), vm) { - return true, nil - } - } - - return false, nil -} - -func (h AttacheeHandler) isVIAttachedToVM(viName string, vm virtv2.VirtualMachine) bool { - for _, bda := range vm.Status.BlockDeviceRefs { - if bda.Kind == virtv2.ImageDevice && bda.Name == viName { - return true - } - } - - return false -} diff --git a/images/virtualization-artifact/pkg/controller/vi/internal/deletion.go b/images/virtualization-artifact/pkg/controller/vi/internal/deletion.go index 030e94d986..711f9beb5e 100644 --- a/images/virtualization-artifact/pkg/controller/vi/internal/deletion.go +++ b/images/virtualization-artifact/pkg/controller/vi/internal/deletion.go @@ -20,12 +20,15 @@ import ( "context" "time" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" "sigs.k8s.io/controller-runtime/pkg/reconcile" + "github.com/deckhouse/virtualization-controller/pkg/controller/conditions" "github.com/deckhouse/virtualization-controller/pkg/controller/vi/internal/source" "github.com/deckhouse/virtualization-controller/pkg/logger" virtv2 "github.com/deckhouse/virtualization/api/core/v1alpha2" + "github.com/deckhouse/virtualization/api/core/v1alpha2/vicondition" ) const deletionHandlerName = "DeletionHandler" @@ -44,6 +47,11 @@ func (h DeletionHandler) Handle(ctx context.Context, vi *virtv2.VirtualImage) (r log := logger.FromContext(ctx).With(logger.SlogHandler(deletionHandlerName)) if vi.DeletionTimestamp != nil { + inUseCondition, _ := conditions.GetCondition(vicondition.InUseType, vi.Status.Conditions) + if inUseCondition.Status != metav1.ConditionFalse || !conditions.IsLastUpdated(inUseCondition, vi) { + return reconcile.Result{}, nil + } + requeue, err := h.sources.CleanUp(ctx, vi) if err != nil { return reconcile.Result{}, err diff --git a/images/virtualization-artifact/pkg/controller/vi/internal/interfaces.go b/images/virtualization-artifact/pkg/controller/vi/internal/interfaces.go index 8caec70ae2..980f8842d6 100644 --- a/images/virtualization-artifact/pkg/controller/vi/internal/interfaces.go +++ b/images/virtualization-artifact/pkg/controller/vi/internal/interfaces.go @@ -37,7 +37,6 @@ type Sources interface { } type DiskService interface { - GetStorageClass(ctx context.Context, storageClassName *string) (*storagev1.StorageClass, error) GetPersistentVolumeClaim(ctx context.Context, sup *supplements.Generator) (*corev1.PersistentVolumeClaim, error) } diff --git a/images/virtualization-artifact/pkg/controller/vi/internal/inuse.go b/images/virtualization-artifact/pkg/controller/vi/internal/inuse.go new file mode 100644 index 0000000000..2cef5d1be5 --- /dev/null +++ b/images/virtualization-artifact/pkg/controller/vi/internal/inuse.go @@ -0,0 +1,242 @@ +/* +Copyright 2025 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 internal + +import ( + "context" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + + "github.com/deckhouse/virtualization-controller/pkg/controller/conditions" + "github.com/deckhouse/virtualization-controller/pkg/controller/indexer" + virtv2 "github.com/deckhouse/virtualization/api/core/v1alpha2" + "github.com/deckhouse/virtualization/api/core/v1alpha2/vicondition" +) + +const inUseHandlerName = "InUseHandler" + +type InUseHandler struct { + client client.Client +} + +func NewInUseHandler(client client.Client) *InUseHandler { + return &InUseHandler{ + client: client, + } +} + +func (h InUseHandler) Handle(ctx context.Context, vi *virtv2.VirtualImage) (reconcile.Result, error) { + cb := conditions.NewConditionBuilder(vicondition.InUse).Generation(vi.Generation) + readyCondition, _ := conditions.GetCondition(vicondition.ReadyType, vi.Status.Conditions) + if readyCondition.Status == metav1.ConditionFalse && + readyCondition.Reason != vicondition.Lost.String() && + conditions.IsLastUpdated(readyCondition, vi) { + cb. + Status(metav1.ConditionFalse). + Reason(vicondition.NotInUse). + Message("") + + conditions.SetCondition(cb, &vi.Status.Conditions) + return reconcile.Result{}, nil + } + if readyCondition.Status == metav1.ConditionUnknown || !conditions.IsLastUpdated(readyCondition, vi) { + cb. + Status(metav1.ConditionUnknown). + Reason(conditions.ReasonUnknown). + Message("") + + conditions.SetCondition(cb, &vi.Status.Conditions) + return reconcile.Result{}, nil + } + + hasVM, err := h.hasVMUsingImage(ctx, vi) + if err != nil { + return reconcile.Result{}, err + } + if hasVM { + setInUseConditionTrue(vi, cb) + return reconcile.Result{}, nil + } + + hasVMBDA, err := h.hasVMBDAsUsingImage(ctx, vi) + if err != nil { + return reconcile.Result{}, err + } + if hasVMBDA { + setInUseConditionTrue(vi, cb) + return reconcile.Result{}, nil + } + + hasVD, err := h.hasVDUsingImage(ctx, vi) + if err != nil { + return reconcile.Result{}, err + } + if hasVD { + setInUseConditionTrue(vi, cb) + return reconcile.Result{}, nil + } + + hasVI, err := h.hasVIUsingImage(ctx, vi) + if err != nil { + return reconcile.Result{}, err + } + if hasVI { + setInUseConditionTrue(vi, cb) + return reconcile.Result{}, nil + } + + hasCVI, err := h.hasCVIUsingImage(ctx, vi) + if err != nil { + return reconcile.Result{}, err + } + if hasCVI { + setInUseConditionTrue(vi, cb) + return reconcile.Result{}, nil + } + + cb. + Status(metav1.ConditionFalse). + Reason(vicondition.NotInUse). + Message("") + conditions.SetCondition(cb, &vi.Status.Conditions) + return reconcile.Result{}, nil +} + +func (h InUseHandler) Name() string { + return inUseHandlerName +} + +func (h InUseHandler) hasVMUsingImage(ctx context.Context, vi *virtv2.VirtualImage) (bool, error) { + var vms virtv2.VirtualMachineList + err := h.client.List(ctx, &vms, client.InNamespace(vi.GetNamespace())) + if err != nil { + return false, err + } + + for _, vm := range vms.Items { + if vm.Status.Phase == virtv2.MachineStopped { + continue + } + + for _, bd := range vm.Status.BlockDeviceRefs { + if bd.Kind == virtv2.VirtualImageKind && bd.Name == vi.Name { + return true, nil + } + } + } + + return false, nil +} + +func (h InUseHandler) hasVMBDAsUsingImage(ctx context.Context, vi *virtv2.VirtualImage) (bool, error) { + var vmbdas virtv2.VirtualMachineBlockDeviceAttachmentList + err := h.client.List(ctx, &vmbdas, client.InNamespace(vi.GetNamespace())) + if err != nil { + return false, err + } + + for _, vmbda := range vmbdas.Items { + if vmbda.Spec.BlockDeviceRef.Kind == virtv2.VMBDAObjectRefKindVirtualImage && vmbda.Spec.BlockDeviceRef.Name == vi.Name { + return true, nil + } + } + + return false, nil +} + +func (h InUseHandler) hasVDUsingImage(ctx context.Context, vi *virtv2.VirtualImage) (bool, error) { + var vds virtv2.VirtualDiskList + err := h.client.List(ctx, &vds, client.InNamespace(vi.GetNamespace()), client.MatchingFields{ + indexer.IndexFieldVDByVIDataSource: vi.GetName(), + }) + if err != nil { + return false, err + } + + for _, vd := range vds.Items { + phase := vd.Status.Phase + isProvisioning := (phase == virtv2.DiskPending) || + (phase == virtv2.DiskProvisioning) || + (phase == virtv2.DiskWaitForFirstConsumer) || + (phase == virtv2.DiskFailed) + + if isProvisioning { + return true, nil + } + } + + return false, nil +} + +func (h InUseHandler) hasVIUsingImage(ctx context.Context, vi *virtv2.VirtualImage) (bool, error) { + var vis virtv2.VirtualImageList + err := h.client.List(ctx, &vis, client.InNamespace(vi.GetNamespace()), client.MatchingFields{ + indexer.IndexFieldVIByVIDataSource: vi.GetName(), + }) + if err != nil { + return false, err + } + + for _, viItem := range vis.Items { + phase := viItem.Status.Phase + isProvisioning := (phase == virtv2.ImagePending) || (phase == virtv2.ImageProvisioning) || (phase == virtv2.ImageFailed) + if isProvisioning { + return true, nil + } + } + + return false, nil +} + +func (h InUseHandler) hasCVIUsingImage(ctx context.Context, vi *virtv2.VirtualImage) (bool, error) { + var cvis virtv2.ClusterVirtualImageList + err := h.client.List(ctx, &cvis, client.MatchingFields{ + indexer.IndexFieldCVIByVIDataSource: vi.GetName(), + }) + if err != nil { + return false, err + } + + for _, cvi := range cvis.Items { + if cvi.Spec.DataSource.ObjectRef == nil { + continue + } + + phase := cvi.Status.Phase + isProvisioning := (phase == virtv2.ImagePending) || (phase == virtv2.ImageProvisioning) || (phase == virtv2.ImageFailed) + if !isProvisioning { + continue + } + + if cvi.Spec.DataSource.ObjectRef.Namespace == vi.GetNamespace() { + return true, nil + } + } + + return false, nil +} + +func setInUseConditionTrue(vi *virtv2.VirtualImage, cb *conditions.ConditionBuilder) { + cb. + Status(metav1.ConditionTrue). + Reason(vicondition.InUse). + Message("") + + conditions.SetCondition(cb, &vi.Status.Conditions) +} diff --git a/images/virtualization-artifact/pkg/controller/vi/internal/inuse_test.go b/images/virtualization-artifact/pkg/controller/vi/internal/inuse_test.go new file mode 100644 index 0000000000..25a47e619e --- /dev/null +++ b/images/virtualization-artifact/pkg/controller/vi/internal/inuse_test.go @@ -0,0 +1,405 @@ +/* +Copyright 2025 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 internal + +import ( + "time" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "k8s.io/utils/ptr" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + + cvibuilder "github.com/deckhouse/virtualization-controller/pkg/builder/cvi" + vdbuilder "github.com/deckhouse/virtualization-controller/pkg/builder/vd" + vibuilder "github.com/deckhouse/virtualization-controller/pkg/builder/vi" + vmbdabuilder "github.com/deckhouse/virtualization-controller/pkg/builder/vmbda" + "github.com/deckhouse/virtualization-controller/pkg/common/testutil" + "github.com/deckhouse/virtualization-controller/pkg/controller/conditions" + virtv2 "github.com/deckhouse/virtualization/api/core/v1alpha2" + "github.com/deckhouse/virtualization/api/core/v1alpha2/cvicondition" + "github.com/deckhouse/virtualization/api/core/v1alpha2/vicondition" +) + +var _ = DescribeTable("InUseHandler Handle", func(args inUseHandlerTestArgs) { + vi := &virtv2.VirtualImage{ + ObjectMeta: metav1.ObjectMeta{ + Name: args.VINamespacedName.Name, + Namespace: args.VINamespacedName.Namespace, + DeletionTimestamp: args.DeletionTimestamp, + }, + Status: virtv2.VirtualImageStatus{ + Conditions: []metav1.Condition{ + { + Type: vicondition.ReadyType.String(), + Reason: vicondition.Ready.String(), + Status: metav1.ConditionTrue, + }, + }, + }, + } + + var objects []client.Object + for _, vm := range args.VMs { + objects = append(objects, &vm) + } + + for _, vmbda := range args.VMBDAs { + objects = append(objects, &vmbda) + } + + for _, vd := range args.VDs { + objects = append(objects, &vd) + } + + for _, vi := range args.VIs { + objects = append(objects, &vi) + } + + for _, cvi := range args.CVIs { + objects = append(objects, &cvi) + } + + fakeClient, err := testutil.NewFakeClientWithObjects(objects...) + Expect(err).ShouldNot(HaveOccurred()) + handler := NewInUseHandler(fakeClient) + + result, err := handler.Handle(testutil.ContextBackgroundWithNoOpLogger(), vi) + Expect(err).To(BeNil()) + Expect(result).To(Equal(reconcile.Result{})) + inUseCondition, _ := conditions.GetCondition(vicondition.InUseType, vi.Status.Conditions) + Expect(inUseCondition.Status).To(Equal(args.ExpectedConditionStatus)) + Expect(inUseCondition.Reason).To(Equal(args.ExpectedConditionReason)) +}, + Entry("deletionTimestamp exists but no one uses VI", inUseHandlerTestArgs{ + VMs: []virtv2.VirtualMachine{}, + VINamespacedName: types.NamespacedName{ + Name: "test", + Namespace: "ns", + }, + DeletionTimestamp: ptr.To(metav1.Time{Time: time.Now()}), + ExpectedConditionStatus: metav1.ConditionFalse, + ExpectedConditionReason: vicondition.NotInUse.String(), + }), + Entry("has 1 VirtualMachine with connected terminating VI", inUseHandlerTestArgs{ + VINamespacedName: types.NamespacedName{ + Name: "test", + Namespace: "ns", + }, + DeletionTimestamp: ptr.To(metav1.Time{Time: time.Now()}), + VMs: []virtv2.VirtualMachine{ + generateVMForInUseTest("test-vm", "ns", []virtv2.BlockDeviceStatusRef{ + { + Kind: virtv2.VirtualImageKind, + Name: "test", + }, + }), + }, + ExpectedConditionStatus: metav1.ConditionTrue, + ExpectedConditionReason: vicondition.InUse.String(), + }), + Entry("has 2 VirtualMachines with connected terminating VI", inUseHandlerTestArgs{ + VINamespacedName: types.NamespacedName{ + Name: "test", + Namespace: "ns", + }, + DeletionTimestamp: ptr.To(metav1.Time{Time: time.Now()}), + VMs: []virtv2.VirtualMachine{ + generateVMForInUseTest("test-vm", "ns", []virtv2.BlockDeviceStatusRef{ + { + Kind: virtv2.VirtualImageKind, + Name: "test", + }, + }), + generateVMForInUseTest("test-vm2", "ns", []virtv2.BlockDeviceStatusRef{ + { + Kind: virtv2.VirtualImageKind, + Name: "test", + }, + }), + }, + ExpectedConditionStatus: metav1.ConditionTrue, + ExpectedConditionReason: vicondition.InUse.String(), + }), + Entry("has 5 VirtualMachines with connected terminating VI", inUseHandlerTestArgs{ + VINamespacedName: types.NamespacedName{ + Name: "test", + Namespace: "ns", + }, + DeletionTimestamp: ptr.To(metav1.Time{Time: time.Now()}), + VMs: []virtv2.VirtualMachine{ + generateVMForInUseTest("test-vm", "ns", []virtv2.BlockDeviceStatusRef{ + { + Kind: virtv2.VirtualImageKind, + Name: "test", + }, + }), + generateVMForInUseTest("test-vm2", "ns", []virtv2.BlockDeviceStatusRef{ + { + Kind: virtv2.VirtualImageKind, + Name: "test", + }, + }), + generateVMForInUseTest("test-vm3", "ns", []virtv2.BlockDeviceStatusRef{ + { + Kind: virtv2.VirtualImageKind, + Name: "test", + }, + }), + generateVMForInUseTest("test-vm4", "ns", []virtv2.BlockDeviceStatusRef{ + { + Kind: virtv2.VirtualImageKind, + Name: "test", + }, + }), + generateVMForInUseTest("test-vm5", "ns", []virtv2.BlockDeviceStatusRef{ + { + Kind: virtv2.VirtualImageKind, + Name: "test", + }, + }), + generateVMForInUseTest("test-vm-imposter", "ns-imposter", []virtv2.BlockDeviceStatusRef{ + { + Kind: virtv2.VirtualImageKind, + Name: "test", + }, + }), + { + ObjectMeta: metav1.ObjectMeta{ + Name: "test-vm-stopped", + Namespace: "ns", + }, + TypeMeta: metav1.TypeMeta{ + Kind: virtv2.VirtualMachineKind, + }, + Status: virtv2.VirtualMachineStatus{ + Phase: virtv2.MachineStopped, + BlockDeviceRefs: []virtv2.BlockDeviceStatusRef{ + { + Kind: virtv2.VirtualImageKind, + Name: "test", + }, + }, + }, + }, + }, + ExpectedConditionStatus: metav1.ConditionTrue, + ExpectedConditionReason: vicondition.InUse.String(), + }), + Entry("has 5 VM with connected terminating VI, 1 VMBDA, 4 VD, 2 CVI, 1 VI", inUseHandlerTestArgs{ + VINamespacedName: types.NamespacedName{ + Name: "test", + Namespace: "ns", + }, + DeletionTimestamp: ptr.To(metav1.Time{Time: time.Now()}), + VMs: []virtv2.VirtualMachine{ + generateVMForInUseTest("test-vm", "ns", []virtv2.BlockDeviceStatusRef{ + { + Kind: virtv2.VirtualImageKind, + Name: "test", + }, + }), + generateVMForInUseTest("test-vm2", "ns", []virtv2.BlockDeviceStatusRef{ + { + Kind: virtv2.VirtualImageKind, + Name: "test", + }, + }), + generateVMForInUseTest("test-vm3", "ns", []virtv2.BlockDeviceStatusRef{ + { + Kind: virtv2.VirtualImageKind, + Name: "test", + }, + }), + generateVMForInUseTest("test-vm4", "ns", []virtv2.BlockDeviceStatusRef{ + { + Kind: virtv2.VirtualImageKind, + Name: "test", + }, + }), + generateVMForInUseTest("test-vm5", "ns", []virtv2.BlockDeviceStatusRef{ + { + Kind: virtv2.VirtualImageKind, + Name: "test", + }, + }), + }, + VDs: []virtv2.VirtualDisk{ + generateVDForInUseTest("test1", "ns", virtv2.VirtualDiskDataSource{ + Type: virtv2.DataSourceTypeObjectRef, + ObjectRef: &virtv2.VirtualDiskObjectRef{ + Kind: virtv2.VirtualImageKind, + Name: "test", + }, + }), + generateVDForInUseTest("test2", "ns", virtv2.VirtualDiskDataSource{ + Type: virtv2.DataSourceTypeObjectRef, + ObjectRef: &virtv2.VirtualDiskObjectRef{ + Kind: virtv2.VirtualImageKind, + Name: "test", + }, + }), + generateVDForInUseTest("test3", "ns", virtv2.VirtualDiskDataSource{ + Type: virtv2.DataSourceTypeObjectRef, + ObjectRef: &virtv2.VirtualDiskObjectRef{ + Kind: virtv2.VirtualImageKind, + Name: "test", + }, + }), + generateVDForInUseTest("test4", "ns", virtv2.VirtualDiskDataSource{ + Type: virtv2.DataSourceTypeObjectRef, + ObjectRef: &virtv2.VirtualDiskObjectRef{ + Kind: virtv2.VirtualImageKind, + Name: "test", + }, + }), + generateVDForInUseTest("test5", "ns2", virtv2.VirtualDiskDataSource{ + Type: virtv2.DataSourceTypeObjectRef, + ObjectRef: &virtv2.VirtualDiskObjectRef{ + Kind: virtv2.VirtualImageKind, + Name: "test", + }, + }), + }, + VIs: []virtv2.VirtualImage{ + generateVIForInUseTest("test1", "ns", virtv2.VirtualImageDataSource{ + Type: virtv2.DataSourceTypeObjectRef, + ObjectRef: &virtv2.VirtualImageObjectRef{ + Kind: virtv2.VirtualImageKind, + Name: "test", + }, + }), + }, + CVIs: []virtv2.ClusterVirtualImage{ + generateCVIForInUseTest("test1", virtv2.ClusterVirtualImageDataSource{ + Type: virtv2.DataSourceTypeObjectRef, + ObjectRef: &virtv2.ClusterVirtualImageObjectRef{ + Kind: virtv2.VirtualImageKind, + Name: "test", + Namespace: "ns", + }, + }), + generateCVIForInUseTest("test2", virtv2.ClusterVirtualImageDataSource{ + Type: virtv2.DataSourceTypeObjectRef, + ObjectRef: &virtv2.ClusterVirtualImageObjectRef{ + Kind: virtv2.VirtualImageKind, + Name: "test", + Namespace: "ns", + }, + }), + generateCVIForInUseTest("test3", virtv2.ClusterVirtualImageDataSource{ + Type: virtv2.DataSourceTypeObjectRef, + ObjectRef: &virtv2.ClusterVirtualImageObjectRef{ + Kind: virtv2.VirtualImageKind, + Name: "test", + Namespace: "test", + }, + }), + *cvibuilder.New( + cvibuilder.WithName("test322"), + cvibuilder.WithPhase(virtv2.ImageReady), + cvibuilder.WithCondition(metav1.Condition{ + Status: metav1.ConditionTrue, + Reason: cvicondition.ReadyType.String(), + }), + cvibuilder.WithDatasource(virtv2.ClusterVirtualImageDataSource{ + Type: virtv2.DataSourceTypeObjectRef, + ObjectRef: &virtv2.ClusterVirtualImageObjectRef{ + Kind: virtv2.VirtualImageKind, + Name: "test", + Namespace: "ns", + }, + }), + ), + }, + VMBDAs: []virtv2.VirtualMachineBlockDeviceAttachment{ + generateVMBDAForInUseTest( + "test1", + "ns", + virtv2.VMBDAObjectRef{ + Kind: virtv2.VirtualImageKind, + Name: "test", + }, + "test-vm", + ), + }, + ExpectedConditionStatus: metav1.ConditionTrue, + ExpectedConditionReason: vicondition.InUse.String(), + }), +) + +type inUseHandlerTestArgs struct { + VINamespacedName types.NamespacedName + DeletionTimestamp *metav1.Time + VMs []virtv2.VirtualMachine + VDs []virtv2.VirtualDisk + VIs []virtv2.VirtualImage + CVIs []virtv2.ClusterVirtualImage + VMBDAs []virtv2.VirtualMachineBlockDeviceAttachment + ExpectedConditionReason string + ExpectedConditionStatus metav1.ConditionStatus +} + +func generateVMForInUseTest(name, namespace string, blockDeviceRefs []virtv2.BlockDeviceStatusRef) virtv2.VirtualMachine { + return virtv2.VirtualMachine{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + }, + TypeMeta: metav1.TypeMeta{ + Kind: virtv2.VirtualMachineKind, + }, + Status: virtv2.VirtualMachineStatus{ + BlockDeviceRefs: blockDeviceRefs, + }, + } +} + +func generateVIForInUseTest(name, namespace string, datasource virtv2.VirtualImageDataSource) virtv2.VirtualImage { + return *vibuilder.New( + vibuilder.WithName(name), + vibuilder.WithNamespace(namespace), + vibuilder.WithDatasource(datasource), + ) +} + +func generateCVIForInUseTest(name string, datasource virtv2.ClusterVirtualImageDataSource) virtv2.ClusterVirtualImage { + return *cvibuilder.New( + cvibuilder.WithName(name), + cvibuilder.WithDatasource(datasource), + ) +} + +func generateVDForInUseTest(name, namespace string, datasource virtv2.VirtualDiskDataSource) virtv2.VirtualDisk { + return *vdbuilder.New( + vdbuilder.WithName(name), + vdbuilder.WithNamespace(namespace), + vdbuilder.WithDatasource(&datasource), + ) +} + +func generateVMBDAForInUseTest(name, namespace string, bdRef virtv2.VMBDAObjectRef, vmName string) virtv2.VirtualMachineBlockDeviceAttachment { + return *vmbdabuilder.New( + vmbdabuilder.WithName(name), + vmbdabuilder.WithNamespace(namespace), + vmbdabuilder.WithBlockDeviceRef(bdRef), + vmbdabuilder.WithVMName(vmName), + ) +} diff --git a/images/virtualization-artifact/pkg/controller/vi/internal/life_cycle.go b/images/virtualization-artifact/pkg/controller/vi/internal/life_cycle.go index 8048ef0338..b07bfe5cd8 100644 --- a/images/virtualization-artifact/pkg/controller/vi/internal/life_cycle.go +++ b/images/virtualization-artifact/pkg/controller/vi/internal/life_cycle.go @@ -25,7 +25,9 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/reconcile" + "github.com/deckhouse/virtualization-controller/pkg/common/annotations" "github.com/deckhouse/virtualization-controller/pkg/controller/conditions" + "github.com/deckhouse/virtualization-controller/pkg/controller/supplements" "github.com/deckhouse/virtualization-controller/pkg/controller/vi/internal/source" "github.com/deckhouse/virtualization-controller/pkg/eventrecord" virtv2 "github.com/deckhouse/virtualization/api/core/v1alpha2" @@ -33,16 +35,18 @@ import ( ) type LifeCycleHandler struct { - client client.Client - sources Sources - recorder eventrecord.EventRecorderLogger + client client.Client + sources Sources + diskService DiskService + recorder eventrecord.EventRecorderLogger } -func NewLifeCycleHandler(recorder eventrecord.EventRecorderLogger, sources Sources, client client.Client) *LifeCycleHandler { +func NewLifeCycleHandler(recorder eventrecord.EventRecorderLogger, sources Sources, client client.Client, diskService DiskService) *LifeCycleHandler { return &LifeCycleHandler{ - recorder: recorder, - client: client, - sources: sources, + recorder: recorder, + client: client, + sources: sources, + diskService: diskService, } } @@ -59,6 +63,28 @@ func (h LifeCycleHandler) Handle(ctx context.Context, vi *virtv2.VirtualImage) ( } if vi.DeletionTimestamp != nil { + // It is necessary to update this condition in order to use this image as a datasource. + if readyCondition.Status == metav1.ConditionTrue { + cb := conditions.NewConditionBuilder(vicondition.ReadyType).Generation(vi.Generation) + + if vi.Spec.Storage == virtv2.StorageContainerRegistry { + cb. + Status(metav1.ConditionTrue). + Reason(vicondition.Ready). + Message("") + } else { + supgen := supplements.NewGenerator(annotations.VIShortName, vi.Name, vi.Namespace, vi.UID) + pvc, err := h.diskService.GetPersistentVolumeClaim(ctx, supgen) + if err != nil { + return reconcile.Result{}, err + } + + source.SetPhaseConditionForFinishedImage(pvc, cb, &vi.Status.Phase, supgen) + } + + conditions.SetCondition(cb, &vi.Status.Conditions) + } + vi.Status.Phase = virtv2.ImageTerminating return reconcile.Result{}, nil } diff --git a/images/virtualization-artifact/pkg/controller/vi/internal/life_cycle_test.go b/images/virtualization-artifact/pkg/controller/vi/internal/life_cycle_test.go index f2ca0daf0c..fe270784d9 100644 --- a/images/virtualization-artifact/pkg/controller/vi/internal/life_cycle_test.go +++ b/images/virtualization-artifact/pkg/controller/vi/internal/life_cycle_test.go @@ -77,7 +77,7 @@ var _ = Describe("LifeCycleHandler Run", func() { EventFunc: func(_ client.Object, _, _, _ string) {}, } - handler := NewLifeCycleHandler(recorder, &sourcesMock, nil) + handler := NewLifeCycleHandler(recorder, &sourcesMock, nil, nil) _, _ = handler.Handle(context.TODO(), &vi) @@ -158,7 +158,7 @@ var _ = Describe("LifeCycleHandler Run", func() { return &handler, false } - handler := NewLifeCycleHandler(nil, &sourcesMock, nil) + handler := NewLifeCycleHandler(nil, &sourcesMock, nil, nil) _, _ = handler.Handle(context.TODO(), &vi) diff --git a/images/virtualization-artifact/pkg/controller/vi/internal/mock.go b/images/virtualization-artifact/pkg/controller/vi/internal/mock.go index e376867bd2..5e195b481e 100644 --- a/images/virtualization-artifact/pkg/controller/vi/internal/mock.go +++ b/images/virtualization-artifact/pkg/controller/vi/internal/mock.go @@ -27,9 +27,6 @@ var _ DiskService = &DiskServiceMock{} // GetPersistentVolumeClaimFunc: func(ctx context.Context, sup *supplements.Generator) (*corev1.PersistentVolumeClaim, error) { // panic("mock out the GetPersistentVolumeClaim method") // }, -// GetStorageClassFunc: func(ctx context.Context, storageClassName *string) (*storagev1.StorageClass, error) { -// panic("mock out the GetStorageClass method") -// }, // } // // // use mockedDiskService in code that requires DiskService @@ -40,9 +37,6 @@ type DiskServiceMock struct { // GetPersistentVolumeClaimFunc mocks the GetPersistentVolumeClaim method. GetPersistentVolumeClaimFunc func(ctx context.Context, sup *supplements.Generator) (*corev1.PersistentVolumeClaim, error) - // GetStorageClassFunc mocks the GetStorageClass method. - GetStorageClassFunc func(ctx context.Context, storageClassName *string) (*storagev1.StorageClass, error) - // calls tracks calls to the methods. calls struct { // GetPersistentVolumeClaim holds details about calls to the GetPersistentVolumeClaim method. @@ -52,16 +46,8 @@ type DiskServiceMock struct { // Sup is the sup argument value. Sup *supplements.Generator } - // GetStorageClass holds details about calls to the GetStorageClass method. - GetStorageClass []struct { - // Ctx is the ctx argument value. - Ctx context.Context - // StorageClassName is the storageClassName argument value. - StorageClassName *string - } } lockGetPersistentVolumeClaim sync.RWMutex - lockGetStorageClass sync.RWMutex } // GetPersistentVolumeClaim calls GetPersistentVolumeClaimFunc. @@ -100,42 +86,6 @@ func (mock *DiskServiceMock) GetPersistentVolumeClaimCalls() []struct { return calls } -// GetStorageClass calls GetStorageClassFunc. -func (mock *DiskServiceMock) GetStorageClass(ctx context.Context, storageClassName *string) (*storagev1.StorageClass, error) { - if mock.GetStorageClassFunc == nil { - panic("DiskServiceMock.GetStorageClassFunc: method is nil but DiskService.GetStorageClass was just called") - } - callInfo := struct { - Ctx context.Context - StorageClassName *string - }{ - Ctx: ctx, - StorageClassName: storageClassName, - } - mock.lockGetStorageClass.Lock() - mock.calls.GetStorageClass = append(mock.calls.GetStorageClass, callInfo) - mock.lockGetStorageClass.Unlock() - return mock.GetStorageClassFunc(ctx, storageClassName) -} - -// GetStorageClassCalls gets all the calls that were made to GetStorageClass. -// Check the length with: -// -// len(mockedDiskService.GetStorageClassCalls()) -func (mock *DiskServiceMock) GetStorageClassCalls() []struct { - Ctx context.Context - StorageClassName *string -} { - var calls []struct { - Ctx context.Context - StorageClassName *string - } - mock.lockGetStorageClass.RLock() - calls = mock.calls.GetStorageClass - mock.lockGetStorageClass.RUnlock() - return calls -} - // Ensure, that SourcesMock does implement Sources. // If this is not the case, regenerate this file with moq. var _ Sources = &SourcesMock{} diff --git a/images/virtualization-artifact/pkg/controller/vi/internal/source/http.go b/images/virtualization-artifact/pkg/controller/vi/internal/source/http.go index 06f8ebf0f0..68f9986dac 100644 --- a/images/virtualization-artifact/pkg/controller/vi/internal/source/http.go +++ b/images/virtualization-artifact/pkg/controller/vi/internal/source/http.go @@ -221,7 +221,7 @@ func (ds HTTPDataSource) StoreToPVC(ctx context.Context, vi *virtv2.VirtualImage case IsImageProvisioningFinished(condition): log.Info("Image provisioning finished: clean up") - setPhaseConditionForFinishedImage(pvc, cb, &vi.Status.Phase, supgen) + SetPhaseConditionForFinishedImage(pvc, cb, &vi.Status.Phase, supgen) // Protect Ready Disk and underlying PVC. err = ds.diskService.Protect(ctx, vi, nil, pvc) diff --git a/images/virtualization-artifact/pkg/controller/vi/internal/source/object_ref.go b/images/virtualization-artifact/pkg/controller/vi/internal/source/object_ref.go index 4d962d0c5b..fc4073f4e5 100644 --- a/images/virtualization-artifact/pkg/controller/vi/internal/source/object_ref.go +++ b/images/virtualization-artifact/pkg/controller/vi/internal/source/object_ref.go @@ -151,7 +151,7 @@ func (ds ObjectRefDataSource) StoreToPVC(ctx context.Context, vi *virtv2.Virtual case IsImageProvisioningFinished(condition): log.Info("Disk provisioning finished: clean up") - setPhaseConditionForFinishedImage(pvc, cb, &vi.Status.Phase, supgen) + SetPhaseConditionForFinishedImage(pvc, cb, &vi.Status.Phase, supgen) // Protect Ready Disk and underlying PVC. err = ds.diskService.Protect(ctx, vi, nil, pvc) @@ -505,7 +505,9 @@ func (ds ObjectRefDataSource) Validate(ctx context.Context, vi *virtv2.VirtualIm } if viRef.Spec.Storage == virtv2.StorageKubernetes || viRef.Spec.Storage == virtv2.StoragePersistentVolumeClaim { - if viRef.Status.Phase != virtv2.ImageReady { + readyCondition, _ := conditions.GetCondition(vicondition.ReadyType, viRef.Status.Conditions) + + if readyCondition.Status != metav1.ConditionTrue { return NewImageNotReadyError(vi.Spec.DataSource.ObjectRef.Name) } return nil diff --git a/images/virtualization-artifact/pkg/controller/vi/internal/source/object_ref_vd.go b/images/virtualization-artifact/pkg/controller/vi/internal/source/object_ref_vd.go index f83f89ed83..5f6c651f52 100644 --- a/images/virtualization-artifact/pkg/controller/vi/internal/source/object_ref_vd.go +++ b/images/virtualization-artifact/pkg/controller/vi/internal/source/object_ref_vd.go @@ -239,7 +239,7 @@ func (ds ObjectRefVirtualDisk) StoreToPVC(ctx context.Context, vi *virtv2.Virtua case IsImageProvisioningFinished(condition): log.Info("Disk provisioning finished: clean up") - setPhaseConditionForFinishedImage(pvc, cb, &vi.Status.Phase, supgen) + SetPhaseConditionForFinishedImage(pvc, cb, &vi.Status.Phase, supgen) // Protect Ready Disk and underlying PVC. err = ds.diskService.Protect(ctx, vi, nil, pvc) diff --git a/images/virtualization-artifact/pkg/controller/vi/internal/source/object_ref_vi_on_pvc.go b/images/virtualization-artifact/pkg/controller/vi/internal/source/object_ref_vi_on_pvc.go index 95487fe724..ee0a03ecf6 100644 --- a/images/virtualization-artifact/pkg/controller/vi/internal/source/object_ref_vi_on_pvc.go +++ b/images/virtualization-artifact/pkg/controller/vi/internal/source/object_ref_vi_on_pvc.go @@ -216,7 +216,7 @@ func (ds ObjectRefDataVirtualImageOnPVC) StoreToPVC(ctx context.Context, vi, viR case IsImageProvisioningFinished(condition): log.Info("Disk provisioning finished: clean up") - setPhaseConditionForFinishedImage(pvc, cb, &vi.Status.Phase, supgen) + SetPhaseConditionForFinishedImage(pvc, cb, &vi.Status.Phase, supgen) // Protect Ready Disk and underlying PVC. err = ds.diskService.Protect(ctx, vi, nil, pvc) diff --git a/images/virtualization-artifact/pkg/controller/vi/internal/source/registry.go b/images/virtualization-artifact/pkg/controller/vi/internal/source/registry.go index 0c6e17060a..81dcfb1392 100644 --- a/images/virtualization-artifact/pkg/controller/vi/internal/source/registry.go +++ b/images/virtualization-artifact/pkg/controller/vi/internal/source/registry.go @@ -108,7 +108,7 @@ func (ds RegistryDataSource) StoreToPVC(ctx context.Context, vi *virtv2.VirtualI case IsImageProvisioningFinished(condition): log.Info("Disk provisioning finished: clean up") - setPhaseConditionForFinishedImage(pvc, cb, &vi.Status.Phase, supgen) + SetPhaseConditionForFinishedImage(pvc, cb, &vi.Status.Phase, supgen) // Protect Ready Disk and underlying PVC. err = ds.diskService.Protect(ctx, vi, nil, pvc) diff --git a/images/virtualization-artifact/pkg/controller/vi/internal/source/sources.go b/images/virtualization-artifact/pkg/controller/vi/internal/source/sources.go index 72674d6b00..567e90554d 100644 --- a/images/virtualization-artifact/pkg/controller/vi/internal/source/sources.go +++ b/images/virtualization-artifact/pkg/controller/vi/internal/source/sources.go @@ -109,7 +109,7 @@ type CheckImportProcess interface { CheckImportProcess(ctx context.Context, dv *cdiv1.DataVolume, pvc *corev1.PersistentVolumeClaim) error } -func setPhaseConditionForFinishedImage( +func SetPhaseConditionForFinishedImage( pvc *corev1.PersistentVolumeClaim, cb *conditions.ConditionBuilder, phase *virtv2.ImagePhase, diff --git a/images/virtualization-artifact/pkg/controller/vi/internal/source/upload.go b/images/virtualization-artifact/pkg/controller/vi/internal/source/upload.go index 15ff8aea43..d05e2d5853 100644 --- a/images/virtualization-artifact/pkg/controller/vi/internal/source/upload.go +++ b/images/virtualization-artifact/pkg/controller/vi/internal/source/upload.go @@ -111,7 +111,7 @@ func (ds UploadDataSource) StoreToPVC(ctx context.Context, vi *virtv2.VirtualIma case IsImageProvisioningFinished(condition): log.Info("Disk provisioning finished: clean up") - setPhaseConditionForFinishedImage(pvc, cb, &vi.Status.Phase, supgen) + SetPhaseConditionForFinishedImage(pvc, cb, &vi.Status.Phase, supgen) // Protect Ready Disk and underlying PVC. err = ds.diskService.Protect(ctx, vi, nil, pvc) diff --git a/images/virtualization-artifact/pkg/controller/vi/internal/watcher/cvi_watcher.go b/images/virtualization-artifact/pkg/controller/vi/internal/watcher/cvi_watcher.go new file mode 100644 index 0000000000..faed6c1e6e --- /dev/null +++ b/images/virtualization-artifact/pkg/controller/vi/internal/watcher/cvi_watcher.go @@ -0,0 +1,126 @@ +/* +Copyright 2025 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" + + "k8s.io/apimachinery/pkg/types" + "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/virtualization-controller/pkg/controller/conditions" + "github.com/deckhouse/virtualization-controller/pkg/logger" + virtv2 "github.com/deckhouse/virtualization/api/core/v1alpha2" + "github.com/deckhouse/virtualization/api/core/v1alpha2/cvicondition" +) + +type ClusterVirtualImageWatcher struct { + client client.Client +} + +func NewClusterVirtualImageWatcher(client client.Client) *ClusterVirtualImageWatcher { + return &ClusterVirtualImageWatcher{ + client: client, + } +} + +func (w ClusterVirtualImageWatcher) Watch(mgr manager.Manager, ctr controller.Controller) error { + return ctr.Watch( + source.Kind(mgr.GetCache(), &virtv2.ClusterVirtualImage{}), + handler.EnqueueRequestsFromMapFunc(w.enqueueRequests), + predicate.Funcs{ + CreateFunc: func(e event.CreateEvent) bool { + return true + }, + DeleteFunc: func(e event.DeleteEvent) bool { + return true + }, + UpdateFunc: w.filterUpdateEvents, + }, + ) +} + +func (w ClusterVirtualImageWatcher) enqueueRequests(ctx context.Context, obj client.Object) (requests []reconcile.Request) { + var viList virtv2.VirtualImageList + err := w.client.List(ctx, &viList) + if err != nil { + logger.FromContext(ctx).Error(fmt.Sprintf("failed to list vi: %s", err)) + return + } + + // We need to trigger reconcile for the vi resources that use changed image as a datasource so they can continue provisioning. + for _, vi := range viList.Items { + if vi.Spec.DataSource.Type != virtv2.DataSourceTypeObjectRef || vi.Spec.DataSource.ObjectRef == nil { + continue + } + + if vi.Spec.DataSource.ObjectRef.Kind != virtv2.ClusterVirtualImageKind || vi.Spec.DataSource.ObjectRef.Name != obj.GetName() { + continue + } + + requests = append(requests, reconcile.Request{ + NamespacedName: types.NamespacedName{ + Name: vi.Name, + Namespace: vi.Namespace, + }, + }) + } + + cvi, ok := obj.(*virtv2.ClusterVirtualImage) + if ok && cvi.Spec.DataSource.Type == virtv2.DataSourceTypeObjectRef { + if cvi.Spec.DataSource.ObjectRef != nil && cvi.Spec.DataSource.ObjectRef.Kind == virtv2.VirtualImageKind { + // Need to trigger reconcile for update InUse condition. + requests = append(requests, reconcile.Request{ + NamespacedName: types.NamespacedName{ + Name: cvi.Spec.DataSource.ObjectRef.Name, + Namespace: cvi.Spec.DataSource.ObjectRef.Namespace, + }, + }) + } + } + + return +} + +func (w ClusterVirtualImageWatcher) filterUpdateEvents(e event.UpdateEvent) bool { + oldCVI, ok := e.ObjectOld.(*virtv2.ClusterVirtualImage) + if !ok { + return false + } + + newCVI, ok := e.ObjectNew.(*virtv2.ClusterVirtualImage) + if !ok { + return false + } + + oldReadyCondition, _ := conditions.GetCondition(cvicondition.ReadyType, oldCVI.Status.Conditions) + newReadyCondition, _ := conditions.GetCondition(cvicondition.ReadyType, newCVI.Status.Conditions) + + if oldCVI.Status.Phase != newCVI.Status.Phase || oldReadyCondition.Status != newReadyCondition.Status { + return true + } + + return false +} diff --git a/images/virtualization-artifact/pkg/controller/vi/internal/watcher/vd_watcher.go b/images/virtualization-artifact/pkg/controller/vi/internal/watcher/vd_watcher.go new file mode 100644 index 0000000000..9974328681 --- /dev/null +++ b/images/virtualization-artifact/pkg/controller/vi/internal/watcher/vd_watcher.go @@ -0,0 +1,134 @@ +/* +Copyright 2025 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" + + "k8s.io/apimachinery/pkg/types" + "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/virtualization-controller/pkg/controller/conditions" + "github.com/deckhouse/virtualization-controller/pkg/logger" + virtv2 "github.com/deckhouse/virtualization/api/core/v1alpha2" + "github.com/deckhouse/virtualization/api/core/v1alpha2/vdcondition" +) + +type VirtualDiskWatcher struct { + client client.Client +} + +func NewVirtualDiskWatcher(client client.Client) *VirtualDiskWatcher { + return &VirtualDiskWatcher{ + client: client, + } +} + +func (w VirtualDiskWatcher) Watch(mgr manager.Manager, ctr controller.Controller) error { + return ctr.Watch( + source.Kind(mgr.GetCache(), &virtv2.VirtualDisk{}), + handler.EnqueueRequestsFromMapFunc(w.enqueueRequests), + predicate.Funcs{ + CreateFunc: func(e event.CreateEvent) bool { + return true + }, + DeleteFunc: func(e event.DeleteEvent) bool { + return true + }, + UpdateFunc: w.filterUpdateEvents, + }, + ) +} + +func (w VirtualDiskWatcher) enqueueRequests(ctx context.Context, obj client.Object) (requests []reconcile.Request) { + var viList virtv2.VirtualImageList + err := w.client.List(ctx, &viList, &client.ListOptions{ + Namespace: obj.GetNamespace(), + }) + if err != nil { + logger.FromContext(ctx).Error(fmt.Sprintf("failed to list vi: %s", err)) + return + } + + // We need to trigger reconcile for the vi resources that use changed image as a datasource so they can continue provisioning. + for _, vi := range viList.Items { + if vi.Spec.DataSource.Type != virtv2.DataSourceTypeObjectRef || vi.Spec.DataSource.ObjectRef == nil { + continue + } + + if vi.Spec.DataSource.ObjectRef.Kind != virtv2.VirtualDiskKind || vi.Spec.DataSource.ObjectRef.Name != obj.GetName() { + continue + } + + requests = append(requests, reconcile.Request{ + NamespacedName: types.NamespacedName{ + Name: vi.Name, + Namespace: vi.Namespace, + }, + }) + } + + vd, ok := obj.(*virtv2.VirtualDisk) + if ok && vd.Spec.DataSource != nil && vd.Spec.DataSource.Type == virtv2.DataSourceTypeObjectRef { + if vd.Spec.DataSource.ObjectRef != nil && vd.Spec.DataSource.ObjectRef.Kind == virtv2.VirtualImageKind { + // Need to trigger reconcile for update InUse condition. + requests = append(requests, reconcile.Request{ + NamespacedName: types.NamespacedName{ + Name: vd.Spec.DataSource.ObjectRef.Name, + Namespace: vd.Namespace, + }, + }) + } + } + + return +} + +func (w VirtualDiskWatcher) filterUpdateEvents(e event.UpdateEvent) bool { + oldVD, ok := e.ObjectOld.(*virtv2.VirtualDisk) + if !ok { + return false + } + + newVD, ok := e.ObjectNew.(*virtv2.VirtualDisk) + if !ok { + return false + } + + oldInUseCondition, _ := conditions.GetCondition(vdcondition.InUseType, oldVD.Status.Conditions) + newInUseCondition, _ := conditions.GetCondition(vdcondition.InUseType, newVD.Status.Conditions) + + oldReadyCondition, _ := conditions.GetCondition(vdcondition.ReadyType, oldVD.Status.Conditions) + newReadyCondition, _ := conditions.GetCondition(vdcondition.ReadyType, newVD.Status.Conditions) + + if oldVD.Status.Phase != newVD.Status.Phase || + len(oldVD.Status.AttachedToVirtualMachines) != len(newVD.Status.AttachedToVirtualMachines) || + oldInUseCondition.Status != newInUseCondition.Status || + oldReadyCondition.Status != newReadyCondition.Status { + return true + } + + return false +} diff --git a/images/virtualization-artifact/pkg/controller/vi/internal/watcher/vi_watcher.go b/images/virtualization-artifact/pkg/controller/vi/internal/watcher/vi_watcher.go new file mode 100644 index 0000000000..9ed266cb8c --- /dev/null +++ b/images/virtualization-artifact/pkg/controller/vi/internal/watcher/vi_watcher.go @@ -0,0 +1,128 @@ +/* +Copyright 2025 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" + + "k8s.io/apimachinery/pkg/types" + "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/virtualization-controller/pkg/controller/conditions" + "github.com/deckhouse/virtualization-controller/pkg/logger" + virtv2 "github.com/deckhouse/virtualization/api/core/v1alpha2" + "github.com/deckhouse/virtualization/api/core/v1alpha2/vicondition" +) + +type VirtualImageWatcher struct { + client client.Client +} + +func NewVirtualImageWatcher(client client.Client) *VirtualImageWatcher { + return &VirtualImageWatcher{ + client: client, + } +} + +func (w VirtualImageWatcher) Watch(mgr manager.Manager, ctr controller.Controller) error { + return ctr.Watch( + source.Kind(mgr.GetCache(), &virtv2.VirtualImage{}), + handler.EnqueueRequestsFromMapFunc(w.enqueueRequests), + predicate.Funcs{ + CreateFunc: func(e event.CreateEvent) bool { + return true + }, + DeleteFunc: func(e event.DeleteEvent) bool { + return true + }, + UpdateFunc: w.filterUpdateEvents, + }, + ) +} + +func (w VirtualImageWatcher) enqueueRequests(ctx context.Context, obj client.Object) (requests []reconcile.Request) { + var viList virtv2.VirtualImageList + err := w.client.List(ctx, &viList, &client.ListOptions{ + Namespace: obj.GetNamespace(), + }) + if err != nil { + logger.FromContext(ctx).Error(fmt.Sprintf("failed to list vi: %s", err)) + return + } + + // We need to trigger reconcile for the vi resources that use changed image as a datasource so they can continue provisioning. + for _, vi := range viList.Items { + if vi.Spec.DataSource.Type != virtv2.DataSourceTypeObjectRef || vi.Spec.DataSource.ObjectRef == nil { + continue + } + + if vi.Spec.DataSource.ObjectRef.Kind != virtv2.VirtualImageKind || vi.Spec.DataSource.ObjectRef.Name != obj.GetName() { + continue + } + + requests = append(requests, reconcile.Request{ + NamespacedName: types.NamespacedName{ + Name: vi.Name, + Namespace: vi.Namespace, + }, + }) + } + + vi, ok := obj.(*virtv2.VirtualImage) + if ok && vi.Spec.DataSource.Type == virtv2.DataSourceTypeObjectRef { + if vi.Spec.DataSource.ObjectRef != nil && vi.Spec.DataSource.ObjectRef.Kind == virtv2.VirtualImageKind { + // Need to trigger reconcile for update InUse condition. + requests = append(requests, reconcile.Request{ + NamespacedName: types.NamespacedName{ + Name: vi.Spec.DataSource.ObjectRef.Name, + Namespace: vi.Namespace, + }, + }) + } + } + + return +} + +func (w VirtualImageWatcher) filterUpdateEvents(e event.UpdateEvent) bool { + oldVI, ok := e.ObjectOld.(*virtv2.VirtualImage) + if !ok { + return false + } + + newVI, ok := e.ObjectNew.(*virtv2.VirtualImage) + if !ok { + return false + } + + oldReadyCondition, _ := conditions.GetCondition(vicondition.ReadyType, oldVI.Status.Conditions) + newReadyCondition, _ := conditions.GetCondition(vicondition.ReadyType, newVI.Status.Conditions) + + if oldVI.Status.Phase != newVI.Status.Phase || oldReadyCondition.Status != newReadyCondition.Status { + return true + } + + return false +} diff --git a/images/virtualization-artifact/pkg/controller/vi/vi_controller.go b/images/virtualization-artifact/pkg/controller/vi/vi_controller.go index ed69d604b9..1a395b8e38 100644 --- a/images/virtualization-artifact/pkg/controller/vi/vi_controller.go +++ b/images/virtualization-artifact/pkg/controller/vi/vi_controller.go @@ -81,9 +81,9 @@ func NewController( mgr.GetClient(), internal.NewStorageClassReadyHandler(recorder, scService), internal.NewDatasourceReadyHandler(sources), - internal.NewLifeCycleHandler(recorder, sources, mgr.GetClient()), + internal.NewLifeCycleHandler(recorder, sources, mgr.GetClient(), disk), + internal.NewInUseHandler(mgr.GetClient()), internal.NewDeletionHandler(sources), - internal.NewAttacheeHandler(mgr.GetClient()), ) viController, err := controller.New(ControllerName, mgr, controller.Options{ diff --git a/images/virtualization-artifact/pkg/controller/vi/vi_reconciler.go b/images/virtualization-artifact/pkg/controller/vi/vi_reconciler.go index 0220c5f636..a1d9d07ff2 100644 --- a/images/virtualization-artifact/pkg/controller/vi/vi_reconciler.go +++ b/images/virtualization-artifact/pkg/controller/vi/vi_reconciler.go @@ -117,6 +117,9 @@ func (r *Reconciler) SetupController(_ context.Context, mgr manager.Manager, ctr watcher.NewDataVolumeWatcher(), watcher.NewPersistentVolumeClaimWatcher(), watcher.NewVirtualDiskWatcher(mgrClient), + watcher.NewVirtualDiskWatcher(mgr.GetClient()), + watcher.NewClusterVirtualImageWatcher(mgr.GetClient()), + watcher.NewVirtualImageWatcher(mgr.GetClient()), } { err := w.Watch(mgr, ctr) if err != nil { diff --git a/images/virtualization-artifact/pkg/controller/vmbda/internal/block_device_ready.go b/images/virtualization-artifact/pkg/controller/vmbda/internal/block_device_ready.go index 9d41c74e41..7e513ace1d 100644 --- a/images/virtualization-artifact/pkg/controller/vmbda/internal/block_device_ready.go +++ b/images/virtualization-artifact/pkg/controller/vmbda/internal/block_device_ready.go @@ -28,7 +28,9 @@ import ( "github.com/deckhouse/virtualization-controller/pkg/controller/conditions" "github.com/deckhouse/virtualization-controller/pkg/controller/service" virtv2 "github.com/deckhouse/virtualization/api/core/v1alpha2" + "github.com/deckhouse/virtualization/api/core/v1alpha2/cvicondition" "github.com/deckhouse/virtualization/api/core/v1alpha2/vdcondition" + "github.com/deckhouse/virtualization/api/core/v1alpha2/vicondition" "github.com/deckhouse/virtualization/api/core/v1alpha2/vmbdacondition" ) @@ -161,7 +163,8 @@ func (h BlockDeviceReadyHandler) Handle(ctx context.Context, vmbda *virtv2.Virtu return reconcile.Result{}, nil } - if vi.Status.Phase != virtv2.ImageReady { + viReady, _ := conditions.GetCondition(vicondition.ReadyType, vi.Status.Conditions) + if viReady.Status != metav1.ConditionTrue { cb. Status(metav1.ConditionFalse). Reason(vmbdacondition.BlockDeviceNotReady). @@ -191,7 +194,7 @@ func (h BlockDeviceReadyHandler) Handle(ctx context.Context, vmbda *virtv2.Virtu return reconcile.Result{}, nil } - if vi.Status.Phase == virtv2.ImageReady && pvc.Status.Phase != corev1.ClaimBound { + if pvc.Status.Phase != corev1.ClaimBound { cb. Status(metav1.ConditionFalse). Reason(vmbdacondition.BlockDeviceNotReady). @@ -238,7 +241,8 @@ func (h BlockDeviceReadyHandler) Handle(ctx context.Context, vmbda *virtv2.Virtu return reconcile.Result{}, nil } - if cvi.Status.Phase != virtv2.ImageReady { + cviReady, _ := conditions.GetCondition(cvicondition.ReadyType, cvi.Status.Conditions) + if cviReady.Status != metav1.ConditionTrue { cb. Status(metav1.ConditionFalse). Reason(vmbdacondition.BlockDeviceNotReady). diff --git a/images/virtualization-artifact/pkg/controller/watchers/cvi_enqueuer.go b/images/virtualization-artifact/pkg/controller/watchers/cvi_enqueuer.go index e8824b5bae..066c11bdbe 100644 --- a/images/virtualization-artifact/pkg/controller/watchers/cvi_enqueuer.go +++ b/images/virtualization-artifact/pkg/controller/watchers/cvi_enqueuer.go @@ -38,15 +38,6 @@ type ClusterVirtualImageRequestEnqueuer struct { logger *log.Logger } -func NewClusterVirtualImageRequestEnqueuer(client client.Client, enqueueFromObj client.Object, enqueueFromKind virtv2.ClusterVirtualImageObjectRefKind) *ClusterVirtualImageRequestEnqueuer { - return &ClusterVirtualImageRequestEnqueuer{ - enqueueFromObj: enqueueFromObj, - enqueueFromKind: enqueueFromKind, - client: client, - logger: log.Default().With("enqueuer", "cvi"), - } -} - func (w ClusterVirtualImageRequestEnqueuer) GetEnqueueFrom() client.Object { return w.enqueueFromObj } From 01fcb32eea93028ac88907acf0c6c6bc03b09526 Mon Sep 17 00:00:00 2001 From: Valeriy Khorunzhin Date: Tue, 24 Jun 2025 18:29:06 +0300 Subject: [PATCH 02/11] fix problems) Signed-off-by: Valeriy Khorunzhin --- .../pkg/controller/cvi/internal/life_cycle.go | 11 +++++++++-- .../pkg/controller/vd/internal/life_cycle.go | 3 +-- .../pkg/controller/vi/internal/life_cycle.go | 14 ++++++++------ 3 files changed, 18 insertions(+), 10 deletions(-) diff --git a/images/virtualization-artifact/pkg/controller/cvi/internal/life_cycle.go b/images/virtualization-artifact/pkg/controller/cvi/internal/life_cycle.go index 72d0a0a854..2d7fa26134 100644 --- a/images/virtualization-artifact/pkg/controller/cvi/internal/life_cycle.go +++ b/images/virtualization-artifact/pkg/controller/cvi/internal/life_cycle.go @@ -56,14 +56,21 @@ func (h LifeCycleHandler) Handle(ctx context.Context, cvi *virtv2.ClusterVirtual if cvi.DeletionTimestamp != nil { // It is necessary to update this condition in order to use this image as a data source. + cb := conditions.NewConditionBuilder(cvicondition.ReadyType).Generation(cvi.Generation) + if readyCondition.Status == metav1.ConditionTrue { - cb := conditions.NewConditionBuilder(cvicondition.ReadyType).Generation(cvi.Generation). + cb. Status(metav1.ConditionTrue). Reason(cvicondition.Ready). Message("") - conditions.SetCondition(cb, &cvi.Status.Conditions) + } else { + cb. + Status(readyCondition.Status). + Reason(conditions.ReasonUnknown). + Message("") } + conditions.SetCondition(cb, &cvi.Status.Conditions) cvi.Status.Phase = virtv2.ImageTerminating return reconcile.Result{}, nil } diff --git a/images/virtualization-artifact/pkg/controller/vd/internal/life_cycle.go b/images/virtualization-artifact/pkg/controller/vd/internal/life_cycle.go index 4ce87295a8..c68d295ec4 100644 --- a/images/virtualization-artifact/pkg/controller/vd/internal/life_cycle.go +++ b/images/virtualization-artifact/pkg/controller/vd/internal/life_cycle.go @@ -65,7 +65,6 @@ func (h LifeCycleHandler) Handle(ctx context.Context, vd *virtv2.VirtualDisk) (r } if vd.DeletionTimestamp != nil { - vd.Status.Phase = virtv2.DiskTerminating if readyCondition.Status == metav1.ConditionTrue { cb := conditions.NewConditionBuilder(vdcondition.ReadyType).Generation(vd.Generation) @@ -76,9 +75,9 @@ func (h LifeCycleHandler) Handle(ctx context.Context, vd *virtv2.VirtualDisk) (r } source.SetPhaseConditionForFinishedDisk(pvc, cb, &vd.Status.Phase, supgen) - conditions.SetCondition(cb, &vd.Status.Conditions) } + vd.Status.Phase = virtv2.DiskTerminating return reconcile.Result{}, nil } diff --git a/images/virtualization-artifact/pkg/controller/vi/internal/life_cycle.go b/images/virtualization-artifact/pkg/controller/vi/internal/life_cycle.go index b07bfe5cd8..3b04b4e80f 100644 --- a/images/virtualization-artifact/pkg/controller/vi/internal/life_cycle.go +++ b/images/virtualization-artifact/pkg/controller/vi/internal/life_cycle.go @@ -62,11 +62,11 @@ func (h LifeCycleHandler) Handle(ctx context.Context, vi *virtv2.VirtualImage) ( conditions.SetCondition(cb, &vi.Status.Conditions) } + cb := conditions.NewConditionBuilder(vicondition.ReadyType).Generation(vi.Generation) + if vi.DeletionTimestamp != nil { // It is necessary to update this condition in order to use this image as a datasource. if readyCondition.Status == metav1.ConditionTrue { - cb := conditions.NewConditionBuilder(vicondition.ReadyType).Generation(vi.Generation) - if vi.Spec.Storage == virtv2.StorageContainerRegistry { cb. Status(metav1.ConditionTrue). @@ -81,10 +81,14 @@ func (h LifeCycleHandler) Handle(ctx context.Context, vi *virtv2.VirtualImage) ( source.SetPhaseConditionForFinishedImage(pvc, cb, &vi.Status.Phase, supgen) } - - conditions.SetCondition(cb, &vi.Status.Conditions) + } else { + cb. + Status(readyCondition.Status). + Reason(conditions.ReasonUnknown). + Message("") } + conditions.SetCondition(cb, &vi.Status.Conditions) vi.Status.Phase = virtv2.ImageTerminating return reconcile.Result{}, nil } @@ -114,8 +118,6 @@ func (h LifeCycleHandler) Handle(ctx context.Context, vi *virtv2.VirtualImage) ( return reconcile.Result{Requeue: true}, nil } - cb := conditions.NewConditionBuilder(vicondition.ReadyType).Generation(vi.Generation) - // TODO: Reconciliation in source handlers for ready images should not be blocked by a missing datasource. datasourceReadyCondition, _ := conditions.GetCondition(vicondition.DatasourceReadyType, vi.Status.Conditions) if datasourceReadyCondition.Status != metav1.ConditionTrue || !conditions.IsLastUpdated(datasourceReadyCondition, vi) { From bf7f7f5f27b92d30e06c5e53ae424d16d482be2b Mon Sep 17 00:00:00 2001 From: Valeriy Khorunzhin Date: Thu, 26 Jun 2025 23:28:11 +0300 Subject: [PATCH 03/11] fix vi/cvi working with vm in terminating phase Signed-off-by: Valeriy Khorunzhin --- .../vm/internal/block_device_condition.go | 20 +++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/images/virtualization-artifact/pkg/controller/vm/internal/block_device_condition.go b/images/virtualization-artifact/pkg/controller/vm/internal/block_device_condition.go index 3732e13c44..ca810f47fb 100644 --- a/images/virtualization-artifact/pkg/controller/vm/internal/block_device_condition.go +++ b/images/virtualization-artifact/pkg/controller/vm/internal/block_device_condition.go @@ -27,7 +27,9 @@ import ( "github.com/deckhouse/virtualization-controller/pkg/controller/vm/internal/state" "github.com/deckhouse/virtualization-controller/pkg/logger" virtv2 "github.com/deckhouse/virtualization/api/core/v1alpha2" + "github.com/deckhouse/virtualization/api/core/v1alpha2/cvicondition" "github.com/deckhouse/virtualization/api/core/v1alpha2/vdcondition" + "github.com/deckhouse/virtualization/api/core/v1alpha2/vicondition" "github.com/deckhouse/virtualization/api/core/v1alpha2/vmcondition" ) @@ -289,15 +291,21 @@ func (h *BlockDeviceHandler) countReadyBlockDevices(vm *virtv2.VirtualMachine, s for _, bd := range vm.Spec.BlockDeviceRefs { switch bd.Kind { case virtv2.ImageDevice: - if vi, hasKey := s.VIByName[bd.Name]; hasKey && vi.Status.Phase == virtv2.ImageReady { - ready++ - continue + if vi, hasKey := s.VIByName[bd.Name]; hasKey { + readyCondition, _ := conditions.GetCondition(vicondition.ReadyType, vi.Status.Conditions) + if readyCondition.Status == metav1.ConditionTrue && conditions.IsLastUpdated(readyCondition, vi) { + ready++ + continue + } } canStartKVVM = false case virtv2.ClusterImageDevice: - if cvi, hasKey := s.CVIByName[bd.Name]; hasKey && cvi.Status.Phase == virtv2.ImageReady { - ready++ - continue + if cvi, hasKey := s.CVIByName[bd.Name]; hasKey { + readyCondition, _ := conditions.GetCondition(cvicondition.ReadyType, cvi.Status.Conditions) + if readyCondition.Status == metav1.ConditionTrue && conditions.IsLastUpdated(readyCondition, cvi) { + ready++ + continue + } } canStartKVVM = false case virtv2.DiskDevice: From 52720995a3743f3554f9a644e018c85dd2710184 Mon Sep 17 00:00:00 2001 From: Valeriy Khorunzhin Date: Fri, 27 Jun 2025 09:41:53 +0300 Subject: [PATCH 04/11] fix unit tests Signed-off-by: Valeriy Khorunzhin --- .../vm/internal/block_devices_test.go | 26 +++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/images/virtualization-artifact/pkg/controller/vm/internal/block_devices_test.go b/images/virtualization-artifact/pkg/controller/vm/internal/block_devices_test.go index 9607d2894c..0bc1019513 100644 --- a/images/virtualization-artifact/pkg/controller/vm/internal/block_devices_test.go +++ b/images/virtualization-artifact/pkg/controller/vm/internal/block_devices_test.go @@ -36,7 +36,9 @@ import ( "github.com/deckhouse/virtualization-controller/pkg/controller/vm/internal/state" "github.com/deckhouse/virtualization-controller/pkg/logger" virtv2 "github.com/deckhouse/virtualization/api/core/v1alpha2" + "github.com/deckhouse/virtualization/api/core/v1alpha2/cvicondition" "github.com/deckhouse/virtualization/api/core/v1alpha2/vdcondition" + "github.com/deckhouse/virtualization/api/core/v1alpha2/vicondition" "github.com/deckhouse/virtualization/api/core/v1alpha2/vmcondition" ) @@ -1188,11 +1190,27 @@ var _ = Describe("BlockDeviceHandler", func() { h = NewBlockDeviceHandler(nil, blockDeviceHandlerMock) vi = &virtv2.VirtualImage{ ObjectMeta: metav1.ObjectMeta{Name: "vi-01"}, - Status: virtv2.VirtualImageStatus{Phase: virtv2.ImageReady}, + Status: virtv2.VirtualImageStatus{ + Phase: virtv2.ImageReady, + Conditions: []metav1.Condition{ + { + Type: vicondition.ReadyType.String(), + Status: metav1.ConditionTrue, + }, + }, + }, } cvi = &virtv2.ClusterVirtualImage{ ObjectMeta: metav1.ObjectMeta{Name: "cvi-01"}, - Status: virtv2.ClusterVirtualImageStatus{Phase: virtv2.ImageReady}, + Status: virtv2.ClusterVirtualImageStatus{ + Phase: virtv2.ImageReady, + Conditions: []metav1.Condition{ + { + Type: cvicondition.ReadyType.String(), + Status: metav1.ConditionTrue, + }, + }, + }, } vdFoo = &virtv2.VirtualDisk{ ObjectMeta: metav1.ObjectMeta{Name: "vd1-foo"}, @@ -1266,6 +1284,8 @@ var _ = Describe("BlockDeviceHandler", func() { Context("Image is not ready", func() { It("VirtualImage not ready: cannot start, no warnings", func() { vi.Status.Phase = virtv2.ImagePending + readyConditionCB := conditions.NewConditionBuilder(vicondition.ReadyType).Status(metav1.ConditionFalse) + conditions.SetCondition(readyConditionCB, &vi.Status.Conditions) state := getBlockDevicesState(vi, cvi, vdFoo, vdBar) ready, canStart, warnings := h.countReadyBlockDevices(vm, state, false) Expect(ready).To(Equal(3)) @@ -1275,6 +1295,8 @@ var _ = Describe("BlockDeviceHandler", func() { It("ClusterVirtualImage not ready: cannot start, no warnings", func() { cvi.Status.Phase = virtv2.ImagePending + readyConditionCB := conditions.NewConditionBuilder(cvicondition.ReadyType).Status(metav1.ConditionFalse) + conditions.SetCondition(readyConditionCB, &cvi.Status.Conditions) state := getBlockDevicesState(vi, cvi, vdFoo, vdBar) ready, canStart, warnings := h.countReadyBlockDevices(vm, state, false) Expect(ready).To(Equal(3)) From 19fbf0a4f4dd5c5a4cef571a54a464a5967fce51 Mon Sep 17 00:00:00 2001 From: Valeriy Khorunzhin Date: Tue, 1 Jul 2025 17:43:17 +0300 Subject: [PATCH 05/11] fix cvi namespace usage Signed-off-by: Valeriy Khorunzhin --- .../virtualization-artifact/pkg/controller/cvi/internal/inuse.go | 1 + 1 file changed, 1 insertion(+) diff --git a/images/virtualization-artifact/pkg/controller/cvi/internal/inuse.go b/images/virtualization-artifact/pkg/controller/cvi/internal/inuse.go index bbd10ea77a..5b8d1058e4 100644 --- a/images/virtualization-artifact/pkg/controller/cvi/internal/inuse.go +++ b/images/virtualization-artifact/pkg/controller/cvi/internal/inuse.go @@ -42,6 +42,7 @@ func NewInUseHandler(client client.Client) *InUseHandler { func (h InUseHandler) Handle(ctx context.Context, cvi *virtv2.ClusterVirtualImage) (reconcile.Result, error) { cb := conditions.NewConditionBuilder(cvicondition.InUse).Generation(cvi.Generation) readyCondition, _ := conditions.GetCondition(cvicondition.ReadyType, cvi.Status.Conditions) + cvi.Status.UsedInNamespaces = []string{} if readyCondition.Status == metav1.ConditionFalse && conditions.IsLastUpdated(readyCondition, cvi) { cb. Status(metav1.ConditionFalse). From 35d7ec9a255a5628e7e67bd79389f10de99731e1 Mon Sep 17 00:00:00 2001 From: Valeriy Khorunzhin Date: Tue, 1 Jul 2025 20:20:09 +0300 Subject: [PATCH 06/11] fix finalizers logic Signed-off-by: Valeriy Khorunzhin --- .../pkg/controller/cvi/internal/deletion.go | 4 ++++ .../pkg/controller/vi/internal/deletion.go | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/images/virtualization-artifact/pkg/controller/cvi/internal/deletion.go b/images/virtualization-artifact/pkg/controller/cvi/internal/deletion.go index c84a933a46..50bbcfe519 100644 --- a/images/virtualization-artifact/pkg/controller/cvi/internal/deletion.go +++ b/images/virtualization-artifact/pkg/controller/cvi/internal/deletion.go @@ -52,6 +52,9 @@ func (h DeletionHandler) Handle(ctx context.Context, cvi *virtv2.ClusterVirtualI return reconcile.Result{}, nil } + log.Info("Deletion observed: remove protection finalizer from ClusterVirtualImage") + controllerutil.RemoveFinalizer(cvi, virtv2.FinalizerCVIProtection) + requeue, err := h.sources.CleanUp(ctx, cvi) if err != nil { return reconcile.Result{}, err @@ -67,5 +70,6 @@ func (h DeletionHandler) Handle(ctx context.Context, cvi *virtv2.ClusterVirtualI } controllerutil.AddFinalizer(cvi, virtv2.FinalizerCVICleanup) + controllerutil.AddFinalizer(cvi, virtv2.FinalizerCVIProtection) return reconcile.Result{}, nil } diff --git a/images/virtualization-artifact/pkg/controller/vi/internal/deletion.go b/images/virtualization-artifact/pkg/controller/vi/internal/deletion.go index 711f9beb5e..e6bcac9d5b 100644 --- a/images/virtualization-artifact/pkg/controller/vi/internal/deletion.go +++ b/images/virtualization-artifact/pkg/controller/vi/internal/deletion.go @@ -52,6 +52,9 @@ func (h DeletionHandler) Handle(ctx context.Context, vi *virtv2.VirtualImage) (r return reconcile.Result{}, nil } + log.Info("Deletion observed: remove protection finalizer from VirtualImage") + controllerutil.RemoveFinalizer(vi, virtv2.FinalizerVIProtection) + requeue, err := h.sources.CleanUp(ctx, vi) if err != nil { return reconcile.Result{}, err @@ -67,6 +70,7 @@ func (h DeletionHandler) Handle(ctx context.Context, vi *virtv2.VirtualImage) (r } controllerutil.AddFinalizer(vi, virtv2.FinalizerVICleanup) + controllerutil.AddFinalizer(vi, virtv2.FinalizerVIProtection) return reconcile.Result{}, nil } From a0f26bd218e57a5d07de8692f4e99a5f95af014f Mon Sep 17 00:00:00 2001 From: Valeriy Khorunzhin Date: Thu, 3 Jul 2025 16:40:01 +0300 Subject: [PATCH 07/11] fix vmbda watching Signed-off-by: Valeriy Khorunzhin --- .../pkg/controller/cvi/cvi_reconciler.go | 1 + .../cvi/internal/watcher/vmbda_watcher.go | 91 ++++++++++++++++++ .../vi/internal/watcher/vmbda_watcher.go | 92 +++++++++++++++++++ .../pkg/controller/vi/vi_reconciler.go | 1 + 4 files changed, 185 insertions(+) create mode 100644 images/virtualization-artifact/pkg/controller/cvi/internal/watcher/vmbda_watcher.go create mode 100644 images/virtualization-artifact/pkg/controller/vi/internal/watcher/vmbda_watcher.go diff --git a/images/virtualization-artifact/pkg/controller/cvi/cvi_reconciler.go b/images/virtualization-artifact/pkg/controller/cvi/cvi_reconciler.go index 609afde5e0..0ab38d39e6 100644 --- a/images/virtualization-artifact/pkg/controller/cvi/cvi_reconciler.go +++ b/images/virtualization-artifact/pkg/controller/cvi/cvi_reconciler.go @@ -113,6 +113,7 @@ func (r *Reconciler) SetupController(_ context.Context, mgr manager.Manager, ctr watcher.NewVirtualImageWatcher(mgr.GetClient()), watcher.NewVirtualDiskWatcher(mgr.GetClient()), watcher.NewVirtualDiskSnapshotWatcher(mgr.GetClient()), + watcher.NewVirtualMachineBlockDeviceAttachmentWatcher(mgr.GetClient()), } { err := w.Watch(mgr, ctr) if err != nil { diff --git a/images/virtualization-artifact/pkg/controller/cvi/internal/watcher/vmbda_watcher.go b/images/virtualization-artifact/pkg/controller/cvi/internal/watcher/vmbda_watcher.go new file mode 100644 index 0000000000..282f64f902 --- /dev/null +++ b/images/virtualization-artifact/pkg/controller/cvi/internal/watcher/vmbda_watcher.go @@ -0,0 +1,91 @@ +/* +Copyright 2025 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" + + "k8s.io/apimachinery/pkg/types" + "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" + virtv2 "github.com/deckhouse/virtualization/api/core/v1alpha2" +) + +type VirtualMachineBlockDeviceAttachmentWatcher struct { + logger *log.Logger + client client.Client +} + +func NewVirtualMachineBlockDeviceAttachmentWatcher(client client.Client) *VirtualMachineBlockDeviceAttachmentWatcher { + return &VirtualMachineBlockDeviceAttachmentWatcher{ + logger: log.Default().With("watcher", "vmbda"), + client: client, + } +} + +func (w VirtualMachineBlockDeviceAttachmentWatcher) Watch(mgr manager.Manager, ctr controller.Controller) error { + return ctr.Watch( + source.Kind(mgr.GetCache(), &virtv2.VirtualMachineBlockDeviceAttachment{}), + handler.EnqueueRequestsFromMapFunc(w.enqueueRequests), + predicate.Funcs{ + CreateFunc: func(e event.CreateEvent) bool { + return w.isClusterVirtualImageRef(e.Object) + }, + DeleteFunc: func(e event.DeleteEvent) bool { + return w.isClusterVirtualImageRef(e.Object) + }, + UpdateFunc: func(e event.UpdateEvent) bool { + return w.isClusterVirtualImageRef(e.ObjectOld) || w.isClusterVirtualImageRef(e.ObjectNew) + }, + }, + ) +} + +func (w VirtualMachineBlockDeviceAttachmentWatcher) enqueueRequests(ctx context.Context, obj client.Object) (requests []reconcile.Request) { + vmbda, ok := obj.(*virtv2.VirtualMachineBlockDeviceAttachment) + if !ok { + w.logger.Error(fmt.Sprintf("expected a VirtualMachineBlockDeviceAttachment but got a %T", obj)) + return + } + + requests = append(requests, reconcile.Request{ + NamespacedName: types.NamespacedName{ + Name: vmbda.Spec.BlockDeviceRef.Name, + }, + }) + + return +} + +func (w VirtualMachineBlockDeviceAttachmentWatcher) isClusterVirtualImageRef(obj client.Object) bool { + vmbda, ok := obj.(*virtv2.VirtualMachineBlockDeviceAttachment) + if !ok { + w.logger.Error(fmt.Sprintf("expected a VirtualMachineBlockDeviceAttachment but got a %T", obj)) + return false + } + + return vmbda.Spec.BlockDeviceRef.Kind == virtv2.ClusterVirtualImageKind +} diff --git a/images/virtualization-artifact/pkg/controller/vi/internal/watcher/vmbda_watcher.go b/images/virtualization-artifact/pkg/controller/vi/internal/watcher/vmbda_watcher.go new file mode 100644 index 0000000000..2d8095be98 --- /dev/null +++ b/images/virtualization-artifact/pkg/controller/vi/internal/watcher/vmbda_watcher.go @@ -0,0 +1,92 @@ +/* +Copyright 2025 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" + + "k8s.io/apimachinery/pkg/types" + "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" + virtv2 "github.com/deckhouse/virtualization/api/core/v1alpha2" +) + +type VirtualMachineBlockDeviceAttachmentWatcher struct { + logger *log.Logger + client client.Client +} + +func NewVirtualMachineBlockDeviceAttachmentWatcher(client client.Client) *VirtualMachineBlockDeviceAttachmentWatcher { + return &VirtualMachineBlockDeviceAttachmentWatcher{ + logger: log.Default().With("watcher", "vmbda"), + client: client, + } +} + +func (w VirtualMachineBlockDeviceAttachmentWatcher) Watch(mgr manager.Manager, ctr controller.Controller) error { + return ctr.Watch( + source.Kind(mgr.GetCache(), &virtv2.VirtualMachineBlockDeviceAttachment{}), + handler.EnqueueRequestsFromMapFunc(w.enqueueRequests), + predicate.Funcs{ + CreateFunc: func(e event.CreateEvent) bool { + return w.isVirtualImageRef(e.Object) + }, + DeleteFunc: func(e event.DeleteEvent) bool { + return w.isVirtualImageRef(e.Object) + }, + UpdateFunc: func(e event.UpdateEvent) bool { + return w.isVirtualImageRef(e.ObjectOld) || w.isVirtualImageRef(e.ObjectNew) + }, + }, + ) +} + +func (w VirtualMachineBlockDeviceAttachmentWatcher) enqueueRequests(ctx context.Context, obj client.Object) (requests []reconcile.Request) { + vmbda, ok := obj.(*virtv2.VirtualMachineBlockDeviceAttachment) + if !ok { + w.logger.Error(fmt.Sprintf("expected a VirtualMachineBlockDeviceAttachment but got a %T", obj)) + return + } + + requests = append(requests, reconcile.Request{ + NamespacedName: types.NamespacedName{ + Name: vmbda.Spec.BlockDeviceRef.Name, + Namespace: vmbda.Namespace, + }, + }) + + return +} + +func (w VirtualMachineBlockDeviceAttachmentWatcher) isVirtualImageRef(obj client.Object) bool { + vmbda, ok := obj.(*virtv2.VirtualMachineBlockDeviceAttachment) + if !ok { + w.logger.Error(fmt.Sprintf("expected a VirtualMachineBlockDeviceAttachment but got a %T", obj)) + return false + } + + return vmbda.Spec.BlockDeviceRef.Kind == virtv2.VirtualImageKind +} diff --git a/images/virtualization-artifact/pkg/controller/vi/vi_reconciler.go b/images/virtualization-artifact/pkg/controller/vi/vi_reconciler.go index a1d9d07ff2..d09976d84f 100644 --- a/images/virtualization-artifact/pkg/controller/vi/vi_reconciler.go +++ b/images/virtualization-artifact/pkg/controller/vi/vi_reconciler.go @@ -120,6 +120,7 @@ func (r *Reconciler) SetupController(_ context.Context, mgr manager.Manager, ctr watcher.NewVirtualDiskWatcher(mgr.GetClient()), watcher.NewClusterVirtualImageWatcher(mgr.GetClient()), watcher.NewVirtualImageWatcher(mgr.GetClient()), + watcher.NewVirtualMachineBlockDeviceAttachmentWatcher(mgr.GetClient()), } { err := w.Watch(mgr, ctr) if err != nil { From 61ac8c9f2d8f7bdb1dd79542c7b1651b18c24a15 Mon Sep 17 00:00:00 2001 From: Valeriy Khorunzhin Date: Mon, 25 Aug 2025 16:13:42 +0300 Subject: [PATCH 08/11] fix watchers Signed-off-by: Valeriy Khorunzhin --- .../cvi/internal/watcher/cvi_watcher.go | 78 +++++----- .../cvi/internal/watcher/vd_watcher.go | 130 ----------------- .../cvi/internal/watcher/vi_watcher.go | 66 ++++----- .../internal/watcher/virtualdisk_watcher.go | 19 ++- .../cvi/internal/watcher/vmbda_watcher.go | 43 ++---- .../vi/internal/watcher/cvi_watcher.go | 58 +++----- .../vi/internal/watcher/vd_watcher.go | 134 ------------------ .../vi/internal/watcher/vi_watcher.go | 75 +++++----- .../internal/watcher/virdualdisk_watcher.go | 14 +- .../vi/internal/watcher/vmbda_watcher.go | 42 ++---- .../pkg/controller/vi/vi_reconciler.go | 17 --- 11 files changed, 180 insertions(+), 496 deletions(-) delete mode 100644 images/virtualization-artifact/pkg/controller/cvi/internal/watcher/vd_watcher.go delete mode 100644 images/virtualization-artifact/pkg/controller/vi/internal/watcher/vd_watcher.go diff --git a/images/virtualization-artifact/pkg/controller/cvi/internal/watcher/cvi_watcher.go b/images/virtualization-artifact/pkg/controller/cvi/internal/watcher/cvi_watcher.go index 31b9a03817..a6afc8fc45 100644 --- a/images/virtualization-artifact/pkg/controller/cvi/internal/watcher/cvi_watcher.go +++ b/images/virtualization-artifact/pkg/controller/cvi/internal/watcher/cvi_watcher.go @@ -30,43 +30,57 @@ import ( "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/conditions" - "github.com/deckhouse/virtualization-controller/pkg/logger" virtv2 "github.com/deckhouse/virtualization/api/core/v1alpha2" "github.com/deckhouse/virtualization/api/core/v1alpha2/cvicondition" ) type ClusterVirtualImageWatcher struct { client client.Client + logger *log.Logger } func NewClusterVirtualImageWatcher(client client.Client) *ClusterVirtualImageWatcher { return &ClusterVirtualImageWatcher{ client: client, + logger: log.Default().With("watcher", "cvi"), } } func (w ClusterVirtualImageWatcher) Watch(mgr manager.Manager, ctr controller.Controller) error { - return ctr.Watch( - source.Kind(mgr.GetCache(), &virtv2.ClusterVirtualImage{}), - handler.EnqueueRequestsFromMapFunc(w.enqueueRequests), - predicate.Funcs{ - CreateFunc: func(e event.CreateEvent) bool { - return true - }, - DeleteFunc: func(e event.DeleteEvent) bool { - return true + if err := ctr.Watch( + source.Kind(mgr.GetCache(), + &virtv2.ClusterVirtualImage{}, + handler.TypedEnqueueRequestsFromMapFunc(w.enqueueRequests), + predicate.TypedFuncs[*virtv2.ClusterVirtualImage]{ + UpdateFunc: func(e event.TypedUpdateEvent[*virtv2.ClusterVirtualImage]) bool { + if e.ObjectOld.GetGeneration() != e.ObjectNew.GetGeneration() { + return true + } + + oldReadyCondition, _ := conditions.GetCondition(cvicondition.ReadyType, e.ObjectOld.Status.Conditions) + newReadyCondition, _ := conditions.GetCondition(cvicondition.ReadyType, e.ObjectNew.Status.Conditions) + + if e.ObjectOld.Status.Phase != e.ObjectNew.Status.Phase || oldReadyCondition.Status != newReadyCondition.Status { + return true + } + + return false + }, }, - UpdateFunc: w.filterUpdateEvents, - }, - ) + ), + ); err != nil { + return fmt.Errorf("error setting watch on CVIs: %w", err) + } + return nil } -func (w ClusterVirtualImageWatcher) enqueueRequests(ctx context.Context, obj client.Object) (requests []reconcile.Request) { +func (w ClusterVirtualImageWatcher) enqueueRequests(ctx context.Context, obj *virtv2.ClusterVirtualImage) (requests []reconcile.Request) { var cviList virtv2.ClusterVirtualImageList err := w.client.List(ctx, &cviList) if err != nil { - logger.FromContext(ctx).Error(fmt.Sprintf("failed to list cvi: %s", err)) + w.logger.Error(fmt.Sprintf("failed to list cvi: %s", err)) return } @@ -87,38 +101,22 @@ func (w ClusterVirtualImageWatcher) enqueueRequests(ctx context.Context, obj cli }) } - cvi, ok := obj.(*virtv2.ClusterVirtualImage) - if ok && cvi.Spec.DataSource.Type == virtv2.DataSourceTypeObjectRef { - if cvi.Spec.DataSource.ObjectRef != nil && cvi.Spec.DataSource.ObjectRef.Kind == virtv2.ClusterVirtualImageKind { + if obj.Spec.DataSource.Type == virtv2.DataSourceTypeObjectRef { + if obj.Spec.DataSource.ObjectRef != nil && obj.Spec.DataSource.ObjectRef.Kind == virtv2.ClusterVirtualImageKind { // Need to trigger reconcile for update InUse condition. requests = append(requests, reconcile.Request{ NamespacedName: types.NamespacedName{ - Name: cvi.Spec.DataSource.ObjectRef.Name, + Name: obj.Spec.DataSource.ObjectRef.Name, }, }) } } - return -} - -func (w ClusterVirtualImageWatcher) filterUpdateEvents(e event.UpdateEvent) bool { - oldCVI, ok := e.ObjectOld.(*virtv2.ClusterVirtualImage) - if !ok { - return false - } - - newCVI, ok := e.ObjectNew.(*virtv2.ClusterVirtualImage) - if !ok { - return false - } - - oldReadyCondition, _ := conditions.GetCondition(cvicondition.ReadyType, oldCVI.Status.Conditions) - newReadyCondition, _ := conditions.GetCondition(cvicondition.ReadyType, newCVI.Status.Conditions) - - if oldCVI.Status.Phase != newCVI.Status.Phase || oldReadyCondition.Status != newReadyCondition.Status { - return true - } + requests = append(requests, reconcile.Request{ + NamespacedName: types.NamespacedName{ + Name: obj.Name, + }, + }) - return false + return } diff --git a/images/virtualization-artifact/pkg/controller/cvi/internal/watcher/vd_watcher.go b/images/virtualization-artifact/pkg/controller/cvi/internal/watcher/vd_watcher.go deleted file mode 100644 index 79985805b1..0000000000 --- a/images/virtualization-artifact/pkg/controller/cvi/internal/watcher/vd_watcher.go +++ /dev/null @@ -1,130 +0,0 @@ -/* -Copyright 2025 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" - - "k8s.io/apimachinery/pkg/types" - "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/virtualization-controller/pkg/controller/conditions" - "github.com/deckhouse/virtualization-controller/pkg/logger" - virtv2 "github.com/deckhouse/virtualization/api/core/v1alpha2" - "github.com/deckhouse/virtualization/api/core/v1alpha2/vdcondition" -) - -type VirtualDiskWatcher struct { - client client.Client -} - -func NewVirtualDiskWatcher(client client.Client) *VirtualDiskWatcher { - return &VirtualDiskWatcher{ - client: client, - } -} - -func (w VirtualDiskWatcher) Watch(mgr manager.Manager, ctr controller.Controller) error { - return ctr.Watch( - source.Kind(mgr.GetCache(), &virtv2.VirtualDisk{}), - handler.EnqueueRequestsFromMapFunc(w.enqueueRequests), - predicate.Funcs{ - CreateFunc: func(e event.CreateEvent) bool { - return true - }, - DeleteFunc: func(e event.DeleteEvent) bool { - return true - }, - UpdateFunc: w.filterUpdateEvents, - }, - ) -} - -func (w VirtualDiskWatcher) enqueueRequests(ctx context.Context, obj client.Object) (requests []reconcile.Request) { - var cviList virtv2.ClusterVirtualImageList - err := w.client.List(ctx, &cviList) - if err != nil { - logger.FromContext(ctx).Error(fmt.Sprintf("failed to list cvi: %s", err)) - return - } - - // We need to trigger reconcile for the cvi resources that use changed disk as a datasource so they can continue provisioning. - for _, cvi := range cviList.Items { - if cvi.Spec.DataSource.Type != virtv2.DataSourceTypeObjectRef || cvi.Spec.DataSource.ObjectRef == nil { - continue - } - - if cvi.Spec.DataSource.ObjectRef.Kind != virtv2.VirtualDiskKind || cvi.Spec.DataSource.ObjectRef.Name != obj.GetName() { - continue - } - - requests = append(requests, reconcile.Request{ - NamespacedName: types.NamespacedName{ - Name: cvi.Name, - }, - }) - } - - vd, ok := obj.(*virtv2.VirtualDisk) - if ok && vd.Spec.DataSource != nil && vd.Spec.DataSource.Type == virtv2.DataSourceTypeObjectRef { - if vd.Spec.DataSource.ObjectRef != nil && vd.Spec.DataSource.ObjectRef.Kind == virtv2.ClusterVirtualImageKind { - // Need to trigger reconcile for update InUse condition. - requests = append(requests, reconcile.Request{ - NamespacedName: types.NamespacedName{ - Name: vd.Spec.DataSource.ObjectRef.Name, - }, - }) - } - } - - return -} - -func (w VirtualDiskWatcher) filterUpdateEvents(e event.UpdateEvent) bool { - oldVD, ok := e.ObjectOld.(*virtv2.VirtualDisk) - if !ok { - return false - } - - newVD, ok := e.ObjectNew.(*virtv2.VirtualDisk) - if !ok { - return false - } - - oldInUseCondition, _ := conditions.GetCondition(vdcondition.InUseType, oldVD.Status.Conditions) - newInUseCondition, _ := conditions.GetCondition(vdcondition.InUseType, newVD.Status.Conditions) - - oldReadyCondition, _ := conditions.GetCondition(vdcondition.ReadyType, oldVD.Status.Conditions) - newReadyCondition, _ := conditions.GetCondition(vdcondition.ReadyType, newVD.Status.Conditions) - - if oldVD.Status.Phase != newVD.Status.Phase || - len(oldVD.Status.AttachedToVirtualMachines) != len(newVD.Status.AttachedToVirtualMachines) || - oldInUseCondition.Status != newInUseCondition.Status || - oldReadyCondition.Status != newReadyCondition.Status { - return true - } - - return false -} diff --git a/images/virtualization-artifact/pkg/controller/cvi/internal/watcher/vi_watcher.go b/images/virtualization-artifact/pkg/controller/cvi/internal/watcher/vi_watcher.go index 3144deb1f8..99ec137558 100644 --- a/images/virtualization-artifact/pkg/controller/cvi/internal/watcher/vi_watcher.go +++ b/images/virtualization-artifact/pkg/controller/cvi/internal/watcher/vi_watcher.go @@ -30,43 +30,53 @@ import ( "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/conditions" - "github.com/deckhouse/virtualization-controller/pkg/logger" virtv2 "github.com/deckhouse/virtualization/api/core/v1alpha2" "github.com/deckhouse/virtualization/api/core/v1alpha2/vicondition" ) type VirtualImageWatcher struct { client client.Client + logger *log.Logger } func NewVirtualImageWatcher(client client.Client) *VirtualImageWatcher { return &VirtualImageWatcher{ client: client, + logger: log.Default().With("watcher", "vi"), } } func (w VirtualImageWatcher) Watch(mgr manager.Manager, ctr controller.Controller) error { - return ctr.Watch( - source.Kind(mgr.GetCache(), &virtv2.VirtualImage{}), - handler.EnqueueRequestsFromMapFunc(w.enqueueRequests), - predicate.Funcs{ - CreateFunc: func(e event.CreateEvent) bool { - return true - }, - DeleteFunc: func(e event.DeleteEvent) bool { - return true + if err := ctr.Watch( + source.Kind(mgr.GetCache(), + &virtv2.VirtualImage{}, + handler.TypedEnqueueRequestsFromMapFunc(w.enqueueRequests), + predicate.TypedFuncs[*virtv2.VirtualImage]{ + UpdateFunc: func(e event.TypedUpdateEvent[*virtv2.VirtualImage]) bool { + oldReadyCondition, _ := conditions.GetCondition(vicondition.ReadyType, e.ObjectOld.Status.Conditions) + newReadyCondition, _ := conditions.GetCondition(vicondition.ReadyType, e.ObjectNew.Status.Conditions) + + if e.ObjectOld.Status.Phase != e.ObjectNew.Status.Phase || oldReadyCondition.Status != newReadyCondition.Status { + return true + } + + return false + }, }, - UpdateFunc: w.filterUpdateEvents, - }, - ) + ), + ); err != nil { + return fmt.Errorf("error setting watch on VIs: %w", err) + } + return nil } -func (w VirtualImageWatcher) enqueueRequests(ctx context.Context, obj client.Object) (requests []reconcile.Request) { +func (w VirtualImageWatcher) enqueueRequests(ctx context.Context, vi *virtv2.VirtualImage) (requests []reconcile.Request) { var cviList virtv2.ClusterVirtualImageList err := w.client.List(ctx, &cviList) if err != nil { - logger.FromContext(ctx).Error(fmt.Sprintf("failed to list cvi: %s", err)) + w.logger.Error(fmt.Sprintf("failed to list cvi: %s", err)) return } @@ -76,7 +86,7 @@ func (w VirtualImageWatcher) enqueueRequests(ctx context.Context, obj client.Obj continue } - if cvi.Spec.DataSource.ObjectRef.Kind != virtv2.VirtualImageKind || cvi.Spec.DataSource.ObjectRef.Name != obj.GetName() { + if cvi.Spec.DataSource.ObjectRef.Kind != virtv2.VirtualImageKind || cvi.Spec.DataSource.ObjectRef.Name != vi.GetName() { continue } @@ -87,8 +97,7 @@ func (w VirtualImageWatcher) enqueueRequests(ctx context.Context, obj client.Obj }) } - vi, ok := obj.(*virtv2.VirtualImage) - if ok && vi.Spec.DataSource.Type == virtv2.DataSourceTypeObjectRef { + if vi.Spec.DataSource.Type == virtv2.DataSourceTypeObjectRef { if vi.Spec.DataSource.ObjectRef != nil && vi.Spec.DataSource.ObjectRef.Kind == virtv2.ClusterVirtualImageKind { // Need to trigger reconcile for update InUse condition. requests = append(requests, reconcile.Request{ @@ -101,24 +110,3 @@ func (w VirtualImageWatcher) enqueueRequests(ctx context.Context, obj client.Obj return } - -func (w VirtualImageWatcher) filterUpdateEvents(e event.UpdateEvent) bool { - oldVI, ok := e.ObjectOld.(*virtv2.VirtualImage) - if !ok { - return false - } - - newVI, ok := e.ObjectNew.(*virtv2.VirtualImage) - if !ok { - return false - } - - oldReadyCondition, _ := conditions.GetCondition(vicondition.ReadyType, oldVI.Status.Conditions) - newReadyCondition, _ := conditions.GetCondition(vicondition.ReadyType, newVI.Status.Conditions) - - if oldVI.Status.Phase != newVI.Status.Phase || oldReadyCondition.Status != newReadyCondition.Status { - return true - } - - return false -} diff --git a/images/virtualization-artifact/pkg/controller/cvi/internal/watcher/virtualdisk_watcher.go b/images/virtualization-artifact/pkg/controller/cvi/internal/watcher/virtualdisk_watcher.go index 13f543ed37..2e00cb597e 100644 --- a/images/virtualization-artifact/pkg/controller/cvi/internal/watcher/virtualdisk_watcher.go +++ b/images/virtualization-artifact/pkg/controller/cvi/internal/watcher/virtualdisk_watcher.go @@ -57,7 +57,13 @@ func (w *VirtualDiskWatcher) Watch(mgr manager.Manager, ctr controller.Controlle oldInUseCondition, _ := conditions.GetCondition(vdcondition.InUseType, e.ObjectOld.Status.Conditions) newInUseCondition, _ := conditions.GetCondition(vdcondition.InUseType, e.ObjectNew.Status.Conditions) - if e.ObjectOld.Status.Phase != e.ObjectNew.Status.Phase || len(e.ObjectOld.Status.AttachedToVirtualMachines) != len(e.ObjectNew.Status.AttachedToVirtualMachines) || oldInUseCondition != newInUseCondition { + oldReadyCondition, _ := conditions.GetCondition(vdcondition.ReadyType, e.ObjectOld.Status.Conditions) + newReadyCondition, _ := conditions.GetCondition(vdcondition.ReadyType, e.ObjectNew.Status.Conditions) + + if e.ObjectOld.Status.Phase != e.ObjectNew.Status.Phase || + len(e.ObjectOld.Status.AttachedToVirtualMachines) != len(e.ObjectNew.Status.AttachedToVirtualMachines) || + oldInUseCondition != newInUseCondition || + oldReadyCondition.Status != newReadyCondition.Status { return true } @@ -95,5 +101,16 @@ func (w *VirtualDiskWatcher) enqueueRequestsFromVDs(ctx context.Context, vd *vir }) } + if vd.Spec.DataSource != nil && vd.Spec.DataSource.Type == virtv2.DataSourceTypeObjectRef { + if vd.Spec.DataSource.ObjectRef != nil && vd.Spec.DataSource.ObjectRef.Kind == virtv2.ClusterVirtualImageKind { + // Need to trigger reconcile for update InUse condition. + requests = append(requests, reconcile.Request{ + NamespacedName: types.NamespacedName{ + Name: vd.Spec.DataSource.ObjectRef.Name, + }, + }) + } + } + return } diff --git a/images/virtualization-artifact/pkg/controller/cvi/internal/watcher/vmbda_watcher.go b/images/virtualization-artifact/pkg/controller/cvi/internal/watcher/vmbda_watcher.go index 282f64f902..550929e589 100644 --- a/images/virtualization-artifact/pkg/controller/cvi/internal/watcher/vmbda_watcher.go +++ b/images/virtualization-artifact/pkg/controller/cvi/internal/watcher/vmbda_watcher.go @@ -47,30 +47,23 @@ func NewVirtualMachineBlockDeviceAttachmentWatcher(client client.Client) *Virtua } func (w VirtualMachineBlockDeviceAttachmentWatcher) Watch(mgr manager.Manager, ctr controller.Controller) error { - return ctr.Watch( - source.Kind(mgr.GetCache(), &virtv2.VirtualMachineBlockDeviceAttachment{}), - handler.EnqueueRequestsFromMapFunc(w.enqueueRequests), - predicate.Funcs{ - CreateFunc: func(e event.CreateEvent) bool { - return w.isClusterVirtualImageRef(e.Object) + if err := ctr.Watch( + source.Kind(mgr.GetCache(), + &virtv2.VirtualMachineBlockDeviceAttachment{}, + handler.TypedEnqueueRequestsFromMapFunc(w.enqueueRequests), + predicate.TypedFuncs[*virtv2.VirtualMachineBlockDeviceAttachment]{ + UpdateFunc: func(e event.TypedUpdateEvent[*virtv2.VirtualMachineBlockDeviceAttachment]) bool { + return w.isClusterVirtualImageRef(e.ObjectOld) || w.isClusterVirtualImageRef(e.ObjectNew) + }, }, - DeleteFunc: func(e event.DeleteEvent) bool { - return w.isClusterVirtualImageRef(e.Object) - }, - UpdateFunc: func(e event.UpdateEvent) bool { - return w.isClusterVirtualImageRef(e.ObjectOld) || w.isClusterVirtualImageRef(e.ObjectNew) - }, - }, - ) -} - -func (w VirtualMachineBlockDeviceAttachmentWatcher) enqueueRequests(ctx context.Context, obj client.Object) (requests []reconcile.Request) { - vmbda, ok := obj.(*virtv2.VirtualMachineBlockDeviceAttachment) - if !ok { - w.logger.Error(fmt.Sprintf("expected a VirtualMachineBlockDeviceAttachment but got a %T", obj)) - return + ), + ); err != nil { + return fmt.Errorf("error setting watch on VIs: %w", err) } + return nil +} +func (w VirtualMachineBlockDeviceAttachmentWatcher) enqueueRequests(ctx context.Context, vmbda *virtv2.VirtualMachineBlockDeviceAttachment) (requests []reconcile.Request) { requests = append(requests, reconcile.Request{ NamespacedName: types.NamespacedName{ Name: vmbda.Spec.BlockDeviceRef.Name, @@ -80,12 +73,6 @@ func (w VirtualMachineBlockDeviceAttachmentWatcher) enqueueRequests(ctx context. return } -func (w VirtualMachineBlockDeviceAttachmentWatcher) isClusterVirtualImageRef(obj client.Object) bool { - vmbda, ok := obj.(*virtv2.VirtualMachineBlockDeviceAttachment) - if !ok { - w.logger.Error(fmt.Sprintf("expected a VirtualMachineBlockDeviceAttachment but got a %T", obj)) - return false - } - +func (w VirtualMachineBlockDeviceAttachmentWatcher) isClusterVirtualImageRef(vmbda *virtv2.VirtualMachineBlockDeviceAttachment) bool { return vmbda.Spec.BlockDeviceRef.Kind == virtv2.ClusterVirtualImageKind } diff --git a/images/virtualization-artifact/pkg/controller/vi/internal/watcher/cvi_watcher.go b/images/virtualization-artifact/pkg/controller/vi/internal/watcher/cvi_watcher.go index faed6c1e6e..d387ce1206 100644 --- a/images/virtualization-artifact/pkg/controller/vi/internal/watcher/cvi_watcher.go +++ b/images/virtualization-artifact/pkg/controller/vi/internal/watcher/cvi_watcher.go @@ -30,43 +30,49 @@ import ( "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/conditions" - "github.com/deckhouse/virtualization-controller/pkg/logger" virtv2 "github.com/deckhouse/virtualization/api/core/v1alpha2" "github.com/deckhouse/virtualization/api/core/v1alpha2/cvicondition" ) type ClusterVirtualImageWatcher struct { + logger *log.Logger client client.Client } func NewClusterVirtualImageWatcher(client client.Client) *ClusterVirtualImageWatcher { return &ClusterVirtualImageWatcher{ + logger: log.Default().With("watcher", "cvi"), client: client, } } func (w ClusterVirtualImageWatcher) Watch(mgr manager.Manager, ctr controller.Controller) error { return ctr.Watch( - source.Kind(mgr.GetCache(), &virtv2.ClusterVirtualImage{}), - handler.EnqueueRequestsFromMapFunc(w.enqueueRequests), - predicate.Funcs{ - CreateFunc: func(e event.CreateEvent) bool { - return true - }, - DeleteFunc: func(e event.DeleteEvent) bool { - return true + source.Kind(mgr.GetCache(), &virtv2.ClusterVirtualImage{}, + handler.TypedEnqueueRequestsFromMapFunc(w.enqueueRequests), + predicate.TypedFuncs[*virtv2.ClusterVirtualImage]{ + UpdateFunc: func(e event.TypedUpdateEvent[*virtv2.ClusterVirtualImage]) bool { + oldReadyCondition, _ := conditions.GetCondition(cvicondition.ReadyType, e.ObjectOld.Status.Conditions) + newReadyCondition, _ := conditions.GetCondition(cvicondition.ReadyType, e.ObjectNew.Status.Conditions) + + if e.ObjectOld.Status.Phase != e.ObjectNew.Status.Phase || oldReadyCondition.Status != newReadyCondition.Status { + return true + } + + return false + }, }, - UpdateFunc: w.filterUpdateEvents, - }, + ), ) } -func (w ClusterVirtualImageWatcher) enqueueRequests(ctx context.Context, obj client.Object) (requests []reconcile.Request) { +func (w ClusterVirtualImageWatcher) enqueueRequests(ctx context.Context, cvi *virtv2.ClusterVirtualImage) (requests []reconcile.Request) { var viList virtv2.VirtualImageList err := w.client.List(ctx, &viList) if err != nil { - logger.FromContext(ctx).Error(fmt.Sprintf("failed to list vi: %s", err)) + w.logger.Error(fmt.Sprintf("failed to list vi: %s", err)) return } @@ -76,7 +82,7 @@ func (w ClusterVirtualImageWatcher) enqueueRequests(ctx context.Context, obj cli continue } - if vi.Spec.DataSource.ObjectRef.Kind != virtv2.ClusterVirtualImageKind || vi.Spec.DataSource.ObjectRef.Name != obj.GetName() { + if vi.Spec.DataSource.ObjectRef.Kind != virtv2.ClusterVirtualImageKind || vi.Spec.DataSource.ObjectRef.Name != cvi.GetName() { continue } @@ -88,8 +94,7 @@ func (w ClusterVirtualImageWatcher) enqueueRequests(ctx context.Context, obj cli }) } - cvi, ok := obj.(*virtv2.ClusterVirtualImage) - if ok && cvi.Spec.DataSource.Type == virtv2.DataSourceTypeObjectRef { + if cvi.Spec.DataSource.Type == virtv2.DataSourceTypeObjectRef { if cvi.Spec.DataSource.ObjectRef != nil && cvi.Spec.DataSource.ObjectRef.Kind == virtv2.VirtualImageKind { // Need to trigger reconcile for update InUse condition. requests = append(requests, reconcile.Request{ @@ -103,24 +108,3 @@ func (w ClusterVirtualImageWatcher) enqueueRequests(ctx context.Context, obj cli return } - -func (w ClusterVirtualImageWatcher) filterUpdateEvents(e event.UpdateEvent) bool { - oldCVI, ok := e.ObjectOld.(*virtv2.ClusterVirtualImage) - if !ok { - return false - } - - newCVI, ok := e.ObjectNew.(*virtv2.ClusterVirtualImage) - if !ok { - return false - } - - oldReadyCondition, _ := conditions.GetCondition(cvicondition.ReadyType, oldCVI.Status.Conditions) - newReadyCondition, _ := conditions.GetCondition(cvicondition.ReadyType, newCVI.Status.Conditions) - - if oldCVI.Status.Phase != newCVI.Status.Phase || oldReadyCondition.Status != newReadyCondition.Status { - return true - } - - return false -} diff --git a/images/virtualization-artifact/pkg/controller/vi/internal/watcher/vd_watcher.go b/images/virtualization-artifact/pkg/controller/vi/internal/watcher/vd_watcher.go deleted file mode 100644 index 9974328681..0000000000 --- a/images/virtualization-artifact/pkg/controller/vi/internal/watcher/vd_watcher.go +++ /dev/null @@ -1,134 +0,0 @@ -/* -Copyright 2025 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" - - "k8s.io/apimachinery/pkg/types" - "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/virtualization-controller/pkg/controller/conditions" - "github.com/deckhouse/virtualization-controller/pkg/logger" - virtv2 "github.com/deckhouse/virtualization/api/core/v1alpha2" - "github.com/deckhouse/virtualization/api/core/v1alpha2/vdcondition" -) - -type VirtualDiskWatcher struct { - client client.Client -} - -func NewVirtualDiskWatcher(client client.Client) *VirtualDiskWatcher { - return &VirtualDiskWatcher{ - client: client, - } -} - -func (w VirtualDiskWatcher) Watch(mgr manager.Manager, ctr controller.Controller) error { - return ctr.Watch( - source.Kind(mgr.GetCache(), &virtv2.VirtualDisk{}), - handler.EnqueueRequestsFromMapFunc(w.enqueueRequests), - predicate.Funcs{ - CreateFunc: func(e event.CreateEvent) bool { - return true - }, - DeleteFunc: func(e event.DeleteEvent) bool { - return true - }, - UpdateFunc: w.filterUpdateEvents, - }, - ) -} - -func (w VirtualDiskWatcher) enqueueRequests(ctx context.Context, obj client.Object) (requests []reconcile.Request) { - var viList virtv2.VirtualImageList - err := w.client.List(ctx, &viList, &client.ListOptions{ - Namespace: obj.GetNamespace(), - }) - if err != nil { - logger.FromContext(ctx).Error(fmt.Sprintf("failed to list vi: %s", err)) - return - } - - // We need to trigger reconcile for the vi resources that use changed image as a datasource so they can continue provisioning. - for _, vi := range viList.Items { - if vi.Spec.DataSource.Type != virtv2.DataSourceTypeObjectRef || vi.Spec.DataSource.ObjectRef == nil { - continue - } - - if vi.Spec.DataSource.ObjectRef.Kind != virtv2.VirtualDiskKind || vi.Spec.DataSource.ObjectRef.Name != obj.GetName() { - continue - } - - requests = append(requests, reconcile.Request{ - NamespacedName: types.NamespacedName{ - Name: vi.Name, - Namespace: vi.Namespace, - }, - }) - } - - vd, ok := obj.(*virtv2.VirtualDisk) - if ok && vd.Spec.DataSource != nil && vd.Spec.DataSource.Type == virtv2.DataSourceTypeObjectRef { - if vd.Spec.DataSource.ObjectRef != nil && vd.Spec.DataSource.ObjectRef.Kind == virtv2.VirtualImageKind { - // Need to trigger reconcile for update InUse condition. - requests = append(requests, reconcile.Request{ - NamespacedName: types.NamespacedName{ - Name: vd.Spec.DataSource.ObjectRef.Name, - Namespace: vd.Namespace, - }, - }) - } - } - - return -} - -func (w VirtualDiskWatcher) filterUpdateEvents(e event.UpdateEvent) bool { - oldVD, ok := e.ObjectOld.(*virtv2.VirtualDisk) - if !ok { - return false - } - - newVD, ok := e.ObjectNew.(*virtv2.VirtualDisk) - if !ok { - return false - } - - oldInUseCondition, _ := conditions.GetCondition(vdcondition.InUseType, oldVD.Status.Conditions) - newInUseCondition, _ := conditions.GetCondition(vdcondition.InUseType, newVD.Status.Conditions) - - oldReadyCondition, _ := conditions.GetCondition(vdcondition.ReadyType, oldVD.Status.Conditions) - newReadyCondition, _ := conditions.GetCondition(vdcondition.ReadyType, newVD.Status.Conditions) - - if oldVD.Status.Phase != newVD.Status.Phase || - len(oldVD.Status.AttachedToVirtualMachines) != len(newVD.Status.AttachedToVirtualMachines) || - oldInUseCondition.Status != newInUseCondition.Status || - oldReadyCondition.Status != newReadyCondition.Status { - return true - } - - return false -} diff --git a/images/virtualization-artifact/pkg/controller/vi/internal/watcher/vi_watcher.go b/images/virtualization-artifact/pkg/controller/vi/internal/watcher/vi_watcher.go index 9ed266cb8c..0bc3435d06 100644 --- a/images/virtualization-artifact/pkg/controller/vi/internal/watcher/vi_watcher.go +++ b/images/virtualization-artifact/pkg/controller/vi/internal/watcher/vi_watcher.go @@ -30,45 +30,55 @@ import ( "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/conditions" - "github.com/deckhouse/virtualization-controller/pkg/logger" virtv2 "github.com/deckhouse/virtualization/api/core/v1alpha2" "github.com/deckhouse/virtualization/api/core/v1alpha2/vicondition" ) type VirtualImageWatcher struct { + logger *log.Logger client client.Client } func NewVirtualImageWatcher(client client.Client) *VirtualImageWatcher { return &VirtualImageWatcher{ + logger: log.Default().With("watcher", "vi"), client: client, } } func (w VirtualImageWatcher) Watch(mgr manager.Manager, ctr controller.Controller) error { return ctr.Watch( - source.Kind(mgr.GetCache(), &virtv2.VirtualImage{}), - handler.EnqueueRequestsFromMapFunc(w.enqueueRequests), - predicate.Funcs{ - CreateFunc: func(e event.CreateEvent) bool { - return true - }, - DeleteFunc: func(e event.DeleteEvent) bool { - return true + source.Kind(mgr.GetCache(), &virtv2.VirtualImage{}, + handler.TypedEnqueueRequestsFromMapFunc(w.enqueueRequests), + predicate.TypedFuncs[*virtv2.VirtualImage]{ + UpdateFunc: func(e event.TypedUpdateEvent[*virtv2.VirtualImage]) bool { + if e.ObjectOld.GetGeneration() != e.ObjectNew.GetGeneration() { + return true + } + + oldReadyCondition, _ := conditions.GetCondition(vicondition.ReadyType, e.ObjectOld.Status.Conditions) + newReadyCondition, _ := conditions.GetCondition(vicondition.ReadyType, e.ObjectNew.Status.Conditions) + + if e.ObjectOld.Status.Phase != e.ObjectNew.Status.Phase || oldReadyCondition.Status != newReadyCondition.Status { + return true + } + + return false + }, }, - UpdateFunc: w.filterUpdateEvents, - }, + ), ) } -func (w VirtualImageWatcher) enqueueRequests(ctx context.Context, obj client.Object) (requests []reconcile.Request) { +func (w VirtualImageWatcher) enqueueRequests(ctx context.Context, obj *virtv2.VirtualImage) (requests []reconcile.Request) { var viList virtv2.VirtualImageList err := w.client.List(ctx, &viList, &client.ListOptions{ Namespace: obj.GetNamespace(), }) if err != nil { - logger.FromContext(ctx).Error(fmt.Sprintf("failed to list vi: %s", err)) + w.logger.Error(fmt.Sprintf("failed to list vi: %s", err)) return } @@ -78,7 +88,7 @@ func (w VirtualImageWatcher) enqueueRequests(ctx context.Context, obj client.Obj continue } - if vi.Spec.DataSource.ObjectRef.Kind != virtv2.VirtualImageKind || vi.Spec.DataSource.ObjectRef.Name != obj.GetName() { + if vi.Spec.DataSource.ObjectRef.Kind != virtv2.VirtualImageKind || vi.Spec.DataSource.ObjectRef.Name != vi.GetName() { continue } @@ -90,39 +100,24 @@ func (w VirtualImageWatcher) enqueueRequests(ctx context.Context, obj client.Obj }) } - vi, ok := obj.(*virtv2.VirtualImage) - if ok && vi.Spec.DataSource.Type == virtv2.DataSourceTypeObjectRef { - if vi.Spec.DataSource.ObjectRef != nil && vi.Spec.DataSource.ObjectRef.Kind == virtv2.VirtualImageKind { + if obj.Spec.DataSource.Type == virtv2.DataSourceTypeObjectRef { + if obj.Spec.DataSource.ObjectRef != nil && obj.Spec.DataSource.ObjectRef.Kind == virtv2.VirtualImageKind { // Need to trigger reconcile for update InUse condition. requests = append(requests, reconcile.Request{ NamespacedName: types.NamespacedName{ - Name: vi.Spec.DataSource.ObjectRef.Name, - Namespace: vi.Namespace, + Name: obj.Spec.DataSource.ObjectRef.Name, + Namespace: obj.Namespace, }, }) } } - return -} - -func (w VirtualImageWatcher) filterUpdateEvents(e event.UpdateEvent) bool { - oldVI, ok := e.ObjectOld.(*virtv2.VirtualImage) - if !ok { - return false - } - - newVI, ok := e.ObjectNew.(*virtv2.VirtualImage) - if !ok { - return false - } - - oldReadyCondition, _ := conditions.GetCondition(vicondition.ReadyType, oldVI.Status.Conditions) - newReadyCondition, _ := conditions.GetCondition(vicondition.ReadyType, newVI.Status.Conditions) - - if oldVI.Status.Phase != newVI.Status.Phase || oldReadyCondition.Status != newReadyCondition.Status { - return true - } + requests = append(requests, reconcile.Request{ + NamespacedName: types.NamespacedName{ + Namespace: obj.Namespace, + Name: obj.Name, + }, + }) - return false + return } diff --git a/images/virtualization-artifact/pkg/controller/vi/internal/watcher/virdualdisk_watcher.go b/images/virtualization-artifact/pkg/controller/vi/internal/watcher/virdualdisk_watcher.go index 7d3fad3f8f..fe2a3fc410 100644 --- a/images/virtualization-artifact/pkg/controller/vi/internal/watcher/virdualdisk_watcher.go +++ b/images/virtualization-artifact/pkg/controller/vi/internal/watcher/virdualdisk_watcher.go @@ -19,7 +19,6 @@ package watcher import ( "context" "fmt" - "log/slog" "k8s.io/apimachinery/pkg/types" "sigs.k8s.io/controller-runtime/pkg/client" @@ -31,17 +30,20 @@ import ( "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/conditions" virtv2 "github.com/deckhouse/virtualization/api/core/v1alpha2" "github.com/deckhouse/virtualization/api/core/v1alpha2/vdcondition" ) type VirtualDiskWatcher struct { + logger *log.Logger client client.Client } func NewVirtualDiskWatcher(client client.Client) *VirtualDiskWatcher { return &VirtualDiskWatcher{ + logger: log.Default().With("watcher", "vd"), client: client, } } @@ -55,7 +57,13 @@ func (w *VirtualDiskWatcher) Watch(mgr manager.Manager, ctr controller.Controlle oldInUseCondition, _ := conditions.GetCondition(vdcondition.InUseType, e.ObjectOld.Status.Conditions) newInUseCondition, _ := conditions.GetCondition(vdcondition.InUseType, e.ObjectNew.Status.Conditions) - if e.ObjectOld.Status.Phase != e.ObjectNew.Status.Phase || len(e.ObjectOld.Status.AttachedToVirtualMachines) != len(e.ObjectNew.Status.AttachedToVirtualMachines) || oldInUseCondition != newInUseCondition { + oldReadyCondition, _ := conditions.GetCondition(vdcondition.ReadyType, e.ObjectOld.Status.Conditions) + newReadyCondition, _ := conditions.GetCondition(vdcondition.ReadyType, e.ObjectNew.Status.Conditions) + + if e.ObjectOld.Status.Phase != e.ObjectNew.Status.Phase || + len(e.ObjectOld.Status.AttachedToVirtualMachines) != len(e.ObjectNew.Status.AttachedToVirtualMachines) || + oldInUseCondition != newInUseCondition || + oldReadyCondition.Status != newReadyCondition.Status { return true } @@ -75,7 +83,7 @@ func (w *VirtualDiskWatcher) enqueueRequestsFromVDs(ctx context.Context, vd *vir Namespace: vd.GetNamespace(), }) if err != nil { - slog.Default().Error(fmt.Sprintf("failed to list vi: %s", err)) + w.logger.Error(fmt.Sprintf("failed to list vi: %s", err)) return } diff --git a/images/virtualization-artifact/pkg/controller/vi/internal/watcher/vmbda_watcher.go b/images/virtualization-artifact/pkg/controller/vi/internal/watcher/vmbda_watcher.go index 2d8095be98..009bda6bed 100644 --- a/images/virtualization-artifact/pkg/controller/vi/internal/watcher/vmbda_watcher.go +++ b/images/virtualization-artifact/pkg/controller/vi/internal/watcher/vmbda_watcher.go @@ -18,7 +18,6 @@ package watcher import ( "context" - "fmt" "k8s.io/apimachinery/pkg/types" "sigs.k8s.io/controller-runtime/pkg/client" @@ -48,29 +47,24 @@ func NewVirtualMachineBlockDeviceAttachmentWatcher(client client.Client) *Virtua func (w VirtualMachineBlockDeviceAttachmentWatcher) Watch(mgr manager.Manager, ctr controller.Controller) error { return ctr.Watch( - source.Kind(mgr.GetCache(), &virtv2.VirtualMachineBlockDeviceAttachment{}), - handler.EnqueueRequestsFromMapFunc(w.enqueueRequests), - predicate.Funcs{ - CreateFunc: func(e event.CreateEvent) bool { - return w.isVirtualImageRef(e.Object) + source.Kind(mgr.GetCache(), &virtv2.VirtualMachineBlockDeviceAttachment{}, + handler.TypedEnqueueRequestsFromMapFunc(w.enqueueRequests), + predicate.TypedFuncs[*virtv2.VirtualMachineBlockDeviceAttachment]{ + CreateFunc: func(e event.TypedCreateEvent[*virtv2.VirtualMachineBlockDeviceAttachment]) bool { + return w.isVirtualImageRef(e.Object) + }, + DeleteFunc: func(e event.TypedDeleteEvent[*virtv2.VirtualMachineBlockDeviceAttachment]) bool { + return w.isVirtualImageRef(e.Object) + }, + UpdateFunc: func(e event.TypedUpdateEvent[*virtv2.VirtualMachineBlockDeviceAttachment]) bool { + return w.isVirtualImageRef(e.ObjectOld) || w.isVirtualImageRef(e.ObjectNew) + }, }, - DeleteFunc: func(e event.DeleteEvent) bool { - return w.isVirtualImageRef(e.Object) - }, - UpdateFunc: func(e event.UpdateEvent) bool { - return w.isVirtualImageRef(e.ObjectOld) || w.isVirtualImageRef(e.ObjectNew) - }, - }, + ), ) } -func (w VirtualMachineBlockDeviceAttachmentWatcher) enqueueRequests(ctx context.Context, obj client.Object) (requests []reconcile.Request) { - vmbda, ok := obj.(*virtv2.VirtualMachineBlockDeviceAttachment) - if !ok { - w.logger.Error(fmt.Sprintf("expected a VirtualMachineBlockDeviceAttachment but got a %T", obj)) - return - } - +func (w VirtualMachineBlockDeviceAttachmentWatcher) enqueueRequests(ctx context.Context, vmbda *virtv2.VirtualMachineBlockDeviceAttachment) (requests []reconcile.Request) { requests = append(requests, reconcile.Request{ NamespacedName: types.NamespacedName{ Name: vmbda.Spec.BlockDeviceRef.Name, @@ -81,12 +75,6 @@ func (w VirtualMachineBlockDeviceAttachmentWatcher) enqueueRequests(ctx context. return } -func (w VirtualMachineBlockDeviceAttachmentWatcher) isVirtualImageRef(obj client.Object) bool { - vmbda, ok := obj.(*virtv2.VirtualMachineBlockDeviceAttachment) - if !ok { - w.logger.Error(fmt.Sprintf("expected a VirtualMachineBlockDeviceAttachment but got a %T", obj)) - return false - } - +func (w VirtualMachineBlockDeviceAttachmentWatcher) isVirtualImageRef(vmbda *virtv2.VirtualMachineBlockDeviceAttachment) bool { return vmbda.Spec.BlockDeviceRef.Kind == virtv2.VirtualImageKind } diff --git a/images/virtualization-artifact/pkg/controller/vi/vi_reconciler.go b/images/virtualization-artifact/pkg/controller/vi/vi_reconciler.go index d09976d84f..438ace9734 100644 --- a/images/virtualization-artifact/pkg/controller/vi/vi_reconciler.go +++ b/images/virtualization-artifact/pkg/controller/vi/vi_reconciler.go @@ -23,12 +23,8 @@ import ( "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/virtualization-controller/pkg/controller/reconciler" "github.com/deckhouse/virtualization-controller/pkg/controller/vi/internal/watcher" @@ -83,19 +79,6 @@ func (r *Reconciler) Reconcile(ctx context.Context, req reconcile.Request) (reco } func (r *Reconciler) SetupController(_ context.Context, mgr manager.Manager, ctr controller.Controller) error { - if err := ctr.Watch( - source.Kind(mgr.GetCache(), &virtv2.VirtualImage{}, - &handler.TypedEnqueueRequestForObject[*virtv2.VirtualImage]{}, - predicate.TypedFuncs[*virtv2.VirtualImage]{ - UpdateFunc: func(e event.TypedUpdateEvent[*virtv2.VirtualImage]) bool { - return e.ObjectOld.GetGeneration() != e.ObjectNew.GetGeneration() - }, - }, - ), - ); err != nil { - return fmt.Errorf("error setting watch on VirtualImage: %w", err) - } - viFromVIEnqueuer := watchers.NewVirtualImageRequestEnqueuer(mgr.GetClient(), &virtv2.VirtualImage{}, virtv2.VirtualImageObjectRefKindVirtualImage) viWatcher := watchers.NewObjectRefWatcher(watchers.NewVirtualImageFilter(), viFromVIEnqueuer) if err := viWatcher.Run(mgr, ctr); err != nil { From 27d3eeb31416f13750337cbdd5176ae4b87d2d70 Mon Sep 17 00:00:00 2001 From: Valeriy Khorunzhin Date: Mon, 25 Aug 2025 22:26:15 +0300 Subject: [PATCH 09/11] fix vi vd watcher Signed-off-by: Valeriy Khorunzhin --- .../vi/internal/watcher/virdualdisk_watcher.go | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/images/virtualization-artifact/pkg/controller/vi/internal/watcher/virdualdisk_watcher.go b/images/virtualization-artifact/pkg/controller/vi/internal/watcher/virdualdisk_watcher.go index fe2a3fc410..e398b68576 100644 --- a/images/virtualization-artifact/pkg/controller/vi/internal/watcher/virdualdisk_watcher.go +++ b/images/virtualization-artifact/pkg/controller/vi/internal/watcher/virdualdisk_watcher.go @@ -104,5 +104,17 @@ func (w *VirtualDiskWatcher) enqueueRequestsFromVDs(ctx context.Context, vd *vir }) } + if vd.Spec.DataSource != nil && vd.Spec.DataSource.Type == virtv2.DataSourceTypeObjectRef { + if vd.Spec.DataSource.ObjectRef != nil && vd.Spec.DataSource.ObjectRef.Kind == virtv2.VirtualImageKind { + // Need to trigger reconcile for update InUse condition. + requests = append(requests, reconcile.Request{ + NamespacedName: types.NamespacedName{ + Name: vd.Spec.DataSource.ObjectRef.Name, + Namespace: vd.Namespace, + }, + }) + } + } + return } From 650f3446d3f8569096d135c363da4724587979a3 Mon Sep 17 00:00:00 2001 From: Valeriy Khorunzhin Date: Mon, 25 Aug 2025 22:57:53 +0300 Subject: [PATCH 10/11] fix test Signed-off-by: Valeriy Khorunzhin --- .../pkg/controller/vd/internal/life_cycle_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/images/virtualization-artifact/pkg/controller/vd/internal/life_cycle_test.go b/images/virtualization-artifact/pkg/controller/vd/internal/life_cycle_test.go index 5ec8892edd..4b79204648 100644 --- a/images/virtualization-artifact/pkg/controller/vd/internal/life_cycle_test.go +++ b/images/virtualization-artifact/pkg/controller/vd/internal/life_cycle_test.go @@ -267,7 +267,7 @@ var _ = Describe("LifeCycleHandler Run", func() { return reconcile.Result{}, nil }}, true } - handler := NewLifeCycleHandler(recorder, nil, &sourcesMock, nil) + handler := NewLifeCycleHandler(recorder, nil, &sourcesMock, nil, nil) _, _ = handler.Handle(ctx, &vd) readyCond, _ := conditions.GetCondition(vdcondition.ReadyType, vd.Status.Conditions) Expect(readyCond.Reason).Should(Equal(expectedReadyReason)) From 4f1a2e59869c8c49c3d2ca58e138c93bd5288670 Mon Sep 17 00:00:00 2001 From: Valeriy Khorunzhin Date: Thu, 4 Sep 2025 11:18:33 +0300 Subject: [PATCH 11/11] fix builders Signed-off-by: Valeriy Khorunzhin --- .../pkg/builder/cvi/option.go | 20 ++++++++++++++++--- .../pkg/builder/vd/option.go | 14 +++++++++++++ .../pkg/builder/vi/option.go | 14 +++++++++++++ 3 files changed, 45 insertions(+), 3 deletions(-) diff --git a/images/virtualization-artifact/pkg/builder/cvi/option.go b/images/virtualization-artifact/pkg/builder/cvi/option.go index f4c5f2ce1c..366f33f6ff 100644 --- a/images/virtualization-artifact/pkg/builder/cvi/option.go +++ b/images/virtualization-artifact/pkg/builder/cvi/option.go @@ -14,6 +14,8 @@ limitations under the License. package cvi import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "github.com/deckhouse/virtualization-controller/pkg/builder/meta" "github.com/deckhouse/virtualization/api/core/v1alpha2" ) @@ -34,8 +36,20 @@ func WithPhase(phase v1alpha2.ImagePhase) func(vi *v1alpha2.ClusterVirtualImage) } } -func WithCDROM(cdrom bool) func(vi *v1alpha2.ClusterVirtualImage) { - return func(vi *v1alpha2.ClusterVirtualImage) { - vi.Status.CDROM = cdrom +func WithCDROM(cdrom bool) func(cvi *v1alpha2.ClusterVirtualImage) { + return func(cvi *v1alpha2.ClusterVirtualImage) { + cvi.Status.CDROM = cdrom + } +} + +func WithDatasource(datasource v1alpha2.ClusterVirtualImageDataSource) func(cvi *v1alpha2.ClusterVirtualImage) { + return func(cvi *v1alpha2.ClusterVirtualImage) { + cvi.Spec.DataSource = datasource + } +} + +func WithCondition(condition metav1.Condition) func(cvi *v1alpha2.ClusterVirtualImage) { + return func(cvi *v1alpha2.ClusterVirtualImage) { + cvi.Status.Conditions = append(cvi.Status.Conditions, condition) } } diff --git a/images/virtualization-artifact/pkg/builder/vd/option.go b/images/virtualization-artifact/pkg/builder/vd/option.go index e3c58b28e4..f2d257f4a0 100644 --- a/images/virtualization-artifact/pkg/builder/vd/option.go +++ b/images/virtualization-artifact/pkg/builder/vd/option.go @@ -14,6 +14,8 @@ limitations under the License. package vd import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "github.com/deckhouse/virtualization-controller/pkg/builder/meta" "github.com/deckhouse/virtualization/api/core/v1alpha2" ) @@ -28,3 +30,15 @@ var ( WithAnnotation = meta.WithAnnotation[*v1alpha2.VirtualDisk] WithAnnotations = meta.WithAnnotations[*v1alpha2.VirtualDisk] ) + +func WithDatasource(datasource *v1alpha2.VirtualDiskDataSource) func(vd *v1alpha2.VirtualDisk) { + return func(vd *v1alpha2.VirtualDisk) { + vd.Spec.DataSource = datasource + } +} + +func WithCondition(condition metav1.Condition) func(vi *v1alpha2.VirtualDisk) { + return func(vd *v1alpha2.VirtualDisk) { + vd.Status.Conditions = append(vd.Status.Conditions, condition) + } +} diff --git a/images/virtualization-artifact/pkg/builder/vi/option.go b/images/virtualization-artifact/pkg/builder/vi/option.go index 26c0e8d1de..38189ae952 100644 --- a/images/virtualization-artifact/pkg/builder/vi/option.go +++ b/images/virtualization-artifact/pkg/builder/vi/option.go @@ -14,6 +14,8 @@ limitations under the License. package vi import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "github.com/deckhouse/virtualization-controller/pkg/builder/meta" "github.com/deckhouse/virtualization/api/core/v1alpha2" ) @@ -40,3 +42,15 @@ func WithCDROM(cdrom bool) func(vi *v1alpha2.VirtualImage) { vi.Status.CDROM = cdrom } } + +func WithDatasource(datasource v1alpha2.VirtualImageDataSource) func(vi *v1alpha2.VirtualImage) { + return func(vi *v1alpha2.VirtualImage) { + vi.Spec.DataSource = datasource + } +} + +func WithCondition(condition metav1.Condition) func(vi *v1alpha2.VirtualImage) { + return func(vi *v1alpha2.VirtualImage) { + vi.Status.Conditions = append(vi.Status.Conditions, condition) + } +}