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
diff --git a/Makefile b/Makefile
index 00ca7aa..440712e 100644
--- a/Makefile
+++ b/Makefile
@@ -1,4 +1,4 @@
1DISCOURSE_VERSION ?= v2.5.11DISCOURSE_VERSION ?= v2.5.2
2IMAGE_VERSION ?= $(DISCOURSE_VERSION)2IMAGE_VERSION ?= $(DISCOURSE_VERSION)
3IMAGE_NAME ?=discourse3IMAGE_NAME ?=discourse
44
diff --git a/image/Dockerfile b/image/Dockerfile
index 7baf5cb..96d49df 100644
--- a/image/Dockerfile
+++ b/image/Dockerfile
@@ -13,7 +13,7 @@ ARG CONTAINER_APP_GID
1313
14# Copy any args we got into the environment.14# Copy any args we got into the environment.
15ENV CONTAINER_APP_NAME ${CONTAINER_APP_NAME:-discourse}15ENV CONTAINER_APP_NAME ${CONTAINER_APP_NAME:-discourse}
16ENV CONTAINER_APP_VERSION ${CONTAINER_APP_VERSION:-v2.4.3}16ENV CONTAINER_APP_VERSION ${CONTAINER_APP_VERSION:-v2.5.2}
17ENV CONTAINER_APP_USERNAME ${CONTAINER_APP_USERNAME:-discourse}17ENV CONTAINER_APP_USERNAME ${CONTAINER_APP_USERNAME:-discourse}
18ENV CONTAINER_APP_UID ${CONTAINER_APP_UID:-200}18ENV CONTAINER_APP_UID ${CONTAINER_APP_UID:-200}
19ENV CONTAINER_APP_GROUP ${CONTAINER_APP_GROUP:-discourse}19ENV CONTAINER_APP_GROUP ${CONTAINER_APP_GROUP:-discourse}
diff --git a/src/charm.py b/src/charm.py
index b7e5e2b..bb33602 100755
--- a/src/charm.py
+++ b/src/charm.py
@@ -7,13 +7,14 @@ from ops.charm import CharmBase
7from ops.main import main7from ops.main import main
8from ops.framework import StoredState8from ops.framework import StoredState
99
10from ops.model import MaintenanceStatus, BlockedStatus, ActiveStatus10from ops.model import ActiveStatus, BlockedStatus, MaintenanceStatus
1111
1212
13pgsql = ops.lib.use("pgsql", 1, "postgresql-charmers@lists.launchpad.net")13pgsql = ops.lib.use("pgsql", 1, "postgresql-charmers@lists.launchpad.net")
1414
1515
16def create_discourse_pod_config(config):16def create_discourse_pod_config(config):
17 """Create the pod environment config from the juju config."""
17 pod_config = {18 pod_config = {
18 'DISCOURSE_POSTGRES_USERNAME': config['db_user'],19 'DISCOURSE_POSTGRES_USERNAME': config['db_user'],
19 'DISCOURSE_POSTGRES_PASSWORD': config['db_password'],20 'DISCOURSE_POSTGRES_PASSWORD': config['db_password'],
@@ -36,6 +37,7 @@ def create_discourse_pod_config(config):
3637
3738
38def create_ingress_config(app_name, config):39def create_ingress_config(app_name, config):
40 """Create the ingress config form the juju config."""
39 annotations = {}41 annotations = {}
40 ingressResource = {42 ingressResource = {
41 "name": app_name + "-ingress",43 "name": app_name + "-ingress",
@@ -61,6 +63,13 @@ def create_ingress_config(app_name, config):
6163
6264
63def get_pod_spec(app_name, config):65def get_pod_spec(app_name, config):
66 """Get the entire pod spec using the juju config.
67
68 - uses create_discourse_pod_config() to generate pod envConfig.
69
70 - uses create_ingress_config() to generate pod ingressResources.
71
72 """
64 pod_spec = {73 pod_spec = {
65 "version": 3,74 "version": 3,
66 "containers": [75 "containers": [
@@ -92,10 +101,16 @@ def get_pod_spec(app_name, config):
92101
93102
94def check_for_config_problems(config):103def check_for_config_problems(config):
104 """Check if there are issues with the juju config.
105
106 - Primarily looks for missing config options using check_for_missing_config_fields()
107
108 - Returns a list of errors if any were found.
109 """
95 errors = []110 errors = []
96 missing_fields = check_for_missing_config_fields(config)111 missing_fields = check_for_missing_config_fields(config)
97112
98 if len(missing_fields):113 if missing_fields:
99 errors.append('Required configuration missing: {}'.format(" ".join(missing_fields)))114 errors.append('Required configuration missing: {}'.format(" ".join(missing_fields)))
100115
101 if "db_host" not in config or config["db_host"] is None:116 if "db_host" not in config or config["db_host"] is None:
@@ -105,6 +120,11 @@ def check_for_config_problems(config):
105120
106121
107def check_for_missing_config_fields(config):122def check_for_missing_config_fields(config):
123 """Check for missing fields in juju config.
124
125 - Returns a list of required fields that are either not present
126 or are empty.
127 """
108 missing_fields = []128 missing_fields = []
109129
110 needed_fields = [130 needed_fields = [
@@ -125,13 +145,18 @@ def check_for_missing_config_fields(config):
125145
126146
127class DiscourseCharm(CharmBase):147class DiscourseCharm(CharmBase):
128 state = StoredState()148 stored = StoredState()
129149
130 def __init__(self, *args):150 def __init__(self, *args):
151 """Initialization.
152
153 - Primarily sets up defaults and event handlers.
154
155 """
131 super().__init__(*args)156 super().__init__(*args)
132157
133 # TODO: is_started is unused. Remove?158 # TODO: is_started is unused. Remove?
134 self.state.set_default(is_started=False, db_user=None, db_password=None, db_host=None)159 self.stored.set_default(is_started=False, db_user=None, db_password=None, db_host=None)
135 self.framework.observe(self.on.leader_elected, self.configure_pod)160 self.framework.observe(self.on.leader_elected, self.configure_pod)
136 self.framework.observe(self.on.config_changed, self.configure_pod)161 self.framework.observe(self.on.config_changed, self.configure_pod)
137 self.framework.observe(self.on.upgrade_charm, self.configure_pod)162 self.framework.observe(self.on.upgrade_charm, self.configure_pod)
@@ -141,11 +166,17 @@ class DiscourseCharm(CharmBase):
141 self.framework.observe(self.db.on.master_changed, self.on_database_changed)166 self.framework.observe(self.db.on.master_changed, self.on_database_changed)
142167
143 def check_config_is_valid(self, config):168 def check_config_is_valid(self, config):
169 """Check that the provided config is valid.
170
171 - Returns True if config is valid, False otherwise.
172
173 - Sets model status as appropriate.
174 """
144 valid_config = True175 valid_config = True
145 errors = check_for_config_problems(config)176 errors = check_for_config_problems(config)
146177
147 # set status if we have a bad config178 # Set status if we have a bad config.
148 if len(errors) > 0:179 if errors:
149 self.model.unit.status = BlockedStatus(", ".join(errors))180 self.model.unit.status = BlockedStatus(", ".join(errors))
150 valid_config = False181 valid_config = False
151 else:182 else:
@@ -153,10 +184,13 @@ class DiscourseCharm(CharmBase):
153184
154 return valid_config185 return valid_config
155186
156 def get_pod_spec(self, config):
157 return get_pod_spec(self.framework.model.app.name, config)
158
159 def configure_pod(self, event=None):187 def configure_pod(self, event=None):
188 """Configure pod.
189
190 - Verifies config is valid and unit is leader.
191
192 - Configures pod using pod_spec generated from config.
193 """
160 # Set our status while we get configured.194 # Set our status while we get configured.
161 self.model.unit.status = MaintenanceStatus('Configuring pod')195 self.model.unit.status = MaintenanceStatus('Configuring pod')
162196
@@ -168,43 +202,49 @@ class DiscourseCharm(CharmBase):
168 config = dict(self.model.config)202 config = dict(self.model.config)
169 if not config["db_name"]:203 if not config["db_name"]:
170 config["db_name"] = self.app.name204 config["db_name"] = self.app.name
171 config["db_user"] = self.state.db_user205 config["db_user"] = self.stored.db_user
172 config["db_password"] = self.state.db_password206 config["db_password"] = self.stored.db_password
173 config["db_host"] = self.state.db_host207 config["db_host"] = self.stored.db_host
174 # Get our spec definition.208 # Get our spec definition.
175 if self.check_config_is_valid(config):209 if self.check_config_is_valid(config):
176 # Get pod spec using our app name and config210 # Get pod spec using our app name and config
177 pod_spec = self.get_pod_spec(config)211 pod_spec = get_pod_spec(self.framework.model.app.name, config)
178 # Set our pod spec.212 # Set our pod spec.
179 self.model.pod.set_spec(pod_spec)213 self.model.pod.set_spec(pod_spec)
180 self.state.is_started = True214 self.stored.is_started = True
181 self.model.unit.status = ActiveStatus()215 self.model.unit.status = ActiveStatus()
182 else:216 else:
183 self.state.is_started = True217 self.stored.is_started = True
184 self.model.unit.status = ActiveStatus()218 self.model.unit.status = ActiveStatus()
185219
186 # TODO: This is unused? Remove?
187 def on_new_client(self, event):
188 if not self.state.is_started:
189 return event.defer()
190 event.client.serve(hosts=[event.client.ingress_address], port=self.model.config['http_port'])
191
192 def on_database_relation_joined(self, event):220 def on_database_relation_joined(self, event):
221 """Event handler for a newly joined database relation.
222
223 - Sets the event.database field on the database joined event.
224
225 - Required because setting the database name is only possible
226 from inside the event handler per https://github.com/canonical/ops-lib-pgsql/issues/2
227 """
193 # Per https://github.com/canonical/ops-lib-pgsql/issues/2,228 # Per https://github.com/canonical/ops-lib-pgsql/issues/2,
194 # changing the setting in the config will not take effect,229 # changing the setting in the config will not take effect,
195 # unless the relation is dropped and recreated.230 # unless the relation is dropped and recreated.
196 event.database = self.model.config["db_name"]231 event.database = self.model.config["db_name"]
197232
198 def on_database_changed(self, event):233 def on_database_changed(self, event):
234 """Event handler for database relation change.
235
236 - Sets our database parameters based on what was provided
237 in the relation event.
238 """
199 if event.master is None:239 if event.master is None:
200 self.state.db_user = None240 self.stored.db_user = None
201 self.state.db_password = None241 self.stored.db_password = None
202 self.state.db_host = None242 self.stored.db_host = None
203 return243 return
204244
205 self.state.db_user = event.master.user245 self.stored.db_user = event.master.user
206 self.state.db_password = event.master.password246 self.stored.db_password = event.master.password
207 self.state.db_host = event.master.host247 self.stored.db_host = event.master.host
208248
209 self.configure_pod()249 self.configure_pod()
210250
diff --git a/tests/unit/fixtures/config_invalid_2.yaml b/tests/unit/fixtures/config_invalid_missing_cors.yaml
211similarity index 76%251similarity index 76%
212rename from tests/unit/fixtures/config_invalid_2.yaml252rename from tests/unit/fixtures/config_invalid_2.yaml
213rename to tests/unit/fixtures/config_invalid_missing_cors.yaml253rename to tests/unit/fixtures/config_invalid_missing_cors.yaml
index 57ec729..da4a5c8 100644
--- a/tests/unit/fixtures/config_invalid_2.yaml
+++ b/tests/unit/fixtures/config_invalid_missing_cors.yaml
@@ -2,7 +2,7 @@ config:
2 cors_origin: ''2 cors_origin: ''
3 db_name: discourse3 db_name: discourse
4 db_host: 10.25.244.124 db_host: 10.25.244.12
5 developer_emails: jay.kuri@canonical.com5 developer_emails: some.person@example.com
6 discourse_image: discourse-k8s:1.0.7f6 discourse_image: discourse-k8s:1.0.7f
7 enable_cors: true7 enable_cors: true
8 external_hostname: discourse.local8 external_hostname: discourse.local
@@ -11,9 +11,10 @@ config:
11 redis_host: 10.9.89.19711 redis_host: 10.9.89.197
12 smtp_address: 167.89.123.5812 smtp_address: 167.89.123.58
13 smtp_authentication: login13 smtp_authentication: login
14 smtp_domain: canonical.com14 smtp_domain: example.com
15 smtp_openssl_verify_mode: none15 smtp_openssl_verify_mode: none
16 smtp_password: T6DY2MzWV0AJk16 smtp_password: OBV10USLYF4K3
17 smtp_port: 58717 smtp_port: 587
18 smtp_username: apikey18 smtp_username: apikey
19expected_error_status: 'Required configuration missing: cors_origin'19missing_fields:
20 - 'cors_origin'
diff --git a/tests/unit/fixtures/config_invalid_1.yaml b/tests/unit/fixtures/config_invalid_missing_db_name.yaml
20similarity index 72%21similarity index 72%
21rename from tests/unit/fixtures/config_invalid_1.yaml22rename from tests/unit/fixtures/config_invalid_1.yaml
22rename to tests/unit/fixtures/config_invalid_missing_db_name.yaml23rename to tests/unit/fixtures/config_invalid_missing_db_name.yaml
index e5df777..0e2ea39 100644
--- a/tests/unit/fixtures/config_invalid_1.yaml
+++ b/tests/unit/fixtures/config_invalid_missing_db_name.yaml
@@ -1,7 +1,6 @@
1config:1config:
2 cors_origin: '*'2 cors_origin: '*'
3 db_name: discourse3 developer_emails: some.person@example.com
4 developer_emails: jay.kuri@canonical.com
5 discourse_image: discourse-k8s:1.0.7f4 discourse_image: discourse-k8s:1.0.7f
6 enable_cors: true5 enable_cors: true
7 external_hostname: discourse.local6 external_hostname: discourse.local
@@ -10,9 +9,10 @@ config:
10 redis_host: 10.9.89.1979 redis_host: 10.9.89.197
11 smtp_address: 167.89.123.5810 smtp_address: 167.89.123.58
12 smtp_authentication: login11 smtp_authentication: login
13 smtp_domain: canonical.com12 smtp_domain: example.com
14 smtp_openssl_verify_mode: none13 smtp_openssl_verify_mode: none
15 smtp_password: T6DY2MzWV0AJk14 smtp_password: OBV10USLYF4K3
16 smtp_port: 58715 smtp_port: 587
17 smtp_username: apikey16 smtp_username: apikey
18expected_error_status: 'db relation is required'17missing_fields:
18 - 'db_name'
diff --git a/tests/unit/fixtures/config_valid_1.yaml b/tests/unit/fixtures/config_valid_complete.yaml
19similarity index 57%19similarity index 57%
20rename from tests/unit/fixtures/config_valid_1.yaml20rename from tests/unit/fixtures/config_valid_1.yaml
21rename to tests/unit/fixtures/config_valid_complete.yaml21rename to tests/unit/fixtures/config_valid_complete.yaml
index ae6aa35..28e7367 100644
--- a/tests/unit/fixtures/config_valid_1.yaml
+++ b/tests/unit/fixtures/config_valid_complete.yaml
@@ -4,7 +4,7 @@ config:
4 db_name: discourse4 db_name: discourse
5 db_password: a_real_password5 db_password: a_real_password
6 db_user: discourse_m6 db_user: discourse_m
7 developer_emails: jay.kuri@canonical.com7 developer_emails: some.person@example.com
8 discourse_image: discourse-k8s:1.0.7f8 discourse_image: discourse-k8s:1.0.7f
9 enable_cors: true9 enable_cors: true
10 external_hostname: discourse.local10 external_hostname: discourse.local
@@ -13,54 +13,26 @@ config:
13 redis_host: 10.9.89.19713 redis_host: 10.9.89.197
14 smtp_address: 167.89.123.5814 smtp_address: 167.89.123.58
15 smtp_authentication: login15 smtp_authentication: login
16 smtp_domain: canonical.com16 smtp_domain: example.com
17 smtp_openssl_verify_mode: none17 smtp_openssl_verify_mode: none
18 smtp_password: T6DY2MzWV0AJk18 smtp_password: OBV10USLYF4K3
19 smtp_port: 58719 smtp_port: 587
20 smtp_username: apikey20 smtp_username: apikey
21spec:21 tls_secret_name: discourse_local
22 containers:22pod_config:
23 - envConfig:23 DISCOURSE_CORS_ORIGIN: '*'
24 DISCOURSE_CORS_ORIGIN: '*'24 DISCOURSE_DEVELOPER_EMAILS: some.person@example.com
25 DISCOURSE_DEVELOPER_EMAILS: jay.kuri@canonical.com25 DISCOURSE_ENABLE_CORS: true
26 DISCOURSE_ENABLE_CORS: true26 DISCOURSE_HOSTNAME: discourse.local
27 DISCOURSE_HOSTNAME: discourse.local27 DISCOURSE_POSTGRES_HOST: 10.9.89.237
28 DISCOURSE_POSTGRES_HOST: 10.9.89.23728 DISCOURSE_POSTGRES_NAME: discourse
29 DISCOURSE_POSTGRES_NAME: discourse29 DISCOURSE_POSTGRES_PASSWORD: a_real_password
30 DISCOURSE_POSTGRES_PASSWORD: a_real_password30 DISCOURSE_POSTGRES_USERNAME: discourse_m
31 DISCOURSE_POSTGRES_USERNAME: discourse_m31 DISCOURSE_REDIS_HOST: 10.9.89.197
32 DISCOURSE_REDIS_HOST: 10.9.89.19732 DISCOURSE_SMTP_ADDRESS: 167.89.123.58
33 DISCOURSE_SMTP_ADDRESS: 167.89.123.5833 DISCOURSE_SMTP_AUTHENTICATION: login
34 DISCOURSE_SMTP_AUTHENTICATION: login34 DISCOURSE_SMTP_DOMAIN: example.com
35 DISCOURSE_SMTP_DOMAIN: canonical.com35 DISCOURSE_SMTP_OPENSSL_VERIFY_MODE: none
36 DISCOURSE_SMTP_OPENSSL_VERIFY_MODE: none36 DISCOURSE_SMTP_PASSWORD: OBV10USLYF4K3
37 DISCOURSE_SMTP_PASSWORD: T6DY2MzWV0AJk37 DISCOURSE_SMTP_PORT: 587
38 DISCOURSE_SMTP_PORT: 58738 DISCOURSE_SMTP_USER_NAME: apikey
39 DISCOURSE_SMTP_USER_NAME: apikey
40 imageDetails:
41 imagePath: discourse-k8s:1.0.7f
42 imagePullPolicy: IfNotPresent
43 kubernetes:
44 readinessProbe:
45 httpGet:
46 path: /srv/status
47 port: 3000
48 name: discourse
49 ports:
50 - containerPort: 3000
51 protocol: TCP
52 kubernetesResources:
53 ingressResources:
54 - name: discourse-ingress
55 annotations:
56 nginx.ingress.kubernetes.io/ssl-redirect: 'false'
57 spec:
58 rules:
59 - host: discourse.local
60 http:
61 paths:
62 - backend:
63 serviceName: discourse
64 servicePort: 3000
65 path: /
66 version: 3
diff --git a/tests/unit/fixtures/config_valid_3.yaml b/tests/unit/fixtures/config_valid_with_tls.yaml
67similarity index 57%39similarity index 57%
68rename from tests/unit/fixtures/config_valid_3.yaml40rename from tests/unit/fixtures/config_valid_3.yaml
69rename to tests/unit/fixtures/config_valid_with_tls.yaml41rename to tests/unit/fixtures/config_valid_with_tls.yaml
index b568c5a..fa19ce1 100644
--- a/tests/unit/fixtures/config_valid_3.yaml
+++ b/tests/unit/fixtures/config_valid_with_tls.yaml
@@ -4,7 +4,7 @@ config:
4 db_name: discourse4 db_name: discourse
5 db_password: a_real_password5 db_password: a_real_password
6 db_user: discourse_m6 db_user: discourse_m
7 developer_emails: jay.kuri@canonical.com7 developer_emails: some.person@example.com
8 discourse_image: discourse-k8s:1.0.7f8 discourse_image: discourse-k8s:1.0.7f
9 enable_cors: true9 enable_cors: true
10 external_hostname: discourse.local10 external_hostname: discourse.local
@@ -13,58 +13,26 @@ config:
13 redis_host: 10.9.89.19713 redis_host: 10.9.89.197
14 smtp_address: 167.89.123.5814 smtp_address: 167.89.123.58
15 smtp_authentication: login15 smtp_authentication: login
16 smtp_domain: canonical.com16 smtp_domain: example.com
17 smtp_openssl_verify_mode: none17 smtp_openssl_verify_mode: none
18 smtp_password: T6DY2MzWV0AJk18 smtp_password: OBV10USLYF4K3
19 smtp_port: 58719 smtp_port: 587
20 smtp_username: apikey20 smtp_username: apikey
21 tls_secret_name: discourse-local21 tls_secret_name: discourse-local
22spec:22pod_config:
23 containers:23 DISCOURSE_CORS_ORIGIN: '*'
24 - envConfig:24 DISCOURSE_DEVELOPER_EMAILS: some.person@example.com
25 DISCOURSE_CORS_ORIGIN: '*'25 DISCOURSE_ENABLE_CORS: true
26 DISCOURSE_DEVELOPER_EMAILS: jay.kuri@canonical.com26 DISCOURSE_HOSTNAME: discourse.local
27 DISCOURSE_ENABLE_CORS: true27 DISCOURSE_POSTGRES_HOST: 10.9.89.237
28 DISCOURSE_HOSTNAME: discourse.local28 DISCOURSE_POSTGRES_NAME: discourse
29 DISCOURSE_POSTGRES_HOST: 10.9.89.23729 DISCOURSE_POSTGRES_PASSWORD: a_real_password
30 DISCOURSE_POSTGRES_NAME: discourse30 DISCOURSE_POSTGRES_USERNAME: discourse_m
31 DISCOURSE_POSTGRES_PASSWORD: a_real_password31 DISCOURSE_REDIS_HOST: 10.9.89.197
32 DISCOURSE_POSTGRES_USERNAME: discourse_m32 DISCOURSE_SMTP_ADDRESS: 167.89.123.58
33 DISCOURSE_REDIS_HOST: 10.9.89.19733 DISCOURSE_SMTP_AUTHENTICATION: login
34 DISCOURSE_SMTP_ADDRESS: 167.89.123.5834 DISCOURSE_SMTP_DOMAIN: example.com
35 DISCOURSE_SMTP_AUTHENTICATION: login35 DISCOURSE_SMTP_OPENSSL_VERIFY_MODE: none
36 DISCOURSE_SMTP_DOMAIN: canonical.com36 DISCOURSE_SMTP_PASSWORD: OBV10USLYF4K3
37 DISCOURSE_SMTP_OPENSSL_VERIFY_MODE: none37 DISCOURSE_SMTP_PORT: 587
38 DISCOURSE_SMTP_PASSWORD: T6DY2MzWV0AJk38 DISCOURSE_SMTP_USER_NAME: apikey
39 DISCOURSE_SMTP_PORT: 587
40 DISCOURSE_SMTP_USER_NAME: apikey
41 imageDetails:
42 imagePath: discourse-k8s:1.0.7f
43 username: discourse_role
44 password: discourse123
45 imagePullPolicy: IfNotPresent
46 kubernetes:
47 readinessProbe:
48 httpGet:
49 path: /srv/status
50 port: 3000
51 name: discourse
52 ports:
53 - containerPort: 3000
54 protocol: TCP
55 kubernetesResources:
56 ingressResources:
57 - name: discourse-ingress
58 spec:
59 tls:
60 - hosts: discourse.local
61 secretName: discourse-local
62 rules:
63 - host: discourse.local
64 http:
65 paths:
66 - backend:
67 serviceName: discourse
68 servicePort: 3000
69 path: /
70 version: 3
diff --git a/tests/unit/fixtures/config_valid_2.yaml b/tests/unit/fixtures/config_valid_without_tls.yaml
71similarity index 62%39similarity index 62%
72rename from tests/unit/fixtures/config_valid_2.yaml40rename from tests/unit/fixtures/config_valid_2.yaml
73rename to tests/unit/fixtures/config_valid_without_tls.yaml41rename to tests/unit/fixtures/config_valid_without_tls.yaml
index 9bd94ae..8c440b8 100644
--- a/tests/unit/fixtures/config_valid_2.yaml
+++ b/tests/unit/fixtures/config_valid_without_tls.yaml
@@ -4,7 +4,7 @@ config:
4 db_name: discourse4 db_name: discourse
5 db_password: a_real_password5 db_password: a_real_password
6 db_user: discourse_m6 db_user: discourse_m
7 developer_emails: is-admin@canonical.com7 developer_emails: is-admin@example.com
8 discourse_image: discourse-k8s:1.0.88 discourse_image: discourse-k8s:1.0.8
9 enable_cors: true9 enable_cors: true
10 external_hostname: discourse.example.com10 external_hostname: discourse.example.com
@@ -13,55 +13,25 @@ config:
13 redis_host: 10.25.242.1213 redis_host: 10.25.242.12
14 smtp_address: smtp.internal14 smtp_address: smtp.internal
15 smtp_authentication: login15 smtp_authentication: login
16 smtp_domain: canonical.com16 smtp_domain: example.com
17 smtp_openssl_verify_mode: none17 smtp_openssl_verify_mode: none
18 smtp_password:18 smtp_password:
19 smtp_port: 58719 smtp_port: 587
20 smtp_username: apikey20 smtp_username: apikey
21spec:21pod_config:
22 containers:22 DISCOURSE_CORS_ORIGIN: '*'
23 - envConfig:23 DISCOURSE_DEVELOPER_EMAILS: is-admin@example.com
24 DISCOURSE_CORS_ORIGIN: '*'24 DISCOURSE_ENABLE_CORS: true
25 DISCOURSE_DEVELOPER_EMAILS: is-admin@canonical.com25 DISCOURSE_HOSTNAME: discourse.example.com
26 DISCOURSE_ENABLE_CORS: true26 DISCOURSE_POSTGRES_HOST: 10.9.89.237
27 DISCOURSE_HOSTNAME: discourse.example.com27 DISCOURSE_POSTGRES_NAME: discourse
28 DISCOURSE_POSTGRES_HOST: 10.9.89.23728 DISCOURSE_POSTGRES_PASSWORD: a_real_password
29 DISCOURSE_POSTGRES_NAME: discourse29 DISCOURSE_POSTGRES_USERNAME: discourse_m
30 DISCOURSE_POSTGRES_PASSWORD: a_real_password30 DISCOURSE_REDIS_HOST: 10.25.242.12
31 DISCOURSE_POSTGRES_USERNAME: discourse_m31 DISCOURSE_SMTP_ADDRESS: smtp.internal
32 DISCOURSE_REDIS_HOST: 10.25.242.1232 DISCOURSE_SMTP_AUTHENTICATION: login
33 DISCOURSE_SMTP_ADDRESS: smtp.internal33 DISCOURSE_SMTP_DOMAIN: example.com
34 DISCOURSE_SMTP_AUTHENTICATION: login34 DISCOURSE_SMTP_OPENSSL_VERIFY_MODE: none
35 DISCOURSE_SMTP_DOMAIN: canonical.com35 DISCOURSE_SMTP_PASSWORD: null
36 DISCOURSE_SMTP_OPENSSL_VERIFY_MODE: none36 DISCOURSE_SMTP_PORT: 587
37 DISCOURSE_SMTP_PASSWORD: null37 DISCOURSE_SMTP_USER_NAME: apikey
38 DISCOURSE_SMTP_PORT: 587
39 DISCOURSE_SMTP_USER_NAME: apikey
40 imageDetails:
41 imagePath: discourse-k8s:1.0.8
42 imagePullPolicy: IfNotPresent
43 kubernetes:
44 readinessProbe:
45 httpGet:
46 path: /srv/status
47 port: 3000
48 name: discourse
49 ports:
50 - containerPort: 3000
51 protocol: TCP
52 kubernetesResources:
53 ingressResources:
54 - name: discourse-ingress
55 annotations:
56 nginx.ingress.kubernetes.io/ssl-redirect: 'false'
57 spec:
58 rules:
59 - host: discourse.example.com
60 http:
61 paths:
62 - backend:
63 serviceName: discourse
64 servicePort: 3000
65 path: /
66 version: 3
67
diff --git a/tests/unit/test_charm.py b/tests/unit/test_charm.py
index 4fd7324..214bfa1 100644
--- a/tests/unit/test_charm.py
+++ b/tests/unit/test_charm.py
@@ -3,40 +3,41 @@
3# Copyright 2020 Canonical Ltd.3# Copyright 2020 Canonical Ltd.
4# See LICENSE file for licensing details.4# See LICENSE file for licensing details.
55
6import os6import copy
7import glob7import glob
8import mock
9import os
8import unittest10import unittest
9import yaml11import yaml
10import mock
11from pprint import pprint
1212
13from charm import DiscourseCharm, BlockedStatus13from types import SimpleNamespace
14
15from charm import (
16 check_for_missing_config_fields,
17 create_discourse_pod_config,
18 get_pod_spec,
19 DiscourseCharm,
20)
1421
15from ops import testing22from ops import testing
1623
17dirname = os.path.dirname(__file__)24
18
19
20# Valid and Invalid configs are present in the fixtures
21# directory. The files contain the juju config, along with
22# the spec that that config should produce in the case of
23# valid configs. In the case of invalid configs, they contain
24# the juju config and the error that should be triggered by
25# the config. These scenarios are tested below. Additional
26# config variations can be created by creating an appropriately
27# named config file in the fixtures directory. Valid configs
28# should be named: config_valid_###.yaml and invalid configs
29# should be named: config_invalid_###.yaml.
30#
31# This function loads the configs for use by the tests.
32def load_configs(directory):25def load_configs(directory):
26 """Load configs for use by tests.
27
28 Valid and invalid configs are present in the fixtures directory. The files
29 contain the juju config, along with the spec that that config should
30 produce in the case of valid configs. In the case of invalid configs, they
31 contain the juju config and the error that should be triggered by the
32 config. These scenarios are tested below. Additional config variations can
33 be created by creating an appropriately named config file in the fixtures
34 directory. Valid configs should be named: config_valid_###.yaml and invalid
35 configs should be named: config_invalid_###.yaml."""
33 configs = {}36 configs = {}
34 for filename in glob.glob(os.path.join(directory, 'config*.yaml')):37 for filename in glob.glob(os.path.join(directory, 'config*.yaml')):
35 pprint(filename)
36 with open(filename) as file:38 with open(filename) as file:
37 name = os.path.splitext(os.path.basename(filename))39 name, _ = os.path.splitext(os.path.basename(filename))
38 pprint(name[0])40 configs[name] = yaml.full_load(file)
39 configs[name[0]] = yaml.full_load(file)
40 return configs41 return configs
4142
4243
@@ -46,81 +47,70 @@ class TestDiscourseK8sCharmHooksDisabled(unittest.TestCase):
46 self.harness.disable_hooks()47 self.harness.disable_hooks()
47 self.harness.set_leader(True)48 self.harness.set_leader(True)
48 self.harness.begin()49 self.harness.begin()
49 self.configs = load_configs(os.path.join(dirname, 'fixtures'))50 self.configs = load_configs(os.path.join(os.path.dirname(__file__), 'fixtures'))
5051
51 def test_valid_configs_are_ok(self):52 def test_valid_configs_are_ok(self):
52 """Test that a valid config is considered valid."""53 """Test that a valid config is considered valid."""
53 for config_key in self.configs:54 for config_key in self.configs:
54 if config_key.startswith('config_valid_'):55 if config_key.startswith('config_valid_'):
55 self.harness.update_config(self.configs[config_key]['config'])56 config_valid = self.harness.charm.check_config_is_valid(self.configs[config_key]['config'])
56 valid_config = self.harness.charm.check_config_is_valid(self.configs[config_key]['config'])57 pod_config = create_discourse_pod_config(self.configs[config_key]['config'])
57 self.assertEqual(valid_config, True, 'Valid config {} is not recognized as valid.'.format(config_key))58 self.assertEqual(config_valid, True, 'Valid config is not recognized as valid')
58
59 def test_charm_identifies_bad_configs(self):
60 """Test that bad configs are identified by the charm."""
61 for config_key in self.configs:
62 if config_key.startswith('config_invalid_'):
63 self.harness.update_config(self.configs[config_key]['config'])
64 valid_config = self.harness.charm.check_config_is_valid(self.configs[config_key]['config'])
65 self.assertEqual(valid_config, False, 'Bad Config {} is recognized.'.format(config_key))
66 self.assertEqual(59 self.assertEqual(
67 self.harness.charm.model.unit.status,60 pod_config,
68 BlockedStatus(self.configs[config_key]['expected_error_status']),61 self.configs[config_key]['pod_config'],
69 'Invalid config {} does not produce correct status'.format(config_key),62 'Valid config {} is does not produce expected config options for pod.'.format(config_key),
70 )63 )
7164
72 def test_charm_creates_valid_ingress_config(self):65 def test_invalid_configs_are_recognized(self):
73 """Test that a valid config creates a valid ingress spec."""66 """Test that bad configs are identified by the charm."""
74 for config_key in self.configs:
75 if config_key.startswith('config_valid_'):
76 self.harness.update_config(self.configs[config_key]['config'])
77 spec = self.harness.charm.get_pod_spec(self.configs[config_key]['config'])
78 self.assertEqual(
79 spec['kubernetesResources']['ingressResources'],
80 self.configs[config_key]['spec']['kubernetesResources']['ingressResources'],
81 'Valid config {} does not produce expected ingress config.'.format(config_key),
82 )
83
84 def test_valid_pod_spec(self):
85 """A valid config results in a valid pod spec."""
86 for config_key in self.configs:67 for config_key in self.configs:
87 if config_key.startswith('config_valid_'):68 if config_key.startswith('config_invalid_'):
88 self.harness.update_config(self.configs[config_key]['config'])69 config_valid = self.harness.charm.check_config_is_valid(self.configs[config_key]['config'])
89 spec = self.harness.charm.get_pod_spec(self.configs[config_key]['config'])70 missing_fields = check_for_missing_config_fields(self.configs[config_key]['config'])
71 self.assertEqual(config_valid, False, 'Invalid config is not recognized as invalid')
90 self.assertEqual(72 self.assertEqual(
91 spec,73 missing_fields,
92 self.configs[config_key]['spec'],74 self.configs[config_key]['missing_fields'],
93 'Valid config {} does not produce expected pod spec.'.format(config_key),75 'Invalid config {} does not fail as expected.'.format(config_key),
94 )76 )
9577
96 def test_charm_config_process(self):78 def test_charm_config_process(self):
97 action_event = mock.Mock()79 """Test that the entire config process for the charm works."""
98 self.harness.update_config(self.configs['config_valid_1']['config'])80 test_config = copy.deepcopy(self.configs['config_valid_complete']['config'])
99 self.harness.charm.configure_pod(action_event)81 test_config['db_user'] = 'discourse_m'
100 (configured_spec, k8s_resources) = self.harness.get_pod_spec()82 test_config['db_password'] = 'a_real_password'
101 self.assertEqual(83 test_config['db_host'] = '10.9.89.237'
102 configured_spec,84 db_event = SimpleNamespace()
103 self.configs['config_valid_1']['spec'],85 db_event.master = SimpleNamespace()
104 'Valid config does not cause charm to set expected pod spec.',86 db_event.master.user = 'discourse_m'
105 )87 db_event.master.password = 'a_real_password'
10688 db_event.master.host = '10.9.89.237'
107 def test_charm_config_process_invalid_config(self):89 expected_spec = (get_pod_spec(self.harness.charm.framework.model.app.name, test_config), None)
108 action_event = mock.Mock()90 self.harness.update_config(self.configs['config_valid_complete']['config'])
109 self.harness.update_config(self.configs['config_invalid_1']['config'])91 self.harness.charm.on_database_relation_joined(db_event)
110 self.harness.charm.configure_pod(action_event)92 self.harness.charm.on_database_changed(db_event)
111 result = self.harness.get_pod_spec()93 configured_spec = self.harness.get_pod_spec()
112 pprint(result)94 self.assertEqual(configured_spec, expected_spec, 'Configured spec does not match expected pod spec.')
113 self.assertEqual(result, None, 'Invalid config does not get set as pod spec.')
11495
115 def test_charm_config_process_not_leader(self):96 def test_charm_config_process_not_leader(self):
97 """Test that the config process on a non-leader does not trigger a pod_spec change."""
116 non_leader_harness = testing.Harness(DiscourseCharm)98 non_leader_harness = testing.Harness(DiscourseCharm)
117 non_leader_harness.disable_hooks()99 non_leader_harness.disable_hooks()
118 non_leader_harness.set_leader(False)100 non_leader_harness.set_leader(False)
119 non_leader_harness.begin()101 non_leader_harness.begin()
120102
121 action_event = mock.Mock()103 action_event = mock.Mock()
122 non_leader_harness.update_config(self.configs['config_valid_1']['config'])104 non_leader_harness.update_config(self.configs['config_valid_complete']['config'])
123 non_leader_harness.charm.configure_pod(action_event)105 non_leader_harness.charm.configure_pod(action_event)
124 result = non_leader_harness.get_pod_spec()106 result = non_leader_harness.get_pod_spec()
125 pprint(result)
126 self.assertEqual(result, None, 'Non-leader does not set pod spec.')107 self.assertEqual(result, None, 'Non-leader does not set pod spec.')
108
109 def test_lost_db_relation(self):
110 """Test that losing our DB relation triggers a drop of pod config."""
111 self.harness.update_config(self.configs['config_valid_complete']['config'])
112 db_event = SimpleNamespace()
113 db_event.master = None
114 self.harness.charm.on_database_changed(db_event)
115 configured_spec = self.harness.get_pod_spec()
116 self.assertEqual(configured_spec, None, 'Lost DB relation does not result in empty spec as expected')

Subscribers

People subscribed via source and target branches