read the running rev and the hyperhive flake from /etc files, not the unit
This commit is contained in:
parent
32af7e0639
commit
aed7c8451a
4 changed files with 416 additions and 85 deletions
|
|
@ -10,6 +10,7 @@ use std::process::Stdio;
|
|||
use std::sync::{Arc, Mutex};
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use crate::hyperhive_flake::HyperhiveFlake;
|
||||
use swarm_wire::builder::ArchetypesReply;
|
||||
use tokio::io::AsyncReadExt as _;
|
||||
|
||||
|
|
@ -17,8 +18,9 @@ use tokio::io::AsyncReadExt as _;
|
|||
pub struct Evaluator {
|
||||
/// The `nix` binary.
|
||||
pub nix: PathBuf,
|
||||
/// The hyperhive source this host was deployed from.
|
||||
pub hyperhive: String,
|
||||
/// The hyperhive source this host was deployed from, read for each
|
||||
/// evaluation.
|
||||
pub hyperhive: HyperhiveFlake,
|
||||
/// 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,
|
||||
|
|
@ -30,16 +32,17 @@ 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())
|
||||
let hyperhive = self.hyperhive.current();
|
||||
cmd.args(self.args(&hyperhive))
|
||||
.stdin(Stdio::null())
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped())
|
||||
.kill_on_drop(true);
|
||||
run(cmd, &self.hyperhive, self.deadline).await
|
||||
run(cmd, &hyperhive, self.deadline).await
|
||||
}
|
||||
|
||||
/// The `nix eval` arguments.
|
||||
fn args(&self) -> Vec<String> {
|
||||
/// The `nix eval` arguments, on the hyperhive source at `hyperhive`.
|
||||
fn args(&self, hyperhive: &str) -> Vec<String> {
|
||||
[
|
||||
"eval",
|
||||
"--json",
|
||||
|
|
@ -51,7 +54,7 @@ impl Evaluator {
|
|||
.into_iter()
|
||||
.map(str::to_owned)
|
||||
.chain([
|
||||
format!("{}#hyperhiveArchetypes", self.hyperhive),
|
||||
format!("{hyperhive}#hyperhiveArchetypes"),
|
||||
"--override-input".to_owned(),
|
||||
"nixpkgs".to_owned(),
|
||||
self.nixpkgs.clone(),
|
||||
|
|
@ -165,7 +168,7 @@ mod tests {
|
|||
fn evaluator() -> Evaluator {
|
||||
Evaluator {
|
||||
nix: PathBuf::from("nix"),
|
||||
hyperhive: "/nix/store/h-source".to_owned(),
|
||||
hyperhive: HyperhiveFlake::fixed("/nix/store/h-source"),
|
||||
nixpkgs: "/nix/store/n-source".to_owned(),
|
||||
deadline: swarm_wire::builder::ARCHETYPES_DEADLINE,
|
||||
}
|
||||
|
|
@ -173,7 +176,7 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn only_meta_is_evaluated_on_this_hosts_sources() {
|
||||
let args = evaluator().args();
|
||||
let args = evaluator().args("/nix/store/h-source");
|
||||
assert!(
|
||||
args.iter()
|
||||
.any(|a| a == "/nix/store/h-source#hyperhiveArchetypes")
|
||||
|
|
|
|||
159
swarm-builder/src/hyperhive_flake.rs
Normal file
159
swarm-builder/src/hyperhive_flake.rs
Normal file
|
|
@ -0,0 +1,159 @@
|
|||
//! The hyperhive source builds are made on, read from the file
|
||||
//! `swarm-builder.nix` writes it to.
|
||||
//!
|
||||
//! It is a file, not part of the unit: the store path changes with almost
|
||||
//! any commit, and a changed unit restarts the daemon and ends the build in
|
||||
//! progress. [`HyperhiveFlake::current`] reads the file again for each build
|
||||
//! and evaluation.
|
||||
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use anyhow::{Context, Result, bail};
|
||||
|
||||
/// Where `swarm-builder.nix` writes the file, in the container's `/etc`.
|
||||
pub const FILE: &str = "/etc/hyperhive/swarm-builder.json";
|
||||
|
||||
/// The hyperhive flake's store path, as the file last named it.
|
||||
#[derive(Clone)]
|
||||
pub struct HyperhiveFlake(Arc<Inner>);
|
||||
|
||||
struct Inner {
|
||||
file: PathBuf,
|
||||
/// The last value read, which [`HyperhiveFlake::current`] falls back to
|
||||
/// when the file cannot be read.
|
||||
last: Mutex<String>,
|
||||
}
|
||||
|
||||
impl HyperhiveFlake {
|
||||
/// [`FILE`] as it is now.
|
||||
///
|
||||
/// # Errors
|
||||
/// When the file is missing, unreadable, not JSON, or names no path: the
|
||||
/// daemon cannot build without a source.
|
||||
pub fn load() -> Result<Self> {
|
||||
Self::load_from(Path::new(FILE))
|
||||
}
|
||||
|
||||
fn load_from(file: &Path) -> Result<Self> {
|
||||
let initial = read(file)?;
|
||||
Ok(Self::new(file.to_owned(), initial))
|
||||
}
|
||||
|
||||
/// `initial` until `file` can be read.
|
||||
pub fn new(file: PathBuf, initial: String) -> Self {
|
||||
Self(Arc::new(Inner {
|
||||
file,
|
||||
last: Mutex::new(initial),
|
||||
}))
|
||||
}
|
||||
|
||||
/// `path`, with a file that does not exist: what a test hands code that
|
||||
/// reads the flake.
|
||||
#[cfg(test)]
|
||||
pub fn fixed(path: &str) -> Self {
|
||||
Self::new(
|
||||
PathBuf::from("/nonexistent/swarm-builder.json"),
|
||||
path.to_owned(),
|
||||
)
|
||||
}
|
||||
|
||||
/// The path the file names now. A file that cannot be read keeps the last
|
||||
/// path read, and is logged.
|
||||
///
|
||||
/// # Panics
|
||||
/// When another thread panicked while it held the stored value.
|
||||
#[must_use]
|
||||
pub fn current(&self) -> String {
|
||||
let mut last = self.0.last.lock().expect("unpoisoned");
|
||||
match read(&self.0.file) {
|
||||
Ok(path) => *last = path,
|
||||
Err(e) => {
|
||||
tracing::warn!(error = %format!("{e:#}"), "hyperhive flake file not read; using the last path");
|
||||
}
|
||||
}
|
||||
last.clone()
|
||||
}
|
||||
}
|
||||
|
||||
fn read(file: &Path) -> Result<String> {
|
||||
let raw = std::fs::read_to_string(file).with_context(|| format!("read {}", file.display()))?;
|
||||
let contents: serde_json::Value =
|
||||
serde_json::from_str(&raw).with_context(|| format!("parse {}", file.display()))?;
|
||||
match contents["hyperhive_flake"].as_str() {
|
||||
Some(path) if !path.is_empty() => Ok(path.to_owned()),
|
||||
_ => bail!("{}: hyperhive_flake is absent or empty", file.display()),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// A fresh directory for one test, removed on drop.
|
||||
struct Dir(PathBuf);
|
||||
|
||||
impl Dir {
|
||||
fn new(name: &str) -> Self {
|
||||
let dir =
|
||||
std::env::temp_dir().join(format!("hyperhive-flake-{name}-{}", std::process::id()));
|
||||
std::fs::create_dir_all(&dir).unwrap();
|
||||
Self(dir)
|
||||
}
|
||||
|
||||
fn file(&self) -> PathBuf {
|
||||
self.0.join("swarm-builder.json")
|
||||
}
|
||||
|
||||
fn write(&self, content: &str) {
|
||||
std::fs::write(self.file(), content).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for Dir {
|
||||
fn drop(&mut self) {
|
||||
let _ = std::fs::remove_dir_all(&self.0);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_path_is_read_again_each_time() {
|
||||
let dir = Dir::new("reread");
|
||||
dir.write(r#"{"hyperhive_flake":"/nix/store/a-source"}"#);
|
||||
let flake = HyperhiveFlake::load_from(&dir.file()).unwrap();
|
||||
assert_eq!(flake.current(), "/nix/store/a-source");
|
||||
dir.write(r#"{"hyperhive_flake":"/nix/store/b-source"}"#);
|
||||
assert_eq!(flake.current(), "/nix/store/b-source");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_file_that_cannot_be_used_keeps_the_last_path() {
|
||||
let dir = Dir::new("keeps");
|
||||
dir.write(r#"{"hyperhive_flake":"/nix/store/a-source"}"#);
|
||||
let flake = HyperhiveFlake::load_from(&dir.file()).unwrap();
|
||||
// Control: `the_path_is_read_again_each_time` shows a good file
|
||||
// replacing the path.
|
||||
for (name, content) in [
|
||||
("empty value", Some(r#"{"hyperhive_flake":""}"#)),
|
||||
("no key", Some("{}")),
|
||||
("not JSON", Some("not json")),
|
||||
("missing file", None),
|
||||
] {
|
||||
match content {
|
||||
Some(content) => dir.write(content),
|
||||
None => std::fs::remove_file(dir.file()).unwrap(),
|
||||
}
|
||||
assert_eq!(flake.current(), "/nix/store/a-source", "{name}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn start_fails_without_a_usable_file() {
|
||||
let dir = Dir::new("start");
|
||||
assert!(HyperhiveFlake::load_from(&dir.file()).is_err(), "missing");
|
||||
dir.write(r#"{"hyperhive_flake":""}"#);
|
||||
assert!(HyperhiveFlake::load_from(&dir.file()).is_err(), "empty");
|
||||
dir.write(r#"{"hyperhive_flake":"/nix/store/a-source"}"#);
|
||||
assert!(HyperhiveFlake::load_from(&dir.file()).is_ok(), "control");
|
||||
}
|
||||
}
|
||||
|
|
@ -25,6 +25,7 @@
|
|||
//! The store identity comes from the `BAO_*` environment. The queue client
|
||||
//! secret is read at start and again when authelia refuses it, the forge token
|
||||
//! once per build, both from the store under that identity.
|
||||
//! The hyperhive source is read again for each build ([`hyperhive_flake`]).
|
||||
|
||||
use std::future::Future;
|
||||
use std::path::PathBuf;
|
||||
|
|
@ -45,6 +46,7 @@ use swarm_wire::builder::{
|
|||
};
|
||||
|
||||
mod archetypes;
|
||||
mod hyperhive_flake;
|
||||
mod metrics;
|
||||
mod nix_build;
|
||||
mod push;
|
||||
|
|
@ -96,7 +98,7 @@ async fn main() -> Result<()> {
|
|||
let builder = Arc::new(nix_build::Builder {
|
||||
nix: PathBuf::from(var("NIX")?),
|
||||
check_flake: var("CHECK_FLAKE")?,
|
||||
hyperhive: var("HYPERHIVE_FLAKE")?,
|
||||
hyperhive: hyperhive_flake::HyperhiveFlake::load()?,
|
||||
nixpkgs: var("NIXPKGS_FLAKE")?,
|
||||
forge_url: forge_url()?,
|
||||
config_org: var("CONFIG_ORG")?,
|
||||
|
|
@ -1005,7 +1007,7 @@ mod tests {
|
|||
Arc::new(nix_build::Builder {
|
||||
nix: self.0.join("nix"),
|
||||
check_flake: "/nix/store/c-check".to_owned(),
|
||||
hyperhive: "/nix/store/h-source".to_owned(),
|
||||
hyperhive: hyperhive_flake::HyperhiveFlake::fixed("/nix/store/h-source"),
|
||||
nixpkgs: "/nix/store/n-source".to_owned(),
|
||||
forge_url: "https://forge.example".to_owned(),
|
||||
config_org: "agent-configs".to_owned(),
|
||||
|
|
@ -1694,7 +1696,7 @@ mod tests {
|
|||
fn evaluator(fake: &Fake) -> archetypes::Evaluator {
|
||||
archetypes::Evaluator {
|
||||
nix: fake.0.join("nix"),
|
||||
hyperhive: "/nix/store/h-source".to_owned(),
|
||||
hyperhive: hyperhive_flake::HyperhiveFlake::fixed("/nix/store/h-source"),
|
||||
nixpkgs: "/nix/store/n-source".to_owned(),
|
||||
deadline: Duration::from_secs(30),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ use std::path::PathBuf;
|
|||
use std::process::Stdio;
|
||||
use std::time::Duration;
|
||||
|
||||
use crate::hyperhive_flake::HyperhiveFlake;
|
||||
use hive_types::NixBuilds;
|
||||
use swarm_wire::builder::{
|
||||
BuildAgent, BuildFromMeta, FromMetaReply, LockMeta, LockReply, META_REPO_PATH, Reply,
|
||||
|
|
@ -46,8 +47,8 @@ pub struct Builder {
|
|||
pub nix: PathBuf,
|
||||
/// The check flake's directory in the store.
|
||||
pub check_flake: String,
|
||||
/// The hyperhive source this host was deployed from.
|
||||
pub hyperhive: String,
|
||||
/// The hyperhive source this host was deployed from, read for each build.
|
||||
pub hyperhive: HyperhiveFlake,
|
||||
/// The nixpkgs source this host was deployed from.
|
||||
pub nixpkgs: String,
|
||||
/// `https://<forge>`, with no trailing slash and no path.
|
||||
|
|
@ -159,7 +160,7 @@ impl Builder {
|
|||
format!("{}#check", self.check_flake),
|
||||
"--override-input".to_owned(),
|
||||
"hyperhive".to_owned(),
|
||||
self.hyperhive.clone(),
|
||||
self.hyperhive.current(),
|
||||
"--override-input".to_owned(),
|
||||
"nixpkgs".to_owned(),
|
||||
self.nixpkgs.clone(),
|
||||
|
|
@ -525,7 +526,7 @@ mod tests {
|
|||
fn builder() -> Builder {
|
||||
Builder {
|
||||
check_flake: "/nix/store/c-check".to_owned(),
|
||||
hyperhive: "/nix/store/h-source".to_owned(),
|
||||
hyperhive: HyperhiveFlake::fixed("/nix/store/h-source"),
|
||||
nixpkgs: "/nix/store/n-source".to_owned(),
|
||||
forge_url: "https://forge.example".to_owned(),
|
||||
config_org: "agent-configs".to_owned(),
|
||||
|
|
|
|||
Loading…
Reference in a new issue