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

kubevirt / containerized-data-importer / #5999

12 May 2026 10:29AM UTC coverage: 49.6% (-0.01%) from 49.614%
#5999

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

29 of 33 new or added lines in 1 file covered. (87.88%)

13 existing lines in 2 files now uncovered.

14995 of 30232 relevant lines covered (49.6%)

0.56 hits per line

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

66.15
/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
                outdated, err := r.importOutdated(ctx, dataImportCron, dv, desiredStorageClass)
1✔
367
                if err != nil {
1✔
NEW
368
                        return res, err
×
NEW
369
                }
×
370
                if outdated {
2✔
371
                        if err := r.resetImport(ctx, dataImportCron, desiredSc); err != nil {
1✔
372
                                return res, err
×
373
                        }
×
374
                        return reconcile.Result{RequeueAfter: time.Second}, nil
1✔
375
                }
376
                cc.AddAnnotation(dataImportCron, AnnStorageClass, desiredSc)
1✔
377
        }
378
        format, err := r.getSourceFormat(ctx, desiredStorageClass)
1✔
379
        if err != nil {
1✔
380
                return res, err
×
381
        }
×
382
        snapshot, err := r.getSnapshot(ctx, dataImportCron)
1✔
383
        if err != nil {
1✔
384
                return res, err
×
385
        }
×
386

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

398
                return nil
1✔
399
        }
400

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

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

486
        if err := r.updateDataSource(ctx, dataImportCron, format); err != nil {
1✔
487
                return res, err
×
488
        }
×
489

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

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

524
        if err := updateLastExecutionTimestamp(dataImportCron); err != nil {
1✔
525
                return res, err
×
526
        }
×
527

528
        if !reflect.DeepEqual(dataImportCron, dataImportCronCopy) {
2✔
529
                if err := r.client.Update(ctx, dataImportCron); err != nil {
1✔
530
                        return res, err
×
531
                }
×
532
        }
533
        return res, nil
1✔
534
}
535

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

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

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

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

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

578
        return snapshot, nil
1✔
579
}
580

581
func (r *DataImportCronReconciler) getDataSource(ctx context.Context, dataImportCron *cdiv1.DataImportCron) (*cdiv1.DataSource, error) {
1✔
582
        dataSourceName := dataImportCron.Spec.ManagedDataSource
1✔
583
        dataSource := &cdiv1.DataSource{}
1✔
584
        if err := r.client.Get(ctx, types.NamespacedName{Namespace: dataImportCron.Namespace, Name: dataSourceName}, dataSource); err != nil {
2✔
585
                return nil, err
1✔
586
        }
1✔
587
        if dataSource.Labels[common.DataImportCronLabel] != dataImportCron.Name {
1✔
588
                log := r.log.WithName("getCronManagedDataSource")
×
589
                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)
×
590
                return nil, ErrNotManagedByCron
×
591
        }
×
592
        return dataSource, nil
1✔
593
}
594

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

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

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

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

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

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

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

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

1✔
741
        cc.SetRestrictedSecurityContext(&pod.Spec)
1✔
742
        if pod.Spec.SecurityContext != nil {
2✔
743
                pod.Spec.SecurityContext.FSGroup = nil
1✔
744
        }
1✔
745

746
        return false, r.client.Create(ctx, pod)
1✔
747
}
748

749
func fetchContainerImageDigest(pod *corev1.Pod) (string, error) {
1✔
750
        if len(pod.Status.ContainerStatuses) == 0 {
1✔
751
                return "", nil
×
752
        }
×
753

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

764
        if status.State.Terminated == nil {
1✔
765
                return "", nil
×
766
        }
×
767

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

777
        return imageID[idx:], nil
1✔
778
}
779

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

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

1✔
822
        sourcePVC := dataImportCron.Status.LastImportedPVC
1✔
823
        populateDataSource(format, dataSource, sourcePVC)
1✔
824

1✔
825
        if !reflect.DeepEqual(dataSource, dataSourceCopy) {
2✔
826
                if err := r.client.Update(ctx, dataSource); err != nil {
1✔
827
                        return err
×
828
                }
×
829
        }
830

831
        return nil
1✔
832
}
833

834
func populateDataSource(format cdiv1.DataImportCronSourceFormat, dataSource *cdiv1.DataSource, sourcePVC *cdiv1.DataVolumeSourcePVC) {
1✔
835
        if sourcePVC == nil {
2✔
836
                return
1✔
837
        }
1✔
838

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

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

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

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

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

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

1✔
931
        return nil
1✔
932
}
933

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

949
        return true, nil
1✔
950
}
951

952
type authProxy struct {
953
        client client.Client
954
}
955

956
func (p *authProxy) CreateSar(sar *authorizationv1.SubjectAccessReview) (*authorizationv1.SubjectAccessReview, error) {
1✔
957
        if err := p.client.Create(context.TODO(), sar); err != nil {
1✔
958
                return nil, err
×
959
        }
×
960
        return sar, nil
1✔
961
}
962

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

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

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

1008
        return nil
1✔
1009
}
1010

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

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

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

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

1088
        return nil
1✔
1089
}
1090

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

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

1109
        r.log.Info("Snapshot class changed, deleting", "name", snapshot.Name, "from", actualVSC, "to", desiredVSC)
×
1110
        if err := r.client.Delete(ctx, snapshot); err != nil {
×
1111
                return false, client.IgnoreNotFound(err)
×
1112
        }
×
1113
        return true, nil
×
1114
}
1115

1116
func (r *DataImportCronReconciler) importOutdated(ctx context.Context, cron *cdiv1.DataImportCron, currentDV *cdiv1.DataVolume, desiredSC *storagev1.StorageClass) (bool, error) {
1✔
1117
        currentSc, hasCurrent := cron.Annotations[AnnStorageClass]
1✔
1118
        if hasCurrent && currentSc != desiredSC.Name {
2✔
1119
                r.log.Info("Storage class changed", "currentSc", currentSc, "desiredSc", desiredSC.Name)
1✔
1120
                return true, nil
1✔
1121
        }
1✔
1122
        if !hasCurrent && len(cron.Status.CurrentImports) > 0 {
2✔
1123
                r.log.Info("First default storage class set, existing import needs recreate")
1✔
1124
                return true, nil
1✔
1125
        }
1✔
1126
        if currentDV == nil {
2✔
1127
                return false, nil
1✔
1128
        }
1✔
1129
        sp := &cdiv1.StorageProfile{}
1✔
1130
        if err := r.client.Get(ctx, types.NamespacedName{Name: desiredSC.Name}, sp); err != nil {
1✔
NEW
1131
                return false, err
×
NEW
1132
        }
×
1133
        desiredDV := r.newSourceDataVolume(cron, currentDV.Name, sp)
1✔
1134
        if !reflect.DeepEqual(currentDV.Spec, desiredDV.Spec) {
2✔
1135
                r.log.Info("DV spec mismatch, needs recreate", "dv", currentDV.Name)
1✔
1136
                return true, nil
1✔
1137
        }
1✔
1138
        return false, nil
1✔
1139
}
1140

