1from typing import ClassVar, Generator, Tuple, Union
2
3from abstract_tests import HTTP, AmbassadorTest, Node, ServiceType
4from kat.harness import Query
5from tests.selfsigned import TLSCerts
6
7SECRETS = (
8 """
9---
10apiVersion: v1
11metadata:
12 name: {self.path.k8s}-client-cert-secret
13data:
14 tls.crt: """
15 + TLSCerts["master.datawire.io"].k8s_crt
16 + """
17kind: Secret
18type: Opaque
19"""
20)
21
22
23class ConsulPod(ServiceType):
24 skip_variant: ClassVar[bool] = True
25 service_account_name: str
26 datacenter_json: str
27
28 def __init__(self, service_account_name: str, datacenter_json: str, *args, **kwargs) -> None:
29 self.service_account_name = service_account_name
30 self.datacenter_json = datacenter_json
31 kwargs[
32 "service_manifests"
33 ] = """
34---
35apiVersion: v1
36kind: Service
37metadata:
38 name: {self.path.k8s}
39spec:
40 type: ClusterIP
41 ports:
42 - name: consul
43 protocol: TCP
44 port: 8500
45 targetPort: 8500
46 selector:
47 backend: {self.path.k8s}
48---
49apiVersion: v1
50kind: Pod
51metadata:
52 name: {self.path.k8s}
53 annotations:
54 sidecar.istio.io/inject: "false"
55 labels:
56 backend: {self.path.k8s}
57spec:
58 serviceAccountName: {self.service_account_name}
59 containers:
60 - name: consul
61 image: consul:1.4.3
62 env:
63 - name: CONSUL_LOCAL_CONFIG
64 value: "{self.datacenter_json}"
65 restartPolicy: Always
66"""
67 super().__init__(*args, **kwargs)
68
69 def requirements(self):
70 yield ("url", Query("http://%s:8500/ui/" % self.path.fqdn))
71
72
73class ConsulTest(AmbassadorTest):
74 k8s_target: ServiceType
75 k8s_ns_target: ServiceType
76 consul_target: ServiceType
77
78 def init(self):
79 self.k8s_target = HTTP(name="k8s")
80 self.k8s_ns_target = HTTP(name="k8s-ns", namespace="consul-test-namespace")
81
82 # This is the datacenter we'll use.
83 self.datacenter = "dc12"
84
85 # We use Consul's local-config environment variable to set the datacenter name
86 # on the actual Consul pod. That means that we need to supply the datacenter
87 # name in JSON format.
88 #
89 # In a perfect world this would just be
90 #
91 # self.datacenter_dict = { "datacenter": self.datacenter }
92 #
93 # but the world is not perfect, so we have to supply it as JSON with LOTS of
94 # escaping, since this gets passed through self.format (hence two layers of
95 # doubled braces) and JSON decoding (hence backslash-escaped double quotes,
96 # and of course the backslashes themselves have to be escaped...)
97 self.consul_target = ConsulPod(
98 service_account_name="consultest", # `=self.name.k8s`, but self.name isn't set yet
99 datacenter_json=f'{{{{\\"datacenter\\":\\"{self.datacenter}\\"}}}}',
100 )
101
102 def manifests(self) -> str:
103 # Unlike usual, we have stuff both before and after super().manifests():
104 # we want the namespace early, but we want the superclass before our other
105 # manifests, because of some magic with ServiceAccounts?
106 return (
107 self.format(
108 """
109---
110apiVersion: v1
111kind: Namespace
112metadata:
113 name: consul-test-namespace
114"""
115 )
116 + super().manifests()
117 + self.format(
118 """
119---
120apiVersion: getambassador.io/v3alpha1
121kind: ConsulResolver
122metadata:
123 name: {self.path.k8s}-resolver
124spec:
125 ambassador_id: [consultest]
126 address: {self.consul_target.path.k8s}:$CONSUL_WATCHER_PORT
127 datacenter: {self.datacenter}
128---
129apiVersion: getambassador.io/v3alpha1
130kind: Mapping
131metadata:
132 name: {self.path.k8s}-consul-ns-mapping
133 namespace: consul-test-namespace
134spec:
135 ambassador_id: [consultest]
136 hostname: "*"
137 prefix: /{self.path.k8s}_consul_ns/
138 service: {self.path.k8s}-consul-ns-service
139 resolver: {self.path.k8s}-resolver
140 load_balancer:
141 policy: round_robin
142"""
143 + SECRETS
144 )
145 )
146
147 def config(self) -> Generator[Union[str, Tuple[Node, str]], None, None]:
148 yield self.k8s_target, self.format(
149 """
150---
151apiVersion: getambassador.io/v3alpha1
152kind: Mapping
153name: {self.path.k8s}_k8s_mapping
154hostname: "*"
155prefix: /{self.path.k8s}_k8s/
156service: {self.k8s_target.path.k8s}
157---
158apiVersion: getambassador.io/v3alpha1
159kind: Mapping
160name: {self.path.k8s}_consul_mapping
161hostname: "*"
162prefix: /{self.path.k8s}_consul/
163service: {self.path.k8s}-consul-service
164# tls: {self.path.k8s}-client-context # this doesn't seem to work... ambassador complains with "no private key in secret ..."
165resolver: {self.path.k8s}-resolver
166load_balancer:
167 policy: round_robin
168---
169apiVersion: getambassador.io/v3alpha1
170kind: Mapping
171name: {self.path.k8s}_consul_node_mapping
172hostname: "*"
173prefix: /{self.path.k8s}_consul_node/ # this is testing that Ambassador correctly falls back to the `Address` if `Service.Address` does not exist
174service: {self.path.k8s}-consul-node
175# tls: {self.path.k8s}-client-context # this doesn't seem to work... ambassador complains with "no private key in secret ..."
176resolver: {self.path.k8s}-resolver
177load_balancer:
178 policy: round_robin
179---
180kind: TLSContext
181name: {self.path.k8s}-client-context
182secret: {self.path.k8s}-client-cert-secret
183---
184apiVersion: getambassador.io/v3alpha1
185kind: Host
186name: {self.path.k8s}-client-host
187requestPolicy:
188 insecure:
189 action: Route
190"""
191 )
192
193 def queries(self):
194 # Deregister the Consul services in phase 0.
195 yield Query(
196 self.format("http://{self.consul_target.path.k8s}:8500/v1/catalog/deregister"),
197 method="PUT",
198 body={
199 "Datacenter": self.datacenter,
200 "Node": self.format("{self.path.k8s}-consul-service"),
201 },
202 phase=0,
203 )
204 yield Query(
205 self.format("http://{self.consul_target.path.k8s}:8500/v1/catalog/deregister"),
206 method="PUT",
207 body={
208 "Datacenter": self.datacenter,
209 "Node": self.format("{self.path.k8s}-consul-ns-service"),
210 },
211 phase=0,
212 )
213 yield Query(
214 self.format("http://{self.consul_target.path.k8s}:8500/v1/catalog/deregister"),
215 method="PUT",
216 body={
217 "Datacenter": self.datacenter,
218 "Node": self.format("{self.path.k8s}-consul-node"),
219 },
220 phase=0,
221 )
222
223 # The K8s service should be OK. The Consul services should 503 since they have no upstreams
224 # in phase 1.
225 yield Query(self.url(self.format("{self.path.k8s}_k8s/")), expected=200, phase=1)
226 yield Query(self.url(self.format("{self.path.k8s}_consul/")), expected=503, phase=1)
227 yield Query(self.url(self.format("{self.path.k8s}_consul_ns/")), expected=503, phase=1)
228 yield Query(self.url(self.format("{self.path.k8s}_consul_node/")), expected=503, phase=1)
229
230 # Register the Consul services in phase 2.
231 yield Query(
232 self.format("http://{self.consul_target.path.k8s}:8500/v1/catalog/register"),
233 method="PUT",
234 body={
235 "Datacenter": self.datacenter,
236 "Node": self.format("{self.path.k8s}-consul-service"),
237 "Address": self.k8s_target.path.k8s,
238 "Service": {
239 "Service": self.format("{self.path.k8s}-consul-service"),
240 "Address": self.k8s_target.path.k8s,
241 "Port": 80,
242 },
243 },
244 phase=2,
245 )
246 yield Query(
247 self.format("http://{self.consul_target.path.k8s}:8500/v1/catalog/register"),
248 method="PUT",
249 body={
250 "Datacenter": self.datacenter,
251 "Node": self.format("{self.path.k8s}-consul-ns-service"),
252 "Address": self.format("{self.k8s_ns_target.path.k8s}.consul-test-namespace"),
253 "Service": {
254 "Service": self.format("{self.path.k8s}-consul-ns-service"),
255 "Address": self.format("{self.k8s_ns_target.path.k8s}.consul-test-namespace"),
256 "Port": 80,
257 },
258 },
259 phase=2,
260 )
261 yield Query(
262 self.format("http://{self.consul_target.path.k8s}:8500/v1/catalog/register"),
263 method="PUT",
264 body={
265 "Datacenter": self.datacenter,
266 "Node": self.format("{self.path.k8s}-consul-node"),
267 "Address": self.k8s_target.path.k8s,
268 "Service": {"Service": self.format("{self.path.k8s}-consul-node"), "Port": 80},
269 },
270 phase=2,
271 )
272
273 # All services should work in phase 3.
274 yield Query(self.url(self.format("{self.path.k8s}_k8s/")), expected=200, phase=3)
275 yield Query(self.url(self.format("{self.path.k8s}_consul/")), expected=200, phase=3)
276 yield Query(self.url(self.format("{self.path.k8s}_consul_ns/")), expected=200, phase=3)
277 yield Query(self.url(self.format("{self.path.k8s}_consul_node/")), expected=200, phase=3)
278
279 def check(self):
280 pass
View as plain text