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

kubevirt / containerized-data-importer / #5761

13 Jan 2026 07:24AM UTC coverage: 49.643% (+0.3%) from 49.388%
#5761

Pull #4010

travis-ci

halfcrazy
feat: Add checksum validation for HTTP/HTTPS DataVolume sources

Introduces cryptographic hash validation for HTTP/HTTPS
import sources to prevent data tampering during download.

**Usage Example:**

```yaml
apiVersion: cdi.kubevirt.io/v1beta1
kind: DataVolume
metadata:
  name: fedora-dv
spec:
  source:
    http:
      url: "https://download.fedoraproject.org/pub/fedora/linux/releases/39/Cloud/x86_64/images/Fedora-Cloud-Base-39-1.5.x86_64.qcow2"
      checksum: "sha256:c5b50f903e39b3c5d3b7c7bb9a4c5e4f3"
  pvc:
    accessModes:
      - ReadWriteOnce
    resources:
      requests:
        storage: 10Gi
```

Signed-off-by: Yan Zhu <hackzhuyan@gmail.com>
Pull Request #4010: feat: Add checksum validation for HTTP/HTTPS DataVolume sources

153 of 171 new or added lines in 6 files covered. (89.47%)

619 existing lines in 7 files now uncovered.

14755 of 29722 relevant lines covered (49.64%)

0.56 hits per line

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

13.47
/pkg/controller/common/util.go
1
/*
2
Copyright 2022 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
See the License for the specific language governing permissions and
14
limitations under the License.
15
*/
16

17
package common
18

19
import (
20
        "context"
21
        "crypto/rand"
22
        "crypto/rsa"
23
        "crypto/tls"
24
        "fmt"
25
        "io"
26
        "net"
27
        "net/http"
28
        "reflect"
29
        "regexp"
30
        "sort"
31
        "strconv"
32
        "strings"
33
        "sync"
34
        "time"
35

36
        "github.com/go-logr/logr"
37
        snapshotv1 "github.com/kubernetes-csi/external-snapshotter/client/v6/apis/volumesnapshot/v1"
38
        ocpconfigv1 "github.com/openshift/api/config/v1"
39
        "github.com/pkg/errors"
40

41
        corev1 "k8s.io/api/core/v1"
42
        storagev1 "k8s.io/api/storage/v1"
43
        extv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
44
        k8serrors "k8s.io/apimachinery/pkg/api/errors"
45
        "k8s.io/apimachinery/pkg/api/meta"
46
        "k8s.io/apimachinery/pkg/api/resource"
47
        metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
48
        "k8s.io/apimachinery/pkg/labels"
49
        "k8s.io/apimachinery/pkg/runtime"
50
        "k8s.io/apimachinery/pkg/types"
51
        "k8s.io/apimachinery/pkg/util/sets"
52
        "k8s.io/client-go/tools/cache"
53
        "k8s.io/client-go/tools/record"
54
        "k8s.io/klog/v2"
55
        "k8s.io/utils/ptr"
56

57
        runtimecache "sigs.k8s.io/controller-runtime/pkg/cache"
58
        "sigs.k8s.io/controller-runtime/pkg/client"
59
        "sigs.k8s.io/controller-runtime/pkg/client/fake"
60

61
        cdiv1 "kubevirt.io/containerized-data-importer-api/pkg/apis/core/v1beta1"
62
        cdiv1utils "kubevirt.io/containerized-data-importer-api/pkg/apis/core/v1beta1/utils"
63
        "kubevirt.io/containerized-data-importer/pkg/client/clientset/versioned/scheme"
64
        "kubevirt.io/containerized-data-importer/pkg/common"
65
        featuregates "kubevirt.io/containerized-data-importer/pkg/feature-gates"
66
        "kubevirt.io/containerized-data-importer/pkg/token"
67
        "kubevirt.io/containerized-data-importer/pkg/util"
68
        sdkapi "kubevirt.io/controller-lifecycle-operator-sdk/api"
69
)
70

71
const (
72
        // DataVolName provides a const to use for creating volumes in pod specs
73
        DataVolName = "cdi-data-vol"
74

75
        // ScratchVolName provides a const to use for creating scratch pvc volumes in pod specs
76
        ScratchVolName = "cdi-scratch-vol"
77

78
        // AnnAPIGroup is the APIGroup for CDI
79
        AnnAPIGroup = "cdi.kubevirt.io"
80
        // AnnCreatedBy is a pod annotation indicating if the pod was created by the PVC
81
        AnnCreatedBy = AnnAPIGroup + "/storage.createdByController"
82
        // AnnPodPhase is a PVC annotation indicating the related pod progress (phase)
83
        AnnPodPhase = AnnAPIGroup + "/storage.pod.phase"
84
        // AnnPodReady tells whether the pod is ready
85
        AnnPodReady = AnnAPIGroup + "/storage.pod.ready"
86
        // AnnPodRestarts is a PVC annotation that tells how many times a related pod was restarted
87
        AnnPodRestarts = AnnAPIGroup + "/storage.pod.restarts"
88
        // AnnPodSchedulable is a PVC annotation that tells if the Pod is schedulable or not
89
        AnnPodSchedulable = AnnAPIGroup + "/storage.pod.schedulable"
90
        // AnnPopulatedFor is a PVC annotation telling the datavolume controller that the PVC is already populated
91
        AnnPopulatedFor = AnnAPIGroup + "/storage.populatedFor"
92
        // AnnPrePopulated is a PVC annotation telling the datavolume controller that the PVC is already populated
93
        AnnPrePopulated = AnnAPIGroup + "/storage.prePopulated"
94
        // AnnPriorityClassName is PVC annotation to indicate the priority class name for importer, cloner and uploader pod
95
        AnnPriorityClassName = AnnAPIGroup + "/storage.pod.priorityclassname"
96
        // AnnExternalPopulation annotation marks a PVC as "externally populated", allowing the import-controller to skip it
97
        AnnExternalPopulation = AnnAPIGroup + "/externalPopulation"
98

99
        // AnnPodRetainAfterCompletion is PVC annotation for retaining transfer pods after completion
100
        AnnPodRetainAfterCompletion = AnnAPIGroup + "/storage.pod.retainAfterCompletion"
101

102
        // AnnPreviousCheckpoint provides a const to indicate the previous snapshot for a multistage import
103
        AnnPreviousCheckpoint = AnnAPIGroup + "/storage.checkpoint.previous"
104
        // AnnCurrentCheckpoint provides a const to indicate the current snapshot for a multistage import
105
        AnnCurrentCheckpoint = AnnAPIGroup + "/storage.checkpoint.current"
106
        // AnnFinalCheckpoint provides a const to indicate whether the current checkpoint is the last one
107
        AnnFinalCheckpoint = AnnAPIGroup + "/storage.checkpoint.final"
108
        // AnnCheckpointsCopied is a prefix for recording which checkpoints have already been copied
109
        AnnCheckpointsCopied = AnnAPIGroup + "/storage.checkpoint.copied"
110

111
        // AnnCurrentPodID keeps track of the latest pod servicing this PVC
112
        AnnCurrentPodID = AnnAPIGroup + "/storage.checkpoint.pod.id"
113
        // AnnMultiStageImportDone marks a multi-stage import as totally finished
114
        AnnMultiStageImportDone = AnnAPIGroup + "/storage.checkpoint.done"
115

116
        // AnnPopulatorProgress is a standard annotation that can be used progress reporting
117
        AnnPopulatorProgress = AnnAPIGroup + "/storage.populator.progress"
118

119
        // AnnPreallocationRequested provides a const to indicate whether preallocation should be performed on the PV
120
        AnnPreallocationRequested = AnnAPIGroup + "/storage.preallocation.requested"
121
        // AnnPreallocationApplied provides a const for PVC preallocation annotation
122
        AnnPreallocationApplied = AnnAPIGroup + "/storage.preallocation"
123

124
        // AnnRunningCondition provides a const for the running condition
125
        AnnRunningCondition = AnnAPIGroup + "/storage.condition.running"
126
        // AnnRunningConditionMessage provides a const for the running condition
127
        AnnRunningConditionMessage = AnnAPIGroup + "/storage.condition.running.message"
128
        // AnnRunningConditionReason provides a const for the running condition
129
        AnnRunningConditionReason = AnnAPIGroup + "/storage.condition.running.reason"
130

131
        // AnnBoundCondition provides a const for the running condition
132
        AnnBoundCondition = AnnAPIGroup + "/storage.condition.bound"
133
        // AnnBoundConditionMessage provides a const for the running condition
134
        AnnBoundConditionMessage = AnnAPIGroup + "/storage.condition.bound.message"
135
        // AnnBoundConditionReason provides a const for the running condition
136
        AnnBoundConditionReason = AnnAPIGroup + "/storage.condition.bound.reason"
137

138
        // AnnSourceRunningCondition provides a const for the running condition
139
        AnnSourceRunningCondition = AnnAPIGroup + "/storage.condition.source.running"
140
        // AnnSourceRunningConditionMessage provides a const for the running condition
141
        AnnSourceRunningConditionMessage = AnnAPIGroup + "/storage.condition.source.running.message"
142
        // AnnSourceRunningConditionReason provides a const for the running condition
143
        AnnSourceRunningConditionReason = AnnAPIGroup + "/storage.condition.source.running.reason"
144

145
        // AnnVddkVersion shows the last VDDK library version used by a DV's importer pod
146
        AnnVddkVersion = AnnAPIGroup + "/storage.pod.vddk.version"
147
        // AnnVddkHostConnection shows the last ESX host that serviced a DV's importer pod
148
        AnnVddkHostConnection = AnnAPIGroup + "/storage.pod.vddk.host"
149
        // AnnVddkInitImageURL saves a per-DV VDDK image URL on the PVC
150
        AnnVddkInitImageURL = AnnAPIGroup + "/storage.pod.vddk.initimageurl"
151
        // AnnVddkExtraArgs references a ConfigMap that holds arguments to pass directly to the VDDK library
152
        AnnVddkExtraArgs = AnnAPIGroup + "/storage.pod.vddk.extraargs"
153

154
        // AnnRequiresScratch provides a const for our PVC requiring scratch annotation
155
        AnnRequiresScratch = AnnAPIGroup + "/storage.import.requiresScratch"
156

157
        // AnnRequiresDirectIO provides a const for our PVC requiring direct io annotation (due to OOMs we need to try qemu cache=none)
158
        AnnRequiresDirectIO = AnnAPIGroup + "/storage.import.requiresDirectIo"
159
        // OOMKilledReason provides a value that container runtimes must return in the reason field for an OOMKilled container
160
        OOMKilledReason = "OOMKilled"
161

162
        // AnnContentType provides a const for the PVC content-type
163
        AnnContentType = AnnAPIGroup + "/storage.contentType"
164

165
        // AnnSource provide a const for our PVC import source annotation
166
        AnnSource = AnnAPIGroup + "/storage.import.source"
167
        // AnnEndpoint provides a const for our PVC endpoint annotation
168
        AnnEndpoint = AnnAPIGroup + "/storage.import.endpoint"
169

170
        // AnnSecret provides a const for our PVC secretName annotation
171
        AnnSecret = AnnAPIGroup + "/storage.import.secretName"
172
        // AnnCertConfigMap is the name of a configmap containing tls certs
173
        AnnCertConfigMap = AnnAPIGroup + "/storage.import.certConfigMap"
174
        // AnnRegistryImportMethod provides a const for registry import method annotation
175
        AnnRegistryImportMethod = AnnAPIGroup + "/storage.import.registryImportMethod"
176
        // AnnRegistryImageStream provides a const for registry image stream annotation
177
        AnnRegistryImageStream = AnnAPIGroup + "/storage.import.registryImageStream"
178
        // AnnImportPod provides a const for our PVC importPodName annotation
179
        AnnImportPod = AnnAPIGroup + "/storage.import.importPodName"
180
        // AnnDiskID provides a const for our PVC diskId annotation
181
        AnnDiskID = AnnAPIGroup + "/storage.import.diskId"
182
        // AnnUUID provides a const for our PVC uuid annotation
183
        AnnUUID = AnnAPIGroup + "/storage.import.uuid"
184
        // AnnInsecureSkipVerify provides a const for skipping certificate verification
185
        AnnInsecureSkipVerify = AnnAPIGroup + "/storage.import.insecureSkipVerify"
186
        // AnnBackingFile provides a const for our PVC backing file annotation
187
        AnnBackingFile = AnnAPIGroup + "/storage.import.backingFile"
188
        // AnnThumbprint provides a const for our PVC backing thumbprint annotation
189
        AnnThumbprint = AnnAPIGroup + "/storage.import.vddk.thumbprint"
190
        // AnnExtraHeaders provides a const for our PVC extraHeaders annotation
191
        AnnExtraHeaders = AnnAPIGroup + "/storage.import.extraHeaders"
192
        // AnnSecretExtraHeaders provides a const for our PVC secretExtraHeaders annotation
193
        AnnSecretExtraHeaders = AnnAPIGroup + "/storage.import.secretExtraHeaders"
194
        // AnnChecksum provides a const for our PVC checksum annotation
195
        AnnChecksum = AnnAPIGroup + "/storage.import.checksum"
196
        // AnnRegistryImageArchitecture provides a const for our PVC registryImageArchitecture annotation
197
        AnnRegistryImageArchitecture = AnnAPIGroup + "/storage.import.registryImageArchitecture"
198

199
        // AnnCloneToken is the annotation containing the clone token
200
        AnnCloneToken = AnnAPIGroup + "/storage.clone.token"
201
        // AnnExtendedCloneToken is the annotation containing the long term clone token
202
        AnnExtendedCloneToken = AnnAPIGroup + "/storage.extended.clone.token"
203
        // AnnPermissiveClone annotation allows the clone-controller to skip the clone size validation
204
        AnnPermissiveClone = AnnAPIGroup + "/permissiveClone"
205
        // AnnOwnerUID annotation has the owner UID
206
        AnnOwnerUID = AnnAPIGroup + "/ownerUID"
207
        // AnnCloneType is the comuuted/requested clone type
208
        AnnCloneType = AnnAPIGroup + "/cloneType"
209
        // AnnCloneSourcePod name of the source clone pod
210
        AnnCloneSourcePod = AnnAPIGroup + "/storage.sourceClonePodName"
211

212
        // AnnUploadRequest marks that a PVC should be made available for upload
213
        AnnUploadRequest = AnnAPIGroup + "/storage.upload.target"
214

215
        // AnnCheckStaticVolume checks if a statically allocated PV exists before creating the target PVC.
216
        // If so, PVC is still created but population is skipped
217
        AnnCheckStaticVolume = AnnAPIGroup + "/storage.checkStaticVolume"
218

219
        // AnnPersistentVolumeList is an annotation storing a list of PV names
220
        AnnPersistentVolumeList = AnnAPIGroup + "/storage.persistentVolumeList"
221

222
        // AnnPopulatorKind annotation is added to a PVC' to specify the population kind, so it's later
223
        // checked by the common populator watches.
224
        AnnPopulatorKind = AnnAPIGroup + "/storage.populator.kind"
225
        // AnnUsePopulator annotation indicates if the datavolume population will use populators
226
        AnnUsePopulator = AnnAPIGroup + "/storage.usePopulator"
227

228
        // AnnMinimumSupportedPVCSize annotation on a StorageProfile specifies its minimum supported PVC size
229
        AnnMinimumSupportedPVCSize = AnnAPIGroup + "/minimumSupportedPvcSize"
230

231
        // AnnDefaultStorageClass is the annotation indicating that a storage class is the default one
232
        AnnDefaultStorageClass = "storageclass.kubernetes.io/is-default-class"
233
        // AnnDefaultVirtStorageClass is the annotation indicating that a storage class is the default one for virtualization purposes
234
        AnnDefaultVirtStorageClass = "storageclass.kubevirt.io/is-default-virt-class"
235
        // AnnDefaultSnapshotClass is the annotation indicating that a snapshot class is the default one
236
        AnnDefaultSnapshotClass = "snapshot.storage.kubernetes.io/is-default-class"
237

238
        // AnnSourceVolumeMode is the volume mode of the source PVC specified as an annotation on snapshots
239
        AnnSourceVolumeMode = AnnAPIGroup + "/storage.import.sourceVolumeMode"
240

241
        // AnnOpenShiftImageLookup is the annotation for OpenShift image stream lookup
242
        AnnOpenShiftImageLookup = "alpha.image.policy.openshift.io/resolve-names"
243

244
        // AnnCloneRequest sets our expected annotation for a CloneRequest
245
        AnnCloneRequest = "k8s.io/CloneRequest"
246
        // AnnCloneOf is used to indicate that cloning was complete
247
        AnnCloneOf = "k8s.io/CloneOf"
248

249
        // AnnPodNetwork is used for specifying Pod Network
250
        AnnPodNetwork = "k8s.v1.cni.cncf.io/networks"
251
        // AnnPodMultusDefaultNetwork is used for specifying default Pod Network
252
        AnnPodMultusDefaultNetwork = "v1.multus-cni.io/default-network"
253
        // AnnPodSidecarInjectionIstio is used for enabling/disabling Pod istio/AspenMesh sidecar injection
254
        AnnPodSidecarInjectionIstio = "sidecar.istio.io/inject"
255
        // AnnPodSidecarInjectionIstioDefault is the default value passed for AnnPodSidecarInjection
256
        AnnPodSidecarInjectionIstioDefault = "false"
257
        // AnnPodSidecarInjectionLinkerd is used to enable/disable linkerd sidecar injection
258
        AnnPodSidecarInjectionLinkerd = "linkerd.io/inject"
259
        // AnnPodSidecarInjectionLinkerdDefault is the default value passed for AnnPodSidecarInjectionLinkerd
260
        AnnPodSidecarInjectionLinkerdDefault = "disabled"
261

262
        // AnnImmediateBinding provides a const to indicate whether immediate binding should be performed on the PV (overrides global config)
263
        AnnImmediateBinding = AnnAPIGroup + "/storage.bind.immediate.requested"
264

265
        // AnnSelectedNode annotation is added to a PVC that has been triggered by scheduler to
266
        // be dynamically provisioned. Its value is the name of the selected node.
267
        AnnSelectedNode = "volume.kubernetes.io/selected-node"
268

269
        // CloneUniqueID is used as a special label to be used when we search for the pod
270
        CloneUniqueID = AnnAPIGroup + "/storage.clone.cloneUniqeId"
271

272
        // CloneSourceInUse is reason for event created when clone source pvc is in use
273
        CloneSourceInUse = "CloneSourceInUse"
274

275
        // CloneComplete message
276
        CloneComplete = "Clone Complete"
277

278
        cloneTokenLeeway = 10 * time.Second
279

280
        // Default value for preallocation option if not defined in DV or CDIConfig
281
        defaultPreallocation = false
282

283
        // ErrStartingPod provides a const to indicate that a pod wasn't able to start without providing sensitive information (reason)
284
        ErrStartingPod = "ErrStartingPod"
285
        // MessageErrStartingPod provides a const to indicate that a pod wasn't able to start without providing sensitive information (message)
286
        MessageErrStartingPod = "Error starting pod '%s': For more information, request access to cdi-deploy logs from your sysadmin"
287
        // ErrClaimNotValid provides a const to indicate a claim is not valid
288
        ErrClaimNotValid = "ErrClaimNotValid"
289
        // ErrExceededQuota provides a const to indicate the claim has exceeded the quota
290
        ErrExceededQuota = "ErrExceededQuota"
291
        // ErrIncompatiblePVC provides a const to indicate a clone is not possible due to an incompatible PVC
292
        ErrIncompatiblePVC = "ErrIncompatiblePVC"
293

294
        // SourceHTTP is the source type HTTP, if unspecified or invalid, it defaults to SourceHTTP
295
        SourceHTTP = "http"
296
        // SourceS3 is the source type S3
297
        SourceS3 = "s3"
298
        // SourceGCS is the source type GCS
299
        SourceGCS = "gcs"
300
        // SourceGlance is the source type of glance
301
        SourceGlance = "glance"
302
        // SourceNone means there is no source.
303
        SourceNone = "none"
304
        // SourceRegistry is the source type of Registry
305
        SourceRegistry = "registry"
306
        // SourceImageio is the source type ovirt-imageio
307
        SourceImageio = "imageio"
308
        // SourceVDDK is the source type of VDDK
309
        SourceVDDK = "vddk"
310

311
        // VolumeSnapshotClassSelected reports that a VolumeSnapshotClass was selected
312
        VolumeSnapshotClassSelected = "VolumeSnapshotClassSelected"
313
        // MessageStorageProfileVolumeSnapshotClassSelected reports that a VolumeSnapshotClass was selected according to StorageProfile
314
        MessageStorageProfileVolumeSnapshotClassSelected = "VolumeSnapshotClass selected according to StorageProfile"
315
        // MessageDefaultVolumeSnapshotClassSelected reports that the default VolumeSnapshotClass was selected
316
        MessageDefaultVolumeSnapshotClassSelected = "Default VolumeSnapshotClass selected"
317
        // MessageFirstVolumeSnapshotClassSelected reports that the first VolumeSnapshotClass was selected
318
        MessageFirstVolumeSnapshotClassSelected = "First VolumeSnapshotClass selected"
319

320
        // ClaimLost reason const
321
        ClaimLost = "ClaimLost"
322
        // NotFound reason const
323
        NotFound = "NotFound"
324

325
        // LabelDefaultInstancetype provides a default VirtualMachine{ClusterInstancetype,Instancetype} that can be used by a VirtualMachine booting from a given PVC
326
        LabelDefaultInstancetype = "instancetype.kubevirt.io/default-instancetype"
327
        // LabelDefaultInstancetypeKind provides a default kind of either VirtualMachineClusterInstancetype or VirtualMachineInstancetype
328
        LabelDefaultInstancetypeKind = "instancetype.kubevirt.io/default-instancetype-kind"
329
        // LabelDefaultPreference provides a default VirtualMachine{ClusterPreference,Preference} that can be used by a VirtualMachine booting from a given PVC
330
        LabelDefaultPreference = "instancetype.kubevirt.io/default-preference"
331
        // LabelDefaultPreferenceKind provides a default kind of either VirtualMachineClusterPreference or VirtualMachinePreference
332
        LabelDefaultPreferenceKind = "instancetype.kubevirt.io/default-preference-kind"
333

334
        // LabelDynamicCredentialSupport specifies if the OS supports updating credentials at runtime.
335
        //nolint:gosec // These are not credentials
336
        LabelDynamicCredentialSupport = "kubevirt.io/dynamic-credentials-support"
337

338
        // LabelExcludeFromVeleroBackup provides a const to indicate whether an object should be excluded from velero backup
339
        LabelExcludeFromVeleroBackup = "velero.io/exclude-from-backup"
340

341
        // ProgressDone this means we are DONE
342
        ProgressDone = "100.0%"
343

344
        // AnnEventSourceKind is the source kind that should be related to events
345
        AnnEventSourceKind = AnnAPIGroup + "/events.source.kind"
346
        // AnnEventSource is the source that should be related to events (namespace/name)
347
        AnnEventSource = AnnAPIGroup + "/events.source"
348

349
        // AnnAllowClaimAdoption is the annotation that allows a claim to be adopted by a DataVolume
350
        AnnAllowClaimAdoption = AnnAPIGroup + "/allowClaimAdoption"
351

352
        // AnnCdiCustomizeComponentHash annotation is a hash of all customizations that live under spec.CustomizeComponents
353
        AnnCdiCustomizeComponentHash = AnnAPIGroup + "/customizer-identifier"
354

355
        // AnnCreatedForDataVolume stores the UID of the datavolume that the PVC was created for
356
        AnnCreatedForDataVolume = AnnAPIGroup + "/createdForDataVolume"
357

358
        // AnnPVCPrimeName annotation is the name of the PVC' that is used to populate the PV which is then rebound to the target PVC
359
        AnnPVCPrimeName = AnnAPIGroup + "/storage.populator.pvcPrime"
360
)
361

