From 8cad660646873e758d274efc434a2fd7c7f873fe Mon Sep 17 00:00:00 2001 From: Mia Altieri <32723809+MiaAltieri@users.noreply.github.com> Date: Fri, 23 Aug 2024 17:15:33 +0200 Subject: [PATCH] [DPE-5090] - Update mongodb provider to support K8s router (#462) ## Issue mongos k8s router cannot yet make use of the mongodb_provider lib due to the lib not be flexible to support mongos ## Solution Update the lib so that it can support mongos --------- Co-authored-by: Neha Oudin <17551419+Gu1nness@users.noreply.github.com> Co-authored-by: Mehdi Bendriss --- .../mongodb/v0/config_server_interface.py | 4 +- lib/charms/mongodb/v1/mongodb_provider.py | 101 ++++++++++-------- src/charm.py | 5 + tests/unit/test_mongodb_provider.py | 22 ++-- 4 files changed, 74 insertions(+), 58 deletions(-) diff --git a/lib/charms/mongodb/v0/config_server_interface.py b/lib/charms/mongodb/v0/config_server_interface.py index ba515f3e0..1b5b3b897 100644 --- a/lib/charms/mongodb/v0/config_server_interface.py +++ b/lib/charms/mongodb/v0/config_server_interface.py @@ -42,7 +42,7 @@ # Increment this PATCH version before using `charmcraft publish-lib` or reset # to 0 if you are raising the major API version -LIBPATCH = 9 +LIBPATCH = 10 class ClusterProvider(Object): @@ -328,6 +328,8 @@ def _on_relation_broken(self, event: RelationBrokenEvent) -> None: # K8s charm have a 1:Many client scheme and share connection info in a different manner. if self.substrate == Config.Substrate.VM: self.charm.remove_connection_info() + else: + self.db_initialised = False # BEGIN: helper functions def pass_hook_checks(self, event): diff --git a/lib/charms/mongodb/v1/mongodb_provider.py b/lib/charms/mongodb/v1/mongodb_provider.py index c9bd68d47..8fee64c0f 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,14 @@ # Increment this PATCH version before using `charmcraft publish-lib` or reset # to 0 if you are raising the major API version -LIBPATCH = 10 +LIBPATCH = 11 logger = logging.getLogger(__name__) REL_NAME = "database" - MONGOS_RELATIONS = "cluster" +MONGOS_CLIENT_RELATIONS = "mongos_proxy" # 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__ = """ @@ -62,24 +59,19 @@ 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 ) @@ -91,7 +83,9 @@ def pass_hook_checks(self, event: EventBase) -> bool: 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 @@ -164,12 +158,18 @@ def oversee_users(self, departed_relation_id: Optional[int], event): 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. """ - 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: 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) for username in relation_users - database_users: @@ -245,10 +245,10 @@ def update_app_relation_data(self) -> None: 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) @@ -290,16 +290,21 @@ 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 + 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 +323,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 +338,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 +357,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 +376,25 @@ 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 @staticmethod def _get_database_from_relation(relation: Relation) -> Optional[str]: diff --git a/src/charm.py b/src/charm.py index 0690ff78d..b63047202 100755 --- a/src/charm.py +++ b/src/charm.py @@ -257,6 +257,11 @@ def remote_mongodb_config(self, hosts, replset=None, standalone=None) -> MongoCo OperatorUser, hosts, replset=replset, standalone=standalone ) + @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: """Generates a MongoConfiguration object for this deployment of MongoDB.""" diff --git a/tests/unit/test_mongodb_provider.py b/tests/unit/test_mongodb_provider.py index 581eee1fb..493bb3d18 100644 --- a/tests/unit/test_mongodb_provider.py +++ b/tests/unit/test_mongodb_provider.py @@ -130,7 +130,7 @@ def test_relation_event_oversee_users_fails_to_get_relation( 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: @@ -143,7 +143,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. @@ -163,7 +163,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 @@ -182,7 +182,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 @@ -205,7 +205,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 ): @@ -226,7 +226,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 @@ -245,7 +245,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 @@ -263,7 +263,7 @@ 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 @@ -282,7 +282,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 ): @@ -299,7 +299,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}) @@ -317,7 +317,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 ):