1
16
17 package admission
18
19 import (
20 "bytes"
21 "context"
22 "fmt"
23 "net/http"
24 "net/http/httptest"
25 "testing"
26
27 "github.com/lithammer/dedent"
28 "github.com/stretchr/testify/assert"
29 "github.com/stretchr/testify/require"
30 admission "k8s.io/api/admission/v1"
31 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
32 )
33
34 var decoder = codecs.UniversalDeserializer()
35
36 func TestServeHTTPInvalidBody(t *testing.T) {
37 assert := assert.New(t)
38 res := httptest.NewRecorder()
39 handler := http.HandlerFunc(ServeHTTP)
40 req, err := http.NewRequest("POST", "", nil)
41 req = req.WithContext(context.Background())
42 assert.Nil(err)
43 handler.ServeHTTP(res, req)
44 assert.Equal(400, res.Code)
45 assert.Equal("admission review object is missing\n",
46 res.Body.String())
47 }
48
49 func TestServeHTTPInvalidMethod(t *testing.T) {
50 assert := assert.New(t)
51 res := httptest.NewRecorder()
52 handler := http.HandlerFunc(ServeHTTP)
53 req, err := http.NewRequest("GET", "", nil)
54 req = req.WithContext(context.Background())
55 assert.Nil(err)
56 handler.ServeHTTP(res, req)
57 assert.Equal(http.StatusMethodNotAllowed, res.Code)
58 assert.Equal("invalid method GET, only POST requests are allowed\n",
59 res.Body.String())
60 }
61
62 func TestServeHTTPSubmissions(t *testing.T) {
63 for _, apiVersion := range []string{
64 "admission.k8s.io/v1",
65 "admission.k8s.io/v1",
66 } {
67 for _, tt := range []struct {
68 name string
69 reqBody string
70
71 wantRespCode int
72 wantSuccessResponse admission.AdmissionResponse
73 wantFailureMessage string
74 }{
75 {
76 name: "malformed json missing colon at resource",
77 reqBody: dedent.Dedent(`{
78 "kind": "AdmissionReview",
79 "apiVersion": "` + apiVersion + `",
80 "request": {
81 "uid": "7313cd05-eddc-4150-b88c-971a0d53b2ab",
82 "resource": {
83 "group": "networking.x-k8s.io",
84 "version": "v1alpha1",
85 "resource" "httproutes"
86 },
87 "object": {
88 "apiVersion": "networking.x-k8s.io/v1alpha1",
89 "kind": "HTTPRoute"
90 },
91 "operation": "CREATE"
92 }
93 }`),
94 wantRespCode: http.StatusBadRequest,
95 wantFailureMessage: "invalid character '\"' after object key\n",
96 },
97 {
98 name: "request with empty body",
99 wantRespCode: http.StatusBadRequest,
100 wantFailureMessage: "unexpected end of JSON input\n",
101 },
102 {
103 name: "valid json but not of kind AdmissionReview",
104 reqBody: dedent.Dedent(`{
105 "kind": "NotReviewYouAreLookingFor",
106 "apiVersion": "` + apiVersion + `",
107 "request": {
108 "uid": "7313cd05-eddc-4150-b88c-971a0d53b2ab",
109 "resource": {
110 "group": "gateway.networking.k8s.io",
111 "version": "v1",
112 "resource": "httproutes"
113 },
114 "object": {
115 "apiVersion": "gateway.networking.k8s.io/v1",
116 "kind": "HTTPRoute"
117 },
118 "operation": "CREATE"
119 }
120 }`),
121 wantRespCode: http.StatusBadRequest,
122 wantFailureMessage: "submitted object is not of kind AdmissionReview\n",
123 },
124 {
125 name: "valid v1 Gateway resource",
126 reqBody: dedent.Dedent(`{
127 "kind": "AdmissionReview",
128 "apiVersion": "` + apiVersion + `",
129 "request": {
130 "uid": "7313cd05-eddc-4150-b88c-971a0d53b2ab",
131 "resource": {
132 "group": "gateway.networking.k8s.io",
133 "version": "v1",
134 "resource": "gateways"
135 },
136 "object": {
137 "kind": "Gateway",
138 "apiVersion": "gateway.networking.k8s.io/v1",
139 "metadata": {
140 "name": "gateway-1",
141 "labels": {
142 "app": "foo"
143 }
144 },
145 "spec": {
146 "gatewayClassName": "contour-class",
147 "listeners": [
148 {
149 "port": 80,
150 "protocol": "HTTP",
151 "hostname": "foo.com",
152 "routes": {
153 "group": "gateway.networking.k8s.io",
154 "kind": "HTTPRoute",
155 "namespaces": {
156 "from": "All"
157 }
158 }
159 }
160 ]
161 }
162 },
163 "operation": "CREATE"
164 }
165 }`),
166 wantRespCode: http.StatusOK,
167 wantSuccessResponse: admission.AdmissionResponse{
168 UID: "7313cd05-eddc-4150-b88c-971a0d53b2ab",
169 Allowed: true,
170 Result: &metav1.Status{},
171 },
172 },
173 {
174 name: "valid v1 HTTPRoute resource",
175 reqBody: dedent.Dedent(`{
176 "kind": "AdmissionReview",
177 "apiVersion": "` + apiVersion + `",
178 "request": {
179 "uid": "7313cd05-eddc-4150-b88c-971a0d53b2ab",
180 "resource": {
181 "group": "gateway.networking.k8s.io",
182 "version": "v1",
183 "resource": "httproutes"
184 },
185 "object": {
186 "kind": "HTTPRoute",
187 "apiVersion": "gateway.networking.k8s.io/v1",
188 "metadata": {
189 "name": "http-app-1",
190 "labels": {
191 "app": "foo"
192 }
193 },
194 "spec": {
195 "hostnames": [
196 "foo.com"
197 ],
198 "rules": [
199 {
200 "matches": [
201 {
202 "path": {
203 "type": "PathPrefix",
204 "value": "/bar"
205 }
206 }
207 ],
208 "filters": [
209 {
210 "type": "RequestMirror",
211 "requestMirror": {
212 "serviceName": "my-service1-staging",
213 "port": 8080
214 }
215 }
216 ],
217 "forwardTo": [
218 {
219 "serviceName": "my-service1",
220 "port": 8080
221 }
222 ]
223 }
224 ]
225 }
226 },
227 "operation": "CREATE"
228 }
229 }`),
230 wantRespCode: http.StatusOK,
231 wantSuccessResponse: admission.AdmissionResponse{
232 UID: "7313cd05-eddc-4150-b88c-971a0d53b2ab",
233 Allowed: true,
234 Result: &metav1.Status{},
235 },
236 },
237 {
238 name: "valid v1 HTTPRoute resource with two request mirror filters",
239 reqBody: dedent.Dedent(`{
240 "kind": "AdmissionReview",
241 "apiVersion": "` + apiVersion + `",
242 "request": {
243 "uid": "7313cd05-eddc-4150-b88c-971a0d53b2ab",
244 "resource": {
245 "group": "gateway.networking.k8s.io",
246 "version": "v1",
247 "resource": "httproutes"
248 },
249 "object": {
250 "kind": "HTTPRoute",
251 "apiVersion": "gateway.networking.k8s.io/v1",
252 "metadata": {
253 "name": "http-app-1",
254 "labels": {
255 "app": "foo"
256 }
257 },
258 "spec": {
259 "hostnames": [
260 "foo.com"
261 ],
262 "rules": [
263 {
264 "matches": [
265 {
266 "path": {
267 "type": "PathPrefix",
268 "value": "/bar"
269 }
270 }
271 ],
272 "filters": [
273 {
274 "type": "RequestMirror",
275 "requestMirror": {
276 "serviceName": "my-service1-staging",
277 "port": 8080
278 }
279 },
280 {
281 "type": "RequestMirror",
282 "requestMirror": {
283 "serviceName": "my-service2-staging",
284 "port": 8080
285 }
286 }
287 ],
288 "backendRefs": [
289 {
290 "name": "RequestMirror",
291 "port": 8080
292 }
293 ]
294 }
295 ]
296 }
297 },
298 "operation": "CREATE"
299 }
300 }`),
301 wantRespCode: http.StatusOK,
302 wantSuccessResponse: admission.AdmissionResponse{
303 UID: "7313cd05-eddc-4150-b88c-971a0d53b2ab",
304 Allowed: true,
305 Result: &metav1.Status{},
306 },
307 },
308 {
309 name: "v1a2 GatewayClass create events do not result in an error",
310 reqBody: dedent.Dedent(`{
311 "kind": "AdmissionReview",
312 "apiVersion": "` + apiVersion + `",
313 "request": {
314 "uid": "7313cd05-eddc-4150-b88c-971a0d53b2ab",
315 "resource": {
316 "group": "gateway.networking.k8s.io",
317 "version": "v1",
318 "resource": "gatewayclasses"
319 },
320 "object": {
321 "kind": "GatewayClass",
322 "apiVersion": "gateway.networking.k8s.io/v1",
323 "metadata": {
324 "name": "gateway-class-1"
325 },
326 "spec": {
327 "controller": "example.com/foo"
328 }
329 },
330 "operation": "CREATE"
331 }
332 }`),
333 wantRespCode: http.StatusOK,
334 wantSuccessResponse: admission.AdmissionResponse{
335 UID: "7313cd05-eddc-4150-b88c-971a0d53b2ab",
336 Allowed: true,
337 Result: &metav1.Status{},
338 },
339 },
340 {
341 name: "update to v1 GatewayClass parameters field does" +
342 " not result in an error",
343 reqBody: dedent.Dedent(`{
344 "kind": "AdmissionReview",
345 "apiVersion": "` + apiVersion + `",
346 "request": {
347 "uid": "7313cd05-eddc-4150-b88c-971a0d53b2ab",
348 "resource": {
349 "group": "gateway.networking.k8s.io",
350 "version": "v1",
351 "resource": "gatewayclasses"
352 },
353 "object": {
354 "kind": "GatewayClass",
355 "apiVersion": "gateway.networking.k8s.io/v1",
356 "metadata": {
357 "name": "gateway-class-1"
358 },
359 "spec": {
360 "controllerName": "example.com/foo"
361 }
362 },
363 "oldObject": {
364 "kind": "GatewayClass",
365 "apiVersion": "gateway.networking.k8s.io/v1",
366 "metadata": {
367 "name": "gateway-class-1"
368 },
369 "spec": {
370 "controllerName": "example.com/foo",
371 "parametersRef": {
372 "name": "foo",
373 "namespace": "bar",
374 "scope": "Namespace",
375 "group": "example.com",
376 "kind": "ExampleConfig"
377 }
378 }
379 },
380 "operation": "UPDATE"
381 }
382 }`),
383 wantRespCode: http.StatusOK,
384 wantSuccessResponse: admission.AdmissionResponse{
385 UID: "7313cd05-eddc-4150-b88c-971a0d53b2ab",
386 Allowed: true,
387 Result: &metav1.Status{},
388 },
389 },
390 {
391 name: "update to v1 GatewayClass controllerName field" +
392 " results in an error ",
393 reqBody: dedent.Dedent(`{
394 "kind": "AdmissionReview",
395 "apiVersion": "` + apiVersion + `",
396 "request": {
397 "uid": "7313cd05-eddc-4150-b88c-971a0d53b2ab",
398 "resource": {
399 "group": "gateway.networking.k8s.io",
400 "version": "v1",
401 "resource": "gatewayclasses"
402 },
403 "object": {
404 "kind": "GatewayClass",
405 "apiVersion": "gateway.networking.k8s.io/v1",
406 "metadata": {
407 "name": "gateway-class-1"
408 },
409 "spec": {
410 "controllerName": "example.com/foo"
411 }
412 },
413 "oldObject": {
414 "kind": "GatewayClass",
415 "apiVersion": "gateway.networking.k8s.io/v1",
416 "metadata": {
417 "name": "gateway-class-1"
418 },
419 "spec": {
420 "controllerName": "example.com/bar"
421 }
422 },
423 "operation": "UPDATE"
424 }
425 }`),
426 wantRespCode: http.StatusOK,
427 wantSuccessResponse: admission.AdmissionResponse{
428 UID: "7313cd05-eddc-4150-b88c-971a0d53b2ab",
429 Allowed: false,
430 Result: &metav1.Status{
431 Code: 400,
432 Message: `spec.controllerName: Invalid value: "example.com/foo": cannot update an immutable field`,
433 },
434 },
435 },
436 {
437 name: "unknown resource under networking.x-k8s.io",
438 reqBody: dedent.Dedent(`{
439 "kind": "AdmissionReview",
440 "apiVersion": "` + apiVersion + `",
441 "request": {
442 "uid": "7313cd05-eddc-4150-b88c-971a0d53b2ab",
443 "resource": {
444 "group": "gateway.networking.k8s.io",
445 "version": "v1",
446 "resource": "brokenroutes"
447 },
448 "object": {
449 "apiVersion": "gateway.networking.k8s.io/v1",
450 "kind": "HTTPRoute"
451 },
452 "operation": "CREATE"
453 }
454 }`),
455 wantRespCode: http.StatusInternalServerError,
456 wantFailureMessage: "unknown resource 'brokenroutes'\n",
457 },
458 } {
459 tt := tt
460 t.Run(fmt.Sprintf("%s/%s", apiVersion, tt.name), func(t *testing.T) {
461 assert := assert.New(t)
462 res := httptest.NewRecorder()
463 handler := http.HandlerFunc(ServeHTTP)
464
465
466 req, err := http.NewRequest("POST", "", bytes.NewBuffer([]byte(tt.reqBody)))
467 req = req.WithContext(context.Background())
468 require.NoError(t, err)
469 handler.ServeHTTP(res, req)
470
471
472 assert.Equal(tt.wantRespCode, res.Code)
473 if tt.wantRespCode == http.StatusOK {
474 var review admission.AdmissionReview
475 _, _, err = decoder.Decode(res.Body.Bytes(), nil, &review)
476 require.NoError(t, err)
477 assert.EqualValues(&tt.wantSuccessResponse, review.Response)
478 } else {
479 assert.Equal(res.Body.String(), tt.wantFailureMessage)
480 }
481 })
482 }
483 }
484 }
485
View as plain text