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

kubevirt / containerized-data-importer / #5518

05 Aug 2025 01:47PM UTC coverage: 59.268% (-0.03%) from 59.294%
#5518

Pull #3852

travis-ci

arnongilboa
Support storageProfile minimumSupportedPVCSize in clone

When the target DataVolume storage requests a size smaller than the
source PVC. For target without size it already worked correctly.

Signed-off-by: Arnon Gilboa <agilboa@redhat.com>
Pull Request #3852: Support storageProfile minimumSupportedPVCSize in clone

15 of 47 new or added lines in 4 files covered. (31.91%)

352 existing lines in 4 files now uncovered.

17173 of 28975 relevant lines covered (59.27%)

0.65 hits per line

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

13.96
/pkg/controller/common/util.go
1
/*
2
Copyright 2022 The CDI Authors.
3

4
Licensed under the Apache License, Version 2.0 (the "License");
5
you may not use this file except in compliance with the License.
6
You may obtain a copy of the License at
7

8
        http://www.apache.org/licenses/LICENSE-2.0
9

10
Unless required by applicable law or agreed to in writing, software
11
distributed under the License is distributed on an "AS IS" BASIS,
12
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
See the License for the specific language governing permissions and
14
limitations under the License.
15
*/
16

17
package common
18

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

208
        // AnnUploadRequest marks that a PVC should be made available for upload
209
        AnnUploadRequest = AnnAPIGroup + "/storage.upload.target"
210

211
        // AnnCheckStaticVolume checks if a statically allocated PV exists before creating the target PVC.
212
        // If so, PVC is still created but population is skipped
213
        AnnCheckStaticVolume = AnnAPIGroup + "/storage.checkStaticVolume"
214

215
        // AnnPersistentVolumeList is an annotation storing a list of PV names
216
        AnnPersistentVolumeList = AnnAPIGroup + "/storage.persistentVolumeList"
217

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

224
        // AnnMinimumSupportedPVCSize annotation on a StorageProfile specifies its minimum supported PVC size
225
        AnnMinimumSupportedPVCSize = AnnAPIGroup + "/minimumSupportedPvcSize"
226

227
        // AnnDefaultStorageClass is the annotation indicating that a storage class is the default one
228
        AnnDefaultStorageClass = "storageclass.kubernetes.io/is-default-class"
229
        // AnnDefaultVirtStorageClass is the annotation indicating that a storage class is the default one for virtualization purposes
230
        AnnDefaultVirtStorageClass = "storageclass.kubevirt.io/is-default-virt-class"
231
        // AnnDefaultSnapshotClass is the annotation indicating that a snapshot class is the default one
232
        AnnDefaultSnapshotClass = "snapshot.storage.kubernetes.io/is-default-class"
233

234
        // AnnSourceVolumeMode is the volume mode of the source PVC specified as an annotation on snapshots
235
        AnnSourceVolumeMode = AnnAPIGroup + "/storage.import.sourceVolumeMode"
236

237
        // AnnOpenShiftImageLookup is the annotation for OpenShift image stream lookup
238
        AnnOpenShiftImageLookup = "alpha.image.policy.openshift.io/resolve-names"
239

240
        // AnnCloneRequest sets our expected annotation for a CloneRequest
241
        AnnCloneRequest = "k8s.io/CloneRequest"
242
        // AnnCloneOf is used to indicate that cloning was complete
243
        AnnCloneOf = "k8s.io/CloneOf"
244

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

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

261
        // AnnSelectedNode annotation is added to a PVC that has been triggered by scheduler to
262
        // be dynamically provisioned. Its value is the name of the selected node.
263
        AnnSelectedNode = "volume.kubernetes.io/selected-node"
264

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

268
        // CloneSourceInUse is reason for event created when clone source pvc is in use
269
        CloneSourceInUse = "CloneSourceInUse"
270

271
        // CloneComplete message
272
        CloneComplete = "Clone Complete"
273

274
        cloneTokenLeeway = 10 * time.Second
275

276
        // Default value for preallocation option if not defined in DV or CDIConfig
277
        defaultPreallocation = false
278

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

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

307
        // VolumeSnapshotClassSelected reports that a VolumeSnapshotClass was selected
308
        VolumeSnapshotClassSelected = "VolumeSnapshotClassSelected"
309
        // MessageStorageProfileVolumeSnapshotClassSelected reports that a VolumeSnapshotClass was selected according to StorageProfile
310
        MessageStorageProfileVolumeSnapshotClassSelected = "VolumeSnapshotClass selected according to StorageProfile"
311
        // MessageDefaultVolumeSnapshotClassSelected reports that the default VolumeSnapshotClass was selected
312
        MessageDefaultVolumeSnapshotClassSelected = "Default VolumeSnapshotClass selected"
313
        // MessageFirstVolumeSnapshotClassSelected reports that the first VolumeSnapshotClass was selected
314
        MessageFirstVolumeSnapshotClassSelected = "First VolumeSnapshotClass selected"
315

316
        // ClaimLost reason const
317
        ClaimLost = "ClaimLost"
318
        // NotFound reason const
319
        NotFound = "NotFound"
320

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

330
        // LabelDynamicCredentialSupport specifies if the OS supports updating credentials at runtime.
331
        //nolint:gosec // These are not credentials
332
        LabelDynamicCredentialSupport = "kubevirt.io/dynamic-credentials-support"
333

334
        // LabelExcludeFromVeleroBackup provides a const to indicate whether an object should be excluded from velero backup
335
        LabelExcludeFromVeleroBackup = "velero.io/exclude-from-backup"
336

337
        // ProgressDone this means we are DONE
338
        ProgressDone = "100.0%"
339

340
        // AnnEventSourceKind is the source kind that should be related to events
341
        AnnEventSourceKind = AnnAPIGroup + "/events.source.kind"
342
        // AnnEventSource is the source that should be related to events (namespace/name)
343
        AnnEventSource = AnnAPIGroup + "/events.source"
344

345
        // AnnAllowClaimAdoption is the annotation that allows a claim to be adopted by a DataVolume
346
        AnnAllowClaimAdoption = AnnAPIGroup + "/allowClaimAdoption"
347

348
        // AnnCdiCustomizeComponentHash annotation is a hash of all customizations that live under spec.CustomizeComponents
349
        AnnCdiCustomizeComponentHash = AnnAPIGroup + "/customizer-identifier"
350

351
        // AnnCreatedForDataVolume stores the UID of the datavolume that the PVC was created for
352
        AnnCreatedForDataVolume = AnnAPIGroup + "/createdForDataVolume"
353

354
        // AnnPVCPrimeName annotation is the name of the PVC' that is used to populate the PV which is then rebound to the target PVC
355
        AnnPVCPrimeName = AnnAPIGroup + "/storage.populator.pvcPrime"
356
)
357

358
// Size-detection pod error codes
359
const (
360
        NoErr int = iota
361
        ErrBadArguments
362
        ErrInvalidFile
363
        ErrInvalidPath
364
        ErrBadTermFile
365
        ErrUnknown
366
)
367

368
var (
369
        // BlockMode is raw block device mode
370
        BlockMode = corev1.PersistentVolumeBlock
371
        // FilesystemMode is filesystem device mode
372
        FilesystemMode = corev1.PersistentVolumeFilesystem
373

374
        // DefaultInstanceTypeLabels is a list of currently supported default instance type labels
375
        DefaultInstanceTypeLabels = []string{
376
                LabelDefaultInstancetype,
377
                LabelDefaultInstancetypeKind,
378
                LabelDefaultPreference,
379
                LabelDefaultPreferenceKind,
380
        }
381

382
        apiServerKeyOnce sync.Once
383
        apiServerKey     *rsa.PrivateKey
384

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

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

397
        ErrDataSourceMaxDepthReached = errors.New("DataSource reference chain exceeds maximum depth of 1")
398
        ErrDataSourceSelfReference   = errors.New("DataSource cannot self-reference")
399
        ErrDataSourceCrossNamespace  = errors.New("DataSource cannot reference a DataSource in another namespace")
400
)
401

402
// FakeValidator is a fake token validator
403
type FakeValidator struct {
404
        Match     string
405
        Operation token.Operation
406
        Name      string
407
        Namespace string
408
        Resource  metav1.GroupVersionResource
409
        Params    map[string]string
410
}
411

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

429
// MultiTokenValidator is a token validator that can validate both short and long tokens
430
type MultiTokenValidator struct {
431
        ShortTokenValidator token.Validator
432
        LongTokenValidator  token.Validator
433
}
434

435
// ValidatePVC validates a PVC
436
func (mtv *MultiTokenValidator) ValidatePVC(source, target *corev1.PersistentVolumeClaim) error {
×
437
        tok, v := mtv.getTokenAndValidator(target)
×
438
        return ValidateCloneTokenPVC(tok, v, source, target)
×
439
}
×
440

441
// ValidatePopulator valades a token for a populator
442
func (mtv *MultiTokenValidator) ValidatePopulator(vcs *cdiv1.VolumeCloneSource, pvc *corev1.PersistentVolumeClaim) error {
×
443
        if vcs.Namespace == pvc.Namespace {
×
444
                return nil
×
445
        }
×
446

447
        tok, v := mtv.getTokenAndValidator(pvc)
×
448

×
449
        tokenData, err := v.Validate(tok)
×
450
        if err != nil {
×
451
                return errors.Wrap(err, "error verifying token")
×
452
        }
×
453

454
        var tokenResourceName string
×
455
        switch vcs.Spec.Source.Kind {
×
456
        case "PersistentVolumeClaim":
×
457
                tokenResourceName = "persistentvolumeclaims"
×
458
        case "VolumeSnapshot":
×
459
                tokenResourceName = "volumesnapshots"
×
460
        }
461
        srcName := vcs.Spec.Source.Name
×
462

×
463
        return validateTokenData(tokenData, vcs.Namespace, srcName, pvc.Namespace, pvc.Name, string(pvc.UID), tokenResourceName)
×
464
}
465

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

