sigs.k8s.io/controller-runtime@v0.18.2/pkg/webhook/admission/http_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 "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 // This should not be blocked by the circular calling of writeResponse and writeAdmissionResponse 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 }