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}