Export bootable environments as OCI layouts

This commit is contained in:
Thales Maciel 2026-03-08 18:17:25 -03:00
parent 89f3d6f012
commit f6d3bf0e90
3 changed files with 450 additions and 6 deletions

View file

@ -1,5 +1,6 @@
from __future__ import annotations
import argparse
import hashlib
import json
import tarfile
@ -11,7 +12,9 @@ from pyro_mcp.runtime_build import (
_build_paths,
_load_lock,
build_bundle,
export_environment_oci_layout,
generate_manifest,
main,
materialize_binaries,
stage_agent,
stage_binaries,
@ -187,3 +190,127 @@ def test_runtime_build_materializes_firecracker_release(tmp_path: Path) -> None:
assert (paths.materialized_platform_root / "bin/jailer").read_text(encoding="utf-8") == (
"real-jailer\n"
)
def test_export_environment_oci_layout_writes_boot_artifacts(tmp_path: Path) -> None:
source_dir, build_dir, bundle_dir = _make_source_tree(tmp_path)
paths = _build_paths(
source_dir=source_dir,
build_dir=build_dir,
bundle_dir=bundle_dir,
materialized_dir=tmp_path / "materialized_sources",
platform="linux-x86_64",
)
result = export_environment_oci_layout(
paths,
environment="debian:12-base",
output_dir=tmp_path / "oci_layouts",
)
layout_dir = Path(str(result["layout_dir"]))
assert layout_dir == tmp_path / "oci_layouts" / "debian_12-base"
assert json.loads((layout_dir / "oci-layout").read_text(encoding="utf-8")) == {
"imageLayoutVersion": "1.0.0"
}
index = json.loads((layout_dir / "index.json").read_text(encoding="utf-8"))
manifest_descriptor = index["manifests"][0]
assert manifest_descriptor["annotations"]["org.opencontainers.image.ref.name"] == "1.0.0"
assert manifest_descriptor["platform"] == {"os": "linux", "architecture": "amd64"}
manifest_digest = str(result["manifest_digest"]).split(":", 1)[1]
manifest = json.loads(
(layout_dir / "blobs" / "sha256" / manifest_digest).read_text(encoding="utf-8")
)
assert manifest["config"]["mediaType"] == "application/vnd.oci.image.config.v1+json"
assert len(manifest["layers"]) == 3
extracted_files: dict[str, str] = {}
for layer in manifest["layers"]:
layer_digest = str(layer["digest"]).split(":", 1)[1]
layer_path = layout_dir / "blobs" / "sha256" / layer_digest
with tarfile.open(layer_path, "r:") as archive:
members = archive.getmembers()
assert len(members) == 1
member = members[0]
extracted = archive.extractfile(member)
if extracted is None:
raise AssertionError("expected layer member content")
extracted_files[member.name] = extracted.read().decode("utf-8")
assert extracted_files["vmlinux"] == "kernel-base\n"
assert extracted_files["rootfs.ext4"] == "rootfs-base\n"
metadata = json.loads(extracted_files["environment.json"])
assert metadata["environment"] == "debian:12-base"
assert metadata["source_profile"] == "debian-base"
def test_export_environment_oci_layout_rejects_missing_profile(tmp_path: Path) -> None:
source_dir, build_dir, bundle_dir = _make_source_tree(tmp_path)
paths = _build_paths(
source_dir=source_dir,
build_dir=build_dir,
bundle_dir=bundle_dir,
materialized_dir=tmp_path / "materialized_sources",
platform="linux-x86_64",
)
lock_path = source_dir / "linux-x86_64/runtime.lock.json"
lock = json.loads(lock_path.read_text(encoding="utf-8"))
del lock["profiles"]["debian-base"]
lock_path.write_text(json.dumps(lock, indent=2) + "\n", encoding="utf-8")
with pytest.raises(RuntimeError, match="does not define source profile"):
export_environment_oci_layout(
paths,
environment="debian:12-base",
output_dir=tmp_path / "oci_layouts",
)
def test_runtime_build_main_dispatches_export(
monkeypatch: pytest.MonkeyPatch,
capsys: pytest.CaptureFixture[str],
tmp_path: Path,
) -> None:
source_dir, build_dir, bundle_dir = _make_source_tree(tmp_path)
runtime_paths = _build_paths(
source_dir=source_dir,
build_dir=build_dir,
bundle_dir=bundle_dir,
materialized_dir=tmp_path / "materialized",
platform="linux-x86_64",
)
class StubParser:
def parse_args(self) -> argparse.Namespace:
return argparse.Namespace(
command="export-environment-oci",
platform="linux-x86_64",
source_dir=str(source_dir),
build_dir=str(build_dir),
bundle_dir=str(bundle_dir),
materialized_dir=str(tmp_path / "materialized"),
environment="debian:12-base",
output_dir=str(tmp_path / "oci_layouts"),
reference="1.0.0",
)
monkeypatch.setattr("pyro_mcp.runtime_build._build_parser", lambda: StubParser())
monkeypatch.setattr(
"pyro_mcp.runtime_build._load_lock",
lambda paths: _load_lock(runtime_paths),
)
monkeypatch.setattr(
"pyro_mcp.runtime_build.export_environment_oci_layout",
lambda paths, **kwargs: {
"environment": kwargs["environment"],
"layout_dir": str(Path(kwargs["output_dir"]) / "debian_12-base"),
"manifest_digest": "sha256:test",
},
)
main()
payload = json.loads(capsys.readouterr().out)
assert payload["environment"] == "debian:12-base"
assert payload["manifest_digest"] == "sha256:test"