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}