diff --git a/bot_bottle/backend/smolmachines/bottle_plan.py b/bot_bottle/backend/smolmachines/bottle_plan.py index 48d6a1f..62bf28b 100644 --- a/bot_bottle/backend/smolmachines/bottle_plan.py +++ b/bot_bottle/backend/smolmachines/bottle_plan.py @@ -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 = "" diff --git a/bot_bottle/backend/smolmachines/launch.py b/bot_bottle/backend/smolmachines/launch.py index 2d2b430..1cb1f4a 100644 --- a/bot_bottle/backend/smolmachines/launch.py +++ b/bot_bottle/backend/smolmachines/launch.py @@ -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 /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 diff --git a/bot_bottle/backend/smolmachines/resolve_plan.py b/bot_bottle/backend/smolmachines/resolve_plan.py index 8d8dfcb..f81e77b 100644 --- a/bot_bottle/backend/smolmachines/resolve_plan.py +++ b/bot_bottle/backend/smolmachines/resolve_plan.py @@ -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) diff --git a/bot_bottle/backend/smolmachines/sidecar_bundle.py b/bot_bottle/backend/smolmachines/sidecar_bundle.py index 59916b1..6118014 100644 --- a/bot_bottle/backend/smolmachines/sidecar_bundle.py +++ b/bot_bottle/backend/smolmachines/sidecar_bundle.py @@ -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 `.2` so the - smolvm guest's TSI allowlist (`/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 /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 - `:` 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 ''}" - ) - # 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()}" - ) diff --git a/tests/integration/test_smolmachines_bundle_bringup.py b/tests/integration/test_smolmachines_bundle_bringup.py deleted file mode 100644 index 043a4a9..0000000 --- a/tests/integration/test_smolmachines_bundle_bringup.py +++ /dev/null @@ -1,111 +0,0 @@ -"""Integration: PRD 0023 chunk 2c — bundle bringup on a per-bottle -docker bridge with the pinned IP. - -End-to-end against the real docker daemon. Brings up just the -sidecar bundle on its own bridge, confirms the container lands at -the pinned IP, then tears down. Skipped under act_runner (docker -socket mount topology breaks bridge visibility) and when the -bundle image isn't available. - -Full launch flow (smolvm + bundle + provisioning + the -localhost-reach / egress-port-bypass probes) lives in chunk 2d.""" - -from __future__ import annotations - -import os -import subprocess -import time -import unittest - -from bot_bottle.backend.smolmachines.sidecar_bundle import ( - BundleLaunchSpec, - bundle_container_name, - bundle_network_name, - create_bundle_network, - remove_bundle_network, - start_bundle, - stop_bundle, -) -from tests._docker import skip_unless_docker - - -@skip_unless_docker() -@unittest.skipIf( - os.environ.get("GITEA_ACTIONS") == "true", - "skipped under act_runner: docker socket mount topology breaks " - "in-process visibility of networks created on the host daemon", -) -class TestBundleBringup(unittest.TestCase): - def setUp(self): - self.slug = f"cb-test-bundle-{os.getpid()}-{int(time.time())}" - self.network = bundle_network_name(self.slug) - self.container = bundle_container_name(self.slug) - - def tearDown(self): - stop_bundle(self.slug) - remove_bundle_network(self.network) - - def _bundle_image_built(self) -> bool: - """The bundle image (`bot-bottle-sidecars:latest`) is - built lazily by the docker backend's compose. If a - smolmachines-only operator hasn't run the docker backend - first, the image won't exist locally. Skip rather than - fail.""" - r = subprocess.run( - ["docker", "image", "inspect", "bot-bottle-sidecars:latest"], - stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, - check=False, - ) - return r.returncode == 0 - - def test_create_network_then_start_bundle_pins_ip(self): - if not self._bundle_image_built(): - self.skipTest( - "bot-bottle-sidecars:latest not built; run a docker " - "bottle first or `docker build -f Dockerfile.sidecars .`" - ) - - # Pick a subnet unlikely to collide on the host. Last - # octet of the slug hash isn't deterministic across runs; - # we hardcode a high octet (.211) that the docker default - # bridges almost never use. - subnet = "192.168.211.0/24" - gateway = "192.168.211.1" - bundle_ip = "192.168.211.2" - - create_bundle_network(self.network, subnet, gateway) - - spec = BundleLaunchSpec( - slug=self.slug, - network_name=self.network, - subnet=subnet, - gateway=gateway, - bundle_ip=bundle_ip, - # Empty daemons_csv → init exits "no daemons selected" - # immediately. We just need the container to land on - # the network at the right IP before it exits. - daemons_csv="", # empty → init exits "no daemons selected" - ) - start_bundle(spec) - - # Inspect the container's IP on the per-bottle network. - r = subprocess.run( - ["docker", "inspect", - "--format", - "{{(index .NetworkSettings.Networks \"" + self.network + "\").IPAddress}}", - self.container], - capture_output=True, text=True, check=False, - ) - # Container may have exited (no daemons selected → exit 0). - # The inspect still works on exited containers as long as - # `--rm` hasn't fired yet, which is a race. Even if it has, - # the launch succeeded — the container existed, on the - # right network, at the right IP. We don't fail here on - # missing inspect. - if r.returncode == 0 and r.stdout.strip(): - self.assertEqual(bundle_ip, r.stdout.strip(), - f"bundle landed at wrong IP: {r.stdout!r}") - - -if __name__ == "__main__": - unittest.main() diff --git a/tests/integration/test_smolmachines_launch.py b/tests/integration/test_smolmachines_launch.py index b93756a..90881f1 100644 --- a/tests/integration/test_smolmachines_launch.py +++ b/tests/integration/test_smolmachines_launch.py @@ -1,10 +1,9 @@ -"""Integration: PRD 0023 chunk 2d — end-to-end launch + exec -round trip + the acceptance probes. +"""Integration: end-to-end smolmachines launch + exec round trip. -The smoke confirms the launch flow (per-bottle docker bridge → -sidecar bundle with host-loopback published ports → smolvm guest -with TSI allowlist → exec) plumbs together end to end. The probes confirm the -security properties the design pivot was about: +The smoke confirms the launch flow (sidecar bundle smolVM → +host-loopback forwarders → agent smolVM with TSI allowlist → exec) +plumbs together end to end. The probes confirm the security +properties the design pivot was about: - **localhost-reach probe** — guest tries to dial a service bound on the host's `127.0.0.1`. TSI's per-bottle loopback @@ -14,13 +13,6 @@ security properties the design pivot was about: the injected `HTTPS_PROXY`/`HTTP_PROXY` URL on the per-bottle loopback alias, while direct egress with proxy vars unset fails. - - **egress-port-bypass probe** — guest tries to dial - `:9099` (egress's port). TSI permits the IP but - the bundle's egress daemon binds `127.0.0.1` inside its - container, so the connect refuses at the socket level. The - bind-address mitigation is what closes TSI's port-granularity - gap. - Gated on macOS/Linux + smolvm + docker + not GITEA_ACTIONS — the runner can't host libkrun-backed VMs.""" @@ -114,8 +106,8 @@ class TestSmolmachinesLaunch(unittest.TestCase): def test_localhost_reach_probe(self): # Agent dials a 127.0.0.1 service on the host. TSI's - # allowlist contains only /32, so this must - # refuse. We use a port unlikely to be bound on the host + # allowlist contains only the per-bottle loopback alias, so + # this must refuse. We use a port unlikely to be bound on the host # (high-numbered) so we're confirming TSI refusal, not # just "no service listening." r = self.bottle.exec( @@ -196,28 +188,6 @@ class TestSmolmachinesLaunch(unittest.TestCase): self.assertEqual(0, r.returncode, msg=r.stderr) self.assertEqual(_AGENT_PROMPT, r.stdout.rstrip("\n")) - def test_egress_port_bypass_probe(self): - # Agent dials :9099 (egress's port). TSI - # permits the IP, but egress will bind 127.0.0.1:9099 - # inside the bundle in chunk 3, so the connect refuses - # at the socket level. NOTE: in chunk 2d the bundle's - # daemons aren't running (daemons_csv=""), so nothing - # is listening on :9099 anyway — this test asserts the - # connect fails, which is the property chunk 3 will - # preserve once egress is actually running. - r = self.bottle.exec( - "env -u HTTPS_PROXY -u HTTP_PROXY -u https_proxy -u http_proxy " - f"curl -s --show-error --max-time 3 http://{self.plan.bundle_ip}:9099 " - "2>&1 || true" - ) - self.assertTrue( - "refused" in r.stdout.lower() - or "timed out" in r.stdout.lower() - or "unreachable" in r.stdout.lower() - or "failed" in r.stdout.lower(), - f"expected egress port refusal; got: {r.stdout!r}", - ) - if __name__ == "__main__": unittest.main() diff --git a/tests/unit/test_smolmachines_sidecar_bundle.py b/tests/unit/test_smolmachines_sidecar_bundle.py index ac165dc..57c6aa4 100644 --- a/tests/unit/test_smolmachines_sidecar_bundle.py +++ b/tests/unit/test_smolmachines_sidecar_bundle.py @@ -1,9 +1,4 @@ -"""Unit: bundle bringup primitives for the smolmachines backend -(PRD 0023 chunk 2c). - -Tests mock `subprocess.run` and assert on the docker argv shape. -The end-to-end integration smoke (real docker daemon, real -bundle image) lands in chunk 2d.""" +"""Unit: bundle bringup primitives for the smolmachines backend.""" from __future__ import annotations @@ -17,28 +12,12 @@ from bot_bottle.backend.smolmachines.sidecar_bundle import ( allocate_raw_host_ports, bundle_container_name, bundle_network_name, - create_bundle_network, ensure_bundle_image, start_bundle_vm, - remove_bundle_network, - start_bundle, - stop_bundle, stop_bundle_vm, ) -def _ok(stdout: str = "", stderr: str = "") -> subprocess.CompletedProcess: # type: ignore - return subprocess.CompletedProcess( - args=[], returncode=0, stdout=stdout, stderr=stderr, - ) - - -def _fail(stderr: str = "boom") -> subprocess.CompletedProcess: # type: ignore - return subprocess.CompletedProcess( - args=[], returncode=1, stdout="", stderr=stderr, - ) - - def _spec(**kwargs) -> BundleLaunchSpec: # type: ignore defaults = dict( slug="demo-abc12", @@ -71,123 +50,6 @@ class TestNamingHelpers(unittest.TestCase): ) -class TestNetworkLifecycle(unittest.TestCase): - def _patch_run(self, **kwargs): # type: ignore - return patch( - "bot_bottle.backend.smolmachines.sidecar_bundle.subprocess.run", - **kwargs, - ) - - def test_create_argv_explicit_subnet_and_gateway(self): - with self._patch_run(return_value=_ok()) as m: - create_bundle_network("nn", "192.168.50.0/24", "192.168.50.1") - self.assertEqual( - ["docker", "network", "create", - "--subnet", "192.168.50.0/24", - "--gateway", "192.168.50.1", - "nn"], - m.call_args.args[0], - ) - - def test_create_treats_existing_network_as_success(self): - with self._patch_run(return_value=_fail("network nn already exists")): - # No SystemExit. - create_bundle_network("nn", "192.168.50.0/24", "192.168.50.1") - - def test_create_other_failure_is_fatal(self): - with self._patch_run(return_value=_fail("invalid subnet")): - with self.assertRaises(SystemExit): - create_bundle_network("nn", "bogus", "bogus") - - def test_remove_missing_network_is_idempotent(self): - # No SystemExit / no warn-and-continue noise; missing - # network is the expected case during a partial teardown. - with self._patch_run(return_value=_fail("Error: No such network: nn")): - remove_bundle_network("nn") - - def test_remove_clean_returns_success(self): - with self._patch_run(return_value=_ok()): - remove_bundle_network("nn") - - -class TestStartBundle(unittest.TestCase): - def _patch_run(self): - return patch( - "bot_bottle.backend.smolmachines.sidecar_bundle.subprocess.run", - return_value=_ok(), - ) - - def test_argv_pins_ip_on_network(self): - with self._patch_run() as m: - start_bundle(_spec()) - argv = m.call_args.args[0] - # --network NETNAME --ip on the docker run. - self.assertIn("--network", argv) - self.assertIn("bot-bottle-bundle-demo-abc12", argv) - self.assertIn("--ip", argv) - self.assertIn("192.168.50.2", argv) - # Detached and auto-removed. - self.assertIn("--detach", argv) - self.assertIn("--rm", argv) - # Container name uses the per-slug bundle prefix. - i = argv.index("--name") - self.assertEqual("bot-bottle-sidecars-demo-abc12", argv[i + 1]) - # Image at the end. - self.assertEqual("bot-bottle-sidecars:latest", argv[-1]) - - def test_daemons_env_passed_in(self): - with self._patch_run() as m: - start_bundle(_spec(daemons_csv="egress,supervise")) - argv = m.call_args.args[0] - self.assertIn("-e", argv) - self.assertIn( - "BOT_BOTTLE_SIDECAR_DAEMONS=egress,supervise", - argv, - ) - - def test_environment_entries_pass_through(self): - with self._patch_run() as m: - start_bundle(_spec(environment=( - "SUPERVISE_BOTTLE_SLUG=demo-abc12", - "EGRESS_TOKEN_0", # bare-name → host env inherit - ))) - argv = m.call_args.args[0] - self.assertIn("SUPERVISE_BOTTLE_SLUG=demo-abc12", argv) - self.assertIn("EGRESS_TOKEN_0", argv) - - def test_volumes_render_with_ro_flag(self): - with self._patch_run() as m: - start_bundle(_spec(volumes=( - ("/host/egress-ca.pem", "/home/mitmproxy/.mitmproxy/mitmproxy-ca.pem", True), - ("/host/queue", "/run/supervise/queue", False), - ))) - argv = m.call_args.args[0] - self.assertIn( - "/host/egress-ca.pem:/home/mitmproxy/.mitmproxy/mitmproxy-ca.pem:ro", - argv, - ) - self.assertIn("/host/queue:/run/supervise/queue", argv) - - def test_failure_dies(self): - with patch( - "bot_bottle.backend.smolmachines.sidecar_bundle.subprocess.run", - return_value=_fail("invalid mount"), - ): - with self.assertRaises(SystemExit): - start_bundle(_spec()) - - def test_host_env_inherited_to_subprocess(self): - # Bare-name entries in spec.environment rely on the docker - # subprocess being run with the host env. Confirm `env=` - # threads through. - with patch( - "bot_bottle.backend.smolmachines.sidecar_bundle.subprocess.run", - return_value=_ok(), - ) as m: - start_bundle(_spec(), env={"FOO": "bar"}) - self.assertEqual({"FOO": "bar"}, m.call_args.kwargs["env"]) - - class TestEnsureBundleImage(unittest.TestCase): def test_builds_sidecar_dockerfile_before_plain_docker_run(self): with patch( @@ -263,28 +125,6 @@ class TestStartBundleVm(unittest.TestCase): ) -class TestStopBundle(unittest.TestCase): - def _patch_run(self, **kwargs): # type: ignore - return patch( - "bot_bottle.backend.smolmachines.sidecar_bundle.subprocess.run", - **kwargs, - ) - - def test_argv_force_removes(self): - with self._patch_run(return_value=_ok()) as m: - stop_bundle("demo-abc12") - self.assertEqual( - ["docker", "rm", "-f", "bot-bottle-sidecars-demo-abc12"], - m.call_args.args[0], - ) - - def test_missing_container_is_idempotent(self): - with self._patch_run(return_value=_fail( - "Error: No such container: bot-bottle-sidecars-demo-abc12" - )): - stop_bundle("demo-abc12") # no raise - - class TestStopBundleVm(unittest.TestCase): def test_stops_then_deletes_sidecar_vm(self): with patch(