1use crate::error::{Error, Result};
48use crate::platform::PlatformInfo;
49use async_trait::async_trait;
50use std::collections::HashMap;
51use std::ffi::OsStr;
52use std::path::PathBuf;
53use std::process::Stdio;
54use std::time::Duration;
55use tokio::process::Command as TokioCommand;
56
57pub mod attach;
59pub mod bake;
60pub mod build;
61pub mod builder;
62pub mod commit;
63#[cfg(feature = "compose")]
64pub mod compose;
65pub mod container_prune;
66pub mod context;
67pub mod cp;
68pub mod create;
69pub mod diff;
70pub mod events;
71pub mod exec;
72pub mod export;
73pub mod generic;
74pub mod history;
75pub mod image_prune;
76pub mod images;
77pub mod import;
78pub mod info;
79pub mod init;
80pub mod inspect;
81pub mod kill;
82pub mod load;
83pub mod login;
84pub mod logout;
85pub mod logs;
86#[cfg(feature = "manifest")]
87pub mod manifest;
88pub mod network;
89pub mod pause;
90pub mod port;
91pub mod ps;
92pub mod pull;
93pub mod push;
94pub mod rename;
95pub mod restart;
96pub mod rm;
97pub mod rmi;
98pub mod run;
99pub mod save;
100pub mod search;
101pub mod start;
102pub mod stats;
103pub mod stop;
104#[cfg(feature = "swarm")]
105pub mod swarm;
106pub mod system;
107pub mod tag;
108pub mod top;
109pub mod unpause;
110pub mod update;
111pub mod version;
112pub mod volume;
113pub mod wait;
114
115#[async_trait]
117pub trait DockerCommand {
118 type Output;
120
121 fn get_executor(&self) -> &CommandExecutor;
123
124 fn get_executor_mut(&mut self) -> &mut CommandExecutor;
126
127 fn build_command_args(&self) -> Vec<String>;
129
130 async fn execute(&self) -> Result<Self::Output>;
132
133 async fn execute_command(&self, command_args: Vec<String>) -> Result<CommandOutput> {
135 let executor = self.get_executor();
136
137 if command_args.first() == Some(&"compose".to_string()) {
140 executor.execute_command("docker", command_args).await
142 } else {
143 let command_name = command_args
145 .first()
146 .unwrap_or(&"docker".to_string())
147 .clone();
148 let remaining_args = command_args.iter().skip(1).cloned().collect();
149 executor
150 .execute_command(&command_name, remaining_args)
151 .await
152 }
153 }
154
155 fn arg<S: AsRef<OsStr>>(&mut self, arg: S) -> &mut Self {
157 self.get_executor_mut().add_arg(arg);
158 self
159 }
160
161 fn args<I, S>(&mut self, args: I) -> &mut Self
163 where
164 I: IntoIterator<Item = S>,
165 S: AsRef<OsStr>,
166 {
167 self.get_executor_mut().add_args(args);
168 self
169 }
170
171 fn flag(&mut self, flag: &str) -> &mut Self {
173 self.get_executor_mut().add_flag(flag);
174 self
175 }
176
177 fn option(&mut self, key: &str, value: &str) -> &mut Self {
179 self.get_executor_mut().add_option(key, value);
180 self
181 }
182
183 fn with_timeout(&mut self, timeout: std::time::Duration) -> &mut Self {
188 self.get_executor_mut().timeout = Some(timeout);
189 self
190 }
191
192 fn with_timeout_secs(&mut self, seconds: u64) -> &mut Self {
194 self.get_executor_mut().timeout = Some(std::time::Duration::from_secs(seconds));
195 self
196 }
197}
198
199#[derive(Debug, Clone, Default)]
201pub struct ComposeConfig {
202 pub files: Vec<PathBuf>,
204 pub project_name: Option<String>,
206 pub project_directory: Option<PathBuf>,
208 pub profiles: Vec<String>,
210 pub env_file: Option<PathBuf>,
212 pub compatibility: bool,
214 pub dry_run: bool,
216 pub progress: Option<ProgressType>,
218 pub ansi: Option<AnsiMode>,
220 pub parallel: Option<i32>,
222}
223
224#[derive(Debug, Clone, Copy)]
226pub enum ProgressType {
227 Auto,
229 Tty,
231 Plain,
233 Json,
235 Quiet,
237}
238
239impl std::fmt::Display for ProgressType {
240 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
241 match self {
242 Self::Auto => write!(f, "auto"),
243 Self::Tty => write!(f, "tty"),
244 Self::Plain => write!(f, "plain"),
245 Self::Json => write!(f, "json"),
246 Self::Quiet => write!(f, "quiet"),
247 }
248 }
249}
250
251#[derive(Debug, Clone, Copy)]
253pub enum AnsiMode {
254 Never,
256 Always,
258 Auto,
260}
261
262impl std::fmt::Display for AnsiMode {
263 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
264 match self {
265 Self::Never => write!(f, "never"),
266 Self::Always => write!(f, "always"),
267 Self::Auto => write!(f, "auto"),
268 }
269 }
270}
271
272impl ComposeConfig {
273 #[must_use]
275 pub fn new() -> Self {
276 Self::default()
277 }
278
279 #[must_use]
281 pub fn file(mut self, path: impl Into<PathBuf>) -> Self {
282 self.files.push(path.into());
283 self
284 }
285
286 #[must_use]
288 pub fn project_name(mut self, name: impl Into<String>) -> Self {
289 self.project_name = Some(name.into());
290 self
291 }
292
293 #[must_use]
295 pub fn project_directory(mut self, dir: impl Into<PathBuf>) -> Self {
296 self.project_directory = Some(dir.into());
297 self
298 }
299
300 #[must_use]
302 pub fn profile(mut self, profile: impl Into<String>) -> Self {
303 self.profiles.push(profile.into());
304 self
305 }
306
307 #[must_use]
309 pub fn env_file(mut self, path: impl Into<PathBuf>) -> Self {
310 self.env_file = Some(path.into());
311 self
312 }
313
314 #[must_use]
316 pub fn compatibility(mut self) -> Self {
317 self.compatibility = true;
318 self
319 }
320
321 #[must_use]
323 pub fn dry_run(mut self) -> Self {
324 self.dry_run = true;
325 self
326 }
327
328 #[must_use]
330 pub fn progress(mut self, progress: ProgressType) -> Self {
331 self.progress = Some(progress);
332 self
333 }
334
335 #[must_use]
337 pub fn ansi(mut self, ansi: AnsiMode) -> Self {
338 self.ansi = Some(ansi);
339 self
340 }
341
342 #[must_use]
344 pub fn parallel(mut self, parallel: i32) -> Self {
345 self.parallel = Some(parallel);
346 self
347 }
348
349 #[must_use]
351 pub fn build_global_args(&self) -> Vec<String> {
352 let mut args = Vec::new();
353
354 for file in &self.files {
356 args.push("--file".to_string());
357 args.push(file.to_string_lossy().to_string());
358 }
359
360 if let Some(ref name) = self.project_name {
362 args.push("--project-name".to_string());
363 args.push(name.clone());
364 }
365
366 if let Some(ref dir) = self.project_directory {
368 args.push("--project-directory".to_string());
369 args.push(dir.to_string_lossy().to_string());
370 }
371
372 for profile in &self.profiles {
374 args.push("--profile".to_string());
375 args.push(profile.clone());
376 }
377
378 if let Some(ref env_file) = self.env_file {
380 args.push("--env-file".to_string());
381 args.push(env_file.to_string_lossy().to_string());
382 }
383
384 if self.compatibility {
386 args.push("--compatibility".to_string());
387 }
388
389 if self.dry_run {
390 args.push("--dry-run".to_string());
391 }
392
393 if let Some(progress) = self.progress {
395 args.push("--progress".to_string());
396 args.push(progress.to_string());
397 }
398
399 if let Some(ansi) = self.ansi {
401 args.push("--ansi".to_string());
402 args.push(ansi.to_string());
403 }
404
405 if let Some(parallel) = self.parallel {
407 args.push("--parallel".to_string());
408 args.push(parallel.to_string());
409 }
410
411 args
412 }
413}
414
415pub trait ComposeCommand: DockerCommand {
417 fn get_config(&self) -> &ComposeConfig;
419
420 fn get_config_mut(&mut self) -> &mut ComposeConfig;
422
423 fn subcommand(&self) -> &'static str;
425
426 fn build_subcommand_args(&self) -> Vec<String>;
428
429 fn build_command_args(&self) -> Vec<String> {
432 let mut args = vec!["compose".to_string()];
433
434 args.extend(self.get_config().build_global_args());
436
437 args.push(self.subcommand().to_string());
439
440 args.extend(self.build_subcommand_args());
442
443 args.extend(self.get_executor().raw_args.clone());
445
446 args
447 }
448
449 #[must_use]
451 fn file<P: Into<PathBuf>>(mut self, file: P) -> Self
452 where
453 Self: Sized,
454 {
455 self.get_config_mut().files.push(file.into());
456 self
457 }
458
459 #[must_use]
461 fn project_name(mut self, name: impl Into<String>) -> Self
462 where
463 Self: Sized,
464 {
465 self.get_config_mut().project_name = Some(name.into());
466 self
467 }
468}
469
470pub const DEFAULT_COMMAND_TIMEOUT: Duration = Duration::from_secs(30);
472
473#[derive(Debug, Clone)]
475pub struct CommandExecutor {
476 pub raw_args: Vec<String>,
478 pub platform_info: Option<PlatformInfo>,
480 pub timeout: Option<Duration>,
482}
483
484impl CommandExecutor {
485 #[must_use]
487 pub fn new() -> Self {
488 Self {
489 raw_args: Vec::new(),
490 platform_info: None,
491 timeout: None,
492 }
493 }
494
495 pub fn with_platform() -> Result<Self> {
501 let platform_info = PlatformInfo::detect()?;
502 Ok(Self {
503 raw_args: Vec::new(),
504 platform_info: Some(platform_info),
505 timeout: None,
506 })
507 }
508
509 #[must_use]
511 pub fn platform(mut self, platform_info: PlatformInfo) -> Self {
512 self.platform_info = Some(platform_info);
513 self
514 }
515
516 #[must_use]
521 pub fn timeout(mut self, timeout: Duration) -> Self {
522 self.timeout = Some(timeout);
523 self
524 }
525
526 #[must_use]
528 pub fn timeout_secs(mut self, seconds: u64) -> Self {
529 self.timeout = Some(Duration::from_secs(seconds));
530 self
531 }
532
533 fn get_runtime_command(&self) -> String {
535 if let Some(ref platform_info) = self.platform_info {
536 platform_info.runtime.command().to_string()
537 } else {
538 "docker".to_string()
539 }
540 }
541
542 pub async fn execute_command(
548 &self,
549 command_name: &str,
550 args: Vec<String>,
551 ) -> Result<CommandOutput> {
552 let mut all_args = self.raw_args.clone();
554 all_args.extend(args);
555
556 all_args.insert(0, command_name.to_string());
558
559 let runtime_command = self.get_runtime_command();
560
561 if let Some(timeout_duration) = self.timeout {
563 self.execute_with_timeout(&runtime_command, &all_args, timeout_duration)
564 .await
565 } else {
566 self.execute_internal(&runtime_command, &all_args).await
567 }
568 }
569
570 async fn execute_internal(
572 &self,
573 runtime_command: &str,
574 all_args: &[String],
575 ) -> Result<CommandOutput> {
576 let mut command = TokioCommand::new(runtime_command);
577
578 if let Some(ref platform_info) = self.platform_info {
580 for (key, value) in platform_info.environment_vars() {
581 command.env(key, value);
582 }
583 }
584
585 let output = command
586 .args(all_args)
587 .stdout(Stdio::piped())
588 .stderr(Stdio::piped())
589 .output()
590 .await
591 .map_err(|e| {
592 Error::custom(format!(
593 "Failed to execute {runtime_command} {}: {e}",
594 all_args.first().unwrap_or(&String::new())
595 ))
596 })?;
597
598 let stdout = String::from_utf8_lossy(&output.stdout).to_string();
599 let stderr = String::from_utf8_lossy(&output.stderr).to_string();
600 let success = output.status.success();
601 let exit_code = output.status.code().unwrap_or(-1);
602
603 if !success {
604 return Err(Error::command_failed(
605 format!("{} {}", runtime_command, all_args.join(" ")),
606 exit_code,
607 stdout,
608 stderr,
609 ));
610 }
611
612 Ok(CommandOutput {
613 stdout,
614 stderr,
615 exit_code,
616 success,
617 })
618 }
619
620 async fn execute_with_timeout(
622 &self,
623 runtime_command: &str,
624 all_args: &[String],
625 timeout_duration: Duration,
626 ) -> Result<CommandOutput> {
627 use tokio::time::timeout;
628
629 match timeout(
630 timeout_duration,
631 self.execute_internal(runtime_command, all_args),
632 )
633 .await
634 {
635 Ok(result) => result,
636 Err(_) => Err(Error::timeout(timeout_duration.as_secs())),
637 }
638 }
639
640 pub fn add_arg<S: AsRef<OsStr>>(&mut self, arg: S) {
642 self.raw_args
643 .push(arg.as_ref().to_string_lossy().to_string());
644 }
645
646 pub fn add_args<I, S>(&mut self, args: I)
648 where
649 I: IntoIterator<Item = S>,
650 S: AsRef<OsStr>,
651 {
652 for arg in args {
653 self.add_arg(arg);
654 }
655 }
656
657 pub fn add_flag(&mut self, flag: &str) {
659 let flag_arg = if flag.starts_with('-') {
660 flag.to_string()
661 } else if flag.len() == 1 {
662 format!("-{flag}")
663 } else {
664 format!("--{flag}")
665 };
666 self.raw_args.push(flag_arg);
667 }
668
669 pub fn add_option(&mut self, key: &str, value: &str) {
671 let key_arg = if key.starts_with('-') {
672 key.to_string()
673 } else if key.len() == 1 {
674 format!("-{key}")
675 } else {
676 format!("--{key}")
677 };
678 self.raw_args.push(key_arg);
679 self.raw_args.push(value.to_string());
680 }
681}
682
683impl Default for CommandExecutor {
684 fn default() -> Self {
685 Self::new()
686 }
687}
688
689#[derive(Debug, Clone)]
691pub struct CommandOutput {
692 pub stdout: String,
694 pub stderr: String,
696 pub exit_code: i32,
698 pub success: bool,
700}
701
702impl CommandOutput {
703 #[must_use]
705 pub fn stdout_lines(&self) -> Vec<&str> {
706 self.stdout.lines().collect()
707 }
708
709 #[must_use]
711 pub fn stderr_lines(&self) -> Vec<&str> {
712 self.stderr.lines().collect()
713 }
714
715 #[must_use]
717 pub fn stdout_is_empty(&self) -> bool {
718 self.stdout.trim().is_empty()
719 }
720
721 #[must_use]
723 pub fn stderr_is_empty(&self) -> bool {
724 self.stderr.trim().is_empty()
725 }
726}
727
728#[derive(Debug, Clone, Default)]
730pub struct EnvironmentBuilder {
731 vars: HashMap<String, String>,
732}
733
734impl EnvironmentBuilder {
735 #[must_use]
737 pub fn new() -> Self {
738 Self::default()
739 }
740
741 #[must_use]
743 pub fn var(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
744 self.vars.insert(key.into(), value.into());
745 self
746 }
747
748 #[must_use]
750 pub fn vars(mut self, vars: HashMap<String, String>) -> Self {
751 self.vars.extend(vars);
752 self
753 }
754
755 #[must_use]
757 pub fn build_args(&self) -> Vec<String> {
758 let mut args = Vec::new();
759 for (key, value) in &self.vars {
760 args.push("--env".to_string());
761 args.push(format!("{key}={value}"));
762 }
763 args
764 }
765
766 #[must_use]
768 pub fn as_map(&self) -> &HashMap<String, String> {
769 &self.vars
770 }
771}
772
773#[derive(Debug, Clone, Default)]
775pub struct PortBuilder {
776 mappings: Vec<PortMapping>,
777}
778
779impl PortBuilder {
780 #[must_use]
782 pub fn new() -> Self {
783 Self::default()
784 }
785
786 #[must_use]
788 pub fn port(mut self, host_port: u16, container_port: u16) -> Self {
789 self.mappings.push(PortMapping {
790 host_port: Some(host_port),
791 container_port,
792 protocol: Protocol::Tcp,
793 host_ip: None,
794 });
795 self
796 }
797
798 #[must_use]
800 pub fn port_with_protocol(
801 mut self,
802 host_port: u16,
803 container_port: u16,
804 protocol: Protocol,
805 ) -> Self {
806 self.mappings.push(PortMapping {
807 host_port: Some(host_port),
808 container_port,
809 protocol,
810 host_ip: None,
811 });
812 self
813 }
814
815 #[must_use]
817 pub fn dynamic_port(mut self, container_port: u16) -> Self {
818 self.mappings.push(PortMapping {
819 host_port: None,
820 container_port,
821 protocol: Protocol::Tcp,
822 host_ip: None,
823 });
824 self
825 }
826
827 #[must_use]
829 pub fn build_args(&self) -> Vec<String> {
830 let mut args = Vec::new();
831 for mapping in &self.mappings {
832 args.push("--publish".to_string());
833 args.push(mapping.to_string());
834 }
835 args
836 }
837
838 #[must_use]
840 pub fn mappings(&self) -> &[PortMapping] {
841 &self.mappings
842 }
843}
844
845#[derive(Debug, Clone)]
847pub struct PortMapping {
848 pub host_port: Option<u16>,
850 pub container_port: u16,
852 pub protocol: Protocol,
854 pub host_ip: Option<std::net::IpAddr>,
856}
857
858impl std::fmt::Display for PortMapping {
859 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
860 let protocol_suffix = match self.protocol {
861 Protocol::Tcp => "",
862 Protocol::Udp => "/udp",
863 };
864
865 if let Some(host_port) = self.host_port {
866 if let Some(host_ip) = self.host_ip {
867 write!(
868 f,
869 "{}:{}:{}{}",
870 host_ip, host_port, self.container_port, protocol_suffix
871 )
872 } else {
873 write!(
874 f,
875 "{}:{}{}",
876 host_port, self.container_port, protocol_suffix
877 )
878 }
879 } else {
880 write!(f, "{}{}", self.container_port, protocol_suffix)
881 }
882 }
883}
884
885#[derive(Debug, Clone, Copy, PartialEq, Eq)]
887pub enum Protocol {
888 Tcp,
890 Udp,
892}
893
894#[cfg(test)]
895mod tests {
896 use super::*;
897
898 #[test]
899 fn test_command_executor_args() {
900 let mut executor = CommandExecutor::new();
901 executor.add_arg("test");
902 executor.add_args(vec!["arg1", "arg2"]);
903 executor.add_flag("detach");
904 executor.add_flag("d");
905 executor.add_option("name", "test-container");
906
907 assert_eq!(
908 executor.raw_args,
909 vec![
910 "test",
911 "arg1",
912 "arg2",
913 "--detach",
914 "-d",
915 "--name",
916 "test-container"
917 ]
918 );
919 }
920
921 #[test]
922 fn test_command_executor_timeout() {
923 let executor = CommandExecutor::new();
924 assert!(executor.timeout.is_none());
925
926 let executor_with_timeout = CommandExecutor::new().timeout(Duration::from_secs(10));
927 assert_eq!(executor_with_timeout.timeout, Some(Duration::from_secs(10)));
928
929 let executor_with_secs = CommandExecutor::new().timeout_secs(30);
930 assert_eq!(executor_with_secs.timeout, Some(Duration::from_secs(30)));
931 }
932
933 #[test]
934 fn test_environment_builder() {
935 let env = EnvironmentBuilder::new()
936 .var("KEY1", "value1")
937 .var("KEY2", "value2");
938
939 let args = env.build_args();
940 assert!(args.contains(&"--env".to_string()));
941 assert!(args.contains(&"KEY1=value1".to_string()));
942 assert!(args.contains(&"KEY2=value2".to_string()));
943 }
944
945 #[test]
946 fn test_port_builder() {
947 let ports = PortBuilder::new()
948 .port(8080, 80)
949 .dynamic_port(443)
950 .port_with_protocol(8081, 81, Protocol::Udp);
951
952 let args = ports.build_args();
953 assert!(args.contains(&"--publish".to_string()));
954 assert!(args.contains(&"8080:80".to_string()));
955 assert!(args.contains(&"443".to_string()));
956 assert!(args.contains(&"8081:81/udp".to_string()));
957 }
958
959 #[test]
960 fn test_port_mapping_display() {
961 let tcp_mapping = PortMapping {
962 host_port: Some(8080),
963 container_port: 80,
964 protocol: Protocol::Tcp,
965 host_ip: None,
966 };
967 assert_eq!(tcp_mapping.to_string(), "8080:80");
968
969 let udp_mapping = PortMapping {
970 host_port: Some(8081),
971 container_port: 81,
972 protocol: Protocol::Udp,
973 host_ip: None,
974 };
975 assert_eq!(udp_mapping.to_string(), "8081:81/udp");
976
977 let dynamic_mapping = PortMapping {
978 host_port: None,
979 container_port: 443,
980 protocol: Protocol::Tcp,
981 host_ip: None,
982 };
983 assert_eq!(dynamic_mapping.to_string(), "443");
984 }
985
986 #[test]
987 fn test_command_output_helpers() {
988 let output = CommandOutput {
989 stdout: "line1\nline2".to_string(),
990 stderr: "error1\nerror2".to_string(),
991 exit_code: 0,
992 success: true,
993 };
994
995 assert_eq!(output.stdout_lines(), vec!["line1", "line2"]);
996 assert_eq!(output.stderr_lines(), vec!["error1", "error2"]);
997 assert!(!output.stdout_is_empty());
998 assert!(!output.stderr_is_empty());
999
1000 let empty_output = CommandOutput {
1001 stdout: " ".to_string(),
1002 stderr: String::new(),
1003 exit_code: 0,
1004 success: true,
1005 };
1006
1007 assert!(empty_output.stdout_is_empty());
1008 assert!(empty_output.stderr_is_empty());
1009 }
1010}