build agents only from internal/meta; delete the hive's local meta flake
The hive no longer renders, locks or commits a meta flake. A rebuild's MetaSync node picks the internal/meta commit and records it with the hyperhive and config pins it holds; Prebuild, Swap and Create build from it, and a head that does not pin the agent's current config fails the build naming the agent. hive-priv's meta_rev is required. Removed: meta.rs sync/lock/render, the manual-rebuild relock, the boot sweep's hyperhive bump (MetaLock), the update-meta-inputs form and its panel, MetaWindow, the hive's hyperhive_rev setting. Kept: agent_set::refresh holds what sync_agents also did (topology.json, the gateway's agent-socket map and nginx include), called from spawn, destroy, rebuild and the migration. The boot reconcile and the stale-start upgrade compare the agent's marker with the hyperhive pin of internal/meta; the dashboard's deployed chip reads the config marker a successful build writes.
This commit is contained in:
parent
cea4827918
commit
4aa11b3a94
1 changed files with 1042 additions and 3793 deletions
|
|
@ -1,15 +1,20 @@
|
|||
//! The `internal/meta` commit a hive builds an agent from.
|
||||
//!
|
||||
//! `internal/meta` is the swarm's meta flake, committed by swarm-controller.
|
||||
//! A rebuild's `MetaSync` node resolves its head once ([`resolve`]) and
|
||||
//! records it ([`record`]); `Prebuild` and `Swap` read the record
|
||||
//! ([`recorded`]), so both build the same flake. A create resolves and uses
|
||||
//! the rev directly. No rev, whether because the repo or its commit is absent
|
||||
//! or because the commit does not pin this agent's current config, leaves the
|
||||
//! build on the hive's own meta flake.
|
||||
//! `internal/meta` is the swarm's meta flake, committed by swarm-controller;
|
||||
//! a hive has no meta flake of its own. A rebuild's `MetaSync` node resolves
|
||||
//! the head ([`resolve`]) and records it ([`record`]); `Prebuild` and `Swap`
|
||||
//! read the record ([`recorded`]), so both build the same flake. A create
|
||||
//! resolves and uses the commit directly. When the head does not pin the
|
||||
//! agent's current config, the build fails rather than falling back: the
|
||||
//! swarm re-pins before it asks a hive to deploy.
|
||||
//!
|
||||
//! The record also names the pins a successful build deploys. [`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};
|
||||
use anyhow::{Context, Result, bail};
|
||||
use hive_priv_sock::{meta_flake_url, 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.
|
||||
|
|
@ -26,118 +31,197 @@ pub(crate) trait MetaSource {
|
|||
async fn config_head(&self, agent: &str) -> Result<Option<String>>;
|
||||
}
|
||||
|
||||
/// The `internal/meta` commit to build `agent` from, or `None` for the hive's
|
||||
/// own flake.
|
||||
/// 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 build `agent` from.
|
||||
///
|
||||
/// `internal/meta` is private, so a hive whose nix cannot authenticate
|
||||
/// ([`MetaSource::nix_can_fetch`]) builds from its own flake. 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.
|
||||
/// ([`MetaSource::nix_can_fetch`]) cannot build. 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
|
||||
/// A forge read that failed.
|
||||
/// 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<Option<String>> {
|
||||
) -> Result<MetaBuild> {
|
||||
if !source.nix_can_fetch() {
|
||||
return Ok(None);
|
||||
bail!("{agent}: this hive has no forge credential for nix to fetch internal/meta with");
|
||||
}
|
||||
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);
|
||||
};
|
||||
if !valid_meta_rev(&rev) {
|
||||
return Ok(None);
|
||||
}
|
||||
let flake = source.flake_at(&rev).await?;
|
||||
let config_head = if archetype.is_some() {
|
||||
None
|
||||
} else {
|
||||
let Some(head) = source.config_head(agent).await? else {
|
||||
return Ok(None);
|
||||
};
|
||||
Some(head)
|
||||
};
|
||||
Ok(
|
||||
hive_meta_flake::builds_agent_at(&flake, agent, archetype, config_head.as_deref())
|
||||
.then_some(rev),
|
||||
)
|
||||
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()))
|
||||
}
|
||||
|
||||
/// [`choose`] against the forge this hive reads. `None`, logged, whenever the
|
||||
/// hive cannot tell: no forge credential, a failed read, no usable commit.
|
||||
pub(crate) async fn resolve(agent: &str) -> Option<String> {
|
||||
let archetype = match crate::archetypes::of(agent) {
|
||||
Ok(archetype) => archetype,
|
||||
Err(e) => {
|
||||
tracing::warn!(%agent, error = ?e, "internal/meta: cannot read the agent's archetype; building from the hive's own flake");
|
||||
return None;
|
||||
}
|
||||
};
|
||||
/// 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!(%agent, "internal/meta: no forge read token; building from the hive's own flake");
|
||||
tracing::info!("internal/meta: no forge read token; cannot tell which hyperhive it pins");
|
||||
return None;
|
||||
};
|
||||
match choose(&source, agent, archetype.as_ref()).await {
|
||||
Ok(Some(rev)) => {
|
||||
tracing::info!(%agent, %rev, "internal/meta: building from the swarm's meta flake");
|
||||
Some(rev)
|
||||
}
|
||||
Ok(None) => {
|
||||
tracing::info!(%agent, "internal/meta: absent or not pinning this agent's config; building from the hive's own flake");
|
||||
None
|
||||
}
|
||||
match hyperhive_pin(&source).await {
|
||||
Ok(pin) => pin,
|
||||
Err(e) => {
|
||||
tracing::warn!(%agent, error = %format!("{e:#}"), "internal/meta: cannot read it; building from the hive's own flake");
|
||||
tracing::warn!(error = %format!("{e:#}"), "internal/meta: cannot read its hyperhive pin");
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Record `rev` as the commit `agent`'s build reads, or clear the record for
|
||||
/// `None`. The record is in hive-c0re's own state ([`crate::paths::meta_rev_dir`]),
|
||||
/// where the agent cannot write.
|
||||
/// [`choose`] against the forge this hive reads.
|
||||
///
|
||||
/// # Errors
|
||||
/// The record file could not be written or removed.
|
||||
pub(crate) fn record(agent: &str, rev: Option<&str>) -> Result<()> {
|
||||
record_in(&crate::paths::meta_rev_dir(), agent, rev)
|
||||
/// 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 commit [`record`] stored for `agent`, if it is a valid one.
|
||||
pub(crate) fn recorded(agent: &str) -> Option<String> {
|
||||
/// Record `build` as what `agent`'s rebuild builds from. 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, rev: Option<&str>) -> Result<()> {
|
||||
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);
|
||||
match rev {
|
||||
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 nothing to build from.
|
||||
///
|
||||
/// # 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::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()))
|
||||
std::fs::write(config, rev).with_context(|| format!("write {}", config.display()))
|
||||
}
|
||||
None => match std::fs::remove_file(&path) {
|
||||
None => match std::fs::remove_file(config) {
|
||||
Err(e) if e.kind() != std::io::ErrorKind::NotFound => {
|
||||
Err(e).with_context(|| format!("remove {}", path.display()))
|
||||
Err(e).with_context(|| format!("remove {}", config.display()))
|
||||
}
|
||||
_ => Ok(()),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn recorded_in(dir: &std::path::Path, agent: &str) -> Option<String> {
|
||||
let rev = std::fs::read_to_string(dir.join(agent)).ok()?;
|
||||
valid_meta_rev(rev.trim()).then(|| rev.trim().to_owned())
|
||||
/// 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 flake to build from either.
|
||||
pub(crate) fn marker_is_fresh(pin: Option<&str>, stored: Option<&str>) -> bool {
|
||||
pin.is_none_or(|pin| stored == Some(pin))
|
||||
}
|
||||
|
||||
/// The `<flake>#<agent>` ref [`crate::lifecycle::prebuild_toplevel`] builds
|
||||
|
|
@ -233,36 +317,52 @@ mod tests {
|
|||
}
|
||||
}
|
||||
|
||||
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(),
|
||||
Some(META.to_owned())
|
||||
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_not_taken() {
|
||||
assert_eq!(
|
||||
choose(&forge(Some(STALE)), "argus", None).await.unwrap(),
|
||||
None
|
||||
);
|
||||
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_not_taken() {
|
||||
async fn a_head_without_the_agent_is_an_error() {
|
||||
let mut source = forge(Some(ARGUS_HEAD));
|
||||
source.flake = source.flake.replace("argus", "someone");
|
||||
assert_eq!(choose(&source, "argus", None).await.unwrap(), None);
|
||||
}
|
||||
|
||||
fn reviewer() -> ArchetypeRef {
|
||||
ArchetypeRef {
|
||||
source: "hyperhive".to_owned(),
|
||||
name: "reviewer".to_owned(),
|
||||
}
|
||||
let err = choose(&source, "argus", None).await.unwrap_err();
|
||||
assert_unpinned(&err, "argus");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
|
|
@ -285,84 +385,120 @@ mod tests {
|
|||
let source = Archetypes(forge(None));
|
||||
assert_eq!(
|
||||
choose(&source, "rev", Some(&reviewer())).await.unwrap(),
|
||||
Some(META.to_owned())
|
||||
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_eq!(
|
||||
choose(&source, "rev", Some(&other)).await.unwrap(),
|
||||
None,
|
||||
"the head lists another archetype"
|
||||
assert_unpinned(
|
||||
&choose(&source, "rev", Some(&other)).await.unwrap_err(),
|
||||
"rev",
|
||||
);
|
||||
assert_eq!(
|
||||
choose(&source, "ghost", Some(&reviewer())).await.unwrap(),
|
||||
None
|
||||
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_not_taken() {
|
||||
async fn an_agent_switched_between_custom_and_archetype_is_an_error() {
|
||||
let source = forge(Some(ARGUS_HEAD));
|
||||
assert_eq!(
|
||||
choose(&source, "argus", Some(&reviewer())).await.unwrap(),
|
||||
None,
|
||||
"custom in the head, archetype now"
|
||||
);
|
||||
assert_eq!(
|
||||
choose(&source, "rev", None).await.unwrap(),
|
||||
None,
|
||||
"archetype in the head, custom now"
|
||||
assert_unpinned(
|
||||
&choose(&source, "argus", Some(&reviewer()))
|
||||
.await
|
||||
.unwrap_err(),
|
||||
"argus",
|
||||
);
|
||||
assert_unpinned(&choose(&source, "rev", None).await.unwrap_err(), "rev");
|
||||
}
|
||||
|
||||
/// A hive without the credential nix's git reads builds from its own flake
|
||||
/// even though its own token reads the repo; the first test is the control.
|
||||
/// A hive without the credential nix's git reads cannot build from the
|
||||
/// repo even though its own token reads it; the first test is the control.
|
||||
#[tokio::test]
|
||||
async fn a_hive_whose_nix_cannot_authenticate_builds_from_its_own_flake() {
|
||||
async fn a_hive_whose_nix_cannot_authenticate_cannot_build() {
|
||||
let mut source = forge(Some(ARGUS_HEAD));
|
||||
source.fetchable = false;
|
||||
assert_eq!(choose(&source, "argus", None).await.unwrap(), None);
|
||||
let err = choose(&source, "argus", None).await.unwrap_err();
|
||||
assert!(
|
||||
format!("{err:#}").contains("no forge credential"),
|
||||
"{err:#}"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn no_repo_or_no_commit_is_the_hives_own_flake() {
|
||||
async fn no_repo_or_no_commit_is_an_error_naming_the_agent() {
|
||||
let mut source = forge(Some(ARGUS_HEAD));
|
||||
source.head = None;
|
||||
assert_eq!(choose(&source, "argus", None).await.unwrap(), 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_the_hives_own_flake() {
|
||||
assert_eq!(choose(&forge(None), "argus", None).await.unwrap(), None);
|
||||
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_cleared() {
|
||||
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", Some(META)).unwrap();
|
||||
assert_eq!(recorded_in(dir.path(), "argus"), Some(META.to_owned()));
|
||||
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"
|
||||
);
|
||||
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();
|
||||
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", Some(META)).unwrap();
|
||||
record_in(&records, "argus", &build()).unwrap();
|
||||
let names: Vec<_> = std::fs::read_dir(&records)
|
||||
.unwrap()
|
||||
.map(|e| e.unwrap().file_name())
|
||||
|
|
@ -370,11 +506,33 @@ mod tests {
|
|||
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_is_not_a_commit_id_is_no_record() {
|
||||
fn a_record_that_does_not_name_commit_ids_is_no_record() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
std::fs::write(dir.path().join("argus"), "main\n").unwrap();
|
||||
assert_eq!(recorded_in(dir.path(), "argus"), None);
|
||||
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
|
||||
|
|
@ -385,6 +543,32 @@ mod tests {
|
|||
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));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_prebuild_ref_is_the_flake_hive_priv_builds() {
|
||||
assert_eq!(
|
||||
|
|
|
|||
Loading…
Reference in a new issue