refactor(firecracker): split the combined rootfs into per-plane artifacts

Each infra VM now boots its own rootfs instead of a shared combined image, so
the exposed gateway VM no longer carries buildah + the control-plane code it
never runs (a fatter, less-isolated exposed surface — the opposite of the plane
split's intent). The combined image bought only "one artifact"; each VM already
kept a full copy of the shared rootfs, so nothing was saved at boot.

  * orchestrator rootfs — control plane + buildah (Dockerfile.orchestrator.fc,
    FROM orchestrator); the slim gateway rootfs boots bot-bottle-gateway:latest
    directly. Dockerfile.infra.fc (the combined image) is deleted.
  * `_infra_init` splits into `_orchestrator_init` / `_gateway_init` (shared
    preamble via `_init_head`); each per-plane rootfs bakes only its role init,
    so the `bb_role` cmdline branch is gone.
  * infra_artifact is role-parametrized: a per-role package
    (bot-bottle-firecracker-<role>), version hash, URL, cache dir, and candidate
    subdir. `ensure_built` pulls both; `_expected_version` combines both markers.
  * publish_infra builds the images once, then builds + publishes an
    orchestrator and a gateway artifact under DIR/<role>/.

coverage.sh's candidate-dir plumbing is layout-agnostic (publish_infra --output
now fills DIR/<role>/, which ensure_artifact_gz reads). Full suite green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-25 15:21:03 -04:00
parent b7599ed146
commit d0d1da612e
8 changed files with 497 additions and 430 deletions
+74 -80
View File
@@ -61,155 +61,149 @@ class TestPut(unittest.TestCase):
self.assertEqual(b"abc123 rootfs\n", captured[0].data)
_ROLE = "orchestrator"
class TestPublishBundle(unittest.TestCase):
def _bundle(self, root: Path, version: str) -> None:
def _bundle(self, role_dir: Path, version: str) -> None:
payload = b"candidate"
(root / "version.txt").write_text(version + "\n")
(root / "rootfs.ext4.gz").write_bytes(payload)
role_dir.mkdir(parents=True, exist_ok=True)
(role_dir / "version.txt").write_text(version + "\n")
(role_dir / "rootfs.ext4.gz").write_bytes(payload)
digest = hashlib.sha256(payload).hexdigest()
(root / "rootfs.ext4.gz.sha256").write_text(
(role_dir / "rootfs.ext4.gz.sha256").write_text(
f"{digest} rootfs.ext4.gz\n")
def test_existing_identical_artifact_is_success(self) -> None:
with tempfile.TemporaryDirectory() as d:
root = Path(d)
self._bundle(root, "v1")
sha = (root / "rootfs.ext4.gz.sha256").read_bytes()
role_dir = Path(d) / _ROLE
self._bundle(role_dir, "v1")
sha = (role_dir / "rootfs.ext4.gz.sha256").read_bytes()
response = mock.MagicMock()
response.__enter__.return_value.read.return_value = sha
with mock.patch.object(
pub.infra_artifact, "infra_artifact_version", return_value="v1"
), mock.patch.object(
pub.urllib.request, "urlopen", return_value=response
), mock.patch.object(pub, "_put") as put:
self.assertEqual("v1", pub._publish_bundle(root, "token"))
with mock.patch.object(pub, "_role_version", return_value="v1"), \
mock.patch.object(
pub.urllib.request, "urlopen", return_value=response
), mock.patch.object(pub, "_put") as put:
self.assertEqual("v1", pub._publish_bundle(_ROLE, role_dir, "token"))
put.assert_not_called()
def test_partial_artifact_is_replaced(self) -> None:
with tempfile.TemporaryDirectory() as d:
root = Path(d)
self._bundle(root, "v1")
role_dir = Path(d) / _ROLE
self._bundle(role_dir, "v1")
missing = urllib.error.HTTPError("u", 404, "missing", Message(), None)
with mock.patch.object(
pub.infra_artifact, "infra_artifact_version", return_value="v1"
), mock.patch.object(
pub.urllib.request, "urlopen", side_effect=missing
), mock.patch.object(pub, "_delete") as delete, \
with mock.patch.object(pub, "_role_version", return_value="v1"), \
mock.patch.object(
pub.urllib.request, "urlopen", side_effect=missing
), mock.patch.object(pub, "_delete") as delete, \
mock.patch.object(pub, "_put") as put:
pub._publish_bundle(root, "token")
pub._publish_bundle(_ROLE, role_dir, "token")
self.assertEqual(3, delete.call_count)
self.assertEqual(3, put.call_count)
def test_rejects_bundle_for_different_checkout(self) -> None:
with tempfile.TemporaryDirectory() as d:
root = Path(d)
self._bundle(root, "old")
with mock.patch.object(
pub.infra_artifact, "infra_artifact_version", return_value="new"
):
role_dir = Path(d) / _ROLE
self._bundle(role_dir, "old")
with mock.patch.object(pub, "_role_version", return_value="new"):
with self.assertRaises(SystemExit) as ctx:
pub._publish_bundle(root, "token")
pub._publish_bundle(_ROLE, role_dir, "token")
self.assertIn("does not match checkout", str(ctx.exception))
def test_rejects_bad_bundle_checksum(self) -> None:
with tempfile.TemporaryDirectory() as d:
root = Path(d)
self._bundle(root, "v1")
(root / "rootfs.ext4.gz").write_bytes(b"tampered")
with mock.patch.object(
pub.infra_artifact, "infra_artifact_version", return_value="v1"
):
role_dir = Path(d) / _ROLE
self._bundle(role_dir, "v1")
(role_dir / "rootfs.ext4.gz").write_bytes(b"tampered")
with mock.patch.object(pub, "_role_version", return_value="v1"):
with self.assertRaises(SystemExit) as ctx:
pub._publish_bundle(root, "token")
pub._publish_bundle(_ROLE, role_dir, "token")
self.assertIn("checksum mismatch", str(ctx.exception))
def test_registry_lookup_failure_is_reported(self) -> None:
with tempfile.TemporaryDirectory() as d:
root = Path(d)
self._bundle(root, "v1")
role_dir = Path(d) / _ROLE
self._bundle(role_dir, "v1")
failure = urllib.error.URLError("offline")
with mock.patch.object(
pub.infra_artifact, "infra_artifact_version", return_value="v1"
), mock.patch.object(pub.urllib.request, "urlopen", side_effect=failure):
with mock.patch.object(pub, "_role_version", return_value="v1"), \
mock.patch.object(pub.urllib.request, "urlopen", side_effect=failure):
with self.assertRaises(SystemExit) as ctx:
pub._publish_bundle(root, "token")
pub._publish_bundle(_ROLE, role_dir, "token")
self.assertIn("registry unreachable", str(ctx.exception))
class TestTryDownloadPublished(unittest.TestCase):
def test_downloads_existing_artifact(self) -> None:
with tempfile.TemporaryDirectory() as d, \
mock.patch.object(pub.infra_vm, "_infra_init", return_value="init"), \
mock.patch.object(pub, "_role_version", return_value="v1"), \
mock.patch.object(
pub.infra_artifact, "infra_artifact_version", return_value="v1"
), mock.patch.object(
pub.urllib.request, "urlopen", return_value=_Resp()
), mock.patch.object(pub.infra_artifact, "_download") as download:
root = Path(d)
result = pub._try_download_published(root)
self.assertEqual(
("v1", root / "rootfs.ext4.gz", root / "rootfs.ext4.gz.sha256"),
result,
)
self.assertEqual(2, download.call_count)
role_dir = Path(d) / _ROLE
role_dir.mkdir()
result = pub._try_download_published(_ROLE, role_dir)
self.assertEqual("v1", result)
self.assertEqual("v1\n", (role_dir / "version.txt").read_text())
self.assertEqual(2, download.call_count)
def test_missing_artifact_returns_none(self) -> None:
missing = urllib.error.HTTPError("u", 404, "missing", Message(), None)
with tempfile.TemporaryDirectory() as d, mock.patch.object(
pub.urllib.request, "urlopen", side_effect=missing
):
self.assertIsNone(pub._try_download_published(Path(d)))
with tempfile.TemporaryDirectory() as d, \
mock.patch.object(pub, "_role_version", return_value="v1"), \
mock.patch.object(pub.urllib.request, "urlopen", side_effect=missing):
self.assertIsNone(pub._try_download_published(_ROLE, Path(d)))
def test_registry_http_failure_is_reported(self) -> None:
failure = urllib.error.HTTPError("u", 500, "failed", Message(), None)
with tempfile.TemporaryDirectory() as d, mock.patch.object(
pub.urllib.request, "urlopen", side_effect=failure
):
with tempfile.TemporaryDirectory() as d, \
mock.patch.object(pub, "_role_version", return_value="v1"), \
mock.patch.object(pub.urllib.request, "urlopen", side_effect=failure):
with self.assertRaises(SystemExit) as ctx:
pub._try_download_published(Path(d))
pub._try_download_published(_ROLE, Path(d))
self.assertIn("registry check failed (HTTP 500)", str(ctx.exception))
def test_registry_connection_failure_is_reported(self) -> None:
with tempfile.TemporaryDirectory() as d, mock.patch.object(
pub.urllib.request, "urlopen", side_effect=urllib.error.URLError("offline")
):
with tempfile.TemporaryDirectory() as d, \
mock.patch.object(pub, "_role_version", return_value="v1"), \
mock.patch.object(
pub.urllib.request, "urlopen",
side_effect=urllib.error.URLError("offline")):
with self.assertRaises(SystemExit) as ctx:
pub._try_download_published(Path(d))
pub._try_download_published(_ROLE, Path(d))
self.assertIn("registry unreachable", str(ctx.exception))
class TestMain(unittest.TestCase):
def test_output_builds_candidate_and_records_version(self) -> None:
def test_output_builds_a_candidate_per_role(self) -> None:
with tempfile.TemporaryDirectory() as d:
root = Path(d) / "candidate"
with mock.patch.object(
pub, "build_artifact", return_value=("v1", root / "g", root / "s")
) as build:
with mock.patch.object(pub.infra_vm, "build_infra_images_with_docker") as images, \
mock.patch.object(pub, "build_role_artifact", return_value="v1") as build:
self.assertEqual(0, pub.main(["--output", str(root)]))
build.assert_called_once_with(root)
self.assertEqual("v1\n", (root / "version.txt").read_text())
images.assert_called_once()
built_roles = {c.args[0] for c in build.call_args_list}
self.assertEqual({"orchestrator", "gateway"}, built_roles)
def test_output_reuses_published_candidate(self) -> None:
def test_output_reuses_published_candidates(self) -> None:
with tempfile.TemporaryDirectory() as d:
root = Path(d) / "candidate"
reused = ("v1", root / "rootfs.ext4.gz", root / "rootfs.ext4.gz.sha256")
with mock.patch.object(
pub, "_try_download_published", return_value=reused
) as reuse, mock.patch.object(pub, "build_artifact") as build:
with mock.patch.object(pub, "_try_download_published", return_value="v1") as reuse, \
mock.patch.object(pub.infra_vm, "build_infra_images_with_docker") as images, \
mock.patch.object(pub, "build_role_artifact") as build:
self.assertEqual(
0, pub.main(["--output", str(root), "--reuse-published"])
)
reuse.assert_called_once_with(root)
0, pub.main(["--output", str(root), "--reuse-published"]))
self.assertEqual(2, reuse.call_count) # once per role
images.assert_not_called()
build.assert_not_called()
self.assertEqual("v1\n", (root / "version.txt").read_text())
def test_publish_dir_publishes_existing_candidate(self) -> None:
def test_publish_dir_publishes_both_roles(self) -> None:
with tempfile.TemporaryDirectory() as d, \
mock.patch.object(pub.infra_artifact, "_config", return_value=("", "", "t")), \
mock.patch.object(pub, "_publish_bundle", return_value="v1") as publish:
self.assertEqual(0, pub.main(["--publish-dir", d]))
publish.assert_called_once_with(Path(d), "t")
published_roles = {c.args[0] for c in publish.call_args_list}
self.assertEqual({"orchestrator", "gateway"}, published_roles)
if __name__ == "__main__":