mirror of
https://github.com/NVIDIA/OpenShell.git
synced 2026-10-02 07:34:45 +08:00
fix(gateway): try harder to detect Podman (#1536)
Auto-detection previously treated Podman as available only when the podman CLI was visible on PATH. However, package manager services can run with a restricted PATH, which lets Docker be selected even when a Podman API socket is reachable. Additionally, podman may symlink /var/run/docker.sock to podman's machine unix socket, which would be incorrectly detected as Docker. Worse still: the podman machine may not even be running. This replaces the Podman binary check with a functional HTTP probe against the standard Podman socket paths. The probe requires /_ping to answer with a Libpod-Api-Version header before treating the socket as Podman, which lets the gateway select the embedded Podman driver only when the API is usable. Signed-off-by: Kris Hicks <khicks@nvidia.com>
This commit is contained in:
@@ -5,12 +5,16 @@
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fmt;
|
||||
#[cfg(unix)]
|
||||
use std::io::{Read, Write};
|
||||
use std::net::SocketAddr;
|
||||
#[cfg(unix)]
|
||||
use std::os::unix::fs::FileTypeExt;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::Command;
|
||||
use std::str::FromStr;
|
||||
#[cfg(unix)]
|
||||
use std::time::Duration;
|
||||
|
||||
// ── Public default constants ────────────────────────────────────────────
|
||||
//
|
||||
@@ -101,8 +105,8 @@ pub fn detect_driver() -> Option<ComputeDriverKind> {
|
||||
return Some(ComputeDriverKind::Kubernetes);
|
||||
}
|
||||
|
||||
// Podman: check if podman binary is available
|
||||
if is_binary_available("podman") {
|
||||
// Podman: check for a reachable local API socket.
|
||||
if is_podman_available() {
|
||||
return Some(ComputeDriverKind::Podman);
|
||||
}
|
||||
|
||||
@@ -122,6 +126,54 @@ fn is_binary_available(name: &str) -> bool {
|
||||
.is_ok_and(|output| output.status.success())
|
||||
}
|
||||
|
||||
fn is_podman_available() -> bool {
|
||||
podman_socket_candidates()
|
||||
.iter()
|
||||
.any(|path| podman_socket_responds(path))
|
||||
}
|
||||
|
||||
fn podman_socket_candidates() -> Vec<PathBuf> {
|
||||
let socket = std::env::var("OPENSHELL_PODMAN_SOCKET")
|
||||
.ok()
|
||||
.filter(|path| !path.trim().is_empty())
|
||||
.map(PathBuf::from);
|
||||
podman_socket_candidates_from_env(
|
||||
socket,
|
||||
std::env::var_os("XDG_RUNTIME_DIR").map(PathBuf::from),
|
||||
std::env::var_os("HOME").map(PathBuf::from),
|
||||
)
|
||||
}
|
||||
|
||||
fn podman_socket_candidates_from_env(
|
||||
socket: Option<PathBuf>,
|
||||
runtime_dir: Option<PathBuf>,
|
||||
home: Option<PathBuf>,
|
||||
) -> Vec<PathBuf> {
|
||||
let mut candidates = Vec::new();
|
||||
|
||||
if let Some(path) = socket {
|
||||
candidates.push(path);
|
||||
}
|
||||
|
||||
if let Some(runtime_dir) = runtime_dir {
|
||||
candidates.push(runtime_dir.join("podman/podman.sock"));
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
candidates.push(PathBuf::from(format!(
|
||||
"/run/user/{}/podman/podman.sock",
|
||||
current_uid()
|
||||
)));
|
||||
}
|
||||
|
||||
if let Some(home) = home {
|
||||
candidates.push(home.join(".local/share/containers/podman/machine/podman.sock"));
|
||||
}
|
||||
|
||||
candidates
|
||||
}
|
||||
|
||||
fn is_docker_available() -> bool {
|
||||
is_binary_available("docker") || docker_socket_available()
|
||||
}
|
||||
@@ -165,12 +217,86 @@ fn is_unix_socket(path: &Path) -> bool {
|
||||
.is_ok_and(|metadata| metadata.file_type().is_socket())
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
fn podman_socket_responds(path: &Path) -> bool {
|
||||
unix_socket_http_ping(path, |response| {
|
||||
http_response_is_success(response) && contains_ascii(response, b"Libpod-Api-Version:")
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
fn unix_socket_http_ping(path: &Path, accepts_response: impl FnOnce(&[u8]) -> bool) -> bool {
|
||||
const PROBE_TIMEOUT: Duration = Duration::from_secs(1);
|
||||
const PING_REQUEST: &[u8] =
|
||||
b"GET /_ping HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n";
|
||||
|
||||
if !is_unix_socket(path) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let Ok(mut stream) = std::os::unix::net::UnixStream::connect(path) else {
|
||||
return false;
|
||||
};
|
||||
if stream.set_read_timeout(Some(PROBE_TIMEOUT)).is_err()
|
||||
|| stream.set_write_timeout(Some(PROBE_TIMEOUT)).is_err()
|
||||
|| stream.write_all(PING_REQUEST).is_err()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
let mut response = [0_u8; 512];
|
||||
let mut total = 0;
|
||||
while total < response.len() {
|
||||
let Ok(n) = stream.read(&mut response[total..]) else {
|
||||
return false;
|
||||
};
|
||||
if n == 0 {
|
||||
break;
|
||||
}
|
||||
total += n;
|
||||
if contains_ascii(&response[..total], b"\r\n\r\n") {
|
||||
break;
|
||||
}
|
||||
}
|
||||
total > 0 && accepts_response(&response[..total])
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
fn http_response_is_success(response: &[u8]) -> bool {
|
||||
response.starts_with(b"HTTP/1.1 200") || response.starts_with(b"HTTP/1.0 200")
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
fn contains_ascii(haystack: &[u8], needle: &[u8]) -> bool {
|
||||
haystack
|
||||
.windows(needle.len())
|
||||
.any(|window| window.eq_ignore_ascii_case(needle))
|
||||
}
|
||||
|
||||
#[cfg(all(unix, test))]
|
||||
fn is_reachable_unix_socket(path: &Path) -> bool {
|
||||
is_unix_socket(path) && std::os::unix::net::UnixStream::connect(path).is_ok()
|
||||
}
|
||||
|
||||
#[cfg(all(unix, target_os = "linux"))]
|
||||
fn current_uid() -> u32 {
|
||||
use std::os::unix::fs::MetadataExt;
|
||||
|
||||
std::fs::metadata("/proc/self").map_or(0, |metadata| metadata.uid())
|
||||
}
|
||||
|
||||
#[cfg(not(unix))]
|
||||
fn is_unix_socket(path: &Path) -> bool {
|
||||
let _ = path;
|
||||
false
|
||||
}
|
||||
|
||||
#[cfg(not(unix))]
|
||||
fn podman_socket_responds(path: &Path) -> bool {
|
||||
let _ = path;
|
||||
false
|
||||
}
|
||||
|
||||
/// Server configuration.
|
||||
///
|
||||
/// Built programmatically in [`crate::Config::new`] and the gateway CLI from
|
||||
@@ -597,10 +723,15 @@ const fn default_ssh_session_ttl_secs() -> u64 {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#[cfg(unix)]
|
||||
use super::is_reachable_unix_socket;
|
||||
use super::{
|
||||
ComputeDriverKind, Config, DEFAULT_SERVICE_ROUTING_DOMAIN, detect_driver,
|
||||
docker_host_unix_socket_path, is_unix_socket,
|
||||
docker_host_unix_socket_path, is_unix_socket, podman_socket_candidates_from_env,
|
||||
podman_socket_responds,
|
||||
};
|
||||
#[cfg(unix)]
|
||||
use std::io::{Read as _, Write as _};
|
||||
use std::net::SocketAddr;
|
||||
#[cfg(unix)]
|
||||
use std::os::unix::net::UnixListener;
|
||||
@@ -704,9 +835,10 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn detect_driver_returns_none_without_k8s_env_or_binaries() {
|
||||
// When KUBERNETES_SERVICE_HOST is not set and no docker/podman binaries
|
||||
// or Docker socket are available, detect_driver should return None.
|
||||
fn detect_driver_returns_none_without_k8s_env_or_local_runtime() {
|
||||
// When KUBERNETES_SERVICE_HOST is not set, no Docker binary/socket is
|
||||
// available, and no Podman API socket is available, detect_driver
|
||||
// should return None.
|
||||
// This test may pass or fail depending on the test environment,
|
||||
// but it documents the expected behavior.
|
||||
let _ = detect_driver(); // Returns Some or None based on environment
|
||||
@@ -730,10 +862,73 @@ mod tests {
|
||||
let _listener = UnixListener::bind(&socket_path).expect("bind unix socket");
|
||||
|
||||
assert!(is_unix_socket(&socket_path));
|
||||
assert!(is_reachable_unix_socket(&socket_path));
|
||||
|
||||
let regular_file = temp_dir.path().join("not-a-socket");
|
||||
std::fs::write(®ular_file, b"not a socket").expect("write regular file");
|
||||
assert!(!is_unix_socket(®ular_file));
|
||||
assert!(!is_reachable_unix_socket(®ular_file));
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
fn podman_socket_probe_accepts_successful_ping_response() {
|
||||
let temp_dir = tempfile::tempdir().expect("create temp dir");
|
||||
let socket_path = temp_dir.path().join("podman.sock");
|
||||
let listener = UnixListener::bind(&socket_path).expect("bind podman socket");
|
||||
|
||||
let handle = std::thread::spawn(move || {
|
||||
let (mut stream, _) = listener.accept().expect("accept podman probe");
|
||||
let mut request = [0_u8; 128];
|
||||
let n = stream.read(&mut request).expect("read podman probe");
|
||||
assert!(request[..n].starts_with(b"GET /_ping HTTP/1.1\r\n"));
|
||||
stream
|
||||
.write_all(
|
||||
b"HTTP/1.1 200 OK\r\nLibpod-Api-Version: 5.8.2\r\nContent-Length: 2\r\n\r\nOK",
|
||||
)
|
||||
.expect("write podman ping response");
|
||||
});
|
||||
|
||||
assert!(podman_socket_responds(&socket_path));
|
||||
handle.join().expect("probe server exits");
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
fn podman_socket_probe_rejects_docker_ping_response() {
|
||||
let temp_dir = tempfile::tempdir().expect("create temp dir");
|
||||
let socket_path = temp_dir.path().join("podman.sock");
|
||||
let listener = UnixListener::bind(&socket_path).expect("bind podman socket");
|
||||
|
||||
let handle = std::thread::spawn(move || {
|
||||
let (mut stream, _) = listener.accept().expect("accept podman probe");
|
||||
let mut request = [0_u8; 128];
|
||||
let n = stream.read(&mut request).expect("read podman probe");
|
||||
assert!(request[..n].starts_with(b"GET /_ping HTTP/1.1\r\n"));
|
||||
stream
|
||||
.write_all(
|
||||
b"HTTP/1.1 200 OK\r\nServer: Docker/29.2.1\r\nContent-Length: 2\r\n\r\nOK",
|
||||
)
|
||||
.expect("write docker ping response");
|
||||
});
|
||||
|
||||
assert!(!podman_socket_responds(&socket_path));
|
||||
handle.join().expect("probe server exits");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn podman_socket_candidates_include_env_runtime_and_home_paths() {
|
||||
let candidates = podman_socket_candidates_from_env(
|
||||
Some(PathBuf::from("/tmp/custom-podman.sock")),
|
||||
Some(PathBuf::from("/tmp/runtime")),
|
||||
Some(PathBuf::from("/tmp/home")),
|
||||
);
|
||||
|
||||
assert!(candidates.contains(&PathBuf::from("/tmp/custom-podman.sock")));
|
||||
assert!(candidates.contains(&PathBuf::from("/tmp/runtime/podman/podman.sock")));
|
||||
assert!(candidates.contains(&PathBuf::from(
|
||||
"/tmp/home/.local/share/containers/podman/machine/podman.sock"
|
||||
)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user