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