feat(supervise): start orchestrator on demand via backend-agnostic bring-up
Add BottleBackend.ensure_orchestrator() -> str: the backend-agnostic entry point that brings up the per-host orchestrator + shared gateway (idempotent) and returns its host-reachable control-plane URL. Docker starts the orchestrator + gateway containers (OrchestratorService); firecracker boots the infra VM; macos-container dies with a pointer (no orchestrator). Previously bring-up was reachable only through each backend's consolidated_launch, with no shared handle. Wire it into `bot-bottle supervise`: supervise is often the first thing an operator runs, before any bottle has booted the control plane, so `_resolve_orchestrator_url` now starts the selected backend's orchestrator on demand when discovery finds nothing, instead of failing with "launch a bottle first". Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UoEZHDjv84ChoZbozQERhJ
This commit is contained in:
@@ -511,6 +511,18 @@ class BottleBackend(ABC, Generic[PlanT, CleanupT]):
|
||||
del plan
|
||||
return ""
|
||||
|
||||
def ensure_orchestrator(self) -> str:
|
||||
"""Bring up this backend's per-host orchestrator + shared gateway
|
||||
(idempotent) and return the host-reachable control-plane URL.
|
||||
|
||||
This is the backend-agnostic bring-up entry point: `launch` calls
|
||||
it as part of starting a bottle, and operator tools (`supervise`)
|
||||
call it to start the control plane on demand when none is running
|
||||
yet. Docker starts the orchestrator + gateway containers;
|
||||
firecracker boots the infra VM. Backends with no orchestrator
|
||||
(macos-container) die with a pointer — the default here."""
|
||||
die(f"backend {self.name!r} has no orchestrator control plane")
|
||||
|
||||
@abstractmethod
|
||||
def prepare_cleanup(self) -> CleanupT:
|
||||
"""Enumerate orphaned resources from previous bottles. No side
|
||||
|
||||
@@ -105,6 +105,10 @@ class DockerBottleBackend(BottleBackend["DockerBottlePlan", "DockerBottleCleanup
|
||||
with _launch.launch(plan, provision=self.provision) as bottle:
|
||||
yield bottle
|
||||
|
||||
def ensure_orchestrator(self) -> str:
|
||||
from ...orchestrator.lifecycle import OrchestratorService
|
||||
return OrchestratorService().ensure_running()
|
||||
|
||||
def supervise_mcp_url(self, plan: DockerBottlePlan) -> str:
|
||||
"""Docker bottles reach the supervise daemon via the
|
||||
compose-network alias `supervise:9100`. No per-bottle URL
|
||||
|
||||
@@ -110,3 +110,7 @@ class FirecrackerBottleBackend(
|
||||
|
||||
def supervise_mcp_url(self, plan: FirecrackerBottlePlan) -> str:
|
||||
return plan.agent_supervise_url
|
||||
|
||||
def ensure_orchestrator(self) -> str:
|
||||
from . import infra_vm
|
||||
return infra_vm.ensure_running().control_plane_url
|
||||
|
||||
@@ -65,10 +65,26 @@ ApplyError = (OrchestratorClientError,)
|
||||
_client_instance: OrchestratorClient | None = None
|
||||
|
||||
|
||||
def _resolve_orchestrator_url() -> str:
|
||||
"""URL of the running orchestrator control plane, starting one on demand.
|
||||
|
||||
Supervise is often the first thing an operator runs — before any bottle
|
||||
has booted the control plane. So when discovery finds nothing, bring up
|
||||
the selected backend's orchestrator + gateway (idempotent) rather than
|
||||
failing with "launch a bottle first"."""
|
||||
try:
|
||||
return discover_orchestrator_url()
|
||||
except OrchestratorClientError:
|
||||
from ..backend import get_bottle_backend
|
||||
backend = get_bottle_backend()
|
||||
info(f"no orchestrator control plane running; starting one ({backend.name})…")
|
||||
return backend.ensure_orchestrator()
|
||||
|
||||
|
||||
def _client() -> OrchestratorClient:
|
||||
global _client_instance # noqa: PLW0603 — CLI-session singleton
|
||||
if _client_instance is None:
|
||||
_client_instance = OrchestratorClient(discover_orchestrator_url())
|
||||
_client_instance = OrchestratorClient(_resolve_orchestrator_url())
|
||||
return _client_instance
|
||||
|
||||
|
||||
@@ -204,12 +220,18 @@ def cmd_supervise(argv: list[str]) -> int:
|
||||
args = parser.parse_args(argv)
|
||||
|
||||
# Establish the orchestrator connection up front so a missing control
|
||||
# plane is a clean one-line error, not a curses crash mid-loop.
|
||||
# plane is a clean one-line error, not a curses crash mid-loop. This also
|
||||
# starts the orchestrator on demand when none is running (see `_client`).
|
||||
try:
|
||||
_client()
|
||||
except OrchestratorClientError as e:
|
||||
error(str(e))
|
||||
return 1
|
||||
except Die as e:
|
||||
# Backend has no orchestrator to start (e.g. macos-container).
|
||||
if e.message:
|
||||
error(e.message)
|
||||
return e.code if isinstance(e.code, int) else 1
|
||||
|
||||
if args.once:
|
||||
return _list_once()
|
||||
|
||||
Reference in New Issue
Block a user