refactor(firecracker): single infra VM builds too — buildah in one image (PR #354 review)
Addresses the review finding that buildah lived only in the orchestrator
image, so the persistent infra VM wasn't the builder — a separate throwaway
builder VM contended with it for the orchestrator TAP. Consolidate:
- **Rebase the gateway (and thus infra) on `python:3.12-slim` = Debian
trixie**, pip-installing mitmproxy instead of `FROM mitmproxy/mitmproxy`
(Debian bookworm). trixie ships buildah 1.39, which can build agent
Dockerfiles that use heredocs; bookworm's 1.28 can't (`Unknown
instruction: "{"`). CA path is unchanged (set via `--set confdir=`).
- **buildah lives only in `Dockerfile.infra`** now (removed from the
orchestrator image, which is lean/stdlib-only again).
- **Shared orchestrator content**: `Dockerfile.orchestrator` is the single
definition of the control-plane payload; the infra image `COPY --from`s
it (same trixie base → clean copy, and future deps like iroh are added
once). The docker backend runs the orchestrator image directly.
- **`image_builder` builds inside the infra VM** (which now has buildah)
over SSH — no throwaway builder VM, so the `bborch0` contention is gone.
`ensure_built` builds orchestrator + gateway before infra (FROM gateway,
COPY --from orchestrator).
Verified on a KVM host: images build (buildah 1.39 in infra), the agent
image builds *inside* the infra VM (heredoc Dockerfile and all), the infra
VM stays healthy, the agent boots and `claude --version` = 2.1.172. The
rebased gateway still starts as a docker container and generates its CA
(docker backend unaffected).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WBMWTEtQdJ4W5UrWuLHCck
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
"""Unit tests for the docker-free Firecracker agent-image builder.
|
||||
|
||||
The VM boot / SSH / buildah plumbing (`_build_in_vm`) is integration-tested
|
||||
The VM boot / SSH / buildah plumbing (`_build_in_infra`) is integration-tested
|
||||
on a KVM host; here we cover the cache decision, the boot-bit injection, and
|
||||
the smoke-test no-op — the logic that must hold without a VM.
|
||||
"""
|
||||
@@ -29,7 +29,7 @@ class TestBuildAgentRootfsDir(unittest.TestCase):
|
||||
base.mkdir(parents=True)
|
||||
(base / ".bb-ready").write_text("ok\n")
|
||||
with patch.object(image_builder.util, "cache_dir", return_value=self.cache), \
|
||||
patch.object(image_builder, "_build_in_vm") as build:
|
||||
patch.object(image_builder, "_build_in_infra") as build:
|
||||
out = image_builder.build_agent_rootfs_dir(
|
||||
self.dockerfile, image_tag="t:latest")
|
||||
build.assert_not_called()
|
||||
@@ -37,7 +37,7 @@ class TestBuildAgentRootfsDir(unittest.TestCase):
|
||||
|
||||
def test_cache_miss_builds_injects_and_marks_ready(self):
|
||||
with patch.object(image_builder.util, "cache_dir", return_value=self.cache), \
|
||||
patch.object(image_builder, "_build_in_vm") as build, \
|
||||
patch.object(image_builder, "_build_in_infra") as build, \
|
||||
patch.object(image_builder.util, "inject_guest_boot") as inject:
|
||||
out = image_builder.build_agent_rootfs_dir(
|
||||
self.dockerfile, image_tag="t:latest", smoke_test=("claude", "--version"))
|
||||
@@ -59,7 +59,7 @@ class TestBuildAgentRootfsDir(unittest.TestCase):
|
||||
class TestSmokeTest(unittest.TestCase):
|
||||
def test_empty_argv_is_noop(self):
|
||||
with patch.object(image_builder, "_ssh") as ssh:
|
||||
image_builder._smoke_test(Path("/k"), "10.0.0.1", ())
|
||||
image_builder._smoke_test(Path("/k"), "10.0.0.1", "tag", ())
|
||||
ssh.assert_not_called()
|
||||
|
||||
def test_failed_smoke_dies(self):
|
||||
@@ -67,7 +67,7 @@ class TestSmokeTest(unittest.TestCase):
|
||||
result = subprocess.CompletedProcess([], 1, stdout="broken", stderr="")
|
||||
with patch.object(image_builder, "_ssh", return_value=result), \
|
||||
self.assertRaises(SystemExit):
|
||||
image_builder._smoke_test(Path("/k"), "10.0.0.1", ("claude", "--version"))
|
||||
image_builder._smoke_test(Path("/k"), "10.0.0.1", "tag", ("claude", "--version"))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -95,11 +95,14 @@ class TestRegistryVolume(unittest.TestCase):
|
||||
|
||||
|
||||
class TestEnsureBuilt(unittest.TestCase):
|
||||
def test_builds_gateway_then_infra(self):
|
||||
def test_builds_deps_before_infra(self):
|
||||
with patch.object(infra_vm.docker_mod, "build_image") as build:
|
||||
infra_vm.ensure_built()
|
||||
tags = [c.args[0] for c in build.call_args_list]
|
||||
self.assertEqual([infra_vm._GATEWAY_IMAGE, infra_vm._INFRA_IMAGE], tags)
|
||||
# infra is FROM gateway and COPY --from orchestrator, so both first.
|
||||
self.assertEqual(infra_vm._INFRA_IMAGE, tags[-1])
|
||||
self.assertIn(infra_vm._ORCHESTRATOR_IMAGE, tags[:-1])
|
||||
self.assertIn(infra_vm._GATEWAY_IMAGE, tags[:-1])
|
||||
|
||||
|
||||
class TestWaitForHealth(unittest.TestCase):
|
||||
|
||||
Reference in New Issue
Block a user