Merge ~mthaddon/charm-k8s-gunicorn/+git/charm-k8s-gunicorn:pebble into charm-k8s-gunicorn:master

Proposed by Tom Haddon on 2021-03-11
Status: Work in progress
Proposed branch: ~mthaddon/charm-k8s-gunicorn/+git/charm-k8s-gunicorn:pebble
Merge into: charm-k8s-gunicorn:master
Diff against target: 580 lines (+332/-89) (has conflicts)
6 files modified
lib/charms/nginx_ingress_integrator/v0/ingress.py (+174/-0)
metadata.yaml (+18/-0)
requirements.txt (+1/-1)
src/charm.py (+136/-66)
tests/unit/scenario.py (+3/-0)
tests/unit/test_charm.py (+0/-22)
Conflict in metadata.yaml
Conflict in src/charm.py
Conflict in tests/unit/scenario.py
Reviewer Review Type Date Requested Status
gunicorn-charmers 2021-03-11 Pending
Review via email: mp+399520@code.launchpad.net

Commit message

Test of pebble version of the charm

Description of the change

Test of pebble version of the charm.

$ git clone -b 2.9 https://github.com/juju/juju
$ cd juju
$ make install
$ make microk8s-operator-update # to make the microk8s image and push to Docker
$ export PATH="/home/${USER}/go/bin:$PATH"
$ juju bootstrap microk8s
$ juju add-model gunicorn
$ juju deploy ./gunicorn.charm --resource gunicorn-image='gunicorncharmers/gunicorn-app:20.0.4-20.04_edge'

Once deployed, check juju status for the IP address and then visit that in a browser. You should see something like:

------------------
One of the nice things about the new operator framework is how easy it is to get started.
KUBERNETES_SERVICE_PORT_HTTPS: 443
KUBERNETES_SERVICE_PORT: 443
GUNICORN_PORT: tcp://10.152.183.237:65535
HOSTNAME: gunicorn-0
JUJU_CONTAINER_NAME: gunicorn
MODELOPERATOR_PORT_17071_TCP_ADDR: 10.152.183.72
GUNICORN_SERVICE_HOST: 10.152.183.237
PEBBLE_SOCKET: /charm/container/pebble.socket
PWD: /srv/gunicorn
HOME: /root
KUBERNETES_PORT_443_TCP: tcp://10.152.183.1:443
GUNICORN_PORT_65535_TCP_PROTO: tcp
MODELOPERATOR_SERVICE_HOST: 10.152.183.72
GUNICORN_PORT_65535_TCP_ADDR: 10.152.183.237
GUNICORN_SERVICE_PORT_PLACEHOLDER: 65535
GUNICORN_PORT_65535_TCP_PORT: 65535
APP_WSGI: app:app
APP_NAME: my-awesome-app
SHLVL: 0
MODELOPERATOR_PORT_17071_TCP: tcp://10.152.183.72:17071
KUBERNETES_PORT_443_TCP_PROTO: tcp
KUBERNETES_PORT_443_TCP_ADDR: 10.152.183.1
MODELOPERATOR_PORT_17071_TCP_PORT: 17071
GUNICORN_PORT_65535_TCP: tcp://10.152.183.237:65535
MODELOPERATOR_PORT: tcp://10.152.183.72:17071
KUBERNETES_SERVICE_HOST: 10.152.183.1
KUBERNETES_PORT: tcp://10.152.183.1:443
KUBERNETES_PORT_443_TCP_PORT: 443
PATH: /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
GUNICORN_SERVICE_PORT: 65535
MODELOPERATOR_PORT_17071_TCP_PROTO: tcp
DEBIAN_FRONTEND: noninteractive
MODELOPERATOR_SERVICE_PORT: 17071
LC_CTYPE: C.UTF-8
SERVER_SOFTWARE: gunicorn/20.0.4
------------------

To test juju config updates, create a yaml file with the following sample contents:

FOOD: burgers
DRINK: ale

Then run `juju config gunicorn environment=@${PATH_TO_FILENAME}`.

