@@ -10,9 +10,13 @@ def mock_config():
1010 config = MagicMock (spec = Config )
1111 config .job_grouping_labels = ["app" , "team" ]
1212 config .job_grouping_limit = 3 # Small limit for testing
13+ config .discovery_job_batch_size = 1000
14+ config .discovery_job_max_batches = 50
1315 config .max_workers = 4
1416 config .get_kube_client = MagicMock ()
1517 config .resources = "*"
18+ config .selector = None
19+ config .namespaces = "*" # Add namespaces setting
1620 return config
1721
1822
@@ -23,7 +27,14 @@ def mock_kubernetes_loader(mock_config):
2327 loader = ClusterLoader ()
2428 loader .batch = MagicMock ()
2529 loader .core = MagicMock ()
30+
31+ # Mock executor to return a proper Future
32+ from concurrent .futures import Future
33+ mock_future = Future ()
34+ mock_future .set_result (None ) # Set a dummy result
2635 loader .executor = MagicMock ()
36+ loader .executor .submit .return_value = mock_future
37+
2738 loader ._ClusterLoader__hpa_list = {} # type: ignore # needed for mock
2839 return loader
2940
@@ -60,16 +71,24 @@ async def test_list_all_groupedjobs_with_limit(mock_kubernetes_loader, mock_conf
6071 create_mock_job ("job-9" , "default" , {"app" : "backend" }), # This should be excluded
6172 ]
6273
63- # Mock the _list_namespaced_or_global_objects method
64- mock_kubernetes_loader ._list_namespaced_or_global_objects = AsyncMock (return_value = mock_jobs )
74+ # Mock the _list_namespaced_or_global_objects_batched method
75+ async def mock_batched_method (* args , ** kwargs ):
76+ # Create mock response objects that have the expected structure
77+ mock_response = MagicMock ()
78+ mock_response .items = mock_jobs
79+ mock_response .metadata = MagicMock ()
80+ mock_response .metadata ._continue = None
81+ return (mock_jobs , None ) # Return (jobs, continue_token)
82+ mock_kubernetes_loader ._list_namespaced_or_global_objects_batched = mock_batched_method
6583
6684 # Mock the __build_scannable_object method
6785 def mock_build_scannable_object (item , container , kind ):
6886 obj = MagicMock ()
6987 obj ._api_resource = MagicMock ()
88+ obj .container = container .name
7089 return obj
7190
72- mock_kubernetes_loader ._KubernetesLoader__build_scannable_object = mock_build_scannable_object
91+ mock_kubernetes_loader ._ClusterLoader__build_scannable_object = mock_build_scannable_object
7392
7493 # Patch the settings to use our mock config
7594 with patch ("robusta_krr.core.integrations.kubernetes.settings" , mock_config ):
@@ -88,23 +107,23 @@ def mock_build_scannable_object(item, container, kind):
88107 assert frontend_objects [0 ].namespace == "default"
89108 assert frontend_objects [0 ].container == "main-container"
90109
91- # Verify we got 1 backend object (one per unique container name)
110+ # Verify we got 1 backend object
92111 assert len (backend_objects ) == 1
93112 assert backend_objects [0 ].namespace == "default"
94113 assert backend_objects [0 ].container == "main-container"
95114
96- # Verify all objects in each group have the same grouped_jobs list
115+ # Verify all objects in each group have lightweight job info
97116 frontend_grouped_jobs = frontend_objects [0 ]._api_resource ._grouped_jobs
98117 assert len (frontend_grouped_jobs ) == 3
99- assert frontend_grouped_jobs [0 ].metadata . name == "job-1"
100- assert frontend_grouped_jobs [1 ].metadata . name == "job-2"
101- assert frontend_grouped_jobs [2 ].metadata . name == "job-3"
118+ assert frontend_grouped_jobs [0 ].name == "job-1"
119+ assert frontend_grouped_jobs [1 ].name == "job-2"
120+ assert frontend_grouped_jobs [2 ].name == "job-3"
102121
103122 backend_grouped_jobs = backend_objects [0 ]._api_resource ._grouped_jobs
104123 assert len (backend_grouped_jobs ) == 3
105- assert backend_grouped_jobs [0 ].metadata . name == "job-6"
106- assert backend_grouped_jobs [1 ].metadata . name == "job-7"
107- assert backend_grouped_jobs [2 ].metadata . name == "job-8"
124+ assert backend_grouped_jobs [0 ].name == "job-6"
125+ assert backend_grouped_jobs [1 ].name == "job-7"
126+ assert backend_grouped_jobs [2 ].name == "job-8"
108127
109128
110129@pytest .mark .asyncio
@@ -119,14 +138,22 @@ async def test_list_all_groupedjobs_with_different_namespaces(mock_kubernetes_lo
119138 create_mock_job ("job-4" , "namespace-2" , {"app" : "frontend" }),
120139 ]
121140
122- mock_kubernetes_loader ._list_namespaced_or_global_objects = AsyncMock (return_value = mock_jobs )
141+ async def mock_batched_method (* args , ** kwargs ):
142+ # Create mock response objects that have the expected structure
143+ mock_response = MagicMock ()
144+ mock_response .items = mock_jobs
145+ mock_response .metadata = MagicMock ()
146+ mock_response .metadata ._continue = None
147+ return (mock_jobs , None ) # Return (jobs, continue_token)
148+ mock_kubernetes_loader ._list_namespaced_or_global_objects_batched = mock_batched_method
123149
124150 def mock_build_scannable_object (item , container , kind ):
125151 obj = MagicMock ()
126152 obj ._api_resource = MagicMock ()
153+ obj .container = container .name
127154 return obj
128155
129- mock_kubernetes_loader ._KubernetesLoader__build_scannable_object = mock_build_scannable_object
156+ mock_kubernetes_loader ._ClusterLoader__build_scannable_object = mock_build_scannable_object
130157
131158 # Patch the settings to use our mock config
132159 with patch ("robusta_krr.core.integrations.kubernetes.settings" , mock_config ):
@@ -166,14 +193,22 @@ async def test_list_all_groupedjobs_with_cronjob_owner_reference(mock_kubernetes
166193 # Add CronJob owner reference to the second job
167194 mock_jobs [1 ].metadata .owner_references = [MagicMock (kind = "CronJob" )]
168195
169- mock_kubernetes_loader ._list_namespaced_or_global_objects = AsyncMock (return_value = mock_jobs )
196+ async def mock_batched_method (* args , ** kwargs ):
197+ # Create mock response objects that have the expected structure
198+ mock_response = MagicMock ()
199+ mock_response .items = mock_jobs
200+ mock_response .metadata = MagicMock ()
201+ mock_response .metadata ._continue = None
202+ return (mock_jobs , None ) # Return (jobs, continue_token)
203+ mock_kubernetes_loader ._list_namespaced_or_global_objects_batched = mock_batched_method
170204
171205 def mock_build_scannable_object (item , container , kind ):
172206 obj = MagicMock ()
173207 obj ._api_resource = MagicMock ()
208+ obj .container = container .name # Set the actual container name
174209 return obj
175210
176- mock_kubernetes_loader ._KubernetesLoader__build_scannable_object = mock_build_scannable_object
211+ mock_kubernetes_loader ._ClusterLoader__build_scannable_object = mock_build_scannable_object
177212
178213 # Patch the settings to use our mock config
179214 with patch ("robusta_krr.core.integrations.kubernetes.settings" , mock_config ):
@@ -185,7 +220,7 @@ def mock_build_scannable_object(item, container, kind):
185220 obj = result [0 ]
186221 assert obj .name == "app=frontend"
187222 assert len (obj ._api_resource ._grouped_jobs ) == 1
188- assert obj ._api_resource ._grouped_jobs [0 ].metadata . name == "job-1"
223+ assert obj ._api_resource ._grouped_jobs [0 ].name == "job-1"
189224
190225
191226@pytest .mark .asyncio
@@ -212,14 +247,22 @@ async def test_list_all_groupedjobs_multiple_labels(mock_kubernetes_loader, mock
212247 create_mock_job ("job-3" , "default" , {"app" : "api" }),
213248 ]
214249
215- mock_kubernetes_loader ._list_namespaced_or_global_objects = AsyncMock (return_value = mock_jobs )
250+ async def mock_batched_method (* args , ** kwargs ):
251+ # Create mock response objects that have the expected structure
252+ mock_response = MagicMock ()
253+ mock_response .items = mock_jobs
254+ mock_response .metadata = MagicMock ()
255+ mock_response .metadata ._continue = None
256+ return (mock_jobs , None ) # Return (jobs, continue_token)
257+ mock_kubernetes_loader ._list_namespaced_or_global_objects_batched = mock_batched_method
216258
217259 def mock_build_scannable_object (item , container , kind ):
218260 obj = MagicMock ()
219261 obj ._api_resource = MagicMock ()
262+ obj .container = container .name
220263 return obj
221264
222- mock_kubernetes_loader ._KubernetesLoader__build_scannable_object = mock_build_scannable_object
265+ mock_kubernetes_loader ._ClusterLoader__build_scannable_object = mock_build_scannable_object
223266
224267 # Patch the settings to use our mock config
225268 with patch ("robusta_krr.core.integrations.kubernetes.settings" , mock_config ):
@@ -236,3 +279,65 @@ def mock_build_scannable_object(item, container, kind):
236279
237280 # Verify all objects have the same container name
238281 assert all (obj .container == "main-container" for obj in result )
282+
283+
284+ @pytest .mark .asyncio
285+ async def test_list_all_groupedjobs_job_in_multiple_groups (mock_kubernetes_loader , mock_config ):
286+ """Test that a job with multiple grouping labels is added to all matching groups"""
287+
288+ # Create a job that matches multiple grouping labels
289+ mock_jobs = [
290+ create_mock_job ("job-1" , "default" , {"app" : "frontend" , "team" : "web" }),
291+ create_mock_job ("job-2" , "default" , {"app" : "backend" , "team" : "api" }),
292+ ]
293+
294+ async def mock_batched_method (* args , ** kwargs ):
295+ # Create mock response objects that have the expected structure
296+ mock_response = MagicMock ()
297+ mock_response .items = mock_jobs
298+ mock_response .metadata = MagicMock ()
299+ mock_response .metadata ._continue = None
300+ return (mock_jobs , None ) # Return (jobs, continue_token)
301+ mock_kubernetes_loader ._list_namespaced_or_global_objects_batched = mock_batched_method
302+
303+ def mock_build_scannable_object (item , container , kind ):
304+ obj = MagicMock ()
305+ obj ._api_resource = MagicMock ()
306+ obj .container = container .name
307+ return obj
308+
309+ mock_kubernetes_loader ._ClusterLoader__build_scannable_object = mock_build_scannable_object
310+
311+ # Patch the settings to use our mock config
312+ with patch ("robusta_krr.core.integrations.kubernetes.settings" , mock_config ):
313+ # Call the method
314+ result = await mock_kubernetes_loader ._list_all_groupedjobs ()
315+
316+ # Verify we got 4 objects (2 jobs × 2 labels each = 4 groups)
317+ assert len (result ) == 4
318+
319+ group_names = {g .name for g in result }
320+ assert "app=frontend" in group_names
321+ assert "app=backend" in group_names
322+ assert "team=web" in group_names
323+ assert "team=api" in group_names
324+
325+ # Find each group and verify it contains the correct job
326+ frontend_group = next (g for g in result if g .name == "app=frontend" )
327+ backend_group = next (g for g in result if g .name == "app=backend" )
328+ web_group = next (g for g in result if g .name == "team=web" )
329+ api_group = next (g for g in result if g .name == "team=api" )
330+
331+ # Verify job-1 is in both app=frontend and team=web groups
332+ assert len (frontend_group ._api_resource ._grouped_jobs ) == 1
333+ assert frontend_group ._api_resource ._grouped_jobs [0 ].name == "job-1"
334+
335+ assert len (web_group ._api_resource ._grouped_jobs ) == 1
336+ assert web_group ._api_resource ._grouped_jobs [0 ].name == "job-1"
337+
338+ # Verify job-2 is in both app=backend and team=api groups
339+ assert len (backend_group ._api_resource ._grouped_jobs ) == 1
340+ assert backend_group ._api_resource ._grouped_jobs [0 ].name == "job-2"
341+
342+ assert len (api_group ._api_resource ._grouped_jobs ) == 1
343+ assert api_group ._api_resource ._grouped_jobs [0 ].name == "job-2"
0 commit comments