bindy/
http_errors.rs

1// Copyright (c) 2025 Erick Bourgeois, firestoned
2// SPDX-License-Identifier: MIT
3
4//! HTTP error code mapping to Kubernetes status condition reasons.
5//!
6//! This module provides utilities for mapping HTTP status codes from the Bindcar API
7//! to standardized Kubernetes condition reasons. This enables consistent error handling
8//! and troubleshooting across the operator.
9//!
10//! # Usage
11//!
12//! ```rust,no_run
13//! use bindy::http_errors::{map_http_error_to_reason, map_connection_error};
14//!
15//! # async fn example() {
16//! # let client = reqwest::Client::new();
17//! // When making HTTP calls to Bindcar API
18//! match client.get("http://localhost:8080/zones").send().await {
19//!     Ok(response) => {
20//!         if response.status().is_success() {
21//!             // Handle success
22//!         } else {
23//!             let (reason, message) = map_http_error_to_reason(response.status().as_u16());
24//!             // Set condition with this reason
25//!         }
26//!     }
27//!     Err(e) => {
28//!         let (reason, message) = map_connection_error();
29//!         // Set condition for connection failure
30//!     }
31//! }
32//! # }
33//! ```
34
35use crate::status_reasons::{
36    REASON_BINDCAR_AUTH_FAILED, REASON_BINDCAR_BAD_REQUEST, REASON_BINDCAR_INTERNAL_ERROR,
37    REASON_BINDCAR_NOT_IMPLEMENTED, REASON_BINDCAR_UNREACHABLE, REASON_GATEWAY_ERROR, REASON_READY,
38    REASON_ZONE_NOT_FOUND,
39};
40
41/// Map HTTP status code to condition reason and message.
42///
43/// This function converts HTTP status codes from Bindcar API responses into
44/// standardized Kubernetes condition reasons and human-readable messages.
45///
46/// # Arguments
47///
48/// * `status_code` - HTTP status code (e.g., 400, 404, 500)
49///
50/// # Returns
51///
52/// A tuple of `(reason, message)`:
53/// - `reason` - Constant from `status_reasons` module
54/// - `message` - Human-readable explanation of the error
55///
56/// # HTTP Code Mapping
57///
58/// | HTTP Code | Reason | Meaning |
59/// |-----------|--------|---------|
60/// | 400 | `BindcarBadRequest` | Invalid request format |
61/// | 401 | `BindcarAuthFailed` | Authentication required |
62/// | 403 | `BindcarAuthFailed` | Insufficient permissions |
63/// | 404 | `ZoneNotFound` | Resource not found |
64/// | 500 | `BindcarInternalError` | Internal server error |
65/// | 501 | `BindcarNotImplemented` | Feature not implemented |
66/// | 502 | `GatewayError` | Bad gateway |
67/// | 503 | `GatewayError` | Service unavailable |
68/// | 504 | `GatewayError` | Gateway timeout |
69/// | Other | `BindcarUnreachable` | Unexpected error |
70///
71/// # Example
72///
73/// ```rust
74/// use bindy::http_errors::map_http_error_to_reason;
75///
76/// let (reason, message) = map_http_error_to_reason(404);
77/// assert_eq!(reason, "ZoneNotFound");
78/// assert!(message.contains("404"));
79///
80/// let (reason, message) = map_http_error_to_reason(503);
81/// assert_eq!(reason, "GatewayError");
82/// assert!(message.contains("503"));
83/// ```
84#[must_use]
85pub fn map_http_error_to_reason(status_code: u16) -> (&'static str, String) {
86    match status_code {
87        400 => (
88            REASON_BINDCAR_BAD_REQUEST,
89            "Invalid request to Bindcar API (400)".into(),
90        ),
91        401 => (
92            REASON_BINDCAR_AUTH_FAILED,
93            "Bindcar authentication required (401)".into(),
94        ),
95        403 => (
96            REASON_BINDCAR_AUTH_FAILED,
97            "Bindcar authorization failed (403)".into(),
98        ),
99        404 => (
100            REASON_ZONE_NOT_FOUND,
101            "Zone or resource not found in BIND9 (404)".into(),
102        ),
103        500 => (
104            REASON_BINDCAR_INTERNAL_ERROR,
105            "Bindcar API internal error (500)".into(),
106        ),
107        501 => (
108            REASON_BINDCAR_NOT_IMPLEMENTED,
109            "Operation not supported by Bindcar (501)".into(),
110        ),
111        502 => (
112            REASON_GATEWAY_ERROR,
113            "Bad gateway reaching Bindcar (502)".into(),
114        ),
115        503 => (
116            REASON_GATEWAY_ERROR,
117            "Bindcar service unavailable (503)".into(),
118        ),
119        504 => (
120            REASON_GATEWAY_ERROR,
121            "Gateway timeout reaching Bindcar (504)".into(),
122        ),
123        _ => (
124            REASON_BINDCAR_UNREACHABLE,
125            format!("Unexpected HTTP error from Bindcar ({status_code})"),
126        ),
127    }
128}
129
130/// Map connection error to condition reason and message.
131///
132/// Use this when the HTTP client cannot establish a connection to Bindcar,
133/// before receiving any HTTP status code.
134///
135/// # Returns
136///
137/// A tuple of `(reason, message)`:
138/// - `reason` - `REASON_BINDCAR_UNREACHABLE`
139/// - `message` - Human-readable explanation
140///
141/// # Common Causes
142///
143/// - Bindcar container not running
144/// - Network policy blocking traffic
145/// - Bindcar listening on wrong port
146/// - DNS resolution failure
147///
148/// # Example
149///
150/// ```rust,no_run
151/// use bindy::http_errors::map_connection_error;
152///
153/// # async fn example() {
154/// # let client = reqwest::Client::new();
155/// match client.get("http://localhost:8080/zones").send().await {
156///     Ok(response) => { /* handle response */ }
157///     Err(e) => {
158///         let (reason, message) = map_connection_error();
159///         // Set condition: Pod-0 status=False reason=BindcarUnreachable
160///     }
161/// }
162/// # }
163/// ```
164#[must_use]
165pub fn map_connection_error() -> (&'static str, String) {
166    (
167        REASON_BINDCAR_UNREACHABLE,
168        "Cannot connect to Bindcar API".into(),
169    )
170}
171
172/// Check if HTTP status code indicates success (2xx).
173///
174/// # Arguments
175///
176/// * `status_code` - HTTP status code
177///
178/// # Returns
179///
180/// `true` if status code is in the 2xx range, `false` otherwise
181///
182/// # Example
183///
184/// ```rust
185/// use bindy::http_errors::is_success_status;
186///
187/// assert!(is_success_status(200));
188/// assert!(is_success_status(201));
189/// assert!(is_success_status(204));
190/// assert!(!is_success_status(404));
191/// assert!(!is_success_status(500));
192/// ```
193#[must_use]
194pub const fn is_success_status(status_code: u16) -> bool {
195    status_code >= 200 && status_code < 300
196}
197
198/// Get condition reason for successful operations.
199///
200/// Use this when an operation completes successfully and you need to set
201/// a condition to reflect success.
202///
203/// # Returns
204///
205/// A tuple of `(reason, message)` for successful operations
206///
207/// # Example
208///
209/// ```rust
210/// use bindy::http_errors::success_reason;
211///
212/// let (reason, message) = success_reason();
213/// // Set condition: Pod-0 status=True reason=Ready message="Pod is ready"
214/// ```
215#[must_use]
216pub fn success_reason() -> (&'static str, &'static str) {
217    (REASON_READY, "Operation completed successfully")
218}
219
220#[cfg(test)]
221mod tests {
222    use super::*;
223
224    #[test]
225    fn test_map_http_400() {
226        let (reason, message) = map_http_error_to_reason(400);
227        assert_eq!(reason, REASON_BINDCAR_BAD_REQUEST);
228        assert!(message.contains("400"));
229        assert!(message.contains("Invalid"));
230    }
231
232    #[test]
233    fn test_map_http_401() {
234        let (reason, message) = map_http_error_to_reason(401);
235        assert_eq!(reason, REASON_BINDCAR_AUTH_FAILED);
236        assert!(message.contains("401"));
237    }
238
239    #[test]
240    fn test_map_http_403() {
241        let (reason, message) = map_http_error_to_reason(403);
242        assert_eq!(reason, REASON_BINDCAR_AUTH_FAILED);
243        assert!(message.contains("403"));
244    }
245
246    #[test]
247    fn test_map_http_404() {
248        let (reason, message) = map_http_error_to_reason(404);
249        assert_eq!(reason, REASON_ZONE_NOT_FOUND);
250        assert!(message.contains("404"));
251        assert!(message.contains("not found"));
252    }
253
254    #[test]
255    fn test_map_http_500() {
256        let (reason, message) = map_http_error_to_reason(500);
257        assert_eq!(reason, REASON_BINDCAR_INTERNAL_ERROR);
258        assert!(message.contains("500"));
259    }
260
261    #[test]
262    fn test_map_http_501() {
263        let (reason, message) = map_http_error_to_reason(501);
264        assert_eq!(reason, REASON_BINDCAR_NOT_IMPLEMENTED);
265        assert!(message.contains("501"));
266    }
267
268    #[test]
269    fn test_map_http_502() {
270        let (reason, message) = map_http_error_to_reason(502);
271        assert_eq!(reason, REASON_GATEWAY_ERROR);
272        assert!(message.contains("502"));
273    }
274
275    #[test]
276    fn test_map_http_503() {
277        let (reason, message) = map_http_error_to_reason(503);
278        assert_eq!(reason, REASON_GATEWAY_ERROR);
279        assert!(message.contains("503"));
280    }
281
282    #[test]
283    fn test_map_http_504() {
284        let (reason, message) = map_http_error_to_reason(504);
285        assert_eq!(reason, REASON_GATEWAY_ERROR);
286        assert!(message.contains("504"));
287    }
288
289    #[test]
290    fn test_map_http_unknown() {
291        let (reason, message) = map_http_error_to_reason(418); // I'm a teapot
292        assert_eq!(reason, REASON_BINDCAR_UNREACHABLE);
293        assert!(message.contains("418"));
294    }
295
296    #[test]
297    fn test_map_connection_error() {
298        let (reason, message) = map_connection_error();
299        assert_eq!(reason, REASON_BINDCAR_UNREACHABLE);
300        assert!(message.contains("connect"));
301    }
302
303    #[test]
304    fn test_is_success_status() {
305        // Success codes
306        assert!(is_success_status(200));
307        assert!(is_success_status(201));
308        assert!(is_success_status(204));
309        assert!(is_success_status(299));
310
311        // Non-success codes
312        assert!(!is_success_status(199));
313        assert!(!is_success_status(300));
314        assert!(!is_success_status(400));
315        assert!(!is_success_status(404));
316        assert!(!is_success_status(500));
317    }
318
319    #[test]
320    fn test_success_reason() {
321        let (reason, message) = success_reason();
322        assert_eq!(reason, REASON_READY);
323        assert!(message.contains("success"));
324    }
325}