1
16
17 package auth
18
19 import (
20 "context"
21 "encoding/json"
22 "fmt"
23 "path"
24 "regexp"
25 "strings"
26 "time"
27
28 authenticationv1 "k8s.io/api/authentication/v1"
29 v1 "k8s.io/api/core/v1"
30 rbacv1 "k8s.io/api/rbac/v1"
31 apierrors "k8s.io/apimachinery/pkg/api/errors"
32 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
33 "k8s.io/apimachinery/pkg/types"
34 utilrand "k8s.io/apimachinery/pkg/util/rand"
35 "k8s.io/apimachinery/pkg/util/sets"
36 "k8s.io/apimachinery/pkg/util/uuid"
37 "k8s.io/apimachinery/pkg/util/wait"
38 watch "k8s.io/apimachinery/pkg/watch"
39 "k8s.io/client-go/util/retry"
40 "k8s.io/kubernetes/plugin/pkg/admission/serviceaccount"
41 "k8s.io/kubernetes/test/e2e/framework"
42 e2ekubectl "k8s.io/kubernetes/test/e2e/framework/kubectl"
43 e2epod "k8s.io/kubernetes/test/e2e/framework/pod"
44 e2eoutput "k8s.io/kubernetes/test/e2e/framework/pod/output"
45 e2eskipper "k8s.io/kubernetes/test/e2e/framework/skipper"
46 "k8s.io/kubernetes/test/e2e/nodefeature"
47 imageutils "k8s.io/kubernetes/test/utils/image"
48 admissionapi "k8s.io/pod-security-admission/api"
49 utilptr "k8s.io/utils/pointer"
50
51 "github.com/onsi/ginkgo/v2"
52 "github.com/onsi/gomega"
53 )
54
55 const rootCAConfigMapName = "kube-root-ca.crt"
56
57 var _ = SIGDescribe("ServiceAccounts", func() {
58 f := framework.NewDefaultFramework("svcaccounts")
59 f.NamespacePodSecurityLevel = admissionapi.LevelBaseline
60
61 ginkgo.It("no secret-based service account token should be auto-generated", func(ctx context.Context) {
62 {
63 ginkgo.By("ensuring no secret-based service account token exists")
64 time.Sleep(10 * time.Second)
65 sa, err := f.ClientSet.CoreV1().ServiceAccounts(f.Namespace.Name).Get(ctx, "default", metav1.GetOptions{})
66 framework.ExpectNoError(err)
67 gomega.Expect(sa.Secrets).To(gomega.BeEmpty())
68 }
69 })
70
71
80 framework.ConformanceIt("should mount an API token into pods", func(ctx context.Context) {
81 sa, err := f.ClientSet.CoreV1().ServiceAccounts(f.Namespace.Name).Create(ctx, &v1.ServiceAccount{ObjectMeta: metav1.ObjectMeta{Name: "mount-test"}}, metav1.CreateOptions{})
82 framework.ExpectNoError(err)
83
84 zero := int64(0)
85 pod, err := f.ClientSet.CoreV1().Pods(f.Namespace.Name).Create(ctx, &v1.Pod{
86 ObjectMeta: metav1.ObjectMeta{
87 Name: "pod-service-account-" + string(uuid.NewUUID()),
88 },
89 Spec: v1.PodSpec{
90 ServiceAccountName: sa.Name,
91 Containers: []v1.Container{{
92 Name: "test",
93 Image: imageutils.GetE2EImage(imageutils.BusyBox),
94 Command: []string{"sleep", "100000"},
95 }},
96 TerminationGracePeriodSeconds: &zero,
97 RestartPolicy: v1.RestartPolicyNever,
98 },
99 }, metav1.CreateOptions{})
100 framework.ExpectNoError(err)
101 framework.ExpectNoError(e2epod.WaitForPodRunningInNamespace(ctx, f.ClientSet, pod))
102
103 tk := e2ekubectl.NewTestKubeconfig(framework.TestContext.CertDir, framework.TestContext.Host, framework.TestContext.KubeConfig, framework.TestContext.KubeContext, framework.TestContext.KubectlPath, f.Namespace.Name)
104 mountedToken, err := tk.ReadFileViaContainer(pod.Name, pod.Spec.Containers[0].Name, path.Join(serviceaccount.DefaultAPITokenMountPath, v1.ServiceAccountTokenKey))
105 framework.ExpectNoError(err)
106 mountedCA, err := tk.ReadFileViaContainer(pod.Name, pod.Spec.Containers[0].Name, path.Join(serviceaccount.DefaultAPITokenMountPath, v1.ServiceAccountRootCAKey))
107 framework.ExpectNoError(err)
108 mountedNamespace, err := tk.ReadFileViaContainer(pod.Name, pod.Spec.Containers[0].Name, path.Join(serviceaccount.DefaultAPITokenMountPath, v1.ServiceAccountNamespaceKey))
109 framework.ExpectNoError(err)
110
111
112 rootCA, err := f.ClientSet.CoreV1().ConfigMaps(f.Namespace.Name).Get(ctx, rootCAConfigMapName, metav1.GetOptions{})
113 framework.ExpectNoError(err)
114 framework.Logf("Got root ca configmap in namespace %q", f.Namespace.Name)
115 gomega.Expect(mountedCA).To(gomega.Equal(rootCA.Data["ca.crt"]))
116 gomega.Expect(mountedNamespace).To(gomega.Equal(f.Namespace.Name))
117
118 tokenReview := &authenticationv1.TokenReview{Spec: authenticationv1.TokenReviewSpec{Token: mountedToken}}
119 tokenReview, err = f.ClientSet.AuthenticationV1().TokenReviews().Create(ctx, tokenReview, metav1.CreateOptions{})
120 framework.ExpectNoError(err)
121 if !tokenReview.Status.Authenticated {
122 framework.Fail("tokenReview is not authenticated")
123 }
124 gomega.Expect(tokenReview.Status.Error).To(gomega.BeEmpty())
125 gomega.Expect(tokenReview.Status.User.Username).To(gomega.Equal("system:serviceaccount:" + f.Namespace.Name + ":" + sa.Name))
126 groups := sets.NewString(tokenReview.Status.User.Groups...)
127 if !groups.Has("system:authenticated") {
128 framework.Failf("expected system:authenticated group, had %v", groups.List())
129 }
130 if !groups.Has("system:serviceaccounts") {
131 framework.Failf("expected system:serviceaccounts group, had %v", groups.List())
132 }
133 if !groups.Has("system:serviceaccounts:" + f.Namespace.Name) {
134 framework.Failf("expected system:serviceaccounts:%s group, had %v", f.Namespace.Name, groups.List())
135 }
136 })
137
138
163 framework.ConformanceIt("should allow opting out of API token automount", func(ctx context.Context) {
164
165 var err error
166 trueValue := true
167 falseValue := false
168 mountSA := &v1.ServiceAccount{ObjectMeta: metav1.ObjectMeta{Name: "mount"}, AutomountServiceAccountToken: &trueValue}
169 nomountSA := &v1.ServiceAccount{ObjectMeta: metav1.ObjectMeta{Name: "nomount"}, AutomountServiceAccountToken: &falseValue}
170 mountSA, err = f.ClientSet.CoreV1().ServiceAccounts(f.Namespace.Name).Create(ctx, mountSA, metav1.CreateOptions{})
171 framework.ExpectNoError(err)
172 nomountSA, err = f.ClientSet.CoreV1().ServiceAccounts(f.Namespace.Name).Create(ctx, nomountSA, metav1.CreateOptions{})
173 framework.ExpectNoError(err)
174
175 testcases := []struct {
176 PodName string
177 ServiceAccountName string
178 AutomountPodSpec *bool
179 ExpectTokenVolume bool
180 }{
181 {
182 PodName: "pod-service-account-defaultsa",
183 ServiceAccountName: "default",
184 AutomountPodSpec: nil,
185 ExpectTokenVolume: true,
186 },
187 {
188 PodName: "pod-service-account-mountsa",
189 ServiceAccountName: mountSA.Name,
190 AutomountPodSpec: nil,
191 ExpectTokenVolume: true,
192 },
193 {
194 PodName: "pod-service-account-nomountsa",
195 ServiceAccountName: nomountSA.Name,
196 AutomountPodSpec: nil,
197 ExpectTokenVolume: false,
198 },
199
200
201 {
202 PodName: "pod-service-account-defaultsa-mountspec",
203 ServiceAccountName: "default",
204 AutomountPodSpec: &trueValue,
205 ExpectTokenVolume: true,
206 },
207 {
208 PodName: "pod-service-account-mountsa-mountspec",
209 ServiceAccountName: mountSA.Name,
210 AutomountPodSpec: &trueValue,
211 ExpectTokenVolume: true,
212 },
213 {
214 PodName: "pod-service-account-nomountsa-mountspec",
215 ServiceAccountName: nomountSA.Name,
216 AutomountPodSpec: &trueValue,
217 ExpectTokenVolume: true,
218 },
219
220
221 {
222 PodName: "pod-service-account-defaultsa-nomountspec",
223 ServiceAccountName: "default",
224 AutomountPodSpec: &falseValue,
225 ExpectTokenVolume: false,
226 },
227 {
228 PodName: "pod-service-account-mountsa-nomountspec",
229 ServiceAccountName: mountSA.Name,
230 AutomountPodSpec: &falseValue,
231 ExpectTokenVolume: false,
232 },
233 {
234 PodName: "pod-service-account-nomountsa-nomountspec",
235 ServiceAccountName: nomountSA.Name,
236 AutomountPodSpec: &falseValue,
237 ExpectTokenVolume: false,
238 },
239 }
240
241 for _, tc := range testcases {
242 pod := &v1.Pod{
243 ObjectMeta: metav1.ObjectMeta{Name: tc.PodName},
244 Spec: v1.PodSpec{
245 Containers: []v1.Container{{Name: "token-test", Image: imageutils.GetE2EImage(imageutils.Agnhost)}},
246 RestartPolicy: v1.RestartPolicyNever,
247 ServiceAccountName: tc.ServiceAccountName,
248 AutomountServiceAccountToken: tc.AutomountPodSpec,
249 },
250 }
251 createdPod, err := f.ClientSet.CoreV1().Pods(f.Namespace.Name).Create(ctx, pod, metav1.CreateOptions{})
252 framework.ExpectNoError(err)
253 framework.Logf("created pod %s", tc.PodName)
254
255 hasServiceAccountTokenVolume := false
256 for _, c := range createdPod.Spec.Containers {
257 for _, vm := range c.VolumeMounts {
258 if vm.MountPath == serviceaccount.DefaultAPITokenMountPath {
259 hasServiceAccountTokenVolume = true
260 }
261 }
262 }
263
264 if hasServiceAccountTokenVolume != tc.ExpectTokenVolume {
265 framework.Failf("%s: expected volume=%v, got %v (%#v)", tc.PodName, tc.ExpectTokenVolume, hasServiceAccountTokenVolume, createdPod)
266 } else {
267 framework.Logf("pod %s service account token volume mount: %v", tc.PodName, hasServiceAccountTokenVolume)
268 }
269 }
270 })
271
272
277 framework.ConformanceIt("should mount projected service account token", func(ctx context.Context) {
278
279 var (
280 podName = "test-pod-" + string(uuid.NewUUID())
281 volumeName = "test-volume"
282 volumeMountPath = "/test-volume"
283 tokenVolumePath = "/test-volume/token"
284 )
285
286 volumes := []v1.Volume{
287 {
288 Name: volumeName,
289 VolumeSource: v1.VolumeSource{
290 Projected: &v1.ProjectedVolumeSource{
291 Sources: []v1.VolumeProjection{
292 {
293 ServiceAccountToken: &v1.ServiceAccountTokenProjection{
294 Path: "token",
295 ExpirationSeconds: utilptr.Int64Ptr(60 * 60),
296 },
297 },
298 },
299 },
300 },
301 },
302 }
303 volumeMounts := []v1.VolumeMount{
304 {
305 Name: volumeName,
306 MountPath: volumeMountPath,
307 ReadOnly: true,
308 },
309 }
310 mounttestArgs := []string{
311 "mounttest",
312 fmt.Sprintf("--file_content=%v", tokenVolumePath),
313 }
314
315 pod := e2epod.NewAgnhostPod(f.Namespace.Name, podName, volumes, volumeMounts, nil, mounttestArgs...)
316 pod.Spec.RestartPolicy = v1.RestartPolicyNever
317
318 output := []string{
319 fmt.Sprintf("content of file \"%v\": %s", tokenVolumePath, `[A-Za-z0-9-_=]+\.[A-Za-z0-9-_=]+\.?[A-Za-z0-9-_.+/=]*`),
320 }
321
322 e2eoutput.TestContainerOutputRegexp(ctx, f, "service account token: ", pod, 0, output)
323 })
324
325
338 f.It("should set ownership and permission when RunAsUser or FsGroup is present [LinuxOnly]", nodefeature.FSGroup, func(ctx context.Context) {
339 e2eskipper.SkipIfNodeOSDistroIs("windows")
340
341 var (
342 podName = "test-pod-" + string(uuid.NewUUID())
343 volumeName = "test-volume"
344 volumeMountPath = "/test-volume"
345 tokenVolumePath = "/test-volume/token"
346 )
347
348 volumes := []v1.Volume{
349 {
350 Name: volumeName,
351 VolumeSource: v1.VolumeSource{
352 Projected: &v1.ProjectedVolumeSource{
353 Sources: []v1.VolumeProjection{
354 {
355 ServiceAccountToken: &v1.ServiceAccountTokenProjection{
356 Path: "token",
357 ExpirationSeconds: utilptr.Int64Ptr(60 * 60),
358 },
359 },
360 },
361 },
362 },
363 },
364 }
365 volumeMounts := []v1.VolumeMount{
366 {
367 Name: volumeName,
368 MountPath: volumeMountPath,
369 ReadOnly: true,
370 },
371 }
372 mounttestArgs := []string{
373 "mounttest",
374 fmt.Sprintf("--file_perm=%v", tokenVolumePath),
375 fmt.Sprintf("--file_owner=%v", tokenVolumePath),
376 fmt.Sprintf("--file_content=%v", tokenVolumePath),
377 }
378
379 pod := e2epod.NewAgnhostPod(f.Namespace.Name, podName, volumes, volumeMounts, nil, mounttestArgs...)
380 pod.Spec.RestartPolicy = v1.RestartPolicyNever
381
382 testcases := []struct {
383 runAsUser bool
384 fsGroup bool
385 wantPerm string
386 wantUID int64
387 wantGID int64
388 }{
389 {
390 runAsUser: true,
391 wantPerm: "-rw-------",
392 wantUID: 1000,
393 wantGID: 0,
394 },
395 {
396 fsGroup: true,
397 wantPerm: "-rw-r-----",
398 wantUID: 0,
399 wantGID: 10000,
400 },
401 {
402 runAsUser: true,
403 fsGroup: true,
404 wantPerm: "-rw-r-----",
405 wantUID: 1000,
406 wantGID: 10000,
407 },
408 {
409 wantPerm: "-rw-r--r--",
410 wantUID: 0,
411 wantGID: 0,
412 },
413 }
414
415 for _, tc := range testcases {
416 pod.Spec.SecurityContext = &v1.PodSecurityContext{}
417 if tc.runAsUser {
418 pod.Spec.SecurityContext.RunAsUser = &tc.wantUID
419 }
420 if tc.fsGroup {
421 pod.Spec.SecurityContext.FSGroup = &tc.wantGID
422 }
423
424 output := []string{
425 fmt.Sprintf("perms of file \"%v\": %s", tokenVolumePath, tc.wantPerm),
426 fmt.Sprintf("content of file \"%v\": %s", tokenVolumePath, `[A-Za-z0-9-_=]+\.[A-Za-z0-9-_=]+\.?[A-Za-z0-9-_.+/=]*`),
427 fmt.Sprintf("owner UID of \"%v\": %d", tokenVolumePath, tc.wantUID),
428 fmt.Sprintf("owner GID of \"%v\": %d", tokenVolumePath, tc.wantGID),
429 }
430 e2eoutput.TestContainerOutputRegexp(ctx, f, "service account token: ", pod, 0, output)
431 }
432 })
433
434 f.It("should support InClusterConfig with token rotation", f.WithSlow(), func(ctx context.Context) {
435 tenMin := int64(10 * 60)
436 pod := &v1.Pod{
437 ObjectMeta: metav1.ObjectMeta{Name: "inclusterclient"},
438 Spec: v1.PodSpec{
439 Containers: []v1.Container{{
440 Name: "inclusterclient",
441 Image: imageutils.GetE2EImage(imageutils.Agnhost),
442 Args: []string{"inclusterclient"},
443 VolumeMounts: []v1.VolumeMount{{
444 MountPath: "/var/run/secrets/kubernetes.io/serviceaccount",
445 Name: "kube-api-access-e2e",
446 ReadOnly: true,
447 }},
448 }},
449 RestartPolicy: v1.RestartPolicyNever,
450 ServiceAccountName: "default",
451 Volumes: []v1.Volume{{
452 Name: "kube-api-access-e2e",
453 VolumeSource: v1.VolumeSource{
454 Projected: &v1.ProjectedVolumeSource{
455 Sources: []v1.VolumeProjection{
456 {
457 ServiceAccountToken: &v1.ServiceAccountTokenProjection{
458 Path: "token",
459 ExpirationSeconds: &tenMin,
460 },
461 },
462 {
463 ConfigMap: &v1.ConfigMapProjection{
464 LocalObjectReference: v1.LocalObjectReference{
465 Name: "kube-root-ca.crt",
466 },
467 Items: []v1.KeyToPath{
468 {
469 Key: "ca.crt",
470 Path: "ca.crt",
471 },
472 },
473 },
474 },
475 {
476 DownwardAPI: &v1.DownwardAPIProjection{
477 Items: []v1.DownwardAPIVolumeFile{
478 {
479 Path: "namespace",
480 FieldRef: &v1.ObjectFieldSelector{
481 APIVersion: "v1",
482 FieldPath: "metadata.namespace",
483 },
484 },
485 },
486 },
487 },
488 },
489 },
490 },
491 }},
492 },
493 }
494 pod, err := f.ClientSet.CoreV1().Pods(f.Namespace.Name).Create(ctx, pod, metav1.CreateOptions{})
495 framework.ExpectNoError(err)
496
497 framework.Logf("created pod")
498 framework.ExpectNoError(e2epod.WaitTimeoutForPodReadyInNamespace(ctx, f.ClientSet, pod.Name, f.Namespace.Name, time.Minute))
499
500 framework.Logf("pod is ready")
501
502 var logs string
503 if err := wait.Poll(1*time.Minute, 20*time.Minute, func() (done bool, err error) {
504 framework.Logf("polling logs")
505 logs, err = e2epod.GetPodLogs(ctx, f.ClientSet, f.Namespace.Name, "inclusterclient", "inclusterclient")
506 if err != nil {
507 framework.Logf("Error pulling logs: %v", err)
508 return false, nil
509 }
510 tokenCount, err := ParseInClusterClientLogs(logs)
511 if err != nil {
512 return false, fmt.Errorf("inclusterclient reported an error: %w", err)
513 }
514 if tokenCount < 2 {
515 framework.Logf("Retrying. Still waiting to see more unique tokens: got=%d, want=2", tokenCount)
516 return false, nil
517 }
518 return true, nil
519 }); err != nil {
520 framework.Failf("Unexpected error: %v\n%s", err, logs)
521 }
522 })
523
524
531 framework.ConformanceIt("ServiceAccountIssuerDiscovery should support OIDC discovery of service account issuer", func(ctx context.Context) {
532
533
534
535
536
537
538 const clusterRoleName = "system:service-account-issuer-discovery"
539 crbName := fmt.Sprintf("%s-%s", f.Namespace.Name, clusterRoleName)
540 if crb, err := f.ClientSet.RbacV1().ClusterRoleBindings().Create(
541 ctx,
542 &rbacv1.ClusterRoleBinding{
543 ObjectMeta: metav1.ObjectMeta{
544 Name: crbName,
545 },
546 Subjects: []rbacv1.Subject{
547 {
548 Kind: rbacv1.ServiceAccountKind,
549 APIGroup: "",
550 Name: "default",
551 Namespace: f.Namespace.Name,
552 },
553 },
554 RoleRef: rbacv1.RoleRef{
555 Name: clusterRoleName,
556 APIGroup: rbacv1.GroupName,
557 Kind: "ClusterRole",
558 },
559 },
560 metav1.CreateOptions{}); err != nil {
561
562 framework.Logf("error granting ClusterRoleBinding %s: %v", crbName, err)
563 } else {
564 defer func() {
565 framework.ExpectNoError(
566 f.ClientSet.RbacV1().ClusterRoleBindings().Delete(
567 ctx,
568 crb.Name, metav1.DeleteOptions{}))
569 }()
570 }
571
572
573 tokenPath := "/var/run/secrets/tokens"
574 tokenName := "sa-token"
575 audience := "oidc-discovery-test"
576 tenMin := int64(10 * 60)
577
578 pod := &v1.Pod{
579 ObjectMeta: metav1.ObjectMeta{Name: "oidc-discovery-validator"},
580 Spec: v1.PodSpec{
581 Containers: []v1.Container{{
582 Name: "oidc-discovery-validator",
583 Image: imageutils.GetE2EImage(imageutils.Agnhost),
584 Args: []string{
585 "test-service-account-issuer-discovery",
586 "--token-path", path.Join(tokenPath, tokenName),
587 "--audience", audience,
588 },
589 VolumeMounts: []v1.VolumeMount{{
590 MountPath: tokenPath,
591 Name: tokenName,
592 ReadOnly: true,
593 }},
594 }},
595 RestartPolicy: v1.RestartPolicyNever,
596 ServiceAccountName: "default",
597 Volumes: []v1.Volume{{
598 Name: tokenName,
599 VolumeSource: v1.VolumeSource{
600 Projected: &v1.ProjectedVolumeSource{
601 Sources: []v1.VolumeProjection{
602 {
603 ServiceAccountToken: &v1.ServiceAccountTokenProjection{
604 Path: tokenName,
605 ExpirationSeconds: &tenMin,
606 Audience: audience,
607 },
608 },
609 },
610 },
611 },
612 }},
613 },
614 }
615 pod, err := f.ClientSet.CoreV1().Pods(f.Namespace.Name).Create(ctx, pod, metav1.CreateOptions{})
616 framework.ExpectNoError(err)
617
618 framework.Logf("created pod")
619 podErr := e2epod.WaitForPodSuccessInNamespace(ctx, f.ClientSet, pod.Name, f.Namespace.Name)
620
621
622 var logs string
623 if err := wait.Poll(30*time.Second, 2*time.Minute, func() (done bool, err error) {
624 framework.Logf("polling logs")
625 logs, err = e2epod.GetPodLogs(ctx, f.ClientSet, f.Namespace.Name, pod.Name, pod.Spec.Containers[0].Name)
626 if err != nil {
627 framework.Logf("Error pulling logs: %v", err)
628 return false, nil
629 }
630 return true, nil
631 }); err != nil {
632 framework.Failf("Unexpected error getting pod logs: %v\n%s", err, logs)
633 } else {
634 framework.Logf("Pod logs: \n%v", logs)
635 }
636
637 framework.ExpectNoError(podErr)
638 framework.Logf("completed pod")
639 })
640
641
649 framework.ConformanceIt("should run through the lifecycle of a ServiceAccount", func(ctx context.Context) {
650 testNamespaceName := f.Namespace.Name
651 testServiceAccountName := "testserviceaccount"
652 testServiceAccountStaticLabels := map[string]string{"test-serviceaccount-static": "true"}
653 testServiceAccountStaticLabelsFlat := "test-serviceaccount-static=true"
654
655 ginkgo.By("creating a ServiceAccount")
656 testServiceAccount := v1.ServiceAccount{
657 ObjectMeta: metav1.ObjectMeta{
658 Name: testServiceAccountName,
659 Labels: testServiceAccountStaticLabels,
660 },
661 }
662 createdServiceAccount, err := f.ClientSet.CoreV1().ServiceAccounts(testNamespaceName).Create(ctx, &testServiceAccount, metav1.CreateOptions{})
663 framework.ExpectNoError(err, "failed to create a ServiceAccount")
664
665 getServiceAccount, err := f.ClientSet.CoreV1().ServiceAccounts(testNamespaceName).Get(ctx, testServiceAccountName, metav1.GetOptions{})
666 framework.ExpectNoError(err, "failed to fetch the created ServiceAccount")
667 gomega.Expect(createdServiceAccount.UID).To(gomega.Equal(getServiceAccount.UID))
668
669 ginkgo.By("watching for the ServiceAccount to be added")
670 resourceWatchTimeoutSeconds := int64(180)
671 resourceWatch, err := f.ClientSet.CoreV1().ServiceAccounts(testNamespaceName).Watch(ctx, metav1.ListOptions{LabelSelector: testServiceAccountStaticLabelsFlat, TimeoutSeconds: &resourceWatchTimeoutSeconds})
672 if err != nil {
673 fmt.Println(err, "failed to setup watch on newly created ServiceAccount")
674 return
675 }
676
677 resourceWatchChan := resourceWatch.ResultChan()
678 eventFound := false
679 for watchEvent := range resourceWatchChan {
680 if watchEvent.Type == watch.Added {
681 eventFound = true
682 break
683 }
684 }
685 if !eventFound {
686 framework.Failf("failed to find %v event", watch.Added)
687 }
688 ginkgo.By("patching the ServiceAccount")
689 boolFalse := false
690 testServiceAccountPatchData, err := json.Marshal(v1.ServiceAccount{
691 AutomountServiceAccountToken: &boolFalse,
692 })
693 framework.ExpectNoError(err, "failed to marshal JSON patch for the ServiceAccount")
694 _, err = f.ClientSet.CoreV1().ServiceAccounts(testNamespaceName).Patch(ctx, testServiceAccountName, types.StrategicMergePatchType, []byte(testServiceAccountPatchData), metav1.PatchOptions{})
695 framework.ExpectNoError(err, "failed to patch the ServiceAccount")
696 eventFound = false
697 for watchEvent := range resourceWatchChan {
698 if watchEvent.Type == watch.Modified {
699 eventFound = true
700 break
701 }
702 }
703 if !eventFound {
704 framework.Failf("failed to find %v event", watch.Modified)
705 }
706 ginkgo.By("finding ServiceAccount in list of all ServiceAccounts (by LabelSelector)")
707 serviceAccountList, err := f.ClientSet.CoreV1().ServiceAccounts("").List(ctx, metav1.ListOptions{LabelSelector: testServiceAccountStaticLabelsFlat})
708 framework.ExpectNoError(err, "failed to list ServiceAccounts by LabelSelector")
709 foundServiceAccount := false
710 for _, serviceAccountItem := range serviceAccountList.Items {
711 if serviceAccountItem.ObjectMeta.Name == testServiceAccountName && serviceAccountItem.ObjectMeta.Namespace == testNamespaceName && *serviceAccountItem.AutomountServiceAccountToken == boolFalse {
712 foundServiceAccount = true
713 break
714 }
715 }
716 if !foundServiceAccount {
717 framework.Fail("failed to find the created ServiceAccount")
718 }
719 ginkgo.By("deleting the ServiceAccount")
720 err = f.ClientSet.CoreV1().ServiceAccounts(testNamespaceName).DeleteCollection(ctx, metav1.DeleteOptions{}, metav1.ListOptions{})
721 framework.ExpectNoError(err, "failed to delete the ServiceAccount by Collection")
722 eventFound = false
723 for watchEvent := range resourceWatchChan {
724 if watchEvent.Type == watch.Deleted {
725 eventFound = true
726 break
727 }
728 }
729 if !eventFound {
730 framework.Failf("failed to find %v event", watch.Deleted)
731 }
732 })
733
734
742 framework.ConformanceIt("should guarantee kube-root-ca.crt exist in any namespace", func(ctx context.Context) {
743 framework.ExpectNoError(wait.PollImmediate(500*time.Millisecond, wait.ForeverTestTimeout, func() (bool, error) {
744 _, err := f.ClientSet.CoreV1().ConfigMaps(f.Namespace.Name).Get(ctx, rootCAConfigMapName, metav1.GetOptions{})
745 if err == nil {
746 return true, nil
747 }
748 if apierrors.IsNotFound(err) {
749 ginkgo.By("root ca configmap not found, retrying")
750 return false, nil
751 }
752 return false, err
753 }))
754 framework.Logf("Got root ca configmap in namespace %q", f.Namespace.Name)
755
756 framework.ExpectNoError(f.ClientSet.CoreV1().ConfigMaps(f.Namespace.Name).Delete(ctx, rootCAConfigMapName, metav1.DeleteOptions{GracePeriodSeconds: utilptr.Int64Ptr(0)}))
757 framework.Logf("Deleted root ca configmap in namespace %q", f.Namespace.Name)
758
759 framework.ExpectNoError(wait.Poll(500*time.Millisecond, wait.ForeverTestTimeout, func() (bool, error) {
760 ginkgo.By("waiting for a new root ca configmap created")
761 _, err := f.ClientSet.CoreV1().ConfigMaps(f.Namespace.Name).Get(ctx, rootCAConfigMapName, metav1.GetOptions{})
762 if err == nil {
763 return true, nil
764 }
765 if apierrors.IsNotFound(err) {
766 ginkgo.By("root ca configmap not found, retrying")
767 return false, nil
768 }
769 return false, err
770 }))
771 framework.Logf("Recreated root ca configmap in namespace %q", f.Namespace.Name)
772
773 _, err := f.ClientSet.CoreV1().ConfigMaps(f.Namespace.Name).Update(ctx, &v1.ConfigMap{
774 ObjectMeta: metav1.ObjectMeta{
775 Name: rootCAConfigMapName,
776 },
777 Data: map[string]string{
778 "ca.crt": "",
779 },
780 }, metav1.UpdateOptions{})
781 framework.ExpectNoError(err)
782 framework.Logf("Updated root ca configmap in namespace %q", f.Namespace.Name)
783
784 framework.ExpectNoError(wait.Poll(500*time.Millisecond, wait.ForeverTestTimeout, func() (bool, error) {
785 ginkgo.By("waiting for the root ca configmap reconciled")
786 cm, err := f.ClientSet.CoreV1().ConfigMaps(f.Namespace.Name).Get(ctx, rootCAConfigMapName, metav1.GetOptions{})
787 if err != nil {
788 if apierrors.IsNotFound(err) {
789 ginkgo.By("root ca configmap not found, retrying")
790 return false, nil
791 }
792 return false, err
793 }
794 if value, ok := cm.Data["ca.crt"]; !ok || value == "" {
795 ginkgo.By("root ca configmap is not reconciled yet, retrying")
796 return false, nil
797 }
798 return true, nil
799 }))
800 framework.Logf("Reconciled root ca configmap in namespace %q", f.Namespace.Name)
801 })
802
803
810 framework.ConformanceIt("should update a ServiceAccount", func(ctx context.Context) {
811 saClient := f.ClientSet.CoreV1().ServiceAccounts(f.Namespace.Name)
812 saName := "e2e-sa-" + utilrand.String(5)
813
814 initialServiceAccount := &v1.ServiceAccount{
815 ObjectMeta: metav1.ObjectMeta{
816 Name: saName,
817 },
818 AutomountServiceAccountToken: utilptr.Bool(false),
819 }
820
821 ginkgo.By(fmt.Sprintf("Creating ServiceAccount %q ", saName))
822 createdServiceAccount, err := saClient.Create(ctx, initialServiceAccount, metav1.CreateOptions{})
823 framework.ExpectNoError(err)
824 gomega.Expect(createdServiceAccount.AutomountServiceAccountToken).To(gomega.Equal(utilptr.Bool(false)), "Failed to set AutomountServiceAccountToken")
825 framework.Logf("AutomountServiceAccountToken: %v", *createdServiceAccount.AutomountServiceAccountToken)
826
827 ginkgo.By(fmt.Sprintf("Updating ServiceAccount %q ", saName))
828 var updatedServiceAccount *v1.ServiceAccount
829
830 err = retry.RetryOnConflict(retry.DefaultRetry, func() error {
831 updateServiceAccount, err := saClient.Get(ctx, saName, metav1.GetOptions{})
832 framework.ExpectNoError(err, "Unable to get ServiceAccount %q", saName)
833 updateServiceAccount.AutomountServiceAccountToken = utilptr.Bool(true)
834 updatedServiceAccount, err = saClient.Update(ctx, updateServiceAccount, metav1.UpdateOptions{})
835 return err
836 })
837 framework.ExpectNoError(err, "Failed to update ServiceAccount")
838 gomega.Expect(updatedServiceAccount.AutomountServiceAccountToken).To(gomega.Equal(utilptr.Bool(true)), "Failed to set AutomountServiceAccountToken")
839 framework.Logf("AutomountServiceAccountToken: %v", *updatedServiceAccount.AutomountServiceAccountToken)
840 })
841 })
842
843 var reportLogsParser = regexp.MustCompile("([a-zA-Z0-9-_]*)=([a-zA-Z0-9-_]*)$")
844
845
846 func ParseInClusterClientLogs(logs string) (int, error) {
847 seenTokens := map[string]struct{}{}
848
849 lines := strings.Split(logs, "\n")
850 for _, line := range lines {
851 parts := reportLogsParser.FindStringSubmatch(line)
852 if len(parts) != 3 {
853 continue
854 }
855
856 key, value := parts[1], parts[2]
857 switch key {
858 case "authz_header":
859 if value == "<empty>" {
860 return 0, fmt.Errorf("saw empty Authorization header")
861 }
862 seenTokens[value] = struct{}{}
863 case "status":
864 if value == "failed" {
865 return 0, fmt.Errorf("saw status=failed")
866 }
867 }
868 }
869
870 return len(seenTokens), nil
871 }
872
View as plain text