Added e2e test to check admission webhook works for pods/attach.
This commit is contained in:
		| @@ -54,6 +54,7 @@ const ( | |||||||
| 	// The webhook configuration names should not be reused between test instances. | 	// The webhook configuration names should not be reused between test instances. | ||||||
| 	crWebhookConfigName           = "e2e-test-webhook-config-cr" | 	crWebhookConfigName           = "e2e-test-webhook-config-cr" | ||||||
| 	webhookConfigName             = "e2e-test-webhook-config" | 	webhookConfigName             = "e2e-test-webhook-config" | ||||||
|  | 	attachingPodWebhookConfigName = "e2e-test-webhook-config-attaching-pod" | ||||||
| 	mutatingWebhookConfigName     = "e2e-test-mutating-webhook-config" | 	mutatingWebhookConfigName     = "e2e-test-mutating-webhook-config" | ||||||
| 	podMutatingWebhookConfigName  = "e2e-test-mutating-webhook-pod" | 	podMutatingWebhookConfigName  = "e2e-test-mutating-webhook-pod" | ||||||
| 	crMutatingWebhookConfigName   = "e2e-test-mutating-webhook-config-cr" | 	crMutatingWebhookConfigName   = "e2e-test-mutating-webhook-config-cr" | ||||||
| @@ -67,6 +68,7 @@ const ( | |||||||
| 	skipNamespaceLabelValue = "yes" | 	skipNamespaceLabelValue = "yes" | ||||||
| 	skippedNamespaceName    = "exempted-namesapce" | 	skippedNamespaceName    = "exempted-namesapce" | ||||||
| 	disallowedPodName       = "disallowed-pod" | 	disallowedPodName       = "disallowed-pod" | ||||||
|  | 	toBeAttachedPodName     = "to-be-attached-pod" | ||||||
| 	hangingPodName          = "hanging-pod" | 	hangingPodName          = "hanging-pod" | ||||||
| 	disallowedConfigMapName = "disallowed-configmap" | 	disallowedConfigMapName = "disallowed-configmap" | ||||||
| 	allowedConfigMapName    = "allowed-configmap" | 	allowedConfigMapName    = "allowed-configmap" | ||||||
| @@ -117,6 +119,12 @@ var _ = SIGDescribe("AdmissionWebhook", func() { | |||||||
| 		testWebhook(f) | 		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() { | 	It("Should be able to deny custom resource creation", func() { | ||||||
| 		testcrd, err := framework.CreateTestCRD(f) | 		testcrd, err := framework.CreateTestCRD(f) | ||||||
| 		if err != nil { | 		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() { | func registerMutatingWebhookForConfigMap(f *framework.Framework, context *certContext) func() { | ||||||
| 	client := f.ClientSet | 	client := f.ClientSet | ||||||
| 	By("Registering the mutating configmap webhook via the AdmissionRegistration API") | 	By("Registering the mutating configmap webhook via the AdmissionRegistration API") | ||||||
| @@ -642,6 +697,21 @@ func testWebhook(f *framework.Framework) { | |||||||
| 	Expect(err).To(BeNil()) | 	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, | // failingWebhook returns a webhook with rule of create configmaps, | ||||||
| // but with an invalid client config so that server cannot communicate with it | // but with an invalid client config so that server cannot communicate with it | ||||||
| func failingWebhook(namespace, name string) v1beta1.Webhook { | 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 { | func nonCompliantConfigMap(f *framework.Framework) *v1.ConfigMap { | ||||||
| 	return &v1.ConfigMap{ | 	return &v1.ConfigMap{ | ||||||
| 		ObjectMeta: metav1.ObjectMeta{ | 		ObjectMeta: metav1.ObjectMeta{ | ||||||
|   | |||||||
| @@ -1 +1 @@ | |||||||
| 1.12v1 | 1.12v2 | ||||||
|   | |||||||
| @@ -60,7 +60,7 @@ func serve(w http.ResponseWriter, r *http.Request, admit admitFunc) { | |||||||
| 		return | 		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 | 	// The AdmissionReview that was sent to the webhook | ||||||
| 	requestedAdmissionReview := v1beta1.AdmissionReview{} | 	requestedAdmissionReview := v1beta1.AdmissionReview{} | ||||||
| @@ -99,6 +99,10 @@ func servePods(w http.ResponseWriter, r *http.Request) { | |||||||
| 	serve(w, r, admitPods) | 	serve(w, r, admitPods) | ||||||
| } | } | ||||||
|  |  | ||||||
|  | func serveAttachingPods(w http.ResponseWriter, r *http.Request) { | ||||||
|  | 	serve(w, r, denySpecificAttachment) | ||||||
|  | } | ||||||
|  |  | ||||||
| func serveMutatePods(w http.ResponseWriter, r *http.Request) { | func serveMutatePods(w http.ResponseWriter, r *http.Request) { | ||||||
| 	serve(w, r, mutatePods) | 	serve(w, r, mutatePods) | ||||||
| } | } | ||||||
| @@ -130,6 +134,7 @@ func main() { | |||||||
|  |  | ||||||
| 	http.HandleFunc("/always-deny", serveAlwaysDeny) | 	http.HandleFunc("/always-deny", serveAlwaysDeny) | ||||||
| 	http.HandleFunc("/pods", servePods) | 	http.HandleFunc("/pods", servePods) | ||||||
|  | 	http.HandleFunc("/pods/attach", serveAttachingPods) | ||||||
| 	http.HandleFunc("/mutating-pods", serveMutatePods) | 	http.HandleFunc("/mutating-pods", serveMutatePods) | ||||||
| 	http.HandleFunc("/configmaps", serveConfigmaps) | 	http.HandleFunc("/configmaps", serveConfigmaps) | ||||||
| 	http.HandleFunc("/mutating-configmaps", serveMutateConfigmaps) | 	http.HandleFunc("/mutating-configmaps", serveMutateConfigmaps) | ||||||
|   | |||||||
| @@ -101,3 +101,41 @@ func mutatePods(ar v1beta1.AdmissionReview) *v1beta1.AdmissionResponse { | |||||||
| 	} | 	} | ||||||
| 	return &reviewResponse | 	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", | ||||||
|  | 		}, | ||||||
|  | 	} | ||||||
|  | } | ||||||
|   | |||||||
| @@ -49,7 +49,7 @@ func (i *ImageConfig) SetVersion(version string) { | |||||||
| } | } | ||||||
|  |  | ||||||
| var ( | var ( | ||||||
| 	AdmissionWebhook         = ImageConfig{e2eRegistry, "webhook", "1.12v1", false} | 	AdmissionWebhook         = ImageConfig{e2eRegistry, "webhook", "1.12v2", false} | ||||||
| 	APIServer                = ImageConfig{e2eRegistry, "sample-apiserver", "1.0", false} | 	APIServer                = ImageConfig{e2eRegistry, "sample-apiserver", "1.0", false} | ||||||
| 	AppArmorLoader           = ImageConfig{gcRegistry, "apparmor-loader", "0.1", false} | 	AppArmorLoader           = ImageConfig{gcRegistry, "apparmor-loader", "0.1", false} | ||||||
| 	BusyBox                  = ImageConfig{gcRegistry, "busybox", "1.24", false} | 	BusyBox                  = ImageConfig{gcRegistry, "busybox", "1.24", false} | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user
	 Chao Xu
					Chao Xu