diff --git a/images/virtualization-artifact/pkg/common/imageformat/format.go b/images/virtualization-artifact/pkg/common/imageformat/format.go index 708bd2ee93..dc8001ea35 100644 --- a/images/virtualization-artifact/pkg/common/imageformat/format.go +++ b/images/virtualization-artifact/pkg/common/imageformat/format.go @@ -20,7 +20,7 @@ import "strings" const ( FormatISO = "iso" - FormatRAW = "raw" + FormatRaw = "raw" ) func IsISO(format string) bool { diff --git a/images/virtualization-artifact/pkg/controller/conditions/getter.go b/images/virtualization-artifact/pkg/controller/conditions/getter.go index 9d6424298c..8361ee39d7 100644 --- a/images/virtualization-artifact/pkg/controller/conditions/getter.go +++ b/images/virtualization-artifact/pkg/controller/conditions/getter.go @@ -19,6 +19,7 @@ package conditions import ( corev1 "k8s.io/api/core/v1" virtv1 "kubevirt.io/api/core/v1" + cdiv1 "kubevirt.io/containerized-data-importer-api/pkg/apis/core/v1beta1" ) func GetPodCondition(condType corev1.PodConditionType, conds []corev1.PodCondition) (corev1.PodCondition, bool) { @@ -31,6 +32,22 @@ func GetPodCondition(condType corev1.PodConditionType, conds []corev1.PodConditi return corev1.PodCondition{}, false } +const ( + DVRunningConditionType cdiv1.DataVolumeConditionType = "Running" + DVQoutaNotExceededConditionType cdiv1.DataVolumeConditionType = "QuotaNotExceeded" + DVImagePullFailedReason string = "ImagePullFailed" +) + +func GetDataVolumeCondition(condType cdiv1.DataVolumeConditionType, conds []cdiv1.DataVolumeCondition) (cdiv1.DataVolumeCondition, bool) { + for _, cond := range conds { + if cond.Type == condType { + return cond, true + } + } + + return cdiv1.DataVolumeCondition{}, false +} + func GetKVVMICondition(condType virtv1.VirtualMachineInstanceConditionType, conds []virtv1.VirtualMachineInstanceCondition) (virtv1.VirtualMachineInstanceCondition, bool) { for _, cond := range conds { if cond.Type == condType { diff --git a/images/virtualization-artifact/pkg/controller/service/condition.go b/images/virtualization-artifact/pkg/controller/service/condition.go index 6cc581e545..01cbbd1910 100644 --- a/images/virtualization-artifact/pkg/controller/service/condition.go +++ b/images/virtualization-artifact/pkg/controller/service/condition.go @@ -36,6 +36,7 @@ func CapitalizeFirstLetter(s string) string { return string(runes) } +// Deprecated. Use conditions.GetDataVolumeCondition instead. func GetDataVolumeCondition(conditionType cdiv1.DataVolumeConditionType, conditions []cdiv1.DataVolumeCondition) *cdiv1.DataVolumeCondition { for i, condition := range conditions { if condition.Type == conditionType { diff --git a/images/virtualization-artifact/pkg/controller/vi/internal/source/interfaces.go b/images/virtualization-artifact/pkg/controller/vi/internal/source/interfaces.go index 759ef6dac6..a791c04bc5 100644 --- a/images/virtualization-artifact/pkg/controller/vi/internal/source/interfaces.go +++ b/images/virtualization-artifact/pkg/controller/vi/internal/source/interfaces.go @@ -74,5 +74,7 @@ type Bounder interface { } type Disk interface { + step.CreateDataVolumeStepDisk + step.WaitForDVStepDisk CleanUpSupplements(ctx context.Context, sup *supplements.Generator) (bool, error) } diff --git a/images/virtualization-artifact/pkg/controller/vi/internal/source/mock.go b/images/virtualization-artifact/pkg/controller/vi/internal/source/mock.go index a087c8c437..ac009124eb 100644 --- a/images/virtualization-artifact/pkg/controller/vi/internal/source/mock.go +++ b/images/virtualization-artifact/pkg/controller/vi/internal/source/mock.go @@ -13,8 +13,11 @@ import ( virtv2 "github.com/deckhouse/virtualization/api/core/v1alpha2" corev1 "k8s.io/api/core/v1" netv1 "k8s.io/api/networking/v1" + storagev1 "k8s.io/api/storage/v1" + "k8s.io/apimachinery/pkg/api/resource" 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/reconcile" "sync" ) @@ -1944,6 +1947,15 @@ var _ Disk = &DiskMock{} // CleanUpSupplementsFunc: func(ctx context.Context, sup *supplements.Generator) (bool, error) { // panic("mock out the CleanUpSupplements method") // }, +// GetProgressFunc: func(dv *cdiv1.DataVolume, prevProgress string, opts ...service.GetProgressOption) string { +// panic("mock out the GetProgress method") +// }, +// GetStorageClassFunc: func(ctx context.Context, scName string) (*storagev1.StorageClass, error) { +// panic("mock out the GetStorageClass method") +// }, +// StartImmediateFunc: func(ctx context.Context, size resource.Quantity, sc *storagev1.StorageClass, source *cdiv1.DataVolumeSource, obj service.ObjectKind, sup *supplements.Generator) error { +// panic("mock out the StartImmediate method") +// }, // } // // // use mockedDisk in code that requires Disk @@ -1954,6 +1966,15 @@ type DiskMock struct { // CleanUpSupplementsFunc mocks the CleanUpSupplements method. CleanUpSupplementsFunc func(ctx context.Context, sup *supplements.Generator) (bool, error) + // GetProgressFunc mocks the GetProgress method. + GetProgressFunc func(dv *cdiv1.DataVolume, prevProgress string, opts ...service.GetProgressOption) string + + // GetStorageClassFunc mocks the GetStorageClass method. + GetStorageClassFunc func(ctx context.Context, scName string) (*storagev1.StorageClass, error) + + // StartImmediateFunc mocks the StartImmediate method. + StartImmediateFunc func(ctx context.Context, size resource.Quantity, sc *storagev1.StorageClass, source *cdiv1.DataVolumeSource, obj service.ObjectKind, sup *supplements.Generator) error + // calls tracks calls to the methods. calls struct { // CleanUpSupplements holds details about calls to the CleanUpSupplements method. @@ -1963,8 +1984,42 @@ type DiskMock struct { // Sup is the sup argument value. Sup *supplements.Generator } + // GetProgress holds details about calls to the GetProgress method. + GetProgress []struct { + // Dv is the dv argument value. + Dv *cdiv1.DataVolume + // PrevProgress is the prevProgress argument value. + PrevProgress string + // Opts is the opts argument value. + Opts []service.GetProgressOption + } + // GetStorageClass holds details about calls to the GetStorageClass method. + GetStorageClass []struct { + // Ctx is the ctx argument value. + Ctx context.Context + // ScName is the scName argument value. + ScName string + } + // StartImmediate holds details about calls to the StartImmediate method. + StartImmediate []struct { + // Ctx is the ctx argument value. + Ctx context.Context + // Size is the size argument value. + Size resource.Quantity + // Sc is the sc argument value. + Sc *storagev1.StorageClass + // Source is the source argument value. + Source *cdiv1.DataVolumeSource + // Obj is the obj argument value. + Obj service.ObjectKind + // Sup is the sup argument value. + Sup *supplements.Generator + } } lockCleanUpSupplements sync.RWMutex + lockGetProgress sync.RWMutex + lockGetStorageClass sync.RWMutex + lockStartImmediate sync.RWMutex } // CleanUpSupplements calls CleanUpSupplementsFunc. @@ -2002,3 +2057,131 @@ func (mock *DiskMock) CleanUpSupplementsCalls() []struct { mock.lockCleanUpSupplements.RUnlock() return calls } + +// GetProgress calls GetProgressFunc. +func (mock *DiskMock) GetProgress(dv *cdiv1.DataVolume, prevProgress string, opts ...service.GetProgressOption) string { + if mock.GetProgressFunc == nil { + panic("DiskMock.GetProgressFunc: method is nil but Disk.GetProgress was just called") + } + callInfo := struct { + Dv *cdiv1.DataVolume + PrevProgress string + Opts []service.GetProgressOption + }{ + Dv: dv, + PrevProgress: prevProgress, + Opts: opts, + } + mock.lockGetProgress.Lock() + mock.calls.GetProgress = append(mock.calls.GetProgress, callInfo) + mock.lockGetProgress.Unlock() + return mock.GetProgressFunc(dv, prevProgress, opts...) +} + +// GetProgressCalls gets all the calls that were made to GetProgress. +// Check the length with: +// +// len(mockedDisk.GetProgressCalls()) +func (mock *DiskMock) GetProgressCalls() []struct { + Dv *cdiv1.DataVolume + PrevProgress string + Opts []service.GetProgressOption +} { + var calls []struct { + Dv *cdiv1.DataVolume + PrevProgress string + Opts []service.GetProgressOption + } + mock.lockGetProgress.RLock() + calls = mock.calls.GetProgress + mock.lockGetProgress.RUnlock() + return calls +} + +// GetStorageClass calls GetStorageClassFunc. +func (mock *DiskMock) GetStorageClass(ctx context.Context, scName string) (*storagev1.StorageClass, error) { + if mock.GetStorageClassFunc == nil { + panic("DiskMock.GetStorageClassFunc: method is nil but Disk.GetStorageClass was just called") + } + callInfo := struct { + Ctx context.Context + ScName string + }{ + Ctx: ctx, + ScName: scName, + } + mock.lockGetStorageClass.Lock() + mock.calls.GetStorageClass = append(mock.calls.GetStorageClass, callInfo) + mock.lockGetStorageClass.Unlock() + return mock.GetStorageClassFunc(ctx, scName) +} + +// GetStorageClassCalls gets all the calls that were made to GetStorageClass. +// Check the length with: +// +// len(mockedDisk.GetStorageClassCalls()) +func (mock *DiskMock) GetStorageClassCalls() []struct { + Ctx context.Context + ScName string +} { + var calls []struct { + Ctx context.Context + ScName string + } + mock.lockGetStorageClass.RLock() + calls = mock.calls.GetStorageClass + mock.lockGetStorageClass.RUnlock() + return calls +} + +// StartImmediate calls StartImmediateFunc. +func (mock *DiskMock) StartImmediate(ctx context.Context, size resource.Quantity, sc *storagev1.StorageClass, source *cdiv1.DataVolumeSource, obj service.ObjectKind, sup *supplements.Generator) error { + if mock.StartImmediateFunc == nil { + panic("DiskMock.StartImmediateFunc: method is nil but Disk.StartImmediate was just called") + } + callInfo := struct { + Ctx context.Context + Size resource.Quantity + Sc *storagev1.StorageClass + Source *cdiv1.DataVolumeSource + Obj service.ObjectKind + Sup *supplements.Generator + }{ + Ctx: ctx, + Size: size, + Sc: sc, + Source: source, + Obj: obj, + Sup: sup, + } + mock.lockStartImmediate.Lock() + mock.calls.StartImmediate = append(mock.calls.StartImmediate, callInfo) + mock.lockStartImmediate.Unlock() + return mock.StartImmediateFunc(ctx, size, sc, source, obj, sup) +} + +// StartImmediateCalls gets all the calls that were made to StartImmediate. +// Check the length with: +// +// len(mockedDisk.StartImmediateCalls()) +func (mock *DiskMock) StartImmediateCalls() []struct { + Ctx context.Context + Size resource.Quantity + Sc *storagev1.StorageClass + Source *cdiv1.DataVolumeSource + Obj service.ObjectKind + Sup *supplements.Generator +} { + var calls []struct { + Ctx context.Context + Size resource.Quantity + Sc *storagev1.StorageClass + Source *cdiv1.DataVolumeSource + Obj service.ObjectKind + Sup *supplements.Generator + } + mock.lockStartImmediate.RLock() + calls = mock.calls.StartImmediate + mock.lockStartImmediate.RUnlock() + return calls +} 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 00feaafc55..0b1f136ac8 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 @@ -60,7 +60,8 @@ type ObjectRefDataSource struct { recorder eventrecord.EventRecorderLogger viObjectRefOnPvc *ObjectRefDataVirtualImageOnPVC - vdSyncer *ObjectRefVirtualDisk + vdCRSyncer *ObjectRefVirtualDiskCR + vdPVCSyncer *ObjectRefVirtualDiskPVC vdSnapshotCRSyncer *ObjectRefVirtualDiskSnapshotCR vdSnapshotPVCSyncer *ObjectRefVirtualDiskSnapshotPVC } @@ -83,15 +84,19 @@ func NewObjectRefDataSource( diskService: diskService, recorder: recorder, viObjectRefOnPvc: NewObjectRefDataVirtualImageOnPVC(recorder, statService, importerService, dvcrSettings, client, diskService), - vdSyncer: NewObjectRefVirtualDisk(recorder, importerService, client, diskService, dvcrSettings, statService), + vdCRSyncer: NewObjectRefVirtualDiskCR(client, importerService, diskService, statService, dvcrSettings, recorder), + vdPVCSyncer: NewObjectRefVirtualDiskPVC(bounderService, client, diskService, recorder), vdSnapshotCRSyncer: NewObjectRefVirtualDiskSnapshotCR(importerService, statService, diskService, client, dvcrSettings, recorder), - vdSnapshotPVCSyncer: NewObjectRefVirtualDiskSnapshotPVC(importerService, statService, bounderService, client, dvcrSettings, recorder), + vdSnapshotPVCSyncer: NewObjectRefVirtualDiskSnapshotPVC(importerService, statService, bounderService, client, recorder), } } func (ds ObjectRefDataSource) StoreToPVC(ctx context.Context, vi *virtv2.VirtualImage) (reconcile.Result, error) { - if vi.Spec.DataSource.ObjectRef.Kind == virtv2.VirtualDiskSnapshotKind { + switch vi.Spec.DataSource.ObjectRef.Kind { + case virtv2.VirtualDiskSnapshotKind: return ds.vdSnapshotPVCSyncer.Sync(ctx, vi) + case virtv2.VirtualDiskKind: + return ds.vdPVCSyncer.Sync(ctx, vi) } log, ctx := logger.GetDataSourceContext(ctx, objectRefDataSource) @@ -115,18 +120,6 @@ func (ds ObjectRefDataSource) StoreToPVC(ctx context.Context, vi *virtv2.Virtual if viRef.Spec.Storage == virtv2.StorageKubernetes || viRef.Spec.Storage == virtv2.StoragePersistentVolumeClaim { return ds.viObjectRefOnPvc.StoreToPVC(ctx, vi, viRef, cb) } - case virtv2.VirtualDiskKind: - vdKey := types.NamespacedName{Name: vi.Spec.DataSource.ObjectRef.Name, Namespace: vi.Namespace} - vd, err := object.FetchObject(ctx, vdKey, ds.client, &virtv2.VirtualDisk{}) - if err != nil { - return reconcile.Result{}, fmt.Errorf("unable to get VD %s: %w", vdKey, err) - } - - if vd == nil { - return reconcile.Result{}, fmt.Errorf("VD object ref %s is nil", vdKey) - } - - return ds.vdSyncer.StoreToPVC(ctx, vi, vd, cb) } supgen := supplements.NewGenerator(annotations.VIShortName, vi.Name, vi.Namespace, vi.UID) @@ -297,8 +290,11 @@ func (ds ObjectRefDataSource) StoreToPVC(ctx context.Context, vi *virtv2.Virtual } func (ds ObjectRefDataSource) StoreToDVCR(ctx context.Context, vi *virtv2.VirtualImage) (reconcile.Result, error) { - if vi.Spec.DataSource.ObjectRef.Kind == virtv2.VirtualDiskSnapshotKind { + switch vi.Spec.DataSource.ObjectRef.Kind { + case virtv2.VirtualDiskSnapshotKind: return ds.vdSnapshotCRSyncer.Sync(ctx, vi) + case virtv2.VirtualDiskKind: + return ds.vdCRSyncer.Sync(ctx, vi) } log, ctx := logger.GetDataSourceContext(ctx, "objectref") @@ -322,18 +318,6 @@ func (ds ObjectRefDataSource) StoreToDVCR(ctx context.Context, vi *virtv2.Virtua if viRef.Spec.Storage == virtv2.StorageKubernetes || viRef.Spec.Storage == virtv2.StoragePersistentVolumeClaim { return ds.viObjectRefOnPvc.StoreToDVCR(ctx, vi, viRef, cb) } - case virtv2.VirtualDiskKind: - viKey := types.NamespacedName{Name: vi.Spec.DataSource.ObjectRef.Name, Namespace: vi.Namespace} - vd, err := object.FetchObject(ctx, viKey, ds.client, &virtv2.VirtualDisk{}) - if err != nil { - return reconcile.Result{}, fmt.Errorf("unable to get VD %s: %w", viKey, err) - } - - if vd == nil { - return reconcile.Result{}, fmt.Errorf("VD object ref %s is nil", viKey) - } - - return ds.vdSyncer.StoreToDVCR(ctx, vi, vd, cb) } supgen := supplements.NewGenerator(annotations.VIShortName, vi.Name, vi.Namespace, vi.UID) @@ -532,16 +516,23 @@ func (ds ObjectRefDataSource) Validate(ctx context.Context, vi *virtv2.VirtualIm return NewClusterImageNotReadyError(vi.Spec.DataSource.ObjectRef.Name) case virtv2.VirtualImageObjectRefKindVirtualDisk: - return ds.vdSyncer.Validate(ctx, vi) + switch vi.Spec.Storage { + case virtv2.StorageKubernetes, virtv2.StoragePersistentVolumeClaim: + return ds.vdPVCSyncer.Validate(ctx, vi) + case virtv2.StorageContainerRegistry: + return ds.vdCRSyncer.Validate(ctx, vi) + default: + return fmt.Errorf("unexpected storage: %s", vi.Spec.Storage) + } case virtv2.VirtualImageObjectRefKindVirtualDiskSnapshot: switch vi.Spec.Storage { case virtv2.StorageKubernetes, virtv2.StoragePersistentVolumeClaim: return ds.vdSnapshotPVCSyncer.Validate(ctx, vi) case virtv2.StorageContainerRegistry: return ds.vdSnapshotCRSyncer.Validate(ctx, vi) + default: + return fmt.Errorf("unexpected storage: %s", vi.Spec.Storage) } - - return fmt.Errorf("unexpected object ref kind: %s", vi.Spec.DataSource.ObjectRef.Kind) default: return fmt.Errorf("unexpected object ref kind: %s", vi.Spec.DataSource.ObjectRef.Kind) } 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 deleted file mode 100644 index ecf7bc674e..0000000000 --- a/images/virtualization-artifact/pkg/controller/vi/internal/source/object_ref_vd.go +++ /dev/null @@ -1,449 +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 source - -import ( - "context" - "errors" - "fmt" - "strconv" - - corev1 "k8s.io/api/core/v1" - storagev1 "k8s.io/api/storage/v1" - "k8s.io/apimachinery/pkg/api/resource" - 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" - "sigs.k8s.io/controller-runtime/pkg/reconcile" - - "github.com/deckhouse/virtualization-controller/pkg/common" - "github.com/deckhouse/virtualization-controller/pkg/common/annotations" - "github.com/deckhouse/virtualization-controller/pkg/common/datasource" - "github.com/deckhouse/virtualization-controller/pkg/common/imageformat" - "github.com/deckhouse/virtualization-controller/pkg/common/object" - podutil "github.com/deckhouse/virtualization-controller/pkg/common/pod" - "github.com/deckhouse/virtualization-controller/pkg/common/pointer" - "github.com/deckhouse/virtualization-controller/pkg/controller/conditions" - "github.com/deckhouse/virtualization-controller/pkg/controller/importer" - "github.com/deckhouse/virtualization-controller/pkg/controller/service" - "github.com/deckhouse/virtualization-controller/pkg/controller/supplements" - "github.com/deckhouse/virtualization-controller/pkg/dvcr" - "github.com/deckhouse/virtualization-controller/pkg/eventrecord" - "github.com/deckhouse/virtualization-controller/pkg/logger" - 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 ObjectRefVirtualDisk struct { - importerService Importer - diskService *service.DiskService - statService Stat - dvcrSettings *dvcr.Settings - client client.Client - recorder eventrecord.EventRecorderLogger -} - -func NewObjectRefVirtualDisk( - recorder eventrecord.EventRecorderLogger, - importerService Importer, - client client.Client, - diskService *service.DiskService, - dvcrSettings *dvcr.Settings, - statService Stat, -) *ObjectRefVirtualDisk { - return &ObjectRefVirtualDisk{ - importerService: importerService, - client: client, - recorder: recorder, - diskService: diskService, - statService: statService, - dvcrSettings: dvcrSettings, - } -} - -func (ds ObjectRefVirtualDisk) StoreToDVCR(ctx context.Context, vi *virtv2.VirtualImage, vdRef *virtv2.VirtualDisk, cb *conditions.ConditionBuilder) (reconcile.Result, error) { - log, ctx := logger.GetDataSourceContext(ctx, "objectref") - - supgen := supplements.NewGenerator(annotations.VIShortName, vi.Name, vi.Namespace, vi.UID) - pod, err := ds.importerService.GetPod(ctx, supgen) - if err != nil { - return reconcile.Result{}, err - } - - condition, _ := conditions.GetCondition(vicondition.ReadyType, vi.Status.Conditions) - switch { - case IsImageProvisioningFinished(condition): - log.Info("Virtual image provisioning finished: clean up") - - cb. - Status(metav1.ConditionTrue). - Reason(vicondition.Ready). - Message("") - - vi.Status.Phase = virtv2.ImageReady - - err = ds.importerService.Unprotect(ctx, pod) - if err != nil { - return reconcile.Result{}, err - } - - return CleanUpSupplements(ctx, vi, ds) - case object.IsTerminating(pod): - vi.Status.Phase = virtv2.ImagePending - - log.Info("Cleaning up...") - case pod == nil: - vi.Status.Progress = ds.statService.GetProgress(vi.GetUID(), pod, vi.Status.Progress) - vi.Status.Target.RegistryURL = ds.statService.GetDVCRImageName(pod) - - envSettings := ds.getEnvSettings(vi, supgen) - - ownerRef := metav1.NewControllerRef(vi, vi.GroupVersionKind()) - podSettings := ds.importerService.GetPodSettingsWithPVC(ownerRef, supgen, vdRef.Status.Target.PersistentVolumeClaim, vdRef.Namespace) - err = ds.importerService.StartWithPodSetting(ctx, envSettings, supgen, datasource.NewCABundleForVMI(vi.GetNamespace(), vi.Spec.DataSource), podSettings) - switch { - case err == nil: - // OK. - case common.ErrQuotaExceeded(err): - ds.recorder.Event(vi, corev1.EventTypeWarning, virtv2.ReasonDataSourceQuotaExceeded, "DataSource quota exceed") - return setQuotaExceededPhaseCondition(cb, &vi.Status.Phase, err, vi.CreationTimestamp), nil - default: - setPhaseConditionToFailed(cb, &vi.Status.Phase, fmt.Errorf("unexpected error: %w", err)) - return reconcile.Result{}, err - } - - vi.Status.Phase = virtv2.ImageProvisioning - cb. - Status(metav1.ConditionFalse). - Reason(vicondition.Provisioning). - Message("DVCR Provisioner not found: create the new one.") - - log.Info("Create importer pod...", "progress", vi.Status.Progress, "pod.phase", "nil") - - return reconcile.Result{Requeue: true}, nil - case podutil.IsPodComplete(pod): - err = ds.statService.CheckPod(pod) - if err != nil { - vi.Status.Phase = virtv2.ImageFailed - - switch { - case errors.Is(err, service.ErrProvisioningFailed): - ds.recorder.Event(vi, corev1.EventTypeWarning, virtv2.ReasonDataSourceDiskProvisioningFailed, "Disk provisioning failed") - cb. - Status(metav1.ConditionFalse). - Reason(vicondition.ProvisioningFailed). - Message(service.CapitalizeFirstLetter(err.Error() + ".")) - return reconcile.Result{}, nil - default: - return reconcile.Result{}, err - } - } - - cb. - Status(metav1.ConditionTrue). - Reason(vicondition.Ready). - Message("") - - vi.Status.Phase = virtv2.ImageReady - vi.Status.Size = ds.statService.GetSize(pod) - vi.Status.CDROM = ds.statService.GetCDROM(pod) - vi.Status.Format = ds.statService.GetFormat(pod) - vi.Status.Progress = "100%" - vi.Status.Target.RegistryURL = ds.statService.GetDVCRImageName(pod) - - log.Info("Ready", "progress", vi.Status.Progress, "pod.phase", pod.Status.Phase) - default: - err = ds.statService.CheckPod(pod) - if err != nil { - vi.Status.Phase = virtv2.ImageFailed - - switch { - case errors.Is(err, service.ErrNotInitialized), errors.Is(err, service.ErrNotScheduled): - cb. - Status(metav1.ConditionFalse). - Reason(vicondition.ProvisioningNotStarted). - Message(service.CapitalizeFirstLetter(err.Error() + ".")) - return reconcile.Result{}, nil - case errors.Is(err, service.ErrProvisioningFailed): - ds.recorder.Event(vi, corev1.EventTypeWarning, virtv2.ReasonDataSourceDiskProvisioningFailed, "Disk provisioning failed") - cb. - Status(metav1.ConditionFalse). - Reason(vicondition.ProvisioningFailed). - Message(service.CapitalizeFirstLetter(err.Error() + ".")) - return reconcile.Result{}, nil - default: - return reconcile.Result{}, err - } - } - - err = ds.importerService.Protect(ctx, pod) - if err != nil { - return reconcile.Result{}, err - } - - cb. - Status(metav1.ConditionFalse). - Reason(vicondition.Provisioning). - Message("Import is in the process of provisioning to DVCR.") - - vi.Status.Phase = virtv2.ImageProvisioning - vi.Status.Progress = ds.statService.GetProgress(vi.GetUID(), pod, vi.Status.Progress) - vi.Status.Target.RegistryURL = ds.statService.GetDVCRImageName(pod) - - log.Info("Provisioning...", "progress", vi.Status.Progress, "pod.phase", pod.Status.Phase) - } - - return reconcile.Result{Requeue: true}, nil -} - -func (ds ObjectRefVirtualDisk) StoreToPVC(ctx context.Context, vi *virtv2.VirtualImage, vdRef *virtv2.VirtualDisk, cb *conditions.ConditionBuilder) (reconcile.Result, error) { - log, ctx := logger.GetDataSourceContext(ctx, objectRefDataSource) - - supgen := supplements.NewGenerator(annotations.VIShortName, vi.Name, vi.Namespace, vi.UID) - dv, err := ds.diskService.GetDataVolume(ctx, supgen) - if err != nil { - return reconcile.Result{}, err - } - - pvc, err := ds.diskService.GetPersistentVolumeClaim(ctx, supgen) - if err != nil { - return reconcile.Result{}, err - } - - var dvQuotaNotExceededCondition *cdiv1.DataVolumeCondition - var dvRunningCondition *cdiv1.DataVolumeCondition - if dv != nil { - dvQuotaNotExceededCondition = service.GetDataVolumeCondition(DVQoutaNotExceededConditionType, dv.Status.Conditions) - dvRunningCondition = service.GetDataVolumeCondition(DVRunningConditionType, dv.Status.Conditions) - } - - condition, _ := conditions.GetCondition(vicondition.ReadyType, vi.Status.Conditions) - switch { - case IsImageProvisioningFinished(condition): - log.Info("Disk provisioning finished: clean up") - - setPhaseConditionForFinishedImage(pvc, cb, &vi.Status.Phase, supgen) - - // Protect Ready Disk and underlying PVC. - err = ds.diskService.Protect(ctx, vi, nil, pvc) - if err != nil { - return reconcile.Result{}, err - } - - err = ds.diskService.Unprotect(ctx, dv) - if err != nil { - return reconcile.Result{}, err - } - - return CleanUpSupplements(ctx, vi, ds) - case object.AnyTerminating(dv, pvc): - log.Info("Waiting for supplements to be terminated") - case dv == nil: - ds.recorder.Event( - vi, - corev1.EventTypeNormal, - virtv2.ReasonDataSourceSyncStarted, - "The ObjectRef DataSource import has started", - ) - - vi.Status.Progress = "0%" - vi.Status.SourceUID = pointer.GetPointer(vdRef.GetUID()) - - source := &cdiv1.DataVolumeSource{ - PVC: &cdiv1.DataVolumeSourcePVC{ - Name: vdRef.Status.Target.PersistentVolumeClaim, - Namespace: vdRef.Namespace, - }, - } - - var size resource.Quantity - size, err = resource.ParseQuantity(vdRef.Status.Capacity) - if err != nil { - return reconcile.Result{}, err - } - - var sc *storagev1.StorageClass - sc, err = ds.diskService.GetStorageClass(ctx, vi.Status.StorageClassName) - if err != nil { - return reconcile.Result{}, err - } - err = ds.diskService.StartImmediate(ctx, size, sc, source, vi, supgen) - if updated, err := setPhaseConditionFromStorageError(err, vi, cb); err != nil || updated { - return reconcile.Result{}, err - } - - vi.Status.Phase = virtv2.ImageProvisioning - cb. - Status(metav1.ConditionFalse). - Reason(vicondition.Provisioning). - Message("PVC Provisioner not found: create the new one.") - - return reconcile.Result{Requeue: true}, nil - case dvQuotaNotExceededCondition != nil && dvQuotaNotExceededCondition.Status == corev1.ConditionFalse: - vi.Status.Phase = virtv2.ImagePending - cb. - Status(metav1.ConditionFalse). - Reason(vicondition.QuotaExceeded). - Message(dvQuotaNotExceededCondition.Message) - return reconcile.Result{}, nil - case dvRunningCondition != nil && dvRunningCondition.Status != corev1.ConditionTrue && dvRunningCondition.Reason == DVImagePullFailedReason: - vi.Status.Phase = virtv2.ImagePending - cb. - Status(metav1.ConditionFalse). - Reason(vicondition.ImagePullFailed). - Message(dvRunningCondition.Message) - ds.recorder.Event(vi, corev1.EventTypeWarning, vicondition.ImagePullFailed.String(), dvRunningCondition.Message) - return reconcile.Result{}, nil - case pvc == nil: - vi.Status.Phase = virtv2.ImageProvisioning - cb. - Status(metav1.ConditionFalse). - Reason(vicondition.Provisioning). - Message("PVC not found: waiting for creation.") - return reconcile.Result{Requeue: true}, nil - case ds.diskService.IsImportDone(dv, pvc): - log.Info("Import has completed", "dvProgress", dv.Status.Progress, "dvPhase", dv.Status.Phase, "pvcPhase", pvc.Status.Phase) - ds.recorder.Event( - vi, - corev1.EventTypeNormal, - virtv2.ReasonDataSourceSyncCompleted, - "The ObjectRef DataSource import has completed", - ) - - vi.Status.Phase = virtv2.ImageReady - cb. - Status(metav1.ConditionTrue). - Reason(vicondition.Ready). - Message("") - - q, err := resource.ParseQuantity(vdRef.Status.Capacity) - if err != nil { - return reconcile.Result{}, err - } - - intQ, ok := q.AsInt64() - if !ok { - return reconcile.Result{}, errors.New("fail to convert quantity to int64") - } - - vi.Status.Size = virtv2.ImageStatusSize{ - Stored: vdRef.Status.Capacity, - StoredBytes: strconv.FormatInt(intQ, 10), - Unpacked: vdRef.Status.Capacity, - UnpackedBytes: strconv.FormatInt(intQ, 10), - } - - vi.Status.Format = imageformat.FormatRAW - vi.Status.Progress = "100%" - vi.Status.Target.PersistentVolumeClaim = dv.Status.ClaimName - default: - log.Info("Provisioning to PVC is in progress", "dvProgress", dv.Status.Progress, "dvPhase", dv.Status.Phase, "pvcPhase", pvc.Status.Phase) - - vi.Status.Progress = ds.diskService.GetProgress(dv, vi.Status.Progress, service.NewScaleOption(0, 100)) - vi.Status.Target.PersistentVolumeClaim = dv.Status.ClaimName - - err = ds.diskService.Protect(ctx, vi, dv, pvc) - if err != nil { - return reconcile.Result{}, err - } - - err = setPhaseConditionForPVCProvisioningImage(ctx, dv, vi, pvc, cb, ds.diskService) - if err != nil { - return reconcile.Result{}, err - } - - return reconcile.Result{}, nil - } - - return reconcile.Result{Requeue: true}, nil -} - -func (ds ObjectRefVirtualDisk) CleanUpSupplements(ctx context.Context, vi *virtv2.VirtualImage) (reconcile.Result, error) { - supgen := supplements.NewGenerator(annotations.VIShortName, vi.Name, vi.Namespace, vi.UID) - - importerRequeue, err := ds.importerService.CleanUpSupplements(ctx, supgen) - if err != nil { - return reconcile.Result{}, err - } - - diskRequeue, err := ds.diskService.CleanUpSupplements(ctx, supgen) - if err != nil { - return reconcile.Result{}, err - } - - return reconcile.Result{Requeue: importerRequeue || diskRequeue}, nil -} - -func (ds ObjectRefVirtualDisk) CleanUp(ctx context.Context, vi *virtv2.VirtualImage) (bool, error) { - supgen := supplements.NewGenerator(annotations.VIShortName, vi.Name, vi.Namespace, vi.UID) - - importerRequeue, err := ds.importerService.CleanUp(ctx, supgen) - if err != nil { - return false, err - } - - diskRequeue, err := ds.diskService.CleanUp(ctx, supgen) - if err != nil { - return false, err - } - - return importerRequeue || diskRequeue, nil -} - -func (ds ObjectRefVirtualDisk) getEnvSettings(vi *virtv2.VirtualImage, sup *supplements.Generator) *importer.Settings { - var settings importer.Settings - importer.ApplyBlockDeviceSourceSettings(&settings) - importer.ApplyDVCRDestinationSettings( - &settings, - ds.dvcrSettings, - sup, - ds.dvcrSettings.RegistryImageForVI(vi), - ) - - return &settings -} - -func (ds ObjectRefVirtualDisk) Validate(ctx context.Context, vi *virtv2.VirtualImage) error { - if vi.Spec.DataSource.ObjectRef == nil || vi.Spec.DataSource.ObjectRef.Kind != virtv2.VirtualImageObjectRefKindVirtualDisk { - return fmt.Errorf("not a %s data source", virtv2.VirtualImageObjectRefKindVirtualDisk) - } - - vd, err := object.FetchObject(ctx, types.NamespacedName{Name: vi.Spec.DataSource.ObjectRef.Name, Namespace: vi.Namespace}, ds.client, &virtv2.VirtualDisk{}) - if err != nil { - return err - } - - if vd == nil || vd.Status.Phase != virtv2.DiskReady { - return NewVirtualDiskNotReadyError(vi.Spec.DataSource.ObjectRef.Name) - } - - inUseCondition, _ := conditions.GetCondition(vdcondition.InUseType, vd.Status.Conditions) - if inUseCondition.Status != metav1.ConditionTrue || !conditions.IsLastUpdated(inUseCondition, vd) { - return NewVirtualDiskNotReadyForUseError(vd.Name) - } - - switch inUseCondition.Reason { - case vdcondition.UsedForImageCreation.String(): - return nil - case vdcondition.AttachedToVirtualMachine.String(): - return NewVirtualDiskAttachedToVirtualMachineError(vd.Name) - default: - return NewVirtualDiskNotReadyForUseError(vd.Name) - } -} diff --git a/images/virtualization-artifact/pkg/controller/vi/internal/source/object_ref_vd_cr.go b/images/virtualization-artifact/pkg/controller/vi/internal/source/object_ref_vd_cr.go new file mode 100644 index 0000000000..130d8cbccc --- /dev/null +++ b/images/virtualization-artifact/pkg/controller/vi/internal/source/object_ref_vd_cr.go @@ -0,0 +1,156 @@ +/* +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 source + +import ( + "context" + "errors" + "fmt" + + 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" + + "github.com/deckhouse/virtualization-controller/pkg/common/annotations" + "github.com/deckhouse/virtualization-controller/pkg/common/blockdevice" + "github.com/deckhouse/virtualization-controller/pkg/common/object" + "github.com/deckhouse/virtualization-controller/pkg/controller/conditions" + "github.com/deckhouse/virtualization-controller/pkg/controller/importer" + "github.com/deckhouse/virtualization-controller/pkg/controller/supplements" + "github.com/deckhouse/virtualization-controller/pkg/controller/vi/internal/source/step" + "github.com/deckhouse/virtualization-controller/pkg/dvcr" + "github.com/deckhouse/virtualization-controller/pkg/eventrecord" + 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 ObjectRefVirtualDiskCR struct { + client client.Client + importer Importer + disk Disk + stat Stat + dvcrSettings *dvcr.Settings + recorder eventrecord.EventRecorderLogger +} + +func NewObjectRefVirtualDiskCR( + client client.Client, + importer Importer, + diskService Disk, + stat Stat, + dvcrSettings *dvcr.Settings, + recorder eventrecord.EventRecorderLogger, +) *ObjectRefVirtualDiskCR { + return &ObjectRefVirtualDiskCR{ + client: client, + importer: importer, + disk: diskService, + stat: stat, + dvcrSettings: dvcrSettings, + recorder: recorder, + } +} + +func (ds ObjectRefVirtualDiskCR) Sync(ctx context.Context, vi *virtv2.VirtualImage) (reconcile.Result, error) { + if vi.Spec.DataSource.ObjectRef == nil || vi.Spec.DataSource.ObjectRef.Kind != virtv2.VirtualImageObjectRefKindVirtualDisk { + return reconcile.Result{}, errors.New("object ref missed for data source") + } + + supgen := supplements.NewGenerator(annotations.VIShortName, vi.Name, vi.Namespace, vi.UID) + + cb := conditions.NewConditionBuilder(vicondition.ReadyType).Generation(vi.Generation) + defer func() { conditions.SetCondition(cb, &vi.Status.Conditions) }() + + pod, err := importer.FindPod(ctx, ds.client, supgen) + if err != nil { + return reconcile.Result{}, fmt.Errorf("fetch pod: %w", err) + } + + return blockdevice.NewStepTakers[*virtv2.VirtualImage]( + step.NewReadyContainerRegistryStep(pod, ds.importer, ds.disk, ds.stat, ds.recorder, cb), + step.NewTerminatingStep(pod), + step.NewSetSourceUIDStep(ds.GetSourceUID), + step.NewCreatePodStep(pod, ds.dvcrSettings, ds.recorder, ds.importer, ds.stat, ds.GetPodSettings, cb), + step.NewWaitForPodStep(pod, ds.stat, cb), + ).Run(ctx, vi) +} + +func (ds ObjectRefVirtualDiskCR) GetSourceUID(ctx context.Context, vi *virtv2.VirtualImage) (*types.UID, error) { + vdRefKey := types.NamespacedName{Name: vi.Spec.DataSource.ObjectRef.Name, Namespace: vi.Namespace} + vdRef, err := object.FetchObject(ctx, vdRefKey, ds.client, &virtv2.VirtualDisk{}) + if err != nil { + return nil, fmt.Errorf("fetch vd %q: %w", vdRefKey, err) + } + + if vdRef == nil { + return nil, fmt.Errorf("vd object ref %q is nil", vdRefKey) + } + + return ptr.To(vdRef.UID), nil +} + +func (ds ObjectRefVirtualDiskCR) GetPodSettings(ctx context.Context, vi *virtv2.VirtualImage) (*importer.PodSettings, error) { + vdRefKey := types.NamespacedName{Name: vi.Spec.DataSource.ObjectRef.Name, Namespace: vi.Namespace} + vdRef, err := object.FetchObject(ctx, vdRefKey, ds.client, &virtv2.VirtualDisk{}) + if err != nil { + return nil, fmt.Errorf("fetch vd %q: %w", vdRefKey, err) + } + + if vdRef == nil { + return nil, fmt.Errorf("vd object ref %q is nil", vdRefKey) + } + + supgen := supplements.NewGenerator(annotations.VIShortName, vi.Name, vi.Namespace, vi.UID) + ownerRef := metav1.NewControllerRef(vi, vi.GroupVersionKind()) + return ds.importer.GetPodSettingsWithPVC(ownerRef, supgen, vdRef.Status.Target.PersistentVolumeClaim, vdRef.Namespace), nil +} + +func (ds ObjectRefVirtualDiskCR) Validate(ctx context.Context, vi *virtv2.VirtualImage) error { + return validateVirtualDisk(ctx, vi, ds.client) +} + +func validateVirtualDisk(ctx context.Context, vi *virtv2.VirtualImage, client client.Client) error { + if vi.Spec.DataSource.ObjectRef == nil || vi.Spec.DataSource.ObjectRef.Kind != virtv2.VirtualImageObjectRefKindVirtualDisk { + return errors.New("object ref missed for data source") + } + + vd, err := object.FetchObject(ctx, types.NamespacedName{Name: vi.Spec.DataSource.ObjectRef.Name, Namespace: vi.Namespace}, client, &virtv2.VirtualDisk{}) + if err != nil { + return fmt.Errorf("fetch virtual disk: %w", err) + } + + if vd == nil || vd.Status.Phase != virtv2.DiskReady { + return NewVirtualDiskNotReadyError(vi.Spec.DataSource.ObjectRef.Name) + } + + inUseCondition, _ := conditions.GetCondition(vdcondition.InUseType, vd.Status.Conditions) + if inUseCondition.Status != metav1.ConditionTrue || inUseCondition.ObservedGeneration != vd.Generation { + return NewVirtualDiskNotReadyForUseError(vd.Name) + } + + switch inUseCondition.Reason { + case vdcondition.UsedForImageCreation.String(): + return nil + case vdcondition.AttachedToVirtualMachine.String(): + return NewVirtualDiskAttachedToVirtualMachineError(vd.Name) + default: + return NewVirtualDiskNotReadyForUseError(vd.Name) + } +} diff --git a/images/virtualization-artifact/pkg/controller/vi/internal/source/object_ref_vd_cr_test.go b/images/virtualization-artifact/pkg/controller/vi/internal/source/object_ref_vd_cr_test.go new file mode 100644 index 0000000000..f71005440c --- /dev/null +++ b/images/virtualization-artifact/pkg/controller/vi/internal/source/object_ref_vd_cr_test.go @@ -0,0 +1,170 @@ +/* +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 source + +import ( + "context" + "log/slog" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "sigs.k8s.io/controller-runtime/pkg/client/fake" + + "github.com/deckhouse/virtualization-controller/pkg/common/datasource" + importersettings "github.com/deckhouse/virtualization-controller/pkg/controller/importer" + "github.com/deckhouse/virtualization-controller/pkg/controller/supplements" + "github.com/deckhouse/virtualization-controller/pkg/dvcr" + "github.com/deckhouse/virtualization-controller/pkg/eventrecord" + "github.com/deckhouse/virtualization-controller/pkg/logger" + virtv2 "github.com/deckhouse/virtualization/api/core/v1alpha2" + "github.com/deckhouse/virtualization/api/core/v1alpha2/vicondition" +) + +var _ = Describe("ObjectRef VirtualDisk ContainerRegistry", func() { + var ( + ctx context.Context + scheme *runtime.Scheme + vi *virtv2.VirtualImage + vd *virtv2.VirtualDisk + pod *corev1.Pod + settings *dvcr.Settings + recorder eventrecord.EventRecorderLogger + importer *ImporterMock + disk *DiskMock + stat *StatMock + ) + + BeforeEach(func() { + ctx = logger.ToContext(context.TODO(), slog.Default()) + + scheme = runtime.NewScheme() + Expect(virtv2.AddToScheme(scheme)).To(Succeed()) + Expect(corev1.AddToScheme(scheme)).To(Succeed()) + + importer, disk, stat, recorder, _ = getServiceMocks() + settings = &dvcr.Settings{} + + vd = getVirtualDisk() + vi = getVirtualImage(virtv2.StorageContainerRegistry, virtv2.VirtualImageDataSource{ + Type: virtv2.DataSourceTypeObjectRef, + ObjectRef: &virtv2.VirtualImageObjectRef{ + Kind: virtv2.VirtualImageObjectRefKindVirtualDisk, + Name: vd.Name, + }, + }) + pod = getPod(vi) + }) + + Context("VirtualImage has just been created", func() { + It("must create Pod", func() { + var podCreated bool + importer.StartWithPodSettingFunc = func(_ context.Context, _ *importersettings.Settings, _ *supplements.Generator, _ *datasource.CABundle, _ *importersettings.PodSettings) error { + podCreated = true + return nil + } + + client := fake.NewClientBuilder().WithScheme(scheme).WithObjects(vd).Build() + + vi.Status = virtv2.VirtualImageStatus{} + + syncer := NewObjectRefVirtualDiskCR(client, importer, nil, stat, settings, recorder) + + res, err := syncer.Sync(ctx, vi) + Expect(err).ToNot(HaveOccurred()) + Expect(res.IsZero()).To(BeTrue()) + + Expect(podCreated).To(BeTrue()) + + ExpectCondition(vi, metav1.ConditionFalse, vicondition.Provisioning, true) + Expect(vi.Status.SourceUID).ToNot(BeNil()) + Expect(*vi.Status.SourceUID).ToNot(BeEmpty()) + Expect(vi.Status.Phase).To(Equal(virtv2.ImageProvisioning)) + Expect(vi.Status.Target.RegistryURL).ToNot(BeEmpty()) + Expect(vi.Status.Target.PersistentVolumeClaim).To(BeEmpty()) + }) + }) + + Context("VirtualImage waits for the Pod to be Completed", func() { + It("waits for the Pod to be Running", func() { + pod.Status.Phase = corev1.PodPending + client := fake.NewClientBuilder().WithScheme(scheme).WithObjects(vd, pod).Build() + + syncer := NewObjectRefVirtualDiskCR(client, importer, nil, stat, settings, recorder) + + res, err := syncer.Sync(ctx, vi) + Expect(err).ToNot(HaveOccurred()) + Expect(res.IsZero()).To(BeTrue()) + + ExpectCondition(vi, metav1.ConditionFalse, vicondition.Provisioning, true) + Expect(vi.Status.Phase).To(Equal(virtv2.ImageProvisioning)) + }) + + It("waits for the Pod to be Succeeded", func() { + pod.Status.Phase = corev1.PodRunning + client := fake.NewClientBuilder().WithScheme(scheme).WithObjects(vd, pod).Build() + + syncer := NewObjectRefVirtualDiskCR(client, importer, nil, stat, settings, recorder) + + res, err := syncer.Sync(ctx, vi) + Expect(err).ToNot(HaveOccurred()) + Expect(res.RequeueAfter).ToNot(BeZero()) + + ExpectCondition(vi, metav1.ConditionFalse, vicondition.Provisioning, true) + Expect(vi.Status.Phase).To(Equal(virtv2.ImageProvisioning)) + }) + }) + + Context("VirtualImage is ready", func() { + It("has Pod in Succeeded phase", func() { + pod.Status.Phase = corev1.PodSucceeded + client := fake.NewClientBuilder().WithScheme(scheme).WithObjects(pod).Build() + + syncer := NewObjectRefVirtualDiskCR(client, importer, disk, stat, settings, recorder) + + res, err := syncer.Sync(ctx, vi) + Expect(err).ToNot(HaveOccurred()) + Expect(res.IsZero()).To(BeTrue()) + + ExpectCondition(vi, metav1.ConditionTrue, vicondition.Ready, false) + Expect(vi.Status.Phase).To(Equal(virtv2.ImageReady)) + }) + + It("does not have Pod", func() { + vi.Status.Conditions = []metav1.Condition{ + { + Type: vicondition.ReadyType.String(), + Status: metav1.ConditionTrue, + Reason: vicondition.Ready.String(), + }, + } + + client := fake.NewClientBuilder().WithScheme(scheme).WithObjects().Build() + + syncer := NewObjectRefVirtualDiskCR(client, importer, nil, stat, settings, recorder) + + res, err := syncer.Sync(ctx, vi) + Expect(err).ToNot(HaveOccurred()) + Expect(res.IsZero()).To(BeTrue()) + + ExpectCondition(vi, metav1.ConditionTrue, vicondition.Ready, false) + Expect(vi.Status.Phase).To(Equal(virtv2.ImageReady)) + }) + }) +}) diff --git a/images/virtualization-artifact/pkg/controller/vi/internal/source/object_ref_vd_pvc.go b/images/virtualization-artifact/pkg/controller/vi/internal/source/object_ref_vd_pvc.go new file mode 100644 index 0000000000..1186a68ce2 --- /dev/null +++ b/images/virtualization-artifact/pkg/controller/vi/internal/source/object_ref_vd_pvc.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 source + +import ( + "context" + "errors" + "fmt" + + corev1 "k8s.io/api/core/v1" + cdiv1 "kubevirt.io/containerized-data-importer-api/pkg/apis/core/v1beta1" + "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/common/blockdevice" + "github.com/deckhouse/virtualization-controller/pkg/common/object" + "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/step" + "github.com/deckhouse/virtualization-controller/pkg/eventrecord" + virtv2 "github.com/deckhouse/virtualization/api/core/v1alpha2" + "github.com/deckhouse/virtualization/api/core/v1alpha2/vicondition" +) + +type ObjectRefVirtualDiskPVC struct { + bounder Bounder + client client.Client + disk Disk + recorder eventrecord.EventRecorderLogger +} + +func NewObjectRefVirtualDiskPVC( + bounder Bounder, + client client.Client, + disk Disk, + recorder eventrecord.EventRecorderLogger, +) *ObjectRefVirtualDiskPVC { + return &ObjectRefVirtualDiskPVC{ + bounder: bounder, + client: client, + disk: disk, + recorder: recorder, + } +} + +func (ds ObjectRefVirtualDiskPVC) Sync(ctx context.Context, vi *virtv2.VirtualImage) (reconcile.Result, error) { + if vi.Spec.DataSource.ObjectRef == nil || vi.Spec.DataSource.ObjectRef.Kind != virtv2.VirtualImageObjectRefKindVirtualDisk { + return reconcile.Result{}, errors.New("object ref missed for data source") + } + + supgen := supplements.NewGenerator(annotations.VIShortName, vi.Name, vi.Namespace, vi.UID) + + cb := conditions.NewConditionBuilder(vicondition.ReadyType).Generation(vi.Generation) + defer func() { conditions.SetCondition(cb, &vi.Status.Conditions) }() + + pvc, err := object.FetchObject(ctx, supgen.PersistentVolumeClaim(), ds.client, &corev1.PersistentVolumeClaim{}) + if err != nil { + return reconcile.Result{}, fmt.Errorf("fetch pvc: %w", err) + } + + dv, err := object.FetchObject(ctx, supgen.DataVolume(), ds.client, &cdiv1.DataVolume{}) + if err != nil { + return reconcile.Result{}, fmt.Errorf("fetch dv: %w", err) + } + + return blockdevice.NewStepTakers[*virtv2.VirtualImage]( + step.NewReadyPersistentVolumeClaimStep(pvc, ds.bounder, ds.recorder, cb), + step.NewTerminatingStep(pvc, dv), + step.NewCreateDataVolumeFromVirtualDiskStep(dv, ds.recorder, ds.disk, ds.client, cb), + step.NewWaitForDVStep(pvc, dv, ds.disk, ds.client, cb), + step.NewWaitForPVCStep(pvc, cb), + ).Run(ctx, vi) +} + +func (ds ObjectRefVirtualDiskPVC) Validate(ctx context.Context, vi *virtv2.VirtualImage) error { + return validateVirtualDisk(ctx, vi, ds.client) +} diff --git a/images/virtualization-artifact/pkg/controller/vi/internal/source/object_ref_vd_pvc_test.go b/images/virtualization-artifact/pkg/controller/vi/internal/source/object_ref_vd_pvc_test.go new file mode 100644 index 0000000000..ebf52995db --- /dev/null +++ b/images/virtualization-artifact/pkg/controller/vi/internal/source/object_ref_vd_pvc_test.go @@ -0,0 +1,157 @@ +/* +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 source + +import ( + "context" + "log/slog" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + corev1 "k8s.io/api/core/v1" + storagev1 "k8s.io/api/storage/v1" + "k8s.io/apimachinery/pkg/api/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + cdiv1 "kubevirt.io/containerized-data-importer-api/pkg/apis/core/v1beta1" + "sigs.k8s.io/controller-runtime/pkg/client/fake" + + "github.com/deckhouse/virtualization-controller/pkg/controller/service" + "github.com/deckhouse/virtualization-controller/pkg/controller/supplements" + "github.com/deckhouse/virtualization-controller/pkg/eventrecord" + "github.com/deckhouse/virtualization-controller/pkg/logger" + virtv2 "github.com/deckhouse/virtualization/api/core/v1alpha2" + "github.com/deckhouse/virtualization/api/core/v1alpha2/vicondition" +) + +var _ = Describe("ObjectRef VirtualDisk PersistentVolumeClaim", func() { + var ( + ctx context.Context + scheme *runtime.Scheme + vi *virtv2.VirtualImage + vd *virtv2.VirtualDisk + pvc *corev1.PersistentVolumeClaim + recorder eventrecord.EventRecorderLogger + bounder *BounderMock + disk *DiskMock + ) + + BeforeEach(func() { + ctx = logger.ToContext(context.TODO(), slog.Default()) + + scheme = runtime.NewScheme() + Expect(virtv2.AddToScheme(scheme)).To(Succeed()) + Expect(corev1.AddToScheme(scheme)).To(Succeed()) + Expect(cdiv1.AddToScheme(scheme)).To(Succeed()) + + _, disk, _, recorder, bounder = getServiceMocks() + + vd = getVirtualDisk() + vi = getVirtualImage(virtv2.StoragePersistentVolumeClaim, virtv2.VirtualImageDataSource{ + Type: virtv2.DataSourceTypeObjectRef, + ObjectRef: &virtv2.VirtualImageObjectRef{ + Kind: virtv2.VirtualImageObjectRefKindVirtualDisk, + Name: vd.Name, + }, + }) + pvc = getPVC(vi, "") + }) + + Context("VirtualImage has just been created", func() { + It("must create DV", func() { + vi.Status = virtv2.VirtualImageStatus{} + client := fake.NewClientBuilder().WithScheme(scheme).WithObjects(vd).Build() + + var started bool + disk.StartImmediateFunc = func(_ context.Context, _ resource.Quantity, _ *storagev1.StorageClass, _ *cdiv1.DataVolumeSource, _ service.ObjectKind, _ *supplements.Generator) error { + started = true + return nil + } + + syncer := NewObjectRefVirtualDiskPVC(bounder, client, disk, recorder) + + res, err := syncer.Sync(ctx, vi) + Expect(err).ToNot(HaveOccurred()) + Expect(res.IsZero()).To(BeTrue()) + + Expect(started).To(BeTrue()) + + ExpectCondition(vi, metav1.ConditionFalse, vicondition.Provisioning, true) + Expect(vi.Status.SourceUID).ToNot(BeNil()) + Expect(*vi.Status.SourceUID).ToNot(BeEmpty()) + Expect(vi.Status.Phase).To(Equal(virtv2.ImageProvisioning)) + Expect(vi.Status.Target.RegistryURL).To(BeEmpty()) + Expect(vi.Status.Target.PersistentVolumeClaim).NotTo(BeEmpty()) + }) + }) + + Context("VirtualImage is ready", func() { + It("has PVC in Bound phase", func() { + pvc.Status.Phase = corev1.ClaimBound + client := fake.NewClientBuilder().WithScheme(scheme).WithObjects(pvc).Build() + + syncer := NewObjectRefVirtualDiskPVC(bounder, client, disk, recorder) + + res, err := syncer.Sync(ctx, vi) + Expect(err).ToNot(HaveOccurred()) + Expect(res.IsZero()).To(BeTrue()) + + ExpectCondition(vi, metav1.ConditionTrue, vicondition.Ready, false) + Expect(vi.Status.Phase).To(Equal(virtv2.ImageReady)) + }) + }) + + Context("VirtualImage is lost", func() { + It("is lost when PVC is not found", func() { + vi.Status.Target.PersistentVolumeClaim = pvc.Name + vi.Status.Conditions = []metav1.Condition{ + { + Type: vicondition.ReadyType.String(), + Reason: vicondition.Ready.String(), + Status: metav1.ConditionTrue, + }, + } + client := fake.NewClientBuilder().WithScheme(scheme).WithObjects().Build() + + syncer := NewObjectRefVirtualDiskPVC(bounder, client, disk, recorder) + + res, err := syncer.Sync(ctx, vi) + Expect(err).ToNot(HaveOccurred()) + Expect(res.IsZero()).To(BeTrue()) + + ExpectCondition(vi, metav1.ConditionFalse, vicondition.Lost, true) + Expect(vi.Status.Phase).To(Equal(virtv2.ImageLost)) + Expect(vi.Status.Target.PersistentVolumeClaim).NotTo(BeEmpty()) + }) + + It("is lost when PVC is lost as well", func() { + pvc.Status.Phase = corev1.ClaimLost + vi.Status.Target.PersistentVolumeClaim = pvc.Name + client := fake.NewClientBuilder().WithScheme(scheme).WithObjects(pvc).Build() + + syncer := NewObjectRefVirtualDiskPVC(bounder, client, disk, recorder) + + res, err := syncer.Sync(ctx, vi) + Expect(err).ToNot(HaveOccurred()) + Expect(res.IsZero()).To(BeTrue()) + + ExpectCondition(vi, metav1.ConditionFalse, vicondition.Lost, true) + Expect(vi.Status.Phase).To(Equal(virtv2.ImageLost)) + Expect(vi.Status.Target.PersistentVolumeClaim).NotTo(BeEmpty()) + }) + }) +}) diff --git a/images/virtualization-artifact/pkg/controller/vi/internal/source/object_ref_vdsnapshot_cr.go b/images/virtualization-artifact/pkg/controller/vi/internal/source/object_ref_vdsnapshot_cr.go index a04b0a17a1..15b04b22e0 100644 --- a/images/virtualization-artifact/pkg/controller/vi/internal/source/object_ref_vdsnapshot_cr.go +++ b/images/virtualization-artifact/pkg/controller/vi/internal/source/object_ref_vdsnapshot_cr.go @@ -23,6 +23,7 @@ import ( vsv1 "github.com/kubernetes-csi/external-snapshotter/client/v6/apis/volumesnapshot/v1" corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/reconcile" @@ -91,11 +92,18 @@ func (ds ObjectRefVirtualDiskSnapshotCR) Sync(ctx context.Context, vi *virtv2.Vi step.NewReadyContainerRegistryStep(pod, ds.importer, ds.diskService, ds.stat, ds.recorder, cb), step.NewTerminatingStep(pvc), step.NewCreatePersistentVolumeClaimStep(pvc, ds.recorder, ds.client, cb), - step.NewCreatePodStep(pod, ds.dvcrSettings, ds.recorder, ds.importer, ds.stat, cb), - step.NewWaitForPodStep(pod, pvc, ds.stat, cb), + step.NewCreatePodStep(pod, ds.dvcrSettings, ds.recorder, ds.importer, ds.stat, ds.GetPodSettings, cb), + step.NewWaitForPodStep(pod, ds.stat, cb), ).Run(ctx, vi) } +func (ds ObjectRefVirtualDiskSnapshotCR) GetPodSettings(_ context.Context, vi *virtv2.VirtualImage) (*importer.PodSettings, error) { + supgen := supplements.NewGenerator(annotations.VIShortName, vi.Name, vi.Namespace, vi.UID) + ownerRef := metav1.NewControllerRef(vi, vi.GroupVersionKind()) + pvcKey := supgen.PersistentVolumeClaim() + return ds.importer.GetPodSettingsWithPVC(ownerRef, supgen, pvcKey.Name, pvcKey.Namespace), nil +} + func (ds ObjectRefVirtualDiskSnapshotCR) Validate(ctx context.Context, vi *virtv2.VirtualImage) error { return validateVirtualDiskSnapshot(ctx, vi, ds.client) } diff --git a/images/virtualization-artifact/pkg/controller/vi/internal/source/object_ref_vdsnapshot_cr_test.go b/images/virtualization-artifact/pkg/controller/vi/internal/source/object_ref_vdsnapshot_cr_test.go index 2878af8278..901e285a9d 100644 --- a/images/virtualization-artifact/pkg/controller/vi/internal/source/object_ref_vdsnapshot_cr_test.go +++ b/images/virtualization-artifact/pkg/controller/vi/internal/source/object_ref_vdsnapshot_cr_test.go @@ -1,5 +1,5 @@ /* -Copyright 2024 Flant JSC +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. @@ -19,7 +19,6 @@ package source import ( "context" "log/slog" - "testing" vsv1 "github.com/kubernetes-csi/external-snapshotter/client/v6/apis/volumesnapshot/v1" . "github.com/onsi/ginkgo/v2" @@ -28,17 +27,13 @@ import ( storagev1 "k8s.io/api/storage/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" - "k8s.io/apimachinery/pkg/types" "k8s.io/utils/ptr" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/client/fake" "sigs.k8s.io/controller-runtime/pkg/client/interceptor" - "github.com/deckhouse/virtualization-controller/pkg/common/annotations" "github.com/deckhouse/virtualization-controller/pkg/common/datasource" - "github.com/deckhouse/virtualization-controller/pkg/controller/conditions" - importer2 "github.com/deckhouse/virtualization-controller/pkg/controller/importer" - "github.com/deckhouse/virtualization-controller/pkg/controller/service" + importersettings "github.com/deckhouse/virtualization-controller/pkg/controller/importer" "github.com/deckhouse/virtualization-controller/pkg/controller/supplements" "github.com/deckhouse/virtualization-controller/pkg/dvcr" "github.com/deckhouse/virtualization-controller/pkg/eventrecord" @@ -47,12 +42,7 @@ import ( "github.com/deckhouse/virtualization/api/core/v1alpha2/vicondition" ) -func TestHandlers(t *testing.T) { - RegisterFailHandler(Fail) - RunSpecs(t, "Sources") -} - -var _ = Describe("ObjectRef VirtualImageSnapshot ContainerRegistry", func() { +var _ = Describe("ObjectRef VirtualDiskSnapshot ContainerRegistry", func() { var ( ctx context.Context scheme *runtime.Scheme @@ -78,42 +68,7 @@ var _ = Describe("ObjectRef VirtualImageSnapshot ContainerRegistry", func() { Expect(vsv1.AddToScheme(scheme)).To(Succeed()) Expect(storagev1.AddToScheme(scheme)).To(Succeed()) - recorder = &eventrecord.EventRecorderLoggerMock{ - EventFunc: func(_ client.Object, _, _, _ string) {}, - } - - importer = &ImporterMock{ - CleanUpSupplementsFunc: func(_ context.Context, _ *supplements.Generator) (bool, error) { - return false, nil - }, - } - stat = &StatMock{ - GetDVCRImageNameFunc: func(_ *corev1.Pod) string { - return "image" - }, - CheckPodFunc: func(_ *corev1.Pod) error { - return nil - }, - GetSizeFunc: func(_ *corev1.Pod) virtv2.ImageStatusSize { - return virtv2.ImageStatusSize{} - }, - GetCDROMFunc: func(_ *corev1.Pod) bool { - return false - }, - GetFormatFunc: func(_ *corev1.Pod) string { - return "iso" - }, - GetProgressFunc: func(_ types.UID, _ *corev1.Pod, _ string, _ ...service.GetProgressOption) string { - return "N%" - }, - } - - diskService = &DiskMock{ - CleanUpSupplementsFunc: func(ctx context.Context, sup *supplements.Generator) (bool, error) { - return false, nil - }, - } - + importer, diskService, stat, recorder, _ = getServiceMocks() settings = &dvcr.Settings{} sc = &storagev1.StorageClass{ @@ -143,42 +98,16 @@ var _ = Describe("ObjectRef VirtualImageSnapshot ContainerRegistry", func() { }, } - vi = &virtv2.VirtualImage{ - ObjectMeta: metav1.ObjectMeta{ - Name: "vi", - Generation: 1, - UID: "22222222-2222-2222-2222-222222222222", - }, - Spec: virtv2.VirtualImageSpec{ - DataSource: virtv2.VirtualImageDataSource{ - Type: virtv2.DataSourceTypeObjectRef, - ObjectRef: &virtv2.VirtualImageObjectRef{ - Kind: virtv2.VirtualImageObjectRefKindVirtualDiskSnapshot, - Name: vdSnapshot.Name, - }, - }, - }, - } - - supgen := supplements.NewGenerator(annotations.VIShortName, vi.Name, vi.Namespace, vi.UID) - - pvc = &corev1.PersistentVolumeClaim{ - ObjectMeta: metav1.ObjectMeta{ - Name: supgen.PersistentVolumeClaim().Name, + vi = getVirtualImage(virtv2.StorageContainerRegistry, virtv2.VirtualImageDataSource{ + Type: virtv2.DataSourceTypeObjectRef, + ObjectRef: &virtv2.VirtualImageObjectRef{ + Kind: virtv2.VirtualImageObjectRefKindVirtualDiskSnapshot, + Name: vdSnapshot.Name, }, - Spec: corev1.PersistentVolumeClaimSpec{ - StorageClassName: &sc.Name, - }, - Status: corev1.PersistentVolumeClaimStatus{ - Phase: corev1.ClaimBound, - }, - } + }) - pod = &corev1.Pod{ - ObjectMeta: metav1.ObjectMeta{ - Name: supgen.ImporterPod().Name, - }, - } + pvc = getPVC(vi, sc.Name) + pod = getPod(vi) }) Context("VirtualImage has just been created", func() { @@ -186,10 +115,10 @@ var _ = Describe("ObjectRef VirtualImageSnapshot ContainerRegistry", func() { var pvcCreated bool var podCreated bool - importer.GetPodSettingsWithPVCFunc = func(_ *metav1.OwnerReference, _ *supplements.Generator, _, _ string) *importer2.PodSettings { + importer.GetPodSettingsWithPVCFunc = func(_ *metav1.OwnerReference, _ *supplements.Generator, _, _ string) *importersettings.PodSettings { return nil } - importer.StartWithPodSettingFunc = func(_ context.Context, _ *importer2.Settings, _ *supplements.Generator, _ *datasource.CABundle, _ *importer2.PodSettings) error { + importer.StartWithPodSettingFunc = func(_ context.Context, _ *importersettings.Settings, _ *supplements.Generator, _ *datasource.CABundle, _ *importersettings.PodSettings) error { podCreated = true return nil } @@ -306,16 +235,3 @@ var _ = Describe("ObjectRef VirtualImageSnapshot ContainerRegistry", func() { }) }) }) - -func ExpectCondition(vi *virtv2.VirtualImage, status metav1.ConditionStatus, reason vicondition.ReadyReason, msgExists bool) { - ready, _ := conditions.GetCondition(vicondition.Ready, vi.Status.Conditions) - Expect(ready.Status).To(Equal(status)) - Expect(ready.Reason).To(Equal(reason.String())) - Expect(ready.ObservedGeneration).To(Equal(vi.Generation)) - - if msgExists { - Expect(ready.Message).ToNot(BeEmpty()) - } else { - Expect(ready.Message).To(BeEmpty()) - } -} diff --git a/images/virtualization-artifact/pkg/controller/vi/internal/source/object_ref_vdsnapshot_pvc.go b/images/virtualization-artifact/pkg/controller/vi/internal/source/object_ref_vdsnapshot_pvc.go index 68c9679ca2..9b5e74e0e6 100644 --- a/images/virtualization-artifact/pkg/controller/vi/internal/source/object_ref_vdsnapshot_pvc.go +++ b/images/virtualization-artifact/pkg/controller/vi/internal/source/object_ref_vdsnapshot_pvc.go @@ -31,19 +31,17 @@ import ( "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/step" - "github.com/deckhouse/virtualization-controller/pkg/dvcr" "github.com/deckhouse/virtualization-controller/pkg/eventrecord" virtv2 "github.com/deckhouse/virtualization/api/core/v1alpha2" "github.com/deckhouse/virtualization/api/core/v1alpha2/vicondition" ) type ObjectRefVirtualDiskSnapshotPVC struct { - importer Importer - stat Stat - bounder Bounder - client client.Client - dvcrSettings *dvcr.Settings - recorder eventrecord.EventRecorderLogger + importer Importer + stat Stat + bounder Bounder + client client.Client + recorder eventrecord.EventRecorderLogger } func NewObjectRefVirtualDiskSnapshotPVC( @@ -51,16 +49,14 @@ func NewObjectRefVirtualDiskSnapshotPVC( stat Stat, bounder Bounder, client client.Client, - dvcrSettings *dvcr.Settings, recorder eventrecord.EventRecorderLogger, ) *ObjectRefVirtualDiskSnapshotPVC { return &ObjectRefVirtualDiskSnapshotPVC{ - importer: importer, - stat: stat, - bounder: bounder, - client: client, - dvcrSettings: dvcrSettings, - recorder: recorder, + importer: importer, + stat: stat, + bounder: bounder, + client: client, + recorder: recorder, } } diff --git a/images/virtualization-artifact/pkg/controller/vi/internal/source/object_ref_vdsnapshot_pvc_test.go b/images/virtualization-artifact/pkg/controller/vi/internal/source/object_ref_vdsnapshot_pvc_test.go index 3444c7f417..99d1b09645 100644 --- a/images/virtualization-artifact/pkg/controller/vi/internal/source/object_ref_vdsnapshot_pvc_test.go +++ b/images/virtualization-artifact/pkg/controller/vi/internal/source/object_ref_vdsnapshot_pvc_test.go @@ -1,5 +1,5 @@ /* -Copyright 2024 Flant JSC +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. @@ -27,23 +27,18 @@ import ( storagev1 "k8s.io/api/storage/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" - "k8s.io/apimachinery/pkg/types" "k8s.io/utils/ptr" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/client/fake" "sigs.k8s.io/controller-runtime/pkg/client/interceptor" - "github.com/deckhouse/virtualization-controller/pkg/common/annotations" - "github.com/deckhouse/virtualization-controller/pkg/controller/service" - "github.com/deckhouse/virtualization-controller/pkg/controller/supplements" - "github.com/deckhouse/virtualization-controller/pkg/dvcr" "github.com/deckhouse/virtualization-controller/pkg/eventrecord" "github.com/deckhouse/virtualization-controller/pkg/logger" virtv2 "github.com/deckhouse/virtualization/api/core/v1alpha2" "github.com/deckhouse/virtualization/api/core/v1alpha2/vicondition" ) -var _ = Describe("ObjectRef VirtualImageSnapshot PersistentVolumeClaim", func() { +var _ = Describe("ObjectRef VirtualDiskSnapshot PersistentVolumeClaim", func() { var ( ctx context.Context scheme *runtime.Scheme @@ -52,7 +47,6 @@ var _ = Describe("ObjectRef VirtualImageSnapshot PersistentVolumeClaim", func() sc *storagev1.StorageClass vdSnapshot *virtv2.VirtualDiskSnapshot pvc *corev1.PersistentVolumeClaim - settings *dvcr.Settings recorder eventrecord.EventRecorderLogger importer *ImporterMock bounder *BounderMock @@ -68,41 +62,7 @@ var _ = Describe("ObjectRef VirtualImageSnapshot PersistentVolumeClaim", func() Expect(vsv1.AddToScheme(scheme)).To(Succeed()) Expect(storagev1.AddToScheme(scheme)).To(Succeed()) - recorder = &eventrecord.EventRecorderLoggerMock{ - EventFunc: func(_ client.Object, _, _, _ string) {}, - } - - importer = &ImporterMock{ - CleanUpSupplementsFunc: func(_ context.Context, _ *supplements.Generator) (bool, error) { - return false, nil - }, - } - bounder = &BounderMock{ - CleanUpSupplementsFunc: func(_ context.Context, _ *supplements.Generator) (bool, error) { - return false, nil - }, - } - stat = &StatMock{ - GetDVCRImageNameFunc: func(_ *corev1.Pod) string { - return "image" - }, - CheckPodFunc: func(_ *corev1.Pod) error { - return nil - }, - GetSizeFunc: func(_ *corev1.Pod) virtv2.ImageStatusSize { - return virtv2.ImageStatusSize{} - }, - GetCDROMFunc: func(_ *corev1.Pod) bool { - return false - }, - GetFormatFunc: func(_ *corev1.Pod) string { - return "iso" - }, - GetProgressFunc: func(_ types.UID, _ *corev1.Pod, _ string, _ ...service.GetProgressOption) string { - return "N%" - }, - } - settings = &dvcr.Settings{} + importer, _, stat, recorder, bounder = getServiceMocks() sc = &storagev1.StorageClass{ ObjectMeta: metav1.ObjectMeta{ @@ -131,37 +91,15 @@ var _ = Describe("ObjectRef VirtualImageSnapshot PersistentVolumeClaim", func() }, } - vi = &virtv2.VirtualImage{ - ObjectMeta: metav1.ObjectMeta{ - Name: "vi", - Generation: 1, - UID: "22222222-2222-2222-2222-222222222222", - }, - Spec: virtv2.VirtualImageSpec{ - Storage: virtv2.StoragePersistentVolumeClaim, - DataSource: virtv2.VirtualImageDataSource{ - Type: virtv2.DataSourceTypeObjectRef, - ObjectRef: &virtv2.VirtualImageObjectRef{ - Kind: virtv2.VirtualImageObjectRefKindVirtualDiskSnapshot, - Name: vdSnapshot.Name, - }, - }, + vi = getVirtualImage(virtv2.StoragePersistentVolumeClaim, virtv2.VirtualImageDataSource{ + Type: virtv2.DataSourceTypeObjectRef, + ObjectRef: &virtv2.VirtualImageObjectRef{ + Kind: virtv2.VirtualImageObjectRefKindVirtualDiskSnapshot, + Name: vdSnapshot.Name, }, - } - - supgen := supplements.NewGenerator(annotations.VIShortName, vi.Name, vi.Namespace, vi.UID) + }) - pvc = &corev1.PersistentVolumeClaim{ - ObjectMeta: metav1.ObjectMeta{ - Name: supgen.PersistentVolumeClaim().Name, - }, - Spec: corev1.PersistentVolumeClaimSpec{ - StorageClassName: &sc.Name, - }, - Status: corev1.PersistentVolumeClaimStatus{ - Phase: corev1.ClaimBound, - }, - } + pvc = getPVC(vi, sc.Name) }) Context("VirtualImage has just been created", func() { @@ -181,7 +119,7 @@ var _ = Describe("ObjectRef VirtualImageSnapshot PersistentVolumeClaim", func() }, }).Build() - syncer := NewObjectRefVirtualDiskSnapshotPVC(importer, stat, nil, client, settings, recorder) + syncer := NewObjectRefVirtualDiskSnapshotPVC(importer, stat, nil, client, recorder) res, err := syncer.Sync(ctx, vi) Expect(err).ToNot(HaveOccurred()) @@ -193,6 +131,7 @@ var _ = Describe("ObjectRef VirtualImageSnapshot PersistentVolumeClaim", func() Expect(vi.Status.SourceUID).ToNot(BeNil()) Expect(*vi.Status.SourceUID).ToNot(BeEmpty()) Expect(vi.Status.Phase).To(Equal(virtv2.ImageProvisioning)) + Expect(vi.Status.Target.RegistryURL).To(BeEmpty()) Expect(vi.Status.Target.PersistentVolumeClaim).NotTo(BeEmpty()) }) }) @@ -202,7 +141,7 @@ var _ = Describe("ObjectRef VirtualImageSnapshot PersistentVolumeClaim", func() pvc.Status.Phase = corev1.ClaimBound client := fake.NewClientBuilder().WithScheme(scheme).WithObjects(pvc).Build() - syncer := NewObjectRefVirtualDiskSnapshotPVC(importer, stat, bounder, client, nil, recorder) + syncer := NewObjectRefVirtualDiskSnapshotPVC(importer, stat, bounder, client, recorder) res, err := syncer.Sync(ctx, vi) Expect(err).ToNot(HaveOccurred()) @@ -225,7 +164,7 @@ var _ = Describe("ObjectRef VirtualImageSnapshot PersistentVolumeClaim", func() } client := fake.NewClientBuilder().WithScheme(scheme).WithObjects().Build() - syncer := NewObjectRefVirtualDiskSnapshotPVC(importer, stat, nil, client, nil, recorder) + syncer := NewObjectRefVirtualDiskSnapshotPVC(importer, stat, nil, client, recorder) res, err := syncer.Sync(ctx, vi) Expect(err).ToNot(HaveOccurred()) @@ -241,7 +180,7 @@ var _ = Describe("ObjectRef VirtualImageSnapshot PersistentVolumeClaim", func() vi.Status.Target.PersistentVolumeClaim = pvc.Name client := fake.NewClientBuilder().WithScheme(scheme).WithObjects(pvc).Build() - syncer := NewObjectRefVirtualDiskSnapshotPVC(importer, stat, nil, client, nil, recorder) + syncer := NewObjectRefVirtualDiskSnapshotPVC(importer, stat, nil, client, recorder) res, err := syncer.Sync(ctx, vi) Expect(err).ToNot(HaveOccurred()) diff --git a/images/virtualization-artifact/pkg/controller/vi/internal/source/sources_test.go b/images/virtualization-artifact/pkg/controller/vi/internal/source/sources_test.go new file mode 100644 index 0000000000..a9a81a3d60 --- /dev/null +++ b/images/virtualization-artifact/pkg/controller/vi/internal/source/sources_test.go @@ -0,0 +1,164 @@ +/* +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 source + +import ( + "context" + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + corev1 "k8s.io/api/core/v1" + storagev1 "k8s.io/api/storage/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "k8s.io/utils/ptr" + "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/deckhouse/virtualization-controller/pkg/common/annotations" + "github.com/deckhouse/virtualization-controller/pkg/controller/conditions" + importer2 "github.com/deckhouse/virtualization-controller/pkg/controller/importer" + "github.com/deckhouse/virtualization-controller/pkg/controller/service" + "github.com/deckhouse/virtualization-controller/pkg/controller/supplements" + "github.com/deckhouse/virtualization-controller/pkg/eventrecord" + virtv2 "github.com/deckhouse/virtualization/api/core/v1alpha2" + "github.com/deckhouse/virtualization/api/core/v1alpha2/vicondition" +) + +func TestSources(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Sources") +} + +func ExpectCondition(vi *virtv2.VirtualImage, status metav1.ConditionStatus, reason vicondition.ReadyReason, msgExists bool) { + ready, _ := conditions.GetCondition(vicondition.Ready, vi.Status.Conditions) + Expect(ready.Status).To(Equal(status)) + Expect(ready.Reason).To(Equal(reason.String())) + Expect(ready.ObservedGeneration).To(Equal(vi.Generation)) + + if msgExists { + Expect(ready.Message).ToNot(BeEmpty()) + } else { + Expect(ready.Message).To(BeEmpty()) + } +} + +func getVirtualDisk() *virtv2.VirtualDisk { + return &virtv2.VirtualDisk{ + ObjectMeta: metav1.ObjectMeta{ + Name: "vd", + Generation: 1, + UID: "33333333-3333-3333-3333-333333333333", + }, + Status: virtv2.VirtualDiskStatus{ + Capacity: "100Mi", + }, + } +} + +func getVirtualImage(storage virtv2.StorageType, ds virtv2.VirtualImageDataSource) *virtv2.VirtualImage { + return &virtv2.VirtualImage{ + ObjectMeta: metav1.ObjectMeta{ + Name: "vi", + Generation: 1, + UID: "22222222-2222-2222-2222-222222222222", + }, + Spec: virtv2.VirtualImageSpec{ + Storage: storage, + DataSource: ds, + }, + } +} + +func getPod(vi *virtv2.VirtualImage) *corev1.Pod { + supgen := supplements.NewGenerator(annotations.VIShortName, vi.Name, vi.Namespace, vi.UID) + + return &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: supgen.ImporterPod().Name, + }, + } +} + +func getPVC(vi *virtv2.VirtualImage, scName string) *corev1.PersistentVolumeClaim { + supgen := supplements.NewGenerator(annotations.VIShortName, vi.Name, vi.Namespace, vi.UID) + + return &corev1.PersistentVolumeClaim{ + ObjectMeta: metav1.ObjectMeta{ + Name: supgen.PersistentVolumeClaim().Name, + }, + Spec: corev1.PersistentVolumeClaimSpec{ + StorageClassName: ptr.To(scName), + }, + Status: corev1.PersistentVolumeClaimStatus{ + Phase: corev1.ClaimBound, + }, + } +} + +func getServiceMocks() (importer *ImporterMock, disk *DiskMock, stat *StatMock, recorder *eventrecord.EventRecorderLoggerMock, bounder *BounderMock) { + importer = &ImporterMock{ + CleanUpSupplementsFunc: func(_ context.Context, _ *supplements.Generator) (bool, error) { + return false, nil + }, + GetPodSettingsWithPVCFunc: func(_ *metav1.OwnerReference, _ *supplements.Generator, _, _ string) *importer2.PodSettings { + return nil + }, + } + + disk = &DiskMock{ + CleanUpSupplementsFunc: func(_ context.Context, _ *supplements.Generator) (bool, error) { + return false, nil + }, + GetStorageClassFunc: func(_ context.Context, _ string) (*storagev1.StorageClass, error) { + return &storagev1.StorageClass{}, nil + }, + } + + stat = &StatMock{ + GetDVCRImageNameFunc: func(_ *corev1.Pod) string { + return "image" + }, + CheckPodFunc: func(_ *corev1.Pod) error { + return nil + }, + GetSizeFunc: func(_ *corev1.Pod) virtv2.ImageStatusSize { + return virtv2.ImageStatusSize{} + }, + GetCDROMFunc: func(_ *corev1.Pod) bool { + return false + }, + GetFormatFunc: func(_ *corev1.Pod) string { + return "iso" + }, + GetProgressFunc: func(_ types.UID, _ *corev1.Pod, _ string, _ ...service.GetProgressOption) string { + return "N%" + }, + } + + recorder = &eventrecord.EventRecorderLoggerMock{ + EventFunc: func(_ client.Object, _, _, _ string) {}, + } + + bounder = &BounderMock{ + CleanUpSupplementsFunc: func(_ context.Context, _ *supplements.Generator) (bool, error) { + return false, nil + }, + } + + return +} diff --git a/images/virtualization-artifact/pkg/controller/vi/internal/source/step/create_dv_from_vd_step.go b/images/virtualization-artifact/pkg/controller/vi/internal/source/step/create_dv_from_vd_step.go new file mode 100644 index 0000000000..7bcca2b730 --- /dev/null +++ b/images/virtualization-artifact/pkg/controller/vi/internal/source/step/create_dv_from_vd_step.go @@ -0,0 +1,90 @@ +/* +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 step + +import ( + "context" + "fmt" + + "k8s.io/apimachinery/pkg/api/resource" + "k8s.io/apimachinery/pkg/types" + "k8s.io/utils/ptr" + cdiv1 "kubevirt.io/containerized-data-importer-api/pkg/apis/core/v1beta1" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + + "github.com/deckhouse/virtualization-controller/pkg/common/object" + "github.com/deckhouse/virtualization-controller/pkg/controller/conditions" + "github.com/deckhouse/virtualization-controller/pkg/eventrecord" + virtv2 "github.com/deckhouse/virtualization/api/core/v1alpha2" +) + +type CreateDataVolumeFromVirtualDiskStep struct { + dv *cdiv1.DataVolume + recorder eventrecord.EventRecorderLogger + disk CreateDataVolumeStepDisk + client client.Client + cb *conditions.ConditionBuilder +} + +func NewCreateDataVolumeFromVirtualDiskStep( + dv *cdiv1.DataVolume, + recorder eventrecord.EventRecorderLogger, + disk CreateDataVolumeStepDisk, + client client.Client, + cb *conditions.ConditionBuilder, +) *CreateDataVolumeFromVirtualDiskStep { + return &CreateDataVolumeFromVirtualDiskStep{ + dv: dv, + recorder: recorder, + disk: disk, + client: client, + cb: cb, + } +} + +func (s CreateDataVolumeFromVirtualDiskStep) Take(ctx context.Context, vi *virtv2.VirtualImage) (*reconcile.Result, error) { + if s.dv != nil { + return nil, nil + } + + vdRefKey := types.NamespacedName{Name: vi.Spec.DataSource.ObjectRef.Name, Namespace: vi.Namespace} + vdRef, err := object.FetchObject(ctx, vdRefKey, s.client, &virtv2.VirtualDisk{}) + if err != nil { + return nil, fmt.Errorf("fetch vd %q: %w", vdRefKey, err) + } + + if vdRef == nil { + return nil, fmt.Errorf("vd object ref %q is nil", vdRefKey) + } + + vi.Status.SourceUID = ptr.To(vdRef.UID) + + source := &cdiv1.DataVolumeSource{ + PVC: &cdiv1.DataVolumeSourcePVC{ + Name: vdRef.Status.Target.PersistentVolumeClaim, + Namespace: vdRef.Namespace, + }, + } + + size, err := resource.ParseQuantity(vdRef.Status.Capacity) + if err != nil { + return nil, fmt.Errorf("parse quantity: %w", err) + } + + return NewCreateDataVolumeStep(s.dv, s.recorder, s.disk, source, size, s.cb).Take(ctx, vi) +} diff --git a/images/virtualization-artifact/pkg/controller/vi/internal/source/step/create_dv_step.go b/images/virtualization-artifact/pkg/controller/vi/internal/source/step/create_dv_step.go new file mode 100644 index 0000000000..b9771d5ffe --- /dev/null +++ b/images/virtualization-artifact/pkg/controller/vi/internal/source/step/create_dv_step.go @@ -0,0 +1,117 @@ +/* +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 step + +import ( + "context" + "errors" + "fmt" + + corev1 "k8s.io/api/core/v1" + storagev1 "k8s.io/api/storage/v1" + "k8s.io/apimachinery/pkg/api/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + cdiv1 "kubevirt.io/containerized-data-importer-api/pkg/apis/core/v1beta1" + "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/service" + "github.com/deckhouse/virtualization-controller/pkg/controller/supplements" + "github.com/deckhouse/virtualization-controller/pkg/eventrecord" + virtv2 "github.com/deckhouse/virtualization/api/core/v1alpha2" + "github.com/deckhouse/virtualization/api/core/v1alpha2/vicondition" +) + +type CreateDataVolumeStepDisk interface { + StartImmediate(ctx context.Context, size resource.Quantity, sc *storagev1.StorageClass, source *cdiv1.DataVolumeSource, obj service.ObjectKind, sup *supplements.Generator) error + GetStorageClass(ctx context.Context, scName string) (*storagev1.StorageClass, error) +} + +type CreateDataVolumeStep struct { + dv *cdiv1.DataVolume + recorder eventrecord.EventRecorderLogger + disk CreateDataVolumeStepDisk + source *cdiv1.DataVolumeSource + size resource.Quantity + cb *conditions.ConditionBuilder +} + +func NewCreateDataVolumeStep( + dv *cdiv1.DataVolume, + recorder eventrecord.EventRecorderLogger, + disk CreateDataVolumeStepDisk, + source *cdiv1.DataVolumeSource, + size resource.Quantity, + cb *conditions.ConditionBuilder, +) *CreateDataVolumeStep { + return &CreateDataVolumeStep{ + dv: dv, + recorder: recorder, + disk: disk, + source: source, + size: size, + cb: cb, + } +} + +func (s CreateDataVolumeStep) Take(ctx context.Context, vi *virtv2.VirtualImage) (*reconcile.Result, error) { + if s.dv != nil { + return nil, nil + } + + s.recorder.Event( + vi, + corev1.EventTypeNormal, + virtv2.ReasonDataSourceSyncStarted, + "The ObjectRef DataSource import has started", + ) + + supgen := supplements.NewGenerator(annotations.VIShortName, vi.Name, vi.Namespace, vi.UID) + + vi.Status.Progress = "0%" + vi.Status.Target.PersistentVolumeClaim = supgen.PersistentVolumeClaim().Name + + sc, err := s.disk.GetStorageClass(ctx, vi.Status.StorageClassName) + if err != nil { + return nil, fmt.Errorf("get sc: %w", err) + } + + err = s.disk.StartImmediate(ctx, s.size, sc, s.source, vi, supgen) + switch { + case err == nil: + // OK. + case errors.Is(err, service.ErrStorageProfileNotFound): + vi.Status.Phase = virtv2.ImageFailed + s.cb. + Status(metav1.ConditionFalse). + Reason(vicondition.ProvisioningFailed). + Message("StorageProfile not found in the cluster: Please check a StorageClass name in the cluster or set a default StorageClass.") + return &reconcile.Result{}, nil + case errors.Is(err, service.ErrDefaultStorageClassNotFound): + vi.Status.Phase = virtv2.ImagePending + s.cb. + Status(metav1.ConditionFalse). + Reason(vicondition.ProvisioningFailed). + Message("Default StorageClass not found in the cluster: please provide a StorageClass name or set a default StorageClass.") + return &reconcile.Result{}, nil + default: + return nil, fmt.Errorf("start immediate: %w", err) + } + + return nil, nil +} diff --git a/images/virtualization-artifact/pkg/controller/vi/internal/source/step/create_pod_step.go b/images/virtualization-artifact/pkg/controller/vi/internal/source/step/create_pod_step.go index fd1ab6071c..420fad8971 100644 --- a/images/virtualization-artifact/pkg/controller/vi/internal/source/step/create_pod_step.go +++ b/images/virtualization-artifact/pkg/controller/vi/internal/source/step/create_pod_step.go @@ -51,13 +51,16 @@ type CreatePodStepStat interface { GetCDROM(pod *corev1.Pod) bool } +type PodSettingsGetter func(ctx context.Context, vi *virtv2.VirtualImage) (*importer.PodSettings, error) + type CreatePodStep struct { - pod *corev1.Pod - dvcrSettings *dvcr.Settings - recorder eventrecord.EventRecorderLogger - importer CreatePodStepImporter - stat CreatePodStepStat - cb *conditions.ConditionBuilder + pod *corev1.Pod + dvcrSettings *dvcr.Settings + recorder eventrecord.EventRecorderLogger + importer CreatePodStepImporter + stat CreatePodStepStat + getPodSettings PodSettingsGetter + cb *conditions.ConditionBuilder } func NewCreatePodStep( @@ -66,15 +69,17 @@ func NewCreatePodStep( recorder eventrecord.EventRecorderLogger, importer CreatePodStepImporter, stat CreatePodStepStat, + getPodSettings PodSettingsGetter, cb *conditions.ConditionBuilder, ) *CreatePodStep { return &CreatePodStep{ - pod: pod, - dvcrSettings: dvcrSettings, - recorder: recorder, - importer: importer, - stat: stat, - cb: cb, + pod: pod, + dvcrSettings: dvcrSettings, + recorder: recorder, + importer: importer, + stat: stat, + getPodSettings: getPodSettings, + cb: cb, } } @@ -83,14 +88,15 @@ func (s CreatePodStep) Take(ctx context.Context, vi *virtv2.VirtualImage) (*reco return nil, nil } - ownerRef := metav1.NewControllerRef(vi, vi.GroupVersionKind()) supgen := supplements.NewGenerator(annotations.VIShortName, vi.Name, vi.Namespace, vi.UID) - pvcKey := supgen.PersistentVolumeClaim() - podSettings := s.importer.GetPodSettingsWithPVC(ownerRef, supgen, pvcKey.Name, pvcKey.Namespace) - envSettings := s.getEnvSettings(vi, supgen) - err := s.importer.StartWithPodSetting(ctx, envSettings, supgen, datasource.NewCABundleForVMI(vi.GetNamespace(), vi.Spec.DataSource), podSettings) + podSettings, err := s.getPodSettings(ctx, vi) + if err != nil { + return nil, fmt.Errorf("get pod settings: %w", err) + } + + err = s.importer.StartWithPodSetting(ctx, envSettings, supgen, datasource.NewCABundleForVMI(vi.GetNamespace(), vi.Spec.DataSource), podSettings) switch { case err == nil: // OK. diff --git a/images/virtualization-artifact/pkg/controller/vi/internal/source/step/ready_cr_step.go b/images/virtualization-artifact/pkg/controller/vi/internal/source/step/ready_cr_step.go index 308faed65f..5d3012e7ee 100644 --- a/images/virtualization-artifact/pkg/controller/vi/internal/source/step/ready_cr_step.go +++ b/images/virtualization-artifact/pkg/controller/vi/internal/source/step/ready_cr_step.go @@ -53,29 +53,29 @@ type ReadyContainerRegistryStepStat interface { } type ReadyContainerRegistryStep struct { - pod *corev1.Pod - importer ReadyContainerRegistryStepImporter - stat ReadyContainerRegistryStepStat - diskService ReadyContainerRegistryStepDiskService - recorder eventrecord.EventRecorderLogger - cb *conditions.ConditionBuilder + pod *corev1.Pod + importer ReadyContainerRegistryStepImporter + stat ReadyContainerRegistryStepStat + disk ReadyContainerRegistryStepDiskService + recorder eventrecord.EventRecorderLogger + cb *conditions.ConditionBuilder } func NewReadyContainerRegistryStep( pod *corev1.Pod, - diskService ReadyContainerRegistryStepDiskService, importer ReadyContainerRegistryStepImporter, + disk ReadyContainerRegistryStepDiskService, stat ReadyContainerRegistryStepStat, recorder eventrecord.EventRecorderLogger, cb *conditions.ConditionBuilder, ) *ReadyContainerRegistryStep { return &ReadyContainerRegistryStep{ - pod: pod, - importer: importer, - diskService: diskService, - stat: stat, - recorder: recorder, - cb: cb, + pod: pod, + importer: importer, + disk: disk, + stat: stat, + recorder: recorder, + cb: cb, } } @@ -154,7 +154,7 @@ func (s ReadyContainerRegistryStep) cleanUpSupplements(ctx context.Context, vi * return err } - _, err = s.diskService.CleanUpSupplements(ctx, supgen) + _, err = s.disk.CleanUpSupplements(ctx, supgen) if err != nil { return err } diff --git a/images/virtualization-artifact/pkg/controller/vi/internal/source/step/ready_pvc_step.go b/images/virtualization-artifact/pkg/controller/vi/internal/source/step/ready_pvc_step.go index 68cc6cc0da..ef41239006 100644 --- a/images/virtualization-artifact/pkg/controller/vi/internal/source/step/ready_pvc_step.go +++ b/images/virtualization-artifact/pkg/controller/vi/internal/source/step/ready_pvc_step.go @@ -27,6 +27,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/reconcile" "github.com/deckhouse/virtualization-controller/pkg/common/annotations" + "github.com/deckhouse/virtualization-controller/pkg/common/imageformat" "github.com/deckhouse/virtualization-controller/pkg/controller/conditions" "github.com/deckhouse/virtualization-controller/pkg/controller/supplements" "github.com/deckhouse/virtualization-controller/pkg/eventrecord" @@ -125,6 +126,7 @@ func (s ReadyPersistentVolumeClaimStep) Take(ctx context.Context, vi *virtv2.Vir return nil, errors.New("failed to convert quantity to int64") } + vi.Status.Format = imageformat.FormatRaw vi.Status.Size = virtv2.ImageStatusSize{ Stored: res.String(), StoredBytes: strconv.FormatInt(intQ, 10), diff --git a/images/virtualization-artifact/pkg/controller/vi/internal/source/step/set_source_uid_step.go b/images/virtualization-artifact/pkg/controller/vi/internal/source/step/set_source_uid_step.go new file mode 100644 index 0000000000..d7284962a4 --- /dev/null +++ b/images/virtualization-artifact/pkg/controller/vi/internal/source/step/set_source_uid_step.go @@ -0,0 +1,48 @@ +/* +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 step + +import ( + "context" + + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + + virtv2 "github.com/deckhouse/virtualization/api/core/v1alpha2" +) + +type SourceUIDGetter func(ctx context.Context, vi *virtv2.VirtualImage) (*types.UID, error) + +type SetSourceUIDStep struct { + getSourceUID SourceUIDGetter +} + +func NewSetSourceUIDStep(getSourceUID SourceUIDGetter) *SetSourceUIDStep { + return &SetSourceUIDStep{ + getSourceUID: getSourceUID, + } +} + +func (s SetSourceUIDStep) Take(ctx context.Context, vi *virtv2.VirtualImage) (*reconcile.Result, error) { + var err error + vi.Status.SourceUID, err = s.getSourceUID(ctx, vi) + if err != nil { + return nil, err + } + + return nil, nil +} diff --git a/images/virtualization-artifact/pkg/controller/vi/internal/source/step/terminating_step.go b/images/virtualization-artifact/pkg/controller/vi/internal/source/step/terminating_step.go index 6d4efbb907..31bb5ef409 100644 --- a/images/virtualization-artifact/pkg/controller/vi/internal/source/step/terminating_step.go +++ b/images/virtualization-artifact/pkg/controller/vi/internal/source/step/terminating_step.go @@ -20,6 +20,7 @@ import ( "context" corev1 "k8s.io/api/core/v1" + "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/reconcile" "github.com/deckhouse/virtualization-controller/pkg/common/object" @@ -28,12 +29,13 @@ import ( ) type TerminatingStep struct { - pvc *corev1.PersistentVolumeClaim + pvc *corev1.PersistentVolumeClaim + objs []client.Object } -func NewTerminatingStep(pvc *corev1.PersistentVolumeClaim) *TerminatingStep { +func NewTerminatingStep(obj client.Object, objs ...client.Object) *TerminatingStep { return &TerminatingStep{ - pvc: pvc, + objs: append(objs, obj), } } @@ -42,9 +44,9 @@ func (s TerminatingStep) Take(ctx context.Context, _ *virtv2.VirtualImage) (*rec return nil, nil } - if object.IsTerminating(s.pvc) { + if object.AnyTerminating(s.objs...) { log, _ := logger.GetDataSourceContext(ctx, "objectref") - log.Info("The PVC is terminating during an unfinished import process.") + log.Info("Some object is terminating during an unfinished import process.") return &reconcile.Result{Requeue: true}, nil } diff --git a/images/virtualization-artifact/pkg/controller/vi/internal/source/step/wait_for_dv_step.go b/images/virtualization-artifact/pkg/controller/vi/internal/source/step/wait_for_dv_step.go new file mode 100644 index 0000000000..5e1abd12a3 --- /dev/null +++ b/images/virtualization-artifact/pkg/controller/vi/internal/source/step/wait_for_dv_step.go @@ -0,0 +1,174 @@ +/* +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 step + +import ( + "context" + "fmt" + "strings" + + 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" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + + dvutil "github.com/deckhouse/virtualization-controller/pkg/common/datavolume" + "github.com/deckhouse/virtualization-controller/pkg/common/object" + "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/vicondition" +) + +type WaitForDVStepDisk interface { + GetProgress(dv *cdiv1.DataVolume, prevProgress string, opts ...service.GetProgressOption) string +} + +type WaitForDVStep struct { + pvc *corev1.PersistentVolumeClaim + dv *cdiv1.DataVolume + disk WaitForDVStepDisk + client client.Client + cb *conditions.ConditionBuilder +} + +func NewWaitForDVStep( + pvc *corev1.PersistentVolumeClaim, + dv *cdiv1.DataVolume, + disk WaitForDVStepDisk, + client client.Client, + cb *conditions.ConditionBuilder, +) *WaitForDVStep { + return &WaitForDVStep{ + pvc: pvc, + dv: dv, + disk: disk, + client: client, + cb: cb, + } +} + +func (s WaitForDVStep) Take(ctx context.Context, vi *virtv2.VirtualImage) (*reconcile.Result, error) { + if s.dv == nil { + vi.Status.Phase = virtv2.ImageProvisioning + s.cb. + Status(metav1.ConditionFalse). + Reason(vicondition.Provisioning). + Message("Waiting for the VirtualDisk importer to be created.") + return &reconcile.Result{}, nil + } + + vi.Status.Progress = s.disk.GetProgress(s.dv, vi.Status.Progress, service.NewScaleOption(0, 100)) + + ok := s.checkQoutaNotExceededCondition(vi) + if !ok { + return &reconcile.Result{}, nil + } + + ok = s.checkRunningCondition(vi) + if !ok { + return &reconcile.Result{}, nil + } + + ok, err := s.checkImporterPrimePod(ctx, vi) + if err != nil { + return nil, err + } + if !ok { + return &reconcile.Result{}, nil + } + + return nil, nil +} + +func (s WaitForDVStep) checkQoutaNotExceededCondition(vi *virtv2.VirtualImage) (ok bool) { + dvQuotaNotExceededCondition, ok := conditions.GetDataVolumeCondition(conditions.DVQoutaNotExceededConditionType, s.dv.Status.Conditions) + if ok && dvQuotaNotExceededCondition.Status == corev1.ConditionFalse { + vi.Status.Phase = virtv2.ImagePending + s.cb. + Status(metav1.ConditionFalse). + Reason(vicondition.QuotaExceeded). + Message(dvQuotaNotExceededCondition.Message) + return false + } + + return true +} + +func (s WaitForDVStep) checkRunningCondition(vi *virtv2.VirtualImage) (ok bool) { + dvRunningCondition, ok := conditions.GetDataVolumeCondition(conditions.DVRunningConditionType, s.dv.Status.Conditions) + switch { + case dvRunningCondition.Reason == conditions.DVImagePullFailedReason: + vi.Status.Phase = virtv2.ImagePending + s.cb. + Status(metav1.ConditionFalse). + Reason(vicondition.ImagePullFailed). + Message(dvRunningCondition.Message) + return false + case strings.Contains(dvRunningCondition.Reason, "Error"): + vi.Status.Phase = virtv2.ImagePending + s.cb. + Status(metav1.ConditionFalse). + Reason(vicondition.ProvisioningFailed). + Message(dvRunningCondition.Message) + return false + default: + return true + } +} + +func (s WaitForDVStep) checkImporterPrimePod(ctx context.Context, vi *virtv2.VirtualImage) (ok bool, err error) { + if s.pvc == nil { + return true, nil + } + + cdiImporterPrimeKey := types.NamespacedName{ + Namespace: s.pvc.Namespace, + Name: dvutil.GetImporterPrimeName(s.pvc.UID), + } + + cdiImporterPrime, err := object.FetchObject(ctx, cdiImporterPrimeKey, s.client, &corev1.Pod{}) + if err != nil { + return false, fmt.Errorf("fetch importer prime pod: %w", err) + } + + if cdiImporterPrime != nil { + podInitializedCond, _ := conditions.GetPodCondition(corev1.PodInitialized, cdiImporterPrime.Status.Conditions) + if podInitializedCond.Status == corev1.ConditionFalse && strings.Contains(podInitializedCond.Reason, "Error") { + vi.Status.Phase = virtv2.ImagePending + s.cb. + Status(metav1.ConditionFalse). + Reason(vicondition.ImagePullFailed). + Message(fmt.Sprintf("The PVC importer is not initialized; %s error %s: %s", cdiImporterPrimeKey.String(), podInitializedCond.Reason, podInitializedCond.Message)) + return false, nil + } + + podScheduledCond, _ := conditions.GetPodCondition(corev1.PodScheduled, cdiImporterPrime.Status.Conditions) + if podScheduledCond.Status == corev1.ConditionFalse && strings.Contains(podScheduledCond.Reason, "Error") { + vi.Status.Phase = virtv2.ImagePending + s.cb. + Status(metav1.ConditionFalse). + Reason(vicondition.ImagePullFailed). + Message(fmt.Sprintf("The PVC importer is not scheduled; %s error %s: %s", cdiImporterPrimeKey.String(), podScheduledCond.Reason, podScheduledCond.Message)) + return false, nil + } + } + + return true, nil +} diff --git a/images/virtualization-artifact/pkg/controller/vi/internal/source/step/wait_for_pod_step.go b/images/virtualization-artifact/pkg/controller/vi/internal/source/step/wait_for_pod_step.go index 432d2a2ff6..3ef2894b98 100644 --- a/images/virtualization-artifact/pkg/controller/vi/internal/source/step/wait_for_pod_step.go +++ b/images/virtualization-artifact/pkg/controller/vi/internal/source/step/wait_for_pod_step.go @@ -41,20 +41,17 @@ type WaitForPodStepStat interface { type WaitForPodStep struct { pod *corev1.Pod - pvc *corev1.PersistentVolumeClaim stat WaitForPodStepStat cb *conditions.ConditionBuilder } func NewWaitForPodStep( pod *corev1.Pod, - pvc *corev1.PersistentVolumeClaim, stat WaitForPodStepStat, cb *conditions.ConditionBuilder, ) *WaitForPodStep { return &WaitForPodStep{ pod: pod, - pvc: pvc, stat: stat, cb: cb, } diff --git a/images/virtualization-artifact/pkg/controller/vi/internal/source/step/wait_for_pvc_step.go b/images/virtualization-artifact/pkg/controller/vi/internal/source/step/wait_for_pvc_step.go index 83702504a4..554ab6612c 100644 --- a/images/virtualization-artifact/pkg/controller/vi/internal/source/step/wait_for_pvc_step.go +++ b/images/virtualization-artifact/pkg/controller/vi/internal/source/step/wait_for_pvc_step.go @@ -64,7 +64,7 @@ func (s WaitForPVCStep) Take(_ context.Context, vi *virtv2.VirtualImage) (*recon s.cb. Status(metav1.ConditionFalse). Reason(vdcondition.Provisioning). - Message(fmt.Sprintf("Waiting for the PVC %s to be Bound.", s.pvc.Name)) + Message(fmt.Sprintf("Waiting for the underlying PersistentVolumeClaim %s to be Bound.", s.pvc.Name)) return &reconcile.Result{}, nil }