Skip to content

Commit 90fbbee

Browse files
authored
Merge pull request kubernetes#76910 from liggitt/pod-admission
webhook admission tests: connect, proxy, binding, eviction
2 parents bd12b01 + a4576ec commit 90fbbee

File tree

4 files changed

+173
-12
lines changed

4 files changed

+173
-12
lines changed

pkg/registry/core/pod/storage/eviction.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,13 @@ func (r *EvictionREST) Create(ctx context.Context, obj runtime.Object, createVal
8787
return nil, err
8888
}
8989
pod := obj.(*api.Pod)
90+
91+
if createValidation != nil {
92+
if err := createValidation(eviction); err != nil {
93+
return nil, err
94+
}
95+
}
96+
9097
// Evicting a terminal pod should result in direct deletion of pod as it already caused disruption by the time we are evicting.
9198
// There is no need to check for pdb.
9299
if pod.Status.Phase == api.PodSucceeded || pod.Status.Phase == api.PodFailed {

pkg/registry/core/pod/storage/storage.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,12 @@ func (r *BindingREST) Create(ctx context.Context, obj runtime.Object, createVali
149149
return nil, errs.ToAggregate()
150150
}
151151

152+
if createValidation != nil {
153+
if err := createValidation(binding); err != nil {
154+
return nil, err
155+
}
156+
}
157+
152158
err = r.assignPod(ctx, binding.Name, binding.Target.Name, binding.Annotations, dryrun.IsDryRun(options.DryRun))
153159
out = &metav1.Status{Status: metav1.StatusSuccess}
154160
return

test/integration/apiserver/admissionwebhook/BUILD

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ go_test(
1717
"//staging/src/k8s.io/api/apps/v1beta1:go_default_library",
1818
"//staging/src/k8s.io/api/core/v1:go_default_library",
1919
"//staging/src/k8s.io/api/extensions/v1beta1:go_default_library",
20+
"//staging/src/k8s.io/api/policy/v1beta1:go_default_library",
2021
"//staging/src/k8s.io/apimachinery/pkg/api/errors:go_default_library",
2122
"//staging/src/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
2223
"//staging/src/k8s.io/apimachinery/pkg/apis/meta/v1/unstructured:go_default_library",

test/integration/apiserver/admissionwebhook/admission_test.go

Lines changed: 159 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,10 @@ import (
3333
"k8s.io/api/admission/v1beta1"
3434
admissionv1beta1 "k8s.io/api/admissionregistration/v1beta1"
3535
appsv1beta1 "k8s.io/api/apps/v1beta1"
36+
corev1 "k8s.io/api/core/v1"
3637
v1 "k8s.io/api/core/v1"
3738
extensionsv1beta1 "k8s.io/api/extensions/v1beta1"
39+
policyv1beta1 "k8s.io/api/policy/v1beta1"
3840
"k8s.io/apimachinery/pkg/api/errors"
3941
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
4042
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
@@ -44,6 +46,7 @@ import (
4446
"k8s.io/apimachinery/pkg/util/sets"
4547
"k8s.io/apimachinery/pkg/util/wait"
4648
dynamic "k8s.io/client-go/dynamic"
49+
"k8s.io/client-go/kubernetes"
4750
clientset "k8s.io/client-go/kubernetes"
4851
"k8s.io/client-go/util/retry"
4952
"k8s.io/kubernetes/cmd/kube-apiserver/app/options"
@@ -63,6 +66,8 @@ type testContext struct {
6366
admissionHolder *holder
6467

6568
client dynamic.Interface
69+
clientset kubernetes.Interface
70+
verb string
6671
gvr schema.GroupVersionResource
6772
resource metav1.APIResource
6873
resources map[schema.GroupVersionResource]metav1.APIResource
@@ -90,9 +95,22 @@ var (
9095

9196
// customTestFuncs holds custom test functions by resource and verb.
9297
customTestFuncs = map[schema.GroupVersionResource]map[string]testFunc{
93-
gvr("", "v1", "namespaces"): {"delete": testNamespaceDelete},
98+
gvr("", "v1", "namespaces"): {"delete": testNamespaceDelete},
99+
94100
gvr("apps", "v1beta1", "deployments/rollback"): {"create": testDeploymentRollback},
95101
gvr("extensions", "v1beta1", "deployments/rollback"): {"create": testDeploymentRollback},
102+
103+
gvr("", "v1", "pods/attach"): {"create": testPodConnectSubresource},
104+
gvr("", "v1", "pods/exec"): {"create": testPodConnectSubresource},
105+
gvr("", "v1", "pods/portforward"): {"create": testPodConnectSubresource},
106+
107+
gvr("", "v1", "bindings"): {"create": testPodBindingEviction},
108+
gvr("", "v1", "pods/binding"): {"create": testPodBindingEviction},
109+
gvr("", "v1", "pods/eviction"): {"create": testPodBindingEviction},
110+
111+
gvr("", "v1", "nodes/proxy"): {"*": testSubresourceProxy},
112+
gvr("", "v1", "pods/proxy"): {"*": testSubresourceProxy},
113+
gvr("", "v1", "services/proxy"): {"*": testSubresourceProxy},
96114
}
97115

98116
// excludedResources lists resources / verb combinations that are not yet tested. this set should trend to zero.
@@ -112,17 +130,6 @@ var (
112130
// TODO: webhook config objects are not subject to admission, verify CRUD works and webhooks do not observe them
113131
gvr("admissionregistration.k8s.io", "v1beta1", "mutatingwebhookconfigurations"): sets.NewString("*"),
114132
gvr("admissionregistration.k8s.io", "v1beta1", "validatingwebhookconfigurations"): sets.NewString("*"),
115-
116-
// TODO: implement custom subresource tests (requires special states or requests)
117-
gvr("", "v1", "bindings"): sets.NewString("create"),
118-
gvr("", "v1", "nodes/proxy"): sets.NewString("*"),
119-
gvr("", "v1", "pods/attach"): sets.NewString("create"),
120-
gvr("", "v1", "pods/binding"): sets.NewString("create"),
121-
gvr("", "v1", "pods/eviction"): sets.NewString("create"),
122-
gvr("", "v1", "pods/exec"): sets.NewString("create"),
123-
gvr("", "v1", "pods/portforward"): sets.NewString("create"),
124-
gvr("", "v1", "pods/proxy"): sets.NewString("*"),
125-
gvr("", "v1", "services/proxy"): sets.NewString("*"),
126133
}
127134

128135
parentResources = map[schema.GroupVersionResource]schema.GroupVersionResource{
@@ -393,6 +400,8 @@ func TestWebhookV1beta1(t *testing.T) {
393400
t: t,
394401
admissionHolder: holder,
395402
client: dynamicClient,
403+
clientset: master.Client,
404+
verb: verb,
396405
gvr: gvr,
397406
resource: resource,
398407
resources: resourcesByGVR,
@@ -735,6 +744,141 @@ func testDeploymentRollback(c *testContext) {
735744
}
736745
}
737746

747+
// testPodConnectSubresource verifies connect subresources
748+
func testPodConnectSubresource(c *testContext) {
749+
podGVR := gvr("", "v1", "pods")
750+
pod, err := createOrGetResource(c.client, podGVR, c.resources[podGVR])
751+
if err != nil {
752+
c.t.Error(err)
753+
return
754+
}
755+
756+
// check all upgradeable verbs
757+
for _, httpMethod := range []string{"GET", "POST"} {
758+
c.t.Logf("verifying %v", httpMethod)
759+
760+
c.admissionHolder.expect(c.gvr, gvk(c.resource.Group, c.resource.Version, c.resource.Kind), v1beta1.Connect, pod.GetName(), pod.GetNamespace(), true, false)
761+
var err error
762+
switch c.gvr {
763+
case gvr("", "v1", "pods/exec"):
764+
err = c.clientset.CoreV1().RESTClient().Verb(httpMethod).Namespace(pod.GetNamespace()).Resource("pods").Name(pod.GetName()).SubResource("exec").Do().Error()
765+
case gvr("", "v1", "pods/attach"):
766+
err = c.clientset.CoreV1().RESTClient().Verb(httpMethod).Namespace(pod.GetNamespace()).Resource("pods").Name(pod.GetName()).SubResource("attach").Do().Error()
767+
case gvr("", "v1", "pods/portforward"):
768+
err = c.clientset.CoreV1().RESTClient().Verb(httpMethod).Namespace(pod.GetNamespace()).Resource("pods").Name(pod.GetName()).SubResource("portforward").Do().Error()
769+
default:
770+
c.t.Errorf("unknown subresource %#v", c.gvr)
771+
return
772+
}
773+
774+
if err != nil {
775+
c.t.Logf("debug: result of subresource connect: %v", err)
776+
}
777+
c.admissionHolder.verify(c.t)
778+
779+
}
780+
}
781+
782+
// testPodBindingEviction verifies pod binding and eviction admission
783+
func testPodBindingEviction(c *testContext) {
784+
podGVR := gvr("", "v1", "pods")
785+
pod, err := createOrGetResource(c.client, podGVR, c.resources[podGVR])
786+
if err != nil {
787+
c.t.Error(err)
788+
return
789+
}
790+
791+
background := metav1.DeletePropagationBackground
792+
zero := int64(0)
793+
forceDelete := &metav1.DeleteOptions{GracePeriodSeconds: &zero, PropagationPolicy: &background}
794+
defer func() {
795+
err := c.clientset.CoreV1().Pods(pod.GetNamespace()).Delete(pod.GetName(), forceDelete)
796+
if err != nil && !errors.IsNotFound(err) {
797+
c.t.Error(err)
798+
return
799+
}
800+
}()
801+
802+
c.admissionHolder.expect(c.gvr, gvk(c.resource.Group, c.resource.Version, c.resource.Kind), v1beta1.Create, pod.GetName(), pod.GetNamespace(), true, false)
803+
804+
switch c.gvr {
805+
case gvr("", "v1", "bindings"):
806+
err = c.clientset.CoreV1().RESTClient().Post().Namespace(pod.GetNamespace()).Resource("bindings").Body(&corev1.Binding{
807+
ObjectMeta: metav1.ObjectMeta{Name: pod.GetName()},
808+
Target: corev1.ObjectReference{Name: "foo", Kind: "Node", APIVersion: "v1"},
809+
}).Do().Error()
810+
811+
case gvr("", "v1", "pods/binding"):
812+
err = c.clientset.CoreV1().RESTClient().Post().Namespace(pod.GetNamespace()).Resource("pods").Name(pod.GetName()).SubResource("binding").Body(&corev1.Binding{
813+
ObjectMeta: metav1.ObjectMeta{Name: pod.GetName()},
814+
Target: corev1.ObjectReference{Name: "foo", Kind: "Node", APIVersion: "v1"},
815+
}).Do().Error()
816+
817+
case gvr("", "v1", "pods/eviction"):
818+
err = c.clientset.CoreV1().RESTClient().Post().Namespace(pod.GetNamespace()).Resource("pods").Name(pod.GetName()).SubResource("eviction").Body(&policyv1beta1.Eviction{
819+
ObjectMeta: metav1.ObjectMeta{Name: pod.GetName()},
820+
DeleteOptions: forceDelete,
821+
}).Do().Error()
822+
823+
default:
824+
c.t.Errorf("unhandled resource %#v", c.gvr)
825+
return
826+
}
827+
828+
if err != nil {
829+
c.t.Error(err)
830+
return
831+
}
832+
}
833+
834+
// testSubresourceProxy verifies proxy subresources
835+
func testSubresourceProxy(c *testContext) {
836+
parentGVR := getParentGVR(c.gvr)
837+
parentResource := c.resources[parentGVR]
838+
obj, err := createOrGetResource(c.client, parentGVR, parentResource)
839+
if err != nil {
840+
c.t.Error(err)
841+
return
842+
}
843+
844+
gvrWithoutSubresources := c.gvr
845+
gvrWithoutSubresources.Resource = strings.Split(gvrWithoutSubresources.Resource, "/")[0]
846+
subresources := strings.Split(c.gvr.Resource, "/")[1:]
847+
848+
verbToHTTPMethods := map[string][]string{
849+
"create": {"POST", "GET", "HEAD", "OPTIONS"}, // also test read-only verbs map to Connect admission
850+
"update": {"PUT"},
851+
"patch": {"PATCH"},
852+
"delete": {"DELETE"},
853+
}
854+
httpMethodsToTest, ok := verbToHTTPMethods[c.verb]
855+
if !ok {
856+
c.t.Errorf("unknown verb %v", c.verb)
857+
return
858+
}
859+
860+
for _, httpMethod := range httpMethodsToTest {
861+
c.t.Logf("testing %v", httpMethod)
862+
request := c.clientset.CoreV1().RESTClient().Verb(httpMethod)
863+
864+
// add the namespace if required
865+
if len(obj.GetNamespace()) > 0 {
866+
request = request.Namespace(obj.GetNamespace())
867+
}
868+
869+
// set expectations
870+
c.admissionHolder.expect(c.gvr, gvk(c.resource.Group, c.resource.Version, c.resource.Kind), v1beta1.Connect, obj.GetName(), obj.GetNamespace(), true, false)
871+
// run the request. we don't actually care if the request is successful, just that admission gets called as expected
872+
err = request.Resource(gvrWithoutSubresources.Resource).Name(obj.GetName()).SubResource(subresources...).Do().Error()
873+
if err != nil {
874+
c.t.Logf("debug: result of subresource proxy (error expected): %v", err)
875+
}
876+
// verify the result
877+
c.admissionHolder.verify(c.t)
878+
}
879+
880+
}
881+
738882
//
739883
// utility methods
740884
//
@@ -808,6 +952,9 @@ func getTestFunc(gvr schema.GroupVersionResource, verb string) testFunc {
808952
if f, found := customTestFuncs[gvr][verb]; found {
809953
return f
810954
}
955+
if f, found := customTestFuncs[gvr]["*"]; found {
956+
return f
957+
}
811958
if strings.Contains(gvr.Resource, "/") {
812959
if f, found := defaultSubresourceFuncs[verb]; found {
813960
return f

0 commit comments

Comments
 (0)