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

kubevirt / containerized-data-importer / #5783

20 Jan 2026 07:26PM UTC coverage: 49.456% (+0.02%) from 49.439%
#5783

Pull #3991

travis-ci

noamasu
Add provisioner-aware DataImportCron configuration via StorageProfile annotations

Add support for provisioner-specific requirements when creating snapshots
and PVCs for DataImportCron. Some provisioners have specific needs:

- GKE Persistent Disk requires snapshot-type: images parameter in VSC
- GKE Persistent Disk and Rook Ceph RBD require RWO access mode for DataImportCron PVCs

Change details:
- Add StorageProfile annotations for DataImportCron configuration:
    cdi.kubevirt.io/useReadWriteOnceForDataImportCron: Signals RWO access mode
    cdi.kubevirt.io/snapshotClassForDataImportCron: Specifies VSC name
- Centralize provisioner-specific configuration in storagecapabilities:
    UseReadWriteOnceForDataImportCronByProvisionerKey: Maps provisioners requiring RWO
    SnapshotClassParametersForDataImportCronByProvisionerKey: Maps provisioners to VSC parameters
- StorageProfile controller automatically reconciles annotations based on provisioner
- DataImportCron controller applies RWO from StorageProfile when DV doesn't specify access modes
- DataImportCron controller selects VSC with priority: StorageProfile annotation > StorageProfile status > standard selection
- Unit tests for both controllers
- Update documentation with annotation details

Signed-off-by: Noam Assouline <nassouli@redhat.com>
Pull Request #3991: Add provisioner-aware VolumeSnapshotClass selection and RWO access mode for DataImportCron

82 of 147 new or added lines in 3 files covered. (55.78%)

2 existing lines in 1 file now uncovered.

14689 of 29701 relevant lines covered (49.46%)

0.55 hits per line

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

68.61
/pkg/controller/storageprofile-controller.go
1
package controller
2

3
import (
4
        "context"
5
        "errors"
6
        "fmt"
7
        "reflect"
8
        "sort"
9
        "strconv"
10

11
        "github.com/go-logr/logr"
12
        snapshotv1 "github.com/kubernetes-csi/external-snapshotter/client/v6/apis/volumesnapshot/v1"
13
        ocpconfigv1 "github.com/openshift/api/config/v1"
14
        "github.com/prometheus/client_golang/prometheus"
15

16
        v1 "k8s.io/api/core/v1"
17
        storagev1 "k8s.io/api/storage/v1"
18
        apiequality "k8s.io/apimachinery/pkg/api/equality"
19
        k8serrors "k8s.io/apimachinery/pkg/api/errors"
20
        "k8s.io/apimachinery/pkg/api/meta"
21
        metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
22
        "k8s.io/apimachinery/pkg/runtime"
23
        "k8s.io/apimachinery/pkg/types"
24
        "k8s.io/client-go/tools/record"
25
        storagehelpers "k8s.io/component-helpers/storage/volume"
26

27
        "sigs.k8s.io/controller-runtime/pkg/client"
28
        "sigs.k8s.io/controller-runtime/pkg/controller"
29
        "sigs.k8s.io/controller-runtime/pkg/event"
30
        "sigs.k8s.io/controller-runtime/pkg/handler"
31
        "sigs.k8s.io/controller-runtime/pkg/manager"
32
        "sigs.k8s.io/controller-runtime/pkg/predicate"
33
        "sigs.k8s.io/controller-runtime/pkg/reconcile"
34
        "sigs.k8s.io/controller-runtime/pkg/source"
35

36
        cdiv1 "kubevirt.io/containerized-data-importer-api/pkg/apis/core/v1beta1"
37
        "kubevirt.io/containerized-data-importer/pkg/common"
38
        cc "kubevirt.io/containerized-data-importer/pkg/controller/common"
39
        metrics "kubevirt.io/containerized-data-importer/pkg/monitoring/metrics/cdi-controller"
40
        "kubevirt.io/containerized-data-importer/pkg/operator"
41
        "kubevirt.io/containerized-data-importer/pkg/storagecapabilities"
42
        "kubevirt.io/containerized-data-importer/pkg/util"
43
)
44

45
const (
46
        storageProfileControllerName = "storageprofile-controller"
47
        counterLabelStorageClass     = "storageclass"
48
        counterLabelProvisioner      = "provisioner"
49
        counterLabelComplete         = "complete"
50
        counterLabelDefault          = "default"
51
        counterLabelVirtDefault      = "virtdefault"
52
        counterLabelRWX              = "rwx"
53
        counterLabelSmartClone       = "smartclone"
54
        counterLabelDegraded         = "degraded"
55
)
56

