• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

kubevirt / containerized-data-importer / #5670

16 Nov 2025 09:50AM UTC coverage: 58.748% (-0.1%) from 58.845%
#5670

Pull #3852

travis-ci

arnongilboa
Support storageProfile minimumSupportedPVCSize in clone

When the target DataVolume storage requests a size smaller than the
source PVC. For target without size it already worked correctly.

Signed-off-by: Arnon Gilboa <agilboa@redhat.com>
Pull Request #3852: Support storageProfile minimumSupportedPVCSize in clone

21 of 56 new or added lines in 5 files covered. (37.5%)

7 existing lines in 4 files now uncovered.

17400 of 29618 relevant lines covered (58.75%)

0.65 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

79.46
/pkg/controller/clone/planner.go
1
package clone
2

3
import (
4
        "context"
5
        "fmt"
6
        "reflect"
7
        "sort"
8
        "sync"
9

10
        "github.com/go-logr/logr"
11
        snapshotv1 "github.com/kubernetes-csi/external-snapshotter/client/v6/apis/volumesnapshot/v1"
12

13
        corev1 "k8s.io/api/core/v1"
14
        storagev1 "k8s.io/api/storage/v1"
15
        "k8s.io/apimachinery/pkg/api/meta"
16
        metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
17
        "k8s.io/apimachinery/pkg/labels"
18
        "k8s.io/apimachinery/pkg/types"
19
        "k8s.io/client-go/tools/record"
20

21
        "sigs.k8s.io/controller-runtime/pkg/cache"
22
        "sigs.k8s.io/controller-runtime/pkg/client"
23
        "sigs.k8s.io/controller-runtime/pkg/controller"
24
        "sigs.k8s.io/controller-runtime/pkg/handler"
25
        "sigs.k8s.io/controller-runtime/pkg/reconcile"
26
        "sigs.k8s.io/controller-runtime/pkg/source"
27

28
        cdiv1 "kubevirt.io/containerized-data-importer-api/pkg/apis/core/v1beta1"
29
        cc "kubevirt.io/containerized-data-importer/pkg/controller/common"
30
)
31

32
const (
33
        // CloneValidationFailed reports that a clone wasn't admitted by our validation mechanism (reason)
34
        CloneValidationFailed = "CloneValidationFailed"
35

36
        // MessageCloneValidationFailed reports that a clone wasn't admitted by our validation mechanism (message)
37
        MessageCloneValidationFailed = "The clone doesn't meet the validation requirements"
38

39
        // CloneWithoutSource reports that the source of a clone doesn't exists (reason)
40
        CloneWithoutSource = "CloneWithoutSource"
41

42
        // MessageCloneWithoutSource reports that the source of a clone doesn't exists (message)
43
        MessageCloneWithoutSource = "The source %s %s doesn't exist"
44

45
        // NoVolumeSnapshotClass reports that no compatible volumesnapshotclass was found (reason)
46
        NoVolumeSnapshotClass = "NoVolumeSnapshotClass"
47

48
        // MessageNoVolumeSnapshotClass reports that no compatible volumesnapshotclass was found (message)
49
        MessageNoVolumeSnapshotClass = "No compatible volumesnapshotclass found"
50

51
        // IncompatibleVolumeModes reports that the volume modes of source and target are incompatible (reason)
52
        IncompatibleVolumeModes = "IncompatibleVolumeModes"
53

54
        // MessageIncompatibleVolumeModes reports that the volume modes of source and target are incompatible (message)
55
        MessageIncompatibleVolumeModes = "The volume modes of source and target are incompatible"
56

57
        // NoVolumeExpansion reports that no volume expansion is possible (reason)
58
        NoVolumeExpansion = "NoVolumeExpansion"
59

60
        // MessageNoVolumeExpansion reports that no volume expansion is possible (message)
61
        MessageNoVolumeExpansion = "No volume expansion is possible"
62

63
        // NoProvisionerMatch reports that the storageclass provisioner does not match the volumesnapshotcontent driver (reason)
64
        NoProvisionerMatch = "NoProvisionerMatch"
65

66
        // MessageNoProvisionerMatch reports that the storageclass provisioner does not match the volumesnapshotcontent driver (message)
67
        MessageNoProvisionerMatch = "The storageclass provisioner does not match the volumesnapshotcontent driver"
68

69
        // IncompatibleProvisioners reports that the provisioners are incompatible (reason)
70
        IncompatibleProvisioners = "IncompatibleProvisioners"
71

72
        // MessageIncompatibleProvisioners reports that the provisioners are incompatible (message)
73
        MessageIncompatibleProvisioners = "Provisioners are incompatible"
74
)
75

76
// Planner plans clone operations
77
type Planner struct {
78
        RootObjectType  client.ObjectList
79
        OwnershipLabel  string
80
        UIDField        string
81
        Image           string
82
        PullPolicy      corev1.PullPolicy
83
        InstallerLabels map[string]string
84
        Client          client.Client
85
        Recorder        record.EventRecorder
86
        Controller      controller.Controller
87
        GetCache        func() cache.Cache
88

89
        watchingCore      bool
90
        watchingSnapshots bool
91
        watchMutex        sync.Mutex
92
}
93

94
// Phase is the interface implemented by all clone phases
95
type Phase interface {
96
        Name() string
97
        Reconcile(context.Context) (*reconcile.Result, error)
98
}
99

100
// PhaseStatus contains phase status data
101
type PhaseStatus struct {
102
        Progress    string
103
        Annotations map[string]string
104
}
105

106
// StatusReporter allows a phase to report status
107
type StatusReporter interface {
108
        Status(context.Context) (*PhaseStatus, error)
109
}
110

111
// list of all possible (core) types created
112
var coreTypesCreated = []client.Object{
113
        &corev1.PersistentVolumeClaim{},
114
        &corev1.Pod{},
115
}
116

