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 VersionCommand {
34 format: Option<String>,
36 executor: CommandExecutor,
38}
39
40#[derive(Debug, Clone, PartialEq)]
42pub struct ClientVersion {
43 pub version: String,
45 pub api_version: String,
47 pub git_commit: String,
49 pub built: String,
51 pub go_version: String,
53 pub os: String,
55 pub arch: String,
57}
58
59#[derive(Debug, Clone, PartialEq)]
61pub struct ServerVersion {
62 pub version: String,
64 pub api_version: String,
66 pub min_api_version: String,
68 pub git_commit: String,
70 pub built: String,
72 pub go_version: String,
74 pub os: String,
76 pub arch: String,
78 pub kernel_version: String,
80 pub experimental: bool,
82}
83
84#[derive(Debug, Clone, PartialEq)]
86pub struct VersionInfo {
87 pub client: ClientVersion,
89 pub server: Option<ServerVersion>,
91}
92
93#[derive(Debug, Clone)]
98pub struct VersionOutput {
99 pub output: CommandOutput,
101 pub version_info: Option<VersionInfo>,
103}
104
105impl VersionCommand {
106 #[must_use]
116 pub fn new() -> Self {
117 Self {
118 format: None,
119 executor: CommandExecutor::default(),
120 }
121 }
122
123 #[must_use]
138 pub fn format(mut self, format: impl Into<String>) -> Self {
139 self.format = Some(format.into());
140 self
141 }
142
143 #[must_use]
153 pub fn format_json(self) -> Self {
154 self.format("json")
155 }
156
157 #[must_use]
159 pub fn format_table(self) -> Self {
160 Self {
161 format: None,
162 executor: self.executor,
163 }
164 }
165
166 #[must_use]
172 pub fn executor(mut self, executor: CommandExecutor) -> Self {
173 self.executor = executor;
174 self
175 }
176
177 fn build_command_args(&self) -> Vec<String> {
179 let mut args = vec!["version".to_string()];
180
181 if let Some(ref format) = self.format {
183 args.push("--format".to_string());
184 args.push(format.clone());
185 }
186
187 args
188 }
189
190 fn parse_output(&self, output: &CommandOutput) -> Result<Option<VersionInfo>> {
192 if let Some(ref format) = self.format {
193 if format == "json" {
194 return Self::parse_json_output(output);
195 }
196 }
197
198 Ok(Self::parse_table_output(output))
199 }
200
201 fn parse_json_output(output: &CommandOutput) -> Result<Option<VersionInfo>> {
203 let parsed: serde_json::Value = serde_json::from_str(&output.stdout)
204 .map_err(|e| Error::parse_error(format!("Failed to parse version JSON output: {e}")))?;
205
206 let client_data = &parsed["Client"];
208 let client = ClientVersion {
209 version: client_data["Version"].as_str().unwrap_or("").to_string(),
210 api_version: client_data["ApiVersion"].as_str().unwrap_or("").to_string(),
211 git_commit: client_data["GitCommit"].as_str().unwrap_or("").to_string(),
212 built: client_data["Built"].as_str().unwrap_or("").to_string(),
213 go_version: client_data["GoVersion"].as_str().unwrap_or("").to_string(),
214 os: client_data["Os"].as_str().unwrap_or("").to_string(),
215 arch: client_data["Arch"].as_str().unwrap_or("").to_string(),
216 };
217
218 let server = parsed.get("Server").map(|server_data| ServerVersion {
220 version: server_data["Version"].as_str().unwrap_or("").to_string(),
221 api_version: server_data["ApiVersion"].as_str().unwrap_or("").to_string(),
222 min_api_version: server_data["MinAPIVersion"]
223 .as_str()
224 .unwrap_or("")
225 .to_string(),
226 git_commit: server_data["GitCommit"].as_str().unwrap_or("").to_string(),
227 built: server_data["Built"].as_str().unwrap_or("").to_string(),
228 go_version: server_data["GoVersion"].as_str().unwrap_or("").to_string(),
229 os: server_data["Os"].as_str().unwrap_or("").to_string(),
230 arch: server_data["Arch"].as_str().unwrap_or("").to_string(),
231 kernel_version: server_data["KernelVersion"]
232 .as_str()
233 .unwrap_or("")
234 .to_string(),
235 experimental: server_data["Experimental"].as_bool().unwrap_or(false),
236 });
237
238 Ok(Some(VersionInfo { client, server }))
239 }
240
241 fn parse_table_output(output: &CommandOutput) -> Option<VersionInfo> {
243 let lines: Vec<&str> = output.stdout.lines().collect();
244
245 if lines.is_empty() {
246 return None;
247 }
248
249 let mut client_section = false;
250 let mut server_section = false;
251 let mut client_data = std::collections::HashMap::new();
252 let mut server_data = std::collections::HashMap::new();
253
254 for line in lines {
255 let trimmed = line.trim();
256
257 if trimmed.starts_with("Client:") {
258 client_section = true;
259 server_section = false;
260 continue;
261 } else if trimmed.starts_with("Server:") {
262 client_section = false;
263 server_section = true;
264 continue;
265 }
266
267 if trimmed.is_empty() {
268 continue;
269 }
270
271 if let Some(colon_pos) = trimmed.find(':') {
273 let key = trimmed[..colon_pos].trim();
274 let value = trimmed[colon_pos + 1..].trim();
275
276 if client_section {
277 client_data.insert(key.to_string(), value.to_string());
278 } else if server_section {
279 server_data.insert(key.to_string(), value.to_string());
280 }
281 }
282 }
283
284 let client = ClientVersion {
285 version: client_data.get("Version").cloned().unwrap_or_default(),
286 api_version: client_data.get("API version").cloned().unwrap_or_default(),
287 git_commit: client_data.get("Git commit").cloned().unwrap_or_default(),
288 built: client_data.get("Built").cloned().unwrap_or_default(),
289 go_version: client_data.get("Go version").cloned().unwrap_or_default(),
290 os: client_data.get("OS/Arch").cloned().unwrap_or_default(),
291 arch: String::new(), };
293
294 let server = if server_data.is_empty() {
295 None
296 } else {
297 Some(ServerVersion {
298 version: server_data.get("Version").cloned().unwrap_or_default(),
299 api_version: server_data.get("API version").cloned().unwrap_or_default(),
300 min_api_version: server_data
301 .get("Minimum API version")
302 .cloned()
303 .unwrap_or_default(),
304 git_commit: server_data.get("Git commit").cloned().unwrap_or_default(),
305 built: server_data.get("Built").cloned().unwrap_or_default(),
306 go_version: server_data.get("Go version").cloned().unwrap_or_default(),
307 os: server_data.get("OS/Arch").cloned().unwrap_or_default(),
308 arch: String::new(), kernel_version: server_data
310 .get("Kernel Version")
311 .cloned()
312 .unwrap_or_default(),
313 experimental: server_data.get("Experimental").is_some_and(|s| s == "true"),
314 })
315 };
316
317 Some(VersionInfo { client, server })
318 }
319
320 #[must_use]
322 pub fn get_format(&self) -> Option<&str> {
323 self.format.as_deref()
324 }
325}
326
327impl Default for VersionCommand {
328 fn default() -> Self {
329 Self::new()
330 }
331}
332
333impl VersionOutput {
334 #[must_use]
336 pub fn success(&self) -> bool {
337 self.output.success
338 }
339
340 #[must_use]
342 pub fn client_version(&self) -> Option<&str> {
343 self.version_info
344 .as_ref()
345 .map(|v| v.client.version.as_str())
346 }
347
348 #[must_use]
350 pub fn server_version(&self) -> Option<&str> {
351 self.version_info
352 .as_ref()
353 .and_then(|v| v.server.as_ref())
354 .map(|s| s.version.as_str())
355 }
356
357 #[must_use]
359 pub fn api_version(&self) -> Option<&str> {
360 self.version_info
361 .as_ref()
362 .map(|v| v.client.api_version.as_str())
363 }
364
365 #[must_use]
367 pub fn has_server_info(&self) -> bool {
368 self.version_info
369 .as_ref()
370 .is_some_and(|v| v.server.is_some())
371 }
372
373 #[must_use]
375 pub fn is_experimental(&self) -> bool {
376 self.version_info
377 .as_ref()
378 .and_then(|v| v.server.as_ref())
379 .is_some_and(|s| s.experimental)
380 }
381
382 #[must_use]
384 pub fn is_compatible(&self, min_version: &str) -> bool {
385 if let Some(version) = self.client_version() {
386 version >= min_version
388 } else {
389 false
390 }
391 }
392}
393
394#[async_trait]
395impl DockerCommand for VersionCommand {
396 type Output = VersionOutput;
397
398 fn command_name(&self) -> &'static str {
399 "version"
400 }
401
402 fn build_args(&self) -> Vec<String> {
403 self.build_command_args()
404 }
405
406 async fn execute(&self) -> Result<Self::Output> {
407 let output = self
408 .executor
409 .execute_command(self.command_name(), self.build_args())
410 .await?;
411
412 let version_info = self.parse_output(&output)?;
413
414 Ok(VersionOutput {
415 output,
416 version_info,
417 })
418 }
419
420 fn arg<S: AsRef<OsStr>>(&mut self, arg: S) -> &mut Self {
421 self.executor.add_arg(arg);
422 self
423 }
424
425 fn args<I, S>(&mut self, args: I) -> &mut Self
426 where
427 I: IntoIterator<Item = S>,
428 S: AsRef<OsStr>,
429 {
430 self.executor.add_args(args);
431 self
432 }
433
434 fn flag(&mut self, flag: &str) -> &mut Self {
435 self.executor.add_flag(flag);
436 self
437 }
438
439 fn option(&mut self, key: &str, value: &str) -> &mut Self {
440 match key {
441 "--format" | "-f" | "format" => {
442 self.format = Some(value.to_string());
443 }
444 _ => {
445 self.executor.add_option(key, value);
446 }
447 }
448 self
449 }
450}
451
452impl fmt::Display for VersionCommand {
453 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
454 write!(f, "docker version")?;
455
456 if let Some(ref format) = self.format {
457 write!(f, " --format {format}")?;
458 }
459
460 Ok(())
461 }
462}
463
464#[cfg(test)]
465mod tests {
466 use super::*;
467
468 #[test]
469 fn test_version_command_basic() {
470 let version = VersionCommand::new();
471
472 assert_eq!(version.get_format(), None);
473
474 let args = version.build_command_args();
475 assert_eq!(args, vec!["version"]);
476 }
477
478 #[test]
479 fn test_version_command_with_format() {
480 let version = VersionCommand::new().format("{{.Client.Version}}");
481
482 assert_eq!(version.get_format(), Some("{{.Client.Version}}"));
483
484 let args = version.build_command_args();
485 assert_eq!(args, vec!["version", "--format", "{{.Client.Version}}"]);
486 }
487
488 #[test]
489 fn test_version_command_json_format() {
490 let version = VersionCommand::new().format_json();
491
492 assert_eq!(version.get_format(), Some("json"));
493
494 let args = version.build_command_args();
495 assert_eq!(args, vec!["version", "--format", "json"]);
496 }
497
498 #[test]
499 fn test_version_command_table_format() {
500 let version = VersionCommand::new().format_json().format_table();
501
502 assert_eq!(version.get_format(), None);
503
504 let args = version.build_command_args();
505 assert_eq!(args, vec!["version"]);
506 }
507
508 #[test]
509 fn test_version_command_default() {
510 let version = VersionCommand::default();
511
512 assert_eq!(version.get_format(), None);
513 let args = version.build_command_args();
514 assert_eq!(args, vec!["version"]);
515 }
516
517 #[test]
518 fn test_client_version_creation() {
519 let client = ClientVersion {
520 version: "20.10.17".to_string(),
521 api_version: "1.41".to_string(),
522 git_commit: "100c701".to_string(),
523 built: "Mon Jun 6 23:02:57 2022".to_string(),
524 go_version: "go1.17.11".to_string(),
525 os: "linux".to_string(),
526 arch: "amd64".to_string(),
527 };
528
529 assert_eq!(client.version, "20.10.17");
530 assert_eq!(client.api_version, "1.41");
531 assert_eq!(client.os, "linux");
532 assert_eq!(client.arch, "amd64");
533 }
534
535 #[test]
536 fn test_server_version_creation() {
537 let server = ServerVersion {
538 version: "20.10.17".to_string(),
539 api_version: "1.41".to_string(),
540 min_api_version: "1.12".to_string(),
541 git_commit: "100c701".to_string(),
542 built: "Mon Jun 6 23:02:57 2022".to_string(),
543 go_version: "go1.17.11".to_string(),
544 os: "linux".to_string(),
545 arch: "amd64".to_string(),
546 kernel_version: "5.15.0".to_string(),
547 experimental: false,
548 };
549
550 assert_eq!(server.version, "20.10.17");
551 assert_eq!(server.min_api_version, "1.12");
552 assert!(!server.experimental);
553 }
554
555 #[test]
556 fn test_version_info_creation() {
557 let client = ClientVersion {
558 version: "20.10.17".to_string(),
559 api_version: "1.41".to_string(),
560 git_commit: "100c701".to_string(),
561 built: "Mon Jun 6 23:02:57 2022".to_string(),
562 go_version: "go1.17.11".to_string(),
563 os: "linux".to_string(),
564 arch: "amd64".to_string(),
565 };
566
567 let version_info = VersionInfo {
568 client,
569 server: None,
570 };
571
572 assert_eq!(version_info.client.version, "20.10.17");
573 assert!(version_info.server.is_none());
574 }
575
576 #[test]
577 fn test_version_output_helpers() {
578 let client = ClientVersion {
579 version: "20.10.17".to_string(),
580 api_version: "1.41".to_string(),
581 git_commit: "100c701".to_string(),
582 built: "Mon Jun 6 23:02:57 2022".to_string(),
583 go_version: "go1.17.11".to_string(),
584 os: "linux".to_string(),
585 arch: "amd64".to_string(),
586 };
587
588 let server = ServerVersion {
589 version: "20.10.17".to_string(),
590 api_version: "1.41".to_string(),
591 min_api_version: "1.12".to_string(),
592 git_commit: "100c701".to_string(),
593 built: "Mon Jun 6 23:02:57 2022".to_string(),
594 go_version: "go1.17.11".to_string(),
595 os: "linux".to_string(),
596 arch: "amd64".to_string(),
597 kernel_version: "5.15.0".to_string(),
598 experimental: true,
599 };
600
601 let version_info = VersionInfo {
602 client,
603 server: Some(server),
604 };
605
606 let output = VersionOutput {
607 output: CommandOutput {
608 stdout: String::new(),
609 stderr: String::new(),
610 exit_code: 0,
611 success: true,
612 },
613 version_info: Some(version_info),
614 };
615
616 assert_eq!(output.client_version(), Some("20.10.17"));
617 assert_eq!(output.server_version(), Some("20.10.17"));
618 assert_eq!(output.api_version(), Some("1.41"));
619 assert!(output.has_server_info());
620 assert!(output.is_experimental());
621 assert!(output.is_compatible("20.10.0"));
622 assert!(!output.is_compatible("21.0.0"));
623 }
624
625 #[test]
626 fn test_version_output_no_server() {
627 let client = ClientVersion {
628 version: "20.10.17".to_string(),
629 api_version: "1.41".to_string(),
630 git_commit: "100c701".to_string(),
631 built: "Mon Jun 6 23:02:57 2022".to_string(),
632 go_version: "go1.17.11".to_string(),
633 os: "linux".to_string(),
634 arch: "amd64".to_string(),
635 };
636
637 let version_info = VersionInfo {
638 client,
639 server: None,
640 };
641
642 let output = VersionOutput {
643 output: CommandOutput {
644 stdout: String::new(),
645 stderr: String::new(),
646 exit_code: 0,
647 success: true,
648 },
649 version_info: Some(version_info),
650 };
651
652 assert_eq!(output.client_version(), Some("20.10.17"));
653 assert_eq!(output.server_version(), None);
654 assert!(!output.has_server_info());
655 assert!(!output.is_experimental());
656 }
657
658 #[test]
659 fn test_version_command_display() {
660 let version = VersionCommand::new().format("{{.Client.Version}}");
661
662 let display = format!("{version}");
663 assert_eq!(display, "docker version --format {{.Client.Version}}");
664 }
665
666 #[test]
667 fn test_version_command_display_no_format() {
668 let version = VersionCommand::new();
669
670 let display = format!("{version}");
671 assert_eq!(display, "docker version");
672 }
673
674 #[test]
675 fn test_version_command_name() {
676 let version = VersionCommand::new();
677 assert_eq!(version.command_name(), "version");
678 }
679
680 #[test]
681 fn test_version_command_extensibility() {
682 let mut version = VersionCommand::new();
683
684 version
686 .arg("extra")
687 .args(vec!["more", "args"])
688 .flag("--verbose")
689 .option("--format", "json");
690
691 assert_eq!(version.get_format(), Some("json"));
693 }
694
695 #[test]
696 fn test_parse_json_output_concept() {
697 let json_output = r#"{"Client":{"Version":"20.10.17","ApiVersion":"1.41"}}"#;
699
700 let output = CommandOutput {
701 stdout: json_output.to_string(),
702 stderr: String::new(),
703 exit_code: 0,
704 success: true,
705 };
706
707 let result = VersionCommand::parse_json_output(&output);
708
709 assert!(result.is_ok());
711 }
712
713 #[test]
714 fn test_parse_table_output_concept() {
715 let table_output =
717 "Client:\n Version: 20.10.17\n API version: 1.41\n\nServer:\n Version: 20.10.17";
718
719 let output = CommandOutput {
720 stdout: table_output.to_string(),
721 stderr: String::new(),
722 exit_code: 0,
723 success: true,
724 };
725
726 let result = VersionCommand::parse_table_output(&output);
727
728 assert!(result.is_some() || result.is_none());
730 }
731}