c4ccd74f9b
Extract the Apple-Container gateway data plane out of `MacosInfraService` into `MacosGateway`, the macOS implementation of the shared `Gateway` service. `connect_to_orchestrator(url, gateway_token)` runs the triple-homed gateway container (egress + agent + control networks) carrying the mitmproxy CA and the pre-minted `gateway` token; `address()` / `ca_cert_pem()` / `provisioning_transport()` round out the contract. The infra service now composes the gateway: `ensure_running` mints the role-scoped `gateway` JWT (it holds the signing key; the gateway never does — #469) and hands it to `gateway().connect_to_orchestrator(...)`, and `ca_cert_pem` delegates to the service. The inlined `_ensure_gateway_container` + CA-read are gone. `AppleGatewayTransport` + the gateway container name/label now live with the gateway module, breaking the old infra→provision coupling. Splits the gateway-run + CA tests out of test_macos_infra into a dedicated test_macos_gateway; the infra tests mock `svc.gateway`. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
148 lines
6.1 KiB
Python
148 lines
6.1 KiB
Python
"""Unit: the macOS gateway data plane as an Apple container (PRD 0070)."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import unittest
|
|
from pathlib import Path
|
|
from unittest.mock import Mock, patch
|
|
|
|
from bot_bottle.backend.macos_container.gateway import (
|
|
GATEWAY_NAME,
|
|
GatewayError,
|
|
MacosGateway,
|
|
)
|
|
|
|
_GW = "bot_bottle.backend.macos_container.gateway"
|
|
|
|
_ORCH_URL = "http://192.168.128.2:8099"
|
|
_TOKEN = "gw-jwt-token"
|
|
_CA_PEM = "-----BEGIN CERTIFICATE-----\nMII...\n-----END CERTIFICATE-----\n"
|
|
|
|
|
|
def _proc(returncode: int = 0, stdout: str = "", stderr: str = "") -> Mock:
|
|
return Mock(returncode=returncode, stdout=stdout, stderr=stderr)
|
|
|
|
|
|
def _spec(src: str, tgt: str, readonly: bool = False) -> str:
|
|
return f"type=bind,source={src},target={tgt}" + (",readonly" if readonly else "")
|
|
|
|
|
|
class TestMacosGatewayConnect(unittest.TestCase):
|
|
def setUp(self) -> None:
|
|
self.gw = MacosGateway("bot-bottle-gateway:latest")
|
|
|
|
def _connect(self, url: str = _ORCH_URL, token: str = _TOKEN) -> Mock:
|
|
run = Mock(return_value=_proc())
|
|
with patch(f"{_GW}.container_mod") as mod, \
|
|
patch(f"{_GW}.host_gateway_ca_dir", return_value=Path("/ca")):
|
|
mod.dns_server.return_value = "1.1.1.1"
|
|
mod.bind_mount_spec.side_effect = _spec
|
|
mod.run_container_argv = run
|
|
self.gw.connect_to_orchestrator(url, token)
|
|
return run
|
|
|
|
def test_default_name(self) -> None:
|
|
self.assertEqual(GATEWAY_NAME, self.gw.name)
|
|
|
|
def test_refuses_without_orchestrator_url(self) -> None:
|
|
with patch(f"{_GW}.container_mod") as mod:
|
|
with self.assertRaises(GatewayError):
|
|
self.gw.connect_to_orchestrator("", _TOKEN)
|
|
mod.run_container_argv.assert_not_called()
|
|
|
|
def test_refuses_without_gateway_token(self) -> None:
|
|
with patch(f"{_GW}.container_mod") as mod:
|
|
with self.assertRaises(GatewayError):
|
|
self.gw.connect_to_orchestrator(_ORCH_URL, "")
|
|
mod.run_container_argv.assert_not_called()
|
|
|
|
def test_triple_homed(self) -> None:
|
|
argv = self._connect().call_args.args[0]
|
|
nets = [argv[i + 1] for i, a in enumerate(argv) if a == "--network"]
|
|
self.assertEqual(
|
|
["bot-bottle-mac-egress", "bot-bottle-mac-gateway", "bot-bottle-mac-control"],
|
|
nets,
|
|
)
|
|
|
|
def test_resolves_orchestrator_and_holds_ca(self) -> None:
|
|
argv = self._connect().call_args.args[0]
|
|
self.assertIn(f"BOT_BOTTLE_ORCHESTRATOR_URL={_ORCH_URL}", argv)
|
|
self.assertIn("BOT_BOTTLE_GATEWAY_DAEMONS=egress,git-http,supervise", argv)
|
|
self.assertIn("bot-bottle-gateway:latest", argv)
|
|
mounts = [argv[i + 1] for i, a in enumerate(argv) if a == "--mount"]
|
|
self.assertTrue([m for m in mounts if "target=/home/mitmproxy/.mitmproxy" in m])
|
|
|
|
def test_injects_the_pre_minted_gateway_token(self) -> None:
|
|
# The gateway presents the token it was handed — it never mints (holds no
|
|
# signing key). The value rides the env, not argv.
|
|
from bot_bottle.paths import ORCHESTRATOR_AUTH_JWT_ENV
|
|
|
|
run = self._connect()
|
|
env = run.call_args.kwargs["env"]
|
|
self.assertEqual(_TOKEN, env[ORCHESTRATOR_AUTH_JWT_ENV])
|
|
|
|
def test_raises_on_start_failure(self) -> None:
|
|
with patch(f"{_GW}.container_mod") as mod, \
|
|
patch(f"{_GW}.host_gateway_ca_dir", return_value=Path("/ca")):
|
|
mod.dns_server.return_value = "1.1.1.1"
|
|
mod.bind_mount_spec.side_effect = _spec
|
|
mod.run_container_argv.return_value = _proc(returncode=1, stderr="boom")
|
|
with self.assertRaises(GatewayError):
|
|
self.gw.connect_to_orchestrator(_ORCH_URL, _TOKEN)
|
|
|
|
|
|
class TestMacosGatewayLifecycle(unittest.TestCase):
|
|
def setUp(self) -> None:
|
|
self.gw = MacosGateway("bot-bottle-gateway:latest")
|
|
|
|
def test_is_running_reads_container_state(self) -> None:
|
|
with patch(f"{_GW}.container_mod") as mod:
|
|
mod.container_is_running.return_value = True
|
|
self.assertTrue(self.gw.is_running())
|
|
with patch(f"{_GW}.container_mod") as mod:
|
|
mod.container_is_running.return_value = False
|
|
self.assertFalse(self.gw.is_running())
|
|
|
|
def test_stop_removes_the_container(self) -> None:
|
|
with patch(f"{_GW}.container_mod") as mod:
|
|
self.gw.stop()
|
|
mod.force_remove_container.assert_called_once_with(self.gw.name)
|
|
|
|
def test_address_reads_agent_network_ip(self) -> None:
|
|
with patch(f"{_GW}.container_mod") as mod:
|
|
mod.try_container_ipv4_on_network.return_value = "192.168.128.3"
|
|
self.assertEqual("192.168.128.3", self.gw.address())
|
|
mod.try_container_ipv4_on_network.assert_called_once_with(
|
|
self.gw.name, self.gw.network)
|
|
|
|
def test_address_raises_when_unassigned(self) -> None:
|
|
with patch(f"{_GW}.container_mod") as mod:
|
|
mod.try_container_ipv4_on_network.return_value = ""
|
|
with self.assertRaises(GatewayError):
|
|
self.gw.address()
|
|
|
|
def test_ca_cert_pem_reads_from_the_gateway_container(self) -> None:
|
|
with patch(f"{_GW}.container_mod") as mod:
|
|
mod.run_container_argv.return_value = _proc(stdout=_CA_PEM)
|
|
self.assertEqual(_CA_PEM, self.gw.ca_cert_pem())
|
|
argv = mod.run_container_argv.call_args.args[0]
|
|
self.assertEqual(["container", "exec", self.gw.name, "cat"], argv[:4])
|
|
|
|
def test_ca_cert_pem_raises_when_absent(self) -> None:
|
|
with patch(f"{_GW}.container_mod") as mod:
|
|
mod.run_container_argv.return_value = _proc(returncode=1, stderr="no file")
|
|
with self.assertRaises(GatewayError):
|
|
self.gw.ca_cert_pem(timeout=0)
|
|
|
|
def test_provisioning_transport_targets_the_gateway_container(self) -> None:
|
|
from bot_bottle.backend.macos_container.gateway_provision import (
|
|
AppleGatewayTransport,
|
|
)
|
|
transport = self.gw.provisioning_transport()
|
|
assert isinstance(transport, AppleGatewayTransport)
|
|
self.assertEqual(self.gw.name, transport.gateway)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|