57
// StorageProfileReconciler members
58
type StorageProfileReconciler struct {
59
        client client.Client
60
        // use this for getting any resources not in the install namespace or cluster scope
61
        uncachedClient  client.Client
62
        recorder        record.EventRecorder
63
        scheme          *runtime.Scheme
64
        log             logr.Logger
65
        installerLabels map[string]string
66
}
67

68
// Reconcile the reconcile.Reconciler implementation for the StorageProfileReconciler object.
69
func (r *StorageProfileReconciler) Reconcile(_ context.Context, req reconcile.Request) (reconcile.Result, error) {
1✔
70
        log := r.log.WithValues("StorageProfile", req.NamespacedName)
1✔
71
        log.Info("reconciling StorageProfile")
1✔
72

1✔
73
        storageClass := &storagev1.StorageClass{}
1✔
74
        if err := r.client.Get(context.TODO(), req.NamespacedName, storageClass); err != nil {
2✔
75
                if k8serrors.IsNotFound(err) {
2✔
76
                        return reconcile.Result{}, r.deleteStorageProfile(req.NamespacedName.Name, log)
1✔
77
                }
1✔
78
                return reconcile.Result{}, err
×
79
        } else if storageClass.GetDeletionTimestamp() != nil {
1✔
80
                return reconcile.Result{}, r.deleteStorageProfile(req.NamespacedName.Name, log)
×
81
        }
×
82

83
        return r.reconcileStorageProfile(storageClass)
1✔
84
}
85

86
func (r *StorageProfileReconciler) reconcileStorageProfile(sc *storagev1.StorageClass) (reconcile.Result, error) {
1✔
87
        log := r.log.WithValues("StorageProfile", sc.Name)
1✔
88

1✔
89
        storageProfile, prevStorageProfile, err := r.getStorageProfile(sc)
1✔
90
        if err != nil {
1✔
91
                log.Error(err, "Unable to create StorageProfile")
×
92
                return reconcile.Result{}, err
×
93
        }
×
94

95
        storageProfile.Status.StorageClass = &sc.Name
1✔
96
        storageProfile.Status.Provisioner = &sc.Provisioner
1✔
97
        snapClass, err := cc.GetSnapshotClassForSmartClone(nil, &sc.Name, storageProfile.Spec.SnapshotClass, r.log, r.client, r.recorder)
1✔
98
        if err != nil {
2✔
99
                return reconcile.Result{}, err
1✔
100
        }
1✔
101
        if snapClass != "" {
2✔
102
                storageProfile.Status.SnapshotClass = &snapClass
1✔
103
        }
1✔
104
        storageProfile.Status.CloneStrategy = r.reconcileCloneStrategy(sc, storageProfile.Spec.CloneStrategy, snapClass)
1✔
105
        storageProfile.Status.DataImportCronSourceFormat = r.reconcileDataImportCronSourceFormat(sc, storageProfile.Spec.DataImportCronSourceFormat, snapClass)
1✔
106

1✔
107
        // Reconcile StorageProfile annotations based on provisioner capabilities
1✔
108
        r.reconcileMinimumSupportedPVCSize(sc, storageProfile)
1✔
109
        r.reconcileUseReadWriteOnceForDataImportCron(context.TODO(), sc, storageProfile)
1✔
110
        r.reconcileSnapshotClassForDataImportCron(context.TODO(), sc, storageProfile)
1✔
111

1✔
112
        var claimPropertySets []cdiv1.ClaimPropertySet
1✔
113

1✔
114
        if len(storageProfile.Spec.ClaimPropertySets) > 0 {
2✔
115
                for _, cps := range storageProfile.Spec.ClaimPropertySets {
2✔
116
                        if cps.VolumeMode == nil || len(cps.AccessModes) == 0 {
2✔
117
                                err = errors.New("each ClaimPropertySet must provide both volume mode and access modes")
1✔
118
                                log.Error(err, "Unable to update StorageProfile")
1✔
119
                                return reconcile.Result{}, err
1✔
120
                        }
1✔
121
                }
122
                claimPropertySets = storageProfile.Spec.ClaimPropertySets
1✔
123
        } else {
1✔
124
                claimPropertySets = r.reconcilePropertySets(sc)
1✔
125
        }
1✔
126

127
        storageProfile.Status.ClaimPropertySets = claimPropertySets
1✔
128

1✔
129
        util.SetRecommendedLabels(storageProfile, r.installerLabels, "cdi-controller")
1✔
130
        if err := r.updateStorageProfile(prevStorageProfile, storageProfile, log); err != nil {
1✔
131
                return reconcile.Result{}, err
×
132
        }
×
133

134
        return reconcile.Result{}, r.computeMetrics(storageProfile, sc)
1✔
135
}
136

