feat(orchestrator): slice 5 — build the consolidated sidecar bundle image (#352)
Answers "where do we build the consolidated sidecar": nowhere, until now.
* sidecar.py — `Sidecar.ensure_built()` (default no-op) + `DockerSidecar`
now defaults its image to the real bundle (`bot-bottle-sidecars`) and
`ensure_built()` builds it from `Dockerfile.sidecars` when
`docker image inspect` shows it's missing (no-op when present or when no
dockerfile is configured, e.g. a pre-pulled image). `image_exists()`
added.
* service.py — `ensure_sidecar()` now builds then runs.
* __main__.py — `--sidecar` runs the consolidated bundle (build-if-missing).
Scope note: this builds + launches the bundle *container*; making the
running instance functional across bottles needs the per-bottle,
source-IP-keyed multi-tenant config + registration/reload, and routing
agent bottles to it — the next slices (added to PRD 0070's roadmap).
Tests: unit (docker mocked) — image_exists, ensure_built builds when
missing / no-op when present / no-op without a dockerfile / raises on build
failure; ensure_sidecar builds-then-runs; integration (gated, no heavy
build) — image_exists reflects real docker state. Full suite green (only
pre-existing /bin/sleep errors).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WBMWTEtQdJ4W5UrWuLHCck
This commit is contained in:
@@ -30,8 +30,12 @@ class _FakeSidecar(Sidecar):
|
||||
def __init__(self) -> None:
|
||||
self.name = "fake-sidecar"
|
||||
self.ensured = 0
|
||||
self.built = 0
|
||||
self._running = False
|
||||
|
||||
def ensure_built(self) -> None:
|
||||
self.built += 1
|
||||
|
||||
def ensure_running(self) -> None:
|
||||
self.ensured += 1
|
||||
self._running = True
|
||||
@@ -100,7 +104,8 @@ class TestOrchestrator(unittest.TestCase):
|
||||
orch.sidecar_status(),
|
||||
)
|
||||
orch.ensure_sidecar()
|
||||
self.assertEqual(1, sc.ensured)
|
||||
self.assertEqual(1, sc.built) # ensure_sidecar builds first,
|
||||
self.assertEqual(1, sc.ensured) # then runs
|
||||
self.assertEqual(
|
||||
{"configured": True, "name": "fake-sidecar", "running": True},
|
||||
orch.sidecar_status(),
|
||||
|
||||
@@ -75,5 +75,55 @@ class TestDockerSidecar(unittest.TestCase):
|
||||
self.sc.stop()
|
||||
|
||||
|
||||
class TestDockerSidecarBuild(unittest.TestCase):
|
||||
def setUp(self) -> None:
|
||||
self.sc = DockerSidecar() # defaults to the real bundle image + dockerfile
|
||||
|
||||
def test_image_exists_reads_docker_inspect(self) -> None:
|
||||
with patch(_RUN_DOCKER, return_value=_proc(returncode=0)):
|
||||
self.assertTrue(self.sc.image_exists())
|
||||
with patch(_RUN_DOCKER, return_value=_proc(returncode=1)):
|
||||
self.assertFalse(self.sc.image_exists())
|
||||
|
||||
def test_ensure_built_is_noop_when_image_present(self) -> None:
|
||||
with patch(_RUN_DOCKER, return_value=_proc(returncode=0)) as m:
|
||||
self.sc.ensure_built()
|
||||
self.assertEqual(1, m.call_count) # only the image-inspect probe
|
||||
|
||||
def test_ensure_built_builds_from_dockerfile_when_missing(self) -> None:
|
||||
calls: list[list[str]] = []
|
||||
|
||||
def fake(argv: list[str]) -> Mock:
|
||||
calls.append(argv)
|
||||
if argv[:3] == ["docker", "image", "inspect"]:
|
||||
return _proc(returncode=1) # missing
|
||||
return _proc()
|
||||
|
||||
with patch(_RUN_DOCKER, side_effect=fake):
|
||||
self.sc.ensure_built()
|
||||
|
||||
builds = [c for c in calls if c[:2] == ["docker", "build"]]
|
||||
self.assertEqual(1, len(builds))
|
||||
self.assertIn(self.sc.image_ref, builds[0])
|
||||
self.assertIn("-f", builds[0])
|
||||
self.assertTrue(any(a.endswith("Dockerfile.sidecars") for a in builds[0]))
|
||||
|
||||
def test_ensure_built_noop_when_no_dockerfile(self) -> None:
|
||||
sc = DockerSidecar("busybox", dockerfile=None)
|
||||
with patch(_RUN_DOCKER) as m:
|
||||
sc.ensure_built()
|
||||
m.assert_not_called()
|
||||
|
||||
def test_ensure_built_raises_on_build_failure(self) -> None:
|
||||
def fake(argv: list[str]) -> Mock:
|
||||
if argv[:3] == ["docker", "image", "inspect"]:
|
||||
return _proc(returncode=1)
|
||||
return _proc(returncode=1, stderr="build boom")
|
||||
|
||||
with patch(_RUN_DOCKER, side_effect=fake):
|
||||
with self.assertRaises(SidecarError):
|
||||
self.sc.ensure_built()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
Reference in New Issue
Block a user