362
// Size-detection pod error codes
363
const (
364
        NoErr int = iota
365
        ErrBadArguments
366
        ErrInvalidFile
367
        ErrInvalidPath
368
        ErrBadTermFile
369
        ErrUnknown
370
)
371

372
var (
373
        // BlockMode is raw block device mode
374
        BlockMode = corev1.PersistentVolumeBlock
375
        // FilesystemMode is filesystem device mode
376
        FilesystemMode = corev1.PersistentVolumeFilesystem
377

378
        // DefaultInstanceTypeLabels is a list of currently supported default instance type labels
379
        DefaultInstanceTypeLabels = []string{
380
                LabelDefaultInstancetype,
381
                LabelDefaultInstancetypeKind,
382
                LabelDefaultPreference,
383
                LabelDefaultPreferenceKind,
384
        }
385

386
        apiServerKeyOnce sync.Once
387
        apiServerKey     *rsa.PrivateKey
388

389
        // allowedAnnotations is a list of annotations
390
        // that can be propagated from the pvc/dv to a pod
391
        allowedAnnotations = map[string]string{
392
                AnnPodNetwork:                 "",
393
                AnnPodSidecarInjectionIstio:   AnnPodSidecarInjectionIstioDefault,
394
                AnnPodSidecarInjectionLinkerd: AnnPodSidecarInjectionLinkerdDefault,
395
                AnnPriorityClassName:          "",
396
                AnnPodMultusDefaultNetwork:    "",
397
        }
398

399
        validLabelsMatch = regexp.MustCompile(`^([\w.]+\.kubevirt.io|kubevirt.io)/[\w.-]+$`)
400

401
        ErrDataSourceMaxDepthReached = errors.New("DataSource reference chain exceeds maximum depth of 1")
402
        ErrDataSourceSelfReference   = errors.New("DataSource cannot self-reference")
403
        ErrDataSourceCrossNamespace  = errors.New("DataSource cannot reference a DataSource in another namespace")
404
)
405

406
// FakeValidator is a fake token validator
407
type FakeValidator struct {
408
        Match     string
409
        Operation token.Operation
410
        Name      string
411
        Namespace string
412
        Resource  metav1.GroupVersionResource
413
        Params    map[string]string
414
}
415

416
// Validate is a fake token validation
UNCOV
417
func (v *FakeValidator) Validate(value string) (*token.Payload, error) {
×
UNCOV
418
        if value != v.Match {
×
419
                return nil, fmt.Errorf("token does not match expected")
×
420
        }
×
421
        resource := metav1.GroupVersionResource{
×
422
                Resource: "persistentvolumeclaims",
×
423
        }
×
424
        return &token.Payload{
×
425
                Name:      v.Name,
×
426
                Namespace: v.Namespace,
×
427
                Operation: token.OperationClone,
×
428
                Resource:  resource,
×
429
                Params:    v.Params,
×
430
        }, nil
×
431
}
432

433
// MultiTokenValidator is a token validator that can validate both short and long tokens
434
type MultiTokenValidator struct {
435
        ShortTokenValidator token.Validator
436
        LongTokenValidator  token.Validator
437
}
438

439
// ValidatePVC validates a PVC
UNCOV
440
func (mtv *MultiTokenValidator) ValidatePVC(source, target *corev1.PersistentVolumeClaim) error {
×
UNCOV
441
        tok, v := mtv.getTokenAndValidator(target)
×
442
        return ValidateCloneTokenPVC(tok, v, source, target)
×
443
}
×
444

445
// ValidatePopulator valades a token for a populator
UNCOV
446
func (mtv *MultiTokenValidator) ValidatePopulator(vcs *cdiv1.VolumeCloneSource, pvc *corev1.PersistentVolumeClaim) error {
×
UNCOV
447
        if vcs.Namespace == pvc.Namespace {
×
448
                return nil
×
449
        }
×
450

451
        tok, v := mtv.getTokenAndValidator(pvc)
×
UNCOV
452

×
453
        tokenData, err := v.Validate(tok)
×
454
        if err != nil {
×
455
                return errors.Wrap(err, "error verifying token")
×
456
        }
×
457

458
        var tokenResourceName string
×
UNCOV
459
        switch vcs.Spec.Source.Kind {
×
460
        case "PersistentVolumeClaim":
×
461
                tokenResourceName = "persistentvolumeclaims"
×
462
        case "VolumeSnapshot":
×
463
                tokenResourceName = "volumesnapshots"
×
464
        }
465
        srcName := vcs.Spec.Source.Name
×
UNCOV
466

×
467
        return validateTokenData(tokenData, vcs.Namespace, srcName, pvc.Namespace, pvc.Name, string(pvc.UID), tokenResourceName)
×
468
}
469

UNCOV
470
func (mtv *MultiTokenValidator) getTokenAndValidator(pvc *corev1.PersistentVolumeClaim) (string, token.Validator) {
×
UNCOV
471
        v := mtv.LongTokenValidator
×
472
        tok, ok := pvc.Annotations[AnnExtendedCloneToken]
×
473
        if !ok {
×
474
                // if token doesn't exist, no prob for same namespace
×
475
                tok = pvc.Annotations[AnnCloneToken]
×
476
                v = mtv.ShortTokenValidator
×
477
        }
×
478
        return tok, v
×
479
}
480

481
// NewMultiTokenValidator returns a new multi token validator
UNCOV
482
func NewMultiTokenValidator(key *rsa.PublicKey) *MultiTokenValidator {
×
UNCOV
483
        return &MultiTokenValidator{
×
484
                ShortTokenValidator: NewCloneTokenValidator(common.CloneTokenIssuer, key),
×
485
                LongTokenValidator:  NewCloneTokenValidator(common.ExtendedCloneTokenIssuer, key),
×
486
        }
×
487
}
×
488

489
// NewCloneTokenValidator returns a new token validator
UNCOV
490
func NewCloneTokenValidator(issuer string, key *rsa.PublicKey) token.Validator {
×
UNCOV
491
        return token.NewValidator(issuer, key, cloneTokenLeeway)
×
492
}
×
493

494
// GetRequestedImageSize returns the PVC requested size
495
func GetRequestedImageSize(pvc *corev1.PersistentVolumeClaim) (string, error) {
1✔
496
        pvcSize, found := pvc.Spec.Resources.Requests[corev1.ResourceStorage]
1✔
497
        if !found {
2✔
498
                return "", errors.Errorf("storage request is missing in pvc \"%s/%s\"", pvc.Namespace, pvc.Name)
1✔
499
        }
1✔
500
        return pvcSize.String(), nil
1✔
501
}
502

503
// GetVolumeMode returns the volumeMode from PVC handling default empty value
UNCOV
504
func GetVolumeMode(pvc *corev1.PersistentVolumeClaim) corev1.PersistentVolumeMode {
×
UNCOV
505
        return util.ResolveVolumeMode(pvc.Spec.VolumeMode)
×
506
}
×
507

508
// IsDataVolumeUsingDefaultStorageClass checks if the DataVolume is using the default StorageClass
UNCOV
509
func IsDataVolumeUsingDefaultStorageClass(dv *cdiv1.DataVolume) bool {
×
UNCOV
510
        return GetStorageClassFromDVSpec(dv) == nil
×
511
}
×
512

513
// GetStorageClassFromDVSpec returns the StorageClassName from DataVolume PVC or Storage spec
UNCOV
514
func GetStorageClassFromDVSpec(dv *cdiv1.DataVolume) *string {
×
UNCOV
515
        if dv.Spec.PVC != nil {
×
516
                return dv.Spec.PVC.StorageClassName
×
517
        }
×
518

519
        if dv.Spec.Storage != nil {
×
UNCOV
520
                return dv.Spec.Storage.StorageClassName
×
521
        }
×
522

523
        return nil
×
524
}
525

526
// getStorageClassByName looks up the storage class based on the name.
527
// If name is nil, it performs fallback to default according to the provided content type
528
// If no storage class is found, returns nil
529
func getStorageClassByName(ctx context.Context, client client.Client, name *string, contentType cdiv1.DataVolumeContentType) (*storagev1.StorageClass, error) {
1✔
530
        if name == nil {
2✔
531
                return getFallbackStorageClass(ctx, client, contentType)
1✔
532
        }
1✔
533

534
        // look up storage class by name
UNCOV
535
        storageClass := &storagev1.StorageClass{}
×
UNCOV
536
        if err := client.Get(ctx, types.NamespacedName{Name: *name}, storageClass); err != nil {
×
537
                if k8serrors.IsNotFound(err) {
×
538
                        return nil, nil
×
539
                }
×
540
                klog.V(3).Info("Unable to retrieve storage class", "storage class name", *name)
×
541
                return nil, errors.Errorf("unable to retrieve storage class %s", *name)
×
542
        }
543

UNCOV
544
        return storageClass, nil
×
545
}
546

547
// GetStorageClassByNameWithK8sFallback looks up the storage class based on the name
548
// If name is nil, it looks for the default k8s storage class storageclass.kubernetes.io/is-default-class
549
// If no storage class is found, returns nil
550
func GetStorageClassByNameWithK8sFallback(ctx context.Context, client client.Client, name *string) (*storagev1.StorageClass, error) {
1✔
551
        return getStorageClassByName(ctx, client, name, cdiv1.DataVolumeArchive)
1✔
552
}
1✔
553

554
// GetStorageClassByNameWithVirtFallback looks up the storage class based on the name
555
// If name is nil, it looks for the following, in this order:
556
// default kubevirt storage class (if the caller is interested) storageclass.kubevirt.io/is-default-class
557
// default k8s storage class storageclass.kubernetes.io/is-default-class
558
// If no storage class is found, returns nil
559
func GetStorageClassByNameWithVirtFallback(ctx context.Context, client client.Client, name *string, contentType cdiv1.DataVolumeContentType) (*storagev1.StorageClass, error) {
1✔
560
        return getStorageClassByName(ctx, client, name, contentType)
1✔
561
}
1✔
562

563
// getFallbackStorageClass looks for a default virt/k8s storage class according to the content type
564
// If no storage class is found, returns nil
565
func getFallbackStorageClass(ctx context.Context, client client.Client, contentType cdiv1.DataVolumeContentType) (*storagev1.StorageClass, error) {
1✔
566
        storageClasses := &storagev1.StorageClassList{}
1✔
567
        if err := client.List(ctx, storageClasses); err != nil {
1✔
UNCOV
568
                klog.V(3).Info("Unable to retrieve available storage classes")
×
UNCOV
569
                return nil, errors.New("unable to retrieve storage classes")
×
570
        }
×
571

572
        if GetContentType(contentType) == cdiv1.DataVolumeKubeVirt {
2✔
573
                if virtSc := GetPlatformDefaultStorageClass(storageClasses, AnnDefaultVirtStorageClass); virtSc != nil {
2✔
574
                        return virtSc, nil
1✔
575
                }
1✔
576
        }
577
        return GetPlatformDefaultStorageClass(storageClasses, AnnDefaultStorageClass), nil
1✔
578
}
579

580
// GetPlatformDefaultStorageClass returns the default storage class according to the provided annotation or nil if none found
581
func GetPlatformDefaultStorageClass(storageClasses *storagev1.StorageClassList, defaultAnnotationKey string) *storagev1.StorageClass {
1✔
582
        defaultClasses := []storagev1.StorageClass{}
1✔
583

1✔
584
        for _, storageClass := range storageClasses.Items {
2✔
585
                if storageClass.Annotations[defaultAnnotationKey] == "true" {
2✔
586
                        defaultClasses = append(defaultClasses, storageClass)
1✔
587
                }
1✔
588
        }
589

590
        if len(defaultClasses) == 0 {
2✔
591
                return nil
1✔
592
        }
1✔
593

594
        // Primary sort by creation timestamp, newest first
595
        // Secondary sort by class name, ascending order
596
        // Follows k8s behavior
597
        // https://github.com/kubernetes/kubernetes/blob/731068288e112c8b5af70f676296cc44661e84f4/pkg/volume/util/storageclass.go#L58-L59
598
        sort.Slice(defaultClasses, func(i, j int) bool {
2✔
599
                if defaultClasses[i].CreationTimestamp.UnixNano() == defaultClasses[j].CreationTimestamp.UnixNano() {
2✔
600
                        return defaultClasses[i].Name < defaultClasses[j].Name
1✔
601
                }
1✔
602
                return defaultClasses[i].CreationTimestamp.UnixNano() > defaultClasses[j].CreationTimestamp.UnixNano()
1✔
603
        })
604
        if len(defaultClasses) > 1 {
2✔
605
                klog.V(3).Infof("%d default StorageClasses were found, choosing: %s", len(defaultClasses), defaultClasses[0].Name)
1✔
606
        }
1✔
607

608
        return &defaultClasses[0]
1✔
609
}
610

611
// GetFilesystemOverheadForStorageClass determines the filesystem overhead defined in CDIConfig for the storageClass.
UNCOV
612
func GetFilesystemOverheadForStorageClass(ctx context.Context, client client.Client, storageClassName *string) (cdiv1.Percent, error) {
×
UNCOV
613
        if storageClassName != nil && *storageClassName == "" {
×
614
                klog.V(3).Info("No storage class name passed")
×
615
                return "0", nil
×
616
        }
×
617

618
        cdiConfig := &cdiv1.CDIConfig{}
×
UNCOV
619
        if err := client.Get(ctx, types.NamespacedName{Name: common.ConfigName}, cdiConfig); err != nil {
×
620
                if k8serrors.IsNotFound(err) {
×
621
                        klog.V(1).Info("CDIConfig does not exist, pod will not start until it does")
×
622
                        return "0", nil
×
623
                }
×
624
                return "0", err
×
625
        }
626

UNCOV
627
        targetStorageClass, err := GetStorageClassByNameWithK8sFallback(ctx, client, storageClassName)
×
UNCOV
628
        if err != nil || targetStorageClass == nil {
×
629
                klog.V(3).Info("Storage class", storageClassName, "not found, trying default storage class")
×
630
                targetStorageClass, err = GetStorageClassByNameWithK8sFallback(ctx, client, nil)
×
631
                if err != nil {
×
632
                        klog.V(3).Info("No default storage class found, continuing with global overhead")
×
633
                        return cdiConfig.Status.FilesystemOverhead.Global, nil
×
634
                }
×
635
        }
636

UNCOV
637
        if cdiConfig.Status.FilesystemOverhead == nil {
×
UNCOV
638
                klog.Errorf("CDIConfig filesystemOverhead used before config controller ran reconcile. Hopefully this only happens during unit testing.")
×
639
                return "0", nil
×
640
        }
×
641

642
        if targetStorageClass == nil {
×
UNCOV
643
                klog.V(3).Info("Storage class", storageClassName, "not found, continuing with global overhead")
×
644
                return cdiConfig.Status.FilesystemOverhead.Global, nil
×
645
        }
×
646

647
        klog.V(3).Info("target storage class for overhead", targetStorageClass.GetName())
×
UNCOV
648

×
649
        perStorageConfig := cdiConfig.Status.FilesystemOverhead.StorageClass
×
650

×
651
        storageClassOverhead, found := perStorageConfig[targetStorageClass.GetName()]
×
652
        if found {
×
653
                return storageClassOverhead, nil
×
654
        }
×
655

656
        return cdiConfig.Status.FilesystemOverhead.Global, nil
×
657
}
658

659
// GetDefaultPodResourceRequirements gets default pod resource requirements from cdi config status
UNCOV
660
func GetDefaultPodResourceRequirements(client client.Client) (*corev1.ResourceRequirements, error) {
×
UNCOV
661
        cdiconfig := &cdiv1.CDIConfig{}
×
662
        if err := client.Get(context.TODO(), types.NamespacedName{Name: common.ConfigName}, cdiconfig); err != nil {
×
663
                klog.Errorf("Unable to find CDI configuration, %v\n", err)
×
664
                return nil, err
×
665
        }
×
666

667
        return cdiconfig.Status.DefaultPodResourceRequirements, nil
×
668
}
669

670
// GetImagePullSecrets gets the imagePullSecrets needed to pull images from the cdi config
UNCOV
671
func GetImagePullSecrets(client client.Client) ([]corev1.LocalObjectReference, error) {
×
UNCOV
672
        cdiconfig := &cdiv1.CDIConfig{}
×
673
        if err := client.Get(context.TODO(), types.NamespacedName{Name: common.ConfigName}, cdiconfig); err != nil {
×
674
                klog.Errorf("Unable to find CDI configuration, %v\n", err)
×
675
                return nil, err
×
676
        }
×
677

678
        return cdiconfig.Status.ImagePullSecrets, nil
×
679
}
680

681
// GetPodFromPvc determines the pod associated with the pvc passed in.
UNCOV
682
func GetPodFromPvc(c client.Client, namespace string, pvc *corev1.PersistentVolumeClaim) (*corev1.Pod, error) {
×
UNCOV
683
        l, _ := labels.Parse(common.PrometheusLabelKey)
×
684
        pods := &corev1.PodList{}
×
685
        listOptions := client.ListOptions{
×
686
                LabelSelector: l,
×
687
        }
×
688
        if err := c.List(context.TODO(), pods, &listOptions); err != nil {
×
689
                return nil, err
×
690
        }
×
691

692
        pvcUID := pvc.GetUID()
×
UNCOV
693
        for _, pod := range pods.Items {
×
694
                if ShouldIgnorePod(&pod, pvc) {
×
695
                        continue
×
696
                }
697
                for _, or := range pod.OwnerReferences {
×
UNCOV
698
                        if or.UID == pvcUID {
×
699
                                return &pod, nil
×
700
                        }
×
701
                }
702

703
                // TODO: check this
UNCOV
704
                val, exists := pod.Labels[CloneUniqueID]
×
UNCOV
705
                if exists && val == string(pvcUID)+common.ClonerSourcePodNameSuffix {
×
706
                        return &pod, nil
×
707
                }
×
708
        }
709
        return nil, errors.Errorf("Unable to find pod owned by UID: %s, in namespace: %s", string(pvcUID), namespace)
×
710
}
711

712
// AddVolumeDevices returns VolumeDevice slice with one block device for pods using PV with block volume mode
UNCOV
713
func AddVolumeDevices() []corev1.VolumeDevice {
×
UNCOV
714
        volumeDevices := []corev1.VolumeDevice{
×
715
                {
×
716
                        Name:       DataVolName,
×
717
                        DevicePath: common.WriteBlockPath,
×
718
                },
×
719
        }
×
720
        return volumeDevices
×
721
}
×
722

723
// GetPodsUsingPVCs returns Pods currently using PVCs
UNCOV
724
func GetPodsUsingPVCs(ctx context.Context, c client.Client, namespace string, names sets.Set[string], allowReadOnly bool) ([]corev1.Pod, error) {
×
UNCOV
725
        pl := &corev1.PodList{}
×
726
        // hopefully using cached client here
×
727
        err := c.List(ctx, pl, &client.ListOptions{Namespace: namespace})
×
728
        if err != nil {
×
729
                return nil, err
×
730
        }
×
731

732
        var pods []corev1.Pod
×
UNCOV
733
        for _, pod := range pl.Items {
×
734
                if pod.Status.Phase == corev1.PodSucceeded || pod.Status.Phase == corev1.PodFailed {
×
735
                        continue
×
736
                }
737
                for _, volume := range pod.Spec.Volumes {
×
UNCOV
738
                        if volume.VolumeSource.PersistentVolumeClaim != nil &&
×
739
                                names.Has(volume.PersistentVolumeClaim.ClaimName) {
×
740
                                addPod := true
×
741
                                if allowReadOnly {
×
742
                                        if !volume.VolumeSource.PersistentVolumeClaim.ReadOnly {
×
743
                                                onlyReadOnly := true
×
744
                                                for _, c := range pod.Spec.Containers {
×
745
                                                        for _, vm := range c.VolumeMounts {
×
746
                                                                if vm.Name == volume.Name && !vm.ReadOnly {
×
747
                                                                        onlyReadOnly = false
×
748
                                                                }
×
749
                                                        }
750
                                                        for _, vm := range c.VolumeDevices {
×
UNCOV
751
                                                                if vm.Name == volume.Name {
×
752
                                                                        // Node level rw mount and container can't mount block device ro
×
753
                                                                        onlyReadOnly = false
×
754
                                                                }
×
755
                                                        }
756
                                                }
UNCOV
757
                                                if onlyReadOnly {
×
UNCOV
758
                                                        // no rw mounts
×
759
                                                        addPod = false
×
760
                                                }
×
761
                                        } else {
×
762
                                                // all mounts must be ro
×
763
                                                addPod = false
×
764
                                        }
×
765
                                        if strings.HasSuffix(pod.Name, common.ClonerSourcePodNameSuffix) && pod.Labels != nil &&
×
766
                                                pod.Labels[common.CDIComponentLabel] == common.ClonerSourcePodName {
×
767
                                                // Host assisted clone source pod only reads from source
×
768
                                                // But some drivers disallow mounting a block PVC ReadOnly
×
769
                                                addPod = false
×
770
                                        }
×
771
                                }
772
                                if addPod {
×
UNCOV
773
                                        pods = append(pods, pod)
×
774
                                        break
×
775
                                }
776
                        }
777
                }
778
        }
779

UNCOV
780
        return pods, nil
×
781
}
782

783
// GetWorkloadNodePlacement extracts the workload-specific nodeplacement values from the CDI CR
UNCOV
784
func GetWorkloadNodePlacement(ctx context.Context, c client.Client) (*sdkapi.NodePlacement, error) {
×
UNCOV
785
        cr, err := GetActiveCDI(ctx, c)
×
786
        if err != nil {
×
787
                return nil, err
×
788
        }
×
789

790
        if cr == nil {
×
UNCOV
791
                return nil, fmt.Errorf("no active CDI")
×
792
        }
×
793

794
        return &cr.Spec.Workloads, nil
×
795
}
796

797
// GetActiveCDI returns the active CDI CR
798
func GetActiveCDI(ctx context.Context, c client.Client) (*cdiv1.CDI, error) {
1✔
799
        crList := &cdiv1.CDIList{}
1✔
800
        if err := c.List(ctx, crList, &client.ListOptions{}); err != nil {
1✔
UNCOV
801
                return nil, err
×
UNCOV
802
        }
×
803

804
        if len(crList.Items) == 0 {
2✔
805
                return nil, nil
1✔
806
        }
1✔
807

808
        if len(crList.Items) == 1 {
2✔
809
                return &crList.Items[0], nil
1✔
810
        }
1✔
811

812
        var activeResources []cdiv1.CDI
1✔
813
        for _, cr := range crList.Items {
2✔
814
                if cr.Status.Phase != sdkapi.PhaseError {
2✔
815
                        activeResources = append(activeResources, cr)
1✔
816
                }
1✔
817
        }
818

819
        if len(activeResources) != 1 {
2✔
820
                return nil, fmt.Errorf("invalid number of active CDI resources: %d", len(activeResources))
1✔
821
        }
1✔
822

823
        return &activeResources[0], nil
1✔
824
}
825

