...

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

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

     1import json
     2import logging
     3import os
     4import subprocess
     5import tempfile
     6from base64 import b64encode
     7from collections import namedtuple
     8
     9import pytest
    10from OpenSSL import crypto
    11
    12from ambassador import IR, Cache
    13from ambassador.compile import Compile
    14from ambassador.utils import NullSecretHandler, parse_bool
    15
    16logger = logging.getLogger("ambassador")
    17
    18
    19def zipkin_tracing_service_manifest():
    20    return """
    21---
    22apiVersion: getambassador.io/v3alpha1
    23kind: TracingService
    24metadata:
    25  name: tracing
    26  namespace: ambassador
    27spec:
    28  service: zipkin:9411
    29  driver: zipkin
    30  config: {}
    31"""
    32
    33
    34def default_listener_manifests():
    35    return """
    36---
    37apiVersion: getambassador.io/v3alpha1
    38kind: Listener
    39metadata:
    40  name: listener-8080
    41  namespace: default
    42spec:
    43  port: 8080
    44  protocol: HTTPS
    45  securityModel: XFP
    46  hostBinding:
    47    namespace:
    48      from: ALL
    49---
    50apiVersion: getambassador.io/v3alpha1
    51kind: Listener
    52metadata:
    53  name: listener-8443
    54  namespace: default
    55spec:
    56  port: 8443
    57  protocol: HTTPS
    58  securityModel: XFP
    59  hostBinding:
    60    namespace:
    61      from: ALL
    62"""
    63
    64
    65def default_http3_listener_manifest():
    66    return """
    67---
    68apiVersion: getambassador.io/v3alpha1
    69kind: Listener
    70metadata:
    71  name: listener-http3-8443
    72  namespace: default
    73spec:
    74  port: 8443
    75  protocolStack:
    76    - TLS
    77    - HTTP
    78    - UDP
    79  securityModel: XFP
    80  hostBinding:
    81    namespace:
    82      from: ALL  
    83  """
    84
    85
    86def default_udp_listener_manifest():
    87    return """
    88---
    89apiVersion: getambassador.io/v3alpha1
    90kind: Listener
    91metadata:
    92  name: listener-udp-8443
    93  namespace: default
    94spec:
    95  port: 8443
    96  protocolStack:
    97    - TLS
    98    - UDP
    99  securityModel: XFP
   100  hostBinding:
   101    namespace:
   102      from: ALL  
   103  """
   104
   105
   106def default_tcp_listener_manifest():
   107    return """
   108---
   109apiVersion: getambassador.io/v3alpha1
   110kind: Listener
   111metadata:
   112  name: listener-tcp-8443
   113  namespace: default
   114spec:
   115  port: 8443
   116  protocolStack:
   117    - TLS
   118    - TCP
   119  securityModel: XFP
   120  hostBinding:
   121    namespace:
   122      from: ALL  
   123  """
   124
   125
   126def module_and_mapping_manifests(module_confs, mapping_confs):
   127    yaml = (
   128        default_listener_manifests()
   129        + """
   130---
   131apiVersion: getambassador.io/v3alpha1
   132kind: Module
   133metadata:
   134  name: ambassador
   135  namespace: default
   136spec:
   137  config:"""
   138    )
   139    if module_confs:
   140        for module_conf in module_confs:
   141            yaml = (
   142                yaml
   143                + """
   144    {}
   145""".format(
   146                    module_conf
   147                )
   148            )
   149    else:
   150        yaml = yaml + " {}\n"
   151
   152    yaml = (
   153        yaml
   154        + """
   155---
   156apiVersion: getambassador.io/v3alpha1
   157kind: Mapping
   158metadata:
   159  name: ambassador
   160  namespace: default
   161spec:
   162  hostname: "*"
   163  prefix: /httpbin/
   164  service: httpbin"""
   165    )
   166    if mapping_confs:
   167        for mapping_conf in mapping_confs:
   168            yaml = (
   169                yaml
   170                + """
   171  {}""".format(
   172                    mapping_conf
   173                )
   174            )
   175    return yaml
   176
   177
   178def _require_no_errors(ir: IR):
   179    assert ir.aconf.errors == {}, f"{repr(ir.aconf.errors)}"
   180
   181
   182def _secret_handler():
   183    source_root = tempfile.TemporaryDirectory(prefix="null-secret-", suffix="-source")
   184    cache_dir = tempfile.TemporaryDirectory(prefix="null-secret-", suffix="-cache")
   185    return NullSecretHandler(logger, source_root.name, cache_dir.name, "fake")
   186
   187
   188def compile_with_cachecheck(yaml, errors_ok=False):
   189    # Compile with and without a cache. Neither should produce errors.
   190    cache = Cache(logger)
   191    secret_handler = _secret_handler()
   192    r1 = Compile(logger, yaml, k8s=True, secret_handler=secret_handler)
   193    r2 = Compile(logger, yaml, k8s=True, secret_handler=secret_handler, cache=cache)
   194
   195    if not errors_ok:
   196        _require_no_errors(r1["ir"])
   197        _require_no_errors(r2["ir"])
   198
   199    # Both should produce equal Envoy config as sorted json.
   200    r1j = json.dumps(r1["xds"].as_dict(), sort_keys=True, indent=2)
   201    r2j = json.dumps(r2["xds"].as_dict(), sort_keys=True, indent=2)
   202    assert r1j == r2j
   203
   204    # All good.
   205    return r1
   206
   207
   208EnvoyFilterInfo = namedtuple("EnvoyFilterInfo", ["name", "type"])
   209
   210EnvoyHCMInfo = EnvoyFilterInfo(
   211    name="envoy.filters.network.http_connection_manager",
   212    type="type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager",
   213)
   214
   215EnvoyTCPInfo = EnvoyFilterInfo(
   216    name="envoy.filters.network.tcp_proxy",
   217    type="type.googleapis.com/envoy.extensions.filters.network.tcp_proxy.v3.TcpProxy",
   218)
   219
   220
   221def econf_compile(yaml):
   222    compiled = compile_with_cachecheck(yaml)
   223    return compiled["xds"].as_dict()
   224
   225
   226def econf_foreach_listener(econf, fn, listener_count=1):
   227    listeners = econf["static_resources"]["listeners"]
   228
   229    wanted_plural = "" if (listener_count == 1) else "s"
   230    assert (
   231        len(listeners) == listener_count
   232    ), f"Expected {listener_count} listener{wanted_plural}, got {len(listeners)}"
   233
   234    for listener in listeners:
   235        fn(listener)
   236
   237
   238def econf_foreach_listener_chain(
   239    listener, fn, chain_count=2, need_name=None, need_type=None, dump_info=None
   240):
   241    # We need a specific number of filter chains. Normally it's 2,
   242    # since the compiler tests don't generally supply Listeners or Hosts,
   243    # so we get secure and insecure chains.
   244    filter_chains = listener["filter_chains"]
   245
   246    if dump_info:
   247        dump_info(filter_chains)
   248
   249    wanted_plural = "" if (chain_count == 1) else "s"
   250    assert (
   251        len(filter_chains) == chain_count
   252    ), f"Expected {chain_count} filter chain{wanted_plural}, got {len(filter_chains)}"
   253
   254    for chain in filter_chains:
   255        # We expect one filter on this chain.
   256        filters = chain["filters"]
   257        got_count = len(filters)
   258        got_plural = "" if (got_count == 1) else "s"
   259        assert got_count == 1, f"Expected just one filter, got {got_count} filter{got_plural}"
   260
   261        # The http connection manager is the only filter on the chain from the one and only vhost.
   262        filter = filters[0]
   263
   264        if need_name:
   265            assert filter["name"] == need_name
   266
   267        typed_config = filter["typed_config"]
   268
   269        if need_type:
   270            assert (
   271                typed_config["@type"] == need_type
   272            ), f"bad type: got {repr(typed_config['@type'])} but expected {repr(need_type)}"
   273
   274        fn(typed_config)
   275
   276
   277def econf_foreach_hcm(econf, fn, chain_count=2):
   278    for listener in econf["static_resources"]["listeners"]:
   279        if listener["name"].startswith("ambassador-listener-ready"):
   280            # don't want to test the ready listener since it's different from the default 8080/8443
   281            # listeners and is already tested in test_ready.py
   282            continue
   283        hcm_info = EnvoyHCMInfo
   284
   285        econf_foreach_listener_chain(
   286            listener, fn, chain_count=chain_count, need_name=hcm_info.name, need_type=hcm_info.type
   287        )
   288
   289
   290def econf_foreach_cluster(econf, fn, name="cluster_httpbin_default"):
   291    for cluster in econf["static_resources"]["clusters"]:
   292        if cluster["name"] != name:
   293            continue
   294
   295        found_cluster = True
   296        r = fn(cluster)
   297        if not r:
   298            break
   299    assert found_cluster
   300
   301
   302def assert_valid_envoy_config(config_dict, extra_dirs=[]):
   303    with tempfile.TemporaryDirectory() as tmpdir:
   304        econf = open(os.path.join(tmpdir, "econf.json"), "xt")
   305        econf.write(json.dumps(config_dict))
   306        econf.close()
   307        img = os.environ.get("ENVOY_DOCKER_TAG")
   308        assert img
   309        cmd = [
   310            "docker",
   311            "run",
   312            "--rm",
   313            f"--volume={tmpdir}:/ambassador:ro",
   314            *[f"--volume={extra_dir}:{extra_dir}:ro" for extra_dir in extra_dirs],
   315            img,
   316            "/usr/local/bin/envoy-static-stripped",
   317            "--config-path",
   318            "/ambassador/econf.json",
   319            "--mode",
   320            "validate",
   321        ]
   322        p = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
   323        if p.returncode != 0:
   324            print(p.stdout.decode())
   325        p.check_returncode()
   326
   327
   328def create_crl_pem_b64(issuerCert, issuerKey, revokedCerts):
   329    when = b"20220516010101Z"
   330    crl = crypto.CRL()
   331    crl.set_lastUpdate(when)
   332
   333    for revokedCert in revokedCerts:
   334        clientCert = crypto.load_certificate(crypto.FILETYPE_PEM, bytes(revokedCert, "utf-8"))
   335        r = crypto.Revoked()
   336        r.set_serial(bytes("{:x}".format(clientCert.get_serial_number()), "ascii"))
   337        r.set_rev_date(when)
   338        r.set_reason(None)
   339        crl.add_revoked(r)
   340
   341    cert = crypto.load_certificate(crypto.FILETYPE_PEM, bytes(issuerCert, "utf-8"))
   342    key = crypto.load_privatekey(crypto.FILETYPE_PEM, bytes(issuerKey, "utf-8"))
   343    crl.sign(cert, key, b"sha256")
   344    return b64encode(
   345        (crypto.dump_crl(crypto.FILETYPE_PEM, crl).decode("utf-8") + "\n").encode("utf-8")
   346    ).decode("utf-8")
   347
   348
   349def skip_edgestack():
   350    isEdgeStack = parse_bool(os.environ.get("EDGE_STACK", "false"))
   351
   352    return pytest.mark.skipif(
   353        isEdgeStack,
   354        reason=f"Skipping because EdgeStack behaves differently and tested separately",
   355    )

View as plain text