k8s.io/kubernetes@v1.29.3/test/e2e/auth/service_accounts.go (about)

     1  /*
     2  Copyright 2014 The Kubernetes 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 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  	/*
    72  	   Release: v1.9
    73  	   Testname: Service Account Tokens Must AutoMount
    74  	   Description: Ensure that Service Account keys are mounted into the Container. Pod
    75  	                contains three containers each will read Service Account token,
    76  	                root CA and default namespace respectively from the default API
    77  	                Token Mount path. All these three files MUST exist and the Service
    78  	                Account mount path MUST be auto mounted to the Container.
    79  	*/
    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  		// CA and namespace should be identical
   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  		// Token should be a valid credential that identifies the pod's service account
   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  	/*
   139  	   Release: v1.9
   140  	   Testname: Service account tokens auto mount optionally
   141  	   Description: Ensure that Service Account keys are mounted into the Pod only
   142  	                when AutoMountServiceToken is not set to false. We test the
   143  	                following scenarios here.
   144  	   1. Create Pod, Pod Spec has AutomountServiceAccountToken set to nil
   145  	      a) Service Account with default value,
   146  	      b) Service Account is an configured AutomountServiceAccountToken set to true,
   147  	      c) Service Account is an configured AutomountServiceAccountToken set to false
   148  	   2. Create Pod, Pod Spec has AutomountServiceAccountToken set to true
   149  	      a) Service Account with default value,
   150  	      b) Service Account is configured with AutomountServiceAccountToken set to true,
   151  	      c) Service Account is configured with AutomountServiceAccountToken set to false
   152  	   3. Create Pod, Pod Spec has AutomountServiceAccountToken set to false
   153  	      a) Service Account with default value,
   154  	      b) Service Account is configured with AutomountServiceAccountToken set to true,
   155  	      c) Service Account is configured with AutomountServiceAccountToken set to false
   156  
   157  	   The Containers running in these pods MUST verify that the ServiceTokenVolume path is
   158  	   auto mounted only when Pod Spec has AutomountServiceAccountToken not set to false
   159  	   and ServiceAccount object has AutomountServiceAccountToken not set to false, this
   160  	   include test cases 1a,1b,2a,2b and 2c.
   161  	   In the test cases 1c,3a,3b and 3c the ServiceTokenVolume MUST not be auto mounted.
   162  	*/
   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, // default is 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  			// Make sure pod spec trumps when opting in
   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, // pod spec trumps
   218  			},
   219  
   220  			// Make sure pod spec trumps when opting out
   221  			{
   222  				PodName:            "pod-service-account-defaultsa-nomountspec",
   223  				ServiceAccountName: "default",
   224  				AutomountPodSpec:   &falseValue,
   225  				ExpectTokenVolume:  false, // pod spec trumps
   226  			},
   227  			{
   228  				PodName:            "pod-service-account-mountsa-nomountspec",
   229  				ServiceAccountName: mountSA.Name,
   230  				AutomountPodSpec:   &falseValue,
   231  				ExpectTokenVolume:  false, // pod spec trumps
   232  			},
   233  			{
   234  				PodName:            "pod-service-account-nomountsa-nomountspec",
   235  				ServiceAccountName: nomountSA.Name,
   236  				AutomountPodSpec:   &falseValue,
   237  				ExpectTokenVolume:  false, // pod spec trumps
   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  	/*
   273  	  Release : v1.20
   274  	  Testname: TokenRequestProjection should mount a projected volume with token using TokenRequest API.
   275  	  Description: Ensure that projected service account token is mounted.
   276  	*/
   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  	/*
   326  	   Testname: Projected service account token file ownership and permission.
   327  	   Description: Ensure that Projected Service Account Token is mounted with
   328  	               correct file ownership and permission mounted. We test the
   329  	               following scenarios here.
   330  	   1. RunAsUser is set,
   331  	   2. FsGroup is set,
   332  	   3. RunAsUser and FsGroup are set,
   333  	   4. Default, neither RunAsUser nor FsGroup is set,
   334  
   335  	   Containers MUST verify that the projected service account token can be
   336  	   read and has correct file mode set including ownership and permission.
   337  	*/
   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  	/*
   525  	   Release: v1.21
   526  	   Testname: OIDC Discovery (ServiceAccountIssuerDiscovery)
   527  	   Description: Ensure kube-apiserver serves correct OIDC discovery
   528  	   endpoints by deploying a Pod that verifies its own
   529  	   token against these endpoints.
   530  	*/
   531  	framework.ConformanceIt("ServiceAccountIssuerDiscovery should support OIDC discovery of service account issuer", func(ctx context.Context) {
   532  
   533  		// Allow the test pod access to the OIDC discovery non-resource URLs.
   534  		// The role should have already been automatically created as part of the
   535  		// RBAC bootstrap policy, but not the role binding. If RBAC is disabled,
   536  		// we skip creating the binding. We also make sure we clean up the
   537  		// binding after the test.
   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  			// Tolerate RBAC not being enabled
   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  		// Create the pod with tokens.
   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  		// Get the logs before calling ExpectNoError, so we can debug any errors.
   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  	/*
   642  			   Release: v1.19
   643  			   Testname: ServiceAccount lifecycle test
   644  			   Description: Creates a ServiceAccount with a static Label MUST be added as shown in watch event.
   645  		                        Patching the ServiceAccount MUST return it's new property.
   646  		                        Listing the ServiceAccounts MUST return the test ServiceAccount with it's patched values.
   647  		                        ServiceAccount will be deleted and MUST find a deleted watch event.
   648  	*/
   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  	/*
   735  		Release: v1.21
   736  		Testname: RootCA ConfigMap test
   737  		Description: Ensure every namespace exist a ConfigMap for root ca cert.
   738  			1. Created automatically
   739  			2. Recreated if deleted
   740  			3. Reconciled if modified
   741  	*/
   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  	/*
   804  		Release: v1.26
   805  		Testname: ServiceAccount, update a ServiceAccount
   806  		Description: A ServiceAccount is created which MUST succeed. When
   807  		updating the ServiceAccount it MUST succeed and the field MUST equal
   808  		the new value.
   809  	*/
   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  // ParseInClusterClientLogs parses logs of pods using inclusterclient.
   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  }