...

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

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

     1from typing import ClassVar, Generator, Tuple, Union
     2
     3from abstract_tests import HTTP, MappingTest, Node, ServiceType
     4from kat.harness import Query
     5
     6
     7class ShadowBackend(ServiceType):
     8    skip_variant: ClassVar[bool] = True
     9
    10    def __init__(self, *args, **kwargs) -> None:
    11        kwargs[
    12            "service_manifests"
    13        ] = """
    14---
    15apiVersion: v1
    16kind: Service
    17metadata:
    18  name: {self.path.k8s}
    19spec:
    20  selector:
    21    backend: {self.path.k8s}
    22  ports:
    23  - port: 80
    24    name: http
    25    targetPort: http
    26  type: ClusterIP
    27---
    28apiVersion: apps/v1
    29kind: Deployment
    30metadata:
    31  name: {self.path.k8s}
    32spec:
    33  selector:
    34    matchLabels:
    35      backend: {self.path.k8s}
    36  replicas: 1
    37  strategy:
    38    type: RollingUpdate
    39  template:
    40    metadata:
    41      labels:
    42        backend: {self.path.k8s}
    43    spec:
    44      containers:
    45      - name: shadow
    46        image: {images[test-shadow]}
    47        ports:
    48        - name: http
    49          containerPort: 3000
    50"""
    51        super().__init__(*args, **kwargs)
    52
    53    def requirements(self):
    54        yield ("url", Query(f"http://{self.path.fqdn}/clear/"))
    55
    56
    57class ShadowTestCANFLAKE(MappingTest):
    58    shadow: ServiceType
    59
    60    # XXX This type: ignore is here because we're deliberately overriding the
    61    # parent's init to have a different signature... but it's also intimately
    62    # (nay, incestuously) related to the variant()'s yield() above, and I really
    63    # don't want to deal with that right now. So. We'll deal with it later.
    64    def init(self) -> None:  # type: ignore
    65        MappingTest.init(self, HTTP(name="target"))
    66        self.shadow = ShadowBackend()
    67
    68    def config(self) -> Generator[Union[str, Tuple[Node, str]], None, None]:
    69        yield self.target, self.format(
    70            """
    71---
    72apiVersion: getambassador.io/v3alpha1
    73kind: Mapping
    74name:  {self.name}-target
    75hostname: "*"
    76prefix: /{self.name}/mark/
    77rewrite: /mark/
    78service: https://{self.target.path.fqdn}
    79---
    80apiVersion: getambassador.io/v3alpha1
    81kind: Mapping
    82name:  {self.name}-weighted-target
    83hostname: "*"
    84prefix: /{self.name}/weighted-mark/
    85rewrite: /mark/
    86service: https://{self.target.path.fqdn}
    87---
    88apiVersion: getambassador.io/v3alpha1
    89kind: Mapping
    90name:  {self.name}-shadow
    91hostname: "*"
    92prefix: /{self.name}/mark/
    93rewrite: /mark/
    94service: {self.shadow.path.fqdn}
    95shadow: true
    96---
    97apiVersion: getambassador.io/v3alpha1
    98kind: Mapping
    99name:  {self.name}-weighted-shadow
   100hostname: "*"
   101prefix: /{self.name}/weighted-mark/
   102rewrite: /mark/
   103service: {self.shadow.path.fqdn}
   104weight: 10
   105shadow: true
   106---
   107apiVersion: getambassador.io/v3alpha1
   108kind: Mapping
   109name:  {self.name}-checkshadow
   110hostname: "*"
   111prefix: /{self.name}/check/
   112rewrite: /check/
   113service: {self.shadow.path.fqdn}
   114"""
   115        )
   116
   117    def queries(self):
   118        # There should be no Ambassador errors. At all.
   119        yield Query(self.parent.url("ambassador/v0/diag/?json=true&filter=errors"), phase=1)
   120
   121        for i in range(100):
   122            # First query marks one bucket from 0 - 9. The main target service is just a
   123            # normal KAT backend so nothing funky happens there, but it's also shadowed
   124            # to our shadow service that's tallying calls by bucket. So, basically, each
   125            # shadow bucket 0-9 should end up with 10 call.s
   126            bucket = i % 10
   127            yield Query(self.parent.url(f"{self.name}/mark/{bucket}"))
   128
   129        for i in range(500):
   130            # We also do a call to weighted-mark, which is exactly the same _but_ the
   131            # shadow is just 20%. So instead of 50 calls per bucket, we should expect
   132            # 10.
   133            #
   134            # We use different bucket numbers so we can tell which call was which
   135            # shadow.
   136
   137            bucket = (i % 10) + 100
   138            yield Query(self.parent.url(f"{self.name}/weighted-mark/{bucket}"))
   139
   140        # Finally, in phase 2, grab the bucket counts.
   141        yield Query(self.parent.url("%s/check/" % self.name), phase=2)
   142
   143    def check(self):
   144        # XXX Ew. If self.results[0].json is empty, the harness won't convert it to a response.
   145        errors = self.results[0].json or {}
   146
   147        # We shouldn't have any missing-CRD-types errors any more.
   148        for source, error in errors:
   149            if ("could not find" in error) and ("CRD definitions" in error):
   150                assert False, f"Missing CRDs: {error}"
   151
   152            if "Ingress resources" in error:
   153                assert False, f"Ingress resource error: {error}"
   154
   155        # The default errors assume that we have missing CRDs, and that's not correct any more,
   156        # so don't try to use assert_default_errors here.
   157
   158        for result in self.results:
   159            if "mark" in result.query.url:
   160                assert not result.headers.get("X-Shadowed", False)
   161            elif "check" in result.query.url:
   162                data = result.json
   163                weighted_total = 0
   164
   165                for i in range(10):
   166                    # Buckets 0-9 should have 10 per bucket. We'll actually check these values
   167                    # pretty carefully, because this bit of routing isn't probabilistic.
   168                    value = data.get(str(i), -1)
   169                    error = abs(value - 10)
   170
   171                    assert error <= 2, f"bucket {i} should have 10 calls, got {value}"
   172
   173                    # Buckets 100-109 should also have 10 per bucket... but honestly, this is
   174                    # a pretty small sample size, and Envoy's randomization seems to kinda suck
   175                    # at small sample sizes. Since we're here to test Ambassador's ability to
   176                    # configure Envoy, rather than trying to test Envoy's ability to properly
   177                    # weight things, we'll just make sure that _some_ calls got into the shadow
   178                    # buckets, and not worry about how many it was exactly.
   179
   180                    wi = i + 100
   181
   182                    value = data.get(str(wi), 0)
   183
   184                    # error = abs(value - 10)
   185                    # assert error <= 2, f'bucket {wi} should have 10 calls, got {value}'
   186
   187                    weighted_total += value
   188
   189                # See above for why we're just doing a >0 check here.
   190                # assert abs(weighted_total - 50) <= 10, f'weighted buckets should have 50 total calls, got {weighted_total}'
   191                assert (
   192                    weighted_total > 0
   193                ), f"weighted buckets should have 50 total calls but got zero"

View as plain text