137
func (r *StorageProfileReconciler) updateStorageProfile(prevStorageProfile runtime.Object, storageProfile *cdiv1.StorageProfile, log logr.Logger) error {
1✔
138
        var prevSP *cdiv1.StorageProfile
1✔
139
        if p, ok := prevStorageProfile.(*cdiv1.StorageProfile); ok {
2✔
140
                prevSP = p
1✔
141
        }
1✔
142

143
        if prevSP == nil {
2✔
144
                return r.client.Create(context.TODO(), storageProfile)
1✔
145
        }
1✔
146

147
        if storageProfileSpecMetaChanged(prevSP, storageProfile) {
2✔
148
                log.Info("Updating StorageProfile", "StorageProfile.Name", storageProfile.Name, "storageProfile", storageProfile)
1✔
149
                if err := r.client.Update(context.TODO(), storageProfile); err != nil {
1✔
150
                        return err
×
151
                }
×
152
        }
153

154
        if !reflect.DeepEqual(prevSP.Status, storageProfile.Status) {
2✔
155
                log.Info("Updating StorageProfile Status", "StorageProfile.Name", storageProfile.Name, "storageProfile", storageProfile)
1✔
156
                if err := r.client.Status().Update(context.TODO(), storageProfile); err != nil {
1✔
157
                        return err
×
158
                }
×
159
        }
160

161
        return nil
1✔
162
}
163

164
// storageProfileSpecMetaChanged returns true if Spec, Labels, or Annotations differ
165
func storageProfileSpecMetaChanged(previous, desired *cdiv1.StorageProfile) bool {
1✔
166
        if previous == nil || desired == nil {
1✔
167
                return previous != desired
×
168
        }
×
169
        if !apiequality.Semantic.DeepEqual(previous.Spec, desired.Spec) {
1✔
170
                return true
×
171
        }
×
172
        if !apiequality.Semantic.DeepEqual(previous.GetLabels(), desired.GetLabels()) {
2✔
173
                return true
1✔
174
        }
1✔
175
        if !apiequality.Semantic.DeepEqual(previous.GetAnnotations(), desired.GetAnnotations()) {
1✔
176
                return true
×
177
        }
×
178
        return false
1✔
179
}
180

181
func (r *StorageProfileReconciler) getStorageProfile(sc *storagev1.StorageClass) (*cdiv1.StorageProfile, runtime.Object, error) {
1✔
182
        var prevStorageProfile runtime.Object
1✔
183
        storageProfile := &cdiv1.StorageProfile{}
1✔
184

1✔
185
        if err := r.client.Get(context.TODO(), types.NamespacedName{Name: sc.Name}, storageProfile); err != nil {
2✔
186
                if k8serrors.IsNotFound(err) {
2✔
187
                        storageProfile, err = r.createEmptyStorageProfile(sc)
1✔
188
                        if err != nil {
1✔
189
                                return nil, nil, err
×
190
                        }
×
191
                } else {
×
192
                        return nil, nil, err
×
193
                }
×
194
        } else {
1✔
195
                prevStorageProfile = storageProfile.DeepCopyObject()
1✔
196
        }
1✔
197

198
        return storageProfile, prevStorageProfile, nil
1✔
199
}
200

201
func (r *StorageProfileReconciler) reconcilePropertySets(sc *storagev1.StorageClass) []cdiv1.ClaimPropertySet {
1✔
202
        claimPropertySets := []cdiv1.ClaimPropertySet{}
1✔
203
        capabilities, found := storagecapabilities.GetCapabilities(r.client, sc)
1✔
204
        if found {
2✔
205
                for i := range capabilities {
2✔
206
                        claimPropertySet := cdiv1.ClaimPropertySet{
1✔
207
                                AccessModes: []v1.PersistentVolumeAccessMode{capabilities[i].AccessMode},
1✔
208
                                VolumeMode:  &capabilities[i].VolumeMode,
1✔
209
                        }
1✔
210
                        claimPropertySets = append(claimPropertySets, claimPropertySet)
1✔
211
                }
1✔
212
        }
213
        return claimPropertySets
1✔
214
}
215

