From d03230b893c56cb04bcc4c5762c1aa68fd6ea4fb Mon Sep 17 00:00:00 2001 From: Luca Bello Date: Thu, 17 Nov 2022 15:22:08 +0100 Subject: [PATCH 01/14] update charms.traefik_route_k8s.v0.traefik_route library --- .../traefik_route_k8s/v0/traefik_route.py | 46 ++++++++++++++++--- 1 file changed, 40 insertions(+), 6 deletions(-) diff --git a/lib/charms/traefik_route_k8s/v0/traefik_route.py b/lib/charms/traefik_route_k8s/v0/traefik_route.py index 10a266c7..4cebccac 100644 --- a/lib/charms/traefik_route_k8s/v0/traefik_route.py +++ b/lib/charms/traefik_route_k8s/v0/traefik_route.py @@ -88,7 +88,7 @@ def __init__(self, *args): # Increment this PATCH version before using `charmcraft publish-lib` or reset # to 0 if you are raising the major API version -LIBPATCH = 3 +LIBPATCH = 2 log = logging.getLogger(__name__) @@ -148,7 +148,7 @@ def __init__( super().__init__(charm, relation_name) self._stored.set_default(external_host=None) - self.charm = charm + self._charm = charm self._relation_name = relation_name if self._stored.external_host != external_host: @@ -157,9 +157,31 @@ def __init__( self._update_requirers_with_external_host() self.framework.observe( - self.charm.on[relation_name].relation_changed, self._on_relation_changed + self._charm.on[relation_name].relation_changed, self._on_relation_changed ) + @property + def external_host(self) -> str: + """Return the external host set by Traefik, if any.""" + self._update_stored_external_host() + return self._stored.external_host or "" + + def _update_stored_external_host(self) -> None: + """Ensure that the stored host is up to date. + + This is split out into a separate method since, in the case of multi-unit deployments, + removal of a `TraefikRouteRequirer` will not cause a `RelationEvent`, but the guard on + app data ensures that only the previous leader will know what it is. Separating it + allows for re-use both when the property is called and if the relation changes, so a + leader change where the new leader checks the property will do the right thing.""" + if self._charm.unit.is_leader(): + for relation in self._charm.model.relations[self._relation_name]: + if not relation.app: + self._stored.external_host = "" + return + external_host = relation.data[relation.app].get("external_host", "") + self._stored.external_host = external_host or self._stored.external_host + def _on_relation_changed(self, event: RelationEvent): if self.is_ready(event.relation): # todo check data is valid here? @@ -168,11 +190,11 @@ def _on_relation_changed(self, event: RelationEvent): def _update_requirers_with_external_host(self): """Ensure that requirers know the external host for Traefik.""" - if not self.charm.unit.is_leader(): + if not self._charm.unit.is_leader(): return - for relation in self.charm.model.relations[self._relation_name]: - relation.data[self.charm.app]["external_host"] = self._stored.external_host + for relation in self._charm.model.relations[self._relation_name]: + relation.data[self._charm.app]["external_host"] = self.external_host @staticmethod def is_ready(relation: Relation) -> bool: @@ -213,6 +235,9 @@ def __init__(self, charm: CharmBase, relation: Relation, relation_name: str = "t self.framework.observe( self._charm.on[relation_name].relation_changed, self._on_relation_changed ) + self.framework.observe( + self._charm.on[relation_name].relation_broken, self._on_relation_broken + ) @property def external_host(self) -> str: @@ -231,6 +256,9 @@ def _update_stored_external_host(self) -> None: if self._charm.unit.is_leader(): if self._relation: for relation in self._charm.model.relations[self._relation.name]: + if not relation.app: + self._stored.external_host = "" + return external_host = relation.data[relation.app].get("external_host", "") self._stored.external_host = external_host or self._stored.external_host @@ -240,6 +268,12 @@ def _on_relation_changed(self, event: RelationEvent) -> None: if self._charm.unit.is_leader(): self.on.ready.emit(event.relation) + def _on_relation_broken(self, event: RelationEvent) -> None: + """On RelationBroken, clear the stored data if set and emit an event.""" + self._stored.external_host = "" + if self._charm.unit.is_leader(): + self.on.ready.emit(event.relation) + def is_ready(self) -> bool: """Is the TraefikRouteRequirer ready to submit data to Traefik?""" return self._relation is not None From 3009f6f21c85ef2a74891c6127f37f34f40cf14f Mon Sep 17 00:00:00 2001 From: Pietro Pasotti Date: Fri, 25 Nov 2022 12:26:17 +0100 Subject: [PATCH 02/14] itest updates --- .gitignore | 2 +- tests/integration/__init__.py | 7 ++ tests/integration/charms/README.md | 11 +++ tests/integration/conftest.py | 88 +++++++++++++++---- tests/integration/test_charm_ipa.py | 19 +--- tests/integration/test_charm_ipu.py | 21 +---- tests/integration/test_charm_tcp.py | 4 +- tests/integration/test_compatibility.py | 47 ++++++---- .../test_traefik_deployed_before_metallb.py | 50 +++-------- tests/integration/testers/.gitignore | 2 + .../integration/testers/route/charmcraft.yaml | 10 +++ tests/integration/testers/route/metadata.yaml | 10 +++ .../testers/route/requirements.txt | 1 + tests/integration/testers/route/src/charm.py | 23 +++++ 14 files changed, 187 insertions(+), 108 deletions(-) create mode 100644 tests/integration/__init__.py create mode 100644 tests/integration/charms/README.md create mode 100644 tests/integration/testers/route/charmcraft.yaml create mode 100644 tests/integration/testers/route/metadata.yaml create mode 100644 tests/integration/testers/route/requirements.txt create mode 100755 tests/integration/testers/route/src/charm.py diff --git a/.gitignore b/.gitignore index ca711473..e36a5b63 100644 --- a/.gitignore +++ b/.gitignore @@ -5,4 +5,4 @@ build/ .coverage __pycache__/ *.py[cod] -.idea \ No newline at end of file +.idea diff --git a/tests/integration/__init__.py b/tests/integration/__init__.py new file mode 100644 index 00000000..3d769cc5 --- /dev/null +++ b/tests/integration/__init__.py @@ -0,0 +1,7 @@ +import sys +import os +from pathlib import Path + +charm_root = Path(__file__).parent.parent.parent +sys.path.append(str(charm_root.absolute())) +os.environ['TOX_ENV_DIR'] = str((charm_root / '.tox' / 'integration').absolute()) diff --git a/tests/integration/charms/README.md b/tests/integration/charms/README.md new file mode 100644 index 00000000..55e87cf5 --- /dev/null +++ b/tests/integration/charms/README.md @@ -0,0 +1,11 @@ +# About this folder +In `conftest.py` there's a `build_charm_or_fetch_cached` function that: + +- checks if a `.charm` file is present in this folder +- if so, returns that package +- if not, builds the charm and puts it here, then returns the package + +This means that you can iterate on test code quickly, but whenever there's changes to the charm code, you'll need to clear the cache manually. + +You can enable using the cache by (temporarily) setting `conftest.CACHE_CHARMS = True`. +This feature is by default disabled. \ No newline at end of file diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py index acc1c393..a06dbfda 100644 --- a/tests/integration/conftest.py +++ b/tests/integration/conftest.py @@ -3,11 +3,13 @@ import functools import logging import os +import shutil from collections import defaultdict from dataclasses import dataclass from datetime import datetime from pathlib import Path from subprocess import PIPE, Popen +from typing import Union import pytest import yaml @@ -16,6 +18,13 @@ charm_root = Path(__file__).parent.parent.parent trfk_meta = yaml.safe_load((charm_root / "metadata.yaml").read_text()) trfk_resources = {name: val["upstream-source"] for name, val in trfk_meta["resources"].items()} +charm_cache = Path(__file__).parent / 'charms' + +USE_CACHE = False # you can flip this to true when testing locally. Do not commit! +if USE_CACHE: + logging.warning('USE_CACHE:: charms will be packed once and stored in ' + './tests/integration/charms. Clear them manually if you ' + 'have made changes to the charm code.') _JUJU_DATA_CACHE = {} _JUJU_KEYS = ("egress-subnets", "ingress-address", "private-address") @@ -60,11 +69,61 @@ async def wrapper(*args, **kwargs): return wrapper +async def build_charm_or_fetch_cached( + ops_test: OpsTest, + charm_name: str, + build_root: Union[str, Path]): + if USE_CACHE: + cached_charm_path = charm_cache / f'{charm_name}.charm' + if cached_charm_path.exists(): + tstamp = datetime.fromtimestamp(os.path.getmtime(cached_charm_path)) + logger.info(f'Found cached charm {charm_name} timestamp={tstamp}.') + charm_copy = Path(build_root) / f'{charm_name}.fromcache.charm' + shutil.copyfile(cached_charm_path, charm_copy) + return charm_copy.absolute() # in case someone deletes it after deploy. + + charm = await ops_test.build_charm(build_root) + if USE_CACHE: + shutil.copyfile(charm, cached_charm_path) + return charm + else: + return await ops_test.build_charm(build_root) + + @pytest.fixture(scope="module") @timed_memoizer async def traefik_charm(ops_test: OpsTest): - charm = await ops_test.build_charm(".") - return charm + return await build_charm_or_fetch_cached(ops_test, 'traefik', './') + + +@pytest.fixture(scope="module") +async def ipa_tester_charm(ops_test: OpsTest): + ipa_charm_root = (Path(__file__).parent / "testers" / "ipa").absolute() + lib_source = Path() / "lib" / "charms" / "traefik_k8s" / "v1" / "ingress.py" + libs_folder = ipa_charm_root / "lib" / "charms" / "traefik_k8s" / "v1" + libs_folder.mkdir(parents=True, exist_ok=True) + shutil.copy(lib_source, libs_folder) + return await build_charm_or_fetch_cached(ops_test, 'ipa-tester', ipa_charm_root) + + +@pytest.fixture(scope="module") +async def ipu_tester_charm(ops_test: OpsTest): + ipu_charm_root = (Path(__file__).parent / "testers" / "ipu").absolute() + lib_source = Path() / "lib" / "charms" / "traefik_k8s" / "v1" / "ingress_per_unit.py" + libs_folder = ipu_charm_root / "lib" / "charms" / "traefik_k8s" / "v1" + libs_folder.mkdir(parents=True, exist_ok=True) + shutil.copy(lib_source, libs_folder) + return await build_charm_or_fetch_cached(ops_test, 'ipu-tester', ipu_charm_root) + + +@pytest.fixture(scope="module") +async def route_tester_charm(ops_test: OpsTest): + route_charm_root = (Path(__file__).parent / "testers" / "route").absolute() + lib_source = Path() / "lib" / "charms" / "traefik_route_k8s" / "v0" / "traefik_route.py" + libs_folder = route_charm_root / "lib" / "charms" / "traefik_route_k8s" / "v0" + libs_folder.mkdir(parents=True, exist_ok=True) + shutil.copy(lib_source, libs_folder) + return await build_charm_or_fetch_cached(ops_test, 'route-tester', route_charm_root) def purge(data: dict): @@ -127,10 +186,10 @@ def get_relation_by_endpoint(relations, local_endpoint, remote_endpoint, remote_ r for r in relations if ( - (r["endpoint"] == local_endpoint and r["related-endpoint"] == remote_endpoint) - or (r["endpoint"] == remote_endpoint and r["related-endpoint"] == local_endpoint) - ) - and remote_obj in r["related-units"] + (r["endpoint"] == local_endpoint and r["related-endpoint"] == remote_endpoint) + or (r["endpoint"] == remote_endpoint and r["related-endpoint"] == local_endpoint) + ) + and remote_obj in r["related-units"] ] if not matches: raise ValueError( @@ -157,7 +216,7 @@ class UnitRelationData: def get_content( - obj: str, other_obj, include_default_juju_keys: bool = False, model: str = None + obj: str, other_obj, include_default_juju_keys: bool = False, model: str = None ) -> UnitRelationData: """Get the content of the databag of `obj`, as seen from `other_obj`.""" unit_name, endpoint = obj.split(":") @@ -199,11 +258,11 @@ class RelationData: def get_relation_data( - *, - provider_endpoint: str, - requirer_endpoint: str, - include_default_juju_keys: bool = False, - model: str = None, + *, + provider_endpoint: str, + requirer_endpoint: str, + include_default_juju_keys: bool = False, + model: str = None, ): """Get relation databags for a juju relation. @@ -237,13 +296,10 @@ async def deploy_traefik_if_not_deployed(ops_test: OpsTest, traefik_charm): # if we're running this locally, we need to wait for "waiting" # CI however deploys all in a single model, so traefik is active already # if a previous test has already set it up. - wait_for = "waiting" - else: - wait_for = "active" # block until traefik goes to... async with ops_test.fast_forward(): - await ops_test.model.wait_for_idle(["traefik-k8s"], status=wait_for, timeout=1000) + await ops_test.model.wait_for_idle(["traefik-k8s"], status="active", timeout=1000) # we set the external hostname to traefik-k8s's own ip traefik_address = await get_address(ops_test, "traefik-k8s") diff --git a/tests/integration/test_charm_ipa.py b/tests/integration/test_charm_ipa.py index 54c64dec..664d7f71 100644 --- a/tests/integration/test_charm_ipa.py +++ b/tests/integration/test_charm_ipa.py @@ -15,21 +15,6 @@ get_relation_data, ) -ipa_charm_root = (Path(__file__).parent / "testers" / "ipa").absolute() -meta = yaml.safe_load((ipa_charm_root / "metadata.yaml").read_text()) -ipa_tester_resources = { - name: val["upstream-source"] for name, val in meta.get("resources", {}).items() -} - - -@pytest_asyncio.fixture -async def ipa_tester_charm(ops_test: OpsTest): - lib_source = Path() / "lib" / "charms" / "traefik_k8s" / "v1" / "ingress.py" - libs_folder = ipa_charm_root / "lib" / "charms" / "traefik_k8s" / "v1" - libs_folder.mkdir(parents=True, exist_ok=True) - shutil.copy(lib_source, libs_folder) - return await ops_test.build_charm(ipa_charm_root) - @pytest.mark.abort_on_fail async def test_deployment(ops_test: OpsTest, traefik_charm, ipa_tester_charm): @@ -46,7 +31,7 @@ async def test_relate(ops_test: OpsTest): await ops_test.model.wait_for_idle(["traefik-k8s", "ipa-tester"]) -async def assert_ipa_charm_has_ingress(ops_test: OpsTest): +def assert_ipa_charm_has_ingress(ops_test: OpsTest): data = get_relation_data( requirer_endpoint="ipa-tester/0:ingress", provider_endpoint="traefik-k8s/0:ingress", @@ -59,7 +44,7 @@ async def assert_ipa_charm_has_ingress(ops_test: OpsTest): async def test_ipa_charm_has_ingress(ops_test: OpsTest): - await assert_ipa_charm_has_ingress(ops_test) + assert_ipa_charm_has_ingress(ops_test) @pytest.mark.abort_on_fail diff --git a/tests/integration/test_charm_ipu.py b/tests/integration/test_charm_ipu.py index fb1a9250..4e492973 100644 --- a/tests/integration/test_charm_ipu.py +++ b/tests/integration/test_charm_ipu.py @@ -12,24 +12,9 @@ assert_can_ping, deploy_traefik_if_not_deployed, get_address, - get_relation_data, + get_relation_data, build_charm_or_fetch_cached, ) -ipu_charm_root = (Path(__file__).parent / "testers" / "ipu").absolute() -meta = yaml.safe_load((ipu_charm_root / "metadata.yaml").read_text()) -ipu_tester_resources = { - name: val["upstream-source"] for name, val in meta.get("resources", {}).items() -} - - -@pytest_asyncio.fixture -async def ipu_tester_charm(ops_test: OpsTest): - lib_source = Path() / "lib" / "charms" / "traefik_k8s" / "v1" / "ingress_per_unit.py" - libs_folder = ipu_charm_root / "lib" / "charms" / "traefik_k8s" / "v1" - libs_folder.mkdir(parents=True, exist_ok=True) - shutil.copy(lib_source, libs_folder) - return await ops_test.build_charm(ipu_charm_root) - @pytest.mark.abort_on_fail async def test_deployment(ops_test: OpsTest, traefik_charm, ipu_tester_charm): @@ -50,7 +35,7 @@ async def test_relate(ops_test: OpsTest): await ops_test.model.wait_for_idle(["traefik-k8s", "ipu-tester"]) -async def assert_ipu_charm_has_ingress(ops_test: OpsTest): +def assert_ipu_charm_has_ingress(ops_test: OpsTest): data = get_relation_data( requirer_endpoint="ipu-tester/0:ingress-per-unit", provider_endpoint="traefik-k8s/0:ingress-per-unit", @@ -63,7 +48,7 @@ async def assert_ipu_charm_has_ingress(ops_test: OpsTest): async def test_ipu_charm_has_ingress(ops_test: OpsTest): - await assert_ipu_charm_has_ingress(ops_test) + assert_ipu_charm_has_ingress(ops_test) @pytest.mark.abort_on_fail diff --git a/tests/integration/test_charm_tcp.py b/tests/integration/test_charm_tcp.py index 6b30d536..e10d2cb7 100644 --- a/tests/integration/test_charm_tcp.py +++ b/tests/integration/test_charm_tcp.py @@ -95,7 +95,7 @@ async def test_relation_data_shape(ops_test: OpsTest): assert provider_app_data == {"tcp-tester/0": {"url": f"{traefik_unit_ip}:{port}"}} -async def assert_tcp_charm_has_ingress(ops_test: OpsTest): +def assert_tcp_charm_has_ingress(ops_test: OpsTest): traefik_unit_ip = get_unit_ip(ops_test) data = get_relation_data( requirer_endpoint="tcp-tester/0:ingress-per-unit", @@ -113,7 +113,7 @@ async def assert_tcp_charm_has_ingress(ops_test: OpsTest): async def test_tcp_connection(ops_test: OpsTest): - await assert_tcp_charm_has_ingress(ops_test) + assert_tcp_charm_has_ingress(ops_test) async def test_remove_relation(ops_test: OpsTest): diff --git a/tests/integration/test_compatibility.py b/tests/integration/test_compatibility.py index a04987d3..139e4e46 100644 --- a/tests/integration/test_compatibility.py +++ b/tests/integration/test_compatibility.py @@ -1,5 +1,7 @@ # Copyright 2022 Canonical Ltd. # See LICENSE file for licensing details. +import asyncio + import juju.errors import pytest_asyncio from pytest_operator.plugin import OpsTest @@ -36,15 +38,20 @@ async def safe_relate(ops_test: OpsTest, ep1, ep2): @pytest_asyncio.fixture async def tcp_ipa_deployment( - ops_test: OpsTest, traefik_charm, tcp_tester_charm, ipa_tester_charm # noqa + ops_test: OpsTest, traefik_charm, tcp_tester_charm, ipa_tester_charm # noqa ): - await deploy_traefik_if_not_deployed(ops_test, traefik_charm) - await deploy_charm_if_not_deployed( - ops_test, tcp_tester_charm, "tcp-tester", resources=tcp_charm_resources + await asyncio.gather( + deploy_traefik_if_not_deployed(ops_test, traefik_charm), + deploy_charm_if_not_deployed( + ops_test, tcp_tester_charm, "tcp-tester", resources=tcp_charm_resources + ), + deploy_charm_if_not_deployed(ops_test, ipa_tester_charm, "ipa-tester") + ) + await asyncio.gather( + safe_relate(ops_test, "tcp-tester", "traefik-k8s"), + safe_relate(ops_test, "ipa-tester", "traefik-k8s") ) - await deploy_charm_if_not_deployed(ops_test, ipa_tester_charm, "ipa-tester") - await safe_relate(ops_test, "tcp-tester", "traefik-k8s") - await safe_relate(ops_test, "ipa-tester", "traefik-k8s") + async with ops_test.fast_forward(): await ops_test.model.wait_for_idle( ["traefik-k8s", "tcp-tester", "ipa-tester"], status="active", timeout=1000 @@ -57,15 +64,19 @@ async def tcp_ipa_deployment( @pytest_asyncio.fixture async def tcp_ipu_deployment( - ops_test: OpsTest, traefik_charm, tcp_tester_charm, ipu_tester_charm # noqa + ops_test: OpsTest, traefik_charm, tcp_tester_charm, ipu_tester_charm # noqa ): - await deploy_traefik_if_not_deployed(ops_test, traefik_charm) - await deploy_charm_if_not_deployed( - ops_test, tcp_tester_charm, "tcp-tester", resources=tcp_charm_resources + await asyncio.gather( + deploy_traefik_if_not_deployed(ops_test, traefik_charm), + await deploy_charm_if_not_deployed( + ops_test, tcp_tester_charm, "tcp-tester", resources=tcp_charm_resources + ), + await deploy_charm_if_not_deployed(ops_test, ipu_tester_charm, "ipu-tester") + ) + await asyncio.gather( + safe_relate(ops_test, "tcp-tester", "traefik-k8s"), + safe_relate(ops_test, "ipu-tester", "traefik-k8s") ) - await deploy_charm_if_not_deployed(ops_test, ipu_tester_charm, "ipu-tester") - await safe_relate(ops_test, "tcp-tester", "traefik-k8s") - await safe_relate(ops_test, "ipu-tester", "traefik-k8s") async with ops_test.fast_forward(): await ops_test.model.wait_for_idle( ["traefik-k8s", "tcp-tester", "ipu-tester"], status="active", timeout=1000 @@ -76,10 +87,10 @@ async def tcp_ipu_deployment( async def test_tcp_ipu_compatibility(ops_test, tcp_ipu_deployment): - await assert_tcp_charm_has_ingress(ops_test) - await assert_ipu_charm_has_ingress(ops_test) + assert_tcp_charm_has_ingress(ops_test) + assert_ipu_charm_has_ingress(ops_test) async def test_tcp_ipa_compatibility(ops_test, tcp_ipa_deployment): - await assert_tcp_charm_has_ingress(ops_test) - await assert_ipa_charm_has_ingress(ops_test) + assert_tcp_charm_has_ingress(ops_test) + assert_ipa_charm_has_ingress(ops_test) diff --git a/tests/integration/test_traefik_deployed_before_metallb.py b/tests/integration/test_traefik_deployed_before_metallb.py index 5fcdc1ea..2ccc02df 100644 --- a/tests/integration/test_traefik_deployed_before_metallb.py +++ b/tests/integration/test_traefik_deployed_before_metallb.py @@ -17,11 +17,10 @@ import asyncio import logging from pathlib import Path -from types import SimpleNamespace import pytest import yaml -from helpers import disable_metallb, enable_metallb +from tests.integration.helpers import disable_metallb, enable_metallb from pytest_operator.plugin import OpsTest logger = logging.getLogger(__name__) @@ -29,53 +28,32 @@ METADATA = yaml.safe_load(Path("./metadata.yaml").read_text()) resources = {"traefik-image": METADATA["resources"]["traefik-image"]["upstream-source"]} -trfk = SimpleNamespace(name="traefik", resources=resources) - -ipu = SimpleNamespace(charm="ch:prometheus-k8s", name="prometheus") # per unit -ipa = SimpleNamespace(charm="ch:alertmanager-k8s", name="alertmanager") # per app -ipr = SimpleNamespace(charm="ch:grafana-k8s", name="grafana") # traefik route idle_period = 90 @pytest.mark.abort_on_fail -async def test_build_and_deploy(ops_test: OpsTest, traefik_charm): +async def test_build_and_deploy(ops_test: OpsTest, traefik_charm, + ipa_tester_charm, ipu_tester_charm, + route_tester_charm): logger.info("First, disable metallb, in case it's enabled") await disable_metallb() await asyncio.gather( ops_test.model.deploy( - traefik_charm, resources=trfk.resources, application_name=trfk.name, series="focal" - ), - ops_test.model.deploy( - ipu.charm, - application_name=ipu.name, - channel="edge", # TODO change to "stable" once available - trust=True, - series="focal", - ), - ops_test.model.deploy( - ipa.charm, - application_name=ipa.name, - channel="edge", # TODO change to "stable" once available - trust=True, - series="focal", - ), - ops_test.model.deploy( - ipr.charm, - application_name=ipr.name, - channel="edge", # TODO change to "stable" once available - trust=True, - series="focal", + traefik_charm, resources=resources, application_name="traefik", series="focal" ), + ops_test.model.deploy(ipu_tester_charm, application_name='ipu-tester', series="focal"), + ops_test.model.deploy(ipa_tester_charm, application_name='ipa-tester', series="focal"), + ops_test.model.deploy(route_tester_charm, application_name='route-tester', series="focal"), ) await ops_test.model.wait_for_idle(timeout=600, idle_period=30) await asyncio.gather( - ops_test.model.add_relation(f"{ipu.name}:ingress", trfk.name), - ops_test.model.add_relation(f"{ipa.name}:ingress", trfk.name), - ops_test.model.add_relation(f"{ipr.name}:ingress", trfk.name), + ops_test.model.add_relation("ipu-tester", "traefik"), + ops_test.model.add_relation("ipa-tester", "traefik"), + ops_test.model.add_relation("route-tester", "traefik"), ) await ops_test.model.wait_for_idle(timeout=600, idle_period=idle_period) @@ -91,9 +69,9 @@ async def test_ingressed_endpoints_reachable_after_metallb_enabled(ops_test: Ops endpoints = [ f"{ip}/{path}" for path in [ - f"{ops_test.model_name}-{ipr.name}", - f"{ops_test.model_name}-{ipu.name}-0", - f"{ops_test.model_name}-{ipa.name}", + f"{ops_test.model_name}-route-tester", + f"{ops_test.model_name}-ipu-tester-0", + f"{ops_test.model_name}-ipa-tester", ] ] for ep in endpoints: diff --git a/tests/integration/testers/.gitignore b/tests/integration/testers/.gitignore index e595a077..fc2384ad 100644 --- a/tests/integration/testers/.gitignore +++ b/tests/integration/testers/.gitignore @@ -1,3 +1,5 @@ /ipa/lib/charms/traefik_k8s/v1/ingress.py /ipu/lib/charms/traefik_k8s/v1/ingress_per_unit.py /tcp/lib/charms/traefik_k8s/v1/ingress_per_unit.py +/route/lib/charms/traefik_route_k8s/v0/traefik_route.py +/charms/ diff --git a/tests/integration/testers/route/charmcraft.yaml b/tests/integration/testers/route/charmcraft.yaml new file mode 100644 index 00000000..6cb33e54 --- /dev/null +++ b/tests/integration/testers/route/charmcraft.yaml @@ -0,0 +1,10 @@ +# Copyright 2022 Canonical Ltd. +# See LICENSE file for licensing details. +type: charm +bases: + - build-on: + - name: "ubuntu" + channel: "20.04" + run-on: + - name: "ubuntu" + channel: "20.04" \ No newline at end of file diff --git a/tests/integration/testers/route/metadata.yaml b/tests/integration/testers/route/metadata.yaml new file mode 100644 index 00000000..32e25ac6 --- /dev/null +++ b/tests/integration/testers/route/metadata.yaml @@ -0,0 +1,10 @@ +# Copyright 2022 Canonical Ltd. +# See LICENSE file for licensing details. +name: route-requirer-mock +display-name: route-requirer-mock +description: route tester +summary: route tester +requires: + traefik-route: + interface: traefik_route + limit: 1 diff --git a/tests/integration/testers/route/requirements.txt b/tests/integration/testers/route/requirements.txt new file mode 100644 index 00000000..f7d96f1c --- /dev/null +++ b/tests/integration/testers/route/requirements.txt @@ -0,0 +1 @@ +ops == 1.5.0 diff --git a/tests/integration/testers/route/src/charm.py b/tests/integration/testers/route/src/charm.py new file mode 100755 index 00000000..4682a63f --- /dev/null +++ b/tests/integration/testers/route/src/charm.py @@ -0,0 +1,23 @@ +#!/usr/bin/env python3 +# Copyright 2022 Canonical Ltd. +# See LICENSE file for licensing details. +from ops.charm import CharmBase +from ops.model import ActiveStatus + +from charms.traefik_route_k8s.v0.traefik_route import TraefikRouteRequirer + + +class RouteRequirerMock(CharmBase): + def __init__(self, framework): + super().__init__(framework, None) + self.traefik_route = TraefikRouteRequirer( + self, self.model.get_relation('traefik-route'), 'traefik_route' + ) + if self.traefik_route.is_ready(): + self.traefik_route.submit_to_traefik(config={}) + self.unit.status = ActiveStatus('ready') + + +if __name__ == "__main__": + from ops.main import main + main(RouteRequirerMock) From 31a1ee23866ee08d3ae38d65247d4bd8fc771acc Mon Sep 17 00:00:00 2001 From: Pietro Pasotti Date: Fri, 25 Nov 2022 14:00:39 +0100 Subject: [PATCH 03/14] spellbook --- tests/integration/__init__.py | 4 +- tests/integration/conftest.py | 100 ++++++++---------- .../{charms => spellbook}/README.md | 0 .../integration/spellbook/build_all_caches.py | 18 ++++ tests/integration/spellbook/cache.py | 86 +++++++++++++++ tests/integration/test_charm_ipu.py | 5 +- tests/integration/test_charm_tcp.py | 9 -- tests/integration/test_compatibility.py | 31 +++--- .../test_traefik_deployed_after_metallb.py | 3 +- .../test_traefik_deployed_before_metallb.py | 15 +-- tests/integration/testers/route/src/charm.py | 8 +- 11 files changed, 175 insertions(+), 104 deletions(-) rename tests/integration/{charms => spellbook}/README.md (100%) create mode 100644 tests/integration/spellbook/build_all_caches.py create mode 100644 tests/integration/spellbook/cache.py diff --git a/tests/integration/__init__.py b/tests/integration/__init__.py index 3d769cc5..455ccc49 100644 --- a/tests/integration/__init__.py +++ b/tests/integration/__init__.py @@ -1,7 +1,7 @@ -import sys import os +import sys from pathlib import Path charm_root = Path(__file__).parent.parent.parent sys.path.append(str(charm_root.absolute())) -os.environ['TOX_ENV_DIR'] = str((charm_root / '.tox' / 'integration').absolute()) +os.environ["TOX_ENV_DIR"] = str((charm_root / ".tox" / "integration").absolute()) diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py index a06dbfda..8f3c9c02 100644 --- a/tests/integration/conftest.py +++ b/tests/integration/conftest.py @@ -9,22 +9,17 @@ from datetime import datetime from pathlib import Path from subprocess import PIPE, Popen -from typing import Union import pytest import yaml from pytest_operator.plugin import OpsTest +from tests.integration.spellbook.cache import build_charm_or_fetch_cached + charm_root = Path(__file__).parent.parent.parent trfk_meta = yaml.safe_load((charm_root / "metadata.yaml").read_text()) trfk_resources = {name: val["upstream-source"] for name, val in trfk_meta["resources"].items()} -charm_cache = Path(__file__).parent / 'charms' -USE_CACHE = False # you can flip this to true when testing locally. Do not commit! -if USE_CACHE: - logging.warning('USE_CACHE:: charms will be packed once and stored in ' - './tests/integration/charms. Clear them manually if you ' - 'have made changes to the charm code.') _JUJU_DATA_CACHE = {} _JUJU_KEYS = ("egress-subnets", "ingress-address", "private-address") @@ -69,61 +64,50 @@ async def wrapper(*args, **kwargs): return wrapper -async def build_charm_or_fetch_cached( - ops_test: OpsTest, - charm_name: str, - build_root: Union[str, Path]): - if USE_CACHE: - cached_charm_path = charm_cache / f'{charm_name}.charm' - if cached_charm_path.exists(): - tstamp = datetime.fromtimestamp(os.path.getmtime(cached_charm_path)) - logger.info(f'Found cached charm {charm_name} timestamp={tstamp}.') - charm_copy = Path(build_root) / f'{charm_name}.fromcache.charm' - shutil.copyfile(cached_charm_path, charm_copy) - return charm_copy.absolute() # in case someone deletes it after deploy. - - charm = await ops_test.build_charm(build_root) - if USE_CACHE: - shutil.copyfile(charm, cached_charm_path) - return charm - else: - return await ops_test.build_charm(build_root) - - @pytest.fixture(scope="module") @timed_memoizer -async def traefik_charm(ops_test: OpsTest): - return await build_charm_or_fetch_cached(ops_test, 'traefik', './') +async def traefik_charm(): + return build_charm_or_fetch_cached("traefik", "./") @pytest.fixture(scope="module") -async def ipa_tester_charm(ops_test: OpsTest): +async def ipa_tester_charm(): ipa_charm_root = (Path(__file__).parent / "testers" / "ipa").absolute() - lib_source = Path() / "lib" / "charms" / "traefik_k8s" / "v1" / "ingress.py" - libs_folder = ipa_charm_root / "lib" / "charms" / "traefik_k8s" / "v1" - libs_folder.mkdir(parents=True, exist_ok=True) - shutil.copy(lib_source, libs_folder) - return await build_charm_or_fetch_cached(ops_test, 'ipa-tester', ipa_charm_root) + return build_charm_or_fetch_cached( + "ipa-tester", + ipa_charm_root, + pull_libs=[Path() / "lib" / "charms" / "traefik_k8s" / "v1" / "ingress.py"], + ) @pytest.fixture(scope="module") -async def ipu_tester_charm(ops_test: OpsTest): +async def ipu_tester_charm(): ipu_charm_root = (Path(__file__).parent / "testers" / "ipu").absolute() - lib_source = Path() / "lib" / "charms" / "traefik_k8s" / "v1" / "ingress_per_unit.py" - libs_folder = ipu_charm_root / "lib" / "charms" / "traefik_k8s" / "v1" - libs_folder.mkdir(parents=True, exist_ok=True) - shutil.copy(lib_source, libs_folder) - return await build_charm_or_fetch_cached(ops_test, 'ipu-tester', ipu_charm_root) + return build_charm_or_fetch_cached( + "ipu-tester", + ipu_charm_root, + pull_libs=[Path() / "lib" / "charms" / "traefik_k8s" / "v1" / "ingress_per_unit.py"], + ) @pytest.fixture(scope="module") -async def route_tester_charm(ops_test: OpsTest): +async def ipu_tester_charm(): + tcp_charm_root = (Path(__file__).parent / "testers" / "tcp").absolute() + return build_charm_or_fetch_cached( + "tcp-tester", + tcp_charm_root, + pull_libs=[Path() / "lib" / "charms" / "traefik_k8s" / "v1" / "ingress_per_unit.py"], + ) + + +@pytest.fixture(scope="module") +async def route_tester_charm(): route_charm_root = (Path(__file__).parent / "testers" / "route").absolute() - lib_source = Path() / "lib" / "charms" / "traefik_route_k8s" / "v0" / "traefik_route.py" - libs_folder = route_charm_root / "lib" / "charms" / "traefik_route_k8s" / "v0" - libs_folder.mkdir(parents=True, exist_ok=True) - shutil.copy(lib_source, libs_folder) - return await build_charm_or_fetch_cached(ops_test, 'route-tester', route_charm_root) + return build_charm_or_fetch_cached( + "route-tester", + route_charm_root, + pull_libs=[Path() / "lib" / "charms" / "traefik_route_k8s" / "v0" / "traefik_route.py"], + ) def purge(data: dict): @@ -186,10 +170,10 @@ def get_relation_by_endpoint(relations, local_endpoint, remote_endpoint, remote_ r for r in relations if ( - (r["endpoint"] == local_endpoint and r["related-endpoint"] == remote_endpoint) - or (r["endpoint"] == remote_endpoint and r["related-endpoint"] == local_endpoint) - ) - and remote_obj in r["related-units"] + (r["endpoint"] == local_endpoint and r["related-endpoint"] == remote_endpoint) + or (r["endpoint"] == remote_endpoint and r["related-endpoint"] == local_endpoint) + ) + and remote_obj in r["related-units"] ] if not matches: raise ValueError( @@ -216,7 +200,7 @@ class UnitRelationData: def get_content( - obj: str, other_obj, include_default_juju_keys: bool = False, model: str = None + obj: str, other_obj, include_default_juju_keys: bool = False, model: str = None ) -> UnitRelationData: """Get the content of the databag of `obj`, as seen from `other_obj`.""" unit_name, endpoint = obj.split(":") @@ -258,11 +242,11 @@ class RelationData: def get_relation_data( - *, - provider_endpoint: str, - requirer_endpoint: str, - include_default_juju_keys: bool = False, - model: str = None, + *, + provider_endpoint: str, + requirer_endpoint: str, + include_default_juju_keys: bool = False, + model: str = None, ): """Get relation databags for a juju relation. diff --git a/tests/integration/charms/README.md b/tests/integration/spellbook/README.md similarity index 100% rename from tests/integration/charms/README.md rename to tests/integration/spellbook/README.md diff --git a/tests/integration/spellbook/build_all_caches.py b/tests/integration/spellbook/build_all_caches.py new file mode 100644 index 00000000..b37ff73f --- /dev/null +++ b/tests/integration/spellbook/build_all_caches.py @@ -0,0 +1,18 @@ +# Copyright 2022 Canonical Ltd. +# See LICENSE file for licensing details. +from pathlib import Path + +from tests.integration.spellbook.cache import build_charm_or_fetch_cached + +testers_root = Path(__file__).parent.parent / "testers" + + +def main(): + build_charm_or_fetch_cached("route-tester", testers_root / "route"), + build_charm_or_fetch_cached("ipa-tester", testers_root / "ipa"), + build_charm_or_fetch_cached("ipu-tester", testers_root / "ipu"), + build_charm_or_fetch_cached("tcp-tester", testers_root / "tcp"), + + +if __name__ == "__main__": + main() diff --git a/tests/integration/spellbook/cache.py b/tests/integration/spellbook/cache.py new file mode 100644 index 00000000..a00de0ee --- /dev/null +++ b/tests/integration/spellbook/cache.py @@ -0,0 +1,86 @@ +# Copyright 2022 Canonical Ltd. +# See LICENSE file for licensing details. +import logging +import os +import shutil +from datetime import datetime +from hashlib import md5 +from pathlib import Path +from subprocess import getoutput +from typing import List, Union + +charm_cache = Path(__file__).parent + +USE_CACHE = True # you can flip this to true when testing locally. Do not commit! +if USE_CACHE: + logging.warning( + "USE_CACHE:: charms will be packed once and stored in " + "./tests/integration/charms. Clear them manually if you " + "have made changes to the charm code." + ) + + +def build_charm_or_fetch_cached( + charm_name: str, + build_root: Union[str, Path], + pull_libs: List[Path] = None, + use_cache=USE_CACHE, +): + # caching or not, we need to ensure the libs the charm depends on are up to date. + + if pull_libs: + for lib in pull_libs: + lib_source = Path(lib) + lib_path = build_root + + for part in lib_source.parent.parts[:-5:-1]: + lib_path /= part + + lib_path = lib_path.absolute() + # ensure it exists + lib_path.mkdir(parents=True, exist_ok=True) + shutil.copy(lib_source, lib_path) + logging.info(f"copying {lib_source} -> {lib_path}") + + def do_build(): + pack_out = getoutput(f"charmcraft pack -p {build_root}") + return (Path(os.getcwd()) / pack_out.split("\n")[-1].strip()).absolute() + + if not use_cache: + logging.info("not using cache") + return do_build() + + logging.info(f"hashing {build_root}") + root_md5 = getoutput(f'find {build_root} -type f -exec md5sum "{{}}" +') + # builtins.hash() is unpredictable on str + charm_tree_sum = md5(root_md5.encode("utf-8")).hexdigest() + + logging.info(f"hash: {charm_tree_sum}") + + cached_charm_path = charm_cache / f"{charm_name}.{charm_tree_sum}.charm" + # in case someone deletes it after deploy, we make a copy. + charm_copy = (charm_cache / f"{charm_name}.unfrozen.charm").absolute() + + # clear any dirty cache + dirty_cache_found = False + for fname in charm_cache.glob(f"{charm_name}.*"): + if fname != cached_charm_path: + dirty_cache_found = True + logging.info(f"deleting dirty cache: {fname}") + fname.unlink() + + if cached_charm_path.exists(): + tstamp = datetime.fromtimestamp(os.path.getmtime(cached_charm_path)) + logging.info(f"Found cached charm {charm_name} timestamp={tstamp}.") + shutil.copyfile(cached_charm_path, charm_copy) + return charm_copy + + if dirty_cache_found: + logging.info(f"Cache for {charm_name} is dirty. Repacking...") + else: + logging.info(f"Cache not found for charm {charm_name}. Packing...") + + charm = do_build() + shutil.copyfile(charm, cached_charm_path) + shutil.copyfile(charm, charm_copy) + return charm_copy diff --git a/tests/integration/test_charm_ipu.py b/tests/integration/test_charm_ipu.py index 4e492973..075bf604 100644 --- a/tests/integration/test_charm_ipu.py +++ b/tests/integration/test_charm_ipu.py @@ -1,10 +1,7 @@ # Copyright 2022 Canonical Ltd. # See LICENSE file for licensing details. -import shutil -from pathlib import Path import pytest -import pytest_asyncio import yaml from pytest_operator.plugin import OpsTest @@ -12,7 +9,7 @@ assert_can_ping, deploy_traefik_if_not_deployed, get_address, - get_relation_data, build_charm_or_fetch_cached, + get_relation_data, ) diff --git a/tests/integration/test_charm_tcp.py b/tests/integration/test_charm_tcp.py index e10d2cb7..d0cb08ca 100644 --- a/tests/integration/test_charm_tcp.py +++ b/tests/integration/test_charm_tcp.py @@ -24,15 +24,6 @@ } -@pytest_asyncio.fixture -async def tcp_tester_charm(ops_test: OpsTest): - lib_source = charm_root / "lib" / "charms" / "traefik_k8s" / "v1" / "ingress_per_unit.py" - libs_folder = tcp_charm_root / "lib" / "charms" / "traefik_k8s" / "v1" - libs_folder.mkdir(parents=True, exist_ok=True) - shutil.copy(lib_source, libs_folder) - return await ops_test.build_charm(tcp_charm_root) - - def get_unit_ip(ops_test: OpsTest): proc = Popen(f"juju status -m {ops_test.model_name}".split(), stdout=PIPE) raw_status = proc.stdout.read() diff --git a/tests/integration/test_compatibility.py b/tests/integration/test_compatibility.py index 139e4e46..c3f536e6 100644 --- a/tests/integration/test_compatibility.py +++ b/tests/integration/test_compatibility.py @@ -10,18 +10,11 @@ deploy_charm_if_not_deployed, deploy_traefik_if_not_deployed, ) -from tests.integration.test_charm_ipa import ( # noqa - assert_ipa_charm_has_ingress, - ipa_tester_charm, -) -from tests.integration.test_charm_ipu import ( # noqa - assert_ipu_charm_has_ingress, - ipu_tester_charm, -) +from tests.integration.test_charm_ipa import assert_ipa_charm_has_ingress # noqa +from tests.integration.test_charm_ipu import assert_ipu_charm_has_ingress # noqa from tests.integration.test_charm_tcp import ( # noqa assert_tcp_charm_has_ingress, tcp_charm_resources, - tcp_tester_charm, ) @@ -38,18 +31,18 @@ async def safe_relate(ops_test: OpsTest, ep1, ep2): @pytest_asyncio.fixture async def tcp_ipa_deployment( - ops_test: OpsTest, traefik_charm, tcp_tester_charm, ipa_tester_charm # noqa + ops_test: OpsTest, traefik_charm, tcp_tester_charm, ipa_tester_charm # noqa ): await asyncio.gather( - deploy_traefik_if_not_deployed(ops_test, traefik_charm), - deploy_charm_if_not_deployed( - ops_test, tcp_tester_charm, "tcp-tester", resources=tcp_charm_resources - ), - deploy_charm_if_not_deployed(ops_test, ipa_tester_charm, "ipa-tester") + deploy_traefik_if_not_deployed(ops_test, traefik_charm), + deploy_charm_if_not_deployed( + ops_test, tcp_tester_charm, "tcp-tester", resources=tcp_charm_resources + ), + deploy_charm_if_not_deployed(ops_test, ipa_tester_charm, "ipa-tester"), ) await asyncio.gather( safe_relate(ops_test, "tcp-tester", "traefik-k8s"), - safe_relate(ops_test, "ipa-tester", "traefik-k8s") + safe_relate(ops_test, "ipa-tester", "traefik-k8s"), ) async with ops_test.fast_forward(): @@ -64,18 +57,18 @@ async def tcp_ipa_deployment( @pytest_asyncio.fixture async def tcp_ipu_deployment( - ops_test: OpsTest, traefik_charm, tcp_tester_charm, ipu_tester_charm # noqa + ops_test: OpsTest, traefik_charm, tcp_tester_charm, ipu_tester_charm # noqa ): await asyncio.gather( deploy_traefik_if_not_deployed(ops_test, traefik_charm), await deploy_charm_if_not_deployed( ops_test, tcp_tester_charm, "tcp-tester", resources=tcp_charm_resources ), - await deploy_charm_if_not_deployed(ops_test, ipu_tester_charm, "ipu-tester") + await deploy_charm_if_not_deployed(ops_test, ipu_tester_charm, "ipu-tester"), ) await asyncio.gather( safe_relate(ops_test, "tcp-tester", "traefik-k8s"), - safe_relate(ops_test, "ipu-tester", "traefik-k8s") + safe_relate(ops_test, "ipu-tester", "traefik-k8s"), ) async with ops_test.fast_forward(): await ops_test.model.wait_for_idle( diff --git a/tests/integration/test_traefik_deployed_after_metallb.py b/tests/integration/test_traefik_deployed_after_metallb.py index 2b362735..9b959d03 100644 --- a/tests/integration/test_traefik_deployed_after_metallb.py +++ b/tests/integration/test_traefik_deployed_after_metallb.py @@ -20,9 +20,10 @@ import pytest import yaml -from helpers import disable_metallb, enable_metallb from pytest_operator.plugin import OpsTest +from tests.integration.helpers import disable_metallb, enable_metallb + logger = logging.getLogger(__name__) diff --git a/tests/integration/test_traefik_deployed_before_metallb.py b/tests/integration/test_traefik_deployed_before_metallb.py index 2ccc02df..565efc16 100644 --- a/tests/integration/test_traefik_deployed_before_metallb.py +++ b/tests/integration/test_traefik_deployed_before_metallb.py @@ -20,9 +20,10 @@ import pytest import yaml -from tests.integration.helpers import disable_metallb, enable_metallb from pytest_operator.plugin import OpsTest +from tests.integration.helpers import disable_metallb, enable_metallb + logger = logging.getLogger(__name__) @@ -33,9 +34,9 @@ @pytest.mark.abort_on_fail -async def test_build_and_deploy(ops_test: OpsTest, traefik_charm, - ipa_tester_charm, ipu_tester_charm, - route_tester_charm): +async def test_build_and_deploy( + ops_test: OpsTest, traefik_charm, ipa_tester_charm, ipu_tester_charm, route_tester_charm +): logger.info("First, disable metallb, in case it's enabled") await disable_metallb() @@ -43,9 +44,9 @@ async def test_build_and_deploy(ops_test: OpsTest, traefik_charm, ops_test.model.deploy( traefik_charm, resources=resources, application_name="traefik", series="focal" ), - ops_test.model.deploy(ipu_tester_charm, application_name='ipu-tester', series="focal"), - ops_test.model.deploy(ipa_tester_charm, application_name='ipa-tester', series="focal"), - ops_test.model.deploy(route_tester_charm, application_name='route-tester', series="focal"), + ops_test.model.deploy(ipu_tester_charm, application_name="ipu-tester", series="focal"), + ops_test.model.deploy(ipa_tester_charm, application_name="ipa-tester", series="focal"), + ops_test.model.deploy(route_tester_charm, application_name="route-tester", series="focal"), ) await ops_test.model.wait_for_idle(timeout=600, idle_period=30) diff --git a/tests/integration/testers/route/src/charm.py b/tests/integration/testers/route/src/charm.py index 4682a63f..9ecb27bf 100755 --- a/tests/integration/testers/route/src/charm.py +++ b/tests/integration/testers/route/src/charm.py @@ -1,23 +1,23 @@ #!/usr/bin/env python3 # Copyright 2022 Canonical Ltd. # See LICENSE file for licensing details. +from charms.traefik_route_k8s.v0.traefik_route import TraefikRouteRequirer from ops.charm import CharmBase from ops.model import ActiveStatus -from charms.traefik_route_k8s.v0.traefik_route import TraefikRouteRequirer - class RouteRequirerMock(CharmBase): def __init__(self, framework): super().__init__(framework, None) self.traefik_route = TraefikRouteRequirer( - self, self.model.get_relation('traefik-route'), 'traefik_route' + self, self.model.get_relation("traefik-route"), "traefik_route" ) if self.traefik_route.is_ready(): self.traefik_route.submit_to_traefik(config={}) - self.unit.status = ActiveStatus('ready') + self.unit.status = ActiveStatus("ready") if __name__ == "__main__": from ops.main import main + main(RouteRequirerMock) From 6f256e654c88a4ec61b1bb22346996a48bd61ea6 Mon Sep 17 00:00:00 2001 From: Pietro Pasotti Date: Fri, 25 Nov 2022 14:21:57 +0100 Subject: [PATCH 04/14] lint --- tests/integration/__init__.py | 7 ------- tests/integration/conftest.py | 15 +++++++-------- tests/integration/spellbook/README.md | 2 +- tests/integration/spellbook/build_all_caches.py | 10 +++++----- tests/integration/spellbook/cache.py | 2 +- tests/integration/test_charm_ipa.py | 4 ---- tests/integration/test_charm_tcp.py | 8 +------- 7 files changed, 15 insertions(+), 33 deletions(-) delete mode 100644 tests/integration/__init__.py diff --git a/tests/integration/__init__.py b/tests/integration/__init__.py deleted file mode 100644 index 455ccc49..00000000 --- a/tests/integration/__init__.py +++ /dev/null @@ -1,7 +0,0 @@ -import os -import sys -from pathlib import Path - -charm_root = Path(__file__).parent.parent.parent -sys.path.append(str(charm_root.absolute())) -os.environ["TOX_ENV_DIR"] = str((charm_root / ".tox" / "integration").absolute()) diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py index 8f3c9c02..e10f5a9a 100644 --- a/tests/integration/conftest.py +++ b/tests/integration/conftest.py @@ -3,7 +3,6 @@ import functools import logging import os -import shutil from collections import defaultdict from dataclasses import dataclass from datetime import datetime @@ -14,7 +13,7 @@ import yaml from pytest_operator.plugin import OpsTest -from tests.integration.spellbook.cache import build_charm_or_fetch_cached +from tests.integration.spellbook.cache import spellbook_fetch charm_root = Path(__file__).parent.parent.parent trfk_meta = yaml.safe_load((charm_root / "metadata.yaml").read_text()) @@ -67,13 +66,13 @@ async def wrapper(*args, **kwargs): @pytest.fixture(scope="module") @timed_memoizer async def traefik_charm(): - return build_charm_or_fetch_cached("traefik", "./") + return spellbook_fetch("traefik", "./") @pytest.fixture(scope="module") async def ipa_tester_charm(): ipa_charm_root = (Path(__file__).parent / "testers" / "ipa").absolute() - return build_charm_or_fetch_cached( + return spellbook_fetch( "ipa-tester", ipa_charm_root, pull_libs=[Path() / "lib" / "charms" / "traefik_k8s" / "v1" / "ingress.py"], @@ -83,7 +82,7 @@ async def ipa_tester_charm(): @pytest.fixture(scope="module") async def ipu_tester_charm(): ipu_charm_root = (Path(__file__).parent / "testers" / "ipu").absolute() - return build_charm_or_fetch_cached( + return spellbook_fetch( "ipu-tester", ipu_charm_root, pull_libs=[Path() / "lib" / "charms" / "traefik_k8s" / "v1" / "ingress_per_unit.py"], @@ -91,9 +90,9 @@ async def ipu_tester_charm(): @pytest.fixture(scope="module") -async def ipu_tester_charm(): +async def tcp_tester_charm(): tcp_charm_root = (Path(__file__).parent / "testers" / "tcp").absolute() - return build_charm_or_fetch_cached( + return spellbook_fetch( "tcp-tester", tcp_charm_root, pull_libs=[Path() / "lib" / "charms" / "traefik_k8s" / "v1" / "ingress_per_unit.py"], @@ -103,7 +102,7 @@ async def ipu_tester_charm(): @pytest.fixture(scope="module") async def route_tester_charm(): route_charm_root = (Path(__file__).parent / "testers" / "route").absolute() - return build_charm_or_fetch_cached( + return spellbook_fetch( "route-tester", route_charm_root, pull_libs=[Path() / "lib" / "charms" / "traefik_route_k8s" / "v0" / "traefik_route.py"], diff --git a/tests/integration/spellbook/README.md b/tests/integration/spellbook/README.md index 55e87cf5..c1257040 100644 --- a/tests/integration/spellbook/README.md +++ b/tests/integration/spellbook/README.md @@ -1,5 +1,5 @@ # About this folder -In `conftest.py` there's a `build_charm_or_fetch_cached` function that: +In `conftest.py` there's a `spellbook_fetch` function that: - checks if a `.charm` file is present in this folder - if so, returns that package diff --git a/tests/integration/spellbook/build_all_caches.py b/tests/integration/spellbook/build_all_caches.py index b37ff73f..19091d19 100644 --- a/tests/integration/spellbook/build_all_caches.py +++ b/tests/integration/spellbook/build_all_caches.py @@ -2,16 +2,16 @@ # See LICENSE file for licensing details. from pathlib import Path -from tests.integration.spellbook.cache import build_charm_or_fetch_cached +from tests.integration.spellbook.cache import spellbook_fetch testers_root = Path(__file__).parent.parent / "testers" def main(): - build_charm_or_fetch_cached("route-tester", testers_root / "route"), - build_charm_or_fetch_cached("ipa-tester", testers_root / "ipa"), - build_charm_or_fetch_cached("ipu-tester", testers_root / "ipu"), - build_charm_or_fetch_cached("tcp-tester", testers_root / "tcp"), + spellbook_fetch("route-tester", testers_root / "route"), + spellbook_fetch("ipa-tester", testers_root / "ipa"), + spellbook_fetch("ipu-tester", testers_root / "ipu"), + spellbook_fetch("tcp-tester", testers_root / "tcp"), if __name__ == "__main__": diff --git a/tests/integration/spellbook/cache.py b/tests/integration/spellbook/cache.py index a00de0ee..1bb1a90a 100644 --- a/tests/integration/spellbook/cache.py +++ b/tests/integration/spellbook/cache.py @@ -20,7 +20,7 @@ ) -def build_charm_or_fetch_cached( +def spellbook_fetch( charm_name: str, build_root: Union[str, Path], pull_libs: List[Path] = None, diff --git a/tests/integration/test_charm_ipa.py b/tests/integration/test_charm_ipa.py index 664d7f71..5eb952a2 100644 --- a/tests/integration/test_charm_ipa.py +++ b/tests/integration/test_charm_ipa.py @@ -1,10 +1,6 @@ # Copyright 2022 Canonical Ltd. # See LICENSE file for licensing details. -import shutil -from pathlib import Path - import pytest -import pytest_asyncio import yaml from pytest_operator.plugin import OpsTest diff --git a/tests/integration/test_charm_tcp.py b/tests/integration/test_charm_tcp.py index d0cb08ca..1c95eebd 100644 --- a/tests/integration/test_charm_tcp.py +++ b/tests/integration/test_charm_tcp.py @@ -1,21 +1,15 @@ # Copyright 2022 Canonical Ltd. # See LICENSE file for licensing details. import re -import shutil import socket from pathlib import Path from subprocess import PIPE, Popen import pytest -import pytest_asyncio import yaml from pytest_operator.plugin import OpsTest -from tests.integration.conftest import ( - charm_root, - deploy_traefik_if_not_deployed, - get_relation_data, -) +from tests.integration.conftest import deploy_traefik_if_not_deployed, get_relation_data tcp_charm_root = (Path(__file__).parent / "testers" / "tcp").absolute() tcp_charm_meta = yaml.safe_load((tcp_charm_root / "metadata.yaml").read_text()) From f1dd185daec2832b5f45cc1d4a3ff4f7ebb02676 Mon Sep 17 00:00:00 2001 From: Pietro Pasotti Date: Fri, 25 Nov 2022 15:11:41 +0100 Subject: [PATCH 05/14] lintclear --- tests/integration/conftest.py | 52 +++++++----- .../integration/spellbook/build_all_caches.py | 16 +++- tests/integration/spellbook/cache.py | 83 +++++++++++++++---- tests/integration/test_compatibility.py | 4 +- 4 files changed, 109 insertions(+), 46 deletions(-) diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py index e10f5a9a..b12ffb47 100644 --- a/tests/integration/conftest.py +++ b/tests/integration/conftest.py @@ -11,15 +11,15 @@ import pytest import yaml +from juju.errors import JujuError from pytest_operator.plugin import OpsTest from tests.integration.spellbook.cache import spellbook_fetch -charm_root = Path(__file__).parent.parent.parent -trfk_meta = yaml.safe_load((charm_root / "metadata.yaml").read_text()) +trfk_root = Path(__file__).parent.parent.parent +trfk_meta = yaml.safe_load((trfk_root / "metadata.yaml").read_text()) trfk_resources = {name: val["upstream-source"] for name, val in trfk_meta["resources"].items()} - _JUJU_DATA_CACHE = {} _JUJU_KEYS = ("egress-subnets", "ingress-address", "private-address") @@ -66,15 +66,23 @@ async def wrapper(*args, **kwargs): @pytest.fixture(scope="module") @timed_memoizer async def traefik_charm(): - return spellbook_fetch("traefik", "./") + return spellbook_fetch( + trfk_root, charm_name='traefik', + hash_paths=[trfk_root / 'src', + trfk_root / 'lib', + trfk_root / 'metadata.yaml', + trfk_root / 'config.yaml', + trfk_root / 'charmcraft.yaml'] + ) + @pytest.fixture(scope="module") async def ipa_tester_charm(): ipa_charm_root = (Path(__file__).parent / "testers" / "ipa").absolute() return spellbook_fetch( - "ipa-tester", ipa_charm_root, + charm_name="ipa-tester", pull_libs=[Path() / "lib" / "charms" / "traefik_k8s" / "v1" / "ingress.py"], ) @@ -83,8 +91,8 @@ async def ipa_tester_charm(): async def ipu_tester_charm(): ipu_charm_root = (Path(__file__).parent / "testers" / "ipu").absolute() return spellbook_fetch( - "ipu-tester", ipu_charm_root, + charm_name="ipu-tester", pull_libs=[Path() / "lib" / "charms" / "traefik_k8s" / "v1" / "ingress_per_unit.py"], ) @@ -93,8 +101,8 @@ async def ipu_tester_charm(): async def tcp_tester_charm(): tcp_charm_root = (Path(__file__).parent / "testers" / "tcp").absolute() return spellbook_fetch( - "tcp-tester", tcp_charm_root, + charm_name="tcp-tester", pull_libs=[Path() / "lib" / "charms" / "traefik_k8s" / "v1" / "ingress_per_unit.py"], ) @@ -103,8 +111,8 @@ async def tcp_tester_charm(): async def route_tester_charm(): route_charm_root = (Path(__file__).parent / "testers" / "route").absolute() return spellbook_fetch( - "route-tester", route_charm_root, + charm_name="route-tester", pull_libs=[Path() / "lib" / "charms" / "traefik_route_k8s" / "v0" / "traefik_route.py"], ) @@ -169,10 +177,10 @@ def get_relation_by_endpoint(relations, local_endpoint, remote_endpoint, remote_ r for r in relations if ( - (r["endpoint"] == local_endpoint and r["related-endpoint"] == remote_endpoint) - or (r["endpoint"] == remote_endpoint and r["related-endpoint"] == local_endpoint) - ) - and remote_obj in r["related-units"] + (r["endpoint"] == local_endpoint and r["related-endpoint"] == remote_endpoint) + or (r["endpoint"] == remote_endpoint and r["related-endpoint"] == local_endpoint) + ) + and remote_obj in r["related-units"] ] if not matches: raise ValueError( @@ -199,7 +207,7 @@ class UnitRelationData: def get_content( - obj: str, other_obj, include_default_juju_keys: bool = False, model: str = None + obj: str, other_obj, include_default_juju_keys: bool = False, model: str = None ) -> UnitRelationData: """Get the content of the databag of `obj`, as seen from `other_obj`.""" unit_name, endpoint = obj.split(":") @@ -241,11 +249,11 @@ class RelationData: def get_relation_data( - *, - provider_endpoint: str, - requirer_endpoint: str, - include_default_juju_keys: bool = False, - model: str = None, + *, + provider_endpoint: str, + requirer_endpoint: str, + include_default_juju_keys: bool = False, + model: str = None, ): """Get relation databags for a juju relation. @@ -272,13 +280,13 @@ async def get_address(ops_test: OpsTest, app_name: str, unit=0): async def deploy_traefik_if_not_deployed(ops_test: OpsTest, traefik_charm): - if not ops_test.model.applications.get("traefik-k8s"): + try: await ops_test.model.deploy( traefik_charm, application_name="traefik-k8s", resources=trfk_resources, series="focal" ) - # if we're running this locally, we need to wait for "waiting" - # CI however deploys all in a single model, so traefik is active already - # if a previous test has already set it up. + except JujuError as e: + if e.message != 'cannot add application "traefik-k8s": application already exists': + raise e # block until traefik goes to... async with ops_test.fast_forward(): diff --git a/tests/integration/spellbook/build_all_caches.py b/tests/integration/spellbook/build_all_caches.py index 19091d19..24a5ad2e 100644 --- a/tests/integration/spellbook/build_all_caches.py +++ b/tests/integration/spellbook/build_all_caches.py @@ -4,14 +4,22 @@ from tests.integration.spellbook.cache import spellbook_fetch +traefik_root = Path(__file__).parent.parent.parent.parent testers_root = Path(__file__).parent.parent / "testers" def main(): - spellbook_fetch("route-tester", testers_root / "route"), - spellbook_fetch("ipa-tester", testers_root / "ipa"), - spellbook_fetch("ipu-tester", testers_root / "ipu"), - spellbook_fetch("tcp-tester", testers_root / "tcp"), + spellbook_fetch(charm_name="fockit", charm_root=traefik_root, + hash_paths=[traefik_root / 'src', + traefik_root / 'lib', + traefik_root / 'metadata.yaml', + traefik_root / 'config.yaml', + traefik_root / 'charmcraft.yaml'] + ), + spellbook_fetch(charm_name="route-tester", charm_root=testers_root / "route"), + spellbook_fetch(charm_name="ipa-tester", charm_root=testers_root / "ipa"), + spellbook_fetch(charm_name="ipu-tester", charm_root=testers_root / "ipu"), + spellbook_fetch(charm_name="tcp-tester", charm_root=testers_root / "tcp"), if __name__ == "__main__": diff --git a/tests/integration/spellbook/cache.py b/tests/integration/spellbook/cache.py index 1bb1a90a..17746df7 100644 --- a/tests/integration/spellbook/cache.py +++ b/tests/integration/spellbook/cache.py @@ -9,8 +9,12 @@ from subprocess import getoutput from typing import List, Union -charm_cache = Path(__file__).parent +import yaml +charm_cache = Path(__file__).parent / 'cache' +charm_shelf = Path(__file__).parent / 'shelf' + +COPY_TAG = 'unfrozen' # tag for charm copies USE_CACHE = True # you can flip this to true when testing locally. Do not commit! if USE_CACHE: logging.warning( @@ -20,18 +24,50 @@ ) +def _get_charm_name(metadata: Path): + if not metadata.exists() or not metadata.is_file(): + raise RuntimeError(f'invalid charm metadata file: {metadata}') + meta = yaml.safe_load(metadata.read_text()) + if 'name' not in meta: + raise RuntimeError('unable to fetch charm name from metadata') + return meta['name'] + + def spellbook_fetch( - charm_name: str, - build_root: Union[str, Path], + charm_root: Union[str, Path] = './', + charm_name: str = None, + hash_paths: List[Path] = None, pull_libs: List[Path] = None, use_cache=USE_CACHE, + cache_dir=charm_cache, + shelf_dir=charm_shelf, ): + """Cache for charmcraft pack. + + Params:: + :param charm_root: Charm tree root. + :param charm_name: Name of the charm. If not given, will default to whatever + charm_root/metadata.yaml says. + :param hash_paths: Specific directories or files to base the hashing on. + Defaults to 'charm_root/'. + :param pull_libs: Path to local charm lib files to include in the package. + :param use_cache: Flag to disable caching entirely. + :param cache_dir: Directory in which to store the cached charm files. Defaults to ./cache + :param shelf_dir: Directory in which to store the copies of the cached charm files + whose paths are returned by this function. Defaults to ./shelf + """ + # caching or not, we need to ensure the libs the charm depends on are up to date. + if use_cache: + # ensure cache dirs exist + cache_dir.mkdir(parents=True, exist_ok=True) + shelf_dir.mkdir(parents=True, exist_ok=True) + if pull_libs: for lib in pull_libs: lib_source = Path(lib) - lib_path = build_root + lib_path = charm_root for part in lib_source.parent.parts[:-5:-1]: lib_path /= part @@ -43,27 +79,35 @@ def spellbook_fetch( logging.info(f"copying {lib_source} -> {lib_path}") def do_build(): - pack_out = getoutput(f"charmcraft pack -p {build_root}") + pack_out = getoutput(f"charmcraft pack -p {charm_root}") return (Path(os.getcwd()) / pack_out.split("\n")[-1].strip()).absolute() if not use_cache: logging.info("not using cache") return do_build() - logging.info(f"hashing {build_root}") - root_md5 = getoutput(f'find {build_root} -type f -exec md5sum "{{}}" +') + logging.info(f"hashing {charm_root}") + + # todo check that if a hash path does not exist we don't blow up + hash_path = charm_root if not hash_paths else ' '.join(map(str, hash_paths)) + root_md5 = getoutput(f'find {hash_path} -type f -exec md5sum "{{}}" +') # builtins.hash() is unpredictable on str charm_tree_sum = md5(root_md5.encode("utf-8")).hexdigest() logging.info(f"hash: {charm_tree_sum}") - cached_charm_path = charm_cache / f"{charm_name}.{charm_tree_sum}.charm" - # in case someone deletes it after deploy, we make a copy. - charm_copy = (charm_cache / f"{charm_name}.unfrozen.charm").absolute() + charm_tag = charm_name or _get_charm_name(charm_root/'metadata.yaml') + + cached_charm_path = cache_dir / f"{charm_tag}.{charm_tree_sum}.charm" + + # in case someone deletes it after deploy, we make a copy and keep it in the shelf + shelved_charm_copy = (shelf_dir / f"{charm_tag}.{COPY_TAG}.charm").absolute() # clear any dirty cache dirty_cache_found = False - for fname in charm_cache.glob(f"{charm_name}.*"): + for fname in cache_dir.glob(f"{charm_tag}.*"): + if fname.name.startswith(f"{charm_tag}.{COPY_TAG}."): + continue if fname != cached_charm_path: dirty_cache_found = True logging.info(f"deleting dirty cache: {fname}") @@ -71,16 +115,19 @@ def do_build(): if cached_charm_path.exists(): tstamp = datetime.fromtimestamp(os.path.getmtime(cached_charm_path)) - logging.info(f"Found cached charm {charm_name} timestamp={tstamp}.") - shutil.copyfile(cached_charm_path, charm_copy) - return charm_copy + logging.info(f"Found cached charm {charm_tag} timestamp={tstamp}.") + shutil.copyfile(cached_charm_path, shelved_charm_copy) + return shelved_charm_copy if dirty_cache_found: - logging.info(f"Cache for {charm_name} is dirty. Repacking...") + logging.info(f"Cache for {charm_tag} is dirty. Repacking...") else: - logging.info(f"Cache not found for charm {charm_name}. Packing...") + logging.info(f"Cache not found for charm {charm_tag}. Packing...") charm = do_build() shutil.copyfile(charm, cached_charm_path) - shutil.copyfile(charm, charm_copy) - return charm_copy + shutil.copyfile(charm, shelved_charm_copy) + charm.unlink() + return shelved_charm_copy + + diff --git a/tests/integration/test_compatibility.py b/tests/integration/test_compatibility.py index c3f536e6..aa367122 100644 --- a/tests/integration/test_compatibility.py +++ b/tests/integration/test_compatibility.py @@ -61,10 +61,10 @@ async def tcp_ipu_deployment( ): await asyncio.gather( deploy_traefik_if_not_deployed(ops_test, traefik_charm), - await deploy_charm_if_not_deployed( + deploy_charm_if_not_deployed( ops_test, tcp_tester_charm, "tcp-tester", resources=tcp_charm_resources ), - await deploy_charm_if_not_deployed(ops_test, ipu_tester_charm, "ipu-tester"), + deploy_charm_if_not_deployed(ops_test, ipu_tester_charm, "ipu-tester"), ) await asyncio.gather( safe_relate(ops_test, "tcp-tester", "traefik-k8s"), From 8dae06e40d70420ad8701dd399f93b937c57a2e3 Mon Sep 17 00:00:00 2001 From: Pietro Pasotti Date: Fri, 25 Nov 2022 15:22:11 +0100 Subject: [PATCH 06/14] lint --- tests/integration/conftest.py | 44 ++++++++-------- .../integration/spellbook/build_all_caches.py | 44 ++++++++++++---- tests/integration/spellbook/cache.py | 51 +++++++++---------- 3 files changed, 80 insertions(+), 59 deletions(-) diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py index b12ffb47..8e71f70a 100644 --- a/tests/integration/conftest.py +++ b/tests/integration/conftest.py @@ -67,23 +67,25 @@ async def wrapper(*args, **kwargs): @timed_memoizer async def traefik_charm(): return spellbook_fetch( - trfk_root, charm_name='traefik', - hash_paths=[trfk_root / 'src', - trfk_root / 'lib', - trfk_root / 'metadata.yaml', - trfk_root / 'config.yaml', - trfk_root / 'charmcraft.yaml'] + trfk_root, + charm_name="traefik", + hash_paths=[ + trfk_root / "src", + trfk_root / "lib", + trfk_root / "metadata.yaml", + trfk_root / "config.yaml", + trfk_root / "charmcraft.yaml", + ], ) - @pytest.fixture(scope="module") async def ipa_tester_charm(): ipa_charm_root = (Path(__file__).parent / "testers" / "ipa").absolute() return spellbook_fetch( ipa_charm_root, charm_name="ipa-tester", - pull_libs=[Path() / "lib" / "charms" / "traefik_k8s" / "v1" / "ingress.py"], + pull_libs=[trfk_root / "lib" / "charms" / "traefik_k8s" / "v1" / "ingress.py"], ) @@ -93,7 +95,7 @@ async def ipu_tester_charm(): return spellbook_fetch( ipu_charm_root, charm_name="ipu-tester", - pull_libs=[Path() / "lib" / "charms" / "traefik_k8s" / "v1" / "ingress_per_unit.py"], + pull_libs=[trfk_root / "lib" / "charms" / "traefik_k8s" / "v1" / "ingress_per_unit.py"], ) @@ -103,7 +105,7 @@ async def tcp_tester_charm(): return spellbook_fetch( tcp_charm_root, charm_name="tcp-tester", - pull_libs=[Path() / "lib" / "charms" / "traefik_k8s" / "v1" / "ingress_per_unit.py"], + pull_libs=[trfk_root / "lib" / "charms" / "traefik_k8s" / "v1" / "ingress_per_unit.py"], ) @@ -113,7 +115,7 @@ async def route_tester_charm(): return spellbook_fetch( route_charm_root, charm_name="route-tester", - pull_libs=[Path() / "lib" / "charms" / "traefik_route_k8s" / "v0" / "traefik_route.py"], + pull_libs=[trfk_root / "lib" / "charms" / "traefik_route_k8s" / "v0" / "traefik_route.py"], ) @@ -177,10 +179,10 @@ def get_relation_by_endpoint(relations, local_endpoint, remote_endpoint, remote_ r for r in relations if ( - (r["endpoint"] == local_endpoint and r["related-endpoint"] == remote_endpoint) - or (r["endpoint"] == remote_endpoint and r["related-endpoint"] == local_endpoint) - ) - and remote_obj in r["related-units"] + (r["endpoint"] == local_endpoint and r["related-endpoint"] == remote_endpoint) + or (r["endpoint"] == remote_endpoint and r["related-endpoint"] == local_endpoint) + ) + and remote_obj in r["related-units"] ] if not matches: raise ValueError( @@ -207,7 +209,7 @@ class UnitRelationData: def get_content( - obj: str, other_obj, include_default_juju_keys: bool = False, model: str = None + obj: str, other_obj, include_default_juju_keys: bool = False, model: str = None ) -> UnitRelationData: """Get the content of the databag of `obj`, as seen from `other_obj`.""" unit_name, endpoint = obj.split(":") @@ -249,11 +251,11 @@ class RelationData: def get_relation_data( - *, - provider_endpoint: str, - requirer_endpoint: str, - include_default_juju_keys: bool = False, - model: str = None, + *, + provider_endpoint: str, + requirer_endpoint: str, + include_default_juju_keys: bool = False, + model: str = None, ): """Get relation databags for a juju relation. diff --git a/tests/integration/spellbook/build_all_caches.py b/tests/integration/spellbook/build_all_caches.py index 24a5ad2e..2f7af470 100644 --- a/tests/integration/spellbook/build_all_caches.py +++ b/tests/integration/spellbook/build_all_caches.py @@ -9,17 +9,39 @@ def main(): - spellbook_fetch(charm_name="fockit", charm_root=traefik_root, - hash_paths=[traefik_root / 'src', - traefik_root / 'lib', - traefik_root / 'metadata.yaml', - traefik_root / 'config.yaml', - traefik_root / 'charmcraft.yaml'] - ), - spellbook_fetch(charm_name="route-tester", charm_root=testers_root / "route"), - spellbook_fetch(charm_name="ipa-tester", charm_root=testers_root / "ipa"), - spellbook_fetch(charm_name="ipu-tester", charm_root=testers_root / "ipu"), - spellbook_fetch(charm_name="tcp-tester", charm_root=testers_root / "tcp"), + spellbook_fetch( + charm_name="fockit", + charm_root=traefik_root, + hash_paths=[ + traefik_root / "src", + traefik_root / "lib", + traefik_root / "metadata.yaml", + traefik_root / "config.yaml", + traefik_root / "charmcraft.yaml", + ], + ), + spellbook_fetch( + charm_name="route-tester", + charm_root=testers_root / "route", + pull_libs=[ + traefik_root / "lib" / "charms" / "traefik_route_k8s" / "v0" / "traefik_route.py" + ], + ), + spellbook_fetch( + charm_name="ipa-tester", + charm_root=testers_root / "ipa", + pull_libs=[traefik_root / "lib" / "charms" / "traefik_k8s" / "v1" / "ingress.py"], + ), + spellbook_fetch( + charm_name="ipu-tester", + charm_root=testers_root / "ipu", + pull_libs=[traefik_root / "lib" / "charms" / "traefik_k8s" / "v1" / "ingress_per_unit.py"], + ), + spellbook_fetch( + charm_name="tcp-tester", + charm_root=testers_root / "tcp", + pull_libs=[traefik_root / "lib" / "charms" / "traefik_k8s" / "v1" / "ingress_per_unit.py"], + ), if __name__ == "__main__": diff --git a/tests/integration/spellbook/cache.py b/tests/integration/spellbook/cache.py index 17746df7..6087a96b 100644 --- a/tests/integration/spellbook/cache.py +++ b/tests/integration/spellbook/cache.py @@ -11,10 +11,10 @@ import yaml -charm_cache = Path(__file__).parent / 'cache' -charm_shelf = Path(__file__).parent / 'shelf' +charm_cache = Path(__file__).parent / "cache" +charm_shelf = Path(__file__).parent / "shelf" -COPY_TAG = 'unfrozen' # tag for charm copies +COPY_TAG = "unfrozen" # tag for charm copies USE_CACHE = True # you can flip this to true when testing locally. Do not commit! if USE_CACHE: logging.warning( @@ -26,15 +26,22 @@ def _get_charm_name(metadata: Path): if not metadata.exists() or not metadata.is_file(): - raise RuntimeError(f'invalid charm metadata file: {metadata}') + raise RuntimeError(f"invalid charm metadata file: {metadata}") meta = yaml.safe_load(metadata.read_text()) - if 'name' not in meta: - raise RuntimeError('unable to fetch charm name from metadata') - return meta['name'] + if "name" not in meta: + raise RuntimeError("unable to fetch charm name from metadata") + return meta["name"] -def spellbook_fetch( - charm_root: Union[str, Path] = './', +def _get_libpath(base, source): + root = Path(base) + for part in source.parent.parts[:-6:-1]: + root /= part + return root.absolute() + + +def spellbook_fetch( # ignore: C901 + charm_root: Union[str, Path] = "./", charm_name: str = None, hash_paths: List[Path] = None, pull_libs: List[Path] = None, @@ -56,23 +63,11 @@ def spellbook_fetch( :param shelf_dir: Directory in which to store the copies of the cached charm files whose paths are returned by this function. Defaults to ./shelf """ - - # caching or not, we need to ensure the libs the charm depends on are up to date. - - if use_cache: - # ensure cache dirs exist - cache_dir.mkdir(parents=True, exist_ok=True) - shelf_dir.mkdir(parents=True, exist_ok=True) - + # caching or not, we need to ensure the libs the charm depends on are up-to-date. if pull_libs: for lib in pull_libs: lib_source = Path(lib) - lib_path = charm_root - - for part in lib_source.parent.parts[:-5:-1]: - lib_path /= part - - lib_path = lib_path.absolute() + lib_path = _get_libpath(charm_root, lib_source) # ensure it exists lib_path.mkdir(parents=True, exist_ok=True) shutil.copy(lib_source, lib_path) @@ -86,17 +81,21 @@ def do_build(): logging.info("not using cache") return do_build() + # ensure cache dirs exist + cache_dir.mkdir(parents=True, exist_ok=True) + shelf_dir.mkdir(parents=True, exist_ok=True) + logging.info(f"hashing {charm_root}") # todo check that if a hash path does not exist we don't blow up - hash_path = charm_root if not hash_paths else ' '.join(map(str, hash_paths)) + hash_path = charm_root if not hash_paths else " ".join(map(str, hash_paths)) root_md5 = getoutput(f'find {hash_path} -type f -exec md5sum "{{}}" +') # builtins.hash() is unpredictable on str charm_tree_sum = md5(root_md5.encode("utf-8")).hexdigest() logging.info(f"hash: {charm_tree_sum}") - charm_tag = charm_name or _get_charm_name(charm_root/'metadata.yaml') + charm_tag = charm_name or _get_charm_name(charm_root / "metadata.yaml") cached_charm_path = cache_dir / f"{charm_tag}.{charm_tree_sum}.charm" @@ -129,5 +128,3 @@ def do_build(): shutil.copyfile(charm, shelved_charm_copy) charm.unlink() return shelved_charm_copy - - From d84a29c8b64210a835a909121c173dc22c9e6ded Mon Sep 17 00:00:00 2001 From: Pietro Pasotti Date: Fri, 25 Nov 2022 15:27:19 +0100 Subject: [PATCH 07/14] fixed libpath --- .../integration/spellbook/build_all_caches.py | 34 +++++++++---------- tests/integration/spellbook/cache.py | 3 +- 2 files changed, 19 insertions(+), 18 deletions(-) diff --git a/tests/integration/spellbook/build_all_caches.py b/tests/integration/spellbook/build_all_caches.py index 2f7af470..c75e6af8 100644 --- a/tests/integration/spellbook/build_all_caches.py +++ b/tests/integration/spellbook/build_all_caches.py @@ -19,29 +19,29 @@ def main(): traefik_root / "config.yaml", traefik_root / "charmcraft.yaml", ], - ), + ) spellbook_fetch( charm_name="route-tester", charm_root=testers_root / "route", pull_libs=[ traefik_root / "lib" / "charms" / "traefik_route_k8s" / "v0" / "traefik_route.py" ], - ), - spellbook_fetch( - charm_name="ipa-tester", - charm_root=testers_root / "ipa", - pull_libs=[traefik_root / "lib" / "charms" / "traefik_k8s" / "v1" / "ingress.py"], - ), - spellbook_fetch( - charm_name="ipu-tester", - charm_root=testers_root / "ipu", - pull_libs=[traefik_root / "lib" / "charms" / "traefik_k8s" / "v1" / "ingress_per_unit.py"], - ), - spellbook_fetch( - charm_name="tcp-tester", - charm_root=testers_root / "tcp", - pull_libs=[traefik_root / "lib" / "charms" / "traefik_k8s" / "v1" / "ingress_per_unit.py"], - ), + ) + # spellbook_fetch( + # charm_name="ipa-tester", + # charm_root=testers_root / "ipa", + # pull_libs=[traefik_root / "lib" / "charms" / "traefik_k8s" / "v1" / "ingress.py"], + # ) + # spellbook_fetch( + # charm_name="ipu-tester", + # charm_root=testers_root / "ipu", + # pull_libs=[traefik_root / "lib" / "charms" / "traefik_k8s" / "v1" / "ingress_per_unit.py"], + # ) + # spellbook_fetch( + # charm_name="tcp-tester", + # charm_root=testers_root / "tcp", + # pull_libs=[traefik_root / "lib" / "charms" / "traefik_k8s" / "v1" / "ingress_per_unit.py"], + # ) if __name__ == "__main__": diff --git a/tests/integration/spellbook/cache.py b/tests/integration/spellbook/cache.py index 6087a96b..0448e2c3 100644 --- a/tests/integration/spellbook/cache.py +++ b/tests/integration/spellbook/cache.py @@ -35,7 +35,7 @@ def _get_charm_name(metadata: Path): def _get_libpath(base, source): root = Path(base) - for part in source.parent.parts[:-6:-1]: + for part in source.parent.parts[-4:]: root /= part return root.absolute() @@ -74,6 +74,7 @@ def spellbook_fetch( # ignore: C901 logging.info(f"copying {lib_source} -> {lib_path}") def do_build(): + logging.info(f'building {charm_root}') pack_out = getoutput(f"charmcraft pack -p {charm_root}") return (Path(os.getcwd()) / pack_out.split("\n")[-1].strip()).absolute() From b4fb9fa16f2aa435d3a2a3e4a75639097b2bb797 Mon Sep 17 00:00:00 2001 From: Pietro Pasotti Date: Fri, 25 Nov 2022 15:29:42 +0100 Subject: [PATCH 08/14] lint --- .../integration/spellbook/build_all_caches.py | 30 +++++++++---------- tests/integration/spellbook/cache.py | 2 +- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/tests/integration/spellbook/build_all_caches.py b/tests/integration/spellbook/build_all_caches.py index c75e6af8..7553184c 100644 --- a/tests/integration/spellbook/build_all_caches.py +++ b/tests/integration/spellbook/build_all_caches.py @@ -27,21 +27,21 @@ def main(): traefik_root / "lib" / "charms" / "traefik_route_k8s" / "v0" / "traefik_route.py" ], ) - # spellbook_fetch( - # charm_name="ipa-tester", - # charm_root=testers_root / "ipa", - # pull_libs=[traefik_root / "lib" / "charms" / "traefik_k8s" / "v1" / "ingress.py"], - # ) - # spellbook_fetch( - # charm_name="ipu-tester", - # charm_root=testers_root / "ipu", - # pull_libs=[traefik_root / "lib" / "charms" / "traefik_k8s" / "v1" / "ingress_per_unit.py"], - # ) - # spellbook_fetch( - # charm_name="tcp-tester", - # charm_root=testers_root / "tcp", - # pull_libs=[traefik_root / "lib" / "charms" / "traefik_k8s" / "v1" / "ingress_per_unit.py"], - # ) + spellbook_fetch( + charm_name="ipa-tester", + charm_root=testers_root / "ipa", + pull_libs=[traefik_root / "lib" / "charms" / "traefik_k8s" / "v1" / "ingress.py"], + ) + spellbook_fetch( + charm_name="ipu-tester", + charm_root=testers_root / "ipu", + pull_libs=[traefik_root / "lib" / "charms" / "traefik_k8s" / "v1" / "ingress_per_unit.py"], + ) + spellbook_fetch( + charm_name="tcp-tester", + charm_root=testers_root / "tcp", + pull_libs=[traefik_root / "lib" / "charms" / "traefik_k8s" / "v1" / "ingress_per_unit.py"], + ) if __name__ == "__main__": diff --git a/tests/integration/spellbook/cache.py b/tests/integration/spellbook/cache.py index 0448e2c3..b8ac8faf 100644 --- a/tests/integration/spellbook/cache.py +++ b/tests/integration/spellbook/cache.py @@ -74,7 +74,7 @@ def spellbook_fetch( # ignore: C901 logging.info(f"copying {lib_source} -> {lib_path}") def do_build(): - logging.info(f'building {charm_root}') + logging.info(f"building {charm_root}") pack_out = getoutput(f"charmcraft pack -p {charm_root}") return (Path(os.getcwd()) / pack_out.split("\n")[-1].strip()).absolute() From dda543993ba28488111f980eb7d430c612135728 Mon Sep 17 00:00:00 2001 From: Pietro Pasotti Date: Mon, 28 Nov 2022 10:48:20 +0100 Subject: [PATCH 09/14] remove early active assert --- tests/integration/conftest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py index 8e71f70a..598599dc 100644 --- a/tests/integration/conftest.py +++ b/tests/integration/conftest.py @@ -292,7 +292,7 @@ async def deploy_traefik_if_not_deployed(ops_test: OpsTest, traefik_charm): # block until traefik goes to... async with ops_test.fast_forward(): - await ops_test.model.wait_for_idle(["traefik-k8s"], status="active", timeout=1000) + await ops_test.model.wait_for_idle(["traefik-k8s"], timeout=1000) # we set the external hostname to traefik-k8s's own ip traefik_address = await get_address(ops_test, "traefik-k8s") From 74d1a5d7c9df1b1ad5eb32e04568d5d4837a5544 Mon Sep 17 00:00:00 2001 From: Pietro Pasotti Date: Mon, 28 Nov 2022 14:54:34 +0100 Subject: [PATCH 10/14] attempt fix juju error catch --- tests/integration/conftest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py index 598599dc..20ff43db 100644 --- a/tests/integration/conftest.py +++ b/tests/integration/conftest.py @@ -287,7 +287,7 @@ async def deploy_traefik_if_not_deployed(ops_test: OpsTest, traefik_charm): traefik_charm, application_name="traefik-k8s", resources=trfk_resources, series="focal" ) except JujuError as e: - if e.message != 'cannot add application "traefik-k8s": application already exists': + if 'cannot add application "traefik-k8s": application already exists' not in str(e): raise e # block until traefik goes to... From a9d6ff19f5012a4f6d6aad1195f4f0a78e570621 Mon Sep 17 00:00:00 2001 From: Pietro Pasotti Date: Tue, 29 Nov 2022 10:40:16 +0100 Subject: [PATCH 11/14] specify endpoints on safe_relate calls --- tests/integration/test_compatibility.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/tests/integration/test_compatibility.py b/tests/integration/test_compatibility.py index aa367122..ad6a0ec0 100644 --- a/tests/integration/test_compatibility.py +++ b/tests/integration/test_compatibility.py @@ -1,6 +1,7 @@ # Copyright 2022 Canonical Ltd. # See LICENSE file for licensing details. import asyncio +import logging import juju.errors import pytest_asyncio @@ -24,8 +25,9 @@ async def safe_relate(ops_test: OpsTest, ep1, ep2): # are already related. try: await ops_test.model.add_relation(ep1, ep2) - except juju.errors.JujuAPIError: + except juju.errors.JujuAPIError as e: # relation already exists? skip + logging.error(e) pass @@ -41,8 +43,8 @@ async def tcp_ipa_deployment( deploy_charm_if_not_deployed(ops_test, ipa_tester_charm, "ipa-tester"), ) await asyncio.gather( - safe_relate(ops_test, "tcp-tester", "traefik-k8s"), - safe_relate(ops_test, "ipa-tester", "traefik-k8s"), + safe_relate(ops_test, "tcp-tester:ingress-per-unit", "traefik-k8s:ingress-per-unit"), + safe_relate(ops_test, "ipa-tester:ingress", "traefik-k8s:ingress"), ) async with ops_test.fast_forward(): @@ -67,8 +69,8 @@ async def tcp_ipu_deployment( deploy_charm_if_not_deployed(ops_test, ipu_tester_charm, "ipu-tester"), ) await asyncio.gather( - safe_relate(ops_test, "tcp-tester", "traefik-k8s"), - safe_relate(ops_test, "ipu-tester", "traefik-k8s"), + safe_relate(ops_test, "tcp-tester:ingress-per-unit", "traefik-k8s:ingress-per-unit"), + safe_relate(ops_test, "ipu-tester:ingress", "traefik-k8s:ingress"), ) async with ops_test.fast_forward(): await ops_test.model.wait_for_idle( From 6909877dbacea9ef2e539dc8b0431f8984f7aa34 Mon Sep 17 00:00:00 2001 From: Pietro Pasotti Date: Tue, 29 Nov 2022 12:39:45 +0100 Subject: [PATCH 12/14] fixed ipu endpoint --- tests/integration/test_compatibility.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/integration/test_compatibility.py b/tests/integration/test_compatibility.py index ad6a0ec0..8fb8286d 100644 --- a/tests/integration/test_compatibility.py +++ b/tests/integration/test_compatibility.py @@ -70,7 +70,7 @@ async def tcp_ipu_deployment( ) await asyncio.gather( safe_relate(ops_test, "tcp-tester:ingress-per-unit", "traefik-k8s:ingress-per-unit"), - safe_relate(ops_test, "ipu-tester:ingress", "traefik-k8s:ingress"), + safe_relate(ops_test, "ipu-tester:ingress-per-unit", "traefik-k8s:ingress-per-unit"), ) async with ops_test.fast_forward(): await ops_test.model.wait_for_idle( From 88fc3bad746632b3c056a9e774fc2b8164aa8eda Mon Sep 17 00:00:00 2001 From: Pietro Pasotti Date: Tue, 29 Nov 2022 15:00:13 +0100 Subject: [PATCH 13/14] split off ipa and ipu compat tests --- tests/integration/conftest.py | 13 ++++++ .../integration/test_tcp_ipa_compatibility.py | 45 +++++++++++++++++++ ...ility.py => test_tcp_ipu_compatibility.py} | 44 +----------------- 3 files changed, 59 insertions(+), 43 deletions(-) create mode 100644 tests/integration/test_tcp_ipa_compatibility.py rename tests/integration/{test_compatibility.py => test_tcp_ipu_compatibility.py} (52%) diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py index 20ff43db..8daf12f5 100644 --- a/tests/integration/conftest.py +++ b/tests/integration/conftest.py @@ -9,6 +9,7 @@ from pathlib import Path from subprocess import PIPE, Popen +import juju import pytest import yaml from juju.errors import JujuError @@ -316,3 +317,15 @@ async def deploy_charm_if_not_deployed(ops_test: OpsTest, charm, app_name: str, # if we're running this locally, we need to wait for "waiting" # CI however deploys all in a single model, so traefik is active already. await ops_test.model.wait_for_idle([app_name], status="active", timeout=1000) + + +async def safe_relate(ops_test: OpsTest, ep1, ep2): + # in pytest-operator CI, we deploy all tests in the same model. + # Therefore, it might be that by the time we run this module, the two endpoints + # are already related. + try: + await ops_test.model.add_relation(ep1, ep2) + except juju.errors.JujuAPIError as e: + # relation already exists? skip + logging.error(e) + pass diff --git a/tests/integration/test_tcp_ipa_compatibility.py b/tests/integration/test_tcp_ipa_compatibility.py new file mode 100644 index 00000000..705bc083 --- /dev/null +++ b/tests/integration/test_tcp_ipa_compatibility.py @@ -0,0 +1,45 @@ +# Copyright 2022 Canonical Ltd. +# See LICENSE file for licensing details. +import asyncio + +import pytest_asyncio +from pytest_operator.plugin import OpsTest + +from tests.integration.conftest import ( + deploy_charm_if_not_deployed, + deploy_traefik_if_not_deployed, + safe_relate, +) +from tests.integration.test_charm_ipa import assert_ipa_charm_has_ingress # noqa +from tests.integration.test_charm_ipu import assert_ipu_charm_has_ingress # noqa +from tests.integration.test_charm_tcp import ( # noqa + assert_tcp_charm_has_ingress, + tcp_charm_resources, +) + + +@pytest_asyncio.fixture +async def tcp_ipa_deployment( + ops_test: OpsTest, traefik_charm, tcp_tester_charm, ipa_tester_charm # noqa +): + await asyncio.gather( + deploy_traefik_if_not_deployed(ops_test, traefik_charm), + deploy_charm_if_not_deployed( + ops_test, tcp_tester_charm, "tcp-tester", resources=tcp_charm_resources + ), + deploy_charm_if_not_deployed(ops_test, ipa_tester_charm, "ipa-tester"), + ) + await asyncio.gather( + safe_relate(ops_test, "tcp-tester:ingress-per-unit", "traefik-k8s:ingress-per-unit"), + safe_relate(ops_test, "ipa-tester:ingress", "traefik-k8s:ingress"), + ) + + async with ops_test.fast_forward(): + await ops_test.model.wait_for_idle( + ["traefik-k8s", "tcp-tester", "ipa-tester"], status="active", timeout=1000 + ) + + +async def test_tcp_ipa_compatibility(ops_test, tcp_ipa_deployment): + assert_tcp_charm_has_ingress(ops_test) + assert_ipa_charm_has_ingress(ops_test) diff --git a/tests/integration/test_compatibility.py b/tests/integration/test_tcp_ipu_compatibility.py similarity index 52% rename from tests/integration/test_compatibility.py rename to tests/integration/test_tcp_ipu_compatibility.py index 8fb8286d..66ad2da3 100644 --- a/tests/integration/test_compatibility.py +++ b/tests/integration/test_tcp_ipu_compatibility.py @@ -10,6 +10,7 @@ from tests.integration.conftest import ( deploy_charm_if_not_deployed, deploy_traefik_if_not_deployed, + safe_relate, ) from tests.integration.test_charm_ipa import assert_ipa_charm_has_ingress # noqa from tests.integration.test_charm_ipu import assert_ipu_charm_has_ingress # noqa @@ -19,44 +20,6 @@ ) -async def safe_relate(ops_test: OpsTest, ep1, ep2): - # in pytest-operator CI, we deploy all tests in the same model. - # Therefore, it might be that by the time we run this module, the two endpoints - # are already related. - try: - await ops_test.model.add_relation(ep1, ep2) - except juju.errors.JujuAPIError as e: - # relation already exists? skip - logging.error(e) - pass - - -@pytest_asyncio.fixture -async def tcp_ipa_deployment( - ops_test: OpsTest, traefik_charm, tcp_tester_charm, ipa_tester_charm # noqa -): - await asyncio.gather( - deploy_traefik_if_not_deployed(ops_test, traefik_charm), - deploy_charm_if_not_deployed( - ops_test, tcp_tester_charm, "tcp-tester", resources=tcp_charm_resources - ), - deploy_charm_if_not_deployed(ops_test, ipa_tester_charm, "ipa-tester"), - ) - await asyncio.gather( - safe_relate(ops_test, "tcp-tester:ingress-per-unit", "traefik-k8s:ingress-per-unit"), - safe_relate(ops_test, "ipa-tester:ingress", "traefik-k8s:ingress"), - ) - - async with ops_test.fast_forward(): - await ops_test.model.wait_for_idle( - ["traefik-k8s", "tcp-tester", "ipa-tester"], status="active", timeout=1000 - ) - - yield - await ops_test.model.applications["tcp-tester"].remove() - await ops_test.model.applications["ipa-tester"].remove() - - @pytest_asyncio.fixture async def tcp_ipu_deployment( ops_test: OpsTest, traefik_charm, tcp_tester_charm, ipu_tester_charm # noqa @@ -84,8 +47,3 @@ async def tcp_ipu_deployment( async def test_tcp_ipu_compatibility(ops_test, tcp_ipu_deployment): assert_tcp_charm_has_ingress(ops_test) assert_ipu_charm_has_ingress(ops_test) - - -async def test_tcp_ipa_compatibility(ops_test, tcp_ipa_deployment): - assert_tcp_charm_has_ingress(ops_test) - assert_ipa_charm_has_ingress(ops_test) From d29183fa57df10ec3bd5ea93e43a448489c494ad Mon Sep 17 00:00:00 2001 From: Pietro Pasotti Date: Tue, 29 Nov 2022 15:41:47 +0100 Subject: [PATCH 14/14] lint --- tests/integration/test_tcp_ipu_compatibility.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/integration/test_tcp_ipu_compatibility.py b/tests/integration/test_tcp_ipu_compatibility.py index 66ad2da3..19f9fe14 100644 --- a/tests/integration/test_tcp_ipu_compatibility.py +++ b/tests/integration/test_tcp_ipu_compatibility.py @@ -1,9 +1,7 @@ # Copyright 2022 Canonical Ltd. # See LICENSE file for licensing details. import asyncio -import logging -import juju.errors import pytest_asyncio from pytest_operator.plugin import OpsTest