Skip to main content

omry_integration/
containers.rs

1//! Utilities for setting up and working with test containers.
2use std::collections::HashMap;
3
4use anyhow::Result;
5use testcontainers::bollard::Docker;
6use testcontainers::bollard::query_parameters::ListNetworksOptions;
7use testcontainers::core::ContainerRequest;
8use testcontainers::core::logs::consumer::{LogConsumer, logging_consumer::LoggingConsumer};
9use testcontainers::{Image, ImageExt};
10
11/// Trait alias for the [`LogConsumer`] trait with trait bounds we need to use it
12/// with the [`ContainerConfig`] struct.
13pub trait ContainerLogConsumer: LogConsumer + Send + Sync + 'static {}
14impl<L> ContainerLogConsumer for L where L: LogConsumer + Send + Sync + 'static {}
15
16/// Container configuration for the subset of [`ImageExt`] parameters
17/// that we use in tests.
18#[allow(clippy::missing_docs_in_private_items)]
19#[derive(Debug, Clone)]
20pub struct ImageExtConfig<L>
21where
22    L: ContainerLogConsumer,
23{
24    container_name: Option<String>,
25    network: Option<String>,
26    log_consumer: Option<L>,
27}
28
29impl<L> ImageExtConfig<L>
30where
31    L: ContainerLogConsumer,
32{
33    /// Sets the container name in the config to the given string.
34    #[must_use]
35    pub fn with_container_name(mut self, container_name: String) -> Self {
36        self.container_name = Some(container_name);
37        self
38    }
39
40    /// Sets the container network name to the given string.
41    #[must_use]
42    pub fn with_network(mut self, name: String) -> Self {
43        self.network = Some(name);
44        self
45    }
46
47    /// Sets the log consumer for the container to the given log consumer.
48    #[must_use]
49    pub fn with_log_consumer(mut self, log_consumer: L) -> Self {
50        self.log_consumer = Some(log_consumer);
51        self
52    }
53
54    /// Calls the appropriate [`ImageExt`] methods for the values
55    /// that have been set in the other methods.
56    pub fn configure_container<RI, I>(self, image: RI) -> ContainerRequest<I>
57    where
58        RI: Into<ContainerRequest<I>>,
59        I: Image,
60    {
61        let mut container_request = image.into();
62
63        if let Some(container_name) = self.container_name {
64            container_request = container_request.with_container_name(container_name);
65        }
66
67        if let Some(network) = self.network {
68            container_request = container_request.with_network(network);
69        }
70
71        if let Some(log_consumer) = self.log_consumer {
72            container_request = container_request.with_log_consumer(log_consumer);
73        }
74
75        container_request
76    }
77}
78
79impl Default for ImageExtConfig<LoggingConsumer> {
80    fn default() -> Self {
81        Self {
82            container_name: None,
83            network: None,
84            log_consumer: None,
85        }
86    }
87}
88
89/// Image configuration for the subset of [`Image`] parameters
90/// that we use in tests.
91#[derive(Debug, Copy, Clone)]
92pub struct ImageConfig {
93    /// Whether the image should be configured to expose any posts to the host.
94    ///
95    /// If [`true`], it is up to the [`Image`] implementation to decide
96    /// which ports to expose, if any.
97    pub should_expose_ports: bool,
98}
99
100impl Default for ImageConfig {
101    /// Creates an [`ImageConfig`] with the default values that make sense
102    /// for most tests.
103    fn default() -> Self {
104        Self {
105            should_expose_ports: true,
106        }
107    }
108}
109
110impl ImageConfig {
111    /// Sets `should_expose_ports` to the given value.
112    #[must_use]
113    pub fn with_expose_ports(mut self, should_expose_ports: bool) -> Self {
114        self.should_expose_ports = should_expose_ports;
115        self
116    }
117}
118
119/// Struct to hold container configuration values.
120#[derive(Debug)]
121pub struct ContainerConfig<L>
122where
123    L: ContainerLogConsumer,
124{
125    /// Subset of options available for configuring [`Image`] implementations.
126    pub image_config: ImageConfig,
127
128    /// Subset of options available for configuring containers via [`ImageExt`] methods.
129    pub image_ext_config: ImageExtConfig<L>,
130}
131
132impl<L> ContainerConfig<L>
133where
134    L: ContainerLogConsumer,
135{
136    /// Creates a new [`ContainerConfig`] with the given configuration values.
137    pub fn new(image_config: ImageConfig, image_ext_config: ImageExtConfig<L>) -> Self {
138        Self {
139            image_config,
140            image_ext_config,
141        }
142    }
143
144    /// Alias for [`ImageExtConfig::configure_container`].
145    pub fn configure_container<RI, I>(self, image: RI) -> ContainerRequest<I>
146    where
147        RI: Into<ContainerRequest<I>>,
148        I: Image,
149    {
150        self.image_ext_config.configure_container(image)
151    }
152}
153
154impl Default for ContainerConfig<LoggingConsumer> {
155    fn default() -> Self {
156        Self {
157            image_config: ImageConfig::default(),
158            image_ext_config: ImageExtConfig::default(),
159        }
160    }
161}
162
163/// Tries to get the gateway IP of the docker bridge network.
164///
165/// We need this to connect to local services within docker
166/// regardless if we're running on Mac or linux.
167///
168/// ## Errors
169/// Returns [`anyhow::Result`] if we can't:
170///
171/// - connect to Docker on the default socket
172/// - list Docker networks
173/// - find the 'bridge' network
174/// - get the IPAM gateway IP for the bridge network
175pub async fn get_bridge_gateway_ip() -> Result<String> {
176    let docker = Docker::connect_with_socket_defaults()?;
177
178    let mut filters = HashMap::new();
179    let network_name = "bridge".to_string();
180    filters.insert("name".to_string(), vec![network_name.clone()]);
181    let list_options = ListNetworksOptions {
182        filters: Some(filters),
183    };
184
185    let networks = docker.list_networks(Some(list_options)).await?;
186    let bridge = networks
187        .first()
188        .ok_or_else(|| anyhow::anyhow!("can't find network '{network_name}'"))?;
189
190    let gateway_ip = bridge
191        .ipam
192        .as_ref()
193        .and_then(|ipam| ipam.config.as_ref())
194        .and_then(|configs| configs.first())
195        .and_then(|config| config.gateway.as_ref())
196        .ok_or_else(|| {
197            anyhow::anyhow!(
198                "can't get the IPAM gateway IP for network. \
199        full network object:\n{bridge:#?}"
200            )
201        })?;
202
203    Ok(gateway_ip.clone())
204}