From 7c80365d0139e55651dd226564edd2b1b0943bc1 Mon Sep 17 00:00:00 2001 From: Judit Novak Date: Tue, 28 May 2024 14:29:14 +0200 Subject: [PATCH] [DPE-4416] URI exists while re-creating secret with modified label (#170) * Logging format change, outside of the socpe of the PR * codespell-required changes * [BUGFIX] URI exists while re-creating secret with modified label * Unittests * Integration tests * Changes on PR request --- .github/workflows/ci.yaml | 24 +- .../data_platform_libs/v0/data_interfaces.py | 10 +- tests/__init__.py | 2 + tests/integration/conftest.py | 37 ++ tests/integration/database-charm/actions.yaml | 17 + tests/integration/database-charm/src/charm.py | 31 + ...t_rolling_upgrade_from_specific_version.py | 538 ++++++++++++++++++ tests/unit/test_data_interfaces.py | 2 - tests/unit/test_data_models.py | 6 +- tests/unit/test_upgrade.py | 4 +- tox.ini | 20 +- 11 files changed, 676 insertions(+), 15 deletions(-) create mode 100644 tests/__init__.py create mode 100644 tests/integration/test_rolling_upgrade_from_specific_version.py diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 83340f48..2c248d95 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -71,10 +71,30 @@ jobs: juju-snap-channel: "3.1/stable" libjuju-version: "3.2.2" include: - - {tox-environments: integration-upgrades, + - {tox-environments: integration-interfaces-upgrade-from-version-1-earlier, juju-version: {juju-snap-channel: "3.1/stable", - juju-bootstrap-option: "3.1.6", + juju-bootstrap-option: "3.1.7", + libjuju-version: "3.2.2"}} + - {tox-environments: integration-interfaces-upgrade-from-version-2-earlier, + juju-version: + {juju-snap-channel: "3.1/stable", + juju-bootstrap-option: "3.1.7", + libjuju-version: "3.2.2"}} + - {tox-environments: integration-interfaces-upgrade-from-version-3-earlier, + juju-version: + {juju-snap-channel: "3.1/stable", + juju-bootstrap-option: "3.1.7", + libjuju-version: "3.2.2"}} + - {tox-environments: integration-interfaces-upgrade-from-version-4-earlier, + juju-version: + {juju-snap-channel: "3.1/stable", + juju-bootstrap-option: "3.1.7", + libjuju-version: "3.2.2"}} + - {tox-environments: integration-upgrades-databag-to-secrets, + juju-version: + {juju-snap-channel: "3.1/stable", + juju-bootstrap-option: "3.1.7", libjuju-version: "3.2.2"}} - {tox-environments: integration-secrets, juju-version: diff --git a/lib/charms/data_platform_libs/v0/data_interfaces.py b/lib/charms/data_platform_libs/v0/data_interfaces.py index 5cb309b1..b331bdce 100644 --- a/lib/charms/data_platform_libs/v0/data_interfaces.py +++ b/lib/charms/data_platform_libs/v0/data_interfaces.py @@ -331,7 +331,7 @@ def _on_topic_requested(self, event: TopicRequestedEvent): # Increment this PATCH version before using `charmcraft publish-lib` or reset # to 0 if you are raising the major API version -LIBPATCH = 35 +LIBPATCH = 36 PYDEPS = ["ops>=2.0.0"] @@ -642,8 +642,8 @@ def _move_to_new_label_if_needed(self): return # Create a new secret with the new label - old_meta = self._secret_meta content = self._secret_meta.get_content() + self._secret_uri = None # I wish we could just check if we are the owners of the secret... try: @@ -651,7 +651,7 @@ def _move_to_new_label_if_needed(self): except ModelError as err: if "this unit is not the leader" not in str(err): raise - old_meta.remove_all_revisions() + self.current_label = None def set_content(self, content: Dict[str, str]) -> None: """Setting cached secret content.""" @@ -1586,7 +1586,7 @@ def _register_secret_to_relation( """ label = self._generate_secret_label(relation_name, relation_id, group) - # Fetchin the Secret's meta information ensuring that it's locally getting registered with + # Fetching the Secret's meta information ensuring that it's locally getting registered with CachedSecret(self._model, self.component, label, secret_id).meta def _register_secrets_to_relation(self, relation: Relation, params_name_list: List[str]): @@ -2309,7 +2309,7 @@ def _secrets(self) -> dict: return self._cached_secrets def _get_secret(self, group) -> Optional[Dict[str, str]]: - """Retrieveing secrets.""" + """Retrieving secrets.""" if not self.app: return if not self._secrets.get(group): diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 00000000..db3bfe1a --- /dev/null +++ b/tests/__init__.py @@ -0,0 +1,2 @@ +# Copyright 2023 Canonical Ltd. +# See LICENSE file for licensing details. diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py index 6bbfa3d4..dc2cbad7 100644 --- a/tests/integration/conftest.py +++ b/tests/integration/conftest.py @@ -7,6 +7,7 @@ import shutil from datetime import datetime from pathlib import Path +from subprocess import check_call, check_output import pytest from pytest_operator.plugin import OpsTest @@ -153,3 +154,39 @@ async def without_errors(ops_test: OpsTest, request): continue if logitems[2] == "ERROR": assert any(white in line for white in whitelist) + + +@pytest.fixture(scope="session") +def fetch_old_versions(): + """Fetching the previous 4 versions of the lib for upgrade tests.""" + cwd = os.getcwd() + src_path = "lib/charms/data_platform_libs/v0/data_interfaces.py" + data_path = f"{cwd}/tests/integration/data/data_interfaces.py" + tmp_path = "./tmp_repo_checkout" + + os.mkdir(tmp_path) + os.chdir(tmp_path) + check_call("git clone https://github.com/canonical/data-platform-libs.git", shell=True) + os.chdir("data-platform-libs") + last_commits = check_output( + "git show --pretty=format:'%h' --no-patch -15", shell=True, universal_newlines=True + ).split() + + versions = [] + for commit in last_commits: + check_call(f"git checkout {commit}", shell=True) + version = check_output( + "grep LIBPATCH lib/charms/data_platform_libs/v0/data_interfaces.py | cut -d ' ' -f 3", + shell=True, + universal_newlines=True, + ) + version = version.strip() + if version not in versions: + shutil.copyfile(src_path, f"{data_path}.v{version}") + versions.append(version) + + if len(versions) == 4: + break + + os.chdir(cwd) + shutil.rmtree(tmp_path) diff --git a/tests/integration/database-charm/actions.yaml b/tests/integration/database-charm/actions.yaml index c2492ecd..710d8a68 100644 --- a/tests/integration/database-charm/actions.yaml +++ b/tests/integration/database-charm/actions.yaml @@ -79,6 +79,23 @@ set-peer-relation-field: type: string description: Value of the field to set +set-peer-relation-field-multiple: + description: Set fields from the second-database relation multiple times + params: + component: + type: string + description: app/unit + field: + type: string + description: Relation field + value: + type: string + description: Value of the field to set + count: + type: integer + description: Number of iterations + default: 3 + set-peer-secret: description: Set fields from the second-database relation params: diff --git a/tests/integration/database-charm/src/charm.py b/tests/integration/database-charm/src/charm.py index de86f9a6..e73e8a11 100755 --- a/tests/integration/database-charm/src/charm.py +++ b/tests/integration/database-charm/src/charm.py @@ -104,6 +104,10 @@ def __init__(self, *args): self.framework.observe( self.on.set_peer_relation_field_action, self._on_set_peer_relation_field ) + self.framework.observe( + self.on.set_peer_relation_field_multiple_action, + self._on_set_peer_relation_field_multiple, + ) self.framework.observe(self.on.set_peer_secret_action, self._on_set_peer_secret) self.framework.observe( self.on.delete_peer_relation_field_action, self._on_delete_peer_relation_field @@ -341,6 +345,33 @@ def _on_set_peer_relation_field(self, event: ActionEvent): relation.id, {event.params["field"]: event.params["value"]} ) + def _on_set_peer_relation_field_multiple(self, event: ActionEvent): + """Set requested relation field.""" + component = event.params["component"] + count = event.params["count"] + + # Charms should be compatible with old vesrions, to simulate rolling upgrade + for cnt in range(count): + value = event.params["value"] + f"{cnt}" + if DATA_INTERFACES_VERSION <= 17: + relation = self.model.get_relation(PEER) + if component == "app": + relation.data[self.app][event.params["field"]] = value + else: + relation.data[self.unit][event.params["field"]] = value + return + + if component == "app": + relation = self.peer_relation_app.relations[0] + self.peer_relation_app.update_relation_data( + relation.id, {event.params["field"]: value} + ) + else: + relation = self.peer_relation_unit.relations[0] + self.peer_relation_unit.update_relation_data( + relation.id, {event.params["field"]: value} + ) + def _on_set_peer_secret(self, event: ActionEvent): """Set requested relation field.""" component = event.params["component"] diff --git a/tests/integration/test_rolling_upgrade_from_specific_version.py b/tests/integration/test_rolling_upgrade_from_specific_version.py new file mode 100644 index 00000000..a9cc643f --- /dev/null +++ b/tests/integration/test_rolling_upgrade_from_specific_version.py @@ -0,0 +1,538 @@ +#!/usr/bin/env python3 +# Copyright 2023 Canonical Ltd. +# See LICENSE file for licensing details. +import asyncio +import logging +import os +from pathlib import Path + +import pytest +import yaml +from lib.charms.data_platform_libs.v0.data_interfaces import LIBPATCH +from pytest_operator.plugin import OpsTest + +from .helpers import get_leader_id + +logger = logging.getLogger(__name__) + +APPLICATION_APP_NAME = "application" +DATABASE_APP_NAME = "database" +APP_NAMES = [APPLICATION_APP_NAME, DATABASE_APP_NAME] +DATABASE_APP_METADATA = yaml.safe_load( + Path("./tests/integration/database-charm/metadata.yaml").read_text() +) +FIRST_DATABASE_RELATION_NAME = "first-database" +SECOND_DATABASE_RELATION_NAME = "second-database" + +SECRET_REF_PREFIX = "secret-" + + +def old_version_to_upgrade_from(): + """Determine how many versions to go back from tox environment (default: previous version).""" + try: + go_backwards = int(os.environ["TOX_ENV"].split("-")[5]) + except TypeError: + go_backwards = 1 + return LIBPATCH - go_backwards + + +async def downgrade_to_old_version(ops_test, app_name): + """Helper function simulating a "rolling downgrade". + + The data_interfaces module is replaced "on-the-fly" by an older version. + """ + for unit in ops_test.model.applications[app_name].units: + unit_name_with_dash = unit.name.replace("/", "-") + version = old_version_to_upgrade_from() + complete_command = ( + f"scp tests/integration/data/data_interfaces.py.v{version} " + f"{unit.name}:/var/lib/juju/agents/unit-{unit_name_with_dash}" + "/charm/lib/charms/data_platform_libs/v0/data_interfaces.py" + ) + x, stdout, y = await ops_test.juju(*complete_command.split()) + + +async def upgrade_to_new_version(ops_test, app_name): + """Helper function simulating a "rolling upgrade". + + The data_interfaces module is replaced "on-the-fly" by the latest version. + """ + for unit in ops_test.model.applications[app_name].units: + unit_name_with_dash = unit.name.replace("/", "-") + complete_command = ( + "scp lib/charms/data_platform_libs/v0/data_interfaces.py " + f"{unit.name}:/var/lib/juju/agents/unit-{unit_name_with_dash}/charm/lib/charms/data_platform_libs/v0/" + ) + x, stdout, y = await ops_test.juju(*complete_command.split()) + + +@pytest.mark.usefixtures("fetch_old_versions") +@pytest.mark.abort_on_fail +async def test_deploy_charms(ops_test: OpsTest, application_charm, database_charm): + """Deploy both charms (application and database) to use in the tests.""" + await asyncio.gather( + ops_test.model.deploy( + application_charm, application_name=APPLICATION_APP_NAME, num_units=1, series="jammy" + ), + ops_test.model.deploy( + database_charm, + resources={ + "database-image": DATABASE_APP_METADATA["resources"]["database-image"][ + "upstream-source" + ] + }, + application_name=DATABASE_APP_NAME, + num_units=1, + series="jammy", + ), + ) + await ops_test.model.wait_for_idle( + apps=[APPLICATION_APP_NAME, DATABASE_APP_NAME], + status="active", + wait_for_exact_units=1, + ) + + +# ----------------------------------------------------- +# Testing 'Peer Relation' +# ----------------------------------------------------- + + +@pytest.mark.abort_on_fail +@pytest.mark.parametrize("component", ["app", "unit"]) +async def test_peer_relation(component, ops_test: OpsTest): + """Peer relation safe across upgrades.""" + await downgrade_to_old_version(ops_test, DATABASE_APP_NAME) + + # Setting and verifying two fields (one that should be a secret, one plain text) + leader_id = await get_leader_id(ops_test, DATABASE_APP_NAME) + unit_name = f"{DATABASE_APP_NAME}/{leader_id}" + action = await ops_test.model.units.get(unit_name).run_action( + "set-peer-relation-field", + **{"component": component, "field": "monitor-password", "value": "blablabla"}, + ) + await action.wait() + + action = await ops_test.model.units.get(unit_name).run_action( + "set-peer-relation-field", + **{"component": component, "field": "not-a-secret", "value": "plain text"}, + ) + await action.wait() + + action = await ops_test.model.units.get(unit_name).run_action( + "get-peer-relation-field", **{"component": component, "field": "monitor-password"} + ) + await action.wait() + assert action.results.get("value") == "blablabla" + + action = await ops_test.model.units.get(unit_name).run_action( + "get-peer-relation-field", **{"component": component, "field": "not-a-secret"} + ) + await action.wait() + assert action.results.get("value") == "plain text" + + # Upgrade + await upgrade_to_new_version(ops_test, DATABASE_APP_NAME) + + # Both secret and databag content can be modified -- even twice ;-) + action = await ops_test.model.units.get(unit_name).run_action( + "set-peer-relation-field-multiple", + **{"component": component, "field": "monitor-password", "value": "blablabla_new"}, + ) + await action.wait() + + action = await ops_test.model.units.get(unit_name).run_action( + "set-peer-relation-field-multiple", + **{"component": component, "field": "not-a-secret", "value": "even more plain text"}, + ) + await action.wait() + + action = await ops_test.model.units.get(unit_name).run_action( + "get-peer-relation-field", **{"component": component, "field": "monitor-password"} + ) + await action.wait() + assert action.results.get("value") == "blablabla_new2" + + await upgrade_to_new_version(ops_test, DATABASE_APP_NAME) + + action = await ops_test.model.units.get(unit_name).run_action( + "get-peer-relation-field", **{"component": component, "field": "not-a-secret"} + ) + await action.wait() + assert action.results.get("value") == "even more plain text2" + + # Removing both secret and databag content can be modified + action = await ops_test.model.units.get(unit_name).run_action( + "delete-peer-relation-field", **{"component": component, "field": "monitor-password"} + ) + await action.wait() + + action = await ops_test.model.units.get(unit_name).run_action( + "delete-peer-relation-field", **{"component": component, "field": "not-a-secret"} + ) + await action.wait() + + # ...successfully + action = await ops_test.model.units.get(unit_name).run_action( + "get-peer-relation-field", **{"component": component, "field": "monitor-password"} + ) + await action.wait() + assert not action.results.get("value") + + action = await ops_test.model.units.get(unit_name).run_action( + "get-peer-relation-field", **{"component": component, "field": "not-a-secret"} + ) + await action.wait() + assert not action.results.get("value") + + +# +# NOTE: Tests below have follow a strict sequence. +# Pls only insert among if strongly justified. +# + +# ----------------------------------------------------- +# Testing 'Requires - databag' vs. 'Provides - secrets' +# ----------------------------------------------------- + + +@pytest.mark.abort_on_fail +async def test_unbalanced_versions_req_old_vs_prov_new( + ops_test: OpsTest, +): + """Relating Requires (old version) with Provides (latest).""" + await downgrade_to_old_version(ops_test, APPLICATION_APP_NAME) + + # Storing Relation object in 'pytest' global namespace for the session + pytest.first_database_relation = await ops_test.model.add_relation( + f"{APPLICATION_APP_NAME}:{FIRST_DATABASE_RELATION_NAME}", DATABASE_APP_NAME + ) + await ops_test.model.wait_for_idle(apps=APP_NAMES, status="active") + + # Username + leader_app_id = await get_leader_id(ops_test, APPLICATION_APP_NAME) + leader_app_name = f"{APPLICATION_APP_NAME}/{leader_app_id}" + action = await ops_test.model.units.get(leader_app_name).run_action( + "get-relation-field", + **{"relation_id": pytest.first_database_relation.id, "field": "username"}, + ) + await action.wait() + assert action.results.get("value") + + username = action.results.get("value") + + # Password + action = await ops_test.model.units.get(leader_app_name).run_action( + "get-relation-field", + **{"relation_id": pytest.first_database_relation.id, "field": "password"}, + ) + await action.wait() + assert action.results.get("value") + + password = action.results.get("value") + + # Username is correct + leader_id = await get_leader_id(ops_test, DATABASE_APP_NAME) + leader_name = f"{DATABASE_APP_NAME}/{leader_id}" + action = await ops_test.model.units.get(leader_name).run_action( + "get-relation-self-side-field", + **{"relation_id": pytest.first_database_relation.id, "field": "username"}, + ) + await action.wait() + assert action.results.get("value") == username + + # Password is correct + action = await ops_test.model.units.get(leader_name).run_action( + "get-relation-self-side-field", + **{"relation_id": pytest.first_database_relation.id, "field": "password"}, + ) + await action.wait() + assert action.results.get("value") == password + + +@pytest.mark.abort_on_fail +async def test_rolling_upgrade_requires_old_vs_provides_new(ops_test: OpsTest): + """Upgrading Requires to the latest version of the libs.""" + await upgrade_to_new_version(ops_test, APPLICATION_APP_NAME) + + # Application charm leader + leader_app_id = await get_leader_id(ops_test, APPLICATION_APP_NAME) + leader_app_name = f"{APPLICATION_APP_NAME}/{leader_app_id}" + + # DB leader + leader_id = await get_leader_id(ops_test, DATABASE_APP_NAME) + leader_name = f"{DATABASE_APP_NAME}/{leader_id}" + + # Set new sensitive information (if secrets: stored in 'secret-user') + uris_new_val = "http://username:password@example.com" + action = await ops_test.model.units.get(leader_name).run_action( + "set-relation-field", + **{ + "relation_id": pytest.first_database_relation.id, + "field": "uris", + "value": uris_new_val, + }, + ) + await action.wait() + + # Interface functions (invoked by actions below) are consistent + action = await ops_test.model.units.get(leader_app_name).run_action( + "get-relation-field", + **{"relation_id": pytest.first_database_relation.id, "field": "uris"}, + ) + await action.wait() + assert action.results.get("value") == uris_new_val + + action = await ops_test.model.units.get(leader_name).run_action( + "get-relation-self-side-field", + **{"relation_id": pytest.first_database_relation.id, "field": "uris"}, + ) + await action.wait() + assert action.results.get("value") == uris_new_val + + action = await ops_test.model.units.get(leader_name).run_action( + "delete-relation-field", + **{"relation_id": pytest.first_database_relation.id, "field": "uris"}, + ) + await action.wait() + + # Interface functions (invoked by actions below) are consistent + action = await ops_test.model.units.get(leader_name).run_action( + "get-relation-self-side-field", + **{"relation_id": pytest.first_database_relation.id, "field": "uris"}, + ) + await action.wait() + assert not action.results.get("value") + + +@pytest.mark.abort_on_fail +async def test_rolling_upgrade_requires_old_vs_provides_new_upgrade_new_secret( + ops_test: OpsTest, +): + """After Requires upgrade new secrets are possible to define.""" + # Application charm leader + leader_app_id = await get_leader_id(ops_test, APPLICATION_APP_NAME) + leader_app_name = f"{APPLICATION_APP_NAME}/{leader_app_id}" + + # DB leader + leader_id = await get_leader_id(ops_test, DATABASE_APP_NAME) + leader_name = f"{DATABASE_APP_NAME}/{leader_id}" + + # Set new sensitive information (if secrets: stored in 'secret-tls') + tls_ca_val = "" + action = await ops_test.model.units.get(leader_name).run_action( + "set-relation-field", + **{ + "relation_id": pytest.first_database_relation.id, + "field": "tls-ca", + "value": tls_ca_val, + }, + ) + await action.wait() + + # Interface functions (invoked by actions below) are consistent + action = await ops_test.model.units.get(leader_app_name).run_action( + "get-relation-field", + **{"relation_id": pytest.first_database_relation.id, "field": "tls-ca"}, + ) + await action.wait() + assert action.results.get("value") == tls_ca_val + + action = await ops_test.model.units.get(leader_name).run_action( + "get-relation-self-side-field", + **{"relation_id": pytest.first_database_relation.id, "field": "tls-ca"}, + ) + await action.wait() + assert action.results.get("value") == tls_ca_val + + action = await ops_test.model.units.get(leader_name).run_action( + "delete-relation-field", + **{"relation_id": pytest.first_database_relation.id, "field": "tls-ca"}, + ) + await action.wait() + + action = await ops_test.model.units.get(leader_app_name).run_action( + "get-relation-field", + **{"relation_id": pytest.first_database_relation.id, "field": "tls-ca"}, + ) + await action.wait() + assert not action.results.get("value") + + # Username exists + action = await ops_test.model.units.get(leader_app_name).run_action( + "get-relation-field", + **{"relation_id": pytest.first_database_relation.id, "field": "username"}, + ) + await action.wait() + assert action.results.get("value") + + # Password exists + action = await ops_test.model.units.get(leader_name).run_action( + "get-relation-self-side-field", + **{"relation_id": pytest.first_database_relation.id, "field": "password"}, + ) + await action.wait() + assert action.results.get("value") + + +# ----------------------------------------------------- +# Testing 'Requires - secrets' vs. 'Provides - databag' +# ----------------------------------------------------- + + +@pytest.mark.abort_on_fail +async def test_unbalanced_versions_req_new_vs_prov_old( + ops_test: OpsTest, +): + """Relating Requires (latest) with Provides (old version).""" + await downgrade_to_old_version(ops_test, DATABASE_APP_NAME) + + # Storing Relation object in 'pytest' global namespace for the session + pytest.second_database_relation = await ops_test.model.add_relation( + f"{APPLICATION_APP_NAME}:{SECOND_DATABASE_RELATION_NAME}", DATABASE_APP_NAME + ) + await ops_test.model.wait_for_idle(apps=APP_NAMES, status="active") + + # Username exists + leader_id = await get_leader_id(ops_test, DATABASE_APP_NAME) + leader_name = f"{DATABASE_APP_NAME}/{leader_id}" + action = await ops_test.model.units.get(leader_name).run_action( + "get-relation-self-side-field", + **{"relation_id": pytest.second_database_relation.id, "field": "username"}, + ) + await action.wait() + assert action.results.get("value") + + # Password exists + action = await ops_test.model.units.get(leader_name).run_action( + "get-relation-self-side-field", + **{"relation_id": pytest.second_database_relation.id, "field": "password"}, + ) + await action.wait() + assert action.results.get("value") + + +@pytest.mark.abort_on_fail +async def test_rolling_upgrade_requires_new_vs_provides_old( + ops_test: OpsTest, +): + """Upgrading Provides to latest version.""" + await upgrade_to_new_version(ops_test, DATABASE_APP_NAME) + + # Application charm leader + leader_app_id = await get_leader_id(ops_test, APPLICATION_APP_NAME) + leader_app_name = f"{APPLICATION_APP_NAME}/{leader_app_id}" + + # DB leader + leader_id = await get_leader_id(ops_test, DATABASE_APP_NAME) + leader_name = f"{DATABASE_APP_NAME}/{leader_id}" + + # Set new sensitive information (if secrets: stored in 'secret-user') + uris_new_val = "http://username:password@example.com" + action = await ops_test.model.units.get(leader_name).run_action( + "set-relation-field", + **{ + "relation_id": pytest.second_database_relation.id, + "field": "uris", + "value": uris_new_val, + }, + ) + await action.wait() + + # Interface functions (invoked by actions below) are consistent + action = await ops_test.model.units.get(leader_app_name).run_action( + "get-relation-field", + **{"relation_id": pytest.second_database_relation.id, "field": "uris"}, + ) + await action.wait() + assert action.results.get("value") == uris_new_val + + action = await ops_test.model.units.get(leader_name).run_action( + "get-relation-self-side-field", + **{"relation_id": pytest.second_database_relation.id, "field": "uris"}, + ) + await action.wait() + assert action.results.get("value") == uris_new_val + + action = await ops_test.model.units.get(leader_name).run_action( + "delete-relation-field", + **{"relation_id": pytest.second_database_relation.id, "field": "uris"}, + ) + await action.wait() + + # Interface functions (invoked by actions below) are consistent + action = await ops_test.model.units.get(leader_name).run_action( + "get-relation-self-side-field", + **{"relation_id": pytest.second_database_relation.id, "field": "uris"}, + ) + await action.wait() + assert not action.results.get("value") + + +@pytest.mark.abort_on_fail +async def test_rolling_upgrade_requires_new_vs_provides_old_upgrade_new_secret( + ops_test: OpsTest, +): + """After Provider upgrade, we can safely define new secrets.""" + # Application charm leader + leader_app_id = await get_leader_id(ops_test, APPLICATION_APP_NAME) + leader_app_name = f"{APPLICATION_APP_NAME}/{leader_app_id}" + + # DB leader + leader_id = await get_leader_id(ops_test, DATABASE_APP_NAME) + leader_name = f"{DATABASE_APP_NAME}/{leader_id}" + + # Set new sensitive information (if secrets: stored in 'secret-tls') + tls_ca_val = "" + action = await ops_test.model.units.get(leader_name).run_action( + "set-relation-field", + **{ + "relation_id": pytest.second_database_relation.id, + "field": "tls-ca", + "value": tls_ca_val, + }, + ) + await action.wait() + + # Interface functions (invoked by actions below) are consistent + action = await ops_test.model.units.get(leader_app_name).run_action( + "get-relation-field", + **{"relation_id": pytest.second_database_relation.id, "field": "tls-ca"}, + ) + await action.wait() + assert action.results.get("value") == tls_ca_val + + action = await ops_test.model.units.get(leader_name).run_action( + "get-relation-self-side-field", + **{"relation_id": pytest.second_database_relation.id, "field": "tls-ca"}, + ) + await action.wait() + assert action.results.get("value") == tls_ca_val + + action = await ops_test.model.units.get(leader_name).run_action( + "delete-relation-field", + **{"relation_id": pytest.second_database_relation.id, "field": "tls-ca"}, + ) + await action.wait() + + action = await ops_test.model.units.get(leader_app_name).run_action( + "get-relation-field", + **{"relation_id": pytest.second_database_relation.id, "field": "tls-ca"}, + ) + await action.wait() + assert not action.results.get("value") + + action = await ops_test.model.units.get(leader_app_name).run_action( + "get-relation-field", + **{"relation_id": pytest.second_database_relation.id, "field": "password"}, + ) + await action.wait() + assert action.results.get("value") + password = action.results.get("value") + + action = await ops_test.model.units.get(leader_name).run_action( + "get-relation-self-side-field", + **{"relation_id": pytest.second_database_relation.id, "field": "password"}, + ) + await action.wait() + assert action.results.get("value") == password diff --git a/tests/unit/test_data_interfaces.py b/tests/unit/test_data_interfaces.py index 19916f13..603ace3f 100644 --- a/tests/unit/test_data_interfaces.py +++ b/tests/unit/test_data_interfaces.py @@ -632,8 +632,6 @@ def test_peer_relation_interface_backwards_compatible_legacy_label(self, interfa secret2 = self.harness.model.get_secret(label=f"{PEER_RELATION_NAME}.database.{scope}") assert secret2.id != secret_id assert interface.fetch_my_relation_field(relation_id, "secret-field") == "blabla" - with pytest.raises(SecretNotFoundError): - secret = self.harness.model.get_secret(label=f"database.{scope}") # After update the old label is gone. But sadly unittests don't allow us for verifying that :-/ diff --git a/tests/unit/test_data_models.py b/tests/unit/test_data_models.py index e5621b8a..173156ba 100644 --- a/tests/unit/test_data_models.py +++ b/tests/unit/test_data_models.py @@ -173,7 +173,7 @@ def test_action_params_parsing_ok(self): mock_event.params = {"host": "my-host"} with self.assertLogs(level="INFO") as logger: self.assertTrue(self.harness.charm._set_server_action(mock_event)) - self.assertEqual(sorted(logger.output), ["INFO:unit.test_data_models:my-host:80"]) + self.assertEqual(sorted(logger.output), ["INFO:tests.unit.test_data_models:my-host:80"]) def test_action_params_parsing_ko(self): """Test that action parameters validation would raise an exception.""" @@ -198,7 +198,9 @@ def test_relation_databag_io(self): with self.assertLogs(level="INFO") as logger: self.harness.update_relation_data(relation_id, "mongodb", {"key": "1.0"}) - self.assertEqual(logger.output, ["INFO:unit.test_data_models:Field type: "]) + self.assertEqual( + logger.output, ["INFO:tests.unit.test_data_models:Field type: "] + ) def test_relation_databag_merged(self): """Test that relation databag of unit and app can be read and merged into a single pydantic object.""" diff --git a/tests/unit/test_upgrade.py b/tests/unit/test_upgrade.py index 467da1f4..b0ce49f7 100644 --- a/tests/unit/test_upgrade.py +++ b/tests/unit/test_upgrade.py @@ -261,7 +261,7 @@ def test_dependency_model_raises_for_bad_dependency(value): def test_dependency_model_raises_for_bad_nested_dependency(value): deps = { "gandalf_the_white": { - "dependencies": {"gandalf_the_grey": "~1.0", "durin": value}, + "dependencies": {"gandalf_the_grey": "~1.0", "bilbo": value}, "name": "gandalf", "upgrade_supported": ">6", "version": "7", @@ -294,7 +294,7 @@ def test_dependency_model_succeeds(): def test_dependency_model_succeeds_nested(): deps = { "gandalf_the_white": { - "dependencies": {"gandalf_the_grey": "~1.0", "durin": "^1.2.5"}, + "dependencies": {"gandalf_the_grey": "~1.0", "bilbo": "^1.2.5"}, "name": "gandalf", "upgrade_supported": ">1.2", "version": "7", diff --git a/tox.ini b/tox.ini index 0ba2ae36..6adfeb50 100644 --- a/tox.ini +++ b/tox.ini @@ -54,7 +54,9 @@ commands = --skip {tox_root}/venv \ --skip {tox_root}/.mypy_cache \ --skip {tox_root}/icon.svg \ - --skip {tox_root}/poetry.lock + --skip {tox_root}/poetry.lock \ + --skip {tox_root}/tests/integration/data \ + --ignore-words-list "assertIn" codespell {[vars]lib_path} ruff {[vars]all_path} @@ -88,7 +90,7 @@ deps = commands = pytest -v --tb native --log-cli-level=INFO -s {posargs} {[vars]tests_path}/integration/test_charm.py -[testenv:integration-upgrades] +[testenv:integration-upgrades-databag-to-secrets] description = Run database integration tests deps = psycopg2-binary @@ -100,6 +102,20 @@ deps = commands = pytest -v --tb native --log-cli-level=INFO -s {posargs} {[vars]tests_path}/integration/test_rolling_upgrade.py +[testenv:integration-interfaces-upgrade-from-version-{1,2,3,4}-earlier] +description = Run database integration tests +deps = + psycopg2-binary + pytest<8.2.0 + juju{env:LIBJUJU_VERSION_SPECIFIER:==2.9.42.4} + pytest-operator + pytest-mock + -r {tox_root}/requirements.txt +set_env = + TOX_ENV = {envname} +commands = + pytest -v --tb native --log-cli-level=INFO -s {posargs} {[vars]tests_path}/integration/test_rolling_upgrade_from_specific_version.py + [testenv:integration-kafka] description = Run Kafka integration tests deps =