Merge ~bind-charmers/charm-k8s-bind/+git/charmcraft-review:review into ~bind-charmers/charm-k8s-bind/+git/charmcraft-review:master
- Git
- lp:~bind-charmers/charm-k8s-bind/+git/charmcraft-review
- review
- Merge into master
Status: | Merged |
---|---|
Merge reported by: | Tom Haddon |
Merged at revision: | 8616c50ee14acbcd2292ab0324aaed870e8e7a08 |
Proposed branch: | ~bind-charmers/charm-k8s-bind/+git/charmcraft-review:review |
Merge into: | ~bind-charmers/charm-k8s-bind/+git/charmcraft-review:master |
Diff against target: |
745 lines (+551/-86) 13 files modified
.jujuignore (+7/-3) Makefile (+46/-0) README.md (+44/-16) config.yaml (+44/-8) dev/null (+0/-36) dockerfile (+33/-0) image-scripts/dns-check.sh (+10/-0) image-scripts/docker-entrypoint.sh (+23/-0) pyproject.toml (+3/-0) src/charm.py (+124/-23) tests/unit/requirements.txt (+4/-0) tests/unit/test_charm.py (+167/-0) tox.ini (+46/-0) |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Facundo Batista (community) | Approve | ||
Barry Price | Needs Resubmitting | ||
Review via email: mp+389995@code.launchpad.net |
Commit message
Bind charm updates for charmcraft review
Description of the change
Bind charm updates for charmcraft review.
This is a temporary branch/MP created to make charm review easier. Created by running `charmcraft init` and then merging the existing main branch into that.
We'll take any changes from here and merge them back into the main branch.
- 0188896... by Barry Price
-
Address charmcraft review
Barry Price (barryprice) wrote : | # |
> Added some comments inline, thanks!
Thank you! All addressed, I think.
Facundo Batista (facundo) wrote : | # |
Still a couple of details to take care of.
PLEASE respond my comments with what you think of them (even a "ok" is enough, if you're ok with the suggestion), thanks.
Barry Price (barryprice) wrote : | # |
Thanks, and sorry - responses to the first round of inline comments added (already addressed).
I'll go through the second round and push some more fixes next.
Barry Price (barryprice) wrote : | # |
And addressed the second round inline, I'll push the fixed code shortly.
Thanks again!
- 8616c50... by Barry Price
-
Address review comments, and drop the io import since we no longer use it
Barry Price (barryprice) wrote : | # |
All addressed
Facundo Batista (facundo) wrote : | # |
Wonderful, thanks!
Tom Haddon (mthaddon) wrote : | # |
Marking this as merged, as we've taken the updates from this and applied them to the main branch.
Preview Diff
1 | diff --git a/.jujuignore b/.jujuignore |
2 | index 6ccd559..d968a07 100644 |
3 | --- a/.jujuignore |
4 | +++ b/.jujuignore |
5 | @@ -1,3 +1,7 @@ |
6 | -/venv |
7 | -*.py[cod] |
8 | -*.charm |
9 | +*~ |
10 | +.coverage |
11 | +__pycache__ |
12 | +/dockerfile |
13 | +/image-scripts/ |
14 | +/tests/ |
15 | +/Makefile |
16 | diff --git a/Makefile b/Makefile |
17 | new file mode 100644 |
18 | index 0000000..66fc8ed |
19 | --- /dev/null |
20 | +++ b/Makefile |
21 | @@ -0,0 +1,46 @@ |
22 | +DIST_RELEASE ?= focal |
23 | +DOCKER_DEPS = bind9 bind9-dnsutils git |
24 | + |
25 | +blacken: |
26 | + @echo "Normalising python layout with black." |
27 | + @tox -e black |
28 | + |
29 | + |
30 | +lint: blacken |
31 | + @echo "Running flake8" |
32 | + @tox -e lint |
33 | + |
34 | +# We actually use the build directory created by charmcraft, |
35 | +# but the .charm file makes a much more convenient sentinel. |
36 | +unittest: bind.charm |
37 | + @tox -e unit |
38 | + |
39 | +test: lint unittest |
40 | + |
41 | +clean: |
42 | + @echo "Cleaning files" |
43 | + @git clean -fXd |
44 | + |
45 | +bind.charm: src/*.py requirements.txt |
46 | + charmcraft build |
47 | + |
48 | +image-deps: |
49 | + @echo "Checking shellcheck is present." |
50 | + @command -v shellcheck >/dev/null || { echo "Please install shellcheck to continue ('sudo snap install shellcheck')" && false; } |
51 | + |
52 | +image-lint: image-deps |
53 | + @echo "Running shellcheck." |
54 | + @shellcheck files/docker-entrypoint.sh |
55 | + @shellcheck files/dns-check.sh |
56 | + |
57 | +image-build: image-lint |
58 | + @echo "Building the image." |
59 | + @docker build \ |
60 | + --no-cache=true \ |
61 | + --build-arg BUILD_DATE=$$(date -u +'%Y-%m-%dT%H:%M:%SZ') \ |
62 | + --build-arg PKGS_TO_INSTALL='$(DOCKER_DEPS)' \ |
63 | + --build-arg DIST_RELEASE=$(DIST_RELEASE) \ |
64 | + -t bind:$(DIST_RELEASE)-latest \ |
65 | + . |
66 | + |
67 | +.PHONY: blacken lint unittest test clean image-deps image-lint image-build |
68 | diff --git a/README.md b/README.md |
69 | index 25fdae7..a5ba4ae 100644 |
70 | --- a/README.md |
71 | +++ b/README.md |
72 | @@ -1,28 +1,56 @@ |
73 | -# charmcraft-review |
74 | +# Bind charm |
75 | |
76 | -## Description |
77 | +A Juju charm deploying Bind, configurable to use a git repository for its configuration files. |
78 | |
79 | -TODO: fill out the description |
80 | +## Overview |
81 | |
82 | -## Usage |
83 | +This is a k8s workload charm and can only be deployed to a Juju k8s cloud, |
84 | +attached to a controller using `juju add-k8s`. |
85 | |
86 | -TODO: explain how to use the charm |
87 | +This charm is not currently ready for production due to issues with providing |
88 | +an egress to route TCP and UDP traffic to the pods. See: |
89 | |
90 | -### Scale Out Usage |
91 | +https://bugs.launchpad.net/charm-k8s-bind/+bug/1889746 |
92 | |
93 | -... |
94 | +https://bugs.launchpad.net/juju/+bug/1889703 |
95 | |
96 | -## Developing |
97 | +## Details |
98 | |
99 | -Create and activate a virtualenv, |
100 | -and install the development requirements, |
101 | +See config option descriptions in config.yaml. |
102 | |
103 | - virtualenv -p python3 venv |
104 | - source venv/bin/activate |
105 | - pip install -r requirements-dev.txt |
106 | +## Getting Started |
107 | |
108 | -## Testing |
109 | +Notes for deploying a test setup locally using microk8s: |
110 | |
111 | -Just run `run_tests`: |
112 | + sudo snap install juju --classic |
113 | + sudo snap install juju-wait --classic |
114 | + sudo snap install microk8s --classic |
115 | + sudo snap alias microk8s.kubectl kubectl |
116 | + sudo snap install charmcraft |
117 | + git clone https://git.launchpad.net/charm-k8s-bind |
118 | + make bind.charm |
119 | |
120 | - ./run_tests |
121 | + microk8s.reset # Warning! Clean slate! |
122 | + microk8s.enable dns dashboard registry storage |
123 | + microk8s.status --wait-ready |
124 | + microk8s.config | juju add-k8s myk8s --client |
125 | + |
126 | + # Build your Bind image |
127 | + make build-image |
128 | + docker push localhost:32000/bind |
129 | + |
130 | + juju bootstrap myk8s |
131 | + juju add-model bind-test |
132 | + juju deploy ./bind.charm --config bind_image_path=localhost:32000/bind:latest bind |
133 | + juju wait |
134 | + juju status |
135 | + |
136 | +Assuming you're using the image as built locally from this repo, the charm will |
137 | +deploy bind with its stock Ubuntu package configuration, which will forward all |
138 | +queries to root name servers. |
139 | + |
140 | +DNSSEC is also enabled by default. |
141 | + |
142 | +Custom config can be deployed by setting the `custom_config_repo` option to |
143 | +point to a Git repository containing a valid set of configuration files with |
144 | +which to populate the /etc/bind/ directory within the pod(s). |
145 | diff --git a/config.yaml b/config.yaml |
146 | index 0073c17..408ae9d 100644 |
147 | --- a/config.yaml |
148 | +++ b/config.yaml |
149 | @@ -1,10 +1,46 @@ |
150 | -# Copyright 2020 Tom Haddon |
151 | -# See LICENSE file for licensing details. |
152 | -# |
153 | -# This is only an example, and you should edit to suit your needs. |
154 | -# If you don't need config, you can remove the file entirely. |
155 | options: |
156 | - thing: |
157 | - default: 🎁 |
158 | - description: A thing used by the charm. |
159 | + bind_image_path: |
160 | type: string |
161 | + description: | |
162 | + The location of the image to use, e.g. "registry.example.com/bind:v1". |
163 | + |
164 | + This setting is required. |
165 | + default: "" |
166 | + bind_image_username: |
167 | + type: string |
168 | + description: "Username to use for the configured image registry, if required" |
169 | + default: "" |
170 | + bind_image_password: |
171 | + type: string |
172 | + description: "Password to use for the configured image registry, if required" |
173 | + default: "" |
174 | + container_config: |
175 | + type: string |
176 | + description: > |
177 | + YAML formatted map of container config keys & values. These are |
178 | + generally accessed from inside the image as environment variables. |
179 | + Use to configure customized Wordpress images. This configuration |
180 | + gets logged; use container_secrets for secrets. |
181 | + default: "" |
182 | + container_secrets: |
183 | + type: string |
184 | + description: > |
185 | + YAML formatted map of secrets. Works just like container_config, |
186 | + except that values should not be logged. |
187 | + default: "" |
188 | + custom_config_repo: |
189 | + type: string |
190 | + description: | |
191 | + Repository from which to populate /etc/bind/. |
192 | + If unset, bind will be deployed with the package defaults. |
193 | + e.g. http://github.com/foo/my-custom-bind-config |
194 | + default: "" |
195 | + https_proxy: |
196 | + type: string |
197 | + description: | |
198 | + Proxy address to set in the environment, e.g. http://192.168.1.1:8080 |
199 | + Used to clone the configuration files from custom_config_repo, if set. |
200 | + If a username/password is required, they can be embedded in the proxy |
201 | + address e.g. http://username:password@192.168.1.1:8080 |
202 | + Traffic is expected to be HTTPS, but this will also work for HTTP. |
203 | + default: "" |
204 | diff --git a/dockerfile b/dockerfile |
205 | new file mode 100644 |
206 | index 0000000..2e580d4 |
207 | --- /dev/null |
208 | +++ b/dockerfile |
209 | @@ -0,0 +1,33 @@ |
210 | +ARG DIST_RELEASE |
211 | + |
212 | +FROM ubuntu:${DIST_RELEASE} |
213 | + |
214 | +LABEL maintainer="bind-charmers@lists.launchpad.net" |
215 | + |
216 | +ARG BUILD_DATE |
217 | +ARG PKGS_TO_INSTALL |
218 | + |
219 | +LABEL org.label-schema.build-date=${BUILD_DATE} |
220 | + |
221 | +ENV BIND_CONFDIR=/etc/bind |
222 | + |
223 | +# Avoid interactive prompts |
224 | +RUN echo 'debconf debconf/frontend select Noninteractive' | debconf-set-selections |
225 | + |
226 | +# Update all packages, remove cruft, install required packages |
227 | +RUN apt-get update && apt-get -y dist-upgrade \ |
228 | + && apt-get --purge autoremove -y \ |
229 | + && apt-get install -y ${PKGS_TO_INSTALL} |
230 | + |
231 | +# entrypoint script will configure Bind based on env variables |
232 | +# dns-check script will provide a readinessProbe |
233 | +COPY ./files/docker-entrypoint.sh /usr/local/bin/ |
234 | +COPY ./files/dns-check.sh /usr/local/bin/ |
235 | +RUN chmod 0755 /usr/local/bin/docker-entrypoint.sh |
236 | +RUN chmod 0755 /usr/local/bin/dns-check.sh |
237 | + |
238 | +EXPOSE 53/udp |
239 | +EXPOSE 53/tcp |
240 | + |
241 | +ENTRYPOINT ["/usr/local/bin/docker-entrypoint.sh"] |
242 | +CMD /usr/sbin/named -g -u bind -c /etc/bind/named.conf |
243 | diff --git a/image-scripts/dns-check.sh b/image-scripts/dns-check.sh |
244 | new file mode 100644 |
245 | index 0000000..ca4a5a0 |
246 | --- /dev/null |
247 | +++ b/image-scripts/dns-check.sh |
248 | @@ -0,0 +1,10 @@ |
249 | +#!/bin/bash |
250 | +set -eu |
251 | + |
252 | +TEST_DOMAIN="ddebs.ubuntu.com." |
253 | +NSLOOKUP_PATH="/usr/bin/nslookup" |
254 | +OUR_ADDRESS="127.0.0.1" |
255 | + |
256 | +command -v "${NSLOOKUP_PATH}" >/dev/null || { echo "Cannot find the 'nslookup' command" && exit 1; } |
257 | + |
258 | +exec "${NSLOOKUP_PATH}" "${TEST_DOMAIN}" "${OUR_ADDRESS}" >/dev/null |
259 | diff --git a/image-scripts/docker-entrypoint.sh b/image-scripts/docker-entrypoint.sh |
260 | new file mode 100644 |
261 | index 0000000..2ed0857 |
262 | --- /dev/null |
263 | +++ b/image-scripts/docker-entrypoint.sh |
264 | @@ -0,0 +1,23 @@ |
265 | +#!/bin/bash |
266 | +set -eu |
267 | + |
268 | +if [ -z "${BIND_CONFDIR-}" ]; then |
269 | + # If BIND_CONFDIR wasn't set, use the package default |
270 | + BIND_CONFDIR="/etc/bind"; |
271 | +fi |
272 | + |
273 | +if [ -z "${CUSTOM_CONFIG_REPO-}" ]; then |
274 | + echo "No custom repo set, will fall back to package default config"; |
275 | +else |
276 | + echo "Pulling config from $CUSTOM_CONFIG_REPO"; |
277 | + if [ -d "${BIND_CONFDIR}" ]; then |
278 | + mv "${BIND_CONFDIR}" "${BIND_CONFDIR}_$(date +"%Y-%m-%d_%H-%M-%S")"; |
279 | + fi |
280 | + git clone "$CUSTOM_CONFIG_REPO" "$BIND_CONFDIR"; |
281 | +fi |
282 | + |
283 | +if [ -d "${BIND_CONFDIR}" ]; then |
284 | + exec "$@" |
285 | +else |
286 | + echo "Something went wrong, ${BIND_CONFDIR} does not exist, not starting"; |
287 | +fi |
288 | diff --git a/pyproject.toml b/pyproject.toml |
289 | new file mode 100644 |
290 | index 0000000..d2f23b9 |
291 | --- /dev/null |
292 | +++ b/pyproject.toml |
293 | @@ -0,0 +1,3 @@ |
294 | +[tool.black] |
295 | +skip-string-normalization = true |
296 | +line-length = 120 |
297 | diff --git a/src/charm.py b/src/charm.py |
298 | index 640fea5..0faf651 100755 |
299 | --- a/src/charm.py |
300 | +++ b/src/charm.py |
301 | @@ -1,38 +1,139 @@ |
302 | #!/usr/bin/env python3 |
303 | -# Copyright 2020 Tom Haddon |
304 | -# See LICENSE file for licensing details. |
305 | |
306 | -import logging |
307 | +# Copyright 2020 Canonical Ltd. |
308 | +# Licensed under the GPLv3, see LICENCE file for details. |
309 | |
310 | +import logging |
311 | from ops.charm import CharmBase |
312 | from ops.main import main |
313 | -from ops.framework import StoredState |
314 | +from ops.model import ActiveStatus, MaintenanceStatus |
315 | +from pprint import pformat |
316 | +from yaml import safe_load |
317 | |
318 | -logger = logging.getLogger(__name__) |
319 | +logger = logging.getLogger() |
320 | |
321 | +REQUIRED_SETTINGS = ['bind_image_path'] |
322 | |
323 | -class CharmcraftReviewCharm(CharmBase): |
324 | - _stored = StoredState() |
325 | |
326 | +class BindK8sCharm(CharmBase): |
327 | def __init__(self, *args): |
328 | + """Initialise our class, we only care about 'start' and 'config-changed' hooks.""" |
329 | super().__init__(*args) |
330 | - self.framework.observe(self.on.config_changed, self._on_config_changed) |
331 | - self.framework.observe(self.on.fortune_action, self._on_fortune_action) |
332 | - self._stored.set_default(things=[]) |
333 | - |
334 | - def _on_config_changed(self, _): |
335 | - current = self.model.config["thing"] |
336 | - if current not in self._stored.things: |
337 | - logger.debug("found a new thing: %r", current) |
338 | - self._stored.things.append(current) |
339 | - |
340 | - def _on_fortune_action(self, event): |
341 | - fail = event.params["fail"] |
342 | - if fail: |
343 | - event.fail(fail) |
344 | + self.framework.observe(self.on.start, self.on_config_changed) |
345 | + self.framework.observe(self.on.config_changed, self.on_config_changed) |
346 | + |
347 | + def _check_for_config_problems(self): |
348 | + """Return a string describing any configuration problems (or an empty string if none).""" |
349 | + problems = '' |
350 | + |
351 | + missing = self._missing_charm_settings() |
352 | + if missing: |
353 | + problems = 'required setting(s) empty: {}'.format(', '.join(sorted(missing))) |
354 | + |
355 | + return problems |
356 | + |
357 | + def _missing_charm_settings(self): |
358 | + """Return a list of missing configuration settings (or an empty list if none).""" |
359 | + config = self.model.config |
360 | + |
361 | + missing = {setting for setting in REQUIRED_SETTINGS if not config[setting]} |
362 | + |
363 | + if config['bind_image_username'] and not config['bind_image_password']: |
364 | + missing.add('bind_image_password') |
365 | + |
366 | + return missing |
367 | + |
368 | + def on_config_changed(self, event): |
369 | + """Check that we're leader, and if so, set up the pod.""" |
370 | + if self.model.unit.is_leader(): |
371 | + # Only the leader can set_spec(). |
372 | + resources = self.make_pod_resources() |
373 | + spec = self.make_pod_spec() |
374 | + spec.update(resources) |
375 | + |
376 | + msg = "Configuring pod" |
377 | + logger.info(msg) |
378 | + self.model.unit.status = MaintenanceStatus(msg) |
379 | + self.model.pod.set_spec(spec) |
380 | + |
381 | + msg = "Pod configured" |
382 | + logger.info(msg) |
383 | + self.model.unit.status = ActiveStatus(msg) |
384 | + else: |
385 | + logger.info("Spec changes ignored by non-leader") |
386 | + self.model.unit.status = ActiveStatus() |
387 | + |
388 | + def make_pod_resources(self): |
389 | + """Compile and return our pod resources (e.g. ingresses).""" |
390 | + # LP#1889746: We need to define a manual ingress here to work around LP#1889703. |
391 | + resources = {} # TODO |
392 | + logger.info("This is the Kubernetes Pod resources <<EOM\n{}\nEOM".format(pformat(resources))) |
393 | + return resources |
394 | + |
395 | + def generate_pod_config(self, secured=True): |
396 | + """Kubernetes pod config generator. |
397 | + |
398 | + generate_pod_config generates Kubernetes deployment config. |
399 | + If the secured keyword is set then it will return a sanitised copy |
400 | + without exposing secrets. |
401 | + """ |
402 | + config = self.model.config |
403 | + pod_config = {} |
404 | + if config["container_config"].strip(): |
405 | + pod_config = safe_load(config["container_config"]) |
406 | + |
407 | + if config["custom_config_repo"].strip(): |
408 | + pod_config["CUSTOM_CONFIG_REPO"] = config["custom_config_repo"] |
409 | + |
410 | + if config["https_proxy"].strip(): |
411 | + pod_config["http_proxy"] = config["https_proxy"] |
412 | + pod_config["https_proxy"] = config["https_proxy"] |
413 | + |
414 | + if secured: |
415 | + return pod_config |
416 | + |
417 | + if config["container_secrets"].strip(): |
418 | + container_secrets = safe_load(config["container_secrets"]) |
419 | else: |
420 | - event.set_results({"fortune": "A bug in the code is worth two in the documentation."}) |
421 | + container_secrets = {} |
422 | + |
423 | + pod_config.update(container_secrets) |
424 | + return pod_config |
425 | + |
426 | + def make_pod_spec(self): |
427 | + """Set up and return our full pod spec here.""" |
428 | + config = self.model.config |
429 | + full_pod_config = self.generate_pod_config(secured=False) |
430 | + secure_pod_config = self.generate_pod_config(secured=True) |
431 | + |
432 | + ports = [ |
433 | + {"name": "domain-tcp", "containerPort": 53, "protocol": "TCP"}, |
434 | + {"name": "domain-udp", "containerPort": 53, "protocol": "UDP"}, |
435 | + ] |
436 | + |
437 | + spec = { |
438 | + "version": 2, |
439 | + "containers": [ |
440 | + { |
441 | + "name": self.app.name, |
442 | + "imageDetails": {"imagePath": config["bind_image_path"]}, |
443 | + "ports": ports, |
444 | + "config": secure_pod_config, |
445 | + "kubernetes": {"readinessProbe": {"exec": {"command": ["/usr/local/bin/dns-check.sh"]}}}, |
446 | + } |
447 | + ], |
448 | + } |
449 | + |
450 | + logger.info("This is the Kubernetes Pod spec config (sans secrets) <<EOM\n{}\nEOM".format(pformat(spec))) |
451 | + |
452 | + if config.get("bind_image_username") and config.get("bind_image_password"): |
453 | + spec.get("containers")[0].get("imageDetails")["username"] = config["bind_image_username"] |
454 | + spec.get("containers")[0].get("imageDetails")["password"] = config["bind_image_password"] |
455 | + |
456 | + secure_pod_config.update(full_pod_config) |
457 | + |
458 | + return spec |
459 | |
460 | |
461 | if __name__ == "__main__": |
462 | - main(CharmcraftReviewCharm) |
463 | + main(BindK8sCharm) |
464 | diff --git a/tests/__init__.py b/tests/__init__.py |
465 | deleted file mode 100644 |
466 | index e69de29..0000000 |
467 | --- a/tests/__init__.py |
468 | +++ /dev/null |
469 | diff --git a/tests/test_charm.py b/tests/test_charm.py |
470 | deleted file mode 100644 |
471 | index 629f0ea..0000000 |
472 | --- a/tests/test_charm.py |
473 | +++ /dev/null |
474 | @@ -1,36 +0,0 @@ |
475 | -# Copyright 2020 Tom Haddon |
476 | -# See LICENSE file for licensing details. |
477 | - |
478 | -import unittest |
479 | -from unittest.mock import Mock |
480 | - |
481 | -from ops.testing import Harness |
482 | -from charm import CharmcraftReviewCharm |
483 | - |
484 | - |
485 | -class TestCharm(unittest.TestCase): |
486 | - def test_config_changed(self): |
487 | - harness = Harness(CharmcraftReviewCharm) |
488 | - # from 0.8 you should also do: |
489 | - # self.addCleanup(harness.cleanup) |
490 | - harness.begin() |
491 | - self.assertEqual(list(harness.charm._stored.things), []) |
492 | - harness.update_config({"thing": "foo"}) |
493 | - self.assertEqual(list(harness.charm._stored.things), ["foo"]) |
494 | - |
495 | - def test_action(self): |
496 | - harness = Harness(CharmcraftReviewCharm) |
497 | - harness.begin() |
498 | - # the harness doesn't (yet!) help much with actions themselves |
499 | - action_event = Mock(params={"fail": ""}) |
500 | - harness.charm._on_fortune_action(action_event) |
501 | - |
502 | - self.assertTrue(action_event.set_results.called) |
503 | - |
504 | - def test_action_fail(self): |
505 | - harness = Harness(CharmcraftReviewCharm) |
506 | - harness.begin() |
507 | - action_event = Mock(params={"fail": "fail this"}) |
508 | - harness.charm._on_fortune_action(action_event) |
509 | - |
510 | - self.assertEqual(action_event.fail.call_args, [("fail this",)]) |
511 | diff --git a/tests/unit/requirements.txt b/tests/unit/requirements.txt |
512 | new file mode 100644 |
513 | index 0000000..65431fc |
514 | --- /dev/null |
515 | +++ b/tests/unit/requirements.txt |
516 | @@ -0,0 +1,4 @@ |
517 | +mock |
518 | +pytest |
519 | +pytest-cov |
520 | +pyyaml |
521 | diff --git a/tests/unit/test_charm.py b/tests/unit/test_charm.py |
522 | new file mode 100644 |
523 | index 0000000..dc53ac0 |
524 | --- /dev/null |
525 | +++ b/tests/unit/test_charm.py |
526 | @@ -0,0 +1,167 @@ |
527 | +# Copyright 2020 Canonical Ltd. |
528 | +# Licensed under the GPLv3, see LICENCE file for details. |
529 | + |
530 | +import unittest |
531 | + |
532 | +from charm import BindK8sCharm |
533 | + |
534 | +from ops import testing |
535 | +from ops.model import ActiveStatus |
536 | + |
537 | +CONFIG_EMPTY = { |
538 | + 'bind_image_path': '', |
539 | + 'bind_image_username': '', |
540 | + 'bind_image_password': '', |
541 | + 'container_config': '', |
542 | + 'container_secrets': '', |
543 | + 'custom_config_repo': '', |
544 | + 'https_proxy': '', |
545 | +} |
546 | + |
547 | +CONFIG_IMAGE_PASSWORD_MISSING = { |
548 | + 'bind_image_path': 'example.com/bind:v1', |
549 | + 'bind_image_username': 'username', |
550 | + 'bind_image_password': '', |
551 | + 'container_config': '', |
552 | + 'container_secrets': '', |
553 | + 'custom_config_repo': '', |
554 | + 'https_proxy': '', |
555 | +} |
556 | + |
557 | +CONFIG_VALID = { |
558 | + 'bind_image_path': 'example.com/bind:v1', |
559 | + 'bind_image_username': '', |
560 | + 'bind_image_password': '', |
561 | + 'container_config': '', |
562 | + 'container_secrets': '', |
563 | + 'custom_config_repo': '', |
564 | + 'https_proxy': '', |
565 | +} |
566 | + |
567 | +CONFIG_VALID_WITH_CONTAINER_CONFIG = { |
568 | + 'bind_image_path': 'example.com/bind:v1', |
569 | + 'bind_image_username': '', |
570 | + 'bind_image_password': '', |
571 | + 'container_config': '"magic_number": 123', |
572 | + 'container_secrets': '', |
573 | + 'custom_config_repo': '', |
574 | + 'https_proxy': '', |
575 | +} |
576 | + |
577 | +CONFIG_VALID_WITH_CONTAINER_CONFIG_AND_SECRETS = { |
578 | + 'bind_image_path': 'example.com/bind:v1', |
579 | + 'bind_image_username': '', |
580 | + 'bind_image_password': '', |
581 | + 'container_config': '"magic_number": 123', |
582 | + 'container_secrets': '"secret_password": "xyzzy"', |
583 | + 'custom_config_repo': '', |
584 | + 'https_proxy': '', |
585 | +} |
586 | + |
587 | + |
588 | +class TestBindK8s(unittest.TestCase): |
589 | + maxDiff = None |
590 | + |
591 | + def setUp(self): |
592 | + self.harness = testing.Harness(BindK8sCharm) |
593 | + self.harness.begin() |
594 | + self.harness.disable_hooks() |
595 | + |
596 | + def test_check_for_config_problems_empty_image_path(self): |
597 | + """Confirm that we generate an error if we're not told what image to use.""" |
598 | + self.harness.update_config(CONFIG_EMPTY) |
599 | + expected = 'required setting(s) empty: bind_image_path' |
600 | + self.assertEqual(self.harness.charm._check_for_config_problems(), expected) |
601 | + |
602 | + def test_check_for_config_problems_empty_image_password(self): |
603 | + """Confirm that we generate an error if we're not given valid registry creds.""" |
604 | + self.harness.update_config(CONFIG_IMAGE_PASSWORD_MISSING) |
605 | + expected = 'required setting(s) empty: bind_image_password' |
606 | + self.assertEqual(self.harness.charm._check_for_config_problems(), expected) |
607 | + |
608 | + def test_check_for_config_problems_none(self): |
609 | + """Confirm that we accept valid config.""" |
610 | + self.harness.update_config(CONFIG_VALID) |
611 | + expected = '' |
612 | + self.assertEqual(self.harness.charm._check_for_config_problems(), expected) |
613 | + |
614 | + def test_make_pod_resources(self): |
615 | + """Confirm that we generate the expected pod resources (see LP#1889746).""" |
616 | + expected = {} |
617 | + self.assertEqual(self.harness.charm.make_pod_resources(), expected) |
618 | + |
619 | + def test_make_pod_spec_basic(self): |
620 | + """Confirm that we generate the expected pod spec from valid config.""" |
621 | + self.harness.update_config(CONFIG_VALID) |
622 | + expected = { |
623 | + 'version': 2, |
624 | + 'containers': [ |
625 | + { |
626 | + 'name': 'bind', |
627 | + 'imageDetails': {'imagePath': 'example.com/bind:v1'}, |
628 | + 'ports': [ |
629 | + {'containerPort': 53, 'name': 'domain-tcp', 'protocol': 'TCP'}, |
630 | + {'containerPort': 53, 'name': 'domain-udp', 'protocol': 'UDP'}, |
631 | + ], |
632 | + 'config': {}, |
633 | + 'kubernetes': {'readinessProbe': {'exec': {'command': ['/usr/local/bin/dns-check.sh']}}}, |
634 | + } |
635 | + ], |
636 | + } |
637 | + self.assertEqual(self.harness.charm.make_pod_spec(), expected) |
638 | + |
639 | + def test_make_pod_spec_with_extra_config(self): |
640 | + """Confirm that we generate the expected pod spec from a more involved valid config.""" |
641 | + self.harness.update_config(CONFIG_VALID_WITH_CONTAINER_CONFIG) |
642 | + expected = { |
643 | + 'version': 2, |
644 | + 'containers': [ |
645 | + { |
646 | + 'name': 'bind', |
647 | + 'imageDetails': {'imagePath': 'example.com/bind:v1'}, |
648 | + 'ports': [ |
649 | + {'containerPort': 53, 'name': 'domain-tcp', 'protocol': 'TCP'}, |
650 | + {'containerPort': 53, 'name': 'domain-udp', 'protocol': 'UDP'}, |
651 | + ], |
652 | + 'config': {'magic_number': 123}, |
653 | + 'kubernetes': {'readinessProbe': {'exec': {'command': ['/usr/local/bin/dns-check.sh']}}}, |
654 | + } |
655 | + ], |
656 | + } |
657 | + self.assertEqual(self.harness.charm.make_pod_spec(), expected) |
658 | + |
659 | + def test_make_pod_spec_with_extra_config_and_secrets(self): |
660 | + """Confirm that we generate the expected pod spec from a more involved valid config that includes secrets.""" |
661 | + self.harness.update_config(CONFIG_VALID_WITH_CONTAINER_CONFIG_AND_SECRETS) |
662 | + expected = { |
663 | + 'version': 2, |
664 | + 'containers': [ |
665 | + { |
666 | + 'name': 'bind', |
667 | + 'imageDetails': {'imagePath': 'example.com/bind:v1'}, |
668 | + 'ports': [ |
669 | + {'containerPort': 53, 'name': 'domain-tcp', 'protocol': 'TCP'}, |
670 | + {'containerPort': 53, 'name': 'domain-udp', 'protocol': 'UDP'}, |
671 | + ], |
672 | + 'config': {'magic_number': 123, 'secret_password': 'xyzzy'}, |
673 | + 'kubernetes': {'readinessProbe': {'exec': {'command': ['/usr/local/bin/dns-check.sh']}}}, |
674 | + } |
675 | + ], |
676 | + } |
677 | + self.assertEqual(self.harness.charm.make_pod_spec(), expected) |
678 | + |
679 | + def test_configure_pod_as_leader(self): |
680 | + """Confirm that our status is set correctly when we're the leader.""" |
681 | + self.harness.enable_hooks() |
682 | + self.harness.set_leader(True) |
683 | + self.harness.update_config(CONFIG_VALID) |
684 | + expected = ActiveStatus('Pod configured') |
685 | + self.assertEqual(self.harness.model.unit.status, expected) |
686 | + |
687 | + def test_configure_pod_as_non_leader(self): |
688 | + """Confirm that our status is set correctly when we're not the leader.""" |
689 | + self.harness.enable_hooks() |
690 | + self.harness.set_leader(False) |
691 | + self.harness.update_config(CONFIG_VALID) |
692 | + expected = ActiveStatus() |
693 | + self.assertEqual(self.harness.model.unit.status, expected) |
694 | diff --git a/tox.ini b/tox.ini |
695 | new file mode 100644 |
696 | index 0000000..0de5149 |
697 | --- /dev/null |
698 | +++ b/tox.ini |
699 | @@ -0,0 +1,46 @@ |
700 | +[tox] |
701 | +skipsdist=True |
702 | +envlist = unit, functional |
703 | + |
704 | +[testenv] |
705 | +basepython = python3 |
706 | +setenv = |
707 | + PYTHONPATH = {toxinidir}/build/lib:{toxinidir}/build/venv |
708 | + |
709 | +[testenv:unit] |
710 | +commands = |
711 | + pytest --ignore mod --ignore {toxinidir}/tests/functional \ |
712 | + {posargs:-v --cov=src --cov-report=term-missing --cov-branch} |
713 | +deps = -r{toxinidir}/tests/unit/requirements.txt |
714 | + -r{toxinidir}/requirements.txt |
715 | +setenv = |
716 | + PYTHONPATH={toxinidir}/src:{toxinidir}/build/lib:{toxinidir}/build/venv |
717 | + TZ=UTC |
718 | + |
719 | +[testenv:functional] |
720 | +passenv = |
721 | + HOME |
722 | + JUJU_REPOSITORY |
723 | + PATH |
724 | +commands = |
725 | + pytest -v --ignore mod --ignore {toxinidir}/tests/unit {posargs} |
726 | +deps = -r{toxinidir}/tests/functional/requirements.txt |
727 | + -r{toxinidir}/requirements.txt |
728 | + |
729 | +[testenv:black] |
730 | +commands = black --skip-string-normalization --line-length=120 src/ tests/ |
731 | +deps = black |
732 | + |
733 | +[testenv:lint] |
734 | +commands = flake8 src/ tests/ |
735 | +# Pin flake8 to 3.7.9 to match focal |
736 | +deps = |
737 | + flake8==3.7.9 |
738 | + |
739 | +[flake8] |
740 | +exclude = |
741 | + .git, |
742 | + __pycache__, |
743 | + .tox, |
744 | +max-line-length = 120 |
745 | +max-complexity = 10 |
Added some comments inline, thanks!