Skip to content
Open
Show file tree
Hide file tree
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
47 changes: 43 additions & 4 deletions crates/openshell-core/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -171,9 +171,19 @@ pub fn detect_driver() -> Option<ComputeDriverKind> {
}

fn is_podman_available() -> bool {
podman_socket_candidates()
detect_podman_socket().is_some()
}

/// Return the first responsive Podman API socket, or `None` if none respond.
pub fn detect_podman_socket() -> Option<PathBuf> {
detect_podman_socket_from_candidates(&podman_socket_candidates())
}

fn detect_podman_socket_from_candidates(candidates: &[PathBuf]) -> Option<PathBuf> {
candidates
.iter()
.any(|path| podman_socket_responds(path))
.find(|path| podman_socket_responds(path))
.cloned()
}

fn podman_socket_candidates() -> Vec<PathBuf> {
Expand Down Expand Up @@ -972,8 +982,9 @@ mod tests {
ComputeDriverKind, Config, DEFAULT_SERVICE_ROUTING_DOMAIN, GatewayInterceptorBindingPolicy,
GatewayInterceptorConfig, GatewayInterceptorFailurePolicy, GatewayJwtConfig,
GatewayProviderProfileSourceConfig, detect_docker_socket_from_candidates, detect_driver,
docker_host_unix_socket_path, docker_socket_responds, is_unix_socket,
normalize_compute_driver_name, podman_socket_candidates_from_env, podman_socket_responds,
detect_podman_socket_from_candidates, docker_host_unix_socket_path, docker_socket_responds,
is_unix_socket, normalize_compute_driver_name, podman_socket_candidates_from_env,
podman_socket_responds,
};
#[cfg(unix)]
use std::io::{Read as _, Write as _};
Expand Down Expand Up @@ -1349,6 +1360,34 @@ mod tests {
)));
}

