hive: stop controlling the swarm's forge and matrix containers
The forge and matrix are swarm services; per mara's ruling on #5475 (shape a) a hive no longer starts, stops or restarts them. - hive-priv-sock: InfraContainer keeps only Gateway; InfraTarget goes, service_unit maps the gateway to nginx.service directly. A ControlInfraContainer naming Forge/Matrix fails to decode. - hive-host-sock: LifecycleScope drops forge/matrix and refuses unknown fields, so an older hivectl's --forge is an error rather than an all-false scope meaning everything. - hive-c0re: scoped_infra selects only the gateway. - hivectl: --forge/--matrix removed from stop/start/restart. - docs: services.md says how the forge and matrix are managed; security.md, coordinator.md and hivectl-cli.md follow. - hive-host-sock, hivectl/power.rs: Stop and RestartScoped doc comments name the gateway (host nginx.service), not infra containers.
This commit is contained in:
parent
be92bc0835
commit
d4f3d1ef8e
12 changed files with 177 additions and 203 deletions
|
|
@ -363,13 +363,12 @@ pub async fn set_agent_paused(agent_name: &str, paused: bool) -> Result<()> {
|
|||
.await?)
|
||||
}
|
||||
|
||||
/// Start / stop a hive infrastructure service (`hive-gateway`, `hive-forge`,
|
||||
/// `hive-matrix`) on the host via `systemctl <action> <unit>`,
|
||||
/// where the unit is derived root-side from the variant
|
||||
/// (`container@<name>.service`, or `nginx.service` for the gateway). The
|
||||
/// [`InfraContainer`] enum is the allowlist — hive-priv needs no name
|
||||
/// re-validation. Used by the hive-wide `hivectl stop` / `start` flow and
|
||||
/// the dashboard's operator-only infra panel. No agent-facing path exists.
|
||||
/// Start / stop a hive infrastructure service (`hive-gateway`) on the host
|
||||
/// via `systemctl <action> <unit>`, where the unit is derived root-side from
|
||||
/// the variant (`nginx.service` for the gateway). The [`InfraContainer`]
|
||||
/// enum is the allowlist — hive-priv needs no name re-validation. Used by
|
||||
/// the hive-wide `hivectl stop` / `start` / `restart` flow. No agent-facing
|
||||
/// path exists.
|
||||
pub async fn control_infra_container(container: InfraContainer, action: InfraAction) -> Result<()> {
|
||||
ok(call(&PrivRequest::ControlInfraContainer { container, action }).await?)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -570,9 +570,9 @@ async fn submit_single(coord: &Arc<Coordinator>, name: &str, verb: Verb) -> Host
|
|||
}
|
||||
}
|
||||
|
||||
/// Stop the given `agents` (resolved logical names) then `infra` containers
|
||||
/// Stop the given `agents` (resolved logical names) then `infra` services
|
||||
/// (`hivectl stop`). Agents go down before infra so they're not mid-request
|
||||
/// against a forge/matrix that's already gone. Per-target failures are
|
||||
/// through a gateway that's already gone. Per-target failures are
|
||||
/// aggregated rather than aborting on the first error, mirroring
|
||||
/// `finish_lifecycle` below. Callers resolve the [`LifecycleScope`] to these
|
||||
/// explicit name lists up front — this never sees the "all" flag.
|
||||
|
|
@ -582,7 +582,7 @@ async fn submit_single(coord: &Arc<Coordinator>, name: &str, verb: Verb) -> Host
|
|||
/// a hard stop a plain stop DAG — both persist `wanted = Offline` and
|
||||
/// serialize on the agent's lease so nothing races an in-flight
|
||||
/// rebuild. The response carries the DAG ids so `hivectl` can wait
|
||||
/// with per-node progress. Infra containers have no harness / lease
|
||||
/// with per-node progress. Infra services have no harness / lease
|
||||
/// and stay direct + synchronous.
|
||||
async fn handle_stop(
|
||||
coord: &Arc<Coordinator>,
|
||||
|
|
@ -608,8 +608,8 @@ async fn handle_stop(
|
|||
}
|
||||
}
|
||||
|
||||
// Agents go down before infra so they're not mid-request against a
|
||||
// forge/matrix that's already gone. Hard stops are quick kills —
|
||||
// Agents go down before infra so they're not mid-request through a
|
||||
// gateway that's already gone. Hard stops are quick kills —
|
||||
// await their DAGs (bounded) before touching infra. Graceful stops
|
||||
// keep the immediate return (drains take minutes and the
|
||||
// agents-then-infra race pre-existed there).
|
||||
|
|
@ -659,9 +659,9 @@ async fn await_dags(coord: &Arc<Coordinator>, ids: &[u64], timeout: std::time::D
|
|||
}
|
||||
}
|
||||
|
||||
/// Start the given `infra` containers then `agents` (`hivectl start`) — the
|
||||
/// Start the given `infra` services then `agents` (`hivectl start`) — the
|
||||
/// inverse of [`handle_stop`]. Infra comes up before agents so the agents
|
||||
/// find forge/matrix/gateway ready. Per-target failures aggregated. Callers
|
||||
/// find the gateway ready. Per-target failures aggregated. Callers
|
||||
/// resolve the [`LifecycleScope`] to these explicit name lists up front.
|
||||
async fn handle_start(
|
||||
coord: &Arc<Coordinator>,
|
||||
|
|
@ -713,7 +713,7 @@ async fn handle_start(
|
|||
/// concurrently on their own leases), not N separate DAGs — a hive-wide
|
||||
/// restart is one job. `graceful` prepends signal→drain per agent. No
|
||||
/// client-side stop-then-start composition, so a dropped `hivectl`
|
||||
/// connection never strands an agent. Infra containers have no lease/DAG
|
||||
/// connection never strands an agent. Infra services have no lease/DAG
|
||||
/// and restart synchronously (stop then start).
|
||||
async fn handle_restart_scoped(
|
||||
coord: &Arc<Coordinator>,
|
||||
|
|
@ -814,22 +814,15 @@ async fn scoped_agents(scope: &LifecycleScope) -> Result<Vec<String>> {
|
|||
Ok(set.into_iter().collect())
|
||||
}
|
||||
|
||||
/// Resolve which infra containers a scope targets. An "everything" scope
|
||||
/// (no flags set) selects all controllable infra; otherwise each set flag
|
||||
/// maps to its [`InfraContainer`]. Fixed order for deterministic output.
|
||||
/// Resolve which infra services a scope targets: the gateway, when the
|
||||
/// scope is "everything" (no flags set) or names it. The swarm's forge and
|
||||
/// matrix are never a target.
|
||||
fn scoped_infra(scope: &LifecycleScope) -> Vec<InfraContainer> {
|
||||
let everything = scope.is_everything();
|
||||
let mut out = Vec::new();
|
||||
if everything || scope.forge {
|
||||
out.push(InfraContainer::Forge);
|
||||
if scope.is_everything() || scope.gateway {
|
||||
vec![InfraContainer::Gateway]
|
||||
} else {
|
||||
Vec::new()
|
||||
}
|
||||
if everything || scope.gateway {
|
||||
out.push(InfraContainer::Gateway);
|
||||
}
|
||||
if everything || scope.matrix {
|
||||
out.push(InfraContainer::Matrix);
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// Build the aggregated lifecycle response: `ok` with the touched names when
|
||||
|
|
@ -916,4 +909,42 @@ mod tests {
|
|||
assert!(message.starts_with("request timed out"), "{message}");
|
||||
handler.await.expect("join").expect("clean close");
|
||||
}
|
||||
|
||||
/// A bare `hivectl stop` / `start` / `restart` touches the gateway and no
|
||||
/// swarm service. Control: an agents-only scope touches no infra.
|
||||
#[test]
|
||||
fn everything_scope_selects_only_the_gateway() {
|
||||
assert_eq!(
|
||||
scoped_infra(&LifecycleScope::default()),
|
||||
[InfraContainer::Gateway]
|
||||
);
|
||||
let gateway = LifecycleScope {
|
||||
gateway: true,
|
||||
..LifecycleScope::default()
|
||||
};
|
||||
assert_eq!(scoped_infra(&gateway), [InfraContainer::Gateway]);
|
||||
let agents = LifecycleScope {
|
||||
agents: true,
|
||||
..LifecycleScope::default()
|
||||
};
|
||||
assert!(scoped_infra(&agents).is_empty());
|
||||
}
|
||||
|
||||
/// A stop from an older `hivectl stop --forge` is refused rather than
|
||||
/// read as an all-false scope, which would stop everything. Control: the
|
||||
/// same request with `gateway` decodes.
|
||||
#[test]
|
||||
fn stop_naming_forge_or_matrix_is_refused() {
|
||||
let req = |field: &str| {
|
||||
format!(r#"{{"cmd":"stop","scope":{{"{field}":true}},"graceful":false}}"#)
|
||||
};
|
||||
for field in ["forge", "matrix"] {
|
||||
let err = serde_json::from_str::<HostRequest>(&req(field)).unwrap_err();
|
||||
assert!(err.to_string().contains("unknown field"), "{field}: {err}");
|
||||
}
|
||||
assert!(matches!(
|
||||
serde_json::from_str::<HostRequest>(&req("gateway")),
|
||||
Ok(HostRequest::Stop { scope, .. }) if scope.gateway && !scope.is_everything()
|
||||
));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue