Skip to main content

hoprd_api/
checks.rs

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