postgresql-charms-single-kernel 16.3.5__tar.gz → 16.3.7__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {postgresql_charms_single_kernel-16.3.5 → postgresql_charms_single_kernel-16.3.7}/PKG-INFO +1 -1
- {postgresql_charms_single_kernel-16.3.5 → postgresql_charms_single_kernel-16.3.7}/pyproject.toml +1 -1
- {postgresql_charms_single_kernel-16.3.5 → postgresql_charms_single_kernel-16.3.7}/pyproject.toml.orig +1 -1
- {postgresql_charms_single_kernel-16.3.5 → postgresql_charms_single_kernel-16.3.7}/single_kernel_postgresql/charms/abstract_charm.py +49 -9
- {postgresql_charms_single_kernel-16.3.5 → postgresql_charms_single_kernel-16.3.7}/single_kernel_postgresql/charms/k8s_charm.py +21 -6
- {postgresql_charms_single_kernel-16.3.5 → postgresql_charms_single_kernel-16.3.7}/single_kernel_postgresql/charms/vm_charm.py +22 -6
- {postgresql_charms_single_kernel-16.3.5 → postgresql_charms_single_kernel-16.3.7}/single_kernel_postgresql/core/state.py +13 -14
- postgresql_charms_single_kernel-16.3.7/single_kernel_postgresql/events/database.py +243 -0
- {postgresql_charms_single_kernel-16.3.5 → postgresql_charms_single_kernel-16.3.7}/single_kernel_postgresql/managers/config.py +32 -9
- postgresql_charms_single_kernel-16.3.7/single_kernel_postgresql/managers/database.py +675 -0
- {postgresql_charms_single_kernel-16.3.5 → postgresql_charms_single_kernel-16.3.7}/LICENSE +0 -0
- {postgresql_charms_single_kernel-16.3.5 → postgresql_charms_single_kernel-16.3.7}/README.md +0 -0
- {postgresql_charms_single_kernel-16.3.5 → postgresql_charms_single_kernel-16.3.7}/single_kernel_postgresql/__init__.py +0 -0
- {postgresql_charms_single_kernel-16.3.5 → postgresql_charms_single_kernel-16.3.7}/single_kernel_postgresql/charmcraft.yaml +0 -0
- {postgresql_charms_single_kernel-16.3.5 → postgresql_charms_single_kernel-16.3.7}/single_kernel_postgresql/charms/__init__.py +0 -0
- {postgresql_charms_single_kernel-16.3.5 → postgresql_charms_single_kernel-16.3.7}/single_kernel_postgresql/compat/__init__.py +0 -0
- {postgresql_charms_single_kernel-16.3.5 → postgresql_charms_single_kernel-16.3.7}/single_kernel_postgresql/compat/postgresql.py +0 -0
- {postgresql_charms_single_kernel-16.3.5 → postgresql_charms_single_kernel-16.3.7}/single_kernel_postgresql/compat/pyproject.toml +0 -0
- {postgresql_charms_single_kernel-16.3.5 → postgresql_charms_single_kernel-16.3.7}/single_kernel_postgresql/config/__init__.py +0 -0
- {postgresql_charms_single_kernel-16.3.5 → postgresql_charms_single_kernel-16.3.7}/single_kernel_postgresql/config/enums.py +0 -0
- {postgresql_charms_single_kernel-16.3.5 → postgresql_charms_single_kernel-16.3.7}/single_kernel_postgresql/config/exceptions.py +0 -0
- {postgresql_charms_single_kernel-16.3.5 → postgresql_charms_single_kernel-16.3.7}/single_kernel_postgresql/config/literals.py +0 -0
- {postgresql_charms_single_kernel-16.3.5 → postgresql_charms_single_kernel-16.3.7}/single_kernel_postgresql/config/locales.py +0 -0
- {postgresql_charms_single_kernel-16.3.5 → postgresql_charms_single_kernel-16.3.7}/single_kernel_postgresql/config/pyproject.toml +0 -0
- {postgresql_charms_single_kernel-16.3.5 → postgresql_charms_single_kernel-16.3.7}/single_kernel_postgresql/config/statuses.py +0 -0
- {postgresql_charms_single_kernel-16.3.5 → postgresql_charms_single_kernel-16.3.7}/single_kernel_postgresql/core/__init__.py +0 -0
- {postgresql_charms_single_kernel-16.3.5 → postgresql_charms_single_kernel-16.3.7}/single_kernel_postgresql/core/config.py +0 -0
- {postgresql_charms_single_kernel-16.3.5 → postgresql_charms_single_kernel-16.3.7}/single_kernel_postgresql/core/peer_relation.py +0 -0
- {postgresql_charms_single_kernel-16.3.5 → postgresql_charms_single_kernel-16.3.7}/single_kernel_postgresql/core/relation_state.py +0 -0
- {postgresql_charms_single_kernel-16.3.5 → postgresql_charms_single_kernel-16.3.7}/single_kernel_postgresql/events/__init__.py +0 -0
- {postgresql_charms_single_kernel-16.3.5 → postgresql_charms_single_kernel-16.3.7}/single_kernel_postgresql/events/postgresql.py +0 -0
- {postgresql_charms_single_kernel-16.3.5 → postgresql_charms_single_kernel-16.3.7}/single_kernel_postgresql/events/tls.py +0 -0
- {postgresql_charms_single_kernel-16.3.5 → postgresql_charms_single_kernel-16.3.7}/single_kernel_postgresql/events/tls_transfer.py +0 -0
- {postgresql_charms_single_kernel-16.3.5 → postgresql_charms_single_kernel-16.3.7}/single_kernel_postgresql/lib/charms/data_platform_libs/v0/data_interfaces.py +0 -0
- {postgresql_charms_single_kernel-16.3.5 → postgresql_charms_single_kernel-16.3.7}/single_kernel_postgresql/lib/charms/data_platform_libs/v1/data_models.py +0 -0
- {postgresql_charms_single_kernel-16.3.5 → postgresql_charms_single_kernel-16.3.7}/single_kernel_postgresql/managers/__init__.py +0 -0
- {postgresql_charms_single_kernel-16.3.5 → postgresql_charms_single_kernel-16.3.7}/single_kernel_postgresql/managers/base.py +0 -0
- {postgresql_charms_single_kernel-16.3.5 → postgresql_charms_single_kernel-16.3.7}/single_kernel_postgresql/managers/cluster.py +0 -0
- {postgresql_charms_single_kernel-16.3.5 → postgresql_charms_single_kernel-16.3.7}/single_kernel_postgresql/managers/k8s.py +0 -0
- {postgresql_charms_single_kernel-16.3.5 → postgresql_charms_single_kernel-16.3.7}/single_kernel_postgresql/managers/patroni.py +0 -0
- {postgresql_charms_single_kernel-16.3.5 → postgresql_charms_single_kernel-16.3.7}/single_kernel_postgresql/managers/tls.py +0 -0
- {postgresql_charms_single_kernel-16.3.5 → postgresql_charms_single_kernel-16.3.7}/single_kernel_postgresql/templates/__init__.py +0 -0
- {postgresql_charms_single_kernel-16.3.5 → postgresql_charms_single_kernel-16.3.7}/single_kernel_postgresql/templates/patroni.yml.j2 +0 -0
- {postgresql_charms_single_kernel-16.3.5 → postgresql_charms_single_kernel-16.3.7}/single_kernel_postgresql/utils/__init__.py +0 -0
- {postgresql_charms_single_kernel-16.3.5 → postgresql_charms_single_kernel-16.3.7}/single_kernel_postgresql/utils/arch.py +0 -0
- {postgresql_charms_single_kernel-16.3.5 → postgresql_charms_single_kernel-16.3.7}/single_kernel_postgresql/utils/filesystem.py +0 -0
- {postgresql_charms_single_kernel-16.3.5 → postgresql_charms_single_kernel-16.3.7}/single_kernel_postgresql/utils/postgresql.py +0 -0
- {postgresql_charms_single_kernel-16.3.5 → postgresql_charms_single_kernel-16.3.7}/single_kernel_postgresql/utils/secret.py +0 -0
- {postgresql_charms_single_kernel-16.3.5 → postgresql_charms_single_kernel-16.3.7}/single_kernel_postgresql/utils/status.py +0 -0
- {postgresql_charms_single_kernel-16.3.5 → postgresql_charms_single_kernel-16.3.7}/single_kernel_postgresql/workload/__init__.py +0 -0
- {postgresql_charms_single_kernel-16.3.5 → postgresql_charms_single_kernel-16.3.7}/single_kernel_postgresql/workload/base.py +0 -0
- {postgresql_charms_single_kernel-16.3.5 → postgresql_charms_single_kernel-16.3.7}/single_kernel_postgresql/workload/k8s.py +0 -0
- {postgresql_charms_single_kernel-16.3.5 → postgresql_charms_single_kernel-16.3.7}/single_kernel_postgresql/workload/paths/__init__.py +0 -0
- {postgresql_charms_single_kernel-16.3.5 → postgresql_charms_single_kernel-16.3.7}/single_kernel_postgresql/workload/paths/base.py +0 -0
- {postgresql_charms_single_kernel-16.3.5 → postgresql_charms_single_kernel-16.3.7}/single_kernel_postgresql/workload/paths/k8s.py +0 -0
- {postgresql_charms_single_kernel-16.3.5 → postgresql_charms_single_kernel-16.3.7}/single_kernel_postgresql/workload/paths/vm.py +0 -0
- {postgresql_charms_single_kernel-16.3.5 → postgresql_charms_single_kernel-16.3.7}/single_kernel_postgresql/workload/vm.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: postgresql-charms-single-kernel
|
|
3
|
-
Version: 16.3.
|
|
3
|
+
Version: 16.3.7
|
|
4
4
|
Summary: Shared and reusable code for PostgreSQL-related charms
|
|
5
5
|
Author: Canonical Data Platform
|
|
6
6
|
Author-email: Canonical Data Platform <data-platform@lists.launchpad.net>
|
|
@@ -5,18 +5,25 @@
|
|
|
5
5
|
from abc import ABC, abstractmethod
|
|
6
6
|
|
|
7
7
|
from data_platform_helpers.advanced_statuses import StatusHandler
|
|
8
|
+
from ops import StatusBase
|
|
8
9
|
from ops.charm import CharmBase
|
|
9
10
|
|
|
10
11
|
from single_kernel_postgresql.core.state import CharmState
|
|
12
|
+
from single_kernel_postgresql.events.database import DatabaseEventsHandler
|
|
11
13
|
from single_kernel_postgresql.events.postgresql import PostgreSQLEventsHandler
|
|
12
14
|
from single_kernel_postgresql.events.tls import TLS
|
|
15
|
+
from single_kernel_postgresql.lib.charms.data_platform_libs.v0.data_interfaces import (
|
|
16
|
+
DatabaseProvides,
|
|
17
|
+
)
|
|
13
18
|
from single_kernel_postgresql.managers.cluster import ClusterManager
|
|
14
19
|
from single_kernel_postgresql.managers.config import ConfigManager
|
|
20
|
+
from single_kernel_postgresql.managers.database import DatabaseManager
|
|
15
21
|
from single_kernel_postgresql.managers.patroni import PatroniManager
|
|
16
22
|
from single_kernel_postgresql.managers.tls import TLSManager
|
|
17
|
-
from single_kernel_postgresql.workload.base import BaseWorkload
|
|
23
|
+
from single_kernel_postgresql.workload.base import BaseWorkload, ResourceProvider
|
|
18
24
|
|
|
19
25
|
from ..config.enums import Substrates
|
|
26
|
+
from ..config.literals import DATABASE
|
|
20
27
|
from ..utils.postgresql import PostgreSQL
|
|
21
28
|
|
|
22
29
|
|
|
@@ -42,13 +49,27 @@ class AbstractPostgreSQLCharm(CharmBase, ABC):
|
|
|
42
49
|
)
|
|
43
50
|
self.patroni_manager = PatroniManager(state=self.state, workload=self.workload)
|
|
44
51
|
self.cluster_manager = ClusterManager(state=self.state, workload=self.workload)
|
|
52
|
+
|
|
53
|
+
# Client-relation subsystem: the charm is the composition root, as with the
|
|
54
|
+
# other managers; the handler owns only the observers and guard/defer decisions.
|
|
55
|
+
self.database_manager = DatabaseManager(
|
|
56
|
+
state=self.state,
|
|
57
|
+
workload=self.workload,
|
|
58
|
+
database_provides=DatabaseProvides(self, relation_name=DATABASE),
|
|
59
|
+
set_unit_status=self.set_unit_status,
|
|
60
|
+
)
|
|
61
|
+
self.database = DatabaseEventsHandler(
|
|
62
|
+
self, self.state, self.database_manager, self.patroni_manager, self.tls_manager
|
|
63
|
+
)
|
|
64
|
+
|
|
45
65
|
self.config_manager = ConfigManager(
|
|
46
66
|
state=self.state,
|
|
47
67
|
workload=self.workload,
|
|
48
68
|
tls_manager=self.tls_manager,
|
|
49
69
|
patroni_manager=self.patroni_manager,
|
|
70
|
+
database_manager=self.database_manager,
|
|
71
|
+
resource_provider=self.get_resource_provider,
|
|
50
72
|
request_restart=self.request_restart,
|
|
51
|
-
refresh_endpoints=self.refresh_endpoints,
|
|
52
73
|
restart_services=self.restart_services,
|
|
53
74
|
)
|
|
54
75
|
|
|
@@ -93,20 +114,39 @@ class AbstractPostgreSQLCharm(CharmBase, ABC):
|
|
|
93
114
|
"""Access current substrate."""
|
|
94
115
|
pass
|
|
95
116
|
|
|
96
|
-
#
|
|
97
|
-
# substrate-tangled
|
|
98
|
-
#
|
|
117
|
+
# Charm-side bridges the lib calls back into. request_restart/restart_services are
|
|
118
|
+
# substrate-tangled and stay until their own migration phases; update_config still
|
|
119
|
+
# supplies the ldap/async/watcher values those phases own; primary_endpoint is the
|
|
120
|
+
# VM's Patroni-derived primary lookup. set_unit_status routes status writes through
|
|
121
|
+
# the charm_refresh priority gate and stays until the refresh logic itself migrates
|
|
122
|
+
# into the library, at which point the managers own their status writes.
|
|
99
123
|
@abstractmethod
|
|
100
|
-
def
|
|
101
|
-
"""
|
|
124
|
+
def get_resource_provider(self) -> ResourceProvider:
|
|
125
|
+
"""Return the substrate's (cpu_cores, memory_bytes) introspector."""
|
|
102
126
|
pass
|
|
103
127
|
|
|
104
128
|
@abstractmethod
|
|
105
|
-
def
|
|
106
|
-
"""
|
|
129
|
+
def request_restart(self) -> None:
|
|
130
|
+
"""Run the substrate pre-restart side effect and acquire the restart lock."""
|
|
107
131
|
pass
|
|
108
132
|
|
|
109
133
|
@abstractmethod
|
|
110
134
|
def restart_services(self) -> None:
|
|
111
135
|
"""Restart the monitoring and LDAP-sync sidecar services."""
|
|
112
136
|
pass
|
|
137
|
+
|
|
138
|
+
@abstractmethod
|
|
139
|
+
def set_unit_status(self, status: StatusBase) -> None:
|
|
140
|
+
"""Set the unit status without overriding a higher-priority refresh status."""
|
|
141
|
+
pass
|
|
142
|
+
|
|
143
|
+
@abstractmethod
|
|
144
|
+
def update_config(self) -> bool:
|
|
145
|
+
"""Re-render the Patroni configuration and apply it."""
|
|
146
|
+
pass
|
|
147
|
+
|
|
148
|
+
@property
|
|
149
|
+
@abstractmethod
|
|
150
|
+
def primary_endpoint(self) -> str | None:
|
|
151
|
+
"""Address of the cluster primary, or None when there is not one."""
|
|
152
|
+
pass
|
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
|
|
7
7
|
import logging
|
|
8
8
|
|
|
9
|
+
from ops import StatusBase
|
|
10
|
+
|
|
9
11
|
from single_kernel_postgresql.charms.abstract_charm import AbstractPostgreSQLCharm, PostgreSQL
|
|
10
12
|
from single_kernel_postgresql.config.enums import Substrates
|
|
11
13
|
from single_kernel_postgresql.config.literals import CONTAINER_NAME, SYSTEM_USERS, USER
|
|
@@ -26,7 +28,6 @@ class PostgreSQLK8sCharm(AbstractPostgreSQLCharm):
|
|
|
26
28
|
"Workload must be an instance of K8sWorkload"
|
|
27
29
|
)
|
|
28
30
|
self.k8s_manager = K8sManager(self.state, self.workload)
|
|
29
|
-
self.state.resource_provider = self.k8s_manager
|
|
30
31
|
|
|
31
32
|
@property
|
|
32
33
|
def postgresql(self) -> PostgreSQL:
|
|
@@ -67,13 +68,27 @@ class PostgreSQLK8sCharm(AbstractPostgreSQLCharm):
|
|
|
67
68
|
return Substrates.K8S
|
|
68
69
|
|
|
69
70
|
# The concrete production charm owns these bridges (update_scrape_job_spec +
|
|
70
|
-
# acquire_lock,
|
|
71
|
-
#
|
|
71
|
+
# acquire_lock, pebble metrics/ldap restarts, the refresh-aware status write and the
|
|
72
|
+
# config re-render), so they are minimal here.
|
|
73
|
+
def get_resource_provider(self) -> K8sManager:
|
|
74
|
+
"""Return the substrate's (cpu_cores, memory_bytes) introspector."""
|
|
75
|
+
return self.k8s_manager
|
|
76
|
+
|
|
72
77
|
def request_restart(self) -> None:
|
|
73
78
|
"""Run the substrate pre-restart side effect and acquire the restart lock."""
|
|
74
79
|
|
|
75
|
-
def refresh_endpoints(self) -> None:
|
|
76
|
-
"""Refresh the client relation endpoints."""
|
|
77
|
-
|
|
78
80
|
def restart_services(self) -> None:
|
|
79
81
|
"""Restart the monitoring and LDAP-sync sidecar services."""
|
|
82
|
+
|
|
83
|
+
def set_unit_status(self, status: StatusBase) -> None:
|
|
84
|
+
"""Set the unit status without overriding a higher-priority refresh status."""
|
|
85
|
+
self.unit.status = status
|
|
86
|
+
|
|
87
|
+
def update_config(self) -> bool:
|
|
88
|
+
"""Re-render the Patroni configuration and apply it."""
|
|
89
|
+
return self.config_manager.update_config(self.postgresql)
|
|
90
|
+
|
|
91
|
+
@property
|
|
92
|
+
def primary_endpoint(self) -> str | None:
|
|
93
|
+
"""Address of the cluster primary's Service."""
|
|
94
|
+
return self.state.primary_endpoint
|
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
|
|
7
7
|
import logging
|
|
8
8
|
|
|
9
|
+
from ops import StatusBase
|
|
10
|
+
|
|
9
11
|
from single_kernel_postgresql.charms.abstract_charm import AbstractPostgreSQLCharm, PostgreSQL
|
|
10
12
|
from single_kernel_postgresql.config.enums import Substrates
|
|
11
13
|
from single_kernel_postgresql.config.literals import SYSTEM_USERS, USER
|
|
@@ -20,7 +22,6 @@ class PostgreSQLVMCharm(AbstractPostgreSQLCharm):
|
|
|
20
22
|
def __init__(self, *args):
|
|
21
23
|
"""Initialize the PostgreSQL VM Charm."""
|
|
22
24
|
super().__init__(*args)
|
|
23
|
-
self.state.resource_provider = self.workload
|
|
24
25
|
|
|
25
26
|
@property
|
|
26
27
|
def postgresql(self) -> PostgreSQL:
|
|
@@ -58,13 +59,28 @@ class PostgreSQLVMCharm(AbstractPostgreSQLCharm):
|
|
|
58
59
|
return Substrates.VM
|
|
59
60
|
|
|
60
61
|
# The concrete production charm owns these bridges (pops postgresql_restarted +
|
|
61
|
-
# acquire_lock,
|
|
62
|
-
#
|
|
62
|
+
# acquire_lock, snap metrics/ldap restarts, the refresh-aware status write, the
|
|
63
|
+
# Patroni-derived primary lookup and the config re-render), so they are minimal here.
|
|
64
|
+
def get_resource_provider(self) -> VMWorkload:
|
|
65
|
+
"""Return the substrate's (cpu_cores, memory_bytes) introspector."""
|
|
66
|
+
return self.workload
|
|
67
|
+
|
|
63
68
|
def request_restart(self) -> None:
|
|
64
69
|
"""Run the substrate pre-restart side effect and acquire the restart lock."""
|
|
65
70
|
|
|
66
|
-
def refresh_endpoints(self) -> None:
|
|
67
|
-
"""Refresh the client relation endpoints."""
|
|
68
|
-
|
|
69
71
|
def restart_services(self) -> None:
|
|
70
72
|
"""Restart the monitoring and LDAP-sync sidecar services."""
|
|
73
|
+
|
|
74
|
+
def set_unit_status(self, status: StatusBase) -> None:
|
|
75
|
+
"""Set the unit status without overriding a higher-priority refresh status."""
|
|
76
|
+
self.unit.status = status
|
|
77
|
+
|
|
78
|
+
def update_config(self) -> bool:
|
|
79
|
+
"""Re-render the Patroni configuration and apply it."""
|
|
80
|
+
return self.config_manager.update_config(self.postgresql)
|
|
81
|
+
|
|
82
|
+
@property
|
|
83
|
+
def primary_endpoint(self) -> str | None:
|
|
84
|
+
"""Address of the cluster primary, or None when there is not one."""
|
|
85
|
+
primary = self.patroni_manager.get_primary()
|
|
86
|
+
return self.patroni_manager.get_member_ip(primary) if primary else None
|
|
@@ -8,7 +8,7 @@ import re
|
|
|
8
8
|
import socket
|
|
9
9
|
from contextlib import suppress
|
|
10
10
|
from functools import cached_property
|
|
11
|
-
from typing import
|
|
11
|
+
from typing import Any, get_args
|
|
12
12
|
|
|
13
13
|
from data_platform_helpers.advanced_statuses import StatusesState, StatusObject
|
|
14
14
|
from data_platform_helpers.advanced_statuses.types import Scope as AdvancedStatusesScope
|
|
@@ -44,9 +44,6 @@ from single_kernel_postgresql.utils import unit_name_to_pod_name
|
|
|
44
44
|
from single_kernel_postgresql.utils.secret import translate_field_to_secret_key
|
|
45
45
|
from single_kernel_postgresql.utils.status import format_status
|
|
46
46
|
|
|
47
|
-
if TYPE_CHECKING:
|
|
48
|
-
from single_kernel_postgresql.workload.base import ResourceProvider
|
|
49
|
-
|
|
50
47
|
|
|
51
48
|
class CharmState(Object):
|
|
52
49
|
"""The global PostgreSQL Charm State."""
|
|
@@ -64,16 +61,6 @@ class CharmState(Object):
|
|
|
64
61
|
|
|
65
62
|
self.statuses = StatusesState(self, STATUS_PEERS_RELATION)
|
|
66
63
|
|
|
67
|
-
# Set by the charm to the substrate's resource introspector (VM workload / K8s manager).
|
|
68
|
-
self.resource_provider: ResourceProvider | None = None
|
|
69
|
-
|
|
70
|
-
@property
|
|
71
|
-
def available_resources(self) -> tuple[int, int]:
|
|
72
|
-
"""(cpu_cores, memory_bytes) for this unit, via the substrate's provider."""
|
|
73
|
-
if self.resource_provider is None:
|
|
74
|
-
raise RuntimeError("resource_provider was not set by the charm")
|
|
75
|
-
return self.resource_provider.get_available_resources()
|
|
76
|
-
|
|
77
64
|
# -- Charm Config
|
|
78
65
|
@cached_property
|
|
79
66
|
def config(self) -> CharmConfig:
|
|
@@ -248,6 +235,13 @@ class CharmState(Object):
|
|
|
248
235
|
addresses.remove(current_unit_ip)
|
|
249
236
|
return addresses
|
|
250
237
|
|
|
238
|
+
def unit_database_address(self, unit: Unit, relation_name: str = DATABASE) -> str | None:
|
|
239
|
+
"""The client-facing address a peer unit published for the given relation."""
|
|
240
|
+
if not (peer_relation := self.peer_relation):
|
|
241
|
+
return None
|
|
242
|
+
with suppress(KeyError):
|
|
243
|
+
return peer_relation.data[unit].get(f"{relation_name}-address")
|
|
244
|
+
|
|
251
245
|
@property
|
|
252
246
|
def host(self) -> str:
|
|
253
247
|
"""Current unit host."""
|
|
@@ -539,3 +533,8 @@ class CharmState(Object):
|
|
|
539
533
|
def primary_endpoint(self) -> str:
|
|
540
534
|
"""Returns the endpoint of the primary instance's service."""
|
|
541
535
|
return self._build_service_name("primary")
|
|
536
|
+
|
|
537
|
+
@property
|
|
538
|
+
def replicas_endpoint(self) -> str:
|
|
539
|
+
"""Returns the endpoint of the replicas instances' service."""
|
|
540
|
+
return self._build_service_name("replicas")
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# Copyright 2026 Canonical Ltd.
|
|
3
|
+
# See LICENSE file for licensing details.
|
|
4
|
+
"""Client-relation events handler — owns the client-relation observers."""
|
|
5
|
+
|
|
6
|
+
import logging
|
|
7
|
+
|
|
8
|
+
from ops import BlockedStatus, ModelError, Object, RelationBrokenEvent, RelationDepartedEvent
|
|
9
|
+
|
|
10
|
+
from single_kernel_postgresql.config.enums import Substrates
|
|
11
|
+
from single_kernel_postgresql.config.literals import DATABASE, DATABASE_PORT
|
|
12
|
+
from single_kernel_postgresql.core.state import CharmState
|
|
13
|
+
from single_kernel_postgresql.lib.charms.data_platform_libs.v0.data_interfaces import (
|
|
14
|
+
DatabaseRequestedEvent,
|
|
15
|
+
)
|
|
16
|
+
from single_kernel_postgresql.managers.database import (
|
|
17
|
+
NO_ACCESS_TO_SECRET_MSG,
|
|
18
|
+
DatabaseManager,
|
|
19
|
+
DatabaseRequest,
|
|
20
|
+
)
|
|
21
|
+
from single_kernel_postgresql.managers.patroni import PatroniManager
|
|
22
|
+
from single_kernel_postgresql.managers.tls import TLSManager
|
|
23
|
+
from single_kernel_postgresql.utils.postgresql import (
|
|
24
|
+
PostgreSQLCreateDatabaseError,
|
|
25
|
+
PostgreSQLCreateUserError,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
logger = logging.getLogger(__name__)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class DatabaseEventsHandler(Object):
|
|
32
|
+
"""Handles the ``database`` (``postgresql_client``) provider relation.
|
|
33
|
+
|
|
34
|
+
Owns the three observers the client relation needs and drives the
|
|
35
|
+
:class:`~single_kernel_postgresql.managers.database.DatabaseManager` the charm
|
|
36
|
+
constructs, mirroring how the postgresql events handler receives its managers.
|
|
37
|
+
|
|
38
|
+
Each handler keeps its guard and its work in one observer: ``defer()`` is
|
|
39
|
+
per-observer, so splitting the readiness check from the action would let a deferred
|
|
40
|
+
action retry alone against state its guard never re-checked.
|
|
41
|
+
|
|
42
|
+
``charm`` is deliberately untyped, as in the TLS handler: the production charms do
|
|
43
|
+
not derive from :class:`AbstractPostgreSQLCharm` until the cutover phase, and they
|
|
44
|
+
are the callers that construct this.
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
def __init__(
|
|
48
|
+
self,
|
|
49
|
+
charm,
|
|
50
|
+
state: CharmState,
|
|
51
|
+
database_manager: DatabaseManager,
|
|
52
|
+
patroni_manager: PatroniManager,
|
|
53
|
+
tls_manager: TLSManager,
|
|
54
|
+
relation_name: str = DATABASE,
|
|
55
|
+
) -> None:
|
|
56
|
+
super().__init__(charm, key="database")
|
|
57
|
+
self.charm = charm
|
|
58
|
+
self.state = state
|
|
59
|
+
self.relation_name = relation_name
|
|
60
|
+
# The charm is the composition root: it constructs the manager and the provider
|
|
61
|
+
# interface; this handler owns the observers and the guard/defer decisions.
|
|
62
|
+
self.manager = database_manager
|
|
63
|
+
self.database_provides = database_manager.database_provides
|
|
64
|
+
# Held for the readiness guards and the endpoint-input gathers: the manager
|
|
65
|
+
# takes no peer-manager references.
|
|
66
|
+
self.patroni_manager = patroni_manager
|
|
67
|
+
self.tls_manager = tls_manager
|
|
68
|
+
|
|
69
|
+
self.framework.observe(
|
|
70
|
+
charm.on[relation_name].relation_departed, self._on_relation_departed
|
|
71
|
+
)
|
|
72
|
+
self.framework.observe(charm.on[relation_name].relation_broken, self._on_relation_broken)
|
|
73
|
+
self.framework.observe(
|
|
74
|
+
self.database_provides.on.database_requested, self._on_database_requested
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
def _ready_for_request(self) -> bool:
|
|
78
|
+
"""Whether the cluster can serve a new client request right now."""
|
|
79
|
+
if not self.state.application.is_cluster_initialised:
|
|
80
|
+
return False
|
|
81
|
+
if self.state.substrate == Substrates.K8S:
|
|
82
|
+
return self.patroni_manager.primary_endpoint_ready
|
|
83
|
+
return bool(self.patroni_manager.member_started and self.charm.primary_endpoint)
|
|
84
|
+
|
|
85
|
+
def _ready_for_removal(self) -> bool:
|
|
86
|
+
"""Whether the cluster can serve a relation removal right now.
|
|
87
|
+
|
|
88
|
+
K8s settles for a started member here where a request additionally waits on the
|
|
89
|
+
primary endpoint being reachable.
|
|
90
|
+
"""
|
|
91
|
+
if not self.state.application.is_cluster_initialised:
|
|
92
|
+
return False
|
|
93
|
+
if self.state.substrate == Substrates.K8S:
|
|
94
|
+
return self.patroni_manager.member_started
|
|
95
|
+
return bool(self.patroni_manager.member_started and self.charm.primary_endpoint)
|
|
96
|
+
|
|
97
|
+
def _on_database_requested(self, event: DatabaseRequestedEvent) -> None:
|
|
98
|
+
"""Generate password and handle user and database creation for the related app."""
|
|
99
|
+
# The provider library only emits this on the leader.
|
|
100
|
+
if not self.state.peer.is_app_leader:
|
|
101
|
+
return
|
|
102
|
+
|
|
103
|
+
if not self._ready_for_request():
|
|
104
|
+
logger.debug(
|
|
105
|
+
"Deferring on_database_requested: %s",
|
|
106
|
+
"Cluster must be initialized before database can be requested"
|
|
107
|
+
if self.state.substrate == Substrates.K8S
|
|
108
|
+
else "cluster not initialized, Patroni not started or primary endpoint not available",
|
|
109
|
+
)
|
|
110
|
+
event.defer()
|
|
111
|
+
return
|
|
112
|
+
|
|
113
|
+
# The vendored property resolves the secret on access: until the grant lands it
|
|
114
|
+
# raises ModelError, which the manager's own guard can no longer see.
|
|
115
|
+
try:
|
|
116
|
+
requested_entity_secret_content = event.requested_entity_secret_content
|
|
117
|
+
except ModelError:
|
|
118
|
+
self.charm.set_unit_status(BlockedStatus(NO_ACCESS_TO_SECRET_MSG))
|
|
119
|
+
return
|
|
120
|
+
|
|
121
|
+
request = DatabaseRequest(
|
|
122
|
+
relation_id=event.relation.id,
|
|
123
|
+
database=event.database or "",
|
|
124
|
+
extra_user_roles=event.extra_user_roles,
|
|
125
|
+
prefix_matching=event.prefix_matching,
|
|
126
|
+
requested_entity_secret_content=requested_entity_secret_content,
|
|
127
|
+
)
|
|
128
|
+
postgresql = self.charm.postgresql
|
|
129
|
+
|
|
130
|
+
if not (creds := self.manager.get_credentials(postgresql, request)):
|
|
131
|
+
return
|
|
132
|
+
user, password = creds
|
|
133
|
+
|
|
134
|
+
if not (databases_setup := self.manager.collect_databases(postgresql, user, request)):
|
|
135
|
+
return
|
|
136
|
+
database, databases = databases_setup
|
|
137
|
+
|
|
138
|
+
self.manager.update_username_mapping(event.relation.id, user)
|
|
139
|
+
self.charm.update_config()
|
|
140
|
+
if not self.manager.are_units_in_sync():
|
|
141
|
+
logger.debug("Not all units have synced configuration")
|
|
142
|
+
event.defer()
|
|
143
|
+
return
|
|
144
|
+
|
|
145
|
+
extra_user_roles = self.manager.build_extra_user_roles(event.extra_user_roles)
|
|
146
|
+
|
|
147
|
+
try:
|
|
148
|
+
self.manager.create_relation_user_and_database(
|
|
149
|
+
postgresql, user, password, database, databases, extra_user_roles
|
|
150
|
+
)
|
|
151
|
+
self.database_provides.set_credentials(event.relation.id, user, password)
|
|
152
|
+
self._publish_k8s_inline_endpoints(event.relation.id, user, password, database)
|
|
153
|
+
self.database_provides.set_version(
|
|
154
|
+
event.relation.id, postgresql.get_postgresql_version()
|
|
155
|
+
)
|
|
156
|
+
self.database_provides.set_database(event.relation.id, database)
|
|
157
|
+
if self.state.substrate == Substrates.K8S:
|
|
158
|
+
# K8s refreshed every relation from the request handler.
|
|
159
|
+
self.manager.update_endpoints(
|
|
160
|
+
client_tls_files=self.tls_manager.get_client_tls_files()
|
|
161
|
+
)
|
|
162
|
+
else:
|
|
163
|
+
self.manager.update_endpoints(
|
|
164
|
+
event.relation.id,
|
|
165
|
+
online_members=self.patroni_manager.online_cluster_members(),
|
|
166
|
+
client_tls_files=self.tls_manager.get_client_tls_files(),
|
|
167
|
+
)
|
|
168
|
+
self.manager.update_unit_status(postgresql, event.relation)
|
|
169
|
+
self.charm.update_config()
|
|
170
|
+
except self.manager.request_errors as e:
|
|
171
|
+
if self.state.substrate == Substrates.K8S:
|
|
172
|
+
logger.exception(e)
|
|
173
|
+
self.charm.set_unit_status(
|
|
174
|
+
BlockedStatus(
|
|
175
|
+
e.message
|
|
176
|
+
if isinstance(e, PostgreSQLCreateDatabaseError | PostgreSQLCreateUserError)
|
|
177
|
+
and e.message is not None
|
|
178
|
+
# The clearer of the two charms' wordings, now shared by both.
|
|
179
|
+
else f"Failed to initialize {self.relation_name} relation"
|
|
180
|
+
)
|
|
181
|
+
)
|
|
182
|
+
return
|
|
183
|
+
|
|
184
|
+
def _publish_k8s_inline_endpoints(
|
|
185
|
+
self, relation_id: int, user: str, password: str, database: str
|
|
186
|
+
) -> None:
|
|
187
|
+
"""Publish the endpoint/URI/TLS fields the K8s charm wrote inside the request handler.
|
|
188
|
+
|
|
189
|
+
``update_endpoints`` writes the same values moments later, so this is redundant;
|
|
190
|
+
it is kept so the port does not change K8s databag write ordering.
|
|
191
|
+
"""
|
|
192
|
+
if self.state.substrate != Substrates.K8S:
|
|
193
|
+
return
|
|
194
|
+
primary = f"{self.state.primary_endpoint}:{DATABASE_PORT}"
|
|
195
|
+
self.database_provides.set_endpoints(relation_id, primary)
|
|
196
|
+
self.database_provides.set_uris(
|
|
197
|
+
relation_id, f"postgresql://{user}:{password}@{primary}/{database}"
|
|
198
|
+
)
|
|
199
|
+
client_tls_files = self.tls_manager.get_client_tls_files()
|
|
200
|
+
self.database_provides.set_tls(relation_id, "True" if all(client_tls_files) else "False")
|
|
201
|
+
if all(client_tls_files):
|
|
202
|
+
self.database_provides.set_tls_ca(relation_id, client_tls_files[1] or "")
|
|
203
|
+
|
|
204
|
+
def _on_relation_departed(self, event: RelationDepartedEvent) -> None:
|
|
205
|
+
"""Set a flag to avoid deleting database users when not wanted."""
|
|
206
|
+
# Set a flag to avoid deleting database users when this unit
|
|
207
|
+
# is removed and receives relation broken events from related applications.
|
|
208
|
+
# This is needed because of https://bugs.launchpad.net/juju/+bug/1979811.
|
|
209
|
+
if event.departing_unit == self.state.model.unit and self.state.peer_relation:
|
|
210
|
+
self.state.peer.data.update({"departing": "True"})
|
|
211
|
+
|
|
212
|
+
def _on_relation_broken(self, event: RelationBrokenEvent) -> None:
|
|
213
|
+
"""Remove the user created for this relation."""
|
|
214
|
+
if not self.state.peer_relation or not self._ready_for_removal():
|
|
215
|
+
logger.debug(
|
|
216
|
+
"Deferring on_relation_broken: %s",
|
|
217
|
+
"Cluster must be initialized before user can be deleted"
|
|
218
|
+
if self.state.substrate == Substrates.K8S
|
|
219
|
+
else "Cluster must be initialized and primary available before user can be deleted",
|
|
220
|
+
)
|
|
221
|
+
event.defer()
|
|
222
|
+
return
|
|
223
|
+
|
|
224
|
+
postgresql = self.charm.postgresql
|
|
225
|
+
self.manager.update_unit_status(postgresql, event.relation)
|
|
226
|
+
|
|
227
|
+
if self.state.peer.is_unit_departing:
|
|
228
|
+
logger.debug("Early exit on_relation_broken: Skipping departing unit")
|
|
229
|
+
return
|
|
230
|
+
|
|
231
|
+
if not self.state.peer.is_app_leader:
|
|
232
|
+
user = self.manager.get_username_mapping().get(
|
|
233
|
+
str(event.relation.id), self.manager.relation_username(event.relation.id)
|
|
234
|
+
)
|
|
235
|
+
if user in postgresql.list_users():
|
|
236
|
+
logger.debug("Deferring on_relation_broken: user was not deleted yet")
|
|
237
|
+
event.defer()
|
|
238
|
+
else:
|
|
239
|
+
self.charm.update_config()
|
|
240
|
+
return
|
|
241
|
+
|
|
242
|
+
self.manager.delete_relation_user(postgresql, event.relation.id)
|
|
243
|
+
self.charm.update_config()
|
|
@@ -36,10 +36,11 @@ from single_kernel_postgresql.managers.patroni import PatroniManager
|
|
|
36
36
|
from single_kernel_postgresql.managers.tls import TLSManager
|
|
37
37
|
from single_kernel_postgresql.utils import _change_owner, render_file
|
|
38
38
|
from single_kernel_postgresql.utils.postgresql import PostgreSQL as PostgreSQLClient
|
|
39
|
-
from single_kernel_postgresql.workload.base import BaseWorkload
|
|
39
|
+
from single_kernel_postgresql.workload.base import BaseWorkload, ResourceProvider
|
|
40
40
|
|
|
41
41
|
if TYPE_CHECKING:
|
|
42
42
|
# Import-time only: the VM workload pulls the snap charm lib, which K8s does not ship.
|
|
43
|
+
from single_kernel_postgresql.managers.database import DatabaseManager
|
|
43
44
|
from single_kernel_postgresql.workload.vm import VMWorkload
|
|
44
45
|
|
|
45
46
|
logger = logging.getLogger(__name__)
|
|
@@ -57,17 +58,21 @@ class ConfigManager(BaseManager):
|
|
|
57
58
|
workload: BaseWorkload,
|
|
58
59
|
tls_manager: TLSManager,
|
|
59
60
|
patroni_manager: PatroniManager,
|
|
61
|
+
database_manager: "DatabaseManager",
|
|
62
|
+
resource_provider: Callable[[], ResourceProvider],
|
|
60
63
|
request_restart: Callable[[], None],
|
|
61
|
-
refresh_endpoints: Callable[[], None],
|
|
62
64
|
restart_services: Callable[[], None],
|
|
63
65
|
):
|
|
64
66
|
super().__init__(state, workload, "config_manager")
|
|
65
67
|
self.tls_manager = tls_manager
|
|
66
68
|
self.patroni_manager = patroni_manager
|
|
67
|
-
|
|
68
|
-
#
|
|
69
|
+
self.database_manager = database_manager
|
|
70
|
+
# Resolved on use, not at construction: the K8s manager that provides it is built
|
|
71
|
+
# after this manager in the charm's __init__.
|
|
72
|
+
self.resource_provider = resource_provider
|
|
73
|
+
# Charm-side bridges: the substrate-tangled restart trigger and the monitoring/ldap
|
|
74
|
+
# service restarts stay in the charm until their own migration phases.
|
|
69
75
|
self.request_restart = request_restart
|
|
70
|
-
self.refresh_endpoints = refresh_endpoints
|
|
71
76
|
self.restart_services = restart_services
|
|
72
77
|
|
|
73
78
|
@staticmethod
|
|
@@ -304,6 +309,24 @@ class ConfigManager(BaseManager):
|
|
|
304
309
|
self.tls_manager.client_tls_files_on_disk()
|
|
305
310
|
)
|
|
306
311
|
|
|
312
|
+
def _client_relation_endpoint_inputs(self) -> dict:
|
|
313
|
+
"""Gather the DatabaseManager.update_endpoints inputs this caller must supply.
|
|
314
|
+
|
|
315
|
+
The Patroni cluster-status query is a live REST call: gather it only where
|
|
316
|
+
update_endpoints would use it — on the leader (it returns early otherwise) and
|
|
317
|
+
on VM (K8s endpoints come from the Services state). The raw TLS files are
|
|
318
|
+
passed as-is: the endpoint publish uses the databag-only view, not this
|
|
319
|
+
manager's on-disk-augmented is_tls_enabled.
|
|
320
|
+
"""
|
|
321
|
+
return {
|
|
322
|
+
"online_members": (
|
|
323
|
+
self.patroni_manager.online_cluster_members()
|
|
324
|
+
if self.state.peer.is_app_leader and self.state.substrate == Substrates.VM
|
|
325
|
+
else None
|
|
326
|
+
),
|
|
327
|
+
"client_tls_files": self.tls_manager.get_client_tls_files(),
|
|
328
|
+
}
|
|
329
|
+
|
|
307
330
|
@cached_property
|
|
308
331
|
def generate_config_hash(self) -> str:
|
|
309
332
|
"""Generate current configuration hash."""
|
|
@@ -432,7 +455,7 @@ class ConfigManager(BaseManager):
|
|
|
432
455
|
pass
|
|
433
456
|
|
|
434
457
|
self.state.peer.tls = self.is_tls_enabled
|
|
435
|
-
self.
|
|
458
|
+
self.database_manager.update_endpoints(**self._client_relation_endpoint_inputs())
|
|
436
459
|
|
|
437
460
|
# Restart PostgreSQL if TLS configuration has changed
|
|
438
461
|
# (so the both old and new connections use the configuration).
|
|
@@ -443,7 +466,6 @@ class ConfigManager(BaseManager):
|
|
|
443
466
|
def update_config(
|
|
444
467
|
self,
|
|
445
468
|
postgresql_client: PostgreSQLClient,
|
|
446
|
-
user_hash: str,
|
|
447
469
|
is_creating_backup: bool = False,
|
|
448
470
|
# TODO add rel handler
|
|
449
471
|
relations_user_databases_map: dict[str, Any] | None = None,
|
|
@@ -468,7 +490,7 @@ class ConfigManager(BaseManager):
|
|
|
468
490
|
# Snapshot resources once so parameter-building and the API patch agree. On K8s
|
|
469
491
|
# these are lightkube reads; re-fetching per callee doubled the API calls and
|
|
470
492
|
# could disagree mid-scaling.
|
|
471
|
-
cpu_cores, available_memory = self.
|
|
493
|
+
cpu_cores, available_memory = self.resource_provider().get_available_resources()
|
|
472
494
|
|
|
473
495
|
# Build PostgreSQL parameters
|
|
474
496
|
pg_parameters = self._build_postgresql_parameters(
|
|
@@ -514,7 +536,7 @@ class ConfigManager(BaseManager):
|
|
|
514
536
|
# in a bundle together with the TLS certificates operator. This flag is used to
|
|
515
537
|
# know when to call the Patroni API using HTTP or HTTPS.
|
|
516
538
|
self.state.peer.tls = self.is_tls_enabled
|
|
517
|
-
self.
|
|
539
|
+
self.database_manager.update_endpoints(**self._client_relation_endpoint_inputs())
|
|
518
540
|
logger.debug("Early exit update_config: Workload not started yet")
|
|
519
541
|
return True
|
|
520
542
|
|
|
@@ -563,6 +585,7 @@ class ConfigManager(BaseManager):
|
|
|
563
585
|
|
|
564
586
|
self.restart_services()
|
|
565
587
|
|
|
588
|
+
user_hash = self.database_manager.user_hash
|
|
566
589
|
self.state.peer.user_hash = user_hash
|
|
567
590
|
self.state.peer.config_hash = self.generate_config_hash
|
|
568
591
|
if self.state.peer.is_app_leader:
|