4
4
import yaml
5
5
6
6
import easykube
7
- from easykube . rest . util import PropertyDict
7
+ import httpx
8
8
9
- from capi_janitor .openstack import operator , openstack
9
+ from capi_janitor .openstack import openstack
10
+ from capi_janitor .openstack import operator
10
11
from capi_janitor .openstack .operator import OPENSTACK_USER_VOLUMES_RECLAIM_PROPERTY
11
12
12
13
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
+
13
36
class TestOperator (unittest .IsolatedAsyncioTestCase ):
14
37
async def test_operator (self ):
15
38
mock_easykube = mock .AsyncMock (spec = easykube .AsyncClient )
@@ -19,6 +42,183 @@ async def test_operator(self):
19
42
20
43
mock_easykube .aclose .assert_awaited_once_with ()
21
44
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
+
22
222
@mock .patch .object (operator , "patch_finalizers" )
23
223
@mock .patch .object (operator , "_get_os_cluster_client" )
24
224
async def test_on_openstackcluster_event_adds_finalizers (
@@ -147,47 +347,125 @@ async def test_on_openstackcluster_event_calls_purge(
147
347
]
148
348
)
149
349
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" ,
177
371
},
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 ,
189
388
)
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