postgresql-charms-single-kernel 16.3.0__tar.gz → 16.3.2__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 (54) hide show
  1. {postgresql_charms_single_kernel-16.3.0 → postgresql_charms_single_kernel-16.3.2}/PKG-INFO +8 -3
  2. {postgresql_charms_single_kernel-16.3.0 → postgresql_charms_single_kernel-16.3.2}/pyproject.toml +15 -8
  3. {postgresql_charms_single_kernel-16.3.0 → postgresql_charms_single_kernel-16.3.2}/single_kernel_postgresql/charms/abstract_charm.py +4 -12
  4. {postgresql_charms_single_kernel-16.3.0 → postgresql_charms_single_kernel-16.3.2}/single_kernel_postgresql/charms/k8s_charm.py +1 -1
  5. postgresql_charms_single_kernel-16.3.2/single_kernel_postgresql/config/exceptions.py +75 -0
  6. {postgresql_charms_single_kernel-16.3.0 → postgresql_charms_single_kernel-16.3.2}/single_kernel_postgresql/config/literals.py +14 -1
  7. {postgresql_charms_single_kernel-16.3.0 → postgresql_charms_single_kernel-16.3.2}/single_kernel_postgresql/core/peer_relation.py +71 -1
  8. {postgresql_charms_single_kernel-16.3.0 → postgresql_charms_single_kernel-16.3.2}/single_kernel_postgresql/core/state.py +83 -7
  9. {postgresql_charms_single_kernel-16.3.0 → postgresql_charms_single_kernel-16.3.2}/single_kernel_postgresql/events/postgresql.py +2 -2
  10. {postgresql_charms_single_kernel-16.3.0 → postgresql_charms_single_kernel-16.3.2}/single_kernel_postgresql/managers/base.py +1 -5
  11. {postgresql_charms_single_kernel-16.3.0 → postgresql_charms_single_kernel-16.3.2}/single_kernel_postgresql/managers/cluster.py +11 -10
  12. postgresql_charms_single_kernel-16.3.2/single_kernel_postgresql/managers/config.py +295 -0
  13. {postgresql_charms_single_kernel-16.3.0 → postgresql_charms_single_kernel-16.3.2}/single_kernel_postgresql/managers/k8s.py +2 -3
  14. postgresql_charms_single_kernel-16.3.2/single_kernel_postgresql/managers/patroni.py +896 -0
  15. {postgresql_charms_single_kernel-16.3.0 → postgresql_charms_single_kernel-16.3.2}/single_kernel_postgresql/managers/tls.py +2 -3
  16. {postgresql_charms_single_kernel-16.3.0 → postgresql_charms_single_kernel-16.3.2}/single_kernel_postgresql/workload/base.py +5 -0
  17. {postgresql_charms_single_kernel-16.3.0 → postgresql_charms_single_kernel-16.3.2}/single_kernel_postgresql/workload/k8s.py +15 -6
  18. {postgresql_charms_single_kernel-16.3.0 → postgresql_charms_single_kernel-16.3.2}/single_kernel_postgresql/workload/paths/base.py +16 -2
  19. {postgresql_charms_single_kernel-16.3.0 → postgresql_charms_single_kernel-16.3.2}/single_kernel_postgresql/workload/paths/k8s.py +15 -4
  20. {postgresql_charms_single_kernel-16.3.0 → postgresql_charms_single_kernel-16.3.2}/single_kernel_postgresql/workload/paths/vm.py +16 -4
  21. {postgresql_charms_single_kernel-16.3.0 → postgresql_charms_single_kernel-16.3.2}/single_kernel_postgresql/workload/vm.py +1 -1
  22. postgresql_charms_single_kernel-16.3.0/single_kernel_postgresql/config/exceptions.py +0 -27
  23. postgresql_charms_single_kernel-16.3.0/single_kernel_postgresql/managers/config.py +0 -194
  24. postgresql_charms_single_kernel-16.3.0/single_kernel_postgresql/managers/patroni.py +0 -109
  25. {postgresql_charms_single_kernel-16.3.0 → postgresql_charms_single_kernel-16.3.2}/LICENSE +0 -0
  26. {postgresql_charms_single_kernel-16.3.0 → postgresql_charms_single_kernel-16.3.2}/README.md +0 -0
  27. {postgresql_charms_single_kernel-16.3.0 → postgresql_charms_single_kernel-16.3.2}/single_kernel_postgresql/__init__.py +0 -0
  28. {postgresql_charms_single_kernel-16.3.0 → postgresql_charms_single_kernel-16.3.2}/single_kernel_postgresql/charmcraft.yaml +0 -0
  29. {postgresql_charms_single_kernel-16.3.0 → postgresql_charms_single_kernel-16.3.2}/single_kernel_postgresql/charms/__init__.py +0 -0
  30. {postgresql_charms_single_kernel-16.3.0 → postgresql_charms_single_kernel-16.3.2}/single_kernel_postgresql/charms/vm_charm.py +0 -0
  31. {postgresql_charms_single_kernel-16.3.0 → postgresql_charms_single_kernel-16.3.2}/single_kernel_postgresql/compat/__init__.py +0 -0
  32. {postgresql_charms_single_kernel-16.3.0 → postgresql_charms_single_kernel-16.3.2}/single_kernel_postgresql/compat/postgresql.py +0 -0
  33. {postgresql_charms_single_kernel-16.3.0 → postgresql_charms_single_kernel-16.3.2}/single_kernel_postgresql/compat/pyproject.toml +0 -0
  34. {postgresql_charms_single_kernel-16.3.0 → postgresql_charms_single_kernel-16.3.2}/single_kernel_postgresql/config/__init__.py +0 -0
  35. {postgresql_charms_single_kernel-16.3.0 → postgresql_charms_single_kernel-16.3.2}/single_kernel_postgresql/config/enums.py +0 -0
  36. {postgresql_charms_single_kernel-16.3.0 → postgresql_charms_single_kernel-16.3.2}/single_kernel_postgresql/config/locales.py +0 -0
  37. {postgresql_charms_single_kernel-16.3.0 → postgresql_charms_single_kernel-16.3.2}/single_kernel_postgresql/config/pyproject.toml +0 -0
  38. {postgresql_charms_single_kernel-16.3.0 → postgresql_charms_single_kernel-16.3.2}/single_kernel_postgresql/config/statuses.py +0 -0
  39. {postgresql_charms_single_kernel-16.3.0 → postgresql_charms_single_kernel-16.3.2}/single_kernel_postgresql/core/__init__.py +0 -0
  40. {postgresql_charms_single_kernel-16.3.0 → postgresql_charms_single_kernel-16.3.2}/single_kernel_postgresql/core/config.py +0 -0
  41. {postgresql_charms_single_kernel-16.3.0 → postgresql_charms_single_kernel-16.3.2}/single_kernel_postgresql/core/relation_state.py +0 -0
  42. {postgresql_charms_single_kernel-16.3.0 → postgresql_charms_single_kernel-16.3.2}/single_kernel_postgresql/events/__init__.py +0 -0
  43. {postgresql_charms_single_kernel-16.3.0 → postgresql_charms_single_kernel-16.3.2}/single_kernel_postgresql/events/tls_transfer.py +0 -0
  44. {postgresql_charms_single_kernel-16.3.0 → postgresql_charms_single_kernel-16.3.2}/single_kernel_postgresql/lib/charms/data_platform_libs/v0/data_interfaces.py +0 -0
  45. {postgresql_charms_single_kernel-16.3.0 → postgresql_charms_single_kernel-16.3.2}/single_kernel_postgresql/lib/charms/data_platform_libs/v1/data_models.py +0 -0
  46. {postgresql_charms_single_kernel-16.3.0 → postgresql_charms_single_kernel-16.3.2}/single_kernel_postgresql/managers/__init__.py +0 -0
  47. {postgresql_charms_single_kernel-16.3.0 → postgresql_charms_single_kernel-16.3.2}/single_kernel_postgresql/utils/__init__.py +0 -0
  48. {postgresql_charms_single_kernel-16.3.0 → postgresql_charms_single_kernel-16.3.2}/single_kernel_postgresql/utils/arch.py +0 -0
  49. {postgresql_charms_single_kernel-16.3.0 → postgresql_charms_single_kernel-16.3.2}/single_kernel_postgresql/utils/filesystem.py +0 -0
  50. {postgresql_charms_single_kernel-16.3.0 → postgresql_charms_single_kernel-16.3.2}/single_kernel_postgresql/utils/postgresql.py +0 -0
  51. {postgresql_charms_single_kernel-16.3.0 → postgresql_charms_single_kernel-16.3.2}/single_kernel_postgresql/utils/secret.py +0 -0
  52. {postgresql_charms_single_kernel-16.3.0 → postgresql_charms_single_kernel-16.3.2}/single_kernel_postgresql/utils/status.py +0 -0
  53. {postgresql_charms_single_kernel-16.3.0 → postgresql_charms_single_kernel-16.3.2}/single_kernel_postgresql/workload/__init__.py +0 -0
  54. {postgresql_charms_single_kernel-16.3.0 → postgresql_charms_single_kernel-16.3.2}/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.0