1141
// getSnapshotClassForDataImportCron returns the VolumeSnapshotClass name to use for DataImportCron snapshots.
1142
func (r *DataImportCronReconciler) getSnapshotClassForDataImportCron(pvc *corev1.PersistentVolumeClaim, storageProfile *cdiv1.StorageProfile) (string, error) {
1✔
1143
        if vscName := storageProfile.Annotations[cc.AnnSnapshotClassForDataImportCron]; vscName != "" {
2✔
1144
                return vscName, nil
1✔
1145
        }
1✔
1146
        return cc.GetSnapshotClassForSmartClone(pvc, &storageProfile.Name, storageProfile.Status.SnapshotClass, r.log, r.client, r.recorder)
1✔
1147
}
1148

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

1✔
1152
        switch format {
1✔
1153
        case cdiv1.DataImportCronSourceFormatPvc:
1✔
1154
                updateDataImportCronCondition(dataImportCron, cdiv1.DataImportCronUpToDate, corev1.ConditionTrue, "Latest import is up to date", upToDate)
1✔
1155
        case cdiv1.DataImportCronSourceFormatSnapshot:
1✔
1156
                if snapshot == nil {
2✔
1157
                        // Snapshot create/update will trigger reconcile
1✔
1158
                        return nil
1✔
1159
                }
1✔
1160
                if cc.IsSnapshotReady(snapshot) {
2✔
1161
                        updateDataImportCronCondition(dataImportCron, cdiv1.DataImportCronUpToDate, corev1.ConditionTrue, "Latest import is up to date", upToDate)
1✔
1162
                } else {
2✔
1163
                        updateDataImportCronCondition(dataImportCron, cdiv1.DataImportCronUpToDate, corev1.ConditionFalse, "Snapshot of imported data is progressing", inProgress)
1✔
1164
                }
1✔
1165
        default:
×
1166
                return fmt.Errorf("unknown source format for snapshot")
×
1167
        }
1168

1169
        return nil
1✔
1170
}
1171

1172
func (r *DataImportCronReconciler) getSourceFormat(ctx context.Context, desiredStorageClass *storagev1.StorageClass) (cdiv1.DataImportCronSourceFormat, error) {
1✔
1173
        format := cdiv1.DataImportCronSourceFormatPvc
1✔
1174
        if desiredStorageClass == nil {
2✔
1175
                return format, nil
1✔
1176
        }
1✔
1177

1178
        storageProfile := &cdiv1.StorageProfile{}
1✔
1179
        if err := r.client.Get(ctx, types.NamespacedName{Name: desiredStorageClass.Name}, storageProfile); err != nil {
1✔
1180
                return format, err
×
1181
        }
×
1182
        if storageProfile.Status.DataImportCronSourceFormat != nil {
2✔
1183
                format = *storageProfile.Status.DataImportCronSourceFormat
1✔
1184
        }
1✔
1185

1186
        return format, nil
1✔
1187
}
1188

1189
func (r *DataImportCronReconciler) garbageCollectOldImports(ctx context.Context, cron *cdiv1.DataImportCron) error {
1✔
1190
        if cron.Spec.GarbageCollect != nil && *cron.Spec.GarbageCollect != cdiv1.DataImportCronGarbageCollectOutdated {
1✔
1191
                return nil
×
1192
        }
×
1193
        selector, err := getSelector(map[string]string{common.DataImportCronLabel: cron.Name})
1✔
1194
        if err != nil {
1✔
1195
                return err
×
1196
        }
×
1197

1198
        maxImports := defaultImportsToKeepPerCron
1✔
1199

1✔
1200
        if cron.Spec.ImportsToKeep != nil && *cron.Spec.ImportsToKeep >= 0 {
2✔
1201
                maxImports = int(*cron.Spec.ImportsToKeep)
1✔
1202
        }
1✔
1203

1204
        if err := r.garbageCollectPVCs(ctx, cron.Namespace, cron.Name, selector, maxImports); err != nil {
1✔
1205
                return err
×
1206
        }
×
1207
        if err := r.garbageCollectSnapshots(ctx, cron.Namespace, selector, maxImports); err != nil {
1✔
1208
                return err
×
1209
        }
×
1210

1211
        return nil
1✔
1212
}
1213

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

1✔
1217
        if err := r.client.List(ctx, pvcList, &client.ListOptions{Namespace: namespace, LabelSelector: selector}); err != nil {
1✔
1218
                return err
×
1219
        }
×
1220
        if len(pvcList.Items) > maxImports {
2✔
1221
                sort.Slice(pvcList.Items, func(i, j int) bool {
2✔
1222
                        return pvcList.Items[i].Annotations[AnnLastUseTime] > pvcList.Items[j].Annotations[AnnLastUseTime]
1✔
1223
                })
1✔
1224
                for _, pvc := range pvcList.Items[maxImports:] {
2✔
1225
                        r.log.Info("Deleting dv/pvc", "name", pvc.Name, "pvc.uid", pvc.UID)
1✔
1226
                        if err := r.deleteDvPvc(ctx, pvc.Name, pvc.Namespace); err != nil {
1✔
1227
                                return err
×
1228
                        }
×
1229
                }
1230
        }
1231

1232
        dvList := &cdiv1.DataVolumeList{}
1✔
1233
        if err := r.client.List(ctx, dvList, &client.ListOptions{Namespace: namespace, LabelSelector: selector}); err != nil {
1✔
1234
                return err
×
1235
        }
×
1236

1237
        if len(dvList.Items) > maxImports {
2✔
1238
                for _, dv := range dvList.Items {
2✔
1239
                        pvc := &corev1.PersistentVolumeClaim{}
1✔
1240
                        if err := r.client.Get(ctx, types.NamespacedName{Namespace: namespace, Name: dv.Name}, pvc); err != nil {
1✔
1241
                                return err
×
1242
                        }
×
1243

1244
                        if pvc.Labels[common.DataImportCronLabel] != cronName {
2✔
1245
                                r.log.Info("Deleting old version dv/pvc", "name", pvc.Name, "pvc.uid", pvc.UID)
1✔
1246
                                if err := r.deleteDvPvc(ctx, dv.Name, dv.Namespace); err != nil {
1✔
1247
                                        return err
×
1248
                                }
×
1249
                        }
1250
                }
1251
        }
1252

1253
        return nil
1✔
1254
}
1255

1256
// deleteDvPvc deletes DV or PVC if DV was GCed
1257
func (r *DataImportCronReconciler) deleteDvPvc(ctx context.Context, name, namespace string) error {
1✔
1258
        om := metav1.ObjectMeta{Name: name, Namespace: namespace}
1✔
1259
        dv := &cdiv1.DataVolume{ObjectMeta: om}
1✔
1260
        if err := r.client.Delete(ctx, dv); err == nil || !k8serrors.IsNotFound(err) {
2✔
1261
                return err
1✔
1262
        }
1✔
1263
        pvc := &corev1.PersistentVolumeClaim{ObjectMeta: om}
1✔
1264
        if err := r.client.Delete(ctx, pvc); err != nil && !k8serrors.IsNotFound(err) {
1✔
1265
                return err
×
1266
        }
×
1267
        return nil
1✔
1268
}
1269

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

