From c9ad12a44dad721c1198d22122332c661b4318e1 Mon Sep 17 00:00:00 2001 From: Mia Altieri Date: Wed, 1 Nov 2023 10:58:53 +0000 Subject: [PATCH 1/4] test unpermitted shard relations --- tests/integration/sharding_tests/conftest.py | 21 +++++ .../sharding_tests/test_sharding_relations.py | 84 +++++++++++++++++++ tox.ini | 15 ++++ 3 files changed, 120 insertions(+) create mode 100644 tests/integration/sharding_tests/conftest.py create mode 100644 tests/integration/sharding_tests/test_sharding_relations.py diff --git a/tests/integration/sharding_tests/conftest.py b/tests/integration/sharding_tests/conftest.py new file mode 100644 index 000000000..d91962292 --- /dev/null +++ b/tests/integration/sharding_tests/conftest.py @@ -0,0 +1,21 @@ +#!/usr/bin/env python3 +# Copyright 2023 Canonical Ltd. +# See LICENSE file for licensing details. + +import pytest +from pytest_operator.plugin import OpsTest + + +@pytest.fixture(scope="module") +async def application_charm(ops_test: OpsTest): + """Build the application charm.""" + charm_path = "tests/integration/relation_tests/new_relations/application-charm" + charm = await ops_test.build_charm(charm_path) + return charm + + +@pytest.fixture(scope="module") +async def database_charm(ops_test: OpsTest): + """Build the database charm.""" + charm = await ops_test.build_charm(".") + return charm diff --git a/tests/integration/sharding_tests/test_sharding_relations.py b/tests/integration/sharding_tests/test_sharding_relations.py new file mode 100644 index 000000000..6215fb34d --- /dev/null +++ b/tests/integration/sharding_tests/test_sharding_relations.py @@ -0,0 +1,84 @@ +#!/usr/bin/env python3 +# Copyright 2023 Canonical Ltd. +# See LICENSE file for licensing details. +import pytest +from pytest_operator.plugin import OpsTest +from juju.errors import JujuAPIError + +SHARD_ONE_APP_NAME = "shard-one" +CONFIG_SERVER_ONE_APP_NAME = "config-server-one" +CONFIG_SERVER_TWO_APP_NAME = "config-server-two" +APP_CHARM_NAME = "application" + +CONFIG_SERVER_REL_NAME = "config-server" +SHARD_REL_NAME = "sharding" +DATABASE_REL_NAME = "first-database" + +# for now we have a large timeout due to the slow drainage of the `config.system.sessions` +# collection. More info here: +# https://stackoverflow.com/questions/77364840/mongodb-slow-chunk-migration-for-collection-config-system-sessions-with-remov +TIMEOUT = 30 * 60 + + +@pytest.mark.abort_on_fail +async def test_build_and_deploy(ops_test: OpsTest, application_charm, database_charm) -> None: + """Build and deploy a sharded cluster.""" + await ops_test.model.deploy( + database_charm, + config={"role": "config-server"}, + application_name=CONFIG_SERVER_ONE_APP_NAME, + ) + await ops_test.model.deploy( + database_charm, + config={"role": "config-server"}, + application_name=CONFIG_SERVER_TWO_APP_NAME, + ) + await ops_test.model.deploy( + database_charm, num_units=1, config={"role": "shard"}, application_name=SHARD_ONE_APP_NAME + ) + await ops_test.model.deploy(application_charm, application_name=APP_CHARM_NAME) + + async with ops_test.fast_forward(): + await ops_test.model.wait_for_idle( + apps=[ + CONFIG_SERVER_ONE_APP_NAME, + CONFIG_SERVER_TWO_APP_NAME, + SHARD_ONE_APP_NAME, + ], + idle_period=20, + raise_on_blocked=False, + timeout=TIMEOUT, + ) + + +async def test_only_one_config_server_relation(ops_test: OpsTest) -> None: + """Verify that a shard can only be related to one config server.""" + await ops_test.model.integrate( + f"{SHARD_ONE_APP_NAME}:{SHARD_REL_NAME}", + f"{CONFIG_SERVER_ONE_APP_NAME}:{CONFIG_SERVER_REL_NAME}", + ) + + multiple_config_server_rels_allowed = True + try: + await ops_test.model.integrate( + f"{SHARD_ONE_APP_NAME}:{SHARD_REL_NAME}", + f"{CONFIG_SERVER_TWO_APP_NAME}:{CONFIG_SERVER_REL_NAME}", + ) + except JujuAPIError as e: + if e.error_code == "quota limit exceeded": + multiple_config_server_rels_allowed = False + + assert not multiple_config_server_rels_allowed, "Shard can relate to multiple config servers." + + +async def test_cannot_use_db_relation(ops_test: OpsTest) -> None: + """Verify that a shard cannot use the DB relation.""" + await ops_test.model.integrate( + f"{APP_CHARM_NAME}:{DATABASE_REL_NAME}", + SHARD_ONE_APP_NAME, + ) + + shard_unit = ops_test.model.applications[SHARD_ONE_APP_NAME].units[0] + assert ( + shard_unit.workload_status_message == "Sharding roles do not support database interface." + ), "Shard cannot be related using the database relation" diff --git a/tox.ini b/tox.ini index 49f16d8bf..edaae3a4a 100644 --- a/tox.ini +++ b/tox.ini @@ -192,6 +192,21 @@ deps = commands = pytest -v --tb native --log-cli-level=INFO -s --durations=0 {posargs} {[vars]tests_path}/integration/sharding_tests/test_sharding.py +[testenv:sharding-relation-integration] +description = Run sharding integration tests +pass_env = + {[testenv]pass_env} + CI +deps = + pytest + juju==3.2.0.1 + pytest-mock + pytest-operator + git+https://github.com/canonical/data-platform-workflows@v5.1.2\#subdirectory=python/pytest_plugins/pytest_operator_cache + -r {tox_root}/requirements.txt +commands = + pytest -v --tb native --log-cli-level=INFO -s --durations=0 {posargs} {[vars]tests_path}/integration/sharding_tests/test_sharding_relations.py + [testenv:integration] description = Run all integration tests From 8d00835951244ce220a7137315862a2212a4096c Mon Sep 17 00:00:00 2001 From: Mia Altieri Date: Wed, 1 Nov 2023 17:28:15 +0000 Subject: [PATCH 2/4] add legacy tests --- .../dummy_legacy_app/charmcraft.yaml | 11 +++ .../dummy_legacy_app/metadata.yaml | 14 ++++ .../dummy_legacy_app/requirements.txt | 1 + .../integration/dummy_legacy_app/src/charm.py | 38 +++++++++ tests/integration/sharding_tests/conftest.py | 12 +++ .../sharding_tests/test_sharding_relations.py | 81 ++++++++++++++++--- 6 files changed, 144 insertions(+), 13 deletions(-) create mode 100644 tests/integration/dummy_legacy_app/charmcraft.yaml create mode 100644 tests/integration/dummy_legacy_app/metadata.yaml create mode 100644 tests/integration/dummy_legacy_app/requirements.txt create mode 100755 tests/integration/dummy_legacy_app/src/charm.py diff --git a/tests/integration/dummy_legacy_app/charmcraft.yaml b/tests/integration/dummy_legacy_app/charmcraft.yaml new file mode 100644 index 000000000..d12b960e6 --- /dev/null +++ b/tests/integration/dummy_legacy_app/charmcraft.yaml @@ -0,0 +1,11 @@ +# Copyright 2023 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" diff --git a/tests/integration/dummy_legacy_app/metadata.yaml b/tests/integration/dummy_legacy_app/metadata.yaml new file mode 100644 index 000000000..0a5ca7db6 --- /dev/null +++ b/tests/integration/dummy_legacy_app/metadata.yaml @@ -0,0 +1,14 @@ +# Copyright 2023 Canonical Ltd. +# See LICENSE file for licensing details. +name: legacy-application +description: | + Dummy legacy application charm used in integration tests. +summary: | + Dummy legacy application charm meant only to be used in non-legacy integration tests. Legacy + integration tests should use a production legacy application. +series: + - focal + +requires: + obsolete: + interface: mongodb diff --git a/tests/integration/dummy_legacy_app/requirements.txt b/tests/integration/dummy_legacy_app/requirements.txt new file mode 100644 index 000000000..96faf889a --- /dev/null +++ b/tests/integration/dummy_legacy_app/requirements.txt @@ -0,0 +1 @@ +ops >= 1.4.0 diff --git a/tests/integration/dummy_legacy_app/src/charm.py b/tests/integration/dummy_legacy_app/src/charm.py new file mode 100755 index 000000000..2c3e5dca8 --- /dev/null +++ b/tests/integration/dummy_legacy_app/src/charm.py @@ -0,0 +1,38 @@ +#!/usr/bin/env python3 +# Copyright 2023 Canonical Ltd. +# See LICENSE file for licensing details. + +"""Application charm that connects to database charms. + +This charm is meant to be used only for testing +of the libraries in this repository. +""" + +import logging + +from ops.charm import CharmBase +from ops.main import main +from ops.model import ActiveStatus + +logger = logging.getLogger(__name__) + +# Extra roles that this application needs when interacting with the database. +EXTRA_USER_ROLES = "admin" + + +class ApplicationCharm(CharmBase): + """Application charm that connects to database charms.""" + + def __init__(self, *args): + super().__init__(*args) + + # Default charm events. + self.framework.observe(self.on.start, self._on_start) + + def _on_start(self, _) -> None: + """Only sets an Active status.""" + self.unit.status = ActiveStatus() + + +if __name__ == "__main__": + main(ApplicationCharm) diff --git a/tests/integration/sharding_tests/conftest.py b/tests/integration/sharding_tests/conftest.py index d91962292..8bef5a710 100644 --- a/tests/integration/sharding_tests/conftest.py +++ b/tests/integration/sharding_tests/conftest.py @@ -6,6 +6,18 @@ from pytest_operator.plugin import OpsTest +@pytest.fixture(scope="module") +async def legacy_charm(ops_test: OpsTest): + """Build an application charm that uses the legacy interface. + + Note: this should only be used outside of the legacy integration tests, as those tests should + test a real legacy application. + """ + charm_path = "tests/integration/dummy_legacy_app" + charm = await ops_test.build_charm(charm_path) + return charm + + @pytest.fixture(scope="module") async def application_charm(ops_test: OpsTest): """Build the application charm.""" diff --git a/tests/integration/sharding_tests/test_sharding_relations.py b/tests/integration/sharding_tests/test_sharding_relations.py index 6215fb34d..7b7660adf 100644 --- a/tests/integration/sharding_tests/test_sharding_relations.py +++ b/tests/integration/sharding_tests/test_sharding_relations.py @@ -2,17 +2,19 @@ # Copyright 2023 Canonical Ltd. # See LICENSE file for licensing details. import pytest -from pytest_operator.plugin import OpsTest from juju.errors import JujuAPIError +from pytest_operator.plugin import OpsTest SHARD_ONE_APP_NAME = "shard-one" CONFIG_SERVER_ONE_APP_NAME = "config-server-one" CONFIG_SERVER_TWO_APP_NAME = "config-server-two" APP_CHARM_NAME = "application" +LEGACY_APP_CHARM_NAME = "legacy-application" CONFIG_SERVER_REL_NAME = "config-server" SHARD_REL_NAME = "sharding" DATABASE_REL_NAME = "first-database" +LEGACY_RELATION_NAME = "obsolete" # for now we have a large timeout due to the slow drainage of the `config.system.sessions` # collection. More info here: @@ -21,7 +23,9 @@ @pytest.mark.abort_on_fail -async def test_build_and_deploy(ops_test: OpsTest, application_charm, database_charm) -> None: +async def test_build_and_deploy( + ops_test: OpsTest, application_charm, legacy_charm, database_charm +) -> None: """Build and deploy a sharded cluster.""" await ops_test.model.deploy( database_charm, @@ -37,18 +41,18 @@ async def test_build_and_deploy(ops_test: OpsTest, application_charm, database_c database_charm, num_units=1, config={"role": "shard"}, application_name=SHARD_ONE_APP_NAME ) await ops_test.model.deploy(application_charm, application_name=APP_CHARM_NAME) + await ops_test.model.deploy(legacy_charm, application_name=LEGACY_APP_CHARM_NAME) - async with ops_test.fast_forward(): - await ops_test.model.wait_for_idle( - apps=[ - CONFIG_SERVER_ONE_APP_NAME, - CONFIG_SERVER_TWO_APP_NAME, - SHARD_ONE_APP_NAME, - ], - idle_period=20, - raise_on_blocked=False, - timeout=TIMEOUT, - ) + await ops_test.model.wait_for_idle( + apps=[ + CONFIG_SERVER_ONE_APP_NAME, + CONFIG_SERVER_TWO_APP_NAME, + SHARD_ONE_APP_NAME, + ], + idle_period=20, + raise_on_blocked=False, + timeout=TIMEOUT, + ) async def test_only_one_config_server_relation(ops_test: OpsTest) -> None: @@ -78,7 +82,58 @@ async def test_cannot_use_db_relation(ops_test: OpsTest) -> None: SHARD_ONE_APP_NAME, ) + await ops_test.model.wait_for_idle( + apps=[SHARD_ONE_APP_NAME], + idle_period=20, + raise_on_blocked=False, + timeout=TIMEOUT, + ) + shard_unit = ops_test.model.applications[SHARD_ONE_APP_NAME].units[0] assert ( shard_unit.workload_status_message == "Sharding roles do not support database interface." ), "Shard cannot be related using the database relation" + + await ops_test.model.applications[SHARD_ONE_APP_NAME].remove_relation( + f"{APP_CHARM_NAME}:{DATABASE_REL_NAME}", + SHARD_ONE_APP_NAME, + ) + + await ops_test.model.wait_for_idle( + apps=[SHARD_ONE_APP_NAME], + idle_period=20, + raise_on_blocked=False, + timeout=TIMEOUT, + ) + + +async def test_cannot_use_legacy_db_relation(ops_test: OpsTest) -> None: + """Verify that a shard cannot use the DB relation.""" + await ops_test.model.integrate( + LEGACY_APP_CHARM_NAME, + SHARD_ONE_APP_NAME, + ) + + await ops_test.model.wait_for_idle( + apps=[SHARD_ONE_APP_NAME], + idle_period=20, + raise_on_blocked=False, + timeout=TIMEOUT, + ) + + shard_unit = ops_test.model.applications[SHARD_ONE_APP_NAME].units[0] + assert ( + shard_unit.workload_status_message == "Sharding roles do not support obsolete interface." + ), "Shard cannot be related using the mongodb relation" + + await ops_test.model.applications[SHARD_ONE_APP_NAME].remove_relation( + f"{SHARD_ONE_APP_NAME}:{LEGACY_RELATION_NAME}", + f"{LEGACY_APP_CHARM_NAME}:{LEGACY_RELATION_NAME}", + ) + + await ops_test.model.wait_for_idle( + apps=[SHARD_ONE_APP_NAME], + idle_period=20, + raise_on_blocked=False, + timeout=TIMEOUT, + ) From e46b31718b91ccc61ff54780c4a193cce3fba99f Mon Sep 17 00:00:00 2001 From: Mia Altieri Date: Wed, 1 Nov 2023 17:34:48 +0000 Subject: [PATCH 3/4] update ci --- .github/workflows/ci.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 3c0bdab19..e42a302a1 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -82,6 +82,7 @@ jobs: - backup-integration - metric-integration - sharding-integration + - sharding-relation-integration name: ${{ matrix.tox-environments }} needs: - lint From 62a461437a5cdac092177c743808e31ffdb66e5a Mon Sep 17 00:00:00 2001 From: Mia Altieri Date: Thu, 2 Nov 2023 10:12:32 +0000 Subject: [PATCH 4/4] pr feedback round 1 --- .../sharding_tests/test_sharding_relations.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/tests/integration/sharding_tests/test_sharding_relations.py b/tests/integration/sharding_tests/test_sharding_relations.py index 7b7660adf..36288c5f8 100644 --- a/tests/integration/sharding_tests/test_sharding_relations.py +++ b/tests/integration/sharding_tests/test_sharding_relations.py @@ -16,6 +16,7 @@ DATABASE_REL_NAME = "first-database" LEGACY_RELATION_NAME = "obsolete" +RELATION_LIMIT_MESSAGE = 'cannot add relation "shard-one:sharding config-server-two:config-server": establishing a new relation for shard-one:sharding would exceed its maximum relation limit of 1' # for now we have a large timeout due to the slow drainage of the `config.system.sessions` # collection. More info here: # https://stackoverflow.com/questions/77364840/mongodb-slow-chunk-migration-for-collection-config-system-sessions-with-remov @@ -62,17 +63,15 @@ async def test_only_one_config_server_relation(ops_test: OpsTest) -> None: f"{CONFIG_SERVER_ONE_APP_NAME}:{CONFIG_SERVER_REL_NAME}", ) - multiple_config_server_rels_allowed = True - try: + with pytest.raises(JujuAPIError) as juju_error: await ops_test.model.integrate( f"{SHARD_ONE_APP_NAME}:{SHARD_REL_NAME}", f"{CONFIG_SERVER_TWO_APP_NAME}:{CONFIG_SERVER_REL_NAME}", ) - except JujuAPIError as e: - if e.error_code == "quota limit exceeded": - multiple_config_server_rels_allowed = False - assert not multiple_config_server_rels_allowed, "Shard can relate to multiple config servers." + assert ( + juju_error.value.args[0] == RELATION_LIMIT_MESSAGE + ), "Shard can relate to multiple config servers." async def test_cannot_use_db_relation(ops_test: OpsTest) -> None: @@ -94,6 +93,7 @@ async def test_cannot_use_db_relation(ops_test: OpsTest) -> None: shard_unit.workload_status_message == "Sharding roles do not support database interface." ), "Shard cannot be related using the database relation" + # clean up relation await ops_test.model.applications[SHARD_ONE_APP_NAME].remove_relation( f"{APP_CHARM_NAME}:{DATABASE_REL_NAME}", SHARD_ONE_APP_NAME, @@ -108,7 +108,7 @@ async def test_cannot_use_db_relation(ops_test: OpsTest) -> None: async def test_cannot_use_legacy_db_relation(ops_test: OpsTest) -> None: - """Verify that a shard cannot use the DB relation.""" + """Verify that a shard cannot use the legcy DB relation.""" await ops_test.model.integrate( LEGACY_APP_CHARM_NAME, SHARD_ONE_APP_NAME, @@ -126,6 +126,7 @@ async def test_cannot_use_legacy_db_relation(ops_test: OpsTest) -> None: shard_unit.workload_status_message == "Sharding roles do not support obsolete interface." ), "Shard cannot be related using the mongodb relation" + # clean up relation await ops_test.model.applications[SHARD_ONE_APP_NAME].remove_relation( f"{SHARD_ONE_APP_NAME}:{LEGACY_RELATION_NAME}", f"{LEGACY_APP_CHARM_NAME}:{LEGACY_RELATION_NAME}",