1from typing import Generator, Tuple, Union
2
3from abstract_tests import HTTP, AmbassadorTest, Node
4from kat.harness import Query
5
6
7class ErrorResponseOnStatusCode(AmbassadorTest):
8 """
9 Check that we can return a customized error response where the body is built as a formatted string.
10 """
11
12 def init(self):
13 self.target = HTTP()
14
15 def config(self) -> Generator[Union[str, Tuple[Node, str]], None, None]:
16 yield self, f"""
17---
18apiVersion: getambassador.io/v3alpha1
19kind: Module
20name: ambassador
21ambassador_id: ["{self.ambassador_id}"]
22config:
23 error_response_overrides:
24 - on_status_code: 401
25 body:
26 text_format: 'you get a 401'
27 - on_status_code: 403
28 body:
29 text_format: 'and you get a 403'
30 - on_status_code: 404
31 body:
32 text_format: 'cannot find the thing'
33 - on_status_code: 418
34 body:
35 text_format: '2teapot2reply'
36 - on_status_code: 500
37 body:
38 text_format: 'a five hundred happened'
39 - on_status_code: 501
40 body:
41 text_format: 'very not implemented'
42 - on_status_code: 503
43 body:
44 text_format: 'the upstream probably died'
45 - on_status_code: 504
46 body:
47 text_format: 'took too long, sorry'
48 content_type: 'apology'
49---
50apiVersion: getambassador.io/v3alpha1
51kind: Mapping
52name: {self.target.path.k8s}
53ambassador_id: ["{self.ambassador_id}"]
54hostname: "*"
55prefix: /target/
56service: {self.target.path.fqdn}
57---
58apiVersion: getambassador.io/v3alpha1
59kind: Mapping
60name: {self.target.path.k8s}-invalidservice
61ambassador_id: ["{self.ambassador_id}"]
62hostname: "*"
63prefix: /target/invalidservice
64service: {self.target.path.fqdn}-invalidservice
65---
66apiVersion: getambassador.io/v3alpha1
67kind: Mapping
68name: {self.target.path.k8s}-invalidservice-empty
69ambassador_id: ["{self.ambassador_id}"]
70hostname: "*"
71prefix: /target/invalidservice/empty
72service: {self.target.path.fqdn}-invalidservice-empty
73error_response_overrides:
74- on_status_code: 503
75 body:
76 text_format: ''
77"""
78
79 def queries(self):
80 # [0]
81 yield Query(self.url("does-not-exist/"), expected=404)
82 # [1]
83 yield Query(self.url("target/"), headers={"requested-status": "401"}, expected=401)
84 # [2]
85 yield Query(self.url("target/"), headers={"requested-status": "403"}, expected=403)
86 # [3]
87 yield Query(self.url("target/"), headers={"requested-status": "404"}, expected=404)
88 # [4]
89 yield Query(self.url("target/"), headers={"requested-status": "418"}, expected=418)
90 # [5]
91 yield Query(self.url("target/"), headers={"requested-status": "500"}, expected=500)
92 # [6]
93 yield Query(self.url("target/"), headers={"requested-status": "501"}, expected=501)
94 # [7]
95 yield Query(self.url("target/"), headers={"requested-status": "503"}, expected=503)
96 # [8]
97 yield Query(self.url("target/"), headers={"requested-status": "504"}, expected=504)
98 # [9]
99 yield Query(self.url("target/"))
100 # [10]
101 yield Query(self.url("target/invalidservice"), expected=503)
102 # [11]
103 yield Query(self.url("target/invalidservice/empty"), expected=503)
104
105 def check(self):
106 # [0]
107 assert (
108 self.results[0].text == "cannot find the thing"
109 ), f"unexpected response body: {self.results[0].text}"
110
111 # [1]
112 assert (
113 self.results[1].text == "you get a 401"
114 ), f"unexpected response body: {self.results[1].text}"
115
116 # [2]
117 assert (
118 self.results[2].text == "and you get a 403"
119 ), f"unexpected response body: {self.results[2].text}"
120
121 # [3]
122 assert (
123 self.results[3].text == "cannot find the thing"
124 ), f"unexpected response body: {self.results[3].text}"
125
126 # [4]
127 assert (
128 self.results[4].text == "2teapot2reply"
129 ), f"unexpected response body: {self.results[4].text}"
130
131 # [5]
132 assert (
133 self.results[5].text == "a five hundred happened"
134 ), f"unexpected response body: {self.results[5].text}"
135
136 # [6]
137 assert (
138 self.results[6].text == "very not implemented"
139 ), f"unexpected response body: {self.results[6].text}"
140
141 # [7]
142 assert (
143 self.results[7].text == "the upstream probably died"
144 ), f"unexpected response body: {self.results[7].text}"
145
146 # [8]
147 assert (
148 self.results[8].text == "took too long, sorry"
149 ), f"unexpected response body: {self.results[8].text}"
150 assert self.results[8].headers["Content-Type"] == [
151 "apology"
152 ], f"unexpected Content-Type: {self.results[8].headers}"
153
154 # [9] should just succeed
155 assert self.results[9].text == None, f"unexpected response body: {self.results[9].text}"
156
157 # [10] envoy-generated 503, since the upstream is 'invalidservice'.
158 assert (
159 self.results[10].text == "the upstream probably died"
160 ), f"unexpected response body: {self.results[10].text}"
161
162 # [11] envoy-generated 503, with an empty body override
163 assert self.results[11].text == "", f"unexpected response body: {self.results[11].text}"
164
165
166class ErrorResponseOnStatusCodeMappingCRD(AmbassadorTest):
167 """
168 Check that we can return a customized error response where the body is built as a formatted string.
169 """
170
171 def init(self):
172 self.target = HTTP()
173
174 def manifests(self) -> str:
175 return (
176 super().manifests()
177 + f"""
178---
179apiVersion: getambassador.io/v3alpha1
180kind: Mapping
181metadata:
182 name: {self.target.path.k8s}-crd
183spec:
184 ambassador_id: ["{self.ambassador_id}"]
185 hostname: "*"
186 prefix: /target/
187 service: {self.target.path.fqdn}
188 error_response_overrides:
189 - on_status_code: 401
190 body:
191 text_format: 'you get a 401'
192 - on_status_code: 403
193 body:
194 text_format: 'and you get a 403'
195 - on_status_code: 404
196 body:
197 text_format: 'cannot find the thing'
198 - on_status_code: 418
199 body:
200 text_format: '2teapot2reply'
201 - on_status_code: 500
202 body:
203 text_format: 'a five hundred happened'
204 - on_status_code: 501
205 body:
206 text_format: 'very not implemented'
207 - on_status_code: 503
208 body:
209 text_format: 'the upstream probably died'
210 - on_status_code: 504
211 body:
212 text_format: 'took too long, sorry'
213 content_type: 'apology'
214---
215apiVersion: getambassador.io/v3alpha1
216kind: Mapping
217metadata:
218 name: {self.target.path.k8s}-invalidservice-crd
219spec:
220 ambassador_id: ["{self.ambassador_id}"]
221 hostname: "*"
222 prefix: /target/invalidservice
223 service: {self.target.path.fqdn}-invalidservice
224---
225apiVersion: getambassador.io/v3alpha1
226kind: Mapping
227metadata:
228 name: {self.target.path.k8s}-invalidservice-override-crd
229spec:
230 ambassador_id: ["{self.ambassador_id}"]
231 hostname: "*"
232 prefix: /target/invalidservice/override
233 service: {self.target.path.fqdn}-invalidservice
234 error_response_overrides:
235 - on_status_code: 503
236 body:
237 text_format_source:
238 filename: /etc/issue
239"""
240 )
241
242 def queries(self):
243 # [0]
244 yield Query(self.url("does-not-exist/"), expected=404)
245 # [1]
246 yield Query(self.url("target/"), headers={"requested-status": "401"}, expected=401)
247 # [2]
248 yield Query(self.url("target/"), headers={"requested-status": "403"}, expected=403)
249 # [3]
250 yield Query(self.url("target/"), headers={"requested-status": "404"}, expected=404)
251 # [4]
252 yield Query(self.url("target/"), headers={"requested-status": "418"}, expected=418)
253 # [5]
254 yield Query(self.url("target/"), headers={"requested-status": "500"}, expected=500)
255 # [6]
256 yield Query(self.url("target/"), headers={"requested-status": "501"}, expected=501)
257 # [7]
258 yield Query(self.url("target/"), headers={"requested-status": "503"}, expected=503)
259 # [8]
260 yield Query(self.url("target/"), headers={"requested-status": "504"}, expected=504)
261 # [9]
262 yield Query(self.url("target/"))
263 # [10]
264 yield Query(self.url("target/invalidservice"), expected=503)
265 # [11]
266 yield Query(self.url("target/invalidservice/override"), expected=503)
267
268 def check(self):
269 # [0] does not match the error response mapping, so no 404 response.
270 # when envoy directly replies with 404, we see it as an empty string.
271 assert self.results[0].text == "", f"unexpected response body: {self.results[0].text}"
272
273 # [1]
274 assert (
275 self.results[1].text == "you get a 401"
276 ), f"unexpected response body: {self.results[1].text}"
277
278 # [2]
279 assert (
280 self.results[2].text == "and you get a 403"
281 ), f"unexpected response body: {self.results[2].text}"
282
283 # [3]
284 assert (
285 self.results[3].text == "cannot find the thing"
286 ), f"unexpected response body: {self.results[3].text}"
287
288 # [4]
289 assert (
290 self.results[4].text == "2teapot2reply"
291 ), f"unexpected response body: {self.results[4].text}"
292
293 # [5]
294 assert (
295 self.results[5].text == "a five hundred happened"
296 ), f"unexpected response body: {self.results[5].text}"
297
298 # [6]
299 assert (
300 self.results[6].text == "very not implemented"
301 ), f"unexpected response body: {self.results[6].text}"
302
303 # [7]
304 assert (
305 self.results[7].text == "the upstream probably died"
306 ), f"unexpected response body: {self.results[7].text}"
307
308 # [8]
309 assert (
310 self.results[8].text == "took too long, sorry"
311 ), f"unexpected response body: {self.results[8].text}"
312 assert self.results[8].headers["Content-Type"] == [
313 "apology"
314 ], f"unexpected Content-Type: {self.results[8].headers}"
315
316 # [9] should just succeed
317 assert self.results[9].text == None, f"unexpected response body: {self.results[9].text}"
318
319 # [10] envoy-generated 503, since the upstream is 'invalidservice'.
320 # this response body comes unmodified from envoy, since it goes through
321 # a mapping with no error response overrides and there's no overrides
322 # on the Ambassador module
323 assert (
324 self.results[10].text == "no healthy upstream"
325 ), f"unexpected response body: {self.results[10].text}"
326
327 # [11] envoy-generated 503, since the upstream is 'invalidservice'.
328 # this response body should be matched by the `text_format_source` override
329 # sorry for using /etc/issue, by the way.
330 assert (
331 "Welcome to Alpine Linux" in self.results[11].text
332 ), f"unexpected response body: {self.results[11].text}"
333
334
335class ErrorResponseReturnBodyFormattedText(AmbassadorTest):
336 """
337 Check that we can return a customized error response where the body is built as a formatted string.
338 """
339
340 def init(self):
341 self.target = HTTP()
342
343 def config(self) -> Generator[Union[str, Tuple[Node, str]], None, None]:
344 yield self, f"""
345---
346apiVersion: getambassador.io/v3alpha1
347kind: Module
348name: ambassador
349ambassador_id: ["{self.ambassador_id}"]
350config:
351 error_response_overrides:
352 - on_status_code: 404
353 body:
354 text_format: 'there has been an error: %RESPONSE_CODE%'
355 - on_status_code: 429
356 body:
357 text_format: '<html>2fast %PROTOCOL%</html>'
358 content_type: 'text/html'
359 - on_status_code: 504
360 body:
361 text_format: '<html>2slow %PROTOCOL%</html>'
362 content_type: 'text/html; charset="utf-8"'
363---
364apiVersion: getambassador.io/v3alpha1
365kind: Mapping
366name: {self.target.path.k8s}
367ambassador_id: ["{self.ambassador_id}"]
368hostname: "*"
369prefix: /target/
370service: {self.target.path.fqdn}
371"""
372
373 def queries(self):
374 # [0]
375 yield Query(self.url("does-not-exist/"), expected=404)
376
377 # [1]
378 yield Query(self.url("target/"), headers={"requested-status": "429"}, expected=429)
379
380 # [2]
381 yield Query(self.url("target/"), headers={"requested-status": "504"}, expected=504)
382
383 def check(self):
384 # [0]
385 assert (
386 self.results[0].text == "there has been an error: 404"
387 ), f"unexpected response body: {self.results[0].text}"
388 assert self.results[0].headers["Content-Type"] == [
389 "text/plain"
390 ], f"unexpected Content-Type: {self.results[0].headers}"
391
392 # [1]
393 assert (
394 self.results[1].text == "<html>2fast HTTP/1.1</html>"
395 ), f"unexpected response body: {self.results[1].text}"
396 assert self.results[1].headers["Content-Type"] == [
397 "text/html"
398 ], f"unexpected Content-type: {self.results[1].headers}"
399
400 # [2]
401 assert (
402 self.results[2].text == "<html>2slow HTTP/1.1</html>"
403 ), f"unexpected response body: {self.results[2].text}"
404 assert self.results[2].headers["Content-Type"] == [
405 'text/html; charset="utf-8"'
406 ], f"unexpected Content-Type: {self.results[2].headers}"
407
408
409class ErrorResponseReturnBodyFormattedJson(AmbassadorTest):
410 """
411 Check that we can return a customized error response where the body is built from a text source.
412 """
413
414 def init(self):
415 self.target = HTTP()
416
417 def config(self) -> Generator[Union[str, Tuple[Node, str]], None, None]:
418 yield self, f"""
419---
420apiVersion: getambassador.io/v3alpha1
421kind: Module
422name: ambassador
423ambassador_id: ["{self.ambassador_id}"]
424config:
425 error_response_overrides:
426 - on_status_code: 401
427 body:
428 json_format:
429 error: 'unauthorized'
430 - on_status_code: 404
431 body:
432 json_format:
433 custom_error: 'truth'
434 code: '%RESPONSE_CODE%'
435 - on_status_code: 429
436 body:
437 json_format:
438 custom_error: 'yep'
439 toofast: 'definitely'
440 code: 'code was %RESPONSE_CODE%'
441---
442apiVersion: getambassador.io/v3alpha1
443kind: Mapping
444name: {self.target.path.k8s}
445ambassador_id: ["{self.ambassador_id}"]
446hostname: "*"
447prefix: /target/
448service: {self.target.path.fqdn}
449"""
450
451 def queries(self):
452 yield Query(self.url("does-not-exist/"), expected=404)
453 yield Query(self.url("target/"), headers={"requested-status": "429"}, expected=429)
454 yield Query(self.url("target/"), headers={"requested-status": "401"}, expected=401)
455
456 def check(self):
457 # [0]
458 # Strange gotcha: it looks like we always get an integer code here
459 # even though the field specifier above is wrapped in single quotes.
460 assert self.results[0].json == {
461 "custom_error": "truth",
462 "code": 404,
463 }, f"unexpected response body: {self.results[0].json}"
464 assert self.results[0].headers["Content-Type"] == [
465 "application/json"
466 ], f"unexpected Content-Type: {self.results[0].headers}"
467
468 # [1]
469 assert self.results[1].json == {
470 "custom_error": "yep",
471 "toofast": "definitely",
472 "code": "code was 429",
473 }, f"unexpected response body: {self.results[1].json}"
474 assert self.results[1].headers["Content-Type"] == [
475 "application/json"
476 ], f"unexpected Content-Type: {self.results[1].headers}"
477
478 # [2]
479 assert self.results[2].json == {
480 "error": "unauthorized"
481 }, f"unexpected response body: {self.results[2].json}"
482 assert self.results[2].headers["Content-Type"] == [
483 "application/json"
484 ], f"unexpected Content-Type: {self.results[2].headers}"
485
486
487class ErrorResponseReturnBodyTextSource(AmbassadorTest):
488 """
489 Check that we can return a customized error response where the body is built as a formatted string.
490 """
491
492 def init(self):
493 self.target = HTTP()
494
495 def config(self) -> Generator[Union[str, Tuple[Node, str]], None, None]:
496 yield self, f"""
497---
498apiVersion: getambassador.io/v3alpha1
499kind: Module
500name: ambassador
501ambassador_id: ["{self.ambassador_id}"]
502config:
503 error_response_overrides:
504 - on_status_code: 500
505 body:
506 text_format_source:
507 filename: '/etc/issue'
508 content_type: 'application/etcissue'
509 - on_status_code: 503
510 body:
511 text_format_source:
512 filename: '/etc/motd'
513 content_type: 'application/motd'
514 - on_status_code: 504
515 body:
516 text_format_source:
517 filename: '/etc/shells'
518---
519apiVersion: getambassador.io/v3alpha1
520kind: Mapping
521name: {self.target.path.k8s}
522ambassador_id: ["{self.ambassador_id}"]
523hostname: "*"
524prefix: /target/
525service: {self.target.path.fqdn}
526"""
527
528 def queries(self):
529 # [0]
530 yield Query(self.url("target/"), headers={"requested-status": "500"}, expected=500)
531
532 # [1]
533 yield Query(self.url("target/"), headers={"requested-status": "503"}, expected=503)
534
535 # [2]
536 yield Query(self.url("target/"), headers={"requested-status": "504"}, expected=504)
537
538 def check(self):
539 # [0] Sorry for using /etc/issue...
540 print("headers = %s" % self.results[0].headers)
541 assert (
542 "Welcome to Alpine Linux" in self.results[0].text
543 ), f"unexpected response body: {self.results[0].text}"
544 assert self.results[0].headers["Content-Type"] == [
545 "application/etcissue"
546 ], f"unexpected Content-Type: {self.results[0].headers}"
547
548 # [1] ...and sorry for using /etc/motd...
549 assert (
550 "You may change this message by editing /etc/motd." in self.results[1].text
551 ), f"unexpected response body: {self.results[1].text}"
552 assert self.results[1].headers["Content-Type"] == [
553 "application/motd"
554 ], f"unexpected Content-Type: {self.results[1].headers}"
555
556 # [2] ...and sorry for using /etc/shells
557 assert (
558 "# valid login shells" in self.results[2].text
559 ), f"unexpected response body: {self.results[2].text}"
560 assert self.results[2].headers["Content-Type"] == [
561 "text/plain"
562 ], f"unexpected Content-Type: {self.results[2].headers}"
563
564
565class ErrorResponseMappingBypass(AmbassadorTest):
566 """
567 Check that we can return a bypass custom error responses at the mapping level
568 """
569
570 def init(self):
571 self.target = HTTP()
572
573 def config(self) -> Generator[Union[str, Tuple[Node, str]], None, None]:
574 yield self, f"""
575---
576apiVersion: getambassador.io/v3alpha1
577kind: Module
578name: ambassador
579ambassador_id: ["{self.ambassador_id}"]
580config:
581 error_response_overrides:
582 - on_status_code: 404
583 body:
584 text_format: 'this is a custom 404 response'
585 content_type: 'text/custom'
586 - on_status_code: 418
587 body:
588 text_format: 'bad teapot request'
589 - on_status_code: 503
590 body:
591 text_format: 'the upstream is not happy'
592---
593apiVersion: getambassador.io/v3alpha1
594kind: Mapping
595name: {self.target.path.k8s}
596ambassador_id: ["{self.ambassador_id}"]
597hostname: "*"
598prefix: /target/
599service: {self.target.path.fqdn}
600---
601apiVersion: getambassador.io/v3alpha1
602kind: Mapping
603name: {self.target.path.k8s}-invalidservice
604ambassador_id: ["{self.ambassador_id}"]
605hostname: "*"
606prefix: /target/invalidservice
607service: {self.target.path.fqdn}-invalidservice
608---
609apiVersion: getambassador.io/v3alpha1
610kind: Mapping
611name: {self.target.path.k8s}-bypass
612ambassador_id: ["{self.ambassador_id}"]
613hostname: "*"
614prefix: /bypass/
615service: {self.target.path.fqdn}
616bypass_error_response_overrides: true
617---
618apiVersion: getambassador.io/v3alpha1
619kind: Mapping
620name: {self.target.path.k8s}-target-bypass
621ambassador_id: ["{self.ambassador_id}"]
622hostname: "*"
623prefix: /target/bypass/
624service: {self.target.path.fqdn}
625bypass_error_response_overrides: true
626---
627apiVersion: getambassador.io/v3alpha1
628kind: Mapping
629name: {self.target.path.k8s}-bypass-invalidservice
630ambassador_id: ["{self.ambassador_id}"]
631hostname: "*"
632prefix: /bypass/invalidservice
633service: {self.target.path.fqdn}-invalidservice
634bypass_error_response_overrides: true
635"""
636
637 def queries(self):
638 # [0]
639 yield Query(self.url("bypass/"), headers={"requested-status": "404"}, expected=404)
640 # [1]
641 yield Query(self.url("target/"), headers={"requested-status": "404"}, expected=404)
642 # [2]
643 yield Query(self.url("target/bypass/"), headers={"requested-status": "418"}, expected=418)
644 # [3]
645 yield Query(self.url("target/"), headers={"requested-status": "418"}, expected=418)
646 # [4]
647 yield Query(self.url("target/invalidservice"), expected=503)
648 # [5]
649 yield Query(self.url("bypass/invalidservice"), expected=503)
650 # [6]
651 yield Query(self.url("bypass/"), headers={"requested-status": "503"}, expected=503)
652 # [7]
653 yield Query(self.url("target/"), headers={"requested-status": "503"}, expected=503)
654 # [8]
655 yield Query(self.url("bypass/"), headers={"requested-status": "200"})
656 # [9]
657 yield Query(self.url("target/"), headers={"requested-status": "200"})
658
659 def check(self):
660 # [0]
661 assert self.results[0].text is None, f"unexpected response body: {self.results[0].text}"
662
663 # [1]
664 assert (
665 self.results[1].text == "this is a custom 404 response"
666 ), f"unexpected response body: {self.results[1].text}"
667 assert self.results[1].headers["Content-Type"] == [
668 "text/custom"
669 ], f"unexpected Content-Type: {self.results[1].headers}"
670
671 # [2]
672 assert self.results[2].text is None, f"unexpected response body: {self.results[2].text}"
673
674 # [3]
675 assert (
676 self.results[3].text == "bad teapot request"
677 ), f"unexpected response body: {self.results[3].text}"
678
679 # [4]
680 assert (
681 self.results[4].text == "the upstream is not happy"
682 ), f"unexpected response body: {self.results[4].text}"
683
684 # [5]
685 assert (
686 self.results[5].text == "no healthy upstream"
687 ), f"unexpected response body: {self.results[5].text}"
688 assert self.results[5].headers["Content-Type"] == [
689 "text/plain"
690 ], f"unexpected Content-Type: {self.results[5].headers}"
691
692 # [6]
693 assert self.results[6].text is None, f"unexpected response body: {self.results[6].text}"
694
695 # [7]
696 assert (
697 self.results[7].text == "the upstream is not happy"
698 ), f"unexpected response body: {self.results[7].text}"
699
700 # [8]
701 assert self.results[8].text is None, f"unexpected response body: {self.results[8].text}"
702
703 # [9]
704 assert self.results[9].text is None, f"unexpected response body: {self.results[9].text}"
705
706
707class ErrorResponseMappingBypassAlternate(AmbassadorTest):
708 """
709 Check that we can alternate between serving a custom error response and not
710 serving one. This is a baseline sanity check against Envoy's response map
711 filter incorrectly persisting state across filter chain iterations.
712 """
713
714 def init(self):
715 self.target = HTTP()
716
717 def config(self) -> Generator[Union[str, Tuple[Node, str]], None, None]:
718 yield self, f"""
719---
720apiVersion: getambassador.io/v3alpha1
721kind: Module
722name: ambassador
723ambassador_id: ["{self.ambassador_id}"]
724config:
725 error_response_overrides:
726 - on_status_code: 404
727 body:
728 text_format: 'this is a custom 404 response'
729 content_type: 'text/custom'
730---
731apiVersion: getambassador.io/v3alpha1
732kind: Mapping
733name: {self.target.path.k8s}
734ambassador_id: ["{self.ambassador_id}"]
735hostname: "*"
736prefix: /target/
737service: {self.target.path.fqdn}
738---
739apiVersion: getambassador.io/v3alpha1
740kind: Mapping
741name: {self.target.path.k8s}-invalidservice
742ambassador_id: ["{self.ambassador_id}"]
743hostname: "*"
744prefix: /target/invalidservice
745service: {self.target.path.fqdn}-invalidservice
746---
747apiVersion: getambassador.io/v3alpha1
748kind: Mapping
749name: {self.target.path.k8s}-bypass
750ambassador_id: ["{self.ambassador_id}"]
751hostname: "*"
752prefix: /bypass/
753service: {self.target.path.fqdn}
754bypass_error_response_overrides: true
755"""
756
757 def queries(self):
758 # [0]
759 yield Query(self.url("target/"), headers={"requested-status": "404"}, expected=404)
760 # [1]
761 yield Query(self.url("bypass/"), headers={"requested-status": "404"}, expected=404)
762 # [2]
763 yield Query(self.url("target/"), headers={"requested-status": "404"}, expected=404)
764
765 def check(self):
766 # [0]
767 assert (
768 self.results[0].text == "this is a custom 404 response"
769 ), f"unexpected response body: {self.results[0].text}"
770 assert self.results[0].headers["Content-Type"] == [
771 "text/custom"
772 ], f"unexpected Content-Type: {self.results[0].headers}"
773
774 # [1]
775 assert self.results[1].text is None, f"unexpected response body: {self.results[1].text}"
776
777 # [2]
778 assert (
779 self.results[2].text == "this is a custom 404 response"
780 ), f"unexpected response body: {self.results[2].text}"
781 assert self.results[2].headers["Content-Type"] == [
782 "text/custom"
783 ], f"unexpected Content-Type: {self.results[2].headers}"
784
785
786class ErrorResponseMapping404Body(AmbassadorTest):
787 """
788 Check that a 404 body is consistent whether error response overrides exist or not
789 """
790
791 def init(self):
792 self.target = HTTP()
793
794 def config(self) -> Generator[Union[str, Tuple[Node, str]], None, None]:
795 yield self, f"""
796---
797apiVersion: getambassador.io/v3alpha1
798kind: Module
799name: ambassador
800ambassador_id: ["{self.ambassador_id}"]
801config:
802 error_response_overrides:
803 - on_status_code: 401
804 body:
805 text_format: 'this is a custom 401 response'
806 content_type: 'text/custom'
807---
808apiVersion: getambassador.io/v3alpha1
809kind: Mapping
810name: {self.target.path.k8s}
811ambassador_id: ["{self.ambassador_id}"]
812hostname: "*"
813prefix: /target/
814service: {self.target.path.fqdn}
815---
816apiVersion: getambassador.io/v3alpha1
817kind: Mapping
818name: {self.target.path.k8s}-bypass
819ambassador_id: ["{self.ambassador_id}"]
820hostname: "*"
821prefix: /bypass/
822service: {self.target.path.fqdn}
823bypass_error_response_overrides: true
824---
825apiVersion: getambassador.io/v3alpha1
826kind: Mapping
827name: {self.target.path.k8s}-overrides
828ambassador_id: ["{self.ambassador_id}"]
829hostname: "*"
830prefix: /overrides/
831service: {self.target.path.fqdn}
832error_response_overrides:
833- on_status_code: 503
834 body:
835 text_format: 'custom 503'
836"""
837
838 def queries(self):
839 # [0]
840 yield Query(self.url("does-not-exist/"), expected=404)
841 # [1]
842 yield Query(self.url("target/"), headers={"requested-status": "404"}, expected=404)
843 # [2]
844 yield Query(self.url("bypass/"), headers={"requested-status": "404"}, expected=404)
845 # [3]
846 yield Query(self.url("overrides/"), headers={"requested-status": "404"}, expected=404)
847
848 def check(self):
849 # [0] does not match the error response mapping, so no 404 response.
850 # when envoy directly replies with 404, we see it as an empty string.
851 assert self.results[0].text == "", f"unexpected response body: {self.results[0].text}"
852
853 # [1]
854 assert self.results[1].text is None, f"unexpected response body: {self.results[1].text}"
855
856 # [2]
857 assert self.results[2].text is None, f"unexpected response body: {self.results[2].text}"
858
859 # [3]
860 assert self.results[3].text is None, f"unexpected response body: {self.results[3].text}"
861
862
863class ErrorResponseMappingOverride(AmbassadorTest):
864 """
865 Check that we can return a custom error responses at the mapping level
866 """
867
868 def init(self):
869 self.target = HTTP()
870
871 def config(self) -> Generator[Union[str, Tuple[Node, str]], None, None]:
872 yield self, f"""
873---
874apiVersion: getambassador.io/v3alpha1
875kind: Module
876name: ambassador
877ambassador_id: ["{self.ambassador_id}"]
878config:
879 error_response_overrides:
880 - on_status_code: 401
881 body:
882 text_format: 'this is a custom 401 response'
883 content_type: 'text/custom'
884 - on_status_code: 503
885 body:
886 text_format: 'the upstream is not happy'
887 - on_status_code: 504
888 body:
889 text_format: 'the upstream took a really long time'
890---
891apiVersion: getambassador.io/v3alpha1
892kind: Mapping
893name: {self.target.path.k8s}
894ambassador_id: ["{self.ambassador_id}"]
895hostname: "*"
896prefix: /target/
897service: {self.target.path.fqdn}
898---
899apiVersion: getambassador.io/v3alpha1
900kind: Mapping
901name: {self.target.path.k8s}-override-401
902ambassador_id: ["{self.ambassador_id}"]
903hostname: "*"
904prefix: /override/401/
905service: {self.target.path.fqdn}
906error_response_overrides:
907- on_status_code: 401
908 body:
909 json_format:
910 x: "1"
911 status: '%RESPONSE_CODE%'
912---
913apiVersion: getambassador.io/v3alpha1
914kind: Mapping
915name: {self.target.path.k8s}-override-503
916ambassador_id: ["{self.ambassador_id}"]
917hostname: "*"
918prefix: /override/503/
919service: {self.target.path.fqdn}
920error_response_overrides:
921- on_status_code: 503
922 body:
923 json_format:
924 "y": "2"
925 status: '%RESPONSE_CODE%'
926"""
927
928 def queries(self):
929 # [0] Should match module's on_response_code 401
930 yield Query(self.url("target/"), headers={"requested-status": "401"}, expected=401)
931
932 # [1] Should match mapping-specific on_response_code 401
933 yield Query(self.url("override/401/"), headers={"requested-status": "401"}, expected=401)
934
935 # [2] Should match mapping-specific on_response_code 503
936 yield Query(self.url("override/503/"), headers={"requested-status": "503"}, expected=503)
937
938 # [3] Should not match mapping-specific rule, therefore no rewrite
939 yield Query(self.url("override/401/"), headers={"requested-status": "503"}, expected=503)
940
941 # [4] Should not match mapping-specific rule, therefore no rewrite
942 yield Query(self.url("override/503/"), headers={"requested-status": "401"}, expected=401)
943
944 # [5] Should not match mapping-specific rule, therefore no rewrite
945 yield Query(self.url("override/401/"), headers={"requested-status": "504"}, expected=504)
946
947 # [6] Should not match mapping-specific rule, therefore no rewrite
948 yield Query(self.url("override/503/"), headers={"requested-status": "504"}, expected=504)
949
950 # [7] Should match module's on_response_code 503
951 yield Query(self.url("target/"), headers={"requested-status": "503"}, expected=503)
952
953 # [8] Should match module's on_response_code 504
954 yield Query(self.url("target/"), headers={"requested-status": "504"}, expected=504)
955
956 def check(self):
957 # [0] Module's 401 rule with custom header
958 assert (
959 self.results[0].text == "this is a custom 401 response"
960 ), f"unexpected response body: {self.results[0].text}"
961 assert self.results[0].headers["Content-Type"] == [
962 "text/custom"
963 ], f"unexpected Content-Type: {self.results[0].headers}"
964
965 # [1] Mapping's 401 rule with json response
966 assert self.results[1].json == {
967 "x": "1",
968 "status": 401,
969 }, f"unexpected response body: {self.results[1].json}"
970 assert self.results[1].headers["Content-Type"] == [
971 "application/json"
972 ], f"unexpected Content-Type: {self.results[1].headers}"
973
974 # [2] Mapping's 503 rule with json response
975 assert self.results[2].json == {
976 "y": "2",
977 "status": 503,
978 }, f"unexpected response body: {self.results[2].json}"
979 assert self.results[2].headers["Content-Type"] == [
980 "application/json"
981 ], f"unexpected Content-Type: {self.results[2].headers}"
982
983 # [3] Mapping has 401 rule, but response code is 503, no rewrite.
984 assert self.results[3].text is None, f"unexpected response body: {self.results[3].text}"
985
986 # [4] Mapping has 503 rule, but response code is 401, no rewrite.
987 assert self.results[4].text is None, f"unexpected response body: {self.results[4].text}"
988
989 # [5] Mapping has 401 rule, but response code is 504, no rewrite.
990 assert self.results[5].text is None, f"unexpected response body: {self.results[5].text}"
991
992 # [6] Mapping has 503 rule, but response code is 504, no rewrite.
993 assert self.results[6].text is None, f"unexpected response body: {self.results[6].text}"
994
995 # [7] Module's 503 rule, no custom header
996 assert (
997 self.results[7].text == "the upstream is not happy"
998 ), f"unexpected response body: {self.results[7].text}"
999 assert self.results[7].headers["Content-Type"] == [
1000 "text/plain"
1001 ], f"unexpected Content-Type: {self.results[7].headers}"
1002
1003 # [8] Module's 504 rule, no custom header
1004 assert (
1005 self.results[8].text == "the upstream took a really long time"
1006 ), f"unexpected response body: {self.results[8].text}"
1007 assert self.results[8].headers["Content-Type"] == [
1008 "text/plain"
1009 ], f"unexpected Content-Type: {self.results[8].headers}"
1010
1011
1012class ErrorResponseSeveralMappings(AmbassadorTest):
1013 """
1014 Check that we can specify separate error response overrides on two mappings with no Module
1015 config
1016 """
1017
1018 def init(self):
1019 self.target = HTTP()
1020
1021 def manifests(self) -> str:
1022 return (
1023 super().manifests()
1024 + f"""
1025---
1026apiVersion: getambassador.io/v3alpha1
1027kind: Mapping
1028metadata:
1029 name: {self.target.path.k8s}-one
1030spec:
1031 ambassador_id: ["{self.ambassador_id}"]
1032 hostname: "*"
1033 prefix: /target-one/
1034 service: {self.target.path.fqdn}
1035 error_response_overrides:
1036 - on_status_code: 404
1037 body:
1038 text_format: '%RESPONSE_CODE% from first mapping'
1039 - on_status_code: 504
1040 body:
1041 text_format: 'a custom 504 response'
1042---
1043apiVersion: getambassador.io/v3alpha1
1044kind: Mapping
1045metadata:
1046 name: {self.target.path.k8s}-two
1047spec:
1048 ambassador_id: ["{self.ambassador_id}"]
1049 hostname: "*"
1050 prefix: /target-two/
1051 service: {self.target.path.fqdn}
1052 error_response_overrides:
1053 - on_status_code: 404
1054 body:
1055 text_format: '%RESPONSE_CODE% from second mapping'
1056 - on_status_code: 429
1057 body:
1058 text_format: 'a custom 429 response'
1059---
1060apiVersion: getambassador.io/v3alpha1
1061kind: Mapping
1062metadata:
1063 name: {self.target.path.k8s}-three
1064spec:
1065 ambassador_id: ["{self.ambassador_id}"]
1066 hostname: "*"
1067 prefix: /target-three/
1068 service: {self.target.path.fqdn}
1069---
1070apiVersion: getambassador.io/v3alpha1
1071kind: Mapping
1072metadata:
1073 name: {self.target.path.k8s}-four
1074spec:
1075 ambassador_id: ["{self.ambassador_id}"]
1076 hostname: "*"
1077 prefix: /target-four/
1078 service: {self.target.path.fqdn}
1079 error_response_overrides:
1080 - on_status_code: 500
1081 body:
1082 text_format: '500 is a bad status code'
1083"""
1084 )
1085
1086 _queries = [
1087 {"url": "does-not-exist/", "status": 404, "text": ""},
1088 {"url": "target-one/", "status": 404, "text": "404 from first mapping"},
1089 {"url": "target-one/", "status": 429, "text": None},
1090 {"url": "target-one/", "status": 504, "text": "a custom 504 response"},
1091 {"url": "target-two/", "status": 404, "text": "404 from second mapping"},
1092 {"url": "target-two/", "status": 429, "text": "a custom 429 response"},
1093 {"url": "target-two/", "status": 504, "text": None},
1094 {"url": "target-three/", "status": 404, "text": None},
1095 {"url": "target-three/", "status": 429, "text": None},
1096 {"url": "target-three/", "status": 504, "text": None},
1097 {"url": "target-four/", "status": 404, "text": None},
1098 {"url": "target-four/", "status": 429, "text": None},
1099 {"url": "target-four/", "status": 504, "text": None},
1100 ]
1101
1102 def queries(self):
1103 for x in self._queries:
1104 yield Query(
1105 self.url(x["url"]),
1106 headers={"requested-status": str(x["status"])},
1107 expected=x["status"],
1108 )
1109
1110 def check(self):
1111 for i in range(len(self._queries)):
1112 expected = self._queries[i]["text"]
1113 res = self.results[i]
1114 assert (
1115 res.text == expected
1116 ), f'unexpected response body on query {i}: "{res.text}", wanted "{expected}"'
View as plain text