117
// all types that may have been created
118
var listTypesToDelete = []client.ObjectList{
119
        &corev1.PersistentVolumeClaimList{},
120
        &corev1.PodList{},
121
        &snapshotv1.VolumeSnapshotList{},
122
}
123

124
// AddCoreWatches watches "core" types
125
func (p *Planner) AddCoreWatches(log logr.Logger) error {
×
126
        p.watchMutex.Lock()
×
127
        defer p.watchMutex.Unlock()
×
128
        if p.watchingCore {
×
129
                return nil
×
130
        }
×
131

132
        for _, obj := range coreTypesCreated {
×
133
                if err := p.watchOwned(log, obj); err != nil {
×
134
                        return err
×
135
                }
×
136
        }
137

138
        p.watchingCore = true
×
139

×
140
        return nil
×
141
}
142

143
// ChooseStrategyArgs are args for ChooseStrategy function
144
type ChooseStrategyArgs struct {
145
        Log         logr.Logger
146
        TargetClaim *corev1.PersistentVolumeClaim
147
        DataSource  *cdiv1.VolumeCloneSource
148
}
149

150
// ChooseStrategyResult is result returned by ChooseStrategy function
151
type ChooseStrategyResult struct {
152
        Strategy       cdiv1.CDICloneStrategy
153
        FallbackReason *string
154
}
155

156
// ChooseStrategy picks the strategy for a clone op
157
func (p *Planner) ChooseStrategy(ctx context.Context, args *ChooseStrategyArgs) (*ChooseStrategyResult, error) {
1✔
158
        if IsDataSourcePVC(args.DataSource.Spec.Source.Kind) {
2✔
159
                args.Log.V(3).Info("Getting strategy for PVC source")
1✔
160
                return p.computeStrategyForSourcePVC(ctx, args)
1✔
161
        }
1✔
162
        if IsDataSourceSnapshot(args.DataSource.Spec.Source.Kind) {
2✔
163
                args.Log.V(3).Info("Getting strategy for Snapshot source")
1✔
164
                return p.computeStrategyForSourceSnapshot(ctx, args)
1✔
165
        }
1✔
166
        return nil, fmt.Errorf("unsupported datasource")
1✔
167
}
168

169
// PlanArgs are args to plan clone op for populator
170
type PlanArgs struct {
171
        Log         logr.Logger
172
        TargetClaim *corev1.PersistentVolumeClaim
173
        DataSource  *cdiv1.VolumeCloneSource
174
        Strategy    cdiv1.CDICloneStrategy
175
}
176

177
// Plan creates phases for populator clone
178
func (p *Planner) Plan(ctx context.Context, args *PlanArgs) ([]Phase, error) {
1✔
179
        if args.Strategy == cdiv1.CloneStrategySnapshot {
2✔
180
                if err := p.watchSnapshots(ctx, args.Log); err != nil {
1✔
181
                        return nil, err
×
182
                }
×
183
        }
184

185
        if IsDataSourcePVC(args.DataSource.Spec.Source.Kind) {
2✔
186
                if args.Strategy == cdiv1.CloneStrategyHostAssisted {
2✔
187
                        args.Log.V(3).Info("Planning host assisted clone from PVC")
1✔
188

1✔
189
                        return p.planHostAssistedFromPVC(ctx, args)
1✔
190
                } else if args.Strategy == cdiv1.CloneStrategySnapshot {
3✔
191
                        args.Log.V(3).Info("Planning snapshot clone from PVC")
1✔
192

1✔
193
                        return p.planSnapshotFromPVC(ctx, args)
1✔
194
                } else if args.Strategy == cdiv1.CloneStrategyCsiClone {
3✔
195
                        args.Log.V(3).Info("Planning csi clone from PVC")
1✔
196

1✔
197
                        return p.planCSIClone(ctx, args)
1✔
198
                }
1✔
199
        }
200

201
        if IsDataSourceSnapshot(args.DataSource.Spec.Source.Kind) {
2✔
202
                if args.Strategy == cdiv1.CloneStrategyHostAssisted {
2✔
203
                        args.Log.V(3).Info("Planning host assisted clone from Snapshot")
1✔
204

1✔
205
                        return p.planHostAssistedFromSnapshot(ctx, args)
1✔
206
                } else if args.Strategy == cdiv1.CloneStrategySnapshot {
3✔
207
                        args.Log.V(3).Info("Planning Smart clone from Snapshot")
1✔
208

1✔
209
                        return p.planSmartCloneFromSnapshot(ctx, args)
1✔
210
                }
1✔
211
        }
212

213
        return nil, fmt.Errorf("unknown strategy/source %s", string(args.Strategy))
×
214
}
215

216
// Cleanup cleans up after a clone op
217
func (p *Planner) Cleanup(ctx context.Context, log logr.Logger, owner client.Object) error {
1✔
218
        log.V(3).Info("Cleaning up for obj", "obj", owner)
1✔
219

1✔
220
        for _, lt := range listTypesToDelete {
2✔
221
                ls, err := labels.Parse(fmt.Sprintf("%s=%s", p.OwnershipLabel, string(owner.GetUID())))
1✔
222
                if err != nil {
1✔
223
                        return err
×
224
                }
×
225

226
                lo := &client.ListOptions{
1✔
227
                        LabelSelector: ls,
1✔
228
                }
1✔
229
                if err := cc.BulkDeleteResources(ctx, p.Client, lt, lo); err != nil {
1✔
230
                        return err
×
231
                }
×
232
        }
233

234
        return nil
1✔
235
}
236

237
func (p *Planner) watchSnapshots(ctx context.Context, log logr.Logger) error {
1✔
238
        p.watchMutex.Lock()
1✔
239
        defer p.watchMutex.Unlock()
1✔
240
        if p.watchingSnapshots {
2✔
241
                return nil
1✔
242
        }
1✔
243

244
        vsl := &snapshotv1.VolumeSnapshotList{}
×
245
        lo := &client.ListOptions{Limit: 1}
×
246
        if err := p.Client.List(ctx, vsl, lo); err != nil {
×
247
                if meta.IsNoMatchError(err) {
×
248
                        return nil
×
249
                }
×
250
        }
251

252
        if err := p.watchOwned(log, &snapshotv1.VolumeSnapshot{}); err != nil {
×
253
                return err
×
254
        }
×
255

256
        log.V(3).Info("watching volumesnapshots now")
×
257
        p.watchingSnapshots = true
×
258

×
259
        return nil
×
260
}
261

262
func (p *Planner) watchOwned(log logr.Logger, obj client.Object) error {
×
263
        objList := p.RootObjectType.DeepCopyObject().(client.ObjectList)
×
264
        if err := p.Controller.Watch(source.Kind(p.GetCache(), obj, handler.EnqueueRequestsFromMapFunc(
×
265
                func(ctx context.Context, obj client.Object) []reconcile.Request {
×
266
                        uid, ok := obj.GetLabels()[p.OwnershipLabel]
×
267
                        if !ok {
×
268
                                return nil
×
269
                        }
×
270
                        matchingFields := client.MatchingFields{
×
271
                                p.UIDField: uid,
×
272
                        }
×
273
                        if err := p.Client.List(ctx, objList, matchingFields); err != nil {
×
274
                                log.Error(err, "Unable to list resource", "matchingFields", matchingFields)
×
275
                                return nil
×
276
                        }
×
277
                        sv := reflect.ValueOf(objList).Elem()
×
278
                        iv := sv.FieldByName("Items")
×
279
                        var reqs []reconcile.Request
×
280
                        for i := 0; i < iv.Len(); i++ {
×
281
                                o := iv.Index(i).Addr().Interface().(client.Object)
×
282
                                reqs = append(reqs, reconcile.Request{
×
283
                                        NamespacedName: types.NamespacedName{
×
284
                                                Namespace: o.GetNamespace(),
×
285
                                                Name:      o.GetName(),
×
286
                                        },
×
287
                                })
×
288
                        }
×
289
                        return reqs
×
290
                }),
291
        )); err != nil {
×
292
                return err
×
293
        }
×
294
        return nil
×
295
}
296

297
func (p *Planner) computeStrategyForSourcePVC(ctx context.Context, args *ChooseStrategyArgs) (*ChooseStrategyResult, error) {
1✔
298
        res := &ChooseStrategyResult{}
1✔
299

1✔
300
        if ok, err := p.validateTargetStorageClassAssignment(ctx, args); !ok || err != nil {
2✔
301
                return nil, err
1✔
302
        }
1✔
303

304
        sourceClaim := &corev1.PersistentVolumeClaim{}
1✔
305
        exists, err := getResource(ctx, p.Client, args.DataSource.Namespace, args.DataSource.Spec.Source.Name, sourceClaim)
1✔
306
        if err != nil {
1✔
307
                return nil, err
×
308
        }
×
309

310
        if !exists {
2✔
311
                message := fmt.Sprintf(MessageCloneWithoutSource, "pvc", args.DataSource.Spec.Source.Name)
1✔
312
                p.Recorder.Event(args.TargetClaim, corev1.EventTypeWarning, CloneWithoutSource, message)
1✔
313
                args.Log.V(3).Info("Source PVC does not exist, cannot compute strategy")
1✔
314
                return nil, nil
1✔
315
        }
1✔
316

317
        if err = p.validateSourcePVC(args, sourceClaim); err != nil {
2✔
318
                p.Recorder.Event(args.TargetClaim, corev1.EventTypeWarning, CloneValidationFailed, MessageCloneValidationFailed)
1✔
319
                args.Log.V(3).Info("Validation failed", "target", args.TargetClaim, "source", sourceClaim)
1✔
320
                return nil, err
1✔
321
        }
1✔
322

323
        strategy := cdiv1.CloneStrategySnapshot
1✔
324
        cs, err := GetGlobalCloneStrategyOverride(ctx, p.Client)
1✔
325
        if err != nil {
1✔
326
                return nil, err
×
327
        }
×
328

329
        if cs != nil {
2✔
330
                strategy = *cs
1✔
331
        } else if args.TargetClaim.Spec.StorageClassName != nil {
3✔
332
                sp := &cdiv1.StorageProfile{}
1✔
333
                exists, err := getResource(ctx, p.Client, metav1.NamespaceNone, *args.TargetClaim.Spec.StorageClassName, sp)
1✔
334
                if err != nil {
1✔
335
                        return nil, err
×
336
                }
×
337

338
                if !exists {
2✔
339
                        args.Log.V(3).Info("missing storageprofile for", "name", *args.TargetClaim.Spec.StorageClassName)
1✔
340
                }
1✔
341

342
                if exists && sp.Status.CloneStrategy != nil {
2✔
343
                        strategy = *sp.Status.CloneStrategy
1✔
344
                }
1✔
345
        }
346

347
        if strategy == cdiv1.CloneStrategySnapshot {
2✔
348
                n, err := GetCompatibleVolumeSnapshotClass(ctx, p.Client, args.Log, p.Recorder, sourceClaim, args.TargetClaim)
1✔
349
                if err != nil {
1✔
350
                        return nil, err
×
351
                }
×
352

353
                if n == nil {
2✔
354
                        p.fallbackToHostAssisted(args.TargetClaim, res, NoVolumeSnapshotClass, MessageNoVolumeSnapshotClass)
1✔
355
                        return res, nil
1✔
356
                }
1✔
357
        }
358

359
        res.Strategy = strategy
1✔
360
        if strategy == cdiv1.CloneStrategySnapshot ||
1✔
361
                strategy == cdiv1.CloneStrategyCsiClone {
2✔
362
                if err := p.validateAdvancedClonePVC(ctx, args, res, sourceClaim); err != nil {
1✔
363
                        return nil, err
×
364
                }
×
365
        }
366

367
        return res, nil
1✔
368
}
369

370
func (p *Planner) computeStrategyForSourceSnapshot(ctx context.Context, args *ChooseStrategyArgs) (*ChooseStrategyResult, error) {
1✔
371
        res := &ChooseStrategyResult{}
1✔
372

1✔
373
        if ok, err := p.validateTargetStorageClassAssignment(ctx, args); !ok || err != nil {
2✔
374
                return nil, err
1✔
375
        }
1✔
376

377
        // Check that snapshot exists
378
        sourceSnapshot := &snapshotv1.VolumeSnapshot{}
1✔
379
        exists, err := getResource(ctx, p.Client, args.DataSource.Namespace, args.DataSource.Spec.Source.Name, sourceSnapshot)
1✔
380
        if err != nil {
1✔
381
                return nil, err
×
382
        }
×
383
        if !exists {
2✔
384
                message := fmt.Sprintf(MessageCloneWithoutSource, "snapshot", args.DataSource.Spec.Source.Name)
1✔
385
                p.Recorder.Event(args.TargetClaim, corev1.EventTypeWarning, CloneWithoutSource, message)
1✔
386
                args.Log.V(3).Info("Source Snapshot does not exist, cannot compute strategy")
1✔
387
                return nil, nil
1✔
388
        }
1✔
389

390
        // Do snapshot and storage class validation
391
        vsc, err := GetSnapshotContentFromSnapshot(ctx, p.Client, sourceSnapshot)
1✔
392
        if err != nil {
2✔
393
                return nil, err
1✔
394
        }
1✔
395
        targetStorageClass, err := GetStorageClassForClaim(ctx, p.Client, args.TargetClaim)
1✔
396
        if err != nil {
1✔
397
                return nil, err
×
398
        }
×
399
        if targetStorageClass == nil {
1✔
400
                return nil, fmt.Errorf("target claim's storageclass doesn't exist, clone will not work")
×
401
        }
×
402
        valid, err := cc.ValidateSnapshotCloneProvisioners(vsc, targetStorageClass)
1✔
403
        if err != nil {
1✔
404
                return nil, err
×
405
        }
×
406
        if !valid {
2✔
407
                p.fallbackToHostAssisted(args.TargetClaim, res, NoProvisionerMatch, MessageNoProvisionerMatch)
1✔
408
                args.Log.V(3).Info("Provisioner differs, need to fall back to host assisted")
1✔
409
                return res, nil
1✔
410
        }
1✔
411

412
        // do size validation
413
        valid, err = cc.ValidateSnapshotCloneSize(sourceSnapshot, &args.TargetClaim.Spec, targetStorageClass, args.Log)
1✔
414
        if err != nil {
2✔
415
                return nil, err
1✔
416
        }
1✔
417
        if !valid {
2✔
418
                p.fallbackToHostAssisted(args.TargetClaim, res, NoVolumeExpansion, MessageNoVolumeExpansion)
1✔
419
                return res, nil
1✔
420
        }
1✔
421

422
        // Lastly, do volume mode validation to determine whether to use dumb or smart cloning
423
        if !SameVolumeMode(vsc.Spec.SourceVolumeMode, args.TargetClaim) {
2✔
424
                p.fallbackToHostAssisted(args.TargetClaim, res, IncompatibleVolumeModes, MessageIncompatibleVolumeModes)
1✔
425
                args.Log.V(3).Info("Volume modes differs, need to fall back to host assisted - Snapshot")
1✔
426
                return res, nil
1✔
427
        }
1✔
428

429
        res.Strategy = cdiv1.CloneStrategySnapshot
1✔
430
        return res, nil
1✔
431
}
432

433
func (p *Planner) validateTargetStorageClassAssignment(ctx context.Context, args *ChooseStrategyArgs) (bool, error) {
1✔
434
        if args.TargetClaim.Spec.StorageClassName == nil {
2✔
435
                args.Log.V(3).Info("Target PVC has nil storage class, cannot compute strategy")
1✔
436
                return false, nil
1✔
437
        }
1✔
438

439
        if *args.TargetClaim.Spec.StorageClassName == "" {
2✔
440
                args.Log.V(3).Info("Target PVC has \"\" storage class, cannot compute strategy")
1✔
441
                return false, fmt.Errorf("claim has emptystring storageclass, will not work")
1✔
442
        }
1✔
443

444
        sc, err := GetStorageClassForClaim(ctx, p.Client, args.TargetClaim)
1✔
445
        if err != nil {
1✔
446
                return false, err
×
447
        }
×
448

449
        if sc == nil {
2✔
450
                args.Log.V(3).Info("Target PVC has no storage class, cannot compute strategy")
1✔
451
                return false, fmt.Errorf("target storage class not found")
1✔
452
        }
1✔
453

454
        return true, nil
1✔
455
}
456

