feat: add infra container start/stop/restart tab to C0R3 page
New POST /api/infra-container/{name}/{action} dashboard route (start/
stop/restart on hive-ci/hive-forge/hive-gateway/hive-matrix), reusing
the existing priv_client::control_infra_container helper the
infra_admin agent path already uses, plus an audit_log entry per
attempt. Adds infra_containers to the /api/state StateSnapshot (name +
live running status via systemctl is-active). New 1NFR4 sub-tab on the
C0R3 dashboard page: one row per infra container with a running/
stopped badge and start/stop/restart buttons, polled every 5s while
the sub-tab is open.
This commit is contained in:
parent
79d4c345bb
commit
ef14641b94
7 changed files with 229 additions and 5 deletions
63
hive-c0re/src/dashboard/infra_containers.rs
Normal file
63
hive-c0re/src/dashboard/infra_containers.rs
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
//! Dashboard endpoint for operator-driven infra-container lifecycle
|
||||
//! (start / stop / restart on `hive-ci`, `hive-forge`, `hive-gateway`,
|
||||
//! `hive-matrix`). Parallels the `infra_admin`-gated agent path in
|
||||
//! `socket_server/lifecycle_handlers.rs::handle_restart_infra`, but this one
|
||||
//! is reached from the dashboard — already fully operator-authenticated —
|
||||
//! so no capability check is needed here, just the same audit trail.
|
||||
|
||||
use axum::{
|
||||
extract::{Path as AxumPath, State},
|
||||
http::StatusCode,
|
||||
response::{IntoResponse, Response},
|
||||
};
|
||||
use hive_sh4re::priv_proto::{InfraAction, InfraContainer};
|
||||
|
||||
use super::{AppState, error_response};
|
||||
|
||||
/// `POST /api/infra-container/{name}/{action}` — start / stop / restart a
|
||||
/// hive infrastructure container from the dashboard. `name` parses into
|
||||
/// [`InfraContainer`] (the allowlist; unrecognised names 400), `action`
|
||||
/// into `start` / `stop` / `restart`. Every attempt lands in the audit log
|
||||
/// (actor `"operator"`, action `start_infra` / `stop_infra` /
|
||||
/// `restart_infra`) and streams as an `AuditEntryAdded` event, so
|
||||
/// operator-driven and agent-driven (`infra_admin`) infra actions show up
|
||||
/// in the same AUDIT view.
|
||||
pub(super) async fn post_infra_container(
|
||||
State(state): State<AppState>,
|
||||
AxumPath((name, action)): AxumPath<(String, String)>,
|
||||
) -> Response {
|
||||
let Ok(container) = name.parse::<InfraContainer>() else {
|
||||
return error_response(&format!("unknown infra container: {name}"));
|
||||
};
|
||||
let (infra_action, action_label) = match action.as_str() {
|
||||
"start" => (InfraAction::Start, "start_infra"),
|
||||
"stop" => (InfraAction::Stop, "stop_infra"),
|
||||
"restart" => (InfraAction::Restart, "restart_infra"),
|
||||
other => {
|
||||
return error_response(&format!(
|
||||
"unknown action: {other} (want start|stop|restart)"
|
||||
));
|
||||
}
|
||||
};
|
||||
let unit = container.unit_name();
|
||||
tracing::info!(%unit, %action, "dashboard: infra container action");
|
||||
let result = crate::priv_client::control_infra_container(container, infra_action).await;
|
||||
let outcome = if result.is_ok() {
|
||||
crate::audit_log::AuditOutcome::Ok
|
||||
} else {
|
||||
crate::audit_log::AuditOutcome::Err
|
||||
};
|
||||
let detail = result.as_ref().err().map(|e| format!("{e:#}"));
|
||||
if let Some(entry) =
|
||||
state
|
||||
.coord
|
||||
.audit_log
|
||||
.record("operator", action_label, unit, outcome, detail.as_deref())
|
||||
{
|
||||
state.coord.emit_audit_entry(entry);
|
||||
}
|
||||
match result {
|
||||
Ok(()) => (StatusCode::OK, "ok").into_response(),
|
||||
Err(e) => error_response(&format!("{unit}: {e:#}")),
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue