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