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:
parent
51866929d5
commit
4323097964
2 changed files with 65 additions and 23 deletions
|
|
@ -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]
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue