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

Proposed by Tom Haddon
Status: Work in progress
Proposed branch: ~mthaddon/charm-k8s-gunicorn/+git/charm-k8s-gunicorn:pebble
Merge into: charm-k8s-gunicorn:master
Diff against target: 577 lines (+329/-89) (has conflicts)
6 files modified
lib/charms/nginx_ingress_integrator/v0/ingress.py (+174/-0)
metadata.yaml (+15/-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 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

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

eba7f4a... by Tom Haddon

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

Apply black formatting

373c49c... by Tom Haddon

Fix up the pebble config structure based on feedback from jnsgruk

096a11a... by Tom Haddon

No need to run _make_pod_env twice

6b856a5... by Tom Haddon

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

e53d086... by Tom Haddon

Remove image configuration since we're switching to oci resources

6abe151... by Tom Haddon

Remove unused textwrap

c5ec8a4... by Tom Haddon

Set a default external hostname for easier devel deploys

635d741... by Tom Haddon

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

2b5702f... by Tom Haddon

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

966c4b3... by Tom Haddon

Begin trying to handle config changed hook

7402e87... by Tom Haddon

Refactor how we generate pebble config

e20825e... by Tom Haddon

Remove unused copy module

610fe9e... by Tom Haddon

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

fc03593... by Tom Haddon

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

01b829b... by Tom Haddon

Remove obsolete tests, handle upgrade-charm hook

24dbd63... by Tom Haddon

Remove commented hook handlers - no longer needed as a reference

b4f9f6a... by Tom Haddon

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

688b00e... by Tom Haddon

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

Update for new pebble events and metadata.yaml changes

fa0fe9e... by Tom Haddon

Metadata updates for rc8

e993422... by Tom Haddon

Implement the ingress relation

295358c... by Tom Haddon

Use application rather than unit data for ingress relation

6d1a2da... by Tom Haddon

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

5969f8f... by Tom Haddon

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

f8c5a96... by Tom Haddon

Include return value hint for ingress relation changed handler

203f1a7... by Tom Haddon

Use the new ingress library

2cef81d... by Tom Haddon

Increment patch number

59862e5... by Tom Haddon

Update ingress relation, and handle config changes

ff3ed82... by Tom Haddon

Use new approach to ingress

0e93a5d... by Tom Haddon

Use ServiceStatus to determine whether to stop and start the service

309793e... by Tom Haddon

Switch to nginx-ingress-integrator library

c3f137e... by Tom Haddon

Remove manifest.yaml which is now machine-generated

9cf5653... by Tom Haddon

Remove bases from metadata.yaml, no longer needed

Unmerged commits

9cf5653... by Tom Haddon

Remove bases from metadata.yaml, no longer needed

c3f137e... by Tom Haddon

Remove manifest.yaml which is now machine-generated

309793e... by Tom Haddon

Switch to nginx-ingress-integrator library

0e93a5d... by Tom Haddon

Use ServiceStatus to determine whether to stop and start the service

ff3ed82... by Tom Haddon

Use new approach to ingress

59862e5... by Tom Haddon

Update ingress relation, and handle config changes

2cef81d... by Tom Haddon

Increment patch number

203f1a7... by Tom Haddon

Use the new ingress library

f8c5a96... by Tom Haddon

Include return value hint for ingress relation changed handler

5969f8f... by Tom Haddon

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

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
diff --git a/lib/charms/nginx_ingress_integrator/v0/ingress.py b/lib/charms/nginx_ingress_integrator/v0/ingress.py
0new file mode 1006440new file mode 100644
index 0000000..65d08a7
--- /dev/null
+++ b/lib/charms/nginx_ingress_integrator/v0/ingress.py
@@ -0,0 +1,174 @@
1"""Library for the ingress relation.
2
3This library contains the Requires and Provides classes for handling
4the ingress interface.
5
6Import `IngressRequires` in your charm, with two required options:
7 - "self" (the charm itself)
8 - config_dict
9
10`config_dict` accepts the following keys:
11 - service-hostname (required)
12 - service-name (required)
13 - service-port (required)
14 - limit-rps
15 - limit-whitelist
16 - max_body-size
17 - retry-errors
18 - service-namespace
19 - session-cookie-max-age
20 - tls-secret-name
21
22See `config.yaml` for descriptions of each, along with the required type.
23
24As an example:
25```
26from charms.nginx_ingress_integrator.v0.ingress import IngressRequires
27
28# In your charm's `__init__` method.
29self.ingress = IngressRequires(self, {"service-hostname": self.config["external_hostname"],
30 "service-name": self.app.name,
31 "service-port": 80})
32
33# In your charm's `config-changed` handler.
34self.ingress.update_config({"service-hostname": self.config["external_hostname"]})
35```
36"""
37
38import logging
39
40from ops.charm import CharmEvents
41from ops.framework import EventBase, EventSource, Object
42from ops.model import BlockedStatus
43
44# The unique Charmhub library identifier, never change it
45LIBID = "db0af4367506491c91663468fb5caa4c"
46
47# Increment this major API version when introducing breaking changes
48LIBAPI = 0
49
50# Increment this PATCH version before using `charmcraft push-lib` or reset
51# to 0 if you are raising the major API version
52LIBPATCH = 1
53
54logger = logging.getLogger(__name__)
55
56REQUIRED_INGRESS_RELATION_FIELDS = {
57 "service-hostname",
58 "service-name",
59 "service-port",
60}
61
62OPTIONAL_INGRESS_RELATION_FIELDS = {
63 "limit-rps",
64 "limit-whitelist",
65 "max-body-size",
66 "retry-errors",
67 "service-namespace",
68 "session-cookie-max-age",
69 "tls-secret-name",
70}
71
72
73class IngressAvailableEvent(EventBase):
74 pass
75
76
77class IngressCharmEvents(CharmEvents):
78 """Custom charm events."""
79
80 ingress_available = EventSource(IngressAvailableEvent)
81
82
83class IngressRequires(Object):
84 """This class defines the functionality for the 'requires' side of the 'ingress' relation.
85
86 Hook events observed:
87 - relation-changed
88 """
89
90 def __init__(self, charm, config_dict):
91 super().__init__(charm, "ingress")
92
93 self.framework.observe(charm.on["ingress"].relation_changed, self._on_relation_changed)
94
95 self.config_dict = config_dict
96
97 def _config_dict_errors(self, update_only=False):
98 """Check our config dict for errors."""
99 block_status = False
100 unknown = [
101 x for x in self.config_dict if x not in REQUIRED_INGRESS_RELATION_FIELDS | OPTIONAL_INGRESS_RELATION_FIELDS
102 ]
103 if unknown:
104 logger.error("Unknown key(s) in config dictionary found: %s", ", ".join(unknown))
105 block_status = True
106 if not update_only:
107 missing = [x for x in REQUIRED_INGRESS_RELATION_FIELDS if x not in self.config_dict]
108 if missing:
109 logger.error("Missing required key(s) in config dictionary: %s", ", ".join(missing))
110 block_status = True
111 if block_status:
112 self.model.unit.status = BlockedStatus("Error in ingress relation, check `juju debug-log`")
113 return True
114 return False
115
116 def _on_relation_changed(self, event):
117 """Handle the relation-changed event."""
118 # `self.unit` isn't available here, so use `self.model.unit`.
119 if self.model.unit.is_leader():
120 if self._config_dict_errors():
121 return
122 for key in self.config_dict:
123 event.relation.data[self.model.app][key] = str(self.config_dict[key])
124
125 def update_config(self, config_dict):
126 """Allow for updates to relation."""
127 if self.model.unit.is_leader():
128 self.config_dict = config_dict
129 if self._config_dict_errors(update_only=True):
130 return
131 relation = self.model.get_relation("ingress")
132 if relation:
133 for key in self.config_dict:
134 relation.data[self.model.app][key] = str(self.config_dict[key])
135
136
137class IngressProvides(Object):
138 """This class defines the functionality for the 'provides' side of the 'ingress' relation.
139
140 Hook events observed:
141 - relation-changed
142 """
143
144 def __init__(self, charm):
145 super().__init__(charm, "ingress")
146 # Observe the relation-changed hook event and bind
147 # self.on_relation_changed() to handle the event.
148 self.framework.observe(charm.on["ingress"].relation_changed, self._on_relation_changed)
149 self.charm = charm
150
151 def _on_relation_changed(self, event):
152 """Handle a change to the ingress relation.
153
154 Confirm we have the fields we expect to receive."""
155 # `self.unit` isn't available here, so use `self.model.unit`.
156 if not self.model.unit.is_leader():
157 return
158
159 ingress_data = {
160 field: event.relation.data[event.app].get(field)
161 for field in REQUIRED_INGRESS_RELATION_FIELDS | OPTIONAL_INGRESS_RELATION_FIELDS
162 }
163
164 missing_fields = sorted(
165 [field for field in REQUIRED_INGRESS_RELATION_FIELDS if ingress_data.get(field) is None]
166 )
167
168 if missing_fields:
169 logger.error("Missing required data fields for ingress relation: {}".format(", ".join(missing_fields)))
170 self.model.unit.status = BlockedStatus("Missing fields for ingress: {}".format(", ".join(missing_fields)))
171
172 # Create an event that our charm can use to decide it's okay to
173 # configure the ingress.
174 self.charm.on.ingress_available.emit()
diff --git a/metadata.yaml b/metadata.yaml
index b1a1aa7..37ac9d9 100644
--- a/metadata.yaml
+++ b/metadata.yaml
@@ -5,6 +5,7 @@ description: |
5 Gunicorn charm5 Gunicorn charm
6summary: |6summary: |
7 Gunicorn charm7 Gunicorn charm
8<<<<<<< metadata.yaml
8series: [kubernetes]9series: [kubernetes]
9min-juju-version: 2.8.0 # charm storage in state10min-juju-version: 2.8.0 # charm storage in state
10resources:11resources:
@@ -13,6 +14,18 @@ resources:
13 description: docker image for Gunicorn14 description: docker image for Gunicorn
14 auto-fetch: true15 auto-fetch: true
15 upstream-source: 'gunicorncharmers/gunicorn-app:20.0.4-20.04_edge'16 upstream-source: 'gunicorncharmers/gunicorn-app:20.0.4-20.04_edge'
17=======
18
19containers:
20 gunicorn:
21 resource: gunicorn-image
22
23resources:
24 gunicorn-image:
25 type: oci-image
26 description: Docker image for gunicorn to run
27
28>>>>>>> metadata.yaml
16requires:29requires:
17 pg:30 pg:
18 interface: pgsql31 interface: pgsql
@@ -20,3 +33,5 @@ requires:
20 influxdb:33 influxdb:
21 interface: influxdb-api34 interface: influxdb-api
22 limit: 135 limit: 1
36 ingress:
37 interface: ingress
diff --git a/requirements.txt b/requirements.txt
index 6b14e66..9313aff 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,3 +1,3 @@
1ops1https://github.com/canonical/operator/archive/refs/heads/master.zip
2ops-lib-pgsql2ops-lib-pgsql
3https://github.com/juju-solutions/resource-oci-image/archive/master.zip3https://github.com/juju-solutions/resource-oci-image/archive/master.zip
diff --git a/src/charm.py b/src/charm.py
index c25df78..13524f7 100755
--- a/src/charm.py
+++ b/src/charm.py
@@ -6,6 +6,7 @@ from jinja2 import Environment, BaseLoader, meta
6import logging6import logging
7import yaml7import yaml
88
9from charms.nginx_ingress_integrator.v0.ingress import IngressRequires
9import ops10import ops
10from oci_image import OCIImageResource, OCIImageResourceError11from oci_image import OCIImageResource, OCIImageResourceError
11from ops.framework import StoredState12from ops.framework import StoredState
@@ -14,8 +15,8 @@ from ops.main import main
14from ops.model import (15from ops.model import (
15 ActiveStatus,16 ActiveStatus,
16 BlockedStatus,17 BlockedStatus,
17 MaintenanceStatus,
18)18)
19from ops.pebble import ServiceStatus
19import pgsql20import pgsql
2021
2122
@@ -28,13 +29,13 @@ JUJU_CONFIG_YAML_DICT_ITEMS = ['environment']
28class GunicornK8sCharmJujuConfigError(Exception):29class GunicornK8sCharmJujuConfigError(Exception):
29 """Exception when the Juju config is bad."""30 """Exception when the Juju config is bad."""
3031
31 pass
32
3332
34class GunicornK8sCharmYAMLError(Exception):33class GunicornK8sCharmYAMLError(Exception):
35 """Exception raised when parsing YAML fails"""34 """Exception raised when parsing YAML fails"""
3635
37 pass36
37class GunicornK8sWaitingForRelationsError(Exception):
38 """Exception when waiting for relations."""
3839
3940
40class GunicornK8sCharm(CharmBase):41class GunicornK8sCharm(CharmBase):
@@ -43,18 +44,134 @@ class GunicornK8sCharm(CharmBase):
43 def __init__(self, *args):44 def __init__(self, *args):
44 super().__init__(*args)45 super().__init__(*args)
4546
47<<<<<<< src/charm.py
46 self.image = OCIImageResource(self, 'gunicorn-image')48 self.image = OCIImageResource(self, 'gunicorn-image')
4749
48 self.framework.observe(self.on.start, self._configure_pod)50 self.framework.observe(self.on.start, self._configure_pod)
49 self.framework.observe(self.on.config_changed, self._configure_pod)51 self.framework.observe(self.on.config_changed, self._configure_pod)
50 self.framework.observe(self.on.leader_elected, self._configure_pod)52 self.framework.observe(self.on.leader_elected, self._configure_pod)
51 self.framework.observe(self.on.upgrade_charm, self._configure_pod)53 self.framework.observe(self.on.upgrade_charm, self._configure_pod)
54=======
55 self.framework.observe(self.on.config_changed, self._on_config_changed)
56 self.framework.observe(self.on.upgrade_charm, self._on_upgrade_charm)
57 self.framework.observe(self.on.gunicorn_pebble_ready, self._on_gunicorn_pebble_ready)
58
59 self.ingress = IngressRequires(
60 self,
61 {
62 "service-hostname": self.config["external_hostname"],
63 "service-name": self.app.name,
64 "service-port": 80,
65 },
66 )
67>>>>>>> src/charm.py
5268
53 # For special-cased relations69 self._stored.set_default(
54 self._stored.set_default(reldata={})70 gunicorn_pebble_ready=False,
71 reldata={},
72 )
5573
56 self._init_postgresql_relation()74 self._init_postgresql_relation()
5775
76 def _get_pebble_config(self, event: ops.framework.EventBase) -> dict:
77 """Generate pebble config."""
78 pebble_config = {
79 "summary": "gunicorn layer",
80 "description": "gunicorn layer",
81 "services": {
82 "gunicorn": {
83 "override": "replace",
84 "summary": "gunicorn service",
85 "command": "/srv/gunicorn/run",
86 "startup": "enabled",
87 }
88 },
89 }
90
91 # Update pod environment config.
92 try:
93 pod_env_config = self._make_pod_env()
94 except GunicornK8sCharmJujuConfigError as e:
95 logger.exception("Error getting pod_env_config: %s", e)
96 self.unit.status = BlockedStatus('Error getting pod_env_config')
97 return {}
98 except GunicornK8sWaitingForRelationsError as e:
99 self.unit.status = BlockedStatus(str(e))
100 event.defer()
101 return {}
102
103 try:
104 self._check_juju_config()
105 except GunicornK8sCharmJujuConfigError as e:
106 self.unit.status = BlockedStatus(str(e))
107 return {}
108
109 if pod_env_config:
110 pebble_config["services"]["gunicorn"]["environment"] = pod_env_config
111 return pebble_config
112
113 def _on_config_changed(self, event: ops.framework.EventBase) -> None:
114 """Handle the config changed event."""
115 if not self._stored.gunicorn_pebble_ready:
116 logger.info(
117 "Got a config changed event, but the workload isn't ready yet. Doing nothing, config will be "
118 "picked up when workload is ready."
119 )
120 return
121
122 pebble_config = self._get_pebble_config(event)
123 if not pebble_config:
124 # Charm will be in blocked status.
125 return
126
127 # Ensure the ingress relation has the external hostname.
128 self.ingress.update_config({"service-hostname": self.config["external_hostname"]})
129
130 container = self.unit.get_container("gunicorn")
131 plan = container.get_plan().to_dict()
132 if plan["services"] != pebble_config["services"]:
133 container.add_layer("gunicorn", pebble_config, combine=True)
134
135 status = container.get_service("gunicorn")
136 if status.current == ServiceStatus.ACTIVE:
137 container.stop("gunicorn")
138 container.start("gunicorn")
139
140 self.unit.status = ActiveStatus()
141
142 def _on_upgrade_charm(self, event: ops.framework.EventBase) -> None:
143 """Handle the upgrade charm event."""
144 # An 'upgrade-charm' hook (which will also be triggered by an
145 # 'attach-resource' event) will cause the pod to be rescheduled:
146 # even though the name remains the same, the IP may change.
147 # The workload won't be running, so we need to handle that in the
148 # course of subsequent events that will be triggered after this.
149 #
150 # Setting pebble_ready to `False` will ensure a 'config-changed'
151 # hook waits for the workload to be ready before doing anything.
152 self._stored.gunicorn_pebble_ready = False
153 # An upgrade-charm hook will be followed by others such as config-changed
154 # and workload-ready, so just do nothing else for now.
155 return
156
157 def _on_gunicorn_pebble_ready(self, event: ops.framework.EventBase) -> None:
158 """Handle the workload ready event."""
159 self._stored.gunicorn_pebble_ready = True
160
161 pebble_config = self._get_pebble_config(event)
162 if not pebble_config:
163 # Charm will be in blocked status.
164 return
165
166 container = event.workload
167 logger.debug("About to add_layer with pebble_config:\n{}".format(yaml.dump(pebble_config)))
168 # `container.add_layer` accepts str (YAML) or dict or pebble.Layer
169 # object directly.
170 container.add_layer("gunicorn", pebble_config)
171 # Start the container and set status.
172 container.autostart()
173 self.unit.status = ActiveStatus()
174
58 def _init_postgresql_relation(self) -> None:175 def _init_postgresql_relation(self) -> None:
59 """Initialization related to the postgresql relation"""176 """Initialization related to the postgresql relation"""
60 if 'pg' not in self._stored.reldata:177 if 'pg' not in self._stored.reldata:
@@ -118,38 +235,6 @@ class GunicornK8sCharm(CharmBase):
118 "Required Juju config item(s) not set : {}".format(", ".join(sorted(errors)))235 "Required Juju config item(s) not set : {}".format(", ".join(sorted(errors)))
119 )236 )
120237
121 def _make_k8s_ingress(self) -> list:
122 """Return an ingress that you can use in k8s_resources
123
124 :returns: A list to be used as k8s ingress
125 """
126
127 hostname = self.model.config['external_hostname']
128
129 ingress = {
130 "name": "{}-ingress".format(self.app.name),
131 "spec": {
132 "rules": [
133 {
134 "host": hostname,
135 "http": {
136 "paths": [
137 {
138 "path": "/",
139 "backend": {"serviceName": self.app.name, "servicePort": 80},
140 }
141 ]
142 },
143 }
144 ]
145 },
146 "annotations": {
147 'nginx.ingress.kubernetes.io/ssl-redirect': 'false',
148 },
149 }
150
151 return [ingress]
152
153 def _render_template(self, tmpl: str, ctx: dict) -> str:238 def _render_template(self, tmpl: str, ctx: dict) -> str:
154 """Render a Jinja2 template239 """Render a Jinja2 template
155240
@@ -243,6 +328,7 @@ class GunicornK8sCharm(CharmBase):
243 return {}328 return {}
244329
245 ctx = self._get_context_from_relations()330 ctx = self._get_context_from_relations()
331<<<<<<< src/charm.py
246 rendered_env = self._render_template(env, ctx)332 rendered_env = self._render_template(env, ctx)
247333
248 try:334 try:
@@ -300,6 +386,8 @@ class GunicornK8sCharm(CharmBase):
300 env = self.model.config['environment']386 env = self.model.config['environment']
301 ctx = self._get_context_from_relations()387 ctx = self._get_context_from_relations()
302388
389=======
390>>>>>>> src/charm.py
303 if env:391 if env:
304 j2env = Environment(loader=BaseLoader)392 j2env = Environment(loader=BaseLoader)
305 j2template = j2env.parse(env)393 j2template = j2env.parse(env)
@@ -310,40 +398,22 @@ class GunicornK8sCharm(CharmBase):
310 missing_vars.add(req_var)398 missing_vars.add(req_var)
311399
312 if missing_vars:400 if missing_vars:
313 logger.info(401 raise GunicornK8sWaitingForRelationsError(
314 "Missing YAML vars to interpolate the 'environment' config option, "402 'Waiting for {} relation(s)'.format(", ".join(sorted(missing_vars)))
315 "setting status to 'waiting' : %s",
316 ", ".join(sorted(missing_vars)),
317 )403 )
318 self.unit.status = BlockedStatus('Waiting for {} relation(s)'.format(", ".join(sorted(missing_vars))))
319 event.defer()
320 return
321
322 if not self.unit.is_leader():
323 self.unit.status = ActiveStatus()
324 return
325404
326 try:405 rendered_env = self._render_template(env, ctx)
327 self._check_juju_config()
328 except GunicornK8sCharmJujuConfigError as e:
329 self.unit.status = BlockedStatus(str(e))
330 return
331
332 self.unit.status = MaintenanceStatus('Assembling pod spec')
333406
334 try:407 try:
335 pod_spec = self._make_pod_spec()408 self._validate_yaml(rendered_env, dict)
336 except GunicornK8sCharmJujuConfigError as e:409 except GunicornK8sCharmYAMLError:
337 self.unit.status = BlockedStatus(str(e))410 raise GunicornK8sCharmJujuConfigError(
338 return411 "Could not parse Juju config 'environment' as a YAML dict - check \"juju debug-log -l ERROR\""
412 )
339413
340 resources = pod_spec.get('kubernetesResources', {})414 env = yaml.safe_load(rendered_env)
341 resources['ingressResources'] = self._make_k8s_ingress()
342415
343 self.unit.status = MaintenanceStatus('Setting pod spec')416 return env
344 self.model.pod.set_spec(pod_spec, k8s_resources={'kubernetesResources': resources})
345 logger.info("Setting active status")
346 self.unit.status = ActiveStatus()
347417
348418
349if __name__ == "__main__": # pragma: no cover419if __name__ == "__main__": # pragma: no cover
diff --git a/tests/unit/scenario.py b/tests/unit/scenario.py
index 3402589..f695a2e 100644
--- a/tests/unit/scenario.py
+++ b/tests/unit/scenario.py
@@ -78,6 +78,7 @@ TEST_CONFIGURE_POD = {
78 },78 },
79}79}
8080
81<<<<<<< tests/unit/scenario.py
81TEST_MAKE_POD_SPEC = {82TEST_MAKE_POD_SPEC = {
82 'basic_no_env': {83 'basic_no_env': {
83 'config': {84 'config': {
@@ -158,6 +159,8 @@ TEST_MAKE_K8S_INGRESS = {
158 },159 },
159}160}
160161
162=======
163>>>>>>> tests/unit/scenario.py
161TEST_RENDER_TEMPLATE = {164TEST_RENDER_TEMPLATE = {
162 'working': {165 'working': {
163 'tmpl': "test {{db.x}}",166 'tmpl': "test {{db.x}}",
diff --git a/tests/unit/test_charm.py b/tests/unit/test_charm.py
index b5e1f87..4572910 100755
--- a/tests/unit/test_charm.py
+++ b/tests/unit/test_charm.py
@@ -18,8 +18,6 @@ from scenario import (
18 JUJU_DEFAULT_CONFIG,18 JUJU_DEFAULT_CONFIG,
19 TEST_JUJU_CONFIG,19 TEST_JUJU_CONFIG,
20 TEST_CONFIGURE_POD,20 TEST_CONFIGURE_POD,
21 TEST_MAKE_POD_SPEC,
22 TEST_MAKE_K8S_INGRESS,
23 TEST_RENDER_TEMPLATE,21 TEST_RENDER_TEMPLATE,
24 TEST_PG_URI,22 TEST_PG_URI,
25 TEST_PG_CONNSTR,23 TEST_PG_CONNSTR,
@@ -161,16 +159,6 @@ class TestGunicornK8sCharm(unittest.TestCase):
161 # The second argument is the list of key to reset159 # The second argument is the list of key to reset
162 self.harness.update_config(JUJU_DEFAULT_CONFIG)160 self.harness.update_config(JUJU_DEFAULT_CONFIG)
163161
164 def test_make_k8s_ingress(self):
165 """Check the crafting of the ingress part of the pod spec."""
166 self.harness.update_config(JUJU_DEFAULT_CONFIG)
167
168 for scenario, values in TEST_MAKE_K8S_INGRESS.items():
169 with self.subTest(scenario=scenario):
170 self.harness.update_config(values['config'])
171 self.assertEqual(self.harness.charm._make_k8s_ingress(), values['expected'])
172 self.harness.update_config(JUJU_DEFAULT_CONFIG) # You need to clean the config after each run
173
174 def test_render_template(self):162 def test_render_template(self):
175 """Test template rendering."""163 """Test template rendering."""
176164
@@ -406,16 +394,6 @@ class TestGunicornK8sCharm(unittest.TestCase):
406394
407 self.assertEqual(self.harness.charm.unit.status, BlockedStatus(expected_status))395 self.assertEqual(self.harness.charm.unit.status, BlockedStatus(expected_status))
408396
409 def test_make_pod_spec(self):
410 """Check the crafting of the pod spec."""
411 self.harness.update_config(JUJU_DEFAULT_CONFIG)
412
413 for scenario, values in TEST_MAKE_POD_SPEC.items():
414 with self.subTest(scenario=scenario):
415 self.harness.update_config(values['config'])
416 self.assertEqual(self.harness.charm._make_pod_spec(), values['pod_spec'])
417 self.harness.update_config(JUJU_DEFAULT_CONFIG) # You need to clean the config after each run
418
419397
420if __name__ == '__main__':398if __name__ == '__main__':
421 unittest.main()399 unittest.main()

Subscribers

People subscribed via source and target branches

to all changes: