• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In
Build has been canceled!

kubevirt / containerized-data-importer / #5491

17 Jul 2025 01:25AM UTC coverage: 59.326% (-0.2%) from 59.502%
#5491

push

travis-ci

web-flow
Populate DV with events from PVC Prime (#3764)

* update role for controller so it can get list of events

Signed-off-by: dsanatar <dsanatar@redhat.com>

* add new field index for events so we can filter by the object's name. add new function that gets all events associated with a primePvc and re-emits them for the regular pvc

Signed-off-by: dsanatar <dsanatar@redhat.com>

* add watcher for Events filtered by PVC type. modify copyEvent func to only emit unique events from primePVC

Signed-off-by: dsanatar <dsanatar@redhat.com>

* add new field index for event uids so we can filter accordingly

Signed-off-by: dsanatar <dsanatar@redhat.com>

* sort events by most recent timestamps and so we can loop more efficiently to emit new events

Signed-off-by: dsanatar <dsanatar@redhat.com>

* fix linting

Signed-off-by: dsanatar <dsanatar@redhat.com>

* modify watcher to filter on only prime pvc events. move copyEvents to ReconcileTargetPvc func. modify copyEvents logic to handle edge case where events have same timestamps

Signed-off-by: dsanatar <dsanatar@redhat.com>

* add missing bracket

Signed-off-by: dsanatar <dsanatar@redhat.com>

* modify CopyEvents func to take in two client.Objects instead so we can resuse the same func when we need to copy events from primePvc to pvc and from pvc to dv

Signed-off-by: dsanatar <dsanatar@redhat.com>

* move func call to CopyEvents to emitEvents func so it only occurs when DVs status has changed

Signed-off-by: dsanatar <dsanatar@redhat.com>

* add conditional to CopyEvents so when we are handling DVs we only copy over events from the primePVC

Signed-off-by: dsanatar <dsanatar@redhat.com>

* remove debug logs

Signed-off-by: dsanatar <dsanatar@redhat.com>

* reuse existing function to add pvcPrime name annotation to import populator so that we can access the prime name downstream

Signed-off-by: dsanatar <dsanatar@redhat.com>

* update DV bound condition to include PVC Prime name if one exists while the claim is stil... (continued)

75 of 206 new or added lines in 11 files covered. (36.41%)

5 existing lines in 2 files now uncovered.

17163 of 28930 relevant lines covered (59.33%)

0.66 hits per line

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

71.39
/pkg/controller/import-controller.go
1
package controller
2

3
import (
4
        "context"
5
        "fmt"
6
        "net/url"
7
        "path"
8
        "reflect"
9
        "strconv"
10
        "strings"
11
        "time"
12

13
        "github.com/go-logr/logr"
14
        "github.com/pkg/errors"
15

16
        corev1 "k8s.io/api/core/v1"
17
        v1 "k8s.io/api/core/v1"
18
        k8serrors "k8s.io/apimachinery/pkg/api/errors"
19
        metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
20
        "k8s.io/apimachinery/pkg/runtime"
21
        "k8s.io/apimachinery/pkg/types"
22
        "k8s.io/apimachinery/pkg/util/sets"
23
        "k8s.io/client-go/tools/record"
24
        "k8s.io/utils/ptr"
25

26
        "sigs.k8s.io/controller-runtime/pkg/client"
27
        "sigs.k8s.io/controller-runtime/pkg/controller"
28
        "sigs.k8s.io/controller-runtime/pkg/handler"
29
        "sigs.k8s.io/controller-runtime/pkg/manager"
30
        "sigs.k8s.io/controller-runtime/pkg/reconcile"
31
        "sigs.k8s.io/controller-runtime/pkg/source"
32

33
        cdiv1 "kubevirt.io/containerized-data-importer-api/pkg/apis/core/v1beta1"
34
        "kubevirt.io/containerized-data-importer/pkg/common"
35
        cc "kubevirt.io/containerized-data-importer/pkg/controller/common"
36
        featuregates "kubevirt.io/containerized-data-importer/pkg/feature-gates"
37
        "kubevirt.io/containerized-data-importer/pkg/util"
38
        "kubevirt.io/containerized-data-importer/pkg/util/naming"
39
        sdkapi "kubevirt.io/controller-lifecycle-operator-sdk/api"
40
)
41

42
const (
43
        // ErrImportFailedPVC provides a const to indicate an import to the PVC failed
44
        ErrImportFailedPVC = "ErrImportFailed"
45
        // ImportSucceededPVC provides a const to indicate an import to the PVC failed
46
        ImportSucceededPVC = "ImportSucceeded"
47

48
        // creatingScratch provides a const to indicate scratch is being created.
49
        creatingScratch = "CreatingScratchSpace"
50

51
        // ImportTargetInUse is reason for event created when an import pvc is in use
52
        ImportTargetInUse = "ImportTargetInUse"
53

54
        // importPodImageStreamFinalizer ensures image stream import pod is deleted when pvc is deleted,
55
        // as in this case pod has no pvc OwnerReference
56
        importPodImageStreamFinalizer = "cdi.kubevirt.io/importImageStream"
57

58
        // secretExtraHeadersVolumeName is the format string that specifies where extra HTTP header secrets will be mounted
59
        secretExtraHeadersVolumeName = "cdi-secret-extra-headers-vol-%d"
60
)
61

62
// ImportReconciler members
63
type ImportReconciler struct {
64
        client             client.Client
65
        uncachedClient     client.Client
66
        recorder           record.EventRecorder
67
        scheme             *runtime.Scheme
68
        log                logr.Logger
69
        image              string
70
        verbose            string
71
        pullPolicy         string
72
        filesystemOverhead string //nolint:unused // TODO: check if need to remove this field
73
        cdiNamespace       string
74
        featureGates       featuregates.FeatureGates
75
        installerLabels    map[string]string
76
}
77

78
type importPodEnvVar struct {
79
        ep                        string
80
        secretName                string
81
        source                    string
82
        contentType               string
83
        imageSize                 string
84
        certConfigMap             string
85
        diskID                    string
86
        uuid                      string
87
        pullMethod                string
88
        readyFile                 string
89
        doneFile                  string
90
        backingFile               string
91
        thumbprint                string
92
        filesystemOverhead        string
93
        insecureTLS               bool
94
        currentCheckpoint         string
95
        previousCheckpoint        string
96
        finalCheckpoint           string
97
        preallocation             bool
98
        httpProxy                 string
99
        httpsProxy                string
100
        noProxy                   string
101
        certConfigMapProxy        string
102
        extraHeaders              []string
103
        secretExtraHeaders        []string
104
        cacheMode                 string
105
        registryImageArchitecture string
106
}
107

108
type importerPodArgs struct {
109
        image                   string
110
        importImage             string
111
        verbose                 string
112
        pullPolicy              string
113
        podEnvVar               *importPodEnvVar
114
        pvc                     *corev1.PersistentVolumeClaim
115
        scratchPvcName          *string
116
        podResourceRequirements *corev1.ResourceRequirements
117
        imagePullSecrets        []corev1.LocalObjectReference
118
        workloadNodePlacement   *sdkapi.NodePlacement
119
        vddkImageName           *string
120
        vddkExtraArgs           *string
121
        priorityClassName       string
122
}
123

124
// NewImportController creates a new instance of the import controller.
125
func NewImportController(mgr manager.Manager, log logr.Logger, importerImage, pullPolicy, verbose string, installerLabels map[string]string) (controller.Controller, error) {
×
126
        uncachedClient, err := client.New(mgr.GetConfig(), client.Options{
×
127
                Scheme: mgr.GetScheme(),
×
128
                Mapper: mgr.GetRESTMapper(),
×
129
        })
×
130
        if err != nil {
×
131
                return nil, err
×
132
        }
×
133
        client := mgr.GetClient()
×
134
        reconciler := &ImportReconciler{
×
135
                client:          client,
×
136
                uncachedClient:  uncachedClient,
×
137
                scheme:          mgr.GetScheme(),
×
138
                log:             log.WithName("import-controller"),
×
139
                image:           importerImage,
×
140
                verbose:         verbose,
×
141
                pullPolicy:      pullPolicy,
×
142
                recorder:        mgr.GetEventRecorderFor("import-controller"),
×
143
                cdiNamespace:    util.GetNamespace(),
×
144
                featureGates:    featuregates.NewFeatureGates(client),
×
145
                installerLabels: installerLabels,
×
146
        }
×
147
        importController, err := controller.New("import-controller", mgr, controller.Options{
×
148
                MaxConcurrentReconciles: 3,
×
149
                Reconciler:              reconciler,
×
150
        })
×
151
        if err != nil {
×
152
                return nil, err
×
153
        }
×
154
        if err := addImportControllerWatches(mgr, importController); err != nil {
×
155
                return nil, err
×
156
        }
×
157
        return importController, nil
×
158
}
159

160
func addImportControllerWatches(mgr manager.Manager, importController controller.Controller) error {
×
161
        // Setup watches
×
162
        if err := importController.Watch(source.Kind(mgr.GetCache(), &corev1.PersistentVolumeClaim{}, &handler.TypedEnqueueRequestForObject[*corev1.PersistentVolumeClaim]{})); err != nil {
×
163
                return err
×
164
        }
×
165
        if err := importController.Watch(source.Kind(mgr.GetCache(), &corev1.Pod{}, handler.TypedEnqueueRequestForOwner[*corev1.Pod](
×
166
                mgr.GetScheme(), mgr.GetClient().RESTMapper(), &corev1.PersistentVolumeClaim{}, handler.OnlyControllerOwner()))); err != nil {
×
167
                return err
×
168
        }
×
169

170
        return nil
×
171
}
172

173
func (r *ImportReconciler) shouldReconcilePVC(pvc *corev1.PersistentVolumeClaim,
174
        log logr.Logger) (bool, error) {
1✔
175
        _, pvcUsesExternalPopulator := pvc.Annotations[cc.AnnExternalPopulation]
1✔
176
        if pvcUsesExternalPopulator {
1✔
177
                return false, nil
×
178
        }
×
179

180
        waitForFirstConsumerEnabled, err := cc.IsWaitForFirstConsumerEnabled(pvc, r.featureGates)
1✔
181
        if err != nil {
1✔
182
                return false, err
×
183
        }
×
184

185
        return (!cc.IsPVCComplete(pvc) || cc.IsMultiStageImportInProgress(pvc)) &&
1✔
186
                        (checkPVC(pvc, cc.AnnEndpoint, log) || checkPVC(pvc, cc.AnnSource, log)) &&
1✔
187
                        shouldHandlePvc(pvc, waitForFirstConsumerEnabled, log),
1✔
188
                nil
1✔
189
}
190

191
// Reconcile the reconcile loop for the CDIConfig object.
192
func (r *ImportReconciler) Reconcile(_ context.Context, req reconcile.Request) (reconcile.Result, error) {
1✔
193
        log := r.log.WithValues("PVC", req.NamespacedName)
1✔
194
        log.V(1).Info("reconciling Import PVCs")
1✔
195

1✔
196
        // Get the PVC.
1✔
197
        pvc := &corev1.PersistentVolumeClaim{}
1✔
198
        if err := r.client.Get(context.TODO(), req.NamespacedName, pvc); err != nil {
2✔
199
                if k8serrors.IsNotFound(err) {
2✔
200
                        return reconcile.Result{}, nil
1✔
201
                }
1✔
202
                return reconcile.Result{}, err
×
203
        }
204

205
        // only want to update bound condition for relevant type
206
        if checkPVC(pvc, cc.AnnEndpoint, log) || checkPVC(pvc, cc.AnnSource, log) {
2✔
207
                if err := cc.UpdatePVCBoundContionFromEvents(pvc, r.client, log); err != nil {
1✔
NEW
208
                        return reconcile.Result{}, err
×
NEW
209
                }
×
210
        }
211

212
        shouldReconcile, err := r.shouldReconcilePVC(pvc, log)
1✔
213
        if err != nil {
1✔
214
                return reconcile.Result{}, err
×
215
        }
×
216
        if !shouldReconcile {
2✔
217
                multiStageImport := metav1.HasAnnotation(pvc.ObjectMeta, cc.AnnCurrentCheckpoint)
1✔
218
                multiStageAlreadyDone := metav1.HasAnnotation(pvc.ObjectMeta, cc.AnnMultiStageImportDone)
1✔
219

1✔
220
                log.V(3).Info("Should not reconcile this PVC",
1✔
221
                        "pvc.annotation.phase.complete", cc.IsPVCComplete(pvc),
1✔
222
                        "pvc.annotations.endpoint", checkPVC(pvc, cc.AnnEndpoint, log),
1✔
223
                        "pvc.annotations.source", checkPVC(pvc, cc.AnnSource, log),
1✔
224
                        "isBound", isBound(pvc, log), "isMultistage", multiStageImport, "multiStageDone", multiStageAlreadyDone)
1✔
225
                return reconcile.Result{}, nil
1✔
226
        }
1✔
227

228
        return r.reconcilePvc(pvc, log)
1✔
229
}
230

231
func (r *ImportReconciler) findImporterPod(pvc *corev1.PersistentVolumeClaim, log logr.Logger) (*corev1.Pod, error) {
1✔
232
        podName := getImportPodNameFromPvc(pvc)
1✔
233
        pod := &corev1.Pod{}
1✔
234
        if err := r.client.Get(context.TODO(), types.NamespacedName{Name: podName, Namespace: pvc.GetNamespace()}, pod); err != nil {
2✔
235
                if !k8serrors.IsNotFound(err) {
1✔
236
                        return nil, errors.Wrapf(err, "error getting import pod %s/%s", pvc.Namespace, podName)
×
237
                }
×
238
                return nil, nil
1✔
239
        }
240
        if !metav1.IsControlledBy(pod, pvc) && !cc.IsImageStream(pvc) {
2✔
241
                return nil, errors.Errorf("Pod is not owned by PVC")
1✔
242
        }
1✔
243
        log.V(1).Info("Pod is owned by PVC", pod.Name, pvc.Name)
1✔
244
        return pod, nil
1✔
245
}
246

247
func (r *ImportReconciler) reconcilePvc(pvc *corev1.PersistentVolumeClaim, log logr.Logger) (reconcile.Result, error) {
1✔
248
        // See if we have a pod associated with the PVC, we know the PVC has the needed annotations.
1✔
249
        pod, err := r.findImporterPod(pvc, log)
1✔
250
        if err != nil {
2✔
251
                return reconcile.Result{}, err
1✔
252
        }
1✔
253

254
        if pod == nil {
2✔
255
                if cc.IsPVCComplete(pvc) {
1✔
256
                        // Don't create the POD if the PVC is completed already
×
257
                        log.V(1).Info("PVC is already complete")
×
258
                } else if pvc.DeletionTimestamp == nil {
2✔
259
                        podsUsingPVC, err := cc.GetPodsUsingPVCs(context.TODO(), r.client, pvc.Namespace, sets.New(pvc.Name), false)
1✔
260
                        if err != nil {
1✔
261
                                return reconcile.Result{}, err
×
262
                        }
×
263

264
                        if len(podsUsingPVC) > 0 {
2✔
265
                                for _, pod := range podsUsingPVC {
2✔
266
                                        r.log.V(1).Info("can't create import pod, pvc in use by other pod",
1✔
267
                                                "namespace", pvc.Namespace, "name", pvc.Name, "pod", pod.Name)
1✔
268
                                        r.recorder.Eventf(pvc, corev1.EventTypeWarning, ImportTargetInUse,
1✔
269
                                                "pod %s/%s using PersistentVolumeClaim %s", pod.Namespace, pod.Name, pvc.Name)
1✔
270
                                }
1✔
271
                                return reconcile.Result{Requeue: true}, nil
1✔
272
                        }
273

274
                        if _, ok := pvc.Annotations[cc.AnnImportPod]; ok {
2✔
275
                                // Create importer pod, make sure the PVC owns it.
1✔
276
                                if err := r.createImporterPod(pvc); err != nil {
1✔
277
                                        return reconcile.Result{}, err
×
278
                                }
×
279
                        } else {
1✔
280
                                // Create importer pod Name and store in PVC?
1✔
281
                                if err := r.initPvcPodName(pvc, log); err != nil {
1✔
282
                                        return reconcile.Result{}, err
×
283
                                }
×
284
                        }
285
                }
286
        } else {
1✔
287
                if pvc.DeletionTimestamp != nil {
1✔
288
                        log.V(1).Info("PVC being terminated, delete pods", "pod.Name", pod.Name)
×
289
                        if err := r.cleanup(pvc, pod, log); err != nil {
×
290
                                return reconcile.Result{}, err
×
291
                        }
×
292
                } else {
1✔
293
                        // Copy import proxy ConfigMap (if exists) from cdi namespace to the import namespace
1✔
294
                        if err := r.copyImportProxyConfigMap(pvc, pod); err != nil {
1✔
295
                                return reconcile.Result{}, err
×
296
                        }
×
297
                        // Pod exists, we need to update the PVC status.
298
                        if err := r.updatePvcFromPod(pvc, pod, log); err != nil {
1✔
299
                                return reconcile.Result{}, err
×
300
                        }
×
301
                }
302
        }
303

304
        if !cc.IsPVCComplete(pvc) {
2✔
305
                // We are not done yet, force a re-reconcile in 2 seconds to get an update.
1✔
306
                log.V(1).Info("Force Reconcile pvc import not finished", "pvc.Name", pvc.Name)
1✔
307

1✔
308
                return reconcile.Result{RequeueAfter: 2 * time.Second}, nil
1✔
309
        }
1✔
310
        return reconcile.Result{}, nil
1✔
311
}
312

313
func (r *ImportReconciler) copyImportProxyConfigMap(pvc *corev1.PersistentVolumeClaim, pod *corev1.Pod) error {
1✔
314
        cdiConfig := &cdiv1.CDIConfig{}
1✔
315
        if err := r.client.Get(context.TODO(), types.NamespacedName{Name: common.ConfigName}, cdiConfig); err != nil {
1✔
316
                return err
×
317
        }
×
318
        cmName, err := GetImportProxyConfig(cdiConfig, common.ImportProxyConfigMapName)
1✔
319
        if err != nil || cmName == "" {
2✔
320
                return nil
1✔
321
        }
1✔
322
        cdiConfigMap := &corev1.ConfigMap{}
×
323
        if err := r.uncachedClient.Get(context.TODO(), types.NamespacedName{Name: cmName, Namespace: r.cdiNamespace}, cdiConfigMap); err != nil {
×
324
                return err
×
325
        }
×
326
        importConfigMap := &corev1.ConfigMap{
×
327
                ObjectMeta: metav1.ObjectMeta{
×
328
                        Name:      GetImportProxyConfigMapName(pvc.Name),
×
329
                        Namespace: pvc.Namespace,
×
330
                        OwnerReferences: []metav1.OwnerReference{{
×
331
                                APIVersion:         pod.APIVersion,
×
332
                                Kind:               pod.Kind,
×
333
                                Name:               pod.Name,
×
334
                                UID:                pod.UID,
×
335
                                BlockOwnerDeletion: ptr.To[bool](true),
×
336
                                Controller:         ptr.To[bool](true),
×
337
                        }},
×
338
                },
×
339
                Data: cdiConfigMap.Data,
×
340
        }
×
341
        if err := r.client.Create(context.TODO(), importConfigMap); err != nil && !k8serrors.IsAlreadyExists(err) {
×
342
                return err
×
343
        }
×
344
        return nil
×
345
}
346

347
// GetImportProxyConfigMapName returns the import proxy ConfigMap name
348
func GetImportProxyConfigMapName(pvcName string) string {
×
349
        return naming.GetResourceName("import-proxy-cm", pvcName)
×
350
}
×
351

352
func (r *ImportReconciler) initPvcPodName(pvc *corev1.PersistentVolumeClaim, log logr.Logger) error {
1✔
353
        currentPvcCopy := pvc.DeepCopyObject()
1✔
354

1✔
355
        log.V(1).Info("Init pod name on PVC")
1✔
356
        anno := pvc.GetAnnotations()
1✔
357

1✔
358
        anno[cc.AnnImportPod] = createImportPodNameFromPvc(pvc)
1✔
359

1✔
360
        requiresScratch := r.requiresScratchSpace(pvc)
1✔
361
        if requiresScratch {
1✔
362
                anno[cc.AnnRequiresScratch] = "true"
×
363
        }
×
364

365
        if !reflect.DeepEqual(currentPvcCopy, pvc) {
2✔
366
                if err := r.updatePVC(pvc, log); err != nil {
1✔
367
                        return err
×
368
                }
×
369
                log.V(1).Info("Updated PVC", "pvc.anno.AnnImportPod", anno[cc.AnnImportPod])
1✔
370
        }
371
        return nil
1✔
372
}
373

374
func (r *ImportReconciler) updatePvcFromPod(pvc *corev1.PersistentVolumeClaim, pod *corev1.Pod, log logr.Logger) error {
1✔
375
        // Keep a copy of the original for comparison later.
1✔
376
        currentPvcCopy := pvc.DeepCopyObject()
1✔
377

1✔
378
        log.V(1).Info("Updating PVC from pod")
1✔
379
        anno := pvc.GetAnnotations()
1✔
380

1✔
381
        termMsg, err := parseTerminationMessage(pod)
1✔
382
        if err != nil {
2✔
383
                log.V(3).Info("Ignoring failure to parse termination message", "error", err.Error())
1✔
384
        }
1✔
385
        setAnnotationsFromPodWithPrefix(anno, pod, termMsg, cc.AnnRunningCondition)
1✔
386

1✔
387
        scratchSpaceRequired := termMsg != nil && termMsg.ScratchSpaceRequired != nil && *termMsg.ScratchSpaceRequired
1✔
388
        if scratchSpaceRequired {
2✔
389
                log.V(1).Info("Pod requires scratch space, terminating pod, and restarting with scratch space", "pod.Name", pod.Name)
1✔
390
        }
1✔
391
        podModificationsNeeded := scratchSpaceRequired
1✔
392

1✔
393
        if statuses := pod.Status.ContainerStatuses; len(statuses) > 0 {
2✔
394
                if isOOMKilled(statuses[0]) {
2✔
395
                        log.V(1).Info("Pod died of an OOM, deleting pod, and restarting with qemu cache mode=none if storage supports it", "pod.Name", pod.Name)
1✔
396
                        podModificationsNeeded = true
1✔
397
                        anno[cc.AnnRequiresDirectIO] = "true"
1✔
398
                }
1✔
399
                if terminated := statuses[0].State.Terminated; terminated != nil && terminated.ExitCode > 0 {
2✔
400
                        log.Info("Pod termination code", "pod.Name", pod.Name, "ExitCode", terminated.ExitCode)
1✔
401
                        r.recorder.Event(pvc, corev1.EventTypeWarning, ErrImportFailedPVC, terminated.Message)
1✔
402
                }
1✔
403
        }
404

405
        if anno[cc.AnnCurrentCheckpoint] != "" {
1✔
406
                anno[cc.AnnCurrentPodID] = string(pod.ObjectMeta.UID)
×
407
        }
×
408

409
        anno[cc.AnnImportPod] = pod.Name
1✔
410
        if !podModificationsNeeded {
2✔
411
                // No scratch space required, update the phase based on the pod. If we require scratch space we don't want to update the
1✔
412
                // phase, because the pod might terminate cleanly and mistakenly mark the import complete.
1✔
413
                anno[cc.AnnPodPhase] = string(pod.Status.Phase)
1✔
414
        }
1✔
415

416
        anno[cc.AnnPodSchedulable] = "true"
1✔
417
        if phase, ok := anno[cc.AnnPodPhase]; ok && phase == string(corev1.PodPending) {
2✔
418
                for _, cond := range pod.Status.Conditions {
1✔
419
                        if cond.Type == corev1.PodScheduled && cond.Reason == corev1.PodReasonUnschedulable {
×
420
                                anno[cc.AnnPodSchedulable] = "false"
×
421
                                break
×
422
                        }
423
                }
424
        }
425

426
        for _, ev := range pod.Spec.Containers[0].Env {
2✔
427
                if ev.Name == common.CacheMode && ev.Value == common.CacheModeTryNone {
1✔
428
                        anno[cc.AnnRequiresDirectIO] = "false"
×
429
                }
×
430
        }
431

432
        // Check if the POD is waiting for scratch space, if so create some.
433
        if pod.Status.Phase == corev1.PodPending && r.requiresScratchSpace(pvc) {
2✔
434
                if err := r.createScratchPvcForPod(pvc, pod); err != nil {
1✔
435
                        if !k8serrors.IsAlreadyExists(err) {
×
436
                                return err
×
437
                        }
×
438
                }
439
        } else {
1✔
440
                // No scratch space, or scratch space is bound, remove annotation
1✔
441
                delete(anno, cc.AnnBoundCondition)
1✔
442
                delete(anno, cc.AnnBoundConditionMessage)
1✔
443
                delete(anno, cc.AnnBoundConditionReason)
1✔
444
        }
1✔
445

446
        if pvc.GetLabels() == nil {
2✔
447
                pvc.SetLabels(make(map[string]string, 0))
1✔
448
        }
1✔
449
        if !checkIfLabelExists(pvc, common.CDILabelKey, common.CDILabelValue) {
2✔
450
                pvc.GetLabels()[common.CDILabelKey] = common.CDILabelValue
1✔
451
        }
1✔
452
        if cc.IsPVCComplete(pvc) {
2✔
453
                pvc.SetLabels(addLabelsFromTerminationMessage(pvc.GetLabels(), termMsg))
1✔
454
        }
1✔
455

456
        if !reflect.DeepEqual(currentPvcCopy, pvc) {
2✔
457
                if err := r.updatePVC(pvc, log); err != nil {
1✔
458
                        return err
×
459
                }
×
460
                log.V(1).Info("Updated PVC", "pvc.anno.Phase", anno[cc.AnnPodPhase], "pvc.anno.Restarts", anno[cc.AnnPodRestarts])
1✔
461
        }
462

463
        if cc.IsPVCComplete(pvc) || podModificationsNeeded {
2✔
464
                if !podModificationsNeeded {
2✔
465
                        r.recorder.Event(pvc, corev1.EventTypeNormal, ImportSucceededPVC, "Import Successful")
1✔
466
                        log.V(1).Info("Import completed successfully")
1✔
467
                }
1✔
468
                if cc.ShouldDeletePod(pvc) {
2✔
469
                        log.V(1).Info("Deleting pod", "pod.Name", pod.Name)
1✔
470
                        if err := r.cleanup(pvc, pod, log); err != nil {
1✔
471
                                return err
×
472
                        }
×
473
                }
474
        }
475
        return nil
1✔
476
}
477

478
func (r *ImportReconciler) cleanup(pvc *corev1.PersistentVolumeClaim, pod *corev1.Pod, log logr.Logger) error {
1✔
479
        if err := r.client.Delete(context.TODO(), pod); cc.IgnoreNotFound(err) != nil {
1✔
480
                return err
×
481
        }
×
482
        if cc.HasFinalizer(pvc, importPodImageStreamFinalizer) {
1✔
483
                cc.RemoveFinalizer(pvc, importPodImageStreamFinalizer)
×
484
                if err := r.updatePVC(pvc, log); err != nil {
×
485
                        return err
×
486
                }
×
487
        }
488
        return nil
1✔
489
}
490

491
func (r *ImportReconciler) updatePVC(pvc *corev1.PersistentVolumeClaim, log logr.Logger) error {
1✔
492
        if err := r.client.Update(context.TODO(), pvc); err != nil {
1✔
493
                return err
×
494
        }
×
495
        return nil
1✔
496
}
497

498
func (r *ImportReconciler) createImporterPod(pvc *corev1.PersistentVolumeClaim) error {
1✔
499
        r.log.V(1).Info("Creating importer POD for PVC", "pvc.Name", pvc.Name)
1✔
500
        var scratchPvcName *string
1✔
501
        var vddkImageName *string
1✔
502
        var vddkExtraArgs *string
1✔
503
        var err error
1✔
504

1✔
505
        requiresScratch := r.requiresScratchSpace(pvc)
1✔
506
        if requiresScratch {
1✔
507
                name := createScratchNameFromPvc(pvc)
×
508
                scratchPvcName = &name
×
509
        }
×
510

511
        if cc.GetSource(pvc) == cc.SourceVDDK {
2✔
512
                r.log.V(1).Info("Pod requires VDDK sidecar for VMware transfer")
1✔
513
                anno := pvc.GetAnnotations()
1✔
514
                if imageName, ok := anno[cc.AnnVddkInitImageURL]; ok {
2✔
515
                        vddkImageName = &imageName
1✔
516
                } else {
2✔
517
                        if vddkImageName, err = r.getVddkImageName(); err != nil {
2✔
518
                                r.log.V(1).Error(err, "failed to get VDDK image name from configmap")
1✔
519
                        }
1✔
520
                }
521
                if vddkImageName == nil {
2✔
522
                        message := fmt.Sprintf("waiting for %s configmap or %s annotation for VDDK image", common.VddkConfigMap, cc.AnnVddkInitImageURL)
1✔
523
                        anno[cc.AnnBoundCondition] = "false"
1✔
524
                        anno[cc.AnnBoundConditionMessage] = message
1✔
525
                        anno[cc.AnnBoundConditionReason] = common.AwaitingVDDK
1✔
526
                        if err := r.updatePVC(pvc, r.log); err != nil {
1✔
527
                                return err
×
528
                        }
×
529
                        return errors.New(message)
1✔
530
                }
531

532
                if extraArgs, ok := anno[cc.AnnVddkExtraArgs]; ok && extraArgs != "" {
2✔
533
                        r.log.V(1).Info("Mounting extra VDDK args ConfigMap to importer pod", "ConfigMap", extraArgs)
1✔
534
                        vddkExtraArgs = &extraArgs
1✔
535
                }
1✔
536
        }
537

538
        podEnvVar, err := r.createImportEnvVar(pvc)
1✔
539
        if err != nil {
1✔
540
                return err
×
541
        }
×
542
        // all checks passed, let's create the importer pod!
543
        podArgs := &importerPodArgs{
1✔
544
                image:             r.image,
1✔
545
                verbose:           r.verbose,
1✔
546
                pullPolicy:        r.pullPolicy,
1✔
547
                podEnvVar:         podEnvVar,
1✔
548
                pvc:               pvc,
1✔
549
                scratchPvcName:    scratchPvcName,
1✔
550
                vddkImageName:     vddkImageName,
1✔
551
                vddkExtraArgs:     vddkExtraArgs,
1✔
552
                priorityClassName: cc.GetPriorityClass(pvc),
1✔
553
        }
1✔
554

1✔
555
        pod, err := createImporterPod(context.TODO(), r.log, r.client, podArgs, r.installerLabels)
1✔
556
        // Check if pod has failed and, in that case, record an event with the error
1✔
557
        if podErr := cc.HandleFailedPod(err, pvc.Annotations[cc.AnnImportPod], pvc, r.recorder, r.client); podErr != nil {
1✔
558
                return podErr
×
559
        }
×
560

561
        r.log.V(1).Info("Created POD", "pod.Name", pod.Name)
1✔
562

1✔
563
        // If importing from image stream, add finalizer. Note we don't watch the importer pod in this case,
1✔
564
        // so to prevent a deadlock we add finalizer only if the pod is not retained after completion.
1✔
565
        if cc.IsImageStream(pvc) && pvc.GetAnnotations()[cc.AnnPodRetainAfterCompletion] != "true" {
1✔
566
                cc.AddFinalizer(pvc, importPodImageStreamFinalizer)
×
567
                if err := r.updatePVC(pvc, r.log); err != nil {
×
568
                        return err
×
569
                }
×
570
        }
571

572
        if requiresScratch {
1✔
573
                r.log.V(1).Info("Pod requires scratch space")
×
574
                return r.createScratchPvcForPod(pvc, pod)
×
575
        }
×
576

577
        return nil
1✔
578
}
579

580
func createScratchNameFromPvc(pvc *v1.PersistentVolumeClaim) string {
×
581
        return naming.GetResourceName(pvc.Name, common.ScratchNameSuffix)
×
582
}
×
583

584
func (r *ImportReconciler) createImportEnvVar(pvc *corev1.PersistentVolumeClaim) (*importPodEnvVar, error) {
1✔
585
        podEnvVar := &importPodEnvVar{}
1✔
586
        podEnvVar.source = cc.GetSource(pvc)
1✔
587
        podEnvVar.contentType = string(cc.GetPVCContentType(pvc))
1✔
588

1✔
589
        var err error
1✔
590
        if podEnvVar.source != cc.SourceNone {
2✔
591
                podEnvVar.ep, err = cc.GetEndpoint(pvc)
1✔
592
                if err != nil {
1✔
593
                        return nil, err
×
594
                }
×
595
                podEnvVar.secretName = r.getSecretName(pvc)
1✔
596
                if podEnvVar.secretName == "" {
2✔
597
                        r.log.V(2).Info("no secret will be supplied to endpoint", "endPoint", podEnvVar.ep)
1✔
598
                }
1✔
599
                //get the CDIConfig to extract the proxy configuration to be used to import an image
600
                cdiConfig := &cdiv1.CDIConfig{}
1✔
601
                err = r.client.Get(context.TODO(), types.NamespacedName{Name: common.ConfigName}, cdiConfig)
1✔
602
                if err != nil {
1✔
603
                        return nil, err
×
604
                }
×
605
                podEnvVar.certConfigMap, err = r.getCertConfigMap(pvc)
1✔
606
                if err != nil {
1✔
607
                        return nil, err
×
608
                }
×
609
                podEnvVar.insecureTLS, err = r.isInsecureTLS(pvc, cdiConfig)
1✔
610
                if err != nil {
1✔
611
                        return nil, err
×
612
                }
×
613
                podEnvVar.diskID = getValueFromAnnotation(pvc, cc.AnnDiskID)
1✔
614
                podEnvVar.backingFile = getValueFromAnnotation(pvc, cc.AnnBackingFile)
1✔
615
                podEnvVar.uuid = getValueFromAnnotation(pvc, cc.AnnUUID)
1✔
616
                podEnvVar.thumbprint = getValueFromAnnotation(pvc, cc.AnnThumbprint)
1✔
617
                podEnvVar.previousCheckpoint = getValueFromAnnotation(pvc, cc.AnnPreviousCheckpoint)
1✔
618
                podEnvVar.currentCheckpoint = getValueFromAnnotation(pvc, cc.AnnCurrentCheckpoint)
1✔
619
                podEnvVar.finalCheckpoint = getValueFromAnnotation(pvc, cc.AnnFinalCheckpoint)
1✔
620
                podEnvVar.registryImageArchitecture = getValueFromAnnotation(pvc, cc.AnnRegistryImageArchitecture)
1✔
621

1✔
622
                for annotation, value := range pvc.Annotations {
2✔
623
                        if strings.HasPrefix(annotation, cc.AnnExtraHeaders) {
1✔
624
                                podEnvVar.extraHeaders = append(podEnvVar.extraHeaders, value)
×
625
                        }
×
626
                        if strings.HasPrefix(annotation, cc.AnnSecretExtraHeaders) {
1✔
627
                                podEnvVar.secretExtraHeaders = append(podEnvVar.secretExtraHeaders, value)
×
628
                        }
×
629
                }
630

631
                var field string
1✔
632
                if field, err = GetImportProxyConfig(cdiConfig, common.ImportProxyHTTP); err != nil {
2✔
633
                        r.log.V(3).Info("no proxy http url will be supplied:", "error", err.Error())
1✔
634
                }
1✔
635
                podEnvVar.httpProxy = field
1✔
636
                if field, err = GetImportProxyConfig(cdiConfig, common.ImportProxyHTTPS); err != nil {
2✔
637
                        r.log.V(3).Info("no proxy https url will be supplied:", "error", err.Error())
1✔
638
                }
1✔
639
                podEnvVar.httpsProxy = field
1✔
640
                if field, err = GetImportProxyConfig(cdiConfig, common.ImportProxyNoProxy); err != nil {
2✔
641
                        r.log.V(3).Info("the noProxy field will not be supplied:", "error", err.Error())
1✔
642
                }
1✔
643
                podEnvVar.noProxy = field
1✔
644
                if field, err = GetImportProxyConfig(cdiConfig, common.ImportProxyConfigMapName); err != nil {
2✔
645
                        r.log.V(3).Info("no proxy CA certiticate will be supplied:", "error", err.Error())
1✔
646
                }
1✔
647
                podEnvVar.certConfigMapProxy = field
1✔
648
        }
649

650
        fsOverhead, err := GetFilesystemOverhead(context.TODO(), r.client, pvc)
1✔
651
        if err != nil {
1✔
652
                return nil, err
×
653
        }
×
654
        podEnvVar.filesystemOverhead = string(fsOverhead)
1✔
655

1✔
656
        if preallocation, err := strconv.ParseBool(getValueFromAnnotation(pvc, cc.AnnPreallocationRequested)); err == nil {
1✔
657
                podEnvVar.preallocation = preallocation
×
658
        } // else use the default "false"
×
659

660
        //get the requested image size.
661
        podEnvVar.imageSize, err = cc.GetRequestedImageSize(pvc)
1✔
662
        if err != nil {
1✔
663
                return nil, err
×
664
        }
×
665

666
        if v, ok := pvc.Annotations[cc.AnnRequiresDirectIO]; ok && v == "true" {
2✔
667
                podEnvVar.cacheMode = common.CacheModeTryNone
1✔
668
        }
1✔
669

670
        return podEnvVar, nil
1✔
671
}
672

673
func (r *ImportReconciler) isInsecureTLS(pvc *corev1.PersistentVolumeClaim, cdiConfig *cdiv1.CDIConfig) (bool, error) {
1✔
674
        ep, ok := pvc.Annotations[cc.AnnEndpoint]
1✔
675
        if !ok || ep == "" {
2✔
676
                return false, nil
1✔
677
        }
1✔
678
        return IsInsecureTLS(ep, cdiConfig, r.log)
1✔
679
}
680

681
// IsInsecureTLS checks if TLS security is disabled for the given endpoint
682
func IsInsecureTLS(ep string, cdiConfig *cdiv1.CDIConfig, log logr.Logger) (bool, error) {
1✔
683
        url, err := url.Parse(ep)
1✔
684
        if err != nil {
1✔
685
                return false, err
×
686
        }
×
687

688
        if url.Scheme != "docker" {
2✔
689
                return false, nil
1✔
690
        }
1✔
691

692
        for _, value := range cdiConfig.Spec.InsecureRegistries {
2✔
693
                log.V(1).Info("Checking host against value", "host", url.Host, "value", value)
1✔
694
                if value == url.Host {
2✔
695
                        return true, nil
1✔
696
                }
1✔
697
        }
698
        return false, nil
1✔
699
}
700

701
func (r *ImportReconciler) getCertConfigMap(pvc *corev1.PersistentVolumeClaim) (string, error) {
1✔
702
        value, ok := pvc.Annotations[cc.AnnCertConfigMap]
1✔
703
        if !ok || value == "" {
2✔
704
                return "", nil
1✔
705
        }
1✔
706

707
        configMap := &corev1.ConfigMap{}
1✔
708
        if err := r.uncachedClient.Get(context.TODO(), types.NamespacedName{Name: value, Namespace: pvc.Namespace}, configMap); err != nil {
2✔
709
                if k8serrors.IsNotFound(err) {
2✔
710
                        r.log.V(1).Info("Configmap does not exist, pod will not start until it does", "configMapName", value)
1✔
711
                        return value, nil
1✔
712
                }
1✔
713

714
                return "", err
×
715
        }
716

717
        return value, nil
1✔
718
}
719

720
// returns the name of the secret containing endpoint credentials consumed by the importer pod.
721
// A value of "" implies there are no credentials for the endpoint being used. A returned error
722
// causes processNextItem() to stop.
723
func (r *ImportReconciler) getSecretName(pvc *corev1.PersistentVolumeClaim) string {
1✔
724
        ns := pvc.Namespace
1✔
725
        name, found := pvc.Annotations[cc.AnnSecret]
1✔
726
        if !found || name == "" {
2✔
727
                msg := "getEndpointSecret: "
1✔
728
                if !found {
2✔
729
                        msg += fmt.Sprintf("annotation %q is missing in pvc \"%s/%s\"", cc.AnnSecret, ns, pvc.Name)
1✔
730
                } else {
1✔
731
                        msg += fmt.Sprintf("secret name is missing from annotation %q in pvc \"%s/%s\"", cc.AnnSecret, ns, pvc.Name)
×
732
                }
×
733
                r.log.V(2).Info(msg)
1✔
734
                return "" // importer pod will not contain secret credentials
1✔
735
        }
736
        return name
1✔
737
}
738

739
func (r *ImportReconciler) requiresScratchSpace(pvc *corev1.PersistentVolumeClaim) bool {
1✔
740
        scratchRequired := false
1✔
741
        contentType := cc.GetPVCContentType(pvc)
1✔
742
        // All archive requires scratch space.
1✔
743
        if contentType == cdiv1.DataVolumeArchive {
1✔
744
                scratchRequired = true
×
745
        } else {
1✔
746
                switch cc.GetSource(pvc) {
1✔
747
                case cc.SourceGlance:
×
748
                        scratchRequired = true
×
749
                case cc.SourceImageio:
×
750
                        if val, ok := pvc.Annotations[cc.AnnCurrentCheckpoint]; ok {
×
751
                                scratchRequired = val != ""
×
752
                        }
×
753
                case cc.SourceRegistry:
1✔
754
                        scratchRequired = pvc.Annotations[cc.AnnRegistryImportMethod] != string(cdiv1.RegistryPullNode)
1✔
755
                }
756
        }
757
        value, ok := pvc.Annotations[cc.AnnRequiresScratch]
1✔
758
        if ok {
2✔
759
                boolVal, _ := strconv.ParseBool(value)
1✔
760
                scratchRequired = scratchRequired || boolVal
1✔
761
        }
1✔
762
        return scratchRequired
1✔
763
}
764

765
func (r *ImportReconciler) createScratchPvcForPod(pvc *corev1.PersistentVolumeClaim, pod *corev1.Pod) error {
1✔
766
        scratchPvc := &corev1.PersistentVolumeClaim{}
1✔
767
        scratchPVCName, exists := getScratchNameFromPod(pod)
1✔
768
        if !exists {
1✔
769
                return errors.New("Scratch Volume not configured for pod")
×
770
        }
×
771
        anno := pvc.GetAnnotations()
1✔
772
        err := r.client.Get(context.TODO(), types.NamespacedName{Namespace: pvc.GetNamespace(), Name: scratchPVCName}, scratchPvc)
1✔
773
        if cc.IgnoreNotFound(err) != nil {
1✔
774
                return err
×
775
        }
×
776
        if k8serrors.IsNotFound(err) {
2✔
777
                r.log.V(1).Info("Creating scratch space for POD and PVC", "pod.Name", pod.Name, "pvc.Name", pvc.Name)
1✔
778

1✔
779
                storageClassName := GetScratchPvcStorageClass(r.client, pvc)
1✔
780
                // Scratch PVC doesn't exist yet, create it. Determine which storage class to use.
1✔
781
                _, err = createScratchPersistentVolumeClaim(r.client, pvc, pod, scratchPVCName, storageClassName, r.installerLabels, r.recorder)
1✔
782
                if err != nil {
1✔
783
                        return err
×
784
                }
×
785
                anno[cc.AnnBoundCondition] = "false"
1✔
786
                anno[cc.AnnBoundConditionMessage] = "Creating scratch space"
1✔
787
                anno[cc.AnnBoundConditionReason] = creatingScratch
1✔
788
        } else {
×
789
                if scratchPvc.DeletionTimestamp != nil {
×
790
                        // Delete the pod since we are in a deadlock situation now. The scratch PVC from the previous import is not gone
×
791
                        // yet but terminating, and the new pod is still being created and the scratch PVC now has a finalizer on it.
×
792
                        // Only way to break it, is to delete the importer pod, and give the pvc a chance to disappear.
×
793
                        err = r.client.Delete(context.TODO(), pod)
×
794
                        if err != nil {
×
795
                                return err
×
796
                        }
×
797
                        return fmt.Errorf("terminating scratch space found, deleting pod %s", pod.Name)
×
798
                }
799
        }
800
        anno[cc.AnnRequiresScratch] = "false"
1✔
801
        return nil
1✔
802
}
803

804
// Get path to VDDK image from 'v2v-vmware' ConfigMap
805
func (r *ImportReconciler) getVddkImageName() (*string, error) {
1✔
806
        namespace := util.GetNamespace()
1✔
807

1✔
808
        cm := &corev1.ConfigMap{}
1✔
809
        err := r.uncachedClient.Get(context.TODO(), types.NamespacedName{Name: common.VddkConfigMap, Namespace: namespace}, cm)
1✔
810
        if k8serrors.IsNotFound(err) {
2✔
811
                return nil, errors.Errorf("No %s ConfigMap present in namespace %s", common.VddkConfigMap, namespace)
1✔
812
        }
1✔
813

814
        image, found := cm.Data[common.VddkConfigDataKey]
1✔
815
        if found {
2✔
816
                msg := fmt.Sprintf("Found %s ConfigMap in namespace %s, VDDK image path is: ", common.VddkConfigMap, namespace)
1✔
817
                r.log.V(1).Info(msg, common.VddkConfigDataKey, image)
1✔
818
                return &image, nil
1✔
819
        }
1✔
820

821
        return nil, errors.Errorf("found %s ConfigMap in namespace %s, but it does not contain a '%s' entry", common.VddkConfigMap, namespace, common.VddkConfigDataKey)
×
822
}
823

824
// returns the import image part of the endpoint string
825
func getRegistryImportImage(pvc *corev1.PersistentVolumeClaim) (string, error) {
1✔
826
        ep, err := cc.GetEndpoint(pvc)
1✔
827
        if err != nil {
1✔
828
                return "", nil
×
829
        }
×
830
        if cc.IsImageStream(pvc) {
1✔
831
                return ep, nil
×
832
        }
×
833
        url, err := url.Parse(ep)
1✔
834
        if err != nil {
1✔
835
                return "", errors.Errorf("illegal registry endpoint %s", ep)
×
836
        }
×
837
        return url.Host + url.Path, nil
1✔
838
}
839

840
// getValueFromAnnotation returns the value of an annotation
841
func getValueFromAnnotation(pvc *corev1.PersistentVolumeClaim, annotation string) string {
1✔
842
        return pvc.Annotations[annotation]
1✔
843
}
1✔
844

845
// If this pod is going to transfer one checkpoint in a multi-stage import, attach the checkpoint name to the pod name so
846
// that each checkpoint gets a unique pod. That way each pod can be inspected using the retainAfterCompletion annotation.
847
func podNameWithCheckpoint(pvc *corev1.PersistentVolumeClaim) string {
1✔
848
        if checkpoint := pvc.Annotations[cc.AnnCurrentCheckpoint]; checkpoint != "" {
2✔
849
                return pvc.Name + "-checkpoint-" + checkpoint
1✔
850
        }
1✔
851
        return pvc.Name
1✔
852
}
853

854
func getImportPodNameFromPvc(pvc *corev1.PersistentVolumeClaim) string {
1✔
855
        podName, ok := pvc.Annotations[cc.AnnImportPod]
1✔
856
        if ok {
2✔
857
                return podName
1✔
858
        }
1✔
859
        // fallback to legacy naming, in fact the following function is fully compatible with legacy
860
        // name concatenation "importer-{pvc.Name}" if the name length is under the size limits,
861
        return naming.GetResourceName(common.ImporterPodName, podNameWithCheckpoint(pvc))
1✔
862
}
863

864
func createImportPodNameFromPvc(pvc *corev1.PersistentVolumeClaim) string {
1✔
865
        return naming.GetResourceName(common.ImporterPodName, podNameWithCheckpoint(pvc))
1✔
866
}
1✔
867

868
// createImporterPod creates and returns a pointer to a pod which is created based on the passed-in endpoint, secret
869
// name, and pvc. A nil secret means the endpoint credentials are not passed to the
870
// importer pod.
871
func createImporterPod(ctx context.Context, log logr.Logger, client client.Client, args *importerPodArgs, installerLabels map[string]string) (*corev1.Pod, error) {
1✔
872
        var err error
1✔
873
        args.podResourceRequirements, err = cc.GetDefaultPodResourceRequirements(client)
1✔
874
        if err != nil {
1✔
875
                return nil, err
×
876
        }
×
877

878
        args.imagePullSecrets, err = cc.GetImagePullSecrets(client)
1✔
879
        if err != nil {
1✔
880
                return nil, err
×
881
        }
×
882

883
        args.workloadNodePlacement, err = cc.GetWorkloadNodePlacement(ctx, client)
1✔
884
        if err != nil {
1✔
885
                return nil, err
×
886
        }
×
887

888
        if isRegistryNodeImport(args) {
2✔
889
                args.importImage, err = getRegistryImportImage(args.pvc)
1✔
890
                if err != nil {
1✔
891
                        return nil, err
×
892
                }
×
893
                setRegistryNodeImportEnvVars(args)
1✔
894
                if args.podEnvVar.registryImageArchitecture != "" {
1✔
895
                        setRegistryNodeImportNodeSelector(args)
×
896
                }
×
897
        }
898

899
        pod := makeImporterPodSpec(args)
1✔
900

1✔
901
        util.SetRecommendedLabels(pod, installerLabels, "cdi-controller")
1✔
902

1✔
903
        // add any labels from pvc to the importer pod
1✔
904
        util.MergeLabels(args.pvc.Labels, pod.Labels)
1✔
905

1✔
906
        if err = client.Create(context.TODO(), pod); err != nil {
1✔
907
                return nil, err
×
908
        }
×
909

910
        log.V(3).Info("importer pod created\n", "pod.Name", pod.Name, "pod.Namespace", pod.Namespace, "image name", args.image)
1✔
911
        return pod, nil
1✔
912
}
913

914
// makeImporterPodSpec creates and return the importer pod spec based on the passed-in endpoint, secret and pvc.
915
func makeImporterPodSpec(args *importerPodArgs) *corev1.Pod {
1✔
916
        // importer pod name contains the pvc name
1✔
917
        podName := args.pvc.Annotations[cc.AnnImportPod]
1✔
918

1✔
919
        pod := &corev1.Pod{
1✔
920
                TypeMeta: metav1.TypeMeta{
1✔
921
                        Kind:       "Pod",
1✔
922
                        APIVersion: "v1",
1✔
923
                },
1✔
924
                ObjectMeta: metav1.ObjectMeta{
1✔
925
                        Name:      podName,
1✔
926
                        Namespace: args.pvc.Namespace,
1✔
927
                        Annotations: map[string]string{
1✔
928
                                cc.AnnCreatedBy: "yes",
1✔
929
                        },
1✔
930
                        Labels: map[string]string{
1✔
931
                                common.CDILabelKey:        common.CDILabelValue,
1✔
932
                                common.CDIComponentLabel:  common.ImporterPodName,
1✔
933
                                common.PrometheusLabelKey: common.PrometheusLabelValue,
1✔
934
                        },
1✔
935
                        OwnerReferences: []metav1.OwnerReference{
1✔
936
                                {
1✔
937
                                        APIVersion:         "v1",
1✔
938
                                        Kind:               "PersistentVolumeClaim",
1✔
939
                                        Name:               args.pvc.Name,
1✔
940
                                        UID:                args.pvc.GetUID(),
1✔
941
                                        BlockOwnerDeletion: ptr.To[bool](true),
1✔
942
                                        Controller:         ptr.To[bool](true),
1✔
943
                                },
1✔
944
                        },
1✔
945
                },
1✔
946
                Spec: corev1.PodSpec{
1✔
947
                        Containers:        makeImporterContainerSpec(args),
1✔
948
                        InitContainers:    makeImporterInitContainersSpec(args),
1✔
949
                        Volumes:           makeImporterVolumeSpec(args),
1✔
950
                        RestartPolicy:     corev1.RestartPolicyOnFailure,
1✔
951
                        NodeSelector:      args.workloadNodePlacement.NodeSelector,
1✔
952
                        Tolerations:       args.workloadNodePlacement.Tolerations,
1✔
953
                        Affinity:          args.workloadNodePlacement.Affinity,
1✔
954
                        PriorityClassName: args.priorityClassName,
1✔
955
                        ImagePullSecrets:  args.imagePullSecrets,
1✔
956
                },
1✔
957
        }
1✔
958

1✔
959
        /**
1✔
960
        FIXME: When registry source is ImageStream, if we set importer pod OwnerReference (to its pvc, like all other cases),
1✔
961
        for some reason (OCP issue?) we get the following error:
1✔
962
                Failed to pull image "imagestream-name": rpc error: code = Unknown
1✔
963
                desc = Error reading manifest latest in docker.io/library/imagestream-name: errors:
1✔
964
                denied: requested access to the resource is denied
1✔
965
                unauthorized: authentication required
1✔
966
        When we don't set pod OwnerReferences, all works well.
1✔
967
        */
1✔
968
        if isRegistryNodeImport(args) && cc.IsImageStream(args.pvc) {
1✔
969
                pod.OwnerReferences = nil
×
970
                pod.Annotations[cc.AnnOpenShiftImageLookup] = "*"
×
971
        }
×
972

973
        cc.CopyAllowedAnnotations(args.pvc, pod)
1✔
974
        cc.SetRestrictedSecurityContext(&pod.Spec)
1✔
975
        // We explicitly define a NodeName for dynamically provisioned PVCs
1✔
976
        // when the PVC is being handled by a populator (PVC')
1✔
977
        cc.SetNodeNameIfPopulator(args.pvc, &pod.Spec)
1✔
978

1✔
979
        return pod
1✔
980
}
981

982
func makeImporterContainerSpec(args *importerPodArgs) []corev1.Container {
1✔
983
        containers := []corev1.Container{
1✔
984
                {
1✔
985
                        Name:            common.ImporterPodName,
1✔
986
                        Image:           args.image,
1✔
987
                        ImagePullPolicy: corev1.PullPolicy(args.pullPolicy),
1✔
988
                        Args:            []string{"-v=" + args.verbose},
1✔
989
                        Env:             makeImportEnv(args.podEnvVar, getOwnerUID(args)),
1✔
990
                        Ports: []corev1.ContainerPort{
1✔
991
                                {
1✔
992
                                        Name:          "metrics",
1✔
993
                                        ContainerPort: 8443,
1✔
994
                                        Protocol:      corev1.ProtocolTCP,
1✔
995
                                },
1✔
996
                        },
1✔
997
                        TerminationMessagePolicy: corev1.TerminationMessageFallbackToLogsOnError,
1✔
998
                },
1✔
999
        }
1✔
1000
        if cc.GetVolumeMode(args.pvc) == corev1.PersistentVolumeBlock {
2✔
1001
                containers[0].VolumeDevices = cc.AddVolumeDevices()
1✔
1002
        } else {
2✔
1003
                containers[0].VolumeMounts = cc.AddImportVolumeMounts()
1✔
1004
        }
1✔
1005
        if isRegistryNodeImport(args) {
2✔
1006
                containers = append(containers, corev1.Container{
1✔
1007
                        Name:            "server",
1✔
1008
                        Image:           args.importImage,
1✔
1009
                        ImagePullPolicy: corev1.PullPolicy(args.pullPolicy),
1✔
1010
                        Command:         []string{"/shared/server", "-p", "8100", "-image-dir", "/disk", "-ready-file", "/shared/ready", "-done-file", "/shared/done"},
1✔
1011
                        VolumeMounts: []corev1.VolumeMount{
1✔
1012
                                {
1✔
1013
                                        MountPath: "/shared",
1✔
1014
                                        Name:      "shared-volume",
1✔
1015
                                },
1✔
1016
                        },
1✔
1017
                        TerminationMessagePolicy: corev1.TerminationMessageFallbackToLogsOnError,
1✔
1018
                })
1✔
1019
                containers[0].VolumeMounts = append(containers[0].VolumeMounts, corev1.VolumeMount{
1✔
1020
                        MountPath: "/shared",
1✔
1021
                        Name:      "shared-volume",
1✔
1022
                })
1✔
1023
        }
1✔
1024
        if args.scratchPvcName != nil {
2✔
1025
                containers[0].VolumeMounts = append(containers[0].VolumeMounts, corev1.VolumeMount{
1✔
1026
                        Name:      cc.ScratchVolName,
1✔
1027
                        MountPath: common.ScratchDataDir,
1✔
1028
                })
1✔
1029
        }
1✔
1030
        if args.vddkImageName != nil {
2✔
1031
                containers[0].VolumeMounts = append(containers[0].VolumeMounts, corev1.VolumeMount{
1✔
1032
                        Name:      "vddk-vol-mount",
1✔
1033
                        MountPath: "/opt",
1✔
1034
                })
1✔
1035
        }
1✔
1036
        if args.vddkExtraArgs != nil {
2✔
1037
                containers[0].VolumeMounts = append(containers[0].VolumeMounts, corev1.VolumeMount{
1✔
1038
                        Name:      common.VddkArgsVolName,
1✔
1039
                        MountPath: common.VddkArgsDir,
1✔
1040
                })
1✔
1041
        }
1✔
1042
        if args.podEnvVar.certConfigMap != "" {
1✔
1043
                containers[0].VolumeMounts = append(containers[0].VolumeMounts, corev1.VolumeMount{
×
1044
                        Name:      CertVolName,
×
1045
                        MountPath: common.ImporterCertDir,
×
1046
                })
×
1047
        }
×
1048
        if args.podEnvVar.certConfigMapProxy != "" {
1✔
1049
                containers[0].VolumeMounts = append(containers[0].VolumeMounts, corev1.VolumeMount{
×
1050
                        Name:      ProxyCertVolName,
×
1051
                        MountPath: common.ImporterProxyCertDir,
×
1052
                })
×
1053
        }
×
1054
        if args.podEnvVar.source == cc.SourceGCS && args.podEnvVar.secretName != "" {
1✔
1055
                containers[0].VolumeMounts = append(containers[0].VolumeMounts, corev1.VolumeMount{
×
1056
                        Name:      SecretVolName,
×
1057
                        MountPath: common.ImporterGoogleCredentialDir,
×
1058
                })
×
1059
        }
×
1060
        for index := range args.podEnvVar.secretExtraHeaders {
1✔
1061
                containers[0].VolumeMounts = append(containers[0].VolumeMounts, corev1.VolumeMount{
×
1062
                        Name:      fmt.Sprintf(secretExtraHeadersVolumeName, index),
×
1063
                        MountPath: path.Join(common.ImporterSecretExtraHeadersDir, fmt.Sprint(index)),
×
1064
                })
×
1065
        }
×
1066
        if args.podResourceRequirements != nil {
1✔
1067
                for i := range containers {
×
1068
                        containers[i].Resources = *args.podResourceRequirements
×
1069
                }
×
1070
        }
1071
        return containers
1✔
1072
}
1073

1074
func makeImporterVolumeSpec(args *importerPodArgs) []corev1.Volume {
1✔
1075
        volumes := []corev1.Volume{
1✔
1076
                {
1✔
1077
                        Name: cc.DataVolName,
1✔
1078
                        VolumeSource: corev1.VolumeSource{
1✔
1079
                                PersistentVolumeClaim: &corev1.PersistentVolumeClaimVolumeSource{
1✔
1080
                                        ClaimName: args.pvc.Name,
1✔
1081
                                        ReadOnly:  false,
1✔
1082
                                },
1✔
1083
                        },
1✔
1084
                },
1✔
1085
        }
1✔
1086
        if isRegistryNodeImport(args) {
2✔
1087
                volumes = append(volumes, corev1.Volume{
1✔
1088
                        Name: "shared-volume",
1✔
1089
                        VolumeSource: corev1.VolumeSource{
1✔
1090
                                EmptyDir: &corev1.EmptyDirVolumeSource{},
1✔
1091
                        },
1✔
1092
                })
1✔
1093
        }
1✔
1094
        if args.scratchPvcName != nil {
2✔
1095
                volumes = append(volumes, corev1.Volume{
1✔
1096
                        Name: cc.ScratchVolName,
1✔
1097
                        VolumeSource: corev1.VolumeSource{
1✔
1098
                                PersistentVolumeClaim: &corev1.PersistentVolumeClaimVolumeSource{
1✔
1099
                                        ClaimName: *args.scratchPvcName,
1✔
1100
                                        ReadOnly:  false,
1✔
1101
                                },
1✔
1102
                        },
1✔
1103
                })
1✔
1104
        }
1✔
1105
        if args.vddkImageName != nil {
2✔
1106
                volumes = append(volumes, corev1.Volume{
1✔
1107
                        Name: "vddk-vol-mount",
1✔
1108
                        VolumeSource: corev1.VolumeSource{
1✔
1109
                                EmptyDir: &corev1.EmptyDirVolumeSource{},
1✔
1110
                        },
1✔
1111
                })
1✔
1112
        }
1✔
1113
        if args.vddkExtraArgs != nil {
2✔
1114
                volumes = append(volumes, corev1.Volume{
1✔
1115
                        Name: common.VddkArgsVolName,
1✔
1116
                        VolumeSource: corev1.VolumeSource{
1✔
1117
                                ConfigMap: &v1.ConfigMapVolumeSource{
1✔
1118
                                        LocalObjectReference: v1.LocalObjectReference{
1✔
1119
                                                Name: *args.vddkExtraArgs,
1✔
1120
                                        },
1✔
1121
                                },
1✔
1122
                        },
1✔
1123
                })
1✔
1124
        }
1✔
1125
        if args.podEnvVar.certConfigMap != "" {
1✔
1126
                volumes = append(volumes, createConfigMapVolume(CertVolName, args.podEnvVar.certConfigMap))
×
1127
        }
×
1128
        if args.podEnvVar.certConfigMapProxy != "" {
1✔
1129
                volumes = append(volumes, createConfigMapVolume(ProxyCertVolName, GetImportProxyConfigMapName(args.pvc.Name)))
×
1130
        }
×
1131
        if args.podEnvVar.source == cc.SourceGCS && args.podEnvVar.secretName != "" {
1✔
1132
                volumes = append(volumes, createSecretVolume(SecretVolName, args.podEnvVar.secretName))
×
1133
        }
×
1134
        for index, header := range args.podEnvVar.secretExtraHeaders {
1✔
1135
                volumes = append(volumes, corev1.Volume{
×
1136
                        Name: fmt.Sprintf(secretExtraHeadersVolumeName, index),
×
1137
                        VolumeSource: corev1.VolumeSource{
×
1138
                                Secret: &corev1.SecretVolumeSource{
×
1139
                                        SecretName: header,
×
1140
                                },
×
1141
                        },
×
1142
                })
×
1143
        }
×
1144
        return volumes
1✔
1145
}
1146

1147
func makeImporterInitContainersSpec(args *importerPodArgs) []corev1.Container {
1✔
1148
        var initContainers []corev1.Container
1✔
1149
        if isRegistryNodeImport(args) {
2✔
1150
                initContainers = append(initContainers, corev1.Container{
1✔
1151
                        Name:            "init",
1✔
1152
                        Image:           args.image,
1✔
1153
                        ImagePullPolicy: corev1.PullPolicy(args.pullPolicy),
1✔
1154
                        Command:         []string{"sh", "-c", "cp /usr/bin/cdi-containerimage-server /shared/server"},
1✔
1155
                        VolumeMounts: []corev1.VolumeMount{
1✔
1156
                                {
1✔
1157
                                        MountPath: "/shared",
1✔
1158
                                        Name:      "shared-volume",
1✔
1159
                                },
1✔
1160
                        },
1✔
1161
                        TerminationMessagePolicy: corev1.TerminationMessageFallbackToLogsOnError,
1✔
1162
                })
1✔
1163
        }
1✔
1164
        if args.vddkImageName != nil {
2✔
1165
                initContainers = append(initContainers, corev1.Container{
1✔
1166
                        Name:  "vddk-side-car",
1✔
1167
                        Image: *args.vddkImageName,
1✔
1168
                        VolumeMounts: []corev1.VolumeMount{
1✔
1169
                                {
1✔
1170
                                        Name:      "vddk-vol-mount",
1✔
1171
                                        MountPath: "/opt",
1✔
1172
                                },
1✔
1173
                        },
1✔
1174
                        TerminationMessagePolicy: corev1.TerminationMessageFallbackToLogsOnError,
1✔
1175
                })
1✔
1176
        }
1✔
1177
        if args.podResourceRequirements != nil {
1✔
1178
                for i := range initContainers {
×
1179
                        initContainers[i].Resources = *args.podResourceRequirements
×
1180
                }
×
1181
        }
1182
        return initContainers
1✔
1183
}
1184

1185
func isRegistryNodeImport(args *importerPodArgs) bool {
1✔
1186
        return cc.GetSource(args.pvc) == cc.SourceRegistry &&
1✔
1187
                args.pvc.Annotations[cc.AnnRegistryImportMethod] == string(cdiv1.RegistryPullNode)
1✔
1188
}
1✔
1189

1190
func getOwnerUID(args *importerPodArgs) types.UID {
1✔
1191
        if len(args.pvc.OwnerReferences) == 1 {
1✔
1192
                return args.pvc.OwnerReferences[0].UID
×
1193
        }
×
1194
        return args.pvc.UID
1✔
1195
}
1196

1197
func setRegistryNodeImportEnvVars(args *importerPodArgs) {
1✔
1198
        args.podEnvVar.source = cc.SourceHTTP
1✔
1199
        args.podEnvVar.ep = "http://localhost:8100/disk.img"
1✔
1200
        args.podEnvVar.pullMethod = string(cdiv1.RegistryPullNode)
1✔
1201
        args.podEnvVar.readyFile = "/shared/ready"
1✔
1202
        args.podEnvVar.doneFile = "/shared/done"
1✔
1203
}
1✔
1204

1205
func setRegistryNodeImportNodeSelector(args *importerPodArgs) {
×
1206
        if args.workloadNodePlacement.NodeSelector == nil {
×
1207
                args.workloadNodePlacement.NodeSelector = make(map[string]string, 0)
×
1208
        }
×
1209
        args.workloadNodePlacement.NodeSelector[v1.LabelArchStable] = args.podEnvVar.registryImageArchitecture
×
1210
}
1211

1212
func createConfigMapVolume(certVolName, objRef string) corev1.Volume {
1✔
1213
        return corev1.Volume{
1✔
1214
                Name: certVolName,
1✔
1215
                VolumeSource: corev1.VolumeSource{
1✔
1216
                        ConfigMap: &corev1.ConfigMapVolumeSource{
1✔
1217
                                LocalObjectReference: corev1.LocalObjectReference{
1✔
1218
                                        Name: objRef,
1✔
1219
                                },
1✔
1220
                        },
1✔
1221
                },
1✔
1222
        }
1✔
1223
}
1✔
1224

1225
func createSecretVolume(thisVolName, objRef string) corev1.Volume {
×
1226
        return corev1.Volume{
×
1227
                Name: thisVolName,
×
1228
                VolumeSource: corev1.VolumeSource{
×
1229
                        Secret: &corev1.SecretVolumeSource{
×
1230
                                SecretName: objRef,
×
1231
                        },
×
1232
                },
×
1233
        }
×
1234
}
×
1235

1236
// return the Env portion for the importer container.
1237
func makeImportEnv(podEnvVar *importPodEnvVar, uid types.UID) []corev1.EnvVar {
1✔
1238
        env := []corev1.EnvVar{
1✔
1239
                {
1✔
1240
                        Name:  common.ImporterSource,
1✔
1241
                        Value: podEnvVar.source,
1✔
1242
                },
1✔
1243
                {
1✔
1244
                        Name:  common.ImporterEndpoint,
1✔
1245
                        Value: podEnvVar.ep,
1✔
1246
                },
1✔
1247
                {
1✔
1248
                        Name:  common.ImporterContentType,
1✔
1249
                        Value: podEnvVar.contentType,
1✔
1250
                },
1✔
1251
                {
1✔
1252
                        Name:  common.ImporterImageSize,
1✔
1253
                        Value: podEnvVar.imageSize,
1✔
1254
                },
1✔
1255
                {
1✔
1256
                        Name:  common.OwnerUID,
1✔
1257
                        Value: string(uid),
1✔
1258
                },
1✔
1259
                {
1✔
1260
                        Name:  common.FilesystemOverheadVar,
1✔
1261
                        Value: podEnvVar.filesystemOverhead,
1✔
1262
                },
1✔
1263
                {
1✔
1264
                        Name:  common.InsecureTLSVar,
1✔
1265
                        Value: strconv.FormatBool(podEnvVar.insecureTLS),
1✔
1266
                },
1✔
1267
                {
1✔
1268
                        Name:  common.ImporterDiskID,
1✔
1269
                        Value: podEnvVar.diskID,
1✔
1270
                },
1✔
1271
                {
1✔
1272
                        Name:  common.ImporterUUID,
1✔
1273
                        Value: podEnvVar.uuid,
1✔
1274
                },
1✔
1275
                {
1✔
1276
                        Name:  common.ImporterPullMethod,
1✔
1277
                        Value: podEnvVar.pullMethod,
1✔
1278
                },
1✔
1279
                {
1✔
1280
                        Name:  common.ImporterReadyFile,
1✔
1281
                        Value: podEnvVar.readyFile,
1✔
1282
                },
1✔
1283
                {
1✔
1284
                        Name:  common.ImporterDoneFile,
1✔
1285
                        Value: podEnvVar.doneFile,
1✔
1286
                },
1✔
1287
                {
1✔
1288
                        Name:  common.ImporterBackingFile,
1✔
1289
                        Value: podEnvVar.backingFile,
1✔
1290
                },
1✔
1291
                {
1✔
1292
                        Name:  common.ImporterThumbprint,
1✔
1293
                        Value: podEnvVar.thumbprint,
1✔
1294
                },
1✔
1295
                {
1✔
1296
                        Name:  common.ImportProxyHTTP,
1✔
1297
                        Value: podEnvVar.httpProxy,
1✔
1298
                },
1✔
1299
                {
1✔
1300
                        Name:  common.ImportProxyHTTPS,
1✔
1301
                        Value: podEnvVar.httpsProxy,
1✔
1302
                },
1✔
1303
                {
1✔
1304
                        Name:  common.ImportProxyNoProxy,
1✔
1305
                        Value: podEnvVar.noProxy,
1✔
1306
                },
1✔
1307
                {
1✔
1308
                        Name:  common.ImporterCurrentCheckpoint,
1✔
1309
                        Value: podEnvVar.currentCheckpoint,
1✔
1310
                },
1✔
1311
                {
1✔
1312
                        Name:  common.ImporterPreviousCheckpoint,
1✔
1313
                        Value: podEnvVar.previousCheckpoint,
1✔
1314
                },
1✔
1315
                {
1✔
1316
                        Name:  common.ImporterFinalCheckpoint,
1✔
1317
                        Value: podEnvVar.finalCheckpoint,
1✔
1318
                },
1✔
1319
                {
1✔
1320
                        Name:  common.Preallocation,
1✔
1321
                        Value: strconv.FormatBool(podEnvVar.preallocation),
1✔
1322
                },
1✔
1323
                {
1✔
1324
                        Name:  common.CacheMode,
1✔
1325
                        Value: podEnvVar.cacheMode,
1✔
1326
                },
1✔
1327
                {
1✔
1328
                        Name:  common.ImporterRegistryImageArchitecture,
1✔
1329
                        Value: podEnvVar.registryImageArchitecture,
1✔
1330
                },
1✔
1331
        }
1✔
1332
        if podEnvVar.secretName != "" && podEnvVar.source != cc.SourceGCS {
1✔
1333
                env = append(env, corev1.EnvVar{
×
1334
                        Name: common.ImporterAccessKeyID,
×
1335
                        ValueFrom: &corev1.EnvVarSource{
×
1336
                                SecretKeyRef: &corev1.SecretKeySelector{
×
1337
                                        LocalObjectReference: corev1.LocalObjectReference{
×
1338
                                                Name: podEnvVar.secretName,
×
1339
                                        },
×
1340
                                        Key: common.KeyAccess,
×
1341
                                },
×
1342
                        },
×
1343
                }, corev1.EnvVar{
×
1344
                        Name: common.ImporterSecretKey,
×
1345
                        ValueFrom: &corev1.EnvVarSource{
×
1346
                                SecretKeyRef: &corev1.SecretKeySelector{
×
1347
                                        LocalObjectReference: corev1.LocalObjectReference{
×
1348
                                                Name: podEnvVar.secretName,
×
1349
                                        },
×
1350
                                        Key: common.KeySecret,
×
1351
                                },
×
1352
                        },
×
1353
                })
×
1354
        }
×
1355
        if podEnvVar.secretName != "" && podEnvVar.source == cc.SourceGCS {
1✔
1356
                env = append(env, corev1.EnvVar{
×
1357
                        Name:  common.ImporterGoogleCredentialFileVar,
×
1358
                        Value: common.ImporterGoogleCredentialFile,
×
1359
                })
×
1360
        }
×
1361
        if podEnvVar.certConfigMap != "" {
1✔
1362
                env = append(env, corev1.EnvVar{
×
1363
                        Name:  common.ImporterCertDirVar,
×
1364
                        Value: common.ImporterCertDir,
×
1365
                })
×
1366
        }
×
1367
        if podEnvVar.certConfigMapProxy != "" {
1✔
1368
                env = append(env, corev1.EnvVar{
×
1369
                        Name:  common.ImporterProxyCertDirVar,
×
1370
                        Value: common.ImporterProxyCertDir,
×
1371
                })
×
1372
        }
×
1373
        for index, header := range podEnvVar.extraHeaders {
1✔
1374
                env = append(env, corev1.EnvVar{
×
1375
                        Name:  fmt.Sprintf("%s%d", common.ImporterExtraHeader, index),
×
1376
                        Value: header,
×
1377
                })
×
1378
        }
×
1379
        return env
1✔
1380
}
1381

1382
func isOOMKilled(status v1.ContainerStatus) bool {
1✔
1383
        if terminated := status.State.Terminated; terminated != nil {
2✔
1384
                if terminated.Reason == cc.OOMKilledReason {
2✔
1385
                        return true
1✔
1386
                }
1✔
1387
        }
1388
        if terminated := status.LastTerminationState.Terminated; terminated != nil {
2✔
1389
                if terminated.Reason == cc.OOMKilledReason {
1✔
1390
                        return true
×
1391
                }
×
1392
        }
1393

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

© 2025 Coveralls, Inc