Skip to content

Commit

Permalink
ci: add lxd provider for integration tests
Browse files Browse the repository at this point in the history
  • Loading branch information
skatsaounis committed Apr 1, 2024
1 parent 120cb31 commit cb3030b
Show file tree
Hide file tree
Showing 14 changed files with 176 additions and 88 deletions.
2 changes: 2 additions & 0 deletions .github/workflows/pull-request.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,12 @@ jobs:
secrets: inherit
with:
charm-path: maas-region
provider: lxd

pull-request-agent:
name: PR Agent
uses: canonical/observability/.github/workflows/charm-pull-request.yaml@main
secrets: inherit
with:
charm-path: maas-agent
provider: lxd
37 changes: 19 additions & 18 deletions maas-agent/lib/charms/maas_region/v0/maas.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,15 @@
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
from ops.framework import EventSource, Handle, Object
from typing_extensions import Self

# The unique Charmhub library identifier, never change it
LIBID = "50055f0422414543ba96d10a9fb7d129"
LIBID = "3e4a25698b094f96a59aa01367416ecb"

# Increment this major API version when introducing breaking changes
LIBAPI = 0
Expand All @@ -23,6 +23,7 @@
# to 0 if you are raising the major API version
LIBPATCH = 1


DEFAULT_ENDPOINT_NAME = "maas-region"
BUILTIN_JUJU_KEYS = {"ingress-address", "private-address", "egress-subnets"}

Expand All @@ -44,10 +45,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 = {}
Expand All @@ -71,7 +72,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:
Expand Down Expand Up @@ -130,12 +131,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)
Expand All @@ -156,7 +157,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
Expand All @@ -176,16 +177,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:
Expand All @@ -196,7 +197,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
Expand All @@ -218,18 +219,18 @@ 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)
self._charm = charm
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)
Expand All @@ -242,7 +243,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:
Expand All @@ -260,7 +261,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:
Expand All @@ -272,7 +273,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}")
Expand Down
5 changes: 3 additions & 2 deletions maas-agent/src/charm.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

import logging
import socket
from typing import Union

import ops
from charms.grafana_agent.v0 import cos_agent
Expand Down Expand Up @@ -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:
Expand All @@ -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:
Expand Down
17 changes: 9 additions & 8 deletions maas-agent/src/helper.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

import subprocess
from pathlib import Path
from typing import Union

from charms.operator_libs_linux.v2.snap import SnapCache, SnapState

Expand Down Expand Up @@ -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:
Expand All @@ -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:
Expand Down
42 changes: 40 additions & 2 deletions maas-agent/tests/integration/test_charm.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"]


Expand All @@ -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
),
Expand Down
3 changes: 2 additions & 1 deletion maas-agent/tests/unit/test_charm.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

import socket
import unittest
from typing import List
from unittest.mock import patch

import ops
Expand Down Expand Up @@ -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}
)
Expand Down
1 change: 0 additions & 1 deletion maas-agent/tests/unit/test_helper.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
22 changes: 15 additions & 7 deletions maas-agent/tox.ini
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand All @@ -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
Expand All @@ -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]
Expand All @@ -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
Expand Down
Loading

0 comments on commit cb3030b

Please sign in to comment.