477
// NewMultiTokenValidator returns a new multi token validator
478
func NewMultiTokenValidator(key *rsa.PublicKey) *MultiTokenValidator {
×
479
        return &MultiTokenValidator{
×
480
                ShortTokenValidator: NewCloneTokenValidator(common.CloneTokenIssuer, key),
×
481
                LongTokenValidator:  NewCloneTokenValidator(common.ExtendedCloneTokenIssuer, key),
×
482
        }
×
483
}
×
484

485
// NewCloneTokenValidator returns a new token validator
486
func NewCloneTokenValidator(issuer string, key *rsa.PublicKey) token.Validator {
×
487
        return token.NewValidator(issuer, key, cloneTokenLeeway)
×
488
}
×
489

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

499
// GetVolumeMode returns the volumeMode from PVC handling default empty value
500
func GetVolumeMode(pvc *corev1.PersistentVolumeClaim) corev1.PersistentVolumeMode {
×
501
        return util.ResolveVolumeMode(pvc.Spec.VolumeMode)
×
502
}
×
503

504
// IsDataVolumeUsingDefaultStorageClass checks if the DataVolume is using the default StorageClass
505
func IsDataVolumeUsingDefaultStorageClass(dv *cdiv1.DataVolume) bool {
×
506
        return GetStorageClassFromDVSpec(dv) == nil
×
507
}
×
508

509
// GetStorageClassFromDVSpec returns the StorageClassName from DataVolume PVC or Storage spec
510
func GetStorageClassFromDVSpec(dv *cdiv1.DataVolume) *string {
×
511
        if dv.Spec.PVC != nil {
×
512
                return dv.Spec.PVC.StorageClassName
×
513
        }
×
514

515
        if dv.Spec.Storage != nil {
×
516
                return dv.Spec.Storage.StorageClassName
×
517
        }
×
518

519
        return nil
×
520
}
521

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

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

540
        return storageClass, nil
×
541
}
542

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

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

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

568
        if GetContentType(contentType) == cdiv1.DataVolumeKubeVirt {
2✔
569
                if virtSc := GetPlatformDefaultStorageClass(storageClasses, AnnDefaultVirtStorageClass); virtSc != nil {
2✔
570
                        return virtSc, nil
1✔
571
                }
1✔
572
        }
573
        return GetPlatformDefaultStorageClass(storageClasses, AnnDefaultStorageClass), nil
1✔
574
}
575

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

1✔
580
        for _, storageClass := range storageClasses.Items {
2✔
581
                if storageClass.Annotations[defaultAnnotationKey] == "true" {
2✔
582
                        defaultClasses = append(defaultClasses, storageClass)
1✔
583
                }
1✔
584
        }
585

586
        if len(defaultClasses) == 0 {
2✔
587
                return nil
1✔
588
        }
1✔
589

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

604
        return &defaultClasses[0]
1✔
605
}
606

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

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

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

633
        if cdiConfig.Status.FilesystemOverhead == nil {
×
634
                klog.Errorf("CDIConfig filesystemOverhead used before config controller ran reconcile. Hopefully this only happens during unit testing.")
×
635
                return "0", nil
×
636
        }
×
637

638
        if targetStorageClass == nil {
×
639
                klog.V(3).Info("Storage class", storageClassName, "not found, continuing with global overhead")
×
640
                return cdiConfig.Status.FilesystemOverhead.Global, nil
×
641
        }
×
642

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

×
645
        perStorageConfig := cdiConfig.Status.FilesystemOverhead.StorageClass
×
646

×
647
        storageClassOverhead, found := perStorageConfig[targetStorageClass.GetName()]
×
648
        if found {
×
649
                return storageClassOverhead, nil
×
650
        }
×
651

652
        return cdiConfig.Status.FilesystemOverhead.Global, nil
×
653
}
654

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

663
        return cdiconfig.Status.DefaultPodResourceRequirements, nil
×
664
}
665

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

674
        return cdiconfig.Status.ImagePullSecrets, nil
×
675
}
676

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

688
        pvcUID := pvc.GetUID()
×
689
        for _, pod := range pods.Items {
×
690
                if ShouldIgnorePod(&pod, pvc) {
×
691
                        continue
×
692
                }
693
                for _, or := range pod.OwnerReferences {
×
694
                        if or.UID == pvcUID {
×
695
                                return &pod, nil
×
696
                        }
×
697
                }
698

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

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

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

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

776
        return pods, nil
×
777
}
778

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

786
        if cr == nil {
×
787
                return nil, fmt.Errorf("no active CDI")
×
788
        }
×
789

790
        return &cr.Spec.Workloads, nil
×
791
}
792

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

800
        if len(crList.Items) == 0 {
2✔
801
                return nil, nil
1✔
802
        }
1✔
803

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

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

815
        if len(activeResources) != 1 {
2✔
816
                return nil, fmt.Errorf("invalid number of active CDI resources: %d", len(activeResources))
1✔
817
        }
1✔
818

819
        return &activeResources[0], nil
1✔
820
}
821

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

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

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

844
        return cdiconfig.Status.Preallocation
×
845
}
846

847
// ImmediateBindingRequested returns if an object has the ImmediateBinding annotation
848
func ImmediateBindingRequested(obj metav1.Object) bool {
×
849
        _, isImmediateBindingRequested := obj.GetAnnotations()[AnnImmediateBinding]
×
850
        return isImmediateBindingRequested
×
851
}
×
852

853
// GetPriorityClass gets PVC priority class
854
func GetPriorityClass(pvc *corev1.PersistentVolumeClaim) string {
×
855
        anno := pvc.GetAnnotations()
×
856
        return anno[AnnPriorityClassName]
×
857
}
×
858

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

864
// AddFinalizer adds a finalizer to a resource
865
func AddFinalizer(obj metav1.Object, name string) {
×
866
        if HasFinalizer(obj, name) {
×
867
                return
×
868
        }
×
869

870
        obj.SetFinalizers(append(obj.GetFinalizers(), name))
×
871
}
872

873
// RemoveFinalizer removes a finalizer from a resource
874
func RemoveFinalizer(obj metav1.Object, name string) {
×
875
        if !HasFinalizer(obj, name) {
×
876
                return
×
877
        }
×
878

879
        var finalizers []string
×
880
        for _, f := range obj.GetFinalizers() {
×
881
                if f != name {
×
882
                        finalizers = append(finalizers, f)
×
883
                }
×
884
        }
885

886
        obj.SetFinalizers(finalizers)
×
887
}
888

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

899
// ValidateCloneTokenPVC validates clone token for source and target PVCs
900
func ValidateCloneTokenPVC(t string, v token.Validator, source, target *corev1.PersistentVolumeClaim) error {
×
901
        if source.Namespace == target.Namespace {
×
902
                return nil
×
903
        }
×
904

905
        tokenData, err := v.Validate(t)
×
906
        if err != nil {
×
907
                return errors.Wrap(err, "error verifying token")
×
908
        }
×
909

910
        tokenResourceName := getTokenResourceNamePvc(source)
×
911
        srcName := getSourceNamePvc(source)
×
912

×
913
        return validateTokenData(tokenData, source.Namespace, srcName, target.Namespace, target.Name, string(target.UID), tokenResourceName)
×
914
}
915

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

923
        tok, ok := dv.Annotations[AnnCloneToken]
×
924
        if !ok {
×
925
                return errors.New("clone token missing")
×
926
        }
×
927

928
        tokenData, err := validator.Validate(tok)
×
929
        if err != nil {
×
930
                return errors.Wrap(err, "error verifying token")
×
931
        }
×
932

933
        tokenResourceName := getTokenResourceNameDataVolume(dv.Spec.Source)
×
934
        if tokenResourceName == "" {
×
935
                return errors.New("token resource name empty, can't verify properly")
×
936
        }
×
937

938
        return validateTokenData(tokenData, sourceNamespace, sourceName, dv.Namespace, dv.Name, "", tokenResourceName)
×
939
}
940

941
func getTokenResourceNameDataVolume(source *cdiv1.DataVolumeSource) string {
×
942
        if source.PVC != nil {
×
943
                return "persistentvolumeclaims"
×
944
        } else if source.Snapshot != nil {
×
945
                return "volumesnapshots"
×
946
        }
×
947

948
        return ""
×
949
}
950

951
func getTokenResourceNamePvc(sourcePvc *corev1.PersistentVolumeClaim) string {
×
952
        if v, ok := sourcePvc.Labels[common.CDIComponentLabel]; ok && v == common.CloneFromSnapshotFallbackPVCCDILabel {
×
953
                return "volumesnapshots"
×
954
        }
×
955

956
        return "persistentvolumeclaims"
×
957
}
958

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

966
        return sourcePvc.Name
×
967
}
968

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

981
        return nil
×
982
}
983

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

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

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

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

×
1031
        // Error handling to fine-tune the event with pertinent info
×
1032
        if ErrQuotaExceeded(err) {
×
1033
                reason = ErrExceededQuota
×
1034
        }
