refactor(gateway): introduce the Gateway service ABC (docker impl)

Replace the docker backend's ad-hoc gateway wiring with the shared `Gateway`
service ABC (in the gateway package), the first of the two per-host service
classes that supersede the per-backend infra glue.

The contract is backend-neutral: `connect_to_orchestrator(url, gateway_token)`
binds the gateway to its control plane and brings it up, `address()` reports
the agent-facing proxy target, `ca_cert_pem()` vends the mitmproxy CA, and
`provisioning_transport()` hands back the exec/cp seam git-gate provisioning
stages per-bottle repos + deploy keys through. `GatewayProvisionError` +
`GatewayTransport` move to the ABC so every backend shares them.

Key trust-model change: the gateway no longer mints its own token. It never
holds the signing key (#469), so the orchestrator mints the role-scoped
`gateway` JWT and hands it in via `connect_to_orchestrator`; the gateway only
injects it. `DockerGateway.ensure_running` becomes `connect_to_orchestrator`
(stashing the URL + token as instance state), and the docker launch flow reads
`address()` / `provisioning_transport()` off the service rather than
re-deriving the container IP + transport itself.

macOS + firecracker gateways move under the ABC in following commits.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-25 12:58:00 -04:00
parent 18d9b81add
commit 05df21f210
9 changed files with 205 additions and 97 deletions
+5 -5
View File
@@ -2,7 +2,7 @@
`DockerInfraService` brings up two containers — the lean orchestrator on the
`--internal` control network + host loopback, and the dual-homed gateway. These
tests isolate the orchestrator-container logic by mocking `_gateway` (the
tests isolate the orchestrator-container logic by mocking `gateway` (the
gateway runs via `DockerGateway`, exercised in its own test)."""
from __future__ import annotations
@@ -52,7 +52,7 @@ class TestDockerInfraService(unittest.TestCase):
# Isolate the orchestrator-container logic: the gateway is brought up by
# DockerGateway (its own module/run_docker), tested separately.
self.gw = MagicMock()
patcher = patch.object(self.svc, "_gateway", return_value=self.gw)
patcher = patch.object(self.svc, "gateway", return_value=self.gw)
patcher.start()
self.addCleanup(patcher.stop)
@@ -86,7 +86,7 @@ class TestDockerInfraService(unittest.TestCase):
self.assertEqual(self.svc.url, self.svc.ensure_running())
runs = [c.args[0] for c in run.call_args_list if c.args[0][:2] == ["docker", "run"]]
self.assertEqual([], runs)
self.gw.ensure_running.assert_called_once_with()
self.gw.connect_to_orchestrator.assert_called_once()
def test_recreates_orchestrator_when_source_changed(self) -> None:
def fake(argv: list[str], **_kw: object) -> Mock:
@@ -129,7 +129,7 @@ class TestDockerInfraService(unittest.TestCase):
self.assertIn("--broker", argv)
self.assertIn("stub", argv)
# Gateway is brought up after the control plane is healthy.
self.gw.ensure_running.assert_called_once_with()
self.gw.connect_to_orchestrator.assert_called_once()
def test_builds_gateway_and_orchestrator_images(self) -> None:
def fake(argv: list[str], **_kw: object) -> Mock:
@@ -154,7 +154,7 @@ class TestDockerInfraService(unittest.TestCase):
return _proc()
svc = DockerInfraService(port=20001)
with patch.object(svc, "_gateway", return_value=MagicMock()), \
with patch.object(svc, "gateway", return_value=MagicMock()), \
patch(_URLOPEN, side_effect=[urllib.error.URLError("down"), _health(200)]), \
patch(_RUN, side_effect=fake) as run, patch(_SLEEP):
svc.ensure_running()