457
func (p *Planner) validateSourcePVC(args *ChooseStrategyArgs, sourceClaim *corev1.PersistentVolumeClaim) error {
1✔
458
        _, permissive := args.TargetClaim.Annotations[cc.AnnPermissiveClone]
1✔
459
        if permissive {
1✔
460
                args.Log.V(3).Info("permissive clone annotation found, skipping size validation")
×
461
                return nil
×
462
        }
×
463
        targetResources, err := cc.GetEffectiveStorageResources(context.TODO(), p.Client, args.TargetClaim.Spec.Resources,
1✔
464
                args.TargetClaim.Spec.StorageClassName, cc.GetPVCContentType(args.TargetClaim), args.Log)
1✔
465
        if err != nil {
1✔
NEW
466
                return err
×
NEW
467
        }
×
468

469
        if err := cc.ValidateRequestedCloneSize(sourceClaim.Spec.Resources, targetResources); err != nil {
2✔
470
                p.Recorder.Eventf(args.TargetClaim, corev1.EventTypeWarning, cc.ErrIncompatiblePVC, err.Error())
1✔
471
                return err
1✔
472
        }
1✔
473

474
        return nil
1✔
475
}
476

477
func (p *Planner) validateAdvancedClonePVC(ctx context.Context, args *ChooseStrategyArgs, res *ChooseStrategyResult, sourceClaim *corev1.PersistentVolumeClaim) error {
1✔
478
        driver, err := GetCommonDriver(ctx, p.Client, sourceClaim, args.TargetClaim)
1✔
479
        if err != nil {
1✔
480
                return err
×
481
        }
×
482

483
        if driver == nil {
2✔
484
                p.fallbackToHostAssisted(args.TargetClaim, res, IncompatibleProvisioners, MessageIncompatibleProvisioners)
1✔
485
                args.Log.V(3).Info("CSIDrivers not compatible for advanced clone")
1✔
486
                return nil
1✔
487
        }
1✔
488

489
        if !SameVolumeMode(sourceClaim.Spec.VolumeMode, args.TargetClaim) {
2✔
490
                p.fallbackToHostAssisted(args.TargetClaim, res, IncompatibleVolumeModes, MessageIncompatibleVolumeModes)
1✔
491
                args.Log.V(3).Info("volume modes not compatible for advanced clone")
1✔
492
                return nil
1✔
493
        }
1✔
494

495
        sc, err := GetStorageClassForClaim(ctx, p.Client, args.TargetClaim)
1✔
496
        if err != nil {
1✔
497
                return err
×
498
        }
×
499

500
        if sc == nil {
1✔
501
                args.Log.V(3).Info("target storage class not found")
×
502
                return fmt.Errorf("target storage class not found")
×
503
        }
×
504

505
        srcCapacity, hasSrcCapacity := sourceClaim.Status.Capacity[corev1.ResourceStorage]
1✔
506
        targetRequest, hasTargetRequest := args.TargetClaim.Spec.Resources.Requests[corev1.ResourceStorage]
1✔
507
        allowExpansion := sc.AllowVolumeExpansion != nil && *sc.AllowVolumeExpansion
1✔
508
        if !hasSrcCapacity || !hasTargetRequest {
1✔
509
                return fmt.Errorf("source/target size info missing")
×
510
        }
×
511

512
        if srcCapacity.Cmp(targetRequest) < 0 && !allowExpansion {
2✔
513
                p.fallbackToHostAssisted(args.TargetClaim, res, NoVolumeExpansion, MessageNoVolumeExpansion)
1✔
514
                args.Log.V(3).Info("advanced clone not possible, no volume expansion")
1✔
515
        }
1✔
516

517
        return nil
1✔
518
}
519

520
func (p *Planner) fallbackToHostAssisted(targetClaim *corev1.PersistentVolumeClaim, res *ChooseStrategyResult, reason, message string) {
1✔
521
        res.Strategy = cdiv1.CloneStrategyHostAssisted
1✔
522
        res.FallbackReason = &message
1✔
523
        p.Recorder.Event(targetClaim, corev1.EventTypeWarning, reason, message)
1✔
524
}
1✔
525

526
func (p *Planner) planHostAssistedFromPVC(ctx context.Context, args *PlanArgs) ([]Phase, error) {
1✔
527
        desiredClaim := createDesiredClaim(args.DataSource.Namespace, args.TargetClaim)
1✔
528

1✔
529
        hcp := &HostClonePhase{
1✔
530
                Owner:          args.TargetClaim,
1✔
531
                Namespace:      args.DataSource.Namespace,
1✔
532
                SourceName:     args.DataSource.Spec.Source.Name,
1✔
533
                DesiredClaim:   desiredClaim,
1✔
534
                ImmediateBind:  true,
1✔
535
                OwnershipLabel: p.OwnershipLabel,
1✔
536
                Preallocation:  cc.GetPreallocation(ctx, p.Client, args.DataSource.Spec.Preallocation),
1✔
537
                Client:         p.Client,
1✔
538
                Log:            args.Log,
1✔
539
                Recorder:       p.Recorder,
1✔
540
        }
1✔
541

1✔
542
        if args.DataSource.Spec.PriorityClassName != nil {
1✔
543
                hcp.PriorityClassName = *args.DataSource.Spec.PriorityClassName
×
544
        }
×
545

546
        rp := &RebindPhase{
1✔
547
                SourceNamespace: desiredClaim.Namespace,
1✔
548
                SourceName:      desiredClaim.Name,
1✔
549
                TargetNamespace: args.TargetClaim.Namespace,
1✔
550
                TargetName:      args.TargetClaim.Name,
1✔
551
                Client:          p.Client,
1✔
552
                Log:             args.Log,
1✔
553
                Recorder:        p.Recorder,
1✔
554
        }
1✔
555

1✔
556
        return []Phase{hcp, rp}, nil
1✔
557
}
558