×
1035

1036
        recorder.Event(pvc, corev1.EventTypeWarning, reason, msg)
×
1037

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

1048
        AddAnnotation(pvc, AnnPodPhase, string(corev1.PodFailed))
×
1049
        if err := c.Update(context.TODO(), pvc); err != nil {
×
1050
                return err
×
1051
        }
×
1052

1053
        return err
×
1054
}
1055

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

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

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

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

NEW
1111
        storageProfile := &cdiv1.StorageProfile{}
×
NEW
1112
        if err := client.Get(ctx, types.NamespacedName{Name: sc.Name}, storageProfile); err != nil {
×
NEW
1113
                return storageResources, IgnoreNotFound(err)
×
NEW
1114
        }
×
1115

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

NEW
1121
        if val, exists := storageProfile.Annotations[AnnMinimumSupportedPVCSize]; exists {
×
NEW
1122
                if minSize, err := resource.ParseQuantity(val); err == nil {
×
NEW
1123
                        if requestedSize.Cmp(minSize) == -1 {
×
NEW
1124
                                return corev1.VolumeResourceRequirements{
×
NEW
1125
                                        Requests: corev1.ResourceList{
×
NEW
1126
                                                corev1.ResourceStorage: requestedSize,
×
NEW
1127
                                        },
×
NEW
1128
                                }, nil
×
NEW
1129
                        }
×
NEW
1130
                } else {
×
NEW
1131
                        log.V(1).Info("Invalid minimum PVC size in annotation", "value", val, "error", err)
×
NEW
1132
                }
×
1133
        }
1134

NEW
1135
        return storageResources, nil
×
1136
}
1137

1138
// ValidateRequestedCloneSize validates the clone size requirements on block
1139
func ValidateRequestedCloneSize(sourceResources, targetResources corev1.VolumeResourceRequirements) error {
×
1140
        sourceRequest, hasSource := sourceResources.Requests[corev1.ResourceStorage]
×
1141
        targetRequest, hasTarget := targetResources.Requests[corev1.ResourceStorage]
×
1142
        if !hasSource || !hasTarget {
×
1143
                return errors.New("source/target missing storage resource requests")
×
1144
        }
×
1145

1146
        // Verify that the target PVC size is equal or larger than the source.
1147
        if sourceRequest.Value() > targetRequest.Value() {
×
1148
                return errors.Errorf("target resources requests storage size is smaller than the source %d < %d", targetRequest.Value(), sourceRequest.Value())
×
1149
        }
×
1150
        return nil
×
1151
}
1152

1153
// CreateCloneSourcePodName creates clone source pod name
UNCOV
1154
func CreateCloneSourcePodName(targetPvc *corev1.PersistentVolumeClaim) string {
×
UNCOV
1155
        return string(targetPvc.GetUID()) + common.ClonerSourcePodNameSuffix
×
1156
}
×
1157

1158
// IsPVCComplete returns true if a PVC is in 'Succeeded' phase, false if not
1159
func IsPVCComplete(pvc *corev1.PersistentVolumeClaim) bool {
×
UNCOV
1160
        if pvc != nil {
×
UNCOV
1161
                phase, exists := pvc.ObjectMeta.Annotations[AnnPodPhase]
×
UNCOV
1162
                return exists && (phase == string(corev1.PodSucceeded))
×
1163
        }
×
1164
        return false
×
1165
}
1166

1167
// IsMultiStageImportInProgress returns true when a PVC is being part of an ongoing multi-stage import
1168
func IsMultiStageImportInProgress(pvc *corev1.PersistentVolumeClaim) bool {
×
1169
        if pvc != nil {
×
1170
                multiStageImport := metav1.HasAnnotation(pvc.ObjectMeta, AnnCurrentCheckpoint)
×
1171
                multiStageAlreadyDone := metav1.HasAnnotation(pvc.ObjectMeta, AnnMultiStageImportDone)
×
1172
                return multiStageImport && !multiStageAlreadyDone
×
1173
        }
×
UNCOV
1174
        return false
×
1175
}
1176

1177
// SetRestrictedSecurityContext sets the pod security params to be compatible with restricted PSA
1178
func SetRestrictedSecurityContext(podSpec *corev1.PodSpec) {
×
1179
        hasVolumeMounts := false
×
1180
        for _, containers := range [][]corev1.Container{podSpec.InitContainers, podSpec.Containers} {
×
1181
                for i := range containers {
×
1182
                        container := &containers[i]
×
1183
                        if container.SecurityContext == nil {
×
UNCOV
1184
                                container.SecurityContext = &corev1.SecurityContext{}
×
UNCOV
1185
                        }
×
UNCOV
1186
                        container.SecurityContext.Capabilities = &corev1.Capabilities{
×
1187
                                Drop: []corev1.Capability{
×
1188
                                        "ALL",
×
1189
                                },
×
1190
                        }
×
1191
                        container.SecurityContext.SeccompProfile = &corev1.SeccompProfile{
×
1192
                                Type: corev1.SeccompProfileTypeRuntimeDefault,
×
1193
                        }
×
1194
                        container.SecurityContext.AllowPrivilegeEscalation = ptr.To[bool](false)
×
1195
                        container.SecurityContext.RunAsNonRoot = ptr.To[bool](true)
×
1196
                        container.SecurityContext.RunAsUser = ptr.To[int64](common.QemuSubGid)
×
1197
                        if len(container.VolumeMounts) > 0 {
×
1198
                                hasVolumeMounts = true
×
1199
                        }
×
1200
                }
1201
        }
1202

1203
        if podSpec.SecurityContext == nil {
×
1204
                podSpec.SecurityContext = &corev1.PodSecurityContext{}
×
1205
        }
×
1206
        // Some tools like istio inject containers and thus rely on a pod level seccomp profile being specified
1207
        podSpec.SecurityContext.SeccompProfile = &corev1.SeccompProfile{
×
1208
                Type: corev1.SeccompProfileTypeRuntimeDefault,
×
UNCOV
1209
        }
×
UNCOV
1210
        if hasVolumeMounts {
×
UNCOV
1211
                podSpec.SecurityContext.FSGroup = ptr.To[int64](common.QemuSubGid)
×
1212
        }
×
1213
}
1214

1215
// SetNodeNameIfPopulator sets NodeName in a pod spec when the PVC is being handled by a CDI volume populator
1216
func SetNodeNameIfPopulator(pvc *corev1.PersistentVolumeClaim, podSpec *corev1.PodSpec) {
×
1217
        _, isPopulator := pvc.Annotations[AnnPopulatorKind]
×
1218
        nodeName := pvc.Annotations[AnnSelectedNode]
×
1219
        if isPopulator && nodeName != "" {
×
1220
                podSpec.NodeName = nodeName
×
1221
        }
×
1222
}
1223

1224
// CreatePvc creates PVC
1225
func CreatePvc(name, ns string, annotations, labels map[string]string) *corev1.PersistentVolumeClaim {
1✔
1226
        return CreatePvcInStorageClass(name, ns, nil, annotations, labels, corev1.ClaimBound)
1✔
1227
}
1✔
1228

1229
// CreatePvcInStorageClass creates PVC with storgae class
1230
func CreatePvcInStorageClass(name, ns string, storageClassName *string, annotations, labels map[string]string, phase corev1.PersistentVolumeClaimPhase) *corev1.PersistentVolumeClaim {
1✔
1231
        pvc := &corev1.PersistentVolumeClaim{
1✔
1232
                ObjectMeta: metav1.ObjectMeta{
1✔
1233
                        Name:        name,
1✔
1234
                        Namespace:   ns,
1✔
1235
                        Annotations: annotations,
1✔
1236
                        Labels:      labels,
1✔
1237
                        UID:         types.UID(ns + "-" + name),
1✔
1238
                },
1✔
1239
                Spec: corev1.PersistentVolumeClaimSpec{
1✔
1240
                        AccessModes: []corev1.PersistentVolumeAccessMode{corev1.ReadOnlyMany, corev1.ReadWriteOnce},
1✔
1241
                        Resources: corev1.VolumeResourceRequirements{
1✔
1242
                                Requests: corev1.ResourceList{
1✔
1243
                                        corev1.ResourceStorage: resource.MustParse("1G"),
1✔
1244
                                },
1✔
1245
                        },
1✔
1246
                        StorageClassName: storageClassName,
1✔
1247
                },
1✔
1248
                Status: corev1.PersistentVolumeClaimStatus{
1✔
1249
                        Phase: phase,
1✔
1250
                },
1✔
1251
        }
1✔
1252
        pvc.Status.Capacity = pvc.Spec.Resources.Requests.DeepCopy()
1✔
1253
        if pvc.Status.Phase == corev1.ClaimBound {
2✔
1254
                pvc.Spec.VolumeName = "pv-" + string(pvc.UID)
1✔
1255
        }
1✔
1256
        return pvc
1✔
1257
}
1258

1259
// GetAPIServerKey returns API server RSA key
UNCOV
1260
func GetAPIServerKey() *rsa.PrivateKey {
×
UNCOV
1261
        apiServerKeyOnce.Do(func() {
×
UNCOV
1262
                apiServerKey, _ = rsa.GenerateKey(rand.Reader, 2048)
×
UNCOV
1263
        })
×
UNCOV
1264
        return apiServerKey
×
1265
}
1266

