feat(infra): pull artifacts pinned by package release
This commit is contained in:
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user