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