Skip to content

Commit

Permalink
Recover from hook errors when creating/deleting MySQL users
Browse files Browse the repository at this point in the history
  • Loading branch information
carlcsaposs-canonical committed Mar 5, 2024
1 parent d11f8ed commit 21d100d
Show file tree
Hide file tree
Showing 3 changed files with 58 additions and 32 deletions.
12 changes: 8 additions & 4 deletions src/mysql_shell.py
Original file line number Diff line number Diff line change
Expand Up @@ -164,11 +164,15 @@ def remove_router_from_cluster_metadata(self, router_id: str) -> None:
)
logger.debug(f"Removed {router_id=} from cluster metadata")

def delete_user(self, username: str) -> None:
def delete_user(self, username: str, *, must_exist=True) -> None:
"""Delete user."""
logger.debug(f"Deleting {username=}")
self._run_sql([f"DROP USER `{username}`"])
logger.debug(f"Deleted {username=}")
logger.debug(f"Deleting {username=} {must_exist=}")
if must_exist:
statement = f"DROP USER `{username}`"
else:
statement = f"DROP USER IF EXISTS `{username}`"
self._run_sql([statement])
logger.debug(f"Deleted {username=} {must_exist=}")

def is_router_in_cluster_set(self, router_id: str) -> bool:
"""Check if MySQL Router is part of InnoDB ClusterSet."""
Expand Down
39 changes: 25 additions & 14 deletions src/relations/database_provides.py
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,14 @@ def create_database_and_user(
) -> None:
"""Create database & user and update databag."""
username = self._get_username(shell.username)
# Delete user if exists
# (If the user was previously created by this charm—but the hook failed—the user will
# persist in MySQL but will not persist in the databag. Therefore, we lose the user's
# password and need to re-create the user.)
logger.debug("Deleting user if exists before creating user")
shell.delete_user(username, must_exist=False)
logger.debug("Deleted user if exists before creating user")

password = shell.create_application_database_and_user(
username=username, database=self._database
)
Expand All @@ -115,11 +123,11 @@ def create_database_and_user(
)


class _UserNotCreated(Exception):
class _UserNotShared(Exception):
"""Database & user has not been provided to related application charm"""


class _RelationWithCreatedUser(_Relation):
class _RelationWithSharedUser(_Relation):
"""Related application charm that has been provided with a database & user"""