826
// IsPopulated returns if the passed in PVC has been populated according to the rules outlined in pkg/apis/core/<version>/utils.go
UNCOV
827
func IsPopulated(pvc *corev1.PersistentVolumeClaim, c client.Client) (bool, error) {
×
UNCOV
828
        return cdiv1utils.IsPopulated(pvc, func(name, namespace string) (*cdiv1.DataVolume, error) {
×
829
                dv := &cdiv1.DataVolume{}
×
830
                err := c.Get(context.TODO(), types.NamespacedName{Name: name, Namespace: namespace}, dv)
×
831
                return dv, err
×
832
        })
×
833
}
834

835
// GetPreallocation returns the preallocation setting for the specified object (DV or VolumeImportSource), falling back to StorageClass and global setting (in this order)
UNCOV
836
func GetPreallocation(ctx context.Context, client client.Client, preallocation *bool) bool {
×
UNCOV
837
        // First, the DV's preallocation
×
838
        if preallocation != nil {
×
839
                return *preallocation
×
840
        }
×
841

842
        cdiconfig := &cdiv1.CDIConfig{}
×
UNCOV
843
        if err := client.Get(context.TODO(), types.NamespacedName{Name: common.ConfigName}, cdiconfig); err != nil {
×
844
                klog.Errorf("Unable to find CDI configuration, %v\n", err)
×
845
                return defaultPreallocation
×
846
        }
×
847

848
        return cdiconfig.Status.Preallocation
×
849
}
850

851
// ImmediateBindingRequested returns if an object has the ImmediateBinding annotation
UNCOV
852
func ImmediateBindingRequested(obj metav1.Object) bool {
×
UNCOV
853
        _, isImmediateBindingRequested := obj.GetAnnotations()[AnnImmediateBinding]
×
854
        return isImmediateBindingRequested
×
855
}
×
856

857
// GetPriorityClass gets PVC priority class
UNCOV
858
func GetPriorityClass(pvc *corev1.PersistentVolumeClaim) string {
×
UNCOV
859
        anno := pvc.GetAnnotations()
×
860
        return anno[AnnPriorityClassName]
×
861
}
×
862

863
// ShouldDeletePod returns whether the PVC workload pod should be deleted
UNCOV
864
func ShouldDeletePod(pvc *corev1.PersistentVolumeClaim) bool {
×
UNCOV
865
        return pvc.GetAnnotations()[AnnPodRetainAfterCompletion] != "true" || pvc.GetAnnotations()[AnnRequiresScratch] == "true" || pvc.GetAnnotations()[AnnRequiresDirectIO] == "true" || pvc.DeletionTimestamp != nil
×
866
}
×
867

868
// AddFinalizer adds a finalizer to a resource
UNCOV
869
func AddFinalizer(obj metav1.Object, name string) {
×
UNCOV
870
        if HasFinalizer(obj, name) {
×
871
                return
×
872
        }
×
873

874
        obj.SetFinalizers(append(obj.GetFinalizers(), name))
×
875
}
876

877
// RemoveFinalizer removes a finalizer from a resource
UNCOV
878
func RemoveFinalizer(obj metav1.Object, name string) {
×
UNCOV
879
        if !HasFinalizer(obj, name) {
×
880
                return
×
881
        }
×
882

883
        var finalizers []string
×
UNCOV
884
        for _, f := range obj.GetFinalizers() {
×
885
                if f != name {
×
886
                        finalizers = append(finalizers, f)
×
887
                }
×
888
        }
889

UNCOV
890
        obj.SetFinalizers(finalizers)
×
891
}
892

893
// HasFinalizer returns true if a resource has a specific finalizer
UNCOV
894
func HasFinalizer(object metav1.Object, value string) bool {
×
UNCOV
895
        for _, f := range object.GetFinalizers() {
×
896
                if f == value {
×
897
                        return true
×
898
                }
×
899
        }
900
        return false
×
901
}
902

903
// ValidateCloneTokenPVC validates clone token for source and target PVCs
UNCOV
904
func ValidateCloneTokenPVC(t string, v token.Validator, source, target *corev1.PersistentVolumeClaim) error {
×
UNCOV
905
        if source.Namespace == target.Namespace {
×
906
                return nil
×
907
        }
×
908

909
        tokenData, err := v.Validate(t)
×
UNCOV
910
        if err != nil {
×
911
                return errors.Wrap(err, "error verifying token")
×
912
        }
×
913

914
        tokenResourceName := getTokenResourceNamePvc(source)
×
UNCOV
915
        srcName := getSourceNamePvc(source)
×
916

×
917
        return validateTokenData(tokenData, source.Namespace, srcName, target.Namespace, target.Name, string(target.UID), tokenResourceName)
×
918
}
919

920
// ValidateCloneTokenDV validates clone token for DV
UNCOV
921
func ValidateCloneTokenDV(validator token.Validator, dv *cdiv1.DataVolume) error {
×
UNCOV
922
        _, sourceName, sourceNamespace := GetCloneSourceInfo(dv)
×
923
        if sourceNamespace == "" || sourceNamespace == dv.Namespace {
×
924
                return nil
×
925
        }
×
926

927
        tok, ok := dv.Annotations[AnnCloneToken]
×
UNCOV
928
        if !ok {
×
929
                return errors.New("clone token missing")
×
930
        }
×
931

932
        tokenData, err := validator.Validate(tok)
×
UNCOV
933
        if err != nil {
×
934
                return errors.Wrap(err, "error verifying token")
×
935
        }
×
936

937
        tokenResourceName := getTokenResourceNameDataVolume(dv.Spec.Source)
×
UNCOV
938
        if tokenResourceName == "" {
×
939
                return errors.New("token resource name empty, can't verify properly")
×
940
        }
×
941

942
        return validateTokenData(tokenData, sourceNamespace, sourceName, dv.Namespace, dv.Name, "", tokenResourceName)
×
943
}
944

UNCOV
945
func getTokenResourceNameDataVolume(source *cdiv1.DataVolumeSource) string {
×
UNCOV
946
        if source.PVC != nil {
×
947
                return "persistentvolumeclaims"
×
948
        } else if source.Snapshot != nil {
×
949
                return "volumesnapshots"
×
950
        }
×
951

952
        return ""
×
953
}
954

UNCOV
955
func getTokenResourceNamePvc(sourcePvc *corev1.PersistentVolumeClaim) string {
×
UNCOV
956
        if v, ok := sourcePvc.Labels[common.CDIComponentLabel]; ok && v == common.CloneFromSnapshotFallbackPVCCDILabel {
×
957
                return "volumesnapshots"
×
958
        }
×
959

960
        return "persistentvolumeclaims"
×
961
}
962

UNCOV
963
func getSourceNamePvc(sourcePvc *corev1.PersistentVolumeClaim) string {
×
UNCOV
964
        if v, ok := sourcePvc.Labels[common.CDIComponentLabel]; ok && v == common.CloneFromSnapshotFallbackPVCCDILabel {
×
965
                if sourcePvc.Spec.DataSourceRef != nil {
×
966
                        return sourcePvc.Spec.DataSourceRef.Name
×
967
                }
×
968
        }
969

UNCOV
970
        return sourcePvc.Name
×
971
}
972

UNCOV
973
func validateTokenData(tokenData *token.Payload, srcNamespace, srcName, targetNamespace, targetName, targetUID, tokenResourceName string) error {
×
UNCOV
974
        uid := tokenData.Params["uid"]
×
975
        if tokenData.Operation != token.OperationClone ||
×
976
                tokenData.Name != srcName ||
×
977
                tokenData.Namespace != srcNamespace ||
×
978
                tokenData.Resource.Resource != tokenResourceName ||
×
979
                tokenData.Params["targetNamespace"] != targetNamespace ||
×
980
                tokenData.Params["targetName"] != targetName ||
×
981
                (uid != "" && uid != targetUID) {
×
982
                return errors.New("invalid token")
×
983
        }
×
984

985
        return nil
×
986
}
987

988
// IsSnapshotValidForClone returns an error if the passed snapshot is not valid for cloning
UNCOV
989
func IsSnapshotValidForClone(sourceSnapshot *snapshotv1.VolumeSnapshot) error {
×
UNCOV
990
        if sourceSnapshot.Status == nil {
×
991
                return fmt.Errorf("no status on source snapshot yet")
×
992
        }
×
993
        if !IsSnapshotReady(sourceSnapshot) {
×
994
                klog.V(3).Info("snapshot not ReadyToUse, while we allow this, probably going to be an issue going forward", "namespace", sourceSnapshot.Namespace, "name", sourceSnapshot.Name)
×
995
        }
×
996
        if sourceSnapshot.Status.Error != nil {
×
997
                errMessage := "no details"
×
998
                if msg := sourceSnapshot.Status.Error.Message; msg != nil {
×
999
                        errMessage = *msg
×
1000
                }
×
1001
                return fmt.Errorf("snapshot in error state with msg: %s", errMessage)
×
1002
        }
1003
        if sourceSnapshot.Spec.VolumeSnapshotClassName == nil ||
×
UNCOV
1004
                *sourceSnapshot.Spec.VolumeSnapshotClassName == "" {
×
1005
                return fmt.Errorf("snapshot %s/%s does not have volume snap class populated, can't clone", sourceSnapshot.Name, sourceSnapshot.Namespace)
×
1006
        }
×
1007
        return nil
×
1008
}
1009

1010
// AddAnnotation adds an annotation to an object
1011
func AddAnnotation(obj metav1.Object, key, value string) {
1✔
1012
        if obj.GetAnnotations() == nil {
2✔
1013
                obj.SetAnnotations(make(map[string]string))
1✔
1014
        }
1✔
1015
        obj.GetAnnotations()[key] = value
1✔
1016
}
1017

1018
// AddLabel adds a label to an object
1019
func AddLabel(obj metav1.Object, key, value string) {
1✔
1020
        if obj.GetLabels() == nil {
2✔
1021
                obj.SetLabels(make(map[string]string))
1✔
1022
        }
1✔
1023
        obj.GetLabels()[key] = value
1✔
1024
}
1025

1026
// HandleFailedPod handles pod-creation errors and updates the pod's PVC without providing sensitive information
UNCOV
1027
func HandleFailedPod(err error, podName string, pvc *corev1.PersistentVolumeClaim, recorder record.EventRecorder, c client.Client) error {
×
UNCOV
1028
        if err == nil {
×
1029
                return nil
×
1030
        }
×
1031
        // Generic reason and msg to avoid providing sensitive information
1032
        reason := ErrStartingPod
×
UNCOV
1033
        msg := fmt.Sprintf(MessageErrStartingPod, podName)
×
1034

×
1035
        // Error handling to fine-tune the event with pertinent info
×
1036
        if ErrQuotaExceeded(err) {
×
1037
                reason = ErrExceededQuota
×
1038
        }
×
1039

1040
        recorder.Event(pvc, corev1.EventTypeWarning, reason, msg)
×
UNCOV
1041

×
1042
        if isCloneSourcePod := CreateCloneSourcePodName(pvc) == podName; isCloneSourcePod {
×
1043
                AddAnnotation(pvc, AnnSourceRunningCondition, "false")
×
1044
                AddAnnotation(pvc, AnnSourceRunningConditionReason, reason)
×
1045
                AddAnnotation(pvc, AnnSourceRunningConditionMessage, msg)
×
1046
        } else {
×
1047
                AddAnnotation(pvc, AnnRunningCondition, "false")
×
1048
                AddAnnotation(pvc, AnnRunningConditionReason, reason)
×
1049
                AddAnnotation(pvc, AnnRunningConditionMessage, msg)
×
1050
        }
×
1051

1052
        AddAnnotation(pvc, AnnPodPhase, string(corev1.PodFailed))
×
UNCOV
1053
        if err := c.Update(context.TODO(), pvc); err != nil {
×
1054
                return err
×
1055
        }
×
1056

1057
        return err
×
1058
}
1059

1060
// GetSource returns the source string which determines the type of source. If no source or invalid source found, default to http
UNCOV
1061
func GetSource(pvc *corev1.PersistentVolumeClaim) string {
×
UNCOV
1062
        source, found := pvc.Annotations[AnnSource]
×
1063
        if !found {
×
1064
                source = ""
×
1065
        }
×
1066
        switch source {
×
1067
        case
1068
                SourceHTTP,
1069
                SourceS3,
1070
                SourceGCS,
1071
                SourceGlance,
1072
                SourceNone,
1073
                SourceRegistry,
1074
                SourceImageio,
UNCOV
1075
                SourceVDDK:
×
UNCOV
1076
        default:
×
1077
                source = SourceHTTP
×
1078
        }
1079
        return source
×
1080
}
1081

1082
// GetEndpoint returns the endpoint string which contains the full path URI of the target object to be copied.
UNCOV
1083
func GetEndpoint(pvc *corev1.PersistentVolumeClaim) (string, error) {
×
UNCOV
1084
        ep, found := pvc.Annotations[AnnEndpoint]
×
1085
        if !found || ep == "" {
×
1086
                verb := "empty"
×
1087
                if !found {
×
1088
                        verb = "missing"
×
1089
                }
×
1090
                return ep, errors.Errorf("annotation %q in pvc \"%s/%s\" is %s", AnnEndpoint, pvc.Namespace, pvc.Name, verb)
×
1091
        }
1092
        return ep, nil
×
1093
}
1094

1095
// AddImportVolumeMounts is being called for pods using PV with filesystem volume mode
UNCOV
1096
func AddImportVolumeMounts() []corev1.VolumeMount {
×
UNCOV
1097
        volumeMounts := []corev1.VolumeMount{
×
1098
                {
×
1099
                        Name:      DataVolName,
×
1100
                        MountPath: common.ImporterDataDir,
×
1101
                },
×
1102
        }
×
1103
        return volumeMounts
×
1104
}
×
1105

1106
// GetEffectiveStorageResources returns the maximum of the passed storageResources and the storageProfile minimumSupportedPVCSize.
1107
// If the passed storageResources has no size, it is returned as-is.
1108
func GetEffectiveStorageResources(ctx context.Context, client client.Client, storageResources corev1.VolumeResourceRequirements,
UNCOV
1109
        storageClassName *string, contentType cdiv1.DataVolumeContentType, log logr.Logger) (corev1.VolumeResourceRequirements, error) {
×
UNCOV
1110
        sc, err := GetStorageClassByNameWithVirtFallback(ctx, client, storageClassName, contentType)
×
1111
        if err != nil || sc == nil {
×
1112
                return storageResources, err
×
1113
        }
×
1114

1115
        requestedSize, hasSize := storageResources.Requests[corev1.ResourceStorage]
×
UNCOV
1116
        if !hasSize {
×
1117
                return storageResources, nil
×
1118
        }
×
1119

1120
        if requestedSize, err = GetEffectiveVolumeSize(ctx, client, requestedSize, sc.Name, &log); err != nil {
×
UNCOV
1121
                return storageResources, err
×
1122
        }
×
1123

1124
        return corev1.VolumeResourceRequirements{
×
UNCOV
1125
                Requests: corev1.ResourceList{
×
1126
                        corev1.ResourceStorage: requestedSize,
×
1127
                },
×
1128
        }, nil
×
1129
}
1130

