Merge ~jk0ne/charm-k8s-discourse/+git/charm-k8s-discourse:master into charm-k8s-discourse:master
- Git
- lp:~jk0ne/charm-k8s-discourse/+git/charm-k8s-discourse
- master
- Merge into master
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) |
Related bugs: |
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.
Description of the change
Jay Kuri (jk0ne) wrote : | # |
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.
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 TelegrafK8sChar
"""
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_
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_
In check_config_
Please alpha sort imports in the unit tests.
I'm not sure why you have 'dirname = os.path.
self.configs = load_configs(
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.
configs[name[0]] = yaml.full_
This could be (which seems a little easier to follow to me, but YMMV):
name, _ = os.path.
configs[name] = yaml.full_
Tom Haddon (mthaddon) wrote : | # |
Looks good, thanks for the follow ups
🤖 Canonical IS Merge Bot (canonical-is-mergebot) wrote : | # |
This merge proposal is being monitored by mergebot. Change the status to Approved to merge.
🤖 Canonical IS Merge Bot (canonical-is-mergebot) wrote : | # |
Change successfully merged at revision f1599379f9b7d3c
Preview Diff
1 | diff --git a/Makefile b/Makefile |
2 | index 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 | |
11 | diff --git a/image/Dockerfile b/image/Dockerfile |
12 | index 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} |
24 | diff --git a/src/charm.py b/src/charm.py |
25 | index 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 | |
222 | diff --git a/tests/unit/fixtures/config_invalid_2.yaml b/tests/unit/fixtures/config_invalid_missing_cors.yaml |
223 | similarity index 76% |
224 | rename from tests/unit/fixtures/config_invalid_2.yaml |
225 | rename to tests/unit/fixtures/config_invalid_missing_cors.yaml |
226 | index 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' |
252 | diff --git a/tests/unit/fixtures/config_invalid_1.yaml b/tests/unit/fixtures/config_invalid_missing_db_name.yaml |
253 | similarity index 72% |
254 | rename from tests/unit/fixtures/config_invalid_1.yaml |
255 | rename to tests/unit/fixtures/config_invalid_missing_db_name.yaml |
256 | index 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' |
282 | diff --git a/tests/unit/fixtures/config_valid_1.yaml b/tests/unit/fixtures/config_valid_complete.yaml |
283 | similarity index 57% |
284 | rename from tests/unit/fixtures/config_valid_1.yaml |
285 | rename to tests/unit/fixtures/config_valid_complete.yaml |
286 | index 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 |
373 | diff --git a/tests/unit/fixtures/config_valid_3.yaml b/tests/unit/fixtures/config_valid_with_tls.yaml |
374 | similarity index 57% |
375 | rename from tests/unit/fixtures/config_valid_3.yaml |
376 | rename to tests/unit/fixtures/config_valid_with_tls.yaml |
377 | index 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 |
467 | diff --git a/tests/unit/fixtures/config_valid_2.yaml b/tests/unit/fixtures/config_valid_without_tls.yaml |
468 | similarity index 62% |
469 | rename from tests/unit/fixtures/config_valid_2.yaml |
470 | rename to tests/unit/fixtures/config_valid_without_tls.yaml |
471 | index 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 |
557 | diff --git a/tests/unit/test_charm.py b/tests/unit/test_charm.py |
558 | index 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') |
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.