Watch
0
0
Fork
You've already forked hyperhive
0

keep the meta-rev record in hive-c0re's state, written by rename

on deploy, the commit a rebuild builds an agent from is recorded under
/var/lib/hyperhive/meta-rev/<agent> instead of the agent's runtime dir, which
is mounted read-write into its container. an agent can no longer pick or
redirect the record. the file is written to a temp name and renamed over.
This commit is contained in:
atlas 2026-10-24 19:20:37 +02:00
commit 4323097964
2 changed files with 65 additions and 23 deletions

View file

@ -94,29 +94,33 @@ pub(crate) async fn resolve(agent: &str) -> Option<String> {
} }
} }
/// The record file in `agent`'s runtime dir, which a reboot clears, so a
/// record never outlives the boot it was made in.
const RECORD_FILE: &str = "meta-rev";
/// Record `rev` as the commit `agent`'s build reads, or clear the record for /// Record `rev` as the commit `agent`'s build reads, or clear the record for
/// `None`. /// `None`. The record is in hive-c0re's own state ([`crate::paths::meta_rev_dir`]),
/// where the agent cannot write.
/// ///
/// # Errors /// # Errors
/// The record file could not be written or removed. /// The record file could not be written or removed.
pub(crate) fn record(agent: &str, rev: Option<&str>) -> Result<()> { pub(crate) fn record(agent: &str, rev: Option<&str>) -> Result<()> {
record_in(&crate::paths::agent_runtime_dir(agent), rev) record_in(&crate::paths::meta_rev_dir(), agent, rev)
} }
/// The commit [`record`] stored for `agent`, if it is a valid one. /// The commit [`record`] stored for `agent`, if it is a valid one.
pub(crate) fn recorded(agent: &str) -> Option<String> { pub(crate) fn recorded(agent: &str) -> Option<String> {
recorded_in(&crate::paths::agent_runtime_dir(agent)) recorded_in(&crate::paths::meta_rev_dir(), agent)
} }
fn record_in(dir: &std::path::Path, rev: Option<&str>) -> Result<()> { /// Written whole to a temp file beside the record and renamed over it, so a
let path = dir.join(RECORD_FILE); /// reader sees the old record or the new one and a crash leaves no partial
/// file.
fn record_in(dir: &std::path::Path, agent: &str, rev: Option<&str>) -> Result<()> {
let path = dir.join(agent);
match rev { match rev {
Some(rev) => { Some(rev) => {
std::fs::write(&path, rev).with_context(|| format!("write {}", path.display())) std::fs::create_dir_all(dir).with_context(|| format!("create {}", dir.display()))?;
let tmp = dir.join(format!(".{agent}.tmp"));
std::fs::write(&tmp, rev).with_context(|| format!("write {}", tmp.display()))?;
std::fs::rename(&tmp, &path)
.with_context(|| format!("rename {} to {}", tmp.display(), path.display()))
} }
None => match std::fs::remove_file(&path) { None => match std::fs::remove_file(&path) {
Err(e) if e.kind() != std::io::ErrorKind::NotFound => { Err(e) if e.kind() != std::io::ErrorKind::NotFound => {
@ -127,8 +131,8 @@ fn record_in(dir: &std::path::Path, rev: Option<&str>) -> Result<()> {
} }
} }
fn recorded_in(dir: &std::path::Path) -> Option<String> { fn recorded_in(dir: &std::path::Path, agent: &str) -> Option<String> {
let rev = std::fs::read_to_string(dir.join(RECORD_FILE)).ok()?; let rev = std::fs::read_to_string(dir.join(agent)).ok()?;
valid_meta_rev(rev.trim()).then(|| rev.trim().to_owned()) valid_meta_rev(rev.trim()).then(|| rev.trim().to_owned())
} }
@ -282,37 +286,64 @@ mod tests {
async fn a_hive_whose_nix_cannot_authenticate_builds_from_its_own_flake() { async fn a_hive_whose_nix_cannot_authenticate_builds_from_its_own_flake() {
let mut source = forge(Some(ARGUS_HEAD)); let mut source = forge(Some(ARGUS_HEAD));
source.fetchable = false; source.fetchable = false;
assert_eq!(choose(&source, "argus", false).await.unwrap(), None); assert_eq!(choose(&source, "argus", None).await.unwrap(), None);
} }
#[tokio::test] #[tokio::test]
async fn no_repo_or_no_commit_is_the_hives_own_flake() { async fn no_repo_or_no_commit_is_the_hives_own_flake() {
let mut source = forge(Some(ARGUS_HEAD)); let mut source = forge(Some(ARGUS_HEAD));
source.head = None; source.head = None;
assert_eq!(choose(&source, "argus", false).await.unwrap(), None); assert_eq!(choose(&source, "argus", None).await.unwrap(), None);
} }
#[tokio::test] #[tokio::test]
async fn a_custom_agent_with_no_config_commit_is_the_hives_own_flake() { async fn a_custom_agent_with_no_config_commit_is_the_hives_own_flake() {
assert_eq!(choose(&forge(None), "argus", false).await.unwrap(), None); assert_eq!(choose(&forge(None), "argus", None).await.unwrap(), None);
} }
#[test] #[test]
fn a_record_is_read_back_and_cleared() { fn a_record_is_read_back_and_cleared() {
let dir = tempfile::tempdir().unwrap(); let dir = tempfile::tempdir().unwrap();
assert_eq!(recorded_in(dir.path()), None); assert_eq!(recorded_in(dir.path(), "argus"), None);
record_in(dir.path(), Some(META)).unwrap(); record_in(dir.path(), "argus", Some(META)).unwrap();
assert_eq!(recorded_in(dir.path()), Some(META.to_owned())); assert_eq!(recorded_in(dir.path(), "argus"), Some(META.to_owned()));
record_in(dir.path(), None).unwrap(); assert_eq!(
assert_eq!(recorded_in(dir.path()), None); recorded_in(dir.path(), "rev"),
record_in(dir.path(), None).unwrap(); None,
"a record is one agent's"
);
record_in(dir.path(), "argus", Some(STALE)).unwrap();
assert_eq!(recorded_in(dir.path(), "argus"), Some(STALE.to_owned()));
record_in(dir.path(), "argus", None).unwrap();
assert_eq!(recorded_in(dir.path(), "argus"), None);
record_in(dir.path(), "argus", None).unwrap();
}
#[test]
fn a_record_leaves_no_temp_file_and_creates_its_dir() {
let dir = tempfile::tempdir().unwrap();
let records = dir.path().join("meta-rev");
record_in(&records, "argus", Some(META)).unwrap();
let names: Vec<_> = std::fs::read_dir(&records)
.unwrap()
.map(|e| e.unwrap().file_name())
.collect();
assert_eq!(names, ["argus"]);
} }
#[test] #[test]
fn a_record_that_is_not_a_commit_id_is_no_record() { fn a_record_that_is_not_a_commit_id_is_no_record() {
let dir = tempfile::tempdir().unwrap(); let dir = tempfile::tempdir().unwrap();
std::fs::write(dir.path().join(RECORD_FILE), "main\n").unwrap(); std::fs::write(dir.path().join("argus"), "main\n").unwrap();
assert_eq!(recorded_in(dir.path()), None); assert_eq!(recorded_in(dir.path(), "argus"), None);
}
/// The record is hive-c0re's own state, never the agent-writable runtime
/// dir bind-mounted into the container.
#[test]
fn the_record_is_outside_the_agents_runtime_dir() {
assert!(!crate::paths::meta_rev_dir().starts_with(crate::paths::runtime_root()));
assert!(crate::paths::meta_rev_dir().starts_with(crate::paths::state_root()));
} }
#[test] #[test]

View file

@ -195,6 +195,17 @@ pub fn meta_git_index_lock() -> PathBuf {
meta_root().join(".git/index.lock") meta_root().join(".git/index.lock")
} }
/// `meta-rev/` — one file per agent naming the `internal/meta` commit its
/// rebuild builds from (`crate::meta_rev`).
///
/// ⚠️ Deliberately **not** in the agent's runtime dir: that is bind-mounted
/// read-write into the container as `/run/hive`, and the commit decides which
/// flake the agent's own toplevel is built from.
#[must_use]
pub fn meta_rev_dir() -> PathBuf {
state_root().join("meta-rev")
}
/// `shared/` — the cross-agent `/shared` scratch space. A `&str`, so it /// `shared/` — the cross-agent `/shared` scratch space. A `&str`, so it
/// stays a const; [`shared_root`] wraps it. /// stays a const; [`shared_root`] wraps it.
// nix: bind-mounted into every agent container as `/shared` (the harness nix modules) — must match. // nix: bind-mounted into every agent container as `/shared` (the harness nix modules) — must match.