contain_rs_core/
container.rs

1//!
2//! The container module contains all the basic data types that make up a container.
3//!
4//! Containers can then be run through [`clients`](crate::client::Client).
5//!
6//! See [Container] for further information on containers.
7//!
8
9use std::{fmt::Display, str::FromStr, time::Duration};
10
11use lazy_static::lazy_static;
12use rand::{distributions::Alphanumeric, Rng};
13use regex::Regex;
14
15use crate::error::{ContainerResult, ContainersError};
16
17lazy_static! {
18    static ref IMAGE_REGEX: Regex = Regex::new("([0-9a-zA-Z./]+)(:([0-9a-zA-Z.]+))?").unwrap();
19}
20
21pub trait TryIntoContainer {
22    fn try_into_container(self) -> ContainerResult<Container>;
23}
24
25pub trait IntoContainer {
26    fn into_container(self) -> Container;
27}
28
29impl<T: TryIntoContainer> IntoContainer for T {
30    fn into_container(self) -> Container {
31        self.try_into_container().unwrap()
32    }
33}
34
35impl IntoContainer for Container {
36    fn into_container(self) -> Container {
37        self
38    }
39}
40
41#[derive(Clone)]
42pub struct HealthCheck {
43    pub command: String,
44    pub retries: Option<u32>,
45    pub interval: Option<Duration>,
46    pub start_period: Option<Duration>,
47    pub timeout: Option<Duration>,
48}
49
50impl HealthCheck {
51    pub fn new(command: &str) -> Self {
52        Self {
53            command: command.into(),
54            retries: None,
55            interval: None,
56            start_period: None,
57            timeout: None,
58        }
59    }
60
61    pub fn retries(mut self, retries: u32) -> Self {
62        self.retries = Some(retries);
63        self
64    }
65
66    pub fn interval(mut self, interval: Duration) -> Self {
67        self.interval = Some(interval);
68        self
69    }
70
71    pub fn start_period(mut self, start_period: Duration) -> Self {
72        self.start_period = Some(start_period);
73        self
74    }
75
76    pub fn timeout(mut self, timeout: Duration) -> Self {
77        self.timeout = Some(timeout);
78        self
79    }
80}
81
82///
83/// A wait strategy can be used to wait for a cotnainer to be ready.
84///
85#[derive(Clone, Debug)]
86pub enum WaitStrategy {
87    ///
88    /// Waits for a log message to appear.
89    ///
90    LogMessage { pattern: Regex },
91    ///
92    /// Waits for the container to be healty.
93    ///
94    HealthCheck,
95    ///
96    /// Wait for some amount of time.
97    ///
98    WaitTime { duration: Duration },
99}
100
101#[derive(Clone)]
102pub struct Network {
103    // TODO
104}
105
106#[derive(Clone, PartialEq, Eq)]
107pub struct Port {
108    pub number: String,
109}
110
111impl<T> From<T> for Port
112where
113    T: ToString,
114{
115    fn from(value: T) -> Self {
116        Self {
117            number: value.to_string(),
118        }
119    }
120}
121
122#[derive(Clone)]
123pub struct PortMapping {
124    pub source: Port,
125    pub target: Port,
126}
127
128#[derive(Clone)]
129pub struct EnvVar {
130    pub key: String,
131    pub value: String,
132}
133
134impl EnvVar {
135    pub fn new(key: String, value: String) -> Self {
136        Self { key, value }
137    }
138}
139
140impl<K, V> From<(K, V)> for EnvVar
141where
142    K: Into<String>,
143    V: Into<String>,
144{
145    fn from(value: (K, V)) -> Self {
146        EnvVar::new(value.0.into(), value.1.into())
147    }
148}
149
150#[derive(Clone)]
151pub struct Image {
152    pub name: String,
153    pub tag: String,
154}
155
156impl Display for Image {
157    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
158        write!(f, "{}:{}", self.name, self.tag)
159    }
160}
161
162impl Image {
163    pub fn from_name_and_tag(name: &str, tag: &str) -> Self {
164        Image {
165            name: name.to_string(),
166            tag: tag.to_string(),
167        }
168    }
169}
170
171impl FromStr for Image {
172    type Err = ContainersError;
173
174    fn from_str(s: &str) -> Result<Self, Self::Err> {
175        let caps = IMAGE_REGEX.captures(s);
176
177        if let Some(cap) = caps {
178            Ok(Self::from_name_and_tag(
179                cap.get(1).unwrap().as_str(),
180                cap.get(3).map(|m| m.as_str()).unwrap_or("latest"),
181            ))
182        } else {
183            Err(ContainersError::InvalidImageName {
184                name: s.to_string(),
185            })
186        }
187    }
188}
189
190impl From<Image> for String {
191    fn from(i: Image) -> Self {
192        format!("{}:{}", i.name, i.tag)
193    }
194}
195
196impl From<&Image> for String {
197    fn from(i: &Image) -> Self {
198        format!("{}:{}", i.name, i.tag)
199    }
200}
201
202#[derive(Clone)]
203pub enum Volume {
204    Mount {
205        host_path: String,
206        mount_point: String,
207    },
208    Named {
209        name: String,
210        mount_point: String,
211    },
212}
213
214///
215/// A container makes up the schedulable unit of this crate.
216///
217/// You can define an [Image] for it to be used and define port mappings and environment variables on it for example.
218///
219#[derive(Clone)]
220pub struct Container {
221    pub name: String,
222    pub image: Image,
223    pub command: Vec<String>,
224    pub network: Option<Network>,
225    pub volumes: Vec<Volume>,
226    pub port_mappings: Vec<PortMapping>,
227    pub env_vars: Vec<EnvVar>,
228    pub health_check: Option<HealthCheck>,
229    pub wait_strategy: Option<WaitStrategy>,
230    pub additional_wait_period: Duration,
231}
232
233impl Container {
234    fn gen_hash() -> String {
235        rand::thread_rng()
236            .sample_iter(&Alphanumeric)
237            .take(8)
238            .map(char::from)
239            .collect()
240    }
241
242    ///
243    /// Creates a new container from and [Image]
244    ///
245    pub fn from_image(image: Image) -> Self {
246        Container {
247            name: format!("contain-rs-{}", Self::gen_hash()),
248            image,
249            command: Vec::new(),
250            network: None,
251            port_mappings: Vec::new(),
252            env_vars: Vec::new(),
253            volumes: Vec::new(),
254            health_check: None,
255            wait_strategy: None,
256            additional_wait_period: Duration::from_secs(0),
257        }
258    }
259
260    ///
261    /// Define a specific name for the container.
262    ///
263    /// In case no explicit name is defined contain-rs will generate one as the name is being used by the [crate::client::Client] for interaction.
264    ///
265    pub fn name(&mut self, name: &str) -> &mut Self {
266        self.name = name.into();
267        self
268    }
269
270    ///
271    /// Define an explicit command to run in the container.
272    ///
273    pub fn command(&mut self, command: Vec<String>) -> &mut Self {
274        self.command = command;
275        self
276    }
277
278    pub fn arg<T: Into<String>>(&mut self, arg: T) -> &mut Self {
279        self.command.push(arg.into());
280        self
281    }
282
283    pub fn map_ports<T, T2>(&mut self, ports: &[(T, T2)]) -> &mut Self
284    where
285        T: Into<Port> + Clone,
286        T2: Into<Port> + Clone,
287    {
288        self.port_mappings = ports
289            .iter()
290            .cloned()
291            .map(|mapping| PortMapping {
292                source: mapping.0.into(),
293                target: mapping.1.into(),
294            })
295            .collect();
296
297        self
298    }
299
300    pub fn volume(&mut self, name: &str, mount_point: &str) -> &mut Self {
301        self.volumes.push(Volume::Named {
302            name: name.to_string(),
303            mount_point: mount_point.to_string(),
304        });
305
306        self
307    }
308
309    pub fn mount(&mut self, host_path: &str, mount_point: &str) -> &mut Self {
310        self.volumes.push(Volume::Mount {
311            host_path: host_path.to_string(),
312            mount_point: mount_point.to_string(),
313        });
314
315        self
316    }
317
318    ///
319    /// Map a port from `source` on the host to `target` in the container.
320    ///
321    pub fn map_port(&mut self, source: impl Into<Port>, target: impl Into<Port>) -> &mut Self {
322        self.port_mappings.push(PortMapping {
323            source: source.into(),
324            target: target.into(),
325        });
326        self
327    }
328
329    ///
330    /// Define an environment variable for the container.
331    ///
332    pub fn env_var<T: Into<String>>(&mut self, name: T, value: T) -> &mut Self {
333        self.env_vars.push((name, value).into());
334        self
335    }
336
337    ///
338    /// Add a [WaitStrategy] to be used when running the container.
339    ///
340    pub fn wait_for(&mut self, strategy: WaitStrategy) -> &mut Self {
341        self.wait_strategy = Some(strategy);
342        self
343    }
344
345    ///
346    /// Add some additional wait time for concidering the container healthy.
347    ///
348    /// Contain-rs waits this additional time after the [WaitStrategy] has been concidered successful.
349    ///
350    pub fn additional_wait_period(&mut self, period: Duration) -> &mut Self {
351        self.additional_wait_period = period;
352        self
353    }
354
355    ///
356    /// Add an arbitrary healthcheck to the container.
357    ///
358    /// Some images may define healthchecks already, yet you can use this one to define one yourself explicitly.
359    ///
360    pub fn health_check(&mut self, health_check: HealthCheck) -> &mut Self {
361        self.health_check = Some(health_check);
362        self
363    }
364}
365
366impl From<Image> for Container {
367    fn from(image: Image) -> Self {
368        Container::from_image(image)
369    }
370}