hive-priv applies only the path its last RealiseClosure returned for that agent. On drift the swap and create record the pins of the commit the closure was built from.
606 lines
22 KiB
Rust
606 lines
22 KiB
Rust
//! The `internal/meta` commit a hive deploys an agent from.
|
|
//!
|
|
//! `internal/meta` is the swarm's meta flake, committed by swarm-controller;
|
|
//! a hive has no meta flake of its own and builds nothing from it. A rebuild's
|
|
//! `MetaSync` node resolves the head ([`resolve`]) and records it ([`record`]);
|
|
//! `Prebuild` and `Swap` read the record ([`recorded`]) for the commit the
|
|
//! swarm's closure is expected to come from. A create resolves and uses the
|
|
//! commit directly. When the head does not pin the agent's current config, the
|
|
//! deploy fails rather than falling back: the swarm re-pins before it asks a
|
|
//! hive to deploy.
|
|
//!
|
|
//! The record also names the pins a successful deploy applies. [`mark_deployed`]
|
|
//! copies them to the agent's markers, which the boot reconcile
|
|
//! ([`crate::auto_update`]) and the dashboard's update chip read.
|
|
|
|
use anyhow::{Context, Result, bail};
|
|
use hive_priv_sock::valid_meta_rev;
|
|
use serde::{Deserialize, Serialize};
|
|
use swarm_queue_client::agent_archetype::ArchetypeRef;
|
|
|
|
/// What the forge says about `internal/meta` and an agent's config repo.
|
|
pub(crate) trait MetaSource {
|
|
/// The default branch's head commit of `internal/meta`; `None` when the
|
|
/// repo is absent or has no commit.
|
|
async fn meta_head(&self) -> Result<Option<String>>;
|
|
/// `internal/meta`'s `flake.nix` at `rev`.
|
|
async fn flake_at(&self, rev: &str) -> Result<String>;
|
|
/// The head of `agent-configs/<agent>`'s `main`; `None` when it has none.
|
|
async fn config_head(&self, agent: &str) -> Result<Option<String>>;
|
|
}
|
|
|
|
/// An `internal/meta` commit and the pins it holds for one agent.
|
|
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
|
|
pub(crate) struct MetaBuild {
|
|
/// The `internal/meta` commit.
|
|
pub rev: String,
|
|
/// The hyperhive commit its flake pins.
|
|
pub hyperhive: String,
|
|
/// The agent's config commit its flake pins; none for an archetype agent.
|
|
pub config: Option<String>,
|
|
}
|
|
|
|
/// The `internal/meta` commit to deploy `agent` from.
|
|
///
|
|
/// The head is taken only when its `flake.nix` builds the agent the way it is
|
|
/// wanted now: from `archetype` when that is set, otherwise from the config
|
|
/// commit the forge's `main` is at. A head that is behind (the swarm's update
|
|
/// has not run, or failed), lacks the agent or lists it under another archetype
|
|
/// would build an older module than the one being deployed.
|
|
///
|
|
/// # Errors
|
|
/// No usable head, a head that does not pin the agent's current config, or a
|
|
/// forge read that failed.
|
|
pub(crate) async fn choose(
|
|
source: &impl MetaSource,
|
|
agent: &str,
|
|
archetype: Option<&ArchetypeRef>,
|
|
) -> Result<MetaBuild> {
|
|
let Some(rev) = source.meta_head().await? else {
|
|
bail!("{agent}: the swarm has not published internal/meta yet");
|
|
};
|
|
if !valid_meta_rev(&rev) {
|
|
bail!("{agent}: internal/meta's head {rev:?} is not a commit id");
|
|
}
|
|
let flake = source.flake_at(&rev).await?;
|
|
let config = if archetype.is_some() {
|
|
None
|
|
} else {
|
|
source.config_head(agent).await?
|
|
};
|
|
if !hive_meta_flake::builds_agent_at(&flake, agent, archetype, config.as_deref()) {
|
|
bail!("{agent}: the swarm meta flake does not pin its config yet (internal/meta at {rev})");
|
|
}
|
|
let Some(hyperhive) = hive_meta_flake::pinned_rev(&flake, "hyperhive") else {
|
|
bail!("{agent}: internal/meta at {rev} pins no hyperhive commit");
|
|
};
|
|
Ok(MetaBuild {
|
|
rev,
|
|
hyperhive: hyperhive.to_owned(),
|
|
config,
|
|
})
|
|
}
|
|
|
|
/// The hyperhive commit `internal/meta`'s head pins; `None` when the repo or
|
|
/// its commit is absent.
|
|
///
|
|
/// # Errors
|
|
/// A forge read that failed, or a head that pins no hyperhive commit.
|
|
pub(crate) async fn hyperhive_pin(source: &impl MetaSource) -> Result<Option<String>> {
|
|
let Some(rev) = source.meta_head().await? else {
|
|
return Ok(None);
|
|
};
|
|
let flake = source.flake_at(&rev).await?;
|
|
let pin = hive_meta_flake::pinned_rev(&flake, "hyperhive")
|
|
.with_context(|| format!("internal/meta at {rev} pins no hyperhive commit"))?;
|
|
Ok(Some(pin.to_owned()))
|
|
}
|
|
|
|
/// The hyperhive commit `internal/meta` pins now, as this hive reads it: what
|
|
/// an agent's marker is compared with to tell it is stale. `None`, logged,
|
|
/// when the hive cannot tell (no forge token, a failed read, no commit yet),
|
|
/// which callers treat as nothing to rebuild for.
|
|
pub(crate) async fn current_hyperhive_pin() -> Option<String> {
|
|
let Some(source) = crate::forge::internal_meta_source().await else {
|
|
tracing::info!("internal/meta: no forge read token; cannot tell which hyperhive it pins");
|
|
return None;
|
|
};
|
|
match hyperhive_pin(&source).await {
|
|
Ok(pin) => pin,
|
|
Err(e) => {
|
|
tracing::warn!(error = %format!("{e:#}"), "internal/meta: cannot read its hyperhive pin");
|
|
None
|
|
}
|
|
}
|
|
}
|
|
|
|
/// [`choose`] against the forge this hive reads.
|
|
///
|
|
/// # Errors
|
|
/// As [`choose`], or this hive holds no forge token or cannot read the
|
|
/// agent's archetype.
|
|
pub(crate) async fn resolve(agent: &str) -> Result<MetaBuild> {
|
|
let archetype = crate::archetypes::of(agent)
|
|
.with_context(|| format!("{agent}: cannot read the agent's archetype"))?;
|
|
let Some(source) = crate::forge::internal_meta_source().await else {
|
|
bail!("{agent}: this hive holds no forge token to read internal/meta with");
|
|
};
|
|
let build = choose(&source, agent, archetype.as_ref()).await?;
|
|
tracing::info!(%agent, rev = %build.rev, "internal/meta: building from the swarm's meta flake");
|
|
Ok(build)
|
|
}
|
|
|
|
/// The pins `internal/meta` at `rev` holds for `agent`: the hyperhive commit and,
|
|
/// for a custom agent (`archetype` false), its config commit. This is what a
|
|
/// deploy of the swarm's closure for `rev` applies, which can differ from the
|
|
/// head [`choose`] took.
|
|
///
|
|
/// # Errors
|
|
/// `rev` is not a commit id, the forge read failed, or the flake pins no
|
|
/// hyperhive commit or, for a custom agent, no config commit.
|
|
pub(crate) async fn pins_at(
|
|
source: &impl MetaSource,
|
|
agent: &str,
|
|
archetype: bool,
|
|
rev: &str,
|
|
) -> Result<MetaBuild> {
|
|
if !valid_meta_rev(rev) {
|
|
bail!("{agent}: {rev:?} is not an internal/meta commit id");
|
|
}
|
|
let flake = source.flake_at(rev).await?;
|
|
let Some(hyperhive) = hive_meta_flake::pinned_rev(&flake, "hyperhive") else {
|
|
bail!("{agent}: internal/meta at {rev} pins no hyperhive commit");
|
|
};
|
|
let config = if archetype {
|
|
None
|
|
} else {
|
|
let Some(config) = hive_meta_flake::pinned_rev(&flake, &format!("agent-{agent}")) else {
|
|
bail!("{agent}: internal/meta at {rev} pins no config commit for it");
|
|
};
|
|
Some(config.to_owned())
|
|
};
|
|
Ok(MetaBuild {
|
|
rev: rev.to_owned(),
|
|
hyperhive: hyperhive.to_owned(),
|
|
config,
|
|
})
|
|
}
|
|
|
|
/// [`pins_at`] against the forge this hive reads.
|
|
///
|
|
/// # Errors
|
|
/// As [`pins_at`], or this hive holds no forge token or cannot read the
|
|
/// agent's archetype.
|
|
pub(crate) async fn resolve_at(agent: &str, rev: &str) -> Result<MetaBuild> {
|
|
let archetype = crate::archetypes::of(agent)
|
|
.with_context(|| format!("{agent}: cannot read the agent's archetype"))?;
|
|
let Some(source) = crate::forge::internal_meta_source().await else {
|
|
bail!("{agent}: this hive holds no forge token to read internal/meta with");
|
|
};
|
|
pins_at(&source, agent, archetype.is_some(), rev).await
|
|
}
|
|
|
|
/// Record `build` as the commit `agent`'s rebuild expects the swarm's closure
|
|
/// from, and the pins it deploys. The record is in
|
|
/// hive-c0re's own state ([`crate::paths::meta_rev_dir`]), where the agent
|
|
/// cannot write.
|
|
///
|
|
/// # Errors
|
|
/// The record file could not be written.
|
|
pub(crate) fn record(agent: &str, build: &MetaBuild) -> Result<()> {
|
|
record_in(&crate::paths::meta_rev_dir(), agent, build)
|
|
}
|
|
|
|
/// What [`record`] stored for `agent`, if it is a valid record.
|
|
pub(crate) fn recorded(agent: &str) -> Option<MetaBuild> {
|
|
recorded_in(&crate::paths::meta_rev_dir(), agent)
|
|
}
|
|
|
|
/// Written whole to a temp file beside the record and renamed over it, so a
|
|
/// 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, build: &MetaBuild) -> Result<()> {
|
|
std::fs::create_dir_all(dir).with_context(|| format!("create {}", dir.display()))?;
|
|
let path = dir.join(agent);
|
|
let tmp = dir.join(format!(".{agent}.tmp"));
|
|
std::fs::write(&tmp, serde_json::to_vec(build)?)
|
|
.with_context(|| format!("write {}", tmp.display()))?;
|
|
std::fs::rename(&tmp, &path)
|
|
.with_context(|| format!("rename {} to {}", tmp.display(), path.display()))
|
|
}
|
|
|
|
fn recorded_in(dir: &std::path::Path, agent: &str) -> Option<MetaBuild> {
|
|
let build: MetaBuild = serde_json::from_slice(&std::fs::read(dir.join(agent)).ok()?).ok()?;
|
|
let valid = valid_meta_rev(&build.rev)
|
|
&& valid_meta_rev(&build.hyperhive)
|
|
&& build.config.as_deref().is_none_or(valid_meta_rev);
|
|
valid.then_some(build)
|
|
}
|
|
|
|
/// The record of `agent`'s build, or the error naming why a `Prebuild` or
|
|
/// `Swap` has no commit to expect.
|
|
///
|
|
/// # Errors
|
|
/// No valid record: its `MetaSync` did not run, or ran in an older hive-c0re.
|
|
pub(crate) fn recorded_or_err(agent: &str) -> Result<MetaBuild> {
|
|
recorded(agent)
|
|
.with_context(|| format!("{agent}: no internal/meta commit was recorded for this build"))
|
|
}
|
|
|
|
/// Mark `agent` as running what `build` pins: the hyperhive commit for the
|
|
/// boot reconcile ([`crate::paths::applied_rev_marker`]), the config commit
|
|
/// for the dashboard's deployed chip ([`crate::paths::applied_config_marker`]).
|
|
///
|
|
/// # Errors
|
|
/// A marker could not be written or removed.
|
|
pub(crate) fn mark_deployed(agent: &str, build: &MetaBuild) -> Result<()> {
|
|
mark_deployed_in(
|
|
&crate::paths::applied_rev_marker(agent),
|
|
&crate::paths::applied_config_marker(agent),
|
|
build,
|
|
)
|
|
}
|
|
|
|
fn mark_deployed_in(
|
|
hyperhive: &std::path::Path,
|
|
config: &std::path::Path,
|
|
build: &MetaBuild,
|
|
) -> Result<()> {
|
|
std::fs::write(hyperhive, &build.hyperhive)
|
|
.with_context(|| format!("write {}", hyperhive.display()))?;
|
|
match &build.config {
|
|
Some(rev) => {
|
|
std::fs::write(config, rev).with_context(|| format!("write {}", config.display()))
|
|
}
|
|
None => match std::fs::remove_file(config) {
|
|
Err(e) if e.kind() != std::io::ErrorKind::NotFound => {
|
|
Err(e).with_context(|| format!("remove {}", config.display()))
|
|
}
|
|
_ => Ok(()),
|
|
},
|
|
}
|
|
}
|
|
|
|
/// Whether an agent's `stored` marker says it runs the hyperhive commit `pin`
|
|
/// names. With no `pin`, because the hive can't read `internal/meta`, nothing
|
|
/// is stale: a rebuild would have no commit to deploy either.
|
|
pub(crate) fn marker_is_fresh(pin: Option<&str>, stored: Option<&str>) -> bool {
|
|
pin.is_none_or(|pin| stored == Some(pin))
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
const HYPERHIVE: &str = "1111111111111111111111111111111111111111";
|
|
const META: &str = "3333333333333333333333333333333333333333";
|
|
const ARGUS_HEAD: &str = "2222222222222222222222222222222222222222";
|
|
const STALE: &str = "4444444444444444444444444444444444444444";
|
|
|
|
/// A forge holding `internal/meta` at [`META`] with the given flake, and
|
|
/// `agent-configs/argus` at `config_head`.
|
|
struct Forge {
|
|
head: Option<&'static str>,
|
|
flake: String,
|
|
config_head: Option<&'static str>,
|
|
}
|
|
|
|
impl MetaSource for Forge {
|
|
async fn meta_head(&self) -> Result<Option<String>> {
|
|
Ok(self.head.map(str::to_owned))
|
|
}
|
|
async fn flake_at(&self, rev: &str) -> Result<String> {
|
|
assert_eq!(Some(rev), self.head, "the flake is read at the head");
|
|
Ok(self.flake.clone())
|
|
}
|
|
async fn config_head(&self, _: &str) -> Result<Option<String>> {
|
|
Ok(self.config_head.map(str::to_owned))
|
|
}
|
|
}
|
|
|
|
/// `internal/meta`'s flake as swarm-controller renders it: argus pinned at
|
|
/// [`ARGUS_HEAD`], and a reviewer from an archetype.
|
|
fn flake() -> String {
|
|
use swarm_wire::agent_archetype::ArchetypeRef;
|
|
|
|
let url = |repo: &str, rev: &str| {
|
|
hive_meta_flake::pinned_git_url(&format!("https://f.t/{repo}.git"), Some(rev)).unwrap()
|
|
};
|
|
let spec = |name: &str, archetype, pin| hive_meta_flake::AgentSpec {
|
|
name: name.to_owned(),
|
|
port: hive_meta_flake::agent_web_port(name),
|
|
archetype,
|
|
pin,
|
|
};
|
|
hive_meta_flake::render_flake(
|
|
&url("hyperhive/hyperhive", HYPERHIVE),
|
|
&[
|
|
spec(
|
|
"argus",
|
|
None,
|
|
Some(hive_meta_flake::AgentPin {
|
|
url: url("agent-configs/argus", ARGUS_HEAD),
|
|
canonical_inputs: vec![],
|
|
}),
|
|
),
|
|
spec(
|
|
"rev",
|
|
Some(ArchetypeRef {
|
|
source: "hyperhive".to_owned(),
|
|
name: "reviewer".to_owned(),
|
|
}),
|
|
None,
|
|
),
|
|
],
|
|
&[],
|
|
false,
|
|
)
|
|
.unwrap()
|
|
}
|
|
|
|
fn forge(config_head: Option<&'static str>) -> Forge {
|
|
Forge {
|
|
head: Some(META),
|
|
flake: flake(),
|
|
config_head,
|
|
}
|
|
}
|
|
|
|
fn reviewer() -> ArchetypeRef {
|
|
ArchetypeRef {
|
|
source: "hyperhive".to_owned(),
|
|
name: "reviewer".to_owned(),
|
|
}
|
|
}
|
|
|
|
/// The error a build with no usable pin ends in: it names the agent and
|
|
/// says the swarm meta flake does not pin its config yet.
|
|
fn assert_unpinned(err: &anyhow::Error, agent: &str) {
|
|
let msg = format!("{err:#}");
|
|
assert!(
|
|
msg.starts_with(&format!("{agent}: "))
|
|
&& msg.contains("swarm meta flake does not pin its config yet"),
|
|
"{msg}"
|
|
);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn the_head_is_taken_when_it_pins_the_agents_current_config() {
|
|
assert_eq!(
|
|
choose(&forge(Some(ARGUS_HEAD)), "argus", None)
|
|
.await
|
|
.unwrap(),
|
|
MetaBuild {
|
|
rev: META.to_owned(),
|
|
hyperhive: HYPERHIVE.to_owned(),
|
|
config: Some(ARGUS_HEAD.to_owned()),
|
|
}
|
|
);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn a_head_behind_the_agents_config_is_an_error() {
|
|
let err = choose(&forge(Some(STALE)), "argus", None)
|
|
.await
|
|
.unwrap_err();
|
|
assert_unpinned(&err, "argus");
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn a_head_without_the_agent_is_an_error() {
|
|
let mut source = forge(Some(ARGUS_HEAD));
|
|
source.flake = source.flake.replace("argus", "someone");
|
|
let err = choose(&source, "argus", None).await.unwrap_err();
|
|
assert_unpinned(&err, "argus");
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn an_archetype_agent_is_taken_when_the_head_lists_that_archetype() {
|
|
struct Archetypes(Forge);
|
|
impl MetaSource for Archetypes {
|
|
async fn meta_head(&self) -> Result<Option<String>> {
|
|
self.0.meta_head().await
|
|
}
|
|
async fn flake_at(&self, rev: &str) -> Result<String> {
|
|
self.0.flake_at(rev).await
|
|
}
|
|
async fn config_head(&self, _: &str) -> Result<Option<String>> {
|
|
panic!("an archetype agent has no config repo to read")
|
|
}
|
|
}
|
|
let source = Archetypes(forge(None));
|
|
assert_eq!(
|
|
choose(&source, "rev", Some(&reviewer())).await.unwrap(),
|
|
MetaBuild {
|
|
rev: META.to_owned(),
|
|
hyperhive: HYPERHIVE.to_owned(),
|
|
config: None,
|
|
},
|
|
"an archetype agent pins no config"
|
|
);
|
|
let other = ArchetypeRef {
|
|
name: "planner".to_owned(),
|
|
..reviewer()
|
|
};
|
|
assert_unpinned(
|
|
&choose(&source, "rev", Some(&other)).await.unwrap_err(),
|
|
"rev",
|
|
);
|
|
assert_unpinned(
|
|
&choose(&source, "ghost", Some(&reviewer()))
|
|
.await
|
|
.unwrap_err(),
|
|
"ghost",
|
|
);
|
|
}
|
|
|
|
/// The head lists argus as a custom agent and rev as an archetype agent;
|
|
/// an agent switched either way is not what the head builds.
|
|
#[tokio::test]
|
|
async fn an_agent_switched_between_custom_and_archetype_is_an_error() {
|
|
let source = forge(Some(ARGUS_HEAD));
|
|
assert_unpinned(
|
|
&choose(&source, "argus", Some(&reviewer()))
|
|
.await
|
|
.unwrap_err(),
|
|
"argus",
|
|
);
|
|
assert_unpinned(&choose(&source, "rev", None).await.unwrap_err(), "rev");
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn the_pins_at_a_commit_are_what_its_flake_holds() {
|
|
let source = forge(Some(ARGUS_HEAD));
|
|
let argus = pins_at(&source, "argus", false, META).await.unwrap();
|
|
assert_eq!(
|
|
argus,
|
|
MetaBuild {
|
|
rev: META.to_owned(),
|
|
hyperhive: HYPERHIVE.to_owned(),
|
|
config: Some(ARGUS_HEAD.to_owned()),
|
|
}
|
|
);
|
|
// An archetype agent has no config pin of its own.
|
|
let rev = pins_at(&source, "rev", true, META).await.unwrap();
|
|
assert_eq!((rev.hyperhive.as_str(), rev.config), (HYPERHIVE, None));
|
|
// A custom agent the flake doesn't list, and a rev that is no commit id.
|
|
let err = pins_at(&source, "nobody", false, META).await.unwrap_err();
|
|
assert!(format!("{err:#}").contains("no config commit"), "{err:#}");
|
|
let err = pins_at(&source, "argus", false, "main").await.unwrap_err();
|
|
assert!(format!("{err:#}").contains("not an internal/meta commit"));
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn no_repo_or_no_commit_is_an_error_naming_the_agent() {
|
|
let mut source = forge(Some(ARGUS_HEAD));
|
|
source.head = None;
|
|
let err = choose(&source, "argus", None).await.unwrap_err();
|
|
assert!(
|
|
format!("{err:#}").starts_with("argus: the swarm has not published internal/meta"),
|
|
"{err:#}"
|
|
);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn a_custom_agent_with_no_config_commit_is_an_error() {
|
|
let err = choose(&forge(None), "argus", None).await.unwrap_err();
|
|
assert_unpinned(&err, "argus");
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn the_hyperhive_pin_is_the_one_in_the_head() {
|
|
assert_eq!(
|
|
hyperhive_pin(&forge(None)).await.unwrap().as_deref(),
|
|
Some(HYPERHIVE)
|
|
);
|
|
let mut source = forge(None);
|
|
source.head = None;
|
|
assert_eq!(hyperhive_pin(&source).await.unwrap(), None);
|
|
let mut source = forge(None);
|
|
source.flake = source.flake.replace("hyperhive.url", "other.url");
|
|
assert!(hyperhive_pin(&source).await.is_err(), "a head with no pin");
|
|
}
|
|
|
|
fn build() -> MetaBuild {
|
|
MetaBuild {
|
|
rev: META.to_owned(),
|
|
hyperhive: HYPERHIVE.to_owned(),
|
|
config: Some(ARGUS_HEAD.to_owned()),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn a_record_is_read_back_and_replaced() {
|
|
let dir = tempfile::tempdir().unwrap();
|
|
assert_eq!(recorded_in(dir.path(), "argus"), None);
|
|
record_in(dir.path(), "argus", &build()).unwrap();
|
|
assert_eq!(recorded_in(dir.path(), "argus"), Some(build()));
|
|
assert_eq!(
|
|
recorded_in(dir.path(), "rev"),
|
|
None,
|
|
"a record is one agent's"
|
|
);
|
|
let next = MetaBuild {
|
|
rev: STALE.to_owned(),
|
|
config: None,
|
|
..build()
|
|
};
|
|
record_in(dir.path(), "argus", &next).unwrap();
|
|
assert_eq!(recorded_in(dir.path(), "argus"), Some(next));
|
|
}
|
|
|
|
#[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", &build()).unwrap();
|
|
let names: Vec<_> = std::fs::read_dir(&records)
|
|
.unwrap()
|
|
.map(|e| e.unwrap().file_name())
|
|
.collect();
|
|
assert_eq!(names, ["argus"]);
|
|
}
|
|
|
|
/// Anything but a record naming commit ids is no record: a plain commit id
|
|
/// (the older format), a branch name, a bad pin.
|
|
#[test]
|
|
fn a_record_that_does_not_name_commit_ids_is_no_record() {
|
|
let dir = tempfile::tempdir().unwrap();
|
|
for bad in [
|
|
META.to_owned(),
|
|
"main\n".to_owned(),
|
|
serde_json::to_string(&MetaBuild {
|
|
rev: "main".to_owned(),
|
|
..build()
|
|
})
|
|
.unwrap(),
|
|
serde_json::to_string(&MetaBuild {
|
|
hyperhive: "main".to_owned(),
|
|
..build()
|
|
})
|
|
.unwrap(),
|
|
serde_json::to_string(&MetaBuild {
|
|
config: Some("main".to_owned()),
|
|
..build()
|
|
})
|
|
.unwrap(),
|
|
] {
|
|
std::fs::write(dir.path().join("argus"), &bad).unwrap();
|
|
assert_eq!(recorded_in(dir.path(), "argus"), None, "{bad}");
|
|
}
|
|
}
|
|
|
|
/// 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]
|
|
fn marking_deployed_writes_both_pins_and_clears_the_config_of_an_archetype_agent() {
|
|
let dir = tempfile::tempdir().unwrap();
|
|
let hyperhive = dir.path().join("hyperhive");
|
|
let config = dir.path().join("config");
|
|
mark_deployed_in(&hyperhive, &config, &build()).unwrap();
|
|
assert_eq!(std::fs::read_to_string(&hyperhive).unwrap(), HYPERHIVE);
|
|
assert_eq!(std::fs::read_to_string(&config).unwrap(), ARGUS_HEAD);
|
|
let archetype = MetaBuild {
|
|
config: None,
|
|
..build()
|
|
};
|
|
mark_deployed_in(&hyperhive, &config, &archetype).unwrap();
|
|
assert!(!config.exists(), "an archetype agent pins no config");
|
|
mark_deployed_in(&hyperhive, &config, &archetype).unwrap();
|
|
}
|
|
|
|
#[test]
|
|
fn a_marker_is_fresh_only_at_the_pin() {
|
|
assert!(marker_is_fresh(Some(HYPERHIVE), Some(HYPERHIVE)));
|
|
assert!(!marker_is_fresh(Some(HYPERHIVE), Some(STALE)), "behind");
|
|
assert!(!marker_is_fresh(Some(HYPERHIVE), None), "never built");
|
|
assert!(marker_is_fresh(None, Some(STALE)), "no pin to compare with");
|
|
assert!(marker_is_fresh(None, None));
|
|
}
|
|
}
|