1131
// GetEffectiveVolumeSize returns the maximum of the passed requestedSize and the storageProfile minimumSupportedPVCSize.
UNCOV
1132
func GetEffectiveVolumeSize(ctx context.Context, client client.Client, requestedSize resource.Quantity, storageClassName string, log *logr.Logger) (resource.Quantity, error) {
×
UNCOV
1133
        storageProfile := &cdiv1.StorageProfile{}
×
1134
        if err := client.Get(ctx, types.NamespacedName{Name: storageClassName}, storageProfile); err != nil {
×
1135
                return requestedSize, IgnoreNotFound(err)
×
1136
        }
×
1137

1138
        if val, exists := storageProfile.Annotations[AnnMinimumSupportedPVCSize]; exists {
×
UNCOV
1139
                if minSize, err := resource.ParseQuantity(val); err == nil {
×
1140
                        if requestedSize.Cmp(minSize) == -1 {
×
1141
                                return minSize, nil
×
1142
                        }
×
1143
                } else if log != nil {
×
1144
                        log.V(1).Info("Invalid minimum PVC size in annotation", "value", val, "error", err)
×
1145
                }
×
1146
        }
1147

UNCOV
1148
        return requestedSize, nil
×
1149
}
1150

1151
// ValidateRequestedCloneSize validates the clone size requirements on block
UNCOV
1152
func ValidateRequestedCloneSize(sourceResources, targetResources corev1.VolumeResourceRequirements) error {
×
UNCOV
1153
        sourceRequest, hasSource := sourceResources.Requests[corev1.ResourceStorage]
×
1154
        targetRequest, hasTarget := targetResources.Requests[corev1.ResourceStorage]
×
1155
        if !hasSource || !hasTarget {
×
1156
                return errors.New("source/target missing storage resource requests")
×
1157
        }
×
1158

1159
        // Verify that the target PVC size is equal or larger than the source.
UNCOV
1160
        if sourceRequest.Value() > targetRequest.Value() {
×
UNCOV
1161
                return errors.Errorf("target resources requests storage size is smaller than the source %d < %d", targetRequest.Value(), sourceRequest.Value())
×
1162
        }
×
1163
        return nil
×
1164
}
1165

1166
// CreateCloneSourcePodName creates clone source pod name
UNCOV
1167
func CreateCloneSourcePodName(targetPvc *corev1.PersistentVolumeClaim) string {
×
UNCOV
1168
        return string(targetPvc.GetUID()) + common.ClonerSourcePodNameSuffix
×
1169
}
×
1170

1171
// IsPVCComplete returns true if a PVC is in 'Succeeded' phase, false if not
UNCOV
1172
func IsPVCComplete(pvc *corev1.PersistentVolumeClaim) bool {
×
UNCOV
1173
        if pvc != nil {
×
1174
                phase, exists := pvc.ObjectMeta.Annotations[AnnPodPhase]
×
1175
                return exists && (phase == string(corev1.PodSucceeded))
×
1176
        }
×
1177
        return false
×
1178
}
1179

1180
// IsMultiStageImportInProgress returns true when a PVC is being part of an ongoing multi-stage import
UNCOV
1181
func IsMultiStageImportInProgress(pvc *corev1.PersistentVolumeClaim) bool {
×
UNCOV
1182
        if pvc != nil {
×
1183
                multiStageImport := metav1.HasAnnotation(pvc.ObjectMeta, AnnCurrentCheckpoint)
×
1184
                multiStageAlreadyDone := metav1.HasAnnotation(pvc.ObjectMeta, AnnMultiStageImportDone)
×
1185
                return multiStageImport && !multiStageAlreadyDone
×
1186
        }
×
1187
        return false
×
1188
}
1189

1190
// SetRestrictedSecurityContext sets the pod security params to be compatible with restricted PSA
UNCOV
1191
func SetRestrictedSecurityContext(podSpec *corev1.PodSpec) {
×
UNCOV
1192
        hasVolumeMounts := false
×
1193
        for _, containers := range [][]corev1.Container{podSpec.InitContainers, podSpec.Containers} {
×
1194
                for i := range containers {
×
1195
                        container := &containers[i]
×
1196
                        if container.SecurityContext == nil {
×
1197
                                container.SecurityContext = &corev1.SecurityContext{}
×
1198
                        }
×
1199
                        container.SecurityContext.Capabilities = &corev1.Capabilities{
×
1200
                                Drop: []corev1.Capability{
×
1201
                                        "ALL",
×
1202
                                },
×
1203
                        }
×
1204
                        container.SecurityContext.SeccompProfile = &corev1.SeccompProfile{
×
1205
                                Type: corev1.SeccompProfileTypeRuntimeDefault,
×
1206
                        }
×
1207
                        container.SecurityContext.AllowPrivilegeEscalation = ptr.To[bool](false)
×
1208
                        container.SecurityContext.RunAsNonRoot = ptr.To[bool](true)
×
1209
                        container.SecurityContext.RunAsUser = ptr.To[int64](common.QemuSubGid)
×
1210
                        if len(container.VolumeMounts) > 0 {
×
1211
                                hasVolumeMounts = true
×
1212
                        }
×
1213
                }
1214
        }
1215

UNCOV
1216
        if podSpec.SecurityContext == nil {
×
UNCOV
1217
                podSpec.SecurityContext = &corev1.PodSecurityContext{}
×
1218
        }
×
1219
        // Some tools like istio inject containers and thus rely on a pod level seccomp profile being specified
1220
        podSpec.SecurityContext.SeccompProfile = &corev1.SeccompProfile{
×
UNCOV
1221
                Type: corev1.SeccompProfileTypeRuntimeDefault,
×
1222
        }
×
1223
        if hasVolumeMounts {
×
1224
                podSpec.SecurityContext.FSGroup = ptr.To[int64](common.QemuSubGid)
×
1225
        }
×
1226
}
1227

1228
// SetNodeNameIfPopulator sets NodeName in a pod spec when the PVC is being handled by a CDI volume populator
UNCOV
1229
func SetNodeNameIfPopulator(pvc *corev1.PersistentVolumeClaim, podSpec *corev1.PodSpec) {
×
UNCOV
1230
        _, isPopulator := pvc.Annotations[AnnPopulatorKind]
×
1231
        nodeName := pvc.Annotations[AnnSelectedNode]
×
1232
        if isPopulator && nodeName != "" {
×
1233
                podSpec.NodeName = nodeName
×
1234
        }
×
1235
}
1236

1237
// CreatePvc creates PVC
1238
func CreatePvc(name, ns string, annotations, labels map[string]string) *corev1.PersistentVolumeClaim {
1✔
1239
        return CreatePvcInStorageClass(name, ns, nil, annotations, labels, corev1.ClaimBound)
1✔
1240
}
1✔
1241

1242
// CreatePvcInStorageClass creates PVC with storgae class
1243
func CreatePvcInStorageClass(name, ns string, storageClassName *string, annotations, labels map[string]string, phase corev1.PersistentVolumeClaimPhase) *corev1.PersistentVolumeClaim {
1✔
1244
        pvc := &corev1.PersistentVolumeClaim{
1✔
1245
                ObjectMeta: metav1.ObjectMeta{
1✔
1246
                        Name:        name,
1✔
1247
                        Namespace:   ns,
1✔
1248
                        Annotations: annotations,
1✔
1249
                        Labels:      labels,
1✔
1250
                        UID:         types.UID(ns + "-" + name),
1✔
1251
                },
1✔
1252
                Spec: corev1.PersistentVolumeClaimSpec{
1✔
1253
                        AccessModes: []corev1.PersistentVolumeAccessMode{corev1.ReadOnlyMany, corev1.ReadWriteOnce},
1✔
1254
                        Resources: corev1.VolumeResourceRequirements{
1✔
1255
                                Requests: corev1.ResourceList{
1✔
1256
                                        corev1.ResourceStorage: resource.MustParse("1G"),
1✔
1257
                                },
1✔
1258
                        },
1✔
1259
                        StorageClassName: storageClassName,
1✔
1260
                },
1✔
1261
                Status: corev1.PersistentVolumeClaimStatus{
1✔
1262
                        Phase: phase,
1✔
1263
                },
1✔
1264
        }
1✔
1265
        pvc.Status.Capacity = pvc.Spec.Resources.Requests.DeepCopy()
1✔
1266
        if pvc.Status.Phase == corev1.ClaimBound {
2✔
1267
                pvc.Spec.VolumeName = "pv-" + string(pvc.UID)
1✔
1268
        }
1✔
1269
        return pvc
1✔
1270
}
1271

1272
// GetAPIServerKey returns API server RSA key
UNCOV
1273
func GetAPIServerKey() *rsa.PrivateKey {
×
UNCOV
1274
        apiServerKeyOnce.Do(func() {
×
1275
                apiServerKey, _ = rsa.GenerateKey(rand.Reader, 2048)
×
1276
        })
×
1277
        return apiServerKey
×
1278
}
1279

1280
// CreateStorageClass creates storage class CR
1281
func CreateStorageClass(name string, annotations map[string]string) *storagev1.StorageClass {
1✔
1282
        return &storagev1.StorageClass{
1✔
1283
                ObjectMeta: metav1.ObjectMeta{
1✔
1284
                        Name:        name,
1✔
1285
                        Annotations: annotations,
1✔
1286
                },
1✔
1287
        }
1✔
1288
}
1✔
1289

1290
// CreateImporterTestPod creates importer test pod CR
UNCOV
1291
func CreateImporterTestPod(pvc *corev1.PersistentVolumeClaim, dvname string, scratchPvc *corev1.PersistentVolumeClaim) *corev1.Pod {
×
UNCOV
1292
        // importer pod name contains the pvc name
×
1293
        podName := fmt.Sprintf("%s-%s", common.ImporterPodName, pvc.Name)
×
1294

×
1295
        blockOwnerDeletion := true
×
1296
        isController := true
×
1297

×
1298
        volumes := []corev1.Volume{
×
1299
                {
×
1300
                        Name: dvname,
×
1301
                        VolumeSource: corev1.VolumeSource{
×
1302
                                PersistentVolumeClaim: &corev1.PersistentVolumeClaimVolumeSource{
×
1303
                                        ClaimName: pvc.Name,
×
1304
                                        ReadOnly:  false,
×
1305
                                },
×
1306
                        },
×
1307
                },
×
1308
        }
×
1309

×
1310
        if scratchPvc != nil {
×
1311
                volumes = append(volumes, corev1.Volume{
×
1312
                        Name: ScratchVolName,
×
1313
                        VolumeSource: corev1.VolumeSource{
×
1314
                                PersistentVolumeClaim: &corev1.PersistentVolumeClaimVolumeSource{
×
1315
                                        ClaimName: scratchPvc.Name,
×
1316
                                        ReadOnly:  false,
×
1317
                                },
×
1318
                        },
×
1319
                })
×
1320
        }
×
1321

1322
        pod := &corev1.Pod{
×
UNCOV
1323
                TypeMeta: metav1.TypeMeta{
×
1324
                        Kind:       "Pod",
×
1325
                        APIVersion: "v1",
×
1326
                },
×
1327
                ObjectMeta: metav1.ObjectMeta{
×
1328
                        Name:      podName,
×
1329
                        Namespace: pvc.Namespace,
×
1330
                        Annotations: map[string]string{
×
1331
                                AnnCreatedBy: "yes",
×
1332
                        },
×
1333
                        Labels: map[string]string{
×
1334
                                common.CDILabelKey:        common.CDILabelValue,
×
1335
                                common.CDIComponentLabel:  common.ImporterPodName,
×
1336
                                common.PrometheusLabelKey: common.PrometheusLabelValue,
×
1337
                        },
×
1338
                        OwnerReferences: []metav1.OwnerReference{
×
1339
                                {
×
1340
                                        APIVersion:         "v1",
×
1341
                                        Kind:               "PersistentVolumeClaim",
×
1342
                                        Name:               pvc.Name,
×
1343
                                        UID:                pvc.GetUID(),
×
1344
                                        BlockOwnerDeletion: &blockOwnerDeletion,
×
1345
                                        Controller:         &isController,
×
1346
                                },
×
1347
                        },
×
1348
                },
×
1349
                Spec: corev1.PodSpec{
×
1350
                        Containers: []corev1.Container{
×
1351
                                {
×
1352
                                        Name:            common.ImporterPodName,
×
1353
                                        Image:           "test/myimage",
×
1354
                                        ImagePullPolicy: corev1.PullPolicy("Always"),
×
1355
                                        Args:            []string{"-v=5"},
×
1356
                                        Ports: []corev1.ContainerPort{
×
1357
                                                {
×
1358
                                                        Name:          "metrics",
×
1359
                                                        ContainerPort: 8443,
×
1360
                                                        Protocol:      corev1.ProtocolTCP,
×
1361
                                                },
×
1362
                                        },
×
1363
                                },
×
1364
                        },
×
1365
                        RestartPolicy: corev1.RestartPolicyOnFailure,
×
1366
                        Volumes:       volumes,
×
1367
                },
×
1368
        }
×
1369

×
1370
        ep, _ := GetEndpoint(pvc)
×
1371
        source := GetSource(pvc)
×
1372
        contentType := GetPVCContentType(pvc)
×
1373
        imageSize, _ := GetRequestedImageSize(pvc)
×
1374
        volumeMode := GetVolumeMode(pvc)
×
1375

×
1376
        env := []corev1.EnvVar{
×
1377
                {
×
1378
                        Name:  common.ImporterSource,
×
1379
                        Value: source,
×
1380
                },
×
1381
                {
×
1382
                        Name:  common.ImporterEndpoint,
×
1383
                        Value: ep,
×
1384
                },
×
1385
                {
×
1386
                        Name:  common.ImporterContentType,
×
1387
                        Value: string(contentType),
×
1388
                },
×
1389
                {
×
1390
                        Name:  common.ImporterImageSize,
×
1391
                        Value: imageSize,
×
1392
                },
×
1393
                {
×
1394
                        Name:  common.OwnerUID,
×
1395
                        Value: string(pvc.UID),
×
1396
                },
×
1397
                {
×
1398
                        Name:  common.InsecureTLSVar,
×
1399
                        Value: "false",
×
1400
                },
×
1401
        }
×
1402
        pod.Spec.Containers[0].Env = env
×
1403
        if volumeMode == corev1.PersistentVolumeBlock {
×
1404
                pod.Spec.Containers[0].VolumeDevices = AddVolumeDevices()
×
1405
        } else {
×
1406
                pod.Spec.Containers[0].VolumeMounts = AddImportVolumeMounts()
×
1407
        }
×
1408

1409
        if scratchPvc != nil {
×
UNCOV
1410
                pod.Spec.Containers[0].VolumeMounts = append(pod.Spec.Containers[0].VolumeMounts, corev1.VolumeMount{
×
1411
                        Name:      ScratchVolName,
×
1412
                        MountPath: common.ScratchDataDir,
×
1413
                })
×
1414
        }
×
1415

1416
        return pod
×
1417
}
1418

1419
// CreateStorageClassWithProvisioner creates CR of storage class with provisioner
UNCOV
1420
func CreateStorageClassWithProvisioner(name string, annotations, labels map[string]string, provisioner string) *storagev1.StorageClass {
×
UNCOV
1421
        return &storagev1.StorageClass{
×
1422
                Provisioner: provisioner,
×
1423
                ObjectMeta: metav1.ObjectMeta{
×
1424
                        Name:        name,
×
1425
                        Annotations: annotations,
×
1426
                        Labels:      labels,
×
1427
                },
×
1428
        }
×
1429
}
×
1430

1431
// CreateClient creates a fake client
1432
func CreateClient(objs ...runtime.Object) client.Client {
1✔
1433
        s := scheme.Scheme
1✔
1434
        _ = cdiv1.AddToScheme(s)
1✔
1435
        _ = corev1.AddToScheme(s)
1✔
1436
        _ = storagev1.AddToScheme(s)
1✔
1437
        _ = ocpconfigv1.Install(s)
1✔
1438

1✔
1439
        return fake.NewClientBuilder().WithScheme(s).WithRuntimeObjects(objs...).Build()
1✔
1440
}
1✔
1441