This charm handles an upgrade-charm hook (assuming it's not failing due to config issues as above.

Next steps:
- Handle relations.
- Update unit tests.
- Document missing functionality versus the current implementation.

To post a comment you must log in.
786a573... by Tom Haddon on 2021-03-11

Comment out series in metadata.yaml and add empty manifest.yaml

eba7f4a... by Tom Haddon on 2021-03-11

Manifest yaml needs an empty dict, try to include env vars in pebble config by defining and then dumping to yaml

52d1486... by Tom Haddon on 2021-03-11

Apply black formatting

373c49c... by Tom Haddon on 2021-03-12

Fix up the pebble config structure based on feedback from jnsgruk

096a11a... by Tom Haddon on 2021-03-12

No need to run _make_pod_env twice

6b856a5... by Tom Haddon on 2021-03-12

Update to use specific version of the operator framework that snappass-test uses

e53d086... by Tom Haddon on 2021-03-12

Remove image configuration since we're switching to oci resources

6abe151... by Tom Haddon on 2021-03-12

Remove unused textwrap

c5ec8a4... by Tom Haddon on 2021-03-12

Set a default external hostname for easier devel deploys

635d741... by Tom Haddon on 2021-03-12

Get OCI image details so we can get past pod-spec-set

2b5702f... by Tom Haddon on 2021-03-12

Comment out legacy hooks for now, only pass pod_env_config if we have some to pass

966c4b3... by Tom Haddon on 2021-03-15

Begin trying to handle config changed hook

7402e87... by Tom Haddon on 2021-03-15

Refactor how we generate pebble config

e20825e... by Tom Haddon on 2021-03-15

Remove unused copy module

610fe9e... by Tom Haddon on 2021-03-15

Make the return values of the get_pebble_config function of a consistent type, improve logging of pebble_config

fc03593... by Tom Haddon on 2021-03-15

Remove unused code, migrate checks for template variables into the right place

01b829b... by Tom Haddon on 2021-03-16

Remove obsolete tests, handle upgrade-charm hook

24dbd63... by Tom Haddon on 2021-03-16

Remove commented hook handlers - no longer needed as a reference

b4f9f6a... by Tom Haddon on 2021-03-16

Switch to archive zip so we always get the latest version of the framework

688b00e... by Tom Haddon on 2021-03-21

Pin to specific version of the operator framework to avoid needing to rebuild juju with version that supports pebble_ready vs. workload_ready

6d3bdd5... by Tom Haddon on 2021-03-25

Update for new pebble events and metadata.yaml changes

fa0fe9e... by Tom Haddon on 2021-03-26

Metadata updates for rc8

e993422... by Tom Haddon on 2021-03-26

Implement the ingress relation

295358c... by Tom Haddon on 2021-03-29

Use application rather than unit data for ingress relation

6d1a2da... by Tom Haddon on 2021-03-30

Pass a dict directly to pebble now it supports that, add a comment about is_running helper method

5969f8f... by Tom Haddon on 2021-03-31

Use the application name rather than the model name to create the ingress service

f8c5a96... by Tom Haddon on 2021-03-31

Include return value hint for ingress relation changed handler

203f1a7... by Tom Haddon on 2021-04-02

Use the new ingress library

2cef81d... by Tom Haddon on 2021-04-02

Increment patch number

59862e5... by Tom Haddon on 2021-04-06

Update ingress relation, and handle config changes

ff3ed82... by Tom Haddon on 2021-04-08

Use new approach to ingress

0e93a5d... by Tom Haddon on 2021-04-08

Use ServiceStatus to determine whether to stop and start the service

309793e... by Tom Haddon on 2021-04-12

Switch to nginx-ingress-integrator library

c3f137e... by Tom Haddon on 2021-04-15

Remove manifest.yaml which is now machine-generated

Unmerged commits

c3f137e... by Tom Haddon on 2021-04-15

Remove manifest.yaml which is now machine-generated

309793e... by Tom Haddon on 2021-04-12

Switch to nginx-ingress-integrator library

0e93a5d... by Tom Haddon on 2021-04-08

Use ServiceStatus to determine whether to stop and start the service

ff3ed82... by Tom Haddon on 2021-04-08

Use new approach to ingress

59862e5... by Tom Haddon on 2021-04-06

Update ingress relation, and handle config changes

2cef81d... by Tom Haddon on 2021-04-02

Increment patch number

203f1a7... by Tom Haddon on 2021-04-02

Use the new ingress library

f8c5a96... by Tom Haddon on 2021-03-31

Include return value hint for ingress relation changed handler

5969f8f... by Tom Haddon on 2021-03-31

Use the application name rather than the model name to create the ingress service

6d1a2da... by Tom Haddon on 2021-03-30

Pass a dict directly to pebble now it supports that, add a comment about is_running helper method

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/lib/charms/nginx_ingress_integrator/v0/ingress.py b/lib/charms/nginx_ingress_integrator/v0/ingress.py
2new file mode 100644
3index 0000000..65d08a7
4--- /dev/null
5+++ b/lib/charms/nginx_ingress_integrator/v0/ingress.py
6@@ -0,0 +1,174 @@
7+"""Library for the ingress relation.
8+
9+This library contains the Requires and Provides classes for handling
10+the ingress interface.
11+
12+Import `IngressRequires` in your charm, with two required options:
13+ - "self" (the charm itself)
14+ - config_dict
15+
16+`config_dict` accepts the following keys:
17+ - service-hostname (required)
18+ - service-name (required)
19+ - service-port (required)
20+ - limit-rps
21+ - limit-whitelist
22+ - max_body-size
23+ - retry-errors
24+ - service-namespace
25+ - session-cookie-max-age
26+ - tls-secret-name
27+
28+See `config.yaml` for descriptions of each, along with the required type.
29+
30+As an example:
31+```
32+from charms.nginx_ingress_integrator.v0.ingress import IngressRequires
33+
34+# In your charm's `__init__` method.
35+self.ingress = IngressRequires(self, {"service-hostname": self.config["external_hostname"],
36+ "service-name": self.app.name,
37+ "service-port": 80})
38+
39+# In your charm's `config-changed` handler.
40+self.ingress.update_config({"service-hostname": self.config["external_hostname"]})
41+```
42+"""
43+
44+import logging
45+
46+from ops.charm import CharmEvents
47+from ops.framework import EventBase, EventSource, Object
48+from ops.model import BlockedStatus
49+
50+# The unique Charmhub library identifier, never change it
51+LIBID = "db0af4367506491c91663468fb5caa4c"
52+
53+# Increment this major API version when introducing breaking changes
54+LIBAPI = 0
55+
56+# Increment this PATCH version before using `charmcraft push-lib` or reset
57+# to 0 if you are raising the major API version
58+LIBPATCH = 1
59+
60+logger = logging.getLogger(__name__)
61+
62+REQUIRED_INGRESS_RELATION_FIELDS = {
63+ "service-hostname",
64+ "service-name",
65+ "service-port",
66+}
67+
68+OPTIONAL_INGRESS_RELATION_FIELDS = {
69+ "limit-rps",
70+ "limit-whitelist",
71+ "max-body-size",
72+ "retry-errors",
73+ "service-namespace",
74+ "session-cookie-max-age",
75+ "tls-secret-name",
76+}
77+
78+
79+class IngressAvailableEvent(EventBase):
80+ pass
81+
82+
83+class IngressCharmEvents(CharmEvents):
84+ """Custom charm events."""
85+
86+ ingress_available = EventSource(IngressAvailableEvent)
87+
88+
89+class IngressRequires(Object):
90+ """This class defines the functionality for the 'requires' side of the 'ingress' relation.
91+
92+ Hook events observed:
93+ - relation-changed
94+ """
95+
96+ def __init__(self, charm, config_dict):
97+ super().__init__(charm, "ingress")
98+
99+ self.framework.observe(charm.on["ingress"].relation_changed, self._on_relation_changed)
100+
101+ self.config_dict = config_dict
102+
103+ def _config_dict_errors(self, update_only=False):
104+ """Check our config dict for errors."""
105+ block_status = False
106+ unknown = [
107+ x for x in self.config_dict if x not in REQUIRED_INGRESS_RELATION_FIELDS | OPTIONAL_INGRESS_RELATION_FIELDS
108+ ]
109+ if unknown:
110+ logger.error("Unknown key(s) in config dictionary found: %s", ", ".join(unknown))
111+ block_status = True
112+ if not update_only:
113+ missing = [x for x in REQUIRED_INGRESS_RELATION_FIELDS if x not in self.config_dict]
114+ if missing:
115+ logger.error("Missing required key(s) in config dictionary: %s", ", ".join(missing))
116+ block_status = True
117+ if block_status:
118+ self.model.unit.status = BlockedStatus("Error in ingress relation, check `juju debug-log`")
119+ return True
120+ return False
121+
122+ def _on_relation_changed(self, event):
123+ """Handle the relation-changed event."""
124+ # `self.unit` isn't available here, so use `self.model.unit`.
125+ if self.model.unit.is_leader():
126+ if self._config_dict_errors():
127+ return
128+ for key in self.config_dict:
129+ event.relation.data[self.model.app][key] = str(self.config_dict[key])
130+
131+ def update_config(self, config_dict):
132+ """Allow for updates to relation."""
133+ if self.model.unit.is_leader():
134+ self.config_dict = config_dict
135+ if self._config_dict_errors(update_only=True):
136+ return
137+ relation = self.model.get_relation("ingress")
138+ if relation:
139+ for key in self.config_dict:
140+ relation.data[self.model.app][key] = str(self.config_dict[key])
141+
142+
143+class IngressProvides(Object):
144+ """This class defines the functionality for the 'provides' side of the 'ingress' relation.
145+
146+ Hook events observed:
147+ - relation-changed
148+ """
149+
150+ def __init__(self, charm):
151+ super().__init__(charm, "ingress")
152+ # Observe the relation-changed hook event and bind
153+ # self.on_relation_changed() to handle the event.
154+ self.framework.observe(charm.on["ingress"].relation_changed, self._on_relation_changed)
155+ self.charm = charm
156+
157+ def _on_relation_changed(self, event):
158+ """Handle a change to the ingress relation.
159+
160+ Confirm we have the fields we expect to receive."""
161+ # `self.unit` isn't available here, so use `self.model.unit`.
162+ if not self.model.unit.is_leader():
163+ return
164+
165+ ingress_data = {
166+ field: event.relation.data[event.app].get(field)
167+ for field in REQUIRED_INGRESS_RELATION_FIELDS | OPTIONAL_INGRESS_RELATION_FIELDS
168+ }
169+
170+ missing_fields = sorted(
171+ [field for field in REQUIRED_INGRESS_RELATION_FIELDS if ingress_data.get(field) is None]
172+ )
173+
174+ if missing_fields:
175+ logger.error("Missing required data fields for ingress relation: {}".format(", ".join(missing_fields)))
176+ self.model.unit.status = BlockedStatus("Missing fields for ingress: {}".format(", ".join(missing_fields)))
177+
178+ # Create an event that our charm can use to decide it's okay to
179+ # configure the ingress.
180+ self.charm.on.ingress_available.emit()
181diff --git a/metadata.yaml b/metadata.yaml
182index b1a1aa7..b6b36af 100644
183--- a/metadata.yaml
184+++ b/metadata.yaml
185@@ -5,6 +5,7 @@ description: |
186 Gunicorn charm
187 summary: |
188 Gunicorn charm
189+<<<<<<< metadata.yaml
190 series: [kubernetes]
191 min-juju-version: 2.8.0 # charm storage in state
192 resources:
193@@ -13,6 +14,21 @@ resources:
194 description: docker image for Gunicorn
195 auto-fetch: true
196 upstream-source: 'gunicorncharmers/gunicorn-app:20.0.4-20.04_edge'
197+=======
198+bases:
199+ - name: ubuntu
200+ channel: 20.04/stable
201+
202+containers:
203+ gunicorn:
204+ resource: gunicorn-image
205+
206+resources:
207+ gunicorn-image:
208+ type: oci-image
209+ description: Docker image for gunicorn to run
210+
211+>>>>>>> metadata.yaml
212 requires:
213 pg:
214 interface: pgsql
215@@ -20,3 +36,5 @@ requires:
216 influxdb:
217 interface: influxdb-api
218 limit: 1
219+ ingress:
220+ interface: ingress
221diff --git a/requirements.txt b/requirements.txt
222index 6b14e66..9313aff 100644
223--- a/requirements.txt
224+++ b/requirements.txt
225@@ -1,3 +1,3 @@
226-ops
227+https://github.com/canonical/operator/archive/refs/heads/master.zip
228 ops-lib-pgsql
229 https://github.com/juju-solutions/resource-oci-image/archive/master.zip
230diff --git a/src/charm.py b/src/charm.py
231index c25df78..13524f7 100755
232--- a/src/charm.py
233+++ b/src/charm.py
234@@ -6,6 +6,7 @@ from jinja2 import Environment, BaseLoader, meta
235 import logging
236 import yaml
237
238+from charms.nginx_ingress_integrator.v0.ingress import IngressRequires
239 import ops
240 from oci_image import OCIImageResource, OCIImageResourceError
241 from ops.framework import StoredState
242@@ -14,8 +15,8 @@ from ops.main import main
243 from ops.model import (
244 ActiveStatus,
245 BlockedStatus,
246- MaintenanceStatus,
247 )
248+from ops.pebble import ServiceStatus
249 import pgsql
250
251
252@@ -28,13 +29,13 @@ JUJU_CONFIG_YAML_DICT_ITEMS = ['environment']
253 class GunicornK8sCharmJujuConfigError(Exception):
254 """Exception when the Juju config is bad."""
255
256- pass
257-
258
259 class GunicornK8sCharmYAMLError(Exception):
260 """Exception raised when parsing YAML fails"""
261
262- pass
263+
264+class GunicornK8sWaitingForRelationsError(Exception):
265+ """Exception when waiting for relations."""
266
267
268 class GunicornK8sCharm(CharmBase):
269@@ -43,18 +44,134 @@ class GunicornK8sCharm(CharmBase):
270 def __init__(self, *args):
271 super().__init__(*args)
272
273+<<<<<<< src/charm.py
274 self.image = OCIImageResource(self, 'gunicorn-image')
275
276 self.framework.observe(self.on.start, self._configure_pod)
277 self.framework.observe(self.on.config_changed, self._configure_pod)
278 self.framework.observe(self.on.leader_elected, self._configure_pod)
279 self.framework.observe(self.on.upgrade_charm, self._configure_pod)
280+=======
281+ self.framework.observe(self.on.config_changed, self._on_config_changed)
282+ self.framework.observe(self.on.upgrade_charm, self._on_upgrade_charm)
283+ self.framework.observe(self.on.gunicorn_pebble_ready, self._on_gunicorn_pebble_ready)
284+
285+ self.ingress = IngressRequires(
286+ self,
287+ {
288+ "service-hostname": self.config["external_hostname"],
289+ "service-name": self.app.name,
290+ "service-port": 80,
291+ },
292+ )
293+>>>>>>> src/charm.py
294
295- # For special-cased relations
296- self._stored.set_default(reldata={})
297+ self._stored.set_default(
298+ gunicorn_pebble_ready=False,
299+ reldata={},
300+ )
301
302 self._init_postgresql_relation()
303
304+ def _get_pebble_config(self, event: ops.framework.EventBase) -> dict:
305+ """Generate pebble config."""
306+ pebble_config = {
307+ "summary": "gunicorn layer",
308+ "description": "gunicorn layer",
309+ "services": {
310+ "gunicorn": {
311+ "override": "replace",
312+ "summary": "gunicorn service",
313+ "command": "/srv/gunicorn/run",
314+ "startup": "enabled",
315+ }
316+ },
317+ }
318+
319+ # Update pod environment config.
320+ try:
321+ pod_env_config = self._make_pod_env()
322+ except GunicornK8sCharmJujuConfigError as e:
323+ logger.exception("Error getting pod_env_config: %s", e)
324+ self.unit.status = BlockedStatus('Error getting pod_env_config')
325+ return {}
326+ except GunicornK8sWaitingForRelationsError as e:
327+ self.unit.status = BlockedStatus(str(e))
328+ event.defer()
329+ return {}
330+
331+ try:
332+ self._check_juju_config()
333+ except GunicornK8sCharmJujuConfigError as e:
334+ self.unit.status = BlockedStatus(str(e))
335+ return {}
336+
337+ if pod_env_config:
338+ pebble_config["services"]["gunicorn"]["environment"] = pod_env_config
339+ return pebble_config
340+
341+ def _on_config_changed(self, event: ops.framework.EventBase) -> None:
342+ """Handle the config changed event."""
343+ if not self._stored.gunicorn_pebble_ready:
344+ logger.info(
345+ "Got a config changed event, but the workload isn't ready yet. Doing nothing, config will be "
346+ "picked up when workload is ready."
347+ )
348+ return
349+
350+ pebble_config = self._get_pebble_config(event)
351+ if not pebble_config:
352+ # Charm will be in blocked status.
353+ return
354+
355+ # Ensure the ingress relation has the external hostname.
356+ self.ingress.update_config({"service-hostname": self.config["external_hostname"]})
357+
358+ container = self.unit.get_container("gunicorn")
359+ plan = container.get_plan().to_dict()
360+ if plan["services"] != pebble_config["services"]:
361+ container.add_layer("gunicorn", pebble_config, combine=True)
362+
363+ status = container.get_service("gunicorn")
364+ if status.current == ServiceStatus.ACTIVE:
365+ container.stop("gunicorn")
366+ container.start("gunicorn")
367+
368+ self.unit.status = ActiveStatus()
369+
370+ def _on_upgrade_charm(self, event: ops.framework.EventBase) -> None:
371+ """Handle the upgrade charm event."""
372+ # An 'upgrade-charm' hook (which will also be triggered by an
373+ # 'attach-resource' event) will cause the pod to be rescheduled:
374+ # even though the name remains the same, the IP may change.
375+ # The workload won't be running, so we need to handle that in the
376+ # course of subsequent events that will be triggered after this.
377+ #
378+ # Setting pebble_ready to `False` will ensure a 'config-changed'
379+ # hook waits for the workload to be ready before doing anything.
380+ self._stored.gunicorn_pebble_ready = False
381+ # An upgrade-charm hook will be followed by others such as config-changed
382+ # and workload-ready, so just do nothing else for now.
383+ return
384+
385+ def _on_gunicorn_pebble_ready(self, event: ops.framework.EventBase) -> None:
386+ """Handle the workload ready event."""
387+ self._stored.gunicorn_pebble_ready = True
388+
389+ pebble_config = self._get_pebble_config(event)
390+ if not pebble_config:
391+ # Charm will be in blocked status.
392+ return
393+
394+ container = event.workload
395+ logger.debug("About to add_layer with pebble_config:\n{}".format(yaml.dump(pebble_config)))
396+ # `container.add_layer` accepts str (YAML) or dict or pebble.Layer
397+ # object directly.
398+ container.add_layer("gunicorn", pebble_config)
399+ # Start the container and set status.
400+ container.autostart()
401+ self.unit.status = ActiveStatus()
402+
403 def _init_postgresql_relation(self) -> None:
404 """Initialization related to the postgresql relation"""
405 if 'pg' not in self._stored.reldata:
406@@ -118,38 +235,6 @@ class GunicornK8sCharm(CharmBase):
407 "Required Juju config item(s) not set : {}".format(", ".join(sorted(errors)))
408 )
409
410- def _make_k8s_ingress(self) -> list:
411- """Return an ingress that you can use in k8s_resources
412-
413- :returns: A list to be used as k8s ingress
414- """
415-
416- hostname = self.model.config['external_hostname']
417-
418- ingress = {
419- "name": "{}-ingress".format(self.app.name),
420- "spec": {
421- "rules": [
422- {
423- "host": hostname,
424- "http": {
425- "paths": [
426- {
427- "path": "/",
428- "backend": {"serviceName": self.app.name, "servicePort": 80},
429- }
430- ]
431- },
432- }
433- ]
434- },
435- "annotations": {
436- 'nginx.ingress.kubernetes.io/ssl-redirect': 'false',
437- },
438- }
439-
440- return [ingress]
441-
442 def _render_template(self, tmpl: str, ctx: dict) -> str:
443 """Render a Jinja2 template
444
445@@ -243,6 +328,7 @@ class GunicornK8sCharm(CharmBase):
446 return {}
447
448 ctx = self._get_context_from_relations()
449+<<<<<<< src/charm.py
450 rendered_env = self._render_template(env, ctx)
451
452 try:
453@@ -300,6 +386,8 @@ class GunicornK8sCharm(CharmBase):
454 env = self.model.config['environment']
455 ctx = self._get_context_from_relations()
456
457+=======
458+>>>>>>> src/charm.py
459 if env:
460 j2env = Environment(loader=BaseLoader)
461 j2template = j2env.parse(env)
462@@ -310,40 +398,22 @@ class GunicornK8sCharm(CharmBase):
463 missing_vars.add(req_var)
464
465 if missing_vars:
466- logger.info(
467- "Missing YAML vars to interpolate the 'environment' config option, "
468- "setting status to 'waiting' : %s",
469- ", ".join(sorted(missing_vars)),
470+ raise GunicornK8sWaitingForRelationsError(
471+ 'Waiting for {} relation(s)'.format(", ".join(sorted(missing_vars)))
472 )
473- self.unit.status = BlockedStatus('Waiting for {} relation(s)'.format(", ".join(sorted(missing_vars))))
474- event.defer()
475- return
476-
477- if not self.unit.is_leader():
478- self.unit.status = ActiveStatus()
479- return
480
481- try:
482- self._check_juju_config()
483- except GunicornK8sCharmJujuConfigError as e:
484- self.unit.status = BlockedStatus(str(e))
485- return
486-
487- self.unit.status = MaintenanceStatus('Assembling pod spec')
488+ rendered_env = self._render_template(env, ctx)
489
490 try:
491- pod_spec = self._make_pod_spec()
492- except GunicornK8sCharmJujuConfigError as e:
493- self.unit.status = BlockedStatus(str(e))
494- return
495+ self._validate_yaml(rendered_env, dict)
496+ except GunicornK8sCharmYAMLError:
497+ raise GunicornK8sCharmJujuConfigError(
498+ "Could not parse Juju config 'environment' as a YAML dict - check \"juju debug-log -l ERROR\""
499+ )
500
501- resources = pod_spec.get('kubernetesResources', {})
502- resources['ingressResources'] = self._make_k8s_ingress()
503+ env = yaml.safe_load(rendered_env)
504
505- self.unit.status = MaintenanceStatus('Setting pod spec')
506- self.model.pod.set_spec(pod_spec, k8s_resources={'kubernetesResources': resources})
507- logger.info("Setting active status")
508- self.unit.status = ActiveStatus()
509+ return env
510
511
512 if __name__ == "__main__": # pragma: no cover
513diff --git a/tests/unit/scenario.py b/tests/unit/scenario.py
514index 3402589..f695a2e 100644
515--- a/tests/unit/scenario.py
516+++ b/tests/unit/scenario.py
517@@ -78,6 +78,7 @@ TEST_CONFIGURE_POD = {
518 },
519 }
520
521+<<<<<<< tests/unit/scenario.py
522 TEST_MAKE_POD_SPEC = {
523 'basic_no_env': {
524 'config': {
525@@ -158,6 +159,8 @@ TEST_MAKE_K8S_INGRESS = {
526 },
527 }
528
529+=======
530+>>>>>>> tests/unit/scenario.py
531 TEST_RENDER_TEMPLATE = {
532 'working': {
533 'tmpl': "test {{db.x}}",
534diff --git a/tests/unit/test_charm.py b/tests/unit/test_charm.py
535index b5e1f87..4572910 100755
536--- a/tests/unit/test_charm.py
537+++ b/tests/unit/test_charm.py
538@@ -18,8 +18,6 @@ from scenario import (
539 JUJU_DEFAULT_CONFIG,
540 TEST_JUJU_CONFIG,
541 TEST_CONFIGURE_POD,
542- TEST_MAKE_POD_SPEC,
543- TEST_MAKE_K8S_INGRESS,
544 TEST_RENDER_TEMPLATE,
545 TEST_PG_URI,
546 TEST_PG_CONNSTR,
547@@ -161,16 +159,6 @@ class TestGunicornK8sCharm(unittest.TestCase):
548 # The second argument is the list of key to reset
549 self.harness.update_config(JUJU_DEFAULT_CONFIG)
550
551- def test_make_k8s_ingress(self):
552- """Check the crafting of the ingress part of the pod spec."""
553- self.harness.update_config(JUJU_DEFAULT_CONFIG)
554-
555- for scenario, values in TEST_MAKE_K8S_INGRESS.items():
556- with self.subTest(scenario=scenario):
557- self.harness.update_config(values['config'])
558- self.assertEqual(self.harness.charm._make_k8s_ingress(), values['expected'])
559- self.harness.update_config(JUJU_DEFAULT_CONFIG) # You need to clean the config after each run
560-
561 def test_render_template(self):
562 """Test template rendering."""
563
564@@ -406,16 +394,6 @@ class TestGunicornK8sCharm(unittest.TestCase):
565
566 self.assertEqual(self.harness.charm.unit.status, BlockedStatus(expected_status))
567
568- def test_make_pod_spec(self):
569- """Check the crafting of the pod spec."""
570- self.harness.update_config(JUJU_DEFAULT_CONFIG)
571-
572- for scenario, values in TEST_MAKE_POD_SPEC.items():
573- with self.subTest(scenario=scenario):
574- self.harness.update_config(values['config'])
575- self.assertEqual(self.harness.charm._make_pod_spec(), values['pod_spec'])
576- self.harness.update_config(JUJU_DEFAULT_CONFIG) # You need to clean the config after each run
577-
578
579 if __name__ == '__main__':
580 unittest.main()

Subscribers

People subscribed via source and target branches

to all changes: