Merge ~ballot/charm-k8s-mm-pd-bot/+git/charm-k8s-mm-pd-bot:reboot into charm-k8s-mm-pd-bot:master

Proposed by Benjamin Allot
Status: Work in progress
Proposed branch: ~ballot/charm-k8s-mm-pd-bot/+git/charm-k8s-mm-pd-bot:reboot
Merge into: charm-k8s-mm-pd-bot:master
Diff against target: 1484 lines (+841/-283)
11 files modified
Makefile (+6/-2)
README.md (+23/-14)
actions.yaml (+18/-0)
charmcraft.yaml (+10/-0)
config.yaml (+4/-23)
lib/charms/nginx_ingress_integrator/v0/ingress.py (+398/-0)
metadata.yaml (+24/-5)
requirements.txt (+1/-1)
src/charm.py (+262/-149)
tests/unit/scenario.py (+21/-23)
tests/unit/test_charm.py (+74/-66)
Reviewer Review Type Date Requested Status
MatterMost Pagerduty Bot Charmers Pending
Review via email: mp+436576@code.launchpad.net
To post a comment you must log in.

Unmerged commits

c8449a3... by Benjamin Allot

Huge revamp using old code and new ones

* Convert to pebble
* Use https://github.com/canonical/nginx-ingress-integrator-operator
* Revamp of the docs (but not quite finished)
* Tests need to be fixed
* Using charmcraft and resources ibstead of an image path

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/Makefile b/Makefile
2index 0e38a25..6e00e0b 100644
3--- a/Makefile
4+++ b/Makefile
5@@ -8,7 +8,7 @@ TESTS := $(shell find $(PROJECTPATH)/tests -name "*.py" -o -name "requirements.t
6
7 # Rebuild the charm each time we have an update in the sources or tests
8 $(CHARM_FILE): $(SOURCES) $(TESTS) $(PROJECTPATH)/requirements.txt
9- charmcraft build -f $(PROJECTPATH)
10+ charmcraft pack -v -p $(PROJECTPATH)
11
12 all: test
13
14@@ -25,6 +25,10 @@ unittest: $(CHARM_FILE)
15
16 test: lint unittest
17
18+build:
19+ @rm -f $(CHARM_FILE)
20+ charmcraft pack -v -p $(PROJECTPATH)
21+
22 clean:
23 @echo "Cleaning files"
24 @find $(PROJECTPATH) -name "*.pyc" -exec rm -f {} +
25@@ -32,4 +36,4 @@ clean:
26 @rm -f $(CHARM_FILE)
27 @rm -rf $(PROJECTPATH)/{build,charm} # Delete the build directory
28
29-.PHONY: all lint test unittest clean
30+.PHONY: all lint test unittest clean build
31diff --git a/README.md b/README.md
32index a62e135..d8e5ac0 100644
33--- a/README.md
34+++ b/README.md
35@@ -2,30 +2,39 @@
36
37 A juju charm deploying Mattermost PagerDuty Bot.
38
39+## Prerequisites
40+
41+To deploy and use this charm you will need:
42+
43+ * [Juju and a kubernetes cluster](https://juju.is/docs/olm/installing-juju)
44+ * [A PagerDuty account and a General access PagerDuty token](https://support.pagerduty.com/docs/generating-api-keys)
45+ * [A Bot account on a Mattermost server](https://docs.mattermost.com/developer/bot-accounts.html)
46+ * A kubernetes deployment with an ingress activated. [Microk8s is recommended for a test environment](https://microk8s.io/docs/addon-ingress)
47+
48 ## Overview
49
50-This is a k8s workload charm and can only be deployed to to a Juju k8s
51-cloud, attached to a controller using `juju add-k8s`.
52+This charm will deploy a daemon interacting with PagerDuty and Mattermost.
53+
54+Alerts in PagerDuty will be displayed on a Mattermost channel and you will be able to interact with those alerts through the chat.
55+
56+You can quickly acknowledge, resolve or reassign incidents directly from Mattermost.
57
58-A valid configuration file will be needed. See [the example](examples/bot.cfg)
59+The Mattermost PagerDuty Bot uses a configuration file. See [the example](https://git.launchpad.net/charm-k8s-mm-pd-bot/tree/examples/bot.cfg).
60
61 ## Getting Started
62
63-For details on the configuration file, [see here](https://charmhub.io/mm-pd-bot-k8s/docs/configuration).
64-To deploy into a juju k8s model, run:
65+For details on the configuration file, [see here](https://charmhub.io/mm-pd-bot/docs/configuration).
66+To deploy into a juju kubernetes model, run:
67
68 ```
69-juju deploy mm-pd-bot-k8s \
70- --config image_path=rocks.example.com/mm-pd-bot:devel \
71- --config mm_pd_bot_cfg="$(cat bot.cfg)" \ # Don't forget the double quotes here
72- --config juju-external-hostname=mm-pd-bot.local \
73- --config kubernetes-ingress-allow-http=true \
74+juju deploy ch:mm-pd-bot \
75+ --config mm_pd_bot_cfg=@bot.cfg \
76+ --config external_hostname=mm-pd-bot.mydomain.com \
77 mm-pd-bot
78-juju wait
79-juju status
80-juju expose mm-pd-bot
81+juju deploy nginx-ingress-integrator
82+juju relate mm-pd-bot nginx-ingress-integrator
83 ```
84
85 ---
86
87-For more details, [see here](https://charmhub.io/mm-pd-bot-k8s/docs)
88+For more details, [see here](https://charmhub.io/mm-pd-bot/docs)
89diff --git a/actions.yaml b/actions.yaml
90new file mode 100644
91index 0000000..2d869d9
92--- /dev/null
93+++ b/actions.yaml
94@@ -0,0 +1,18 @@
95+---
96+set-pagerduty-credentials:
97+ description: Set the general access PagerDuty token
98+ params:
99+ account:
100+ type: string
101+ description: The PagerDuty account
102+ token:
103+ type: string
104+ description: The PagerDuty token
105+ required: [account, token]
106+set-mattermost-bot-token:
107+ description: Set the Mattermost Bot account token
108+ params:
109+ token:
110+ type: string
111+ description: The Mattermost bot token
112+ required: [token]
113diff --git a/charmcraft.yaml b/charmcraft.yaml
114new file mode 100644
115index 0000000..e72c4e8
116--- /dev/null
117+++ b/charmcraft.yaml
118@@ -0,0 +1,10 @@
119+---
120+type: "charm"
121+
122+bases:
123+ - build-on:
124+ - name: ubuntu
125+ channel: "20.04"
126+ run-on:
127+ - name: ubuntu
128+ channel: "20.04"
129diff --git a/config.yaml b/config.yaml
130index 1e1e3e8..95e6a90 100644
131--- a/config.yaml
132+++ b/config.yaml
133@@ -1,27 +1,15 @@
134 options:
135- image_path:
136+ mm_pd_bot_cfg:
137 type: string
138 description: |
139- The location of the image to use, e.g. "registry.example.com/mm-pd-bot:v1".
140+ The whole configuration file of the bot is passed as content to this setting.
141
142 This setting is required.
143 default: ''
144- image_username:
145- type: string
146- description: |
147- The username for accessing the registry specified in image_path.
148- default: ''
149- image_password:
150- type: string
151- description: |
152- The password associated with image_username for accessing the registry specified in image_path.
153- default: ''
154- mm_pd_bot_cfg:
155+ external_hostname:
156 type: string
157 description: |
158- The whole configuration file of the bot is passed as content to this setting.
159-
160- This setting is required.
161+ The FQDN used to reach the bot via the webhooks.
162 default: ''
163 tls_secret_name:
164 type: string
165@@ -30,10 +18,3 @@ options:
166
167 This setting is ignored unless juju-external-hostname begins with "https://".
168 default: ''
169- ingress_whitelist_source_range:
170- type: string
171- description: |
172- A comma-separated list of CIDRs to store in the ingress.kubernetes.io/whitelist-source-range annotation.
173-
174- This can be used to lock down access to Mattermost PagerDuty Bot based on source IP address.
175- default: ''
176diff --git a/lib/charms/nginx_ingress_integrator/v0/ingress.py b/lib/charms/nginx_ingress_integrator/v0/ingress.py
177new file mode 100644
178index 0000000..4ed6345
179--- /dev/null
180+++ b/lib/charms/nginx_ingress_integrator/v0/ingress.py
181@@ -0,0 +1,398 @@
182+# Copyright 2023 Canonical Ltd.
183+# Licensed under the Apache2.0, see LICENCE file in charm source for details.
184+"""Library for the ingress relation.
185+
186+This library contains the Requires and Provides classes for handling
187+the ingress interface.
188+
189+Import `IngressRequires` in your charm, with two required options:
190+- "self" (the charm itself)
191+- config_dict
192+
193+`config_dict` accepts the following keys:
194+- additional-hostnames
195+- limit-rps
196+- limit-whitelist
197+- max-body-size
198+- owasp-modsecurity-crs
199+- owasp-modsecurity-custom-rules
200+- path-routes
201+- retry-errors
202+- rewrite-enabled
203+- rewrite-target
204+- service-hostname (required)
205+- service-name (required)
206+- service-namespace
207+- service-port (required)
208+- session-cookie-max-age
209+- tls-secret-name
210+
211+See [the config section](https://charmhub.io/nginx-ingress-integrator/configure) for descriptions
212+of each, along with the required type.
213+
214+As an example, add the following to `src/charm.py`:
215+```
216+from charms.nginx_ingress_integrator.v0.ingress import IngressRequires
217+
218+# In your charm's `__init__` method.
219+self.ingress = IngressRequires(self, {
220+ "service-hostname": self.config["external_hostname"],
221+ "service-name": self.app.name,
222+ "service-port": 80,
223+ }
224+)
225+
226+# In your charm's `config-changed` handler.
227+self.ingress.update_config({"service-hostname": self.config["external_hostname"]})
228+```
229+And then add the following to `metadata.yaml`:
230+```
231+requires:
232+ ingress:
233+ interface: ingress
234+```
235+You _must_ register the IngressRequires class as part of the `__init__` method
236+rather than, for instance, a config-changed event handler, for the relation
237+changed event to be properly handled.
238+"""
239+
240+import copy
241+import logging
242+from typing import Dict
243+
244+from ops.charm import CharmEvents, RelationBrokenEvent, RelationChangedEvent
245+from ops.framework import EventBase, EventSource, Object
246+from ops.model import BlockedStatus
247+
248+INGRESS_RELATION_NAME = "ingress"
249+INGRESS_PROXY_RELATION_NAME = "ingress-proxy"
250+
251+# The unique Charmhub library identifier, never change it
252+LIBID = "db0af4367506491c91663468fb5caa4c"
253+
254+# Increment this major API version when introducing breaking changes
255+LIBAPI = 0
256+
257+# Increment this PATCH version before using `charmcraft publish-lib` or reset
258+# to 0 if you are raising the major API version
259+LIBPATCH = 14
260+
261+LOGGER = logging.getLogger(__name__)
262+
263+REQUIRED_INGRESS_RELATION_FIELDS = {"service-hostname", "service-name", "service-port"}
264+
265+OPTIONAL_INGRESS_RELATION_FIELDS = {
266+ "additional-hostnames",
267+ "limit-rps",
268+ "limit-whitelist",
269+ "max-body-size",
270+ "owasp-modsecurity-crs",
271+ "owasp-modsecurity-custom-rules",
272+ "path-routes",
273+ "retry-errors",
274+ "rewrite-target",
275+ "rewrite-enabled",
276+ "service-namespace",
277+ "session-cookie-max-age",
278+ "tls-secret-name",
279+}
280+
281+RELATION_INTERFACES_MAPPINGS = {
282+ "service-hostname": "host",
283+ "service-name": "name",
284+ "service-namespace": "model",
285+ "service-port": "port",
286+}
287+RELATION_INTERFACES_MAPPINGS_VALUES = {v for v in RELATION_INTERFACES_MAPPINGS.values()}
288+
289+
290+class IngressAvailableEvent(EventBase):
291+ """IngressAvailableEvent custom event.
292+
293+ This event indicates the Ingress provider is available.
294+ """
295+
296+
297+class IngressProxyAvailableEvent(EventBase):
298+ """IngressProxyAvailableEvent custom event.
299+
300+ This event indicates the IngressProxy provider is available.
301+ """
302+
303+
304+class IngressBrokenEvent(RelationBrokenEvent):
305+ """IngressBrokenEvent custom event.
306+
307+ This event indicates the Ingress provider is broken.
308+ """
309+
310+
311+class IngressCharmEvents(CharmEvents):
312+ """Custom charm events.
313+
314+ Attrs:
315+ ingress_available: Event to indicate that Ingress is available.
316+ ingress_proxy_available: Event to indicate that IngressProxy is available.
317+ ingress_broken: Event to indicate that Ingress is broken.
318+ """
319+
320+ ingress_available = EventSource(IngressAvailableEvent)
321+ ingress_proxy_available = EventSource(IngressProxyAvailableEvent)
322+ ingress_broken = EventSource(IngressBrokenEvent)
323+
324+
325+class IngressRequires(Object):
326+ """This class defines the functionality for the 'requires' side of the 'ingress' relation.
327+
328+ Hook events observed:
329+ - relation-changed
330+
331+ Attrs:
332+ model: Juju model where the charm is deployed.
333+ config_dict: Contains all the configuration options for Ingress.
334+ """
335+
336+ def __init__(self, charm, config_dict):
337+ """Init function for the IngressRequires class.
338+
339+ Args:
340+ charm: The charm that requires the ingress relation.
341+ config_dict: Contains all the configuration options for Ingress.
342+ """
343+ super().__init__(charm, INGRESS_RELATION_NAME)
344+
345+ self.framework.observe(
346+ charm.on[INGRESS_RELATION_NAME].relation_changed, self._on_relation_changed
347+ )
348+
349+ # Set default values.
350+ default_relation_fields = {
351+ "service-namespace": self.model.name,
352+ }
353+ config_dict.update(
354+ (key, value)
355+ for key, value in default_relation_fields.items()
356+ if key not in config_dict or not config_dict[key]
357+ )
358+
359+ self.config_dict = self._convert_to_relation_interface(config_dict)
360+
361+ @staticmethod
362+ def _convert_to_relation_interface(config_dict: Dict) -> Dict:
363+ """Create a new relation dict that conforms with charm-relation-interfaces.
364+
365+ Args:
366+ config_dict: Ingress configuration that doesn't conform with charm-relation-interfaces.
367+
368+ Returns:
369+ The Ingress configuration conforming with charm-relation-interfaces.
370+ """
371+ config_dict = copy.copy(config_dict)
372+ config_dict.update(
373+ (key, config_dict[old_key])
374+ for old_key, key in RELATION_INTERFACES_MAPPINGS.items()
375+ if old_key in config_dict and config_dict[old_key]
376+ )
377+ return config_dict
378+
379+ def _config_dict_errors(self, config_dict: Dict, update_only: bool = False) -> bool:
380+ """Check our config dict for errors.
381+
382+ Args:
383+ config_dict: Contains all the configuration options for Ingress.
384+ update_only: If the charm needs to update only existing keys.
385+
386+ Returns:
387+ If we need to update the config dict ot not.
388+ """
389+ blocked_message = "Error in ingress relation, check `juju debug-log`"
390+ unknown = [
391+ config_key
392+ for config_key in config_dict
393+ if config_key
394+ not in REQUIRED_INGRESS_RELATION_FIELDS
395+ | OPTIONAL_INGRESS_RELATION_FIELDS
396+ | RELATION_INTERFACES_MAPPINGS_VALUES
397+ ]
398+ if unknown:
399+ LOGGER.error(
400+ "Ingress relation error, unknown key(s) in config dictionary found: %s",
401+ ", ".join(unknown),
402+ )
403+ self.model.unit.status = BlockedStatus(blocked_message)
404+ return True
405+ if not update_only:
406+ missing = tuple(
407+ config_key
408+ for config_key in REQUIRED_INGRESS_RELATION_FIELDS
409+ if config_key not in self.config_dict
410+ )
411+ if missing:
412+ LOGGER.error(
413+ "Ingress relation error, missing required key(s) in config dictionary: %s",
414+ ", ".join(sorted(missing)),
415+ )
416+ self.model.unit.status = BlockedStatus(blocked_message)
417+ return True
418+ return False
419+
420+ def _on_relation_changed(self, event: RelationChangedEvent) -> None:
421+ """Handle the relation-changed event.
422+
423+ Args:
424+ event: Event triggering the relation-changed hook for the relation.
425+ """
426+ # `self.unit` isn't available here, so use `self.model.unit`.
427+ if self.model.unit.is_leader():
428+ if self._config_dict_errors(config_dict=self.config_dict):
429+ return
430+ event.relation.data[self.model.app].update(
431+ (key, str(self.config_dict[key])) for key in self.config_dict
432+ )
433+
434+ def update_config(self, config_dict: Dict) -> None:
435+ """Allow for updates to relation.
436+
437+ Args:
438+ config_dict: Contains all the configuration options for Ingress.
439+
440+ Attrs:
441+ config_dict: Contains all the configuration options for Ingress.
442+ """
443+ if self.model.unit.is_leader():
444+ self.config_dict = self._convert_to_relation_interface(config_dict)
445+ if self._config_dict_errors(self.config_dict, update_only=True):
446+ return
447+ relation = self.model.get_relation(INGRESS_RELATION_NAME)
448+ if relation:
449+ for key in self.config_dict:
450+ relation.data[self.model.app][key] = str(self.config_dict[key])
451+
452+
453+class IngressBaseProvides(Object):
454+ """Parent class for IngressProvides and IngressProxyProvides.
455+
456+ Attrs:
457+ model: Juju model where the charm is deployed.
458+ """
459+
460+ def _on_relation_changed(self, event: RelationChangedEvent) -> None:
461+ """Handle a change to the ingress/ingress-proxy relation.
462+
463+ Confirm we have the fields we expect to receive.
464+
465+ Args:
466+ event: Event triggering the relation-changed hook for the relation.
467+ """
468+ # `self.unit` isn't available here, so use `self.model.unit`.
469+ if not self.model.unit.is_leader():
470+ return
471+
472+ relation_name = event.relation.name
473+
474+ if not event.relation.data[event.app]:
475+ LOGGER.info(
476+ "%s hasn't finished configuring, waiting until relation is changed again.",
477+ relation_name,
478+ )
479+ return
480+
481+ ingress_data = {
482+ field: event.relation.data[event.app].get(field)
483+ for field in REQUIRED_INGRESS_RELATION_FIELDS | OPTIONAL_INGRESS_RELATION_FIELDS
484+ }
485+
486+ missing_fields = sorted(
487+ field for field in REQUIRED_INGRESS_RELATION_FIELDS if ingress_data.get(field) is None
488+ )
489+
490+ if missing_fields:
491+ LOGGER.warning(
492+ "Missing required data fields for %s relation: %s",
493+ relation_name,
494+ ", ".join(missing_fields),
495+ )
496+ self.model.unit.status = BlockedStatus(
497+ f"Missing fields for {relation_name}: {', '.join(missing_fields)}"
498+ )
499+
500+ if relation_name == INGRESS_RELATION_NAME:
501+ # Conform to charm-relation-interfaces.
502+ if "name" in ingress_data and "port" in ingress_data:
503+ name = ingress_data["name"]
504+ port = ingress_data["port"]
505+ else:
506+ name = ingress_data["service-name"]
507+ port = ingress_data["service-port"]
508+ event.relation.data[self.model.app]["url"] = f"http://{name}:{port}/"
509+
510+ # Create an event that our charm can use to decide it's okay to
511+ # configure the ingress.
512+ self.charm.on.ingress_available.emit()
513+ elif relation_name == INGRESS_PROXY_RELATION_NAME:
514+ self.charm.on.ingress_proxy_available.emit()
515+
516+
517+class IngressProvides(IngressBaseProvides):
518+ """Class containing the functionality for the 'provides' side of the 'ingress' relation.
519+
520+ Attrs:
521+ charm: The charm that provides the ingress relation.
522+
523+ Hook events observed:
524+ - relation-changed
525+ """
526+
527+ def __init__(self, charm):
528+ """Init function for the IngressProvides class.
529+
530+ Args:
531+ charm: The charm that provides the ingress relation.
532+ """
533+ super().__init__(charm, INGRESS_RELATION_NAME)
534+ # Observe the relation-changed hook event and bind
535+ # self.on_relation_changed() to handle the event.
536+ self.framework.observe(
537+ charm.on[INGRESS_RELATION_NAME].relation_changed, self._on_relation_changed
538+ )
539+ self.framework.observe(
540+ charm.on[INGRESS_RELATION_NAME].relation_broken, self._on_relation_broken
541+ )
542+ self.charm = charm
543+
544+ def _on_relation_broken(self, event: RelationBrokenEvent) -> None:
545+ """Handle a relation-broken event in the ingress relation.
546+
547+ Args:
548+ event: Event triggering the relation-broken hook for the relation.
549+ """
550+ if not self.model.unit.is_leader():
551+ return
552+
553+ # Create an event that our charm can use to remove the ingress resource.
554+ self.charm.on.ingress_broken.emit(event.relation)
555+
556+
557+class IngressProxyProvides(IngressBaseProvides):
558+ """Class containing the functionality for the 'provides' side of the 'ingress-proxy' relation.
559+
560+ Attrs:
561+ charm: The charm that provides the ingress-proxy relation.
562+
563+ Hook events observed:
564+ - relation-changed
565+ """
566+
567+ def __init__(self, charm):
568+ """Init function for the IngressProxyProvides class.
569+
570+ Args:
571+ charm: The charm that provides the ingress-proxy relation.
572+ """
573+ super().__init__(charm, INGRESS_PROXY_RELATION_NAME)
574+ # Observe the relation-changed hook event and bind
575+ # self.on_relation_changed() to handle the event.
576+ self.framework.observe(
577+ charm.on[INGRESS_PROXY_RELATION_NAME].relation_changed, self._on_relation_changed
578+ )
579+ self.charm = charm
580diff --git a/metadata.yaml b/metadata.yaml
581index 427f0c7..603c650 100644
582--- a/metadata.yaml
583+++ b/metadata.yaml
584@@ -1,14 +1,33 @@
585-name: mm-pd-bot-k8s
586+name: mm-pd-bot
587+
588 display-name: Mattermost PagerDuty Bot
589-docs: https://discourse.charmhub.io/t/mattermost-pagerduty-bot-docs-index/4596
590+
591+docs: https://charmhub.io/mm-pd-bot
592+
593 summary: MatterMost Pagerduty Bot charm
594+
595 maintainers:
596 - launchpad.net/~mm-pd-bot-charmers
597+
598 description: |
599 A charm which deploys Mattermost Pagerduty Bot on kubernetes.
600 Mattermost is a flexible, open source messaging platform that enables
601 secure team collaboration.
602 This bot manages incidents on PagerDuty through commands sent to a MatterMost channel
603-min-juju-version: 2.8.0 # charm storage in state
604-series:
605- - kubernetes
606+
607+requires:
608+ ingress:
609+ interface: ingress
610+
611+assumes:
612+ - juju < 3.0
613+ - k8s-api
614+
615+containers:
616+ mm-pd-bot:
617+ resource: mm-pd-bot-image
618+
619+resources:
620+ mm-pd-bot-image:
621+ type: oci-image
622+ description: Docker image for Mattermost PagerDuty Bot to run
623diff --git a/requirements.txt b/requirements.txt
624index eaae2b5..b8a4bc5 100644
625--- a/requirements.txt
626+++ b/requirements.txt
627@@ -1 +1 @@
628-ops>=0.8
629+ops>=1.5.0
630diff --git a/src/charm.py b/src/charm.py
631index 7b2807f..9d45a93 100755
632--- a/src/charm.py
633+++ b/src/charm.py
634@@ -3,10 +3,12 @@
635 # Copyright 2020 Canonical Ltd.
636 # Licensed under the GPLv3, see LICENCE file for details.
637
638+import collections
639 import configparser
640 import logging
641-from urllib.parse import urlparse
642+import io
643
644+from charms.nginx_ingress_integrator.v0.ingress import IngressRequires
645 import ops
646 from ops.charm import CharmBase
647 from ops.main import main
648@@ -14,15 +16,16 @@ from ops.model import (
649 ActiveStatus,
650 BlockedStatus,
651 )
652+from ops.pebble import ConnectionError, PathError, ServiceStatus
653
654
655-logger = logging.getLogger()
656+logger = logging.getLogger(__name__)
657
658-REQUIRED_JUJU_SETTINGS = ["image_path", "mm_pd_bot_cfg"]
659+REQUIRED_JUJU_SETTINGS = ["external_hostname", "mm_pd_bot_cfg"]
660 REQUIRED_CFG_SETTINGS = {
661- "PagerDuty": ["account", "api-token", "private-channel", "public-channel"],
662+ "PagerDuty": ["private-channel", "public-channel"],
663 "Prometheus": ["max_cache_size"],
664- "httpd": ["hostname", "listen-ip", "listen-port", "magic-uuid"],
665+ "httpd": ["listen-ip", "listen-port", "magic-uuid"],
666 "MattermostBot": ["bot_url", "bot_team"],
667 # Those sections can be empty
668 "nickname to email": [],
669@@ -30,20 +33,219 @@ REQUIRED_CFG_SETTINGS = {
670 "PagerDuty service to Mattermost Channel": [],
671 }
672 BOT_CFG_SECTION = "MattermostBot"
673+# TODO: get it from the metadata.yaml dynamically
674+CONTAINER_NAME = "mm-pd-bot"
675
676
677 class MmPdBotK8sCharmConfigError(Exception):
678 """Exception when configuration is bad."""
679
680
681+class MmPdBotPebbleLayerError(Exception):
682+ """Exception when configuration is bad."""
683+
684+
685 class MmPdBotK8sCharm(CharmBase):
686 def __init__(self, *args) -> None:
687 super().__init__(*args)
688
689- self.framework.observe(self.on.start, self.configure_pod)
690- self.framework.observe(self.on.config_changed, self.configure_pod)
691- self.framework.observe(self.on.leader_elected, self.configure_pod)
692- self.framework.observe(self.on.upgrade_charm, self.configure_pod)
693+ # PEBBLE: on.start probably useless
694+ # Need the following actions
695+ # 1. Validate configuration (everything except secrets comapred to before)
696+ # 2. Check ingress availability. block on lack of relation
697+ # 3. Check secrets availability. Block on lack of tokens for PD and MM
698+ # 4. 3. must be set via an action in a second time, for now still in the conf file.
699+ # 5. Application leader is relevant for any action towards MM/PD. No need to do every actions several times.
700+ # 6. Upgrade-charm does not call config-changed so it needs to be handled
701+
702+ # self.framework.observe(self.on.start, self.configure_pod)
703+
704+ # No need for install as config-changed is called right after
705+ # https://juju.is/docs/sdk/events
706+ self.framework.observe(self.on.config_changed, self.configure_workload)
707+ # The name of the event is the key in metadata.yaml for "containers" + _pebble_ready
708+ # See https://ops.readthedocs.io/en/latest/#ops.charm.PebbleReadyEvent
709+ self.framework.observe(self.on.mm_pd_bot_pebble_ready, self._on_pebble_ready)
710+ self.framework.observe(self.on.ingress_relation_changed, self._configure_ingress)
711+ self.framework.observe(self.on.set_mattermost_bot_token_action, self._set_mm_bot_token)
712+ self.framework.observe(self.on.set_pagerduty_credentials_action, self._set_pagerduty_credentials)
713+
714+ self.ingress = IngressRequires(self, {})
715+ self.bot_config = self._load_mm_pd_bot_configuration(self.config["mm_pd_bot_cfg"])
716+
717+ def _configure_ingress(self, event: ops.charm.EventBase) -> None:
718+ """Configure the ingress relation."""
719+
720+ if not self._check_charm_config():
721+ logger.info("Configuration is not ready yet. Deferring ingress configuration.")
722+ event.defer()
723+ return
724+ container_port = self.bot_config.getint("httpd", "listen-port")
725+ # magic_uuid = self.bot_config.get("httpd", "magic-uuid")
726+ ingress_data = {
727+ "service-hostname": self.config["external_hostname"],
728+ "service-name": self.app.name,
729+ "service-port": container_port,
730+ # Default is empty string and the ingress won't configure it if that's the case
731+ # TODO: Check how a switch from 443 to 80 behave on ingress side
732+ "tls-secret-name": self.config["tls_secret_name"],
733+ }
734+
735+ logger.debug("Ingress relation data: %s", ingress_data)
736+ self.ingress.update_config(ingress_data)
737+
738+ def _get_pebble_layers(self) -> dict:
739+ """Configure pebble layers.
740+
741+ :returns: a dict representing the various pebble layers.
742+ """
743+
744+ mm_pd_bot_layers = {
745+ "summary": "Pebble layers for Mattermost PagerDuty Bot",
746+ "description": "All pebble layers for the various mm-pd-bot workload",
747+ "services": {
748+ "mm-pd-bot": {
749+ "override": "replace",
750+ "summary": "mm-pd-bot",
751+ "command": "bash -c '/app/bot.py --log-level debug /app/bot.cfg &>/proc/1/fd/1'",
752+ "startup": "enabled",
753+ }
754+ },
755+ }
756+
757+ return mm_pd_bot_layers
758+
759+ def _set_pagerduty_credentials(self, event: ops.charm.ActionEvent):
760+ """Set the PagerDuty token in pebble layer
761+
762+ :param event: ActionEvent
763+ """
764+ if not self._is_pebble_ready():
765+ logger.info("Pebble is not ready.")
766+ event.defer()
767+ return
768+ account: str = event.params.get("account")
769+ token: str = event.params.get("token")
770+ msg: str = "Setting PagerDuty credentials"
771+ logger.debug(msg)
772+ event.log(msg)
773+ self.bot_config["PagerDuty"]["account"] = account
774+ self.bot_config["PagerDuty"]["api-token"] = token
775+ self.configure_workload(event)
776+
777+ def _set_mm_bot_token(self, event: ops.charm.ActionEvent):
778+ """Set the Mattermost Bot token in pebble layer
779+
780+ :param event: ActionEvent
781+ """
782+ if not self._is_pebble_ready():
783+ logger.info("Pebble is not ready.")
784+ event.defer()
785+ return
786+ token: str = event.params.get("token")
787+ msg: str = "Setting Mattermost Bot token"
788+ logger.debug(msg)
789+ event.log(msg)
790+ self.bot_config[BOT_CFG_SECTION]["bot_token"] = token
791+ self.configure_workload(event)
792+
793+ def _load_current_config(self):
794+ """Load the current mm-pd-bot configuration."""
795+ try:
796+ deployed_config = self.container.pull("/app/bot.cfg").read()
797+ self.current_config = self._load_mm_pd_bot_configuration(deployed_config)
798+ except PathError:
799+ logger.debug("Current configuration /app/bot.cfg does not exist.")
800+ self.current_config = None
801+
802+ def _on_pebble_ready(self, event: ops.charm.EventBase) -> None:
803+ """Handle PebbleReady event
804+
805+ :param event: Event fired.
806+ """
807+ if not self._is_pebble_ready():
808+ logger.error("PebbleReady received but pebble is not ready.")
809+ event.defer()
810+ return
811+ logger.info("Pebble is ready.")
812+ self.configure_workload(event)
813+
814+ def _is_pebble_ready(self) -> bool:
815+ """Check that Pebble is ready.
816+
817+ Try to get a plan from Pebble. If that fails, it will throw an exception and we will know it was not ready.
818+ :returns: True if Pebble is ready. False otherwise.
819+ """
820+ logger.debug("Checking Pebble status.")
821+ self.container = self.unit.get_container(CONTAINER_NAME)
822+ try:
823+ self.container.get_plan().to_dict().get("services", {})
824+ logger.debug("Pebble is ready.")
825+ logger.debug("Load current configuration if it exists")
826+ self._load_current_config()
827+ except ConnectionError as exc:
828+ logger.info("Pebble is not ready yet.")
829+ logger.debug(str(exc))
830+ return False
831+ return True
832+
833+ def configure_workload(self, event: ops.charm.EventBase) -> None:
834+ """Configure the workload container.
835+
836+ Configure the workload container and the application once pebble is started and ready.
837+
838+ :param event: Event fired.
839+ """
840+
841+ if not self._is_pebble_ready():
842+ logger.info("Aborting workload container configuration.")
843+ if event:
844+ event.defer()
845+ return
846+
847+ if not self._check_charm_config():
848+ logger.info("Charm configuration is not ready.")
849+ return
850+
851+ mm_pd_bot_layers = self._get_pebble_layers()
852+
853+ # Ensure the ingress relation has the external hostname.
854+ self._configure_ingress(event)
855+
856+ restart = False
857+ if self.bot_config != self.current_config:
858+ logger.info("Configuration needs to be updated. Restarting mm-pd-bot...")
859+ with io.StringIO() as current, io.StringIO() as new:
860+ if self.current_config:
861+ self.current_config.write(current)
862+ current.seek(0)
863+ logger.debug("Current config : %s", current.read())
864+ self.bot_config.write(new)
865+ new.seek(0)
866+ logger.debug("New config : %s", new.read())
867+ restart = True
868+
869+ # Compare the current plan with the one we generated
870+ services = self.container.get_plan().to_dict().get("services", {})
871+ if services != mm_pd_bot_layers["services"]:
872+ logger.info("Adding pebble layer: %s", mm_pd_bot_layers)
873+ self.container.add_layer("mm_pd_bot", mm_pd_bot_layers, combine=True)
874+
875+ if restart:
876+ # TODO Figure out the UID and lessen the permission or inject environment variables)
877+ with io.StringIO() as buffer:
878+ logger.debug("Pushing file bot.cfg to /app/bot.cfg")
879+ self.bot_config.write(buffer)
880+ buffer.seek(0)
881+ self.container.push(path="/app/bot.cfg", permissions=0o644, source=buffer.read())
882+ status = self.container.get_service(CONTAINER_NAME)
883+ if status.current == ServiceStatus.ACTIVE:
884+ logger.debug("Pebble service %s is active. Stopping it", CONTAINER_NAME)
885+ self.container.stop(CONTAINER_NAME)
886+ logger.debug("Starting pebble service %s", CONTAINER_NAME)
887+ self.container.start(CONTAINER_NAME)
888+
889+ self.unit.status = ActiveStatus()
890
891 def _validate_bot_creds(self) -> None:
892 """Validate the authentication method input for the bot.
893@@ -52,15 +254,38 @@ class MmPdBotK8sCharm(CharmBase):
894 bot_password.
895 """
896 bot_token = self.bot_config.get(BOT_CFG_SECTION, "bot_token", fallback=None)
897- bot_login = self.bot_config.get(BOT_CFG_SECTION, "bot_login", fallback=None)
898- bot_password = self.bot_config.get(BOT_CFG_SECTION, "bot_password", fallback=None)
899-
900- if not bot_token and (not bot_login or not bot_password):
901- err_msg = "bot_token or bot_login/bot_password in {0} section required".format(BOT_CFG_SECTION)
902+ pd_account = self.bot_config.get("PagerDuty", "account", fallback=None)
903+ pd_token = self.bot_config.get("PagerDuty", "api-token", fallback=None)
904+ try:
905+ cur_bot_token = self.current_config.get(BOT_CFG_SECTION, "bot_token", fallback=None)
906+ cur_pd_account = self.current_config.get("PagerDuty", "account", fallback=None)
907+ cur_pd_token = self.current_config.get("PagerDuty", "api-token", fallback=None)
908+ except AttributeError: # self.current_config is None
909+ cur_bot_token = None
910+ cur_pd_account = None
911+ cur_pd_token = None
912+
913+ # Load the values present in current configuration if not specified.
914+ # This way we retrieve previously set values and overwrite when using an action
915+ if not bot_token and cur_bot_token:
916+ logger.debug("Current secret Mattermost bot_token already set")
917+ self.bot_config[BOT_CFG_SECTION]["bot_token"] = cur_bot_token
918+ bot_token = cur_bot_token
919+ if not pd_account and cur_pd_account:
920+ logger.debug("Current secret PagerDuty account already set")
921+ self.bot_config["PagerDuty"]["account"] = cur_pd_account
922+ pd_account = cur_pd_account
923+ if not pd_token and cur_pd_token:
924+ logger.debug("Current secret PagerDuty api-token already set")
925+ self.bot_config["PagerDuty"]["api-token"] = cur_pd_token
926+ pd_token = cur_pd_token
927+
928+ if not bot_token:
929+ err_msg = 'bot_token in {0} section required. See juju action "set-mattermost-bot-token".'
930 logger.error(err_msg)
931 raise MmPdBotK8sCharmConfigError(err_msg)
932- if bot_token and bot_login:
933- err_msg = "bot_token and bot_login are both set. Pick one of them"
934+ if not pd_account or not pd_token:
935+ err_msg = 'PagerDuty credentials incomplete. See juju action "set-pagerduty-credentials".'
936 logger.error(err_msg)
937 raise MmPdBotK8sCharmConfigError(err_msg)
938
939@@ -71,7 +296,7 @@ class MmPdBotK8sCharm(CharmBase):
940 """
941 errors = []
942 for required in REQUIRED_JUJU_SETTINGS:
943- if not self.model.config[required]:
944+ if not self.config[required]:
945 logger.error("Required setting empty: %s", required)
946 errors.append(required)
947 if errors:
948@@ -118,156 +343,44 @@ class MmPdBotK8sCharm(CharmBase):
949 :returns: Returns a configparser.ConfigParser object with the configuration.
950 :raises: MmPdBotK8sCharmConfigError if the configuration is invalid.
951 """
952- config = configparser.ConfigParser(allow_no_value=True)
953+ config = configparser.ConfigParser(allow_no_value=True, dict_type=collections.OrderedDict)
954 try:
955 config.read_string(configuration)
956- except configparser.Error as exc:
957- err_msg = "Error while parsing mm_pd_bot_cfg setting"
958+ except configparser.Error:
959+ err_msg = "Error while parsing mm_pd_bot_cfg setting. \
960+ Check https://git.launchpad.net/charm-k8s-mm-pd-bot/tree/examples/bot.cfg."
961 logger.error(err_msg)
962- raise MmPdBotK8sCharmConfigError(err_msg) from exc
963+ self.unit.status = BlockedStatus(err_msg)
964+ return
965+ for section in config._sections:
966+ config._sections[section] = collections.OrderedDict(
967+ sorted(config._sections[section].items(), key=lambda t: t[0])
968+ )
969+ config._sections = collections.OrderedDict(sorted(config._sections.items(), key=lambda t: t[0]))
970 return config
971
972 def _validate_mm_pd_bot_configuration(self) -> None:
973 """Check the configuration part related to mm_pd_bot configuration."""
974- self.bot_config = self._load_mm_pd_bot_configuration(self.model.config["mm_pd_bot_cfg"])
975+ logger.debug("Validate mm-pd-bot configuration.")
976 self._validate_mm_pd_bot_configuration_content()
977+ logger.debug("Validate mm-pd-bot secrets.")
978 self._validate_bot_creds()
979
980- def _check_for_config_problems(self) -> None:
981+ def _check_charm_config(self) -> bool:
982 """Check that the mandatory configuration items are all set.
983
984 Each method will raise an exception if an error is detected.
985- """
986- self._check_juju_settings()
987- self._validate_mm_pd_bot_configuration()
988-
989- def _make_pod_config(self) -> dict:
990- """Return an envConfig with some core configuration.
991-
992- :returns: A dictionary used for envConfig in podspec
993- """
994- config = self.model.config
995- pod_config = {
996- "MM_PD_BOT_CFG": config["mm_pd_bot_cfg"],
997- }
998-
999- return pod_config
1000-
1001- def _update_pod_spec_for_k8s_ingress(self, pod_spec: dict) -> None:
1002- """Add resources to pod_spec configuring site ingress, if needed.
1003-
1004- :param pod_spec: pod spec v3 as defined by juju.
1005- """
1006-
1007- hostname = self.bot_config.get("httpd", "hostname")
1008- container_port = self.bot_config.getint("httpd", "listen-port")
1009- magic_uuid = self.bot_config.get("httpd", "magic-uuid")
1010- tls_secret_name = self.model.config.get("tls_secret_name", None)
1011-
1012- if tls_secret_name:
1013- scheme = "https"
1014- else:
1015- scheme = "http"
1016-
1017- bot_listening_url = "{0}://{1}:{2}/{3}/".format(scheme, hostname, container_port, magic_uuid)
1018- bot_listening_url_parsed = urlparse(bot_listening_url)
1019-
1020- annotations = {}
1021- ingress = {
1022- "name": "{}-ingress".format(self.app.name),
1023- "spec": {
1024- "rules": [
1025- {
1026- "host": bot_listening_url_parsed.hostname,
1027- "http": {
1028- "paths": [
1029- {
1030- "path": "/{0}/".format(magic_uuid),
1031- "backend": {"serviceName": self.app.name, "servicePort": container_port},
1032- }
1033- ]
1034- },
1035- }
1036- ]
1037- },
1038- }
1039- if tls_secret_name:
1040- ingress["spec"]["tls"] = [
1041- {"hosts": [bot_listening_url_parsed.hostname], "secretName": tls_secret_name},
1042- ]
1043- else:
1044- annotations["nginx.ingress.kubernetes.io/ssl-redirect"] = "false"
1045-
1046- ingress_whitelist_source_range = self.model.config.get("ingress_whitelist_source_range", None)
1047- if ingress_whitelist_source_range:
1048- annotations["nginx.ingress.kubernetes.io/whitelist-source-range"] = ingress_whitelist_source_range
1049-
1050- if annotations:
1051- ingress["annotations"] = annotations
1052-
1053- # Due to https://github.com/canonical/operator/issues/293 we
1054- # can't use pod.set_spec's k8s_resources argument.
1055- resources = pod_spec.get("kubernetesResources", {})
1056- resources["ingressResources"] = [ingress]
1057- pod_spec["kubernetesResources"] = resources
1058-
1059- def _make_pod_spec(self) -> None:
1060- """Create a pod spec with some core configuration."""
1061-
1062- config = self.model.config
1063- container_port = self.bot_config.getint("httpd", "listen-port")
1064- # TODO: The bot replies 501 on HTTP GET so until we have something to probe readiness, magic-uuid is not needed
1065- # here
1066- # magic_uuid = self.bot_config.get('httpd', 'magic-uuid')
1067- image_details = {
1068- "imagePath": config["image_path"],
1069- }
1070- if config.get("image_username", None):
1071- image_details.update({"username": config["image_username"], "password": config["image_password"]})
1072- pod_config = self._make_pod_config()
1073-
1074- return {
1075- "version": 3, # otherwise resources are ignored
1076- "containers": [
1077- {
1078- "name": self.app.name,
1079- "imageDetails": image_details,
1080- # TODO: debatable. The idea is that if you want to force an update with the same image name, you
1081- # don't need to empty kubelet cache on each node to have the right version.
1082- # This implies a performance drop upon start.
1083- "imagePullPolicy": "Always",
1084- "ports": [{"containerPort": container_port, "protocol": "TCP"}],
1085- "envConfig": pod_config,
1086- # TODO: Add readiness probe in the bot.
1087- # 'kubernetes': {
1088- # 'readinessProbe': {'httpGet': {'path': '/{0}/'.format(magic_uuid), 'port': container_port}},
1089- # },
1090- }
1091- ],
1092- }
1093-
1094- def configure_pod(self, event: ops.framework.EventBase) -> None:
1095- """Assemble the pod spec and apply it, if possible.
1096-
1097- :param event: Event that triggered the method.
1098+ :returns: True if the configuration is valid. False otherwise.
1099 """
1100
1101- if not self.unit.is_leader():
1102- self.unit.status = ActiveStatus()
1103- return
1104 try:
1105- self._check_for_config_problems()
1106+ self._check_juju_settings()
1107+ self._validate_mm_pd_bot_configuration()
1108 except MmPdBotK8sCharmConfigError as exc:
1109 self.unit.status = BlockedStatus(str(exc))
1110- return
1111-
1112- logger.info("Assembling pod spec")
1113- pod_spec = self._make_pod_spec()
1114- self._update_pod_spec_for_k8s_ingress(pod_spec)
1115- logger.info("Setting pod spec")
1116- self.model.pod.set_spec(pod_spec)
1117- self.unit.status = ActiveStatus()
1118+ return False
1119+ return True
1120
1121
1122 if __name__ == "__main__": # pragma: no cover
1123- main(MmPdBotK8sCharm, use_juju_for_storage=True)
1124+ main(MmPdBotK8sCharm)
1125diff --git a/tests/unit/scenario.py b/tests/unit/scenario.py
1126index e46c189..212ca54 100644
1127--- a/tests/unit/scenario.py
1128+++ b/tests/unit/scenario.py
1129@@ -57,37 +57,35 @@ def get_juju_settings_default_value() -> list:
1130 JUJU_SETTINGS_DEFAULT = get_juju_settings_default_value()
1131
1132 TEST_JUJU_SETTINGS = {
1133- "empty_image_path": {
1134- "config": {"mm_pd_bot_cfg": "dummy"},
1135- "logger": ["ERROR:root:Required setting empty: image_path"],
1136- "expected": "Required setting(s) empty: image_path",
1137- },
1138 "empty_mm_pd_bot_cfg": {
1139- "config": {"image_path": "mm_pd_bot:devel"},
1140+ "config": {"external_hostname": "mm-pd-bot.internal"},
1141 "logger": ["ERROR:root:Required setting empty: mm_pd_bot_cfg"],
1142 "expected": "Required setting(s) empty: mm_pd_bot_cfg",
1143 },
1144 "empty_all_settings": {
1145 "config": {},
1146- "logger": ["ERROR:root:Required setting empty: image_path", "ERROR:root:Required setting empty: mm_pd_bot_cfg"],
1147- "expected": "Required setting(s) empty: image_path, mm_pd_bot_cfg",
1148+ "logger": [
1149+ "ERROR:root:Required setting empty: external_hostname",
1150+ "ERROR:root:Required setting empty: mm_pd_bot_cfg",
1151+ ],
1152+ "expected": "Required setting(s) empty: external_hostname, mm_pd_bot_cfg",
1153 },
1154 }
1155
1156
1157 VALIDATE_MM_PD_BOT_CFG = {
1158 "good_configuration": {
1159- "config": {"image_path": "mm_pd_bot:devel", "mm_pd_bot_cfg": get_cfg_content("good_configuration")},
1160+ "config": {"external_hostname": "mm-pd-bot.internal", "mm_pd_bot_cfg": get_cfg_content("good_configuration")},
1161 "expected": True,
1162 "logger": [],
1163 },
1164 "bad_formatted_configuration": {
1165- "config": {"image_path": "mm_pd_bot:devel", "mm_pd_bot_cfg": "[PagerDuty"},
1166+ "config": {"external_hostname": "mm-pd-bot.internal", "mm_pd_bot_cfg": "[PagerDuty"},
1167 "expected": "Error while parsing mm_pd_bot_cfg setting",
1168 "logger": ["ERROR:root:Error while parsing mm_pd_bot_cfg setting"],
1169 },
1170 "missing_sections": {
1171- "config": {"image_path": "mm_pd_bot:devel", "mm_pd_bot_cfg": get_cfg_content("missing_sections")},
1172+ "config": {"external_hostname": "mm-pd-bot.internal", "mm_pd_bot_cfg": get_cfg_content("missing_sections")},
1173 "expected": "Required section(s) missing in bot configuration file: "
1174 "MattermostBot, Prometheus, httpd, nickname to email",
1175 "logger": [
1176@@ -98,7 +96,7 @@ VALIDATE_MM_PD_BOT_CFG = {
1177 ],
1178 },
1179 "missing_options": {
1180- "config": {"image_path": "mm_pd_bot:devel", "mm_pd_bot_cfg": get_cfg_content("missing_options")},
1181+ "config": {"external_hostname": "mm-pd-bot.internal", "mm_pd_bot_cfg": get_cfg_content("missing_options")},
1182 "expected": "Required option(s) missing in section MattermostBot: bot_team, bot_url, "
1183 "Required option(s) missing in section PagerDuty: api-token, "
1184 "Required option(s) missing in section httpd: hostname, listen-ip, listen-port, magic-uuid",
1185@@ -131,7 +129,7 @@ VALIDATE_POD_SPEC = {
1186 "version": 3, # otherwise resources are ignored
1187 "containers": [
1188 {
1189- "name": "mm-pd-bot-k8s",
1190+ "name": "mm-pd-bot",
1191 "imageDetails": {
1192 "imagePath": "mm_pd_bot:devel",
1193 },
1194@@ -153,7 +151,7 @@ VALIDATE_POD_SPEC = {
1195 "version": 3, # otherwise resources are ignored
1196 "containers": [
1197 {
1198- "name": "mm-pd-bot-k8s",
1199+ "name": "mm-pd-bot",
1200 "imageDetails": {"imagePath": "mm_pd_bot:devel", "username": "rockity", "password": "rock"},
1201 "imagePullPolicy": "Always",
1202 "ports": [{"containerPort": 2160, "protocol": "TCP"}],
1203@@ -172,7 +170,7 @@ VALIDATE_POD_SPEC_AND_INGRESS = {
1204 "version": 3, # otherwise resources are ignored
1205 "containers": [
1206 {
1207- "name": "mm-pd-bot-k8s",
1208+ "name": "mm-pd-bot",
1209 "imageDetails": {"imagePath": "mm_pd_bot:devel"},
1210 "imagePullPolicy": "Always",
1211 "ports": [{"containerPort": 2160, "protocol": "TCP"}],
1212@@ -182,7 +180,7 @@ VALIDATE_POD_SPEC_AND_INGRESS = {
1213 "kubernetesResources": {
1214 "ingressResources": [
1215 {
1216- "name": "mm-pd-bot-k8s-ingress",
1217+ "name": "mm-pd-bot-ingress",
1218 "spec": {
1219 "rules": [
1220 {
1221@@ -191,7 +189,7 @@ VALIDATE_POD_SPEC_AND_INGRESS = {
1222 "paths": [
1223 {
1224 "path": "/bdddcacb-ab42-40ac-9106-4275c1db1519/",
1225- "backend": {"serviceName": "mm-pd-bot-k8s", "servicePort": 2160},
1226+ "backend": {"serviceName": "mm-pd-bot", "servicePort": 2160},
1227 },
1228 ],
1229 },
1230@@ -215,7 +213,7 @@ VALIDATE_POD_SPEC_AND_INGRESS = {
1231 "version": 3, # otherwise resources are ignored
1232 "containers": [
1233 {
1234- "name": "mm-pd-bot-k8s",
1235+ "name": "mm-pd-bot",
1236 "imageDetails": {"imagePath": "mm_pd_bot:devel"},
1237 "imagePullPolicy": "Always",
1238 "ports": [{"containerPort": 2160, "protocol": "TCP"}],
1239@@ -225,7 +223,7 @@ VALIDATE_POD_SPEC_AND_INGRESS = {
1240 "kubernetesResources": {
1241 "ingressResources": [
1242 {
1243- "name": "mm-pd-bot-k8s-ingress",
1244+ "name": "mm-pd-bot-ingress",
1245 "spec": {
1246 "rules": [
1247 {
1248@@ -234,7 +232,7 @@ VALIDATE_POD_SPEC_AND_INGRESS = {
1249 "paths": [
1250 {
1251 "path": "/bdddcacb-ab42-40ac-9106-4275c1db1519/",
1252- "backend": {"serviceName": "mm-pd-bot-k8s", "servicePort": 2160},
1253+ "backend": {"serviceName": "mm-pd-bot", "servicePort": 2160},
1254 },
1255 ],
1256 },
1257@@ -258,7 +256,7 @@ VALIDATE_POD_SPEC_AND_INGRESS = {
1258 "version": 3, # otherwise resources are ignored
1259 "containers": [
1260 {
1261- "name": "mm-pd-bot-k8s",
1262+ "name": "mm-pd-bot",
1263 "imageDetails": {"imagePath": "mm_pd_bot:devel"},
1264 "imagePullPolicy": "Always",
1265 "ports": [{"containerPort": 2160, "protocol": "TCP"}],
1266@@ -268,7 +266,7 @@ VALIDATE_POD_SPEC_AND_INGRESS = {
1267 "kubernetesResources": {
1268 "ingressResources": [
1269 {
1270- "name": "mm-pd-bot-k8s-ingress",
1271+ "name": "mm-pd-bot-ingress",
1272 "spec": {
1273 "rules": [
1274 {
1275@@ -277,7 +275,7 @@ VALIDATE_POD_SPEC_AND_INGRESS = {
1276 "paths": [
1277 {
1278 "path": "/bdddcacb-ab42-40ac-9106-4275c1db1519/",
1279- "backend": {"serviceName": "mm-pd-bot-k8s", "servicePort": 2160},
1280+ "backend": {"serviceName": "mm-pd-bot", "servicePort": 2160},
1281 },
1282 ],
1283 },
1284diff --git a/tests/unit/test_charm.py b/tests/unit/test_charm.py
1285index d56fcde..93527a3 100755
1286--- a/tests/unit/test_charm.py
1287+++ b/tests/unit/test_charm.py
1288@@ -4,7 +4,7 @@
1289
1290 import unittest
1291 import configparser
1292-from unittest.mock import MagicMock, patch
1293+from unittest.mock import patch
1294
1295 from charm import (
1296 MmPdBotK8sCharm,
1297@@ -12,17 +12,15 @@ from charm import (
1298 )
1299
1300 from ops import testing
1301-from ops.model import (
1302- ActiveStatus,
1303- BlockedStatus,
1304-)
1305+# from ops.model import (
1306+# ActiveStatus,
1307+# BlockedStatus,
1308+# )
1309
1310 from scenario import (
1311 JUJU_SETTINGS_DEFAULT,
1312 TEST_JUJU_SETTINGS,
1313 VALIDATE_MM_PD_BOT_CFG,
1314- VALIDATE_POD_SPEC,
1315- VALIDATE_POD_SPEC_AND_INGRESS,
1316 )
1317
1318
1319@@ -41,14 +39,15 @@ class TestMmPdBotK8sCharmBlockedStatus(unittest.TestCase):
1320
1321 @patch("charm.MmPdBotK8sCharm._validate_mm_pd_bot_configuration")
1322 @patch("charm.MmPdBotK8sCharm._check_juju_settings")
1323- def test_check_for_config_problems(self, mock_juju, mock_mm_pd_bot):
1324+ def test_check_charm_config(self, mock_juju, mock_mm_pd_bot):
1325 """Check the calls for config problems."""
1326- self.harness.charm._check_for_config_problems()
1327+ self.harness.charm._check_charm_config()
1328 mock_juju.assert_called_once()
1329 mock_mm_pd_bot.assert_called_once()
1330
1331 def test_check_juju_settings(self):
1332 """Check the required juju settings."""
1333+ self.harness.disable_hooks()
1334 for scenario, values in TEST_JUJU_SETTINGS.items():
1335 with self.subTest(scenario=scenario):
1336 with self.assertLogs(level="ERROR") as logger:
1337@@ -59,9 +58,11 @@ class TestMmPdBotK8sCharmBlockedStatus(unittest.TestCase):
1338 self.assertEqual(sorted(logger.output), sorted(values["logger"]))
1339 self.assertEqual(str(exc.exception), values["expected"])
1340 self.harness.update_config(JUJU_SETTINGS_DEFAULT) # You need to clean the config after each run
1341+ self.harness.enable_hooks()
1342
1343 def test_validate_mm_pd_bot_configuration(self):
1344 """Check the MM_PD_BOT_CFG string is a valid INI."""
1345+ self.harness.disable_hooks()
1346 for scenario, values in VALIDATE_MM_PD_BOT_CFG.items():
1347 with self.subTest(scenario=scenario):
1348 self.harness.update_config(values["config"])
1349@@ -74,6 +75,7 @@ class TestMmPdBotK8sCharmBlockedStatus(unittest.TestCase):
1350 self.assertEqual(str(exc.exception), values["expected"])
1351 self.assertEqual(sorted(logger.output), sorted(values["logger"]))
1352 self.harness.update_config(JUJU_SETTINGS_DEFAULT) # You need to clean the config after each run
1353+ self.harness.enable_hooks()
1354
1355 def test_load_mm_pd_bot_configuration(self):
1356 """Test the loading of the configuration for the bot."""
1357@@ -86,64 +88,70 @@ class TestMmPdBotK8sCharmBlockedStatus(unittest.TestCase):
1358 loaded_config = self.harness.charm._load_mm_pd_bot_configuration(config)
1359 self.assertEqual(str(exc.exception), VALIDATE_MM_PD_BOT_CFG["bad_formatted_configuration"]["expected"])
1360
1361- def test_configure_pod(self):
1362- """Test the pod configuration."""
1363- mock_event = MagicMock()
1364-
1365- # Good configuration but not leader
1366- self.harness.update_config(VALIDATE_MM_PD_BOT_CFG["good_configuration"]["config"])
1367- self.harness.set_leader(False)
1368- self.harness.charm.unit.status = BlockedStatus("Testing")
1369- self.harness.charm.configure_pod(mock_event)
1370- self.assertEqual(self.harness.charm.unit.status, ActiveStatus())
1371- self.harness.update_config(JUJU_SETTINGS_DEFAULT) # You need to clean the config after each run
1372-
1373- # Good configuration and leader
1374+ def test_configure_workload(self):
1375+ """Test the workload configuration."""
1376+ self.harness.disable_hooks()
1377 self.harness.update_config(VALIDATE_MM_PD_BOT_CFG["good_configuration"]["config"])
1378- self.harness.set_leader(True)
1379- self.harness.charm.unit.status = BlockedStatus("Testing")
1380- self.harness.charm.configure_pod(mock_event)
1381- with self.assertLogs(level="INFO") as logger:
1382- self.harness.charm.configure_pod(mock_event)
1383- self.assertEqual(
1384- sorted(logger.output),
1385- ["INFO:root:Assembling pod spec", "INFO:root:Setting pod spec"],
1386- )
1387- self.assertEqual(self.harness.charm.unit.status, ActiveStatus())
1388- self.harness.update_config(JUJU_SETTINGS_DEFAULT) # You need to clean the config after each run
1389-
1390- # Bad configuration and leader
1391- config = VALIDATE_MM_PD_BOT_CFG["bad_formatted_configuration"]["config"]
1392- expected = VALIDATE_MM_PD_BOT_CFG["bad_formatted_configuration"]["expected"]
1393- self.harness.update_config(config)
1394- self.harness.set_leader(True)
1395- self.harness.charm.configure_pod(mock_event)
1396- self.assertEqual(self.harness.charm.unit.status, BlockedStatus(expected))
1397- self.harness.update_config(JUJU_SETTINGS_DEFAULT) # You need to clean the config after each run
1398-
1399- def test_make_pod_spec(self):
1400- """Check the crafting of the pod spec."""
1401- for scenario, values in VALIDATE_POD_SPEC.items():
1402- with self.subTest(scenario=scenario):
1403- self.harness.update_config(values["config"])
1404- self.harness.charm.bot_config = self.harness.charm._load_mm_pd_bot_configuration(
1405- values["config"]["mm_pd_bot_cfg"]
1406- )
1407- self.assertEqual(self.harness.charm._make_pod_spec(), values["pod_spec"])
1408- self.harness.update_config(JUJU_SETTINGS_DEFAULT) # You need to clean the config after each run
1409-
1410- def test_update_pod_spec_for_k8s_ingress(self):
1411- """Check the crafting of the ingress part of the pod spec."""
1412- for scenario, values in VALIDATE_POD_SPEC_AND_INGRESS.items():
1413- with self.subTest(scenario=scenario):
1414- self.harness.update_config(values["config"])
1415- self.harness.charm.bot_config = self.harness.charm._load_mm_pd_bot_configuration(
1416- values["config"]["mm_pd_bot_cfg"]
1417- )
1418- pod_spec = self.harness.charm._make_pod_spec()
1419- self.harness.charm._update_pod_spec_for_k8s_ingress(pod_spec)
1420- self.assertEqual(pod_spec, values["pod_spec"])
1421- self.harness.update_config(JUJU_SETTINGS_DEFAULT) # You need to clean the config after each run
1422+ self.harness.charm.configure_workload(None)
1423+ self.harness.enable_hooks()
1424+
1425+ # def test_configure_pod(self):
1426+ # """Test the pod configuration."""
1427+
1428+ # # Good configuration but not leader
1429+ # self.harness.update_config(VALIDATE_MM_PD_BOT_CFG["good_configuration"]["config"])
1430+ # self.harness.set_leader(False)
1431+ # self.harness.charm.unit.status = BlockedStatus("Testing")
1432+ # self.harness.charm.configure_pod(mock_event)
1433+ # self.assertEqual(self.harness.charm.unit.status, ActiveStatus())
1434+ # self.harness.update_config(JUJU_SETTINGS_DEFAULT) # You need to clean the config after each run
1435+
1436+ # # Good configuration and leader
1437+ # self.harness.update_config(VALIDATE_MM_PD_BOT_CFG["good_configuration"]["config"])
1438+ # self.harness.set_leader(True)
1439+ # self.harness.charm.unit.status = BlockedStatus("Testing")
1440+ # self.harness.charm.configure_pod(mock_event)
1441+ # with self.assertLogs(level="INFO") as logger:
1442+ # self.harness.charm.configure_pod(mock_event)
1443+ # self.assertEqual(
1444+ # sorted(logger.output),
1445+ # ["INFO:root:Assembling pod spec", "INFO:root:Setting pod spec"],
1446+ # )
1447+ # self.assertEqual(self.harness.charm.unit.status, ActiveStatus())
1448+ # self.harness.update_config(JUJU_SETTINGS_DEFAULT) # You need to clean the config after each run
1449+
1450+ # # Bad configuration and leader
1451+ # config = VALIDATE_MM_PD_BOT_CFG["bad_formatted_configuration"]["config"]
1452+ # expected = VALIDATE_MM_PD_BOT_CFG["bad_formatted_configuration"]["expected"]
1453+ # self.harness.update_config(config)
1454+ # self.harness.set_leader(True)
1455+ # self.harness.charm.configure_pod(mock_event)
1456+ # self.assertEqual(self.harness.charm.unit.status, BlockedStatus(expected))
1457+ # self.harness.update_config(JUJU_SETTINGS_DEFAULT) # You need to clean the config after each run
1458+
1459+ # def test_make_pod_spec(self):
1460+ # """Check the crafting of the pod spec."""
1461+ # for scenario, values in VALIDATE_POD_SPEC.items():
1462+ # with self.subTest(scenario=scenario):
1463+ # self.harness.update_config(values["config"])
1464+ # self.harness.charm.bot_config = self.harness.charm._load_mm_pd_bot_configuration(
1465+ # values["config"]["mm_pd_bot_cfg"]
1466+ # )
1467+ # self.assertEqual(self.harness.charm._make_pod_spec(), values["pod_spec"])
1468+ # self.harness.update_config(JUJU_SETTINGS_DEFAULT) # You need to clean the config after each run
1469+
1470+ # def test_update_pod_spec_for_k8s_ingress(self):
1471+ # """Check the crafting of the ingress part of the pod spec."""
1472+ # for scenario, values in VALIDATE_POD_SPEC_AND_INGRESS.items():
1473+ # with self.subTest(scenario=scenario):
1474+ # self.harness.update_config(values["config"])
1475+ # self.harness.charm.bot_config = self.harness.charm._load_mm_pd_bot_configuration(
1476+ # values["config"]["mm_pd_bot_cfg"]
1477+ # )
1478+ # pod_spec = self.harness.charm._make_pod_spec()
1479+ # self.harness.charm._update_pod_spec_for_k8s_ingress(pod_spec)
1480+ # self.assertEqual(pod_spec, values["pod_spec"])
1481+ # self.harness.update_config(JUJU_SETTINGS_DEFAULT) # You need to clean the config after each run
1482
1483
1484 if __name__ == "__main__":

Subscribers

People subscribed via source and target branches