Skip to content

Commit 919e818

Browse files
committed
fix(driver-podman): resolve Podman socket via auto-detection
Align Podman socket selection with the existing Docker model: explicit config wins, otherwise probe openshell-core for a responsive socket. This also fixes the original HOME-unset panic, since resolution no longer hardcodes a per-OS default path. - add detect_podman_socket() in openshell-core, mirroring detect_docker_socket - PodmanComputeConfig.socket_path is now Option<PathBuf>, no default - remove default_socket_path() (podman driver) and podman_socket_path() (vm driver), both replaced by the shared detector - update server env override and CLI for the new Option type - add tests: responsive-candidate detection in openshell-core, and config-error (not panic) when no socket is configured or reachable
1 parent d0961cd commit 919e818

8 files changed

Lines changed: 132 additions & 120 deletions

File tree

crates/openshell-core/src/config.rs

Lines changed: 43 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -171,9 +171,19 @@ pub fn detect_driver() -> Option<ComputeDriverKind> {
171171
}
172172

173173
fn is_podman_available() -> bool {
174-
podman_socket_candidates()
174+
detect_podman_socket().is_some()
175+
}
176+
177+
/// Return the first responsive Podman API socket, or `None` if none respond.
178+
pub fn detect_podman_socket() -> Option<PathBuf> {
179+
detect_podman_socket_from_candidates(&podman_socket_candidates())
180+
}
181+
182+
fn detect_podman_socket_from_candidates(candidates: &[PathBuf]) -> Option<PathBuf> {
183+
candidates
175184
.iter()
176-
.any(|path| podman_socket_responds(path))
185+
.find(|path| podman_socket_responds(path))
186+
.cloned()
177187
}
178188

179189
fn podman_socket_candidates() -> Vec<PathBuf> {
@@ -972,8 +982,9 @@ mod tests {
972982
ComputeDriverKind, Config, DEFAULT_SERVICE_ROUTING_DOMAIN, GatewayInterceptorBindingPolicy,
973983
GatewayInterceptorConfig, GatewayInterceptorFailurePolicy, GatewayJwtConfig,
974984
GatewayProviderProfileSourceConfig, detect_docker_socket_from_candidates, detect_driver,
975-
docker_host_unix_socket_path, docker_socket_responds, is_unix_socket,
976-
normalize_compute_driver_name, podman_socket_candidates_from_env, podman_socket_responds,
985+
detect_podman_socket_from_candidates, docker_host_unix_socket_path, docker_socket_responds,
986+
is_unix_socket, normalize_compute_driver_name, podman_socket_candidates_from_env,
987+
podman_socket_responds,
977988
};
978989
#[cfg(unix)]
979990
use std::io::{Read as _, Write as _};
@@ -1349,6 +1360,34 @@ mod tests {
13491360
)));
13501361
}
13511362

