create downloads the agent's cached closure before building
This commit is contained in:
parent
42e63b1ee7
commit
375290e409
3 changed files with 93 additions and 13 deletions
|
|
@ -42,11 +42,11 @@ because there is no malformed spec to reject.
|
||||||
|
|
||||||
Nix-heavy — hold one of the `buildSlots` permits for the node's duration:
|
Nix-heavy — hold one of the `buildSlots` permits for the node's duration:
|
||||||
|
|
||||||
| Node | Wraps |
|
| Node | Wraps |
|
||||||
| ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
| ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||||
| `Prebuild` | `lifecycle::prebuild_toplevel` — build the toplevel out-of-band while the container keeps serving (its meta preamble is the upstream `MetaSync` node). Skipped when the container is already down; `Swap` builds inline instead |
|
| `Prebuild` | `lifecycle::prebuild_toplevel` — build the toplevel out-of-band while the container keeps serving (its meta preamble is the upstream `MetaSync` node). Skipped when the container is already down; `Swap` builds inline instead |
|
||||||
| `Swap` | drop-in rewrite + `nixos-container update` profile-swap (requires the container stopped); the post-swap bookkeeping tail lives in the sibling `RebuildBookkeeping` node |
|
| `Swap` | drop-in rewrite + `nixos-container update` profile-swap (requires the container stopped); the post-swap bookkeeping tail lives in the sibling `RebuildBookkeeping` node |
|
||||||
| `Create` | first-spawn `nixos-container create` proper; assumes the upstream `Provision` node already registered the agent; builds from the `internal/meta` commit it resolves and marks the agent's pins |
|
| `Create` | first-spawn `nixos-container create` proper; assumes the upstream `Provision` node already registered the agent; downloads the closure the swarm cached for the `internal/meta` commit it resolves, else builds it locally, and marks the agent's pins |
|
||||||
|
|
||||||
Cheap — no build slot:
|
Cheap — no build slot:
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -310,7 +310,7 @@ async fn dispatch(
|
||||||
NodeKind::Swap { .. } => run_swap(host, coord, agent, id).await,
|
NodeKind::Swap { .. } => run_swap(host, coord, agent, id).await,
|
||||||
NodeKind::RebuildBookkeeping { .. } => run_rebuild_bookkeeping(host, coord, agent).await,
|
NodeKind::RebuildBookkeeping { .. } => run_rebuild_bookkeeping(host, coord, agent).await,
|
||||||
NodeKind::Provision { .. } => run_provision(host, agent).await,
|
NodeKind::Provision { .. } => run_provision(host, agent).await,
|
||||||
NodeKind::Create { .. } => run_create(host, agent).await,
|
NodeKind::Create { .. } => run_create(host, agent, id).await,
|
||||||
NodeKind::DestroyContainer { .. } => run_destroy_container(host, coord, agent).await,
|
NodeKind::DestroyContainer { .. } => run_destroy_container(host, coord, agent).await,
|
||||||
NodeKind::PurgeState { .. } => {
|
NodeKind::PurgeState { .. } => {
|
||||||
run_purge_state(host, agent).await;
|
run_purge_state(host, agent).await;
|
||||||
|
|
@ -672,7 +672,7 @@ fn drift(downloaded: &str, evaluated: Option<&str>) -> Option<String> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Why a rebuild builds instead of downloading the swarm's closure.
|
/// Why a rebuild or create builds instead of downloading the swarm's closure.
|
||||||
struct LocalBuild {
|
struct LocalBuild {
|
||||||
reason: String,
|
reason: String,
|
||||||
/// The cache served a path no trusted key signed.
|
/// The cache served a path no trusted key signed.
|
||||||
|
|
@ -776,8 +776,30 @@ async fn run_provision(host: &impl Host, name: &str) -> Result<()> {
|
||||||
/// `Reconcile` (`converge_start_preamble` + `register_agent`) so this
|
/// `Reconcile` (`converge_start_preamble` + `register_agent`) so this
|
||||||
/// node stays purely "create", not "create + start". Marks the agent as
|
/// node stays purely "create", not "create + start". Marks the agent as
|
||||||
/// running the pins it was created from.
|
/// running the pins it was created from.
|
||||||
async fn run_create(host: &impl Host, name: &str) -> Result<()> {
|
///
|
||||||
|
/// The swarm's cache is tried first, as in [`run_prebuild`]: when it holds the
|
||||||
|
/// closure for the resolved commit it is downloaded, and the local evaluation
|
||||||
|
/// that follows is a no-op build on a path match and builds the evaluated path
|
||||||
|
/// on a mismatch. Without a cached closure the create builds it locally.
|
||||||
|
async fn run_create(host: &impl Host, name: &str, id: NodeId) -> Result<()> {
|
||||||
let build = host.resolve_meta(name).await?;
|
let build = host.resolve_meta(name).await?;
|
||||||
|
match substitute(host, name, &build.rev).await {
|
||||||
|
Ok(path) => {
|
||||||
|
let evaluated = host.prebuild_toplevel(name, &build.rev, id.get()).await?;
|
||||||
|
match drift(&path, evaluated.as_deref()) {
|
||||||
|
None => tracing::info!(%name, rev = %build.rev, "create: substituted {path}"),
|
||||||
|
Some(note) => tracing::warn!(%name, rev = %build.rev, "create: {note}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(fallback) => {
|
||||||
|
let reason = &fallback.reason;
|
||||||
|
if fallback.signature_refused {
|
||||||
|
tracing::error!(%name, rev = %build.rev, "create: built locally: {reason}");
|
||||||
|
} else {
|
||||||
|
tracing::warn!(%name, rev = %build.rev, "create: built locally: {reason}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
host.create_only(name, &build.rev).await?;
|
host.create_only(name, &build.rev).await?;
|
||||||
host.mark_deployed(name, &build)?;
|
host.mark_deployed(name, &build)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
|
||||||
|
|
@ -834,26 +834,84 @@ async fn provision_and_write_dropin_forward_and_propagate_failure() {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn create_builds_at_the_resolved_rev_then_marks_it_deployed() {
|
async fn create_without_a_record_builds_locally_at_the_resolved_rev() {
|
||||||
let host = FakeHost::up();
|
let host = FakeHost::up();
|
||||||
run_create(&host, "alice").await.expect("ok");
|
run_create(&host, "alice", node_id()).await.expect("ok");
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
host.calls(),
|
host.calls(),
|
||||||
[
|
[
|
||||||
"resolve_meta alice".to_owned(),
|
"resolve_meta alice".to_owned(),
|
||||||
|
format!("cached_closure alice {REV}"),
|
||||||
format!("create_only alice {REV}"),
|
format!("create_only alice {REV}"),
|
||||||
format!("mark_deployed alice {REV}"),
|
format!("mark_deployed alice {REV}"),
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn create_downloads_a_cached_closure_and_builds_nothing() {
|
||||||
|
let host = FakeHost::up().with_closure();
|
||||||
|
let id = node_id();
|
||||||
|
run_create(&host, "alice", id).await.expect("ok");
|
||||||
|
assert_eq!(
|
||||||
|
host.calls(),
|
||||||
|
[
|
||||||
|
"resolve_meta alice".to_owned(),
|
||||||
|
format!("cached_closure alice {REV}"),
|
||||||
|
format!("realise alice {CLOSURE}"),
|
||||||
|
format!("prebuild alice {REV} node={}", id.get()),
|
||||||
|
format!("create_only alice {REV}"),
|
||||||
|
format!("mark_deployed alice {REV}"),
|
||||||
|
]
|
||||||
|
);
|
||||||
|
assert_eq!(host.builds(), Vec::<String>::new());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn create_builds_locally_when_the_download_fails() {
|
||||||
|
let host = FakeHost::up().with_closure().failing("realise");
|
||||||
|
run_create(&host, "alice", node_id()).await.expect("ok");
|
||||||
|
assert_eq!(
|
||||||
|
host.calls(),
|
||||||
|
[
|
||||||
|
"resolve_meta alice".to_owned(),
|
||||||
|
format!("cached_closure alice {REV}"),
|
||||||
|
format!("realise alice {CLOSURE}"),
|
||||||
|
format!("create_only alice {REV}"),
|
||||||
|
format!("mark_deployed alice {REV}"),
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn create_builds_the_evaluated_path_when_it_differs_from_the_download() {
|
||||||
|
let other = "/nix/store/zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz-nixos-system-alice";
|
||||||
|
let host = FakeHost {
|
||||||
|
evaluates: Some(other.to_owned()),
|
||||||
|
..FakeHost::up().with_closure()
|
||||||
|
};
|
||||||
|
run_create(&host, "alice", node_id()).await.expect("ok");
|
||||||
|
assert_eq!(host.builds(), [other.to_owned()]);
|
||||||
|
let calls = host.calls();
|
||||||
|
assert!(
|
||||||
|
calls
|
||||||
|
.iter()
|
||||||
|
.any(|c| c == &format!("create_only alice {REV}")),
|
||||||
|
"{calls:?}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn a_failed_create_is_never_marked_deployed() {
|
async fn a_failed_create_is_never_marked_deployed() {
|
||||||
let host = FakeHost::up().failing("create_only");
|
let host = FakeHost::up().failing("create_only");
|
||||||
run_create(&host, "alice").await.expect_err("create fails");
|
run_create(&host, "alice", node_id())
|
||||||
assert_eq!(host.calls().len(), 2);
|
.await
|
||||||
|
.expect_err("create fails");
|
||||||
|
assert_eq!(host.calls().len(), 3);
|
||||||
let host = FakeHost::up().failing("resolve_meta");
|
let host = FakeHost::up().failing("resolve_meta");
|
||||||
run_create(&host, "alice").await.expect_err("resolve fails");
|
run_create(&host, "alice", node_id())
|
||||||
|
.await
|
||||||
|
.expect_err("resolve fails");
|
||||||
assert_eq!(host.calls(), ["resolve_meta alice"]);
|
assert_eq!(host.calls(), ["resolve_meta alice"]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue