1
16
17 package admission
18
19 import (
20 "bytes"
21 "context"
22 "crypto/rand"
23 "fmt"
24 "io"
25 "net/http"
26 "net/http/httptest"
27 "time"
28
29 . "github.com/onsi/ginkgo/v2"
30 . "github.com/onsi/gomega"
31
32 admissionv1 "k8s.io/api/admission/v1"
33 )
34
35 var _ = Describe("Admission Webhooks", func() {
36
37 const (
38 gvkJSONv1 = `"kind":"AdmissionReview","apiVersion":"admission.k8s.io/v1"`
39 gvkJSONv1beta1 = `"kind":"AdmissionReview","apiVersion":"admission.k8s.io/v1beta1"`
40 )
41
42 Describe("HTTP Handler", func() {
43 var respRecorder *httptest.ResponseRecorder
44 webhook := &Webhook{
45 Handler: nil,
46 }
47 BeforeEach(func() {
48 respRecorder = &httptest.ResponseRecorder{
49 Body: bytes.NewBuffer(nil),
50 }
51 })
52
53 It("should return bad-request when given an empty body", func() {
54 req := &http.Request{Body: nil}
55
56 expected := `{"response":{"uid":"","allowed":false,"status":{"metadata":{},"message":"request body is empty","code":400}}}
57 `
58 webhook.ServeHTTP(respRecorder, req)
59 Expect(respRecorder.Body.String()).To(Equal(expected))
60 })
61
62 It("should return bad-request when given the wrong content-type", func() {
63 req := &http.Request{
64 Header: http.Header{"Content-Type": []string{"application/foo"}},
65 Body: nopCloser{Reader: bytes.NewBuffer(nil)},
66 }
67
68 expected :=
69 `{"response":{"uid":"","allowed":false,"status":{"metadata":{},"message":"contentType=application/foo, expected application/json","code":400}}}
70 `
71 webhook.ServeHTTP(respRecorder, req)
72 Expect(respRecorder.Body.String()).To(Equal(expected))
73 })
74
75 It("should return bad-request when given an undecodable body", func() {
76 req := &http.Request{
77 Header: http.Header{"Content-Type": []string{"application/json"}},
78 Body: nopCloser{Reader: bytes.NewBufferString("{")},
79 }
80
81 expected :=
82 `{"response":{"uid":"","allowed":false,"status":{"metadata":{},"message":"couldn't get version/kind; json parse error: unexpected end of JSON input","code":400}}}
83 `
84 webhook.ServeHTTP(respRecorder, req)
85 Expect(respRecorder.Body.String()).To(Equal(expected))
86 })
87
88 It("should error when given a NoBody", func() {
89 req := &http.Request{
90 Header: http.Header{"Content-Type": []string{"application/json"}},
91 Method: http.MethodPost,
92 Body: http.NoBody,
93 }
94
95 expected := `{"response":{"uid":"","allowed":false,"status":{"metadata":{},"message":"request body is empty","code":400}}}
96 `
97 webhook.ServeHTTP(respRecorder, req)
98 Expect(respRecorder.Body.String()).To(Equal(expected))
99 })
100
101 It("should error when given an infinite body", func() {
102 req := &http.Request{
103 Header: http.Header{"Content-Type": []string{"application/json"}},
104 Method: http.MethodPost,
105 Body: nopCloser{Reader: rand.Reader},
106 }
107
108 expected := `{"response":{"uid":"","allowed":false,"status":{"metadata":{},"message":"request entity is too large; limit is 7340032 bytes","code":413}}}
109 `
110 webhook.ServeHTTP(respRecorder, req)
111 Expect(respRecorder.Body.String()).To(Equal(expected))
112 })
113
114 It("should return the response given by the handler with version defaulted to v1", func() {
115 req := &http.Request{
116 Header: http.Header{"Content-Type": []string{"application/json"}},
117 Body: nopCloser{Reader: bytes.NewBufferString(`{"request":{}}`)},
118 }
119 webhook := &Webhook{
120 Handler: &fakeHandler{},
121 }
122
123 expected := fmt.Sprintf(`{%s,"response":{"uid":"","allowed":true,"status":{"metadata":{},"code":200}}}
124 `, gvkJSONv1)
125 webhook.ServeHTTP(respRecorder, req)
126 Expect(respRecorder.Body.String()).To(Equal(expected))
127 })
128
129 It("should return the v1 response given by the handler", func() {
130 req := &http.Request{
131 Header: http.Header{"Content-Type": []string{"application/json"}},
132 Body: nopCloser{Reader: bytes.NewBufferString(fmt.Sprintf(`{%s,"request":{}}`, gvkJSONv1))},
133 }
134 webhook := &Webhook{
135 Handler: &fakeHandler{},
136 }
137
138 expected := fmt.Sprintf(`{%s,"response":{"uid":"","allowed":true,"status":{"metadata":{},"code":200}}}
139 `, gvkJSONv1)
140 webhook.ServeHTTP(respRecorder, req)
141 Expect(respRecorder.Body.String()).To(Equal(expected))
142 })
143
144 It("should return the v1beta1 response given by the handler", func() {
145 req := &http.Request{
146 Header: http.Header{"Content-Type": []string{"application/json"}},
147 Body: nopCloser{Reader: bytes.NewBufferString(fmt.Sprintf(`{%s,"request":{}}`, gvkJSONv1beta1))},
148 }
149 webhook := &Webhook{
150 Handler: &fakeHandler{},
151 }
152
153 expected := fmt.Sprintf(`{%s,"response":{"uid":"","allowed":true,"status":{"metadata":{},"code":200}}}
154 `, gvkJSONv1beta1)
155 webhook.ServeHTTP(respRecorder, req)
156 Expect(respRecorder.Body.String()).To(Equal(expected))
157 })
158
159 It("should present the Context from the HTTP request, if any", func() {
160 req := &http.Request{
161 Header: http.Header{"Content-Type": []string{"application/json"}},
162 Body: nopCloser{Reader: bytes.NewBufferString(`{"request":{}}`)},
163 }
164 type ctxkey int
165 const key ctxkey = 1
166 const value = "from-ctx"
167 webhook := &Webhook{
168 Handler: &fakeHandler{
169 fn: func(ctx context.Context, req Request) Response {
170 <-ctx.Done()
171 return Allowed(ctx.Value(key).(string))
172 },
173 },
174 }
175
176 expected := fmt.Sprintf(`{%s,"response":{"uid":"","allowed":true,"status":{"metadata":{},"message":%q,"code":200}}}
177 `, gvkJSONv1, value)
178
179 ctx, cancel := context.WithCancel(context.WithValue(context.Background(), key, value))
180 cancel()
181 webhook.ServeHTTP(respRecorder, req.WithContext(ctx))
182 Expect(respRecorder.Body.String()).To(Equal(expected))
183 })
184
185 It("should mutate the Context from the HTTP request, if func supplied", func() {
186 req := &http.Request{
187 Header: http.Header{"Content-Type": []string{"application/json"}},
188 Body: nopCloser{Reader: bytes.NewBufferString(`{"request":{}}`)},
189 }
190 type ctxkey int
191 const key ctxkey = 1
192 webhook := &Webhook{
193 Handler: &fakeHandler{
194 fn: func(ctx context.Context, req Request) Response {
195 return Allowed(ctx.Value(key).(string))
196 },
197 },
198 WithContextFunc: func(ctx context.Context, r *http.Request) context.Context {
199 return context.WithValue(ctx, key, r.Header["Content-Type"][0])
200 },
201 }
202
203 expected := fmt.Sprintf(`{%s,"response":{"uid":"","allowed":true,"status":{"metadata":{},"message":%q,"code":200}}}
204 `, gvkJSONv1, "application/json")
205
206 ctx, cancel := context.WithCancel(context.Background())
207 cancel()
208 webhook.ServeHTTP(respRecorder, req.WithContext(ctx))
209 Expect(respRecorder.Body.String()).To(Equal(expected))
210 })
211
212 It("should never run into circular calling if the writer has broken", func() {
213 req := &http.Request{
214 Header: http.Header{"Content-Type": []string{"application/json"}},
215 Body: nopCloser{Reader: bytes.NewBufferString(fmt.Sprintf(`{%s,"request":{}}`, gvkJSONv1))},
216 }
217 webhook := &Webhook{
218 Handler: &fakeHandler{},
219 }
220
221 bw := &brokenWriter{ResponseWriter: respRecorder}
222 Eventually(func() int {
223
224 webhook.ServeHTTP(bw, req)
225 return respRecorder.Body.Len()
226 }, time.Second*3).Should(Equal(0))
227 })
228 })
229 })
230
231 type nopCloser struct {
232 io.Reader
233 }
234
235 func (nopCloser) Close() error { return nil }
236
237 type fakeHandler struct {
238 invoked bool
239 fn func(context.Context, Request) Response
240 }
241
242 func (h *fakeHandler) Handle(ctx context.Context, req Request) Response {
243 h.invoked = true
244 if h.fn != nil {
245 return h.fn(ctx, req)
246 }
247 return Response{AdmissionResponse: admissionv1.AdmissionResponse{
248 Allowed: true,
249 }}
250 }
251
252 type brokenWriter struct {
253 http.ResponseWriter
254 }
255
256 func (bw *brokenWriter) Write(buf []byte) (int, error) {
257 return 0, fmt.Errorf("mock: write: broken pipe")
258 }
259
View as plain text