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

kubevirt / containerized-data-importer / #5922

29 Mar 2026 02:45PM UTC coverage: 49.335% (+0.03%) from 49.301%
#5922

Pull #4074

travis-ci

noamasu
Fix DataImportCron reconciliation when first default StorageClass is set

The default SC change condition only handled changes from one SC to another, but did not handle the first default SC being set.

To get the DIC-created DV with the DIC-special RWO preference, it must be deleted (as DV is immutable) and recreated with the right settings from the new default StorageClass.

Signed-off-by: Noam Assouline <nassouli@redhat.com>
Pull Request #4074: Recreate DIC import DV when first default SC is set

26 of 30 new or added lines in 1 file covered. (86.67%)

257 existing lines in 1 file now uncovered.

14763 of 29924 relevant lines covered (49.33%)

0.55 hits per line

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

65.99
/pkg/controller/dataimportcron-controller.go
1
/*
2
Copyright 2021 The CDI Authors.
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
limitations under the License.
14
See the License for the specific language governing permissions and
15
*/
16

17
package controller
18

19
import (
20
        "context"
21
        "fmt"
22
        "net/url"
23
        "reflect"
24
        "sort"
25
        "strings"
26
        "time"
27

28
        "github.com/containers/image/v5/docker/reference"
29
        "github.com/go-logr/logr"
30
        snapshotv1 "github.com/kubernetes-csi/external-snapshotter/client/v6/apis/volumesnapshot/v1"
31
        imagev1 "github.com/openshift/api/image/v1"
32
        secv1 "github.com/openshift/api/security/v1"
33
        "github.com/pkg/errors"
34
        cronexpr "github.com/robfig/cron/v3"
35

36
        authorizationv1 "k8s.io/api/authorization/v1"
37
        batchv1 "k8s.io/api/batch/v1"
38
        corev1 "k8s.io/api/core/v1"
39
        storagev1 "k8s.io/api/storage/v1"
40
        k8serrors "k8s.io/apimachinery/pkg/api/errors"
41
        "k8s.io/apimachinery/pkg/api/meta"
42
        "k8s.io/apimachinery/pkg/api/resource"
43
        metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
44
        "k8s.io/apimachinery/pkg/labels"
45
        "k8s.io/apimachinery/pkg/runtime"
46
        "k8s.io/apimachinery/pkg/types"
47
        "k8s.io/client-go/tools/record"
48
        openapicommon "k8s.io/kube-openapi/pkg/common"
49
        "k8s.io/utils/ptr"
50

51
        "sigs.k8s.io/controller-runtime/pkg/client"
52
        "sigs.k8s.io/controller-runtime/pkg/controller"
53
        "sigs.k8s.io/controller-runtime/pkg/event"
54
        "sigs.k8s.io/controller-runtime/pkg/handler"
55
        "sigs.k8s.io/controller-runtime/pkg/manager"
56
        "sigs.k8s.io/controller-runtime/pkg/predicate"
57
        "sigs.k8s.io/controller-runtime/pkg/reconcile"
58
        "sigs.k8s.io/controller-runtime/pkg/source"
59

60
        cdiv1 "kubevirt.io/containerized-data-importer-api/pkg/apis/core/v1beta1"
61
        "kubevirt.io/containerized-data-importer/pkg/common"
62
        cc "kubevirt.io/containerized-data-importer/pkg/controller/common"
63
        dvc "kubevirt.io/containerized-data-importer/pkg/controller/datavolume"
64
        metrics "kubevirt.io/containerized-data-importer/pkg/monitoring/metrics/cdi-controller"
65
        "kubevirt.io/containerized-data-importer/pkg/operator"
66
        "kubevirt.io/containerized-data-importer/pkg/util"
67
        "kubevirt.io/containerized-data-importer/pkg/util/naming"
68
)
69

70
const (
71
        // ErrDataSourceAlreadyManaged provides a const to indicate DataSource already managed error
72
        ErrDataSourceAlreadyManaged = "ErrDataSourceAlreadyManaged"
73
        // MessageDataSourceAlreadyManaged provides a const to form DataSource already managed error message
74
        MessageDataSourceAlreadyManaged = "DataSource %s is already managed by DataImportCron %s"
75
)
76

77
// DataImportCronReconciler members
78
type DataImportCronReconciler struct {
79
        client          client.Client
80
        uncachedClient  client.Client
81
        recorder        record.EventRecorder
82
        scheme          *runtime.Scheme
83
        log             logr.Logger
84
        image           string
85
        pullPolicy      string
86
        cdiNamespace    string
87
        installerLabels map[string]string
88
}
89

90
const (
91
        // AnnSourceDesiredDigest is the digest of the pending updated image
92
        AnnSourceDesiredDigest = cc.AnnAPIGroup + "/storage.import.sourceDesiredDigest"
93
        // AnnImageStreamDockerRef is the ImageStream Docker reference
94
        AnnImageStreamDockerRef = cc.AnnAPIGroup + "/storage.import.imageStreamDockerRef"
95
        // AnnNextCronTime is the next time stamp which satisfies the cron expression
96
        AnnNextCronTime = cc.AnnAPIGroup + "/storage.import.nextCronTime"
97
        // AnnLastCronTime is the cron last execution time stamp
98
        AnnLastCronTime = cc.AnnAPIGroup + "/storage.import.lastCronTime"
99
        // AnnLastUseTime is the PVC last use time stamp
100
        AnnLastUseTime = cc.AnnAPIGroup + "/storage.import.lastUseTime"
101
        // AnnStorageClass is the cron DV's storage class
102
        AnnStorageClass = cc.AnnAPIGroup + "/storage.import.storageClass"
103

104
        dataImportControllerName    = "dataimportcron-controller"
105
        digestSha256Prefix          = "sha256:"
106
        digestUIDPrefix             = "uid:"
107
        digestDvNameSuffixLength    = 12
108
        cronJobUIDSuffixLength      = 8
109
        defaultImportsToKeepPerCron = 3
110
)
111

112
var ErrNotManagedByCron = errors.New("DataSource is not managed by this DataImportCron")
113

114
// Reconcile loop for DataImportCronReconciler
115
func (r *DataImportCronReconciler) Reconcile(ctx context.Context, req reconcile.Request) (reconcile.Result, error) {
1✔
116
        dataImportCron := &cdiv1.DataImportCron{}
1✔
117
        if err := r.client.Get(ctx, req.NamespacedName, dataImportCron); cc.IgnoreNotFound(err) != nil {
2✔
118
                return reconcile.Result{}, err
1✔
119
        } else if err != nil || dataImportCron.DeletionTimestamp != nil {
3✔
120
                err := r.cleanup(ctx, req.NamespacedName)
1✔
121
                return reconcile.Result{}, err
1✔
122
        }
1✔
123
        shouldReconcile, err := r.shouldReconcileCron(ctx, dataImportCron)
1✔
124
        if !shouldReconcile || err != nil {
1✔
125
                return reconcile.Result{}, err
×
126
        }
×
127

128
        if err := r.initCron(ctx, dataImportCron); err != nil {
1✔
129
                return reconcile.Result{}, err
×
130
        }
×
131

132
        return r.update(ctx, dataImportCron)
1✔
133
}
134

135
func (r *DataImportCronReconciler) shouldReconcileCron(ctx context.Context, cron *cdiv1.DataImportCron) (bool, error) {
1✔
136
        dataSource := &cdiv1.DataSource{}
1✔
137
        if err := r.client.Get(ctx, types.NamespacedName{Namespace: cron.Namespace, Name: cron.Spec.ManagedDataSource}, dataSource); err != nil {
2✔
138
                if k8serrors.IsNotFound(err) {
2✔
139
                        return true, nil
1✔
140
                }
1✔
141
                return false, err
×
142
        }
143
        dataSourceCronLabel := dataSource.Labels[common.DataImportCronLabel]
1✔
144
        if dataSourceCronLabel == cron.Name || dataSourceCronLabel == "" {
2✔
145
                return true, nil
1✔
146
        }
1✔
147
        otherCron := &cdiv1.DataImportCron{}
1✔
148
        if err := r.client.Get(ctx, types.NamespacedName{Namespace: cron.Namespace, Name: dataSourceCronLabel}, otherCron); err != nil {
2✔
149
                if k8serrors.IsNotFound(err) {
2✔
150
                        return true, nil
1✔
151
                }
1✔
152
                return false, err
×
153
        }
154
        if otherCron.Spec.ManagedDataSource == dataSource.Name {
2✔
155
                msg := fmt.Sprintf(MessageDataSourceAlreadyManaged, dataSource.Name, otherCron.Name)
1✔
156
                r.recorder.Event(cron, corev1.EventTypeWarning, ErrDataSourceAlreadyManaged, msg)
1✔
157
                r.log.V(3).Info(msg)
1✔
158
                return false, nil
1✔
159
        }
1✔
160
        return true, nil
×
161
}
162

163
func (r *DataImportCronReconciler) initCron(ctx context.Context, dataImportCron *cdiv1.DataImportCron) error {
1✔
164
        if dataImportCron.Spec.Schedule == "" {
2✔
165
                return nil
1✔
166
        }
1✔
167
        if isControllerPolledSource(dataImportCron) {
2✔
168
                if dataImportCron.Annotations[AnnNextCronTime] == "" {
2✔
169
                        cc.AddAnnotation(dataImportCron, AnnNextCronTime, time.Now().Format(time.RFC3339))
1✔
170
                }
1✔
171
                return nil
1✔
172
        }
173
        if !isURLSource(dataImportCron) {
1✔
174
                return nil
×
175
        }
×
176
        exists, err := r.cronJobExistsAndUpdated(ctx, dataImportCron)
1✔
177
        if exists || err != nil {
2✔
178
                return err
1✔
179
        }
1✔
180
        cronJob, err := r.newCronJob(dataImportCron)
1✔
181
        if err != nil {
1✔
182
                return err
×
183
        }
×
184
        if err := r.client.Create(ctx, cronJob); err != nil {
1✔
185
                return err
×
186
        }
×
187
        job, err := r.newInitialJob(dataImportCron, cronJob)
1✔
188
        if err != nil {
1✔
189
                return err
×
190
        }
×
191
        if err := r.client.Create(ctx, job); err != nil {
1✔
192
                return err
×
193
        }
×
194
        return nil
1✔
195
}
196

197
func (r *DataImportCronReconciler) getImageStream(ctx context.Context, imageStreamName, imageStreamNamespace string) (*imagev1.ImageStream, string, error) {
1✔
198
        if imageStreamName == "" || imageStreamNamespace == "" {
1✔
199
                return nil, "", errors.Errorf("Missing ImageStream name or namespace")
×
200
        }
×
201
        imageStream := &imagev1.ImageStream{}
1✔
202
        name, tag, err := splitImageStreamName(imageStreamName)
1✔
203
        if err != nil {
2✔
204
                return nil, "", err
1✔
205
        }
1✔
206
        imageStreamNamespacedName := types.NamespacedName{
1✔
207
                Namespace: imageStreamNamespace,
1✔
208
                Name:      name,
1✔
209
        }
1✔
210
        if err := r.client.Get(ctx, imageStreamNamespacedName, imageStream); err != nil {
2✔
211
                return nil, "", err
1✔
212
        }
1✔
213
        return imageStream, tag, nil
1✔
214
}
215

216
func getImageStreamDigest(imageStream *imagev1.ImageStream, imageStreamTag string) (string, string, error) {
1✔
217
        if imageStream == nil {
1✔
218
                return "", "", errors.Errorf("No ImageStream")
×
219
        }
×
220
        tags := imageStream.Status.Tags
1✔
221
        if len(tags) == 0 {
1✔
222
                return "", "", errors.Errorf("ImageStream %s has no tags", imageStream.Name)
×
223
        }
×
224

225
        tagIdx := 0
1✔
226
        if len(imageStreamTag) > 0 {
2✔
227
                tagIdx = -1
1✔
228
                for i, tag := range tags {
2✔
229
                        if tag.Tag == imageStreamTag {
2✔
230
                                tagIdx = i
1✔
231
                                break
1✔
232
                        }
233
                }
234
        }
235
        if tagIdx == -1 {
2✔
236
                return "", "", errors.Errorf("ImageStream %s has no tag %s", imageStream.Name, imageStreamTag)
1✔
237
        }
1✔
238

239
        if len(tags[tagIdx].Items) == 0 {
2✔
240
                return "", "", errors.Errorf("ImageStream %s tag %s has no items", imageStream.Name, imageStreamTag)
1✔
241
        }
1✔
242
        return tags[tagIdx].Items[0].Image, tags[tagIdx].Items[0].DockerImageReference, nil
1✔
243
}
244

245
func splitImageStreamName(imageStreamName string) (string, string, error) {
1✔
246
        if subs := strings.Split(imageStreamName, ":"); len(subs) == 1 {
2✔
247
                return imageStreamName, "", nil
1✔
248
        } else if len(subs) == 2 && len(subs[0]) > 0 && len(subs[1]) > 0 {
3✔
249
                return subs[0], subs[1], nil
1✔
250
        }
1✔
251
        return "", "", errors.Errorf("Illegal ImageStream name %s", imageStreamName)
1✔
252
}
253

254
func (r *DataImportCronReconciler) pollSourceDigest(ctx context.Context, dataImportCron *cdiv1.DataImportCron) (reconcile.Result, error) {
1✔
255
        nextTimeStr := dataImportCron.Annotations[AnnNextCronTime]
1✔
256
        if nextTimeStr == "" {
1✔
257
                return r.setNextCronTime(dataImportCron)
×
258
        }
×
259
        nextTime, err := time.Parse(time.RFC3339, nextTimeStr)
1✔
260
        if err != nil {
1✔
261
                return reconcile.Result{}, err
×
262
        }
×
263
        if nextTime.After(time.Now()) {
2✔
264
                return r.setNextCronTime(dataImportCron)
1✔
265
        }
1✔
266
        switch {
1✔
267
        case isImageStreamSource(dataImportCron):
1✔
268
                if err := r.updateImageStreamDesiredDigest(ctx, dataImportCron); err != nil {
2✔
269
                        return reconcile.Result{}, err
1✔
270
                }
1✔
271
        case isPvcSource(dataImportCron):
1✔
272
                if err := r.updatePvcDesiredDigest(ctx, dataImportCron); err != nil {
2✔
273
                        return reconcile.Result{}, err
1✔
274
                }
1✔
275
        case isNodePull(dataImportCron):
1✔
276
                if done, err := r.updateContainerImageDesiredDigest(ctx, dataImportCron); !done {
2✔
277
                        return reconcile.Result{RequeueAfter: 3 * time.Second}, err
1✔
278
                } else if err != nil {
2✔
279
                        return reconcile.Result{}, err
×
280
                }
×
281
        }
282

283
        return r.setNextCronTime(dataImportCron)
1✔
284
}
285

286
func (r *DataImportCronReconciler) setNextCronTime(dataImportCron *cdiv1.DataImportCron) (reconcile.Result, error) {
1✔
287
        now := time.Now()
1✔
288
        expr, err := cronexpr.ParseStandard(dataImportCron.Spec.Schedule)
1✔
289
        if err != nil {
1✔
290
                return reconcile.Result{}, err
×
291
        }
×
292
        nextTime := expr.Next(now)
1✔
293
        requeueAfter := nextTime.Sub(now)
1✔
294
        res := reconcile.Result{RequeueAfter: requeueAfter}
1✔
295
        cc.AddAnnotation(dataImportCron, AnnNextCronTime, nextTime.Format(time.RFC3339))
1✔
296
        return res, err
1✔
297
}
298

299
func isImageStreamSource(dataImportCron *cdiv1.DataImportCron) bool {
1✔
300
        regSource, err := getCronRegistrySource(dataImportCron)
1✔
301
        return err == nil && regSource.ImageStream != nil
1✔
302
}
1✔
303

304
func isURLSource(dataImportCron *cdiv1.DataImportCron) bool {
1✔
305
        regSource, err := getCronRegistrySource(dataImportCron)
1✔
306
        return err == nil && regSource.URL != nil
1✔
307
}
1✔
308

309
func isNodePull(cron *cdiv1.DataImportCron) bool {
1✔
310
        regSource, err := getCronRegistrySource(cron)
1✔
311
        return err == nil && regSource != nil && regSource.PullMethod != nil &&
1✔
312
                *regSource.PullMethod == cdiv1.RegistryPullNode
1✔
313
}
1✔
314

315
func getCronRegistrySource(cron *cdiv1.DataImportCron) (*cdiv1.DataVolumeSourceRegistry, error) {
1✔
316
        if !isCronRegistrySource(cron) {
2✔
317
                return nil, errors.Errorf("Cron has no registry source %s", cron.Name)
1✔
318
        }
1✔
319
        return cron.Spec.Template.Spec.Source.Registry, nil
1✔
320
}
321

322
func isCronRegistrySource(cron *cdiv1.DataImportCron) bool {
1✔
323
        source := cron.Spec.Template.Spec.Source
1✔
324
        return source != nil && source.Registry != nil
1✔
325
}
1✔
326

327
func getCronPvcSource(cron *cdiv1.DataImportCron) (*cdiv1.DataVolumeSourcePVC, error) {
1✔
328
        if !isPvcSource(cron) {
1✔
329
                return nil, errors.Errorf("Cron has no PVC source %s", cron.Name)
×
330
        }
×
331
        return cron.Spec.Template.Spec.Source.PVC, nil
1✔
332
}
333

334
func isPvcSource(cron *cdiv1.DataImportCron) bool {
1✔
335
        source := cron.Spec.Template.Spec.Source
1✔
336
        return source != nil && source.PVC != nil
1✔
337
}
1✔
338

339
func isControllerPolledSource(cron *cdiv1.DataImportCron) bool {
1✔
340
        return isImageStreamSource(cron) || isPvcSource(cron) || isNodePull(cron)
1✔
341
}
1✔
342

343
func (r *DataImportCronReconciler) update(ctx context.Context, dataImportCron *cdiv1.DataImportCron) (reconcile.Result, error) {
1✔
344
        res := reconcile.Result{}
1✔
345

1✔
346
        dv, pvc, err := r.getImportState(ctx, dataImportCron)
1✔
347
        if err != nil {
1✔
348
                return res, err
×
349
        }
×
350

351
        dataImportCronCopy := dataImportCron.DeepCopy()
1✔
352
        imports := dataImportCron.Status.CurrentImports
1✔
353
        importSucceeded := false
1✔
354

1✔
355
        dataVolume := dataImportCron.Spec.Template
1✔
356
        explicitScName := cc.GetStorageClassFromDVSpec(&dataVolume)
1✔
357
        desiredStorageClass, err := cc.GetStorageClassByNameWithVirtFallback(ctx, r.client, explicitScName, dataVolume.Spec.ContentType)
1✔
358
        if err != nil {
1✔
359
                return res, err
×
360
        }
×
361
        if desiredStorageClass != nil {
2✔
362
                desiredSc := desiredStorageClass.Name
1✔
363
                if deleted, err := r.deleteOutdatedPendingPvc(ctx, pvc, desiredSc, dataImportCron.Name); deleted || err != nil {
2✔
364
                        return res, err
1✔
365
                }
1✔
366
                currentSc, hasCurrent := dataImportCron.Annotations[AnnStorageClass]
1✔
367
                var storageClassChanged bool
1✔
368
                if hasCurrent {
2✔
369
                        storageClassChanged = currentSc != desiredSc
1✔
370
                } else {
2✔
371
                        storageClassChanged = len(imports) > 0
1✔
372
                }
1✔
373
                if storageClassChanged {
2✔
374
                        r.log.Info("Storage class changed, deleting imports on old SC", "currentSc", currentSc, "desiredSc", desiredSc)
1✔
375
                        if err := r.handleStorageClassChange(ctx, dataImportCron, desiredSc); err != nil {
1✔
NEW
376
                                return res, err
×
UNCOV
377
                        }
×
378
                        return reconcile.Result{RequeueAfter: time.Second}, nil
1✔
379
                }
380
                cc.AddAnnotation(dataImportCron, AnnStorageClass, desiredSc)
1✔
381
        }
382
        format, err := r.getSourceFormat(ctx, desiredStorageClass)
1✔
383
        if err != nil {
1✔
384
                return res, err
×
385
        }
×
386
        snapshot, err := r.getSnapshot(ctx, dataImportCron)
1✔
387
        if err != nil {
1✔
UNCOV
388
                return res, err
×
UNCOV
389
        }
×
390

391
        handlePopulatedPvc := func() error {
2✔
392
                if pvc != nil {
2✔
393
                        if err := r.updateSource(ctx, dataImportCron, pvc); err != nil {
1✔
UNCOV
394
                                return err
×
395
                        }
×
396
                }
397
                importSucceeded = true
1✔
398
                if err := r.handleCronFormat(ctx, dataImportCron, pvc, format, desiredStorageClass); err != nil {
1✔
UNCOV
399
                        return err
×
UNCOV
400
                }
×
401

402
                return nil
1✔
403
        }
404

405
        switch {
1✔
406
        case dv != nil:
1✔
407
                switch dv.Status.Phase {
1✔
408
                case cdiv1.Succeeded:
1✔
409
                        if err := handlePopulatedPvc(); err != nil {
1✔
UNCOV
410
                                return res, err
×
UNCOV
411
                        }
×
412
                case cdiv1.ImportScheduled:
1✔
413
                        updateDataImportCronCondition(dataImportCron, cdiv1.DataImportCronProgressing, corev1.ConditionFalse, "Import is scheduled", scheduled)
1✔
414
                case cdiv1.ImportInProgress:
1✔
415
                        updateDataImportCronCondition(dataImportCron, cdiv1.DataImportCronProgressing, corev1.ConditionTrue, "Import is progressing", inProgress)
1✔
416
                default:
1✔
417
                        dvPhase := string(dv.Status.Phase)
1✔
418
                        updateDataImportCronCondition(dataImportCron, cdiv1.DataImportCronProgressing, corev1.ConditionFalse, fmt.Sprintf("Import DataVolume phase %s", dvPhase), dvPhase)
1✔
419
                }
420
        case pvc != nil && pvc.Status.Phase == corev1.ClaimBound:
1✔
421
                if err := handlePopulatedPvc(); err != nil {
1✔
422
                        return res, err
×
423
                }
×
424
        case snapshot != nil:
1✔
425
                if format == cdiv1.DataImportCronSourceFormatPvc {
1✔
426
                        if err := r.client.Delete(ctx, snapshot); cc.IgnoreNotFound(err) != nil {
×
UNCOV
427
                                return res, err
×
UNCOV
428
                        }
×
UNCOV
429
                        r.log.Info("Snapshot is around even though format switched to PVC, requeueing")
×
UNCOV
430
                        return reconcile.Result{RequeueAfter: time.Second}, nil
×
431
                }
432
                // Check if snapshot class changed
433
                if desiredStorageClass != nil {
2✔
434
                        changed, err := r.handleSnapshotClassChange(ctx, snapshot, desiredStorageClass.Name)
1✔
435
                        if err != nil {
1✔
436
                                return res, err
×
UNCOV
437
                        }
×
438
                        if changed {
1✔
UNCOV
439
                                return reconcile.Result{RequeueAfter: time.Second}, nil
×
UNCOV
440
                        }
×
441
                }
442
                // Below k8s 1.29 there's no way to know the source volume mode
443
                // Let's at least expose this info on our own snapshots
444
                if _, ok := snapshot.Annotations[cc.AnnSourceVolumeMode]; !ok {
2✔
445
                        volMode, err := inferVolumeModeForSnapshot(ctx, r.client, dataImportCron)
1✔
446
                        if err != nil {
1✔
UNCOV
447
                                return res, err
×
UNCOV
448
                        }
×
449
                        if volMode != nil {
2✔
450
                                cc.AddAnnotation(snapshot, cc.AnnSourceVolumeMode, string(*volMode))
1✔
451
                        }
1✔
452
                }
453
                if _, ok := snapshot.Annotations[cc.AnnAdvisedRestoreSize]; !ok {
2✔
454
                        size := inferAdvisedRestoreSizeForSnapshot(&dataImportCron.Spec.Template, snapshot, nil)
1✔
455
                        if size != nil {
2✔
456
                                cc.AddAnnotation(snapshot, cc.AnnAdvisedRestoreSize, size.String())
1✔
457
                        }
1✔
458
                }
459
                // Copy labels found on dataSource to the existing snapshot in case of upgrades.
460
                dataSource, err := r.getDataSource(ctx, dataImportCron)
1✔
461
                if err != nil {
2✔
462
                        if !k8serrors.IsNotFound(err) && !errors.Is(err, ErrNotManagedByCron) {
1✔
UNCOV
463
                                return res, err
×
UNCOV
464
                        }
×
465
                } else {
1✔
466
                        cc.CopyAllowedLabels(dataSource.Labels, snapshot, true)
1✔
467
                }
1✔
468
                if err := r.updateSource(ctx, dataImportCron, snapshot); err != nil {
1✔
UNCOV
469
                        return res, err
×
UNCOV
470
                }
×
471
                importSucceeded = true
1✔
472
        default:
1✔
473
                if len(imports) > 0 {
2✔
474
                        imports = imports[1:]
1✔
475
                        dataImportCron.Status.CurrentImports = imports
1✔
476
                }
1✔
477
                updateDataImportCronCondition(dataImportCron, cdiv1.DataImportCronProgressing, corev1.ConditionFalse, "No current import", noImport)
1✔
478
        }
479

480
        if importSucceeded {
2✔
481
                if err := updateDataImportCronOnSuccess(dataImportCron); err != nil {
1✔
482
                        return res, err
×
483
                }
×
484
                updateDataImportCronCondition(dataImportCron, cdiv1.DataImportCronProgressing, corev1.ConditionFalse, "No current import", noImport)
1✔
485
                if err := r.garbageCollectOldImports(ctx, dataImportCron); err != nil {
1✔
UNCOV
486
                        return res, err
×
487
                }
×
488
        }
489

490
        if err := r.updateDataSource(ctx, dataImportCron, format); err != nil {
1✔
UNCOV
491
                return res, err
×
UNCOV
492
        }
×
493

494
        // Skip if schedule is disabled
495
        if isControllerPolledSource(dataImportCron) && dataImportCron.Spec.Schedule != "" {
2✔
496
                // We use the poll returned reconcile.Result for RequeueAfter if needed
1✔
497
                pollRes, err := r.pollSourceDigest(ctx, dataImportCron)
1✔
498
                if err != nil {
2✔
499
                        return pollRes, err
1✔
500
                }
1✔
501
                res = pollRes
1✔
502
        }
503

504
        desiredDigest := dataImportCron.Annotations[AnnSourceDesiredDigest]
1✔
505
        digestUpdated := desiredDigest != "" && (len(imports) == 0 || desiredDigest != imports[0].Digest)
1✔
506
        if digestUpdated {
2✔
507
                updateDataImportCronCondition(dataImportCron, cdiv1.DataImportCronUpToDate, corev1.ConditionFalse, "Source digest updated since last import", outdated)
1✔
508
                if dv != nil {
1✔
UNCOV
509
                        if err := r.deleteErroneousDataVolume(ctx, dataImportCron, dv); err != nil {
×
UNCOV
510
                                return res, err
×
UNCOV
511
                        }
×
512
                }
513
                if importSucceeded || len(imports) == 0 {
2✔
514
                        if err := r.createImportDataVolume(ctx, dataImportCron, desiredStorageClass); err != nil {
2✔
515
                                return res, err
1✔
516
                        }
1✔
517
                }
518
        } else if importSucceeded {
2✔
519
                if err := r.updateDataImportCronSuccessCondition(dataImportCron, format, snapshot); err != nil {
1✔
UNCOV
520
                        return res, err
×
UNCOV
521
                }
×
522
        } else if len(imports) > 0 {
2✔
523
                updateDataImportCronCondition(dataImportCron, cdiv1.DataImportCronUpToDate, corev1.ConditionFalse, "Import is progressing", inProgress)
1✔
524
        } else {
2✔
525
                updateDataImportCronCondition(dataImportCron, cdiv1.DataImportCronUpToDate, corev1.ConditionFalse, "No source digest", noDigest)
1✔
526
        }
1✔
527

528
        if err := updateLastExecutionTimestamp(dataImportCron); err != nil {
1✔
UNCOV
529
                return res, err
×
530
        }
×
531

532
        if !reflect.DeepEqual(dataImportCron, dataImportCronCopy) {
2✔
533
                if err := r.client.Update(ctx, dataImportCron); err != nil {
1✔
UNCOV
534
                        return res, err
×
UNCOV
535
                }
×
536
        }
537
        return res, nil
1✔
538
}
539

