05df21f210
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>
52 lines
1.7 KiB
Python
52 lines
1.7 KiB
Python
"""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 gateway or a leftover from another run.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import secrets
|
|
import subprocess
|
|
import unittest
|
|
|
|
from bot_bottle.backend.docker.gateway import DockerGateway
|
|
from tests._backend import skip_unless_backend
|
|
|
|
IMAGE = "busybox"
|
|
|
|
|
|
@skip_unless_backend("docker")
|
|
class TestDockerGatewayIntegration(unittest.TestCase):
|
|
def setUp(self) -> None:
|
|
self.name = "bot-bottle-orch-gateway-itest-" + secrets.token_hex(4)
|
|
self.sc = DockerGateway(IMAGE, name=self.name)
|
|
self.addCleanup(self.sc.stop)
|
|
|
|
def _connect(self) -> None:
|
|
# Resolver-only data plane (PRD 0070) requires an orchestrator URL + a
|
|
# pre-minted `gateway` token to run; busybox never dials the orchestrator
|
|
# or presents the token, so placeholders are enough here.
|
|
self.sc.connect_to_orchestrator("http://orchestrator:9000", "placeholder-token")
|
|
|
|
def _count(self) -> int:
|
|
proc = subprocess.run(
|
|
["docker", "ps", "-a", "--filter", f"name=^{self.name}$",
|
|
"--format", "{{.Names}}"],
|
|
stdout=subprocess.PIPE, text=True, check=False,
|
|
)
|
|
return sum(1 for n in proc.stdout.split() if n == self.name)
|
|
|
|
def test_ensure_is_idempotent_singleton(self) -> None:
|
|
self._connect()
|
|
self.assertEqual(1, self._count())
|
|
# A second connect must not spawn a second container — one per host.
|
|
self._connect()
|
|
self.assertEqual(1, self._count())
|
|
self.sc.stop()
|
|
self.assertEqual(0, self._count())
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|