def __init__(
Expand All @@ -130,7 +138,7 @@ def __init__(
self._local_databag = self._interface.fetch_my_relation_data([relation.id])[relation.id]
for key in ("database", "username", "password", "endpoints", "read-only-endpoints"):
if key not in self._local_databag:
raise _UserNotCreated
raise _UserNotShared

def delete_databag(self) -> None:
"""Remove connection information from databag."""
Expand All @@ -141,7 +149,10 @@ def delete_databag(self) -> None:
def delete_user(self, *, shell: mysql_shell.Shell) -> None:
"""Delete user and update databag."""
self.delete_databag()
shell.delete_user(self._get_username(shell.username))
# Delete user if exists
# (If the user was previously deleted by this charm—but the hook failed—the user will be
# deleted in MySQL but will persist in the databag.)
shell.delete_user(self._get_username(shell.username), must_exist=False)


class RelationEndpoint:
Expand All @@ -157,16 +168,16 @@ def __init__(self, charm_: "abstract_charm.MySQLRouterCharm") -> None:

@property
# TODO python3.10 min version: Use `list` instead of `typing.List`
def _created_users(self) -> typing.List[_RelationWithCreatedUser]:
created_users = []
def _shared_users(self) -> typing.List[_RelationWithSharedUser]:
shared_users = []
for relation in self._interface.relations:
try:
created_users.append(
_RelationWithCreatedUser(relation=relation, interface=self._interface)
shared_users.append(
_RelationWithSharedUser(relation=relation, interface=self._interface)
)
except _UserNotCreated:
except _UserNotShared:
pass
return created_users
return shared_users

def reconcile_users(
self,
Expand Down Expand Up @@ -199,15 +210,15 @@ def reconcile_users(
_UnsupportedExtraUserRole,
):
pass
logger.debug(f"State of reconcile users {requested_users=}, {self._created_users=}")
logger.debug(f"State of reconcile users {requested_users=}, {self._shared_users=}")
for relation in requested_users:
if relation not in self._created_users:
if relation not in self._shared_users:
relation.create_database_and_user(
router_read_write_endpoint=router_read_write_endpoint,
router_read_only_endpoint=router_read_only_endpoint,
shell=shell,
)
for relation in self._created_users:
for relation in self._shared_users:
if relation not in requested_users:
relation.delete_user(shell=shell)
logger.debug(
Expand All @@ -223,7 +234,7 @@ def delete_all_databags(self) -> None:
will need to be created.
"""
logger.debug("Deleting all application databags")
for relation in self._created_users:
for relation in self._shared_users:
# MySQL charm will delete user; just delete databag
relation.delete_databag()
logger.debug("Deleted all application databags")
Expand Down
39 changes: 25 additions & 14 deletions src/relations/deprecated_shared_db_database_provides.py
Original file line number Diff line number Diff line change
Expand Up @@ -140,18 +140,26 @@ def create_database_and_user(
shell: mysql_shell.Shell,
) -> None:
"""Create database & user and update databag."""
# Delete user if exists
# (If the user was previously created by this charm—but the hook failed—the user will
# persist in MySQL but will not persist in the databag. Therefore, we lose the user's
# password and need to re-create the user.)
logger.debug("Deleting user if exists before creating user")
shell.delete_user(self._username, must_exist=False)
logger.debug("Deleted user if exists before creating user")

password = shell.create_application_database_and_user(
username=self._username, database=self._database
)
self._peer_app_databag[self.peer_databag_password_key] = password
self.set_databag(password=password)


class _UserNotCreated(Exception):
class _UserNotShared(Exception):
"""Database & user has not been provided to related application charm"""


class _RelationWithCreatedUser(_Relation):
class _RelationWithSharedUser(_Relation):
"""Related application charm that has been provided with a database & user"""

def __init__(
Expand All @@ -163,7 +171,7 @@ def __init__(
super().__init__(relation=relation, peer_relation_app_databag=peer_relation_app_databag)
for key in (self._peer_databag_username_key, self.peer_databag_password_key):
if key not in self._peer_app_databag:
raise _UserNotCreated
raise _UserNotShared

def delete_databag(self) -> None:
"""Remove connection information from databag."""
Expand All @@ -176,7 +184,10 @@ def delete_user(self, *, shell: mysql_shell.Shell) -> None:
"""Delete user and update databag."""
username = self._peer_app_databag[self._peer_databag_username_key]
logger.debug(f"Deleting user {username=}")
shell.delete_user(username)
# Delete user if exists
# (If the user was previously deleted by this charm—but the hook failed—the user will be
# deleted in MySQL but will persist in the databag.)
shell.delete_user(username, must_exist=False)
logger.debug(f"Deleted user {username=}")
self.delete_databag()

Expand Down Expand Up @@ -241,19 +252,19 @@ def _update_unit_databag(self, _) -> None:

@property
# TODO python3.10 min version: Use `list` instead of `typing.List`
def _created_users(self) -> typing.List[_RelationWithCreatedUser]:
created_users = []
def _shared_users(self) -> typing.List[_RelationWithSharedUser]:
shared_users = []
for relation in self._relations:
try:
created_users.append(
_RelationWithCreatedUser(
shared_users.append(
_RelationWithSharedUser(
relation=relation,
peer_relation_app_databag=self._peer_app_databag,
)
)
except _UserNotCreated:
except _UserNotShared:
pass
return created_users
return shared_users

def reconcile_users(
self,
Expand Down Expand Up @@ -284,13 +295,13 @@ def reconcile_users(
remote_databag.IncompleteDatabag,
):
pass
logger.debug(f"State of reconcile users {requested_users=}, {self._created_users=}")
logger.debug(f"State of reconcile users {requested_users=}, {self._shared_users=}")
for relation in requested_users:
if relation not in self._created_users:
if relation not in self._shared_users:
relation.create_database_and_user(
shell=shell,
)
for relation in self._created_users:
for relation in self._shared_users:
if relation not in requested_users:
relation.delete_user(shell=shell)
logger.debug(f"Reconciled users {event=}")
Expand All @@ -304,7 +315,7 @@ def delete_all_databags(self) -> None:
will need to be created.
"""
logger.debug("Deleting all application databags")
for relation in self._created_users:
for relation in self._shared_users:
# MySQL charm will delete user; just delete databag
relation.delete_databag()
logger.debug("Deleted all application databags")
Expand Down

0 comments on commit 21d100d

Please sign in to comment.