swarm: add a declared "paused" agent wanted state
mara (#4170): swarm-ui's wanted-state dropdown could only ever declare up/offline/destroy, with no way to swarm-declare the existing hive-local turn-loop pause (`hivectl agent pause|resume`). `AgentState::Paused` is not a fifth peer of Up/Offline/Destroyed on the power axis this enum otherwise answers — it's Up plus an orthogonal turn-loop pause. `hive-c0re`'s `workers::wanted` reconcile loop now decides the two axes independently (`decide` for power, the new `decide_pause` for the marker), so a stopped agent declared Paused converges with both a Start and a Pause in the same pass. Known, deliberate limitation: a Paused declaration on an agent this hive has never deployed only reaches Deploy this pass — writing the pause marker into a harness dir that may not exist yet was judged not worth the risk, so it converges on the next pass once the agent is present instead. swarm-ui's WantedMenu gains a fourth "paused" option (warning-tone badge). No separate "resume" entry — selecting "up" from a paused row already clears the marker via the same decide_pause path. Pause/resume marker writes go through one shared Coordinator::set_paused_by_name helper, used by both the interactive dashboard pause/resume handlers and this reconcile loop, instead of each duplicating the parse-name/write-marker/track-rescan shape. swarm-ui's "offline" and "paused" confirm dialogs share one confirmTarget state and one ConfirmDialog instead of two near-identical copies. Closes #4170
This commit is contained in:
parent
560f727797
commit
513554fe9a
6 changed files with 483 additions and 55 deletions
|
|
@ -262,12 +262,8 @@ pub(super) async fn post_pause(
|
|||
if let Some(reject) = guard_agent_name(&state, &logical).await {
|
||||
return reject;
|
||||
}
|
||||
let ident = match Ident::parse(&logical) {
|
||||
Ok(i) => i,
|
||||
Err(e) => return (StatusCode::BAD_REQUEST, format!("bad agent name: {e}")).into_response(),
|
||||
};
|
||||
if let Err(e) = crate::coordinator::Coordinator::set_paused(&ident, true).await {
|
||||
return error_response(&format!("pause {logical}: {e}"));
|
||||
if let Err(e) = crate::coordinator::Coordinator::set_paused_by_name(&logical, true).await {
|
||||
return set_paused_error_response(&logical, &e);
|
||||
}
|
||||
state.coord.rescan_containers_and_emit().await;
|
||||
(StatusCode::OK, "ok").into_response()
|
||||
|
|
@ -297,17 +293,30 @@ pub(super) async fn post_resume(
|
|||
if let Some(reject) = guard_agent_name(&state, &logical).await {
|
||||
return reject;
|
||||
}
|
||||
let ident = match Ident::parse(&logical) {
|
||||
Ok(i) => i,
|
||||
Err(e) => return (StatusCode::BAD_REQUEST, format!("bad agent name: {e}")).into_response(),
|
||||
};
|
||||
if let Err(e) = crate::coordinator::Coordinator::set_paused(&ident, false).await {
|
||||
return error_response(&format!("resume {logical}: {e}"));
|
||||
if let Err(e) = crate::coordinator::Coordinator::set_paused_by_name(&logical, false).await {
|
||||
return set_paused_error_response(&logical, &e);
|
||||
}
|
||||
state.coord.rescan_containers_and_emit().await;
|
||||
(StatusCode::OK, "ok").into_response()
|
||||
}
|
||||
|
||||
/// Render a [`crate::coordinator::SetPausedByNameError`] as the response
|
||||
/// `post_pause`/`post_resume` both need: 400 for a bad name (the caller's
|
||||
/// mistake), 500 for a write failure (this host's).
|
||||
fn set_paused_error_response(
|
||||
logical: &str,
|
||||
e: &crate::coordinator::SetPausedByNameError,
|
||||
) -> Response {
|
||||
match e {
|
||||
crate::coordinator::SetPausedByNameError::BadName(_) => {
|
||||
(StatusCode::BAD_REQUEST, e.to_string()).into_response()
|
||||
}
|
||||
crate::coordinator::SetPausedByNameError::Write(_) => {
|
||||
error_response(&format!("{logical}: {e}"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Form fields for `post_resource_limits`. Both fields are optional strings;
|
||||
/// an empty value clears the per-agent override for that field, falling back
|
||||
/// to the hive-wide default.
|
||||
|
|
|
|||
Loading…
Reference in a new issue