Skip to content

Commit ead8a76

Browse files
[DPE-10615] Migrate TLS to the single-kernel library (VM) (#1816)
* 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> * build(deps): bump single-kernel lib to the 16.3.3 stack tip The TLS lib stack was rebased onto current 16/edge and re-bumped to 16.3.3 because 16/edge had meanwhile shipped its own 16.3.2, and the rebase dropped the dead workload parameter from the TLS events handler constructor, so the pin and the construction call move together. Signed-off-by: Marcelo Henrique Neppel <marcelo.neppel@canonical.com> * test(tls): assert relation_broken bridge ordering and exception defer The relation_broken bridge registrations reference only self.on[...], so an __init__ reorder above the lib handler's construction would silently run the reload before the lib clears state and pushes files; the prior test asserted membership without order. The bridge's except-and-defer branch also had no coverage, so a refactor could drop the broad guard without failing CI. Signed-off-by: Marcelo Henrique Neppel <marcelo.neppel@canonical.com> * build(deps): bump single-kernel pin to a1d2178 (cleaned lib) Re-pin the unreleased single-kernel library from 5a9fa05 to a1d2178, the rebased TLS-stack tip. a1d2178 includes the operator-cert manager/ events handler (the migration payload) plus the docstring/comment cleanup that dropped stale operator-repo file-path references and #168/V3-V4 idiom mentions from the lib. The 5a9fa05→a1d2178 diff is docstring/comment-only, so no charm-side code changes are required. Signed-off-by: Marcelo Henrique Neppel <marcelo.neppel@canonical.com> * test(tls): drop observer-wiring tests from test_tls.py Remove _platform_machine, _observers_for, and the three reload-bridge observer-ordering tests that exercised charm-side __init__ wiring now that the bridge itself is transitional and scheduled to leave the charm when update_config ports to the lib. _platform_machine was redundant: refresh_versions.toml keys both x86_64 and aarch64, so the platform.machine() snap-revision lookup succeeds natively on either arch without the monkeypatch (proven by the suite passing on aarch64 after removal). Its harness dependency and the now-unused TLS_CLIENT/TLS_PEER_RELATION imports go with it. Signed-off-by: Marcelo Henrique Neppel <marcelo.neppel@canonical.com> * build(deps): pin single-kernel lib to published 16.3.3 The TLS flip branch pinned postgresql-charms-single-kernel to a GitHub archive URL (commit a1d2178) to test unreleased lib changes. 16.3.3 is now published to PyPI carrying the same TLS state + manager/events content, so restore the published-version pin form and regenerate poetry.lock against the PyPI wheel/tarball. Signed-off-by: Marcelo Henrique Neppel <marcelo.neppel@canonical.com> * build(deps): pin single-kernel to PITR-fix archive on TLS branch Point the lib dependency at the #179 fix archive (16.3.4 = 16.3.3 plus the get_member_status RetryError guard) so this TLS-migration branch's CI proves the PITR restore integration test stops failing before 16.3.4 is published. The archive carries the same 4-arg TLSManager this branch already targets, so no charm code changes are needed. Temporary: revert to the published version once the lib release lands. Signed-off-by: Marcelo Henrique Neppel <marcelo.neppel@canonical.com> * build(deps): pin single-kernel lib to published 16.3.4 Move the TLS branch off the pre-release GitHub-archive commit pin to the released PyPI 16.3.4 package. 16.3.4 ships the DPE-10663 fix for Patroni get_member_status when the cluster is unreachable — the path test_tls_enabled exercises when patroni is stopped mid-test — which the archive commit pin was capturing ahead of release; the published release now supersedes that stopgap. Drop the redundant `vm` extra: the charm already declares charmlibs-snap, charmlibs-systemd, psutil, and pysyncobj as direct dependencies, so the lib extra only re-declared them. Signed-off-by: Marcelo Henrique Neppel <marcelo.neppel@canonical.com> * build(deps): restore the vm extra on the single-kernel pin Keep both extras on the lib pin to match the branch's original pin shape. The vm extra carries the lower-bound constraints for the VM-only deps (charmlibs-snap, charmlibs-systemd, psutil, pysyncobj) that the lib expects, which the charm also declares directly. Signed-off-by: Marcelo Henrique Neppel <marcelo.neppel@canonical.com> --------- Signed-off-by: Marcelo Henrique Neppel <marcelo.neppel@canonical.com>
1 parent d4c9209 commit ead8a76

8 files changed

Lines changed: 221 additions & 501 deletions

File tree

poetry.lock

Lines changed: 4 additions & 4 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.2"}
24+
postgresql-charms-single-kernel = {extras = ["postgresql", "vm"], version = "16.3.4"}
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)
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)