109109 SNAP_USER ,
110110 SPI_MODULE ,
111111 SYSTEM_USERS ,
112- TLS_CA_BUNDLE_FILE ,
113- TLS_CA_FILE ,
114- TLS_CERT_FILE ,
115- TLS_KEY_FILE ,
112+ TLS_CLIENT_RELATION ,
113+ TLS_PEER_RELATION ,
116114 TRACING_PROTOCOL ,
117115 TRACING_RELATION_NAME ,
118116 UNIT_SCOPE ,
121119)
122120from single_kernel_postgresql .core .config import CharmConfig
123121from single_kernel_postgresql .core .state import CharmState
122+ from single_kernel_postgresql .events .tls import TLS
124123from single_kernel_postgresql .events .tls_transfer import TLSTransfer
125124from single_kernel_postgresql .managers .cluster import ClusterManager
126125from single_kernel_postgresql .managers .config import ConfigManager
127126from single_kernel_postgresql .managers .patroni import PatroniManager
128127from single_kernel_postgresql .managers .tls import TLSManager
129- from single_kernel_postgresql .utils import label2name , new_password , render_file
128+ from single_kernel_postgresql .utils import label2name , new_password
130129from single_kernel_postgresql .utils .postgresql import (
131130 ACCESS_GROUP_IDENTITY ,
132131 ACCESS_GROUPS ,
155154)
156155from constants import (
157156 MONITORING_SNAP_SERVICE ,
158- PATRONI_CONF_PATH ,
159157 PGBACKREST_MONITORING_SNAP_SERVICE ,
160158 POSTGRESQL_DATA_DIR ,
161159 RAFT_PARTNER_PREFIX ,
167165from ldap import PostgreSQLLDAP
168166from relations .async_replication import PostgreSQLAsyncReplication
169167from relations .postgresql_provider import PostgreSQLProvider
170- from relations .tls import TLS
171168from relations .watcher import PostgreSQLWatcherRelation
172169from rotate_logs import RotateLogs
173170
@@ -379,11 +376,10 @@ def __init__(self, *args):
379376
380377 # TODO switch to the abstract class base
381378 # State
382- self .state = CharmState (charm = self , substrate = self .substrate ) # type: ignore
379+ self .state = CharmState (charm = self , substrate = self .substrate )
383380
384381 # Managers
385382 self .patroni_manager = PatroniManager (state = self .state , workload = self .workload )
386- self .tls_manager = TLSManager (state = self .state , workload = self .workload )
387383 self .cluster_manager = ClusterManager (state = self .state , workload = self .workload )
388384 self .config_manager = ConfigManager (state = self .state , workload = self .workload )
389385
@@ -421,7 +417,31 @@ def __init__(self, *args):
421417 self .postgresql_client_relation = PostgreSQLProvider (self )
422418 self .backup = PostgreSQLBackups (self , "s3-parameters" )
423419 self .ldap = PostgreSQLLDAP (self , "ldap" )
424- self .tls = TLS (self , PEER_RELATION )
420+ # TLS events handler owns the two cert requirers; build it before the TLS
421+ # manager so the manager can constructor-inject them for its live-fetch getters.
422+ self .tls = TLS (self , self .state , self .workload )
423+ self .tls_manager = TLSManager (
424+ state = self .state ,
425+ workload = self .workload ,
426+ client_certificate = self .tls .client_certificate ,
427+ peer_certificate = self .tls .peer_certificate ,
428+ )
429+ # Bridge the lib TLS handler's requirer events back into a PostgreSQL reload:
430+ # the lib handler stores+pushes certs on certificate_available, then we reload.
431+ # Also fires on relation_broken so detaching the TLS operator re-renders Patroni
432+ # with TLS disabled.
433+ self .framework .observe (
434+ self .tls .client_certificate .on .certificate_available , self ._reload_tls_after_push
435+ )
436+ self .framework .observe (
437+ self .tls .peer_certificate .on .certificate_available , self ._reload_tls_after_push
438+ )
439+ self .framework .observe (
440+ self .on [TLS_CLIENT_RELATION ].relation_broken , self ._reload_tls_after_push
441+ )
442+ self .framework .observe (
443+ self .on [TLS_PEER_RELATION ].relation_broken , self ._reload_tls_after_push
444+ )
425445 self .tls_transfer = TLSTransfer (self , PEER_RELATION )
426446 self .async_replication = PostgreSQLAsyncReplication (self )
427447 self .watcher_offer = PostgreSQLWatcherRelation (self )
@@ -498,6 +518,49 @@ def substrate(self) -> Substrates:
498518 """
499519 return Substrates .VM
500520
521+ def _reload_tls_after_push (self , event ) -> None :
522+ """Reload PostgreSQL after the lib TLS handler stores+pushes certs.
523+
524+ Also fires on the TLS relations' relation_broken so detaching the TLS
525+ operator re-renders Patroni with TLS disabled and reloads.
526+
527+ Mirror the handler's readiness guard: when the internal CA is absent the
528+ handler defers its push (no files on disk), so skip the reload to avoid
529+ rendering ssl:on against missing TLS files on an already-running unit.
530+
531+ A transient config-apply failure (Patroni API unreachable, member not
532+ started) defers and retries rather than leaving stale TLS state or failing
533+ the hook.
534+ """
535+ if not self .get_secret (APP_SCOPE , "internal-ca" ):
536+ return
537+ # Don't enable TLS in the config until the lib has written the cert files to
538+ # disk (its push can defer while this local render would still succeed, which
539+ # would start Patroni ssl:on against missing files).
540+ if self .is_tls_enabled and not self .tls_manager .client_tls_files_on_disk ():
541+ event .defer ()
542+ return
543+ try :
544+ if not self .update_config ():
545+ event .defer ()
546+ except Exception :
547+ logger .exception ("TLS reload (update_config) failed; deferring" )
548+ event .defer ()
549+
550+ def _regenerate_internal_cert (self , * , reload : bool = True ) -> None :
551+ """Generate the internal peer cert, push it to the workload, and (optionally) reload.
552+
553+ reload=False is used at cluster bootstrap: the leader renders patroni.yml on
554+ leader-elected and each replica renders it in _on_peer_relation_changed just
555+ before starting Patroni, so a reload here would be redundant. The internal peer
556+ cert does not toggle ssl in the config -- only the operator/client cert does, via
557+ is_tls_enabled -- so skipping the reload cannot leave a stale ssl setting.
558+ """
559+ self .tls_manager .generate_internal_peer_cert ()
560+ self .tls_manager .push_tls_files ()
561+ if reload :
562+ self .update_config ()
563+
501564 def _check_and_update_internal_cert (self ) -> None :
502565 """Check if the internal cert CN matches the unit IP and regenerate if needed."""
503566 try :
@@ -509,7 +572,7 @@ def _check_and_update_internal_cert(self) -> None:
509572 != self .state .unit_ip
510573 )
511574 ):
512- self .tls . generate_internal_peer_cert ()
575+ self ._regenerate_internal_cert ()
513576 except Exception :
514577 logger .exception ("Unable to check or update internal cert" )
515578
@@ -1548,7 +1611,7 @@ def is_standby_cluster(self) -> bool:
15481611 @property
15491612 def is_tls_enabled (self ) -> bool :
15501613 """Return whether TLS is enabled."""
1551- return all (self .tls .get_client_tls_files ())
1614+ return all (self .tls_manager .get_client_tls_files ())
15521615
15531616 @property
15541617 def _peer_members_ips (self ) -> set [str ]:
@@ -1759,7 +1822,7 @@ def _on_leader_elected(self, event: LeaderElectedEvent) -> None: # noqa: C901
17591822 logger .debug ("On leader elected failed to reconfigure cluster." )
17601823
17611824 if not self .get_secret (APP_SCOPE , "internal-ca" ):
1762- self .tls .generate_internal_peer_ca ()
1825+ self .tls_manager .generate_internal_peer_ca ()
17631826 self .update_config ()
17641827
17651828 # Don't update connection endpoints in the first time this event run for
@@ -1946,7 +2009,7 @@ def _on_start(self, event: StartEvent) -> None:
19462009 event .defer ()
19472010 return
19482011 if not self .get_secret (UNIT_SCOPE , "internal-cert" ):
1949- self .tls . generate_internal_peer_cert ( )
2012+ self ._regenerate_internal_cert ( reload = False )
19502013
19512014 self .unit_peer_data .update ({"ip" : self .state .unit_ip })
19522015 self ._ensure_storage_layout ()
@@ -2497,10 +2560,12 @@ def _update_certificate(self) -> None:
24972560 # Request the certificate only if there is already one. If there isn't,
24982561 # the certificate will be generated in the relation joined event when
24992562 # relating to the TLS Certificates Operator.
2500- if all (self .tls .get_client_tls_files ()) or all (self .tls .get_peer_tls_files ()):
2563+ if all (self .tls_manager .get_client_tls_files ()) or all (
2564+ self .tls_manager .get_peer_tls_files ()
2565+ ):
25012566 self .tls .refresh_tls_certificates_event .emit ()
25022567 if self .get_secret (UNIT_SCOPE , "internal-cert" ):
2503- self .tls . generate_internal_peer_cert ()
2568+ self ._regenerate_internal_cert ()
25042569
25052570 @property
25062571 def is_blocked (self ) -> bool :
@@ -2582,37 +2647,6 @@ def _peers(self) -> Relation | None:
25822647 """
25832648 return self .model .get_relation (PEER_RELATION )
25842649
2585- def push_tls_files_to_workload (self ) -> bool :
2586- """Move TLS files to the PostgreSQL storage path and enable TLS."""
2587- key , ca , cert = self .tls .get_client_tls_files ()
2588- if key is not None :
2589- render_file (Substrates .VM , f"{ PATRONI_CONF_PATH } /{ TLS_KEY_FILE } " , key , 0o600 )
2590- if ca is not None :
2591- render_file (Substrates .VM , f"{ PATRONI_CONF_PATH } /{ TLS_CA_FILE } " , ca , 0o600 )
2592- if cert is not None :
2593- render_file (Substrates .VM , f"{ PATRONI_CONF_PATH } /{ TLS_CERT_FILE } " , cert , 0o600 )
2594-
2595- key , ca , cert = self .tls .get_peer_tls_files ()
2596- if key is not None :
2597- render_file (Substrates .VM , f"{ PATRONI_CONF_PATH } /peer_{ TLS_KEY_FILE } " , key , 0o600 )
2598- if ca is not None :
2599- render_file (Substrates .VM , f"{ PATRONI_CONF_PATH } /peer_{ TLS_CA_FILE } " , ca , 0o600 )
2600- if cert is not None :
2601- render_file (Substrates .VM , f"{ PATRONI_CONF_PATH } /peer_{ TLS_CERT_FILE } " , cert , 0o600 )
2602-
2603- render_file (
2604- Substrates .VM ,
2605- f"{ PATRONI_CONF_PATH } /{ TLS_CA_BUNDLE_FILE } " ,
2606- self .tls .get_peer_ca_bundle (),
2607- 0o600 ,
2608- )
2609-
2610- try :
2611- return self .update_config ()
2612- except Exception :
2613- logger .exception ("TLS files failed to push. Error in config update" )
2614- return False
2615-
26162650 def push_ca_file_into_workload (self , secret_name : str ) -> bool :
26172651 """Move CA certificates file into the PostgreSQL storage path."""
26182652 certs = self .get_secret (UNIT_SCOPE , secret_name )
0 commit comments