...

Text file src/github.com/emissary-ingress/emissary/v3/python/tests/unit/test_healthchecks.py

Documentation: github.com/emissary-ingress/emissary/v3/python/tests/unit

     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