refactor(smolmachines): drop obsolete docker-sidecar helpers
This commit is contained in:
@@ -1,10 +1,8 @@
|
||||
"""SmolmachinesBottlePlan — concrete BottlePlan for the smolmachines
|
||||
backend (PRD 0023).
|
||||
|
||||
Slug + bundle docker subnet / gateway / pinned IP + smolvm
|
||||
machine name + agent `.smolmachine` artifact + per-bottle guest
|
||||
env. Provisioning fields (CA cert path, prompt path, etc.) land
|
||||
in chunk 4."""
|
||||
Slug + legacy bundle network coordinates + smolvm machine name +
|
||||
agent `.smolmachine` artifact + per-bottle guest env."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -23,9 +21,10 @@ class SmolmachinesBottlePlan(BottlePlan):
|
||||
`supervise_plan`, and `agent_provision` from BottlePlan."""
|
||||
|
||||
slug: str
|
||||
# Per-bottle docker subnet for the sidecar bundle container.
|
||||
# The bundle runs at `bundle_ip` (always `.2`); the gateway is
|
||||
# at `.1`. smolvm's TSI allowlist is set to `bundle_ip/32`.
|
||||
# Legacy per-bottle bundle network coordinates. These remain on
|
||||
# the plan while BundleLaunchSpec still carries the original shape,
|
||||
# but the smolmachines launch path exposes the sidecar VM through
|
||||
# host-loopback forwarders instead of a Docker bridge IP.
|
||||
bundle_subnet: str
|
||||
bundle_gateway: str
|
||||
bundle_ip: str
|
||||
@@ -36,22 +35,10 @@ class SmolmachinesBottlePlan(BottlePlan):
|
||||
# `--smolfile` is mutually exclusive with `--from`, and
|
||||
# `--from` is the path that avoids the registry-pull race).
|
||||
guest_env: dict[str, str]
|
||||
# Inner Plans for the sidecar bundle daemons. The same shape the
|
||||
# docker backend uses — same `.prepare()` calls produced
|
||||
# them — but our launch step doesn't populate the
|
||||
# docker-specific network fields (internal_network,
|
||||
# egress_network) because the smolmachines bundle isn't on
|
||||
# docker's `--internal` + egress bridge topology; it's on a
|
||||
# per-bottle bridge with a pinned IP. The unused fields stay
|
||||
# at their dataclass defaults.
|
||||
# Agent-side endpoints. On Docker Desktop the docker bridge
|
||||
# IPs aren't reachable from the smolvm guest (TSI uses macOS
|
||||
# networking; docker container IPs live in the daemon's VM),
|
||||
# so the agent dials the bundle via host loopback +
|
||||
# docker-published random ports. Empty at prepare time;
|
||||
# launch populates these after bundle bringup via
|
||||
# `dataclasses.replace`. Format: a `host:port` for git-gate
|
||||
# (insteadOf URL prefix) + full URLs for proxy / supervise.
|
||||
# Agent-side endpoints. Empty at prepare time; launch populates
|
||||
# these after sidecar VM bringup via `dataclasses.replace`.
|
||||
# Format: a `host:port` for git-gate (insteadOf URL prefix) +
|
||||
# full URLs for proxy / supervise.
|
||||
agent_proxy_url: str = ""
|
||||
agent_git_gate_host: str = ""
|
||||
agent_supervise_url: str = ""
|
||||
|
||||
@@ -1,12 +1,9 @@
|
||||
"""End-to-end launch flow for the smolmachines backend
|
||||
(PRD 0023 chunks 2d + 4b).
|
||||
"""End-to-end launch flow for the smolmachines backend.
|
||||
|
||||
Brings up the per-bottle docker bridge + sidecar bundle (with
|
||||
real daemons + their config files), creates + starts the smolvm
|
||||
guest pointed at the bundle's pinned IP via TSI's
|
||||
`--allow-cidr <bundle-ip>/32` allowlist, yields a
|
||||
`SmolmachinesBottle` handle, tears everything down on context
|
||||
exit.
|
||||
Builds the sidecar bundle smolmachine, starts it as a sidecar VM
|
||||
with real daemons + their config files, creates + starts the agent
|
||||
smolVM, yields a `SmolmachinesBottle` handle, and tears everything
|
||||
down on context exit.
|
||||
|
||||
The bundle's daemons consume the inner Plans the docker backend
|
||||
already produces: egress reads routes + CAs from the EgressPlan.
|
||||
@@ -72,9 +69,8 @@ _SMOLMACHINE_CACHE_DIR = Path.home() / ".cache" / "bot-bottle" / "smolmachines"
|
||||
|
||||
|
||||
# Container-internal listening ports for each bundle daemon. The
|
||||
# bundle publishes each one on a random host loopback port (see
|
||||
# `_bundle.start_bundle`), and `_bundle.bundle_host_port` looks
|
||||
# them up post-start.
|
||||
# sidecar VM publishes each one on a random host loopback port, and
|
||||
# the launch flow wraps those raw ports with per-bottle forwarders.
|
||||
_GIT_HTTP_PORT = 9420
|
||||
_SUPERVISE_PORT = SUPERVISE_PORT
|
||||
|
||||
|
||||
@@ -56,13 +56,11 @@ def resolve_plan(
|
||||
git_gate_plan: GitGatePlan,
|
||||
stage_dir: Path,
|
||||
) -> SmolmachinesBottlePlan:
|
||||
"""Materialize the smolmachines plan. The bundle's docker
|
||||
subnet + pinned IP are derived from the slug; the agent's
|
||||
`.smolmachine` artifact is built (or cache-hit) here so
|
||||
launch's `machine create --from` boots without a registry
|
||||
pull. Per-bottle guest env + the TSI allow_cidrs land on the
|
||||
plan for launch to pass straight through to
|
||||
`machine create` flags."""
|
||||
"""Materialize the smolmachines plan. The agent `.smolmachine`
|
||||
artifact is built (or cache-hit) here so launch's
|
||||
`machine create --from` boots without a registry pull. Per-bottle
|
||||
guest env lands on the plan for launch to pass straight through
|
||||
to `machine create` flags."""
|
||||
|
||||
# ==== smolmachines specific setup ====
|
||||
subnet, gateway, bundle_ip = smolmachines_bundle_subnet(slug)
|
||||
|
||||
@@ -1,36 +1,18 @@
|
||||
"""Per-bottle sidecar bundle bringup for the smolmachines backend
|
||||
(PRD 0023).
|
||||
"""Per-bottle sidecar bundle bringup for the smolmachines backend.
|
||||
|
||||
Two docker resources per bottle live here:
|
||||
|
||||
- **A dedicated bridge network**, subnet derived from the slug.
|
||||
The bundle container gets a pinned IP at `<subnet>.2` so the
|
||||
smolvm guest's TSI allowlist (`<bundle-ip>/32`) has a stable
|
||||
target. Without pinning, we'd have to inspect the container's
|
||||
assigned IP after start and feed it back into the Smolfile
|
||||
— a race we can sidestep with `--ip`.
|
||||
|
||||
- **The bundle container itself**, running the PRD 0024 bundle
|
||||
image (`bot-bottle-sidecars:latest` by default). Same
|
||||
image, same daemons, same daemon-private env / bind-mounts
|
||||
as the docker backend.
|
||||
|
||||
This module ships the lifecycle primitives only — create
|
||||
network, start bundle, stop bundle, remove network — wrapped
|
||||
around `subprocess.run(["docker", ...])`. Wiring them into the
|
||||
launch flow + populating the `BundleLaunchSpec` from the inner
|
||||
Plans (EgressPlan, …) lands in chunk 2d."""
|
||||
The sidecar bundle runs as its own smolVM. The agent VM reaches
|
||||
bundle daemons through host-loopback ports published by that sidecar
|
||||
VM and wrapped by per-bottle address-bound forwarders."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import socket
|
||||
import subprocess
|
||||
from dataclasses import dataclass, field
|
||||
from pathlib import Path
|
||||
from typing import Sequence
|
||||
|
||||
from ...log import die, warn
|
||||
from ...log import warn
|
||||
from ..docker import util as docker_mod
|
||||
from ..docker.sidecar_bundle import (
|
||||
SIDECAR_BUNDLE_DOCKERFILE,
|
||||
@@ -78,10 +60,8 @@ class BundleLaunchSpec:
|
||||
# supervisor inside the bundle reads it to skip
|
||||
# bottle-irrelevant daemons (e.g. supervise=False bottles).
|
||||
daemons_csv: str = "egress"
|
||||
# Plain "KEY=VALUE" strings + "KEY" bare names (the bare-name
|
||||
# form inherits the value from the docker-run subprocess env,
|
||||
# matching the docker backend's compose-up secret-forwarding
|
||||
# pattern).
|
||||
# Plain "KEY=VALUE" strings + "KEY" bare names. Bare names inherit
|
||||
# from the host env passed to the sidecar VM launch.
|
||||
environment: Sequence[str] = field(default_factory=tuple)
|
||||
# (host_path, container_path, read_only) bind mounts.
|
||||
volumes: Sequence[tuple[str, str, bool]] = field(default_factory=tuple)
|
||||
@@ -182,138 +162,3 @@ def stop_bundle_vm(slug: str) -> None:
|
||||
_smolvm.machine_delete(name)
|
||||
except _smolvm.SmolvmError as exc:
|
||||
warn(f"smolvm machine delete {name} failed: {exc}")
|
||||
|
||||
|
||||
def create_bundle_network(network_name: str, subnet: str, gateway: str) -> None:
|
||||
"""`docker network create` with an explicit subnet + gateway
|
||||
so the bundle's `--ip` lands on the address the Smolfile's
|
||||
TSI allowlist points at. Idempotent on the caller's side —
|
||||
`start_bundle` catches the "network exists" error and treats
|
||||
it as success (chunk-2d teardown is paired with each create).
|
||||
"""
|
||||
result = subprocess.run(
|
||||
["docker", "network", "create",
|
||||
"--subnet", subnet, "--gateway", gateway,
|
||||
network_name],
|
||||
capture_output=True, text=True, check=False,
|
||||
)
|
||||
if result.returncode != 0:
|
||||
# Already-exists is fine on a resume path; everything else
|
||||
# is fatal — the bundle won't have an addressable network.
|
||||
if "already exists" in (result.stderr or "").lower():
|
||||
return
|
||||
die(
|
||||
f"docker network create {network_name} failed: "
|
||||
f"{(result.stderr or '').strip()}"
|
||||
)
|
||||
|
||||
|
||||
def remove_bundle_network(network_name: str) -> None:
|
||||
"""Idempotent: a missing network returns success."""
|
||||
result = subprocess.run(
|
||||
["docker", "network", "rm", network_name],
|
||||
capture_output=True, text=True, check=False,
|
||||
)
|
||||
if result.returncode == 0:
|
||||
return
|
||||
if "no such network" in (result.stderr or "").lower():
|
||||
return
|
||||
# Network with attached containers is the common non-fatal
|
||||
# case during a partial teardown — warn but don't die.
|
||||
warn(
|
||||
f"docker network rm {network_name} failed: "
|
||||
f"{(result.stderr or '').strip()}"
|
||||
)
|
||||
|
||||
|
||||
def start_bundle(spec: BundleLaunchSpec, *,
|
||||
env: dict[str, str] | None = None) -> None:
|
||||
"""Bring the bundle container up on the per-bottle bridge with
|
||||
the pinned IP. Argv is built deterministically from `spec`;
|
||||
`env` is the host subprocess env (forwarded values for any
|
||||
bare-name entries in `spec.environment`)."""
|
||||
container = bundle_container_name(spec.slug)
|
||||
argv = [
|
||||
"docker", "run",
|
||||
"--name", container,
|
||||
"--detach",
|
||||
"--rm",
|
||||
"--network", spec.network_name,
|
||||
"--ip", spec.bundle_ip,
|
||||
"-e", f"BOT_BOTTLE_SIDECAR_DAEMONS={spec.daemons_csv}",
|
||||
]
|
||||
for entry in spec.environment:
|
||||
argv += ["-e", entry]
|
||||
for host_path, container_path, read_only in spec.volumes:
|
||||
suffix = ":ro" if read_only else ""
|
||||
argv += ["-v", f"{host_path}:{container_path}{suffix}"]
|
||||
# Loopback-only host port-forwards — the smolvm guest's TSI
|
||||
# uses macOS networking, and macOS loopback is the only host
|
||||
# surface that round-trips into Docker Desktop's daemon VM.
|
||||
# Binds to the per-bottle alias so TSI's IP-only allowlist
|
||||
# narrows reachability to this bottle's bundle only.
|
||||
for port in spec.ports_to_publish:
|
||||
argv += ["-p", f"{spec.publish_host_ip}::{port}"]
|
||||
argv.append(spec.image)
|
||||
result = subprocess.run(
|
||||
argv, capture_output=True, text=True,
|
||||
env=dict(env) if env is not None else None, check=False,
|
||||
)
|
||||
if result.returncode != 0:
|
||||
die(
|
||||
f"docker run for bundle {container} failed: "
|
||||
f"{(result.stderr or '').strip()}"
|
||||
)
|
||||
|
||||
|
||||
def bundle_host_port(
|
||||
slug: str, container_port: int, *, host_ip: str = "127.0.0.1",
|
||||
) -> int:
|
||||
"""`docker port <bundle> <container_port>/tcp` → the random
|
||||
host-side port docker assigned for the binding on `host_ip`.
|
||||
Called after `start_bundle` on each container port listed in
|
||||
`BundleLaunchSpec.ports_to_publish` so the launch step can
|
||||
build the agent's HTTPS_PROXY / GIT_GATE / SUPERVISE URLs in
|
||||
`<host_ip>:<host port>` form."""
|
||||
container = bundle_container_name(slug)
|
||||
result = subprocess.run(
|
||||
["docker", "port", container, f"{container_port}/tcp"],
|
||||
capture_output=True, text=True, check=False,
|
||||
)
|
||||
if result.returncode != 0:
|
||||
die(
|
||||
f"docker port {container} {container_port}/tcp failed: "
|
||||
f"{(result.stderr or '').strip() or '<no stderr>'}"
|
||||
)
|
||||
# Each line looks like `127.0.0.16:54321` — one per address
|
||||
# family / host IP. Match on the expected host_ip prefix so
|
||||
# bottles bound to per-bottle aliases pick the right line.
|
||||
for raw in (result.stdout or "").splitlines():
|
||||
line = raw.strip()
|
||||
if line.startswith(f"{host_ip}:"):
|
||||
_, _, port_str = line.rpartition(":")
|
||||
try:
|
||||
return int(port_str)
|
||||
except ValueError:
|
||||
die(f"unexpected `docker port` output: {line!r}")
|
||||
die(
|
||||
f"no port mapping on {host_ip} for {container} "
|
||||
f"{container_port}/tcp; got: {(result.stdout or '').strip()!r}"
|
||||
)
|
||||
|
||||
|
||||
def stop_bundle(slug: str) -> None:
|
||||
"""Idempotent: a missing container returns success."""
|
||||
container = bundle_container_name(slug)
|
||||
result = subprocess.run(
|
||||
["docker", "rm", "-f", container],
|
||||
capture_output=True, text=True, check=False,
|
||||
)
|
||||
if result.returncode == 0:
|
||||
return
|
||||
if "no such container" in (result.stderr or "").lower():
|
||||
return
|
||||
warn(
|
||||
f"docker rm -f {container} failed: "
|
||||
f"{(result.stderr or '').strip()}"
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user