1
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 })
269
View as plain text