docker_wrapper/
platform.rs

1//! Platform detection and runtime abstraction for Docker environments.
2
3use crate::error::{Error, Result};
4use std::env;
5use std::path::{Path, PathBuf};
6use std::process::Command;
7
8/// Represents the detected container runtime
9#[derive(Debug, Clone, PartialEq, Eq)]
10pub enum Runtime {
11    /// Docker runtime
12    Docker,
13    /// Podman runtime (Docker-compatible)
14    Podman,
15    /// Colima runtime (Docker-compatible on macOS)
16    Colima,
17    /// Rancher Desktop runtime
18    RancherDesktop,
19    /// `OrbStack` runtime (macOS)
20    OrbStack,
21    /// Docker Desktop
22    DockerDesktop,
23}
24
25impl Runtime {
26    /// Get the command name for this runtime
27    #[must_use]
28    pub fn command(&self) -> &str {
29        match self {
30            Runtime::Docker
31            | Runtime::Colima
32            | Runtime::RancherDesktop
33            | Runtime::OrbStack
34            | Runtime::DockerDesktop => "docker",
35            Runtime::Podman => "podman",
36        }
37    }
38
39    /// Check if this runtime supports Docker Compose
40    #[must_use]
41    pub fn supports_compose(&self) -> bool {
42        matches!(
43            self,
44            Runtime::Docker
45                | Runtime::DockerDesktop
46                | Runtime::Colima
47                | Runtime::RancherDesktop
48                | Runtime::OrbStack
49        )
50    }
51
52    /// Get compose command for this runtime
53    #[must_use]
54    pub fn compose_command(&self) -> Vec<String> {
55        match self {
56            Runtime::Podman => vec!["podman-compose".to_string()],
57            Runtime::Docker
58            | Runtime::DockerDesktop
59            | Runtime::Colima
60            | Runtime::RancherDesktop
61            | Runtime::OrbStack => {
62                vec!["docker".to_string(), "compose".to_string()]
63            }
64        }
65    }
66}
67
68impl std::fmt::Display for Runtime {
69    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
70        match self {
71            Runtime::Docker => write!(f, "Docker"),
72            Runtime::Podman => write!(f, "Podman"),
73            Runtime::Colima => write!(f, "Colima"),
74            Runtime::RancherDesktop => write!(f, "Rancher Desktop"),
75            Runtime::OrbStack => write!(f, "OrbStack"),
76            Runtime::DockerDesktop => write!(f, "Docker Desktop"),
77        }
78    }
79}
80
81/// Operating system platform
82#[derive(Debug, Clone, PartialEq, Eq)]
83pub enum Platform {
84    /// Linux
85    Linux,
86    /// macOS
87    MacOS,
88    /// Windows
89    Windows,
90    /// FreeBSD
91    FreeBSD,
92    /// Other/Unknown
93    Other(String),
94}
95
96impl Platform {
97    /// Detect the current platform
98    #[must_use]
99    pub fn detect() -> Self {
100        match env::consts::OS {
101            "linux" => Platform::Linux,
102            "macos" | "darwin" => Platform::MacOS,
103            "windows" => Platform::Windows,
104            "freebsd" => Platform::FreeBSD,
105            other => Platform::Other(other.to_string()),
106        }
107    }
108
109    /// Check if running inside WSL
110    #[must_use]
111    pub fn is_wsl(&self) -> bool {
112        if !matches!(self, Platform::Linux) {
113            return false;
114        }
115
116        // Check for WSL-specific files/environment
117        Path::new("/proc/sys/fs/binfmt_misc/WSLInterop").exists()
118            || env::var("WSL_DISTRO_NAME").is_ok()
119            || env::var("WSL_INTEROP").is_ok()
120    }
121
122    /// Get the default Docker socket path for this platform
123    #[must_use]
124    pub fn default_socket_path(&self) -> PathBuf {
125        match self {
126            Platform::MacOS => {
127                // Check for various Docker socket locations on macOS
128                let locations = [
129                    "/var/run/docker.sock",
130                    "/Users/$USER/.docker/run/docker.sock",
131                    "/Users/$USER/.colima/docker.sock",
132                    "/Users/$USER/.orbstack/run/docker.sock",
133                ];
134
135                for location in &locations {
136                    let path = if location.contains("$USER") {
137                        let user = env::var("USER").unwrap_or_else(|_| "unknown".to_string());
138                        PathBuf::from(location.replace("$USER", &user))
139                    } else {
140                        PathBuf::from(location)
141                    };
142
143                    if path.exists() {
144                        return path;
145                    }
146                }
147
148                PathBuf::from("/var/run/docker.sock")
149            }
150            Platform::Windows => PathBuf::from("//./pipe/docker_engine"),
151            Platform::Linux | Platform::FreeBSD | Platform::Other(_) => {
152                PathBuf::from("/var/run/docker.sock")
153            }
154        }
155    }
156}
157
158impl std::fmt::Display for Platform {
159    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
160        match self {
161            Platform::Linux => write!(f, "Linux"),
162            Platform::MacOS => write!(f, "macOS"),
163            Platform::Windows => write!(f, "Windows"),
164            Platform::FreeBSD => write!(f, "FreeBSD"),
165            Platform::Other(s) => write!(f, "{s}"),
166        }
167    }
168}
169
170/// Platform and runtime detection
171#[derive(Debug, Clone)]
172pub struct PlatformInfo {
173    /// Operating system platform
174    pub platform: Platform,
175    /// Container runtime
176    pub runtime: Runtime,
177    /// Docker/runtime version
178    pub version: String,
179    /// Whether running in WSL
180    pub is_wsl: bool,
181    /// Docker socket path
182    pub socket_path: PathBuf,
183}
184
185impl PlatformInfo {
186    /// Detect platform and runtime information
187    ///
188    /// # Errors
189    ///
190    /// Returns an error if no container runtime is detected
191    pub fn detect() -> Result<Self> {
192        let platform = Platform::detect();
193        let is_wsl = platform.is_wsl();
194        let socket_path = Self::find_socket_path(&platform);
195
196        // Detect runtime
197        let runtime = Self::detect_runtime()?;
198        let version = Self::get_runtime_version(&runtime)?;
199
200        Ok(Self {
201            platform,
202            runtime,
203            version,
204            is_wsl,
205            socket_path,
206        })
207    }
208
209    /// Find the Docker socket path
210    fn find_socket_path(platform: &Platform) -> PathBuf {
211        // Check DOCKER_HOST environment variable first
212        if let Ok(docker_host) = env::var("DOCKER_HOST") {
213            if docker_host.starts_with("unix://") {
214                return PathBuf::from(docker_host.trim_start_matches("unix://"));
215            }
216        }
217
218        platform.default_socket_path()
219    }
220
221    /// Detect the container runtime
222    fn detect_runtime() -> Result<Runtime> {
223        // Check for specific runtime environment variables
224        if env::var("ORBSTACK_HOME").is_ok() {
225            return Ok(Runtime::OrbStack);
226        }
227
228        if env::var("COLIMA_HOME").is_ok() {
229            return Ok(Runtime::Colima);
230        }
231
232        // Try to detect by checking version output
233        if let Ok(output) = Command::new("docker").arg("version").output() {
234            let version_str = String::from_utf8_lossy(&output.stdout);
235
236            if version_str.contains("Docker Desktop") {
237                return Ok(Runtime::DockerDesktop);
238            }
239
240            if version_str.contains("Rancher Desktop") {
241                return Ok(Runtime::RancherDesktop);
242            }
243
244            if version_str.contains("podman") {
245                return Ok(Runtime::Podman);
246            }
247
248            if version_str.contains("colima") {
249                return Ok(Runtime::Colima);
250            }
251
252            if version_str.contains("OrbStack") {
253                return Ok(Runtime::OrbStack);
254            }
255
256            // Generic Docker
257            if version_str.contains("Docker") {
258                return Ok(Runtime::Docker);
259            }
260        }
261
262        // Try podman as fallback
263        if Command::new("podman").arg("version").output().is_ok() {
264            return Ok(Runtime::Podman);
265        }
266
267        Err(Error::DockerNotFound)
268    }
269
270    /// Get runtime version
271    fn get_runtime_version(runtime: &Runtime) -> Result<String> {
272        let output = Command::new(runtime.command())
273            .arg("version")
274            .arg("--format")
275            .arg("{{.Server.Version}}")
276            .output()
277            .map_err(|e| {
278                Error::command_failed(
279                    format!("{} version", runtime.command()),
280                    -1,
281                    "",
282                    e.to_string(),
283                )
284            })?;
285
286        if output.status.success() {
287            Ok(String::from_utf8_lossy(&output.stdout).trim().to_string())
288        } else {
289            // Fallback to parsing regular version output
290            let output = Command::new(runtime.command())
291                .arg("version")
292                .output()
293                .map_err(|e| {
294                    Error::command_failed(
295                        format!("{} version", runtime.command()),
296                        -1,
297                        "",
298                        e.to_string(),
299                    )
300                })?;
301
302            let version_str = String::from_utf8_lossy(&output.stdout);
303            Ok(Self::parse_version(&version_str))
304        }
305    }
306
307    /// Parse version from version string
308    fn parse_version(version_str: &str) -> String {
309        // Look for version patterns
310        for line in version_str.lines() {
311            if line.contains("Version:") {
312                if let Some(version) = line.split(':').nth(1) {
313                    return version.trim().to_string();
314                }
315            }
316        }
317
318        "unknown".to_string()
319    }
320
321    /// Check if the runtime is available and working
322    ///
323    /// # Errors
324    ///
325    /// Returns an error if the runtime is not found or not running
326    pub fn check_runtime(&self) -> Result<()> {
327        let output = Command::new(self.runtime.command())
328            .arg("info")
329            .output()
330            .map_err(|_| Error::DockerNotFound)?;
331
332        if !output.status.success() {
333            let stderr = String::from_utf8_lossy(&output.stderr);
334            if stderr.contains("Cannot connect to the Docker daemon") {
335                return Err(Error::DaemonNotRunning);
336            }
337            return Err(Error::command_failed(
338                format!("{} info", self.runtime.command()),
339                -1,
340                "",
341                stderr,
342            ));
343        }
344
345        Ok(())
346    }
347
348    /// Get runtime-specific environment variables
349    #[must_use]
350    pub fn environment_vars(&self) -> Vec<(String, String)> {
351        let mut vars = Vec::new();
352
353        // Add socket path if needed
354        if self.socket_path.exists() {
355            vars.push((
356                "DOCKER_HOST".to_string(),
357                format!("unix://{}", self.socket_path.display()),
358            ));
359        }
360
361        // Add runtime-specific vars
362        match self.runtime {
363            Runtime::Podman => {
364                vars.push(("DOCKER_BUILDKIT".to_string(), "0".to_string()));
365            }
366            Runtime::DockerDesktop | Runtime::Docker => {
367                vars.push(("DOCKER_BUILDKIT".to_string(), "1".to_string()));
368            }
369            _ => {}
370        }
371
372        vars
373    }
374}
375
376impl std::fmt::Display for PlatformInfo {
377    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
378        write!(
379            f,
380            "{} on {} (version: {})",
381            self.runtime, self.platform, self.version
382        )?;
383        if self.is_wsl {
384            write!(f, " [WSL]")?;
385        }
386        Ok(())
387    }
388}
389
390#[cfg(test)]
391mod tests {
392    use super::*;
393
394    #[test]
395    fn test_platform_detection() {
396        let platform = Platform::detect();
397        // Should detect something
398        assert!(matches!(
399            platform,
400            Platform::Linux
401                | Platform::MacOS
402                | Platform::Windows
403                | Platform::FreeBSD
404                | Platform::Other(_)
405        ));
406    }
407
408    #[test]
409    fn test_runtime_command() {
410        assert_eq!(Runtime::Docker.command(), "docker");
411        assert_eq!(Runtime::Podman.command(), "podman");
412        assert_eq!(Runtime::Colima.command(), "docker");
413    }
414
415    #[test]
416    fn test_runtime_compose_support() {
417        assert!(Runtime::Docker.supports_compose());
418        assert!(Runtime::DockerDesktop.supports_compose());
419        assert!(Runtime::Colima.supports_compose());
420        assert!(!Runtime::Podman.supports_compose());
421    }
422
423    #[test]
424    fn test_platform_display() {
425        assert_eq!(Platform::Linux.to_string(), "Linux");
426        assert_eq!(Platform::MacOS.to_string(), "macOS");
427        assert_eq!(Platform::Windows.to_string(), "Windows");
428    }
429
430    #[test]
431    fn test_runtime_display() {
432        assert_eq!(Runtime::Docker.to_string(), "Docker");
433        assert_eq!(Runtime::Podman.to_string(), "Podman");
434        assert_eq!(Runtime::OrbStack.to_string(), "OrbStack");
435    }
436}