Skip to content

Commit d5e5948

Browse files
committed
feat(tls): migrate TLS to the single-kernel library
Consume the single-kernel library's operator-certificate TLS handler (events.tls.TLS) and TLSManager instead of the charm-side src/relations/tls.py, stacked on the DPE-10062 Patroni/cluster port (#1788). The TLS events handler owns the two certificate requirers and is constructor-injected into TLSManager, whose live-fetch getters read cert/key from them. A charm-side reload bridge (_reload_tls_after_push) reloads PostgreSQL after the lib handler stores+pushes certs; it also fires on relation_broken so detaching the TLS operator re-renders Patroni with TLS disabled. Removes src/relations/tls.py and push_tls_files_to_workload (now owned by the lib), routes internal-cert regeneration through _regenerate_internal_cert, redirects the operator-cert getters to tls_manager, and pins the library to the TLS stack tip via archive URL (16.3.2 is not yet released). Signed-off-by: Marcelo Henrique Neppel <marcelo.neppel@canonical.com>
1 parent 9bd8260 commit d5e5948

8 files changed

Lines changed: 284 additions & 522 deletions

File tree

poetry.lock

Lines changed: 23 additions & 20 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ charm-refresh = "^3.1.0.2"
2121
charmlibs-snap = "^1.0.1"
2222
charmlibs-systemd = "^1.0.0.post0"
2323
charmlibs-interfaces-tls-certificates = "^1.8.3"
24-
postgresql-charms-single-kernel = {extras = ["postgresql", "vm"], version = "16.3.1"}
24+
postgresql-charms-single-kernel = {url = "https://github.com/canonical/postgresql-single-kernel-library/archive/5aeebec.zip", extras = ["postgresql", "vm"]}
2525

2626
[tool.poetry.group.charm-libs.dependencies]
2727
# data_platform_libs/v0/data_interfaces.py

src/charm.py

Lines changed: 81 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -109,10 +109,8 @@
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,
@@ -121,12 +119,13 @@
121119
)
122120
from single_kernel_postgresql.core.config import CharmConfig
123121
from single_kernel_postgresql.core.state import CharmState
122+
from single_kernel_postgresql.events.tls import TLS
124123
from single_kernel_postgresql.events.tls_transfer import TLSTransfer
125124
from single_kernel_postgresql.managers.cluster import ClusterManager
126125
from single_kernel_postgresql.managers.config import ConfigManager
127126
from single_kernel_postgresql.managers.patroni import PatroniManager
128127
from 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
130129
from single_kernel_postgresql.utils.postgresql import (
131130
ACCESS_GROUP_IDENTITY,
132131
ACCESS_GROUPS,
@@ -155,7 +154,6 @@
155154
)
156155
from constants import (
157156
MONITORING_SNAP_SERVICE,
158-
PATRONI_CONF_PATH,
159157
PGBACKREST_MONITORING_SNAP_SERVICE,
160158
POSTGRESQL_DATA_DIR,
161159
RAFT_PARTNER_PREFIX,
@@ -167,7 +165,6 @@
167165
from ldap import PostgreSQLLDAP
168166
from relations.async_replication import PostgreSQLAsyncReplication
169167
from relations.postgresql_provider import PostgreSQLProvider
170-
from relations.tls import TLS
171168
from relations.watcher import PostgreSQLWatcherRelation
172169
from 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)

src/relations/postgresql_provider.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -485,7 +485,7 @@ def update_endpoints(self, event: DatabaseRequestedEvent | None = None) -> None:
485485
tls = "True" if self.charm.is_tls_enabled else "False"
486486
ca = None
487487
if tls == "True":
488-
_, ca, _ = self.charm.tls.get_client_tls_files()
488+
_, ca, _ = self.charm.tls_manager.get_client_tls_files()
489489
if not ca:
490490
ca = ""
491491

0 commit comments

Comments
 (0)