testcontainers_ext/
lib.rs1#![crate_name = "testcontainers_ext"]
2
3use bollard::{
4 models::ContainerSummaryStateEnum, query_parameters::ListContainersOptions,
5 query_parameters::StopContainerOptions,
6};
7use std::future::Future;
8use testcontainers::{ContainerRequest, Image, ImageExt, TestcontainersError};
9
10pub trait ImagePruneExistedLabelExt<I>: Sized + ImageExt<I> + Send
11where
12 I: Image,
13{
14 fn with_prune_existed_label(
39 self,
40 scope: &str,
41 container_label: &str,
42 prune: bool,
43 force: bool,
44 ) -> impl Future<Output = Result<ContainerRequest<I>, TestcontainersError>> + Send {
45 use std::collections::HashMap;
46
47 use bollard::query_parameters::PruneContainersOptions;
48 use testcontainers::core::client::docker_client_instance;
49
50 let testcontainers_project_key = format!("{scope}.testcontainers.scope");
51 let testcontainers_container_key = format!("{scope}.testcontainers.container");
52 let testcontainers_prune_key = format!("{scope}.testcontainers.prune");
53
54 async move {
55 if prune {
56 let client = docker_client_instance().await?;
57
58 let mut filters = HashMap::<String, Vec<String>>::new();
59
60 filters.insert(
61 String::from("label"),
62 vec![
63 format!("{testcontainers_prune_key}=true"),
64 format!("{}={}", testcontainers_project_key, scope),
65 format!("{}={}", testcontainers_container_key, container_label),
66 ],
67 );
68
69 if force {
70 let result = client
71 .list_containers(Some(ListContainersOptions {
72 all: false,
73 filters: Some(filters.clone()),
74 ..Default::default()
75 }))
76 .await
77 .map_err(|err| TestcontainersError::Other(Box::new(err)))?;
78
79 let remove_containers = result
80 .iter()
81 .filter(|c| {
82 c.state
83 .is_some_and(|s| matches!(s, ContainerSummaryStateEnum::RUNNING))
84 })
85 .flat_map(|c| c.id.as_deref())
86 .collect::<Vec<_>>();
87
88 futures::future::try_join_all(
89 remove_containers
90 .iter()
91 .map(|c| client.stop_container(c, None::<StopContainerOptions>)),
92 )
93 .await
94 .map_err(|error| TestcontainersError::Other(Box::new(error)))?;
95
96 #[cfg(feature = "tracing")]
97 if !remove_containers.is_empty() {
98 tracing::warn!(name = "stop running containers", result = ?remove_containers);
99 }
100 }
101
102 let prune_result = client
103 .prune_containers(Some(PruneContainersOptions {
104 filters: Some(filters),
105 }))
106 .await;
107
108 match prune_result {
109 Ok(_result) => {
110 #[cfg(feature = "tracing")]
111 if _result
112 .containers_deleted
113 .as_ref()
114 .is_some_and(|c| !c.is_empty())
115 {
116 tracing::warn!(name = "prune existed containers", result = ?_result);
117 }
118 }
119 Err(bollard::errors::Error::DockerResponseServerError { status_code: 409, .. }) => {
120 #[cfg(feature = "tracing")]
121 tracing::debug!("Concurrent prune currently in progress, safely ignoring HTTP 409.");
122 }
123 Err(err) => return Err(TestcontainersError::Other(Box::new(err))),
124 }
125 }
126
127 let result = self.with_labels([
128 (testcontainers_prune_key, "true"),
129 (testcontainers_project_key, scope),
130 (testcontainers_container_key, container_label),
131 ]);
132
133 Ok(result)
134 }
135 }
136}
137
138impl<R, I> ImagePruneExistedLabelExt<I> for R
139where
140 R: Sized + ImageExt<I> + Send,
141 I: Image,
142{
143}
144
145pub trait ImageDefaultLogConsumerExt<I>: Sized + ImageExt<I>
146where
147 I: Image,
148{
149 fn with_default_log_consumer(self) -> ContainerRequest<I> {
173 use testcontainers::core::logs::consumer::logging_consumer::LoggingConsumer;
174
175 self.with_log_consumer(
176 LoggingConsumer::new()
177 .with_stdout_level(log::Level::Info)
178 .with_stderr_level(log::Level::Error),
179 )
180 }
181}
182
183impl<R, I> ImageDefaultLogConsumerExt<I> for R
184where
185 R: Sized + ImageExt<I>,
186 I: Image,
187{
188}