Watch
0
0
Fork
You've already forked hyperhive
0
hyperhive/hive-priv/src/infra.rs
atlas b79896c237 hive reads the forge for nix as its own account, not core
hive-c0re copies the hive's store read token to /run/hyperhive/forge-hive-token
for nix's git credential helper, and no longer mints, reads or falls back to
the core admin token. drops the core user sweep, avatar upload, webhook
cleanup and hive-priv's RunForgeAdmin.
2026-10-25 18:06:48 +01:00

151 lines
5.8 KiB
Rust

//! Host infrastructure ops: `ControlInfraContainer` (start/stop of an
//! [`InfraContainer`] unit) and `ReloadGatewayNginx`. Neither takes a unit or
//! container name from the caller.
use anyhow::{Context as _, Result, bail};
use hive_priv_sock::{InfraAction, InfraContainer};
use tokio::process::Command;
/// `ControlInfraContainer` — start/stop a hive infrastructure service via
/// `systemctl <verb> <unit>`. The [`InfraContainer`] enum is the allowlist:
/// serde already rejected any unknown / unsafe name (hive-c0re, the forge
/// and the matrix have no variant) at deserialisation, so no root-side
/// `.contains()` check is needed here. Serves the hive-wide
/// `hivectl stop`/`start`/`restart` flow. No agent-facing path exists.
///
/// ⚠️ The unit is derived from the variant, never sent by the caller —
/// which is what keeps this from being a general `systemctl` pass-through.
pub(crate) async fn control_infra_container(
container: InfraContainer,
action: InfraAction,
) -> Result<(String, String)> {
let verb = action.systemctl_verb();
let unit = container.service_unit();
let out = Command::new("systemctl")
.args([verb, unit])
.output()
.await
.with_context(|| format!("systemctl {verb} {unit}"))?;
if !out.status.success() {
bail!(
"systemctl {verb} {unit} exited {}: {}",
out.status,
String::from_utf8_lossy(&out.stderr).trim()
);
}
tracing::info!(target: "infra-control", "{verb} {unit}");
Ok((
String::from_utf8_lossy(&out.stdout).into_owned(),
String::from_utf8_lossy(&out.stderr).into_owned(),
))
}
/// Synchronise the host's nginx unit after an `agents.conf` write.
///
/// Queries `ActiveState` and dispatches:
/// - `active` → `systemctl reload nginx` (SIGHUP, zero-downtime)
/// - `failed` → `systemctl reset-failed nginx` + `systemctl start nginx`
/// - otherwise → `systemctl start nginx`
///
/// ⚠️ `nginx` is hard-coded on purpose — see `PrivRequest::ReloadGatewayNginx`.
/// The unit name is the scope of this verb: nginx is a host unit, so no
/// namespace bounds it and the literal is the only thing standing between
/// "reload the gateway" and "reload anything".
///
/// Returns `(String::new(), String::new())` on success so it fits the
/// `exec` return type directly.
pub(crate) async fn sync_gateway_nginx() -> Result<(String, String)> {
let state_out = Command::new("systemctl")
.args(["show", "--property=ActiveState", "--value", "nginx"])
.output()
.await
.context("query gateway nginx ActiveState")?;
if !state_out.status.success() {
tracing::warn!(
exit_code = ?state_out.status.code(),
stderr = %String::from_utf8_lossy(&state_out.stderr).trim(),
"systemctl show ActiveState exited non-zero — gateway nginx may be down"
);
}
let state = String::from_utf8_lossy(&state_out.stdout).trim().to_owned();
// State-aware dispatch: reload when running; reset+start after
// start-limit failure; plain start when inactive or unknown.
match state.as_str() {
"active" => {
let out = Command::new("systemctl")
.args(["reload", "nginx"])
.output()
.await
.context("reload gateway nginx")?;
if !out.status.success() {
bail!(
"gateway nginx reload failed ({}): {}",
out.status,
String::from_utf8_lossy(&out.stderr).trim()
);
}
}
"failed" => {
// Clear start-limit so the next start can proceed.
let _ = Command::new("systemctl")
.args(["reset-failed", "nginx"])
.status()
.await;
let out = Command::new("systemctl")
.args(["start", "nginx"])
.output()
.await
.context("start gateway nginx after reset-failed")?;
if !out.status.success() {
bail!(
"gateway nginx start (after reset-failed) failed ({}): {}",
out.status,
String::from_utf8_lossy(&out.stderr).trim()
);
}
}
_ => {
// inactive, activating, deactivating, unknown — just start.
let out = Command::new("systemctl")
.args(["start", "nginx"])
.output()
.await
.context("start gateway nginx")?;
if !out.status.success() {
bail!(
"gateway nginx start failed (state={state:?}) ({}): {}",
out.status,
String::from_utf8_lossy(&out.stderr).trim()
);
}
}
}
Ok((String::new(), String::new()))
}
#[cfg(test)]
mod tests {
use super::{InfraAction, InfraContainer};
use hive_priv_sock::PrivRequest;
/// A start/stop naming the forge or matrix container fails to decode, so
/// `run` answers it with a parse error before anything reaches
/// `systemctl`. Control: the same request naming the gateway decodes.
#[test]
fn infra_control_refuses_swarm_services() {
let req = |c: &str| {
format!(r#"{{"op":"control_infra_container","container":"{c}","action":"stop"}}"#)
};
for c in ["Forge", "Matrix"] {
let err = serde_json::from_str::<PrivRequest>(&req(c)).unwrap_err();
assert!(err.to_string().contains("unknown variant"), "{c}: {err}");
}
assert!(matches!(
serde_json::from_str::<PrivRequest>(&req("Gateway")),
Ok(PrivRequest::ControlInfraContainer {
container: InfraContainer::Gateway,
action: InfraAction::Stop,
})
));
}
}