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

Proposed by Barry Price
Status: Merged
Merged at revision: 5d73401594539e7abca5cfebbd38a1e9e2883c4a
Proposed branch: ~barryprice/charm-k8s-wordpress/+git/charm-k8s-wordpress:operator
Merge into: charm-k8s-wordpress:master
Diff against target: 778 lines (+307/-78)
10 files modified
.gitignore (+1/-0)
.gitmodules (+3/-0)
Makefile (+2/-9)
dev/null (+0/-1)
hooks/start (+1/-0)
lib/ops (+1/-0)
mod/operator (+1/-0)
src/charm.py (+294/-0)
tests/unit/test_wordpress.py (+0/-64)
tox.ini (+4/-4)
Reviewer Review Type Date Requested Status
Wordpress Charmers Pending
Review via email: mp+381231@code.launchpad.net

Commit message

Porting to Operator (WIP)

To post a comment you must log in.
Revision history for this message
Tom Haddon (mthaddon) wrote :

Some comments inline

Revision history for this message
Barry Price (barryprice) wrote :

Thanks, left some responses and will push fixes for these as well as others.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
diff --git a/.gitignore b/.gitignore
0new file mode 1006440new file mode 100644
index 0000000..172bf57
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1 @@
1.tox
diff --git a/.gitmodules b/.gitmodules
0new file mode 1006442new file mode 100644
index 0000000..8c05fa9
--- /dev/null
+++ b/.gitmodules
@@ -0,0 +1,3 @@
1[submodule "mod/operator"]
2 path = mod/operator
3 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:
99
10test: lint unittest10test: lint unittest
1111
12build: lint
13 charm build
14
15clean:12clean:
16 @echo "Cleaning files"13 @echo "Cleaning files"
17 @rm -rf ./.tox14 @git clean -fXd
18 @rm -rf ./.pytest_cache
19 @rm -rf ./tests/unit/__pycache__ ./reactive/__pycache__ ./lib/__pycache__
20 @rm -rf ./.coverage ./.unit-state.db
21
2215
23.PHONY: lint test unittest build clean16.PHONY: lint test unittest clean
diff --git a/hooks/start b/hooks/start
24new file mode 12000017new file mode 120000
index 0000000..25b1f68
--- /dev/null
+++ b/hooks/start
@@ -0,0 +1 @@
1../src/charm.py
0\ No newline at end of file2\ No newline at end of file
diff --git a/layer.yaml b/layer.yaml
1deleted file mode 1006443deleted file mode 100644
index d7700f7..0000000
--- a/layer.yaml
+++ /dev/null
@@ -1,4 +0,0 @@
1includes:
2 - 'layer:caas-base'
3 - 'layer:status'
4repo: git+ssh://git.launchpad.net/charm-k8s-wordpress
diff --git a/lib/ops b/lib/ops
5new file mode 1200000new file mode 120000
index 0000000..d934193
--- /dev/null
+++ b/lib/ops
@@ -0,0 +1 @@
1../mod/operator/ops
0\ No newline at end of file2\ No newline at end of file
diff --git a/mod/operator b/mod/operator
1new file mode 1600003new file mode 160000
index 0000000..44dff93
--- /dev/null
+++ b/mod/operator
@@ -0,0 +1 @@
1Subproject commit 44dff930667aa8e9b179c11fa87ceb8c9b85ec5a
diff --git a/reactive/wordpress.py b/reactive/wordpress.py
0deleted file mode 1006442deleted file mode 100644
index 9a1b013..0000000
--- a/reactive/wordpress.py
+++ /dev/null
@@ -1,292 +0,0 @@
1import io
2import os
3import re
4import requests
5from pprint import pprint
6from urllib.parse import urlparse, urlunparse
7from yaml import safe_load
8
9from charmhelpers.core import host, hookenv
10from charms import reactive
11from charms.layer import caas_base, status
12from charms.reactive import hook, when, when_not
13
14
15@hook("upgrade-charm")
16def upgrade_charm():
17 status.maintenance("Upgrading charm")
18 reactive.clear_flag("wordpress.configured")
19
20
21@when("config.changed")
22def reconfig():
23 status.maintenance("charm configuration changed")
24 reactive.clear_flag("wordpress.configured")
25
26 # Validate config
27 valid = True
28 config = hookenv.config()
29 # Ensure required strings
30 for k in ["image", "db_host", "db_name", "db_user", "db_password"]:
31 if config[k].strip() == "":
32 status.blocked("{!r} config is required".format(k))
33 valid = False
34
35 reactive.toggle_flag("wordpress.config.valid", valid)
36
37
38@when("wordpress.config.valid")
39@when_not("wordpress.configured")
40def deploy_container():
41 spec = make_pod_spec()
42 if spec is None:
43 return # Status already set
44 if reactive.data_changed("wordpress.spec", spec):
45 status.maintenance("configuring container")
46 try:
47 caas_base.pod_spec_set(spec)
48 except Exception as e:
49 hookenv.log("pod_spec_set failed: {}".format(e), hookenv.DEBUG)
50 status.blocked("pod_spec_set failed! Check logs and k8s dashboard.")
51 return
52 else:
53 hookenv.log("No changes to pod spec")
54 if first_install():
55 reactive.set_flag("wordpress.configured")
56
57
58@when("wordpress.configured")
59def ready():
60 status.active("Ready")
61
62
63def sanitized_container_config():
64 """Container config without secrets"""
65 config = hookenv.config()
66 if config["container_config"].strip() == "":
67 container_config = {}
68 else:
69 container_config = safe_load(config["container_config"])
70 if not isinstance(container_config, dict):
71 status.blocked("container_config is not a YAML mapping")
72 return None
73 container_config["WORDPRESS_DB_HOST"] = config["db_host"]
74 container_config["WORDPRESS_DB_NAME"] = config["db_name"]
75 container_config["WORDPRESS_DB_USER"] = config["db_user"]
76 if config.get("wp_plugin_openid_team_map"):
77 container_config["WP_PLUGIN_OPENID_TEAM_MAP"] = config["wp_plugin_openid_team_map"]
78 return container_config
79
80
81def full_container_config():
82 """Container config with secrets"""
83 config = hookenv.config()
84 container_config = sanitized_container_config()
85 if container_config is None:
86 return None
87 if config["container_secrets"].strip() == "":
88 container_secrets = {}
89 else:
90 container_secrets = safe_load(config["container_secrets"])
91 if not isinstance(container_secrets, dict):
92 status.blocked("container_secrets is not a YAML mapping")
93 return None
94 container_config.update(container_secrets)
95 # Add secrets from charm config
96 container_config["WORDPRESS_DB_PASSWORD"] = config["db_password"]
97 if config.get("wp_plugin_akismet_key"):
98 container_config["WP_PLUGIN_AKISMET_KEY"] = config["wp_plugin_akismet_key"]
99 return container_config
100
101
102def make_pod_spec():
103 config = hookenv.config()
104 container_config = sanitized_container_config()
105 if container_config is None:
106 return # Status already set
107
108 ports = [
109 {"name": name, "containerPort": int(port), "protocol": "TCP"}
110 for name, port in [addr.split(":", 1) for addr in config["ports"].split()]
111 ]
112
113 # PodSpec v1? https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.13/#podspec-v1-core
114 spec = {
115 "containers": [
116 {
117 "name": hookenv.charm_name(),
118 "imageDetails": {"imagePath": config["image"]},
119 "ports": ports,
120 "config": container_config,
121 }
122 ]
123 }
124 out = io.StringIO()
125 pprint(spec, out)
126 hookenv.log("Container environment config (sans secrets) <<EOM\n{}\nEOM".format(out.getvalue()))
127
128 # If we need credentials (secrets) for our image, add them to the spec after logging
129 if config.get("image_user") and config.get("image_pass"):
130 spec.get("containers")[0].get("imageDetails")["username"] = config["image_user"]
131 spec.get("containers")[0].get("imageDetails")["password"] = config["image_pass"]
132
133 config_with_secrets = full_container_config()
134 if config_with_secrets is None:
135 return None # Status already set
136 container_config.update(config_with_secrets)
137
138 return spec
139
140
141def first_install():
142 """Perform initial configuration of wordpress if needed."""
143 config = hookenv.config()
144 if not is_pod_up("website"):
145 hookenv.log("Pod not yet ready - retrying")
146 return False
147 elif not is_vhost_ready():
148 hookenv.log("Wordpress vhost is not yet listening - retrying")
149 return False
150 elif wordpress_configured() or not config["initial_settings"]:
151 hookenv.log("No initial_setting provided or wordpress already configured. Skipping first install.")
152 return True
153 hookenv.log("Starting wordpress initial configuration")
154 payload = {
155 "admin_password": host.pwgen(24),
156 "blog_public": "checked",
157 "Submit": "submit",
158 }
159 payload.update(safe_load(config["initial_settings"]))
160 payload["admin_password2"] = payload["admin_password"]
161 if not payload["blog_public"]:
162 payload["blog_public"] = "unchecked"
163 required_config = set(("user_name", "admin_email"))
164 missing = required_config.difference(payload.keys())
165 if missing:
166 hookenv.log("Error: missing wordpress settings: {}".format(missing))
167 return False
168 call_wordpress("/wp-admin/install.php?step=2", redirects=True, payload=payload)
169 host.write_file(os.path.join("/root/", "initial.passwd"), payload["admin_password"], perms=0o400)
170 return True
171
172
173def call_wordpress(uri, redirects=True, payload={}, _depth=1):
174 max_depth = 10
175 if _depth > max_depth:
176 hookenv.log("Redirect loop detected in call_worpress()")
177 raise RuntimeError("Redirect loop detected in call_worpress()")
178 config = hookenv.config()
179 service_ip = get_service_ip("website")
180 if service_ip:
181 headers = {"Host": config["blog_hostname"]}
182 url = urlunparse(("http", service_ip, uri, "", "", ""))
183 if payload:
184 r = requests.post(url, allow_redirects=False, headers=headers, data=payload, timeout=30)
185 else:
186 r = requests.get(url, allow_redirects=False, headers=headers, timeout=30)
187 if redirects and r.is_redirect:
188 # Recurse, but strip the scheme and host first, we need to connect over HTTP by bare IP
189 o = urlparse(r.headers.get("Location"))
190 return call_wordpress(o.path, redirects=redirects, payload=payload, _depth=_depth + 1)
191 else:
192 return r
193 else:
194 hookenv.log("Error getting service IP")
195 return False
196
197
198def wordpress_configured():
199 """Check whether first install has been completed."""
200 # Check whether pod is deployed
201 if not is_pod_up("website"):
202 return False
203 # Check if we have WP code deployed at all
204 if not is_vhost_ready():
205 return False
206 # We have code on disk, check if configured
207 try:
208 r = call_wordpress("/", redirects=False)
209 except requests.exceptions.ConnectionError:
210 return False
211 if r.status_code == 302 and re.match("^.*/wp-admin/install.php", r.headers.get("location", "")):
212 return False
213 elif r.status_code == 302 and re.match("^.*/wp-admin/setup-config.php", r.headers.get("location", "")):
214 hookenv.log("MySQL database setup failed, we likely have no wp-config.php")
215 status.blocked("MySQL database setup failed, we likely have no wp-config.php")
216 return False
217 else:
218 return True
219
220
221def is_vhost_ready():
222 """Check whether wordpress is available using http."""
223 # Check if we have WP code deployed at all
224 try:
225 r = call_wordpress("/wp-login.php", redirects=False)
226 except requests.exceptions.ConnectionError:
227 hookenv.log("call_wordpress() returned requests.exceptions.ConnectionError")
228 return False
229 if r is None:
230 hookenv.log("call_wordpress() returned None")
231 return False
232 if hasattr(r, "status_code") and r.status_code in (403, 404):
233 hookenv.log("call_wordpress() returned status {}".format(r.status_code))
234 return False
235 else:
236 return True
237
238
239def get_service_ip(endpoint):
240 try:
241 info = hookenv.network_get(endpoint, hookenv.relation_id())
242 if "ingress-addresses" in info:
243 addr = info["ingress-addresses"][0]
244 if len(addr):
245 return addr
246 else:
247 hookenv.log("No ingress-addresses: {}".format(info))
248 except Exception as e:
249 hookenv.log("Caught exception checking for service IP: {}".format(e))
250
251 return None
252
253
254def is_pod_up(endpoint):
255 """Check to see if the pod of a relation is up.
256
257 application-vimdb: 19:29:10 INFO unit.vimdb/0.juju-log network info
258
259 In the example below:
260 - 10.1.1.105 is the address of the application pod.
261 - 10.152.183.199 is the service cluster ip
262
263 {
264 'bind-addresses': [{
265 'macaddress': '',
266 'interfacename': '',
267 'addresses': [{
268 'hostname': '',
269 'address': '10.1.1.105',
270 'cidr': ''
271 }]
272 }],
273 'egress-subnets': [
274 '10.152.183.199/32'
275 ],
276 'ingress-addresses': [
277 '10.152.183.199',
278 '10.1.1.105'
279 ]
280 }
281 """
282 try:
283 info = hookenv.network_get(endpoint, hookenv.relation_id())
284
285 # Check to see if the pod has been assigned its internal and external ips
286 for ingress in info["ingress-addresses"]:
287 if len(ingress) == 0:
288 return False
289 except Exception:
290 return False
291
292 return True
diff --git a/src/charm.py b/src/charm.py
293new file mode 1007550new file mode 100755
index 0000000..a19bc28
--- /dev/null
+++ b/src/charm.py
@@ -0,0 +1,294 @@
1#!/usr/bin/env python3
2
3import io
4import re
5import subprocess
6import sys
7from pprint import pprint
8from time import sleep
9from urllib.parse import urlparse, urlunparse
10from yaml import safe_load
11
12sys.path.append("lib")
13
14from ops.charm import CharmBase # NoQA: E402
15from ops.framework import StoredState # NoQA: E402
16from ops.main import main # NoQA: E402
17from ops.model import ActiveStatus, BlockedStatus, MaintenanceStatus # NoQA: E402
18
19import logging # NoQA: E402
20
21logger = logging.getLogger()
22
23
24class Wordpress(CharmBase):
25 state = StoredState()
26
27 def __init__(self, *args):
28 super().__init__(*args)
29 for event in (
30 self.on.start,
31 self.on.config_changed,
32 ):
33 self.framework.observe(event, self)
34
35 def on_start(self, event):
36 logger.info("Here we go...")
37 # There may be a nicer way to do this!
38 # https://github.com/canonical/operator/issues/156
39 subprocess.check_call(['apt-get', 'update'])
40 subprocess.check_call(['apt-get', '-y', 'install', 'python3-requests'])
41 # Initialise states
42 self.state._started = True
43 self.state._configured = False
44 self.state._valid = False
45
46 def on_config_changed(self, event):
47 valid = True
48 config = self.model.config
49 for k in ["image", "db_host", "db_name", "db_user", "db_password"]:
50 if config[k].strip() == "":
51 self.model.unit.status = BlockedStatus("{!r} config is required".format(k))
52 valid = False
53 self.state._valid = valid
54 self.configure_pod(event)
55
56 def sanitized_container_config(self):
57 """Container config without secrets"""
58 config = self.model.config
59 if config["container_config"].strip() == "":
60 container_config = {}
61 else:
62 container_config = safe_load(config["container_config"])
63 if not isinstance(container_config, dict):
64 self.model.unit.status = BlockedStatus("container_config is not a YAML mapping")
65 return None
66 container_config["WORDPRESS_DB_HOST"] = config["db_host"]
67 container_config["WORDPRESS_DB_NAME"] = config["db_name"]
68 container_config["WORDPRESS_DB_USER"] = config["db_user"]
69 if config.get("wp_plugin_openid_team_map"):
70 container_config["WP_PLUGIN_OPENID_TEAM_MAP"] = config["wp_plugin_openid_team_map"]
71 return container_config
72
73 def full_container_config(self):
74 """Container config with secrets"""
75 config = self.model.config
76 container_config = self.sanitized_container_config()
77 if container_config is None:
78 return None
79 if config["container_secrets"].strip() == "":
80 container_secrets = {}
81 else:
82 container_secrets = safe_load(config["container_secrets"])
83 if not isinstance(container_secrets, dict):
84 self.model.unit.status = BlockedStatus("container_secrets is not a YAML mapping")
85 return None
86 container_config.update(container_secrets)
87 # Add secrets from charm config
88 container_config["WORDPRESS_DB_PASSWORD"] = config["db_password"]
89 if config.get("wp_plugin_akismet_key"):
90 container_config["WP_PLUGIN_AKISMET_KEY"] = config["wp_plugin_akismet_key"]
91 return container_config
92
93 def make_pod_spec(self):
94 config = self.model.config
95 container_config = self.sanitized_container_config()
96 if container_config is None:
97 return # Status already set
98
99 ports = [
100 {"name": name, "containerPort": int(port), "protocol": "TCP"}
101 for name, port in [addr.split(":", 1) for addr in config["ports"].split()]
102 ]
103
104 # PodSpec v1? https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.13/#podspec-v1-core
105 spec = {
106 "containers": [
107 {
108 "name": self.app.name,
109 "imageDetails": {"imagePath": config["image"]},
110 "ports": ports,
111 "config": container_config,
112 "readinessProbe": {"exec": {"command": ["cat", "/srv/wordpress-helpers/.ready"]}},
113 }
114 ]
115 }
116 out = io.StringIO()
117 pprint(spec, out)
118 logger.info("Container environment config (sans secrets) <<EOM\n{}\nEOM".format(out.getvalue()))
119
120 # If we need credentials (secrets) for our image, add them to the spec after logging
121 if config.get("image_user") and config.get("image_pass"):
122 spec.get("containers")[0].get("imageDetails")["username"] = config["image_user"]
123 spec.get("containers")[0].get("imageDetails")["password"] = config["image_pass"]
124
125 config_with_secrets = self.full_container_config()
126 if config_with_secrets is None:
127 return None # Status already set
128 container_config.update(config_with_secrets)
129
130 return spec
131
132 def first_install(self):
133 """Perform initial configuration of wordpress if needed."""
134 config = self.model.config
135 if not self.is_pod_up("website"):
136 logger.info("Pod not yet ready - retrying")
137 return False
138 elif not self.is_vhost_ready():
139 logger.info("Wordpress vhost is not yet listening - retrying")
140 return False
141 elif self.state._configured or not config["initial_settings"]:
142 logger.info("No initial_setting provided or wordpress already configured. Skipping first install.")
143 return True
144 logger.info("Starting wordpress initial configuration")
145 payload = {
146 # "admin_password": host.pwgen(24), ## TODO: pwgen
147 "admin_password": "letmein123",
148 "blog_public": "checked",
149 "Submit": "submit",
150 }
151 payload.update(safe_load(config["initial_settings"]))
152 payload["admin_password2"] = payload["admin_password"]
153 if not payload["blog_public"]:
154 payload["blog_public"] = "unchecked"
155 required_config = set(("user_name", "admin_email"))
156 missing = required_config.difference(payload.keys())
157 if missing:
158 logger.info("Error: missing wordpress settings: {}".format(missing))
159 return False
160 self.call_wordpress("/wp-admin/install.php?step=2", redirects=True, payload=payload)
161 # TODO: write_file
162 # host.write_file(os.path.join("/root/", "initial.passwd"), payload["admin_password"], perms=0o400)
163 return True
164
165 def call_wordpress(self, uri, redirects=True, payload={}, _depth=1):
166 import requests
167
168 max_depth = 10
169 if _depth > max_depth:
170 logger.info("Redirect loop detected in call_worpress()")
171 raise RuntimeError("Redirect loop detected in call_worpress()")
172 config = self.model.config
173 service_ip = self.get_service_ip("website")
174 if service_ip:
175 headers = {"Host": config["blog_hostname"]}
176 url = urlunparse(("http", service_ip, uri, "", "", ""))
177 if payload:
178 r = requests.post(url, allow_redirects=False, headers=headers, data=payload, timeout=30)
179 else:
180 r = requests.get(url, allow_redirects=False, headers=headers, timeout=30)
181 if redirects and r.is_redirect:
182 # Recurse, but strip the scheme and host first, we need to connect over HTTP by bare IP
183 o = urlparse(r.headers.get("Location"))
184 return self.call_wordpress(o.path, redirects=redirects, payload=payload, _depth=_depth + 1)
185 else:
186 return r
187 else:
188 logger.info("Error getting service IP")
189 return False
190
191 def wordpress_configured(self):
192 """Check whether first install has been completed."""
193 import requests
194
195 # Check whether pod is deployed
196 if not self.is_pod_up("website"):
197 return False
198 # Check if we have WP code deployed at all
199 if not self.is_vhost_ready():
200 return False
201 # We have code on disk, check if configured
202 try:
203 r = self.call_wordpress("/", redirects=False)
204 except requests.exceptions.ConnectionError:
205 return False
206 if r.status_code == 302 and re.match("^.*/wp-admin/install.php", r.headers.get("location", "")):
207 return False
208 elif r.status_code == 302 and re.match("^.*/wp-admin/setup-config.php", r.headers.get("location", "")):
209 logger.info("MySQL database setup failed, we likely have no wp-config.php")
210 self.model.unit.status = BlockedStatus("MySQL database setup failed, we likely have no wp-config.php")
211 return False
212 else:
213 return True
214
215 def is_vhost_ready(self):
216 """Check whether wordpress is available using http."""
217 import requests
218
219 # Check if we have WP code deployed at all
220 try:
221 r = self.call_wordpress("/wp-login.php", redirects=False)
222 except requests.exceptions.ConnectionError:
223 logger.info("call_wordpress() returned requests.exceptions.ConnectionError")
224 return False
225 if r is None:
226 logger.info("call_wordpress() returned None")
227 return False
228 if hasattr(r, "status_code") and r.status_code in (403, 404):
229 logger.info("call_wordpress() returned status {}".format(r.status_code))
230 return False
231 else:
232 return True
233
234 def get_service_ip(self, endpoint):
235 try:
236 self.model.get_binding(endpoint).network is not False
237 except Exception:
238 logger.info("We don't have networking yet")
239 return ''
240 try:
241 return str(self.model.get_binding(endpoint).network.ingress_addresses[0])
242 except Exception:
243 logger.info("We don't have any ingress addresses yet")
244 return ''
245
246 def is_pod_up(self, endpoint):
247 """Check to see if the pod of a relation is up"""
248 ingress = self.get_service_ip(endpoint)
249 if len(ingress) == 0:
250 return False
251 return True
252
253 def configure_pod(self, event): # NoQA: C901
254 if not self.state._started:
255 return
256 if not self.state._valid:
257 return
258 elif self.model.unit.is_leader():
259 # only the leader can set_spec()
260 spec = self.make_pod_spec()
261 if spec is None:
262 return # Status already set
263 # TODO: Figure out operator equivalent of reactive.data_changed()
264 # https://github.com/canonical/operator/issues/189
265 # if reactive.data_changed("wordpress.spec", spec):
266 # "if True" works around it, but we'll respwan on every hook run,
267 # which is decidedly sub-optimal
268 if True:
269 self.model.unit.status = MaintenanceStatus("Configuring container")
270 try:
271 self.model.pod.set_spec(spec)
272 except Exception as e:
273 logger.info("pod_spec_set failed: {}".format(e))
274 self.model.unit.status = BlockedStatus("pod_spec_set failed! Check logs and k8s dashboard.")
275 return
276 else:
277 logger.info("No changes to pod spec")
278 if not self.state._configured:
279 tries = 0
280 max_tries = 20
281 while tries < max_tries:
282 if self.first_install():
283 self.state._configured = True
284 self.model.unit.status = ActiveStatus()
285 else:
286 tries = tries + 1
287 logger.info("Sleeping 30s after attempt {}/{}, will try again".format(tries, max_tries))
288 sleep(30)
289 logger.error("Giving up on configuration after attempt {}/{}".format(tries, max_tries))
290 self.model.unit.status = BlockedStatus("first_install failed! Check logs")
291
292
293if __name__ == "__main__":
294 main(Wordpress)
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 @@
1import os
2import shutil
3import sys
4import tempfile
5import unittest
6from unittest import mock
7
8# We also need to mock up charms.layer so we can run unit tests without having
9# to build the charm and pull in layers such as layer-status.
10sys.modules['charms.layer'] = mock.MagicMock()
11
12from charms.layer import status # NOQA: E402
13
14# Add path to where our reactive layer lives and import.
15sys.path.append(os.path.dirname(os.path.dirname(os.path.dirname(os.path.realpath(__file__)))))
16from reactive import wordpress # NOQA: E402
17
18
19class TestCharm(unittest.TestCase):
20 def setUp(self):
21 self.maxDiff = None
22 self.tmpdir = tempfile.mkdtemp(prefix='charm-unittests-')
23 self.addCleanup(shutil.rmtree, self.tmpdir)
24
25 self.charm_dir = os.path.dirname(os.path.dirname(os.path.dirname(os.path.realpath(__file__))))
26
27 patcher = mock.patch('charmhelpers.core.hookenv.log')
28 self.mock_log = patcher.start()
29 self.addCleanup(patcher.stop)
30 self.mock_log.return_value = ''
31
32 patcher = mock.patch('charmhelpers.core.hookenv.charm_dir')
33 self.mock_charm_dir = patcher.start()
34 self.addCleanup(patcher.stop)
35 self.mock_charm_dir.return_value = self.charm_dir
36
37 patcher = mock.patch('charmhelpers.core.hookenv.local_unit')
38 self.mock_local_unit = patcher.start()
39 self.addCleanup(patcher.stop)
40 self.mock_local_unit.return_value = 'mock-wordpress/0'
41
42 patcher = mock.patch('charmhelpers.core.hookenv.config')
43 self.mock_config = patcher.start()
44 self.addCleanup(patcher.stop)
45 self.mock_config.return_value = {'blog_hostname': 'myblog.example.com'}
46
47 patcher = mock.patch('charmhelpers.core.host.log')
48 self.mock_log = patcher.start()
49 self.addCleanup(patcher.stop)
50 self.mock_log.return_value = ''
51
52 status.active.reset_mock()
53 status.blocked.reset_mock()
54 status.maintenance.reset_mock()
55
56 @mock.patch('charms.reactive.clear_flag')
57 def test_hook_upgrade_charm_flags(self, clear_flag):
58 '''Test correct flags set via upgrade-charm hook'''
59 wordpress.upgrade_charm()
60 self.assertFalse(status.maintenance.assert_called())
61 want = [
62 mock.call('wordpress.configured'),
63 ]
64 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 =
1010
11[testenv:unit]11[testenv:unit]
12commands =12commands =
13 pytest --ignore {toxinidir}/tests/functional \13 pytest --ignore mod --ignore {toxinidir}/tests/functional \
14 {posargs:-v --cov=reactive --cov-report=term-missing --cov-branch}14 {posargs:-v --cov=reactive --cov-report=term-missing --cov-branch}
15deps = -r{toxinidir}/tests/unit/requirements.txt15deps = -r{toxinidir}/tests/unit/requirements.txt
16 -r{toxinidir}/requirements.txt16 -r{toxinidir}/requirements.txt
@@ -24,16 +24,16 @@ passenv =
24 JUJU_REPOSITORY24 JUJU_REPOSITORY
25 PATH25 PATH
26commands =26commands =
27 pytest -v --ignore {toxinidir}/tests/unit {posargs}27 pytest -v --ignore mod --ignore {toxinidir}/tests/unit {posargs}
28deps = -r{toxinidir}/tests/functional/requirements.txt28deps = -r{toxinidir}/tests/functional/requirements.txt
29 -r{toxinidir}/requirements.txt29 -r{toxinidir}/requirements.txt
3030
31[testenv:black]31[testenv:black]
32commands = black --skip-string-normalization --line-length=120 .32commands = black --skip-string-normalization --line-length=120 src/ tests/
33deps = black33deps = black
3434
35[testenv:lint]35[testenv:lint]
36commands = flake836commands = flake8 src/ tests/
37deps = flake837deps = flake8
3838
39[flake8]39[flake8]
diff --git a/wheelhouse.txt b/wheelhouse.txt
40deleted file mode 10064440deleted file mode 100644
index f229360..0000000
--- a/wheelhouse.txt
+++ /dev/null
@@ -1 +0,0 @@
1requests

Subscribers

People subscribed via source and target branches