77import logging
88from typing import Dict , List , Optional
99
10- from charms .postgresql_k8s .v0 .postgresql import PostgreSQL
10+ from charms .postgresql_k8s .v0 .postgresql import (
11+ PostgreSQL ,
12+ PostgreSQLUpdateUserPasswordError ,
13+ )
1114from lightkube import ApiError , Client , codecs
1215from lightkube .resources .core_v1 import Endpoints , Pod , Service
1316from ops .charm import (
3033from requests import ConnectionError
3134from tenacity import RetryError
3235
33- from constants import PEER , REPLICATION_PASSWORD_KEY , USER , USER_PASSWORD_KEY
36+ from constants import (
37+ PEER ,
38+ REPLICATION_PASSWORD_KEY ,
39+ REPLICATION_USER ,
40+ SYSTEM_USERS ,
41+ USER ,
42+ USER_PASSWORD_KEY ,
43+ )
3444from patroni import NotReadyError , Patroni
3545from relations .db import DbProvides
3646from relations .postgresql_provider import PostgreSQLProvider
@@ -59,9 +69,8 @@ def __init__(self, *args):
5969 self .framework .observe (self .on .postgresql_pebble_ready , self ._on_postgresql_pebble_ready )
6070 self .framework .observe (self .on .stop , self ._on_stop )
6171 self .framework .observe (self .on .upgrade_charm , self ._on_upgrade_charm )
62- self .framework .observe (
63- self .on .get_operator_password_action , self ._on_get_operator_password
64- )
72+ self .framework .observe (self .on .get_password_action , self ._on_get_password )
73+ self .framework .observe (self .on .set_password_action , self ._on_set_password )
6574 self .framework .observe (self .on .get_primary_action , self ._on_get_primary )
6675 self .framework .observe (self .on .update_status , self ._on_update_status )
6776 self ._storage_path = self .meta .storages ["pgdata" ].location
@@ -118,7 +127,7 @@ def postgresql(self) -> PostgreSQL:
118127 return PostgreSQL (
119128 host = self .primary_endpoint ,
120129 user = USER ,
121- password = self ._get_operator_password ( ),
130+ password = self ._get_secret ( "app" , f" { USER } -password" ),
122131 database = "postgres" ,
123132 )
124133
@@ -426,9 +435,74 @@ def _create_resources(self) -> None:
426435 self .unit .status = BlockedStatus (f"failed to create services { e } " )
427436 return
428437
429- def _on_get_operator_password (self , event : ActionEvent ) -> None :
430- """Returns the password for the operator user as an action response."""
431- event .set_results ({USER_PASSWORD_KEY : self ._get_operator_password ()})
438+ def _on_get_password (self , event : ActionEvent ) -> None :
439+ """Returns the password for a user as an action response.
440+
441+ If no user is provided, the password of the operator user is returned.
442+ """
443+ username = event .params .get ("username" , USER )
444+ if username not in SYSTEM_USERS :
445+ event .fail (
446+ f"The action can be run only for users used by the charm or Patroni:"
447+ f" { ', ' .join (SYSTEM_USERS )} not { username } "
448+ )
449+ return
450+ event .set_results (
451+ {f"{ username } -password" : self ._get_secret ("app" , f"{ username } -password" )}
452+ )
453+
454+ def _on_set_password (self , event : ActionEvent ) -> None :
455+ """Set the password for the specified user."""
456+ # Only leader can write the new password into peer relation.
457+ if not self .unit .is_leader ():
458+ event .fail ("The action can be run only on leader unit" )
459+ return
460+
461+ username = event .params .get ("username" , USER )
462+ if username not in SYSTEM_USERS :
463+ event .fail (
464+ f"The action can be run only for users used by the charm:"
465+ f" { ', ' .join (SYSTEM_USERS )} not { username } "
466+ )
467+ return
468+
469+ password = new_password ()
470+ if "password" in event .params :
471+ password = event .params ["password" ]
472+
473+ if password == self ._get_secret ("app" , f"{ username } -password" ):
474+ event .log ("The old and new passwords are equal." )
475+ event .set_results ({f"{ username } -password" : password })
476+ return
477+
478+ # Ensure all members are ready before trying to reload Patroni
479+ # configuration to avoid errors (like the API not responding in
480+ # one instance because PostgreSQL and/or Patroni are not ready).
481+ if not self ._patroni .are_all_members_ready ():
482+ event .fail (
483+ "Failed changing the password: Not all members healthy or finished initial sync."
484+ )
485+ return
486+
487+ # Update the password in the PostgreSQL instance.
488+ try :
489+ self .postgresql .update_user_password (username , password )
490+ except PostgreSQLUpdateUserPasswordError as e :
491+ logger .exception (e )
492+ event .fail (
493+ "Failed changing the password: Not all members healthy or finished initial sync."
494+ )
495+ return
496+
497+ # Update the password in the secret store.
498+ self ._set_secret ("app" , f"{ username } -password" , password )
499+
500+ # Update and reload Patroni configuration in this unit to use the new password.
501+ # Other units Patroni configuration will be reloaded in the peer relation changed event.
502+ self ._patroni .render_patroni_yml_file ()
503+ self ._patroni .reload_patroni_configuration ()
504+
505+ event .set_results ({f"{ username } -password" : password })
432506
433507 def _on_get_primary (self , event : ActionEvent ) -> None :
434508 """Get primary instance."""
@@ -495,6 +569,8 @@ def _patroni(self):
495569 self ._namespace ,
496570 self .app .planned_units (),
497571 self ._storage_path ,
572+ self ._get_secret ("app" , USER_PASSWORD_KEY ),
573+ self ._get_secret ("app" , REPLICATION_PASSWORD_KEY ),
498574 )
499575
500576 @property
@@ -555,10 +631,8 @@ def _postgresql_layer(self) -> Layer:
555631 "PATRONI_KUBERNETES_USE_ENDPOINTS" : "true" ,
556632 "PATRONI_NAME" : pod_name ,
557633 "PATRONI_SCOPE" : f"patroni-{ self ._name } " ,
558- "PATRONI_REPLICATION_USERNAME" : "replication" ,
559- "PATRONI_REPLICATION_PASSWORD" : self ._replication_password ,
634+ "PATRONI_REPLICATION_USERNAME" : REPLICATION_USER ,
560635 "PATRONI_SUPERUSER_USERNAME" : USER ,
561- "PATRONI_SUPERUSER_PASSWORD" : self ._get_operator_password (),
562636 },
563637 }
564638 },
@@ -575,15 +649,6 @@ def _peers(self) -> Relation:
575649 """
576650 return self .model .get_relation (PEER )
577651
578- def _get_operator_password (self ) -> str :
579- """Get operator user password."""
580- return self ._get_secret ("app" , USER_PASSWORD_KEY )
581-
582- @property
583- def _replication_password (self ) -> str :
584- """Get replication user password."""
585- return self ._get_secret ("app" , REPLICATION_PASSWORD_KEY )
586-
587652 def _unit_name_to_pod_name (self , unit_name : str ) -> str :
588653 """Converts unit name to pod name.
589654
0 commit comments