1442
// ErrQuotaExceeded checked is the error is of exceeded quota
UNCOV
1443
func ErrQuotaExceeded(err error) bool {
×
UNCOV
1444
        return strings.Contains(err.Error(), "exceeded quota:")
×
1445
}
×
1446

1447
// GetContentType returns the content type. If invalid or not set, default to kubevirt
1448
func GetContentType(contentType cdiv1.DataVolumeContentType) cdiv1.DataVolumeContentType {
1✔
1449
        switch contentType {
1✔
1450
        case
1451
                cdiv1.DataVolumeKubeVirt,
1452
                cdiv1.DataVolumeArchive:
1✔
UNCOV
1453
        default:
×
UNCOV
1454
                // TODO - shouldn't archive be the default?
×
1455
                contentType = cdiv1.DataVolumeKubeVirt
×
1456
        }
1457
        return contentType
1✔
1458
}
1459

1460
// GetPVCContentType returns the content type of the source image. If invalid or not set, default to kubevirt
UNCOV
1461
func GetPVCContentType(pvc *corev1.PersistentVolumeClaim) cdiv1.DataVolumeContentType {
×
UNCOV
1462
        contentType, found := pvc.Annotations[AnnContentType]
×
1463
        if !found {
×
1464
                // TODO - shouldn't archive be the default?
×
1465
                return cdiv1.DataVolumeKubeVirt
×
1466
        }
×
1467

1468
        return GetContentType(cdiv1.DataVolumeContentType(contentType))
×
1469
}
1470

1471
// GetNamespace returns the given namespace if not empty, otherwise the default namespace
UNCOV
1472
func GetNamespace(namespace, defaultNamespace string) string {
×
UNCOV
1473
        if namespace == "" {
×
1474
                return defaultNamespace
×
1475
        }
×
1476
        return namespace
×
1477
}
1478

1479
// IsErrCacheNotStarted checked is the error is of cache not started
UNCOV
1480
func IsErrCacheNotStarted(err error) bool {
×
UNCOV
1481
        target := &runtimecache.ErrCacheNotStarted{}
×
1482
        return errors.As(err, &target)
×
1483
}
×
1484

1485
// NewImportDataVolume returns new import DataVolume CR
UNCOV
1486
func NewImportDataVolume(name string) *cdiv1.DataVolume {
×
UNCOV
1487
        return &cdiv1.DataVolume{
×
1488
                TypeMeta: metav1.TypeMeta{APIVersion: cdiv1.SchemeGroupVersion.String()},
×
1489
                ObjectMeta: metav1.ObjectMeta{
×
1490
                        Name:      name,
×
1491
                        Namespace: metav1.NamespaceDefault,
×
1492
                        UID:       types.UID(metav1.NamespaceDefault + "-" + name),
×
1493
                },
×
1494
                Spec: cdiv1.DataVolumeSpec{
×
1495
                        Source: &cdiv1.DataVolumeSource{
×
1496
                                HTTP: &cdiv1.DataVolumeSourceHTTP{
×
1497
                                        URL: "http://example.com/data",
×
1498
                                },
×
1499
                        },
×
1500
                        PVC: &corev1.PersistentVolumeClaimSpec{
×
1501
                                AccessModes: []corev1.PersistentVolumeAccessMode{corev1.ReadWriteOnce},
×
1502
                        },
×
1503
                        PriorityClassName: "p0",
×
1504
                },
×
1505
        }
×
1506
}
×
1507

1508
// GetCloneSourceInfo returns the type, name and namespace of the cloning source
UNCOV
1509
func GetCloneSourceInfo(dv *cdiv1.DataVolume) (sourceType, sourceName, sourceNamespace string) {
×
UNCOV
1510
        // Cloning sources are mutually exclusive
×
1511
        if dv.Spec.Source.PVC != nil {
×
1512
                return "pvc", dv.Spec.Source.PVC.Name, dv.Spec.Source.PVC.Namespace
×
1513
        }
×
1514

1515
        if dv.Spec.Source.Snapshot != nil {
×
UNCOV
1516
                return "snapshot", dv.Spec.Source.Snapshot.Name, dv.Spec.Source.Snapshot.Namespace
×
1517
        }
×
1518

1519
        return "", "", ""
×
1520
}
1521

1522
// IsWaitForFirstConsumerEnabled tells us if we should respect "real" WFFC behavior or just let our worker pods randomly spawn
UNCOV
1523
func IsWaitForFirstConsumerEnabled(obj metav1.Object, gates featuregates.FeatureGates) (bool, error) {
×
UNCOV
1524
        // when PVC requests immediateBinding it cannot honor wffc logic
×
1525
        isImmediateBindingRequested := ImmediateBindingRequested(obj)
×
1526
        pvcHonorWaitForFirstConsumer := !isImmediateBindingRequested
×
1527
        globalHonorWaitForFirstConsumer, err := gates.HonorWaitForFirstConsumerEnabled()
×
1528
        if err != nil {
×
1529
                return false, err
×
1530
        }
×
1531

1532
        return pvcHonorWaitForFirstConsumer && globalHonorWaitForFirstConsumer, nil
×
1533
}
1534

1535
// AddImmediateBindingAnnotationIfWFFCDisabled adds the immediateBinding annotation if wffc feature gate is disabled
UNCOV
1536
func AddImmediateBindingAnnotationIfWFFCDisabled(obj metav1.Object, gates featuregates.FeatureGates) error {
×
UNCOV
1537
        globalHonorWaitForFirstConsumer, err := gates.HonorWaitForFirstConsumerEnabled()
×
1538
        if err != nil {
×
1539
                return err
×
1540
        }
×
1541
        if !globalHonorWaitForFirstConsumer {
×
1542
                AddAnnotation(obj, AnnImmediateBinding, "")
×
1543
        }
×
1544
        return nil
×
1545
}
1546

1547
// InflateSizeWithOverhead inflates a storage size with proper overhead calculations
UNCOV
1548
func InflateSizeWithOverhead(ctx context.Context, c client.Client, imgSize int64, pvcSpec *corev1.PersistentVolumeClaimSpec) (resource.Quantity, error) {
×
UNCOV
1549
        var returnSize resource.Quantity
×
1550

×
1551
        if util.ResolveVolumeMode(pvcSpec.VolumeMode) == corev1.PersistentVolumeFilesystem {
×
1552
                fsOverhead, err := GetFilesystemOverheadForStorageClass(ctx, c, pvcSpec.StorageClassName)
×
1553
                if err != nil {
×
1554
                        return resource.Quantity{}, err
×
1555
                }
×
1556
                // Parse filesystem overhead (percentage) into a 64-bit float
1557
                fsOverheadFloat, _ := strconv.ParseFloat(string(fsOverhead), 64)
×
UNCOV
1558

×
1559
                // Merge the previous values into a 'resource.Quantity' struct
×
1560
                requiredSpace := util.GetRequiredSpace(fsOverheadFloat, imgSize)
×
1561
                returnSize = *resource.NewScaledQuantity(requiredSpace, 0)
×
1562
        } else {
×
1563
                // Inflation is not needed with 'Block' mode
×
1564
                returnSize = *resource.NewScaledQuantity(imgSize, 0)
×
1565
        }
×
1566

1567
        return returnSize, nil
×
1568
}
1569

1570
// IsBound returns if the pvc is bound
UNCOV
1571
func IsBound(pvc *corev1.PersistentVolumeClaim) bool {
×
UNCOV
1572
        return pvc != nil && pvc.Status.Phase == corev1.ClaimBound
×
1573
}
×
1574

1575
// IsUnbound returns if the pvc is not bound yet
UNCOV
1576
func IsUnbound(pvc *corev1.PersistentVolumeClaim) bool {
×
UNCOV
1577
        return !IsBound(pvc)
×
1578
}
×
1579

1580
// IsLost returns if the pvc is lost
UNCOV
1581
func IsLost(pvc *corev1.PersistentVolumeClaim) bool {
×
UNCOV
1582
        return pvc != nil && pvc.Status.Phase == corev1.ClaimLost
×
1583
}
×
1584

1585
// IsImageStream returns true if registry source is ImageStream
UNCOV
1586
func IsImageStream(pvc *corev1.PersistentVolumeClaim) bool {
×
UNCOV
1587
        return pvc.Annotations[AnnRegistryImageStream] == "true"
×
1588
}
×
1589

1590
// ShouldIgnorePod checks if a pod should be ignored.
1591
// If this is a completed pod that was used for one checkpoint of a multi-stage import, it
1592
// should be ignored by pod lookups as long as the retainAfterCompletion annotation is set.
UNCOV
1593
func ShouldIgnorePod(pod *corev1.Pod, pvc *corev1.PersistentVolumeClaim) bool {
×
UNCOV
1594
        retain := pvc.ObjectMeta.Annotations[AnnPodRetainAfterCompletion]
×
1595
        checkpoint := pvc.ObjectMeta.Annotations[AnnCurrentCheckpoint]
×
1596
        if checkpoint != "" && pod.Status.Phase == corev1.PodSucceeded {
×
1597
                return retain == "true"
×
1598
        }
×
1599
        return false
×
1600
}
1601

1602
// BuildHTTPClient generates an http client that accepts any certificate, since we are using
1603
// it to get prometheus data it doesn't matter if someone can intercept the data. Once we have
1604
// a mechanism to properly sign the server, we can update this method to get a proper client.
UNCOV
1605
func BuildHTTPClient(httpClient *http.Client) *http.Client {
×
UNCOV
1606
        if httpClient == nil {
×
1607
                defaultTransport := http.DefaultTransport.(*http.Transport)
×
1608
                // Create new Transport that ignores self-signed SSL
×
1609
                //nolint:gosec
×
1610
                tr := &http.Transport{
×
1611
                        Proxy:                 defaultTransport.Proxy,
×
1612
                        DialContext:           defaultTransport.DialContext,
×
1613
                        MaxIdleConns:          defaultTransport.MaxIdleConns,
×
1614
                        IdleConnTimeout:       defaultTransport.IdleConnTimeout,
×
1615
                        ExpectContinueTimeout: defaultTransport.ExpectContinueTimeout,
×
1616
                        TLSHandshakeTimeout:   defaultTransport.TLSHandshakeTimeout,
×
1617
                        TLSClientConfig:       &tls.Config{InsecureSkipVerify: true},
×
1618
                }
×
1619
                httpClient = &http.Client{
×
1620
                        Transport: tr,
×
1621
                }
×
1622
        }
×
1623
        return httpClient
×
1624
}
1625

1626
// ErrConnectionRefused checks for connection refused errors
UNCOV
1627
func ErrConnectionRefused(err error) bool {
×
UNCOV
1628
        return strings.Contains(err.Error(), "connection refused")
×
1629
}
×
1630

1631
// GetPodMetricsPort returns, if exists, the metrics port from the passed pod
1632
func GetPodMetricsPort(pod *corev1.Pod) (int, error) {
1✔
1633
        for _, container := range pod.Spec.Containers {
2✔
1634
                for _, port := range container.Ports {
2✔
1635
                        if port.Name == "metrics" {
2✔
1636
                                return int(port.ContainerPort), nil
1✔
1637
                        }
1✔
1638
                }
1639
        }
1640
        return 0, errors.New("Metrics port not found in pod")
1✔
1641
}
1642

1643
// GetMetricsURL builds the metrics URL according to the specified pod
1644
func GetMetricsURL(pod *corev1.Pod) (string, error) {
1✔
1645
        if pod == nil {
1✔
UNCOV
1646
                return "", nil
×
UNCOV
1647
        }
×
1648
        port, err := GetPodMetricsPort(pod)
1✔
1649
        if err != nil || pod.Status.PodIP == "" {
2✔
1650
                return "", err
1✔
1651
        }
1✔
1652
        domain := net.JoinHostPort(pod.Status.PodIP, fmt.Sprint(port))
1✔
1653
        url := fmt.Sprintf("https://%s/metrics", domain)
1✔
1654
        return url, nil
1✔
1655
}
1656

1657
// GetProgressReportFromURL fetches the progress report from the passed URL according to an specific metric expression and ownerUID
UNCOV
1658
func GetProgressReportFromURL(ctx context.Context, url string, httpClient *http.Client, metricExp, ownerUID string) (string, error) {
×
UNCOV
1659
        regExp := regexp.MustCompile(fmt.Sprintf("(%s)\\{ownerUID\\=%q\\} (\\d{1,3}\\.?\\d*)", metricExp, ownerUID))
×
1660
        // pod could be gone, don't block an entire thread for 30 seconds
×
1661
        // just to get back an i/o timeout
×
1662
        ctx, cancel := context.WithTimeout(ctx, 2*time.Second)
×
1663
        defer cancel()
×
1664
        req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
×
1665
        if err != nil {
×
1666
                return "", err
×
1667
        }
×
1668
        resp, err := httpClient.Do(req)
×
1669
        if err != nil {
×
1670
                if ErrConnectionRefused(err) {
×
1671
                        return "", nil
×
1672
                }
×
1673
                return "", err
×
1674
        }
1675
        defer resp.Body.Close()
×
UNCOV
1676
        body, err := io.ReadAll(resp.Body)
×
1677
        if err != nil {
×
1678
                return "", err
×
1679
        }
×
1680

1681
        // Parse the progress from the body
UNCOV
1682
        progressReport := ""
×
UNCOV
1683
        match := regExp.FindStringSubmatch(string(body))
×
1684
        if match != nil {
×
1685
                progressReport = match[len(match)-1]
×
1686
        }
×
1687
        return progressReport, nil
×
1688
}
1689

1690
// UpdateHTTPAnnotations updates the passed annotations for proper http import
UNCOV
1691
func UpdateHTTPAnnotations(annotations map[string]string, http *cdiv1.DataVolumeSourceHTTP) {
×
UNCOV
1692
        annotations[AnnEndpoint] = http.URL
×
1693
        annotations[AnnSource] = SourceHTTP
×
1694

×
1695
        if http.SecretRef != "" {
×
1696
                annotations[AnnSecret] = http.SecretRef
×
1697
        }
×
1698
        if http.CertConfigMap != "" {
×
1699
                annotations[AnnCertConfigMap] = http.CertConfigMap
×
1700
        }
×
NEW
1701
        if http.Checksum != nil && *http.Checksum != "" {
×
NEW
1702
                annotations[AnnChecksum] = *http.Checksum
×
NEW
1703
        }
×
1704
        for index, header := range http.ExtraHeaders {
×
1705
                annotations[fmt.Sprintf("%s.%d", AnnExtraHeaders, index)] = header
×
1706
        }
×
1707
        for index, header := range http.SecretExtraHeaders {
×
1708
                annotations[fmt.Sprintf("%s.%d", AnnSecretExtraHeaders, index)] = header
×
1709
        }
×
1710
}
1711

1712
// UpdateS3Annotations updates the passed annotations for proper S3 import
UNCOV
1713
func UpdateS3Annotations(annotations map[string]string, s3 *cdiv1.DataVolumeSourceS3) {
×
UNCOV
1714
        annotations[AnnEndpoint] = s3.URL
×
1715
        annotations[AnnSource] = SourceS3
×
1716
        if s3.SecretRef != "" {
×
1717
                annotations[AnnSecret] = s3.SecretRef
×
1718
        }
×
1719
        if s3.CertConfigMap != "" {
×
1720
                annotations[AnnCertConfigMap] = s3.CertConfigMap
×
1721
        }
×
1722
}
1723

