Merge pull request #108899 from jsturtevant/windows-gmsa-deployment
Windows gmsa e2e: Don't assume bash is avaliable for webhook deployment
This commit is contained in:
		| @@ -44,23 +44,22 @@ import ( | |||||||
| 	"context" | 	"context" | ||||||
| 	"fmt" | 	"fmt" | ||||||
| 	"os" | 	"os" | ||||||
| 	"os/exec" |  | ||||||
| 	"path" |  | ||||||
| 	"regexp" | 	"regexp" | ||||||
| 	"strings" | 	"strings" | ||||||
| 	"time" | 	"time" | ||||||
|  |  | ||||||
|  | 	"github.com/onsi/ginkgo" | ||||||
|  | 	"github.com/onsi/gomega" | ||||||
|  | 	appsv1 "k8s.io/api/apps/v1" | ||||||
| 	v1 "k8s.io/api/core/v1" | 	v1 "k8s.io/api/core/v1" | ||||||
| 	rbacv1 "k8s.io/api/rbac/v1" | 	rbacv1 "k8s.io/api/rbac/v1" | ||||||
| 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" | 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" | ||||||
| 	"k8s.io/apimachinery/pkg/util/uuid" | 	"k8s.io/apimachinery/pkg/util/uuid" | ||||||
| 	clientset "k8s.io/client-go/kubernetes" | 	clientset "k8s.io/client-go/kubernetes" | ||||||
| 	"k8s.io/kubernetes/test/e2e/framework" | 	"k8s.io/kubernetes/test/e2e/framework" | ||||||
|  | 	e2epod "k8s.io/kubernetes/test/e2e/framework/pod" | ||||||
| 	e2eskipper "k8s.io/kubernetes/test/e2e/framework/skipper" | 	e2eskipper "k8s.io/kubernetes/test/e2e/framework/skipper" | ||||||
| 	imageutils "k8s.io/kubernetes/test/utils/image" | 	imageutils "k8s.io/kubernetes/test/utils/image" | ||||||
|  |  | ||||||
| 	"github.com/onsi/ginkgo" |  | ||||||
| 	"github.com/onsi/gomega" |  | ||||||
| ) | ) | ||||||
|  |  | ||||||
| const ( | const ( | ||||||
| @@ -77,7 +76,6 @@ const ( | |||||||
| 	gmsaCustomResourceName = "gmsa-e2e" | 	gmsaCustomResourceName = "gmsa-e2e" | ||||||
|  |  | ||||||
| 	// gmsaWebhookDeployScriptURL is the URL of the deploy script for the GMSA webook | 	// gmsaWebhookDeployScriptURL is the URL of the deploy script for the GMSA webook | ||||||
| 	// TODO(wk8): we should pin versions. |  | ||||||
| 	gmsaWebhookDeployScriptURL = "https://raw.githubusercontent.com/kubernetes-sigs/windows-gmsa/master/admission-webhook/deploy/deploy-gmsa-webhook.sh" | 	gmsaWebhookDeployScriptURL = "https://raw.githubusercontent.com/kubernetes-sigs/windows-gmsa/master/admission-webhook/deploy/deploy-gmsa-webhook.sh" | ||||||
|  |  | ||||||
| 	// output from the nltest /query command should have this in it | 	// output from the nltest /query command should have this in it | ||||||
| @@ -107,15 +105,8 @@ var _ = SIGDescribe("[Feature:Windows] GMSA Full [Serial] [Slow]", func() { | |||||||
| 			ginkgo.By("retrieving the contents of the GMSACredentialSpec custom resource manifest from the node") | 			ginkgo.By("retrieving the contents of the GMSACredentialSpec custom resource manifest from the node") | ||||||
| 			crdManifestContents := retrieveCRDManifestFileContents(f, node) | 			crdManifestContents := retrieveCRDManifestFileContents(f, node) | ||||||
|  |  | ||||||
| 			ginkgo.By("downloading the GMSA webhook deploy script") |  | ||||||
| 			deployScriptPath, err := downloadFile(gmsaWebhookDeployScriptURL) |  | ||||||
| 			defer func() { os.Remove(deployScriptPath) }() |  | ||||||
| 			if err != nil { |  | ||||||
| 				framework.Failf(err.Error()) |  | ||||||
| 			} |  | ||||||
|  |  | ||||||
| 			ginkgo.By("deploying the GMSA webhook") | 			ginkgo.By("deploying the GMSA webhook") | ||||||
| 			webhookCleanUp, err := deployGmsaWebhook(f, deployScriptPath) | 			webhookCleanUp, err := deployGmsaWebhook(f) | ||||||
| 			defer webhookCleanUp() | 			defer webhookCleanUp() | ||||||
| 			if err != nil { | 			if err != nil { | ||||||
| 				framework.Failf(err.Error()) | 				framework.Failf(err.Error()) | ||||||
| @@ -184,15 +175,8 @@ var _ = SIGDescribe("[Feature:Windows] GMSA Full [Serial] [Slow]", func() { | |||||||
| 			ginkgo.By("retrieving the contents of the GMSACredentialSpec custom resource manifest from the node") | 			ginkgo.By("retrieving the contents of the GMSACredentialSpec custom resource manifest from the node") | ||||||
| 			crdManifestContents := retrieveCRDManifestFileContents(f, node) | 			crdManifestContents := retrieveCRDManifestFileContents(f, node) | ||||||
|  |  | ||||||
| 			ginkgo.By("downloading the GMSA webhook deploy script") |  | ||||||
| 			deployScriptPath, err := downloadFile(gmsaWebhookDeployScriptURL) |  | ||||||
| 			defer func() { os.Remove(deployScriptPath) }() |  | ||||||
| 			if err != nil { |  | ||||||
| 				framework.Failf(err.Error()) |  | ||||||
| 			} |  | ||||||
|  |  | ||||||
| 			ginkgo.By("deploying the GMSA webhook") | 			ginkgo.By("deploying the GMSA webhook") | ||||||
| 			webhookCleanUp, err := deployGmsaWebhook(f, deployScriptPath) | 			webhookCleanUp, err := deployGmsaWebhook(f) | ||||||
| 			defer webhookCleanUp() | 			defer webhookCleanUp() | ||||||
| 			if err != nil { | 			if err != nil { | ||||||
| 				framework.Failf(err.Error()) | 				framework.Failf(err.Error()) | ||||||
| @@ -236,6 +220,7 @@ var _ = SIGDescribe("[Feature:Windows] GMSA Full [Serial] [Slow]", func() { | |||||||
| 				} | 				} | ||||||
| 				return strings.Contains(output, "This is a test file.") | 				return strings.Contains(output, "This is a test file.") | ||||||
| 			}, 1*time.Minute, 1*time.Second).Should(gomega.BeTrue()) | 			}, 1*time.Minute, 1*time.Second).Should(gomega.BeTrue()) | ||||||
|  |  | ||||||
| 		}) | 		}) | ||||||
| 	}) | 	}) | ||||||
| }) | }) | ||||||
| @@ -315,41 +300,82 @@ func retrieveCRDManifestFileContents(f *framework.Framework, node v1.Node) strin | |||||||
| // deployGmsaWebhook deploys the GMSA webhook, and returns a cleanup function | // deployGmsaWebhook deploys the GMSA webhook, and returns a cleanup function | ||||||
| // to be called when done with testing, that removes the temp files it's created | // to be called when done with testing, that removes the temp files it's created | ||||||
| // on disks as well as the API resources it's created. | // on disks as well as the API resources it's created. | ||||||
| func deployGmsaWebhook(f *framework.Framework, deployScriptPath string) (func(), error) { | func deployGmsaWebhook(f *framework.Framework) (func(), error) { | ||||||
| 	cleanUpFunc := func() {} | 	deployerName := "webhook-deployer" | ||||||
|  | 	deployerNamespace := f.Namespace.Name | ||||||
| 	tempDir, err := os.MkdirTemp("", "") | 	webHookName := "gmsa-webhook" | ||||||
| 	if err != nil { | 	webHookNamespace := deployerNamespace + "-webhook" | ||||||
| 		return cleanUpFunc, fmt.Errorf("unable to create temp dir: %w", err) |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	manifestsFile := path.Join(tempDir, "manifests.yml") |  | ||||||
| 	name := "gmsa-webhook" |  | ||||||
| 	namespace := f.Namespace.Name + "-webhook" |  | ||||||
| 	certsDir := path.Join(tempDir, "certs") |  | ||||||
|  |  | ||||||
| 	// regardless of whether the deployment succeeded, let's do a best effort at cleanup | 	// regardless of whether the deployment succeeded, let's do a best effort at cleanup | ||||||
| 	cleanUpFunc = func() { | 	cleanUpFunc := func() { | ||||||
| 		framework.RunKubectl(f.Namespace.Name, "delete", "--filename", manifestsFile) | 		framework.Logf("Best effort clean up of the webhook:\n") | ||||||
| 		framework.RunKubectl(f.Namespace.Name, "delete", "CustomResourceDefinition", "gmsacredentialspecs.windows.k8s.io") | 		stdout, err := framework.RunKubectl("", "delete", "CustomResourceDefinition", "gmsacredentialspecs.windows.k8s.io") | ||||||
| 		framework.RunKubectl(f.Namespace.Name, "delete", "CertificateSigningRequest", fmt.Sprintf("%s.%s", name, namespace)) | 		framework.Logf("stdout:%s\nerror:%s", stdout, err) | ||||||
| 		os.RemoveAll(tempDir) |  | ||||||
|  | 		stdout, err = framework.RunKubectl("", "delete", "CertificateSigningRequest", fmt.Sprintf("%s.%s", webHookName, webHookNamespace)) | ||||||
|  | 		framework.Logf("stdout:%s\nerror:%s", stdout, err) | ||||||
|  |  | ||||||
|  | 		stdout, err = runKubectlExecInNamespace(deployerNamespace, deployerName, "--", "kubectl", "delete", "-f", "/manifests.yml") | ||||||
|  | 		framework.Logf("stdout:%s\nerror:%s", stdout, err) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	cmd := exec.Command("bash", deployScriptPath, | 	// ensure the deployer has ability to approve certificatesigningrequests to install the webhook | ||||||
| 		"--file", manifestsFile, | 	s := createServiceAccount(f) | ||||||
| 		"--name", name, | 	bindClusterRBACRoleToServiceAccount(f, s, "cluster-admin") | ||||||
| 		"--namespace", namespace, |  | ||||||
| 		"--certs-dir", certsDir, |  | ||||||
| 		"--tolerate-master") |  | ||||||
|  |  | ||||||
| 	output, err := cmd.CombinedOutput() | 	installSteps := []string{ | ||||||
|  | 		"echo \"@testing http://dl-cdn.alpinelinux.org/alpine/edge/testing/\" >> /etc/apk/repositories", | ||||||
|  | 		"&& apk add kubectl@testing gettext openssl", | ||||||
|  | 		"&& apk add --update coreutils", | ||||||
|  | 		fmt.Sprintf("&& curl %s > gmsa.sh", gmsaWebhookDeployScriptURL), | ||||||
|  | 		"&& chmod +x gmsa.sh", | ||||||
|  | 		fmt.Sprintf("&& ./gmsa.sh --file %s --name %s --namespace %s --certs-dir %s --tolerate-master", "/manifests.yml", webHookName, webHookNamespace, "certs"), | ||||||
|  | 		"&& /agnhost pause", | ||||||
|  | 	} | ||||||
|  | 	installCommand := strings.Join(installSteps, " ") | ||||||
|  |  | ||||||
|  | 	pod := &v1.Pod{ | ||||||
|  | 		ObjectMeta: metav1.ObjectMeta{ | ||||||
|  | 			Name:      deployerName, | ||||||
|  | 			Namespace: deployerNamespace, | ||||||
|  | 		}, | ||||||
|  | 		Spec: v1.PodSpec{ | ||||||
|  | 			ServiceAccountName: s, | ||||||
|  | 			NodeSelector: map[string]string{ | ||||||
|  | 				"kubernetes.io/os": "linux", | ||||||
|  | 			}, | ||||||
|  | 			Containers: []v1.Container{ | ||||||
|  | 				{ | ||||||
|  | 					Name:    deployerName, | ||||||
|  | 					Image:   imageutils.GetE2EImage(imageutils.Agnhost), | ||||||
|  | 					Command: []string{"bash", "-c"}, | ||||||
|  | 					Args:    []string{installCommand}, | ||||||
|  | 				}, | ||||||
|  | 			}, | ||||||
|  | 			Tolerations: []v1.Toleration{ | ||||||
|  | 				{ | ||||||
|  | 					Operator: v1.TolerationOpExists, | ||||||
|  | 					Effect:   v1.TaintEffectNoSchedule, | ||||||
|  | 				}, | ||||||
|  | 			}, | ||||||
|  | 		}, | ||||||
|  | 	} | ||||||
|  | 	f.PodClient().CreateSync(pod) | ||||||
|  |  | ||||||
|  | 	// Wait for the Webhook deployment to become ready. The deployer pod takes a few seconds to initialize and create resources | ||||||
|  | 	err := waitForDeployment(func() (*appsv1.Deployment, error) { | ||||||
|  | 		return f.ClientSet.AppsV1().Deployments(webHookNamespace).Get(context.TODO(), webHookName, metav1.GetOptions{}) | ||||||
|  | 	}, 10*time.Second, f.Timeouts.PodStart) | ||||||
| 	if err == nil { | 	if err == nil { | ||||||
| 		framework.Logf("GMSA webhook successfully deployed, output:\n%s", string(output)) | 		framework.Logf("GMSA webhook successfully deployed") | ||||||
| 	} else { | 	} else { | ||||||
| 		err = fmt.Errorf("unable to deploy GMSA webhook, output:\n%s: %w", string(output), err) | 		err = fmt.Errorf("GMSA webhook did not become ready: %w", err) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | 	// Dump deployer logs | ||||||
|  | 	logs, _ := e2epod.GetPodLogs(f.ClientSet, deployerNamespace, deployerName, deployerName) | ||||||
|  | 	framework.Logf("GMSA deployment logs:\n%s", logs) | ||||||
|  |  | ||||||
| 	return cleanUpFunc, err | 	return cleanUpFunc, err | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -419,7 +445,7 @@ func createRBACRoleForGmsa(f *framework.Framework) (string, func(), error) { | |||||||
|  |  | ||||||
| // createServiceAccount creates a service account, and returns its name. | // createServiceAccount creates a service account, and returns its name. | ||||||
| func createServiceAccount(f *framework.Framework) string { | func createServiceAccount(f *framework.Framework) string { | ||||||
| 	accountName := f.Namespace.Name + "-sa" | 	accountName := f.Namespace.Name + "-sa-" + string(uuid.NewUUID()) | ||||||
| 	account := &v1.ServiceAccount{ | 	account := &v1.ServiceAccount{ | ||||||
| 		ObjectMeta: metav1.ObjectMeta{ | 		ObjectMeta: metav1.ObjectMeta{ | ||||||
| 			Name:      accountName, | 			Name:      accountName, | ||||||
| @@ -455,6 +481,28 @@ func bindRBACRoleToServiceAccount(f *framework.Framework, serviceAccountName, rb | |||||||
| 	f.ClientSet.RbacV1().RoleBindings(f.Namespace.Name).Create(context.TODO(), binding, metav1.CreateOptions{}) | 	f.ClientSet.RbacV1().RoleBindings(f.Namespace.Name).Create(context.TODO(), binding, metav1.CreateOptions{}) | ||||||
| } | } | ||||||
|  |  | ||||||
|  | func bindClusterRBACRoleToServiceAccount(f *framework.Framework, serviceAccountName, rbacRoleName string) { | ||||||
|  | 	binding := &rbacv1.ClusterRoleBinding{ | ||||||
|  | 		ObjectMeta: metav1.ObjectMeta{ | ||||||
|  | 			Name:      f.Namespace.Name + "-rbac-binding", | ||||||
|  | 			Namespace: f.Namespace.Name, | ||||||
|  | 		}, | ||||||
|  | 		Subjects: []rbacv1.Subject{ | ||||||
|  | 			{ | ||||||
|  | 				Kind:      "ServiceAccount", | ||||||
|  | 				Name:      serviceAccountName, | ||||||
|  | 				Namespace: f.Namespace.Name, | ||||||
|  | 			}, | ||||||
|  | 		}, | ||||||
|  | 		RoleRef: rbacv1.RoleRef{ | ||||||
|  | 			APIGroup: "rbac.authorization.k8s.io", | ||||||
|  | 			Kind:     "ClusterRole", | ||||||
|  | 			Name:     rbacRoleName, | ||||||
|  | 		}, | ||||||
|  | 	} | ||||||
|  | 	f.ClientSet.RbacV1().ClusterRoleBindings().Create(context.TODO(), binding, metav1.CreateOptions{}) | ||||||
|  | } | ||||||
|  |  | ||||||
| // createPodWithGmsa creates a pod using the test GMSA cred spec, and returns its name. | // createPodWithGmsa creates a pod using the test GMSA cred spec, and returns its name. | ||||||
| func createPodWithGmsa(f *framework.Framework, serviceAccountName string) string { | func createPodWithGmsa(f *framework.Framework, serviceAccountName string) string { | ||||||
| 	podName := "pod-with-gmsa" | 	podName := "pod-with-gmsa" | ||||||
|   | |||||||
| @@ -17,27 +17,30 @@ limitations under the License. | |||||||
| package windows | package windows | ||||||
|  |  | ||||||
| import ( | import ( | ||||||
| 	"fmt" | 	"time" | ||||||
| 	"io" |  | ||||||
| 	"net/http" | 	appsv1 "k8s.io/api/apps/v1" | ||||||
| 	"os" | 	apierrors "k8s.io/apimachinery/pkg/api/errors" | ||||||
|  | 	"k8s.io/apimachinery/pkg/util/wait" | ||||||
|  | 	"k8s.io/kubernetes/pkg/controller/deployment/util" | ||||||
|  | 	"k8s.io/kubernetes/test/e2e/framework" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| // downloadFile saves a remote URL to a local temp file, and returns its path. | // waits for a deployment to be created and the desired replicas | ||||||
| // It's the caller's responsibility to clean up the temp file when done. | // are updated and available, and no old pods are running. | ||||||
| func downloadFile(url string) (string, error) { | func waitForDeployment(getDeploymentFunc func() (*appsv1.Deployment, error), interval, timeout time.Duration) error { | ||||||
| 	response, err := http.Get(url) | 	return wait.PollImmediate(interval, timeout, func() (bool, error) { | ||||||
| 	if err != nil { | 		deployment, err := getDeploymentFunc() | ||||||
| 		return "", fmt.Errorf("unable to download from %q: %w", url, err) | 		if err != nil { | ||||||
| 	} | 			if apierrors.IsNotFound(err) { | ||||||
| 	defer response.Body.Close() | 				framework.Logf("deployment not found, continue waiting: %s", err) | ||||||
|  | 				return false, nil | ||||||
|  | 			} | ||||||
|  |  | ||||||
| 	tempFile, err := os.CreateTemp("", "") | 			framework.Logf("error while deploying, error %s", err) | ||||||
| 	if err != nil { | 			return false, err | ||||||
| 		return "", fmt.Errorf("unable to create temp file: %w", err) | 		} | ||||||
| 	} | 		framework.Logf("deployment status %s", &deployment.Status) | ||||||
| 	defer tempFile.Close() | 		return util.DeploymentComplete(deployment, &deployment.Status), nil | ||||||
|  | 	}) | ||||||
| 	_, err = io.Copy(tempFile, response.Body) |  | ||||||
| 	return tempFile.Name(), err |  | ||||||
| } | } | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user
	 Kubernetes Prow Robot
					Kubernetes Prow Robot