Skip to main content

greentic_runner_host/http/
admin.rs

1use axum::Json;
2use axum::extract::State;
3use axum::http::StatusCode;
4use axum::response::IntoResponse;
5use serde_json::json;
6use time::format_description::well_known::Rfc3339;
7
8use crate::http::auth::AdminGuard;
9use crate::runner::ServerState;
10
11pub async fn status(AdminGuard: AdminGuard, State(state): State<ServerState>) -> impl IntoResponse {
12    let snapshot = state.active.snapshot();
13    let tenants = snapshot
14        .iter()
15        .map(|(key, runtime)| {
16            let pack = runtime.pack();
17            let metadata = pack.metadata();
18            let required_secrets = runtime.required_secrets();
19            let missing_secrets = runtime.missing_secrets();
20            let overlays = runtime
21                .overlays()
22                .into_iter()
23                .zip(runtime.overlay_digests())
24                .map(|(overlay, digest)| {
25                    let meta = overlay.metadata();
26                    json!({
27                        "pack_id": meta.pack_id,
28                        "version": meta.version,
29                        "digest": digest,
30                    })
31                })
32                .collect::<Vec<_>>();
33            json!({
34                "tenant": key.tenant,
35                "pack_id": metadata.pack_id,
36                "version": metadata.version,
37                "digest": runtime.digest(),
38                "overlays": overlays,
39                "required_secrets": required_secrets,
40                "missing_secrets": missing_secrets,
41            })
42        })
43        .collect::<Vec<_>>();
44
45    let health = state.health.snapshot();
46    let last_reload = health.last_reload.and_then(|ts| ts.format(&Rfc3339).ok());
47
48    Json(json!({
49        "tenants": tenants,
50        "active": snapshot.len(),
51        "last_reload": last_reload,
52        "last_error": health.last_error,
53    }))
54}
55
56pub async fn reload(AdminGuard: AdminGuard, State(state): State<ServerState>) -> impl IntoResponse {
57    if let Some(handle) = &state.reload {
58        match handle.trigger().await {
59            Ok(()) => {
60                tracing::info!("pack.reload.requested");
61                (
62                    StatusCode::ACCEPTED,
63                    Json(json!({ "status": "reload requested" })),
64                )
65            }
66            Err(err) => {
67                tracing::warn!(error = %err, "reload trigger failed");
68                (
69                    StatusCode::INTERNAL_SERVER_ERROR,
70                    Json(json!({ "error": err.to_string() })),
71                )
72            }
73        }
74    } else {
75        (
76            StatusCode::NOT_IMPLEMENTED,
77            Json(json!({ "error": "reload handle unavailable" })),
78        )
79    }
80}
81
82#[cfg(test)]
83mod tests {
84    use super::*;
85    use crate::http::auth::AdminAuth;
86    use crate::http::health::HealthState;
87    use crate::routing::{RoutingConfig, TenantRouting};
88    use crate::runtime::ActivePacks;
89    use axum::body::to_bytes;
90    use axum::response::Response;
91    use std::sync::Arc;
92
93    fn state() -> ServerState {
94        ServerState {
95            active: Arc::new(ActivePacks::new()),
96            routing: TenantRouting::new(RoutingConfig::default()),
97            health: Arc::new(HealthState::new()),
98            reload: None,
99            admin: AdminAuth::default(),
100        }
101    }
102
103    async fn json_body(response: Response) -> serde_json::Value {
104        let body = to_bytes(response.into_body(), usize::MAX)
105            .await
106            .expect("read response body");
107        serde_json::from_slice(&body).expect("json body")
108    }
109
110    #[tokio::test]
111    async fn status_reports_empty_runtime_snapshot() {
112        let state = state();
113        state.health.record_reload_success();
114
115        let response = status(AdminGuard, State(state)).await.into_response();
116        let body = json_body(response).await;
117
118        assert_eq!(body["active"], 0);
119        assert_eq!(body["tenants"], serde_json::Value::Array(Vec::new()));
120        assert!(body["last_reload"].is_string());
121        assert!(body["last_error"].is_null());
122    }
123
124    #[tokio::test]
125    async fn reload_without_handle_reports_not_implemented() {
126        let response = reload(AdminGuard, State(state())).await.into_response();
127
128        assert_eq!(response.status(), StatusCode::NOT_IMPLEMENTED);
129        let body = json_body(response).await;
130        assert_eq!(body["error"], "reload handle unavailable");
131    }
132}