feat(firecracker): git-gate over SSH into the gateway VM (Stage B, 7/n)
git-gate now works end-to-end for git-upstream bottles on the infra VM.
Three fixes surfaced by driving a real clone through git-http:
- `SshGatewayTransport.cp_into` preserves the source file mode (docker cp
does). The access-hook is staged 0700 and git-http execs it directly; a
plain `cat >` landed it 0644 -> EACCES. Keys stay 0600.
- the infra init runs `BOT_BOTTLE_GATEWAY_DAEMONS=egress,git-http,supervise`:
the VM backend reaches git over git-http (9420), so the git:// daemon
(git-gate, whose /git-gate-entrypoint.sh the consolidated model doesn't
stage) is left out instead of crash-looping.
- `build_infra_rootfs_dir` folds the init's content-hash into the rootfs
cache key, so an init change actually rebuilds the rootfs (the base image
digest alone wouldn't catch it).
Verified on a KVM host: launch_consolidated provisions a git-upstream
bottle's repo + creds into the gateway VM over SSH; an agent VM clones via
git-http (source-IP attributed) and the access-hook resolves the
provisioned key + known_hosts and attempts the upstream SSH fetch —
failing closed only because the test used a dummy key + fake upstream
("refusing to serve stale data"). With real creds the fetch serves.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WBMWTEtQdJ4W5UrWuLHCck
This commit is contained in:
@@ -17,9 +17,11 @@ surface.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import os
|
||||
import shlex
|
||||
import signal
|
||||
import stat
|
||||
import subprocess
|
||||
import time
|
||||
import urllib.error
|
||||
@@ -115,9 +117,13 @@ def ensure_built() -> None:
|
||||
|
||||
def build_infra_rootfs_dir() -> Path:
|
||||
"""The infra VM's base rootfs: the infra image prepared with the
|
||||
control-plane + gateway init as PID 1."""
|
||||
control-plane + gateway init as PID 1. The init's content is folded into
|
||||
the cache key so an init change rebuilds the rootfs (the base image digest
|
||||
alone wouldn't catch it)."""
|
||||
init = _infra_init()
|
||||
tag = hashlib.sha256(init.encode()).hexdigest()[:8]
|
||||
return util.build_base_rootfs_dir(
|
||||
_INFRA_IMAGE, variant="-infra", init_script=_infra_init(),
|
||||
_INFRA_IMAGE, variant=f"-infra-{tag}", init_script=init,
|
||||
)
|
||||
|
||||
|
||||
@@ -278,8 +284,14 @@ class SshGatewayTransport:
|
||||
f"infra gateway exec {argv!r} failed: {proc.stderr.strip()}")
|
||||
|
||||
def cp_into(self, src: str, dest: str) -> None:
|
||||
# Preserve the source mode (docker cp does): the access-hook is staged
|
||||
# 0700 and git-http execs it directly — a plain `cat >` would land it
|
||||
# 0644 and the exec fails with EACCES; keys stay 0600.
|
||||
mode = stat.S_IMODE(os.stat(src).st_mode)
|
||||
q = shlex.quote(dest)
|
||||
proc = subprocess.run(
|
||||
util.ssh_base_argv(self._key, self._ip) + [f"cat > {shlex.quote(dest)}"],
|
||||
util.ssh_base_argv(self._key, self._ip)
|
||||
+ [f"cat > {q} && chmod {mode:o} {q}"],
|
||||
input=Path(src).read_bytes(), capture_output=True, timeout=30, check=False,
|
||||
)
|
||||
if proc.returncode != 0:
|
||||
@@ -363,8 +375,11 @@ cd /app
|
||||
BOT_BOTTLE_ROOT=/var/lib/bot-bottle python3 -m bot_bottle.orchestrator \\
|
||||
--host 0.0.0.0 --port {CONTROL_PLANE_PORT} --broker stub &
|
||||
|
||||
# Gateway data plane (egress / git-gate / supervise), multi-tenant: each
|
||||
# request resolves source-IP -> policy against the local control plane.
|
||||
# Gateway data plane, multi-tenant: each request resolves source-IP ->
|
||||
# policy against the local control plane. The VM backend reaches git over
|
||||
# git-http (9420), so the git:// daemon (git-gate, needs a per-bottle
|
||||
# entrypoint the consolidated model doesn't use) is left out.
|
||||
BOT_BOTTLE_GATEWAY_DAEMONS=egress,git-http,supervise \\
|
||||
BOT_BOTTLE_ORCHESTRATOR_URL=http://127.0.0.1:{CONTROL_PLANE_PORT} \\
|
||||
python3 /app/gateway_init.py &
|
||||
|
||||
|
||||
@@ -31,7 +31,8 @@ class TestBuildInfraRootfs(unittest.TestCase):
|
||||
infra_vm.build_infra_rootfs_dir()
|
||||
build.assert_called_once()
|
||||
self.assertEqual(infra_vm._INFRA_IMAGE, build.call_args.args[0])
|
||||
self.assertEqual("-infra", build.call_args.kwargs["variant"])
|
||||
# variant is "-infra-<init-hash>" so an init change rebuilds the rootfs.
|
||||
self.assertTrue(build.call_args.kwargs["variant"].startswith("-infra-"))
|
||||
# The init runs BOTH the control plane and the gateway data plane,
|
||||
# and exports PATH so gateway_init's subprocess daemons find python3.
|
||||
init = build.call_args.kwargs["init_script"]
|
||||
@@ -40,6 +41,31 @@ class TestBuildInfraRootfs(unittest.TestCase):
|
||||
self.assertIn("export PATH=", init)
|
||||
# Persistent registry volume mounted at the DB dir before the CP starts.
|
||||
self.assertIn("/dev/vdb", init)
|
||||
# VM backend uses git-http (9420); the git:// daemon is left out.
|
||||
self.assertIn("BOT_BOTTLE_GATEWAY_DAEMONS=egress,git-http,supervise", init)
|
||||
|
||||
|
||||
class TestSshGatewayTransport(unittest.TestCase):
|
||||
def test_cp_into_preserves_source_mode(self):
|
||||
import os
|
||||
import tempfile
|
||||
from subprocess import CompletedProcess
|
||||
with tempfile.NamedTemporaryFile() as f:
|
||||
os.chmod(f.name, 0o700) # like the staged access-hook
|
||||
t = infra_vm.SshGatewayTransport(Path("/k"), "10.0.0.1")
|
||||
with patch.object(infra_vm.subprocess, "run",
|
||||
return_value=CompletedProcess([], 0)) as run:
|
||||
t.cp_into(f.name, "/etc/git-gate/access-hook")
|
||||
remote_cmd = run.call_args.args[0][-1]
|
||||
self.assertIn("chmod 700", remote_cmd) # exec bit preserved over SSH
|
||||
|
||||
def test_exec_raises_on_failure(self):
|
||||
from subprocess import CompletedProcess
|
||||
t = infra_vm.SshGatewayTransport(Path("/k"), "10.0.0.1")
|
||||
with patch.object(infra_vm.subprocess, "run",
|
||||
return_value=CompletedProcess([], 1, stderr="nope")), \
|
||||
self.assertRaises(infra_vm.GatewayProvisionError):
|
||||
t.exec(["mkdir", "-p", "/git-gate"])
|
||||
|
||||
|
||||
class TestRegistryVolume(unittest.TestCase):
|
||||
|
||||
Reference in New Issue
Block a user