docker_image_pusher/error/
handlers.rs1use crate::error::{PusherError, Result};
4use reqwest::StatusCode;
5use std::time::Duration;
6
7pub struct HttpErrorHandler;
9
10impl HttpErrorHandler {
11 pub fn handle_upload_error(status: StatusCode, error_text: &str, context: &str) -> PusherError {
13 let error_msg = match status.as_u16() {
14 400 => {
15 if error_text.contains("exist blob require digest") {
16 format!(
17 "Digest validation failed - Registry reports blob exists but digest mismatch: {}",
18 error_text
19 )
20 } else if error_text.contains("BAD_REQUEST") {
21 format!(
22 "Bad request - Check digest format and data integrity: {}",
23 error_text
24 )
25 } else {
26 format!("Bad request during {}: {}", context, error_text)
27 }
28 }
29 401 => format!("Authentication failed during {}: {}", context, error_text),
30 403 => format!("Permission denied for {}: {}", context, error_text),
31 404 => format!(
32 "Repository not found or {} session expired: {}",
33 context, error_text
34 ),
35 409 => format!(
36 "Conflict - Blob already exists with different digest: {}",
37 error_text
38 ),
39 413 => format!("File too large for {}: {}", context, error_text),
40 422 => format!("Invalid digest or data for {}: {}", context, error_text),
41 500 => format!("Registry server error during {}: {}", context, error_text),
42 502 | 503 => format!("Registry unavailable during {}: {}", context, error_text),
43 507 => format!("Registry out of storage during {}: {}", context, error_text),
44 508 => format!("Streaming {} timeout: {}", context, error_text),
45 _ => format!("{} failed (status {}): {}", context, status, error_text),
46 };
47
48 PusherError::Upload(error_msg)
49 }
50
51 pub fn handle_auth_error(status: StatusCode, error_text: &str) -> PusherError {
53 let error_msg = match status.as_u16() {
54 400 => "Invalid token request parameters".to_string(),
55 401 => "Invalid credentials provided".to_string(),
56 403 => "Access denied - insufficient permissions".to_string(),
57 404 => "Authentication endpoint not found".to_string(),
58 _ => format!("Authentication failed (status {}): {}", status, error_text),
59 };
60
61 PusherError::Authentication(error_msg)
62 }
63
64 pub fn handle_registry_error(
66 status: StatusCode,
67 error_text: &str,
68 operation: &str,
69 ) -> PusherError {
70 let error_msg = match status.as_u16() {
71 401 => format!(
72 "Unauthorized to perform {} operation: {}",
73 operation, error_text
74 ),
75 403 => format!(
76 "Forbidden: insufficient permissions for {}: {}",
77 operation, error_text
78 ),
79 404 => format!("Resource not found for {}: {}", operation, error_text),
80 429 => format!("Rate limited during {}: {}", operation, error_text),
81 500 => format!("Registry server error during {}: {}", operation, error_text),
82 502 | 503 => format!("Registry unavailable for {}: {}", operation, error_text),
83 _ => format!("{} failed (status {}): {}", operation, status, error_text),
84 };
85
86 PusherError::Registry(error_msg)
87 }
88
89 pub fn handle_streaming_error(status: StatusCode, error_text: &str) -> PusherError {
91 let error_msg = match status.as_u16() {
92 400 => {
93 if error_text.contains("DIGEST_INVALID") {
94 "Digest validation failed - Registry reports uploaded content doesn't match expected digest".to_string()
95 } else {
96 format!("Bad request during streaming upload: {}", error_text)
97 }
98 }
99 413 => "File too large for registry".to_string(),
100 507 => "Insufficient storage space on registry".to_string(),
101 401 => "Authentication failed during upload".to_string(),
102 403 => "Permission denied for upload".to_string(),
103 408 | 504 => "Streaming upload timeout".to_string(),
104 500 => {
105 if error_text.contains("s3aws") || error_text.contains("DriverName") {
106 format!(
107 "Registry storage backend error (S3): {}. This is typically a temporary issue with the registry's storage system",
108 error_text
109 )
110 } else {
111 format!("Registry internal server error: {}", error_text)
112 }
113 }
114 502 | 503 => format!("Registry temporarily unavailable: {}", error_text),
115 _ => format!(
116 "Streaming upload failed (status {}): {}",
117 status, error_text
118 ),
119 };
120
121 PusherError::Upload(error_msg)
122 }
123
124 pub fn is_storage_backend_error(error_text: &str) -> bool {
126 error_text.contains("s3aws")
127 || error_text.contains("DriverName")
128 || error_text.contains("storage backend")
129 || error_text.contains("500 Internal Server Error")
130 }
131
132 pub fn get_storage_error_retry_delay(attempt: u32, base_delay_secs: u64) -> Duration {
134 let backoff_multiplier = 2_u64.pow(attempt.min(4));
135 Duration::from_secs(base_delay_secs * backoff_multiplier)
136 }
137}
138
139pub struct NetworkErrorHandler;
141
142impl NetworkErrorHandler {
143 pub fn handle_network_error(error: &reqwest::Error, context: &str) -> PusherError {
145 if error.is_timeout() {
146 PusherError::Timeout(format!("{} timeout: {}", context, error))
147 } else if error.is_connect() {
148 PusherError::Network(format!("Connection error during {}: {}", context, error))
149 } else if error.to_string().contains("dns") {
150 PusherError::Network(format!("DNS resolution error for {}: {}", context, error))
151 } else if error.to_string().contains("certificate") {
152 PusherError::Network(format!(
153 "TLS certificate error during {}: {}",
154 context, error
155 ))
156 } else {
157 PusherError::Network(format!("{} network error: {}", context, error))
158 }
159 }
160}
161
162pub struct ValidationErrorHandler;
164
165impl ValidationErrorHandler {
166 pub fn validate_file_path(file_path: &str) -> Result<()> {
168 use std::path::Path;
169
170 let path = Path::new(file_path);
171
172 if !path.exists() {
173 return Err(PusherError::Validation(format!(
174 "Input file does not exist: {}",
175 file_path
176 )));
177 }
178
179 if !path.is_file() {
180 return Err(PusherError::Validation(format!(
181 "Input path is not a file: {}",
182 file_path
183 )));
184 }
185
186 let extension = path.extension().and_then(|ext| ext.to_str()).unwrap_or("");
188
189 if !matches!(extension.to_lowercase().as_str(), "tar" | "tar.gz" | "tgz") {
190 return Err(PusherError::Validation(format!(
191 "Input file must be a tar archive (.tar, .tar.gz, or .tgz): {}",
192 file_path
193 )));
194 }
195
196 Ok(())
197 }
198
199 pub fn validate_repository_url(url: &str) -> Result<()> {
201 if url.is_empty() {
202 return Err(PusherError::Validation(
203 "Repository URL cannot be empty".to_string(),
204 ));
205 }
206
207 if !url.contains("://") {
208 return Err(PusherError::Validation(
209 "Repository URL must include protocol (http:// or https://)".to_string(),
210 ));
211 }
212
213 Ok(())
214 }
215
216 pub fn validate_credentials(
218 username: &Option<String>,
219 password: &Option<String>,
220 ) -> Result<()> {
221 match (username, password) {
222 (Some(_), None) => Err(PusherError::Validation(
223 "Password is required when username is provided".to_string(),
224 )),
225 (None, Some(_)) => Err(PusherError::Validation(
226 "Username is required when password is provided".to_string(),
227 )),
228 _ => Ok(()), }
230 }
231
232 pub fn validate_timeout(timeout: u64) -> Result<()> {
234 if timeout == 0 {
235 return Err(PusherError::Validation(
236 "Timeout must be greater than 0".to_string(),
237 ));
238 }
239
240 if timeout > 86400 {
241 return Err(PusherError::Validation(
243 "Timeout cannot exceed 24 hours (86400 seconds)".to_string(),
244 ));
245 }
246
247 Ok(())
248 }
249}
250
251#[macro_export]
253macro_rules! with_context {
254 ($result:expr, $context:expr) => {
255 $result.map_err(|e| match e {
256 PusherError::Config(msg) => PusherError::Config(format!("{}: {}", $context, msg)),
257 PusherError::Authentication(msg) => {
258 PusherError::Authentication(format!("{}: {}", $context, msg))
259 }
260 PusherError::Network(msg) => PusherError::Network(format!("{}: {}", $context, msg)),
261 PusherError::Upload(msg) => PusherError::Upload(format!("{}: {}", $context, msg)),
262 PusherError::Io(msg) => PusherError::Io(format!("{}: {}", $context, msg)),
263 PusherError::Parse(msg) => PusherError::Parse(format!("{}: {}", $context, msg)),
264 PusherError::Registry(msg) => PusherError::Registry(format!("{}: {}", $context, msg)),
265 PusherError::ImageParsing(msg) => {
266 PusherError::ImageParsing(format!("{}: {}", $context, msg))
267 }
268 PusherError::Validation(msg) => {
269 PusherError::Validation(format!("{}: {}", $context, msg))
270 }
271 PusherError::Timeout(msg) => PusherError::Timeout(format!("{}: {}", $context, msg)),
272 })
273 };
274}