1use crate::printer::{color_println, color_println_fmt, Color};
2use crate::utils::{
3 get_containers_from_stack, get_timestamp, is_terminal, kill_containers, list_containers,
4 parse_inspect_data, parse_stats_data, spawn_container_logger, update_container_by_name,
5 InspectData, StatsData,
6};
7use anyhow::Context;
8use std::collections::hash_map::HashMap;
9use std::io::{self, BufRead, BufReader, Write};
10use std::process::{Command, Stdio};
11
12pub const DOCKER: &str = "docker";
13const DSD: &str = "docker-stack-deploy";
14const PATH_DSD_COMPOSE: &str = "/var/lib/docker-stack-deploy/compose.yml";
15
16pub fn init(project_dir: String, git_url: String) -> anyhow::Result<()> {
18 Command::new(DOCKER)
19 .args(["run", "--rm", "-it"])
20 .args(["-v", "/var/run/docker.sock:/var/run/docker.sock"])
21 .args(["-v", &format!("{project_dir}:{project_dir}")])
22 .args(["ghcr.io/wez/docker-stack-deploy"])
23 .args([DSD, "bootstrap"])
24 .args(["--project-dir", &project_dir])
25 .args(["--git-url", &git_url])
26 .status()
27 .context("Failed to bootstrap docker-stack-deploy")?;
28
29 println!();
30
31 let use_color = is_terminal();
32
33 if use_color {
34 color_println(
35 Color::Green,
36 "Bootstrap success! Following docker-stack-deploy logs...",
37 );
38 } else {
39 println!("Bootstrap success! Following docker-stack-deploy logs...")
40 }
41
42 println!();
43
44 let start_time = std::time::SystemTime::now()
45 .duration_since(std::time::UNIX_EPOCH)
46 .context("Failed to get current time")?
47 .as_secs();
48
49 let mut logs_process = Command::new(DOCKER)
51 .args([
52 "compose",
53 "-f",
54 PATH_DSD_COMPOSE,
55 "logs",
56 "--follow",
57 "--no-log-prefix",
58 "--since",
59 &start_time.to_string(),
60 ])
61 .stdout(Stdio::piped())
62 .spawn()
63 .context("Failed to start following logs")?;
64
65 if let Some(stdout) = logs_process.stdout.take() {
66 let reader = BufReader::new(stdout);
67 for (i, line) in reader.lines().map_while(Result::ok).enumerate() {
68 if use_color {
69 println!(
70 "[{} | {}] {}",
71 color_println_fmt(Color::Cyan, &get_timestamp()),
72 color_println_fmt(Color::Magenta, DSD),
73 line
74 );
75 } else {
76 println!("[{} | {}] {}", &get_timestamp(), DSD, line);
77 }
78 if line.contains("Already up to date") && i > 0 {
79 break;
81 }
82 }
83 }
84
85 let _ = logs_process.kill();
86 let _ = logs_process.wait();
87
88 Ok(())
89}
90
91pub fn logs(
93 containers: Option<Vec<String>>,
94 stacks: Option<Vec<String>>,
95 tail: u32,
96 all: bool,
97) -> anyhow::Result<()> {
98 let use_color = is_terminal();
99
100 let containers = if all {
101 let container_ids = list_containers()?;
102
103 if container_ids.is_empty() {
104 if use_color {
105 color_println(Color::Red, "No containers running");
106 } else {
107 println!("No containers running");
108 }
109 return Ok(());
110 }
111
112 container_ids
113 } else if let Some(containers) = containers {
114 containers
115 } else if let Some(stacks) = stacks {
116 let mut containers = vec![];
117
118 for stack in &stacks {
119 let container_names = get_containers_from_stack(stack)?;
120 containers.extend(container_names);
121 }
122
123 containers
124 } else {
125 anyhow::bail!("Must specify containers, use --stacks (-s) or use --all (-a)")
126 };
127
128 if use_color {
129 color_println(
130 Color::Cyan,
131 &format!("Following logs for container: {}", &containers.len()),
132 );
133 } else {
134 println!("Following logs for container: {}", &containers.len());
135 }
136 let (tx, rx) = std::sync::mpsc::channel::<String>();
137 let mut handles: Vec<std::thread::JoinHandle<()>> = vec![];
138
139 for container in containers {
140 let tx = tx.clone();
141 let is_container_id = all;
142 let handle = spawn_container_logger(&container, is_container_id, use_color, tail, tx)
143 .with_context(|| format!("Failed to spawn container logger for {container}"))?;
144 handles.push(handle);
145 }
146
147 drop(tx);
148
149 for log_line in rx {
150 println!("{log_line}");
151 }
152
153 for handle in handles {
154 let _ = handle.join();
155 }
156
157 Ok(())
158}
159
160pub fn nuke() -> anyhow::Result<()> {
162 color_println(
164 Color::Yellow,
165 "WARNING: All of your containers will be forcefully removed!",
166 );
167 println!(
168 "After removal, {} will be restarted to redeploy all associated containers.\n",
169 color_println_fmt(Color::Magenta, DSD)
170 );
171 print!("Are you sure you want to nuke your docker stacks? [y/N]: ");
172 let _ = io::stdout().flush();
173
174 let mut input = String::new();
176 let _ = io::stdin().read_line(&mut input);
177 let response = input.trim().to_lowercase();
178
179 match response.as_str() {
181 "yes" | "y" => {
182 color_println(Color::Yellow, "Nuking docker containers");
183 }
184 _ => {
185 color_println(Color::Green, "Nuke aborted!");
186 return Ok(());
187 }
188 };
189
190 let container_ids = list_containers()?;
192
193 if container_ids.is_empty() {
195 color_println(Color::Red, "No containers running");
196 return Ok(());
197 } else {
198 kill_containers(container_ids)?
199 }
200
201 color_println(Color::Green, "Running docker-stack-deploy...");
202
203 Command::new(DOCKER)
205 .args(["compose", "-f", PATH_DSD_COMPOSE, "up", "-d"])
206 .status()
207 .context("Failed to start docker-stack-deploy")?;
208
209 color_println(
210 Color::Green,
211 "Following logs until all containers deployed...",
212 );
213
214 let start_time = std::time::SystemTime::now()
215 .duration_since(std::time::UNIX_EPOCH)
216 .context("Failed to get current time")?
217 .as_secs();
218
219 let mut logs_process = Command::new(DOCKER)
221 .args([
222 "compose",
223 "-f",
224 PATH_DSD_COMPOSE,
225 "logs",
226 "--follow",
227 "--no-log-prefix",
228 "--since",
229 &start_time.to_string(),
230 ])
231 .stdout(Stdio::piped())
232 .spawn()
233 .context("Failed to start following logs")?;
234
235 if let Some(stdout) = logs_process.stdout.take() {
236 let reader = BufReader::new(stdout);
237 for (i, line) in reader.lines().map_while(Result::ok).enumerate() {
238 println!(
239 "[{} | {}] {}",
240 color_println_fmt(Color::Cyan, &get_timestamp()),
241 color_println_fmt(Color::Magenta, DSD),
242 line
243 );
244 if line.contains("Already up to date") && i > 0 {
245 break;
247 }
248 }
249 }
250
251 let _ = logs_process.kill();
252 let _ = logs_process.wait();
253
254 Ok(())
255}
256
257pub fn restart(
259 containers: Option<Vec<String>>,
260 stacks: Option<Vec<String>>,
261 all: bool,
262) -> anyhow::Result<()> {
263 let containers = if all {
264 list_containers()?
265 } else if let Some(containers) = containers {
266 containers
267 } else if let Some(stacks) = stacks {
268 let mut containers = vec![];
269
270 for stack in &stacks {
271 let container_names = get_containers_from_stack(stack)?;
272 containers.extend(container_names);
273 }
274
275 containers
276 } else {
277 anyhow::bail!("Must specify containers, use --stacks (-s) or use --all (-a)")
278 };
279
280 let use_color = is_terminal();
281
282 for container in &containers {
283 if use_color {
284 color_println(
285 Color::Cyan,
286 &format!("Restarting container: {}", &container),
287 );
288 } else {
289 println!("Restarting container: {}", &container)
290 }
291
292 Command::new(DOCKER)
293 .args(["restart", container])
294 .status()
295 .context(format!("Failed to restart {}", &container))?;
296 }
297
298 Ok(())
299}
300
301#[derive(Debug, Clone)]
303struct ContainerStats {
304 name: String,
305 status: String,
306 health: String,
307 restart_policy: String,
308 uptime: String,
309 cpu_usage: String,
310 memory_usage: String,
311 ports: String,
312}
313
314pub fn stats(
316 containers: Option<Vec<String>>,
317 stacks: Option<Vec<String>>,
318 all: bool,
319) -> anyhow::Result<()> {
320 let use_color = is_terminal();
321 let containers = if all {
322 let container_ids = list_containers()?;
323
324 if container_ids.is_empty() {
325 if use_color {
326 color_println(Color::Red, "No containers running");
327 } else {
328 println!("No containers running");
329 }
330 return Ok(());
331 }
332
333 container_ids
334 } else if let Some(containers) = containers {
335 containers
336 } else if let Some(stacks) = stacks {
337 let mut containers = vec![];
338
339 for stack in &stacks {
340 let container_names = get_containers_from_stack(stack)?;
341 containers.extend(container_names);
342 }
343
344 containers
345 } else {
346 anyhow::bail!("Must specify containers, use --stacks (-s) or use --all (-a)")
347 };
348
349 let stats_output = Command::new(DOCKER)
351 .args([
352 "stats",
353 "--no-stream",
354 "--format",
355 "table {{.Name}}\t{{.CPUPerc}}\t{{.MemPerc}}",
356 ])
357 .args(&containers)
358 .output()
359 .context("Failed to get stats for containers")?;
360
361 let inspect_format = concat!(
363 "{{.Name}},",
364 "{{.State.Status}},",
365 "{{if .HostConfig.RestartPolicy}}{{if .HostConfig.RestartPolicy.Name}}{{.HostConfig.RestartPolicy.Name}}{{else}}no{{end}}{{else}}no{{end}},",
366 "{{if index .State \"Health\"}}{{.State.Health.Status}}{{else}}N/A{{end}},",
367 "{{.State.StartedAt}},",
368 "{{if .NetworkSettings.Ports}}{{range $key, $value := .NetworkSettings.Ports}}{{$key}}{{if $value}}:{{(index $value 0).HostPort}}{{end}} {{end}}{{else}}N/A{{end}}"
369 );
370
371 let inspect_output = Command::new(DOCKER)
372 .arg("inspect")
373 .args(&containers)
374 .args(["--format", inspect_format])
375 .output()
376 .context("Failed to inspect containers")?;
377
378 let stats_string = String::from_utf8(stats_output.stdout)?;
379 let inspect_string = String::from_utf8(inspect_output.stdout)?;
380
381 let mut temp_stats_map: HashMap<String, StatsData> = HashMap::new();
382 let mut temp_inspect_map: HashMap<String, InspectData> = HashMap::new();
383
384 for line in stats_string.lines().skip(1) {
386 let parsed = parse_stats_data(line)?;
387 temp_stats_map.insert(
388 parsed.container_name.clone(),
389 StatsData {
390 container_name: parsed.container_name,
391 cpu: parsed.cpu,
392 memory: parsed.memory,
393 },
394 );
395 }
396
397 for line in inspect_string.lines() {
398 let parsed = parse_inspect_data(line)?;
399 temp_inspect_map.insert(
400 parsed.container_name.clone(),
401 InspectData {
402 container_name: parsed.container_name,
403 status: parsed.status,
404 restart_policy: parsed.restart_policy,
405 health: parsed.health,
406 uptime: parsed.uptime,
407 ports: parsed.ports,
408 },
409 );
410 }
411
412 assert_eq!(&temp_stats_map.len(), &temp_inspect_map.len());
413
414 let mut total_stats_map: HashMap<String, ContainerStats> = HashMap::new();
415
416 for key in temp_stats_map.keys() {
417 let stats = temp_stats_map
418 .get(key)
419 .with_context(|| format!("Failed to get stats for {key}"))?;
420 let inspect = temp_inspect_map
421 .get(key)
422 .with_context(|| format!("Failed to get stats for {key}"))?;
423
424 let container_stats = if use_color {
425 ContainerStats {
426 name: color_println_fmt(Color::Cyan, &stats.container_name),
427 status: {
428 if &inspect.status.to_lowercase() == "running" {
429 color_println_fmt(Color::Green, &inspect.status)
430 } else if &inspect.status.to_lowercase() == "created" {
431 color_println_fmt(Color::Cyan, &inspect.status)
432 } else if &inspect.status.to_lowercase() == "paused"
433 || &inspect.status.to_lowercase() == "restarting"
434 {
435 color_println_fmt(Color::Yellow, &inspect.status)
436 } else {
437 color_println_fmt(Color::Red, &inspect.status)
438 }
439 },
440 restart_policy: inspect.restart_policy.to_string(),
441 health: {
442 if &inspect.health.to_lowercase() == "healthy" {
443 color_println_fmt(Color::Green, &inspect.health)
444 } else if &inspect.health.to_lowercase() == "unhealthy" {
445 color_println_fmt(Color::Red, &inspect.health)
446 } else if &inspect.health.to_lowercase() == "starting" {
447 color_println_fmt(Color::Cyan, &inspect.health)
448 } else {
449 color_println_fmt(Color::White, &inspect.health)
450 }
451 },
452 uptime: inspect.uptime.to_string(),
453 cpu_usage: stats.cpu.to_string(),
454 memory_usage: stats.memory.to_string(),
455 ports: inspect.ports.to_string(),
456 }
457 } else {
458 ContainerStats {
459 name: stats.container_name.to_string(),
460 status: inspect.status.to_string(),
461 restart_policy: inspect.restart_policy.to_string(),
462 health: inspect.health.to_string(),
463 uptime: inspect.uptime.to_string(),
464 cpu_usage: stats.cpu.to_string(),
465 memory_usage: stats.memory.to_string(),
466 ports: inspect.ports.to_string(),
467 }
468 };
469
470 total_stats_map.insert(key.to_string(), container_stats);
471 }
472 if use_color {
473 println!(
474 "{:<35} {:<20} {:<16} {:<20} {:<18} {:<8} {:<8} {:<20}",
475 &color_println_fmt(Color::White, "NAME"),
476 &color_println_fmt(Color::White, "STATUS"),
477 "RESTART",
478 &color_println_fmt(Color::White, "HEALTH"),
479 "UPTIME",
480 "CPU %",
481 "MEM %",
482 "PORTS"
483 );
484 } else {
485 println!(
486 "{:<35} {:<20} {:<16} {:<20} {:<18} {:<8} {:<8} {:<20}",
487 "NAME", "STATUS", "RESTART", "HEALTH", "UPTIME", "CPU %", "MEM %", "PORTS"
488 );
489 }
490
491 println!();
492
493 for key in total_stats_map.keys() {
495 let container = total_stats_map.get(key).context("Failed to get item")?;
496
497 println!(
498 "{:<35} {:<20} {:<16} {:<20} {:<18} {:<8} {:<8} {:<20}",
499 container.name,
500 container.status,
501 container.restart_policy,
502 container.health,
503 container.uptime,
504 container.cpu_usage,
505 container.memory_usage,
506 container.ports
507 );
508 }
509
510 Ok(())
511}
512
513pub fn update(
515 containers: Option<Vec<String>>,
516 stacks: Option<Vec<String>>,
517 all: bool,
518) -> anyhow::Result<()> {
519 let containers = if all {
520 list_containers()?
521 } else if let Some(containers) = containers {
522 containers
523 } else if let Some(stacks) = stacks {
524 let mut containers = vec![];
525
526 for stack in &stacks {
527 let container_names = get_containers_from_stack(stack)?;
528 containers.extend(container_names);
529 }
530
531 containers
532 } else {
533 anyhow::bail!("Must specify containers, use --stacks (-s) or use --all (-a)")
534 };
535
536 let use_color = is_terminal();
537
538 let mut num_containers_updated = 0;
539
540 for container in &containers {
541 num_containers_updated += update_container_by_name(container)?;
542 }
543
544 if num_containers_updated == 0 {
545 if use_color {
546 color_println(Color::Yellow, "No new container images to update");
547 } else {
548 println!("No new container images to pull");
549 }
550
551 return Ok(());
552 }
553
554 if use_color {
555 println!(
556 "{}: {}",
557 &color_println_fmt(Color::Cyan, "New images pulled"),
558 &color_println_fmt(Color::Green, &num_containers_updated.to_string())
559 );
560 println!();
561 color_println(Color::Green, &format!("Restarting {DSD}"));
562 } else {
563 println!("New images pulled: {num_containers_updated}");
564 println!();
565 println!("Restarting {DSD}");
566 }
567
568 Command::new(DOCKER)
570 .args(["restart", DSD])
571 .status()
572 .context(format!("Failed to restart {DSD}"))?;
573
574 Ok(())
575}