From 06440531ddc9010ba3f5d189c0553fdcca520e1a Mon Sep 17 00:00:00 2001 From: Stamatis Katsaounis Date: Fri, 29 Mar 2024 15:02:54 +0200 Subject: [PATCH] ci: add lxd provider for integration tests --- .github/workflows/pull-request.yaml | 2 + maas-agent/lib/charms/maas_region/v0/maas.py | 36 ++++++++-------- maas-agent/src/charm.py | 5 ++- maas-agent/src/helper.py | 17 ++++---- maas-agent/tests/integration/test_charm.py | 42 ++++++++++++++++++- maas-agent/tests/unit/test_charm.py | 3 +- maas-agent/tests/unit/test_helper.py | 1 - maas-agent/tox.ini | 22 ++++++---- maas-region/lib/charms/maas_region/v0/maas.py | 36 ++++++++-------- maas-region/src/charm.py | 20 +++++---- maas-region/src/helper.py | 25 +++++------ maas-region/tests/integration/test_charm.py | 30 ++++++++++++- maas-region/tests/unit/test_charm.py | 1 - maas-region/tox.ini | 22 ++++++---- 14 files changed, 174 insertions(+), 88 deletions(-) diff --git a/.github/workflows/pull-request.yaml b/.github/workflows/pull-request.yaml index f14d585..09f4e66 100644 --- a/.github/workflows/pull-request.yaml +++ b/.github/workflows/pull-request.yaml @@ -12,6 +12,7 @@ jobs: secrets: inherit with: charm-path: maas-region + provider: lxd pull-request-agent: name: PR Agent @@ -19,3 +20,4 @@ jobs: secrets: inherit with: charm-path: maas-agent + provider: lxd diff --git a/maas-agent/lib/charms/maas_region/v0/maas.py b/maas-agent/lib/charms/maas_region/v0/maas.py index b3d085c..a6a6cb0 100644 --- a/maas-agent/lib/charms/maas_region/v0/maas.py +++ b/maas-agent/lib/charms/maas_region/v0/maas.py @@ -6,7 +6,7 @@ import dataclasses import json import logging -from typing import Any, Dict, MutableMapping +from typing import Any, Dict, List, MutableMapping, Union import ops from ops.charm import CharmEvents @@ -21,7 +21,7 @@ # Increment this PATCH version before using `charmcraft publish-lib` or reset # to 0 if you are raising the major API version -LIBPATCH = 1 +LIBPATCH = 2 DEFAULT_ENDPOINT_NAME = "maas-region" BUILTIN_JUJU_KEYS = {"ingress-address", "private-address", "egress-subnets"} @@ -44,10 +44,10 @@ def load(cls, data: Dict[str, str]) -> Self: init_vals = {} for f in dataclasses.fields(cls): val = data.get(f.name) - init_vals[f.name] = val if f.type == str else json.loads(val) + init_vals[f.name] = val if f.type == str else json.loads(val) # type: ignore return cls(**init_vals) - def dump(self, databag: MutableMapping[str, str] | None = None) -> None: + def dump(self, databag: Union[MutableMapping[str, str], None] = None) -> None: """Write the contents of this model to Juju databag.""" if databag is None: databag = {} @@ -71,7 +71,7 @@ class MaasProviderAppData(MaasDatabag): """The schema for the Provider side of this relation.""" api_url: str - regions: list[str] + regions: List[str] maas_secret_id: str def get_secret(self, model: ops.Model) -> str: @@ -130,12 +130,12 @@ class MaasRegionRequirerEvents(CharmEvents): class MaasRegionRequirer(Object): """Requires-side of the MAAS relation.""" - on = MaasRegionRequirerEvents() + on = MaasRegionRequirerEvents() # type: ignore def __init__( self, charm: ops.CharmBase, - key: str | None = None, + key: Union[str, None] = None, endpoint: str = DEFAULT_ENDPOINT_NAME, ): super().__init__(charm, key or endpoint) @@ -156,7 +156,7 @@ def __init__( ) @property - def _relation(self) -> ops.Relation | None: + def _relation(self) -> Union[ops.Relation, None]: # filter out common unhappy relation states relation = self.model.get_relation(self._endpoint) return relation if relation and relation.app and relation.data else None @@ -176,16 +176,16 @@ def _on_relation_created(self, event: ops.RelationCreatedEvent) -> None: def _on_relation_broken(self, _event: ops.RelationBrokenEvent) -> None: self.on.removed.emit() - def get_enroll_data(self) -> MaasProviderAppData | None: + def get_enroll_data(self) -> Union[MaasProviderAppData, None]: """Get enrollment data from databag.""" relation = self._relation if relation: assert relation.app is not None try: databag = relation.data[relation.app] - return MaasProviderAppData.load(databag) + return MaasProviderAppData.load(databag) # type: ignore except TypeError: - log.debug(f"invalid databag contents: {databag}") + log.debug(f"invalid databag contents: {databag}") # type: ignore return None def is_published(self) -> bool: @@ -196,7 +196,7 @@ def is_published(self) -> bool: unit_data = relation.data[self._charm.unit] try: - MaasRequirerUnitData.load(unit_data) + MaasRequirerUnitData.load(unit_data) # type: ignore return True except TypeError: return False @@ -218,7 +218,7 @@ class MaasRegionProvider(Object): def __init__( self, charm: ops.CharmBase, - key: str | None = None, + key: Union[str, None] = None, endpoint: str = DEFAULT_ENDPOINT_NAME, ): super().__init__(charm, key or endpoint) @@ -226,10 +226,10 @@ def __init__( self._endpoint = endpoint @property - def _relations(self) -> list[ops.Relation]: + def _relations(self) -> List[ops.Relation]: return self.model.relations[self._endpoint] - def _update_secret(self, relation: ops.Relation, content: dict[str, str]) -> str: + def _update_secret(self, relation: ops.Relation, content: Dict[str, str]) -> str: label = f"enroll-{relation.name}-{relation.id}.secret" try: secret = self.model.get_secret(label=label) @@ -242,7 +242,7 @@ def _update_secret(self, relation: ops.Relation, content: dict[str, str]) -> str secret.grant(relation) return secret.get_info().id - def publish_enroll_token(self, maas_api: str, regions: list[str], maas_secret: str) -> None: + def publish_enroll_token(self, maas_api: str, regions: List[str], maas_secret: str) -> None: """Publish enrollment data. Args: @@ -260,7 +260,7 @@ def publish_enroll_token(self, maas_api: str, regions: list[str], maas_secret: s ) local_app_databag.dump(relation.data[self.model.app]) - def gather_rack_units(self) -> dict[str, ops.model.Unit]: + def gather_rack_units(self) -> Dict[str, ops.model.Unit]: """Get a map of Rack units. Returns: @@ -272,7 +272,7 @@ def gather_rack_units(self) -> dict[str, ops.model.Unit]: continue for worker_unit in relation.units: try: - worker_data = MaasRequirerUnitData.load(relation.data[worker_unit]) + worker_data = MaasRequirerUnitData.load(relation.data[worker_unit]) # type: ignore url = worker_data.url except TypeError as e: log.debug(f"invalid databag contents: {e}") diff --git a/maas-agent/src/charm.py b/maas-agent/src/charm.py index 995f838..a1a2fab 100755 --- a/maas-agent/src/charm.py +++ b/maas-agent/src/charm.py @@ -6,6 +6,7 @@ import logging import socket +from typing import Union import ops from charms.grafana_agent.v0 import cos_agent @@ -61,7 +62,7 @@ def __init__(self, *args): # self.framework.observe(self.maas_region.on.removed, self._on_maas_removed) @property - def version(self) -> str | None: + def version(self) -> Union[str, None]: """Reports the current workload version. Returns: @@ -70,7 +71,7 @@ def version(self) -> str | None: return MaasHelper.get_installed_version() @property - def maas_id(self) -> str | None: + def maas_id(self) -> Union[str, None]: """Reports the MAAS ID. Returns: diff --git a/maas-agent/src/helper.py b/maas-agent/src/helper.py index 8e4774e..a767a3e 100644 --- a/maas-agent/src/helper.py +++ b/maas-agent/src/helper.py @@ -5,6 +5,7 @@ import subprocess from pathlib import Path +from typing import Union from charms.operator_libs_linux.v2.snap import SnapCache, SnapState @@ -38,31 +39,31 @@ def uninstall() -> None: maas.ensure(SnapState.Absent) @staticmethod - def get_installed_version() -> str | None: + def get_installed_version() -> Union[str, None]: """Get installed version. Returns: - str | None: version if installed + Union[str, None]: version if installed """ maas = SnapCache()[MAAS_SNAP_NAME] return maas.revision if maas.present else None @staticmethod - def get_installed_channel() -> str | None: + def get_installed_channel() -> Union[str, None]: """Get installed channel. Returns: - str | None: channel if installed + Union[str, None]: channel if installed """ maas = SnapCache()[MAAS_SNAP_NAME] return maas.channel if maas.present else None @staticmethod - def get_maas_id() -> str | None: + def get_maas_id() -> Union[str, None]: """Get MAAS system ID. Returns: - str | None: system_id, or None if not present + Union[str, None]: system_id, or None if not present """ try: with MAAS_ID.open() as file: @@ -71,11 +72,11 @@ def get_maas_id() -> str | None: return None @staticmethod - def get_maas_mode() -> str | None: + def get_maas_mode() -> Union[str, None]: """Get MAAS operation mode. Returns: - str | None: mode, or None if not initialised + Union[str, None]: mode, or None if not initialised """ try: with MAAS_MODE.open() as file: diff --git a/maas-agent/tests/integration/test_charm.py b/maas-agent/tests/integration/test_charm.py index ed2b820..dcb0c82 100644 --- a/maas-agent/tests/integration/test_charm.py +++ b/maas-agent/tests/integration/test_charm.py @@ -12,7 +12,7 @@ logger = logging.getLogger(__name__) -METADATA = yaml.safe_load(Path("./metadata.yaml").read_text()) +METADATA = yaml.safe_load(Path("./charmcraft.yaml").read_text()) APP_NAME = METADATA["name"] @@ -25,9 +25,47 @@ async def test_build_and_deploy(ops_test: OpsTest): # Build and deploy charm from local source folder charm = await ops_test.build_charm(".") - # Deploy the charm and wait for active/idle status + # Deploy the charm and wait for waiting/idle status await asyncio.gather( ops_test.model.deploy(charm, application_name=APP_NAME), + ops_test.model.wait_for_idle( + apps=[APP_NAME], status="waiting", raise_on_blocked=True, timeout=1000 + ), + ) + + +@pytest.mark.abort_on_fail +async def test_region_integration(ops_test: OpsTest): + """Verify that the charm integrates with the database. + + Assert that the charm is active if the integration is established. + """ + # Build and deploy region charm from local source folder + region_charm = await ops_test.build_charm("../maas-region/") + + # Deploy the region charm and wait for active/idle status + await asyncio.gather( + ops_test.model.deploy(region_charm, application_name="maas-region"), + ops_test.model.wait_for_idle( + apps=["maas-region"], status="waiting", raise_on_blocked=True, timeout=1000 + ), + ) + + await asyncio.gather( + ops_test.model.deploy( + "postgresql", + application_name="postgresql", + channel="14/stable", + trust=True, + ), + ops_test.model.wait_for_idle( + apps=["postgresql"], status="active", raise_on_blocked=True, timeout=1000 + ), + ) + + await asyncio.gather( + ops_test.model.integrate("maas-region", "postgresql"), + ops_test.model.integrate(f"{APP_NAME}", "maas-region"), ops_test.model.wait_for_idle( apps=[APP_NAME], status="active", raise_on_blocked=True, timeout=1000 ), diff --git a/maas-agent/tests/unit/test_charm.py b/maas-agent/tests/unit/test_charm.py index 954cb18..05b195f 100644 --- a/maas-agent/tests/unit/test_charm.py +++ b/maas-agent/tests/unit/test_charm.py @@ -5,6 +5,7 @@ import socket import unittest +from typing import List from unittest.mock import patch import ops @@ -54,7 +55,7 @@ def setUp(self): self.harness.begin() self.addCleanup(self.harness.cleanup) - def _enroll(self, rel_id: int, regions: list[str]): + def _enroll(self, rel_id: int, regions: List[str]): secret_id = self.harness.add_model_secret( self.remote_app, {"maas-secret": self.maas_secret} ) diff --git a/maas-agent/tests/unit/test_helper.py b/maas-agent/tests/unit/test_helper.py index 9101dec..e8cc61d 100644 --- a/maas-agent/tests/unit/test_helper.py +++ b/maas-agent/tests/unit/test_helper.py @@ -78,7 +78,6 @@ def test_set_not_running(self, mock_snap): class TestHelperFiles(unittest.TestCase): - @patch("pathlib.Path.open", new_callable=lambda: mock_open(read_data="maas-id\n")) def test_get_maas_id(self, _): self.assertEqual(MaasHelper.get_maas_id(), "maas-id") diff --git a/maas-agent/tox.ini b/maas-agent/tox.ini index 618774f..1046432 100644 --- a/maas-agent/tox.ini +++ b/maas-agent/tox.ini @@ -4,7 +4,7 @@ [tox] no_package = True skip_missing_interpreters = True -env_list = format, lint, static, unit +env_list = format, lint, static-{charm,lib}, unit, scenario min_version = 4.0.0 [vars] @@ -30,7 +30,7 @@ deps = ruff commands = black {[vars]all_path} - ruff --fix {[vars]all_path} + ruff check --fix {[vars]all_path} [testenv:lint] description = Check code against coding style standards @@ -43,7 +43,7 @@ commands = # and uncomment the following line # codespell {[vars]lib_path} codespell {tox_root} - ruff {[vars]all_path} + ruff check {[vars]all_path} black --check --diff {[vars]all_path} [testenv:unit] @@ -62,13 +62,21 @@ commands = {[vars]tests_path}/unit coverage report -[testenv:static] -description = Run static type checks +[testenv:static-{charm,lib}] +description = Run static analysis checks deps = pyright - -r {tox_root}/requirements.txt + typing-extensions + charm: -r{toxinidir}/requirements.txt + lib: ops + lib: jinja2 + unit: {[testenv:unit]deps} + integration: {[testenv:integration]deps} commands = - pyright {posargs} + charm: pyright --pythonversion 3.8 {[vars]src_path} {posargs} + lib: pyright --pythonversion 3.8 {[vars]lib_path} {posargs} + lib: /usr/bin/env sh -c 'for m in $(git diff main --name-only --line-prefix=`git rev-parse --show-toplevel`/ {[vars]lib_path}); do if ! git diff main $m | grep -q "+LIBPATCH\|+LIBAPI"; then echo "You forgot to bump the version on $m!"; exit 1; fi; done' +allowlist_externals = /usr/bin/env [testenv:integration] description = Run integration tests diff --git a/maas-region/lib/charms/maas_region/v0/maas.py b/maas-region/lib/charms/maas_region/v0/maas.py index b3d085c..a6a6cb0 100644 --- a/maas-region/lib/charms/maas_region/v0/maas.py +++ b/maas-region/lib/charms/maas_region/v0/maas.py @@ -6,7 +6,7 @@ import dataclasses import json import logging -from typing import Any, Dict, MutableMapping +from typing import Any, Dict, List, MutableMapping, Union import ops from ops.charm import CharmEvents @@ -21,7 +21,7 @@ # Increment this PATCH version before using `charmcraft publish-lib` or reset # to 0 if you are raising the major API version -LIBPATCH = 1 +LIBPATCH = 2 DEFAULT_ENDPOINT_NAME = "maas-region" BUILTIN_JUJU_KEYS = {"ingress-address", "private-address", "egress-subnets"} @@ -44,10 +44,10 @@ def load(cls, data: Dict[str, str]) -> Self: init_vals = {} for f in dataclasses.fields(cls): val = data.get(f.name) - init_vals[f.name] = val if f.type == str else json.loads(val) + init_vals[f.name] = val if f.type == str else json.loads(val) # type: ignore return cls(**init_vals) - def dump(self, databag: MutableMapping[str, str] | None = None) -> None: + def dump(self, databag: Union[MutableMapping[str, str], None] = None) -> None: """Write the contents of this model to Juju databag.""" if databag is None: databag = {} @@ -71,7 +71,7 @@ class MaasProviderAppData(MaasDatabag): """The schema for the Provider side of this relation.""" api_url: str - regions: list[str] + regions: List[str] maas_secret_id: str def get_secret(self, model: ops.Model) -> str: @@ -130,12 +130,12 @@ class MaasRegionRequirerEvents(CharmEvents): class MaasRegionRequirer(Object): """Requires-side of the MAAS relation.""" - on = MaasRegionRequirerEvents() + on = MaasRegionRequirerEvents() # type: ignore def __init__( self, charm: ops.CharmBase, - key: str | None = None, + key: Union[str, None] = None, endpoint: str = DEFAULT_ENDPOINT_NAME, ): super().__init__(charm, key or endpoint) @@ -156,7 +156,7 @@ def __init__( ) @property - def _relation(self) -> ops.Relation | None: + def _relation(self) -> Union[ops.Relation, None]: # filter out common unhappy relation states relation = self.model.get_relation(self._endpoint) return relation if relation and relation.app and relation.data else None @@ -176,16 +176,16 @@ def _on_relation_created(self, event: ops.RelationCreatedEvent) -> None: def _on_relation_broken(self, _event: ops.RelationBrokenEvent) -> None: self.on.removed.emit() - def get_enroll_data(self) -> MaasProviderAppData | None: + def get_enroll_data(self) -> Union[MaasProviderAppData, None]: """Get enrollment data from databag.""" relation = self._relation if relation: assert relation.app is not None try: databag = relation.data[relation.app] - return MaasProviderAppData.load(databag) + return MaasProviderAppData.load(databag) # type: ignore except TypeError: - log.debug(f"invalid databag contents: {databag}") + log.debug(f"invalid databag contents: {databag}") # type: ignore return None def is_published(self) -> bool: @@ -196,7 +196,7 @@ def is_published(self) -> bool: unit_data = relation.data[self._charm.unit] try: - MaasRequirerUnitData.load(unit_data) + MaasRequirerUnitData.load(unit_data) # type: ignore return True except TypeError: return False @@ -218,7 +218,7 @@ class MaasRegionProvider(Object): def __init__( self, charm: ops.CharmBase, - key: str | None = None, + key: Union[str, None] = None, endpoint: str = DEFAULT_ENDPOINT_NAME, ): super().__init__(charm, key or endpoint) @@ -226,10 +226,10 @@ def __init__( self._endpoint = endpoint @property - def _relations(self) -> list[ops.Relation]: + def _relations(self) -> List[ops.Relation]: return self.model.relations[self._endpoint] - def _update_secret(self, relation: ops.Relation, content: dict[str, str]) -> str: + def _update_secret(self, relation: ops.Relation, content: Dict[str, str]) -> str: label = f"enroll-{relation.name}-{relation.id}.secret" try: secret = self.model.get_secret(label=label) @@ -242,7 +242,7 @@ def _update_secret(self, relation: ops.Relation, content: dict[str, str]) -> str secret.grant(relation) return secret.get_info().id - def publish_enroll_token(self, maas_api: str, regions: list[str], maas_secret: str) -> None: + def publish_enroll_token(self, maas_api: str, regions: List[str], maas_secret: str) -> None: """Publish enrollment data. Args: @@ -260,7 +260,7 @@ def publish_enroll_token(self, maas_api: str, regions: list[str], maas_secret: s ) local_app_databag.dump(relation.data[self.model.app]) - def gather_rack_units(self) -> dict[str, ops.model.Unit]: + def gather_rack_units(self) -> Dict[str, ops.model.Unit]: """Get a map of Rack units. Returns: @@ -272,7 +272,7 @@ def gather_rack_units(self) -> dict[str, ops.model.Unit]: continue for worker_unit in relation.units: try: - worker_data = MaasRequirerUnitData.load(relation.data[worker_unit]) + worker_data = MaasRequirerUnitData.load(relation.data[worker_unit]) # type: ignore url = worker_data.url except TypeError as e: log.debug(f"invalid databag contents: {e}") diff --git a/maas-region/src/charm.py b/maas-region/src/charm.py index 4775e60..ccd9644 100755 --- a/maas-region/src/charm.py +++ b/maas-region/src/charm.py @@ -8,7 +8,7 @@ import logging import socket import subprocess -from typing import Any +from typing import Any, List, Union import ops import yaml @@ -109,7 +109,7 @@ def __init__(self, *args): self.framework.observe(self.on.get_api_endpoint_action, self._on_get_api_endpoint_action) @property - def peers(self) -> ops.Relation | None: + def peers(self) -> Union[ops.Relation, None]: """Fetch the peer relation.""" return self.model.get_relation(MAAS_PEER_NAME) @@ -131,7 +131,7 @@ def connection_string(self) -> str: return f"postgres://{username}:{password}@{endpoints}/{self.maasdb_name}" @property - def version(self) -> str | None: + def version(self) -> Union[str, None]: """Reports the current workload version. Returns: @@ -140,7 +140,7 @@ def version(self) -> str | None: return MaasHelper.get_installed_version() @property - def enrollment_token(self) -> str | None: + def enrollment_token(self) -> Union[str, None]: """Reports the enrollment token. Returns: @@ -149,7 +149,7 @@ def enrollment_token(self) -> str | None: return MaasHelper.get_maas_secret() @property - def bind_address(self) -> str | None: + def bind_address(self) -> Union[str, None]: """Get Unit bind address. Returns: @@ -175,7 +175,7 @@ def maas_api_url(self) -> str: return "" @property - def maas_id(self) -> str | None: + def maas_id(self) -> Union[str, None]: """Reports the MAAS ID. Returns: @@ -192,13 +192,15 @@ def get_operational_mode(self) -> str: has_agent = self.maas_region.gather_rack_units().get(socket.getfqdn()) return "region+rack" if has_agent else "region" - def set_peer_data(self, app_or_unit: ops.Application | ops.Unit, key: str, data: Any) -> None: + def set_peer_data( + self, app_or_unit: Union[ops.Application, ops.Unit], key: str, data: Any + ) -> None: """Put information into the peer data bucket.""" if not self.peers: return self.peers.data[app_or_unit][key] = json.dumps(data or {}) - def get_peer_data(self, app_or_unit: ops.Application | ops.Unit, key: str) -> Any: + def get_peer_data(self, app_or_unit: Union[ops.Application, ops.Unit], key: str) -> Any: """Retrieve information from the peer data bucket.""" if not self.peers: return {} @@ -237,7 +239,7 @@ def _publish_tokens(self) -> bool: return True return False - def _get_regions(self) -> list[str]: + def _get_regions(self) -> List[str]: eps = [socket.getfqdn()] if peers := self.peers: for u in peers.units: diff --git a/maas-region/src/helper.py b/maas-region/src/helper.py index d576262..608f8b5 100644 --- a/maas-region/src/helper.py +++ b/maas-region/src/helper.py @@ -5,6 +5,7 @@ import subprocess from pathlib import Path +from typing import Union from charms.operator_libs_linux.v2.snap import SnapCache, SnapState @@ -38,31 +39,31 @@ def uninstall() -> None: maas.ensure(SnapState.Absent) @staticmethod - def get_installed_version() -> str | None: + def get_installed_version() -> Union[str, None]: """Get installed version. Returns: - str | None: version if installed + Union[str, None]: version if installed """ maas = SnapCache()[MAAS_SNAP_NAME] return maas.revision if maas.present else None @staticmethod - def get_installed_channel() -> str | None: + def get_installed_channel() -> Union[str, None]: """Get installed channel. Returns: - str | None: channel if installed + Union[str, None]: channel if installed """ maas = SnapCache()[MAAS_SNAP_NAME] return maas.channel if maas.present else None @staticmethod - def get_maas_id() -> str | None: + def get_maas_id() -> Union[str, None]: """Get MAAS system ID. Returns: - str | None: system_id, or None if not present + Union[str, None]: system_id, or None if not present """ try: with MAAS_ID.open() as file: @@ -71,11 +72,11 @@ def get_maas_id() -> str | None: return None @staticmethod - def get_maas_mode() -> str | None: + def get_maas_mode() -> Union[str, None]: """Get MAAS operation mode. Returns: - str | None: mode, or None if not initialised + Union[str, None]: mode, or None if not initialised """ try: with MAAS_MODE.open() as file: @@ -109,7 +110,7 @@ def set_running(enable: bool) -> None: @staticmethod def create_admin_user( - username: str, password: str, email: str, ssh_import: str | None + username: str, password: str, email: str, ssh_import: Union[str, None] ) -> None: """Create an Admin user. @@ -117,7 +118,7 @@ def create_admin_user( username (str): username password (str): new password email (str): user e-mail address - ssh_import (str | None): optional ssh to import + ssh_import (Union[str, None]): optional ssh to import Raises: CalledProcessError: failed to create user @@ -186,11 +187,11 @@ def setup_region(maas_url: str, dsn: str, mode: str) -> None: subprocess.check_call(cmd) @staticmethod - def get_maas_secret() -> str | None: + def get_maas_secret() -> Union[str, None]: """Get MAAS enrollment secret token. Returns: - str | None: token, or None if not present + Union[str, None]: token, or None if not present """ try: with MAAS_SECRET.open() as file: diff --git a/maas-region/tests/integration/test_charm.py b/maas-region/tests/integration/test_charm.py index ed2b820..6dae890 100644 --- a/maas-region/tests/integration/test_charm.py +++ b/maas-region/tests/integration/test_charm.py @@ -12,7 +12,7 @@ logger = logging.getLogger(__name__) -METADATA = yaml.safe_load(Path("./metadata.yaml").read_text()) +METADATA = yaml.safe_load(Path("./charmcraft.yaml").read_text()) APP_NAME = METADATA["name"] @@ -25,9 +25,35 @@ async def test_build_and_deploy(ops_test: OpsTest): # Build and deploy charm from local source folder charm = await ops_test.build_charm(".") - # Deploy the charm and wait for active/idle status + # Deploy the charm and wait for waiting/idle status await asyncio.gather( ops_test.model.deploy(charm, application_name=APP_NAME), + ops_test.model.wait_for_idle( + apps=[APP_NAME], status="waiting", raise_on_blocked=True, timeout=1000 + ), + ) + + +@pytest.mark.abort_on_fail +async def test_database_integration(ops_test: OpsTest): + """Verify that the charm integrates with the database. + + Assert that the charm is active if the integration is established. + """ + await asyncio.gather( + ops_test.model.deploy( + "postgresql", + application_name="postgresql", + channel="14/stable", + trust=True, + ), + ops_test.model.wait_for_idle( + apps=["postgresql"], status="active", raise_on_blocked=True, timeout=1000 + ), + ) + + await asyncio.gather( + ops_test.model.integrate(f"{APP_NAME}", "postgresql"), ops_test.model.wait_for_idle( apps=[APP_NAME], status="active", raise_on_blocked=True, timeout=1000 ), diff --git a/maas-region/tests/unit/test_charm.py b/maas-region/tests/unit/test_charm.py index 2d3ad7b..a186728 100644 --- a/maas-region/tests/unit/test_charm.py +++ b/maas-region/tests/unit/test_charm.py @@ -25,7 +25,6 @@ class TestCharm(unittest.TestCase): - def setUp(self): self.harness = ops.testing.Harness(MaasRegionCharm) self.harness.add_network("10.0.0.10") diff --git a/maas-region/tox.ini b/maas-region/tox.ini index 618774f..1046432 100644 --- a/maas-region/tox.ini +++ b/maas-region/tox.ini @@ -4,7 +4,7 @@ [tox] no_package = True skip_missing_interpreters = True -env_list = format, lint, static, unit +env_list = format, lint, static-{charm,lib}, unit, scenario min_version = 4.0.0 [vars] @@ -30,7 +30,7 @@ deps = ruff commands = black {[vars]all_path} - ruff --fix {[vars]all_path} + ruff check --fix {[vars]all_path} [testenv:lint] description = Check code against coding style standards @@ -43,7 +43,7 @@ commands = # and uncomment the following line # codespell {[vars]lib_path} codespell {tox_root} - ruff {[vars]all_path} + ruff check {[vars]all_path} black --check --diff {[vars]all_path} [testenv:unit] @@ -62,13 +62,21 @@ commands = {[vars]tests_path}/unit coverage report -[testenv:static] -description = Run static type checks +[testenv:static-{charm,lib}] +description = Run static analysis checks deps = pyright - -r {tox_root}/requirements.txt + typing-extensions + charm: -r{toxinidir}/requirements.txt + lib: ops + lib: jinja2 + unit: {[testenv:unit]deps} + integration: {[testenv:integration]deps} commands = - pyright {posargs} + charm: pyright --pythonversion 3.8 {[vars]src_path} {posargs} + lib: pyright --pythonversion 3.8 {[vars]lib_path} {posargs} + lib: /usr/bin/env sh -c 'for m in $(git diff main --name-only --line-prefix=`git rev-parse --show-toplevel`/ {[vars]lib_path}); do if ! git diff main $m | grep -q "+LIBPATCH\|+LIBAPI"; then echo "You forgot to bump the version on $m!"; exit 1; fi; done' +allowlist_externals = /usr/bin/env [testenv:integration] description = Run integration tests