e3376b8809
Remove symbols with no remaining consumer: - `gateway_name` property on Docker/MacosInfraService — nothing uses it; the launch flow reads `gateway().name` off the Gateway service. (+ its docker test.) - firecracker `infra_vm` EGRESS_PORT / SUPERVISE_PORT / GIT_HTTP_PORT — dead duplicates; launch.py uses supervisor.types / docker.egress / a local const. - macOS `INFRA_DB_VOLUME` back-compat alias — unused since the DB-volume test moved to `ORCHESTRATOR_DB_VOLUME`. - docker `infra.py`: the unused `ORCHESTRATOR_SOURCE_HASH_LABEL` import and the `__all__` re-exports of the orchestrator constants (consumers import them from `docker.orchestrator`, not `docker.infra`). - macOS `infra.py`: the unused `GatewayError` import + re-export. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
162 lines
6.4 KiB
Python
162 lines
6.4 KiB
Python
"""The per-host control plane + gateway for the macOS backend (PRD 0070).
|
|
|
|
Two Apple containers — the orchestrator (control plane) and the gateway (data
|
|
plane) — split now that #469 got the DB off the data plane. The single-container
|
|
model existed only because two Apple-Container guests writing one `bot-bottle.db`
|
|
over virtiofs would race incoherent `fcntl` locks; with the data plane no longer
|
|
opening the DB at all, only the orchestrator does, so the split is safe.
|
|
|
|
* `bot-bottle-mac-orchestrator` — the lean control plane (`MacosOrchestrator`).
|
|
Joins the host-only **control network** (`bot-bottle-mac-control`) only. Sole
|
|
opener of the container-only DB volume; holds the signing key. The host CLI
|
|
reaches it at its control-network address; the gateway reaches it there too.
|
|
* `bot-bottle-mac-infra` — the gateway data plane (`MacosGateway`). Triple-homed:
|
|
the NAT egress network (route out), the host-only agent network (agents + CLI
|
|
reach the gateway), and the control network (reach the orchestrator by IP —
|
|
Apple has no container DNS). Holds the mitmproxy CA + the `gateway` JWT.
|
|
|
|
`MacosInfraService` composes the two services and brings them up as an idempotent
|
|
per-host pair. Agents sit on the agent network only, never the control network,
|
|
so they have no route to the control plane (the L3 block, not just the JWT).
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from pathlib import Path
|
|
|
|
from ...orchestrator.lifecycle import (
|
|
DEFAULT_PORT,
|
|
DEFAULT_STARTUP_TIMEOUT_SECONDS,
|
|
OrchestratorStartError,
|
|
)
|
|
from ..infra_service import InfraService
|
|
from . import util as container_mod
|
|
from .gateway import (
|
|
CONTROL_NETWORK,
|
|
DEFAULT_CA_TIMEOUT_SECONDS,
|
|
GATEWAY_EGRESS_NETWORK,
|
|
GATEWAY_IMAGE,
|
|
GATEWAY_NAME,
|
|
GATEWAY_NETWORK,
|
|
MacosGateway,
|
|
ensure_networks,
|
|
)
|
|
from .orchestrator import (
|
|
ORCHESTRATOR_DB_VOLUME,
|
|
ORCHESTRATOR_IMAGE,
|
|
ORCHESTRATOR_NAME,
|
|
MacosOrchestrator,
|
|
probe_orchestrator_url,
|
|
)
|
|
|
|
# `INFRA_NAME` is kept — now aliasing the gateway container — for callers that
|
|
# still import it (probe / reprovision attribute against the gateway).
|
|
INFRA_NAME = GATEWAY_NAME
|
|
|
|
_REPO_ROOT = Path(__file__).resolve().parents[3]
|
|
|
|
|
|
class MacosInfraService(InfraService):
|
|
"""Composes the per-host orchestrator + gateway containers. Callers use
|
|
`ensure_running()` (returns the control-plane URL), the `orchestrator()` /
|
|
`gateway()` accessors, and `ca_cert_pem()`."""
|
|
|
|
def __init__(
|
|
self,
|
|
*,
|
|
port: int = DEFAULT_PORT,
|
|
network: str = GATEWAY_NETWORK,
|
|
egress_network: str = GATEWAY_EGRESS_NETWORK,
|
|
control_network: str = CONTROL_NETWORK,
|
|
gateway_image: str = GATEWAY_IMAGE,
|
|
orchestrator_image: str = ORCHESTRATOR_IMAGE,
|
|
repo_root: Path = _REPO_ROOT,
|
|
orchestrator_name: str = ORCHESTRATOR_NAME,
|
|
gateway_name: str = INFRA_NAME,
|
|
db_volume: str = ORCHESTRATOR_DB_VOLUME,
|
|
) -> None:
|
|
self.port = port
|
|
self.network = network
|
|
self.egress_network = egress_network
|
|
self.control_network = control_network
|
|
self.gateway_image = gateway_image
|
|
self.orchestrator_image = orchestrator_image
|
|
self._repo_root = repo_root
|
|
self._orchestrator_name = orchestrator_name
|
|
self._gateway_name = gateway_name
|
|
self._db_volume = db_volume
|
|
|
|
def orchestrator(self) -> MacosOrchestrator:
|
|
"""The control-plane service on the host-only control network. Cheap to
|
|
reconstruct — the launch flow reads its `url()` / `gateway_url()` /
|
|
`mint_gateway_token()` off it."""
|
|
return MacosOrchestrator(
|
|
self.orchestrator_image,
|
|
name=self._orchestrator_name,
|
|
port=self.port,
|
|
control_network=self.control_network,
|
|
repo_root=self._repo_root,
|
|
db_volume=self._db_volume,
|
|
)
|
|
|
|
def gateway(self) -> MacosGateway:
|
|
"""The data-plane gateway service, triple-homed on the egress + agent +
|
|
control networks. Cheap to reconstruct — the launch flow reads its
|
|
`address()` / CA / provisioning transport off it, and `ensure_running`
|
|
connects it to the control plane."""
|
|
return MacosGateway(
|
|
self.gateway_image,
|
|
name=self._gateway_name,
|
|
network=self.network,
|
|
egress_network=self.egress_network,
|
|
control_network=self.control_network,
|
|
repo_root=self._repo_root,
|
|
)
|
|
|
|
def ensure_running(
|
|
self, *, startup_timeout: float = DEFAULT_STARTUP_TIMEOUT_SECONDS,
|
|
) -> str:
|
|
"""Ensure the orchestrator + gateway containers are up; return the host
|
|
control-plane URL. Idempotent per-host singleton — a healthy orchestrator
|
|
on current source is left untouched. Raises `OrchestratorStartError` on
|
|
control-plane startup timeout."""
|
|
# The networks (host-only agent + NAT egress + host-only control) are a
|
|
# shared concern — create them before either plane comes up.
|
|
ensure_networks(self.network, self.egress_network, self.control_network)
|
|
|
|
orchestrator = self.orchestrator()
|
|
gateway = self.gateway()
|
|
orchestrator.ensure_built()
|
|
gateway.ensure_built()
|
|
|
|
orchestrator.ensure_running(startup_timeout=startup_timeout)
|
|
|
|
# (Re)ensure the gateway once the control plane it resolves against is
|
|
# healthy — it needs the orchestrator's control-network address. The
|
|
# orchestrator (which holds the signing key) mints the role-scoped
|
|
# `gateway` JWT and hands it to the gateway, which never sees the key
|
|
# (#469).
|
|
url = orchestrator.url()
|
|
gateway.connect_to_orchestrator(url, orchestrator.mint_gateway_token())
|
|
return url
|
|
|
|
def ca_cert_pem(self, *, timeout: float = DEFAULT_CA_TIMEOUT_SECONDS) -> str:
|
|
"""The gateway's mitmproxy CA (PEM) agents install to trust its TLS
|
|
interception — delegated to the gateway service (reads it out of the
|
|
gateway container, polling until mitmproxy writes it)."""
|
|
return self.gateway().ca_cert_pem(timeout=timeout)
|
|
|
|
def stop(self) -> None:
|
|
"""Remove both containers (idempotent). The DB volume persists."""
|
|
container_mod.force_remove_container(self._gateway_name)
|
|
container_mod.force_remove_container(self._orchestrator_name)
|
|
|
|
|
|
__all__ = [
|
|
"MacosInfraService",
|
|
"OrchestratorStartError",
|
|
"ORCHESTRATOR_NAME",
|
|
"INFRA_NAME",
|
|
"probe_orchestrator_url",
|
|
]
|