//! A job's artifacts (spec ยง5.2): read from its disks after the microVM //! has stopped, with debugfs, so the runner needs no mount rights. use anyhow::{bail, Context, Result}; use flow_core::{Job, MountSource}; use std::path::{Path, PathBuf}; /// Largest artifact a runner takes: 512 MiB. pub const MAX_ARTIFACT_BYTES: u64 = 512 << 20; /// Where guest path `path` lives on the host: the disk image, and the path /// inside its file system. The workspace is `/src`; each `ephemeral://disk` /// mount is `mount-N.ext4` in `scratch`, numbered as `microvm::run` does. pub fn locate(job: &Job, workspace: &Path, scratch: &Path, path: &str) -> (PathBuf, String) { for (i, m) in job.mounts.iter().filter(|m| m.source == MountSource::Disk).enumerate() { if let Some(rest) = path.strip_prefix(&format!("{}/", m.target)) { return (scratch.join(format!("mount-{i}.ext4")), format!("/{rest}")); } } (workspace.to_path_buf(), path.strip_prefix("/src").unwrap_or(path).to_string()) } fn debugfs(image: &Path, request: &str) -> Result { let out = std::process::Command::new("debugfs").arg("-R").arg(request).arg(image).output().context("run debugfs (e2fsprogs)")?; if !out.status.success() { bail!("debugfs {request:?}: {}", String::from_utf8_lossy(&out.stderr).trim()); } Ok(String::from_utf8_lossy(&out.stdout).into_owned()) } /// Read regular file `inner` of ext4 image `image`, at most /// [`MAX_ARTIFACT_BYTES`]. Paths come from a checked pipeline: absolute, /// clean, and free of quotes and spaces. pub fn read(image: &Path, inner: &str, scratch: &Path) -> Result> { let stat = debugfs(image, &format!("stat \"{inner}\""))?; if !stat.contains("Type: regular") { bail!("{inner} is not a regular file in the job's disk (it may be missing or a link)"); } let size: u64 = stat .split("Size: ") .nth(1) .and_then(|s| s.split_whitespace().next()) .and_then(|s| s.parse().ok()) .context("debugfs gave no size")?; if size > MAX_ARTIFACT_BYTES { bail!("{inner} is {} MiB; artifacts are at most {} MiB", size >> 20, MAX_ARTIFACT_BYTES >> 20); } let dest = scratch.join("artifact.out"); let _ = std::fs::remove_file(&dest); debugfs(image, &format!("dump \"{inner}\" \"{}\"", dest.display()))?; let bytes = std::fs::read(&dest).with_context(|| format!("{inner}: debugfs wrote nothing"))?; let _ = std::fs::remove_file(&dest); if bytes.len() as u64 != size { bail!("{inner}: read {} bytes of {size}", bytes.len()); } Ok(bytes) } #[cfg(test)] mod tests { use super::*; use flow_core::Mount; fn job() -> Job { Job { name: "build".into(), sandbox: "rust".into(), scope: vec![], depends_on: vec![], env: Default::default(), exec: vec!["make".into()], mounts: vec![ Mount { source: MountSource::Tmpfs, target: "/src/cache".into(), size_mib: 64 }, Mount { source: MountSource::Disk, target: "/src/target".into(), size_mib: 1024 }, Mount { source: MountSource::Disk, target: "/src/work".into(), size_mib: 1024 }, ], outputs: vec![], runs_on: vec![], timeout_s: None, on: None, } } #[test] fn paths_are_found_on_the_disk_that_holds_them() { let (w, s) = (Path::new("/w.ext4"), Path::new("/scratch")); assert_eq!(locate(&job(), w, s, "/src/target/release/oxy"), (s.join("mount-0.ext4"), "/release/oxy".into())); assert_eq!(locate(&job(), w, s, "/src/work/a"), (s.join("mount-1.ext4"), "/a".into())); assert_eq!(locate(&job(), w, s, "/src/docs/a.1"), (w.to_path_buf(), "/docs/a.1".into())); assert_eq!(locate(&job(), w, s, "/src/targets/x"), (w.to_path_buf(), "/targets/x".into()), "a prefix is not a parent"); } #[test] fn files_are_read_back_from_an_ext4_image() { let dir = tempfile::tempdir().unwrap(); let tree = dir.path().join("tree"); std::fs::create_dir_all(tree.join("release")).unwrap(); let body: Vec = (0..200_000u32).map(|i| (i % 251) as u8).collect(); std::fs::write(tree.join("release/tool"), &body).unwrap(); std::os::unix::fs::symlink("tool", tree.join("release/link")).unwrap(); let image = dir.path().join("disk.ext4"); crate::workspace::image(&tree, &image).unwrap(); assert_eq!(read(&image, "/release/tool", dir.path()).unwrap(), body); assert!(read(&image, "/release/missing", dir.path()).is_err()); assert!(read(&image, "/release/link", dir.path()).unwrap_err().to_string().contains("not a regular file")); } }