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 context;
41pub mod cp;
42pub mod create;
43pub mod diff;
44pub mod events;
45pub mod exec;
46pub mod export;
47pub mod history;
48pub mod image_prune;
49pub mod images;
50pub mod import;
51pub mod info;
52pub mod init;
53pub mod inspect;
54pub mod kill;
55pub mod load;
56pub mod login;
57pub mod logout;
58pub mod logs;
59pub mod network;
60pub mod pause;
61pub mod port;
62pub mod ps;
63pub mod pull;
64pub mod push;
65pub mod rename;
66pub mod restart;
67pub mod rm;
68pub mod rmi;
69pub mod run;
70pub mod save;
71pub mod search;
72pub mod start;
73pub mod stats;
74pub mod stop;
75pub mod system;
76pub mod tag;
77pub mod top;
78pub mod unpause;
79pub mod update;
80pub mod version;
81pub mod volume;
82pub mod wait;
83
84#[async_trait]
86pub trait DockerCommand {
87 type Output;
89
90 fn get_executor(&self) -> &CommandExecutor;
92
93 fn get_executor_mut(&mut self) -> &mut CommandExecutor;
95
96 fn build_command_args(&self) -> Vec<String>;
98
99 async fn execute(&self) -> Result<Self::Output>;
101
102 async fn execute_command(&self, command_args: Vec<String>) -> Result<CommandOutput> {
104 let executor = self.get_executor();
105
106 if command_args.first() == Some(&"compose".to_string()) {
109 executor.execute_command("docker", command_args).await
111 } else {
112 let command_name = command_args
114 .first()
115 .unwrap_or(&"docker".to_string())
116 .clone();
117 let remaining_args = command_args.iter().skip(1).cloned().collect();
118 executor
119 .execute_command(&command_name, remaining_args)
120 .await
121 }
122 }
123
124 fn arg<S: AsRef<OsStr>>(&mut self, arg: S) -> &mut Self {
126 self.get_executor_mut().add_arg(arg);
127 self
128 }
129
130 fn args<I, S>(&mut self, args: I) -> &mut Self
132 where
133 I: IntoIterator<Item = S>,
134 S: AsRef<OsStr>,
135 {
136 self.get_executor_mut().add_args(args);
137 self
138 }
139
140 fn flag(&mut self, flag: &str) -> &mut Self {
142 self.get_executor_mut().add_flag(flag);
143 self
144 }
145
146 fn option(&mut self, key: &str, value: &str) -> &mut Self {
148 self.get_executor_mut().add_option(key, value);
149 self
150 }
151}
152
153#[derive(Debug, Clone, Default)]
155pub struct ComposeConfig {
156 pub files: Vec<PathBuf>,
158 pub project_name: Option<String>,
160 pub project_directory: Option<PathBuf>,
162 pub profiles: Vec<String>,
164 pub env_file: Option<PathBuf>,
166 pub compatibility: bool,
168 pub dry_run: bool,
170 pub progress: Option<ProgressType>,
172 pub ansi: Option<AnsiMode>,
174 pub parallel: Option<i32>,
176}
177
178#[derive(Debug, Clone, Copy)]
180pub enum ProgressType {
181 Auto,
183 Tty,
185 Plain,
187 Json,
189 Quiet,
191}
192
193impl std::fmt::Display for ProgressType {
194 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
195 match self {
196 Self::Auto => write!(f, "auto"),
197 Self::Tty => write!(f, "tty"),
198 Self::Plain => write!(f, "plain"),
199 Self::Json => write!(f, "json"),
200 Self::Quiet => write!(f, "quiet"),
201 }
202 }
203}
204
205#[derive(Debug, Clone, Copy)]
207pub enum AnsiMode {
208 Never,
210 Always,
212 Auto,
214}
215
216impl std::fmt::Display for AnsiMode {
217 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
218 match self {
219 Self::Never => write!(f, "never"),
220 Self::Always => write!(f, "always"),
221 Self::Auto => write!(f, "auto"),
222 }
223 }
224}
225
226impl ComposeConfig {
227 #[must_use]
229 pub fn new() -> Self {
230 Self::default()
231 }
232
233 #[must_use]
235 pub fn file(mut self, path: impl Into<PathBuf>) -> Self {
236 self.files.push(path.into());
237 self
238 }
239
240 #[must_use]
242 pub fn project_name(mut self, name: impl Into<String>) -> Self {
243 self.project_name = Some(name.into());
244 self
245 }
246
247 #[must_use]
249 pub fn project_directory(mut self, dir: impl Into<PathBuf>) -> Self {
250 self.project_directory = Some(dir.into());
251 self
252 }
253
254 #[must_use]
256 pub fn profile(mut self, profile: impl Into<String>) -> Self {
257 self.profiles.push(profile.into());
258 self
259 }
260
261 #[must_use]
263 pub fn env_file(mut self, path: impl Into<PathBuf>) -> Self {
264 self.env_file = Some(path.into());
265 self
266 }
267
268 #[must_use]
270 pub fn compatibility(mut self) -> Self {
271 self.compatibility = true;
272 self
273 }
274
275 #[must_use]
277 pub fn dry_run(mut self) -> Self {
278 self.dry_run = true;
279 self
280 }
281
282 #[must_use]
284 pub fn progress(mut self, progress: ProgressType) -> Self {
285 self.progress = Some(progress);
286 self
287 }
288
289 #[must_use]
291 pub fn ansi(mut self, ansi: AnsiMode) -> Self {
292 self.ansi = Some(ansi);
293 self
294 }
295
296 #[must_use]
298 pub fn parallel(mut self, parallel: i32) -> Self {
299 self.parallel = Some(parallel);
300 self
301 }
302
303 #[must_use]
305 pub fn build_global_args(&self) -> Vec<String> {
306 let mut args = Vec::new();
307
308 for file in &self.files {
310 args.push("--file".to_string());
311 args.push(file.to_string_lossy().to_string());
312 }
313
314 if let Some(ref name) = self.project_name {
316 args.push("--project-name".to_string());
317 args.push(name.clone());
318 }
319
320 if let Some(ref dir) = self.project_directory {
322 args.push("--project-directory".to_string());
323 args.push(dir.to_string_lossy().to_string());
324 }
325
326 for profile in &self.profiles {
328 args.push("--profile".to_string());
329 args.push(profile.clone());
330 }
331
332 if let Some(ref env_file) = self.env_file {
334 args.push("--env-file".to_string());
335 args.push(env_file.to_string_lossy().to_string());
336 }
337
338 if self.compatibility {
340 args.push("--compatibility".to_string());
341 }
342
343 if self.dry_run {
344 args.push("--dry-run".to_string());
345 }
346
347 if let Some(progress) = self.progress {
349 args.push("--progress".to_string());
350 args.push(progress.to_string());
351 }
352
353 if let Some(ansi) = self.ansi {
355 args.push("--ansi".to_string());
356 args.push(ansi.to_string());
357 }
358
359 if let Some(parallel) = self.parallel {
361 args.push("--parallel".to_string());
362 args.push(parallel.to_string());
363 }
364
365 args
366 }
367}
368
369pub trait ComposeCommand: DockerCommand {
371 fn get_config(&self) -> &ComposeConfig;
373
374 fn get_config_mut(&mut self) -> &mut ComposeConfig;
376
377 fn subcommand(&self) -> &'static str;
379
380 fn build_subcommand_args(&self) -> Vec<String>;
382
383 fn build_command_args(&self) -> Vec<String> {
386 let mut args = vec!["compose".to_string()];
387
388 args.extend(self.get_config().build_global_args());
390
391 args.push(self.subcommand().to_string());
393
394 args.extend(self.build_subcommand_args());
396
397 args.extend(self.get_executor().raw_args.clone());
399
400 args
401 }
402
403 #[must_use]
405 fn file<P: Into<PathBuf>>(mut self, file: P) -> Self
406 where
407 Self: Sized,
408 {
409 self.get_config_mut().files.push(file.into());
410 self
411 }
412
413 #[must_use]
415 fn project_name(mut self, name: impl Into<String>) -> Self
416 where
417 Self: Sized,
418 {
419 self.get_config_mut().project_name = Some(name.into());
420 self
421 }
422}
423
424#[derive(Debug, Clone)]
426pub struct CommandExecutor {
427 pub raw_args: Vec<String>,
429 pub platform_info: Option<PlatformInfo>,
431}
432
433impl CommandExecutor {
434 #[must_use]
436 pub fn new() -> Self {
437 Self {
438 raw_args: Vec::new(),
439 platform_info: None,
440 }
441 }
442
443 pub fn with_platform() -> Result<Self> {
449 let platform_info = PlatformInfo::detect()?;
450 Ok(Self {
451 raw_args: Vec::new(),
452 platform_info: Some(platform_info),
453 })
454 }
455
456 #[must_use]
458 pub fn platform(mut self, platform_info: PlatformInfo) -> Self {
459 self.platform_info = Some(platform_info);
460 self
461 }
462
463 fn get_runtime_command(&self) -> String {
465 if let Some(ref platform_info) = self.platform_info {
466 platform_info.runtime.command().to_string()
467 } else {
468 "docker".to_string()
469 }
470 }
471
472 pub async fn execute_command(
477 &self,
478 command_name: &str,
479 args: Vec<String>,
480 ) -> Result<CommandOutput> {
481 let mut all_args = self.raw_args.clone();
483 all_args.extend(args);
484
485 all_args.insert(0, command_name.to_string());
487
488 let runtime_command = self.get_runtime_command();
489 let mut command = TokioCommand::new(&runtime_command);
490
491 if let Some(ref platform_info) = self.platform_info {
493 for (key, value) in platform_info.environment_vars() {
494 command.env(key, value);
495 }
496 }
497
498 let output = command
499 .args(&all_args)
500 .stdout(Stdio::piped())
501 .stderr(Stdio::piped())
502 .output()
503 .await
504 .map_err(|e| {
505 Error::custom(format!(
506 "Failed to execute {runtime_command} {command_name}: {e}"
507 ))
508 })?;
509
510 let stdout = String::from_utf8_lossy(&output.stdout).to_string();
511 let stderr = String::from_utf8_lossy(&output.stderr).to_string();
512 let success = output.status.success();
513 let exit_code = output.status.code().unwrap_or(-1);
514
515 if !success {
516 return Err(Error::command_failed(
517 format!("{} {}", runtime_command, all_args.join(" ")),
518 exit_code,
519 stdout,
520 stderr,
521 ));
522 }
523
524 Ok(CommandOutput {
525 stdout,
526 stderr,
527 exit_code,
528 success,
529 })
530 }
531
532 pub fn add_arg<S: AsRef<OsStr>>(&mut self, arg: S) {
534 self.raw_args
535 .push(arg.as_ref().to_string_lossy().to_string());
536 }
537
538 pub fn add_args<I, S>(&mut self, args: I)
540 where
541 I: IntoIterator<Item = S>,
542 S: AsRef<OsStr>,
543 {
544 for arg in args {
545 self.add_arg(arg);
546 }
547 }
548
549 pub fn add_flag(&mut self, flag: &str) {
551 let flag_arg = if flag.starts_with('-') {
552 flag.to_string()
553 } else if flag.len() == 1 {
554 format!("-{flag}")
555 } else {
556 format!("--{flag}")
557 };
558 self.raw_args.push(flag_arg);
559 }
560
561 pub fn add_option(&mut self, key: &str, value: &str) {
563 let key_arg = if key.starts_with('-') {
564 key.to_string()
565 } else if key.len() == 1 {
566 format!("-{key}")
567 } else {
568 format!("--{key}")
569 };
570 self.raw_args.push(key_arg);
571 self.raw_args.push(value.to_string());
572 }
573}
574
575impl Default for CommandExecutor {
576 fn default() -> Self {
577 Self::new()
578 }
579}
580
581#[derive(Debug, Clone)]
583pub struct CommandOutput {
584 pub stdout: String,
586 pub stderr: String,
588 pub exit_code: i32,
590 pub success: bool,
592}
593
594impl CommandOutput {
595 #[must_use]
597 pub fn stdout_lines(&self) -> Vec<&str> {
598 self.stdout.lines().collect()
599 }
600
601 #[must_use]
603 pub fn stderr_lines(&self) -> Vec<&str> {
604 self.stderr.lines().collect()
605 }
606
607 #[must_use]
609 pub fn stdout_is_empty(&self) -> bool {
610 self.stdout.trim().is_empty()
611 }
612
613 #[must_use]
615 pub fn stderr_is_empty(&self) -> bool {
616 self.stderr.trim().is_empty()
617 }
618}
619
620#[derive(Debug, Clone, Default)]
622pub struct EnvironmentBuilder {
623 vars: HashMap<String, String>,
624}
625
626impl EnvironmentBuilder {
627 #[must_use]
629 pub fn new() -> Self {
630 Self::default()
631 }
632
633 #[must_use]
635 pub fn var(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
636 self.vars.insert(key.into(), value.into());
637 self
638 }
639
640 #[must_use]
642 pub fn vars(mut self, vars: HashMap<String, String>) -> Self {
643 self.vars.extend(vars);
644 self
645 }
646
647 #[must_use]
649 pub fn build_args(&self) -> Vec<String> {
650 let mut args = Vec::new();
651 for (key, value) in &self.vars {
652 args.push("--env".to_string());
653 args.push(format!("{key}={value}"));
654 }
655 args
656 }
657
658 #[must_use]
660 pub fn as_map(&self) -> &HashMap<String, String> {
661 &self.vars
662 }
663}
664
665#[derive(Debug, Clone, Default)]
667pub struct PortBuilder {
668 mappings: Vec<PortMapping>,
669}
670
671impl PortBuilder {
672 #[must_use]
674 pub fn new() -> Self {
675 Self::default()
676 }
677
678 #[must_use]
680 pub fn port(mut self, host_port: u16, container_port: u16) -> Self {
681 self.mappings.push(PortMapping {
682 host_port: Some(host_port),
683 container_port,
684 protocol: Protocol::Tcp,
685 host_ip: None,
686 });
687 self
688 }
689
690 #[must_use]
692 pub fn port_with_protocol(
693 mut self,
694 host_port: u16,
695 container_port: u16,
696 protocol: Protocol,
697 ) -> Self {
698 self.mappings.push(PortMapping {
699 host_port: Some(host_port),
700 container_port,
701 protocol,
702 host_ip: None,
703 });
704 self
705 }
706
707 #[must_use]
709 pub fn dynamic_port(mut self, container_port: u16) -> Self {
710 self.mappings.push(PortMapping {
711 host_port: None,
712 container_port,
713 protocol: Protocol::Tcp,
714 host_ip: None,
715 });
716 self
717 }
718
719 #[must_use]
721 pub fn build_args(&self) -> Vec<String> {
722 let mut args = Vec::new();
723 for mapping in &self.mappings {
724 args.push("--publish".to_string());
725 args.push(mapping.to_string());
726 }
727 args
728 }
729
730 #[must_use]
732 pub fn mappings(&self) -> &[PortMapping] {
733 &self.mappings
734 }
735}
736
737#[derive(Debug, Clone)]
739pub struct PortMapping {
740 pub host_port: Option<u16>,
742 pub container_port: u16,
744 pub protocol: Protocol,
746 pub host_ip: Option<std::net::IpAddr>,
748}
749
750impl std::fmt::Display for PortMapping {
751 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
752 let protocol_suffix = match self.protocol {
753 Protocol::Tcp => "",
754 Protocol::Udp => "/udp",
755 };
756
757 if let Some(host_port) = self.host_port {
758 if let Some(host_ip) = self.host_ip {
759 write!(
760 f,
761 "{}:{}:{}{}",
762 host_ip, host_port, self.container_port, protocol_suffix
763 )
764 } else {
765 write!(
766 f,
767 "{}:{}{}",
768 host_port, self.container_port, protocol_suffix
769 )
770 }
771 } else {
772 write!(f, "{}{}", self.container_port, protocol_suffix)
773 }
774 }
775}
776
777#[derive(Debug, Clone, Copy, PartialEq, Eq)]
779pub enum Protocol {
780 Tcp,
782 Udp,
784}
785
786#[cfg(test)]
787mod tests {
788 use super::*;
789
790 #[test]
791 fn test_command_executor_args() {
792 let mut executor = CommandExecutor::new();
793 executor.add_arg("test");
794 executor.add_args(vec!["arg1", "arg2"]);
795 executor.add_flag("detach");
796 executor.add_flag("d");
797 executor.add_option("name", "test-container");
798
799 assert_eq!(
800 executor.raw_args,
801 vec![
802 "test",
803 "arg1",
804 "arg2",
805 "--detach",
806 "-d",
807 "--name",
808 "test-container"
809 ]
810 );
811 }
812
813 #[test]
814 fn test_environment_builder() {
815 let env = EnvironmentBuilder::new()
816 .var("KEY1", "value1")
817 .var("KEY2", "value2");
818
819 let args = env.build_args();
820 assert!(args.contains(&"--env".to_string()));
821 assert!(args.contains(&"KEY1=value1".to_string()));
822 assert!(args.contains(&"KEY2=value2".to_string()));
823 }
824
825 #[test]
826 fn test_port_builder() {
827 let ports = PortBuilder::new()
828 .port(8080, 80)
829 .dynamic_port(443)
830 .port_with_protocol(8081, 81, Protocol::Udp);
831
832 let args = ports.build_args();
833 assert!(args.contains(&"--publish".to_string()));
834 assert!(args.contains(&"8080:80".to_string()));
835 assert!(args.contains(&"443".to_string()));
836 assert!(args.contains(&"8081:81/udp".to_string()));
837 }
838
839 #[test]
840 fn test_port_mapping_display() {
841 let tcp_mapping = PortMapping {
842 host_port: Some(8080),
843 container_port: 80,
844 protocol: Protocol::Tcp,
845 host_ip: None,
846 };
847 assert_eq!(tcp_mapping.to_string(), "8080:80");
848
849 let udp_mapping = PortMapping {
850 host_port: Some(8081),
851 container_port: 81,
852 protocol: Protocol::Udp,
853 host_ip: None,
854 };
855 assert_eq!(udp_mapping.to_string(), "8081:81/udp");
856
857 let dynamic_mapping = PortMapping {
858 host_port: None,
859 container_port: 443,
860 protocol: Protocol::Tcp,
861 host_ip: None,
862 };
863 assert_eq!(dynamic_mapping.to_string(), "443");
864 }
865
866 #[test]
867 fn test_command_output_helpers() {
868 let output = CommandOutput {
869 stdout: "line1\nline2".to_string(),
870 stderr: "error1\nerror2".to_string(),
871 exit_code: 0,
872 success: true,
873 };
874
875 assert_eq!(output.stdout_lines(), vec!["line1", "line2"]);
876 assert_eq!(output.stderr_lines(), vec!["error1", "error2"]);
877 assert!(!output.stdout_is_empty());
878 assert!(!output.stderr_is_empty());
879
880 let empty_output = CommandOutput {
881 stdout: " ".to_string(),
882 stderr: String::new(),
883 exit_code: 0,
884 success: true,
885 };
886
887 assert!(empty_output.stdout_is_empty());
888 assert!(empty_output.stderr_is_empty());
889 }
890}