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}