feat(firecracker): implement consolidated orchestrator launch (PRD 0070)
Replace the per-bottle Docker sidecar bundle with the shared per-host orchestrator + gateway, mirroring what the Docker backend already has. - Add `bot_bottle/backend/firecracker/consolidated_launch.py`: `_FirecrackerOrchestratorService` (subclasses `OrchestratorService`, overrides `_gateway()` to return a `DockerGateway` with host port bindings so Firecracker VMs can reach it via their TAP link); `launch_consolidated()` registers the bottle by guest IP (attribution key), provisions git-gate into the shared gateway, and returns the shared CA + orchestrator URL for teardown; `teardown_consolidated()` deregisters and cleans up. - Rewrite `bot_bottle/backend/firecracker/launch.py`: removes the per-bottle sidecar bundle (`_start_sidecar_bundle`, `_stage_git_gate`, etc.) and `_mint_certs`; wires `launch_consolidated()` instead. The VM still sends to `host_tap_ip:PORT` — Docker's PREROUTING DNAT + the nft `ct status dnat accept` rule in the forward chain route the traffic to the shared gateway container. - Extend `DockerGateway` with `host_port_bindings` so the Firecracker gateway publishes its ports on the host (`0.0.0.0:PORT`). - Parameterise `OrchestratorService` with `orchestrator_name` / `orchestrator_label` so Docker and Firecracker orchestrators can coexist on the same host (`bot-bottle-orchestrator` vs `bot-bottle-fc-orchestrator`). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -100,6 +100,7 @@ class DockerGateway(Gateway):
|
||||
orchestrator_url: str = "",
|
||||
build_context: Path | None = None,
|
||||
dockerfile: str | None = GATEWAY_DOCKERFILE,
|
||||
host_port_bindings: tuple[int, ...] = (),
|
||||
) -> None:
|
||||
self.image_ref = image_ref
|
||||
self.name = name
|
||||
@@ -110,6 +111,10 @@ class DockerGateway(Gateway):
|
||||
self._orchestrator_url = orchestrator_url
|
||||
self._build_context = build_context or _REPO_ROOT
|
||||
self._dockerfile = dockerfile
|
||||
# Ports published on the host (0.0.0.0). Used by the Firecracker
|
||||
# backend's dev-harness gateway so VMs can reach it via their TAP link;
|
||||
# Docker's DNAT + the nft `ct status dnat accept` rule handle the rest.
|
||||
self._host_port_bindings = host_port_bindings
|
||||
|
||||
def image_exists(self) -> bool:
|
||||
return run_docker(["docker", "image", "inspect", self.image_ref]).returncode == 0
|
||||
@@ -188,6 +193,8 @@ class DockerGateway(Gateway):
|
||||
# trust it) — see GATEWAY_CA_VOLUME.
|
||||
"--volume", f"{GATEWAY_CA_VOLUME}:{MITMPROXY_HOME}",
|
||||
]
|
||||
for port in self._host_port_bindings:
|
||||
argv += ["--publish", f"0.0.0.0:{port}:{port}"]
|
||||
if self._orchestrator_url:
|
||||
# Makes the gateway's egress / git / supervise daemons multi-tenant:
|
||||
# each request resolves source-IP -> policy against the control plane.
|
||||
|
||||
@@ -48,7 +48,13 @@ class OrchestratorStartError(RuntimeError):
|
||||
|
||||
class OrchestratorService:
|
||||
"""Manages the orchestrator control-plane container + the shared gateway.
|
||||
Callers only need `ensure_running()` + `url`."""
|
||||
Callers only need `ensure_running()` + `url`.
|
||||
|
||||
`orchestrator_name` / `orchestrator_label` let backends run independent
|
||||
orchestrators on the same host without name collisions (e.g. the
|
||||
Firecracker backend uses `bot-bottle-fc-orchestrator` alongside the Docker
|
||||
backend's `bot-bottle-orchestrator`). Subclass and override `_gateway()`
|
||||
to supply a backend-specific gateway variant."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@@ -58,12 +64,16 @@ class OrchestratorService:
|
||||
image: str = GATEWAY_IMAGE,
|
||||
repo_root: Path = _REPO_ROOT,
|
||||
host_root: Path | None = None,
|
||||
orchestrator_name: str = ORCHESTRATOR_NAME,
|
||||
orchestrator_label: str = ORCHESTRATOR_LABEL,
|
||||
) -> None:
|
||||
self.port = port
|
||||
self.network = network
|
||||
self.image = image
|
||||
self._repo_root = repo_root
|
||||
self._host_root = host_root or bot_bottle_root()
|
||||
self._orchestrator_name = orchestrator_name
|
||||
self._orchestrator_label = orchestrator_label
|
||||
|
||||
@property
|
||||
def url(self) -> str:
|
||||
@@ -75,7 +85,7 @@ class OrchestratorService:
|
||||
"""Control-plane URL as the gateway container reaches it — by name over
|
||||
docker DNS on the shared network. This is the gateway's
|
||||
BOT_BOTTLE_ORCHESTRATOR_URL."""
|
||||
return f"http://{ORCHESTRATOR_NAME}:{self.port}"
|
||||
return f"http://{self._orchestrator_name}:{self.port}"
|
||||
|
||||
def is_healthy(self, *, timeout: float = _HEALTH_REQUEST_TIMEOUT_SECONDS) -> bool:
|
||||
try:
|
||||
@@ -91,11 +101,11 @@ class OrchestratorService:
|
||||
def _run_orchestrator_container(self) -> None:
|
||||
"""Start the control-plane container (idempotent: clears a stale
|
||||
fixed-name container first). Register-only broker → no docker socket."""
|
||||
run_docker(["docker", "rm", "--force", ORCHESTRATOR_NAME])
|
||||
run_docker(["docker", "rm", "--force", self._orchestrator_name])
|
||||
proc = run_docker([
|
||||
"docker", "run", "--detach",
|
||||
"--name", ORCHESTRATOR_NAME,
|
||||
"--label", ORCHESTRATOR_LABEL,
|
||||
"--name", self._orchestrator_name,
|
||||
"--label", self._orchestrator_label,
|
||||
"--network", self.network,
|
||||
# Host CLI reaches the control plane here; bound to loopback so it
|
||||
# is not exposed on the host's external interfaces.
|
||||
@@ -137,7 +147,7 @@ class OrchestratorService:
|
||||
# (~seconds); the registry DB persists and the current launch
|
||||
# re-registers its own in-memory state. (The dedicated orchestrator
|
||||
# image follow-up replaces this with image-staleness detection.)
|
||||
log.info("starting orchestrator container", context={"name": ORCHESTRATOR_NAME})
|
||||
log.info("starting orchestrator container", context={"name": self._orchestrator_name})
|
||||
self._run_orchestrator_container()
|
||||
|
||||
deadline = time.monotonic() + startup_timeout
|
||||
@@ -152,7 +162,7 @@ class OrchestratorService:
|
||||
|
||||
def stop(self) -> None:
|
||||
"""Remove the orchestrator + gateway containers (idempotent)."""
|
||||
run_docker(["docker", "rm", "--force", ORCHESTRATOR_NAME])
|
||||
run_docker(["docker", "rm", "--force", self._orchestrator_name])
|
||||
self._gateway().stop()
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user