Skip to content

Commit 42b7e00

Browse files
[DPE-8473] Add 'databases' to the invalid database names list (#18)
* Add 'databases' to invalid database names list Signed-off-by: Marcelo Henrique Neppel <[email protected]> * Fix ownership check for also considering rock user Signed-off-by: Marcelo Henrique Neppel <[email protected]> * Add substrate argument to select between Snap and rock users Signed-off-by: Marcelo Henrique Neppel <[email protected]> * Add K8s substrate container for ownership change Signed-off-by: Marcelo Henrique Neppel <[email protected]> * Check permissions also in the container Signed-off-by: Marcelo Henrique Neppel <[email protected]> * Pass container to ownership and permissions check function Signed-off-by: Marcelo Henrique Neppel <[email protected]> * Strip single quotes in container command output Signed-off-by: Marcelo Henrique Neppel <[email protected]> * Remove logic for rock user Signed-off-by: Marcelo Henrique Neppel <[email protected]> * Revert part of the code to the initial approach, but adding the condition to limit the check only for VM Signed-off-by: Marcelo Henrique Neppel <[email protected]> * Revert import format Signed-off-by: Marcelo Henrique Neppel <[email protected]> * Revert test changes Signed-off-by: Marcelo Henrique Neppel <[email protected]> * Revert test deletion Signed-off-by: Marcelo Henrique Neppel <[email protected]> --------- Signed-off-by: Marcelo Henrique Neppel <[email protected]>
1 parent fb3c7f8 commit 42b7e00

File tree

7 files changed

+109
-17
lines changed

7 files changed

+109
-17
lines changed

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
[project]
55
name = "postgresql-charms-single-kernel"
66
description = "Shared and reusable code for PostgreSQL-related charms"
7-
version = "16.0.2"
7+
version = "16.1.0"
88
readme = "README.md"
99
license = "Apache-2.0"
1010
authors = [

single_kernel_postgresql/abstract_charm.py

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

55
from ops.charm import CharmBase
66

7-
from .config.literals import SYSTEM_USERS, USER
7+
from .config.literals import SYSTEM_USERS, USER, Substrates
88
from .utils.postgresql import PostgreSQL
99

1010

@@ -15,6 +15,7 @@ def __init__(self, *args):
1515
super().__init__(*args)
1616

1717
self.postgresql = PostgreSQL(
18+
substrate=Substrates.VM,
1819
primary_host="localhost",
1920
current_host="localhost",
2021
user=USER,

single_kernel_postgresql/config/literals.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
This module should contain the literals used in the charms (paths, enums, etc).
66
"""
77

8+
from enum import Enum
9+
810
# Permissions.
911
POSTGRESQL_STORAGE_PERMISSIONS = 0o700
1012

@@ -19,3 +21,10 @@
1921
SNAP_USER = "_daemon_"
2022
USER = "operator"
2123
SYSTEM_USERS = [MONITORING_USER, REPLICATION_USER, REWIND_USER, USER]
24+
25+
26+
class Substrates(str, Enum):
27+
"""Possible substrates."""
28+
29+
K8S = "k8s"
30+
VM = "vm"

single_kernel_postgresql/utils/postgresql.py

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,13 @@
3030
from ops import ConfigData
3131
from psycopg2.sql import SQL, Identifier, Literal
3232

33-
from ..config.literals import BACKUP_USER, POSTGRESQL_STORAGE_PERMISSIONS, SNAP_USER, SYSTEM_USERS
33+
from ..config.literals import (
34+
BACKUP_USER,
35+
POSTGRESQL_STORAGE_PERMISSIONS,
36+
SNAP_USER,
37+
SYSTEM_USERS,
38+
Substrates,
39+
)
3440
from .filesystem import change_owner
3541

3642
# Groups to distinguish HBA access
@@ -60,6 +66,7 @@
6066
}
6167

6268
INVALID_DATABASE_NAME_BLOCKING_MESSAGE = "invalid database name"
69+
INVALID_DATABASE_NAMES = ["databases", "postgres", "template0", "template1"]
6370
INVALID_EXTRA_USER_ROLE_BLOCKING_MESSAGE = "invalid role(s) for extra user roles"
6471

6572
REQUIRED_PLUGINS = {
@@ -216,13 +223,26 @@ class PostgreSQL:
216223

217224
def __init__(
218225
self,
226+
substrate: Substrates,
219227
primary_host: Optional[str],
220228
current_host: Optional[str],
221229
user: str,
222230
password: Optional[str],
223231
database: str,
224232
system_users: Optional[List[str]] = None,
225233
):
234+
"""Create a PostgreSQL helper.
235+
236+
Args:
237+
substrate: substrate where the charm is running (Substrates.K8S or Substrates.VM).
238+
primary_host: hostname or address for primary database host.
239+
current_host: hostname or address for the current database host.
240+
user: username to connect as.
241+
password: password for the user.
242+
database: default database name.
243+
system_users: list of system users.
244+
"""
245+
self.substrate = substrate
226246
self.primary_host = primary_host
227247
self.current_host = current_host
228248
self.user = user
@@ -322,7 +342,7 @@ def create_database(
322342
if len(database) > 49:
323343
logger.error(f"Invalid database name (it must not exceed 49 characters): {database}.")
324344
raise PostgreSQLCreateDatabaseError(INVALID_DATABASE_NAME_BLOCKING_MESSAGE)
325-
if database in ["postgres", "template0", "template1"]:
345+
if database in INVALID_DATABASE_NAMES:
326346
logger.error(f"Invalid database name: {database}.")
327347
raise PostgreSQLCreateDatabaseError(INVALID_DATABASE_NAME_BLOCKING_MESSAGE)
328348
plugins = plugins if plugins else []
@@ -1077,7 +1097,7 @@ def set_up_database(self, temp_location: Optional[str] = None) -> None:
10771097
if temp_location is not None:
10781098
# Fix permissions on the temporary tablespace location when a reboot happens and tmpfs is being used.
10791099
temp_location_stats = os.stat(temp_location)
1080-
if (
1100+
if self.substrate == Substrates.VM and (
10811101
pwd.getpwuid(temp_location_stats.st_uid).pw_name != SNAP_USER
10821102
or int(temp_location_stats.st_mode & 0o777) != POSTGRESQL_STORAGE_PERMISSIONS
10831103
):

tests/unit/test_filesystem.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from unittest.mock import MagicMock, patch
55

66
import pytest
7+
from single_kernel_postgresql.config.literals import SNAP_USER
78
from single_kernel_postgresql.utils.filesystem import change_owner
89

910

@@ -21,7 +22,8 @@ def test_change_owner_calls_pwd_and_os_chown_with_daemon_user():
2122

2223
change_owner(tmp.name)
2324

24-
getpwnam.assert_called_once_with("_daemon_")
25+
# Ensure getpwnam was called for SNAP_USER and ended up using snap user
26+
getpwnam.assert_called_once_with(SNAP_USER)
2527
chown.assert_called_once_with(tmp.name, uid=1234, gid=4321)
2628

2729

tests/unit/test_postgresql.py

Lines changed: 70 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
PostgreSQLUndefinedPasswordError,
2929
ROLE_DATABASES_OWNER,
3030
)
31+
from single_kernel_postgresql.config.literals import Substrates
3132

3233

3334
@pytest.fixture(autouse=True)
@@ -341,9 +342,11 @@ def test_set_up_database_with_temp_tablespace_and_missing_owner_role(harness):
341342
patch("single_kernel_postgresql.utils.postgresql.pwd.getpwuid") as _getpwuid,
342343
):
343344
# Simulate a temp location owned by wrong user/permissions to trigger fixup (33188 means 0o644)
344-
stat_result = type("stat_result", (), {"st_uid": 0, "st_mode": 33188})
345+
stat_result = type("stat_result", (), {"st_uid": 0, "st_gid": 0, "st_mode": 33188})
345346
_stat.return_value = stat_result
346347
_getpwuid.return_value.pw_name = "root"
348+
_getpwuid.return_value.pw_uid = 0
349+
_getpwuid.return_value.pw_gid = 0
347350

348351
# First connection (non-context) for temp tablespace
349352
execute_direct = _connect_to_database.return_value.cursor.return_value.execute
@@ -403,9 +406,13 @@ def test_set_up_database_owner_mismatch_triggers_rename_and_fix(harness):
403406
patch("single_kernel_postgresql.utils.postgresql.datetime") as _dt,
404407
):
405408
# Owner differs, permissions are correct (16832 means 0o700)
406-
stat_result = type("stat_result", (), {"st_uid": 0, "st_mode": 16832})
409+
# Simulate directory owned by uid 1000 while expected owner has uid 0 to force mismatch
410+
stat_result = type("stat_result", (), {"st_uid": 1000, "st_gid": 1000, "st_mode": 16832})
407411
_stat.return_value = stat_result
412+
# The expected owner (SNAP_USER) resolves to uid 0/gid 0 for the test
408413
_getpwuid.return_value.pw_name = "root"
414+
_getpwuid.return_value.pw_uid = 0
415+
_getpwuid.return_value.pw_gid = 0
409416

410417
# Mock datetime.now(timezone.utc) to a fixed timestamp
411418
_dt.now.return_value = datetime(2025, 1, 1, 1, 2, 3, tzinfo=UTC)
@@ -439,9 +446,11 @@ def test_set_up_database_permissions_mismatch_triggers_rename_and_fix(harness):
439446
patch("single_kernel_postgresql.utils.postgresql.datetime") as _dt,
440447
):
441448
# Owner matches SNAP_USER, permissions differ (33188 means 0o644)
442-
stat_result = type("stat_result", (), {"st_uid": 0, "st_mode": 33188})
449+
stat_result = type("stat_result", (), {"st_uid": 0, "st_gid": 0, "st_mode": 33188})
443450
_stat.return_value = stat_result
444451
_getpwuid.return_value.pw_name = SNAP_USER
452+
_getpwuid.return_value.pw_uid = 0
453+
_getpwuid.return_value.pw_gid = 0
445454

446455
# Mock datetime.now(timezone.utc) to a fixed timestamp
447456
_dt.now.return_value = datetime(2025, 1, 1, 1, 2, 3, tzinfo=UTC)
@@ -497,7 +506,13 @@ def test_set_up_database_raises_wrapped_error(harness):
497506
) as _connect_to_database,
498507
patch("single_kernel_postgresql.utils.postgresql.change_owner"),
499508
patch("single_kernel_postgresql.utils.postgresql.os.chmod"),
509+
patch("single_kernel_postgresql.utils.postgresql.pwd.getpwuid") as _getpwuid,
500510
):
511+
# Provide a dummy passwd entry so has_correct_ownership_and_permissions does not raise
512+
_getpwuid.return_value.pw_name = SNAP_USER
513+
_getpwuid.return_value.pw_uid = 0
514+
_getpwuid.return_value.pw_gid = 0
515+
501516
execute_direct = _connect_to_database.return_value.cursor.return_value.execute
502517
execute_direct.side_effect = psycopg2.Error
503518
with pytest.raises(PostgreSQLDatabasesSetupError):
@@ -506,17 +521,17 @@ def test_set_up_database_raises_wrapped_error(harness):
506521

507522
def test_connect_to_database():
508523
# Error on no host
509-
pg = PostgreSQL(None, None, "operator", None, "postgres", None)
524+
pg = PostgreSQL(Substrates.VM, None, None, "operator", None, "postgres", None)
510525
with pytest.raises(PostgreSQLUndefinedHostError):
511526
pg._connect_to_database()
512527

513528
# Error on no password
514-
pg = PostgreSQL("primary", "current", "operator", None, "postgres", None)
529+
pg = PostgreSQL(Substrates.VM, "primary", "current", "operator", None, "postgres", None)
515530
with pytest.raises(PostgreSQLUndefinedPasswordError):
516531
pg._connect_to_database()
517532

518533
# Returns connection
519-
pg = PostgreSQL("primary", "current", "operator", "password", "postgres", None)
534+
pg = PostgreSQL(Substrates.VM, "primary", "current", "operator", "password", "postgres", None)
520535
with patch(
521536
"single_kernel_postgresql.utils.postgresql.psycopg2.connect",
522537
return_value=sentinel.connection,
@@ -531,7 +546,9 @@ def test_is_user_in_hba():
531546
with patch(
532547
"single_kernel_postgresql.utils.postgresql.PostgreSQL._connect_to_database",
533548
) as _connect_to_database:
534-
pg = PostgreSQL("primary", "current", "operator", "password", "postgres", None)
549+
pg = PostgreSQL(
550+
Substrates.VM, "primary", "current", "operator", "password", "postgres", None
551+
)
535552
_cursor = _connect_to_database().__enter__().cursor().__enter__()
536553

537554
# No result
@@ -562,7 +579,9 @@ def test_drop_hba_triggers():
562579
) as _connect_to_database,
563580
patch("single_kernel_postgresql.utils.postgresql.logger") as _logger,
564581
):
565-
pg = PostgreSQL("primary", "current", "operator", "password", "postgres", None)
582+
pg = PostgreSQL(
583+
Substrates.VM, "primary", "current", "operator", "password", "postgres", None
584+
)
566585
_cursor = _connect_to_database().__enter__().cursor().__enter__()
567586
_cursor.fetchall.return_value = (("db1",), ("db2",))
568587

@@ -614,7 +633,9 @@ def test_create_user():
614633
"single_kernel_postgresql.utils.postgresql.PostgreSQL._process_extra_user_roles",
615634
) as _process_extra_user_roles,
616635
):
617-
pg = PostgreSQL("primary", "current", "operator", "password", "postgres", None)
636+
pg = PostgreSQL(
637+
Substrates.VM, "primary", "current", "operator", "password", "postgres", None
638+
)
618639
_cursor = _connect_to_database().__enter__().cursor().__enter__()
619640
_process_extra_user_roles.return_value = (["role1", "role2"], ["priv1", "priv2"])
620641

@@ -731,9 +752,11 @@ def test_set_up_database_owner_and_permissions_match_no_rename_or_fix(harness):
731752
patch("single_kernel_postgresql.utils.postgresql.pwd.getpwuid") as _getpwuid,
732753
):
733754
# Owner matches SNAP_USER and permissions are correct (16832 means 0o700)
734-
stat_result = type("stat_result", (), {"st_uid": 0, "st_mode": 16832})
755+
stat_result = type("stat_result", (), {"st_uid": 0, "st_gid": 0, "st_mode": 16832})
735756
_stat.return_value = stat_result
736757
_getpwuid.return_value.pw_name = SNAP_USER
758+
_getpwuid.return_value.pw_uid = 0
759+
_getpwuid.return_value.pw_gid = 0
737760

738761
execute_direct = _connect_to_database.return_value.cursor.return_value.execute
739762
fetchone_direct = _connect_to_database.return_value.cursor.return_value.fetchone
@@ -753,3 +776,40 @@ def test_set_up_database_owner_and_permissions_match_no_rename_or_fix(harness):
753776
for c in execute_direct.call_args_list:
754777
if c.args:
755778
assert "ALTER TABLESPACE temp RENAME TO" not in c.args[0]
779+
780+
781+
def test_set_up_database_k8s_skips_change_owner_and_chmod(harness):
782+
"""When running on the K8S substrate, filesystem ownership/permission fixes should be skipped.
783+
784+
Even if the on-disk owner/permissions appear incorrect, change_owner and os.chmod must
785+
not be called for Substrates.K8S.
786+
"""
787+
with (
788+
patch(
789+
"single_kernel_postgresql.utils.postgresql.PostgreSQL._connect_to_database"
790+
) as _connect_to_database,
791+
patch("single_kernel_postgresql.utils.postgresql.PostgreSQL.set_up_login_hook_function"),
792+
patch(
793+
"single_kernel_postgresql.utils.postgresql.PostgreSQL.set_up_predefined_catalog_roles_function"
794+
),
795+
patch("single_kernel_postgresql.utils.postgresql.change_owner") as _change_owner,
796+
patch("single_kernel_postgresql.utils.postgresql.os.chmod") as _chmod,
797+
patch("single_kernel_postgresql.utils.postgresql.os.stat") as _stat,
798+
patch("single_kernel_postgresql.utils.postgresql.pwd.getpwuid") as _getpwuid,
799+
):
800+
# Simulate a temp location owned by wrong user/permissions which would normally
801+
# trigger a fixup when running on VM substrate.
802+
stat_result = type("stat_result", (), {"st_uid": 0, "st_gid": 0, "st_mode": 33188})
803+
_stat.return_value = stat_result
804+
_getpwuid.return_value.pw_name = "root"
805+
_getpwuid.return_value.pw_uid = 0
806+
_getpwuid.return_value.pw_gid = 0
807+
808+
# Force the charm's PostgreSQL instance to think it's running on K8S.
809+
harness.charm.postgresql.substrate = Substrates.K8S
810+
811+
harness.charm.postgresql.set_up_database(temp_location="/var/lib/postgresql/tmp")
812+
813+
# On K8S substrate we must not attempt to change ownership or chmod the path.
814+
_change_owner.assert_not_called()
815+
_chmod.assert_not_called()

uv.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)