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

kubevirt / containerized-data-importer / #5788

22 Jan 2026 02:39AM UTC coverage: 49.624% (+0.2%) from 49.439%
#5788

Pull #4010

travis-ci

halfcrazy
fix review round 2

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

154 of 212 new or added lines in 14 files covered. (72.64%)

522 existing lines in 3 files now uncovered.

14779 of 29782 relevant lines covered (49.62%)

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
        // AnnImportFatalError is a PVC annotation that indicates a fatal import error that should not be retried
91
        AnnImportFatalError = AnnAPIGroup + "/storage.import.fatalError"
92
        // AnnPopulatedFor is a PVC annotation telling the datavolume controller that the PVC is already populated
93
        AnnPopulatedFor = AnnAPIGroup + "/storage.populatedFor"
94
        // AnnPrePopulated is a PVC annotation telling the datavolume controller that the PVC is already populated
95
        AnnPrePopulated = AnnAPIGroup + "/storage.prePopulated"
96
        // AnnPriorityClassName is PVC annotation to indicate the priority class name for importer, cloner and uploader pod
97
        AnnPriorityClassName = AnnAPIGroup + "/storage.pod.priorityclassname"
98
        // AnnExternalPopulation annotation marks a PVC as "externally populated", allowing the import-controller to skip it
99
        AnnExternalPopulation = AnnAPIGroup + "/externalPopulation"
100

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

277
        // CloneComplete message
278
        CloneComplete = "Clone Complete"
279

280
        cloneTokenLeeway = 10 * time.Second
281

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

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

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

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

322
        // ClaimLost reason const
323
        ClaimLost = "ClaimLost"
324
        // NotFound reason const
325
        NotFound = "NotFound"
326

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

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

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

343
        // ProgressDone this means we are DONE
344
        ProgressDone = "100.0%"
345

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

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

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

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

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

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

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

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

388
        apiServerKeyOnce sync.Once
389
        apiServerKey     *rsa.PrivateKey
390

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

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

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

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

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

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

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

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

×
453
        tok, v := mtv.getTokenAndValidator(pvc)
×
454

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

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

469
        return validateTokenData(tokenData, vcs.Namespace, srcName, pvc.Namespace, pvc.Name, string(pvc.UID), tokenResourceName)
×
UNCOV
470
}
×
UNCOV
471

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

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

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

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

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

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

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

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

×
525
        return nil
×
526
}
UNCOV
527

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

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

×
546
        return storageClass, nil
547
}
UNCOV
548

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

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

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

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

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

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

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

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

1✔
610
        return &defaultClasses[0]
1✔
611
}
612

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

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

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

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

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

×
649
        klog.V(3).Info("target storage class for overhead", targetStorageClass.GetName())
×
650

651
        perStorageConfig := cdiConfig.Status.FilesystemOverhead.StorageClass
×
652

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

×
658
        return cdiConfig.Status.FilesystemOverhead.Global, nil
×
659
}
UNCOV
660

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

×
669
        return cdiconfig.Status.DefaultPodResourceRequirements, nil
×
670
}
UNCOV
671

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

×
680
        return cdiconfig.Status.ImagePullSecrets, nil
×
681
}
UNCOV
682

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

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

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

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

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

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

782
        return pods, nil
783
}
UNCOV
784

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

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

×
796
        return &cr.Spec.Workloads, nil
×
797
}
UNCOV
798

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

×
UNCOV
806
        if len(crList.Items) == 0 {
×
807
                return nil, nil
808
        }
2✔
809

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

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

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

1✔
825
        return &activeResources[0], nil
1✔
826
}
827

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

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

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

×
850
        return cdiconfig.Status.Preallocation
×
851
}
UNCOV
852

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

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

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

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

×
876
        obj.SetFinalizers(append(obj.GetFinalizers(), name))
×
877
}
UNCOV
878

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

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

×
892
        obj.SetFinalizers(finalizers)
893
}
UNCOV
894

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

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

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

×
916
        tokenResourceName := getTokenResourceNamePvc(source)
×
917
        srcName := getSourceNamePvc(source)
918

×
919
        return validateTokenData(tokenData, source.Namespace, srcName, target.Namespace, target.Name, string(target.UID), tokenResourceName)
×
UNCOV
920
}
×
UNCOV
921

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

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

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

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

×
944
        return validateTokenData(tokenData, sourceNamespace, sourceName, dv.Namespace, dv.Name, "", tokenResourceName)
×
945
}
UNCOV
946

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

×
954
        return ""
×
955
}
UNCOV
956

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

×
962
        return "persistentvolumeclaims"
×
963
}
UNCOV
964

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

×
972
        return sourcePvc.Name
