Skip to main content

testcontainers_ext/
lib.rs

1#![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    /// Given a scope, a container label, a prune flag, and a force flag,
15    /// this method will prune the container if the prune flag is true.
16    ///
17    /// Example:
18    ///
19    /// ```
20    /// use tokio::runtime::Runtime;
21    /// use testcontainers::{core::{IntoContainerPort, WaitFor}, runners::AsyncRunner, GenericImage, ImageExt};
22    /// use testcontainers_ext::ImagePruneExistedLabelExt;
23    /// use anyhow::Result;
24    ///
25    /// async fn test () -> Result<()> {
26    ///   let container = GenericImage::new("redis", "7.2.4")
27    ///         .with_exposed_port(6379.tcp())
28    ///         .with_wait_for(WaitFor::message_on_stdout("Ready to accept connections"))
29    ///         .with_prune_existed_label("my-project-scope", "redis", true, true).await?
30    ///         .start()
31    ///         .await?;
32    ///    Ok(())
33    /// }
34    ///
35    /// Runtime::new().unwrap().block_on(test()).unwrap();
36    /// ```
37    ///
38    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    /// Given a container, this method will return a container request with a default log consumer.
150    ///
151    /// Example:
152    ///
153    /// ```
154    /// use tokio::runtime::Runtime;
155    /// use testcontainers::{core::{IntoContainerPort, WaitFor}, runners::AsyncRunner, GenericImage, ImageExt};
156    /// use testcontainers_ext::ImageDefaultLogConsumerExt;
157    /// use anyhow::Result;
158    ///
159    /// async fn test () -> Result<()> {
160    ///     let container = GenericImage::new("redis", "7.2.4")
161    ///         .with_exposed_port(6379.tcp())
162    ///         .with_wait_for(WaitFor::message_on_stdout("Ready to accept connections"))
163    ///         .with_default_log_consumer()
164    ///         .start()
165    ///         .await?;
166    ///   Ok(())
167    /// }
168    ///
169    /// Runtime::new().unwrap().block_on(test()).unwrap();
170    /// ```
171    ///
172    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}