Skip to content

Commit 9ab116c

Browse files
committed
Merge branch '16/edge' into sync14to16
2 parents fb7ef70 + 2bb989c commit 9ab116c

File tree

23 files changed

+429
-122
lines changed

23 files changed

+429
-122
lines changed

lib/charms/operator_libs_linux/v2/snap.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@
102102

103103
# Increment this PATCH version before using `charmcraft publish-lib` or reset
104104
# to 0 if you are raising the major API version
105-
LIBPATCH = 10
105+
LIBPATCH = 11
106106

107107

108108
# Regex to locate 7-bit C1 ANSI sequences
@@ -577,6 +577,9 @@ def _refresh(
577577
if revision:
578578
args.append(f'--revision="{revision}"')
579579

580+
if self.confinement == 'classic':
581+
args.append('--classic')
582+
580583
if devmode:
581584
args.append("--devmode")
582585

lib/charms/postgresql_k8s/v0/postgresql.py

Lines changed: 69 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,11 +31,11 @@
3131
LIBID = "24ee217a54e840a598ff21a079c3e678"
3232

3333
# Increment this major API version when introducing breaking changes
34-
LIBAPI = 0
34+
LIBAPI = 1
3535

3636
# Increment this PATCH version before using `charmcraft publish-lib` or reset
3737
# to 0 if you are raising the major API version
38-
LIBPATCH = 53
38+
LIBPATCH = 0
3939

4040
# Groups to distinguish HBA access
4141
ACCESS_GROUP_IDENTITY = "identity_access"
@@ -49,6 +49,11 @@
4949
ACCESS_GROUP_RELATION,
5050
]
5151

52+
ROLE_STATS = "charmed_stats"
53+
ROLE_READ = "charmed_read"
54+
ROLE_DML = "charmed_dml"
55+
ROLE_BACKUP = "charmed_backup"
56+
5257
# Groups to distinguish database permissions
5358
PERMISSIONS_GROUP_ADMIN = "admin"
5459

@@ -129,6 +134,14 @@ class PostgreSQLUpdateUserPasswordError(Exception):
129134
"""Exception raised when updating a user password fails."""
130135

131136

137+
class PostgreSQLCreatePredefinedRolesError(Exception):
138+
"""Exception raised when creating predefined roles."""
139+
140+
141+
class PostgreSQLGrantDatabasePrivilegesToUserError(Exception):
142+
"""Exception raised when granting database privileges to user."""
143+
144+
132145
class PostgreSQL:
133146
"""Class to encapsulate all operations related to interacting with PostgreSQL instance."""
134147

