Skip to content

Commit 59a0008

Browse files
committed
add user wait and ac uplload
1 parent 077ba59 commit 59a0008

File tree

2 files changed

+95
-34
lines changed

2 files changed

+95
-34
lines changed

docker/mongodb-kubernetes-tests/kubetester/mongotester.py

Lines changed: 81 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,19 @@
99
from typing import Callable, Dict, List, Optional
1010

1111
import pymongo
12-
from kubetester import kubetester
13-
from kubetester.kubetester import KubernetesTester
1412
from opentelemetry import trace
1513
from pycognito import Cognito
1614
from pymongo.auth_oidc import OIDCCallback, OIDCCallbackContext, OIDCCallbackResult
1715
from pymongo.errors import OperationFailure, PyMongoError, ServerSelectionTimeoutError
1816
from pytest import fail
1917

18+
from tests.conftest import get_central_cluster_client
19+
import kubernetes.client as client
20+
from kubetester import kubetester
21+
from kubetester.kubetester import KubernetesTester
22+
from kubetester.mongodb_user import MongoDBUser
23+
from kubetester.phase import Phase
24+
2025
TEST_DB = "test-db"
2126
TEST_COLLECTION = "test-collection"
2227

@@ -76,6 +81,57 @@ def fetch(self, context: OIDCCallbackContext) -> OIDCCallbackResult:
7681
return OIDCCallbackResult(access_token=u.id_token)
7782

7883

84+
def _wait_for_mongodbuser_reconciliation() -> None:
85+
"""
86+
Wait for ALL MongoDBUser resources in the namespace to be reconciled before attempting authentication.
87+
This prevents race conditions when passwords or user configurations have been recently changed.
88+
89+
Lists all MongoDBUser resources in the namespace and waits for ALL of them to reach Updated phase.
90+
"""
91+
try:
92+
namespace = KubernetesTester.get_namespace()
93+
api_client = client.CustomObjectsApi(api_client=get_central_cluster_client())
94+
95+
try:
96+
mongodb_users = api_client.list_namespaced_custom_object(
97+
group="mongodb.com",
98+
version="v1",
99+
namespace=namespace,
100+
plural="mongodbusers"
101+
)
102+
103+
all_users = []
104+
105+
for user_item in mongodb_users.get("items", []):
106+
user_name = user_item.get("metadata", {}).get("name", "unknown")
107+
username = user_item.get("spec", {}).get("username", "unknown")
108+
all_users.append((user_name, username))
109+
110+
if not all_users:
111+
return
112+
113+
logging.info(f"Found {len(all_users)} MongoDBUser resource(s) in namespace '{namespace}', waiting for all to reach Updated phase...")
114+
115+
for user_name, username in all_users:
116+
try:
117+
logging.info(f"Waiting for MongoDBUser '{user_name}' (username: {username}) to reach Updated phase...")
118+
119+
user = MongoDBUser(name=user_name, namespace=namespace)
120+
user.assert_reaches_phase(Phase.Updated, timeout=300)
121+
logging.info(f"MongoDBUser '{user_name}' reached Updated phase - reconciliation complete")
122+
123+
except Exception as e:
124+
logging.warning(f"Failed to wait for MongoDBUser '{user_name}' reconciliation: {e}")
125+
# Continue with other users - don't fail the entire test
126+
127+
logging.info("All MongoDBUser resources reconciliation check complete")
128+
129+
except Exception as e:
130+
logging.warning(f"Failed to list MongoDBUser resources: {e} - proceeding without reconciliation wait")
131+
except Exception as e:
132+
logging.warning(f"Error while waiting for MongoDBUser reconciliation: {e} - proceeding with authentication")
133+
134+
79135
class MongoTester:
80136
"""MongoTester is a general abstraction to work with mongo database. It encapsulates the client created in
81137
the constructor. All general methods non-specific to types of mongodb topologies should reside here."""
@@ -115,7 +171,7 @@ def _init_client(self, **kwargs):
115171

116172
def assert_connectivity(
117173
self,
118-
attempts: int = 30,
174+
attempts: int = 50,
119175
db: str = "admin",
120176
col: str = "myCol",
121177
opts: Optional[List[Dict[str, any]]] = None,
@@ -175,13 +231,17 @@ def assert_scram_sha_authentication(
175231
username: str,
176232
password: str,
177233
auth_mechanism: str,
178-
attempts: int = 30,
234+
attempts: int = 50,
179235
ssl: bool = False,
180236
**kwargs,
181237
) -> None:
182238
assert attempts > 0
183239
assert auth_mechanism in {"SCRAM-SHA-256", "SCRAM-SHA-1"}
184240

241+
# Wait for ALL MongoDBUser resources to be reconciled before attempting authentication
242+
# This prevents race conditions when passwords have been recently changed
243+
_wait_for_mongodbuser_reconciliation()
244+
185245
for i in reversed(range(attempts)):
186246
try:
187247
self._authenticate_with_scram(
@@ -209,7 +269,7 @@ def assert_scram_sha_authentication_fails(
209269
self,
210270
username: str,
211271
password: str,
212-
retries: int = 30,
272+
attempts: int = 50,
213273
ssl: bool = False,
214274
**kwargs,
215275
):
@@ -219,13 +279,16 @@ def assert_scram_sha_authentication_fails(
219279
which still exists. When we change a password, we should eventually no longer be able to auth with
220280
that user's credentials.
221281
"""
222-
for i in range(retries):
282+
283+
_wait_for_mongodbuser_reconciliation()
284+
285+
for i in range(attempts):
223286
try:
224287
self._authenticate_with_scram(username, password, ssl=ssl, **kwargs)
225288
except OperationFailure:
226289
return
227290
time.sleep(5)
228-
fail(f"was still able to authenticate with username={username} password={password} after {retries} attempts")
291+
fail(f"was still able to authenticate with username={username} password={password} after {attempts} attempts")
229292

230293
def _authenticate_with_scram(
231294
self,
@@ -247,9 +310,11 @@ def _authenticate_with_scram(
247310
# authentication doesn't actually happen until we interact with a database
248311
self.client["admin"]["myCol"].insert_one({})
249312

250-
def assert_x509_authentication(self, cert_file_name: str, attempts: int = 30, **kwargs):
313+
def assert_x509_authentication(self, cert_file_name: str, attempts: int = 50, **kwargs):
251314
assert attempts > 0
252315

316+
_wait_for_mongodbuser_reconciliation()
317+
253318
options = self._merge_options(
254319
[
255320
with_x509(cert_file_name, kwargs.get("tlsCAFile", kubetester.SSL_CA_CERT)),
@@ -267,14 +332,7 @@ def assert_x509_authentication(self, cert_file_name: str, attempts: int = 30, **
267332
if attempts == 0:
268333
fail(f"unable to authenticate after {total_attempts} attempts")
269334

270-
# Use adaptive retry delays for better credential propagation handling
271-
remaining_attempts = attempts
272-
if remaining_attempts >= total_attempts * 0.7: # First ~30% of attempts
273-
delay = 10 # Longer delay for initial propagation
274-
else:
275-
delay = 5 # Standard delay for normal retries
276-
277-
time.sleep(delay)
335+
time.sleep(5)
278336

279337
def assert_ldap_authentication(
280338
self,
@@ -284,8 +342,9 @@ def assert_ldap_authentication(
284342
collection: str = "myCol",
285343
tls_ca_file: Optional[str] = None,
286344
ssl_certfile: str = None,
287-
attempts: int = 30,
345+
attempts: int = 50,
288346
):
347+
_wait_for_mongodbuser_reconciliation()
289348

290349
options = with_ldap(ssl_certfile, tls_ca_file)
291350
total_attempts = attempts
@@ -307,23 +366,18 @@ def assert_ldap_authentication(
307366
if attempts <= 0:
308367
fail(f"unable to authenticate after {total_attempts} attempts")
309368

310-
# Use adaptive retry delays for better credential propagation handling
311-
remaining_attempts = attempts
312-
if remaining_attempts >= total_attempts * 0.7: # First ~30% of attempts
313-
delay = 10 # Longer delay for initial propagation
314-
else:
315-
delay = 5 # Standard delay for normal retries
316-
317-
time.sleep(delay)
369+
time.sleep(5)
318370

319371
def assert_oidc_authentication(
320372
self,
321373
db: str = "admin",
322374
collection: str = "myCol",
323-
attempts: int = 10,
375+
attempts: int = 50,
324376
):
325377
assert attempts > 0
326378

379+
_wait_for_mongodbuser_reconciliation()
380+
327381
props = {"OIDC_CALLBACK": MyOIDCCallback()}
328382

329383
total_attempts = attempts
@@ -342,14 +396,7 @@ def assert_oidc_authentication(
342396
if attempts == 0:
343397
raise RuntimeError(f"Unable to authenticate after {total_attempts} attempts: {e}")
344398

345-
# Use adaptive retry delays for better credential propagation handling
346-
remaining_attempts = attempts
347-
if remaining_attempts >= total_attempts * 0.7: # First ~30% of attempts
348-
delay = 10 # Longer delay for initial propagation
349-
else:
350-
delay = 5 # Standard delay for normal retries
351-
352-
time.sleep(delay)
399+
time.sleep(5)
353400

354401
def assert_oidc_authentication_fails(self, db: str = "admin", collection: str = "myCol", attempts: int = 10):
355402
assert attempts > 0

docker/mongodb-kubernetes-tests/tests/conftest.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1684,6 +1684,20 @@ def pytest_sessionfinish(session, exitstatus):
16841684
ev = tester.get_project_events().json()["results"]
16851685
with open(f"/tmp/diagnostics/{project_id}-events.json", "w", encoding="utf-8") as f:
16861686
json.dump(ev, f, ensure_ascii=False, indent=4)
1687+
1688+
if exitstatus != 0:
1689+
try:
1690+
automation_config_tester = tester.get_automation_config_tester()
1691+
automation_config = automation_config_tester.automation_config
1692+
if not automation_config:
1693+
continue
1694+
1695+
with open(f"/tmp/diagnostics/{project_id}-automation-config.json", "w", encoding="utf-8") as f:
1696+
json.dump(automation_config, f, ensure_ascii=False, indent=4)
1697+
1698+
logging.info(f"Saved automation config for project {project_id}")
1699+
except Exception as e:
1700+
logging.warning(f"Failed to collect automation config for project {project_id}: {e}")
16871701
else:
16881702
logging.info("om is not healthy - not collecting events information")
16891703

0 commit comments

Comments
 (0)