swarm-controller: pin what the backfill route queues
The mint route shipped without tests while the create route beside it has three, so the two properties that make it a backfill rather than a second creation route were unasserted: that it refuses before queuing, and that it queues the mint node and nothing else. Queuing a config-repo scaffold or a deploy against an agent that already exists is the failure the second of those catches, and it is invisible from the status code -- a version that queued the whole creation graph would answer 200 with a node id just the same. The agent name gets its own arm. create_agent's hive is checked against the swarm roster, which incidentally rejects a name that is not an identifier; an agent name has no roster to check against, so the parse is the only thing between a traversal and a store path built out of it. MintAgentIdentityResponse derives Clone + Debug to match CreateAgentResponse -- expect_err on the refusal arms needs Debug on the success type.
This commit is contained in:
parent
1442168715
commit
8b6dc72526
1 changed files with 102 additions and 1 deletions
|
|
@ -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 {
|
||||
|
|
|
|||
Loading…
Reference in a new issue