feat(reconcile): reprovision CA, git-gate, and egress tokens on gateway bring-up (PRD 0081)
prd-number-check / require-numbered-prds (pull_request) Successful in 10s
tracker-policy-pr / check-pr (pull_request) Successful in 16s
lint / lint (push) Failing after 1m1s
test / unit (pull_request) Successful in 56s
test / image-input-builds (pull_request) Successful in 1m0s
test / integration-docker (pull_request) Failing after 3m7s
test / coverage (pull_request) Has been skipped

On a gateway cold boot, reconcile every live agent VM against the fresh
gateway: push the new CA into each agent's trust store, re-provision
git-gate repos/creds from the persisted upstreams snapshot, and restore
egress tokens. Per-bottle failures are logged and skipped rather than
aborting the whole reconcile.

New: reconcile.py — attach_bottled_agents_to_gateway, _push_ca,
_reprovision_git_gate, _guest_ip_from_config.

New: git_gate/provision.py writes upstreams.json after key provisioning
so the bring-up reconcile can reconstruct the upstream table without the
manifest.

Wired into FirecrackerInfraService.ensure_running() cold-boot path;
base.py BottleBackend gets a no-op default. Old _reprovision_running_bottles
removed from consolidated_launch.py (superseded by reconcile.py). Tests
migrated and extended.
This commit is contained in:
2026-07-28 00:28:26 +00:00
parent 314b30c013
commit d066e4032b
8 changed files with 560 additions and 85 deletions
@@ -2,7 +2,6 @@
from __future__ import annotations
import json
import subprocess
import tempfile
import unittest
@@ -107,21 +106,6 @@ class TestDockerReprovision(unittest.TestCase):
class TestFirecrackerReprovision(unittest.TestCase):
def _run_dir(self, root: Path, ip: str = "10.243.0.3") -> Path:
run_dir = root / "demo"
run_dir.mkdir()
(run_dir / "bottle_id_ed25519").write_text("key")
(run_dir / "config.json").write_text(json.dumps({
"boot-source": {"boot_args": f"root=/dev/vda ip={ip}::gw:mask::eth0:off"}
}))
return run_dir
def test_extracts_guest_ip_from_config(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
run_dir = self._run_dir(Path(tmp))
self.assertEqual("10.243.0.3", fc._guest_ip_from_config(run_dir / "config.json"))
self.assertEqual("", fc._guest_ip_from_config(run_dir / "missing.json"))
def test_persists_key_over_stdin_not_argv(self) -> None:
with patch.object(fc.util, "ssh_base_argv", return_value=["ssh", "guest"]), \
patch.object(fc.subprocess, "run", return_value=_proc()) as run:
@@ -135,29 +119,6 @@ class TestFirecrackerReprovision(unittest.TestCase):
with self.assertRaisesRegex(fc.ConsolidatedLaunchError, "denied"):
fc.persist_env_var_secret(Path("/key"), "10.0.0.1", "secret")
def test_reads_live_vm_key_and_reprovisions(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
run_dir = self._run_dir(Path(tmp))
client = Mock()
with patch.object(fc.cleanup, "live_run_dirs", return_value=(run_dir,)), \
patch.object(fc.util, "ssh_base_argv", return_value=["ssh", "guest"]), \
patch.object(fc.subprocess, "run", return_value=_proc(stdout="secret\n")), \
patch.object(fc, "reprovision_bottles", return_value=1) as restore, \
patch.object(fc, "info"):
fc._reprovision_running_bottles(client)
restore.assert_called_once_with(client, {"10.243.0.3": "secret"})
def test_unreadable_vm_is_skipped(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
run_dir = self._run_dir(Path(tmp))
client = Mock()
with patch.object(fc.cleanup, "live_run_dirs", return_value=(run_dir,)), \
patch.object(fc.util, "ssh_base_argv", return_value=["ssh", "guest"]), \
patch.object(fc.subprocess, "run", return_value=_proc(1)), \
patch.object(fc, "reprovision_bottles", return_value=0) as restore:
fc._reprovision_running_bottles(client)
restore.assert_called_once_with(client, {})
if __name__ == "__main__":
unittest.main()