14
14
from typing import List , Optional , Set
15
15
16
16
from charms .data_platform_libs .v0 .data_interfaces import DatabaseProvides
17
+ from charms .mongodb .v0 .mongo import MongoConfiguration , MongoConnection
17
18
from charms .mongodb .v1 .helpers import generate_password
18
- from charms .mongodb .v1 .mongodb import MongoConfiguration , MongoDBConnection
19
19
from ops .charm import CharmBase , EventBase , RelationBrokenEvent , RelationChangedEvent
20
20
from ops .framework import Object
21
21
from ops .model import Relation
31
31
32
32
# Increment this PATCH version before using `charmcraft publish-lib` or reset
33
33
# to 0 if you are raising the major API version
34
- LIBPATCH = 10
34
+ LIBPATCH = 14
35
35
36
36
logger = logging .getLogger (__name__ )
37
37
REL_NAME = "database"
38
-
39
38
MONGOS_RELATIONS = "cluster"
39
+ MONGOS_CLIENT_RELATIONS = "mongos_proxy"
40
+ MANAGED_USERS_KEY = "managed-users-key"
40
41
41
42
# We expect the MongoDB container to use the default ports
42
- MONGODB_PORT = 27017
43
- MONGODB_VERSION = "5.0"
44
- PEER = "database-peers"
45
43
46
44
Diff = namedtuple ("Diff" , "added changed deleted" )
47
45
Diff .__doc__ = """
54
52
class MongoDBProvider (Object ):
55
53
"""In this class, we manage client database relations."""
56
54
57
- def __init__ (self , charm : CharmBase , substrate = "k8s" , relation_name : str = "database" ) -> None :
55
+ def __init__ (self , charm : CharmBase , substrate = "k8s" , relation_name : str = REL_NAME ) -> None :
58
56
"""Constructor for MongoDBProvider object.
59
57
60
58
Args:
61
59
charm: the charm for which this relation is provided
62
60
substrate: host type, either "k8s" or "vm"
63
61
relation_name: the name of the relation
64
62
"""
65
- self .relation_name = relation_name
66
63
self .substrate = substrate
67
64
self .charm = charm
68
65
69
- super ().__init__ (charm , self . relation_name )
66
+ super ().__init__ (charm , relation_name )
70
67
self .framework .observe (
71
- charm .on [self . relation_name ].relation_departed ,
68
+ charm .on [relation_name ].relation_departed ,
72
69
self .charm .check_relation_broken_or_scale_down ,
73
70
)
74
- self .framework .observe (
75
- charm .on [self .relation_name ].relation_broken , self ._on_relation_event
76
- )
77
- self .framework .observe (
78
- charm .on [self .relation_name ].relation_changed , self ._on_relation_event
79
- )
71
+ self .framework .observe (charm .on [relation_name ].relation_broken , self ._on_relation_event )
72
+ self .framework .observe (charm .on [relation_name ].relation_changed , self ._on_relation_event )
80
73
81
74
# Charm events defined in the database provides charm library.
82
- self .database_provides = DatabaseProvides (self .charm , relation_name = self . relation_name )
75
+ self .database_provides = DatabaseProvides (self .charm , relation_name = relation_name )
83
76
self .framework .observe (
84
77
self .database_provides .on .database_requested , self ._on_relation_event
85
78
)
86
79
87
- def pass_hook_checks (self , event : EventBase ) -> bool :
88
- """Runs the pre-hooks checks for MongoDBProvider, returns True if all pass ."""
80
+ def pass_sanity_hook_checks (self ) -> bool :
81
+ """Runs reusable and event agnostic checks ."""
89
82
# We shouldn't try to create or update users if the database is not
90
83
# initialised. We will create users as part of initialisation.
91
84
if not self .charm .db_initialised :
92
85
return False
93
86
94
- if not self .charm .is_relation_feasible (self .relation_name ):
87
+ if not self .charm .is_role (Config .Role .MONGOS ) and not self .charm .is_relation_feasible (
88
+ self .get_relation_name ()
89
+ ):
95
90
logger .info ("Skipping code for relations." )
96
91
return False
97
92
98
93
if not self .charm .unit .is_leader ():
99
94
return False
100
95
96
+ return True
97
+
98
+ def pass_hook_checks (self , event : EventBase ) -> bool :
99
+ """Runs the pre-hooks checks for MongoDBProvider, returns True if all pass."""
100
+ if not self .pass_sanity_hook_checks ():
101
+ return False
102
+
101
103
if self .charm .upgrade_in_progress :
102
104
logger .warning (
103
105
"Adding relations is not supported during an upgrade. The charm may be in a broken, unrecoverable state."
@@ -163,16 +165,54 @@ def oversee_users(self, departed_relation_id: Optional[int], event):
163
165
When the function is executed in relation departed event, the departed
164
166
relation is still on the list of all relations. Therefore, for proper
165
167
work of the function, we need to exclude departed relation from the list.
168
+
169
+ Raises:
170
+ PyMongoError
166
171
"""
167
- with MongoDBConnection (self .charm .mongodb_config ) as mongo :
172
+ with MongoConnection (self .charm .mongo_config ) as mongo :
168
173
database_users = mongo .get_users ()
169
- relation_users = self ._get_users_from_relations (departed_relation_id )
170
174
171
- for username in database_users - relation_users :
175
+ users_being_managed = database_users .intersection (self ._get_relational_users_to_manage ())
176
+ expected_current_users = self ._get_users_from_relations (departed_relation_id )
177
+
178
+ self .remove_users (users_being_managed , expected_current_users )
179
+ self .add_users (users_being_managed , expected_current_users )
180
+ self .update_users (event , users_being_managed , expected_current_users )
181
+ self .auto_delete_dbs (departed_relation_id )
182
+
183
+ def remove_users (
184
+ self , users_being_managed : Set [str ], expected_current_users : Set [str ]
185
+ ) -> None :
186
+ """Removes users from Charmed MongoDB.
187
+
188
+ Note this only removes users that this application of Charmed MongoDB is responsible for
189
+ managing. It won't remove:
190
+ 1. users created from other applications
191
+ 2. users created from other mongos routers.
192
+
193
+ Raises:
194
+ PyMongoError
195
+ """
196
+ with MongoConnection (self .charm .mongo_config ) as mongo :
197
+ for username in users_being_managed - expected_current_users :
172
198
logger .info ("Remove relation user: %s" , username )
199
+ if (
200
+ self .charm .is_role (Config .Role .MONGOS )
201
+ and username == self .charm .mongo_config .username
202
+ ):
203
+ continue
204
+
173
205
mongo .drop_user (username )
206
+ self ._remove_from_relational_users_to_manage (username )
207
+
208
+ def add_users (self , users_being_managed : Set [str ], expected_current_users : Set [str ]) -> None :
209
+ """Adds users to Charmed MongoDB.
174
210
175
- for username in relation_users - database_users :
211
+ Raises:
212
+ PyMongoError
213
+ """
214
+ with MongoConnection (self .charm .mongo_config ) as mongo :
215
+ for username in expected_current_users - users_being_managed :
176
216
config = self ._get_config (username , None )
177
217
if config .database is None :
178
218
# We need to wait for the moment when the provider library
@@ -181,15 +221,33 @@ def oversee_users(self, departed_relation_id: Optional[int], event):
181
221
logger .info ("Create relation user: %s on %s" , config .username , config .database )
182
222
183
223
mongo .create_user (config )
224
+ self ._add_to_relational_users_to_manage (username )
184
225
self ._set_relation (config )
185
226
186
- for username in relation_users .intersection (database_users ):
227
+ def update_users (
228
+ self , event : EventBase , users_being_managed : Set [str ], expected_current_users : Set [str ]
229
+ ) -> None :
230
+ """Updates existing users in Charmed MongoDB.
231
+
232
+ Raises:
233
+ PyMongoError
234
+ """
235
+ with MongoConnection (self .charm .mongo_config ) as mongo :
236
+ for username in expected_current_users .intersection (users_being_managed ):
187
237
config = self ._get_config (username , None )
188
238
logger .info ("Update relation user: %s on %s" , config .username , config .database )
189
239
mongo .update_user (config )
190
240
logger .info ("Updating relation data according to diff" )
191
241
self ._diff (event )
192
242
243
+ def auto_delete_dbs (self , departed_relation_id ):
244
+ """Delete's unused dbs if configured to do so.
245
+
246
+ Raises:
247
+ PyMongoError
248
+ """
249
+ with MongoConnection (self .charm .mongo_config ) as mongo :
250
+
193
251
if not self .charm .model .config ["auto-delete" ]:
194
252
return
195
253
@@ -240,15 +298,15 @@ def _diff(self, event: RelationChangedEvent) -> Diff:
240
298
241
299
def update_app_relation_data (self ) -> None :
242
300
"""Helper function to update application relation data."""
243
- if not self .charm . db_initialised :
301
+ if not self .pass_sanity_hook_checks () :
244
302
return
245
303
246
304
database_users = set ()
247
305
248
- with MongoDBConnection (self .charm .mongodb_config ) as mongo :
306
+ with MongoConnection (self .charm .mongo_config ) as mongo :
249
307
database_users = mongo .get_users ()
250
308
251
- for relation in self ._get_relations (rel = REL_NAME ):
309
+ for relation in self ._get_relations ():
252
310
username = self ._get_username_from_relation_id (relation .id )
253
311
password = self ._get_or_set_password (relation )
254
312
config = self ._get_config (username , password )
@@ -282,24 +340,34 @@ def _get_or_set_password(self, relation: Relation) -> str:
282
340
self .database_provides .update_relation_data (relation .id , {"password" : password })
283
341
return password
284
342
285
- def _get_config (self , username : str , password : Optional [str ]) -> MongoConfiguration :
343
+ def _get_config (
344
+ self , username : str , password : Optional [str ], event = None
345
+ ) -> MongoConfiguration :
286
346
"""Construct the config object for future user creation."""
287
347
relation = self ._get_relation_from_username (username )
288
348
if not password :
289
349
password = self ._get_or_set_password (relation )
290
350
291
351
database_name = self ._get_database_from_relation (relation )
292
352
293
- return MongoConfiguration (
294
- replset = self .charm .app .name ,
295
- database = database_name ,
296
- username = username ,
297
- password = password ,
298
- hosts = self .charm .mongodb_config .hosts ,
299
- roles = self ._get_roles_from_relation (relation ),
300
- tls_external = False ,
301
- tls_internal = False ,
302
- )
353
+ mongo_args = {
354
+ "database" : database_name ,
355
+ "username" : username ,
356
+ "password" : password ,
357
+ "hosts" : self .charm .mongo_config .hosts ,
358
+ "roles" : self ._get_roles_from_relation (relation ),
359
+ "tls_external" : False ,
360
+ "tls_internal" : False ,
361
+ }
362
+
363
+ if self .charm .is_role (Config .Role .MONGOS ):
364
+ mongo_args ["port" ] = Config .MONGOS_PORT
365
+ if self .substrate == Config .Substrate .K8S :
366
+ mongo_args ["hosts" ] = self .charm .get_mongos_hosts_for_client ()
367
+ else :
368
+ mongo_args ["replset" ] = self .charm .app .name
369
+
370
+ return MongoConfiguration (** mongo_args )
303
371
304
372
def _set_relation (self , config : MongoConfiguration ):
305
373
"""Save all output fields into application relation."""
@@ -318,10 +386,11 @@ def _set_relation(self, config: MongoConfiguration):
318
386
relation .id ,
319
387
"," .join (config .hosts ),
320
388
)
321
- self .database_provides .set_replset (
322
- relation .id ,
323
- config .replset ,
324
- )
389
+ if not self .charm .is_role (Config .Role .MONGOS ):
390
+ self .database_provides .set_replset (
391
+ relation .id ,
392
+ config .replset ,
393
+ )
325
394
self .database_provides .set_uris (
326
395
relation .id ,
327
396
config .uri ,
@@ -332,9 +401,9 @@ def _get_username_from_relation_id(relation_id: int) -> str:
332
401
"""Construct username."""
333
402
return f"relation-{ relation_id } "
334
403
335
- def _get_users_from_relations (self , departed_relation_id : Optional [int ], rel = REL_NAME ):
404
+ def _get_users_from_relations (self , departed_relation_id : Optional [int ]):
336
405
"""Return usernames for all relations except departed relation."""
337
- relations = self ._get_relations (rel )
406
+ relations = self ._get_relations ()
338
407
return set (
339
408
[
340
409
self ._get_username_from_relation_id (relation .id )
@@ -351,7 +420,7 @@ def _get_databases_from_relations(self, departed_relation_id: Optional[int]) ->
351
420
except for those databases that belong to the departing
352
421
relation specified.
353
422
"""
354
- relations = self ._get_relations (rel = REL_NAME )
423
+ relations = self ._get_relations ()
355
424
databases = set ()
356
425
for relation in relations :
357
426
if relation .id == departed_relation_id :
@@ -370,22 +439,54 @@ def _get_relation_from_username(self, username: str) -> Relation:
370
439
assert match is not None , "No relation match"
371
440
relation_id = int (match .group (1 ))
372
441
logger .debug ("Relation ID: %s" , relation_id )
373
- relation_name = (
374
- MONGOS_RELATIONS if self .charm .is_role (Config .Role .CONFIG_SERVER ) else REL_NAME
375
- )
442
+ relation_name = self .get_relation_name ()
376
443
return self .model .get_relation (relation_name , relation_id )
377
444
378
- def _get_relations (self , rel = REL_NAME ) -> List [Relation ]:
445
+ def _get_relations (self ) -> List [Relation ]:
379
446
"""Return the set of relations for users.
380
447
381
448
We create users for either direct relations to charm or for relations through the mongos
382
449
charm.
383
450
"""
384
- return (
385
- self .model .relations [MONGOS_RELATIONS ]
386
- if self .charm .is_role (Config .Role .CONFIG_SERVER )
387
- else self .model .relations [rel ]
388
- )
451
+ return self .model .relations [self .get_relation_name ()]
452
+
453
+ def get_relation_name (self ):
454
+ """Returns the name of the relation to use."""
455
+ if self .charm .is_role (Config .Role .CONFIG_SERVER ):
456
+ return MONGOS_RELATIONS
457
+ elif self .charm .is_role (Config .Role .MONGOS ):
458
+ return MONGOS_CLIENT_RELATIONS
459
+ else :
460
+ return REL_NAME
461
+
462
+ def _get_relational_users_to_manage (self ) -> Set [str ]:
463
+ """Returns a set of the users to manage.
464
+
465
+ Note json cannot serialise sets. Convert from list.
466
+ """
467
+ return set (json .loads (self .charm .app_peer_data .get (MANAGED_USERS_KEY , "[]" )))
468
+
469
+ def _update_relational_users_to_manage (self , new_users : Set [str ]) -> None :
470
+ """Updates the set of the users to manage.
471
+
472
+ Note json cannot serialise sets. Convert from list.
473
+ """
474
+ if not self .charm .unit .is_leader ():
475
+ raise Exception ("Cannot update relational data on non-leader unit" )
476
+
477
+ self .charm .app_peer_data [MANAGED_USERS_KEY ] = json .dumps (list (new_users ))
478
+
479
+ def _remove_from_relational_users_to_manage (self , user_to_remove : str ) -> None :
480
+ """Removes the provided user from the set of the users to manage."""
481
+ current_users = self ._get_relational_users_to_manage ()
482
+ updated_users = current_users - {user_to_remove }
483
+ self ._update_relational_users_to_manage (updated_users )
484
+
485
+ def _add_to_relational_users_to_manage (self , user_to_add : str ) -> None :
486
+ """Adds the provided user to the set of the users to manage."""
487
+ current_users = self ._get_relational_users_to_manage ()
488
+ current_users .add (user_to_add )
489
+ self ._update_relational_users_to_manage (current_users )
389
490
390
491
@staticmethod
391
492
def _get_database_from_relation (relation : Relation ) -> Optional [str ]:
0 commit comments