1724
// UpdateGCSAnnotations updates the passed annotations for proper GCS import
UNCOV
1725
func UpdateGCSAnnotations(annotations map[string]string, gcs *cdiv1.DataVolumeSourceGCS) {
×
UNCOV
1726
        annotations[AnnEndpoint] = gcs.URL
×
1727
        annotations[AnnSource] = SourceGCS
×
1728
        if gcs.SecretRef != "" {
×
1729
                annotations[AnnSecret] = gcs.SecretRef
×
1730
        }
×
1731
}
1732

1733
// UpdateRegistryAnnotations updates the passed annotations for proper registry import
UNCOV
1734
func UpdateRegistryAnnotations(annotations map[string]string, registry *cdiv1.DataVolumeSourceRegistry) {
×
UNCOV
1735
        annotations[AnnSource] = SourceRegistry
×
1736
        pullMethod := registry.PullMethod
×
1737
        if pullMethod != nil && *pullMethod != "" {
×
1738
                annotations[AnnRegistryImportMethod] = string(*pullMethod)
×
1739
        }
×
1740
        url := registry.URL
×
1741
        if url != nil && *url != "" {
×
1742
                annotations[AnnEndpoint] = *url
×
1743
        } else {
×
1744
                imageStream := registry.ImageStream
×
1745
                if imageStream != nil && *imageStream != "" {
×
1746
                        annotations[AnnEndpoint] = *imageStream
×
1747
                        annotations[AnnRegistryImageStream] = "true"
×
1748
                }
×
1749
        }
1750
        secretRef := registry.SecretRef
×
UNCOV
1751
        if secretRef != nil && *secretRef != "" {
×
1752
                annotations[AnnSecret] = *secretRef
×
1753
        }
×
1754
        certConfigMap := registry.CertConfigMap
×
1755
        if certConfigMap != nil && *certConfigMap != "" {
×
1756
                annotations[AnnCertConfigMap] = *certConfigMap
×
1757
        }
×
1758

1759
        if registry.Platform != nil && registry.Platform.Architecture != "" {
×
UNCOV
1760
                annotations[AnnRegistryImageArchitecture] = registry.Platform.Architecture
×
1761
        }
×
1762
}
1763

1764
// UpdateVDDKAnnotations updates the passed annotations for proper VDDK import
UNCOV
1765
func UpdateVDDKAnnotations(annotations map[string]string, vddk *cdiv1.DataVolumeSourceVDDK) {
×
UNCOV
1766
        annotations[AnnEndpoint] = vddk.URL
×
1767
        annotations[AnnSource] = SourceVDDK
×
1768
        annotations[AnnSecret] = vddk.SecretRef
×
1769
        annotations[AnnBackingFile] = vddk.BackingFile
×
1770
        annotations[AnnUUID] = vddk.UUID
×
1771
        annotations[AnnThumbprint] = vddk.Thumbprint
×
1772
        if vddk.InitImageURL != "" {
×
1773
                annotations[AnnVddkInitImageURL] = vddk.InitImageURL
×
1774
        }
×
1775
        if vddk.ExtraArgs != "" {
×
1776
                annotations[AnnVddkExtraArgs] = vddk.ExtraArgs
×
1777
        }
×
1778
}
1779

1780
// UpdateImageIOAnnotations updates the passed annotations for proper imageIO import
UNCOV
1781
func UpdateImageIOAnnotations(annotations map[string]string, imageio *cdiv1.DataVolumeSourceImageIO) {
×
UNCOV
1782
        annotations[AnnEndpoint] = imageio.URL
×
1783
        annotations[AnnSource] = SourceImageio
×
1784
        annotations[AnnSecret] = imageio.SecretRef
×
1785
        annotations[AnnCertConfigMap] = imageio.CertConfigMap
×
1786
        annotations[AnnDiskID] = imageio.DiskID
×
1787
        if imageio.InsecureSkipVerify != nil && *imageio.InsecureSkipVerify {
×
1788
                annotations[AnnInsecureSkipVerify] = "true"
×
1789
        }
×
1790
}
1791

1792
// IsPVBoundToPVC checks if a PV is bound to a specific PVC
1793
func IsPVBoundToPVC(pv *corev1.PersistentVolume, pvc *corev1.PersistentVolumeClaim) bool {
1✔
1794
        claimRef := pv.Spec.ClaimRef
1✔
1795
        return claimRef != nil && claimRef.Name == pvc.Name && claimRef.Namespace == pvc.Namespace && claimRef.UID == pvc.UID
1✔
1796
}
1✔
1797

1798
// Rebind binds the PV of source to target
1799
func Rebind(ctx context.Context, c client.Client, source, target *corev1.PersistentVolumeClaim) error {
1✔
1800
        pv := &corev1.PersistentVolume{
1✔
1801
                ObjectMeta: metav1.ObjectMeta{
1✔
1802
                        Name: source.Spec.VolumeName,
1✔
1803
                },
1✔
1804
        }
1✔
1805

1✔
1806
        if err := c.Get(ctx, client.ObjectKeyFromObject(pv), pv); err != nil {
2✔
1807
                return err
1✔
1808
        }
1✔
1809

1810
        // Examine the claimref for the PV and see if it's still bound to PVC'
1811
        if pv.Spec.ClaimRef == nil {
1✔
UNCOV
1812
                return fmt.Errorf("PV %s claimRef is nil", pv.Name)
×
UNCOV
1813
        }
×
1814

1815
        if !IsPVBoundToPVC(pv, source) {
2✔
1816
                // Something is not right if the PV is neither bound to PVC' nor target PVC
1✔
1817
                if !IsPVBoundToPVC(pv, target) {
2✔
1818
                        klog.Errorf("PV bound to unexpected PVC: Could not rebind to target PVC '%s'", target.Name)
1✔
1819
                        return fmt.Errorf("PV %s bound to unexpected claim %s", pv.Name, pv.Spec.ClaimRef.Name)
1✔
1820
                }
1✔
1821
                // our work is done
1822
                return nil
1✔
1823
        }
1824

1825
        // Rebind PVC to target PVC
1826
        pv.Spec.ClaimRef = &corev1.ObjectReference{
1✔
1827
                Namespace:       target.Namespace,
1✔
1828
                Name:            target.Name,
1✔
1829
                UID:             target.UID,
1✔
1830
                ResourceVersion: target.ResourceVersion,
1✔
1831
        }
1✔
1832
        klog.V(3).Info("Rebinding PV to target PVC", "PVC", target.Name)
1✔
1833
        if err := c.Update(context.TODO(), pv); err != nil {
1✔
UNCOV
1834
                return err
×
UNCOV
1835
        }
×
1836

1837
        return nil
1✔
1838
}
1839

1840
// BulkDeleteResources deletes a bunch of resources
UNCOV
1841
func BulkDeleteResources(ctx context.Context, c client.Client, obj client.ObjectList, lo client.ListOption) error {
×
UNCOV
1842
        if err := c.List(ctx, obj, lo); err != nil {
×
1843
                if meta.IsNoMatchError(err) {
×
1844
                        return nil
×
1845
                }
×
1846
                return err
×
1847
        }
1848

UNCOV
1849
        sv := reflect.ValueOf(obj).Elem()
×
UNCOV
1850
        iv := sv.FieldByName("Items")
×
1851

×
1852
        for i := 0; i < iv.Len(); i++ {
×
1853
                obj := iv.Index(i).Addr().Interface().(client.Object)
×
1854
                if obj.GetDeletionTimestamp().IsZero() {
×
1855
                        klog.V(3).Infof("Deleting type %+v %+v", reflect.TypeOf(obj), obj)
×
1856
                        if err := c.Delete(ctx, obj); err != nil {
×
1857
                                return err
×
1858
                        }
×
1859
                }
1860
        }
1861

UNCOV
1862
        return nil
×
1863
}
1864

1865
// ValidateSnapshotCloneSize does proper size validation when doing a clone from snapshot operation
UNCOV
1866
func ValidateSnapshotCloneSize(snapshot *snapshotv1.VolumeSnapshot, pvcSpec *corev1.PersistentVolumeClaimSpec, targetSC *storagev1.StorageClass, log logr.Logger) (bool, error) {
×
UNCOV
1867
        restoreSize := snapshot.Status.RestoreSize
×
1868
        if restoreSize == nil {
×
1869
                return false, fmt.Errorf("snapshot has no RestoreSize")
×
1870
        }
×
1871
        targetRequest, hasTargetRequest := pvcSpec.Resources.Requests[corev1.ResourceStorage]
×
1872
        allowExpansion := targetSC.AllowVolumeExpansion != nil && *targetSC.AllowVolumeExpansion
×
1873
        if hasTargetRequest {
×
1874
                // otherwise will just use restoreSize
×
1875
                if restoreSize.Cmp(targetRequest) < 0 && !allowExpansion {
×
1876
                        log.V(3).Info("Can't expand restored PVC because SC does not allow expansion, need to fall back to host assisted")
×
1877
                        return false, nil
×
1878
                }
×
1879
        }
1880
        return true, nil
×
1881
}
1882

1883
// ValidateSnapshotCloneProvisioners validates the target PVC storage class against the snapshot class provisioner
UNCOV
1884
func ValidateSnapshotCloneProvisioners(vsc *snapshotv1.VolumeSnapshotContent, storageClass *storagev1.StorageClass) (bool, error) {
×
UNCOV
1885
        // Do snapshot and storage class validation
×
1886
        if storageClass == nil {
×
1887
                return false, fmt.Errorf("target storage class not found")
×
1888
        }
×
1889
        if storageClass.Provisioner != vsc.Spec.Driver {
×
1890
                return false, nil
×
1891
        }
×
1892
        // TODO: get sourceVolumeMode from volumesnapshotcontent and validate against target spec
1893
        // currently don't have CRDs in CI with sourceVolumeMode which is pretty new
1894
        // converting volume mode is possible but has security implications
UNCOV
1895
        return true, nil
×
1896
}
1897

1898
// GetSnapshotClassForSmartClone looks up the snapshot class based on the storage class
UNCOV
1899
func GetSnapshotClassForSmartClone(pvc *corev1.PersistentVolumeClaim, targetPvcStorageClassName, snapshotClassName *string, log logr.Logger, client client.Client, recorder record.EventRecorder) (string, error) {
×
UNCOV
1900
        logger := log.WithName("GetSnapshotClassForSmartClone").V(3)
×
1901
        // Check if relevant CRDs are available
×
1902
        if !isCsiCrdsDeployed(client, log) {
×
1903
                logger.Info("Missing CSI snapshotter CRDs, falling back to host assisted clone")
×
1904
                return "", nil
×
1905
        }
×
1906

1907
        targetStorageClass, err := GetStorageClassByNameWithK8sFallback(context.TODO(), client, targetPvcStorageClassName)
×
UNCOV
1908
        if err != nil {
×
1909
                return "", err
×
1910
        }
×
1911
        if targetStorageClass == nil {
×
1912
                logger.Info("Target PVC's Storage Class not found")
×
1913
                return "", nil
×
1914
        }
×
1915

1916
        vscName, err := GetVolumeSnapshotClass(context.TODO(), client, pvc, targetStorageClass.Provisioner, snapshotClassName, logger, recorder)
×
UNCOV
1917
        if err != nil {
×
1918
                return "", err
×
1919
        }
×
1920
        if vscName != nil {
×
1921
                if pvc != nil {
×
1922
                        logger.Info("smart-clone is applicable for datavolume", "datavolume",
×
1923
                                pvc.Name, "snapshot class", *vscName)
×
1924
                }
×
1925
                return *vscName, nil
×
1926
        }
1927

UNCOV
1928
        logger.Info("Could not match snapshotter with storage class, falling back to host assisted clone")
×
UNCOV
1929
        return "", nil
×
1930
}
1931

1932
// GetVolumeSnapshotClass looks up the snapshot class based on the driver and an optional specific name
1933
// In case of multiple candidates, it returns the default-annotated one, or the sorted list first one if no such default
UNCOV
1934
func GetVolumeSnapshotClass(ctx context.Context, c client.Client, pvc *corev1.PersistentVolumeClaim, driver string, snapshotClassName *string, log logr.Logger, recorder record.EventRecorder) (*string, error) {
×
UNCOV
1935
        logger := log.WithName("GetVolumeSnapshotClass").V(3)
×
1936

×
1937
        logEvent := func(message, vscName string) {
×
1938
                logger.Info(message, "name", vscName)
×
1939
                if pvc != nil {
×
1940
                        msg := fmt.Sprintf("%s %s", message, vscName)
×
1941
                        recorder.Event(pvc, corev1.EventTypeNormal, VolumeSnapshotClassSelected, msg)
×
1942
                }
×
1943
        }
1944

UNCOV
1945
        if snapshotClassName != nil {
×
UNCOV
1946
                vsc := &snapshotv1.VolumeSnapshotClass{}
×
1947
                if err := c.Get(context.TODO(), types.NamespacedName{Name: *snapshotClassName}, vsc); err != nil {
×
1948
                        return nil, err
×
1949
                }
×
1950
                if vsc.Driver == driver {
×
1951
                        logEvent(MessageStorageProfileVolumeSnapshotClassSelected, vsc.Name)
×
1952
                        return snapshotClassName, nil
×
1953
                }
×
1954
                return nil, nil
×
1955
        }
1956

UNCOV
1957
        vscList := &snapshotv1.VolumeSnapshotClassList{}
×
UNCOV
1958
        if err := c.List(ctx, vscList); err != nil {
×
1959
                if meta.IsNoMatchError(err) {
×
1960
                        return nil, nil
×
1961
                }
×
1962
                return nil, err
×
1963
        }
1964

UNCOV
1965
        var candidates []string
×
UNCOV
1966
        for _, vsc := range vscList.Items {
×
1967
                if vsc.Driver == driver {
×
1968
                        if vsc.Annotations[AnnDefaultSnapshotClass] == "true" {
×
1969
                                logEvent(MessageDefaultVolumeSnapshotClassSelected, vsc.Name)
×
1970
                                vscName := vsc.Name
×
1971
                                return &vscName, nil
×
1972
                        }
×
1973
                        candidates = append(candidates, vsc.Name)
×
1974
                }
1975
        }
1976

UNCOV
1977
        if len(candidates) > 0 {
×
UNCOV
1978
                sort.Strings(candidates)
×
1979
                logEvent(MessageFirstVolumeSnapshotClassSelected, candidates[0])
×
1980
                return &candidates[0], nil
×
1981
        }
×
1982

1983
        return nil, nil
×
1984
}
1985

1986
// isCsiCrdsDeployed checks whether the CSI snapshotter CRD are deployed
UNCOV
1987
func isCsiCrdsDeployed(c client.Client, log logr.Logger) bool {
×
UNCOV
1988
        version := "v1"
×
1989
        vsClass := "volumesnapshotclasses." + snapshotv1.GroupName
×
1990
        vsContent := "volumesnapshotcontents." + snapshotv1.GroupName
×
1991
        vs := "volumesnapshots." + snapshotv1.GroupName
×
1992

×
1993
        return isCrdDeployed(c, vsClass, version, log) &&
×
1994
                isCrdDeployed(c, vsContent, version, log) &&
×
1995
                isCrdDeployed(c, vs, version, log)
×
1996
}
×
1997

1998
// isCrdDeployed checks whether a CRD is deployed
UNCOV
1999
func isCrdDeployed(c client.Client, name, version string, log logr.Logger) bool {
×
UNCOV
2000
        crd := &extv1.CustomResourceDefinition{}
×
2001
        err := c.Get(context.TODO(), types.NamespacedName{Name: name}, crd)
×
2002
        if err != nil {
×
2003
                if !k8serrors.IsNotFound(err) {
×
2004
                        log.Info("Error looking up CRD", "crd name", name, "version", version, "error", err)
×
2005
                }
×
2006
                return false
×
2007
        }
2008

UNCOV
2009
        for _, v := range crd.Spec.Versions {
×
UNCOV
2010
                if v.Name == version && v.Served {
×
2011
                        return true
×
2012
                }
×
2013
        }
2014

UNCOV
2015
        return false
×
2016
}
2017

