testcontainers_rs/
client.rs

1use super::container::Container;
2use super::image::DockerImage;
3use super::logs::LogStream;
4use super::ports::Ports;
5use async_trait::async_trait;
6use std::net;
7
8#[async_trait]
9pub trait DockerClient
10where
11    Self: Sized,
12{
13    type Client;
14    type Error: std::error::Error;
15
16    fn native(&self) -> &Self::Client;
17    fn stdout_logs(&self, id: &str) -> LogStream<'_>;
18    fn stderr_logs(&self, id: &str) -> LogStream<'_>;
19
20    async fn create<I: Into<DockerImage> + Send>(
21        &self,
22        image: I,
23    ) -> Result<Container<Self>, Self::Error>;
24
25    async fn host(&self, id: &str) -> Result<net::IpAddr, Self::Error>;
26    async fn ports(&self, id: &str) -> Result<Ports, Self::Error>;
27    async fn rm(&self, id: &str) -> Result<(), Self::Error>;
28    async fn stop(&self, id: &str) -> Result<(), Self::Error>;
29    async fn start(&self, id: &str) -> Result<(), Self::Error>;
30
31    // async fn inspect(&self, id: &str) -> ContainerInspectResponse;
32}
33
34pub mod bollard {
35    use super::{Container, DockerClient, DockerImage, LogStream, Ports};
36    use async_trait::async_trait;
37    use bollard::container::LogsOptions;
38    use color_eyre::eyre;
39    use futures::{StreamExt, TryStreamExt};
40    use std::sync::Arc;
41    use std::{fmt, io, net};
42
43    #[derive(thiserror::Error, Debug)]
44    pub enum Error {
45        #[error("missing host")]
46        MissingHost,
47
48        #[error("failed to parse address {addr}")]
49        ParseAddr {
50            addr: String,
51            source: std::net::AddrParseError,
52        },
53
54        #[error("failed to connect to the docker daemon")]
55        Connection(#[source] bollard::errors::Error),
56
57        #[error(transparent)]
58        Bollard(#[from] bollard::errors::Error),
59    }
60
61    #[derive(Clone)]
62    pub struct Client {
63        inner: Arc<bollard::Docker>,
64        id: Option<String>,
65    }
66
67    impl fmt::Debug for Client {
68        fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
69            f.debug_struct("Client").field("id", &self.id).finish()
70        }
71    }
72
73    impl Client {
74        pub async fn new() -> Result<Self, Error> {
75            let client =
76                bollard::Docker::connect_with_local_defaults().map_err(Error::Connection)?;
77            // bollard::Docker::connect_with_http_defaults().map_err(Error::Connection)?;
78            let inner = Arc::new(client);
79            let id = inner.info().await.ok().and_then(|info| info.id);
80            Ok(Self { inner, id })
81        }
82
83        fn logs(&self, id: &str, options: LogsOptions<String>) -> LogStream<'_> {
84            let stream = self
85                .inner
86                .logs(&id, Some(options))
87                .map_err(|err| io::Error::new(io::ErrorKind::Other, err))
88                .map(|chunk| {
89                    let bytes = chunk?.into_bytes();
90                    Ok(String::from_utf8_lossy(bytes.as_ref()).to_string())
91                });
92            LogStream::new(stream)
93        }
94    }
95
96    // impl std::ops::Deref for Client {
97    //     type Target = bollard::Docker;
98
99    //     fn deref(&self) -> &Self::Target {
100    //         &self.inner
101    //     }
102    // }
103
104    // impl std::ops::DerefMut for Client {
105    //     fn deref_mut(&mut self) -> &mut Self::Target {
106    //         &mut self.inner
107    //     }
108    // }
109
110    #[async_trait]
111    impl DockerClient for Client {
112        type Client = bollard::Docker;
113        type Error = Error;
114
115        async fn create<I: Into<DockerImage> + Send>(
116            &self,
117            image: I,
118        ) -> Result<Container<Self>, Self::Error> {
119            use bollard::container::{Config, CreateContainerOptions};
120            use bollard::models::{HostConfig, PortBinding};
121            use std::collections::HashMap;
122
123            let image = image.into();
124
125            let volumes: HashMap<String, HashMap<(), ()>> = image
126                .volumes
127                .iter()
128                .map(|(orig, dest)| (format!("{}:{}", orig, dest), HashMap::new()))
129                .collect();
130
131            let mut exposed_ports: HashMap<String, HashMap<(), ()>> = HashMap::new();
132            // let mut exposed_ports = HashMap::new();
133            let mut port_bindings = HashMap::new();
134            for port in &image.exposed_ports {
135                let proto_port = format!("{}/tcp", port.container);
136                exposed_ports.insert(proto_port.clone(), HashMap::new());
137                port_bindings.insert(
138                    proto_port,
139                    None::<Vec<PortBinding>>,
140                    // Some(vec![PortBinding {
141                    //     host_ip: Some(String::from("127.0.0.1")),
142                    //     host_port: Some(port.host.to_string()),
143                    // }]),
144                );
145            }
146
147            // let exposed_ports: HashMap<String, Option<Vec<HashMap<(), ()>> =
148            //     HashMap::from_iter(vec![("80".to_string(), HashMap::new())]);
149
150            let mut host_config = HostConfig {
151                shm_size: image.shm_size,
152                // port_bindings: Some(port_bindings),
153                ..Default::default()
154            };
155
156            // let exposed_ports: HashMap<String, HashMap<(), ()>> =
157            //     HashMap::from_iter(vec![("80".to_string(), HashMap::new())]);
158
159            let mut config: Config<String> = Config {
160                image: Some(image.descriptor()),
161                cmd: Some(image.cmd.clone()),
162                exposed_ports: Some(exposed_ports),
163                // env: Some(image.env),
164                // volumes: Some(image.volumes),
165                entrypoint: Some(image.entrypoint.clone()),
166                volumes: Some(volumes),
167                host_config: Some(host_config),
168                ..Default::default()
169            };
170
171            // // create network and add it to container creation
172            // if let Some(network) = image.network() {
173            //     config.host_config = config.host_config.map(|mut host_config| {
174            //         host_config.network_mode = Some(network.to_string());
175            //         host_config
176            //     });
177            //     // if self.create_network_if_not_exists(network).await {
178            //     //     let mut guard = self
179            //     //         .inner
180            //     //         .created_networks
181            //     //         .write()
182            //     //         .expect("'failed to lock RwLock'");
183            //     //     guard.push(network.clone());
184            //     // }
185            // }
186
187            let create_options: Option<CreateContainerOptions<String>> = image
188                .container_name
189                .as_ref()
190                .map(|name| CreateContainerOptions {
191                    name: name.to_owned(),
192                });
193
194            // pull the image first
195            use bollard::image::CreateImageOptions;
196            let pull_options = Some(CreateImageOptions {
197                from_image: image.descriptor(),
198                ..Default::default()
199            });
200            let mut pulling = self.inner.create_image(pull_options, None, None);
201            log::debug!("Pulling docker container {}", image.descriptor());
202            while let Some(result) = pulling.next().await {
203                if let Err(err) = result {
204                    return Err(err.into());
205                }
206            }
207            log::debug!("Pulled docker container {}", image.descriptor());
208
209            let container = self.inner.create_container(create_options, config).await?;
210            // match container {
211            //         // Ok(container) => container.id,
212            //         Err(bollard::errors::Error::DockerResponseServerError {
213            //             status_code: 404,
214            //             ..
215            //         }) => {
216            //             // image not found locally, pull first
217            //             {
218            //                 use bollard::image::CreateImageOptions;
219            //                 let pull_options = Some(CreateImageOptions {
220            //                     from_image: image.descriptor(),
221            //                     ..Default::default()
222            //                 });
223            //                 let mut pulling = self.inner.create_image(pull_options, None, None);
224            //                 while let Some(result) = pulling.next().await {
225            //                     if
226            //                     // if result.is_err() {
227            //                     //     result.unwrap();
228            //                     // }
229            //                 }
230            //             }
231            //             // try again
232            //             self.create_container(create_options, config)
233            //                 .await
234            //                 .unwrap()
235            //                 .id
236            //         }
237            //         Err(err) => return
238            //     }
239
240            // let container_id = {
241            //     match container {
242            //         Ok(container) => container.id,
243            //         Err(bollard::errors::Error::DockerResponseServerError {
244            //             status_code: 404,
245            //             ..
246            //         }) => {
247            //             // image not found locally, pull first
248            //             {
249            //                 use bollard::image::CreateImageOptions;
250            //                 let pull_options = Some(CreateImageOptions {
251            //                     from_image: image.descriptor(),
252            //                     ..Default::default()
253            //                 });
254            //                 let mut pulling = self.inner.create_image(pull_options, None, None);
255            //                 while let Some(result) = pulling.next().await {
256            //                     if
257            //                     // if result.is_err() {
258            //                     //     result.unwrap();
259            //                     // }
260            //                 }
261            //             }
262            //             // try again
263            //             self.create_container(create_options, config)
264            //                 .await
265            //                 .unwrap()
266            //                 .id
267            //         }
268            //         Err(err) => panic!("{}", err),
269            //     }
270            // };
271
272            // let container_id = created_container.id;
273            // let container = Container::new(container_id, self.clone(), image).await;
274            Ok(Container::new(container.id, self.clone(), image).await)
275        }
276
277        fn native(&self) -> &Self::Client {
278            &self.inner
279        }
280
281        fn stdout_logs(&self, id: &str) -> LogStream<'_> {
282            self.logs(
283                id,
284                LogsOptions {
285                    follow: true,
286                    stdout: true,
287                    tail: "all".to_string(),
288                    ..Default::default()
289                },
290            )
291        }
292
293        fn stderr_logs(&self, id: &str) -> LogStream<'_> {
294            self.logs(
295                id,
296                LogsOptions {
297                    follow: true,
298                    stderr: true,
299                    tail: "all".to_string(),
300                    ..Default::default()
301                },
302            )
303        }
304
305        async fn host(&self, id: &str) -> Result<net::IpAddr, Self::Error> {
306            let inspect = self.inner.inspect_container(id, None).await?;
307            let addr = inspect
308                .network_settings
309                .and_then(|network| network.ip_address)
310                .ok_or(Self::Error::MissingHost)?;
311            addr.parse()
312                .map_err(|err| Self::Error::ParseAddr { addr, source: err })
313        }
314
315        async fn ports(&self, id: &str) -> Result<Ports, Self::Error> {
316            let inspect = self.inner.inspect_container(id, None).await?;
317            log::debug!("network settings: {:?}", inspect.network_settings);
318            let ports: Ports = inspect
319                .network_settings
320                .unwrap_or_default()
321                .ports
322                .unwrap_or_default()
323                .into();
324            Ok(ports)
325            // .unwrap_or_default()
326        }
327
328        // async fn inspect(&self, id: &str) -> Result<ContainerInspectResponse, Self::Error> {
329        //     Ok(self.inner.inspect_container(id, None).await?)
330        // }
331
332        async fn rm(&self, id: &str) -> Result<(), Self::Error> {
333            Ok(self
334                .inner
335                .remove_container(
336                    id,
337                    Some(bollard::container::RemoveContainerOptions {
338                        force: true,
339                        v: true,
340                        ..Default::default()
341                    }),
342                )
343                .await?)
344        }
345
346        async fn stop(&self, id: &str) -> Result<(), Self::Error> {
347            Ok(self.inner.stop_container(id, None).await?)
348        }
349
350        async fn start(&self, id: &str) -> Result<(), Self::Error> {
351            Ok(self.inner.start_container::<String>(id, None).await?)
352        }
353    }
354
355    #[cfg(test)]
356    mod tests {
357        use super::{Client, DockerClient};
358        use color_eyre::eyre;
359        use pretty_assertions::{assert_eq, assert_ne};
360
361        #[tokio::test(flavor = "multi_thread")]
362        async fn get_native_client() -> eyre::Result<()> {
363            let concrete: Client = Client::new().await?;
364            // let client: Box<&dyn DockerClient<Client = _, Error = _>> = Box::new(&concrete);
365            let native: &bollard::Docker = concrete.native();
366            assert!(std::ptr::eq(&*concrete.inner, native));
367            Ok(())
368        }
369
370        #[tokio::test(flavor = "multi_thread")]
371        async fn expose_all_ports_by_default() -> eyre::Result<()> {
372            let client = Client::new().await?;
373            // let docker = Http::new();
374            // let image = HelloWorld::default();
375            // let container = client.run(image).await?;
376
377            // // inspect volume and env
378            // let container_details = inspect(&docker.inner.bollard, container.id()).await;
379            // assert_that!(container_details.host_config.unwrap().publish_all_ports)
380            //     .is_equal_to(Some(true));
381            Ok(())
382        }
383    }
384}