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,54 @@ 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_candidates ( )
126+ . iter ( )
127+ . any ( |path| podman_socket_responds ( path) )
128+ }
129+
130+ fn podman_socket_candidates ( ) -> Vec < PathBuf > {
131+ let socket = std:: env:: var ( "OPENSHELL_PODMAN_SOCKET" )
132+ . ok ( )
133+ . filter ( |path| !path. trim ( ) . is_empty ( ) )
134+ . map ( PathBuf :: from) ;
135+ podman_socket_candidates_from_env (
136+ socket,
137+ std:: env:: var_os ( "XDG_RUNTIME_DIR" ) . map ( PathBuf :: from) ,
138+ std:: env:: var_os ( "HOME" ) . map ( PathBuf :: from) ,
139+ )
140+ }
141+
142+ fn podman_socket_candidates_from_env (
143+ socket : Option < PathBuf > ,
144+ runtime_dir : Option < PathBuf > ,
145+ home : Option < PathBuf > ,
146+ ) -> Vec < PathBuf > {
147+ let mut candidates = Vec :: new ( ) ;
148+
149+ if let Some ( path) = socket {
150+ candidates. push ( path) ;
151+ }
152+
153+ if let Some ( runtime_dir) = runtime_dir {
154+ candidates. push ( runtime_dir. join ( "podman/podman.sock" ) ) ;
155+ }
156+
157+ #[ cfg( target_os = "linux" ) ]
158+ {
159+ candidates. push ( PathBuf :: from ( format ! (
160+ "/run/user/{}/podman/podman.sock" ,
161+ current_uid( )
162+ ) ) ) ;
163+ }
164+
165+ if let Some ( home) = home {
166+ candidates. push ( home. join ( ".local/share/containers/podman/machine/podman.sock" ) ) ;
167+ }
168+
169+ candidates
170+ }
171+
120172fn is_docker_available ( ) -> bool {
121173 is_binary_available ( "docker" ) || docker_socket_available ( )
122174}
@@ -160,12 +212,86 @@ fn is_unix_socket(path: &Path) -> bool {
160212 . is_ok_and ( |metadata| metadata. file_type ( ) . is_socket ( ) )
161213}
162214
215+ #[ cfg( unix) ]
216+ fn podman_socket_responds ( path : & Path ) -> bool {
217+ unix_socket_http_ping ( path, |response| {
218+ http_response_is_success ( response) && contains_ascii ( response, b"Libpod-Api-Version:" )
219+ } )
220+ }
221+
222+ #[ cfg( unix) ]
223+ fn unix_socket_http_ping ( path : & Path , accepts_response : impl FnOnce ( & [ u8 ] ) -> bool ) -> bool {
224+ const PROBE_TIMEOUT : Duration = Duration :: from_secs ( 1 ) ;
225+ const PING_REQUEST : & [ u8 ] =
226+ b"GET /_ping HTTP/1.1\r \n Host: localhost\r \n Connection: close\r \n \r \n " ;
227+
228+ if !is_unix_socket ( path) {
229+ return false ;
230+ }
231+
232+ let Ok ( mut stream) = std:: os:: unix:: net:: UnixStream :: connect ( path) else {
233+ return false ;
234+ } ;
235+ if stream. set_read_timeout ( Some ( PROBE_TIMEOUT ) ) . is_err ( )
236+ || stream. set_write_timeout ( Some ( PROBE_TIMEOUT ) ) . is_err ( )
237+ || stream. write_all ( PING_REQUEST ) . is_err ( )
238+ {
239+ return false ;
240+ }
241+
242+ let mut response = [ 0_u8 ; 512 ] ;
243+ let mut total = 0 ;
244+ while total < response. len ( ) {
245+ let Ok ( n) = stream. read ( & mut response[ total..] ) else {
246+ return false ;
247+ } ;
248+ if n == 0 {
249+ break ;
250+ }
251+ total += n;
252+ if contains_ascii ( & response[ ..total] , b"\r \n \r \n " ) {
253+ break ;
254+ }
255+ }
256+ total > 0 && accepts_response ( & response[ ..total] )
257+ }
258+
259+ #[ cfg( unix) ]
260+ fn http_response_is_success ( response : & [ u8 ] ) -> bool {
261+ response. starts_with ( b"HTTP/1.1 200" ) || response. starts_with ( b"HTTP/1.0 200" )
262+ }
263+
264+ #[ cfg( unix) ]
265+ fn contains_ascii ( haystack : & [ u8 ] , needle : & [ u8 ] ) -> bool {
266+ haystack
267+ . windows ( needle. len ( ) )
268+ . any ( |window| window. eq_ignore_ascii_case ( needle) )
269+ }
270+
271+ #[ cfg( all( unix, test) ) ]
272+ fn is_reachable_unix_socket ( path : & Path ) -> bool {
273+ is_unix_socket ( path) && std:: os:: unix:: net:: UnixStream :: connect ( path) . is_ok ( )
274+ }
275+
276+ #[ cfg( all( unix, target_os = "linux" ) ) ]
277+ fn current_uid ( ) -> u32 {
278+ use std:: os:: unix:: fs:: MetadataExt ;
279+
280+ std:: fs:: metadata ( "/proc/self" ) . map_or ( 0 , |metadata| metadata. uid ( ) )
281+ }
282+
163283#[ cfg( not( unix) ) ]
164284fn is_unix_socket ( path : & Path ) -> bool {
165285 let _ = path;
166286 false
167287}
168288
289+ #[ cfg( not( unix) ) ]
290+ fn podman_socket_responds ( path : & Path ) -> bool {
291+ let _ = path;
292+ false
293+ }
294+
169295/// Server configuration.
170296#[ derive( Debug , Clone , Serialize , Deserialize ) ]
171297pub struct Config {
@@ -593,10 +719,15 @@ const fn default_ssh_session_ttl_secs() -> u64 {
593719
594720#[ cfg( test) ]
595721mod tests {
722+ #[ cfg( unix) ]
723+ use super :: is_reachable_unix_socket;
596724 use super :: {
597725 ComputeDriverKind , Config , DEFAULT_SERVICE_ROUTING_DOMAIN , detect_driver,
598- docker_host_unix_socket_path, is_unix_socket,
726+ docker_host_unix_socket_path, is_unix_socket, podman_socket_candidates_from_env,
727+ podman_socket_responds,
599728 } ;
729+ #[ cfg( unix) ]
730+ use std:: io:: { Read as _, Write as _} ;
600731 use std:: net:: SocketAddr ;
601732 #[ cfg( unix) ]
602733 use std:: os:: unix:: net:: UnixListener ;
@@ -700,9 +831,10 @@ mod tests {
700831 }
701832
702833 #[ 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.
834+ fn detect_driver_returns_none_without_k8s_env_or_local_runtime ( ) {
835+ // When KUBERNETES_SERVICE_HOST is not set, no Docker binary/socket is
836+ // available, and no Podman API socket is available, detect_driver
837+ // should return None.
706838 // This test may pass or fail depending on the test environment,
707839 // but it documents the expected behavior.
708840 let _ = detect_driver ( ) ; // Returns Some or None based on environment
@@ -726,10 +858,73 @@ mod tests {
726858 let _listener = UnixListener :: bind ( & socket_path) . expect ( "bind unix socket" ) ;
727859
728860 assert ! ( is_unix_socket( & socket_path) ) ;
861+ assert ! ( is_reachable_unix_socket( & socket_path) ) ;
729862
730863 let regular_file = temp_dir. path ( ) . join ( "not-a-socket" ) ;
731864 std:: fs:: write ( & regular_file, b"not a socket" ) . expect ( "write regular file" ) ;
732865 assert ! ( !is_unix_socket( & regular_file) ) ;
866+ assert ! ( !is_reachable_unix_socket( & regular_file) ) ;
867+ }
868+
869+ #[ cfg( unix) ]
870+ #[ test]
871+ fn podman_socket_probe_accepts_successful_ping_response ( ) {
872+ let temp_dir = tempfile:: tempdir ( ) . expect ( "create temp dir" ) ;
873+ let socket_path = temp_dir. path ( ) . join ( "podman.sock" ) ;
874+ let listener = UnixListener :: bind ( & socket_path) . expect ( "bind podman socket" ) ;
875+
876+ let handle = std:: thread:: spawn ( move || {
877+ let ( mut stream, _) = listener. accept ( ) . expect ( "accept podman probe" ) ;
878+ let mut request = [ 0_u8 ; 128 ] ;
879+ let n = stream. read ( & mut request) . expect ( "read podman probe" ) ;
880+ assert ! ( request[ ..n] . starts_with( b"GET /_ping HTTP/1.1\r \n " ) ) ;
881+ stream
882+ . write_all (
883+ b"HTTP/1.1 200 OK\r \n Libpod-Api-Version: 5.8.2\r \n Content-Length: 2\r \n \r \n OK" ,
884+ )
885+ . expect ( "write podman ping response" ) ;
886+ } ) ;
887+
888+ assert ! ( podman_socket_responds( & socket_path) ) ;
889+ handle. join ( ) . expect ( "probe server exits" ) ;
890+ }
891+
892+ #[ cfg( unix) ]
893+ #[ test]
894+ fn podman_socket_probe_rejects_docker_ping_response ( ) {
895+ let temp_dir = tempfile:: tempdir ( ) . expect ( "create temp dir" ) ;
896+ let socket_path = temp_dir. path ( ) . join ( "podman.sock" ) ;
897+ let listener = UnixListener :: bind ( & socket_path) . expect ( "bind podman socket" ) ;
898+
899+ let handle = std:: thread:: spawn ( move || {
900+ let ( mut stream, _) = listener. accept ( ) . expect ( "accept podman probe" ) ;
901+ let mut request = [ 0_u8 ; 128 ] ;
902+ let n = stream. read ( & mut request) . expect ( "read podman probe" ) ;
903+ assert ! ( request[ ..n] . starts_with( b"GET /_ping HTTP/1.1\r \n " ) ) ;
904+ stream
905+ . write_all (
906+ b"HTTP/1.1 200 OK\r \n Server: Docker/29.2.1\r \n Content-Length: 2\r \n \r \n OK" ,
907+ )
908+ . expect ( "write docker ping response" ) ;
909+ } ) ;
910+
911+ assert ! ( !podman_socket_responds( & socket_path) ) ;
912+ handle. join ( ) . expect ( "probe server exits" ) ;
913+ }
914+
915+ #[ test]
916+ fn podman_socket_candidates_include_env_runtime_and_home_paths ( ) {
917+ let candidates = podman_socket_candidates_from_env (
918+ Some ( PathBuf :: from ( "/tmp/custom-podman.sock" ) ) ,
919+ Some ( PathBuf :: from ( "/tmp/runtime" ) ) ,
920+ Some ( PathBuf :: from ( "/tmp/home" ) ) ,
921+ ) ;
922+
923+ assert ! ( candidates. contains( & PathBuf :: from( "/tmp/custom-podman.sock" ) ) ) ;
924+ assert ! ( candidates. contains( & PathBuf :: from( "/tmp/runtime/podman/podman.sock" ) ) ) ;
925+ assert ! ( candidates. contains( & PathBuf :: from(
926+ "/tmp/home/.local/share/containers/podman/machine/podman.sock"
927+ ) ) ) ;
733928 }
734929
735930 #[ test]
0 commit comments