controller gates logs and metrics on the hive capability and mirrors the grant
This commit is contained in:
parent
3b1d118f25
commit
e7e3281098
1 changed files with 519 additions and 38 deletions
|
|
@ -10,9 +10,11 @@ use axum::{
|
|||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use swarm_controller_api::agents::{CreateAgentRequest, CreateAgentResponse};
|
||||
use swarm_secret_client::observability::Grant;
|
||||
use utoipa::ToSchema;
|
||||
|
||||
use crate::jobq::{SwarmNodeKind, SwarmResourceKind, declare_agent_job};
|
||||
use crate::observability_grant::{self, grant_of, mirror, require_tolerant};
|
||||
use crate::routes::hives::HiveEntry;
|
||||
use crate::routes::{AppState, StatusUnavailable, error_problem, wanted_error_status};
|
||||
use crate::{
|
||||
|
|
@ -623,26 +625,18 @@ pub(crate) async fn create_agent(
|
|||
// Checked after the roster, so a mistyped hive stays a 400 while the queue
|
||||
// is down.
|
||||
let settings_queue = settings.queue(&state).await?;
|
||||
let requested = settings.permissions.permissions.clone().unwrap_or_default();
|
||||
if grant_of(&requested) != Grant::default() {
|
||||
require_tolerant(&settings_queue, &hive)
|
||||
.await
|
||||
.map_err(|why| error_problem(axum::http::StatusCode::CONFLICT, &why))?;
|
||||
}
|
||||
|
||||
// Agent names are one swarm-wide namespace: identity, forge user, store
|
||||
// path and matrix id all carry the bare name. The same name on the same
|
||||
// hive is that agent being re-created, and goes through.
|
||||
let _gate = state.create_gate.lock().await;
|
||||
let elsewhere = placements_elsewhere(
|
||||
&agent,
|
||||
&hive,
|
||||
|| queued_placements_now(&state),
|
||||
|| declarations_elsewhere(&state, &agent, &hive),
|
||||
)
|
||||
.await?;
|
||||
if !elsewhere.is_empty() {
|
||||
let detail = format!(
|
||||
"agent name {agent:?} is already placed on hive {} — agent names are unique across \
|
||||
the swarm; destroy it there first to move it, or choose another name",
|
||||
elsewhere.join(", ")
|
||||
);
|
||||
return Err(error_problem(axum::http::StatusCode::CONFLICT, &detail));
|
||||
}
|
||||
refuse_if_placed_elsewhere(&state, &agent, &hive).await?;
|
||||
// Stored ahead of the job graph, not once the agent is declared: the hive
|
||||
// reads these by agent name when it first runs the agent. A failure from
|
||||
// here on puts back what was stored, so a refused creation leaves no
|
||||
|
|
@ -657,6 +651,22 @@ pub(crate) async fn create_agent(
|
|||
settings.agents_md.as_deref(),
|
||||
)
|
||||
.await?;
|
||||
// The record this create replaced is what the mirror may still describe.
|
||||
// A create that names no list keeps the stored one, and its mirror with it.
|
||||
let earlier = stored
|
||||
.permissions
|
||||
.as_ref()
|
||||
.filter(|_| settings.permissions.permissions.is_some())
|
||||
.and_then(|replaced| replaced.previous())
|
||||
.and_then(|record| record.permissions.clone())
|
||||
.unwrap_or_default();
|
||||
if let Err(e) = mirror(&observability_grant::Store, &agent, &earlier, &requested).await {
|
||||
stored.roll_back(&settings_queue, &agent).await;
|
||||
return Err(error_problem(
|
||||
axum::http::StatusCode::INTERNAL_SERVER_ERROR,
|
||||
&format!("mirroring logs/metrics to the secret store failed: {e:#}"),
|
||||
));
|
||||
}
|
||||
let queued = {
|
||||
let mut sched = state
|
||||
.jobq
|
||||
|
|
@ -670,6 +680,8 @@ pub(crate) async fn create_agent(
|
|||
Ok(ids) => ids,
|
||||
Err(e) => {
|
||||
stored.roll_back(&settings_queue, &agent).await;
|
||||
// Best effort, like the roll back: the mirror follows the record.
|
||||
let _ = mirror(&observability_grant::Store, &agent, &requested, &earlier).await;
|
||||
return Err(error_problem(
|
||||
axum::http::StatusCode::INTERNAL_SERVER_ERROR,
|
||||
&e.to_string(),
|
||||
|
|
@ -685,6 +697,30 @@ pub(crate) async fn create_agent(
|
|||
}))
|
||||
}
|
||||
|
||||
/// A 409 when `agent` is already placed on a hive other than `hive`.
|
||||
async fn refuse_if_placed_elsewhere(
|
||||
state: &AppState,
|
||||
agent: &str,
|
||||
hive: &str,
|
||||
) -> Result<(), problem_details::ProblemDetails> {
|
||||
let elsewhere = placements_elsewhere(
|
||||
agent,
|
||||
hive,
|
||||
|| queued_placements_now(state),
|
||||
|| declarations_elsewhere(state, agent, hive),
|
||||
)
|
||||
.await?;
|
||||
if elsewhere.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
let detail = format!(
|
||||
"agent name {agent:?} is already placed on hive {} — agent names are unique across \
|
||||
the swarm; destroy it there first to move it, or choose another name",
|
||||
elsewhere.join(", ")
|
||||
);
|
||||
Err(error_problem(axum::http::StatusCode::CONFLICT, &detail))
|
||||
}
|
||||
|
||||
/// The settings a `POST /api/agents` body gives, as they are stored.
|
||||
struct CreateSettings {
|
||||
limits: Option<swarm_wire::agent_limits::AgentCaps>,
|
||||
|
|
@ -1552,6 +1588,35 @@ mod tests {
|
|||
assert_eq!(node_count(&state.jobq), 0, "nothing may be queued");
|
||||
}
|
||||
|
||||
/// A create naming `logs` or `metrics` on a hive that does not advertise
|
||||
/// tolerant decoding is a 409 that stores and queues nothing. Control: the
|
||||
/// same create without them is accepted by that hive.
|
||||
#[tokio::test]
|
||||
async fn a_create_granting_logs_needs_a_tolerant_hive() {
|
||||
let nats = crate::test_nats::TestNats::start();
|
||||
let client = nats.client().await;
|
||||
let state = crate::agent_limits::tests::state(&client);
|
||||
let create = |perms: &str| {
|
||||
let req: CreateAgentRequest = serde_json::from_str(&format!(
|
||||
r#"{{"name":"atlas","hive":"pr1ma","permissions":[{perms}]}}"#
|
||||
))
|
||||
.expect("body parses");
|
||||
super::create_agent(axum::extract::State(state.clone()), axum::Json(req))
|
||||
};
|
||||
let err = create(r#""messaging","logs""#).await.expect_err("refused");
|
||||
assert_eq!(
|
||||
axum::response::IntoResponse::into_response(err).status(),
|
||||
axum::http::StatusCode::CONFLICT
|
||||
);
|
||||
assert_eq!(
|
||||
crate::agent_permissions::tests::published(&client, "atlas").await,
|
||||
None
|
||||
);
|
||||
let _ = create(r#""messaging""#)
|
||||
.await
|
||||
.expect("control: no log grant");
|
||||
}
|
||||
|
||||
/// `state_with_roster` with a second hive, `sec0nd`.
|
||||
async fn state_with_two_hives() -> (
|
||||
crate::test_nats::TestNats,
|
||||
|
|
|
|||
Loading…
Reference in a new issue