postgresql-charms-single-kernel 16.3.8__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.
- {postgresql_charms_single_kernel-16.3.8 → postgresql_charms_single_kernel-16.3.9}/PKG-INFO +3 -1
- {postgresql_charms_single_kernel-16.3.8 → postgresql_charms_single_kernel-16.3.9}/pyproject.toml +6 -4
- {postgresql_charms_single_kernel-16.3.8 → postgresql_charms_single_kernel-16.3.9}/pyproject.toml.orig +6 -4
- {postgresql_charms_single_kernel-16.3.8 → postgresql_charms_single_kernel-16.3.9}/single_kernel_postgresql/charms/abstract_charm.py +7 -2
- {postgresql_charms_single_kernel-16.3.8 → postgresql_charms_single_kernel-16.3.9}/single_kernel_postgresql/config/exceptions.py +12 -0
- {postgresql_charms_single_kernel-16.3.8 → postgresql_charms_single_kernel-16.3.9}/single_kernel_postgresql/config/literals.py +10 -3
- {postgresql_charms_single_kernel-16.3.8 → postgresql_charms_single_kernel-16.3.9}/single_kernel_postgresql/core/peer_relation.py +166 -0
- postgresql_charms_single_kernel-16.3.9/single_kernel_postgresql/core/s3.py +62 -0
- {postgresql_charms_single_kernel-16.3.8 → postgresql_charms_single_kernel-16.3.9}/single_kernel_postgresql/core/state.py +36 -0
- {postgresql_charms_single_kernel-16.3.8 → postgresql_charms_single_kernel-16.3.9}/single_kernel_postgresql/events/tls_transfer.py +1 -1
- postgresql_charms_single_kernel-16.3.9/single_kernel_postgresql/lib/charms/data_platform_libs/v0/s3.py +792 -0
- postgresql_charms_single_kernel-16.3.9/single_kernel_postgresql/managers/backup.py +489 -0
- postgresql_charms_single_kernel-16.3.9/single_kernel_postgresql/templates/k8s/pgbackrest.conf.j2 +54 -0
- postgresql_charms_single_kernel-16.3.9/single_kernel_postgresql/templates/k8s/pgbackrest.logrotate.j2 +10 -0
- postgresql_charms_single_kernel-16.3.9/single_kernel_postgresql/templates/vm/pgbackrest.conf.j2 +58 -0
- postgresql_charms_single_kernel-16.3.9/single_kernel_postgresql/templates/vm/pgbackrest.logrotate.j2 +10 -0
- postgresql_charms_single_kernel-16.3.9/single_kernel_postgresql/utils/backup.py +240 -0
- postgresql_charms_single_kernel-16.3.9/single_kernel_postgresql/utils/s3.py +178 -0
- {postgresql_charms_single_kernel-16.3.8 → postgresql_charms_single_kernel-16.3.9}/single_kernel_postgresql/workload/base.py +114 -5
- {postgresql_charms_single_kernel-16.3.8 → postgresql_charms_single_kernel-16.3.9}/single_kernel_postgresql/workload/k8s.py +85 -9
- {postgresql_charms_single_kernel-16.3.8 → postgresql_charms_single_kernel-16.3.9}/single_kernel_postgresql/workload/paths/base.py +6 -0
- {postgresql_charms_single_kernel-16.3.8 → postgresql_charms_single_kernel-16.3.9}/single_kernel_postgresql/workload/paths/k8s.py +6 -0
- {postgresql_charms_single_kernel-16.3.8 → postgresql_charms_single_kernel-16.3.9}/single_kernel_postgresql/workload/paths/vm.py +6 -0
- {postgresql_charms_single_kernel-16.3.8 → postgresql_charms_single_kernel-16.3.9}/single_kernel_postgresql/workload/vm.py +137 -12
- {postgresql_charms_single_kernel-16.3.8 → postgresql_charms_single_kernel-16.3.9}/LICENSE +0 -0
- {postgresql_charms_single_kernel-16.3.8 → postgresql_charms_single_kernel-16.3.9}/README.md +0 -0
- {postgresql_charms_single_kernel-16.3.8 → postgresql_charms_single_kernel-16.3.9}/single_kernel_postgresql/__init__.py +0 -0
- {postgresql_charms_single_kernel-16.3.8 → postgresql_charms_single_kernel-16.3.9}/single_kernel_postgresql/charmcraft.yaml +0 -0
- {postgresql_charms_single_kernel-16.3.8 → postgresql_charms_single_kernel-16.3.9}/single_kernel_postgresql/charms/__init__.py +0 -0
- {postgresql_charms_single_kernel-16.3.8 → postgresql_charms_single_kernel-16.3.9}/single_kernel_postgresql/charms/k8s_charm.py +0 -0
- {postgresql_charms_single_kernel-16.3.8 → postgresql_charms_single_kernel-16.3.9}/single_kernel_postgresql/charms/vm_charm.py +0 -0
- {postgresql_charms_single_kernel-16.3.8 → postgresql_charms_single_kernel-16.3.9}/single_kernel_postgresql/compat/__init__.py +0 -0
- {postgresql_charms_single_kernel-16.3.8 → postgresql_charms_single_kernel-16.3.9}/single_kernel_postgresql/compat/postgresql.py +0 -0
- {postgresql_charms_single_kernel-16.3.8 → postgresql_charms_single_kernel-16.3.9}/single_kernel_postgresql/compat/pyproject.toml +0 -0
- {postgresql_charms_single_kernel-16.3.8 → postgresql_charms_single_kernel-16.3.9}/single_kernel_postgresql/config/__init__.py +0 -0
- {postgresql_charms_single_kernel-16.3.8 → postgresql_charms_single_kernel-16.3.9}/single_kernel_postgresql/config/enums.py +0 -0
- {postgresql_charms_single_kernel-16.3.8 → postgresql_charms_single_kernel-16.3.9}/single_kernel_postgresql/config/locales.py +0 -0
- {postgresql_charms_single_kernel-16.3.8 → postgresql_charms_single_kernel-16.3.9}/single_kernel_postgresql/config/pyproject.toml +0 -0
- {postgresql_charms_single_kernel-16.3.8 → postgresql_charms_single_kernel-16.3.9}/single_kernel_postgresql/config/statuses.py +0 -0
- {postgresql_charms_single_kernel-16.3.8 → postgresql_charms_single_kernel-16.3.9}/single_kernel_postgresql/core/__init__.py +0 -0
- {postgresql_charms_single_kernel-16.3.8 → postgresql_charms_single_kernel-16.3.9}/single_kernel_postgresql/core/config.py +0 -0
- {postgresql_charms_single_kernel-16.3.8 → postgresql_charms_single_kernel-16.3.9}/single_kernel_postgresql/core/relation_state.py +0 -0
- {postgresql_charms_single_kernel-16.3.8 → postgresql_charms_single_kernel-16.3.9}/single_kernel_postgresql/events/__init__.py +0 -0
- {postgresql_charms_single_kernel-16.3.8 → postgresql_charms_single_kernel-16.3.9}/single_kernel_postgresql/events/database.py +0 -0
- {postgresql_charms_single_kernel-16.3.8 → postgresql_charms_single_kernel-16.3.9}/single_kernel_postgresql/events/ldap.py +0 -0
- {postgresql_charms_single_kernel-16.3.8 → postgresql_charms_single_kernel-16.3.9}/single_kernel_postgresql/events/postgresql.py +0 -0
- {postgresql_charms_single_kernel-16.3.8 → postgresql_charms_single_kernel-16.3.9}/single_kernel_postgresql/events/tls.py +0 -0
- {postgresql_charms_single_kernel-16.3.8 → postgresql_charms_single_kernel-16.3.9}/single_kernel_postgresql/lib/charms/data_platform_libs/v0/data_interfaces.py +0 -0
- {postgresql_charms_single_kernel-16.3.8 → postgresql_charms_single_kernel-16.3.9}/single_kernel_postgresql/lib/charms/data_platform_libs/v1/data_models.py +0 -0
- {postgresql_charms_single_kernel-16.3.8 → postgresql_charms_single_kernel-16.3.9}/single_kernel_postgresql/lib/charms/glauth_k8s/v0/ldap.py +0 -0
- {postgresql_charms_single_kernel-16.3.8 → postgresql_charms_single_kernel-16.3.9}/single_kernel_postgresql/managers/__init__.py +0 -0
- {postgresql_charms_single_kernel-16.3.8 → postgresql_charms_single_kernel-16.3.9}/single_kernel_postgresql/managers/base.py +0 -0
- {postgresql_charms_single_kernel-16.3.8 → postgresql_charms_single_kernel-16.3.9}/single_kernel_postgresql/managers/cluster.py +0 -0
- {postgresql_charms_single_kernel-16.3.8 → postgresql_charms_single_kernel-16.3.9}/single_kernel_postgresql/managers/config.py +0 -0
- {postgresql_charms_single_kernel-16.3.8 → postgresql_charms_single_kernel-16.3.9}/single_kernel_postgresql/managers/database.py +0 -0
- {postgresql_charms_single_kernel-16.3.8 → postgresql_charms_single_kernel-16.3.9}/single_kernel_postgresql/managers/k8s.py +0 -0
- {postgresql_charms_single_kernel-16.3.8 → postgresql_charms_single_kernel-16.3.9}/single_kernel_postgresql/managers/patroni.py +0 -0
- {postgresql_charms_single_kernel-16.3.8 → postgresql_charms_single_kernel-16.3.9}/single_kernel_postgresql/managers/tls.py +0 -0
- {postgresql_charms_single_kernel-16.3.8 → postgresql_charms_single_kernel-16.3.9}/single_kernel_postgresql/templates/__init__.py +0 -0
- {postgresql_charms_single_kernel-16.3.8 → postgresql_charms_single_kernel-16.3.9}/single_kernel_postgresql/templates/patroni.yml.j2 +0 -0
- {postgresql_charms_single_kernel-16.3.8 → postgresql_charms_single_kernel-16.3.9}/single_kernel_postgresql/utils/__init__.py +0 -0
- {postgresql_charms_single_kernel-16.3.8 → postgresql_charms_single_kernel-16.3.9}/single_kernel_postgresql/utils/arch.py +0 -0
- {postgresql_charms_single_kernel-16.3.8 → postgresql_charms_single_kernel-16.3.9}/single_kernel_postgresql/utils/filesystem.py +0 -0
- {postgresql_charms_single_kernel-16.3.8 → postgresql_charms_single_kernel-16.3.9}/single_kernel_postgresql/utils/postgresql.py +0 -0
- {postgresql_charms_single_kernel-16.3.8 → postgresql_charms_single_kernel-16.3.9}/single_kernel_postgresql/utils/secret.py +0 -0
- {postgresql_charms_single_kernel-16.3.8 → postgresql_charms_single_kernel-16.3.9}/single_kernel_postgresql/utils/status.py +0 -0
- {postgresql_charms_single_kernel-16.3.8 → postgresql_charms_single_kernel-16.3.9}/single_kernel_postgresql/workload/__init__.py +0 -0
- {postgresql_charms_single_kernel-16.3.8 → 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.
|
|
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'
|
{postgresql_charms_single_kernel-16.3.8 → postgresql_charms_single_kernel-16.3.9}/pyproject.toml
RENAMED
|
@@ -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.
|
|
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.
|
|
160
|
+
format = ["ruff==0.16.8"]
|
|
159
161
|
lint = [
|
|
160
162
|
"codespell==2.4.3",
|
|
161
|
-
"ty==0.0.
|
|
163
|
+
"ty==0.0.82",
|
|
162
164
|
]
|
|
163
165
|
unit = [
|
|
164
|
-
"coverage[toml]==7.
|
|
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
|
+
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.
|
|
66
|
+
"ruff==0.16.8"
|
|
65
67
|
]
|
|
66
68
|
lint = [
|
|
67
69
|
"codespell==2.4.3",
|
|
68
|
-
"ty==0.0.
|
|
70
|
+
"ty==0.0.82"
|
|
69
71
|
]
|
|
70
72
|
unit = [
|
|
71
|
-
"coverage[toml]==7.
|
|
73
|
+
"coverage[toml]==7.16.1",
|
|
72
74
|
"pytest==9.1.1"
|
|
73
75
|
]
|
|
74
76
|
|
|
@@ -16,6 +16,7 @@ from single_kernel_postgresql.events.tls import TLS
|
|
|
16
16
|
from single_kernel_postgresql.lib.charms.data_platform_libs.v0.data_interfaces import (
|
|
17
17
|
DatabaseProvides,
|
|
18
18
|
)
|
|
19
|
+
from single_kernel_postgresql.lib.charms.data_platform_libs.v0.s3 import S3Requirer
|
|
19
20
|
from single_kernel_postgresql.managers.cluster import ClusterManager
|
|
20
21
|
from single_kernel_postgresql.managers.config import ConfigManager
|
|
21
22
|
from single_kernel_postgresql.managers.database import DatabaseManager
|
|
@@ -24,7 +25,7 @@ from single_kernel_postgresql.managers.tls import TLSManager
|
|
|
24
25
|
from single_kernel_postgresql.workload.base import BaseWorkload, ResourceProvider
|
|
25
26
|
|
|
26
27
|
from ..config.enums import Substrates
|
|
27
|
-
from ..config.literals import DATABASE
|
|
28
|
+
from ..config.literals import DATABASE, S3_RELATION_NAME
|
|
28
29
|
from ..utils.postgresql import PostgreSQL
|
|
29
30
|
|
|
30
31
|
|
|
@@ -35,7 +36,11 @@ class AbstractPostgreSQLCharm(CharmBase, ABC):
|
|
|
35
36
|
super().__init__(*args)
|
|
36
37
|
|
|
37
38
|
# State
|
|
38
|
-
self.state = CharmState(
|
|
39
|
+
self.state = CharmState(
|
|
40
|
+
charm=self,
|
|
41
|
+
substrate=self.substrate,
|
|
42
|
+
s3_requirer=S3Requirer(self, relation_name=S3_RELATION_NAME),
|
|
43
|
+
)
|
|
39
44
|
|
|
40
45
|
# TLS events handler owns the two certificate requirers; build it before the
|
|
41
46
|
# TLS manager so the manager can constructor-inject them for its live-fetch getters.
|
|
@@ -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/
|
|
32
|
+
BASE_SNAP_DIR = f"/var/snap/{POSTGRESQL_SNAP_NAME}"
|
|
29
33
|
SNAP_DATA = "current"
|
|
30
34
|
SNAP_COMMON = "common"
|
|
31
|
-
SNAP = "/snap/
|
|
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
|
|
@@ -179,5 +185,6 @@ RAFT_PORT = 2222
|
|
|
179
185
|
RAFT_PARTNER_PREFIX = "partner_node_status_server_"
|
|
180
186
|
|
|
181
187
|
# VM services
|
|
182
|
-
VM_PATRONI_SERVICE_NAME = "snap.
|
|
188
|
+
VM_PATRONI_SERVICE_NAME = f"snap.{POSTGRESQL_SNAP_NAME}.patroni.service"
|
|
183
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:
|
|
@@ -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
|
|