216
func (r *StorageProfileReconciler) reconcileCloneStrategy(sc *storagev1.StorageClass, desiredCloneStrategy *cdiv1.CDICloneStrategy, snapClass string) *cdiv1.CDICloneStrategy {
1✔
217
        if desiredCloneStrategy != nil {
2✔
218
                return desiredCloneStrategy
1✔
219
        }
1✔
220

221
        if annStrategyVal, ok := sc.Annotations["cdi.kubevirt.io/clone-strategy"]; ok {
2✔
222
                return r.getCloneStrategyFromStorageClass(annStrategyVal)
1✔
223
        }
1✔
224

225
        // Default to trying snapshot clone unless volume snapshot class missing
226
        hostAssistedStrategy := cdiv1.CloneStrategyHostAssisted
1✔
227
        strategy := hostAssistedStrategy
1✔
228
        if snapClass != "" {
2✔
229
                strategy = cdiv1.CloneStrategySnapshot
1✔
230
        }
1✔
231

232
        if knownStrategy, ok := storagecapabilities.GetAdvisedCloneStrategy(sc); ok {
2✔
233
                strategy = knownStrategy
1✔
234
        }
1✔
235

236
        if strategy == cdiv1.CloneStrategySnapshot && snapClass == "" {
2✔
237
                r.log.Info("No VolumeSnapshotClass found for storage class, falling back to host assisted cloning", "StorageClass.Name", sc.Name)
1✔
238
                return &hostAssistedStrategy
1✔
239
        }
1✔
240

241
        return &strategy
1✔
242
}
243

244
func (r *StorageProfileReconciler) getCloneStrategyFromStorageClass(annStrategyVal string) *cdiv1.CDICloneStrategy {
1✔
245
        var strategy cdiv1.CDICloneStrategy
1✔
246

1✔
247
        switch annStrategyVal {
1✔
248
        case "copy":
1✔
249
                strategy = cdiv1.CloneStrategyHostAssisted
1✔
250
        case "snapshot":
1✔
251
                strategy = cdiv1.CloneStrategySnapshot
1✔
252
        case "csi-clone":
1✔
253
                strategy = cdiv1.CloneStrategyCsiClone
1✔
254
        }
255

256
        return &strategy
1✔
257
}
258

259
func (r *StorageProfileReconciler) reconcileDataImportCronSourceFormat(sc *storagev1.StorageClass, desiredFormat *cdiv1.DataImportCronSourceFormat, snapClass string) *cdiv1.DataImportCronSourceFormat {
1✔
260
        if desiredFormat != nil {
1✔
261
                return desiredFormat
×
262
        }
×
263

264
        // This can be changed later on
265
        // for example, if at some point we're confident snapshot sources should be the default
266
        pvcFormat := cdiv1.DataImportCronSourceFormatPvc
1✔
267
        format := pvcFormat
1✔
268

1✔
269
        if knownFormat, ok := storagecapabilities.GetAdvisedSourceFormat(sc); ok {
2✔
270
                format = knownFormat
1✔
271
        }
1✔
272

273
        if format == cdiv1.DataImportCronSourceFormatSnapshot && snapClass == "" {
2✔
274
                // No point using snapshots without a corresponding snapshot class
1✔
275
                r.log.Info("No VolumeSnapshotClass found for storage class, falling back to pvc sources for DataImportCrons", "StorageClass.Name", sc.Name)
1✔
276
                return &pvcFormat
1✔
277
        }
1✔
278

279
        return &format
1✔
280
}
281

282
func (r *StorageProfileReconciler) reconcileMinimumSupportedPVCSize(sc *storagev1.StorageClass, sp *cdiv1.StorageProfile) {
1✔
283
        if size, hasSize := storagecapabilities.GetMinimumSupportedPVCSize(sc); hasSize {
2✔
284
                if _, isAnnotated := sp.Annotations[cc.AnnMinimumSupportedPVCSize]; !isAnnotated {
2✔
285
                        if sp.Annotations == nil {
2✔
286
                                sp.Annotations = make(map[string]string)
1✔
287
                        }
1✔
288
                        sp.Annotations[cc.AnnMinimumSupportedPVCSize] = size
1✔
289
                }
290
        }
291
}
292

