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
diff --git a/.jujuignore b/.jujuignore
index 6ccd559..d968a07 100644
--- a/.jujuignore
+++ b/.jujuignore
@@ -1,3 +1,7 @@
1/venv1*~
2*.py[cod]2.coverage
3*.charm3__pycache__
4/dockerfile
5/image-scripts/
6/tests/
7/Makefile
diff --git a/Makefile b/Makefile
4new file mode 1006448new file mode 100644
index 0000000..66fc8ed
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,46 @@
1DIST_RELEASE ?= focal
2DOCKER_DEPS = bind9 bind9-dnsutils git
3
4blacken:
5 @echo "Normalising python layout with black."
6 @tox -e black
7
8
9lint: blacken
10 @echo "Running flake8"
11 @tox -e lint
12
13# We actually use the build directory created by charmcraft,
14# but the .charm file makes a much more convenient sentinel.
15unittest: bind.charm
16 @tox -e unit
17
18test: lint unittest
19
20clean:
21 @echo "Cleaning files"
22 @git clean -fXd
23
24bind.charm: src/*.py requirements.txt
25 charmcraft build
26
27image-deps:
28 @echo "Checking shellcheck is present."
29 @command -v shellcheck >/dev/null || { echo "Please install shellcheck to continue ('sudo snap install shellcheck')" && false; }
30
31image-lint: image-deps
32 @echo "Running shellcheck."
33 @shellcheck files/docker-entrypoint.sh
34 @shellcheck files/dns-check.sh
35
36image-build: image-lint
37 @echo "Building the image."
38 @docker build \
39 --no-cache=true \
40 --build-arg BUILD_DATE=$$(date -u +'%Y-%m-%dT%H:%M:%SZ') \
41 --build-arg PKGS_TO_INSTALL='$(DOCKER_DEPS)' \
42 --build-arg DIST_RELEASE=$(DIST_RELEASE) \
43 -t bind:$(DIST_RELEASE)-latest \
44 .
45
46.PHONY: blacken lint unittest test clean image-deps image-lint image-build
diff --git a/README.md b/README.md
index 25fdae7..a5ba4ae 100644
--- a/README.md
+++ b/README.md
@@ -1,28 +1,56 @@
1# charmcraft-review1# Bind charm
22
3## Description3A Juju charm deploying Bind, configurable to use a git repository for its configuration files.
44
5TODO: fill out the description5## Overview
66
7## Usage7This is a k8s workload charm and can only be deployed to a Juju k8s cloud,
8attached to a controller using `juju add-k8s`.
89
9TODO: explain how to use the charm10This charm is not currently ready for production due to issues with providing
11an egress to route TCP and UDP traffic to the pods. See:
1012
11### Scale Out Usage13https://bugs.launchpad.net/charm-k8s-bind/+bug/1889746
1214
13...15https://bugs.launchpad.net/juju/+bug/1889703
1416
15## Developing17## Details
1618
17Create and activate a virtualenv,19See config option descriptions in config.yaml.
18and install the development requirements,
1920
20 virtualenv -p python3 venv21## Getting Started
21 source venv/bin/activate
22 pip install -r requirements-dev.txt
2322
24## Testing23Notes for deploying a test setup locally using microk8s:
2524
26Just run `run_tests`:25 sudo snap install juju --classic
26 sudo snap install juju-wait --classic
27 sudo snap install microk8s --classic
28 sudo snap alias microk8s.kubectl kubectl
29 sudo snap install charmcraft
30 git clone https://git.launchpad.net/charm-k8s-bind
31 make bind.charm
2732
28 ./run_tests33 microk8s.reset # Warning! Clean slate!
34 microk8s.enable dns dashboard registry storage
35 microk8s.status --wait-ready
36 microk8s.config | juju add-k8s myk8s --client
37
38 # Build your Bind image
39 make build-image
40 docker push localhost:32000/bind
41
42 juju bootstrap myk8s
43 juju add-model bind-test
44 juju deploy ./bind.charm --config bind_image_path=localhost:32000/bind:latest bind
45 juju wait
46 juju status
47
48Assuming you're using the image as built locally from this repo, the charm will
49deploy bind with its stock Ubuntu package configuration, which will forward all
50queries to root name servers.
51
52DNSSEC is also enabled by default.
53
54Custom config can be deployed by setting the `custom_config_repo` option to
55point to a Git repository containing a valid set of configuration files with
56which to populate the /etc/bind/ directory within the pod(s).
diff --git a/config.yaml b/config.yaml
index 0073c17..408ae9d 100644
--- a/config.yaml
+++ b/config.yaml
@@ -1,10 +1,46 @@
1# Copyright 2020 Tom Haddon
2# See LICENSE file for licensing details.
3#
4# This is only an example, and you should edit to suit your needs.
5# If you don't need config, you can remove the file entirely.
6options:1options:
7 thing:2 bind_image_path:
8 default: 🎁
9 description: A thing used by the charm.
10 type: string3 type: string
4 description: |
5 The location of the image to use, e.g. "registry.example.com/bind:v1".
6
7 This setting is required.
8 default: ""
9 bind_image_username:
10 type: string
11 description: "Username to use for the configured image registry, if required"
12 default: ""
13 bind_image_password:
14 type: string
15 description: "Password to use for the configured image registry, if required"
16 default: ""
17 container_config:
18 type: string
19 description: >
20 YAML formatted map of container config keys & values. These are
21 generally accessed from inside the image as environment variables.
22 Use to configure customized Wordpress images. This configuration
23 gets logged; use container_secrets for secrets.
24 default: ""
25 container_secrets:
26 type: string
27 description: >
28 YAML formatted map of secrets. Works just like container_config,
29 except that values should not be logged.
30 default: ""
31 custom_config_repo:
32 type: string
33 description: |
34 Repository from which to populate /etc/bind/.
35 If unset, bind will be deployed with the package defaults.
36 e.g. http://github.com/foo/my-custom-bind-config
37 default: ""
38 https_proxy:
39 type: string
40 description: |
41 Proxy address to set in the environment, e.g. http://192.168.1.1:8080
42 Used to clone the configuration files from custom_config_repo, if set.
43 If a username/password is required, they can be embedded in the proxy
44 address e.g. http://username:password@192.168.1.1:8080
45 Traffic is expected to be HTTPS, but this will also work for HTTP.
46 default: ""
diff --git a/dockerfile b/dockerfile
11new file mode 10064447new file mode 100644
index 0000000..2e580d4
--- /dev/null
+++ b/dockerfile
@@ -0,0 +1,33 @@
1ARG DIST_RELEASE
2
3FROM ubuntu:${DIST_RELEASE}
4
5LABEL maintainer="bind-charmers@lists.launchpad.net"
6
7ARG BUILD_DATE
8ARG PKGS_TO_INSTALL
9
10LABEL org.label-schema.build-date=${BUILD_DATE}
11
12ENV BIND_CONFDIR=/etc/bind
13
14# Avoid interactive prompts
15RUN echo 'debconf debconf/frontend select Noninteractive' | debconf-set-selections
16
17# Update all packages, remove cruft, install required packages
18RUN apt-get update && apt-get -y dist-upgrade \
19 && apt-get --purge autoremove -y \
20 && apt-get install -y ${PKGS_TO_INSTALL}
21
22# entrypoint script will configure Bind based on env variables
23# dns-check script will provide a readinessProbe
24COPY ./files/docker-entrypoint.sh /usr/local/bin/
25COPY ./files/dns-check.sh /usr/local/bin/
26RUN chmod 0755 /usr/local/bin/docker-entrypoint.sh
27RUN chmod 0755 /usr/local/bin/dns-check.sh
28
29EXPOSE 53/udp
30EXPOSE 53/tcp
31
32ENTRYPOINT ["/usr/local/bin/docker-entrypoint.sh"]
33CMD /usr/sbin/named -g -u bind -c /etc/bind/named.conf
diff --git a/image-scripts/dns-check.sh b/image-scripts/dns-check.sh
0new file mode 10064434new file mode 100644
index 0000000..ca4a5a0
--- /dev/null
+++ b/image-scripts/dns-check.sh
@@ -0,0 +1,10 @@
1#!/bin/bash
2set -eu
3
4TEST_DOMAIN="ddebs.ubuntu.com."
5NSLOOKUP_PATH="/usr/bin/nslookup"
6OUR_ADDRESS="127.0.0.1"
7
8command -v "${NSLOOKUP_PATH}" >/dev/null || { echo "Cannot find the 'nslookup' command" && exit 1; }
9
10exec "${NSLOOKUP_PATH}" "${TEST_DOMAIN}" "${OUR_ADDRESS}" >/dev/null
diff --git a/image-scripts/docker-entrypoint.sh b/image-scripts/docker-entrypoint.sh
0new file mode 10064411new file mode 100644
index 0000000..2ed0857
--- /dev/null
+++ b/image-scripts/docker-entrypoint.sh
@@ -0,0 +1,23 @@
1#!/bin/bash
2set -eu
3
4if [ -z "${BIND_CONFDIR-}" ]; then
5 # If BIND_CONFDIR wasn't set, use the package default
6 BIND_CONFDIR="/etc/bind";
7fi
8
9if [ -z "${CUSTOM_CONFIG_REPO-}" ]; then
10 echo "No custom repo set, will fall back to package default config";
11else
12 echo "Pulling config from $CUSTOM_CONFIG_REPO";
13 if [ -d "${BIND_CONFDIR}" ]; then
14 mv "${BIND_CONFDIR}" "${BIND_CONFDIR}_$(date +"%Y-%m-%d_%H-%M-%S")";
15 fi
16 git clone "$CUSTOM_CONFIG_REPO" "$BIND_CONFDIR";
17fi
18
19if [ -d "${BIND_CONFDIR}" ]; then
20 exec "$@"
21else
22 echo "Something went wrong, ${BIND_CONFDIR} does not exist, not starting";
23fi
diff --git a/pyproject.toml b/pyproject.toml
0new file mode 10064424new file mode 100644
index 0000000..d2f23b9
--- /dev/null
+++ b/pyproject.toml
@@ -0,0 +1,3 @@
1[tool.black]
2skip-string-normalization = true
3line-length = 120
diff --git a/src/charm.py b/src/charm.py
index 640fea5..0faf651 100755
--- a/src/charm.py
+++ b/src/charm.py
@@ -1,38 +1,139 @@
1#!/usr/bin/env python31#!/usr/bin/env python3
2# Copyright 2020 Tom Haddon
3# See LICENSE file for licensing details.
42
5import logging3# Copyright 2020 Canonical Ltd.
4# Licensed under the GPLv3, see LICENCE file for details.
65
6import logging
7from ops.charm import CharmBase7from ops.charm import CharmBase
8from ops.main import main8from ops.main import main
9from ops.framework import StoredState9from ops.model import ActiveStatus, MaintenanceStatus
10from pprint import pformat
11from yaml import safe_load
1012
11logger = logging.getLogger(__name__)13logger = logging.getLogger()
1214
15REQUIRED_SETTINGS = ['bind_image_path']
1316
14class CharmcraftReviewCharm(CharmBase):
15 _stored = StoredState()
1617
18class BindK8sCharm(CharmBase):
17 def __init__(self, *args):19 def __init__(self, *args):
20 """Initialise our class, we only care about 'start' and 'config-changed' hooks."""
18 super().__init__(*args)21 super().__init__(*args)
19 self.framework.observe(self.on.config_changed, self._on_config_changed)22 self.framework.observe(self.on.start, self.on_config_changed)
20 self.framework.observe(self.on.fortune_action, self._on_fortune_action)23 self.framework.observe(self.on.config_changed, self.on_config_changed)
21 self._stored.set_default(things=[])24
2225 def _check_for_config_problems(self):
23 def _on_config_changed(self, _):26 """Return a string describing any configuration problems (or an empty string if none)."""
24 current = self.model.config["thing"]27 problems = ''
25 if current not in self._stored.things:28
26 logger.debug("found a new thing: %r", current)29 missing = self._missing_charm_settings()
27 self._stored.things.append(current)30 if missing:
2831 problems = 'required setting(s) empty: {}'.format(', '.join(sorted(missing)))
29 def _on_fortune_action(self, event):32
30 fail = event.params["fail"]33 return problems
31 if fail:34
32 event.fail(fail)35 def _missing_charm_settings(self):
36 """Return a list of missing configuration settings (or an empty list if none)."""
37 config = self.model.config
38
39 missing = {setting for setting in REQUIRED_SETTINGS if not config[setting]}
40
41 if config['bind_image_username'] and not config['bind_image_password']:
42 missing.add('bind_image_password')
43
44 return missing
45
46 def on_config_changed(self, event):
47 """Check that we're leader, and if so, set up the pod."""
48 if self.model.unit.is_leader():
49 # Only the leader can set_spec().
50 resources = self.make_pod_resources()
51 spec = self.make_pod_spec()
52 spec.update(resources)
53
54 msg = "Configuring pod"
55 logger.info(msg)
56 self.model.unit.status = MaintenanceStatus(msg)
57 self.model.pod.set_spec(spec)
58
59 msg = "Pod configured"
60 logger.info(msg)
61 self.model.unit.status = ActiveStatus(msg)
62 else:
63 logger.info("Spec changes ignored by non-leader")
64 self.model.unit.status = ActiveStatus()
65
66 def make_pod_resources(self):
67 """Compile and return our pod resources (e.g. ingresses)."""
68 # LP#1889746: We need to define a manual ingress here to work around LP#1889703.
69 resources = {} # TODO
70 logger.info("This is the Kubernetes Pod resources <<EOM\n{}\nEOM".format(pformat(resources)))
71 return resources
72
73 def generate_pod_config(self, secured=True):
74 """Kubernetes pod config generator.
75
76 generate_pod_config generates Kubernetes deployment config.
77 If the secured keyword is set then it will return a sanitised copy
78 without exposing secrets.
79 """
80 config = self.model.config
81 pod_config = {}
82 if config["container_config"].strip():
83 pod_config = safe_load(config["container_config"])
84
85 if config["custom_config_repo"].strip():
86 pod_config["CUSTOM_CONFIG_REPO"] = config["custom_config_repo"]
87
88 if config["https_proxy"].strip():
89 pod_config["http_proxy"] = config["https_proxy"]
90 pod_config["https_proxy"] = config["https_proxy"]
91
92 if secured:
93 return pod_config
94
95 if config["container_secrets"].strip():
96 container_secrets = safe_load(config["container_secrets"])
33 else:97 else:
34 event.set_results({"fortune": "A bug in the code is worth two in the documentation."})98 container_secrets = {}
99
100 pod_config.update(container_secrets)
101 return pod_config
102
103 def make_pod_spec(self):
104 """Set up and return our full pod spec here."""
105 config = self.model.config
106 full_pod_config = self.generate_pod_config(secured=False)
107 secure_pod_config = self.generate_pod_config(secured=True)
108
109 ports = [
110 {"name": "domain-tcp", "containerPort": 53, "protocol": "TCP"},
111 {"name": "domain-udp", "containerPort": 53, "protocol": "UDP"},
112 ]
113
114 spec = {
115 "version": 2,
116 "containers": [
117 {
118 "name": self.app.name,
119 "imageDetails": {"imagePath": config["bind_image_path"]},
120 "ports": ports,
121 "config": secure_pod_config,
122 "kubernetes": {"readinessProbe": {"exec": {"command": ["/usr/local/bin/dns-check.sh"]}}},
123 }
124 ],
125 }
126
127 logger.info("This is the Kubernetes Pod spec config (sans secrets) <<EOM\n{}\nEOM".format(pformat(spec)))
128
129 if config.get("bind_image_username") and config.get("bind_image_password"):
130 spec.get("containers")[0].get("imageDetails")["username"] = config["bind_image_username"]
131 spec.get("containers")[0].get("imageDetails")["password"] = config["bind_image_password"]
132
133 secure_pod_config.update(full_pod_config)
134
135 return spec
35136
36137
37if __name__ == "__main__":138if __name__ == "__main__":
38 main(CharmcraftReviewCharm)139 main(BindK8sCharm)
diff --git a/tests/__init__.py b/tests/__init__.py
39deleted file mode 100644140deleted file mode 100644
index e69de29..0000000
--- a/tests/__init__.py
+++ /dev/null
diff --git a/tests/test_charm.py b/tests/test_charm.py
40deleted file mode 100644141deleted file mode 100644
index 629f0ea..0000000
--- a/tests/test_charm.py
+++ /dev/null
@@ -1,36 +0,0 @@
1# Copyright 2020 Tom Haddon
2# See LICENSE file for licensing details.
3
4import unittest
5from unittest.mock import Mock
6
7from ops.testing import Harness
8from charm import CharmcraftReviewCharm
9
10
11class TestCharm(unittest.TestCase):
12 def test_config_changed(self):
13 harness = Harness(CharmcraftReviewCharm)
14 # from 0.8 you should also do:
15 # self.addCleanup(harness.cleanup)
16 harness.begin()
17 self.assertEqual(list(harness.charm._stored.things), [])
18 harness.update_config({"thing": "foo"})
19 self.assertEqual(list(harness.charm._stored.things), ["foo"])
20
21 def test_action(self):
22 harness = Harness(CharmcraftReviewCharm)
23 harness.begin()
24 # the harness doesn't (yet!) help much with actions themselves
25 action_event = Mock(params={"fail": ""})
26 harness.charm._on_fortune_action(action_event)
27
28 self.assertTrue(action_event.set_results.called)
29
30 def test_action_fail(self):
31 harness = Harness(CharmcraftReviewCharm)
32 harness.begin()
33 action_event = Mock(params={"fail": "fail this"})
34 harness.charm._on_fortune_action(action_event)
35
36 self.assertEqual(action_event.fail.call_args, [("fail this",)])
diff --git a/tests/unit/requirements.txt b/tests/unit/requirements.txt
37new file mode 1006440new file mode 100644
index 0000000..65431fc
--- /dev/null
+++ b/tests/unit/requirements.txt
@@ -0,0 +1,4 @@
1mock
2pytest
3pytest-cov
4pyyaml
diff --git a/tests/unit/test_charm.py b/tests/unit/test_charm.py
0new file mode 1006445new file mode 100644
index 0000000..dc53ac0
--- /dev/null
+++ b/tests/unit/test_charm.py
@@ -0,0 +1,167 @@
1# Copyright 2020 Canonical Ltd.
2# Licensed under the GPLv3, see LICENCE file for details.
3
4import unittest
5
6from charm import BindK8sCharm
7
8from ops import testing
9from ops.model import ActiveStatus
10
11CONFIG_EMPTY = {
12 'bind_image_path': '',
13 'bind_image_username': '',
14 'bind_image_password': '',
15 'container_config': '',
16 'container_secrets': '',
17 'custom_config_repo': '',
18 'https_proxy': '',
19}
20
21CONFIG_IMAGE_PASSWORD_MISSING = {
22 'bind_image_path': 'example.com/bind:v1',
23 'bind_image_username': 'username',
24 'bind_image_password': '',
25 'container_config': '',
26 'container_secrets': '',
27 'custom_config_repo': '',
28 'https_proxy': '',
29}
30
31CONFIG_VALID = {
32 'bind_image_path': 'example.com/bind:v1',
33 'bind_image_username': '',
34 'bind_image_password': '',
35 'container_config': '',
36 'container_secrets': '',
37 'custom_config_repo': '',
38 'https_proxy': '',
39}
40
41CONFIG_VALID_WITH_CONTAINER_CONFIG = {
42 'bind_image_path': 'example.com/bind:v1',
43 'bind_image_username': '',
44 'bind_image_password': '',
45 'container_config': '"magic_number": 123',
46 'container_secrets': '',
47 'custom_config_repo': '',
48 'https_proxy': '',
49}
50
51CONFIG_VALID_WITH_CONTAINER_CONFIG_AND_SECRETS = {
52 'bind_image_path': 'example.com/bind:v1',
53 'bind_image_username': '',
54 'bind_image_password': '',
55 'container_config': '"magic_number": 123',
56 'container_secrets': '"secret_password": "xyzzy"',
57 'custom_config_repo': '',
58 'https_proxy': '',
59}
60
61
62class TestBindK8s(unittest.TestCase):
63 maxDiff = None
64
65 def setUp(self):
66 self.harness = testing.Harness(BindK8sCharm)
67 self.harness.begin()
68 self.harness.disable_hooks()
69
70 def test_check_for_config_problems_empty_image_path(self):
71 """Confirm that we generate an error if we're not told what image to use."""
72 self.harness.update_config(CONFIG_EMPTY)
73 expected = 'required setting(s) empty: bind_image_path'
74 self.assertEqual(self.harness.charm._check_for_config_problems(), expected)
75
76 def test_check_for_config_problems_empty_image_password(self):
77 """Confirm that we generate an error if we're not given valid registry creds."""
78 self.harness.update_config(CONFIG_IMAGE_PASSWORD_MISSING)
79 expected = 'required setting(s) empty: bind_image_password'
80 self.assertEqual(self.harness.charm._check_for_config_problems(), expected)
81
82 def test_check_for_config_problems_none(self):
83 """Confirm that we accept valid config."""
84 self.harness.update_config(CONFIG_VALID)
85 expected = ''
86 self.assertEqual(self.harness.charm._check_for_config_problems(), expected)
87
88 def test_make_pod_resources(self):
89 """Confirm that we generate the expected pod resources (see LP#1889746)."""
90 expected = {}
91 self.assertEqual(self.harness.charm.make_pod_resources(), expected)
92
93 def test_make_pod_spec_basic(self):
94 """Confirm that we generate the expected pod spec from valid config."""
95 self.harness.update_config(CONFIG_VALID)
96 expected = {
97 'version': 2,
98 'containers': [
99 {
100 'name': 'bind',
101 'imageDetails': {'imagePath': 'example.com/bind:v1'},
102 'ports': [
103 {'containerPort': 53, 'name': 'domain-tcp', 'protocol': 'TCP'},
104 {'containerPort': 53, 'name': 'domain-udp', 'protocol': 'UDP'},
105 ],
106 'config': {},
107 'kubernetes': {'readinessProbe': {'exec': {'command': ['/usr/local/bin/dns-check.sh']}}},
108 }
109 ],
110 }
111 self.assertEqual(self.harness.charm.make_pod_spec(), expected)
112
113 def test_make_pod_spec_with_extra_config(self):
114 """Confirm that we generate the expected pod spec from a more involved valid config."""
115 self.harness.update_config(CONFIG_VALID_WITH_CONTAINER_CONFIG)
116 expected = {
117 'version': 2,
118 'containers': [
119 {
120 'name': 'bind',
121 'imageDetails': {'imagePath': 'example.com/bind:v1'},
122 'ports': [
123 {'containerPort': 53, 'name': 'domain-tcp', 'protocol': 'TCP'},
124 {'containerPort': 53, 'name': 'domain-udp', 'protocol': 'UDP'},
125 ],
126 'config': {'magic_number': 123},
127 'kubernetes': {'readinessProbe': {'exec': {'command': ['/usr/local/bin/dns-check.sh']}}},
128 }
129 ],
130 }
131 self.assertEqual(self.harness.charm.make_pod_spec(), expected)
132
133 def test_make_pod_spec_with_extra_config_and_secrets(self):
134 """Confirm that we generate the expected pod spec from a more involved valid config that includes secrets."""
135 self.harness.update_config(CONFIG_VALID_WITH_CONTAINER_CONFIG_AND_SECRETS)
136 expected = {
137 'version': 2,
138 'containers': [
139 {
140 'name': 'bind',
141 'imageDetails': {'imagePath': 'example.com/bind:v1'},
142 'ports': [
143 {'containerPort': 53, 'name': 'domain-tcp', 'protocol': 'TCP'},
144 {'containerPort': 53, 'name': 'domain-udp', 'protocol': 'UDP'},
145 ],
146 'config': {'magic_number': 123, 'secret_password': 'xyzzy'},
147 'kubernetes': {'readinessProbe': {'exec': {'command': ['/usr/local/bin/dns-check.sh']}}},
148 }
149 ],
150 }
151 self.assertEqual(self.harness.charm.make_pod_spec(), expected)
152
153 def test_configure_pod_as_leader(self):
154 """Confirm that our status is set correctly when we're the leader."""
155 self.harness.enable_hooks()
156 self.harness.set_leader(True)
157 self.harness.update_config(CONFIG_VALID)
158 expected = ActiveStatus('Pod configured')
159 self.assertEqual(self.harness.model.unit.status, expected)
160
161 def test_configure_pod_as_non_leader(self):
162 """Confirm that our status is set correctly when we're not the leader."""
163 self.harness.enable_hooks()
164 self.harness.set_leader(False)
165 self.harness.update_config(CONFIG_VALID)
166 expected = ActiveStatus()
167 self.assertEqual(self.harness.model.unit.status, expected)
diff --git a/tox.ini b/tox.ini
0new file mode 100644168new file mode 100644
index 0000000..0de5149
--- /dev/null
+++ b/tox.ini
@@ -0,0 +1,46 @@
1[tox]
2skipsdist=True
3envlist = unit, functional
4
5[testenv]
6basepython = python3
7setenv =
8 PYTHONPATH = {toxinidir}/build/lib:{toxinidir}/build/venv
9
10[testenv:unit]
11commands =
12 pytest --ignore mod --ignore {toxinidir}/tests/functional \
13 {posargs:-v --cov=src --cov-report=term-missing --cov-branch}
14deps = -r{toxinidir}/tests/unit/requirements.txt
15 -r{toxinidir}/requirements.txt
16setenv =
17 PYTHONPATH={toxinidir}/src:{toxinidir}/build/lib:{toxinidir}/build/venv
18 TZ=UTC
19
20[testenv:functional]
21passenv =
22 HOME
23 JUJU_REPOSITORY
24 PATH
25commands =
26 pytest -v --ignore mod --ignore {toxinidir}/tests/unit {posargs}
27deps = -r{toxinidir}/tests/functional/requirements.txt
28 -r{toxinidir}/requirements.txt
29
30[testenv:black]
31commands = black --skip-string-normalization --line-length=120 src/ tests/
32deps = black
33
34[testenv:lint]
35commands = flake8 src/ tests/
36# Pin flake8 to 3.7.9 to match focal
37deps =
38 flake8==3.7.9
39
40[flake8]
41exclude =
42 .git,
43 __pycache__,
44 .tox,
45max-line-length = 120
46max-complexity = 10

Subscribers

People subscribed via source and target branches

to all changes: