Barry Price has proposed merging ~barryprice/charm-k8s-bind/+git/charm-k8s-bind:master into charm-k8s-bind:master.
Commit message: Very basic first pass at an Operator charm to deploy Bind onto Kubernetes. Tests and further functionality to follow. Requested reviews: Bind Charmers (bind-charmers) For more details, see: https://code.launchpad.net/~barryprice/charm-k8s-bind/+git/charm-k8s-bind/+merge/387828 -- Your team Bind Charmers is requested to review the proposed merge of ~barryprice/charm-k8s-bind/+git/charm-k8s-bind:master into charm-k8s-bind:master.
diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..490cc43 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +*~ +*.charm +.tox +.coverage +__pycache__ +build diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..d403fd0 --- /dev/null +++ b/Makefile @@ -0,0 +1,24 @@ +blacken: + @echo "Normalising python layout with black." + @tox -e black + + +lint: blacken + @echo "Running flake8" + @tox -e lint + +# We actually use the build directory created by charmcraft, +# but the .charm file makes a much more convenient sentinel. +unittest: bind.charm + @tox -e unit + +test: lint unittest + +clean: + @echo "Cleaning files" + @git clean -fXd + +bind.charm: src/*.py requirements.txt + charmcraft build + +.PHONY: blacken lint unittest test clean diff --git a/config.yaml b/config.yaml new file mode 100644 index 0000000..408ae9d --- /dev/null +++ b/config.yaml @@ -0,0 +1,46 @@ +options: + bind_image_path: + type: string + description: | + The location of the image to use, e.g. "registry.example.com/bind:v1". + + This setting is required. + default: "" + bind_image_username: + type: string + description: "Username to use for the configured image registry, if required" + default: "" + bind_image_password: + type: string + description: "Password to use for the configured image registry, if required" + default: "" + container_config: + type: string + description: > + YAML formatted map of container config keys & values. These are + generally accessed from inside the image as environment variables. + Use to configure customized Wordpress images. This configuration + gets logged; use container_secrets for secrets. + default: "" + container_secrets: + type: string + description: > + YAML formatted map of secrets. Works just like container_config, + except that values should not be logged. + default: "" + custom_config_repo: + type: string + description: | + Repository from which to populate /etc/bind/. + If unset, bind will be deployed with the package defaults. + e.g. http://github.com/foo/my-custom-bind-config + default: "" + https_proxy: + type: string + description: | + Proxy address to set in the environment, e.g. http://192.168.1.1:8080 + Used to clone the configuration files from custom_config_repo, if set. + If a username/password is required, they can be embedded in the proxy + address e.g. http://username:[email protected]:8080 + Traffic is expected to be HTTPS, but this will also work for HTTP. + default: "" diff --git a/metadata.yaml b/metadata.yaml new file mode 100644 index 0000000..12075b3 --- /dev/null +++ b/metadata.yaml @@ -0,0 +1,11 @@ +name: "bind" +summary: "Bind" +description: "The original, complete open source DNS implementation" +min-juju-version: 2.8.0 +maintainers: + - https://launchpad.net/~bind-charmers <[email protected]> +tags: + - network + - ops +series: + - kubernetes diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..d2f23b9 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,3 @@ +[tool.black] +skip-string-normalization = true +line-length = 120 diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..2d81d3b --- /dev/null +++ b/requirements.txt @@ -0,0 +1 @@ +ops diff --git a/src/charm.py b/src/charm.py new file mode 100755 index 0000000..efb18f5 --- /dev/null +++ b/src/charm.py @@ -0,0 +1,139 @@ +#!/usr/bin/env python3 + +import io +import logging +from ops.charm import CharmBase +from ops.main import main +from ops.model import ActiveStatus, MaintenanceStatus +from pprint import pprint +from yaml import safe_load + +logger = logging.getLogger() + +REQUIRED_SETTINGS = ['bind_image_path'] + + +def generate_pod_config(config, secured=True): + """Kubernetes pod config generator. + + generate_pod_config generates Kubernetes deployment config. + If the secured keyword is set then it will return a sanitised copy + without exposing secrets. + """ + pod_config = {} + if config["container_config"].strip(): + pod_config = safe_load(config["container_config"]) + + if config["custom_config_repo"].strip(): + pod_config["CUSTOM_CONFIG_REPO"] = config["custom_config_repo"] + + if config["https_proxy"].strip(): + pod_config["http_proxy"] = config["https_proxy"] + pod_config["HTTP_PROXY"] = config["https_proxy"] + pod_config["https_proxy"] = config["https_proxy"] + pod_config["HTTPS_PROXY"] = config["https_proxy"] + + if secured: + return pod_config + + # Add secrets from charm config. + pass + + return pod_config + + +class Bind(CharmBase): + def __init__(self, *args): + super().__init__(*args) + self.framework.observe(self.on.start, self.on_config_changed) + self.framework.observe(self.on.config_changed, self.on_config_changed) + + def _check_for_config_problems(self): + """Check for some simple configuration problems and return a + string describing them, otherwise return an empty string.""" + problems = [] + + missing = self._missing_charm_settings() + if missing: + problems.append('required setting(s) empty: {}'.format(', '.join(sorted(missing)))) + + return '; '.join(filter(None, problems)) + + def _missing_charm_settings(self): + """Check configuration setting dependencies and return a list of + missing settings; otherwise return an empty list.""" + config = self.model.config + missing = [] + + missing.extend([setting for setting in REQUIRED_SETTINGS if not config[setting]]) + + if config['bind_image_username'] and not config['bind_image_password']: + missing.append('bind_image_password') + + return sorted(list(set(missing))) + + def on_config_changed(self, event): + self.configure_pod() + + def configure_pod(self): + # Only the leader can set_spec(). + if self.model.unit.is_leader(): + resources = self.make_pod_resources() + spec = self.make_pod_spec() + spec.update(resources) + + msg = "Configuring pod" + logger.info(msg) + self.model.unit.status = MaintenanceStatus(msg) + self.model.pod.set_spec(spec) + + msg = "Pod configured" + logger.info(msg) + self.model.unit.status = ActiveStatus(msg) + else: + logger.info("Spec changes ignored by non-leader") + + def make_pod_resources(self): + resources = {} + out = io.StringIO() + pprint(resources, out) + logger.info("This is the Kubernetes Pod resources <<EOM\n{}\nEOM".format(out.getvalue())) + return resources + + def make_pod_spec(self): + config = self.model.config + full_pod_config = generate_pod_config(config, secured=False) + secure_pod_config = generate_pod_config(config, secured=True) + + ports = [ + {"name": "domain-tcp", "containerPort": 53, "protocol": "TCP"}, + {"name": "domain-udp", "containerPort": 53, "protocol": "UDP"}, + ] + + spec = { + "version": 2, + "containers": [ + { + "name": self.app.name, + "imageDetails": {"imagePath": config["bind_image_path"]}, + "ports": ports, + "config": secure_pod_config, + } + ], + } + + out = io.StringIO() + pprint(spec, out) + logger.info("This is the Kubernetes Pod spec config (sans secrets) <<EOM\n{}\nEOM".format(out.getvalue())) + + if config.get("bind_image_username") and config.get("bind_image_password"): + spec.get("containers")[0].get("imageDetails")["username"] = config["bind_image_username"] + spec.get("containers")[0].get("imageDetails")["password"] = config["bind_image_password"] + + secure_pod_config.update(full_pod_config) + + return spec + + +if __name__ == "__main__": + main(Bind) diff --git a/tests/unit/requirements.txt b/tests/unit/requirements.txt new file mode 100644 index 0000000..65431fc --- /dev/null +++ b/tests/unit/requirements.txt @@ -0,0 +1,4 @@ +mock +pytest +pytest-cov +pyyaml diff --git a/tests/unit/test_charm.py b/tests/unit/test_charm.py new file mode 100644 index 0000000..9d69e47 --- /dev/null +++ b/tests/unit/test_charm.py @@ -0,0 +1,27 @@ +import unittest + +from charm import Bind + +from ops import testing + +EMPTY_CONFIG = { + 'bind_image_path': '', + 'bind_image_username': '', + 'bind_image_password': '', + 'container_config': '', + 'container_secrets': '', + 'custom_config_repo': '', + 'https_proxy': '', +} + + +class TestBindK8sCharmHooksDisabled(unittest.TestCase): + def setUp(self): + self.harness = testing.Harness(Bind) + self.harness.begin() + self.harness.disable_hooks() + + def test_check_for_config_problems(self): + self.harness.update_config(EMPTY_CONFIG) + expected = 'required setting(s) empty: bind_image_path' + self.assertEqual(self.harness.charm._check_for_config_problems(), expected) diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..91adecf --- /dev/null +++ b/tox.ini @@ -0,0 +1,48 @@ +[tox] +skipsdist=True +envlist = unit, functional + +[testenv] +basepython = python3 +setenv = + PYTHONPATH = {toxinidir}/build/lib:{toxinidir}/build/venv + +[testenv:unit] +commands = + pytest --ignore mod --ignore {toxinidir}/tests/functional \ + {posargs:-v --cov=src --cov-report=term-missing --cov-branch} +deps = -r{toxinidir}/tests/unit/requirements.txt + -r{toxinidir}/requirements.txt +setenv = + PYTHONPATH={toxinidir}/src:{toxinidir}/build/lib:{toxinidir}/build/venv + TZ=UTC + +[testenv:functional] +passenv = + HOME + JUJU_REPOSITORY + PATH +commands = + pytest -v --ignore mod --ignore {toxinidir}/tests/unit {posargs} +deps = -r{toxinidir}/tests/functional/requirements.txt + -r{toxinidir}/requirements.txt + +[testenv:black] +commands = black --skip-string-normalization --line-length=120 src/ tests/ +deps = black + +[testenv:lint] +commands = flake8 src/ tests/ +# Pin flake8 to 3.7.9 to match focal +deps = + flake8==3.7.9 + +[flake8] +exclude = + .git, + __pycache__, + .tox, +# Ignore E231 because using black creates errors with this +ignore = E231 +max-line-length = 120 +max-complexity = 10
-- Mailing list: https://launchpad.net/~bind-charmers Post to : [email protected] Unsubscribe : https://launchpad.net/~bind-charmers More help : https://help.launchpad.net/ListHelp

