docker_wrapper/
prerequisites.rs1use crate::error::{Error, Result};
7use serde::{Deserialize, Serialize};
8use std::process::Stdio;
9use tokio::process::Command;
10use tracing::{debug, info, warn};
11
12#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
14pub struct DockerVersion {
15 pub version: String,
17 pub major: u32,
19 pub minor: u32,
21 pub patch: u32,
23}
24
25impl DockerVersion {
26 pub fn parse(version_str: &str) -> Result<Self> {
31 let clean_version = version_str.trim().trim_start_matches('v');
32 let parts: Vec<&str> = clean_version.split('.').collect();
33
34 if parts.len() < 3 {
35 return Err(Error::parse_error(format!(
36 "Invalid version format: {version_str}"
37 )));
38 }
39
40 let major = parts[0]
41 .parse()
42 .map_err(|_| Error::parse_error(format!("Invalid major version: {}", parts[0])))?;
43
44 let minor = parts[1]
45 .parse()
46 .map_err(|_| Error::parse_error(format!("Invalid minor version: {}", parts[1])))?;
47
48 let patch = parts[2]
49 .parse()
50 .map_err(|_| Error::parse_error(format!("Invalid patch version: {}", parts[2])))?;
51
52 Ok(Self {
53 version: clean_version.to_string(),
54 major,
55 minor,
56 patch,
57 })
58 }
59
60 #[must_use]
62 pub fn meets_minimum(&self, minimum: &DockerVersion) -> bool {
63 if self.major > minimum.major {
64 return true;
65 }
66 if self.major == minimum.major {
67 if self.minor > minimum.minor {
68 return true;
69 }
70 if self.minor == minimum.minor && self.patch >= minimum.patch {
71 return true;
72 }
73 }
74 false
75 }
76}
77
78#[derive(Debug, Clone, Serialize, Deserialize)]
80pub struct DockerInfo {
81 pub version: DockerVersion,
83 pub binary_path: String,
85 pub daemon_running: bool,
87 pub server_version: Option<DockerVersion>,
89 pub os: String,
91 pub architecture: String,
93}
94
95pub struct DockerPrerequisites {
97 pub minimum_version: DockerVersion,
99}
100
101impl Default for DockerPrerequisites {
102 fn default() -> Self {
103 Self {
104 minimum_version: DockerVersion {
105 version: "20.10.0".to_string(),
106 major: 20,
107 minor: 10,
108 patch: 0,
109 },
110 }
111 }
112}
113
114impl DockerPrerequisites {
115 #[must_use]
117 pub fn new(minimum_version: DockerVersion) -> Self {
118 Self { minimum_version }
119 }
120
121 pub async fn check(&self) -> Result<DockerInfo> {
127 info!("Checking Docker prerequisites...");
128
129 let binary_path = self.find_docker_binary().await?;
131 debug!("Found Docker binary at: {}", binary_path);
132
133 let version = self.get_docker_version(&binary_path).await?;
135 info!("Found Docker version: {}", version.version);
136
137 if !version.meets_minimum(&self.minimum_version) {
139 return Err(Error::UnsupportedVersion {
140 found: version.version.clone(),
141 minimum: self.minimum_version.version.clone(),
142 });
143 }
144
145 let (daemon_running, server_version) = self.check_daemon(&binary_path).await;
147
148 if daemon_running {
149 info!("Docker daemon is running");
150 } else {
151 warn!("Docker daemon is not running");
152 }
153
154 let (os, architecture) = Self::get_system_info();
156
157 Ok(DockerInfo {
158 version,
159 binary_path,
160 daemon_running,
161 server_version,
162 os,
163 architecture,
164 })
165 }
166
167 async fn find_docker_binary(&self) -> Result<String> {
169 let output = Command::new("which")
170 .arg("docker")
171 .stdout(Stdio::piped())
172 .stderr(Stdio::piped())
173 .output()
174 .await
175 .map_err(|e| Error::custom(format!("Failed to run 'which docker': {e}")))?;
176
177 if !output.status.success() {
178 return Err(Error::DockerNotFound);
179 }
180
181 let path = String::from_utf8_lossy(&output.stdout).trim().to_string();
182 if path.is_empty() {
183 return Err(Error::DockerNotFound);
184 }
185
186 Ok(path)
187 }
188
189 async fn get_docker_version(&self, binary_path: &str) -> Result<DockerVersion> {
191 let output = Command::new(binary_path)
192 .args(["--version"])
193 .stdout(Stdio::piped())
194 .stderr(Stdio::piped())
195 .output()
196 .await
197 .map_err(|e| Error::custom(format!("Failed to run 'docker --version': {e}")))?;
198
199 if !output.status.success() {
200 return Err(Error::command_failed(
201 "docker --version",
202 output.status.code().unwrap_or(-1),
203 String::from_utf8_lossy(&output.stdout).to_string(),
204 String::from_utf8_lossy(&output.stderr).to_string(),
205 ));
206 }
207
208 let version_output = String::from_utf8_lossy(&output.stdout);
209 debug!("Docker version output: {}", version_output);
210
211 let version_str = version_output
213 .split_whitespace()
214 .nth(2)
215 .and_then(|v| v.split(',').next())
216 .ok_or_else(|| {
217 Error::parse_error(format!("Could not parse version from: {version_output}"))
218 })?;
219
220 DockerVersion::parse(version_str)
221 }
222
223 async fn check_daemon(&self, binary_path: &str) -> (bool, Option<DockerVersion>) {
225 let output = Command::new(binary_path)
226 .args(["version", "--format", "{{.Server.Version}}"])
227 .stdout(Stdio::piped())
228 .stderr(Stdio::piped())
229 .output()
230 .await;
231
232 match output {
233 Ok(output) if output.status.success() => {
234 let server_version_str = String::from_utf8_lossy(&output.stdout).trim().to_string();
235 if server_version_str.is_empty() {
236 (false, None)
237 } else {
238 match DockerVersion::parse(&server_version_str) {
239 Ok(version) => (true, Some(version)),
240 Err(_) => (true, None),
241 }
242 }
243 }
244 _ => (false, None),
245 }
246 }
247
248 fn get_system_info() -> (String, String) {
250 let os = std::env::consts::OS.to_string();
251 let arch = std::env::consts::ARCH.to_string();
252 (os, arch)
253 }
254}
255
256pub async fn ensure_docker() -> Result<DockerInfo> {
262 let checker = DockerPrerequisites::default();
263 checker.check().await
264}
265
266#[cfg(test)]
267mod tests {
268 use super::*;
269
270 #[test]
271 fn test_docker_version_parse() {
272 let version = DockerVersion::parse("24.0.7").unwrap();
273 assert_eq!(version.major, 24);
274 assert_eq!(version.minor, 0);
275 assert_eq!(version.patch, 7);
276 assert_eq!(version.version, "24.0.7");
277 }
278
279 #[test]
280 fn test_docker_version_parse_with_v_prefix() {
281 let version = DockerVersion::parse("v20.10.21").unwrap();
282 assert_eq!(version.major, 20);
283 assert_eq!(version.minor, 10);
284 assert_eq!(version.patch, 21);
285 assert_eq!(version.version, "20.10.21");
286 }
287
288 #[test]
289 fn test_docker_version_parse_invalid() {
290 assert!(DockerVersion::parse("invalid").is_err());
291 assert!(DockerVersion::parse("1.2").is_err());
292 assert!(DockerVersion::parse("a.b.c").is_err());
293 }
294
295 #[test]
296 fn test_version_meets_minimum() {
297 let current = DockerVersion::parse("24.0.7").unwrap();
298 let minimum = DockerVersion::parse("20.10.0").unwrap();
299 let too_high = DockerVersion::parse("25.0.0").unwrap();
300
301 assert!(current.meets_minimum(&minimum));
302 assert!(!current.meets_minimum(&too_high));
303
304 let exact = DockerVersion::parse("20.10.0").unwrap();
306 assert!(exact.meets_minimum(&minimum));
307
308 let newer_minor = DockerVersion::parse("20.11.0").unwrap();
310 let older_minor = DockerVersion::parse("20.9.0").unwrap();
311 assert!(newer_minor.meets_minimum(&minimum));
312 assert!(!older_minor.meets_minimum(&minimum));
313
314 let newer_patch = DockerVersion::parse("20.10.1").unwrap();
316 let older_patch = DockerVersion::parse("20.10.0").unwrap();
317 assert!(newer_patch.meets_minimum(&minimum));
318 assert!(older_patch.meets_minimum(&minimum)); }
320
321 #[test]
322 fn test_prerequisites_default() {
323 let prereqs = DockerPrerequisites::default();
324 assert_eq!(prereqs.minimum_version.version, "20.10.0");
325 }
326
327 #[test]
328 fn test_prerequisites_custom_minimum() {
329 let custom_version = DockerVersion::parse("25.0.0").unwrap();
330 let prereqs = DockerPrerequisites::new(custom_version.clone());
331 assert_eq!(prereqs.minimum_version, custom_version);
332 }
333
334 #[tokio::test]
335 async fn test_ensure_docker_integration() {
336 let result = ensure_docker().await;
339
340 match result {
341 Ok(info) => {
342 assert!(!info.binary_path.is_empty());
343 assert!(!info.version.version.is_empty());
344 assert!(info.version.major >= 20);
345 println!(
346 "Docker found: {} at {}",
347 info.version.version, info.binary_path
348 );
349
350 if info.daemon_running {
351 println!("Docker daemon is running");
352 if let Some(server_version) = info.server_version {
353 println!("Server version: {}", server_version.version);
354 }
355 } else {
356 println!("Docker daemon is not running");
357 }
358 }
359 Err(Error::DockerNotFound) => {
360 println!("Docker not found - skipping integration test");
361 }
362 Err(e) => {
363 println!("Prerequisites check failed: {e}");
364 }
365 }
366 }
367}