Skip to main content

dsd_util/
utils.rs

1use crate::commands::DOCKER;
2use crate::printer::{color_println, color_println_fmt, Color};
3use anyhow::Context;
4use chrono::{DateTime, Local, Utc};
5use std::io::{BufRead, BufReader, IsTerminal};
6use std::process::{Command, Stdio};
7use std::sync::Arc;
8
9/// Determine if stdout is going to terminal
10pub fn is_terminal() -> bool {
11    std::io::stdout().is_terminal()
12}
13
14/// Gets the current time on the system in readable format
15pub fn get_timestamp() -> String {
16    Local::now().format("%Y-%m-%dT%H:%M:%S").to_string()
17}
18
19/// Lists currently running docker containers
20pub fn list_containers() -> anyhow::Result<Vec<String>> {
21    if is_terminal() {
22        color_println(Color::Magenta, "Listing docker containers...");
23    }
24
25    // Use docker to list container_ids
26    let container_ids = Command::new(DOCKER)
27        .args(["ps", "-q"])
28        .output()
29        .context("Failed to list docker containers")?;
30
31    // Turn Output into String
32    let container_id_list = String::from_utf8(container_ids.stdout)
33        .context("Failed to create string of container id's")?;
34
35    // Parse/sanitize container ids and collecto into Vec
36    let ids = container_id_list
37        .split_whitespace()
38        .map(String::from)
39        .collect::<Vec<String>>();
40
41    Ok(ids)
42}
43
44/// Force removes all docker containers provided in argument
45pub fn kill_containers(container_ids: Vec<String>) -> anyhow::Result<()> {
46    if is_terminal() {
47        color_println(Color::Yellow, "Killing docker containers...");
48    } else {
49        println!("Killing docker containers...")
50    }
51
52    Command::new(DOCKER)
53        .args(["rm", "-f"])
54        .args(&container_ids)
55        .status()
56        .context("Failed to remove containers")?;
57
58    Ok(())
59}
60
61/// Gets container names from a given stack
62pub fn get_containers_from_stack(stack: &str) -> anyhow::Result<Vec<String>> {
63    let output = Command::new(DOCKER)
64        .args([
65            "ps",
66            "-q",
67            "--filter",
68            &format!("label=com.docker.compose.project={}", &stack),
69        ])
70        .output()
71        .context(format!("Failed to containers in stack: {}", &stack))?;
72
73    let container_ids =
74        String::from_utf8(output.stdout).expect("Failed to parse container name from output");
75
76    let container_ids_vec = container_ids.split_whitespace().map(String::from);
77
78    let containers = container_ids_vec
79        .filter_map(|id| get_container_name(&id).ok())
80        .collect();
81
82    Ok(containers)
83}
84
85/// Gets the name of a docker container by the container_id passed as argument
86pub fn get_container_name(container_id: &str) -> anyhow::Result<String> {
87    // get container name by referencing id
88    let output = Command::new(DOCKER)
89        .args(["inspect", "--format", "{{.Name}}", container_id])
90        .output()
91        .context("Failed to inspect container")?;
92
93    // parse output into clean String
94    let name = String::from_utf8(output.stdout)
95        .context("Failed to parse container name from output")?
96        .trim()
97        .trim_start_matches('/') // Docker names start with '/'
98        .to_string();
99
100    Ok(name)
101}
102
103/// Updates a container by the container_name provided as argument
104pub fn update_container_by_name(container_name: &str) -> anyhow::Result<u8> {
105    let mut is_updated: u8 = 0;
106    // get container image string by referenciing the container_name
107    let image_output = Command::new(DOCKER)
108        .args(["inspect", "--format", "{{.Config.Image}}", container_name])
109        .output()
110        .context("Failed to inspect container")?;
111
112    // parse output into clean String
113    let image_name = String::from_utf8(image_output.stdout)
114        .context("Failed to parse image name from output")?
115        .trim()
116        .to_string();
117
118    if is_terminal() {
119        color_println(
120            Color::Cyan,
121            &format!("Pulling image for {}: {}", &container_name, &image_name),
122        );
123    } else {
124        println!("Pulling image for {}: {}", &container_name, &image_name)
125    }
126
127    // pull new image for container
128    let mut logs_process = Command::new(DOCKER)
129        .args(["pull", &image_name])
130        .stdout(Stdio::piped())
131        .spawn()
132        .context(format!("Failed to pull image: {}", &image_name))?;
133
134    if let Some(stdout) = logs_process.stdout.take() {
135        let reader = BufReader::new(stdout);
136        for line in reader.lines().map_while(Result::ok) {
137            println!("{line}");
138            if line.contains("Status: Downloaded newer image") {
139                is_updated = 1
140            }
141        }
142    }
143
144    let _ = logs_process.kill();
145    let _ = logs_process.wait();
146
147    Ok(is_updated)
148}
149
150/// Spawns threads to handle container logs
151pub fn spawn_container_logger(
152    container: &str,
153    is_container_id: bool,
154    use_color: bool,
155    tail: u32,
156    tx: std::sync::mpsc::Sender<String>,
157) -> anyhow::Result<std::thread::JoinHandle<()>> {
158    let container_identifier = Arc::new(container.to_string());
159
160    let handle = std::thread::spawn(move || {
161        let container_name = if is_container_id {
162            match get_container_name(&container_identifier) {
163                Ok(name) => Arc::new(name),
164                Err(_) => Arc::clone(&container_identifier),
165            }
166        } else {
167            Arc::clone(&container_identifier)
168        };
169
170        let mut logs_process = match Command::new(DOCKER)
171            .args([
172                "logs",
173                &container_name,
174                "--tail",
175                &tail.to_string(),
176                "--follow",
177            ])
178            .stdout(Stdio::piped())
179            .stderr(Stdio::piped())
180            .spawn()
181        {
182            Ok(proc) => proc,
183            Err(_) => {
184                let _ = tx.send(if use_color {
185                    color_println_fmt(
186                        Color::Red,
187                        &format!("[ERROR] - Failed to log {container_name}"),
188                    )
189                } else {
190                    format!("[ERROR] - Failed to log {container_name}")
191                });
192                return;
193            }
194        };
195
196        let mut handles: Vec<std::thread::JoinHandle<()>> = vec![];
197
198        // handle stdout
199        if let Some(stdout) = logs_process.stdout.take() {
200            let tx_stdout = tx.clone();
201            let container_name_stdout = Arc::clone(&container_name);
202            let handle_stdout = std::thread::spawn(move || {
203                let reader = BufReader::new(stdout);
204                for line in reader.lines().map_while(Result::ok) {
205                    if tx_stdout
206                        .send(if use_color {
207                            format!(
208                                "[{} | {}] {}",
209                                color_println_fmt(Color::Cyan, &get_timestamp()),
210                                color_println_fmt(Color::Green, &container_name_stdout),
211                                line
212                            )
213                        } else {
214                            format!(
215                                "[{} | {}] {}",
216                                &get_timestamp(),
217                                &container_name_stdout,
218                                line
219                            )
220                        })
221                        .is_err()
222                    {
223                        break; // Receiver closed
224                    }
225                }
226            });
227
228            handles.push(handle_stdout);
229        }
230
231        // handle stderr
232        if let Some(stderr) = logs_process.stderr.take() {
233            let tx_stderr = tx.clone();
234            let container_name_stderr = Arc::clone(&container_name);
235            let handle_stderr = std::thread::spawn(move || {
236                let reader = BufReader::new(stderr);
237                for line in reader.lines().map_while(Result::ok) {
238                    if tx_stderr
239                        .send(if use_color {
240                            format!(
241                                "[{} | {}] {}",
242                                color_println_fmt(Color::Cyan, &get_timestamp()),
243                                color_println_fmt(Color::Green, &container_name_stderr),
244                                line
245                            )
246                        } else {
247                            format!(
248                                "[{} | {}] {}",
249                                &get_timestamp(),
250                                &container_name_stderr,
251                                line
252                            )
253                        })
254                        .is_err()
255                    {
256                        break; // Receiver closed
257                    }
258                }
259            });
260
261            handles.push(handle_stderr);
262        }
263
264        for handle in handles {
265            let _ = handle.join();
266        }
267
268        let _ = logs_process.kill();
269        let _ = logs_process.wait();
270    });
271
272    Ok(handle)
273}
274
275/// Shape of stats data
276#[derive(Debug, Clone)]
277pub struct StatsData {
278    pub container_name: String,
279    pub cpu: String,
280    pub memory: String,
281}
282
283/// Parse stats data
284pub fn parse_stats_data(stats: &str) -> anyhow::Result<StatsData> {
285    let parsed = stats
286        .trim_start_matches("/")
287        .split_whitespace()
288        .collect::<Vec<&str>>();
289
290    Ok(StatsData {
291        container_name: parsed[0].to_string(),
292        cpu: parsed[1].to_string(),
293        memory: parsed[2].to_string(),
294    })
295}
296
297/// Shape of inspected data
298#[derive(Debug, Clone)]
299pub struct InspectData {
300    pub container_name: String,
301    pub status: String,
302    pub restart_policy: String,
303    pub health: String,
304    pub uptime: String,
305    pub ports: String,
306}
307
308/// Parses inspected data
309pub fn parse_inspect_data(stats: &str) -> anyhow::Result<InspectData> {
310    let parsed = stats
311        .trim_start_matches("/")
312        .split(",")
313        .collect::<Vec<&str>>();
314
315    Ok(InspectData {
316        container_name: parsed[0].to_string(),
317        status: parsed[1].to_string(),
318        restart_policy: parsed[2].to_string(),
319        health: parsed[3].to_string(),
320        uptime: calc_uptime(parsed[4])?,
321        ports: parsed[5].to_string(),
322    })
323}
324
325/// Calculate uptime for a container
326fn calc_uptime(start_time: &str) -> anyhow::Result<String> {
327    let start_time =
328        DateTime::parse_from_rfc3339(start_time).context("Failed to parse start_time")?;
329    let now = Utc::now();
330    let duration = now.signed_duration_since(start_time.with_timezone(&Utc));
331
332    let days = duration.num_days();
333    let hours = duration.num_hours() % 24;
334    let minutes = duration.num_minutes() % 60;
335
336    let uptime = if days > 0 {
337        format!("{days}D {hours}H {minutes}m")
338    } else if hours > 0 {
339        format!("{hours}H {minutes}m")
340    } else {
341        format!("{minutes}m")
342    };
343
344    Ok(uptime)
345}