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
|
|
@ -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()`
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Reference in a new issue