-
Notifications
You must be signed in to change notification settings - Fork 23
CLOUDP-356376 Allow Search to work with x509 cluster auth #568
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
0f77ed9
046af5a
84950c1
612a4d2
6998d9f
ff0c564
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| --- | ||
| kind: feature | ||
| date: 2025-11-03 | ||
| --- | ||
|
|
||
| * **MongoDBSearch**: MongoDB deployments using X509 internal cluster authentication are now supported. Previously MongoDB Search required SCRAM authentication among members of a MongoDB replica set. Note: SCRAM client authentication is still required, this change merely relaxes the requirements on internal cluster authentication. | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,261 @@ | ||
| import yaml | ||
| from kubetester import create_or_update_secret, run_periodically, try_load | ||
| from kubetester.certs import ( | ||
| create_agent_tls_certs, | ||
| create_tls_certs, | ||
| create_x509_mongodb_tls_certs, | ||
| ) | ||
| from kubetester.kubetester import KubernetesTester | ||
| from kubetester.kubetester import fixture as yaml_fixture | ||
| from kubetester.mongodb import MongoDB | ||
| from kubetester.mongodb_search import MongoDBSearch | ||
| from kubetester.mongodb_user import MongoDBUser | ||
| from kubetester.omtester import skip_if_cloud_manager | ||
| from kubetester.phase import Phase | ||
| from pytest import fixture, mark | ||
| from tests import test_logger | ||
| from tests.common.search import movies_search_helper | ||
| from tests.common.search.search_tester import SearchTester | ||
| from tests.conftest import get_default_operator, get_issuer_ca_filepath | ||
| from tests.search.om_deployment import get_ops_manager | ||
|
|
||
| logger = test_logger.get_test_logger(__name__) | ||
|
|
||
| ADMIN_USER_NAME = "mdb-admin-user" | ||
| ADMIN_USER_PASSWORD = f"{ADMIN_USER_NAME}-password" | ||
|
|
||
| MONGOT_USER_NAME = "search-sync-source" | ||
| MONGOT_USER_PASSWORD = f"{MONGOT_USER_NAME}-password" | ||
|
|
||
| USER_NAME = "mdb-user" | ||
| USER_PASSWORD = f"{USER_NAME}-password" | ||
|
|
||
| MDB_RESOURCE_NAME = "mdb-ent-tls" | ||
|
|
||
| # MongoDBSearch TLS configuration | ||
| MDBS_TLS_SECRET_NAME = "mdbs-tls-secret" | ||
|
|
||
|
|
||
| @fixture(scope="function") | ||
| def mdb(namespace: str, issuer_ca_configmap: str) -> MongoDB: | ||
| resource = MongoDB.from_yaml( | ||
| yaml_fixture("enterprise-replicaset-sample-mflix.yaml"), | ||
| name=MDB_RESOURCE_NAME, | ||
| namespace=namespace, | ||
| ) | ||
|
|
||
| if try_load(resource): | ||
| return resource | ||
|
|
||
| resource.configure(om=get_ops_manager(namespace), project_name=MDB_RESOURCE_NAME) | ||
| resource.configure_custom_tls(issuer_ca_configmap, "certs") | ||
| resource["spec"]["security"]["authentication"] = { | ||
| "enabled": True, | ||
| "modes": ["X509", "SCRAM"], | ||
| "agents": {"mode": "X509"}, | ||
| "internalCluster": "X509", | ||
| } | ||
|
Comment on lines
+52
to
+57
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is the only addition to the code already in the |
||
|
|
||
| return resource | ||
|
|
||
|
|
||
| @fixture(scope="function") | ||
| def mdbs(namespace: str) -> MongoDBSearch: | ||
| resource = MongoDBSearch.from_yaml(yaml_fixture("search-minimal.yaml"), namespace=namespace, name=MDB_RESOURCE_NAME) | ||
|
|
||
| if try_load(resource): | ||
| return resource | ||
|
|
||
| # Add TLS configuration to MongoDBSearch | ||
| if "spec" not in resource: | ||
| resource["spec"] = {} | ||
|
|
||
| resource["spec"]["security"] = {"tls": {"certificateKeySecretRef": {"name": MDBS_TLS_SECRET_NAME}}} | ||
|
|
||
| return resource | ||
|
|
||
|
|
||
| @fixture(scope="function") | ||
| def admin_user(namespace: str) -> MongoDBUser: | ||
| resource = MongoDBUser.from_yaml( | ||
| yaml_fixture("mongodbuser-mdb-admin.yaml"), namespace=namespace, name=ADMIN_USER_NAME | ||
| ) | ||
|
|
||
| if try_load(resource): | ||
| return resource | ||
|
|
||
| resource["spec"]["mongodbResourceRef"]["name"] = MDB_RESOURCE_NAME | ||
| resource["spec"]["username"] = resource.name | ||
| resource["spec"]["passwordSecretKeyRef"]["name"] = f"{resource.name}-password" | ||
|
|
||
| return resource | ||
|
|
||
|
|
||
| @fixture(scope="function") | ||
| def user(namespace: str) -> MongoDBUser: | ||
| resource = MongoDBUser.from_yaml(yaml_fixture("mongodbuser-mdb-user.yaml"), namespace=namespace, name=USER_NAME) | ||
|
|
||
| if try_load(resource): | ||
| return resource | ||
|
|
||
| resource["spec"]["mongodbResourceRef"]["name"] = MDB_RESOURCE_NAME | ||
| resource["spec"]["username"] = resource.name | ||
| resource["spec"]["passwordSecretKeyRef"]["name"] = f"{resource.name}-password" | ||
|
|
||
| return resource | ||
|
|
||
|
|
||
| @fixture(scope="function") | ||
| def mongot_user(namespace: str, mdbs: MongoDBSearch) -> MongoDBUser: | ||
| resource = MongoDBUser.from_yaml( | ||
| yaml_fixture("mongodbuser-search-sync-source-user.yaml"), | ||
| namespace=namespace, | ||
| name=f"{mdbs.name}-{MONGOT_USER_NAME}", | ||
| ) | ||
|
|
||
| if try_load(resource): | ||
| return resource | ||
|
|
||
| resource["spec"]["mongodbResourceRef"]["name"] = MDB_RESOURCE_NAME | ||
| resource["spec"]["username"] = MONGOT_USER_NAME | ||
| resource["spec"]["passwordSecretKeyRef"]["name"] = f"{resource.name}-password" | ||
|
|
||
| return resource | ||
|
|
||
|
|
||
| @mark.e2e_search_enterprise_x509_cluster_auth | ||
| def test_install_operator(namespace: str, operator_installation_config: dict[str, str]): | ||
| operator = get_default_operator(namespace, operator_installation_config=operator_installation_config) | ||
| operator.assert_is_running() | ||
|
|
||
|
|
||
| @mark.e2e_search_enterprise_x509_cluster_auth | ||
| @skip_if_cloud_manager | ||
| def test_create_ops_manager(namespace: str): | ||
| ops_manager = get_ops_manager(namespace) | ||
| ops_manager.update() | ||
| ops_manager.om_status().assert_reaches_phase(Phase.Running, timeout=1200) | ||
| ops_manager.appdb_status().assert_reaches_phase(Phase.Running, timeout=600) | ||
|
|
||
|
|
||
| @mark.e2e_search_enterprise_x509_cluster_auth | ||
| def test_install_tls_secrets_and_configmaps(namespace: str, mdb: MongoDB, mdbs: MongoDBSearch, issuer: str): | ||
| create_agent_tls_certs(issuer, namespace, mdb.name, "certs") | ||
| create_x509_mongodb_tls_certs(issuer, namespace, mdb.name, f"certs-{mdb.name}-clusterfile") | ||
| create_x509_mongodb_tls_certs(issuer, namespace, mdb.name, f"certs-{mdb.name}-cert", mdb.get_members()) | ||
|
Comment on lines
+143
to
+145
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The other change, necessary to populate the secrets expected by the MongoDB CR change above. |
||
|
|
||
| search_service_name = f"{mdbs.name}-search-svc" | ||
| create_tls_certs( | ||
| issuer, | ||
| namespace, | ||
| f"{mdbs.name}-search", | ||
| replicas=1, | ||
| service_name=search_service_name, | ||
| additional_domains=[f"{search_service_name}.{namespace}.svc.cluster.local"], | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is this service FQDN actually validated by mongod when connecting to mongot? It should, right? Also when we switch to L7 our proxy component should probably present the same cert, or at least cert issued for the same service FQDN?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm actually not a 100% if mongod performs hostname validation. I'll comment this out and see what happens. re. L7 proxies - the proxy will terminate the TLS connection from mongod, and establish its own TLS connection to the mongots it represents. It can use the same TLS certificate as mongot, or its own (though it too will have to be signed by the same CA). We'll sort this out separately with load balancing, however, and adjust as necessary. This is just a copy of the existing TLS test we have.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @lsierant it does seem that when mongod connects to mongot the hostname is validated against mongot's TLS certificate. Commenting out the |
||
| secret_name=MDBS_TLS_SECRET_NAME, | ||
| ) | ||
|
|
||
|
|
||
| @mark.e2e_search_enterprise_x509_cluster_auth | ||
| def test_create_database_resource(mdb: MongoDB): | ||
| mdb.update() | ||
| mdb.assert_reaches_phase(Phase.Running, timeout=300) | ||
|
|
||
|
|
||
| @mark.e2e_search_enterprise_x509_cluster_auth | ||
| def test_create_users( | ||
| namespace: str, admin_user: MongoDBUser, user: MongoDBUser, mongot_user: MongoDBUser, mdb: MongoDB | ||
| ): | ||
| create_or_update_secret( | ||
| namespace, name=admin_user["spec"]["passwordSecretKeyRef"]["name"], data={"password": ADMIN_USER_PASSWORD} | ||
| ) | ||
| admin_user.update() | ||
| admin_user.assert_reaches_phase(Phase.Updated, timeout=300) | ||
|
|
||
| create_or_update_secret( | ||
| namespace, name=user["spec"]["passwordSecretKeyRef"]["name"], data={"password": USER_PASSWORD} | ||
| ) | ||
| user.update() | ||
| user.assert_reaches_phase(Phase.Updated, timeout=300) | ||
|
|
||
| create_or_update_secret( | ||
| namespace, name=mongot_user["spec"]["passwordSecretKeyRef"]["name"], data={"password": MONGOT_USER_PASSWORD} | ||
| ) | ||
| mongot_user.update() | ||
| mongot_user.assert_reaches_phase(Phase.Updated, timeout=300) | ||
|
|
||
|
|
||
| @mark.e2e_search_enterprise_x509_cluster_auth | ||
| def test_create_search_resource(mdbs: MongoDBSearch): | ||
| mdbs.update() | ||
| mdbs.assert_reaches_phase(Phase.Running, timeout=300) | ||
|
|
||
|
|
||
| # After picking up MongoDBSearch CR, MongoDB reconciler will add mongod parameters to each process. | ||
| # Due to how MongoDB reconciler works (blocking on waiting for agents and not changing the status to pending) | ||
| # the phase won't be updated to Pending and we need to wait by checking agents' status directly in OM. | ||
| @mark.e2e_search_enterprise_x509_cluster_auth | ||
| def test_wait_for_agents_ready(mdb: MongoDB): | ||
| mdb.get_om_tester().wait_agents_ready() | ||
| mdb.assert_reaches_phase(Phase.Running, timeout=300) | ||
|
|
||
|
|
||
| @mark.e2e_search_enterprise_x509_cluster_auth | ||
| def test_wait_for_mongod_parameters(mdb: MongoDB): | ||
| # After search CR is deployed, MongoDB controller will pick it up | ||
| # and start adding search-related parameters to the automation config. | ||
| def check_mongod_parameters(): | ||
| parameters_are_set = True | ||
| pod_parameters = [] | ||
| for idx in range(mdb.get_members()): | ||
| mongod_config = yaml.safe_load( | ||
| KubernetesTester.run_command_in_pod_container( | ||
| f"{mdb.name}-{idx}", mdb.namespace, ["cat", "/data/automation-mongod.conf"] | ||
| ) | ||
| ) | ||
| set_parameter = mongod_config.get("setParameter", {}) | ||
| parameters_are_set = parameters_are_set and ( | ||
| "mongotHost" in set_parameter and "searchIndexManagementHostAndPort" in set_parameter | ||
| ) | ||
| pod_parameters.append(f"pod {idx} setParameter: {set_parameter}") | ||
|
|
||
| return parameters_are_set, f'Not all pods have mongot parameters set:\n{"\n".join(pod_parameters)}' | ||
|
|
||
| run_periodically(check_mongod_parameters, timeout=600) | ||
|
|
||
|
|
||
| @mark.e2e_search_enterprise_x509_cluster_auth | ||
| def test_search_restore_sample_database(mdb: MongoDB): | ||
| get_admin_sample_movies_helper(mdb).restore_sample_database() | ||
|
|
||
|
|
||
| @mark.e2e_search_enterprise_x509_cluster_auth | ||
| def test_search_create_search_index(mdb: MongoDB): | ||
| get_user_sample_movies_helper(mdb).create_search_index() | ||
|
|
||
|
|
||
| @mark.e2e_search_enterprise_x509_cluster_auth | ||
| def test_search_assert_search_query(mdb: MongoDB): | ||
| get_user_sample_movies_helper(mdb).assert_search_query(retry_timeout=60) | ||
|
|
||
|
|
||
| def get_connection_string(mdb: MongoDB, user_name: str, user_password: str) -> str: | ||
| return f"mongodb://{user_name}:{user_password}@{mdb.name}-0.{mdb.name}-svc.{mdb.namespace}.svc.cluster.local:27017/?replicaSet={mdb.name}" | ||
|
|
||
|
|
||
| def get_admin_sample_movies_helper(mdb): | ||
| return movies_search_helper.SampleMoviesSearchHelper( | ||
| SearchTester( | ||
| get_connection_string(mdb, ADMIN_USER_NAME, ADMIN_USER_PASSWORD), | ||
| use_ssl=True, | ||
| ca_path=get_issuer_ca_filepath(), | ||
| ) | ||
| ) | ||
|
|
||
|
|
||
| def get_user_sample_movies_helper(mdb): | ||
| return movies_search_helper.SampleMoviesSearchHelper( | ||
| SearchTester( | ||
| get_connection_string(mdb, USER_NAME, USER_PASSWORD), use_ssl=True, ca_path=get_issuer_ca_filepath() | ||
| ) | ||
| ) | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM!