Skip to main content

alopex_server/http/
admin.rs

1use std::net::SocketAddr;
2use std::sync::Arc;
3
4use axum::extract::{ConnectInfo, Extension};
5use axum::http::StatusCode;
6use axum::middleware;
7use axum::response::{IntoResponse, Response};
8use axum::{Json, Router};
9use serde::Serialize;
10
11use crate::ops::status::StatusReporter;
12use crate::server::ServerState;
13
14#[derive(Serialize)]
15struct StatusResponse {
16    status: &'static str,
17}
18
19pub fn router(state: Arc<ServerState>) -> Router {
20    Router::new()
21        .route("/healthz", axum::routing::get(healthz))
22        .route("/status", axum::routing::get(status))
23        .route("/metrics", axum::routing::get(metrics))
24        .layer(middleware::from_fn(allowlist_middleware))
25        .layer(axum::Extension(state))
26}
27
28async fn healthz() -> impl IntoResponse {
29    StatusCode::OK
30}
31
32async fn status() -> impl IntoResponse {
33    Json(StatusResponse { status: "ok" })
34}
35
36async fn metrics(Extension(state): Extension<Arc<ServerState>>) -> Response {
37    if !state.config.metrics_enabled {
38        return StatusCode::NOT_FOUND.into_response();
39    }
40    let reporter = StatusReporter::new(state.lifecycle_state.clone(), state.recovery_info.clone());
41    reporter.refresh_metrics(&state.metrics);
42    match state.cluster_status_snapshot() {
43        Ok(snapshot) => state.metrics.record_cluster_status(&snapshot),
44        Err(err) => return (StatusCode::INTERNAL_SERVER_ERROR, err.to_string()).into_response(),
45    }
46    match state.metrics.expose_prometheus() {
47        Ok(body) => (
48            StatusCode::OK,
49            [(
50                axum::http::header::CONTENT_TYPE,
51                "text/plain; version=0.0.4",
52            )],
53            body,
54        )
55            .into_response(),
56        Err(err) => (StatusCode::INTERNAL_SERVER_ERROR, err.to_string()).into_response(),
57    }
58}
59
60async fn allowlist_middleware(
61    Extension(state): Extension<Arc<ServerState>>,
62    req: axum::extract::Request,
63    next: middleware::Next,
64) -> Response {
65    if state.config.admin_bind.ip().is_loopback() {
66        return next.run(req).await;
67    }
68    let Some(addr) = req.extensions().get::<ConnectInfo<SocketAddr>>() else {
69        return StatusCode::FORBIDDEN.into_response();
70    };
71    let ip = addr.ip();
72    if ip.is_loopback() || state.config.admin_allowlist.contains(&ip) {
73        next.run(req).await
74    } else {
75        StatusCode::FORBIDDEN.into_response()
76    }
77}