540
// Returns the current import DV if exists, and the last imported PVC
541
func (r *DataImportCronReconciler) getImportState(ctx context.Context, cron *cdiv1.DataImportCron) (*cdiv1.DataVolume, *corev1.PersistentVolumeClaim, error) {
1✔
542
        imports := cron.Status.CurrentImports
1✔
543
        if len(imports) == 0 {
2✔
544
                return nil, nil, nil
1✔
545
        }
1✔
546

547
        dvName := imports[0].DataVolumeName
1✔
548
        dv := &cdiv1.DataVolume{}
1✔
549
        if err := r.client.Get(ctx, types.NamespacedName{Namespace: cron.Namespace, Name: dvName}, dv); err != nil {
2✔
550
                if !k8serrors.IsNotFound(err) {
1✔
UNCOV
551
                        return nil, nil, err
×
UNCOV
552
                }
×
553
                dv = nil
1✔
554
        }
555

556
        pvc := &corev1.PersistentVolumeClaim{}
1✔
557
        if err := r.client.Get(ctx, types.NamespacedName{Namespace: cron.Namespace, Name: dvName}, pvc); err != nil {
2✔
558
                if !k8serrors.IsNotFound(err) {
1✔
UNCOV
559
                        return nil, nil, err
×
UNCOV
560
                }
×
561
                pvc = nil
1✔
562
        }
563
        return dv, pvc, nil
1✔
564
}
565

566
// Returns the current import DV if exists, and the last imported PVC
567
func (r *DataImportCronReconciler) getSnapshot(ctx context.Context, cron *cdiv1.DataImportCron) (*snapshotv1.VolumeSnapshot, error) {
1✔
568
        imports := cron.Status.CurrentImports
1✔
569
        if len(imports) == 0 {
2✔
570
                return nil, nil
1✔
571
        }
1✔
572

573
        snapName := imports[0].DataVolumeName
1✔
574
        snapshot := &snapshotv1.VolumeSnapshot{}
1✔
575
        if err := r.client.Get(ctx, types.NamespacedName{Namespace: cron.Namespace, Name: snapName}, snapshot); err != nil {
2✔
576
                if !k8serrors.IsNotFound(err) && !meta.IsNoMatchError(err) {
1✔
UNCOV
577
                        return nil, err
×
UNCOV
578
                }
×
579
                return nil, nil
1✔
580
        }
581

582
        return snapshot, nil
1✔
583
}
584

585
func (r *DataImportCronReconciler) getDataSource(ctx context.Context, dataImportCron *cdiv1.DataImportCron) (*cdiv1.DataSource, error) {
1✔
586
        dataSourceName := dataImportCron.Spec.ManagedDataSource
1✔
587
        dataSource := &cdiv1.DataSource{}
1✔
588
        if err := r.client.Get(ctx, types.NamespacedName{Namespace: dataImportCron.Namespace, Name: dataSourceName}, dataSource); err != nil {
2✔
589
                return nil, err
1✔
590
        }
1✔
591
        if dataSource.Labels[common.DataImportCronLabel] != dataImportCron.Name {
1✔
UNCOV
592
                log := r.log.WithName("getCronManagedDataSource")
×
UNCOV
593
                log.Info("DataSource has no DataImportCron label or is not managed by cron, so it is not updated", "name", dataSourceName, "uid", dataSource.UID, "cron", dataImportCron.Name)
×
UNCOV
594
                return nil, ErrNotManagedByCron
×
UNCOV
595
        }
×
596
        return dataSource, nil
1✔
597
}
598

599
func (r *DataImportCronReconciler) updateSource(ctx context.Context, cron *cdiv1.DataImportCron, obj client.Object) error {
1✔
600
        objCopy := obj.DeepCopyObject()
1✔
601
        cc.AddAnnotation(obj, AnnLastUseTime, time.Now().UTC().Format(time.RFC3339Nano))
1✔
602
        r.setDataImportCronResourceLabels(cron, obj)
1✔
603
        if !reflect.DeepEqual(obj, objCopy) {
2✔
604
                if err := r.client.Update(ctx, obj); err != nil {
1✔
UNCOV
605
                        return err
×
UNCOV
606
                }
×
607
        }
608
        return nil
1✔
609
}
610

611
func (r *DataImportCronReconciler) deleteErroneousDataVolume(ctx context.Context, cron *cdiv1.DataImportCron, dv *cdiv1.DataVolume) error {
×
612
        log := r.log.WithValues("name", dv.Name).WithValues("uid", dv.UID)
×
613
        if cond := dvc.FindConditionByType(cdiv1.DataVolumeRunning, dv.Status.Conditions); cond != nil {
×
614
                if cond.Status == corev1.ConditionFalse &&
×
615
                        (cond.Reason == common.GenericError || cond.Reason == ImagePullFailedReason) {
×
616
                        log.Info("Delete DataVolume and reset DesiredDigest due to error", "message", cond.Message)
×
617
                        // Unlabel the DV before deleting it, to eliminate reconcile before DIC is updated
×
618
                        dv.Labels[common.DataImportCronLabel] = ""
×
619
                        if err := r.client.Update(ctx, dv); cc.IgnoreNotFound(err) != nil {
×
620
                                return err
×
621
                        }
×
UNCOV
622
                        if err := r.client.Delete(ctx, dv); cc.IgnoreNotFound(err) != nil {
×
UNCOV
623
                                return err
×
624
                        }
×
UNCOV
625
                        cron.Status.CurrentImports = nil
×
626
                }
627
        }
UNCOV
628
        return nil
×
629
}
630

631
func (r *DataImportCronReconciler) updateImageStreamDesiredDigest(ctx context.Context, dataImportCron *cdiv1.DataImportCron) error {
1✔
632
        log := r.log.WithValues("name", dataImportCron.Name).WithValues("uid", dataImportCron.UID)
1✔
633
        regSource, err := getCronRegistrySource(dataImportCron)
1✔
634
        if err != nil {
1✔
635
                return err
×
UNCOV
636
        }
×
637
        if regSource.ImageStream == nil {
1✔
UNCOV
638
                return nil
×
UNCOV
639
        }
×
640
        imageStream, imageStreamTag, err := r.getImageStream(ctx, *regSource.ImageStream, dataImportCron.Namespace)
1✔
641
        if err != nil {
2✔
642
                return err
1✔
643
        }
1✔
644
        digest, dockerRef, err := getImageStreamDigest(imageStream, imageStreamTag)
1✔
645
        if err != nil {
2✔
646
                return err
1✔
647
        }
1✔
648
        cc.AddAnnotation(dataImportCron, AnnLastCronTime, time.Now().Format(time.RFC3339))
1✔
649
        if digest != "" && dataImportCron.Annotations[AnnSourceDesiredDigest] != digest {
2✔
650
                log.Info("Updating DataImportCron", "digest", digest)
1✔
651
                cc.AddAnnotation(dataImportCron, AnnSourceDesiredDigest, digest)
1✔
652
                cc.AddAnnotation(dataImportCron, AnnImageStreamDockerRef, dockerRef)
1✔
653
        }
1✔
654
        return nil
1✔
655
}
656

657
func (r *DataImportCronReconciler) updateContainerImageDesiredDigest(ctx context.Context, cron *cdiv1.DataImportCron) (bool, error) {
1✔
658
        log := r.log.WithValues("name", cron.Name).WithValues("uid", cron.UID)
1✔
659
        podName := getPollerPodName(cron)
1✔
660
        ns := cron.Namespace
1✔
661
        nn := types.NamespacedName{Name: podName, Namespace: ns}
1✔
662
        pod := &corev1.Pod{}
1✔
663

1✔
664
        if err := r.client.Get(ctx, nn, pod); err == nil {
2✔
665
                digest, err := fetchContainerImageDigest(pod)
1✔
666
                if err != nil || digest == "" {
1✔
UNCOV
667
                        return false, err
×
UNCOV
668
                }
×
669
                cc.AddAnnotation(cron, AnnLastCronTime, time.Now().Format(time.RFC3339))
1✔
670
                if cron.Annotations[AnnSourceDesiredDigest] != digest {
2✔
671
                        log.Info("Updating DataImportCron", "digest", digest)
1✔
672
                        cc.AddAnnotation(cron, AnnSourceDesiredDigest, digest)
1✔
673
                }
1✔
674
                return true, r.client.Delete(ctx, pod)
1✔
675
        } else if cc.IgnoreNotFound(err) != nil {
1✔
UNCOV
676
                return false, err
×
677
        }
×
678

679
        workloadNodePlacement, err := cc.GetWorkloadNodePlacement(ctx, r.client)
1✔
680
        if err != nil {
1✔
681
                return false, err
×
682
        }
×
683
        platform := cron.Spec.Template.Spec.Source.Registry.Platform
1✔
684
        if platform != nil && platform.Architecture != "" {
1✔
UNCOV
685
                if workloadNodePlacement.NodeSelector == nil {
×
UNCOV
686
                        workloadNodePlacement.NodeSelector = map[string]string{}
×
UNCOV
687
                }
×
UNCOV
688
                workloadNodePlacement.NodeSelector[corev1.LabelArchStable] = platform.Architecture
×
689
        }
690

691
        containerImage := strings.TrimPrefix(*cron.Spec.Template.Spec.Source.Registry.URL, "docker://")
1✔
692

1✔
693
        pod = &corev1.Pod{
1✔
694
                ObjectMeta: metav1.ObjectMeta{
1✔
695
                        Name:      podName,
1✔
696
                        Namespace: ns,
1✔
697
                        OwnerReferences: []metav1.OwnerReference{
1✔
698
                                {
1✔
699
                                        APIVersion:         cron.APIVersion,
1✔
700
                                        Kind:               cron.Kind,
1✔
701
                                        Name:               cron.Name,
1✔
702
                                        UID:                cron.UID,
1✔
703
                                        BlockOwnerDeletion: ptr.To[bool](true),
1✔
704
                                        Controller:         ptr.To[bool](true),
1✔
705
                                },
1✔
706
                        },
1✔
707
                },
1✔
708
                Spec: corev1.PodSpec{
1✔
709
                        TerminationGracePeriodSeconds: ptr.To[int64](0),
1✔
710
                        RestartPolicy:                 corev1.RestartPolicyNever,
1✔
711
                        NodeSelector:                  workloadNodePlacement.NodeSelector,
1✔
712
                        Tolerations:                   workloadNodePlacement.Tolerations,
1✔
713
                        Affinity:                      workloadNodePlacement.Affinity,
1✔
714
                        Volumes: []corev1.Volume{
1✔
715
                                {
1✔
716
                                        Name: "shared-volume",
1✔
717
                                        VolumeSource: corev1.VolumeSource{
1✔
718
                                                EmptyDir: &corev1.EmptyDirVolumeSource{},
1✔
719
                                        },
1✔
720
                                },
1✔
721
                        },
1✔
722
                        InitContainers: []corev1.Container{
1✔
723
                                {
1✔
724
                                        Name:                     "init",
1✔
725
                                        Image:                    r.image,
1✔
726
                                        ImagePullPolicy:          corev1.PullPolicy(r.pullPolicy),
1✔
727
                                        Command:                  []string{"sh", "-c", "cp /usr/bin/cdi-containerimage-server /shared/server"},
1✔
728
                                        VolumeMounts:             []corev1.VolumeMount{{Name: "shared-volume", MountPath: "/shared"}},
1✔
729
                                        TerminationMessagePolicy: corev1.TerminationMessageFallbackToLogsOnError,
1✔
730
                                },
1✔
731
                        },
1✔
732
                        Containers: []corev1.Container{
1✔
733
                                {
1✔
734
                                        Name:                     "image-container",
1✔
735
                                        Image:                    containerImage,
1✔
736
                                        ImagePullPolicy:          corev1.PullAlways,
1✔
737
                                        Command:                  []string{"/shared/server", "-h"},
1✔
738
                                        VolumeMounts:             []corev1.VolumeMount{{Name: "shared-volume", MountPath: "/shared"}},
1✔
739
                                        TerminationMessagePolicy: corev1.TerminationMessageFallbackToLogsOnError,
1✔
740
                                },
1✔
741
                        },
1✔
742
                },
1✔
743
        }
1✔
744

1✔
745
        cc.SetRestrictedSecurityContext(&pod.Spec)
1✔
746
        if pod.Spec.SecurityContext != nil {
2✔
747
                pod.Spec.SecurityContext.FSGroup = nil
1✔
748
        }
1✔
749

750
        return false, r.client.Create(ctx, pod)
1✔
751
}
752

753
func fetchContainerImageDigest(pod *corev1.Pod) (string, error) {
1✔
754
        if len(pod.Status.ContainerStatuses) == 0 {
1✔
UNCOV
755
                return "", nil
×
756
        }
×
757

758
        status := pod.Status.ContainerStatuses[0]
1✔
759
        if status.State.Waiting != nil {
1✔
UNCOV
760
                reason := status.State.Waiting.Reason
×
761
                switch reason {
×
UNCOV
762
                case "ImagePullBackOff", "ErrImagePull", "InvalidImageName":
×
UNCOV
763
                        return "", errors.Errorf("%s %s: %s", common.ImagePullFailureText, status.Image, reason)
×
764
                }
765
                return "", nil
×
766
        }
767

768
        if status.State.Terminated == nil {
1✔
UNCOV
769
                return "", nil
×
770
        }
×
771

772
        imageID := status.ImageID
1✔
773
        if imageID == "" {
1✔
774
                return "", errors.Errorf("Container has no imageID")
×
775
        }
×
776
        idx := strings.Index(imageID, digestSha256Prefix)
1✔
777
        if idx < 0 {
1✔
UNCOV
778
                return "", errors.Errorf("Container image %s ID has no digest: %s", status.Image, imageID)
×
UNCOV
779
        }
×
780

781
        return imageID[idx:], nil
1✔
782
}
783

784
func (r *DataImportCronReconciler) updatePvcDesiredDigest(ctx context.Context, dataImportCron *cdiv1.DataImportCron) error {
1✔
785
        log := r.log.WithValues("name", dataImportCron.Name).WithValues("uid", dataImportCron.UID)
1✔
786
        pvcSource, err := getCronPvcSource(dataImportCron)
1✔
787
        if err != nil {
1✔
UNCOV
788
                return err
×
UNCOV
789
        }
×
790
        ns := pvcSource.Namespace
1✔
791
        if ns == "" {
2✔
792
                ns = dataImportCron.Namespace
1✔
793
        }
1✔
794
        pvc := &corev1.PersistentVolumeClaim{}
1✔
795
        if err := r.client.Get(ctx, types.NamespacedName{Namespace: ns, Name: pvcSource.Name}, pvc); err != nil {
2✔
796
                return err
1✔
797
        }
1✔
798
        digest := fmt.Sprintf("%s%s", digestUIDPrefix, pvc.UID)
1✔
799
        cc.AddAnnotation(dataImportCron, AnnLastCronTime, time.Now().Format(time.RFC3339))
1✔
800
        if digest != "" && dataImportCron.Annotations[AnnSourceDesiredDigest] != digest {
2✔
801
                log.Info("Updating DataImportCron", "digest", digest)
1✔
802
                cc.AddAnnotation(dataImportCron, AnnSourceDesiredDigest, digest)
1✔
803
        }
1✔
804
        return nil
1✔
805
}
806

807
func (r *DataImportCronReconciler) updateDataSource(ctx context.Context, dataImportCron *cdiv1.DataImportCron, format cdiv1.DataImportCronSourceFormat) error {
1✔
808
        log := r.log.WithName("updateDataSource")
1✔
809
        dataSource, err := r.getDataSource(ctx, dataImportCron)
1✔
810
        if err != nil {
2✔
811
                if k8serrors.IsNotFound(err) {
2✔
812
                        dataSource = r.newDataSource(dataImportCron)
1✔
813
                        if err := r.client.Create(ctx, dataSource); err != nil {
1✔
814
                                return err
×
815
                        }
×
816
                        log.Info("DataSource created", "name", dataSource.Name, "uid", dataSource.UID)
1✔
817
                } else if errors.Is(err, ErrNotManagedByCron) {
×
UNCOV
818
                        return nil
×
UNCOV
819
                } else {
×
UNCOV
820
                        return err
×
UNCOV
821
                }
×
822
        }
823
        dataSourceCopy := dataSource.DeepCopy()
1✔
824
        r.setDataImportCronResourceLabels(dataImportCron, dataSource)
1✔
825

1✔
826
        sourcePVC := dataImportCron.Status.LastImportedPVC
1✔
827
        populateDataSource(format, dataSource, sourcePVC)
1✔
828

1✔
829
        if !reflect.DeepEqual(dataSource, dataSourceCopy) {
2✔
830
                if err := r.client.Update(ctx, dataSource); err != nil {
1✔
UNCOV
831
                        return err
×
UNCOV
832
                }
×
833
        }
834

835
        return nil
1✔
836
}
837

838
func populateDataSource(format cdiv1.DataImportCronSourceFormat, dataSource *cdiv1.DataSource, sourcePVC *cdiv1.DataVolumeSourcePVC) {
1✔
839
        if sourcePVC == nil {
2✔
840
                return
1✔
841
        }
1✔
842

843
        switch format {
1✔
844
        case cdiv1.DataImportCronSourceFormatPvc:
1✔
845
                dataSource.Spec.Source = cdiv1.DataSourceSource{
1✔
846
                        PVC: sourcePVC,
1✔
847
                }
1✔
848
        case cdiv1.DataImportCronSourceFormatSnapshot:
1✔
849
                dataSource.Spec.Source = cdiv1.DataSourceSource{
1✔
850
                        Snapshot: &cdiv1.DataVolumeSourceSnapshot{
1✔
851
                                Namespace: sourcePVC.Namespace,
1✔
852
                                Name:      sourcePVC.Name,
1✔
853
                        },
1✔
854
                }
1✔
855
        }
856
}
857

858
func updateDataImportCronOnSuccess(dataImportCron *cdiv1.DataImportCron) error {
1✔
859
        if dataImportCron.Status.CurrentImports == nil {
1✔
UNCOV
860
                return errors.Errorf("No CurrentImports in cron %s", dataImportCron.Name)
×
UNCOV
861
        }
×
862
        sourcePVC := &cdiv1.DataVolumeSourcePVC{
1✔
863
                Namespace: dataImportCron.Namespace,
1✔
864
                Name:      dataImportCron.Status.CurrentImports[0].DataVolumeName,
1✔
865
        }
1✔
866
        if dataImportCron.Status.LastImportedPVC == nil || *dataImportCron.Status.LastImportedPVC != *sourcePVC {
2✔
867
                dataImportCron.Status.LastImportedPVC = sourcePVC
1✔
868
                now := metav1.Now()
1✔
869
                dataImportCron.Status.LastImportTimestamp = &now
1✔
870
        }
1✔
871
        return nil
1✔
872
}
873

874
func updateLastExecutionTimestamp(cron *cdiv1.DataImportCron) error {
1✔
875
        lastTimeStr := cron.Annotations[AnnLastCronTime]
1✔
876
        if lastTimeStr == "" {
2✔
877
                return nil
1✔
878
        }
1✔
879
        lastTime, err := time.Parse(time.RFC3339, lastTimeStr)
1✔
880
        if err != nil {
1✔
UNCOV
881
                return err
×
UNCOV
882
        }
×
883
        if ts := cron.Status.LastExecutionTimestamp; ts == nil || ts.Time != lastTime {
2✔
884
                cron.Status.LastExecutionTimestamp = &metav1.Time{Time: lastTime}
1✔
885
        }
1✔
886
        return nil
1✔
887
}
888

889
func (r *DataImportCronReconciler) createImportDataVolume(ctx context.Context, dataImportCron *cdiv1.DataImportCron, desiredStorageClass *storagev1.StorageClass) error {
1✔
890
        dataSourceName := dataImportCron.Spec.ManagedDataSource
1✔
891
        digest := dataImportCron.Annotations[AnnSourceDesiredDigest]
1✔
892
        if digest == "" {
1✔
UNCOV
893
                return nil
×
UNCOV
894
        }
×
895
        dvName, err := createDvName(dataSourceName, digest)
1✔
896
        if err != nil {
2✔
897
                return err
1✔
898
        }
1✔
899

900
        sources := []client.Object{&snapshotv1.VolumeSnapshot{}, &corev1.PersistentVolumeClaim{}}
1✔
901
        for _, src := range sources {
2✔
902
                if err := r.client.Get(ctx, types.NamespacedName{Namespace: dataImportCron.Namespace, Name: dvName}, src); err != nil {
2✔
903
                        if !k8serrors.IsNotFound(err) && !meta.IsNoMatchError(err) {
1✔
904
                                return err
×
905
                        }
×
906
                } else {
1✔
907
                        if err := r.updateSource(ctx, dataImportCron, src); err != nil {
1✔
UNCOV
908
                                return err
×
UNCOV
909
                        }
×
910
                        // If source exists don't create DV
911
                        dataImportCron.Status.CurrentImports = []cdiv1.ImportStatus{{DataVolumeName: dvName, Digest: digest}}
1✔
912
                        return nil
1✔
913
                }
914
        }
915

916
        storageProfile := &cdiv1.StorageProfile{}
1✔
917
        if desiredStorageClass != nil {
2✔
918
                if err := r.client.Get(ctx, types.NamespacedName{Name: desiredStorageClass.Name}, storageProfile); err != nil {
1✔
UNCOV
919
                        return err
×
920
                }
×
921
        }
922
        dv := r.newSourceDataVolume(dataImportCron, dvName, storageProfile)
1✔
923
        if allowed, err := r.authorizeCloneDataVolume(dataImportCron, dv); err != nil {
1✔
UNCOV
924
                return err
×
925
        } else if !allowed {
2✔
926
                updateDataImportCronCondition(dataImportCron, cdiv1.DataImportCronProgressing, corev1.ConditionFalse,
1✔
927
                        "Not authorized to create DataVolume", notAuthorized)
1✔
928
                return nil
1✔
929
        }
1✔
930
        if err := r.client.Create(ctx, dv); err != nil && !k8serrors.IsAlreadyExists(err) {
1✔
UNCOV
931
                return err
×
UNCOV
932
        }
×
933
        dataImportCron.Status.CurrentImports = []cdiv1.ImportStatus{{DataVolumeName: dvName, Digest: digest}}
1✔
934

1✔
935
        return nil
1✔
936
}
937

938
func (r *DataImportCronReconciler) authorizeCloneDataVolume(dataImportCron *cdiv1.DataImportCron, dv *cdiv1.DataVolume) (bool, error) {
1✔
939
        if !isPvcSource(dataImportCron) {
2✔
940
                return true, nil
1✔
941
        }
1✔
942
        saName := "default"
1✔
943
        if dataImportCron.Spec.ServiceAccountName != nil {
1✔
UNCOV
944
                saName = *dataImportCron.Spec.ServiceAccountName
×
UNCOV
945
        }
×
946
        if resp, err := dv.AuthorizeSA(dv.Namespace, dv.Name, &authProxy{r.client}, dataImportCron.Namespace, saName); err != nil {
1✔
UNCOV
947
                return false, err
×
948
        } else if !resp.Allowed {
2✔
949
                r.log.Info("Not authorized to create DataVolume", "cron", dataImportCron.Name, "reason", resp.Reason)
1✔
950
                return false, nil
1✔
951
        }
1✔
952

953
        return true, nil
1✔
954
}
955

956
type authProxy struct {
957
        client client.Client
958
}
959

960
func (p *authProxy) CreateSar(sar *authorizationv1.SubjectAccessReview) (*authorizationv1.SubjectAccessReview, error) {
1✔
961
        if err := p.client.Create(context.TODO(), sar); err != nil {
1✔
UNCOV
962
                return nil, err
×
UNCOV
963
        }
×
964
        return sar, nil
1✔
965
}
966

967
func (p *authProxy) GetNamespace(name string) (*corev1.Namespace, error) {
1✔
968
        ns := &corev1.Namespace{}
1✔
969
        if err := p.client.Get(context.TODO(), types.NamespacedName{Name: name}, ns); err != nil {
1✔
UNCOV
970
                return nil, err
×
971
        }
×
972
        return ns, nil
1✔
973
}
974

975
func (p *authProxy) GetDataSource(namespace, name string) (*cdiv1.DataSource, error) {
×
976
        das := &cdiv1.DataSource{}
×
UNCOV
977
        if err := p.client.Get(context.TODO(), types.NamespacedName{Namespace: namespace, Name: name}, das); err != nil {
×
UNCOV
978
                return nil, err
×
NEW
979
        }
×
UNCOV
980
        return das, nil
×
981
}
982

983
func (r *DataImportCronReconciler) handleStorageClassChange(ctx context.Context, dataImportCron *cdiv1.DataImportCron, desiredStorageClass string) error {
1✔
984
        digest, ok := dataImportCron.Annotations[AnnSourceDesiredDigest]
1✔
985
        if !ok {
1✔
UNCOV
986
                // nothing to delete
×
987
                return nil
×
988
        }
×
989
        name, err := createDvName(dataImportCron.Spec.ManagedDataSource, digest)
1✔
990
        if err != nil {
1✔
UNCOV
991
                return err
×
UNCOV
992
        }
×
993
        om := metav1.ObjectMeta{Name: name, Namespace: dataImportCron.Namespace}
1✔
994
        sources := []client.Object{&snapshotv1.VolumeSnapshot{ObjectMeta: om}, &cdiv1.DataVolume{ObjectMeta: om}, &corev1.PersistentVolumeClaim{ObjectMeta: om}}
1✔
995
        for _, src := range sources {
2✔
996
                if err := r.client.Delete(ctx, src); cc.IgnoreNotFound(err) != nil {
1✔
UNCOV
997
                        return err
×
998
                }
×
999
        }
1000
        for _, src := range sources {
2✔
1001
                if err := r.client.Get(ctx, client.ObjectKeyFromObject(src), src); err == nil || !k8serrors.IsNotFound(err) {
1✔
UNCOV
1002
                        return fmt.Errorf("waiting for old sources to get cleaned up: %w", err)
×
UNCOV
1003
                }
×
1004
        }
1005
        // Only update desired storage class once garbage collection went through
1006
        annPatch := fmt.Sprintf(`[{"op":"add","path":"/metadata/annotations/%s","value":"%s" }]`, openapicommon.EscapeJsonPointer(AnnStorageClass), desiredStorageClass)
1✔
1007
        err = r.client.Patch(ctx, dataImportCron, client.RawPatch(types.JSONPatchType, []byte(annPatch)))
1✔
1008
        if err != nil {
1✔
UNCOV
1009
                return err
×
UNCOV
1010
        }
×
1011

1012
        return nil
1✔
1013
}
1014

1015
func (r *DataImportCronReconciler) handleCronFormat(ctx context.Context, dataImportCron *cdiv1.DataImportCron, pvc *corev1.PersistentVolumeClaim, format cdiv1.DataImportCronSourceFormat, desiredStorageClass *storagev1.StorageClass) error {
1✔
1016
        switch format {
1✔
1017
        case cdiv1.DataImportCronSourceFormatPvc:
1✔
1018
                return nil
1✔
1019
        case cdiv1.DataImportCronSourceFormatSnapshot:
1✔
1020
                return r.handleSnapshot(ctx, dataImportCron, pvc, desiredStorageClass)
1✔
UNCOV
1021
        default:
×
UNCOV
1022
                return fmt.Errorf("unknown source format for snapshot")
×
1023
        }
1024
}
1025

1026
func (r *DataImportCronReconciler) handleSnapshot(ctx context.Context, dataImportCron *cdiv1.DataImportCron, pvc *corev1.PersistentVolumeClaim, desiredStorageClass *storagev1.StorageClass) error {
1✔
1027
        if pvc == nil {
1✔
UNCOV
1028
                return nil
×
UNCOV
1029
        }
×
1030
        if sc := pvc.Spec.StorageClassName; sc != nil && *sc != desiredStorageClass.Name {
2✔
1031
                r.log.Info("Attempt to change storage class, will not try making a snapshot of the old PVC")
1✔
1032
                return nil
1✔
1033
        }
1✔
1034
        storageProfile := &cdiv1.StorageProfile{}
1✔
1035
        if err := r.client.Get(ctx, types.NamespacedName{Name: desiredStorageClass.Name}, storageProfile); err != nil {
1✔
UNCOV
1036
                return err
×
UNCOV
1037
        }
×
1038

1039
        desiredSnapshot := &snapshotv1.VolumeSnapshot{
1✔
1040
                ObjectMeta: metav1.ObjectMeta{
1✔
1041
                        Name:      pvc.Name,
1✔
1042
                        Namespace: dataImportCron.Namespace,
1✔
1043
                        Labels: map[string]string{
1✔
1044
                                common.CDILabelKey:       common.CDILabelValue,
1✔
1045
                                common.CDIComponentLabel: "",
1✔
1046
                        },
1✔
1047
                },
1✔
1048
                Spec: snapshotv1.VolumeSnapshotSpec{
1✔
1049
                        Source: snapshotv1.VolumeSnapshotSource{
1✔
1050
                                PersistentVolumeClaimName: &pvc.Name,
1✔
1051
                        },
1✔
1052
                },
1✔
1053
        }
1✔
1054
        // Select VolumeSnapshotClass for boot source snapshot
1✔
1055
        snapshotClassName, err := r.getSnapshotClassForDataImportCron(pvc, storageProfile)
1✔
1056
        if err != nil {
1✔
UNCOV
1057
                return err
×
UNCOV
1058
        }
×
1059
        if snapshotClassName != "" {
2✔
1060
                desiredSnapshot.Spec.VolumeSnapshotClassName = &snapshotClassName
1✔
1061
        }
1✔
1062
        r.setDataImportCronResourceLabels(dataImportCron, desiredSnapshot)
1✔
1063
        cc.CopyAllowedLabels(pvc.GetLabels(), desiredSnapshot, false)
1✔
1064

1✔
1065
        currentSnapshot := &snapshotv1.VolumeSnapshot{}
1✔
1066
        if err := r.client.Get(ctx, client.ObjectKeyFromObject(desiredSnapshot), currentSnapshot); err != nil {
2✔
1067
                if !k8serrors.IsNotFound(err) {
1✔
UNCOV
1068
                        return err
×
UNCOV
1069
                }
×
1070
                cc.AddAnnotation(desiredSnapshot, AnnLastUseTime, time.Now().UTC().Format(time.RFC3339Nano))
1✔
1071
                pvcSize := pvc.Spec.Resources.Requests[corev1.ResourceStorage]
1✔
1072
                size := inferAdvisedRestoreSizeForSnapshot(&dataImportCron.Spec.Template, desiredSnapshot, &pvcSize)
1✔
1073
                if size != nil {
2✔
1074
                        cc.AddAnnotation(desiredSnapshot, cc.AnnAdvisedRestoreSize, size.String())
1✔
1075
                }
1✔
1076
                if pvc.Spec.VolumeMode != nil {
2✔
1077
                        cc.AddAnnotation(desiredSnapshot, cc.AnnSourceVolumeMode, string(*pvc.Spec.VolumeMode))
1✔
1078
                }
1✔
1079
                if err := r.client.Create(ctx, desiredSnapshot); err != nil {
1✔
UNCOV
1080
                        return err
×
UNCOV
1081
                }
×
1082
        } else {
1✔
1083
                if cc.IsSnapshotReady(currentSnapshot) {
2✔
1084
                        // Clean up DV/PVC as they are not needed anymore
1✔
1085
                        r.log.Info("Deleting dv/pvc as snapshot is ready", "name", desiredSnapshot.Name)
1✔
1086
                        if err := r.deleteDvPvc(ctx, desiredSnapshot.Name, desiredSnapshot.Namespace); err != nil {
1✔
UNCOV
1087
                                return err
×
UNCOV
1088
                        }
×
1089
                }
1090
        }
1091

1092
        return nil
1✔
1093
}
1094

1095
func (r *DataImportCronReconciler) handleSnapshotClassChange(ctx context.Context, snapshot *snapshotv1.VolumeSnapshot, storageClassName string) (bool, error) {
1✔
1096
        sp := &cdiv1.StorageProfile{}
1✔
1097
        if err := r.client.Get(ctx, types.NamespacedName{Name: storageClassName}, sp); err != nil {
1✔
UNCOV
1098
                return false, client.IgnoreNotFound(err)
×
1099
        }
×
1100

1101
        desiredVSC, err := r.getSnapshotClassForDataImportCron(nil, sp)
1✔
1102
        if err != nil {
1✔
1103
                return false, err
×
1104
        }
×
1105
        actualVSC := ""
1✔
1106
        if snapshot.Spec.VolumeSnapshotClassName != nil {
1✔
UNCOV
1107
                actualVSC = *snapshot.Spec.VolumeSnapshotClassName
×
UNCOV
1108
        }
×
1109
        if desiredVSC == "" || actualVSC == desiredVSC {
2✔
1110
                return false, nil
1✔
1111
        }
1✔
1112

1113
        r.log.Info("Snapshot class changed, deleting", "name", snapshot.Name, "from", actualVSC, "to", desiredVSC)
×
UNCOV
1114
        if err := r.client.Delete(ctx, snapshot); err != nil {
×
UNCOV
1115
                return false, client.IgnoreNotFound(err)
×
NEW
1116
        }
×
NEW
1117
        return true, nil
×
1118
}
1119

1120
// getSnapshotClassForDataImportCron returns the VolumeSnapshotClass name to use for DataImportCron snapshots.
1121
func (r *DataImportCronReconciler) getSnapshotClassForDataImportCron(pvc *corev1.PersistentVolumeClaim, storageProfile *cdiv1.StorageProfile) (string, error) {
1✔
1122
        if vscName := storageProfile.Annotations[cc.AnnSnapshotClassForDataImportCron]; vscName != "" {
2✔
1123
                return vscName, nil
1✔
1124
        }
1✔
1125
        return cc.GetSnapshotClassForSmartClone(pvc, &storageProfile.Name, storageProfile.Status.SnapshotClass, r.log, r.client, r.recorder)
1✔
1126
}
1127

1128
func (r *DataImportCronReconciler) updateDataImportCronSuccessCondition(dataImportCron *cdiv1.DataImportCron, format cdiv1.DataImportCronSourceFormat, snapshot *snapshotv1.VolumeSnapshot) error {
1✔
1129
        dataImportCron.Status.SourceFormat = &format
1✔
1130

1✔
1131
        switch format {
1✔
1132
        case cdiv1.DataImportCronSourceFormatPvc:
1✔
1133
                updateDataImportCronCondition(dataImportCron, cdiv1.DataImportCronUpToDate, corev1.ConditionTrue, "Latest import is up to date", upToDate)
1✔
1134
        case cdiv1.DataImportCronSourceFormatSnapshot:
1✔
1135
                if snapshot == nil {
2✔
1136
                        // Snapshot create/update will trigger reconcile
1✔
1137
                        return nil
1✔
1138
                }
1✔
1139
                if cc.IsSnapshotReady(snapshot) {
2✔
1140
                        updateDataImportCronCondition(dataImportCron, cdiv1.DataImportCronUpToDate, corev1.ConditionTrue, "Latest import is up to date", upToDate)
1✔
1141
                } else {
2✔
1142
                        updateDataImportCronCondition(dataImportCron, cdiv1.DataImportCronUpToDate, corev1.ConditionFalse, "Snapshot of imported data is progressing", inProgress)
1✔
1143
                }
1✔
UNCOV
1144
        default:
×
UNCOV
1145
                return fmt.Errorf("unknown source format for snapshot")
×
1146
        }
1147

1148
        return nil
1✔
1149
}
1150

1151
func (r *DataImportCronReconciler) getSourceFormat(ctx context.Context, desiredStorageClass *storagev1.StorageClass) (cdiv1.DataImportCronSourceFormat, error) {
1✔
1152
        format := cdiv1.DataImportCronSourceFormatPvc
1✔
1153
        if desiredStorageClass == nil {
2✔
1154
                return format, nil
1✔
1155
        }
1✔
1156

1157
        storageProfile := &cdiv1.StorageProfile{}
1✔
1158
        if err := r.client.Get(ctx, types.NamespacedName{Name: desiredStorageClass.Name}, storageProfile); err != nil {
1✔
UNCOV
1159
                return format, err
×
UNCOV
1160
        }
×
1161
        if storageProfile.Status.DataImportCronSourceFormat != nil {
2✔
1162
                format = *storageProfile.Status.DataImportCronSourceFormat
1✔
1163
        }
1✔
1164

1165
        return format, nil
1✔
1166
}
1167

1168
func (r *DataImportCronReconciler) garbageCollectOldImports(ctx context.Context, cron *cdiv1.DataImportCron) error {
1✔
1169
        if cron.Spec.GarbageCollect != nil && *cron.Spec.GarbageCollect != cdiv1.DataImportCronGarbageCollectOutdated {
1✔
UNCOV
1170
                return nil
×
UNCOV
1171
        }
×
1172
        selector, err := getSelector(map[string]string{common.DataImportCronLabel: cron.Name})
1✔
1173
        if err != nil {
1✔
UNCOV
1174
                return err
×
UNCOV
1175
        }
×
1176

1177
        maxImports := defaultImportsToKeepPerCron
1✔
1178

1✔
1179
        if cron.Spec.ImportsToKeep != nil && *cron.Spec.ImportsToKeep >= 0 {
2✔
1180
                maxImports = int(*cron.Spec.ImportsToKeep)
1✔
1181
        }
1✔
1182

1183
        if err := r.garbageCollectPVCs(ctx, cron.Namespace, cron.Name, selector, maxImports); err != nil {
1✔
UNCOV
1184
                return err
×
UNCOV
1185
        }
×
1186
        if err := r.garbageCollectSnapshots(ctx, cron.Namespace, selector, maxImports); err != nil {
1✔
UNCOV
1187
                return err
×
UNCOV
1188
        }
×
1189

1190
        return nil
1✔
1191
}
1192

1193
func (r *DataImportCronReconciler) garbageCollectPVCs(ctx context.Context, namespace, cronName string, selector labels.Selector, maxImports int) error {
1✔
1194
        pvcList := &corev1.PersistentVolumeClaimList{}
1✔
1195

1✔
1196
        if err := r.client.List(ctx, pvcList, &client.ListOptions{Namespace: namespace, LabelSelector: selector}); err != nil {
1✔
UNCOV
1197
                return err
×
UNCOV
1198
        }
×
1199
        if len(pvcList.Items) > maxImports {
2✔
1200
                sort.Slice(pvcList.Items, func(i, j int) bool {
2✔
1201
                        return pvcList.Items[i].Annotations[AnnLastUseTime] > pvcList.Items[j].Annotations[AnnLastUseTime]
1✔
1202
                })
1✔
1203
                for _, pvc := range pvcList.Items[maxImports:] {
2✔
1204
                        r.log.Info("Deleting dv/pvc", "name", pvc.Name, "pvc.uid", pvc.UID)
1✔
1205
                        if err := r.deleteDvPvc(ctx, pvc.Name, pvc.Namespace); err != nil {
1✔
1206
                                return err
×
UNCOV
1207
                        }
×
1208
                }
1209
        }
1210

1211
        dvList := &cdiv1.DataVolumeList{}
1✔
1212
        if err := r.client.List(ctx, dvList, &client.ListOptions{Namespace: namespace, LabelSelector: selector}); err != nil {
1✔
UNCOV
1213
                return err
×
UNCOV
1214
        }
×
1215

1216
        if len(dvList.Items) > maxImports {
2✔
1217
                for _, dv := range dvList.Items {
2✔
1218
                        pvc := &corev1.PersistentVolumeClaim{}
1✔
1219
                        if err := r.client.Get(ctx, types.NamespacedName{Namespace: namespace, Name: dv.Name}, pvc); err != nil {
1✔
UNCOV
1220
                                return err
×
UNCOV
1221
                        }
×
1222

1223
                        if pvc.Labels[common.DataImportCronLabel] != cronName {
2✔
1224
                                r.log.Info("Deleting old version dv/pvc", "name", pvc.Name, "pvc.uid", pvc.UID)
1✔
1225
                                if err := r.deleteDvPvc(ctx, dv.Name, dv.Namespace); err != nil {
1✔
UNCOV
1226
                                        return err
×
1227
                                }
×
1228
                        }
1229
                }
1230
        }
1231

1232
        return nil
1✔
1233
}
1234

1235
// deleteDvPvc deletes DV or PVC if DV was GCed
1236
func (r *DataImportCronReconciler) deleteDvPvc(ctx context.Context, name, namespace string) error {
1✔
1237
        om := metav1.ObjectMeta{Name: name, Namespace: namespace}
1✔
1238
        dv := &cdiv1.DataVolume{ObjectMeta: om}
1✔
1239
        if err := r.client.Delete(ctx, dv); err == nil || !k8serrors.IsNotFound(err) {
2✔
1240
                return err
1✔
1241
        }
1✔
1242
        pvc := &corev1.PersistentVolumeClaim{ObjectMeta: om}
1✔
1243
        if err := r.client.Delete(ctx, pvc); err != nil && !k8serrors.IsNotFound(err) {
1✔
UNCOV
1244
                return err
×
UNCOV
1245
        }
×
1246
        return nil
1✔
1247
}
1248

1249
func (r *DataImportCronReconciler) garbageCollectSnapshots(ctx context.Context, namespace string, selector labels.Selector, maxImports int) error {
1✔
1250
        snapList := &snapshotv1.VolumeSnapshotList{}
1✔
1251

1✔
1252
        if err := r.client.List(ctx, snapList, &client.ListOptions{Namespace: namespace, LabelSelector: selector}); err != nil {
1✔
UNCOV
1253
                if meta.IsNoMatchError(err) {
×
UNCOV
1254
                        return nil
×
UNCOV
1255
                }
×
UNCOV
1256
                return err
×
1257
        }
1258
        if len(snapList.Items) > maxImports {
1✔
UNCOV
1259
                sort.Slice(snapList.Items, func(i, j int) bool {
×
UNCOV
1260
                        return snapList.Items[i].Annotations[AnnLastUseTime] > snapList.Items[j].Annotations[AnnLastUseTime]
×
UNCOV
1261
                })
×
UNCOV
1262
                for _, snap := range snapList.Items[maxImports:] {
×
UNCOV
1263
                        r.log.Info("Deleting snapshot", "name", snap.Name, "uid", snap.UID)
×
UNCOV
1264
                        if err := r.client.Delete(ctx, &snap); err != nil && !k8serrors.IsNotFound(err) {
×
1265
                                return err
×
1266
                        }
×
1267
                }
1268
        }
1269

1270
        return nil
1✔
1271
}
1272

1273
func (r *DataImportCronReconciler) cleanup(ctx context.Context, cron types.NamespacedName) error {
1✔
1274
        // Don't keep alerting over a cron thats being deleted, will get set back to 1 again by reconcile loop if needed.
1✔
1275
        metrics.DeleteDataImportCronOutdated(getPrometheusCronLabels(cron.Namespace, cron.Name))
1✔
1276

1✔
1277
        if err := r.deleteJobs(ctx, cron); err != nil {
1✔
UNCOV
1278
                return err
×
UNCOV
1279
        }
×
1280
        selector, err := getSelector(map[string]string{common.DataImportCronLabel: cron.Name, common.DataImportCronCleanupLabel: "true"})
1✔
1281
        if err != nil {
1✔
1282
                return err
×
1283
        }
×
1284
        opts := &client.DeleteAllOfOptions{ListOptions: client.ListOptions{Namespace: cron.Namespace, LabelSelector: selector}}
1✔
1285
        if err := r.client.DeleteAllOf(ctx, &cdiv1.DataSource{}, opts); err != nil {
1✔
1286
                return err
×
1287
        }
×
1288
        if err := r.client.DeleteAllOf(ctx, &cdiv1.DataVolume{}, opts); err != nil {
1✔
UNCOV
1289
                return err
×
UNCOV
1290
        }
×
1291
        if err := r.client.DeleteAllOf(ctx, &corev1.PersistentVolumeClaim{}, opts); err != nil {
1✔
UNCOV
1292
                return err
×
UNCOV
1293
        }
×
1294
        if err := r.client.DeleteAllOf(ctx, &snapshotv1.VolumeSnapshot{}, opts); cc.IgnoreIsNoMatchError(err) != nil {
1✔
UNCOV
1295
                return err
×
UNCOV
1296
        }
×
1297
        return nil
1✔
1298
}
1299

1300
func (r *DataImportCronReconciler) deleteJobs(ctx context.Context, cron types.NamespacedName) error {
1✔
1301
        deleteOpts := client.DeleteOptions{PropagationPolicy: ptr.To[metav1.DeletionPropagation](metav1.DeletePropagationBackground)}
1✔
1302
        selector, err := getSelector(map[string]string{common.DataImportCronNsLabel: cron.Namespace, common.DataImportCronLabel: cron.Name})
1✔
1303
        if err != nil {
1✔
1304
                return err
×
UNCOV
1305
        }
×
1306
        opts := &client.DeleteAllOfOptions{ListOptions: client.ListOptions{Namespace: r.cdiNamespace, LabelSelector: selector}, DeleteOptions: deleteOpts}
1✔
1307
        if err := r.client.DeleteAllOf(ctx, &batchv1.CronJob{}, opts); err != nil {
1✔
1308
                return err
×
UNCOV
1309
        }
×
1310
        if err := r.client.DeleteAllOf(ctx, &batchv1.Job{}, opts); err != nil {
1✔
1311
                return err
×
UNCOV
1312
        }
×
1313

1314
        return nil
1✔
1315
}
1316

1317
// NewDataImportCronController creates a new instance of the DataImportCron controller
UNCOV
1318
func NewDataImportCronController(mgr manager.Manager, log logr.Logger, importerImage, pullPolicy string, installerLabels map[string]string) (controller.Controller, error) {
×
UNCOV
1319
        uncachedClient, err := client.New(mgr.GetConfig(), client.Options{
×
UNCOV
1320
                Scheme: mgr.GetScheme(),
×
UNCOV
1321
                Mapper: mgr.GetRESTMapper(),
×
UNCOV
1322
        })
×
UNCOV
1323
        if err != nil {
×
UNCOV
1324
                return nil, err
×
1325
        }
×
1326
        reconciler := &DataImportCronReconciler{
×
UNCOV
1327
                client:          mgr.GetClient(),
×
UNCOV
1328
                uncachedClient:  uncachedClient,
×
1329
                recorder:        mgr.GetEventRecorderFor(dataImportControllerName),
×
1330
                scheme:          mgr.GetScheme(),
×
UNCOV
1331
                log:             log.WithName(dataImportControllerName),
×
1332
                image:           importerImage,
×
1333
                pullPolicy:      pullPolicy,
×
UNCOV
1334
                cdiNamespace:    util.GetNamespace(),
×
UNCOV
1335
                installerLabels: installerLabels,
×
UNCOV
1336
        }
×
UNCOV
1337
        dataImportCronController, err := controller.New(dataImportControllerName, mgr, controller.Options{
×
UNCOV
1338
                MaxConcurrentReconciles: 3,
×
1339
                Reconciler:              reconciler,
×
1340
        })
×
1341
        if err != nil {
×
1342
                return nil, err
×
1343
        }
×
1344
        if err := addDataImportCronControllerWatches(mgr, dataImportCronController); err != nil {
×
1345
                return nil, err
×
1346
        }
×
1347
        log.Info("Initialized DataImportCron controller")
×
1348
        return dataImportCronController, nil
×
1349
}
1350

1351
func getCronName(obj client.Object) string {
×
1352
        return obj.GetLabels()[common.DataImportCronLabel]
×
1353
}
×
1354

1355
func getCronNs(obj client.Object) string {
×
1356
        return obj.GetLabels()[common.DataImportCronNsLabel]
×
1357
}
×
1358

1359
func mapSourceObjectToCron[T client.Object](_ context.Context, obj T) []reconcile.Request {
×
1360
        if cronName := getCronName(obj); cronName != "" {
×
1361
                return []reconcile.Request{{NamespacedName: types.NamespacedName{Name: cronName, Namespace: obj.GetNamespace()}}}
×
1362
        }
×
1363
        return nil
×
1364
}
1365

1366
func addDataImportCronControllerWatches(mgr manager.Manager, c controller.Controller) error {
×
1367
        if err := c.Watch(source.Kind(mgr.GetCache(), &cdiv1.DataImportCron{}, &handler.TypedEnqueueRequestForObject[*cdiv1.DataImportCron]{})); err != nil {
×
1368
                return err
×
1369
        }
×
1370

UNCOV
1371
        mapStorageProfileToCron := func(ctx context.Context, obj *cdiv1.StorageProfile) []reconcile.Request {
×
1372
                // TODO: Get rid of this after at least one version; use indexer on storage class annotation instead
×
1373
                // Otherwise we risk losing the storage profile event
×
1374
                var crons cdiv1.DataImportCronList
×
UNCOV
1375
                if err := mgr.GetClient().List(ctx, &crons); err != nil {
×
1376
                        c.GetLogger().Error(err, "Unable to list DataImportCrons")
×
1377
                        return nil
×
1378
                }
×
1379
                // Storage profiles are 1:1 to storage classes
1380
                scName := obj.GetName()
×
1381
                var reqs []reconcile.Request
×
1382
                for _, cron := range crons.Items {
×
1383
                        dataVolume := cron.Spec.Template
×
1384
                        explicitScName := cc.GetStorageClassFromDVSpec(&dataVolume)
×
UNCOV
1385
                        templateSc, err := cc.GetStorageClassByNameWithVirtFallback(ctx, mgr.GetClient(), explicitScName, dataVolume.Spec.ContentType)
×
UNCOV
1386
                        if err != nil || templateSc == nil {
×
1387
                                c.GetLogger().Error(err, "Unable to get storage class", "templateSc", templateSc)
×
1388
                                return reqs
×
1389
                        }
×
1390
                        if templateSc.Name == scName {
×
UNCOV
1391
                                reqs = append(reqs, reconcile.Request{NamespacedName: types.NamespacedName{Namespace: cron.Namespace, Name: cron.Name}})
×
1392
                        }
×
1393
                }
1394
                return reqs
×
1395
        }
1396

1397
        if err := c.Watch(source.Kind(mgr.GetCache(), &cdiv1.DataVolume{},
×
1398
                handler.TypedEnqueueRequestsFromMapFunc[*cdiv1.DataVolume](mapSourceObjectToCron),
×
1399
                predicate.TypedFuncs[*cdiv1.DataVolume]{
×
UNCOV
1400
                        CreateFunc: func(event.TypedCreateEvent[*cdiv1.DataVolume]) bool { return false },
×
1401
                        UpdateFunc: func(e event.TypedUpdateEvent[*cdiv1.DataVolume]) bool { return getCronName(e.ObjectNew) != "" },
×
1402
                        DeleteFunc: func(e event.TypedDeleteEvent[*cdiv1.DataVolume]) bool { return getCronName(e.Object) != "" },
×
1403
                },
1404
        )); err != nil {
×
1405
                return err
×
1406
        }
×
1407

1408
        if err := c.Watch(source.Kind(mgr.GetCache(), &cdiv1.DataSource{},
×
1409
                handler.TypedEnqueueRequestsFromMapFunc[*cdiv1.DataSource](mapSourceObjectToCron),
×
1410
                predicate.TypedFuncs[*cdiv1.DataSource]{
×
1411
                        CreateFunc: func(event.TypedCreateEvent[*cdiv1.DataSource]) bool { return false },
×
1412
                        UpdateFunc: func(e event.TypedUpdateEvent[*cdiv1.DataSource]) bool { return getCronName(e.ObjectNew) != "" },
×
1413
                        DeleteFunc: func(e event.TypedDeleteEvent[*cdiv1.DataSource]) bool { return getCronName(e.Object) != "" },
×
1414
                },
1415
        )); err != nil {
×
UNCOV
1416
                return err
×
UNCOV
1417
        }
×
1418

1419
        if err := c.Watch(source.Kind(mgr.GetCache(), &corev1.PersistentVolumeClaim{},
×
1420
                handler.TypedEnqueueRequestsFromMapFunc[*corev1.PersistentVolumeClaim](mapSourceObjectToCron),
×
1421
                predicate.TypedFuncs[*corev1.PersistentVolumeClaim]{
×
1422
                        CreateFunc: func(event.TypedCreateEvent[*corev1.PersistentVolumeClaim]) bool { return false },
×
1423
                        UpdateFunc: func(event.TypedUpdateEvent[*corev1.PersistentVolumeClaim]) bool { return false },
×
UNCOV
1424
                        DeleteFunc: func(e event.TypedDeleteEvent[*corev1.PersistentVolumeClaim]) bool { return getCronName(e.Object) != "" },
×
1425
                },
1426
        )); err != nil {
×
1427
                return err
×
UNCOV
1428
        }
×
1429

1430
        if err := addDefaultStorageClassUpdateWatch(mgr, c); err != nil {
×
1431
                return err
×
1432
        }
×
1433

1434
        if err := c.Watch(source.Kind(mgr.GetCache(), &cdiv1.StorageProfile{},
×
UNCOV
1435
                handler.TypedEnqueueRequestsFromMapFunc[*cdiv1.StorageProfile](mapStorageProfileToCron),
×
1436
                predicate.TypedFuncs[*cdiv1.StorageProfile]{
×
1437
                        CreateFunc: func(event.TypedCreateEvent[*cdiv1.StorageProfile]) bool { return true },
×
1438
                        DeleteFunc: func(event.TypedDeleteEvent[*cdiv1.StorageProfile]) bool { return false },
×
UNCOV
1439
                        UpdateFunc: func(e event.TypedUpdateEvent[*cdiv1.StorageProfile]) bool {
×
1440
                                return dicRelevantFieldsChanged(e.ObjectOld, e.ObjectNew)
×
1441
                        },
×
1442
                },
1443
        )); err != nil {
×
1444
                return err
×
1445
        }
×
1446

1447
        mapCronJobToCron := func(_ context.Context, obj *batchv1.CronJob) []reconcile.Request {
×
1448
                return []reconcile.Request{{NamespacedName: types.NamespacedName{Namespace: getCronNs(obj), Name: getCronName(obj)}}}
×
1449
        }
×
1450

1451
        if err := c.Watch(source.Kind(mgr.GetCache(), &batchv1.CronJob{},
×
1452
                handler.TypedEnqueueRequestsFromMapFunc[*batchv1.CronJob](mapCronJobToCron),
×
1453
                predicate.TypedFuncs[*batchv1.CronJob]{
×
UNCOV
1454
                        CreateFunc: func(e event.TypedCreateEvent[*batchv1.CronJob]) bool {
×
1455
                                return getCronName(e.Object) != "" && getCronNs(e.Object) != ""
×
1456
                        },
×
1457
                        DeleteFunc: func(event.TypedDeleteEvent[*batchv1.CronJob]) bool { return false },
×
1458
                        UpdateFunc: func(event.TypedUpdateEvent[*batchv1.CronJob]) bool { return false },
×
1459
                },
1460
        )); err != nil {
×
1461
                return err
×
1462
        }
×
1463

1464
        if err := mgr.GetClient().List(context.TODO(), &snapshotv1.VolumeSnapshotList{}); err != nil {
×
1465
                if meta.IsNoMatchError(err) {
×
1466
                        // Back out if there's no point to attempt watch
×
UNCOV
1467
                        return nil
×
1468
                }
×
1469
                if !cc.IsErrCacheNotStarted(err) {
×
1470
                        return err
×
UNCOV
1471
                }
×
1472
        }
1473
        if err := c.Watch(source.Kind(mgr.GetCache(), &snapshotv1.VolumeSnapshot{},
×
1474
                handler.TypedEnqueueRequestsFromMapFunc[*snapshotv1.VolumeSnapshot](mapSourceObjectToCron),
×
1475
                predicate.TypedFuncs[*snapshotv1.VolumeSnapshot]{
×
1476
                        CreateFunc: func(event.TypedCreateEvent[*snapshotv1.VolumeSnapshot]) bool { return false },
×
1477
                        UpdateFunc: func(event.TypedUpdateEvent[*snapshotv1.VolumeSnapshot]) bool { return false },
×
1478
                        DeleteFunc: func(e event.TypedDeleteEvent[*snapshotv1.VolumeSnapshot]) bool { return getCronName(e.Object) != "" },
×
1479
                },
UNCOV
1480
        )); err != nil {
×
1481
                return err
×
1482
        }
×
1483

UNCOV
1484
        return nil
×
1485
}
1486

1487
func dicRelevantFieldsChanged(oldSp, newSp *cdiv1.StorageProfile) bool {
×
1488
        sourceFormatChanged := oldSp.Status.DataImportCronSourceFormat != newSp.Status.DataImportCronSourceFormat
×
1489
        rwoAnnotationChanged := oldSp.Annotations[cc.AnnUseReadWriteOnceForDataImportCron] != newSp.Annotations[cc.AnnUseReadWriteOnceForDataImportCron]
×
1490
        snapshotClassChanged := oldSp.Annotations[cc.AnnSnapshotClassForDataImportCron] != newSp.Annotations[cc.AnnSnapshotClassForDataImportCron]
×
1491
        return sourceFormatChanged || rwoAnnotationChanged || snapshotClassChanged
×
1492
}
×
1493

1494
// addDefaultStorageClassUpdateWatch watches for default/virt default storage class updates
1495
func addDefaultStorageClassUpdateWatch(mgr manager.Manager, c controller.Controller) error {
×
1496
        if err := c.Watch(source.Kind(mgr.GetCache(), &storagev1.StorageClass{},
×
1497
                handler.TypedEnqueueRequestsFromMapFunc[*storagev1.StorageClass](
×
1498
                        func(ctx context.Context, obj *storagev1.StorageClass) []reconcile.Request {
×
1499
                                log := c.GetLogger().WithName("DefaultStorageClassUpdateWatch")
×
UNCOV
1500
                                log.Info("Update", "sc", obj.GetName(),
×
1501
                                        "default", obj.GetAnnotations()[cc.AnnDefaultStorageClass] == "true",
×
1502
                                        "defaultVirt", obj.GetAnnotations()[cc.AnnDefaultVirtStorageClass] == "true")
×
1503
                                reqs, err := getReconcileRequestsForDicsWithoutExplicitStorageClass(ctx, mgr.GetClient())
×
UNCOV
1504
                                if err != nil {
×
1505
                                        log.Error(err, "Failed getting DataImportCrons with pending PVCs")
×
UNCOV
1506
                                }
×
UNCOV
1507
                                return reqs
×
1508
                        },
1509
                ),
1510
                predicate.TypedFuncs[*storagev1.StorageClass]{
1511
                        CreateFunc: func(event.TypedCreateEvent[*storagev1.StorageClass]) bool { return false },
×
1512
                        DeleteFunc: func(event.TypedDeleteEvent[*storagev1.StorageClass]) bool { return false },
×
1513
                        UpdateFunc: func(e event.TypedUpdateEvent[*storagev1.StorageClass]) bool {
×
UNCOV
1514
                                return (e.ObjectNew.Annotations[cc.AnnDefaultStorageClass] != e.ObjectOld.Annotations[cc.AnnDefaultStorageClass]) ||
×
UNCOV
1515
                                        (e.ObjectNew.Annotations[cc.AnnDefaultVirtStorageClass] != e.ObjectOld.Annotations[cc.AnnDefaultVirtStorageClass])
×
1516
                        },
×
1517
                },
1518
        )); err != nil {
×
1519
                return err
×
1520
        }
×
1521

1522
        return nil
×
1523
}
1524

1525
func getReconcileRequestsForDicsWithoutExplicitStorageClass(ctx context.Context, c client.Client) ([]reconcile.Request, error) {
×
1526
        dicList := &cdiv1.DataImportCronList{}
×
1527
        if err := c.List(ctx, dicList); err != nil {
×
1528
                return nil, err
×
UNCOV
1529
        }
×
UNCOV
1530
        reqs := []reconcile.Request{}
×
UNCOV
1531
        for _, dic := range dicList.Items {
×
1532
                if cc.GetStorageClassFromDVSpec(&dic.Spec.Template) != nil {
×
1533
                        continue
×
1534
                }
1535

1536
                reqs = append(reqs, reconcile.Request{NamespacedName: types.NamespacedName{Name: dic.Name, Namespace: dic.Namespace}})
×
1537
        }
1538

1539
        return reqs, nil
×
1540
}
1541

1542
func (r *DataImportCronReconciler) deleteOutdatedPendingPvc(ctx context.Context, pvc *corev1.PersistentVolumeClaim, desiredStorageClass, cronName string) (bool, error) {
1✔
1543
        if pvc == nil || pvc.Status.Phase != corev1.ClaimPending || pvc.Labels[common.DataImportCronLabel] != cronName {
2✔
1544
                return false, nil
1✔
1545
        }
1✔
1546

1547
        sc := pvc.Spec.StorageClassName
1✔
1548
        if sc == nil || *sc == desiredStorageClass {
2✔
1549
                return false, nil
1✔
1550
        }
1✔
1551

1552
        r.log.Info("Delete pending pvc", "name", pvc.Name, "ns", pvc.Namespace, "sc", *sc)
1✔
1553
        if err := r.client.Delete(ctx, pvc); cc.IgnoreNotFound(err) != nil {
1✔
1554
                return false, err
×
UNCOV
1555
        }
×
1556

1557
        return true, nil
1✔
1558
}
1559

1560
func (r *DataImportCronReconciler) cronJobExistsAndUpdated(ctx context.Context, cron *cdiv1.DataImportCron) (bool, error) {
1✔
1561
        cronJob := &batchv1.CronJob{}
1✔
1562
        cronJobKey := types.NamespacedName{Namespace: r.cdiNamespace, Name: GetCronJobName(cron)}
1✔
1563
        if err := r.client.Get(ctx, cronJobKey, cronJob); err != nil {
2✔
1564
                return false, cc.IgnoreNotFound(err)
1✔
1565
        }
1✔
1566

1567
        cronJobCopy := cronJob.DeepCopy()
1✔
1568
        if err := r.initCronJob(cron, cronJobCopy); err != nil {
1✔
UNCOV
1569
                return false, err
×
UNCOV
1570
        }
×
1571

1572
        if !reflect.DeepEqual(cronJob, cronJobCopy) {
2✔
1573
                r.log.Info("Updating CronJob", "name", cronJob.GetName())
1✔
1574
                if err := r.client.Update(ctx, cronJobCopy); err != nil {
1✔
1575
                        return false, cc.IgnoreNotFound(err)
×
1576
                }
×
1577
        }
1578
        return true, nil
1✔
1579
}
1580

1581
func (r *DataImportCronReconciler) newCronJob(cron *cdiv1.DataImportCron) (*batchv1.CronJob, error) {
1✔
1582
        cronJob := &batchv1.CronJob{
1✔
1583
                ObjectMeta: metav1.ObjectMeta{
1✔
1584
                        Name:      GetCronJobName(cron),
1✔
1585
                        Namespace: r.cdiNamespace,
1✔
1586
                },
1✔
1587
        }
1✔
1588
        if err := r.initCronJob(cron, cronJob); err != nil {
1✔
UNCOV
1589
                return nil, err
×
1590
        }
×
1591
        return cronJob, nil
1✔
1592
}
1593

1594
// InitPollerPod inits poller Pod
1595
func InitPollerPod(c client.Client, cron *cdiv1.DataImportCron, pod *corev1.PodTemplateSpec, image string, pullPolicy corev1.PullPolicy, log logr.Logger) error {
1✔
1596
        regSource, err := getCronRegistrySource(cron)
1✔
1597
        if err != nil {
1✔
UNCOV
1598
                return err
×
UNCOV
1599
        }
×
1600
        if regSource.URL == nil {
1✔
UNCOV
1601
                return errors.Errorf("No URL source in cron %s", cron.Name)
×
UNCOV
1602
        }
×
1603
        cdiConfig := &cdiv1.CDIConfig{}
1✔
1604
        if err := c.Get(context.TODO(), types.NamespacedName{Name: common.ConfigName}, cdiConfig); err != nil {
1✔
UNCOV
1605
                return err
×
UNCOV
1606
        }
×
1607
        insecureTLS, err := IsInsecureTLS(*regSource.URL, cdiConfig, log)
1✔
1608
        if err != nil {
1✔
UNCOV
1609
                return err
×
1610
        }
×
1611
        container := corev1.Container{
1✔
1612
                Name:  "cdi-source-update-poller",
1✔
1613
                Image: image,
1✔
1614
                Command: []string{
1✔
1615
                        "/usr/bin/cdi-source-update-poller",
1✔
1616
                        "-ns", cron.Namespace,
1✔
1617
                        "-cron", cron.Name,
1✔
1618
                        "-url", *regSource.URL,
1✔
1619
                },
1✔
1620
                ImagePullPolicy:          pullPolicy,
1✔
1621
                TerminationMessagePath:   corev1.TerminationMessagePathDefault,
1✔
1622
                TerminationMessagePolicy: corev1.TerminationMessageFallbackToLogsOnError,
1✔
1623
        }
1✔
1624

1✔
1625
        var volumes []corev1.Volume
1✔
1626
        hasCertConfigMap := regSource.CertConfigMap != nil && *regSource.CertConfigMap != ""
1✔
1627
        if hasCertConfigMap {
1✔
UNCOV
1628
                vm := corev1.VolumeMount{
×
UNCOV
1629
                        Name:      CertVolName,
×
1630
                        MountPath: common.ImporterCertDir,
×
1631
                }
×
UNCOV
1632
                container.VolumeMounts = append(container.VolumeMounts, vm)
×
UNCOV
1633
                container.Command = append(container.Command, "-certdir", common.ImporterCertDir)
×
UNCOV
1634
                volumes = append(volumes, createConfigMapVolume(CertVolName, *regSource.CertConfigMap))
×
UNCOV
1635
        }
×
1636

1637
        if volName, _ := GetImportProxyConfig(cdiConfig, common.ImportProxyConfigMapName); volName != "" {
2✔
1638
                vm := corev1.VolumeMount{
1✔
1639
                        Name:      ProxyCertVolName,
1✔
1640
                        MountPath: common.ImporterProxyCertDir,
1✔
1641
                }
1✔
1642
                container.VolumeMounts = append(container.VolumeMounts, vm)
1✔
1643
                volumes = append(volumes, createConfigMapVolume(ProxyCertVolName, volName))
1✔
1644
        }
1✔
1645

1646
        if regSource.SecretRef != nil && *regSource.SecretRef != "" {
1✔
UNCOV
1647
                container.Env = append(container.Env,
×
UNCOV
1648
                        corev1.EnvVar{
×
1649
                                Name: common.ImporterAccessKeyID,
×
1650
                                ValueFrom: &corev1.EnvVarSource{
×
1651
                                        SecretKeyRef: &corev1.SecretKeySelector{
×
1652
                                                LocalObjectReference: corev1.LocalObjectReference{
×
1653
                                                        Name: *regSource.SecretRef,
×
1654
                                                },
×
1655
                                                Key: common.KeyAccess,
×
1656
                                        },
×
UNCOV
1657
                                },
×
UNCOV
1658
                        },
×
UNCOV
1659
                        corev1.EnvVar{
×
UNCOV
1660
                                Name: common.ImporterSecretKey,
×
UNCOV
1661
                                ValueFrom: &corev1.EnvVarSource{
×
UNCOV
1662
                                        SecretKeyRef: &corev1.SecretKeySelector{
×
UNCOV
1663
                                                LocalObjectReference: corev1.LocalObjectReference{
×
UNCOV
1664
                                                        Name: *regSource.SecretRef,
×
UNCOV
1665
                                                },
×
UNCOV
1666
                                                Key: common.KeySecret,
×
UNCOV
1667
                                        },
×
1668
                                },
×
1669
                        },
×
1670
                )
×
1671
        }
×
1672

1673
        addEnvVar := func(varName, value string) {
2✔
1674
                container.Env = append(container.Env, corev1.EnvVar{Name: varName, Value: value})
1✔
1675
        }
1✔
1676

1677
        if insecureTLS {
1✔
1678
                addEnvVar(common.InsecureTLSVar, "true")
×
1679
        }
×
1680

1681
        addEnvVarFromImportProxyConfig := func(varName string) {
2✔
1682
                if value, err := GetImportProxyConfig(cdiConfig, varName); err == nil {
2✔
1683
                        addEnvVar(varName, value)
1✔
1684
                }
1✔
1685
        }
1686

1687
        addEnvVarFromImportProxyConfig(common.ImportProxyHTTP)
1✔
1688
        addEnvVarFromImportProxyConfig(common.ImportProxyHTTPS)
1✔
1689
        addEnvVarFromImportProxyConfig(common.ImportProxyNoProxy)
1✔
1690

1✔
1691
        imagePullSecrets, err := cc.GetImagePullSecrets(c)
1✔
1692
        if err != nil {
1✔
UNCOV
1693
                return err
×
UNCOV
1694
        }
×
1695
        workloadNodePlacement, err := cc.GetWorkloadNodePlacement(context.TODO(), c)
1✔
1696
        if err != nil {
1✔
UNCOV
1697
                return err
×
UNCOV
1698
        }
×
1699

1700
        podSpec := &pod.Spec
1✔
1701

1✔
1702
        podSpec.RestartPolicy = corev1.RestartPolicyNever
1✔
1703
        podSpec.TerminationGracePeriodSeconds = ptr.To[int64](0)
1✔
1704
        podSpec.Containers = []corev1.Container{container}
1✔
1705
        podSpec.ServiceAccountName = common.CronJobServiceAccountName
1✔
1706
        podSpec.Volumes = volumes
1✔
1707
        podSpec.ImagePullSecrets = imagePullSecrets
1✔
1708
        podSpec.NodeSelector = workloadNodePlacement.NodeSelector
1✔
1709
        podSpec.Tolerations = workloadNodePlacement.Tolerations
1✔
1710
        podSpec.Affinity = workloadNodePlacement.Affinity
1✔
1711

1✔
1712
        cc.SetRestrictedSecurityContext(podSpec)
1✔
1713
        // No need for specifid uid/fsgroup here since this doesn't write or use qemu
1✔
1714
        if podSpec.SecurityContext != nil {
2✔
1715
                podSpec.SecurityContext.FSGroup = nil
1✔
1716
        }
1✔
1717
        if podSpec.Containers[0].SecurityContext != nil {
2✔
1718
                podSpec.Containers[0].SecurityContext.RunAsUser = nil
1✔
1719
        }
1✔
1720

1721
        if pod.Labels == nil {
2✔
1722
                pod.Labels = map[string]string{}
1✔
1723
        }
1✔
1724
        pod.Labels[common.DataImportCronPollerLabel] = ""
1✔
1725

1✔
1726
        return nil
1✔
1727
}
1728

