44import yaml
55
66import easykube
7- from easykube . rest . util import PropertyDict
7+ import httpx
88
9- from capi_janitor .openstack import operator , openstack
9+ from capi_janitor .openstack import openstack
10+ from capi_janitor .openstack import operator
1011from capi_janitor .openstack .operator import OPENSTACK_USER_VOLUMES_RECLAIM_PROPERTY
1112
1213
14+ # Helper to create an async iterable
15+ class AsyncIterList :
16+ def __init__ (self , items ):
17+ self .items = items
18+ self .kwargs = None
19+
20+ async def list (self , ** kwargs ):
21+ self .kwargs = kwargs
22+ for item in self .items :
23+ yield item
24+
25+ def __aiter__ (self ):
26+ self ._iter = iter (self .items )
27+ return self
28+
29+ async def __anext__ (self ):
30+ try :
31+ return next (self ._iter )
32+ except StopIteration :
33+ raise StopAsyncIteration
34+
35+
1336class TestOperator (unittest .IsolatedAsyncioTestCase ):
1437 async def test_operator (self ):
1538 mock_easykube = mock .AsyncMock (spec = easykube .AsyncClient )
@@ -19,6 +42,183 @@ async def test_operator(self):
1942
2043 mock_easykube .aclose .assert_awaited_once_with ()
2144
45+ async def test_fips_for_cluster (self ):
46+ prefix = "Floating IP for Kubernetes external service from cluster"
47+ fips = [
48+ mock .Mock (description = f"{ prefix } mycluster" ),
49+ mock .Mock (description = f"{ prefix } asdf" ),
50+ mock .Mock (description = "Some other description" ),
51+ mock .Mock (description = f"{ prefix } mycluster" ),
52+ ]
53+ resource_mock = AsyncIterList (fips )
54+
55+ result = [
56+ fip async for fip in operator .fips_for_cluster (resource_mock , "mycluster" )
57+ ]
58+
59+ self .assertEqual (len (result ), 2 )
60+ self .assertEqual (result [0 ], fips [0 ])
61+ self .assertEqual (result [1 ], fips [3 ])
62+ self .assertEqual (resource_mock .kwargs , {})
63+
64+ async def test_lbs_for_cluster (self ):
65+ lbs = [
66+ mock .Mock (name = "lb0" ),
67+ mock .Mock (name = "lb1" ),
68+ mock .Mock (name = "lb2" ),
69+ mock .Mock (name = "lb3" ),
70+ ]
71+ # can't pass name into mock.Mock() to do this
72+ lbs [0 ].name = "kube_service_mycluster_api"
73+ lbs [1 ].name = "kube_service_othercluster_api"
74+ lbs [2 ].name = "fake_service_mycluster_api"
75+ lbs [3 ].name = "kube_service_mycluster_ui"
76+ resource_mock = AsyncIterList (lbs )
77+
78+ result = [
79+ lb async for lb in operator .lbs_for_cluster (resource_mock , "mycluster" )
80+ ]
81+
82+ self .assertEqual (len (result ), 2 )
83+ self .assertEqual (result [0 ], lbs [0 ])
84+ self .assertEqual (result [1 ], lbs [3 ])
85+
86+ async def test_secgroups_for_cluster (self ):
87+ prefix = "Security Group for Service LoadBalancer in cluster"
88+ secgroups = [
89+ mock .Mock (description = f"{ prefix } mycluster" ),
90+ mock .Mock (description = f"{ prefix } othercluster" ),
91+ mock .Mock (description = "Other description" ),
92+ mock .Mock (description = f"{ prefix } mycluster" ),
93+ ]
94+ resource_mock = AsyncIterList (secgroups )
95+
96+ result = []
97+ async for sg in operator .secgroups_for_cluster (resource_mock , "mycluster" ):
98+ result .append (sg )
99+
100+ self .assertEqual (len (result ), 2 )
101+ self .assertEqual (result [0 ], secgroups [0 ])
102+ self .assertEqual (result [1 ], secgroups [3 ])
103+
104+ async def test_filtered_volumes_for_cluster (self ):
105+ volumes = [
106+ mock .Mock (
107+ metadata = {
108+ "cinder.csi.openstack.org/cluster" : "mycluster" ,
109+ OPENSTACK_USER_VOLUMES_RECLAIM_PROPERTY : "false" ,
110+ }
111+ ),
112+ mock .Mock (
113+ metadata = {
114+ "cinder.csi.openstack.org/cluster" : "mycluster" ,
115+ OPENSTACK_USER_VOLUMES_RECLAIM_PROPERTY : "true" ,
116+ }
117+ ),
118+ mock .Mock (
119+ metadata = {
120+ "cinder.csi.openstack.org/cluster" : "othercluster" ,
121+ OPENSTACK_USER_VOLUMES_RECLAIM_PROPERTY : "false" ,
122+ }
123+ ),
124+ mock .Mock (
125+ metadata = {
126+ "cinder.csi.openstack.org/cluster" : "mycluster" ,
127+ }
128+ ),
129+ mock .Mock (metadata = {"other_key" : "value" }),
130+ ]
131+ resource_mock = AsyncIterList (volumes )
132+
133+ result = []
134+ async for vol in operator .filtered_volumes_for_cluster (
135+ resource_mock , "mycluster"
136+ ):
137+ result .append (vol )
138+
139+ self .assertEqual (len (result ), 2 )
140+ self .assertEqual (result [0 ], volumes [0 ])
141+ self .assertEqual (result [1 ], volumes [3 ])
142+
143+ async def test_snapshots_for_cluster (self ):
144+ snapshots = [
145+ mock .Mock (metadata = {"cinder.csi.openstack.org/cluster" : "mycluster" }),
146+ mock .Mock (metadata = {"cinder.csi.openstack.org/cluster" : "othercluster" }),
147+ mock .Mock (metadata = {"cinder.csi.openstack.org/cluster" : "mycluster" }),
148+ mock .Mock (metadata = {"cinder.csi.openstack.org/cluster" : "othercluster" }),
149+ # Volumes with invalid metadata
150+ mock .Mock (metadata = {"another_key" : "value" }),
151+ ]
152+ resource_mock = AsyncIterList (snapshots )
153+
154+ result = []
155+ async for snap in operator .snapshots_for_cluster (resource_mock , "mycluster" ):
156+ result .append (snap )
157+
158+ self .assertEqual (len (result ), 2 )
159+ self .assertEqual (result [0 ], snapshots [0 ])
160+ self .assertEqual (result [1 ], snapshots [2 ])
161+
162+ async def test_empty_iterator_returns_true (self ):
163+ async_iter = AsyncIterList ([]).__aiter__ ()
164+ result = await operator .empty (async_iter )
165+ self .assertTrue (result )
166+
167+ async def test_non_empty_iterator_returns_false (self ):
168+ async_iter = AsyncIterList ([1 , 2 , 3 ]).__aiter__ ()
169+ result = await operator .empty (async_iter )
170+ self .assertFalse (result )
171+
172+ async def test_try_delete_success (self ):
173+ instances = [mock .Mock (id = 1 ), mock .Mock (id = 2 )]
174+ resource = mock .Mock ()
175+ resource .delete = mock .AsyncMock ()
176+ resource .singular_name = "test_resource"
177+ logger = mock .Mock ()
178+
179+ result = await operator .try_delete (logger , resource , AsyncIterList (instances ))
180+
181+ self .assertTrue (result )
182+ resource .delete .assert_any_await (1 )
183+ resource .delete .assert_any_await (2 )
184+ logger .warn .assert_not_called ()
185+
186+ async def test_try_delete_with_400_and_409_errors (self ):
187+ instances = [mock .Mock (id = 1 ), mock .Mock (id = 2 )]
188+ resource = mock .Mock ()
189+ resource .singular_name = "test_resource"
190+ logger = mock .Mock ()
191+
192+ resource .delete = mock .AsyncMock (
193+ side_effect = [
194+ httpx .HTTPStatusError (
195+ "error" , request = mock .Mock (), response = mock .Mock (status_code = 400 )
196+ ),
197+ httpx .HTTPStatusError (
198+ "error" , request = mock .Mock (), response = mock .Mock (status_code = 409 )
199+ ),
200+ ]
201+ )
202+ result = await operator .try_delete (logger , resource , AsyncIterList (instances ))
203+
204+ self .assertTrue (result )
205+ self .assertEqual (resource .delete .await_count , 2 )
206+ self .assertEqual (logger .warn .call_count , 2 )
207+
208+ async def test_delete_with_unexpected_error_raises (self ):
209+ instances = [mock .Mock (id = 1 )]
210+ resource = mock .Mock ()
211+ resource .singular_name = "test_resource"
212+ logger = mock .Mock ()
213+
214+ resource .delete = mock .AsyncMock (
215+ side_effect = httpx .HTTPStatusError (
216+ "error" , request = mock .Mock (), response = mock .Mock (status_code = 500 )
217+ )
218+ )
219+ with self .assertRaises (httpx .HTTPStatusError ):
220+ await operator .try_delete (logger , resource , AsyncIterList (instances ))
221+
22222 @mock .patch .object (operator , "patch_finalizers" )
23223 @mock .patch .object (operator , "_get_os_cluster_client" )
24224 async def test_on_openstackcluster_event_adds_finalizers (
@@ -147,47 +347,125 @@ async def test_on_openstackcluster_event_calls_purge(
147347 ]
148348 )
149349
150- @mock .patch .object (openstack , "Resource" )
151- async def test_user_keep_volumes_filter (self , mock_volumes_resource ):
152- # Arrange
153- async def _list_volumes ():
154- test_volumes = [
155- {
156- "id" : "123" ,
157- "name" : "volume-1" ,
158- "metadata" : {
159- "cinder.csi.openstack.org/cluster" : "cluster-1" ,
160- OPENSTACK_USER_VOLUMES_RECLAIM_PROPERTY : "anything-but-true" ,
161- },
162- },
163- {
164- "id" : "456" ,
165- "name" : "volume-2" ,
166- "metadata" : {
167- "cinder.csi.openstack.org/cluster" : "cluster-1" ,
168- OPENSTACK_USER_VOLUMES_RECLAIM_PROPERTY : "true" ,
169- },
170- },
171- {
172- "id" : "789" ,
173- "name" : "volume-3" ,
174- "metadata" : {
175- "cinder.csi.openstack.org/cluster" : "cluster-2" ,
176- OPENSTACK_USER_VOLUMES_RECLAIM_PROPERTY : "true" ,
350+ @mock .patch .object (openstack .Cloud , "from_clouds" )
351+ async def test_purge_openstack_resources_raises (self , mock_from_clouds ):
352+ mock_networkapi = mock .AsyncMock ()
353+ mock_networkapi .resource .side_effect = lambda resource : resource
354+
355+ mock_cloud = mock .AsyncMock ()
356+ mock_cloud .__aenter__ .return_value = mock_cloud
357+ mock_cloud .is_authenticated = False
358+ mock_cloud .current_user_id = "user"
359+ mock_cloud .api_client .return_value = mock_networkapi
360+
361+ mock_from_clouds .return_value = mock_cloud
362+
363+ logger = mock .Mock ()
364+ clouds_yaml_data = {
365+ "clouds" : {
366+ "openstack" : {
367+ "auth" : {
368+ "auth_url" : "https://example.com:5000/v3" ,
369+ "application_credential_id" : "user" ,
370+ "application_credential_secret" : "pass" ,
177371 },
178- },
179- ]
180- for volume in map (PropertyDict , test_volumes ):
181- yield volume
182-
183- mock_volumes_resource .list .return_value = _list_volumes ()
184- # Act
185- filtered_volumes = [
186- v
187- async for v in operator .filtered_volumes_for_cluster (
188- mock_volumes_resource , "cluster-1"
372+ "region_name" : "RegionOne" ,
373+ "interface" : "public" ,
374+ "identity_api_version" : 3 ,
375+ "auth_type" : "v3applicationcredential" ,
376+ }
377+ }
378+ }
379+ with self .assertRaises (openstack .AuthenticationError ) as e :
380+ await operator .purge_openstack_resources (
381+ logger ,
382+ clouds_yaml_data ,
383+ "openstack" ,
384+ None ,
385+ "mycluster" ,
386+ True ,
387+ False ,
189388 )
190- ]
191- # Assert
192- self .assertEqual (len (filtered_volumes ), 1 )
193- self .assertEqual (filtered_volumes [0 ].get ("name" ), "volume-1" )
389+ self .assertEqual (
390+ str (e .exception ),
391+ "failed to authenticate as user: user" ,
392+ )
393+
394+ # @mock.patch.object(openstack.Cloud, "from_clouds")
395+ # async def test_purge_openstack_resources_success(self, mock_from_clouds):
396+ # # Mocking the cloud object and API clients
397+ # mock_cloud = mock.AsyncMock()
398+ # mock_networkapi = mock.AsyncMock()
399+ # mock_lbapi = mock.AsyncMock()
400+ # mock_volumeapi = mock.AsyncMock()
401+ # mock_identityapi = mock.AsyncMock()
402+
403+ # # Mock the __aenter__ to return the mock_cloud
404+ # mock_cloud.__aenter__.return_value = mock_cloud
405+ # mock_cloud.is_authenticated = True
406+ # mock_cloud.current_user_id = "user"
407+ # mock_cloud.api_client.return_value = mock_networkapi
408+
409+ # # Return mock clients for different services when requested
410+ # mock_from_clouds.return_value = mock_cloud
411+
412+ # # Mocking the resources for Network, Load Balancer, and Volume APIs
413+ # mock_networkapi.resource.side_effect = lambda resource: {
414+ # "floatingips": mock.AsyncMock(),
415+ # "security-groups": mock.AsyncMock(),
416+ # }.get(resource, None)
417+
418+ # mock_lbapi.resource.side_effect = lambda resource: {
419+ # "loadbalancers": mock.AsyncMock(),
420+ # }.get(resource, None)
421+
422+ # mock_volumeapi.resource.side_effect = lambda resource: {
423+ # "snapshots/detail": mock.AsyncMock(),
424+ # "snapshots": mock.AsyncMock(),
425+ # "volumes/detail": mock.AsyncMock(),
426+ # "volumes": mock.AsyncMock(),
427+ # }.get(resource, None)
428+
429+ # mock_identityapi.resource.side_effect = lambda resource: {
430+ # "application_credentials": mock.AsyncMock(),
431+ # }.get(resource, None)
432+
433+ # # Mock logger
434+ # logger = mock.Mock()
435+
436+ # clouds_yaml_data = {
437+ # "clouds": {
438+ # "openstack": {
439+ # "auth": {
440+ # "auth_url": "https://example.com:5000/v3",
441+ # "application_credential_id": "user",
442+ # "application_credential_secret": "pass",
443+ # },
444+ # "region_name": "RegionOne",
445+ # "interface": "public",
446+ # "identity_api_version": 3,
447+ # "auth_type": "v3applicationcredential",
448+ # }
449+ # }
450+ # }
451+
452+ # # Simulate the purge_openstack_resources method behavior
453+ # await operator.purge_openstack_resources(
454+ # logger,
455+ # clouds_yaml_data, # Pass the mock cloud config
456+ # "openstack",
457+ # None,
458+ # "mycluster",
459+ # True,
460+ # False,
461+ # )
462+
463+ # # Add assertions here based on expected behavior
464+ # # Example: Check that the resources were interacted with
465+ # mock_networkapi.resource.assert_any_call("floatingips")
466+ # mock_lbapi.resource.assert_any_call("loadbalancers")
467+ # mock_volumeapi.resource.assert_any_call("snapshots")
468+ # mock_volumeapi.resource.assert_any_call("volumes")
469+
470+ # # Example: Validate if appcred deletion was attempted
471+ # mock_identityapi.resource.assert_any_call("application_credentials")
0 commit comments