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}