Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 24 additions & 20 deletions poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ charm-refresh = "^3.1.0.2"
charmlibs-snap = "^1.0.1"
charmlibs-systemd = "^1.0.0.post0"
charmlibs-interfaces-tls-certificates = "^1.8.3"
postgresql-charms-single-kernel = {extras = ["postgresql", "vm"], version = "16.3.1"}
postgresql-charms-single-kernel = {url = "https://github.com/canonical/postgresql-single-kernel-library/archive/5a9fa05.zip", extras = ["postgresql", "vm"]}

[tool.poetry.group.charm-libs.dependencies]
# data_platform_libs/v0/data_interfaces.py
Expand Down
128 changes: 81 additions & 47 deletions src/charm.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,10 +109,8 @@
SNAP_USER,
SPI_MODULE,
SYSTEM_USERS,
TLS_CA_BUNDLE_FILE,
TLS_CA_FILE,
TLS_CERT_FILE,
TLS_KEY_FILE,
TLS_CLIENT_RELATION,
TLS_PEER_RELATION,
TRACING_PROTOCOL,
TRACING_RELATION_NAME,
UNIT_SCOPE,
Expand All @@ -121,12 +119,13 @@
)
from single_kernel_postgresql.core.config import CharmConfig
from single_kernel_postgresql.core.state import CharmState
from single_kernel_postgresql.events.tls import TLS
from single_kernel_postgresql.events.tls_transfer import TLSTransfer
from single_kernel_postgresql.managers.cluster import ClusterManager
from single_kernel_postgresql.managers.config import ConfigManager
from single_kernel_postgresql.managers.patroni import PatroniManager
from single_kernel_postgresql.managers.tls import TLSManager
from single_kernel_postgresql.utils import label2name, new_password, render_file
from single_kernel_postgresql.utils import label2name, new_password
from single_kernel_postgresql.utils.postgresql import (
ACCESS_GROUP_IDENTITY,
ACCESS_GROUPS,
Expand Down Expand Up @@ -155,7 +154,6 @@
)
from constants import (
MONITORING_SNAP_SERVICE,
PATRONI_CONF_PATH,
PGBACKREST_MONITORING_SNAP_SERVICE,
POSTGRESQL_DATA_DIR,
RAFT_PARTNER_PREFIX,
Expand All @@ -167,7 +165,6 @@
from ldap import PostgreSQLLDAP
from relations.async_replication import PostgreSQLAsyncReplication
from relations.postgresql_provider import PostgreSQLProvider
from relations.tls import TLS
from relations.watcher import PostgreSQLWatcherRelation
from rotate_logs import RotateLogs

Expand Down Expand Up @@ -379,11 +376,10 @@ def __init__(self, *args):

# TODO switch to the abstract class base
# State
self.state = CharmState(charm=self, substrate=self.substrate) # type: ignore
self.state = CharmState(charm=self, substrate=self.substrate)

# Managers
self.patroni_manager = PatroniManager(state=self.state, workload=self.workload)
self.tls_manager = TLSManager(state=self.state, workload=self.workload)
self.cluster_manager = ClusterManager(state=self.state, workload=self.workload)
self.config_manager = ConfigManager(state=self.state, workload=self.workload)

Expand Down Expand Up @@ -421,7 +417,31 @@ def __init__(self, *args):
self.postgresql_client_relation = PostgreSQLProvider(self)
self.backup = PostgreSQLBackups(self, "s3-parameters")
self.ldap = PostgreSQLLDAP(self, "ldap")
self.tls = TLS(self, PEER_RELATION)
# TLS events handler owns the two cert requirers; build it before the TLS
# manager so the manager can constructor-inject them for its live-fetch getters.
self.tls = TLS(self, self.state)
self.tls_manager = TLSManager(
state=self.state,
workload=self.workload,
client_certificate=self.tls.client_certificate,
peer_certificate=self.tls.peer_certificate,
)
# Bridge the lib TLS handler's requirer events back into a PostgreSQL reload:
# the lib handler stores+pushes certs on certificate_available, then we reload.
# Also fires on relation_broken so detaching the TLS operator re-renders Patroni
# with TLS disabled.
self.framework.observe(
self.tls.client_certificate.on.certificate_available, self._reload_tls_after_push
)
self.framework.observe(
self.tls.peer_certificate.on.certificate_available, self._reload_tls_after_push
)
self.framework.observe(
self.on[TLS_CLIENT_RELATION].relation_broken, self._reload_tls_after_push
)
self.framework.observe(
self.on[TLS_PEER_RELATION].relation_broken, self._reload_tls_after_push
)
self.tls_transfer = TLSTransfer(self, PEER_RELATION)
self.async_replication = PostgreSQLAsyncReplication(self)
self.watcher_offer = PostgreSQLWatcherRelation(self)
Expand Down Expand Up @@ -498,6 +518,49 @@ def substrate(self) -> Substrates:
"""
return Substrates.VM

def _reload_tls_after_push(self, event) -> None:
"""Reload PostgreSQL after the lib TLS handler stores+pushes certs.

Also fires on the TLS relations' relation_broken so detaching the TLS
operator re-renders Patroni with TLS disabled and reloads.

Mirror the handler's readiness guard: when the internal CA is absent the
handler defers its push (no files on disk), so skip the reload to avoid
rendering ssl:on against missing TLS files on an already-running unit.

A transient config-apply failure (Patroni API unreachable, member not
started) defers and retries rather than leaving stale TLS state or failing
the hook.
"""
if not self.get_secret(APP_SCOPE, "internal-ca"):
return
# Don't enable TLS in the config until the lib has written the cert files to
# disk (its push can defer while this local render would still succeed, which
# would start Patroni ssl:on against missing files).
if self.is_tls_enabled and not self.tls_manager.client_tls_files_on_disk():
event.defer()
return
try:
if not self.update_config():
event.defer()
except Exception:
logger.exception("TLS reload (update_config) failed; deferring")
event.defer()

def _regenerate_internal_cert(self, *, reload: bool = True) -> None:
"""Generate the internal peer cert, push it to the workload, and (optionally) reload.

reload=False is used at cluster bootstrap: the leader renders patroni.yml on
leader-elected and each replica renders it in _on_peer_relation_changed just
before starting Patroni, so a reload here would be redundant. The internal peer
cert does not toggle ssl in the config -- only the operator/client cert does, via
is_tls_enabled -- so skipping the reload cannot leave a stale ssl setting.
"""
self.tls_manager.generate_internal_peer_cert()
self.tls_manager.push_tls_files()
if reload:
self.update_config()

def _check_and_update_internal_cert(self) -> None:
"""Check if the internal cert CN matches the unit IP and regenerate if needed."""
try:
Expand All @@ -509,7 +572,7 @@ def _check_and_update_internal_cert(self) -> None:
!= self.state.unit_ip
)
):
self.tls.generate_internal_peer_cert()
self._regenerate_internal_cert()
except Exception:
logger.exception("Unable to check or update internal cert")

Expand Down Expand Up @@ -1548,7 +1611,7 @@ def is_standby_cluster(self) -> bool:
@property
def is_tls_enabled(self) -> bool:
"""Return whether TLS is enabled."""
return all(self.tls.get_client_tls_files())
return all(self.tls_manager.get_client_tls_files())

@property
def _peer_members_ips(self) -> set[str]:
Expand Down Expand Up @@ -1759,7 +1822,7 @@ def _on_leader_elected(self, event: LeaderElectedEvent) -> None: # noqa: C901
logger.debug("On leader elected failed to reconfigure cluster.")

if not self.get_secret(APP_SCOPE, "internal-ca"):
self.tls.generate_internal_peer_ca()
self.tls_manager.generate_internal_peer_ca()
self.update_config()

# Don't update connection endpoints in the first time this event run for
Expand Down Expand Up @@ -1946,7 +2009,7 @@ def _on_start(self, event: StartEvent) -> None:
event.defer()
return
if not self.get_secret(UNIT_SCOPE, "internal-cert"):
self.tls.generate_internal_peer_cert()
self._regenerate_internal_cert(reload=False)

self.unit_peer_data.update({"ip": self.state.unit_ip})
self._ensure_storage_layout()
Expand Down Expand Up @@ -2497,10 +2560,12 @@ def _update_certificate(self) -> None:
# Request the certificate only if there is already one. If there isn't,
# the certificate will be generated in the relation joined event when
# relating to the TLS Certificates Operator.
if all(self.tls.get_client_tls_files()) or all(self.tls.get_peer_tls_files()):
if all(self.tls_manager.get_client_tls_files()) or all(
self.tls_manager.get_peer_tls_files()
):
self.tls.refresh_tls_certificates_event.emit()
if self.get_secret(UNIT_SCOPE, "internal-cert"):
self.tls.generate_internal_peer_cert()
self._regenerate_internal_cert()

@property
def is_blocked(self) -> bool:
Expand Down Expand Up @@ -2582,37 +2647,6 @@ def _peers(self) -> Relation | None:
"""
return self.model.get_relation(PEER_RELATION)

def push_tls_files_to_workload(self) -> bool:
"""Move TLS files to the PostgreSQL storage path and enable TLS."""
key, ca, cert = self.tls.get_client_tls_files()
if key is not None:
render_file(Substrates.VM, f"{PATRONI_CONF_PATH}/{TLS_KEY_FILE}", key, 0o600)
if ca is not None:
render_file(Substrates.VM, f"{PATRONI_CONF_PATH}/{TLS_CA_FILE}", ca, 0o600)
if cert is not None:
render_file(Substrates.VM, f"{PATRONI_CONF_PATH}/{TLS_CERT_FILE}", cert, 0o600)

key, ca, cert = self.tls.get_peer_tls_files()
if key is not None:
render_file(Substrates.VM, f"{PATRONI_CONF_PATH}/peer_{TLS_KEY_FILE}", key, 0o600)
if ca is not None:
render_file(Substrates.VM, f"{PATRONI_CONF_PATH}/peer_{TLS_CA_FILE}", ca, 0o600)
if cert is not None:
render_file(Substrates.VM, f"{PATRONI_CONF_PATH}/peer_{TLS_CERT_FILE}", cert, 0o600)

render_file(
Substrates.VM,
f"{PATRONI_CONF_PATH}/{TLS_CA_BUNDLE_FILE}",
self.tls.get_peer_ca_bundle(),
0o600,
)

try:
return self.update_config()
except Exception:
logger.exception("TLS files failed to push. Error in config update")
return False

def push_ca_file_into_workload(self, secret_name: str) -> bool:
"""Move CA certificates file into the PostgreSQL storage path."""
certs = self.get_secret(UNIT_SCOPE, secret_name)
Expand Down
2 changes: 1 addition & 1 deletion src/relations/postgresql_provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -485,7 +485,7 @@ def update_endpoints(self, event: DatabaseRequestedEvent | None = None) -> None:
tls = "True" if self.charm.is_tls_enabled else "False"
ca = None
if tls == "True":
_, ca, _ = self.charm.tls.get_client_tls_files()
_, ca, _ = self.charm.tls_manager.get_client_tls_files()
if not ca:
ca = ""

Expand Down
Loading
Loading