diff --git a/bot_bottle/backend/docker/util.py b/bot_bottle/backend/docker/util.py index f9ce729..af955f5 100644 --- a/bot_bottle/backend/docker/util.py +++ b/bot_bottle/backend/docker/util.py @@ -7,6 +7,7 @@ from __future__ import annotations import re import shutil import subprocess +import tempfile from typing import Iterable, Iterator from ...log import die, info @@ -130,17 +131,26 @@ def build_image_with_cwd( if not os.path.isdir(cwd): die(f"cwd not found at {cwd}") info(f"building image {derived} from {base} with {cwd} -> {workspace.guest_path}") - dockerfile = ( - f"FROM {base}\n" - f"COPY --chown=node:node . {workspace.guest_path}\n" - f"WORKDIR {workspace.workdir}\n" - ) - subprocess.run( - ["docker", "build", "-t", derived, "-f", "-", cwd], - input=dockerfile, - text=True, - check=True, - ) + with tempfile.TemporaryDirectory(prefix="bot-bottle-cwd.") as tmp: + context_dir = os.path.join(tmp, "context") + staged_workspace = os.path.join(context_dir, "workspace") + shutil.copytree( + cwd, + staged_workspace, + symlinks=True, + ignore=shutil.ignore_patterns(".git"), + ) + dockerfile = ( + f"FROM {base}\n" + f"COPY --chown=node:node workspace/. {workspace.guest_path}\n" + f"WORKDIR {workspace.workdir}\n" + ) + subprocess.run( + ["docker", "build", "-t", derived, "-f", "-", context_dir], + input=dockerfile, + text=True, + check=True, + ) def image_id(ref: str) -> str: diff --git a/tests/unit/test_docker_util_image.py b/tests/unit/test_docker_util_image.py index a230290..941bebf 100644 --- a/tests/unit/test_docker_util_image.py +++ b/tests/unit/test_docker_util_image.py @@ -85,11 +85,45 @@ class TestBuildImageWithCwd(unittest.TestCase): argv = run.call_args.args[0] dockerfile = run.call_args.kwargs["input"] - self.assertEqual(["docker", "build", "-t", "derived:tag", "-f", "-", tmp], argv) + self.assertEqual(["docker", "build", "-t", "derived:tag", "-f", "-"], argv[:6]) + self.assertTrue(argv[6].endswith("/context")) self.assertIn("FROM base:tag\n", dockerfile) - self.assertIn("COPY --chown=node:node . /guest/home/workspace\n", dockerfile) + self.assertIn( + "COPY --chown=node:node workspace/. /guest/home/workspace\n", + dockerfile, + ) self.assertIn("WORKDIR /guest/home/workspace\n", dockerfile) + def test_staged_context_includes_hidden_files_but_not_git_dir(self): + with tempfile.TemporaryDirectory(prefix="bb-docker-cwd.") as tmp: + root = Path(tmp) + (root / ".gitignore").write_text("*.pyc\n") + (root / ".dockerignore").write_text(".gitignore\n") + (root / ".env.example").write_text("SAFE=1\n") + (root / ".git").mkdir() + (root / ".git" / "config").write_text("[core]\n") + workspace = WorkspacePlan( + enabled=True, + host_path=root, + guest_home="/guest/home", + guest_path="/guest/home/workspace", + workdir="/guest/home/workspace", + ) + + def inspect_context(*args, **kwargs): + context = Path(args[0][-1]) + staged = context / "workspace" + self.assertTrue((staged / ".gitignore").is_file()) + self.assertTrue((staged / ".dockerignore").is_file()) + self.assertTrue((staged / ".env.example").is_file()) + self.assertFalse((staged / ".git").exists()) + return _ok() + + with patch.object( + docker_mod.subprocess, "run", side_effect=inspect_context, + ): + docker_mod.build_image_with_cwd("derived:tag", "base:tag", workspace) + if __name__ == "__main__": unittest.main()