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`.
|
/// Success body of `POST /api/agents/{name}/identity`.
|
||||||
#[derive(Serialize, ToSchema)]
|
#[derive(Clone, Debug, Serialize, ToSchema)]
|
||||||
struct MintAgentIdentityResponse {
|
struct MintAgentIdentityResponse {
|
||||||
/// The queued node, so a caller can follow it in the job view.
|
/// The queued node, so a caller can follow it in the job view.
|
||||||
node_id: u64,
|
node_id: u64,
|
||||||
|
|
@ -2176,6 +2176,107 @@ mod tests {
|
||||||
assert!(queued > 0, "an accepted creation must queue work");
|
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]
|
#[tokio::test]
|
||||||
async fn create_repo_node_runs_end_to_end_and_fails_without_forge_configured() {
|
async fn create_repo_node_runs_end_to_end_and_fails_without_forge_configured() {
|
||||||
unsafe {
|
unsafe {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue