Files
zooqueenandHanzo Dev fde9f72cc4 build: an image, built by BuildKit in its own microVM
`hanzo build [path] -t REF --push` boots a hanzo-vm microVM from a
buildkit checkpoint, starts buildkitd in it and drives it with buildctl
over a loopback forward. Both halves come from the same sha256-pinned
BuildKit release. The context, secrets and registry credentials stay on
the host and reach buildkitd through the session. `-o FILE` writes an
OCI archive; `--platform` builds other architectures under the guest's
qemu. One build, one kernel: no docker, no dockerd, and no daemon shared
between builds.

The two ends speak mutual TLS with one certificate made in the guest for
that build, so another user on the same loopback gets no worker. Measured
against a live build: no certificate, no client certificate and a foreign
certificate are all refused; the build's own identity is answered.

tests/build.rs boots the vm, builds a Dockerfile whose RUN step executes
in the guest and reads that step's output back out of the archive. It
needs a hypervisor and the network, so it runs with
`cargo test --test build -- --ignored`.

Co-authored-by: Hanzo Dev <[email protected]>
2026-09-15 11:59:39 -07:00

84 lines
3.3 KiB
Rust

//! `hanzo build` against a real BuildKit. The shipped binary boots a hanzo-vm
//! microVM, starts buildkitd in it, builds a Dockerfile whose RUN step executes
//! in the guest, and writes the image back to this machine as an OCI archive —
//! so the test fails whenever the command cannot reach BuildKit.
//!
//! It needs a hypervisor (Virtualization.framework, or /dev/kvm) and the
//! network, so `cargo test` lists it as ignored. Run it with
//! `cargo test --test build -- --ignored`.
use assert_cmd::Command;
use serde_json::Value;
use std::collections::HashMap;
use std::io::Read;
use std::path::Path;
#[test]
#[ignore = "boots a hanzo-vm microVM: cargo test --test build -- --ignored"]
fn a_dockerfile_builds_in_the_vm_and_comes_back_as_an_oci_archive() {
let dir = tempfile::tempdir().unwrap();
std::fs::write(
dir.path().join("Dockerfile"),
"FROM busybox:1.36\nCOPY note /note\nRUN cat /note > /built && uname -m >> /built\n",
)
.unwrap();
std::fs::write(dir.path().join("note"), "from the host\n").unwrap();
let archive = dir.path().join("image.tar");
Command::cargo_bin("hanzo")
.unwrap()
.args(["build", "--cpus", "2", "--memory", "2048"])
.arg(dir.path())
.args(["-t", "localhost/hanzo-build-test:e2e", "-o"])
.arg(&archive)
.assert()
.success();
// The guest runs the host's architecture, so the RUN step reports it.
let arch = if cfg!(target_arch = "aarch64") {
"aarch64"
} else {
"x86_64"
};
assert_eq!(
file_in_image(&archive, "built"),
format!("from the host\n{arch}\n")
);
}
/// Read `name` out of the image an OCI archive holds, top layer first.
fn file_in_image(archive: &Path, name: &str) -> String {
let mut blobs = HashMap::new();
let mut tar = tar::Archive::new(std::fs::File::open(archive).expect("the archive was written"));
for entry in tar.entries().unwrap() {
let mut entry = entry.unwrap();
let path = entry.path().unwrap().to_string_lossy().into_owned();
let mut bytes = Vec::new();
entry.read_to_end(&mut bytes).unwrap();
blobs.insert(path, bytes);
}
let blob = |digest: &Value| &blobs[&format!("blobs/sha256/{}", &digest.as_str().unwrap()[7..])];
let mut doc: Value = serde_json::from_slice(&blobs["index.json"]).unwrap();
// Descend through any index to the linux image manifest.
while let Some(manifests) = doc.get("manifests").and_then(Value::as_array) {
let image = manifests
.iter()
.find(|m| m["platform"]["os"].as_str().is_none_or(|os| os == "linux"))
.expect("an image manifest");
doc = serde_json::from_slice(blob(&image["digest"])).unwrap();
}
for layer in doc["layers"].as_array().unwrap().iter().rev() {
let gz = blob(&layer["digest"]);
let mut files = tar::Archive::new(flate2::read::GzDecoder::new(&gz[..]));
for entry in files.entries().unwrap() {
let mut entry = entry.unwrap();
if entry.path().unwrap().to_str() == Some(name) {
let mut text = String::new();
entry.read_to_string(&mut text).unwrap();
return text;
}
}
}
panic!("{name} is in no layer of the image");
}