Skip to content
Draft
2 changes: 2 additions & 0 deletions refresh_versions.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
charm = "16/0.0.0"
workload = "16.0"
99 changes: 95 additions & 4 deletions single_kernel_postgresql/charms/abstract_charm.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"""Skeleton for the abstract charm."""

from abc import ABC, abstractmethod
from typing import TYPE_CHECKING
from typing import TYPE_CHECKING, cast

from data_platform_helpers.advanced_statuses import StatusHandler
from ops import StatusBase
Expand All @@ -20,6 +20,7 @@
from single_kernel_postgresql.events.database import DatabaseEventsHandler
from single_kernel_postgresql.events.ldap import LDAP
from single_kernel_postgresql.events.logical_replication import PostgreSQLLogicalReplication
from single_kernel_postgresql.events.observer import ClusterTopologyChangeCharmEvents
from single_kernel_postgresql.events.postgresql import PostgreSQLEventsHandler
from single_kernel_postgresql.events.tls import TLS
from single_kernel_postgresql.events.watcher import WatcherEventsHandler
Expand All @@ -30,23 +31,38 @@
from single_kernel_postgresql.managers.async_replication import (
AsyncReplicationManager,
)
from single_kernel_postgresql.managers.backup import BackupManager
from single_kernel_postgresql.managers.cluster import ClusterManager
from single_kernel_postgresql.managers.cluster_membership import ClusterMembershipManager
from single_kernel_postgresql.managers.config import ConfigManager
from single_kernel_postgresql.managers.database import DatabaseManager
from single_kernel_postgresql.managers.logical_replication import LogicalReplicationManager
from single_kernel_postgresql.managers.observer import ObserverManager
from single_kernel_postgresql.managers.patroni import PatroniManager
from single_kernel_postgresql.managers.raft import RaftManager
from single_kernel_postgresql.managers.refresh import RefreshManager
from single_kernel_postgresql.managers.restore import RestoreManager
from single_kernel_postgresql.managers.tls import TLSManager
from single_kernel_postgresql.utils.s3 import S3Client
from single_kernel_postgresql.workload.base import BaseWorkload, ResourceProvider

from ..config.enums import Substrates
from ..config.literals import DATABASE, S3_RELATION_NAME
from ..config.literals import (
DATABASE,
REPLICATION_CONSUMER_RELATION,
REPLICATION_OFFER_RELATION,
S3_RELATION_NAME,
)
from ..utils.postgresql import PostgreSQL


class AbstractPostgreSQLCharm(CharmBase, ABC):
"""An abstract PostgreSQL charm."""

# Custom charm events dispatched by the observer scripts (cluster topology
# changes and raft reconnection).
on = ClusterTopologyChangeCharmEvents()

def __init__(self, *args):
super().__init__(*args)

Expand Down Expand Up @@ -151,6 +167,7 @@ def __init__(self, *args):
resource_provider=self.get_resource_provider,
request_restart=self.request_restart,
restart_services=self.restart_services,
set_unit_status=self.set_unit_status,
logical_replication_slots=self.logical_replication.replication_slots,
)

Expand All @@ -164,6 +181,61 @@ def __init__(self, *args):
set_default_status=self.set_default_unit_status,
)

# The watcher handler feeds the membership subsystem (endpoints, raft addresses).
self.watcher_handler = WatcherEventsHandler(self, self.workload, self.state)

# The RAFT manager owns the low-level raft operations; the membership
# manager owns the VM peer-relation orchestration and the raft
# recovery state machine (late-bound bridges as callables).
self.raft_manager = RaftManager(
state=self.state,
workload=self.workload,
patroni_manager=self.patroni_manager,
watcher_handler=self.watcher_handler,
update_config=self.update_config,
set_unit_status=self.set_unit_status,
peer_relation_changed=lambda event: (
self.postgresql_events_handler._on_peer_relation_changed(event) # ty: ignore[unresolved-attribute]
),
remove_from_members_ips=lambda ip: self.membership_manager.remove_member_ip(ip),
)
self.membership_manager = ClusterMembershipManager(
state=self.state,
workload=self.workload,
patroni_manager=self.patroni_manager,
update_config=self.update_config,
set_unit_status=self.set_unit_status,
watcher_handler=self.watcher_handler,
async_replication_manager=self.async_replication_manager,
update_relation_endpoints=self.update_relation_endpoints,
raft_manager=self.raft_manager,
set_primary_status_message=self.set_primary_status_message,
)

# The backup subsystem (port of the VM charm's composition wiring): the
# charm-side duplicates are retired when the thin charm lands.
self.s3_client = S3Client(self.workload)
self.backup_manager = BackupManager(
state=self.state,
workload=self.workload,
s3_client=self.s3_client,
patroni_manager=self.patroni_manager,
update_config=self.update_config,
resource_provider=cast("ResourceProvider", self.workload),
is_standby_cluster=self._is_standby_cluster,
set_unit_status=self.set_unit_status,
refresh_primary_status=self.set_primary_status_message,
)
self.restore_manager = RestoreManager(
state=self.state,
workload=self.workload,
patroni_manager=self.patroni_manager,
update_config=self.update_config,
backup_manager=self.backup_manager,
is_standby_cluster=self._is_standby_cluster,
)
self.observer_manager = ObserverManager(self.state, self.workload)

# Events Handler
self.postgresql_events_handler = PostgreSQLEventsHandler(
self,
Expand All @@ -174,8 +246,14 @@ def __init__(self, *args):
self.config_manager,
self.patroni_manager,
self.refresh_manager,
membership_manager=self.membership_manager,
backup_manager=self.backup_manager,
restore_manager=self.restore_manager,
observer_manager=self.observer_manager,
async_replication_manager=self.async_replication_manager,
database_manager=self.database_manager,
postgresql=lambda: self.postgresql,
)
self.watcher_handler = WatcherEventsHandler(self, self.workload, self.state)

# Resume or prepare the refresh (the charms' post-construction resume block).
self.refresh_manager.on_init()
Expand All @@ -191,6 +269,15 @@ def __init__(self, *args):
self.logical_replication_manager,
)

def _is_standby_cluster(self) -> bool:
"""Whether this unit belongs to a standby (read-only) cluster (port)."""
if (
self.state.model.get_relation(REPLICATION_CONSUMER_RELATION) is None
and self.state.model.get_relation(REPLICATION_OFFER_RELATION) is None
):
return False
return not self.async_replication_manager.is_primary_cluster()

# Postgresql Client
@property
@abstractmethod
Expand Down Expand Up @@ -270,7 +357,11 @@ def set_default_unit_status(self) -> None:

@abstractmethod
def update_config(
self, *, refresh: "charm_refresh.Machines | charm_refresh.Kubernetes | None" = None
self,
is_creating_backup: bool = False,
no_peers: bool = False,
*,
refresh: "charm_refresh.Machines | charm_refresh.Kubernetes | None" = None,
) -> bool:
"""Re-render the Patroni configuration and apply it."""
pass
Expand Down
6 changes: 5 additions & 1 deletion single_kernel_postgresql/charms/k8s_charm.py
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,11 @@ def has_async_replication_relation(self) -> bool:
return False

def update_config(
self, *, refresh: "charm_refresh.Machines | charm_refresh.Kubernetes | None" = None
self,
is_creating_backup: bool = False,
no_peers: bool = False,
*,
refresh: "charm_refresh.Machines | charm_refresh.Kubernetes | None" = None,
) -> bool:
"""Re-render the Patroni configuration and apply it."""
return self.config_manager.update_config(self.postgresql)
Expand Down
Loading
Loading