diff --git a/swarm-controller/src/main.rs b/swarm-controller/src/main.rs index 3621ad39..04f69945 100644 --- a/swarm-controller/src/main.rs +++ b/swarm-controller/src/main.rs @@ -1515,7 +1515,7 @@ struct MintAgentIdentityRequest { } /// Success body of `POST /api/agents/{name}/identity`. -#[derive(Serialize, ToSchema)] +#[derive(Clone, Debug, Serialize, ToSchema)] struct MintAgentIdentityResponse { /// The queued node, so a caller can follow it in the job view. node_id: u64, @@ -2176,6 +2176,107 @@ mod tests { assert!(queued > 0, "an accepted creation must queue work"); } + /// The backfill route's roster check, asserted by effect for the same + /// reason its sibling above is: a refusal that queued first would still + /// re-mint the agent's certificate, which every running agent on the + /// named hive picks up on its next boot. + #[tokio::test] + async fn a_backfill_for_a_hive_outside_the_roster_queues_nothing() { + let (state, sched) = state_with_roster(); + + let err = super::mint_agent_identity( + axum::extract::State(state), + axum::extract::Path("atlas".to_owned()), + axum::Json(super::MintAgentIdentityRequest { + hive: "pr1maa".to_owned(), + }), + ) + .await + .expect_err("a hive outside the roster must be refused"); + + let rendered = format!("{err:?}"); + assert!( + rendered.contains("pr1ma"), + "the refusal should name the known hives, got: {rendered}" + ); + + let queued = sched + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner) + .graph() + .nodes() + .count(); + assert_eq!(queued, 0, "a refused backfill must queue no work"); + } + + /// A name that is not an identifier is refused before it can reach a + /// store path. `create_agent` gets this for free from the roster check + /// on the hive; the agent name has no roster to check against, so this + /// is the only thing standing between `../beta` and a path built out of + /// it. + #[tokio::test] + async fn a_backfill_for_a_name_that_is_not_an_identifier_queues_nothing() { + let (state, sched) = state_with_roster(); + + super::mint_agent_identity( + axum::extract::State(state), + axum::extract::Path("../beta".to_owned()), + axum::Json(super::MintAgentIdentityRequest { + hive: "pr1ma".to_owned(), + }), + ) + .await + .expect_err("a traversal in the agent name must be refused"); + + let queued = sched + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner) + .graph() + .nodes() + .count(); + assert_eq!(queued, 0, "a refused backfill must queue no work"); + } + + /// The accept arm, and the property that makes this a *backfill* route + /// rather than a second creation route: it queues the mint node **and + /// nothing else**. `create_agent`'s graph also scaffolds a config repo + /// and publishes a deploy; running either of those against an agent that + /// already exists is the failure this assertion exists to catch. + #[tokio::test] + async fn a_backfill_queues_the_mint_node_and_only_the_mint_node() { + let (state, sched) = state_with_roster(); + + let queued = super::mint_agent_identity( + axum::extract::State(state), + axum::extract::Path("atlas".to_owned()), + axum::Json(super::MintAgentIdentityRequest { + hive: "pr1ma".to_owned(), + }), + ) + .await + .expect("a hive in the roster must be accepted"); + + let guard = sched + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner); + let nodes: Vec<_> = guard.graph().nodes().collect(); + let [node] = nodes[..] else { + panic!("a backfill queues exactly one node, got {}", nodes.len()); + }; + assert!( + matches!( + &node.payload, + SwarmNodeKind::MintAgentIdentity { hive, agent } + if hive == "pr1ma" && agent == "atlas" + ), + "the one node must be the mint, carrying both names: {:?}", + node.payload + ); + // The id the operator is told to watch has to be the node that was + // actually inserted, or `swarmctl` prints a handle to nothing. + assert_eq!(queued.node_id, node.id.get()); + } + #[tokio::test] async fn create_repo_node_runs_end_to_end_and_fails_without_forge_configured() { unsafe {