featherbit/admin/
status.rs1use std::sync::Arc;
5
6use axum::extract::State;
7use axum::http::StatusCode;
8use axum::response::IntoResponse;
9use axum::routing::{get, post};
10use axum::{Json, Router};
11
12use crate::state::SharedState;
13
14pub fn router() -> Router<Arc<SharedState>> {
17 Router::new()
18 .route("/healthz", get(healthz))
19 .route("/readyz", get(readyz))
20 .route("/api/status", get(status))
21 .route("/api/config/export", get(export_config))
22 .route("/api/config/reload", post(reload_config))
23 .route("/metrics", get(metrics))
24}
25
26async fn healthz() -> impl IntoResponse {
29 (
30 StatusCode::OK,
31 Json(serde_json::json!({"status": "healthy"})),
32 )
33}
34
35async fn readyz(State(state): State<Arc<SharedState>>) -> impl IntoResponse {
47 let routes = state.routes.read().await;
48 if routes.is_empty() {
49 return (
50 StatusCode::SERVICE_UNAVAILABLE,
51 Json(serde_json::json!({
52 "status": "not_ready",
53 "reason": "no routes loaded",
54 "acme": {"placeholder": Vec::<String>::new()},
55 })),
56 );
57 }
58 if state.acme_expected && state.acme.load().is_none() {
63 return (
64 StatusCode::SERVICE_UNAVAILABLE,
65 Json(serde_json::json!({
66 "status": "not_ready",
67 "reason": "acme starting",
68 "acme": {"placeholder": Vec::<String>::new()},
69 })),
70 );
71 }
72 let placeholders = state
73 .acme
74 .load()
75 .as_ref()
76 .map(|rt| rt.placeholder_ids())
77 .unwrap_or_default();
78 if !placeholders.is_empty() {
79 return (
80 StatusCode::SERVICE_UNAVAILABLE,
81 Json(serde_json::json!({
82 "status": "not_ready",
83 "reason": "acme placeholder certs",
84 "acme": {"placeholder": placeholders},
85 })),
86 );
87 }
88 (
89 StatusCode::OK,
90 Json(serde_json::json!({
91 "status": "ready",
92 "routes": routes.len(),
93 "acme": {"placeholder": placeholders},
94 })),
95 )
96}
97
98async fn status(State(state): State<Arc<SharedState>>) -> impl IntoResponse {
104 let routes = state.routes.read().await;
105 let gw = state.gateway.read().await;
106 Json(serde_json::json!({
107 "version": env!("CARGO_PKG_VERSION"),
108 "routes": routes.len(),
109 "policies": gw.policies.len(),
110 }))
111}
112
113async fn export_config(State(state): State<Arc<SharedState>>) -> impl IntoResponse {
124 let gw = state.gateway.read().await;
125 match serde_yaml::to_string(&*gw) {
126 Ok(yaml) => (
127 StatusCode::OK,
128 [("content-type", "text/yaml; charset=utf-8")],
129 yaml,
130 )
131 .into_response(),
132 Err(e) => (
133 StatusCode::INTERNAL_SERVER_ERROR,
134 Json(serde_json::json!({"error": format!("failed to serialize config: {}", e)})),
135 )
136 .into_response(),
137 }
138}
139
140async fn metrics(State(state): State<Arc<SharedState>>) -> impl IntoResponse {
144 (
145 StatusCode::OK,
146 [("content-type", "text/plain; charset=utf-8")],
147 state.metrics.render(),
148 )
149}
150
151async fn reload_config(State(state): State<Arc<SharedState>>) -> impl IntoResponse {
158 match state.reload_from_disk().await {
159 Ok(_) => (
160 StatusCode::OK,
161 Json(serde_json::json!({"status": "reloaded"})),
162 )
163 .into_response(),
164 Err(e) => (
165 StatusCode::INTERNAL_SERVER_ERROR,
166 Json(serde_json::json!({"error": e})),
167 )
168 .into_response(),
169 }
170}
171
172#[cfg(test)]
173mod acme_readyz_tests {
174 use super::*;
175 use crate::config::{GatewayConfig, SystemConfig};
176 use crate::config_store::FileConfigStore;
177 use axum::body::Body;
178 use axum::http::Request;
179 use tower::ServiceExt;
180
181 fn state() -> Arc<SharedState> {
182 state_with("{}")
183 }
184
185 fn state_with(system_yaml: &str) -> Arc<SharedState> {
186 let system: SystemConfig = serde_yaml::from_str(system_yaml).unwrap();
187 let gateway: GatewayConfig = serde_yaml::from_str(
188 "routes:\n - name: r\n match:\n path: /x\n policy: p\npolicies:\n - name: p\n nodes:\n - id: in\n type: listener\n - id: out\n type: client\n edges:\n - { from: in.out, to: out.in }\n",
189 )
190 .unwrap();
191 Arc::new(
192 SharedState::new(
193 system,
194 gateway,
195 None,
196 Arc::new(FileConfigStore::new("g.yaml".into())),
197 )
198 .unwrap(),
199 )
200 }
201
202 async fn readyz_status(state: Arc<SharedState>) -> (StatusCode, serde_json::Value) {
203 let resp = router()
204 .with_state(state)
205 .oneshot(
206 Request::builder()
207 .uri("/readyz")
208 .body(Body::empty())
209 .unwrap(),
210 )
211 .await
212 .unwrap();
213 let status = resp.status();
214 let bytes = axum::body::to_bytes(resp.into_body(), usize::MAX)
215 .await
216 .unwrap();
217 (status, serde_json::from_slice(&bytes).unwrap())
218 }
219
220 #[tokio::test]
221 async fn readyz_is_503_while_a_managed_cert_is_a_placeholder() {
222 let s = state();
223 let (status, _) = readyz_status(s.clone()).await;
224 assert_eq!(status, StatusCode::OK, "no acme ⇒ ready");
225
226 s.acme
227 .store(Some(crate::acme::testing::placeholder_runtime(&[
228 "p.example.com",
229 ])));
230 let (status, body) = readyz_status(s.clone()).await;
231 assert_eq!(status, StatusCode::SERVICE_UNAVAILABLE);
232 assert_eq!(body["acme"]["placeholder"][0], "p.example.com");
233
234 s.acme.store(Some(crate::acme::testing::issued_runtime(&[
235 "p.example.com",
236 ])));
237 let (status, body) = readyz_status(s).await;
238 assert_eq!(status, StatusCode::OK);
239 assert_eq!(body["acme"]["placeholder"].as_array().unwrap().len(), 0);
240 }
241
242 #[tokio::test]
247 async fn readyz_is_503_before_the_acme_runtime_is_seeded() {
248 let s = state_with(
249 "acme:
250 terms_of_service_agreed: true
251 directory_url: https://127.0.0.1:1/directory
252tls:
253 acme:
254 domains: [p.example.com]
255",
256 );
257 assert!(s.acme_expected);
258 let (status, body) = readyz_status(s.clone()).await;
259 assert_eq!(status, StatusCode::SERVICE_UNAVAILABLE);
260 assert_eq!(body["reason"], "acme starting");
261 assert_eq!(body["acme"]["placeholder"].as_array().unwrap().len(), 0);
262
263 s.acme.store(Some(crate::acme::testing::issued_runtime(&[
264 "p.example.com",
265 ])));
266 let (status, _) = readyz_status(s).await;
267 assert_eq!(status, StatusCode::OK);
268
269 let file_tls = state_with(
271 "tls:
272 cert_path: /tmp/c.pem
273 key_path: /tmp/k.pem
274",
275 );
276 assert!(!file_tls.acme_expected);
277 assert_eq!(readyz_status(file_tls).await.0, StatusCode::OK);
278 }
279}