tembo/cli/
docker.rs

1use crate::tui::{self, colors, white_confirmation};
2use anyhow::{bail, Context, Error};
3use colorful::{Color, Colorful};
4use simplelog::*;
5use spinoff::{spinners, Spinner};
6use std::io::{BufRead, BufReader};
7use std::path::Path;
8use std::process::Output;
9use std::process::{Command as ShellCommand, Stdio};
10use std::thread;
11
12pub struct Docker {}
13
14impl Docker {
15    pub fn info() -> Output {
16        ShellCommand::new("sh")
17            .arg("-c")
18            .arg("docker info")
19            .output()
20            .expect("failed to execute process")
21    }
22
23    pub fn installed_and_running() -> Result<(), anyhow::Error> {
24        info!("Checking requirements: [Docker]");
25
26        let output = Self::info();
27        let stdout = String::from_utf8(output.stdout).unwrap();
28        let stderr = String::from_utf8(output.stderr).unwrap();
29
30        // determine if docker is installed
31        if stdout.is_empty() && !stderr.is_empty() {
32            bail!("- Docker is not installed, please visit docker.com to install")
33        } else {
34            // determine if docker is running
35            if !stdout.is_empty() && !stderr.is_empty() {
36                let connection_err = stderr.find("Cannot connect to the Docker daemon");
37
38                if connection_err.is_some() {
39                    bail!("- Docker is not running, please start it and try again")
40                }
41            }
42        }
43
44        Ok(())
45    }
46
47    pub fn build(instance_name: String, verbose: bool) -> Result<(), anyhow::Error> {
48        let mut sp = if !verbose {
49            Some(Spinner::new(
50                spinners::Dots,
51                "Running Docker Build",
52                spinoff::Color::White,
53            ))
54        } else {
55            None
56        };
57
58        let mut show_message = |message: &str, new_spinner: bool| {
59            if let Some(mut spinner) = sp.take() {
60                spinner.stop_with_message(&format!(
61                    "{} {}",
62                    "✓".color(colors::indicator_good()).bold(),
63                    message.color(Color::White).bold()
64                ));
65                if new_spinner {
66                    sp = Some(Spinner::new(
67                        spinners::Dots,
68                        format!("Building container for {}", instance_name),
69                        spinoff::Color::White,
70                    ));
71                }
72            } else {
73                white_confirmation(message);
74            }
75        };
76
77        let command = format!(
78            "cd {} && docker build . -t postgres-{}",
79            instance_name, instance_name
80        );
81        run_command(&command, verbose)?;
82
83        show_message(
84            &format!("Docker Build completed for {}", instance_name),
85            false,
86        );
87
88        Ok(())
89    }
90
91    pub fn docker_compose_up(verbose: bool) -> Result<(), anyhow::Error> {
92        let mut sp = if !verbose {
93            Some(Spinner::new(
94                spinners::Dots,
95                "Running Docker Compose Up",
96                spinoff::Color::White,
97            ))
98        } else {
99            None
100        };
101
102        let mut show_message = |message: &str, new_spinner: bool| {
103            if let Some(mut spinner) = sp.take() {
104                spinner.stop_with_message(&format!(
105                    "{} {}",
106                    "✓".color(colors::indicator_good()).bold(),
107                    message.color(Color::White).bold()
108                ));
109                if new_spinner {
110                    sp = Some(Spinner::new(
111                        spinners::Dots,
112                        "Running docker compose up",
113                        spinoff::Color::White,
114                    ));
115                }
116            } else {
117                white_confirmation(message);
118            }
119        };
120
121        let command = "docker compose up -d --build";
122
123        if verbose {
124            run_command(command, verbose)?;
125        } else {
126            let output = match ShellCommand::new("sh").arg("-c").arg(command).output() {
127                Ok(output) => output,
128                Err(err) => {
129                    return Err(Error::msg(format!("Issue starting the instances: {}", err)))
130                }
131            };
132            let stderr = String::from_utf8(output.stderr).unwrap();
133
134            if !output.status.success() {
135                tui::error(&format!(
136                    "\nThere was an issue starting the instances: {}",
137                    stderr
138                ));
139
140                return Err(Error::msg("Error running docker compose up!"));
141            }
142        }
143
144        show_message("Docker Compose Up completed", false);
145
146        Ok(())
147    }
148
149    pub fn docker_compose_down(verbose: bool) -> Result<(), anyhow::Error> {
150        let path: &Path = Path::new("docker-compose.yml");
151        if !path.exists() {
152            if verbose {
153                println!(
154                    "{} {}",
155                    "✓".color(colors::indicator_good()).bold(),
156                    "No docker-compose.yml found in the directory"
157                        .color(Color::White)
158                        .bold()
159                )
160            }
161            return Ok(());
162        }
163
164        let mut sp = Spinner::new(
165            spinners::Dots,
166            "Running Docker Compose Down",
167            spinoff::Color::White,
168        );
169
170        let command: String = String::from("docker compose down");
171
172        let output = match ShellCommand::new("sh").arg("-c").arg(&command).output() {
173            Ok(output) => output,
174            Err(_) => {
175                sp.stop_with_message("- Tembo instances failed to stop & remove");
176                bail!("There was an issue stopping the instances")
177            }
178        };
179
180        sp.stop_with_message(&format!(
181            "{} {}",
182            "✓".color(colors::indicator_good()).bold(),
183            "Tembo instances stopped & removed"
184                .color(Color::White)
185                .bold()
186        ));
187
188        let stderr = String::from_utf8(output.stderr).unwrap();
189
190        if !output.status.success() {
191            tui::error(&format!(
192                "\nThere was an issue stopping the instances: {}",
193                stderr
194            ));
195
196            return Err(Error::msg("Error running docker compose down!"));
197        }
198
199        Ok(())
200    }
201}
202
203pub fn run_command(command: &str, verbose: bool) -> Result<(), anyhow::Error> {
204    let mut child = ShellCommand::new("sh")
205        .arg("-c")
206        .arg(command)
207        .stdout(Stdio::piped())
208        .stderr(Stdio::piped())
209        .spawn()
210        .with_context(|| format!("Failed to spawn command '{}'", command))?;
211
212    if verbose {
213        let stdout = BufReader::new(child.stdout.take().expect("Failed to open stdout"));
214        let stderr = BufReader::new(child.stderr.take().expect("Failed to open stderr"));
215        let stdout_handle = thread::spawn(move || {
216            for line in stdout.lines() {
217                match line {
218                    Ok(line) => println!("{}", line),
219                    Err(e) => eprintln!("Error reading line from stdout: {}", e),
220                }
221            }
222        });
223        let stderr_handle = thread::spawn(move || {
224            for line in stderr.lines() {
225                match line {
226                    Ok(line) => eprintln!("{}", line),
227                    Err(e) => eprintln!("Error reading line from stderr: {}", e),
228                }
229            }
230        });
231
232        stdout_handle.join().expect("Stdout thread panicked");
233        stderr_handle.join().expect("Stderr thread panicked");
234    }
235
236    let status = child.wait().expect("Failed to wait on child");
237
238    if !status.success() {
239        return Err(Error::msg("Command executed with failures!"));
240    }
241
242    Ok(())
243}
244
245#[cfg(test)]
246mod tests {
247    #[test]
248    #[ignore] // TODO: implement a mocking library and mock the info function
249    fn docker_installed_and_running_test() {
250        // without docker installed
251        // with docker installed and running
252        // with docker installed by not running
253    }
254}