greentic_runner_host/http/
admin.rs1use 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}