1✔
1273
        if err := r.client.List(ctx, snapList, &client.ListOptions{Namespace: namespace, LabelSelector: selector}); err != nil {
1✔
1274
                if meta.IsNoMatchError(err) {
×
1275
                        return nil
×
1276
                }
×
1277
                return err
×
1278
        }
1279
        if len(snapList.Items) > maxImports {
1✔
1280
                sort.Slice(snapList.Items, func(i, j int) bool {
×
1281
                        return snapList.Items[i].Annotations[AnnLastUseTime] > snapList.Items[j].Annotations[AnnLastUseTime]
×
1282
                })
×
1283
                for _, snap := range snapList.Items[maxImports:] {
×
1284
                        r.log.Info("Deleting snapshot", "name", snap.Name, "uid", snap.UID)
×
1285
                        if err := r.client.Delete(ctx, &snap); err != nil && !k8serrors.IsNotFound(err) {
×
1286
                                return err
×
1287
                        }
×
1288
                }
1289
        }
1290

1291
        return nil
1✔
1292
}
1293

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

1✔
1298
        if err := r.deleteJobs(ctx, cron); err != nil {
1✔
1299
                return err
×
1300
        }
×
1301
        selector, err := getSelector(map[string]string{common.DataImportCronLabel: cron.Name, common.DataImportCronCleanupLabel: "true"})
1✔
1302
        if err != nil {
1✔
1303
                return err
×
1304
        }
×
1305
        opts := &client.DeleteAllOfOptions{ListOptions: client.ListOptions{Namespace: cron.Namespace, LabelSelector: selector}}
1✔
1306
        if err := r.client.DeleteAllOf(ctx, &cdiv1.DataSource{}, opts); err != nil {
1✔
1307
                return err
×
1308
        }
×
1309
        if err := r.client.DeleteAllOf(ctx, &cdiv1.DataVolume{}, opts); err != nil {
1✔
1310
                return err
×
1311
        }
×
1312
        if err := r.client.DeleteAllOf(ctx, &corev1.PersistentVolumeClaim{}, opts); err != nil {
1✔
1313
                return err
×
1314
        }
×
1315
        if err := r.client.DeleteAllOf(ctx, &snapshotv1.VolumeSnapshot{}, opts); cc.IgnoreIsNoMatchError(err) != nil {
1✔
1316
                return err
×
1317
        }
×
1318
        return nil
1✔
1319
}
1320

1321
func (r *DataImportCronReconciler) deleteJobs(ctx context.Context, cron types.NamespacedName) error {
1✔
1322
        deleteOpts := client.DeleteOptions{PropagationPolicy: ptr.To[metav1.DeletionPropagation](metav1.DeletePropagationBackground)}
1✔
1323
        selector, err := getSelector(map[string]string{common.DataImportCronNsLabel: cron.Namespace, common.DataImportCronLabel: cron.Name})
1✔
1324
        if err != nil {
1✔
1325
                return err
×
1326
        }
×
1327
        opts := &client.DeleteAllOfOptions{ListOptions: client.ListOptions{Namespace: r.cdiNamespace, LabelSelector: selector}, DeleteOptions: deleteOpts}
1✔
1328
        if err := r.client.DeleteAllOf(ctx, &batchv1.CronJob{}, opts); err != nil {
1✔
1329
                return err
×
1330
        }
×
1331
        if err := r.client.DeleteAllOf(ctx, &batchv1.Job{}, opts); err != nil {
1✔
1332
                return err
×
1333
        }
×
1334

1335
        return nil
1✔
1336
}
1337

1338
// NewDataImportCronController creates a new instance of the DataImportCron controller
1339
func NewDataImportCronController(mgr manager.Manager, log logr.Logger, importerImage, pullPolicy string, installerLabels map[string]string) (controller.Controller, error) {
×
1340
        uncachedClient, err := client.New(mgr.GetConfig(), client.Options{
×
1341
                Scheme: mgr.GetScheme(),
×
1342
                Mapper: mgr.GetRESTMapper(),
×
1343
        })
×
1344
        if err != nil {
×
1345
                return nil, err
×
1346
        }
×
1347
        reconciler := &DataImportCronReconciler{
×
1348
                client:          mgr.GetClient(),
×
1349
                uncachedClient:  uncachedClient,
×
1350
                recorder:        mgr.GetEventRecorderFor(dataImportControllerName),
×
1351
                scheme:          mgr.GetScheme(),
×
1352
                log:             log.WithName(dataImportControllerName),
×
1353
                image:           importerImage,
×
1354
                pullPolicy:      pullPolicy,
×
1355
                cdiNamespace:    util.GetNamespace(),
×
1356
                installerLabels: installerLabels,
×
1357
        }
×
1358
        dataImportCronController, err := controller.New(dataImportControllerName, mgr, controller.Options{
×
1359
                MaxConcurrentReconciles: 3,
×
1360
                Reconciler:              reconciler,
×
1361
        })
×
1362
        if err != nil {
×
1363
                return nil, err
×
1364
        }
×
1365
        if err := addDataImportCronControllerWatches(mgr, dataImportCronController); err != nil {
×
1366
                return nil, err
×
1367
        }
×
1368
        log.Info("Initialized DataImportCron controller")
×
1369
        return dataImportCronController, nil
×
1370
}
1371

1372
func getCronName(obj client.Object) string {
×
1373
        return obj.GetLabels()[common.DataImportCronLabel]
×
1374
}
×
1375

1376
func getCronNs(obj client.Object) string {
×
1377
        return obj.GetLabels()[common.DataImportCronNsLabel]
×
1378
}
×
1379

1380
func mapSourceObjectToCron[T client.Object](_ context.Context, obj T) []reconcile.Request {
×
1381
        if cronName := getCronName(obj); cronName != "" {
×
1382
                return []reconcile.Request{{NamespacedName: types.NamespacedName{Name: cronName, Namespace: obj.GetNamespace()}}}
×
1383
        }
×
1384
        return nil
×
1385
}
1386

