Skip to main content

rightsize_docker/
provider.rs

1//! [`DockerBackendProvider`]: the discoverable factory `rightsize::backends::resolve`
2//! picks among. Supported exactly when `GET /_ping` succeeds against the daemon this
3//! process would otherwise talk to — no Docker/Podman/Colima running is the common
4//! "not supported" case on a microVM-capable host that just prefers msb anyway.
5
6use rightsize::backend::{BackendProvider, SandboxBackend};
7use rightsize::error::Result;
8
9use crate::backend::DockerBackend;
10use crate::client::DockerClient;
11
12/// The Docker backend's [`BackendProvider`]. Priority 10 — lower than msb's 20, so a
13/// microVM-capable host prefers the microVM by default; Docker is the fallback for
14/// hosts without one (Intel Macs, Windows, no `/dev/kvm`), and doubles as the
15/// contract suite's correctness oracle.
16pub struct DockerBackendProvider;
17
18impl BackendProvider for DockerBackendProvider {
19    fn name(&self) -> &str {
20        "docker"
21    }
22
23    fn priority(&self) -> u32 {
24        10
25    }
26
27    fn is_supported(&self) -> bool {
28        // `BackendProvider::is_supported` is a plain synchronous fn — it can run
29        // before any Tokio runtime exists yet (early process startup), or from
30        // *inside* one already (a `#[tokio::test]` resolving a backend), and
31        // `Runtime::block_on` panics in the latter case ("cannot start a runtime from
32        // within a runtime"). Rather than juggle "is there already a runtime" logic,
33        // this probe is a plain blocking `std::os::unix::net::UnixStream` GET
34        // /_ping — the same blocking-transport shape `DockerBackend::cleanup_sync`
35        // already uses for its own no-Tokio-in-context constraint, and the honest fit
36        // for a synchronous trait method that must work in either context.
37        blocking_ping(DockerClient::from_env().socket_path())
38    }
39
40    fn unsupported_reason(&self) -> String {
41        "no reachable Docker-API socket (Docker/Podman/Colima not running?)".to_string()
42    }
43
44    fn create(&self) -> Result<Box<dyn SandboxBackend>> {
45        Ok(Box::new(DockerBackend::connecting_to_env()))
46    }
47}
48
49/// A minimal blocking `GET /_ping`, true only on a 2xx response — see
50/// [`DockerBackendProvider::is_supported`]'s doc for why this is blocking std I/O
51/// rather than a reused async request.
52fn blocking_ping(socket_path: &std::path::Path) -> bool {
53    use std::io::{Read, Write};
54    use std::os::unix::net::UnixStream;
55    use std::time::Duration;
56
57    let probe = || -> std::io::Result<bool> {
58        let mut stream = UnixStream::connect(socket_path)?;
59        stream.set_read_timeout(Some(Duration::from_secs(2)))?;
60        stream.set_write_timeout(Some(Duration::from_secs(2)))?;
61        stream.write_all(b"GET /_ping HTTP/1.1\r\nHost: docker\r\nConnection: close\r\n\r\n")?;
62        let mut response = Vec::new();
63        let _ = stream.read_to_end(&mut response);
64        let text = String::from_utf8_lossy(&response);
65        let status_line = text.lines().next().unwrap_or_default();
66        Ok(status_line
67            .split_whitespace()
68            .nth(1)
69            .and_then(|code| code.parse::<u16>().ok())
70            .map(|code| (200..300).contains(&code))
71            .unwrap_or(false))
72    };
73    probe().unwrap_or(false)
74}
75
76#[cfg(test)]
77mod tests {
78    use super::*;
79
80    #[test]
81    fn name_and_priority_are_pinned() {
82        let provider = DockerBackendProvider;
83        assert_eq!(provider.name(), "docker");
84        assert_eq!(provider.priority(), 10);
85    }
86
87    #[test]
88    fn unsupported_reason_names_the_daemon_socket() {
89        let provider = DockerBackendProvider;
90        let reason = provider.unsupported_reason();
91        assert!(reason.to_lowercase().contains("docker"));
92    }
93}