Don't assume bash is avaliable for webhook deployment

This commit is contained in:
James Sturtevant 2022-03-22 16:44:27 -07:00
parent 41501c4fcf
commit 61af67ef10
2 changed files with 120 additions and 69 deletions

View File

@ -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"

View File

@ -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
} }