From 201111b644e5897243cad11d4d2eb375ee5f0c3f Mon Sep 17 00:00:00 2001 From: Chao Xu Date: Tue, 31 Jul 2018 16:20:44 -0700 Subject: [PATCH] Added e2e test to check admission webhook works for pods/attach. --- test/e2e/apimachinery/webhook.go | 106 ++++++++++++++++++++++++++++--- test/images/webhook/VERSION | 2 +- test/images/webhook/main.go | 7 +- test/images/webhook/pods.go | 38 +++++++++++ test/utils/image/manifest.go | 2 +- 5 files changed, 142 insertions(+), 13 deletions(-) diff --git a/test/e2e/apimachinery/webhook.go b/test/e2e/apimachinery/webhook.go index a50dc41f23..ef942e18ad 100644 --- a/test/e2e/apimachinery/webhook.go +++ b/test/e2e/apimachinery/webhook.go @@ -52,21 +52,23 @@ const ( roleBindingName = "webhook-auth-reader" // The webhook configuration names should not be reused between test instances. - crWebhookConfigName = "e2e-test-webhook-config-cr" - webhookConfigName = "e2e-test-webhook-config" - mutatingWebhookConfigName = "e2e-test-mutating-webhook-config" - podMutatingWebhookConfigName = "e2e-test-mutating-webhook-pod" - crMutatingWebhookConfigName = "e2e-test-mutating-webhook-config-cr" - webhookFailClosedConfigName = "e2e-test-webhook-fail-closed" - webhookForWebhooksConfigName = "e2e-test-webhook-for-webhooks-config" - removableValidatingHookName = "e2e-test-should-be-removable-validating-webhook-config" - removableMutatingHookName = "e2e-test-should-be-removable-mutating-webhook-config" - crdWebhookConfigName = "e2e-test-webhook-config-crd" + crWebhookConfigName = "e2e-test-webhook-config-cr" + webhookConfigName = "e2e-test-webhook-config" + attachingPodWebhookConfigName = "e2e-test-webhook-config-attaching-pod" + mutatingWebhookConfigName = "e2e-test-mutating-webhook-config" + podMutatingWebhookConfigName = "e2e-test-mutating-webhook-pod" + crMutatingWebhookConfigName = "e2e-test-mutating-webhook-config-cr" + webhookFailClosedConfigName = "e2e-test-webhook-fail-closed" + webhookForWebhooksConfigName = "e2e-test-webhook-for-webhooks-config" + removableValidatingHookName = "e2e-test-should-be-removable-validating-webhook-config" + removableMutatingHookName = "e2e-test-should-be-removable-mutating-webhook-config" + crdWebhookConfigName = "e2e-test-webhook-config-crd" skipNamespaceLabelKey = "skip-webhook-admission" skipNamespaceLabelValue = "yes" skippedNamespaceName = "exempted-namesapce" disallowedPodName = "disallowed-pod" + toBeAttachedPodName = "to-be-attached-pod" hangingPodName = "hanging-pod" disallowedConfigMapName = "disallowed-configmap" allowedConfigMapName = "allowed-configmap" @@ -117,6 +119,12 @@ var _ = SIGDescribe("AdmissionWebhook", func() { testWebhook(f) }) + It("Should be able to deny attaching pod", func() { + webhookCleanup := registerWebhookForAttachingPod(f, context) + defer webhookCleanup() + testAttachingPodWebhook(f) + }) + It("Should be able to deny custom resource creation", func() { testcrd, err := framework.CreateTestCRD(f) if err != nil { @@ -405,6 +413,53 @@ func registerWebhook(f *framework.Framework, context *certContext) func() { } } +func registerWebhookForAttachingPod(f *framework.Framework, context *certContext) func() { + client := f.ClientSet + By("Registering the webhook via the AdmissionRegistration API") + + namespace := f.Namespace.Name + configName := attachingPodWebhookConfigName + // A webhook that cannot talk to server, with fail-open policy + failOpenHook := failingWebhook(namespace, "fail-open.k8s.io") + policyIgnore := v1beta1.Ignore + failOpenHook.FailurePolicy = &policyIgnore + + _, err := client.AdmissionregistrationV1beta1().ValidatingWebhookConfigurations().Create(&v1beta1.ValidatingWebhookConfiguration{ + ObjectMeta: metav1.ObjectMeta{ + Name: configName, + }, + Webhooks: []v1beta1.Webhook{ + { + Name: "deny-attaching-pod.k8s.io", + Rules: []v1beta1.RuleWithOperations{{ + Operations: []v1beta1.OperationType{v1beta1.Connect}, + Rule: v1beta1.Rule{ + APIGroups: []string{""}, + APIVersions: []string{"v1"}, + Resources: []string{"pods/attach"}, + }, + }}, + ClientConfig: v1beta1.WebhookClientConfig{ + Service: &v1beta1.ServiceReference{ + Namespace: namespace, + Name: serviceName, + Path: strPtr("/pods/attach"), + }, + CABundle: context.signingCert, + }, + }, + }, + }) + framework.ExpectNoError(err, "registering webhook config %s with namespace %s", configName, namespace) + + // The webhook configuration is honored in 10s. + time.Sleep(10 * time.Second) + + return func() { + client.AdmissionregistrationV1beta1().ValidatingWebhookConfigurations().Delete(configName, nil) + } +} + func registerMutatingWebhookForConfigMap(f *framework.Framework, context *certContext) func() { client := f.ClientSet By("Registering the mutating configmap webhook via the AdmissionRegistration API") @@ -642,6 +697,21 @@ func testWebhook(f *framework.Framework) { Expect(err).To(BeNil()) } +func testAttachingPodWebhook(f *framework.Framework) { + By("create a pod") + client := f.ClientSet + pod := toBeAttachedPod(f) + _, err := client.CoreV1().Pods(f.Namespace.Name).Create(pod) + Expect(err).To(BeNil()) + + By("'kubectl attach' the pod, should be denied by the webhook") + _, err = framework.NewKubectlCommand("attach", fmt.Sprintf("--namespace=%v", f.Namespace.Name), pod.Name, "-i", "-c=container1").Exec() + Expect(err).NotTo(BeNil()) + if e, a := "attaching to pod 'to-be-attached-pod' is not allowed", err.Error(); !strings.Contains(a, e) { + framework.Failf("unexpected 'kubectl attach' error message. expected to contain %q, got %q", e, a) + } +} + // failingWebhook returns a webhook with rule of create configmaps, // but with an invalid client config so that server cannot communicate with it func failingWebhook(namespace, name string) v1beta1.Webhook { @@ -930,6 +1000,22 @@ func hangingPod(f *framework.Framework) *v1.Pod { } } +func toBeAttachedPod(f *framework.Framework) *v1.Pod { + return &v1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: toBeAttachedPodName, + }, + Spec: v1.PodSpec{ + Containers: []v1.Container{ + { + Name: "container1", + Image: imageutils.GetPauseImageName(), + }, + }, + }, + } +} + func nonCompliantConfigMap(f *framework.Framework) *v1.ConfigMap { return &v1.ConfigMap{ ObjectMeta: metav1.ObjectMeta{ diff --git a/test/images/webhook/VERSION b/test/images/webhook/VERSION index c4247db8f7..1b67ceb785 100644 --- a/test/images/webhook/VERSION +++ b/test/images/webhook/VERSION @@ -1 +1 @@ -1.12v1 +1.12v2 diff --git a/test/images/webhook/main.go b/test/images/webhook/main.go index d453932ed4..aca37e46ab 100644 --- a/test/images/webhook/main.go +++ b/test/images/webhook/main.go @@ -60,7 +60,7 @@ func serve(w http.ResponseWriter, r *http.Request, admit admitFunc) { return } - glog.V(2).Info(fmt.Sprintf("handling request: %v", body)) + glog.V(2).Info(fmt.Sprintf("handling request: %s", body)) // The AdmissionReview that was sent to the webhook requestedAdmissionReview := v1beta1.AdmissionReview{} @@ -99,6 +99,10 @@ func servePods(w http.ResponseWriter, r *http.Request) { serve(w, r, admitPods) } +func serveAttachingPods(w http.ResponseWriter, r *http.Request) { + serve(w, r, denySpecificAttachment) +} + func serveMutatePods(w http.ResponseWriter, r *http.Request) { serve(w, r, mutatePods) } @@ -130,6 +134,7 @@ func main() { http.HandleFunc("/always-deny", serveAlwaysDeny) http.HandleFunc("/pods", servePods) + http.HandleFunc("/pods/attach", serveAttachingPods) http.HandleFunc("/mutating-pods", serveMutatePods) http.HandleFunc("/configmaps", serveConfigmaps) http.HandleFunc("/mutating-configmaps", serveMutateConfigmaps) diff --git a/test/images/webhook/pods.go b/test/images/webhook/pods.go index 2a81850182..ee0bbb2e1e 100644 --- a/test/images/webhook/pods.go +++ b/test/images/webhook/pods.go @@ -101,3 +101,41 @@ func mutatePods(ar v1beta1.AdmissionReview) *v1beta1.AdmissionResponse { } return &reviewResponse } + +// denySpecificAttachment denies `kubectl attach to-be-attached-pod -i -c=container1" +// or equivalent client requests. +func denySpecificAttachment(ar v1beta1.AdmissionReview) *v1beta1.AdmissionResponse { + glog.V(2).Info("handling attaching pods") + if ar.Request.Name != "to-be-attached-pod" { + return &v1beta1.AdmissionResponse{Allowed: true} + } + podResource := metav1.GroupVersionResource{Group: "", Version: "v1", Resource: "pods"} + if e, a := podResource, ar.Request.Resource; e != a { + err := fmt.Errorf("expect resource to be %s, got %s", e, a) + glog.Error(err) + return toAdmissionResponse(err) + } + if e, a := "attach", ar.Request.SubResource; e != a { + err := fmt.Errorf("expect subresource to be %s, got %s", e, a) + glog.Error(err) + return toAdmissionResponse(err) + } + + raw := ar.Request.Object.Raw + podAttachOptions := corev1.PodAttachOptions{} + deserializer := codecs.UniversalDeserializer() + if _, _, err := deserializer.Decode(raw, nil, &podAttachOptions); err != nil { + glog.Error(err) + return toAdmissionResponse(err) + } + glog.V(2).Info(fmt.Sprintf("podAttachOptions=%#v\n", podAttachOptions)) + if !podAttachOptions.Stdin || podAttachOptions.Container != "container1" { + return &v1beta1.AdmissionResponse{Allowed: true} + } + return &v1beta1.AdmissionResponse{ + Allowed: false, + Result: &metav1.Status{ + Message: "attaching to pod 'to-be-attached-pod' is not allowed", + }, + } +} diff --git a/test/utils/image/manifest.go b/test/utils/image/manifest.go index f9dfc6a4c6..977cae3358 100644 --- a/test/utils/image/manifest.go +++ b/test/utils/image/manifest.go @@ -49,7 +49,7 @@ func (i *ImageConfig) SetVersion(version string) { } var ( - AdmissionWebhook = ImageConfig{e2eRegistry, "webhook", "1.12v1", false} + AdmissionWebhook = ImageConfig{e2eRegistry, "webhook", "1.12v2", false} APIServer = ImageConfig{e2eRegistry, "sample-apiserver", "1.0", false} AppArmorLoader = ImageConfig{gcRegistry, "apparmor-loader", "0.1", false} BusyBox = ImageConfig{gcRegistry, "busybox", "1.24", false}