gate each deploy on a swarm build of the agent from internal/meta
This commit is contained in:
parent
3db835fb57
commit
5e64048e53
3 changed files with 1570 additions and 178 deletions
67
swarm-wire/src/agent_closures.rs
Normal file
67
swarm-wire/src/agent_closures.rs
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
//! 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.
|
||||
|
||||
/// 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,
|
||||
}
|
||||
|
||||
#[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,
|
||||
};
|
||||
let wire = serde_json::json!({
|
||||
"meta_rev": "abc",
|
||||
"store_path": "/nix/store/x",
|
||||
"system": "x86_64-linux",
|
||||
"built_unix": 7,
|
||||
});
|
||||
assert_eq!(serde_json::to_value(&value).expect("encodes"), wire);
|
||||
assert_eq!(
|
||||
serde_json::from_value::<AgentClosure>(wire).expect("decodes"),
|
||||
value
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
//! The swarm builder's requests: the subjects swarm-controller sends a build,
|
||||
//! an archetype evaluation and a meta-flake lock on, and what travels each way.
|
||||
//! an archetype evaluation, a meta-flake lock and an agent build from the meta
|
||||
//! flake on, and what travels each way.
|
||||
//!
|
||||
//! Three crates name these: the controller sends the requests, the builder
|
||||
//! answers them, and the auth-callout responder grants the subjects to each.
|
||||
|
|
@ -115,6 +116,41 @@ pub enum LockReply {
|
|||
Failed { error_tail: String },
|
||||
}
|
||||
|
||||
/// The subject an agent build from the swarm meta flake goes to, with the same
|
||||
/// one publisher and one subscriber as [`SUBJECT`]. A subject of its own, so a
|
||||
/// builder that predates the request has no subscriber and the controller gets
|
||||
/// no responders at once.
|
||||
pub const META_SUBJECT: &str = "$SWARM.builder.build-meta";
|
||||
|
||||
/// The swarm meta flake's repo on the forge, as `<org>/<repo>`.
|
||||
pub const META_REPO_PATH: &str = "internal/meta";
|
||||
|
||||
/// One agent built from the swarm meta flake: the toplevel of
|
||||
/// `nixosConfigurations.<agent>`, with every input at the flake's own lock.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct BuildFromMeta {
|
||||
/// The agent, which is also its name in the meta flake.
|
||||
pub agent: String,
|
||||
/// The `internal/meta` commit to build, a full 40-hex id.
|
||||
pub meta_rev: String,
|
||||
}
|
||||
|
||||
/// The builder's answer to one [`BuildFromMeta`].
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(tag = "state", rename_all = "kebab-case")]
|
||||
pub enum FromMetaReply {
|
||||
/// The toplevel built; `out_path` is its store path in the builder host's
|
||||
/// store and `system` the platform the builder host builds for.
|
||||
Built { out_path: String, system: String },
|
||||
/// `nix` ran and the build did not finish: the last lines it printed, or
|
||||
/// the deadline it hit. The controller holds the deploy back.
|
||||
Failed { error_tail: String },
|
||||
/// The builder did not get to run `nix`: the build slot stayed busy or the
|
||||
/// forge token could not be read. Says nothing about the config, so the
|
||||
/// controller tries again.
|
||||
Unavailable { reason: String },
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use serde_json::json;
|
||||
|
|
@ -199,11 +235,53 @@ mod tests {
|
|||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_meta_build_request_and_reply_serialize_as_pinned() {
|
||||
let req = BuildFromMeta {
|
||||
agent: "atlas".to_owned(),
|
||||
meta_rev: "abc".to_owned(),
|
||||
};
|
||||
let value = serde_json::to_value(&req).expect("encodes");
|
||||
assert_eq!(value, json!({"agent": "atlas", "meta_rev": "abc"}));
|
||||
assert_eq!(
|
||||
serde_json::from_value::<BuildFromMeta>(value).expect("decodes"),
|
||||
req
|
||||
);
|
||||
for (reply, wire) in [
|
||||
(
|
||||
FromMetaReply::Built {
|
||||
out_path: "/nix/store/x".to_owned(),
|
||||
system: "x86_64-linux".to_owned(),
|
||||
},
|
||||
json!({"state": "built", "out_path": "/nix/store/x", "system": "x86_64-linux"}),
|
||||
),
|
||||
(
|
||||
FromMetaReply::Failed {
|
||||
error_tail: "e".to_owned(),
|
||||
},
|
||||
json!({"state": "failed", "error_tail": "e"}),
|
||||
),
|
||||
(
|
||||
FromMetaReply::Unavailable {
|
||||
reason: "r".to_owned(),
|
||||
},
|
||||
json!({"state": "unavailable", "reason": "r"}),
|
||||
),
|
||||
] {
|
||||
assert_eq!(serde_json::to_value(&reply).expect("encodes"), wire);
|
||||
assert_eq!(
|
||||
serde_json::from_value::<FromMetaReply>(wire).expect("decodes"),
|
||||
reply
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_subjects_are_pinned() {
|
||||
assert_eq!(SUBJECT, "$SWARM.builder.build");
|
||||
assert_eq!(ARCHETYPES_SUBJECT, "$SWARM.builder.archetypes");
|
||||
assert_eq!(LOCK_SUBJECT, "$SWARM.builder.lock-meta");
|
||||
assert_eq!(META_SUBJECT, "$SWARM.builder.build-meta");
|
||||
assert_eq!(QUEUE_GROUP, "builder");
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@
|
|||
//! grants the subject) depends on this one without linking either.
|
||||
|
||||
pub mod agent_archetype;
|
||||
pub mod agent_closures;
|
||||
pub mod agent_icon;
|
||||
pub mod agent_inference;
|
||||
pub mod agent_limits;
|
||||
|
|
|
|||
Loading…
Reference in a new issue