1use super::{CommandExecutor, CommandOutput, DockerCommand};
7use crate::error::{Error, Result};
8use async_trait::async_trait;
9use std::ffi::OsStr;
10use std::fmt;
11
12#[derive(Debug, Clone)]
33pub struct InfoCommand {
34 format: Option<String>,
36 executor: CommandExecutor,
38}
39
40#[derive(Debug, Clone, PartialEq)]
42pub struct SystemInfo {
43 pub server_version: String,
45 pub storage_driver: String,
47 pub logging_driver: String,
49 pub cgroup_driver: String,
51 pub cgroup_version: String,
53 pub containers: u32,
55 pub containers_running: u32,
57 pub containers_paused: u32,
59 pub containers_stopped: u32,
61 pub images: u32,
63 pub docker_root_dir: String,
65 pub debug: bool,
67 pub experimental: bool,
69 pub mem_total: u64,
71 pub ncpu: u32,
73 pub operating_system: String,
75 pub os_type: String,
77 pub architecture: String,
79 pub kernel_version: String,
81 pub name: String,
83 pub id: String,
85}
86
87#[derive(Debug, Clone, PartialEq)]
89pub struct RegistryConfig {
90 pub insecure_registries: Vec<String>,
92 pub index_configs: Vec<String>,
94 pub mirrors: Vec<String>,
96}
97
98#[derive(Debug, Clone, PartialEq)]
100pub struct RuntimeInfo {
101 pub default_runtime: String,
103 pub runtimes: Vec<String>,
105}
106
107#[derive(Debug, Clone, PartialEq)]
109pub struct DockerInfo {
110 pub system: SystemInfo,
112 pub registry: Option<RegistryConfig>,
114 pub runtime: Option<RuntimeInfo>,
116 pub warnings: Vec<String>,
118}
119
120#[derive(Debug, Clone)]
125pub struct InfoOutput {
126 pub output: CommandOutput,
128 pub docker_info: Option<DockerInfo>,
130}
131
132impl InfoCommand {
133 #[must_use]
143 pub fn new() -> Self {
144 Self {
145 format: None,
146 executor: CommandExecutor::default(),
147 }
148 }
149
150 #[must_use]
165 pub fn format(mut self, format: impl Into<String>) -> Self {
166 self.format = Some(format.into());
167 self
168 }
169
170 #[must_use]
180 pub fn format_json(self) -> Self {
181 self.format("json")
182 }
183
184 #[must_use]
186 pub fn format_table(self) -> Self {
187 Self {
188 format: None,
189 executor: self.executor,
190 }
191 }
192
193 #[must_use]
199 pub fn executor(mut self, executor: CommandExecutor) -> Self {
200 self.executor = executor;
201 self
202 }
203
204 fn build_command_args(&self) -> Vec<String> {
206 let mut args = vec!["info".to_string()];
207
208 if let Some(ref format) = self.format {
210 args.push("--format".to_string());
211 args.push(format.clone());
212 }
213
214 args
215 }
216
217 fn parse_output(&self, output: &CommandOutput) -> Result<Option<DockerInfo>> {
219 if let Some(ref format) = self.format {
220 if format == "json" {
221 return Self::parse_json_output(output);
222 }
223 }
224
225 Ok(Self::parse_table_output(output))
226 }
227
228 fn parse_json_output(output: &CommandOutput) -> Result<Option<DockerInfo>> {
230 let parsed: serde_json::Value = serde_json::from_str(&output.stdout)
231 .map_err(|e| Error::parse_error(format!("Failed to parse info JSON output: {e}")))?;
232
233 let system = SystemInfo {
234 server_version: parsed["ServerVersion"].as_str().unwrap_or("").to_string(),
235 storage_driver: parsed["Driver"].as_str().unwrap_or("").to_string(),
236 logging_driver: parsed["LoggingDriver"].as_str().unwrap_or("").to_string(),
237 cgroup_driver: parsed["CgroupDriver"].as_str().unwrap_or("").to_string(),
238 cgroup_version: parsed["CgroupVersion"].as_str().unwrap_or("").to_string(),
239 containers: u32::try_from(parsed["Containers"].as_u64().unwrap_or(0)).unwrap_or(0),
240 containers_running: u32::try_from(parsed["ContainersRunning"].as_u64().unwrap_or(0))
241 .unwrap_or(0),
242 containers_paused: u32::try_from(parsed["ContainersPaused"].as_u64().unwrap_or(0))
243 .unwrap_or(0),
244 containers_stopped: u32::try_from(parsed["ContainersStopped"].as_u64().unwrap_or(0))
245 .unwrap_or(0),
246 images: u32::try_from(parsed["Images"].as_u64().unwrap_or(0)).unwrap_or(0),
247 docker_root_dir: parsed["DockerRootDir"].as_str().unwrap_or("").to_string(),
248 debug: parsed["Debug"].as_bool().unwrap_or(false),
249 experimental: parsed["ExperimentalBuild"].as_bool().unwrap_or(false),
250 mem_total: parsed["MemTotal"].as_u64().unwrap_or(0),
251 ncpu: u32::try_from(parsed["NCPU"].as_u64().unwrap_or(0)).unwrap_or(0),
252 operating_system: parsed["OperatingSystem"].as_str().unwrap_or("").to_string(),
253 os_type: parsed["OSType"].as_str().unwrap_or("").to_string(),
254 architecture: parsed["Architecture"].as_str().unwrap_or("").to_string(),
255 kernel_version: parsed["KernelVersion"].as_str().unwrap_or("").to_string(),
256 name: parsed["Name"].as_str().unwrap_or("").to_string(),
257 id: parsed["ID"].as_str().unwrap_or("").to_string(),
258 };
259
260 let registry = parsed.get("RegistryConfig").map(|registry_data| {
262 let insecure_registries = registry_data["InsecureRegistryCIDRs"]
263 .as_array()
264 .map(|arr| {
265 arr.iter()
266 .filter_map(|v| v.as_str())
267 .map(String::from)
268 .collect()
269 })
270 .unwrap_or_default();
271
272 let index_configs = registry_data["IndexConfigs"]
273 .as_object()
274 .map(|obj| obj.keys().map(String::from).collect())
275 .unwrap_or_default();
276
277 let mirrors = registry_data["Mirrors"]
278 .as_array()
279 .map(|arr| {
280 arr.iter()
281 .filter_map(|v| v.as_str())
282 .map(String::from)
283 .collect()
284 })
285 .unwrap_or_default();
286
287 RegistryConfig {
288 insecure_registries,
289 index_configs,
290 mirrors,
291 }
292 });
293
294 let runtime = parsed.get("Runtimes").map(|runtimes_data| {
296 let default_runtime = parsed["DefaultRuntime"].as_str().unwrap_or("").to_string();
297
298 let runtimes = runtimes_data
299 .as_object()
300 .map(|obj| obj.keys().map(String::from).collect())
301 .unwrap_or_default();
302
303 RuntimeInfo {
304 default_runtime,
305 runtimes,
306 }
307 });
308
309 let warnings = parsed["Warnings"]
311 .as_array()
312 .map(|arr| {
313 arr.iter()
314 .filter_map(|v| v.as_str())
315 .map(String::from)
316 .collect()
317 })
318 .unwrap_or_default();
319
320 Ok(Some(DockerInfo {
321 system,
322 registry,
323 runtime,
324 warnings,
325 }))
326 }
327
328 fn parse_table_output(output: &CommandOutput) -> Option<DockerInfo> {
330 let lines: Vec<&str> = output.stdout.lines().collect();
331
332 if lines.is_empty() {
333 return None;
334 }
335
336 let mut data = std::collections::HashMap::new();
337 let mut warnings = Vec::new();
338
339 for line in lines {
340 let trimmed = line.trim();
341
342 if trimmed.is_empty() {
343 continue;
344 }
345
346 if trimmed.starts_with("WARNING:") {
348 warnings.push(trimmed.to_string());
349 continue;
350 }
351
352 if let Some(colon_pos) = trimmed.find(':') {
354 let key = trimmed[..colon_pos].trim();
355 let value = trimmed[colon_pos + 1..].trim();
356 data.insert(key.to_string(), value.to_string());
357 }
358 }
359
360 let system = SystemInfo {
361 server_version: data.get("Server Version").cloned().unwrap_or_default(),
362 storage_driver: data.get("Storage Driver").cloned().unwrap_or_default(),
363 logging_driver: data.get("Logging Driver").cloned().unwrap_or_default(),
364 cgroup_driver: data.get("Cgroup Driver").cloned().unwrap_or_default(),
365 cgroup_version: data.get("Cgroup Version").cloned().unwrap_or_default(),
366 containers: data
367 .get("Containers")
368 .and_then(|s| s.parse().ok())
369 .unwrap_or(0),
370 containers_running: data
371 .get("Running")
372 .and_then(|s| s.parse().ok())
373 .unwrap_or(0),
374 containers_paused: data.get("Paused").and_then(|s| s.parse().ok()).unwrap_or(0),
375 containers_stopped: data
376 .get("Stopped")
377 .and_then(|s| s.parse().ok())
378 .unwrap_or(0),
379 images: data.get("Images").and_then(|s| s.parse().ok()).unwrap_or(0),
380 docker_root_dir: data.get("Docker Root Dir").cloned().unwrap_or_default(),
381 debug: data.get("Debug Mode").is_some_and(|s| s == "true"),
382 experimental: data.get("Experimental").is_some_and(|s| s == "true"),
383 mem_total: data
384 .get("Total Memory")
385 .and_then(|s| s.split_whitespace().next())
386 .and_then(|s| s.parse().ok())
387 .unwrap_or(0),
388 ncpu: data.get("CPUs").and_then(|s| s.parse().ok()).unwrap_or(0),
389 operating_system: data.get("Operating System").cloned().unwrap_or_default(),
390 os_type: data.get("OSType").cloned().unwrap_or_default(),
391 architecture: data.get("Architecture").cloned().unwrap_or_default(),
392 kernel_version: data.get("Kernel Version").cloned().unwrap_or_default(),
393 name: data.get("Name").cloned().unwrap_or_default(),
394 id: data.get("ID").cloned().unwrap_or_default(),
395 };
396
397 Some(DockerInfo {
398 system,
399 registry: None, runtime: None, warnings,
402 })
403 }
404
405 #[must_use]
407 pub fn get_format(&self) -> Option<&str> {
408 self.format.as_deref()
409 }
410}
411
412impl Default for InfoCommand {
413 fn default() -> Self {
414 Self::new()
415 }
416}
417
418impl InfoOutput {
419 #[must_use]
421 pub fn success(&self) -> bool {
422 self.output.success
423 }
424
425 #[must_use]
427 pub fn server_version(&self) -> Option<&str> {
428 self.docker_info
429 .as_ref()
430 .map(|info| info.system.server_version.as_str())
431 }
432
433 #[must_use]
435 pub fn storage_driver(&self) -> Option<&str> {
436 self.docker_info
437 .as_ref()
438 .map(|info| info.system.storage_driver.as_str())
439 }
440
441 #[must_use]
443 pub fn container_count(&self) -> u32 {
444 self.docker_info
445 .as_ref()
446 .map_or(0, |info| info.system.containers)
447 }
448
449 #[must_use]
451 pub fn running_containers(&self) -> u32 {
452 self.docker_info
453 .as_ref()
454 .map_or(0, |info| info.system.containers_running)
455 }
456
457 #[must_use]
459 pub fn image_count(&self) -> u32 {
460 self.docker_info
461 .as_ref()
462 .map_or(0, |info| info.system.images)
463 }
464
465 #[must_use]
467 pub fn is_debug(&self) -> bool {
468 self.docker_info
469 .as_ref()
470 .is_some_and(|info| info.system.debug)
471 }
472
473 #[must_use]
475 pub fn is_experimental(&self) -> bool {
476 self.docker_info
477 .as_ref()
478 .is_some_and(|info| info.system.experimental)
479 }
480
481 #[must_use]
483 pub fn operating_system(&self) -> Option<&str> {
484 self.docker_info
485 .as_ref()
486 .map(|info| info.system.operating_system.as_str())
487 }
488
489 #[must_use]
491 pub fn architecture(&self) -> Option<&str> {
492 self.docker_info
493 .as_ref()
494 .map(|info| info.system.architecture.as_str())
495 }
496
497 #[must_use]
499 pub fn warnings(&self) -> Vec<&str> {
500 self.docker_info
501 .as_ref()
502 .map(|info| info.warnings.iter().map(String::as_str).collect())
503 .unwrap_or_default()
504 }
505
506 #[must_use]
508 pub fn has_warnings(&self) -> bool {
509 self.docker_info
510 .as_ref()
511 .is_some_and(|info| !info.warnings.is_empty())
512 }
513
514 #[must_use]
516 pub fn resource_summary(&self) -> (u32, u32, u32) {
517 if let Some(info) = &self.docker_info {
518 (
519 info.system.containers,
520 info.system.containers_running,
521 info.system.images,
522 )
523 } else {
524 (0, 0, 0)
525 }
526 }
527}
528
529#[async_trait]
530impl DockerCommand for InfoCommand {
531 type Output = InfoOutput;
532
533 fn command_name(&self) -> &'static str {
534 "info"
535 }
536
537 fn build_args(&self) -> Vec<String> {
538 self.build_command_args()
539 }
540
541 async fn execute(&self) -> Result<Self::Output> {
542 let output = self
543 .executor
544 .execute_command(self.command_name(), self.build_args())
545 .await?;
546
547 let docker_info = self.parse_output(&output)?;
548
549 Ok(InfoOutput {
550 output,
551 docker_info,
552 })
553 }
554
555 fn arg<S: AsRef<OsStr>>(&mut self, arg: S) -> &mut Self {
556 self.executor.add_arg(arg);
557 self
558 }
559
560 fn args<I, S>(&mut self, args: I) -> &mut Self
561 where
562 I: IntoIterator<Item = S>,
563 S: AsRef<OsStr>,
564 {
565 self.executor.add_args(args);
566 self
567 }
568
569 fn flag(&mut self, flag: &str) -> &mut Self {
570 self.executor.add_flag(flag);
571 self
572 }
573
574 fn option(&mut self, key: &str, value: &str) -> &mut Self {
575 match key {
576 "--format" | "-f" | "format" => {
577 self.format = Some(value.to_string());
578 }
579 _ => {
580 self.executor.add_option(key, value);
581 }
582 }
583 self
584 }
585}
586
587impl fmt::Display for InfoCommand {
588 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
589 write!(f, "docker info")?;
590
591 if let Some(ref format) = self.format {
592 write!(f, " --format {format}")?;
593 }
594
595 Ok(())
596 }
597}
598
599#[cfg(test)]
600mod tests {
601 use super::*;
602
603 #[test]
604 fn test_info_command_basic() {
605 let info = InfoCommand::new();
606
607 assert_eq!(info.get_format(), None);
608
609 let args = info.build_command_args();
610 assert_eq!(args, vec!["info"]);
611 }
612
613 #[test]
614 fn test_info_command_with_format() {
615 let info = InfoCommand::new().format("{{.ServerVersion}}");
616
617 assert_eq!(info.get_format(), Some("{{.ServerVersion}}"));
618
619 let args = info.build_command_args();
620 assert_eq!(args, vec!["info", "--format", "{{.ServerVersion}}"]);
621 }
622
623 #[test]
624 fn test_info_command_json_format() {
625 let info = InfoCommand::new().format_json();
626
627 assert_eq!(info.get_format(), Some("json"));
628
629 let args = info.build_command_args();
630 assert_eq!(args, vec!["info", "--format", "json"]);
631 }
632
633 #[test]
634 fn test_info_command_table_format() {
635 let info = InfoCommand::new().format_json().format_table();
636
637 assert_eq!(info.get_format(), None);
638
639 let args = info.build_command_args();
640 assert_eq!(args, vec!["info"]);
641 }
642
643 #[test]
644 fn test_info_command_default() {
645 let info = InfoCommand::default();
646
647 assert_eq!(info.get_format(), None);
648 let args = info.build_command_args();
649 assert_eq!(args, vec!["info"]);
650 }
651
652 #[test]
653 fn test_system_info_creation() {
654 let system = SystemInfo {
655 server_version: "20.10.17".to_string(),
656 storage_driver: "overlay2".to_string(),
657 logging_driver: "json-file".to_string(),
658 cgroup_driver: "systemd".to_string(),
659 cgroup_version: "2".to_string(),
660 containers: 10,
661 containers_running: 3,
662 containers_paused: 0,
663 containers_stopped: 7,
664 images: 25,
665 docker_root_dir: "/var/lib/docker".to_string(),
666 debug: false,
667 experimental: false,
668 mem_total: 8_589_934_592,
669 ncpu: 8,
670 operating_system: "Ubuntu 20.04.4 LTS".to_string(),
671 os_type: "linux".to_string(),
672 architecture: "x86_64".to_string(),
673 kernel_version: "5.15.0-56-generic".to_string(),
674 name: "docker-host".to_string(),
675 id: "ABCD:1234:5678:90EF".to_string(),
676 };
677
678 assert_eq!(system.server_version, "20.10.17");
679 assert_eq!(system.storage_driver, "overlay2");
680 assert_eq!(system.containers, 10);
681 assert_eq!(system.containers_running, 3);
682 assert_eq!(system.images, 25);
683 assert!(!system.debug);
684 assert!(!system.experimental);
685 }
686
687 #[test]
688 fn test_registry_config_creation() {
689 let registry = RegistryConfig {
690 insecure_registries: vec!["localhost:5000".to_string()],
691 index_configs: vec!["https://index.docker.io/v1/".to_string()],
692 mirrors: vec!["https://mirror.gcr.io".to_string()],
693 };
694
695 assert_eq!(registry.insecure_registries.len(), 1);
696 assert_eq!(registry.index_configs.len(), 1);
697 assert_eq!(registry.mirrors.len(), 1);
698 }
699
700 #[test]
701 fn test_runtime_info_creation() {
702 let runtime = RuntimeInfo {
703 default_runtime: "runc".to_string(),
704 runtimes: vec!["runc".to_string(), "nvidia".to_string()],
705 };
706
707 assert_eq!(runtime.default_runtime, "runc");
708 assert_eq!(runtime.runtimes.len(), 2);
709 }
710
711 #[test]
712 fn test_docker_info_creation() {
713 let system = SystemInfo {
714 server_version: "20.10.17".to_string(),
715 storage_driver: "overlay2".to_string(),
716 logging_driver: "json-file".to_string(),
717 cgroup_driver: "systemd".to_string(),
718 cgroup_version: "2".to_string(),
719 containers: 5,
720 containers_running: 2,
721 containers_paused: 0,
722 containers_stopped: 3,
723 images: 10,
724 docker_root_dir: "/var/lib/docker".to_string(),
725 debug: true,
726 experimental: true,
727 mem_total: 8_589_934_592,
728 ncpu: 4,
729 operating_system: "Ubuntu 20.04".to_string(),
730 os_type: "linux".to_string(),
731 architecture: "x86_64".to_string(),
732 kernel_version: "5.15.0".to_string(),
733 name: "test-host".to_string(),
734 id: "TEST:1234".to_string(),
735 };
736
737 let docker_info = DockerInfo {
738 system,
739 registry: None,
740 runtime: None,
741 warnings: vec!["Test warning".to_string()],
742 };
743
744 assert_eq!(docker_info.system.server_version, "20.10.17");
745 assert_eq!(docker_info.warnings.len(), 1);
746 assert!(docker_info.registry.is_none());
747 assert!(docker_info.runtime.is_none());
748 }
749
750 #[test]
751 fn test_info_output_helpers() {
752 let system = SystemInfo {
753 server_version: "20.10.17".to_string(),
754 storage_driver: "overlay2".to_string(),
755 logging_driver: "json-file".to_string(),
756 cgroup_driver: "systemd".to_string(),
757 cgroup_version: "2".to_string(),
758 containers: 15,
759 containers_running: 5,
760 containers_paused: 1,
761 containers_stopped: 9,
762 images: 30,
763 docker_root_dir: "/var/lib/docker".to_string(),
764 debug: true,
765 experimental: false,
766 mem_total: 8_589_934_592,
767 ncpu: 8,
768 operating_system: "Ubuntu 22.04 LTS".to_string(),
769 os_type: "linux".to_string(),
770 architecture: "x86_64".to_string(),
771 kernel_version: "5.15.0-56-generic".to_string(),
772 name: "test-docker".to_string(),
773 id: "TEST:ABCD:1234".to_string(),
774 };
775
776 let docker_info = DockerInfo {
777 system,
778 registry: None,
779 runtime: None,
780 warnings: vec![
781 "WARNING: No swap limit support".to_string(),
782 "WARNING: No memory limit support".to_string(),
783 ],
784 };
785
786 let output = InfoOutput {
787 output: CommandOutput {
788 stdout: String::new(),
789 stderr: String::new(),
790 exit_code: 0,
791 success: true,
792 },
793 docker_info: Some(docker_info),
794 };
795
796 assert_eq!(output.server_version(), Some("20.10.17"));
797 assert_eq!(output.storage_driver(), Some("overlay2"));
798 assert_eq!(output.container_count(), 15);
799 assert_eq!(output.running_containers(), 5);
800 assert_eq!(output.image_count(), 30);
801 assert!(output.is_debug());
802 assert!(!output.is_experimental());
803 assert_eq!(output.operating_system(), Some("Ubuntu 22.04 LTS"));
804 assert_eq!(output.architecture(), Some("x86_64"));
805 assert!(output.has_warnings());
806 assert_eq!(output.warnings().len(), 2);
807
808 let (total, running, images) = output.resource_summary();
809 assert_eq!(total, 15);
810 assert_eq!(running, 5);
811 assert_eq!(images, 30);
812 }
813
814 #[test]
815 fn test_info_output_no_data() {
816 let output = InfoOutput {
817 output: CommandOutput {
818 stdout: String::new(),
819 stderr: String::new(),
820 exit_code: 0,
821 success: true,
822 },
823 docker_info: None,
824 };
825
826 assert_eq!(output.server_version(), None);
827 assert_eq!(output.storage_driver(), None);
828 assert_eq!(output.container_count(), 0);
829 assert_eq!(output.running_containers(), 0);
830 assert_eq!(output.image_count(), 0);
831 assert!(!output.is_debug());
832 assert!(!output.is_experimental());
833 assert!(!output.has_warnings());
834 assert_eq!(output.warnings().len(), 0);
835
836 let (total, running, images) = output.resource_summary();
837 assert_eq!(total, 0);
838 assert_eq!(running, 0);
839 assert_eq!(images, 0);
840 }
841
842 #[test]
843 fn test_info_command_display() {
844 let info = InfoCommand::new().format("{{.ServerVersion}}");
845
846 let display = format!("{info}");
847 assert_eq!(display, "docker info --format {{.ServerVersion}}");
848 }
849
850 #[test]
851 fn test_info_command_display_no_format() {
852 let info = InfoCommand::new();
853
854 let display = format!("{info}");
855 assert_eq!(display, "docker info");
856 }
857
858 #[test]
859 fn test_info_command_name() {
860 let info = InfoCommand::new();
861 assert_eq!(info.command_name(), "info");
862 }
863
864 #[test]
865 fn test_info_command_extensibility() {
866 let mut info = InfoCommand::new();
867
868 info.arg("extra")
870 .args(vec!["more", "args"])
871 .flag("--verbose")
872 .option("--format", "json");
873
874 assert_eq!(info.get_format(), Some("json"));
876 }
877
878 #[test]
879 fn test_parse_json_output_concept() {
880 let json_output = r#"{"ServerVersion":"20.10.17","Driver":"overlay2","Containers":5}"#;
882
883 let output = CommandOutput {
884 stdout: json_output.to_string(),
885 stderr: String::new(),
886 exit_code: 0,
887 success: true,
888 };
889
890 let result = InfoCommand::parse_json_output(&output);
891
892 assert!(result.is_ok());
894 }
895
896 #[test]
897 fn test_parse_table_output_concept() {
898 let table_output = "Server Version: 20.10.17\nStorage Driver: overlay2\nContainers: 5\nRunning: 2\nImages: 10\nWARNING: Test warning";
900
901 let output = CommandOutput {
902 stdout: table_output.to_string(),
903 stderr: String::new(),
904 exit_code: 0,
905 success: true,
906 };
907
908 let result = InfoCommand::parse_table_output(&output);
909
910 assert!(result.is_some() || result.is_none());
912 }
913}