Skip to content

Commit fad69b1

Browse files
Recover from hook errors when creating/deleting MySQL users (#165)
Fixes #157 Previously, the charm assumed that if a user was created or deleted that the databag would be updated accordingly. However, if a user is created or deleted and the event/hook fails, the user creation/deletion will go through for MySQL but the databag will not be updated (since the event failed). When the event is retried, the charm tried to create a user that already exists or delete a user that doesn't exist
1 parent fdde410 commit fad69b1

File tree

3 files changed

+33
-38
lines changed

3 files changed

+33
-38
lines changed

poetry.lock

Lines changed: 0 additions & 20 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/mysql_shell.py

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -164,11 +164,15 @@ def remove_router_from_cluster_metadata(self, router_id: str) -> None:
164164
)
165165
logger.debug(f"Removed {router_id=} from cluster metadata")
166166

167-
def delete_user(self, username: str) -> None:
167+
def delete_user(self, username: str, *, must_exist=True) -> None:
168168
"""Delete user."""
169-
logger.debug(f"Deleting {username=}")
170-
self._run_sql([f"DROP USER `{username}`"])
171-
logger.debug(f"Deleted {username=}")
169+
logger.debug(f"Deleting {username=} {must_exist=}")
170+
if must_exist:
171+
statement = f"DROP USER `{username}`"
172+
else:
173+
statement = f"DROP USER IF EXISTS `{username}`"
174+
self._run_sql([statement])
175+
logger.debug(f"Deleted {username=} {must_exist=}")
172176

173177
def is_router_in_cluster_set(self, router_id: str) -> bool:
174178
"""Check if MySQL Router is part of InnoDB ClusterSet."""

src/relations/database_provides.py

Lines changed: 25 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,14 @@ def create_database_and_user(
104104
) -> None:
105105
"""Create database & user and update databag."""
106106
username = self._get_username(shell.username)
107+
# Delete user if exists
108+
# (If the user was previously created by this charm—but the hook failed—the user will
109+
# persist in MySQL but will not persist in the databag. Therefore, we lose the user's
110+
# password and need to re-create the user.)
111+
logger.debug("Deleting user if exists before creating user")
112+
shell.delete_user(username, must_exist=False)
113+
logger.debug("Deleted user if exists before creating user")
114+
107115
password = shell.create_application_database_and_user(
108116
username=username, database=self._database
109117
)
@@ -115,11 +123,11 @@ def create_database_and_user(
115123
)
116124

117125

118-
class _UserNotCreated(Exception):
126+
class _UserNotShared(Exception):
119127
"""Database & user has not been provided to related application charm"""
120128

121129

122-
class _RelationWithCreatedUser(_Relation):
130+
class _RelationWithSharedUser(_Relation):
123131
"""Related application charm that has been provided with a database & user"""
124132

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

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

146157

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

158169
@property
159170
# TODO python3.10 min version: Use `list` instead of `typing.List`
160-
def _created_users(self) -> typing.List[_RelationWithCreatedUser]:
161-
created_users = []
171+
def _shared_users(self) -> typing.List[_RelationWithSharedUser]:
172+
shared_users = []
162173
for relation in self._interface.relations:
163174
try:
164-
created_users.append(
165-
_RelationWithCreatedUser(relation=relation, interface=self._interface)
175+
shared_users.append(
176+
_RelationWithSharedUser(relation=relation, interface=self._interface)
166177
)
167-
except _UserNotCreated:
178+
except _UserNotShared:
168179
pass
169-
return created_users
180+
return shared_users
170181

171182
def reconcile_users(
172183
self,
@@ -199,15 +210,15 @@ def reconcile_users(
199210
_UnsupportedExtraUserRole,
200211
):
201212
pass
202-
logger.debug(f"State of reconcile users {requested_users=}, {self._created_users=}")
213+
logger.debug(f"State of reconcile users {requested_users=}, {self._shared_users=}")
203214
for relation in requested_users:
204-
if relation not in self._created_users:
215+
if relation not in self._shared_users:
205216
relation.create_database_and_user(
206217
router_read_write_endpoint=router_read_write_endpoint,
207218
router_read_only_endpoint=router_read_only_endpoint,
208219
shell=shell,
209220
)
210-
for relation in self._created_users:
221+
for relation in self._shared_users:
211222
if relation not in requested_users:
212223
relation.delete_user(shell=shell)
213224
logger.debug(
@@ -223,7 +234,7 @@ def delete_all_databags(self) -> None:
223234
will need to be created.
224235
"""
225236
logger.debug("Deleting all application databags")
226-
for relation in self._created_users:
237+
for relation in self._shared_users:
227238
# MySQL charm will delete user; just delete databag
228239
relation.delete_databag()
229240
logger.debug("Deleted all application databags")

0 commit comments

Comments
 (0)