postgresql-charms-single-kernel 16.3.4__tar.gz → 16.3.6__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.

Potentially problematic release.


This version of postgresql-charms-single-kernel might be problematic. Click here for more details.

Files changed (59) hide show
  1. {postgresql_charms_single_kernel-16.3.4 → postgresql_charms_single_kernel-16.3.6}/PKG-INFO +4 -1
  2. postgresql_charms_single_kernel-16.3.6/pyproject.toml +166 -0
  3. postgresql_charms_single_kernel-16.3.4/pyproject.toml → postgresql_charms_single_kernel-16.3.6/pyproject.toml.orig +10 -6
  4. postgresql_charms_single_kernel-16.3.6/single_kernel_postgresql/charms/abstract_charm.py +152 -0
  5. {postgresql_charms_single_kernel-16.3.4 → postgresql_charms_single_kernel-16.3.6}/single_kernel_postgresql/charms/k8s_charm.py +30 -1
  6. {postgresql_charms_single_kernel-16.3.4 → postgresql_charms_single_kernel-16.3.6}/single_kernel_postgresql/charms/vm_charm.py +31 -3
  7. {postgresql_charms_single_kernel-16.3.4 → postgresql_charms_single_kernel-16.3.6}/single_kernel_postgresql/config/exceptions.py +4 -0
  8. {postgresql_charms_single_kernel-16.3.4 → postgresql_charms_single_kernel-16.3.6}/single_kernel_postgresql/core/peer_relation.py +56 -0
  9. {postgresql_charms_single_kernel-16.3.4 → postgresql_charms_single_kernel-16.3.6}/single_kernel_postgresql/core/state.py +12 -0
  10. postgresql_charms_single_kernel-16.3.6/single_kernel_postgresql/events/database.py +243 -0
  11. {postgresql_charms_single_kernel-16.3.4 → postgresql_charms_single_kernel-16.3.6}/single_kernel_postgresql/events/tls.py +19 -1
  12. postgresql_charms_single_kernel-16.3.6/single_kernel_postgresql/managers/config.py +751 -0
  13. postgresql_charms_single_kernel-16.3.6/single_kernel_postgresql/managers/database.py +667 -0
  14. {postgresql_charms_single_kernel-16.3.4 → postgresql_charms_single_kernel-16.3.6}/single_kernel_postgresql/managers/k8s.py +85 -2
  15. {postgresql_charms_single_kernel-16.3.4 → postgresql_charms_single_kernel-16.3.6}/single_kernel_postgresql/managers/patroni.py +1 -1
  16. postgresql_charms_single_kernel-16.3.6/single_kernel_postgresql/templates/__init__.py +4 -0
  17. postgresql_charms_single_kernel-16.3.6/single_kernel_postgresql/templates/patroni.yml.j2 +296 -0
  18. {postgresql_charms_single_kernel-16.3.4 → postgresql_charms_single_kernel-16.3.6}/single_kernel_postgresql/workload/base.py +9 -5
  19. {postgresql_charms_single_kernel-16.3.4 → postgresql_charms_single_kernel-16.3.6}/single_kernel_postgresql/workload/k8s.py +0 -4
  20. {postgresql_charms_single_kernel-16.3.4 → postgresql_charms_single_kernel-16.3.6}/single_kernel_postgresql/workload/paths/base.py +5 -0
  21. {postgresql_charms_single_kernel-16.3.4 → postgresql_charms_single_kernel-16.3.6}/single_kernel_postgresql/workload/paths/k8s.py +2 -2
  22. {postgresql_charms_single_kernel-16.3.4 → postgresql_charms_single_kernel-16.3.6}/single_kernel_postgresql/workload/paths/vm.py +1 -1
  23. {postgresql_charms_single_kernel-16.3.4 → postgresql_charms_single_kernel-16.3.6}/single_kernel_postgresql/workload/vm.py +10 -0
  24. postgresql_charms_single_kernel-16.3.4/single_kernel_postgresql/charms/abstract_charm.py +0 -86
  25. postgresql_charms_single_kernel-16.3.4/single_kernel_postgresql/managers/config.py +0 -295
  26. {postgresql_charms_single_kernel-16.3.4 → postgresql_charms_single_kernel-16.3.6}/LICENSE +0 -0
  27. {postgresql_charms_single_kernel-16.3.4 → postgresql_charms_single_kernel-16.3.6}/README.md +0 -0
  28. {postgresql_charms_single_kernel-16.3.4 → postgresql_charms_single_kernel-16.3.6}/single_kernel_postgresql/__init__.py +0 -0
  29. {postgresql_charms_single_kernel-16.3.4 → postgresql_charms_single_kernel-16.3.6}/single_kernel_postgresql/charmcraft.yaml +0 -0
  30. {postgresql_charms_single_kernel-16.3.4 → postgresql_charms_single_kernel-16.3.6}/single_kernel_postgresql/charms/__init__.py +0 -0
  31. {postgresql_charms_single_kernel-16.3.4 → postgresql_charms_single_kernel-16.3.6}/single_kernel_postgresql/compat/__init__.py +0 -0
  32. {postgresql_charms_single_kernel-16.3.4 → postgresql_charms_single_kernel-16.3.6}/single_kernel_postgresql/compat/postgresql.py +0 -0
  33. {postgresql_charms_single_kernel-16.3.4 → postgresql_charms_single_kernel-16.3.6}/single_kernel_postgresql/compat/pyproject.toml +0 -0
  34. {postgresql_charms_single_kernel-16.3.4 → postgresql_charms_single_kernel-16.3.6}/single_kernel_postgresql/config/__init__.py +0 -0
  35. {postgresql_charms_single_kernel-16.3.4 → postgresql_charms_single_kernel-16.3.6}/single_kernel_postgresql/config/enums.py +0 -0
  36. {postgresql_charms_single_kernel-16.3.4 → postgresql_charms_single_kernel-16.3.6}/single_kernel_postgresql/config/literals.py +0 -0
  37. {postgresql_charms_single_kernel-16.3.4 → postgresql_charms_single_kernel-16.3.6}/single_kernel_postgresql/config/locales.py +0 -0
  38. {postgresql_charms_single_kernel-16.3.4 → postgresql_charms_single_kernel-16.3.6}/single_kernel_postgresql/config/pyproject.toml +0 -0
  39. {postgresql_charms_single_kernel-16.3.4 → postgresql_charms_single_kernel-16.3.6}/single_kernel_postgresql/config/statuses.py +0 -0
  40. {postgresql_charms_single_kernel-16.3.4 → postgresql_charms_single_kernel-16.3.6}/single_kernel_postgresql/core/__init__.py +0 -0
  41. {postgresql_charms_single_kernel-16.3.4 → postgresql_charms_single_kernel-16.3.6}/single_kernel_postgresql/core/config.py +0 -0
  42. {postgresql_charms_single_kernel-16.3.4 → postgresql_charms_single_kernel-16.3.6}/single_kernel_postgresql/core/relation_state.py +0 -0
  43. {postgresql_charms_single_kernel-16.3.4 → postgresql_charms_single_kernel-16.3.6}/single_kernel_postgresql/events/__init__.py +0 -0
  44. {postgresql_charms_single_kernel-16.3.4 → postgresql_charms_single_kernel-16.3.6}/single_kernel_postgresql/events/postgresql.py +0 -0
  45. {postgresql_charms_single_kernel-16.3.4 → postgresql_charms_single_kernel-16.3.6}/single_kernel_postgresql/events/tls_transfer.py +0 -0
  46. {postgresql_charms_single_kernel-16.3.4 → postgresql_charms_single_kernel-16.3.6}/single_kernel_postgresql/lib/charms/data_platform_libs/v0/data_interfaces.py +0 -0
  47. {postgresql_charms_single_kernel-16.3.4 → postgresql_charms_single_kernel-16.3.6}/single_kernel_postgresql/lib/charms/data_platform_libs/v1/data_models.py +0 -0
  48. {postgresql_charms_single_kernel-16.3.4 → postgresql_charms_single_kernel-16.3.6}/single_kernel_postgresql/managers/__init__.py +0 -0
  49. {postgresql_charms_single_kernel-16.3.4 → postgresql_charms_single_kernel-16.3.6}/single_kernel_postgresql/managers/base.py +0 -0
  50. {postgresql_charms_single_kernel-16.3.4 → postgresql_charms_single_kernel-16.3.6}/single_kernel_postgresql/managers/cluster.py +0 -0
  51. {postgresql_charms_single_kernel-16.3.4 → postgresql_charms_single_kernel-16.3.6}/single_kernel_postgresql/managers/tls.py +0 -0
  52. {postgresql_charms_single_kernel-16.3.4 → postgresql_charms_single_kernel-16.3.6}/single_kernel_postgresql/utils/__init__.py +0 -0
  53. {postgresql_charms_single_kernel-16.3.4 → postgresql_charms_single_kernel-16.3.6}/single_kernel_postgresql/utils/arch.py +0 -0
  54. {postgresql_charms_single_kernel-16.3.4 → postgresql_charms_single_kernel-16.3.6}/single_kernel_postgresql/utils/filesystem.py +0 -0
  55. {postgresql_charms_single_kernel-16.3.4 → postgresql_charms_single_kernel-16.3.6}/single_kernel_postgresql/utils/postgresql.py +0 -0
  56. {postgresql_charms_single_kernel-16.3.4 → postgresql_charms_single_kernel-16.3.6}/single_kernel_postgresql/utils/secret.py +0 -0
  57. {postgresql_charms_single_kernel-16.3.4 → postgresql_charms_single_kernel-16.3.6}/single_kernel_postgresql/utils/status.py +0 -0
  58. {postgresql_charms_single_kernel-16.3.4 → postgresql_charms_single_kernel-16.3.6}/single_kernel_postgresql/workload/__init__.py +0 -0
  59. {postgresql_charms_single_kernel-16.3.4 → postgresql_charms_single_kernel-16.3.6}/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.4
3
+ Version: 16.3.6
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>
@@ -212,6 +212,8 @@ Classifier: Operating System :: POSIX :: Linux
212
212
  Requires-Dist: ops>=2.0.0
213
213
  Requires-Dist: tenacity>=9.0.0
214
214
  Requires-Dist: psycopg2>=2.9.10 ; extra == 'db-driver'
215
+ Requires-Dist: lightkube>=0.21.0 ; python_full_version >= '3.12' and extra == 'k8s'
216
+ Requires-Dist: lightkube-models>=1.28.1.4 ; python_full_version >= '3.12' and extra == 'k8s'
215
217
  Requires-Dist: httpx ; python_full_version >= '3.12' and extra == 'postgresql'
216
218
  Requires-Dist: data-platform-helpers>=0.1.7 ; python_full_version >= '3.12' and extra == 'postgresql'
217
219
  Requires-Dist: charmlibs-pathops>=1.0.1 ; python_full_version >= '3.12' and extra == 'postgresql'
@@ -228,6 +230,7 @@ Requires-Dist: charmlibs-snap>=1.0.1 ; python_full_version >= '3.12' and extra =
228
230
  Requires-Dist: charmlibs-systemd>=1.0.0.post0 ; python_full_version >= '3.12' and extra == 'vm'
229
231
  Requires-Python: >=3.8, <4.0
230
232
  Provides-Extra: db-driver
233
+ Provides-Extra: k8s
231
234
  Provides-Extra: postgresql
232
235
  Provides-Extra: vm
233
236
  Description-Content-Type: text/markdown
@@ -0,0 +1,166 @@
1
+ [project]
2
+ name = "postgresql-charms-single-kernel"
3
+ description = "Shared and reusable code for PostgreSQL-related charms"
4
+ version = "16.3.6"
5
+ readme = "README.md"
6
+ classifiers = [
7
+ "Development Status :: 3 - Alpha",
8
+ "Intended Audience :: Developers",
9
+ "Operating System :: POSIX :: Linux",
10
+ ]
11
+ requires-python = ">=3.8,<4.0"
12
+ dependencies = [
13
+ "ops>=2.0.0",
14
+ "tenacity>=9.0.0",
15
+ ]
16
+
17
+ [project.license]
18
+ file = "LICENSE"
19
+
20
+ [[project.authors]]
21
+ name = "Canonical Data Platform"
22
+ email = "data-platform@lists.launchpad.net"
23
+
24
+ [project.optional-dependencies]
25
+ postgresql = [
26
+ "httpx; python_version >= '3.12'",
27
+ "data-platform-helpers>=0.1.7; python_version >= '3.12'",
28
+ "charmlibs-pathops>=1.0.1; python_version >= '3.12'",
29
+ "charmlibs-interfaces-tls-certificates>=1.8.3; python_version >= '3.12'",
30
+ "charm-refresh; python_version >= '3.12'",
31
+ "requests; python_version >= '3.12'",
32
+ "tomli; python_version >= '3.12'",
33
+ "pydantic>=2.0; python_version >= '3.12'",
34
+ "jinja2 >=3.1.6; python_version >= '3.12'",
35
+ "charmlibs-rollingops>=1.1.1; python_version >= '3.12'",
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
+ k8s = [
44
+ "lightkube>=0.21.0; python_version >= '3.12'",
45
+ "lightkube-models>=1.28.1.4; python_version >= '3.12'",
46
+ ]
47
+ db-driver = ["psycopg2>=2.9.10"]
48
+
49
+ [build-system]
50
+ requires = ["uv_build>=0.12.1,<0.13.0"]
51
+ build-backend = "uv_build"
52
+
53
+ [tool.uv]
54
+ exclude-newer = "1 day"
55
+
56
+ [tool.uv.build-backend]
57
+ module-name = "single_kernel_postgresql"
58
+ module-root = ""
59
+
60
+ [tool.uv.dependency-groups.unit]
61
+ requires-python = ">=3.12"
62
+
63
+ [tool.uv.dependency-groups.lint]
64
+ requires-python = ">=3.12"
65
+
66
+ [tool.uv.dependency-groups.format]
67
+ requires-python = ">=3.12"
68
+
69
+ [tool.coverage.run]
70
+ branch = true
71
+
72
+ [tool.coverage.report]
73
+ show_missing = true
74
+
75
+ [tool.pytest.ini_options]
76
+ minversion = "6.0"
77
+ log_cli_level = "INFO"
78
+
79
+ [tool.ruff]
80
+ preview = true
81
+ target-version = "py312"
82
+ src = [
83
+ "src",
84
+ ".",
85
+ ]
86
+ line-length = 99
87
+
88
+ [tool.ruff.lint]
89
+ explicit-preview-rules = true
90
+ select = [
91
+ "A",
92
+ "E",
93
+ "W",
94
+ "F",
95
+ "C",
96
+ "N",
97
+ "D",
98
+ "I001",
99
+ "B",
100
+ "CPY001",
101
+ "RUF",
102
+ "S",
103
+ "SIM",
104
+ "UP",
105
+ "TCH",
106
+ ]
107
+ extend-ignore = [
108
+ "D203",
109
+ "D204",
110
+ "D213",
111
+ "D215",
112
+ "D400",
113
+ "D404",
114
+ "D406",
115
+ "D407",
116
+ "D408",
117
+ "D409",
118
+ "D413",
119
+ ]
120
+ ignore = [
121
+ "E501",
122
+ "D107",
123
+ ]
124
+
125
+ [tool.ruff.lint.per-file-ignores]
126
+ "tests/*" = [
127
+ "D100",
128
+ "D101",
129
+ "D102",
130
+ "D103",
131
+ "D104",
132
+ "B011",
133
+ "S",
134
+ ]
135
+
136
+ [tool.ruff.lint.flake8-copyright]
137
+ author = "Canonical Ltd."
138
+ notice-rgx = 'Copyright\s\d{4}([-,]\d{4})*\s+'
139
+ min-file-size = 1
140
+
141
+ [tool.ruff.lint.mccabe]
142
+ max-complexity = 10
143
+
144
+ [tool.ruff.lint.pydocstyle]
145
+ convention = "google"
146
+
147
+ [tool.ty.environment]
148
+ python = ".tox/lint/"
149
+ python-version = "3.12"
150
+
151
+ [tool.ty.src]
152
+ exclude = [
153
+ "tests",
154
+ "single_kernel_postgresql/lib",
155
+ ]
156
+
157
+ [dependency-groups]
158
+ format = ["ruff==0.16.3"]
159
+ lint = [
160
+ "codespell==2.4.3",
161
+ "ty==0.0.72",
162
+ ]
163
+ unit = [
164
+ "coverage[toml]==7.15.4",
165
+ "pytest==9.1.1",
166
+ ]
@@ -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.4"
7
+ version = "16.3.6"
8
8
  readme = "README.md"
9
9
  license = {file = "LICENSE"}
10
10
  authors = [
@@ -40,12 +40,16 @@ vm = [
40
40
  "charmlibs-snap>=1.0.1; python_version >= '3.12'",
41
41
  "charmlibs-systemd>=1.0.0.post0; python_version >= '3.12'"
42
42
  ]
43
+ k8s = [
44
+ "lightkube>=0.21.0; python_version >= '3.12'",
45
+ "lightkube-models>=1.28.1.4; python_version >= '3.12'",
46
+ ]
43
47
  db-driver = [
44
48
  "psycopg2>=2.9.10",
45
49
  ]
46
50
 
47
51
  [build-system]
48
- requires = ["uv_build>=0.11.0,<0.12.0"]
52
+ requires = ["uv_build>=0.12.1,<0.13.0"]
49
53
  build-backend = "uv_build"
50
54
 
51
55
  [tool.uv]
@@ -57,14 +61,14 @@ module-root = ""
57
61
 
58
62
  [dependency-groups]
59
63
  format = [
60
- "ruff==0.15.21"
64
+ "ruff==0.16.3"
61
65
  ]
62
66
  lint = [
63
- "codespell==2.4.2",
64
- "ty==0.0.59"
67
+ "codespell==2.4.3",
68
+ "ty==0.0.72"
65
69
  ]
66
70
  unit = [
67
- "coverage[toml]==7.15.1",
71
+ "coverage[toml]==7.15.4",
68
72
  "pytest==9.1.1"
69
73
  ]
70
74
 
@@ -0,0 +1,152 @@
1
+ # Copyright 2025 Canonical Ltd.
2
+ # See LICENSE file for licensing details.
3
+ """Skeleton for the abstract charm."""
4
+
5
+ from abc import ABC, abstractmethod
6
+
7
+ from data_platform_helpers.advanced_statuses import StatusHandler
8
+ from ops import StatusBase
9
+ from ops.charm import CharmBase
10
+
11
+ from single_kernel_postgresql.core.state import CharmState
12
+ from single_kernel_postgresql.events.database import DatabaseEventsHandler
13
+ from single_kernel_postgresql.events.postgresql import PostgreSQLEventsHandler
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
+ )
18
+ from single_kernel_postgresql.managers.cluster import ClusterManager
19
+ from single_kernel_postgresql.managers.config import ConfigManager
20
+ from single_kernel_postgresql.managers.database import DatabaseManager
21
+ from single_kernel_postgresql.managers.patroni import PatroniManager
22
+ from single_kernel_postgresql.managers.tls import TLSManager
23
+ from single_kernel_postgresql.workload.base import BaseWorkload, ResourceProvider
24
+
25
+ from ..config.enums import Substrates
26
+ from ..config.literals import DATABASE
27
+ from ..utils.postgresql import PostgreSQL
28
+
29
+
30
+ class AbstractPostgreSQLCharm(CharmBase, ABC):
31
+ """An abstract PostgreSQL charm."""
32
+
33
+ def __init__(self, *args):
34
+ super().__init__(*args)
35
+
36
+ # State
37
+ self.state = CharmState(charm=self, substrate=self.substrate)
38
+
39
+ # TLS events handler owns the two certificate requirers; build it before the
40
+ # TLS manager so the manager can constructor-inject them for its live-fetch getters.
41
+ self.tls = TLS(self, self.state)
42
+
43
+ # Managers
44
+ self.tls_manager = TLSManager(
45
+ state=self.state,
46
+ workload=self.workload,
47
+ client_certificate=self.tls.client_certificate,
48
+ peer_certificate=self.tls.peer_certificate,
49
+ )
50
+ self.patroni_manager = PatroniManager(state=self.state, workload=self.workload)
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
+
65
+ self.config_manager = ConfigManager(
66
+ state=self.state,
67
+ workload=self.workload,
68
+ tls_manager=self.tls_manager,
69
+ patroni_manager=self.patroni_manager,
70
+ database_manager=self.database_manager,
71
+ resource_provider=self.get_resource_provider,
72
+ request_restart=self.request_restart,
73
+ restart_services=self.restart_services,
74
+ )
75
+
76
+ # Events Handler
77
+ self.postgresql_events_handler = PostgreSQLEventsHandler(
78
+ self,
79
+ self.workload,
80
+ self.state,
81
+ self.cluster_manager,
82
+ self.tls_manager,
83
+ self.config_manager,
84
+ self.patroni_manager,
85
+ )
86
+
87
+ # Status Handler
88
+ self.status_handler = StatusHandler(
89
+ self,
90
+ self.cluster_manager,
91
+ self.tls_manager,
92
+ self.config_manager,
93
+ self.patroni_manager,
94
+ )
95
+
96
+ # Postgresql Client
97
+ @property
98
+ @abstractmethod
99
+ def postgresql(self) -> PostgreSQL:
100
+ """Return a PostgreSQL client."""
101
+ pass
102
+
103
+ # Postgresql Workload
104
+ @property
105
+ @abstractmethod
106
+ def workload(self) -> BaseWorkload:
107
+ """Access current workload."""
108
+ pass
109
+
110
+ # Postgresql Substrate
111
+ @property
112
+ @abstractmethod
113
+ def substrate(self) -> Substrates:
114
+ """Access current substrate."""
115
+ pass
116
+
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.
123
+ @abstractmethod
124
+ def get_resource_provider(self) -> ResourceProvider:
125
+ """Return the substrate's (cpu_cores, memory_bytes) introspector."""
126
+ pass
127
+
128
+ @abstractmethod
129
+ def request_restart(self) -> None:
130
+ """Run the substrate pre-restart side effect and acquire the restart lock."""
131
+ pass
132
+
133
+ @abstractmethod
134
+ def restart_services(self) -> None:
135
+ """Restart the monitoring and LDAP-sync sidecar services."""
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
@@ -52,7 +54,8 @@ class PostgreSQLK8sCharm(AbstractPostgreSQLCharm):
52
54
  BaseWorkload: The K8sWorkload instance for this charm
53
55
  """
54
56
  return K8sWorkload(
55
- charm_dir=self.charm_dir, container=self.unit.get_container(CONTAINER_NAME)
57
+ charm_dir=self.charm_dir,
58
+ container=self.unit.get_container(CONTAINER_NAME),
56
59
  )
57
60
 
58
61
  @property
@@ -63,3 +66,29 @@ class PostgreSQLK8sCharm(AbstractPostgreSQLCharm):
63
66
  Substrates: always Substrates.K8S for this charm
64
67
  """
65
68
  return Substrates.K8S
69
+
70
+ # The concrete production charm owns these bridges (update_scrape_job_spec +
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
+
77
+ def request_restart(self) -> None:
78
+ """Run the substrate pre-restart side effect and acquire the restart lock."""
79
+
80
+ def restart_services(self) -> None:
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,10 +6,11 @@
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
12
- from single_kernel_postgresql.workload.base import BaseWorkload
13
14
  from single_kernel_postgresql.workload.vm import VMWorkload
14
15
 
15
16
  logger = logging.getLogger(__name__)
@@ -38,13 +39,13 @@ class PostgreSQLVMCharm(AbstractPostgreSQLCharm):
38
39
  )
