build an archetype agent from internal/meta only at the archetype it has now
on deploy, hive-c0re takes the internal/meta head for an archetype agent only when the head lists that same archetype, and for a custom agent only when the head lists it as custom, so a switch between the two, or to another archetype, builds from the hive's own flake until the swarm commits the new flake. the record location is described in the same docs paragraph.
This commit is contained in:
parent
4323097964
commit
4697740a7e
3 changed files with 138 additions and 55 deletions
|
|
@ -173,13 +173,16 @@ older or newer than the controller keeps building.
|
|||
|
||||
- **Choosing the commit.** The rebuild's `MetaSync` node reads the repo's
|
||||
default-branch head with the hive's read token (`forge::reader_token()`)
|
||||
and takes it when its `flake.nix` lists the agent and, for a custom agent,
|
||||
pins the config commit at the head of `agent-configs/<n>` `main` now.
|
||||
and takes it when its `flake.nix` builds the agent the way the agent needs now:
|
||||
a custom agent from the config commit at the head of `agent-configs/<n>`
|
||||
`main`, an archetype agent from the archetype it has now. An agent switched
|
||||
between custom and archetype, or to another archetype, fails that test.
|
||||
A hive without the `core` token file that nix's git reads, a repo with no
|
||||
commit, a head that lacks the agent and a head behind the config all select
|
||||
the hive's own flake. `MetaSync` records the commit under the agent's
|
||||
runtime directory; `Prebuild` and `Swap` read that record, so
|
||||
both build one flake. A first deploy resolves the commit in its `Create`
|
||||
the hive's own flake. `MetaSync` records the commit in `hive-c0re`'s own state,
|
||||
`/var/lib/hyperhive/meta-rev/<n>`, which the agent can't write (the container mounts its runtime
|
||||
directory read-write); `Prebuild` and `Swap` read that
|
||||
record, so both build one flake. A first deploy resolves the commit in its `Create`
|
||||
node, since it has no `MetaSync`.
|
||||
- **The build.** `hive-c0re` prebuilds
|
||||
`git+<forge>/internal/meta.git?rev=<commit>#nixosConfigurations.<n>…` and
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@
|
|||
|
||||
use anyhow::{Context, Result};
|
||||
use hive_priv_sock::{meta_flake_url, valid_meta_rev};
|
||||
use swarm_queue_client::agent_archetype::ArchetypeRef;
|
||||
|
||||
/// What the forge says about `internal/meta` and an agent's config repo.
|
||||
pub(crate) trait MetaSource {
|
||||
|
|
@ -30,18 +31,18 @@ pub(crate) trait MetaSource {
|
|||
///
|
||||
/// `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 from the
|
||||
/// config commit the forge's `main` is at now (`archetype`: when it lists the
|
||||
/// agent at all). A head that is behind (the swarm's update has not run, or
|
||||
/// failed) or lacks the agent would build an older config than the one being
|
||||
/// deployed.
|
||||
/// 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.
|
||||
pub(crate) async fn choose(
|
||||
source: &impl MetaSource,
|
||||
agent: &str,
|
||||
archetype: bool,
|
||||
archetype: Option<&ArchetypeRef>,
|
||||
) -> Result<Option<String>> {
|
||||
if !source.nix_can_fetch() {
|
||||
return Ok(None);
|
||||
|
|
@ -53,7 +54,7 @@ pub(crate) async fn choose(
|
|||
return Ok(None);
|
||||
}
|
||||
let flake = source.flake_at(&rev).await?;
|
||||
let config_head = if archetype {
|
||||
let config_head = if archetype.is_some() {
|
||||
None
|
||||
} else {
|
||||
let Some(head) = source.config_head(agent).await? else {
|
||||
|
|
@ -61,14 +62,17 @@ pub(crate) async fn choose(
|
|||
};
|
||||
Some(head)
|
||||
};
|
||||
Ok(hive_meta_flake::builds_agent_at(&flake, agent, config_head.as_deref()).then_some(rev))
|
||||
Ok(
|
||||
hive_meta_flake::builds_agent_at(&flake, agent, archetype, config_head.as_deref())
|
||||
.then_some(rev),
|
||||
)
|
||||
}
|
||||
|
||||
/// [`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.is_some(),
|
||||
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;
|
||||
|
|
@ -78,7 +82,7 @@ pub(crate) async fn resolve(agent: &str) -> Option<String> {
|
|||
tracing::info!(%agent, "internal/meta: no forge read token; building from the hive's own flake");
|
||||
return None;
|
||||
};
|
||||
match choose(&source, agent, archetype).await {
|
||||
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)
|
||||
|
|
@ -175,8 +179,7 @@ mod tests {
|
|||
assert_eq!(Some(rev), self.head, "the flake is read at the head");
|
||||
Ok(self.flake.clone())
|
||||
}
|
||||
async fn config_head(&self, agent: &str) -> Result<Option<String>> {
|
||||
assert_eq!(agent, "argus");
|
||||
async fn config_head(&self, _: &str) -> Result<Option<String>> {
|
||||
Ok(self.config_head.map(str::to_owned))
|
||||
}
|
||||
}
|
||||
|
|
@ -233,7 +236,7 @@ mod tests {
|
|||
#[tokio::test]
|
||||
async fn the_head_is_taken_when_it_pins_the_agents_current_config() {
|
||||
assert_eq!(
|
||||
choose(&forge(Some(ARGUS_HEAD)), "argus", false)
|
||||
choose(&forge(Some(ARGUS_HEAD)), "argus", None)
|
||||
.await
|
||||
.unwrap(),
|
||||
Some(META.to_owned())
|
||||
|
|
@ -243,7 +246,7 @@ mod tests {
|
|||
#[tokio::test]
|
||||
async fn a_head_behind_the_agents_config_is_not_taken() {
|
||||
assert_eq!(
|
||||
choose(&forge(Some(STALE)), "argus", false).await.unwrap(),
|
||||
choose(&forge(Some(STALE)), "argus", None).await.unwrap(),
|
||||
None
|
||||
);
|
||||
}
|
||||
|
|
@ -252,11 +255,18 @@ mod tests {
|
|||
async fn a_head_without_the_agent_is_not_taken() {
|
||||
let mut source = forge(Some(ARGUS_HEAD));
|
||||
source.flake = source.flake.replace("argus", "someone");
|
||||
assert_eq!(choose(&source, "argus", false).await.unwrap(), None);
|
||||
assert_eq!(choose(&source, "argus", None).await.unwrap(), None);
|
||||
}
|
||||
|
||||
fn reviewer() -> ArchetypeRef {
|
||||
ArchetypeRef {
|
||||
source: "hyperhive".to_owned(),
|
||||
name: "reviewer".to_owned(),
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn an_archetype_agent_needs_only_to_be_listed() {
|
||||
async fn an_archetype_agent_is_taken_when_the_head_lists_that_archetype() {
|
||||
struct Archetypes(Forge);
|
||||
impl MetaSource for Archetypes {
|
||||
fn nix_can_fetch(&self) -> bool {
|
||||
|
|
@ -274,10 +284,39 @@ mod tests {
|
|||
}
|
||||
let source = Archetypes(forge(None));
|
||||
assert_eq!(
|
||||
choose(&source, "rev", true).await.unwrap(),
|
||||
choose(&source, "rev", Some(&reviewer())).await.unwrap(),
|
||||
Some(META.to_owned())
|
||||
);
|
||||
assert_eq!(choose(&source, "ghost", true).await.unwrap(), None);
|
||||
let other = ArchetypeRef {
|
||||
name: "planner".to_owned(),
|
||||
..reviewer()
|
||||
};
|
||||
assert_eq!(
|
||||
choose(&source, "rev", Some(&other)).await.unwrap(),
|
||||
None,
|
||||
"the head lists another archetype"
|
||||
);
|
||||
assert_eq!(
|
||||
choose(&source, "ghost", Some(&reviewer())).await.unwrap(),
|
||||
None
|
||||
);
|
||||
}
|
||||
|
||||
/// 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() {
|
||||
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"
|
||||
);
|
||||
}
|
||||
|
||||
/// A hive without the credential nix's git reads builds from its own flake
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@
|
|||
//! builds has one renderer.
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use swarm_wire::agent_archetype::ArchetypeRef;
|
||||
|
||||
/// One agent of the meta flake.
|
||||
#[derive(Debug, Clone)]
|
||||
|
|
@ -14,7 +15,7 @@ pub struct AgentSpec {
|
|||
pub port: u16,
|
||||
/// The archetype the agent is built from, `None`
|
||||
/// for a custom agent built from its own config repo.
|
||||
pub archetype: Option<swarm_wire::agent_archetype::ArchetypeRef>,
|
||||
pub archetype: Option<ArchetypeRef>,
|
||||
/// The pin of a custom agent's config input. `None` until the caller
|
||||
/// resolves it; an archetype agent has no config input and keeps `None`.
|
||||
pub pin: Option<AgentPin>,
|
||||
|
|
@ -404,25 +405,26 @@ fn push_agent_environment(out: &mut String, vars: &[(&'static str, String)]) {
|
|||
|
||||
/// One `mkAgent` line per agent, closing `outputs`.
|
||||
fn push_agent_entries(out: &mut String, agents: &[AgentSpec]) {
|
||||
use std::fmt::Write as _;
|
||||
for spec in agents {
|
||||
// `source` and `name` are identifiers (`archetypes::store_read`), so
|
||||
// they need no escaping.
|
||||
let archetype = spec.archetype.as_ref().map_or(String::new(), |a| {
|
||||
format!(
|
||||
" archetype = {{ source = \"{}\"; name = \"{}\"; }};",
|
||||
a.source, a.name
|
||||
)
|
||||
});
|
||||
let _ = writeln!(
|
||||
out,
|
||||
" {} = mkAgent {{ name = \"{}\"; port = {};{archetype} }};",
|
||||
spec.name, spec.name, spec.port,
|
||||
);
|
||||
out.push_str(&agent_entry(&spec.name, spec.port, spec.archetype.as_ref()));
|
||||
out.push('\n');
|
||||
}
|
||||
out.push_str(" };\n };\n}\n");
|
||||
}
|
||||
|
||||
/// One agent's `mkAgent` line in `nixosConfigurations`, without its newline.
|
||||
fn agent_entry(name: &str, port: u16, archetype: Option<&ArchetypeRef>) -> String {
|
||||
// `source` and `name` are identifiers (`archetypes::store_read`), so
|
||||
// they need no escaping.
|
||||
let archetype = archetype.map_or(String::new(), |a| {
|
||||
format!(
|
||||
" archetype = {{ source = \"{}\"; name = \"{}\"; }};",
|
||||
a.source, a.name
|
||||
)
|
||||
});
|
||||
format!(" {name} = mkAgent {{ name = \"{name}\"; port = {port};{archetype} }};")
|
||||
}
|
||||
|
||||
/// Web UI port range, shared by every agent including the manager.
|
||||
/// Deterministic from the agent's name (FNV-1a hash mod range size), so the
|
||||
/// dashboard can compute the same port without asking hive-c0re.
|
||||
|
|
@ -474,22 +476,29 @@ pub fn pinned_rev<'a>(flake_nix: &'a str, input: &str) -> Option<&'a str> {
|
|||
is_rev(rev).then_some(rev)
|
||||
}
|
||||
|
||||
/// Whether a flake [`render_flake`] rendered builds agent `name` from the
|
||||
/// config commit `config_head`. `None` is an archetype agent, which has no
|
||||
/// config input to compare.
|
||||
/// Whether a flake [`render_flake`] rendered builds agent `name` the way the
|
||||
/// caller wants it built: from `archetype` when that is set, otherwise from
|
||||
/// its own config repo at the commit `config_head`. A flake that lists the
|
||||
/// agent under another archetype, or as custom where `archetype` is set (or
|
||||
/// the reverse), does not; nor does a custom agent with no `config_head`.
|
||||
#[must_use]
|
||||
pub fn builds_agent_at(flake_nix: &str, name: &str, config_head: Option<&str>) -> bool {
|
||||
let attr = format!("\n {name} = mkAgent {{ name = \"{name}\";");
|
||||
if !flake_nix.contains(&attr) {
|
||||
pub fn builds_agent_at(
|
||||
flake_nix: &str,
|
||||
name: &str,
|
||||
archetype: Option<&ArchetypeRef>,
|
||||
config_head: Option<&str>,
|
||||
) -> bool {
|
||||
let entry = format!("\n{}\n", agent_entry(name, agent_web_port(name), archetype));
|
||||
if !flake_nix.contains(&entry) {
|
||||
return false;
|
||||
}
|
||||
config_head.is_none_or(|head| pinned_rev(flake_nix, &format!("agent-{name}")) == Some(head))
|
||||
archetype.is_some()
|
||||
|| config_head
|
||||
.is_some_and(|head| pinned_rev(flake_nix, &format!("agent-{name}")) == Some(head))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use swarm_wire::agent_archetype::ArchetypeRef;
|
||||
|
||||
use super::*;
|
||||
|
||||
const HYPERHIVE: &str = "1111111111111111111111111111111111111111";
|
||||
|
|
@ -560,26 +569,58 @@ mod tests {
|
|||
);
|
||||
}
|
||||
|
||||
fn reviewer() -> ArchetypeRef {
|
||||
ArchetypeRef {
|
||||
source: "hyperhive".to_owned(),
|
||||
name: "reviewer".to_owned(),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_agent_is_built_only_at_the_config_head_the_flake_pins() {
|
||||
fn a_custom_agent_is_built_only_at_the_config_head_the_flake_pins() {
|
||||
let flake = rendered();
|
||||
assert!(builds_agent_at(&flake, "argus", Some(ARGUS)));
|
||||
assert!(builds_agent_at(&flake, "argus", None, Some(ARGUS)));
|
||||
assert!(
|
||||
!builds_agent_at(&flake, "argus", Some(HYPERHIVE)),
|
||||
!builds_agent_at(&flake, "argus", None, Some(HYPERHIVE)),
|
||||
"a stale pin"
|
||||
);
|
||||
assert!(
|
||||
!builds_agent_at(&flake, "ghost", Some(ARGUS)),
|
||||
!builds_agent_at(&flake, "argus", None, None),
|
||||
"a custom agent with no config commit"
|
||||
);
|
||||
assert!(
|
||||
!builds_agent_at(&flake, "ghost", None, Some(ARGUS)),
|
||||
"an agent the flake lacks"
|
||||
);
|
||||
assert!(builds_agent_at(&flake, "rev", None));
|
||||
assert!(!builds_agent_at(&flake, "ghost", None));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_archetype_agent_is_built_only_from_the_archetype_the_flake_lists() {
|
||||
let flake = rendered();
|
||||
assert!(builds_agent_at(&flake, "rev", Some(&reviewer()), None));
|
||||
let other = ArchetypeRef {
|
||||
name: "planner".to_owned(),
|
||||
..reviewer()
|
||||
};
|
||||
assert!(
|
||||
!builds_agent_at(&flake, "rev", Some(&other), None),
|
||||
"another archetype of the same source"
|
||||
);
|
||||
assert!(
|
||||
!builds_agent_at(&flake, "rev", None, Some(ARGUS)),
|
||||
"listed as an archetype agent, wanted as a custom one"
|
||||
);
|
||||
assert!(
|
||||
!builds_agent_at(&flake, "argus", Some(&reviewer()), None),
|
||||
"listed as a custom agent, wanted as an archetype one"
|
||||
);
|
||||
assert!(!builds_agent_at(&flake, "ghost", Some(&reviewer()), None));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_agent_whose_name_ends_another_s_is_not_that_agent() {
|
||||
let flake = rendered();
|
||||
assert!(!builds_agent_at(&flake, "gus", Some(ARGUS)));
|
||||
assert!(!builds_agent_at(&flake, "v", None));
|
||||
assert!(!builds_agent_at(&flake, "gus", None, Some(ARGUS)));
|
||||
assert!(!builds_agent_at(&flake, "v", Some(&reviewer()), None));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue