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
|
|
@ -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