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;
56use tracing::{debug, error, instrument, trace, warn};
57
58pub mod attach;
60pub mod bake;
61pub mod build;
62pub mod builder;
63pub mod commit;
64#[cfg(feature = "compose")]
65pub mod compose;
66pub mod container_prune;
67pub mod context;
68pub mod cp;
69pub mod create;
70pub mod diff;
71pub mod events;
72pub mod exec;
73pub mod export;
74pub mod generic;
75pub mod history;
76pub mod image_prune;
77pub mod images;
78pub mod import;
79pub mod info;
80pub mod init;
81pub mod inspect;
82pub mod kill;
83pub mod load;
84pub mod login;
85pub mod logout;
86pub mod logs;
87#[cfg(feature = "manifest")]
88pub mod manifest;
89pub mod network;
90pub mod pause;
91pub mod port;
92pub mod ps;
93pub mod pull;
94pub mod push;
95pub mod rename;
96pub mod restart;
97pub mod rm;
98pub mod rmi;
99pub mod run;
100pub mod save;
101pub mod search;
102pub mod start;
103pub mod stats;
104pub mod stop;
105#[cfg(feature = "swarm")]
106pub mod swarm;
107pub mod system;
108pub mod tag;
109pub mod top;
110pub mod unpause;
111pub mod update;
112pub mod version;
113pub mod volume;
114pub mod wait;
115
116#[async_trait]
118pub trait DockerCommand {
119 type Output;
121
122 fn get_executor(&self) -> &CommandExecutor;
124
125 fn get_executor_mut(&mut self) -> &mut CommandExecutor;
127
128 fn build_command_args(&self) -> Vec<String>;
130
131 async fn execute(&self) -> Result<Self::Output>;
133
134 async fn execute_command(&self, command_args: Vec<String>) -> Result<CommandOutput> {
136 let executor = self.get_executor();
137
138 if command_args.first() == Some(&"compose".to_string()) {
141 executor.execute_command("docker", command_args).await
143 } else {
144 let command_name = command_args
146 .first()
147 .unwrap_or(&"docker".to_string())
148 .clone();
149 let remaining_args = command_args.iter().skip(1).cloned().collect();
150 executor
151 .execute_command(&command_name, remaining_args)
152 .await
153 }
154 }
155
156 fn arg<S: AsRef<OsStr>>(&mut self, arg: S) -> &mut Self {
158 self.get_executor_mut().add_arg(arg);
159 self
160 }
161
162 fn args<I, S>(&mut self, args: I) -> &mut Self
164 where
165 I: IntoIterator<Item = S>,
166 S: AsRef<OsStr>,
167 {
168 self.get_executor_mut().add_args(args);
169 self
170 }
171
172 fn flag(&mut self, flag: &str) -> &mut Self {
174 self.get_executor_mut().add_flag(flag);
175 self
176 }
177
178 fn option(&mut self, key: &str, value: &str) -> &mut Self {
180 self.get_executor_mut().add_option(key, value);
181 self
182 }
183
184 fn with_timeout(&mut self, timeout: std::time::Duration) -> &mut Self {
189 self.get_executor_mut().timeout = Some(timeout);
190 self
191 }
192
193 fn with_timeout_secs(&mut self, seconds: u64) -> &mut Self {
195 self.get_executor_mut().timeout = Some(std::time::Duration::from_secs(seconds));
196 self
197 }
198}
199
200#[derive(Debug, Clone, Default)]
202pub struct ComposeConfig {
203 pub files: Vec<PathBuf>,
205 pub project_name: Option<String>,
207 pub project_directory: Option<PathBuf>,
209 pub profiles: Vec<String>,
211 pub env_file: Option<PathBuf>,
213 pub compatibility: bool,
215 pub dry_run: bool,
217 pub progress: Option<ProgressType>,
219 pub ansi: Option<AnsiMode>,
221 pub parallel: Option<i32>,
223}
224
225#[derive(Debug, Clone, Copy)]
227pub enum ProgressType {
228 Auto,
230 Tty,
232 Plain,
234 Json,
236 Quiet,
238}
239
240impl std::fmt::Display for ProgressType {
241 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
242 match self {
243 Self::Auto => write!(f, "auto"),
244 Self::Tty => write!(f, "tty"),
245 Self::Plain => write!(f, "plain"),
246 Self::Json => write!(f, "json"),
247 Self::Quiet => write!(f, "quiet"),
248 }
249 }
250}
251
252#[derive(Debug, Clone, Copy)]
254pub enum AnsiMode {
255 Never,
257 Always,
259 Auto,
261}
262
263impl std::fmt::Display for AnsiMode {
264 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
265 match self {
266 Self::Never => write!(f, "never"),
267 Self::Always => write!(f, "always"),
268 Self::Auto => write!(f, "auto"),
269 }
270 }
271}
272
273impl ComposeConfig {
274 #[must_use]
276 pub fn new() -> Self {
277 Self::default()
278 }
279
280 #[must_use]
282 pub fn file(mut self, path: impl Into<PathBuf>) -> Self {
283 self.files.push(path.into());
284 self
285 }
286
287 #[must_use]
289 pub fn project_name(mut self, name: impl Into<String>) -> Self {
290 self.project_name = Some(name.into());
291 self
292 }
293
294 #[must_use]
296 pub fn project_directory(mut self, dir: impl Into<PathBuf>) -> Self {
297 self.project_directory = Some(dir.into());
298 self
299 }
300
301 #[must_use]
303 pub fn profile(mut self, profile: impl Into<String>) -> Self {
304 self.profiles.push(profile.into());
305 self
306 }
307
308 #[must_use]
310 pub fn env_file(mut self, path: impl Into<PathBuf>) -> Self {
311 self.env_file = Some(path.into());
312 self
313 }
314
315 #[must_use]
317 pub fn compatibility(mut self) -> Self {
318 self.compatibility = true;
319 self
320 }
321
322 #[must_use]
324 pub fn dry_run(mut self) -> Self {
325 self.dry_run = true;
326 self
327 }
328
329 #[must_use]
331 pub fn progress(mut self, progress: ProgressType) -> Self {
332 self.progress = Some(progress);
333 self
334 }
335
336 #[must_use]
338 pub fn ansi(mut self, ansi: AnsiMode) -> Self {
339 self.ansi = Some(ansi);
340 self
341 }
342
343 #[must_use]
345 pub fn parallel(mut self, parallel: i32) -> Self {
346 self.parallel = Some(parallel);
347 self
348 }
349
350 #[must_use]
352 pub fn build_global_args(&self) -> Vec<String> {
353 let mut args = Vec::new();
354
355 for file in &self.files {
357 args.push("--file".to_string());
358 args.push(file.to_string_lossy().to_string());
359 }
360
361 if let Some(ref name) = self.project_name {
363 args.push("--project-name".to_string());
364 args.push(name.clone());
365 }
366
367 if let Some(ref dir) = self.project_directory {
369 args.push("--project-directory".to_string());
370 args.push(dir.to_string_lossy().to_string());
371 }
372
373 for profile in &self.profiles {
375 args.push("--profile".to_string());
376 args.push(profile.clone());
377 }
378
379 if let Some(ref env_file) = self.env_file {
381 args.push("--env-file".to_string());
382 args.push(env_file.to_string_lossy().to_string());
383 }
384
385 if self.compatibility {
387 args.push("--compatibility".to_string());
388 }
389
390 if self.dry_run {
391 args.push("--dry-run".to_string());
392 }
393
394 if let Some(progress) = self.progress {
396 args.push("--progress".to_string());
397 args.push(progress.to_string());
398 }
399
400 if let Some(ansi) = self.ansi {
402 args.push("--ansi".to_string());
403 args.push(ansi.to_string());
404 }
405
406 if let Some(parallel) = self.parallel {
408 args.push("--parallel".to_string());
409 args.push(parallel.to_string());
410 }
411
412 args
413 }
414}
415
416pub trait ComposeCommand: DockerCommand {
418 fn get_config(&self) -> &ComposeConfig;
420
421 fn get_config_mut(&mut self) -> &mut ComposeConfig;
423
424 fn subcommand(&self) -> &'static str;
426
427 fn build_subcommand_args(&self) -> Vec<String>;
429
430 fn build_command_args(&self) -> Vec<String> {
433 let mut args = vec!["compose".to_string()];
434
435 args.extend(self.get_config().build_global_args());
437
438 args.push(self.subcommand().to_string());
440
441 args.extend(self.build_subcommand_args());
443
444 args.extend(self.get_executor().raw_args.clone());
446
447 args
448 }
449
450 #[must_use]
452 fn file<P: Into<PathBuf>>(mut self, file: P) -> Self
453 where
454 Self: Sized,
455 {
456 self.get_config_mut().files.push(file.into());
457 self
458 }
459
460 #[must_use]
462 fn project_name(mut self, name: impl Into<String>) -> Self
463 where
464 Self: Sized,
465 {
466 self.get_config_mut().project_name = Some(name.into());
467 self
468 }
469}
470
471pub const DEFAULT_COMMAND_TIMEOUT: Duration = Duration::from_secs(30);
473
474#[derive(Debug, Clone)]
476pub struct CommandExecutor {
477 pub raw_args: Vec<String>,
479 pub platform_info: Option<PlatformInfo>,
481 pub timeout: Option<Duration>,
483}
484
485impl CommandExecutor {
486 #[must_use]
488 pub fn new() -> Self {
489 Self {
490 raw_args: Vec::new(),
491 platform_info: None,
492 timeout: None,
493 }
494 }
495
496 pub fn with_platform() -> Result<Self> {
502 let platform_info = PlatformInfo::detect()?;
503 Ok(Self {
504 raw_args: Vec::new(),
505 platform_info: Some(platform_info),
506 timeout: None,
507 })
508 }
509
510 #[must_use]
512 pub fn platform(mut self, platform_info: PlatformInfo) -> Self {
513 self.platform_info = Some(platform_info);
514 self
515 }
516
517 #[must_use]
522 pub fn timeout(mut self, timeout: Duration) -> Self {
523 self.timeout = Some(timeout);
524 self
525 }
526
527 #[must_use]
529 pub fn timeout_secs(mut self, seconds: u64) -> Self {
530 self.timeout = Some(Duration::from_secs(seconds));
531 self
532 }
533
534 fn get_runtime_command(&self) -> String {
536 if let Some(ref platform_info) = self.platform_info {
537 platform_info.runtime.command().to_string()
538 } else {
539 "docker".to_string()
540 }
541 }
542
543 #[instrument(
549 name = "docker.command",
550 skip(self, args),
551 fields(
552 command = %command_name,
553 runtime = %self.get_runtime_command(),
554 timeout_secs = self.timeout.map(|t| t.as_secs()),
555 )
556 )]
557 pub async fn execute_command(
558 &self,
559 command_name: &str,
560 args: Vec<String>,
561 ) -> Result<CommandOutput> {
562 let mut all_args = self.raw_args.clone();
564 all_args.extend(args);
565
566 all_args.insert(0, command_name.to_string());
568
569 let runtime_command = self.get_runtime_command();
570
571 trace!(args = ?all_args, "executing docker command");
572
573 let result = if let Some(timeout_duration) = self.timeout {
575 self.execute_with_timeout(&runtime_command, &all_args, timeout_duration)
576 .await
577 } else {
578 self.execute_internal(&runtime_command, &all_args).await
579 };
580
581 match &result {
582 Ok(output) => {
583 debug!(
584 exit_code = output.exit_code,
585 stdout_len = output.stdout.len(),
586 stderr_len = output.stderr.len(),
587 "command completed successfully"
588 );
589 trace!(stdout = %output.stdout, "command stdout");
590 if !output.stderr.is_empty() {
591 trace!(stderr = %output.stderr, "command stderr");
592 }
593 }
594 Err(e) => {
595 error!(error = %e, "command failed");
596 }
597 }
598
599 result
600 }
601
602 #[instrument(
604 name = "docker.process",
605 skip(self, all_args),
606 fields(
607 full_command = %format!("{} {}", runtime_command, all_args.join(" ")),
608 )
609 )]
610 async fn execute_internal(
611 &self,
612 runtime_command: &str,
613 all_args: &[String],
614 ) -> Result<CommandOutput> {
615 let mut command = TokioCommand::new(runtime_command);
616
617 if let Some(ref platform_info) = self.platform_info {
619 let env_count = platform_info.environment_vars().len();
620 if env_count > 0 {
621 trace!(
622 env_vars = env_count,
623 "setting platform environment variables"
624 );
625 }
626 for (key, value) in platform_info.environment_vars() {
627 command.env(key, value);
628 }
629 }
630
631 trace!("spawning process");
632
633 let output = command
634 .args(all_args)
635 .stdout(Stdio::piped())
636 .stderr(Stdio::piped())
637 .output()
638 .await
639 .map_err(|e| {
640 error!(error = %e, "failed to spawn process");
641 Error::custom(format!(
642 "Failed to execute {runtime_command} {}: {e}",
643 all_args.first().unwrap_or(&String::new())
644 ))
645 })?;
646
647 let stdout = String::from_utf8_lossy(&output.stdout).to_string();
648 let stderr = String::from_utf8_lossy(&output.stderr).to_string();
649 let success = output.status.success();
650 let exit_code = output.status.code().unwrap_or(-1);
651
652 trace!(
653 exit_code = exit_code,
654 success = success,
655 stdout_bytes = output.stdout.len(),
656 stderr_bytes = output.stderr.len(),
657 "process completed"
658 );
659
660 if !success {
661 return Err(Error::command_failed(
662 format!("{} {}", runtime_command, all_args.join(" ")),
663 exit_code,
664 stdout,
665 stderr,
666 ));
667 }
668
669 Ok(CommandOutput {
670 stdout,
671 stderr,
672 exit_code,
673 success,
674 })
675 }
676
677 #[instrument(
679 name = "docker.timeout",
680 skip(self, all_args),
681 fields(timeout_secs = timeout_duration.as_secs())
682 )]
683 async fn execute_with_timeout(
684 &self,
685 runtime_command: &str,
686 all_args: &[String],
687 timeout_duration: Duration,
688 ) -> Result<CommandOutput> {
689 use tokio::time::timeout;
690
691 debug!("executing with timeout");
692
693 if let Ok(result) = timeout(
694 timeout_duration,
695 self.execute_internal(runtime_command, all_args),
696 )
697 .await
698 {
699 result
700 } else {
701 warn!(
702 timeout_secs = timeout_duration.as_secs(),
703 "command timed out"
704 );
705 Err(Error::timeout(timeout_duration.as_secs()))
706 }
707 }
708
709 pub fn add_arg<S: AsRef<OsStr>>(&mut self, arg: S) {
711 self.raw_args
712 .push(arg.as_ref().to_string_lossy().to_string());
713 }
714
715 pub fn add_args<I, S>(&mut self, args: I)
717 where
718 I: IntoIterator<Item = S>,
719 S: AsRef<OsStr>,
720 {
721 for arg in args {
722 self.add_arg(arg);
723 }
724 }
725
726 pub fn add_flag(&mut self, flag: &str) {
728 let flag_arg = if flag.starts_with('-') {
729 flag.to_string()
730 } else if flag.len() == 1 {
731 format!("-{flag}")
732 } else {
733 format!("--{flag}")
734 };
735 self.raw_args.push(flag_arg);
736 }
737
738 pub fn add_option(&mut self, key: &str, value: &str) {
740 let key_arg = if key.starts_with('-') {
741 key.to_string()
742 } else if key.len() == 1 {
743 format!("-{key}")
744 } else {
745 format!("--{key}")
746 };
747 self.raw_args.push(key_arg);
748 self.raw_args.push(value.to_string());
749 }
750}
751
752impl Default for CommandExecutor {
753 fn default() -> Self {
754 Self::new()
755 }
756}
757
758#[derive(Debug, Clone)]
760pub struct CommandOutput {
761 pub stdout: String,
763 pub stderr: String,
765 pub exit_code: i32,
767 pub success: bool,
769}
770
771impl CommandOutput {
772 #[must_use]
774 pub fn stdout_lines(&self) -> Vec<&str> {
775 self.stdout.lines().collect()
776 }
777
778 #[must_use]
780 pub fn stderr_lines(&self) -> Vec<&str> {
781 self.stderr.lines().collect()
782 }
783
784 #[must_use]
786 pub fn stdout_is_empty(&self) -> bool {
787 self.stdout.trim().is_empty()
788 }
789
790 #[must_use]
792 pub fn stderr_is_empty(&self) -> bool {
793 self.stderr.trim().is_empty()
794 }
795}
796
797#[derive(Debug, Clone, Default)]
799pub struct EnvironmentBuilder {
800 vars: HashMap<String, String>,
801}
802
803impl EnvironmentBuilder {
804 #[must_use]
806 pub fn new() -> Self {
807 Self::default()
808 }
809
810 #[must_use]
812 pub fn var(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
813 self.vars.insert(key.into(), value.into());
814 self
815 }
816
817 #[must_use]
819 pub fn vars(mut self, vars: HashMap<String, String>) -> Self {
820 self.vars.extend(vars);
821 self
822 }
823
824 #[must_use]
826 pub fn build_args(&self) -> Vec<String> {
827 let mut args = Vec::new();
828 for (key, value) in &self.vars {
829 args.push("--env".to_string());
830 args.push(format!("{key}={value}"));
831 }
832 args
833 }
834
835 #[must_use]
837 pub fn as_map(&self) -> &HashMap<String, String> {
838 &self.vars
839 }
840}
841
842#[derive(Debug, Clone, Default)]
844pub struct PortBuilder {
845 mappings: Vec<PortMapping>,
846}
847
848impl PortBuilder {
849 #[must_use]
851 pub fn new() -> Self {
852 Self::default()
853 }
854
855 #[must_use]
857 pub fn port(mut self, host_port: u16, container_port: u16) -> Self {
858 self.mappings.push(PortMapping {
859 host_port: Some(host_port),
860 container_port,
861 protocol: Protocol::Tcp,
862 host_ip: None,
863 });
864 self
865 }
866
867 #[must_use]
869 pub fn port_with_protocol(
870 mut self,
871 host_port: u16,
872 container_port: u16,
873 protocol: Protocol,
874 ) -> Self {
875 self.mappings.push(PortMapping {
876 host_port: Some(host_port),
877 container_port,
878 protocol,
879 host_ip: None,
880 });
881 self
882 }
883
884 #[must_use]
886 pub fn dynamic_port(mut self, container_port: u16) -> Self {
887 self.mappings.push(PortMapping {
888 host_port: None,
889 container_port,
890 protocol: Protocol::Tcp,
891 host_ip: None,
892 });
893 self
894 }
895
896 #[must_use]
898 pub fn build_args(&self) -> Vec<String> {
899 let mut args = Vec::new();
900 for mapping in &self.mappings {
901 args.push("--publish".to_string());
902 args.push(mapping.to_string());
903 }
904 args
905 }
906
907 #[must_use]
909 pub fn mappings(&self) -> &[PortMapping] {
910 &self.mappings
911 }
912}
913
914#[derive(Debug, Clone)]
916pub struct PortMapping {
917 pub host_port: Option<u16>,
919 pub container_port: u16,
921 pub protocol: Protocol,
923 pub host_ip: Option<std::net::IpAddr>,
925}
926
927impl std::fmt::Display for PortMapping {
928 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
929 let protocol_suffix = match self.protocol {
930 Protocol::Tcp => "",
931 Protocol::Udp => "/udp",
932 };
933
934 if let Some(host_port) = self.host_port {
935 if let Some(host_ip) = self.host_ip {
936 write!(
937 f,
938 "{}:{}:{}{}",
939 host_ip, host_port, self.container_port, protocol_suffix
940 )
941 } else {
942 write!(
943 f,
944 "{}:{}{}",
945 host_port, self.container_port, protocol_suffix
946 )
947 }
948 } else {
949 write!(f, "{}{}", self.container_port, protocol_suffix)
950 }
951 }
952}
953
954#[derive(Debug, Clone, Copy, PartialEq, Eq)]
956pub enum Protocol {
957 Tcp,
959 Udp,
961}
962
963#[cfg(test)]
964mod tests {
965 use super::*;
966
967 #[test]
968 fn test_command_executor_args() {
969 let mut executor = CommandExecutor::new();
970 executor.add_arg("test");
971 executor.add_args(vec!["arg1", "arg2"]);
972 executor.add_flag("detach");
973 executor.add_flag("d");
974 executor.add_option("name", "test-container");
975
976 assert_eq!(
977 executor.raw_args,
978 vec![
979 "test",
980 "arg1",
981 "arg2",
982 "--detach",
983 "-d",
984 "--name",
985 "test-container"
986 ]
987 );
988 }
989
990 #[test]
991 fn test_command_executor_timeout() {
992 let executor = CommandExecutor::new();
993 assert!(executor.timeout.is_none());
994
995 let executor_with_timeout = CommandExecutor::new().timeout(Duration::from_secs(10));
996 assert_eq!(executor_with_timeout.timeout, Some(Duration::from_secs(10)));
997
998 let executor_with_secs = CommandExecutor::new().timeout_secs(30);
999 assert_eq!(executor_with_secs.timeout, Some(Duration::from_secs(30)));
1000 }
1001
1002 #[test]
1003 fn test_environment_builder() {
1004 let env = EnvironmentBuilder::new()
1005 .var("KEY1", "value1")
1006 .var("KEY2", "value2");
1007
1008 let args = env.build_args();
1009 assert!(args.contains(&"--env".to_string()));
1010 assert!(args.contains(&"KEY1=value1".to_string()));
1011 assert!(args.contains(&"KEY2=value2".to_string()));
1012 }
1013
1014 #[test]
1015 fn test_port_builder() {
1016 let ports = PortBuilder::new()
1017 .port(8080, 80)
1018 .dynamic_port(443)
1019 .port_with_protocol(8081, 81, Protocol::Udp);
1020
1021 let args = ports.build_args();
1022 assert!(args.contains(&"--publish".to_string()));
1023 assert!(args.contains(&"8080:80".to_string()));
1024 assert!(args.contains(&"443".to_string()));
1025 assert!(args.contains(&"8081:81/udp".to_string()));
1026 }
1027
1028 #[test]
1029 fn test_port_mapping_display() {
1030 let tcp_mapping = PortMapping {
1031 host_port: Some(8080),
1032 container_port: 80,
1033 protocol: Protocol::Tcp,
1034 host_ip: None,
1035 };
1036 assert_eq!(tcp_mapping.to_string(), "8080:80");
1037
1038 let udp_mapping = PortMapping {
1039 host_port: Some(8081),
1040 container_port: 81,
1041 protocol: Protocol::Udp,
1042 host_ip: None,
1043 };
1044 assert_eq!(udp_mapping.to_string(), "8081:81/udp");
1045
1046 let dynamic_mapping = PortMapping {
1047 host_port: None,
1048 container_port: 443,
1049 protocol: Protocol::Tcp,
1050 host_ip: None,
1051 };
1052 assert_eq!(dynamic_mapping.to_string(), "443");
1053 }
1054
1055 #[test]
1056 fn test_command_output_helpers() {
1057 let output = CommandOutput {
1058 stdout: "line1\nline2".to_string(),
1059 stderr: "error1\nerror2".to_string(),
1060 exit_code: 0,
1061 success: true,
1062 };
1063
1064 assert_eq!(output.stdout_lines(), vec!["line1", "line2"]);
1065 assert_eq!(output.stderr_lines(), vec!["error1", "error2"]);
1066 assert!(!output.stdout_is_empty());
1067 assert!(!output.stderr_is_empty());
1068
1069 let empty_output = CommandOutput {
1070 stdout: " ".to_string(),
1071 stderr: String::new(),
1072 exit_code: 0,
1073 success: true,
1074 };
1075
1076 assert!(empty_output.stdout_is_empty());
1077 assert!(empty_output.stderr_is_empty());
1078 }
1079}