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

kubevirt / containerized-data-importer / #5512

31 Jul 2025 03:21PM UTC coverage: 59.26% (-0.09%) from 59.346%
#5512

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

16 of 53 new or added lines in 4 files covered. (30.19%)

15 existing lines in 2 files now uncovered.

17174 of 28981 relevant lines covered (59.26%)

0.65 hits per line

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

13.91
/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
)
400

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

×
518
        return nil
519
}
×
520

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

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

539
        return storageClass, nil
540
}
×
541

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

775
        return pods, nil
776
}
×
777

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

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

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

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

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

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

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

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

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

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

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

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

×
843
        return cdiconfig.Status.Preallocation
844
}
×
845

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

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

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

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

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

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

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

885
        obj.SetFinalizers(finalizers)
886
}
×
887

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

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

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

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

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

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

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

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

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

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

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

×
947
        return ""
948
}
×
949

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

×
955
        return "persistentvolumeclaims"
956
}
×
957

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

965
        return sourcePvc.Name
966
}
×
967

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

×
980
        return nil
981
}
×
982

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

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

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

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

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

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

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

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

×
1052
        return err
1053
}
×
1054

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

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

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

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

×
1113
        storageProfile := &cdiv1.StorageProfile{}
NEW
1114
        if err := client.Get(ctx, types.NamespacedName{Name: sc.Name}, storageProfile); err != nil {
×
NEW
1115
                if k8serrors.IsNotFound(err) {
×
NEW
1116
                        return &storageResources, nil
×
NEW
1117
                }
×
NEW
1118
                return nil, err
×
NEW
1119
        }
×
1120

1121
        requestedSize, hasSize := storageResources.Requests[corev1.ResourceStorage]
NEW
1122
        if !hasSize {
×
NEW
1123
                return &storageResources, nil
×
NEW
1124
        }
×
NEW
1125

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

1140
        return &storageResources, nil
NEW
1141
}
×
1142

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

×
1151
        // Verify that the target PVC size is equal or larger than the source.
1152
        if sourceRequest.Value() > targetRequest.Value() {
1153
                return errors.Errorf("target resources requests storage size is smaller than the source %d < %d", targetRequest.Value(), sourceRequest.Value())
×
1154
        }
×
1155
        return nil
×
1156
}
×
1157

1158
// CreateCloneSourcePodName creates clone source pod name
1159
func CreateCloneSourcePodName(targetPvc *corev1.PersistentVolumeClaim) string {
1160
        return string(targetPvc.GetUID()) + common.ClonerSourcePodNameSuffix
×
1161
}
×
1162

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

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

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

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

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

1229
// CreatePvc creates PVC
1230
func CreatePvc(name, ns string, annotations, labels map[string]string) *corev1.PersistentVolumeClaim {
1231
        return CreatePvcInStorageClass(name, ns, nil, annotations, labels, corev1.ClaimBound)
1✔
1232
}
1✔
1233

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

1264
// GetAPIServerKey returns API server RSA key
1265
func GetAPIServerKey() *rsa.PrivateKey {
1266
        apiServerKeyOnce.Do(func() {
×
1267
                apiServerKey, _ = rsa.GenerateKey(rand.Reader, 2048)
×
1268
        })
×
1269
        return apiServerKey
×
1270
}
×
1271

1272
// CreateStorageClass creates storage class CR
1273
func CreateStorageClass(name string, annotations map[string]string) *storagev1.StorageClass {
1274
        return &storagev1.StorageClass{
1✔
1275
                ObjectMeta: metav1.ObjectMeta{
1✔
1276
                        Name:        name,
1✔
1277
                        Annotations: annotations,
1✔
1278
                },
1✔
1279
        }
1✔
1280
}
1✔
1281