293
func (r *StorageProfileReconciler) reconcileUseReadWriteOnceForDataImportCron(ctx context.Context, sc *storagev1.StorageClass, sp *cdiv1.StorageProfile) {
1✔
294
        if !storagecapabilities.ShouldUseReadWriteOnceForDataImportCron(sc) {
2✔
295
                return
1✔
296
        }
1✔
297
        if sp.Annotations != nil {
2✔
298
                if _, exists := sp.Annotations[cc.AnnUseReadWriteOnceForDataImportCron]; exists {
2✔
299
                        return
1✔
300
                }
1✔
301
        }
302
        if sp.Annotations == nil {
2✔
303
                sp.Annotations = make(map[string]string)
1✔
304
        }
1✔
305
        sp.Annotations[cc.AnnUseReadWriteOnceForDataImportCron] = "true"
1✔
306
}
307

308
func (r *StorageProfileReconciler) reconcileSnapshotClassForDataImportCron(ctx context.Context, sc *storagev1.StorageClass, sp *cdiv1.StorageProfile) {
1✔
309
        desiredClass, err := r.findSnapshotClassForDataImportCron(ctx, sc)
1✔
310
        if err != nil {
1✔
NEW
311
                r.log.V(3).Info("Error finding snapshot class for DataImportCron", "error", err)
×
NEW
312
                return
×
NEW
313
        }
×
314

315
        if desiredClass == "" {
2✔
316
                delete(sp.Annotations, cc.AnnSnapshotClassForDataImportCron)
1✔
317
                return
1✔
318
        }
1✔
319

320
        if sp.Annotations == nil {
1✔
NEW
321
                sp.Annotations = make(map[string]string)
×
NEW
322
        }
×
323
        sp.Annotations[cc.AnnSnapshotClassForDataImportCron] = desiredClass
1✔
324
}
325

326
// findSnapshotClassForDataImportCron finds a VolumeSnapshotClass that is suitable for DataImportCron snapshots.
327
func (r *StorageProfileReconciler) findSnapshotClassForDataImportCron(ctx context.Context, sc *storagev1.StorageClass) (string, error) {
1✔
328
        vscList := &snapshotv1.VolumeSnapshotClassList{}
1✔
329
        if err := r.client.List(ctx, vscList); err != nil {
1✔
NEW
330
                if meta.IsNoMatchError(err) {
×
NEW
331
                        return "", nil
×
NEW
332
                }
×
NEW
333
                return "", err
×
334
        }
335

336
        var candidates []string
1✔
337
        for _, vsc := range vscList.Items {
2✔
338
                if !storagecapabilities.MatchesDataImportCronVSC(sc, &vsc) {
2✔
339
                        continue
1✔
340
                }
341
                // Found a match - prefer default-annotated one
342
                if vsc.Annotations[cc.AnnDefaultSnapshotClass] == "true" {
1✔
NEW
343
                        return vsc.Name, nil
×
NEW
344
                }
×
345
                candidates = append(candidates, vsc.Name)
1✔
346
        }
347

348
        if len(candidates) > 0 {
2✔
349
                sort.Strings(candidates)
1✔
350
                return candidates[0], nil
1✔
351
        }
1✔
352
        return "", nil
1✔
353
}
354

355
func (r *StorageProfileReconciler) createEmptyStorageProfile(sc *storagev1.StorageClass) (*cdiv1.StorageProfile, error) {
1✔
356
        storageProfile := MakeEmptyStorageProfileSpec(sc.Name)
1✔
357
        util.SetRecommendedLabels(storageProfile, r.installerLabels, "cdi-controller")
1✔
358
        // uncachedClient is used to directly get the config map
1✔
359
        // the controller runtime client caches objects that are read once, and thus requires a list/watch
1✔
360
        // should be cheaper than watching
1✔
361
        if err := operator.SetOwnerRuntime(r.uncachedClient, storageProfile); err != nil {
1✔
362
                return nil, err
×
363
        }
×
364
        return storageProfile, nil
1✔
365
}
366

367
func (r *StorageProfileReconciler) deleteStorageProfile(name string, log logr.Logger) error {
1✔
368
        log.Info("Cleaning up StorageProfile that corresponds to deleted StorageClass", "StorageClass.Name", name)
1✔
369
        profile := &cdiv1.StorageProfile{
1✔
370
                ObjectMeta: metav1.ObjectMeta{
1✔
371
                        Name: name,
1✔
372
                },
1✔
373
        }
1✔
374

1✔
375
        if err := r.client.Delete(context.TODO(), profile); cc.IgnoreNotFound(err) != nil {
1✔
376
                return err
×
377
        }
×
378

379
        labels := prometheus.Labels{
1✔
380
                counterLabelStorageClass: name,
1✔
381
        }
1✔
382
        metrics.DeleteStorageProfileStatus(labels)
1✔
383
        return nil
1✔
384
}
385

386
func isNoProvisioner(name string, cl client.Client) bool {
×
387
        storageClass := &storagev1.StorageClass{}
×
388
        if err := cl.Get(context.TODO(), types.NamespacedName{Name: name}, storageClass); err != nil {
×
389
                return false
×
390
        }
×
391
        return storageClass.Provisioner == storagehelpers.NotSupportedProvisioner
×
392
}
393

394
func (r *StorageProfileReconciler) computeMetrics(profile *cdiv1.StorageProfile, sc *storagev1.StorageClass) error {
1✔
395
        if profile.Status.StorageClass == nil || profile.Status.Provisioner == nil {
1✔
396
                return nil
×
397
        }
×
398

399
        storageClass := *profile.Status.StorageClass
1✔
400
        provisioner := *profile.Status.Provisioner
1✔
401

1✔
402
        // We don't count explicitly unsupported provisioners as incomplete
1✔
403
        _, found := storagecapabilities.UnsupportedProvisioners[*profile.Status.Provisioner]
1✔
404
        isComplete := found || !isIncomplete(profile.Status.ClaimPropertySets)
1✔
405
        isDefault := sc.Annotations[cc.AnnDefaultStorageClass] == "true"
1✔
406
        isVirtDefault := sc.Annotations[cc.AnnDefaultVirtStorageClass] == "true"
1✔
407
        isRWX := hasRWX(profile.Status.ClaimPropertySets)
1✔
408
        isSmartClone, err := r.hasSmartClone(profile)
1✔
409
        if err != nil {
1✔
410
                return err
×
411
        }
×
412

413
        isSNO := false
1✔
414
        clusterInfra := &ocpconfigv1.Infrastructure{}
1✔
415
        if err := r.client.Get(context.TODO(), types.NamespacedName{Name: "cluster"}, clusterInfra); err != nil {
2✔
416
                if !meta.IsNoMatchError(err) && !k8serrors.IsNotFound(err) {
1✔
417
                        return err
×
418
                }
×
419
        } else {
1✔
420
                isSNO = clusterInfra.Status.ControlPlaneTopology == ocpconfigv1.SingleReplicaTopologyMode &&
1✔
421
                        clusterInfra.Status.InfrastructureTopology == ocpconfigv1.SingleReplicaTopologyMode
1✔
422
        }
1✔
423

424
        isDegraded := (!isSNO && !isRWX) || !isSmartClone
1✔
425

1✔
426
        // Setting the labeled Gauge to 1 will not delete older metric, so we need to explicitly delete them
1✔
427
        scLabels := prometheus.Labels{counterLabelStorageClass: storageClass, counterLabelProvisioner: provisioner}
1✔
428
        metricsDeleted := metrics.DeleteStorageProfileStatus(scLabels)
1✔
429
        scLabels = createLabels(storageClass, provisioner, isComplete, isDefault, isVirtDefault, isRWX, isSmartClone, isDegraded)
1✔
430
        metrics.SetStorageProfileStatus(scLabels, 1)
1✔
431
        r.log.Info(fmt.Sprintf("Set metric:%s complete:%t default:%t vdefault:%t rwx:%t smartclone:%t degraded:%t (deleted %d)",
1✔
432
                storageClass, isComplete, isDefault, isVirtDefault, isRWX, isSmartClone, isDegraded, metricsDeleted))
1✔
433

1✔
434
        return nil
1✔
435
}
436

437
func (r *StorageProfileReconciler) hasSmartClone(sp *cdiv1.StorageProfile) (bool, error) {
1✔
438
        strategy := sp.Status.CloneStrategy
1✔
439
        provisioner := sp.Status.Provisioner
1✔
440

1✔
441
        if strategy != nil {
2✔
442
                if *strategy == cdiv1.CloneStrategyHostAssisted {
2✔
443
                        return false, nil
1✔
444
                }
1✔
445
                if *strategy == cdiv1.CloneStrategyCsiClone && provisioner != nil {
2✔
446
                        driver := &storagev1.CSIDriver{}
1✔
447
                        if err := r.client.Get(context.TODO(), types.NamespacedName{Name: *provisioner}, driver); err != nil {
2✔
448
                                return false, cc.IgnoreNotFound(err)
1✔
449
                        }
1✔
450
                        return true, nil
1✔
451
                }
452
        }
453

454
        if (strategy == nil || *strategy == cdiv1.CloneStrategySnapshot) && provisioner != nil {
2✔
455
                vscs := &snapshotv1.VolumeSnapshotClassList{}
1✔
456
                if err := r.client.List(context.TODO(), vscs); err != nil {
1✔
457
                        return false, err
×
458
                }
×
459
                return hasDriver(vscs, *provisioner), nil
1✔
460
        }
461

462
        return false, nil
×
463
}
464