3
+ Version: 16.3.2
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>
@@ -211,11 +211,10 @@ Classifier: Intended Audience :: Developers
211
211
  Classifier: Operating System :: POSIX :: Linux
212
212
  Requires-Dist: ops>=2.0.0
213
213
  Requires-Dist: tenacity>=9.0.0
214
- Requires-Dist: psycopg2>=2.9.10 ; extra == 'postgresql'
214
+ Requires-Dist: psycopg2>=2.9.10 ; extra == 'db-driver'
215
215
  Requires-Dist: httpx ; python_full_version >= '3.12' and extra == 'postgresql'
216
216
  Requires-Dist: data-platform-helpers>=0.1.7 ; python_full_version >= '3.12' and extra == 'postgresql'
217
217
  Requires-Dist: charmlibs-pathops>=1.0.1 ; python_full_version >= '3.12' and extra == 'postgresql'
218
- Requires-Dist: charmlibs-snap>=1.0.1 ; python_full_version >= '3.12' and extra == 'postgresql'
219
218
  Requires-Dist: charmlibs-interfaces-tls-certificates>=1.8.3 ; python_full_version >= '3.12' and extra == 'postgresql'
220
219
  Requires-Dist: charm-refresh ; python_full_version >= '3.12' and extra == 'postgresql'
221
220
  Requires-Dist: requests ; python_full_version >= '3.12' and extra == 'postgresql'
@@ -223,8 +222,14 @@ Requires-Dist: tomli ; python_full_version >= '3.12' and extra == 'postgresql'
223
222
  Requires-Dist: pydantic>=2.0 ; python_full_version >= '3.12' and extra == 'postgresql'
224
223
  Requires-Dist: jinja2>=3.1.6 ; python_full_version >= '3.12' and extra == 'postgresql'
225
224
  Requires-Dist: charmlibs-rollingops>=1.1.1 ; python_full_version >= '3.12' and extra == 'postgresql'
225
+ Requires-Dist: pysyncobj>=0.3.15 ; python_full_version >= '3.12' and extra == 'vm'
226
+ Requires-Dist: psutil>=7.2.2 ; python_full_version >= '3.12' and extra == 'vm'
227
+ Requires-Dist: charmlibs-snap>=1.0.1 ; python_full_version >= '3.12' and extra == 'vm'
228
+ Requires-Dist: charmlibs-systemd>=1.0.0.post0 ; python_full_version >= '3.12' and extra == 'vm'
226
229
  Requires-Python: >=3.8, <4.0
230
+ Provides-Extra: db-driver
227
231
  Provides-Extra: postgresql
232
+ Provides-Extra: vm
228
233
  Description-Content-Type: text/markdown
229
234
 
230
235
  # postgresql-single-kernel-library
@@ -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.0"
7
+ version = "16.3.2"
8
8
  readme = "README.md"
9
9
  license = {file = "LICENSE"}
10
10
  authors = [
@@ -23,11 +23,9 @@ dependencies = [
23
23
 
24
24
  [project.optional-dependencies]
25
25
  postgresql = [
26
- "psycopg2>=2.9.10",
27
26
  "httpx; python_version >= '3.12'",
28
27
  "data-platform-helpers>=0.1.7; python_version >= '3.12'",
29
28
  "charmlibs-pathops>=1.0.1; python_version >= '3.12'",
30
- "charmlibs-snap>=1.0.1; python_version >= '3.12'",
31
29
  "charmlibs-interfaces-tls-certificates>=1.8.3; python_version >= '3.12'",
32
30
  "charm-refresh; python_version >= '3.12'",
33
31
  "requests; python_version >= '3.12'",
@@ -35,7 +33,16 @@ postgresql = [
35
33
  "pydantic>=2.0; python_version >= '3.12'",
36
34
  "jinja2 >=3.1.6; python_version >= '3.12'",
37
35
  "charmlibs-rollingops>=1.1.1; python_version >= '3.12'",
38
- ]
36
+ ]
37
+ vm = [
38
+ "pysyncobj>=0.3.15; python_version >= '3.12'",
39
+ "psutil>=7.2.2; python_version >= '3.12'",
40
+ "charmlibs-snap>=1.0.1; python_version >= '3.12'",
41
+ "charmlibs-systemd>=1.0.0.post0; python_version >= '3.12'"
42
+ ]
43
+ db-driver = [
44
+ "psycopg2>=2.9.10",
45
+ ]
39
46
 
40
47
  [build-system]
41
48
  requires = ["uv_build>=0.11.0,<0.12.0"]
@@ -47,15 +54,15 @@ module-root = ""
47
54
 
48
55
  [dependency-groups]
49
56
  format = [
50
- "ruff==0.15.17"
57
+ "ruff==0.15.20"
51
58
  ]
52
59
  lint = [
53
60
  "codespell==2.4.2",
54
- "ty==0.0.49"
61
+ "ty==0.0.55"
55
62
  ]
56
63
  unit = [
57
- "coverage[toml]==7.14.1",
58
- "pytest==9.1.0"
64
+ "coverage[toml]==7.14.3",
65
+ "pytest==9.1.1"
59
66
  ]
60
67
 
61
68
 
@@ -29,18 +29,10 @@ class AbstractPostgreSQLCharm(CharmBase, ABC):
29
29
  self.state = CharmState(charm=self, substrate=self.substrate)
30
30
 
31
31
  # Managers
32
- self.cluster_manager = ClusterManager(
33
- state=self.state, workload=self.workload, client=self.postgresql
34
- )
35
- self.tls_manager = TLSManager(
36
- state=self.state, workload=self.workload, client=self.postgresql
37
- )
38
- self.config_manager = ConfigManager(
39
- state=self.state, workload=self.workload, client=self.postgresql
40
- )
41
- self.patroni_manager = PatroniManager(
42
- state=self.state, workload=self.workload, client=self.postgresql
43
- )
32
+ self.tls_manager = TLSManager(state=self.state, workload=self.workload)
33
+ self.patroni_manager = PatroniManager(state=self.state, workload=self.workload)
34
+ self.cluster_manager = ClusterManager(state=self.state, workload=self.workload)
35
+ self.config_manager = ConfigManager(state=self.state, workload=self.workload)
44
36
 
45
37
  # Events Handler
46
38
  self.postgresql_events_handler = PostgreSQLEventsHandler(
@@ -25,7 +25,7 @@ class PostgreSQLK8sCharm(AbstractPostgreSQLCharm):
25
25
  assert isinstance(self.workload, K8sWorkload), ( # noqa: S101
26
26
  "Workload must be an instance of K8sWorkload"
27
27
  )
28
- self.k8s_manager = K8sManager(self.state, self.workload, self.postgresql)
28
+ self.k8s_manager = K8sManager(self.state, self.workload)
29
29
 
30
30
  @property
31
31
  def postgresql(self) -> PostgreSQL:
@@ -0,0 +1,75 @@
1
+ #!/usr/bin/env python3
2
+ # Copyright 2025 Canonical Ltd.
3
+ # See LICENSE file for licensing details.
4
+
5
+ """Charm-specific exceptions."""
6
+
7
+ from single_kernel_postgresql.compat.postgresql import PostgreSQLBaseError
8
+
9
+
10
+ class PostgreSQLFileOperationError(PostgreSQLBaseError):
11
+ """Exception thrown when file operations related to PostgreSQL fail."""
12
+
13
+
14
+ class StorageUnavailableError(Exception):
15
+ """Cannot find storage mountpoint."""
16
+
17
+
18
+ class SettingSystemPasswordError(PostgreSQLBaseError):
19
+ """Exception thrown when setting the system password fails."""
20
+
21
+
22
+ class PostgreSQLCannotConnectError(Exception):
23
+ """Cannot run smoke check on connected Database."""
24
+
25
+
26
+ class TlsError(Exception):
27
+ """TLS implementation internal exception."""
28
+
29
+
30
+ class RaftPostgresqlNotUpError(Exception):
31
+ """Postgresql not yet started."""
32
+
33
+
34
+ class RaftPostgresqlStillUpError(Exception):
35
+ """Postgresql not yet down."""
36
+
37
+
38
+ class RaftNotPromotedError(Exception):
39
+ """Leader not yet set when reinitialising raft."""
40
+
41
+
42
+ class ClusterNotPromotedError(Exception):
43
+ """Raised when a cluster is not promoted."""
44
+
45
+
46
+ class NotReadyError(Exception):
47
+ """Raised when not all cluster members healthy or finished initial sync."""
48
+
49
+
50
+ class EndpointNotReadyError(Exception):
51
+ """Raised when an endpoint is not ready."""
52
+
53
+
54
+ class StandbyClusterAlreadyPromotedError(Exception):
55
+ """Raised when a standby cluster is already promoted."""
56
+
57
+
58
+ class RemoveRaftMemberFailedError(Exception):
59
+ """Raised when a remove raft member failed for some reason."""
60
+
61
+
62
+ class AddRaftMemberFailedError(Exception):
63
+ """Raised when adding raft member failed for some reason."""
64
+
65
+
66
+ class SwitchoverFailedError(Exception):
67
+ """Raised when a switchover failed for some reason."""
68
+
69
+
70
+ class SwitchoverNotSyncError(SwitchoverFailedError):
71
+ """Raised when a switchover failed because node is not sync."""
72
+
73
+
74
+ class UpdateSyncNodeCountError(Exception):
75
+ """Raised when updating synchronous_node_count failed for some reason."""
@@ -31,6 +31,9 @@ SNAP_COMMON = "common"
31
31
  SNAP = "/snap/charmed-postgresql/current"
32
32
  VM_LOGS_PATH = "var/log/postgresql"
33
33
  VM_DATA_PATH = "var/lib/postgresql"
34
+ VM_ARCHIVE_PATH = "data/archive"
35
+ VM_DATA_LOGS_PATH = "data/logs"
36
+ VM_TEMP_PATH = "data/temp"
34
37
 
35
38
  ## K8s Paths
36
39
  K8S_DATA_PATH = "var/lib/pg/data"
@@ -42,7 +45,9 @@ K8S_DATA_PATH = "var/lib/pg/data"
42
45
  # e.g. snap_current / "/etc/postgresql" will result in "/etc/postgresql" instead of "/var/snap/postgresql/current/etc/postgresql"
43
46
  POSTGRESQL_CONF_PATH = "etc/postgresql"
44
47
  POSTGRESQL_CONF_FILE = "postgresql.conf"
45
-
48
+ ## Pgbackreest Paths
49
+ PGBACKREST_CONF_PATH = "etc/pgbackrest"
50
+ PGBACKREST_CONF_FILE = "pgbackrest.conf"
46
51
 
47
52
  ## TLS Paths
48
53
  TLS_CA_BUNDLE_FILE = "peer_ca_bundle.pem"
@@ -161,3 +166,11 @@ WATCHER_RELATION = "watcher"
161
166
  WATCHER_USER = "watcher"
162
167
  WATCHER_PASSWORD_KEY = "watcher-password" # noqa: S105
163
168
  WATCHER_SECRET_LABEL = "watcher-secret" # noqa: S105
169
+
170
+ # Raft
171
+ RAFT_PORT = 2222
172
+ RAFT_PARTNER_PREFIX = "partner_node_status_server_"
173
+
174
+ # VM services
175
+ VM_PATRONI_SERVICE_NAME = "snap.charmed-postgresql.patroni.service"
176
+ VM_PATRONI_SERVICE_DEFAULT_PATH = f"/etc/systemd/system/{VM_PATRONI_SERVICE_NAME}"
@@ -6,6 +6,8 @@
6
6
  """State objects for database-peers relation."""
7
7
 
8
8
  import json
9
+ from collections.abc import MutableMapping
10
+ from functools import cached_property
9
11
 
10
12
  from ops import Application, BlockedStatus, Relation, Unit
11
13
 
@@ -177,6 +179,34 @@ class PostgreSQLPeer(RelationState):
177
179
  peer_addrs.add(addr)
178
180
  return peer_addrs
179
181
 
182
+ @property
183
+ def is_unit_departing(self) -> bool:
184
+ """Returns whether the unit is departing."""
185
+ if not self.relation:
186
+ return False
187
+ return "departing" in self.relation.data[self.unit]
188
+
189
+ @property
190
+ def is_unit_stopped(self) -> bool:
191
+ """Returns whether the unit is stopped."""
192
+ if not self.relation:
193
+ return False
194
+ return "stopped" in self.relation.data[self.unit]
195
+
196
+ @property
197
+ def is_connectivity_enabled(self) -> bool:
198
+ """Return whether this unit can be connected externally."""
199
+ if not self.relation:
200
+ return True
201
+ return self.relation.data[self.unit].get("connectivity", "on") == "on"
202
+
203
+ @cached_property
204
+ def data(self) -> MutableMapping[str, str]:
205
+ """Escape hatch method to access the peer data directly."""
206
+ if not self.relation:
207
+ return {}
208
+ return self.relation.data[self.unit]
209
+
180
210
 
181
211
  class PostgreSQLApplication(RelationState):
182
212
  """An PostgreSQL Application is the peer application state.
@@ -290,7 +320,7 @@ class PostgreSQLApplication(RelationState):
290
320
  return f"patroni-{self.app.name}"
291
321
  return self.app.name
292
322
 
293
- @property
323
+ @cached_property
294
324
  def planned_units(self) -> int:
295
325
  """Get the number of planned units for the application."""
296
326
  return self.app.planned_units()
@@ -309,6 +339,39 @@ class PostgreSQLApplication(RelationState):
309
339
  return set()
310
340
  return set(json.loads(self.relation.data[self.app].get("endpoints", "[]")))
311
341
 
342
+ @property
343
+ def is_cluster_initialised(self) -> bool:
344
+ """Returns whether the cluster is already initialised."""
345
+ if not self.relation:
346
+ return False
347
+ return "cluster_initialised" in self.relation.data[self.app]
348
+
349
+ @property
350
+ def is_cluster_restoring_backup(self) -> bool:
351
+ """Returns whether the cluster is restoring a backup."""
352
+ if not self.relation:
353
+ return False
354
+ return "restoring-backup" in self.relation.data[self.app]
355
+
356
+ @property
357
+ def is_cluster_restoring_to_time(self) -> bool:
358
+ """Returns whether the cluster is restoring a backup to a specific time."""
359
+ if not self.relation:
360
+ return False
361
+ return "restore-to-time" in self.relation.data[self.app]
362
+
363
+ @property
364
+ def is_ldap_charm_related(self) -> bool:
365
+ """Return whether this unit has an LDAP charm related."""
366
+ if not self.relation:
367
+ return False
368
+ return self.relation.data[self.app].get("ldap_enabled", "False") == "True"
369
+
370
+ @property
371
+ def is_ldap_enabled(self) -> bool:
372
+ """Return whether this unit has LDAP enabled."""
373
+ return self.is_ldap_charm_related and self.is_cluster_initialised
374
+
312
375
  def get_secret(self, key: str) -> str | None:
313
376
  """Get the secret value for 'key' from the peer relation data."""
314
377
  if not self.relation:
@@ -326,3 +389,10 @@ class PostgreSQLApplication(RelationState):
326
389
  if not self.relation:
327
390
  return
328
391
  self.data_interface.delete_relation_data(self.relation.id, [key])
392
+
393
+ @cached_property
394
+ def data(self) -> MutableMapping[str, str]:
395
+ """Escape hatch method to access the peer data directly."""
396
+ if not self.relation:
397
+ return {}
398
+ return self.relation.data[self.app]
@@ -6,17 +6,30 @@
6
6
 
7
7
  import re
8
8
  import socket
9
+ from contextlib import suppress
9
10
  from functools import cached_property
10
11
  from typing import TYPE_CHECKING, Any, get_args
11
12
 
12
13
  from data_platform_helpers.advanced_statuses import StatusesState, StatusObject
13
14
  from data_platform_helpers.advanced_statuses.types import Scope as AdvancedStatusesScope
14
- from ops import ConfigData, JujuVersion, ModelError, Object, Relation, SecretNotFoundError, Unit
15
+ from ops import (
16
+ ConfigData,
17
+ JujuVersion,
18
+ ModelError,
19
+ Object,
20
+ Relation,
21
+ RelationNotFoundError,
22
+ SecretNotFoundError,
23
+ Unit,
24
+ )
15
25
 
16
26
  from single_kernel_postgresql.config.enums import Substrates
17
27
  from single_kernel_postgresql.config.literals import (
18
28
  APP_SCOPE,
29
+ DATABASE,
19
30
  PEER_RELATION,
31
+ REPLICATION_CONSUMER_RELATION,
32
+ REPLICATION_OFFER_RELATION,
20
33
  SCOPES,
21
34
  STATUS_PEERS_RELATION,
22
35
  )
@@ -153,6 +166,27 @@ class CharmState(Object):
153
166
  if binding := self.model.get_binding(PEER_RELATION):
154
167
  return str(binding.network.bind_address)
155
168
 
169
+ @property
170
+ def database_ip(self) -> str | None:
171
+ """Database endpoint address."""
172
+ with suppress(RelationNotFoundError):
173
+ if binding := self.model.get_binding(DATABASE):
174
+ return str(binding.network.bind_address)
175
+
176
+ @property
177
+ def replication_offer_ip(self) -> str | None:
178
+ """Async replication offer endpoint address."""
179
+ with suppress(RelationNotFoundError):
180
+ if binding := self.model.get_binding(REPLICATION_OFFER_RELATION):
181
+ return str(binding.network.bind_address)
182
+
183
+ @property
184
+ def replication_consumer_ip(self) -> str | None:
185
+ """Async replication consumer endpoint address."""
186
+ with suppress(RelationNotFoundError):
187
+ if binding := self.model.get_binding(REPLICATION_CONSUMER_RELATION):
188
+ return str(binding.network.bind_address)
189
+
156
190
  @property
157
191
  def fqdn(self) -> str | None:
158
192
  """Current unit fqdn."""
@@ -165,7 +199,7 @@ class CharmState(Object):
165
199
  def endpoint(self) -> str | None:
166
200
  """Current unit endpoint."""
167
201
  if self.substrate == Substrates.K8S:
168
- return self.fqdn
202
+ return f"{self.peer.unit.name.replace('/', '-')}.{self.application.app.name}-endpoints"
169
203
  else:
170
204
  return self.unit_ip
171
205
 
@@ -173,9 +207,11 @@ class CharmState(Object):
173
207
  def endpoints(self) -> set[str]:
174
208
  """Returns the list of endpoints of the current members of the cluster."""
175
209
  if self.peer_relation:
176
- return self.application.endpoints
177
- else:
178
- return {self.endpoint} if self.endpoint else set()
210
+ if self.substrate == Substrates.K8S:
211
+ return self.application.endpoints
212
+ else:
213
+ return self.peer_members_ips
214
+ return {self.endpoint} if self.endpoint else set()
179
215
 
180
216
  @property
181
217
  def model_name(self) -> str:
@@ -185,7 +221,7 @@ class CharmState(Object):
185
221
  @cached_property
186
222
  def patroni_url(self) -> str:
187
223
  """Patroni REST API URL."""
188
- return f"https://{self.unit_ip}:8007"
224
+ return f"https://{self.endpoint}:8008"
189
225
 
190
226
  @property
191
227
  def peer_members_ips(self) -> set[str]:
@@ -227,7 +263,12 @@ class CharmState(Object):
227
263
  ips = []
228
264
  if self.unit_ip:
229
265
  ips.append(self.unit_ip)
230
- # TODO: Add other ips
266
+ if self.database_ip and self.database_ip not in ips:
267
+ ips.append(self.database_ip)
268
+ if self.replication_offer_ip and self.replication_offer_ip not in ips:
269
+ ips.append(self.replication_offer_ip)
270
+ if self.replication_consumer_ip and self.replication_consumer_ip not in ips:
271
+ ips.append(self.replication_consumer_ip)
231
272
  return ips
232
273
 
233
274
  # -- Secrets
@@ -403,3 +444,38 @@ class CharmState(Object):
403
444
  if re.fullmatch(regex_pattern, present_status.message) is not None:
404
445
  return present_status
405
446
  return None
447
+
448
+ @cached_property
449
+ def synchronous_node_count(self) -> int:
450
+ """Number of expected sync standbys."""
451
+ planned_units = self.application.planned_units
452
+ if self.config.synchronous_node_count == "all":
453
+ return planned_units - 1
454
+ elif self.config.synchronous_node_count == "majority":
455
+ return planned_units // 2
456
+ # -1 for leader
457
+ return (
458
+ self.config.synchronous_node_count
459
+ if self.config.synchronous_node_count < planned_units - 1
460
+ else planned_units - 1
461
+ )
462
+
463
+ @cached_property
464
+ def synchronous_configuration(self) -> dict[str, Any]:
465
+ """Synchronous mode configuration."""
466
+ # Try to update synchronous_node_count.
467
+ return {
468
+ "synchronous_node_count": self.synchronous_node_count,
469
+ "synchronous_mode_strict": len(self.application.members_ips) > 1
470
+ and self.config.synchronous_mode_strict
471
+ and self.synchronous_node_count > 0,
472
+ }
473
+
474
+ def _build_service_name(self, service: str) -> str:
475
+ """Build a full k8s service name based on the service name."""
476
+ return f"{self.application.app.name}-{service}.{self.model_name}.svc.cluster.local"
477
+
478
+ @property
479
+ def primary_endpoint(self) -> str:
480
+ """Returns the endpoint of the primary instance's service."""
481
+ return self._build_service_name("primary")
@@ -177,8 +177,8 @@ class PostgreSQLEventsHandler(Object):
177
177
 
178
178
  self.tls_manager.configure_internal_peer_ca()
179
179
 
180
- if self.charm.substrate == Substrates.VM:
181
- self.config_manager.update_config()
180
+ # if self.charm.substrate == Substrates.VM:
181
+ # self.config_manager.update_config()
182
182
 
183
183
  # TODO: Add next steps of leader elected
184
184
 
@@ -11,7 +11,6 @@ from data_platform_helpers.advanced_statuses.types import Scope as AdvancedStatu
11
11
 
12
12
  from single_kernel_postgresql.config.statuses import GeneralStatuses
13
13
  from single_kernel_postgresql.core.state import CharmState
14
- from single_kernel_postgresql.utils.postgresql import PostgreSQL as PostgreSQLClient
15
14
  from single_kernel_postgresql.workload.base import BaseWorkload
16
15
 
17
16
  logger = logging.getLogger(__name__)
@@ -25,13 +24,10 @@ class BaseManager(ManagerStatusProtocol):
25
24
 
26
25
  state: CharmState
27
26
 
28
- def __init__(
29
- self, state: CharmState, workload: BaseWorkload, name: str, client: PostgreSQLClient
30
- ):
27
+ def __init__(self, state: CharmState, workload: BaseWorkload, name: str):
31
28
  self.state = state
32
29
  self.workload = workload
33
30
  self.name = name
34
- self.postgresql_client = client
35
31
 
36
32
  def get_statuses(
37
33
  self, scope: AdvancedStatusesScope, recompute: bool = False
@@ -34,7 +34,6 @@ from single_kernel_postgresql.managers.base import BaseManager
34
34
  from single_kernel_postgresql.utils import new_password
35
35
  from single_kernel_postgresql.utils.postgresql import PostgreSQL as PostgreSQLClient
36
36
  from single_kernel_postgresql.workload.base import BaseWorkload
37
- from single_kernel_postgresql.workload.vm import VMWorkload
38
37
 
39
38
  logger = logging.getLogger(__name__)
40
39
 
@@ -45,15 +44,15 @@ class ClusterManager(BaseManager):
45
44
  This manager is responsible for handling cluster-wide operations.
46
45
  """
47
46
 
48
- def __init__(self, state: CharmState, workload: BaseWorkload, client: PostgreSQLClient):
49
- super().__init__(state, workload, "cluster_manager", client)
47
+ def __init__(self, state: CharmState, workload: BaseWorkload):
48
+ super().__init__(state, workload, "cluster_manager")
50
49
 
51
50
  def install_workload(self) -> None:
52
51
  """Install the workload."""
53
- if self.state.substrate == Substrates.VM and isinstance(self.workload, VMWorkload):
54
- self.workload.install_snap_package(revision=None)
55
- self.workload.create_snap_alias("patronictl")
56
- self.workload.create_snap_alias("psql")
52
+ if self.state.substrate == Substrates.VM:
53
+ self.workload.install_snap_package(revision=None) # type: ignore
54
+ self.workload.create_snap_alias("patronictl") # type:ignore
55
+ self.workload.create_snap_alias("psql") # type: ignore
57
56
  else:
58
57
  logger.debug(
59
58
  "No workload installation steps defined for substrate %s", self.state.substrate
@@ -108,14 +107,16 @@ class ClusterManager(BaseManager):
108
107
  except ModelError:
109
108
  logger.exception("failed to open port")
110
109
 
111
- def can_connect_to_postgresql(self, retry: bool = True) -> bool:
110
+ def can_connect_to_postgresql(
111
+ self, postgresql_client: PostgreSQLClient, retry: bool = True
112
+ ) -> bool:
112
113
  """Whether the local PostgreSQL instance is reachable and responding."""
113
- if not self.postgresql_client.password or not self.postgresql_client.current_host:
114
+ if not postgresql_client.password or not postgresql_client.current_host:
114
115
  return False
115
116
 
116
117
  def _check_connection():
117
118
  try:
118
- if not self.postgresql_client.get_postgresql_timezones():
119
+ if not postgresql_client.get_postgresql_timezones():
119
120
  logger.debug("Cannot connect to database (CannotConnectError)")
120
121
  raise PostgreSQLCannotConnectError
121
122
  except Exception as e: