Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
213 changes: 207 additions & 6 deletions crates/openshell-core/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 ────────────────────────────────────────────
//
Expand Down Expand Up @@ -96,8 +100,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);
}

Expand All @@ -117,6 +121,58 @@ fn is_binary_available(name: &str) -> bool {
.is_ok_and(|output| output.status.success())
}

fn is_podman_available() -> bool {
podman_socket_available()
}

fn podman_socket_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()
}
Expand Down Expand Up @@ -160,12 +216,88 @@ 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(|metadata| metadata.uid())
.unwrap_or(0)
}

#[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.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Config {
Expand Down Expand Up @@ -593,10 +725,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;
Expand Down Expand Up @@ -700,9 +837,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
Expand All @@ -726,10 +864,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(&regular_file, b"not a socket").expect("write regular file");
assert!(!is_unix_socket(&regular_file));
assert!(!is_reachable_unix_socket(&regular_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]
Expand Down
Loading