1use bollard::{
2 Docker,
3 container::LogOutput,
4 exec::{CreateExecOptions, StartExecOptions, StartExecResults},
5 models::{
6 ContainerCreateBody, ContainerCreateResponse, ContainerInspectResponse, ContainerSummary,
7 },
8 query_parameters::{
9 CreateContainerOptions, CreateImageOptionsBuilder, InspectContainerOptions,
10 ListContainersOptions, LogsOptions, RemoveContainerOptions, StartContainerOptions,
11 StopContainerOptions,
12 },
13};
14use futures_util::{Stream, StreamExt, TryStreamExt};
15
16use crate::models::ContainerHealthStatus;
17
18#[derive(Debug, Clone, PartialEq, thiserror::Error)]
19pub enum DockerError {
20 #[error("resource not modified")]
21 NotModified,
22 #[error("bad request")]
23 BadRequest,
24 #[error("unauthorized")]
25 Unauthorized,
26 #[error("forbidden")]
27 Forbidden,
28 #[error("not found")]
29 NotFound,
30 #[error("conflict")]
31 Conflict,
32 #[error("internal server error")]
33 ServerError,
34 #[error("docker error (status {status_code:?}): {message}")]
35 Other {
36 status_code: Option<u16>,
37 message: String,
38 },
39}
40
41impl From<bollard::errors::Error> for DockerError {
42 fn from(err: bollard::errors::Error) -> Self {
43 match err {
44 bollard::errors::Error::DockerResponseServerError {
45 status_code,
46 message,
47 } => match status_code {
48 304 => DockerError::NotModified,
49 400 => DockerError::BadRequest,
50 401 => DockerError::Unauthorized,
51 403 => DockerError::Forbidden,
52 404 => DockerError::NotFound,
53 409 => DockerError::Conflict,
54 500 => DockerError::ServerError,
55 _ => DockerError::Other {
56 status_code: Some(status_code),
57 message,
58 },
59 },
60 _ => DockerError::Other {
61 status_code: None,
62 message: err.to_string(),
63 },
64 }
65 }
66}
67
68impl From<bollard::models::HealthStatusEnum> for ContainerHealthStatus {
69 fn from(status: bollard::models::HealthStatusEnum) -> Self {
70 match status {
71 bollard::models::HealthStatusEnum::EMPTY => ContainerHealthStatus::Empty,
72 bollard::models::HealthStatusEnum::HEALTHY => ContainerHealthStatus::Healthy,
73 bollard::models::HealthStatusEnum::UNHEALTHY => ContainerHealthStatus::Unhealthy,
74 bollard::models::HealthStatusEnum::NONE => ContainerHealthStatus::None,
75 bollard::models::HealthStatusEnum::STARTING => ContainerHealthStatus::Starting,
76 }
77 }
78}
79
80pub trait DockerInspectContainer {
81 fn inspect_container(
82 &self,
83 container_id: &str,
84 options: Option<InspectContainerOptions>,
85 ) -> impl Future<Output = Result<ContainerInspectResponse, DockerError>> + Send;
86}
87
88impl DockerInspectContainer for Docker {
89 async fn inspect_container(
90 &self,
91 container_id: &str,
92 options: Option<InspectContainerOptions>,
93 ) -> Result<ContainerInspectResponse, DockerError> {
94 self.inspect_container(container_id, options)
95 .await
96 .map_err(DockerError::from)
97 }
98}
99
100pub trait DockerListContainers {
101 fn list_containers(
102 &self,
103 options: Option<ListContainersOptions>,
104 ) -> impl Future<Output = Result<Vec<ContainerSummary>, DockerError>> + Send;
105}
106
107impl DockerListContainers for Docker {
108 async fn list_containers(
109 &self,
110 options: Option<ListContainersOptions>,
111 ) -> Result<Vec<ContainerSummary>, DockerError> {
112 self.list_containers(options)
113 .await
114 .map_err(DockerError::from)
115 }
116}
117
118pub trait DockerPullImage {
119 fn pull_image(
120 &self,
121 image: &str,
122 tag: &str,
123 ) -> impl Future<Output = Result<(), DockerError>> + Send;
124}
125
126impl DockerPullImage for Docker {
127 async fn pull_image(&self, image: &str, tag: &str) -> Result<(), DockerError> {
128 let create_image_options = CreateImageOptionsBuilder::default()
129 .from_image(image)
130 .tag(tag)
131 .build();
132
133 let mut stream = self.create_image(Some(create_image_options), None, None);
134
135 while let Some(result) = stream.next().await {
136 result.map_err(DockerError::from)?;
137 }
138
139 Ok(())
140 }
141}
142
143pub trait DockerStopContainer {
144 fn stop_container(
145 &self,
146 container_id: &str,
147 options: Option<StopContainerOptions>,
148 ) -> impl Future<Output = Result<(), DockerError>> + Send;
149}
150
151impl DockerStopContainer for Docker {
152 async fn stop_container(
153 &self,
154 container_id: &str,
155 options: Option<StopContainerOptions>,
156 ) -> Result<(), DockerError> {
157 self.stop_container(container_id, options)
158 .await
159 .map_err(DockerError::from)
160 }
161}
162
163pub trait DockerRemoveContainer {
164 fn remove_container(
165 &self,
166 container_id: &str,
167 options: Option<RemoveContainerOptions>,
168 ) -> impl Future<Output = Result<(), DockerError>> + Send;
169}
170
171impl DockerRemoveContainer for Docker {
172 async fn remove_container(
173 &self,
174 container_id: &str,
175 options: Option<RemoveContainerOptions>,
176 ) -> Result<(), DockerError> {
177 self.remove_container(container_id, options)
178 .await
179 .map_err(DockerError::from)
180 }
181}
182
183pub trait DockerCreateContainer {
184 fn create_container(
185 &self,
186 options: Option<CreateContainerOptions>,
187 config: ContainerCreateBody,
188 ) -> impl Future<Output = Result<ContainerCreateResponse, DockerError>> + Send;
189}
190
191impl DockerCreateContainer for Docker {
192 async fn create_container(
193 &self,
194 options: Option<CreateContainerOptions>,
195 config: ContainerCreateBody,
196 ) -> Result<ContainerCreateResponse, DockerError> {
197 self.create_container(options, config)
198 .await
199 .map_err(DockerError::from)
200 }
201}
202
203pub trait DockerStartContainer {
204 fn start_container(
205 &self,
206 container_id: &str,
207 options: Option<StartContainerOptions>,
208 ) -> impl Future<Output = Result<(), DockerError>> + Send;
209}
210
211impl DockerStartContainer for Docker {
212 async fn start_container(
213 &self,
214 container_id: &str,
215 options: Option<StartContainerOptions>,
216 ) -> Result<(), DockerError> {
217 self.start_container(container_id, options)
218 .await
219 .map_err(DockerError::from)
220 }
221}
222
223pub trait DockerPauseContainer {
224 fn pause_container(
225 &self,
226 container_id: &str,
227 ) -> impl Future<Output = Result<(), DockerError>> + Send;
228}
229
230impl DockerPauseContainer for Docker {
231 async fn pause_container(&self, container_id: &str) -> Result<(), DockerError> {
232 self.pause_container(container_id)
233 .await
234 .map_err(DockerError::from)
235 }
236}
237
238pub trait DockerUnpauseContainer {
239 fn unpause_container(
240 &self,
241 container_id: &str,
242 ) -> impl Future<Output = Result<(), DockerError>> + Send;
243}
244
245impl DockerUnpauseContainer for Docker {
246 async fn unpause_container(&self, container_id: &str) -> Result<(), DockerError> {
247 self.unpause_container(container_id)
248 .await
249 .map_err(DockerError::from)
250 }
251}
252
253pub trait RunCommandInContainer {
254 fn run_command_in_container(
255 &self,
256 container_id: &str,
257 command: Vec<String>,
258 ) -> impl Future<Output = Result<CommandOutput, RunCommandInContainerError>> + Send;
259}
260
261pub struct CommandOutput {
262 pub stdout: Vec<String>,
263 pub stderr: Vec<String>,
264}
265
266#[derive(Debug, thiserror::Error)]
267pub enum RunCommandInContainerError {
268 #[error("Failed to create exec: {0}")]
269 CreateExec(DockerError),
270 #[error("Failed to start exec: {0}")]
271 StartExec(DockerError),
272 #[error("Failed to get output, output was not attached")]
273 GetOutput,
274 #[error("Failed to get output: {0}")]
275 GetOutputError(DockerError),
276}
277
278impl RunCommandInContainer for Docker {
279 async fn run_command_in_container(
280 &self,
281 container_id: &str,
282 command: Vec<String>,
283 ) -> Result<CommandOutput, RunCommandInContainerError> {
284 let exec = self
285 .create_exec(
286 container_id,
287 CreateExecOptions {
288 attach_stdout: Some(true),
289 attach_stderr: Some(true),
290 cmd: Some(command),
291 ..Default::default()
292 },
293 )
294 .await
295 .map_err(|e| RunCommandInContainerError::CreateExec(DockerError::from(e)))?;
296
297 let exec = self
298 .start_exec(
299 &exec.id,
300 Some(StartExecOptions {
301 detach: false,
302 tty: false,
303 output_capacity: None,
304 }),
305 )
306 .await
307 .map_err(|e| RunCommandInContainerError::StartExec(DockerError::from(e)))?;
308
309 let StartExecResults::Attached { mut output, .. } = exec else {
310 return Err(RunCommandInContainerError::GetOutput);
311 };
312
313 let mut stdout = String::new();
314 let mut stderr = String::new();
315
316 while let Some(result) = output.next().await {
317 let log_ouput = result
318 .map_err(|e| RunCommandInContainerError::GetOutputError(DockerError::from(e)))?;
319
320 match log_ouput {
321 LogOutput::StdOut { message } => {
322 stdout.push_str(&String::from_utf8_lossy(message.as_ref()));
323 }
324 LogOutput::StdErr { message } => {
325 stderr.push_str(&String::from_utf8_lossy(message.as_ref()));
326 }
327 _ => {}
328 }
329 }
330
331 Ok(CommandOutput {
332 stdout: stdout.lines().map(str::to_string).collect(),
333 stderr: stderr.lines().map(str::to_string).collect(),
334 })
335 }
336}
337
338#[cfg(test)]
339mod tests {
340 use super::*;
341
342 #[test]
343 fn test_docker_error_from_bollard_not_modified() {
344 let err = bollard::errors::Error::DockerResponseServerError {
345 status_code: 304,
346 message: "Not Modified".to_string(),
347 };
348 assert_eq!(DockerError::from(err), DockerError::NotModified);
349 }
350
351 #[test]
352 fn test_docker_error_from_bollard_bad_request() {
353 let err = bollard::errors::Error::DockerResponseServerError {
354 status_code: 400,
355 message: "Bad Request".to_string(),
356 };
357 assert_eq!(DockerError::from(err), DockerError::BadRequest);
358 }
359
360 #[test]
361 fn test_docker_error_from_bollard_unauthorized() {
362 let err = bollard::errors::Error::DockerResponseServerError {
363 status_code: 401,
364 message: "Unauthorized".to_string(),
365 };
366 assert_eq!(DockerError::from(err), DockerError::Unauthorized);
367 }
368
369 #[test]
370 fn test_docker_error_from_bollard_forbidden() {
371 let err = bollard::errors::Error::DockerResponseServerError {
372 status_code: 403,
373 message: "Forbidden".to_string(),
374 };
375 assert_eq!(DockerError::from(err), DockerError::Forbidden);
376 }
377
378 #[test]
379 fn test_docker_error_from_bollard_not_found() {
380 let err = bollard::errors::Error::DockerResponseServerError {
381 status_code: 404,
382 message: "Not Found".to_string(),
383 };
384 assert_eq!(DockerError::from(err), DockerError::NotFound);
385 }
386
387 #[test]
388 fn test_docker_error_from_bollard_conflict() {
389 let err = bollard::errors::Error::DockerResponseServerError {
390 status_code: 409,
391 message: "Conflict".to_string(),
392 };
393 assert_eq!(DockerError::from(err), DockerError::Conflict);
394 }
395
396 #[test]
397 fn test_docker_error_from_bollard_server_error() {
398 let err = bollard::errors::Error::DockerResponseServerError {
399 status_code: 500,
400 message: "Internal Server Error".to_string(),
401 };
402 assert_eq!(DockerError::from(err), DockerError::ServerError);
403 }
404
405 #[test]
406 fn test_docker_error_from_bollard_other_status_code() {
407 let err = bollard::errors::Error::DockerResponseServerError {
408 status_code: 503,
409 message: "Service Unavailable".to_string(),
410 };
411 assert_eq!(
412 DockerError::from(err),
413 DockerError::Other {
414 status_code: Some(503),
415 message: "Service Unavailable".to_string(),
416 }
417 );
418 }
419
420 #[test]
421 fn test_docker_error_from_bollard_non_server_error() {
422 let err = bollard::errors::Error::RequestTimeoutError;
423 let result = DockerError::from(err);
424 assert!(matches!(
425 result,
426 DockerError::Other {
427 status_code: None,
428 ..
429 }
430 ));
431 }
432
433 #[test]
434 fn test_docker_error_display() {
435 assert_eq!(
436 DockerError::NotModified.to_string(),
437 "resource not modified"
438 );
439 assert_eq!(DockerError::BadRequest.to_string(), "bad request");
440 assert_eq!(DockerError::Unauthorized.to_string(), "unauthorized");
441 assert_eq!(DockerError::Forbidden.to_string(), "forbidden");
442 assert_eq!(DockerError::NotFound.to_string(), "not found");
443 assert_eq!(DockerError::Conflict.to_string(), "conflict");
444 assert_eq!(
445 DockerError::ServerError.to_string(),
446 "internal server error"
447 );
448 assert_eq!(
449 DockerError::Other {
450 status_code: Some(503),
451 message: "oops".to_string(),
452 }
453 .to_string(),
454 "docker error (status Some(503)): oops"
455 );
456 }
457
458 #[test]
459 fn test_container_health_status_from_bollard_empty() {
460 assert_eq!(
461 ContainerHealthStatus::from(bollard::models::HealthStatusEnum::EMPTY),
462 ContainerHealthStatus::Empty
463 );
464 }
465
466 #[test]
467 fn test_container_health_status_from_bollard_healthy() {
468 assert_eq!(
469 ContainerHealthStatus::from(bollard::models::HealthStatusEnum::HEALTHY),
470 ContainerHealthStatus::Healthy
471 );
472 }
473
474 #[test]
475 fn test_container_health_status_from_bollard_unhealthy() {
476 assert_eq!(
477 ContainerHealthStatus::from(bollard::models::HealthStatusEnum::UNHEALTHY),
478 ContainerHealthStatus::Unhealthy
479 );
480 }
481
482 #[test]
483 fn test_container_health_status_from_bollard_none() {
484 assert_eq!(
485 ContainerHealthStatus::from(bollard::models::HealthStatusEnum::NONE),
486 ContainerHealthStatus::None
487 );
488 }
489
490 #[test]
491 fn test_container_health_status_from_bollard_starting() {
492 assert_eq!(
493 ContainerHealthStatus::from(bollard::models::HealthStatusEnum::STARTING),
494 ContainerHealthStatus::Starting
495 );
496 }
497}
498
499pub trait DockerLogContainer {
500 fn logs<'a>(
501 &'a self,
502 container_id: &'a str,
503 options: Option<LogsOptions>,
504 ) -> impl Stream<Item = Result<LogOutput, String>> + 'a;
505}
506
507impl DockerLogContainer for Docker {
508 fn logs<'a>(
509 &'a self,
510 container_id: &'a str,
511 options: Option<LogsOptions>,
512 ) -> impl Stream<Item = Result<LogOutput, String>> + 'a {
513 self.logs(container_id, options).map_err(|e| e.to_string())
514 }
515}