1267
// CreateStorageClass creates storage class CR
1268
func CreateStorageClass(name string, annotations map[string]string) *storagev1.StorageClass {
1✔
1269
        return &storagev1.StorageClass{
1✔
1270
                ObjectMeta: metav1.ObjectMeta{
1✔
1271
                        Name:        name,
1✔
1272
                        Annotations: annotations,
1✔
1273
                },
1✔
1274
        }
1✔
1275
}
1✔
1276

1277
// CreateImporterTestPod creates importer test pod CR
UNCOV
1278
func CreateImporterTestPod(pvc *corev1.PersistentVolumeClaim, dvname string, scratchPvc *corev1.PersistentVolumeClaim) *corev1.Pod {
×
UNCOV
1279
        // importer pod name contains the pvc name
×
UNCOV
1280
        podName := fmt.Sprintf("%s-%s", common.ImporterPodName, pvc.Name)
×
UNCOV
1281

×
UNCOV
1282
        blockOwnerDeletion := true
×
UNCOV
1283
        isController := true
×
UNCOV
1284

×
UNCOV
1285
        volumes := []corev1.Volume{
×
UNCOV
1286
                {
×
1287
                        Name: dvname,
×
1288
                        VolumeSource: corev1.VolumeSource{
×
1289
                                PersistentVolumeClaim: &corev1.PersistentVolumeClaimVolumeSource{
×
1290
                                        ClaimName: pvc.Name,
×
1291
                                        ReadOnly:  false,
×
1292
                                },
×
1293
                        },
×
1294
                },
×
1295
        }
×
1296

×
1297
        if scratchPvc != nil {
×
1298
                volumes = append(volumes, corev1.Volume{
×
1299
                        Name: ScratchVolName,
×
1300
                        VolumeSource: corev1.VolumeSource{
×
1301
                                PersistentVolumeClaim: &corev1.PersistentVolumeClaimVolumeSource{
×
1302
                                        ClaimName: scratchPvc.Name,
×
1303
                                        ReadOnly:  false,
×
1304
                                },
×
1305
                        },
×
1306
                })
×
1307
        }
×
1308

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

×
1357
        ep, _ := GetEndpoint(pvc)
×
1358
        source := GetSource(pvc)
×
1359
        contentType := GetPVCContentType(pvc)
×
1360
        imageSize, _ := GetRequestedImageSize(pvc)
×
1361
        volumeMode := GetVolumeMode(pvc)
×
1362

×
1363
        env := []corev1.EnvVar{
×
1364
                {
×
1365
                        Name:  common.ImporterSource,
×
1366
                        Value: source,
×
1367
                },
×
1368
                {
×
1369
                        Name:  common.ImporterEndpoint,
×
1370
                        Value: ep,
×
1371
                },
×
1372
                {
×
1373
                        Name:  common.ImporterContentType,
×
1374
                        Value: string(contentType),
×
1375
                },
×
1376
                {
×
1377
                        Name:  common.ImporterImageSize,
×
1378
                        Value: imageSize,
×
1379
                },
×
1380
                {
×
1381
                        Name:  common.OwnerUID,
×
1382
                        Value: string(pvc.UID),
×
1383
                },
×
1384
                {
×
1385
                        Name:  common.InsecureTLSVar,
×
1386
                        Value: "false",
×
1387
                },
×
1388
        }
×
1389
        pod.Spec.Containers[0].Env = env
×
1390
        if volumeMode == corev1.PersistentVolumeBlock {
×
1391
                pod.Spec.Containers[0].VolumeDevices = AddVolumeDevices()
×
1392
        } else {
×
1393
                pod.Spec.Containers[0].VolumeMounts = AddImportVolumeMounts()
×
1394
        }
×
1395

1396
        if scratchPvc != nil {
×
1397
                pod.Spec.Containers[0].VolumeMounts = append(pod.Spec.Containers[0].VolumeMounts, corev1.VolumeMount{
×
1398
                        Name:      ScratchVolName,
×
1399
                        MountPath: common.ScratchDataDir,
×
1400
                })
×
1401
        }
×
1402

1403
        return pod
×
1404
}
1405

1406
// CreateStorageClassWithProvisioner creates CR of storage class with provisioner
1407
func CreateStorageClassWithProvisioner(name string, annotations, labels map[string]string, provisioner string) *storagev1.StorageClass {
×
1408
        return &storagev1.StorageClass{
×
1409
                Provisioner: provisioner,
×
1410
                ObjectMeta: metav1.ObjectMeta{
×
UNCOV
1411
                        Name:        name,
×
1412
                        Annotations: annotations,
×
UNCOV
1413
                        Labels:      labels,
×
UNCOV
1414
                },
×
UNCOV
1415
        }
×
1416
}
×
1417

1418
// CreateClient creates a fake client
1419
func CreateClient(objs ...runtime.Object) client.Client {
1✔
1420
        s := scheme.Scheme
1✔
1421
        _ = cdiv1.AddToScheme(s)
1✔
1422
        _ = corev1.AddToScheme(s)
1✔
1423
        _ = storagev1.AddToScheme(s)
1✔
1424
        _ = ocpconfigv1.Install(s)
1✔
1425

1✔
1426
        return fake.NewClientBuilder().WithScheme(s).WithRuntimeObjects(objs...).Build()
1✔
1427
}
1✔
1428

1429
// ErrQuotaExceeded checked is the error is of exceeded quota
UNCOV
1430
func ErrQuotaExceeded(err error) bool {
×
UNCOV
1431
        return strings.Contains(err.Error(), "exceeded quota:")
×
UNCOV
1432
}
×
1433

1434
// GetContentType returns the content type. If invalid or not set, default to kubevirt
1435
func GetContentType(contentType cdiv1.DataVolumeContentType) cdiv1.DataVolumeContentType {
1✔
1436
        switch contentType {
1✔
1437
        case
1438
                cdiv1.DataVolumeKubeVirt,
1439
                cdiv1.DataVolumeArchive:
1✔
1440
        default:
×
1441
                // TODO - shouldn't archive be the default?
×
UNCOV
1442
                contentType = cdiv1.DataVolumeKubeVirt
×
1443
        }
1444
        return contentType
1✔
1445
}
1446

1447
// GetPVCContentType returns the content type of the source image. If invalid or not set, default to kubevirt
UNCOV
1448
func GetPVCContentType(pvc *corev1.PersistentVolumeClaim) cdiv1.DataVolumeContentType {
×
1449
        contentType, found := pvc.Annotations[AnnContentType]
×
1450
        if !found {
×
1451
                // TODO - shouldn't archive be the default?
×
UNCOV
1452
                return cdiv1.DataVolumeKubeVirt
×
UNCOV
1453
        }
×
1454

UNCOV
1455
        return GetContentType(cdiv1.DataVolumeContentType(contentType))
×
1456
}
1457

1458
// GetNamespace returns the given namespace if not empty, otherwise the default namespace
1459
func GetNamespace(namespace, defaultNamespace string) string {
×
1460
        if namespace == "" {
×
1461
                return defaultNamespace
×
1462
        }
×
UNCOV
1463
        return namespace
×
1464
}
1465

1466
// IsErrCacheNotStarted checked is the error is of cache not started
UNCOV
1467
func IsErrCacheNotStarted(err error) bool {
×
1468
        target := &runtimecache.ErrCacheNotStarted{}
×
1469
        return errors.As(err, &target)
×
1470
}
×
1471

1472
// NewImportDataVolume returns new import DataVolume CR
UNCOV
1473
func NewImportDataVolume(name string) *cdiv1.DataVolume {
×
UNCOV
1474
        return &cdiv1.DataVolume{
×
UNCOV
1475
                TypeMeta: metav1.TypeMeta{APIVersion: cdiv1.SchemeGroupVersion.String()},
×
1476
                ObjectMeta: metav1.ObjectMeta{
×
1477
                        Name:      name,
×
1478
                        Namespace: metav1.NamespaceDefault,
×
1479
                        UID:       types.UID(metav1.NamespaceDefault + "-" + name),
×
UNCOV
1480
                },
×
UNCOV
1481
                Spec: cdiv1.DataVolumeSpec{
×
1482
                        Source: &cdiv1.DataVolumeSource{
×
1483
                                HTTP: &cdiv1.DataVolumeSourceHTTP{
×
1484
                                        URL: "http://example.com/data",
×
1485
                                },
×
1486
                        },
×
1487
                        PVC: &corev1.PersistentVolumeClaimSpec{
×
1488
                                AccessModes: []corev1.PersistentVolumeAccessMode{corev1.ReadWriteOnce},
×
1489
                        },
×
1490
                        PriorityClassName: "p0",
×
1491
                },
×
1492
        }
×
1493
}
×
1494

1495
// GetCloneSourceInfo returns the type, name and namespace of the cloning source
1496
func GetCloneSourceInfo(dv *cdiv1.DataVolume) (sourceType, sourceName, sourceNamespace string) {
×
1497
        // Cloning sources are mutually exclusive
×
1498
        if dv.Spec.Source.PVC != nil {
×
1499
                return "pvc", dv.Spec.Source.PVC.Name, dv.Spec.Source.PVC.Namespace
×
1500
        }
×
1501

1502
        if dv.Spec.Source.Snapshot != nil {
×
UNCOV
1503
                return "snapshot", dv.Spec.Source.Snapshot.Name, dv.Spec.Source.Snapshot.Namespace
×
UNCOV
1504
        }
×
1505

1506
        return "", "", ""
×
1507
}
1508

