diff --git a/poetry.lock b/poetry.lock index a339a3f8fb..0cb4009a74 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 2.1.2 and should not be changed by hand. +# This file is automatically @generated by Poetry 2.2.1 and should not be changed by hand. [[package]] name = "allure-pytest" @@ -62,14 +62,14 @@ files = [ [[package]] name = "anyio" -version = "4.10.0" +version = "4.11.0" description = "High-level concurrency and networking framework on top of asyncio or Trio" optional = false python-versions = ">=3.9" groups = ["main"] files = [ - {file = "anyio-4.10.0-py3-none-any.whl", hash = "sha256:60e474ac86736bbfd6f210f7a61218939c318f43f9972497381f1c5e930ed3d1"}, - {file = "anyio-4.10.0.tar.gz", hash = "sha256:3f3fae35c96039744587aa5b8371e7e8e603c0702999535961dd336026973ba6"}, + {file = "anyio-4.11.0-py3-none-any.whl", hash = "sha256:0287e96f4d26d4149305414d4e3bc32f0dcd0862365a4bddea19d7a1ec38c4fc"}, + {file = "anyio-4.11.0.tar.gz", hash = "sha256:82a8d0b81e318cc5ce71a5f1f8b5c4e63619620b63141ef8c995fa0db95a57c4"}, ] [package.dependencies] @@ -78,7 +78,7 @@ sniffio = ">=1.1" typing_extensions = {version = ">=4.5", markers = "python_version < \"3.13\""} [package.extras] -trio = ["trio (>=0.26.1)"] +trio = ["trio (>=0.31.0)"] [[package]] name = "asttokens" @@ -1719,14 +1719,14 @@ testing = ["coverage", "pytest", "pytest-benchmark"] [[package]] name = "postgresql-charms-single-kernel" -version = "0.0.1" +version = "16.0.2" description = "Shared and reusable code for PostgreSQL-related charms" optional = false python-versions = "<4.0,>=3.8" groups = ["main"] files = [ - {file = "postgresql_charms_single_kernel-0.0.1-py3-none-any.whl", hash = "sha256:9696cbe8ca2ac5b766ae4ca135f813d08f83894d387a9be9640ca4dd0027d5eb"}, - {file = "postgresql_charms_single_kernel-0.0.1.tar.gz", hash = "sha256:cc6b921445ed41abf8ebf1550adcc6486908be4dddab7bf010f8798e3a7f500b"}, + {file = "postgresql_charms_single_kernel-16.0.2-py3-none-any.whl", hash = "sha256:774d7e6b056338bdf661766cc9b65f5d0238c47813a7c4fcdb6d0d66a6c1d25b"}, + {file = "postgresql_charms_single_kernel-16.0.2.tar.gz", hash = "sha256:d20efef478b6bcb54025aef2c7a36b539e356d2c5095e81af2e297042138ec5c"}, ] [[package]] @@ -1802,7 +1802,6 @@ files = [ {file = "psycopg2-2.9.10-cp311-cp311-win_amd64.whl", hash = "sha256:0435034157049f6846e95103bd8f5a668788dd913a7c30162ca9503fdf542cb4"}, {file = "psycopg2-2.9.10-cp312-cp312-win32.whl", hash = "sha256:65a63d7ab0e067e2cdb3cf266de39663203d38d6a8ed97f5ca0cb315c73fe067"}, {file = "psycopg2-2.9.10-cp312-cp312-win_amd64.whl", hash = "sha256:4a579d6243da40a7b3182e0430493dbd55950c493d8c68f4eec0b302f6bbf20e"}, - {file = "psycopg2-2.9.10-cp313-cp313-win_amd64.whl", hash = "sha256:91fd603a2155da8d0cfcdbf8ab24a2d54bca72795b90d2a3ed2b6da8d979dee2"}, {file = "psycopg2-2.9.10-cp39-cp39-win32.whl", hash = "sha256:9d5b3b94b79a844a986d029eee38998232451119ad653aea42bb9220a8c5066b"}, {file = "psycopg2-2.9.10-cp39-cp39-win_amd64.whl", hash = "sha256:88138c8dedcbfa96408023ea2b0c369eda40fe5d75002c0964c78f46f11fa442"}, {file = "psycopg2-2.9.10.tar.gz", hash = "sha256:12ec0b40b0273f95296233e8750441339298e6a572f7039da5b260e3c8b60e11"}, @@ -1863,7 +1862,6 @@ files = [ {file = "psycopg2_binary-2.9.10-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:bb89f0a835bcfc1d42ccd5f41f04870c1b936d8507c6df12b7737febc40f0909"}, {file = "psycopg2_binary-2.9.10-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f0c2d907a1e102526dd2986df638343388b94c33860ff3bbe1384130828714b1"}, {file = "psycopg2_binary-2.9.10-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f8157bed2f51db683f31306aa497311b560f2265998122abe1dce6428bd86567"}, - {file = "psycopg2_binary-2.9.10-cp313-cp313-win_amd64.whl", hash = "sha256:27422aa5f11fbcd9b18da48373eb67081243662f9b46e6fd07c3eb46e4535142"}, {file = "psycopg2_binary-2.9.10-cp38-cp38-macosx_12_0_x86_64.whl", hash = "sha256:eb09aa7f9cecb45027683bb55aebaaf45a0df8bf6de68801a6afdc7947bb09d4"}, {file = "psycopg2_binary-2.9.10-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b73d6d7f0ccdad7bc43e6d34273f70d587ef62f824d7261c4ae9b8b1b6af90e8"}, {file = "psycopg2_binary-2.9.10-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ce5ab4bf46a211a8e924d307c1b1fcda82368586a19d0a24f8ae166f5c784864"}, @@ -3087,4 +3085,4 @@ type = ["pytest-mypy"] [metadata] lock-version = "2.1" python-versions = "^3.12" -content-hash = "6401d4db095ebd6a4cdf9ee51eecb4ad42f18179b33722e336c4acd1c030c128" +content-hash = "e150f4e46ff0872c1d5da8e9e4f6fde42a7f710b09dde10db97b268e3026b9c2" diff --git a/pyproject.toml b/pyproject.toml index c654c91d02..dd1b495591 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -19,7 +19,7 @@ pysyncobj = "^0.3.14" psutil = "^7.1.0" charm-refresh = "^3.1.0.2" httpx = "^0.28.1" -postgresql-charms-single-kernel = "^0.0.1" +postgresql-charms-single-kernel = "16.0.2" [tool.poetry.group.charm-libs.dependencies] # data_platform_libs/v0/data_interfaces.py diff --git a/src/cluster.py b/src/cluster.py index 75177bbb89..7c5173ed90 100644 --- a/src/cluster.py +++ b/src/cluster.py @@ -31,6 +31,7 @@ from requests.auth import HTTPBasicAuth from single_kernel_postgresql.config.literals import ( PEER, + POSTGRESQL_STORAGE_PERMISSIONS, REWIND_USER, USER, ) @@ -229,7 +230,7 @@ def configure_patroni_on_unit(self): # Expected permission # Replicas refuse to start with the default permissions - os.chmod(POSTGRESQL_DATA_PATH, 0o750) # noqa: S103 + os.chmod(POSTGRESQL_DATA_PATH, POSTGRESQL_STORAGE_PERMISSIONS) def _change_owner(self, path: str) -> None: """Change the ownership of a file or a directory to the postgres user. diff --git a/tests/integration/test_tmpfs_restart.py b/tests/integration/test_tmpfs_restart.py new file mode 100644 index 0000000000..7e4a8427c1 --- /dev/null +++ b/tests/integration/test_tmpfs_restart.py @@ -0,0 +1,100 @@ +#!/usr/bin/env python3 +# Copyright 2025 Canonical Ltd. +# See LICENSE file for licensing details. + +import logging +import subprocess + +import jubilant +import psycopg2 +import pytest + +from .helpers import DATABASE_APP_NAME +from .jubilant_helpers import get_credentials + +logger = logging.getLogger(__name__) + +TIMEOUT = 20 * 60 +RELATION_ENDPOINT = "postgresql" +DATA_INTEGRATOR_APP_NAME = "data-integrator" + + +@pytest.mark.abort_on_fail +def test_deploy_with_tmpfs_storage(juju: jubilant.Juju, charm) -> None: + """Deploy PostgreSQL with tmpfs temp storage and data-integrator.""" + # Deploy database app with tmpfs for temporary storage. + if DATABASE_APP_NAME not in juju.status().apps: + logger.info("Deploying PostgreSQL with tmpfs temporary storage") + juju.deploy( + charm, + app=DATABASE_APP_NAME, + num_units=1, + config={"profile": "testing"}, + storage={"temp": "5G,tmpfs"}, + ) + + # Deploy data-integrator to get credentials. + if DATA_INTEGRATOR_APP_NAME not in juju.status().apps: + logger.info("Deploying data-integrator") + juju.deploy(DATA_INTEGRATOR_APP_NAME, config={"database-name": "test"}) + + # Relate if not already related + status = juju.status() + if not status.apps[DATABASE_APP_NAME].relations.get(RELATION_ENDPOINT): + juju.integrate(DATA_INTEGRATOR_APP_NAME, DATABASE_APP_NAME) + + logger.info("Waiting for both applications to become active") + juju.wait( + lambda s: jubilant.all_active(s, DATABASE_APP_NAME, DATA_INTEGRATOR_APP_NAME), + timeout=TIMEOUT, + ) + + +def test_restart_and_temp_table(juju: jubilant.Juju) -> None: + """Restart the LXD machine and verify TEMP TABLE creation works afterwards.""" + unit_name = f"{DATABASE_APP_NAME}/0" + + # Find machine name and restart + status = juju.status() + unit_info = status.get_units(unit_name.split("/")[0]).get(unit_name) + machine_name = None + if unit_info: + machine_id = getattr(unit_info, "machine", None) + if machine_id: + # Look up the machine object in status and try common attributes that hold the LXD name + machine_obj = ( + getattr(status, "machines", {}).get(machine_id) + if hasattr(status, "machines") + else None + ) + if machine_obj: + machine_name = getattr(machine_obj, "instance_id", None) + + if machine_name is None: + raise RuntimeError("Unable to determine LXD machine/container name for unit " + unit_name) + + logger.info(f"Restarting LXD machine {machine_name}") + subprocess.check_call(["lxc", "restart", machine_name]) + + # Wait for unit to go active/idle again + logger.info("Waiting for PostgreSQL unit to become active after restart") + juju.wait( + lambda s: jubilant.all_active(s, DATABASE_APP_NAME, DATA_INTEGRATOR_APP_NAME), + delay=30, + timeout=TIMEOUT, + ) + + # Obtain credentials via data-integrator action + creds = get_credentials(juju, f"{DATA_INTEGRATOR_APP_NAME}/0") + uri = creds[RELATION_ENDPOINT]["uris"] + + # Connect and create a TEMPORARY TABLE + connection = None + try: + connection = psycopg2.connect(uri) + connection.autocommit = True + with connection.cursor() as cur: + cur.execute("CREATE TEMPORARY TABLE test (lines TEXT);") + finally: + if connection is not None: + connection.close() diff --git a/tests/spread/test_tmpfs_restart.py/task.yaml b/tests/spread/test_tmpfs_restart.py/task.yaml new file mode 100644 index 0000000000..20bc1d8a8f --- /dev/null +++ b/tests/spread/test_tmpfs_restart.py/task.yaml @@ -0,0 +1,7 @@ +summary: test_tmpfs_restart.py +environment: + TEST_MODULE: test_tmpfs_restart.py +execute: | + tox run -e integration -- "tests/integration/$TEST_MODULE" --model testing --alluredir="$SPREAD_TASK/allure-results" +artifacts: + - allure-results diff --git a/tests/unit/test_cluster.py b/tests/unit/test_cluster.py index 8bae32ee5a..1306958a14 100644 --- a/tests/unit/test_cluster.py +++ b/tests/unit/test_cluster.py @@ -503,7 +503,7 @@ def test_configure_patroni_on_unit(peers_ips, patroni): _open.assert_called_once_with(CREATE_CLUSTER_CONF_PATH, "a") _chmod.assert_called_once_with( - "/var/snap/charmed-postgresql/common/var/lib/postgresql", 488 + "/var/snap/charmed-postgresql/common/var/lib/postgresql", 448 )