wordpress-charmers team mailing list archive
-
wordpress-charmers team
-
Mailing list archive
-
Message #00103
[Merge] ~tcuthbert/charm-k8s-wordpress/+git/charm-k8s-wordpress:operator into charm-k8s-wordpress:master
Tom Haddon has proposed merging ~tcuthbert/charm-k8s-wordpress/+git/charm-k8s-wordpress:operator into charm-k8s-wordpress:master.
Commit message:
Convert to operator framework
Requested reviews:
Wordpress Charmers (wordpress-charmers)
For more details, see:
https://code.launchpad.net/~tcuthbert/charm-k8s-wordpress/+git/charm-k8s-wordpress/+merge/381502
Convert to operator framework
--
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/.gitignore b/.gitignore
new file mode 100644
index 0000000..172bf57
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1 @@
+.tox
diff --git a/.gitmodules b/.gitmodules
new file mode 100644
index 0000000..8c05fa9
--- /dev/null
+++ b/.gitmodules
@@ -0,0 +1,3 @@
+[submodule "mod/operator"]
+ path = mod/operator
+ url = https://github.com/canonical/operator
diff --git a/Makefile b/Makefile
index a46c2e2..cf18918 100644
--- a/Makefile
+++ b/Makefile
@@ -9,15 +9,8 @@ unittest:
test: lint unittest
-build: lint
- charm build
-
clean:
@echo "Cleaning files"
- @rm -rf ./.tox
- @rm -rf ./.pytest_cache
- @rm -rf ./tests/unit/__pycache__ ./reactive/__pycache__ ./lib/__pycache__
- @rm -rf ./.coverage ./.unit-state.db
-
+ @git clean -fXd
-.PHONY: lint test unittest build clean
+.PHONY: lint test unittest clean
diff --git a/hooks/start b/hooks/start
new file mode 120000
index 0000000..25b1f68
--- /dev/null
+++ b/hooks/start
@@ -0,0 +1 @@
+../src/charm.py
\ No newline at end of file
diff --git a/layer.yaml b/layer.yaml
deleted file mode 100644
index d7700f7..0000000
--- a/layer.yaml
+++ /dev/null
@@ -1,4 +0,0 @@
-includes:
- - 'layer:caas-base'
- - 'layer:status'
-repo: git+ssh://git.launchpad.net/charm-k8s-wordpress
diff --git a/lib/ops b/lib/ops
new file mode 120000
index 0000000..d934193
--- /dev/null
+++ b/lib/ops
@@ -0,0 +1 @@
+../mod/operator/ops
\ No newline at end of file
diff --git a/mod/operator b/mod/operator
new file mode 160000
index 0000000..44dff93
--- /dev/null
+++ b/mod/operator
@@ -0,0 +1 @@
+Subproject commit 44dff930667aa8e9b179c11fa87ceb8c9b85ec5a
diff --git a/reactive/wordpress.py b/reactive/wordpress.py
deleted file mode 100644
index 9a1b013..0000000
--- a/reactive/wordpress.py
+++ /dev/null
@@ -1,292 +0,0 @@
-import io
-import os
-import re
-import requests
-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")
- 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,
- }
- ]
- }
- 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.")
- 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)
- 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)
- 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
-
-
-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
- 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.
-
- 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:
- info = hookenv.network_get(endpoint, hookenv.relation_id())
-
- # 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
diff --git a/src/charm.py b/src/charm.py
new file mode 100755
index 0000000..967a0a9
--- /dev/null
+++ b/src/charm.py
@@ -0,0 +1,332 @@
+#!/usr/bin/env python3
+
+import io
+import re
+import secrets
+import subprocess
+import string
+import sys
+from urllib.parse import urlparse, urlunparse
+from yaml import safe_load
+
+sys.path.append("lib")
+
+from ops.charm import CharmBase, CharmEvents # NoQA: E402
+from ops.framework import EventBase, EventSource, StoredState # NoQA: E402
+from ops.main import main # NoQA: E402
+from ops.model import ActiveStatus, BlockedStatus, MaintenanceStatus, WaitingStatus # NoQA: E402
+
+import logging # NoQA: E402
+
+logger = logging.getLogger()
+
+
+def password_generator():
+ alphabet = string.ascii_letters + string.digits
+ return ''.join(secrets.choice(alphabet) for i in range(8))
+
+class WordpressInitialiseEvent(EventBase):
+ pass
+
+
+class WordpressCharmEvents(CharmEvents):
+ wp_initialise = EventSource(WordpressInitialiseEvent)
+
+
+class WordpressK8sCharm(CharmBase):
+ state = StoredState()
+ on = WordpressCharmEvents()
+
+ def __init__(self, *args):
+ super().__init__(*args)
+ for event in (self.on.start,
+ self.on.config_changed,
+ self.on.wp_initialise,
+ ):
+ self.framework.observe(event, self)
+
+ self.state.set_default(_init=True)
+ self.state.set_default(_started=False)
+ self.state.set_default(_valid=False)
+ self.state.set_default(_configured=False)
+
+ def on_start(self, event):
+ self.state._started = True
+
+ def on_config_changed(self, event):
+ if not self.state._valid:
+ config = self.model.config
+ want = ("image", "db_host", "db_name", "db_user", "db_password")
+ missing = [k for k in want if config[k].rstrip() == ""]
+ if missing:
+ message = " ".join(missing)
+ logger.info("Missing required config: {}".format(message))
+ self.model.unit.status = BlockedStatus("{} config is required".format(message))
+ event.defer()
+ return
+
+ self.state._valid = True
+
+ if not self.state._configured:
+ logger.info("Configuring pod")
+ return self.configure_pod()
+
+ if self.model.unit.is_leader() and self.state._init:
+ self.on.wp_initialise.emit()
+
+ def on_wp_initialise(self, event):
+ if not self.state._init:
+ return
+
+ 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.
+ self.model.unit.status = WaitingStatus("Waiting for pod to be ready")
+ event.defer()
+ return
+
+ installed = self.first_install()
+ if not installed:
+ event.defer()
+ return
+
+ logger.info("Wordpress installed and initialised")
+ self.state._init = False
+
+ def configure_pod(self):
+ # only the leader can set_spec()
+ if self.model.unit.is_leader():
+ spec = self.make_pod_spec()
+ self.model.unit.status = MaintenanceStatus("Configuring container")
+ self.model.pod.set_spec(spec)
+ self.state._configured = True
+
+ 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()]
+ ]
+
+ # 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,
+ "readinessProbe": {"exec": {"command": ["/bin/cat", "/srv/wordpress-helpers/.ready"]}},
+ }
+ ]
+ }
+
+ # 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 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
+ 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 install_ready(self):
+ ready = True
+ config = self.model.config
+ if not self.is_pod_up("website"):
+ logger.info("Pod not yet ready - retrying")
+ ready = False
+
+ try:
+ if not self.is_vhost_ready():
+ ready = False
+ except Exception as e:
+ logger.info("Wordpress vhost is not yet listening - retrying: {}".format(e))
+ ready = False
+
+ if not config["initial_settings"]:
+ logger.info("No initial_setting provided or wordpress already configured. Skipping first install.")
+ logger.info("{} {}".format(self.state._configured, config["initial_settings"]))
+ 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"]
+
+ 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:
+ r = 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 call_wordpress(self, uri, redirects=True, payload={}, _depth=1):
+ try:
+ import requests
+ except ImportError:
+ subprocess.check_call(['apt-get', 'update'])
+ subprocess.check_call(['apt-get', '-y', 'install', 'python3-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."""
+ try:
+ import requests
+ except ImportError:
+ subprocess.check_call(['apt-get', 'update'])
+ subprocess.check_call(['apt-get', '-y', 'install', 'python3-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."""
+ try:
+ import requests
+ except ImportError:
+ subprocess.check_call(['apt-get', 'update'])
+ subprocess.check_call(['apt-get', '-y', 'install', 'python3-requests'])
+ import requests
+
+ rv = True
+ # Check if we have WP code deployed at all
+ 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("Apache 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])
+ 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
+
+
+if __name__ == "__main__":
+ main(WordpressK8sCharm)
diff --git a/tests/unit/test_wordpress.py b/tests/unit/test_wordpress.py
index 5ade069..e69de29 100644
--- a/tests/unit/test_wordpress.py
+++ b/tests/unit/test_wordpress.py
@@ -1,64 +0,0 @@
-import os
-import shutil
-import sys
-import tempfile
-import unittest
-from unittest import mock
-
-# We also need to mock up charms.layer so we can run unit tests without having
-# to build the charm and pull in layers such as layer-status.
-sys.modules['charms.layer'] = mock.MagicMock()
-
-from charms.layer import status # NOQA: E402
-
-# Add path to where our reactive layer lives and import.
-sys.path.append(os.path.dirname(os.path.dirname(os.path.dirname(os.path.realpath(__file__)))))
-from reactive import wordpress # NOQA: E402
-
-
-class TestCharm(unittest.TestCase):
- def setUp(self):
- self.maxDiff = None
- self.tmpdir = tempfile.mkdtemp(prefix='charm-unittests-')
- self.addCleanup(shutil.rmtree, self.tmpdir)
-
- self.charm_dir = os.path.dirname(os.path.dirname(os.path.dirname(os.path.realpath(__file__))))
-
- patcher = mock.patch('charmhelpers.core.hookenv.log')
- self.mock_log = patcher.start()
- self.addCleanup(patcher.stop)
- self.mock_log.return_value = ''
-
- patcher = mock.patch('charmhelpers.core.hookenv.charm_dir')
- self.mock_charm_dir = patcher.start()
- self.addCleanup(patcher.stop)
- self.mock_charm_dir.return_value = self.charm_dir
-
- patcher = mock.patch('charmhelpers.core.hookenv.local_unit')
- self.mock_local_unit = patcher.start()
- self.addCleanup(patcher.stop)
- self.mock_local_unit.return_value = 'mock-wordpress/0'
-
- patcher = mock.patch('charmhelpers.core.hookenv.config')
- self.mock_config = patcher.start()
- self.addCleanup(patcher.stop)
- self.mock_config.return_value = {'blog_hostname': 'myblog.example.com'}
-
- patcher = mock.patch('charmhelpers.core.host.log')
- self.mock_log = patcher.start()
- self.addCleanup(patcher.stop)
- self.mock_log.return_value = ''
-
- status.active.reset_mock()
- status.blocked.reset_mock()
- status.maintenance.reset_mock()
-
- @mock.patch('charms.reactive.clear_flag')
- def test_hook_upgrade_charm_flags(self, clear_flag):
- '''Test correct flags set via upgrade-charm hook'''
- wordpress.upgrade_charm()
- self.assertFalse(status.maintenance.assert_called())
- want = [
- mock.call('wordpress.configured'),
- ]
- self.assertFalse(clear_flag.assert_has_calls(want, any_order=True))
diff --git a/tox.ini b/tox.ini
index 7b45934..3f4c39f 100644
--- a/tox.ini
+++ b/tox.ini
@@ -10,7 +10,7 @@ setenv =
[testenv:unit]
commands =
- pytest --ignore {toxinidir}/tests/functional \
+ pytest --ignore mod --ignore {toxinidir}/tests/functional \
{posargs:-v --cov=reactive --cov-report=term-missing --cov-branch}
deps = -r{toxinidir}/tests/unit/requirements.txt
-r{toxinidir}/requirements.txt
@@ -24,16 +24,16 @@ passenv =
JUJU_REPOSITORY
PATH
commands =
- pytest -v --ignore {toxinidir}/tests/unit {posargs}
+ 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 .
+commands = black --skip-string-normalization --line-length=120 src/ tests/
deps = black
[testenv:lint]
-commands = flake8
+commands = flake8 src/ tests/
deps = flake8
[flake8]
diff --git a/wheelhouse.txt b/wheelhouse.txt
deleted file mode 100644
index f229360..0000000
--- a/wheelhouse.txt
+++ /dev/null
@@ -1 +0,0 @@
-requests
Follow ups
-
[Merge] ~tcuthbert/charm-k8s-wordpress/+git/charm-k8s-wordpress:operator into charm-k8s-wordpress:master
From: noreply, 2020-04-06
-
Re: [Merge] ~tcuthbert/charm-k8s-wordpress/+git/charm-k8s-wordpress:operator into charm-k8s-wordpress:master
From: Canonical IS Mergebot, 2020-04-06
-
[Merge] ~tcuthbert/charm-k8s-wordpress/+git/charm-k8s-wordpress:operator into charm-k8s-wordpress:master
From: Thomas Cuthbert, 2020-04-06
-
Re: [Merge] ~tcuthbert/charm-k8s-wordpress/+git/charm-k8s-wordpress:operator into charm-k8s-wordpress:master
From: Stuart Bishop, 2020-04-06
-
Re: [Merge] ~tcuthbert/charm-k8s-wordpress/+git/charm-k8s-wordpress:operator into charm-k8s-wordpress:master
From: Tom Haddon, 2020-04-06
-
Re: [Merge] ~tcuthbert/charm-k8s-wordpress/+git/charm-k8s-wordpress:operator into charm-k8s-wordpress:master
From: Stuart Bishop, 2020-04-06
-
Re: [Merge] ~tcuthbert/charm-k8s-wordpress/+git/charm-k8s-wordpress:operator into charm-k8s-wordpress:master
From: Stuart Bishop, 2020-04-06
-
Re: [Merge] ~tcuthbert/charm-k8s-wordpress/+git/charm-k8s-wordpress:operator into charm-k8s-wordpress:master
From: Thomas Cuthbert, 2020-04-04
-
Re: [Merge] ~tcuthbert/charm-k8s-wordpress/+git/charm-k8s-wordpress:operator into charm-k8s-wordpress:master
From: Tom Haddon, 2020-04-03
-
Re: [Merge] ~tcuthbert/charm-k8s-wordpress/+git/charm-k8s-wordpress:operator into charm-k8s-wordpress:master
From: Canonical IS Mergebot, 2020-04-03