55
66use serde:: { Deserialize , Serialize } ;
77use std:: fmt;
8+ #[ cfg( unix) ]
9+ use std:: io:: { Read , Write } ;
810use std:: net:: SocketAddr ;
911#[ cfg( unix) ]
1012use std:: os:: unix:: fs:: FileTypeExt ;
1113use std:: path:: { Path , PathBuf } ;
1214use std:: process:: Command ;
1315use std:: str:: FromStr ;
16+ #[ cfg( unix) ]
17+ use std:: time:: Duration ;
1418
1519// ── Public default constants ────────────────────────────────────────────
1620//
@@ -96,8 +100,8 @@ pub fn detect_driver() -> Option<ComputeDriverKind> {
96100 return Some ( ComputeDriverKind :: Kubernetes ) ;
97101 }
98102
99- // Podman: check if podman binary is available
100- if is_binary_available ( "podman" ) {
103+ // Podman: check for a reachable local API socket.
104+ if is_podman_available ( ) {
101105 return Some ( ComputeDriverKind :: Podman ) ;
102106 }
103107
@@ -117,6 +121,58 @@ fn is_binary_available(name: &str) -> bool {
117121 . is_ok_and ( |output| output. status . success ( ) )
118122}
119123
124+ fn is_podman_available ( ) -> bool {
125+ podman_socket_available ( )
126+ }
127+
128+ fn podman_socket_available ( ) -> bool {
129+ podman_socket_candidates ( )
130+ . iter ( )
131+ . any ( |path| podman_socket_responds ( path) )
132+ }
133+
134+ fn podman_socket_candidates ( ) -> Vec < PathBuf > {
135+ let socket = std:: env:: var ( "OPENSHELL_PODMAN_SOCKET" )
136+ . ok ( )
137+ . filter ( |path| !path. trim ( ) . is_empty ( ) )
138+ . map ( PathBuf :: from) ;
139+ podman_socket_candidates_from_env (
140+ socket,
141+ std:: env:: var_os ( "XDG_RUNTIME_DIR" ) . map ( PathBuf :: from) ,
142+ std:: env:: var_os ( "HOME" ) . map ( PathBuf :: from) ,
143+ )
144+ }
145+
146+ fn podman_socket_candidates_from_env (
147+ socket : Option < PathBuf > ,
148+ runtime_dir : Option < PathBuf > ,
149+ home : Option < PathBuf > ,
150+ ) -> Vec < PathBuf > {
151+ let mut candidates = Vec :: new ( ) ;
152+
153+ if let Some ( path) = socket {
154+ candidates. push ( path) ;
155+ }
156+
157+ if let Some ( runtime_dir) = runtime_dir {
158+ candidates. push ( runtime_dir. join ( "podman/podman.sock" ) ) ;
159+ }
160+
161+ #[ cfg( target_os = "linux" ) ]
162+ {
163+ candidates. push ( PathBuf :: from ( format ! (
164+ "/run/user/{}/podman/podman.sock" ,
165+ current_uid( )
166+ ) ) ) ;
167+ }
168+
169+ if let Some ( home) = home {
170+ candidates. push ( home. join ( ".local/share/containers/podman/machine/podman.sock" ) ) ;
171+ }
172+
173+ candidates
174+ }
175+
120176fn is_docker_available ( ) -> bool {
121177 is_binary_available ( "docker" ) || docker_socket_available ( )
122178}
@@ -160,12 +216,86 @@ fn is_unix_socket(path: &Path) -> bool {
160216 . is_ok_and ( |metadata| metadata. file_type ( ) . is_socket ( ) )
161217}
162218
219+ #[ cfg( unix) ]
220+ fn podman_socket_responds ( path : & Path ) -> bool {
221+ unix_socket_http_ping ( path, |response| {
222+ http_response_is_success ( response) && contains_ascii ( response, b"Libpod-Api-Version:" )
223+ } )
224+ }
225+
226+ #[ cfg( unix) ]
227+ fn unix_socket_http_ping ( path : & Path , accepts_response : impl FnOnce ( & [ u8 ] ) -> bool ) -> bool {
228+ const PROBE_TIMEOUT : Duration = Duration :: from_secs ( 1 ) ;
229+ const PING_REQUEST : & [ u8 ] =
230+ b"GET /_ping HTTP/1.1\r \n Host: localhost\r \n Connection: close\r \n \r \n " ;
231+
232+ if !is_unix_socket ( path) {
233+ return false ;
234+ }
235+
236+ let Ok ( mut stream) = std:: os:: unix:: net:: UnixStream :: connect ( path) else {
237+ return false ;
238+ } ;
239+ if stream. set_read_timeout ( Some ( PROBE_TIMEOUT ) ) . is_err ( )
240+ || stream. set_write_timeout ( Some ( PROBE_TIMEOUT ) ) . is_err ( )
241+ || stream. write_all ( PING_REQUEST ) . is_err ( )
242+ {
243+ return false ;
244+ }
245+
246+ let mut response = [ 0_u8 ; 512 ] ;
247+ let mut total = 0 ;
248+ while total < response. len ( ) {
249+ let Ok ( n) = stream. read ( & mut response[ total..] ) else {
250+ return false ;
251+ } ;
252+ if n == 0 {
253+ break ;
254+ }
255+ total += n;
256+ if contains_ascii ( & response[ ..total] , b"\r \n \r \n " ) {
257+ break ;
258+ }
259+ }
260+ total > 0 && accepts_response ( & response[ ..total] )
261+ }
262+
263+ #[ cfg( unix) ]
264+ fn http_response_is_success ( response : & [ u8 ] ) -> bool {
265+ response. starts_with ( b"HTTP/1.1 200" ) || response. starts_with ( b"HTTP/1.0 200" )
266+ }
267+
268+ #[ cfg( unix) ]
269+ fn contains_ascii ( haystack : & [ u8 ] , needle : & [ u8 ] ) -> bool {
270+ haystack
271+ . windows ( needle. len ( ) )
272+ . any ( |window| window. eq_ignore_ascii_case ( needle) )
273+ }
274+
275+ #[ cfg( all( unix, test) ) ]
276+ fn is_reachable_unix_socket ( path : & Path ) -> bool {
277+ is_unix_socket ( path) && std:: os:: unix:: net:: UnixStream :: connect ( path) . is_ok ( )
278+ }
279+
280+ #[ cfg( all( unix, target_os = "linux" ) ) ]
281+ fn current_uid ( ) -> u32 {
282+ use std:: os:: unix:: fs:: MetadataExt ;
283+
284+ std:: fs:: metadata ( "/proc/self" ) . map_or ( 0 , |metadata| metadata. uid ( ) )
285+ }
286+
163287#[ cfg( not( unix) ) ]
164288fn is_unix_socket ( path : & Path ) -> bool {
165289 let _ = path;
166290 false
167291}
168292
293+ #[ cfg( not( unix) ) ]
294+ fn podman_socket_responds ( path : & Path ) -> bool {
295+ let _ = path;
296+ false
297+ }
298+
169299/// Server configuration.
170300#[ derive( Debug , Clone , Serialize , Deserialize ) ]
171301pub struct Config {
@@ -593,10 +723,15 @@ const fn default_ssh_session_ttl_secs() -> u64 {
593723
594724#[ cfg( test) ]
595725mod tests {
726+ #[ cfg( unix) ]
727+ use super :: is_reachable_unix_socket;
596728 use super :: {
597729 ComputeDriverKind , Config , DEFAULT_SERVICE_ROUTING_DOMAIN , detect_driver,
598- docker_host_unix_socket_path, is_unix_socket,
730+ docker_host_unix_socket_path, is_unix_socket, podman_socket_candidates_from_env,
731+ podman_socket_responds,
599732 } ;
733+ #[ cfg( unix) ]
734+ use std:: io:: { Read as _, Write as _} ;
600735 use std:: net:: SocketAddr ;
601736 #[ cfg( unix) ]
602737 use std:: os:: unix:: net:: UnixListener ;
@@ -700,9 +835,10 @@ mod tests {
700835 }
701836
702837 #[ test]
703- fn detect_driver_returns_none_without_k8s_env_or_binaries ( ) {
704- // When KUBERNETES_SERVICE_HOST is not set and no docker/podman binaries
705- // or Docker socket are available, detect_driver should return None.
838+ fn detect_driver_returns_none_without_k8s_env_or_local_runtime ( ) {
839+ // When KUBERNETES_SERVICE_HOST is not set, no Docker binary/socket is
840+ // available, and no Podman API socket is available, detect_driver
841+ // should return None.
706842 // This test may pass or fail depending on the test environment,
707843 // but it documents the expected behavior.
708844 let _ = detect_driver ( ) ; // Returns Some or None based on environment
@@ -726,10 +862,73 @@ mod tests {
726862 let _listener = UnixListener :: bind ( & socket_path) . expect ( "bind unix socket" ) ;
727863
728864 assert ! ( is_unix_socket( & socket_path) ) ;
865+ assert ! ( is_reachable_unix_socket( & socket_path) ) ;
729866
730867 let regular_file = temp_dir. path ( ) . join ( "not-a-socket" ) ;
731868 std:: fs:: write ( & regular_file, b"not a socket" ) . expect ( "write regular file" ) ;
732869 assert ! ( !is_unix_socket( & regular_file) ) ;
870+ assert ! ( !is_reachable_unix_socket( & regular_file) ) ;
871+ }
872+
873+ #[ cfg( unix) ]
874+ #[ test]
875+ fn podman_socket_probe_accepts_successful_ping_response ( ) {
876+ let temp_dir = tempfile:: tempdir ( ) . expect ( "create temp dir" ) ;
877+ let socket_path = temp_dir. path ( ) . join ( "podman.sock" ) ;
878+ let listener = UnixListener :: bind ( & socket_path) . expect ( "bind podman socket" ) ;
879+
880+ let handle = std:: thread:: spawn ( move || {
881+ let ( mut stream, _) = listener. accept ( ) . expect ( "accept podman probe" ) ;
882+ let mut request = [ 0_u8 ; 128 ] ;
883+ let n = stream. read ( & mut request) . expect ( "read podman probe" ) ;
884+ assert ! ( request[ ..n] . starts_with( b"GET /_ping HTTP/1.1\r \n " ) ) ;
885+ stream
886+ . write_all (
887+ b"HTTP/1.1 200 OK\r \n Libpod-Api-Version: 5.8.2\r \n Content-Length: 2\r \n \r \n OK" ,
888+ )
889+ . expect ( "write podman ping response" ) ;
890+ } ) ;
891+
892+ assert ! ( podman_socket_responds( & socket_path) ) ;
893+ handle. join ( ) . expect ( "probe server exits" ) ;
894+ }
895+
896+ #[ cfg( unix) ]
897+ #[ test]
898+ fn podman_socket_probe_rejects_docker_ping_response ( ) {
899+ let temp_dir = tempfile:: tempdir ( ) . expect ( "create temp dir" ) ;
900+ let socket_path = temp_dir. path ( ) . join ( "podman.sock" ) ;
901+ let listener = UnixListener :: bind ( & socket_path) . expect ( "bind podman socket" ) ;
902+
903+ let handle = std:: thread:: spawn ( move || {
904+ let ( mut stream, _) = listener. accept ( ) . expect ( "accept podman probe" ) ;
905+ let mut request = [ 0_u8 ; 128 ] ;
906+ let n = stream. read ( & mut request) . expect ( "read podman probe" ) ;
907+ assert ! ( request[ ..n] . starts_with( b"GET /_ping HTTP/1.1\r \n " ) ) ;
908+ stream
909+ . write_all (
910+ b"HTTP/1.1 200 OK\r \n Server: Docker/29.2.1\r \n Content-Length: 2\r \n \r \n OK" ,
911+ )
912+ . expect ( "write docker ping response" ) ;
913+ } ) ;
914+
915+ assert ! ( !podman_socket_responds( & socket_path) ) ;
916+ handle. join ( ) . expect ( "probe server exits" ) ;
917+ }
918+
919+ #[ test]
920+ fn podman_socket_candidates_include_env_runtime_and_home_paths ( ) {
921+ let candidates = podman_socket_candidates_from_env (
922+ Some ( PathBuf :: from ( "/tmp/custom-podman.sock" ) ) ,
923+ Some ( PathBuf :: from ( "/tmp/runtime" ) ) ,
924+ Some ( PathBuf :: from ( "/tmp/home" ) ) ,
925+ ) ;
926+
927+ assert ! ( candidates. contains( & PathBuf :: from( "/tmp/custom-podman.sock" ) ) ) ;
928+ assert ! ( candidates. contains( & PathBuf :: from( "/tmp/runtime/podman/podman.sock" ) ) ) ;
929+ assert ! ( candidates. contains( & PathBuf :: from(
930+ "/tmp/home/.local/share/containers/podman/machine/podman.sock"
931+ ) ) ) ;
733932 }
734933
735934 #[ test]
0 commit comments