Watch
0
0
Fork
You've already forked hyperhive
0
hyperhive/swarm-builder/src/archetypes.rs

189 lines
6.1 KiB
Rust

//! One archetype evaluation: `nix eval` of the `meta` of every entry in this
//! host's hyperhive source's `hyperhiveArchetypes` output, as JSON.
//!
//! Only `meta` is evaluated, never `module`: `meta` is plain data, so the
//! evaluation builds nothing. Both inputs are store paths, so it fetches
//! nothing and needs no forge token.
use std::path::PathBuf;
use std::process::Stdio;
use std::time::Duration;
use swarm_wire::builder::ArchetypesReply;
/// What every evaluation is run with, fixed at start.
pub struct Evaluator {
/// The `nix` binary.
pub nix: PathBuf,
/// The hyperhive source this host was deployed from.
pub hyperhive: String,
/// The nixpkgs source this host was deployed from, which the hyperhive
/// source's own `nixpkgs` input is pointed at, as a hive's meta flake does.
pub nixpkgs: String,
/// How long one evaluation may run.
pub deadline: Duration,
}
impl Evaluator {
/// Evaluate the archetypes' `meta`.
pub async fn eval(&self) -> ArchetypesReply {
let mut cmd = tokio::process::Command::new(&self.nix);
cmd.args(self.args())
.stdin(Stdio::null())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.kill_on_drop(true);
run(cmd, &self.hyperhive, self.deadline).await
}
/// The `nix eval` arguments.
fn args(&self) -> Vec<String> {
[
"eval",
"--json",
"--no-write-lock-file",
"--option",
"accept-flake-config",
"false",
]
.into_iter()
.map(str::to_owned)
.chain([
format!("{}#hyperhiveArchetypes", self.hyperhive),
"--override-input".to_owned(),
"nixpkgs".to_owned(),
self.nixpkgs.clone(),
"--apply".to_owned(),
"builtins.mapAttrs (_: a: a.meta)".to_owned(),
])
.collect()
}
}
/// Run `cmd` to its end or `deadline`, and read the JSON object it printed or
/// the tail of what it said.
async fn run(
mut cmd: tokio::process::Command,
source: &str,
deadline: Duration,
) -> ArchetypesReply {
let failed = |error_tail: String| ArchetypesReply::Failed { error_tail };
let output = match tokio::time::timeout(deadline, cmd.output()).await {
Err(_) => {
return failed(format!(
"the evaluation did not finish within {} seconds",
deadline.as_secs()
));
}
Ok(Err(e)) => return failed(format!("starting nix: {e}")),
Ok(Ok(output)) => output,
};
if !output.status.success() {
return failed(crate::nix_build::tail(
&String::from_utf8_lossy(&output.stderr),
output.status,
));
}
match serde_json::from_slice::<serde_json::Value>(&output.stdout) {
Ok(serde_json::Value::Object(archetypes)) => ArchetypesReply::Evaluated {
source: source.to_owned(),
archetypes,
},
Ok(other) => failed(format!(
"hyperhiveArchetypes is not an attribute set of archetypes: {other}"
)),
Err(e) => failed(format!("nix printed no JSON: {e}")),
}
}
#[cfg(test)]
mod tests {
use super::*;
fn evaluator() -> Evaluator {
Evaluator {
nix: PathBuf::from("nix"),
hyperhive: "/nix/store/h-source".to_owned(),
nixpkgs: "/nix/store/n-source".to_owned(),
deadline: swarm_wire::builder::ARCHETYPES_DEADLINE,
}
}
#[test]
fn only_meta_is_evaluated_on_this_hosts_sources() {
let args = evaluator().args();
assert!(
args.iter()
.any(|a| a == "/nix/store/h-source#hyperhiveArchetypes")
);
let overridden: Vec<&str> = args
.windows(2)
.filter(|w| w[0] == "--override-input")
.map(|w| w[1].as_str())
.collect();
assert_eq!(overridden, ["nixpkgs"]);
let apply = args
.windows(2)
.find(|w| w[0] == "--apply")
.map(|w| w[1].as_str());
assert_eq!(apply, Some("builtins.mapAttrs (_: a: a.meta)"));
assert!(args.iter().any(|a| a == "--no-write-lock-file"));
}
fn sh(script: &str, deadline: Duration) -> impl Future<Output = ArchetypesReply> {
let mut cmd = tokio::process::Command::new("sh");
cmd.args(["-c", script])
.stdin(Stdio::null())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.kill_on_drop(true);
run(cmd, "/nix/store/h-source", deadline)
}
#[tokio::test]
async fn an_evaluation_replies_with_each_archetypes_meta() {
let reply = sh(
r#"echo '{"reviewer":{"description":"reads"}}'"#,
Duration::from_secs(30),
)
.await;
let ArchetypesReply::Evaluated { source, archetypes } = reply else {
panic!("{reply:?}")
};
assert_eq!(source, "/nix/store/h-source");
assert_eq!(archetypes["reviewer"]["description"], "reads");
}
#[tokio::test]
async fn a_failure_replies_with_what_nix_said_last() {
let reply = sh(
"echo 'error: attribute hyperhiveArchetypes missing' >&2; exit 1",
Duration::from_secs(30),
)
.await;
assert!(
matches!(&reply, ArchetypesReply::Failed { error_tail } if error_tail.ends_with("missing")),
"{reply:?}"
);
}
#[tokio::test]
async fn output_that_is_not_an_attribute_set_is_refused() {
for script in ["echo '[1]'", "echo 'not json'"] {
let reply = sh(script, Duration::from_secs(30)).await;
assert!(
matches!(reply, ArchetypesReply::Failed { .. }),
"{script}: {reply:?}"
);
}
}
#[tokio::test]
async fn an_evaluation_past_its_deadline_is_stopped() {
let reply = sh("sleep 30", Duration::from_millis(200)).await;
assert!(
matches!(&reply, ArchetypesReply::Failed { error_tail } if error_tail.contains("did not finish")),
"{reply:?}"
);
}
}