feat(infra): pull artifacts pinned by package release

This commit is contained in:
2026-07-27 15:13:52 +00:00
parent 63595f123a
commit 1a9056c648
29 changed files with 621 additions and 56 deletions
+1 -1
View File
@@ -61,7 +61,7 @@ class FirecrackerInfraService(InfraService):
return url
# Clear stale/hung/OUTDATED VMs holding either link before booting fresh.
infra_vm.stop()
infra_vm.ensure_built()
infra_vm.ensure_available()
# Orchestrator first — the gateway daemons reach the control plane at
# startup. Each service owns its own boot; the orchestrator (which
# holds the signing key) mints the role-scoped `gateway` JWT for the
@@ -195,7 +195,9 @@ def _sha256_file(path: Path) -> str:
return h.hexdigest()
def ensure_artifact_gz(version: str, *, role: str) -> Path:
def ensure_artifact_gz(
version: str, *, role: str, expected_sha256: str | None = None,
) -> Path:
"""The verified, cached `rootfs.ext4.gz` for `role` at `version` —
downloading it (and its `.sha256`) once, then reusing it. Fail-closed on a
checksum mismatch: the partial is removed and we die rather than boot an
@@ -222,6 +224,11 @@ def ensure_artifact_gz(version: str, *, role: str) -> Path:
if not gz.is_file() or not sha.is_file():
die(f"infra candidate bundle is incomplete: {root}")
expected = sha.read_text().split()[0].strip().lower()
if expected_sha256 is not None and expected != expected_sha256:
die(
f"infra candidate packaged checksum mismatch ({role}) for {version}:\n"
f" packaged {expected_sha256}\n candidate {expected}"
)
actual = _sha256_file(gz)
if actual != expected:
die(
@@ -235,7 +242,13 @@ def ensure_artifact_gz(version: str, *, role: str) -> Path:
gz = root / _GZ_NAME
ok = root / ".verified"
if gz.is_file() and ok.is_file():
return gz
actual = _sha256_file(gz)
recorded = ok.read_text(encoding="utf-8").strip()
wanted = expected_sha256 or recorded
if actual == recorded == wanted:
return gz
gz.unlink(missing_ok=True)
ok.unlink(missing_ok=True)
info(f"pulling infra rootfs artifact {_package(role)}/{version}")
_download(artifact_url(version, _GZ_NAME, role=role), gz)
@@ -243,6 +256,14 @@ def ensure_artifact_gz(version: str, *, role: str) -> Path:
_download(artifact_url(version, _SHA_NAME, role=role), sha)
expected = sha.read_text().split()[0].strip().lower()
if expected_sha256 is not None and expected != expected_sha256:
gz.unlink(missing_ok=True)
sha.unlink(missing_ok=True)
die(
f"infra artifact published checksum mismatch ({role}) for {version}:\n"
f" packaged {expected_sha256}\n"
f" published {expected}"
)
actual = _sha256_file(gz)
if actual != expected:
gz.unlink(missing_ok=True)
@@ -253,15 +274,22 @@ def ensure_artifact_gz(version: str, *, role: str) -> Path:
f" actual {actual}\n"
f" refusing to boot an unverified rootfs."
)
ok.write_text("ok\n")
ok.write_text(actual + "\n")
return gz
def materialize_ext4(version: str, dest: Path, *, role: str) -> None:
def materialize_ext4(
version: str,
dest: Path,
*,
role: str,
expected_sha256: str | None = None,
) -> None:
"""Ensure the verified `role` artifact is cached, then gunzip it to `dest` —
a fresh, writable per-boot rootfs (the VM mutates it; the cached `.gz` stays
pristine). Atomic via a `.part` sibling."""
gz = ensure_artifact_gz(version, role=role)
gz = ensure_artifact_gz(
version, role=role, expected_sha256=expected_sha256)
tmp = dest.with_suffix(dest.suffix + ".part")
info(f"expanding {role} infra rootfs -> {dest}")
with gzip.open(gz, "rb") as src, open(tmp, "wb") as out:
+25 -4
View File
@@ -43,6 +43,7 @@ from pathlib import Path
from typing import Generator
from ... import resources
from ... import release_manifest
from ...log import die, info
from ..docker import util as docker_mod
from . import firecracker_vm, infra_artifact, netpool, util
@@ -108,6 +109,18 @@ def _role_version(role: str) -> str:
return infra_artifact.infra_artifact_version(role_init(role), role)
def _role_release(role: str) -> tuple[str, str | None]:
manifest = release_manifest.packaged_manifest()
if manifest is None or release_manifest.local_build_requested():
return _role_version(role), None
artifact = (
manifest.firecracker_orchestrator
if role == "orchestrator"
else manifest.firecracker_gateway
)
return artifact.version, artifact.sha256
def ensure_built() -> None:
"""Ensure both infra rootfs artifacts are available before boot.
@@ -117,11 +130,16 @@ def ensure_built() -> None:
`BOT_BOTTLE_INFRA_BUILD=local` instead builds the images from source with
host Docker (the orchestrator-fc image is `FROM` the orchestrator image, so
it must exist first) — for iterating on the Dockerfiles."""
if infra_artifact.local_build_requested():
if release_manifest.local_build_requested():
build_infra_images_with_docker()
return
for role in infra_artifact.ROLES:
infra_artifact.ensure_artifact_gz(_role_version(role), role=role)
version, expected = _role_release(role)
infra_artifact.ensure_artifact_gz(
version, role=role, expected_sha256=expected)
ensure_available = ensure_built
def build_infra_images_with_docker() -> None:
@@ -214,7 +232,9 @@ def boot_vm(
else:
# Prebuilt artifact already carries the role's build slack; expand it to
# a fresh writable rootfs for this boot.
infra_artifact.materialize_ext4(_role_version(role), rootfs, role=role)
version, expected = _role_release(role)
infra_artifact.materialize_ext4(
version, rootfs, role=role, expected_sha256=expected)
private_key, pubkey = _stable_keypair()
info(f"booting {role} VM on {slot.iface} (guest {slot.guest_ip})")
@@ -264,7 +284,8 @@ def _version_file() -> Path:
def expected_version() -> str:
"""The combined marker for the running pair: both per-plane artifact
versions, so a change to either rootfs dislodges the adopted pair."""
return " ".join(f"{role}={_role_version(role)}" for role in infra_artifact.ROLES)
return " ".join(
f"{role}={_role_release(role)[0]}" for role in infra_artifact.ROLES)
def adoptable(key: Path, url: str, want: str) -> bool: