testcontainers_ext/
lib.rs

1#![crate_name = "testcontainers_ext"]
2
3use bollard::container::ListContainersOptions;
4use std::future::Future;
5use testcontainers::{ContainerRequest, Image, ImageExt, TestcontainersError};
6
7pub trait ImagePruneExistedLabelExt<I>: Sized + ImageExt<I> + Send
8where
9    I: Image,
10{
11    /// Given a scope, a container label, a prune flag, and a force flag,
12    /// this method will prune the container if the prune flag is true.
13    ///
14    /// Example:
15    ///
16    /// ```
17    /// use tokio::runtime::Runtime;
18    /// use testcontainers::{core::{IntoContainerPort, WaitFor}, runners::AsyncRunner, GenericImage, ImageExt};
19    /// use testcontainers_ext::ImagePruneExistedLabelExt;
20    /// use anyhow::Result;
21    ///
22    /// async fn test () -> Result<()> {
23    ///   let container = GenericImage::new("redis", "7.2.4")
24    ///         .with_exposed_port(6379.tcp())
25    ///         .with_wait_for(WaitFor::message_on_stdout("Ready to accept connections"))
26    ///         .with_prune_existed_label("my-project-scope", "redis", true, true).await?
27    ///         .start()
28    ///         .await?;
29    ///    Ok(())
30    /// }
31    ///
32    /// Runtime::new().unwrap().block_on(test()).unwrap();
33    /// ```
34    ///
35    fn with_prune_existed_label(
36        self,
37        scope: &str,
38        container_label: &str,
39        prune: bool,
40        force: bool,
41    ) -> impl Future<Output = Result<ContainerRequest<I>, TestcontainersError>> + Send {
42        use std::collections::HashMap;
43
44        use bollard::container::PruneContainersOptions;
45        use testcontainers::core::client::docker_client_instance;
46
47        let testcontainers_project_key = format!("{scope}.testcontainers.scope");
48        let testcontainers_container_key = format!("{scope}.testcontainers.container");
49        let testcontainers_prune_key = format!("{scope}.testcontainers.prune");
50
51        async move {
52            if prune {
53                let client = docker_client_instance().await?;
54
55                let mut filters = HashMap::<String, Vec<String>>::new();
56
57                filters.insert(
58                    String::from("label"),
59                    vec![
60                        format!("{testcontainers_prune_key}=true"),
61                        format!("{}={}", testcontainers_project_key, scope),
62                        format!("{}={}", testcontainers_container_key, container_label),
63                    ],
64                );
65
66                if force {
67                    let result = client
68                        .list_containers(Some(ListContainersOptions {
69                            all: false,
70                            filters: filters.clone(),
71                            ..Default::default()
72                        }))
73                        .await
74                        .map_err(|err| TestcontainersError::Other(Box::new(err)))?;
75
76                    let remove_containers = result
77                        .iter()
78                        .filter(|c| matches!(c.state.as_deref(), Some("running")))
79                        .flat_map(|c| c.id.as_deref())
80                        .collect::<Vec<_>>();
81
82                    futures::future::try_join_all(
83                        remove_containers
84                            .iter()
85                            .map(|c| client.stop_container(c, None)),
86                    )
87                    .await
88                    .map_err(|error| TestcontainersError::Other(Box::new(error)))?;
89
90                    #[cfg(feature = "tracing")]
91                    if !remove_containers.is_empty() {
92                        tracing::warn!(name = "stop running containers", result = ?remove_containers);
93                    }
94                }
95
96                let _result = client
97                    .prune_containers(Some(PruneContainersOptions { filters }))
98                    .await
99                    .map_err(|err| TestcontainersError::Other(Box::new(err)))?;
100
101                #[cfg(feature = "tracing")]
102                if _result
103                    .containers_deleted
104                    .as_ref()
105                    .is_some_and(|c| !c.is_empty())
106                {
107                    tracing::warn!(name = "prune existed containers", result = ?_result);
108                }
109            }
110
111            let result = self.with_labels([
112                (testcontainers_prune_key, "true"),
113                (testcontainers_project_key, scope),
114                (testcontainers_container_key, container_label),
115            ]);
116
117            Ok(result)
118        }
119    }
120}
121
122impl<R, I> ImagePruneExistedLabelExt<I> for R
123where
124    R: Sized + ImageExt<I> + Send,
125    I: Image,
126{
127}
128
129pub trait ImageDefaultLogConsumerExt<I>: Sized + ImageExt<I>
130where
131    I: Image,
132{
133    /// Given a container, this method will return a container request with a default log consumer.
134    ///
135    /// Example:
136    ///
137    /// ```
138    /// use tokio::runtime::Runtime;
139    /// use testcontainers::{core::{IntoContainerPort, WaitFor}, runners::AsyncRunner, GenericImage, ImageExt};
140    /// use testcontainers_ext::ImageDefaultLogConsumerExt;
141    /// use anyhow::Result;
142    ///
143    /// async fn test () -> Result<()> {
144    ///     let container = GenericImage::new("redis", "7.2.4")
145    ///         .with_exposed_port(6379.tcp())
146    ///         .with_wait_for(WaitFor::message_on_stdout("Ready to accept connections"))
147    ///         .with_default_log_consumer()
148    ///         .start()
149    ///         .await?;
150    ///   Ok(())
151    /// }
152    ///
153    /// Runtime::new().unwrap().block_on(test()).unwrap();
154    /// ```
155    ///
156    fn with_default_log_consumer(self) -> ContainerRequest<I> {
157        use testcontainers::core::logs::consumer::logging_consumer::LoggingConsumer;
158
159        self.with_log_consumer(
160            LoggingConsumer::new()
161                .with_stdout_level(log::Level::Info)
162                .with_stderr_level(log::Level::Error),
163        )
164    }
165}
166
167impl<R, I> ImageDefaultLogConsumerExt<I> for R
168where
169    R: Sized + ImageExt<I>,
170    I: Image,
171{
172}