465
func createLabels(storageClass, provisioner string, isComplete, isDefault, isVirtDefault, isRWX, isSmartClone, isDegraded bool) prometheus.Labels {
1✔
466
        return prometheus.Labels{
1✔
467
                counterLabelStorageClass: storageClass,
1✔
468
                counterLabelProvisioner:  provisioner,
1✔
469
                counterLabelComplete:     strconv.FormatBool(isComplete),
1✔
470
                counterLabelDefault:      strconv.FormatBool(isDefault),
1✔
471
                counterLabelVirtDefault:  strconv.FormatBool(isVirtDefault),
1✔
472
                counterLabelRWX:          strconv.FormatBool(isRWX),
1✔
473
                counterLabelSmartClone:   strconv.FormatBool(isSmartClone),
1✔
474
                counterLabelDegraded:     strconv.FormatBool(isDegraded),
1✔
475
        }
1✔
476
}
1✔
477

478
// MakeEmptyStorageProfileSpec creates StorageProfile manifest
479
func MakeEmptyStorageProfileSpec(name string) *cdiv1.StorageProfile {
1✔
480
        return &cdiv1.StorageProfile{
1✔
481
                TypeMeta: metav1.TypeMeta{
1✔
482
                        Kind:       "StorageProfile",
1✔
483
                        APIVersion: "cdi.kubevirt.io/v1beta1",
1✔
484
                },
1✔
485
                ObjectMeta: metav1.ObjectMeta{
1✔
486
                        Name: name,
1✔
487
                        Labels: map[string]string{
1✔
488
                                common.CDILabelKey:       common.CDILabelValue,
1✔
489
                                common.CDIComponentLabel: "",
1✔
490
                        },
1✔
491
                },
1✔
492
        }
1✔
493
}
1✔
494

495
// NewStorageProfileController creates a new instance of the StorageProfile controller.
496
func NewStorageProfileController(mgr manager.Manager, log logr.Logger, installerLabels map[string]string) (controller.Controller, error) {
×
497
        uncachedClient, err := client.New(mgr.GetConfig(), client.Options{
×
498
                Scheme: mgr.GetScheme(),
×
499
                Mapper: mgr.GetRESTMapper(),
×
500
        })
×
501
        if err != nil {
×
502
                return nil, err
×
503
        }
×
504

505
        reconciler := &StorageProfileReconciler{
×
506
                client:          mgr.GetClient(),
×
507
                uncachedClient:  uncachedClient,
×
508
                recorder:        mgr.GetEventRecorderFor(storageProfileControllerName),
×
509
                scheme:          mgr.GetScheme(),
×
510
                log:             log.WithName(storageProfileControllerName),
×
511
                installerLabels: installerLabels,
×
512
        }
×
513

×
514
        storageProfileController, err := controller.New(
×
515
                storageProfileControllerName,
×
516
                mgr,
×
517
                controller.Options{Reconciler: reconciler, MaxConcurrentReconciles: 3})
×
518
        if err != nil {
×
519
                return nil, err
×
520
        }
×
521
        if err := addStorageProfileControllerWatches(mgr, storageProfileController, log); err != nil {
×
522
                return nil, err
×
523
        }
×
524

525
        log.Info("Initialized StorageProfile controller")
×
526
        return storageProfileController, nil
×
527
}
528

