//! 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`/``. #[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, /// 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, } #[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::(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::(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::(wire.clone()) .expect("decodes") .private_users ); assert_eq!( serde_json::from_value::(wire).expect("decodes"), record ); } }