1387
func addDataImportCronControllerWatches(mgr manager.Manager, c controller.Controller) error {
×
1388
        if err := c.Watch(source.Kind(mgr.GetCache(), &cdiv1.DataImportCron{}, &handler.TypedEnqueueRequestForObject[*cdiv1.DataImportCron]{})); err != nil {
×
1389
                return err
×
1390
        }
×
1391

1392
        mapStorageProfileToCron := func(ctx context.Context, obj *cdiv1.StorageProfile) []reconcile.Request {
×
1393
                // TODO: Get rid of this after at least one version; use indexer on storage class annotation instead
×
1394
                // Otherwise we risk losing the storage profile event
×
1395
                var crons cdiv1.DataImportCronList
×
1396
                if err := mgr.GetClient().List(ctx, &crons); err != nil {
×
1397
                        c.GetLogger().Error(err, "Unable to list DataImportCrons")
×
1398
                        return nil
×
1399
                }
×
1400
                // Storage profiles are 1:1 to storage classes
1401
                scName := obj.GetName()
×
1402
                var reqs []reconcile.Request
×
1403
                for _, cron := range crons.Items {
×
1404
                        dataVolume := cron.Spec.Template
×
1405
                        explicitScName := cc.GetStorageClassFromDVSpec(&dataVolume)
×
1406
                        templateSc, err := cc.GetStorageClassByNameWithVirtFallback(ctx, mgr.GetClient(), explicitScName, dataVolume.Spec.ContentType)
×
1407
                        if err != nil || templateSc == nil {
×
1408
                                c.GetLogger().Error(err, "Unable to get storage class", "templateSc", templateSc)
×
1409
                                return reqs
×
1410
                        }
×
1411
                        if templateSc.Name == scName {
×
1412
                                reqs = append(reqs, reconcile.Request{NamespacedName: types.NamespacedName{Namespace: cron.Namespace, Name: cron.Name}})
×
1413
                        }
×
1414
                }
1415
                return reqs
×
1416
        }
1417

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

1429
        if err := c.Watch(source.Kind(mgr.GetCache(), &cdiv1.DataSource{},
×
1430
                handler.TypedEnqueueRequestsFromMapFunc[*cdiv1.DataSource](mapSourceObjectToCron),
×
1431
                predicate.TypedFuncs[*cdiv1.DataSource]{
×
1432
                        CreateFunc: func(event.TypedCreateEvent[*cdiv1.DataSource]) bool { return false },
×
1433
                        UpdateFunc: func(e event.TypedUpdateEvent[*cdiv1.DataSource]) bool { return getCronName(e.ObjectNew) != "" },
×
1434
                        DeleteFunc: func(e event.TypedDeleteEvent[*cdiv1.DataSource]) bool { return getCronName(e.Object) != "" },
×
1435
                },
1436
        )); err != nil {
×
1437
                return err
×
1438
        }
×
1439

1440
        if err := c.Watch(source.Kind(mgr.GetCache(), &corev1.PersistentVolumeClaim{},
×
1441
                handler.TypedEnqueueRequestsFromMapFunc[*corev1.PersistentVolumeClaim](mapSourceObjectToCron),
×
1442
                predicate.TypedFuncs[*corev1.PersistentVolumeClaim]{
×
1443
                        CreateFunc: func(event.TypedCreateEvent[*corev1.PersistentVolumeClaim]) bool { return false },
×
1444
                        UpdateFunc: func(event.TypedUpdateEvent[*corev1.PersistentVolumeClaim]) bool { return false },
×
1445
                        DeleteFunc: func(e event.TypedDeleteEvent[*corev1.PersistentVolumeClaim]) bool { return getCronName(e.Object) != "" },
×
1446
                },
1447
        )); err != nil {
×
1448
                return err
×
1449
        }
×
1450

1451
        if err := addDefaultStorageClassUpdateWatch(mgr, c); err != nil {
×
1452
                return err
×
1453
        }
×
1454

1455
        if err := c.Watch(source.Kind(mgr.GetCache(), &cdiv1.StorageProfile{},
×
1456
                handler.TypedEnqueueRequestsFromMapFunc[*cdiv1.StorageProfile](mapStorageProfileToCron),
×
1457
                predicate.TypedFuncs[*cdiv1.StorageProfile]{
×
1458
                        CreateFunc: func(event.TypedCreateEvent[*cdiv1.StorageProfile]) bool { return true },
×
1459
                        DeleteFunc: func(event.TypedDeleteEvent[*cdiv1.StorageProfile]) bool { return false },
×
1460
                        UpdateFunc: func(e event.TypedUpdateEvent[*cdiv1.StorageProfile]) bool {
×
1461
                                return dicRelevantFieldsChanged(e.ObjectOld, e.ObjectNew)
×
1462
                        },
×
1463
                },
1464
        )); err != nil {
×
1465
                return err
×
1466
        }
×
1467

1468
        mapCronJobToCron := func(_ context.Context, obj *batchv1.CronJob) []reconcile.Request {
×
1469
                return []reconcile.Request{{NamespacedName: types.NamespacedName{Namespace: getCronNs(obj), Name: getCronName(obj)}}}
×
1470
        }
×
1471

1472
        if err := c.Watch(source.Kind(mgr.GetCache(), &batchv1.CronJob{},
×
1473
                handler.TypedEnqueueRequestsFromMapFunc[*batchv1.CronJob](mapCronJobToCron),
×
1474
                predicate.TypedFuncs[*batchv1.CronJob]{
×
1475
                        CreateFunc: func(e event.TypedCreateEvent[*batchv1.CronJob]) bool {
×
1476
                                return getCronName(e.Object) != "" && getCronNs(e.Object) != ""
×
1477
                        },
×
1478
                        DeleteFunc: func(event.TypedDeleteEvent[*batchv1.CronJob]) bool { return false },
×
1479
                        UpdateFunc: func(event.TypedUpdateEvent[*batchv1.CronJob]) bool { return false },
×
1480
                },
1481
        )); err != nil {
×
1482
                return err
×
1483
        }
×
1484

1485
        if err := mgr.GetClient().List(context.TODO(), &snapshotv1.VolumeSnapshotList{}); err != nil {
×
1486
                if meta.IsNoMatchError(err) {
×
1487
                        // Back out if there's no point to attempt watch
×
1488
                        return nil
×
1489
                }
×
1490
                if !cc.IsErrCacheNotStarted(err) {
×
1491
                        return err
×
1492
                }
×
1493
        }
1494
        if err := c.Watch(source.Kind(mgr.GetCache(), &snapshotv1.VolumeSnapshot{},
×
1495
                handler.TypedEnqueueRequestsFromMapFunc[*snapshotv1.VolumeSnapshot](mapSourceObjectToCron),
×
1496
                predicate.TypedFuncs[*snapshotv1.VolumeSnapshot]{
×
1497
                        CreateFunc: func(event.TypedCreateEvent[*snapshotv1.VolumeSnapshot]) bool { return false },
×
1498
                        UpdateFunc: func(event.TypedUpdateEvent[*snapshotv1.VolumeSnapshot]) bool { return false },
×
1499
                        DeleteFunc: func(e event.TypedDeleteEvent[*snapshotv1.VolumeSnapshot]) bool { return getCronName(e.Object) != "" },
×
1500
                },
1501
        )); err != nil {
×
1502
                return err
×
1503
        }
×
1504

1505
        return nil
×
1506
}
1507

