Merge ~axino/charm-canonical-is-reviewbot/+git/charm-canonical-is-reviewbot:axino into charm-canonical-is-reviewbot:main

Proposed by Junien F
Status: Merged
Approved by: Junien F
Approved revision: 971bbcf68ce690076c434f1c292c6d71ddd11148
Merged at revision: 9763831c66fb1ba544d4b657de58f8bac49ee979
Proposed branch: ~axino/charm-canonical-is-reviewbot/+git/charm-canonical-is-reviewbot:axino
Merge into: charm-canonical-is-reviewbot:main
Diff against target: 735 lines (+663/-0)
12 files modified
.gitignore (+9/-0)
CONTRIBUTING.md (+33/-0)
LICENSE (+202/-0)
README.md (+7/-0)
charmcraft.yaml (+11/-0)
config.yaml (+30/-0)
metadata.yaml (+18/-0)
pyproject.toml (+39/-0)
requirements.txt (+1/-0)
src/charm.py (+121/-0)
tests/unit/test_charm.py (+108/-0)
tox.ini (+84/-0)
Reviewer Review Type Date Requested Status
Barry Price Approve
Review via email: mp+436216@code.launchpad.net

Commit message

initial commit

To post a comment you must log in.
Revision history for this message
Barry Price (barryprice) wrote :

Tiny nitpick inline, LGTM otherwise

review: Approve
Revision history for this message
Junien F (axino) wrote :

Fixed, thanks !

Revision history for this message
🤖 Canonical IS Merge Bot (canonical-is-mergebot) wrote :

This merge proposal is being monitored by mergebot. Change the status to Approved to merge.

Revision history for this message
🤖 Canonical IS Merge Bot (canonical-is-mergebot) wrote :

Change has no commit message, setting status to needs review.

Revision history for this message
🤖 Canonical IS Merge Bot (canonical-is-mergebot) wrote :

Change successfully merged at revision 9763831c66fb1ba544d4b657de58f8bac49ee979

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/.gitignore b/.gitignore
2new file mode 100644
3index 0000000..a26d707
4--- /dev/null
5+++ b/.gitignore
6@@ -0,0 +1,9 @@
7+venv/
8+build/
9+*.charm
10+.tox/
11+.coverage
12+__pycache__/
13+*.py[cod]
14+.idea
15+.vscode/
16diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
17new file mode 100644
18index 0000000..2969183
19--- /dev/null
20+++ b/CONTRIBUTING.md
21@@ -0,0 +1,33 @@
22+# Contributing
23+
24+To make contributions to this charm, you'll need a working [development setup](https://juju.is/docs/sdk/dev-setup).
25+
26+You can use the environments created by `tox` for development:
27+
28+```shell
29+tox --notest -e unit
30+source .tox/unit/bin/activate
31+```
32+
33+## Testing
34+
35+This project uses `tox` for managing test environments. There are some pre-configured environments
36+that can be used for linting and formatting code when you're preparing contributions to the charm:
37+
38+```shell
39+tox -e fmt # update your code according to linting rules
40+tox -e lint # code style
41+tox -e unit # unit tests
42+tox -e integration # integration tests
43+tox # runs 'lint' and 'unit' environments
44+```
45+
46+## Build the charm
47+
48+Build the charm in this git repository using:
49+
50+```shell
51+charmcraft pack
52+```
53+
54+<!-- You may want to include any contribution/style guidelines in this document>
55diff --git a/LICENSE b/LICENSE
56new file mode 100644
57index 0000000..ecbda55
58--- /dev/null
59+++ b/LICENSE
60@@ -0,0 +1,202 @@
61+
62+ Apache License
63+ Version 2.0, January 2004
64+ http://www.apache.org/licenses/
65+
66+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
67+
68+ 1. Definitions.
69+
70+ "License" shall mean the terms and conditions for use, reproduction,
71+ and distribution as defined by Sections 1 through 9 of this document.
72+
73+ "Licensor" shall mean the copyright owner or entity authorized by
74+ the copyright owner that is granting the License.
75+
76+ "Legal Entity" shall mean the union of the acting entity and all
77+ other entities that control, are controlled by, or are under common
78+ control with that entity. For the purposes of this definition,
79+ "control" means (i) the power, direct or indirect, to cause the
80+ direction or management of such entity, whether by contract or
81+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
82+ outstanding shares, or (iii) beneficial ownership of such entity.
83+
84+ "You" (or "Your") shall mean an individual or Legal Entity
85+ exercising permissions granted by this License.
86+
87+ "Source" form shall mean the preferred form for making modifications,
88+ including but not limited to software source code, documentation
89+ source, and configuration files.
90+
91+ "Object" form shall mean any form resulting from mechanical
92+ transformation or translation of a Source form, including but
93+ not limited to compiled object code, generated documentation,
94+ and conversions to other media types.
95+
96+ "Work" shall mean the work of authorship, whether in Source or
97+ Object form, made available under the License, as indicated by a
98+ copyright notice that is included in or attached to the work
99+ (an example is provided in the Appendix below).
100+
101+ "Derivative Works" shall mean any work, whether in Source or Object
102+ form, that is based on (or derived from) the Work and for which the
103+ editorial revisions, annotations, elaborations, or other modifications
104+ represent, as a whole, an original work of authorship. For the purposes
105+ of this License, Derivative Works shall not include works that remain
106+ separable from, or merely link (or bind by name) to the interfaces of,
107+ the Work and Derivative Works thereof.
108+
109+ "Contribution" shall mean any work of authorship, including
110+ the original version of the Work and any modifications or additions
111+ to that Work or Derivative Works thereof, that is intentionally
112+ submitted to Licensor for inclusion in the Work by the copyright owner
113+ or by an individual or Legal Entity authorized to submit on behalf of
114+ the copyright owner. For the purposes of this definition, "submitted"
115+ means any form of electronic, verbal, or written communication sent
116+ to the Licensor or its representatives, including but not limited to
117+ communication on electronic mailing lists, source code control systems,
118+ and issue tracking systems that are managed by, or on behalf of, the
119+ Licensor for the purpose of discussing and improving the Work, but
120+ excluding communication that is conspicuously marked or otherwise
121+ designated in writing by the copyright owner as "Not a Contribution."
122+
123+ "Contributor" shall mean Licensor and any individual or Legal Entity
124+ on behalf of whom a Contribution has been received by Licensor and
125+ subsequently incorporated within the Work.
126+
127+ 2. Grant of Copyright License. Subject to the terms and conditions of
128+ this License, each Contributor hereby grants to You a perpetual,
129+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
130+ copyright license to reproduce, prepare Derivative Works of,
131+ publicly display, publicly perform, sublicense, and distribute the
132+ Work and such Derivative Works in Source or Object form.
133+
134+ 3. Grant of Patent License. Subject to the terms and conditions of
135+ this License, each Contributor hereby grants to You a perpetual,
136+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
137+ (except as stated in this section) patent license to make, have made,
138+ use, offer to sell, sell, import, and otherwise transfer the Work,
139+ where such license applies only to those patent claims licensable
140+ by such Contributor that are necessarily infringed by their
141+ Contribution(s) alone or by combination of their Contribution(s)
142+ with the Work to which such Contribution(s) was submitted. If You
143+ institute patent litigation against any entity (including a
144+ cross-claim or counterclaim in a lawsuit) alleging that the Work
145+ or a Contribution incorporated within the Work constitutes direct
146+ or contributory patent infringement, then any patent licenses
147+ granted to You under this License for that Work shall terminate
148+ as of the date such litigation is filed.
149+
150+ 4. Redistribution. You may reproduce and distribute copies of the
151+ Work or Derivative Works thereof in any medium, with or without
152+ modifications, and in Source or Object form, provided that You
153+ meet the following conditions:
154+
155+ (a) You must give any other recipients of the Work or
156+ Derivative Works a copy of this License; and
157+
158+ (b) You must cause any modified files to carry prominent notices
159+ stating that You changed the files; and
160+
161+ (c) You must retain, in the Source form of any Derivative Works
162+ that You distribute, all copyright, patent, trademark, and
163+ attribution notices from the Source form of the Work,
164+ excluding those notices that do not pertain to any part of
165+ the Derivative Works; and
166+
167+ (d) If the Work includes a "NOTICE" text file as part of its
168+ distribution, then any Derivative Works that You distribute must
169+ include a readable copy of the attribution notices contained
170+ within such NOTICE file, excluding those notices that do not
171+ pertain to any part of the Derivative Works, in at least one
172+ of the following places: within a NOTICE text file distributed
173+ as part of the Derivative Works; within the Source form or
174+ documentation, if provided along with the Derivative Works; or,
175+ within a display generated by the Derivative Works, if and
176+ wherever such third-party notices normally appear. The contents
177+ of the NOTICE file are for informational purposes only and
178+ do not modify the License. You may add Your own attribution
179+ notices within Derivative Works that You distribute, alongside
180+ or as an addendum to the NOTICE text from the Work, provided
181+ that such additional attribution notices cannot be construed
182+ as modifying the License.
183+
184+ You may add Your own copyright statement to Your modifications and
185+ may provide additional or different license terms and conditions
186+ for use, reproduction, or distribution of Your modifications, or
187+ for any such Derivative Works as a whole, provided Your use,
188+ reproduction, and distribution of the Work otherwise complies with
189+ the conditions stated in this License.
190+
191+ 5. Submission of Contributions. Unless You explicitly state otherwise,
192+ any Contribution intentionally submitted for inclusion in the Work
193+ by You to the Licensor shall be under the terms and conditions of
194+ this License, without any additional terms or conditions.
195+ Notwithstanding the above, nothing herein shall supersede or modify
196+ the terms of any separate license agreement you may have executed
197+ with Licensor regarding such Contributions.
198+
199+ 6. Trademarks. This License does not grant permission to use the trade
200+ names, trademarks, service marks, or product names of the Licensor,
201+ except as required for reasonable and customary use in describing the
202+ origin of the Work and reproducing the content of the NOTICE file.
203+
204+ 7. Disclaimer of Warranty. Unless required by applicable law or
205+ agreed to in writing, Licensor provides the Work (and each
206+ Contributor provides its Contributions) on an "AS IS" BASIS,
207+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
208+ implied, including, without limitation, any warranties or conditions
209+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
210+ PARTICULAR PURPOSE. You are solely responsible for determining the
211+ appropriateness of using or redistributing the Work and assume any
212+ risks associated with Your exercise of permissions under this License.
213+
214+ 8. Limitation of Liability. In no event and under no legal theory,
215+ whether in tort (including negligence), contract, or otherwise,
216+ unless required by applicable law (such as deliberate and grossly
217+ negligent acts) or agreed to in writing, shall any Contributor be
218+ liable to You for damages, including any direct, indirect, special,
219+ incidental, or consequential damages of any character arising as a
220+ result of this License or out of the use or inability to use the
221+ Work (including but not limited to damages for loss of goodwill,
222+ work stoppage, computer failure or malfunction, or any and all
223+ other commercial damages or losses), even if such Contributor
224+ has been advised of the possibility of such damages.
225+
226+ 9. Accepting Warranty or Additional Liability. While redistributing
227+ the Work or Derivative Works thereof, You may choose to offer,
228+ and charge a fee for, acceptance of support, warranty, indemnity,
229+ or other liability obligations and/or rights consistent with this
230+ License. However, in accepting such obligations, You may act only
231+ on Your own behalf and on Your sole responsibility, not on behalf
232+ of any other Contributor, and only if You agree to indemnify,
233+ defend, and hold each Contributor harmless for any liability
234+ incurred by, or claims asserted against, such Contributor by reason
235+ of your accepting any such warranty or additional liability.
236+
237+ END OF TERMS AND CONDITIONS
238+
239+ APPENDIX: How to apply the Apache License to your work.
240+
241+ To apply the Apache License to your work, attach the following
242+ boilerplate notice, with the fields enclosed by brackets "[]"
243+ replaced with your own identifying information. (Don't include
244+ the brackets!) The text should be enclosed in the appropriate
245+ comment syntax for the file format. We also recommend that a
246+ file or class name and description of purpose be included on the
247+ same "printed page" as the copyright notice for easier
248+ identification within third-party archives.
249+
250+ Copyright 2022 Ubuntu
251+
252+ Licensed under the Apache License, Version 2.0 (the "License");
253+ you may not use this file except in compliance with the License.
254+ You may obtain a copy of the License at
255+
256+ http://www.apache.org/licenses/LICENSE-2.0
257+
258+ Unless required by applicable law or agreed to in writing, software
259+ distributed under the License is distributed on an "AS IS" BASIS,
260+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
261+ See the License for the specific language governing permissions and
262+ limitations under the License.
263diff --git a/README.md b/README.md
264new file mode 100644
265index 0000000..6cca19d
266--- /dev/null
267+++ b/README.md
268@@ -0,0 +1,7 @@
269+# charm-canonical-is-reviewbot
270+
271+Charmhub package name: mm-reviewbot-charmers-canonical-is-reviewbot
272+More information: https://charmhub.io/mm-reviewbot-charmers-canonical-is-reviewbot
273+
274+This charm deploys the Canonical IS reviewbot, which helps doing reviews of
275+Launchpad merge proposals for the IS team.
276diff --git a/charmcraft.yaml b/charmcraft.yaml
277new file mode 100644
278index 0000000..900d34a
279--- /dev/null
280+++ b/charmcraft.yaml
281@@ -0,0 +1,11 @@
282+# This file configures Charmcraft.
283+# See https://juju.is/docs/sdk/charmcraft-config for guidance.
284+
285+type: charm
286+bases:
287+ - build-on:
288+ - name: ubuntu
289+ channel: "22.04"
290+ run-on:
291+ - name: ubuntu
292+ channel: "22.04"
293diff --git a/config.yaml b/config.yaml
294new file mode 100644
295index 0000000..79e1a0d
296--- /dev/null
297+++ b/config.yaml
298@@ -0,0 +1,30 @@
299+# This file defines charm config options, and populates the Configure tab on Charmhub.
300+# If your charm does not require configuration options, delete this file entirely.
301+#
302+# See https://juju.is/docs/config for guidance.
303+
304+options:
305+ launchpad_secret:
306+ description: "Launchpad credential secret"
307+ default: ""
308+ type: string
309+ launchpad_token:
310+ description: "Launchpad credential token"
311+ default: ""
312+ type: string
313+ mattermost_token:
314+ description: "Mattermost API token"
315+ default: ""
316+ type: string
317+ mattermost_channel:
318+ description: "Mattermost channel in which to expect review requests"
319+ default: ""
320+ type: string
321+ mattermost_url:
322+ description: "Mattermost URL"
323+ default: ""
324+ type: string
325+ mattermost_team:
326+ description: "Mattermost team"
327+ default: ""
328+ type: string
329diff --git a/metadata.yaml b/metadata.yaml
330new file mode 100644
331index 0000000..83d6842
332--- /dev/null
333+++ b/metadata.yaml
334@@ -0,0 +1,18 @@
335+name: canonical-is-reviewbot
336+display-name: Mattermost Review Bot
337+summary: MatterMost Review Bot charm
338+maintainers:
339+ - https://launchpad.net/~mm-reviewbot-charmers
340+description: |
341+ A charm which deploys Mattermost Review Bot on kubernetes.
342+ Mattermost is a flexible, open source messaging platform that enables
343+ secure team collaboration.
344+ This bot allows reviewing Launchpad merge proposals on Mattermost
345+containers:
346+ reviewbot:
347+ resource: reviewbot-image
348+resources:
349+ reviewbot-image:
350+ type: oci-image
351+ description: Reviewbot docker image
352+ auto-fetch: true
353diff --git a/pyproject.toml b/pyproject.toml
354new file mode 100644
355index 0000000..3f51442
356--- /dev/null
357+++ b/pyproject.toml
358@@ -0,0 +1,39 @@
359+# Testing tools configuration
360+[tool.coverage.run]
361+branch = true
362+
363+[tool.coverage.report]
364+show_missing = true
365+
366+[tool.pytest.ini_options]
367+minversion = "6.0"
368+log_cli_level = "INFO"
369+
370+# Formatting tools configuration
371+[tool.black]
372+line-length = 99
373+target-version = ["py38"]
374+
375+# Linting tools configuration
376+[tool.ruff]
377+line-length = 99
378+select = ["E", "W", "F", "C", "N", "D", "I001"]
379+extend-ignore = [
380+ "D203",
381+ "D204",
382+ "D213",
383+ "D215",
384+ "D400",
385+ "D404",
386+ "D406",
387+ "D407",
388+ "D408",
389+ "D409",
390+ "D413",
391+]
392+ignore = ["E501", "D107"]
393+extend-exclude = ["__pycache__", "*.egg_info"]
394+per-file-ignores = {"tests/*" = ["D100","D101","D102","D103","D104"]}
395+
396+[tool.ruff.mccabe]
397+max-complexity = 10
398diff --git a/requirements.txt b/requirements.txt
399new file mode 100644
400index 0000000..56f5f64
401--- /dev/null
402+++ b/requirements.txt
403@@ -0,0 +1 @@
404+ops >= 1.5.0
405diff --git a/src/charm.py b/src/charm.py
406new file mode 100755
407index 0000000..e2bf66d
408--- /dev/null
409+++ b/src/charm.py
410@@ -0,0 +1,121 @@
411+#!/usr/bin/env python3
412+# Copyright 2022 Ubuntu
413+# See LICENSE file for licensing details.
414+#
415+# Learn more at: https://juju.is/docs/sdk
416+
417+"""Charm the service.
418+
419+Refer to the following post for a quick-start guide that will help you
420+develop a new k8s charm using the Operator Framework:
421+
422+https://discourse.charmhub.io/t/4208
423+"""
424+
425+import logging
426+
427+from ops.charm import CharmBase
428+from ops.main import main
429+from ops.model import ActiveStatus, BlockedStatus, WaitingStatus
430+
431+# Log messages can be retrieved using juju debug-log
432+logger = logging.getLogger(__name__)
433+
434+VALID_LOG_LEVELS = ["info", "debug", "warning", "error", "critical"]
435+
436+
437+class CharmReviewbotCharm(CharmBase):
438+ """Charm the service."""
439+
440+ def __init__(self, *args):
441+ super().__init__(*args)
442+ self.framework.observe(self.on.reviewbot_pebble_ready, self._on_reviewbot_pebble_ready)
443+ self.framework.observe(self.on.config_changed, self._on_config_changed)
444+
445+ def _missing_config(self):
446+ # All config options must be set
447+ missing_config = []
448+ for k, v in self.model.config.items():
449+ if not v:
450+ missing_config.append(k)
451+ return missing_config
452+
453+ def _on_reviewbot_pebble_ready(self, event):
454+ """Define and start a workload using the Pebble API.
455+
456+ Change this example to suit your needs. You'll need to specify the right entrypoint and
457+ environment configuration for your specific workload.
458+
459+ Learn more about interacting with Pebble at at https://juju.is/docs/sdk/pebble.
460+ """
461+ missing_config = self._missing_config()
462+ if missing_config:
463+ self.unit.status = BlockedStatus(f"required config not set: '{missing_config}'")
464+ return
465+ # Get a reference the container attribute on the PebbleReadyEvent
466+ container = event.workload
467+ # Add initial Pebble config layer using the Pebble API
468+ container.add_layer("reviewbot", self._pebble_layer, combine=True)
469+ # Make Pebble reevaluate its plan, ensuring any services are started if enabled.
470+ container.replan()
471+ # Learn more about statuses in the SDK docs:
472+ # https://juju.is/docs/sdk/constructs#heading--statuses
473+ self.unit.status = ActiveStatus()
474+
475+ def _get_reviewbot_env_config(self) -> dict:
476+ """Return an envConfig with configuration."""
477+ env_config = {
478+ "REVIEWBOT_LP_SECRET": self.config["launchpad_secret"],
479+ "REVIEWBOT_LP_TOKEN": self.config["launchpad_token"],
480+ "REVIEWBOT_MM_CHANNEL": self.config["mattermost_channel"],
481+ "REVIEWBOT_MM_TEAM": self.config["mattermost_team"],
482+ "REVIEWBOT_MM_TOKEN": self.config["mattermost_token"],
483+ "REVIEWBOT_MM_URL": self.config["mattermost_url"],
484+ "HOME": "/home/reviewbot", # work around https://github.com/canonical/pebble/issues/183
485+ }
486+
487+ return env_config
488+
489+ def _on_config_changed(self, event):
490+ """Handle changed configuration."""
491+ missing_config = self._missing_config()
492+ if missing_config:
493+ self.unit.status = BlockedStatus(f"required config not set: '{missing_config}'")
494+ return
495+
496+ # The config is good, so update the configuration of the workload
497+ container = self.unit.get_container("reviewbot")
498+ # Verify that we can connect to the Pebble API in the workload container
499+ if container.can_connect():
500+ # Push an updated layer with the new config
501+ container.add_layer("reviewbot", self._pebble_layer, combine=True)
502+ container.replan()
503+
504+ self.unit.status = ActiveStatus()
505+ else:
506+ # We were unable to connect to the Pebble API, so we defer this event
507+ event.defer()
508+ self.unit.status = WaitingStatus("waiting for Pebble API")
509+
510+ @property
511+ def _pebble_layer(self):
512+ """Return a dictionary representing a Pebble layer."""
513+ return {
514+ "summary": "reviewbot layer",
515+ "description": "pebble config layer for reviewbot",
516+ "services": {
517+ "reviewbot": {
518+ "override": "replace",
519+ "summary": "reviewbot",
520+ "user": "reviewbot",
521+ "group": "reviewbot",
522+ "command": "/app/bot.py",
523+ "startup": "enabled",
524+ "environment": self._get_reviewbot_env_config(),
525+ },
526+ },
527+ }
528+
529+
530+if __name__ == "__main__": # pragma: nocover
531+ main(CharmReviewbotCharm)
532diff --git a/tests/unit/test_charm.py b/tests/unit/test_charm.py
533new file mode 100644
534index 0000000..3399c04
535--- /dev/null
536+++ b/tests/unit/test_charm.py
537@@ -0,0 +1,108 @@
538+# Copyright 2022 Ubuntu
539+# See LICENSE file for licensing details.
540+#
541+# Learn more about testing at: https://juju.is/docs/sdk/testing
542+
543+import unittest
544+
545+import ops.testing
546+from charm import CharmReviewbotCharm
547+from ops.model import ActiveStatus, BlockedStatus, WaitingStatus
548+from ops.testing import Harness
549+
550+
551+class TestCharm(unittest.TestCase):
552+ base_config = {
553+ "launchpad_secret": "lps",
554+ "launchpad_token": "lpt",
555+ "mattermost_token": "mmtok",
556+ "mattermost_channel": "mmc",
557+ "mattermost_url": "mmu",
558+ "mattermost_team": "mmt",
559+ }
560+
561+ def setUp(self):
562+ # Enable more accurate simulation of container networking.
563+ # For more information, see https://juju.is/docs/sdk/testing#heading--simulate-can-connect
564+ ops.testing.SIMULATE_CAN_CONNECT = True
565+ self.addCleanup(setattr, ops.testing, "SIMULATE_CAN_CONNECT", False)
566+
567+ self.harness = Harness(CharmReviewbotCharm)
568+ self.addCleanup(self.harness.cleanup)
569+ self.harness.begin()
570+
571+ def test_reviewbot_pebble_ready(self):
572+ # Expected plan after Pebble ready with default config
573+ expected_plan = {
574+ "services": {
575+ "reviewbot": {
576+ "override": "replace",
577+ "summary": "reviewbot",
578+ "command": "/app/bot.py",
579+ "startup": "enabled",
580+ "environment": {
581+ "REVIEWBOT_LP_SECRET": "lps",
582+ "REVIEWBOT_LP_TOKEN": "lpt",
583+ "REVIEWBOT_MM_CHANNEL": "mmc",
584+ "REVIEWBOT_MM_TEAM": "mmt",
585+ "REVIEWBOT_MM_TOKEN": "mmtok",
586+ "REVIEWBOT_MM_URL": "mmu",
587+ "HOME": "/home/reviewbot",
588+ },
589+ "user": "reviewbot",
590+ "group": "reviewbot",
591+ }
592+ },
593+ }
594+ # Simulate the container coming up and emission of pebble-ready event
595+ self.harness.update_config(self.base_config)
596+ self.harness.container_pebble_ready("reviewbot")
597+ # Get the plan now we've run PebbleReady
598+ updated_plan = self.harness.get_container_pebble_plan("reviewbot").to_dict()
599+ # Check we've got the plan we expected
600+ self.assertEqual(expected_plan, updated_plan)
601+ # Check the service was started
602+ service = self.harness.model.unit.get_container("reviewbot").get_service("reviewbot")
603+ self.assertTrue(service.is_running())
604+ # Ensure we set an ActiveStatus with no message
605+ self.assertEqual(self.harness.model.unit.status, ActiveStatus())
606+
607+ def test_reviewbot_pebble_ready_no_config(self):
608+ # Expect an empty plan when a config item is missing
609+ expected_plan = {}
610+ # Simulate the container coming up and emission of pebble-ready event
611+ self.harness.container_pebble_ready("reviewbot")
612+ # Get the plan now we've run PebbleReady
613+ updated_plan = self.harness.get_container_pebble_plan("reviewbot").to_dict()
614+ # Check we've got the plan we expected
615+ self.assertEqual(expected_plan, updated_plan)
616+ # Ensure we got a BlockedStatus
617+ self.assertIsInstance(self.harness.model.unit.status, BlockedStatus)
618+
619+ def test_config_changed_valid_can_connect(self):
620+ # Ensure the simulated Pebble API is reachable
621+ self.harness.set_can_connect("reviewbot", True)
622+ # Trigger a config-changed event with an updated value
623+ changed_config = self.base_config.copy()
624+ changed_config["launchpad_secret"] = "CHANGED"
625+ self.harness.update_config(changed_config)
626+ # Get the plan now we've run PebbleReady
627+ updated_plan = self.harness.get_container_pebble_plan("reviewbot").to_dict()
628+ updated_env = updated_plan["services"]["reviewbot"]["environment"]
629+ # Check the config change was effective
630+ self.assertEqual(updated_env["REVIEWBOT_LP_SECRET"], "CHANGED")
631+ self.assertEqual(self.harness.model.unit.status, ActiveStatus())
632+
633+ def test_config_changed_valid_cannot_connect(self):
634+ # Trigger a config-changed event with an updated value
635+ self.harness.update_config(self.base_config)
636+ # Check the charm is in WaitingStatus
637+ self.assertIsInstance(self.harness.model.unit.status, WaitingStatus)
638+
639+ def test_config_changed_invalid(self):
640+ # Ensure the simulated Pebble API is reachable
641+ self.harness.set_can_connect("reviewbot", True)
642+ # Trigger a config-changed event with an updated value
643+ self.harness.update_config({})
644+ # Check the charm is in BlockedStatus
645+ self.assertIsInstance(self.harness.model.unit.status, BlockedStatus)
646diff --git a/tox.ini b/tox.ini
647new file mode 100644
648index 0000000..d4284f8
649--- /dev/null
650+++ b/tox.ini
651@@ -0,0 +1,84 @@
652+# Copyright 2022 Ubuntu
653+# See LICENSE file for licensing details.
654+
655+[tox]
656+skipsdist=True
657+skip_missing_interpreters = True
658+envlist = fmt, lint, unit
659+
660+[vars]
661+src_path = {toxinidir}/src/
662+tst_path = {toxinidir}/tests/
663+;lib_path = {toxinidir}/lib/charms/operator_name_with_underscores
664+all_path = {[vars]src_path} {[vars]tst_path}
665+
666+[testenv]
667+setenv =
668+ PYTHONPATH = {toxinidir}:{toxinidir}/lib:{[vars]src_path}
669+ PYTHONBREAKPOINT=pdb.set_trace
670+ PY_COLORS=1
671+passenv =
672+ PYTHONPATH
673+ CHARM_BUILD_DIR
674+ MODEL_SETTINGS
675+
676+[testenv:fmt]
677+description = Apply coding style standards to code
678+deps =
679+ black
680+ ruff
681+commands =
682+ black {[vars]all_path}
683+ ruff --fix {[vars]all_path}
684+
685+[testenv:lint]
686+description = Check code against coding style standards
687+deps =
688+ black
689+ ruff
690+ codespell
691+commands =
692+ # uncomment the following line if this charm owns a lib
693+ # codespell {[vars]lib_path}
694+ codespell {toxinidir} \
695+ --skip {toxinidir}/.git \
696+ --skip {toxinidir}/.tox \
697+ --skip {toxinidir}/build \
698+ --skip {toxinidir}/lib \
699+ --skip {toxinidir}/venv \
700+ --skip {toxinidir}/.mypy_cache \
701+ --skip {toxinidir}/icon.svg
702+
703+ ruff {[vars]all_path}
704+ black --check --diff {[vars]all_path}
705+
706+[testenv:unit]
707+description = Run unit tests
708+deps =
709+ pytest
710+ coverage[toml]
711+ -r{toxinidir}/requirements.txt
712+commands =
713+ coverage run --source={[vars]src_path} \
714+ -m pytest \
715+ --ignore={[vars]tst_path}integration \
716+ --tb native \
717+ -v \
718+ -s \
719+ {posargs}
720+ coverage report
721+
722+[testenv:integration]
723+description = Run integration tests
724+deps =
725+ pytest
726+ juju
727+ pytest-operator
728+ -r{toxinidir}/requirements.txt
729+commands =
730+ pytest -v \
731+ -s \
732+ --tb native \
733+ --ignore={[vars]tst_path}unit \
734+ --log-cli-level=INFO \
735+ {posargs}

Subscribers

People subscribed via source and target branches