refactor(orchestrator): rename Sidecar -> Gateway for the consolidated data plane

Retire "sidecar" for the consolidated per-host path (PRD 0070 naming
decision): the orchestrator is the umbrella/control plane, and the
egress/git/supervise data-plane unit it runs is the "gateway".

- git mv sidecar.py -> gateway.py and the two integration + one unit test
  files; DockerSidecar->DockerGateway, Sidecar->Gateway,
  SidecarError->GatewayError, SIDECAR_*->GATEWAY_*, ensure_sidecar->
  ensure_gateway, sidecar_status->gateway_status, container name
  bot-bottle-orch-sidecar->bot-bottle-orch-gateway.
- Prose rename across broker/registry/egress/policy_resolver + PRD 0070.
- Preserved: the image name bot-bottle-sidecars, the
  BOT_BOTTLE_SIDECAR_IMAGE env var, Dockerfile.sidecars, and PRD 0069's
  own stage-name cross-references (that doc still uses "sidecar").

No behavior change. Full unit suite green (1679 tests; the 13
test_sidecar_init /bin/sleep errors are pre-existing NixOS-local noise).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WBMWTEtQdJ4W5UrWuLHCck
This commit is contained in:
2026-07-13 18:12:17 -04:00
parent 2cbb178f88
commit 00418a2834
17 changed files with 151 additions and 151 deletions
@@ -1,7 +1,7 @@
"""Integration: the consolidated Docker sidecar is an idempotent singleton.
"""Integration: the consolidated Docker gateway is an idempotent singleton.
Gated on a reachable Docker daemon. Uses a unique name so it never
collides with a real per-host sidecar or a leftover from another run.
collides with a real per-host gateway or a leftover from another run.
"""
from __future__ import annotations
@@ -10,17 +10,17 @@ import secrets
import subprocess
import unittest
from bot_bottle.orchestrator.sidecar import DockerSidecar
from bot_bottle.orchestrator.gateway import DockerGateway
from tests._docker import skip_unless_docker
IMAGE = "busybox"
@skip_unless_docker()
class TestDockerSidecarIntegration(unittest.TestCase):
class TestDockerGatewayIntegration(unittest.TestCase):
def setUp(self) -> None:
self.name = "bot-bottle-orch-sidecar-itest-" + secrets.token_hex(4)
self.sc = DockerSidecar(IMAGE, name=self.name)
self.name = "bot-bottle-orch-gateway-itest-" + secrets.token_hex(4)
self.sc = DockerGateway(IMAGE, name=self.name)
self.addCleanup(self.sc.stop)
def _count(self) -> int:
@@ -1,4 +1,4 @@
"""Integration: DockerSidecar.image_exists reflects real docker state.
"""Integration: DockerGateway.image_exists reflects real docker state.
Gated on a reachable Docker daemon. Deliberately does NOT build the full
sidecar bundle (a heavy, slow image build) it exercises the `image_exists`
@@ -11,14 +11,14 @@ from __future__ import annotations
import subprocess
import unittest
from bot_bottle.orchestrator.sidecar import DockerSidecar
from bot_bottle.orchestrator.gateway import DockerGateway
from tests._docker import skip_unless_docker
IMAGE = "busybox"
@skip_unless_docker()
class TestDockerSidecarImageExists(unittest.TestCase):
class TestDockerGatewayImageExists(unittest.TestCase):
def test_image_exists_true_for_present_false_for_absent(self) -> None:
# Ensure the tiny image is present (build_if_missing is disabled here
# so this never triggers a Dockerfile.sidecars build).
@@ -26,9 +26,9 @@ class TestDockerSidecarImageExists(unittest.TestCase):
["docker", "pull", IMAGE],
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, check=False,
)
self.assertTrue(DockerSidecar(IMAGE, dockerfile=None).image_exists())
self.assertTrue(DockerGateway(IMAGE, dockerfile=None).image_exists())
self.assertFalse(
DockerSidecar("bot-bottle-nonexistent:doesnotexist", dockerfile=None).image_exists()
DockerGateway("bot-bottle-nonexistent:doesnotexist", dockerfile=None).image_exists()
)
@@ -118,8 +118,8 @@ class TestDispatch(unittest.TestCase):
status, _ = dispatch(self.orch, "GET", "/health/", b"")
self.assertEqual(200, status)
def test_sidecar_status_unconfigured(self) -> None:
status, payload = dispatch(self.orch, "GET", "/sidecar", b"")
def test_gateway_status_unconfigured(self) -> None:
status, payload = dispatch(self.orch, "GET", "/gateway", b"")
self.assertEqual(200, status)
self.assertEqual(False, payload["configured"])
@@ -1,29 +1,29 @@
"""Unit tests for the consolidated Docker sidecar (PRD 0070). Docker mocked."""
"""Unit tests for the consolidated Docker gateway (PRD 0070). Docker mocked."""
from __future__ import annotations
import unittest
from unittest.mock import Mock, patch
from bot_bottle.orchestrator.sidecar import (
SIDECAR_NAME,
DockerSidecar,
SidecarError,
from bot_bottle.orchestrator.gateway import (
GATEWAY_NAME,
DockerGateway,
GatewayError,
)
_RUN_DOCKER = "bot_bottle.orchestrator.sidecar.run_docker"
_RUN_DOCKER = "bot_bottle.orchestrator.gateway.run_docker"
def _proc(returncode: int = 0, stdout: str = "", stderr: str = "") -> Mock:
return Mock(returncode=returncode, stdout=stdout, stderr=stderr)
class TestDockerSidecar(unittest.TestCase):
class TestDockerGateway(unittest.TestCase):
def setUp(self) -> None:
self.sc = DockerSidecar("bot-bottle-sidecars:latest")
self.sc = DockerGateway("bot-bottle-sidecars:latest")
def test_default_name(self) -> None:
self.assertEqual(SIDECAR_NAME, self.sc.name)
self.assertEqual(GATEWAY_NAME, self.sc.name)
def test_is_running_reads_docker_ps(self) -> None:
with patch(_RUN_DOCKER, return_value=_proc(stdout=self.sc.name + "\n")):
@@ -61,7 +61,7 @@ class TestDockerSidecar(unittest.TestCase):
return _proc()
with patch(_RUN_DOCKER, side_effect=fake):
with self.assertRaises(SidecarError):
with self.assertRaises(GatewayError):
self.sc.ensure_running()
def test_stop_is_idempotent_on_missing(self) -> None:
@@ -71,13 +71,13 @@ class TestDockerSidecar(unittest.TestCase):
def test_stop_raises_on_other_failure(self) -> None:
with patch(_RUN_DOCKER, return_value=_proc(returncode=1, stderr="daemon down")):
with self.assertRaises(SidecarError):
with self.assertRaises(GatewayError):
self.sc.stop()
class TestDockerSidecarBuild(unittest.TestCase):
class TestDockerGatewayBuild(unittest.TestCase):
def setUp(self) -> None:
self.sc = DockerSidecar() # defaults to the real bundle image + dockerfile
self.sc = DockerGateway() # 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)):
@@ -109,7 +109,7 @@ class TestDockerSidecarBuild(unittest.TestCase):
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)
sc = DockerGateway("busybox", dockerfile=None)
with patch(_RUN_DOCKER) as m:
sc.ensure_built()
m.assert_not_called()
@@ -121,7 +121,7 @@ class TestDockerSidecarBuild(unittest.TestCase):
return _proc(returncode=1, stderr="build boom")
with patch(_RUN_DOCKER, side_effect=fake):
with self.assertRaises(SidecarError):
with self.assertRaises(GatewayError):
self.sc.ensure_built()
+16 -16
View File
@@ -10,7 +10,7 @@ from pathlib import Path
from bot_bottle.orchestrator.broker import LaunchBroker, LaunchRequest, StubBroker
from bot_bottle.orchestrator.registry import RegistryStore
from bot_bottle.orchestrator.service import Orchestrator
from bot_bottle.orchestrator.sidecar import Sidecar
from bot_bottle.orchestrator.gateway import Gateway
class _FailingBroker(LaunchBroker):
@@ -24,11 +24,11 @@ class _FailingBroker(LaunchBroker):
pass
class _FakeSidecar(Sidecar):
"""In-memory sidecar for wiring tests."""
class _FakeGateway(Gateway):
"""In-memory gateway for wiring tests."""
def __init__(self) -> None:
self.name = "fake-sidecar"
self.name = "fake-gateway"
self.ensured = 0
self.built = 0
self._running = False
@@ -120,23 +120,23 @@ class TestOrchestrator(unittest.TestCase):
orch.launch_bottle("10.243.0.9")
self.assertEqual([], self.store.all()) # no orphan
def test_sidecar_unconfigured_by_default(self) -> None:
self.assertEqual({"configured": False}, self.orch.sidecar_status())
self.orch.ensure_sidecar() # no-op, must not raise
def test_gateway_unconfigured_by_default(self) -> None:
self.assertEqual({"configured": False}, self.orch.gateway_status())
self.orch.ensure_gateway() # no-op, must not raise
def test_sidecar_wired_and_ensured(self) -> None:
sc = _FakeSidecar()
orch = Orchestrator(self.store, self.broker, self.secret, sidecar=sc)
def test_gateway_wired_and_ensured(self) -> None:
sc = _FakeGateway()
orch = Orchestrator(self.store, self.broker, self.secret, gateway=sc)
self.assertEqual(
{"configured": True, "name": "fake-sidecar", "running": False},
orch.sidecar_status(),
{"configured": True, "name": "fake-gateway", "running": False},
orch.gateway_status(),
)
orch.ensure_sidecar()
self.assertEqual(1, sc.built) # ensure_sidecar builds first,
orch.ensure_gateway()
self.assertEqual(1, sc.built) # ensure_gateway builds first,
self.assertEqual(1, sc.ensured) # then runs
self.assertEqual(
{"configured": True, "name": "fake-sidecar", "running": True},
orch.sidecar_status(),
{"configured": True, "name": "fake-gateway", "running": True},
orch.gateway_status(),
)
+1 -1
View File
@@ -1,4 +1,4 @@
"""Unit tests for the sidecar-side PolicyResolver (PRD 0070). HTTP mocked."""
"""Unit tests for the gateway-side PolicyResolver (PRD 0070). HTTP mocked."""
from __future__ import annotations