11
11
import pymongo
12
12
from kubetester import kubetester
13
13
from kubetester .kubetester import KubernetesTester
14
+ from kubetester .phase import Phase
14
15
from opentelemetry import trace
15
16
from pycognito import Cognito
16
17
from pymongo .auth_oidc import OIDCCallback , OIDCCallbackContext , OIDCCallbackResult
@@ -76,6 +77,63 @@ def fetch(self, context: OIDCCallbackContext) -> OIDCCallbackResult:
76
77
return OIDCCallbackResult (access_token = u .id_token )
77
78
78
79
80
+ def _wait_for_mongodbuser_reconciliation () -> None :
81
+ """
82
+ Wait for ALL MongoDBUser resources in the namespace to be reconciled before attempting authentication.
83
+ This prevents race conditions when passwords or user configurations have been recently changed.
84
+
85
+ Lists all MongoDBUser resources in the namespace and waits for ALL of them to reach Updated phase.
86
+ """
87
+ try :
88
+ # Import inside function to avoid circular imports
89
+ import kubernetes .client as client
90
+ from kubetester .mongodb_user import MongoDBUser
91
+ from tests .conftest import get_central_cluster_client
92
+
93
+ namespace = KubernetesTester .get_namespace ()
94
+ api_client = client .CustomObjectsApi (api_client = get_central_cluster_client ())
95
+
96
+ try :
97
+ mongodb_users = api_client .list_namespaced_custom_object (
98
+ group = "mongodb.com" , version = "v1" , namespace = namespace , plural = "mongodbusers"
99
+ )
100
+
101
+ all_users = []
102
+
103
+ for user_item in mongodb_users .get ("items" , []):
104
+ user_name = user_item .get ("metadata" , {}).get ("name" , "unknown" )
105
+ username = user_item .get ("spec" , {}).get ("username" , "unknown" )
106
+ all_users .append ((user_name , username ))
107
+
108
+ if not all_users :
109
+ return
110
+
111
+ logging .info (
112
+ f"Found { len (all_users )} MongoDBUser resource(s) in namespace '{ namespace } ', waiting for all to reach Updated phase..."
113
+ )
114
+
115
+ for user_name , username in all_users :
116
+ try :
117
+ logging .info (
118
+ f"Waiting for MongoDBUser '{ user_name } ' (username: { username } ) to reach Updated phase..."
119
+ )
120
+
121
+ user = MongoDBUser (name = user_name , namespace = namespace )
122
+ user .assert_reaches_phase (Phase .Updated , timeout = 300 )
123
+ logging .info (f"MongoDBUser '{ user_name } ' reached Updated phase - reconciliation complete" )
124
+
125
+ except Exception as e :
126
+ logging .warning (f"Failed to wait for MongoDBUser '{ user_name } ' reconciliation: { e } " )
127
+ # Continue with other users - don't fail the entire test
128
+
129
+ logging .info ("All MongoDBUser resources reconciliation check complete" )
130
+
131
+ except Exception as e :
132
+ logging .warning (f"Failed to list MongoDBUser resources: { e } - proceeding without reconciliation wait" )
133
+ except Exception as e :
134
+ logging .warning (f"Error while waiting for MongoDBUser reconciliation: { e } - proceeding with authentication" )
135
+
136
+
79
137
class MongoTester :
80
138
"""MongoTester is a general abstraction to work with mongo database. It encapsulates the client created in
81
139
the constructor. All general methods non-specific to types of mongodb topologies should reside here."""
@@ -115,7 +173,7 @@ def _init_client(self, **kwargs):
115
173
116
174
def assert_connectivity (
117
175
self ,
118
- attempts : int = 20 ,
176
+ attempts : int = 50 ,
119
177
db : str = "admin" ,
120
178
col : str = "myCol" ,
121
179
opts : Optional [List [Dict [str , any ]]] = None ,
@@ -175,13 +233,17 @@ def assert_scram_sha_authentication(
175
233
username : str ,
176
234
password : str ,
177
235
auth_mechanism : str ,
178
- attempts : int = 20 ,
236
+ attempts : int = 50 ,
179
237
ssl : bool = False ,
180
238
** kwargs ,
181
239
) -> None :
182
240
assert attempts > 0
183
241
assert auth_mechanism in {"SCRAM-SHA-256" , "SCRAM-SHA-1" }
184
242
243
+ # Wait for ALL MongoDBUser resources to be reconciled before attempting authentication
244
+ # This prevents race conditions when passwords have been recently changed
245
+ _wait_for_mongodbuser_reconciliation ()
246
+
185
247
for i in reversed (range (attempts )):
186
248
try :
187
249
self ._authenticate_with_scram (
@@ -194,14 +256,15 @@ def assert_scram_sha_authentication(
194
256
return
195
257
except OperationFailure as e :
196
258
if i == 0 :
197
- fail (msg = f"unable to authenticate after { attempts } attempts with error: { e } " )
259
+ fail (f"unable to authenticate after { attempts } attempts with error: { e } " )
260
+
198
261
time .sleep (5 )
199
262
200
263
def assert_scram_sha_authentication_fails (
201
264
self ,
202
265
username : str ,
203
266
password : str ,
204
- retries : int = 20 ,
267
+ attempts : int = 50 ,
205
268
ssl : bool = False ,
206
269
** kwargs ,
207
270
):
@@ -211,13 +274,16 @@ def assert_scram_sha_authentication_fails(
211
274
which still exists. When we change a password, we should eventually no longer be able to auth with
212
275
that user's credentials.
213
276
"""
214
- for i in range (retries ):
277
+
278
+ _wait_for_mongodbuser_reconciliation ()
279
+
280
+ for i in range (attempts ):
215
281
try :
216
282
self ._authenticate_with_scram (username , password , ssl = ssl , ** kwargs )
217
283
except OperationFailure :
218
284
return
219
285
time .sleep (5 )
220
- fail (f"was still able to authenticate with username={ username } password={ password } after { retries } attempts" )
286
+ fail (f"was still able to authenticate with username={ username } password={ password } after { attempts } attempts" )
221
287
222
288
def _authenticate_with_scram (
223
289
self ,
@@ -239,9 +305,11 @@ def _authenticate_with_scram(
239
305
# authentication doesn't actually happen until we interact with a database
240
306
self .client ["admin" ]["myCol" ].insert_one ({})
241
307
242
- def assert_x509_authentication (self , cert_file_name : str , attempts : int = 20 , ** kwargs ):
308
+ def assert_x509_authentication (self , cert_file_name : str , attempts : int = 50 , ** kwargs ):
243
309
assert attempts > 0
244
310
311
+ _wait_for_mongodbuser_reconciliation ()
312
+
245
313
options = self ._merge_options (
246
314
[
247
315
with_x509 (cert_file_name , kwargs .get ("tlsCAFile" , kubetester .SSL_CA_CERT )),
@@ -257,7 +325,8 @@ def assert_x509_authentication(self, cert_file_name: str, attempts: int = 20, **
257
325
return
258
326
except OperationFailure :
259
327
if attempts == 0 :
260
- fail (msg = f"unable to authenticate after { total_attempts } attempts" )
328
+ fail (f"unable to authenticate after { total_attempts } attempts" )
329
+
261
330
time .sleep (5 )
262
331
263
332
def assert_ldap_authentication (
@@ -268,8 +337,9 @@ def assert_ldap_authentication(
268
337
collection : str = "myCol" ,
269
338
tls_ca_file : Optional [str ] = None ,
270
339
ssl_certfile : str = None ,
271
- attempts : int = 20 ,
340
+ attempts : int = 50 ,
272
341
):
342
+ _wait_for_mongodbuser_reconciliation ()
273
343
274
344
options = with_ldap (ssl_certfile , tls_ca_file )
275
345
total_attempts = attempts
@@ -289,17 +359,20 @@ def assert_ldap_authentication(
289
359
return
290
360
except OperationFailure :
291
361
if attempts <= 0 :
292
- fail (msg = f"unable to authenticate after { total_attempts } attempts" )
362
+ fail (f"unable to authenticate after { total_attempts } attempts" )
363
+
293
364
time .sleep (5 )
294
365
295
366
def assert_oidc_authentication (
296
367
self ,
297
368
db : str = "admin" ,
298
369
collection : str = "myCol" ,
299
- attempts : int = 10 ,
370
+ attempts : int = 50 ,
300
371
):
301
372
assert attempts > 0
302
373
374
+ _wait_for_mongodbuser_reconciliation ()
375
+
303
376
props = {"OIDC_CALLBACK" : MyOIDCCallback ()}
304
377
305
378
total_attempts = attempts
@@ -317,6 +390,7 @@ def assert_oidc_authentication(
317
390
except OperationFailure as e :
318
391
if attempts == 0 :
319
392
raise RuntimeError (f"Unable to authenticate after { total_attempts } attempts: { e } " )
393
+
320
394
time .sleep (5 )
321
395
322
396
def assert_oidc_authentication_fails (self , db : str = "admin" , collection : str = "myCol" , attempts : int = 10 ):
@@ -326,7 +400,7 @@ def assert_oidc_authentication_fails(self, db: str = "admin", collection: str =
326
400
attempts -= 1
327
401
try :
328
402
if attempts <= 0 :
329
- fail (msg = f"was able to authenticate with OIDC after { total_attempts } attempts" )
403
+ fail (f"was able to authenticate with OIDC after { total_attempts } attempts" )
330
404
331
405
self .assert_oidc_authentication (db , collection , 1 )
332
406
time .sleep (5 )
@@ -362,7 +436,7 @@ def assert_deployment_reachable(self, attempts: int = 10):
362
436
if hosts_unreachable == 0 :
363
437
return
364
438
if attempts <= 0 :
365
- fail (msg = "Some hosts still report NO_DATA state" )
439
+ fail ("Some hosts still report NO_DATA state" )
366
440
time .sleep (10 )
367
441
368
442
0 commit comments