Skip to content

Commit a624e47

Browse files
authored
Search: e2e for sharded with single mongot (#823)
Missing e2e test for sharded search with one mongot instance
1 parent 23f196b commit a624e47

19 files changed

+1033
-196
lines changed

.evergreen-tasks.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1359,3 +1359,8 @@ tasks:
13591359
tags: [ "patch-run" ]
13601360
commands:
13611361
- func: "e2e_test"
1362+
1363+
- name: e2e_search_sharded_external_mongod_single_mongot
1364+
tags: [ "patch-run" ]
1365+
commands:
1366+
- func: "e2e_test"

.evergreen.yml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -811,6 +811,7 @@ task_groups:
811811
- e2e_search_enterprise_basic
812812
- e2e_search_enterprise_tls
813813
- e2e_search_enterprise_x509_cluster_auth
814+
- e2e_search_sharded_external_mongod_single_mongot
814815
<<: *teardown_group
815816

816817
# this task group contains just a one task, which is smoke testing whether the operator
@@ -1318,7 +1319,7 @@ buildvariants:
13181319
display_name: e2e_mdb_kind_ubi_cloudqa
13191320
tags: [ "pr_patch", "staging", "e2e_test_suite", "cloudqa", "cloudqa_non_static" ]
13201321
run_on:
1321-
- ubuntu2404-medium
1322+
- ubuntu2404-large
13221323
<<: *base_no_om_image_dependency
13231324
tasks:
13241325
- name: e2e_mdb_kind_cloudqa_task_group
@@ -1336,7 +1337,7 @@ buildvariants:
13361337
display_name: e2e_static_mdb_kind_ubi_cloudqa
13371338
tags: [ "pr_patch", "staging", "e2e_test_suite", "cloudqa", "static" ]
13381339
run_on:
1339-
- ubuntu2404-medium
1340+
- ubuntu2404-large
13401341
<<: *base_no_om_image_dependency
13411342
tasks:
13421343
- name: e2e_mdb_kind_cloudqa_task_group

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

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -487,6 +487,7 @@ def create_sharded_cluster_certs(
487487
shard_distribution: Optional[List[int]] = None,
488488
mongos_distribution: Optional[List[int]] = None,
489489
config_srv_distribution: Optional[List[int]] = None,
490+
mongos_service_dns_names: Optional[List[str]] = None,
490491
):
491492
cert_generation_func = create_mongodb_tls_certs
492493
if x509_certs:
@@ -573,9 +574,8 @@ def create_sharded_cluster_certs(
573574
secret_backend=secret_backend,
574575
)
575576

576-
additional_domains_for_mongos = None
577+
additional_domains_for_mongos = []
577578
if additional_domains is not None:
578-
additional_domains_for_mongos = []
579579
for domain in additional_domains:
580580
if mongos_distribution is None:
581581
for pod_idx in range(mongos):
@@ -585,6 +585,10 @@ def create_sharded_cluster_certs(
585585
for pod_idx in range(pod_count or 0):
586586
additional_domains_for_mongos.append(f"{resource_name}-mongos-{cluster_idx}-{pod_idx}.{domain}")
587587

588+
# Add service DNS names directly (e.g., for mongot to connect to mongos service)
589+
if mongos_service_dns_names is not None:
590+
additional_domains_for_mongos.extend(mongos_service_dns_names)
591+
588592
secret_name = f"{resource_name}-mongos-cert"
589593
if secret_prefix is not None:
590594
secret_name = secret_prefix + secret_name
@@ -596,7 +600,7 @@ def create_sharded_cluster_certs(
596600
service_name=resource_name + "-svc",
597601
replicas=mongos,
598602
replicas_cluster_distribution=mongos_distribution,
599-
additional_domains=additional_domains_for_mongos,
603+
additional_domains=additional_domains_for_mongos if additional_domains_for_mongos else None,
600604
secret_backend=secret_backend,
601605
)
602606

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
from kubeobject import CustomObject
66

77

8-
class TestCreateOrUpdate(ctx, unittest.TestCase):
8+
class TestCreateOrUpdate(unittest.TestCase):
99
def test_create_or_update_is_not_bound(self):
1010
api_client = MagicMock()
1111
custom_object = CustomObject(
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
import tarfile
2+
import tempfile
3+
4+
from kubernetes import client, config
5+
from kubernetes.stream import stream
6+
from kubetester import get_pod_when_ready
7+
from tests import test_logger
8+
9+
logger = test_logger.get_test_logger(__name__)
10+
11+
TOOLS_POD_NAME = "mongodb-tools-pod"
12+
TOOLS_POD_IMAGE = "mongodb/mongodb-community-server:8.0-ubi9"
13+
14+
15+
class ToolsPod:
16+
"""A pod running MongoDB tools for executing commands like mongorestore inside the cluster."""
17+
18+
def __init__(self, namespace: str):
19+
self.namespace = namespace
20+
self.pod_name = TOOLS_POD_NAME
21+
self.core_v1 = client.CoreV1Api()
22+
23+
def run_command(self, cmd: list[str]):
24+
"""Execute a command in the tools pod and return the output."""
25+
logger.debug(f"Running command in {self.pod_name}: {' '.join(cmd)}")
26+
resp = stream(
27+
self.core_v1.connect_get_namespaced_pod_exec,
28+
self.pod_name,
29+
self.namespace,
30+
command=cmd,
31+
stderr=True,
32+
stdin=False,
33+
stdout=True,
34+
tty=False,
35+
)
36+
logger.debug(f"Command output: {resp}")
37+
return resp
38+
39+
def copy_file_to_pod(self, src_path: str, dest_path: str):
40+
"""Copy a file from the local filesystem to the tools pod."""
41+
logger.debug(f"Copying {src_path} to {self.pod_name}:{dest_path}")
42+
43+
# Create a tar archive containing the file
44+
with tempfile.NamedTemporaryFile(suffix=".tar") as tar_file:
45+
with tarfile.open(tar_file.name, "w") as tar:
46+
tar.add(src_path, arcname=dest_path.split("/")[-1])
47+
48+
tar_file.seek(0)
49+
tar_data = tar_file.read()
50+
51+
# Extract the tar archive in the pod
52+
exec_command = ["tar", "xf", "-", "-C", "/".join(dest_path.split("/")[:-1]) or "/"]
53+
resp = stream(
54+
self.core_v1.connect_get_namespaced_pod_exec,
55+
self.pod_name,
56+
self.namespace,
57+
command=exec_command,
58+
stderr=True,
59+
stdin=True,
60+
stdout=True,
61+
tty=False,
62+
_preload_content=False,
63+
)
64+
65+
# Send the tar data
66+
resp.write_stdin(tar_data)
67+
resp.close()
68+
logger.debug(f"File copied to {self.pod_name}:{dest_path}")
69+
70+
def run_pod_and_wait(self):
71+
"""Create the tools pod and wait for it to be ready."""
72+
pod_body = client.V1Pod(
73+
api_version="v1",
74+
kind="Pod",
75+
metadata=client.V1ObjectMeta(name=self.pod_name, labels={"app": "mongodb-tools"}),
76+
spec=client.V1PodSpec(
77+
containers=[
78+
client.V1Container(
79+
name="mongodb-tools",
80+
image=TOOLS_POD_IMAGE,
81+
command=["/bin/bash", "-c"],
82+
args=["sleep infinity"],
83+
)
84+
],
85+
restart_policy="Never",
86+
),
87+
)
88+
89+
try:
90+
self.core_v1.create_namespaced_pod(namespace=self.namespace, body=pod_body)
91+
logger.info(f"Created {self.pod_name} in namespace {self.namespace}")
92+
except client.exceptions.ApiException as e:
93+
if e.status == 409:
94+
logger.info(f"Pod {self.pod_name} already exists")
95+
else:
96+
raise
97+
98+
pod = get_pod_when_ready(self.namespace, "app=mongodb-tools", default_retry=120)
99+
if pod is None:
100+
raise TimeoutError(f"Timed out waiting for {self.pod_name} to be ready")
101+
logger.info(f"{self.pod_name} is ready")
102+
103+
104+
def get_tools_pod(namespace: str) -> ToolsPod:
105+
"""Create and return a ready tools pod in the given namespace."""
106+
tools_pod = ToolsPod(namespace)
107+
tools_pod.run_pod_and_wait()
108+
return tools_pod

docker/mongodb-kubernetes-tests/tests/common/search/movies_search_helper.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
1-
import logging
2-
31
import pymongo.errors
42
from kubetester import kubetester
53
from tests import test_logger
4+
from tests.common.mongodb_tools_pod.mongodb_tools_pod import ToolsPod
65
from tests.common.search.search_tester import SearchTester
76

87
logger = test_logger.get_test_logger(__name__)
@@ -13,15 +12,19 @@ class SampleMoviesSearchHelper:
1312
db_name: str
1413
col_name: str
1514
archive_url: str
15+
tools_pod: ToolsPod
1616

17-
def __init__(self, search_tester: SearchTester):
17+
def __init__(self, search_tester: SearchTester, tools_pod: ToolsPod):
1818
self.search_tester = search_tester
19+
self.tools_pod = tools_pod
1920
self.db_name = "sample_mflix"
2021
self.col_name = "movies"
2122

2223
def restore_sample_database(self):
2324
self.search_tester.mongorestore_from_url(
24-
"https://atlas-education.s3.amazonaws.com/sample_mflix.archive", f"{self.db_name}.*"
25+
"https://atlas-education.s3.amazonaws.com/sample_mflix.archive",
26+
f"{self.db_name}.*",
27+
self.tools_pod,
2528
)
2629

2730
def create_search_index(self):

docker/mongodb-kubernetes-tests/tests/common/search/search_tester.py

Lines changed: 106 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,10 @@
1-
import os
2-
import tempfile
1+
from typing import Optional, Self
32

43
import kubetester
5-
import requests
6-
from kubetester.helm import process_run_and_check
74
from kubetester.mongotester import MongoTester
85
from pymongo.operations import SearchIndexModel
96
from tests import test_logger
7+
from tests.common.mongodb_tools_pod.mongodb_tools_pod import ToolsPod
108

119
logger = test_logger.get_test_logger(__name__)
1210

@@ -16,23 +14,100 @@ def __init__(
1614
self,
1715
connection_string: str,
1816
use_ssl: bool = False,
19-
ca_path: str | None = None,
17+
ca_path: Optional[str] = None,
2018
):
2119
super().__init__(connection_string, use_ssl, ca_path)
2220

23-
def mongorestore_from_url(self, archive_url: str, ns_include: str, mongodb_tools_dir: str = ""):
24-
logger.debug(f"running mongorestore from {archive_url}")
25-
with tempfile.NamedTemporaryFile(delete=False) as sample_file:
26-
resp = requests.get(archive_url)
27-
size = sample_file.write(resp.content)
28-
logger.debug(f"Downloaded sample file from {archive_url} to {sample_file.name} (size: {size})")
29-
mongorestore_path = os.path.join(mongodb_tools_dir, "mongorestore")
30-
mongorestore_cmd = f"{mongorestore_path} --archive={sample_file.name} --verbose=1 --drop --nsInclude {ns_include} --uri={self.cnx_string}"
31-
if self.default_opts.get("tls", False):
32-
mongorestore_cmd += " --ssl"
33-
if ca_path := self.default_opts.get("tlsCAFile"):
34-
mongorestore_cmd += " --sslCAFile=" + ca_path
35-
process_run_and_check(mongorestore_cmd.split(), capture_output=True)
21+
@classmethod
22+
def for_replicaset(
23+
cls,
24+
mdb,
25+
user_name: str,
26+
password: str,
27+
use_ssl: bool = False,
28+
ca_path: Optional[str] = None,
29+
) -> Self:
30+
"""Create SearchTester for a replica set MongoDB resource.
31+
32+
Args:
33+
mdb: MongoDB or MongoDBCommunity resource with name and namespace attributes
34+
user_name: Username for authentication
35+
password: Password for authentication
36+
use_ssl: Whether to use TLS/SSL connection
37+
ca_path: Path to CA certificate file (required if use_ssl=True)
38+
39+
Returns:
40+
SearchTester instance configured for the replica set
41+
"""
42+
conn_str = (
43+
f"mongodb://{user_name}:{password}@"
44+
f"{mdb.name}-0.{mdb.name}-svc.{mdb.namespace}.svc.cluster.local:27017/"
45+
f"?replicaSet={mdb.name}"
46+
)
47+
return cls(conn_str, use_ssl=use_ssl, ca_path=ca_path)
48+
49+
@classmethod
50+
def for_sharded(
51+
cls,
52+
mdb,
53+
user_name: str,
54+
password: str,
55+
use_ssl: bool = False,
56+
ca_path: Optional[str] = None,
57+
) -> Self:
58+
"""Create SearchTester for a sharded MongoDB resource (connects to mongos).
59+
60+
Args:
61+
mdb: MongoDB resource with name and namespace attributes (sharded cluster)
62+
user_name: Username for authentication
63+
password: Password for authentication
64+
use_ssl: Whether to use TLS/SSL connection
65+
ca_path: Path to CA certificate file (required if use_ssl=True)
66+
67+
Returns:
68+
SearchTester instance configured for the sharded cluster via mongos
69+
"""
70+
conn_str = (
71+
f"mongodb://{user_name}:{password}@"
72+
f"{mdb.name}-mongos-0.{mdb.name}-svc.{mdb.namespace}.svc.cluster.local:27017/"
73+
f"?authSource=admin"
74+
)
75+
return cls(conn_str, use_ssl=use_ssl, ca_path=ca_path)
76+
77+
def mongorestore_from_url(self, archive_url: str, ns_include: str, tools_pod: ToolsPod):
78+
"""Run mongorestore from a URL using the tools pod.
79+
80+
Args:
81+
archive_url: URL to download the archive from
82+
ns_include: Namespace include pattern for mongorestore
83+
tools_pod: ToolsPod instance to run the command in
84+
"""
85+
logger.debug(f"running mongorestore from {archive_url} via tools pod")
86+
archive_path = "/tmp/sample.archive"
87+
88+
# Download the archive directly in the pod using curl
89+
tools_pod.run_command(["curl", "-o", archive_path, "-L", archive_url])
90+
91+
# Build mongorestore command
92+
mongorestore_cmd = [
93+
"mongorestore",
94+
f"--archive={archive_path}",
95+
"--verbose=1",
96+
"--drop",
97+
"--nsInclude",
98+
ns_include,
99+
f"--uri={self.cnx_string}",
100+
]
101+
102+
if self.default_opts.get("tls", False):
103+
mongorestore_cmd.append("--ssl")
104+
if ca_path := self.default_opts.get("tlsCAFile"):
105+
# Copy CA cert to pod and use it
106+
pod_ca_path = "/tmp/ca.crt"
107+
tools_pod.copy_file_to_pod(ca_path, pod_ca_path)
108+
mongorestore_cmd.append(f"--sslCAFile={pod_ca_path}")
109+
110+
tools_pod.run_command(mongorestore_cmd)
36111

37112
def create_search_index(self, database_name: str, collection_name: str):
38113
database = self.client[database_name]
@@ -87,9 +162,19 @@ def search_indexes_ready(self, database_name: str, collection_name: str):
87162
return False
88163

89164
for idx in search_indexes:
90-
if idx.get("status") != "READY":
91-
logger.debug(f"{database_name}/{collection_name}: search index {idx} is not ready")
92-
return False
165+
status = idx.get("status")
166+
queryable = idx.get("queryable")
167+
if status == "READY" or queryable is True:
168+
continue
169+
if status is None and queryable is None and idx.get("latestDefinition") is not None:
170+
logger.debug(
171+
f"{database_name}/{collection_name}: search index {idx.get('name')} has no status but has latestDefinition, considering ready"
172+
)
173+
continue
174+
logger.debug(
175+
f"{database_name}/{collection_name}: search index {idx} is not ready (status={status}, queryable={queryable})"
176+
)
177+
return False
93178
return True
94179

95180
def get_search_indexes(self, database_name, collection_name):

0 commit comments

Comments
 (0)