From 74a3f6161d7dd0a9bfd1bd1e0034c7c777ec120e Mon Sep 17 00:00:00 2001 From: Isteb4k Date: Thu, 23 Oct 2025 18:17:43 +0200 Subject: [PATCH 1/2] feat(vi): create volume snapshot for vi on the pvc Signed-off-by: Isteb4k --- .../pkg/controller/service/disk_service.go | 28 ++++++++++++ .../pkg/controller/vi/internal/source/http.go | 43 +++++++++++++++++++ 2 files changed, 71 insertions(+) diff --git a/images/virtualization-artifact/pkg/controller/service/disk_service.go b/images/virtualization-artifact/pkg/controller/service/disk_service.go index 4865cf173f..292b7d340b 100644 --- a/images/virtualization-artifact/pkg/controller/service/disk_service.go +++ b/images/virtualization-artifact/pkg/controller/service/disk_service.go @@ -238,6 +238,34 @@ func (s DiskService) CheckProvisioning(ctx context.Context, pvc *corev1.Persiste return nil } +func (s DiskService) CreateVolumeSnapshot(ctx context.Context, pvc *corev1.PersistentVolumeClaim) error { + if pvc == nil || pvc.Status.Phase == corev1.ClaimBound { + return errors.New("pvc not Bound") + } + + vs := &vsv1.VolumeSnapshot{ + ObjectMeta: metav1.ObjectMeta{ + Name: pvc.Name, + Namespace: pvc.Namespace, + OwnerReferences: []metav1.OwnerReference{ + MakeOwnerReference(pvc), + }, + }, + Spec: vsv1.VolumeSnapshotSpec{ + Source: vsv1.VolumeSnapshotSource{ + PersistentVolumeClaimName: &pvc.Name, + }, + }, + } + + err := s.client.Create(ctx, vs) + if err != nil && !k8serrors.IsAlreadyExists(err) { + return fmt.Errorf("create vs: %w", err) + } + + return nil +} + func (s DiskService) CreatePersistentVolumeClaim(ctx context.Context, pvc *corev1.PersistentVolumeClaim) error { err := s.client.Create(ctx, pvc) if err != nil && !k8serrors.IsAlreadyExists(err) { diff --git a/images/virtualization-artifact/pkg/controller/vi/internal/source/http.go b/images/virtualization-artifact/pkg/controller/vi/internal/source/http.go index 68e16610a5..ba7a75e006 100644 --- a/images/virtualization-artifact/pkg/controller/vi/internal/source/http.go +++ b/images/virtualization-artifact/pkg/controller/vi/internal/source/http.go @@ -22,6 +22,7 @@ import ( "fmt" "time" + vsv1 "github.com/kubernetes-csi/external-snapshotter/client/v6/apis/volumesnapshot/v1" corev1 "k8s.io/api/core/v1" storagev1 "k8s.io/api/storage/v1" "k8s.io/apimachinery/pkg/api/resource" @@ -379,6 +380,48 @@ func (ds HTTPDataSource) StoreToPVC(ctx context.Context, vi *v1alpha2.VirtualIma "The HTTP DataSource import has completed", ) + _, exists := vi.Annotations["virtualization.deckhouse.io/use-volume-snapshot"] + if exists { + var vs *vsv1.VolumeSnapshot + vs, err = ds.diskService.GetVolumeSnapshot(ctx, pvc.Name, pvc.Namespace) + if err != nil { + vi.Status.Phase = v1alpha2.ImageFailed + cb. + Status(metav1.ConditionFalse). + Reason(vicondition.ProvisioningFailed). + Message(err.Error()) + return reconcile.Result{}, nil + } + + if vs == nil { + err = ds.diskService.CreateVolumeSnapshot(ctx, pvc) + if err != nil { + vi.Status.Phase = v1alpha2.ImageFailed + cb. + Status(metav1.ConditionFalse). + Reason(vicondition.ProvisioningFailed). + Message(err.Error()) + return reconcile.Result{}, nil + } + + vi.Status.Phase = v1alpha2.ImageProvisioning + cb. + Status(metav1.ConditionFalse). + Reason(vicondition.Provisioning). + Message("The VolumeSnapshot has been created.") + return reconcile.Result{RequeueAfter: time.Second}, nil + } + + if vs.Status.ReadyToUse == nil || !(*vs.Status.ReadyToUse) { + vi.Status.Phase = v1alpha2.ImageProvisioning + cb. + Status(metav1.ConditionFalse). + Reason(vicondition.Provisioning). + Message("Waiting for the VolumeSnapshot to be ready to use.") + return reconcile.Result{RequeueAfter: time.Second}, nil + } + } + vi.Status.Phase = v1alpha2.ImageReady cb. Status(metav1.ConditionTrue). From 2ba1f4cfd88f214d145181d7934dcfa1b9e784a0 Mon Sep 17 00:00:00 2001 From: Isteb4k Date: Thu, 23 Oct 2025 18:33:29 +0200 Subject: [PATCH 2/2] feat(vd): create pvc using volume snapshot Signed-off-by: Isteb4k --- .../pkg/common/annotations/annotations.go | 2 + .../pkg/controller/service/disk_service.go | 23 +- .../vd/internal/source/object_ref_vi.go | 1 + .../source/step/create_dv_from_vi_step.go | 6 + .../source/step/create_pvc_from_vs_step.go | 258 ++++++++++++++++++ .../source/step/ensure_node_placement.go | 6 + .../internal/source/step/wait_for_dv_step.go | 6 + .../pkg/controller/vi/internal/source/http.go | 2 +- 8 files changed, 300 insertions(+), 4 deletions(-) create mode 100644 images/virtualization-artifact/pkg/controller/vd/internal/source/step/create_pvc_from_vs_step.go diff --git a/images/virtualization-artifact/pkg/common/annotations/annotations.go b/images/virtualization-artifact/pkg/common/annotations/annotations.go index ba7453b2a8..44218117ef 100644 --- a/images/virtualization-artifact/pkg/common/annotations/annotations.go +++ b/images/virtualization-artifact/pkg/common/annotations/annotations.go @@ -164,6 +164,8 @@ const ( AnnStorageProvisioner = "volume.kubernetes.io/storage-provisioner" AnnStorageProvisionerDeprecated = "volume.beta.kubernetes.io/storage-provisioner" + AnnUseVolumeSnapshot = AnnAPIGroupV + "/use-volume-snapshot" + // AppLabel is the app name label. AppLabel = "app" // CDILabelValue provides a constant for CDI Pod label values. diff --git a/images/virtualization-artifact/pkg/controller/service/disk_service.go b/images/virtualization-artifact/pkg/controller/service/disk_service.go index 292b7d340b..5a79e96856 100644 --- a/images/virtualization-artifact/pkg/controller/service/disk_service.go +++ b/images/virtualization-artifact/pkg/controller/service/disk_service.go @@ -239,14 +239,31 @@ func (s DiskService) CheckProvisioning(ctx context.Context, pvc *corev1.Persiste } func (s DiskService) CreateVolumeSnapshot(ctx context.Context, pvc *corev1.PersistentVolumeClaim) error { - if pvc == nil || pvc.Status.Phase == corev1.ClaimBound { + if pvc == nil || pvc.Status.Phase != corev1.ClaimBound { return errors.New("pvc not Bound") } + anno := make(map[string]string) + if pvc.Spec.StorageClassName != nil && *pvc.Spec.StorageClassName != "" { + anno[annotations.AnnStorageClassName] = *pvc.Spec.StorageClassName + } + + if pvc.Spec.VolumeMode != nil && *pvc.Spec.VolumeMode != "" { + anno[annotations.AnnVolumeMode] = string(*pvc.Spec.VolumeMode) + } + + accessModes := make([]string, 0, len(pvc.Status.AccessModes)) + for _, accessMode := range pvc.Status.AccessModes { + accessModes = append(accessModes, string(accessMode)) + } + + anno[annotations.AnnAccessModes] = strings.Join(accessModes, ",") + vs := &vsv1.VolumeSnapshot{ ObjectMeta: metav1.ObjectMeta{ - Name: pvc.Name, - Namespace: pvc.Namespace, + Name: pvc.Name, + Namespace: pvc.Namespace, + Annotations: anno, OwnerReferences: []metav1.OwnerReference{ MakeOwnerReference(pvc), }, diff --git a/images/virtualization-artifact/pkg/controller/vd/internal/source/object_ref_vi.go b/images/virtualization-artifact/pkg/controller/vd/internal/source/object_ref_vi.go index ecf5891a25..571c42cb70 100644 --- a/images/virtualization-artifact/pkg/controller/vd/internal/source/object_ref_vi.go +++ b/images/virtualization-artifact/pkg/controller/vd/internal/source/object_ref_vi.go @@ -74,6 +74,7 @@ func (ds ObjectRefVirtualImage) Sync(ctx context.Context, vd *v1alpha2.VirtualDi return steptaker.NewStepTakers[*v1alpha2.VirtualDisk]( step.NewReadyStep(ds.diskService, pvc, cb), step.NewTerminatingStep(pvc), + step.NewCreatePVCFromVSStep(pvc, ds.client, cb), step.NewCreateDataVolumeFromVirtualImageStep(pvc, dv, ds.diskService, ds.client, cb), step.NewEnsureNodePlacementStep(pvc, dv, ds.diskService, ds.client, cb), step.NewWaitForDVStep(pvc, dv, ds.diskService, ds.client, cb), diff --git a/images/virtualization-artifact/pkg/controller/vd/internal/source/step/create_dv_from_vi_step.go b/images/virtualization-artifact/pkg/controller/vd/internal/source/step/create_dv_from_vi_step.go index 959f65ecfd..3ee1e7cc71 100644 --- a/images/virtualization-artifact/pkg/controller/vd/internal/source/step/create_dv_from_vi_step.go +++ b/images/virtualization-artifact/pkg/controller/vd/internal/source/step/create_dv_from_vi_step.go @@ -31,6 +31,7 @@ import ( "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/imageformat" "github.com/deckhouse/virtualization-controller/pkg/common/object" "github.com/deckhouse/virtualization-controller/pkg/controller/conditions" @@ -69,6 +70,11 @@ func (s CreateDataVolumeFromVirtualImageStep) Take(ctx context.Context, vd *v1al return nil, nil } + _, exists := vd.Annotations[annotations.AnnUseVolumeSnapshot] + if exists { + return nil, nil + } + viRefKey := types.NamespacedName{Name: vd.Spec.DataSource.ObjectRef.Name, Namespace: vd.Namespace} viRef, err := object.FetchObject(ctx, viRefKey, s.client, &v1alpha2.VirtualImage{}) if err != nil { diff --git a/images/virtualization-artifact/pkg/controller/vd/internal/source/step/create_pvc_from_vs_step.go b/images/virtualization-artifact/pkg/controller/vd/internal/source/step/create_pvc_from_vs_step.go new file mode 100644 index 0000000000..e5ab0085b4 --- /dev/null +++ b/images/virtualization-artifact/pkg/controller/vd/internal/source/step/create_pvc_from_vs_step.go @@ -0,0 +1,258 @@ +/* +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" + "encoding/json" + "errors" + "fmt" + "strings" + + vsv1 "github.com/kubernetes-csi/external-snapshotter/client/v6/apis/volumesnapshot/v1" + corev1 "k8s.io/api/core/v1" + k8serrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/api/resource" + 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/object" + "github.com/deckhouse/virtualization-controller/pkg/common/pointer" + "github.com/deckhouse/virtualization-controller/pkg/controller/conditions" + "github.com/deckhouse/virtualization-controller/pkg/controller/service" + vdsupplements "github.com/deckhouse/virtualization-controller/pkg/controller/vd/internal/supplements" + "github.com/deckhouse/virtualization/api/core/v1alpha2" + "github.com/deckhouse/virtualization/api/core/v1alpha2/vdcondition" +) + +type CreatePVCFromVSStep struct { + pvc *corev1.PersistentVolumeClaim + client client.Client + cb *conditions.ConditionBuilder +} + +func NewCreatePVCFromVSStep( + pvc *corev1.PersistentVolumeClaim, + client client.Client, + cb *conditions.ConditionBuilder, +) *CreatePVCFromVSStep { + return &CreatePVCFromVSStep{ + pvc: pvc, + client: client, + cb: cb, + } +} + +func (s CreatePVCFromVSStep) Take(ctx context.Context, vd *v1alpha2.VirtualDisk) (*reconcile.Result, error) { + if s.pvc != nil { + return nil, nil + } + + _, exists := vd.Annotations[annotations.AnnUseVolumeSnapshot] + if !exists { + return nil, nil + } + + vi, err := object.FetchObject(ctx, types.NamespacedName{ + Namespace: vd.Namespace, + Name: vd.Spec.DataSource.ObjectRef.Name, + }, s.client, &v1alpha2.VirtualImage{}) + if err != nil { + vd.Status.Phase = v1alpha2.DiskFailed + s.cb. + Status(metav1.ConditionFalse). + Reason(vdcondition.ProvisioningFailed). + Message("The VirtualImage not found") + return &reconcile.Result{}, nil + } + + if vi.Status.Target.PersistentVolumeClaim == "" { + vd.Status.Phase = v1alpha2.DiskFailed + s.cb. + Status(metav1.ConditionFalse). + Reason(vdcondition.ProvisioningFailed). + Message("The VirtualImage does not have the target pvc") + return &reconcile.Result{}, nil + } + + vs, err := object.FetchObject(ctx, types.NamespacedName{ + Namespace: vi.Namespace, + Name: vi.Status.Target.PersistentVolumeClaim, + }, s.client, &vsv1.VolumeSnapshot{}) + if err != nil { + vd.Status.Phase = v1alpha2.DiskFailed + s.cb. + Status(metav1.ConditionFalse). + Reason(vdcondition.ProvisioningFailed). + Message("The VolumeSnapshot not found") + return &reconcile.Result{}, nil + } + + if vs.Status == nil || !(*vs.Status.ReadyToUse) { + vd.Status.Phase = v1alpha2.DiskFailed + s.cb. + Status(metav1.ConditionFalse). + Reason(vdcondition.ProvisioningFailed). + Message("The VolumeSnapshot is not ready to use") + return &reconcile.Result{}, nil + } + + pvc, err := s.buildPVC(vd, vs, vi) + if err != nil { + return nil, fmt.Errorf("failed to build pvc: %w", err) + } + + err = s.client.Create(ctx, pvc) + if err != nil && !k8serrors.IsAlreadyExists(err) { + return nil, fmt.Errorf("create pvc: %w", err) + } + + vd.Status.Phase = v1alpha2.DiskProvisioning + s.cb. + Status(metav1.ConditionFalse). + Reason(vdcondition.Provisioning). + Message("The PersistentVolumeClaim has been created: waiting for it to be Bound.") + + vd.Status.Progress = "0%" + vd.Status.SourceUID = pointer.GetPointer(vi.UID) + vdsupplements.SetPVCName(vd, pvc.Name) + + s.addOriginalMetadata(vd, vs) + return nil, nil +} + +func (s CreatePVCFromVSStep) buildPVC(vd *v1alpha2.VirtualDisk, vs *vsv1.VolumeSnapshot, vi *v1alpha2.VirtualImage) (*corev1.PersistentVolumeClaim, error) { + var storageClassName string + if vd.Spec.PersistentVolumeClaim.StorageClass != nil && *vd.Spec.PersistentVolumeClaim.StorageClass != "" { + storageClassName = *vd.Spec.PersistentVolumeClaim.StorageClass + } else { + 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)) + } + + spec := corev1.PersistentVolumeClaimSpec{ + AccessModes: accessModes, + DataSource: &corev1.TypedLocalObjectReference{ + APIGroup: ptr.To(vs.GroupVersionKind().Group), + Kind: vs.Kind, + Name: vi.Status.Target.PersistentVolumeClaim, + }, + } + + if storageClassName != "" { + spec.StorageClassName = &storageClassName + vd.Status.StorageClassName = storageClassName + } + + if volumeMode != "" { + spec.VolumeMode = ptr.To(corev1.PersistentVolumeMode(volumeMode)) + } + + pvcSize, err := s.getPVCSize(vd, vs) + if err != nil { + return nil, err + } + + spec.Resources = corev1.VolumeResourceRequirements{ + Requests: corev1.ResourceList{ + corev1.ResourceStorage: pvcSize, + }, + } + + pvcKey := vdsupplements.NewGenerator(vd).PersistentVolumeClaim() + + return &corev1.PersistentVolumeClaim{ + ObjectMeta: metav1.ObjectMeta{ + Name: pvcKey.Name, + Namespace: pvcKey.Namespace, + OwnerReferences: []metav1.OwnerReference{ + service.MakeOwnerReference(vd), + }, + }, + Spec: spec, + }, nil +} + +func (s CreatePVCFromVSStep) getPVCSize(vd *v1alpha2.VirtualDisk, vs *vsv1.VolumeSnapshot) (resource.Quantity, error) { + if vs.Status == nil || vs.Status.RestoreSize == nil || vs.Status.RestoreSize.IsZero() { + return resource.Quantity{}, errors.New("vs has zero size") + } + + if vd.Spec.PersistentVolumeClaim.Size == nil || vd.Spec.PersistentVolumeClaim.Size.IsZero() { + return *vs.Status.RestoreSize, nil + } + + if vd.Spec.PersistentVolumeClaim.Size.Cmp(*vs.Status.RestoreSize) == 1 { + return *vd.Spec.PersistentVolumeClaim.Size, nil + } + + return *vs.Status.RestoreSize, nil +} + +// AddOriginalMetadata adds original annotations and labels from VolumeSnapshot to VirtualDisk, +// without overwriting existing values +func (s CreatePVCFromVSStep) addOriginalMetadata(vd *v1alpha2.VirtualDisk, vs *vsv1.VolumeSnapshot) { + if vd.Annotations == nil { + vd.Annotations = make(map[string]string) + } + if vd.Labels == nil { + vd.Labels = make(map[string]string) + } + + if annotationsJSON := vs.Annotations[annotations.AnnVirtualDiskOriginalAnnotations]; annotationsJSON != "" { + var originalAnnotations map[string]string + if err := json.Unmarshal([]byte(annotationsJSON), &originalAnnotations); err == nil { + for key, value := range originalAnnotations { + if _, exists := vd.Annotations[key]; !exists { + vd.Annotations[key] = value + } + } + } + } + + if labelsJSON := vs.Annotations[annotations.AnnVirtualDiskOriginalLabels]; labelsJSON != "" { + var originalLabels map[string]string + if err := json.Unmarshal([]byte(labelsJSON), &originalLabels); err == nil { + for key, value := range originalLabels { + if _, exists := vd.Labels[key]; !exists { + vd.Labels[key] = value + } + } + } + } +} diff --git a/images/virtualization-artifact/pkg/controller/vd/internal/source/step/ensure_node_placement.go b/images/virtualization-artifact/pkg/controller/vd/internal/source/step/ensure_node_placement.go index 120505af0f..7be72e5091 100644 --- a/images/virtualization-artifact/pkg/controller/vd/internal/source/step/ensure_node_placement.go +++ b/images/virtualization-artifact/pkg/controller/vd/internal/source/step/ensure_node_placement.go @@ -27,6 +27,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/provisioner" "github.com/deckhouse/virtualization-controller/pkg/controller/conditions" "github.com/deckhouse/virtualization-controller/pkg/controller/service" @@ -71,6 +72,11 @@ func (s EnsureNodePlacementStep) Take(ctx context.Context, vd *v1alpha2.VirtualD return nil, nil } + _, exists := vd.Annotations[annotations.AnnUseVolumeSnapshot] + if exists { + return nil, nil + } + err := s.disk.CheckProvisioning(ctx, s.pvc) switch { case err == nil: diff --git a/images/virtualization-artifact/pkg/controller/vd/internal/source/step/wait_for_dv_step.go b/images/virtualization-artifact/pkg/controller/vd/internal/source/step/wait_for_dv_step.go index 656d60dd72..f03f954917 100644 --- a/images/virtualization-artifact/pkg/controller/vd/internal/source/step/wait_for_dv_step.go +++ b/images/virtualization-artifact/pkg/controller/vd/internal/source/step/wait_for_dv_step.go @@ -29,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" 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" @@ -67,6 +68,11 @@ func NewWaitForDVStep( } func (s WaitForDVStep) Take(ctx context.Context, vd *v1alpha2.VirtualDisk) (*reconcile.Result, error) { + _, exists := vd.Annotations[annotations.AnnUseVolumeSnapshot] + if exists { + return nil, nil + } + if s.dv == nil { vd.Status.Phase = v1alpha2.DiskProvisioning s.cb. diff --git a/images/virtualization-artifact/pkg/controller/vi/internal/source/http.go b/images/virtualization-artifact/pkg/controller/vi/internal/source/http.go index ba7a75e006..d9a39be59d 100644 --- a/images/virtualization-artifact/pkg/controller/vi/internal/source/http.go +++ b/images/virtualization-artifact/pkg/controller/vi/internal/source/http.go @@ -380,7 +380,7 @@ func (ds HTTPDataSource) StoreToPVC(ctx context.Context, vi *v1alpha2.VirtualIma "The HTTP DataSource import has completed", ) - _, exists := vi.Annotations["virtualization.deckhouse.io/use-volume-snapshot"] + _, exists := vi.Annotations[annotations.AnnUseVolumeSnapshot] if exists { var vs *vsv1.VolumeSnapshot vs, err = ds.diskService.GetVolumeSnapshot(ctx, pvc.Name, pvc.Namespace)