diff --git a/.dockerignore b/.dockerignore index 5269dc5a..c68a1138 100644 --- a/.dockerignore +++ b/.dockerignore @@ -3,3 +3,4 @@ # Include the files directory !files/* +!plugins/* diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index ee5fa3ee..d98d9a1a 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -7,3 +7,13 @@ jobs: unit-tests: uses: canonical/operator-workflows/.github/workflows/test.yaml@main secrets: inherit + plugins-test: + name: Specific test for the plugin + runs-on: ubuntu-22.04 + steps: + - uses: actions/checkout@v3 + - name: Install tox + run: | + python -m pip install --upgrade pip + pip install tox + - run: tox -e plugins diff --git a/.gitignore b/.gitignore index c87a4f91..473a910a 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,4 @@ __pycache__/ .idea .vscode .mypy_cache +*.egg-info/ diff --git a/actions.yaml b/actions.yaml index 8b3652a2..9f927a42 100644 --- a/actions.yaml +++ b/actions.yaml @@ -1,2 +1,12 @@ refresh-external-resources: description: Pull changes from the customization repository, reload uWSGI and upgrade the external plugins +add-admin: + description: Add an admin to Indico + params: + email: + type: string + description: User email. + password: + type: string + description: User password. + required: [email, password] diff --git a/indico.Dockerfile b/indico.Dockerfile index dde1898c..e13c8708 100644 --- a/indico.Dockerfile +++ b/indico.Dockerfile @@ -32,12 +32,16 @@ RUN apt-get update \ && addgroup --gid ${indico_gid} indico \ && adduser --system --gid ${indico_gid} --uid ${indico_uid} --home /srv/indico indico +# Add our plugins +COPY --chown=indico:indico plugins /srv/indico/plugins + RUN python3 -m pip install --no-cache-dir --no-warn-script-location --prefer-binary \ indico==3.2.0 \ indico-plugin-piwik \ indico-plugin-storage-s3 \ - python-ldap \ python3-saml \ + python-ldap \ + /srv/indico/plugins/autocreate \ uwsgi \ && /bin/bash -c "mkdir -p --mode=775 /srv/indico/{archive,cache,custom,etc,log,tmp}" \ && /bin/bash -c "chown indico:indico /srv/indico /srv/indico/{archive,cache,custom,etc,log,tmp}" \ diff --git a/plugins/autocreate/README.md b/plugins/autocreate/README.md new file mode 100644 index 00000000..1e347148 --- /dev/null +++ b/plugins/autocreate/README.md @@ -0,0 +1,3 @@ +# Autocreate Plugin + +Extends the indico CLI to add a non-interactive way to create users diff --git a/plugins/autocreate/autocreate/__init__.py b/plugins/autocreate/autocreate/__init__.py new file mode 100644 index 00000000..62918389 --- /dev/null +++ b/plugins/autocreate/autocreate/__init__.py @@ -0,0 +1,5 @@ +#!/usr/bin/env python3 +# Copyright 2023 Canonical Ltd. +# See LICENSE file for licensing details. + +"""Create users non-interactively.""" diff --git a/plugins/autocreate/autocreate/cli.py b/plugins/autocreate/autocreate/cli.py new file mode 100644 index 00000000..23f73130 --- /dev/null +++ b/plugins/autocreate/autocreate/cli.py @@ -0,0 +1,80 @@ +#!/usr/bin/env python3 +# Copyright 2023 Canonical Ltd. +# See LICENSE file for licensing details. + +"""Create users non-interactively.""" + +import click +from indico.cli.core import cli_group +from indico.core.db import db +from indico.modules.auth import Identity +from indico.modules.users import User +from indico.modules.users.operations import create_user +from indico.modules.users.util import search_users + + +@cli_group(name="autocreate") +def cli(): + """Create users non-interactively.""" + + +@cli.command("admin") +@click.argument("email", type=str) +@click.argument("password", type=str) +@click.pass_context +def create_admin(ctx, email, password): + """Create a new admin user non-interactively.""" + + email = email.lower() + username = email + first_name = "unknown" + last_name = "unknown" + affiliation = "unknown" + + if User.query.filter(User.all_emails == email, ~User.is_deleted, ~User.is_pending).has_rows(): + click.secho("This user already exists", fg="red") + ctx.exit(1) + + if password == "": + click.secho("Password should not be empty", fg="red") + ctx.exit(1) + + if Identity.query.filter_by(provider="indico", identifier=username).has_rows(): + click.secho("Username already exists", fg="red") + ctx.exit(1) + + identity = Identity(provider="indico", identifier=username, password=password) + user = create_user( + email, + {"first_name": first_name, "last_name": last_name, "affiliation": affiliation}, + identity, + ) + user.is_admin = True + + # db.session has add() + db.session.add(user) # pylint: disable=no-member + # db.session has commit() + db.session.commit() # pylint: disable=no-member + + # search the created user + res = search_users( + exact=True, + include_deleted=False, + include_pending=False, + include_blocked=False, + external=False, + allow_system_user=False, + email=email, + ) + + if not res: + click.secho("Admin was not correctly created", fg="red") + ctx.exit(1) + + user = res.pop() + + if not user.is_admin: + click.secho("Created user is not admin", fg="red") + ctx.exit(1) + + click.secho(f'Admin with email "{user.email}" correctly created', fg="green") diff --git a/plugins/autocreate/autocreate/plugin.py b/plugins/autocreate/autocreate/plugin.py new file mode 100644 index 00000000..c417fe36 --- /dev/null +++ b/plugins/autocreate/autocreate/plugin.py @@ -0,0 +1,23 @@ +#!/usr/bin/env python3 +# Copyright 2023 Canonical Ltd. +# See LICENSE file for licensing details. + +"""Connect our CLI plugin to the existing Indico's CLI.""" + +from autocreate.cli import cli +from indico.core import signals +from indico.core.plugins import IndicoPlugin + + +class AutocreatePlugin(IndicoPlugin): + """Autocreate + + Provides a way to non-interactively create users via Indico's CLI + """ + + def init(self): + super().init() + self.connect(signals.plugin.cli, self._extend_indico_cli) + + def _extend_indico_cli(self, *_, **__): + return cli diff --git a/plugins/autocreate/setup.cfg b/plugins/autocreate/setup.cfg new file mode 100644 index 00000000..3434aa43 --- /dev/null +++ b/plugins/autocreate/setup.cfg @@ -0,0 +1,28 @@ +[metadata] +name = indico-plugin-autocreate +version = 3.2 +description = Extends the indico CLI to add a non-interactive way to create users +long_description = file: README.md +long_description_content_type = text/markdown; charset=UTF-8; variant=GFM +url = https://github.com/canonical/indico-operator +license = Apache License 2.0 +author = launchpad.net/~canonical-is-devops +author_email = is-devops-team@canonical.com +classifiers = + Environment :: Plugins + Environment :: Web Environment + License :: OSI Approved :: Apache Software License + Programming Language :: Python :: 3.9 + Programming Language :: Python :: 3.10 + +[options] +packages = find: +zip_safe = false +include_package_data = true +python_requires = >=3.9.0, <3.11 +install_requires = + indico>=3.2 + +[options.entry_points] +indico.plugins = + autocreate = autocreate.plugin:AutocreatePlugin diff --git a/plugins/autocreate/setup.py b/plugins/autocreate/setup.py new file mode 100644 index 00000000..96215319 --- /dev/null +++ b/plugins/autocreate/setup.py @@ -0,0 +1,5 @@ +"""Extends the indico CLI to add a non-interactive way to create users.""" + +from setuptools import setup + +setup() diff --git a/src/charm.py b/src/charm.py index 9f5e4a41..2626e8c3 100755 --- a/src/charm.py +++ b/src/charm.py @@ -29,6 +29,8 @@ ) from ops.pebble import ExecError +logger = logging.getLogger(__name__) + CANONICAL_LDAP_HOST = "ldap.canonical.com" CELERY_PROMEXP_PORT = "9808" DATABASE_NAME = "indico" @@ -70,6 +72,7 @@ def __init__(self, *args): self.on.refresh_external_resources_action, self._refresh_external_resources_action ) # self.framework.observe(self.on.update_status, self._refresh_external_resources) + self.framework.observe(self.on.add_admin_action, self._add_admin_action) self._stored.set_default( db_conn_str=None, @@ -685,6 +688,10 @@ def _get_indico_env_config(self, container: Container) -> Dict: env_config = {**env_config, **self._get_http_proxy_configuration()} return env_config + def _get_indico_env_config_str(self, container: Container) -> Dict[str, str]: + indico_env_config = self._get_indico_env_config(container) + return {env_name: str(value) for env_name, value in indico_env_config.items()} + def _get_http_proxy_configuration(self) -> Dict[str, str]: """Generate http proxy config. @@ -889,6 +896,41 @@ def _has_secrets(self) -> bool: # check if the other end of a relation also supports secrets... return juju_version.has_secrets + def _add_admin_action(self, event: ActionEvent) -> None: + """Add a new user to Indico. + + Args: + event: Event triggered by the add_admin action + """ + container = self.unit.get_container("indico") + indico_env_config = self._get_indico_env_config_str(container) + + cmd = [ + "/srv/indico/.local/bin/indico", + "autocreate", + "admin", + event.params["email"], + event.params["password"], + ] + + if container.can_connect(): + process = container.exec( + cmd, + user="indico", + working_dir="/srv/indico", + environment=indico_env_config, + ) + try: + output = process.wait_output() + event.set_results({"user": f"{event.params['email']}", "output": output}) + except ExecError as ex: + logger.exception("Action add-admin failed: %s", ex.stdout) + + event.fail( + # Parameter validation errors are printed to stdout + f"Failed to create admin {event.params['email']}: {ex.stdout!r}" + ) + if __name__ == "__main__": # pragma: no cover main(IndicoOperatorCharm, use_juju_for_storage=True) diff --git a/tests/integration/test_charm.py b/tests/integration/test_charm.py index 9a3eebef..2a37585f 100644 --- a/tests/integration/test_charm.py +++ b/tests/integration/test_charm.py @@ -1,9 +1,10 @@ #!/usr/bin/env python3 -# Copyright 2022 Canonical Ltd. +# Copyright 2023 Canonical Ltd. # See LICENSE file for licensing details. """Indico charm integration tests.""" +import juju.action import pytest import requests from ops.model import ActiveStatus, Application @@ -94,3 +95,29 @@ async def test_health_checks(app: Application): assert stdout.count("0/3") == 1 else: assert stdout.count("0/3") == 2 + + +@pytest.mark.asyncio +@pytest.mark.abort_on_fail +async def test_add_admin(app: Application): + """ + arrange: given charm in its initial state + act: run the add-admin action + assert: check the output in the action result + """ + + # Application actually does have units + assert app.units[0] # type: ignore + + email = "sample@email.com" + # This is a test password + password = "somepassword" # nosec + + # Application actually does have units + action: juju.action.Action = await app.units[0].run_action( # type: ignore + "add-admin", email=email, password=password + ) + await action.wait() + assert action.status == "completed" + assert action.results["user"] == email + assert f'Admin with email "{email}" correctly created' in action.results["output"] diff --git a/tests/unit/test_actions.py b/tests/unit/test_actions.py new file mode 100644 index 00000000..22e51c8e --- /dev/null +++ b/tests/unit/test_actions.py @@ -0,0 +1,205 @@ +# Copyright 2023 Canonical Ltd. +# See LICENSE file for licensing details. + +"""Indico charm unit tests.""" + +# pylint:disable=protected-access + +import typing +from unittest.mock import DEFAULT, MagicMock, patch + +from ops.charm import ActionEvent +from ops.model import Container +from ops.pebble import ExecError + +from tests.unit._patched_charm import IndicoOperatorCharm +from tests.unit.test_base import TestBase + + +class TestActions(TestBase): + """Indico charm unit tests.""" + + @patch.object(Container, "exec") + def test_refresh_external_resources_when_customization_and_plugins_set(self, mock_exec): + """ + arrange: charm created and relations established + act: configure the external resources and trigger the refresh action + assert: the customization sources are pulled and the plugins upgraded + """ + mock_exec.return_value = MagicMock(wait_output=MagicMock(return_value=("", None))) + + self.harness.disable_hooks() + self.set_up_all_relations() + self.harness.set_leader(True) + + self.is_ready( + [ + "nginx-prometheus-exporter", + "indico", + "indico-celery", + "indico-nginx", + ] + ) + self.harness.update_config( + { + "customization_sources_url": "https://example.com/custom", + "external_plugins": "git+https://example.git/#subdirectory=themes_cern", + } + ) + + charm: IndicoOperatorCharm = typing.cast(IndicoOperatorCharm, self.harness.charm) + charm._refresh_external_resources(MagicMock()) + + mock_exec.assert_any_call( + ["git", "pull"], + working_dir="/srv/indico/custom", + user="indico", + environment={}, + ) + mock_exec.assert_any_call( + ["pip", "install", "--upgrade", "git+https://example.git/#subdirectory=themes_cern"], + environment={}, + ) + + @patch.object(Container, "exec") + def test_add_admin(self, mock_exec): + """ + arrange: an email and a password + act: when the _on_add_admin_action method is executed + assert: the indico command to add the user is executed with the appropriate parameters. + """ + + mock_exec.return_value = MagicMock(wait_output=MagicMock(return_value=("", None))) + + self.set_up_all_relations() + self.harness.set_leader(True) + + self.is_ready( + [ + "celery-prometheus-exporter", + "statsd-prometheus-exporter", + "nginx-prometheus-exporter", + "indico", + "indico-celery", + "indico-nginx", + ] + ) + self.harness.disable_hooks() + + container = self.harness.model.unit.get_container("indico") + + charm: IndicoOperatorCharm = typing.cast(IndicoOperatorCharm, self.harness.charm) + + email = "sample@email.com" + password = "somepassword" # nosec + event = MagicMock(spec=ActionEvent) + event.params = { + "email": email, + "password": password, + } + + def event_store_failure(arg): + event.fail_message = arg + + event.fail = event_store_failure + + indico_env_config = charm._get_indico_env_config_str(container) + expected_cmd = [ + "/srv/indico/.local/bin/indico", + "autocreate", + "admin", + email, + password, + ] + + charm._add_admin_action(event) + + mock_exec.assert_any_call( + expected_cmd, + user="indico", + working_dir="/srv/indico", + environment=indico_env_config, + ) + + @patch.object(Container, "exec") + def test_add_admin_fail(self, mock_exec): + """ + arrange: an email and a password + act: when the _on_add_admin_action method is executed + assert: the indico command to add the user is executed with the appropriate parameters. + """ + + mock_wo = MagicMock( + return_value=("", None), + ) + + stdout_mock = "CRASH" + + # I'm disabling unused-argument here because some could be passed to the mock + def mock_wo_side_effect(*args, **kwargs): # pylint: disable=unused-argument + if isinstance(mock_wo.cmd, list) and "autocreate" in mock_wo.cmd: + raise ExecError(command=mock_wo.cmd, exit_code=42, stdout=stdout_mock, stderr="") + return DEFAULT + + mock_wo.side_effect = mock_wo_side_effect + + # I'm disabling unused-argument here because some could be passed to the mock + def mock_exec_side_effect(*args, **kwargs): # pylint: disable=unused-argument + mock_wo.cmd = args[0] + return DEFAULT + + mock_exec.side_effect = mock_exec_side_effect + mock_exec.return_value = MagicMock( + wait_output=mock_wo, + ) + + self.set_up_all_relations() + self.harness.set_leader(True) + + self.is_ready( + [ + "celery-prometheus-exporter", + "statsd-prometheus-exporter", + "nginx-prometheus-exporter", + "indico", + "indico-celery", + "indico-nginx", + ] + ) + self.harness.disable_hooks() + + container = self.harness.model.unit.get_container("indico") + + charm: IndicoOperatorCharm = typing.cast(IndicoOperatorCharm, self.harness.charm) + + email = "sample@email.com" + password = "somepassword" # nosec + event = MagicMock(spec=ActionEvent) + event.params = { + "email": email, + "password": password, + } + + def event_store_failure(arg): + event.fail_message = arg + + event.fail = event_store_failure + + indico_env_config = charm._get_indico_env_config_str(container) + expected_cmd = [ + "/srv/indico/.local/bin/indico", + "autocreate", + "admin", + email, + password, + ] + + charm._add_admin_action(event) + assert event.fail_message == f"Failed to create admin {email}: '{stdout_mock}'" + + mock_exec.assert_any_call( + expected_cmd, + user="indico", + working_dir="/srv/indico", + environment=indico_env_config, + ) diff --git a/tests/unit/test_base.py b/tests/unit/test_base.py new file mode 100644 index 00000000..bfcaa37e --- /dev/null +++ b/tests/unit/test_base.py @@ -0,0 +1,57 @@ +# Copyright 2022 Canonical Ltd. +# See LICENSE file for licensing details. + +"""Indico charm unit tests.""" + +# pylint:disable=protected-access + +import unittest +from typing import List + +from ops.testing import Harness + +from tests.unit._patched_charm import IndicoOperatorCharm, pgsql_patch + + +class TestBase(unittest.TestCase): + """Indico charm unit tests.""" + + def setUp(self): + """Set up test environment.""" + pgsql_patch.start() + self.harness = Harness(IndicoOperatorCharm) + self.addCleanup(self.harness.cleanup) + self.harness.begin() + + def tearDown(self): + """Tear down test environment.""" + pgsql_patch.stop() + + def set_up_all_relations(self): + """Set up all relations for the charm.""" + self.harness.charm._stored.db_uri = "db-uri" + self.db_relation_id = self.harness.add_relation( # pylint: disable=W0201 + "db", "postgresql" + ) + self.harness.add_relation_unit(self.db_relation_id, "postgresql/0") + + self.harness.add_relation("indico-peers", self.harness.charm.app.name) + + broker_relation_id = self.harness.add_relation("redis", "redis-broker") + self.harness.add_relation_unit(broker_relation_id, "redis-broker/0") + + cache_relation_id = self.harness.add_relation("redis", "redis-cache") + self.harness.add_relation_unit(cache_relation_id, "redis-cache/0") + + cache_relation = self.harness.model.get_relation("redis", cache_relation_id) + cache_unit = self.harness.model.get_unit("redis-cache/0") + cache_relation.data = {cache_unit: {"hostname": "cache-host", "port": 1011}} + + broker_relation = self.harness.model.get_relation("redis", broker_relation_id) + broker_unit = self.harness.model.get_unit("redis-broker/0") + broker_relation.data = {broker_unit: {"hostname": "broker-host", "port": 1010}} + + def is_ready(self, apps: List[str]): + """Waiting for all applications to be ready.""" + for app_name in apps: + self.harness.container_pebble_ready(app_name) diff --git a/tests/unit/test_charm.py b/tests/unit/test_core.py similarity index 84% rename from tests/unit/test_charm.py rename to tests/unit/test_core.py index 8e8d22ef..4a0abf5e 100644 --- a/tests/unit/test_charm.py +++ b/tests/unit/test_core.py @@ -5,32 +5,18 @@ # pylint:disable=protected-access -import typing -import unittest from ast import literal_eval from unittest.mock import MagicMock, patch from ops.jujuversion import JujuVersion from ops.model import ActiveStatus, BlockedStatus, Container, WaitingStatus -from ops.testing import Harness -from tests.unit._patched_charm import IndicoOperatorCharm, pgsql_patch +from tests.unit.test_base import TestBase -class TestCharm(unittest.TestCase): +class TestCore(TestBase): """Indico charm unit tests.""" - def setUp(self): - """Set up test environment.""" - pgsql_patch.start() - self.harness = Harness(IndicoOperatorCharm) - self.addCleanup(self.harness.cleanup) - self.harness.begin() - - def tearDown(self): - """Tear down test environment.""" - pgsql_patch.stop() - def test_missing_relations(self): """ arrange: charm created @@ -258,12 +244,17 @@ def test_config_changed(self, mock_exec): # pylint: disable=R0915 self.set_up_all_relations() self.harness.set_leader(True) - self.harness.container_pebble_ready("celery-prometheus-exporter") - self.harness.container_pebble_ready("statsd-prometheus-exporter") - self.harness.container_pebble_ready("nginx-prometheus-exporter") - self.harness.container_pebble_ready("indico") - self.harness.container_pebble_ready("indico-celery") - self.harness.container_pebble_ready("indico-nginx") + # pylint: disable=duplicate-code + self.is_ready( + [ + "celery-prometheus-exporter", + "statsd-prometheus-exporter", + "nginx-prometheus-exporter", + "indico", + "indico-celery", + "indico-nginx", + ] + ) self.harness.update_config( { "customization_debug": True, @@ -391,12 +382,17 @@ def test_config_changed_when_config_invalid(self, mock_exec): self.set_up_all_relations() self.harness.set_leader(True) - self.harness.container_pebble_ready("celery-prometheus-exporter") - self.harness.container_pebble_ready("statsd-prometheus-exporter") - self.harness.container_pebble_ready("nginx-prometheus-exporter") - self.harness.container_pebble_ready("indico") - self.harness.container_pebble_ready("indico-celery") - self.harness.container_pebble_ready("indico-nginx") + # pylint: disable=duplicate-code + self.is_ready( + [ + "celery-prometheus-exporter", + "statsd-prometheus-exporter", + "nginx-prometheus-exporter", + "indico", + "indico-celery", + "indico-nginx", + ] + ) self.harness.update_config({"site_url": "example.local"}) self.assertEqual( self.harness.model.unit.status, @@ -415,12 +411,17 @@ def test_config_changed_with_external_resources(self, mock_exec): self.set_up_all_relations() self.harness.set_leader(True) - self.harness.container_pebble_ready("celery-prometheus-exporter") - self.harness.container_pebble_ready("statsd-prometheus-exporter") - self.harness.container_pebble_ready("nginx-prometheus-exporter") - self.harness.container_pebble_ready("indico") - self.harness.container_pebble_ready("indico-celery") - self.harness.container_pebble_ready("indico-nginx") + # pylint: disable=duplicate-code + self.is_ready( + [ + "celery-prometheus-exporter", + "statsd-prometheus-exporter", + "nginx-prometheus-exporter", + "indico", + "indico-celery", + "indico-nginx", + ] + ) self.harness.update_config( { "customization_sources_url": "https://example.com/custom", @@ -461,9 +462,13 @@ def test_config_changed_when_saml_target_url_invalid(self, mock_exec): self.set_up_all_relations() self.harness.set_leader(True) - self.harness.container_pebble_ready("indico") - self.harness.container_pebble_ready("indico-celery") - self.harness.container_pebble_ready("indico-nginx") + self.is_ready( + [ + "indico", + "indico-celery", + "indico-nginx", + ] + ) self.harness.update_config({"saml_target_url": "sample.com/saml"}) self.assertEqual( @@ -483,9 +488,13 @@ def test_config_changed_when_ldap_host_invalid(self, mock_exec): self.set_up_all_relations() self.harness.set_leader(True) - self.harness.container_pebble_ready("indico") - self.harness.container_pebble_ready("indico-celery") - self.harness.container_pebble_ready("indico-nginx") + self.is_ready( + [ + "indico", + "indico-celery", + "indico-nginx", + ] + ) self.harness.update_config({"ldap_host": "ldap.example.com"}) self.assertEqual( @@ -500,9 +509,13 @@ def test_pebble_ready_when_relations_not_ready(self): act: trigger the pebble ready events assert: the unit reaches waiting status """ - self.harness.container_pebble_ready("indico") - self.harness.container_pebble_ready("indico-celery") - self.harness.container_pebble_ready("indico-nginx") + self.is_ready( + [ + "indico", + "indico-celery", + "indico-nginx", + ] + ) self.assertEqual( self.harness.model.unit.status, WaitingStatus("Waiting for redis-broker availability") @@ -592,65 +605,3 @@ def test_db_relations(self): "postgresql://new_master", "database connection string should change after database master changed", ) - - @patch.object(Container, "exec") - def test_refresh_external_resources_when_customization_and_plugins_set(self, mock_exec): - """ - arrange: charm created and relations established - act: configure the external resources and trigger the refresh action - assert: the customization sources are pulled and the plugins upgraded - """ - mock_exec.return_value = MagicMock(wait_output=MagicMock(return_value=("", None))) - - self.harness.disable_hooks() - self.set_up_all_relations() - self.harness.set_leader(True) - - self.harness.container_pebble_ready("nginx-prometheus-exporter") - self.harness.container_pebble_ready("indico") - self.harness.container_pebble_ready("indico-celery") - self.harness.container_pebble_ready("indico-nginx") - self.harness.update_config( - { - "customization_sources_url": "https://example.com/custom", - "external_plugins": "git+https://example.git/#subdirectory=themes_cern", - } - ) - - charm: IndicoOperatorCharm = typing.cast(IndicoOperatorCharm, self.harness.charm) - charm._refresh_external_resources(MagicMock()) - - mock_exec.assert_any_call( - ["git", "pull"], - working_dir="/srv/indico/custom", - user="indico", - environment={}, - ) - mock_exec.assert_any_call( - ["pip", "install", "--upgrade", "git+https://example.git/#subdirectory=themes_cern"], - environment={}, - ) - - def set_up_all_relations(self): - """Set up all relations for the charm.""" - self.harness.charm._stored.db_uri = "db-uri" - self.db_relation_id = self.harness.add_relation( # pylint: disable=W0201 - "db", "postgresql" - ) - self.harness.add_relation_unit(self.db_relation_id, "postgresql/0") - - self.harness.add_relation("indico-peers", self.harness.charm.app.name) - - broker_relation_id = self.harness.add_relation("redis", "redis-broker") - self.harness.add_relation_unit(broker_relation_id, "redis-broker/0") - - cache_relation_id = self.harness.add_relation("redis", "redis-cache") - self.harness.add_relation_unit(cache_relation_id, "redis-cache/0") - - cache_relation = self.harness.model.get_relation("redis", cache_relation_id) - cache_unit = self.harness.model.get_unit("redis-cache/0") - cache_relation.data = {cache_unit: {"hostname": "cache-host", "port": 1011}} - - broker_relation = self.harness.model.get_relation("redis", broker_relation_id) - broker_unit = self.harness.model.get_unit("redis-broker/0") - broker_relation.data = {broker_unit: {"hostname": "broker-host", "port": 1010}} diff --git a/tox.ini b/tox.ini index fe4fb7c6..4385806e 100644 --- a/tox.ini +++ b/tox.ini @@ -9,6 +9,7 @@ envlist = lint, unit, static, coverage-report [vars] src_path = {toxinidir}/src/ tst_path = {toxinidir}/tests/ +plugins_path = {toxinidir}/plugins/ ;lib_path = {toxinidir}/lib/charms/operator_name_with_underscores all_path = {[vars]src_path} {[vars]tst_path} @@ -69,6 +70,44 @@ commands = mypy {[vars]all_path} pylint {[vars]all_path} +[testenv:plugins] +description = Check plugins code against coding style standards +deps = + black + codespell + flake8<6.0.0 + flake8-builtins + flake8-copyright<6.0.0 + flake8-docstrings>=1.6.0 + flake8-docstrings-complete>=1.0.3 + flake8-test-docs>=1.0 + indico==3.2 + isort + mypy + pep8-naming + plugins/autocreate + pydocstyle>=2.10 + pylint + pyproject-flake8<6.0.0 + pytest + pytest-asyncio + pytest-operator + requests + types-PyYAML + types-requests + -r{toxinidir}/requirements.txt +commands = + codespell {[vars]plugins_path} --skip {toxinidir}/.git --skip {toxinidir}/.tox \ + --skip {toxinidir}/build --skip {toxinidir}/lib --skip {toxinidir}/venv \ + --skip {toxinidir}/.mypy_cache --skip {toxinidir}/icon.svg \ + --skip {toxinidir}/plugins/autocreate/.mypy_cache + # pflake8 wrapper supports config from pyproject.toml + pflake8 {[vars]plugins_path} --ignore=W503 + isort --check-only --diff {[vars]plugins_path} + black --check --diff {[vars]plugins_path} + mypy {[vars]plugins_path} + pylint {[vars]plugins_path} --ignore-paths {[vars]plugins_path}/autocreate/build + [testenv:unit] description = Run unit tests deps =