@@ -81,8 +81,14 @@ impl ContainerRuntimeManager {
8181 let mut map: HashMap < String , String > = std:: collections:: HashMap :: new ( ) ;
8282 map. insert ( "io.kubernetes.pod.uid" . to_string ( ) , pod_uid. clone ( ) ) ;
8383
84+
85+ let ready_state = cri:: PodSandboxStateValue {
86+ state : 0 , // PodSandboxState::SandboxReady
87+ } ;
88+
8489 let pod_filter = cri:: PodSandboxFilter {
8590 label_selector : map,
91+ state : Some ( ready_state) ,
8692 ..Default :: default ( )
8793 } ;
8894
@@ -120,11 +126,16 @@ impl ContainerRuntimeManager {
120126 sandbox_id
121127 ) ;
122128
129+ let container_ready = cri:: ContainerStateValue {
130+ state : 1 , // ContainerState::ContainerRunning
131+ } ;
132+
123133 // 2. List containers in that sandbox
124134 let containers = match client
125135 . list_containers ( ListContainersRequest {
126136 filter : Some ( cri:: ContainerFilter {
127137 pod_sandbox_id : sandbox_id. clone ( ) ,
138+ state : Some ( container_ready) ,
128139 ..Default :: default ( )
129140 } ) ,
130141 } )
@@ -194,6 +205,14 @@ impl ContainerRuntimeManager {
194205 pids. push ( pid. pid ) ;
195206 }
196207
208+ if pids. is_empty ( ) {
209+ tracing:: error!( "No running containers found for pod UID: {}" , pod_uid) ;
210+ return Err ( tonic:: Status :: not_found ( format ! (
211+ "No running containers found for pod UID: {}" ,
212+ pod_uid
213+ ) ) ) ;
214+ }
215+
197216 tracing:: info!(
198217 "Successfully collected {} PIDs for pod UID: {}" ,
199218 pids. len( ) ,
@@ -283,12 +302,18 @@ mod tests {
283302 sandboxes
284303 . into_iter ( )
285304 . filter ( |sandbox| {
286- filter. label_selector . iter ( ) . all ( |( key, expected_value) | {
305+ // Filter by label selector
306+ let labels_match = filter. label_selector . iter ( ) . all ( |( key, expected_value) | {
287307 sandbox
288308 . labels
289309 . get ( key)
290310 . map_or ( false , |actual_value| actual_value == expected_value)
291- } )
311+ } ) ;
312+ // Filter by state if specified
313+ let state_match = filter. state . as_ref ( ) . map_or ( true , |state_filter| {
314+ sandbox. state == state_filter. state
315+ } ) ;
316+ labels_match && state_match
292317 } )
293318 . collect ( )
294319 } else {
@@ -302,10 +327,33 @@ mod tests {
302327
303328 async fn list_containers (
304329 & self ,
305- _request : tonic:: Request < ListContainersRequest > ,
330+ request : tonic:: Request < ListContainersRequest > ,
306331 ) -> std:: result:: Result < tonic:: Response < ListContainersResponse > , tonic:: Status > {
307332 let containers = self . containers . lock ( ) . await . clone ( ) ;
308- Ok ( Response :: new ( ListContainersResponse { containers } ) )
333+ let req = request. into_inner ( ) ;
334+
335+ let filtered = if let Some ( filter) = req. filter {
336+ containers
337+ . into_iter ( )
338+ . filter ( |container| {
339+ // Filter by sandbox ID if specified
340+ let sandbox_match = if filter. pod_sandbox_id . is_empty ( ) {
341+ true
342+ } else {
343+ container. pod_sandbox_id == filter. pod_sandbox_id
344+ } ;
345+ // Filter by state if specified
346+ let state_match = filter. state . as_ref ( ) . map_or ( true , |state_filter| {
347+ container. state == state_filter. state
348+ } ) ;
349+ sandbox_match && state_match
350+ } )
351+ . collect ( )
352+ } else {
353+ containers
354+ } ;
355+
356+ Ok ( Response :: new ( ListContainersResponse { containers : filtered } ) )
309357 }
310358
311359 async fn container_status (
@@ -338,6 +386,7 @@ mod tests {
338386 lock. push ( Container {
339387 id : "fake-id-123" . into ( ) ,
340388 pod_sandbox_id : "fake-sandbox-123" . into ( ) ,
389+ state : 1 , // CONTAINER_RUNNING
341390 ..Default :: default ( )
342391 } ) ;
343392
@@ -429,4 +478,222 @@ mod tests {
429478 server_handle. abort ( ) ;
430479 let _ = std:: fs:: remove_file ( socket_path) ;
431480 }
481+
482+ /// Helper to create a mock server and client for testing
483+ async fn setup_mock_server (
484+ mock : MockRuntimeService ,
485+ socket_path : & str ,
486+ ) -> ( RuntimeServiceClient < tonic:: transport:: Channel > , tokio:: task:: JoinHandle < ( ) > ) {
487+ let _ = std:: fs:: remove_file ( socket_path) ;
488+ let uds = UnixListener :: bind ( socket_path) . expect ( "failed to bind UDS" ) ;
489+ let incoming = UnixListenerStream :: new ( uds) ;
490+
491+ let svc = RuntimeServiceServer :: new ( mock) ;
492+ let server_handle = tokio:: spawn ( async move {
493+ Server :: builder ( )
494+ . add_service ( svc)
495+ . serve_with_incoming ( incoming)
496+ . await
497+ . expect ( "server failed" ) ;
498+ } ) ;
499+
500+ let socket_path_str = socket_path. to_string ( ) ;
501+ let endpoint = Endpoint :: try_from ( "http://[::]:50051" ) . unwrap ( ) ;
502+ let channel = endpoint
503+ . connect_with_connector ( service_fn ( move |_uri : Uri | {
504+ let path = socket_path_str. clone ( ) ;
505+ async move {
506+ let stream = UnixStream :: connect ( path) . await ?;
507+ Ok :: < _ , std:: io:: Error > ( TokioIo :: new ( stream) )
508+ }
509+ } ) )
510+ . await
511+ . expect ( "failed to connect to mock UDS server" ) ;
512+
513+ ( RuntimeServiceClient :: new ( channel) , server_handle)
514+ }
515+
516+ #[ tokio:: test]
517+ async fn test_sandbox_not_ready_returns_not_found ( ) {
518+ // Test that a sandbox in NotReady state (state=1) is filtered out
519+ let mock = MockRuntimeService :: default ( ) ;
520+ {
521+ let mut lock = mock. sandboxes . lock ( ) . await ;
522+ lock. push ( cri:: PodSandbox {
523+ id : "not-ready-sandbox" . into ( ) ,
524+ state : 1 , // SANDBOX_NOTREADY
525+ labels : {
526+ let mut m = HashMap :: new ( ) ;
527+ m. insert ( "io.kubernetes.pod.uid" . into ( ) , "not-ready-pod-uid" . into ( ) ) ;
528+ m
529+ } ,
530+ ..Default :: default ( )
531+ } ) ;
532+ }
533+
534+ let socket_path = "/tmp/test-cri-sandbox-not-ready.sock" ;
535+ let ( client, server_handle) = setup_mock_server ( mock, socket_path) . await ;
536+
537+ let cri_manager = ContainerRuntimeManager :: new_with_client ( client) ;
538+ let result = cri_manager
539+ . get_pids_for_pod ( "not-ready-pod-uid" . to_string ( ) )
540+ . await ;
541+
542+ // Should return not found because sandbox is not in Ready state
543+ assert ! ( result. is_err( ) ) ;
544+ let err = result. unwrap_err ( ) ;
545+ assert_eq ! ( err. code( ) , tonic:: Code :: NotFound ) ;
546+
547+ server_handle. abort ( ) ;
548+ let _ = std:: fs:: remove_file ( socket_path) ;
549+ }
550+
551+ #[ tokio:: test]
552+ async fn test_exited_containers_filtered_out ( ) {
553+ // Test that containers not in Running state are filtered out
554+ let mock = MockRuntimeService :: default ( ) ;
555+ {
556+ // Add a ready sandbox
557+ let mut lock = mock. sandboxes . lock ( ) . await ;
558+ lock. push ( cri:: PodSandbox {
559+ id : "ready-sandbox-123" . into ( ) ,
560+ state : 0 , // SANDBOX_READY
561+ labels : {
562+ let mut m = HashMap :: new ( ) ;
563+ m. insert ( "io.kubernetes.pod.uid" . into ( ) , "pod-with-exited-containers" . into ( ) ) ;
564+ m
565+ } ,
566+ ..Default :: default ( )
567+ } ) ;
568+
569+ // Add containers that are NOT running (exited)
570+ let mut lock = mock. containers . lock ( ) . await ;
571+ lock. push ( Container {
572+ id : "exited-container-1" . into ( ) ,
573+ pod_sandbox_id : "ready-sandbox-123" . into ( ) ,
574+ state : 2 , // CONTAINER_EXITED
575+ ..Default :: default ( )
576+ } ) ;
577+ lock. push ( Container {
578+ id : "exited-container-2" . into ( ) ,
579+ pod_sandbox_id : "ready-sandbox-123" . into ( ) ,
580+ state : 0 , // CONTAINER_CREATED
581+ ..Default :: default ( )
582+ } ) ;
583+
584+ // Add container statuses (should not be queried since containers are filtered)
585+ let mut lock = mock. container_statuses . lock ( ) . await ;
586+ lock. insert (
587+ "exited-container-1" . into ( ) ,
588+ cri:: ContainerStatusResponse {
589+ info : {
590+ let mut m = HashMap :: new ( ) ;
591+ m. insert ( "info" . into ( ) , r#"{"pid": 0}"# . into ( ) ) ;
592+ m
593+ } ,
594+ ..Default :: default ( )
595+ } ,
596+ ) ;
597+ }
598+
599+ let socket_path = "/tmp/test-cri-exited-containers.sock" ;
600+ let ( client, server_handle) = setup_mock_server ( mock, socket_path) . await ;
601+
602+ let cri_manager = ContainerRuntimeManager :: new_with_client ( client) ;
603+ let result = cri_manager
604+ . get_pids_for_pod ( "pod-with-exited-containers" . to_string ( ) )
605+ . await ;
606+
607+ // Should return NotFound error since no containers are running
608+ assert ! ( result. is_err( ) ) ;
609+ let err = result. unwrap_err ( ) ;
610+ assert_eq ! ( err. code( ) , tonic:: Code :: NotFound ) ;
611+ assert ! ( err. message( ) . contains( "No running containers found" ) ) ;
612+
613+ server_handle. abort ( ) ;
614+ let _ = std:: fs:: remove_file ( socket_path) ;
615+ }
616+
617+ #[ tokio:: test]
618+ async fn test_mixed_container_states_returns_only_running ( ) {
619+ // Test that only running containers are returned when there's a mix
620+ let mock = MockRuntimeService :: default ( ) ;
621+ {
622+ let mut lock = mock. sandboxes . lock ( ) . await ;
623+ lock. push ( cri:: PodSandbox {
624+ id : "mixed-sandbox" . into ( ) ,
625+ state : 0 , // SANDBOX_READY
626+ labels : {
627+ let mut m = HashMap :: new ( ) ;
628+ m. insert ( "io.kubernetes.pod.uid" . into ( ) , "mixed-state-pod" . into ( ) ) ;
629+ m
630+ } ,
631+ ..Default :: default ( )
632+ } ) ;
633+
634+ let mut lock = mock. containers . lock ( ) . await ;
635+ // Running container
636+ lock. push ( Container {
637+ id : "running-container" . into ( ) ,
638+ pod_sandbox_id : "mixed-sandbox" . into ( ) ,
639+ state : 1 , // CONTAINER_RUNNING
640+ ..Default :: default ( )
641+ } ) ;
642+ // Exited container
643+ lock. push ( Container {
644+ id : "exited-container" . into ( ) ,
645+ pod_sandbox_id : "mixed-sandbox" . into ( ) ,
646+ state : 2 , // CONTAINER_EXITED
647+ ..Default :: default ( )
648+ } ) ;
649+ // Created but not started container
650+ lock. push ( Container {
651+ id : "created-container" . into ( ) ,
652+ pod_sandbox_id : "mixed-sandbox" . into ( ) ,
653+ state : 0 , // CONTAINER_CREATED
654+ ..Default :: default ( )
655+ } ) ;
656+
657+ let mut lock = mock. container_statuses . lock ( ) . await ;
658+ lock. insert (
659+ "running-container" . into ( ) ,
660+ cri:: ContainerStatusResponse {
661+ info : {
662+ let mut m = HashMap :: new ( ) ;
663+ m. insert ( "info" . into ( ) , r#"{"pid": 9999}"# . into ( ) ) ;
664+ m
665+ } ,
666+ ..Default :: default ( )
667+ } ,
668+ ) ;
669+ // Exited container would have PID 0
670+ lock. insert (
671+ "exited-container" . into ( ) ,
672+ cri:: ContainerStatusResponse {
673+ info : {
674+ let mut m = HashMap :: new ( ) ;
675+ m. insert ( "info" . into ( ) , r#"{"pid": 0}"# . into ( ) ) ;
676+ m
677+ } ,
678+ ..Default :: default ( )
679+ } ,
680+ ) ;
681+ }
682+
683+ let socket_path = "/tmp/test-cri-mixed-states.sock" ;
684+ let ( client, server_handle) = setup_mock_server ( mock, socket_path) . await ;
685+
686+ let cri_manager = ContainerRuntimeManager :: new_with_client ( client) ;
687+ let result = cri_manager
688+ . get_pids_for_pod ( "mixed-state-pod" . to_string ( ) )
689+ . await ;
690+
691+ // Should only return the PID of the running container
692+ assert ! ( result. is_ok( ) ) ;
693+ let pids = result. unwrap ( ) ;
694+ assert_eq ! ( pids, vec![ 9999 ] ) ;
695+
696+ server_handle. abort ( ) ;
697+ let _ = std:: fs:: remove_file ( socket_path) ;
698+ }
432699}
0 commit comments