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