138 lines
5.1 KiB
Rust
138 lines
5.1 KiB
Rust
//! The per-agent closure KV bucket: one key per agent, holding the latest
|
|
//! system closure the swarm builder built for it from the swarm meta flake.
|
|
//!
|
|
//! **swarm-controller is the only writer**, and creates the bucket on its first
|
|
//! write. It writes a key from a builder's `Built` reply and from nothing else,
|
|
//! so a failed build leaves the previous record in place. The builder holds no
|
|
//! grant on this bucket.
|
|
//!
|
|
//! Keyed by the agent alone, like [`crate::agent_limits`]: an agent can move
|
|
//! between hives, and its closure goes with it.
|
|
|
|
use hive_types::NixBuilds;
|
|
|
|
/// The KV bucket closures are recorded in, keyed by agent name.
|
|
pub const BUCKET: &str = "agent-closures";
|
|
|
|
/// The stream backing [`BUCKET`]. `create_key_value` prefixes `KV_`.
|
|
pub const STREAM: &str = "KV_agent-closures";
|
|
|
|
/// The subject `agent`'s entry is stored under.
|
|
#[must_use]
|
|
pub fn key_subject(agent: &str) -> String {
|
|
format!("$KV.{BUCKET}.{agent}")
|
|
}
|
|
|
|
/// One agent's latest green build: the value under `BUCKET`/`<agent>`.
|
|
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
|
|
pub struct AgentClosure {
|
|
/// The `internal/meta` commit the closure was built from.
|
|
pub meta_rev: String,
|
|
/// The system closure's store path, in the builder host's store.
|
|
pub store_path: String,
|
|
/// The platform the closure was built for.
|
|
pub system: String,
|
|
/// When the controller recorded the build, in unix seconds.
|
|
pub built_unix: i64,
|
|
/// Whether the builder pushed the closure to the binary cache. A record
|
|
/// written before the field existed reads as false.
|
|
#[serde(default)]
|
|
pub cached: bool,
|
|
/// Whether the agent's evaluated config sets
|
|
/// `services.hyperhive.agent.user.privateUsers`: the agent runs in its own
|
|
/// user namespace. `None` when the builder could not read it, and for a
|
|
/// record written before the field existed: the record says nothing, and
|
|
/// the reader leaves the agent as its last start set it.
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
pub private_users: Option<bool>,
|
|
/// The agent's `services.hyperhive.agent.nixBuilds`: where its nix builds
|
|
/// run. `None` as for `private_users`.
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
pub nix_builds: Option<NixBuilds>,
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn names_are_pinned() {
|
|
assert_eq!(STREAM, format!("KV_{BUCKET}"));
|
|
assert_eq!(key_subject("iris"), "$KV.agent-closures.iris");
|
|
}
|
|
|
|
#[test]
|
|
fn the_record_serializes_as_pinned() {
|
|
let value = AgentClosure {
|
|
meta_rev: "abc".to_owned(),
|
|
store_path: "/nix/store/x".to_owned(),
|
|
system: "x86_64-linux".to_owned(),
|
|
built_unix: 7,
|
|
cached: true,
|
|
private_users: Some(true),
|
|
nix_builds: Some(NixBuilds::Disabled),
|
|
};
|
|
let wire = serde_json::json!({
|
|
"meta_rev": "abc",
|
|
"store_path": "/nix/store/x",
|
|
"system": "x86_64-linux",
|
|
"built_unix": 7,
|
|
"cached": true,
|
|
"private_users": true,
|
|
"nix_builds": "none",
|
|
});
|
|
assert_eq!(serde_json::to_value(&value).expect("encodes"), wire);
|
|
assert_eq!(
|
|
serde_json::from_value::<AgentClosure>(wire).expect("decodes"),
|
|
value
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn a_record_without_the_fields_reads_as_not_cached_and_says_nothing_of_the_options() {
|
|
let old = serde_json::json!({
|
|
"meta_rev": "abc",
|
|
"store_path": "/nix/store/x",
|
|
"system": "x86_64-linux",
|
|
"built_unix": 7,
|
|
});
|
|
let record = serde_json::from_value::<AgentClosure>(old).expect("decodes");
|
|
assert!(!record.cached);
|
|
assert_eq!(record.private_users, None);
|
|
assert_eq!(record.nix_builds, None);
|
|
}
|
|
|
|
/// An option the builder could not read is left out of the record, not
|
|
/// written as `null`: a reader that predates the option still decodes it.
|
|
#[test]
|
|
fn an_unread_option_is_left_out_of_the_record() {
|
|
// The shape a reader before the option had: a bool that defaults to false.
|
|
#[derive(serde::Deserialize)]
|
|
struct Before {
|
|
#[serde(default)]
|
|
private_users: bool,
|
|
}
|
|
let record = AgentClosure {
|
|
meta_rev: "abc".to_owned(),
|
|
store_path: "/nix/store/x".to_owned(),
|
|
system: "x86_64-linux".to_owned(),
|
|
built_unix: 7,
|
|
cached: true,
|
|
private_users: None,
|
|
nix_builds: Some(NixBuilds::Host),
|
|
};
|
|
let wire = serde_json::to_value(&record).expect("encodes");
|
|
let fields = wire.as_object().expect("an object");
|
|
assert!(!fields.contains_key("private_users"), "{wire}");
|
|
assert_eq!(fields["nix_builds"], "host");
|
|
assert!(
|
|
!serde_json::from_value::<Before>(wire.clone())
|
|
.expect("decodes")
|
|
.private_users
|
|
);
|
|
assert_eq!(
|
|
serde_json::from_value::<AgentClosure>(wire).expect("decodes"),
|
|
record
|
|
);
|
|
}
|
|
}
|