Skip to content

Commit

Permalink
update lib
Browse files Browse the repository at this point in the history
  • Loading branch information
MiaAltieri committed Sep 12, 2024
1 parent f3a0228 commit 8938c34
Show file tree
Hide file tree
Showing 2 changed files with 121 additions and 10 deletions.
120 changes: 110 additions & 10 deletions lib/charms/mongodb/v1/mongodb_provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,13 @@

# Increment this PATCH version before using `charmcraft publish-lib` or reset
# to 0 if you are raising the major API version
LIBPATCH = 11
LIBPATCH = 13

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

Expand All @@ -48,6 +49,10 @@
deleted — key that were deleted."""


class FailedToGetHostsError(Exception):
"""Raised when charm fails to retrieve hosts."""


class MongoDBProvider(Object):
"""In this class, we manage client database relations."""

Expand Down Expand Up @@ -76,8 +81,8 @@ def __init__(self, charm: CharmBase, substrate="k8s", relation_name: str = REL_N
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:
Expand All @@ -92,6 +97,13 @@ def pass_hook_checks(self, event: EventBase) -> bool:
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."
Expand Down Expand Up @@ -137,7 +149,11 @@ def _on_relation_event(self, event):

try:
self.oversee_users(departed_relation_id, event)
except PyMongoError as e:
except (PyMongoError, FailedToGetHostsError) as e:
# Failed to get hosts error is unique to mongos-k8s charm. In other charms we do not
# foresee issues to retrieve hosts. However in external mongos-k8s, the leader can
# attempt to retrieve hosts while non-leader units are still enabling node port
# resulting in an exception.
logger.error("Deferring _on_relation_event since: error=%r", e)
event.defer()
return
Expand All @@ -157,12 +173,36 @@ 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 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)
Expand All @@ -171,8 +211,16 @@ def oversee_users(self, departed_relation_id: Optional[int], event):
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
Expand All @@ -181,15 +229,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

Expand Down Expand Up @@ -240,7 +306,7 @@ 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()
Expand Down Expand Up @@ -282,7 +348,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:
Expand All @@ -299,8 +367,11 @@ def _get_config(self, username: str, password: Optional[str]) -> MongoConfigurat
"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

Expand Down Expand Up @@ -396,6 +467,35 @@ def get_relation_name(self):
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]:
"""Return database name from relation."""
Expand Down
11 changes: 11 additions & 0 deletions src/charm.py
Original file line number Diff line number Diff line change
Expand Up @@ -448,6 +448,17 @@ def get_units(self) -> List[Unit]:
units.extend(self.peers_units)
return units

def get_mongos_hosts_for_client(self) -> Set:
"""Returns the hosts for mongos as a str.
The host for mongos can be either the K8s pod name or an IP address depending on how
the app has been configured.
"""
if self.is_external_client:
return self.get_ext_mongos_hosts()

return self.get_k8s_mongos_hosts()

def get_k8s_mongos_hosts(self) -> Set:
"""Returns the K8s hosts for mongos"""
hosts = set()
Expand Down

0 comments on commit 8938c34

Please sign in to comment.