1✔
1282
// CreateImporterTestPod creates importer test pod CR
1283
func CreateImporterTestPod(pvc *corev1.PersistentVolumeClaim, dvname string, scratchPvc *corev1.PersistentVolumeClaim) *corev1.Pod {
1284
        // importer pod name contains the pvc name
×
1285
        podName := fmt.Sprintf("%s-%s", common.ImporterPodName, pvc.Name)
×
1286

×
1287
        blockOwnerDeletion := true
×
1288
        isController := true
×
1289

×
1290
        volumes := []corev1.Volume{
×
1291
                {
×
1292
                        Name: dvname,
×
1293
                        VolumeSource: corev1.VolumeSource{
×
1294
                                PersistentVolumeClaim: &corev1.PersistentVolumeClaimVolumeSource{
×
1295
                                        ClaimName: pvc.Name,
×
1296
                                        ReadOnly:  false,
×
1297
                                },
×
1298
                        },
×
1299
                },
×
1300
        }
×
1301

×
1302
        if scratchPvc != nil {
×
1303
                volumes = append(volumes, corev1.Volume{
×
1304
                        Name: ScratchVolName,
×
1305
                        VolumeSource: corev1.VolumeSource{
×
1306
                                PersistentVolumeClaim: &corev1.PersistentVolumeClaimVolumeSource{
×
1307
                                        ClaimName: scratchPvc.Name,
×
1308
                                        ReadOnly:  false,
×
1309
                                },
×
1310
                        },
×
1311
                })
×
1312
        }
×
1313

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

×
1362
        ep, _ := GetEndpoint(pvc)
×
1363
        source := GetSource(pvc)
×
1364
        contentType := GetPVCContentType(pvc)
×
1365
        imageSize, _ := GetRequestedImageSize(pvc)
×
1366
        volumeMode := GetVolumeMode(pvc)
×
1367

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

×
1401
        if scratchPvc != nil {
1402
                pod.Spec.Containers[0].VolumeMounts = append(pod.Spec.Containers[0].VolumeMounts, corev1.VolumeMount{
×
1403
                        Name:      ScratchVolName,
×
1404
                        MountPath: common.ScratchDataDir,
×
1405
                })
×
1406
        }
×
1407

×
1408
        return pod
1409
}
×
1410

1411
// CreateStorageClassWithProvisioner creates CR of storage class with provisioner
1412
func CreateStorageClassWithProvisioner(name string, annotations, labels map[string]string, provisioner string) *storagev1.StorageClass {
1413
        return &storagev1.StorageClass{
×
1414
                Provisioner: provisioner,
×
1415
                ObjectMeta: metav1.ObjectMeta{
×
1416
                        Name:        name,
×
1417
                        Annotations: annotations,
×
1418
                        Labels:      labels,
×
1419
                },
×
1420
        }
×
1421
}
×
1422

×
1423
// CreateClient creates a fake client
1424
func CreateClient(objs ...runtime.Object) client.Client {
1425
        s := scheme.Scheme
1✔
1426
        _ = cdiv1.AddToScheme(s)
1✔
1427
        _ = corev1.AddToScheme(s)
1✔
1428
        _ = storagev1.AddToScheme(s)
1✔
1429
        _ = ocpconfigv1.Install(s)
1✔
1430

1✔
1431
        return fake.NewClientBuilder().WithScheme(s).WithRuntimeObjects(objs...).Build()
1✔
1432
}
1✔
1433

1✔
1434
// ErrQuotaExceeded checked is the error is of exceeded quota
1435
func ErrQuotaExceeded(err error) bool {
1436
        return strings.Contains(err.Error(), "exceeded quota:")
×
1437
}
×
1438

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

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

×
1460
        return GetContentType(cdiv1.DataVolumeContentType(contentType))
1461
}
×
1462

1463
// GetNamespace returns the given namespace if not empty, otherwise the default namespace
1464
func GetNamespace(namespace, defaultNamespace string) string {
1465
        if namespace == "" {
×
1466
                return defaultNamespace
×
1467
        }
×
1468
        return namespace
×
1469
}
×
1470

1471
// IsErrCacheNotStarted checked is the error is of cache not started
1472
func IsErrCacheNotStarted(err error) bool {
1473
        target := &runtimecache.ErrCacheNotStarted{}
×
1474
        return errors.As(err, &target)
×
1475
}
×
1476

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

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

×
1507
        if dv.Spec.Source.Snapshot != nil {
1508
                return "snapshot", dv.Spec.Source.Snapshot.Name, dv.Spec.Source.Snapshot.Namespace
×
1509
        }
×
1510

×
1511
        return "", "", ""
1512
}
×
1513

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

×
1524
        return pvcHonorWaitForFirstConsumer && globalHonorWaitForFirstConsumer, nil
1525
}
×
1526

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

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

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

×
1551
                // Merge the previous values into a 'resource.Quantity' struct
×
1552
                requiredSpace := util.GetRequiredSpace(fsOverheadFloat, imgSize)
×
1553
                returnSize = *resource.NewScaledQuantity(requiredSpace, 0)
×
1554
        } else {
×
1555
                // Inflation is not needed with 'Block' mode
×
1556
                returnSize = *resource.NewScaledQuantity(imgSize, 0)
×
1557
        }
×
1558

×
1559
        return returnSize, nil
1560
}
×
1561

1562
// IsBound returns if the pvc is bound
1563
func IsBound(pvc *corev1.PersistentVolumeClaim) bool {
1564
        return pvc != nil && pvc.Status.Phase == corev1.ClaimBound
×
1565
}
×
1566

×
1567
// IsUnbound returns if the pvc is not bound yet
1568
func IsUnbound(pvc *corev1.PersistentVolumeClaim) bool {
1569
        return !IsBound(pvc)
×
1570
}
×
1571

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

×
1577
// IsImageStream returns true if registry source is ImageStream
1578
func IsImageStream(pvc *corev1.PersistentVolumeClaim) bool {
1579
        return pvc.Annotations[AnnRegistryImageStream] == "true"
×
1580
}
×
1581

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

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

1618
// ErrConnectionRefused checks for connection refused errors
1619
func ErrConnectionRefused(err error) bool {
1620
        return strings.Contains(err.Error(), "connection refused")
×
1621
}
×
1622

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

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

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

×
1673
        // Parse the progress from the body
1674
        progressReport := ""
1675
        match := regExp.FindStringSubmatch(string(body))
×
1676
        if match != nil {
×
1677
                progressReport = match[len(match)-1]
×
1678
        }
×
1679
        return progressReport, nil
×
1680
}
×
1681

1682
// UpdateHTTPAnnotations updates the passed annotations for proper http import
1683
func UpdateHTTPAnnotations(annotations map[string]string, http *cdiv1.DataVolumeSourceHTTP) {
1684
        annotations[AnnEndpoint] = http.URL
×
1685
        annotations[AnnSource] = SourceHTTP
×
1686

×
1687
        if http.SecretRef != "" {
×
1688
                annotations[AnnSecret] = http.SecretRef
×
1689
        }
×
1690
        if http.CertConfigMap != "" {
×
1691
                annotations[AnnCertConfigMap] = http.CertConfigMap
×
1692
        }
×
1693
        for index, header := range http.ExtraHeaders {
×
1694
                annotations[fmt.Sprintf("%s.%d", AnnExtraHeaders, index)] = header
×
1695
        }
×
1696
        for index, header := range http.SecretExtraHeaders {
×
1697
                annotations[fmt.Sprintf("%s.%d", AnnSecretExtraHeaders, index)] = header
×
1698
        }
×
1699
}
×
1700

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

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

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

×
1748
        if registry.Platform != nil && registry.Platform.Architecture != "" {
1749
                annotations[AnnRegistryImageArchitecture] = registry.Platform.Architecture
×
1750
        }
×
1751
}
×
1752

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

1769
// UpdateImageIOAnnotations updates the passed annotations for proper imageIO import
1770
func UpdateImageIOAnnotations(annotations map[string]string, imageio *cdiv1.DataVolumeSourceImageIO) {
1771
        annotations[AnnEndpoint] = imageio.URL
×
1772
        annotations[AnnSource] = SourceImageio
×
1773
        annotations[AnnSecret] = imageio.SecretRef
×
1774
        annotations[AnnCertConfigMap] = imageio.CertConfigMap
×
1775
        annotations[AnnDiskID] = imageio.DiskID
×
1776
}
×
1777

×
1778
// IsPVBoundToPVC checks if a PV is bound to a specific PVC
1779
func IsPVBoundToPVC(pv *corev1.PersistentVolume, pvc *corev1.PersistentVolumeClaim) bool {
1780
        claimRef := pv.Spec.ClaimRef
1✔
1781
        return claimRef != nil && claimRef.Name == pvc.Name && claimRef.Namespace == pvc.Namespace && claimRef.UID == pvc.UID
1✔
1782
}
1✔
1783

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

1✔
1792
        if err := c.Get(ctx, client.ObjectKeyFromObject(pv), pv); err != nil {
1✔
1793
                return err
2✔
1794
        }
1✔
1795

1✔
1796
        // Examine the claimref for the PV and see if it's still bound to PVC'
1797
        if pv.Spec.ClaimRef == nil {
1798
                return fmt.Errorf("PV %s claimRef is nil", pv.Name)
1✔
1799
        }
×
1800

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

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

×
1823
        return nil
1824
}
1✔
1825

1826
// BulkDeleteResources deletes a bunch of resources
1827
func BulkDeleteResources(ctx context.Context, c client.Client, obj client.ObjectList, lo client.ListOption) error {
1828
        if err := c.List(ctx, obj, lo); err != nil {
×
1829
                if meta.IsNoMatchError(err) {
×
1830
                        return nil
×
1831
                }
×
1832
                return err
×
1833
        }
×
1834

1835
        sv := reflect.ValueOf(obj).Elem()
1836
        iv := sv.FieldByName("Items")
×
1837

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

1848
        return nil
1849
}
×
1850

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

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

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

×
1893
        targetStorageClass, err := GetStorageClassByNameWithK8sFallback(context.TODO(), client, targetPvcStorageClassName)
1894
        if err != nil {
×
1895
                return "", err
×
1896
        }
×
1897
        if targetStorageClass == nil {
×
1898
                logger.Info("Target PVC's Storage Class not found")
×
1899
                return "", nil
×
1900
        }
×
1901

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

1914
        logger.Info("Could not match snapshotter with storage class, falling back to host assisted clone")
1915
        return "", nil
×
1916
}
×
1917

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

×
1923
        logEvent := func(message, vscName string) {
×
1924
                logger.Info(message, "name", vscName)
×
1925
                if pvc != nil {
×
1926
                        msg := fmt.Sprintf("%s %s", message, vscName)
×
1927
                        recorder.Event(pvc, corev1.EventTypeNormal, VolumeSnapshotClassSelected, msg)
×
1928
                }
×
1929
        }
×
1930

1931
        if snapshotClassName != nil {
1932
                vsc := &snapshotv1.VolumeSnapshotClass{}
×
1933
                if err := c.Get(context.TODO(), types.NamespacedName{Name: *snapshotClassName}, vsc); err != nil {
×
1934
                        return nil, err
×
1935
                }
×
1936
                if vsc.Driver == driver {
×
1937
                        logEvent(MessageStorageProfileVolumeSnapshotClassSelected, vsc.Name)
×
1938
                        return snapshotClassName, nil
×
1939
                }
×
1940
                return nil, nil
×
1941
        }
×
1942

1943
        vscList := &snapshotv1.VolumeSnapshotClassList{}
1944
        if err := c.List(ctx, vscList); err != nil {
×
1945
                if meta.IsNoMatchError(err) {
×
1946
                        return nil, nil
×
1947
                }
×
1948
                return nil, err
×
1949
        }
×
1950

1951
        var candidates []string
1952
        for _, vsc := range vscList.Items {
×
1953
                if vsc.Driver == driver {
×
1954
                        if vsc.Annotations[AnnDefaultSnapshotClass] == "true" {
×
1955
                                logEvent(MessageDefaultVolumeSnapshotClassSelected, vsc.Name)
×
1956
                                vscName := vsc.Name
×
1957
                                return &vscName, nil
×
1958
                        }
×
1959
                        candidates = append(candidates, vsc.Name)
×
1960
                }
×
1961
        }
1962

1963
        if len(candidates) > 0 {
1964
                sort.Strings(candidates)
×
1965
                logEvent(MessageFirstVolumeSnapshotClassSelected, candidates[0])
×
1966
                return &candidates[0], nil
×
1967
        }
×
1968

×
1969
        return nil, nil
1970
}
×
1971

