postgresql-charms-single-kernel 16.3.10__tar.gz → 16.3.12__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.
Files changed (77) hide show
  1. {postgresql_charms_single_kernel-16.3.10 → postgresql_charms_single_kernel-16.3.12}/PKG-INFO +1 -1
  2. {postgresql_charms_single_kernel-16.3.10 → postgresql_charms_single_kernel-16.3.12}/pyproject.toml +4 -4
  3. {postgresql_charms_single_kernel-16.3.10 → postgresql_charms_single_kernel-16.3.12}/pyproject.toml.orig +4 -4
  4. {postgresql_charms_single_kernel-16.3.10 → postgresql_charms_single_kernel-16.3.12}/single_kernel_postgresql/charms/abstract_charm.py +80 -0
  5. {postgresql_charms_single_kernel-16.3.10 → postgresql_charms_single_kernel-16.3.12}/single_kernel_postgresql/charms/k8s_charm.py +28 -1
  6. {postgresql_charms_single_kernel-16.3.10 → postgresql_charms_single_kernel-16.3.12}/single_kernel_postgresql/charms/vm_charm.py +7 -0
  7. {postgresql_charms_single_kernel-16.3.10 → postgresql_charms_single_kernel-16.3.12}/single_kernel_postgresql/config/literals.py +10 -2
  8. {postgresql_charms_single_kernel-16.3.10 → postgresql_charms_single_kernel-16.3.12}/single_kernel_postgresql/core/peer_relation.py +35 -31
  9. postgresql_charms_single_kernel-16.3.12/single_kernel_postgresql/events/async_replication.py +397 -0
  10. postgresql_charms_single_kernel-16.3.12/single_kernel_postgresql/events/watcher.py +524 -0
  11. postgresql_charms_single_kernel-16.3.12/single_kernel_postgresql/managers/async_replication.py +967 -0
  12. {postgresql_charms_single_kernel-16.3.10 → postgresql_charms_single_kernel-16.3.12}/single_kernel_postgresql/managers/k8s.py +23 -1
  13. postgresql_charms_single_kernel-16.3.12/single_kernel_postgresql/scripts/authorisation_rules_observer.py +145 -0
  14. postgresql_charms_single_kernel-16.3.12/single_kernel_postgresql/scripts/cluster_topology_observer.py +177 -0
  15. postgresql_charms_single_kernel-16.3.12/single_kernel_postgresql/scripts/raft_observer.py +85 -0
  16. {postgresql_charms_single_kernel-16.3.10 → postgresql_charms_single_kernel-16.3.12}/single_kernel_postgresql/workload/base.py +15 -0
  17. {postgresql_charms_single_kernel-16.3.10 → postgresql_charms_single_kernel-16.3.12}/single_kernel_postgresql/workload/k8s.py +52 -1
  18. {postgresql_charms_single_kernel-16.3.10 → postgresql_charms_single_kernel-16.3.12}/single_kernel_postgresql/workload/paths/base.py +12 -0
  19. {postgresql_charms_single_kernel-16.3.10 → postgresql_charms_single_kernel-16.3.12}/single_kernel_postgresql/workload/paths/k8s.py +12 -0
  20. {postgresql_charms_single_kernel-16.3.10 → postgresql_charms_single_kernel-16.3.12}/single_kernel_postgresql/workload/paths/vm.py +11 -0
  21. {postgresql_charms_single_kernel-16.3.10 → postgresql_charms_single_kernel-16.3.12}/single_kernel_postgresql/workload/vm.py +82 -0
  22. {postgresql_charms_single_kernel-16.3.10 → postgresql_charms_single_kernel-16.3.12}/LICENSE +0 -0
  23. {postgresql_charms_single_kernel-16.3.10 → postgresql_charms_single_kernel-16.3.12}/README.md +0 -0
  24. {postgresql_charms_single_kernel-16.3.10 → postgresql_charms_single_kernel-16.3.12}/single_kernel_postgresql/__init__.py +0 -0
  25. {postgresql_charms_single_kernel-16.3.10 → postgresql_charms_single_kernel-16.3.12}/single_kernel_postgresql/charmcraft.yaml +0 -0
  26. {postgresql_charms_single_kernel-16.3.10 → postgresql_charms_single_kernel-16.3.12}/single_kernel_postgresql/charms/__init__.py +0 -0
  27. {postgresql_charms_single_kernel-16.3.10 → postgresql_charms_single_kernel-16.3.12}/single_kernel_postgresql/compat/__init__.py +0 -0
  28. {postgresql_charms_single_kernel-16.3.10 → postgresql_charms_single_kernel-16.3.12}/single_kernel_postgresql/compat/postgresql.py +0 -0
  29. {postgresql_charms_single_kernel-16.3.10 → postgresql_charms_single_kernel-16.3.12}/single_kernel_postgresql/compat/pyproject.toml +0 -0
  30. {postgresql_charms_single_kernel-16.3.10 → postgresql_charms_single_kernel-16.3.12}/single_kernel_postgresql/config/__init__.py +0 -0
  31. {postgresql_charms_single_kernel-16.3.10 → postgresql_charms_single_kernel-16.3.12}/single_kernel_postgresql/config/enums.py +0 -0
  32. {postgresql_charms_single_kernel-16.3.10 → postgresql_charms_single_kernel-16.3.12}/single_kernel_postgresql/config/exceptions.py +0 -0
  33. {postgresql_charms_single_kernel-16.3.10 → postgresql_charms_single_kernel-16.3.12}/single_kernel_postgresql/config/locales.py +0 -0
  34. {postgresql_charms_single_kernel-16.3.10 → postgresql_charms_single_kernel-16.3.12}/single_kernel_postgresql/config/pyproject.toml +0 -0
  35. {postgresql_charms_single_kernel-16.3.10 → postgresql_charms_single_kernel-16.3.12}/single_kernel_postgresql/config/statuses.py +0 -0
  36. {postgresql_charms_single_kernel-16.3.10 → postgresql_charms_single_kernel-16.3.12}/single_kernel_postgresql/core/__init__.py +0 -0
  37. {postgresql_charms_single_kernel-16.3.10 → postgresql_charms_single_kernel-16.3.12}/single_kernel_postgresql/core/config.py +0 -0
  38. {postgresql_charms_single_kernel-16.3.10 → postgresql_charms_single_kernel-16.3.12}/single_kernel_postgresql/core/relation_state.py +0 -0
  39. {postgresql_charms_single_kernel-16.3.10 → postgresql_charms_single_kernel-16.3.12}/single_kernel_postgresql/core/s3.py +0 -0
  40. {postgresql_charms_single_kernel-16.3.10 → postgresql_charms_single_kernel-16.3.12}/single_kernel_postgresql/core/state.py +0 -0
  41. {postgresql_charms_single_kernel-16.3.10 → postgresql_charms_single_kernel-16.3.12}/single_kernel_postgresql/events/__init__.py +0 -0
  42. {postgresql_charms_single_kernel-16.3.10 → postgresql_charms_single_kernel-16.3.12}/single_kernel_postgresql/events/backup.py +0 -0
  43. {postgresql_charms_single_kernel-16.3.10 → postgresql_charms_single_kernel-16.3.12}/single_kernel_postgresql/events/database.py +0 -0
  44. {postgresql_charms_single_kernel-16.3.10 → postgresql_charms_single_kernel-16.3.12}/single_kernel_postgresql/events/ldap.py +0 -0
  45. {postgresql_charms_single_kernel-16.3.10 → postgresql_charms_single_kernel-16.3.12}/single_kernel_postgresql/events/postgresql.py +0 -0
  46. {postgresql_charms_single_kernel-16.3.10 → postgresql_charms_single_kernel-16.3.12}/single_kernel_postgresql/events/tls.py +0 -0
  47. {postgresql_charms_single_kernel-16.3.10 → postgresql_charms_single_kernel-16.3.12}/single_kernel_postgresql/events/tls_transfer.py +0 -0
  48. {postgresql_charms_single_kernel-16.3.10 → postgresql_charms_single_kernel-16.3.12}/single_kernel_postgresql/lib/charms/data_platform_libs/v0/data_interfaces.py +0 -0
  49. {postgresql_charms_single_kernel-16.3.10 → postgresql_charms_single_kernel-16.3.12}/single_kernel_postgresql/lib/charms/data_platform_libs/v0/s3.py +0 -0
  50. {postgresql_charms_single_kernel-16.3.10 → postgresql_charms_single_kernel-16.3.12}/single_kernel_postgresql/lib/charms/data_platform_libs/v1/data_models.py +0 -0
  51. {postgresql_charms_single_kernel-16.3.10 → postgresql_charms_single_kernel-16.3.12}/single_kernel_postgresql/lib/charms/glauth_k8s/v0/ldap.py +0 -0
  52. {postgresql_charms_single_kernel-16.3.10 → postgresql_charms_single_kernel-16.3.12}/single_kernel_postgresql/managers/__init__.py +0 -0
  53. {postgresql_charms_single_kernel-16.3.10 → postgresql_charms_single_kernel-16.3.12}/single_kernel_postgresql/managers/backup.py +0 -0
  54. {postgresql_charms_single_kernel-16.3.10 → postgresql_charms_single_kernel-16.3.12}/single_kernel_postgresql/managers/base.py +0 -0
  55. {postgresql_charms_single_kernel-16.3.10 → postgresql_charms_single_kernel-16.3.12}/single_kernel_postgresql/managers/cluster.py +0 -0
  56. {postgresql_charms_single_kernel-16.3.10 → postgresql_charms_single_kernel-16.3.12}/single_kernel_postgresql/managers/config.py +0 -0
  57. {postgresql_charms_single_kernel-16.3.10 → postgresql_charms_single_kernel-16.3.12}/single_kernel_postgresql/managers/database.py +0 -0
  58. {postgresql_charms_single_kernel-16.3.10 → postgresql_charms_single_kernel-16.3.12}/single_kernel_postgresql/managers/patroni.py +0 -0
  59. {postgresql_charms_single_kernel-16.3.10 → postgresql_charms_single_kernel-16.3.12}/single_kernel_postgresql/managers/restore.py +0 -0
  60. {postgresql_charms_single_kernel-16.3.10 → postgresql_charms_single_kernel-16.3.12}/single_kernel_postgresql/managers/tls.py +0 -0
  61. {postgresql_charms_single_kernel-16.3.10 → postgresql_charms_single_kernel-16.3.12}/single_kernel_postgresql/scripts/rotate_logs.py +0 -0
  62. {postgresql_charms_single_kernel-16.3.10 → postgresql_charms_single_kernel-16.3.12}/single_kernel_postgresql/templates/__init__.py +0 -0
  63. {postgresql_charms_single_kernel-16.3.10 → postgresql_charms_single_kernel-16.3.12}/single_kernel_postgresql/templates/k8s/pgbackrest.conf.j2 +0 -0
  64. {postgresql_charms_single_kernel-16.3.10 → postgresql_charms_single_kernel-16.3.12}/single_kernel_postgresql/templates/k8s/pgbackrest.logrotate.j2 +0 -0
  65. {postgresql_charms_single_kernel-16.3.10 → postgresql_charms_single_kernel-16.3.12}/single_kernel_postgresql/templates/patroni.yml.j2 +0 -0
  66. {postgresql_charms_single_kernel-16.3.10 → postgresql_charms_single_kernel-16.3.12}/single_kernel_postgresql/templates/vm/pgbackrest.conf.j2 +0 -0
  67. {postgresql_charms_single_kernel-16.3.10 → postgresql_charms_single_kernel-16.3.12}/single_kernel_postgresql/templates/vm/pgbackrest.logrotate.j2 +0 -0
  68. {postgresql_charms_single_kernel-16.3.10 → postgresql_charms_single_kernel-16.3.12}/single_kernel_postgresql/utils/__init__.py +0 -0
  69. {postgresql_charms_single_kernel-16.3.10 → postgresql_charms_single_kernel-16.3.12}/single_kernel_postgresql/utils/arch.py +0 -0
  70. {postgresql_charms_single_kernel-16.3.10 → postgresql_charms_single_kernel-16.3.12}/single_kernel_postgresql/utils/backup.py +0 -0
  71. {postgresql_charms_single_kernel-16.3.10 → postgresql_charms_single_kernel-16.3.12}/single_kernel_postgresql/utils/filesystem.py +0 -0
  72. {postgresql_charms_single_kernel-16.3.10 → postgresql_charms_single_kernel-16.3.12}/single_kernel_postgresql/utils/postgresql.py +0 -0
  73. {postgresql_charms_single_kernel-16.3.10 → postgresql_charms_single_kernel-16.3.12}/single_kernel_postgresql/utils/s3.py +0 -0
  74. {postgresql_charms_single_kernel-16.3.10 → postgresql_charms_single_kernel-16.3.12}/single_kernel_postgresql/utils/secret.py +0 -0
  75. {postgresql_charms_single_kernel-16.3.10 → postgresql_charms_single_kernel-16.3.12}/single_kernel_postgresql/utils/status.py +0 -0
  76. {postgresql_charms_single_kernel-16.3.10 → postgresql_charms_single_kernel-16.3.12}/single_kernel_postgresql/workload/__init__.py +0 -0
  77. {postgresql_charms_single_kernel-16.3.10 → postgresql_charms_single_kernel-16.3.12}/single_kernel_postgresql/workload/paths/__init__.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: postgresql-charms-single-kernel