1509
// IsWaitForFirstConsumerEnabled tells us if we should respect "real" WFFC behavior or just let our worker pods randomly spawn
UNCOV
1510
func IsWaitForFirstConsumerEnabled(obj metav1.Object, gates featuregates.FeatureGates) (bool, error) {
×
1511
        // when PVC requests immediateBinding it cannot honor wffc logic
×
1512
        isImmediateBindingRequested := ImmediateBindingRequested(obj)
×
1513
        pvcHonorWaitForFirstConsumer := !isImmediateBindingRequested
×
UNCOV
1514
        globalHonorWaitForFirstConsumer, err := gates.HonorWaitForFirstConsumerEnabled()
×
1515
        if err != nil {
×
UNCOV
1516
                return false, err
×
UNCOV
1517
        }
×
1518

1519
        return pvcHonorWaitForFirstConsumer && globalHonorWaitForFirstConsumer, nil
×
1520
}
1521

1522
// AddImmediateBindingAnnotationIfWFFCDisabled adds the immediateBinding annotation if wffc feature gate is disabled
1523
func AddImmediateBindingAnnotationIfWFFCDisabled(obj metav1.Object, gates featuregates.FeatureGates) error {
×
1524
        globalHonorWaitForFirstConsumer, err := gates.HonorWaitForFirstConsumerEnabled()
×
1525
        if err != nil {
×
1526
                return err
×
UNCOV
1527
        }
×
1528
        if !globalHonorWaitForFirstConsumer {
×
UNCOV
1529
                AddAnnotation(obj, AnnImmediateBinding, "")
×
UNCOV
1530
        }
×
UNCOV
1531
        return nil
×
1532
}
1533

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

×
1538
        if util.ResolveVolumeMode(pvcSpec.VolumeMode) == corev1.PersistentVolumeFilesystem {
×
1539
                fsOverhead, err := GetFilesystemOverheadForStorageClass(ctx, c, pvcSpec.StorageClassName)
×
1540
                if err != nil {
×
UNCOV
1541
                        return resource.Quantity{}, err
×
UNCOV
1542
                }
×
1543
                // Parse filesystem overhead (percentage) into a 64-bit float
1544
                fsOverheadFloat, _ := strconv.ParseFloat(string(fsOverhead), 64)
×
1545

×
1546
                // Merge the previous values into a 'resource.Quantity' struct
×
1547
                requiredSpace := util.GetRequiredSpace(fsOverheadFloat, imgSize)
×
1548
                returnSize = *resource.NewScaledQuantity(requiredSpace, 0)
×
1549
        } else {
×
1550
                // Inflation is not needed with 'Block' mode
×
1551
                returnSize = *resource.NewScaledQuantity(imgSize, 0)
×
UNCOV
1552
        }
×
1553

1554
        return returnSize, nil
×
1555
}
1556

1557
// IsBound returns if the pvc is bound
1558
func IsBound(pvc *corev1.PersistentVolumeClaim) bool {
×
1559
        return pvc != nil && pvc.Status.Phase == corev1.ClaimBound
×
1560
}
×
1561

1562
// IsUnbound returns if the pvc is not bound yet
1563
func IsUnbound(pvc *corev1.PersistentVolumeClaim) bool {
×
UNCOV
1564
        return !IsBound(pvc)
×
UNCOV
1565
}
×
1566

1567
// IsLost returns if the pvc is lost
1568
func IsLost(pvc *corev1.PersistentVolumeClaim) bool {
×
1569
        return pvc != nil && pvc.Status.Phase == corev1.ClaimLost
×
UNCOV
1570
}
×
1571

1572
// IsImageStream returns true if registry source is ImageStream
1573
func IsImageStream(pvc *corev1.PersistentVolumeClaim) bool {
×
1574
        return pvc.Annotations[AnnRegistryImageStream] == "true"
×
UNCOV
1575
}
×
1576

1577
// ShouldIgnorePod checks if a pod should be ignored.
1578
// If this is a completed pod that was used for one checkpoint of a multi-stage import, it
1579
// should be ignored by pod lookups as long as the retainAfterCompletion annotation is set.
UNCOV
1580
func ShouldIgnorePod(pod *corev1.Pod, pvc *corev1.PersistentVolumeClaim) bool {
×
UNCOV
1581
        retain := pvc.ObjectMeta.Annotations[AnnPodRetainAfterCompletion]
×
1582
        checkpoint := pvc.ObjectMeta.Annotations[AnnCurrentCheckpoint]
×
1583
        if checkpoint != "" && pod.Status.Phase == corev1.PodSucceeded {
×
1584
                return retain == "true"
×
UNCOV
1585
        }
×
UNCOV
1586
        return false
×
1587
}
1588

1589
// BuildHTTPClient generates an http client that accepts any certificate, since we are using
1590
// it to get prometheus data it doesn't matter if someone can intercept the data. Once we have
1591
// a mechanism to properly sign the server, we can update this method to get a proper client.
1592
func BuildHTTPClient(httpClient *http.Client) *http.Client {
×
1593
        if httpClient == nil {
×
1594
                defaultTransport := http.DefaultTransport.(*http.Transport)
×
1595
                // Create new Transport that ignores self-signed SSL
×
UNCOV
1596
                //nolint:gosec
×
UNCOV
1597
                tr := &http.Transport{
×
UNCOV
1598
                        Proxy:                 defaultTransport.Proxy,
×
UNCOV
1599
                        DialContext:           defaultTransport.DialContext,
×
UNCOV
1600
                        MaxIdleConns:          defaultTransport.MaxIdleConns,
×
1601
                        IdleConnTimeout:       defaultTransport.IdleConnTimeout,
×
1602
                        ExpectContinueTimeout: defaultTransport.ExpectContinueTimeout,
×
1603
                        TLSHandshakeTimeout:   defaultTransport.TLSHandshakeTimeout,
×
1604
                        TLSClientConfig:       &tls.Config{InsecureSkipVerify: true},
×
1605
                }
×
1606
                httpClient = &http.Client{
×
1607
                        Transport: tr,
×
1608
                }
×
1609
        }
×
1610
        return httpClient
×
1611
}
1612

1613
// ErrConnectionRefused checks for connection refused errors
1614
func ErrConnectionRefused(err error) bool {
×
1615
        return strings.Contains(err.Error(), "connection refused")
×
1616
}
×
1617

1618
// GetPodMetricsPort returns, if exists, the metrics port from the passed pod
1619
func GetPodMetricsPort(pod *corev1.Pod) (int, error) {
1✔
1620
        for _, container := range pod.Spec.Containers {
2✔
1621
                for _, port := range container.Ports {
2✔
1622
                        if port.Name == "metrics" {
2✔
1623
                                return int(port.ContainerPort), nil
1✔
1624
                        }
1✔
1625
                }
1626
        }
1627
        return 0, errors.New("Metrics port not found in pod")
1✔
1628
}
1629

1630
// GetMetricsURL builds the metrics URL according to the specified pod
1631
func GetMetricsURL(pod *corev1.Pod) (string, error) {
1✔
1632
        if pod == nil {
1✔
UNCOV
1633
                return "", nil
×
UNCOV
1634
        }
×
1635
        port, err := GetPodMetricsPort(pod)
1✔
1636
        if err != nil || pod.Status.PodIP == "" {
2✔
1637
                return "", err
1✔
1638
        }
1✔
1639
        domain := net.JoinHostPort(pod.Status.PodIP, fmt.Sprint(port))
1✔
1640
        url := fmt.Sprintf("https://%s/metrics", domain)
1✔
1641
        return url, nil
1✔
1642
}
1643

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

1668
        // Parse the progress from the body
1669
        progressReport := ""
×
UNCOV
1670
        match := regExp.FindStringSubmatch(string(body))
×
1671
        if match != nil {
×
1672
                progressReport = match[len(match)-1]
×
1673
        }
×
1674
        return progressReport, nil
×
1675
}
1676

1677
// UpdateHTTPAnnotations updates the passed annotations for proper http import
1678
func UpdateHTTPAnnotations(annotations map[string]string, http *cdiv1.DataVolumeSourceHTTP) {
×
1679
        annotations[AnnEndpoint] = http.URL
×
1680
        annotations[AnnSource] = SourceHTTP
×
1681

×
1682
        if http.SecretRef != "" {
×
1683
                annotations[AnnSecret] = http.SecretRef
×
UNCOV
1684
        }
×
UNCOV
1685
        if http.CertConfigMap != "" {
×
UNCOV
1686
                annotations[AnnCertConfigMap] = http.CertConfigMap
×
1687
        }
×
1688
        for index, header := range http.ExtraHeaders {
×
1689
                annotations[fmt.Sprintf("%s.%d", AnnExtraHeaders, index)] = header
×
1690
        }
×
1691
        for index, header := range http.SecretExtraHeaders {
×
1692
                annotations[fmt.Sprintf("%s.%d", AnnSecretExtraHeaders, index)] = header
×
1693
        }
×
1694
}
1695

1696
// UpdateS3Annotations updates the passed annotations for proper S3 import
1697
func UpdateS3Annotations(annotations map[string]string, s3 *cdiv1.DataVolumeSourceS3) {
×
1698
        annotations[AnnEndpoint] = s3.URL
×
1699
        annotations[AnnSource] = SourceS3
×
1700
        if s3.SecretRef != "" {
×
1701
                annotations[AnnSecret] = s3.SecretRef
×
1702
        }
×
UNCOV
1703
        if s3.CertConfigMap != "" {
×
UNCOV
1704
                annotations[AnnCertConfigMap] = s3.CertConfigMap
×
UNCOV
1705
        }
×
1706
}
1707

