//! 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_priv_sock::{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. #[utoipa::path( post, path = "/api/infra-container/{name}/{action}", params( ("name" = String, Path, description = "infra container name (hive-ci/hive-forge/hive-gateway/hive-matrix)"), ("action" = String, Path, description = "start | stop | restart"), ), 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, AxumPath((name, action)): AxumPath<(String, String)>, ) -> Response { let Ok(container) = name.parse::() 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:#}")), } }