← Back to team overview

wordpress-charmers team mailing list archive

[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