41
41
InstallEvent ,
42
42
LeaderElectedEvent ,
43
43
RelationDepartedEvent ,
44
+ SecretChangedEvent ,
44
45
StartEvent ,
45
46
)
46
47
from ops .framework import EventBase
50
51
MaintenanceStatus ,
51
52
ModelError ,
52
53
Relation ,
54
+ SecretNotFoundError ,
53
55
Unit ,
54
56
WaitingStatus ,
55
57
)
@@ -180,10 +182,10 @@ def __init__(self, *args):
180
182
self .framework .observe (self .on .get_primary_action , self ._on_get_primary )
181
183
self .framework .observe (self .on [PEER ].relation_changed , self ._on_peer_relation_changed )
182
184
self .framework .observe (self .on .secret_changed , self ._on_peer_relation_changed )
185
+ # add specific handler for updated system-user secrets
186
+ self .framework .observe (self .on .secret_changed , self ._on_secret_changed )
183
187
self .framework .observe (self .on [PEER ].relation_departed , self ._on_peer_relation_departed )
184
188
self .framework .observe (self .on .start , self ._on_start )
185
- self .framework .observe (self .on .get_password_action , self ._on_get_password )
186
- self .framework .observe (self .on .set_password_action , self ._on_set_password )
187
189
self .framework .observe (self .on .promote_to_primary_action , self ._on_promote_to_primary )
188
190
self .framework .observe (self .on .update_status , self ._on_update_status )
189
191
self .cluster_name = self .app .name
@@ -328,6 +330,25 @@ def remove_secret(self, scope: Scopes, key: str) -> None:
328
330
secret_key = self ._translate_field_to_secret_key (key )
329
331
self .peer_relation_data (scope ).delete_relation_data (peers .id , [secret_key ])
330
332
333
+ def get_secret_from_id (self , secret_id : str ) -> dict [str , str ]:
334
+ """Resolve the given id of a Juju secret and return the content as a dict.
335
+
336
+ This method can be used to retrieve any secret, not just those used via the peer relation.
337
+ If the secret is not owned by the charm, it has to be granted access to it.
338
+
339
+ Args:
340
+ secret_id (str): The id of the secret.
341
+
342
+ Returns:
343
+ dict: The content of the secret.
344
+ """
345
+ try :
346
+ secret_content = self .model .get_secret (id = secret_id ).get_content (refresh = True )
347
+ except (SecretNotFoundError , ModelError ):
348
+ raise
349
+
350
+ return secret_content
351
+
331
352
@property
332
353
def is_cluster_initialised (self ) -> bool :
333
354
"""Returns whether the cluster is already initialised."""
@@ -718,6 +739,17 @@ def _on_peer_relation_changed(self, event: HookEvent):
718
739
719
740
self ._update_new_unit_status ()
720
741
742
+ def _on_secret_changed (self , event : SecretChangedEvent ) -> None :
743
+ """Handle the secret_changed event."""
744
+ if not self .unit .is_leader ():
745
+ return
746
+
747
+ if (admin_secret_id := self .config .system_users ) and admin_secret_id == event .secret .id :
748
+ try :
749
+ self ._update_admin_password (admin_secret_id )
750
+ except PostgreSQLUpdateUserPasswordError :
751
+ event .defer ()
752
+
721
753
# Split off into separate function, because of complexity _on_peer_relation_changed
722
754
def _start_stop_pgbackrest_service (self , event : HookEvent ) -> None :
723
755
# Start or stop the pgBackRest TLS server service when TLS certificate change.
@@ -1048,8 +1080,19 @@ def _on_install(self, event: InstallEvent) -> None:
1048
1080
1049
1081
self .unit .status = WaitingStatus ("waiting to start PostgreSQL" )
1050
1082
1051
- def _on_leader_elected (self , event : LeaderElectedEvent ) -> None :
1083
+ def _on_leader_elected (self , event : LeaderElectedEvent ) -> None : # noqa: C901
1052
1084
"""Handle the leader-elected event."""
1085
+ # consider configured system user passwords
1086
+ system_user_passwords = {}
1087
+ if admin_secret_id := self .config .system_users :
1088
+ try :
1089
+ system_user_passwords = self .get_secret_from_id (secret_id = admin_secret_id )
1090
+ except (ModelError , SecretNotFoundError ) as e :
1091
+ # only display the error but don't return to make sure all users have passwords
1092
+ logger .error (f"Error setting internal passwords: { e } " )
1093
+ self .unit .status = BlockedStatus ("Password setting for system users failed." )
1094
+ event .defer ()
1095
+
1053
1096
# The leader sets the needed passwords if they weren't set before.
1054
1097
for key in (
1055
1098
USER_PASSWORD_KEY ,
@@ -1060,7 +1103,14 @@ def _on_leader_elected(self, event: LeaderElectedEvent) -> None:
1060
1103
PATRONI_PASSWORD_KEY ,
1061
1104
):
1062
1105
if self .get_secret (APP_SCOPE , key ) is None :
1063
- self .set_secret (APP_SCOPE , key , new_password ())
1106
+ if key in system_user_passwords :
1107
+ # use provided passwords for system-users if available
1108
+ self .set_secret (APP_SCOPE , key , system_user_passwords [key ])
1109
+ logger .info (f"Using configured password for { key } " )
1110
+ else :
1111
+ # generate a password for this user if not provided
1112
+ self .set_secret (APP_SCOPE , key , new_password ())
1113
+ logger .info (f"Generated new password for { key } " )
1064
1114
1065
1115
if self .has_raft_keys ():
1066
1116
self ._raft_reinitialisation ()
@@ -1134,6 +1184,12 @@ def _on_config_changed(self, event) -> None:
1134
1184
# Enable and/or disable the extensions.
1135
1185
self .enable_disable_extensions ()
1136
1186
1187
+ if admin_secret_id := self .config .system_users :
1188
+ try :
1189
+ self ._update_admin_password (admin_secret_id )
1190
+ except PostgreSQLUpdateUserPasswordError :
1191
+ event .defer ()
1192
+
1137
1193
def enable_disable_extensions (self , database : str | None = None ) -> None :
1138
1194
"""Enable/disable PostgreSQL extensions set through config options.
1139
1195
@@ -1362,57 +1418,21 @@ def _start_replica(self, event) -> None:
1362
1418
# Configure Patroni in the replica but don't start it yet.
1363
1419
self ._patroni .configure_patroni_on_unit ()
1364
1420
1365
- def _on_get_password (self , event : ActionEvent ) -> None :
1366
- """Returns the password for a user as an action response.
1367
-
1368
- If no user is provided, the password of the operator user is returned.
1369
- """
1370
- username = event .params .get ("username" , USER )
1371
- if username not in PASSWORD_USERS :
1372
- event .fail (
1373
- f"The action can be run only for users used by the charm or Patroni:"
1374
- f" { ', ' .join (PASSWORD_USERS )} not { username } "
1375
- )
1376
- return
1377
- event .set_results ({"password" : self .get_secret (APP_SCOPE , f"{ username } -password" )})
1378
-
1379
- def _on_set_password (self , event : ActionEvent ) -> None :
1380
- """Set the password for the specified user."""
1381
- # Only leader can write the new password into peer relation.
1382
- if not self .unit .is_leader ():
1383
- event .fail ("The action can be run only on leader unit" )
1384
- return
1385
-
1386
- username = event .params .get ("username" , USER )
1387
- if username not in SYSTEM_USERS :
1388
- event .fail (
1389
- f"The action can be run only for users used by the charm:"
1390
- f" { ', ' .join (SYSTEM_USERS )} not { username } "
1391
- )
1392
- return
1393
-
1394
- password = event .params .get ("password" , new_password ())
1395
-
1396
- if password == self .get_secret (APP_SCOPE , f"{ username } -password" ):
1397
- event .log ("The old and new passwords are equal." )
1398
- event .set_results ({"password" : password })
1399
- return
1400
-
1401
- # Ensure all members are ready before trying to reload Patroni
1402
- # configuration to avoid errors (like the API not responding in
1403
- # one instance because PostgreSQL and/or Patroni are not ready).
1421
+ def _update_admin_password (self , admin_secret_id : str ) -> None :
1422
+ """Check if the password of a system user was changed and update it in the database."""
1404
1423
if not self ._patroni .are_all_members_ready ():
1405
- event .fail (
1424
+ # Ensure all members are ready before reloading Patroni configuration to avoid errors
1425
+ # e.g. API not responding in one instance because PostgreSQL / Patroni are not ready
1426
+ raise PostgreSQLUpdateUserPasswordError (
1406
1427
"Failed changing the password: Not all members healthy or finished initial sync."
1407
1428
)
1408
- return
1409
1429
1410
1430
replication_offer_relation = self .model .get_relation (REPLICATION_OFFER_RELATION )
1431
+ other_cluster_primary_ip = ""
1411
1432
if (
1412
1433
replication_offer_relation is not None
1413
1434
and not self .async_replication .is_primary_cluster ()
1414
1435
):
1415
- # Update the password in the other cluster PostgreSQL primary instance.
1416
1436
other_cluster_endpoints = self .async_replication .get_all_primary_cluster_endpoints ()
1417
1437
other_cluster_primary = self ._patroni .get_primary (
1418
1438
alternative_endpoints = other_cluster_endpoints
@@ -1422,37 +1442,51 @@ def _on_set_password(self, event: ActionEvent) -> None:
1422
1442
for unit in replication_offer_relation .units
1423
1443
if unit .name .replace ("/" , "-" ) == other_cluster_primary
1424
1444
)
1425
- try :
1426
- self .postgresql .update_user_password (
1427
- username , password , database_host = other_cluster_primary_ip
1428
- )
1429
- except PostgreSQLUpdateUserPasswordError as e :
1430
- logger .exception (e )
1431
- event .fail ("Failed changing the password." )
1432
- return
1433
1445
elif self .model .get_relation (REPLICATION_CONSUMER_RELATION ) is not None :
1434
- event . fail (
1435
- "Failed changing the password: This action can be ran only in the cluster from the offer side."
1446
+ logger . error (
1447
+ "Failed changing the password: This can be ran only in the cluster from the offer side."
1436
1448
)
1449
+ self .unit .status = BlockedStatus ("Password update for system users failed." )
1437
1450
return
1438
- else :
1439
- # Update the password in this cluster PostgreSQL primary instance.
1440
- try :
1441
- self .postgresql .update_user_password (username , password )
1442
- except PostgreSQLUpdateUserPasswordError as e :
1443
- logger .exception (e )
1444
- event .fail ("Failed changing the password." )
1445
- return
1446
1451
1447
- # Update the password in the secret store.
1448
- self .set_secret (APP_SCOPE , f"{ username } -password" , password )
1452
+ try :
1453
+ # get the secret content and check each user configured there
1454
+ # only SYSTEM_USERS with changed passwords are processed, all others ignored
1455
+ updated_passwords = self .get_secret_from_id (secret_id = admin_secret_id )
1456
+ for user , password in list (updated_passwords .items ()):
1457
+ if user not in SYSTEM_USERS :
1458
+ logger .error (
1459
+ f"Can only update system users: { ', ' .join (SYSTEM_USERS )} not { user } "
1460
+ )
1461
+ updated_passwords .pop (user )
1462
+ continue
1463
+ if password == self .get_secret (APP_SCOPE , f"{ user } -password" ):
1464
+ updated_passwords .pop (user )
1465
+ except (ModelError , SecretNotFoundError ) as e :
1466
+ logger .error (f"Error updating internal passwords: { e } " )
1467
+ self .unit .status = BlockedStatus ("Password update for system users failed." )
1468
+ return
1469
+
1470
+ try :
1471
+ # perform the actual password update for the remaining users
1472
+ for user , password in updated_passwords .items ():
1473
+ logger .info (f"Updating password for user { user } " )
1474
+ self .postgresql .update_user_password (
1475
+ user ,
1476
+ password ,
1477
+ database_host = other_cluster_primary_ip if other_cluster_primary_ip else None ,
1478
+ )
1479
+ # Update the password in the secret store after updating it in the database
1480
+ self .set_secret (APP_SCOPE , f"{ user } -password" , password )
1481
+ except PostgreSQLUpdateUserPasswordError as e :
1482
+ logger .exception (e )
1483
+ self .unit .status = BlockedStatus ("Password update for system users failed." )
1484
+ return
1449
1485
1450
1486
# Update and reload Patroni configuration in this unit to use the new password.
1451
1487
# Other units Patroni configuration will be reloaded in the peer relation changed event.
1452
1488
self .update_config ()
1453
1489
1454
- event .set_results ({"password" : password })
1455
-
1456
1490
def _on_promote_to_primary (self , event : ActionEvent ) -> None :
1457
1491
if event .params .get ("scope" ) == "cluster" :
1458
1492
return self .async_replication .promote_to_primary (event )
0 commit comments