1
16
17 package storage
18
19 import (
20 "context"
21 "fmt"
22 "path"
23
24 v1 "k8s.io/api/core/v1"
25 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
26 "k8s.io/apimachinery/pkg/util/uuid"
27 "k8s.io/kubernetes/test/e2e/framework"
28 e2epod "k8s.io/kubernetes/test/e2e/framework/pod"
29 e2epodoutput "k8s.io/kubernetes/test/e2e/framework/pod/output"
30 e2eskipper "k8s.io/kubernetes/test/e2e/framework/skipper"
31 "k8s.io/kubernetes/test/e2e/nodefeature"
32 imageutils "k8s.io/kubernetes/test/utils/image"
33 admissionapi "k8s.io/pod-security-admission/api"
34
35 "github.com/onsi/ginkgo/v2"
36 "github.com/onsi/gomega"
37 )
38
39 var _ = SIGDescribe("Projected configMap", func() {
40 f := framework.NewDefaultFramework("projected")
41 f.NamespacePodSecurityLevel = admissionapi.LevelBaseline
42
43
48 framework.ConformanceIt("should be consumable from pods in volume", f.WithNodeConformance(), func(ctx context.Context) {
49 doProjectedConfigMapE2EWithoutMappings(ctx, f, false, 0, nil)
50 })
51
52
58 framework.ConformanceIt("should be consumable from pods in volume with defaultMode set [LinuxOnly]", f.WithNodeConformance(), func(ctx context.Context) {
59 defaultMode := int32(0400)
60 doProjectedConfigMapE2EWithoutMappings(ctx, f, false, 0, &defaultMode)
61 })
62
63 f.It("should be consumable from pods in volume as non-root with defaultMode and fsGroup set [LinuxOnly]", nodefeature.FSGroup, func(ctx context.Context) {
64
65 e2eskipper.SkipIfNodeOSDistroIs("windows")
66 defaultMode := int32(0440)
67 doProjectedConfigMapE2EWithoutMappings(ctx, f, true, 1001, &defaultMode)
68 })
69
70
75 framework.ConformanceIt("should be consumable from pods in volume as non-root", f.WithNodeConformance(), func(ctx context.Context) {
76 doProjectedConfigMapE2EWithoutMappings(ctx, f, true, 0, nil)
77 })
78
79 f.It("should be consumable from pods in volume as non-root with FSGroup [LinuxOnly]", nodefeature.FSGroup, func(ctx context.Context) {
80
81 e2eskipper.SkipIfNodeOSDistroIs("windows")
82 doProjectedConfigMapE2EWithoutMappings(ctx, f, true, 1001, nil)
83 })
84
85
90 framework.ConformanceIt("should be consumable from pods in volume with mappings", f.WithNodeConformance(), func(ctx context.Context) {
91 doProjectedConfigMapE2EWithMappings(ctx, f, false, 0, nil)
92 })
93
94
100 framework.ConformanceIt("should be consumable from pods in volume with mappings and Item mode set [LinuxOnly]", f.WithNodeConformance(), func(ctx context.Context) {
101 mode := int32(0400)
102 doProjectedConfigMapE2EWithMappings(ctx, f, false, 0, &mode)
103 })
104
105
110 framework.ConformanceIt("should be consumable from pods in volume with mappings as non-root", f.WithNodeConformance(), func(ctx context.Context) {
111 doProjectedConfigMapE2EWithMappings(ctx, f, true, 0, nil)
112 })
113
114 f.It("should be consumable from pods in volume with mappings as non-root with FSGroup [LinuxOnly]", nodefeature.FSGroup, func(ctx context.Context) {
115
116 e2eskipper.SkipIfNodeOSDistroIs("windows")
117 doProjectedConfigMapE2EWithMappings(ctx, f, true, 1001, nil)
118 })
119
120
125 framework.ConformanceIt("updates should be reflected in volume", f.WithNodeConformance(), func(ctx context.Context) {
126 podLogTimeout := e2epod.GetPodSecretUpdateTimeout(ctx, f.ClientSet)
127 containerTimeoutArg := fmt.Sprintf("--retry_time=%v", int(podLogTimeout.Seconds()))
128
129 name := "projected-configmap-test-upd-" + string(uuid.NewUUID())
130 volumeName := "projected-configmap-volume"
131 volumeMountPath := "/etc/projected-configmap-volume"
132 configMap := &v1.ConfigMap{
133 ObjectMeta: metav1.ObjectMeta{
134 Namespace: f.Namespace.Name,
135 Name: name,
136 },
137 Data: map[string]string{
138 "data-1": "value-1",
139 },
140 }
141
142 ginkgo.By(fmt.Sprintf("Creating projection with configMap that has name %s", configMap.Name))
143 var err error
144 if configMap, err = f.ClientSet.CoreV1().ConfigMaps(f.Namespace.Name).Create(ctx, configMap, metav1.CreateOptions{}); err != nil {
145 framework.Failf("unable to create test configMap %s: %v", configMap.Name, err)
146 }
147
148 pod := createProjectedConfigMapMounttestPod(f.Namespace.Name, volumeName, name, volumeMountPath,
149 "--break_on_expected_content=false", containerTimeoutArg, "--file_content_in_loop=/etc/projected-configmap-volume/data-1")
150
151 ginkgo.By("Creating the pod")
152 e2epod.NewPodClient(f).CreateSync(ctx, pod)
153
154 pollLogs := func() (string, error) {
155 return e2epod.GetPodLogs(ctx, f.ClientSet, f.Namespace.Name, pod.Name, pod.Spec.Containers[0].Name)
156 }
157
158 gomega.Eventually(ctx, pollLogs, podLogTimeout, framework.Poll).Should(gomega.ContainSubstring("value-1"))
159
160 ginkgo.By(fmt.Sprintf("Updating configmap %v", configMap.Name))
161 configMap.ResourceVersion = ""
162 configMap.Data["data-1"] = "value-2"
163 _, err = f.ClientSet.CoreV1().ConfigMaps(f.Namespace.Name).Update(ctx, configMap, metav1.UpdateOptions{})
164 framework.ExpectNoError(err, "Failed to update configmap %q in namespace %q", configMap.Name, f.Namespace.Name)
165
166 ginkgo.By("waiting to observe update in volume")
167 gomega.Eventually(ctx, pollLogs, podLogTimeout, framework.Poll).Should(gomega.ContainSubstring("value-2"))
168 })
169
170
175 framework.ConformanceIt("optional updates should be reflected in volume", f.WithNodeConformance(), func(ctx context.Context) {
176 podLogTimeout := e2epod.GetPodSecretUpdateTimeout(ctx, f.ClientSet)
177 containerTimeoutArg := fmt.Sprintf("--retry_time=%v", int(podLogTimeout.Seconds()))
178 trueVal := true
179 volumeMountPath := "/etc/projected-configmap-volumes"
180
181 deleteName := "cm-test-opt-del-" + string(uuid.NewUUID())
182 deleteContainerName := "delcm-volume-test"
183 deleteVolumeName := "deletecm-volume"
184 deleteConfigMap := &v1.ConfigMap{
185 ObjectMeta: metav1.ObjectMeta{
186 Namespace: f.Namespace.Name,
187 Name: deleteName,
188 },
189 Data: map[string]string{
190 "data-1": "value-1",
191 },
192 }
193
194 updateName := "cm-test-opt-upd-" + string(uuid.NewUUID())
195 updateContainerName := "updcm-volume-test"
196 updateVolumeName := "updatecm-volume"
197 updateConfigMap := &v1.ConfigMap{
198 ObjectMeta: metav1.ObjectMeta{
199 Namespace: f.Namespace.Name,
200 Name: updateName,
201 },
202 Data: map[string]string{
203 "data-1": "value-1",
204 },
205 }
206
207 createName := "cm-test-opt-create-" + string(uuid.NewUUID())
208 createContainerName := "createcm-volume-test"
209 createVolumeName := "createcm-volume"
210 createConfigMap := &v1.ConfigMap{
211 ObjectMeta: metav1.ObjectMeta{
212 Namespace: f.Namespace.Name,
213 Name: createName,
214 },
215 Data: map[string]string{
216 "data-1": "value-1",
217 },
218 }
219
220 ginkgo.By(fmt.Sprintf("Creating configMap with name %s", deleteConfigMap.Name))
221 var err error
222 if deleteConfigMap, err = f.ClientSet.CoreV1().ConfigMaps(f.Namespace.Name).Create(ctx, deleteConfigMap, metav1.CreateOptions{}); err != nil {
223 framework.Failf("unable to create test configMap %s: %v", deleteConfigMap.Name, err)
224 }
225
226 ginkgo.By(fmt.Sprintf("Creating configMap with name %s", updateConfigMap.Name))
227 if updateConfigMap, err = f.ClientSet.CoreV1().ConfigMaps(f.Namespace.Name).Create(ctx, updateConfigMap, metav1.CreateOptions{}); err != nil {
228 framework.Failf("unable to create test configMap %s: %v", updateConfigMap.Name, err)
229 }
230
231 pod := &v1.Pod{
232 ObjectMeta: metav1.ObjectMeta{
233 Name: "pod-projected-configmaps-" + string(uuid.NewUUID()),
234 },
235 Spec: v1.PodSpec{
236 Volumes: []v1.Volume{
237 {
238 Name: deleteVolumeName,
239 VolumeSource: v1.VolumeSource{
240 Projected: &v1.ProjectedVolumeSource{
241 Sources: []v1.VolumeProjection{
242 {
243 ConfigMap: &v1.ConfigMapProjection{
244 LocalObjectReference: v1.LocalObjectReference{
245 Name: deleteName,
246 },
247 Optional: &trueVal,
248 },
249 },
250 },
251 },
252 },
253 },
254 {
255 Name: updateVolumeName,
256 VolumeSource: v1.VolumeSource{
257 Projected: &v1.ProjectedVolumeSource{
258 Sources: []v1.VolumeProjection{
259 {
260 ConfigMap: &v1.ConfigMapProjection{
261 LocalObjectReference: v1.LocalObjectReference{
262 Name: updateName,
263 },
264 Optional: &trueVal,
265 },
266 },
267 },
268 },
269 },
270 },
271 {
272 Name: createVolumeName,
273 VolumeSource: v1.VolumeSource{
274 Projected: &v1.ProjectedVolumeSource{
275 Sources: []v1.VolumeProjection{
276 {
277 ConfigMap: &v1.ConfigMapProjection{
278 LocalObjectReference: v1.LocalObjectReference{
279 Name: createName,
280 },
281 Optional: &trueVal,
282 },
283 },
284 },
285 },
286 },
287 },
288 },
289 Containers: []v1.Container{
290 {
291 Name: deleteContainerName,
292 Image: imageutils.GetE2EImage(imageutils.Agnhost),
293 Args: []string{"mounttest", "--break_on_expected_content=false", containerTimeoutArg, "--file_content_in_loop=/etc/projected-configmap-volumes/delete/data-1"},
294 VolumeMounts: []v1.VolumeMount{
295 {
296 Name: deleteVolumeName,
297 MountPath: path.Join(volumeMountPath, "delete"),
298 ReadOnly: true,
299 },
300 },
301 },
302 {
303 Name: updateContainerName,
304 Image: imageutils.GetE2EImage(imageutils.Agnhost),
305 Args: []string{"mounttest", "--break_on_expected_content=false", containerTimeoutArg, "--file_content_in_loop=/etc/projected-configmap-volumes/update/data-3"},
306 VolumeMounts: []v1.VolumeMount{
307 {
308 Name: updateVolumeName,
309 MountPath: path.Join(volumeMountPath, "update"),
310 ReadOnly: true,
311 },
312 },
313 },
314 {
315 Name: createContainerName,
316 Image: imageutils.GetE2EImage(imageutils.Agnhost),
317 Args: []string{"mounttest", "--break_on_expected_content=false", containerTimeoutArg, "--file_content_in_loop=/etc/projected-configmap-volumes/create/data-1"},
318 VolumeMounts: []v1.VolumeMount{
319 {
320 Name: createVolumeName,
321 MountPath: path.Join(volumeMountPath, "create"),
322 ReadOnly: true,
323 },
324 },
325 },
326 },
327 RestartPolicy: v1.RestartPolicyNever,
328 },
329 }
330 ginkgo.By("Creating the pod")
331 e2epod.NewPodClient(f).CreateSync(ctx, pod)
332
333 pollCreateLogs := func() (string, error) {
334 return e2epod.GetPodLogs(ctx, f.ClientSet, f.Namespace.Name, pod.Name, createContainerName)
335 }
336 gomega.Eventually(ctx, pollCreateLogs, podLogTimeout, framework.Poll).Should(gomega.ContainSubstring("Error reading file /etc/projected-configmap-volumes/create/data-1"))
337
338 pollUpdateLogs := func() (string, error) {
339 return e2epod.GetPodLogs(ctx, f.ClientSet, f.Namespace.Name, pod.Name, updateContainerName)
340 }
341 gomega.Eventually(ctx, pollUpdateLogs, podLogTimeout, framework.Poll).Should(gomega.ContainSubstring("Error reading file /etc/projected-configmap-volumes/update/data-3"))
342
343 pollDeleteLogs := func() (string, error) {
344 return e2epod.GetPodLogs(ctx, f.ClientSet, f.Namespace.Name, pod.Name, deleteContainerName)
345 }
346 gomega.Eventually(ctx, pollDeleteLogs, podLogTimeout, framework.Poll).Should(gomega.ContainSubstring("value-1"))
347
348 ginkgo.By(fmt.Sprintf("Deleting configmap %v", deleteConfigMap.Name))
349 err = f.ClientSet.CoreV1().ConfigMaps(f.Namespace.Name).Delete(ctx, deleteConfigMap.Name, metav1.DeleteOptions{})
350 framework.ExpectNoError(err, "Failed to delete configmap %q in namespace %q", deleteConfigMap.Name, f.Namespace.Name)
351
352 ginkgo.By(fmt.Sprintf("Updating configmap %v", updateConfigMap.Name))
353 updateConfigMap.ResourceVersion = ""
354 delete(updateConfigMap.Data, "data-1")
355 updateConfigMap.Data["data-3"] = "value-3"
356 _, err = f.ClientSet.CoreV1().ConfigMaps(f.Namespace.Name).Update(ctx, updateConfigMap, metav1.UpdateOptions{})
357 framework.ExpectNoError(err, "Failed to update configmap %q in namespace %q", updateConfigMap.Name, f.Namespace.Name)
358
359 ginkgo.By(fmt.Sprintf("Creating configMap with name %s", createConfigMap.Name))
360 if createConfigMap, err = f.ClientSet.CoreV1().ConfigMaps(f.Namespace.Name).Create(ctx, createConfigMap, metav1.CreateOptions{}); err != nil {
361 framework.Failf("unable to create test configMap %s: %v", createConfigMap.Name, err)
362 }
363
364 ginkgo.By("waiting to observe update in volume")
365
366 gomega.Eventually(ctx, pollCreateLogs, podLogTimeout, framework.Poll).Should(gomega.ContainSubstring("value-1"))
367 gomega.Eventually(ctx, pollUpdateLogs, podLogTimeout, framework.Poll).Should(gomega.ContainSubstring("value-3"))
368 gomega.Eventually(ctx, pollDeleteLogs, podLogTimeout, framework.Poll).Should(gomega.ContainSubstring("Error reading file /etc/projected-configmap-volumes/delete/data-1"))
369 })
370
371
376 framework.ConformanceIt("should be consumable in multiple volumes in the same pod", f.WithNodeConformance(), func(ctx context.Context) {
377 var (
378 name = "projected-configmap-test-volume-" + string(uuid.NewUUID())
379 volumeName = "projected-configmap-volume"
380 volumeMountPath = "/etc/projected-configmap-volume"
381 volumeName2 = "projected-configmap-volume-2"
382 volumeMountPath2 = "/etc/projected-configmap-volume-2"
383 configMap = newConfigMap(f, name)
384 )
385
386 ginkgo.By(fmt.Sprintf("Creating configMap with name %s", configMap.Name))
387 var err error
388 if configMap, err = f.ClientSet.CoreV1().ConfigMaps(f.Namespace.Name).Create(ctx, configMap, metav1.CreateOptions{}); err != nil {
389 framework.Failf("unable to create test configMap %s: %v", configMap.Name, err)
390 }
391
392 pod := &v1.Pod{
393 ObjectMeta: metav1.ObjectMeta{
394 Name: "pod-projected-configmaps-" + string(uuid.NewUUID()),
395 },
396 Spec: v1.PodSpec{
397 Volumes: []v1.Volume{
398 {
399 Name: volumeName,
400 VolumeSource: v1.VolumeSource{
401 Projected: &v1.ProjectedVolumeSource{
402 Sources: []v1.VolumeProjection{
403 {
404 ConfigMap: &v1.ConfigMapProjection{
405 LocalObjectReference: v1.LocalObjectReference{
406 Name: name,
407 },
408 },
409 },
410 },
411 },
412 },
413 },
414 {
415 Name: volumeName2,
416 VolumeSource: v1.VolumeSource{
417 Projected: &v1.ProjectedVolumeSource{
418 Sources: []v1.VolumeProjection{
419 {
420
421 ConfigMap: &v1.ConfigMapProjection{
422 LocalObjectReference: v1.LocalObjectReference{
423 Name: name,
424 },
425 },
426 },
427 },
428 },
429 },
430 },
431 },
432 Containers: []v1.Container{
433 {
434 Name: "projected-configmap-volume-test",
435 Image: imageutils.GetE2EImage(imageutils.Agnhost),
436 Args: []string{"mounttest", "--file_content=/etc/projected-configmap-volume/data-1"},
437 VolumeMounts: []v1.VolumeMount{
438 {
439 Name: volumeName,
440 MountPath: volumeMountPath,
441 ReadOnly: true,
442 },
443 {
444 Name: volumeName2,
445 MountPath: volumeMountPath2,
446 ReadOnly: true,
447 },
448 },
449 },
450 },
451 RestartPolicy: v1.RestartPolicyNever,
452 },
453 }
454
455 e2epodoutput.TestContainerOutput(ctx, f, "consume configMaps", pod, 0, []string{
456 "content of file \"/etc/projected-configmap-volume/data-1\": value-1",
457 })
458
459 })
460
461
462
463
464 f.It("Should fail non-optional pod creation due to configMap object does not exist", f.WithSlow(), func(ctx context.Context) {
465 volumeMountPath := "/etc/projected-configmap-volumes"
466 pod := createNonOptionalConfigMapPod(ctx, f, volumeMountPath)
467 getPod := e2epod.Get(f.ClientSet, pod)
468 gomega.Consistently(ctx, getPod).WithTimeout(f.Timeouts.PodStart).Should(e2epod.BeInPhase(v1.PodPending))
469 })
470
471
472
473
474 f.It("Should fail non-optional pod creation due to the key in the configMap object does not exist", f.WithSlow(), func(ctx context.Context) {
475 volumeMountPath := "/etc/configmap-volumes"
476 pod := createNonOptionalConfigMapPodWithConfig(ctx, f, volumeMountPath)
477 getPod := e2epod.Get(f.ClientSet, pod)
478 gomega.Consistently(ctx, getPod).WithTimeout(f.Timeouts.PodStart).Should(e2epod.BeInPhase(v1.PodPending))
479 })
480 })
481
482 func doProjectedConfigMapE2EWithoutMappings(ctx context.Context, f *framework.Framework, asUser bool, fsGroup int64, defaultMode *int32) {
483 groupID := int64(fsGroup)
484
485 var (
486 name = "projected-configmap-test-volume-" + string(uuid.NewUUID())
487 volumeName = "projected-configmap-volume"
488 volumeMountPath = "/etc/projected-configmap-volume"
489 configMap = newConfigMap(f, name)
490 )
491
492 ginkgo.By(fmt.Sprintf("Creating configMap with name %s", configMap.Name))
493 var err error
494 if configMap, err = f.ClientSet.CoreV1().ConfigMaps(f.Namespace.Name).Create(ctx, configMap, metav1.CreateOptions{}); err != nil {
495 framework.Failf("unable to create test configMap %s: %v", configMap.Name, err)
496 }
497
498 pod := createProjectedConfigMapMounttestPod(f.Namespace.Name, volumeName, name, volumeMountPath,
499 "--file_content=/etc/projected-configmap-volume/data-1", "--file_mode=/etc/projected-configmap-volume/data-1")
500
501 if asUser {
502 setPodNonRootUser(pod)
503 }
504
505 if groupID != 0 {
506 pod.Spec.SecurityContext.FSGroup = &groupID
507 }
508
509 if defaultMode != nil {
510
511 pod.Spec.Volumes[0].VolumeSource.Projected.DefaultMode = defaultMode
512 }
513
514 fileModeRegexp := getFileModeRegex("/etc/projected-configmap-volume/data-1", defaultMode)
515 output := []string{
516 "content of file \"/etc/projected-configmap-volume/data-1\": value-1",
517 fileModeRegexp,
518 }
519 e2epodoutput.TestContainerOutputRegexp(ctx, f, "consume configMaps", pod, 0, output)
520 }
521
522 func doProjectedConfigMapE2EWithMappings(ctx context.Context, f *framework.Framework, asUser bool, fsGroup int64, itemMode *int32) {
523 groupID := int64(fsGroup)
524
525 var (
526 name = "projected-configmap-test-volume-map-" + string(uuid.NewUUID())
527 volumeName = "projected-configmap-volume"
528 volumeMountPath = "/etc/projected-configmap-volume"
529 configMap = newConfigMap(f, name)
530 )
531
532 ginkgo.By(fmt.Sprintf("Creating configMap with name %s", configMap.Name))
533
534 var err error
535 if configMap, err = f.ClientSet.CoreV1().ConfigMaps(f.Namespace.Name).Create(ctx, configMap, metav1.CreateOptions{}); err != nil {
536 framework.Failf("unable to create test configMap %s: %v", configMap.Name, err)
537 }
538
539 pod := createProjectedConfigMapMounttestPod(f.Namespace.Name, volumeName, name, volumeMountPath,
540 "--file_content=/etc/projected-configmap-volume/path/to/data-2", "--file_mode=/etc/projected-configmap-volume/path/to/data-2")
541 pod.Spec.Volumes[0].VolumeSource.Projected.Sources[0].ConfigMap.Items = []v1.KeyToPath{
542 {
543 Key: "data-2",
544 Path: "path/to/data-2",
545 },
546 }
547
548 if asUser {
549 setPodNonRootUser(pod)
550 }
551
552 if groupID != 0 {
553 pod.Spec.SecurityContext.FSGroup = &groupID
554 }
555
556 if itemMode != nil {
557
558 pod.Spec.Volumes[0].VolumeSource.Projected.DefaultMode = itemMode
559 }
560
561
562
563 output := []string{
564 "content of file \"/etc/projected-configmap-volume/path/to/data-2\": value-2",
565 }
566 if fsGroup == 0 {
567 fileModeRegexp := getFileModeRegex("/etc/projected-configmap-volume/path/to/data-2", itemMode)
568 output = append(output, fileModeRegexp)
569 }
570 e2epodoutput.TestContainerOutputRegexp(ctx, f, "consume configMaps", pod, 0, output)
571 }
572
573 func createProjectedConfigMapMounttestPod(namespace, volumeName, referenceName, mountPath string, mounttestArgs ...string) *v1.Pod {
574 volumes := []v1.Volume{
575 {
576 Name: volumeName,
577 VolumeSource: v1.VolumeSource{
578 Projected: &v1.ProjectedVolumeSource{
579 Sources: []v1.VolumeProjection{
580 {
581 ConfigMap: &v1.ConfigMapProjection{
582 LocalObjectReference: v1.LocalObjectReference{
583 Name: referenceName,
584 },
585 },
586 },
587 },
588 },
589 },
590 },
591 }
592 podName := "pod-projected-configmaps-" + string(uuid.NewUUID())
593 mounttestArgs = append([]string{"mounttest"}, mounttestArgs...)
594 pod := e2epod.NewAgnhostPod(namespace, podName, volumes, createMounts(volumeName, mountPath, true), nil, mounttestArgs...)
595 pod.Spec.RestartPolicy = v1.RestartPolicyNever
596 return pod
597 }
598
View as plain text