1508
func dicRelevantFieldsChanged(oldSp, newSp *cdiv1.StorageProfile) bool {
×
1509
        sourceFormatChanged := oldSp.Status.DataImportCronSourceFormat != newSp.Status.DataImportCronSourceFormat
×
1510
        rwoAnnotationChanged := oldSp.Annotations[cc.AnnUseReadWriteOnceForDataImportCron] != newSp.Annotations[cc.AnnUseReadWriteOnceForDataImportCron]
×
1511
        snapshotClassChanged := oldSp.Annotations[cc.AnnSnapshotClassForDataImportCron] != newSp.Annotations[cc.AnnSnapshotClassForDataImportCron]
×
1512
        return sourceFormatChanged || rwoAnnotationChanged || snapshotClassChanged
×
1513
}
×
1514

1515
// addDefaultStorageClassUpdateWatch watches for default/virt default storage class updates
1516
func addDefaultStorageClassUpdateWatch(mgr manager.Manager, c controller.Controller) error {
×
1517
        if err := c.Watch(source.Kind(mgr.GetCache(), &storagev1.StorageClass{},
×
1518
                handler.TypedEnqueueRequestsFromMapFunc[*storagev1.StorageClass](
×
1519
                        func(ctx context.Context, obj *storagev1.StorageClass) []reconcile.Request {
×
1520
                                log := c.GetLogger().WithName("DefaultStorageClassUpdateWatch")
×
1521
                                log.Info("Update", "sc", obj.GetName(),
×
1522
                                        "default", obj.GetAnnotations()[cc.AnnDefaultStorageClass] == "true",
×
1523
                                        "defaultVirt", obj.GetAnnotations()[cc.AnnDefaultVirtStorageClass] == "true")
×
1524
                                reqs, err := getReconcileRequestsForDicsWithoutExplicitStorageClass(ctx, mgr.GetClient())
×
1525
                                if err != nil {
×
1526
                                        log.Error(err, "Failed getting DataImportCrons with pending PVCs")
×
1527
                                }
×
1528
                                return reqs
×
1529
                        },
1530
                ),
1531
                predicate.TypedFuncs[*storagev1.StorageClass]{
1532
                        CreateFunc: func(event.TypedCreateEvent[*storagev1.StorageClass]) bool { return false },
×
1533
                        DeleteFunc: func(event.TypedDeleteEvent[*storagev1.StorageClass]) bool { return false },
×
1534
                        UpdateFunc: func(e event.TypedUpdateEvent[*storagev1.StorageClass]) bool {
×
1535
                                return (e.ObjectNew.Annotations[cc.AnnDefaultStorageClass] != e.ObjectOld.Annotations[cc.AnnDefaultStorageClass]) ||
×
1536
                                        (e.ObjectNew.Annotations[cc.AnnDefaultVirtStorageClass] != e.ObjectOld.Annotations[cc.AnnDefaultVirtStorageClass])
×
1537
                        },
×
1538
                },
1539
        )); err != nil {
×
1540
                return err
×
1541
        }
×
1542

1543
        return nil
×
1544
}
1545

1546
func getReconcileRequestsForDicsWithoutExplicitStorageClass(ctx context.Context, c client.Client) ([]reconcile.Request, error) {
×
1547
        dicList := &cdiv1.DataImportCronList{}
×
1548
        if err := c.List(ctx, dicList); err != nil {
×
1549
                return nil, err
×
1550
        }
×
1551
        reqs := []reconcile.Request{}
×
1552
        for _, dic := range dicList.Items {
×
1553
                if cc.GetStorageClassFromDVSpec(&dic.Spec.Template) != nil {
×
1554
                        continue
×
1555
                }
1556

1557
                reqs = append(reqs, reconcile.Request{NamespacedName: types.NamespacedName{Name: dic.Name, Namespace: dic.Namespace}})
×
1558
        }
1559

1560
        return reqs, nil
×
1561
}
1562

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

1568
        sc := pvc.Spec.StorageClassName
1✔
1569
        if sc == nil || *sc == desiredStorageClass {
2✔
1570
                return false, nil
1✔
1571
        }
1✔
1572

1573
        r.log.Info("Delete pending pvc", "name", pvc.Name, "ns", pvc.Namespace, "sc", *sc)
1✔
1574
        if err := r.client.Delete(ctx, pvc); cc.IgnoreNotFound(err) != nil {
1✔
1575
                return false, err
×
1576
        }
×
1577

1578
        return true, nil
1✔
1579
}
1580

1581
func (r *DataImportCronReconciler) cronJobExistsAndUpdated(ctx context.Context, cron *cdiv1.DataImportCron) (bool, error) {
1✔
1582
        cronJob := &batchv1.CronJob{}
1✔
1583
        cronJobKey := types.NamespacedName{Namespace: r.cdiNamespace, Name: GetCronJobName(cron)}
1✔
1584
        if err := r.client.Get(ctx, cronJobKey, cronJob); err != nil {
2✔
1585
                return false, cc.IgnoreNotFound(err)
1✔
1586
        }
1✔
1587

1588
        cronJobCopy := cronJob.DeepCopy()
1✔
1589
        if err := r.initCronJob(cron, cronJobCopy); err != nil {
1✔
1590
                return false, err
×
1591
        }
×
1592

1593
        if !reflect.DeepEqual(cronJob, cronJobCopy) {
2✔
1594
                r.log.Info("Updating CronJob", "name", cronJob.GetName())
1✔
1595
                if err := r.client.Update(ctx, cronJobCopy); err != nil {
1✔
1596
                        return false, cc.IgnoreNotFound(err)
×
1597
                }
×
1598
        }
1599
        return true, nil
1✔
1600
}
1601

1602
func (r *DataImportCronReconciler) newCronJob(cron *cdiv1.DataImportCron) (*batchv1.CronJob, error) {
1✔
1603
        cronJob := &batchv1.CronJob{
1✔
1604
                ObjectMeta: metav1.ObjectMeta{
1✔
1605
                        Name:      GetCronJobName(cron),
1✔
1606
                        Namespace: r.cdiNamespace,
1✔
1607
                },
1✔
1608
        }
1✔
1609
        if err := r.initCronJob(cron, cronJob); err != nil {
1✔
1610
                return nil, err
×
1611
        }
×
1612
        return cronJob, nil
1✔
1613
}
1614

1615
// InitPollerPod inits poller Pod
1616
func InitPollerPod(c client.Client, cron *cdiv1.DataImportCron, pod *corev1.PodTemplateSpec, image string, pullPolicy corev1.PullPolicy, log logr.Logger) error {
1✔
1617
        regSource, err := getCronRegistrySource(cron)
1✔
1618
        if err != nil {
1✔
1619
                return err
×
1620
        }
×
1621
        if regSource.URL == nil {
1✔
1622
                return errors.Errorf("No URL source in cron %s", cron.Name)
×
1623
        }
×
1624
        cdiConfig := &cdiv1.CDIConfig{}
1✔
1625
        if err := c.Get(context.TODO(), types.NamespacedName{Name: common.ConfigName}, cdiConfig); err != nil {
1✔
1626
                return err
×
1627
        }
×
1628
        insecureTLS, err := IsInsecureTLS(*regSource.URL, cdiConfig, log)
1✔
1629
        if err != nil {
1✔
1630
                return err
×
1631
        }
×
1632
        container := corev1.Container{
1✔
1633
                Name:  "cdi-source-update-poller",
1✔
1634
                Image: image,
1✔
1635
                Command: []string{
1✔
1636
                        "/usr/bin/cdi-source-update-poller",
1✔
1637
                        "-ns", cron.Namespace,
1✔
1638
                        "-cron", cron.Name,
1✔
1639
                        "-url", *regSource.URL,
1✔
1640
                },
1✔
1641
                ImagePullPolicy:          pullPolicy,
1✔
1642
                TerminationMessagePath:   corev1.TerminationMessagePathDefault,
1✔
1643
                TerminationMessagePolicy: corev1.TerminationMessageFallbackToLogsOnError,
1✔
1644
        }
1✔
1645

1✔
1646
        var volumes []corev1.Volume
1✔
1647
        hasCertConfigMap := regSource.CertConfigMap != nil && *regSource.CertConfigMap != ""
1✔
1648
        if hasCertConfigMap {
1✔
1649
                vm := corev1.VolumeMount{
×
1650
                        Name:      CertVolName,
×
1651
                        MountPath: common.ImporterCertDir,
×
1652
                }
×
1653
                container.VolumeMounts = append(container.VolumeMounts, vm)
×
1654
                container.Command = append(container.Command, "-certdir", common.ImporterCertDir)
×
1655
                volumes = append(volumes, createConfigMapVolume(CertVolName, *regSource.CertConfigMap))
×
1656
        }
×
1657

1658
        if volName, _ := GetImportProxyConfig(cdiConfig, common.ImportProxyConfigMapName); volName != "" {
2✔
1659
                vm := corev1.VolumeMount{
1✔
1660
                        Name:      ProxyCertVolName,
1✔
1661
                        MountPath: common.ImporterProxyCertDir,
1✔
1662
                }
1✔
1663
                container.VolumeMounts = append(container.VolumeMounts, vm)
1✔
1664
                volumes = append(volumes, createConfigMapVolume(ProxyCertVolName, volName))
1✔
1665
        }
1✔
1666

1667
        if regSource.SecretRef != nil && *regSource.SecretRef != "" {
1✔
1668
                container.Env = append(container.Env,
×
1669
                        corev1.EnvVar{
×
1670
                                Name: common.ImporterAccessKeyID,
×
1671
                                ValueFrom: &corev1.EnvVarSource{
×
1672
                                        SecretKeyRef: &corev1.SecretKeySelector{
×
1673
                                                LocalObjectReference: corev1.LocalObjectReference{
×
1674
                                                        Name: *regSource.SecretRef,
×
1675
                                                },
×
1676
                                                Key: common.KeyAccess,
×
1677
                                        },
×
1678
                                },
×
1679
                        },
×
1680
                        corev1.EnvVar{
×
1681
                                Name: common.ImporterSecretKey,
×
1682
                                ValueFrom: &corev1.EnvVarSource{
×
1683
                                        SecretKeyRef: &corev1.SecretKeySelector{
×
1684
                                                LocalObjectReference: corev1.LocalObjectReference{
×
1685
                                                        Name: *regSource.SecretRef,
×
1686
                                                },
×
1687
                                                Key: common.KeySecret,
×
1688
                                        },
×
1689
                                },
×
1690
                        },
×
1691
                )
×
1692
        }
×
1693

1694
        addEnvVar := func(varName, value string) {
2✔
1695
                container.Env = append(container.Env, corev1.EnvVar{Name: varName, Value: value})
1✔
1696
        }
1✔
1697

1698
        if insecureTLS {
1✔
1699
                addEnvVar(common.InsecureTLSVar, "true")
×
1700
        }
×
1701

1702
        addEnvVarFromImportProxyConfig := func(varName string) {
2✔
1703
                if value, err := GetImportProxyConfig(cdiConfig, varName); err == nil {
2✔
1704
                        addEnvVar(varName, value)
1✔
1705
                }
1✔
1706
        }
1707

1708
        addEnvVarFromImportProxyConfig(common.ImportProxyHTTP)
1✔
1709
        addEnvVarFromImportProxyConfig(common.ImportProxyHTTPS)
1✔
1710
        addEnvVarFromImportProxyConfig(common.ImportProxyNoProxy)
1✔
1711

1✔
1712
        imagePullSecrets, err := cc.GetImagePullSecrets(c)
1✔
1713
        if err != nil {
1✔
1714
                return err
×
1715
        }
×
1716
        workloadNodePlacement, err := cc.GetWorkloadNodePlacement(context.TODO(), c)
1✔
1717
        if err != nil {
1✔
1718
                return err
×
1719
        }
×
1720

1721
        podSpec := &pod.Spec
1✔
1722

1✔
1723
        podSpec.RestartPolicy = corev1.RestartPolicyNever
1✔
1724
        podSpec.TerminationGracePeriodSeconds = ptr.To[int64](0)
1✔
1725
        podSpec.Containers = []corev1.Container{container}
1✔
1726
        podSpec.ServiceAccountName = common.CronJobServiceAccountName
1✔
1727
        podSpec.Volumes = volumes
1✔
1728
        podSpec.ImagePullSecrets = imagePullSecrets
1✔
1729
        podSpec.NodeSelector = workloadNodePlacement.NodeSelector
1✔
1730
        podSpec.Tolerations = workloadNodePlacement.Tolerations
1✔
1731
        podSpec.Affinity = workloadNodePlacement.Affinity
1✔
1732

1✔
1733
        cc.SetRestrictedSecurityContext(podSpec)
1✔
1734
        // No need for specifid uid/fsgroup here since this doesn't write or use qemu
1✔
1735
        if podSpec.SecurityContext != nil {
2✔
1736
                podSpec.SecurityContext.FSGroup = nil
1✔
1737
        }
1✔
1738
        if podSpec.Containers[0].SecurityContext != nil {
2✔
1739
                podSpec.Containers[0].SecurityContext.RunAsUser = nil
1✔
1740
        }
1✔
1741

1742
        if pod.Labels == nil {
2✔
1743
                pod.Labels = map[string]string{}
1✔
1744
        }
1✔
1745
        pod.Labels[common.DataImportCronPollerLabel] = ""
1✔
1746

1✔
1747
        return nil
1✔
1748
}
1749

1750
func (r *DataImportCronReconciler) initCronJob(cron *cdiv1.DataImportCron, cronJob *batchv1.CronJob) error {
1✔
1751
        cronJobSpec := &cronJob.Spec
1✔
1752
        cronJobSpec.Schedule = cron.Spec.Schedule
1✔
1753
        cronJobSpec.ConcurrencyPolicy = batchv1.ForbidConcurrent
1✔
1754
        cronJobSpec.SuccessfulJobsHistoryLimit = ptr.To[int32](1)
1✔
1755
        cronJobSpec.FailedJobsHistoryLimit = ptr.To[int32](1)
1✔
1756

1✔
1757
        jobSpec := &cronJobSpec.JobTemplate.Spec
1✔
1758
        jobSpec.BackoffLimit = ptr.To[int32](2)
1✔
1759
        jobSpec.TTLSecondsAfterFinished = ptr.To[int32](10)
1✔
1760
        cc.AddAnnotation(&jobSpec.Template, secv1.RequiredSCCAnnotation, common.RestrictedSCCName)
1✔
1761

1✔
1762
        pod := &jobSpec.Template
1✔
1763
        if err := InitPollerPod(r.client, cron, pod, r.image, corev1.PullPolicy(r.pullPolicy), r.log); err != nil {
1✔
1764
                return err
×
1765
        }
×
1766
        if err := r.setJobCommon(cron, cronJob); err != nil {
1✔
1767
                return err
×
1768
        }
×
1769
        return nil
1✔
1770
}
1771

1772
func (r *DataImportCronReconciler) newInitialJob(cron *cdiv1.DataImportCron, cronJob *batchv1.CronJob) (*batchv1.Job, error) {
1✔
1773
        job := &batchv1.Job{
1✔
1774
                ObjectMeta: metav1.ObjectMeta{
1✔
1775
                        Name:      GetInitialJobName(cron),
1✔
1776
                        Namespace: cronJob.Namespace,
1✔
1777
                },
1✔
1778
                Spec: cronJob.Spec.JobTemplate.Spec,
1✔
1779
        }
1✔
1780
        if err := r.setJobCommon(cron, job); err != nil {
1✔
1781
                return nil, err
×
1782
        }
×
1783
        return job, nil
1✔
1784
}
1785

1786
func (r *DataImportCronReconciler) setJobCommon(cron *cdiv1.DataImportCron, obj metav1.Object) error {
1✔
1787
        if err := operator.SetOwnerRuntime(r.uncachedClient, obj); err != nil {
1✔
1788
                return err
×
1789
        }
×
1790
        util.SetRecommendedLabels(obj, r.installerLabels, common.CDIControllerName)
1✔
1791
        labels := obj.GetLabels()
1✔
1792
        labels[common.DataImportCronNsLabel] = cron.Namespace
1✔
1793
        labels[common.DataImportCronLabel] = cron.Name
1✔
1794
        obj.SetLabels(labels)
1✔
1795
        return nil
1✔
1796
}
1797

1798
func (r *DataImportCronReconciler) newSourceDataVolume(cron *cdiv1.DataImportCron, dataVolumeName string, storageProfile *cdiv1.StorageProfile) *cdiv1.DataVolume {
1✔
1799
        dv := cron.Spec.Template.DeepCopy()
1✔
1800
        if isCronRegistrySource(cron) {
2✔
1801
                var digestedURL string
1✔
1802
                if isURLSource(cron) {
2✔
1803
                        digestedURL = untagDigestedDockerURL(*dv.Spec.Source.Registry.URL + "@" + cron.Annotations[AnnSourceDesiredDigest])
1✔
1804
                } else if isImageStreamSource(cron) {
3✔
1805
                        // No way to import image stream by name when we want specific digest, so we use its docker reference
1✔
1806
                        digestedURL = "docker://" + cron.Annotations[AnnImageStreamDockerRef]
1✔
1807
                        dv.Spec.Source.Registry.ImageStream = nil
1✔
1808
                }
1✔
1809
                dv.Spec.Source.Registry.URL = &digestedURL
1✔
1810
        }
1811
        dv.Name = dataVolumeName
1✔
1812
        dv.Namespace = cron.Namespace
1✔
1813
        r.setDataImportCronResourceLabels(cron, dv)
1✔
1814
        cc.AddAnnotation(dv, cc.AnnImmediateBinding, "true")
1✔
1815
        cc.AddAnnotation(dv, AnnLastUseTime, time.Now().UTC().Format(time.RFC3339Nano))
1✔
1816
        passCronAnnotationToDv(cron, dv, cc.AnnPodRetainAfterCompletion)
1✔
1817

1✔
1818
        for _, defaultInstanceTypeLabel := range cc.DefaultInstanceTypeLabels {
2✔
1819
                passCronLabelToDv(cron, dv, defaultInstanceTypeLabel)
1✔
1820
        }
1✔
1821

1822
        passCronLabelToDv(cron, dv, cc.LabelDynamicCredentialSupport)
1✔
1823

1✔
1824
        // Apply RWO access mode as default for DataImportCron (from StorageProfile annotation)
1✔
1825
        // Only applies if the DV doesn't already have AccessModes configured
1✔
1826
        if storageProfile != nil && storageProfile.Annotations[cc.AnnUseReadWriteOnceForDataImportCron] == "true" {
2✔
1827
                if dv.Spec.Storage != nil && len(dv.Spec.Storage.AccessModes) == 0 {
2✔
1828
                        dv.Spec.Storage.AccessModes = []corev1.PersistentVolumeAccessMode{corev1.ReadWriteOnce}
1✔
1829
                }
1✔
1830
        }
1831

1832
        return dv
1✔
1833
}
1834

1835
func (r *DataImportCronReconciler) setDataImportCronResourceLabels(cron *cdiv1.DataImportCron, obj metav1.Object) {
1✔
1836
        util.SetRecommendedLabels(obj, r.installerLabels, common.CDIControllerName)
1✔
1837
        cc.CopyAllowedLabels(cron.GetLabels(), obj, true)
1✔
1838
        labels := obj.GetLabels()
1✔
1839
        labels[common.DataImportCronLabel] = cron.Name
1✔
1840
        if cron.Spec.RetentionPolicy != nil && *cron.Spec.RetentionPolicy == cdiv1.DataImportCronRetainNone {
2✔
1841
                labels[common.DataImportCronCleanupLabel] = "true"
1✔
1842
        }
1✔
1843
        obj.SetLabels(labels)
1✔
1844
}
1845

1846
func untagDigestedDockerURL(dockerURL string) string {
1✔
1847
        if u, err := url.Parse(dockerURL); err == nil {
2✔
1848
                url := u.Host + u.Path
1✔
1849
                subs := reference.ReferenceRegexp.FindStringSubmatch(url)
1✔
1850
                // Check for tag
1✔
1851
                if len(subs) > 2 && len(subs[2]) > 0 {
2✔
1852
                        if untaggedRef, err := reference.ParseDockerRef(url); err == nil {
2✔
1853
                                return u.Scheme + "://" + untaggedRef.String()
1✔
1854
                        }
1✔
1855
                }
1856
        }
1857
        return dockerURL
1✔
1858
}
1859

1860
func passCronLabelToDv(cron *cdiv1.DataImportCron, dv *cdiv1.DataVolume, ann string) {
1✔
1861
        if val := cron.Labels[ann]; val != "" {
2✔
1862
                cc.AddLabel(dv, ann, val)
1✔
1863
        }
1✔
1864
}
1865

