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