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
13//! use bindy::http_errors::map_http_error_to_reason;
14//!
15//! // Map HTTP status codes to Kubernetes condition reasons
16//! let (reason, message) = map_http_error_to_reason(404);
17//! assert_eq!(reason, "ZoneNotFound");
18//!
19//! let (reason, message) = map_http_error_to_reason(500);
20//! assert_eq!(reason, "BindcarInternalError");
21//! ```
22
23use crate::status_reasons::{
24    REASON_BINDCAR_AUTH_FAILED, REASON_BINDCAR_BAD_REQUEST, REASON_BINDCAR_INTERNAL_ERROR,
25    REASON_BINDCAR_NOT_IMPLEMENTED, REASON_BINDCAR_UNREACHABLE, REASON_GATEWAY_ERROR,
26    REASON_ZONE_NOT_FOUND,
27};
28
29/// Map HTTP status code to condition reason and message.
30///
31/// This function converts HTTP status codes from Bindcar API responses into
32/// standardized Kubernetes condition reasons and human-readable messages.
33///
34/// # Arguments
35///
36/// * `status_code` - HTTP status code (e.g., 400, 404, 500)
37///
38/// # Returns
39///
40/// A tuple of `(reason, message)`:
41/// - `reason` - Constant from `status_reasons` module
42/// - `message` - Human-readable explanation of the error
43///
44/// # HTTP Code Mapping
45///
46/// | HTTP Code | Reason | Meaning |
47/// |-----------|--------|---------|
48/// | 400 | `BindcarBadRequest` | Invalid request format |
49/// | 401 | `BindcarAuthFailed` | Authentication required |
50/// | 403 | `BindcarAuthFailed` | Insufficient permissions |
51/// | 404 | `ZoneNotFound` | Resource not found |
52/// | 500 | `BindcarInternalError` | Internal server error |
53/// | 501 | `BindcarNotImplemented` | Feature not implemented |
54/// | 502 | `GatewayError` | Bad gateway |
55/// | 503 | `GatewayError` | Service unavailable |
56/// | 504 | `GatewayError` | Gateway timeout |
57/// | Other | `BindcarUnreachable` | Unexpected error |
58///
59/// # Example
60///
61/// ```rust
62/// use bindy::http_errors::map_http_error_to_reason;
63///
64/// let (reason, message) = map_http_error_to_reason(404);
65/// assert_eq!(reason, "ZoneNotFound");
66/// assert!(message.contains("404"));
67///
68/// let (reason, message) = map_http_error_to_reason(503);
69/// assert_eq!(reason, "GatewayError");
70/// assert!(message.contains("503"));
71/// ```
72#[must_use]
73pub fn map_http_error_to_reason(status_code: u16) -> (&'static str, String) {
74    match status_code {
75        400 => (
76            REASON_BINDCAR_BAD_REQUEST,
77            "Invalid request to Bindcar API (400)".into(),
78        ),
79        401 => (
80            REASON_BINDCAR_AUTH_FAILED,
81            "Bindcar authentication required (401)".into(),
82        ),
83        403 => (
84            REASON_BINDCAR_AUTH_FAILED,
85            "Bindcar authorization failed (403)".into(),
86        ),
87        404 => (
88            REASON_ZONE_NOT_FOUND,
89            "Zone or resource not found in BIND9 (404)".into(),
90        ),
91        500 => (
92            REASON_BINDCAR_INTERNAL_ERROR,
93            "Bindcar API internal error (500)".into(),
94        ),
95        501 => (
96            REASON_BINDCAR_NOT_IMPLEMENTED,
97            "Operation not supported by Bindcar (501)".into(),
98        ),
99        502 => (
100            REASON_GATEWAY_ERROR,
101            "Bad gateway reaching Bindcar (502)".into(),
102        ),
103        503 => (
104            REASON_GATEWAY_ERROR,
105            "Bindcar service unavailable (503)".into(),
106        ),
107        504 => (
108            REASON_GATEWAY_ERROR,
109            "Gateway timeout reaching Bindcar (504)".into(),
110        ),
111        _ => (
112            REASON_BINDCAR_UNREACHABLE,
113            format!("Unexpected HTTP error from Bindcar ({status_code})"),
114        ),
115    }
116}
117
118/// Map connection error to condition reason and message.
119///
120/// Use this when the HTTP client cannot establish a connection to Bindcar,
121/// before receiving any HTTP status code.
122///
123/// # Returns
124///
125/// A tuple of `(reason, message)`:
126/// - `reason` - `REASON_BINDCAR_UNREACHABLE`
127/// - `message` - Human-readable explanation
128///
129/// # Common Causes
130///
131/// - Bindcar container not running
132/// - Network policy blocking traffic
133/// - Bindcar listening on wrong port
134/// - DNS resolution failure
135///
136/// # Example
137///
138/// ```rust,no_run
139/// use bindy::http_errors::map_connection_error;
140///
141/// # async fn example() {
142/// # let client = reqwest::Client::new();
143/// match client.get("http://localhost:8080/zones").send().await {
144///     Ok(response) => { /* handle response */ }
145///     Err(e) => {
146///         let (reason, message) = map_connection_error();
147///         // Set condition: Pod-0 status=False reason=BindcarUnreachable
148///     }
149/// }
150/// # }
151/// ```
152#[cfg(test)]
153mod tests {
154    use super::*;
155
156    #[test]
157    fn test_map_http_400() {
158        let (reason, message) = map_http_error_to_reason(400);
159        assert_eq!(reason, REASON_BINDCAR_BAD_REQUEST);
160        assert!(message.contains("400"));
161        assert!(message.contains("Invalid"));
162    }
163
164    #[test]
165    fn test_map_http_401() {
166        let (reason, message) = map_http_error_to_reason(401);
167        assert_eq!(reason, REASON_BINDCAR_AUTH_FAILED);
168        assert!(message.contains("401"));
169    }
170
171    #[test]
172    fn test_map_http_403() {
173        let (reason, message) = map_http_error_to_reason(403);
174        assert_eq!(reason, REASON_BINDCAR_AUTH_FAILED);
175        assert!(message.contains("403"));
176    }
177
178    #[test]
179    fn test_map_http_404() {
180        let (reason, message) = map_http_error_to_reason(404);
181        assert_eq!(reason, REASON_ZONE_NOT_FOUND);
182        assert!(message.contains("404"));
183        assert!(message.contains("not found"));
184    }
185
186    #[test]
187    fn test_map_http_500() {
188        let (reason, message) = map_http_error_to_reason(500);
189        assert_eq!(reason, REASON_BINDCAR_INTERNAL_ERROR);
190        assert!(message.contains("500"));
191    }
192
193    #[test]
194    fn test_map_http_501() {
195        let (reason, message) = map_http_error_to_reason(501);
196        assert_eq!(reason, REASON_BINDCAR_NOT_IMPLEMENTED);
197        assert!(message.contains("501"));
198    }
199
200    #[test]
201    fn test_map_http_502() {
202        let (reason, message) = map_http_error_to_reason(502);
203        assert_eq!(reason, REASON_GATEWAY_ERROR);
204        assert!(message.contains("502"));
205    }
206
207    #[test]
208    fn test_map_http_503() {
209        let (reason, message) = map_http_error_to_reason(503);
210        assert_eq!(reason, REASON_GATEWAY_ERROR);
211        assert!(message.contains("503"));
212    }
213
214    #[test]
215    fn test_map_http_504() {
216        let (reason, message) = map_http_error_to_reason(504);
217        assert_eq!(reason, REASON_GATEWAY_ERROR);
218        assert!(message.contains("504"));
219    }
220
221    #[test]
222    fn test_map_http_unknown() {
223        let (reason, message) = map_http_error_to_reason(418); // I'm a teapot
224        assert_eq!(reason, REASON_BINDCAR_UNREACHABLE);
225        assert!(message.contains("418"));
226    }
227}