sigs.k8s.io/controller-runtime@v0.18.2/pkg/webhook/admission/webhook_test.go (about) 1 /* 2 Copyright 2018 The Kubernetes Authors. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 package admission 18 19 import ( 20 "context" 21 "io" 22 "net/http" 23 24 "github.com/go-logr/logr" 25 . "github.com/onsi/ginkgo/v2" 26 . "github.com/onsi/gomega" 27 "github.com/onsi/gomega/gbytes" 28 "gomodules.xyz/jsonpatch/v2" 29 admissionv1 "k8s.io/api/admission/v1" 30 authenticationv1 "k8s.io/api/authentication/v1" 31 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 32 machinerytypes "k8s.io/apimachinery/pkg/types" 33 34 logf "sigs.k8s.io/controller-runtime/pkg/log" 35 "sigs.k8s.io/controller-runtime/pkg/log/zap" 36 ) 37 38 var _ = Describe("Admission Webhooks", func() { 39 var ( 40 logBuffer *gbytes.Buffer 41 testLogger logr.Logger 42 ) 43 44 BeforeEach(func() { 45 logBuffer = gbytes.NewBuffer() 46 testLogger = zap.New(zap.JSONEncoder(), zap.WriteTo(io.MultiWriter(logBuffer, GinkgoWriter))) 47 }) 48 49 allowHandler := func() *Webhook { 50 handler := &fakeHandler{ 51 fn: func(ctx context.Context, req Request) Response { 52 return Response{ 53 AdmissionResponse: admissionv1.AdmissionResponse{ 54 Allowed: true, 55 }, 56 } 57 }, 58 } 59 webhook := &Webhook{ 60 Handler: handler, 61 } 62 63 return webhook 64 } 65 66 It("should invoke the handler to get a response", func() { 67 By("setting up a webhook with an allow handler") 68 webhook := allowHandler() 69 70 By("invoking the webhook") 71 resp := webhook.Handle(context.Background(), Request{}) 72 73 By("checking that it allowed the request") 74 Expect(resp.Allowed).To(BeTrue()) 75 }) 76 77 It("should ensure that the response's UID is set to the request's UID", func() { 78 By("setting up a webhook") 79 webhook := allowHandler() 80 81 By("invoking the webhook") 82 resp := webhook.Handle(context.Background(), Request{AdmissionRequest: admissionv1.AdmissionRequest{UID: "foobar"}}) 83 84 By("checking that the response share's the request's UID") 85 Expect(resp.UID).To(Equal(machinerytypes.UID("foobar"))) 86 }) 87 88 It("should populate the status on a response if one is not provided", func() { 89 By("setting up a webhook") 90 webhook := allowHandler() 91 92 By("invoking the webhook") 93 resp := webhook.Handle(context.Background(), Request{}) 94 95 By("checking that the response share's the request's UID") 96 Expect(resp.Result).To(Equal(&metav1.Status{Code: http.StatusOK})) 97 }) 98 99 It("shouldn't overwrite the status on a response", func() { 100 By("setting up a webhook that sets a status") 101 webhook := &Webhook{ 102 Handler: HandlerFunc(func(ctx context.Context, req Request) Response { 103 return Response{ 104 AdmissionResponse: admissionv1.AdmissionResponse{ 105 Allowed: true, 106 Result: &metav1.Status{Message: "Ground Control to Major Tom"}, 107 }, 108 } 109 }), 110 } 111 112 By("invoking the webhook") 113 resp := webhook.Handle(context.Background(), Request{}) 114 115 By("checking that the message is intact") 116 Expect(resp.Result).NotTo(BeNil()) 117 Expect(resp.Result.Message).To(Equal("Ground Control to Major Tom")) 118 }) 119 120 It("should serialize patch operations into a single jsonpatch blob", func() { 121 By("setting up a webhook with a patching handler") 122 webhook := &Webhook{ 123 Handler: HandlerFunc(func(ctx context.Context, req Request) Response { 124 return Patched("", jsonpatch.Operation{Operation: "add", Path: "/a", Value: 2}, jsonpatch.Operation{Operation: "replace", Path: "/b", Value: 4}) 125 }), 126 } 127 128 By("invoking the webhook") 129 resp := webhook.Handle(context.Background(), Request{}) 130 131 By("checking that a JSON patch is populated on the response") 132 patchType := admissionv1.PatchTypeJSONPatch 133 Expect(resp.PatchType).To(Equal(&patchType)) 134 Expect(resp.Patch).To(Equal([]byte(`[{"op":"add","path":"/a","value":2},{"op":"replace","path":"/b","value":4}]`))) 135 }) 136 137 It("should pass a request logger via the context", func() { 138 By("setting up a webhook that uses the request logger") 139 webhook := &Webhook{ 140 Handler: HandlerFunc(func(ctx context.Context, req Request) Response { 141 logf.FromContext(ctx).Info("Received request") 142 143 return Response{ 144 AdmissionResponse: admissionv1.AdmissionResponse{ 145 Allowed: true, 146 }, 147 } 148 }), 149 log: testLogger, 150 } 151 152 By("invoking the webhook") 153 resp := webhook.Handle(context.Background(), Request{AdmissionRequest: admissionv1.AdmissionRequest{ 154 UID: "test123", 155 Name: "foo", 156 Namespace: "bar", 157 Resource: metav1.GroupVersionResource{ 158 Group: "apps", 159 Version: "v1", 160 Resource: "deployments", 161 }, 162 UserInfo: authenticationv1.UserInfo{ 163 Username: "tim", 164 }, 165 }}) 166 Expect(resp.Allowed).To(BeTrue()) 167 168 By("checking that the log message contains the request fields") 169 Eventually(logBuffer).Should(gbytes.Say(`"msg":"Received request","object":{"name":"foo","namespace":"bar"},"namespace":"bar","name":"foo","resource":{"group":"apps","version":"v1","resource":"deployments"},"user":"tim","requestID":"test123"}`)) 170 }) 171 172 It("should pass a request logger created by LogConstructor via the context", func() { 173 By("setting up a webhook that uses the request logger") 174 webhook := &Webhook{ 175 Handler: HandlerFunc(func(ctx context.Context, req Request) Response { 176 logf.FromContext(ctx).Info("Received request") 177 178 return Response{ 179 AdmissionResponse: admissionv1.AdmissionResponse{ 180 Allowed: true, 181 }, 182 } 183 }), 184 LogConstructor: func(base logr.Logger, req *Request) logr.Logger { 185 return base.WithValues("operation", req.Operation, "requestID", req.UID) 186 }, 187 log: testLogger, 188 } 189 190 By("invoking the webhook") 191 resp := webhook.Handle(context.Background(), Request{AdmissionRequest: admissionv1.AdmissionRequest{ 192 UID: "test123", 193 Operation: admissionv1.Create, 194 }}) 195 Expect(resp.Allowed).To(BeTrue()) 196 197 By("checking that the log message contains the request fields") 198 Eventually(logBuffer).Should(gbytes.Say(`"msg":"Received request","operation":"CREATE","requestID":"test123"}`)) 199 }) 200 201 Describe("panic recovery", func() { 202 It("should recover panic if RecoverPanic is true", func() { 203 panicHandler := func() *Webhook { 204 handler := &fakeHandler{ 205 fn: func(ctx context.Context, req Request) Response { 206 panic("fake panic test") 207 }, 208 } 209 webhook := &Webhook{ 210 Handler: handler, 211 RecoverPanic: true, 212 } 213 214 return webhook 215 } 216 217 By("setting up a webhook with a panicking handler") 218 webhook := panicHandler() 219 220 By("invoking the webhook") 221 resp := webhook.Handle(context.Background(), Request{}) 222 223 By("checking that it errored the request") 224 Expect(resp.Allowed).To(BeFalse()) 225 Expect(resp.Result.Code).To(Equal(int32(http.StatusInternalServerError))) 226 Expect(resp.Result.Message).To(Equal("panic: fake panic test [recovered]")) 227 }) 228 229 It("should not recover panic if RecoverPanic is false by default", func() { 230 panicHandler := func() *Webhook { 231 handler := &fakeHandler{ 232 fn: func(ctx context.Context, req Request) Response { 233 panic("fake panic test") 234 }, 235 } 236 webhook := &Webhook{ 237 Handler: handler, 238 } 239 240 return webhook 241 } 242 243 By("setting up a webhook with a panicking handler") 244 defer func() { 245 Expect(recover()).ShouldNot(BeNil()) 246 }() 247 webhook := panicHandler() 248 249 By("invoking the webhook") 250 webhook.Handle(context.Background(), Request{}) 251 }) 252 }) 253 }) 254 255 var _ = Describe("Should be able to write/read admission.Request to/from context", func() { 256 ctx := context.Background() 257 testRequest := Request{ 258 admissionv1.AdmissionRequest{ 259 UID: "test-uid", 260 }, 261 } 262 263 ctx = NewContextWithRequest(ctx, testRequest) 264 265 gotRequest, err := RequestFromContext(ctx) 266 Expect(err).To(Not(HaveOccurred())) 267 Expect(gotRequest).To(Equal(testRequest)) 268 })