wordpress-charmers team mailing list archive
-
wordpress-charmers team
-
Mailing list archive
-
Message #00122
[Merge] ~tcuthbert/charm-k8s-wordpress/+git/charm-k8s-wordpress:operator into charm-k8s-wordpress:master
Thomas Cuthbert has proposed merging ~tcuthbert/charm-k8s-wordpress/+git/charm-k8s-wordpress:operator into charm-k8s-wordpress:master.
Commit message:
Refactor the Wordpress logic into a separate module
Requested reviews:
Canonical IS Reviewers (canonical-is-reviewers)
Wordpress Charmers (wordpress-charmers)
For more details, see:
https://code.launchpad.net/~tcuthbert/charm-k8s-wordpress/+git/charm-k8s-wordpress/+merge/381798
--
Your team Wordpress Charmers is requested to review the proposed merge of ~tcuthbert/charm-k8s-wordpress/+git/charm-k8s-wordpress:operator into charm-k8s-wordpress:master.
diff --git a/Makefile b/Makefile
index d4eb632..cf18918 100644
--- a/Makefile
+++ b/Makefile
@@ -5,7 +5,7 @@ lint:
@tox -e lint
unittest:
- @tox -e unit -vvv
+ @tox -e unit
test: lint unittest
diff --git a/src/charm.py b/src/charm.py
index 7f2eb7c..ddb402e 100755
--- a/src/charm.py
+++ b/src/charm.py
@@ -2,15 +2,12 @@
import io
import logging
-import re
-import secrets
-import string
-import subprocess
import sys
from pprint import pprint
-from urllib.parse import urlparse, urlunparse
from yaml import safe_load
+from wordpress import Wordpress
+
sys.path.append("lib")
from ops.charm import CharmBase, CharmEvents # NoQA: E402
@@ -21,23 +18,6 @@ from ops.model import ActiveStatus, BlockedStatus, MaintenanceStatus, WaitingSta
logger = logging.getLogger()
-def import_requests():
- # Workaround until https://github.com/canonical/operator/issues/156 is fixed.
- try:
- import requests
- except ImportError:
- subprocess.check_call(['apt-get', 'update'])
- subprocess.check_call(['apt-get', '-y', 'install', 'python3-requests'])
- import requests
-
- return requests
-
-
-def password_generator():
- alphabet = string.ascii_letters + string.digits
- return ''.join(secrets.choice(alphabet) for i in range(24))
-
-
def generate_pod_config(config, secured=True):
"""Kubernetes pod config generator.
@@ -104,31 +84,47 @@ class WordpressK8sCharm(CharmBase):
self.state.set_default(_spec=None)
+ self.wordpress = Wordpress(self.model.config)
+
def on_config_changed(self, event):
is_valid = self.is_valid_config()
if not is_valid:
return
self.configure_pod()
-
- if self.state.init and self.model.unit.is_leader() and not self.wordpress_configured():
+ if self.state.init:
self.on.wp_initialise.emit()
def on_wp_initialise(self, event):
- ready = self.install_ready()
- if not ready:
- # Until k8s supports telling Juju our pod is available we need to defer initial
- # site setup for a subsequent update-status or config-changed hook to complete.
- # https://github.com/canonical/operator/issues/214
- self.model.unit.status = WaitingStatus("Waiting for pod to be ready")
+ wordpress_needs_configuring = False
+ pod_alive = self.is_service_up() and self.model.unit.is_leader()
+ if pod_alive:
+ wordpress_configured = self.wordpress.wordpress_configured(self.get_service_ip())
+ wordpress_needs_configuring = self.state.init and not wordpress_configured
+ else:
+ msg = "Workpress workload pod is not ready"
+ logger.info(msg)
+ self.model.unit.status = WaitingStatus(msg)
return
- installed = self.first_install()
- if not installed:
- return
+ if wordpress_needs_configuring:
+ msg = "Wordpress needs configuration"
+ logger.info(msg)
+ self.model.unit.status = MaintenanceStatus(msg)
+ installed = self.wordpress.first_install(self.get_service_ip())
+ if not installed:
+ msg = "Failed to configure wordpress"
+ logger.info(msg)
+ self.model.unit.status = BlockedStatus(msg)
+ return
+
+ self.state.init = False
+ logger.info("Wordpress configured and initialised")
+ self.model.unit.status = ActiveStatus()
- logger.info("Wordpress installed and initialised")
- self.state.init = False
+ else:
+ logger.info("Wordpress workload pod is ready and configured")
+ self.model.unit.status = ActiveStatus()
def configure_pod(self):
spec = self.make_pod_spec()
@@ -183,60 +179,6 @@ class WordpressK8sCharm(CharmBase):
return spec
- def install_ready(self):
- ready = True
- if not self.is_pod_up("website"):
- logger.info("Pod not yet ready")
- ready = False
-
- try:
- if not self.is_vhost_ready():
- ready = False
- except Exception as e:
- logger.info("Wordpress vhost is not yet listening: {}".format(e))
- ready = False
-
- return ready
-
- def first_install(self):
- """Perform initial configuration of wordpress if needed."""
- config = self.model.config
- logger.info("Starting wordpress initial configuration")
- admin_password = password_generator()
- payload = {
- "admin_password": admin_password,
- "blog_public": "checked",
- "Submit": "submit",
- }
- payload.update(safe_load(config["initial_settings"]))
- payload["admin_password2"] = payload["admin_password"]
-
- # Ideally we would store this in state however juju run-action does not
- # currently support being run inside the operator pod which means the
- # StorageState will be split between workload and operator.
- # https://bugs.launchpad.net/juju/+bug/1870487
- with open("/root/initial.passwd", "w") as f:
- f.write(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:
- logger.info("Error: missing wordpress settings: {}".format(missing))
- return
- try:
- self.call_wordpress("/wp-admin/install.php?step=2", redirects=True, payload=payload)
- except Exception as e:
- logger.info("failed to call_wordpress: {}".format(e))
- return
-
- if not self.wordpress_configured():
- self.model.unit.status = BlockedStatus("Failed to install wordpress")
-
- self.model.unit.status = ActiveStatus()
- return True
-
def is_valid_config(self):
is_valid = True
config = self.model.config
@@ -256,86 +198,18 @@ class WordpressK8sCharm(CharmBase):
return is_valid
- def call_wordpress(self, uri, redirects=True, payload={}, _depth=1):
- requests = import_requests()
-
- max_depth = 10
- if _depth > max_depth:
- logger.info("Redirect loop detected in call_worpress()")
- raise RuntimeError("Redirect loop detected in call_worpress()")
- config = self.model.config
- service_ip = self.get_service_ip("website")
- if service_ip:
- headers = {"Host": config["blog_hostname"]}
- url = urlunparse(("http", service_ip, uri, "", "", ""))
- if payload:
- r = requests.post(url, allow_redirects=False, headers=headers, data=payload, timeout=30)
- else:
- r = requests.get(url, allow_redirects=False, headers=headers, timeout=30)
- if redirects and r.is_redirect:
- # Recurse, but strip the scheme and host first, we need to connect over HTTP by bare IP
- o = urlparse(r.headers.get("Location"))
- return self.call_wordpress(o.path, redirects=redirects, payload=payload, _depth=_depth + 1)
- else:
- return r
- else:
- logger.info("Error getting service IP")
- return False
-
- def wordpress_configured(self):
- """Check whether first install has been completed."""
- requests = import_requests()
-
- # Check whether pod is deployed
- if not self.is_pod_up("website"):
- return False
- # Check if we have WP code deployed at all
- if not self.is_vhost_ready():
- return False
- # We have code on disk, check if configured
- try:
- r = self.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
- elif r.status_code == 302 and re.match("^.*/wp-admin/setup-config.php", r.headers.get("location", "")):
- logger.info("MySQL database setup failed, we likely have no wp-config.php")
- self.model.unit.status = BlockedStatus("MySQL database setup failed, we likely have no wp-config.php")
- return False
- else:
- return True
-
- def is_vhost_ready(self):
- """Check whether wordpress is available using http."""
- requests = import_requests()
-
- rv = True
- # Check if we have WP code deployed at all
+ def get_service_ip(self):
try:
- r = self.call_wordpress("/wp-login.php", redirects=False)
- if r is None:
- logger.error("call_wordpress() returned None")
- rv = False
- if hasattr(r, "status_code") and r.status_code in (403, 404):
- logger.info("Wordpress returned an unexpected status {}".format(r.status_code))
- rv = False
- except requests.exceptions.ConnectionError:
- logger.info("HTTP vhost is not ready yet")
- rv = False
-
- return rv
-
- def get_service_ip(self, endpoint):
- try:
- return str(self.model.get_binding(endpoint).network.ingress_addresses[0])
+ return str(self.model.get_binding("website").network.ingress_addresses[0])
except Exception:
logger.info("We don't have any ingress addresses yet")
- def is_pod_up(self, endpoint):
- """Check to see if the pod of a relation is up"""
- return self.get_service_ip(endpoint) or False
+ def is_service_up(self):
+ """Check to see if the HTTP service is up"""
+ service_ip = self.get_service_ip()
+ if service_ip:
+ return self.wordpress.is_vhost_ready(service_ip)
+ return False
if __name__ == "__main__":
diff --git a/src/wordpress.py b/src/wordpress.py
new file mode 100644
index 0000000..171e2cb
--- /dev/null
+++ b/src/wordpress.py
@@ -0,0 +1,135 @@
+#!/usr/bin/env python3
+
+import logging
+import re
+import secrets
+import string
+import subprocess
+from urllib.parse import urlparse, urlunparse
+from yaml import safe_load
+
+logger = logging.getLogger()
+
+
+def import_requests():
+ # Workaround until https://github.com/canonical/operator/issues/156 is fixed.
+ try:
+ import requests
+ except ImportError:
+ subprocess.check_call(['apt-get', 'update'])
+ subprocess.check_call(['apt-get', '-y', 'install', 'python3-requests'])
+ import requests
+
+ return requests
+
+
+def password_generator():
+ alphabet = string.ascii_letters + string.digits
+ return ''.join(secrets.choice(alphabet) for i in range(24))
+
+
+class Wordpress:
+ def __init__(self, model_config):
+ self.model_config = model_config
+
+ def _write_initial_password(self, password, filepath):
+ with open(filepath, "w") as f:
+ f.write(password)
+
+ def first_install(self, service_ip):
+ """Perform initial configuration of wordpress if needed."""
+ config = self.model_config
+ logger.info("Starting wordpress initial configuration")
+ admin_password = password_generator()
+ payload = {
+ "admin_password": admin_password,
+ "blog_public": "checked",
+ "Submit": "submit",
+ }
+ payload.update(safe_load(config["initial_settings"]))
+ payload["admin_password2"] = payload["admin_password"]
+
+ # Ideally we would store this in state however juju run-action does not
+ # currently support being run inside the operator pod which means the
+ # StorageState will be split between workload and operator.
+ # https://bugs.launchpad.net/juju/+bug/1870487
+ self._write_initial_password(payload["admin_password"], "/root/initial.passwd")
+
+ if not payload["blog_public"]:
+ payload["blog_public"] = "unchecked"
+ required_config = set(("user_name", "admin_email"))
+ missing = required_config.difference(payload.keys())
+ if missing:
+ logger.info("Error: missing wordpress settings: {}".format(missing))
+ return
+ try:
+ self.call_wordpress(service_ip, "/wp-admin/install.php?step=2", redirects=True, payload=payload)
+ except Exception as e:
+ logger.info("failed to call_wordpress: {}".format(e))
+ return
+
+ if not self.wordpress_configured(service_ip):
+ return
+
+ return True
+
+ def call_wordpress(self, service_ip, uri, redirects=True, payload={}, _depth=1):
+ requests = import_requests()
+
+ max_depth = 10
+ if _depth > max_depth:
+ logger.info("Redirect loop detected in call_worpress()")
+ raise RuntimeError("Redirect loop detected in call_worpress()")
+ config = self.model_config
+ headers = {"Host": config["blog_hostname"]}
+ url = urlunparse(("http", service_ip, uri, "", "", ""))
+ if payload:
+ r = requests.post(url, allow_redirects=False, headers=headers, data=payload, timeout=30)
+ else:
+ r = requests.get(url, allow_redirects=False, headers=headers, timeout=30)
+ if redirects and r.is_redirect:
+ # Recurse, but strip the scheme and host first, we need to connect over HTTP by bare IP
+ o = urlparse(r.headers.get("Location"))
+ return self.call_wordpress(service_ip, o.path, redirects=redirects, payload=payload, _depth=_depth + 1)
+ else:
+ return r
+
+ def wordpress_configured(self, service_ip):
+ """Check whether first install has been completed."""
+ requests = import_requests()
+
+ # We have code on disk, check if configured
+ try:
+ r = self.call_wordpress(service_ip, "/", 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
+ elif r.status_code == 302 and re.match("^.*/wp-admin/setup-config.php", r.headers.get("location", "")):
+ logger.info("MySQL database setup failed, we likely have no wp-config.php")
+ return False
+ elif r.status_code in (500, 403, 404):
+ raise RuntimeError("unexpected status_code returned from Wordpress")
+
+ return True
+
+ def is_vhost_ready(self, service_ip):
+ """Check whether wordpress is available using http."""
+ requests = import_requests()
+
+ rv = True
+ # Check if we have WP code deployed at all
+ try:
+ r = self.call_wordpress(service_ip, "/wp-login.php", redirects=False)
+ if r is None:
+ logger.error("call_wordpress() returned None")
+ rv = False
+ if hasattr(r, "status_code") and r.status_code in (403, 404):
+ logger.info("Wordpress returned an unexpected status {}".format(r.status_code))
+ rv = False
+ except requests.exceptions.ConnectionError:
+ logger.info("HTTP vhost is not ready yet")
+ rv = False
+
+ return rv
diff --git a/tests/unit/test_wordpress.py b/tests/unit/test_wordpress.py
index 2d4fe25..d0fd5b5 100644
--- a/tests/unit/test_wordpress.py
+++ b/tests/unit/test_wordpress.py
@@ -7,6 +7,7 @@ import yaml
sys.path.append("src")
import charm # noqa: E402
+import wordpress # noqa: E402
TEST_MODEL_CONFIG = {
@@ -33,7 +34,7 @@ class HelperTest(unittest.TestCase):
test_model_config = TEST_MODEL_CONFIG
def test_password_generator(self):
- password = charm.password_generator()
+ password = wordpress.password_generator()
self.assertEqual(len(password), 24)
alphabet = string.ascii_letters + string.digits
for char in password:
Follow ups