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
|
- **Choosing the commit.** The rebuild's `MetaSync` node reads the repo's
|
||||||
default-branch head with the hive's read token (`forge::reader_token()`)
|
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,
|
and takes it when its `flake.nix` builds the agent the way the agent needs now:
|
||||||
pins the config commit at the head of `agent-configs/<n>` `main` 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
|
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
|
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
|
the hive's own flake. `MetaSync` records the commit in `hive-c0re`'s own state,
|
||||||
runtime directory; `Prebuild` and `Swap` read that record, so
|
`/var/lib/hyperhive/meta-rev/<n>`, which the agent can't write (the container mounts its runtime
|
||||||
both build one flake. A first deploy resolves the commit in its `Create`
|
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`.
|
node, since it has no `MetaSync`.
|
||||||
- **The build.** `hive-c0re` prebuilds
|
- **The build.** `hive-c0re` prebuilds
|
||||||
`git+<forge>/internal/meta.git?rev=<commit>#nixosConfigurations.<n>…` and
|
`git+<forge>/internal/meta.git?rev=<commit>#nixosConfigurations.<n>…` and
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@
|
||||||
|
|
||||||
use anyhow::{Context, Result};
|
use anyhow::{Context, Result};
|
||||||
use hive_priv_sock::{meta_flake_url, valid_meta_rev};
|
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.
|
/// What the forge says about `internal/meta` and an agent's config repo.
|
||||||
pub(crate) trait MetaSource {
|
pub(crate) trait MetaSource {
|
||||||
|
|
@ -30,18 +31,18 @@ pub(crate) trait MetaSource {
|
||||||
///
|
///
|
||||||
/// `internal/meta` is private, so a hive whose nix cannot authenticate
|
/// `internal/meta` is private, so a hive whose nix cannot authenticate
|
||||||
/// ([`MetaSource::nix_can_fetch`]) builds from its own flake. The head is
|
/// ([`MetaSource::nix_can_fetch`]) builds from its own flake. The head is
|
||||||
/// taken only when its `flake.nix` builds the agent from the
|
/// taken only when its `flake.nix` builds the agent the way it is wanted now:
|
||||||
/// config commit the forge's `main` is at now (`archetype`: when it lists the
|
/// from `archetype` when that is set, otherwise from the config commit the
|
||||||
/// agent at all). A head that is behind (the swarm's update has not run, or
|
/// forge's `main` is at. A head that is behind (the swarm's update has not
|
||||||
/// failed) or lacks the agent would build an older config than the one being
|
/// run, or failed), lacks the agent or lists it under another archetype would
|
||||||
/// deployed.
|
/// build an older module than the one being deployed.
|
||||||
///
|
///
|
||||||
/// # Errors
|
/// # Errors
|
||||||
/// A forge read that failed.
|
/// A forge read that failed.
|
||||||
pub(crate) async fn choose(
|
pub(crate) async fn choose(
|
||||||
source: &impl MetaSource,
|
source: &impl MetaSource,
|
||||||
agent: &str,
|
agent: &str,
|
||||||
archetype: bool,
|
archetype: Option<&ArchetypeRef>,
|
||||||
) -> Result<Option<String>> {
|
) -> Result<Option<String>> {
|
||||||
if !source.nix_can_fetch() {
|
if !source.nix_can_fetch() {
|
||||||
return Ok(None);
|
return Ok(None);
|
||||||
|
|
@ -53,7 +54,7 @@ pub(crate) async fn choose(
|
||||||
return Ok(None);
|
return Ok(None);
|
||||||
}
|
}
|
||||||
let flake = source.flake_at(&rev).await?;
|
let flake = source.flake_at(&rev).await?;
|
||||||
let config_head = if archetype {
|
let config_head = if archetype.is_some() {
|
||||||
None
|
None
|
||||||
} else {
|
} else {
|
||||||
let Some(head) = source.config_head(agent).await? else {
|
let Some(head) = source.config_head(agent).await? else {
|
||||||
|
|
@ -61,14 +62,17 @@ pub(crate) async fn choose(
|
||||||
};
|
};
|
||||||
Some(head)
|
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
|
/// [`choose`] against the forge this hive reads. `None`, logged, whenever the
|
||||||
/// hive cannot tell: no forge credential, a failed read, no usable commit.
|
/// hive cannot tell: no forge credential, a failed read, no usable commit.
|
||||||
pub(crate) async fn resolve(agent: &str) -> Option<String> {
|
pub(crate) async fn resolve(agent: &str) -> Option<String> {
|
||||||
let archetype = match crate::archetypes::of(agent) {
|
let archetype = match crate::archetypes::of(agent) {
|
||||||
Ok(archetype) => archetype.is_some(),
|
Ok(archetype) => archetype,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
tracing::warn!(%agent, error = ?e, "internal/meta: cannot read the agent's archetype; building from the hive's own flake");
|
tracing::warn!(%agent, error = ?e, "internal/meta: cannot read the agent's archetype; building from the hive's own flake");
|
||||||
return None;
|
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");
|
tracing::info!(%agent, "internal/meta: no forge read token; building from the hive's own flake");
|
||||||
return None;
|
return None;
|
||||||
};
|
};
|
||||||
match choose(&source, agent, archetype).await {
|
match choose(&source, agent, archetype.as_ref()).await {
|
||||||
Ok(Some(rev)) => {
|
Ok(Some(rev)) => {
|
||||||
tracing::info!(%agent, %rev, "internal/meta: building from the swarm's meta flake");
|
tracing::info!(%agent, %rev, "internal/meta: building from the swarm's meta flake");
|
||||||
Some(rev)
|
Some(rev)
|
||||||
|
|
@ -175,8 +179,7 @@ mod tests {
|
||||||
assert_eq!(Some(rev), self.head, "the flake is read at the head");
|
assert_eq!(Some(rev), self.head, "the flake is read at the head");
|
||||||
Ok(self.flake.clone())
|
Ok(self.flake.clone())
|
||||||
}
|
}
|
||||||
async fn config_head(&self, agent: &str) -> Result<Option<String>> {
|
async fn config_head(&self, _: &str) -> Result<Option<String>> {
|
||||||
assert_eq!(agent, "argus");
|
|
||||||
Ok(self.config_head.map(str::to_owned))
|
Ok(self.config_head.map(str::to_owned))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -233,7 +236,7 @@ mod tests {
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn the_head_is_taken_when_it_pins_the_agents_current_config() {
|
async fn the_head_is_taken_when_it_pins_the_agents_current_config() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
choose(&forge(Some(ARGUS_HEAD)), "argus", false)
|
choose(&forge(Some(ARGUS_HEAD)), "argus", None)
|
||||||
.await
|
.await
|
||||||
.unwrap(),
|
.unwrap(),
|
||||||
Some(META.to_owned())
|
Some(META.to_owned())
|
||||||
|
|
@ -243,7 +246,7 @@ mod tests {
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn a_head_behind_the_agents_config_is_not_taken() {
|
async fn a_head_behind_the_agents_config_is_not_taken() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
choose(&forge(Some(STALE)), "argus", false).await.unwrap(),
|
choose(&forge(Some(STALE)), "argus", None).await.unwrap(),
|
||||||
None
|
None
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -252,11 +255,18 @@ mod tests {
|
||||||
async fn a_head_without_the_agent_is_not_taken() {
|
async fn a_head_without_the_agent_is_not_taken() {
|
||||||
let mut source = forge(Some(ARGUS_HEAD));
|
let mut source = forge(Some(ARGUS_HEAD));
|
||||||
source.flake = source.flake.replace("argus", "someone");
|
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]
|
#[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);
|
struct Archetypes(Forge);
|
||||||
impl MetaSource for Archetypes {
|
impl MetaSource for Archetypes {
|
||||||
fn nix_can_fetch(&self) -> bool {
|
fn nix_can_fetch(&self) -> bool {
|
||||||
|
|
@ -274,10 +284,39 @@ mod tests {
|
||||||
}
|
}
|
||||||
let source = Archetypes(forge(None));
|
let source = Archetypes(forge(None));
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
choose(&source, "rev", true).await.unwrap(),
|
choose(&source, "rev", Some(&reviewer())).await.unwrap(),
|
||||||
Some(META.to_owned())
|
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
|
/// A hive without the credential nix's git reads builds from its own flake
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@
|
||||||
//! builds has one renderer.
|
//! builds has one renderer.
|
||||||
|
|
||||||
use anyhow::{Context, Result};
|
use anyhow::{Context, Result};
|
||||||
|
use swarm_wire::agent_archetype::ArchetypeRef;
|
||||||
|
|
||||||
/// One agent of the meta flake.
|
/// One agent of the meta flake.
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
|
|
@ -14,7 +15,7 @@ pub struct AgentSpec {
|
||||||
pub port: u16,
|
pub port: u16,
|
||||||
/// The archetype the agent is built from, `None`
|
/// The archetype the agent is built from, `None`
|
||||||
/// for a custom agent built from its own config repo.
|
/// 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
|
/// 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`.
|
/// resolves it; an archetype agent has no config input and keeps `None`.
|
||||||
pub pin: Option<AgentPin>,
|
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`.
|
/// One `mkAgent` line per agent, closing `outputs`.
|
||||||
fn push_agent_entries(out: &mut String, agents: &[AgentSpec]) {
|
fn push_agent_entries(out: &mut String, agents: &[AgentSpec]) {
|
||||||
use std::fmt::Write as _;
|
|
||||||
for spec in agents {
|
for spec in agents {
|
||||||
// `source` and `name` are identifiers (`archetypes::store_read`), so
|
out.push_str(&agent_entry(&spec.name, spec.port, spec.archetype.as_ref()));
|
||||||
// they need no escaping.
|
out.push('\n');
|
||||||
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(" };\n };\n}\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.
|
/// 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
|
/// Deterministic from the agent's name (FNV-1a hash mod range size), so the
|
||||||
/// dashboard can compute the same port without asking hive-c0re.
|
/// 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)
|
is_rev(rev).then_some(rev)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Whether a flake [`render_flake`] rendered builds agent `name` from the
|
/// Whether a flake [`render_flake`] rendered builds agent `name` the way the
|
||||||
/// config commit `config_head`. `None` is an archetype agent, which has no
|
/// caller wants it built: from `archetype` when that is set, otherwise from
|
||||||
/// config input to compare.
|
/// 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]
|
#[must_use]
|
||||||
pub fn builds_agent_at(flake_nix: &str, name: &str, config_head: Option<&str>) -> bool {
|
pub fn builds_agent_at(
|
||||||
let attr = format!("\n {name} = mkAgent {{ name = \"{name}\";");
|
flake_nix: &str,
|
||||||
if !flake_nix.contains(&attr) {
|
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;
|
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)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use swarm_wire::agent_archetype::ArchetypeRef;
|
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
const HYPERHIVE: &str = "1111111111111111111111111111111111111111";
|
const HYPERHIVE: &str = "1111111111111111111111111111111111111111";
|
||||||
|
|
@ -560,26 +569,58 @@ mod tests {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn reviewer() -> ArchetypeRef {
|
||||||
|
ArchetypeRef {
|
||||||
|
source: "hyperhive".to_owned(),
|
||||||
|
name: "reviewer".to_owned(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[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();
|
let flake = rendered();
|
||||||
assert!(builds_agent_at(&flake, "argus", Some(ARGUS)));
|
assert!(builds_agent_at(&flake, "argus", None, Some(ARGUS)));
|
||||||
assert!(
|
assert!(
|
||||||
!builds_agent_at(&flake, "argus", Some(HYPERHIVE)),
|
!builds_agent_at(&flake, "argus", None, Some(HYPERHIVE)),
|
||||||
"a stale pin"
|
"a stale pin"
|
||||||
);
|
);
|
||||||
assert!(
|
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"
|
"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]
|
#[test]
|
||||||
fn an_agent_whose_name_ends_another_s_is_not_that_agent() {
|
fn an_agent_whose_name_ends_another_s_is_not_that_agent() {
|
||||||
let flake = rendered();
|
let flake = rendered();
|
||||||
assert!(!builds_agent_at(&flake, "gus", Some(ARGUS)));
|
assert!(!builds_agent_at(&flake, "gus", None, Some(ARGUS)));
|
||||||
assert!(!builds_agent_at(&flake, "v", None));
|
assert!(!builds_agent_at(&flake, "v", Some(&reviewer()), None));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue