//! `$FLOW_HOME` (default `~/.local/share/flow`): the pinned parts of each //! isolation tier, rootfs images by hash, scratch space and the runner's //! configuration, all described by `flow.toml`. //! //! ```toml //! [microvm] //! firecracker = { path = "bin/firecracker", blake3 = "…" } //! kernel = { path = "kernel/vmlinux", blake3 = "…" } //! init = { path = "bin/flow-init", blake3 = "…" } //! //! [runner] # only for `flow runner` //! hub = "https://hub.example:7343" //! token = "…" # a runner token from the hub's [flow] runner_tokens //! ca = "/path/to/hub-ca.pem" # when the hub's certificate is self-signed //! name = "earth-1" //! ``` use anyhow::{bail, Context, Result}; use serde::Deserialize; use std::collections::HashMap; use std::path::{Path, PathBuf}; use std::sync::Mutex; #[derive(Clone, Debug, Deserialize)] pub struct Pinned { pub path: PathBuf, pub blake3: String, } #[derive(Clone, Debug, Deserialize)] pub struct MicrovmConfig { pub firecracker: Pinned, pub kernel: Pinned, pub init: Pinned, } #[derive(Clone, Debug, Deserialize)] pub struct RunnerConfig { pub hub: String, pub token: String, pub ca: Option, pub name: Option, } #[derive(Clone, Debug, Default, Deserialize)] pub struct Config { pub microvm: Option, pub runner: Option, } pub struct Home { pub root: PathBuf, pub config: Config, /// Files already verified, by path, with the (len, mtime) they had then. verified: Mutex>, } impl Home { pub fn locate(explicit: Option<&Path>) -> Result { let root = match explicit { Some(p) => p.to_path_buf(), None => match std::env::var_os("FLOW_HOME") { Some(p) => PathBuf::from(p), None => PathBuf::from(std::env::var_os("HOME").context("HOME is not set")?).join(".local/share/flow"), }, }; let file = root.join("flow.toml"); let config = if file.is_file() { let text = std::fs::read_to_string(&file).with_context(|| format!("read {}", file.display()))?; toml::from_str(&text).with_context(|| format!("parse {}", file.display()))? } else { Config::default() }; for d in ["cas", "work", "repos"] { std::fs::create_dir_all(root.join(d)).with_context(|| format!("create {}", root.join(d).display()))?; } Ok(Home { root, config, verified: Mutex::new(HashMap::new()) }) } pub fn resolve(&self, p: &Path) -> PathBuf { if p.is_absolute() { p.to_path_buf() } else { self.root.join(p) } } /// Where a rootfs image with this hash lives. pub fn rootfs(&self, hash: &str) -> PathBuf { self.root.join("cas").join(format!("{hash}.ext4")) } /// A pinned file, checked against its BLAKE3 before use (again only /// when its size or modification time changed since). pub fn verified(&self, path: &Path, blake3_hex: &str) -> Result { let meta = std::fs::metadata(path).with_context(|| format!("{} is missing", path.display()))?; let stamp = (meta.len(), meta.modified()?); if self.verified.lock().unwrap().get(path) == Some(&stamp) { return Ok(path.to_path_buf()); } let got = hash_file(path)?; if got != blake3_hex { bail!("{} has BLAKE3 {got}, but {blake3_hex} is pinned", path.display()); } self.verified.lock().unwrap().insert(path.to_path_buf(), stamp); Ok(path.to_path_buf()) } } pub fn hash_file(path: &Path) -> Result { let mut h = blake3::Hasher::new(); h.update_reader(std::fs::File::open(path).with_context(|| format!("open {}", path.display()))?)?; Ok(h.finalize().to_hex().to_string()) }