Skip to content

Commit 05c28fb

Browse files
committed
feat(vd): create pvc using volume snapshot
Signed-off-by: Isteb4k <[email protected]>
1 parent bcdb347 commit 05c28fb

File tree

8 files changed

+300
-4
lines changed

8 files changed

+300
-4
lines changed

images/virtualization-artifact/pkg/common/annotations/annotations.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,8 @@ const (
164164
AnnStorageProvisioner = "volume.kubernetes.io/storage-provisioner"
165165
AnnStorageProvisionerDeprecated = "volume.beta.kubernetes.io/storage-provisioner"
166166

167+
AnnUseVolumeSnapshot = AnnAPIGroupV + "/use-volume-snapshot"
168+
167169
// AppLabel is the app name label.
168170
AppLabel = "app"
169171
// CDILabelValue provides a constant for CDI Pod label values.

images/virtualization-artifact/pkg/controller/service/disk_service.go

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -239,14 +239,31 @@ func (s DiskService) CheckProvisioning(ctx context.Context, pvc *corev1.Persiste
239239
}
240240

241241
func (s DiskService) CreateVolumeSnapshot(ctx context.Context, pvc *corev1.PersistentVolumeClaim) error {
242-
if pvc == nil || pvc.Status.Phase == corev1.ClaimBound {
242+
if pvc == nil || pvc.Status.Phase != corev1.ClaimBound {
243243
return errors.New("pvc not Bound")
244244
}
245245

246+
anno := make(map[string]string)
247+
if pvc.Spec.StorageClassName != nil && *pvc.Spec.StorageClassName != "" {
248+
anno[annotations.AnnStorageClassName] = *pvc.Spec.StorageClassName
249+
}
250+
251+
if pvc.Spec.VolumeMode != nil && *pvc.Spec.VolumeMode != "" {
252+
anno[annotations.AnnVolumeMode] = string(*pvc.Spec.VolumeMode)
253+
}
254+
255+
accessModes := make([]string, 0, len(pvc.Status.AccessModes))
256+
for _, accessMode := range pvc.Status.AccessModes {
257+
accessModes = append(accessModes, string(accessMode))
258+
}
259+
260+
anno[annotations.AnnAccessModes] = strings.Join(accessModes, ",")
261+
246262
vs := &vsv1.VolumeSnapshot{
247263
ObjectMeta: metav1.ObjectMeta{
248-
Name: pvc.Name,
249-
Namespace: pvc.Namespace,
264+
Name: pvc.Name,
265+
Namespace: pvc.Namespace,
266+
Annotations: anno,
250267
OwnerReferences: []metav1.OwnerReference{
251268
MakeOwnerReference(pvc),
252269
},

images/virtualization-artifact/pkg/controller/vd/internal/source/object_ref_vi.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ func (ds ObjectRefVirtualImage) Sync(ctx context.Context, vd *v1alpha2.VirtualDi
7474
return steptaker.NewStepTakers[*v1alpha2.VirtualDisk](
7575
step.NewReadyStep(ds.diskService, pvc, cb),
7676
step.NewTerminatingStep(pvc),
77+
step.NewCreatePVCFromVSStep(pvc, ds.client, cb),
7778
step.NewCreateDataVolumeFromVirtualImageStep(pvc, dv, ds.diskService, ds.client, cb),
7879
step.NewEnsureNodePlacementStep(pvc, dv, ds.diskService, ds.client, cb),
7980
step.NewWaitForDVStep(pvc, dv, ds.diskService, ds.client, cb),

images/virtualization-artifact/pkg/controller/vd/internal/source/step/create_dv_from_vi_step.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import (
3131
"sigs.k8s.io/controller-runtime/pkg/reconcile"
3232

3333
"github.com/deckhouse/virtualization-controller/pkg/common"
34+
"github.com/deckhouse/virtualization-controller/pkg/common/annotations"
3435
"github.com/deckhouse/virtualization-controller/pkg/common/imageformat"
3536
"github.com/deckhouse/virtualization-controller/pkg/common/object"
3637
"github.com/deckhouse/virtualization-controller/pkg/controller/conditions"
@@ -69,6 +70,11 @@ func (s CreateDataVolumeFromVirtualImageStep) Take(ctx context.Context, vd *v1al
6970
return nil, nil
7071
}
7172

73+
_, exists := vd.Annotations[annotations.AnnUseVolumeSnapshot]
74+
if exists {
75+
return nil, nil
76+
}
77+
7278
viRefKey := types.NamespacedName{Name: vd.Spec.DataSource.ObjectRef.Name, Namespace: vd.Namespace}
7379
viRef, err := object.FetchObject(ctx, viRefKey, s.client, &v1alpha2.VirtualImage{})
7480
if err != nil {
Lines changed: 258 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,258 @@
1+
/*
2+
Copyright 2025 Flant JSC
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package step
18+
19+
import (
20+
"context"
21+
"encoding/json"
22+
"errors"
23+
"fmt"
24+
"strings"
25+
26+
vsv1 "github.com/kubernetes-csi/external-snapshotter/client/v6/apis/volumesnapshot/v1"
27+
corev1 "k8s.io/api/core/v1"
28+
k8serrors "k8s.io/apimachinery/pkg/api/errors"
29+
"k8s.io/apimachinery/pkg/api/resource"
30+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
31+
"k8s.io/apimachinery/pkg/types"
32+
"k8s.io/utils/ptr"
33+
"sigs.k8s.io/controller-runtime/pkg/client"
34+
"sigs.k8s.io/controller-runtime/pkg/reconcile"
35+
36+
"github.com/deckhouse/virtualization-controller/pkg/common/annotations"
37+
"github.com/deckhouse/virtualization-controller/pkg/common/object"
38+
"github.com/deckhouse/virtualization-controller/pkg/common/pointer"
39+
"github.com/deckhouse/virtualization-controller/pkg/controller/conditions"
40+
"github.com/deckhouse/virtualization-controller/pkg/controller/service"
41+
vdsupplements "github.com/deckhouse/virtualization-controller/pkg/controller/vd/internal/supplements"
42+
"github.com/deckhouse/virtualization/api/core/v1alpha2"
43+
"github.com/deckhouse/virtualization/api/core/v1alpha2/vdcondition"
44+
)
45+
46+
type CreatePVCFromVSStep struct {
47+
pvc *corev1.PersistentVolumeClaim
48+
client client.Client
49+
cb *conditions.ConditionBuilder
50+
}
51+
52+
func NewCreatePVCFromVSStep(
53+
pvc *corev1.PersistentVolumeClaim,
54+
client client.Client,
55+
cb *conditions.ConditionBuilder,
56+
) *CreatePVCFromVSStep {
57+
return &CreatePVCFromVSStep{
58+
pvc: pvc,
59+
client: client,
60+
cb: cb,
61+
}
62+
}
63+
64+
func (s CreatePVCFromVSStep) Take(ctx context.Context, vd *v1alpha2.VirtualDisk) (*reconcile.Result, error) {
65+
if s.pvc != nil {
66+
return nil, nil
67+
}
68+
69+
_, exists := vd.Annotations[annotations.AnnUseVolumeSnapshot]
70+
if !exists {
71+
return nil, nil
72+
}
73+
74+
vi, err := object.FetchObject(ctx, types.NamespacedName{
75+
Namespace: vd.Namespace,
76+
Name: vd.Spec.DataSource.ObjectRef.Name,
77+
}, s.client, &v1alpha2.VirtualImage{})
78+
if err != nil {
79+
vd.Status.Phase = v1alpha2.DiskFailed
80+
s.cb.
81+
Status(metav1.ConditionFalse).
82+
Reason(vdcondition.ProvisioningFailed).
83+
Message("The VirtualImage not found")
84+
return &reconcile.Result{}, nil
85+
}
86+
87+
if vi.Status.Target.PersistentVolumeClaim == "" {
88+
vd.Status.Phase = v1alpha2.DiskFailed
89+
s.cb.
90+
Status(metav1.ConditionFalse).
91+
Reason(vdcondition.ProvisioningFailed).
92+
Message("The VirtualImage does not have the target pvc")
93+
return &reconcile.Result{}, nil
94+
}
95+
96+
vs, err := object.FetchObject(ctx, types.NamespacedName{
97+
Namespace: vi.Namespace,
98+
Name: vi.Status.Target.PersistentVolumeClaim,
99+
}, s.client, &vsv1.VolumeSnapshot{})
100+
if err != nil {
101+
vd.Status.Phase = v1alpha2.DiskFailed
102+
s.cb.
103+
Status(metav1.ConditionFalse).
104+
Reason(vdcondition.ProvisioningFailed).
105+
Message("The VolumeSnapshot not found")
106+
return &reconcile.Result{}, nil
107+
}
108+
109+
if vs.Status == nil || !(*vs.Status.ReadyToUse) {
110+
vd.Status.Phase = v1alpha2.DiskFailed
111+
s.cb.
112+
Status(metav1.ConditionFalse).
113+
Reason(vdcondition.ProvisioningFailed).
114+
Message("The VolumeSnapshot is not ready to use")
115+
return &reconcile.Result{}, nil
116+
}
117+
118+
pvc, err := s.buildPVC(vd, vs, vi)
119+
if err != nil {
120+
return nil, fmt.Errorf("failed to build pvc: %w", err)
121+
}
122+
123+
err = s.client.Create(ctx, pvc)
124+
if err != nil && !k8serrors.IsAlreadyExists(err) {
125+
return nil, fmt.Errorf("create pvc: %w", err)
126+
}
127+
128+
vd.Status.Phase = v1alpha2.DiskProvisioning
129+
s.cb.
130+
Status(metav1.ConditionFalse).
131+
Reason(vdcondition.Provisioning).
132+
Message("The PersistentVolumeClaim has been created: waiting for it to be Bound.")
133+
134+
vd.Status.Progress = "0%"
135+
vd.Status.SourceUID = pointer.GetPointer(vi.UID)
136+
vdsupplements.SetPVCName(vd, pvc.Name)
137+
138+
s.addOriginalMetadata(vd, vs)
139+
return nil, nil
140+
}
141+
142+
func (s CreatePVCFromVSStep) buildPVC(vd *v1alpha2.VirtualDisk, vs *vsv1.VolumeSnapshot, vi *v1alpha2.VirtualImage) (*corev1.PersistentVolumeClaim, error) {
143+
var storageClassName string
144+
if vd.Spec.PersistentVolumeClaim.StorageClass != nil && *vd.Spec.PersistentVolumeClaim.StorageClass != "" {
145+
storageClassName = *vd.Spec.PersistentVolumeClaim.StorageClass
146+
} else {
147+
storageClassName = vs.Annotations[annotations.AnnStorageClassName]
148+
if storageClassName == "" {
149+
storageClassName = vs.Annotations[annotations.AnnStorageClassNameDeprecated]
150+
}
151+
}
152+
153+
volumeMode := vs.Annotations[annotations.AnnVolumeMode]
154+
if volumeMode == "" {
155+
volumeMode = vs.Annotations[annotations.AnnVolumeModeDeprecated]
156+
}
157+
accessModesRaw := vs.Annotations[annotations.AnnAccessModes]
158+
if accessModesRaw == "" {
159+
accessModesRaw = vs.Annotations[annotations.AnnAccessModesDeprecated]
160+
}
161+
162+
accessModesStr := strings.Split(accessModesRaw, ",")
163+
accessModes := make([]corev1.PersistentVolumeAccessMode, 0, len(accessModesStr))
164+
for _, accessModeStr := range accessModesStr {
165+
accessModes = append(accessModes, corev1.PersistentVolumeAccessMode(accessModeStr))
166+
}
167+
168+
spec := corev1.PersistentVolumeClaimSpec{
169+
AccessModes: accessModes,
170+
DataSource: &corev1.TypedLocalObjectReference{
171+
APIGroup: ptr.To(vs.GroupVersionKind().Group),
172+
Kind: vs.Kind,
173+
Name: vi.Status.Target.PersistentVolumeClaim,
174+
},
175+
}
176+
177+
if storageClassName != "" {
178+
spec.StorageClassName = &storageClassName
179+
vd.Status.StorageClassName = storageClassName
180+
}
181+
182+
if volumeMode != "" {
183+
spec.VolumeMode = ptr.To(corev1.PersistentVolumeMode(volumeMode))
184+
}
185+
186+
pvcSize, err := s.getPVCSize(vd, vs)
187+
if err != nil {
188+
return nil, err
189+
}
190+
191+
spec.Resources = corev1.VolumeResourceRequirements{
192+
Requests: corev1.ResourceList{
193+
corev1.ResourceStorage: pvcSize,
194+
},
195+
}
196+
197+
pvcKey := vdsupplements.NewGenerator(vd).PersistentVolumeClaim()
198+
199+
return &corev1.PersistentVolumeClaim{
200+
ObjectMeta: metav1.ObjectMeta{
201+
Name: pvcKey.Name,
202+
Namespace: pvcKey.Namespace,
203+
OwnerReferences: []metav1.OwnerReference{
204+
service.MakeOwnerReference(vd),
205+
},
206+
},
207+
Spec: spec,
208+
}, nil
209+
}
210+
211+
func (s CreatePVCFromVSStep) getPVCSize(vd *v1alpha2.VirtualDisk, vs *vsv1.VolumeSnapshot) (resource.Quantity, error) {
212+
if vs.Status == nil || vs.Status.RestoreSize == nil || vs.Status.RestoreSize.IsZero() {
213+
return resource.Quantity{}, errors.New("vs has zero size")
214+
}
215+
216+
if vd.Spec.PersistentVolumeClaim.Size == nil || vd.Spec.PersistentVolumeClaim.Size.IsZero() {
217+
return *vs.Status.RestoreSize, nil
218+
}
219+
220+
if vd.Spec.PersistentVolumeClaim.Size.Cmp(*vs.Status.RestoreSize) == 1 {
221+
return *vd.Spec.PersistentVolumeClaim.Size, nil
222+
}
223+
224+
return *vs.Status.RestoreSize, nil
225+
}
226+
227+
// AddOriginalMetadata adds original annotations and labels from VolumeSnapshot to VirtualDisk,
228+
// without overwriting existing values
229+
func (s CreatePVCFromVSStep) addOriginalMetadata(vd *v1alpha2.VirtualDisk, vs *vsv1.VolumeSnapshot) {
230+
if vd.Annotations == nil {
231+
vd.Annotations = make(map[string]string)
232+
}
233+
if vd.Labels == nil {
234+
vd.Labels = make(map[string]string)
235+
}
236+
237+
if annotationsJSON := vs.Annotations[annotations.AnnVirtualDiskOriginalAnnotations]; annotationsJSON != "" {
238+
var originalAnnotations map[string]string
239+
if err := json.Unmarshal([]byte(annotationsJSON), &originalAnnotations); err == nil {
240+
for key, value := range originalAnnotations {
241+
if _, exists := vd.Annotations[key]; !exists {
242+
vd.Annotations[key] = value
243+
}
244+
}
245+
}
246+
}
247+
248+
if labelsJSON := vs.Annotations[annotations.AnnVirtualDiskOriginalLabels]; labelsJSON != "" {
249+
var originalLabels map[string]string
250+
if err := json.Unmarshal([]byte(labelsJSON), &originalLabels); err == nil {
251+
for key, value := range originalLabels {
252+
if _, exists := vd.Labels[key]; !exists {
253+
vd.Labels[key] = value
254+
}
255+
}
256+
}
257+
}
258+
}

images/virtualization-artifact/pkg/controller/vd/internal/source/step/ensure_node_placement.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import (
2727
"sigs.k8s.io/controller-runtime/pkg/client"
2828
"sigs.k8s.io/controller-runtime/pkg/reconcile"
2929

30+
"github.com/deckhouse/virtualization-controller/pkg/common/annotations"
3031
"github.com/deckhouse/virtualization-controller/pkg/common/provisioner"
3132
"github.com/deckhouse/virtualization-controller/pkg/controller/conditions"
3233
"github.com/deckhouse/virtualization-controller/pkg/controller/service"
@@ -71,6 +72,11 @@ func (s EnsureNodePlacementStep) Take(ctx context.Context, vd *v1alpha2.VirtualD
7172
return nil, nil
7273
}
7374

75+
_, exists := vd.Annotations[annotations.AnnUseVolumeSnapshot]
76+
if exists {
77+
return nil, nil
78+
}
79+
7480
err := s.disk.CheckProvisioning(ctx, s.pvc)
7581
switch {
7682
case err == nil:

images/virtualization-artifact/pkg/controller/vd/internal/source/step/wait_for_dv_step.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import (
2929
"sigs.k8s.io/controller-runtime/pkg/client"
3030
"sigs.k8s.io/controller-runtime/pkg/reconcile"
3131

32+
"github.com/deckhouse/virtualization-controller/pkg/common/annotations"
3233
dvutil "github.com/deckhouse/virtualization-controller/pkg/common/datavolume"
3334
"github.com/deckhouse/virtualization-controller/pkg/common/object"
3435
"github.com/deckhouse/virtualization-controller/pkg/controller/conditions"
@@ -67,6 +68,11 @@ func NewWaitForDVStep(
6768
}
6869

6970
func (s WaitForDVStep) Take(ctx context.Context, vd *v1alpha2.VirtualDisk) (*reconcile.Result, error) {
71+
_, exists := vd.Annotations[annotations.AnnUseVolumeSnapshot]
72+
if exists {
73+
return nil, nil
74+
}
75+
7076
if s.dv == nil {
7177
vd.Status.Phase = v1alpha2.DiskProvisioning
7278
s.cb.

images/virtualization-artifact/pkg/controller/vi/internal/source/http.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -380,7 +380,7 @@ func (ds HTTPDataSource) StoreToPVC(ctx context.Context, vi *v1alpha2.VirtualIma
380380
"The HTTP DataSource import has completed",
381381
)
382382

383-
_, exists := vi.Annotations["virtualization.deckhouse.io/use-volume-snapshot"]
383+
_, exists := vi.Annotations[annotations.AnnUseVolumeSnapshot]
384384
if exists {
385385
var vs *vsv1.VolumeSnapshot
386386
vs, err = ds.diskService.GetVolumeSnapshot(ctx, pvc.Name, pvc.Namespace)

0 commit comments

Comments
 (0)