diff --git a/src/baudolo/backup/compose.py b/src/baudolo/backup/compose.py index 487209d..6a2a011 100644 --- a/src/baudolo/backup/compose.py +++ b/src/baudolo/backup/compose.py @@ -4,88 +4,30 @@ import os import shutil import subprocess from pathlib import Path -from typing import List, Optional - - -def _detect_env_file(project_dir: Path) -> Optional[Path]: - """ - Detect Compose env file in a directory. - Preference (same as Infinito.Nexus wrapper): - 1) /.env (file) - 2) /.env/env (file) (legacy layout) - """ - c1 = project_dir / ".env" - if c1.is_file(): - return c1 - - c2 = project_dir / ".env" / "env" - if c2.is_file(): - return c2 - - return None - - -def _detect_compose_files(project_dir: Path) -> List[Path]: - """ - Detect Compose file stack in a directory (same as Infinito.Nexus wrapper). - Always requires docker-compose.yml. - Optionals: - - docker-compose.override.yml - - docker-compose.ca.override.yml - """ - base = project_dir / "docker-compose.yml" - if not base.is_file(): - raise FileNotFoundError(f"Missing docker-compose.yml in: {project_dir}") - - files = [base] - - override = project_dir / "docker-compose.override.yml" - if override.is_file(): - files.append(override) - - ca_override = project_dir / "docker-compose.ca.override.yml" - if ca_override.is_file(): - files.append(ca_override) - - return files - - -def _compose_wrapper_path() -> Optional[str]: - """ - Prefer the Infinito.Nexus compose wrapper if present. - Equivalent to: `which compose` - """ - return shutil.which("compose") +from typing import List def _build_compose_cmd(project_dir: str, passthrough: List[str]) -> List[str]: """ Build the compose command for this project directory. - Behavior: - - If `compose` wrapper exists: use it with --chdir (so it resolves -f/--env-file itself) - - Else: use `docker compose` and replicate wrapper's file/env detection. + Policy: + - If `compose` wrapper exists (Infinito.Nexus): use it and delegate ALL logic to it. + - Else: use plain `docker compose` with --chdir. + - NO custom compose file/env detection in this project. """ pdir = Path(project_dir).resolve() - wrapper = _compose_wrapper_path() + wrapper = shutil.which("compose") if wrapper: - # Wrapper defaults project name to basename of --chdir. # "--" ensures wrapper stops parsing its own args. return [wrapper, "--chdir", str(pdir), "--", *passthrough] - # Fallback: pure docker compose, but mirror wrapper behavior. - files = _detect_compose_files(pdir) - env_file = _detect_env_file(pdir) + docker = shutil.which("docker") + if docker: + return [docker, "compose", "--chdir", str(pdir), *passthrough] - cmd: List[str] = ["docker", "compose"] - for f in files: - cmd += ["-f", str(f)] - if env_file: - cmd += ["--env-file", str(env_file)] - - cmd += passthrough - return cmd + raise RuntimeError("Neither 'compose' nor 'docker' found in PATH") def hard_restart_docker_services(dir_path: str) -> None: diff --git a/tests/unit/backup/test_compose.py b/tests/unit/backup/test_compose.py index d510c7c..d240dd1 100644 --- a/tests/unit/backup/test_compose.py +++ b/tests/unit/backup/test_compose.py @@ -53,52 +53,6 @@ class TestCompose(unittest.TestCase): cls.compose_mod = mod - def test_detect_env_file_prefers_dotenv_over_legacy(self) -> None: - with tempfile.TemporaryDirectory() as td: - tmp_path = Path(td) - d = _setup_compose_dir(tmp_path, env_layout=".env/env") - # Also create .env file -> should be preferred - _touch(d / ".env") - - env_file = self.compose_mod._detect_env_file(d) - self.assertEqual(env_file, d / ".env") - - def test_detect_env_file_uses_legacy_if_no_dotenv(self) -> None: - with tempfile.TemporaryDirectory() as td: - tmp_path = Path(td) - d = _setup_compose_dir(tmp_path, env_layout=".env/env") - - env_file = self.compose_mod._detect_env_file(d) - self.assertEqual(env_file, d / ".env" / "env") - - def test_detect_compose_files_requires_base(self) -> None: - with tempfile.TemporaryDirectory() as td: - tmp_path = Path(td) - d = tmp_path / "stack" - d.mkdir() - - with self.assertRaises(FileNotFoundError): - self.compose_mod._detect_compose_files(d) - - def test_detect_compose_files_includes_optional_overrides(self) -> None: - with tempfile.TemporaryDirectory() as td: - tmp_path = Path(td) - d = _setup_compose_dir( - tmp_path, - with_override=True, - with_ca_override=True, - ) - - files = self.compose_mod._detect_compose_files(d) - self.assertEqual( - files, - [ - d / "docker-compose.yml", - d / "docker-compose.override.yml", - d / "docker-compose.ca.override.yml", - ], - ) - def test_build_cmd_uses_wrapper_when_present(self) -> None: with tempfile.TemporaryDirectory() as td: tmp_path = Path(td) @@ -106,9 +60,12 @@ class TestCompose(unittest.TestCase): tmp_path, with_override=True, with_ca_override=True, env_layout=".env" ) - with patch.object( - self.compose_mod.shutil, "which", lambda name: "/usr/local/bin/compose" - ): + def fake_which(name: str): + if name == "compose": + return "/usr/local/bin/compose" + return None + + with patch.object(self.compose_mod.shutil, "which", fake_which): cmd = self.compose_mod._build_compose_cmd(str(d), ["up", "-d"]) self.assertEqual( @@ -123,7 +80,7 @@ class TestCompose(unittest.TestCase): ], ) - def test_build_cmd_fallback_docker_compose_with_all_files_and_env(self) -> None: + def test_build_cmd_fallback_uses_plain_docker_compose_chdir(self) -> None: with tempfile.TemporaryDirectory() as td: tmp_path = Path(td) d = _setup_compose_dir( @@ -133,22 +90,23 @@ class TestCompose(unittest.TestCase): env_layout=".env", ) - with patch.object(self.compose_mod.shutil, "which", lambda name: None): + def fake_which(name: str): + if name == "compose": + return None + if name == "docker": + return "/usr/bin/docker" + return None + + with patch.object(self.compose_mod.shutil, "which", fake_which): cmd = self.compose_mod._build_compose_cmd( str(d), ["up", "-d", "--force-recreate"] ) expected: List[str] = [ - "docker", + "/usr/bin/docker", "compose", - "-f", - str((d / "docker-compose.yml").resolve()), - "-f", - str((d / "docker-compose.override.yml").resolve()), - "-f", - str((d / "docker-compose.ca.override.yml").resolve()), - "--env-file", - str((d / ".env").resolve()), + "--chdir", + str(d.resolve()), "up", "-d", "--force-recreate", @@ -160,9 +118,12 @@ class TestCompose(unittest.TestCase): tmp_path = Path(td) d = _setup_compose_dir(tmp_path, name="mailu", env_layout=".env") - with patch.object( - self.compose_mod.shutil, "which", lambda name: "/usr/local/bin/compose" - ): + def fake_which(name: str): + if name == "compose": + return "/usr/local/bin/compose" + return None + + with patch.object(self.compose_mod.shutil, "which", fake_which): calls = [] def fake_run(cmd, check: bool): @@ -210,7 +171,14 @@ class TestCompose(unittest.TestCase): env_layout=".env/env", ) - with patch.object(self.compose_mod.shutil, "which", lambda name: None): + def fake_which(name: str): + if name == "compose": + return None + if name == "docker": + return "/usr/bin/docker" + return None + + with patch.object(self.compose_mod.shutil, "which", fake_which): calls = [] def fake_run(cmd, check: bool): @@ -220,19 +188,32 @@ class TestCompose(unittest.TestCase): with patch.object(self.compose_mod.subprocess, "run", fake_run): self.compose_mod.hard_restart_docker_services(str(d)) - down_cmd = calls[0][0] - up_cmd = calls[1][0] - - self.assertTrue(calls[0][1] is True) - self.assertTrue(calls[1][1] is True) - - self.assertEqual(down_cmd[0:2], ["docker", "compose"]) - self.assertEqual(down_cmd[-1], "down") - self.assertIn("--env-file", down_cmd) - - self.assertEqual(up_cmd[0:2], ["docker", "compose"]) - self.assertTrue(up_cmd[-2:] == ["up", "-d"] or up_cmd[-3:] == ["up", "-d"]) - self.assertIn("--env-file", up_cmd) + self.assertEqual( + calls, + [ + ( + [ + "/usr/bin/docker", + "compose", + "--chdir", + str(d.resolve()), + "down", + ], + True, + ), + ( + [ + "/usr/bin/docker", + "compose", + "--chdir", + str(d.resolve()), + "up", + "-d", + ], + True, + ), + ], + ) if __name__ == "__main__":