← Back to team overview

wordpress-charmers team mailing list archive

Re: [Merge] ~barryprice/charm-k8s-wordpress/+git/charm-k8s-wordpress:operator into charm-k8s-wordpress:master

 

Some comments inline

Diff comments:

> diff --git a/reactive/wordpress.py b/src/charm.py
> similarity index 53%
> rename from reactive/wordpress.py
> rename to src/charm.py
> index 9a1b013..9d5be4d 100644
> --- a/reactive/wordpress.py
> +++ b/src/charm.py
> @@ -1,292 +1,308 @@
> +#!/usr/bin/env python3
> +
>  import io
> -import os
>  import re
>  import requests
> +import sys
>  from pprint import pprint
>  from urllib.parse import urlparse, 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.reactive import hook, when, when_not
> -
> -
> -@hook("upgrade-charm")
> -def upgrade_charm():
> -    status.maintenance("Upgrading 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 as e:
> -            hookenv.log("pod_spec_set failed: {}".format(e), hookenv.DEBUG)
> -            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():
> -    """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"]
> -    if config.get("wp_plugin_openid_team_map"):
> -        container_config["WP_PLUGIN_OPENID_TEAM_MAP"] = config["wp_plugin_openid_team_map"]
> -    return container_config
> -
> -
> -def full_container_config():
> -    """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")
> +sys.path.append("lib")
> +
> +from ops.charm import CharmBase  # NoQA: E402
> +from ops.framework import StoredState  # NoQA: E402
> +from ops.main import main  # NoQA: E402
> +from ops.model import ActiveStatus, BlockedStatus, MaintenanceStatus  # NoQA: E402
> +
> +
> +class Wordpress(CharmBase):
> +    state = StoredState()
> +
> +    def __init__(self, *args):
> +        super().__init__(*args)
> +        for event in (
> +            self.on.install,
> +            self.on.start,
> +            self.on.upgrade_charm,
> +            self.on.update_status,
> +            self.on.config_changed,
> +        ):
> +            self.framework.observe(event, self)
> +
> +    def on_start(self, event):
> +        self.state._started = True

Do we really need this?

> +
> +    def on_upgrade_charm(self, event):
> +        self.on_config_changed(event)
> +
> +    def on_config_changed(self, event):
> +        valid = True
> +        config = self.model.config
> +        for k in ["image", "db_host", "db_name", "db_user", "db_password"]:
> +            if config[k].strip() == "":
> +                self.model.unit.status = BlockedStatus("{!r} config is required".format(k))

Why not just return here, then I don't think you need to worry about valid state at all?

> +                valid = False
> +        self.state._valid = valid
> +        self.configure_pod(event)
> +
> +    def sanitized_container_config(self):
> +        """Container config without secrets"""
> +        config = self.model.config
> +        if config["container_config"].strip() == "":
> +            container_config = {}
> +        else:
> +            container_config = safe_load(config["container_config"])
> +            if not isinstance(container_config, dict):
> +                self.model.unit.status = BlockedStatus("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"]
> +        if config.get("wp_plugin_openid_team_map"):
> +            container_config["WP_PLUGIN_OPENID_TEAM_MAP"] = config["wp_plugin_openid_team_map"]
> +        return container_config
> +
> +    def full_container_config(self):
> +        """Container config with secrets"""
> +        config = self.model.config
> +        container_config = self.sanitized_container_config()
> +        if container_config is None:
>              return None
> -    container_config.update(container_secrets)
> -    # Add secrets from charm config
> -    container_config["WORDPRESS_DB_PASSWORD"] = config["db_password"]
> -    if config.get("wp_plugin_akismet_key"):
> -        container_config["WP_PLUGIN_AKISMET_KEY"] = config["wp_plugin_akismet_key"]
> -    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(),
> -                "imageDetails": {"imagePath": config["image"]},
> -                "ports": ports,
> -                "config": container_config,
> -            }
> +        if config["container_secrets"].strip() == "":
> +            container_secrets = {}
> +        else:
> +            container_secrets = safe_load(config["container_secrets"])
> +            if not isinstance(container_secrets, dict):
> +                self.model.unit.status = BlockedStatus("container_secrets is not a YAML mapping")
> +                return None
> +        container_config.update(container_secrets)
> +        # Add secrets from charm config
> +        container_config["WORDPRESS_DB_PASSWORD"] = config["db_password"]
> +        if config.get("wp_plugin_akismet_key"):
> +            container_config["WP_PLUGIN_AKISMET_KEY"] = config["wp_plugin_akismet_key"]
> +        return container_config
> +
> +    def make_pod_spec(self):
> +        config = self.model.config
> +        container_config = self.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()]
>          ]
> -    }
> -    out = io.StringIO()
> -    pprint(spec, out)
> -    hookenv.log("Container environment config (sans secrets) <<EOM\n{}\nEOM".format(out.getvalue()))
>  
> -    # If we need credentials (secrets) for our image, add them to the spec after logging
> -    if config.get("image_user") and config.get("image_pass"):
> -        spec.get("containers")[0].get("imageDetails")["username"] = config["image_user"]
> -        spec.get("containers")[0].get("imageDetails")["password"] = config["image_pass"]
> -
> -    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 not is_vhost_ready():
> -        hookenv.log("Wordpress vhost is not yet listening - retrying")
> -        return False
> -    elif wordpress_configured() or not config["initial_settings"]:
> -        hookenv.log("No initial_setting provided or wordpress already configured. Skipping first install.")
> +        # PodSpec v1? https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.13/#podspec-v1-core
> +        spec = {
> +            "containers": [
> +                {
> +                    "name": self.app.name,
> +                    "imageDetails": {"imagePath": config["image"]},
> +                    "ports": ports,
> +                    "config": container_config,
> +                }
> +            ]
> +        }
> +        out = io.StringIO()
> +        pprint(spec, out)
> +        # TODO.log("Container environment config (sans secrets) <<EOM\n{}\nEOM".format(out.getvalue()))
> +
> +        # If we need credentials (secrets) for our image, add them to the spec after logging
> +        if config.get("image_user") and config.get("image_pass"):
> +            spec.get("containers")[0].get("imageDetails")["username"] = config["image_user"]
> +            spec.get("containers")[0].get("imageDetails")["password"] = config["image_pass"]
> +
> +        config_with_secrets = self.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(self):
> +        """Perform initial configuration of wordpress if needed."""
> +        config = self.model.config
> +        if not self.is_pod_up("website"):
> +            # TODO.log("Pod not yet ready - retrying")
> +            return False
> +        elif not self.is_vhost_ready():
> +            # TODO.log("Wordpress vhost is not yet listening - retrying")
> +            return False
> +        elif self.wordpress_configured() or not config["initial_settings"]:
> +            # TODO.log("No initial_setting provided or wordpress already configured. Skipping first install.")
> +            return True
> +        # TODO.log("Starting wordpress initial configuration")
> +        payload = {
> +            # "admin_password": host.pwgen(24), ## TODO: pwgen
> +            "admin_password": "letmein123",
> +            "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:
> +            # TODO.log("Error: missing wordpress settings: {}".format(missing))
> +            return False
> +        self.call_wordpress("/wp-admin/install.php?step=2", redirects=True, payload=payload)
> +        # TODO: write_file
> +        # host.write_file(os.path.join("/root/", "initial.passwd"), payload["admin_password"], perms=0o400)
>          return True
> -    hookenv.log("Starting wordpress initial configuration")
> -    payload = {
> -        "admin_password": host.pwgen(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", redirects=True, payload=payload)
> -    host.write_file(os.path.join("/root/", "initial.passwd"), payload["admin_password"], perms=0o400)
> -    return True
>  
> -
> -def call_wordpress(uri, redirects=True, payload={}, _depth=1):
> -    max_depth = 10
> -    if _depth > max_depth:
> -        hookenv.log("Redirect loop detected in call_worpress()")
> -        raise RuntimeError("Redirect loop detected in call_worpress()")
> -    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:
> -            r = requests.post(url, allow_redirects=False, headers=headers, data=payload, timeout=30)
> +    def call_wordpress(self, uri, redirects=True, payload={}, _depth=1):
> +        max_depth = 10
> +        if _depth > max_depth:
> +            # TODO.log("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:
> -            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 call_wordpress(o.path, redirects=redirects, payload=payload, _depth=_depth + 1)
> +            # TODO.log("Error getting service IP")
> +            return False
> +
> +    def wordpress_configured(self):
> +        """Check whether first install has been completed."""
> +        # 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", "")):
> +            # TODO.log("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 r
> -    else:
> -        hookenv.log("Error getting service IP")
> -        return False
> -
> -
> -def wordpress_configured():
> -    """Check whether first install has been completed."""
> -    # Check whether pod is deployed
> -    if not is_pod_up("website"):
> -        return False
> -    # Check if we have WP code deployed at all
> -    if not is_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
> -    elif r.status_code == 302 and re.match("^.*/wp-admin/setup-config.php", r.headers.get("location", "")):
> -        hookenv.log("MySQL database setup failed, we likely have no wp-config.php")
> -        status.blocked("MySQL database setup failed, we likely have no wp-config.php")
> -        return False
> -    else:
> -        return True
> -
> -
> -def is_vhost_ready():
> -    """Check whether wordpress is available using http."""
> -    # Check if we have WP code deployed at all
> -    try:
> -        r = call_wordpress("/wp-login.php", redirects=False)
> -    except requests.exceptions.ConnectionError:
> -        hookenv.log("call_wordpress() returned requests.exceptions.ConnectionError")
> -        return False
> -    if r is None:
> -        hookenv.log("call_wordpress() returned None")
> -        return False
> -    if hasattr(r, "status_code") and r.status_code in (403, 404):
> -        hookenv.log("call_wordpress() returned status {}".format(r.status_code))
> -        return False
> -    else:
> -        return True
> -
> +            return True
>  
> -def get_service_ip(endpoint):
> -    try:
> -        info = hookenv.network_get(endpoint, hookenv.relation_id())
> -        if "ingress-addresses" in info:
> -            addr = info["ingress-addresses"][0]
> -            if len(addr):
> -                return addr
> +    def is_vhost_ready(self):
> +        """Check whether wordpress is available using http."""
> +        # Check if we have WP code deployed at all
> +        try:
> +            r = self.call_wordpress("/wp-login.php", redirects=False)
> +        except requests.exceptions.ConnectionError:
> +            # TODO.log("call_wordpress() returned requests.exceptions.ConnectionError")
> +            return False
> +        if r is None:
> +            # TODO.log("call_wordpress() returned None")
> +            return False
> +        if hasattr(r, "status_code") and r.status_code in (403, 404):
> +            # TODO.log("call_wordpress() returned status {}".format(r.status_code))
> +            return False
>          else:
> -            hookenv.log("No ingress-addresses: {}".format(info))
> -    except Exception as e:
> -        hookenv.log("Caught exception checking for service IP: {}".format(e))
> -
> -    return None
> -
> -
> -def is_pod_up(endpoint):
> -    """Check to see if the pod of a relation is up.
> +            return True
>  
> -    application-vimdb: 19:29:10 INFO unit.vimdb/0.juju-log network info
> -
> -    In the example below:
> -    - 10.1.1.105 is the address of the application pod.
> -    - 10.152.183.199 is the service cluster ip
> +    def get_service_ip(self, endpoint):
> +        try:
> +            # TODO: replacement for network_get
> +            # info = hookenv.network_get(endpoint, hookenv.relation_id())
> +            info = ''  # BROKEN!
> +            if "ingress-addresses" in info:
> +                addr = info["ingress-addresses"][0]
> +                if len(addr):
> +                    return addr
> +            else:
> +                # TODO.log("No ingress-addresses: {}".format(info))
> +                pass
> +        except Exception as e:
> +            # TODO.log("Caught exception checking for service IP: {}".format(e))
> +            pass
>  
> -    {
> -        'bind-addresses': [{
> -            'macaddress': '',
> -            'interfacename': '',
> -            'addresses': [{
> -                'hostname': '',
> -                'address': '10.1.1.105',
> -                'cidr': ''
> -            }]
> -        }],
> -        'egress-subnets': [
> -            '10.152.183.199/32'
> -        ],
> -        'ingress-addresses': [
> -            '10.152.183.199',
> -            '10.1.1.105'
> -        ]
> -    }
> -    """
> -    try:
> -        info = hookenv.network_get(endpoint, hookenv.relation_id())
> +        return None
>  
> -        # Check to see if the pod has been assigned its internal and external ips
> -        for ingress in info["ingress-addresses"]:
> -            if len(ingress) == 0:
> -                return False
> -    except Exception:
> -        return False
> +    def is_pod_up(self, endpoint):
> +        """Check to see if the pod of a relation is up.
> +
> +        application-vimdb: 19:29:10 INFO unit.vimdb/0.juju-log network info
> +
> +        In the example below:
> +        - 10.1.1.105 is the address of the application pod.
> +        - 10.152.183.199 is the service cluster ip
> +
> +        {
> +            'bind-addresses': [{
> +                'macaddress': '',
> +                'interfacename': '',
> +                'addresses': [{
> +                    'hostname': '',
> +                    'address': '10.1.1.105',
> +                    'cidr': ''
> +                }]
> +            }],
> +            'egress-subnets': [
> +                '10.152.183.199/32'
> +            ],
> +            'ingress-addresses': [
> +                '10.152.183.199',
> +                '10.1.1.105'
> +            ]
> +        }
> +        """
> +        try:
> +            # TODO: replacement for network_get
> +            # info = hookenv.network_get(endpoint, hookenv.relation_id())
> +            info = ''
> +
> +            # Check to see if the pod has been assigned its internal and external ips
> +            for ingress in info["ingress-addresses"]:
> +                if len(ingress) == 0:
> +                    return False
> +        except Exception:
> +            return False
> +        return True
>  
> -    return True
> +    def configure_pod(self, event):
> +        if not self._started:
> +            return
> +        if not self._valid:
> +            return
> +        elif self.model.unit.is_leader():
> +            # only the leader can set_spec()
> +            spec = self.make_pod_spec()
> +            if spec is None:
> +                return  # Status already set
> +            # TODO: Figured out operator equivalent of reactive.data_changed()

See https://github.com/canonical/operator/issues/189 for some discussion on this

> +            # if reactive.data_changed("wordpress.spec", spec):
> +            if True:
> +                self.model.unit.status = MaintenanceStatus("Configuring container")
> +                try:
> +                    self.model.pod.set_spec(spec)
> +                except Exception as e:
> +                    # TODO.log("pod_spec_set failed: {}".format(e))
> +                    self.model.unit.status = BlockedStatus("pod_spec_set failed! Check logs and k8s dashboard.")
> +                    return
> +            else:
> +                # TODO.log("No changes to pod spec")
> +                pass
> +            if self.first_install():

How do we make this only gets run once? Or do we not need to?

> +                self.state._configured = True
> +                self.model.unit.status = ActiveStatus()
> +
> +
> +if __name__ == "__main__":
> +    main(Wordpress)


-- 
https://code.launchpad.net/~barryprice/charm-k8s-wordpress/+git/charm-k8s-wordpress/+merge/381231
Your team Wordpress Charmers is requested to review the proposed merge of ~barryprice/charm-k8s-wordpress/+git/charm-k8s-wordpress:operator into charm-k8s-wordpress:master.


Follow ups