529
func addStorageProfileControllerWatches(mgr manager.Manager, c controller.Controller, log logr.Logger) error {
×
530
        if err := c.Watch(source.Kind(mgr.GetCache(), &storagev1.StorageClass{}, &handler.TypedEnqueueRequestForObject[*storagev1.StorageClass]{})); err != nil {
×
531
                return err
×
532
        }
×
533

534
        if err := c.Watch(source.Kind(mgr.GetCache(), &cdiv1.StorageProfile{}, &handler.TypedEnqueueRequestForObject[*cdiv1.StorageProfile]{})); err != nil {
×
535
                return err
×
536
        }
×
537

538
        if err := c.Watch(source.Kind(mgr.GetCache(), &v1.PersistentVolume{}, handler.TypedEnqueueRequestsFromMapFunc[*v1.PersistentVolume](
×
539
                func(_ context.Context, obj *v1.PersistentVolume) []reconcile.Request {
×
540
                        return []reconcile.Request{{
×
541
                                NamespacedName: types.NamespacedName{Name: scName(obj)},
×
542
                        }}
×
543
                },
×
544
        ),
545
                predicate.TypedFuncs[*v1.PersistentVolume]{
546
                        CreateFunc: func(e event.TypedCreateEvent[*v1.PersistentVolume]) bool {
×
547
                                return isNoProvisioner(scName(e.Object), mgr.GetClient())
×
548
                        },
×
549
                        UpdateFunc: func(e event.TypedUpdateEvent[*v1.PersistentVolume]) bool {
×
550
                                return isNoProvisioner(scName(e.ObjectNew), mgr.GetClient())
×
551
                        },
×
552
                        DeleteFunc: func(e event.TypedDeleteEvent[*v1.PersistentVolume]) bool {
×
553
                                return isNoProvisioner(scName(e.Object), mgr.GetClient())
×
554
                        },
×
555
                })); err != nil {
×
556
                return err
×
557
        }
×
558

559
        mapSnapshotClassToProfile := func(ctx context.Context, vsc *snapshotv1.VolumeSnapshotClass) []reconcile.Request {
×
560
                var scList storagev1.StorageClassList
×
561
                if err := mgr.GetClient().List(ctx, &scList); err != nil {
×
562
                        c.GetLogger().Error(err, "Unable to list StorageClasses")
×
563
                        return nil
×
564
                }
×
565
                var reqs []reconcile.Request
×
566
                for _, sc := range scList.Items {
×
567
                        if sc.Provisioner == vsc.Driver {
×
568
                                reqs = append(reqs, reconcile.Request{NamespacedName: types.NamespacedName{Name: sc.Name}})
×
569
                        }
×
570
                }
571
                return reqs
×
572
        }
573
        if err := mgr.GetClient().List(context.TODO(), &snapshotv1.VolumeSnapshotClassList{}, &client.ListOptions{Limit: 1}); err != nil {
×
574
                if meta.IsNoMatchError(err) {
×
575
                        // Back out if there's no point to attempt watch
×
576
                        return nil
×
577
                }
×
578
                if !cc.IsErrCacheNotStarted(err) {
×
579
                        return err
×
580
                }
×
581
        }
582
        if err := c.Watch(source.Kind(mgr.GetCache(), &snapshotv1.VolumeSnapshotClass{},
×
583
                handler.TypedEnqueueRequestsFromMapFunc[*snapshotv1.VolumeSnapshotClass](mapSnapshotClassToProfile),
×
584
        )); err != nil {
×
585
                return err
×
586
        }
×
587

588
        return nil
×
589
}
590

591
func scName(obj client.Object) string {
×
592
        return obj.(*v1.PersistentVolume).Spec.StorageClassName
×
593
}
×
594

595
func isIncomplete(sets []cdiv1.ClaimPropertySet) bool {
1✔
596
        if len(sets) > 0 {
2✔
597
                for _, cps := range sets {
2✔
598
                        if len(cps.AccessModes) == 0 || cps.VolumeMode == nil {
1✔
599
                                return true
×
600
                        }
×
601
                }
602
        } else {
1✔
603
                return true
1✔
604
        }
1✔
605

606
        return false
1✔
607
}
608

609
func hasRWX(cpSets []cdiv1.ClaimPropertySet) bool {
1✔
610
        for _, cpSet := range cpSets {
2✔
611
                for _, am := range cpSet.AccessModes {
2✔
612
                        if am == v1.ReadWriteMany {
2✔
613
                                return true
1✔
614
                        }
1✔
615
                }
616
        }
617
        return false
1✔
618
}
619

620
func hasDriver(vscs *snapshotv1.VolumeSnapshotClassList, driver string) bool {
1✔
621
        for i := range vscs.Items {
2✔
622
                vsc := vscs.Items[i]
1✔
623
                if vsc.Driver == driver {
2✔
624
                        return true
1✔
625
                }
1✔
626
        }
627
        return false
1✔
628
}
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