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

kubevirt / containerized-data-importer / #4711

03 Jun 2024 08:39PM UTC coverage: 59.016% (+0.1%) from 58.918%
#4711

push

travis-ci

web-flow
Make upload client/server certs configurable (#3252)

* Add client cert config to CDI resource

Signed-off-by: Michael Henriksen <mhenriks@redhat.com>

* make client certs configurable

Signed-off-by: Michael Henriksen <mhenriks@redhat.com>

* Create uploadserver.Config

Signed-off-by: Michael Henriksen <mhenriks@redhat.com>

* uploadserver should read certs from files

Signed-off-by: Michael Henriksen <mhenriks@redhat.com>

* make sure to not close doneChan when error occurs

generally tighten up handling of "done" "uploading" and "processing"

Signed-off-by: Michael Henriksen <mhenriks@redhat.com>

* add deadline support to uploadserver

Signed-off-by: Michael Henriksen <mhenriks@redhat.com>

* Add deadline support to upload controller

Signed-off-by: Michael Henriksen <mhenriks@redhat.com>

* clone controller should use configured client cert duration

Signed-off-by: Michael Henriksen <mhenriks@redhat.com>

* make lint check happy

Signed-off-by: Michael Henriksen <mhenriks@redhat.com>

* Extend existing func test to validate client certs configurable and will be rotated

Signed-off-by: Michael Henriksen <mhenriks@redhat.com>

* Use deadline/rotation for clone pods as well

Forgot about the case where a source PVC may be in use.  Bay be a big delay from when target pod is created and source.

Signed-off-by: Michael Henriksen <mhenriks@redhat.com>

---------

Signed-off-by: Michael Henriksen <mhenriks@redhat.com>

283 of 371 new or added lines in 12 files covered. (76.28%)

9 existing lines in 4 files now uncovered.

16269 of 27567 relevant lines covered (59.02%)

0.65 hits per line

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

86.29
/pkg/controller/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
limitations under the License.
14
See the License for the specific language governing permissions and
15
*/
16

17
package controller
18

19
import (
20
        "context"
21
        "crypto/rsa"
22
        "encoding/json"
23
        "fmt"
24
        "strconv"
25
        "strings"
26

27
        "github.com/go-logr/logr"
28
        "github.com/pkg/errors"
29

30
        corev1 "k8s.io/api/core/v1"
31
        k8serrors "k8s.io/apimachinery/pkg/api/errors"
32
        metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
33
        "k8s.io/apimachinery/pkg/types"
34
        "k8s.io/client-go/tools/record"
35
        "k8s.io/klog/v2"
36

37
        "sigs.k8s.io/controller-runtime/pkg/client"
38

39
        cdiv1 "kubevirt.io/containerized-data-importer-api/pkg/apis/core/v1beta1"
40
        "kubevirt.io/containerized-data-importer/pkg/common"
41
        cc "kubevirt.io/containerized-data-importer/pkg/controller/common"
42
        "kubevirt.io/containerized-data-importer/pkg/util"
43
        "kubevirt.io/containerized-data-importer/pkg/util/cert"
44
)
45

46
const (
47
        // CertVolName is the name of the volume containing certs
48
        CertVolName = "cdi-cert-vol"
49

50
        // SecretVolName is the name of the volume containing gcs key
51
        //nolint:gosec // This is not a real secret
52
        SecretVolName = "cdi-secret-vol"
53

54
        // AnnOwnerRef is used when owner is in a different namespace
55
        AnnOwnerRef = cc.AnnAPIGroup + "/storage.ownerRef"
56

57
        // PodRunningReason is const that defines the pod was started as a reason
58
        PodRunningReason = "Pod is running"
59

60
        // ScratchSpaceRequiredReason is a const that defines the pod exited due to a lack of scratch space
61
        ScratchSpaceRequiredReason = "Scratch space required"
62

63
        // ImagePullFailedReason is a const that defines the pod exited due to failure when pulling image
64
        ImagePullFailedReason = "ImagePullFailed"
65

66
        // ImportCompleteMessage is a const that defines the pod completeded the import successfully
67
        ImportCompleteMessage = "Import Complete"
68

69
        // ProxyCertVolName is the name of the volumecontaining certs
70
        ProxyCertVolName = "cdi-proxy-cert-vol"
71
        // ClusterWideProxyAPIGroup is the APIGroup for OpenShift Cluster Wide Proxy
72
        ClusterWideProxyAPIGroup = "config.openshift.io"
73
        // ClusterWideProxyAPIKind is the APIKind for OpenShift Cluster Wide Proxy
74
        ClusterWideProxyAPIKind = "Proxy"
75
        // ClusterWideProxyAPIVersion is the APIVersion for OpenShift Cluster Wide Proxy
76
        ClusterWideProxyAPIVersion = "v1"
77
        // ClusterWideProxyName is the OpenShift Cluster Wide Proxy object name. There is only one obj in the cluster.
78
        ClusterWideProxyName = "cluster"
79
        // ClusterWideProxyConfigMapName is the OpenShift Cluster Wide Proxy ConfigMap name for CA certificates.
80
        ClusterWideProxyConfigMapName = "user-ca-bundle"
81
        // ClusterWideProxyConfigMapNameSpace is the OpenShift Cluster Wide Proxy ConfigMap namespace for CA certificates.
82
        ClusterWideProxyConfigMapNameSpace = "openshift-config"
83
        // ClusterWideProxyConfigMapKey is the OpenShift Cluster Wide Proxy ConfigMap key name for CA certificates.
84
        ClusterWideProxyConfigMapKey = "ca-bundle.crt"
85
)
86

87
func checkPVC(pvc *corev1.PersistentVolumeClaim, annotation string, log logr.Logger) bool {
1✔
88
        // check if we have proper annotation
1✔
89
        if !metav1.HasAnnotation(pvc.ObjectMeta, annotation) {
2✔
90
                log.V(1).Info("PVC annotation not found, skipping pvc", "annotation", annotation)
1✔
91
                return false
1✔
92
        }
1✔
93

94
        return true
1✔
95
}
96

97
// - when the SkipWFFCVolumesEnabled is true, the CDI controller will only handle BOUND the PVC
98
// - when the SkipWFFCVolumesEnabled is false, the CDI controller will can handle it - it will create worker pods for the PVC (this will bind it)
99
func shouldHandlePvc(pvc *corev1.PersistentVolumeClaim, honorWaitForFirstConsumerEnabled bool, log logr.Logger) bool {
1✔
100
        if honorWaitForFirstConsumerEnabled {
2✔
101
                return isBound(pvc, log)
1✔
102
        }
1✔
103
        return true
1✔
104
}
105

106
func isBound(pvc *corev1.PersistentVolumeClaim, log logr.Logger) bool {
1✔
107
        if pvc.Status.Phase != corev1.ClaimBound {
2✔
108
                log.V(1).Info("PVC not bound, skipping pvc", "Phase", pvc.Status.Phase)
1✔
109
                return false
1✔
110
        }
1✔
111

112
        return true
1✔
113
}
114

115
// checks if particular label exists in pvc
116
func checkIfLabelExists(pvc *corev1.PersistentVolumeClaim, lbl string, val string) bool {
1✔
117
        value, exists := pvc.ObjectMeta.Labels[lbl]
1✔
118
        if exists && value == val {
2✔
119
                return true
1✔
120
        }
1✔
121
        return false
1✔
122
}
123

124
// newScratchPersistentVolumeClaimSpec creates a new PVC based on the size of the passed in PVC.
125
// It also sets the appropriate OwnerReferences on the resource
126
// which allows handleObject to discover the pod resource that 'owns' it, and clean up when needed.
127
func newScratchPersistentVolumeClaimSpec(pvc *corev1.PersistentVolumeClaim, pod *corev1.Pod, name, storageClassName string) *corev1.PersistentVolumeClaim {
1✔
128
        labels := map[string]string{
1✔
129
                "app": "containerized-data-importer",
1✔
130
        }
1✔
131

1✔
132
        annotations := make(map[string]string)
1✔
133
        // Copy kubevirt.io annotations, but NOT the CDI annotations as those will trigger another import/upload/clone on the scratchspace
1✔
134
        // pvc.
1✔
135
        if len(pvc.GetAnnotations()) > 0 {
2✔
136
                for k, v := range pvc.GetAnnotations() {
2✔
137
                        if strings.Contains(k, common.KubeVirtAnnKey) && !strings.Contains(k, common.CDIAnnKey) {
1✔
138
                                annotations[k] = v
×
139
                        }
×
140
                }
141
        }
142
        // When the original PVC is being handled by a populator, copy AnnSelectedNode to avoid issues with k8s scheduler
143
        _, isPopulator := pvc.Annotations[cc.AnnPopulatorKind]
1✔
144
        selectedNode := pvc.Annotations[cc.AnnSelectedNode]
1✔
145
        if isPopulator && selectedNode != "" {
1✔
146
                annotations[cc.AnnSelectedNode] = selectedNode
×
147
        }
×
148

149
        pvcDef := &corev1.PersistentVolumeClaim{
1✔
150
                ObjectMeta: metav1.ObjectMeta{
1✔
151
                        Name:        name,
1✔
152
                        Namespace:   pvc.Namespace,
1✔
153
                        Labels:      labels,
1✔
154
                        Annotations: annotations,
1✔
155
                        OwnerReferences: []metav1.OwnerReference{
1✔
156
                                MakePodOwnerReference(pod),
1✔
157
                        },
1✔
158
                },
1✔
159
                Spec: corev1.PersistentVolumeClaimSpec{
1✔
160
                        AccessModes: []corev1.PersistentVolumeAccessMode{"ReadWriteOnce"},
1✔
161
                        Resources:   pvc.Spec.Resources,
1✔
162
                },
1✔
163
        }
1✔
164
        if storageClassName != "" {
2✔
165
                pvcDef.Spec.StorageClassName = &storageClassName
1✔
166
        }
1✔
167
        return pvcDef
1✔
168
}
169

170
// createScratchPersistentVolumeClaim creates and returns a pointer to a scratch PVC which is created based on the passed-in pvc and storage class name.
171
func createScratchPersistentVolumeClaim(client client.Client, pvc *corev1.PersistentVolumeClaim, pod *corev1.Pod, name, storageClassName string, installerLabels map[string]string, recorder record.EventRecorder) (*corev1.PersistentVolumeClaim, error) {
1✔
172
        scratchPvcSpec := newScratchPersistentVolumeClaimSpec(pvc, pod, name, storageClassName)
1✔
173
        util.SetRecommendedLabels(scratchPvcSpec, installerLabels, "cdi-controller")
1✔
174
        if err := client.Create(context.TODO(), scratchPvcSpec); err != nil {
1✔
175
                if cc.ErrQuotaExceeded(err) {
×
NEW
176
                        recorder.Event(pvc, corev1.EventTypeWarning, cc.ErrExceededQuota, err.Error())
×
177
                }
×
178
                if !k8serrors.IsAlreadyExists(err) {
×
179
                        return nil, errors.Wrap(err, "scratch PVC API create errored")
×
180
                }
×
181
        }
182
        scratchPvc := &corev1.PersistentVolumeClaim{}
1✔
183
        if err := client.Get(context.TODO(), types.NamespacedName{Name: scratchPvcSpec.Name, Namespace: pvc.Namespace}, scratchPvc); err != nil {
1✔
184
                klog.Errorf("Unable to get scratch space pvc, %v\n", err)
×
185
                return nil, err
×
186
        }
×
187
        klog.V(3).Infof("scratch PVC \"%s/%s\" created\n", scratchPvc.Namespace, scratchPvc.Name)
1✔
188
        return scratchPvc, nil
1✔
189
}
190

191
// GetFilesystemOverhead determines the filesystem overhead defined in CDIConfig for this PVC's volumeMode and storageClass.
192
func GetFilesystemOverhead(ctx context.Context, client client.Client, pvc *corev1.PersistentVolumeClaim) (cdiv1.Percent, error) {
1✔
193
        if cc.GetVolumeMode(pvc) != corev1.PersistentVolumeFilesystem {
1✔
194
                return "0", nil
×
195
        }
×
196

197
        return cc.GetFilesystemOverheadForStorageClass(ctx, client, pvc.Spec.StorageClassName)
1✔
198
}
199

200
// GetScratchPvcStorageClass tries to determine which storage class to use for use with a scratch persistent
201
// volume claim. The order of preference is the following:
202
// 1. Defined value in CDI Config field scratchSpaceStorageClass.
203
// 2. If 1 is not available, use the storage class name of the original pvc that will own the scratch pvc.
204
// 3. If none of those are available, return blank.
205
func GetScratchPvcStorageClass(client client.Client, pvc *corev1.PersistentVolumeClaim) string {
1✔
206
        config := &cdiv1.CDIConfig{}
1✔
207
        if err := client.Get(context.TODO(), types.NamespacedName{Name: common.ConfigName}, config); err != nil {
2✔
208
                return ""
1✔
209
        }
1✔
210
        storageClassName := config.Status.ScratchSpaceStorageClass
1✔
211
        if storageClassName == "" {
2✔
212
                // Unable to determine scratch storage class, attempt to read the storage class from the pvc.
1✔
213
                if pvc.Spec.StorageClassName != nil {
2✔
214
                        storageClassName = *pvc.Spec.StorageClassName
1✔
215
                        if storageClassName != "" {
2✔
216
                                return storageClassName
1✔
217
                        }
1✔
218
                }
219
        } else {
1✔
220
                return storageClassName
1✔
221
        }
1✔
222
        return ""
1✔
223
}
224

225
// DecodePublicKey turns a bunch of bytes into a public key
226
func DecodePublicKey(keyBytes []byte) (*rsa.PublicKey, error) {
1✔
227
        keys, err := cert.ParsePublicKeysPEM(keyBytes)
1✔
228
        if err != nil {
2✔
229
                return nil, err
1✔
230
        }
1✔
231

232
        if len(keys) != 1 {
1✔
233
                return nil, errors.New("unexected number of pulic keys")
×
234
        }
×
235

236
        key, ok := keys[0].(*rsa.PublicKey)
1✔
237
        if !ok {
1✔
238
                return nil, errors.New("PEM does not contain RSA key")
×
239
        }
×
240

241
        return key, nil
1✔
242
}
243

244
// MakePVCOwnerReference makes owner reference from a PVC
245
func MakePVCOwnerReference(pvc *corev1.PersistentVolumeClaim) metav1.OwnerReference {
1✔
246
        blockOwnerDeletion := true
1✔
247
        isController := true
1✔
248
        return metav1.OwnerReference{
1✔
249
                APIVersion:         "v1",
1✔
250
                Kind:               "PersistentVolumeClaim",
1✔
251
                Name:               pvc.Name,
1✔
252
                UID:                pvc.GetUID(),
1✔
253
                BlockOwnerDeletion: &blockOwnerDeletion,
1✔
254
                Controller:         &isController,
1✔
255
        }
1✔
256
}
1✔
257

258
// MakePodOwnerReference makes owner reference from a Pod
259
func MakePodOwnerReference(pod *corev1.Pod) metav1.OwnerReference {
1✔
260
        blockOwnerDeletion := true
1✔
261
        isController := true
1✔
262
        return metav1.OwnerReference{
1✔
263
                APIVersion:         "v1",
1✔
264
                Kind:               "Pod",
1✔
265
                Name:               pod.Name,
1✔
266
                UID:                pod.GetUID(),
1✔
267
                BlockOwnerDeletion: &blockOwnerDeletion,
1✔
268
                Controller:         &isController,
1✔
269
        }
1✔
270
}
1✔
271

272
func podPhaseFromPVC(pvc *corev1.PersistentVolumeClaim) corev1.PodPhase {
1✔
273
        phase := pvc.ObjectMeta.Annotations[cc.AnnPodPhase]
1✔
274
        return corev1.PodPhase(phase)
1✔
275
}
1✔
276

277
func podSucceededFromPVC(pvc *corev1.PersistentVolumeClaim) bool {
1✔
278
        return podPhaseFromPVC(pvc) == corev1.PodSucceeded
1✔
279
}
1✔
280

281
func setAnnotationsFromPodWithPrefix(anno map[string]string, pod *corev1.Pod, termMsg *common.TerminationMessage, prefix string) {
1✔
282
        if pod == nil || pod.Status.ContainerStatuses == nil {
2✔
283
                return
1✔
284
        }
1✔
285
        annPodRestarts, _ := strconv.Atoi(anno[cc.AnnPodRestarts])
1✔
286
        podRestarts := int(pod.Status.ContainerStatuses[0].RestartCount)
1✔
287
        if podRestarts >= annPodRestarts {
2✔
288
                anno[cc.AnnPodRestarts] = strconv.Itoa(podRestarts)
1✔
289
        }
1✔
290

291
        containerState := pod.Status.ContainerStatuses[0].State
1✔
292
        if containerState.Running != nil {
2✔
293
                anno[prefix] = "true"
1✔
294
                anno[prefix+".message"] = ""
1✔
295
                anno[prefix+".reason"] = PodRunningReason
1✔
296
                return
1✔
297
        }
1✔
298

299
        anno[cc.AnnRunningCondition] = "false"
1✔
300

1✔
301
        for _, status := range pod.Status.ContainerStatuses {
2✔
302
                if status.Started != nil && !(*status.Started) {
1✔
303
                        if status.State.Waiting != nil &&
×
304
                                (status.State.Waiting.Reason == "ImagePullBackOff" || status.State.Waiting.Reason == "ErrImagePull") {
×
305
                                anno[prefix+".message"] = fmt.Sprintf("%s: %s", common.ImagePullFailureText, status.Image)
×
306
                                anno[prefix+".reason"] = ImagePullFailedReason
×
307
                                return
×
308
                        }
×
309
                }
310
        }
311

312
        if containerState.Waiting != nil && containerState.Waiting.Reason != "CrashLoopBackOff" {
2✔
313
                anno[prefix+".message"] = simplifyKnownMessage(containerState.Waiting.Message)
1✔
314
                anno[prefix+".reason"] = containerState.Waiting.Reason
1✔
315
                return
1✔
316
        }
1✔
317

318
        if containerState.Terminated != nil {
2✔
319
                if termMsg != nil {
2✔
320
                        if termMsg.ScratchSpaceRequired != nil && *termMsg.ScratchSpaceRequired {
2✔
321
                                anno[cc.AnnRequiresScratch] = "true"
1✔
322
                                anno[prefix+".message"] = common.ScratchSpaceRequired
1✔
323
                                anno[prefix+".reason"] = ScratchSpaceRequiredReason
1✔
324
                                return
1✔
325
                        }
1✔
326
                        // Handle extended termination message
327
                        if termMsg.Message != nil {
2✔
328
                                anno[prefix+".message"] = *termMsg.Message
1✔
329
                        }
1✔
330
                        if termMsg.VddkInfo != nil {
2✔
331
                                if termMsg.VddkInfo.Host != "" {
2✔
332
                                        anno[cc.AnnVddkHostConnection] = termMsg.VddkInfo.Host
1✔
333
                                }
1✔
334
                                if termMsg.VddkInfo.Version != "" {
2✔
335
                                        anno[cc.AnnVddkVersion] = termMsg.VddkInfo.Version
1✔
336
                                }
1✔
337
                        }
338
                        if termMsg.PreallocationApplied != nil && *termMsg.PreallocationApplied {
2✔
339
                                anno[cc.AnnPreallocationApplied] = "true"
1✔
340
                        }
1✔
341
                } else {
1✔
342
                        // Handle plain termination message (legacy)
1✔
343
                        anno[prefix+".message"] = simplifyKnownMessage(containerState.Terminated.Message)
1✔
344
                        if strings.Contains(containerState.Terminated.Message, common.PreallocationApplied) {
1✔
345
                                anno[cc.AnnPreallocationApplied] = "true"
×
346
                        }
×
347

348
                        if strings.Contains(containerState.Terminated.Message, common.ImagePullFailureText) {
2✔
349
                                anno[prefix+".reason"] = ImagePullFailedReason
1✔
350
                                return
1✔
351
                        }
1✔
352
                }
353
                anno[prefix+".reason"] = containerState.Terminated.Reason
1✔
354
        }
355
}
356

357
func addLabelsFromTerminationMessage(labels map[string]string, termMsg *common.TerminationMessage) map[string]string {
1✔
358
        newLabels := make(map[string]string, 0)
1✔
359
        for k, v := range labels {
2✔
360
                newLabels[k] = v
1✔
361
        }
1✔
362
        if termMsg != nil {
2✔
363
                for k, v := range termMsg.Labels {
2✔
364
                        if _, found := newLabels[k]; !found {
2✔
365
                                newLabels[k] = v
1✔
366
                        }
1✔
367
                }
368
        }
369
        return newLabels
1✔
370
}
371

372
func simplifyKnownMessage(msg string) string {
1✔
373
        if strings.Contains(msg, "is larger than the reported available") ||
1✔
374
                strings.Contains(msg, "no space left on device") ||
1✔
375
                strings.Contains(msg, "file largest block is bigger than maxblock") {
1✔
376
                return "DataVolume too small to contain image"
×
377
        }
×
378

379
        return msg
1✔
380
}
381

382
func parseTerminationMessage(pod *corev1.Pod) (*common.TerminationMessage, error) {
1✔
383
        if pod == nil || pod.Status.ContainerStatuses == nil {
2✔
384
                return nil, nil
1✔
385
        }
1✔
386

387
        state := pod.Status.ContainerStatuses[0].State
1✔
388
        if state.Terminated == nil || state.Terminated.ExitCode != 0 {
2✔
389
                return nil, nil
1✔
390
        }
1✔
391

392
        termMsg := &common.TerminationMessage{}
1✔
393
        if err := json.Unmarshal([]byte(state.Terminated.Message), termMsg); err != nil {
2✔
394
                return nil, err
1✔
395
        }
1✔
396

397
        return termMsg, nil
1✔
398
}
399

400
func setBoundConditionFromPVC(anno map[string]string, prefix string, pvc *corev1.PersistentVolumeClaim) {
1✔
401
        switch pvc.Status.Phase {
1✔
402
        case corev1.ClaimBound:
1✔
403
                anno[prefix] = "true"
1✔
404
                anno[prefix+".message"] = ""
1✔
405
                anno[prefix+".reason"] = ""
1✔
NEW
406
        case corev1.ClaimPending:
×
407
                anno[prefix] = "false"
×
408
                anno[prefix+".message"] = "Claim Pending"
×
409
                anno[prefix+".reason"] = "Claim Pending"
×
NEW
410
        case corev1.ClaimLost:
×
411
                anno[prefix] = "false"
×
412
                anno[prefix+".message"] = cc.ClaimLost
×
413
                anno[prefix+".reason"] = cc.ClaimLost
×
414
        default:
×
415
                anno[prefix] = "false"
×
416
                anno[prefix+".message"] = "Unknown"
×
417
                anno[prefix+".reason"] = "Unknown"
×
418
        }
419
}
420

421
func getScratchNameFromPod(pod *corev1.Pod) (string, bool) {
1✔
422
        for _, vol := range pod.Spec.Volumes {
2✔
423
                if vol.Name == cc.ScratchVolName {
2✔
424
                        return vol.PersistentVolumeClaim.ClaimName, true
1✔
425
                }
1✔
426
        }
427

428
        return "", false
1✔
429
}
430

431
func podUsingPVC(pvc *corev1.PersistentVolumeClaim, readOnly bool) *corev1.Pod {
1✔
432
        return &corev1.Pod{
1✔
433
                ObjectMeta: metav1.ObjectMeta{
1✔
434
                        Namespace: pvc.Namespace,
1✔
435
                        Name:      pvc.Name + "-pod",
1✔
436
                },
1✔
437
                Spec: corev1.PodSpec{
1✔
438
                        Containers: []corev1.Container{
1✔
439
                                {
1✔
440
                                        VolumeMounts: []corev1.VolumeMount{
1✔
441
                                                {
1✔
442
                                                        Name:     "v1",
1✔
443
                                                        ReadOnly: readOnly,
1✔
444
                                                },
1✔
445
                                        },
1✔
446
                                },
1✔
447
                        },
1✔
448
                        Volumes: []corev1.Volume{
1✔
449
                                {
1✔
450
                                        Name: "v1",
1✔
451
                                        VolumeSource: corev1.VolumeSource{
1✔
452
                                                PersistentVolumeClaim: &corev1.PersistentVolumeClaimVolumeSource{
1✔
453
                                                        ClaimName: pvc.Name,
1✔
454
                                                        ReadOnly:  readOnly,
1✔
455
                                                },
1✔
456
                                        },
1✔
457
                                },
1✔
458
                        },
1✔
459
                },
1✔
460
        }
1✔
461
}
1✔
462

463
func createBlockPvc(name, ns string, annotations, labels map[string]string) *corev1.PersistentVolumeClaim {
1✔
464
        pvcDef := cc.CreatePvcInStorageClass(name, ns, nil, annotations, labels, corev1.ClaimBound)
1✔
465
        volumeMode := corev1.PersistentVolumeBlock
1✔
466
        pvcDef.Spec.VolumeMode = &volumeMode
1✔
467
        return pvcDef
1✔
468
}
1✔
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