973
}
UNCOV
974

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

×
987
        return nil
×
988
}
UNCOV
989

×
990
// IsSnapshotValidForClone returns an error if the passed snapshot is not valid for cloning
991
func IsSnapshotValidForClone(sourceSnapshot *snapshotv1.VolumeSnapshot) error {
992
        if sourceSnapshot.Status == nil {
993
                return fmt.Errorf("no status on source snapshot yet")
×
994
        }
×
995
        if !IsSnapshotReady(sourceSnapshot) {
×
996
                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)
×
997
        }
×
998
        if sourceSnapshot.Status.Error != nil {
×
999
                errMessage := "no details"
×
1000
                if msg := sourceSnapshot.Status.Error.Message; msg != nil {
×
1001
                        errMessage = *msg
×
1002
                }
×
1003
                return fmt.Errorf("snapshot in error state with msg: %s", errMessage)
×
UNCOV
1004
        }
×
1005
        if sourceSnapshot.Spec.VolumeSnapshotClassName == nil ||
×
1006
                *sourceSnapshot.Spec.VolumeSnapshotClassName == "" {
1007
                return fmt.Errorf("snapshot %s/%s does not have volume snap class populated, can't clone", sourceSnapshot.Name, sourceSnapshot.Namespace)
×
1008
        }
×
1009
        return nil
×
UNCOV
1010
}
×
UNCOV
1011

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

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

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

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

×
1042
        recorder.Event(pvc, corev1.EventTypeWarning, reason, msg)
×
1043

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

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

×
1059
        return err
×
1060
}
UNCOV
1061

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

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

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

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

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

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

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

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

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

×
1150
        return requestedSize, nil
1151
}
UNCOV
1152

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

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

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

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

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

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

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

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

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

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

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

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

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

×
1297
        blockOwnerDeletion := true
×
1298
        isController := true
×
1299

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

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

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

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

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

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

×
1418
        return pod
×
1419
}
UNCOV
1420

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

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

1✔
1441
        return fake.NewClientBuilder().WithScheme(s).WithRuntimeObjects(objs...).Build()
1✔
1442
}
1✔
1443

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

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

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

×
1470
        return GetContentType(cdiv1.DataVolumeContentType(contentType))
×
1471
}
UNCOV
1472

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

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

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

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

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

×
1521
        return "", "", ""
×
1522
}
UNCOV
1523

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

×
1534
        return pvcHonorWaitForFirstConsumer && globalHonorWaitForFirstConsumer, nil
×
1535
}
UNCOV
1536

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

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

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

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

×
1569
        return returnSize, nil
×
1570
}
UNCOV
1571

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

×
UNCOV
1839
        return nil
×
1840
}
1841

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

×
1851
        sv := reflect.ValueOf(obj).Elem()
1852
        iv := sv.FieldByName("Items")
1853

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

1864
        return nil
1865
}
UNCOV
1866

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

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

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

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

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

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

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

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

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

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

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

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

×
1985
        return nil, nil
×
1986
}
UNCOV
1987

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

×
1995
        return isCrdDeployed(c, vsClass, version, log) &&
×
1996
                isCrdDeployed(c, vsContent, version, log) &&
×
1997
                isCrdDeployed(c, vs, version, log)
×
1998
}
×
UNCOV
1999

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

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

×
2017
        return false
2018
}
UNCOV
2019

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

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

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

×
2036
                return false, err
×
2037
        }
UNCOV
2038

×
2039
        return true, nil
2040
}
UNCOV
2041

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

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

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

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

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

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

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

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

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

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

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

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

×
2167
        return resolved, nil
×
2168
}
UNCOV
2169

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

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

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

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

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

×
2200
        anno := pvc.GetAnnotations()
×
2201
        if anno == nil {
×
2202
                return nil
×
2203
        }
×
UNCOV
2204

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

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

×
2218
                return nil
2219
        }
UNCOV
2220

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

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

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

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

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

×
2244
        pvcPrime, usingPopulator := anno[AnnPVCPrimeName]
×
2245

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

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

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

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

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

×
2277
        return nil
×
2278
}
UNCOV
2279

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

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

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

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

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

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

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

×
2313
        for _, newEvent := range newEvents.Items {
×
2314
                msg := newEvent.Message
2315

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

×
2321
                formattedMsg := srcPrefixMsg + msg
2322
                // check if we already emitted this event with the src prefix
2323
                if _, exists := eventMap[formattedMsg]; exists {
×
2324
                        continue
×
UNCOV
2325
                }
×
2326
                recorder.Event(targetPVC, newEvent.Type, newEvent.Reason, formattedMsg)
×
2327
        }
UNCOV
2328
}
×
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