70 lines
2.7 KiB
Rust
70 lines
2.7 KiB
Rust
//! Dashboard endpoint for operator-driven infra lifecycle (start / stop on
|
|
//! `hive-ci`, `hive-forge`, `hive-gateway`, `hive-matrix`). Operator-only —
|
|
//! there is no agent-facing equivalent for either action.
|
|
//! Already fully operator-authenticated by the time a request reaches here,
|
|
//! so no capability check is needed, just the audit trail.
|
|
|
|
use axum::{
|
|
extract::{Path as AxumPath, State},
|
|
http::StatusCode,
|
|
response::{IntoResponse, Response},
|
|
};
|
|
use hive_priv_sock::{InfraAction, InfraContainer};
|
|
|
|
use super::{AppState, error_response};
|
|
|
|
/// Start / stop a hive infrastructure container from the dashboard.
|
|
///
|
|
/// `name` parses into [`InfraContainer`] (the allowlist; unrecognised
|
|
/// names 400), `action` into `start` / `stop`. Every attempt lands in the
|
|
/// audit log (actor `"operator"`, action `start_infra` / `stop_infra`) and
|
|
/// streams as an `AuditEntryAdded` event.
|
|
#[utoipa::path(
|
|
post,
|
|
path = "/api/infra-container/{name}/{action}",
|
|
params(
|
|
("name" = String, Path, description = "infra service name (hive-ci/hive-forge/hive-gateway/hive-matrix)"),
|
|
("action" = String, Path, description = "start | stop"),
|
|
),
|
|
responses(
|
|
(status = 200, description = "action completed", body = String),
|
|
(status = 500, description = "unknown container/action, or the systemd action failed"),
|
|
),
|
|
tag = "infra_containers"
|
|
)]
|
|
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"),
|
|
other => {
|
|
return error_response(&format!("unknown action: {other} (want start|stop)"));
|
|
}
|
|
};
|
|
let target = container.name();
|
|
tracing::info!(%target, %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, target, outcome, detail.as_deref())
|
|
{
|
|
state.coord.emit_audit_entry(entry);
|
|
}
|
|
match result {
|
|
Ok(()) => (StatusCode::OK, "ok").into_response(),
|
|
Err(e) => error_response(&format!("{target}: {e:#}")),
|
|
}
|
|
}
|