hoprd_api/checks.rs
1use std::sync::Arc;
2
3use axum::{extract::State, http::status::StatusCode, response::IntoResponse};
4use hopr_lib::{Health, state::HoprState};
5
6use crate::AppState;
7
8/// Check whether the node is started.
9///
10/// # Behavior
11///
12/// Returns 200 OK when the node is in `HoprState::Running`.
13/// Returns 412 PRECONDITION_FAILED when the node is in any other state
14/// (Uninitialized, Initializing, Indexing, Starting).
15///
16/// This endpoint checks only the running state, not network connectivity.
17#[utoipa::path(
18 get,
19 path = "/startedz",
20 description="Check whether the node is started",
21 responses(
22 (status = 200, description = "The node is started and running"),
23 (status = 412, description = "The node is not started and running"),
24 ),
25 tag = "Checks"
26 )]
27pub(super) async fn startedz(State(state): State<Arc<AppState>>) -> impl IntoResponse {
28 eval_precondition(is_running(state)) // FIXME: improve this once node state granularity is improved
29}
30
31/// Check whether the node is **ready** to accept connections.
32///
33/// Ready means that the node is running and has at least minimal connectivity.
34///
35/// # Behavior
36///
37/// Both conditions must be true for 200 OK:
38/// 1. Node must be in Running state (`HoprState::Running`)
39/// 2. Network must be minimally connected (`Health::Orange`, `Health::Yellow`, or `Health::Green`)
40///
41/// Returns 412 PRECONDITION_FAILED if either condition is false:
42/// - Node not running (any other `HoprState`)
43/// - Node running but network not minimally connected (`Health::Unknown` or `Health::Red`)
44///
45/// This endpoint is used by Kubernetes readiness probes to determine if the pod should receive traffic.
46#[utoipa::path(
47 get,
48 path = "/readyz",
49 description="Check whether the node is ready to accept connections",
50 responses(
51 (status = 200, description = "The node is ready to accept connections"),
52 (status = 412, description = "The node is not ready to accept connections"),
53 ),
54 tag = "Checks"
55 )]
56pub(super) async fn readyz(State(state): State<Arc<AppState>>) -> impl IntoResponse {
57 eval_precondition(is_running(state.clone()) && is_minimally_connected(state).await)
58}
59
60/// Check whether the node is **healthy**.
61///
62/// Healthy means that the node is running and has at least minimal connectivity.
63///
64/// # Behavior
65///
66/// Both conditions must be true for 200 OK:
67/// 1. Node must be in Running state (`HoprState::Running`)
68/// 2. Network must be minimally connected (`Health::Orange`, `Health::Yellow`, or `Health::Green`)
69///
70/// Returns 412 PRECONDITION_FAILED if either condition is false:
71/// - Node not running (any other `HoprState`)
72/// - Node running but network not minimally connected (`Health::Unknown` or `Health::Red`)
73///
74/// This endpoint is used by Kubernetes liveness probes to determine if the pod should be restarted.
75///
76/// Note: Currently `healthyz` and `readyz` have identical behavior.
77#[utoipa::path(
78 get,
79 path = "/healthyz",
80 description="Check whether the node is healthy",
81 responses(
82 (status = 200, description = "The node is healthy"),
83 (status = 412, description = "The node is not healthy"),
84 ),
85 tag = "Checks"
86 )]
87pub(super) async fn healthyz(State(state): State<Arc<AppState>>) -> impl IntoResponse {
88 eval_precondition(is_running(state.clone()) && is_minimally_connected(state).await)
89}
90
91/// Check if the node has minimal network connectivity.
92///
93/// Returns `true` if the network health is `Orange`, `Yellow`, or `Green`.
94/// Returns `false` if the network health is `Unknown` or `Red`.
95#[inline]
96async fn is_minimally_connected(state: Arc<AppState>) -> bool {
97 matches!(
98 state.hopr.network_health().await,
99 Health::Orange | Health::Yellow | Health::Green
100 )
101}
102
103/// Check if the node is in the Running state.
104///
105/// Returns `true` only when `HoprState::Running`.
106/// Returns `false` for all other states (Uninitialized, Initializing, Indexing, Starting).
107#[inline]
108fn is_running(state: Arc<AppState>) -> bool {
109 matches!(state.hopr.status(), HoprState::Running)
110}
111
112/// Evaluate a precondition and return the appropriate HTTP response.
113///
114/// Returns 200 OK if `precondition` is `true`.
115/// Returns 412 PRECONDITION_FAILED if `precondition` is `false`.
116#[inline]
117fn eval_precondition(precondition: bool) -> impl IntoResponse {
118 if precondition {
119 (StatusCode::OK, "").into_response()
120 } else {
121 (StatusCode::PRECONDITION_FAILED, "").into_response()
122 }
123}
124
125/// Check whether the node is eligible in the network.
126#[utoipa::path(
127 get,
128 path = "/eligiblez",
129 description="Check whether the node is eligible in the network",
130 responses(
131 (status = 200, description = "The node is allowed in the network"),
132 (status = 412, description = "The node is not allowed in the network"),
133 (status = 500, description = "Internal server error"),
134 ),
135 tag = "Checks"
136 )]
137pub(super) async fn eligiblez(State(_state): State<Arc<AppState>>) -> impl IntoResponse {
138 (StatusCode::OK, "").into_response()
139}
140
141#[cfg(test)]
142mod tests {
143 use super::*;
144
145 /// Test that eval_precondition returns 200 OK when the precondition is true
146 #[test]
147 fn test_eval_precondition_true_returns_ok() {
148 let response = eval_precondition(true);
149 let (parts, _) = response.into_response().into_parts();
150 assert_eq!(parts.status, StatusCode::OK);
151 }
152
153 /// Test that eval_precondition returns 412 PRECONDITION_FAILED when the precondition is false
154 #[test]
155 fn test_eval_precondition_false_returns_precondition_failed() {
156 let response = eval_precondition(false);
157 let (parts, _) = response.into_response().into_parts();
158 assert_eq!(parts.status, StatusCode::PRECONDITION_FAILED);
159 }
160}