559
func (p *Planner) planHostAssistedFromSnapshot(ctx context.Context, args *PlanArgs) ([]Phase, error) {
1✔
560
        sourceSnapshot := &snapshotv1.VolumeSnapshot{}
1✔
561
        exists, err := getResource(ctx, p.Client, args.DataSource.Namespace, args.DataSource.Spec.Source.Name, sourceSnapshot)
1✔
562
        if err != nil {
1✔
563
                return nil, err
×
564
        }
×
565
        if !exists {
1✔
566
                return nil, fmt.Errorf("source claim does not exist")
×
567
        }
×
568

569
        sourceClaimForDumbClone, err := createTempSourceClaim(ctx, args.Log, args.DataSource.Namespace, args.TargetClaim, sourceSnapshot, p.Client)
1✔
570
        if err != nil {
2✔
571
                return nil, err
1✔
572
        }
1✔
573
        cfsp := &SnapshotClonePhase{
1✔
574
                Owner:          args.TargetClaim,
1✔
575
                Namespace:      args.DataSource.Namespace,
1✔
576
                SourceName:     args.DataSource.Spec.Source.Name,
1✔
577
                DesiredClaim:   sourceClaimForDumbClone,
1✔
578
                OwnershipLabel: p.OwnershipLabel,
1✔
579
                Client:         p.Client,
1✔
580
                Log:            args.Log,
1✔
581
                Recorder:       p.Recorder,
1✔
582
        }
1✔
583

1✔
584
        pcp := &PrepClaimPhase{
1✔
585
                Owner:           args.TargetClaim,
1✔
586
                DesiredClaim:    sourceClaimForDumbClone.DeepCopy(),
1✔
587
                Image:           p.Image,
1✔
588
                PullPolicy:      p.PullPolicy,
1✔
589
                InstallerLabels: p.InstallerLabels,
1✔
590
                OwnershipLabel:  p.OwnershipLabel,
1✔
591
                Client:          p.Client,
1✔
592
                Log:             args.Log,
1✔
593
                Recorder:        p.Recorder,
1✔
594
        }
1✔
595

1✔
596
        desiredClaim := createDesiredClaim(args.DataSource.Namespace, args.TargetClaim)
1✔
597

1✔
598
        hcp := &HostClonePhase{
1✔
599
                Owner:          args.TargetClaim,
1✔
600
                Namespace:      sourceClaimForDumbClone.Namespace,
1✔
601
                SourceName:     sourceClaimForDumbClone.Name,
1✔
602
                DesiredClaim:   desiredClaim,
1✔
603
                ImmediateBind:  true,
1✔
604
                OwnershipLabel: p.OwnershipLabel,
1✔
605
                Client:         p.Client,
1✔
606
                Log:            args.Log,
1✔
607
                Recorder:       p.Recorder,
1✔
608
        }
1✔
609

1✔
610
        if args.DataSource.Spec.PriorityClassName != nil {
1✔
611
                hcp.PriorityClassName = *args.DataSource.Spec.PriorityClassName
×
612
        }
×
613

614
        rp := &RebindPhase{
1✔
615
                SourceNamespace: desiredClaim.Namespace,
1✔
616
                SourceName:      desiredClaim.Name,
1✔
617
                TargetNamespace: args.TargetClaim.Namespace,
1✔
618
                TargetName:      args.TargetClaim.Name,
1✔
619
                Client:          p.Client,
1✔
620
                Log:             args.Log,
1✔
621
                Recorder:        p.Recorder,
1✔
622
        }
1✔
623

1✔
624
        return []Phase{cfsp, pcp, hcp, rp}, nil
1✔
625
}
626

627
func (p *Planner) planSmartCloneFromSnapshot(ctx context.Context, args *PlanArgs) ([]Phase, error) {
1✔
628
        sourceSnapshot := &snapshotv1.VolumeSnapshot{}
1✔
629
        exists, err := getResource(ctx, p.Client, args.DataSource.Namespace, args.DataSource.Spec.Source.Name, sourceSnapshot)
1✔
630
        if err != nil {
1✔
631
                return nil, err
×
632
        }
×
633
        if !exists {
1✔
634
                return nil, fmt.Errorf("source claim does not exist")
×
635
        }
×
636

637
        desiredClaim := createDesiredClaim(args.DataSource.Namespace, args.TargetClaim)
1✔
638
        cfsp := &SnapshotClonePhase{
1✔
639
                Owner:          args.TargetClaim,
1✔
640
                Namespace:      args.DataSource.Namespace,
1✔
641
                SourceName:     args.DataSource.Spec.Source.Name,
1✔
642
                DesiredClaim:   desiredClaim.DeepCopy(),
1✔
643
                OwnershipLabel: p.OwnershipLabel,
1✔
644
                Client:         p.Client,
1✔
645
                Log:            args.Log,
1✔
646
                Recorder:       p.Recorder,
1✔
647
        }
1✔
648

1✔
649
        pcp := &PrepClaimPhase{
1✔
650
                Owner:           args.TargetClaim,
1✔
651
                DesiredClaim:    desiredClaim.DeepCopy(),
1✔
652
                Image:           p.Image,
1✔
653
                PullPolicy:      p.PullPolicy,
1✔
654
                InstallerLabels: p.InstallerLabels,
1✔
655
                OwnershipLabel:  p.OwnershipLabel,
1✔
656
                Client:          p.Client,
1✔
657
                Log:             args.Log,
1✔
658
                Recorder:        p.Recorder,
1✔
659
        }
1✔
660

1✔
661
        rp := &RebindPhase{
1✔
662
                SourceNamespace: desiredClaim.Namespace,
1✔
663
                SourceName:      desiredClaim.Name,
1✔
664
                TargetNamespace: args.TargetClaim.Namespace,
1✔
665
                TargetName:      args.TargetClaim.Name,
1✔
666
                Client:          p.Client,
1✔
667
                Log:             args.Log,
1✔
668
                Recorder:        p.Recorder,
1✔
669
        }
1✔
670

1✔
671
        return []Phase{cfsp, pcp, rp}, nil
1✔
672
}
673

674
func (p *Planner) planSnapshotFromPVC(ctx context.Context, args *PlanArgs) ([]Phase, error) {
1✔
675
        sourceClaim := &corev1.PersistentVolumeClaim{}
1✔
676
        exists, err := getResource(ctx, p.Client, args.DataSource.Namespace, args.DataSource.Spec.Source.Name, sourceClaim)
1✔
677
        if err != nil {
1✔
678
                return nil, err
×
679
        }
×
680

681
        if !exists {
1✔
682
                return nil, fmt.Errorf("source claim does not exist")
×
683
        }
×
684

685
        vsc, err := GetCompatibleVolumeSnapshotClass(ctx, p.Client, args.Log, p.Recorder, args.TargetClaim)
1✔
686
        if err != nil {
1✔
687
                return nil, err
×
688
        }
×
689

690
        if vsc == nil {
1✔
691
                return nil, fmt.Errorf("no compatible volumesnapshotclass")
×
692
        }
×
693

694
        sp := &SnapshotPhase{
1✔
695
                Owner:               args.TargetClaim,
1✔
696
                SourceNamespace:     args.DataSource.Namespace,
1✔
697
                SourceName:          args.DataSource.Spec.Source.Name,
1✔
698
                TargetName:          fmt.Sprintf("tmp-snapshot-%s", string(args.TargetClaim.UID)),
1✔
699
                VolumeSnapshotClass: *vsc,
1✔
700
                OwnershipLabel:      p.OwnershipLabel,
1✔
701
                Client:              p.Client,
1✔
702
                Log:                 args.Log,
1✔
703
                Recorder:            p.Recorder,
1✔
704
        }
1✔
705

1✔
706
        desiredClaim := createDesiredClaim(args.DataSource.Namespace, args.TargetClaim)
1✔
707
        cfsp := &SnapshotClonePhase{
1✔
708
                Owner:          args.TargetClaim,
1✔
709
                Namespace:      args.DataSource.Namespace,
1✔
710
                SourceName:     sp.TargetName,
1✔
711
                DesiredClaim:   desiredClaim.DeepCopy(),
1✔
712
                OwnershipLabel: p.OwnershipLabel,
1✔
713
                Client:         p.Client,
1✔
714
                Log:            args.Log,
1✔
715
                Recorder:       p.Recorder,
1✔
716
        }
1✔
717

1✔
718
        pcp := &PrepClaimPhase{
1✔
719
                Owner:           args.TargetClaim,
1✔
720
                DesiredClaim:    desiredClaim.DeepCopy(),
1✔
721
                Image:           p.Image,
1✔
722
                PullPolicy:      p.PullPolicy,
1✔
723
                InstallerLabels: p.InstallerLabels,
1✔
724
                OwnershipLabel:  p.OwnershipLabel,
1✔
725
                Client:          p.Client,
1✔
726
                Log:             args.Log,
1✔
727
                Recorder:        p.Recorder,
1✔
728
        }
1✔
729

1✔
730
        rp := &RebindPhase{
1✔
731
                SourceNamespace: desiredClaim.Namespace,
1✔
732
                SourceName:      desiredClaim.Name,
1✔
733
                TargetNamespace: args.TargetClaim.Namespace,
1✔
734
                TargetName:      args.TargetClaim.Name,
1✔
735
                Client:          p.Client,
1✔
736
                Log:             args.Log,
1✔
737
                Recorder:        p.Recorder,
1✔
738
        }
1✔
739

1✔
740
        return []Phase{sp, cfsp, pcp, rp}, nil
1✔
741
}
742

743
func (p *Planner) planCSIClone(ctx context.Context, args *PlanArgs) ([]Phase, error) {
1✔
744
        desiredClaim := createDesiredClaim(args.DataSource.Namespace, args.TargetClaim)
1✔
745
        cp := &CSIClonePhase{
1✔
746
                Owner:          args.TargetClaim,
1✔
747
                Namespace:      args.DataSource.Namespace,
1✔
748
                SourceName:     args.DataSource.Spec.Source.Name,
1✔
749
                DesiredClaim:   desiredClaim.DeepCopy(),
1✔
750
                OwnershipLabel: p.OwnershipLabel,
1✔
751
                Client:         p.Client,
1✔
752
                Log:            args.Log,
1✔
753
                Recorder:       p.Recorder,
1✔
754
        }
1✔
755

1✔
756
        pcp := &PrepClaimPhase{
1✔
757
                Owner:           args.TargetClaim,
1✔
758
                DesiredClaim:    desiredClaim.DeepCopy(),
1✔
759
                Image:           p.Image,
1✔
760
                PullPolicy:      p.PullPolicy,
1✔
761
                InstallerLabels: p.InstallerLabels,
1✔
762
                OwnershipLabel:  p.OwnershipLabel,
1✔
763
                Client:          p.Client,
1✔
764
                Log:             args.Log,
1✔
765
                Recorder:        p.Recorder,
1✔
766
        }
1✔
767

1✔
768
        rp := &RebindPhase{
1✔
769
                SourceNamespace: desiredClaim.Namespace,
1✔
770
                SourceName:      desiredClaim.Name,
1✔
771
                TargetNamespace: args.TargetClaim.Namespace,
1✔
772
                TargetName:      args.TargetClaim.Name,
1✔
773
                Client:          p.Client,
1✔
774
                Log:             args.Log,
1✔
775
                Recorder:        p.Recorder,
1✔
776
        }
1✔
777

1✔
778
        return []Phase{cp, pcp, rp}, nil
1✔
779
}
1✔
780

