fix(gateway): persist git-gate state across gateway restarts
Per-bottle git-gate state (bare repos under /git/<id>, deploy creds under /git-gate/creds/<id>) was provisioned once at bottle launch and lived only in the gateway's ephemeral storage. A gateway rebuild/restart wiped it and nothing re-provisioned already-running bottles, so their agents 404'd on fetch/push. Same class of bug as the CA (#510); the orchestrator restores only egress tokens, not git-gate declarations. Persist the state on both backends, mirroring the CA-persistence approach: - firecracker: attach a second persistent data drive (/dev/vdc) to the gateway VM and bind-mount its git/ + creds/ subdirs onto /git and /git-gate/creds in the gateway guest init, before the data plane starts. Generalize the VM config to a stable-ordered data_drives tuple (CA=vdb, git=vdc; orchestrator registry stays vdb). - docker: bind-mount host dirs (host_gateway_git_dir / creds_dir, under the never-pruned app-data root) onto /git and /git-gate/creds, with BOT_BOTTLE_DOCKER_GIT_MOUNT / _CREDS_MOUNT env overrides so CI isolates them to per-run volumes it cleans up. Teardown already rm -rf's /git/<id> + creds, so the persistent store self-cleans over the normal lifecycle. Closes #512 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -60,9 +60,12 @@ class TestFirecrackerGatewayConnect(unittest.TestCase):
|
||||
gw = FirecrackerGateway()
|
||||
booted = infra_vm.InfraVm(guest_ip="10.243.255.3", private_key=Path("/k"))
|
||||
ca_vol = Path("/gw/gateway-ca.ext4")
|
||||
git_vol = Path("/gw/gateway-git.ext4")
|
||||
with patch.object(infra_vm, "boot_vm", return_value=booted) as boot, \
|
||||
patch.object(FirecrackerGateway, "_ensure_ca_volume",
|
||||
return_value=ca_vol), \
|
||||
patch.object(FirecrackerGateway, "_ensure_git_volume",
|
||||
return_value=git_vol), \
|
||||
patch.object(infra_vm, "push_secret") as push:
|
||||
gw.connect_to_orchestrator(_ORCH_URL, _TOKEN)
|
||||
# Booted on the gateway link with the gateway role; the orchestrator's
|
||||
@@ -70,9 +73,9 @@ class TestFirecrackerGatewayConnect(unittest.TestCase):
|
||||
kw = boot.call_args.kwargs
|
||||
self.assertEqual("gateway", kw["role"])
|
||||
self.assertIn("bb_orch=10.243.255.1", kw["extra_boot_args"])
|
||||
# The persistent CA volume rides as /dev/vdb so the mitmproxy CA
|
||||
# survives a gateway-VM rebuild (issue #450).
|
||||
self.assertEqual(ca_vol, kw.get("data_drive"))
|
||||
# The persistent volumes ride in a FIXED order — CA as /dev/vdb, git-gate
|
||||
# as /dev/vdc — so both survive a gateway-VM rebuild (issues #450, #512).
|
||||
self.assertEqual((ca_vol, git_vol), kw.get("data_drives"))
|
||||
# The host-minted token (never the key — #469) is pushed to the guest.
|
||||
push.assert_called_once()
|
||||
self.assertEqual(_TOKEN, push.call_args.args[1])
|
||||
@@ -107,6 +110,34 @@ class TestGatewayCaVolume(unittest.TestCase):
|
||||
self.assertIn(str(out), argv)
|
||||
|
||||
|
||||
class TestGatewayGitVolume(unittest.TestCase):
|
||||
"""The persistent git-gate volume that keeps per-bottle bare repos + creds
|
||||
stable across a gateway-VM rebuild (issue #512)."""
|
||||
|
||||
def test_reuses_existing_volume(self) -> None:
|
||||
gw = FirecrackerGateway()
|
||||
with tempfile.TemporaryDirectory() as td:
|
||||
vol = Path(td) / "gateway-git.ext4"
|
||||
vol.write_bytes(b"") # already present
|
||||
with patch.object(infra_vm, "_gw_dir", return_value=Path(td)), \
|
||||
patch(f"{_GW}.subprocess.run") as run:
|
||||
out = gw._ensure_git_volume()
|
||||
run.assert_not_called() # no mke2fs when it exists — the repos survive
|
||||
self.assertEqual(vol, out)
|
||||
|
||||
def test_creates_volume_when_missing(self) -> None:
|
||||
gw = FirecrackerGateway()
|
||||
with tempfile.TemporaryDirectory() as td:
|
||||
with patch.object(infra_vm, "_gw_dir", return_value=Path(td)), \
|
||||
patch(f"{_GW}.subprocess.run",
|
||||
return_value=CompletedProcess([], 0)) as run:
|
||||
out = gw._ensure_git_volume()
|
||||
argv = run.call_args.args[0]
|
||||
self.assertIn("mke2fs", argv)
|
||||
self.assertEqual(str(Path(td) / "gateway-git.ext4"), out.__fspath__())
|
||||
self.assertIn(str(out), argv)
|
||||
|
||||
|
||||
class TestFirecrackerGatewaySurface(unittest.TestCase):
|
||||
def test_is_running_reads_the_gateway_pidfile(self) -> None:
|
||||
gw = FirecrackerGateway()
|
||||
|
||||
Reference in New Issue
Block a user