diff --git a/lib/charms/mongodb/v1/mongodb_provider.py b/lib/charms/mongodb/v1/mongodb_provider.py index c9bd68d47..c4ee2426c 100644 --- a/lib/charms/mongodb/v1/mongodb_provider.py +++ b/lib/charms/mongodb/v1/mongodb_provider.py @@ -14,8 +14,8 @@ from typing import List, Optional, Set from charms.data_platform_libs.v0.data_interfaces import DatabaseProvides +from charms.mongodb.v0.mongo import MongoConfiguration, MongoConnection from charms.mongodb.v1.helpers import generate_password -from charms.mongodb.v1.mongodb import MongoConfiguration, MongoDBConnection from ops.charm import CharmBase, EventBase, RelationBrokenEvent, RelationChangedEvent from ops.framework import Object from ops.model import Relation @@ -31,17 +31,15 @@ # Increment this PATCH version before using `charmcraft publish-lib` or reset # to 0 if you are raising the major API version -LIBPATCH = 10 +LIBPATCH = 14 logger = logging.getLogger(__name__) REL_NAME = "database" - MONGOS_RELATIONS = "cluster" +MONGOS_CLIENT_RELATIONS = "mongos_proxy" +MANAGED_USERS_KEY = "managed-users-key" # We expect the MongoDB container to use the default ports -MONGODB_PORT = 27017 -MONGODB_VERSION = "5.0" -PEER = "database-peers" Diff = namedtuple("Diff", "added changed deleted") Diff.__doc__ = """ @@ -54,7 +52,7 @@ class MongoDBProvider(Object): """In this class, we manage client database relations.""" - def __init__(self, charm: CharmBase, substrate="k8s", relation_name: str = "database") -> None: + def __init__(self, charm: CharmBase, substrate="k8s", relation_name: str = REL_NAME) -> None: """Constructor for MongoDBProvider object. Args: @@ -62,42 +60,46 @@ def __init__(self, charm: CharmBase, substrate="k8s", relation_name: str = "data substrate: host type, either "k8s" or "vm" relation_name: the name of the relation """ - self.relation_name = relation_name self.substrate = substrate self.charm = charm - super().__init__(charm, self.relation_name) + super().__init__(charm, relation_name) self.framework.observe( - charm.on[self.relation_name].relation_departed, + charm.on[relation_name].relation_departed, self.charm.check_relation_broken_or_scale_down, ) - self.framework.observe( - charm.on[self.relation_name].relation_broken, self._on_relation_event - ) - self.framework.observe( - charm.on[self.relation_name].relation_changed, self._on_relation_event - ) + self.framework.observe(charm.on[relation_name].relation_broken, self._on_relation_event) + self.framework.observe(charm.on[relation_name].relation_changed, self._on_relation_event) # Charm events defined in the database provides charm library. - self.database_provides = DatabaseProvides(self.charm, relation_name=self.relation_name) + self.database_provides = DatabaseProvides(self.charm, relation_name=relation_name) self.framework.observe( self.database_provides.on.database_requested, self._on_relation_event ) - def pass_hook_checks(self, event: EventBase) -> bool: - """Runs the pre-hooks checks for MongoDBProvider, returns True if all pass.""" + def pass_sanity_hook_checks(self) -> bool: + """Runs reusable and event agnostic checks.""" # We shouldn't try to create or update users if the database is not # initialised. We will create users as part of initialisation. if not self.charm.db_initialised: return False - if not self.charm.is_relation_feasible(self.relation_name): + if not self.charm.is_role(Config.Role.MONGOS) and not self.charm.is_relation_feasible( + self.get_relation_name() + ): logger.info("Skipping code for relations.") return False if not self.charm.unit.is_leader(): return False + return True + + def pass_hook_checks(self, event: EventBase) -> bool: + """Runs the pre-hooks checks for MongoDBProvider, returns True if all pass.""" + if not self.pass_sanity_hook_checks(): + return False + if self.charm.upgrade_in_progress: logger.warning( "Adding relations is not supported during an upgrade. The charm may be in a broken, unrecoverable state." @@ -163,16 +165,54 @@ def oversee_users(self, departed_relation_id: Optional[int], event): When the function is executed in relation departed event, the departed relation is still on the list of all relations. Therefore, for proper work of the function, we need to exclude departed relation from the list. + + Raises: + PyMongoError """ - with MongoDBConnection(self.charm.mongodb_config) as mongo: + with MongoConnection(self.charm.mongo_config) as mongo: database_users = mongo.get_users() - relation_users = self._get_users_from_relations(departed_relation_id) - for username in database_users - relation_users: + users_being_managed = database_users.intersection(self._get_relational_users_to_manage()) + expected_current_users = self._get_users_from_relations(departed_relation_id) + + self.remove_users(users_being_managed, expected_current_users) + self.add_users(users_being_managed, expected_current_users) + self.update_users(event, users_being_managed, expected_current_users) + self.auto_delete_dbs(departed_relation_id) + + def remove_users( + self, users_being_managed: Set[str], expected_current_users: Set[str] + ) -> None: + """Removes users from Charmed MongoDB. + + Note this only removes users that this application of Charmed MongoDB is responsible for + managing. It won't remove: + 1. users created from other applications + 2. users created from other mongos routers. + + Raises: + PyMongoError + """ + with MongoConnection(self.charm.mongo_config) as mongo: + for username in users_being_managed - expected_current_users: logger.info("Remove relation user: %s", username) + if ( + self.charm.is_role(Config.Role.MONGOS) + and username == self.charm.mongo_config.username + ): + continue + mongo.drop_user(username) + self._remove_from_relational_users_to_manage(username) + + def add_users(self, users_being_managed: Set[str], expected_current_users: Set[str]) -> None: + """Adds users to Charmed MongoDB. - for username in relation_users - database_users: + Raises: + PyMongoError + """ + with MongoConnection(self.charm.mongo_config) as mongo: + for username in expected_current_users - users_being_managed: config = self._get_config(username, None) if config.database is None: # We need to wait for the moment when the provider library @@ -181,15 +221,33 @@ def oversee_users(self, departed_relation_id: Optional[int], event): logger.info("Create relation user: %s on %s", config.username, config.database) mongo.create_user(config) + self._add_to_relational_users_to_manage(username) self._set_relation(config) - for username in relation_users.intersection(database_users): + def update_users( + self, event: EventBase, users_being_managed: Set[str], expected_current_users: Set[str] + ) -> None: + """Updates existing users in Charmed MongoDB. + + Raises: + PyMongoError + """ + with MongoConnection(self.charm.mongo_config) as mongo: + for username in expected_current_users.intersection(users_being_managed): config = self._get_config(username, None) logger.info("Update relation user: %s on %s", config.username, config.database) mongo.update_user(config) logger.info("Updating relation data according to diff") self._diff(event) + def auto_delete_dbs(self, departed_relation_id): + """Delete's unused dbs if configured to do so. + + Raises: + PyMongoError + """ + with MongoConnection(self.charm.mongo_config) as mongo: + if not self.charm.model.config["auto-delete"]: return @@ -240,15 +298,15 @@ def _diff(self, event: RelationChangedEvent) -> Diff: def update_app_relation_data(self) -> None: """Helper function to update application relation data.""" - if not self.charm.db_initialised: + if not self.pass_sanity_hook_checks(): return database_users = set() - with MongoDBConnection(self.charm.mongodb_config) as mongo: + with MongoConnection(self.charm.mongo_config) as mongo: database_users = mongo.get_users() - for relation in self._get_relations(rel=REL_NAME): + for relation in self._get_relations(): username = self._get_username_from_relation_id(relation.id) password = self._get_or_set_password(relation) config = self._get_config(username, password) @@ -282,7 +340,9 @@ def _get_or_set_password(self, relation: Relation) -> str: self.database_provides.update_relation_data(relation.id, {"password": password}) return password - def _get_config(self, username: str, password: Optional[str]) -> MongoConfiguration: + def _get_config( + self, username: str, password: Optional[str], event=None + ) -> MongoConfiguration: """Construct the config object for future user creation.""" relation = self._get_relation_from_username(username) if not password: @@ -290,16 +350,24 @@ def _get_config(self, username: str, password: Optional[str]) -> MongoConfigurat database_name = self._get_database_from_relation(relation) - return MongoConfiguration( - replset=self.charm.app.name, - database=database_name, - username=username, - password=password, - hosts=self.charm.mongodb_config.hosts, - roles=self._get_roles_from_relation(relation), - tls_external=False, - tls_internal=False, - ) + mongo_args = { + "database": database_name, + "username": username, + "password": password, + "hosts": self.charm.mongo_config.hosts, + "roles": self._get_roles_from_relation(relation), + "tls_external": False, + "tls_internal": False, + } + + if self.charm.is_role(Config.Role.MONGOS): + mongo_args["port"] = Config.MONGOS_PORT + if self.substrate == Config.Substrate.K8S: + mongo_args["hosts"] = self.charm.get_mongos_hosts_for_client() + else: + mongo_args["replset"] = self.charm.app.name + + return MongoConfiguration(**mongo_args) def _set_relation(self, config: MongoConfiguration): """Save all output fields into application relation.""" @@ -318,10 +386,11 @@ def _set_relation(self, config: MongoConfiguration): relation.id, ",".join(config.hosts), ) - self.database_provides.set_replset( - relation.id, - config.replset, - ) + if not self.charm.is_role(Config.Role.MONGOS): + self.database_provides.set_replset( + relation.id, + config.replset, + ) self.database_provides.set_uris( relation.id, config.uri, @@ -332,9 +401,9 @@ def _get_username_from_relation_id(relation_id: int) -> str: """Construct username.""" return f"relation-{relation_id}" - def _get_users_from_relations(self, departed_relation_id: Optional[int], rel=REL_NAME): + def _get_users_from_relations(self, departed_relation_id: Optional[int]): """Return usernames for all relations except departed relation.""" - relations = self._get_relations(rel) + relations = self._get_relations() return set( [ self._get_username_from_relation_id(relation.id) @@ -351,7 +420,7 @@ def _get_databases_from_relations(self, departed_relation_id: Optional[int]) -> except for those databases that belong to the departing relation specified. """ - relations = self._get_relations(rel=REL_NAME) + relations = self._get_relations() databases = set() for relation in relations: if relation.id == departed_relation_id: @@ -370,22 +439,54 @@ def _get_relation_from_username(self, username: str) -> Relation: assert match is not None, "No relation match" relation_id = int(match.group(1)) logger.debug("Relation ID: %s", relation_id) - relation_name = ( - MONGOS_RELATIONS if self.charm.is_role(Config.Role.CONFIG_SERVER) else REL_NAME - ) + relation_name = self.get_relation_name() return self.model.get_relation(relation_name, relation_id) - def _get_relations(self, rel=REL_NAME) -> List[Relation]: + def _get_relations(self) -> List[Relation]: """Return the set of relations for users. We create users for either direct relations to charm or for relations through the mongos charm. """ - return ( - self.model.relations[MONGOS_RELATIONS] - if self.charm.is_role(Config.Role.CONFIG_SERVER) - else self.model.relations[rel] - ) + return self.model.relations[self.get_relation_name()] + + def get_relation_name(self): + """Returns the name of the relation to use.""" + if self.charm.is_role(Config.Role.CONFIG_SERVER): + return MONGOS_RELATIONS + elif self.charm.is_role(Config.Role.MONGOS): + return MONGOS_CLIENT_RELATIONS + else: + return REL_NAME + + def _get_relational_users_to_manage(self) -> Set[str]: + """Returns a set of the users to manage. + + Note json cannot serialise sets. Convert from list. + """ + return set(json.loads(self.charm.app_peer_data.get(MANAGED_USERS_KEY, "[]"))) + + def _update_relational_users_to_manage(self, new_users: Set[str]) -> None: + """Updates the set of the users to manage. + + Note json cannot serialise sets. Convert from list. + """ + if not self.charm.unit.is_leader(): + raise Exception("Cannot update relational data on non-leader unit") + + self.charm.app_peer_data[MANAGED_USERS_KEY] = json.dumps(list(new_users)) + + def _remove_from_relational_users_to_manage(self, user_to_remove: str) -> None: + """Removes the provided user from the set of the users to manage.""" + current_users = self._get_relational_users_to_manage() + updated_users = current_users - {user_to_remove} + self._update_relational_users_to_manage(updated_users) + + def _add_to_relational_users_to_manage(self, user_to_add: str) -> None: + """Adds the provided user to the set of the users to manage.""" + current_users = self._get_relational_users_to_manage() + current_users.add(user_to_add) + self._update_relational_users_to_manage(current_users) @staticmethod def _get_database_from_relation(relation: Relation) -> Optional[str]: diff --git a/src/charm.py b/src/charm.py index 445956858..c08d0d665 100755 --- a/src/charm.py +++ b/src/charm.py @@ -117,7 +117,7 @@ def __init__(self, *args): self.framework.observe(self.on.secret_remove, self._on_secret_remove) self.framework.observe(self.on.secret_changed, self._on_secret_changed) - self.client_relations = MongoDBProvider(self) + self.client_relations = MongoDBProvider(self, substrate=Config.SUBSTRATE) self.tls = MongoDBTLS(self, Config.Relations.PEERS, Config.SUBSTRATE) self.backups = MongoDBBackups(self) @@ -171,6 +171,11 @@ def peers_units(self) -> list[Unit]: else: return self._peers.units + @property + def mongo_config(self) -> MongoConfiguration: + """Returns a MongoConfiguration object for shared libs with agnostic mongo commands.""" + return self.mongodb_config + @property def mongodb_config(self) -> MongoConfiguration: """Create a configuration object with settings. @@ -339,7 +344,7 @@ def app_peer_data(self) -> RelationDataContent: @property def db_initialised(self) -> bool: """Check if MongoDB is initialised.""" - return "db_initialised" in self.app_peer_data + return json.loads(self.app_peer_data.get("db_initialised", "false")) def is_role_changed(self) -> bool: """Checks if application is running in provided role.""" @@ -367,7 +372,7 @@ def get_config_server_name(self) -> str | None: def db_initialised(self, value): """Set the db_initialised flag.""" if isinstance(value, bool): - self.app_peer_data["db_initialised"] = str(value) + self.app_peer_data["db_initialised"] = json.dumps(value) else: raise ValueError( f"'db_initialised' must be a boolean value. Provided: {value} is of type {type(value)}" @@ -381,13 +386,13 @@ def upgrade_in_progress(self): @property def replica_set_initialised(self) -> bool: """Check if the MongoDB replica set is initialised.""" - return "replica_set_initialised" in self.app_peer_data + return json.loads(self.app_peer_data.get("replica_set_initialised", "false")) @replica_set_initialised.setter def replica_set_initialised(self, value): """Set the replica_set_initialised flag.""" if isinstance(value, bool): - self.app_peer_data["replica_set_initialised"] = str(value) + self.app_peer_data["replica_set_initialised"] = json.dumps(value) else: raise ValueError( f"'replica_set_initialised' must be a boolean value. Proivded: {value} is of type {type(value)}" @@ -396,13 +401,13 @@ def replica_set_initialised(self, value): @property def users_initialized(self) -> bool: """Check if MongoDB users are created.""" - return "users_initialized" in self.app_peer_data + return json.loads(self.app_peer_data.get("users_initialized", "false")) @users_initialized.setter def users_initialized(self, value): """Set the users_initialized flag.""" if isinstance(value, bool): - self.app_peer_data["users_initialized"] = str(value) + self.app_peer_data["users_initialized"] = json.dumps(value) else: raise ValueError( f"'users_initialized' must be a boolean value. Proivded: {value} is of type {type(value)}" @@ -590,7 +595,7 @@ def _on_config_changed(self, event: ConfigChangedEvent) -> None: return logger.error( - f"cluster migration currently not supported, cannot change from { self.model.config['role']} to {self.role}" + f"cluster migration currently not supported, cannot change from {self.model.config['role']} to {self.role}" ) raise ShardingMigrationError( f"Migration of sharding components not permitted, revert config role to {self.role}" @@ -892,8 +897,10 @@ def _initialise_users(self, event: StartEvent) -> None: self._init_operator_user() self._init_backup_user() self._init_monitor_user() - logger.info("Reconcile relations") - self.client_relations.oversee_users(None, event) + # Bare replicas can create users or config-servers for related mongos apps + if not self.is_role(Config.Role.SHARD): + logger.info("Manage users") + self.client_relations.oversee_users(None, event) self.users_initialized = True except ExecError as e: logger.error("Deferring on_start: exit code: %i, stderr: %s", e.exit_code, e.stderr) diff --git a/tests/integration/relation_tests/helpers.py b/tests/integration/relation_tests/helpers.py index 9b5c1f82b..e771e28e1 100644 --- a/tests/integration/relation_tests/helpers.py +++ b/tests/integration/relation_tests/helpers.py @@ -3,6 +3,8 @@ # See LICENSE file for licensing details. import json +from pymongo import MongoClient +from pymongo.errors import OperationFailure from pytest_operator.plugin import OpsTest from tenacity import ( RetryError, @@ -13,7 +15,9 @@ wait_fixed, ) -from ..helpers import get_application_relation_data +from ..helpers import get_address_of_unit, get_application_relation_data + +AUTH_FAILED_CODE = 18 async def verify_application_data( @@ -68,3 +72,24 @@ async def get_connection_string( first_relation_user_data = await get_secret_data(ops_test, secret_uri) return first_relation_user_data.get("uris") + + +async def assert_created_user_can_connect( + ops_test: OpsTest, db_app_name: str, username: str, password: str, database: str +): + """Verifies that the provided username can connect to the DB with the given password.""" + # hosts = [unit.public_address for unit in ops_test.model.applications[database_name].units] + # hosts = ",".join(hosts) + host = await get_address_of_unit(ops_test, unit_id=0, app_name=db_app_name) + + connection_string = f"mongodb://{username}:{password}@{host}/{database}" + client = MongoClient(connection_string, directConnection=True) + try: + client.admin.command("ping") + except OperationFailure as e: + if e.code == AUTH_FAILED_CODE: + assert False, "user does not have access to MongoDB" + + raise + + return True diff --git a/tests/integration/relation_tests/test_charm_relations.py b/tests/integration/relation_tests/test_charm_relations.py index ef3be0415..60825deac 100644 --- a/tests/integration/relation_tests/test_charm_relations.py +++ b/tests/integration/relation_tests/test_charm_relations.py @@ -15,6 +15,7 @@ from ..ha_tests.helpers import get_replica_set_primary as replica_set_primary from ..helpers import check_or_scale_app, get_app_name, is_relation_joined, run_mongo_op from .helpers import ( + assert_created_user_can_connect, get_application_relation_data, get_connection_string, verify_application_data, @@ -36,6 +37,8 @@ APP_NAMES = [APPLICATION_APP_NAME, DATABASE_APP_NAME, ANOTHER_DATABASE_APP_NAME] TEST_APP_CHARM_PATH = "./tests/integration/relation_tests/application-charm" REQUIRED_UNITS = 2 +USER_CREATED_FROM_APP1 = "test_user_1" +PW_CREATED_FROM_APP1 = "test_user_pass_1" @pytest.mark.group(1) @@ -303,7 +306,7 @@ async def test_user_with_extra_roles(ops_test: OpsTest): ops_test, APPLICATION_APP_NAME, FIRST_DATABASE_RELATION_NAME, "database" ) - cmd = f'db.createUser({{user: "newTestUser", pwd: "Test123", roles: [{{role: "readWrite", db: "{database}"}}]}})' + cmd = f'db.createUser({{user: "{USER_CREATED_FROM_APP1}", pwd: "{PW_CREATED_FROM_APP1}", roles: [{{role: "readWrite", db: "{database}"}}]}})' result = await run_mongo_op( ops_test, cmd, @@ -319,11 +322,19 @@ async def test_user_with_extra_roles(ops_test: OpsTest): f'"{connection_string}"', stringify=True, ) - assert result.data["users"][0]["_id"] == "application_first_database.newTestUser" + assert result.data["users"][0]["_id"] == f"application_first_database.{USER_CREATED_FROM_APP1}" - cmd = 'db = db.getSiblingDB("new_database"); EJSON.stringify(db.test_collection.insertOne({"test": "one"}));' - result = await run_mongo_op(ops_test, cmd, f'"{connection_string}"', stringify=False) - assert result.data["acknowledged"] is True + db_app_name = ( + await get_app_name(ops_test, test_deployments=[ANOTHER_DATABASE_APP_NAME]) + or DATABASE_APP_NAME + ) + await assert_created_user_can_connect( + ops_test, + username=USER_CREATED_FROM_APP1, + password=PW_CREATED_FROM_APP1, + db_app_name=db_app_name, + database="application_first_database", + ) @pytest.mark.group(1) @@ -495,3 +506,16 @@ async def test_removed_relation_no_longer_has_access(ops_test: OpsTest): assert ( removed_access ), "application: {APPLICATION_APP_NAME} still has access to mongodb after relation removal." + + # mongodb should not clean up users it does not manage. + db_app_name = ( + await get_app_name(ops_test, test_deployments=[ANOTHER_DATABASE_APP_NAME]) + or DATABASE_APP_NAME + ) + await assert_created_user_can_connect( + ops_test, + username=USER_CREATED_FROM_APP1, + password=PW_CREATED_FROM_APP1, + db_app_name=db_app_name, + database="application_first_database", + ) diff --git a/tests/unit/test_charm.py b/tests/unit/test_charm.py index dc7e12257..cd9b2e7be 100644 --- a/tests/unit/test_charm.py +++ b/tests/unit/test_charm.py @@ -1,5 +1,6 @@ # Copyright 2024 Canonical Ltd. # See LICENSE file for licensing details. +import json import logging import re import unittest @@ -329,9 +330,8 @@ def test_start_already_initialised(self, connection, init_user, provider, defer) mock_container.return_value.can_connect.return_value = True mock_container.return_value.exists.return_value = True self.harness.charm.unit.get_container = mock_container - - self.harness.charm.app_peer_data["replica_set_initialised"] = "True" - self.harness.charm.app_peer_data["users_initialized"] = "True" + self.harness.charm.app_peer_data["replica_set_initialised"] = json.dumps(True) + self.harness.charm.app_peer_data["users_initialized"] = json.dumps(True) self.harness.charm.on.start.emit() @@ -524,7 +524,7 @@ def test_reconfigure_get_members_failure(self, client, connection, defer): """ # presets self.harness.set_leader(True) - self.harness.charm.app_peer_data["db_initialised"] = "True" + self.harness.charm.app_peer_data["db_initialised"] = json.dumps(True) rel = self.harness.charm.model.get_relation("database-peers") for exception, _ in PYMONGO_EXCEPTIONS: @@ -559,7 +559,7 @@ def test_reconfigure_remove_member_failure(self, connection, defer): """ # presets self.harness.set_leader(True) - self.harness.charm.app_peer_data["db_initialised"] = "True" + self.harness.charm.app_peer_data["db_initialised"] = json.dumps(True) connection.return_value.__enter__.return_value.get_replset_members.return_value = { "mongodb-k8s-0.mongodb-k8s-endpoints", "mongodb-k8s-1.mongodb-k8s-endpoints", @@ -593,7 +593,7 @@ def test_reconfigure_peer_not_ready(self, connection, defer): """ # presets self.harness.set_leader(True) - self.harness.charm.app_peer_data["db_initialised"] = "True" + self.harness.charm.app_peer_data["db_initialised"] = json.dumps(True) connection.return_value.__enter__.return_value.get_replset_members.return_value = { "mongodb-k8s-0.mongodb-k8s-endpoints" } @@ -617,7 +617,7 @@ def test_reconfigure_add_member_failure(self, connection, defer): """ # presets self.harness.set_leader(True) - self.harness.charm.app_peer_data["db_initialised"] = "True" + self.harness.charm.app_peer_data["db_initialised"] = json.dumps(True) connection.return_value.__enter__.return_value.get_replset_members.return_value = { "mongodb-k8s-0.mongodb-k8s-endpoints" } @@ -662,7 +662,7 @@ def test_start_init_operator_user_after_second_call(self, connection, oversee_us oversee_users.side_effect = PyMongoError() - self.harness.charm.app_peer_data["replica_set_initialised"] = "True" + self.harness.charm.app_peer_data["replica_set_initialised"] = json.dumps(True) self.harness.charm.on.start.emit() self.assertEqual("operator-user-created" in self.harness.charm.app_peer_data, True) defer.assert_called() @@ -957,7 +957,7 @@ def test__connect_mongodb_exporter_success( container.make_dir("/etc/logrotate.d", make_parents=True) self.harness.set_can_connect(container, True) - self.harness.charm.app_peer_data["db_initialised"] = "True" + self.harness.charm.app_peer_data["db_initialised"] = json.dumps(True) self.harness.charm.on.mongod_pebble_ready.emit(container) password = self.harness.charm.get_secret("app", "monitor-password") @@ -1022,7 +1022,7 @@ def test__backup_user_created( """Tests what backup user was created.""" self.harness.charm._initialise_users.retry.wait = wait_none() container = self.harness.model.unit.get_container("mongod") - self.harness.charm.app_peer_data["replica_set_initialised"] = "True" + self.harness.charm.app_peer_data["replica_set_initialised"] = json.dumps(True) self.harness.set_can_connect(container, True) self.harness.charm.on.start.emit() password = self.harness.charm.get_secret("app", "backup-password") diff --git a/tests/unit/test_mongodb_backups.py b/tests/unit/test_mongodb_backups.py index f3efc27c5..8a25593f0 100644 --- a/tests/unit/test_mongodb_backups.py +++ b/tests/unit/test_mongodb_backups.py @@ -1,5 +1,6 @@ # Copyright 2024 Canonical Ltd. # See LICENSE file for licensing details. +import json import unittest from unittest import mock from unittest.mock import patch @@ -277,7 +278,7 @@ def test_s3_credentials_set_pbm_failure(self, _set_config_options, service): service.return_value = "pbm" _set_config_options.side_effect = SetPBMConfigError - self.harness.charm.app_peer_data["db_initialised"] = "True" + self.harness.charm.app_peer_data["db_initialised"] = json.dumps(True) # triggering s3 event with correct fields mock_s3_info = mock.Mock() @@ -305,7 +306,7 @@ def test_s3_credentials_config_error( """Test charm defers when more time is needed to sync pbm.""" container = self.harness.model.unit.get_container("mongod") self.harness.set_can_connect(container, True) - self.harness.charm.app_peer_data["db_initialised"] = "True" + self.harness.charm.app_peer_data["db_initialised"] = json.dumps(True) service.return_value = "pbm" pbm_status.return_value = ActiveStatus() resync.side_effect = SetPBMConfigError @@ -333,7 +334,7 @@ def test_s3_credentials_syncing(self, pbm_status, service, defer, resync, _set_c """Test charm defers when more time is needed to sync pbm credentials.""" container = self.harness.model.unit.get_container("mongod") self.harness.set_can_connect(container, True) - self.harness.charm.app_peer_data["db_initialised"] = "True" + self.harness.charm.app_peer_data["db_initialised"] = json.dumps(True) service.return_value = "pbm" resync.side_effect = ResyncError @@ -364,7 +365,7 @@ def test_s3_credentials_pbm_busy( """Test charm defers when more time is needed to sync pbm.""" container = self.harness.model.unit.get_container("mongod") self.harness.set_can_connect(container, True) - self.harness.charm.app_peer_data["db_initialised"] = "True" + self.harness.charm.app_peer_data["db_initialised"] = json.dumps(True) service.return_value = "pbm" resync.side_effect = PBMBusyError @@ -397,7 +398,7 @@ def test_s3_credentials_pbm_error( container = self.harness.model.unit.get_container("mongod") self.harness.set_can_connect(container, True) service.return_value = "pbm" - self.harness.charm.app_peer_data["db_initialised"] = "True" + self.harness.charm.app_peer_data["db_initialised"] = json.dumps(True) resync.side_effect = ExecError( command=["/usr/bin/pbm status"], exit_code=1, stdout="status code: 403", stderr="" ) diff --git a/tests/unit/test_mongodb_provider.py b/tests/unit/test_mongodb_provider.py index a09ab0504..53fbf7e59 100644 --- a/tests/unit/test_mongodb_provider.py +++ b/tests/unit/test_mongodb_provider.py @@ -1,6 +1,7 @@ # Copyright 2024 Canonical Ltd. # See LICENSE file for licensing details. +import json import unittest from unittest import mock from unittest.mock import patch @@ -67,7 +68,7 @@ def test_relation_event_oversee_users_mongo_failure(self, oversee_users, defer): """Tests the errors related to pymongo when overseeing users result in a defer.""" # presets self.harness.set_leader(True) - self.harness.charm.app_peer_data["db_initialised"] = "True" + self.harness.charm.app_peer_data["db_initialised"] = json.dumps(True) relation_id = self.harness.add_relation("database", "consumer") for exception, expected_raise in PYMONGO_EXCEPTIONS: @@ -91,7 +92,7 @@ def test_relation_event_oversee_users_fails_to_get_relation(self, oversee_users, """Verifies that when users are formatted incorrectly an assertion error is raised.""" # presets self.harness.set_leader(True) - self.harness.charm.app_peer_data["db_initialised"] = "True" + self.harness.charm.app_peer_data["db_initialised"] = json.dumps(True) relation_id = self.harness.add_relation("database", "consumer") # AssertionError is raised when unable to attain users from relation (due to name @@ -107,7 +108,7 @@ def test_relation_event_oversee_users_fails_to_get_relation(self, oversee_users, self.harness.remove_relation_unit(relation_id, "consumer/0") @patch_network_get(private_address="1.1.1.1") - @patch("charms.mongodb.v1.mongodb_provider.MongoDBConnection") + @patch("charms.mongodb.v1.mongodb_provider.MongoConnection") def test_oversee_users_get_users_failure(self, connection): """Verifies that when unable to retrieve users from mongod an exception is raised.""" for dep_id in DEPARTED_IDS: @@ -120,7 +121,7 @@ def test_oversee_users_get_users_failure(self, connection): @patch_network_get(private_address="1.1.1.1") @patch("charm.MongoDBProvider._get_users_from_relations") - @patch("charms.mongodb.v1.mongodb_provider.MongoDBConnection") + @patch("charms.mongodb.v1.mongodb_provider.MongoConnection") def test_oversee_users_drop_user_failure(self, connection, relation_users): """Verifies that when unable to drop users from mongod an exception is raised.""" # presets, such that there is a need to drop users. @@ -129,7 +130,9 @@ def test_oversee_users_drop_user_failure(self, connection, relation_users): "relation-user1", "relation-user2", } - + self.harness.charm.app_peer_data["managed-users-key"] = json.dumps( + ["relation-user1", "relation-user2"] + ) for dep_id in DEPARTED_IDS: for exception, expected_raise in PYMONGO_EXCEPTIONS: connection.return_value.__enter__.return_value.drop_user.side_effect = exception @@ -140,7 +143,7 @@ def test_oversee_users_drop_user_failure(self, connection, relation_users): @patch_network_get(private_address="1.1.1.1") @patch("charm.MongoDBProvider._get_users_from_relations") - @patch("charms.mongodb.v1.mongodb_provider.MongoDBConnection") + @patch("charms.mongodb.v1.mongodb_provider.MongoConnection") def test_oversee_users_get_config_failure(self, connection, relation_users): """Verifies that when users do not match necessary schema an AssertionError is raised.""" # presets, such that the need to create user relations is triggered. Further presets @@ -159,7 +162,7 @@ def test_oversee_users_get_config_failure(self, connection, relation_users): @patch("charm.MongoDBProvider._set_relation") @patch("charm.MongoDBProvider._get_config") @patch("charm.MongoDBProvider._get_users_from_relations") - @patch("charms.mongodb.v1.mongodb_provider.MongoDBConnection") + @patch("charms.mongodb.v1.mongodb_provider.MongoConnection") @patch("charm.MongoDBProvider._diff") def test_oversee_users_no_config_database( self, diff, connection, relation_users, get_config, set_relation @@ -182,7 +185,7 @@ def test_oversee_users_no_config_database( @patch("charm.MongoDBProvider._set_relation") @patch("charm.MongoDBProvider._get_config") @patch("charm.MongoDBProvider._get_users_from_relations") - @patch("charms.mongodb.v1.mongodb_provider.MongoDBConnection") + @patch("charms.mongodb.v1.mongodb_provider.MongoConnection") def test_oversee_users_create_user_failure( self, connection, relation_users, get_config, set_relation ): @@ -203,7 +206,7 @@ def test_oversee_users_create_user_failure( @patch_network_get(private_address="1.1.1.1") @patch("charm.MongoDBProvider._get_config") @patch("charm.MongoDBProvider._get_users_from_relations") - @patch("charms.mongodb.v1.mongodb_provider.MongoDBConnection") + @patch("charms.mongodb.v1.mongodb_provider.MongoConnection") def test_oversee_users_set_relation_failure(self, connection, relation_users, get_config): """Verifies that when adding a user with an invalid name that an exception is raised.""" # presets, such that the need to create user relations is triggered and user naming such @@ -222,7 +225,7 @@ def test_oversee_users_set_relation_failure(self, connection, relation_users, ge @patch_network_get(private_address="1.1.1.1") @patch("charm.MongoDBProvider._get_users_from_relations") - @patch("charms.mongodb.v1.mongodb_provider.MongoDBConnection") + @patch("charms.mongodb.v1.mongodb_provider.MongoConnection") def test_oversee_users_update_get_config_failure(self, connection, relation_users): """Verifies that when updating a user with an invalid name that an exception is raised.""" # presets, such that the need to update user relations is triggered and user naming such @@ -240,12 +243,13 @@ def test_oversee_users_update_get_config_failure(self, connection, relation_user @patch_network_get(private_address="1.1.1.1") @patch("charm.MongoDBProvider._get_config") @patch("charm.MongoDBProvider._get_users_from_relations") - @patch("charms.mongodb.v1.mongodb_provider.MongoDBConnection") + @patch("charms.mongodb.v1.mongodb_provider.MongoConnection") def test_oversee_users_update_user_failure(self, connection, relation_users, get_config): """Verifies that when updating users fails an exception is raised.""" # presets, such that the need to update user relations is triggered relation_users.return_value = {"relation-user1"} connection.return_value.__enter__.return_value.get_users.return_value = {"relation-user1"} + self.harness.charm.app_peer_data["managed-users-key"] = json.dumps(["relation-user1"]) for dep_id in DEPARTED_IDS: for exception, expected_raise in PYMONGO_EXCEPTIONS: @@ -259,7 +263,7 @@ def test_oversee_users_update_user_failure(self, connection, relation_users, get @patch_network_get(private_address="1.1.1.1") @patch("charm.MongoDBProvider._get_databases_from_relations") @patch("charm.MongoDBProvider._get_users_from_relations") - @patch("charms.mongodb.v1.mongodb_provider.MongoDBConnection") + @patch("charms.mongodb.v1.mongodb_provider.MongoConnection") def test_oversee_users_no_auto_delete( self, connection, relation_users, databases_from_relations ): @@ -276,7 +280,7 @@ def test_oversee_users_no_auto_delete( @patch_network_get(private_address="1.1.1.1") @patch("charm.MongoDBProvider._get_users_from_relations") - @patch("charms.mongodb.v1.mongodb_provider.MongoDBConnection") + @patch("charms.mongodb.v1.mongodb_provider.MongoConnection") def test_oversee_users_mongo_databases_failure(self, connection, relation_users): """Verifies failures in checking for databases with mongod result in raised exceptions.""" self.harness.update_config({"auto-delete": True}) @@ -294,7 +298,7 @@ def test_oversee_users_mongo_databases_failure(self, connection, relation_users) @patch_network_get(private_address="1.1.1.1") @patch("charm.MongoDBProvider._get_databases_from_relations") @patch("charm.MongoDBProvider._get_users_from_relations") - @patch("charms.mongodb.v1.mongodb_provider.MongoDBConnection") + @patch("charms.mongodb.v1.mongodb_provider.MongoConnection") def test_oversee_users_drop_database_failure( self, connection, relation_users, databases_from_relations ):