#[cfg(unix)]
#[test]
fn podman_socket_detection_returns_the_responsive_candidate() {
let temp_dir = tempfile::tempdir().expect("create temp dir");
let inactive_path = temp_dir.path().join("inactive.sock");
let inactive_listener = UnixListener::bind(&inactive_path).expect("bind inactive socket");
drop(inactive_listener);

let responsive_path = temp_dir.path().join("responsive.sock");
let listener = UnixListener::bind(&responsive_path).expect("bind responsive socket");
let handle = std::thread::spawn(move || {
let (mut stream, _) = listener.accept().expect("accept podman probe");
let mut request = [0_u8; 128];
let _ = stream.read(&mut request).expect("read podman probe");
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_eq!(
detect_podman_socket_from_candidates(&[inactive_path, responsive_path.clone(),]),
Some(responsive_path)
);
handle.join().expect("probe server exits");
}

#[test]
#[allow(unsafe_code)] // std::env::set_var/remove_var require unsafe in Rust 2024
fn detect_driver_prefers_kubernetes_when_k8s_env_is_set() {
Expand Down
2 changes: 1 addition & 1 deletion crates/openshell-driver-podman/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -331,7 +331,7 @@ Podman resources after out-of-band container removal or label drift.

| Environment Variable | CLI Flag | Default | Description |
|---|---|---|---|
| `OPENSHELL_PODMAN_SOCKET` | `--podman-socket` | `$XDG_RUNTIME_DIR/podman/podman.sock` on Linux, `$HOME/.local/share/containers/podman/machine/podman.sock` on macOS | Podman API Unix socket path. |
| `OPENSHELL_PODMAN_SOCKET` | `--podman-socket` | Probes known local Podman API sockets and uses the first responsive socket. Fails to start if none respond. | Podman API Unix socket path. |
| `OPENSHELL_SANDBOX_IMAGE` | `--sandbox-image` | From gateway config | Default OCI image for sandboxes. |
| `OPENSHELL_SANDBOX_IMAGE_PULL_POLICY` | `--sandbox-image-pull-policy` | `missing` | Pull policy: `always`, `missing`, `never`, or `newer`. |
| `OPENSHELL_GRPC_ENDPOINT` | `--grpc-endpoint` | Auto-detected via `host.containers.internal` | Gateway gRPC endpoint for sandbox callbacks. |
Expand Down
83 changes: 4 additions & 79 deletions crates/openshell-driver-podman/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -69,10 +69,9 @@ impl FromStr for ImagePullPolicy {
#[derive(Clone, serde::Serialize, serde::Deserialize)]
#[serde(default, deny_unknown_fields)]
pub struct PodmanComputeConfig {
/// Path to the Podman API Unix socket.
/// Default: `$XDG_RUNTIME_DIR/podman/podman.sock` (Linux),
/// `$HOME/.local/share/containers/podman/machine/podman.sock` (macOS).
pub socket_path: PathBuf,
/// Podman API Unix socket. When unset, use the socket selected by
/// gateway auto-detection.
pub socket_path: Option<PathBuf>,
/// Default OCI image for sandboxes.
pub default_image: String,
/// Image pull policy for sandbox images.
Expand Down Expand Up @@ -215,38 +214,12 @@ impl PodmanComputeConfig {
String::new()
}
}

/// Resolve the default socket path from the environment.
///
/// - **macOS**: `$HOME/.local/share/containers/podman/machine/podman.sock`
/// (the symlink created by `podman machine` pointing to the VM API socket).
/// - **Linux**: `$XDG_RUNTIME_DIR/podman/podman.sock` when set (by
/// `pam_systemd`/logind), otherwise `/run/user/{uid}/podman/podman.sock`
/// using the real UID via `getuid()`.
#[must_use]
pub fn default_socket_path() -> PathBuf {
#[cfg(target_os = "macos")]
{
let home = std::env::var("HOME").expect("HOME must be set on macOS");
PathBuf::from(home).join(".local/share/containers/podman/machine/podman.sock")
}
#[cfg(target_os = "linux")]
{
std::env::var("XDG_RUNTIME_DIR").map_or_else(
|_| {
let uid = rustix::process::getuid().as_raw();
PathBuf::from(format!("/run/user/{uid}/podman/podman.sock"))
},
|xdg| PathBuf::from(xdg).join("podman/podman.sock"),
)
}
}
}

impl Default for PodmanComputeConfig {
fn default() -> Self {
Self {
socket_path: Self::default_socket_path(),
socket_path: None,
default_image: openshell_core::image::default_sandbox_image(),
image_pull_policy: ImagePullPolicy::default(),
grpc_endpoint: String::new(),
Expand Down Expand Up @@ -296,39 +269,6 @@ impl std::fmt::Debug for PodmanComputeConfig {
mod tests {
use super::*;

/// Serialises env-mutating tests so that parallel test threads cannot
/// observe each other's changes to `XDG_RUNTIME_DIR`.
static ENV_LOCK: std::sync::LazyLock<std::sync::Mutex<()>> =
std::sync::LazyLock::new(|| std::sync::Mutex::new(()));

#[test]
#[cfg(target_os = "linux")]
fn default_socket_path_respects_xdg_runtime_dir() {
let _guard = ENV_LOCK
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
temp_env::with_vars([("XDG_RUNTIME_DIR", Some("/tmp/test-xdg"))], || {
let path = PodmanComputeConfig::default_socket_path();
assert_eq!(path, PathBuf::from("/tmp/test-xdg/podman/podman.sock"));
});
}

#[test]
#[cfg(target_os = "linux")]
fn default_socket_path_falls_back_to_uid() {
let _guard = ENV_LOCK
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
temp_env::with_vars([("XDG_RUNTIME_DIR", None::<&str>)], || {
let path = PodmanComputeConfig::default_socket_path();
let uid = rustix::process::getuid().as_raw();
assert_eq!(
path,
PathBuf::from(format!("/run/user/{uid}/podman/podman.sock"))
);
});
}

#[test]
fn default_config_sets_health_check_interval() {
let cfg = PodmanComputeConfig::default();
Expand Down Expand Up @@ -382,21 +322,6 @@ mod tests {
assert!(err.to_string().contains("sandbox_pids_limit"));
}

#[test]
#[cfg(target_os = "macos")]
fn default_socket_path_uses_podman_machine_on_macos() {
let _guard = ENV_LOCK
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
temp_env::with_vars([("HOME", Some("/Users/testuser"))], || {
let path = PodmanComputeConfig::default_socket_path();
assert_eq!(
path,
PathBuf::from("/Users/testuser/.local/share/containers/podman/machine/podman.sock")
);
});
}

// ── TLS config validation ─────────────────────────────────────────

#[test]
Expand Down
2 changes: 1 addition & 1 deletion crates/openshell-driver-podman/src/container.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1779,7 +1779,7 @@ mod tests {

fn test_config() -> PodmanComputeConfig {
PodmanComputeConfig {
socket_path: std::path::PathBuf::from("/tmp/test.sock"),
socket_path: Some(std::path::PathBuf::from("/tmp/test.sock")),
default_image: "test-image:latest".to_string(),
grpc_endpoint: "http://localhost:50051".to_string(),
host_gateway_ip: String::new(),
Expand Down
72 changes: 64 additions & 8 deletions crates/openshell-driver-podman/src/driver.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ use openshell_core::gpu::{
use openshell_core::proto::compute::v1::{
DriverSandbox, GetCapabilitiesResponse, GpuResourceRequirements,
};
use std::path::Path;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use std::time::Duration;
use tracing::{info, warn};
Expand Down Expand Up @@ -157,22 +157,48 @@ fn podman_gpu_selection_error(err: CdiGpuSelectionError) -> ComputeDriverError {
ComputeDriverError::Precondition(err.to_string())
}

/// Resolve the socket to connect to: explicit configuration wins, otherwise
/// fall back to `detect`. Returns an error if neither resolves.
///
/// Takes `detect` as a parameter (rather than calling
/// [`openshell_core::config::detect_podman_socket`] directly) so tests can
/// exercise the precedence deterministically, without touching real
/// environment variables or the filesystem.
fn resolve_socket_path(
configured: Option<PathBuf>,
detect: impl FnOnce() -> Option<PathBuf>,
) -> Result<PathBuf, PodmanApiError> {
configured.or_else(detect).ok_or_else(|| {
PodmanApiError::InvalidInput(
"no responsive Podman API socket found; set OPENSHELL_PODMAN_SOCKET \
or configure socket_path"
.to_string(),
)
})
}

impl PodmanComputeDriver {
/// Create a new driver, verifying the Podman socket is reachable.
pub async fn new(mut config: PodmanComputeConfig) -> Result<Self, PodmanApiError> {
const MAX_PING_RETRIES: u32 = 5;
const PING_RETRY_DELAY: Duration = Duration::from_secs(2);

if !config.socket_path.exists() {
let socket_path = resolve_socket_path(
config.socket_path.clone(),
openshell_core::config::detect_podman_socket,
)?;
config.socket_path = Some(socket_path.clone());

if !socket_path.exists() {
if cfg!(target_os = "macos") {
warn!(
path = %config.socket_path.display(),
path = %socket_path.display(),
"Podman socket not found; is podman machine running? \
Try `podman machine start` or set OPENSHELL_PODMAN_SOCKET to override."
);
} else {
warn!(
path = %config.socket_path.display(),
path = %socket_path.display(),
"Podman socket not found; is the Podman service running? \
Set OPENSHELL_PODMAN_SOCKET or XDG_RUNTIME_DIR to override."
);
Expand All @@ -186,7 +212,7 @@ impl PodmanComputeDriver {
config.validate_runtime_limits()?;
config.validate_host_gateway_ip()?;

let client = PodmanClient::new(config.socket_path.clone());
let client = PodmanClient::new(socket_path);

// Verify connectivity, retrying briefly to tolerate transient socket
// unavailability (e.g. podman.socket restarting after a package
Expand Down Expand Up @@ -774,7 +800,7 @@ impl PodmanComputeDriver {
gpu_inventory: CdiGpuInventory,
allow_all_default_gpu: bool,
) -> Self {
let client = PodmanClient::new(config.socket_path.clone());
let client = PodmanClient::new(config.socket_path.clone().unwrap_or_default());
let refresh_inventory = gpu_inventory.clone();
Self {
client,
Expand Down Expand Up @@ -850,6 +876,36 @@ mod tests {
use std::fs;
use std::path::PathBuf;

// ── socket resolution ───────────────────────────────────────────────
//
// These test resolve_socket_path directly with an injected detector, so
// they are deterministic regardless of the host's real environment
// variables or whether a Podman socket happens to be running.

#[test]
fn resolve_socket_path_prefers_explicit_configuration() {
let path = resolve_socket_path(Some(PathBuf::from("/explicit.sock")), || {
Some(PathBuf::from("/detected.sock"))
})
.unwrap();

assert_eq!(path, PathBuf::from("/explicit.sock"));
}

#[test]
fn resolve_socket_path_uses_detected_socket_when_unconfigured() {
let path = resolve_socket_path(None, || Some(PathBuf::from("/detected.sock"))).unwrap();

assert_eq!(path, PathBuf::from("/detected.sock"));
}

#[test]
fn resolve_socket_path_errors_when_neither_source_resolves() {
let err = resolve_socket_path(None, || None).unwrap_err();

assert!(err.to_string().contains("no responsive Podman API socket"));
}

fn cdi_devices_config(device_ids: &[&str]) -> prost_types::Struct {
prost_types::Struct {
fields: std::iter::once((
Expand Down Expand Up @@ -1245,7 +1301,7 @@ mod tests {

fn test_driver(socket_path: PathBuf) -> PodmanComputeDriver {
let config = PodmanComputeConfig {
socket_path,
socket_path: Some(socket_path),
stop_timeout_secs: 10,
..PodmanComputeConfig::default()
};
Expand Down Expand Up @@ -1424,7 +1480,7 @@ mod tests {
)],
);
let config = PodmanComputeConfig {
socket_path: socket_path.clone(),
socket_path: Some(socket_path.clone()),
enable_bind_mounts: true,
..PodmanComputeConfig::default()
};
Expand Down
2 changes: 1 addition & 1 deletion crates/openshell-driver-podman/src/grpc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,7 @@ mod tests {

fn test_service(socket_path: PathBuf) -> ComputeDriverService {
let config = PodmanComputeConfig {
socket_path,
socket_path: Some(socket_path),
stop_timeout_secs: 10,
..PodmanComputeConfig::default()
};
Expand Down
6 changes: 1 addition & 5 deletions crates/openshell-driver-podman/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -114,12 +114,8 @@ async fn main() -> Result<()> {
)
.init();

let socket_path = args
.podman_socket
.unwrap_or_else(PodmanComputeConfig::default_socket_path);

let driver = PodmanComputeDriver::new(PodmanComputeConfig {
socket_path,
socket_path: args.podman_socket,
default_image: args.sandbox_image.unwrap_or_default(),
image_pull_policy: args.sandbox_image_pull_policy,
grpc_endpoint: args.grpc_endpoint.unwrap_or_default(),
Expand Down
26 changes: 3 additions & 23 deletions crates/openshell-driver-vm/src/driver.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3270,10 +3270,9 @@ async fn connect_local_container_engine() -> Option<Docker> {
return Some(docker);
}

let podman_socket = podman_socket_path();
if podman_socket.exists()
&& let Ok(docker) =
Docker::connect_with_unix(podman_socket.to_str()?, 120, bollard::API_DEFAULT_VERSION)
let podman_socket = openshell_core::config::detect_podman_socket()?;
if let Ok(docker) =
Docker::connect_with_unix(podman_socket.to_str()?, 120, bollard::API_DEFAULT_VERSION)
&& docker.ping().await.is_ok()
{
info!(
Expand All @@ -3286,25 +3285,6 @@ async fn connect_local_container_engine() -> Option<Docker> {
None
}

/// Podman user socket path for the current platform.
fn podman_socket_path() -> PathBuf {
#[cfg(target_os = "macos")]
{
let home = std::env::var("HOME").unwrap_or_default();
PathBuf::from(home).join(".local/share/containers/podman/machine/podman.sock")
}
#[cfg(target_os = "linux")]
{
std::env::var("XDG_RUNTIME_DIR").map_or_else(
|_| {
let uid = nix::unistd::getuid();
PathBuf::from(format!("/run/user/{uid}/podman/podman.sock"))
},
|xdg| PathBuf::from(xdg).join("podman/podman.sock"),
)
}
}

fn is_openshell_local_build_image_ref(image_ref: &str) -> bool {
image_ref.starts_with("openshell/sandbox-from:")
}
Expand Down
Loading
Loading