postgresql-charms-single-kernel 16.3.7__tar.gz → 16.3.9__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 (68) hide show
  1. {postgresql_charms_single_kernel-16.3.7 → postgresql_charms_single_kernel-16.3.9}/PKG-INFO +3 -1
  2. {postgresql_charms_single_kernel-16.3.7 → postgresql_charms_single_kernel-16.3.9}/pyproject.toml +6 -4
  3. {postgresql_charms_single_kernel-16.3.7 → postgresql_charms_single_kernel-16.3.9}/pyproject.toml.orig +6 -4
  4. {postgresql_charms_single_kernel-16.3.7 → postgresql_charms_single_kernel-16.3.9}/single_kernel_postgresql/charms/abstract_charm.py +14 -3
  5. {postgresql_charms_single_kernel-16.3.7 → postgresql_charms_single_kernel-16.3.9}/single_kernel_postgresql/config/exceptions.py +12 -0
  6. {postgresql_charms_single_kernel-16.3.7 → postgresql_charms_single_kernel-16.3.9}/single_kernel_postgresql/config/literals.py +13 -3
  7. {postgresql_charms_single_kernel-16.3.7 → postgresql_charms_single_kernel-16.3.9}/single_kernel_postgresql/core/peer_relation.py +166 -0
  8. postgresql_charms_single_kernel-16.3.9/single_kernel_postgresql/core/s3.py +62 -0
  9. {postgresql_charms_single_kernel-16.3.7 → postgresql_charms_single_kernel-16.3.9}/single_kernel_postgresql/core/state.py +36 -0
  10. postgresql_charms_single_kernel-16.3.9/single_kernel_postgresql/events/ldap.py +98 -0
  11. {postgresql_charms_single_kernel-16.3.7 → postgresql_charms_single_kernel-16.3.9}/single_kernel_postgresql/events/tls_transfer.py +1 -1
  12. postgresql_charms_single_kernel-16.3.9/single_kernel_postgresql/lib/charms/data_platform_libs/v0/s3.py +792 -0
  13. postgresql_charms_single_kernel-16.3.9/single_kernel_postgresql/lib/charms/glauth_k8s/v0/ldap.py +583 -0
  14. postgresql_charms_single_kernel-16.3.9/single_kernel_postgresql/managers/backup.py +489 -0
  15. {postgresql_charms_single_kernel-16.3.7 → postgresql_charms_single_kernel-16.3.9}/single_kernel_postgresql/managers/config.py +4 -3
  16. postgresql_charms_single_kernel-16.3.9/single_kernel_postgresql/templates/k8s/pgbackrest.conf.j2 +54 -0
  17. postgresql_charms_single_kernel-16.3.9/single_kernel_postgresql/templates/k8s/pgbackrest.logrotate.j2 +10 -0
  18. postgresql_charms_single_kernel-16.3.9/single_kernel_postgresql/templates/vm/pgbackrest.conf.j2 +58 -0
  19. postgresql_charms_single_kernel-16.3.9/single_kernel_postgresql/templates/vm/pgbackrest.logrotate.j2 +10 -0
  20. postgresql_charms_single_kernel-16.3.9/single_kernel_postgresql/utils/backup.py +240 -0
  21. postgresql_charms_single_kernel-16.3.9/single_kernel_postgresql/utils/s3.py +178 -0
  22. {postgresql_charms_single_kernel-16.3.7 → postgresql_charms_single_kernel-16.3.9}/single_kernel_postgresql/workload/base.py +114 -5
  23. {postgresql_charms_single_kernel-16.3.7 → postgresql_charms_single_kernel-16.3.9}/single_kernel_postgresql/workload/k8s.py +85 -9
  24. {postgresql_charms_single_kernel-16.3.7 → postgresql_charms_single_kernel-16.3.9}/single_kernel_postgresql/workload/paths/base.py +6 -0
  25. {postgresql_charms_single_kernel-16.3.7 → postgresql_charms_single_kernel-16.3.9}/single_kernel_postgresql/workload/paths/k8s.py +6 -0
  26. {postgresql_charms_single_kernel-16.3.7 → postgresql_charms_single_kernel-16.3.9}/single_kernel_postgresql/workload/paths/vm.py +6 -0
  27. {postgresql_charms_single_kernel-16.3.7 → postgresql_charms_single_kernel-16.3.9}/single_kernel_postgresql/workload/vm.py +137 -12
  28. {postgresql_charms_single_kernel-16.3.7 → postgresql_charms_single_kernel-16.3.9}/LICENSE +0 -0
  29. {postgresql_charms_single_kernel-16.3.7 → postgresql_charms_single_kernel-16.3.9}/README.md +0 -0
  30. {postgresql_charms_single_kernel-16.3.7 → postgresql_charms_single_kernel-16.3.9}/single_kernel_postgresql/__init__.py +0 -0
  31. {postgresql_charms_single_kernel-16.3.7 → postgresql_charms_single_kernel-16.3.9}/single_kernel_postgresql/charmcraft.yaml +0 -0
  32. {postgresql_charms_single_kernel-16.3.7 → postgresql_charms_single_kernel-16.3.9}/single_kernel_postgresql/charms/__init__.py +0 -0
  33. {postgresql_charms_single_kernel-16.3.7 → postgresql_charms_single_kernel-16.3.9}/single_kernel_postgresql/charms/k8s_charm.py +0 -0
  34. {postgresql_charms_single_kernel-16.3.7 → postgresql_charms_single_kernel-16.3.9}/single_kernel_postgresql/charms/vm_charm.py +0 -0
  35. {postgresql_charms_single_kernel-16.3.7 → postgresql_charms_single_kernel-16.3.9}/single_kernel_postgresql/compat/__init__.py +0 -0
  36. {postgresql_charms_single_kernel-16.3.7 → postgresql_charms_single_kernel-16.3.9}/single_kernel_postgresql/compat/postgresql.py +0 -0
  37. {postgresql_charms_single_kernel-16.3.7 → postgresql_charms_single_kernel-16.3.9}/single_kernel_postgresql/compat/pyproject.toml +0 -0
  38. {postgresql_charms_single_kernel-16.3.7 → postgresql_charms_single_kernel-16.3.9}/single_kernel_postgresql/config/__init__.py +0 -0
  39. {postgresql_charms_single_kernel-16.3.7 → postgresql_charms_single_kernel-16.3.9}/single_kernel_postgresql/config/enums.py +0 -0
  40. {postgresql_charms_single_kernel-16.3.7 → postgresql_charms_single_kernel-16.3.9}/single_kernel_postgresql/config/locales.py +0 -0
  41. {postgresql_charms_single_kernel-16.3.7 → postgresql_charms_single_kernel-16.3.9}/single_kernel_postgresql/config/pyproject.toml +0 -0
  42. {postgresql_charms_single_kernel-16.3.7 → postgresql_charms_single_kernel-16.3.9}/single_kernel_postgresql/config/statuses.py +0 -0
  43. {postgresql_charms_single_kernel-16.3.7 → postgresql_charms_single_kernel-16.3.9}/single_kernel_postgresql/core/__init__.py +0 -0
  44. {postgresql_charms_single_kernel-16.3.7 → postgresql_charms_single_kernel-16.3.9}/single_kernel_postgresql/core/config.py +0 -0
  45. {postgresql_charms_single_kernel-16.3.7 → postgresql_charms_single_kernel-16.3.9}/single_kernel_postgresql/core/relation_state.py +0 -0
  46. {postgresql_charms_single_kernel-16.3.7 → postgresql_charms_single_kernel-16.3.9}/single_kernel_postgresql/events/__init__.py +0 -0
  47. {postgresql_charms_single_kernel-16.3.7 → postgresql_charms_single_kernel-16.3.9}/single_kernel_postgresql/events/database.py +0 -0
  48. {postgresql_charms_single_kernel-16.3.7 → postgresql_charms_single_kernel-16.3.9}/single_kernel_postgresql/events/postgresql.py +0 -0
  49. {postgresql_charms_single_kernel-16.3.7 → postgresql_charms_single_kernel-16.3.9}/single_kernel_postgresql/events/tls.py +0 -0
  50. {postgresql_charms_single_kernel-16.3.7 → postgresql_charms_single_kernel-16.3.9}/single_kernel_postgresql/lib/charms/data_platform_libs/v0/data_interfaces.py +0 -0
  51. {postgresql_charms_single_kernel-16.3.7 → postgresql_charms_single_kernel-16.3.9}/single_kernel_postgresql/lib/charms/data_platform_libs/v1/data_models.py +0 -0
  52. {postgresql_charms_single_kernel-16.3.7 → postgresql_charms_single_kernel-16.3.9}/single_kernel_postgresql/managers/__init__.py +0 -0
  53. {postgresql_charms_single_kernel-16.3.7 → postgresql_charms_single_kernel-16.3.9}/single_kernel_postgresql/managers/base.py +0 -0
  54. {postgresql_charms_single_kernel-16.3.7 → postgresql_charms_single_kernel-16.3.9}/single_kernel_postgresql/managers/cluster.py +0 -0
  55. {postgresql_charms_single_kernel-16.3.7 → postgresql_charms_single_kernel-16.3.9}/single_kernel_postgresql/managers/database.py +0 -0
  56. {postgresql_charms_single_kernel-16.3.7 → postgresql_charms_single_kernel-16.3.9}/single_kernel_postgresql/managers/k8s.py +0 -0
  57. {postgresql_charms_single_kernel-16.3.7 → postgresql_charms_single_kernel-16.3.9}/single_kernel_postgresql/managers/patroni.py +0 -0
  58. {postgresql_charms_single_kernel-16.3.7 → postgresql_charms_single_kernel-16.3.9}/single_kernel_postgresql/managers/tls.py +0 -0
  59. {postgresql_charms_single_kernel-16.3.7 → postgresql_charms_single_kernel-16.3.9}/single_kernel_postgresql/templates/__init__.py +0 -0
  60. {postgresql_charms_single_kernel-16.3.7 → postgresql_charms_single_kernel-16.3.9}/single_kernel_postgresql/templates/patroni.yml.j2 +0 -0
  61. {postgresql_charms_single_kernel-16.3.7 → postgresql_charms_single_kernel-16.3.9}/single_kernel_postgresql/utils/__init__.py +0 -0
  62. {postgresql_charms_single_kernel-16.3.7 → postgresql_charms_single_kernel-16.3.9}/single_kernel_postgresql/utils/arch.py +0 -0
  63. {postgresql_charms_single_kernel-16.3.7 → postgresql_charms_single_kernel-16.3.9}/single_kernel_postgresql/utils/filesystem.py +0 -0
  64. {postgresql_charms_single_kernel-16.3.7 → postgresql_charms_single_kernel-16.3.9}/single_kernel_postgresql/utils/postgresql.py +0 -0
  65. {postgresql_charms_single_kernel-16.3.7 → postgresql_charms_single_kernel-16.3.9}/single_kernel_postgresql/utils/secret.py +0 -0
  66. {postgresql_charms_single_kernel-16.3.7 → postgresql_charms_single_kernel-16.3.9}/single_kernel_postgresql/utils/status.py +0 -0
  67. {postgresql_charms_single_kernel-16.3.7 → postgresql_charms_single_kernel-16.3.9}/single_kernel_postgresql/workload/__init__.py +0 -0
  68. {postgresql_charms_single_kernel-16.3.7 → postgresql_charms_single_kernel-16.3.9}/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.7
3
+ Version: 16.3.9
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>
@@ -224,6 +224,8 @@ Requires-Dist: tomli ; python_full_version >= '3.12' and extra == 'postgresql'
224
224
  Requires-Dist: pydantic>=2.0 ; python_full_version >= '3.12' and extra == 'postgresql'
225
225
  Requires-Dist: jinja2>=3.1.6 ; python_full_version >= '3.12' and extra == 'postgresql'
226
226
  Requires-Dist: charmlibs-rollingops>=1.1.1 ; python_full_version >= '3.12' and extra == 'postgresql'
227
+ Requires-Dist: boto3>=1.43.85 ; python_full_version >= '3.12' and extra == 'postgresql'
228
+ Requires-Dist: botocore>=1.43.85 ; python_full_version >= '3.12' and extra == 'postgresql'
227
229
  Requires-Dist: pysyncobj>=0.3.15 ; python_full_version >= '3.12' and extra == 'vm'
228
230
  Requires-Dist: psutil>=7.2.2 ; python_full_version >= '3.12' and extra == 'vm'
229
231
  Requires-Dist: charmlibs-snap>=1.0.1 ; python_full_version >= '3.12' and extra == 'vm'
@@ -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.7"
4
+ version = "16.3.9"
5
5
  readme = "README.md"
6
6
  classifiers = [
7
7
  "Development Status :: 3 - Alpha",
@@ -33,6 +33,8 @@ postgresql = [
33
33
  "pydantic>=2.0; python_version >= '3.12'",
34
34
  "jinja2 >=3.1.6; python_version >= '3.12'",
35
35
  "charmlibs-rollingops>=1.1.1; python_version >= '3.12'",
36
+ "boto3>=1.43.85; python_version >= '3.12'",
37
+ "botocore>=1.43.85; python_version >= '3.12'",
36
38
  ]
37
39
  vm = [
38
40
  "pysyncobj>=0.3.15; python_version >= '3.12'",
@@ -155,12 +157,12 @@ exclude = [
155
157
  ]
156
158
 
157
159
  [dependency-groups]
158
- format = ["ruff==0.16.3"]
160
+ format = ["ruff==0.16.8"]
159
161
  lint = [
160
162
  "codespell==2.4.3",
161
- "ty==0.0.72",
163
+ "ty==0.0.82",
162
164
  ]
163
165
  unit = [
164
- "coverage[toml]==7.15.4",
166
+ "coverage[toml]==7.16.1",
165
167
  "pytest==9.1.1",
166
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.7"
7
+ version = "16.3.9"
8
8
  readme = "README.md"
9
9
  license = {file = "LICENSE"}
10
10
  authors = [
@@ -33,6 +33,8 @@ postgresql = [
33
33
  "pydantic>=2.0; python_version >= '3.12'",
34
34
  "jinja2 >=3.1.6; python_version >= '3.12'",
35
35
  "charmlibs-rollingops>=1.1.1; python_version >= '3.12'",
36
+ "boto3>=1.43.85; python_version >= '3.12'",
37
+ "botocore>=1.43.85; python_version >= '3.12'",
36
38
  ]
37
39
  vm = [
38
40
  "pysyncobj>=0.3.15; python_version >= '3.12'",
@@ -61,14 +63,14 @@ module-root = ""
61
63
 
62
64
  [dependency-groups]
63
65
  format = [
64
- "ruff==0.16.3"
66
+ "ruff==0.16.8"
65
67
  ]
66
68
  lint = [
67
69
  "codespell==2.4.3",
68
- "ty==0.0.72"
70
+ "ty==0.0.82"
69
71
  ]
70
72
  unit = [
71
- "coverage[toml]==7.15.4",
73
+ "coverage[toml]==7.16.1",
72
74
  "pytest==9.1.1"
73
75
  ]
74
76
 
@@ -10,11 +10,13 @@ from ops.charm import CharmBase
10
10
 
11
11
  from single_kernel_postgresql.core.state import CharmState
12
12
  from single_kernel_postgresql.events.database import DatabaseEventsHandler
13
+ from single_kernel_postgresql.events.ldap import LDAP
13
14
  from single_kernel_postgresql.events.postgresql import PostgreSQLEventsHandler
14
15
  from single_kernel_postgresql.events.tls import TLS
15
16
  from single_kernel_postgresql.lib.charms.data_platform_libs.v0.data_interfaces import (
16
17
  DatabaseProvides,
17
18
  )
19
+ from single_kernel_postgresql.lib.charms.data_platform_libs.v0.s3 import S3Requirer
18
20
  from single_kernel_postgresql.managers.cluster import ClusterManager
19
21
  from single_kernel_postgresql.managers.config import ConfigManager
20
22
  from single_kernel_postgresql.managers.database import DatabaseManager
@@ -23,7 +25,7 @@ from single_kernel_postgresql.managers.tls import TLSManager
23
25
  from single_kernel_postgresql.workload.base import BaseWorkload, ResourceProvider
24
26
 
25
27
  from ..config.enums import Substrates
26
- from ..config.literals import DATABASE
28
+ from ..config.literals import DATABASE, S3_RELATION_NAME
27
29
  from ..utils.postgresql import PostgreSQL
28
30
 
29
31
 
@@ -34,12 +36,20 @@ class AbstractPostgreSQLCharm(CharmBase, ABC):
34
36
  super().__init__(*args)
35
37
 
36
38
  # State
37
- self.state = CharmState(charm=self, substrate=self.substrate)
39
+ self.state = CharmState(
40
+ charm=self,
41
+ substrate=self.substrate,
42
+ s3_requirer=S3Requirer(self, relation_name=S3_RELATION_NAME),
43
+ )
38
44
 
39
45
  # TLS events handler owns the two certificate requirers; build it before the
40
46
  # TLS manager so the manager can constructor-inject them for its live-fetch getters.
41
47
  self.tls = TLS(self, self.state)
42
48
 
49
+ # LDAP events handler owns the ldap requirer relation and the auth parameters
50
+ # the config manager renders into the Patroni hba section.
51
+ self.ldap = LDAP(self, self.state)
52
+
43
53
  # Managers
44
54
  self.tls_manager = TLSManager(
45
55
  state=self.state,
@@ -68,6 +78,7 @@ class AbstractPostgreSQLCharm(CharmBase, ABC):
68
78
  tls_manager=self.tls_manager,
69
79
  patroni_manager=self.patroni_manager,
70
80
  database_manager=self.database_manager,
81
+ ldap_handler=self.ldap,
71
82
  resource_provider=self.get_resource_provider,
72
83
  request_restart=self.request_restart,
73
84
  restart_services=self.restart_services,
@@ -116,7 +127,7 @@ class AbstractPostgreSQLCharm(CharmBase, ABC):
116
127
 
117
128
  # Charm-side bridges the lib calls back into. request_restart/restart_services are
118
129
  # 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
130
+ # supplies the async/watcher values those phases own; primary_endpoint is the
120
131
  # VM's Patroni-derived primary lookup. set_unit_status routes status writes through
121
132
  # the charm_refresh priority gate and stays until the refresh logic itself migrates
122
133
  # into the library, at which point the managers own their status writes.
@@ -77,3 +77,15 @@ class UpdateSyncNodeCountError(Exception):
77
77
 
78
78
  class DeployedWithoutTrustError(Exception):
79
79
  """Raised when the K8s API denies access because the app wasn't deployed with --trust."""
80
+
81
+
82
+ class StanzaOperationError(PostgreSQLBaseError):
83
+ """Raised when a pgBackRest stanza-create/check/info operation fails."""
84
+
85
+
86
+ class BackupRunError(PostgreSQLBaseError):
87
+ """Raised when a pgBackRest backup run fails and the logs were uploaded."""
88
+
89
+
90
+ class ListBackupsError(PostgreSQLBaseError):
91
+ """Raised when pgBackRest fails to list backups."""
@@ -10,9 +10,13 @@ from typing import Literal
10
10
  # Permissions.
11
11
  POSTGRESQL_STORAGE_PERMISSIONS = 0o700
12
12
 
13
+ # Snap
14
+ POSTGRESQL_SNAP_NAME = "charmed-postgresql"
15
+
13
16
  # Relations
14
17
  PEER_RELATION = "database-peers"
15
18
  STATUS_PEERS_RELATION = "status-peers"
19
+ S3_RELATION_NAME = "s3-parameters"
16
20
 
17
21
  # Users.
18
22
  BACKUP_USER = "backup"
@@ -25,18 +29,20 @@ SYSTEM_USERS = [BACKUP_USER, MONITORING_USER, REPLICATION_USER, REWIND_USER, USE
25
29
 
26
30
  # Paths
27
31
  ## VM Paths
28
- BASE_SNAP_DIR = "/var/snap/charmed-postgresql"
32
+ BASE_SNAP_DIR = f"/var/snap/{POSTGRESQL_SNAP_NAME}"
29
33
  SNAP_DATA = "current"
30
34
  SNAP_COMMON = "common"
31
- SNAP = "/snap/charmed-postgresql/current"
35
+ SNAP = f"/snap/{POSTGRESQL_SNAP_NAME}/current"
32
36
  VM_LOGS_PATH = "var/log/postgresql"
33
37
  VM_DATA_PATH = "var/lib/postgresql"
34
38
  VM_ARCHIVE_PATH = "data/archive"
35
39
  VM_DATA_LOGS_PATH = "data/logs"
36
40
  VM_TEMP_PATH = "data/temp"
41
+ VM_PGBACKREST_LOGS_PATH = "var/log/pgbackrest"
37
42
 
38
43
  ## K8s Paths
39
44
  K8S_DATA_PATH = "var/lib/pg/data"
45
+ K8S_PGBACKREST_LOGS_PATH = "16/main/pgbackrest_logs"
40
46
 
41
47
  ## Shared Paths
42
48
  # NOTE: The paths don't have leading slahes since pathops
@@ -56,6 +62,9 @@ TLS_CA_BUNDLE_FILE = "peer_ca_bundle.pem"
56
62
  TLS_CLIENT_RELATION = "client-certificates"
57
63
  TLS_PEER_RELATION = "peer-certificates"
58
64
 
65
+ # LDAP relation names
66
+ LDAP_RELATION = "ldap"
67
+
59
68
  # Scopes
60
69
  SCOPES = Literal["app", "unit"]
61
70
  APP_SCOPE = "app"
@@ -176,5 +185,6 @@ RAFT_PORT = 2222
176
185
  RAFT_PARTNER_PREFIX = "partner_node_status_server_"
177
186
 
178
187
  # VM services
179
- VM_PATRONI_SERVICE_NAME = "snap.charmed-postgresql.patroni.service"
188
+ VM_PATRONI_SERVICE_NAME = f"snap.{POSTGRESQL_SNAP_NAME}.patroni.service"
180
189
  VM_PATRONI_SERVICE_DEFAULT_PATH = f"/etc/systemd/system/{VM_PATRONI_SERVICE_NAME}"
190
+ VM_PGBACKREST_SERVICE_NAME = "pgbackrest-service"
@@ -72,6 +72,11 @@ class PostgreSQLPeer(RelationState):
72
72
  """Returns whether the unit is in a blocked state."""
73
73
  return isinstance(self.unit.status, BlockedStatus)
74
74
 
75
+ @property
76
+ def status_message(self) -> str:
77
+ """Returns the current unit status message, if any."""
78
+ return self.unit.status.message
79
+
75
80
  @property
76
81
  def internal_cert(self) -> str | None:
77
82
  """Get internal certificate.
@@ -225,6 +230,55 @@ class PostgreSQLPeer(RelationState):
225
230
  return True
226
231
  return self.relation.data[self.unit].get("connectivity", "on") == "on"
227
232
 
233
+ @is_connectivity_enabled.setter
234
+ def is_connectivity_enabled(self, value: bool) -> None:
235
+ """Enable or disable external connectivity to this unit."""
236
+ if not self.relation:
237
+ return
238
+ self.relation.data[self.unit]["connectivity"] = "on" if value else "off"
239
+
240
+ @property
241
+ def stanza(self) -> str | None:
242
+ """Get the pgBackRest stanza name from the unit peer relation data."""
243
+ if not self.relation:
244
+ return None
245
+ return self.relation.data[self.unit].get("stanza")
246
+
247
+ @stanza.setter
248
+ def stanza(self, value: str) -> None:
249
+ """Set the pgBackRest stanza name in the unit peer relation data."""
250
+ if not self.relation:
251
+ return
252
+ self.relation.data[self.unit]["stanza"] = value
253
+
254
+ @property
255
+ def s3_initialization_done(self) -> str | None:
256
+ """Get the s3-initialization-done marker from the unit peer relation data."""
257
+ if not self.relation:
258
+ return None
259
+ return self.relation.data[self.unit].get("s3-initialization-done")
260
+
261
+ @s3_initialization_done.setter
262
+ def s3_initialization_done(self, value: str) -> None:
263
+ """Set the s3-initialization-done marker in the unit peer relation data."""
264
+ if not self.relation:
265
+ return
266
+ self.relation.data[self.unit]["s3-initialization-done"] = value
267
+
268
+ @property
269
+ def s3_initialization_block_message(self) -> str | None:
270
+ """Get the s3 initialization block message from the unit peer relation data."""
271
+ if not self.relation:
272
+ return None
273
+ return self.relation.data[self.unit].get("s3-initialization-block-message")
274
+
275
+ @s3_initialization_block_message.setter
276
+ def s3_initialization_block_message(self, value: str) -> None:
277
+ """Set the s3 initialization block message in the unit peer relation data."""
278
+ if not self.relation:
279
+ return
280
+ self.relation.data[self.unit]["s3-initialization-block-message"] = value
281
+
228
282
  @property
229
283
  def config_hash(self) -> str | None:
230
284
  """Get the last-applied PostgreSQL config hash from the peer relation data."""
@@ -474,6 +528,118 @@ class PostgreSQLApplication(RelationState):
474
528
  return
475
529
  self.relation.data[self.app]["user_hash"] = value
476
530
 
531
+ @property
532
+ def stanza(self) -> str | None:
533
+ """Get the pgBackRest stanza name from the app peer relation data."""
534
+ if not self.relation:
535
+ return None
536
+ return self.relation.data[self.app].get("stanza")
537
+
538
+ @stanza.setter
539
+ def stanza(self, value: str) -> None:
540
+ """Set the pgBackRest stanza name in the app peer relation data."""
541
+ if not self.relation:
542
+ return
543
+ self.relation.data[self.app]["stanza"] = value
544
+
545
+ @property
546
+ def s3_initialization_start(self) -> str | None:
547
+ """Get the s3-initialization-start timestamp from the app peer relation data."""
548
+ if not self.relation:
549
+ return None
550
+ return self.relation.data[self.app].get("s3-initialization-start")
551
+
552
+ @s3_initialization_start.setter
553
+ def s3_initialization_start(self, value: str) -> None:
554
+ """Set the s3-initialization-start timestamp in the app peer relation data."""
555
+ if not self.relation:
556
+ return
557
+ self.relation.data[self.app]["s3-initialization-start"] = value
558
+
559
+ @property
560
+ def s3_initialization_block_message(self) -> str | None:
561
+ """Get the s3 initialization block message from the app peer relation data."""
562
+ if not self.relation:
563
+ return None
564
+ return self.relation.data[self.app].get("s3-initialization-block-message")
565
+
566
+ @s3_initialization_block_message.setter
567
+ def s3_initialization_block_message(self, value: str) -> None:
568
+ """Set the s3 initialization block message in the app peer relation data."""
569
+ if not self.relation:
570
+ return
571
+ self.relation.data[self.app]["s3-initialization-block-message"] = value
572
+
573
+ @property
574
+ def s3_initialization_done(self) -> str | None:
575
+ """Get the s3-initialization-done marker from the app peer relation data."""
576
+ if not self.relation:
577
+ return None
578
+ return self.relation.data[self.app].get("s3-initialization-done")
579
+
580
+ @s3_initialization_done.setter
581
+ def s3_initialization_done(self, value: str) -> None:
582
+ """Set the s3-initialization-done marker in the app peer relation data."""
583
+ if not self.relation:
584
+ return
585
+ self.relation.data[self.app]["s3-initialization-done"] = value
586
+
587
+ @property
588
+ def restoring_backup(self) -> str | None:
589
+ """Get the restoring-backup marker from the app peer relation data."""
590
+ if not self.relation:
591
+ return None
592
+ return self.relation.data[self.app].get("restoring-backup")
593
+
594
+ @restoring_backup.setter
595
+ def restoring_backup(self, value: str) -> None:
596
+ """Set the restoring-backup marker in the app peer relation data."""
597
+ if not self.relation:
598
+ return
599
+ self.relation.data[self.app]["restoring-backup"] = value
600
+
601
+ @property
602
+ def restore_stanza(self) -> str | None:
603
+ """Get the restore-stanza name from the app peer relation data."""
604
+ if not self.relation:
605
+ return None
606
+ return self.relation.data[self.app].get("restore-stanza")
607
+
608
+ @restore_stanza.setter
609
+ def restore_stanza(self, value: str) -> None:
610
+ """Set the restore-stanza name in the app peer relation data."""
611
+ if not self.relation:
612
+ return
613
+ self.relation.data[self.app]["restore-stanza"] = value
614
+
615
+ @property
616
+ def restore_timeline(self) -> str | None:
617
+ """Get the restore-timeline from the app peer relation data."""
618
+ if not self.relation:
619
+ return None
620
+ return self.relation.data[self.app].get("restore-timeline")
621
+
622
+ @restore_timeline.setter
623
+ def restore_timeline(self, value: str) -> None:
624
+ """Set the restore-timeline in the app peer relation data."""
625
+ if not self.relation:
626
+ return
627
+ self.relation.data[self.app]["restore-timeline"] = value
628
+
629
+ @property
630
+ def restore_to_time(self) -> str | None:
631
+ """Get the restore-to-time target from the app peer relation data."""
632
+ if not self.relation:
633
+ return None
634
+ return self.relation.data[self.app].get("restore-to-time")
635
+
636
+ @restore_to_time.setter
637
+ def restore_to_time(self, value: str) -> None:
638
+ """Set the restore-to-time target in the app peer relation data."""
639
+ if not self.relation:
640
+ return
641
+ self.relation.data[self.app]["restore-to-time"] = value
642
+
477
643
  def get_secret(self, key: str) -> str | None:
478
644
  """Get the secret value for 'key' from the peer relation data."""
479
645
  if not self.relation:
@@ -0,0 +1,62 @@
1
+ # Copyright 2026 Canonical Ltd.
2
+ # See LICENSE file for licensing details.
3
+
4
+ """Live-fetch view over the S3 (s3-parameters) relation data."""
5
+
6
+ import logging
7
+
8
+ from single_kernel_postgresql.lib.charms.data_platform_libs.v0.s3 import S3Requirer
9
+
10
+ logger = logging.getLogger(__name__)
11
+
12
+
13
+ class S3ConnectionInfo:
14
+ """Reads and normalizes S3 connection parameters from the s3-parameters relation."""
15
+
16
+ def __init__(self, s3_requirer: S3Requirer):
17
+ """Initialize the S3 connection info with the requirer bound to the relation."""
18
+ self.s3_requirer = s3_requirer
19
+
20
+ def retrieve_s3_parameters(self) -> tuple[dict, list[str]]:
21
+ """Retrieve S3 parameters from the S3 integrator relation.
22
+
23
+ Returns:
24
+ a tuple of (parameters, missing_required_parameters). If any required
25
+ parameter is missing, parameters is empty and missing lists the absent
26
+ keys. Otherwise parameters has defaults applied and values normalized.
27
+ """
28
+ s3_parameters = self.s3_requirer.get_s3_connection_info()
29
+ required_parameters = [
30
+ "bucket",
31
+ "access-key",
32
+ "secret-key",
33
+ ]
34
+ missing_required_parameters = [
35
+ param for param in required_parameters if param not in s3_parameters
36
+ ]
37
+ if missing_required_parameters:
38
+ logger.warning(
39
+ f"Missing required S3 parameters in relation with S3 integrator: {missing_required_parameters}"
40
+ )
41
+ return {}, missing_required_parameters
42
+
43
+ # Add some sensible defaults (as expected by the code) for missing optional parameters
44
+ s3_parameters.setdefault("endpoint", "https://s3.amazonaws.com")
45
+ s3_parameters.setdefault("path", "")
46
+ s3_parameters.setdefault("s3-uri-style", "host")
47
+ s3_parameters.setdefault("delete-older-than-days", "9999999")
48
+
49
+ # Strip whitespaces from all parameters.
50
+ for key, value in s3_parameters.items():
51
+ if isinstance(value, str):
52
+ s3_parameters[key] = value.strip()
53
+
54
+ # Clean up extra slash symbols to avoid issues on 3rd-party storages
55
+ # like Ceph Object Gateway (radosgw).
56
+ s3_parameters["endpoint"] = s3_parameters["endpoint"].rstrip("/")
57
+ s3_parameters["path"] = (
58
+ f"/{s3_parameters['path'].strip('/')}" # The slash in the beginning is required by pgBackRest.
59
+ )
60
+ s3_parameters["bucket"] = s3_parameters["bucket"].strip("/")
61
+
62
+ return s3_parameters, []
@@ -31,15 +31,18 @@ from single_kernel_postgresql.config.literals import (
31
31
  PEER_RELATION,
32
32
  REPLICATION_CONSUMER_RELATION,
33
33
  REPLICATION_OFFER_RELATION,
34
+ S3_RELATION_NAME,
34
35
  SCOPES,
35
36
  STATUS_PEERS_RELATION,
36
37
  )
37
38
  from single_kernel_postgresql.core.config import CharmConfig
38
39
  from single_kernel_postgresql.core.peer_relation import PostgreSQLApplication, PostgreSQLPeer
40
+ from single_kernel_postgresql.core.s3 import S3ConnectionInfo
39
41
  from single_kernel_postgresql.lib.charms.data_platform_libs.v0.data_interfaces import (
40
42
  DataPeerData,
41
43
  DataPeerUnitData,
42
44
  )
45
+ from single_kernel_postgresql.lib.charms.data_platform_libs.v0.s3 import S3Requirer
43
46
  from single_kernel_postgresql.utils import unit_name_to_pod_name
44
47
  from single_kernel_postgresql.utils.secret import translate_field_to_secret_key
45
48
  from single_kernel_postgresql.utils.status import format_status
@@ -52,6 +55,7 @@ class CharmState(Object):
52
55
  self,
53
56
  charm: CharmBase,
54
57
  substrate: Substrates,
58
+ s3_requirer: S3Requirer,
55
59
  ) -> None:
56
60
  """Initialize the CharmState object."""
57
61
  super().__init__(charm, "charm_state")
@@ -60,6 +64,7 @@ class CharmState(Object):
60
64
  self.peer_unit_interface = DataPeerUnitData(model=charm.model, relation_name=PEER_RELATION)
61
65
 
62
66
  self.statuses = StatusesState(self, STATUS_PEERS_RELATION)
67
+ self.s3_requirer = s3_requirer
63
68
 
64
69
  # -- Charm Config
65
70
  @cached_property
@@ -93,6 +98,11 @@ class CharmState(Object):
93
98
  """Get status peers relation."""
94
99
  return self.model.get_relation(STATUS_PEERS_RELATION)
95
100
 
101
+ @property
102
+ def s3_relation(self) -> Relation | None:
103
+ """Get the S3 parameters relation."""
104
+ return self.model.get_relation(S3_RELATION_NAME)
105
+
96
106
  # -- Core State Components
97
107
 
98
108
  @property
@@ -133,6 +143,11 @@ class CharmState(Object):
133
143
  substrate=self.substrate,
134
144
  )
135
145
 
146
+ @property
147
+ def s3_connection_info(self) -> S3ConnectionInfo:
148
+ """Get the S3 connection info state."""
149
+ return S3ConnectionInfo(self.s3_requirer)
150
+
136
151
  # -- Cluster state utilities
137
152
  def _get_hostname_from_unit(self, member: str) -> str:
138
153
  """Create a DNS name for a PostgreSQL/Patroni cluster member.
@@ -216,6 +231,11 @@ class CharmState(Object):
216
231
  """Current model name."""
217
232
  return self.model.name
218
233
 
234
+ @property
235
+ def cluster_name(self) -> str:
236
+ """Patroni cluster name, substrate-conditional (delegates to the app peer state)."""
237
+ return self.application.cluster_name
238
+
219
239
  @cached_property
220
240
  def patroni_url(self) -> str:
221
241
  """Patroni REST API URL."""
@@ -325,6 +345,22 @@ class CharmState(Object):
325
345
  ips.append(self.replication_consumer_ip)
326
346
  return ips
327
347
 
348
+ @property
349
+ def cluster_stanza(self) -> str | None:
350
+ """Return the pgBackRest stanza of the cluster, whichever unit published it.
351
+
352
+ The stanza is written by the primary into its unit databag during S3
353
+ initialization and only later adopted by the leader into app data, so
354
+ while a cluster is mid-initialization (or mid-restore) the authoritative
355
+ copy may still live on a unit. Fetched live on every access.
356
+ """
357
+ if stanza := self.application.stanza:
358
+ return stanza
359
+ for peer in self.application_peers:
360
+ if stanza := peer.stanza:
361
+ return stanza
362
+ return None
363
+
328
364
  # -- Secrets
329
365
  # TODO: This is temporary till data interfaces v1 is integrated
330
366
  def get_secret(self, scope: SCOPES, key: str) -> str | None:
@@ -0,0 +1,98 @@
1
+ #!/usr/bin/env python3
2
+ # Copyright 2025 Canonical Ltd.
3
+ # See LICENSE file for licensing details.
4
+
5
+ """LDAP events handler — owns the ``ldap`` requirer relation and the LDAP auth parameters.
6
+
7
+ Ported from the PostgreSQL VM and K8s charms' LDAP module. The substrate-specific
8
+ LDAP-sync sidecar restart stays behind the charm's ``restart_services`` bridge.
9
+ """
10
+
11
+ import logging
12
+ from typing import Any
13
+
14
+ from ops import Relation
15
+ from ops.framework import Object
16
+ from ops.model import ActiveStatus
17
+
18
+ from single_kernel_postgresql.config.literals import LDAP_RELATION
19
+ from single_kernel_postgresql.core.state import CharmState
20
+ from single_kernel_postgresql.lib.charms.glauth_k8s.v0.ldap import (
21
+ LdapProviderData,
22
+ LdapReadyEvent,
23
+ LdapRequirer,
24
+ LdapUnavailableEvent,
25
+ )
26
+
27
+ logger = logging.getLogger(__name__)
28
+
29
+
30
+ class LDAP(Object):
31
+ """In this class, we manage PostgreSQL LDAP access."""
32
+
33
+ def __init__(self, charm, state: CharmState):
34
+ super().__init__(charm, "ldap")
35
+ self.charm = charm
36
+ self.state = state
37
+
38
+ # LDAP relation handles the config options for LDAP access
39
+ self.ldap = LdapRequirer(self.charm, LDAP_RELATION)
40
+ self.framework.observe(self.ldap.on.ldap_ready, self._on_ldap_ready)
41
+ self.framework.observe(self.ldap.on.ldap_unavailable, self._on_ldap_unavailable)
42
+
43
+ @property
44
+ def _relation(self) -> Relation | None:
45
+ """Return the relation object."""
46
+ return self.model.get_relation(LDAP_RELATION)
47
+
48
+ def _on_ldap_ready(self, _: LdapReadyEvent) -> None:
49
+ """Handler for the LDAP ready event."""
50
+ logger.debug("Enabling LDAP connection")
51
+ if self.charm.unit.is_leader():
52
+ self.state.application.data.update({"ldap_enabled": "True"})
53
+
54
+ self.charm.update_config()
55
+ self.charm.set_unit_status(ActiveStatus())
56
+
57
+ def _on_ldap_unavailable(self, _: LdapUnavailableEvent) -> None:
58
+ """Handler for the LDAP unavailable event."""
59
+ logger.debug("Disabling LDAP connection")
60
+ if self.charm.unit.is_leader():
61
+ self.state.application.data.update({"ldap_enabled": "False"})
62
+
63
+ self.charm.update_config()
64
+
65
+ def get_relation_data(self) -> LdapProviderData | None:
66
+ """Get the LDAP info from the LDAP Provider class."""
67
+ data = self.ldap.consume_ldap_relation_data(relation=self._relation)
68
+ if data is None:
69
+ logger.warning("LDAP relation is not ready")
70
+
71
+ if not self.state.peer.is_connectivity_enabled:
72
+ logger.warning("LDAP server will not be accessible")
73
+
74
+ return data
75
+
76
+ def get_ldap_parameters(self) -> dict[str, Any]:
77
+ """Returns the LDAP configuration to use."""
78
+ if not self.state.application.is_cluster_initialised:
79
+ return {}
80
+ if not self.state.application.is_ldap_charm_related:
81
+ logger.debug("LDAP is not enabled")
82
+ return {}
83
+
84
+ relation_data = self.get_relation_data()
85
+ if relation_data is None:
86
+ return {}
87
+
88
+ return {
89
+ "ldapbasedn": relation_data.base_dn,
90
+ "ldapbinddn": relation_data.bind_dn,
91
+ "ldapbindpasswd": relation_data.bind_password,
92
+ "ldaptls": relation_data.starttls,
93
+ "ldapurl": relation_data.urls[0],
94
+ # LDAP authentication parameters that are exclusive to
95
+ # one of the two supported modes (simple bind or search+bind)
96
+ # must be put at the very end of the parameters string
97
+ "ldapsearchfilter": self.state.config.ldap_search_filter,
98
+ }
@@ -60,7 +60,7 @@ class TLSTransfer(Object):
60
60
  def _on_certificate_removed(self, event: CertificateRemovedEvent) -> None:
61
61
  """Disable TLS when TLS certificate is removed."""
62
62
  relation = self.charm.model.get_relation(TLS_TRANSFER_RELATION, event.relation_id)
63
- if relation is None:
63
+ if relation is None or relation.app is None:
64
64
  logger.error("Relationship not established anymore.")
65
65
  return
66
66