From d123b99347ca657781b11a87f7725304b86f494e Mon Sep 17 00:00:00 2001 From: didericis Date: Wed, 10 Jun 2026 20:12:45 -0400 Subject: [PATCH] fix(macos-container): start builder with dns --- bot_bottle/backend/macos_container/util.py | 49 ++++++++++++++++++++ docs/prds/prd-new-macos-container-backend.md | 5 +- tests/unit/test_macos_container_util.py | 44 ++++++++++++++++-- 3 files changed, 94 insertions(+), 4 deletions(-) diff --git a/bot_bottle/backend/macos_container/util.py b/bot_bottle/backend/macos_container/util.py index 7dddf60..328602c 100644 --- a/bot_bottle/backend/macos_container/util.py +++ b/bot_bottle/backend/macos_container/util.py @@ -45,6 +45,7 @@ def build_image(ref: str, context: str, *, dockerfile: str = "") -> None: f"building image {ref} from {context} with Apple Container " "(layer cache keeps repeat builds fast)" ) + _ensure_builder_dns() args = [_CONTAINER, "build", "-t", ref, "--dns", dns_server()] if dockerfile: args.extend(["-f", dockerfile]) @@ -52,6 +53,54 @@ def build_image(ref: str, context: str, *, dockerfile: str = "") -> None: subprocess.run(args, check=True) +def _ensure_builder_dns() -> None: + dns = dns_server() + if _builder_has_dns(dns): + return + subprocess.run( + [_CONTAINER, "builder", "stop"], + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + check=False, + ) + subprocess.run( + [_CONTAINER, "builder", "start", "--dns", dns], + check=True, + ) + + +def _builder_has_dns(dns: str) -> bool: + result = subprocess.run( + [_CONTAINER, "builder", "status", "--format", "json"], + capture_output=True, + text=True, + check=False, + ) + if result.returncode != 0: + return False + try: + data = json.loads(result.stdout or "[]") + except json.JSONDecodeError: + return False + entries = data if isinstance(data, list) else [data] + for entry in entries: + if not isinstance(entry, dict): + continue + status = entry.get("status") + if isinstance(status, dict) and status.get("state") != "running": + continue + config = entry.get("configuration") + config_dns = config.get("dns") if isinstance(config, dict) else None + nameservers = ( + config_dns.get("nameservers") + if isinstance(config_dns, dict) + else None + ) + if isinstance(nameservers, list) and dns in nameservers: + return True + return False + + def image_exists(ref: str) -> bool: return _silent_run([_CONTAINER, "image", "inspect", ref]) == 0 diff --git a/docs/prds/prd-new-macos-container-backend.md b/docs/prds/prd-new-macos-container-backend.md index de07a92..b4d2f80 100644 --- a/docs/prds/prd-new-macos-container-backend.md +++ b/docs/prds/prd-new-macos-container-backend.md @@ -170,7 +170,10 @@ delivery design lands. - Integration tests run on macOS hosts with Apple Container installed and verify that egress cannot bypass the sidecar. They also preflight Apple Container BuildKit DNS because image builds must resolve - package mirrors before a launch smoke can be meaningful. + package mirrors before a launch smoke can be meaningful. The backend + starts/restarts the Apple Container builder with the configured DNS + server before image builds so BuildKit `RUN` steps inherit a working + resolver. ## References diff --git a/tests/unit/test_macos_container_util.py b/tests/unit/test_macos_container_util.py index fc31bea..ff00538 100644 --- a/tests/unit/test_macos_container_util.py +++ b/tests/unit/test_macos_container_util.py @@ -29,7 +29,16 @@ class TestMacosContainerAvailability(unittest.TestCase): class TestMacosContainerCommands(unittest.TestCase): def test_build_image(self): - with patch.object(util.subprocess, "run") as run, \ + status = util.subprocess.CompletedProcess( + args=[], + returncode=0, + stdout=( + '[{"status":{"state":"running"},' + '"configuration":{"dns":{"nameservers":["9.9.9.9"]}}}]' + ), + stderr="", + ) + with patch.object(util.subprocess, "run", return_value=status) as run, \ patch.object(util.os, "environ", { "BOT_BOTTLE_MACOS_CONTAINER_DNS": "9.9.9.9", }): @@ -39,9 +48,38 @@ class TestMacosContainerCommands(unittest.TestCase): "container", "build", "-t", "bot-bottle-agent:latest", "--dns", "9.9.9.9", "-f", "/repo/Dockerfile", "/repo", ], - run.call_args.args[0], + run.call_args_list[-1].args[0], + ) + self.assertTrue(run.call_args_list[-1].kwargs["check"]) + + def test_build_image_restarts_builder_when_dns_mismatches(self): + status = util.subprocess.CompletedProcess( + args=[], + returncode=0, + stdout=( + '[{"status":{"state":"running"},' + '"configuration":{"dns":{"nameservers":[]}}}]' + ), + stderr="", + ) + with patch.object(util.subprocess, "run", return_value=status) as run, \ + patch.object(util.os, "environ", { + "BOT_BOTTLE_MACOS_CONTAINER_DNS": "9.9.9.9", + }): + util.build_image("bot-bottle-agent:latest", "/repo") + calls = [c.args[0] for c in run.call_args_list] + self.assertIn(["container", "builder", "stop"], calls) + self.assertIn( + ["container", "builder", "start", "--dns", "9.9.9.9"], + calls, + ) + self.assertEqual( + [ + "container", "build", "-t", "bot-bottle-agent:latest", + "--dns", "9.9.9.9", "/repo", + ], + calls[-1], ) - self.assertTrue(run.call_args.kwargs["check"]) def test_container_exists_parses_quiet_list(self): completed = util.subprocess.CompletedProcess(