bind a container apply to the closure realised for the agent, and record the deployed pins on drift
hive-priv applies only the path its last RealiseClosure returned for that agent. On drift the swap and create record the pins of the commit the closure was built from.
This commit is contained in:
parent
4590b04cac
commit
6b800600e2
3 changed files with 338 additions and 71 deletions
|
|
@ -47,8 +47,14 @@ Each module's `//!` header lists its ops.
|
|||
`container_toplevel_action` (in `src/container.rs`) applies a closure that is
|
||||
already in the store; hive-priv builds nothing. hive-c0re names the closure
|
||||
(`toplevel` on `CreateContainer` / `UpdateContainer`), downloaded beforehand
|
||||
with `RealiseClosure`, and hive-priv accepts it only in the shape of a store
|
||||
path.
|
||||
with `RealiseClosure`. hive-priv remembers, per agent, the path its last
|
||||
successful `RealiseClosure` returned (in memory), and applies only that path to
|
||||
that agent. It refuses a path never realised for the agent, another agent's
|
||||
and the host's own, and logs an error naming the agent and both paths. A
|
||||
restart between the download and the apply empties the record, so the apply
|
||||
fails and the job retries. hive-priv does not re-check the signature at apply
|
||||
time: `RealiseClosure` enforces it, and the binding limits an apply to what that
|
||||
call stored.
|
||||
|
||||
`create` passes the path to `nixos-container create --system-path`, so
|
||||
`nixos-container`'s own `buildFlake()` never runs. Without the flag, it builds
|
||||
|
|
|
|||
|
|
@ -143,14 +143,78 @@ fn reason(stderr: &str) -> String {
|
|||
/// out of the store: no cache or key on this host, nix failing or timing out.
|
||||
pub(crate) async fn realise_closure(agent: &str, store_path: &str) -> Result<(String, String)> {
|
||||
let url = std::env::var(CACHE_URL_ENV).ok();
|
||||
realise_with(
|
||||
let done = realise_with(
|
||||
url.as_deref(),
|
||||
Path::new(ROOT_FACTS_DIR),
|
||||
0,
|
||||
agent,
|
||||
store_path,
|
||||
)
|
||||
.await
|
||||
.await?;
|
||||
if let Ok(mut realised) = REALISED.lock() {
|
||||
remember(&mut realised, agent, store_path);
|
||||
}
|
||||
Ok(done)
|
||||
}
|
||||
|
||||
/// The store path each agent's last successful `RealiseClosure` put in the
|
||||
/// store. `CreateContainer` / `UpdateContainer` apply only that path for that
|
||||
/// agent. hive-priv is a long-running process, so the entry outlives the
|
||||
/// calls between the download and the apply; a restart in between empties it
|
||||
/// and the apply is refused.
|
||||
static REALISED: std::sync::Mutex<Vec<(String, String)>> = std::sync::Mutex::new(Vec::new());
|
||||
|
||||
fn remember(realised: &mut Vec<(String, String)>, agent: &str, store_path: &str) {
|
||||
realised.retain(|(a, _)| a != agent);
|
||||
realised.push((agent.to_owned(), store_path.to_owned()));
|
||||
}
|
||||
|
||||
/// The path recorded for `agent`.
|
||||
fn lookup<'a>(realised: &'a [(String, String)], agent: &str) -> Option<&'a str> {
|
||||
realised
|
||||
.iter()
|
||||
.find(|(a, _)| a == agent)
|
||||
.map(|(_, p)| p.as_str())
|
||||
}
|
||||
|
||||
/// `toplevel` when it is a store path and the one `agent`'s last
|
||||
/// `RealiseClosure` returned (`realised`); else the refusal, which is also
|
||||
/// logged at error level with the agent and both paths.
|
||||
///
|
||||
/// The request comes from hive-c0re, which is not root. A signed path is not
|
||||
/// enough: it could be another agent's closure or the host's own.
|
||||
///
|
||||
/// # Errors
|
||||
/// `toplevel` is not a store path, or is not `realised`.
|
||||
pub(crate) fn applicable<'a>(
|
||||
agent: &str,
|
||||
toplevel: &'a str,
|
||||
realised: Option<&str>,
|
||||
) -> Result<&'a str> {
|
||||
if !valid_closure_path(toplevel) {
|
||||
bail!("{toplevel:?} is not a store path");
|
||||
}
|
||||
if realised == Some(toplevel) {
|
||||
return Ok(toplevel);
|
||||
}
|
||||
let realised = realised.unwrap_or("none");
|
||||
tracing::error!(
|
||||
%agent, requested = %toplevel, %realised,
|
||||
"refusing to apply a closure that is not the one realised for the agent"
|
||||
);
|
||||
bail!("{agent}: {toplevel} is not the closure realised for this agent ({realised})")
|
||||
}
|
||||
|
||||
/// [`applicable`] against what `RealiseClosure` recorded for `agent`.
|
||||
///
|
||||
/// # Errors
|
||||
/// As [`applicable`].
|
||||
pub(crate) fn applicable_for<'a>(agent: &str, toplevel: &'a str) -> Result<&'a str> {
|
||||
let realised = REALISED
|
||||
.lock()
|
||||
.ok()
|
||||
.and_then(|r| lookup(&r, agent).map(str::to_owned));
|
||||
applicable(agent, toplevel, realised.as_deref())
|
||||
}
|
||||
|
||||
/// [`realise_closure`] with the cache's address and the key's directory and
|
||||
|
|
@ -216,6 +280,62 @@ mod tests {
|
|||
}
|
||||
}
|
||||
|
||||
const OTHER: &str = "/nix/store/zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz-nixos-system-iris";
|
||||
|
||||
#[test]
|
||||
fn the_path_realised_for_the_agent_is_applied() {
|
||||
assert_eq!(applicable("atlas", PATH, Some(PATH)).unwrap(), PATH);
|
||||
}
|
||||
|
||||
/// Another agent's realised path, a valid-shaped path never realised, and
|
||||
/// a restart that emptied the record are all refused, naming the agent
|
||||
/// and both paths.
|
||||
#[test]
|
||||
fn any_other_path_is_refused() {
|
||||
for (case, realised) in [
|
||||
("another path was realised for the agent", Some(OTHER)),
|
||||
("nothing was realised for the agent", None),
|
||||
] {
|
||||
let err = applicable("atlas", PATH, realised).unwrap_err();
|
||||
let msg = format!("{err:#}");
|
||||
assert!(
|
||||
msg.contains("atlas")
|
||||
&& msg.contains(PATH)
|
||||
&& msg.contains(realised.unwrap_or("none")),
|
||||
"{case}: {msg}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn only_a_store_path_is_applied_even_when_it_was_recorded() {
|
||||
for bad in [
|
||||
"",
|
||||
"main",
|
||||
"/etc/passwd",
|
||||
"/nix/store/x",
|
||||
&format!("{PATH}/bin"),
|
||||
] {
|
||||
let err = applicable("atlas", bad, Some(bad)).unwrap_err();
|
||||
assert!(format!("{err:#}").contains("not a store path"), "{bad}");
|
||||
}
|
||||
}
|
||||
|
||||
/// Each agent keeps its own entry, and a newer download replaces the
|
||||
/// older one.
|
||||
#[test]
|
||||
fn a_record_is_per_agent_and_the_latest_wins() {
|
||||
let mut realised = Vec::new();
|
||||
remember(&mut realised, "atlas", PATH);
|
||||
remember(&mut realised, "iris", OTHER);
|
||||
assert_eq!(lookup(&realised, "atlas"), Some(PATH));
|
||||
assert_eq!(lookup(&realised, "iris"), Some(OTHER));
|
||||
remember(&mut realised, "atlas", OTHER);
|
||||
assert_eq!(lookup(&realised, "atlas"), Some(OTHER));
|
||||
assert_eq!(lookup(&realised, "nobody"), None);
|
||||
assert_eq!(realised.len(), 2);
|
||||
}
|
||||
|
||||
fn option<'a>(args: &'a [String], name: &str) -> Option<&'a str> {
|
||||
args.windows(3)
|
||||
.find(|w| w[0] == "--option" && w[1] == name)
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@
|
|||
//! and stream progress to the caller as [`PrivEvent::Line`]s when asked to.
|
||||
|
||||
use anyhow::{Context as _, Result, bail};
|
||||
use hive_priv_sock::{PrivEvent, PrivStream, PrivStreamLine, valid_closure_path};
|
||||
use hive_priv_sock::{PrivEvent, PrivStream, PrivStreamLine};
|
||||
use tokio::io::{AsyncWriteExt, BufReader};
|
||||
use tokio::net::unix::OwnedWriteHalf;
|
||||
use tokio::process::Command;
|
||||
|
|
@ -71,17 +71,8 @@ pub(crate) async fn destroy_container(name: &str) -> Result<(String, String)> {
|
|||
container_run(&["destroy", &container_system_name(name)]).await
|
||||
}
|
||||
|
||||
/// `toplevel` as a store path, or the error saying it is not one. The path
|
||||
/// crosses the socket from hive-c0re, which is not root.
|
||||
fn toplevel_to_apply(toplevel: &str) -> Result<&str> {
|
||||
if !valid_closure_path(toplevel) {
|
||||
bail!("{toplevel:?} is not a store path");
|
||||
}
|
||||
Ok(toplevel)
|
||||
}
|
||||
|
||||
/// Shared body for `CreateContainer` / `UpdateContainer`: validate the name
|
||||
/// and the closure, and apply it (streaming line events to `writer` when
|
||||
/// and that `toplevel` is the closure realised for the agent, and apply it (streaming line events to `writer` when
|
||||
/// `stream` is set). Nothing is built here: `toplevel` is a closure already in
|
||||
/// the store, which `RealiseClosure` put there.
|
||||
///
|
||||
|
|
@ -102,7 +93,7 @@ pub(crate) async fn container_toplevel_action(
|
|||
writer: &mut OwnedWriteHalf,
|
||||
) -> Result<(String, String)> {
|
||||
validate_agent_name(name)?;
|
||||
let toplevel = toplevel_to_apply(toplevel)?;
|
||||
let toplevel = crate::closure::applicable_for(name, toplevel)?;
|
||||
let system_name = container_system_name(name);
|
||||
if verb == "update" {
|
||||
return swap_container_profile(&system_name, toplevel, stream, writer).await;
|
||||
|
|
@ -567,34 +558,9 @@ async fn container_run_streaming(
|
|||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{BoundedRun, run_bounded, toplevel_to_apply};
|
||||
use super::{BoundedRun, run_bounded};
|
||||
use crate::shared::test_support::scratch;
|
||||
|
||||
const TOPLEVEL: &str = "/nix/store/0123456789abcdfghijklmnpqrsvwxyz-nixos-system-atlas";
|
||||
|
||||
/// A store path passes through unchanged; anything else, whatever the
|
||||
/// request says, never reaches `nix-env` or `nixos-container`.
|
||||
#[test]
|
||||
fn only_a_store_path_is_applied() {
|
||||
assert_eq!(toplevel_to_apply(TOPLEVEL).unwrap(), TOPLEVEL);
|
||||
for bad in [
|
||||
"",
|
||||
"main",
|
||||
"../../x",
|
||||
"/etc/passwd",
|
||||
"/nix/store/x",
|
||||
&format!("{TOPLEVEL}/bin"),
|
||||
&format!("{TOPLEVEL};id"),
|
||||
&"c".repeat(40),
|
||||
] {
|
||||
let err = toplevel_to_apply(bad).unwrap_err();
|
||||
assert!(
|
||||
format!("{err:#}").contains("not a store path"),
|
||||
"{bad}: {err:#}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether `pid` is gone, a zombie (`Z`), or being reaped (`X`): none of
|
||||
/// these runs again, and a process passes through them in that order,
|
||||
/// so once this is true it stays true.
|
||||
|
|
|
|||
Loading…
Reference in a new issue