diff --git a/api/core/v1alpha2/virtual_disk.go b/api/core/v1alpha2/virtual_disk.go index 0bf8eb8b0f..71a4c6022b 100644 --- a/api/core/v1alpha2/virtual_disk.go +++ b/api/core/v1alpha2/virtual_disk.go @@ -97,6 +97,9 @@ type VirtualDiskStatsCreationDuration struct { // Waiting time for dependent resources. // +nullable WaitingForDependencies *metav1.Duration `json:"waitingForDependencies,omitempty"` + // Waiting time in the `WaitForFirstConsumer` phase. + // +nullable + WaitingForFirstConsumer *metav1.Duration `json:"waitingForFirstConsumer,omitempty"` // Duration of the loading into DVCR. // +nullable DVCRProvisioning *metav1.Duration `json:"dvcrProvisioning,omitempty"` diff --git a/api/core/v1alpha2/zz_generated.deepcopy.go b/api/core/v1alpha2/zz_generated.deepcopy.go index 8c6001319b..658bce8efe 100644 --- a/api/core/v1alpha2/zz_generated.deepcopy.go +++ b/api/core/v1alpha2/zz_generated.deepcopy.go @@ -1624,6 +1624,11 @@ func (in *VirtualDiskStatsCreationDuration) DeepCopyInto(out *VirtualDiskStatsCr *out = new(v1.Duration) **out = **in } + if in.WaitingForFirstConsumer != nil { + in, out := &in.WaitingForFirstConsumer, &out.WaitingForFirstConsumer + *out = new(v1.Duration) + **out = **in + } if in.DVCRProvisioning != nil { in, out := &in.DVCRProvisioning, &out.DVCRProvisioning *out = new(v1.Duration) diff --git a/crds/doc-ru-virtualdisks.yaml b/crds/doc-ru-virtualdisks.yaml index af3143b254..c24e41c64d 100644 --- a/crds/doc-ru-virtualdisks.yaml +++ b/crds/doc-ru-virtualdisks.yaml @@ -163,6 +163,9 @@ spec: waitingForDependencies: description: | Длительность ожидания зависимостей для создания виртуального диска. + waitingForFirstConsumer: + description: | + Длительность ожидания в фазе `WaitForFirstConsumer`. dvcrProvisioning: description: | Длительность загрузки в Deckhouse Virtualization Container Registry (DVCR). diff --git a/crds/virtualdisks.yaml b/crds/virtualdisks.yaml index 6f720ded92..01eb3e42ed 100644 --- a/crds/virtualdisks.yaml +++ b/crds/virtualdisks.yaml @@ -460,6 +460,10 @@ spec: description: Waiting time for dependent resources. nullable: true type: string + waitingForFirstConsumer: + description: Waiting time in the `WaitForFirstConsumer` phase. + nullable: true + type: string type: object type: object storageClassName: diff --git a/images/virtualization-artifact/pkg/controller/vd/internal/source/http.go b/images/virtualization-artifact/pkg/controller/vd/internal/source/http.go index 696902d2f9..ba2f910fac 100644 --- a/images/virtualization-artifact/pkg/controller/vd/internal/source/http.go +++ b/images/virtualization-artifact/pkg/controller/vd/internal/source/http.go @@ -117,7 +117,7 @@ func (ds HTTPDataSource) Sync(ctx context.Context, vd *v1alpha2.VirtualDisk) (re case IsDiskProvisioningFinished(condition): log.Debug("Disk provisioning finished: clean up") - setPhaseConditionForFinishedDisk(pvc, cb, &vd.Status.Phase, supgen) + setPhaseConditionForFinishedDisk(vd, pvc, cb, &vd.Status.Phase, supgen) // Protect Ready Disk and underlying PVC. err = ds.diskService.Protect(ctx, supgen, vd, nil, pvc) @@ -161,17 +161,14 @@ func (ds HTTPDataSource) Sync(ctx context.Context, vd *v1alpha2.VirtualDisk) (re // OK. case common.ErrQuotaExceeded(err): ds.recorder.Event(vd, corev1.EventTypeWarning, v1alpha2.ReasonDataSourceQuotaExceeded, "DataSource quota exceed") - return setQuotaExceededPhaseCondition(cb, &vd.Status.Phase, err, vd.CreationTimestamp), nil + return setQuotaExceededPhaseCondition(vd, cb, &vd.Status.Phase, err, vd.CreationTimestamp), nil default: - setPhaseConditionToFailed(cb, &vd.Status.Phase, fmt.Errorf("unexpected error: %w", err)) + setPhaseConditionToFailed(vd, cb, &vd.Status.Phase, fmt.Errorf("unexpected error: %w", err)) return reconcile.Result{}, err } vd.Status.Phase = v1alpha2.DiskProvisioning - cb. - Status(metav1.ConditionFalse). - Reason(vdcondition.Provisioning). - Message("DVCR Provisioner not found: create the new one.") + setReadyConditionWithWFFCAccounting(vd, cb, metav1.ConditionFalse, vdcondition.Provisioning, "DVCR Provisioner not found: create the new one.") return reconcile.Result{RequeueAfter: time.Second}, nil case !podutil.IsPodComplete(pod): @@ -188,10 +185,7 @@ func (ds HTTPDataSource) Sync(ctx context.Context, vd *v1alpha2.VirtualDisk) (re } vd.Status.Phase = v1alpha2.DiskProvisioning - cb. - Status(metav1.ConditionFalse). - Reason(vdcondition.Provisioning). - Message("Import is in the process of provisioning to DVCR.") + setReadyConditionWithWFFCAccounting(vd, cb, metav1.ConditionFalse, vdcondition.Provisioning, "Import is in the process of provisioning to DVCR.") vd.Status.Progress = ds.statService.GetProgress(vd.GetUID(), pod, vd.Status.Progress, service.NewScaleOption(0, 50)) vd.Status.DownloadSpeed = ds.statService.GetDownloadSpeed(vd.GetUID(), pod) @@ -199,6 +193,13 @@ func (ds HTTPDataSource) Sync(ctx context.Context, vd *v1alpha2.VirtualDisk) (re if isStorageClassWFFC(sc) && len(vd.Status.AttachedToVirtualMachines) != 1 { vd.Status.Progress = "50%" vd.Status.Phase = v1alpha2.DiskWaitForFirstConsumer + setReadyConditionWithWFFCAccounting( + vd, + cb, + metav1.ConditionFalse, + vdcondition.WaitingForFirstConsumer, + "The provisioning has been suspended: a created and scheduled virtual machine is awaited.", + ) return reconcile.Result{}, nil } @@ -216,10 +217,7 @@ func (ds HTTPDataSource) Sync(ctx context.Context, vd *v1alpha2.VirtualDisk) (re switch { case errors.Is(err, service.ErrProvisioningFailed): ds.recorder.Event(vd, corev1.EventTypeWarning, v1alpha2.ReasonDataSourceDiskProvisioningFailed, "Disk provisioning failed") - cb. - Status(metav1.ConditionFalse). - Reason(vdcondition.ProvisioningFailed). - Message(service.CapitalizeFirstLetter(err.Error() + ".")) + setReadyConditionWithWFFCAccounting(vd, cb, metav1.ConditionFalse, vdcondition.ProvisioningFailed, service.CapitalizeFirstLetter(err.Error()+".")) return reconcile.Result{}, nil default: return reconcile.Result{}, err @@ -230,14 +228,14 @@ func (ds HTTPDataSource) Sync(ctx context.Context, vd *v1alpha2.VirtualDisk) (re vd.Status.DownloadSpeed = ds.statService.GetDownloadSpeed(vd.GetUID(), pod) if imageformat.IsISO(ds.statService.GetFormat(pod)) { - setPhaseConditionToFailed(cb, &vd.Status.Phase, ErrISOSourceNotSupported) + setPhaseConditionToFailed(vd, cb, &vd.Status.Phase, ErrISOSourceNotSupported) return reconcile.Result{}, nil } var diskSize resource.Quantity diskSize, err = ds.getPVCSize(vd, pod) if err != nil { - setPhaseConditionToFailed(cb, &vd.Status.Phase, err) + setPhaseConditionToFailed(vd, cb, &vd.Status.Phase, err) if errors.Is(err, service.ErrInsufficientPVCSize) { return reconcile.Result{}, nil @@ -251,7 +249,7 @@ func (ds HTTPDataSource) Sync(ctx context.Context, vd *v1alpha2.VirtualDisk) (re var nodePlacement *provisioner.NodePlacement nodePlacement, err = getNodePlacement(ctx, ds.client, vd) if err != nil { - setPhaseConditionToFailed(cb, &vd.Status.Phase, fmt.Errorf("unexpected error: %w", err)) + setPhaseConditionToFailed(vd, cb, &vd.Status.Phase, fmt.Errorf("unexpected error: %w", err)) return reconcile.Result{}, fmt.Errorf("failed to get importer tolerations: %w", err) } @@ -261,10 +259,7 @@ func (ds HTTPDataSource) Sync(ctx context.Context, vd *v1alpha2.VirtualDisk) (re } vd.Status.Phase = v1alpha2.DiskProvisioning - cb. - Status(metav1.ConditionFalse). - Reason(vdcondition.Provisioning). - Message("PVC Provisioner not found: create the new one.") + setReadyConditionWithWFFCAccounting(vd, cb, metav1.ConditionFalse, vdcondition.Provisioning, "PVC Provisioner not found: create the new one.") return reconcile.Result{RequeueAfter: time.Second}, nil case dvQuotaNotExceededCondition != nil && dvQuotaNotExceededCondition.Status == corev1.ConditionFalse: @@ -272,28 +267,19 @@ func (ds HTTPDataSource) Sync(ctx context.Context, vd *v1alpha2.VirtualDisk) (re if dv.Status.ClaimName != "" && isStorageClassWFFC(sc) { vd.Status.Phase = v1alpha2.DiskWaitForFirstConsumer } - cb. - Status(metav1.ConditionFalse). - Reason(vdcondition.QuotaExceeded). - Message(dvQuotaNotExceededCondition.Message) + setReadyConditionWithWFFCAccounting(vd, cb, metav1.ConditionFalse, vdcondition.QuotaExceeded, dvQuotaNotExceededCondition.Message) return reconcile.Result{}, nil case dvRunningCondition != nil && dvRunningCondition.Status != corev1.ConditionTrue && dvRunningCondition.Reason == DVImagePullFailedReason: vd.Status.Phase = v1alpha2.DiskPending if dv.Status.ClaimName != "" && isStorageClassWFFC(sc) { vd.Status.Phase = v1alpha2.DiskWaitForFirstConsumer } - cb. - Status(metav1.ConditionFalse). - Reason(vdcondition.ImagePullFailed). - Message(dvRunningCondition.Message) + setReadyConditionWithWFFCAccounting(vd, cb, metav1.ConditionFalse, vdcondition.ImagePullFailed, dvRunningCondition.Message) ds.recorder.Event(vd, corev1.EventTypeWarning, vdcondition.ImagePullFailed.String(), dvRunningCondition.Message) return reconcile.Result{}, nil case pvc == nil: vd.Status.Phase = v1alpha2.DiskProvisioning - cb. - Status(metav1.ConditionFalse). - Reason(vdcondition.Provisioning). - Message("PVC not found: waiting for creation.") + setReadyConditionWithWFFCAccounting(vd, cb, metav1.ConditionFalse, vdcondition.Provisioning, "PVC not found: waiting for creation.") return reconcile.Result{RequeueAfter: time.Second}, nil case ds.diskService.IsImportDone(dv, pvc): log.Info("Import has completed", "dvProgress", dv.Status.Progress, "dvPhase", dv.Status.Phase, "pvcPhase", pvc.Status.Phase) @@ -306,10 +292,7 @@ func (ds HTTPDataSource) Sync(ctx context.Context, vd *v1alpha2.VirtualDisk) (re ) vd.Status.Phase = v1alpha2.DiskReady - cb. - Status(metav1.ConditionTrue). - Reason(vdcondition.Ready). - Message("") + setReadyConditionWithWFFCAccounting(vd, cb, metav1.ConditionTrue, vdcondition.Ready, "") vd.Status.Progress = "100%" vd.Status.Capacity = ds.diskService.GetCapacity(pvc) diff --git a/images/virtualization-artifact/pkg/controller/vd/internal/source/registry.go b/images/virtualization-artifact/pkg/controller/vd/internal/source/registry.go index 3b67262298..2c2bf4119c 100644 --- a/images/virtualization-artifact/pkg/controller/vd/internal/source/registry.go +++ b/images/virtualization-artifact/pkg/controller/vd/internal/source/registry.go @@ -119,7 +119,7 @@ func (ds RegistryDataSource) Sync(ctx context.Context, vd *v1alpha2.VirtualDisk) case IsDiskProvisioningFinished(condition): log.Debug("Disk provisioning finished: clean up") - setPhaseConditionForFinishedDisk(pvc, cb, &vd.Status.Phase, supgen) + setPhaseConditionForFinishedDisk(vd, pvc, cb, &vd.Status.Phase, supgen) // Protect Ready Disk and underlying PVC. err = ds.diskService.Protect(ctx, supgen, vd, nil, pvc) @@ -163,17 +163,14 @@ func (ds RegistryDataSource) Sync(ctx context.Context, vd *v1alpha2.VirtualDisk) // OK. case common.ErrQuotaExceeded(err): ds.recorder.Event(vd, corev1.EventTypeWarning, v1alpha2.ReasonDataSourceQuotaExceeded, "DataSource quota exceed") - return setQuotaExceededPhaseCondition(cb, &vd.Status.Phase, err, vd.CreationTimestamp), nil + return setQuotaExceededPhaseCondition(vd, cb, &vd.Status.Phase, err, vd.CreationTimestamp), nil default: - setPhaseConditionToFailed(cb, &vd.Status.Phase, fmt.Errorf("unexpected error: %w", err)) + setPhaseConditionToFailed(vd, cb, &vd.Status.Phase, fmt.Errorf("unexpected error: %w", err)) return reconcile.Result{}, err } vd.Status.Phase = v1alpha2.DiskPending - cb. - Status(metav1.ConditionFalse). - Reason(vdcondition.WaitForUserUpload). - Message("DVCR Provisioner not found: create the new one.") + setReadyConditionWithWFFCAccounting(vd, cb, metav1.ConditionFalse, vdcondition.WaitForUserUpload, "DVCR Provisioner not found: create the new one.") return reconcile.Result{RequeueAfter: time.Second}, nil case !podutil.IsPodComplete(pod): @@ -185,10 +182,7 @@ func (ds RegistryDataSource) Sync(ctx context.Context, vd *v1alpha2.VirtualDisk) } vd.Status.Phase = v1alpha2.DiskProvisioning - cb. - Status(metav1.ConditionFalse). - Reason(vdcondition.Provisioning). - Message("DVCR Provisioner not found: create the new one.") + setReadyConditionWithWFFCAccounting(vd, cb, metav1.ConditionFalse, vdcondition.Provisioning, "DVCR Provisioner not found: create the new one.") vd.Status.Progress = ds.statService.GetProgress(vd.GetUID(), pod, vd.Status.Progress, service.NewScaleOption(0, 50)) @@ -200,6 +194,13 @@ func (ds RegistryDataSource) Sync(ctx context.Context, vd *v1alpha2.VirtualDisk) if isStorageClassWFFC(sc) && len(vd.Status.AttachedToVirtualMachines) != 1 { vd.Status.Progress = "50%" vd.Status.Phase = v1alpha2.DiskWaitForFirstConsumer + setReadyConditionWithWFFCAccounting( + vd, + cb, + metav1.ConditionFalse, + vdcondition.WaitingForFirstConsumer, + "The provisioning has been suspended: a created and scheduled virtual machine is awaited.", + ) return reconcile.Result{}, nil } @@ -217,10 +218,7 @@ func (ds RegistryDataSource) Sync(ctx context.Context, vd *v1alpha2.VirtualDisk) switch { case errors.Is(err, service.ErrProvisioningFailed): ds.recorder.Event(vd, corev1.EventTypeWarning, v1alpha2.ReasonDataSourceDiskProvisioningFailed, "Disk provisioning failed") - cb. - Status(metav1.ConditionFalse). - Reason(vdcondition.ProvisioningFailed). - Message(service.CapitalizeFirstLetter(err.Error() + ".")) + setReadyConditionWithWFFCAccounting(vd, cb, metav1.ConditionFalse, vdcondition.ProvisioningFailed, service.CapitalizeFirstLetter(err.Error()+".")) return reconcile.Result{}, nil default: return reconcile.Result{}, err @@ -230,14 +228,14 @@ func (ds RegistryDataSource) Sync(ctx context.Context, vd *v1alpha2.VirtualDisk) vd.Status.Progress = "50%" if imageformat.IsISO(ds.statService.GetFormat(pod)) { - setPhaseConditionToFailed(cb, &vd.Status.Phase, ErrISOSourceNotSupported) + setPhaseConditionToFailed(vd, cb, &vd.Status.Phase, ErrISOSourceNotSupported) return reconcile.Result{}, nil } var diskSize resource.Quantity diskSize, err = ds.getPVCSize(vd, pod) if err != nil { - setPhaseConditionToFailed(cb, &vd.Status.Phase, err) + setPhaseConditionToFailed(vd, cb, &vd.Status.Phase, err) if errors.Is(err, service.ErrInsufficientPVCSize) { return reconcile.Result{}, nil @@ -251,7 +249,7 @@ func (ds RegistryDataSource) Sync(ctx context.Context, vd *v1alpha2.VirtualDisk) var nodePlacement *provisioner.NodePlacement nodePlacement, err = getNodePlacement(ctx, ds.client, vd) if err != nil { - setPhaseConditionToFailed(cb, &vd.Status.Phase, fmt.Errorf("unexpected error: %w", err)) + setPhaseConditionToFailed(vd, cb, &vd.Status.Phase, fmt.Errorf("unexpected error: %w", err)) return reconcile.Result{}, fmt.Errorf("failed to get importer tolerations: %w", err) } @@ -266,10 +264,7 @@ func (ds RegistryDataSource) Sync(ctx context.Context, vd *v1alpha2.VirtualDisk) return reconcile.Result{}, err } vd.Status.Phase = v1alpha2.DiskProvisioning - cb. - Status(metav1.ConditionFalse). - Reason(vdcondition.Provisioning). - Message("PVC Provisioner not found: create the new one.") + setReadyConditionWithWFFCAccounting(vd, cb, metav1.ConditionFalse, vdcondition.Provisioning, "PVC Provisioner not found: create the new one.") return reconcile.Result{RequeueAfter: time.Second}, nil case dvQuotaNotExceededCondition != nil && dvQuotaNotExceededCondition.Status == corev1.ConditionFalse: @@ -277,28 +272,19 @@ func (ds RegistryDataSource) Sync(ctx context.Context, vd *v1alpha2.VirtualDisk) if dv.Status.ClaimName != "" && isStorageClassWFFC(sc) { vd.Status.Phase = v1alpha2.DiskWaitForFirstConsumer } - cb. - Status(metav1.ConditionFalse). - Reason(vdcondition.QuotaExceeded). - Message(dvQuotaNotExceededCondition.Message) + setReadyConditionWithWFFCAccounting(vd, cb, metav1.ConditionFalse, vdcondition.QuotaExceeded, dvQuotaNotExceededCondition.Message) return reconcile.Result{}, nil case dvRunningCondition != nil && dvRunningCondition.Status != corev1.ConditionTrue && dvRunningCondition.Reason == DVImagePullFailedReason: vd.Status.Phase = v1alpha2.DiskPending if dv.Status.ClaimName != "" && isStorageClassWFFC(sc) { vd.Status.Phase = v1alpha2.DiskWaitForFirstConsumer } - cb. - Status(metav1.ConditionFalse). - Reason(vdcondition.ImagePullFailed). - Message(dvRunningCondition.Message) + setReadyConditionWithWFFCAccounting(vd, cb, metav1.ConditionFalse, vdcondition.ImagePullFailed, dvRunningCondition.Message) ds.recorder.Event(vd, corev1.EventTypeWarning, vdcondition.ImagePullFailed.String(), dvRunningCondition.Message) return reconcile.Result{}, nil case pvc == nil: vd.Status.Phase = v1alpha2.DiskProvisioning - cb. - Status(metav1.ConditionFalse). - Reason(vdcondition.Provisioning). - Message("PVC not found: waiting for creation.") + setReadyConditionWithWFFCAccounting(vd, cb, metav1.ConditionFalse, vdcondition.Provisioning, "PVC not found: waiting for creation.") return reconcile.Result{RequeueAfter: time.Second}, nil case ds.diskService.IsImportDone(dv, pvc): log.Info("Import has completed", "dvProgress", dv.Status.Progress, "dvPhase", dv.Status.Phase, "pvcPhase", pvc.Status.Phase) @@ -311,10 +297,7 @@ func (ds RegistryDataSource) Sync(ctx context.Context, vd *v1alpha2.VirtualDisk) ) vd.Status.Phase = v1alpha2.DiskReady - cb. - Status(metav1.ConditionTrue). - Reason(vdcondition.Ready). - Message("") + setReadyConditionWithWFFCAccounting(vd, cb, metav1.ConditionTrue, vdcondition.Ready, "") vd.Status.Progress = "100%" vd.Status.Capacity = ds.diskService.GetCapacity(pvc) diff --git a/images/virtualization-artifact/pkg/controller/vd/internal/source/sources.go b/images/virtualization-artifact/pkg/controller/vd/internal/source/sources.go index b01c4b8579..bbac959659 100644 --- a/images/virtualization-artifact/pkg/controller/vd/internal/source/sources.go +++ b/images/virtualization-artifact/pkg/controller/vd/internal/source/sources.go @@ -101,6 +101,7 @@ func IsDiskProvisioningFinished(c metav1.Condition) bool { } func setPhaseConditionForFinishedDisk( + vd *v1alpha2.VirtualDisk, pvc *corev1.PersistentVolumeClaim, cb *conditions.ConditionBuilder, phase *v1alpha2.DiskPhase, @@ -110,25 +111,18 @@ func setPhaseConditionForFinishedDisk( switch { case pvc == nil: newPhase = v1alpha2.DiskLost - cb. - Status(metav1.ConditionFalse). - Reason(vdcondition.Lost). - Message(fmt.Sprintf("PVC %s not found.", supgen.PersistentVolumeClaim().String())) + setReadyConditionWithWFFCAccounting(vd, cb, metav1.ConditionFalse, vdcondition.Lost, fmt.Sprintf("PVC %s not found.", supgen.PersistentVolumeClaim().String())) case pvc.Status.Phase == corev1.ClaimLost: - cb.Status(metav1.ConditionFalse) if pvc.GetAnnotations()[annotations.AnnDataExportRequest] == "true" { newPhase = v1alpha2.DiskExporting - cb.Reason(vdcondition.Exporting).Message("PV is being exported") + setReadyConditionWithWFFCAccounting(vd, cb, metav1.ConditionFalse, vdcondition.Exporting, "PV is being exported") } else { newPhase = v1alpha2.DiskLost - cb.Reason(vdcondition.Lost).Message(fmt.Sprintf("PV %s not found.", pvc.Spec.VolumeName)) + setReadyConditionWithWFFCAccounting(vd, cb, metav1.ConditionFalse, vdcondition.Lost, fmt.Sprintf("PV %s not found.", pvc.Spec.VolumeName)) } default: newPhase = v1alpha2.DiskReady - cb. - Status(metav1.ConditionTrue). - Reason(vdcondition.Ready). - Message("") + setReadyConditionWithWFFCAccounting(vd, cb, metav1.ConditionTrue, vdcondition.Ready, "") } if phase != nil && string(newPhase) != "" { *phase = newPhase @@ -145,17 +139,23 @@ func setPhaseConditionFromStorageError(err error, vd *v1alpha2.VirtualDisk, cb * return false, nil case errors.Is(err, volumemode.ErrStorageProfileNotFound): vd.Status.Phase = v1alpha2.DiskPending - cb. - Status(metav1.ConditionFalse). - Reason(vdcondition.ProvisioningFailed). - Message("StorageProfile not found in the cluster: Please check a StorageClass name in the cluster or set a default StorageClass.") + setReadyConditionWithWFFCAccounting( + vd, + cb, + metav1.ConditionFalse, + vdcondition.ProvisioningFailed, + "StorageProfile not found in the cluster: Please check a StorageClass name in the cluster or set a default StorageClass.", + ) return true, nil case errors.Is(err, service.ErrDefaultStorageClassNotFound): vd.Status.Phase = v1alpha2.DiskPending - cb. - Status(metav1.ConditionFalse). - Reason(vdcondition.ProvisioningFailed). - Message("Default StorageClass not found in the cluster: please provide a StorageClass name or set a default StorageClass.") + setReadyConditionWithWFFCAccounting( + vd, + cb, + metav1.ConditionFalse, + vdcondition.ProvisioningFailed, + "Default StorageClass not found in the cluster: please provide a StorageClass name or set a default StorageClass.", + ) return true, nil default: return false, err @@ -176,10 +176,7 @@ func setPhaseConditionForPVCProvisioningDisk( case err == nil: if dv == nil { vd.Status.Phase = v1alpha2.DiskProvisioning - cb. - Status(metav1.ConditionFalse). - Reason(vdcondition.Provisioning). - Message("Waiting for the pvc importer to be created") + setReadyConditionWithWFFCAccounting(vd, cb, metav1.ConditionFalse, vdcondition.Provisioning, "Waiting for the pvc importer to be created") return nil } @@ -194,23 +191,56 @@ func setPhaseConditionForPVCProvisioningDisk( } vd.Status.Phase = v1alpha2.DiskProvisioning - cb. - Status(metav1.ConditionFalse). - Reason(vdcondition.Provisioning). - Message("Import is in the process of provisioning to PVC.") + setReadyConditionWithWFFCAccounting(vd, cb, metav1.ConditionFalse, vdcondition.Provisioning, "Import is in the process of provisioning to PVC.") return nil case errors.Is(err, service.ErrDataVolumeNotRunning): vd.Status.Phase = v1alpha2.DiskFailed - cb. - Status(metav1.ConditionFalse). - Reason(vdcondition.ProvisioningFailed). - Message(service.CapitalizeFirstLetter(err.Error())) + setReadyConditionWithWFFCAccounting(vd, cb, metav1.ConditionFalse, vdcondition.ProvisioningFailed, service.CapitalizeFirstLetter(err.Error())) return nil default: return err } } +func addWaitingForFirstConsumerDuration(vd *v1alpha2.VirtualDisk, nextReason string) { + readyCondition, ok := conditions.GetCondition(vdcondition.ReadyType, vd.Status.Conditions) + if !ok || readyCondition.Reason != vdcondition.WaitingForFirstConsumer.String() || nextReason == vdcondition.WaitingForFirstConsumer.String() { + return + } + + if readyCondition.LastTransitionTime.IsZero() { + return + } + + wffcDuration := time.Since(readyCondition.LastTransitionTime.Time).Truncate(time.Second) + if wffcDuration <= 0 { + return + } + + if vd.Status.Stats.CreationDuration.WaitingForFirstConsumer == nil { + vd.Status.Stats.CreationDuration.WaitingForFirstConsumer = &metav1.Duration{ + Duration: wffcDuration, + } + return + } + + vd.Status.Stats.CreationDuration.WaitingForFirstConsumer.Duration += wffcDuration +} + +func setReadyConditionWithWFFCAccounting( + vd *v1alpha2.VirtualDisk, + cb *conditions.ConditionBuilder, + status metav1.ConditionStatus, + reason vdcondition.ReadyReason, + message string, +) { + addWaitingForFirstConsumerDuration(vd, reason.String()) + cb. + Status(status). + Reason(reason). + Message(message) +} + func setPhaseConditionFromPodError( ctx context.Context, podErr error, @@ -222,51 +252,42 @@ func setPhaseConditionFromPodError( switch { case errors.Is(podErr, service.ErrNotInitialized): vd.Status.Phase = v1alpha2.DiskFailed - cb. - Status(metav1.ConditionFalse). - Reason(vdcondition.ProvisioningNotStarted). - Message(service.CapitalizeFirstLetter(podErr.Error()) + ".") + setReadyConditionWithWFFCAccounting(vd, cb, metav1.ConditionFalse, vdcondition.ProvisioningNotStarted, service.CapitalizeFirstLetter(podErr.Error())+".") return nil case errors.Is(podErr, service.ErrNotScheduled): vd.Status.Phase = v1alpha2.DiskPending nodePlacement, err := getNodePlacement(ctx, c, vd) if err != nil { - setPhaseConditionToFailed(cb, &vd.Status.Phase, fmt.Errorf("unexpected error: %w", err)) + setPhaseConditionToFailed(vd, cb, &vd.Status.Phase, fmt.Errorf("unexpected error: %w", err)) return fmt.Errorf("failed to get importer tolerations: %w", err) } var isChanged bool isChanged, err = provisioner.IsNodePlacementChanged(nodePlacement, pod) if err != nil { - setPhaseConditionToFailed(cb, &vd.Status.Phase, fmt.Errorf("unexpected error: %w", err)) + setPhaseConditionToFailed(vd, cb, &vd.Status.Phase, fmt.Errorf("unexpected error: %w", err)) return err } if isChanged { err = c.Delete(ctx, pod) if err != nil { - setPhaseConditionToFailed(cb, &vd.Status.Phase, fmt.Errorf("unexpected error: %w", err)) + setPhaseConditionToFailed(vd, cb, &vd.Status.Phase, fmt.Errorf("unexpected error: %w", err)) return err } - cb. - Status(metav1.ConditionFalse). - Reason(vdcondition.ProvisioningNotStarted). - Message("Provisioner recreation due to a changes in the virtual machine tolerations.") + setReadyConditionWithWFFCAccounting(vd, cb, metav1.ConditionFalse, vdcondition.ProvisioningNotStarted, "Provisioner recreation due to a changes in the virtual machine tolerations.") } else { - cb. - Status(metav1.ConditionFalse). - Reason(vdcondition.ProvisioningNotStarted). - Message(service.CapitalizeFirstLetter(podErr.Error()) + ".") + setReadyConditionWithWFFCAccounting(vd, cb, metav1.ConditionFalse, vdcondition.ProvisioningNotStarted, service.CapitalizeFirstLetter(podErr.Error())+".") } return nil case errors.Is(podErr, service.ErrProvisioningFailed): - setPhaseConditionToFailed(cb, &vd.Status.Phase, podErr) + setPhaseConditionToFailed(vd, cb, &vd.Status.Phase, podErr) return nil default: - setPhaseConditionToFailed(cb, &vd.Status.Phase, fmt.Errorf("unexpected error: %w", podErr)) + setPhaseConditionToFailed(vd, cb, &vd.Status.Phase, fmt.Errorf("unexpected error: %w", podErr)) return podErr } } @@ -289,14 +310,14 @@ func setPhaseConditionFromProvisioningError( nodePlacement, err := getNodePlacement(ctx, c, vd) if err != nil { err = errors.Join(provisioningErr, err) - setPhaseConditionToFailed(cb, &vd.Status.Phase, err) + setPhaseConditionToFailed(vd, cb, &vd.Status.Phase, err) return err } isChanged, err := provisioner.IsNodePlacementChanged(nodePlacement, dv) if err != nil { err = errors.Join(provisioningErr, err) - setPhaseConditionToFailed(cb, &vd.Status.Phase, err) + setPhaseConditionToFailed(vd, cb, &vd.Status.Phase, err) return err } @@ -308,24 +329,18 @@ func setPhaseConditionFromProvisioningError( _, err = cleaner.CleanUp(ctx, supgen) if err != nil { err = errors.Join(provisioningErr, err) - setPhaseConditionToFailed(cb, &vd.Status.Phase, err) + setPhaseConditionToFailed(vd, cb, &vd.Status.Phase, err) return err } - cb. - Status(metav1.ConditionFalse). - Reason(vdcondition.Provisioning). - Message("PVC provisioner recreation due to a changes in the virtual machine tolerations.") + setReadyConditionWithWFFCAccounting(vd, cb, metav1.ConditionFalse, vdcondition.Provisioning, "PVC provisioner recreation due to a changes in the virtual machine tolerations.") } else { - cb. - Status(metav1.ConditionFalse). - Reason(vdcondition.Provisioning). - Message("Trying to schedule the PVC provisioner.") + setReadyConditionWithWFFCAccounting(vd, cb, metav1.ConditionFalse, vdcondition.Provisioning, "Trying to schedule the PVC provisioner.") } return nil default: - setPhaseConditionToFailed(cb, &vd.Status.Phase, provisioningErr) + setPhaseConditionToFailed(vd, cb, &vd.Status.Phase, provisioningErr) return provisioningErr } } @@ -337,11 +352,9 @@ func getNodePlacement(ctx context.Context, c client.Client, vd *v1alpha2.Virtual const retryPeriod = 1 -func setQuotaExceededPhaseCondition(cb *conditions.ConditionBuilder, phase *v1alpha2.DiskPhase, err error, creationTimestamp metav1.Time) reconcile.Result { +func setQuotaExceededPhaseCondition(vd *v1alpha2.VirtualDisk, cb *conditions.ConditionBuilder, phase *v1alpha2.DiskPhase, err error, creationTimestamp metav1.Time) reconcile.Result { *phase = v1alpha2.DiskFailed - cb. - Status(metav1.ConditionFalse). - Reason(vdcondition.ProvisioningFailed) + setReadyConditionWithWFFCAccounting(vd, cb, metav1.ConditionFalse, vdcondition.ProvisioningFailed, "") if creationTimestamp.Add(30 * time.Minute).After(time.Now()) { cb.Message(fmt.Sprintf("Quota exceeded: %s; Please configure quotas or try recreating the resource later.", err)) @@ -352,12 +365,9 @@ func setQuotaExceededPhaseCondition(cb *conditions.ConditionBuilder, phase *v1al return reconcile.Result{RequeueAfter: retryPeriod * time.Minute} } -func setPhaseConditionToFailed(cb *conditions.ConditionBuilder, phase *v1alpha2.DiskPhase, err error) { +func setPhaseConditionToFailed(vd *v1alpha2.VirtualDisk, cb *conditions.ConditionBuilder, phase *v1alpha2.DiskPhase, err error) { *phase = v1alpha2.DiskFailed - cb. - Status(metav1.ConditionFalse). - Reason(vdcondition.ProvisioningFailed). - Message(service.CapitalizeFirstLetter(err.Error()) + ".") + setReadyConditionWithWFFCAccounting(vd, cb, metav1.ConditionFalse, vdcondition.ProvisioningFailed, service.CapitalizeFirstLetter(err.Error())+".") } func isStorageClassWFFC(sc *storagev1.StorageClass) bool { diff --git a/images/virtualization-artifact/pkg/controller/vd/internal/source/upload.go b/images/virtualization-artifact/pkg/controller/vd/internal/source/upload.go index 62bd37e8c2..a13fbbb404 100644 --- a/images/virtualization-artifact/pkg/controller/vd/internal/source/upload.go +++ b/images/virtualization-artifact/pkg/controller/vd/internal/source/upload.go @@ -136,7 +136,7 @@ func (ds UploadDataSource) Sync(ctx context.Context, vd *v1alpha2.VirtualDisk) ( case IsDiskProvisioningFinished(condition): log.Debug("Disk provisioning finished: clean up") - setPhaseConditionForFinishedDisk(pvc, cb, &vd.Status.Phase, supgen) + setPhaseConditionForFinishedDisk(vd, pvc, cb, &vd.Status.Phase, supgen) // Protect Ready Disk and underlying PVC. err = ds.diskService.Protect(ctx, supgen.Generator, vd, nil, pvc) @@ -180,17 +180,14 @@ func (ds UploadDataSource) Sync(ctx context.Context, vd *v1alpha2.VirtualDisk) ( // OK. case common.ErrQuotaExceeded(err): ds.recorder.Event(vd, corev1.EventTypeWarning, v1alpha2.ReasonDataSourceQuotaExceeded, "DataSource quota exceed") - return setQuotaExceededPhaseCondition(cb, &vd.Status.Phase, err, vd.CreationTimestamp), nil + return setQuotaExceededPhaseCondition(vd, cb, &vd.Status.Phase, err, vd.CreationTimestamp), nil default: - setPhaseConditionToFailed(cb, &vd.Status.Phase, fmt.Errorf("unexpected error: %w", err)) + setPhaseConditionToFailed(vd, cb, &vd.Status.Phase, fmt.Errorf("unexpected error: %w", err)) return reconcile.Result{}, err } vd.Status.Phase = v1alpha2.DiskPending - cb. - Status(metav1.ConditionFalse). - Reason(vdcondition.WaitForUserUpload). - Message("DVCR Provisioner not found: create the new one.") + setReadyConditionWithWFFCAccounting(vd, cb, metav1.ConditionFalse, vdcondition.WaitForUserUpload, "DVCR Provisioner not found: create the new one.") return reconcile.Result{RequeueAfter: time.Second}, nil case !podutil.IsPodComplete(pod): @@ -204,10 +201,7 @@ func (ds UploadDataSource) Sync(ctx context.Context, vd *v1alpha2.VirtualDisk) ( log.Info("Waiting for the user upload", "pod.phase", pod.Status.Phase) vd.Status.Phase = v1alpha2.DiskWaitForUserUpload - cb. - Status(metav1.ConditionFalse). - Reason(vdcondition.WaitForUserUpload). - Message("Waiting for the user upload.") + setReadyConditionWithWFFCAccounting(vd, cb, metav1.ConditionFalse, vdcondition.WaitForUserUpload, "Waiting for the user upload.") vd.Status.ImageUploadURLs = &v1alpha2.ImageUploadURLs{ External: ds.uploaderService.GetExternalURL(ctx, ing), InCluster: ds.uploaderService.GetInClusterURL(ctx, svc), @@ -216,10 +210,7 @@ func (ds UploadDataSource) Sync(ctx context.Context, vd *v1alpha2.VirtualDisk) ( log.Info("Waiting for the uploader to be ready to process the user's upload", "pod.phase", pod.Status.Phase) vd.Status.Phase = v1alpha2.DiskPending - cb. - Status(metav1.ConditionFalse). - Reason(vdcondition.ProvisioningNotStarted). - Message(fmt.Sprintf("Waiting for the uploader %q to be ready to process the user's upload.", pod.Name)) + setReadyConditionWithWFFCAccounting(vd, cb, metav1.ConditionFalse, vdcondition.ProvisioningNotStarted, fmt.Sprintf("Waiting for the uploader %q to be ready to process the user's upload.", pod.Name)) } return reconcile.Result{RequeueAfter: time.Second}, nil @@ -228,10 +219,7 @@ func (ds UploadDataSource) Sync(ctx context.Context, vd *v1alpha2.VirtualDisk) ( log.Info("Provisioning to DVCR is in progress", "podPhase", pod.Status.Phase) vd.Status.Phase = v1alpha2.DiskProvisioning - cb. - Status(metav1.ConditionFalse). - Reason(vdcondition.Provisioning). - Message("Import is in the process of provisioning to DVCR.") + setReadyConditionWithWFFCAccounting(vd, cb, metav1.ConditionFalse, vdcondition.Provisioning, "Import is in the process of provisioning to DVCR.") vd.Status.Progress = ds.statService.GetProgress(vd.GetUID(), pod, vd.Status.Progress, service.NewScaleOption(0, 50)) vd.Status.DownloadSpeed = ds.statService.GetDownloadSpeed(vd.GetUID(), pod) @@ -244,6 +232,13 @@ func (ds UploadDataSource) Sync(ctx context.Context, vd *v1alpha2.VirtualDisk) ( if isStorageClassWFFC(sc) && len(vd.Status.AttachedToVirtualMachines) != 1 { vd.Status.Progress = "50%" vd.Status.Phase = v1alpha2.DiskWaitForFirstConsumer + setReadyConditionWithWFFCAccounting( + vd, + cb, + metav1.ConditionFalse, + vdcondition.WaitingForFirstConsumer, + "The provisioning has been suspended: a created and scheduled virtual machine is awaited.", + ) return reconcile.Result{}, nil } @@ -261,10 +256,7 @@ func (ds UploadDataSource) Sync(ctx context.Context, vd *v1alpha2.VirtualDisk) ( switch { case errors.Is(err, service.ErrProvisioningFailed): ds.recorder.Event(vd, corev1.EventTypeWarning, v1alpha2.ReasonDataSourceDiskProvisioningFailed, "Disk provisioning failed") - cb. - Status(metav1.ConditionFalse). - Reason(vdcondition.ProvisioningFailed). - Message(service.CapitalizeFirstLetter(err.Error() + ".")) + setReadyConditionWithWFFCAccounting(vd, cb, metav1.ConditionFalse, vdcondition.ProvisioningFailed, service.CapitalizeFirstLetter(err.Error()+".")) return reconcile.Result{}, nil default: return reconcile.Result{}, err @@ -275,14 +267,14 @@ func (ds UploadDataSource) Sync(ctx context.Context, vd *v1alpha2.VirtualDisk) ( vd.Status.DownloadSpeed = ds.statService.GetDownloadSpeed(vd.GetUID(), pod) if imageformat.IsISO(ds.statService.GetFormat(pod)) { - setPhaseConditionToFailed(cb, &vd.Status.Phase, ErrISOSourceNotSupported) + setPhaseConditionToFailed(vd, cb, &vd.Status.Phase, ErrISOSourceNotSupported) return reconcile.Result{}, nil } var diskSize resource.Quantity diskSize, err = ds.getPVCSize(vd, pod) if err != nil { - setPhaseConditionToFailed(cb, &vd.Status.Phase, err) + setPhaseConditionToFailed(vd, cb, &vd.Status.Phase, err) if errors.Is(err, service.ErrInsufficientPVCSize) { return reconcile.Result{}, nil @@ -302,7 +294,7 @@ func (ds UploadDataSource) Sync(ctx context.Context, vd *v1alpha2.VirtualDisk) ( var nodePlacement *provisioner.NodePlacement nodePlacement, err = getNodePlacement(ctx, ds.client, vd) if err != nil { - setPhaseConditionToFailed(cb, &vd.Status.Phase, fmt.Errorf("unexpected error: %w", err)) + setPhaseConditionToFailed(vd, cb, &vd.Status.Phase, fmt.Errorf("unexpected error: %w", err)) return reconcile.Result{}, fmt.Errorf("failed to get importer tolerations: %w", err) } @@ -312,10 +304,7 @@ func (ds UploadDataSource) Sync(ctx context.Context, vd *v1alpha2.VirtualDisk) ( } vd.Status.Phase = v1alpha2.DiskProvisioning - cb. - Status(metav1.ConditionFalse). - Reason(vdcondition.Provisioning). - Message("PVC Provisioner not found: create the new one.") + setReadyConditionWithWFFCAccounting(vd, cb, metav1.ConditionFalse, vdcondition.Provisioning, "PVC Provisioner not found: create the new one.") return reconcile.Result{RequeueAfter: time.Second}, nil case dvQuotaNotExceededCondition != nil && dvQuotaNotExceededCondition.Status == corev1.ConditionFalse: @@ -323,28 +312,19 @@ func (ds UploadDataSource) Sync(ctx context.Context, vd *v1alpha2.VirtualDisk) ( if dv.Status.ClaimName != "" && isStorageClassWFFC(sc) { vd.Status.Phase = v1alpha2.DiskWaitForFirstConsumer } - cb. - Status(metav1.ConditionFalse). - Reason(vdcondition.QuotaExceeded). - Message(dvQuotaNotExceededCondition.Message) + setReadyConditionWithWFFCAccounting(vd, cb, metav1.ConditionFalse, vdcondition.QuotaExceeded, dvQuotaNotExceededCondition.Message) return reconcile.Result{}, nil case dvRunningCondition != nil && dvRunningCondition.Status != corev1.ConditionTrue && dvRunningCondition.Reason == DVImagePullFailedReason: vd.Status.Phase = v1alpha2.DiskPending if dv.Status.ClaimName != "" && isStorageClassWFFC(sc) { vd.Status.Phase = v1alpha2.DiskWaitForFirstConsumer } - cb. - Status(metav1.ConditionFalse). - Reason(vdcondition.ImagePullFailed). - Message(dvRunningCondition.Message) + setReadyConditionWithWFFCAccounting(vd, cb, metav1.ConditionFalse, vdcondition.ImagePullFailed, dvRunningCondition.Message) ds.recorder.Event(vd, corev1.EventTypeWarning, vdcondition.ImagePullFailed.String(), dvRunningCondition.Message) return reconcile.Result{}, nil case pvc == nil: vd.Status.Phase = v1alpha2.DiskProvisioning - cb. - Status(metav1.ConditionFalse). - Reason(vdcondition.Provisioning). - Message("PVC not found: waiting for creation.") + setReadyConditionWithWFFCAccounting(vd, cb, metav1.ConditionFalse, vdcondition.Provisioning, "PVC not found: waiting for creation.") return reconcile.Result{RequeueAfter: time.Second}, nil case ds.diskService.IsImportDone(dv, pvc): log.Info("Import has completed", "dvProgress", dv.Status.Progress, "dvPhase", dv.Status.Phase, "pvcPhase", pvc.Status.Phase) @@ -357,10 +337,7 @@ func (ds UploadDataSource) Sync(ctx context.Context, vd *v1alpha2.VirtualDisk) ( ) vd.Status.Phase = v1alpha2.DiskReady - cb. - Status(metav1.ConditionTrue). - Reason(vdcondition.Ready). - Message("") + setReadyConditionWithWFFCAccounting(vd, cb, metav1.ConditionTrue, vdcondition.Ready, "") vd.Status.Progress = "100%" vd.Status.Capacity = ds.diskService.GetCapacity(pvc) diff --git a/images/virtualization-artifact/pkg/controller/vd/internal/stats.go b/images/virtualization-artifact/pkg/controller/vd/internal/stats.go index b2532f2bff..e9e56f3d33 100644 --- a/images/virtualization-artifact/pkg/controller/vd/internal/stats.go +++ b/images/virtualization-artifact/pkg/controller/vd/internal/stats.go @@ -73,6 +73,9 @@ func (h StatsHandler) Handle(ctx context.Context, vd *v1alpha2.VirtualDisk) (rec if vd.Status.Stats.CreationDuration.WaitingForDependencies != nil { duration -= vd.Status.Stats.CreationDuration.WaitingForDependencies.Duration } + if vd.Status.Stats.CreationDuration.WaitingForFirstConsumer != nil { + duration -= vd.Status.Stats.CreationDuration.WaitingForFirstConsumer.Duration + } vd.Status.Stats.CreationDuration.TotalProvisioning = &metav1.Duration{ Duration: duration,