1708
// UpdateGCSAnnotations updates the passed annotations for proper GCS import
1709
func UpdateGCSAnnotations(annotations map[string]string, gcs *cdiv1.DataVolumeSourceGCS) {
×
1710
        annotations[AnnEndpoint] = gcs.URL
×
1711
        annotations[AnnSource] = SourceGCS
×
1712
        if gcs.SecretRef != "" {
×
1713
                annotations[AnnSecret] = gcs.SecretRef
×
1714
        }
×
1715
}
1716

1717
// UpdateRegistryAnnotations updates the passed annotations for proper registry import
1718
func UpdateRegistryAnnotations(annotations map[string]string, registry *cdiv1.DataVolumeSourceRegistry) {
×
1719
        annotations[AnnSource] = SourceRegistry
×
1720
        pullMethod := registry.PullMethod
×
1721
        if pullMethod != nil && *pullMethod != "" {
×
1722
                annotations[AnnRegistryImportMethod] = string(*pullMethod)
×
1723
        }
×
UNCOV
1724
        url := registry.URL
×
UNCOV
1725
        if url != nil && *url != "" {
×
UNCOV
1726
                annotations[AnnEndpoint] = *url
×
1727
        } else {
×
1728
                imageStream := registry.ImageStream
×
1729
                if imageStream != nil && *imageStream != "" {
×
1730
                        annotations[AnnEndpoint] = *imageStream
×
1731
                        annotations[AnnRegistryImageStream] = "true"
×
1732
                }
×
1733
        }
1734
        secretRef := registry.SecretRef
×
1735
        if secretRef != nil && *secretRef != "" {
×
1736
                annotations[AnnSecret] = *secretRef
×
1737
        }
×
1738
        certConfigMap := registry.CertConfigMap
×
1739
        if certConfigMap != nil && *certConfigMap != "" {
×
1740
                annotations[AnnCertConfigMap] = *certConfigMap
×
1741
        }
×
1742

1743
        if registry.Platform != nil && registry.Platform.Architecture != "" {
×
1744
                annotations[AnnRegistryImageArchitecture] = registry.Platform.Architecture
×
1745
        }
×
1746
}
1747

1748
// UpdateVDDKAnnotations updates the passed annotations for proper VDDK import
1749
func UpdateVDDKAnnotations(annotations map[string]string, vddk *cdiv1.DataVolumeSourceVDDK) {
×
1750
        annotations[AnnEndpoint] = vddk.URL
×
UNCOV
1751
        annotations[AnnSource] = SourceVDDK
×
1752
        annotations[AnnSecret] = vddk.SecretRef
×
1753
        annotations[AnnBackingFile] = vddk.BackingFile
×
1754
        annotations[AnnUUID] = vddk.UUID
×
UNCOV
1755
        annotations[AnnThumbprint] = vddk.Thumbprint
×
UNCOV
1756
        if vddk.InitImageURL != "" {
×
UNCOV
1757
                annotations[AnnVddkInitImageURL] = vddk.InitImageURL
×
1758
        }
×
1759
        if vddk.ExtraArgs != "" {
×
1760
                annotations[AnnVddkExtraArgs] = vddk.ExtraArgs
×
1761
        }
×
1762
}
1763

1764
// UpdateImageIOAnnotations updates the passed annotations for proper imageIO import
1765
func UpdateImageIOAnnotations(annotations map[string]string, imageio *cdiv1.DataVolumeSourceImageIO) {
×
1766
        annotations[AnnEndpoint] = imageio.URL
×
1767
        annotations[AnnSource] = SourceImageio
×
1768
        annotations[AnnSecret] = imageio.SecretRef
×
1769
        annotations[AnnCertConfigMap] = imageio.CertConfigMap
×
1770
        annotations[AnnDiskID] = imageio.DiskID
×
UNCOV
1771
}
×
1772

1773
// IsPVBoundToPVC checks if a PV is bound to a specific PVC
1774
func IsPVBoundToPVC(pv *corev1.PersistentVolume, pvc *corev1.PersistentVolumeClaim) bool {
1✔
1775
        claimRef := pv.Spec.ClaimRef
1✔
1776
        return claimRef != nil && claimRef.Name == pvc.Name && claimRef.Namespace == pvc.Namespace && claimRef.UID == pvc.UID
1✔
1777
}
1✔
1778

1779
// Rebind binds the PV of source to target
1780
func Rebind(ctx context.Context, c client.Client, source, target *corev1.PersistentVolumeClaim) error {
1✔
1781
        pv := &corev1.PersistentVolume{
1✔
1782
                ObjectMeta: metav1.ObjectMeta{
1✔
1783
                        Name: source.Spec.VolumeName,
1✔
1784
                },
1✔
1785
        }
1✔
1786

1✔
1787
        if err := c.Get(ctx, client.ObjectKeyFromObject(pv), pv); err != nil {
2✔
1788
                return err
1✔
1789
        }
1✔
1790

1791
        // Examine the claimref for the PV and see if it's still bound to PVC'
1792
        if pv.Spec.ClaimRef == nil {
1✔
UNCOV
1793
                return fmt.Errorf("PV %s claimRef is nil", pv.Name)
×
UNCOV
1794
        }
×
1795

1796
        if !IsPVBoundToPVC(pv, source) {
2✔
1797
                // Something is not right if the PV is neither bound to PVC' nor target PVC
1✔
1798
                if !IsPVBoundToPVC(pv, target) {
2✔
1799
                        klog.Errorf("PV bound to unexpected PVC: Could not rebind to target PVC '%s'", target.Name)
1✔
1800
                        return fmt.Errorf("PV %s bound to unexpected claim %s", pv.Name, pv.Spec.ClaimRef.Name)
1✔
1801
                }
1✔
1802
                // our work is done
1803
                return nil
1✔
1804
        }
1805

1806
        // Rebind PVC to target PVC
1807
        pv.Spec.ClaimRef = &corev1.ObjectReference{
1✔
1808
                Namespace:       target.Namespace,
1✔
1809
                Name:            target.Name,
1✔
1810
                UID:             target.UID,
1✔
1811
                ResourceVersion: target.ResourceVersion,
1✔
1812
        }
1✔
1813
        klog.V(3).Info("Rebinding PV to target PVC", "PVC", target.Name)
1✔
1814
        if err := c.Update(context.TODO(), pv); err != nil {
1✔
UNCOV
1815
                return err
×
UNCOV
1816
        }
×
1817

1818
        return nil
1✔
1819
}
1820

1821
// BulkDeleteResources deletes a bunch of resources
UNCOV
1822
func BulkDeleteResources(ctx context.Context, c client.Client, obj client.ObjectList, lo client.ListOption) error {
×
UNCOV
1823
        if err := c.List(ctx, obj, lo); err != nil {
×
1824
                if meta.IsNoMatchError(err) {
×
1825
                        return nil
×
UNCOV
1826
                }
×
UNCOV
1827
                return err
×
1828
        }
1829

UNCOV
1830
        sv := reflect.ValueOf(obj).Elem()
×
1831
        iv := sv.FieldByName("Items")
×
1832

×
1833
        for i := 0; i < iv.Len(); i++ {
×
1834
                obj := iv.Index(i).Addr().Interface().(client.Object)
×
1835
                if obj.GetDeletionTimestamp().IsZero() {
×
1836
                        klog.V(3).Infof("Deleting type %+v %+v", reflect.TypeOf(obj), obj)
×
UNCOV
1837
                        if err := c.Delete(ctx, obj); err != nil {
×
UNCOV
1838
                                return err
×
1839
                        }
×
1840
                }
1841
        }
1842

1843
        return nil
×
1844
}
1845

1846
// ValidateSnapshotCloneSize does proper size validation when doing a clone from snapshot operation
1847
func ValidateSnapshotCloneSize(snapshot *snapshotv1.VolumeSnapshot, pvcSpec *corev1.PersistentVolumeClaimSpec, targetSC *storagev1.StorageClass, log logr.Logger) (bool, error) {
×
1848
        restoreSize := snapshot.Status.RestoreSize
×
UNCOV
1849
        if restoreSize == nil {
×
UNCOV
1850
                return false, fmt.Errorf("snapshot has no RestoreSize")
×
UNCOV
1851
        }
×
1852
        targetRequest, hasTargetRequest := pvcSpec.Resources.Requests[corev1.ResourceStorage]
×
UNCOV
1853
        allowExpansion := targetSC.AllowVolumeExpansion != nil && *targetSC.AllowVolumeExpansion
×
UNCOV
1854
        if hasTargetRequest {
×
UNCOV
1855
                // otherwise will just use restoreSize
×
1856
                if restoreSize.Cmp(targetRequest) < 0 && !allowExpansion {
×
1857
                        log.V(3).Info("Can't expand restored PVC because SC does not allow expansion, need to fall back to host assisted")
×
1858
                        return false, nil
×
1859
                }
×
1860
        }
1861
        return true, nil
×
1862
}
1863

1864
// ValidateSnapshotCloneProvisioners validates the target PVC storage class against the snapshot class provisioner
1865
func ValidateSnapshotCloneProvisioners(vsc *snapshotv1.VolumeSnapshotContent, storageClass *storagev1.StorageClass) (bool, error) {
×
1866
        // Do snapshot and storage class validation
×
1867
        if storageClass == nil {
×
1868
                return false, fmt.Errorf("target storage class not found")
×
UNCOV
1869
        }
×
1870
        if storageClass.Provisioner != vsc.Spec.Driver {
×
UNCOV
1871
                return false, nil
×
UNCOV
1872
        }
×
1873
        // TODO: get sourceVolumeMode from volumesnapshotcontent and validate against target spec
1874
        // currently don't have CRDs in CI with sourceVolumeMode which is pretty new
1875
        // converting volume mode is possible but has security implications
1876
        return true, nil
×
1877
}
1878

1879
// GetSnapshotClassForSmartClone looks up the snapshot class based on the storage class
1880
func GetSnapshotClassForSmartClone(pvc *corev1.PersistentVolumeClaim, targetPvcStorageClassName, snapshotClassName *string, log logr.Logger, client client.Client, recorder record.EventRecorder) (string, error) {
×
1881
        logger := log.WithName("GetSnapshotClassForSmartClone").V(3)
×
UNCOV
1882
        // Check if relevant CRDs are available
×
UNCOV
1883
        if !isCsiCrdsDeployed(client, log) {
×
UNCOV
1884
                logger.Info("Missing CSI snapshotter CRDs, falling back to host assisted clone")
×
1885
                return "", nil
×
UNCOV
1886
        }
×
1887

UNCOV
1888
        targetStorageClass, err := GetStorageClassByNameWithK8sFallback(context.TODO(), client, targetPvcStorageClassName)
×
1889
        if err != nil {
×
1890
                return "", err
×
1891
        }
×
1892
        if targetStorageClass == nil {
×
1893
                logger.Info("Target PVC's Storage Class not found")
×
1894
                return "", nil
×
1895
        }
×
1896

1897
        vscName, err := GetVolumeSnapshotClass(context.TODO(), client, pvc, targetStorageClass.Provisioner, snapshotClassName, logger, recorder)
×
1898
        if err != nil {
×
1899
                return "", err
×
1900
        }
×
1901
        if vscName != nil {
×
1902
                if pvc != nil {
×
1903
                        logger.Info("smart-clone is applicable for datavolume", "datavolume",
×
1904
                                pvc.Name, "snapshot class", *vscName)
×
UNCOV
1905
                }
×
1906
                return *vscName, nil
×
1907
        }
1908

1909
        logger.Info("Could not match snapshotter with storage class, falling back to host assisted clone")
×
1910
        return "", nil
×
1911
}
1912

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

×
1918
        logEvent := func(message, vscName string) {
×
1919
                logger.Info(message, "name", vscName)
×
UNCOV
1920
                if pvc != nil {
×
UNCOV
1921
                        msg := fmt.Sprintf("%s %s", message, vscName)
×
UNCOV
1922
                        recorder.Event(pvc, corev1.EventTypeNormal, VolumeSnapshotClassSelected, msg)
×
UNCOV
1923
                }
×
1924
        }
1925

1926
        if snapshotClassName != nil {
×
1927
                vsc := &snapshotv1.VolumeSnapshotClass{}
×
1928
                if err := c.Get(context.TODO(), types.NamespacedName{Name: *snapshotClassName}, vsc); err != nil {
×
1929
                        return nil, err
×
1930
                }
×
1931
                if vsc.Driver == driver {
×
1932
                        logEvent(MessageStorageProfileVolumeSnapshotClassSelected, vsc.Name)
×
UNCOV
1933
                        return snapshotClassName, nil
×
UNCOV
1934
                }
×
1935
                return nil, nil
×
1936
        }
1937

1938
        vscList := &snapshotv1.VolumeSnapshotClassList{}
×
1939
        if err := c.List(ctx, vscList); err != nil {
×
1940
                if meta.IsNoMatchError(err) {
×
1941
                        return nil, nil
×
1942
                }
×
1943
                return nil, err
×
1944
        }
1945

UNCOV
1946
        var candidates []string
×
1947
        for _, vsc := range vscList.Items {
×
1948
                if vsc.Driver == driver {
×
1949
                        if vsc.Annotations[AnnDefaultSnapshotClass] == "true" {
×
1950
                                logEvent(MessageDefaultVolumeSnapshotClassSelected, vsc.Name)
×
1951
                                vscName := vsc.Name
×
1952
                                return &vscName, nil
×
UNCOV
1953
                        }
×
UNCOV
1954
                        candidates = append(candidates, vsc.Name)
×
1955
                }
1956
        }
1957

1958
        if len(candidates) > 0 {
×
1959
                sort.Strings(candidates)
×
1960
                logEvent(MessageFirstVolumeSnapshotClassSelected, candidates[0])
×
1961
                return &candidates[0], nil
×
1962
        }
×
1963

UNCOV
1964
        return nil, nil
×
1965
}
1966

1967
// isCsiCrdsDeployed checks whether the CSI snapshotter CRD are deployed
1968
func isCsiCrdsDeployed(c client.Client, log logr.Logger) bool {
×
1969
        version := "v1"
×
1970
        vsClass := "volumesnapshotclasses." + snapshotv1.GroupName
×
1971
        vsContent := "volumesnapshotcontents." + snapshotv1.GroupName
×
UNCOV
1972
        vs := "volumesnapshots." + snapshotv1.GroupName
×
1973

×
UNCOV
1974
        return isCrdDeployed(c, vsClass, version, log) &&
×
UNCOV
1975
                isCrdDeployed(c, vsContent, version, log) &&
×
UNCOV
1976
                isCrdDeployed(c, vs, version, log)
×
1977
}
×
1978

1979
// isCrdDeployed checks whether a CRD is deployed
1980
func isCrdDeployed(c client.Client, name, version string, log logr.Logger) bool {
×
1981
        crd := &extv1.CustomResourceDefinition{}
×
1982
        err := c.Get(context.TODO(), types.NamespacedName{Name: name}, crd)
×
1983
        if err != nil {
×
1984
                if !k8serrors.IsNotFound(err) {
×
1985
                        log.Info("Error looking up CRD", "crd name", name, "version", version, "error", err)
×
1986
                }
×
UNCOV
1987
                return false
×
1988
        }
1989

1990
        for _, v := range crd.Spec.Versions {
×
1991
                if v.Name == version && v.Served {
×
1992
                        return true
×
1993
                }
×
1994
        }
1995

1996
        return false
×
1997
}
1998

1999
// IsSnapshotReady indicates if a volume snapshot is ready to be used
2000
func IsSnapshotReady(snapshot *snapshotv1.VolumeSnapshot) bool {
×
2001
        return snapshot.Status != nil && snapshot.Status.ReadyToUse != nil && *snapshot.Status.ReadyToUse
×
2002
}
×
2003

2004
// GetResource updates given obj with the data of the object with the same name and namespace
2005
func GetResource(ctx context.Context, c client.Client, namespace, name string, obj client.Object) (bool, error) {
×
UNCOV
2006
        obj.SetNamespace(namespace)
×
UNCOV
2007
        obj.SetName(name)
×
UNCOV
2008

×
2009
        err := c.Get(ctx, client.ObjectKeyFromObject(obj), obj)
×
2010
        if err != nil {
×
2011
                if k8serrors.IsNotFound(err) {
×
UNCOV
2012
                        return false, nil
×
UNCOV
2013
                }
×
2014

2015
                return false, err
×
2016
        }
2017

2018
        return true, nil
×
2019
}
2020

2021
// PatchArgs are the args for Patch
2022
type PatchArgs struct {
2023
        Client client.Client
2024
        Log    logr.Logger
2025
        Obj    client.Object
2026
        OldObj client.Object
2027
}
2028

2029
// GetAnnotatedEventSource returns resource referenced by AnnEventSource annotations
UNCOV
2030
func GetAnnotatedEventSource(ctx context.Context, c client.Client, obj client.Object) (client.Object, error) {
×
UNCOV
2031
        esk, ok := obj.GetAnnotations()[AnnEventSourceKind]
×
UNCOV
2032
        if !ok {
×
UNCOV
2033
                return obj, nil
×
UNCOV
2034
        }
×
UNCOV
2035
        if esk != "PersistentVolumeClaim" {
×
UNCOV
2036
                return obj, nil
×
UNCOV
2037
        }
×
UNCOV
2038
        es, ok := obj.GetAnnotations()[AnnEventSource]
×
2039
        if !ok {
×
2040
                return obj, nil
×
2041
        }
×
2042
        namespace, name, err := cache.SplitMetaNamespaceKey(es)
×
2043
        if err != nil {
×
2044
                return nil, err
×
2045
        }
×
2046
        pvc := &corev1.PersistentVolumeClaim{
×
2047
                ObjectMeta: metav1.ObjectMeta{
×
2048
                        Namespace: namespace,
×
2049
                        Name:      name,
×
2050
                },
×
2051
        }
×
2052
        if err := c.Get(ctx, client.ObjectKeyFromObject(pvc), pvc); err != nil {
×
2053
                return nil, err
×
2054
        }
×
2055
        return pvc, nil
×
2056
}
2057

2058
// OwnedByDataVolume returns true if the object is owned by a DataVolume
2059
func OwnedByDataVolume(obj metav1.Object) bool {
×
2060
        owner := metav1.GetControllerOf(obj)
×
2061
        return owner != nil && owner.Kind == "DataVolume"
×
2062
}
×
2063

