torrust_tracker/console/ci/e2e/
docker.rs1use std::io;
3use std::process::{Command, Output};
4use std::thread::sleep;
5use std::time::{Duration, Instant};
6
7pub 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 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
28pub struct RunOptions {
30 pub env_vars: Vec<(String, String)>,
31 pub ports: Vec<String>,
32}
33
34impl Docker {
35 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 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 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 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 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 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 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 #[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 #[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 #[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}