Watch
0
0
Fork
You've already forked hyperhive
0

swarmctl shares the create-agent, inference and permissions shapes with the controller

This commit is contained in:
atlas 2026-10-19 17:57:21 +02:00
commit 0831b85b73

View file

@ -15,10 +15,9 @@ use axum::Json;
use axum::extract::{Path, State};
use axum::http::StatusCode;
use problem_details::ProblemDetails;
use serde::{Deserialize, Serialize};
use swarm_controller_api::limits::Caps;
use swarm_queue_client::agent_limits;
use swarm_wire::agent_limits::AgentCaps;
use utoipa::ToSchema;
use crate::routes::{AppState, error_problem, wanted_error_status};
use crate::schedules::{live, queue, read_or_default, refused, write_revision};
@ -27,45 +26,19 @@ use crate::schedules::{live, queue, read_or_default, refused, write_revision};
/// `wanted::MAX_ATTEMPTS`.
const MAX_ATTEMPTS: usize = 5;
/// One agent's caps. A field left out is not capped by the swarm.
///
/// Each value is stored as given, and only in a form systemd applies: a value
/// systemd ignores ("Invalid …, ignoring") would leave the agent uncapped,
/// since a swarm cap replaces the hive's answer.
#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize, ToSchema)]
#[serde(deny_unknown_fields)]
pub struct Caps {
/// systemd `CPUQuota=`: digits, optionally `.` and one or two digits, then
/// `%`; above zero and at most `21474836.47%`. `"100%"` is one core.
#[serde(default, skip_serializing_if = "Option::is_none")]
#[schema(example = "400%")]
cpu_quota: Option<String>,
/// systemd `MemoryMax=`: `infinity`; a percentage of physical memory
/// written as for `cpu_quota`, above zero and at most `100%`; or a size:
/// digits, optionally `.` and digits, then nothing, `B` or one of
/// `K`/`M`/`G`/`T` (1024-based, upper case), at least one byte and below
/// 2^64 - 1 bytes.
#[serde(default, skip_serializing_if = "Option::is_none")]
#[schema(example = "8G")]
memory_max: Option<String>,
/// The caps to store, or why they are refused.
pub(crate) fn into_stored(caps: &Caps) -> Result<AgentCaps, String> {
Ok(AgentCaps {
cpu_quota: caps.cpu_quota.as_deref().map(cpu_quota).transpose()?,
memory_max: caps.memory_max.as_deref().map(memory_max).transpose()?,
})
}
impl Caps {
/// The value to store, or why it is refused.
pub(crate) fn into_stored(self) -> Result<AgentCaps, String> {
Ok(AgentCaps {
cpu_quota: self.cpu_quota.as_deref().map(cpu_quota).transpose()?,
memory_max: self.memory_max.as_deref().map(memory_max).transpose()?,
})
}
}
impl From<AgentCaps> for Caps {
fn from(v: AgentCaps) -> Self {
Self {
cpu_quota: v.cpu_quota,
memory_max: v.memory_max,
}
/// The stored caps as the API shows them.
fn view(v: AgentCaps) -> Caps {
Caps {
cpu_quota: v.cpu_quota,
memory_max: v.memory_max,
}
}
@ -213,7 +186,7 @@ pub(crate) async fn get_agent_limits(
let (current, _, _) = read_or_default::<AgentCaps>(&store, &agent)
.await
.map_err(|e| queue_problem(&e))?;
Ok(Json(current.into()))
Ok(Json(view(current)))
}
/// Set the CPU and memory caps the swarm declares for one agent.
@ -244,9 +217,7 @@ pub(crate) async fn put_agent_limits(
Json(req): Json<Caps>,
) -> Result<Json<Caps>, ProblemDetails> {
let agent = agent_name(&name).map_err(refused)?;
let value = req
.into_stored()
.map_err(|why| error_problem(StatusCode::BAD_REQUEST, &why))?;
let value = into_stored(&req).map_err(|why| error_problem(StatusCode::BAD_REQUEST, &why))?;
let client = queue(&state).map_err(refused)?;
if !live(&state).await?.contains(&agent) {
return Err(error_problem(
@ -255,7 +226,7 @@ pub(crate) async fn put_agent_limits(
));
}
store(&client, &agent, &value).await?;
Ok(Json(value.into()))
Ok(Json(view(value)))
}
/// What [`store`] replaced, or `None` when it wrote nothing because the caps
@ -785,10 +756,7 @@ pub(crate) mod tests {
let nats = TestNats::start();
let client = nats.client().await;
let caps = |json: &str| {
serde_json::from_str::<Caps>(json)
.expect("caps")
.into_stored()
.expect("valid")
super::into_stored(&serde_json::from_str::<Caps>(json).expect("caps")).expect("valid")
};
let first = super::store(&client, "atlas", &caps(CAPPED))
@ -814,10 +782,7 @@ pub(crate) mod tests {
let nats = TestNats::start();
let client = nats.client().await;
let caps = |json: &str| {
serde_json::from_str::<Caps>(json)
.expect("caps")
.into_stored()
.expect("valid")
super::into_stored(&serde_json::from_str::<Caps>(json).expect("caps")).expect("valid")
};
let other = caps(r#"{"cpu_quota":"100%"}"#);