use super::{CommandExecutor, CommandOutput, DockerCommand};
use crate::error::{Error, Result};
use async_trait::async_trait;
use std::fmt;
#[derive(Debug, Clone)]
pub struct VersionCommand {
format: Option<String>,
pub executor: CommandExecutor,
}
#[derive(Debug, Clone, PartialEq)]
pub struct ClientVersion {
pub version: String,
pub api_version: String,
pub git_commit: String,
pub built: String,
pub go_version: String,
pub os: String,
pub arch: String,
}
#[derive(Debug, Clone, PartialEq)]
pub struct ServerVersion {
pub version: String,
pub api_version: String,
pub min_api_version: String,
pub git_commit: String,
pub built: String,
pub go_version: String,
pub os: String,
pub arch: String,
pub kernel_version: String,
pub experimental: bool,
}
#[derive(Debug, Clone, PartialEq)]
pub struct VersionInfo {
pub client: ClientVersion,
pub server: Option<ServerVersion>,
}
#[derive(Debug, Clone)]
pub struct VersionOutput {
pub output: CommandOutput,
pub version_info: Option<VersionInfo>,
}
impl VersionCommand {
#[must_use]
pub fn new() -> Self {
Self {
format: None,
executor: CommandExecutor::default(),
}
}
#[must_use]
pub fn format(mut self, format: impl Into<String>) -> Self {
self.format = Some(format.into());
self
}
#[must_use]
pub fn format_json(self) -> Self {
self.format("json")
}
#[must_use]
pub fn format_table(self) -> Self {
Self {
format: None,
executor: self.executor,
}
}
#[must_use]
pub fn get_executor(&self) -> &CommandExecutor {
&self.executor
}
pub fn get_executor_mut(&mut self) -> &mut CommandExecutor {
&mut self.executor
}
#[must_use]
pub fn build_command_args(&self) -> Vec<String> {
let mut args = vec!["version".to_string()];
if let Some(ref format) = self.format {
args.push("--format".to_string());
args.push(format.clone());
}
args.extend(self.executor.raw_args.clone());
args
}
fn parse_output(&self, output: &CommandOutput) -> Result<Option<VersionInfo>> {
if let Some(ref format) = self.format {
if format == "json" {
return Self::parse_json_output(output);
}
}
Ok(Self::parse_table_output(output))
}
fn parse_json_output(output: &CommandOutput) -> Result<Option<VersionInfo>> {
let parsed: serde_json::Value = serde_json::from_str(&output.stdout)
.map_err(|e| Error::parse_error(format!("Failed to parse version JSON output: {e}")))?;
let client_data = &parsed["Client"];
let client = ClientVersion {
version: client_data["Version"].as_str().unwrap_or("").to_string(),
api_version: client_data["ApiVersion"].as_str().unwrap_or("").to_string(),
git_commit: client_data["GitCommit"].as_str().unwrap_or("").to_string(),
built: client_data["Built"].as_str().unwrap_or("").to_string(),
go_version: client_data["GoVersion"].as_str().unwrap_or("").to_string(),
os: client_data["Os"].as_str().unwrap_or("").to_string(),
arch: client_data["Arch"].as_str().unwrap_or("").to_string(),
};
let server = parsed.get("Server").map(|server_data| ServerVersion {
version: server_data["Version"].as_str().unwrap_or("").to_string(),
api_version: server_data["ApiVersion"].as_str().unwrap_or("").to_string(),
min_api_version: server_data["MinAPIVersion"]
.as_str()
.unwrap_or("")
.to_string(),
git_commit: server_data["GitCommit"].as_str().unwrap_or("").to_string(),
built: server_data["Built"].as_str().unwrap_or("").to_string(),
go_version: server_data["GoVersion"].as_str().unwrap_or("").to_string(),
os: server_data["Os"].as_str().unwrap_or("").to_string(),
arch: server_data["Arch"].as_str().unwrap_or("").to_string(),
kernel_version: server_data["KernelVersion"]
.as_str()
.unwrap_or("")
.to_string(),
experimental: server_data["Experimental"].as_bool().unwrap_or(false),
});
Ok(Some(VersionInfo { client, server }))
}
fn parse_table_output(output: &CommandOutput) -> Option<VersionInfo> {
let lines: Vec<&str> = output.stdout.lines().collect();
if lines.is_empty() {
return None;
}
let mut client_section = false;
let mut server_section = false;
let mut client_data = std::collections::HashMap::new();
let mut server_data = std::collections::HashMap::new();
for line in lines {
let trimmed = line.trim();
if trimmed.starts_with("Client:") {
client_section = true;
server_section = false;
continue;
} else if trimmed.starts_with("Server:") {
client_section = false;
server_section = true;
continue;
}
if trimmed.is_empty() {
continue;
}
if let Some(colon_pos) = trimmed.find(':') {
let key = trimmed[..colon_pos].trim();
let value = trimmed[colon_pos + 1..].trim();
if client_section {
client_data.insert(key.to_string(), value.to_string());
} else if server_section {
server_data.insert(key.to_string(), value.to_string());
}
}
}
let client = ClientVersion {
version: client_data.get("Version").cloned().unwrap_or_default(),
api_version: client_data.get("API version").cloned().unwrap_or_default(),
git_commit: client_data.get("Git commit").cloned().unwrap_or_default(),
built: client_data.get("Built").cloned().unwrap_or_default(),
go_version: client_data.get("Go version").cloned().unwrap_or_default(),
os: client_data.get("OS/Arch").cloned().unwrap_or_default(),
arch: String::new(), };
let server = if server_data.is_empty() {
None
} else {
Some(ServerVersion {
version: server_data.get("Version").cloned().unwrap_or_default(),
api_version: server_data.get("API version").cloned().unwrap_or_default(),
min_api_version: server_data
.get("Minimum API version")
.cloned()
.unwrap_or_default(),
git_commit: server_data.get("Git commit").cloned().unwrap_or_default(),
built: server_data.get("Built").cloned().unwrap_or_default(),
go_version: server_data.get("Go version").cloned().unwrap_or_default(),
os: server_data.get("OS/Arch").cloned().unwrap_or_default(),
arch: String::new(), kernel_version: server_data
.get("Kernel Version")
.cloned()
.unwrap_or_default(),
experimental: server_data.get("Experimental").is_some_and(|s| s == "true"),
})
};
Some(VersionInfo { client, server })
}
#[must_use]
pub fn get_format(&self) -> Option<&str> {
self.format.as_deref()
}
}
impl Default for VersionCommand {
fn default() -> Self {
Self::new()
}
}
impl VersionOutput {
#[must_use]
pub fn success(&self) -> bool {
self.output.success
}
#[must_use]
pub fn client_version(&self) -> Option<&str> {
self.version_info
.as_ref()
.map(|v| v.client.version.as_str())
}
#[must_use]
pub fn server_version(&self) -> Option<&str> {
self.version_info
.as_ref()
.and_then(|v| v.server.as_ref())
.map(|s| s.version.as_str())
}
#[must_use]
pub fn api_version(&self) -> Option<&str> {
self.version_info
.as_ref()
.map(|v| v.client.api_version.as_str())
}
#[must_use]
pub fn has_server_info(&self) -> bool {
self.version_info
.as_ref()
.is_some_and(|v| v.server.is_some())
}
#[must_use]
pub fn is_experimental(&self) -> bool {
self.version_info
.as_ref()
.and_then(|v| v.server.as_ref())
.is_some_and(|s| s.experimental)
}
#[must_use]
pub fn is_compatible(&self, min_version: &str) -> bool {
if let Some(version) = self.client_version() {
version >= min_version
} else {
false
}
}
}
#[async_trait]
impl DockerCommand for VersionCommand {
type Output = VersionOutput;
fn get_executor(&self) -> &CommandExecutor {
&self.executor
}
fn get_executor_mut(&mut self) -> &mut CommandExecutor {
&mut self.executor
}
fn build_command_args(&self) -> Vec<String> {
self.build_command_args()
}
async fn execute(&self) -> Result<Self::Output> {
let args = self.build_command_args();
let output = self.execute_command(args).await?;
let version_info = self.parse_output(&output)?;
Ok(VersionOutput {
output,
version_info,
})
}
}
impl fmt::Display for VersionCommand {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "docker version")?;
if let Some(ref format) = self.format {
write!(f, " --format {format}")?;
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_version_command_basic() {
let version = VersionCommand::new();
assert_eq!(version.get_format(), None);
let args = version.build_command_args();
assert_eq!(args, vec!["version"]);
}
#[test]
fn test_version_command_with_format() {
let version = VersionCommand::new().format("{{.Client.Version}}");
assert_eq!(version.get_format(), Some("{{.Client.Version}}"));
let args = version.build_command_args();
assert_eq!(args, vec!["version", "--format", "{{.Client.Version}}"]);
}
#[test]
fn test_version_command_json_format() {
let version = VersionCommand::new().format_json();
assert_eq!(version.get_format(), Some("json"));
let args = version.build_command_args();
assert_eq!(args, vec!["version", "--format", "json"]);
}
#[test]
fn test_version_command_table_format() {
let version = VersionCommand::new().format_json().format_table();
assert_eq!(version.get_format(), None);
let args = version.build_command_args();
assert_eq!(args, vec!["version"]);
}
#[test]
fn test_version_command_default() {
let version = VersionCommand::default();
assert_eq!(version.get_format(), None);
let args = version.build_command_args();
assert_eq!(args, vec!["version"]);
}
#[test]
fn test_client_version_creation() {
let client = ClientVersion {
version: "20.10.17".to_string(),
api_version: "1.41".to_string(),
git_commit: "100c701".to_string(),
built: "Mon Jun 6 23:02:57 2022".to_string(),
go_version: "go1.17.11".to_string(),
os: "linux".to_string(),
arch: "amd64".to_string(),
};
assert_eq!(client.version, "20.10.17");
assert_eq!(client.api_version, "1.41");
assert_eq!(client.os, "linux");
assert_eq!(client.arch, "amd64");
}
#[test]
fn test_server_version_creation() {
let server = ServerVersion {
version: "20.10.17".to_string(),
api_version: "1.41".to_string(),
min_api_version: "1.12".to_string(),
git_commit: "100c701".to_string(),
built: "Mon Jun 6 23:02:57 2022".to_string(),
go_version: "go1.17.11".to_string(),
os: "linux".to_string(),
arch: "amd64".to_string(),
kernel_version: "5.15.0".to_string(),
experimental: false,
};
assert_eq!(server.version, "20.10.17");
assert_eq!(server.min_api_version, "1.12");
assert!(!server.experimental);
}
#[test]
fn test_version_info_creation() {
let client = ClientVersion {
version: "20.10.17".to_string(),
api_version: "1.41".to_string(),
git_commit: "100c701".to_string(),
built: "Mon Jun 6 23:02:57 2022".to_string(),
go_version: "go1.17.11".to_string(),
os: "linux".to_string(),
arch: "amd64".to_string(),
};
let version_info = VersionInfo {
client,
server: None,
};
assert_eq!(version_info.client.version, "20.10.17");
assert!(version_info.server.is_none());
}
#[test]
fn test_version_output_helpers() {
let client = ClientVersion {
version: "20.10.17".to_string(),
api_version: "1.41".to_string(),
git_commit: "100c701".to_string(),
built: "Mon Jun 6 23:02:57 2022".to_string(),
go_version: "go1.17.11".to_string(),
os: "linux".to_string(),
arch: "amd64".to_string(),
};
let server = ServerVersion {
version: "20.10.17".to_string(),
api_version: "1.41".to_string(),
min_api_version: "1.12".to_string(),
git_commit: "100c701".to_string(),
built: "Mon Jun 6 23:02:57 2022".to_string(),
go_version: "go1.17.11".to_string(),
os: "linux".to_string(),
arch: "amd64".to_string(),
kernel_version: "5.15.0".to_string(),
experimental: true,
};
let version_info = VersionInfo {
client,
server: Some(server),
};
let output = VersionOutput {
output: CommandOutput {
stdout: String::new(),
stderr: String::new(),
exit_code: 0,
success: true,
},
version_info: Some(version_info),
};
assert_eq!(output.client_version(), Some("20.10.17"));
assert_eq!(output.server_version(), Some("20.10.17"));
assert_eq!(output.api_version(), Some("1.41"));
assert!(output.has_server_info());
assert!(output.is_experimental());
assert!(output.is_compatible("20.10.0"));
assert!(!output.is_compatible("21.0.0"));
}
#[test]
fn test_version_output_no_server() {
let client = ClientVersion {
version: "20.10.17".to_string(),
api_version: "1.41".to_string(),
git_commit: "100c701".to_string(),
built: "Mon Jun 6 23:02:57 2022".to_string(),
go_version: "go1.17.11".to_string(),
os: "linux".to_string(),
arch: "amd64".to_string(),
};
let version_info = VersionInfo {
client,
server: None,
};
let output = VersionOutput {
output: CommandOutput {
stdout: String::new(),
stderr: String::new(),
exit_code: 0,
success: true,
},
version_info: Some(version_info),
};
assert_eq!(output.client_version(), Some("20.10.17"));
assert_eq!(output.server_version(), None);
assert!(!output.has_server_info());
assert!(!output.is_experimental());
}
#[test]
fn test_version_command_display() {
let version = VersionCommand::new().format("{{.Client.Version}}");
let display = format!("{version}");
assert_eq!(display, "docker version --format {{.Client.Version}}");
}
#[test]
fn test_version_command_display_no_format() {
let version = VersionCommand::new();
let display = format!("{version}");
assert_eq!(display, "docker version");
}
#[test]
fn test_version_command_name() {
let version = VersionCommand::new();
let args = version.build_command_args();
assert_eq!(args[0], "version");
}
#[test]
fn test_version_command_extensibility() {
let mut version = VersionCommand::new();
version
.get_executor_mut()
.raw_args
.push("--verbose".to_string());
version
.get_executor_mut()
.raw_args
.push("--some-flag".to_string());
let args = version.build_command_args();
assert!(args.contains(&"--verbose".to_string()));
assert!(args.contains(&"--some-flag".to_string()));
}
#[test]
fn test_parse_json_output_concept() {
let json_output = r#"{"Client":{"Version":"20.10.17","ApiVersion":"1.41"}}"#;
let output = CommandOutput {
stdout: json_output.to_string(),
stderr: String::new(),
exit_code: 0,
success: true,
};
let result = VersionCommand::parse_json_output(&output);
assert!(result.is_ok());
}
#[test]
fn test_parse_table_output_concept() {
let table_output =
"Client:\n Version: 20.10.17\n API version: 1.41\n\nServer:\n Version: 20.10.17";
let output = CommandOutput {
stdout: table_output.to_string(),
stderr: String::new(),
exit_code: 0,
success: true,
};
let result = VersionCommand::parse_table_output(&output);
assert!(result.is_some() || result.is_none());
}
}