build agents from internal/meta when it pins their config
A rebuild's MetaSync resolves the head of the swarm's internal/meta once and records it; the prebuild and the swap build that flake instead of the hive's own, and a first deploy does the same in Create. hive-priv takes the commit as a validated meta_rev on UpdateContainer and CreateContainer and builds from its own forge URL (HIVE_PRIV_FORGE_URL). The head is used only when its flake lists the agent at the config commit the forge's main is at; otherwise, and when the repo has no commit or this hive has no core token file for nix's git, the hive renders locally as before. hive-c0re's and hive-priv's gitconfig now share the forge credential helper and rewrite the forge's https URLs to http, which hive-priv lacked.
This commit is contained in:
parent
22e2d30581
commit
7c0a622d41
19 changed files with 831 additions and 64 deletions
|
|
@ -9,3 +9,6 @@ workspace = true
|
|||
|
||||
[dependencies]
|
||||
serde.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
serde_json.workspace = true
|
||||
|
|
|
|||
|
|
@ -101,12 +101,36 @@ impl std::str::FromStr for InfraContainer {
|
|||
}
|
||||
}
|
||||
|
||||
/// Host path of the meta flake. The flake ref for agent `<name>` is
|
||||
/// `{META_DIR}#{name}`, derived by `hive-priv` — never passed over the wire.
|
||||
/// Must stay in sync with `hive-c0re::paths::meta_root()` (`STATE_ROOT +
|
||||
/// "/meta"`); the privsep boundary prevents importing across the crate.
|
||||
/// Host path of the meta flake a hive renders itself. The flake ref for agent
|
||||
/// `<name>` is `{META_DIR}#{name}`, derived by `hive-priv` — never passed over
|
||||
/// the wire. Must stay in sync with `hive-c0re::paths::meta_root()`
|
||||
/// (`STATE_ROOT + "/meta"`); the privsep boundary prevents importing across
|
||||
/// the crate.
|
||||
pub const META_DIR: &str = "/var/lib/hyperhive/meta";
|
||||
|
||||
/// Whether `rev` is a git commit id: 40 lowercase hex digits. The only part of
|
||||
/// the `internal/meta` flake ref that crosses the wire (`meta_rev` on
|
||||
/// `PrivRequest::UpdateContainer` and `CreateContainer`), so `hive-priv` checks
|
||||
/// it before it builds a ref.
|
||||
#[must_use]
|
||||
pub fn valid_meta_rev(rev: &str) -> bool {
|
||||
rev.len() == 40 && rev.bytes().all(|b| matches!(b, b'0'..=b'9' | b'a'..=b'f'))
|
||||
}
|
||||
|
||||
/// The flake the swarm publishes at `internal/meta`, at `rev`:
|
||||
/// `git+<forge_base>/internal/meta.git?rev=<rev>`. `forge_base` is the forge's
|
||||
/// `http` origin, which the hive's git credential helper answers for.
|
||||
///
|
||||
/// One definition for `hive-c0re`'s prebuild and `hive-priv`'s build, so both
|
||||
/// name the same flake.
|
||||
#[must_use]
|
||||
pub fn meta_flake_url(forge_base: &str, rev: &str) -> String {
|
||||
format!(
|
||||
"git+{}/internal/meta.git?rev={rev}",
|
||||
forge_base.trim_end_matches('/')
|
||||
)
|
||||
}
|
||||
|
||||
/// Root of per-agent state directories on the host.
|
||||
/// Subdirectory layout: `<AGENT_STATE_ROOT>/<name>/state/<file>`.
|
||||
/// Used by `WriteAgentStateFile` to derive the write path so the
|
||||
|
|
@ -227,23 +251,33 @@ pub enum PrivRequest {
|
|||
KillContainer { name: String },
|
||||
|
||||
/// `nixos-container update <name> --flake <flake_ref>`
|
||||
/// The flake ref is derived from `name` by hive-priv.
|
||||
/// The flake ref is derived from `name` by hive-priv: the hive's own meta
|
||||
/// flake, or `internal/meta` at `meta_rev` when that is set (the forge URL
|
||||
/// is hive-priv's own and never on the wire).
|
||||
/// When `stream` is `true`, hive-priv sends `PrivEvent::Line` messages
|
||||
/// as the process runs, then a terminal `PrivEvent::Done`.
|
||||
UpdateContainer {
|
||||
name: String,
|
||||
#[serde(default)]
|
||||
stream: bool,
|
||||
/// A commit of `internal/meta` to build from, checked by
|
||||
/// `valid_meta_rev`. `serde(default)` so a request from a hive-c0re
|
||||
/// that predates the field builds from the hive's own flake.
|
||||
#[serde(default)]
|
||||
meta_rev: Option<String>,
|
||||
},
|
||||
|
||||
/// `nixos-container create <name> --flake <flake_ref>`
|
||||
/// The flake ref is derived from `name` by hive-priv.
|
||||
/// The flake ref is derived from `name` (and `meta_rev`) as for
|
||||
/// `UpdateContainer`.
|
||||
/// When `stream` is `true`, hive-priv sends `PrivEvent::Line` messages
|
||||
/// as the process runs, then a terminal `PrivEvent::Done`.
|
||||
CreateContainer {
|
||||
name: String,
|
||||
#[serde(default)]
|
||||
stream: bool,
|
||||
#[serde(default)]
|
||||
meta_rev: Option<String>,
|
||||
},
|
||||
|
||||
/// `nixos-container destroy <name>`
|
||||
|
|
@ -656,7 +690,7 @@ pub enum PrivEvent {
|
|||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::InfraContainer;
|
||||
use super::{InfraContainer, PrivRequest, meta_flake_url, valid_meta_rev};
|
||||
|
||||
#[test]
|
||||
fn infra_control_names_no_c0re_or_swarm_service() {
|
||||
|
|
@ -685,4 +719,56 @@ mod tests {
|
|||
fn gateway_resolves_to_host_nginx() {
|
||||
assert_eq!(InfraContainer::Gateway.service_unit(), "nginx.service");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn only_a_full_lowercase_commit_id_is_a_meta_rev() {
|
||||
assert!(valid_meta_rev(&"a1".repeat(20)));
|
||||
for bad in [
|
||||
"",
|
||||
"main",
|
||||
&"a".repeat(39),
|
||||
&"a".repeat(41),
|
||||
&"A".repeat(40),
|
||||
&format!("{}?x", "a".repeat(38)),
|
||||
&format!("{}#x", "a".repeat(39)),
|
||||
&format!("{} ", "a".repeat(39)),
|
||||
] {
|
||||
assert!(!valid_meta_rev(bad), "{bad:?}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_meta_flake_url_names_internal_meta_at_the_rev() {
|
||||
let rev = "a".repeat(40);
|
||||
assert_eq!(
|
||||
meta_flake_url("http://forge.t.local/", &rev),
|
||||
format!("git+http://forge.t.local/internal/meta.git?rev={rev}")
|
||||
);
|
||||
}
|
||||
|
||||
/// A hive-c0re that predates `meta_rev` sends neither field.
|
||||
#[test]
|
||||
fn a_request_without_meta_rev_builds_from_the_hives_own_flake() {
|
||||
for op in ["update_container", "create_container"] {
|
||||
let req: PrivRequest =
|
||||
serde_json::from_str(&format!(r#"{{"op":"{op}","name":"atlas"}}"#)).unwrap();
|
||||
match req {
|
||||
PrivRequest::UpdateContainer { meta_rev, .. }
|
||||
| PrivRequest::CreateContainer { meta_rev, .. } => assert_eq!(meta_rev, None),
|
||||
other => panic!("{other:?}"),
|
||||
}
|
||||
}
|
||||
let with: PrivRequest = serde_json::from_str(&format!(
|
||||
r#"{{"op":"update_container","name":"atlas","meta_rev":"{}"}}"#,
|
||||
"b".repeat(40)
|
||||
))
|
||||
.unwrap();
|
||||
assert!(matches!(
|
||||
with,
|
||||
PrivRequest::UpdateContainer {
|
||||
meta_rev: Some(_),
|
||||
..
|
||||
}
|
||||
));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue