Merge ~jk0ne/charm-k8s-discourse/+git/charm-k8s-discourse:master into charm-k8s-discourse:master

Proposed by Jay Kuri
Status: Merged
Approved by: Tom Haddon
Approved revision: 09cc41b990631a04bfd33b51f83e3f63f308610b
Merged at revision: f1599379f9b7d3cac3c879dfe70e0ad6e3d7a87b
Proposed branch: ~jk0ne/charm-k8s-discourse/+git/charm-k8s-discourse:master
Merge into: charm-k8s-discourse:master
Diff against target: 750 lines (+206/-265)
9 files modified
Makefile (+1/-1)
image/Dockerfile (+1/-1)
src/charm.py (+67/-27)
tests/unit/fixtures/config_invalid_missing_cors.yaml (+5/-4)
tests/unit/fixtures/config_invalid_missing_db_name.yaml (+5/-5)
tests/unit/fixtures/config_valid_complete.yaml (+21/-49)
tests/unit/fixtures/config_valid_with_tls.yaml (+20/-52)
tests/unit/fixtures/config_valid_without_tls.yaml (+19/-49)
tests/unit/test_charm.py (+67/-77)
Reviewer Review Type Date Requested Status
Tom Haddon Approve
Stuart Bishop (community) Approve
Canonical IS Reviewers Pending
Discourse Charm Maintainers work-in-progress Pending
Review via email: mp+392334@code.launchpad.net

Commit message

Test rewrite per stub's feedback.

To post a comment you must log in.
Revision history for this message
Jay Kuri (jk0ne) wrote :

Updated to finish core tests and test CMR routines. This gets us to 91% coverage... and I think this is ready for review, at least internally.

Revision history for this message
Stuart Bishop (stub) wrote :

This is all looking great. Some inline comments, nothing particularly important or that needs to block landing. If you agree, changes can be made on this branch before landing or in a subsequent cleanup branch.

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

Just to add to stub's feedback, one comment about some test data.

Also, please add docstrings for all functions in src/charm.py and in the unit tests. Per review feedback that we've had on other charm changes:

Docstring's first sentence should be in one line, ending with a point (full stop). Then have a blank line and the rest of text you want.

Something like

"""Verify the config from Juju.

Specifically:

- check if all the required Juju config options are set

- check if all the Juju config options are properly formatted

:raises TelegrafK8sCharmJujuConfigError: if a required config is missing or wrongly formatted
"""

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

Some minor comments inline.

Also, per the comment above please add docstrings to all functions/methods in src/charm.py and in the unit tests.

Thanks for the test data changes. Any objection to trying to find more descriptive names for the test data files than 'config_valid_{1,2,3}.yaml', for instance? Possibly something like 'config_valid_basic.yaml', 'config_valid_with_tls.yaml', etc.? I think this makes it a lot easier to understand for someone new to the code what's being tested.

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

Some other comments from looking at the code outside of this MP.

In src/charm.py can we alpha-sort `from ops.model import MaintenanceStatus, BlockedStatus, ActiveStatus`?

Per a comment we got on the jenkins-agent charm recently, "Please use "store", not "state", for StoredState (with undercase if you want to make it private)." You'll obviously need to update a all the references to self.state as appropriate too.

In check_for_config_problems you have "if len(missing_fields):" but this could just be "if missing_fields:".

In check_config_is_valid you have "if len(errors) > 0:" but this could just be "if errors:".

Please alpha sort imports in the unit tests.

I'm not sure why you have 'dirname = os.path.dirname(__file__)' as a global variable in unit tests. Seems like you could just remove that and replace line 48 with:

self.configs = load_configs(os.path.join(os.path.dirname(__file__), 'fixtures'))

I don't think we need to import (or use) pprint in the unit tests any more? Looks like it was just for debugging of the load_configs function.

And lastly, just a suggestion, feel free to ignore if you like. In load_configs you have:

name = os.path.splitext(os.path.basename(filename))
configs[name[0]] = yaml.full_load(file)

This could be (which seems a little easier to follow to me, but YMMV):

name, _ = os.path.splitext(os.path.basename(filename))
configs[name] = yaml.full_load(file)

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

Looks good, thanks for the follow ups

review: Approve
Revision history for this message
🤖 Canonical IS Merge Bot (canonical-is-mergebot) wrote :

This merge proposal is being monitored by mergebot. Change the status to Approved to merge.

Revision history for this message
🤖 Canonical IS Merge Bot (canonical-is-mergebot) wrote :

Change successfully merged at revision f1599379f9b7d3cac3c879dfe70e0ad6e3d7a87b

Preview Diff

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

Subscribers

People subscribed via source and target branches