2064
// CopyAllowedAnnotations copies the allowed annotations from the source object
2065
// to the destination object
UNCOV
2066
func CopyAllowedAnnotations(srcObj, dstObj metav1.Object) {
×
UNCOV
2067
        for ann, def := range allowedAnnotations {
×
2068
                val, ok := srcObj.GetAnnotations()[ann]
×
2069
                if !ok && def != "" {
×
2070
                        val = def
×
2071
                }
×
UNCOV
2072
                if val != "" {
×
UNCOV
2073
                        klog.V(1).Info("Applying annotation", "Name", dstObj.GetName(), ann, val)
×
UNCOV
2074
                        AddAnnotation(dstObj, ann, val)
×
2075
                }
×
2076
        }
2077
}
2078

2079
// CopyAllowedLabels copies allowed labels matching the validLabelsMatch regexp from the
2080
// source map to the destination object allowing overwrites
2081
func CopyAllowedLabels(srcLabels map[string]string, dstObj metav1.Object, overwrite bool) {
1✔
2082
        for label, value := range srcLabels {
2✔
2083
                if _, found := dstObj.GetLabels()[label]; (!found || overwrite) && validLabelsMatch.MatchString(label) {
2✔
2084
                        AddLabel(dstObj, label, value)
1✔
2085
                }
1✔
2086
        }
2087
}
2088

2089
// ClaimMayExistBeforeDataVolume returns true if the PVC may exist before the DataVolume
UNCOV
2090
func ClaimMayExistBeforeDataVolume(c client.Client, pvc *corev1.PersistentVolumeClaim, dv *cdiv1.DataVolume) (bool, error) {
×
UNCOV
2091
        if ClaimIsPopulatedForDataVolume(pvc, dv) {
×
UNCOV
2092
                return true, nil
×
UNCOV
2093
        }
×
UNCOV
2094
        return AllowClaimAdoption(c, pvc, dv)
×
2095
}
2096

2097
// ClaimIsPopulatedForDataVolume returns true if the PVC is populated for the given DataVolume
UNCOV
2098
func ClaimIsPopulatedForDataVolume(pvc *corev1.PersistentVolumeClaim, dv *cdiv1.DataVolume) bool {
×
2099
        return pvc != nil && dv != nil && pvc.Annotations[AnnPopulatedFor] == dv.Name
×
2100
}
×
2101

2102
// AllowClaimAdoption returns true if the PVC may be adopted
2103
func AllowClaimAdoption(c client.Client, pvc *corev1.PersistentVolumeClaim, dv *cdiv1.DataVolume) (bool, error) {
×
UNCOV
2104
        if pvc == nil || dv == nil {
×
UNCOV
2105
                return false, nil
×
UNCOV
2106
        }
×
2107
        anno, ok := pvc.Annotations[AnnCreatedForDataVolume]
×
2108
        if ok && anno == string(dv.UID) {
×
2109
                return false, nil
×
UNCOV
2110
        }
×
UNCOV
2111
        anno, ok = dv.Annotations[AnnAllowClaimAdoption]
×
2112
        // if annotation exists, go with that regardless of featuregate
×
2113
        if ok {
×
2114
                val, _ := strconv.ParseBool(anno)
×
2115
                return val, nil
×
2116
        }
×
2117
        return featuregates.NewFeatureGates(c).ClaimAdoptionEnabled()
×
2118
}
2119

2120
// ResolveDataSourceChain resolves a DataSource reference.
2121
// Returns an error if DataSource reference is not found or
2122
// DataSource reference points to another DataSource
2123
func ResolveDataSourceChain(ctx context.Context, client client.Client, dataSource *cdiv1.DataSource) (*cdiv1.DataSource, error) {
×
2124
        if dataSource.Spec.Source.DataSource == nil {
×
2125
                return dataSource, nil
×
2126
        }
×
2127

UNCOV
2128
        ref := dataSource.Spec.Source.DataSource
×
UNCOV
2129
        refNs := GetNamespace(ref.Namespace, dataSource.Namespace)
×
UNCOV
2130
        if dataSource.Namespace != refNs {
×
UNCOV
2131
                return dataSource, ErrDataSourceCrossNamespace
×
2132
        }
×
2133
        if ref.Name == dataSource.Name && refNs == dataSource.Namespace {
×
2134
                return nil, ErrDataSourceSelfReference
×
2135
        }
×
2136

2137
        resolved := &cdiv1.DataSource{}
×
2138
        if err := client.Get(ctx, types.NamespacedName{Name: ref.Name, Namespace: refNs}, resolved); err != nil {
×
2139
                return nil, err
×
2140
        }
×
2141

2142
        if resolved.Spec.Source.DataSource != nil {
×
2143
                return nil, ErrDataSourceMaxDepthReached
×
2144
        }
×
2145

2146
        return resolved, nil
×
2147
}
2148

2149
func sortEvents(events *corev1.EventList, usingPopulator bool, pvcPrimeName string) {
1✔
2150
        // Sort event lists by containing primeName substring and most recent timestamp
1✔
2151
        sort.Slice(events.Items, func(i, j int) bool {
2✔
2152
                if usingPopulator {
2✔
2153
                        firstContainsPrime := strings.Contains(events.Items[i].Message, pvcPrimeName)
1✔
2154
                        secondContainsPrime := strings.Contains(events.Items[j].Message, pvcPrimeName)
1✔
2155

1✔
2156
                        if firstContainsPrime && !secondContainsPrime {
2✔
2157
                                return true
1✔
2158
                        }
1✔
2159
                        if !firstContainsPrime && secondContainsPrime {
2✔
2160
                                return false
1✔
2161
                        }
1✔
2162
                }
2163

2164
                // if the timestamps are the same, prioritze longer messages to make sure our sorting is deterministic
2165
                if events.Items[i].LastTimestamp.Time.Equal(events.Items[j].LastTimestamp.Time) {
1✔
UNCOV
2166
                        return len(events.Items[i].Message) > len(events.Items[j].Message)
×
UNCOV
2167
                }
×
2168

2169
                // if both contains primeName substring or neither, just sort on timestamp
2170
                return events.Items[i].LastTimestamp.Time.After(events.Items[j].LastTimestamp.Time)
1✔
2171
        })
2172
}
2173

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

×
UNCOV
2179
        anno := pvc.GetAnnotations()
×
UNCOV
2180
        if anno == nil {
×
UNCOV
2181
                return nil
×
UNCOV
2182
        }
×
2183

UNCOV
2184
        if IsBound(pvc) {
×
2185
                anno := pvc.GetAnnotations()
×
2186
                delete(anno, AnnBoundCondition)
×
2187
                delete(anno, AnnBoundConditionReason)
×
2188
                delete(anno, AnnBoundConditionMessage)
×
2189

×
2190
                if !reflect.DeepEqual(currentPvcCopy, pvc) {
×
2191
                        patch := client.MergeFrom(currentPvcCopy)
×
UNCOV
2192
                        if err := c.Patch(context.TODO(), pvc, patch); err != nil {
×
2193
                                return err
×
2194
                        }
×
2195
                }
2196

2197
                return nil
×
2198
        }
2199

2200
        if pvc.Status.Phase != corev1.ClaimPending {
×
2201
                return nil
×
2202
        }
×
2203

2204
        // set bound condition by getting the latest event
UNCOV
2205
        events := &corev1.EventList{}
×
2206

×
UNCOV
2207
        err := c.List(context.TODO(), events,
×
UNCOV
2208
                client.InNamespace(pvc.GetNamespace()),
×
2209
                client.MatchingFields{"involvedObject.name": pvc.GetName(),
×
2210
                        "involvedObject.uid": string(pvc.GetUID())},
×
2211
        )
×
UNCOV
2212

×
UNCOV
2213
        if err != nil {
×
2214
                // Log the error but don't fail the reconciliation
×
2215
                log.Error(err, "Unable to list events for PVC bound condition update", "pvc", pvc.Name)
×
2216
                return nil
×
2217
        }
×
2218

2219
        if len(events.Items) == 0 {
×
2220
                return nil
×
2221
        }
×
2222

2223
        pvcPrime, usingPopulator := anno[AnnPVCPrimeName]
×
2224

×
2225
        // Sort event lists by containing primeName substring and most recent timestamp
×
2226
        sortEvents(events, usingPopulator, pvcPrime)
×
UNCOV
2227

×
2228
        boundMessage := ""
×
2229
        // check if prime name annotation exists
×
2230
        if usingPopulator {
×
UNCOV
2231
                // if we are using populators get the latest event from prime pvc
×
2232
                pvcPrime = fmt.Sprintf("[%s] : ", pvcPrime)
×
2233

×
2234
                // if the first event does not contain a prime message, none will so return
×
2235
                primeIdx := strings.Index(events.Items[0].Message, pvcPrime)
×
2236
                if primeIdx == -1 {
×
2237
                        log.V(1).Info("No bound message found, skipping bound condition update", "pvc", pvc.Name)
×
2238
                        return nil
×
2239
                }
×
2240
                boundMessage = events.Items[0].Message[primeIdx+len(pvcPrime):]
×
2241
        } else {
×
2242
                // if not using populators just get the latest event
×
2243
                boundMessage = events.Items[0].Message
×
2244
        }
×
2245

2246
        // since we checked status of phase above, we know this is pending
2247
        anno[AnnBoundCondition] = "false"
×
2248
        anno[AnnBoundConditionReason] = "Pending"
×
2249
        anno[AnnBoundConditionMessage] = boundMessage
×
2250

×
2251
        patch := client.MergeFrom(currentPvcCopy)
×
2252
        if err := c.Patch(context.TODO(), pvc, patch); err != nil {
×
2253
                return err
×
UNCOV
2254
        }
×
2255

2256
        return nil
×
2257
}
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