mirror of
https://github.com/kevinveenbirkenbach/docker-volume-backup.git
synced 2026-08-20 21:22:54 +00:00
fix(backup)!: dump the container that holds the database, with its password
Two defects kept dedicated Postgres databases out of the backup. docker exec never forwarded PGPASSWORD. execute_to_file set it on baudolo's own process, but nothing carried it across the container boundary, so an engine whose pg_hba demands a password on TCP loopback refused the dump. forward_env passes a bare `-e NAME`, letting docker copy the value out of this process's environment instead of spelling it into argv, where the host's process list would publish it. get_instance returned the container name unchanged when that name carried no database token, claiming an instance it had never derived. An application container therefore answered the same databases.csv row as its own dedicated engine, and application images often ship the engine's client tools, so the dump command started and wrote a file that looked like a backup and held none of the data. Discourse is the live case: its launcher names the container `discourse`, and the image ships pg_dumpall. The regex stays a normaliser - `<app>-database` from compose and `<app>_database.1.<task>` from swarm still resolve to the same instance. Only the fallthrough changes. BREAKING CHANGE: a database container whose name carries no `database`, `db` or `postgres` token must now be named in --database-containers. Without that declaration its rows no longer match and no dump is written. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
67
tests/unit/backup/test_get_instance.py
Normal file
67
tests/unit/backup/test_get_instance.py
Normal file
@@ -0,0 +1,67 @@
|
||||
"""Which databases.csv instance a container name resolves to.
|
||||
|
||||
The cases are the container names real deployments produce, in both compose
|
||||
and swarm, so a change to the regex has to state which shape it gives up.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import unittest
|
||||
|
||||
from baudolo.backup.db import get_instance
|
||||
|
||||
|
||||
class TestDeclaredContainers(unittest.TestCase):
|
||||
def test_a_declared_container_is_its_own_instance(self) -> None:
|
||||
self.assertEqual(
|
||||
get_instance("postgres-central", ["postgres-central"]), "postgres-central"
|
||||
)
|
||||
|
||||
def test_a_declaration_beats_the_regex(self) -> None:
|
||||
"""A declared name is taken whole even when it carries a token the
|
||||
fallback would otherwise strip."""
|
||||
self.assertEqual(
|
||||
get_instance("shop-database", ["shop-database"]), "shop-database"
|
||||
)
|
||||
|
||||
def test_an_undeclared_central_engine_resolves_to_nothing(self) -> None:
|
||||
"""`postgres-central` has no separator before its token, so nothing is
|
||||
stripped - a central engine has to be declared to be found."""
|
||||
self.assertIsNone(get_instance("postgres-central", []))
|
||||
|
||||
|
||||
class TestDedicatedEngines(unittest.TestCase):
|
||||
def test_compose_names_the_container_with_a_hyphen(self) -> None:
|
||||
self.assertEqual(get_instance("discourse-database", []), "discourse")
|
||||
|
||||
def test_swarm_names_the_task_with_an_underscore_and_a_slot(self) -> None:
|
||||
"""Swarm suppresses container_name and names the task
|
||||
<stack>_<service>.<slot>.<id>, which must land on the same instance as
|
||||
the compose name so one databases.csv serves both modes."""
|
||||
self.assertEqual(get_instance("discourse_database.1.k3f9x2", []), "discourse")
|
||||
|
||||
def test_an_explicitly_named_engine_keeps_its_entity(self) -> None:
|
||||
self.assertEqual(get_instance("bigbluebutton-postgres-1", []), "bigbluebutton")
|
||||
|
||||
def test_the_short_token_is_stripped_too(self) -> None:
|
||||
self.assertEqual(get_instance("matomo-db", []), "matomo")
|
||||
|
||||
def test_mariadb_uses_the_same_suffix(self) -> None:
|
||||
self.assertEqual(get_instance("matomo-database", []), "matomo")
|
||||
|
||||
|
||||
class TestApplicationContainers(unittest.TestCase):
|
||||
def test_a_bare_application_name_is_not_a_database(self) -> None:
|
||||
"""Returning the name unchanged here would offer the application as a
|
||||
second engine for its own dedicated database's instance."""
|
||||
self.assertIsNone(get_instance("discourse", []))
|
||||
|
||||
def test_a_swarm_application_task_is_not_a_database(self) -> None:
|
||||
self.assertIsNone(get_instance("discourse_discourse.1.k3f9x2", []))
|
||||
|
||||
def test_an_application_that_merely_starts_with_a_token_is_not_split(self) -> None:
|
||||
self.assertIsNone(get_instance("dbeaver", []))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user