781
func createDesiredClaim(namespace string, targetClaim *corev1.PersistentVolumeClaim) *corev1.PersistentVolumeClaim {
1✔
782
        targetCpy := targetClaim.DeepCopy()
1✔
783
        desiredClaim := &corev1.PersistentVolumeClaim{
1✔
784
                ObjectMeta: metav1.ObjectMeta{
1✔
785
                        Namespace:   namespace,
1✔
786
                        Name:        fmt.Sprintf("tmp-pvc-%s", string(targetClaim.UID)),
1✔
787
                        Labels:      targetCpy.Labels,
1✔
788
                        Annotations: targetCpy.Annotations,
1✔
789
                },
1✔
790
                Spec: targetCpy.Spec,
1✔
791
        }
1✔
792
        desiredClaim.Spec.DataSource = nil
1✔
793
        desiredClaim.Spec.DataSourceRef = nil
1✔
794

1✔
795
        return desiredClaim
1✔
796
}
1✔
797

798
func createTempSourceClaim(ctx context.Context, log logr.Logger, namespace string, targetClaim *corev1.PersistentVolumeClaim, snapshot *snapshotv1.VolumeSnapshot, client client.Client) (*corev1.PersistentVolumeClaim, error) {
1✔
799
        vsc, err := GetSnapshotContentFromSnapshot(ctx, client, snapshot)
1✔
800
        if err != nil {
1✔
801
                return nil, err
×
802
        }
×
803
        scName, err := getStorageClassNameForTempSourceClaim(ctx, vsc, client)
1✔
804
        if err != nil {
2✔
805
                return nil, err
1✔
806
        }
1✔
807
        targetCpy := targetClaim.DeepCopy()
1✔
808
        fallbackVolumeMode := targetCpy.Spec.VolumeMode
1✔
809
        volumeMode, err := getVolumeModeForTempSourceClaim(log, snapshot, vsc, fallbackVolumeMode)
1✔
810
        if err != nil {
1✔
811
                return nil, err
×
812
        }
×
813
        // Get the appropriate size from the snapshot
814
        if snapshot.Status == nil || snapshot.Status.RestoreSize == nil || snapshot.Status.RestoreSize.Sign() == -1 {
1✔
815
                return nil, fmt.Errorf("snapshot has no RestoreSize")
×
816
        }
×
817
        restoreSize := snapshot.Status.RestoreSize
1✔
818
        if restoreSize.IsZero() {
1✔
819
                reqSize := targetCpy.Spec.Resources.Requests[corev1.ResourceStorage]
×
820
                restoreSize = &reqSize
×
821
        }
×
822
        delete(targetCpy.Annotations, cc.AnnSelectedNode)
1✔
823

1✔
824
        desiredClaim := &corev1.PersistentVolumeClaim{
1✔
825
                ObjectMeta: metav1.ObjectMeta{
1✔
826
                        Namespace:   namespace,
1✔
827
                        Name:        fmt.Sprintf("tmp-source-pvc-%s", string(targetClaim.UID)),
1✔
828
                        Labels:      targetCpy.Labels,
1✔
829
                        Annotations: targetCpy.Annotations,
1✔
830
                },
1✔
831
                Spec: corev1.PersistentVolumeClaimSpec{
1✔
832
                        StorageClassName: &scName,
1✔
833
                        // We've found that ReadWriteOnce is consensus among CSI drivers
1✔
834
                        // Although we know this is read only at all times, some drivers disallow mounting a block PVC ReadOnly
1✔
835
                        AccessModes: []corev1.PersistentVolumeAccessMode{
1✔
836
                                corev1.ReadWriteOnce,
1✔
837
                        },
1✔
838
                        VolumeMode: volumeMode,
1✔
839
                        Resources: corev1.VolumeResourceRequirements{
1✔
840
                                Requests: corev1.ResourceList{
1✔
841
                                        corev1.ResourceStorage: *restoreSize,
1✔
842
                                },
1✔
843
                        },
1✔
844
                },
1✔
845
        }
1✔
846

1✔
847
        return desiredClaim, nil
1✔
848
}
849

850
func getStorageClassNameForTempSourceClaim(ctx context.Context, vsc *snapshotv1.VolumeSnapshotContent, client client.Client) (string, error) {
1✔
851
        var matches []string
1✔
852

1✔
853
        // Attempting to get a storageClass compatible with the source snapshot
1✔
854
        storageClasses := &storagev1.StorageClassList{}
1✔
855
        if err := client.List(ctx, storageClasses); err != nil {
1✔
856
                return "", err
×
857
        }
×
858
        for _, storageClass := range storageClasses.Items {
2✔
859
                if storageClass.Provisioner == vsc.Spec.Driver {
2✔
860
                        matches = append(matches, storageClass.Name)
1✔
861
                }
1✔
862
        }
863
        if len(matches) == 0 {
2✔
864
                return "", fmt.Errorf("unable to find a valid storage class for the temporal source claim")
1✔
865
        }
1✔
866
        sort.Strings(matches)
1✔
867
        return matches[0], nil
1✔
868
}
869

870
func getVolumeModeForTempSourceClaim(log logr.Logger, snapshot *snapshotv1.VolumeSnapshot, vsc *snapshotv1.VolumeSnapshotContent, fallback *corev1.PersistentVolumeMode) (*corev1.PersistentVolumeMode, error) {
1✔
871
        if vsc.Spec.SourceVolumeMode != nil {
2✔
872
                // Since 1.29 we should always return here
1✔
873
                // Older versions did not populate this field and thus need more care
1✔
874
                return vsc.Spec.SourceVolumeMode, nil
1✔
875
        }
1✔
876

877
        if v, ok := snapshot.Annotations[cc.AnnSourceVolumeMode]; ok {
2✔
878
                mode := corev1.PersistentVolumeMode(v)
1✔
879
                return &mode, nil
1✔
880
        }
1✔
881

882
        log.V(1).Info("Could not infer source volume mode of snapshot, creating a temporary restore with target PVC volume mode")
1✔
883
        return fallback, nil
1✔
884
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc