Merge ~jk0ne/charm-k8s-discourse/+git/charm-k8s-discourse:charmcraft-review2 into ~discourse-charmers/charm-k8s-discourse/+git/charmcraft-review:master

Proposed by Jay Kuri
Status: Merged
Merge reported by: Tom Haddon
Merged at revision: 80ff3cc0c39a02da8c7ddc413b92e5aa34fca0c0
Proposed branch: ~jk0ne/charm-k8s-discourse/+git/charm-k8s-discourse:charmcraft-review2
Merge into: ~discourse-charmers/charm-k8s-discourse/+git/charmcraft-review:master
Diff against target: 630 lines (+534/-23)
9 files modified
image/Dockerfile (+1/-1)
src/charm.py (+243/-22)
tests/unit/fixtures/config_invalid_missing_cors.yaml (+20/-0)
tests/unit/fixtures/config_invalid_missing_db_name.yaml (+18/-0)
tests/unit/fixtures/config_valid_complete.yaml (+38/-0)
tests/unit/fixtures/config_valid_with_tls.yaml (+38/-0)
tests/unit/fixtures/config_valid_without_tls.yaml (+37/-0)
tests/unit/requirements.txt (+4/-0)
tests/unit/test_charm.py (+135/-0)
Reviewer Review Type Date Requested Status
Facundo Batista (community) Approve
Discourse Charm Maintainers work in progress Pending
Review via email: mp+392672@code.launchpad.net

Commit message

Complete discourse charm ready for review.

To post a comment you must log in.
Revision history for this message
Facundo Batista (facundo) wrote :

It looks great! Approved, but check a small suggestion in an inline comment.

review: Approve
08fde17... by Jay Kuri

Fix up non-leader event handling and fix missing field check per review

Revision history for this message
Tom Haddon (mthaddon) wrote :

One comment inline

80ff3cc... by Jay Kuri

Tweak missing_field check - remove parens

Revision history for this message
Tom Haddon (mthaddon) wrote :

I'm going to mark this as "Merged" as we want to get this off the review board for MPs for this project. We'll be incorporating the changes from this MP into a new MP against the main charm code.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/image/Dockerfile b/image/Dockerfile
2index 7baf5cb..96d49df 100644
3--- a/image/Dockerfile
4+++ b/image/Dockerfile
5@@ -13,7 +13,7 @@ ARG CONTAINER_APP_GID
6
7 # Copy any args we got into the environment.
8 ENV CONTAINER_APP_NAME ${CONTAINER_APP_NAME:-discourse}
9-ENV CONTAINER_APP_VERSION ${CONTAINER_APP_VERSION:-v2.4.3}
10+ENV CONTAINER_APP_VERSION ${CONTAINER_APP_VERSION:-v2.5.2}
11 ENV CONTAINER_APP_USERNAME ${CONTAINER_APP_USERNAME:-discourse}
12 ENV CONTAINER_APP_UID ${CONTAINER_APP_UID:-200}
13 ENV CONTAINER_APP_GROUP ${CONTAINER_APP_GROUP:-discourse}
14diff --git a/src/charm.py b/src/charm.py
15index a16d636..5bb75ad 100755
16--- a/src/charm.py
17+++ b/src/charm.py
18@@ -2,37 +2,258 @@
19 # Copyright 2020 Canonical Ltd.
20 # See LICENSE file for licensing details.
21
22-import logging
23-
24+import ops.lib
25 from ops.charm import CharmBase
26 from ops.main import main
27 from ops.framework import StoredState
28
29-logger = logging.getLogger(__name__)
30+from ops.model import ActiveStatus, BlockedStatus, MaintenanceStatus
31+
32+
33+pgsql = ops.lib.use("pgsql", 1, "postgresql-charmers@lists.launchpad.net")
34+
35+
36+def create_discourse_pod_config(config):
37+ """Create the pod environment config from the juju config."""
38+ pod_config = {
39+ 'DISCOURSE_POSTGRES_USERNAME': config['db_user'],
40+ 'DISCOURSE_POSTGRES_PASSWORD': config['db_password'],
41+ 'DISCOURSE_POSTGRES_HOST': config['db_host'],
42+ 'DISCOURSE_POSTGRES_NAME': config['db_name'],
43+ 'DISCOURSE_DEVELOPER_EMAILS': config['developer_emails'],
44+ 'DISCOURSE_HOSTNAME': config['external_hostname'],
45+ 'DISCOURSE_SMTP_DOMAIN': config['smtp_domain'],
46+ 'DISCOURSE_SMTP_ADDRESS': config['smtp_address'],
47+ 'DISCOURSE_SMTP_PORT': config['smtp_port'],
48+ 'DISCOURSE_SMTP_AUTHENTICATION': config['smtp_authentication'],
49+ 'DISCOURSE_SMTP_OPENSSL_VERIFY_MODE': config['smtp_openssl_verify_mode'],
50+ 'DISCOURSE_SMTP_USER_NAME': config['smtp_username'],
51+ 'DISCOURSE_SMTP_PASSWORD': config['smtp_password'],
52+ 'DISCOURSE_REDIS_HOST': config['redis_host'],
53+ 'DISCOURSE_ENABLE_CORS': config['enable_cors'],
54+ 'DISCOURSE_CORS_ORIGIN': config['cors_origin'],
55+ }
56+ return pod_config
57+
58+
59+def create_ingress_config(app_name, config):
60+ """Create the ingress config form the juju config."""
61+ annotations = {}
62+ ingressResource = {
63+ "name": app_name + "-ingress",
64+ "spec": {
65+ "rules": [
66+ {
67+ "host": config['external_hostname'],
68+ "http": {"paths": [{"path": "/", "backend": {"serviceName": app_name, "servicePort": 3000}}]},
69+ }
70+ ]
71+ },
72+ }
73+ tls_secret_name = config.get('tls_secret_name')
74+ if tls_secret_name:
75+ ingressResource['spec']['tls'] = [{'hosts': config['external_hostname'], 'secretName': tls_secret_name}]
76+ else:
77+ annotations['nginx.ingress.kubernetes.io/ssl-redirect'] = 'false'
78+
79+ if annotations:
80+ ingressResource['annotations'] = annotations
81+
82+ return ingressResource
83+
84+
85+def get_pod_spec(app_name, config):
86+ """Get the entire pod spec using the juju config.
87+
88+ - uses create_discourse_pod_config() to generate pod envConfig.
89+
90+ - uses create_ingress_config() to generate pod ingressResources.
91+
92+ """
93+ pod_spec = {
94+ "version": 3,
95+ "containers": [
96+ {
97+ "name": app_name,
98+ "imageDetails": {"imagePath": config['discourse_image']},
99+ "imagePullPolicy": "IfNotPresent",
100+ "ports": [{"containerPort": 3000, "protocol": "TCP"}],
101+ "envConfig": create_discourse_pod_config(config),
102+ "kubernetes": {
103+ "readinessProbe": {
104+ "httpGet": {
105+ "path": "/srv/status",
106+ "port": 3000,
107+ }
108+ }
109+ },
110+ }
111+ ],
112+ "kubernetesResources": {"ingressResources": [create_ingress_config(app_name, config)]},
113+ }
114+ # This handles when we are trying to get an image from a private
115+ # registry.
116+ if config['image_user'] and config['image_pass']:
117+ pod_spec['containers'][0]['imageDetails']['username'] = config['image_user']
118+ pod_spec['containers'][0]['imageDetails']['password'] = config['image_pass']
119+
120+ return pod_spec
121+
122+
123+def check_for_config_problems(config):
124+ """Check if there are issues with the juju config.
125+
126+ - Primarily looks for missing config options using check_for_missing_config_fields()
127+
128+ - Returns a list of errors if any were found.
129+ """
130+ errors = []
131+ missing_fields = check_for_missing_config_fields(config)
132+
133+ if missing_fields:
134+ errors.append('Required configuration missing: {}'.format(" ".join(missing_fields)))
135+
136+ if "db_host" not in config or config["db_host"] is None:
137+ errors.append("db relation is required")
138+
139+ return errors
140+
141+
142+def check_for_missing_config_fields(config):
143+ """Check for missing fields in juju config.
144
145+ - Returns a list of required fields that are either not present
146+ or are empty.
147+ """
148+ missing_fields = []
149
150-class CharmK8SDiscourseCharm(CharmBase):
151- _stored = StoredState()
152+ needed_fields = [
153+ 'db_name',
154+ 'smtp_address',
155+ 'redis_host',
156+ 'cors_origin',
157+ 'developer_emails',
158+ 'smtp_domain',
159+ 'discourse_image',
160+ 'external_hostname',
161+ ]
162+ for key in needed_fields:
163+ if not config.get(key):
164+ missing_fields.append(key)
165+
166+ return sorted(missing_fields)
167+
168+
169+class DiscourseCharm(CharmBase):
170+ stored = StoredState()
171
172 def __init__(self, *args):
173+ """Initialization.
174+
175+ - Primarily sets up defaults and event handlers.
176+
177+ """
178 super().__init__(*args)
179- self.framework.observe(self.on.config_changed, self._on_config_changed)
180- self.framework.observe(self.on.fortune_action, self._on_fortune_action)
181- self._stored.set_default(things=[])
182-
183- def _on_config_changed(self, _):
184- current = self.model.config["thing"]
185- if current not in self._stored.things:
186- logger.debug("found a new thing: %r", current)
187- self._stored.things.append(current)
188-
189- def _on_fortune_action(self, event):
190- fail = event.params["fail"]
191- if fail:
192- event.fail(fail)
193+
194+ # TODO: is_started is unused. Remove?
195+ self.stored.set_default(is_started=False, db_user=None, db_password=None, db_host=None)
196+ self.framework.observe(self.on.leader_elected, self.configure_pod)
197+ self.framework.observe(self.on.config_changed, self.configure_pod)
198+ self.framework.observe(self.on.upgrade_charm, self.configure_pod)
199+
200+ self.db = pgsql.PostgreSQLClient(self, 'db')
201+ self.framework.observe(self.db.on.database_relation_joined, self.on_database_relation_joined)
202+ self.framework.observe(self.db.on.master_changed, self.on_database_changed)
203+
204+ def check_config_is_valid(self, config):
205+ """Check that the provided config is valid.
206+
207+ - Returns True if config is valid, False otherwise.
208+
209+ - Sets model status as appropriate.
210+ """
211+ valid_config = True
212+ errors = check_for_config_problems(config)
213+
214+ # Set status if we have a bad config.
215+ if errors:
216+ self.model.unit.status = BlockedStatus(", ".join(errors))
217+ valid_config = False
218+ else:
219+ self.model.unit.status = MaintenanceStatus("Configuration passed validation")
220+
221+ return valid_config
222+
223+ def configure_pod(self, event=None):
224+ """Configure pod.
225+
226+ - Verifies config is valid and unit is leader.
227+
228+ - Configures pod using pod_spec generated from config.
229+ """
230+ # Set our status while we get configured.
231+ self.model.unit.status = MaintenanceStatus('Configuring pod')
232+
233+ # Leader must set the pod spec.
234+ if self.model.unit.is_leader():
235+ # Merge our config and state into a single dict and set
236+ # defaults here, because the helpers avoid dealing with
237+ # the framework.
238+ config = dict(self.model.config)
239+ if not config["db_name"]:
240+ config["db_name"] = self.app.name
241+ config["db_user"] = self.stored.db_user
242+ config["db_password"] = self.stored.db_password
243+ config["db_host"] = self.stored.db_host
244+ # Get our spec definition.
245+ if self.check_config_is_valid(config):
246+ # Get pod spec using our app name and config
247+ pod_spec = get_pod_spec(self.framework.model.app.name, config)
248+ # Set our pod spec.
249+ self.model.pod.set_spec(pod_spec)
250+ self.stored.is_started = True
251+ self.model.unit.status = ActiveStatus()
252 else:
253- event.set_results({"fortune": "A bug in the code is worth two in the documentation."})
254+ self.stored.is_started = True
255+ self.model.unit.status = ActiveStatus()
256+
257+ def on_database_relation_joined(self, event):
258+ """Event handler for a newly joined database relation.
259+
260+ - Sets the event.database field on the database joined event.
261+
262+ - Required because setting the database name is only possible
263+ from inside the event handler per https://github.com/canonical/ops-lib-pgsql/issues/2
264+ """
265+ # Per https://github.com/canonical/ops-lib-pgsql/issues/2,
266+ # changing the setting in the config will not take effect,
267+ # unless the relation is dropped and recreated.
268+ if self.model.unit.is_leader():
269+ event.database = self.model.config["db_name"]
270+ elif event.database != self.model.config["db_name"]:
271+ # Leader has not yet set requirements. Defer, in case this unit
272+ # becomes leader and needs to perform that operation.
273+ event.defer()
274+ return
275+
276+ def on_database_changed(self, event):
277+ """Event handler for database relation change.
278+
279+ - Sets our database parameters based on what was provided
280+ in the relation event.
281+ """
282+ if event.master is None:
283+ self.stored.db_user = None
284+ self.stored.db_password = None
285+ self.stored.db_host = None
286+ return
287+
288+ self.stored.db_user = event.master.user
289+ self.stored.db_password = event.master.password
290+ self.stored.db_host = event.master.host
291+
292+ self.configure_pod()
293
294
295-if __name__ == "__main__":
296- main(CharmK8SDiscourseCharm)
297+if __name__ == '__main__': # pragma: no cover
298+ main(DiscourseCharm)
299diff --git a/tests/unit/fixtures/config_invalid_missing_cors.yaml b/tests/unit/fixtures/config_invalid_missing_cors.yaml
300new file mode 100644
301index 0000000..da4a5c8
302--- /dev/null
303+++ b/tests/unit/fixtures/config_invalid_missing_cors.yaml
304@@ -0,0 +1,20 @@
305+config:
306+ cors_origin: ''
307+ db_name: discourse
308+ db_host: 10.25.244.12
309+ developer_emails: some.person@example.com
310+ discourse_image: discourse-k8s:1.0.7f
311+ enable_cors: true
312+ external_hostname: discourse.local
313+ image_pass: ''
314+ image_user: ''
315+ redis_host: 10.9.89.197
316+ smtp_address: 167.89.123.58
317+ smtp_authentication: login
318+ smtp_domain: example.com
319+ smtp_openssl_verify_mode: none
320+ smtp_password: OBV10USLYF4K3
321+ smtp_port: 587
322+ smtp_username: apikey
323+missing_fields:
324+ - 'cors_origin'
325diff --git a/tests/unit/fixtures/config_invalid_missing_db_name.yaml b/tests/unit/fixtures/config_invalid_missing_db_name.yaml
326new file mode 100644
327index 0000000..0e2ea39
328--- /dev/null
329+++ b/tests/unit/fixtures/config_invalid_missing_db_name.yaml
330@@ -0,0 +1,18 @@
331+config:
332+ cors_origin: '*'
333+ developer_emails: some.person@example.com
334+ discourse_image: discourse-k8s:1.0.7f
335+ enable_cors: true
336+ external_hostname: discourse.local
337+ image_pass: ''
338+ image_user: ''
339+ redis_host: 10.9.89.197
340+ smtp_address: 167.89.123.58
341+ smtp_authentication: login
342+ smtp_domain: example.com
343+ smtp_openssl_verify_mode: none
344+ smtp_password: OBV10USLYF4K3
345+ smtp_port: 587
346+ smtp_username: apikey
347+missing_fields:
348+ - 'db_name'
349diff --git a/tests/unit/fixtures/config_valid_complete.yaml b/tests/unit/fixtures/config_valid_complete.yaml
350new file mode 100644
351index 0000000..28e7367
352--- /dev/null
353+++ b/tests/unit/fixtures/config_valid_complete.yaml
354@@ -0,0 +1,38 @@
355+config:
356+ cors_origin: '*'
357+ db_host: 10.9.89.237
358+ db_name: discourse
359+ db_password: a_real_password
360+ db_user: discourse_m
361+ developer_emails: some.person@example.com
362+ discourse_image: discourse-k8s:1.0.7f
363+ enable_cors: true
364+ external_hostname: discourse.local
365+ image_pass: ''
366+ image_user: ''
367+ redis_host: 10.9.89.197
368+ smtp_address: 167.89.123.58
369+ smtp_authentication: login
370+ smtp_domain: example.com
371+ smtp_openssl_verify_mode: none
372+ smtp_password: OBV10USLYF4K3
373+ smtp_port: 587
374+ smtp_username: apikey
375+ tls_secret_name: discourse_local
376+pod_config:
377+ DISCOURSE_CORS_ORIGIN: '*'
378+ DISCOURSE_DEVELOPER_EMAILS: some.person@example.com
379+ DISCOURSE_ENABLE_CORS: true
380+ DISCOURSE_HOSTNAME: discourse.local
381+ DISCOURSE_POSTGRES_HOST: 10.9.89.237
382+ DISCOURSE_POSTGRES_NAME: discourse
383+ DISCOURSE_POSTGRES_PASSWORD: a_real_password
384+ DISCOURSE_POSTGRES_USERNAME: discourse_m
385+ DISCOURSE_REDIS_HOST: 10.9.89.197
386+ DISCOURSE_SMTP_ADDRESS: 167.89.123.58
387+ DISCOURSE_SMTP_AUTHENTICATION: login
388+ DISCOURSE_SMTP_DOMAIN: example.com
389+ DISCOURSE_SMTP_OPENSSL_VERIFY_MODE: none
390+ DISCOURSE_SMTP_PASSWORD: OBV10USLYF4K3
391+ DISCOURSE_SMTP_PORT: 587
392+ DISCOURSE_SMTP_USER_NAME: apikey
393diff --git a/tests/unit/fixtures/config_valid_with_tls.yaml b/tests/unit/fixtures/config_valid_with_tls.yaml
394new file mode 100644
395index 0000000..fa19ce1
396--- /dev/null
397+++ b/tests/unit/fixtures/config_valid_with_tls.yaml
398@@ -0,0 +1,38 @@
399+config:
400+ cors_origin: '*'
401+ db_host: 10.9.89.237
402+ db_name: discourse
403+ db_password: a_real_password
404+ db_user: discourse_m
405+ developer_emails: some.person@example.com
406+ discourse_image: discourse-k8s:1.0.7f
407+ enable_cors: true
408+ external_hostname: discourse.local
409+ image_pass: 'discourse123'
410+ image_user: 'discourse_role'
411+ redis_host: 10.9.89.197
412+ smtp_address: 167.89.123.58
413+ smtp_authentication: login
414+ smtp_domain: example.com
415+ smtp_openssl_verify_mode: none
416+ smtp_password: OBV10USLYF4K3
417+ smtp_port: 587
418+ smtp_username: apikey
419+ tls_secret_name: discourse-local
420+pod_config:
421+ DISCOURSE_CORS_ORIGIN: '*'
422+ DISCOURSE_DEVELOPER_EMAILS: some.person@example.com
423+ DISCOURSE_ENABLE_CORS: true
424+ DISCOURSE_HOSTNAME: discourse.local
425+ DISCOURSE_POSTGRES_HOST: 10.9.89.237
426+ DISCOURSE_POSTGRES_NAME: discourse
427+ DISCOURSE_POSTGRES_PASSWORD: a_real_password
428+ DISCOURSE_POSTGRES_USERNAME: discourse_m
429+ DISCOURSE_REDIS_HOST: 10.9.89.197
430+ DISCOURSE_SMTP_ADDRESS: 167.89.123.58
431+ DISCOURSE_SMTP_AUTHENTICATION: login
432+ DISCOURSE_SMTP_DOMAIN: example.com
433+ DISCOURSE_SMTP_OPENSSL_VERIFY_MODE: none
434+ DISCOURSE_SMTP_PASSWORD: OBV10USLYF4K3
435+ DISCOURSE_SMTP_PORT: 587
436+ DISCOURSE_SMTP_USER_NAME: apikey
437diff --git a/tests/unit/fixtures/config_valid_without_tls.yaml b/tests/unit/fixtures/config_valid_without_tls.yaml
438new file mode 100644
439index 0000000..8c440b8
440--- /dev/null
441+++ b/tests/unit/fixtures/config_valid_without_tls.yaml
442@@ -0,0 +1,37 @@
443+config:
444+ cors_origin: '*'
445+ db_host: 10.9.89.237
446+ db_name: discourse
447+ db_password: a_real_password
448+ db_user: discourse_m
449+ developer_emails: is-admin@example.com
450+ discourse_image: discourse-k8s:1.0.8
451+ enable_cors: true
452+ external_hostname: discourse.example.com
453+ image_pass: ''
454+ image_user: ''
455+ redis_host: 10.25.242.12
456+ smtp_address: smtp.internal
457+ smtp_authentication: login
458+ smtp_domain: example.com
459+ smtp_openssl_verify_mode: none
460+ smtp_password:
461+ smtp_port: 587
462+ smtp_username: apikey
463+pod_config:
464+ DISCOURSE_CORS_ORIGIN: '*'
465+ DISCOURSE_DEVELOPER_EMAILS: is-admin@example.com
466+ DISCOURSE_ENABLE_CORS: true
467+ DISCOURSE_HOSTNAME: discourse.example.com
468+ DISCOURSE_POSTGRES_HOST: 10.9.89.237
469+ DISCOURSE_POSTGRES_NAME: discourse
470+ DISCOURSE_POSTGRES_PASSWORD: a_real_password
471+ DISCOURSE_POSTGRES_USERNAME: discourse_m
472+ DISCOURSE_REDIS_HOST: 10.25.242.12
473+ DISCOURSE_SMTP_ADDRESS: smtp.internal
474+ DISCOURSE_SMTP_AUTHENTICATION: login
475+ DISCOURSE_SMTP_DOMAIN: example.com
476+ DISCOURSE_SMTP_OPENSSL_VERIFY_MODE: none
477+ DISCOURSE_SMTP_PASSWORD: null
478+ DISCOURSE_SMTP_PORT: 587
479+ DISCOURSE_SMTP_USER_NAME: apikey
480diff --git a/tests/unit/requirements.txt b/tests/unit/requirements.txt
481new file mode 100644
482index 0000000..65431fc
483--- /dev/null
484+++ b/tests/unit/requirements.txt
485@@ -0,0 +1,4 @@
486+mock
487+pytest
488+pytest-cov
489+pyyaml
490diff --git a/tests/unit/test_charm.py b/tests/unit/test_charm.py
491new file mode 100644
492index 0000000..8dc65d8
493--- /dev/null
494+++ b/tests/unit/test_charm.py
495@@ -0,0 +1,135 @@
496+#!/usr/bin/env python3
497+
498+# Copyright 2020 Canonical Ltd.
499+# See LICENSE file for licensing details.
500+
501+import copy
502+import glob
503+import mock
504+import os
505+import unittest
506+import yaml
507+
508+from types import SimpleNamespace
509+
510+from charm import (
511+ check_for_missing_config_fields,
512+ create_discourse_pod_config,
513+ get_pod_spec,
514+ DiscourseCharm,
515+)
516+
517+from ops import testing
518+
519+
520+def load_configs(directory):
521+ """Load configs for use by tests.
522+
523+ Valid and invalid configs are present in the fixtures directory. The files
524+ contain the juju config, along with the spec that that config should
525+ produce in the case of valid configs. In the case of invalid configs, they
526+ contain the juju config and the error that should be triggered by the
527+ config. These scenarios are tested below. Additional config variations can
528+ be created by creating an appropriately named config file in the fixtures
529+ directory. Valid configs should be named: config_valid_###.yaml and invalid
530+ configs should be named: config_invalid_###.yaml."""
531+ configs = {}
532+ for filename in glob.glob(os.path.join(directory, 'config*.yaml')):
533+ with open(filename) as file:
534+ name, _ = os.path.splitext(os.path.basename(filename))
535+ configs[name] = yaml.full_load(file)
536+ return configs
537+
538+
539+class TestDiscourseK8sCharmHooksDisabled(unittest.TestCase):
540+ def setUp(self):
541+ self.harness = testing.Harness(DiscourseCharm)
542+ self.harness.disable_hooks()
543+ self.harness.set_leader(True)
544+ self.harness.begin()
545+ self.configs = load_configs(os.path.join(os.path.dirname(__file__), 'fixtures'))
546+
547+ def test_valid_configs_are_ok(self):
548+ """Test that a valid config is considered valid."""
549+ for config_key in self.configs:
550+ if config_key.startswith('config_valid_'):
551+ config_valid = self.harness.charm.check_config_is_valid(self.configs[config_key]['config'])
552+ pod_config = create_discourse_pod_config(self.configs[config_key]['config'])
553+ self.assertEqual(config_valid, True, 'Valid config is not recognized as valid')
554+ self.assertEqual(
555+ pod_config,
556+ self.configs[config_key]['pod_config'],
557+ 'Valid config {} is does not produce expected config options for pod.'.format(config_key),
558+ )
559+
560+ def test_invalid_configs_are_recognized(self):
561+ """Test that bad configs are identified by the charm."""
562+ for config_key in self.configs:
563+ if config_key.startswith('config_invalid_'):
564+ config_valid = self.harness.charm.check_config_is_valid(self.configs[config_key]['config'])
565+ missing_fields = check_for_missing_config_fields(self.configs[config_key]['config'])
566+ self.assertEqual(config_valid, False, 'Invalid config is not recognized as invalid')
567+ self.assertEqual(
568+ missing_fields,
569+ self.configs[config_key]['missing_fields'],
570+ 'Invalid config {} does not fail as expected.'.format(config_key),
571+ )
572+
573+ def test_charm_config_process(self):
574+ """Test that the entire config process for the charm works."""
575+ test_config = copy.deepcopy(self.configs['config_valid_complete']['config'])
576+ test_config['db_user'] = 'discourse_m'
577+ test_config['db_password'] = 'a_real_password'
578+ test_config['db_host'] = '10.9.89.237'
579+ db_event = SimpleNamespace()
580+ db_event.master = SimpleNamespace()
581+ db_event.master.user = 'discourse_m'
582+ db_event.master.password = 'a_real_password'
583+ db_event.master.host = '10.9.89.237'
584+ expected_spec = (get_pod_spec(self.harness.charm.framework.model.app.name, test_config), None)
585+ self.harness.update_config(self.configs['config_valid_complete']['config'])
586+ self.harness.charm.on_database_relation_joined(db_event)
587+ self.harness.charm.on_database_changed(db_event)
588+ configured_spec = self.harness.get_pod_spec()
589+ self.assertEqual(configured_spec, expected_spec, 'Configured spec does not match expected pod spec.')
590+
591+ def test_charm_config_process_not_leader(self):
592+ """Test that the config process on a non-leader does not trigger a pod_spec change."""
593+ non_leader_harness = testing.Harness(DiscourseCharm)
594+ non_leader_harness.disable_hooks()
595+ non_leader_harness.set_leader(False)
596+ non_leader_harness.begin()
597+
598+ action_event = mock.Mock()
599+ non_leader_harness.update_config(self.configs['config_valid_complete']['config'])
600+ non_leader_harness.charm.configure_pod(action_event)
601+ result = non_leader_harness.get_pod_spec()
602+ self.assertEqual(result, None, 'Non-leader does not set pod spec.')
603+
604+ def test_lost_db_relation(self):
605+ """Test that losing our DB relation triggers a drop of pod config."""
606+ self.harness.update_config(self.configs['config_valid_complete']['config'])
607+ db_event = SimpleNamespace()
608+ db_event.master = None
609+ self.harness.charm.on_database_changed(db_event)
610+ configured_spec = self.harness.get_pod_spec()
611+ self.assertEqual(configured_spec, None, 'Lost DB relation does not result in empty spec as expected')
612+
613+ def test_on_database_relation_joined(self):
614+ """Test joining the DB relation."""
615+ # First test with a non-leader, confirm the database property isn't set.
616+ non_leader_harness = testing.Harness(DiscourseCharm)
617+ non_leader_harness.disable_hooks()
618+ non_leader_harness.set_leader(False)
619+ non_leader_harness.begin()
620+
621+ action_event = mock.Mock()
622+ action_event.database = None
623+ non_leader_harness.update_config(self.configs['config_valid_complete']['config'])
624+ non_leader_harness.charm.on_database_relation_joined(action_event)
625+ self.assertEqual(action_event.database, None)
626+
627+ # Now test with a leader, the database property is set.
628+ self.harness.update_config(self.configs['config_valid_complete']['config'])
629+ self.harness.charm.on_database_relation_joined(action_event)
630+ self.assertEqual(action_event.database, "discourse")

Subscribers

People subscribed via source and target branches