1363+
#[cfg(unix)]
1364+
#[test]
1365+
fn podman_socket_detection_returns_the_responsive_candidate() {
1366+
let temp_dir = tempfile::tempdir().expect("create temp dir");
1367+
let inactive_path = temp_dir.path().join("inactive.sock");
1368+
let inactive_listener = UnixListener::bind(&inactive_path).expect("bind inactive socket");
1369+
drop(inactive_listener);
1370+
1371+
let responsive_path = temp_dir.path().join("responsive.sock");
1372+
let listener = UnixListener::bind(&responsive_path).expect("bind responsive socket");
1373+
let handle = std::thread::spawn(move || {
1374+
let (mut stream, _) = listener.accept().expect("accept podman probe");
1375+
let mut request = [0_u8; 128];
1376+
let _ = stream.read(&mut request).expect("read podman probe");
1377+
stream
1378+
.write_all(
1379+
b"HTTP/1.1 200 OK\r\nLibpod-Api-Version: 5.8.2\r\nContent-Length: 2\r\n\r\nOK",
1380+
)
1381+
.expect("write podman ping response");
1382+
});
1383+
1384+
assert_eq!(
1385+
detect_podman_socket_from_candidates(&[inactive_path, responsive_path.clone(),]),
1386+
Some(responsive_path)
1387+
);
1388+
handle.join().expect("probe server exits");
1389+
}
1390+
13521391
#[test]
13531392
#[allow(unsafe_code)] // std::env::set_var/remove_var require unsafe in Rust 2024
13541393
fn detect_driver_prefers_kubernetes_when_k8s_env_is_set() {

crates/openshell-driver-podman/src/config.rs

Lines changed: 6 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -70,9 +70,11 @@ impl FromStr for ImagePullPolicy {
7070
#[serde(default, deny_unknown_fields)]
7171
pub struct PodmanComputeConfig {
7272
/// Path to the Podman API Unix socket.
73-
/// Default: `$XDG_RUNTIME_DIR/podman/podman.sock` (Linux),
74-
/// `$HOME/.local/share/containers/podman/machine/podman.sock` (macOS).
75-
pub socket_path: PathBuf,
73+
///
74+
/// `None` means auto-detect: the driver probes
75+
/// [`openshell_core::config::detect_podman_socket`] for the first
76+
/// responsive candidate when it starts.
77+
pub socket_path: Option<PathBuf>,
7678
/// Default OCI image for sandboxes.
7779
pub default_image: String,
7880
/// Image pull policy for sandbox images.
@@ -215,38 +217,12 @@ impl PodmanComputeConfig {
215217
String::new()
216218
}
217219
}
218-
219-
/// Resolve the default socket path from the environment.
220-
///
221-
/// - **macOS**: `$HOME/.local/share/containers/podman/machine/podman.sock`
222-
/// (the symlink created by `podman machine` pointing to the VM API socket).
223-
/// - **Linux**: `$XDG_RUNTIME_DIR/podman/podman.sock` when set (by
224-
/// `pam_systemd`/logind), otherwise `/run/user/{uid}/podman/podman.sock`
225-
/// using the real UID via `getuid()`.
226-
#[must_use]
227-
pub fn default_socket_path() -> PathBuf {
228-
#[cfg(target_os = "macos")]
229-
{
230-
let home = std::env::var("HOME").expect("HOME must be set on macOS");
231-
PathBuf::from(home).join(".local/share/containers/podman/machine/podman.sock")
232-
}
233-
#[cfg(target_os = "linux")]
234-
{
235-
std::env::var("XDG_RUNTIME_DIR").map_or_else(
236-
|_| {
237-
let uid = rustix::process::getuid().as_raw();
238-
PathBuf::from(format!("/run/user/{uid}/podman/podman.sock"))
239-
},
240-
|xdg| PathBuf::from(xdg).join("podman/podman.sock"),
241-
)
242-
}
243-
}
244220
}
245221

246222
impl Default for PodmanComputeConfig {
247223
fn default() -> Self {
248224
Self {
249-
socket_path: Self::default_socket_path(),
225+
socket_path: None,
250226
default_image: openshell_core::image::default_sandbox_image(),
251227
image_pull_policy: ImagePullPolicy::default(),
252228
grpc_endpoint: String::new(),
@@ -296,39 +272,6 @@ impl std::fmt::Debug for PodmanComputeConfig {
296272
mod tests {
297273
use super::*;
298274

299-
/// Serialises env-mutating tests so that parallel test threads cannot
300-
/// observe each other's changes to `XDG_RUNTIME_DIR`.
301-
static ENV_LOCK: std::sync::LazyLock<std::sync::Mutex<()>> =
302-
std::sync::LazyLock::new(|| std::sync::Mutex::new(()));
303-
304-
#[test]
305-
#[cfg(target_os = "linux")]
306-
fn default_socket_path_respects_xdg_runtime_dir() {
307-
let _guard = ENV_LOCK
308-
.lock()
309-
.unwrap_or_else(std::sync::PoisonError::into_inner);
310-
temp_env::with_vars([("XDG_RUNTIME_DIR", Some("/tmp/test-xdg"))], || {
311-
let path = PodmanComputeConfig::default_socket_path();
312-
assert_eq!(path, PathBuf::from("/tmp/test-xdg/podman/podman.sock"));
313-
});
314-
}
315-
316-
#[test]
317-
#[cfg(target_os = "linux")]
318-
fn default_socket_path_falls_back_to_uid() {
319-
let _guard = ENV_LOCK
320-
.lock()
321-
.unwrap_or_else(std::sync::PoisonError::into_inner);
322-
temp_env::with_vars([("XDG_RUNTIME_DIR", None::<&str>)], || {
323-
let path = PodmanComputeConfig::default_socket_path();
324-
let uid = rustix::process::getuid().as_raw();
325-
assert_eq!(
326-
path,
327-
PathBuf::from(format!("/run/user/{uid}/podman/podman.sock"))
328-
);
329-
});
330-
}
331-
332275
#[test]
333276
fn default_config_sets_health_check_interval() {
334277
let cfg = PodmanComputeConfig::default();
@@ -382,21 +325,6 @@ mod tests {
382325
assert!(err.to_string().contains("sandbox_pids_limit"));
383326
}
384327

385-
#[test]
386-
#[cfg(target_os = "macos")]
387-
fn default_socket_path_uses_podman_machine_on_macos() {
388-
let _guard = ENV_LOCK
389-
.lock()
390-
.unwrap_or_else(std::sync::PoisonError::into_inner);
391-
temp_env::with_vars([("HOME", Some("/Users/testuser"))], || {
392-
let path = PodmanComputeConfig::default_socket_path();
393-
assert_eq!(
394-
path,
395-
PathBuf::from("/Users/testuser/.local/share/containers/podman/machine/podman.sock")
396-
);
397-
});
398-
}
399-
400328
// ── TLS config validation ─────────────────────────────────────────
401329

402330
#[test]

crates/openshell-driver-podman/src/container.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1808,7 +1808,7 @@ mod tests {
18081808

18091809
fn test_config() -> PodmanComputeConfig {
18101810
PodmanComputeConfig {
1811-
socket_path: std::path::PathBuf::from("/tmp/test.sock"),
1811+
socket_path: Some(std::path::PathBuf::from("/tmp/test.sock")),
18121812
default_image: "test-image:latest".to_string(),
18131813
grpc_endpoint: "http://localhost:50051".to_string(),
18141814
host_gateway_ip: String::new(),

crates/openshell-driver-podman/src/driver.rs

Lines changed: 76 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -163,16 +163,32 @@ impl PodmanComputeDriver {
163163
const MAX_PING_RETRIES: u32 = 5;
164164
const PING_RETRY_DELAY: Duration = Duration::from_secs(2);
165165

166-
if !config.socket_path.exists() {
166+
// Explicit configuration wins; otherwise probe for a responsive socket.
167+
// Unlike Docker, Podman has no single well-known default path, so
168+
// there is no further fallback if neither resolves.
169+
let socket_path = config
170+
.socket_path
171+
.clone()
172+
.or_else(openshell_core::config::detect_podman_socket)
173+
.ok_or_else(|| {
174+
PodmanApiError::InvalidInput(
175+
"no responsive Podman API socket found; set OPENSHELL_PODMAN_SOCKET \
176+
or configure socket_path"
177+
.to_string(),
178+
)
179+
})?;
180+
config.socket_path = Some(socket_path.clone());
181+
182+
if !socket_path.exists() {
167183
if cfg!(target_os = "macos") {
168184
warn!(
169-
path = %config.socket_path.display(),
185+
path = %socket_path.display(),
170186
"Podman socket not found; is podman machine running? \
171187
Try `podman machine start` or set OPENSHELL_PODMAN_SOCKET to override."
172188
);
173189
} else {
174190
warn!(
175-
path = %config.socket_path.display(),
191+
path = %socket_path.display(),
176192
"Podman socket not found; is the Podman service running? \
177193
Set OPENSHELL_PODMAN_SOCKET or XDG_RUNTIME_DIR to override."
178194
);
@@ -186,7 +202,7 @@ impl PodmanComputeDriver {
186202
config.validate_runtime_limits()?;
187203
config.validate_host_gateway_ip()?;
188204

189-
let client = PodmanClient::new(config.socket_path.clone());
205+
let client = PodmanClient::new(socket_path);
190206

191207
// Verify connectivity, retrying briefly to tolerate transient socket
192208
// unavailability (e.g. podman.socket restarting after a package
@@ -774,7 +790,7 @@ impl PodmanComputeDriver {
774790
gpu_inventory: CdiGpuInventory,
775791
allow_all_default_gpu: bool,
776792
) -> Self {
777-
let client = PodmanClient::new(config.socket_path.clone());
793+
let client = PodmanClient::new(config.socket_path.clone().unwrap_or_default());
778794
let refresh_inventory = gpu_inventory.clone();
779795
Self {
780796
client,
@@ -850,6 +866,59 @@ mod tests {
850866
use std::fs;
851867
use std::path::PathBuf;
852868

869+
// ── socket resolution ───────────────────────────────────────────────
870+
871+
/// Restores env vars on drop, even if the test body panics.
872+
struct EnvVarGuard(Vec<(&'static str, Option<String>)>);
873+
874+
impl EnvVarGuard {
875+
#[allow(unsafe_code)] // std::env::set_var requires unsafe in Rust 2024
876+
fn set(vars: &[(&'static str, &str)]) -> Self {
877+
let saved = vars
878+
.iter()
879+
.map(|(name, value)| {
880+
let previous = std::env::var(name).ok();
881+
unsafe { std::env::set_var(name, value) };
882+
(*name, previous)
883+
})
884+
.collect();
885+
Self(saved)
886+
}
887+
}
888+
889+
impl Drop for EnvVarGuard {
890+
#[allow(unsafe_code)] // std::env::set_var/remove_var require unsafe in Rust 2024
891+
fn drop(&mut self) {
892+
for (name, previous) in &self.0 {
893+
unsafe {
894+
match previous {
895+
Some(value) => std::env::set_var(name, value),
896+
None => std::env::remove_var(name),
897+
}
898+
}
899+
}
900+
}
901+
}
902+
903+
#[tokio::test]
904+
async fn new_returns_config_error_when_no_socket_is_configured_or_detected() {
905+
let _guard = EnvVarGuard::set(&[
906+
("OPENSHELL_PODMAN_SOCKET", "/nonexistent/podman.sock"),
907+
("XDG_RUNTIME_DIR", "/nonexistent/xdg"),
908+
("HOME", "/nonexistent/home"),
909+
]);
910+
911+
let config = PodmanComputeConfig {
912+
socket_path: None,
913+
..PodmanComputeConfig::default()
914+
};
915+
let err = PodmanComputeDriver::new(config)
916+
.await
917+
.expect_err("no socket is configured or reachable");
918+
919+
assert!(err.to_string().contains("no responsive Podman API socket"));
920+
}
921+
853922
fn cdi_devices_config(device_ids: &[&str]) -> prost_types::Struct {
854923
prost_types::Struct {
855924
fields: std::iter::once((
@@ -1245,7 +1314,7 @@ mod tests {
12451314

12461315
fn test_driver(socket_path: PathBuf) -> PodmanComputeDriver {
12471316
let config = PodmanComputeConfig {
1248-
socket_path,
1317+
socket_path: Some(socket_path),
12491318
stop_timeout_secs: 10,
12501319
..PodmanComputeConfig::default()
12511320
};
@@ -1453,7 +1522,7 @@ mod tests {
14531522
)],
14541523
);
14551524
let config = PodmanComputeConfig {
1456-
socket_path: socket_path.clone(),
1525+
socket_path: Some(socket_path.clone()),
14571526
enable_bind_mounts: true,
14581527
..PodmanComputeConfig::default()
14591528
};

crates/openshell-driver-podman/src/grpc.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -179,7 +179,7 @@ mod tests {
179179

180180
fn test_service(socket_path: PathBuf) -> ComputeDriverService {
181181
let config = PodmanComputeConfig {
182-
socket_path,
182+
socket_path: Some(socket_path),
183183
stop_timeout_secs: 10,
184184
..PodmanComputeConfig::default()
185185
};

crates/openshell-driver-podman/src/main.rs

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -114,12 +114,8 @@ async fn main() -> Result<()> {
114114
)
115115
.init();
116116

117-
let socket_path = args
118-
.podman_socket
119-
.unwrap_or_else(PodmanComputeConfig::default_socket_path);
120-
121117
let driver = PodmanComputeDriver::new(PodmanComputeConfig {
122-
socket_path,
118+
socket_path: args.podman_socket,
123119
default_image: args.sandbox_image.unwrap_or_default(),
124120
image_pull_policy: args.sandbox_image_pull_policy,
125121
grpc_endpoint: args.grpc_endpoint.unwrap_or_default(),

crates/openshell-driver-vm/src/driver.rs

Lines changed: 3 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -3270,10 +3270,9 @@ async fn connect_local_container_engine() -> Option<Docker> {
32703270
return Some(docker);
32713271
}
32723272

3273-
let podman_socket = podman_socket_path();
3274-
if podman_socket.exists()
3275-
&& let Ok(docker) =
3276-
Docker::connect_with_unix(podman_socket.to_str()?, 120, bollard::API_DEFAULT_VERSION)
3273+
let podman_socket = openshell_core::config::detect_podman_socket()?;
3274+
if let Ok(docker) =
3275+
Docker::connect_with_unix(podman_socket.to_str()?, 120, bollard::API_DEFAULT_VERSION)
32773276
&& docker.ping().await.is_ok()
32783277
{
32793278
info!(
@@ -3286,25 +3285,6 @@ async fn connect_local_container_engine() -> Option<Docker> {
32863285
None
32873286
}
32883287

3289-
/// Podman user socket path for the current platform.
3290-
fn podman_socket_path() -> PathBuf {
3291-
#[cfg(target_os = "macos")]
3292-
{
3293-
let home = std::env::var("HOME").unwrap_or_default();
3294-
PathBuf::from(home).join(".local/share/containers/podman/machine/podman.sock")
3295-
}
3296-
#[cfg(target_os = "linux")]
3297-
{
3298-
std::env::var("XDG_RUNTIME_DIR").map_or_else(
3299-
|_| {
3300-
let uid = nix::unistd::getuid();
3301-
PathBuf::from(format!("/run/user/{uid}/podman/podman.sock"))
3302-
},
3303-
|xdg| PathBuf::from(xdg).join("podman/podman.sock"),
3304-
)
3305-
}
3306-
}
3307-
33083288
fn is_openshell_local_build_image_ref(image_ref: &str) -> bool {
33093289
image_ref.starts_with("openshell/sandbox-from:")
33103290
}

crates/openshell-server/src/compute/driver_config.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -195,7 +195,7 @@ fn apply_vm_runtime_defaults(cfg: &mut VmComputeConfig, context: DriverStartupCo
195195

196196
fn apply_podman_env_overrides(podman: &mut PodmanComputeConfig) {
197197
if let Ok(p) = std::env::var("OPENSHELL_PODMAN_SOCKET") {
198-
podman.socket_path = PathBuf::from(p);
198+
podman.socket_path = Some(PathBuf::from(p));
199199
}
200200
if let Ok(ip) = std::env::var("OPENSHELL_PODMAN_HOST_GATEWAY_IP") {
201201
podman.host_gateway_ip = ip;

0 commit comments

Comments
 (0)