Watch
0
0
Fork
You've already forked hyperhive
0

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:
atlas 2026-10-24 14:59:43 +02:00
commit 7c0a622d41
19 changed files with 831 additions and 64 deletions

View file

@ -8,7 +8,9 @@
//! [`PrivEvent::Line`]s when asked to.
use anyhow::{Context as _, Result, anyhow, bail};
use hive_priv_sock::{META_DIR, PrivEvent, PrivStream, PrivStreamLine};
use hive_priv_sock::{
META_DIR, PrivEvent, PrivStream, PrivStreamLine, meta_flake_url, valid_meta_rev,
};
use tokio::io::{AsyncWriteExt, BufReader};
use tokio::net::unix::OwnedWriteHalf;
use tokio::process::Command;
@ -100,6 +102,7 @@ pub(crate) async fn destroy_container(name: &str) -> Result<(String, String)> {
pub(crate) async fn container_flake_action(
verb: &str,
name: &str,
meta_rev: Option<&str>,
stream: bool,
writer: &mut OwnedWriteHalf,
) -> Result<(String, String)> {
@ -111,7 +114,7 @@ pub(crate) async fn container_flake_action(
// (which is the whole point of this fix) would silently regress every
// UI that shows build progress: nothing until the longest phase
// finishes, then everything at once.
let toplevel = nix_build_toplevel(name, stream.then_some(&mut *writer)).await?;
let toplevel = nix_build_toplevel(name, meta_rev, stream.then_some(&mut *writer)).await?;
let system_name = container_system_name(name);
if verb == "update" {
return swap_container_profile(&system_name, &toplevel, stream, writer).await;
@ -208,12 +211,34 @@ async fn log_and_forward(stdout: &str, stderr: &str, stream: bool, writer: &mut
}
}
/// The environment variable naming the forge's `http` origin, which
/// `internal/meta` is fetched from. Set by the unit; never taken from a
/// request.
pub(crate) const FORGE_URL_ENV: &str = "HIVE_PRIV_FORGE_URL";
/// The flake agent toplevels are built from: the hive's own meta flake, or
/// `internal/meta` at `meta_rev` when the request names one. The URL is
/// `forge_base`, hive-priv's own; only the rev comes from the request, and it
/// is checked here.
fn meta_flake(meta_rev: Option<&str>, forge_base: Option<&str>) -> Result<String> {
let Some(rev) = meta_rev else {
return Ok(META_DIR.to_owned());
};
if !valid_meta_rev(rev) {
bail!("meta_rev {rev:?} is not a 40-digit lowercase hex commit id");
}
let base = forge_base.filter(|b| !b.is_empty()).with_context(|| {
format!("{FORGE_URL_ENV} is unset, so there is no internal/meta to build from")
})?;
Ok(meta_flake_url(base, rev))
}
/// The explicit `nixosConfigurations.<name>.config.system.build.toplevel`
/// flake attr path — same construction `hive-c0re`'s own
/// `lifecycle::prebuild_toplevel` uses, kept here as a pure function so
/// the exact string shape is unit-tested without needing to run `nix`.
fn toplevel_attr(name: &str) -> String {
format!("{META_DIR}#nixosConfigurations.{name}.config.system.build.toplevel")
fn toplevel_attr(flake: &str, name: &str) -> String {
format!("{flake}#nixosConfigurations.{name}.config.system.build.toplevel")
}
/// Wall-clock bound on [`nix_build_toplevel`]. Four times CI's observed
@ -350,8 +375,13 @@ async fn run_bounded(
/// `writer` is `None` for the non-streaming call shape (`stream: false`);
/// stderr still logs to journald either way, just without the
/// `PrivEvent::Line` forwarding. Bounded by [`NIX_BUILD_TIMEOUT`].
async fn nix_build_toplevel(name: &str, writer: Option<&mut OwnedWriteHalf>) -> Result<String> {
let attr = toplevel_attr(name);
async fn nix_build_toplevel(
name: &str,
meta_rev: Option<&str>,
writer: Option<&mut OwnedWriteHalf>,
) -> Result<String> {
let flake = meta_flake(meta_rev, std::env::var(FORGE_URL_ENV).ok().as_deref())?;
let attr = toplevel_attr(&flake, name);
let mut cmd = Command::new("nix");
cmd.args([
"--extra-experimental-features",
@ -687,7 +717,9 @@ async fn container_run_streaming(
}
#[cfg(test)]
mod tests {
use super::{BoundedRun, run_bounded, single_output_path, toplevel_attr};
use super::{
BoundedRun, FORGE_URL_ENV, meta_flake, run_bounded, single_output_path, toplevel_attr,
};
use crate::shared::test_support::scratch;
/// Pins the exact attr path we hand to `nix build` — it has to match
@ -698,11 +730,56 @@ mod tests {
#[test]
fn toplevel_attr_matches_prebuild_toplevels_construction() {
assert_eq!(
toplevel_attr("atlas"),
toplevel_attr(&meta_flake(None, None).unwrap(), "atlas"),
"/var/lib/hyperhive/meta#nixosConfigurations.atlas.config.system.build.toplevel"
);
}
/// The flake is the hive's own unless the request names an `internal/meta`
/// commit, and then its URL is the unit's, never the request's.
#[test]
fn a_meta_rev_builds_from_internal_meta_at_the_units_forge() {
let rev = "c".repeat(40);
let forge = Some("http://forge.t.local");
assert_eq!(
toplevel_attr(&meta_flake(Some(&rev), forge).unwrap(), "atlas"),
format!(
"git+http://forge.t.local/internal/meta.git?rev={rev}\
#nixosConfigurations.atlas.config.system.build.toplevel"
)
);
// No rev: the forge is not consulted at all.
assert_eq!(meta_flake(None, forge).unwrap(), hive_priv_sock::META_DIR);
}
/// Nothing but a commit id gets into the ref, whatever the request says.
#[test]
fn a_meta_rev_that_is_not_a_commit_id_is_refused() {
let forge = Some("http://forge.t.local");
for bad in [
"main",
"",
"../../x",
&format!("{}#evil", "a".repeat(34)),
&format!("{}&ref=x", "a".repeat(34)),
] {
let err = meta_flake(Some(bad), forge).unwrap_err();
assert!(
format!("{err:#}").contains("not a 40-digit"),
"{bad}: {err:#}"
);
}
}
#[test]
fn a_meta_rev_without_a_forge_in_the_unit_is_refused() {
let rev = "c".repeat(40);
for forge in [None, Some("")] {
let err = meta_flake(Some(&rev), forge).unwrap_err();
assert!(format!("{err:#}").contains(FORGE_URL_ENV), "{err:#}");
}
}
/// `--print-out-paths`' exact per-shape table (measured against real
/// `rustc` semantics, not just reasoned about) — a lone `"\n"` and a
/// trailing-space path are the two shapes a bare `.lines().collect()`

View file

@ -349,12 +349,22 @@ async fn exec(
PrivRequest::KillContainer { ref name } => container::kill_container(name).await,
PrivRequest::UpdateContainer { ref name, stream } => {
container::container_flake_action("update", name, stream, writer).await
PrivRequest::UpdateContainer {
ref name,
stream,
ref meta_rev,
} => {
container::container_flake_action("update", name, meta_rev.as_deref(), stream, writer)
.await
}
PrivRequest::CreateContainer { ref name, stream } => {
container::container_flake_action("create", name, stream, writer).await
PrivRequest::CreateContainer {
ref name,
stream,
ref meta_rev,
} => {
container::container_flake_action("create", name, meta_rev.as_deref(), stream, writer)
.await
}
PrivRequest::DestroyContainer { ref name } => container::destroy_container(name).await,