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
9pub fn is_terminal() -> bool {
11 std::io::stdout().is_terminal()
12}
13
14pub fn get_timestamp() -> String {
16 Local::now().format("%Y-%m-%dT%H:%M:%S").to_string()
17}
18
19pub fn list_containers() -> anyhow::Result<Vec<String>> {
21 if is_terminal() {
22 color_println(Color::Magenta, "Listing docker containers...");
23 }
24
25 let container_ids = Command::new(DOCKER)
27 .args(["ps", "-q"])
28 .output()
29 .context("Failed to list docker containers")?;
30
31 let container_id_list = String::from_utf8(container_ids.stdout)
33 .context("Failed to create string of container id's")?;
34
35 let ids = container_id_list
37 .split_whitespace()
38 .map(String::from)
39 .collect::<Vec<String>>();
40
41 Ok(ids)
42}
43
44pub 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
61pub 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
85pub fn get_container_name(container_id: &str) -> anyhow::Result<String> {
87 let output = Command::new(DOCKER)
89 .args(["inspect", "--format", "{{.Name}}", container_id])
90 .output()
91 .context("Failed to inspect container")?;
92
93 let name = String::from_utf8(output.stdout)
95 .context("Failed to parse container name from output")?
96 .trim()
97 .trim_start_matches('/') .to_string();
99
100 Ok(name)
101}
102
103pub fn update_container_by_name(container_name: &str) -> anyhow::Result<u8> {
105 let mut is_updated: u8 = 0;
106 let image_output = Command::new(DOCKER)
108 .args(["inspect", "--format", "{{.Config.Image}}", container_name])
109 .output()
110 .context("Failed to inspect container")?;
111
112 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 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
150pub 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 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; }
225 }
226 });
227
228 handles.push(handle_stdout);
229 }
230
231 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; }
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#[derive(Debug, Clone)]
277pub struct StatsData {
278 pub container_name: String,
279 pub cpu: String,
280 pub memory: String,
281}
282
283pub 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#[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
308pub 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
325fn 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}