Merge ~barryprice/charm-k8s-bind/+git/charm-k8s-bind:sidecar into charm-k8s-bind:master
- Git
- lp:~barryprice/charm-k8s-bind/+git/charm-k8s-bind
- sidecar
- Merge into master
Status: | Work in progress |
---|---|
Proposed branch: | ~barryprice/charm-k8s-bind/+git/charm-k8s-bind:sidecar |
Merge into: | charm-k8s-bind:master |
Diff against target: |
749 lines (+206/-370) 9 files modified
.jujuignore (+10/-5) Makefile (+10/-3) README.md (+1/-1) config.yaml (+0/-29) metadata.yaml (+7/-3) requirements.txt (+1/-1) src/charm.py (+89/-99) tests/unit/test_charm.py (+87/-228) tox.ini (+1/-1) |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Bind Charmers | Pending | ||
Review via email: mp+402876@code.launchpad.net |
Commit message
WIP sidecar conversion for bind (test coverage not yet complete), with a few tweaks to base files
Description of the change
- 492731a... by Barry Price
-
Rename charm for consistency with others
- 66bae80... by Barry Price
-
Bugfixes
- 0511d12... by Barry Price
-
Bind, not Mattermost!
- 0ae5151... by Barry Price
-
Remove duplicate hook
- 270c6f0... by Barry Price
-
Use the new image spec with a single wrapper to apply config and run the service
- 3c06f70... by Barry Price
-
Remove obsolete unit tests (i.e. most of them)
- 0056497... by Barry Price
-
Address conflicts
- a353662... by Barry Price
-
Merge from master, address conflicts
- 1646220... by Barry Price
-
Add tests, bring us up to 55% coverage
- 1594dbe... by Barry Price
-
Add HTML test coverage report
- eb36238... by Barry Price
-
Add a few more tests
- 059eb95... by Barry Price
-
More test fixes, giving us 68% coverage
- 6a7adf0... by Barry Price
-
Pull from upstream, enable RFC1918 net recursion by default, add tests to 69% coverage
- 7f3608e... by Barry Price
-
Pull from master, fix conflicts
- 3334949... by Barry Price
-
Pull from master, fix conflicts
- e9fb7e4... by Barry Price
-
Charmcraft should pack, not build
- 44c1096... by Barry Price
-
Add logging/verbosity throughout
- 74d5bf1... by Barry Price
-
Move wrapper logging inside the script, fix the exec bug
- 6c1203c... by Barry Price
-
Move wrapper logging into the pebble command
- 6baed5e... by Barry Price
-
Restore semicolons to wrapper script
- 9e4f75e... by Barry Price
-
Restore default config if recursion is toggled off post-install
- 3e2c7cb... by Barry Price
-
Merge from master, address conflicts
- 3f0d665... by Barry Price
-
Merge from master, address conflicts
- f524d71... by Barry Price
-
Merge branch 'master' into sidecar
Unmerged commits
- f524d71... by Barry Price
-
Merge branch 'master' into sidecar
- 3f0d665... by Barry Price
-
Merge from master, address conflicts
- 3e2c7cb... by Barry Price
-
Merge from master, address conflicts
- 9e4f75e... by Barry Price
-
Restore default config if recursion is toggled off post-install
- 6baed5e... by Barry Price
-
Restore semicolons to wrapper script
- 6c1203c... by Barry Price
-
Move wrapper logging into the pebble command
- 74d5bf1... by Barry Price
-
Move wrapper logging inside the script, fix the exec bug
- 44c1096... by Barry Price
-
Add logging/verbosity throughout
- e9fb7e4... by Barry Price
-
Charmcraft should pack, not build
- 3334949... by Barry Price
-
Pull from master, fix conflicts
Preview Diff
1 | diff --git a/.jujuignore b/.jujuignore |
2 | index 128851d..2a8f147 100644 |
3 | --- a/.jujuignore |
4 | +++ b/.jujuignore |
5 | @@ -1,9 +1,14 @@ |
6 | -*~ |
7 | *.charm |
8 | +*.py[cod] |
9 | +*~ |
10 | .coverage |
11 | .gitignore |
12 | -__pycache__ |
13 | -/dockerfile |
14 | -/image-scripts/ |
15 | -/tests/ |
16 | +/Dockerfile |
17 | /Makefile |
18 | +/env |
19 | +/htmlcov/ |
20 | +/image-scripts/ |
21 | +__pycache__ |
22 | +requirements.txt |
23 | +tests/ |
24 | +tox.ini |
25 | diff --git a/Makefile b/Makefile |
26 | index 564aeb9..9869bc8 100644 |
27 | --- a/Makefile |
28 | +++ b/Makefile |
29 | @@ -5,11 +5,16 @@ blacken: |
30 | @echo "Normalising python layout with black." |
31 | @tox -e black |
32 | |
33 | - |
34 | lint: blacken |
35 | @echo "Running flake8" |
36 | @tox -e lint |
37 | |
38 | +deps: |
39 | + @echo "Checking charmcraft is present." |
40 | + @command -v charmcraft >/dev/null || { echo "Please install charmcraft to continue ('sudo snap install charmcraft')" && false; } |
41 | + @echo "Checking tox is present." |
42 | + @command -v tox >/dev/null || { echo "Please install tox to continue ('sudo apt-get install tox')" && false; } |
43 | + |
44 | # We actually use the build directory created by charmcraft, |
45 | # but the .charm file makes a much more convenient sentinel. |
46 | unittest: bind-k8s.charm |
47 | @@ -22,11 +27,13 @@ clean: |
48 | @git clean -fXd |
49 | |
50 | bind-k8s.charm: src/*.py requirements.txt |
51 | - charmcraft build |
52 | + charmcraft pack |
53 | |
54 | image-deps: |
55 | @echo "Checking shellcheck is present." |
56 | @command -v shellcheck >/dev/null || { echo "Please install shellcheck to continue ('sudo snap install shellcheck')" && false; } |
57 | + @echo "Checking docker is present." |
58 | + @command -v docker >/dev/null || { echo "Please install docker to continue ('sudo apt-get install docker.io')" && false; } |
59 | |
60 | image-lint: image-deps |
61 | @echo "Running shellcheck." |
62 | @@ -43,4 +50,4 @@ image-build: image-lint |
63 | -t bind:$(DIST_RELEASE)-latest \ |
64 | . |
65 | |
66 | -.PHONY: blacken lint unittest test clean image-deps image-lint image-build |
67 | +.PHONY: blacken lint deps unittest test clean image-deps image-lint image-build |
68 | diff --git a/README.md b/README.md |
69 | index 8a2b7e6..c4f750f 100644 |
70 | --- a/README.md |
71 | +++ b/README.md |
72 | @@ -17,7 +17,7 @@ details on using Juju with MicroK8s for easy local testing [see here](https://ju |
73 | |
74 | To deploy this charm in a juju k8s model: |
75 | ``` |
76 | -juju deploy bind-k8s |
77 | +juju deploy bind-k8s --channel edge |
78 | ``` |
79 | The charm will deploy bind with its stock Ubuntu package configuration, which |
80 | will forward all queries to root name servers. DNSSEC is enabled by default. |
81 | diff --git a/config.yaml b/config.yaml |
82 | index 9b1de1d..e65e986 100644 |
83 | --- a/config.yaml |
84 | +++ b/config.yaml |
85 | @@ -1,33 +1,4 @@ |
86 | options: |
87 | - bind_image_path: |
88 | - type: string |
89 | - description: | |
90 | - The location of the image to use, e.g. "registry.example.com/bind:v1". |
91 | - |
92 | - This setting is required. |
93 | - default: "bindcharmers/bind:v1.0-20.04_edge" |
94 | - bind_image_username: |
95 | - type: string |
96 | - description: "Username to use for the configured image registry, if required" |
97 | - default: "" |
98 | - bind_image_password: |
99 | - type: string |
100 | - description: "Password to use for the configured image registry, if required" |
101 | - default: "" |
102 | - container_config: |
103 | - type: string |
104 | - description: > |
105 | - YAML formatted map of container config keys & values. These are |
106 | - generally accessed from inside the image as environment variables. |
107 | - Use to configure customized Wordpress images. This configuration |
108 | - gets logged; use container_secrets for secrets. |
109 | - default: "" |
110 | - container_secrets: |
111 | - type: string |
112 | - description: > |
113 | - YAML formatted map of secrets. Works just like container_config, |
114 | - except that values should not be logged. |
115 | - default: "" |
116 | custom_config_repo: |
117 | type: string |
118 | description: | |
119 | diff --git a/metadata.yaml b/metadata.yaml |
120 | index 6c09e29..87d17fc 100644 |
121 | --- a/metadata.yaml |
122 | +++ b/metadata.yaml |
123 | @@ -5,11 +5,15 @@ docs: https://discourse.charmhub.io/t/bind-documentation-overview/3973 |
124 | description: | |
125 | The original, complete open source DNS implementation. |
126 | https://www.isc.org/bind/ |
127 | -min-juju-version: 2.8.0 |
128 | maintainers: |
129 | - https://launchpad.net/~bind-charmers <bind-charmers@lists.launchpad.net> |
130 | tags: |
131 | - network |
132 | - ops |
133 | -series: |
134 | - - kubernetes |
135 | +containers: |
136 | + bind: |
137 | + resource: bind-image |
138 | +resources: |
139 | + bind-image: |
140 | + type: oci-image |
141 | + description: Docker image for Bind to run |
142 | diff --git a/requirements.txt b/requirements.txt |
143 | index 2d81d3b..fcf2df8 100644 |
144 | --- a/requirements.txt |
145 | +++ b/requirements.txt |
146 | @@ -1 +1 @@ |
147 | -ops |
148 | +https://github.com/canonical/operator/archive/refs/heads/master.zip |
149 | diff --git a/src/charm.py b/src/charm.py |
150 | index 964922c..c9383e0 100755 |
151 | --- a/src/charm.py |
152 | +++ b/src/charm.py |
153 | @@ -5,29 +5,96 @@ |
154 | |
155 | import logging |
156 | from ops.charm import CharmBase |
157 | +from ops.framework import StoredState |
158 | from ops.main import main |
159 | -from ops.model import ActiveStatus, MaintenanceStatus |
160 | -from pprint import pformat |
161 | -from yaml import safe_load |
162 | +from ops.model import ActiveStatus, BlockedStatus, MaintenanceStatus, WaitingStatus |
163 | |
164 | -logger = logging.getLogger() |
165 | +logger = logging.getLogger(__name__) |
166 | |
167 | -REQUIRED_SETTINGS = ['bind_image_path'] |
168 | +CONTAINER_NAME = 'bind' # per metadata.yaml |
169 | +REQUIRED_SETTINGS = [] |
170 | |
171 | |
172 | class BindK8sCharm(CharmBase): |
173 | + |
174 | + state = StoredState() |
175 | + |
176 | def __init__(self, *args): |
177 | """Initialise our class, we only care about 'start' and 'config-changed' hooks.""" |
178 | super().__init__(*args) |
179 | - self.framework.observe(self.on.start, self.on_config_changed) |
180 | - self.framework.observe(self.on.config_changed, self.on_config_changed) |
181 | + |
182 | + self.framework.observe(self.on.config_changed, self._on_config_changed) |
183 | + self.framework.observe(self.on.bind_pebble_ready, self._on_bind_pebble_ready) |
184 | + |
185 | + # state |
186 | + self.state.set_default(bind_pebble_ready=False) |
187 | + |
188 | + def _get_pebble_config(self): |
189 | + """Generate our pebble config.""" |
190 | + logger.debug('Generating pebble config') |
191 | + env_config = self._generate_env_config() |
192 | + pebble_config = { |
193 | + "summary": "Bind layer", |
194 | + "description": "Bind layer", |
195 | + "services": { |
196 | + "bind": { |
197 | + "override": "replace", |
198 | + "summary": "Bind service", |
199 | + "command": 'bash -c "/usr/local/bin/docker-wrapper.sh &> /proc/1/fd/1"', |
200 | + "startup": "enabled", |
201 | + "environment": env_config, |
202 | + } |
203 | + }, |
204 | + } |
205 | + return pebble_config |
206 | + |
207 | + def _on_bind_pebble_ready(self, event): |
208 | + """Handle the on pebble ready event.""" |
209 | + logger.debug('Setting self.state.bind_pebble_ready to True') |
210 | + self.state.bind_pebble_ready = True |
211 | + |
212 | + def _on_config_changed(self, event): |
213 | + """Handle config-changed event.""" |
214 | + problems = self._check_for_config_problems() |
215 | + if problems: |
216 | + self.unit.status = BlockedStatus(problems) |
217 | + return |
218 | + |
219 | + if not self.state.bind_pebble_ready: |
220 | + logger.debug('Waiting for pebble') |
221 | + self.unit.status = WaitingStatus('Waiting for pebble') |
222 | + event.defer() |
223 | + return |
224 | + |
225 | + pebble_config = self._get_pebble_config() |
226 | + if not pebble_config: |
227 | + # Charm will be in blocked status. |
228 | + event.defer() |
229 | + return |
230 | + |
231 | + container = self.unit.get_container(CONTAINER_NAME) |
232 | + services = container.get_plan().to_dict().get("services", {}) |
233 | + if services != pebble_config["services"]: |
234 | + logger.debug('Adding layer to pebble') |
235 | + self.unit.status = MaintenanceStatus('Adding layer to pebble') |
236 | + container.add_layer("bind", pebble_config, combine=True) |
237 | + |
238 | + self.unit.status = MaintenanceStatus('Restarting bind') |
239 | + service = container.get_service("bind") |
240 | + if service.is_running(): |
241 | + logger.debug('Stopping bind') |
242 | + container.stop("bind") |
243 | + logger.debug('Starting bind') |
244 | + container.start("bind") |
245 | + |
246 | + self.unit.status = ActiveStatus() |
247 | |
248 | def _check_for_config_problems(self): |
249 | """Return a string describing any configuration problems (or an empty string if none).""" |
250 | problems = '' |
251 | |
252 | missing = self._missing_charm_settings() |
253 | - if missing: |
254 | + if missing: # pragma: no cover |
255 | problems = 'required setting(s) empty: {}'.format(', '.join(sorted(missing))) |
256 | |
257 | return problems |
258 | @@ -38,104 +105,27 @@ class BindK8sCharm(CharmBase): |
259 | |
260 | missing = {setting for setting in REQUIRED_SETTINGS if not config[setting]} |
261 | |
262 | - if config['bind_image_username'] and not config['bind_image_password']: |
263 | - missing.add('bind_image_password') |
264 | - |
265 | return missing |
266 | |
267 | - def on_config_changed(self, event): |
268 | - """Check that we're leader, and if so, set up the pod.""" |
269 | - if self.model.unit.is_leader(): |
270 | - # Only the leader can set_spec(). |
271 | - resources = self.make_pod_resources() |
272 | - spec = self.make_pod_spec() |
273 | - spec.update(resources) |
274 | - |
275 | - msg = "Configuring pod" |
276 | - logger.info(msg) |
277 | - self.model.unit.status = MaintenanceStatus(msg) |
278 | - self.model.pod.set_spec(spec) |
279 | - |
280 | - msg = "Pod configured" |
281 | - logger.info(msg) |
282 | - self.model.unit.status = ActiveStatus(msg) |
283 | - else: |
284 | - logger.info("Spec changes ignored by non-leader") |
285 | - self.model.unit.status = ActiveStatus() |
286 | - |
287 | - def make_pod_resources(self): |
288 | - """Compile and return our pod resources (e.g. ingresses).""" |
289 | - # LP#1889746: We need to define a manual ingress here to work around LP#1889703. |
290 | - resources = {} # TODO |
291 | - logger.info("This is the Kubernetes Pod resources <<EOM\n{}\nEOM".format(pformat(resources))) |
292 | - return resources |
293 | - |
294 | - def generate_pod_config(self, secured=True): |
295 | - """Kubernetes pod config generator. |
296 | - |
297 | - generate_pod_config generates Kubernetes deployment config. |
298 | - If the secured keyword is set then it will return a sanitised copy |
299 | - without exposing secrets. |
300 | - """ |
301 | + def _generate_env_config(self): |
302 | + """Kubernetes environment config generator.""" |
303 | config = self.model.config |
304 | - pod_config = {} |
305 | - if config["container_config"].strip(): |
306 | - pod_config = safe_load(config["container_config"]) |
307 | - |
308 | + env_config = {} |
309 | if config["custom_config_repo"].strip(): |
310 | - pod_config["CUSTOM_CONFIG_REPO"] = config["custom_config_repo"] |
311 | - |
312 | - if config["enable_rfc1918_recursion"]: |
313 | - pod_config["ENABLE_RFC1918_RECURSION"] = 1 |
314 | - |
315 | - if config["https_proxy"].strip(): |
316 | - pod_config["http_proxy"] = config["https_proxy"] |
317 | - pod_config["https_proxy"] = config["https_proxy"] |
318 | - |
319 | - if secured: |
320 | - return pod_config |
321 | - |
322 | - if config["container_secrets"].strip(): |
323 | - container_secrets = safe_load(config["container_secrets"]) |
324 | + logger.debug('Custom config repo set') |
325 | + env_config["CUSTOM_CONFIG_REPO"] = config["custom_config_repo"] |
326 | + elif config["enable_rfc1918_recursion"]: |
327 | + logger.debug('No custom config repo set, recursion will be enabled') |
328 | + env_config["ENABLE_RFC1918_RECURSION"] = 1 |
329 | else: |
330 | - container_secrets = {} |
331 | - |
332 | - pod_config.update(container_secrets) |
333 | - return pod_config |
334 | - |
335 | - def make_pod_spec(self): |
336 | - """Set up and return our full pod spec here.""" |
337 | - config = self.model.config |
338 | - full_pod_config = self.generate_pod_config(secured=False) |
339 | - secure_pod_config = self.generate_pod_config(secured=True) |
340 | - |
341 | - ports = [ |
342 | - {"name": "domain-tcp", "containerPort": 53, "protocol": "TCP"}, |
343 | - {"name": "domain-udp", "containerPort": 53, "protocol": "UDP"}, |
344 | - ] |
345 | - |
346 | - spec = { |
347 | - "version": 2, |
348 | - "containers": [ |
349 | - { |
350 | - "name": self.app.name, |
351 | - "imageDetails": {"imagePath": config["bind_image_path"]}, |
352 | - "ports": ports, |
353 | - "config": secure_pod_config, |
354 | - "kubernetes": {"readinessProbe": {"exec": {"command": ["/usr/local/bin/dns-check.sh"]}}}, |
355 | - } |
356 | - ], |
357 | - } |
358 | - |
359 | - logger.info("This is the Kubernetes Pod spec config (sans secrets) <<EOM\n{}\nEOM".format(pformat(spec))) |
360 | + logger.debug('No custom config repo set, recursion disabled too, deploying with stock config') |
361 | + pass |
362 | |
363 | - if config.get("bind_image_username") and config.get("bind_image_password"): |
364 | - spec.get("containers")[0].get("imageDetails")["username"] = config["bind_image_username"] |
365 | - spec.get("containers")[0].get("imageDetails")["password"] = config["bind_image_password"] |
366 | - |
367 | - secure_pod_config.update(full_pod_config) |
368 | + if config["https_proxy"].strip(): |
369 | + env_config["http_proxy"] = config["https_proxy"] |
370 | + env_config["https_proxy"] = config["https_proxy"] |
371 | |
372 | - return spec |
373 | + return env_config |
374 | |
375 | |
376 | if __name__ == "__main__": # pragma: no cover |
377 | diff --git a/tests/unit/test_charm.py b/tests/unit/test_charm.py |
378 | index c9834d1..1736527 100644 |
379 | --- a/tests/unit/test_charm.py |
380 | +++ b/tests/unit/test_charm.py |
381 | @@ -3,268 +3,127 @@ |
382 | |
383 | import unittest |
384 | |
385 | +from unittest.mock import MagicMock |
386 | + |
387 | from charm import BindK8sCharm |
388 | |
389 | from ops import testing |
390 | -from ops.model import ActiveStatus |
391 | +from ops.model import WaitingStatus |
392 | |
393 | -CONFIG_EMPTY = { |
394 | - 'bind_image_path': '', |
395 | - 'bind_image_username': '', |
396 | - 'bind_image_password': '', |
397 | - 'container_config': '', |
398 | - 'container_secrets': '', |
399 | +CONFIG_VALID = { |
400 | 'custom_config_repo': '', |
401 | + 'enable_rfc1918_recursion': True, |
402 | 'https_proxy': '', |
403 | } |
404 | |
405 | -CONFIG_IMAGE_PASSWORD_MISSING = { |
406 | - 'bind_image_path': 'example.com/bind:v1', |
407 | - 'bind_image_username': 'username', |
408 | - 'bind_image_password': '', |
409 | - 'container_config': '', |
410 | - 'container_secrets': '', |
411 | - 'custom_config_repo': '', |
412 | - 'https_proxy': '', |
413 | -} |
414 | |
415 | -CONFIG_VALID = { |
416 | - 'bind_image_path': 'example.com/bind:v1', |
417 | - 'bind_image_username': '', |
418 | - 'bind_image_password': '', |
419 | - 'container_config': '', |
420 | - 'container_secrets': '', |
421 | - 'custom_config_repo': '', |
422 | - 'https_proxy': '', |
423 | -} |
424 | +def _pebble_services_bind(pebble_config): |
425 | + """Extract bind's service from the given pebble_config.""" |
426 | + return pebble_config['services']['bind'] |
427 | |
428 | -CONFIG_VALID_WITHOUT_RECURSION = { |
429 | - 'bind_image_path': 'example.com/bind:v1', |
430 | - 'bind_image_username': '', |
431 | - 'bind_image_password': '', |
432 | - 'enable_rfc1918_recursion': False, |
433 | - 'container_config': '', |
434 | - 'container_secrets': '', |
435 | - 'custom_config_repo': '', |
436 | - 'https_proxy': '', |
437 | -} |
438 | |
439 | -CONFIG_VALID_WITH_CREDS = { |
440 | - 'bind_image_path': 'secure.example.com/bind:v1', |
441 | - 'bind_image_username': 'test-user', |
442 | - 'bind_image_password': 'test-password', |
443 | - 'container_config': '', |
444 | - 'container_secrets': '', |
445 | - 'custom_config_repo': '', |
446 | - 'https_proxy': '', |
447 | -} |
448 | +def _pebble_services_bind_env(pebble_config): |
449 | + """Extract bind's service environment from the given pebble_config.""" |
450 | + return pebble_config['services']['bind']['environment'] |
451 | |
452 | -CONFIG_VALID_WITH_CONTAINER_CONFIG = { |
453 | - 'bind_image_path': 'example.com/bind:v1', |
454 | - 'bind_image_username': '', |
455 | - 'bind_image_password': '', |
456 | - 'container_config': '"magic_number": 123', |
457 | - 'container_secrets': '', |
458 | - 'custom_config_repo': '', |
459 | - 'https_proxy': '', |
460 | -} |
461 | |
462 | -CONFIG_VALID_WITH_CONTAINER_CONFIG_AND_SECRETS = { |
463 | - 'bind_image_path': 'example.com/bind:v1', |
464 | - 'bind_image_username': '', |
465 | - 'bind_image_password': '', |
466 | - 'container_config': '"magic_number": 123', |
467 | - 'container_secrets': '"secret_password": "xyzzy"', |
468 | - 'custom_config_repo': '', |
469 | - 'https_proxy': '', |
470 | -} |
471 | +def _pebble_summary(pebble_config): |
472 | + """Extract bind's summary from the given pebble_config.""" |
473 | + return pebble_config['summary'] |
474 | |
475 | -CONFIG_VALID_WITH_CUSTOM_CONFIG_REPO_AND_PROXY = { |
476 | - 'bind_image_path': 'example.com/bind:v1', |
477 | - 'bind_image_username': '', |
478 | - 'bind_image_password': '', |
479 | - 'container_config': '', |
480 | - 'container_secrets': '', |
481 | - 'custom_config_repo': 'https://git.example.com/example-bind-config.git', |
482 | - 'https_proxy': 'http://webproxy.example.com:3128/', |
483 | -} |
484 | + |
485 | +def _pebble_description(pebble_config): |
486 | + """Extract bind's description from the given pebble_config.""" |
487 | + return pebble_config['description'] |
488 | |
489 | |
490 | class TestBindK8s(unittest.TestCase): |
491 | maxDiff = None |
492 | |
493 | def setUp(self): |
494 | + """Setup the harness object.""" |
495 | self.harness = testing.Harness(BindK8sCharm) |
496 | self.harness.begin() |
497 | - self.harness.disable_hooks() |
498 | + self.harness.add_oci_resource('bind-image') |
499 | |
500 | - def test_check_for_config_problems_empty_image_path(self): |
501 | - """Confirm that we generate an error if we're not told what image to use.""" |
502 | - self.harness.update_config(CONFIG_EMPTY) |
503 | - expected = 'required setting(s) empty: bind_image_path' |
504 | - self.assertEqual(self.harness.charm._check_for_config_problems(), expected) |
505 | + self._expected = dict() |
506 | |
507 | - def test_check_for_config_problems_empty_image_password(self): |
508 | - """Confirm that we generate an error if we're not given valid registry creds.""" |
509 | - self.harness.update_config(CONFIG_IMAGE_PASSWORD_MISSING) |
510 | - expected = 'required setting(s) empty: bind_image_password' |
511 | - self.assertEqual(self.harness.charm._check_for_config_problems(), expected) |
512 | + def tearDown(self): |
513 | + """Cleanup the harness.""" |
514 | + self.harness.cleanup() |
515 | |
516 | def test_check_for_config_problems_none(self): |
517 | """Confirm that we accept valid config.""" |
518 | self.harness.update_config(CONFIG_VALID) |
519 | - expected = '' |
520 | - self.assertEqual(self.harness.charm._check_for_config_problems(), expected) |
521 | + self._expected = '' |
522 | + self.assertEqual(self.harness.charm._check_for_config_problems(), self._expected) |
523 | |
524 | - def test_make_pod_resources(self): |
525 | - """Confirm that we generate the expected pod resources (see LP#1889746).""" |
526 | - expected = {} |
527 | - self.assertEqual(self.harness.charm.make_pod_resources(), expected) |
528 | - |
529 | - def test_make_pod_spec_basic(self): |
530 | - """Confirm that we generate the expected pod spec from valid config.""" |
531 | + def test_on_bind_pebble_ready_false(self): |
532 | self.harness.update_config(CONFIG_VALID) |
533 | - expected = { |
534 | - 'version': 2, |
535 | - 'containers': [ |
536 | - { |
537 | - 'name': 'bind-k8s', |
538 | - 'imageDetails': {'imagePath': 'example.com/bind:v1'}, |
539 | - 'ports': [ |
540 | - {'containerPort': 53, 'name': 'domain-tcp', 'protocol': 'TCP'}, |
541 | - {'containerPort': 53, 'name': 'domain-udp', 'protocol': 'UDP'}, |
542 | - ], |
543 | - 'config': {'ENABLE_RFC1918_RECURSION': 1}, |
544 | - 'kubernetes': {'readinessProbe': {'exec': {'command': ['/usr/local/bin/dns-check.sh']}}}, |
545 | - } |
546 | - ], |
547 | - } |
548 | - self.assertEqual(self.harness.charm.make_pod_spec(), expected) |
549 | - |
550 | - def test_make_pod_spec_without_recursion(self): |
551 | - """Confirm that we generate the expected pod spec from config disabling recursion.""" |
552 | - self.harness.update_config(CONFIG_VALID_WITHOUT_RECURSION) |
553 | - expected = { |
554 | - 'version': 2, |
555 | - 'containers': [ |
556 | - { |
557 | - 'name': 'bind-k8s', |
558 | - 'imageDetails': {'imagePath': 'example.com/bind:v1'}, |
559 | - 'ports': [ |
560 | - {'containerPort': 53, 'name': 'domain-tcp', 'protocol': 'TCP'}, |
561 | - {'containerPort': 53, 'name': 'domain-udp', 'protocol': 'UDP'}, |
562 | - ], |
563 | - 'config': {}, |
564 | - 'kubernetes': {'readinessProbe': {'exec': {'command': ['/usr/local/bin/dns-check.sh']}}}, |
565 | - } |
566 | - ], |
567 | - } |
568 | - self.assertEqual(self.harness.charm.make_pod_spec(), expected) |
569 | + # the event hasn't run yet, so we expect this to be False |
570 | + self._expected = False |
571 | + self.assertEqual(self.harness.charm.state.bind_pebble_ready, self._expected) |
572 | |
573 | - def test_make_pod_spec_with_creds(self): |
574 | - """Confirm that we generate the expected pod spec from config containing credentials.""" |
575 | - self.harness.update_config(CONFIG_VALID_WITH_CREDS) |
576 | - expected = { |
577 | - 'version': 2, |
578 | - 'containers': [ |
579 | - { |
580 | - 'name': 'bind-k8s', |
581 | - 'imageDetails': { |
582 | - 'imagePath': 'secure.example.com/bind:v1', |
583 | - 'username': 'test-user', |
584 | - 'password': 'test-password', |
585 | - }, |
586 | - 'ports': [ |
587 | - {'containerPort': 53, 'name': 'domain-tcp', 'protocol': 'TCP'}, |
588 | - {'containerPort': 53, 'name': 'domain-udp', 'protocol': 'UDP'}, |
589 | - ], |
590 | - 'config': {'ENABLE_RFC1918_RECURSION': 1}, |
591 | - 'kubernetes': {'readinessProbe': {'exec': {'command': ['/usr/local/bin/dns-check.sh']}}}, |
592 | - } |
593 | - ], |
594 | - } |
595 | - self.assertEqual(self.harness.charm.make_pod_spec(), expected) |
596 | - |
597 | - def test_make_pod_spec_with_extra_config(self): |
598 | - """Confirm that we generate the expected pod spec from a more involved valid config.""" |
599 | - self.harness.update_config(CONFIG_VALID_WITH_CONTAINER_CONFIG) |
600 | - expected = { |
601 | - 'version': 2, |
602 | - 'containers': [ |
603 | - { |
604 | - 'name': 'bind-k8s', |
605 | - 'imageDetails': {'imagePath': 'example.com/bind:v1'}, |
606 | - 'ports': [ |
607 | - {'containerPort': 53, 'name': 'domain-tcp', 'protocol': 'TCP'}, |
608 | - {'containerPort': 53, 'name': 'domain-udp', 'protocol': 'UDP'}, |
609 | - ], |
610 | - 'config': { |
611 | - 'ENABLE_RFC1918_RECURSION': 1, |
612 | - 'magic_number': 123, |
613 | - }, |
614 | - 'kubernetes': {'readinessProbe': {'exec': {'command': ['/usr/local/bin/dns-check.sh']}}}, |
615 | - } |
616 | - ], |
617 | - } |
618 | - self.assertEqual(self.harness.charm.make_pod_spec(), expected) |
619 | + def test_on_config_changed(self): |
620 | + self.harness.update_config(CONFIG_VALID) |
621 | + self._expected = WaitingStatus('Waiting for pebble') |
622 | + self.assertEqual(self.harness.model.unit.status, self._expected) |
623 | |
624 | - def test_make_pod_spec_with_extra_config_and_secrets(self): |
625 | - """Confirm that we generate the expected pod spec from a more involved valid config that includes secrets.""" |
626 | - self.harness.update_config(CONFIG_VALID_WITH_CONTAINER_CONFIG_AND_SECRETS) |
627 | - expected = { |
628 | - 'version': 2, |
629 | - 'containers': [ |
630 | - { |
631 | - 'name': 'bind-k8s', |
632 | - 'imageDetails': {'imagePath': 'example.com/bind:v1'}, |
633 | - 'ports': [ |
634 | - {'containerPort': 53, 'name': 'domain-tcp', 'protocol': 'TCP'}, |
635 | - {'containerPort': 53, 'name': 'domain-udp', 'protocol': 'UDP'}, |
636 | - ], |
637 | - 'config': {'ENABLE_RFC1918_RECURSION': 1, 'magic_number': 123, 'secret_password': 'xyzzy'}, |
638 | - 'kubernetes': {'readinessProbe': {'exec': {'command': ['/usr/local/bin/dns-check.sh']}}}, |
639 | - } |
640 | - ], |
641 | - } |
642 | - self.assertEqual(self.harness.charm.make_pod_spec(), expected) |
643 | + def test_missing_charm_settings(self): |
644 | + """Confirm that we're missing no settings.""" |
645 | + self.harness.update_config(CONFIG_VALID) |
646 | + self._expected = set() |
647 | + self.assertEqual(self.harness.charm._missing_charm_settings(), self._expected) |
648 | |
649 | - def test_make_pod_spec_with_custom_config_repo(self): |
650 | - """Confirm that we generate the expected pod spec from a config that includes a custom repo.""" |
651 | - self.harness.update_config(CONFIG_VALID_WITH_CUSTOM_CONFIG_REPO_AND_PROXY) |
652 | - expected = { |
653 | - 'version': 2, |
654 | - 'containers': [ |
655 | - { |
656 | - 'name': 'bind-k8s', |
657 | - 'imageDetails': {'imagePath': 'example.com/bind:v1'}, |
658 | - 'ports': [ |
659 | - {'containerPort': 53, 'name': 'domain-tcp', 'protocol': 'TCP'}, |
660 | - {'containerPort': 53, 'name': 'domain-udp', 'protocol': 'UDP'}, |
661 | - ], |
662 | - 'config': { |
663 | - 'CUSTOM_CONFIG_REPO': 'https://git.example.com/example-bind-config.git', |
664 | - 'ENABLE_RFC1918_RECURSION': 1, |
665 | - 'http_proxy': 'http://webproxy.example.com:3128/', |
666 | - 'https_proxy': 'http://webproxy.example.com:3128/', |
667 | - }, |
668 | - 'kubernetes': {'readinessProbe': {'exec': {'command': ['/usr/local/bin/dns-check.sh']}}}, |
669 | - } |
670 | - ], |
671 | - } |
672 | - self.assertEqual(self.harness.charm.make_pod_spec(), expected) |
673 | + def test_get_pebble_config_description(self): |
674 | + self.harness.update_config(CONFIG_VALID) |
675 | + self._expected = "Bind layer" |
676 | + self.assertEqual(_pebble_description(self.harness.charm._get_pebble_config()), self._expected) |
677 | |
678 | - def test_configure_pod_as_leader(self): |
679 | - """Confirm that our status is set correctly when we're the leader.""" |
680 | - self.harness.enable_hooks() |
681 | - self.harness.set_leader(True) |
682 | + def test_get_pebble_config_summary(self): |
683 | self.harness.update_config(CONFIG_VALID) |
684 | - expected = ActiveStatus('Pod configured') |
685 | - self.assertEqual(self.harness.model.unit.status, expected) |
686 | + self._expected = "Bind layer" |
687 | + self.assertEqual(_pebble_summary(self.harness.charm._get_pebble_config()), self._expected) |
688 | |
689 | - def test_configure_pod_as_non_leader(self): |
690 | - """Confirm that our status is set correctly when we're not the leader.""" |
691 | - self.harness.enable_hooks() |
692 | - self.harness.set_leader(False) |
693 | + def test_get_pebble_config_services_bind(self): |
694 | self.harness.update_config(CONFIG_VALID) |
695 | - expected = ActiveStatus() |
696 | - self.assertEqual(self.harness.model.unit.status, expected) |
697 | + self._expected = { |
698 | + "override": "replace", |
699 | + "summary": "Bind service", |
700 | + "command": 'bash -c "/usr/local/bin/docker-wrapper.sh &> /proc/1/fd/1"', |
701 | + "startup": "enabled", |
702 | + "environment": {'ENABLE_RFC1918_RECURSION': 1}, |
703 | + } |
704 | + self.assertEqual(_pebble_services_bind(self.harness.charm._get_pebble_config()), self._expected) |
705 | + |
706 | + def test_generate_env_config_custom_repo(self): |
707 | + self.harness.update_config({"custom_config_repo": "https://git.example.com/example-bind-config.git"}) |
708 | + self._expected['CUSTOM_CONFIG_REPO'] = "https://git.example.com/example-bind-config.git" |
709 | + self.assertEqual(_pebble_services_bind_env(self.harness.charm._get_pebble_config()), self._expected) |
710 | + |
711 | + def test_generage_env_config_custom_repo_and_proxy(self): |
712 | + self.harness.update_config( |
713 | + { |
714 | + "custom_config_repo": "https://git.example.com/example-bind-config.git", |
715 | + "https_proxy": "http://webproxy.example.com:3128/", |
716 | + } |
717 | + ) |
718 | + self._expected['CUSTOM_CONFIG_REPO'] = "https://git.example.com/example-bind-config.git" |
719 | + self._expected['http_proxy'] = "http://webproxy.example.com:3128/" |
720 | + self._expected['https_proxy'] = "http://webproxy.example.com:3128/" |
721 | + self.assertEqual(_pebble_services_bind_env(self.harness.charm._get_pebble_config()), self._expected) |
722 | + |
723 | + def test_generate_env_config_just_proxy(self): |
724 | + self.harness.update_config({}) |
725 | + self._expected['ENABLE_RFC1918_RECURSION'] = 1 |
726 | + self.assertEqual(_pebble_services_bind_env(self.harness.charm._get_pebble_config()), self._expected) |
727 | + |
728 | + def test_on_bind_pebble_ready(self): |
729 | + """Test the _on_bind_pebble_ready function.""" |
730 | + |
731 | + # No problem |
732 | + mock_event = MagicMock() |
733 | + expected_ret = None |
734 | + |
735 | + r = self.harness.charm._on_bind_pebble_ready(mock_event) |
736 | + self.assertEqual(r, expected_ret) |
737 | diff --git a/tox.ini b/tox.ini |
738 | index 0de5149..fb74730 100644 |
739 | --- a/tox.ini |
740 | +++ b/tox.ini |
741 | @@ -10,7 +10,7 @@ setenv = |
742 | [testenv:unit] |
743 | commands = |
744 | pytest --ignore mod --ignore {toxinidir}/tests/functional \ |
745 | - {posargs:-v --cov=src --cov-report=term-missing --cov-branch} |
746 | + {posargs:-v --cov=src --cov-report=term-missing --cov-branch --cov-report=html} |
747 | deps = -r{toxinidir}/tests/unit/requirements.txt |
748 | -r{toxinidir}/requirements.txt |
749 | setenv = |