k8s.io/kubernetes@v1.29.3/test/e2e/windows/gmsa_full.go (about)

     1  /*
     2  Copyright 2019 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  // This test ensures that the whole GMSA process works as intended.
    18  // However, it does require a pretty heavy weight set up to run correctly;
    19  // in particular, it does make a number of assumptions about the cluster it
    20  // runs against:
    21  //  * there exists a Windows worker node with the agentpool=windowsgmsa label on it
    22  //  * that node is joined to a working Active Directory domain.
    23  //  * a GMSA account has been created in that AD domain, and then installed on that
    24  //    same worker.
    25  //  * a valid k8s manifest file containing a single CRD definition has been generated using
    26  //    https://github.com/kubernetes-sigs/windows-gmsa/blob/master/scripts/GenerateCredentialSpecResource.ps1
    27  //    with the credential specs of that GMSA account, or type GMSACredentialSpec and named gmsa-e2e;
    28  //    and that manifest file has been written to C:\gmsa\gmsa-cred-spec-gmsa-e2e.yml
    29  //    on that same worker node.
    30  //  * the API has both MutatingAdmissionWebhook and ValidatingAdmissionWebhook
    31  //    admission controllers enabled.
    32  //  * the cluster comprises at least one Linux node that accepts workloads - it
    33  //    can be the master, but any other Linux node is fine too. This is needed for
    34  //    the webhook's pod.
    35  //  * in order to run "can read and write file to remote folder" test case, a folder (e.g. "write_test") need to be created
    36  //    in that AD domain and it should be shared with that GMSA account.
    37  // All these assumptions are fulfilled by an AKS extension when setting up the AKS
    38  // cluster we run daily e2e tests against, but they do make running this test
    39  // outside of that very specific context pretty hard.
    40  
    41  package windows
    42  
    43  import (
    44  	"context"
    45  	"fmt"
    46  	"os"
    47  	"regexp"
    48  	"strings"
    49  	"time"
    50  
    51  	"github.com/onsi/ginkgo/v2"
    52  	"github.com/onsi/gomega"
    53  	appsv1 "k8s.io/api/apps/v1"
    54  	v1 "k8s.io/api/core/v1"
    55  	rbacv1 "k8s.io/api/rbac/v1"
    56  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    57  	"k8s.io/apimachinery/pkg/util/uuid"
    58  	clientset "k8s.io/client-go/kubernetes"
    59  	"k8s.io/kubernetes/test/e2e/feature"
    60  	"k8s.io/kubernetes/test/e2e/framework"
    61  	e2ekubectl "k8s.io/kubernetes/test/e2e/framework/kubectl"
    62  	e2epod "k8s.io/kubernetes/test/e2e/framework/pod"
    63  	e2eskipper "k8s.io/kubernetes/test/e2e/framework/skipper"
    64  	imageutils "k8s.io/kubernetes/test/utils/image"
    65  	admissionapi "k8s.io/pod-security-admission/api"
    66  )
    67  
    68  const (
    69  	// gmsaFullNodeLabel is the label we expect to find on at least one node
    70  	// that is then expected to fulfill all the expectations explained above.
    71  	gmsaFullNodeLabel = "agentpool=windowsgmsa"
    72  
    73  	// gmsaCrdManifestPath is where we expect to find the manifest file for
    74  	// the GMSA cred spec on that node - see explanations above.
    75  	gmsaCrdManifestPath = `C:\gmsa\gmsa-cred-spec-gmsa-e2e.yml`
    76  
    77  	// gmsaCustomResourceName is the expected name of the GMSA custom resource
    78  	// defined at gmsaCrdManifestPath
    79  	gmsaCustomResourceName = "gmsa-e2e"
    80  
    81  	// gmsaWebhookDeployScriptURL is the URL of the deploy script for the GMSA webook
    82  	gmsaWebhookDeployScriptURL = "https://raw.githubusercontent.com/kubernetes-sigs/windows-gmsa/master/admission-webhook/deploy/deploy-gmsa-webhook.sh"
    83  
    84  	// output from the nltest /query command should have this in it
    85  	expectedQueryOutput = "The command completed successfully"
    86  
    87  	// The name of the expected domain
    88  	gmsaDomain = "k8sgmsa.lan"
    89  
    90  	// The shared folder on the expected domain for file-writing test
    91  	gmsaSharedFolder = "write_test"
    92  )
    93  
    94  var _ = sigDescribe(feature.Windows, "GMSA Full", framework.WithSerial(), framework.WithSlow(), skipUnlessWindows(func() {
    95  	f := framework.NewDefaultFramework("gmsa-full-test-windows")
    96  	f.NamespacePodSecurityLevel = admissionapi.LevelPrivileged
    97  
    98  	ginkgo.Describe("GMSA support", func() {
    99  		ginkgo.It("works end to end", func(ctx context.Context) {
   100  			defer ginkgo.GinkgoRecover()
   101  
   102  			ginkgo.By("finding the worker node that fulfills this test's assumptions")
   103  			nodes := findPreconfiguredGmsaNodes(ctx, f.ClientSet)
   104  			if len(nodes) != 1 {
   105  				e2eskipper.Skipf("Expected to find exactly one node with the %q label, found %d", gmsaFullNodeLabel, len(nodes))
   106  			}
   107  			node := nodes[0]
   108  
   109  			ginkgo.By("retrieving the contents of the GMSACredentialSpec custom resource manifest from the node")
   110  			crdManifestContents := retrieveCRDManifestFileContents(ctx, f, node)
   111  
   112  			ginkgo.By("deploying the GMSA webhook")
   113  			err := deployGmsaWebhook(ctx, f)
   114  			if err != nil {
   115  				framework.Failf(err.Error())
   116  			}
   117  
   118  			ginkgo.By("creating the GMSA custom resource")
   119  			err = createGmsaCustomResource(f.Namespace.Name, crdManifestContents)
   120  			if err != nil {
   121  				framework.Failf(err.Error())
   122  			}
   123  
   124  			ginkgo.By("creating an RBAC role to grant use access to that GMSA resource")
   125  			rbacRoleName, err := createRBACRoleForGmsa(ctx, f)
   126  			if err != nil {
   127  				framework.Failf(err.Error())
   128  			}
   129  
   130  			ginkgo.By("creating a service account")
   131  			serviceAccountName := createServiceAccount(ctx, f)
   132  
   133  			ginkgo.By("binding the RBAC role to the service account")
   134  			bindRBACRoleToServiceAccount(ctx, f, serviceAccountName, rbacRoleName)
   135  
   136  			ginkgo.By("creating a pod using the GMSA cred spec")
   137  			podName := createPodWithGmsa(ctx, f, serviceAccountName)
   138  
   139  			// nltest /QUERY will only return successfully if there is a GMSA
   140  			// identity configured, _and_ it succeeds in contacting the AD controller
   141  			// and authenticating with it.
   142  			ginkgo.By("checking that nltest /QUERY returns successfully")
   143  			var output string
   144  			gomega.Eventually(ctx, func() bool {
   145  				output, err = runKubectlExecInNamespace(f.Namespace.Name, podName, "nltest", "/QUERY")
   146  				if err != nil {
   147  					framework.Logf("unable to run command in container via exec: %s", err)
   148  					return false
   149  				}
   150  
   151  				if !isValidOutput(output) {
   152  					// try repairing the secure channel by running reset command
   153  					// https://kubernetes.io/docs/tasks/configure-pod-container/configure-gmsa/#troubleshooting
   154  					output, err = runKubectlExecInNamespace(f.Namespace.Name, podName, "nltest", fmt.Sprintf("/sc_reset:%s", gmsaDomain))
   155  					if err != nil {
   156  						framework.Logf("unable to run command in container via exec: %s", err)
   157  						return false
   158  					}
   159  					framework.Logf("failed to connect to domain; tried resetting the domain, output:\n%s", string(output))
   160  					return false
   161  				}
   162  				return true
   163  			}, 1*time.Minute, 1*time.Second).Should(gomega.BeTrue())
   164  		})
   165  
   166  		ginkgo.It("can read and write file to remote SMB folder", func(ctx context.Context) {
   167  			defer ginkgo.GinkgoRecover()
   168  
   169  			ginkgo.By("finding the worker node that fulfills this test's assumptions")
   170  			nodes := findPreconfiguredGmsaNodes(ctx, f.ClientSet)
   171  			if len(nodes) != 1 {
   172  				e2eskipper.Skipf("Expected to find exactly one node with the %q label, found %d", gmsaFullNodeLabel, len(nodes))
   173  			}
   174  			node := nodes[0]
   175  
   176  			ginkgo.By("retrieving the contents of the GMSACredentialSpec custom resource manifest from the node")
   177  			crdManifestContents := retrieveCRDManifestFileContents(ctx, f, node)
   178  
   179  			ginkgo.By("deploying the GMSA webhook")
   180  			err := deployGmsaWebhook(ctx, f)
   181  			if err != nil {
   182  				framework.Failf(err.Error())
   183  			}
   184  
   185  			ginkgo.By("creating the GMSA custom resource")
   186  			err = createGmsaCustomResource(f.Namespace.Name, crdManifestContents)
   187  			if err != nil {
   188  				framework.Failf(err.Error())
   189  			}
   190  
   191  			ginkgo.By("creating an RBAC role to grant use access to that GMSA resource")
   192  			rbacRoleName, err := createRBACRoleForGmsa(ctx, f)
   193  			if err != nil {
   194  				framework.Failf(err.Error())
   195  			}
   196  
   197  			ginkgo.By("creating a service account")
   198  			serviceAccountName := createServiceAccount(ctx, f)
   199  
   200  			ginkgo.By("binding the RBAC role to the service account")
   201  			bindRBACRoleToServiceAccount(ctx, f, serviceAccountName, rbacRoleName)
   202  
   203  			ginkgo.By("creating a pod using the GMSA cred spec")
   204  			podName := createPodWithGmsa(ctx, f, serviceAccountName)
   205  
   206  			ginkgo.By("getting the ip of GMSA domain")
   207  			gmsaDomainIP := getGmsaDomainIP(f, podName)
   208  
   209  			ginkgo.By("checking that file can be read and write from the remote folder successfully")
   210  			filePath := fmt.Sprintf("\\\\%s\\%s\\write-test-%s.txt", gmsaDomainIP, gmsaSharedFolder, string(uuid.NewUUID())[0:4])
   211  			gomega.Eventually(ctx, func() bool {
   212  				// The filePath is a remote folder, do not change the format of it
   213  				_, _ = runKubectlExecInNamespace(f.Namespace.Name, podName, "--", "powershell.exe", "-Command", "echo 'This is a test file.' > "+filePath)
   214  				output, err := runKubectlExecInNamespace(f.Namespace.Name, podName, "powershell.exe", "--", "cat", filePath)
   215  				if err != nil {
   216  					framework.Logf("unable to get file from AD server: %s", err)
   217  					return false
   218  				}
   219  				return strings.Contains(output, "This is a test file.")
   220  			}, 1*time.Minute, 1*time.Second).Should(gomega.BeTrue())
   221  
   222  		})
   223  	})
   224  }))
   225  
   226  func isValidOutput(output string) bool {
   227  	return strings.Contains(output, expectedQueryOutput) &&
   228  		!strings.Contains(output, "ERROR_NO_LOGON_SERVERS") &&
   229  		!strings.Contains(output, "RPC_S_SERVER_UNAVAILABLE")
   230  }
   231  
   232  // findPreconfiguredGmsaNode finds node with the gmsaFullNodeLabel label on it.
   233  func findPreconfiguredGmsaNodes(ctx context.Context, c clientset.Interface) []v1.Node {
   234  	nodeOpts := metav1.ListOptions{
   235  		LabelSelector: gmsaFullNodeLabel,
   236  	}
   237  	nodes, err := c.CoreV1().Nodes().List(ctx, nodeOpts)
   238  	if err != nil {
   239  		framework.Failf("Unable to list nodes: %v", err)
   240  	}
   241  	return nodes.Items
   242  }
   243  
   244  // retrieveCRDManifestFileContents retrieves the contents of the file
   245  // at gmsaCrdManifestPath on node; it does so by scheduling a single pod
   246  // on nodes with the gmsaFullNodeLabel label with that file's directory
   247  // mounted on it, and then exec-ing into that pod to retrieve the file's
   248  // contents.
   249  func retrieveCRDManifestFileContents(ctx context.Context, f *framework.Framework, node v1.Node) string {
   250  	podName := "retrieve-gmsa-crd-contents"
   251  	// we can't use filepath.Dir here since the test itself runs on a Linux machine
   252  	splitPath := strings.Split(gmsaCrdManifestPath, `\`)
   253  	dirPath := strings.Join(splitPath[:len(splitPath)-1], `\`)
   254  	volumeName := "retrieve-gmsa-crd-contents-volume"
   255  
   256  	pod := &v1.Pod{
   257  		ObjectMeta: metav1.ObjectMeta{
   258  			Name:      podName,
   259  			Namespace: f.Namespace.Name,
   260  		},
   261  		Spec: v1.PodSpec{
   262  			NodeSelector: node.Labels,
   263  			Containers: []v1.Container{
   264  				{
   265  					Name:  podName,
   266  					Image: imageutils.GetPauseImageName(),
   267  					VolumeMounts: []v1.VolumeMount{
   268  						{
   269  							Name:      volumeName,
   270  							MountPath: dirPath,
   271  						},
   272  					},
   273  				},
   274  			},
   275  			Volumes: []v1.Volume{
   276  				{
   277  					Name: volumeName,
   278  					VolumeSource: v1.VolumeSource{
   279  						HostPath: &v1.HostPathVolumeSource{
   280  							Path: dirPath,
   281  						},
   282  					},
   283  				},
   284  			},
   285  		},
   286  	}
   287  	e2epod.NewPodClient(f).CreateSync(ctx, pod)
   288  
   289  	output, err := runKubectlExecInNamespace(f.Namespace.Name, podName, "cmd", "/S", "/C", fmt.Sprintf("type %s", gmsaCrdManifestPath))
   290  	if err != nil {
   291  		framework.Failf("failed to retrieve the contents of %q on node %q: %v", gmsaCrdManifestPath, node.Name, err)
   292  	}
   293  
   294  	// Windows to linux new lines
   295  	return strings.ReplaceAll(output, "\r\n", "\n")
   296  }
   297  
   298  // deployGmsaWebhook deploys the GMSA webhook, and returns a cleanup function
   299  // to be called when done with testing, that removes the temp files it's created
   300  // on disks as well as the API resources it's created.
   301  func deployGmsaWebhook(ctx context.Context, f *framework.Framework) error {
   302  	deployerName := "webhook-deployer"
   303  	deployerNamespace := f.Namespace.Name
   304  	webHookName := "gmsa-webhook"
   305  	webHookNamespace := deployerNamespace + "-webhook"
   306  
   307  	// regardless of whether the deployment succeeded, let's do a best effort at cleanup
   308  	ginkgo.DeferCleanup(func() {
   309  		framework.Logf("Best effort clean up of the webhook:\n")
   310  		stdout, err := e2ekubectl.RunKubectl("", "delete", "CustomResourceDefinition", "gmsacredentialspecs.windows.k8s.io")
   311  		framework.Logf("stdout:%s\nerror:%s", stdout, err)
   312  
   313  		stdout, err = e2ekubectl.RunKubectl("", "delete", "CertificateSigningRequest", fmt.Sprintf("%s.%s", webHookName, webHookNamespace))
   314  		framework.Logf("stdout:%s\nerror:%s", stdout, err)
   315  
   316  		stdout, err = runKubectlExecInNamespace(deployerNamespace, deployerName, "--", "kubectl", "delete", "-f", "/manifests.yml")
   317  		framework.Logf("stdout:%s\nerror:%s", stdout, err)
   318  	})
   319  
   320  	// ensure the deployer has ability to approve certificatesigningrequests to install the webhook
   321  	s := createServiceAccount(ctx, f)
   322  	bindClusterRBACRoleToServiceAccount(ctx, f, s, "cluster-admin")
   323  
   324  	installSteps := []string{
   325  		"echo \"@community http://dl-cdn.alpinelinux.org/alpine/edge/community/\" >> /etc/apk/repositories",
   326  		"&& apk add kubectl@community gettext openssl",
   327  		"&& apk add --update coreutils",
   328  		fmt.Sprintf("&& curl %s > gmsa.sh", gmsaWebhookDeployScriptURL),
   329  		"&& chmod +x gmsa.sh",
   330  		fmt.Sprintf("&& ./gmsa.sh --file %s --name %s --namespace %s --certs-dir %s --tolerate-master", "/manifests.yml", webHookName, webHookNamespace, "certs"),
   331  		"&& /agnhost pause",
   332  	}
   333  	installCommand := strings.Join(installSteps, " ")
   334  
   335  	pod := &v1.Pod{
   336  		ObjectMeta: metav1.ObjectMeta{
   337  			Name:      deployerName,
   338  			Namespace: deployerNamespace,
   339  		},
   340  		Spec: v1.PodSpec{
   341  			ServiceAccountName: s,
   342  			NodeSelector: map[string]string{
   343  				"kubernetes.io/os": "linux",
   344  			},
   345  			Containers: []v1.Container{
   346  				{
   347  					Name:    deployerName,
   348  					Image:   imageutils.GetE2EImage(imageutils.Agnhost),
   349  					Command: []string{"bash", "-c"},
   350  					Args:    []string{installCommand},
   351  				},
   352  			},
   353  			Tolerations: []v1.Toleration{
   354  				{
   355  					Operator: v1.TolerationOpExists,
   356  					Effect:   v1.TaintEffectNoSchedule,
   357  				},
   358  			},
   359  		},
   360  	}
   361  	e2epod.NewPodClient(f).CreateSync(ctx, pod)
   362  
   363  	// Wait for the Webhook deployment to become ready. The deployer pod takes a few seconds to initialize and create resources
   364  	err := waitForDeployment(func() (*appsv1.Deployment, error) {
   365  		return f.ClientSet.AppsV1().Deployments(webHookNamespace).Get(ctx, webHookName, metav1.GetOptions{})
   366  	}, 10*time.Second, f.Timeouts.PodStart)
   367  	if err == nil {
   368  		framework.Logf("GMSA webhook successfully deployed")
   369  	} else {
   370  		err = fmt.Errorf("GMSA webhook did not become ready: %w", err)
   371  	}
   372  
   373  	// Dump deployer logs
   374  	logs, _ := e2epod.GetPodLogs(ctx, f.ClientSet, deployerNamespace, deployerName, deployerName)
   375  	framework.Logf("GMSA deployment logs:\n%s", logs)
   376  
   377  	return err
   378  }
   379  
   380  // createGmsaCustomResource creates the GMSA API object from the contents
   381  // of the manifest file retrieved from the worker node.
   382  // It returns a function to clean up both the temp file it creates and
   383  // the API object it creates when done with testing.
   384  func createGmsaCustomResource(ns string, crdManifestContents string) error {
   385  	tempFile, err := os.CreateTemp("", "")
   386  	if err != nil {
   387  		return fmt.Errorf("unable to create temp file: %w", err)
   388  	}
   389  	defer tempFile.Close()
   390  
   391  	ginkgo.DeferCleanup(func() {
   392  		e2ekubectl.RunKubectl(ns, "delete", "--filename", tempFile.Name())
   393  		os.Remove(tempFile.Name())
   394  	})
   395  
   396  	_, err = tempFile.WriteString(crdManifestContents)
   397  	if err != nil {
   398  		err = fmt.Errorf("unable to write GMSA contents to %q: %w", tempFile.Name(), err)
   399  		return err
   400  	}
   401  
   402  	output, err := e2ekubectl.RunKubectl(ns, "apply", "--filename", tempFile.Name())
   403  	if err != nil {
   404  		err = fmt.Errorf("unable to create custom resource, output:\n%s: %w", output, err)
   405  	}
   406  
   407  	return err
   408  }
   409  
   410  // createRBACRoleForGmsa creates an RBAC cluster role to grant use
   411  // access to our test credential spec.
   412  // It returns the role's name, as well as a function to delete it when done.
   413  func createRBACRoleForGmsa(ctx context.Context, f *framework.Framework) (string, error) {
   414  	roleName := f.Namespace.Name + "-rbac-role"
   415  
   416  	role := &rbacv1.ClusterRole{
   417  		ObjectMeta: metav1.ObjectMeta{
   418  			Name: roleName,
   419  		},
   420  		Rules: []rbacv1.PolicyRule{
   421  			{
   422  				APIGroups:     []string{"windows.k8s.io"},
   423  				Resources:     []string{"gmsacredentialspecs"},
   424  				Verbs:         []string{"use"},
   425  				ResourceNames: []string{gmsaCustomResourceName},
   426  			},
   427  		},
   428  	}
   429  
   430  	ginkgo.DeferCleanup(framework.IgnoreNotFound(f.ClientSet.RbacV1().ClusterRoles().Delete), roleName, metav1.DeleteOptions{})
   431  	_, err := f.ClientSet.RbacV1().ClusterRoles().Create(ctx, role, metav1.CreateOptions{})
   432  	if err != nil {
   433  		err = fmt.Errorf("unable to create RBAC cluster role %q: %w", roleName, err)
   434  	}
   435  
   436  	return roleName, err
   437  }
   438  
   439  // createServiceAccount creates a service account, and returns its name.
   440  func createServiceAccount(ctx context.Context, f *framework.Framework) string {
   441  	accountName := f.Namespace.Name + "-sa-" + string(uuid.NewUUID())
   442  	account := &v1.ServiceAccount{
   443  		ObjectMeta: metav1.ObjectMeta{
   444  			Name:      accountName,
   445  			Namespace: f.Namespace.Name,
   446  		},
   447  	}
   448  	if _, err := f.ClientSet.CoreV1().ServiceAccounts(f.Namespace.Name).Create(ctx, account, metav1.CreateOptions{}); err != nil {
   449  		framework.Failf("unable to create service account %q: %v", accountName, err)
   450  	}
   451  	return accountName
   452  }
   453  
   454  // bindRBACRoleToServiceAccount binds the given RBAC cluster role to the given service account.
   455  func bindRBACRoleToServiceAccount(ctx context.Context, f *framework.Framework, serviceAccountName, rbacRoleName string) {
   456  	binding := &rbacv1.RoleBinding{
   457  		ObjectMeta: metav1.ObjectMeta{
   458  			Name:      f.Namespace.Name + "-rbac-binding",
   459  			Namespace: f.Namespace.Name,
   460  		},
   461  		Subjects: []rbacv1.Subject{
   462  			{
   463  				Kind:      "ServiceAccount",
   464  				Name:      serviceAccountName,
   465  				Namespace: f.Namespace.Name,
   466  			},
   467  		},
   468  		RoleRef: rbacv1.RoleRef{
   469  			APIGroup: "rbac.authorization.k8s.io",
   470  			Kind:     "ClusterRole",
   471  			Name:     rbacRoleName,
   472  		},
   473  	}
   474  	_, err := f.ClientSet.RbacV1().RoleBindings(f.Namespace.Name).Create(ctx, binding, metav1.CreateOptions{})
   475  	framework.ExpectNoError(err)
   476  }
   477  
   478  func bindClusterRBACRoleToServiceAccount(ctx context.Context, f *framework.Framework, serviceAccountName, rbacRoleName string) {
   479  	binding := &rbacv1.ClusterRoleBinding{
   480  		ObjectMeta: metav1.ObjectMeta{
   481  			Name:      f.Namespace.Name + "-rbac-binding",
   482  			Namespace: f.Namespace.Name,
   483  		},
   484  		Subjects: []rbacv1.Subject{
   485  			{
   486  				Kind:      "ServiceAccount",
   487  				Name:      serviceAccountName,
   488  				Namespace: f.Namespace.Name,
   489  			},
   490  		},
   491  		RoleRef: rbacv1.RoleRef{
   492  			APIGroup: "rbac.authorization.k8s.io",
   493  			Kind:     "ClusterRole",
   494  			Name:     rbacRoleName,
   495  		},
   496  	}
   497  	_, err := f.ClientSet.RbacV1().ClusterRoleBindings().Create(ctx, binding, metav1.CreateOptions{})
   498  	framework.ExpectNoError(err)
   499  }
   500  
   501  // createPodWithGmsa creates a pod using the test GMSA cred spec, and returns its name.
   502  func createPodWithGmsa(ctx context.Context, f *framework.Framework, serviceAccountName string) string {
   503  	podName := "pod-with-gmsa"
   504  	credSpecName := gmsaCustomResourceName
   505  
   506  	pod := &v1.Pod{
   507  		ObjectMeta: metav1.ObjectMeta{
   508  			Name:      podName,
   509  			Namespace: f.Namespace.Name,
   510  		},
   511  		Spec: v1.PodSpec{
   512  			ServiceAccountName: serviceAccountName,
   513  			Containers: []v1.Container{
   514  				{
   515  					Name:  podName,
   516  					Image: imageutils.GetE2EImage(imageutils.BusyBox),
   517  					Command: []string{
   518  						"powershell.exe",
   519  						"-Command",
   520  						"sleep -Seconds 600",
   521  					},
   522  				},
   523  			},
   524  			SecurityContext: &v1.PodSecurityContext{
   525  				WindowsOptions: &v1.WindowsSecurityContextOptions{
   526  					GMSACredentialSpecName: &credSpecName,
   527  				},
   528  			},
   529  		},
   530  	}
   531  	e2epod.NewPodClient(f).CreateSync(ctx, pod)
   532  
   533  	return podName
   534  }
   535  
   536  func runKubectlExecInNamespace(namespace string, args ...string) (string, error) {
   537  	namespaceOption := fmt.Sprintf("--namespace=%s", namespace)
   538  	return e2ekubectl.RunKubectl(namespace, append([]string{"exec", namespaceOption}, args...)...)
   539  }
   540  
   541  func getGmsaDomainIP(f *framework.Framework, podName string) string {
   542  	output, _ := runKubectlExecInNamespace(f.Namespace.Name, podName, "powershell.exe", "--", "nslookup", gmsaDomain)
   543  	re := regexp.MustCompile(`(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)(\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}`)
   544  	idx := strings.Index(output, gmsaDomain)
   545  
   546  	submatchall := re.FindAllString(output[idx:], -1)
   547  	if len(submatchall) < 1 {
   548  		framework.Logf("fail to get the ip of the gmsa domain")
   549  		return ""
   550  	}
   551  	return submatchall[0]
   552  }