Export bootable environments as OCI layouts
This commit is contained in:
parent
89f3d6f012
commit
f6d3bf0e90
3 changed files with 450 additions and 6 deletions
|
|
@ -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"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue