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