@@ -335,6 +348,60 @@ def create_user(
335348
logger.error(f"Failed to create user: {e}")
336349
raise PostgreSQLCreateUserError() from e
337350

351+
def create_predefined_roles(self) -> None:
352+
"""Create predefined roles."""
353+
role_to_queries = {
354+
ROLE_STATS: [
355+
f"CREATE ROLE {ROLE_STATS} NOSUPERUSER NOCREATEDB NOCREATEROLE NOREPLICATION NOLOGIN IN ROLE pg_monitor",
356+
],
357+
ROLE_READ: [
358+
f"CREATE ROLE {ROLE_READ} NOSUPERUSER NOCREATEDB NOCREATEROLE NOREPLICATION NOLOGIN IN ROLE pg_read_all_data",
359+
],
360+
ROLE_DML: [
361+
f"CREATE ROLE {ROLE_DML} NOSUPERUSER NOCREATEDB NOCREATEROLE NOREPLICATION NOLOGIN IN ROLE pg_write_all_data",
362+
],
363+
ROLE_BACKUP: [
364+
f"CREATE ROLE {ROLE_BACKUP} NOSUPERUSER NOCREATEDB NOCREATEROLE NOREPLICATION NOLOGIN IN ROLE pg_checkpoint",
365+
f"GRANT {ROLE_STATS} TO {ROLE_BACKUP}",
366+
f"GRANT execute ON FUNCTION pg_backup_start TO {ROLE_BACKUP}",
367+
f"GRANT execute ON FUNCTION pg_backup_stop TO {ROLE_BACKUP}",
368+
f"GRANT execute ON FUNCTION pg_create_restore_point TO {ROLE_BACKUP}",
369+
f"GRANT execute ON FUNCTION pg_switch_wal TO {ROLE_BACKUP}",
370+
],
371+
}
372+
373+
_, existing_roles = self.list_valid_privileges_and_roles()
374+
375+
try:
376+
with self._connect_to_database() as connection, connection.cursor() as cursor:
377+
for role, queries in role_to_queries.items():
378+
if role in existing_roles:
379+
logger.debug(f"Role {role} already exists")
380+
continue
381+
382+
logger.info(f"Creating predefined role {role}")
383+
384+
for query in queries:
385+
cursor.execute(SQL(query))
386+
except psycopg2.Error as e:
387+
logger.error(f"Failed to create predefined roles: {e}")
388+
raise PostgreSQLCreatePredefinedRolesError() from e
389+
390+
def grant_database_privileges_to_user(
391+
self, user: str, database: str, privileges: list[str]
392+
) -> None:
393+
"""Grant the specified priviliges on the provided database for the user."""
394+
try:
395+
with self._connect_to_database() as connection, connection.cursor() as cursor:
396+
cursor.execute(
397+
SQL("GRANT {} ON DATABASE {} TO {};").format(
398+
Identifier(", ".join(privileges)), Identifier(database), Identifier(user)
399+
)
400+
)
401+
except psycopg2.Error as e:
402+
logger.error(f"Faield to grant privileges to user: {e}")
403+
raise PostgreSQLGrantDatabasePrivilegesToUserError() from e
404+
338405
def delete_user(self, user: str) -> None:
339406
"""Deletes a database user.
340407

poetry.lock

Lines changed: 7 additions & 49 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyproject.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ package-mode = false
66
requires-poetry = ">=2.0.0"
77

88
[tool.poetry.dependencies]
9-
python = "^3.10"
9+
python = "^3.12"
1010
ops = "^2.22.0"
1111
boto3 = "^1.38.28"
1212
pgconnstr = "^1.0.1"
@@ -105,7 +105,7 @@ target-version = ["py38"]
105105
[tool.ruff]
106106
# preview and explicit preview are enabled for CPY001
107107
preview = true
108-
target-version = "py310"
108+
target-version = "py312"
109109
src = ["src", "."]
110110
line-length = 99
111111

refresh_versions.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,6 @@ name = "charmed-postgresql"
66

77
[snap.revisions]
88
# amd64
9-
x86_64 = "170"
9+
x86_64 = "182"
1010
# arm64
11-
aarch64 = "169"
11+
aarch64 = "181"

src/backups.py

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
import shutil
1212
import tempfile
1313
import time
14-
from datetime import datetime, timezone
14+
from datetime import UTC, datetime
1515
from io import BytesIO
1616
from pathlib import Path
1717
from subprocess import TimeoutExpired, run
@@ -27,7 +27,6 @@
2727
from jinja2 import Template
2828
from ops.charm import ActionEvent, HookEvent
2929
from ops.framework import Object
30-
from ops.jujuversion import JujuVersion
3130
from ops.model import ActiveStatus, MaintenanceStatus
3231
from tenacity import RetryError, Retrying, stop_after_attempt, wait_fixed
3332

@@ -422,9 +421,7 @@ def _generate_backup_list_output(self) -> str:
422421
backup_reference, _ = self._parse_backup_id(backup["reference"][-1])
423422
lsn_start_stop = f"{backup['lsn']['start']} / {backup['lsn']['stop']}"
424423
time_start, time_stop = (
425-
datetime.strftime(
426-
datetime.fromtimestamp(stamp, timezone.utc), "%Y-%m-%dT%H:%M:%SZ"
427-
)
424+
datetime.strftime(datetime.fromtimestamp(stamp, UTC), "%Y-%m-%dT%H:%M:%SZ")
428425
for stamp in backup["timestamp"].values()
429426
)
430427
backup_timeline = (
@@ -527,7 +524,7 @@ def _list_timelines(self) -> dict[str, tuple[str, str]]:
527524

528525
return dict[str, tuple[str, str]]({
529526
datetime.strftime(
530-
datetime.fromtimestamp(timeline_object["time"], timezone.utc),
527+
datetime.fromtimestamp(timeline_object["time"], UTC),
531528
BACKUP_ID_FORMAT,
532529
): (
533530
timeline.split("/")[1],
@@ -575,8 +572,8 @@ def _parse_psql_timestamp(self, timestamp: str) -> datetime:
575572
t = re.sub(r"\.(\d+)", lambda x: f".{x[1]:06}", t)
576573
dt = datetime.fromisoformat(t)
577574
# Convert to the timezone-naive
578-
if dt.tzinfo is not None and dt.tzinfo is not timezone.utc:
579-
dt = dt.astimezone(tz=timezone.utc)
575+
if dt.tzinfo is not None and dt.tzinfo is not UTC:
576+
dt = dt.astimezone(tz=UTC)
580577
return dt.replace(tzinfo=None)
581578

582579
def _parse_backup_id(self, label) -> tuple[str, str]:
@@ -893,12 +890,11 @@ def _on_create_backup_action(self, event) -> None:
893890

894891
# Test uploading metadata to S3 to test credentials before backup.
895892
datetime_backup_requested = datetime.now().strftime("%Y-%m-%dT%H:%M:%SZ")
896-
juju_version = JujuVersion.from_environ()
897893
metadata = f"""Date Backup Requested: {datetime_backup_requested}
898894
Model Name: {self.model.name}
899895
Application Name: {self.model.app.name}
900896
Unit Name: {self.charm.unit.name}
901-
Juju Version: {juju_version!s}
897+
Juju Version: {self.charm.model.juju_version!s}
902898
"""
903899
if not self._upload_content_to_s3(
904900
metadata,

0 commit comments

Comments
 (0)