1866
func passCronAnnotationToDv(cron *cdiv1.DataImportCron, dv *cdiv1.DataVolume, ann string) {
1✔
1867
        if val := cron.Annotations[ann]; val != "" {
1✔
1868
                cc.AddAnnotation(dv, ann, val)
×
1869
        }
×
1870
}
1871

1872
func (r *DataImportCronReconciler) newDataSource(cron *cdiv1.DataImportCron) *cdiv1.DataSource {
1✔
1873
        dataSource := &cdiv1.DataSource{
1✔
1874
                ObjectMeta: metav1.ObjectMeta{
1✔
1875
                        Name:      cron.Spec.ManagedDataSource,
1✔
1876
                        Namespace: cron.Namespace,
1✔
1877
                },
1✔
1878
        }
1✔
1879
        util.SetRecommendedLabels(dataSource, r.installerLabels, common.CDIControllerName)
1✔
1880
        dataSource.Labels[common.DataImportCronLabel] = cron.Name
1✔
1881
        cc.CopyAllowedLabels(cron.GetLabels(), dataSource, true)
1✔
1882
        return dataSource
1✔
1883
}
1✔
1884

1885
// Create DataVolume name based on the DataSource name + prefix of the digest
1886
func createDvName(prefix, digest string) (string, error) {
1✔
1887
        digestPrefix := ""
1✔
1888
        if strings.HasPrefix(digest, digestSha256Prefix) {
2✔
1889
                digestPrefix = digestSha256Prefix
1✔
1890
        } else if strings.HasPrefix(digest, digestUIDPrefix) {
3✔
1891
                digestPrefix = digestUIDPrefix
1✔
1892
        } else {
2✔
1893
                return "", errors.Errorf("Digest has no supported prefix")
1✔
1894
        }
1✔
1895
        fromIdx := len(digestPrefix)
1✔
1896
        toIdx := fromIdx + digestDvNameSuffixLength
1✔
1897
        if len(digest) < toIdx {
2✔
1898
                return "", errors.Errorf("Digest is too short")
1✔
1899
        }
1✔
1900
        return naming.GetResourceName(prefix, digest[fromIdx:toIdx]), nil
1✔
1901
}
1902

1903
// GetCronJobName get CronJob name based on cron name and UID
1904
func GetCronJobName(cron *cdiv1.DataImportCron) string {
1✔
1905
        return naming.GetResourceName(cron.Name, string(cron.UID)[:cronJobUIDSuffixLength])
1✔
1906
}
1✔
1907

1908
// GetInitialJobName get initial job name based on cron name and UID
1909
func GetInitialJobName(cron *cdiv1.DataImportCron) string {
1✔
1910
        return naming.GetResourceName("initial-job", GetCronJobName(cron))
1✔
1911
}
1✔
1912

1913
func getPollerPodName(cron *cdiv1.DataImportCron) string {
1✔
1914
        return naming.GetResourceName("poller-"+cron.Name, string(cron.UID)[:8])
1✔
1915
}
1✔
1916

1917
func getSelector(matchLabels map[string]string) (labels.Selector, error) {
1✔
1918
        return metav1.LabelSelectorAsSelector(&metav1.LabelSelector{MatchLabels: matchLabels})
1✔
1919
}
1✔
1920

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

1✔
1924
        if explicitVolumeMode := getVolumeModeFromDVSpec(dv); explicitVolumeMode != nil {
1✔
1925
                return explicitVolumeMode, nil
×
1926
        }
×
1927

1928
        accessModes := getAccessModesFromDVSpec(dv)
1✔
1929
        inferredPvc := &corev1.PersistentVolumeClaim{
1✔
1930
                Spec: corev1.PersistentVolumeClaimSpec{
1✔
1931
                        StorageClassName: cc.GetStorageClassFromDVSpec(dv),
1✔
1932
                        AccessModes:      accessModes,
1✔
1933
                        VolumeMode:       ptr.To(cdiv1.PersistentVolumeFromStorageProfile),
1✔
1934
                        Resources: corev1.VolumeResourceRequirements{
1✔
1935
                                Requests: corev1.ResourceList{
1✔
1936
                                        // Doesn't matter
1✔
1937
                                        corev1.ResourceStorage: resource.MustParse("1Gi"),
1✔
1938
                                },
1✔
1939
                        },
1✔
1940
                },
1✔
1941
        }
1✔
1942
        if err := dvc.RenderPvc(ctx, client, inferredPvc); err != nil {
1✔
1943
                return nil, err
×
1944
        }
×
1945

1946
        return inferredPvc.Spec.VolumeMode, nil
1✔
1947
}
1948

1949
// getVolumeModeFromDVSpec returns the volume mode from DataVolume PVC or Storage spec
1950
func getVolumeModeFromDVSpec(dv *cdiv1.DataVolume) *corev1.PersistentVolumeMode {
1✔
1951
        if dv.Spec.PVC != nil {
1✔
1952
                return dv.Spec.PVC.VolumeMode
×
1953
        }
×
1954

1955
        if dv.Spec.Storage != nil {
2✔
1956
                return dv.Spec.Storage.VolumeMode
1✔
1957
        }
1✔
1958

1959
        return nil
×
1960
}
1961

1962
// getAccessModesFromDVSpec returns the access modes from DataVolume PVC or Storage spec
1963
func getAccessModesFromDVSpec(dv *cdiv1.DataVolume) []corev1.PersistentVolumeAccessMode {
1✔
1964
        if dv.Spec.PVC != nil {
1✔
1965
                return dv.Spec.PVC.AccessModes
×
1966
        }
×
1967

1968
        if dv.Spec.Storage != nil {
2✔
1969
                return dv.Spec.Storage.AccessModes
1✔
1970
        }
1✔
1971

1972
        return nil
×
1973
}
1974

1975
func inferAdvisedRestoreSizeForSnapshot(dv *cdiv1.DataVolume, snapshot *snapshotv1.VolumeSnapshot, fallback *resource.Quantity) *resource.Quantity {
1✔
1976
        var dvSize resource.Quantity
1✔
1977

1✔
1978
        if dv.Spec.PVC != nil {
1✔
1979
                dvSize = dv.Spec.PVC.Resources.Requests[corev1.ResourceStorage]
×
1980
        }
×
1981

1982
        if dv.Spec.Storage != nil {
2✔
1983
                dvSize = dv.Spec.Storage.Resources.Requests[corev1.ResourceStorage]
1✔
1984
        }
1✔
1985

1986
        if dvSize.IsZero() && fallback != nil {
2✔
1987
                return fallback
1✔
1988
        }
1✔
1989

1990
        if snapshot.Status != nil {
2✔
1991
                if rs := snapshot.Status.RestoreSize; rs != nil && dvSize.Cmp(*rs) < 0 {
2✔
1992
                        return snapshot.Status.RestoreSize
1✔
1993
                }
1✔
1994
        }
1995

1996
        return &dvSize
1✔
1997
}
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