159 lines
5.5 KiB
Python
159 lines
5.5 KiB
Python
"""An engine whose loopback auth really demands a password.
|
|||
|
|
|
||
|
|
Every other Postgres scenario runs stock postgres:alpine, whose generated
|
||
|
|
pg_hba grants trust on 127.0.0.1 and ::1 - so `pg_dump -h localhost` never
|
||
|
|
needs the password and a dump succeeds whether or not baudolo hands one to the
|
||
|
|
container. This module makes the password mandatory, which is what a dedicated
|
||
|
|
engine on a real host does.
|
||
|
|
"""
|
||
|
|
|
||
|
|
import unittest
|
||
|
|
from pathlib import Path
|
||
|
|
|
||
|
|
from baudolo.generation import CLUSTER_SUFFIX, DUMP_SUFFIX, FILES_DIR, SQL_DIR
|
||
|
|
|
||
|
|
from .helpers import (
|
||
|
|
POSTGRES_DATA_DIR,
|
||
|
|
POSTGRES_IMAGE,
|
||
|
|
backup_path,
|
||
|
|
backup_run,
|
||
|
|
cleanup_docker,
|
||
|
|
create_minimal_compose_dir,
|
||
|
|
ensure_empty_dir,
|
||
|
|
latest_version_dir,
|
||
|
|
require_docker,
|
||
|
|
run,
|
||
|
|
unique,
|
||
|
|
wait_for_postgres,
|
||
|
|
write_databases_csv,
|
||
|
|
)
|
||
|
|
|
||
|
|
|
||
|
|
class TestE2EPostgresPasswordRequired(unittest.TestCase):
|
||
|
|
@classmethod
|
||
|
|
def setUpClass(cls) -> None:
|
||
|
|
require_docker()
|
||
|
|
cls.prefix = unique("baudolo-e2e-pg-password-required")
|
||
|
|
cls.backups_dir = f"/tmp/{cls.prefix}/Backups"
|
||
|
|
ensure_empty_dir(cls.backups_dir)
|
||
|
|
cls.compose_dir = create_minimal_compose_dir(f"/tmp/{cls.prefix}")
|
||
|
|
cls.repo_name = cls.prefix
|
||
|
|
|
||
|
|
cls.pg_container = f"{cls.prefix}-pg"
|
||
|
|
cls.pg_volume = f"{cls.prefix}-pg-vol"
|
||
|
|
cls.containers = [cls.pg_container]
|
||
|
|
cls.volumes = [cls.pg_volume]
|
||
|
|
|
||
|
|
run(["docker", "volume", "create", cls.pg_volume])
|
||
|
|
run(
|
||
|
|
[
|
||
|
|
"docker",
|
||
|
|
"run",
|
||
|
|
"-d",
|
||
|
|
"--name",
|
||
|
|
cls.pg_container,
|
||
|
|
"-e",
|
||
|
|
"POSTGRES_PASSWORD=pgpw",
|
||
|
|
"-e",
|
||
|
|
"POSTGRES_DB=appdb",
|
||
|
|
"-e",
|
||
|
|
"POSTGRES_USER=postgres",
|
||
|
|
# The entrypoint evals this into its initdb call, so the host
|
||
|
|
# lines of pg_hba demand scram while the local socket stays
|
||
|
|
# trust - the entrypoint's own init and the seeding below keep
|
||
|
|
# working, and only a TCP connection needs the password.
|
||
|
|
"-e",
|
||
|
|
"POSTGRES_INITDB_ARGS=--auth-host=scram-sha-256",
|
||
|
|
"-v",
|
||
|
|
f"{cls.pg_volume}:{POSTGRES_DATA_DIR}",
|
||
|
|
POSTGRES_IMAGE,
|
||
|
|
]
|
||
|
|
)
|
||
|
|
wait_for_postgres(cls.pg_container, user="postgres", timeout_s=90)
|
||
|
|
|
||
|
|
run(
|
||
|
|
[
|
||
|
|
"docker",
|
||
|
|
"exec",
|
||
|
|
cls.pg_container,
|
||
|
|
"sh",
|
||
|
|
"-lc",
|
||
|
|
(
|
||
|
|
'psql -U postgres -d appdb -c "CREATE TABLE t (id int primary '
|
||
|
|
"key, v text); INSERT INTO t VALUES (1,'ok');\""
|
||
|
|
),
|
||
|
|
],
|
||
|
|
check=True,
|
||
|
|
)
|
||
|
|
|
||
|
|
cls.unauthenticated = run(
|
||
|
|
[
|
||
|
|
"docker",
|
||
|
|
"exec",
|
||
|
|
cls.pg_container,
|
||
|
|
"sh",
|
||
|
|
"-lc",
|
||
|
|
"pg_dump -U postgres -d appdb -h localhost",
|
||
|
|
],
|
||
|
|
capture=True,
|
||
|
|
check=False,
|
||
|
|
)
|
||
|
|
|
||
|
|
cls.databases_csv = f"/tmp/{cls.prefix}/databases.csv"
|
||
|
|
write_databases_csv(
|
||
|
|
cls.databases_csv,
|
||
|
|
[
|
||
|
|
(cls.pg_container, "appdb", "postgres", "pgpw"),
|
||
|
|
(cls.pg_container, "*", "postgres", "pgpw"),
|
||
|
|
],
|
||
|
|
)
|
||
|
|
|
||
|
|
backup_run(
|
||
|
|
backups_dir=cls.backups_dir,
|
||
|
|
repo_name=cls.repo_name,
|
||
|
|
compose_dir=cls.compose_dir,
|
||
|
|
databases_csv=cls.databases_csv,
|
||
|
|
database_containers=[cls.pg_container],
|
||
|
|
images_no_stop_required=[POSTGRES_IMAGE],
|
||
|
|
only_sql=True,
|
||
|
|
)
|
||
|
|
cls.hash, cls.version = latest_version_dir(cls.backups_dir, cls.repo_name)
|
||
|
|
|
||
|
|
@classmethod
|
||
|
|
def tearDownClass(cls) -> None:
|
||
|
|
cleanup_docker(containers=cls.containers, volumes=cls.volumes)
|
||
|
|
|
||
|
|
def volume_dir(self) -> Path:
|
||
|
|
return backup_path(
|
||
|
|
self.backups_dir, self.repo_name, self.version, self.pg_volume
|
||
|
|
)
|
||
|
|
|
||
|
|
def test_a_dump_without_the_password_is_refused_by_the_server(self) -> None:
|
||
|
|
"""Without this the module is vacuous: a pg_hba still saying trust would
|
||
|
|
let a baudolo that forwards nothing pass just as well."""
|
||
|
|
self.assertNotEqual(self.unauthenticated.returncode, 0)
|
||
|
|
self.assertIn("no password supplied", self.unauthenticated.stderr or "")
|
||
|
|
|
||
|
|
def test_the_configured_database_was_dumped(self) -> None:
|
||
|
|
dump = self.volume_dir() / SQL_DIR / f"appdb{DUMP_SUFFIX}"
|
||
|
|
self.assertTrue(dump.is_file(), f"expected a dump at {dump}")
|
||
|
|
self.assertIn("Dumped by pg_dump", dump.read_text(encoding="utf-8"))
|
||
|
|
|
||
|
|
def test_the_dump_carries_the_payload(self) -> None:
|
||
|
|
"""pg_dump emits table data as COPY ... FROM stdin, so the row reads as
|
||
|
|
tab-separated values rather than as an INSERT literal."""
|
||
|
|
dump = self.volume_dir() / SQL_DIR / f"appdb{DUMP_SUFFIX}"
|
||
|
|
self.assertIn("COPY public.t (id, v) FROM stdin;", dump.read_text("utf-8"))
|
||
|
|
self.assertIn("1\tok", dump.read_text(encoding="utf-8"))
|
||
|
|
|
||
|
|
def test_the_cluster_row_was_dumped_too(self) -> None:
|
||
|
|
cluster = self.volume_dir() / SQL_DIR / f"{self.pg_container}{CLUSTER_SUFFIX}"
|
||
|
|
self.assertTrue(cluster.is_file(), f"expected a cluster dump at {cluster}")
|
||
|
|
self.assertIn("CREATE DATABASE", cluster.read_text(encoding="utf-8"))
|
||
|
|
|
||
|
|
def test_only_sql_left_no_file_copy_behind(self) -> None:
|
||
|
|
self.assertFalse((self.volume_dir() / FILES_DIR).exists())
|
||
|
|
|
||
|
|
|
||
|
|
if __name__ == "__main__":
|
||
|
|
unittest.main()
|