1972
// isCsiCrdsDeployed checks whether the CSI snapshotter CRD are deployed
1973
func isCsiCrdsDeployed(c client.Client, log logr.Logger) bool {
1974
        version := "v1"
×
1975
        vsClass := "volumesnapshotclasses." + snapshotv1.GroupName
×
1976
        vsContent := "volumesnapshotcontents." + snapshotv1.GroupName
×
1977
        vs := "volumesnapshots." + snapshotv1.GroupName
×
1978

×
1979
        return isCrdDeployed(c, vsClass, version, log) &&
×
1980
                isCrdDeployed(c, vsContent, version, log) &&
×
1981
                isCrdDeployed(c, vs, version, log)
×
1982
}
×
1983

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

1995
        for _, v := range crd.Spec.Versions {
1996
                if v.Name == version && v.Served {
×
1997
                        return true
×
1998
                }
×
1999
        }
×
2000

2001
        return false
2002
}
×
2003

2004
// IsSnapshotReady indicates if a volume snapshot is ready to be used
2005
func IsSnapshotReady(snapshot *snapshotv1.VolumeSnapshot) bool {
2006
        return snapshot.Status != nil && snapshot.Status.ReadyToUse != nil && *snapshot.Status.ReadyToUse
×
2007
}
×
2008

×
2009
// GetResource updates given obj with the data of the object with the same name and namespace
2010
func GetResource(ctx context.Context, c client.Client, namespace, name string, obj client.Object) (bool, error) {
2011
        obj.SetNamespace(namespace)
×
2012
        obj.SetName(name)
×
2013

×
2014
        err := c.Get(ctx, client.ObjectKeyFromObject(obj), obj)
×
2015
        if err != nil {
×
2016
                if k8serrors.IsNotFound(err) {
×
2017
                        return false, nil
×
2018
                }
×
2019

×
2020
                return false, err
2021
        }
×
2022

2023
        return true, nil
2024
}
×
2025

2026
// PatchArgs are the args for Patch
2027
type PatchArgs struct {
2028
        Client client.Client
2029
        Log    logr.Logger
2030
        Obj    client.Object
2031
        OldObj client.Object
2032
}
2033

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

2063
// OwnedByDataVolume returns true if the object is owned by a DataVolume
2064
func OwnedByDataVolume(obj metav1.Object) bool {
2065
        owner := metav1.GetControllerOf(obj)
×
2066
        return owner != nil && owner.Kind == "DataVolume"
×
2067
}
×
2068

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

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

2094
// ClaimMayExistBeforeDataVolume returns true if the PVC may exist before the DataVolume
2095
func ClaimMayExistBeforeDataVolume(c client.Client, pvc *corev1.PersistentVolumeClaim, dv *cdiv1.DataVolume) (bool, error) {
2096
        if ClaimIsPopulatedForDataVolume(pvc, dv) {
×
2097
                return true, nil
×
2098
        }
×
2099
        return AllowClaimAdoption(c, pvc, dv)
×
2100
}
×
2101

2102
// ClaimIsPopulatedForDataVolume returns true if the PVC is populated for the given DataVolume
2103
func ClaimIsPopulatedForDataVolume(pvc *corev1.PersistentVolumeClaim, dv *cdiv1.DataVolume) bool {
2104
        return pvc != nil && dv != nil && pvc.Annotations[AnnPopulatedFor] == dv.Name
×
2105
}
×
2106

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

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

×
2133
        ref := dataSource.Spec.Source.DataSource
2134
        refNs := GetNamespace(ref.Namespace, dataSource.Namespace)
×
2135
        if ref.Name == dataSource.Name && refNs == dataSource.Namespace {
×
2136
                return nil, ErrDataSourceSelfReference
×
2137
        }
×
2138
        resolved := &cdiv1.DataSource{}
×
2139
        if err := client.Get(ctx, types.NamespacedName{Name: ref.Name, Namespace: refNs}, resolved); err != nil {
×
2140
                return nil, err
×
2141
        }
×
2142

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

×
2147
        return resolved, nil
2148
}
×
2149

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

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

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

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

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

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

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

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

×
2198
                return nil
×
2199
        }
×
2200

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

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

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

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

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

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

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

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

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

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

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

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