3
- Version: 16.3.10
3
+ Version: 16.3.12
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>
@@ -1,7 +1,7 @@
1
1
  [project]
2
2
  name = "postgresql-charms-single-kernel"
3
3
  description = "Shared and reusable code for PostgreSQL-related charms"
4
- version = "16.3.10"
4
+ version = "16.3.12"
5
5
  readme = "README.md"
6
6
  classifiers = [
7
7
  "Development Status :: 3 - Alpha",
@@ -157,12 +157,12 @@ exclude = [
157
157
  ]
158
158
 
159
159
  [dependency-groups]
160
- format = ["ruff==0.16.8"]
160
+ format = ["ruff==0.16.10"]
161
161
  lint = [
162
162
  "codespell==2.4.3",
163
- "ty==0.0.82",
163
+ "ty==0.0.84",
164
164
  ]
165
165
  unit = [
166
- "coverage[toml]==7.16.1",
166
+ "coverage[toml]==7.16.2",
167
167
  "pytest==9.1.1",
168
168
  ]
@@ -4,7 +4,7 @@
4
4
  [project]
5
5
  name = "postgresql-charms-single-kernel"
6
6
  description = "Shared and reusable code for PostgreSQL-related charms"
7
- version = "16.3.10"
7
+ version = "16.3.12"
8
8
  readme = "README.md"
9
9
  license = {file = "LICENSE"}
10
10
  authors = [
@@ -63,14 +63,14 @@ module-root = ""
63
63
 
64
64
  [dependency-groups]
65
65
  format = [
66
- "ruff==0.16.8"
66
+ "ruff==0.16.10"
67
67
  ]
68
68
  lint = [
69
69
  "codespell==2.4.3",
70
- "ty==0.0.82"
70
+ "ty==0.0.84"
71
71
  ]
72
72
  unit = [
73
- "coverage[toml]==7.16.1",
73
+ "coverage[toml]==7.16.2",
74
74
  "pytest==9.1.1"
75
75
  ]
76
76
 
@@ -3,20 +3,26 @@
3
3
  """Skeleton for the abstract charm."""
4
4
 
5
5
  from abc import ABC, abstractmethod
6
+ from typing import TYPE_CHECKING
6
7
 
7
8
  from data_platform_helpers.advanced_statuses import StatusHandler
8
9
  from ops import StatusBase
9
10
  from ops.charm import CharmBase
10
11
 
11
12
  from single_kernel_postgresql.core.state import CharmState
13
+ from single_kernel_postgresql.events.async_replication import PostgreSQLAsyncReplication
12
14
  from single_kernel_postgresql.events.database import DatabaseEventsHandler
13
15
  from single_kernel_postgresql.events.ldap import LDAP
14
16
  from single_kernel_postgresql.events.postgresql import PostgreSQLEventsHandler
15
17
  from single_kernel_postgresql.events.tls import TLS
18
+ from single_kernel_postgresql.events.watcher import WatcherEventsHandler
16
19
  from single_kernel_postgresql.lib.charms.data_platform_libs.v0.data_interfaces import (
17
20
  DatabaseProvides,
18
21
  )
19
22
  from single_kernel_postgresql.lib.charms.data_platform_libs.v0.s3 import S3Requirer
23
+ from single_kernel_postgresql.managers.async_replication import (
24
+ AsyncReplicationManager,
25
+ )
20
26
  from single_kernel_postgresql.managers.cluster import ClusterManager
21
27
  from single_kernel_postgresql.managers.config import ConfigManager
22
28
  from single_kernel_postgresql.managers.database import DatabaseManager
@@ -28,6 +34,9 @@ from ..config.enums import Substrates
28
34
  from ..config.literals import DATABASE, S3_RELATION_NAME
29
35
  from ..utils.postgresql import PostgreSQL
30
36
 
37
+ if TYPE_CHECKING:
38
+ from single_kernel_postgresql.managers.k8s import K8sManager
39
+
31
40
 
32
41
  class AbstractPostgreSQLCharm(CharmBase, ABC):
33
42
  """An abstract PostgreSQL charm."""
@@ -60,6 +69,38 @@ class AbstractPostgreSQLCharm(CharmBase, ABC):
60
69
  self.patroni_manager = PatroniManager(state=self.state, workload=self.workload)
61
70
  self.cluster_manager = ClusterManager(state=self.state, workload=self.workload)
62
71
 
72
+ # Substrate-only K8s API seam: the K8s charm builds it by overriding
73
+ # build_k8s_manager(); VM charms need no K8s API access. Built here so the
74
+ # async-replication subsystem (below) can consume it.
75
+ k8s_manager = self.build_k8s_manager()
76
+
77
+ # Async-replication subsystem: the manager owns the data plane and the
78
+ # promotion/standby flows; the handler owns the observers and the public facade.
79
+ watcher = self._async_watcher()
80
+ self.async_replication_manager = AsyncReplicationManager(
81
+ state=self.state,
82
+ workload=self.workload,
83
+ patroni_manager=self.patroni_manager,
84
+ update_config=self.update_config,
85
+ set_unit_status=self.set_unit_status,
86
+ set_primary_status_message=self.set_primary_status_message,
87
+ set_app_status=self._recompute_async_app_status,
88
+ create_pgdata=self.create_pgdata,
89
+ fix_leader_annotation=self.fix_leader_annotation,
90
+ re_emit_relation_changed=self._re_emit_async_relation_changed,
91
+ k8s_manager=k8s_manager,
92
+ watcher=watcher,
93
+ )
94
+ self.async_replication = PostgreSQLAsyncReplication(
95
+ self,
96
+ self.state,
97
+ self.async_replication_manager,
98
+ self.patroni_manager,
99
+ self.workload,
100
+ k8s_manager=k8s_manager,
101
+ watcher=watcher,
102
+ )
103
+
63
104
  # Client-relation subsystem: the charm is the composition root, as with the
64
105
  # other managers; the handler owns only the observers and guard/defer decisions.
65
106
  self.database_manager = DatabaseManager(
@@ -94,6 +135,7 @@ class AbstractPostgreSQLCharm(CharmBase, ABC):
94
135
  self.config_manager,
95
136
  self.patroni_manager,
96
137
  )
138
+ self.watcher_handler = WatcherEventsHandler(self, self.workload, self.state)
97
139
 
98
140
  # Status Handler
99
141
  self.status_handler = StatusHandler(
@@ -146,6 +188,16 @@ class AbstractPostgreSQLCharm(CharmBase, ABC):
146
188
  """Restart the monitoring and LDAP-sync sidecar services."""
147
189
  pass
148
190
 
191
+ @abstractmethod
192
+ def set_app_status(self, status: StatusBase) -> None:
193
+ """Set the application status through the charm's own status gates."""
194
+ pass
195
+
196
+ @abstractmethod
197
+ def set_primary_status_message(self) -> None:
198
+ """Recompute the unit's primary/standby status message."""
199
+ pass
200
+
149
201
  @abstractmethod
150
202
  def set_unit_status(self, status: StatusBase) -> None:
151
203
  """Set the unit status without overriding a higher-priority refresh status."""
@@ -156,6 +208,34 @@ class AbstractPostgreSQLCharm(CharmBase, ABC):
156
208
  """Re-render the Patroni configuration and apply it."""
157
209
  pass
158
210
 
211
+ # Async-replication wiring helpers. The manager is constructed before the handler,
212
+ # so the framework-facing plumbing is re-exposed through these late-binding bridges;
213
+ # create_pgdata/fix_leader_annotation are K8s-only and default to no-ops the K8s
214
+ # charm overrides.
215
+ def _recompute_async_app_status(self) -> None:
216
+ """Recompute the async-replication app status through the handler."""
217
+ self.async_replication.set_app_status()
218
+
219
+ def _re_emit_async_relation_changed(self) -> None:
220
+ """Re-emitting the async relation-changed event goes through the handler."""
221
+ self.async_replication._re_emit_async_relation_changed_event()
222
+
223
+ def _async_watcher(self) -> "WatcherEventsHandler | None":
224
+ """Overridable hook supplying the VM watcher bridge (K8s has none)."""
225
+ return None
226
+
227
+ def build_k8s_manager(self) -> "K8sManager | None":
228
+ """Overridable hook supplying the K8s API seam (K8s charm only)."""
229
+ return None
230
+
231
+ def create_pgdata(self) -> None:
232
+ """Create the PostgreSQL data directories (K8s only; overridden there)."""
233
+ return None
234
+
235
+ def fix_leader_annotation(self) -> bool:
236
+ """Fix the leader annotation (K8s only; overridden there)."""
237
+ return False
238
+
159
239
  @property
160
240
  @abstractmethod
161
241
  def primary_endpoint(self) -> str | None:
@@ -5,6 +5,7 @@
5
5
  """PostgreSQL Kubernetes Charm."""
6
6
 
7
7
  import logging
8
+ from typing import cast
8
9
 
9
10
  from ops import StatusBase
10
11
 
@@ -21,13 +22,24 @@ logger = logging.getLogger(__name__)
21
22
  class PostgreSQLK8sCharm(AbstractPostgreSQLCharm):
22
23
  """PostgreSQL K8s Charm."""
23
24
 
25
+ # Built by build_k8s_manager() while the abstract __init__ wires the
26
+ # async-replication subsystem; never None on the K8s substrate.
27
+ k8s_manager: K8sManager
28
+
24
29
  def __init__(self, *args):
25
30
  """Initialize the PostgreSQL Kubernetes Charm."""
26
31
  super().__init__(*args)
27
32
  assert isinstance(self.workload, K8sWorkload), ( # noqa: S101
28
33
  "Workload must be an instance of K8sWorkload"
29
34
  )
30
- self.k8s_manager = K8sManager(self.state, self.workload)
35
+
36
+ def build_k8s_manager(self) -> K8sManager:
37
+ """Build the K8s API seam the async-replication subsystem consumes."""
38
+ self.k8s_manager = K8sManager(
39
+ self.state,
40
+ cast("K8sWorkload", self.workload),
41
+ )
42
+ return self.k8s_manager
31
43
 
32
44
  @property
33
45
  def postgresql(self) -> PostgreSQL:
@@ -88,6 +100,21 @@ class PostgreSQLK8sCharm(AbstractPostgreSQLCharm):
88
100
  """Re-render the Patroni configuration and apply it."""
89
101
  return self.config_manager.update_config(self.postgresql)
90
102
 
103
+ def set_app_status(self, status: StatusBase) -> None:
104
+ """Set the application status; the production charm gates this on its own state."""
105
+ self.app.status = status
106
+
107
+ def set_primary_status_message(self) -> None:
108
+ """Recompute the unit's primary/standby status message."""
109
+
110
+ def fix_leader_annotation(self) -> bool:
111
+ """Fix the leader annotation; the production charm owns the real implementation."""
112
+ return False
113
+
114
+ def create_pgdata(self) -> None:
115
+ """Create the PostgreSQL data directories (ported from the K8s charm)."""
116
+ self.workload.init_storage()
117
+
91
118
  @property
92
119
  def primary_endpoint(self) -> str | None:
93
120
  """Address of the cluster primary's Service."""
@@ -79,6 +79,13 @@ class PostgreSQLVMCharm(AbstractPostgreSQLCharm):
79
79
  """Re-render the Patroni configuration and apply it."""
80
80
  return self.config_manager.update_config(self.postgresql)
81
81
 
82
+ def set_app_status(self, status: StatusBase) -> None:
83
+ """Set the application status; the production charm gates this on its own state."""
84
+ self.app.status = status
85
+
86
+ def set_primary_status_message(self) -> None:
87
+ """Recompute the unit's primary/standby status message."""
88
+
82
89
  @property
83
90
  def primary_endpoint(self) -> str | None:
84
91
  """Address of the cluster primary, or None when there is not one."""
@@ -42,6 +42,8 @@ VM_PGBACKREST_LOGS_PATH = "var/log/pgbackrest"
42
42
 
43
43
  ## K8s Paths
44
44
  K8S_DATA_PATH = "var/lib/pg/data"
45
+ K8S_ARCHIVE_PATH = "var/lib/pg/archive"
46
+ K8S_TEMP_STORAGE_PATH = "var/lib/pg/temp"
45
47
  K8S_PGBACKREST_LOGS_PATH = "16/main/pgbackrest_logs"
46
48
 
47
49
  ## Shared Paths
@@ -134,6 +136,10 @@ ALL_CLIENT_RELATIONS = [DATABASE]
134
136
  REPLICATION_CONSUMER_RELATION = "replication"
135
137
  REPLICATION_OFFER_RELATION = "replication-offer"
136
138
 
139
+ # Async replication peer-data key holding the id of the labelless shared cluster-credentials
140
+ # secret the owner persists: referenced by id everywhere, never by label (DPE-10203).
141
+ ASYNC_SHARED_SECRET_ID_KEY = "async-replication-secret-id" # noqa: S105 — a databag key name, not a credential
142
+
137
143
  # TLS files
138
144
  TLS_KEY_FILE = "key.pem"
139
145
  TLS_CA_FILE = "ca.pem"
@@ -192,8 +198,6 @@ VM_ROTATE_LOGS_LOG_FILE = "/var/log/rotate_logs.log"
192
198
 
193
199
  ## K8s restore storage paths (must match the metadata.yaml storage locations)
194
200
  K8S_LOGS_STORAGE_PATH = "var/lib/pg/logs"
195
- K8S_ARCHIVE_PATH = "var/lib/pg/archive"
196
- K8S_TEMP_STORAGE_PATH = "var/lib/pg/temp"
197
201
  K8S_WAL_DIR = "pg_wal"
198
202
  K8S_TEMP_TABLESPACE_DIR = "pgsql_tmp"
199
203
  K8S_PG_LOGS_PATH = "16/main/pg_logs"
@@ -207,3 +211,7 @@ K8S_PGBACKREST_LOGS_SYMLINK_PATH = "var/log/pgbackrest"
207
211
  VM_PATRONICTL_EXECUTABLE = "charmed-postgresql.patronictl"
208
212
  PATRONICTL_REMOVE_CONFIRMATION = "Yes I am aware"
209
213
  RESTORE_REPEAT_CAUSE = "restore-backup"
214
+
215
+ # Watcher relation
216
+ RAFT_PORT = 2222
217
+ RAFT_PARTNER_PREFIX = "partner_node_status_server_"
@@ -9,7 +9,7 @@ import json
9
9
  from collections.abc import MutableMapping
10
10
  from functools import cached_property
11
11
 
12
- from ops import ActiveStatus, Application, BlockedStatus, Relation, Unit
12
+ from ops import ActiveStatus, Application, BlockedStatus, ModelError, Relation, Unit
13
13
 
14
14
  from single_kernel_postgresql.config.enums import Substrates
15
15
  from single_kernel_postgresql.config.literals import (
@@ -27,22 +27,19 @@ from single_kernel_postgresql.lib.charms.data_platform_libs.v0.data_interfaces i
27
27
  )
28
28
 
29
29
 
30
- class PostgreSQLPeer(RelationState):
31
- """State/Relation data collection for a PostgreSQL unit."""
30
+ class PeerRelationState(RelationState):
31
+ """Common state for the unit and application sides of the peer relation."""
32
32
 
33
- data_interface: DataPeerUnitData
34
- unit: Unit
33
+ data_interface: DataPeerData
35
34
 
36
35
  def __init__(
37
36
  self,
38
37
  relation: Relation | None,
39
- data_interface: DataPeerUnitData,
40
- component: Unit,
38
+ data_interface: DataPeerData,
39
+ component: Unit | Application,
41
40
  ):
42
- """Initialize the PostgreSQLPeer object."""
43
41
  super().__init__(relation, data_interface, component)
44
42
  self.data_interface = data_interface
45
- self.unit = component
46
43
 
47
44
  def get_secret(self, key: str) -> str | None:
48
45
  """Get the secret value for 'key' from the peer relation data."""
@@ -62,6 +59,23 @@ class PostgreSQLPeer(RelationState):
62
59
  return
63
60
  self.data_interface.delete_relation_data(self.relation.id, [key])
64
61
 
62
+
63
+ class PostgreSQLPeer(PeerRelationState):
64
+ """State/Relation data collection for a PostgreSQL unit."""
65
+
66
+ data_interface: DataPeerUnitData
67
+ unit: Unit
68
+
69
+ def __init__(
70
+ self,
71
+ relation: Relation | None,
72
+ data_interface: DataPeerUnitData,
73
+ component: Unit,
74
+ ):
75
+ """Initialize the PostgreSQLPeer object."""
76
+ super().__init__(relation, data_interface, component)
77
+ self.unit = component
78
+
65
79
  @property
66
80
  def is_app_leader(self) -> bool:
67
81
  """Check if the current unit is the leader of the application."""
@@ -384,7 +398,7 @@ class PostgreSQLPeer(RelationState):
384
398
  return peer_addrs
385
399
 
386
400
 
387
- class PostgreSQLApplication(RelationState):
401
+ class PostgreSQLApplication(PeerRelationState):
388
402
  """An PostgreSQL Application is the peer application state.
389
403
 
390
404
  This class defines state/relation data for a single PostgreSQL application.
@@ -403,7 +417,6 @@ class PostgreSQLApplication(RelationState):
403
417
  """Initialize the PostgreSQLApplication object."""
404
418
  super().__init__(relation, data_interface, component)
405
419
  self.app = component
406
- self.data_interface = data_interface
407
420
  self.substrate = substrate
408
421
 
409
422
  @property
@@ -498,8 +511,17 @@ class PostgreSQLApplication(RelationState):
498
511
 
499
512
  @cached_property
500
513
  def planned_units(self) -> int:
501
- """Get the number of planned units for the application."""
502
- return self.app.planned_units()
514
+ """Get the number of planned units for the application.
515
+
516
+ ops implements ``Application.planned_units()`` via ``goal-state``, which fails
517
+ ("saas application ... not found") while a cross-model SAAS force-removed during a
518
+ dead-DC teardown still lingers in goal-state. Fall back to the count of currently
519
+ known units so the hook reconciles instead of crashing every caller (DPE-10203).
520
+ """
521
+ try:
522
+ return self.app.planned_units()
523
+ except ModelError:
524
+ return len(self.relation.units) + 1 if self.relation else 1
503
525
 
504
526
  @property
505
527
  def members_ips(self) -> set[str]:
@@ -674,24 +696,6 @@ class PostgreSQLApplication(RelationState):
674
696
  return
675
697
  self.relation.data[self.app]["restore-to-time"] = value
676
698
 
677
- def get_secret(self, key: str) -> str | None:
678
- """Get the secret value for 'key' from the peer relation data."""
679
- if not self.relation:
680
- return None
681
- return self.data_interface.get_secret(self.relation.id, key)
682
-
683
- def set_secret(self, key: str, value: str) -> None:
684
- """Set the secret value for 'key' in the peer relation data."""
685
- if not self.relation:
686
- return
687
- self.data_interface.set_secret(self.relation.id, key, value)
688
-
689
- def remove_secret(self, key: str) -> None:
690
- """Remove the secret value for 'key' from the peer relation data."""
691
- if not self.relation:
692
- return
693
- self.data_interface.delete_relation_data(self.relation.id, [key])
694
-
695
699
  @cached_property
696
700
  def data(self) -> MutableMapping[str, str]:
697
701
  """Escape hatch method to access the peer data directly."""