2018
// IsSnapshotReady indicates if a volume snapshot is ready to be used
UNCOV
2019
func IsSnapshotReady(snapshot *snapshotv1.VolumeSnapshot) bool {
×
UNCOV
2020
        return snapshot.Status != nil && snapshot.Status.ReadyToUse != nil && *snapshot.Status.ReadyToUse
×
2021
}
×
2022

2023
// GetResource updates given obj with the data of the object with the same name and namespace
UNCOV
2024
func GetResource(ctx context.Context, c client.Client, namespace, name string, obj client.Object) (bool, error) {
×
UNCOV
2025
        obj.SetNamespace(namespace)
×
2026
        obj.SetName(name)
×
2027

×
2028
        err := c.Get(ctx, client.ObjectKeyFromObject(obj), obj)
×
2029
        if err != nil {
×
2030
                if k8serrors.IsNotFound(err) {
×
2031
                        return false, nil
×
2032
                }
×
2033

2034
                return false, err
×
2035
        }
2036

UNCOV
2037
        return true, nil
×
2038
}
2039

2040
// PatchArgs are the args for Patch
2041
type PatchArgs struct {
2042
        Client client.Client
2043
        Log    logr.Logger
2044
        Obj    client.Object
2045
        OldObj client.Object
2046
}
2047

2048
// GetAnnotatedEventSource returns resource referenced by AnnEventSource annotations
UNCOV
2049
func GetAnnotatedEventSource(ctx context.Context, c client.Client, obj client.Object) (client.Object, error) {
×
UNCOV
2050
        esk, ok := obj.GetAnnotations()[AnnEventSourceKind]
×
2051
        if !ok {
×
2052
                return obj, nil
×
2053
        }
×
2054
        if esk != "PersistentVolumeClaim" {
×
2055
                return obj, nil
×
2056
        }
×
2057
        es, ok := obj.GetAnnotations()[AnnEventSource]
×
2058
        if !ok {
×
2059
                return obj, nil
×
2060
        }
×
2061
        namespace, name, err := cache.SplitMetaNamespaceKey(es)
×
2062
        if err != nil {
×
2063
                return nil, err
×
2064
        }
×
2065
        pvc := &corev1.PersistentVolumeClaim{
×
2066
                ObjectMeta: metav1.ObjectMeta{
×
2067
                        Namespace: namespace,
×
2068
                        Name:      name,
×
2069
                },
×
2070
        }
×
2071
        if err := c.Get(ctx, client.ObjectKeyFromObject(pvc), pvc); err != nil {
×
2072
                return nil, err
×
2073
        }
×
2074
        return pvc, nil
×
2075
}
2076

2077
// OwnedByDataVolume returns true if the object is owned by a DataVolume
UNCOV
2078
func OwnedByDataVolume(obj metav1.Object) bool {
×
UNCOV
2079
        owner := metav1.GetControllerOf(obj)
×
2080
        return owner != nil && owner.Kind == "DataVolume"
×
2081
}
×
2082

2083
// CopyAllowedAnnotations copies the allowed annotations from the source object
2084
// to the destination object
UNCOV
2085
func CopyAllowedAnnotations(srcObj, dstObj metav1.Object) {
×
UNCOV
2086
        for ann, def := range allowedAnnotations {
×
2087
                val, ok := srcObj.GetAnnotations()[ann]
×
2088
                if !ok && def != "" {
×
2089
                        val = def
×
2090
                }
×
2091
                if val != "" {
×
2092
                        klog.V(1).Info("Applying annotation", "Name", dstObj.GetName(), ann, val)
×
2093
                        AddAnnotation(dstObj, ann, val)
×
2094
                }
×
2095
        }
2096
}
2097

2098
// CopyAllowedLabels copies allowed labels matching the validLabelsMatch regexp from the
2099
// source map to the destination object allowing overwrites
2100
func CopyAllowedLabels(srcLabels map[string]string, dstObj metav1.Object, overwrite bool) {
1✔
2101
        for label, value := range srcLabels {
2✔
2102
                if _, found := dstObj.GetLabels()[label]; (!found || overwrite) && validLabelsMatch.MatchString(label) {
2✔
2103
                        AddLabel(dstObj, label, value)
1✔
2104
                }
1✔
2105
        }
2106
}
2107

2108
// ClaimMayExistBeforeDataVolume returns true if the PVC may exist before the DataVolume
UNCOV
2109
func ClaimMayExistBeforeDataVolume(c client.Client, pvc *corev1.PersistentVolumeClaim, dv *cdiv1.DataVolume) (bool, error) {
×
UNCOV
2110
        if ClaimIsPopulatedForDataVolume(pvc, dv) {
×
2111
                return true, nil
×
2112
        }
×
2113
        return AllowClaimAdoption(c, pvc, dv)
×
2114
}
2115

2116
// ClaimIsPopulatedForDataVolume returns true if the PVC is populated for the given DataVolume
UNCOV
2117
func ClaimIsPopulatedForDataVolume(pvc *corev1.PersistentVolumeClaim, dv *cdiv1.DataVolume) bool {
×
UNCOV
2118
        return pvc != nil && dv != nil && pvc.Annotations[AnnPopulatedFor] == dv.Name
×
2119
}
×
2120

2121
// AllowClaimAdoption returns true if the PVC may be adopted
UNCOV
2122
func AllowClaimAdoption(c client.Client, pvc *corev1.PersistentVolumeClaim, dv *cdiv1.DataVolume) (bool, error) {
×
UNCOV
2123
        if pvc == nil || dv == nil {
×
2124
                return false, nil
×
2125
        }
×
2126
        anno, ok := pvc.Annotations[AnnCreatedForDataVolume]
×
2127
        if ok && anno == string(dv.UID) {
×
2128
                return false, nil
×
2129
        }
×
2130
        anno, ok = dv.Annotations[AnnAllowClaimAdoption]
×
2131
        // if annotation exists, go with that regardless of featuregate
×
2132
        if ok {
×
2133
                val, _ := strconv.ParseBool(anno)
×
2134
                return val, nil
×
2135
        }
×
2136
        return featuregates.NewFeatureGates(c).ClaimAdoptionEnabled()
×
2137
}
2138

2139
// ResolveDataSourceChain resolves a DataSource reference.
2140
// Returns an error if DataSource reference is not found or
2141
// DataSource reference points to another DataSource
UNCOV
2142
func ResolveDataSourceChain(ctx context.Context, client client.Client, dataSource *cdiv1.DataSource) (*cdiv1.DataSource, error) {
×
UNCOV
2143
        if dataSource.Spec.Source.DataSource == nil {
×
2144
                return dataSource, nil
×
2145
        }
×
2146

2147
        ref := dataSource.Spec.Source.DataSource
×
UNCOV
2148
        refNs := GetNamespace(ref.Namespace, dataSource.Namespace)
×
2149
        if dataSource.Namespace != refNs {
×
2150
                return dataSource, ErrDataSourceCrossNamespace
×
2151
        }
×
2152
        if ref.Name == dataSource.Name && refNs == dataSource.Namespace {
×
2153
                return nil, ErrDataSourceSelfReference
×
2154
        }
×
2155

2156
        resolved := &cdiv1.DataSource{}
×
UNCOV
2157
        if err := client.Get(ctx, types.NamespacedName{Name: ref.Name, Namespace: refNs}, resolved); err != nil {
×
2158
                return nil, err
×
2159
        }
×
2160

2161
        if resolved.Spec.Source.DataSource != nil {
×
UNCOV
2162
                return nil, ErrDataSourceMaxDepthReached
×
2163
        }
×
2164

2165
        return resolved, nil
×
2166
}
2167

2168
func sortEvents(events *corev1.EventList, usingPopulator bool, pvcPrimeName string) {
1✔
2169
        // Sort event lists by containing primeName substring and most recent timestamp
1✔
2170
        sort.Slice(events.Items, func(i, j int) bool {
2✔
2171
                if usingPopulator {
2✔
2172
                        firstContainsPrime := strings.Contains(events.Items[i].Message, pvcPrimeName)
1✔
2173
                        secondContainsPrime := strings.Contains(events.Items[j].Message, pvcPrimeName)
1✔
2174

1✔
2175
                        if firstContainsPrime && !secondContainsPrime {
2✔
2176
                                return true
1✔
2177
                        }
1✔
2178
                        if !firstContainsPrime && secondContainsPrime {
2✔
2179
                                return false
1✔
2180
                        }
1✔
2181
                }
2182

2183
                // if the timestamps are the same, prioritze longer messages to make sure our sorting is deterministic
2184
                if events.Items[i].LastTimestamp.Time.Equal(events.Items[j].LastTimestamp.Time) {
1✔
UNCOV
2185
                        return len(events.Items[i].Message) > len(events.Items[j].Message)
×
UNCOV
2186
                }
×
2187

2188
                // if both contains primeName substring or neither, just sort on timestamp
2189
                return events.Items[i].LastTimestamp.Time.After(events.Items[j].LastTimestamp.Time)
1✔
2190
        })
2191
}
2192

2193
// UpdatePVCBoundContionFromEvents updates the bound condition annotations on the PVC based on recent events
2194
// This function can be used by both controller and populator packages to update PVC bound condition information
UNCOV
2195
func UpdatePVCBoundContionFromEvents(pvc *corev1.PersistentVolumeClaim, c client.Client, log logr.Logger) error {
×
UNCOV
2196
        currentPvcCopy := pvc.DeepCopy()
×
2197

×
2198
        anno := pvc.GetAnnotations()
×
2199
        if anno == nil {
×
2200
                return nil
×
2201
        }
×
2202

2203
        if IsBound(pvc) {
×
UNCOV
2204
                anno := pvc.GetAnnotations()
×
2205
                delete(anno, AnnBoundCondition)
×
2206
                delete(anno, AnnBoundConditionReason)
×
2207
                delete(anno, AnnBoundConditionMessage)
×
2208

×
2209
                if !reflect.DeepEqual(currentPvcCopy, pvc) {
×
2210
                        patch := client.MergeFrom(currentPvcCopy)
×
2211
                        if err := c.Patch(context.TODO(), pvc, patch); err != nil {
×
2212
                                return err
×
2213
                        }
×
2214
                }
2215

UNCOV
2216
                return nil
×
2217
        }
2218

UNCOV
2219
        if pvc.Status.Phase != corev1.ClaimPending {
×
UNCOV
2220
                return nil
×
2221
        }
×
2222

2223
        // set bound condition by getting the latest event
UNCOV
2224
        events := &corev1.EventList{}
×
UNCOV
2225

×
2226
        err := c.List(context.TODO(), events,
×
2227
                client.InNamespace(pvc.GetNamespace()),
×
2228
                client.MatchingFields{"involvedObject.name": pvc.GetName(),
×
2229
                        "involvedObject.uid": string(pvc.GetUID())},
×
2230
        )
×
2231

×
2232
        if err != nil {
×
2233
                // Log the error but don't fail the reconciliation
×
2234
                log.Error(err, "Unable to list events for PVC bound condition update", "pvc", pvc.Name)
×
2235
                return nil
×
2236
        }
×
2237

2238
        if len(events.Items) == 0 {
×
UNCOV
2239
                return nil
×
2240
        }
×
2241

2242
        pvcPrime, usingPopulator := anno[AnnPVCPrimeName]
×
UNCOV
2243

×
2244
        // Sort event lists by containing primeName substring and most recent timestamp
×
2245
        sortEvents(events, usingPopulator, pvcPrime)
×
2246

×
2247
        boundMessage := ""
×
2248
        // check if prime name annotation exists
×
2249
        if usingPopulator {
×
2250
                // if we are using populators get the latest event from prime pvc
×
2251
                pvcPrime = fmt.Sprintf("[%s] : ", pvcPrime)
×
2252

×
2253
                // if the first event does not contain a prime message, none will so return
×
2254
                primeIdx := strings.Index(events.Items[0].Message, pvcPrime)
×
2255
                if primeIdx == -1 {
×
2256
                        log.V(1).Info("No bound message found, skipping bound condition update", "pvc", pvc.Name)
×
2257
                        return nil
×
2258
                }
×
2259
                boundMessage = events.Items[0].Message[primeIdx+len(pvcPrime):]
×
2260
        } else {
×
2261
                // if not using populators just get the latest event
×
2262
                boundMessage = events.Items[0].Message
×
2263
        }
×
2264

2265
        // since we checked status of phase above, we know this is pending
UNCOV
2266
        anno[AnnBoundCondition] = "false"
×
UNCOV
2267
        anno[AnnBoundConditionReason] = "Pending"
×
2268
        anno[AnnBoundConditionMessage] = boundMessage
×
2269

×
2270
        patch := client.MergeFrom(currentPvcCopy)
×
2271
        if err := c.Patch(context.TODO(), pvc, patch); err != nil {
×
2272
                return err
×
2273
        }
×
2274

2275
        return nil
×
2276
}
2277

2278
// CopyEvents gets srcPvc events and re-emits them on the target PVC with the src name prefix
UNCOV
2279
func CopyEvents(srcPVC, targetPVC client.Object, c client.Client, recorder record.EventRecorder) {
×
UNCOV
2280
        srcPrefixMsg := fmt.Sprintf("[%s] : ", srcPVC.GetName())
×
2281

×
2282
        newEvents := &corev1.EventList{}
×
2283
        err := c.List(context.TODO(), newEvents,
×
2284
                client.InNamespace(srcPVC.GetNamespace()),
×
2285
                client.MatchingFields{"involvedObject.name": srcPVC.GetName(),
×
2286
                        "involvedObject.uid": string(srcPVC.GetUID())},
×
2287
        )
×
2288

×
2289
        if err != nil {
×
2290
                klog.Error(err, "Could not retrieve srcPVC list of Events")
×
2291
        }
×
2292

2293
        currEvents := &corev1.EventList{}
×
UNCOV
2294
        err = c.List(context.TODO(), currEvents,
×
2295
                client.InNamespace(targetPVC.GetNamespace()),
×
2296
                client.MatchingFields{"involvedObject.name": targetPVC.GetName(),
×
2297
                        "involvedObject.uid": string(targetPVC.GetUID())},
×
2298
        )
×
2299

×
2300
        if err != nil {
×
2301
                klog.Error(err, "Could not retrieve targetPVC list of Events")
×
2302
        }
×
2303

2304
        // use this to hash each message for quick lookup, value is unused
UNCOV
2305
        eventMap := map[string]struct{}{}
×
UNCOV
2306

×
2307
        for _, event := range currEvents.Items {
×
2308
                eventMap[event.Message] = struct{}{}
×
2309
        }
×
2310

2311
        for _, newEvent := range newEvents.Items {
×
UNCOV
2312
                msg := newEvent.Message
×
2313

×
2314
                // check if target PVC already has this equivalent event
×
2315
                if _, exists := eventMap[msg]; exists {
×
2316
                        continue
×
2317
                }
2318

UNCOV
2319
                formattedMsg := srcPrefixMsg + msg
×
UNCOV
2320
                // check if we already emitted this event with the src prefix
×
2321
                if _, exists := eventMap[formattedMsg]; exists {
×
2322
                        continue
×
2323
                }
2324
                recorder.Event(targetPVC, newEvent.Type, newEvent.Reason, formattedMsg)
×
2325
        }
2326
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc