Merge ~barryprice/charm-k8s-wordpress/+git/charm-k8s-wordpress:operator into charm-k8s-wordpress:master
- Git
- lp:~barryprice/charm-k8s-wordpress/+git/charm-k8s-wordpress
- operator
- Merge into 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) |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Wordpress Charmers | Pending | ||
Review via email: mp+381231@code.launchpad.net |
Commit message
Porting to Operator (WIP)
Description of the change
To post a comment you must log in.
Revision history for this message
Tom Haddon (mthaddon) wrote : | # |
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
1 | diff --git a/.gitignore b/.gitignore |
2 | new file mode 100644 |
3 | index 0000000..172bf57 |
4 | --- /dev/null |
5 | +++ b/.gitignore |
6 | @@ -0,0 +1 @@ |
7 | +.tox |
8 | diff --git a/.gitmodules b/.gitmodules |
9 | new file mode 100644 |
10 | index 0000000..8c05fa9 |
11 | --- /dev/null |
12 | +++ b/.gitmodules |
13 | @@ -0,0 +1,3 @@ |
14 | +[submodule "mod/operator"] |
15 | + path = mod/operator |
16 | + url = https://github.com/canonical/operator |
17 | diff --git a/Makefile b/Makefile |
18 | index a46c2e2..cf18918 100644 |
19 | --- a/Makefile |
20 | +++ b/Makefile |
21 | @@ -9,15 +9,8 @@ unittest: |
22 | |
23 | test: lint unittest |
24 | |
25 | -build: lint |
26 | - charm build |
27 | - |
28 | clean: |
29 | @echo "Cleaning files" |
30 | - @rm -rf ./.tox |
31 | - @rm -rf ./.pytest_cache |
32 | - @rm -rf ./tests/unit/__pycache__ ./reactive/__pycache__ ./lib/__pycache__ |
33 | - @rm -rf ./.coverage ./.unit-state.db |
34 | - |
35 | + @git clean -fXd |
36 | |
37 | -.PHONY: lint test unittest build clean |
38 | +.PHONY: lint test unittest clean |
39 | diff --git a/hooks/start b/hooks/start |
40 | new file mode 120000 |
41 | index 0000000..25b1f68 |
42 | --- /dev/null |
43 | +++ b/hooks/start |
44 | @@ -0,0 +1 @@ |
45 | +../src/charm.py |
46 | \ No newline at end of file |
47 | diff --git a/layer.yaml b/layer.yaml |
48 | deleted file mode 100644 |
49 | index d7700f7..0000000 |
50 | --- a/layer.yaml |
51 | +++ /dev/null |
52 | @@ -1,4 +0,0 @@ |
53 | -includes: |
54 | - - 'layer:caas-base' |
55 | - - 'layer:status' |
56 | -repo: git+ssh://git.launchpad.net/charm-k8s-wordpress |
57 | diff --git a/lib/ops b/lib/ops |
58 | new file mode 120000 |
59 | index 0000000..d934193 |
60 | --- /dev/null |
61 | +++ b/lib/ops |
62 | @@ -0,0 +1 @@ |
63 | +../mod/operator/ops |
64 | \ No newline at end of file |
65 | diff --git a/mod/operator b/mod/operator |
66 | new file mode 160000 |
67 | index 0000000..44dff93 |
68 | --- /dev/null |
69 | +++ b/mod/operator |
70 | @@ -0,0 +1 @@ |
71 | +Subproject commit 44dff930667aa8e9b179c11fa87ceb8c9b85ec5a |
72 | diff --git a/reactive/wordpress.py b/reactive/wordpress.py |
73 | deleted file mode 100644 |
74 | index 9a1b013..0000000 |
75 | --- a/reactive/wordpress.py |
76 | +++ /dev/null |
77 | @@ -1,292 +0,0 @@ |
78 | -import io |
79 | -import os |
80 | -import re |
81 | -import requests |
82 | -from pprint import pprint |
83 | -from urllib.parse import urlparse, urlunparse |
84 | -from yaml import safe_load |
85 | - |
86 | -from charmhelpers.core import host, hookenv |
87 | -from charms import reactive |
88 | -from charms.layer import caas_base, status |
89 | -from charms.reactive import hook, when, when_not |
90 | - |
91 | - |
92 | -@hook("upgrade-charm") |
93 | -def upgrade_charm(): |
94 | - status.maintenance("Upgrading charm") |
95 | - reactive.clear_flag("wordpress.configured") |
96 | - |
97 | - |
98 | -@when("config.changed") |
99 | -def reconfig(): |
100 | - status.maintenance("charm configuration changed") |
101 | - reactive.clear_flag("wordpress.configured") |
102 | - |
103 | - # Validate config |
104 | - valid = True |
105 | - config = hookenv.config() |
106 | - # Ensure required strings |
107 | - for k in ["image", "db_host", "db_name", "db_user", "db_password"]: |
108 | - if config[k].strip() == "": |
109 | - status.blocked("{!r} config is required".format(k)) |
110 | - valid = False |
111 | - |
112 | - reactive.toggle_flag("wordpress.config.valid", valid) |
113 | - |
114 | - |
115 | -@when("wordpress.config.valid") |
116 | -@when_not("wordpress.configured") |
117 | -def deploy_container(): |
118 | - spec = make_pod_spec() |
119 | - if spec is None: |
120 | - return # Status already set |
121 | - if reactive.data_changed("wordpress.spec", spec): |
122 | - status.maintenance("configuring container") |
123 | - try: |
124 | - caas_base.pod_spec_set(spec) |
125 | - except Exception as e: |
126 | - hookenv.log("pod_spec_set failed: {}".format(e), hookenv.DEBUG) |
127 | - status.blocked("pod_spec_set failed! Check logs and k8s dashboard.") |
128 | - return |
129 | - else: |
130 | - hookenv.log("No changes to pod spec") |
131 | - if first_install(): |
132 | - reactive.set_flag("wordpress.configured") |
133 | - |
134 | - |
135 | -@when("wordpress.configured") |
136 | -def ready(): |
137 | - status.active("Ready") |
138 | - |
139 | - |
140 | -def sanitized_container_config(): |
141 | - """Container config without secrets""" |
142 | - config = hookenv.config() |
143 | - if config["container_config"].strip() == "": |
144 | - container_config = {} |
145 | - else: |
146 | - container_config = safe_load(config["container_config"]) |
147 | - if not isinstance(container_config, dict): |
148 | - status.blocked("container_config is not a YAML mapping") |
149 | - return None |
150 | - container_config["WORDPRESS_DB_HOST"] = config["db_host"] |
151 | - container_config["WORDPRESS_DB_NAME"] = config["db_name"] |
152 | - container_config["WORDPRESS_DB_USER"] = config["db_user"] |
153 | - if config.get("wp_plugin_openid_team_map"): |
154 | - container_config["WP_PLUGIN_OPENID_TEAM_MAP"] = config["wp_plugin_openid_team_map"] |
155 | - return container_config |
156 | - |
157 | - |
158 | -def full_container_config(): |
159 | - """Container config with secrets""" |
160 | - config = hookenv.config() |
161 | - container_config = sanitized_container_config() |
162 | - if container_config is None: |
163 | - return None |
164 | - if config["container_secrets"].strip() == "": |
165 | - container_secrets = {} |
166 | - else: |
167 | - container_secrets = safe_load(config["container_secrets"]) |
168 | - if not isinstance(container_secrets, dict): |
169 | - status.blocked("container_secrets is not a YAML mapping") |
170 | - return None |
171 | - container_config.update(container_secrets) |
172 | - # Add secrets from charm config |
173 | - container_config["WORDPRESS_DB_PASSWORD"] = config["db_password"] |
174 | - if config.get("wp_plugin_akismet_key"): |
175 | - container_config["WP_PLUGIN_AKISMET_KEY"] = config["wp_plugin_akismet_key"] |
176 | - return container_config |
177 | - |
178 | - |
179 | -def make_pod_spec(): |
180 | - config = hookenv.config() |
181 | - container_config = sanitized_container_config() |
182 | - if container_config is None: |
183 | - return # Status already set |
184 | - |
185 | - ports = [ |
186 | - {"name": name, "containerPort": int(port), "protocol": "TCP"} |
187 | - for name, port in [addr.split(":", 1) for addr in config["ports"].split()] |
188 | - ] |
189 | - |
190 | - # PodSpec v1? https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.13/#podspec-v1-core |
191 | - spec = { |
192 | - "containers": [ |
193 | - { |
194 | - "name": hookenv.charm_name(), |
195 | - "imageDetails": {"imagePath": config["image"]}, |
196 | - "ports": ports, |
197 | - "config": container_config, |
198 | - } |
199 | - ] |
200 | - } |
201 | - out = io.StringIO() |
202 | - pprint(spec, out) |
203 | - hookenv.log("Container environment config (sans secrets) <<EOM\n{}\nEOM".format(out.getvalue())) |
204 | - |
205 | - # If we need credentials (secrets) for our image, add them to the spec after logging |
206 | - if config.get("image_user") and config.get("image_pass"): |
207 | - spec.get("containers")[0].get("imageDetails")["username"] = config["image_user"] |
208 | - spec.get("containers")[0].get("imageDetails")["password"] = config["image_pass"] |
209 | - |
210 | - config_with_secrets = full_container_config() |
211 | - if config_with_secrets is None: |
212 | - return None # Status already set |
213 | - container_config.update(config_with_secrets) |
214 | - |
215 | - return spec |
216 | - |
217 | - |
218 | -def first_install(): |
219 | - """Perform initial configuration of wordpress if needed.""" |
220 | - config = hookenv.config() |
221 | - if not is_pod_up("website"): |
222 | - hookenv.log("Pod not yet ready - retrying") |
223 | - return False |
224 | - elif not is_vhost_ready(): |
225 | - hookenv.log("Wordpress vhost is not yet listening - retrying") |
226 | - return False |
227 | - elif wordpress_configured() or not config["initial_settings"]: |
228 | - hookenv.log("No initial_setting provided or wordpress already configured. Skipping first install.") |
229 | - return True |
230 | - hookenv.log("Starting wordpress initial configuration") |
231 | - payload = { |
232 | - "admin_password": host.pwgen(24), |
233 | - "blog_public": "checked", |
234 | - "Submit": "submit", |
235 | - } |
236 | - payload.update(safe_load(config["initial_settings"])) |
237 | - payload["admin_password2"] = payload["admin_password"] |
238 | - if not payload["blog_public"]: |
239 | - payload["blog_public"] = "unchecked" |
240 | - required_config = set(("user_name", "admin_email")) |
241 | - missing = required_config.difference(payload.keys()) |
242 | - if missing: |
243 | - hookenv.log("Error: missing wordpress settings: {}".format(missing)) |
244 | - return False |
245 | - call_wordpress("/wp-admin/install.php?step=2", redirects=True, payload=payload) |
246 | - host.write_file(os.path.join("/root/", "initial.passwd"), payload["admin_password"], perms=0o400) |
247 | - return True |
248 | - |
249 | - |
250 | -def call_wordpress(uri, redirects=True, payload={}, _depth=1): |
251 | - max_depth = 10 |
252 | - if _depth > max_depth: |
253 | - hookenv.log("Redirect loop detected in call_worpress()") |
254 | - raise RuntimeError("Redirect loop detected in call_worpress()") |
255 | - config = hookenv.config() |
256 | - service_ip = get_service_ip("website") |
257 | - if service_ip: |
258 | - headers = {"Host": config["blog_hostname"]} |
259 | - url = urlunparse(("http", service_ip, uri, "", "", "")) |
260 | - if payload: |
261 | - r = requests.post(url, allow_redirects=False, headers=headers, data=payload, timeout=30) |
262 | - else: |
263 | - r = requests.get(url, allow_redirects=False, headers=headers, timeout=30) |
264 | - if redirects and r.is_redirect: |
265 | - # Recurse, but strip the scheme and host first, we need to connect over HTTP by bare IP |
266 | - o = urlparse(r.headers.get("Location")) |
267 | - return call_wordpress(o.path, redirects=redirects, payload=payload, _depth=_depth + 1) |
268 | - else: |
269 | - return r |
270 | - else: |
271 | - hookenv.log("Error getting service IP") |
272 | - return False |
273 | - |
274 | - |
275 | -def wordpress_configured(): |
276 | - """Check whether first install has been completed.""" |
277 | - # Check whether pod is deployed |
278 | - if not is_pod_up("website"): |
279 | - return False |
280 | - # Check if we have WP code deployed at all |
281 | - if not is_vhost_ready(): |
282 | - return False |
283 | - # We have code on disk, check if configured |
284 | - try: |
285 | - r = call_wordpress("/", redirects=False) |
286 | - except requests.exceptions.ConnectionError: |
287 | - return False |
288 | - if r.status_code == 302 and re.match("^.*/wp-admin/install.php", r.headers.get("location", "")): |
289 | - return False |
290 | - elif r.status_code == 302 and re.match("^.*/wp-admin/setup-config.php", r.headers.get("location", "")): |
291 | - hookenv.log("MySQL database setup failed, we likely have no wp-config.php") |
292 | - status.blocked("MySQL database setup failed, we likely have no wp-config.php") |
293 | - return False |
294 | - else: |
295 | - return True |
296 | - |
297 | - |
298 | -def is_vhost_ready(): |
299 | - """Check whether wordpress is available using http.""" |
300 | - # Check if we have WP code deployed at all |
301 | - try: |
302 | - r = call_wordpress("/wp-login.php", redirects=False) |
303 | - except requests.exceptions.ConnectionError: |
304 | - hookenv.log("call_wordpress() returned requests.exceptions.ConnectionError") |
305 | - return False |
306 | - if r is None: |
307 | - hookenv.log("call_wordpress() returned None") |
308 | - return False |
309 | - if hasattr(r, "status_code") and r.status_code in (403, 404): |
310 | - hookenv.log("call_wordpress() returned status {}".format(r.status_code)) |
311 | - return False |
312 | - else: |
313 | - return True |
314 | - |
315 | - |
316 | -def get_service_ip(endpoint): |
317 | - try: |
318 | - info = hookenv.network_get(endpoint, hookenv.relation_id()) |
319 | - if "ingress-addresses" in info: |
320 | - addr = info["ingress-addresses"][0] |
321 | - if len(addr): |
322 | - return addr |
323 | - else: |
324 | - hookenv.log("No ingress-addresses: {}".format(info)) |
325 | - except Exception as e: |
326 | - hookenv.log("Caught exception checking for service IP: {}".format(e)) |
327 | - |
328 | - return None |
329 | - |
330 | - |
331 | -def is_pod_up(endpoint): |
332 | - """Check to see if the pod of a relation is up. |
333 | - |
334 | - application-vimdb: 19:29:10 INFO unit.vimdb/0.juju-log network info |
335 | - |
336 | - In the example below: |
337 | - - 10.1.1.105 is the address of the application pod. |
338 | - - 10.152.183.199 is the service cluster ip |
339 | - |
340 | - { |
341 | - 'bind-addresses': [{ |
342 | - 'macaddress': '', |
343 | - 'interfacename': '', |
344 | - 'addresses': [{ |
345 | - 'hostname': '', |
346 | - 'address': '10.1.1.105', |
347 | - 'cidr': '' |
348 | - }] |
349 | - }], |
350 | - 'egress-subnets': [ |
351 | - '10.152.183.199/32' |
352 | - ], |
353 | - 'ingress-addresses': [ |
354 | - '10.152.183.199', |
355 | - '10.1.1.105' |
356 | - ] |
357 | - } |
358 | - """ |
359 | - try: |
360 | - info = hookenv.network_get(endpoint, hookenv.relation_id()) |
361 | - |
362 | - # Check to see if the pod has been assigned its internal and external ips |
363 | - for ingress in info["ingress-addresses"]: |
364 | - if len(ingress) == 0: |
365 | - return False |
366 | - except Exception: |
367 | - return False |
368 | - |
369 | - return True |
370 | diff --git a/src/charm.py b/src/charm.py |
371 | new file mode 100755 |
372 | index 0000000..a19bc28 |
373 | --- /dev/null |
374 | +++ b/src/charm.py |
375 | @@ -0,0 +1,294 @@ |
376 | +#!/usr/bin/env python3 |
377 | + |
378 | +import io |
379 | +import re |
380 | +import subprocess |
381 | +import sys |
382 | +from pprint import pprint |
383 | +from time import sleep |
384 | +from urllib.parse import urlparse, urlunparse |
385 | +from yaml import safe_load |
386 | + |
387 | +sys.path.append("lib") |
388 | + |
389 | +from ops.charm import CharmBase # NoQA: E402 |
390 | +from ops.framework import StoredState # NoQA: E402 |
391 | +from ops.main import main # NoQA: E402 |
392 | +from ops.model import ActiveStatus, BlockedStatus, MaintenanceStatus # NoQA: E402 |
393 | + |
394 | +import logging # NoQA: E402 |
395 | + |
396 | +logger = logging.getLogger() |
397 | + |
398 | + |
399 | +class Wordpress(CharmBase): |
400 | + state = StoredState() |
401 | + |
402 | + def __init__(self, *args): |
403 | + super().__init__(*args) |
404 | + for event in ( |
405 | + self.on.start, |
406 | + self.on.config_changed, |
407 | + ): |
408 | + self.framework.observe(event, self) |
409 | + |
410 | + def on_start(self, event): |
411 | + logger.info("Here we go...") |
412 | + # There may be a nicer way to do this! |
413 | + # https://github.com/canonical/operator/issues/156 |
414 | + subprocess.check_call(['apt-get', 'update']) |
415 | + subprocess.check_call(['apt-get', '-y', 'install', 'python3-requests']) |
416 | + # Initialise states |
417 | + self.state._started = True |
418 | + self.state._configured = False |
419 | + self.state._valid = False |
420 | + |
421 | + def on_config_changed(self, event): |
422 | + valid = True |
423 | + config = self.model.config |
424 | + for k in ["image", "db_host", "db_name", "db_user", "db_password"]: |
425 | + if config[k].strip() == "": |
426 | + self.model.unit.status = BlockedStatus("{!r} config is required".format(k)) |
427 | + valid = False |
428 | + self.state._valid = valid |
429 | + self.configure_pod(event) |
430 | + |
431 | + def sanitized_container_config(self): |
432 | + """Container config without secrets""" |
433 | + config = self.model.config |
434 | + if config["container_config"].strip() == "": |
435 | + container_config = {} |
436 | + else: |
437 | + container_config = safe_load(config["container_config"]) |
438 | + if not isinstance(container_config, dict): |
439 | + self.model.unit.status = BlockedStatus("container_config is not a YAML mapping") |
440 | + return None |
441 | + container_config["WORDPRESS_DB_HOST"] = config["db_host"] |
442 | + container_config["WORDPRESS_DB_NAME"] = config["db_name"] |
443 | + container_config["WORDPRESS_DB_USER"] = config["db_user"] |
444 | + if config.get("wp_plugin_openid_team_map"): |
445 | + container_config["WP_PLUGIN_OPENID_TEAM_MAP"] = config["wp_plugin_openid_team_map"] |
446 | + return container_config |
447 | + |
448 | + def full_container_config(self): |
449 | + """Container config with secrets""" |
450 | + config = self.model.config |
451 | + container_config = self.sanitized_container_config() |
452 | + if container_config is None: |
453 | + return None |
454 | + if config["container_secrets"].strip() == "": |
455 | + container_secrets = {} |
456 | + else: |
457 | + container_secrets = safe_load(config["container_secrets"]) |
458 | + if not isinstance(container_secrets, dict): |
459 | + self.model.unit.status = BlockedStatus("container_secrets is not a YAML mapping") |
460 | + return None |
461 | + container_config.update(container_secrets) |
462 | + # Add secrets from charm config |
463 | + container_config["WORDPRESS_DB_PASSWORD"] = config["db_password"] |
464 | + if config.get("wp_plugin_akismet_key"): |
465 | + container_config["WP_PLUGIN_AKISMET_KEY"] = config["wp_plugin_akismet_key"] |
466 | + return container_config |
467 | + |
468 | + def make_pod_spec(self): |
469 | + config = self.model.config |
470 | + container_config = self.sanitized_container_config() |
471 | + if container_config is None: |
472 | + return # Status already set |
473 | + |
474 | + ports = [ |
475 | + {"name": name, "containerPort": int(port), "protocol": "TCP"} |
476 | + for name, port in [addr.split(":", 1) for addr in config["ports"].split()] |
477 | + ] |
478 | + |
479 | + # PodSpec v1? https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.13/#podspec-v1-core |
480 | + spec = { |
481 | + "containers": [ |
482 | + { |
483 | + "name": self.app.name, |
484 | + "imageDetails": {"imagePath": config["image"]}, |
485 | + "ports": ports, |
486 | + "config": container_config, |
487 | + "readinessProbe": {"exec": {"command": ["cat", "/srv/wordpress-helpers/.ready"]}}, |
488 | + } |
489 | + ] |
490 | + } |
491 | + out = io.StringIO() |
492 | + pprint(spec, out) |
493 | + logger.info("Container environment config (sans secrets) <<EOM\n{}\nEOM".format(out.getvalue())) |
494 | + |
495 | + # If we need credentials (secrets) for our image, add them to the spec after logging |
496 | + if config.get("image_user") and config.get("image_pass"): |
497 | + spec.get("containers")[0].get("imageDetails")["username"] = config["image_user"] |
498 | + spec.get("containers")[0].get("imageDetails")["password"] = config["image_pass"] |
499 | + |
500 | + config_with_secrets = self.full_container_config() |
501 | + if config_with_secrets is None: |
502 | + return None # Status already set |
503 | + container_config.update(config_with_secrets) |
504 | + |
505 | + return spec |
506 | + |
507 | + def first_install(self): |
508 | + """Perform initial configuration of wordpress if needed.""" |
509 | + config = self.model.config |
510 | + if not self.is_pod_up("website"): |
511 | + logger.info("Pod not yet ready - retrying") |
512 | + return False |
513 | + elif not self.is_vhost_ready(): |
514 | + logger.info("Wordpress vhost is not yet listening - retrying") |
515 | + return False |
516 | + elif self.state._configured or not config["initial_settings"]: |
517 | + logger.info("No initial_setting provided or wordpress already configured. Skipping first install.") |
518 | + return True |
519 | + logger.info("Starting wordpress initial configuration") |
520 | + payload = { |
521 | + # "admin_password": host.pwgen(24), ## TODO: pwgen |
522 | + "admin_password": "letmein123", |
523 | + "blog_public": "checked", |
524 | + "Submit": "submit", |
525 | + } |
526 | + payload.update(safe_load(config["initial_settings"])) |
527 | + payload["admin_password2"] = payload["admin_password"] |
528 | + if not payload["blog_public"]: |
529 | + payload["blog_public"] = "unchecked" |
530 | + required_config = set(("user_name", "admin_email")) |
531 | + missing = required_config.difference(payload.keys()) |
532 | + if missing: |
533 | + logger.info("Error: missing wordpress settings: {}".format(missing)) |
534 | + return False |
535 | + self.call_wordpress("/wp-admin/install.php?step=2", redirects=True, payload=payload) |
536 | + # TODO: write_file |
537 | + # host.write_file(os.path.join("/root/", "initial.passwd"), payload["admin_password"], perms=0o400) |
538 | + return True |
539 | + |
540 | + def call_wordpress(self, uri, redirects=True, payload={}, _depth=1): |
541 | + import requests |
542 | + |
543 | + max_depth = 10 |
544 | + if _depth > max_depth: |
545 | + logger.info("Redirect loop detected in call_worpress()") |
546 | + raise RuntimeError("Redirect loop detected in call_worpress()") |
547 | + config = self.model.config |
548 | + service_ip = self.get_service_ip("website") |
549 | + if service_ip: |
550 | + headers = {"Host": config["blog_hostname"]} |
551 | + url = urlunparse(("http", service_ip, uri, "", "", "")) |
552 | + if payload: |
553 | + r = requests.post(url, allow_redirects=False, headers=headers, data=payload, timeout=30) |
554 | + else: |
555 | + r = requests.get(url, allow_redirects=False, headers=headers, timeout=30) |
556 | + if redirects and r.is_redirect: |
557 | + # Recurse, but strip the scheme and host first, we need to connect over HTTP by bare IP |
558 | + o = urlparse(r.headers.get("Location")) |
559 | + return self.call_wordpress(o.path, redirects=redirects, payload=payload, _depth=_depth + 1) |
560 | + else: |
561 | + return r |
562 | + else: |
563 | + logger.info("Error getting service IP") |
564 | + return False |
565 | + |
566 | + def wordpress_configured(self): |
567 | + """Check whether first install has been completed.""" |
568 | + import requests |
569 | + |
570 | + # Check whether pod is deployed |
571 | + if not self.is_pod_up("website"): |
572 | + return False |
573 | + # Check if we have WP code deployed at all |
574 | + if not self.is_vhost_ready(): |
575 | + return False |
576 | + # We have code on disk, check if configured |
577 | + try: |
578 | + r = self.call_wordpress("/", redirects=False) |
579 | + except requests.exceptions.ConnectionError: |
580 | + return False |
581 | + if r.status_code == 302 and re.match("^.*/wp-admin/install.php", r.headers.get("location", "")): |
582 | + return False |
583 | + elif r.status_code == 302 and re.match("^.*/wp-admin/setup-config.php", r.headers.get("location", "")): |
584 | + logger.info("MySQL database setup failed, we likely have no wp-config.php") |
585 | + self.model.unit.status = BlockedStatus("MySQL database setup failed, we likely have no wp-config.php") |
586 | + return False |
587 | + else: |
588 | + return True |
589 | + |
590 | + def is_vhost_ready(self): |
591 | + """Check whether wordpress is available using http.""" |
592 | + import requests |
593 | + |
594 | + # Check if we have WP code deployed at all |
595 | + try: |
596 | + r = self.call_wordpress("/wp-login.php", redirects=False) |
597 | + except requests.exceptions.ConnectionError: |
598 | + logger.info("call_wordpress() returned requests.exceptions.ConnectionError") |
599 | + return False |
600 | + if r is None: |
601 | + logger.info("call_wordpress() returned None") |
602 | + return False |
603 | + if hasattr(r, "status_code") and r.status_code in (403, 404): |
604 | + logger.info("call_wordpress() returned status {}".format(r.status_code)) |
605 | + return False |
606 | + else: |
607 | + return True |
608 | + |
609 | + def get_service_ip(self, endpoint): |
610 | + try: |
611 | + self.model.get_binding(endpoint).network is not False |
612 | + except Exception: |
613 | + logger.info("We don't have networking yet") |
614 | + return '' |
615 | + try: |
616 | + return str(self.model.get_binding(endpoint).network.ingress_addresses[0]) |
617 | + except Exception: |
618 | + logger.info("We don't have any ingress addresses yet") |
619 | + return '' |
620 | + |
621 | + def is_pod_up(self, endpoint): |
622 | + """Check to see if the pod of a relation is up""" |
623 | + ingress = self.get_service_ip(endpoint) |
624 | + if len(ingress) == 0: |
625 | + return False |
626 | + return True |
627 | + |
628 | + def configure_pod(self, event): # NoQA: C901 |
629 | + if not self.state._started: |
630 | + return |
631 | + if not self.state._valid: |
632 | + return |
633 | + elif self.model.unit.is_leader(): |
634 | + # only the leader can set_spec() |
635 | + spec = self.make_pod_spec() |
636 | + if spec is None: |
637 | + return # Status already set |
638 | + # TODO: Figure out operator equivalent of reactive.data_changed() |
639 | + # https://github.com/canonical/operator/issues/189 |
640 | + # if reactive.data_changed("wordpress.spec", spec): |
641 | + # "if True" works around it, but we'll respwan on every hook run, |
642 | + # which is decidedly sub-optimal |
643 | + if True: |
644 | + self.model.unit.status = MaintenanceStatus("Configuring container") |
645 | + try: |
646 | + self.model.pod.set_spec(spec) |
647 | + except Exception as e: |
648 | + logger.info("pod_spec_set failed: {}".format(e)) |
649 | + self.model.unit.status = BlockedStatus("pod_spec_set failed! Check logs and k8s dashboard.") |
650 | + return |
651 | + else: |
652 | + logger.info("No changes to pod spec") |
653 | + if not self.state._configured: |
654 | + tries = 0 |
655 | + max_tries = 20 |
656 | + while tries < max_tries: |
657 | + if self.first_install(): |
658 | + self.state._configured = True |
659 | + self.model.unit.status = ActiveStatus() |
660 | + else: |
661 | + tries = tries + 1 |
662 | + logger.info("Sleeping 30s after attempt {}/{}, will try again".format(tries, max_tries)) |
663 | + sleep(30) |
664 | + logger.error("Giving up on configuration after attempt {}/{}".format(tries, max_tries)) |
665 | + self.model.unit.status = BlockedStatus("first_install failed! Check logs") |
666 | + |
667 | + |
668 | +if __name__ == "__main__": |
669 | + main(Wordpress) |
670 | diff --git a/tests/unit/test_wordpress.py b/tests/unit/test_wordpress.py |
671 | index 5ade069..e69de29 100644 |
672 | --- a/tests/unit/test_wordpress.py |
673 | +++ b/tests/unit/test_wordpress.py |
674 | @@ -1,64 +0,0 @@ |
675 | -import os |
676 | -import shutil |
677 | -import sys |
678 | -import tempfile |
679 | -import unittest |
680 | -from unittest import mock |
681 | - |
682 | -# We also need to mock up charms.layer so we can run unit tests without having |
683 | -# to build the charm and pull in layers such as layer-status. |
684 | -sys.modules['charms.layer'] = mock.MagicMock() |
685 | - |
686 | -from charms.layer import status # NOQA: E402 |
687 | - |
688 | -# Add path to where our reactive layer lives and import. |
689 | -sys.path.append(os.path.dirname(os.path.dirname(os.path.dirname(os.path.realpath(__file__))))) |
690 | -from reactive import wordpress # NOQA: E402 |
691 | - |
692 | - |
693 | -class TestCharm(unittest.TestCase): |
694 | - def setUp(self): |
695 | - self.maxDiff = None |
696 | - self.tmpdir = tempfile.mkdtemp(prefix='charm-unittests-') |
697 | - self.addCleanup(shutil.rmtree, self.tmpdir) |
698 | - |
699 | - self.charm_dir = os.path.dirname(os.path.dirname(os.path.dirname(os.path.realpath(__file__)))) |
700 | - |
701 | - patcher = mock.patch('charmhelpers.core.hookenv.log') |
702 | - self.mock_log = patcher.start() |
703 | - self.addCleanup(patcher.stop) |
704 | - self.mock_log.return_value = '' |
705 | - |
706 | - patcher = mock.patch('charmhelpers.core.hookenv.charm_dir') |
707 | - self.mock_charm_dir = patcher.start() |
708 | - self.addCleanup(patcher.stop) |
709 | - self.mock_charm_dir.return_value = self.charm_dir |
710 | - |
711 | - patcher = mock.patch('charmhelpers.core.hookenv.local_unit') |
712 | - self.mock_local_unit = patcher.start() |
713 | - self.addCleanup(patcher.stop) |
714 | - self.mock_local_unit.return_value = 'mock-wordpress/0' |
715 | - |
716 | - patcher = mock.patch('charmhelpers.core.hookenv.config') |
717 | - self.mock_config = patcher.start() |
718 | - self.addCleanup(patcher.stop) |
719 | - self.mock_config.return_value = {'blog_hostname': 'myblog.example.com'} |
720 | - |
721 | - patcher = mock.patch('charmhelpers.core.host.log') |
722 | - self.mock_log = patcher.start() |
723 | - self.addCleanup(patcher.stop) |
724 | - self.mock_log.return_value = '' |
725 | - |
726 | - status.active.reset_mock() |
727 | - status.blocked.reset_mock() |
728 | - status.maintenance.reset_mock() |
729 | - |
730 | - @mock.patch('charms.reactive.clear_flag') |
731 | - def test_hook_upgrade_charm_flags(self, clear_flag): |
732 | - '''Test correct flags set via upgrade-charm hook''' |
733 | - wordpress.upgrade_charm() |
734 | - self.assertFalse(status.maintenance.assert_called()) |
735 | - want = [ |
736 | - mock.call('wordpress.configured'), |
737 | - ] |
738 | - self.assertFalse(clear_flag.assert_has_calls(want, any_order=True)) |
739 | diff --git a/tox.ini b/tox.ini |
740 | index 7b45934..3f4c39f 100644 |
741 | --- a/tox.ini |
742 | +++ b/tox.ini |
743 | @@ -10,7 +10,7 @@ setenv = |
744 | |
745 | [testenv:unit] |
746 | commands = |
747 | - pytest --ignore {toxinidir}/tests/functional \ |
748 | + pytest --ignore mod --ignore {toxinidir}/tests/functional \ |
749 | {posargs:-v --cov=reactive --cov-report=term-missing --cov-branch} |
750 | deps = -r{toxinidir}/tests/unit/requirements.txt |
751 | -r{toxinidir}/requirements.txt |
752 | @@ -24,16 +24,16 @@ passenv = |
753 | JUJU_REPOSITORY |
754 | PATH |
755 | commands = |
756 | - pytest -v --ignore {toxinidir}/tests/unit {posargs} |
757 | + pytest -v --ignore mod --ignore {toxinidir}/tests/unit {posargs} |
758 | deps = -r{toxinidir}/tests/functional/requirements.txt |
759 | -r{toxinidir}/requirements.txt |
760 | |
761 | [testenv:black] |
762 | -commands = black --skip-string-normalization --line-length=120 . |
763 | +commands = black --skip-string-normalization --line-length=120 src/ tests/ |
764 | deps = black |
765 | |
766 | [testenv:lint] |
767 | -commands = flake8 |
768 | +commands = flake8 src/ tests/ |
769 | deps = flake8 |
770 | |
771 | [flake8] |
772 | diff --git a/wheelhouse.txt b/wheelhouse.txt |
773 | deleted file mode 100644 |
774 | index f229360..0000000 |
775 | --- a/wheelhouse.txt |
776 | +++ /dev/null |
777 | @@ -1 +0,0 @@ |
778 | -requests |
Some comments inline