1729
func (r *DataImportCronReconciler) initCronJob(cron *cdiv1.DataImportCron, cronJob *batchv1.CronJob) error {
1✔
1730
        cronJobSpec := &cronJob.Spec
1✔
1731
        cronJobSpec.Schedule = cron.Spec.Schedule
1✔
1732
        cronJobSpec.ConcurrencyPolicy = batchv1.ForbidConcurrent
1✔
1733
        cronJobSpec.SuccessfulJobsHistoryLimit = ptr.To[int32](1)
1✔
1734
        cronJobSpec.FailedJobsHistoryLimit = ptr.To[int32](1)
1✔
1735

1✔
1736
        jobSpec := &cronJobSpec.JobTemplate.Spec
1✔
1737
        jobSpec.BackoffLimit = ptr.To[int32](2)
1✔
1738
        jobSpec.TTLSecondsAfterFinished = ptr.To[int32](10)
1✔
1739
        cc.AddAnnotation(&jobSpec.Template, secv1.RequiredSCCAnnotation, common.RestrictedSCCName)
1✔
1740

1✔
1741
        pod := &jobSpec.Template
1✔
1742
        if err := InitPollerPod(r.client, cron, pod, r.image, corev1.PullPolicy(r.pullPolicy), r.log); err != nil {
1✔
UNCOV
1743
                return err
×
UNCOV
1744
        }
×
1745
        if err := r.setJobCommon(cron, cronJob); err != nil {
1✔
UNCOV
1746
                return err
×
UNCOV
1747
        }
×
1748
        return nil
1✔
1749
}
1750

1751
func (r *DataImportCronReconciler) newInitialJob(cron *cdiv1.DataImportCron, cronJob *batchv1.CronJob) (*batchv1.Job, error) {
1✔
1752
        job := &batchv1.Job{
1✔
1753
                ObjectMeta: metav1.ObjectMeta{
1✔
1754
                        Name:      GetInitialJobName(cron),
1✔
1755
                        Namespace: cronJob.Namespace,
1✔
1756
                },
1✔
1757
                Spec: cronJob.Spec.JobTemplate.Spec,
1✔
1758
        }
1✔
1759
        if err := r.setJobCommon(cron, job); err != nil {
1✔
UNCOV
1760
                return nil, err
×
UNCOV
1761
        }
×
1762
        return job, nil
1✔
1763
}
1764

1765
func (r *DataImportCronReconciler) setJobCommon(cron *cdiv1.DataImportCron, obj metav1.Object) error {
1✔
1766
        if err := operator.SetOwnerRuntime(r.uncachedClient, obj); err != nil {
1✔
1767
                return err
×
1768
        }
×
1769
        util.SetRecommendedLabels(obj, r.installerLabels, common.CDIControllerName)
1✔
1770
        labels := obj.GetLabels()
1✔
1771
        labels[common.DataImportCronNsLabel] = cron.Namespace
1✔
1772
        labels[common.DataImportCronLabel] = cron.Name
1✔
1773
        obj.SetLabels(labels)
1✔
1774
        return nil
1✔
1775
}
1776

1777
func (r *DataImportCronReconciler) newSourceDataVolume(cron *cdiv1.DataImportCron, dataVolumeName string, storageProfile *cdiv1.StorageProfile) *cdiv1.DataVolume {
1✔
1778
        dv := cron.Spec.Template.DeepCopy()
1✔
1779
        if isCronRegistrySource(cron) {
2✔
1780
                var digestedURL string
1✔
1781
                if isURLSource(cron) {
2✔
1782
                        digestedURL = untagDigestedDockerURL(*dv.Spec.Source.Registry.URL + "@" + cron.Annotations[AnnSourceDesiredDigest])
1✔
1783
                } else if isImageStreamSource(cron) {
3✔
1784
                        // No way to import image stream by name when we want specific digest, so we use its docker reference
1✔
1785
                        digestedURL = "docker://" + cron.Annotations[AnnImageStreamDockerRef]
1✔
1786
                        dv.Spec.Source.Registry.ImageStream = nil
1✔
1787
                }
1✔
1788
                dv.Spec.Source.Registry.URL = &digestedURL
1✔
1789
        }
1790
        dv.Name = dataVolumeName
1✔
1791
        dv.Namespace = cron.Namespace
1✔
1792
        r.setDataImportCronResourceLabels(cron, dv)
1✔
1793
        cc.AddAnnotation(dv, cc.AnnImmediateBinding, "true")
1✔
1794
        cc.AddAnnotation(dv, AnnLastUseTime, time.Now().UTC().Format(time.RFC3339Nano))
1✔
1795
        passCronAnnotationToDv(cron, dv, cc.AnnPodRetainAfterCompletion)
1✔
1796

1✔
1797
        for _, defaultInstanceTypeLabel := range cc.DefaultInstanceTypeLabels {
2✔
1798
                passCronLabelToDv(cron, dv, defaultInstanceTypeLabel)
1✔
1799
        }
1✔
1800

1801
        passCronLabelToDv(cron, dv, cc.LabelDynamicCredentialSupport)
1✔
1802

1✔
1803
        // Apply RWO access mode as default for DataImportCron (from StorageProfile annotation)
1✔
1804
        // Only applies if the DV doesn't already have AccessModes configured
1✔
1805
        if storageProfile != nil && storageProfile.Annotations[cc.AnnUseReadWriteOnceForDataImportCron] == "true" {
2✔
1806
                if dv.Spec.Storage != nil && len(dv.Spec.Storage.AccessModes) == 0 {
2✔
1807
                        dv.Spec.Storage.AccessModes = []corev1.PersistentVolumeAccessMode{corev1.ReadWriteOnce}
1✔
1808
                }
1✔
1809
        }
1810

1811
        return dv
1✔
1812
}
1813

1814
func (r *DataImportCronReconciler) setDataImportCronResourceLabels(cron *cdiv1.DataImportCron, obj metav1.Object) {
1✔
1815
        util.SetRecommendedLabels(obj, r.installerLabels, common.CDIControllerName)
1✔
1816
        cc.CopyAllowedLabels(cron.GetLabels(), obj, true)
1✔
1817
        labels := obj.GetLabels()
1✔
1818
        labels[common.DataImportCronLabel] = cron.Name
1✔
1819
        if cron.Spec.RetentionPolicy != nil && *cron.Spec.RetentionPolicy == cdiv1.DataImportCronRetainNone {
2✔
1820
                labels[common.DataImportCronCleanupLabel] = "true"
1✔
1821
        }
1✔
1822
        obj.SetLabels(labels)
1✔
1823
}
1824

1825
func untagDigestedDockerURL(dockerURL string) string {
1✔
1826
        if u, err := url.Parse(dockerURL); err == nil {
2✔
1827
                url := u.Host + u.Path
1✔
1828
                subs := reference.ReferenceRegexp.FindStringSubmatch(url)
1✔
1829
                // Check for tag
1✔
1830
                if len(subs) > 2 && len(subs[2]) > 0 {
2✔
1831
                        if untaggedRef, err := reference.ParseDockerRef(url); err == nil {
2✔
1832
                                return u.Scheme + "://" + untaggedRef.String()
1✔
1833
                        }
1✔
1834
                }
1835
        }
1836
        return dockerURL
1✔
1837
}
1838

1839
func passCronLabelToDv(cron *cdiv1.DataImportCron, dv *cdiv1.DataVolume, ann string) {
1✔
1840
        if val := cron.Labels[ann]; val != "" {
2✔
1841
                cc.AddLabel(dv, ann, val)
1✔
1842
        }
1✔
1843
}
1844

1845
func passCronAnnotationToDv(cron *cdiv1.DataImportCron, dv *cdiv1.DataVolume, ann string) {
1✔
1846
        if val := cron.Annotations[ann]; val != "" {
1✔
UNCOV
1847
                cc.AddAnnotation(dv, ann, val)
×
UNCOV
1848
        }
×
1849
}
1850

1851
func (r *DataImportCronReconciler) newDataSource(cron *cdiv1.DataImportCron) *cdiv1.DataSource {
1✔
1852
        dataSource := &cdiv1.DataSource{
1✔
1853
                ObjectMeta: metav1.ObjectMeta{
1✔
1854
                        Name:      cron.Spec.ManagedDataSource,
1✔
1855
                        Namespace: cron.Namespace,
1✔
1856
                },
1✔
1857
        }
1✔
1858
        util.SetRecommendedLabels(dataSource, r.installerLabels, common.CDIControllerName)
1✔
1859
        dataSource.Labels[common.DataImportCronLabel] = cron.Name
1✔
1860
        cc.CopyAllowedLabels(cron.GetLabels(), dataSource, true)
1✔
1861
        return dataSource
1✔
1862
}
1✔
1863

1864
// Create DataVolume name based on the DataSource name + prefix of the digest
1865
func createDvName(prefix, digest string) (string, error) {
1✔
1866
        digestPrefix := ""
1✔
1867
        if strings.HasPrefix(digest, digestSha256Prefix) {
2✔
1868
                digestPrefix = digestSha256Prefix
1✔
1869
        } else if strings.HasPrefix(digest, digestUIDPrefix) {
3✔
1870
                digestPrefix = digestUIDPrefix
1✔
1871
        } else {
2✔
1872
                return "", errors.Errorf("Digest has no supported prefix")
1✔
1873
        }
1✔
1874
        fromIdx := len(digestPrefix)
1✔
1875
        toIdx := fromIdx + digestDvNameSuffixLength
1✔
1876
        if len(digest) < toIdx {
2✔
1877
                return "", errors.Errorf("Digest is too short")
1✔
1878
        }
1✔
1879
        return naming.GetResourceName(prefix, digest[fromIdx:toIdx]), nil
1✔
1880
}
1881

1882
// GetCronJobName get CronJob name based on cron name and UID
1883
func GetCronJobName(cron *cdiv1.DataImportCron) string {
1✔
1884
        return naming.GetResourceName(cron.Name, string(cron.UID)[:cronJobUIDSuffixLength])
1✔
1885
}
1✔
1886

1887
// GetInitialJobName get initial job name based on cron name and UID
1888
func GetInitialJobName(cron *cdiv1.DataImportCron) string {
1✔
1889
        return naming.GetResourceName("initial-job", GetCronJobName(cron))
1✔
1890
}
1✔
1891

1892
func getPollerPodName(cron *cdiv1.DataImportCron) string {
1✔
1893
        return naming.GetResourceName("poller-"+cron.Name, string(cron.UID)[:8])
1✔
1894
}
1✔
1895

1896
func getSelector(matchLabels map[string]string) (labels.Selector, error) {
1✔
1897
        return metav1.LabelSelectorAsSelector(&metav1.LabelSelector{MatchLabels: matchLabels})
1✔
1898
}
1✔
1899

1900
func inferVolumeModeForSnapshot(ctx context.Context, client client.Client, cron *cdiv1.DataImportCron) (*corev1.PersistentVolumeMode, error) {
1✔
1901
        dv := &cron.Spec.Template
1✔
1902

1✔
1903
        if explicitVolumeMode := getVolumeModeFromDVSpec(dv); explicitVolumeMode != nil {
1✔
UNCOV
1904
                return explicitVolumeMode, nil
×
UNCOV
1905
        }
×
1906

1907
        accessModes := getAccessModesFromDVSpec(dv)
1✔
1908
        inferredPvc := &corev1.PersistentVolumeClaim{
1✔
1909
                Spec: corev1.PersistentVolumeClaimSpec{
1✔
1910
                        StorageClassName: cc.GetStorageClassFromDVSpec(dv),
1✔
1911
                        AccessModes:      accessModes,
1✔
1912
                        VolumeMode:       ptr.To(cdiv1.PersistentVolumeFromStorageProfile),
1✔
1913
                        Resources: corev1.VolumeResourceRequirements{
1✔
1914
                                Requests: corev1.ResourceList{
1✔
1915
                                        // Doesn't matter
1✔
1916
                                        corev1.ResourceStorage: resource.MustParse("1Gi"),
1✔
1917
                                },
1✔
1918
                        },
1✔
1919
                },
1✔
1920
        }
1✔
1921
        if err := dvc.RenderPvc(ctx, client, inferredPvc); err != nil {
1✔
UNCOV
1922
                return nil, err
×
UNCOV
1923
        }
×
1924

1925
        return inferredPvc.Spec.VolumeMode, nil
1✔
1926
}
1927

1928
// getVolumeModeFromDVSpec returns the volume mode from DataVolume PVC or Storage spec
1929
func getVolumeModeFromDVSpec(dv *cdiv1.DataVolume) *corev1.PersistentVolumeMode {
1✔
1930
        if dv.Spec.PVC != nil {
1✔
UNCOV
1931
                return dv.Spec.PVC.VolumeMode
×
UNCOV
1932
        }
×
1933

1934
        if dv.Spec.Storage != nil {
2✔
1935
                return dv.Spec.Storage.VolumeMode
1✔
1936
        }
1✔
1937

UNCOV
1938
        return nil
×
1939
}
1940

1941
// getAccessModesFromDVSpec returns the access modes from DataVolume PVC or Storage spec
1942
func getAccessModesFromDVSpec(dv *cdiv1.DataVolume) []corev1.PersistentVolumeAccessMode {
1✔
1943
        if dv.Spec.PVC != nil {
1✔
1944
                return dv.Spec.PVC.AccessModes
×
UNCOV
1945
        }
×
1946

1947
        if dv.Spec.Storage != nil {
2✔
1948
                return dv.Spec.Storage.AccessModes
1✔
1949
        }
1✔
1950

UNCOV
1951
        return nil
×
1952
}
1953

1954
func inferAdvisedRestoreSizeForSnapshot(dv *cdiv1.DataVolume, snapshot *snapshotv1.VolumeSnapshot, fallback *resource.Quantity) *resource.Quantity {
1✔
1955
        var dvSize resource.Quantity
1✔
1956

1✔
1957
        if dv.Spec.PVC != nil {
1✔
UNCOV
1958
                dvSize = dv.Spec.PVC.Resources.Requests[corev1.ResourceStorage]
×
1959
        }
×
1960

1961
        if dv.Spec.Storage != nil {
2✔
1962
                dvSize = dv.Spec.Storage.Resources.Requests[corev1.ResourceStorage]
1✔
1963
        }
1✔
1964

1965
        if dvSize.IsZero() && fallback != nil {
2✔
1966
                return fallback
1✔
1967
        }
1✔
1968

1969
        if snapshot.Status != nil {
2✔
1970
                if rs := snapshot.Status.RestoreSize; rs != nil && dvSize.Cmp(*rs) < 0 {
2✔
1971
                        return snapshot.Status.RestoreSize
1✔
1972
                }
1✔
1973
        }
1974

1975
        return &dvSize
1✔
1976
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc