fix(smolmachines): mount git-gate files at boot via virtiofs staging dirs
Two issues caused krun_start_enter -22 (VM boot crash): 1. git-gate entrypoint missing at boot: sidecar_init.py starts all daemons (including git-gate) immediately as PID 1. The entrypoint was copied in via machine_cp which only runs after machine_start returns — too late. virtiofs also can't mount files at root (/). Fix: bake a static /git-gate-entrypoint.sh wrapper into the Dockerfile.sidecars image that delegates to /etc/git-gate/entrypoint.sh. For smolmachines, stage per-bottle scripts with correct in-VM names under the git-gate state dir and virtiofs-mount to /etc/git-gate/ at VM creation time. docker/macOS backends are unchanged (their file bind-mount/docker cp still overrides the static wrapper). 2. Egress confdir mounted read-only: egress_entrypoint.sh writes combined-trust.pem to confdir under set -e; mitmproxy also needs to write its per-host cert cache there. Both fail fatally on a read-only virtiofs mount. Fix: change the egress confdir virtiofs mount to writable. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+15
-3
@@ -14,9 +14,10 @@
|
||||
# /app/supervise_server.py + .py supervise MCP server
|
||||
# /app/sidecar_init.py PID 1 supervisor
|
||||
# /etc/egress/routes.yaml bind-mounted at run time
|
||||
# /etc/git-gate/pre-receive docker-cp'd at start time
|
||||
# /git-gate-entrypoint.sh docker-cp'd at start time
|
||||
# /git-gate/creds/* docker-cp'd at start time
|
||||
# /etc/git-gate/entrypoint.sh per-bottle (docker-cp or virtiofs mount)
|
||||
# /etc/git-gate/pre-receive per-bottle (docker-cp or virtiofs mount)
|
||||
# /git-gate-entrypoint.sh static wrapper → /etc/git-gate/entrypoint.sh
|
||||
# /git-gate/creds/* per-bottle (docker-cp or virtiofs mount)
|
||||
# /git/* bare repos, populated at runtime
|
||||
# /run/supervise/bot-bottle.db bind-mounted at run time
|
||||
# /home/mitmproxy/.mitmproxy/ mitmproxy CA dir
|
||||
@@ -90,6 +91,17 @@ RUN mkdir -p \
|
||||
/run/supervise \
|
||||
/home/mitmproxy/.mitmproxy
|
||||
|
||||
# Static wrapper for the git-gate entrypoint. The per-bottle
|
||||
# entrypoint script is either:
|
||||
# - docker-cp'd to /git-gate-entrypoint.sh (docker/macOS backends),
|
||||
# which overwrites this wrapper; or
|
||||
# - virtiofs-mounted at /etc/git-gate/entrypoint.sh (smolmachines),
|
||||
# where this wrapper delegates to it at runtime.
|
||||
# Either way sidecar_init.py calls `/bin/sh /git-gate-entrypoint.sh`.
|
||||
RUN printf '#!/bin/sh\nexec /etc/git-gate/entrypoint.sh "$@"\n' \
|
||||
> /git-gate-entrypoint.sh \
|
||||
&& chmod 755 /git-gate-entrypoint.sh
|
||||
|
||||
# Documentation only — the compose renderer publishes whichever
|
||||
# subset the bottle uses.
|
||||
EXPOSE 8888 9099 9418 9420 9100
|
||||
|
||||
@@ -14,6 +14,7 @@ from __future__ import annotations
|
||||
|
||||
import dataclasses
|
||||
import os
|
||||
import shutil
|
||||
from contextlib import ExitStack, contextmanager
|
||||
from pathlib import Path
|
||||
from typing import Callable, Generator
|
||||
@@ -32,12 +33,7 @@ from ..docker.egress import (
|
||||
EGRESS_PORT as _EGRESS_PORT,
|
||||
egress_tls_init,
|
||||
)
|
||||
from ..docker.git_gate import (
|
||||
GIT_GATE_ACCESS_HOOK_IN_CONTAINER,
|
||||
GIT_GATE_CREDS_DIR_IN_CONTAINER,
|
||||
GIT_GATE_ENTRYPOINT_IN_CONTAINER,
|
||||
GIT_GATE_HOOK_IN_CONTAINER,
|
||||
)
|
||||
from ..docker.git_gate import GIT_GATE_CREDS_DIR_IN_CONTAINER
|
||||
from ...git_gate import (
|
||||
provision_git_gate_dynamic_keys,
|
||||
revoke_git_gate_provisioned_keys,
|
||||
@@ -61,6 +57,14 @@ from .local_registry import crane_push_tarball, ephemeral_registry
|
||||
_REPO_DIR = str(Path(__file__).resolve().parent.parent.parent.parent)
|
||||
|
||||
|
||||
# In-VM directory containing git-gate scripts staged for virtiofs mount.
|
||||
# The Dockerfile.sidecars static wrapper at /git-gate-entrypoint.sh
|
||||
# delegates to this path, so the smolmachines backend can supply the
|
||||
# per-bottle entrypoint via a directory virtiofs mount instead of a
|
||||
# post-boot machine_cp (which runs too late for PID 1 daemons).
|
||||
_GIT_GATE_SCRIPTS_DIR_IN_VM = "/etc/git-gate"
|
||||
|
||||
|
||||
# Per-host cache for `smolvm pack create` outputs. Keyed by the
|
||||
# docker image ID so a Dockerfile change automatically invalidates
|
||||
# the cache. `pack create` is idempotent on the smolvm side but
|
||||
@@ -358,6 +362,47 @@ def _port_for_label(label: str) -> int:
|
||||
raise ValueError(f"unknown sidecar forward label: {label}")
|
||||
|
||||
|
||||
def _stage_git_gate_for_smolvm(
|
||||
plan: SmolmachinesBottlePlan,
|
||||
) -> tuple[Path, Path]:
|
||||
"""Stage git-gate scripts and credentials as virtiofs-mountable dirs.
|
||||
|
||||
virtiofs requires directory sources; the per-bottle git-gate scripts
|
||||
have host-side names that differ from their in-VM paths (e.g.
|
||||
`git_gate_entrypoint.sh` → `entrypoint.sh` in /etc/git-gate/), so
|
||||
they cannot be expressed as parent-dir mounts without renaming.
|
||||
|
||||
Copies them into per-bottle staging dirs under the git-gate state
|
||||
dir. Returns `(scripts_dir, creds_dir)`:
|
||||
- scripts_dir is mounted to /etc/git-gate/ in the sidecar VM;
|
||||
the image's static /git-gate-entrypoint.sh wrapper delegates there.
|
||||
- creds_dir is mounted to /git-gate/creds/ in the sidecar VM."""
|
||||
gp = plan.git_gate_plan
|
||||
state_dir = git_gate_state_dir(plan.slug)
|
||||
|
||||
scripts_dir = state_dir / "smolvm-scripts"
|
||||
scripts_dir.mkdir(parents=True, exist_ok=True)
|
||||
shutil.copy2(str(gp.entrypoint_script), str(scripts_dir / "entrypoint.sh"))
|
||||
shutil.copy2(str(gp.hook_script), str(scripts_dir / "pre-receive"))
|
||||
shutil.copy2(str(gp.access_hook_script), str(scripts_dir / "access-hook"))
|
||||
for name in ("entrypoint.sh", "pre-receive", "access-hook"):
|
||||
(scripts_dir / name).chmod(0o755)
|
||||
|
||||
creds_dir = state_dir / "smolvm-creds"
|
||||
creds_dir.mkdir(parents=True, exist_ok=True)
|
||||
for u in gp.upstreams:
|
||||
keypath = Path(expand_tilde(u.identity_file))
|
||||
dest_key = creds_dir / f"{u.name}-key"
|
||||
shutil.copy2(str(keypath), str(dest_key))
|
||||
dest_key.chmod(0o600)
|
||||
if u.known_hosts_file:
|
||||
dest_kh = creds_dir / f"{u.name}-known_hosts"
|
||||
shutil.copy2(str(u.known_hosts_file), str(dest_kh))
|
||||
dest_kh.chmod(0o600)
|
||||
|
||||
return scripts_dir, creds_dir
|
||||
|
||||
|
||||
def _bundle_launch_spec(
|
||||
plan: SmolmachinesBottlePlan, network: str, proxy_host: str,
|
||||
) -> _bundle.BundleLaunchSpec:
|
||||
@@ -378,33 +423,32 @@ def _bundle_launch_spec(
|
||||
|
||||
# --- egress -----------------------------------------------
|
||||
ep = plan.egress_plan
|
||||
volumes.append((str(ep.mitmproxy_ca_host_path), EGRESS_CA_IN_CONTAINER, True))
|
||||
# virtiofs (smolvm) requires directory mounts — mount the CA's
|
||||
# parent dir so mitmproxy-ca.pem lands at the right in-VM path.
|
||||
# Writable so egress_entrypoint.sh can write combined-trust.pem
|
||||
# and mitmproxy can create its per-host cert cache.
|
||||
volumes.append((
|
||||
str(ep.mitmproxy_ca_host_path.parent),
|
||||
str(Path(EGRESS_CA_IN_CONTAINER).parent),
|
||||
False,
|
||||
))
|
||||
if ep.routes:
|
||||
volumes.append((str(ep.routes_path.parent), str(Path(EGRESS_ROUTES_IN_CONTAINER).parent), True))
|
||||
env.extend(egress_sidecar_env_entries(ep))
|
||||
|
||||
# --- git-gate ---------------------------------------------
|
||||
# virtiofs requires directory mounts; git-gate scripts have
|
||||
# host-side names that differ from their in-VM paths. Stage
|
||||
# them with correct names under the git-gate state dir and
|
||||
# mount those staging dirs. The image's static wrapper at
|
||||
# /git-gate-entrypoint.sh delegates to the staged entrypoint,
|
||||
# so the per-bottle script is available at VM boot time.
|
||||
gp = plan.git_gate_plan
|
||||
if gp.upstreams:
|
||||
daemons += ["git-gate", "git-http"]
|
||||
volumes += [
|
||||
(str(gp.entrypoint_script), GIT_GATE_ENTRYPOINT_IN_CONTAINER, True),
|
||||
(str(gp.hook_script), GIT_GATE_HOOK_IN_CONTAINER, True),
|
||||
(str(gp.access_hook_script), GIT_GATE_ACCESS_HOOK_IN_CONTAINER, True),
|
||||
]
|
||||
for u in gp.upstreams:
|
||||
keypath = expand_tilde(u.identity_file)
|
||||
volumes.append((
|
||||
keypath,
|
||||
f"{GIT_GATE_CREDS_DIR_IN_CONTAINER}/{u.name}-key",
|
||||
True,
|
||||
))
|
||||
if u.known_hosts_file:
|
||||
volumes.append((
|
||||
str(u.known_hosts_file),
|
||||
f"{GIT_GATE_CREDS_DIR_IN_CONTAINER}/{u.name}-known_hosts",
|
||||
True,
|
||||
))
|
||||
scripts_dir, creds_dir = _stage_git_gate_for_smolvm(plan)
|
||||
volumes.append((str(scripts_dir), _GIT_GATE_SCRIPTS_DIR_IN_VM, True))
|
||||
volumes.append((str(creds_dir), GIT_GATE_CREDS_DIR_IN_CONTAINER, True))
|
||||
|
||||
# --- supervise --------------------------------------------
|
||||
sp = plan.supervise_plan
|
||||
@@ -415,7 +459,13 @@ def _bundle_launch_spec(
|
||||
f"SUPERVISE_DB_PATH={DB_PATH_IN_CONTAINER}",
|
||||
f"SUPERVISE_PORT={SUPERVISE_PORT}",
|
||||
]
|
||||
volumes.append((str(sp.db_path), DB_PATH_IN_CONTAINER, False))
|
||||
# virtiofs requires directory mount — mount the DB's parent
|
||||
# dir so bot-bottle.db lands at the right in-VM path.
|
||||
volumes.append((
|
||||
str(sp.db_path.parent),
|
||||
str(Path(DB_PATH_IN_CONTAINER).parent),
|
||||
False,
|
||||
))
|
||||
|
||||
# Container ports the agent reaches from the smolvm guest —
|
||||
# published on `proxy_host` so the TSI allowlist and the docker
|
||||
|
||||
@@ -404,7 +404,11 @@ class TestBundleLaunchSpec(unittest.TestCase):
|
||||
),
|
||||
)
|
||||
|
||||
spec = _bundle_launch_spec(plan, "net", "127.0.0.16")
|
||||
with patch(
|
||||
"bot_bottle.backend.smolmachines.launch._stage_git_gate_for_smolvm",
|
||||
return_value=(Path("/tmp/smolvm-scripts"), Path("/tmp/smolvm-creds")),
|
||||
):
|
||||
spec = _bundle_launch_spec(plan, "net", "127.0.0.16")
|
||||
|
||||
self.assertEqual(
|
||||
"egress,git-gate,git-http",
|
||||
@@ -430,7 +434,9 @@ class TestBundleLaunchSpec(unittest.TestCase):
|
||||
spec = _bundle_launch_spec(plan, "net", "127.0.0.16")
|
||||
self.assertIn("supervise", spec.daemons_csv)
|
||||
self.assertIn(f"SUPERVISE_DB_PATH={DB_PATH_IN_CONTAINER}", spec.environment)
|
||||
self.assertIn(("/tmp/bot-bottle.db", DB_PATH_IN_CONTAINER, False), spec.volumes)
|
||||
# virtiofs requires directory mounts; the DB's parent dir is
|
||||
# mounted so bot-bottle.db lands at the right in-VM path.
|
||||
self.assertIn(("/tmp", str(Path(DB_PATH_IN_CONTAINER).parent), False), spec.volumes)
|
||||
|
||||
def test_canary_env_visible_to_smolvm_guest(self):
|
||||
plan = _plan(canary=True)
|
||||
@@ -556,6 +562,9 @@ class TestLaunchResourceWiring(unittest.TestCase):
|
||||
"bot_bottle.backend.smolmachines.launch._bundle.start_bundle_vm",
|
||||
return_value=raw_launch,
|
||||
) as start_vm, patch(
|
||||
"bot_bottle.backend.smolmachines.launch._stage_git_gate_for_smolvm",
|
||||
return_value=(Path("/tmp/smolvm-scripts"), Path("/tmp/smolvm-creds")),
|
||||
), patch(
|
||||
"bot_bottle.backend.smolmachines.launch._forward.start_forwarder",
|
||||
return_value=handle,
|
||||
) as start_forwarder:
|
||||
|
||||
Reference in New Issue
Block a user