39
40
 
40
41
  @property
41
- def workload(self) -> BaseWorkload:
42
+ def workload(self) -> VMWorkload:
42
43
  """Access current workload instance.
43
44
 
44
45
  Returns the workload object.
45
46
 
46
47
  Returns:
47
- BaseWorkload: The VMWorkload instance for this charm
48
+ VMWorkload: The VMWorkload instance for this charm
48
49
  """
49
50
  return VMWorkload(charm_dir=self.charm_dir)
50
51
 
@@ -56,3 +57,30 @@ class PostgreSQLVMCharm(AbstractPostgreSQLCharm):
56
57
  Substrates: always Substrates.VM for this charm
57
58
  """
58
59
  return Substrates.VM
60
+
61
+ # The concrete production charm owns these bridges (pops postgresql_restarted +
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
+
68
+ def request_restart(self) -> None:
69
+ """Run the substrate pre-restart side effect and acquire the restart lock."""
70
+
71
+ def restart_services(self) -> None:
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
@@ -73,3 +73,7 @@ class SwitchoverNotSyncError(SwitchoverFailedError):
73
73
 
74
74
  class UpdateSyncNodeCountError(Exception):
75
75
  """Raised when updating synchronous_node_count failed for some reason."""
76
+
77
+
78
+ class DeployedWithoutTrustError(Exception):
79
+ """Raised when the K8s API denies access because the app wasn't deployed with --trust."""
@@ -225,6 +225,48 @@ class PostgreSQLPeer(RelationState):
225
225
  return True
