1111import pymongo
1212from kubetester import kubetester
1313from kubetester .kubetester import KubernetesTester
14+ from kubetester .phase import Phase
1415from opentelemetry import trace
1516from pycognito import Cognito
1617from pymongo .auth_oidc import OIDCCallback , OIDCCallbackContext , OIDCCallbackResult
@@ -76,6 +77,63 @@ def fetch(self, context: OIDCCallbackContext) -> OIDCCallbackResult:
7677 return OIDCCallbackResult (access_token = u .id_token )
7778
7879
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+
79137class MongoTester :
80138 """MongoTester is a general abstraction to work with mongo database. It encapsulates the client created in
81139 the constructor. All general methods non-specific to types of mongodb topologies should reside here."""
@@ -115,7 +173,7 @@ def _init_client(self, **kwargs):
115173
116174 def assert_connectivity (
117175 self ,
118- attempts : int = 20 ,
176+ attempts : int = 50 ,
119177 db : str = "admin" ,
120178 col : str = "myCol" ,
121179 opts : Optional [List [Dict [str , any ]]] = None ,
@@ -175,13 +233,17 @@ def assert_scram_sha_authentication(
175233 username : str ,
176234 password : str ,
177235 auth_mechanism : str ,
178- attempts : int = 20 ,
236+ attempts : int = 50 ,
179237 ssl : bool = False ,
180238 ** kwargs ,
181239 ) -> None :
182240 assert attempts > 0
183241 assert auth_mechanism in {"SCRAM-SHA-256" , "SCRAM-SHA-1" }
184242
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+
185247 for i in reversed (range (attempts )):
186248 try :
187249 self ._authenticate_with_scram (
@@ -194,14 +256,15 @@ def assert_scram_sha_authentication(
194256 return
195257 except OperationFailure as e :
196258 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+
198261 time .sleep (5 )
199262
200263 def assert_scram_sha_authentication_fails (
201264 self ,
202265 username : str ,
203266 password : str ,
204- retries : int = 20 ,
267+ attempts : int = 50 ,
205268 ssl : bool = False ,
206269 ** kwargs ,
207270 ):
@@ -211,13 +274,16 @@ def assert_scram_sha_authentication_fails(
211274 which still exists. When we change a password, we should eventually no longer be able to auth with
212275 that user's credentials.
213276 """
214- for i in range (retries ):
277+
278+ _wait_for_mongodbuser_reconciliation ()
279+
280+ for i in range (attempts ):
215281 try :
216282 self ._authenticate_with_scram (username , password , ssl = ssl , ** kwargs )
217283 except OperationFailure :
218284 return
219285 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" )
221287
222288 def _authenticate_with_scram (
223289 self ,
@@ -239,9 +305,11 @@ def _authenticate_with_scram(
239305 # authentication doesn't actually happen until we interact with a database
240306 self .client ["admin" ]["myCol" ].insert_one ({})
241307
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 ):
243309 assert attempts > 0
244310
311+ _wait_for_mongodbuser_reconciliation ()
312+
245313 options = self ._merge_options (
246314 [
247315 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, **
257325 return
258326 except OperationFailure :
259327 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+
261330 time .sleep (5 )
262331
263332 def assert_ldap_authentication (
@@ -268,8 +337,9 @@ def assert_ldap_authentication(
268337 collection : str = "myCol" ,
269338 tls_ca_file : Optional [str ] = None ,
270339 ssl_certfile : str = None ,
271- attempts : int = 20 ,
340+ attempts : int = 50 ,
272341 ):
342+ _wait_for_mongodbuser_reconciliation ()
273343
274344 options = with_ldap (ssl_certfile , tls_ca_file )
275345 total_attempts = attempts
@@ -289,17 +359,20 @@ def assert_ldap_authentication(
289359 return
290360 except OperationFailure :
291361 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+
293364 time .sleep (5 )
294365
295366 def assert_oidc_authentication (
296367 self ,
297368 db : str = "admin" ,
298369 collection : str = "myCol" ,
299- attempts : int = 10 ,
370+ attempts : int = 50 ,
300371 ):
301372 assert attempts > 0
302373
374+ _wait_for_mongodbuser_reconciliation ()
375+
303376 props = {"OIDC_CALLBACK" : MyOIDCCallback ()}
304377
305378 total_attempts = attempts
@@ -317,6 +390,7 @@ def assert_oidc_authentication(
317390 except OperationFailure as e :
318391 if attempts == 0 :
319392 raise RuntimeError (f"Unable to authenticate after { total_attempts } attempts: { e } " )
393+
320394 time .sleep (5 )
321395
322396 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 =
326400 attempts -= 1
327401 try :
328402 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" )
330404
331405 self .assert_oidc_authentication (db , collection , 1 )
332406 time .sleep (5 )
@@ -362,7 +436,7 @@ def assert_deployment_reachable(self, attempts: int = 10):
362436 if hosts_unreachable == 0 :
363437 return
364438 if attempts <= 0 :
365- fail (msg = "Some hosts still report NO_DATA state" )
439+ fail ("Some hosts still report NO_DATA state" )
366440 time .sleep (10 )
367441
368442
0 commit comments