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