wordpress-charmers team mailing list archive
-
wordpress-charmers team
-
Mailing list archive
-
Message #00001
Re: [Merge] ~barryprice/charm-k8s-wordpress/+git/charm-k8s-wordpress:master into charm-k8s-wordpress:master
Two minor comments inline. Would be good to get @stub's feedback on it as well.
Much of this looks like it's testable in a similar way to how we do for the content-cache charm (for example) - see ./tests/unit/test_content_cache.py for examples (we don't need full test coverage initially but it would be worth getting a base to start adding tests to soon.
Diff comments:
> diff --git a/README.md b/README.md
> new file mode 100644
> index 0000000..31e9edc
> --- /dev/null
> +++ b/README.md
> @@ -0,0 +1,54 @@
> += Wordpress k8s charm =
> +
> +A Juju charm for a Kubernetes deployment of Wordpress, using the
> +official Dockerhub Wordpress image or image built from this base.
> +
> +== Overview ==
> +
> +This is a k8s charm and can only be deployed to to a Juju k8s cloud,
> +attached to a controller using 'juju add-k8s'.
> +
> +The image to spin up is specified in the 'image' charm configuration
> +option using standard docker notation (eg. 'localhost:32000/mywork-rev42').
> +Images must be publicly accessible. Default is the Dockerhub
> +`wordpress:php7.3` image.
> +
> +Standard configuration for the Wordpress image is in standard Juju config.
> +In particular:
> +
> +* `db_host`, `db_user` & `db_password`. This charm may in future be relatable
> + to a MySQL deployment, when the MySQL charm is updated to support cross
> + model relations.
> +* `ports`. Custom images may require additional ports to be opened, such
> + as those providing monitoring or metrics endpoints.
> +
> +Additional runtine configuration is specified as YAML snippets in the charm config.
> +Both 'container_config' and 'container_secrets' items are provided,
> +and they are combined together. 'container_config' gets logged,
> +'container_secrets' does not. This allows you to configure customized
> +Wordpress images.
> +
> +== Details ==
> +
> +See config option descriptions in config.yaml.
> +
> +== Quickstart ==
> +
> +Notes for deploying a test setup locally using microk8s:
> +
> + sudo snap install juju --classic
> + sudo snap install juju-wait --classic
> + sudo snap install microk8s --classic
> + sudo snap alias microk8s.kubectl kubectl
> +
> + microk8s.reset # Warning! Clean slate!
> + microk8s.enable dns dashboard registry storage
> + microk8s.status --wait-ready
> + microk8s.config | juju add-k8s myk8s
> + juju bootstrap myk8s
> + juju add-model wordpress-test
> + juju create-storage-pool operator-storage kubernetes storage-class=microk8s-hostpath
> + juju deploy cs:~stub/wordpress-k8s --channel=edge wordpress
Location to deploy from needs updating.
> + juju config wordpress db_host=10.1.1.1 db_user=wp db_password=secret
> + juju wait
> + juju status # Shows IP address, and port is 80
> diff --git a/reactive/wordpress.py b/reactive/wordpress.py
> new file mode 100644
> index 0000000..7a84e48
> --- /dev/null
> +++ b/reactive/wordpress.py
> @@ -0,0 +1,228 @@
> +import io
> +import os
> +import random
> +import re
> +import requests
> +import string
> +from pprint import pprint
> +from urllib.parse import urlunparse
> +from yaml import safe_load
> +
> +from charmhelpers.core import host, hookenv
> +from charms import reactive
> +from charms.layer import caas_base, status
> +from charms.osm.k8s import is_pod_up, get_service_ip
> +from charms.reactive import hook, when, when_not
> +
> +
> +@hook("upgrade-charm")
> +def upgrade_charm():
> + reactive.clear_flag("wordpress.configured")
> +
> +
> +@when("config.changed")
> +def reconfig():
> + status.maintenance("charm configuration changed")
> + reactive.clear_flag("wordpress.configured")
> +
> + # Validate config
> + valid = True
> + config = hookenv.config()
> + # Ensure required strings
> + for k in ["image", "db_host", "db_name", "db_user", "db_password"]:
> + if config[k].strip() == "":
> + status.blocked("{!r} config is required".format(k))
> + valid = False
> +
> + reactive.toggle_flag("wordpress.config.valid", valid)
> +
> +
> +@when("wordpress.config.valid")
> +@when_not("wordpress.configured")
> +def deploy_container():
> + spec = make_pod_spec()
> + if spec is None:
> + return # status already set
> + if reactive.data_changed("wordpress.spec", spec):
> + status.maintenance("configuring container")
> + try:
> + caas_base.pod_spec_set(spec)
> + except Exception:
Seems like it'd be good to log what the failure is to make debugging easier, or is there a reason why we're not doing that?
> + status.blocked("pod_spec_set failed! Check logs and k8s dashboard.")
> + return
> + else:
> + hookenv.log("No changes to pod spec")
> + if first_install():
> + reactive.set_flag("wordpress.configured")
> +
> +
> +@when("wordpress.configured")
> +def ready():
> + status.active("Ready")
> +
> +
> +def sanitized_container_config():
> + """Uninterpolated container config without secrets"""
> + config = hookenv.config()
> + if config["container_config"].strip() == "":
> + container_config = {}
> + else:
> + container_config = safe_load(config["container_config"])
> + if not isinstance(container_config, dict):
> + status.blocked("container_config is not a YAML mapping")
> + return None
> + container_config["WORDPRESS_DB_HOST"] = config["db_host"]
> + container_config["WORDPRESS_DB_NAME"] = config["db_name"]
> + container_config["WORDPRESS_DB_USER"] = config["db_user"]
> + return container_config
> +
> +
> +def full_container_config():
> + """Uninterpolated container config with secrets"""
> + config = hookenv.config()
> + container_config = sanitized_container_config()
> + if container_config is None:
> + return None
> + if config["container_secrets"].strip() == "":
> + container_secrets = {}
> + else:
> + container_secrets = safe_load(config["container_secrets"])
> + if not isinstance(container_secrets, dict):
> + status.blocked("container_secrets is not a YAML mapping")
> + return None
> + container_config.update(container_secrets)
> + container_config["WORDPRESS_DB_PASSWORD"] = config["db_password"]
> + return container_config
> +
> +
> +def make_pod_spec():
> + config = hookenv.config()
> + container_config = sanitized_container_config()
> + if container_config is None:
> + return # status already set
> +
> + ports = [
> + {"name": name, "containerPort": int(port), "protocol": "TCP"}
> + for name, port in [addr.split(":", 1) for addr in config["ports"].split()]
> + ]
> +
> + # PodSpec v1? https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.13/#podspec-v1-core
> + spec = {
> + "containers": [
> + {"name": hookenv.charm_name(), "image": config["image"], "ports": ports, "config": container_config}
> + ]
> + }
> + out = io.StringIO()
> + pprint(spec, out)
> + hookenv.log("Container spec (sans secrets) <<EOM\n{}\nEOM".format(out.getvalue()))
> +
> + # Add the secrets after logging
> + config_with_secrets = full_container_config()
> + if config_with_secrets is None:
> + return None # status already set
> + container_config.update(config_with_secrets)
> +
> + return spec
> +
> +
> +def first_install():
> + """Perform initial configuration of wordpress if needed."""
> + config = hookenv.config()
> + if not is_pod_up("website"):
> + hookenv.log("Pod not yet ready - retrying")
> + return False
> + elif wordpress_configured() or not config["initial_settings"]:
> + hookenv.log("No initial_setting provided or wordpress already configured. Skipping first install.")
> + return True
> + elif not vhost_ready():
> + hookenv.log("Wordpress vhost is not yet listening - retrying")
> + return False
> + hookenv.log("Starting wordpress initial configuration")
> + # TODO: more of the below ought to be configurable
> + payload = {"admin_password": mkpasswd(24), "blog_public": "checked", "Submit": "submit"}
> + payload.update(safe_load(config["initial_settings"]))
> + payload["admin_password2"] = payload["admin_password"]
> + if not payload["blog_public"]:
> + payload["blog_public"] = "unchecked"
> + required_config = set(("user_name", "admin_email"))
> + missing = required_config.difference(payload.keys())
> + if missing:
> + hookenv.log("Error: missing wordpress settings: {}".format(missing))
> + return False
> + call_wordpress("/wp-admin/install.php?step=2", payload=payload)
> + host.write_file(os.path.join("/root/", "initial.passwd"), payload["admin_password"], perms=0o400)
> + return True
> +
> +
> +def mkpasswd(length=64):
> + return "".join(random.choice(string.ascii_letters + string.digits) for _ in range(length))
> +
> +
> +def call_wordpress(uri, redirects=True, payload={}):
> + config = hookenv.config()
> + service_ip = get_service_ip("website")
> + if service_ip:
> + headers = {"Host": config["blog_hostname"]}
> + url = urlunparse((
> + 'http',
> + service_ip,
> + uri,
> + '',
> + '',
> + ''
> + ))
> + if payload:
> + return requests.post(url, allow_redirects=redirects, headers=headers, data=payload)
> + else:
> + return requests.get(url, allow_redirects=redirects, headers=headers)
> + else:
> + hookenv.log("Error getting service IP")
> + return False
> +
> +
> +class wordpress_configured(dict):
> + """Check whether first install has been completed."""
> +
> + def __bool__(self):
> + # Check whether pod is deployed
> + if not is_pod_up("website"):
> + return False
> + # Check if we have WP code deployed at all
> + if not vhost_ready():
> + return False
> + # We have code on disk, check if configured
> + try:
> + r = call_wordpress("/", redirects=False)
> + except requests.exceptions.ConnectionError:
> + return False
> + if r.status_code == 302 and re.match("^.*/wp-admin/install.php", r.headers.get("location", "")):
> + return False
> + else:
> + return True
> +
> + def __nonzero__(self):
> + return self.__bool__()
> +
> + def is_ready(self):
> + return self.__bool__()
> +
> +
> +class vhost_ready(dict):
> + """Check whether wordpress is available using http."""
> +
> + def __bool__(self):
> + # Check if we have WP code deployed at all
> + try:
> + r = call_wordpress("/wp-login.php", redirects=False)
> + except requests.exceptions.ConnectionError:
> + return False
> + if r.status_code in (403, 404):
> + return False
> + else:
> + return True
> +
> + def __nonzero__(self):
> + return self.__bool__()
> +
> + def is_ready(self):
> + return self.__bool__()
--
https://code.launchpad.net/~barryprice/charm-k8s-wordpress/+git/charm-k8s-wordpress/+merge/375596
Your team Wordpress Charmers is requested to review the proposed merge of ~barryprice/charm-k8s-wordpress/+git/charm-k8s-wordpress:master into charm-k8s-wordpress:master.
References