1use crate::error::{Error, Result};
8use crate::platform::PlatformInfo;
9use async_trait::async_trait;
10use std::collections::HashMap;
11use std::ffi::OsStr;
12use std::path::PathBuf;
13use std::process::Stdio;
14use tokio::process::Command as TokioCommand;
15
16pub mod attach;
18pub mod bake;
19pub mod build;
20pub mod builder;
21pub mod commit;
22pub mod compose_attach;
23pub mod compose_build;
24pub mod compose_create;
25pub mod compose_down;
26pub mod compose_exec;
27pub mod compose_kill;
28pub mod compose_logs;
29pub mod compose_ls;
30pub mod compose_pause;
31pub mod compose_ps;
32pub mod compose_restart;
33pub mod compose_rm;
34pub mod compose_run;
35pub mod compose_start;
36pub mod compose_stop;
37pub mod compose_unpause;
38pub mod compose_up;
39pub mod container_prune;
40pub mod cp;
41pub mod create;
42pub mod diff;
43pub mod events;
44pub mod exec;
45pub mod export;
46pub mod history;
47pub mod image_prune;
48pub mod images;
49pub mod import;
50pub mod info;
51pub mod inspect;
52pub mod kill;
53pub mod load;
54pub mod login;
55pub mod logout;
56pub mod logs;
57pub mod network;
58pub mod pause;
59pub mod port;
60pub mod ps;
61pub mod pull;
62pub mod push;
63pub mod rename;
64pub mod restart;
65pub mod rm;
66pub mod rmi;
67pub mod run;
68pub mod save;
69pub mod search;
70pub mod start;
71pub mod stats;
72pub mod stop;
73pub mod system;
74pub mod tag;
75pub mod top;
76pub mod unpause;
77pub mod update;
78pub mod version;
79pub mod volume;
80pub mod wait;
81
82#[async_trait]
84pub trait DockerCommand {
85 type Output;
87
88 fn get_executor(&self) -> &CommandExecutor;
90
91 fn get_executor_mut(&mut self) -> &mut CommandExecutor;
93
94 fn build_command_args(&self) -> Vec<String>;
96
97 async fn execute(&self) -> Result<Self::Output>;
99
100 async fn execute_command(&self, command_args: Vec<String>) -> Result<CommandOutput> {
102 let executor = self.get_executor();
103
104 if command_args.first() == Some(&"compose".to_string()) {
107 executor.execute_command("docker", command_args).await
109 } else {
110 let command_name = command_args
112 .first()
113 .unwrap_or(&"docker".to_string())
114 .clone();
115 let remaining_args = command_args.iter().skip(1).cloned().collect();
116 executor
117 .execute_command(&command_name, remaining_args)
118 .await
119 }
120 }
121
122 fn arg<S: AsRef<OsStr>>(&mut self, arg: S) -> &mut Self {
124 self.get_executor_mut().add_arg(arg);
125 self
126 }
127
128 fn args<I, S>(&mut self, args: I) -> &mut Self
130 where
131 I: IntoIterator<Item = S>,
132 S: AsRef<OsStr>,
133 {
134 self.get_executor_mut().add_args(args);
135 self
136 }
137
138 fn flag(&mut self, flag: &str) -> &mut Self {
140 self.get_executor_mut().add_flag(flag);
141 self
142 }
143
144 fn option(&mut self, key: &str, value: &str) -> &mut Self {
146 self.get_executor_mut().add_option(key, value);
147 self
148 }
149}
150
151#[derive(Debug, Clone, Default)]
153pub struct ComposeConfig {
154 pub files: Vec<PathBuf>,
156 pub project_name: Option<String>,
158 pub project_directory: Option<PathBuf>,
160 pub profiles: Vec<String>,
162 pub env_file: Option<PathBuf>,
164 pub compatibility: bool,
166 pub dry_run: bool,
168 pub progress: Option<ProgressType>,
170 pub ansi: Option<AnsiMode>,
172 pub parallel: Option<i32>,
174}
175
176#[derive(Debug, Clone, Copy)]
178pub enum ProgressType {
179 Auto,
181 Tty,
183 Plain,
185 Json,
187 Quiet,
189}
190
191impl std::fmt::Display for ProgressType {
192 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
193 match self {
194 Self::Auto => write!(f, "auto"),
195 Self::Tty => write!(f, "tty"),
196 Self::Plain => write!(f, "plain"),
197 Self::Json => write!(f, "json"),
198 Self::Quiet => write!(f, "quiet"),
199 }
200 }
201}
202
203#[derive(Debug, Clone, Copy)]
205pub enum AnsiMode {
206 Never,
208 Always,
210 Auto,
212}
213
214impl std::fmt::Display for AnsiMode {
215 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
216 match self {
217 Self::Never => write!(f, "never"),
218 Self::Always => write!(f, "always"),
219 Self::Auto => write!(f, "auto"),
220 }
221 }
222}
223
224impl ComposeConfig {
225 #[must_use]
227 pub fn new() -> Self {
228 Self::default()
229 }
230
231 #[must_use]
233 pub fn file(mut self, path: impl Into<PathBuf>) -> Self {
234 self.files.push(path.into());
235 self
236 }
237
238 #[must_use]
240 pub fn project_name(mut self, name: impl Into<String>) -> Self {
241 self.project_name = Some(name.into());
242 self
243 }
244
245 #[must_use]
247 pub fn project_directory(mut self, dir: impl Into<PathBuf>) -> Self {
248 self.project_directory = Some(dir.into());
249 self
250 }
251
252 #[must_use]
254 pub fn profile(mut self, profile: impl Into<String>) -> Self {
255 self.profiles.push(profile.into());
256 self
257 }
258
259 #[must_use]
261 pub fn env_file(mut self, path: impl Into<PathBuf>) -> Self {
262 self.env_file = Some(path.into());
263 self
264 }
265
266 #[must_use]
268 pub fn compatibility(mut self) -> Self {
269 self.compatibility = true;
270 self
271 }
272
273 #[must_use]
275 pub fn dry_run(mut self) -> Self {
276 self.dry_run = true;
277 self
278 }
279
280 #[must_use]
282 pub fn progress(mut self, progress: ProgressType) -> Self {
283 self.progress = Some(progress);
284 self
285 }
286
287 #[must_use]
289 pub fn ansi(mut self, ansi: AnsiMode) -> Self {
290 self.ansi = Some(ansi);
291 self
292 }
293
294 #[must_use]
296 pub fn parallel(mut self, parallel: i32) -> Self {
297 self.parallel = Some(parallel);
298 self
299 }
300
301 #[must_use]
303 pub fn build_global_args(&self) -> Vec<String> {
304 let mut args = Vec::new();
305
306 for file in &self.files {
308 args.push("--file".to_string());
309 args.push(file.to_string_lossy().to_string());
310 }
311
312 if let Some(ref name) = self.project_name {
314 args.push("--project-name".to_string());
315 args.push(name.clone());
316 }
317
318 if let Some(ref dir) = self.project_directory {
320 args.push("--project-directory".to_string());
321 args.push(dir.to_string_lossy().to_string());
322 }
323
324 for profile in &self.profiles {
326 args.push("--profile".to_string());
327 args.push(profile.clone());
328 }
329
330 if let Some(ref env_file) = self.env_file {
332 args.push("--env-file".to_string());
333 args.push(env_file.to_string_lossy().to_string());
334 }
335
336 if self.compatibility {
338 args.push("--compatibility".to_string());
339 }
340
341 if self.dry_run {
342 args.push("--dry-run".to_string());
343 }
344
345 if let Some(progress) = self.progress {
347 args.push("--progress".to_string());
348 args.push(progress.to_string());
349 }
350
351 if let Some(ansi) = self.ansi {
353 args.push("--ansi".to_string());
354 args.push(ansi.to_string());
355 }
356
357 if let Some(parallel) = self.parallel {
359 args.push("--parallel".to_string());
360 args.push(parallel.to_string());
361 }
362
363 args
364 }
365}
366
367pub trait ComposeCommand: DockerCommand {
369 fn get_config(&self) -> &ComposeConfig;
371
372 fn get_config_mut(&mut self) -> &mut ComposeConfig;
374
375 fn subcommand(&self) -> &'static str;
377
378 fn build_subcommand_args(&self) -> Vec<String>;
380
381 fn build_command_args(&self) -> Vec<String> {
384 let mut args = vec!["compose".to_string()];
385
386 args.extend(self.get_config().build_global_args());
388
389 args.push(self.subcommand().to_string());
391
392 args.extend(self.build_subcommand_args());
394
395 args.extend(self.get_executor().raw_args.clone());
397
398 args
399 }
400
401 #[must_use]
403 fn file<P: Into<PathBuf>>(mut self, file: P) -> Self
404 where
405 Self: Sized,
406 {
407 self.get_config_mut().files.push(file.into());
408 self
409 }
410
411 #[must_use]
413 fn project_name(mut self, name: impl Into<String>) -> Self
414 where
415 Self: Sized,
416 {
417 self.get_config_mut().project_name = Some(name.into());
418 self
419 }
420}
421
422#[derive(Debug, Clone)]
424pub struct CommandExecutor {
425 pub raw_args: Vec<String>,
427 pub platform_info: Option<PlatformInfo>,
429}
430
431impl CommandExecutor {
432 #[must_use]
434 pub fn new() -> Self {
435 Self {
436 raw_args: Vec::new(),
437 platform_info: None,
438 }
439 }
440
441 pub fn with_platform() -> Result<Self> {
447 let platform_info = PlatformInfo::detect()?;
448 Ok(Self {
449 raw_args: Vec::new(),
450 platform_info: Some(platform_info),
451 })
452 }
453
454 #[must_use]
456 pub fn platform(mut self, platform_info: PlatformInfo) -> Self {
457 self.platform_info = Some(platform_info);
458 self
459 }
460
461 fn get_runtime_command(&self) -> String {
463 if let Some(ref platform_info) = self.platform_info {
464 platform_info.runtime.command().to_string()
465 } else {
466 "docker".to_string()
467 }
468 }
469
470 pub async fn execute_command(
475 &self,
476 command_name: &str,
477 args: Vec<String>,
478 ) -> Result<CommandOutput> {
479 let mut all_args = self.raw_args.clone();
481 all_args.extend(args);
482
483 all_args.insert(0, command_name.to_string());
485
486 let runtime_command = self.get_runtime_command();
487 let mut command = TokioCommand::new(&runtime_command);
488
489 if let Some(ref platform_info) = self.platform_info {
491 for (key, value) in platform_info.environment_vars() {
492 command.env(key, value);
493 }
494 }
495
496 let output = command
497 .args(&all_args)
498 .stdout(Stdio::piped())
499 .stderr(Stdio::piped())
500 .output()
501 .await
502 .map_err(|e| {
503 Error::custom(format!(
504 "Failed to execute {runtime_command} {command_name}: {e}"
505 ))
506 })?;
507
508 let stdout = String::from_utf8_lossy(&output.stdout).to_string();
509 let stderr = String::from_utf8_lossy(&output.stderr).to_string();
510 let success = output.status.success();
511 let exit_code = output.status.code().unwrap_or(-1);
512
513 if !success {
514 return Err(Error::command_failed(
515 format!("{} {}", runtime_command, all_args.join(" ")),
516 exit_code,
517 stdout,
518 stderr,
519 ));
520 }
521
522 Ok(CommandOutput {
523 stdout,
524 stderr,
525 exit_code,
526 success,
527 })
528 }
529
530 pub fn add_arg<S: AsRef<OsStr>>(&mut self, arg: S) {
532 self.raw_args
533 .push(arg.as_ref().to_string_lossy().to_string());
534 }
535
536 pub fn add_args<I, S>(&mut self, args: I)
538 where
539 I: IntoIterator<Item = S>,
540 S: AsRef<OsStr>,
541 {
542 for arg in args {
543 self.add_arg(arg);
544 }
545 }
546
547 pub fn add_flag(&mut self, flag: &str) {
549 let flag_arg = if flag.starts_with('-') {
550 flag.to_string()
551 } else if flag.len() == 1 {
552 format!("-{flag}")
553 } else {
554 format!("--{flag}")
555 };
556 self.raw_args.push(flag_arg);
557 }
558
559 pub fn add_option(&mut self, key: &str, value: &str) {
561 let key_arg = if key.starts_with('-') {
562 key.to_string()
563 } else if key.len() == 1 {
564 format!("-{key}")
565 } else {
566 format!("--{key}")
567 };
568 self.raw_args.push(key_arg);
569 self.raw_args.push(value.to_string());
570 }
571}
572
573impl Default for CommandExecutor {
574 fn default() -> Self {
575 Self::new()
576 }
577}
578
579#[derive(Debug, Clone)]
581pub struct CommandOutput {
582 pub stdout: String,
584 pub stderr: String,
586 pub exit_code: i32,
588 pub success: bool,
590}
591
592impl CommandOutput {
593 #[must_use]
595 pub fn stdout_lines(&self) -> Vec<&str> {
596 self.stdout.lines().collect()
597 }
598
599 #[must_use]
601 pub fn stderr_lines(&self) -> Vec<&str> {
602 self.stderr.lines().collect()
603 }
604
605 #[must_use]
607 pub fn stdout_is_empty(&self) -> bool {
608 self.stdout.trim().is_empty()
609 }
610
611 #[must_use]
613 pub fn stderr_is_empty(&self) -> bool {
614 self.stderr.trim().is_empty()
615 }
616}
617
618#[derive(Debug, Clone, Default)]
620pub struct EnvironmentBuilder {
621 vars: HashMap<String, String>,
622}
623
624impl EnvironmentBuilder {
625 #[must_use]
627 pub fn new() -> Self {
628 Self::default()
629 }
630
631 #[must_use]
633 pub fn var(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
634 self.vars.insert(key.into(), value.into());
635 self
636 }
637
638 #[must_use]
640 pub fn vars(mut self, vars: HashMap<String, String>) -> Self {
641 self.vars.extend(vars);
642 self
643 }
644
645 #[must_use]
647 pub fn build_args(&self) -> Vec<String> {
648 let mut args = Vec::new();
649 for (key, value) in &self.vars {
650 args.push("--env".to_string());
651 args.push(format!("{key}={value}"));
652 }
653 args
654 }
655
656 #[must_use]
658 pub fn as_map(&self) -> &HashMap<String, String> {
659 &self.vars
660 }
661}
662
663#[derive(Debug, Clone, Default)]
665pub struct PortBuilder {
666 mappings: Vec<PortMapping>,
667}
668
669impl PortBuilder {
670 #[must_use]
672 pub fn new() -> Self {
673 Self::default()
674 }
675
676 #[must_use]
678 pub fn port(mut self, host_port: u16, container_port: u16) -> Self {
679 self.mappings.push(PortMapping {
680 host_port: Some(host_port),
681 container_port,
682 protocol: Protocol::Tcp,
683 host_ip: None,
684 });
685 self
686 }
687
688 #[must_use]
690 pub fn port_with_protocol(
691 mut self,
692 host_port: u16,
693 container_port: u16,
694 protocol: Protocol,
695 ) -> Self {
696 self.mappings.push(PortMapping {
697 host_port: Some(host_port),
698 container_port,
699 protocol,
700 host_ip: None,
701 });
702 self
703 }
704
705 #[must_use]
707 pub fn dynamic_port(mut self, container_port: u16) -> Self {
708 self.mappings.push(PortMapping {
709 host_port: None,
710 container_port,
711 protocol: Protocol::Tcp,
712 host_ip: None,
713 });
714 self
715 }
716
717 #[must_use]
719 pub fn build_args(&self) -> Vec<String> {
720 let mut args = Vec::new();
721 for mapping in &self.mappings {
722 args.push("--publish".to_string());
723 args.push(mapping.to_string());
724 }
725 args
726 }
727
728 #[must_use]
730 pub fn mappings(&self) -> &[PortMapping] {
731 &self.mappings
732 }
733}
734
735#[derive(Debug, Clone)]
737pub struct PortMapping {
738 pub host_port: Option<u16>,
740 pub container_port: u16,
742 pub protocol: Protocol,
744 pub host_ip: Option<std::net::IpAddr>,
746}
747
748impl std::fmt::Display for PortMapping {
749 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
750 let protocol_suffix = match self.protocol {
751 Protocol::Tcp => "",
752 Protocol::Udp => "/udp",
753 };
754
755 if let Some(host_port) = self.host_port {
756 if let Some(host_ip) = self.host_ip {
757 write!(
758 f,
759 "{}:{}:{}{}",
760 host_ip, host_port, self.container_port, protocol_suffix
761 )
762 } else {
763 write!(
764 f,
765 "{}:{}{}",
766 host_port, self.container_port, protocol_suffix
767 )
768 }
769 } else {
770 write!(f, "{}{}", self.container_port, protocol_suffix)
771 }
772 }
773}
774
775#[derive(Debug, Clone, Copy, PartialEq, Eq)]
777pub enum Protocol {
778 Tcp,
780 Udp,
782}
783
784#[cfg(test)]
785mod tests {
786 use super::*;
787
788 #[test]
789 fn test_command_executor_args() {
790 let mut executor = CommandExecutor::new();
791 executor.add_arg("test");
792 executor.add_args(vec!["arg1", "arg2"]);
793 executor.add_flag("detach");
794 executor.add_flag("d");
795 executor.add_option("name", "test-container");
796
797 assert_eq!(
798 executor.raw_args,
799 vec![
800 "test",
801 "arg1",
802 "arg2",
803 "--detach",
804 "-d",
805 "--name",
806 "test-container"
807 ]
808 );
809 }
810
811 #[test]
812 fn test_environment_builder() {
813 let env = EnvironmentBuilder::new()
814 .var("KEY1", "value1")
815 .var("KEY2", "value2");
816
817 let args = env.build_args();
818 assert!(args.contains(&"--env".to_string()));
819 assert!(args.contains(&"KEY1=value1".to_string()));
820 assert!(args.contains(&"KEY2=value2".to_string()));
821 }
822
823 #[test]
824 fn test_port_builder() {
825 let ports = PortBuilder::new()
826 .port(8080, 80)
827 .dynamic_port(443)
828 .port_with_protocol(8081, 81, Protocol::Udp);
829
830 let args = ports.build_args();
831 assert!(args.contains(&"--publish".to_string()));
832 assert!(args.contains(&"8080:80".to_string()));
833 assert!(args.contains(&"443".to_string()));
834 assert!(args.contains(&"8081:81/udp".to_string()));
835 }
836
837 #[test]
838 fn test_port_mapping_display() {
839 let tcp_mapping = PortMapping {
840 host_port: Some(8080),
841 container_port: 80,
842 protocol: Protocol::Tcp,
843 host_ip: None,
844 };
845 assert_eq!(tcp_mapping.to_string(), "8080:80");
846
847 let udp_mapping = PortMapping {
848 host_port: Some(8081),
849 container_port: 81,
850 protocol: Protocol::Udp,
851 host_ip: None,
852 };
853 assert_eq!(udp_mapping.to_string(), "8081:81/udp");
854
855 let dynamic_mapping = PortMapping {
856 host_port: None,
857 container_port: 443,
858 protocol: Protocol::Tcp,
859 host_ip: None,
860 };
861 assert_eq!(dynamic_mapping.to_string(), "443");
862 }
863
864 #[test]
865 fn test_command_output_helpers() {
866 let output = CommandOutput {
867 stdout: "line1\nline2".to_string(),
868 stderr: "error1\nerror2".to_string(),
869 exit_code: 0,
870 success: true,
871 };
872
873 assert_eq!(output.stdout_lines(), vec!["line1", "line2"]);
874 assert_eq!(output.stderr_lines(), vec!["error1", "error2"]);
875 assert!(!output.stdout_is_empty());
876 assert!(!output.stderr_is_empty());
877
878 let empty_output = CommandOutput {
879 stdout: " ".to_string(),
880 stderr: String::new(),
881 exit_code: 0,
882 success: true,
883 };
884
885 assert!(empty_output.stdout_is_empty());
886 assert!(empty_output.stderr_is_empty());
887 }
888}