1import logging
2import typing
3from typing import Any, Dict, List
4
5import pytest
6
7logging.basicConfig(
8 level=logging.INFO,
9 format="%(asctime)s test %(levelname)s: %(message)s",
10 datefmt="%Y-%m-%d %H:%M:%S",
11)
12
13logger = logging.getLogger("ambassador")
14
15from ambassador import IR, Config, EnvoyConfig
16from ambassador.fetch import ResourceFetcher
17from ambassador.utils import NullSecretHandler
18from tests.utils import default_listener_manifests
19
20
21def _get_cluster_config(clusters, name):
22 for cluster in clusters:
23 # we're only interested in the cluster for the provided name
24 if cluster["name"] == name:
25 return cluster
26 else:
27 continue
28 return False
29
30
31def _get_envoy_config(yaml):
32 aconf = Config()
33 fetcher = ResourceFetcher(logger, aconf)
34 fetcher.parse_yaml(default_listener_manifests() + yaml, k8s=True)
35
36 aconf.load_all(fetcher.sorted())
37
38 secret_handler = NullSecretHandler(logger, None, None, "0")
39
40 ir = IR(aconf, file_checker=lambda path: True, secret_handler=secret_handler)
41
42 assert ir
43 econf = EnvoyConfig.generate(ir)
44 assert econf, "could not create an econf"
45 return econf
46
47
48@pytest.mark.compilertest
49def test_healthcheck():
50
51 baseYaml = """
52---
53apiVersion: getambassador.io/v3alpha1
54kind: Mapping
55metadata:
56 name: healthchecktest
57 namespace: default
58spec:
59 hostname: '*'
60 service: coolsvcname
61 prefix: /test
62 resolver: endpoint
63 health_checks: {}
64"""
65
66 noEndpointYaml = """
67---
68apiVersion: getambassador.io/v3alpha1
69kind: Mapping
70metadata:
71 name: healthchecktest
72 namespace: default
73spec:
74 hostname: '*'
75 service: coolsvcname
76 prefix: /test
77 health_checks: {}
78"""
79 testcases: List[Dict[str, Any]] = [
80 { # Test that the fields we leave out get assigned default values
81 "name": "healthcheck_defaults",
82 "input": baseYaml.format([{"health_check": {"http": {"path": "/health"}}}]),
83 # When fields such as healthy_threshold that have default values
84 # are not supplied by the expected field then we will check that they have their default values
85 "expected": [
86 {
87 "http_health_check": {
88 "path": "/health",
89 },
90 },
91 ],
92 },
93 { # Check that we can override all of the fields that get default values
94 "name": "healthcheck_no_defaults",
95 "input": baseYaml.format(
96 [
97 {
98 "health_check": {
99 "http": {
100 "path": "/health",
101 }
102 },
103 "healthy_threshold": 5,
104 "unhealthy_threshold": 5,
105 "interval": "10s",
106 "timeout": "15s",
107 }
108 ]
109 ),
110 "expected": [
111 {
112 "http_health_check": {
113 "path": "/health",
114 },
115 "healthy_threshold": 5,
116 "unhealthy_threshold": 5,
117 "interval": "10s",
118 "timeout": "15s",
119 },
120 ],
121 },
122 { # Check that both a grpc and http healthceck can be used at the same time
123 "name": "healthcheck_http_plus_grpc",
124 "input": baseYaml.format(
125 [
126 {
127 "health_check": {
128 "grpc": {
129 "upstream_name": "coolsvcname.default",
130 }
131 }
132 },
133 {
134 "health_check": {
135 "http": {
136 "path": "/health",
137 }
138 }
139 },
140 ]
141 ),
142 "expected": [
143 {
144 "grpc_health_check": {
145 "service_name": "coolsvcname.default",
146 }
147 },
148 {
149 "http_health_check": {
150 "path": "/health",
151 }
152 },
153 ],
154 },
155 { # Check that we can set the authority on grpc health checks
156 "name": "healthcheck_grpc_authority",
157 "input": baseYaml.format(
158 [
159 {
160 "health_check": {
161 "grpc": {
162 "upstream_name": "coolsvcname.default",
163 "authority": "dummy.example",
164 }
165 }
166 }
167 ]
168 ),
169 "expected": [
170 {
171 "grpc_health_check": {
172 "service_name": "coolsvcname.default",
173 "authority": "dummy.example",
174 }
175 },
176 ],
177 },
178 { # Check that we can set add/remove headers for a http health check
179 "name": "healthcheck_grpc_authority",
180 "input": baseYaml.format(
181 [
182 {
183 "health_check": {
184 "grpc": {
185 "upstream_name": "coolsvcname.default",
186 "authority": "dummy.example",
187 }
188 }
189 }
190 ]
191 ),
192 "expected": [
193 {
194 "grpc_health_check": {
195 "service_name": "coolsvcname.default",
196 "authority": "dummy.example",
197 }
198 },
199 ],
200 },
201 { # check that we can set hostname on a http health check
202 "name": "healthcheck_http_hostname",
203 "input": baseYaml.format(
204 [{"health_check": {"http": {"path": "/health", "hostname": "dummy.example"}}}]
205 ),
206 "expected": [
207 {
208 "http_health_check": {
209 "path": "/health",
210 # hostname becomes host in the econf
211 "host": "dummy.example",
212 }
213 },
214 ],
215 },
216 { # check that we can set expected statuses on a http health check
217 "name": "healthcheck_http_statuses",
218 "input": baseYaml.format(
219 [
220 {
221 "health_check": {
222 "http": {
223 "path": "/health",
224 "expected_statuses": [
225 {"min": 101, "max": 199},
226 {"min": 201, "max": 299},
227 ],
228 }
229 }
230 }
231 ]
232 ),
233 "expected": [
234 {
235 "http_health_check": {
236 "path": "/health",
237 # We increment the end by 1 in the backend since
238 # envoy treats the end as being excluded (which adds confusion so lets just make the start and end inclusive)
239 "expected_statuses": [
240 {"start": 101, "end": 200},
241 {"start": 201, "end": 300},
242 ],
243 }
244 },
245 ],
246 },
247 { # check that an invalid expected status is ignored
248 "name": "healthcheck_http_statuses_invalid",
249 "input": baseYaml.format(
250 [
251 {
252 "health_check": {
253 "http": {
254 "path": "/health",
255 "expected_statuses": [
256 # this one is invalid since the start is larger than the end so we should just drop it.
257 {"min": 300, "max": 100},
258 {"min": 201, "max": 299},
259 ],
260 }
261 }
262 }
263 ]
264 ),
265 "expected": [
266 {
267 "http_health_check": {
268 "path": "/health",
269 # We increment the end by 1 in the backend since
270 # envoy treats the end as being excluded (which adds confusion so lets just make the start and end inclusive)
271 "expected_statuses": [
272 {"start": 201, "end": 300},
273 ],
274 }
275 },
276 ],
277 },
278 { # check that if all the expected statuses are invalid then we don't set the field
279 "name": "healthcheck_http_statuses_invalid_all",
280 "input": baseYaml.format(
281 [
282 {
283 "health_check": {
284 "http": {
285 "path": "/health",
286 "expected_statuses": [
287 # these are both invalid so the whole field should be ignored
288 {"min": 300, "max": 100},
289 {"min": 400, "max": 300},
290 ],
291 }
292 }
293 }
294 ]
295 ),
296 "expected": [
297 {"http_health_check": {"path": "/health"}},
298 ],
299 },
300 { # check that append headers is true when not provided
301 "name": "healthcheck_http_add_headers",
302 "input": baseYaml.format(
303 [
304 {
305 "health_check": {
306 "http": {
307 "path": "/health",
308 "add_request_headers": {
309 "fruit-one": {"append": False, "value": "banana"},
310 "fruit-two": {"append": True, "value": "orange"},
311 "fruit-three": {"value": "peach"},
312 },
313 }
314 }
315 }
316 ]
317 ),
318 "expected": [
319 {
320 "http_health_check": {
321 "path": "/health",
322 "request_headers_to_add": [
323 {"header": {"key": "fruit-one", "value": "banana"}, "append": False},
324 {"header": {"key": "fruit-two", "value": "orange"}, "append": True},
325 {"header": {"key": "fruit-three", "value": "peach"}, "append": True},
326 ],
327 }
328 },
329 ],
330 },
331 { # check remove headers
332 "name": "healthcheck_http_remove_headers",
333 "input": baseYaml.format(
334 [
335 {
336 "health_check": {
337 "http": {
338 "path": "/health",
339 "remove_request_headers": ["fruit-one", "fruit-two", "fruit-three"],
340 }
341 }
342 }
343 ]
344 ),
345 "expected": [
346 {
347 "http_health_check": {
348 "path": "/health",
349 "request_headers_to_remove": ["fruit-one", "fruit-two", "fruit-three"],
350 }
351 },
352 ],
353 },
354 { # Test that we throw out the health check config when there is no endpoint resolver
355 "name": "healthcheck_no_endpoint",
356 "input": noEndpointYaml.format([{"health_check": {"http": {"path": "/health"}}}]),
357 "expected": None,
358 },
359 ]
360
361 for case in testcases:
362
363 caseYaml = case["input"]
364 testName = case["name"]
365 econf = _get_envoy_config(caseYaml)
366 cluster = _get_cluster_config(econf.clusters, "cluster_coolsvcname_default")
367 assert cluster != False
368
369 expectedChecks = case["expected"]
370 if expectedChecks is None:
371 assert "health_checks" not in cluster, "Failed healthcheck test {}".format(testName)
372 else:
373 assert "health_checks" in cluster, "Failed healthcheck test {}".format(testName)
374
375 hc = cluster["health_checks"]
376 for i in range(0, len(hc)):
377 actual = hc[i]
378 expected = expectedChecks[i]
379
380 check_healthcheck_defaults(expected, actual, testName)
381
382 if "grpc_health_check" in expected:
383 try:
384 check_grpc_healthcheck(
385 expected["grpc_health_check"], actual["grpc_health_check"], testName
386 )
387 except KeyError:
388 assert True == False, "Failed healthcheck test {}".format(testName)
389 if "http_health_check" in expected:
390 try:
391 check_http_healthcheck(
392 expected["http_health_check"], actual["http_health_check"], testName
393 )
394 except KeyError:
395 assert True == False, "Failed healthcheck test {}".format(testName)
396
397
398# Runs a bunch of assert statments to check that the expected
399# healthcheck fields match the actual ones
400def check_healthcheck_defaults(expected, actual, testName):
401 # check all the default values unless we overrode them
402 if "healthy_threshold" in expected:
403 assert (
404 actual["healthy_threshold"] == expected["healthy_threshold"]
405 ), "Failed healthcheck test {}".format(testName)
406 else:
407 assert actual["healthy_threshold"] == 1, "Failed healthcheck test {}".format(testName)
408
409 if "interval" in expected:
410 assert actual["interval"] == expected["interval"], "Failed healthcheck test {}".format(
411 testName
412 )
413 else:
414 assert actual["interval"] == "5s", "Failed healthcheck test {}".format(testName)
415
416 if "timeout" in expected:
417 assert actual["timeout"] == expected["timeout"], "Failed healthcheck test {}".format(
418 testName
419 )
420 else:
421 assert actual["timeout"] == "3s", "Failed healthcheck test {}".format(testName)
422
423 if "unhealthy_threshold" in expected:
424 assert (
425 actual["unhealthy_threshold"] == expected["unhealthy_threshold"]
426 ), "Failed healthcheck test {}".format(testName)
427 else:
428 assert actual["unhealthy_threshold"] == 2, "Failed healthcheck test {}".format(testName)
429
430
431# Runs a bunch of assert statments to check that the expected
432# grpc health check matches the actual one.
433def check_grpc_healthcheck(expected, actual, testName):
434 if expected is not None:
435 assert actual is not None, "Failed healthcheck test {}".format(testName)
436
437 assert (
438 actual["service_name"] == expected["service_name"]
439 ), "Failed healthcheck test {}".format(testName)
440
441 if "authority" in expected:
442 assert (
443 actual["authority"] == expected["authority"]
444 ), "Failed healthcheck test {}".format(testName)
445
446
447# Runs a bunch of assert statments to check that the expected
448# http health check matches the actual one.
449def check_http_healthcheck(expected, actual, testName):
450 if expected is not None:
451 assert actual is not None, "Failed healthcheck test {}".format(testName)
452
453 assert actual["path"] == expected["path"], "Failed healthcheck test {}".format(testName)
454
455 if "host" in expected:
456 assert actual["host"] == expected["host"], "Failed healthcheck test {}".format(testName)
457
458 if "request_headers_to_remove" in expected:
459 assert (
460 actual["request_headers_to_remove"] == expected["request_headers_to_remove"]
461 ), "Failed healthcheck test {}".format(testName)
462
463 if "request_headers_to_add" in expected:
464 assert (
465 actual["request_headers_to_add"] == expected["request_headers_to_add"]
466 ), "Failed healthcheck test {}".format(testName)
467
468 if "expected_statuses" in expected:
469 assert (
470 actual["expected_statuses"] == expected["expected_statuses"]
471 ), "Failed healthcheck test {}".format(testName)
View as plain text