Skip to content

Commit 488bb06

Browse files
Recover from hook errors when creating/deleting MySQL users (#112)
Ported from canonical/mysql-router-k8s-operator#165
1 parent d11f8ed commit 488bb06

File tree

3 files changed

+58
-32
lines changed

3 files changed

+58
-32
lines changed

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")

src/relations/deprecated_shared_db_database_provides.py

Lines changed: 25 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -140,18 +140,26 @@ def create_database_and_user(
140140
shell: mysql_shell.Shell,
141141
) -> None:
142142
"""Create database & user and update databag."""
143+
# Delete user if exists
144+
# (If the user was previously created by this charm—but the hook failed—the user will
145+
# persist in MySQL but will not persist in the databag. Therefore, we lose the user's
146+
# password and need to re-create the user.)
147+
logger.debug("Deleting user if exists before creating user")
148+
shell.delete_user(self._username, must_exist=False)
149+
logger.debug("Deleted user if exists before creating user")
150+
143151
password = shell.create_application_database_and_user(
144152
username=self._username, database=self._database
145153
)
146154
self._peer_app_databag[self.peer_databag_password_key] = password
147155
self.set_databag(password=password)
148156

149157

150-
class _UserNotCreated(Exception):
158+
class _UserNotShared(Exception):
151159
"""Database & user has not been provided to related application charm"""
152160

153161

154-
class _RelationWithCreatedUser(_Relation):
162+
class _RelationWithSharedUser(_Relation):
155163
"""Related application charm that has been provided with a database & user"""
156164

157165
def __init__(
@@ -163,7 +171,7 @@ def __init__(
163171
super().__init__(relation=relation, peer_relation_app_databag=peer_relation_app_databag)
164172
for key in (self._peer_databag_username_key, self.peer_databag_password_key):
165173
if key not in self._peer_app_databag:
166-
raise _UserNotCreated
174+
raise _UserNotShared
167175

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

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

242253
@property
243254
# TODO python3.10 min version: Use `list` instead of `typing.List`
244-
def _created_users(self) -> typing.List[_RelationWithCreatedUser]:
245-
created_users = []
255+
def _shared_users(self) -> typing.List[_RelationWithSharedUser]:
256+
shared_users = []
246257
for relation in self._relations:
247258
try:
248-
created_users.append(
249-
_RelationWithCreatedUser(
259+
shared_users.append(
260+
_RelationWithSharedUser(
250261
relation=relation,
251262
peer_relation_app_databag=self._peer_app_databag,
252263
)
253264
)
254-
except _UserNotCreated:
265+
except _UserNotShared:
255266
pass
256-
return created_users
267+
return shared_users
257268

258269
def reconcile_users(
259270
self,
@@ -284,13 +295,13 @@ def reconcile_users(
284295
remote_databag.IncompleteDatabag,
285296
):
286297
pass
287-
logger.debug(f"State of reconcile users {requested_users=}, {self._created_users=}")
298+
logger.debug(f"State of reconcile users {requested_users=}, {self._shared_users=}")
288299
for relation in requested_users:
289-
if relation not in self._created_users:
300+
if relation not in self._shared_users:
290301
relation.create_database_and_user(
291302
shell=shell,
292303
)
293-
for relation in self._created_users:
304+
for relation in self._shared_users:
294305
if relation not in requested_users:
295306
relation.delete_user(shell=shell)
296307
logger.debug(f"Reconciled users {event=}")
@@ -304,7 +315,7 @@ def delete_all_databags(self) -> None:
304315
will need to be created.
305316
"""
306317
logger.debug("Deleting all application databags")
307-
for relation in self._created_users:
318+
for relation in self._shared_users:
308319
# MySQL charm will delete user; just delete databag
309320
relation.delete_databag()
310321
logger.debug("Deleted all application databags")

0 commit comments

Comments
 (0)