226
226
  return self.relation.data[self.unit].get("connectivity", "on") == "on"
227
227
 
228
+ @property
229
+ def config_hash(self) -> str | None:
230
+ """Get the last-applied PostgreSQL config hash from the peer relation data."""
231
+ if not self.relation:
232
+ return None
233
+ return self.relation.data[self.unit].get("config_hash")
234
+
235
+ @config_hash.setter
236
+ def config_hash(self, value: str) -> None:
237
+ """Set the last-applied PostgreSQL config hash in the peer relation data."""
238
+ if not self.relation:
239
+ return
240
+ self.relation.data[self.unit]["config_hash"] = value
241
+
242
+ @property
243
+ def user_hash(self) -> str | None:
244
+ """Get the last-applied users hash from the peer relation data."""
245
+ if not self.relation:
246
+ return None
247
+ return self.relation.data[self.unit].get("user_hash")
248
+
249
+ @user_hash.setter
250
+ def user_hash(self, value: str) -> None:
251
+ """Set the last-applied users hash in the peer relation data."""
252
+ if not self.relation:
253
+ return
254
+ self.relation.data[self.unit]["user_hash"] = value
255
+
256
+ @property
257
+ def tls(self) -> bool:
258
+ """Get the last-rendered TLS flag from the peer relation data."""
259
+ if not self.relation:
260
+ return False
261
+ return self.relation.data[self.unit].get("tls") == "enabled"
262
+
263
+ @tls.setter
264
+ def tls(self, value: bool) -> None:
265
+ """Set the last-rendered TLS flag in the peer relation data."""
266
+ if not self.relation:
267
+ return
268
+ self.relation.data[self.unit]["tls"] = "enabled" if value else ""
269
+
228
270
  @cached_property
229
271
  def data(self) -> MutableMapping[str, str]:
230
272
  """Escape hatch method to access the peer data directly."""
@@ -418,6 +460,20 @@ class PostgreSQLApplication(RelationState):
418
460
  """Return whether this unit has LDAP enabled."""
419
461
  return self.is_ldap_charm_related and self.is_cluster_initialised
420
462
 
463
+ @property
464
+ def user_hash(self) -> str | None:
465
+ """Get the last-applied users hash from the peer relation data."""
466
+ if not self.relation:
467
+ return None
468
+ return self.relation.data[self.app].get("user_hash")
469
+
470
+ @user_hash.setter
471
+ def user_hash(self, value: str) -> None:
472
+ """Set the last-applied users hash in the peer relation data."""
473
+ if not self.relation:
474
+ return
475
+ self.relation.data[self.app]["user_hash"] = value
476
+
421
477
  def get_secret(self, key: str) -> str | None:
422
478
  """Get the secret value for 'key' from the peer relation data."""
423
479
  if not self.relation:
@@ -235,6 +235,13 @@ class CharmState(Object):
235
235
  addresses.remove(current_unit_ip)
236
236
  return addresses
237
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
+
238
245
  @property
239
246
  def host(self) -> str:
240
247
  """Current unit host."""
@@ -526,3 +533,8 @@ class CharmState(Object):
526
533
  def primary_endpoint(self) -> str:
527
534
  """Returns the endpoint of the primary instance's service."""
528
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")