torrust_tracker/console/ci/e2e/
docker.rs

1//! Docker command wrapper.
2use std::io;
3use std::process::{Command, Output};
4use std::thread::sleep;
5use std::time::{Duration, Instant};
6
7/// Docker command wrapper.
8pub struct Docker {}
9
10#[derive(Clone, Debug)]
11pub struct RunningContainer {
12    pub image: String,
13    pub name: String,
14    pub output: Output,
15}
16
17impl Drop for RunningContainer {
18    /// Ensures that the temporary container is stopped when the struct goes out
19    /// of scope.
20    fn drop(&mut self) {
21        tracing::info!("Dropping running container: {}", self.name);
22        if Docker::is_container_running(&self.name) {
23            let _unused = Docker::stop(self);
24        }
25    }
26}
27
28/// `docker run` command options.
29pub struct RunOptions {
30    pub env_vars: Vec<(String, String)>,
31    pub ports: Vec<String>,
32}
33
34impl Docker {
35    /// Builds a Docker image from a given Dockerfile.
36    ///
37    /// # Errors
38    ///
39    /// Will fail if the docker build command fails.
40    pub fn build(dockerfile: &str, tag: &str) -> io::Result<()> {
41        let status = Command::new("docker")
42            .args(["build", "-f", dockerfile, "-t", tag, "."])
43            .status()?;
44
45        if status.success() {
46            Ok(())
47        } else {
48            Err(io::Error::new(
49                io::ErrorKind::Other,
50                format!("Failed to build Docker image from dockerfile {dockerfile}"),
51            ))
52        }
53    }
54
55    /// Runs a Docker container from a given image with multiple environment variables.
56    ///
57    /// # Arguments
58    ///
59    /// * `image` - The Docker image to run.
60    /// * `container` - The name for the Docker container.
61    /// * `env_vars` - A slice of tuples, each representing an environment variable as ("KEY", "value").
62    ///
63    /// # Errors
64    ///
65    /// Will fail if the docker run command fails.
66    pub fn run(image: &str, container: &str, options: &RunOptions) -> io::Result<RunningContainer> {
67        let initial_args = vec![
68            "run".to_string(),
69            "--detach".to_string(),
70            "--name".to_string(),
71            container.to_string(),
72        ];
73
74        // Add environment variables
75        let mut env_var_args: Vec<String> = vec![];
76        for (key, value) in &options.env_vars {
77            env_var_args.push("--env".to_string());
78            env_var_args.push(format!("{key}={value}"));
79        }
80
81        // Add port mappings
82        let mut port_args: Vec<String> = vec![];
83        for port in &options.ports {
84            port_args.push("--publish".to_string());
85            port_args.push(port.to_string());
86        }
87
88        let args = [initial_args, env_var_args, port_args, [image.to_string()].to_vec()].concat();
89
90        tracing::debug!("Docker run args: {:?}", args);
91
92        let output = Command::new("docker").args(args).output()?;
93
94        if output.status.success() {
95            Ok(RunningContainer {
96                image: image.to_owned(),
97                name: container.to_owned(),
98                output,
99            })
100        } else {
101            Err(io::Error::new(
102                io::ErrorKind::Other,
103                format!("Failed to run Docker image {image}"),
104            ))
105        }
106    }
107
108    /// Stops a Docker container.
109    ///
110    /// # Errors
111    ///
112    /// Will fail if the docker stop command fails.    
113    pub fn stop(container: &RunningContainer) -> io::Result<()> {
114        let status = Command::new("docker").args(["stop", &container.name]).status()?;
115
116        if status.success() {
117            Ok(())
118        } else {
119            Err(io::Error::new(
120                io::ErrorKind::Other,
121                format!("Failed to stop Docker container {}", container.name),
122            ))
123        }
124    }
125
126    /// Removes a Docker container.
127    ///
128    /// # Errors
129    ///
130    /// Will fail if the docker rm command fails.    
131    pub fn remove(container: &str) -> io::Result<()> {
132        let status = Command::new("docker").args(["rm", "-f", container]).status()?;
133
134        if status.success() {
135            Ok(())
136        } else {
137            Err(io::Error::new(
138                io::ErrorKind::Other,
139                format!("Failed to remove Docker container {container}"),
140            ))
141        }
142    }
143
144    /// Fetches logs from a Docker container.
145    ///
146    /// # Errors
147    ///
148    /// Will fail if the docker logs command fails.
149    pub fn logs(container: &str) -> io::Result<String> {
150        let output = Command::new("docker").args(["logs", container]).output()?;
151
152        if output.status.success() {
153            Ok(String::from_utf8_lossy(&output.stdout).to_string())
154        } else {
155            Err(io::Error::new(
156                io::ErrorKind::Other,
157                format!("Failed to fetch logs from Docker container {container}"),
158            ))
159        }
160    }
161
162    /// Checks if a Docker container is healthy.
163    #[must_use]
164    pub fn wait_until_is_healthy(name: &str, timeout: Duration) -> bool {
165        let start = Instant::now();
166
167        while start.elapsed() < timeout {
168            let Ok(output) = Command::new("docker")
169                .args(["ps", "-f", &format!("name={name}"), "--format", "{{.Status}}"])
170                .output()
171            else {
172                return false;
173            };
174
175            let output_str = String::from_utf8_lossy(&output.stdout);
176
177            tracing::info!("Waiting until container is healthy: {:?}", output_str);
178
179            if output_str.contains("(healthy)") {
180                return true;
181            }
182
183            sleep(Duration::from_secs(1));
184        }
185
186        false
187    }
188
189    /// Checks if a Docker container is running.
190    ///
191    /// # Arguments
192    ///
193    /// * `container` - The name of the Docker container.
194    ///
195    /// # Returns
196    ///
197    /// `true` if the container is running, `false` otherwise.
198    #[must_use]
199    pub fn is_container_running(container: &str) -> bool {
200        match Command::new("docker")
201            .args(["ps", "-f", &format!("name={container}"), "--format", "{{.Names}}"])
202            .output()
203        {
204            Ok(output) => {
205                let output_str = String::from_utf8_lossy(&output.stdout);
206                output_str.contains(container)
207            }
208            Err(_) => false,
209        }
210    }
211
212    /// Checks if a Docker container exists.
213    ///
214    /// # Arguments
215    ///
216    /// * `container` - The name of the Docker container.
217    ///
218    /// # Returns
219    ///
220    /// `true` if the container exists, `false` otherwise.
221    #[must_use]
222    pub fn container_exist(container: &str) -> bool {
223        match Command::new("docker")
224            .args(["ps", "-a", "-f", &format!("name={container}"), "--format", "{{.Names}}"])
225            .output()
226        {
227            Ok(output) => {
228                let output_str = String::from_utf8_lossy(&output.stdout);
229                output_str.contains(container)
230            }
231            Err(_) => false,
232        }
233    }
234}