postgresql-charms-single-kernel 16.3.4__tar.gz → 16.3.5__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 (56) hide show
  1. {postgresql_charms_single_kernel-16.3.4 → postgresql_charms_single_kernel-16.3.5}/PKG-INFO +4 -1
  2. postgresql_charms_single_kernel-16.3.5/pyproject.toml +166 -0
  3. postgresql_charms_single_kernel-16.3.4/pyproject.toml → postgresql_charms_single_kernel-16.3.5/pyproject.toml.orig +10 -6
  4. {postgresql_charms_single_kernel-16.3.4 → postgresql_charms_single_kernel-16.3.5}/single_kernel_postgresql/charms/abstract_charm.py +27 -1
  5. {postgresql_charms_single_kernel-16.3.4 → postgresql_charms_single_kernel-16.3.5}/single_kernel_postgresql/charms/k8s_charm.py +15 -1
  6. {postgresql_charms_single_kernel-16.3.4 → postgresql_charms_single_kernel-16.3.5}/single_kernel_postgresql/charms/vm_charm.py +15 -3
  7. {postgresql_charms_single_kernel-16.3.4 → postgresql_charms_single_kernel-16.3.5}/single_kernel_postgresql/config/exceptions.py +4 -0
  8. {postgresql_charms_single_kernel-16.3.4 → postgresql_charms_single_kernel-16.3.5}/single_kernel_postgresql/core/peer_relation.py +56 -0
  9. {postgresql_charms_single_kernel-16.3.4 → postgresql_charms_single_kernel-16.3.5}/single_kernel_postgresql/core/state.py +14 -1
  10. {postgresql_charms_single_kernel-16.3.4 → postgresql_charms_single_kernel-16.3.5}/single_kernel_postgresql/events/tls.py +19 -1
  11. postgresql_charms_single_kernel-16.3.5/single_kernel_postgresql/managers/config.py +728 -0
  12. {postgresql_charms_single_kernel-16.3.4 → postgresql_charms_single_kernel-16.3.5}/single_kernel_postgresql/managers/k8s.py +85 -2
  13. {postgresql_charms_single_kernel-16.3.4 → postgresql_charms_single_kernel-16.3.5}/single_kernel_postgresql/managers/patroni.py +1 -1
  14. postgresql_charms_single_kernel-16.3.5/single_kernel_postgresql/templates/__init__.py +4 -0
  15. postgresql_charms_single_kernel-16.3.5/single_kernel_postgresql/templates/patroni.yml.j2 +296 -0
  16. {postgresql_charms_single_kernel-16.3.4 → postgresql_charms_single_kernel-16.3.5}/single_kernel_postgresql/workload/base.py +9 -5
  17. {postgresql_charms_single_kernel-16.3.4 → postgresql_charms_single_kernel-16.3.5}/single_kernel_postgresql/workload/k8s.py +0 -4
  18. {postgresql_charms_single_kernel-16.3.4 → postgresql_charms_single_kernel-16.3.5}/single_kernel_postgresql/workload/paths/base.py +5 -0
  19. {postgresql_charms_single_kernel-16.3.4 → postgresql_charms_single_kernel-16.3.5}/single_kernel_postgresql/workload/paths/k8s.py +2 -2
  20. {postgresql_charms_single_kernel-16.3.4 → postgresql_charms_single_kernel-16.3.5}/single_kernel_postgresql/workload/paths/vm.py +1 -1
  21. {postgresql_charms_single_kernel-16.3.4 → postgresql_charms_single_kernel-16.3.5}/single_kernel_postgresql/workload/vm.py +10 -0
  22. postgresql_charms_single_kernel-16.3.4/single_kernel_postgresql/managers/config.py +0 -295
  23. {postgresql_charms_single_kernel-16.3.4 → postgresql_charms_single_kernel-16.3.5}/LICENSE +0 -0
  24. {postgresql_charms_single_kernel-16.3.4 → postgresql_charms_single_kernel-16.3.5}/README.md +0 -0
  25. {postgresql_charms_single_kernel-16.3.4 → postgresql_charms_single_kernel-16.3.5}/single_kernel_postgresql/__init__.py +0 -0
  26. {postgresql_charms_single_kernel-16.3.4 → postgresql_charms_single_kernel-16.3.5}/single_kernel_postgresql/charmcraft.yaml +0 -0
  27. {postgresql_charms_single_kernel-16.3.4 → postgresql_charms_single_kernel-16.3.5}/single_kernel_postgresql/charms/__init__.py +0 -0
  28. {postgresql_charms_single_kernel-16.3.4 → postgresql_charms_single_kernel-16.3.5}/single_kernel_postgresql/compat/__init__.py +0 -0
  29. {postgresql_charms_single_kernel-16.3.4 → postgresql_charms_single_kernel-16.3.5}/single_kernel_postgresql/compat/postgresql.py +0 -0
  30. {postgresql_charms_single_kernel-16.3.4 → postgresql_charms_single_kernel-16.3.5}/single_kernel_postgresql/compat/pyproject.toml +0 -0
  31. {postgresql_charms_single_kernel-16.3.4 → postgresql_charms_single_kernel-16.3.5}/single_kernel_postgresql/config/__init__.py +0 -0
  32. {postgresql_charms_single_kernel-16.3.4 → postgresql_charms_single_kernel-16.3.5}/single_kernel_postgresql/config/enums.py +0 -0
  33. {postgresql_charms_single_kernel-16.3.4 → postgresql_charms_single_kernel-16.3.5}/single_kernel_postgresql/config/literals.py +0 -0
  34. {postgresql_charms_single_kernel-16.3.4 → postgresql_charms_single_kernel-16.3.5}/single_kernel_postgresql/config/locales.py +0 -0
  35. {postgresql_charms_single_kernel-16.3.4 → postgresql_charms_single_kernel-16.3.5}/single_kernel_postgresql/config/pyproject.toml +0 -0
  36. {postgresql_charms_single_kernel-16.3.4 → postgresql_charms_single_kernel-16.3.5}/single_kernel_postgresql/config/statuses.py +0 -0
  37. {postgresql_charms_single_kernel-16.3.4 → postgresql_charms_single_kernel-16.3.5}/single_kernel_postgresql/core/__init__.py +0 -0
  38. {postgresql_charms_single_kernel-16.3.4 → postgresql_charms_single_kernel-16.3.5}/single_kernel_postgresql/core/config.py +0 -0
  39. {postgresql_charms_single_kernel-16.3.4 → postgresql_charms_single_kernel-16.3.5}/single_kernel_postgresql/core/relation_state.py +0 -0
  40. {postgresql_charms_single_kernel-16.3.4 → postgresql_charms_single_kernel-16.3.5}/single_kernel_postgresql/events/__init__.py +0 -0
  41. {postgresql_charms_single_kernel-16.3.4 → postgresql_charms_single_kernel-16.3.5}/single_kernel_postgresql/events/postgresql.py +0 -0
  42. {postgresql_charms_single_kernel-16.3.4 → postgresql_charms_single_kernel-16.3.5}/single_kernel_postgresql/events/tls_transfer.py +0 -0
  43. {postgresql_charms_single_kernel-16.3.4 → postgresql_charms_single_kernel-16.3.5}/single_kernel_postgresql/lib/charms/data_platform_libs/v0/data_interfaces.py +0 -0
  44. {postgresql_charms_single_kernel-16.3.4 → postgresql_charms_single_kernel-16.3.5}/single_kernel_postgresql/lib/charms/data_platform_libs/v1/data_models.py +0 -0
  45. {postgresql_charms_single_kernel-16.3.4 → postgresql_charms_single_kernel-16.3.5}/single_kernel_postgresql/managers/__init__.py +0 -0
  46. {postgresql_charms_single_kernel-16.3.4 → postgresql_charms_single_kernel-16.3.5}/single_kernel_postgresql/managers/base.py +0 -0
  47. {postgresql_charms_single_kernel-16.3.4 → postgresql_charms_single_kernel-16.3.5}/single_kernel_postgresql/managers/cluster.py +0 -0
  48. {postgresql_charms_single_kernel-16.3.4 → postgresql_charms_single_kernel-16.3.5}/single_kernel_postgresql/managers/tls.py +0 -0
  49. {postgresql_charms_single_kernel-16.3.4 → postgresql_charms_single_kernel-16.3.5}/single_kernel_postgresql/utils/__init__.py +0 -0
  50. {postgresql_charms_single_kernel-16.3.4 → postgresql_charms_single_kernel-16.3.5}/single_kernel_postgresql/utils/arch.py +0 -0
  51. {postgresql_charms_single_kernel-16.3.4 → postgresql_charms_single_kernel-16.3.5}/single_kernel_postgresql/utils/filesystem.py +0 -0
  52. {postgresql_charms_single_kernel-16.3.4 → postgresql_charms_single_kernel-16.3.5}/single_kernel_postgresql/utils/postgresql.py +0 -0
  53. {postgresql_charms_single_kernel-16.3.4 → postgresql_charms_single_kernel-16.3.5}/single_kernel_postgresql/utils/secret.py +0 -0
  54. {postgresql_charms_single_kernel-16.3.4 → postgresql_charms_single_kernel-16.3.5}/single_kernel_postgresql/utils/status.py +0 -0
  55. {postgresql_charms_single_kernel-16.3.4 → postgresql_charms_single_kernel-16.3.5}/single_kernel_postgresql/workload/__init__.py +0 -0
  56. {postgresql_charms_single_kernel-16.3.4 → postgresql_charms_single_kernel-16.3.5}/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.5
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.5"
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.5"
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
 
@@ -42,7 +42,15 @@ class AbstractPostgreSQLCharm(CharmBase, ABC):
42
42
  )
43
43
  self.patroni_manager = PatroniManager(state=self.state, workload=self.workload)
44
44
  self.cluster_manager = ClusterManager(state=self.state, workload=self.workload)
45
- self.config_manager = ConfigManager(state=self.state, workload=self.workload)
45
+ self.config_manager = ConfigManager(
46
+ state=self.state,
47
+ workload=self.workload,
48
+ tls_manager=self.tls_manager,
49
+ patroni_manager=self.patroni_manager,
50
+ request_restart=self.request_restart,
51
+ refresh_endpoints=self.refresh_endpoints,
52
+ restart_services=self.restart_services,
53
+ )
46
54
 
47
55
  # Events Handler
48
56
  self.postgresql_events_handler = PostgreSQLEventsHandler(
@@ -84,3 +92,21 @@ class AbstractPostgreSQLCharm(CharmBase, ABC):
84
92
  def substrate(self) -> Substrates:
85
93
  """Access current substrate."""
86
94
  pass
95
+
96
+ # Config-update bridges: charm-side callables the ConfigManager invokes for the
97
+ # substrate-tangled restart trigger, endpoint refresh and monitoring/ldap service
98
+ # restarts. They stay in the charm until their own migration phases.
99
+ @abstractmethod
100
+ def request_restart(self) -> None:
101
+ """Run the substrate pre-restart side effect and acquire the restart lock."""
102
+ pass
103
+
104
+ @abstractmethod
105
+ def refresh_endpoints(self) -> None:
106
+ """Refresh the client relation endpoints."""
107
+ pass
108
+
109
+ @abstractmethod
110
+ def restart_services(self) -> None:
111
+ """Restart the monitoring and LDAP-sync sidecar services."""
112
+ pass
@@ -26,6 +26,7 @@ class PostgreSQLK8sCharm(AbstractPostgreSQLCharm):
26
26
  "Workload must be an instance of K8sWorkload"
27
27
  )
28
28
  self.k8s_manager = K8sManager(self.state, self.workload)
29
+ self.state.resource_provider = self.k8s_manager
29
30
 
30
31
  @property
31
32
  def postgresql(self) -> PostgreSQL:
@@ -52,7 +53,8 @@ class PostgreSQLK8sCharm(AbstractPostgreSQLCharm):
52
53
  BaseWorkload: The K8sWorkload instance for this charm
53
54
  """
54
55
  return K8sWorkload(
55
- charm_dir=self.charm_dir, container=self.unit.get_container(CONTAINER_NAME)
56
+ charm_dir=self.charm_dir,
57
+ container=self.unit.get_container(CONTAINER_NAME),
56
58
  )
57
59
 
58
60
  @property
@@ -63,3 +65,15 @@ class PostgreSQLK8sCharm(AbstractPostgreSQLCharm):
63
65
  Substrates: always Substrates.K8S for this charm
64
66
  """
65
67
  return Substrates.K8S
68
+
69
+ # The concrete production charm owns these bridges (update_scrape_job_spec +
70
+ # acquire_lock, update_endpoints, pebble metrics/ldap restarts), so they default to
71
+ # no-ops here.
72
+ def request_restart(self) -> None:
73
+ """Run the substrate pre-restart side effect and acquire the restart lock."""
74
+
75
+ def refresh_endpoints(self) -> None:
76
+ """Refresh the client relation endpoints."""
77
+
78
+ def restart_services(self) -> None:
79
+ """Restart the monitoring and LDAP-sync sidecar services."""
@@ -9,7 +9,6 @@ import logging
9
9
  from single_kernel_postgresql.charms.abstract_charm import AbstractPostgreSQLCharm, PostgreSQL
10
10
  from single_kernel_postgresql.config.enums import Substrates
11
11
  from single_kernel_postgresql.config.literals import SYSTEM_USERS, USER
12
- from single_kernel_postgresql.workload.base import BaseWorkload
13
12
  from single_kernel_postgresql.workload.vm import VMWorkload
14
13
 
15
14
  logger = logging.getLogger(__name__)
@@ -21,6 +20,7 @@ class PostgreSQLVMCharm(AbstractPostgreSQLCharm):
21
20
  def __init__(self, *args):
22
21
  """Initialize the PostgreSQL VM Charm."""
23
22
  super().__init__(*args)
23
+ self.state.resource_provider = self.workload
24
24
 
25
25
  @property
26
26
  def postgresql(self) -> PostgreSQL:
@@ -38,13 +38,13 @@ class PostgreSQLVMCharm(AbstractPostgreSQLCharm):
38
38
  )
39
39
 
40
40
  @property
41
- def workload(self) -> BaseWorkload:
41
+ def workload(self) -> VMWorkload:
42
42
  """Access current workload instance.
43
43
 
44
44
  Returns the workload object.
45
45
 
46
46
  Returns:
47
- BaseWorkload: The VMWorkload instance for this charm
47
+ VMWorkload: The VMWorkload instance for this charm
48
48
  """
49
49
  return VMWorkload(charm_dir=self.charm_dir)
50
50
 
@@ -56,3 +56,15 @@ class PostgreSQLVMCharm(AbstractPostgreSQLCharm):
56
56
  Substrates: always Substrates.VM for this charm
57
57
  """
58
58
  return Substrates.VM
59
+
60
+ # The concrete production charm owns these bridges (pops postgresql_restarted +
61
+ # acquire_lock, update_endpoints, snap metrics/ldap restarts), so they default to
62
+ # no-ops here.
63
+ def request_restart(self) -> None:
64
+ """Run the substrate pre-restart side effect and acquire the restart lock."""
65
+
66
+ def refresh_endpoints(self) -> None:
67
+ """Refresh the client relation endpoints."""
68
+
69
+ def restart_services(self) -> None:
70
+ """Restart the monitoring and LDAP-sync sidecar services."""
@@ -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:
@@ -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 Any, get_args
11
+ from typing import TYPE_CHECKING, 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,6 +44,9 @@ 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
+
47
50
 
48
51
  class CharmState(Object):
49
52
  """The global PostgreSQL Charm State."""
@@ -61,6 +64,16 @@ class CharmState(Object):
61
64
 
62
65
  self.statuses = StatusesState(self, STATUS_PEERS_RELATION)
63
66
 
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
+
64
77
  # -- Charm Config
65
78
  @cached_property
66
79
  def config(self) -> CharmConfig:
@@ -25,6 +25,10 @@ class RefreshTLSCertificatesEvent(EventBase):
25
25
  """Event emitted to trigger a re-request of TLS certificates with updated SANs."""
26
26
 
27
27
 
28
+ class TLSFilesPushedEvent(EventBase):
29
+ """Event emitted after the handler successfully pushes cert files to the workload."""
30
+
31
+
28
32
  class TLS(Object):
29
33
  """Owns the client/peer certificate requirers and pushes assigned certs to the workload.
30
34
 
@@ -50,9 +54,18 @@ class TLS(Object):
50
54
  trigger. The client/peer SANs in the certificate requests remain inert until
51
55
  the cluster (address-writer) code migrates and starts writing
52
56
  ``database-address`` / ``database-peers-address`` keys into the peer databag.
57
+
58
+ 4. **Push-completion event.** ``tls_files_pushed`` fires only after a successful
59
+ ``push_tls_files()``. The charm's reload bridge observes it rather than
60
+ ``certificate_available`` directly, so a deferred push (Pebble not ready, or the
61
+ internal CA not yet present) does not trigger a reload against files that were
62
+ never written -- the retried push is what emits and reloads. Defer is per-observer,
63
+ so coupling the reload to this event is the only way a deferred push and its reload
64
+ retry together.
53
65
  """
54
66
 
55
67
  refresh_tls_certificates_event = EventSource(RefreshTLSCertificatesEvent)
68
+ tls_files_pushed = EventSource(TLSFilesPushedEvent)
56
69
 
57
70
  def __init__(self, charm, state):
58
71
  super().__init__(charm, key="tls")
@@ -118,13 +131,16 @@ class TLS(Object):
118
131
  self.refresh_tls_certificates_event.emit()
119
132
 
120
133
  def _push_tls_files(self, event) -> None:
121
- """Guard-then-push helper: defer if the workload is not yet ready.
134
+ """Guard, push, then emit ``tls_files_pushed`` on success; defer if not ready.
122
135
 
123
136
  Two conditions must hold before files can be written:
124
137
  1. The internal CA secret must exist — it is written by the leader on
125
138
  leader-elected, so non-leaders and early hooks may see it absent.
126
139
  2. The workload must accept file writes — on K8s the Pebble container
127
140
  may not be ready yet, causing PostgreSQLFileOperationError.
141
+
142
+ Either guard defers without emitting, so nothing reloads against unwritten files;
143
+ the emit happens only once the push has actually landed.
128
144
  """
129
145
  if not self.state.application.internal_ca:
130
146
  logger.debug("Internal CA not yet present; deferring TLS file push.")
@@ -135,6 +151,8 @@ class TLS(Object):
135
151
  except PostgreSQLFileOperationError:
136
152
  logger.debug("Workload not ready for TLS file write; deferring.")
137
153
  event.defer()
154
+ return
155
+ self.tls_files_pushed.emit()
138
156
 
139
157
  def _on_certificate_available(self, event) -> None:
140
158
  """Push TLS files; the operator client cert/key is read live at push time."""