Merge ~bind-charmers/charm-k8s-bind/+git/charmcraft-review:review into ~bind-charmers/charm-k8s-bind/+git/charmcraft-review:master

Proposed by Tom Haddon
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)
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.

To post a comment you must log in.
Revision history for this message
Facundo Batista (facundo) wrote :

Added some comments inline, thanks!

review: Needs Fixing
0188896... by Barry Price

Address charmcraft review

Revision history for this message
Barry Price (barryprice) wrote :

> Added some comments inline, thanks!

Thank you! All addressed, I think.

review: Needs Resubmitting
Revision history for this message
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.

review: Needs Fixing
Revision history for this message
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.

Revision history for this message
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

Revision history for this message
Barry Price (barryprice) wrote :

All addressed

review: Needs Resubmitting
Revision history for this message
Facundo Batista (facundo) wrote :

Wonderful, thanks!

review: Approve
Revision history for this message
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

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/.jujuignore b/.jujuignore
2index 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
16diff --git a/Makefile b/Makefile
17new file mode 100644
18index 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
68diff --git a/README.md b/README.md
69index 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).
145diff --git a/config.yaml b/config.yaml
146index 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: ""
204diff --git a/dockerfile b/dockerfile
205new file mode 100644
206index 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
243diff --git a/image-scripts/dns-check.sh b/image-scripts/dns-check.sh
244new file mode 100644
245index 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
259diff --git a/image-scripts/docker-entrypoint.sh b/image-scripts/docker-entrypoint.sh
260new file mode 100644
261index 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
288diff --git a/pyproject.toml b/pyproject.toml
289new file mode 100644
290index 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
297diff --git a/src/charm.py b/src/charm.py
298index 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)
464diff --git a/tests/__init__.py b/tests/__init__.py
465deleted file mode 100644
466index e69de29..0000000
467--- a/tests/__init__.py
468+++ /dev/null
469diff --git a/tests/test_charm.py b/tests/test_charm.py
470deleted file mode 100644
471index 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",)])
511diff --git a/tests/unit/requirements.txt b/tests/unit/requirements.txt
512new file mode 100644
513index 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
521diff --git a/tests/unit/test_charm.py b/tests/unit/test_charm.py
522new file mode 100644
523index 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)
694diff --git a/tox.ini b/tox.ini
695new file mode 100644
696index 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

Subscribers

People subscribed via source and target branches

to all changes: