Skip to content

feat(vd): Add Exporting phase and new conditions to VirtualDisk status #1256

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Aug 7, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions api/core/v1alpha2/vdcondition/condition.go
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,8 @@ const (
Ready ReadyReason = "Ready"
// Lost indicates that the underlying PersistentVolumeClaim has been lost and the `VirtualDisk` can no longer be used.
Lost ReadyReason = "PVCLost"
// Exporting indicates that the VirtualDisk is being exported.
Exporting ReadyReason = "Exporting"
// QuotaExceeded indicates that the VirtualDisk is reached project quotas and can not be provisioned.
QuotaExceeded ReadyReason = "QuotaExceeded"
// ImagePullFailed indicates that there was an issue with importing from DVCR.
Expand Down Expand Up @@ -159,6 +161,8 @@ the `InUse` condition's reason to `AttachedToVirtualMachine`.
const (
// UsedForImageCreation indicates that the VirtualDisk is used for create image.
UsedForImageCreation InUseReason = "UsedForImageCreation"
// UsedForDataExport indicates that the VirtualDisk is used for data export.
UsedForDataExport InUseReason = "UsedForDataExport"
// AttachedToVirtualMachine indicates that the VirtualDisk is attached to VirtualMachine.
AttachedToVirtualMachine InUseReason = "AttachedToVirtualMachine"
// NotInUse indicates that VirtualDisk free for use.
Expand Down
4 changes: 3 additions & 1 deletion api/core/v1alpha2/virtual_disk.go
Original file line number Diff line number Diff line change
Expand Up @@ -196,8 +196,9 @@ type VirtualDiskList struct {
// * `Resizing`: The process of resource resizing is in progress.
// * `Failed`: There was an error when creating the resource.
// * `PVCLost`: The child PVC of the resource is missing. The resource cannot be used.
// * `Exporting`: The child PV of the resource is in the process of exporting.
// * `Terminating`: The resource is being deleted.
// +kubebuilder:validation:Enum:={Pending,Provisioning,WaitForUserUpload,WaitForFirstConsumer,Ready,Resizing,Failed,PVCLost,Terminating}
// +kubebuilder:validation:Enum:={Pending,Provisioning,WaitForUserUpload,WaitForFirstConsumer,Ready,Resizing,Failed,PVCLost,Exporting,Terminating}
type DiskPhase string

const (
Expand All @@ -209,5 +210,6 @@ const (
DiskResizing DiskPhase = "Resizing"
DiskFailed DiskPhase = "Failed"
DiskLost DiskPhase = "PVCLost"
DiskExporting DiskPhase = "Exporting"
DiskTerminating DiskPhase = "Terminating"
)
1 change: 1 addition & 0 deletions crds/doc-ru-virtualdisks.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,7 @@ spec:
* `Resizing` — идёт процесс увеличения размера диска;
* `Failed` — при создании ресурса произошла ошибка;
* `PVCLost` — дочерний PVC ресурса отсутствует. Ресурс не может быть использован;
* `Exporting` - дочерний PV ресурса находится в процессе экспорта;
* `Terminating` - ресурс находится в процессе удаления.
progress:
description: |
Expand Down
2 changes: 2 additions & 0 deletions crds/virtualdisks.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -381,6 +381,7 @@ spec:
* `Resizing`: The process of resource resizing is in progress.
* `Failed`: There was an error when creating the resource.
* `PVCLost`: The child PVC of the resource is missing. The resource cannot be used.
* `Exporting`: The child PV of the resource is in the process of exporting.
* `Terminating`: The resource is being deleted.
enum:
- Pending
Expand All @@ -391,6 +392,7 @@ spec:
- Resizing
- Failed
- PVCLost
- Exporting
- Terminating
type: string
progress:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,20 @@ const (
// AnnNodeCpuFeature is the Kubevirt annotation for CPU feature.
AnnNodeCPUFeature = "cpu-feature.node.virtualization.deckhouse.io/"

// AnnDataExportRequest is the annotation for indicating that export requested.
AnnDataExportRequest = "storage.deckhouse.io/data-export-request"

// TODO: remove deprecated annotations in the v1 version.
// AnnStorageClassName is the annotation for indicating that storage class name. (USED IN STORAGE sds controllers)
AnnStorageClassName = AnnAPIGroupV + "/storage-class-name"
AnnStorageClassNameDeprecated = "storageClass"
// AnnVolumeMode is the annotation for indicating that volume mode. (USED IN STORAGE sds controllers)
AnnVolumeMode = AnnAPIGroupV + "/volume-mode"
AnnVolumeModeDeprecated = "volumeMode"
// AnnAccessMode is the annotation for indicating that access mode. (USED IN STORAGE sds controllers)
AnnAccessModes = AnnAPIGroupV + "/access-mode"
AnnAccessModesDeprecated = "accessModes"

// AppLabel is the app name label.
AppLabel = "app"
// CDILabelValue provides a constant for CDI Pod label values.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -132,9 +132,20 @@ func (ds ObjectRefVirtualDiskSnapshot) Sync(ctx context.Context, cvi *virtv2.Clu

pvcKey := supplements.NewGenerator(annotations.CVIShortName, cvi.Name, cvi.Spec.DataSource.ObjectRef.Namespace, cvi.UID).PersistentVolumeClaim()

storageClassName := vs.Annotations["storageClass"]
volumeMode := vs.Annotations["volumeMode"]
accessModesStr := strings.Split(vs.Annotations["accessModes"], ",")
storageClassName := vs.Annotations[annotations.AnnStorageClassName]
if storageClassName == "" {
storageClassName = vs.Annotations[annotations.AnnStorageClassNameDeprecated]
}
volumeMode := vs.Annotations[annotations.AnnVolumeMode]
if volumeMode == "" {
volumeMode = vs.Annotations[annotations.AnnVolumeModeDeprecated]
}
accessModesRaw := vs.Annotations[annotations.AnnAccessModes]
if accessModesRaw == "" {
accessModesRaw = vs.Annotations[annotations.AnnAccessModesDeprecated]
}

accessModesStr := strings.Split(accessModesRaw, ",")
accessModes := make([]corev1.PersistentVolumeAccessMode, 0, len(accessModesStr))
for _, accessModeStr := range accessModesStr {
accessModes = append(accessModes, corev1.PersistentVolumeAccessMode(accessModeStr))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ import (
"context"
"fmt"
"slices"
"time"

corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
Expand All @@ -30,6 +29,7 @@ import (
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/reconcile"

"github.com/deckhouse/virtualization-controller/pkg/common/annotations"
"github.com/deckhouse/virtualization-controller/pkg/common/object"
"github.com/deckhouse/virtualization-controller/pkg/controller/conditions"
virtv2 "github.com/deckhouse/virtualization/api/core/v1alpha2"
Expand All @@ -49,59 +49,50 @@ func NewInUseHandler(client client.Client) *InUseHandler {
}

func (h InUseHandler) Handle(ctx context.Context, vd *virtv2.VirtualDisk) (reconcile.Result, error) {
inUseCondition, ok := conditions.GetCondition(vdcondition.InUseType, vd.Status.Conditions)
if !ok {
cb := conditions.NewConditionBuilder(vdcondition.InUseType).
Status(metav1.ConditionUnknown).
Reason(conditions.ReasonUnknown).
Generation(vd.Generation)
conditions.SetCondition(cb, &vd.Status.Conditions)
inUseCondition = cb.Condition()
}

err := h.updateAttachedVirtualMachines(ctx, vd)
if err != nil {
return reconcile.Result{}, err
}

usedByVM, usedByImage := false, false
var (
usedByVM bool
usedByImage bool
usedByDataExport bool
)

if inUseCondition.Reason != vdcondition.UsedForImageCreation.String() {
usedByVM = h.checkUsageByVM(vd)

if !usedByVM {
usedByImage, err = h.checkImageUsage(ctx, vd)
if err != nil {
return reconcile.Result{}, err
}
}
} else {
usedByVM = h.checkUsageByVM(vd)
if !usedByVM {
usedByImage, err = h.checkImageUsage(ctx, vd)
if err != nil {
return reconcile.Result{}, err
}

if !usedByImage {
usedByVM = h.checkUsageByVM(vd)
}
if !usedByVM && !usedByImage {
usedByDataExport, err = h.checkDataExportUsage(ctx, vd)
if err != nil {
return reconcile.Result{}, err
}
}

cb := conditions.NewConditionBuilder(vdcondition.InUseType)
cb := conditions.NewConditionBuilder(vdcondition.InUseType).Generation(vd.Generation)
switch {
case usedByVM:
cb.Generation(vd.Generation).
cb.
Status(metav1.ConditionTrue).
Reason(vdcondition.AttachedToVirtualMachine).
Message("").
LastTransitionTime(time.Now())
Message("")
case usedByImage:
cb.Generation(vd.Generation).
cb.
Status(metav1.ConditionTrue).
Reason(vdcondition.UsedForImageCreation).
Message("").
LastTransitionTime(time.Now())
Message("")
case usedByDataExport:
cb.
Status(metav1.ConditionTrue).
Reason(vdcondition.UsedForDataExport).
Message("")
default:
cb.Generation(vd.Generation).
cb.
Status(metav1.ConditionFalse).
Reason(vdcondition.NotInUse).
Message("")
Expand All @@ -121,6 +112,23 @@ func (h InUseHandler) isVDAttachedToVM(vdName string, vm virtv2.VirtualMachine)
return false
}

func (h InUseHandler) checkDataExportUsage(ctx context.Context, vd *virtv2.VirtualDisk) (bool, error) {
pvcName := vd.Status.Target.PersistentVolumeClaim
if pvcName == "" {
return false, nil
}

pvc, err := object.FetchObject(ctx, types.NamespacedName{Name: pvcName, Namespace: vd.Namespace}, h.client, &corev1.PersistentVolumeClaim{})
if err != nil {
return false, fmt.Errorf("fetch pvc: %w", err)
}
if pvc == nil {
return false, nil
}

return pvc.GetAnnotations()[annotations.AnnDataExportRequest] == "true", nil
}

func (h InUseHandler) checkImageUsage(ctx context.Context, vd *virtv2.VirtualDisk) (bool, error) {
// If disk is not ready, it cannot be used for create image
if vd.Status.Phase != virtv2.DiskReady {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,17 +18,18 @@ package internal

import (
"context"
"time"

. "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"
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
virtv1 "kubevirt.io/api/core/v1"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client/fake"

"github.com/deckhouse/virtualization-controller/pkg/common/annotations"
"github.com/deckhouse/virtualization-controller/pkg/controller/conditions"
virtv2 "github.com/deckhouse/virtualization/api/core/v1alpha2"
"github.com/deckhouse/virtualization/api/core/v1alpha2/vdcondition"
Expand Down Expand Up @@ -602,7 +603,6 @@ var _ = Describe("InUseHandler", func() {

Context("when VirtualDisk is used by VirtualMachine after create image", func() {
It("must set status True and reason AllowedForVirtualMachineUsage", func() {
startTime := metav1.Time{Time: time.Now()}
vd := &virtv2.VirtualDisk{
ObjectMeta: metav1.ObjectMeta{
Name: "test-vd",
Expand All @@ -611,10 +611,9 @@ var _ = Describe("InUseHandler", func() {
Status: virtv2.VirtualDiskStatus{
Conditions: []metav1.Condition{
{
Type: vdcondition.InUseType.String(),
Reason: vdcondition.UsedForImageCreation.String(),
Status: metav1.ConditionTrue,
LastTransitionTime: startTime,
Type: vdcondition.InUseType.String(),
Reason: vdcondition.UsedForImageCreation.String(),
Status: metav1.ConditionTrue,
},
},
},
Expand Down Expand Up @@ -647,7 +646,6 @@ var _ = Describe("InUseHandler", func() {
Expect(cond).ToNot(BeNil())
Expect(cond.Status).To(Equal(metav1.ConditionTrue))
Expect(cond.Reason).To(Equal(vdcondition.AttachedToVirtualMachine.String()))
Expect(cond.LastTransitionTime).ToNot(Equal(startTime))
})
})

Expand Down Expand Up @@ -767,4 +765,44 @@ var _ = Describe("InUseHandler", func() {
Expect(cond.Reason).To(Equal(vdcondition.NotInUse.String()))
})
})
Context("when VirtualDisk is used by DataExport", func() {
It("must set status True and reason UsedForDataExport", func() {
vd := &virtv2.VirtualDisk{
ObjectMeta: metav1.ObjectMeta{
Name: "test-vd",
Namespace: "default",
},
Status: virtv2.VirtualDiskStatus{
Conditions: []metav1.Condition{},
Target: virtv2.DiskTarget{
PersistentVolumeClaim: "test-pvc",
},
},
}
pvc := &corev1.PersistentVolumeClaim{
ObjectMeta: metav1.ObjectMeta{
Name: "test-pvc",
Namespace: "default",
Annotations: map[string]string{
annotations.AnnDataExportRequest: "true",
},
},
Status: corev1.PersistentVolumeClaimStatus{
Phase: corev1.ClaimBound,
},
}

k8sClient := fake.NewClientBuilder().WithScheme(scheme).WithObjects(vd, pvc).Build()
handler = NewInUseHandler(k8sClient)

result, err := handler.Handle(ctx, vd)
Expect(err).ToNot(HaveOccurred())
Expect(result).To(Equal(ctrl.Result{}))

cond, _ := conditions.GetCondition(vdcondition.InUseType, vd.Status.Conditions)
Expect(cond).ToNot(BeNil())
Expect(cond.Status).To(Equal(metav1.ConditionTrue))
Expect(cond.Reason).To(Equal(vdcondition.UsedForDataExport.String()))
})
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -113,11 +113,14 @@ func setPhaseConditionForFinishedDisk(
Reason(vdcondition.Lost).
Message(fmt.Sprintf("PVC %s not found.", supgen.PersistentVolumeClaim().String()))
case pvc.Status.Phase == corev1.ClaimLost:
newPhase = virtv2.DiskLost
cb.
Status(metav1.ConditionFalse).
Reason(vdcondition.Lost).
Message(fmt.Sprintf("PV %s not found.", pvc.Spec.VolumeName))
cb.Status(metav1.ConditionFalse)
if pvc.GetAnnotations()[annotations.AnnDataExportRequest] == "true" {
newPhase = virtv2.DiskExporting
cb.Reason(vdcondition.Exporting).Message("PV is being exported")
} else {
newPhase = virtv2.DiskLost
cb.Reason(vdcondition.Lost).Message(fmt.Sprintf("PV %s not found.", pvc.Spec.VolumeName))
}
default:
newPhase = virtv2.DiskReady
cb.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -123,9 +123,20 @@ func (s CreatePVCFromVDSnapshotStep) Take(ctx context.Context, vd *virtv2.Virtua
}

func (s CreatePVCFromVDSnapshotStep) buildPVC(vd *virtv2.VirtualDisk, vs *vsv1.VolumeSnapshot) *corev1.PersistentVolumeClaim {
storageClassName := vs.Annotations["storageClass"]
volumeMode := vs.Annotations["volumeMode"]
accessModesStr := strings.Split(vs.Annotations["accessModes"], ",")
storageClassName := vs.Annotations[annotations.AnnStorageClassName]
if storageClassName == "" {
storageClassName = vs.Annotations[annotations.AnnStorageClassNameDeprecated]
}
volumeMode := vs.Annotations[annotations.AnnVolumeMode]
if volumeMode == "" {
volumeMode = vs.Annotations[annotations.AnnVolumeModeDeprecated]
}
accessModesRaw := vs.Annotations[annotations.AnnAccessModes]
if accessModesRaw == "" {
accessModesRaw = vs.Annotations[annotations.AnnAccessModesDeprecated]
}

accessModesStr := strings.Split(accessModesRaw, ",")
accessModes := make([]corev1.PersistentVolumeAccessMode, 0, len(accessModesStr))
for _, accessModeStr := range accessModesStr {
accessModes = append(accessModes, corev1.PersistentVolumeAccessMode(accessModeStr))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,11 +82,16 @@ func (s ReadyStep) Take(ctx context.Context, vd *virtv2.VirtualDisk) (*reconcile

switch s.pvc.Status.Phase {
case corev1.ClaimLost:
vd.Status.Phase = virtv2.DiskLost
s.cb.
Status(metav1.ConditionFalse).
Reason(vdcondition.Lost).
Message(fmt.Sprintf("The PersistentVolume %q not found.", s.pvc.Spec.VolumeName))
s.cb.Status(metav1.ConditionFalse)
if s.pvc.GetAnnotations()[annotations.AnnDataExportRequest] == "true" {
vd.Status.Phase = virtv2.DiskExporting
s.cb.Reason(vdcondition.Exporting).Message("PV is being exported")
} else {
vd.Status.Phase = virtv2.DiskLost
s.cb.
Reason(vdcondition.Lost).
Message(fmt.Sprintf("The PersistentVolume %q not found.", s.pvc.Spec.VolumeName))
}

log.Debug("PVC is Lost")

Expand Down
Loading
Loading