From a72c5725be163ef70f7936fab4af355915b487f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Sierant?= Date: Fri, 4 Jul 2025 17:26:33 +0200 Subject: [PATCH 01/13] Empty commit From 167e26b3279b05f742e3a5dbed7ec02eebcbe55d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Sierant?= Date: Thu, 17 Jul 2025 16:07:26 +0200 Subject: [PATCH 02/13] Handle new mongot config schema (#230) # Summary Implements minimum necessary changes to run against newest mongot container built from master: * Workaround for mongot's requirement of password file having owner-only permissions * New config schema structure * Switched default mongot image to ECR * Simulated searchCoordinated role --- api/v1/search/mongodbsearch_types.go | 30 +++++++- .../crd/bases/mongodb.com_mongodbsearch.yaml | 15 ++++ config/manager/manager.yaml | 4 +- .../operator/mongodbsearch_controller_test.go | 48 ++++++++---- .../mongodbsearch_reconcile_helper.go | 47 +++++++++--- .../search_controller/search_construction.go | 22 +++++- .../kubetester/helm.py | 3 + .../common/search/movies_search_helper.py | 43 ++++++++--- .../tests/common/search/search_tester.py | 4 + .../community-replicaset-sample-mflix.yaml | 28 +++++-- .../fixtures/search-with-user-password.yaml | 5 ++ .../tests/search/search_community_basic.py | 45 +++++++---- .../crds/mongodb.com_mongodbsearch.yaml | 15 ++++ helm_chart/values.yaml | 10 ++- .../controllers/replica_set_controller.go | 66 +++++++++++++++- .../pkg/mongot/mongot_config.go | 75 +++++++++++++------ public/crds.yaml | 15 ++++ public/mongodb-kubernetes-multi-cluster.yaml | 2 +- public/mongodb-kubernetes-openshift.yaml | 4 +- public/mongodb-kubernetes.yaml | 2 +- scripts/dev/contexts/e2e_mdb_community | 8 ++ scripts/dev/contexts/root-context | 1 + 22 files changed, 394 insertions(+), 98 deletions(-) create mode 100644 docker/mongodb-kubernetes-tests/tests/search/fixtures/search-with-user-password.yaml diff --git a/api/v1/search/mongodbsearch_types.go b/api/v1/search/mongodbsearch_types.go index 43e9be1be..cdb391781 100644 --- a/api/v1/search/mongodbsearch_types.go +++ b/api/v1/search/mongodbsearch_types.go @@ -1,6 +1,8 @@ package search import ( + "fmt" + "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/types" @@ -14,8 +16,9 @@ import ( ) const ( - MongotDefaultPort = 27027 - MongotDefaultMetricsPort = 9946 + MongotDefaultPort = 27027 + MongotDefaultMetricsPort = 9946 + MongotDefaultSyncSourceUsername = "mongot-user" ) func init() { @@ -38,6 +41,10 @@ type MongoDBSearchSpec struct { type MongoDBSource struct { // +optional MongoDBResourceRef *userv1.MongoDBResourceRef `json:"mongodbResourceRef,omitempty"` + // +optional + PasswordSecretRef *userv1.SecretKeyRef `json:"passwordSecretRef,omitempty"` + // +optional + Username *string `json:"username,omitempty"` } type MongoDBSearchStatus struct { @@ -105,6 +112,25 @@ func (s *MongoDBSearch) MongotConfigConfigMapNamespacedName() types.NamespacedNa return types.NamespacedName{Name: s.Name + "-search-config", Namespace: s.Namespace} } +func (s *MongoDBSearch) SourceUserPasswordSecretRef() *userv1.SecretKeyRef { + if s.Spec.Source != nil && s.Spec.Source.PasswordSecretRef != nil { + return s.Spec.Source.PasswordSecretRef + } + + return &userv1.SecretKeyRef{ + Name: fmt.Sprintf("%s-%s-password", s.Name, MongotDefaultSyncSourceUsername), + Key: "password", + } +} + +func (s *MongoDBSearch) SourceUsername() string { + if s.Spec.Source != nil && s.Spec.Source.Username != nil { + return *s.Spec.Source.Username + } + + return MongotDefaultSyncSourceUsername +} + func (s *MongoDBSearch) StatefulSetNamespacedName() types.NamespacedName { return types.NamespacedName{Name: s.Name + "-search", Namespace: s.Namespace} } diff --git a/config/crd/bases/mongodb.com_mongodbsearch.yaml b/config/crd/bases/mongodb.com_mongodbsearch.yaml index 272e7bc59..eeba52768 100644 --- a/config/crd/bases/mongodb.com_mongodbsearch.yaml +++ b/config/crd/bases/mongodb.com_mongodbsearch.yaml @@ -160,6 +160,21 @@ spec: required: - name type: object + passwordSecretRef: + description: |- + SecretKeyRef is a reference to a value in a given secret in the same + namespace. Based on: + https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.15/#secretkeyselector-v1-core + properties: + key: + type: string + name: + type: string + required: + - name + type: object + username: + type: string type: object statefulSet: description: |- diff --git a/config/manager/manager.yaml b/config/manager/manager.yaml index 3e4f72934..ce5ce623d 100644 --- a/config/manager/manager.yaml +++ b/config/manager/manager.yaml @@ -392,9 +392,9 @@ spec: - name: RELATED_IMAGE_MONGODB_IMAGE_8_0_0_ubi9 value: "quay.io/mongodb/mongodb-enterprise-server:8.0.0-ubi9" - name: RELATED_IMAGE_MDB_SEARCH_IMAGE_1_47_0 - value: "quay.io/mongodb/mongodb-search-community:1.47.0" + value: "268558157000.dkr.ecr.us-east-1.amazonaws.com/dev/mongodb-search-community:1.47.0" - name: MDB_SEARCH_COMMUNITY_REPO_URL - value: "quay.io/mongodb" + value: "268558157000.dkr.ecr.us-east-1.amazonaws.com/dev" - name: MDB_SEARCH_COMMUNITY_NAME value: "mongodb-search-community" - name: MDB_SEARCH_COMMUNITY_VERSION diff --git a/controllers/operator/mongodbsearch_controller_test.go b/controllers/operator/mongodbsearch_controller_test.go index 3c4cfb556..974ec3935 100644 --- a/controllers/operator/mongodbsearch_controller_test.go +++ b/controllers/operator/mongodbsearch_controller_test.go @@ -9,6 +9,7 @@ import ( "github.com/stretchr/testify/assert" "k8s.io/apimachinery/pkg/types" "k8s.io/client-go/util/workqueue" + "k8s.io/utils/ptr" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/controller/controllertest" "sigs.k8s.io/controller-runtime/pkg/event" @@ -73,21 +74,42 @@ func newSearchReconciler( } func buildExpectedMongotConfig(search *searchv1.MongoDBSearch, mdbc *mdbcv1.MongoDBCommunity) mongot.Config { - return mongot.Config{CommunityPrivatePreview: mongot.CommunityPrivatePreview{ - MongodHostAndPort: fmt.Sprintf( - "%s.%s.svc.cluster.local:%d", - mdbc.ServiceName(), mdbc.Namespace, - mdbc.GetMongodConfiguration().GetDBPort(), - ), - QueryServerAddress: fmt.Sprintf("localhost:%d", search.GetMongotPort()), - KeyFilePath: "/mongot/keyfile/keyfile", - DataPath: "/mongot/data/config.yml", - Metrics: mongot.Metrics{ + return mongot.Config{ + SyncSource: mongot.ConfigSyncSource{ + ReplicaSet: mongot.ConfigReplicaSet{ + HostAndPort: fmt.Sprintf("%s.%s.svc.cluster.local:%d", mdbc.Name+"-svc", search.Namespace, 27017), + Username: "mongot-user", + PasswordFile: "/tmp/sourceUserPassword", + TLS: ptr.To(false), + ReadPreference: ptr.To("secondaryPreferred"), + ReplicaSetName: "mdb", + }, + }, + Storage: mongot.ConfigStorage{ + DataPath: "/mongot/data/config.yml", + }, + Server: mongot.ConfigServer{ + Wireproto: &mongot.ConfigWireproto{ + Address: "0.0.0.0:27027", + Authentication: &mongot.ConfigAuthentication{ + Mode: "keyfile", + KeyFile: "/tmp/keyfile", + }, + TLS: mongot.ConfigTLS{Mode: "disabled"}, + }, + }, + Metrics: mongot.ConfigMetrics{ Enabled: true, - Address: fmt.Sprintf("localhost:%d", search.GetMongotMetricsPort()), + Address: fmt.Sprintf("localhost:%d", searchv1.MongotDefaultMetricsPort), + }, + HealthCheck: mongot.ConfigHealthCheck{ + Address: "0.0.0.0:8080", }, - Logging: mongot.Logging{Verbosity: "DEBUG"}, - }} + Logging: mongot.ConfigLogging{ + Verbosity: "TRACE", + LogPath: nil, + }, + } } func TestMongoDBSearchReconcile_NotFound(t *testing.T) { diff --git a/controllers/search_controller/mongodbsearch_reconcile_helper.go b/controllers/search_controller/mongodbsearch_reconcile_helper.go index 59523ce8e..65e49f4f9 100644 --- a/controllers/search_controller/mongodbsearch_reconcile_helper.go +++ b/controllers/search_controller/mongodbsearch_reconcile_helper.go @@ -13,6 +13,7 @@ import ( "golang.org/x/xerrors" "k8s.io/apimachinery/pkg/fields" "k8s.io/apimachinery/pkg/util/intstr" + "k8s.io/utils/ptr" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" @@ -207,26 +208,50 @@ func buildSearchHeadlessService(search *searchv1.MongoDBSearch) corev1.Service { } func createMongotConfig(search *searchv1.MongoDBSearch, db SearchSourceDBResource) mongot.Config { - return mongot.Config{CommunityPrivatePreview: mongot.CommunityPrivatePreview{ - MongodHostAndPort: fmt.Sprintf("%s.%s.svc.cluster.local:%d", db.DatabaseServiceName(), db.GetNamespace(), db.DatabasePort()), - QueryServerAddress: fmt.Sprintf("localhost:%d", search.GetMongotPort()), - KeyFilePath: "/mongot/keyfile/keyfile", - DataPath: "/mongot/data/config.yml", - Metrics: mongot.Metrics{ + return mongot.Config{ + SyncSource: mongot.ConfigSyncSource{ + ReplicaSet: mongot.ConfigReplicaSet{ + HostAndPort: fmt.Sprintf("%s.%s.svc.cluster.local:%d", db.DatabaseServiceName(), db.GetNamespace(), db.DatabasePort()), + Username: search.SourceUsername(), + PasswordFile: "/tmp/sourceUserPassword", + TLS: ptr.To(false), + ReadPreference: ptr.To("secondaryPreferred"), + ReplicaSetName: db.Name(), + }, + }, + Storage: mongot.ConfigStorage{ + DataPath: "/mongot/data/config.yml", + }, + Server: mongot.ConfigServer{ + Wireproto: &mongot.ConfigWireproto{ + Address: "0.0.0.0:27027", + Authentication: &mongot.ConfigAuthentication{ + Mode: "keyfile", + KeyFile: "/tmp/keyfile", + }, + TLS: mongot.ConfigTLS{Mode: "disabled"}, + }, + }, + Metrics: mongot.ConfigMetrics{ Enabled: true, Address: fmt.Sprintf("localhost:%d", search.GetMongotMetricsPort()), }, - Logging: mongot.Logging{ - Verbosity: "DEBUG", + HealthCheck: mongot.ConfigHealthCheck{ + Address: "0.0.0.0:8080", }, - }} + Logging: mongot.ConfigLogging{ + Verbosity: "TRACE", + LogPath: nil, + }, + } } func GetMongodConfigParameters(search *searchv1.MongoDBSearch) map[string]interface{} { return map[string]interface{}{ "setParameter": map[string]interface{}{ - "mongotHost": mongotHostAndPort(search), - "searchIndexManagementHostAndPort": mongotHostAndPort(search), + "mongotHost": mongotHostAndPort(search), + "searchIndexManagementHostAndPort": mongotHostAndPort(search), + "skipAuthenticationToSearchIndexManagementServer": false, }, } } diff --git a/controllers/search_controller/search_construction.go b/controllers/search_controller/search_construction.go index ea169339d..2de1364e1 100644 --- a/controllers/search_controller/search_construction.go +++ b/controllers/search_controller/search_construction.go @@ -95,14 +95,18 @@ func CreateSearchStatefulSetFunc(mdbSearch *searchv1.MongoDBSearch, sourceDBReso dataVolumeName := "data" keyfileVolumeName := "keyfile" + sourceUserPasswordVolumeName := "password" mongotConfigVolumeName := "config" pvcVolumeMount := statefulset.CreateVolumeMount(dataVolumeName, "/mongot/data", statefulset.WithSubPath("data")) - keyfileVolume := statefulset.CreateVolumeFromSecret("keyfile", sourceDBResource.KeyfileSecretName()) + keyfileVolume := statefulset.CreateVolumeFromSecret(keyfileVolumeName, sourceDBResource.KeyfileSecretName()) keyfileVolumeMount := statefulset.CreateVolumeMount(keyfileVolumeName, "/mongot/keyfile", statefulset.WithReadOnly(true)) - mongotConfigVolume := statefulset.CreateVolumeFromConfigMap("config", mdbSearch.MongotConfigConfigMapNamespacedName().Name) + sourceUserPasswordVolume := statefulset.CreateVolumeFromSecret(sourceUserPasswordVolumeName, mdbSearch.SourceUserPasswordSecretRef().Name) + sourceUserPasswordVolumeMount := statefulset.CreateVolumeMount(sourceUserPasswordVolumeName, "/mongot/sourceUserPassword", statefulset.WithReadOnly(true)) + + mongotConfigVolume := statefulset.CreateVolumeFromConfigMap(mongotConfigVolumeName, mdbSearch.MongotConfigConfigMapNamespacedName().Name) mongotConfigVolumeMount := statefulset.CreateVolumeMount(mongotConfigVolumeName, "/mongot/config", statefulset.WithReadOnly(true)) var persistenceConfig *common.PersistenceConfig @@ -120,12 +124,14 @@ func CreateSearchStatefulSetFunc(mdbSearch *searchv1.MongoDBSearch, sourceDBReso keyfileVolumeMount, tmpVolumeMount, mongotConfigVolumeMount, + sourceUserPasswordVolumeMount, } volumes := []corev1.Volume{ tmpVolume, keyfileVolume, mongotConfigVolume, + sourceUserPasswordVolume, } stsModifications := []statefulset.Modification{ @@ -180,7 +186,17 @@ func mongodbSearchContainer(mdbSearch *searchv1.MongoDBSearch, volumeMounts []co container.WithCommand([]string{"sh"}), container.WithArgs([]string{ "-c", - "/mongot-community/mongot --config /mongot/config/config.yml", + ` +cp /mongot/keyfile/keyfile /tmp/keyfile +chown 2000:2000 /tmp/keyfile +chmod 0600 /tmp/keyfile + +cp /mongot/sourceUserPassword/password /tmp/sourceUserPassword +chown 2000:2000 /tmp/sourceUserPassword +chmod 0600 /tmp/sourceUserPassword + +/mongot-community/mongot --config /mongot/config/config.yml +`, }), containerSecurityContext, ) diff --git a/docker/mongodb-kubernetes-tests/kubetester/helm.py b/docker/mongodb-kubernetes-tests/kubetester/helm.py index da6887b81..9e0cca8fe 100644 --- a/docker/mongodb-kubernetes-tests/kubetester/helm.py +++ b/docker/mongodb-kubernetes-tests/kubetester/helm.py @@ -120,6 +120,9 @@ def helm_repo_add(repo_name: str, url: str): def process_run_and_check(args, **kwargs): + if "check" not in kwargs: + kwargs["check"] = True + try: logger.debug(f"subprocess.run: {args}") completed_process = subprocess.run(args, **kwargs) diff --git a/docker/mongodb-kubernetes-tests/tests/common/search/movies_search_helper.py b/docker/mongodb-kubernetes-tests/tests/common/search/movies_search_helper.py index e5629bda0..807d97493 100644 --- a/docker/mongodb-kubernetes-tests/tests/common/search/movies_search_helper.py +++ b/docker/mongodb-kubernetes-tests/tests/common/search/movies_search_helper.py @@ -1,3 +1,8 @@ +import logging + +import pymongo.errors + +from kubetester import kubetester from tests import test_logger from tests.common.search.search_tester import SearchTester @@ -26,9 +31,33 @@ def create_search_index(self): def wait_for_search_indexes(self): self.search_tester.wait_for_search_indexes_ready(self.db_name, self.col_name) - def assert_search_query(self): - # sample taken from: https://www.mongodb.com/docs/atlas/atlas-search/tutorial/run-query/#run-a-complex-query-on-the-movies-collection-7 - result = self.search_tester.client[self.db_name][self.col_name].aggregate( + def assert_search_query(self, retry_timeout: int = 1): + def wait_for_search_results(): + # sample taken from: https://www.mongodb.com/docs/atlas/atlas-search/tutorial/run-query/#run-a-complex-query-on-the-movies-collection-7 + count = 0 + status_msg = "" + try: + result = self.execute_example_search_query() + status_msg = f"{self.db_name}/{self.col_name}: search query results:\n" + for r in result: + status_msg += f"{r}\n" + count += 1 + status_msg += f"Count: {count}" + logger.debug(status_msg) + except pymongo.errors.PyMongoError as e: + logger.debug(f"error: {e}") + + return count == 4, status_msg + + kubetester.run_periodically( + fn=wait_for_search_results, + timeout=retry_timeout, + sleep_time=1, + msg="Search query to return correct data", + ) + + def execute_example_search_query(self): + return self.search_tester.client[self.db_name][self.col_name].aggregate( [ { "$search": { @@ -47,11 +76,3 @@ def assert_search_query(self): {"$project": {"title": 1, "plot": 1, "genres": 1, "_id": 0}}, ] ) - - logger.debug(f"{self.db_name}/{self.col_name}: search query results:") - count = 0 - for r in result: - logger.debug(r) - count += 1 - - assert count == 4 diff --git a/docker/mongodb-kubernetes-tests/tests/common/search/search_tester.py b/docker/mongodb-kubernetes-tests/tests/common/search/search_tester.py index bbca34b3b..7c5b78eac 100644 --- a/docker/mongodb-kubernetes-tests/tests/common/search/search_tester.py +++ b/docker/mongodb-kubernetes-tests/tests/common/search/search_tester.py @@ -49,6 +49,10 @@ def wait_for_search_indexes_ready(self, database_name: str, collection_name: str def search_indexes_ready(self, database_name: str, collection_name: str): search_indexes = self.get_search_indexes(database_name, collection_name) + if len(search_indexes) == 0: + logger.error(f"There are no search indexes available in {database_name}.{collection_name}") + return False + for idx in search_indexes: if idx.get("status") != "READY": logger.debug(f"{database_name}/{collection_name}: search index {idx} is not ready") diff --git a/docker/mongodb-kubernetes-tests/tests/search/fixtures/community-replicaset-sample-mflix.yaml b/docker/mongodb-kubernetes-tests/tests/search/fixtures/community-replicaset-sample-mflix.yaml index 4ba3d40b6..4042b7d65 100644 --- a/docker/mongodb-kubernetes-tests/tests/search/fixtures/community-replicaset-sample-mflix.yaml +++ b/docker/mongodb-kubernetes-tests/tests/search/fixtures/community-replicaset-sample-mflix.yaml @@ -9,22 +9,36 @@ spec: version: "8.0.5" security: authentication: - modes: ["SCRAM"] + modes: ["SCRAM-SHA-1"] agent: logLevel: DEBUG users: - - name: my-user + - name: mdb-admin db: admin passwordSecretRef: # a reference to the secret that will be used to generate the user's password - name: my-user-password + name: mdb-admin-user-password roles: - - name: clusterAdmin - db: admin - - name: userAdminAnyDatabase + - name: root db: admin + scramCredentialsSecretName: mdb-admin-user-scram + - name: mdb-user + db: admin + passwordSecretRef: # a reference to the secret that will be used to generate the user's password + name: mdb-user-password + roles: + - name: restore + db: sample_mflix - name: readWrite db: sample_mflix - scramCredentialsSecretName: my-scram + scramCredentialsSecretName: mdb-user-scram + - name: mongot-user + db: admin + passwordSecretRef: # a reference to the secret that will be used to generate the user's password + name: mdbc-rs-mongot-user-password + roles: + - name: searchCoordinator + db: admin + scramCredentialsSecretName: mongot-user-scram statefulSet: spec: template: diff --git a/docker/mongodb-kubernetes-tests/tests/search/fixtures/search-with-user-password.yaml b/docker/mongodb-kubernetes-tests/tests/search/fixtures/search-with-user-password.yaml new file mode 100644 index 000000000..33648e04a --- /dev/null +++ b/docker/mongodb-kubernetes-tests/tests/search/fixtures/search-with-user-password.yaml @@ -0,0 +1,5 @@ +apiVersion: mongodb.com/v1 +kind: MongoDBSearch +metadata: + name: mdbc-rs +spec: {} diff --git a/docker/mongodb-kubernetes-tests/tests/search/search_community_basic.py b/docker/mongodb-kubernetes-tests/tests/search/search_community_basic.py index 69cbca895..18e2ec025 100644 --- a/docker/mongodb-kubernetes-tests/tests/search/search_community_basic.py +++ b/docker/mongodb-kubernetes-tests/tests/search/search_community_basic.py @@ -12,8 +12,15 @@ logger = test_logger.get_test_logger(__name__) -USER_PASSWORD = "Passw0rd." -USER_NAME = "my-user" +ADMIN_USER_NAME = "mdb-admin-user" +ADMIN_USER_PASSWORD = "mdb-admin-user-pass" + +MONGOT_USER_NAME = "mongot-user" +MONGOT_USER_PASSWORD = "mongot-user-password" + +USER_NAME = "mdb-user" +USER_PASSWORD = "mdb-user-pass" + MDBC_RESOURCE_NAME = "mdbc-rs" @@ -27,8 +34,8 @@ def mdbc(namespace: str, custom_mdb_version: str) -> MongoDBCommunity: resource["spec"]["version"] = custom_mdb_version - if try_load(resource): - return resource + # if try_load(resource): + # return resource return resource @@ -40,8 +47,8 @@ def mdbs(namespace: str) -> MongoDBSearch: namespace=namespace, ) - if try_load(resource): - return resource + # if try_load(resource): + # return resource return resource @@ -53,8 +60,14 @@ def test_install_operator(namespace: str, operator_installation_config: dict[str @mark.e2e_search_community_basic -def test_install_secret(namespace: str): - create_or_update_secret(namespace=namespace, name="my-user-password", data={"password": USER_PASSWORD}) +def test_install_secrets(namespace: str, mdbs: MongoDBSearch): + create_or_update_secret(namespace=namespace, name=f"{USER_NAME}-password", data={"password": USER_PASSWORD}) + create_or_update_secret( + namespace=namespace, name=f"{ADMIN_USER_NAME}-password", data={"password": ADMIN_USER_PASSWORD} + ) + create_or_update_secret( + namespace=namespace, name=f"{mdbs.name}-{MONGOT_USER_NAME}-password", data={"password": MONGOT_USER_PASSWORD} + ) @mark.e2e_search_community_basic @@ -76,7 +89,9 @@ def test_wait_for_community_resource_ready(mdbc: MongoDBCommunity): @fixture(scope="function") def sample_movies_helper(mdbc: MongoDBCommunity) -> SampleMoviesSearchHelper: - return movies_search_helper.SampleMoviesSearchHelper(SearchTester(get_connection_string(mdbc))) + return movies_search_helper.SampleMoviesSearchHelper( + SearchTester(get_connection_string(mdbc, USER_NAME, USER_PASSWORD)) + ) @mark.e2e_search_community_basic @@ -89,15 +104,15 @@ def test_search_create_search_index(sample_movies_helper: SampleMoviesSearchHelp sample_movies_helper.create_search_index() -@mark.e2e_search_community_basic -def test_search_wait_for_search_indexes(sample_movies_helper: SampleMoviesSearchHelper): - sample_movies_helper.wait_for_search_indexes() +# @mark.e2e_search_community_basic +# def test_search_wait_for_search_indexes(sample_movies_helper: SampleMoviesSearchHelper): +# sample_movies_helper.wait_for_search_indexes() @mark.e2e_search_community_basic def test_search_assert_search_query(sample_movies_helper: SampleMoviesSearchHelper): - sample_movies_helper.assert_search_query() + sample_movies_helper.assert_search_query(retry_timeout=60) -def get_connection_string(mdbc: MongoDBCommunity) -> str: - return f"mongodb://{USER_NAME}:{USER_PASSWORD}@{mdbc.name}-0.{mdbc.name}-svc.{mdbc.namespace}.svc.cluster.local:27017/?replicaSet={mdbc.name}" +def get_connection_string(mdbc: MongoDBCommunity, user_name: str, user_password: str) -> str: + return f"mongodb://{user_name}:{user_password}@{mdbc.name}-0.{mdbc.name}-svc.{mdbc.namespace}.svc.cluster.local:27017/?replicaSet={mdbc.name}" diff --git a/helm_chart/crds/mongodb.com_mongodbsearch.yaml b/helm_chart/crds/mongodb.com_mongodbsearch.yaml index 272e7bc59..eeba52768 100644 --- a/helm_chart/crds/mongodb.com_mongodbsearch.yaml +++ b/helm_chart/crds/mongodb.com_mongodbsearch.yaml @@ -160,6 +160,21 @@ spec: required: - name type: object + passwordSecretRef: + description: |- + SecretKeyRef is a reference to a value in a given secret in the same + namespace. Based on: + https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.15/#secretkeyselector-v1-core + properties: + key: + type: string + name: + type: string + required: + - name + type: object + username: + type: string type: object statefulSet: description: |- diff --git a/helm_chart/values.yaml b/helm_chart/values.yaml index 0a44d5ca0..281ff3c30 100644 --- a/helm_chart/values.yaml +++ b/helm_chart/values.yaml @@ -222,7 +222,11 @@ community: search: community: # Full Search container image url used for the MongoDB Community Search container will be constructed as {search.community.repo}/{search.community.name}:{search.community.version} - repo: quay.io/mongodb - name: mongodb-search-community +# repo: quay.io/mongodb +# name: mongodb-search-community +# # default MongoDB Search version used; can be overridden by setting MongoDBSearch.spec.version field. +# version: 1.47.0 + repo: 268558157000.dkr.ecr.eu-west-1.amazonaws.com + name: mongot/community # default MongoDB Search version used; can be overridden by setting MongoDBSearch.spec.version field. - version: 1.47.0 + version: b9b80915f5571bfa5fc2aa70acb20d784e68d79b diff --git a/mongodb-community-operator/controllers/replica_set_controller.go b/mongodb-community-operator/controllers/replica_set_controller.go index 6c0190fb0..644719294 100644 --- a/mongodb-community-operator/controllers/replica_set_controller.go +++ b/mongodb-community-operator/controllers/replica_set_controller.go @@ -14,6 +14,7 @@ import ( "k8s.io/apimachinery/pkg/fields" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/types" + "k8s.io/utils/ptr" "sigs.k8s.io/controller-runtime/pkg/builder" "sigs.k8s.io/controller-runtime/pkg/controller" "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" @@ -727,6 +728,7 @@ func (r ReplicaSetReconciler) buildAutomationConfig(ctx context.Context, mdb mdb prometheusModification, processPortManager.GetPortsModification(), getMongodConfigSearchModification(search), + searchCoordinatorCustomRoleModification(search), ) if err != nil { return automationconfig.AutomationConfig{}, fmt.Errorf("could not create an automation config: %s", err) @@ -739,6 +741,66 @@ func (r ReplicaSetReconciler) buildAutomationConfig(ctx context.Context, mdb mdb return automationConfig, nil } +// TODO: remove this as soon as searchCoordinator builtin role is backported +func searchCoordinatorCustomRoleModification(search *searchv1.MongoDBSearch) automationconfig.Modification { + if search == nil { + return automationconfig.NOOP() + } + + return func(ac *automationconfig.AutomationConfig) { + searchCoordinatorRole := searchCoordinatorCustomRoleStruct() + ac.Roles = append(ac.Roles, searchCoordinatorRole) + } +} + +func searchCoordinatorCustomRoleStruct() automationconfig.CustomRole { + // direct translation of https://github.com/10gen/mongo/blob/6f8d95a513eea8f91ea9f5d895dd8a288dfcf725/src/mongo/db/auth/builtin_roles.yml#L652 + return automationconfig.CustomRole{ + Role: "searchCoordinator", + DB: "admin", + Roles: []automationconfig.Role{ + { + Role: "clusterMonitor", + Database: "admin", + }, + { + Role: "directShardOperations", + Database: "admin", + }, + { + Role: "readAnyDatabase", + Database: "admin", + }, + }, + Privileges: []automationconfig.Privilege{ + { + Resource: automationconfig.Resource{ + DB: ptr.To("__mdb_internal_search"), + Collection: ptr.To(""), + }, + Actions: []string{ + "changeStream", "collStats", "dbHash", "dbStats", "find", + "killCursors", "listCollections", "listIndexes", "listSearchIndexes", + // performRawDataOperations is available only on mongod master + // "performRawDataOperations", + "planCacheRead", "cleanupStructuredEncryptionData", + "compactStructuredEncryptionData", "convertToCapped", "createCollection", + "createIndex", "createSearchIndexes", "dropCollection", "dropIndex", + "dropSearchIndex", "insert", "remove", "renameCollectionSameDB", + "update", "updateSearchIndex", + }, + }, + { + Resource: automationconfig.Resource{ + Cluster: true, + }, + Actions: []string{"bypassDefaultMaxTimeMS"}, + }, + }, + AuthenticationRestrictions: nil, + } +} + // OverrideToAutomationConfig turns an automation config override from the resource spec into an automation config // which can be used to merge. func OverrideToAutomationConfig(override mdbv1.AutomationConfigOverride) automationconfig.AutomationConfig { @@ -773,9 +835,7 @@ func getMongodConfigModification(mdb mdbv1.MongoDBCommunity) automationconfig.Mo // into the configuration set up by the operator. func getMongodConfigSearchModification(search *searchv1.MongoDBSearch) automationconfig.Modification { if search == nil { - return func(config *automationconfig.AutomationConfig) { - // do nothing - } + return automationconfig.NOOP() } searchConfigParameters := search_controller.GetMongodConfigParameters(search) diff --git a/mongodb-community-operator/pkg/mongot/mongot_config.go b/mongodb-community-operator/pkg/mongot/mongot_config.go index 80d4c1421..fed5938c6 100644 --- a/mongodb-community-operator/pkg/mongot/mongot_config.go +++ b/mongodb-community-operator/pkg/mongot/mongot_config.go @@ -1,41 +1,68 @@ package mongot type Config struct { - CommunityPrivatePreview CommunityPrivatePreview `json:"communityPrivatePreview"` + SyncSource ConfigSyncSource `json:"syncSource"` + Storage ConfigStorage `json:"storage"` + Server ConfigServer `json:"server"` + Metrics ConfigMetrics `json:"metrics"` + HealthCheck ConfigHealthCheck `json:"healthCheck"` + Logging ConfigLogging `json:"logging"` } -// CommunityPrivatePreview structure reflects private preview configuration from mongot: -// https://github.com/10gen/mongot/blob/060ec179af062ac2639678f4a613b8ab02c21597/src/main/java/com/xgen/mongot/config/provider/community/CommunityConfig.java#L100 -// Comments are from the default config file: https://github.com/10gen/mongot/blob/375379e56a580916695a2f53e12fd4a99aa24f0b/deploy/community-resources/config.default.yml#L1-L0 -type CommunityPrivatePreview struct { - // Socket (IPv4/6) address of the sync source mongod - MongodHostAndPort string `json:"mongodHostAndPort"` - - // Socket (IPv4/6) address on which to listen for wire protocol connections - QueryServerAddress string `json:"queryServerAddress"` +type ConfigSyncSource struct { + ReplicaSet ConfigReplicaSet `json:"replicaSet"` +} - // Keyfile used for mongod -> mongot authentication - KeyFilePath string `json:"keyFilePath"` +type ConfigReplicaSet struct { + HostAndPort string `json:"hostAndPort"` + Username string `json:"username"` + PasswordFile string `json:"passwordFile"` + ReplicaSetName string `json:"replicaSetName"` + TLS *bool `json:"tls,omitempty"` + ReadPreference *string `json:"readPreference,omitempty"` +} - // Filesystem path that all mongot data will be stored at +type ConfigStorage struct { DataPath string `json:"dataPath"` +} - // Options for metrics - Metrics Metrics `json:"metrics,omitempty"` +type ConfigServer struct { + Wireproto *ConfigWireproto `json:"wireproto,omitempty"` +} + +type ConfigWireproto struct { + Address string `json:"address"` + Authentication *ConfigAuthentication `json:"authentication,omitempty"` + TLS ConfigTLS `json:"tls"` +} - // Options for logging - Logging Logging `json:"logging,omitempty"` +type ConfigAuthentication struct { + Mode string `json:"mode"` + KeyFile string `json:"keyFile"` } -type Metrics struct { - // Whether to enable the Prometheus metrics endpoint - Enabled bool `json:"enabled"` +type ConfigTLSMode string + +const ( + ConfigTLSModeTLS ConfigTLSMode = "TLS" + ConfigTLSModeDisabled ConfigTLSMode = "Disabled" +) + +type ConfigTLS struct { + Mode ConfigTLSMode `json:"mode"` + CertificateKeyFile *string `json:"certificateKeyFile,omitempty"` +} + +type ConfigMetrics struct { + Enabled bool `json:"enabled"` + Address string `json:"address"` +} - // Socket address (IPv4/6) on which the Prometheus /metrics endpoint will be exposed +type ConfigHealthCheck struct { Address string `json:"address"` } -type Logging struct { - // Log level - Verbosity string `json:"verbosity"` +type ConfigLogging struct { + Verbosity string `json:"verbosity"` + LogPath *string `json:"logPath,omitempty"` } diff --git a/public/crds.yaml b/public/crds.yaml index 31015b6d5..d5d28ca3a 100644 --- a/public/crds.yaml +++ b/public/crds.yaml @@ -4828,6 +4828,21 @@ spec: required: - name type: object + passwordSecretRef: + description: |- + SecretKeyRef is a reference to a value in a given secret in the same + namespace. Based on: + https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.15/#secretkeyselector-v1-core + properties: + key: + type: string + name: + type: string + required: + - name + type: object + username: + type: string type: object statefulSet: description: |- diff --git a/public/mongodb-kubernetes-multi-cluster.yaml b/public/mongodb-kubernetes-multi-cluster.yaml index f53630a0e..a83643211 100644 --- a/public/mongodb-kubernetes-multi-cluster.yaml +++ b/public/mongodb-kubernetes-multi-cluster.yaml @@ -432,7 +432,7 @@ spec: value: "ubi8" # Community Env Vars End - name: MDB_SEARCH_COMMUNITY_REPO_URL - value: "quay.io/mongodb" + value: "268558157000.dkr.ecr.us-east-1.amazonaws.com/dev" - name: MDB_SEARCH_COMMUNITY_NAME value: "mongodb-search-community" - name: MDB_SEARCH_COMMUNITY_VERSION diff --git a/public/mongodb-kubernetes-openshift.yaml b/public/mongodb-kubernetes-openshift.yaml index ce0cd73b9..cfe6359ca 100644 --- a/public/mongodb-kubernetes-openshift.yaml +++ b/public/mongodb-kubernetes-openshift.yaml @@ -696,9 +696,9 @@ spec: - name: RELATED_IMAGE_MONGODB_IMAGE_8_0_0_ubi9 value: "quay.io/mongodb/mongodb-enterprise-server:8.0.0-ubi9" - name: RELATED_IMAGE_MDB_SEARCH_IMAGE_1_47_0 - value: "quay.io/mongodb/mongodb-search-community:1.47.0" + value: "268558157000.dkr.ecr.us-east-1.amazonaws.com/dev/mongodb-search-community:1.47.0" - name: MDB_SEARCH_COMMUNITY_REPO_URL - value: "quay.io/mongodb" + value: "268558157000.dkr.ecr.us-east-1.amazonaws.com/dev" - name: MDB_SEARCH_COMMUNITY_NAME value: "mongodb-search-community" - name: MDB_SEARCH_COMMUNITY_VERSION diff --git a/public/mongodb-kubernetes.yaml b/public/mongodb-kubernetes.yaml index 69ff3f0e8..16406a9da 100644 --- a/public/mongodb-kubernetes.yaml +++ b/public/mongodb-kubernetes.yaml @@ -428,7 +428,7 @@ spec: value: "ubi8" # Community Env Vars End - name: MDB_SEARCH_COMMUNITY_REPO_URL - value: "quay.io/mongodb" + value: "268558157000.dkr.ecr.us-east-1.amazonaws.com/dev" - name: MDB_SEARCH_COMMUNITY_NAME value: "mongodb-search-community" - name: MDB_SEARCH_COMMUNITY_VERSION diff --git a/scripts/dev/contexts/e2e_mdb_community b/scripts/dev/contexts/e2e_mdb_community index 4f096c2d7..ebce584eb 100644 --- a/scripts/dev/contexts/e2e_mdb_community +++ b/scripts/dev/contexts/e2e_mdb_community @@ -10,3 +10,11 @@ source "${script_dir}/variables/mongodb_latest" # This variable is needed otherwise the `fetch_om_information.sh` script is called and fails the test export OM_EXTERNALLY_CONFIGURED="true" + +# Temporary development images built from mongot master +#export MDB_SEARCH_COMMUNITY_VERSION="776d43523d185b6b234289e17c191712a3e6569b" # master +#export MDB_SEARCH_COMMUNITY_VERSION="d6884ae132aab30497af55dbaff05e8274e9775f" # Local->Admin +#export MDB_SEARCH_COMMUNITY_VERSION="ad8acf5c3a045d6e0306ad67d61fcb5be40f57ae" # hardcoded mdbc-rs replicaset name +export MDB_SEARCH_COMMUNITY_VERSION="b9b80915f5571bfa5fc2aa70acb20d784e68d79b" # hardcoded Local->Admin, handled replicaSetName in config +export MDB_SEARCH_COMMUNITY_NAME="mongot/community" +export MDB_SEARCH_COMMUNITY_REPO_URL="268558157000.dkr.ecr.eu-west-1.amazonaws.com" diff --git a/scripts/dev/contexts/root-context b/scripts/dev/contexts/root-context index 7b147aa34..6ec7c7a61 100644 --- a/scripts/dev/contexts/root-context +++ b/scripts/dev/contexts/root-context @@ -10,6 +10,7 @@ export PROJECT_DIR="${PWD}" export IMAGE_TYPE=ubi export UBI_IMAGE_WITHOUT_SUFFIX=true export WATCH_NAMESPACE=${WATCH_NAMESPACE:-${NAMESPACE}} +export OPERATOR_NAME="mongodb-kubernetes-operator" # # changing variables below should not be necessary From 9ed720daedf1f7484401cb94ac38c8bf0e1b2b65 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Sierant?= Date: Fri, 18 Jul 2025 17:00:57 +0200 Subject: [PATCH 03/13] Reverted replicaSetName in favor of putting more hosts in connection string (#273) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # Summary This reverts handling replicaSetName from mongot config and adds passing all mongod hosts in the connection string. It is not necessary now to set it, as mongot is connecting in a replicaset mode when there is more than one host passed on connection string. ## Proof of Work ## Based on PR #229 ## Chain of upstream PRs as of 2025-07-17 * PR #229: `master` ← `search/public-preview` * **PR #273 (THIS ONE)**: `search/public-preview` ← `lsierant/search-multiple-hosts` --- controllers/operator/mongodbsearch_controller_test.go | 7 +++++-- .../mongodbsearch_reconcile_helper.go | 8 ++++++-- controllers/search_controller/search_construction.go | 5 +++++ helm_chart/values.yaml | 2 +- .../pkg/mongot/mongot_config.go | 11 +++++------ scripts/dev/contexts/e2e_mdb_community | 4 ++-- 6 files changed, 24 insertions(+), 13 deletions(-) diff --git a/controllers/operator/mongodbsearch_controller_test.go b/controllers/operator/mongodbsearch_controller_test.go index 974ec3935..93e9f4abf 100644 --- a/controllers/operator/mongodbsearch_controller_test.go +++ b/controllers/operator/mongodbsearch_controller_test.go @@ -74,15 +74,18 @@ func newSearchReconciler( } func buildExpectedMongotConfig(search *searchv1.MongoDBSearch, mdbc *mdbcv1.MongoDBCommunity) mongot.Config { + var hostAndPorts []string + for i := range mdbc.Spec.Members { + hostAndPorts = append(hostAndPorts, fmt.Sprintf("%s-%d.%s.%s.svc.cluster.local:%d", mdbc.Name, i, mdbc.Name+"-svc", search.Namespace, 27017)) + } return mongot.Config{ SyncSource: mongot.ConfigSyncSource{ ReplicaSet: mongot.ConfigReplicaSet{ - HostAndPort: fmt.Sprintf("%s.%s.svc.cluster.local:%d", mdbc.Name+"-svc", search.Namespace, 27017), + HostAndPort: hostAndPorts, Username: "mongot-user", PasswordFile: "/tmp/sourceUserPassword", TLS: ptr.To(false), ReadPreference: ptr.To("secondaryPreferred"), - ReplicaSetName: "mdb", }, }, Storage: mongot.ConfigStorage{ diff --git a/controllers/search_controller/mongodbsearch_reconcile_helper.go b/controllers/search_controller/mongodbsearch_reconcile_helper.go index 65e49f4f9..e1a657d67 100644 --- a/controllers/search_controller/mongodbsearch_reconcile_helper.go +++ b/controllers/search_controller/mongodbsearch_reconcile_helper.go @@ -208,15 +208,19 @@ func buildSearchHeadlessService(search *searchv1.MongoDBSearch) corev1.Service { } func createMongotConfig(search *searchv1.MongoDBSearch, db SearchSourceDBResource) mongot.Config { + var hostAndPorts []string + for i := range db.Members() { + hostAndPorts = append(hostAndPorts, fmt.Sprintf("%s-%d.%s.%s.svc.cluster.local:%d", db.Name(), i, db.DatabaseServiceName(), db.GetNamespace(), db.DatabasePort())) + } + return mongot.Config{ SyncSource: mongot.ConfigSyncSource{ ReplicaSet: mongot.ConfigReplicaSet{ - HostAndPort: fmt.Sprintf("%s.%s.svc.cluster.local:%d", db.DatabaseServiceName(), db.GetNamespace(), db.DatabasePort()), + HostAndPort: hostAndPorts, Username: search.SourceUsername(), PasswordFile: "/tmp/sourceUserPassword", TLS: ptr.To(false), ReadPreference: ptr.To("secondaryPreferred"), - ReplicaSetName: db.Name(), }, }, Storage: mongot.ConfigStorage{ diff --git a/controllers/search_controller/search_construction.go b/controllers/search_controller/search_construction.go index 2de1364e1..ad6159b80 100644 --- a/controllers/search_controller/search_construction.go +++ b/controllers/search_controller/search_construction.go @@ -38,6 +38,7 @@ type SearchSourceDBResource interface { DatabasePort() int GetMongoDBVersion() string IsSecurityTLSConfigEnabled() bool + Members() int } func NewSearchSourceDBResourceFromMongoDBCommunity(mdbc *mdbcv1.MongoDBCommunity) SearchSourceDBResource { @@ -48,6 +49,10 @@ type mdbcSearchResource struct { db *mdbcv1.MongoDBCommunity } +func (r *mdbcSearchResource) Members() int { + return r.db.Spec.Members +} + func (r *mdbcSearchResource) Name() string { return r.db.Name } diff --git a/helm_chart/values.yaml b/helm_chart/values.yaml index 281ff3c30..2a7a85706 100644 --- a/helm_chart/values.yaml +++ b/helm_chart/values.yaml @@ -229,4 +229,4 @@ search: repo: 268558157000.dkr.ecr.eu-west-1.amazonaws.com name: mongot/community # default MongoDB Search version used; can be overridden by setting MongoDBSearch.spec.version field. - version: b9b80915f5571bfa5fc2aa70acb20d784e68d79b + version: d6884ae132aab30497af55dbaff05e8274e9775f diff --git a/mongodb-community-operator/pkg/mongot/mongot_config.go b/mongodb-community-operator/pkg/mongot/mongot_config.go index fed5938c6..dc3bbc3fd 100644 --- a/mongodb-community-operator/pkg/mongot/mongot_config.go +++ b/mongodb-community-operator/pkg/mongot/mongot_config.go @@ -14,12 +14,11 @@ type ConfigSyncSource struct { } type ConfigReplicaSet struct { - HostAndPort string `json:"hostAndPort"` - Username string `json:"username"` - PasswordFile string `json:"passwordFile"` - ReplicaSetName string `json:"replicaSetName"` - TLS *bool `json:"tls,omitempty"` - ReadPreference *string `json:"readPreference,omitempty"` + HostAndPort []string `json:"hostAndPort"` + Username string `json:"username"` + PasswordFile string `json:"passwordFile"` + TLS *bool `json:"tls,omitempty"` + ReadPreference *string `json:"readPreference,omitempty"` } type ConfigStorage struct { diff --git a/scripts/dev/contexts/e2e_mdb_community b/scripts/dev/contexts/e2e_mdb_community index ebce584eb..71429c016 100644 --- a/scripts/dev/contexts/e2e_mdb_community +++ b/scripts/dev/contexts/e2e_mdb_community @@ -13,8 +13,8 @@ export OM_EXTERNALLY_CONFIGURED="true" # Temporary development images built from mongot master #export MDB_SEARCH_COMMUNITY_VERSION="776d43523d185b6b234289e17c191712a3e6569b" # master -#export MDB_SEARCH_COMMUNITY_VERSION="d6884ae132aab30497af55dbaff05e8274e9775f" # Local->Admin +export MDB_SEARCH_COMMUNITY_VERSION="d6884ae132aab30497af55dbaff05e8274e9775f" # Local->Admin #export MDB_SEARCH_COMMUNITY_VERSION="ad8acf5c3a045d6e0306ad67d61fcb5be40f57ae" # hardcoded mdbc-rs replicaset name -export MDB_SEARCH_COMMUNITY_VERSION="b9b80915f5571bfa5fc2aa70acb20d784e68d79b" # hardcoded Local->Admin, handled replicaSetName in config +#export MDB_SEARCH_COMMUNITY_VERSION="b9b80915f5571bfa5fc2aa70acb20d784e68d79b" # hardcoded Local->Admin, handled replicaSetName in config export MDB_SEARCH_COMMUNITY_NAME="mongot/community" export MDB_SEARCH_COMMUNITY_REPO_URL="268558157000.dkr.ecr.eu-west-1.amazonaws.com" From 5d44ddc17ecebda47430deb64848834aad36646b Mon Sep 17 00:00:00 2001 From: Yavor Georgiev Date: Mon, 21 Jul 2025 17:51:40 +0200 Subject: [PATCH 04/13] CLOUDP-323995 CLOUDP-321068 Ingress and Egress TLS in mongot (#278) # Summary Expose new TLS capabilities in mongot. ## Proof of Work New [passing](https://spruce.mongodb.com/task/mongodb_kubernetes_e2e_mdb_community_e2e_search_community_tls_patch_39025950721c5e2a2b7e69665729018adceb7ce7_687e2c0229d5cb0007cf2080_25_07_21_12_01_09/tests?execution=0&sorts=STATUS%3AASC) test. ## Checklist - [x] Have you linked a jira ticket and/or is the ticket in the title? - [x] Have you checked whether your jira ticket required DOCSP changes? - [x] Have you checked for release_note changes? --- .evergreen-tasks.yml | 5 + .evergreen.yml | 1 + api/v1/search/mongodbsearch_types.go | 29 +++ api/v1/search/zz_generated.deepcopy.go | 43 ++++ .../crd/bases/mongodb.com_mongodbsearch.yaml | 31 +++ .../operator/mongodbsearch_controller.go | 4 +- .../mongodbsearch_reconcile_helper.go | 183 ++++++++++++++---- .../search_controller/search_construction.go | 10 +- .../tests/common/search/search_tester.py | 8 +- .../community-replicaset-sample-mflix.yaml | 2 +- .../tests/search/search_community_basic.py | 4 +- .../tests/search/search_community_tls.py | 173 +++++++++++++++++ .../crds/mongodb.com_mongodbsearch.yaml | 31 +++ .../pkg/mongot/mongot_config.go | 14 ++ mongodb-community-operator/pkg/tls/tls.go | 113 +++++++++++ public/crds.yaml | 31 +++ 16 files changed, 635 insertions(+), 47 deletions(-) create mode 100644 docker/mongodb-kubernetes-tests/tests/search/search_community_tls.py create mode 100644 mongodb-community-operator/pkg/tls/tls.go diff --git a/.evergreen-tasks.yml b/.evergreen-tasks.yml index b5366b2a0..1d289fa3f 100644 --- a/.evergreen-tasks.yml +++ b/.evergreen-tasks.yml @@ -1285,3 +1285,8 @@ tasks: tags: ["patch-run"] commands: - func: "e2e_test" + + - name: e2e_search_community_tls + tags: ["patch-run"] + commands: + - func: "e2e_test" diff --git a/.evergreen.yml b/.evergreen.yml index 058c0ea1f..9fbc0ead5 100644 --- a/.evergreen.yml +++ b/.evergreen.yml @@ -686,6 +686,7 @@ task_groups: tasks: - e2e_community_replicaset_scale - e2e_search_community_basic + - e2e_search_community_tls # This is the task group that contains all the tests run in the e2e_mdb_kind_ubuntu_cloudqa build variant - name: e2e_mdb_kind_cloudqa_task_group diff --git a/api/v1/search/mongodbsearch_types.go b/api/v1/search/mongodbsearch_types.go index cdb391781..d0648b63b 100644 --- a/api/v1/search/mongodbsearch_types.go +++ b/api/v1/search/mongodbsearch_types.go @@ -36,6 +36,8 @@ type MongoDBSearchSpec struct { Persistence *common.Persistence `json:"persistence,omitempty"` // +optional ResourceRequirements *corev1.ResourceRequirements `json:"resourceRequirements,omitempty"` + // +optional + Security Security `json:"security"` } type MongoDBSource struct { @@ -47,6 +49,22 @@ type MongoDBSource struct { Username *string `json:"username,omitempty"` } +type Security struct { + // +optional + TLS TLS `json:"tls"` +} + +type TLS struct { + Enabled bool `json:"enabled"` + // CertificateKeySecret is a reference to a Secret containing a private key and certificate to use for TLS. + // The key and cert are expected to be PEM encoded and available at "tls.key" and "tls.crt". + // This is the same format used for the standard "kubernetes.io/tls" Secret type, but no specific type is required. + // Alternatively, an entry tls.pem, containing the concatenation of cert and key, can be provided. + // If all of tls.pem, tls.crt and tls.key are present, the tls.pem one needs to be equal to the concatenation of tls.crt and tls.key + // +optional + CertificateKeySecret corev1.LocalObjectReference `json:"certificateKeySecretRef"` +} + type MongoDBSearchStatus struct { status.Common `json:",inline"` Version string `json:"version,omitempty"` @@ -160,3 +178,14 @@ func (s *MongoDBSearch) GetMongotPort() int32 { func (s *MongoDBSearch) GetMongotMetricsPort() int32 { return MongotDefaultMetricsPort } + +// TLSSecretNamespacedName will get the namespaced name of the Secret containing the server certificate and key +func (s *MongoDBSearch) TLSSecretNamespacedName() types.NamespacedName { + return types.NamespacedName{Name: s.Spec.Security.TLS.CertificateKeySecret.Name, Namespace: s.Namespace} +} + +// TLSOperatorSecretNamespacedName will get the namespaced name of the Secret created by the operator +// containing the combined certificate and key. +func (s *MongoDBSearch) TLSOperatorSecretNamespacedName() types.NamespacedName { + return types.NamespacedName{Name: s.Name + "-search-certificate-key", Namespace: s.Namespace} +} diff --git a/api/v1/search/zz_generated.deepcopy.go b/api/v1/search/zz_generated.deepcopy.go index e9384e3de..8817e4b46 100644 --- a/api/v1/search/zz_generated.deepcopy.go +++ b/api/v1/search/zz_generated.deepcopy.go @@ -109,6 +109,7 @@ func (in *MongoDBSearchSpec) DeepCopyInto(out *MongoDBSearchSpec) { *out = new(v1.ResourceRequirements) (*in).DeepCopyInto(*out) } + out.Security = in.Security } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MongoDBSearchSpec. @@ -150,6 +151,16 @@ func (in *MongoDBSource) DeepCopyInto(out *MongoDBSource) { *out = new(user.MongoDBResourceRef) **out = **in } + if in.PasswordSecretRef != nil { + in, out := &in.PasswordSecretRef, &out.PasswordSecretRef + *out = new(user.SecretKeyRef) + **out = **in + } + if in.Username != nil { + in, out := &in.Username, &out.Username + *out = new(string) + **out = **in + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MongoDBSource. @@ -161,3 +172,35 @@ func (in *MongoDBSource) DeepCopy() *MongoDBSource { in.DeepCopyInto(out) return out } + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Security) DeepCopyInto(out *Security) { + *out = *in + out.TLS = in.TLS +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Security. +func (in *Security) DeepCopy() *Security { + if in == nil { + return nil + } + out := new(Security) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *TLS) DeepCopyInto(out *TLS) { + *out = *in + out.CertificateKeySecret = in.CertificateKeySecret +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TLS. +func (in *TLS) DeepCopy() *TLS { + if in == nil { + return nil + } + out := new(TLS) + in.DeepCopyInto(out) + return out +} diff --git a/config/crd/bases/mongodb.com_mongodbsearch.yaml b/config/crd/bases/mongodb.com_mongodbsearch.yaml index eeba52768..585b69210 100644 --- a/config/crd/bases/mongodb.com_mongodbsearch.yaml +++ b/config/crd/bases/mongodb.com_mongodbsearch.yaml @@ -149,6 +149,37 @@ spec: More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ type: object type: object + security: + properties: + tls: + properties: + certificateKeySecretRef: + description: |- + CertificateKeySecret is a reference to a Secret containing a private key and certificate to use for TLS. + The key and cert are expected to be PEM encoded and available at "tls.key" and "tls.crt". + This is the same format used for the standard "kubernetes.io/tls" Secret type, but no specific type is required. + Alternatively, an entry tls.pem, containing the concatenation of cert and key, can be provided. + If all of tls.pem, tls.crt and tls.key are present, the tls.pem one needs to be equal to the concatenation of tls.crt and tls.key + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + TODO: Add other useful fields. apiVersion, kind, uid? + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Drop `kubebuilder:default` when controller-gen doesn't need it https://github.com/kubernetes-sigs/kubebuilder/issues/3896. + type: string + type: object + x-kubernetes-map-type: atomic + enabled: + type: boolean + required: + - enabled + type: object + type: object source: properties: mongodbResourceRef: diff --git a/controllers/operator/mongodbsearch_controller.go b/controllers/operator/mongodbsearch_controller.go index 63ed00a5f..bf8a5f022 100644 --- a/controllers/operator/mongodbsearch_controller.go +++ b/controllers/operator/mongodbsearch_controller.go @@ -13,6 +13,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/reconcile" appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" ctrl "sigs.k8s.io/controller-runtime" searchv1 "github.com/mongodb/mongodb-kubernetes/api/v1/search" @@ -67,7 +68,7 @@ func getSourceMongoDBForSearch(ctx context.Context, kubeClient client.Client, se mdbcName := types.NamespacedName{Namespace: search.GetNamespace(), Name: sourceMongoDBResourceRef.Name} mdbc := &mdbcv1.MongoDBCommunity{} if err := kubeClient.Get(ctx, mdbcName, mdbc); err != nil { - return nil, xerrors.Errorf("error getting MongoDBCommunity %s", mdbcName) + return nil, xerrors.Errorf("error getting MongoDBCommunity %s: %w", mdbcName, err) } return search_controller.NewSearchSourceDBResourceFromMongoDBCommunity(mdbc), nil } @@ -89,5 +90,6 @@ func AddMongoDBSearchController(ctx context.Context, mgr manager.Manager, operat For(&searchv1.MongoDBSearch{}). Watches(&mdbcv1.MongoDBCommunity{}, r.mdbcWatcher). Owns(&appsv1.StatefulSet{}). + Owns(&corev1.Secret{}). Complete(r) } diff --git a/controllers/search_controller/mongodbsearch_reconcile_helper.go b/controllers/search_controller/mongodbsearch_reconcile_helper.go index e1a657d67..6c80806e4 100644 --- a/controllers/search_controller/mongodbsearch_reconcile_helper.go +++ b/controllers/search_controller/mongodbsearch_reconcile_helper.go @@ -23,9 +23,13 @@ import ( searchv1 "github.com/mongodb/mongodb-kubernetes/api/v1/search" "github.com/mongodb/mongodb-kubernetes/controllers/operator/workflow" + "github.com/mongodb/mongodb-kubernetes/mongodb-community-operator/pkg/automationconfig" kubernetesClient "github.com/mongodb/mongodb-kubernetes/mongodb-community-operator/pkg/kube/client" + "github.com/mongodb/mongodb-kubernetes/mongodb-community-operator/pkg/kube/container" + "github.com/mongodb/mongodb-kubernetes/mongodb-community-operator/pkg/kube/podtemplatespec" "github.com/mongodb/mongodb-kubernetes/mongodb-community-operator/pkg/kube/service" "github.com/mongodb/mongodb-kubernetes/mongodb-community-operator/pkg/mongot" + "github.com/mongodb/mongodb-kubernetes/mongodb-community-operator/pkg/tls" "github.com/mongodb/mongodb-kubernetes/pkg/kube/commoncontroller" "github.com/mongodb/mongodb-kubernetes/pkg/statefulset" ) @@ -85,13 +89,28 @@ func (r *MongoDBSearchReconcileHelper) reconcile(ctx context.Context, log *zap.S return workflow.Failed(err) } - mongotConfig := createMongotConfig(r.mdbSearch, r.db) - configHash, err := r.ensureMongotConfig(ctx, mongotConfig) + ingressTlsMongotModification, ingressTlsStsModification, err := r.ensureIngressTlsConfig(ctx) if err != nil { return workflow.Failed(err) } - if err := r.createOrUpdateStatefulSet(ctx, log, configHash); err != nil { + egressTlsMongotModification, egressTlsStsModification, err := r.ensureEgressTlsConfig(ctx) + if err != nil { + return workflow.Failed(err) + } + + configHash, err := r.ensureMongotConfig(ctx, log, createMongotConfig(r.mdbSearch, r.db), ingressTlsMongotModification, egressTlsMongotModification) + if err != nil { + return workflow.Failed(err) + } + + configHashModification := statefulset.WithPodSpecTemplate(podtemplatespec.WithAnnotations( + map[string]string{ + "mongotConfigHash": configHash, + }, + )) + + if err := r.createOrUpdateStatefulSet(ctx, log, CreateSearchStatefulSetFunc(r.mdbSearch, r.db, r.buildImageString()), configHashModification, ingressTlsStsModification, egressTlsStsModification); err != nil { return workflow.Failed(err) } @@ -102,18 +121,19 @@ func (r *MongoDBSearchReconcileHelper) reconcile(ctx context.Context, log *zap.S return workflow.OK() } -func (r *MongoDBSearchReconcileHelper) createOrUpdateStatefulSet(ctx context.Context, log *zap.SugaredLogger, mongotConfigHash string) error { +func (r *MongoDBSearchReconcileHelper) buildImageString() string { imageVersion := r.mdbSearch.Spec.Version if imageVersion == "" { imageVersion = r.operatorSearchConfig.SearchVersion } - searchImage := fmt.Sprintf("%s/%s:%s", r.operatorSearchConfig.SearchRepo, r.operatorSearchConfig.SearchName, imageVersion) + return fmt.Sprintf("%s/%s:%s", r.operatorSearchConfig.SearchRepo, r.operatorSearchConfig.SearchName, imageVersion) +} +func (r *MongoDBSearchReconcileHelper) createOrUpdateStatefulSet(ctx context.Context, log *zap.SugaredLogger, modifications ...statefulset.Modification) error { stsName := r.mdbSearch.StatefulSetNamespacedName() sts := &appsv1.StatefulSet{ObjectMeta: metav1.ObjectMeta{Name: stsName.Name, Namespace: stsName.Namespace}} op, err := controllerutil.CreateOrUpdate(ctx, r.client, sts, func() error { - stsModification := CreateSearchStatefulSetFunc(r.mdbSearch, r.db, searchImage, mongotConfigHash) - stsModification(sts) + statefulset.Apply(modifications...)(sts) return nil }) if err != nil { @@ -143,7 +163,9 @@ func (r *MongoDBSearchReconcileHelper) ensureSearchService(ctx context.Context, return nil } -func (r *MongoDBSearchReconcileHelper) ensureMongotConfig(ctx context.Context, mongotConfig mongot.Config) (string, error) { +func (r *MongoDBSearchReconcileHelper) ensureMongotConfig(ctx context.Context, log *zap.SugaredLogger, modifications ...mongot.Modification) (string, error) { + mongotConfig := mongot.Config{} + mongot.Apply(modifications...)(&mongotConfig) configData, err := yaml.Marshal(mongotConfig) if err != nil { return "", err @@ -164,11 +186,98 @@ func (r *MongoDBSearchReconcileHelper) ensureMongotConfig(ctx context.Context, m return "", err } - zap.S().Debugf("Updated mongot config yaml config map: %v (%s) with the following configuration: %s", cmName, op, string(configData)) + log.Debugf("Updated mongot config yaml config map: %v (%s) with the following configuration: %s", cmName, op, string(configData)) return hashMongotConfig(configData), nil } +func (r *MongoDBSearchReconcileHelper) ensureIngressTlsConfig(ctx context.Context) (mongot.Modification, statefulset.Modification, error) { + if !r.mdbSearch.Spec.Security.TLS.Enabled { + mongotModification := func(config *mongot.Config) { + config.Server.Wireproto.TLS.Mode = mongot.ConfigTLSModeDisabled + } + return mongotModification, statefulset.NOOP(), nil + } + + // TODO: validate that the certificate in the user-provided Secret in .spec.security.tls.certificateKeySecret is issued by the CA in the operator's CA Secret + + certFileName, err := tls.EnsureTLSSecret(ctx, r.client, r.mdbSearch) + if err != nil { + return nil, nil, err + } + + mongotModification := func(config *mongot.Config) { + certPath := tls.OperatorSecretMountPath + certFileName + config.Server.Wireproto.TLS.Mode = mongot.ConfigTLSModeTLS + config.Server.Wireproto.TLS.CertificateKeyFile = ptr.To(certPath) + } + + tlsSecret := r.mdbSearch.TLSOperatorSecretNamespacedName() + tlsVolume := statefulset.CreateVolumeFromSecret("tls", tlsSecret.Name) + tlsVolumeMount := statefulset.CreateVolumeMount("tls", tls.OperatorSecretMountPath, statefulset.WithReadOnly(true)) + statefulsetModification := statefulset.WithPodSpecTemplate(podtemplatespec.Apply( + podtemplatespec.WithVolume(tlsVolume), + podtemplatespec.WithContainer(MongotContainerName, container.Apply( + container.WithVolumeMounts([]corev1.VolumeMount{tlsVolumeMount}), + )), + )) + + return mongotModification, statefulsetModification, nil +} + +func (r *MongoDBSearchReconcileHelper) ensureEgressTlsConfig(ctx context.Context) (mongot.Modification, statefulset.Modification, error) { + if !r.db.IsSecurityTLSConfigEnabled() { + return mongot.NOOP(), statefulset.NOOP(), nil + } + + caSecretName := r.db.TLSOperatorCASecretNamespacedName() + caSecret := &corev1.Secret{} + if err := r.client.Get(ctx, caSecretName, caSecret); err != nil { + return nil, nil, xerrors.Errorf("error getting CA Secret %s: %w", caSecretName, err) + } + + // HACK: find a better way of getting the CA file name + var caFileName string + for k := range caSecret.Data { + caFileName = k + } + + mongotModification := func(config *mongot.Config) { + config.SyncSource.ReplicaSet.TLS = ptr.To(true) + } + + _, containerSecurityContext := podtemplatespec.WithDefaultSecurityContextsModifications() + caVolume := statefulset.CreateVolumeFromSecret("ca", caSecretName.Name) + trustStoreVolume := statefulset.CreateVolumeFromEmptyDir("cacerts") + statefulsetModification := statefulset.WithPodSpecTemplate(podtemplatespec.Apply( + podtemplatespec.WithVolume(caVolume), + podtemplatespec.WithVolume(trustStoreVolume), + podtemplatespec.WithInitContainer("init-cacerts", container.Apply( + container.WithImage(r.buildImageString()), + containerSecurityContext, + container.WithVolumeMounts([]corev1.VolumeMount{ + statefulset.CreateVolumeMount(caVolume.Name, tls.CAMountPath, statefulset.WithReadOnly(true)), + statefulset.CreateVolumeMount(trustStoreVolume.Name, "/java/trust-store", statefulset.WithReadOnly(false)), + }), + container.WithCommand([]string{"sh"}), + container.WithArgs([]string{ + "-c", + fmt.Sprintf(` +cp /mongot-community/bin/jdk/lib/security/cacerts /java/trust-store/cacerts +/mongot-community/bin/jdk/bin/keytool -keystore /java/trust-store/cacerts -storepass changeit -noprompt -trustcacerts -importcert -alias mongodcert -file %s/%s + `, tls.CAMountPath, caFileName), + }), + )), + podtemplatespec.WithContainer(MongotContainerName, container.Apply( + container.WithVolumeMounts([]corev1.VolumeMount{ + statefulset.CreateVolumeMount(trustStoreVolume.Name, "/mongot-community/bin/jdk/lib/security/cacerts", statefulset.WithReadOnly(true), statefulset.WithSubPath("cacerts")), + }), + )), + )) + + return mongotModification, statefulsetModification, nil +} + func hashMongotConfig(mongotConfigYaml []byte) string { hashBytes := sha256.Sum256(mongotConfigYaml) return base32.StdEncoding.WithPadding(base32.NoPadding).EncodeToString(hashBytes[:]) @@ -207,14 +316,14 @@ func buildSearchHeadlessService(search *searchv1.MongoDBSearch) corev1.Service { return serviceBuilder.Build() } -func createMongotConfig(search *searchv1.MongoDBSearch, db SearchSourceDBResource) mongot.Config { - var hostAndPorts []string - for i := range db.Members() { - hostAndPorts = append(hostAndPorts, fmt.Sprintf("%s-%d.%s.%s.svc.cluster.local:%d", db.Name(), i, db.DatabaseServiceName(), db.GetNamespace(), db.DatabasePort())) - } +func createMongotConfig(search *searchv1.MongoDBSearch, db SearchSourceDBResource) mongot.Modification { + return func(config *mongot.Config) { + var hostAndPorts []string + for i := range db.Members() { + hostAndPorts = append(hostAndPorts, fmt.Sprintf("%s-%d.%s.%s.svc.cluster.local:%d", db.Name(), i, db.DatabaseServiceName(), db.GetNamespace(), db.DatabasePort())) + } - return mongot.Config{ - SyncSource: mongot.ConfigSyncSource{ + config.SyncSource = mongot.ConfigSyncSource{ ReplicaSet: mongot.ConfigReplicaSet{ HostAndPort: hostAndPorts, Username: search.SourceUsername(), @@ -222,40 +331,44 @@ func createMongotConfig(search *searchv1.MongoDBSearch, db SearchSourceDBResourc TLS: ptr.To(false), ReadPreference: ptr.To("secondaryPreferred"), }, - }, - Storage: mongot.ConfigStorage{ + } + config.Storage = mongot.ConfigStorage{ DataPath: "/mongot/data/config.yml", - }, - Server: mongot.ConfigServer{ + } + config.Server = mongot.ConfigServer{ Wireproto: &mongot.ConfigWireproto{ Address: "0.0.0.0:27027", Authentication: &mongot.ConfigAuthentication{ Mode: "keyfile", KeyFile: "/tmp/keyfile", }, - TLS: mongot.ConfigTLS{Mode: "disabled"}, }, - }, - Metrics: mongot.ConfigMetrics{ + } + config.Metrics = mongot.ConfigMetrics{ Enabled: true, Address: fmt.Sprintf("localhost:%d", search.GetMongotMetricsPort()), - }, - HealthCheck: mongot.ConfigHealthCheck{ + } + config.HealthCheck = mongot.ConfigHealthCheck{ Address: "0.0.0.0:8080", - }, - Logging: mongot.ConfigLogging{ + } + config.Logging = mongot.ConfigLogging{ Verbosity: "TRACE", LogPath: nil, - }, + } } } -func GetMongodConfigParameters(search *searchv1.MongoDBSearch) map[string]interface{} { - return map[string]interface{}{ - "setParameter": map[string]interface{}{ +func GetMongodConfigParameters(search *searchv1.MongoDBSearch) map[string]any { + searchTLSMode := automationconfig.TLSModeDisabled + if search.Spec.Security.TLS.Enabled { + searchTLSMode = automationconfig.TLSModeRequired + } + return map[string]any{ + "setParameter": map[string]any{ "mongotHost": mongotHostAndPort(search), "searchIndexManagementHostAndPort": mongotHostAndPort(search), "skipAuthenticationToSearchIndexManagementServer": false, + "searchTLSMode": searchTLSMode, }, } } @@ -269,12 +382,8 @@ func ValidateSearchSource(db SearchSourceDBResource) error { version, err := semver.ParseTolerant(db.GetMongoDBVersion()) if err != nil { return xerrors.Errorf("error parsing MongoDB version '%s': %w", db.GetMongoDBVersion(), err) - } else if version.Major < 8 { - return xerrors.New("MongoDB version must be 8.0 or higher") - } - - if db.IsSecurityTLSConfigEnabled() { - return xerrors.New("MongoDBSearch does not support TLS-enabled sources") + } else if version.LT(semver.MustParse("8.0.10")) { + return xerrors.New("MongoDB version must be 8.0.10 or higher") } return nil diff --git a/controllers/search_controller/search_construction.go b/controllers/search_controller/search_construction.go index ad6159b80..ee42e4aa4 100644 --- a/controllers/search_controller/search_construction.go +++ b/controllers/search_controller/search_construction.go @@ -38,6 +38,7 @@ type SearchSourceDBResource interface { DatabasePort() int GetMongoDBVersion() string IsSecurityTLSConfigEnabled() bool + TLSOperatorCASecretNamespacedName() types.NamespacedName Members() int } @@ -89,8 +90,12 @@ func (r *mdbcSearchResource) DatabasePort() int { return r.db.GetMongodConfiguration().GetDBPort() } +func (r *mdbcSearchResource) TLSOperatorCASecretNamespacedName() types.NamespacedName { + return r.db.TLSOperatorCASecretNamespacedName() +} + // ReplicaSetOptions returns a set of options which will configure a ReplicaSet StatefulSet -func CreateSearchStatefulSetFunc(mdbSearch *searchv1.MongoDBSearch, sourceDBResource SearchSourceDBResource, searchImage string, mongotConfigHash string) statefulset.Modification { +func CreateSearchStatefulSetFunc(mdbSearch *searchv1.MongoDBSearch, sourceDBResource SearchSourceDBResource, searchImage string) statefulset.Modification { labels := map[string]string{ "app": mdbSearch.SearchServiceNamespacedName().Name, } @@ -153,9 +158,6 @@ func CreateSearchStatefulSetFunc(mdbSearch *searchv1.MongoDBSearch, sourceDBReso podtemplatespec.Apply( podSecurityContext, podtemplatespec.WithPodLabels(labels), - podtemplatespec.WithAnnotations(map[string]string{ - "mongotConfigHash": mongotConfigHash, - }), podtemplatespec.WithVolumes(volumes), podtemplatespec.WithServiceAccount(sourceDBResource.DatabaseServiceName()), podtemplatespec.WithServiceAccount(util.MongoDBServiceAccount), diff --git a/docker/mongodb-kubernetes-tests/tests/common/search/search_tester.py b/docker/mongodb-kubernetes-tests/tests/common/search/search_tester.py index 7c5b78eac..8d8334ee9 100644 --- a/docker/mongodb-kubernetes-tests/tests/common/search/search_tester.py +++ b/docker/mongodb-kubernetes-tests/tests/common/search/search_tester.py @@ -15,8 +15,10 @@ class SearchTester(MongoTester): def __init__( self, connection_string: str, + use_ssl: bool = False, + ca_path: str | None = None, ): - super().__init__(connection_string, False) + super().__init__(connection_string, use_ssl, ca_path) def mongorestore_from_url(self, archive_url: str, ns_include: str, mongodb_tools_dir: str = ""): logger.debug(f"running mongorestore from {archive_url}") @@ -26,6 +28,10 @@ def mongorestore_from_url(self, archive_url: str, ns_include: str, mongodb_tools logger.debug(f"Downloaded sample file from {archive_url} to {sample_file.name} (size: {size})") mongorestore_path = os.path.join(mongodb_tools_dir, "mongorestore") mongorestore_cmd = f"{mongorestore_path} --archive={sample_file.name} --verbose=1 --drop --nsInclude {ns_include} --uri={self.cnx_string}" + if self.default_opts.get("tls", False): + mongorestore_cmd += " --ssl" + if ca_path := self.default_opts.get("tlsCAFile"): + mongorestore_cmd += " --sslCAFile=" + ca_path process_run_and_check(mongorestore_cmd.split()) def create_search_index(self, database_name: str, collection_name: str): diff --git a/docker/mongodb-kubernetes-tests/tests/search/fixtures/community-replicaset-sample-mflix.yaml b/docker/mongodb-kubernetes-tests/tests/search/fixtures/community-replicaset-sample-mflix.yaml index 4042b7d65..490ad3304 100644 --- a/docker/mongodb-kubernetes-tests/tests/search/fixtures/community-replicaset-sample-mflix.yaml +++ b/docker/mongodb-kubernetes-tests/tests/search/fixtures/community-replicaset-sample-mflix.yaml @@ -6,7 +6,7 @@ metadata: spec: members: 3 type: ReplicaSet - version: "8.0.5" + version: "8.0.10" security: authentication: modes: ["SCRAM-SHA-1"] diff --git a/docker/mongodb-kubernetes-tests/tests/search/search_community_basic.py b/docker/mongodb-kubernetes-tests/tests/search/search_community_basic.py index 18e2ec025..3d4953913 100644 --- a/docker/mongodb-kubernetes-tests/tests/search/search_community_basic.py +++ b/docker/mongodb-kubernetes-tests/tests/search/search_community_basic.py @@ -25,15 +25,13 @@ @fixture(scope="function") -def mdbc(namespace: str, custom_mdb_version: str) -> MongoDBCommunity: +def mdbc(namespace: str) -> MongoDBCommunity: resource = MongoDBCommunity.from_yaml( yaml_fixture("community-replicaset-sample-mflix.yaml"), name=MDBC_RESOURCE_NAME, namespace=namespace, ) - resource["spec"]["version"] = custom_mdb_version - # if try_load(resource): # return resource diff --git a/docker/mongodb-kubernetes-tests/tests/search/search_community_tls.py b/docker/mongodb-kubernetes-tests/tests/search/search_community_tls.py new file mode 100644 index 000000000..aa0c53e79 --- /dev/null +++ b/docker/mongodb-kubernetes-tests/tests/search/search_community_tls.py @@ -0,0 +1,173 @@ +import pymongo +from kubetester import create_or_update_secret, try_load +from kubetester.certs import create_tls_certs +from kubetester.kubetester import fixture as yaml_fixture +from kubetester.mongodb_community import MongoDBCommunity +from kubetester.mongodb_search import MongoDBSearch +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.movies_search_helper import SampleMoviesSearchHelper +from tests.common.search.search_tester import SearchTester +from tests.conftest import get_default_operator + +logger = test_logger.get_test_logger(__name__) + +ADMIN_USER_NAME = "mdb-admin-user" +ADMIN_USER_PASSWORD = "mdb-admin-user-pass" + +MONGOT_USER_NAME = "mongot-user" +MONGOT_USER_PASSWORD = "mongot-user-password" + +USER_NAME = "mdb-user" +USER_PASSWORD = "mdb-user-pass" + +MDBC_RESOURCE_NAME = "mdbc-rs" + +TLS_SECRET_NAME = "tls-secret" + +# MongoDBSearch TLS configuration +MDBS_TLS_SECRET_NAME = "mdbs-tls-secret" + + +@fixture(scope="function") +def mdbc(namespace: str) -> MongoDBCommunity: + resource = MongoDBCommunity.from_yaml( + yaml_fixture("community-replicaset-sample-mflix.yaml"), + name=MDBC_RESOURCE_NAME, + namespace=namespace, + ) + + if try_load(resource): + return resource + + # Add TLS configuration + resource["spec"]["security"]["tls"] = { + "enabled": True, + "certificateKeySecretRef": {"name": TLS_SECRET_NAME}, + "caCertificateSecretRef": {"name": TLS_SECRET_NAME}, + } + + return resource + + +@fixture(scope="function") +def mdbs(namespace: str) -> MongoDBSearch: + resource = MongoDBSearch.from_yaml( + yaml_fixture("search-minimal.yaml"), + namespace=namespace, + ) + + if try_load(resource): + return resource + + # Add TLS configuration to MongoDBSearch + if "spec" not in resource: + resource["spec"] = {} + + resource["spec"]["security"] = {"tls": {"enabled": True, "certificateKeySecretRef": {"name": MDBS_TLS_SECRET_NAME}}} + + return resource + + +@mark.e2e_search_community_tls +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_community_tls +def test_install_secrets(namespace: str, mdbs: MongoDBSearch): + # Create user password secrets + create_or_update_secret(namespace=namespace, name=f"{USER_NAME}-password", data={"password": USER_PASSWORD}) + create_or_update_secret( + namespace=namespace, name=f"{ADMIN_USER_NAME}-password", data={"password": ADMIN_USER_PASSWORD} + ) + create_or_update_secret( + namespace=namespace, name=f"{mdbs.name}-{MONGOT_USER_NAME}-password", data={"password": MONGOT_USER_PASSWORD} + ) + + +@mark.e2e_search_community_tls +def test_install_tls_secrets_and_configmaps(namespace: str, mdbc: MongoDBCommunity, mdbs: MongoDBSearch, issuer: str): + create_tls_certs(issuer, namespace, mdbc.name, mdbc["spec"]["members"], secret_name=TLS_SECRET_NAME) + + 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"], + secret_name=MDBS_TLS_SECRET_NAME, + ) + + +@mark.e2e_search_community_tls +def test_create_database_resource(mdbc: MongoDBCommunity): + mdbc.update() + mdbc.assert_reaches_phase(Phase.Running, timeout=1000) + + +@mark.e2e_search_community_tls +def test_create_search_resource(mdbs: MongoDBSearch): + mdbs.update() + mdbs.assert_reaches_phase(Phase.Running, timeout=300) + + +@mark.e2e_search_community_tls +def test_wait_for_community_resource_ready(mdbc: MongoDBCommunity): + mdbc.assert_reaches_phase(Phase.Running, timeout=1800) + + +@mark.e2e_search_community_tls +def test_validate_tls_connections(mdbc: MongoDBCommunity, mdbs: MongoDBSearch, namespace: str, issuer_ca_filepath: str): + with pymongo.MongoClient( + f"mongodb://{mdbc.name}-0.{mdbc.name}-svc.{namespace}.svc.cluster.local:27017/?replicaSet={mdbc.name}", + tls=True, + tlsCAFile=issuer_ca_filepath, + tlsAllowInvalidHostnames=False, + serverSelectionTimeoutMS=30000, + connectTimeoutMS=20000, + ) as mongodb_client: + mongodb_info = mongodb_client.admin.command("hello") + assert mongodb_info.get("ok") == 1, "MongoDBCommunity connection failed" + + with pymongo.MongoClient( + f"mongodb://{mdbs.name}-search-svc.{namespace}.svc.cluster.local:27027", + tls=True, + tlsCAFile=issuer_ca_filepath, + tlsAllowInvalidHostnames=False, + serverSelectionTimeoutMS=10000, + connectTimeoutMS=10000, + ) as search_client: + search_info = search_client.admin.command("hello") + assert search_info.get("ok") == 1, "MongoDBSearch connection failed" + + +@fixture(scope="function") +def sample_movies_helper(mdbc: MongoDBCommunity, issuer_ca_filepath: str) -> SampleMoviesSearchHelper: + return movies_search_helper.SampleMoviesSearchHelper( + SearchTester(get_connection_string(mdbc, USER_NAME, USER_PASSWORD), use_ssl=True, ca_path=issuer_ca_filepath), + ) + + +@mark.e2e_search_community_tls +def test_search_restore_sample_database(sample_movies_helper: SampleMoviesSearchHelper): + sample_movies_helper.restore_sample_database() + + +@mark.e2e_search_community_tls +def test_search_create_search_index(sample_movies_helper: SampleMoviesSearchHelper): + sample_movies_helper.create_search_index() + + +@mark.e2e_search_community_tls +def test_search_assert_search_query(sample_movies_helper: SampleMoviesSearchHelper): + sample_movies_helper.assert_search_query(retry_timeout=60) + + +def get_connection_string(mdbc: MongoDBCommunity, user_name: str, user_password: str) -> str: + return f"mongodb://{user_name}:{user_password}@{mdbc.name}-0.{mdbc.name}-svc.{mdbc.namespace}.svc.cluster.local:27017/?replicaSet={mdbc.name}" diff --git a/helm_chart/crds/mongodb.com_mongodbsearch.yaml b/helm_chart/crds/mongodb.com_mongodbsearch.yaml index eeba52768..585b69210 100644 --- a/helm_chart/crds/mongodb.com_mongodbsearch.yaml +++ b/helm_chart/crds/mongodb.com_mongodbsearch.yaml @@ -149,6 +149,37 @@ spec: More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ type: object type: object + security: + properties: + tls: + properties: + certificateKeySecretRef: + description: |- + CertificateKeySecret is a reference to a Secret containing a private key and certificate to use for TLS. + The key and cert are expected to be PEM encoded and available at "tls.key" and "tls.crt". + This is the same format used for the standard "kubernetes.io/tls" Secret type, but no specific type is required. + Alternatively, an entry tls.pem, containing the concatenation of cert and key, can be provided. + If all of tls.pem, tls.crt and tls.key are present, the tls.pem one needs to be equal to the concatenation of tls.crt and tls.key + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + TODO: Add other useful fields. apiVersion, kind, uid? + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Drop `kubebuilder:default` when controller-gen doesn't need it https://github.com/kubernetes-sigs/kubebuilder/issues/3896. + type: string + type: object + x-kubernetes-map-type: atomic + enabled: + type: boolean + required: + - enabled + type: object + type: object source: properties: mongodbResourceRef: diff --git a/mongodb-community-operator/pkg/mongot/mongot_config.go b/mongodb-community-operator/pkg/mongot/mongot_config.go index dc3bbc3fd..2cd1b5da5 100644 --- a/mongodb-community-operator/pkg/mongot/mongot_config.go +++ b/mongodb-community-operator/pkg/mongot/mongot_config.go @@ -1,5 +1,19 @@ package mongot +type Modification func(*Config) + +func NOOP() Modification { + return func(config *Config) {} +} + +func Apply(modifications ...Modification) func(*Config) { + return func(config *Config) { + for _, mod := range modifications { + mod(config) + } + } +} + type Config struct { SyncSource ConfigSyncSource `json:"syncSource"` Storage ConfigStorage `json:"storage"` diff --git a/mongodb-community-operator/pkg/tls/tls.go b/mongodb-community-operator/pkg/tls/tls.go new file mode 100644 index 000000000..77c49ed2a --- /dev/null +++ b/mongodb-community-operator/pkg/tls/tls.go @@ -0,0 +1,113 @@ +package tls + +import ( + "context" + "crypto/sha256" + "fmt" + "strings" + + "k8s.io/apimachinery/pkg/types" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "github.com/mongodb/mongodb-kubernetes/mongodb-community-operator/pkg/kube/secret" +) + +const ( + CAMountPath = "/var/lib/tls/ca/" + OperatorSecretMountPath = "/var/lib/tls/server/" //nolint + + tlsSecretCertName = "tls.crt" + tlsSecretKeyName = "tls.key" + tlsSecretPemName = "tls.pem" +) + +type TLSConfigurableResource interface { + metav1.Object + TLSSecretNamespacedName() types.NamespacedName + TLSOperatorSecretNamespacedName() types.NamespacedName +} + +// ensureTLSSecret will create or update the operator-managed Secret containing +// the concatenated certificate and key from the user-provided Secret. +// Returns the file name of the concatenated certificate and key +func EnsureTLSSecret(ctx context.Context, getUpdateCreator secret.GetUpdateCreator, resource TLSConfigurableResource) (string, error) { + certKey, err := getPemOrConcatenatedCrtAndKey(ctx, getUpdateCreator, resource.TLSSecretNamespacedName()) + if err != nil { + return "", err + } + // Calculate file name from certificate and key + fileName := OperatorSecretFileName(certKey) + + operatorSecret := secret.Builder(). + SetName(resource.TLSOperatorSecretNamespacedName().Name). + SetNamespace(resource.TLSOperatorSecretNamespacedName().Namespace). + SetField(fileName, certKey). + SetOwnerReferences(resource.GetOwnerReferences()). + Build() + + return fileName, secret.CreateOrUpdate(ctx, getUpdateCreator, operatorSecret) +} + +// getCertAndKey will fetch the certificate and key from the user-provided Secret. +func getCertAndKey(ctx context.Context, getter secret.Getter, secretName types.NamespacedName) string { + cert, err := secret.ReadKey(ctx, getter, tlsSecretCertName, secretName) + if err != nil { + return "" + } + + key, err := secret.ReadKey(ctx, getter, tlsSecretKeyName, secretName) + if err != nil { + return "" + } + + return combineCertificateAndKey(cert, key) +} + +// getPem will fetch the pem from the user-provided secret +func getPem(ctx context.Context, getter secret.Getter, secretName types.NamespacedName) string { + pem, err := secret.ReadKey(ctx, getter, tlsSecretPemName, secretName) + if err != nil { + return "" + } + return pem +} + +func combineCertificateAndKey(cert, key string) string { + trimmedCert := strings.TrimRight(cert, "\n") + trimmedKey := strings.TrimRight(key, "\n") + return fmt.Sprintf("%s\n%s", trimmedCert, trimmedKey) +} + +// getPemOrConcatenatedCrtAndKey will get the final PEM to write to the secret. +// This is either the tls.pem entry in the given secret, or the concatenation +// of tls.crt and tls.key +// It performs a basic validation on the entries. +func getPemOrConcatenatedCrtAndKey(ctx context.Context, getter secret.Getter, secretName types.NamespacedName) (string, error) { + certKey := getCertAndKey(ctx, getter, secretName) + pem := getPem(ctx, getter, secretName) + if certKey == "" && pem == "" { + return "", fmt.Errorf(`neither "%s" nor the pair "%s"/"%s" were present in the TLS secret`, tlsSecretPemName, tlsSecretCertName, tlsSecretKeyName) + } + if certKey == "" { + return pem, nil + } + if pem == "" { + return certKey, nil + } + if certKey != pem { + return "", fmt.Errorf(`if all of "%s", "%s" and "%s" are present in the secret, the entry for "%s" must be equal to the concatenation of "%s" with "%s"`, tlsSecretCertName, tlsSecretKeyName, tlsSecretPemName, tlsSecretPemName, tlsSecretCertName, tlsSecretKeyName) + } + return certKey, nil +} + +// OperatorSecretFileName calculates the file name to use for the mounted +// certificate-key file. The name is based on the hash of the combined cert and key. +// If the certificate or key changes, the file path changes as well which will trigger +// the agent to perform a restart. +// The user-provided secret is being watched and will trigger a reconciliation +// on changes. This enables the operator to automatically handle cert rotations. +func OperatorSecretFileName(certKey string) string { + hash := sha256.Sum256([]byte(certKey)) + return fmt.Sprintf("%x.pem", hash) +} diff --git a/public/crds.yaml b/public/crds.yaml index d5d28ca3a..77106433c 100644 --- a/public/crds.yaml +++ b/public/crds.yaml @@ -4817,6 +4817,37 @@ spec: More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ type: object type: object + security: + properties: + tls: + properties: + certificateKeySecretRef: + description: |- + CertificateKeySecret is a reference to a Secret containing a private key and certificate to use for TLS. + The key and cert are expected to be PEM encoded and available at "tls.key" and "tls.crt". + This is the same format used for the standard "kubernetes.io/tls" Secret type, but no specific type is required. + Alternatively, an entry tls.pem, containing the concatenation of cert and key, can be provided. + If all of tls.pem, tls.crt and tls.key are present, the tls.pem one needs to be equal to the concatenation of tls.crt and tls.key + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + TODO: Add other useful fields. apiVersion, kind, uid? + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Drop `kubebuilder:default` when controller-gen doesn't need it https://github.com/kubernetes-sigs/kubebuilder/issues/3896. + type: string + type: object + x-kubernetes-map-type: atomic + enabled: + type: boolean + required: + - enabled + type: object + type: object source: properties: mongodbResourceRef: From 18ea4abd6aef927e0efa760c9cefc13ce1f720e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Sierant?= Date: Wed, 23 Jul 2025 08:29:08 +0200 Subject: [PATCH 05/13] Using auth source and (#285) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # Summary Handled recent fixes to auth db and replicaset connection in mongot. ## Chain of upstream PRs as of 2025-07-22 * PR #229: `master` ← `search/public-preview` * **PR #285 (THIS ONE)**: `search/public-preview` ← `lsierant/auth-source` --- config/manager/manager.yaml | 6 +++--- controllers/operator/mongodbsearch_controller_test.go | 1 + .../search_controller/mongodbsearch_reconcile_helper.go | 1 + .../tests/common/search/movies_search_helper.py | 1 - helm_chart/values.yaml | 2 +- mongodb-community-operator/pkg/mongot/mongot_config.go | 1 + public/mongodb-kubernetes-multi-cluster.yaml | 4 ++-- public/mongodb-kubernetes-openshift.yaml | 6 +++--- public/mongodb-kubernetes.yaml | 4 ++-- scripts/dev/contexts/e2e_mdb_community | 3 ++- 10 files changed, 16 insertions(+), 13 deletions(-) diff --git a/config/manager/manager.yaml b/config/manager/manager.yaml index ce5ce623d..3c463f2ba 100644 --- a/config/manager/manager.yaml +++ b/config/manager/manager.yaml @@ -392,10 +392,10 @@ spec: - name: RELATED_IMAGE_MONGODB_IMAGE_8_0_0_ubi9 value: "quay.io/mongodb/mongodb-enterprise-server:8.0.0-ubi9" - name: RELATED_IMAGE_MDB_SEARCH_IMAGE_1_47_0 - value: "268558157000.dkr.ecr.us-east-1.amazonaws.com/dev/mongodb-search-community:1.47.0" + value: "268558157000.dkr.ecr.eu-west-1.amazonaws.com/mongot/community:1.47.0" - name: MDB_SEARCH_COMMUNITY_REPO_URL - value: "268558157000.dkr.ecr.us-east-1.amazonaws.com/dev" + value: "268558157000.dkr.ecr.eu-west-1.amazonaws.com" - name: MDB_SEARCH_COMMUNITY_NAME - value: "mongodb-search-community" + value: "mongot/community" - name: MDB_SEARCH_COMMUNITY_VERSION value: "1.47.0" diff --git a/controllers/operator/mongodbsearch_controller_test.go b/controllers/operator/mongodbsearch_controller_test.go index 93e9f4abf..7e498a8df 100644 --- a/controllers/operator/mongodbsearch_controller_test.go +++ b/controllers/operator/mongodbsearch_controller_test.go @@ -86,6 +86,7 @@ func buildExpectedMongotConfig(search *searchv1.MongoDBSearch, mdbc *mdbcv1.Mong PasswordFile: "/tmp/sourceUserPassword", TLS: ptr.To(false), ReadPreference: ptr.To("secondaryPreferred"), + AuthSource: ptr.To("admin"), }, }, Storage: mongot.ConfigStorage{ diff --git a/controllers/search_controller/mongodbsearch_reconcile_helper.go b/controllers/search_controller/mongodbsearch_reconcile_helper.go index 6c80806e4..fa7bd677c 100644 --- a/controllers/search_controller/mongodbsearch_reconcile_helper.go +++ b/controllers/search_controller/mongodbsearch_reconcile_helper.go @@ -330,6 +330,7 @@ func createMongotConfig(search *searchv1.MongoDBSearch, db SearchSourceDBResourc PasswordFile: "/tmp/sourceUserPassword", TLS: ptr.To(false), ReadPreference: ptr.To("secondaryPreferred"), + AuthSource: ptr.To("admin"), }, } config.Storage = mongot.ConfigStorage{ diff --git a/docker/mongodb-kubernetes-tests/tests/common/search/movies_search_helper.py b/docker/mongodb-kubernetes-tests/tests/common/search/movies_search_helper.py index 807d97493..be2151158 100644 --- a/docker/mongodb-kubernetes-tests/tests/common/search/movies_search_helper.py +++ b/docker/mongodb-kubernetes-tests/tests/common/search/movies_search_helper.py @@ -1,7 +1,6 @@ import logging import pymongo.errors - from kubetester import kubetester from tests import test_logger from tests.common.search.search_tester import SearchTester diff --git a/helm_chart/values.yaml b/helm_chart/values.yaml index 2a7a85706..ebb8bd1c4 100644 --- a/helm_chart/values.yaml +++ b/helm_chart/values.yaml @@ -229,4 +229,4 @@ search: repo: 268558157000.dkr.ecr.eu-west-1.amazonaws.com name: mongot/community # default MongoDB Search version used; can be overridden by setting MongoDBSearch.spec.version field. - version: d6884ae132aab30497af55dbaff05e8274e9775f + version: 1.47.0 diff --git a/mongodb-community-operator/pkg/mongot/mongot_config.go b/mongodb-community-operator/pkg/mongot/mongot_config.go index 2cd1b5da5..c8126b173 100644 --- a/mongodb-community-operator/pkg/mongot/mongot_config.go +++ b/mongodb-community-operator/pkg/mongot/mongot_config.go @@ -33,6 +33,7 @@ type ConfigReplicaSet struct { PasswordFile string `json:"passwordFile"` TLS *bool `json:"tls,omitempty"` ReadPreference *string `json:"readPreference,omitempty"` + AuthSource *string `json:"authSource,omitempty"` } type ConfigStorage struct { diff --git a/public/mongodb-kubernetes-multi-cluster.yaml b/public/mongodb-kubernetes-multi-cluster.yaml index a83643211..b0372a6fd 100644 --- a/public/mongodb-kubernetes-multi-cluster.yaml +++ b/public/mongodb-kubernetes-multi-cluster.yaml @@ -432,9 +432,9 @@ spec: value: "ubi8" # Community Env Vars End - name: MDB_SEARCH_COMMUNITY_REPO_URL - value: "268558157000.dkr.ecr.us-east-1.amazonaws.com/dev" + value: "268558157000.dkr.ecr.eu-west-1.amazonaws.com" - name: MDB_SEARCH_COMMUNITY_NAME - value: "mongodb-search-community" + value: "mongot/community" - name: MDB_SEARCH_COMMUNITY_VERSION value: "1.47.0" volumes: diff --git a/public/mongodb-kubernetes-openshift.yaml b/public/mongodb-kubernetes-openshift.yaml index cfe6359ca..da2a95730 100644 --- a/public/mongodb-kubernetes-openshift.yaml +++ b/public/mongodb-kubernetes-openshift.yaml @@ -696,10 +696,10 @@ spec: - name: RELATED_IMAGE_MONGODB_IMAGE_8_0_0_ubi9 value: "quay.io/mongodb/mongodb-enterprise-server:8.0.0-ubi9" - name: RELATED_IMAGE_MDB_SEARCH_IMAGE_1_47_0 - value: "268558157000.dkr.ecr.us-east-1.amazonaws.com/dev/mongodb-search-community:1.47.0" + value: "268558157000.dkr.ecr.eu-west-1.amazonaws.com/mongot/community:1.47.0" - name: MDB_SEARCH_COMMUNITY_REPO_URL - value: "268558157000.dkr.ecr.us-east-1.amazonaws.com/dev" + value: "268558157000.dkr.ecr.eu-west-1.amazonaws.com" - name: MDB_SEARCH_COMMUNITY_NAME - value: "mongodb-search-community" + value: "mongot/community" - name: MDB_SEARCH_COMMUNITY_VERSION value: "1.47.0" diff --git a/public/mongodb-kubernetes.yaml b/public/mongodb-kubernetes.yaml index 16406a9da..2875cdd81 100644 --- a/public/mongodb-kubernetes.yaml +++ b/public/mongodb-kubernetes.yaml @@ -428,8 +428,8 @@ spec: value: "ubi8" # Community Env Vars End - name: MDB_SEARCH_COMMUNITY_REPO_URL - value: "268558157000.dkr.ecr.us-east-1.amazonaws.com/dev" + value: "268558157000.dkr.ecr.eu-west-1.amazonaws.com" - name: MDB_SEARCH_COMMUNITY_NAME - value: "mongodb-search-community" + value: "mongot/community" - name: MDB_SEARCH_COMMUNITY_VERSION value: "1.47.0" diff --git a/scripts/dev/contexts/e2e_mdb_community b/scripts/dev/contexts/e2e_mdb_community index 71429c016..bc2f0f7f0 100644 --- a/scripts/dev/contexts/e2e_mdb_community +++ b/scripts/dev/contexts/e2e_mdb_community @@ -13,8 +13,9 @@ export OM_EXTERNALLY_CONFIGURED="true" # Temporary development images built from mongot master #export MDB_SEARCH_COMMUNITY_VERSION="776d43523d185b6b234289e17c191712a3e6569b" # master -export MDB_SEARCH_COMMUNITY_VERSION="d6884ae132aab30497af55dbaff05e8274e9775f" # Local->Admin +#export MDB_SEARCH_COMMUNITY_VERSION="d6884ae132aab30497af55dbaff05e8274e9775f" # Local->Admin #export MDB_SEARCH_COMMUNITY_VERSION="ad8acf5c3a045d6e0306ad67d61fcb5be40f57ae" # hardcoded mdbc-rs replicaset name #export MDB_SEARCH_COMMUNITY_VERSION="b9b80915f5571bfa5fc2aa70acb20d784e68d79b" # hardcoded Local->Admin, handled replicaSetName in config +export MDB_SEARCH_COMMUNITY_VERSION="fbd60fb055dd500058edcb45677ea85d19421f47" # Nolan's fixes export MDB_SEARCH_COMMUNITY_NAME="mongot/community" export MDB_SEARCH_COMMUNITY_REPO_URL="268558157000.dkr.ecr.eu-west-1.amazonaws.com" From 6e273f4c63599188a305777366ad3cf7a5f48c43 Mon Sep 17 00:00:00 2001 From: Anand <13899132+anandsyncs@users.noreply.github.com> Date: Mon, 28 Jul 2025 10:35:25 +0200 Subject: [PATCH 06/13] CLOUDP-321067: update health check (#268) # Summary This pull request introduces health check functionality for the `mongot` service, updates readiness and liveness probes, and removes a test decorator for local testing. The changes enhance the monitoring and reliability of the `mongot` service and simplify the test suite. ### Enhancements to `mongot` service health checks: * Added a new constant `MongotDefautHealthCheckPort` set to port `8080` in `mongodbsearch_types.go`. A helper method `GetMongotHealthCheckPort` was also introduced to retrieve this value. * Updated the `buildSearchHeadlessService` function to include a new service port for health checks, using the `GetMongotHealthCheckPort` method. * Modified the `createMongotConfig` function to dynamically set the health check address using the new health check port. ### Readiness and liveness probe improvements: * Introduced constants for probe paths (`SearchLivenessProbePath` and `SearchReadinessProbePath`) in `search_construction.go`. * Replaced the existing readiness probe configuration with new methods `mongotLivenessProbe` and `mongotReadinessProbe`. These methods configure HTTP-based probes using the health check port and paths. ### Test suite simplifications: * Removed the `@skip_if_local` decorator from several `test_om_connectivity` test cases in `om_appdb_scale_up_down.py`, enabling them to run in all environments. --- api/v1/search/mongodbsearch_types.go | 5 ++ .../mongodbsearch_reconcile_helper.go | 9 +++- .../search_controller/search_construction.go | 47 ++++++++++++++++--- .../om_appdb_scale_up_down.py | 1 + 4 files changed, 55 insertions(+), 7 deletions(-) diff --git a/api/v1/search/mongodbsearch_types.go b/api/v1/search/mongodbsearch_types.go index d0648b63b..c981b256c 100644 --- a/api/v1/search/mongodbsearch_types.go +++ b/api/v1/search/mongodbsearch_types.go @@ -18,6 +18,7 @@ import ( const ( MongotDefaultPort = 27027 MongotDefaultMetricsPort = 9946 + MongotDefautHealthCheckPort = 8080 MongotDefaultSyncSourceUsername = "mongot-user" ) @@ -189,3 +190,7 @@ func (s *MongoDBSearch) TLSSecretNamespacedName() types.NamespacedName { func (s *MongoDBSearch) TLSOperatorSecretNamespacedName() types.NamespacedName { return types.NamespacedName{Name: s.Name + "-search-certificate-key", Namespace: s.Namespace} } + +func (s *MongoDBSearch) GetMongotHealthCheckPort() int32 { + return MongotDefautHealthCheckPort +} diff --git a/controllers/search_controller/mongodbsearch_reconcile_helper.go b/controllers/search_controller/mongodbsearch_reconcile_helper.go index fa7bd677c..2d1d434b6 100644 --- a/controllers/search_controller/mongodbsearch_reconcile_helper.go +++ b/controllers/search_controller/mongodbsearch_reconcile_helper.go @@ -313,6 +313,13 @@ func buildSearchHeadlessService(search *searchv1.MongoDBSearch) corev1.Service { TargetPort: intstr.FromInt32(search.GetMongotMetricsPort()), }) + serviceBuilder.AddPort(&corev1.ServicePort{ + Name: "healthcheck", + Protocol: corev1.ProtocolTCP, + Port: search.GetMongotHealthCheckPort(), + TargetPort: intstr.FromInt32(search.GetMongotHealthCheckPort()), + }) + return serviceBuilder.Build() } @@ -350,7 +357,7 @@ func createMongotConfig(search *searchv1.MongoDBSearch, db SearchSourceDBResourc Address: fmt.Sprintf("localhost:%d", search.GetMongotMetricsPort()), } config.HealthCheck = mongot.ConfigHealthCheck{ - Address: "0.0.0.0:8080", + Address: fmt.Sprintf("localhost:%d", search.GetMongotHealthCheckPort()), } config.Logging = mongot.ConfigLogging{ Verbosity: "TRACE", diff --git a/controllers/search_controller/search_construction.go b/controllers/search_controller/search_construction.go index ee42e4aa4..6aae8e982 100644 --- a/controllers/search_controller/search_construction.go +++ b/controllers/search_controller/search_construction.go @@ -19,7 +19,9 @@ import ( ) const ( - MongotContainerName = "mongot" + MongotContainerName = "mongot" + SearchLivenessProbePath = "/health" + SearchReadinessProbePath = "/health" // Todo: Update this when search GA is available ) // SearchSourceDBResource is an object wrapping a MongoDBCommunity object @@ -183,11 +185,8 @@ func mongodbSearchContainer(mdbSearch *searchv1.MongoDBSearch, volumeMounts []co container.WithName(MongotContainerName), container.WithImage(searchImage), container.WithImagePullPolicy(corev1.PullAlways), - container.WithReadinessProbe(probes.Apply( - probes.WithTCPSocket("", intstr.FromInt32(mdbSearch.GetMongotPort())), - probes.WithInitialDelaySeconds(20), - probes.WithPeriodSeconds(10), - )), + container.WithLivenessProbe(mongotLivenessProbe(mdbSearch)), + container.WithReadinessProbe(mongotReadinessProbe(mdbSearch)), container.WithResourceRequirements(createSearchResourceRequirements(mdbSearch.Spec.ResourceRequirements)), container.WithVolumeMounts(volumeMounts), container.WithCommand([]string{"sh"}), @@ -209,6 +208,42 @@ chmod 0600 /tmp/sourceUserPassword ) } +func mongotLivenessProbe(search *searchv1.MongoDBSearch) func(*corev1.Probe) { + return probes.Apply( + probes.WithHandler(corev1.ProbeHandler{ + HTTPGet: &corev1.HTTPGetAction{ + Scheme: corev1.URISchemeHTTP, + Port: intstr.FromInt32(search.GetMongotHealthCheckPort()), + Path: SearchLivenessProbePath, + }, + }), + probes.WithInitialDelaySeconds(10), + probes.WithPeriodSeconds(10), + probes.WithTimeoutSeconds(5), + probes.WithSuccessThreshold(1), + probes.WithFailureThreshold(10), + ) +} + +// mongotReadinessProbe just uses the endpoint intended for liveness checks; +// readiness check endpoint may be available in search GA. +func mongotReadinessProbe(search *searchv1.MongoDBSearch) func(*corev1.Probe) { + return probes.Apply( + probes.WithHandler(corev1.ProbeHandler{ + HTTPGet: &corev1.HTTPGetAction{ + Scheme: corev1.URISchemeHTTP, + Port: intstr.FromInt32(search.GetMongotHealthCheckPort()), + Path: SearchReadinessProbePath, + }, + }), + probes.WithInitialDelaySeconds(20), + probes.WithPeriodSeconds(10), + probes.WithTimeoutSeconds(5), + probes.WithSuccessThreshold(1), + probes.WithFailureThreshold(3), + ) +} + func createSearchResourceRequirements(requirements *corev1.ResourceRequirements) corev1.ResourceRequirements { if requirements != nil { return *requirements diff --git a/docker/mongodb-kubernetes-tests/tests/opsmanager/withMonitoredAppDB/om_appdb_scale_up_down.py b/docker/mongodb-kubernetes-tests/tests/opsmanager/withMonitoredAppDB/om_appdb_scale_up_down.py index 9a0f64704..fd14ffa1c 100644 --- a/docker/mongodb-kubernetes-tests/tests/opsmanager/withMonitoredAppDB/om_appdb_scale_up_down.py +++ b/docker/mongodb-kubernetes-tests/tests/opsmanager/withMonitoredAppDB/om_appdb_scale_up_down.py @@ -8,6 +8,7 @@ from pytest import fixture from tests.conftest import is_multi_cluster + # Important - you need to ensure that OM and Appdb images are build and pushed into your current docker registry before # running tests locally - use "make om-image" and "make appdb" to do this From 22d83f8d7eb8d7c35872b6fce85ff1d0606925d3 Mon Sep 17 00:00:00 2001 From: Anand <13899132+anandsyncs@users.noreply.github.com> Date: Mon, 28 Jul 2025 10:37:11 +0200 Subject: [PATCH 07/13] CLOUDP-321075: fail to reconcile mongo db search for version 1.47.0 (#244) # Summary This pull request introduces validation for unsupported MongoDBSearch image versions and adds a corresponding test case. The key changes include implementing a new validation method, updating the reconciliation logic, and adding constants for unsupported versions. ### Validation for unsupported MongoDBSearch image versions: * **New validation method**: Added `ValidateSearchImageVersion` to `MongoDBSearchReconcileHelper` to check if the specified or container image version matches an unsupported version (`1.47.0`). If so, it returns an error. * **Integration into reconciliation workflow**: Updated the `reconcile` method to invoke `ValidateSearchImageVersion` before proceeding with other validations. * **Constants for unsupported version**: Introduced `unsupportedSearchVersion` and `unsupportedSearchVersionErrorFmt` constants to centralize the unsupported version logic and error formatting. ### Test case for validation: * **New test**: Added `TestMongoDBSearchReconcile_InvalidSearchImageVersion` to validate the error handling for unsupported MongoDBSearch versions. This ensures the reconciliation fails with the appropriate error message. ## Proof of Work Test pass ## Checklist - [x] Have you linked a jira ticket and/or is the ticket in the title? - [x] Have you checked whether your jira ticket required DOCSP changes? - [x] Have you checked for release_note changes? --- .../operator/mongodbsearch_controller_test.go | 71 ++++++++++++++++++- .../mongodbsearch_reconcile_helper.go | 49 ++++++++++++- 2 files changed, 116 insertions(+), 4 deletions(-) diff --git a/controllers/operator/mongodbsearch_controller_test.go b/controllers/operator/mongodbsearch_controller_test.go index 7e498a8df..d1d3a5fe7 100644 --- a/controllers/operator/mongodbsearch_controller_test.go +++ b/controllers/operator/mongodbsearch_controller_test.go @@ -27,6 +27,7 @@ import ( "github.com/mongodb/mongodb-kubernetes/controllers/operator/workflow" "github.com/mongodb/mongodb-kubernetes/controllers/search_controller" mdbcv1 "github.com/mongodb/mongodb-kubernetes/mongodb-community-operator/api/v1" + "github.com/mongodb/mongodb-kubernetes/mongodb-community-operator/api/v1/common" "github.com/mongodb/mongodb-kubernetes/mongodb-community-operator/pkg/mongot" ) @@ -52,8 +53,9 @@ func newMongoDBSearch(name, namespace, mdbcName string) *searchv1.MongoDBSearch } } -func newSearchReconciler( +func newSearchReconcilerWithOperatorConfig( mdbc *mdbcv1.MongoDBCommunity, + operatorConfig search_controller.OperatorSearchConfig, searches ...*searchv1.MongoDBSearch, ) (*MongoDBSearchReconciler, client.Client) { builder := mock.NewEmptyFakeClientBuilder() @@ -70,7 +72,15 @@ func newSearchReconciler( } fakeClient := builder.Build() - return newMongoDBSearchReconciler(fakeClient, search_controller.OperatorSearchConfig{}), fakeClient + + return newMongoDBSearchReconciler(fakeClient, operatorConfig), fakeClient +} + +func newSearchReconciler( + mdbc *mdbcv1.MongoDBCommunity, + searches ...*searchv1.MongoDBSearch, +) (*MongoDBSearchReconciler, client.Client) { + return newSearchReconcilerWithOperatorConfig(mdbc, search_controller.OperatorSearchConfig{}, searches...) } func buildExpectedMongotConfig(search *searchv1.MongoDBSearch, mdbc *mdbcv1.MongoDBCommunity) mongot.Config { @@ -229,3 +239,60 @@ func TestMongoDBSearchReconcile_MultipleSearchResources(t *testing.T) { checkSearchReconcileFailed(ctx, t, reconciler, c, search1, "multiple MongoDBSearch") } + +func TestMongoDBSearchReconcile_InvalidSearchImageVersion(t *testing.T) { + ctx := context.Background() + expectedMsg := "MongoDBSearch version 1.47.0 is not supported because of breaking changes. The operator will ignore this resource: it will not reconcile or reconfigure the workload. Existing deployments will continue to run, but cannot be managed by the operator. To regain operator management, you must delete and recreate the MongoDBSearch resource." + + tests := []struct { + name string + specVersion string + operatorVersion string + statefulSetConfig *common.StatefulSetConfiguration + }{ + { + name: "unsupported version in Spec.Version", + specVersion: "1.47.0", + }, + { + name: "unsupported version in operator config", + operatorVersion: "1.47.0", + }, + { + name: "unsupported version in StatefulSetConfiguration", + statefulSetConfig: &common.StatefulSetConfiguration{ + SpecWrapper: common.StatefulSetSpecWrapper{ + Spec: appsv1.StatefulSetSpec{ + Template: corev1.PodTemplateSpec{ + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + { + Name: search_controller.MongotContainerName, + Image: "testrepo/mongot:1.47.0", + }, + }, + }, + }, + }, + }, + }, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + search := newMongoDBSearch("search", mock.TestNamespace, "mdb") + mdbc := newMongoDBCommunity("mdb", mock.TestNamespace) + + search.Spec.Version = tc.specVersion + search.Spec.StatefulSetConfiguration = tc.statefulSetConfig + + operatorConfig := search_controller.OperatorSearchConfig{ + SearchVersion: tc.operatorVersion, + } + reconciler, _ := newSearchReconcilerWithOperatorConfig(mdbc, operatorConfig, search) + + checkSearchReconcileFailed(ctx, t, reconciler, reconciler.kubeClient, search, expectedMsg) + }) + } +} diff --git a/controllers/search_controller/mongodbsearch_reconcile_helper.go b/controllers/search_controller/mongodbsearch_reconcile_helper.go index 2d1d434b6..e9c3f29c6 100644 --- a/controllers/search_controller/mongodbsearch_reconcile_helper.go +++ b/controllers/search_controller/mongodbsearch_reconcile_helper.go @@ -35,7 +35,12 @@ import ( ) const ( - MongoDBSearchIndexFieldName = "mdbsearch-for-mongodbresourceref-index" + MongoDBSearchIndexFieldName = "mdbsearch-for-mongodbresourceref-index" + unsupportedSearchVersion = "1.47.0" + unsupportedSearchVersionErrorFmt = "MongoDBSearch version %s is not supported because of breaking changes. " + + "The operator will ignore this resource: it will not reconcile or reconfigure the workload. " + + "Existing deployments will continue to run, but cannot be managed by the operator. " + + "To regain operator management, you must delete and recreate the MongoDBSearch resource." ) type OperatorSearchConfig struct { @@ -81,6 +86,10 @@ func (r *MongoDBSearchReconcileHelper) reconcile(ctx context.Context, log *zap.S return workflow.Failed(err) } + if err := r.ValidateSearchImageVersion(); err != nil { + return workflow.Failed(err) + } + if err := r.ValidateSingleMongoDBSearchForSearchSource(ctx); err != nil { return workflow.Failed(err) } @@ -410,8 +419,44 @@ func (r *MongoDBSearchReconcileHelper) ValidateSingleMongoDBSearchForSearchSourc for i, search := range searchList.Items { resourceNames[i] = search.Name } - return xerrors.Errorf("Found multiple MongoDBSearch resources for search source '%s': %s", r.db.Name(), strings.Join(resourceNames, ", ")) + return xerrors.Errorf( + "Found multiple MongoDBSearch resources for search source '%s': %s", r.db.Name(), + strings.Join(resourceNames, ", "), + ) + } + + return nil +} + +func (r *MongoDBSearchReconcileHelper) ValidateSearchImageVersion() error { + version := r.getMongotImage() + + if strings.Contains(version, unsupportedSearchVersion) { + return xerrors.Errorf(unsupportedSearchVersionErrorFmt, unsupportedSearchVersion) } return nil } + +func (r *MongoDBSearchReconcileHelper) getMongotImage() string { + version := strings.TrimSpace(r.mdbSearch.Spec.Version) + if version != "" { + return version + } + + if r.operatorSearchConfig.SearchVersion != "" { + return r.operatorSearchConfig.SearchVersion + } + + if r.mdbSearch.Spec.StatefulSetConfiguration == nil { + return "" + } + + for _, container := range r.mdbSearch.Spec.StatefulSetConfiguration.SpecWrapper.Spec.Template.Spec.Containers { + if container.Name == MongotContainerName { + return container.Image + } + } + + return "" +} From 6e1171337279d54d99b8e38bd96191b3ca91eea5 Mon Sep 17 00:00:00 2001 From: Yavor Georgiev Date: Mon, 28 Jul 2025 16:09:31 +0200 Subject: [PATCH 08/13] Apply Search overrides in e2e operator config map (#295) # Summary This makes sure that e2e tests respect the `MDB_SEARCH_COMMUNITY_*` variable overrides in a context. ## Proof of Work [Passing e2e tests](https://spruce.mongodb.com/version/68872f20797f2500073406ff/tasks?page=0&sorts=STATUS%3AASC%3BBASE_STATUS%3ADESC&variant=%5Ee2e_mdb_community%24). ## Checklist - [ ] Have you linked a jira ticket and/or is the ticket in the title? - [ ] Have you checked whether your jira ticket required DOCSP changes? - [ ] Have you checked for release_note changes? --- api/v1/search/mongodbsearch_types.go | 2 +- .../operator/mongodbsearch_controller_test.go | 18 ++++-------------- .../mongodbsearch_reconcile_helper.go | 4 ++-- .../mongodbsearch_reconcile_helper_test.go | 7 +++---- .../om_appdb_scale_up_down.py | 1 - scripts/funcs/operator_deployment | 3 +++ 6 files changed, 13 insertions(+), 22 deletions(-) diff --git a/api/v1/search/mongodbsearch_types.go b/api/v1/search/mongodbsearch_types.go index c981b256c..f07f25053 100644 --- a/api/v1/search/mongodbsearch_types.go +++ b/api/v1/search/mongodbsearch_types.go @@ -18,7 +18,7 @@ import ( const ( MongotDefaultPort = 27027 MongotDefaultMetricsPort = 9946 - MongotDefautHealthCheckPort = 8080 + MongotDefautHealthCheckPort = 8080 MongotDefaultSyncSourceUsername = "mongot-user" ) diff --git a/controllers/operator/mongodbsearch_controller_test.go b/controllers/operator/mongodbsearch_controller_test.go index d1d3a5fe7..bc9f12c91 100644 --- a/controllers/operator/mongodbsearch_controller_test.go +++ b/controllers/operator/mongodbsearch_controller_test.go @@ -37,7 +37,7 @@ func newMongoDBCommunity(name, namespace string) *mdbcv1.MongoDBCommunity { Spec: mdbcv1.MongoDBCommunitySpec{ Type: mdbcv1.ReplicaSet, Members: 1, - Version: "8.0", + Version: "8.0.10", }, } } @@ -109,15 +109,15 @@ func buildExpectedMongotConfig(search *searchv1.MongoDBSearch, mdbc *mdbcv1.Mong Mode: "keyfile", KeyFile: "/tmp/keyfile", }, - TLS: mongot.ConfigTLS{Mode: "disabled"}, + TLS: mongot.ConfigTLS{Mode: mongot.ConfigTLSModeDisabled}, }, }, Metrics: mongot.ConfigMetrics{ Enabled: true, - Address: fmt.Sprintf("localhost:%d", searchv1.MongotDefaultMetricsPort), + Address: fmt.Sprintf("0.0.0.0:%d", search.GetMongotMetricsPort()), }, HealthCheck: mongot.ConfigHealthCheck{ - Address: "0.0.0.0:8080", + Address: fmt.Sprintf("0.0.0.0:%d", search.GetMongotHealthCheckPort()), }, Logging: mongot.ConfigLogging{ Verbosity: "TRACE", @@ -220,16 +220,6 @@ func TestMongoDBSearchReconcile_InvalidVersion(t *testing.T) { checkSearchReconcileFailed(ctx, t, reconciler, c, search, "MongoDB version") } -func TestMongoDBSearchReconcile_TLSNotSupported(t *testing.T) { - ctx := context.Background() - search := newMongoDBSearch("search", mock.TestNamespace, "mdb") - mdbc := newMongoDBCommunity("mdb", mock.TestNamespace) - mdbc.Spec.Security.TLS.Enabled = true - reconciler, c := newSearchReconciler(mdbc, search) - - checkSearchReconcileFailed(ctx, t, reconciler, c, search, "TLS-enabled") -} - func TestMongoDBSearchReconcile_MultipleSearchResources(t *testing.T) { ctx := context.Background() search1 := newMongoDBSearch("search1", mock.TestNamespace, "mdb") diff --git a/controllers/search_controller/mongodbsearch_reconcile_helper.go b/controllers/search_controller/mongodbsearch_reconcile_helper.go index e9c3f29c6..08a264853 100644 --- a/controllers/search_controller/mongodbsearch_reconcile_helper.go +++ b/controllers/search_controller/mongodbsearch_reconcile_helper.go @@ -363,10 +363,10 @@ func createMongotConfig(search *searchv1.MongoDBSearch, db SearchSourceDBResourc } config.Metrics = mongot.ConfigMetrics{ Enabled: true, - Address: fmt.Sprintf("localhost:%d", search.GetMongotMetricsPort()), + Address: fmt.Sprintf("0.0.0.0:%d", search.GetMongotMetricsPort()), } config.HealthCheck = mongot.ConfigHealthCheck{ - Address: fmt.Sprintf("localhost:%d", search.GetMongotHealthCheckPort()), + Address: fmt.Sprintf("0.0.0.0:%d", search.GetMongotHealthCheckPort()), } config.Logging = mongot.ConfigLogging{ Verbosity: "TRACE", diff --git a/controllers/search_controller/mongodbsearch_reconcile_helper_test.go b/controllers/search_controller/mongodbsearch_reconcile_helper_test.go index fe03ceb70..f70ec9a03 100644 --- a/controllers/search_controller/mongodbsearch_reconcile_helper_test.go +++ b/controllers/search_controller/mongodbsearch_reconcile_helper_test.go @@ -34,14 +34,14 @@ func TestMongoDBSearchReconcileHelper_ValidateSearchSource(t *testing.T) { Version: "4.4.0", }, }, - expectedError: "MongoDB version must be 8.0 or higher", + expectedError: "MongoDB version must be 8.0.10 or higher", }, { name: "Valid version", mdbc: mdbcv1.MongoDBCommunity{ ObjectMeta: mdbcMeta, Spec: mdbcv1.MongoDBCommunitySpec{ - Version: "8.0", + Version: "8.0.10", }, }, }, @@ -50,7 +50,7 @@ func TestMongoDBSearchReconcileHelper_ValidateSearchSource(t *testing.T) { mdbc: mdbcv1.MongoDBCommunity{ ObjectMeta: mdbcMeta, Spec: mdbcv1.MongoDBCommunitySpec{ - Version: "8.0", + Version: "8.0.10", Security: mdbcv1.Security{ TLS: mdbcv1.TLS{ Enabled: true, @@ -58,7 +58,6 @@ func TestMongoDBSearchReconcileHelper_ValidateSearchSource(t *testing.T) { }, }, }, - expectedError: "MongoDBSearch does not support TLS-enabled sources", }, } diff --git a/docker/mongodb-kubernetes-tests/tests/opsmanager/withMonitoredAppDB/om_appdb_scale_up_down.py b/docker/mongodb-kubernetes-tests/tests/opsmanager/withMonitoredAppDB/om_appdb_scale_up_down.py index fd14ffa1c..9a0f64704 100644 --- a/docker/mongodb-kubernetes-tests/tests/opsmanager/withMonitoredAppDB/om_appdb_scale_up_down.py +++ b/docker/mongodb-kubernetes-tests/tests/opsmanager/withMonitoredAppDB/om_appdb_scale_up_down.py @@ -8,7 +8,6 @@ from pytest import fixture from tests.conftest import is_multi_cluster - # Important - you need to ensure that OM and Appdb images are build and pushed into your current docker registry before # running tests locally - use "make om-image" and "make appdb" to do this diff --git a/scripts/funcs/operator_deployment b/scripts/funcs/operator_deployment index 36c1f4764..88a0eed0d 100644 --- a/scripts/funcs/operator_deployment +++ b/scripts/funcs/operator_deployment @@ -34,6 +34,9 @@ get_operator_helm_values() { "operator.telemetry.send.enabled=${MDB_OPERATOR_TELEMETRY_SEND_ENABLED:-false}" # lets collect and save in the configmap as frequently as we can "operator.telemetry.collection.frequency=${MDB_OPERATOR_TELEMETRY_COLLECTION_FREQUENCY:-1m}" + "search.community.version=${MDB_SEARCH_COMMUNITY_VERSION}" + "search.community.name=${MDB_SEARCH_COMMUNITY_NAME}" + "search.community.repo=${MDB_SEARCH_COMMUNITY_REPO_URL}" ) if [[ "${MDB_OPERATOR_TELEMETRY_INSTALL_CLUSTER_ROLE_INSTALLATION:-}" != "" ]]; then From 401a5bb1a9e2173a76adac8dd06145afd2579fb5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Sierant?= Date: Tue, 29 Jul 2025 17:04:49 +0200 Subject: [PATCH 09/13] Updated code snippets (#296) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # Based on PR #229 ## Chain of upstream PRs as of 2025-07-28 * PR #229: `master` ← `search/public-preview` * **PR #296 (THIS ONE)**: `search/public-preview` ← `lsierant/search-snippets-update` # Summary ## Proof of Work ## Checklist - [ ] Have you linked a jira ticket and/or is the ticket in the title? - [ ] Have you checked whether your jira ticket required DOCSP changes? - [ ] Have you checked for release_note changes? --------- Co-authored-by: Yavor Georgiev Co-authored-by: Yavor Georgiev --- api/v1/search/mongodbsearch_types.go | 9 +- .../operator/mongodbsearch_controller_test.go | 2 +- .../edit_mms_configuration.go | 4 +- .../community-replicaset-sample-mflix.yaml | 69 +++--- .../tests/search/search_community_basic.py | 19 +- .../tests/search/search_community_tls.py | 4 +- docs/community-search/quick-start/README.md | 211 +++++++----------- .../community-search/quick-start/README.md.j2 | 60 ++--- .../code_snippets/0100_install_operator.sh | 0 ...0_configure_community_search_pullsecret.sh | 25 --- ...0210_verify_community_search_pullsecret.sh | 10 - ...5_create_mongodb_community_user_secrets.sh | 11 +- .../0310_create_mongodb_community_resource.sh | 63 ++++-- .../0315_wait_for_community_resource.sh | 0 .../0320_create_mongodb_search_resource.sh | 0 .../0325_wait_for_search_resource.sh | 0 .../0330_wait_for_community_resource.sh | 0 .../code_snippets/0335_show_running_pods.sh | 0 .../0410_run_mongodb_tools_pod.sh | 2 +- .../0420_import_movies_mflix_database.sh | 3 +- .../code_snippets/0430_create_search_index.sh | 2 +- .../0440_wait_for_search_index_ready.sh | 21 +- .../0450_execute_search_query.sh | 2 +- .../code_snippets/090_helm_add_mogodb_repo.sh | 0 .../code_snippets/9010_delete_namespace.sh | 2 +- .../community_search_snippets_test.sh.run.log | 16 -- .../quick-start/env_variables.sh | 13 +- .../quick-start/env_variables_e2e_private.sh | 5 +- .../env_variables_e2e_private_dev.sh | 33 +++ .../output/0100_install_operator.out | 202 +++++++++++------ ..._configure_community_search_pullsecret.out | 18 -- ...210_verify_community_search_pullsecret.out | 3 - .../output/0335_show_running_pods.out | 18 +- .../0440_wait_for_search_index_ready.out | 5 +- .../output/090_helm_add_mogodb_repo.out | 2 +- docs/community-search/quick-start/test.sh | 4 +- ...ask_kind_community_search_snippets_test.sh | 4 +- .../dev/contexts/private_kind_code_snippets | 1 + scripts/dev/contexts/root-context | 7 +- scripts/dev/update_docs_snippets.sh | 46 ++-- scripts/funcs/operator_deployment | 4 +- 41 files changed, 441 insertions(+), 459 deletions(-) mode change 100644 => 100755 docs/community-search/quick-start/code_snippets/0100_install_operator.sh delete mode 100644 docs/community-search/quick-start/code_snippets/0200_configure_community_search_pullsecret.sh delete mode 100644 docs/community-search/quick-start/code_snippets/0210_verify_community_search_pullsecret.sh mode change 100644 => 100755 docs/community-search/quick-start/code_snippets/0305_create_mongodb_community_user_secrets.sh mode change 100644 => 100755 docs/community-search/quick-start/code_snippets/0310_create_mongodb_community_resource.sh mode change 100644 => 100755 docs/community-search/quick-start/code_snippets/0315_wait_for_community_resource.sh mode change 100644 => 100755 docs/community-search/quick-start/code_snippets/0320_create_mongodb_search_resource.sh mode change 100644 => 100755 docs/community-search/quick-start/code_snippets/0325_wait_for_search_resource.sh mode change 100644 => 100755 docs/community-search/quick-start/code_snippets/0330_wait_for_community_resource.sh mode change 100644 => 100755 docs/community-search/quick-start/code_snippets/0335_show_running_pods.sh mode change 100644 => 100755 docs/community-search/quick-start/code_snippets/0410_run_mongodb_tools_pod.sh mode change 100644 => 100755 docs/community-search/quick-start/code_snippets/0420_import_movies_mflix_database.sh mode change 100644 => 100755 docs/community-search/quick-start/code_snippets/0430_create_search_index.sh mode change 100644 => 100755 docs/community-search/quick-start/code_snippets/0440_wait_for_search_index_ready.sh mode change 100644 => 100755 docs/community-search/quick-start/code_snippets/0450_execute_search_query.sh mode change 100644 => 100755 docs/community-search/quick-start/code_snippets/090_helm_add_mogodb_repo.sh mode change 100644 => 100755 docs/community-search/quick-start/code_snippets/9010_delete_namespace.sh delete mode 100644 docs/community-search/quick-start/community_search_snippets_test.sh.run.log create mode 100644 docs/community-search/quick-start/env_variables_e2e_private_dev.sh delete mode 100644 docs/community-search/quick-start/output/0200_configure_community_search_pullsecret.out delete mode 100644 docs/community-search/quick-start/output/0210_verify_community_search_pullsecret.out diff --git a/api/v1/search/mongodbsearch_types.go b/api/v1/search/mongodbsearch_types.go index f07f25053..d77fc9b4c 100644 --- a/api/v1/search/mongodbsearch_types.go +++ b/api/v1/search/mongodbsearch_types.go @@ -19,7 +19,7 @@ const ( MongotDefaultPort = 27027 MongotDefaultMetricsPort = 9946 MongotDefautHealthCheckPort = 8080 - MongotDefaultSyncSourceUsername = "mongot-user" + MongotDefaultSyncSourceUsername = "search-sync-source" ) func init() { @@ -27,16 +27,23 @@ func init() { } type MongoDBSearchSpec struct { + // Optional version of MongoDB Search component (mongot). If not set, then the operator will set the most appropriate version of MongoDB Search. // +optional Version string `json:"version"` + // MongoDB database connection details from which MongoDB Search will synchronize data to build indexes. // +optional Source *MongoDBSource `json:"source"` + // StatefulSetSpec which the operator will apply to the MongoDB Search StatefulSet at the end of the reconcile loop. Use to provide necessary customizations, + // which aren't exposed as fields in the MongoDBSearch.spec. // +optional StatefulSetConfiguration *common.StatefulSetConfiguration `json:"statefulSet,omitempty"` + // Configure MongoDB Search's persistent volume. If not defined, the operator will request 10GB of storage. // +optional Persistence *common.Persistence `json:"persistence,omitempty"` + // Configure resource requests and limits for the MongoDB Search pods. // +optional ResourceRequirements *corev1.ResourceRequirements `json:"resourceRequirements,omitempty"` + // Configure security settings of the MongoDB Search server that MongoDB database is connecting to when performing search queries. // +optional Security Security `json:"security"` } diff --git a/controllers/operator/mongodbsearch_controller_test.go b/controllers/operator/mongodbsearch_controller_test.go index bc9f12c91..ba3885042 100644 --- a/controllers/operator/mongodbsearch_controller_test.go +++ b/controllers/operator/mongodbsearch_controller_test.go @@ -92,7 +92,7 @@ func buildExpectedMongotConfig(search *searchv1.MongoDBSearch, mdbc *mdbcv1.Mong SyncSource: mongot.ConfigSyncSource{ ReplicaSet: mongot.ConfigReplicaSet{ HostAndPort: hostAndPorts, - Username: "mongot-user", + Username: searchv1.MongotDefaultSyncSourceUsername, PasswordFile: "/tmp/sourceUserPassword", TLS: ptr.To(false), ReadPreference: ptr.To("secondaryPreferred"), diff --git a/docker/mongodb-kubernetes-init-ops-manager/mmsconfiguration/edit_mms_configuration.go b/docker/mongodb-kubernetes-init-ops-manager/mmsconfiguration/edit_mms_configuration.go index 793914d3a..ca7b9a3b0 100755 --- a/docker/mongodb-kubernetes-init-ops-manager/mmsconfiguration/edit_mms_configuration.go +++ b/docker/mongodb-kubernetes-init-ops-manager/mmsconfiguration/edit_mms_configuration.go @@ -145,7 +145,7 @@ func readLinesFromFile(name string) ([]string, error) { func writeLinesToFile(name string, lines []string) error { output := strings.Join(lines, lineBreak) - err := os.WriteFile(name, []byte(output), 0o775) + err := os.WriteFile(name, []byte(output), 0o644) if err != nil { return xerrors.Errorf("error writing to file %s: %w", name, err) } @@ -168,7 +168,7 @@ func appendLinesToFile(name string, lines string) error { func getOmPropertiesFromEnvVars() map[string]string { props := map[string]string{} - for _, pair := range os.Environ() { + for _, pair := range os.Environ() { // nolint:forbidigo if !strings.HasPrefix(pair, omPropertyPrefix) { continue } diff --git a/docker/mongodb-kubernetes-tests/tests/search/fixtures/community-replicaset-sample-mflix.yaml b/docker/mongodb-kubernetes-tests/tests/search/fixtures/community-replicaset-sample-mflix.yaml index 490ad3304..fe54d614d 100644 --- a/docker/mongodb-kubernetes-tests/tests/search/fixtures/community-replicaset-sample-mflix.yaml +++ b/docker/mongodb-kubernetes-tests/tests/search/fixtures/community-replicaset-sample-mflix.yaml @@ -4,59 +4,66 @@ kind: MongoDBCommunity metadata: name: mdbc-rs spec: - members: 3 + version: 8.0.10 type: ReplicaSet - version: "8.0.10" + members: 3 security: authentication: - modes: ["SCRAM-SHA-1"] + ignoreUnknownUsers: true + modes: + - SCRAM agent: logLevel: DEBUG + statefulSet: + spec: + template: + spec: + containers: + - name: mongod + resources: + limits: + cpu: "2" + memory: 2Gi + requests: + cpu: "1" + memory: 1Gi + - name: mongodb-agent + resources: + limits: + cpu: "1" + memory: 2Gi + requests: + cpu: "0.5" + memory: 1Gi users: + # admin user with root role - name: mdb-admin db: admin - passwordSecretRef: # a reference to the secret that will be used to generate the user's password + passwordSecretRef: # a reference to the secret containing user password name: mdb-admin-user-password + scramCredentialsSecretName: mdb-admin-user roles: - name: root db: admin - scramCredentialsSecretName: mdb-admin-user-scram + # user performing search queries - name: mdb-user db: admin - passwordSecretRef: # a reference to the secret that will be used to generate the user's password + passwordSecretRef: # a reference to the secret containing user password name: mdb-user-password + scramCredentialsSecretName: mdb-user-scram roles: - name: restore db: sample_mflix - name: readWrite db: sample_mflix - scramCredentialsSecretName: mdb-user-scram - - name: mongot-user + # user used by MongoDB Search to connect to MongoDB database to synchronize data from + # For MongoDB <8.2, the operator will be creating the searchCoordinator custom role automatically + # From MongoDB 8.2, searchCoordinator role will be a built-in role. + - name: search-sync-source db: admin passwordSecretRef: # a reference to the secret that will be used to generate the user's password - name: mdbc-rs-mongot-user-password + name: mdbc-rs-search-sync-source-password + scramCredentialsSecretName: mdbc-rs-search-sync-source roles: - name: searchCoordinator db: admin - scramCredentialsSecretName: mongot-user-scram - statefulSet: - spec: - template: - spec: - containers: - - name: mongod - resources: - limits: - cpu: "3" - memory: 5Gi - requests: - cpu: "2" - memory: 5Gi - - name: mongodb-agent - resources: - limits: - cpu: "3" - memory: 5Gi - requests: - cpu: "2" - memory: 5Gi diff --git a/docker/mongodb-kubernetes-tests/tests/search/search_community_basic.py b/docker/mongodb-kubernetes-tests/tests/search/search_community_basic.py index 3d4953913..97e9c1af7 100644 --- a/docker/mongodb-kubernetes-tests/tests/search/search_community_basic.py +++ b/docker/mongodb-kubernetes-tests/tests/search/search_community_basic.py @@ -15,8 +15,8 @@ ADMIN_USER_NAME = "mdb-admin-user" ADMIN_USER_PASSWORD = "mdb-admin-user-pass" -MONGOT_USER_NAME = "mongot-user" -MONGOT_USER_PASSWORD = "mongot-user-password" +MONGOT_USER_NAME = "search-sync-source" +MONGOT_USER_PASSWORD = "search-sync-source-user-password" USER_NAME = "mdb-user" USER_PASSWORD = "mdb-user-pass" @@ -32,8 +32,8 @@ def mdbc(namespace: str) -> MongoDBCommunity: namespace=namespace, ) - # if try_load(resource): - # return resource + if try_load(resource): + return resource return resource @@ -45,8 +45,8 @@ def mdbs(namespace: str) -> MongoDBSearch: namespace=namespace, ) - # if try_load(resource): - # return resource + if try_load(resource): + return resource return resource @@ -75,7 +75,7 @@ def test_create_database_resource(mdbc: MongoDBCommunity): @mark.e2e_search_community_basic -def test_create_search_resource(mdbs: MongoDBSearch, mdbc: MongoDBCommunity): +def test_create_search_resource(mdbs: MongoDBSearch): mdbs.update() mdbs.assert_reaches_phase(Phase.Running, timeout=300) @@ -102,11 +102,6 @@ def test_search_create_search_index(sample_movies_helper: SampleMoviesSearchHelp sample_movies_helper.create_search_index() -# @mark.e2e_search_community_basic -# def test_search_wait_for_search_indexes(sample_movies_helper: SampleMoviesSearchHelper): -# sample_movies_helper.wait_for_search_indexes() - - @mark.e2e_search_community_basic def test_search_assert_search_query(sample_movies_helper: SampleMoviesSearchHelper): sample_movies_helper.assert_search_query(retry_timeout=60) diff --git a/docker/mongodb-kubernetes-tests/tests/search/search_community_tls.py b/docker/mongodb-kubernetes-tests/tests/search/search_community_tls.py index aa0c53e79..44418b793 100644 --- a/docker/mongodb-kubernetes-tests/tests/search/search_community_tls.py +++ b/docker/mongodb-kubernetes-tests/tests/search/search_community_tls.py @@ -17,8 +17,8 @@ ADMIN_USER_NAME = "mdb-admin-user" ADMIN_USER_PASSWORD = "mdb-admin-user-pass" -MONGOT_USER_NAME = "mongot-user" -MONGOT_USER_PASSWORD = "mongot-user-password" +MONGOT_USER_NAME = "search-sync-source" +MONGOT_USER_PASSWORD = "search-sync-source-user-password" USER_NAME = "mdb-user" USER_PASSWORD = "mdb-user-pass" diff --git a/docs/community-search/quick-start/README.md b/docs/community-search/quick-start/README.md index 3934db1ab..7f6b6af99 100644 --- a/docs/community-search/quick-start/README.md +++ b/docs/community-search/quick-start/README.md @@ -4,11 +4,9 @@ This guide provides instructions for deploying MongoDB Community Edition along w ## Prerequisites -Community Search is currently in private preview, and access to the image requires a secret to pull the search container image from Quay.io. This secret is specified during the first step of the process below, and must be obtained from MongoDB when requesting access to the private preview. - Before you begin, ensure you have the following tools and configurations in place: -- **Kubernetes cluster**: A running Kubernetes cluster (e.g., Minikube, Kind, GKE, EKS, AKS). +- **Kubernetes cluster**: A running Kubernetes cluster (e.g., Minikube, Kind, GKE, EKS, AKS) with kubeconfig available locally. - **kubectl**: The Kubernetes command-line tool, configured to communicate with your cluster. - **Helm**: The package manager for Kubernetes, used here to install the MongoDB Kubernetes Operator. - **Bash 5.1+**: All shell commands in this guide are intended to be run in Bash. Scripts in this guide are automatically tested on Linux with Bash 5.1. @@ -29,15 +27,18 @@ Download or copy the content of `env_variables.sh`: # set it to the context name of the k8s cluster export K8S_CLUSTER_0_CONTEXT_NAME="" -# At the private preview stage the community search image is accessible only from a private repository. -# Please contact MongoDB Support to get access. -export PRIVATE_PREVIEW_IMAGE_PULLSECRET="<.dockerconfigjson>" - # the following namespace will be created if not exists export MDB_NAMESPACE="mongodb" +# minimum required MongoDB version for running MongoDB Search is 8.0.10 +export MDB_VERSION="8.0.10" + +# root admin user for convenience, not used here at all in this guide export MDB_ADMIN_USER_PASSWORD="admin-user-password-CHANGE-ME" -export MDB_SEARCH_USER_PASSWORD="search-user-password-CHANGE-ME" +# regular user performing restore and search queries on sample mflix database +export MDB_USER_PASSWORD="mdb-user-password-CHANGE-ME" +# user for MongoDB Search to connect to the replica set to synchronise data from +export MDB_SEARCH_SYNC_USER_PASSWORD="search-sync-user-password-CHANGE-ME" export OPERATOR_HELM_CHART="mongodb/mongodb-kubernetes" # comma-separated key=value pairs for additional parameters passed to the helm-chart installing the operator @@ -69,83 +70,39 @@ helm upgrade --install --debug --kube-context "${K8S_CLUSTER_0_CONTEXT_NAME}" \ --set "${OPERATOR_ADDITIONAL_HELM_VALUES:-"dummy=value"}" \ "${OPERATOR_HELM_CHART}" ``` -This command installs the operator in the `mongodb` namespace (creating it if it doesn't exist) and names the release `community-operator`. - -### 4. Configure Pull Secret for MongoDB Community Search - -To use MongoDB Search, your Kubernetes cluster needs to pull the necessary container images. This step creates a Kubernetes secret named `community-private-preview-pullsecret`. This secret stores the credentials required to access the image repository for MongoDB Search. The script then patches the `mongodb-kubernetes-database-pods` service account to include this pull secret, allowing pods managed by this service account to pull the required images. - -[code_snippets/0200_configure_community_search_pullsecret.sh](code_snippets/0200_configure_community_search_pullsecret.sh) -```shell copy -kubectl apply --context "${K8S_CLUSTER_0_CONTEXT_NAME}" -n "${MDB_NAMESPACE}" -f - < /tmp/mdb_script.js -mongosh --quiet "mongodb://search-user:${MDB_SEARCH_USER_PASSWORD}@mdbc-rs-0.mdbc-rs-svc.${MDB_NAMESPACE}.svc.cluster.local:27017/?replicaSet=mdbc-rs" < /tmp/mdb_script.js +mongosh --quiet "mongodb://mdb-user:${MDB_USER_PASSWORD}@mdbc-rs-0.mdbc-rs-svc.${MDB_NAMESPACE}.svc.cluster.local:27017/?replicaSet=mdbc-rs" < /tmp/mdb_script.js EOF )" -``` \ No newline at end of file +``` diff --git a/docs/community-search/quick-start/README.md.j2 b/docs/community-search/quick-start/README.md.j2 index 0e3125075..de32c88b1 100644 --- a/docs/community-search/quick-start/README.md.j2 +++ b/docs/community-search/quick-start/README.md.j2 @@ -4,11 +4,9 @@ This guide provides instructions for deploying MongoDB Community Edition along w ## Prerequisites -Community Search is currently in private preview, and access to the image requires a secret to pull the search container image from Quay.io. This secret is specified during the first step of the process below, and must be obtained from MongoDB when requesting access to the private preview. - Before you begin, ensure you have the following tools and configurations in place: -- **Kubernetes cluster**: A running Kubernetes cluster (e.g., Minikube, Kind, GKE, EKS, AKS). +- **Kubernetes cluster**: A running Kubernetes cluster (e.g., Minikube, Kind, GKE, EKS, AKS) with kubeconfig available locally. - **kubectl**: The Kubernetes command-line tool, configured to communicate with your cluster. - **Helm**: The package manager for Kubernetes, used here to install the MongoDB Kubernetes Operator. - **Bash 5.1+**: All shell commands in this guide are intended to be run in Bash. Scripts in this guide are automatically tested on Linux with Bash 5.1. @@ -47,33 +45,13 @@ Next, install the MongoDB Kubernetes Operator from the Helm repository you just ```shell copy {% include "code_snippets/0100_install_operator.sh" %} ``` -This command installs the operator in the `mongodb` namespace (creating it if it doesn't exist) and names the release `community-operator`. - -### 4. Configure Pull Secret for MongoDB Community Search - -To use MongoDB Search, your Kubernetes cluster needs to pull the necessary container images. This step creates a Kubernetes secret named `community-private-preview-pullsecret`. This secret stores the credentials required to access the image repository for MongoDB Search. The script then patches the `mongodb-kubernetes-database-pods` service account to include this pull secret, allowing pods managed by this service account to pull the required images. - -[code_snippets/0200_configure_community_search_pullsecret.sh](code_snippets/0200_configure_community_search_pullsecret.sh) -```shell copy -{% include "code_snippets/0200_configure_community_search_pullsecret.sh" %} -``` -This script creates a `community-private-preview-pullsecret` secret in your Kubernetes namespace and associates it with the service account used for MongoDB pods. - -### 5. Verify Pull Secret Configuration - -Confirm that the `community-private-preview-pullsecret` has been successfully added to the `mongodb-kubernetes-database-pods` service account. This ensures that Kubernetes can authenticate with the container registry when pulling images for MongoDB Search pods. - -[code_snippets/0210_verify_community_search_pullsecret.sh](code_snippets/0210_verify_community_search_pullsecret.sh) -```shell copy -{% include "code_snippets/0210_verify_community_search_pullsecret.sh" %} -``` -This command checks the `mongodb-kubernetes-database-pods` service account to confirm the presence of `community-private-preview-pullsecret`. +This command installs the operator in the `mongodb` namespace (creating it if it doesn't exist). ## Creating a MongoDB Community Search Deployment With the prerequisites and initial setup complete, you can now deploy MongoDB Community Edition and enable Search. -### 6. Create MongoDB User Secrets +### 4. Create MongoDB User Secrets MongoDB requires authentication for secure access. This step creates two Kubernetes secrets: `admin-user-password` and `search-user-password`. These secrets store the credentials for the MongoDB administrative user and a dedicated search user, respectively. These secrets will be mounted into the MongoDB pods. @@ -83,16 +61,19 @@ MongoDB requires authentication for secure access. This step creates two Kuberne ``` Ensure these secrets are created in the same namespace where you plan to deploy MongoDB. -### 7. Create MongoDB Community Resource +### 5. Create MongoDB Community Resource -Now, deploy MongoDB Community by creating a `MongoDBCommunity` custom resource named `mdbc-rs`. This resource definition instructs the MongoDB Kubernetes Operator to configure a MongoDB replica set with 3 members, running version 8.0.6. MongoDB Community Search is supported only from MongoDB Community Server version 8.0. It also defines CPU and memory resources for the `mongod` and `mongodb-agent` containers, and sets up two users (`admin-user` and `search-user`) with their respective roles and password secrets. User `search-user` will be used to restore, connect and perform search queries on the `sample_mflix` database. +Now, deploy MongoDB Community by creating a `MongoDBCommunity` custom resource named `mdbc-rs`. This resource definition instructs the MongoDB Kubernetes Operator to configure a MongoDB replica set with 3 members, running version 8.0.10. MongoDB Community Search is supported only from MongoDB Community Server version 8.0.10. It also defines CPU and memory resources for the `mongod` and `mongodb-agent` containers, and sets up three users: +* `mdb-user` - a regular user used to that will perform restore of `sample_mflix` database and execute search queries. +* `search-sync-source` - user that MongoDB Search is using to connect to MongoDB database in order to manage and build indexes. This user uses `searchCoordinator` role, which for MongoDB <8.2 is created automatically by the operator. +* `admin-user` and ``) with their respective roles and password secrets. User `search-user` will be used to restore, connect and perform search queries on the `sample_mflix` database. [code_snippets/0310_create_mongodb_community_resource.sh](code_snippets/0310_create_mongodb_community_resource.sh) ```yaml copy {% include "code_snippets/0310_create_mongodb_community_resource.sh" %} ``` -### 8. Wait for MongoDB Community Resource to be Ready +### 6. Wait for MongoDB Community Resource to be Ready After applying the `MongoDBCommunity` custom resource, the operator begins deploying the MongoDB nodes (pods). This step uses `kubectl wait` to pause execution until the `mdbc-rs` resource's status phase becomes `Running`, indicating that the MongoDB Community replica set is operational. @@ -101,13 +82,12 @@ After applying the `MongoDBCommunity` custom resource, the operator begins deplo {% include "code_snippets/0315_wait_for_community_resource.sh" %} ``` -### 9. Create MongoDB Search Resource +### 7. Create MongoDB Search Resource Once your MongoDB deployment is ready, enable Search capabilities by creating a `MongoDBSearch` custom resource, also named `mdbc-rs` to associate it with the MongoDB instance. This resource specifies the CPU and memory resource requirements for the search nodes. -Note: Private preview of MongoDB Community Search comes with some limitations, and it is not suitable for production use: -* TLS cannot be enabled in MongoDB Community deployment (MongoD communicates with MongoT with plain text). -* Only one node of search node is supported (load balancing not supported) +Note: Public Preview of MongoDB Community Search comes with some limitations, and it is not suitable for production use: +* Only one instance of the search node is supported (load balancing is not supported) [code_snippets/0320_create_mongodb_search_resource.sh](code_snippets/0320_create_mongodb_search_resource.sh) ```shell copy @@ -126,7 +106,7 @@ requests: memory: 2G ``` -### 10. Wait for Search Resource to be Ready +### 8. Wait for Search Resource to be Ready Similar to the MongoDB deployment, the Search deployment needs time to initialize. This step uses `kubectl wait` to pause until the `MongoDBSearch` resource `mdbc-rs` reports a `Running` status in its `.status.phase` field, indicating that the search nodes are operational and integrated. @@ -136,7 +116,7 @@ Similar to the MongoDB deployment, the Search deployment needs time to initializ ``` This command polls the status of the `MongoDBSearch` resource `mdbc-rs`. -### 11. Verify MongoDB Community Resource Status +### 9. Verify MongoDB Community Resource Status Double-check the status of your `MongoDBCommunity` resource to ensure it remains healthy and that the integration with the Search resource is reflected if applicable. @@ -146,7 +126,7 @@ Double-check the status of your `MongoDBCommunity` resource to ensure it remains ``` This provides a final confirmation that the core database is operational. -### 12. List Running Pods +### 10. List Running Pods View all the running pods in your namespace. You should see pods for the MongoDB replica set members, the MongoDB Kubernetes Operator, and the MongoDB Search nodes. @@ -159,7 +139,7 @@ View all the running pods in your namespace. You should see pods for the MongoDB Now that your MongoDB Community database with Search is deployed, you can start using its search capabilities. -### 13. Deploy MongoDB Tools Pod +### 11. Deploy MongoDB Tools Pod To interact with your MongoDB deployment, this step deploys a utility pod named `mongodb-tools-pod`. This pod runs a MongoDB Community Server image and is kept running with a `sleep infinity` command, allowing you to use `kubectl exec` to run MongoDB client tools like `mongosh` and `mongorestore` from within the Kubernetes cluster. Running steps in a pod inside the cluster simplifies connectivity to mongodb without neeeding to expose the database externally (provided steps directly connect to the *.cluster.local hostnames). @@ -168,7 +148,7 @@ To interact with your MongoDB deployment, this step deploys a utility pod named {% include "code_snippets/0410_run_mongodb_tools_pod.sh" %} ``` -### 14. Import Sample Data +### 12. Import Sample Data To test the search functionality, this step imports the `sample_mflix.movies` collection. It downloads the sample dataset and uses `mongorestore` to load the data into the `sample_mflix` database in your MongoDB deployment, connecting as the `search-user`. @@ -178,7 +158,7 @@ To test the search functionality, this step imports the `sample_mflix.movies` co ``` This command uses `mongorestore` from the `mongodb-tools-pod` to load data from the downloaded `sample_mflix.archive` file. -### 15. Create Search Index +### 13. Create Search Index Before performing search queries, create a search index. This step uses `kubectl exec` to run `mongosh` in the `mongodb-tools-pod`. It connects to the `sample_mflix` database as `search-user` and calls `db.movies.createSearchIndex()` to create a search index named "default" with dynamic mappings on the `movies` collection. Dynamic mapping automatically indexes all fields with supported types. MongoDB Search offers flexible index definitions, allowing for dynamic and static field mappings, various analyzer types (standard, language-specific, custom), and features like synonyms and faceted search. @@ -187,7 +167,7 @@ Before performing search queries, create a search index. This step uses `kubectl {% include "code_snippets/0430_create_search_index.sh" %} ``` -### 16. Wait for Search Index to be Ready +### 14. Wait for Search Index to be Ready Creating a search index is an asynchronous operation. This script polls periodically the status by executing `db.movies.getSearchIndexes("default")`. @@ -196,7 +176,7 @@ Creating a search index is an asynchronous operation. This script polls periodic {% include "code_snippets/0440_wait_for_search_index_ready.sh" %} ``` -### 17. Execute a Search Query +### 15. Execute a Search Query Once the search index is ready, execute search queries using the `$search` aggregation pipeline stage. MongoDB Search supports a query language, allowing for various types of queries such as text search, autocomplete, faceting, and more. You can combine `$search` with other aggregation stages to further refine and process your results. diff --git a/docs/community-search/quick-start/code_snippets/0100_install_operator.sh b/docs/community-search/quick-start/code_snippets/0100_install_operator.sh old mode 100644 new mode 100755 diff --git a/docs/community-search/quick-start/code_snippets/0200_configure_community_search_pullsecret.sh b/docs/community-search/quick-start/code_snippets/0200_configure_community_search_pullsecret.sh deleted file mode 100644 index 59310c81f..000000000 --- a/docs/community-search/quick-start/code_snippets/0200_configure_community_search_pullsecret.sh +++ /dev/null @@ -1,25 +0,0 @@ -kubectl apply --context "${K8S_CLUSTER_0_CONTEXT_NAME}" -n "${MDB_NAMESPACE}" -f - < /tmp/mdb_script.js -mongosh --quiet "mongodb://search-user:${MDB_SEARCH_USER_PASSWORD}@mdbc-rs-0.mdbc-rs-svc.${MDB_NAMESPACE}.svc.cluster.local:27017/?replicaSet=mdbc-rs" < /tmp/mdb_script.js +mongosh --quiet "mongodb://mdb-user:${MDB_USER_PASSWORD}@mdbc-rs-0.mdbc-rs-svc.${MDB_NAMESPACE}.svc.cluster.local:27017/?replicaSet=mdbc-rs" < /tmp/mdb_script.js EOF )" diff --git a/docs/community-search/quick-start/code_snippets/090_helm_add_mogodb_repo.sh b/docs/community-search/quick-start/code_snippets/090_helm_add_mogodb_repo.sh old mode 100644 new mode 100755 diff --git a/docs/community-search/quick-start/code_snippets/9010_delete_namespace.sh b/docs/community-search/quick-start/code_snippets/9010_delete_namespace.sh old mode 100644 new mode 100755 index 96db9a4b6..ed2d3046d --- a/docs/community-search/quick-start/code_snippets/9010_delete_namespace.sh +++ b/docs/community-search/quick-start/code_snippets/9010_delete_namespace.sh @@ -1 +1 @@ -kubectl --context +kubectl --context "${K8S_CLUSTER_0_CONTEXT_NAME}" delete namespace "${MDB_NAMESPACE}" diff --git a/docs/community-search/quick-start/community_search_snippets_test.sh.run.log b/docs/community-search/quick-start/community_search_snippets_test.sh.run.log deleted file mode 100644 index 643009e51..000000000 --- a/docs/community-search/quick-start/community_search_snippets_test.sh.run.log +++ /dev/null @@ -1,16 +0,0 @@ -090_helm_add_mogodb_repo -0100_install_operator -0200_configure_community_search_pullsecret -0210_verify_community_search_pullsecret -0305_create_mongodb_community_user_secrets -0310_create_mongodb_community_resource -0315_wait_for_community_resource -0320_create_mongodb_search_resource -0325_wait_for_search_resource -0330_wait_for_community_resource -0335_show_running_pods -0410_run_mongodb_tools_pod -0420_import_movies_mflix_database -0430_create_search_index -0440_wait_for_search_index_ready -0450_execute_search_query diff --git a/docs/community-search/quick-start/env_variables.sh b/docs/community-search/quick-start/env_variables.sh index 0d0a98a5f..613b1461b 100644 --- a/docs/community-search/quick-start/env_variables.sh +++ b/docs/community-search/quick-start/env_variables.sh @@ -1,15 +1,18 @@ # set it to the context name of the k8s cluster export K8S_CLUSTER_0_CONTEXT_NAME="" -# At the private preview stage the community search image is accessible only from a private repository. -# Please contact MongoDB Support to get access. -export PRIVATE_PREVIEW_IMAGE_PULLSECRET="<.dockerconfigjson>" - # the following namespace will be created if not exists export MDB_NAMESPACE="mongodb" +# minimum required MongoDB version for running MongoDB Search is 8.0.10 +export MDB_VERSION="8.0.10" + +# root admin user for convenience, not used here at all in this guide export MDB_ADMIN_USER_PASSWORD="admin-user-password-CHANGE-ME" -export MDB_SEARCH_USER_PASSWORD="search-user-password-CHANGE-ME" +# regular user performing restore and search queries on sample mflix database +export MDB_USER_PASSWORD="mdb-user-password-CHANGE-ME" +# user for MongoDB Search to connect to the replica set to synchronise data from +export MDB_SEARCH_SYNC_USER_PASSWORD="search-sync-user-password-CHANGE-ME" export OPERATOR_HELM_CHART="mongodb/mongodb-kubernetes" # comma-separated key=value pairs for additional parameters passed to the helm-chart installing the operator diff --git a/docs/community-search/quick-start/env_variables_e2e_private.sh b/docs/community-search/quick-start/env_variables_e2e_private.sh index 7996f92a9..5b528437c 100644 --- a/docs/community-search/quick-start/env_variables_e2e_private.sh +++ b/docs/community-search/quick-start/env_variables_e2e_private.sh @@ -1,8 +1,7 @@ export K8S_CLUSTER_0_CONTEXT_NAME="${CLUSTER_NAME}" -export PRIVATE_PREVIEW_IMAGE_PULLSECRET="${COMMUNITY_PRIVATE_PREVIEW_PULLSECRET_DOCKERCONFIGJSON}" - -source scripts/funcs/operator_deployment +source "${PROJECT_DIR}/scripts/funcs/operator_deployment" +source "${PROJECT_DIR}/scripts/dev/contexts/e2e_mdb_community" OPERATOR_ADDITIONAL_HELM_VALUES="$(get_operator_helm_values | tr ' ' ',')" export OPERATOR_ADDITIONAL_HELM_VALUES export OPERATOR_HELM_CHART="${PROJECT_DIR}/helm_chart" diff --git a/docs/community-search/quick-start/env_variables_e2e_private_dev.sh b/docs/community-search/quick-start/env_variables_e2e_private_dev.sh new file mode 100644 index 000000000..89f8a3932 --- /dev/null +++ b/docs/community-search/quick-start/env_variables_e2e_private_dev.sh @@ -0,0 +1,33 @@ +export K8S_CLUSTER_0_CONTEXT_NAME="kind-kind" + +# patch id from evergreen patch +version_id="68876175f5ad6d0007fdc1d4" + +search_image_repo="268558157000.dkr.ecr.eu-west-1.amazonaws.com/mongot" +search_image_hash="fbd60fb055dd500058edcb45677ea85d19421f47" + +ecr="268558157000.dkr.ecr.us-east-1.amazonaws.com" +declare -a helm_values=( +"registry.imagePullSecrets=image-registries-secret" +"registry.operator=${ecr}/dev" +"registry.initOpsManager=${ecr}/dev" +"registry.initAppDb=${ecr}/dev" +"registry.initDatabase=${ecr}/dev" +"registry.agent=${ecr}/dev" +"registry.opsManager=quay.io/mongodb" +"registry.appDb=quay.io/mongodb" +"registry.database=${ecr}/dev" +"operator.version=${version_id}" +"initOpsManager.version=${version_id}" +"initAppDb.version=${version_id}" +"initDatabase.version=${version_id}" +"database.version=${version_id}" +"search.community.repo=${search_image_repo}" +"search.community.name=community" +"search.community.version=${search_image_hash}" +) + +OPERATOR_ADDITIONAL_HELM_VALUES="$(echo -n "${helm_values[@]}" | tr ' ' ',')" +export OPERATOR_ADDITIONAL_HELM_VALUES +OPERATOR_HELM_CHART="$(realpath "../../../helm_chart")" +export OPERATOR_HELM_CHART diff --git a/docs/community-search/quick-start/output/0100_install_operator.out b/docs/community-search/quick-start/output/0100_install_operator.out index 035e24e8a..95eb4ab71 100644 --- a/docs/community-search/quick-start/output/0100_install_operator.out +++ b/docs/community-search/quick-start/output/0100_install_operator.out @@ -1,12 +1,36 @@ Release "mongodb-kubernetes" does not exist. Installing it now. NAME: mongodb-kubernetes -LAST DEPLOYED: Tue Jul 8 07:04:51 2025 +LAST DEPLOYED: Mon Jul 28 15:07:59 2025 NAMESPACE: mongodb STATUS: deployed REVISION: 1 TEST SUITE: None USER-SUPPLIED VALUES: -dummy: value +database: + version: 68876175f5ad6d0007fdc1d4 +initAppDb: + version: 68876175f5ad6d0007fdc1d4 +initDatabase: + version: 68876175f5ad6d0007fdc1d4 +initOpsManager: + version: 68876175f5ad6d0007fdc1d4 +operator: + version: 68876175f5ad6d0007fdc1d4 +registry: + agent: 268558157000.dkr.ecr.us-east-1.amazonaws.com/dev + appDb: quay.io/mongodb + database: 268558157000.dkr.ecr.us-east-1.amazonaws.com/dev + imagePullSecrets: image-registries-secret + initAppDb: 268558157000.dkr.ecr.us-east-1.amazonaws.com/dev + initDatabase: 268558157000.dkr.ecr.us-east-1.amazonaws.com/dev + initOpsManager: 268558157000.dkr.ecr.us-east-1.amazonaws.com/dev + operator: 268558157000.dkr.ecr.us-east-1.amazonaws.com/dev + opsManager: quay.io/mongodb +search: + community: + name: community + repo: 268558157000.dkr.ecr.eu-west-1.amazonaws.com/mongot + version: fbd60fb055dd500058edcb45677ea85d19421f47 COMPUTED VALUES: agent: @@ -39,17 +63,16 @@ community: version: 4.4.0 database: name: mongodb-kubernetes-database - version: 1.2.0 -dummy: value + version: 68876175f5ad6d0007fdc1d4 initAppDb: name: mongodb-kubernetes-init-appdb - version: 1.2.0 + version: 68876175f5ad6d0007fdc1d4 initDatabase: name: mongodb-kubernetes-init-database - version: 1.2.0 + version: 68876175f5ad6d0007fdc1d4 initOpsManager: name: mongodb-kubernetes-init-ops-manager - version: 1.2.0 + version: 68876175f5ad6d0007fdc1d4 managedSecurityContext: false mongodb: appdbAssumeOldFormat: false @@ -96,7 +119,7 @@ operator: vaultSecretBackend: enabled: false tlsSecretRef: "" - version: 1.2.0 + version: 68876175f5ad6d0007fdc1d4 watchedResources: - mongodb - opsmanagers @@ -112,24 +135,23 @@ readinessProbe: name: mongodb-kubernetes-readinessprobe version: 1.0.22 registry: - agent: quay.io/mongodb + agent: 268558157000.dkr.ecr.us-east-1.amazonaws.com/dev appDb: quay.io/mongodb - database: quay.io/mongodb - imagePullSecrets: null - initAppDb: quay.io/mongodb - initDatabase: quay.io/mongodb - initOpsManager: quay.io/mongodb - operator: quay.io/mongodb + database: 268558157000.dkr.ecr.us-east-1.amazonaws.com/dev + imagePullSecrets: image-registries-secret + initAppDb: 268558157000.dkr.ecr.us-east-1.amazonaws.com/dev + initDatabase: 268558157000.dkr.ecr.us-east-1.amazonaws.com/dev + initOpsManager: 268558157000.dkr.ecr.us-east-1.amazonaws.com/dev + operator: 268558157000.dkr.ecr.us-east-1.amazonaws.com/dev opsManager: quay.io/mongodb pullPolicy: Always readinessProbe: quay.io/mongodb versionUpgradeHook: quay.io/mongodb search: community: - name: mongodb-search-community - repo: quay.io/mongodb - version: 1.47.0 -subresourceEnabled: true + name: community + repo: 268558157000.dkr.ecr.eu-west-1.amazonaws.com/mongot + version: fbd60fb055dd500058edcb45677ea85d19421f47 versionUpgradeHook: name: mongodb-kubernetes-operator-version-upgrade-post-start-hook version: 1.0.9 @@ -143,6 +165,8 @@ kind: ServiceAccount metadata: name: mongodb-kubernetes-appdb namespace: mongodb +imagePullSecrets: + - name: image-registries-secret --- # Source: mongodb-kubernetes/templates/database-roles.yaml apiVersion: v1 @@ -150,6 +174,8 @@ kind: ServiceAccount metadata: name: mongodb-kubernetes-database-pods namespace: mongodb +imagePullSecrets: + - name: image-registries-secret --- # Source: mongodb-kubernetes/templates/database-roles.yaml apiVersion: v1 @@ -157,6 +183,8 @@ kind: ServiceAccount metadata: name: mongodb-kubernetes-ops-manager namespace: mongodb +imagePullSecrets: + - name: image-registries-secret --- # Source: mongodb-kubernetes/templates/operator-sa.yaml apiVersion: v1 @@ -164,8 +192,10 @@ kind: ServiceAccount metadata: name: mongodb-kubernetes-operator namespace: mongodb +imagePullSecrets: + - name: image-registries-secret --- -# Source: mongodb-kubernetes/templates/operator-roles.yaml +# Source: mongodb-kubernetes/templates/operator-roles-clustermongodbroles.yaml kind: ClusterRole apiVersion: rbac.authorization.k8s.io/v1 metadata: @@ -178,34 +208,7 @@ rules: resources: - clustermongodbroles --- -# Source: mongodb-kubernetes/templates/operator-roles.yaml -kind: ClusterRole -apiVersion: rbac.authorization.k8s.io/v1 -metadata: - name: mongodb-kubernetes-operator-mongodb-webhook -rules: - - apiGroups: - - "admissionregistration.k8s.io" - resources: - - validatingwebhookconfigurations - verbs: - - get - - create - - update - - delete - - apiGroups: - - "" - resources: - - services - verbs: - - get - - list - - watch - - create - - update - - delete ---- -# Source: mongodb-kubernetes/templates/operator-roles.yaml +# Source: mongodb-kubernetes/templates/operator-roles-telemetry.yaml # Additional ClusterRole for clusterVersionDetection kind: ClusterRole apiVersion: rbac.authorization.k8s.io/v1 @@ -233,7 +236,34 @@ rules: verbs: - list --- -# Source: mongodb-kubernetes/templates/operator-roles.yaml +# Source: mongodb-kubernetes/templates/operator-roles-webhook.yaml +kind: ClusterRole +apiVersion: rbac.authorization.k8s.io/v1 +metadata: + name: mongodb-kubernetes-operator-mongodb-webhook +rules: + - apiGroups: + - "admissionregistration.k8s.io" + resources: + - validatingwebhookconfigurations + verbs: + - get + - create + - update + - delete + - apiGroups: + - "" + resources: + - services + verbs: + - get + - list + - watch + - create + - update + - delete +--- +# Source: mongodb-kubernetes/templates/operator-roles-clustermongodbroles.yaml kind: ClusterRoleBinding apiVersion: rbac.authorization.k8s.io/v1 metadata: @@ -247,30 +277,30 @@ subjects: name: mongodb-kubernetes-operator namespace: mongodb --- -# Source: mongodb-kubernetes/templates/operator-roles.yaml +# Source: mongodb-kubernetes/templates/operator-roles-telemetry.yaml +# ClusterRoleBinding for clusterVersionDetection kind: ClusterRoleBinding apiVersion: rbac.authorization.k8s.io/v1 metadata: - name: mongodb-kubernetes-operator-mongodb-webhook-binding + name: mongodb-kubernetes-operator-mongodb-cluster-telemetry-binding roleRef: apiGroup: rbac.authorization.k8s.io kind: ClusterRole - name: mongodb-kubernetes-operator-mongodb-webhook + name: mongodb-kubernetes-operator-cluster-telemetry subjects: - kind: ServiceAccount name: mongodb-kubernetes-operator namespace: mongodb --- -# Source: mongodb-kubernetes/templates/operator-roles.yaml -# ClusterRoleBinding for clusterVersionDetection +# Source: mongodb-kubernetes/templates/operator-roles-webhook.yaml kind: ClusterRoleBinding apiVersion: rbac.authorization.k8s.io/v1 metadata: - name: mongodb-kubernetes-operator-mongodb-cluster-telemetry-binding + name: mongodb-kubernetes-operator-mongodb-webhook-binding roleRef: apiGroup: rbac.authorization.k8s.io kind: ClusterRole - name: mongodb-kubernetes-operator-cluster-telemetry + name: mongodb-kubernetes-operator-mongodb-webhook subjects: - kind: ServiceAccount name: mongodb-kubernetes-operator @@ -298,7 +328,7 @@ rules: - delete - get --- -# Source: mongodb-kubernetes/templates/operator-roles.yaml +# Source: mongodb-kubernetes/templates/operator-roles-base.yaml kind: Role apiVersion: rbac.authorization.k8s.io/v1 metadata: @@ -378,7 +408,14 @@ rules: - opsmanagers/status - mongodbmulticluster/status - mongodbsearch/status - +--- +# Source: mongodb-kubernetes/templates/operator-roles-pvc-resize.yaml +kind: Role +apiVersion: rbac.authorization.k8s.io/v1 +metadata: + name: mongodb-kubernetes-operator-pvc-resize + namespace: mongodb +rules: - apiGroups: - '' resources: @@ -406,7 +443,7 @@ subjects: name: mongodb-kubernetes-appdb namespace: mongodb --- -# Source: mongodb-kubernetes/templates/operator-roles.yaml +# Source: mongodb-kubernetes/templates/operator-roles-base.yaml kind: RoleBinding apiVersion: rbac.authorization.k8s.io/v1 metadata: @@ -421,6 +458,21 @@ subjects: name: mongodb-kubernetes-operator namespace: mongodb --- +# Source: mongodb-kubernetes/templates/operator-roles-pvc-resize.yaml +kind: RoleBinding +apiVersion: rbac.authorization.k8s.io/v1 +metadata: + name: mongodb-kubernetes-operator-pvc-resize-binding + namespace: mongodb +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: mongodb-kubernetes-operator-pvc-resize +subjects: + - kind: ServiceAccount + name: mongodb-kubernetes-operator + namespace: mongodb +--- # Source: mongodb-kubernetes/templates/operator.yaml apiVersion: apps/v1 kind: Deployment @@ -445,9 +497,11 @@ spec: securityContext: runAsNonRoot: true runAsUser: 2000 + imagePullSecrets: + - name: image-registries-secret containers: - name: mongodb-kubernetes-operator - image: "quay.io/mongodb/mongodb-kubernetes:1.2.0" + image: "268558157000.dkr.ecr.us-east-1.amazonaws.com/dev/mongodb-kubernetes:68876175f5ad6d0007fdc1d4" imagePullPolicy: Always args: - -watch-resource=mongodb @@ -488,31 +542,31 @@ spec: value: Always # Database - name: MONGODB_ENTERPRISE_DATABASE_IMAGE - value: quay.io/mongodb/mongodb-kubernetes-database + value: 268558157000.dkr.ecr.us-east-1.amazonaws.com/dev/mongodb-kubernetes-database - name: INIT_DATABASE_IMAGE_REPOSITORY - value: quay.io/mongodb/mongodb-kubernetes-init-database + value: 268558157000.dkr.ecr.us-east-1.amazonaws.com/dev/mongodb-kubernetes-init-database - name: INIT_DATABASE_VERSION - value: 1.2.0 + value: 68876175f5ad6d0007fdc1d4 - name: DATABASE_VERSION - value: 1.2.0 + value: 68876175f5ad6d0007fdc1d4 # Ops Manager - name: OPS_MANAGER_IMAGE_REPOSITORY value: quay.io/mongodb/mongodb-enterprise-ops-manager-ubi - name: INIT_OPS_MANAGER_IMAGE_REPOSITORY - value: quay.io/mongodb/mongodb-kubernetes-init-ops-manager + value: 268558157000.dkr.ecr.us-east-1.amazonaws.com/dev/mongodb-kubernetes-init-ops-manager - name: INIT_OPS_MANAGER_VERSION - value: 1.2.0 + value: 68876175f5ad6d0007fdc1d4 # AppDB - name: INIT_APPDB_IMAGE_REPOSITORY - value: quay.io/mongodb/mongodb-kubernetes-init-appdb + value: 268558157000.dkr.ecr.us-east-1.amazonaws.com/dev/mongodb-kubernetes-init-appdb - name: INIT_APPDB_VERSION - value: 1.2.0 + value: 68876175f5ad6d0007fdc1d4 - name: OPS_MANAGER_IMAGE_PULL_POLICY value: Always - name: AGENT_IMAGE - value: "quay.io/mongodb/mongodb-agent-ubi:108.0.2.8729-1" + value: "268558157000.dkr.ecr.us-east-1.amazonaws.com/dev/mongodb-agent-ubi:108.0.2.8729-1" - name: MDB_AGENT_IMAGE_REPOSITORY - value: "quay.io/mongodb/mongodb-agent-ubi" + value: "268558157000.dkr.ecr.us-east-1.amazonaws.com/dev/mongodb-agent-ubi" - name: MONGODB_IMAGE value: mongodb-enterprise-server - name: MONGODB_REPO_URL @@ -521,6 +575,8 @@ spec: value: ubi8 - name: PERFORM_FAILOVER value: 'true' + - name: IMAGE_PULL_SECRETS + value: image-registries-secret - name: MDB_MAX_CONCURRENT_RECONCILES value: "1" - name: POD_NAME @@ -544,9 +600,9 @@ spec: value: "ubi8" # Community Env Vars End - name: MDB_SEARCH_COMMUNITY_REPO_URL - value: "quay.io/mongodb" + value: "268558157000.dkr.ecr.eu-west-1.amazonaws.com/mongot" - name: MDB_SEARCH_COMMUNITY_NAME - value: "mongodb-search-community" + value: "community" - name: MDB_SEARCH_COMMUNITY_VERSION - value: "1.47.0" + value: "fbd60fb055dd500058edcb45677ea85d19421f47" diff --git a/docs/community-search/quick-start/output/0200_configure_community_search_pullsecret.out b/docs/community-search/quick-start/output/0200_configure_community_search_pullsecret.out deleted file mode 100644 index c04db5317..000000000 --- a/docs/community-search/quick-start/output/0200_configure_community_search_pullsecret.out +++ /dev/null @@ -1,18 +0,0 @@ -secret/community-private-preview-pullsecret created -serviceaccount/mongodb-kubernetes-database-pods patched -ServiceAccount mongodb-kubernetes-database-pods has been patched: -apiVersion: v1 -imagePullSecrets: -- name: community-private-preview-pullsecret -kind: ServiceAccount -metadata: - annotations: - meta.helm.sh/release-name: mongodb-kubernetes - meta.helm.sh/release-namespace: mongodb - creationTimestamp: "2025-07-08T07:04:51Z" - labels: - app.kubernetes.io/managed-by: Helm - name: mongodb-kubernetes-database-pods - namespace: mongodb - resourceVersion: "902" - uid: 69ba91c9-2032-48dd-8490-b1b6b1c82625 diff --git a/docs/community-search/quick-start/output/0210_verify_community_search_pullsecret.out b/docs/community-search/quick-start/output/0210_verify_community_search_pullsecret.out deleted file mode 100644 index 80aafdc2d..000000000 --- a/docs/community-search/quick-start/output/0210_verify_community_search_pullsecret.out +++ /dev/null @@ -1,3 +0,0 @@ -Verifying mongodb-kubernetes-database-pods service account contains proper pull secret -{"name":"community-private-preview-pullsecret"} -SUCCESS: mongodb-kubernetes-database-pods service account contains proper pull secret diff --git a/docs/community-search/quick-start/output/0335_show_running_pods.out b/docs/community-search/quick-start/output/0335_show_running_pods.out index bca15125b..cd7017404 100644 --- a/docs/community-search/quick-start/output/0335_show_running_pods.out +++ b/docs/community-search/quick-start/output/0335_show_running_pods.out @@ -1,16 +1,20 @@ MongoDBCommunity resource NAME PHASE VERSION -mdbc-rs Running 8.0.6 +mdbc-rs Running 8.0.10 MongoDBSearch resource NAME PHASE AGE -mdbc-rs Running 5m21s +mdbc-rs Running 7m58s Pods running in cluster kind-kind NAME READY STATUS RESTARTS AGE -mdbc-rs-0 2/2 Running 1 (35s ago) 7m51s -mdbc-rs-1 2/2 Running 1 (3m11s ago) 6m45s -mdbc-rs-2 2/2 Running 1 (114s ago) 5m57s -mdbc-rs-search-0 1/1 Running 0 5m21s -mongodb-kubernetes-operator-64d5b47b46-fd6nr 1/1 Running 0 7m54s +mdb-debug-mdbc-rs-0-0 1/1 Running 0 10m +mdb-debug-mdbc-rs-1-0 1/1 Running 0 10m +mdb-debug-mdbc-rs-2-0 1/1 Running 0 10m +mdb-debug-mdbc-rs-search-0-0 1/1 Running 0 7m57s +mdbc-rs-0 2/2 Running 1 (3m13s ago) 10m +mdbc-rs-1 2/2 Running 1 (5m49s ago) 9m23s +mdbc-rs-2 2/2 Running 1 (4m32s ago) 8m35s +mdbc-rs-search-0 1/1 Running 0 25s +mongodb-kubernetes-operator-5776c8b4df-wm82f 1/1 Running 0 35s diff --git a/docs/community-search/quick-start/output/0440_wait_for_search_index_ready.out b/docs/community-search/quick-start/output/0440_wait_for_search_index_ready.out index 17d0d593b..b264580af 100644 --- a/docs/community-search/quick-start/output/0440_wait_for_search_index_ready.out +++ b/docs/community-search/quick-start/output/0440_wait_for_search_index_ready.out @@ -1,4 +1 @@ -Search index is not ready yet: status=BUILDING -Search index is not ready yet: status=BUILDING -Search index is not ready yet: status=BUILDING -Search index is ready. +Sleeping to wait for search indexes to be created diff --git a/docs/community-search/quick-start/output/090_helm_add_mogodb_repo.out b/docs/community-search/quick-start/output/090_helm_add_mogodb_repo.out index e8630fdff..b1576c236 100644 --- a/docs/community-search/quick-start/output/090_helm_add_mogodb_repo.out +++ b/docs/community-search/quick-start/output/090_helm_add_mogodb_repo.out @@ -1,4 +1,4 @@ -"mongodb" has been added to your repositories +"mongodb" already exists with the same configuration, skipping Hang tight while we grab the latest from your chart repositories... ...Successfully got an update from the "mongodb" chart repository Update Complete. ⎈Happy Helming!⎈ diff --git a/docs/community-search/quick-start/test.sh b/docs/community-search/quick-start/test.sh index 15caf5fe4..86fb6b23e 100755 --- a/docs/community-search/quick-start/test.sh +++ b/docs/community-search/quick-start/test.sh @@ -5,7 +5,7 @@ set -eou pipefail script_name=$(readlink -f "${BASH_SOURCE[0]}") script_dir=$(dirname "${script_name}") -source scripts/code_snippets/sample_test_runner.sh +source "${script_dir}/../../../scripts/code_snippets/sample_test_runner.sh" cd "${script_dir}" @@ -16,8 +16,6 @@ run 0046_create_image_pull_secrets.sh run_for_output 090_helm_add_mogodb_repo.sh run_for_output 0100_install_operator.sh -run_for_output 0200_configure_community_search_pullsecret.sh -run_for_output 0210_verify_community_search_pullsecret.sh run 0305_create_mongodb_community_user_secrets.sh run 0310_create_mongodb_community_resource.sh run 0315_wait_for_community_resource.sh diff --git a/scripts/code_snippets/task_kind_community_search_snippets_test.sh b/scripts/code_snippets/task_kind_community_search_snippets_test.sh index 4b4501c2c..4f23a3e37 100755 --- a/scripts/code_snippets/task_kind_community_search_snippets_test.sh +++ b/scripts/code_snippets/task_kind_community_search_snippets_test.sh @@ -5,7 +5,9 @@ source scripts/dev/set_env_context.sh dump_logs() { source scripts/evergreen/e2e/dump_diagnostic_information.sh - dump_all_non_default_namespaces "$@" + if [[ "${SKIP_DUMP:-"false"}" != "true" ]]; then + dump_all_non_default_namespaces "$@" + fi } trap dump_logs EXIT diff --git a/scripts/dev/contexts/private_kind_code_snippets b/scripts/dev/contexts/private_kind_code_snippets index 704957a6f..970f0dc3f 100644 --- a/scripts/dev/contexts/private_kind_code_snippets +++ b/scripts/dev/contexts/private_kind_code_snippets @@ -8,6 +8,7 @@ script_name=$(readlink -f "${BASH_SOURCE[0]}") script_dir=$(dirname "${script_name}") source "${script_dir}/root-context" +source "${script_dir}/e2e_mdb_community" export NAMESPACE=mongodb export CODE_SNIPPETS_FLAVOR=e2e_private diff --git a/scripts/dev/contexts/root-context b/scripts/dev/contexts/root-context index 6ec7c7a61..ddae31401 100644 --- a/scripts/dev/contexts/root-context +++ b/scripts/dev/contexts/root-context @@ -6,7 +6,8 @@ script_name=$(readlink -f "${BASH_SOURCE[0]}") script_dir=$(dirname "${script_name}") source "${script_dir}/private-context" -export PROJECT_DIR="${PWD}" +PROJECT_DIR="$(realpath "${script_dir}/../../..")" +export PROJECT_DIR export IMAGE_TYPE=ubi export UBI_IMAGE_WITHOUT_SUFFIX=true export WATCH_NAMESPACE=${WATCH_NAMESPACE:-${NAMESPACE}} @@ -36,7 +37,7 @@ fi export OPERATOR_ENV=${OPERATOR_ENV:-"dev"} -AGENT_VERSION="$(jq -r '.agentVersion' release.json)" +AGENT_VERSION="$(jq -r '.agentVersion' "${PROJECT_DIR}/release.json")" export AGENT_VERSION export AGENT_IMAGE="${MDB_AGENT_IMAGE_REPOSITORY}:${AGENT_VERSION}" @@ -112,7 +113,7 @@ export MDB_COMMUNITY_REPO_URL=quay.io/mongodb export MDB_COMMUNITY_AGENT_IMAGE=${AGENT_IMAGE} export MDB_COMMUNITY_IMAGE_TYPE=ubi8 -MDB_SEARCH_COMMUNITY_VERSION="$(jq -r '.search.community.version' release.json)" +MDB_SEARCH_COMMUNITY_VERSION="$(jq -r '.search.community.version' "${PROJECT_DIR}/release.json")" export MDB_SEARCH_COMMUNITY_VERSION export MDB_SEARCH_COMMUNITY_NAME="mongodb-search-community" diff --git a/scripts/dev/update_docs_snippets.sh b/scripts/dev/update_docs_snippets.sh index b9a45b378..bdec61db2 100755 --- a/scripts/dev/update_docs_snippets.sh +++ b/scripts/dev/update_docs_snippets.sh @@ -46,17 +46,16 @@ function prepare_repositories() { } function copy_files() { - samples_dir=$1 - dst_dir="${DOCS_DIR}/source/includes/code-examples/reference-architectures/${samples_dir}" - src_dir="${MCK_DIR}/public/architectures/${samples_dir}" + local src_dir="$1" + local dst_dir="$2" rm -rf "${dst_dir}" mkdir -p "${dst_dir}" - cp -r "${src_dir}/code_snippets" "${dst_dir}" - cp -r "${src_dir}/output" "${dst_dir}" || true - cp "${src_dir}/env_variables.sh" "${dst_dir}" || true - cp -r "${src_dir}/yamls" "${dst_dir}" || true + cp -r "${src_dir}/code_snippets" "${dst_dir}" 2>/dev/null || true + cp -r "${src_dir}/output" "${dst_dir}" 2>/dev/null || true + cp "${src_dir}/env_variables.sh" "${dst_dir}" 2>/dev/null || true + cp -r "${src_dir}/yamls" "${dst_dir}" 2>/dev/null || true } function prepare_docs_pr() { @@ -74,17 +73,26 @@ function prepare_docs_pr() { pushd ../ prepare_repositories -copy_files "ops-manager-multi-cluster" -copy_files "ops-manager-mc-no-mesh" -copy_files "mongodb-sharded-multi-cluster" -copy_files "mongodb-sharded-mc-no-mesh" -copy_files "mongodb-replicaset-multi-cluster" -copy_files "mongodb-replicaset-mc-no-mesh" -copy_files "setup-multi-cluster/verify-connectivity" -copy_files "setup-multi-cluster/setup-gke" -copy_files "setup-multi-cluster/setup-istio" -copy_files "setup-multi-cluster/setup-operator" -copy_files "setup-multi-cluster/setup-cert-manager" -copy_files "setup-multi-cluster/setup-externaldns" + +REF_ARCH_SRC_DIR="${MCK_DIR}/public/architectures" +REF_ARCH_DST_DIR="${DOCS_DIR}/source/includes/code-examples/reference-architectures" + +copy_files "${REF_ARCH_SRC_DIR}/ops-manager-multi-cluster" "${REF_ARCH_DST_DIR}/ops-manager-multi-cluster" +copy_files "${REF_ARCH_SRC_DIR}/ops-manager-mc-no-mesh" "${REF_ARCH_DST_DIR}/ops-manager-mc-no-mesh" +copy_files "${REF_ARCH_SRC_DIR}/mongodb-sharded-multi-cluster" "${REF_ARCH_DST_DIR}/mongodb-sharded-multi-cluster" +copy_files "${REF_ARCH_SRC_DIR}/mongodb-sharded-mc-no-mesh" "${REF_ARCH_DST_DIR}/mongodb-sharded-mc-no-mesh" +copy_files "${REF_ARCH_SRC_DIR}/mongodb-replicaset-multi-cluster" "${REF_ARCH_DST_DIR}/mongodb-replicaset-multi-cluster" +copy_files "${REF_ARCH_SRC_DIR}/mongodb-replicaset-mc-no-mesh" "${REF_ARCH_DST_DIR}/mongodb-replicaset-mc-no-mesh" +copy_files "${REF_ARCH_SRC_DIR}/setup-multi-cluster/verify-connectivity" "${REF_ARCH_DST_DIR}/setup-multi-cluster/verify-connectivity" +copy_files "${REF_ARCH_SRC_DIR}/setup-multi-cluster/setup-gke" "${REF_ARCH_DST_DIR}/setup-multi-cluster/setup-gke" +copy_files "${REF_ARCH_SRC_DIR}/setup-multi-cluster/setup-istio" "${REF_ARCH_DST_DIR}/setup-multi-cluster/setup-istio" +copy_files "${REF_ARCH_SRC_DIR}/setup-multi-cluster/setup-operator" "${REF_ARCH_DST_DIR}/setup-multi-cluster/setup-operator" +copy_files "${REF_ARCH_SRC_DIR}/setup-multi-cluster/setup-cert-manager" "${REF_ARCH_DST_DIR}/setup-multi-cluster/setup-cert-manager" +copy_files "${REF_ARCH_SRC_DIR}/setup-multi-cluster/setup-externaldns" "${REF_ARCH_DST_DIR}/setup-multi-cluster/setup-externaldns" + +DOCS_SNIPPETS_SRC_DIR="${MCK_DIR}/docs" +DOCS_SNIPPEES_DST_DIR="${DOCS_DIR}/source/includes/code-examples" +copy_files "${DOCS_SNIPPETS_SRC_DIR}/community-search/quick-start" "${DOCS_SNIPPEES_DST_DIR}/community-search/quick-start" + prepare_docs_pr popd diff --git a/scripts/funcs/operator_deployment b/scripts/funcs/operator_deployment index 88a0eed0d..55f690546 100644 --- a/scripts/funcs/operator_deployment +++ b/scripts/funcs/operator_deployment @@ -34,9 +34,9 @@ get_operator_helm_values() { "operator.telemetry.send.enabled=${MDB_OPERATOR_TELEMETRY_SEND_ENABLED:-false}" # lets collect and save in the configmap as frequently as we can "operator.telemetry.collection.frequency=${MDB_OPERATOR_TELEMETRY_COLLECTION_FREQUENCY:-1m}" - "search.community.version=${MDB_SEARCH_COMMUNITY_VERSION}" - "search.community.name=${MDB_SEARCH_COMMUNITY_NAME}" "search.community.repo=${MDB_SEARCH_COMMUNITY_REPO_URL}" + "search.community.name=${MDB_SEARCH_COMMUNITY_NAME}" + "search.community.version=${MDB_SEARCH_COMMUNITY_VERSION}" ) if [[ "${MDB_OPERATOR_TELEMETRY_INSTALL_CLUSTER_ROLE_INSTALLATION:-}" != "" ]]; then From 446c8b379904be6a175621ec33bca97a5481be32 Mon Sep 17 00:00:00 2001 From: Anand <13899132+anandsyncs@users.noreply.github.com> Date: Tue, 26 Aug 2025 10:45:33 +0200 Subject: [PATCH 10/13] CLOUDP-338399: External mongod for Search (#308) # Summary This pull request introduces support for using external MongoDB deployments as sources for `MongoDBSearch` resources, in addition to the previously supported operator-managed deployments. The changes include updates to the CRD API, controller logic, and supporting code to handle configuration, validation, and reconciliation for external sources. Additionally, new end-to-end (e2e) test tasks are added to ensure this functionality is covered. **External MongoDB Source Support** * Added new fields to the `MongoDBSource` spec (`ExternalMongoDBSource` and supporting types) to allow specifying external MongoDB deployments, including host/port, credentials, and TLS settings. * Added logic to distinguish between operator-managed and external sources throughout the controller and resource handling code, including the `IsExternalMongoDBSource` method and updates to resource reference logic. **Controller and Reconciliation Logic Updates** * Refactored the `SearchSourceDBResource` interface and its implementations to support both internal and external MongoDB sources, including connection details and version validation. * Updated reconciliation logic to handle external sources, including skipping version validation and resource watching when not applicable. * Adjusted service and StatefulSet construction to use new abstraction for host seeds and service accounts. **Testing and CI** * Added new e2e test tasks for external MongoDB source scenarios (`e2e_search_external_basic` and `e2e_search_external_tls`) and included them in the appropriate task groups to ensure coverage in CI. * Updated tests and validation helpers to use the new interface methods and logic. These changes collectively enable `MongoDBSearch` resources to work with both operator-managed and external MongoDB deployments, increasing flexibility for users and improving the robustness of the operator. ## Proof of Work Tests pass --- .evergreen-tasks.yml | 10 + .evergreen.yml | 2 + api/v1/search/mongodbsearch_types.go | 27 ++- api/v1/search/zz_generated.deepcopy.go | 55 +++++ .../crd/bases/mongodb.com_mongodbsearch.yaml | 49 ++++- .../operator/mongodbsearch_controller.go | 27 ++- .../mongodbsearch_reconcile_helper.go | 30 +-- .../mongodbsearch_reconcile_helper_test.go | 2 +- .../search_controller/search_construction.go | 77 +++++-- ...nity-replicaset-sample-mflix-external.yaml | 112 +++++++++++ .../search_community_external_mongod_basic.py | 144 +++++++++++++ .../search_community_external_mongod_tls.py | 190 ++++++++++++++++++ .../crds/mongodb.com_mongodbsearch.yaml | 45 +++++ .../controllers/replica_set_controller.go | 8 +- public/crds.yaml | 45 +++++ 15 files changed, 777 insertions(+), 46 deletions(-) create mode 100644 docker/mongodb-kubernetes-tests/tests/search/fixtures/community-replicaset-sample-mflix-external.yaml create mode 100644 docker/mongodb-kubernetes-tests/tests/search/search_community_external_mongod_basic.py create mode 100644 docker/mongodb-kubernetes-tests/tests/search/search_community_external_mongod_tls.py diff --git a/.evergreen-tasks.yml b/.evergreen-tasks.yml index 1d289fa3f..01305de88 100644 --- a/.evergreen-tasks.yml +++ b/.evergreen-tasks.yml @@ -1290,3 +1290,13 @@ tasks: tags: ["patch-run"] commands: - func: "e2e_test" + + - name: e2e_search_external_basic + tags: [ "patch-run" ] + commands: + - func: "e2e_test" + + - name: e2e_search_external_tls + tags: [ "patch-run" ] + commands: + - func: "e2e_test" diff --git a/.evergreen.yml b/.evergreen.yml index 6c886bd1c..8ce085484 100644 --- a/.evergreen.yml +++ b/.evergreen.yml @@ -646,6 +646,8 @@ task_groups: - e2e_community_replicaset_scale - e2e_search_community_basic - e2e_search_community_tls + - e2e_search_external_basic + - e2e_search_external_tls # This is the task group that contains all the tests run in the e2e_mdb_kind_ubuntu_cloudqa build variant - name: e2e_mdb_kind_cloudqa_task_group diff --git a/api/v1/search/mongodbsearch_types.go b/api/v1/search/mongodbsearch_types.go index d77fc9b4c..791c5b1dd 100644 --- a/api/v1/search/mongodbsearch_types.go +++ b/api/v1/search/mongodbsearch_types.go @@ -52,11 +52,26 @@ type MongoDBSource struct { // +optional MongoDBResourceRef *userv1.MongoDBResourceRef `json:"mongodbResourceRef,omitempty"` // +optional + ExternalMongoDBSource *ExternalMongoDBSource `json:"external,omitempty"` + // +optional PasswordSecretRef *userv1.SecretKeyRef `json:"passwordSecretRef,omitempty"` // +optional Username *string `json:"username,omitempty"` } +type ExternalMongoDBSource struct { + HostAndPorts []string `json:"hostAndPorts,omitempty"` + KeyFileSecretKeyRef *userv1.SecretKeyRef `json:"keyFileSecretRef,omitempty"` // This is the mongod credential used to connect to the external MongoDB deployment + // +optional + TLS *ExternalMongodTLS `json:"tls,omitempty"` // TLS configuration for the external MongoDB deployment +} + +type ExternalMongodTLS struct { + Enabled bool `json:"enabled"` + // +optional + CA *corev1.LocalObjectReference `json:"ca,omitempty"` +} + type Security struct { // +optional TLS TLS `json:"tls"` @@ -170,13 +185,17 @@ func (s *MongoDBSearch) GetOwnerReferences() []metav1.OwnerReference { return []metav1.OwnerReference{ownerReference} } -func (s *MongoDBSearch) GetMongoDBResourceRef() userv1.MongoDBResourceRef { +func (s *MongoDBSearch) GetMongoDBResourceRef() *userv1.MongoDBResourceRef { + if s.IsExternalMongoDBSource() { + return nil + } + mdbResourceRef := userv1.MongoDBResourceRef{Namespace: s.Namespace, Name: s.Name} if s.Spec.Source != nil && s.Spec.Source.MongoDBResourceRef != nil && s.Spec.Source.MongoDBResourceRef.Name != "" { mdbResourceRef.Name = s.Spec.Source.MongoDBResourceRef.Name } - return mdbResourceRef + return &mdbResourceRef } func (s *MongoDBSearch) GetMongotPort() int32 { @@ -201,3 +220,7 @@ func (s *MongoDBSearch) TLSOperatorSecretNamespacedName() types.NamespacedName { func (s *MongoDBSearch) GetMongotHealthCheckPort() int32 { return MongotDefautHealthCheckPort } + +func (s *MongoDBSearch) IsExternalMongoDBSource() bool { + return s.Spec.Source != nil && s.Spec.Source.ExternalMongoDBSource != nil +} diff --git a/api/v1/search/zz_generated.deepcopy.go b/api/v1/search/zz_generated.deepcopy.go index 8817e4b46..c66322146 100644 --- a/api/v1/search/zz_generated.deepcopy.go +++ b/api/v1/search/zz_generated.deepcopy.go @@ -28,6 +28,56 @@ import ( runtime "k8s.io/apimachinery/pkg/runtime" ) +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ExternalMongoDBSource) DeepCopyInto(out *ExternalMongoDBSource) { + *out = *in + if in.HostAndPorts != nil { + in, out := &in.HostAndPorts, &out.HostAndPorts + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.KeyFileSecretKeyRef != nil { + in, out := &in.KeyFileSecretKeyRef, &out.KeyFileSecretKeyRef + *out = new(user.SecretKeyRef) + **out = **in + } + if in.TLS != nil { + in, out := &in.TLS, &out.TLS + *out = new(ExternalMongodTLS) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ExternalMongoDBSource. +func (in *ExternalMongoDBSource) DeepCopy() *ExternalMongoDBSource { + if in == nil { + return nil + } + out := new(ExternalMongoDBSource) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ExternalMongodTLS) DeepCopyInto(out *ExternalMongodTLS) { + *out = *in + if in.CA != nil { + in, out := &in.CA, &out.CA + *out = new(v1.LocalObjectReference) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ExternalMongodTLS. +func (in *ExternalMongodTLS) DeepCopy() *ExternalMongodTLS { + if in == nil { + return nil + } + out := new(ExternalMongodTLS) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *MongoDBSearch) DeepCopyInto(out *MongoDBSearch) { *out = *in @@ -151,6 +201,11 @@ func (in *MongoDBSource) DeepCopyInto(out *MongoDBSource) { *out = new(user.MongoDBResourceRef) **out = **in } + if in.ExternalMongoDBSource != nil { + in, out := &in.ExternalMongoDBSource, &out.ExternalMongoDBSource + *out = new(ExternalMongoDBSource) + (*in).DeepCopyInto(*out) + } if in.PasswordSecretRef != nil { in, out := &in.PasswordSecretRef, &out.PasswordSecretRef *out = new(user.SecretKeyRef) diff --git a/config/crd/bases/mongodb.com_mongodbsearch.yaml b/config/crd/bases/mongodb.com_mongodbsearch.yaml index 585b69210..276adf875 100644 --- a/config/crd/bases/mongodb.com_mongodbsearch.yaml +++ b/config/crd/bases/mongodb.com_mongodbsearch.yaml @@ -182,6 +182,51 @@ spec: type: object source: properties: + external: + properties: + hostAndPorts: + items: + type: string + type: array + keyFileSecretRef: + description: |- + SecretKeyRef is a reference to a value in a given secret in the same + namespace. Based on: + https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.15/#secretkeyselector-v1-core + properties: + key: + type: string + name: + type: string + required: + - name + type: object + tls: + properties: + ca: + description: |- + LocalObjectReference contains enough information to let you locate the + referenced object inside the same namespace. + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + TODO: Add other useful fields. apiVersion, kind, uid? + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Drop `kubebuilder:default` when controller-gen doesn't need it https://github.com/kubernetes-sigs/kubebuilder/issues/3896. + type: string + type: object + x-kubernetes-map-type: atomic + enabled: + type: boolean + required: + - enabled + type: object + type: object mongodbResourceRef: properties: name: @@ -209,8 +254,8 @@ spec: type: object statefulSet: description: |- - StatefulSetConfiguration holds the optional custom StatefulSet - that should be merged into the operator created one. + StatefulSetSpec which the operator will apply to the MongoDB Search StatefulSet at the end of the reconcile loop. Use to provide necessary customizations, + which aren't exposed as fields in the MongoDBSearch.spec. properties: metadata: description: StatefulSetMetadataWrapper is a wrapper around Labels diff --git a/controllers/operator/mongodbsearch_controller.go b/controllers/operator/mongodbsearch_controller.go index bf8a5f022..b83791f1f 100644 --- a/controllers/operator/mongodbsearch_controller.go +++ b/controllers/operator/mongodbsearch_controller.go @@ -51,31 +51,46 @@ func (r *MongoDBSearchReconciler) Reconcile(ctx context.Context, request reconci return result, err } - sourceResource, err := getSourceMongoDBForSearch(ctx, r.kubeClient, mdbSearch) + sourceResource, mdbc, err := getSourceMongoDBForSearch(ctx, r.kubeClient, mdbSearch) if err != nil { return reconcile.Result{RequeueAfter: time.Second * util.RetryTimeSec}, err } - r.mdbcWatcher.Watch(ctx, sourceResource.NamespacedName(), request.NamespacedName) + if mdbc != nil { + r.mdbcWatcher.Watch(ctx, mdbc.NamespacedName(), request.NamespacedName) + } reconcileHelper := search_controller.NewMongoDBSearchReconcileHelper(kubernetesClient.NewClient(r.kubeClient), mdbSearch, sourceResource, r.operatorSearchConfig) return reconcileHelper.Reconcile(ctx, log).ReconcileResult() } -func getSourceMongoDBForSearch(ctx context.Context, kubeClient client.Client, search *searchv1.MongoDBSearch) (search_controller.SearchSourceDBResource, error) { +func getSourceMongoDBForSearch(ctx context.Context, kubeClient client.Client, search *searchv1.MongoDBSearch) (search_controller.SearchSourceDBResource, *mdbcv1.MongoDBCommunity, error) { + if search.IsExternalMongoDBSource() { + return search_controller.NewSearchSourceDBResourceFromExternal(search.Namespace, search.Spec.Source.ExternalMongoDBSource), nil, nil + } + sourceMongoDBResourceRef := search.GetMongoDBResourceRef() + if sourceMongoDBResourceRef == nil { + return nil, nil, xerrors.New("MongoDBSearch source MongoDB resource reference is not set") + } + mdbcName := types.NamespacedName{Namespace: search.GetNamespace(), Name: sourceMongoDBResourceRef.Name} mdbc := &mdbcv1.MongoDBCommunity{} if err := kubeClient.Get(ctx, mdbcName, mdbc); err != nil { - return nil, xerrors.Errorf("error getting MongoDBCommunity %s: %w", mdbcName, err) + return nil, nil, xerrors.Errorf("error getting MongoDBCommunity %s: %w", mdbcName, err) } - return search_controller.NewSearchSourceDBResourceFromMongoDBCommunity(mdbc), nil + return search_controller.NewSearchSourceDBResourceFromMongoDBCommunity(mdbc), mdbc, nil } func mdbcSearchIndexBuilder(rawObj client.Object) []string { mdbSearch := rawObj.(*searchv1.MongoDBSearch) - return []string{mdbSearch.GetMongoDBResourceRef().Namespace + "/" + mdbSearch.GetMongoDBResourceRef().Name} + resourceRef := mdbSearch.GetMongoDBResourceRef() + if resourceRef == nil { + return []string{} + } + + return []string{resourceRef.Namespace + "/" + resourceRef.Name} } func AddMongoDBSearchController(ctx context.Context, mgr manager.Manager, operatorSearchConfig search_controller.OperatorSearchConfig) error { diff --git a/controllers/search_controller/mongodbsearch_reconcile_helper.go b/controllers/search_controller/mongodbsearch_reconcile_helper.go index 08a264853..1b0f33603 100644 --- a/controllers/search_controller/mongodbsearch_reconcile_helper.go +++ b/controllers/search_controller/mongodbsearch_reconcile_helper.go @@ -7,7 +7,6 @@ import ( "fmt" "strings" - "github.com/blang/semver" "github.com/ghodss/yaml" "go.uber.org/zap" "golang.org/x/xerrors" @@ -82,7 +81,7 @@ func (r *MongoDBSearchReconcileHelper) reconcile(ctx context.Context, log *zap.S log = log.With("MongoDBSearch", r.mdbSearch.NamespacedName()) log.Infof("Reconciling MongoDBSearch") - if err := ValidateSearchSource(r.db); err != nil { + if err := r.db.ValidateMongoDBVersion(); err != nil { return workflow.Failed(err) } @@ -123,7 +122,7 @@ func (r *MongoDBSearchReconcileHelper) reconcile(ctx context.Context, log *zap.S return workflow.Failed(err) } - if statefulSetStatus := statefulset.GetStatefulSetStatus(ctx, r.db.NamespacedName().Namespace, r.mdbSearch.StatefulSetNamespacedName().Name, r.client); !statefulSetStatus.IsOK() { + if statefulSetStatus := statefulset.GetStatefulSetStatus(ctx, r.mdbSearch.Namespace, r.mdbSearch.StatefulSetNamespacedName().Name, r.client); !statefulSetStatus.IsOK() { return statefulSetStatus } @@ -334,10 +333,7 @@ func buildSearchHeadlessService(search *searchv1.MongoDBSearch) corev1.Service { func createMongotConfig(search *searchv1.MongoDBSearch, db SearchSourceDBResource) mongot.Modification { return func(config *mongot.Config) { - var hostAndPorts []string - for i := range db.Members() { - hostAndPorts = append(hostAndPorts, fmt.Sprintf("%s-%d.%s.%s.svc.cluster.local:%d", db.Name(), i, db.DatabaseServiceName(), db.GetNamespace(), db.DatabasePort())) - } + hostAndPorts := db.HostSeeds() config.SyncSource = mongot.ConfigSyncSource{ ReplicaSet: mongot.ConfigReplicaSet{ @@ -395,23 +391,17 @@ func mongotHostAndPort(search *searchv1.MongoDBSearch) string { return fmt.Sprintf("%s.%s.svc.cluster.local:%d", svcName.Name, svcName.Namespace, search.GetMongotPort()) } -func ValidateSearchSource(db SearchSourceDBResource) error { - version, err := semver.ParseTolerant(db.GetMongoDBVersion()) - if err != nil { - return xerrors.Errorf("error parsing MongoDB version '%s': %w", db.GetMongoDBVersion(), err) - } else if version.LT(semver.MustParse("8.0.10")) { - return xerrors.New("MongoDB version must be 8.0.10 or higher") +func (r *MongoDBSearchReconcileHelper) ValidateSingleMongoDBSearchForSearchSource(ctx context.Context) error { + if r.mdbSearch.Spec.Source != nil && r.mdbSearch.Spec.Source.ExternalMongoDBSource != nil { + return nil } - return nil -} - -func (r *MongoDBSearchReconcileHelper) ValidateSingleMongoDBSearchForSearchSource(ctx context.Context) error { + ref := r.mdbSearch.GetMongoDBResourceRef() searchList := &searchv1.MongoDBSearchList{} if err := r.client.List(ctx, searchList, &client.ListOptions{ - FieldSelector: fields.OneTermEqualSelector(MongoDBSearchIndexFieldName, r.db.GetNamespace()+"/"+r.db.Name()), + FieldSelector: fields.OneTermEqualSelector(MongoDBSearchIndexFieldName, ref.Namespace+"/"+ref.Name), }); err != nil { - return xerrors.Errorf("Error listing MongoDBSearch resources for search source '%s': %w", r.db.Name(), err) + return xerrors.Errorf("Error listing MongoDBSearch resources for search source '%s': %w", ref.Name, err) } if len(searchList.Items) > 1 { @@ -420,7 +410,7 @@ func (r *MongoDBSearchReconcileHelper) ValidateSingleMongoDBSearchForSearchSourc resourceNames[i] = search.Name } return xerrors.Errorf( - "Found multiple MongoDBSearch resources for search source '%s': %s", r.db.Name(), + "Found multiple MongoDBSearch resources for search source '%s': %s", ref.Name, strings.Join(resourceNames, ", "), ) } diff --git a/controllers/search_controller/mongodbsearch_reconcile_helper_test.go b/controllers/search_controller/mongodbsearch_reconcile_helper_test.go index f70ec9a03..c7b4a8dc7 100644 --- a/controllers/search_controller/mongodbsearch_reconcile_helper_test.go +++ b/controllers/search_controller/mongodbsearch_reconcile_helper_test.go @@ -64,7 +64,7 @@ func TestMongoDBSearchReconcileHelper_ValidateSearchSource(t *testing.T) { for _, c := range cases { t.Run(c.name, func(t *testing.T) { db := NewSearchSourceDBResourceFromMongoDBCommunity(&c.mdbc) - err := ValidateSearchSource(db) + err := db.ValidateMongoDBVersion() if c.expectedError == "" { assert.NoError(t, err) } else { diff --git a/controllers/search_controller/search_construction.go b/controllers/search_controller/search_construction.go index 6aae8e982..cf622c0d2 100644 --- a/controllers/search_controller/search_construction.go +++ b/controllers/search_controller/search_construction.go @@ -1,6 +1,10 @@ package search_controller import ( + "fmt" + + "github.com/blang/semver" + "golang.org/x/xerrors" "k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/util/intstr" @@ -31,27 +35,79 @@ const ( // // TODO check if we could use already existing interface (DbCommon, MongoDBStatefulSetOwner, etc.) type SearchSourceDBResource interface { - Name() string - NamespacedName() types.NamespacedName KeyfileSecretName() string - GetNamespace() string - HasSeparateDataAndLogsVolumes() bool - DatabaseServiceName() string - DatabasePort() int - GetMongoDBVersion() string IsSecurityTLSConfigEnabled() bool TLSOperatorCASecretNamespacedName() types.NamespacedName - Members() int + HostSeeds() []string + ValidateMongoDBVersion() error } func NewSearchSourceDBResourceFromMongoDBCommunity(mdbc *mdbcv1.MongoDBCommunity) SearchSourceDBResource { return &mdbcSearchResource{db: mdbc} } +func NewSearchSourceDBResourceFromExternal(namespace string, spec *searchv1.ExternalMongoDBSource) SearchSourceDBResource { + return &externalSearchResource{namespace: namespace, spec: spec} +} + +// externalSearchResource implements SearchSourceDBResource for deployments managed outside the operator. +type externalSearchResource struct { + namespace string + spec *searchv1.ExternalMongoDBSource +} + +func (r *externalSearchResource) ValidateMongoDBVersion() error { + return nil +} + +func (r *externalSearchResource) KeyfileSecretName() string { + if r.spec.KeyFileSecretKeyRef != nil { + return r.spec.KeyFileSecretKeyRef.Name + } + + return "" +} + +func (r *externalSearchResource) IsSecurityTLSConfigEnabled() bool { + return r.spec.TLS != nil && r.spec.TLS.Enabled +} + +func (r *externalSearchResource) TLSOperatorCASecretNamespacedName() types.NamespacedName { + if r.spec.TLS != nil && r.spec.TLS.CA != nil { + return types.NamespacedName{ + Name: r.spec.TLS.CA.Name, + Namespace: r.namespace, + } + } + + return types.NamespacedName{} +} + +func (r *externalSearchResource) HostSeeds() []string { return r.spec.HostAndPorts } + type mdbcSearchResource struct { db *mdbcv1.MongoDBCommunity } +func (r *mdbcSearchResource) ValidateMongoDBVersion() error { + version, err := semver.ParseTolerant(r.db.GetMongoDBVersion()) + if err != nil { + return xerrors.Errorf("error parsing MongoDB version '%s': %w", r.db.GetMongoDBVersion(), err) + } else if version.LT(semver.MustParse("8.0.10")) { + return xerrors.New("MongoDB version must be 8.0.10 or higher") + } + + return nil +} + +func (r *mdbcSearchResource) HostSeeds() []string { + seeds := make([]string, r.db.Spec.Members) + for i := range seeds { + seeds[i] = fmt.Sprintf("%s-%d.%s.%s.svc.cluster.local:%d", r.db.Name, i, r.db.ServiceName(), r.db.Namespace, r.db.GetMongodConfiguration().GetDBPort()) + } + return seeds +} + func (r *mdbcSearchResource) Members() int { return r.db.Spec.Members } @@ -80,10 +136,6 @@ func (r *mdbcSearchResource) DatabaseServiceName() string { return r.db.ServiceName() } -func (r *mdbcSearchResource) GetMongoDBVersion() string { - return r.db.Spec.Version -} - func (r *mdbcSearchResource) IsSecurityTLSConfigEnabled() bool { return r.db.Spec.Security.TLS.Enabled } @@ -161,7 +213,6 @@ func CreateSearchStatefulSetFunc(mdbSearch *searchv1.MongoDBSearch, sourceDBReso podSecurityContext, podtemplatespec.WithPodLabels(labels), podtemplatespec.WithVolumes(volumes), - podtemplatespec.WithServiceAccount(sourceDBResource.DatabaseServiceName()), podtemplatespec.WithServiceAccount(util.MongoDBServiceAccount), podtemplatespec.WithContainer(MongotContainerName, mongodbSearchContainer(mdbSearch, volumeMounts, searchImage)), ), diff --git a/docker/mongodb-kubernetes-tests/tests/search/fixtures/community-replicaset-sample-mflix-external.yaml b/docker/mongodb-kubernetes-tests/tests/search/fixtures/community-replicaset-sample-mflix-external.yaml new file mode 100644 index 000000000..45e332fd3 --- /dev/null +++ b/docker/mongodb-kubernetes-tests/tests/search/fixtures/community-replicaset-sample-mflix-external.yaml @@ -0,0 +1,112 @@ +--- +apiVersion: mongodbcommunity.mongodb.com/v1 +kind: MongoDBCommunity +metadata: + name: mdbc-rs +spec: + version: 8.0.10 + type: ReplicaSet + members: 3 + security: + authentication: + ignoreUnknownUsers: true + modes: + - SCRAM + roles: + - role: searchCoordinator + db: admin + roles: + - name: clusterMonitor + db: admin + - name: directShardOperations + db: admin + - name: readAnyDatabase + db: admin + privileges: + - resource: + db: "__mdb_internal_search" + collection: "" + actions: + - "changeStream" + - "collStats" + - "dbHash" + - "dbStats" + - "find" + - "killCursors" + - "listCollections" + - "listIndexes" + - "listSearchIndexes" + - "planCacheRead" + - "cleanupStructuredEncryptionData" + - "compactStructuredEncryptionData" + - "convertToCapped" + - "createCollection" + - "createIndex" + - "createSearchIndexes" + - "dropCollection" + - "dropIndex" + - "dropSearchIndex" + - "insert" + - "remove" + - "renameCollectionSameDB" + - "update" + - "updateSearchIndex" + - resource: + cluster: true + actions: + - "bypassDefaultMaxTimeMS" + agent: + logLevel: DEBUG + statefulSet: + spec: + template: + spec: + containers: + - name: mongod + resources: + limits: + cpu: "2" + memory: 2Gi + requests: + cpu: "1" + memory: 1Gi + - name: mongodb-agent + resources: + limits: + cpu: "1" + memory: 2Gi + requests: + cpu: "0.5" + memory: 1Gi + users: + # admin user with root role + - name: mdb-admin + db: admin + passwordSecretRef: # a reference to the secret containing user password + name: mdb-admin-user-password + scramCredentialsSecretName: mdb-admin-user + roles: + - name: root + db: admin + # user performing search queries + - name: mdb-user + db: admin + passwordSecretRef: # a reference to the secret containing user password + name: mdb-user-password + scramCredentialsSecretName: mdb-user-scram + roles: + - name: restore + db: sample_mflix + - name: readWrite + db: sample_mflix + # user used by MongoDB Search to connect to MongoDB database to synchronize data from + # For MongoDB <8.2, the operator will be creating the searchCoordinator custom role automatically + # From MongoDB 8.2, searchCoordinator role will be a built-in role. + - name: search-sync-source + db: admin + passwordSecretRef: # a reference to the secret that will be used to generate the user's password + name: mdbc-rs-search-sync-source-password + scramCredentialsSecretName: mdbc-rs-search-sync-source + roles: + - name: searchCoordinator + db: admin diff --git a/docker/mongodb-kubernetes-tests/tests/search/search_community_external_mongod_basic.py b/docker/mongodb-kubernetes-tests/tests/search/search_community_external_mongod_basic.py new file mode 100644 index 000000000..110e134dc --- /dev/null +++ b/docker/mongodb-kubernetes-tests/tests/search/search_community_external_mongod_basic.py @@ -0,0 +1,144 @@ +from kubetester import create_or_update_secret, try_load +from kubetester.kubetester import fixture as yaml_fixture +from kubetester.mongodb_community import MongoDBCommunity +from kubetester.mongodb_search import MongoDBSearch +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.movies_search_helper import SampleMoviesSearchHelper +from tests.common.search.search_tester import SearchTester +from tests.conftest import get_default_operator + +logger = test_logger.get_test_logger(__name__) + +ADMIN_USER_NAME = "mdb-admin-user" +ADMIN_USER_PASSWORD = "mdb-admin-user-pass" + +MONGOT_USER_NAME = "search-sync-source" +MONGOT_USER_PASSWORD = "search-sync-source-password" + +USER_NAME = "mdb-user" +USER_PASSWORD = "mdb-user-pass" + +MDBC_RESOURCE_NAME = "mdbc-rs" +MDBS_RESOURCE_NAME = "mdbs" + + +@fixture(scope="function") +def mdbc(namespace: str) -> MongoDBCommunity: + resource = MongoDBCommunity.from_yaml( + yaml_fixture("community-replicaset-sample-mflix-external.yaml"), + name=MDBC_RESOURCE_NAME, + namespace=namespace, + ) + + if try_load(resource): + return resource + + mongot_host = f"{MDBS_RESOURCE_NAME}-search-svc.{namespace}.svc.cluster.local:27027" + if "additionalMongodConfig" not in resource["spec"]: + resource["spec"]["additionalMongodConfig"] = {} + if "setParameter" not in resource["spec"]["additionalMongodConfig"]: + resource["spec"]["additionalMongodConfig"]["setParameter"] = {} + + resource["spec"]["additionalMongodConfig"]["setParameter"].update( + { + "mongotHost": mongot_host, + "searchIndexManagementHostAndPort": mongot_host, + "skipAuthenticationToSearchIndexManagementServer": False, + "searchTLSMode": "disabled", + } + ) + + return resource + + +@fixture(scope="function") +def mdbs(namespace: str, mdbc: MongoDBCommunity) -> MongoDBSearch: + resource = MongoDBSearch.from_yaml( + yaml_fixture("search-minimal.yaml"), + name=MDBS_RESOURCE_NAME, + namespace=namespace, + ) + + seeds = [ + f"{mdbc.name}-{i}.{mdbc.name}-svc.{namespace}.svc.cluster.local:27017" for i in range(mdbc["spec"]["members"]) + ] + + resource["spec"] = { + "source": { + "external": { + "hostAndPorts": seeds, + "keyFileSecretRef": {"name": f"{mdbc.name}-keyfile", "key": "keyfile"}, + "tls": {"enabled": False}, + }, + "passwordSecretRef": {"name": f"{MDBC_RESOURCE_NAME}-{MONGOT_USER_NAME}-password", "key": "password"}, + "username": MONGOT_USER_NAME, + } + } + + return resource + + +@mark.e2e_search_external_basic +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_external_basic +def test_install_secrets(namespace: str, mdbs: MongoDBSearch): + create_or_update_secret(namespace=namespace, name=f"{USER_NAME}-password", data={"password": USER_PASSWORD}) + create_or_update_secret( + namespace=namespace, name=f"{ADMIN_USER_NAME}-password", data={"password": ADMIN_USER_PASSWORD} + ) + + create_or_update_secret( + namespace=namespace, + name=f"{MDBC_RESOURCE_NAME}-{MONGOT_USER_NAME}-password", + data={"password": MONGOT_USER_PASSWORD}, + ) + + +@mark.e2e_search_external_basic +def test_create_database_resource(mdbc: MongoDBCommunity): + mdbc.update() + mdbc.assert_reaches_phase(Phase.Running, timeout=1000) + + +@mark.e2e_search_external_basic +def test_create_search_resource(mdbs: MongoDBSearch, mdbc: MongoDBCommunity): + mdbs.update() + mdbs.assert_reaches_phase(Phase.Running, timeout=1000) + + +@mark.e2e_search_external_basic +def test_wait_for_community_resource_ready(mdbc: MongoDBCommunity): + mdbc.assert_reaches_phase(Phase.Running, timeout=1800) + + +@fixture(scope="function") +def sample_movies_helper(mdbc: MongoDBCommunity) -> SampleMoviesSearchHelper: + return movies_search_helper.SampleMoviesSearchHelper( + SearchTester(get_connection_string(mdbc, USER_NAME, USER_PASSWORD)) + ) + + +@mark.e2e_search_external_basic +def test_search_restore_sample_database(sample_movies_helper: SampleMoviesSearchHelper): + sample_movies_helper.restore_sample_database() + + +@mark.e2e_search_external_basic +def test_search_create_search_index(sample_movies_helper: SampleMoviesSearchHelper): + sample_movies_helper.create_search_index() + + +@mark.e2e_search_external_basic +def test_search_assert_search_query(sample_movies_helper: SampleMoviesSearchHelper): + sample_movies_helper.assert_search_query(retry_timeout=60) + + +def get_connection_string(mdbc: MongoDBCommunity, user_name: str, user_password: str) -> str: + return f"mongodb://{user_name}:{user_password}@{mdbc.name}-0.{mdbc.name}-svc.{mdbc.namespace}.svc.cluster.local:27017/?replicaSet={mdbc.name}" diff --git a/docker/mongodb-kubernetes-tests/tests/search/search_community_external_mongod_tls.py b/docker/mongodb-kubernetes-tests/tests/search/search_community_external_mongod_tls.py new file mode 100644 index 000000000..f6a270a48 --- /dev/null +++ b/docker/mongodb-kubernetes-tests/tests/search/search_community_external_mongod_tls.py @@ -0,0 +1,190 @@ +from kubetester import create_or_update_secret, try_load +from kubetester.certs import create_tls_certs +from kubetester.kubetester import fixture as yaml_fixture +from kubetester.mongodb_community import MongoDBCommunity +from kubetester.mongodb_search import MongoDBSearch +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.movies_search_helper import SampleMoviesSearchHelper +from tests.common.search.search_tester import SearchTester +from tests.conftest import get_default_operator + +logger = test_logger.get_test_logger(__name__) + +ADMIN_USER_NAME = "mdb-admin-user" +ADMIN_USER_PASSWORD = "mdb-admin-user-pass" + +MONGOT_USER_NAME = "search-sync-source" +MONGOT_USER_PASSWORD = "search-sync-source-user-password" + +USER_NAME = "mdb-user" +USER_PASSWORD = "mdb-user-pass" + +MDBC_RESOURCE_NAME = "mdbc-rs" +MDBS_RESOURCE_NAME = "mdbs" +TLS_SECRET_NAME = "tls-secret" +TLS_CA_SECRET_NAME = "tls-ca-secret" +MDBS_TLS_SECRET_NAME = "mdbs-tls-secret" + + +@fixture(scope="function") +def mdbc(namespace: str) -> MongoDBCommunity: + resource = MongoDBCommunity.from_yaml( + yaml_fixture("community-replicaset-sample-mflix-external.yaml"), + name=MDBC_RESOURCE_NAME, + namespace=namespace, + ) + + if try_load(resource): + return resource + + mongot_host = f"{MDBS_RESOURCE_NAME}-search-svc.{namespace}.svc.cluster.local:27027" + if "additionalMongodConfig" not in resource["spec"]: + resource["spec"]["additionalMongodConfig"] = {} + if "setParameter" not in resource["spec"]["additionalMongodConfig"]: + resource["spec"]["additionalMongodConfig"]["setParameter"] = {} + + resource["spec"]["additionalMongodConfig"]["setParameter"].update( + { + "mongotHost": mongot_host, + "searchIndexManagementHostAndPort": mongot_host, + "skipAuthenticationToSearchIndexManagementServer": False, + "searchTLSMode": "requireTLS", + } + ) + + resource["spec"]["security"]["tls"] = { + "enabled": True, + "certificateKeySecretRef": {"name": TLS_SECRET_NAME}, + "caCertificateSecretRef": {"name": TLS_SECRET_NAME}, + } + + return resource + + +@fixture(scope="function") +def mdbs(namespace: str, mdbc: MongoDBCommunity) -> MongoDBSearch: + resource = MongoDBSearch.from_yaml( + yaml_fixture("search-minimal.yaml"), + name=MDBS_RESOURCE_NAME, + namespace=namespace, + ) + + if try_load(resource): + return resource + + return resource + + +@mark.e2e_search_external_tls +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_external_tls +def test_install_secrets(namespace: str, mdbs: MongoDBSearch): + create_or_update_secret(namespace=namespace, name=f"{USER_NAME}-password", data={"password": USER_PASSWORD}) + create_or_update_secret( + namespace=namespace, name=f"{ADMIN_USER_NAME}-password", data={"password": ADMIN_USER_PASSWORD} + ) + create_or_update_secret( + namespace=namespace, + name=f"{MDBC_RESOURCE_NAME}-{MONGOT_USER_NAME}-password", + data={"password": MONGOT_USER_PASSWORD}, + ) + + +@mark.e2e_search_external_tls +def test_install_tls_secrets_and_configmaps( + namespace: str, mdbc: MongoDBCommunity, mdbs: MongoDBSearch, issuer: str, issuer_ca_filepath: str +): + create_tls_certs(issuer, namespace, mdbc.name, mdbc["spec"]["members"], secret_name=TLS_SECRET_NAME) + + 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"], + secret_name=MDBS_TLS_SECRET_NAME, + ) + + ca = open(issuer_ca_filepath).read() + + ca_secret_name = f"{mdbc.name}-ca" + create_or_update_secret(namespace=namespace, name=ca_secret_name, data={"ca.crt": ca}) + + +@mark.e2e_search_external_tls +def test_create_database_resource(mdbc: MongoDBCommunity): + mdbc.update() + mdbc.assert_reaches_phase(Phase.Running, timeout=1000) + + +@mark.e2e_search_external_tls +def test_create_search_resource(mdbs: MongoDBSearch, mdbc: MongoDBCommunity): + seeds = [ + f"{mdbc.name}-{i}.{mdbc.name}-svc.{mdbc.namespace}.svc.cluster.local:27017" + for i in range(mdbc["spec"]["members"]) + ] + + mdbs["spec"]["source"] = { + "external": { + "hostAndPorts": seeds, + "keyFileSecretRef": {"name": f"{mdbc.name}-keyfile"}, + "tls": { + "enabled": True, + "ca": {"name": f"{mdbc.name}-ca"}, + }, + }, + "passwordSecretRef": {"name": f"{MDBC_RESOURCE_NAME}-{MONGOT_USER_NAME}-password", "key": "password"}, + "username": MONGOT_USER_NAME, + } + + mdbs["spec"]["security"] = {"tls": {"enabled": True, "certificateKeySecretRef": {"name": MDBS_TLS_SECRET_NAME}}} + + mdbs.update() + mdbs.assert_reaches_phase(Phase.Running, timeout=300) + + +@mark.e2e_search_external_tls +def test_wait_for_community_resource_ready(mdbc: MongoDBCommunity): + mdbc.assert_reaches_phase(Phase.Running, timeout=1800) + + +@fixture(scope="function") +def sample_movies_helper(mdbc: MongoDBCommunity, issuer_ca_filepath: str) -> SampleMoviesSearchHelper: + return movies_search_helper.SampleMoviesSearchHelper( + SearchTester( + get_connection_string(mdbc, USER_NAME, USER_PASSWORD), + use_ssl=True, + ca_path=issuer_ca_filepath, + ) + ) + + +@mark.e2e_search_external_tls +def test_search_restore_sample_database(sample_movies_helper: SampleMoviesSearchHelper): + sample_movies_helper.restore_sample_database() + + +@mark.e2e_search_external_tls +def test_search_create_search_index(sample_movies_helper: SampleMoviesSearchHelper): + sample_movies_helper.create_search_index() + + +@mark.e2e_search_external_tls +def test_search_assert_search_query(sample_movies_helper: SampleMoviesSearchHelper): + sample_movies_helper.assert_search_query(retry_timeout=60) + + +def get_connection_string(mdbc: MongoDBCommunity, user_name: str, user_password: str) -> str: + return ( + f"mongodb://{user_name}:{user_password}@{mdbc.name}-0.{mdbc.name}-svc.{mdbc.namespace}.svc.cluster.local:27017/" + f"?replicaSet={mdbc.name}" + ) diff --git a/helm_chart/crds/mongodb.com_mongodbsearch.yaml b/helm_chart/crds/mongodb.com_mongodbsearch.yaml index 585b69210..0cd70706e 100644 --- a/helm_chart/crds/mongodb.com_mongodbsearch.yaml +++ b/helm_chart/crds/mongodb.com_mongodbsearch.yaml @@ -182,6 +182,51 @@ spec: type: object source: properties: + external: + properties: + hostAndPorts: + items: + type: string + type: array + keyFileSecretRef: + description: |- + SecretKeyRef is a reference to a value in a given secret in the same + namespace. Based on: + https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.15/#secretkeyselector-v1-core + properties: + key: + type: string + name: + type: string + required: + - name + type: object + tls: + properties: + ca: + description: |- + LocalObjectReference contains enough information to let you locate the + referenced object inside the same namespace. + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + TODO: Add other useful fields. apiVersion, kind, uid? + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Drop `kubebuilder:default` when controller-gen doesn't need it https://github.com/kubernetes-sigs/kubebuilder/issues/3896. + type: string + type: object + x-kubernetes-map-type: atomic + enabled: + type: boolean + required: + - enabled + type: object + type: object mongodbResourceRef: properties: name: diff --git a/mongodb-community-operator/controllers/replica_set_controller.go b/mongodb-community-operator/controllers/replica_set_controller.go index 7bc38eb4e..9246e4ebb 100644 --- a/mongodb-community-operator/controllers/replica_set_controller.go +++ b/mongodb-community-operator/controllers/replica_set_controller.go @@ -89,6 +89,9 @@ func NewReconciler(mgr manager.Manager, mongodbRepoUrl, mongodbImage, mongodbIma func findMdbcForSearch(ctx context.Context, rawObj k8sClient.Object) []reconcile.Request { mdbSearch := rawObj.(*searchv1.MongoDBSearch) + if mdbSearch.GetMongoDBResourceRef() == nil { + return nil + } return []reconcile.Request{ {NamespacedName: types.NamespacedName{Namespace: mdbSearch.GetMongoDBResourceRef().Namespace, Name: mdbSearch.GetMongoDBResourceRef().Name}}, } @@ -713,7 +716,7 @@ func (r ReplicaSetReconciler) buildAutomationConfig(ctx context.Context, mdb mdb // for the mongod automation config. if len(searchList.Items) == 1 { searchSource := search_controller.NewSearchSourceDBResourceFromMongoDBCommunity(&mdb) - if search_controller.ValidateSearchSource(searchSource) == nil { + if searchSource.ValidateMongoDBVersion() == nil { search = &searchList.Items[0] } } @@ -834,7 +837,8 @@ func getMongodConfigModification(mdb mdbv1.MongoDBCommunity) automationconfig.Mo // getMongodConfigModification will merge the additional configuration in the CRD // into the configuration set up by the operator. func getMongodConfigSearchModification(search *searchv1.MongoDBSearch) automationconfig.Modification { - if search == nil { + // Condition for skipping add parameter if it is external mongod + if search == nil || search.IsExternalMongoDBSource() { return automationconfig.NOOP() } diff --git a/public/crds.yaml b/public/crds.yaml index f1683dd5f..4a63814d0 100644 --- a/public/crds.yaml +++ b/public/crds.yaml @@ -4212,6 +4212,51 @@ spec: type: object source: properties: + external: + properties: + hostAndPorts: + items: + type: string + type: array + keyFileSecretRef: + description: |- + SecretKeyRef is a reference to a value in a given secret in the same + namespace. Based on: + https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.15/#secretkeyselector-v1-core + properties: + key: + type: string + name: + type: string + required: + - name + type: object + tls: + properties: + ca: + description: |- + LocalObjectReference contains enough information to let you locate the + referenced object inside the same namespace. + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + TODO: Add other useful fields. apiVersion, kind, uid? + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Drop `kubebuilder:default` when controller-gen doesn't need it https://github.com/kubernetes-sigs/kubebuilder/issues/3896. + type: string + type: object + x-kubernetes-map-type: atomic + enabled: + type: boolean + required: + - enabled + type: object + type: object mongodbResourceRef: properties: name: From 32a5c4241fa72c8ad34c3522928499781f23a2c6 Mon Sep 17 00:00:00 2001 From: Yavor Georgiev Date: Tue, 2 Sep 2025 15:57:46 +0200 Subject: [PATCH 11/13] CLOUDP-342325 Basic enterprise search support (#309) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # Summary ## Proof of Work ## Checklist - [x] Have you linked a jira ticket and/or is the ticket in the title? - [x] Have you checked whether your jira ticket required DOCSP changes? - [x] Have you added changelog file? - use `skip-changelog` label if not needed - refer to [Changelog files and Release Notes](https://github.com/mongodb/mongodb-kubernetes/blob/master/CONTRIBUTING.md#changelog-files-and-release-notes) section in CONTRIBUTING.md for more details --------- Co-authored-by: Łukasz Sierant --- .evergreen-tasks.yml | 6 + .evergreen.yml | 2 + .../operator/mongodbreplicaset_controller.go | 100 +++++- .../operator/mongodbsearch_controller.go | 53 +++- .../operator/mongodbsearch_controller_test.go | 19 +- .../operator/watch/config_change_handler.go | 12 +- .../community_search_source.go | 65 ++++ .../community_search_source_test.go | 208 +++++++++++++ .../enterprise_search_source.go | 78 +++++ .../enterprise_search_source_test.go | 290 ++++++++++++++++++ .../external_search_source.go | 46 +++ .../mongodbsearch_reconcile_helper.go | 87 +++++- .../mongodbsearch_reconcile_helper_test.go | 61 +--- .../search_controller/search_construction.go | 113 +------ .../tests/common/search/search_tester.py | 2 +- .../enterprise-replicaset-sample-mflix.yaml | 34 ++ .../fixtures/mongodbuser-mdb-admin.yaml | 16 + .../search/fixtures/mongodbuser-mdb-user.yaml | 16 + .../mongodbuser-search-sync-source-user.yaml | 18 ++ .../tests/search/search_enterprise_basic.py | 183 +++++++++++ .../controllers/replica_set_controller.go | 4 +- scripts/dev/contexts/e2e_mdb_community | 9 +- scripts/dev/contexts/e2e_mdb_kind_ubi_cloudqa | 2 + .../contexts/e2e_static_mdb_kind_ubi_cloudqa | 2 + .../dev/contexts/variables/mongodb_search_dev | 13 + 25 files changed, 1217 insertions(+), 222 deletions(-) create mode 100644 controllers/search_controller/community_search_source.go create mode 100644 controllers/search_controller/community_search_source_test.go create mode 100644 controllers/search_controller/enterprise_search_source.go create mode 100644 controllers/search_controller/enterprise_search_source_test.go create mode 100644 controllers/search_controller/external_search_source.go create mode 100644 docker/mongodb-kubernetes-tests/tests/search/fixtures/enterprise-replicaset-sample-mflix.yaml create mode 100644 docker/mongodb-kubernetes-tests/tests/search/fixtures/mongodbuser-mdb-admin.yaml create mode 100644 docker/mongodb-kubernetes-tests/tests/search/fixtures/mongodbuser-mdb-user.yaml create mode 100644 docker/mongodb-kubernetes-tests/tests/search/fixtures/mongodbuser-search-sync-source-user.yaml create mode 100644 docker/mongodb-kubernetes-tests/tests/search/search_enterprise_basic.py create mode 100644 scripts/dev/contexts/variables/mongodb_search_dev diff --git a/.evergreen-tasks.yml b/.evergreen-tasks.yml index 01305de88..94fd8c8c0 100644 --- a/.evergreen-tasks.yml +++ b/.evergreen-tasks.yml @@ -1300,3 +1300,9 @@ tasks: tags: [ "patch-run" ] commands: - func: "e2e_test" + + - name: e2e_search_enterprise_basic + tags: [ "patch-run" ] + commands: + - func: "e2e_test" + diff --git a/.evergreen.yml b/.evergreen.yml index 2ad77df72..a8cfeb53a 100644 --- a/.evergreen.yml +++ b/.evergreen.yml @@ -815,6 +815,8 @@ task_groups: - e2e_replica_set_oidc_workforce - e2e_sharded_cluster_oidc_m2m_group - e2e_sharded_cluster_oidc_m2m_user + # MongoDBSearch test group + - e2e_search_enterprise_basic <<: *teardown_group # this task group contains just a one task, which is smoke testing whether the operator diff --git a/controllers/operator/mongodbreplicaset_controller.go b/controllers/operator/mongodbreplicaset_controller.go index a7d2943aa..36c8c5fcd 100644 --- a/controllers/operator/mongodbreplicaset_controller.go +++ b/controllers/operator/mongodbreplicaset_controller.go @@ -4,12 +4,16 @@ import ( "context" "fmt" + "github.com/blang/semver" "go.uber.org/zap" "golang.org/x/xerrors" "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/fields" "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/controller" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" "sigs.k8s.io/controller-runtime/pkg/event" "sigs.k8s.io/controller-runtime/pkg/handler" "sigs.k8s.io/controller-runtime/pkg/manager" @@ -22,6 +26,7 @@ import ( mdbv1 "github.com/mongodb/mongodb-kubernetes/api/v1/mdb" rolev1 "github.com/mongodb/mongodb-kubernetes/api/v1/role" + searchv1 "github.com/mongodb/mongodb-kubernetes/api/v1/search" mdbstatus "github.com/mongodb/mongodb-kubernetes/api/v1/status" "github.com/mongodb/mongodb-kubernetes/controllers/om" "github.com/mongodb/mongodb-kubernetes/controllers/om/backup" @@ -39,6 +44,7 @@ import ( "github.com/mongodb/mongodb-kubernetes/controllers/operator/recovery" "github.com/mongodb/mongodb-kubernetes/controllers/operator/watch" "github.com/mongodb/mongodb-kubernetes/controllers/operator/workflow" + "github.com/mongodb/mongodb-kubernetes/controllers/search_controller" mcoConstruct "github.com/mongodb/mongodb-kubernetes/mongodb-community-operator/controllers/construct" "github.com/mongodb/mongodb-kubernetes/mongodb-community-operator/pkg/kube/annotations" "github.com/mongodb/mongodb-kubernetes/mongodb-community-operator/pkg/kube/configmap" @@ -52,6 +58,7 @@ import ( "github.com/mongodb/mongodb-kubernetes/pkg/util/architectures" "github.com/mongodb/mongodb-kubernetes/pkg/util/env" util_int "github.com/mongodb/mongodb-kubernetes/pkg/util/int" + "github.com/mongodb/mongodb-kubernetes/pkg/util/maputil" "github.com/mongodb/mongodb-kubernetes/pkg/vault" "github.com/mongodb/mongodb-kubernetes/pkg/vault/vaultwatcher" ) @@ -219,6 +226,8 @@ func (r *ReconcileMongoDbReplicaSet) Reconcile(ctx context.Context, request reco return r.updateStatus(ctx, rs, workflow.Failed(xerrors.Errorf("Failed to reconcileHostnameOverrideConfigMap: %w", err)), log) } + shouldMirrorKeyfile := r.applySearchOverrides(ctx, rs, log) + sts := construct.DatabaseStatefulSet(*rs, rsConfig, log) if status := r.ensureRoles(ctx, rs.Spec.DbCommonSpec, r.enableClusterMongoDBRoles, conn, kube.ObjectKeyFromApiObject(rs), log); !status.IsOK() { return r.updateStatus(ctx, rs, status, log) @@ -238,7 +247,7 @@ func (r *ReconcileMongoDbReplicaSet) Reconcile(ctx context.Context, request reco // See CLOUDP-189433 and CLOUDP-229222 for more details. if recovery.ShouldTriggerRecovery(rs.Status.Phase != mdbstatus.PhaseRunning, rs.Status.LastTransition) { log.Warnf("Triggering Automatic Recovery. The MongoDB resource %s/%s is in %s state since %s", rs.Namespace, rs.Name, rs.Status.Phase, rs.Status.LastTransition) - automationConfigStatus := r.updateOmDeploymentRs(ctx, conn, rs.Status.Members, rs, sts, log, caFilePath, agentCertSecretSelector, prometheusCertHash, true).OnErrorPrepend("Failed to create/update (Ops Manager reconciliation phase):") + automationConfigStatus := r.updateOmDeploymentRs(ctx, conn, rs.Status.Members, rs, sts, log, caFilePath, agentCertSecretSelector, prometheusCertHash, true, shouldMirrorKeyfile).OnErrorPrepend("Failed to create/update (Ops Manager reconciliation phase):") deploymentError := create.DatabaseInKubernetes(ctx, r.client, *rs, sts, rsConfig, log) if deploymentError != nil { log.Errorf("Recovery failed because of deployment errors, %w", deploymentError) @@ -254,7 +263,7 @@ func (r *ReconcileMongoDbReplicaSet) Reconcile(ctx context.Context, request reco } status = workflow.RunInGivenOrder(publishAutomationConfigFirst(ctx, r.client, *rs, lastSpec, rsConfig, log), func() workflow.Status { - return r.updateOmDeploymentRs(ctx, conn, rs.Status.Members, rs, sts, log, caFilePath, agentCertSecretSelector, prometheusCertHash, false).OnErrorPrepend("Failed to create/update (Ops Manager reconciliation phase):") + return r.updateOmDeploymentRs(ctx, conn, rs.Status.Members, rs, sts, log, caFilePath, agentCertSecretSelector, prometheusCertHash, false, shouldMirrorKeyfile).OnErrorPrepend("Failed to create/update (Ops Manager reconciliation phase):") }, func() workflow.Status { workflowStatus := create.HandlePVCResize(ctx, r.client, &sts, log) @@ -408,6 +417,19 @@ func AddReplicaSetController(ctx context.Context, mgr manager.Manager, imageUrls zap.S().Errorf("Failed to watch for vault secret changes: %w", err) } } + + err = c.Watch(source.Kind(mgr.GetCache(), &searchv1.MongoDBSearch{}, + handler.TypedEnqueueRequestsFromMapFunc(func(ctx context.Context, search *searchv1.MongoDBSearch) []reconcile.Request { + source := search.GetMongoDBResourceRef() + if source == nil { + return []reconcile.Request{} + } + return []reconcile.Request{{NamespacedName: types.NamespacedName{Namespace: source.Namespace, Name: source.Name}}} + }))) + if err != nil { + return err + } + zap.S().Infof("Registered controller %s", util.MongoDbReplicaSetController) return nil @@ -415,7 +437,7 @@ func AddReplicaSetController(ctx context.Context, mgr manager.Manager, imageUrls // updateOmDeploymentRs performs OM registration operation for the replicaset. So the changes will be finally propagated // to automation agents in containers -func (r *ReconcileMongoDbReplicaSet) updateOmDeploymentRs(ctx context.Context, conn om.Connection, membersNumberBefore int, rs *mdbv1.MongoDB, set appsv1.StatefulSet, log *zap.SugaredLogger, caFilePath string, agentCertSecretSelector corev1.SecretKeySelector, prometheusCertHash string, isRecovering bool) workflow.Status { +func (r *ReconcileMongoDbReplicaSet) updateOmDeploymentRs(ctx context.Context, conn om.Connection, membersNumberBefore int, rs *mdbv1.MongoDB, set appsv1.StatefulSet, log *zap.SugaredLogger, caFilePath string, agentCertSecretSelector corev1.SecretKeySelector, prometheusCertHash string, isRecovering bool, shouldMirrorKeyfileForMongot bool) workflow.Status { log.Debug("Entering UpdateOMDeployments") // Only "concrete" RS members should be observed // - if scaling down, let's observe only members that will remain after scale-down operation @@ -469,6 +491,11 @@ func (r *ReconcileMongoDbReplicaSet) updateOmDeploymentRs(ctx context.Context, c err = conn.ReadUpdateDeployment( func(d om.Deployment) error { + if shouldMirrorKeyfileForMongot { + if err := r.mirrorKeyfileIntoSecretForMongot(ctx, d, rs, log); err != nil { + return err + } + } return ReconcileReplicaSetAC(ctx, d, rs.Spec.DbCommonSpec, lastRsConfig.ToMap(), rs.Name, replicaSet, caFilePath, internalClusterPath, &p, log) }, log, @@ -609,3 +636,70 @@ func getAllHostsRs(set appsv1.StatefulSet, clusterName string, membersCount int, hostnames, _ := dns.GetDnsForStatefulSetReplicasSpecified(set, clusterName, membersCount, externalDomain) return hostnames } + +func (r *ReconcileMongoDbReplicaSet) applySearchOverrides(ctx context.Context, rs *mdbv1.MongoDB, log *zap.SugaredLogger) bool { + search := r.lookupCorrespondingSearchResource(ctx, rs, log) + if search == nil { + log.Debugf("No MongoDBSearch resource found, skipping search overrides") + return false + } + + log.Infof("Applying search overrides from MongoDBSearch %s", search.NamespacedName()) + + if rs.Spec.AdditionalMongodConfig == nil { + rs.Spec.AdditionalMongodConfig = mdbv1.NewEmptyAdditionalMongodConfig() + } + searchMongodConfig := search_controller.GetMongodConfigParameters(search) + rs.Spec.AdditionalMongodConfig.AddOption("setParameter", searchMongodConfig["setParameter"]) + + mdbVersion, err := semver.ParseTolerant(rs.Spec.Version) + if err != nil { + log.Warnf("Failed to parse MongoDB version %q: %w. Proceeding without the automatic creation of the searchCoordinator role that's necessary for MongoDB <8.2", rs.Spec.Version, err) + } else if semver.MustParse("8.2.0").GT(mdbVersion) { + log.Infof("Polyfilling the searchCoordinator role for MongoDB %s", rs.Spec.Version) + + if rs.Spec.Security == nil { + rs.Spec.Security = &mdbv1.Security{} + } + rs.Spec.Security.Roles = append(rs.Spec.Security.Roles, search_controller.SearchCoordinatorRole()) + } + + return true +} + +func (r *ReconcileMongoDbReplicaSet) mirrorKeyfileIntoSecretForMongot(ctx context.Context, d om.Deployment, rs *mdbv1.MongoDB, log *zap.SugaredLogger) error { + keyfileContents := maputil.ReadMapValueAsString(d, "auth", "key") + keyfileSecret := &corev1.Secret{ObjectMeta: metav1.ObjectMeta{Name: fmt.Sprintf("%s-keyfile", rs.Name), Namespace: rs.Namespace}} + + log.Infof("Mirroring the replicaset %s's keyfile into the secret %s", rs.ObjectKey(), kube.ObjectKeyFromApiObject(keyfileSecret)) + + _, err := controllerutil.CreateOrUpdate(ctx, r.client, keyfileSecret, func() error { + keyfileSecret.StringData = map[string]string{"keyfile": keyfileContents} + return controllerutil.SetOwnerReference(rs, keyfileSecret, r.client.Scheme()) + }) + if err != nil { + return xerrors.Errorf("Failed to mirror the replicaset's keyfile into a secret: %w", err) + } else { + return nil + } +} + +func (r *ReconcileMongoDbReplicaSet) lookupCorrespondingSearchResource(ctx context.Context, rs *mdbv1.MongoDB, log *zap.SugaredLogger) *searchv1.MongoDBSearch { + var search *searchv1.MongoDBSearch + searchList := &searchv1.MongoDBSearchList{} + if err := r.client.List(ctx, searchList, &client.ListOptions{ + FieldSelector: fields.OneTermEqualSelector(search_controller.MongoDBSearchIndexFieldName, rs.GetNamespace()+"/"+rs.GetName()), + }); err != nil { + log.Debugf("Failed to list MongoDBSearch resources: %v", err) + } + // this validates that there is exactly one MongoDBSearch pointing to this resource, + // and that this resource passes search validations. If either fails, proceed without a search target + // for the mongod automation config. + if len(searchList.Items) == 1 { + searchSource := search_controller.NewEnterpriseResourceSearchSource(rs) + if searchSource.Validate() == nil { + search = &searchList.Items[0] + } + } + return search +} diff --git a/controllers/operator/mongodbsearch_controller.go b/controllers/operator/mongodbsearch_controller.go index b83791f1f..7d47f496f 100644 --- a/controllers/operator/mongodbsearch_controller.go +++ b/controllers/operator/mongodbsearch_controller.go @@ -14,12 +14,14 @@ import ( appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" ctrl "sigs.k8s.io/controller-runtime" + mdbv1 "github.com/mongodb/mongodb-kubernetes/api/v1/mdb" searchv1 "github.com/mongodb/mongodb-kubernetes/api/v1/search" + "github.com/mongodb/mongodb-kubernetes/controllers/operator/watch" "github.com/mongodb/mongodb-kubernetes/controllers/search_controller" mdbcv1 "github.com/mongodb/mongodb-kubernetes/mongodb-community-operator/api/v1" - "github.com/mongodb/mongodb-kubernetes/mongodb-community-operator/controllers/watch" kubernetesClient "github.com/mongodb/mongodb-kubernetes/mongodb-community-operator/pkg/kube/client" "github.com/mongodb/mongodb-kubernetes/pkg/kube/commoncontroller" "github.com/mongodb/mongodb-kubernetes/pkg/util" @@ -28,15 +30,14 @@ import ( type MongoDBSearchReconciler struct { kubeClient kubernetesClient.Client - mdbcWatcher *watch.ResourceWatcher + watch *watch.ResourceWatcher operatorSearchConfig search_controller.OperatorSearchConfig } func newMongoDBSearchReconciler(client client.Client, operatorSearchConfig search_controller.OperatorSearchConfig) *MongoDBSearchReconciler { - mdbcWatcher := watch.New() return &MongoDBSearchReconciler{ kubeClient: kubernetesClient.NewClient(client), - mdbcWatcher: &mdbcWatcher, + watch: watch.NewResourceWatcher(), operatorSearchConfig: operatorSearchConfig, } } @@ -51,36 +52,52 @@ func (r *MongoDBSearchReconciler) Reconcile(ctx context.Context, request reconci return result, err } - sourceResource, mdbc, err := getSourceMongoDBForSearch(ctx, r.kubeClient, mdbSearch) + searchSource, err := r.getSourceMongoDBForSearch(ctx, r.kubeClient, mdbSearch, log) if err != nil { return reconcile.Result{RequeueAfter: time.Second * util.RetryTimeSec}, err } - if mdbc != nil { - r.mdbcWatcher.Watch(ctx, mdbc.NamespacedName(), request.NamespacedName) - } + r.watch.AddWatchedResourceIfNotAdded(searchSource.KeyfileSecretName(), mdbSearch.Namespace, watch.Secret, mdbSearch.NamespacedName()) - reconcileHelper := search_controller.NewMongoDBSearchReconcileHelper(kubernetesClient.NewClient(r.kubeClient), mdbSearch, sourceResource, r.operatorSearchConfig) + reconcileHelper := search_controller.NewMongoDBSearchReconcileHelper(kubernetesClient.NewClient(r.kubeClient), mdbSearch, searchSource, r.operatorSearchConfig) return reconcileHelper.Reconcile(ctx, log).ReconcileResult() } -func getSourceMongoDBForSearch(ctx context.Context, kubeClient client.Client, search *searchv1.MongoDBSearch) (search_controller.SearchSourceDBResource, *mdbcv1.MongoDBCommunity, error) { +func (r *MongoDBSearchReconciler) getSourceMongoDBForSearch(ctx context.Context, kubeClient client.Client, search *searchv1.MongoDBSearch, log *zap.SugaredLogger) (search_controller.SearchSourceDBResource, error) { if search.IsExternalMongoDBSource() { - return search_controller.NewSearchSourceDBResourceFromExternal(search.Namespace, search.Spec.Source.ExternalMongoDBSource), nil, nil + return search_controller.NewExternalSearchSource(search.Namespace, search.Spec.Source.ExternalMongoDBSource), nil } sourceMongoDBResourceRef := search.GetMongoDBResourceRef() if sourceMongoDBResourceRef == nil { - return nil, nil, xerrors.New("MongoDBSearch source MongoDB resource reference is not set") + return nil, xerrors.New("MongoDBSearch source MongoDB resource reference is not set") + } + + sourceName := types.NamespacedName{Namespace: search.GetNamespace(), Name: sourceMongoDBResourceRef.Name} + log.Infof("Looking up Search source %s", sourceName) + + mdb := &mdbv1.MongoDB{} + if err := kubeClient.Get(ctx, sourceName, mdb); err != nil { + if !apierrors.IsNotFound(err) { + return nil, xerrors.Errorf("error getting MongoDB %s: %w", sourceName, err) + } + } else { + r.watch.AddWatchedResourceIfNotAdded(sourceMongoDBResourceRef.Name, sourceMongoDBResourceRef.Namespace, watch.MongoDB, search.NamespacedName()) + return search_controller.NewEnterpriseResourceSearchSource(mdb), nil } - mdbcName := types.NamespacedName{Namespace: search.GetNamespace(), Name: sourceMongoDBResourceRef.Name} mdbc := &mdbcv1.MongoDBCommunity{} - if err := kubeClient.Get(ctx, mdbcName, mdbc); err != nil { - return nil, nil, xerrors.Errorf("error getting MongoDBCommunity %s: %w", mdbcName, err) + if err := kubeClient.Get(ctx, sourceName, mdbc); err != nil { + if !apierrors.IsNotFound(err) { + return nil, xerrors.Errorf("error getting MongoDBCommunity %s: %w", sourceName, err) + } + } else { + r.watch.AddWatchedResourceIfNotAdded(sourceMongoDBResourceRef.Name, sourceMongoDBResourceRef.Namespace, "MongoDBCommunity", search.NamespacedName()) + return search_controller.NewCommunityResourceSearchSource(mdbc), nil } - return search_controller.NewSearchSourceDBResourceFromMongoDBCommunity(mdbc), mdbc, nil + + return nil, xerrors.Errorf("No database resource named %s found", sourceName) } func mdbcSearchIndexBuilder(rawObj client.Object) []string { @@ -103,7 +120,9 @@ func AddMongoDBSearchController(ctx context.Context, mgr manager.Manager, operat return ctrl.NewControllerManagedBy(mgr). WithOptions(controller.Options{MaxConcurrentReconciles: env.ReadIntOrDefault(util.MaxConcurrentReconcilesEnv, 1)}). // nolint:forbidigo For(&searchv1.MongoDBSearch{}). - Watches(&mdbcv1.MongoDBCommunity{}, r.mdbcWatcher). + Watches(&mdbv1.MongoDB{}, &watch.ResourcesHandler{ResourceType: watch.MongoDB, ResourceWatcher: r.watch}). + Watches(&mdbcv1.MongoDBCommunity{}, &watch.ResourcesHandler{ResourceType: "MongoDBCommunity", ResourceWatcher: r.watch}). + Watches(&corev1.Secret{}, &watch.ResourcesHandler{ResourceType: watch.Secret, ResourceWatcher: r.watch}). Owns(&appsv1.StatefulSet{}). Owns(&corev1.Secret{}). Complete(r) diff --git a/controllers/operator/mongodbsearch_controller_test.go b/controllers/operator/mongodbsearch_controller_test.go index ba3885042..28f966ff8 100644 --- a/controllers/operator/mongodbsearch_controller_test.go +++ b/controllers/operator/mongodbsearch_controller_test.go @@ -8,11 +8,8 @@ import ( "github.com/ghodss/yaml" "github.com/stretchr/testify/assert" "k8s.io/apimachinery/pkg/types" - "k8s.io/client-go/util/workqueue" "k8s.io/utils/ptr" "sigs.k8s.io/controller-runtime/pkg/client" - "sigs.k8s.io/controller-runtime/pkg/controller/controllertest" - "sigs.k8s.io/controller-runtime/pkg/event" "sigs.k8s.io/controller-runtime/pkg/reconcile" appsv1 "k8s.io/api/apps/v1" @@ -29,6 +26,7 @@ import ( mdbcv1 "github.com/mongodb/mongodb-kubernetes/mongodb-community-operator/api/v1" "github.com/mongodb/mongodb-kubernetes/mongodb-community-operator/api/v1/common" "github.com/mongodb/mongodb-kubernetes/mongodb-community-operator/pkg/mongot" + "github.com/mongodb/mongodb-kubernetes/mongodb-community-operator/pkg/util/constants" ) func newMongoDBCommunity(name, namespace string) *mdbcv1.MongoDBCommunity { @@ -62,7 +60,16 @@ func newSearchReconcilerWithOperatorConfig( builder.WithIndex(&searchv1.MongoDBSearch{}, search_controller.MongoDBSearchIndexFieldName, mdbcSearchIndexBuilder) if mdbc != nil { - builder.WithObjects(mdbc) + keyfileSecret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: mdbc.GetAgentKeyfileSecretNamespacedName().Name, + Namespace: mdbc.Namespace, + }, + StringData: map[string]string{ + constants.AgentKeyfileKey: "keyfile", + }, + } + builder.WithObjects(mdbc, keyfileSecret) } for _, search := range searches { @@ -183,10 +190,6 @@ func TestMongoDBSearchReconcile_Success(t *testing.T) { sts := &appsv1.StatefulSet{} err = c.Get(ctx, search.StatefulSetNamespacedName(), sts) assert.NoError(t, err) - - queue := controllertest.Queue{Interface: workqueue.New()} - reconciler.mdbcWatcher.Create(ctx, event.CreateEvent{Object: mdbc}, &queue) - assert.Equal(t, 1, queue.Len()) } func checkSearchReconcileFailed( diff --git a/controllers/operator/watch/config_change_handler.go b/controllers/operator/watch/config_change_handler.go index b5c36c364..cb6122859 100644 --- a/controllers/operator/watch/config_change_handler.go +++ b/controllers/operator/watch/config_change_handler.go @@ -14,8 +14,6 @@ import ( "sigs.k8s.io/controller-runtime/pkg/reconcile" corev1 "k8s.io/api/core/v1" - - rolev1 "github.com/mongodb/mongodb-kubernetes/api/v1/role" ) // Type is an enum for all kubernetes types watched by controller for changes for configuration @@ -87,10 +85,14 @@ func (c *ResourcesHandler) doHandle(namespace, name string, q workqueue.RateLimi // Seems we don't need to react on config map/secret removal.. func (c *ResourcesHandler) Delete(ctx context.Context, e event.DeleteEvent, q workqueue.RateLimitingInterface) { - switch v := e.Object.(type) { - case *rolev1.ClusterMongoDBRole: - c.doHandle(v.GetNamespace(), v.GetName(), q) + switch e.Object.(type) { + case *corev1.ConfigMap: + return + case *corev1.Secret: + return } + + c.doHandle(e.Object.GetNamespace(), e.Object.GetName(), q) } func (c *ResourcesHandler) Generic(ctx context.Context, _ event.GenericEvent, _ workqueue.RateLimitingInterface) { diff --git a/controllers/search_controller/community_search_source.go b/controllers/search_controller/community_search_source.go new file mode 100644 index 000000000..e4f799c83 --- /dev/null +++ b/controllers/search_controller/community_search_source.go @@ -0,0 +1,65 @@ +package search_controller + +import ( + "fmt" + "strings" + + "github.com/blang/semver" + "golang.org/x/xerrors" + "k8s.io/apimachinery/pkg/types" + + mdbcv1 "github.com/mongodb/mongodb-kubernetes/mongodb-community-operator/api/v1" + "github.com/mongodb/mongodb-kubernetes/pkg/util" +) + +func NewCommunityResourceSearchSource(mdbc *mdbcv1.MongoDBCommunity) SearchSourceDBResource { + return &CommunitySearchSource{MongoDBCommunity: mdbc} +} + +type CommunitySearchSource struct { + *mdbcv1.MongoDBCommunity +} + +func (r *CommunitySearchSource) HostSeeds() []string { + seeds := make([]string, r.Spec.Members) + for i := range seeds { + seeds[i] = fmt.Sprintf("%s-%d.%s.%s.svc.cluster.local:%d", r.Name, i, r.ServiceName(), r.Namespace, r.GetMongodConfiguration().GetDBPort()) + } + return seeds +} + +func (r *CommunitySearchSource) KeyfileSecretName() string { + return r.MongoDBCommunity.GetAgentKeyfileSecretNamespacedName().Name +} + +func (r *CommunitySearchSource) IsSecurityTLSConfigEnabled() bool { + return r.Spec.Security.TLS.Enabled +} + +func (r *CommunitySearchSource) TLSOperatorCASecretNamespacedName() types.NamespacedName { + return r.MongoDBCommunity.TLSOperatorCASecretNamespacedName() +} + +func (r *CommunitySearchSource) Validate() error { + version, err := semver.ParseTolerant(r.GetMongoDBVersion()) + if err != nil { + return xerrors.Errorf("error parsing MongoDB version '%s': %w", r.Spec.Version, err) + } else if version.LT(semver.MustParse("8.0.10")) { + return xerrors.New("MongoDB version must be 8.0.10 or higher") + } + + foundScram := false + for _, authMode := range r.Spec.Security.Authentication.Modes { + // Check for SCRAM, SCRAM-SHA-1, or SCRAM-SHA-256 + if strings.HasPrefix(strings.ToUpper(string(authMode)), util.SCRAM) { + foundScram = true + break + } + } + + if !foundScram && len(r.Spec.Security.Authentication.Modes) > 0 { + return xerrors.New("MongoDBSearch requires SCRAM authentication to be enabled") + } + + return nil +} diff --git a/controllers/search_controller/community_search_source_test.go b/controllers/search_controller/community_search_source_test.go new file mode 100644 index 000000000..90bf9967e --- /dev/null +++ b/controllers/search_controller/community_search_source_test.go @@ -0,0 +1,208 @@ +package search_controller + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + mdbcv1 "github.com/mongodb/mongodb-kubernetes/mongodb-community-operator/api/v1" +) + +func newCommunitySearchSource(version string, authModes []mdbcv1.AuthMode) *CommunitySearchSource { + return &CommunitySearchSource{ + MongoDBCommunity: &mdbcv1.MongoDBCommunity{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-mongodb", + Namespace: "test-namespace", + }, + Spec: mdbcv1.MongoDBCommunitySpec{ + Version: version, + Security: mdbcv1.Security{ + Authentication: mdbcv1.Authentication{ + Modes: authModes, + }, + }, + }, + }, + } +} + +func TestCommunitySearchSource_Validate(t *testing.T) { + cases := []struct { + name string + version string + authModes []mdbcv1.AuthMode + expectError bool + expectedErrMsg string + }{ + // Version validation tests + { + name: "Invalid version", + version: "invalid.version", + authModes: []mdbcv1.AuthMode{"SCRAM-SHA-256"}, + expectError: true, + expectedErrMsg: "error parsing MongoDB version", + }, + { + name: "Version too old", + version: "7.0.0", + authModes: []mdbcv1.AuthMode{"SCRAM-SHA-256"}, + expectError: true, + expectedErrMsg: "MongoDB version must be 8.0.10 or higher", + }, + { + name: "Version just below minimum", + version: "8.0.9", + authModes: []mdbcv1.AuthMode{"SCRAM-SHA-256"}, + expectError: true, + expectedErrMsg: "MongoDB version must be 8.0.10 or higher", + }, + { + name: "Valid minimum version", + version: "8.0.10", + authModes: []mdbcv1.AuthMode{"SCRAM-SHA-256"}, + expectError: false, + }, + { + name: "Version above minimum", + version: "8.1.0", + authModes: []mdbcv1.AuthMode{"SCRAM-SHA-256"}, + expectError: false, + }, + { + name: "Version with build number", + version: "8.1.0-rc1", + authModes: []mdbcv1.AuthMode{"SCRAM-SHA-256"}, + expectError: false, + }, + // Authentication mode tests - empty/nil cases + { + name: "Empty auth modes", + version: "8.0.10", + authModes: []mdbcv1.AuthMode{}, + expectError: false, + }, + { + name: "Nil auth modes", + version: "8.0.10", + authModes: nil, + expectError: false, + }, + { + name: "X509 mode only", + version: "8.0.10", + authModes: []mdbcv1.AuthMode{"X509"}, + expectError: true, + expectedErrMsg: "MongoDBSearch requires SCRAM authentication to be enabled", + }, + { + name: "X509 and SCRAM", + version: "8.0.10", + authModes: []mdbcv1.AuthMode{"X509", "SCRAM-SHA-256"}, + expectError: false, + }, + { + name: "Multiple auth modes with SCRAM first", + version: "8.0.10", + authModes: []mdbcv1.AuthMode{"SCRAM-SHA-1", "X509"}, + expectError: false, + }, + { + name: "Multiple auth modes with SCRAM last", + version: "8.0.10", + authModes: []mdbcv1.AuthMode{"PLAIN", "X509", "SCRAM-SHA-256"}, + expectError: false, + }, + { + name: "Multiple non-SCRAM modes", + version: "8.0.10", + authModes: []mdbcv1.AuthMode{"PLAIN", "X509"}, + expectError: true, + expectedErrMsg: "MongoDBSearch requires SCRAM authentication to be enabled", + }, + // SCRAM variant tests + { + name: "SCRAM only", + version: "8.0.10", + authModes: []mdbcv1.AuthMode{"SCRAM"}, + expectError: false, + }, + { + name: "SCRAM-SHA-1 only", + version: "8.0.10", + authModes: []mdbcv1.AuthMode{"SCRAM-SHA-1"}, + expectError: false, + }, + { + name: "SCRAM-SHA-256 only", + version: "8.0.10", + authModes: []mdbcv1.AuthMode{"SCRAM-SHA-256"}, + expectError: false, + }, + { + name: "All SCRAM variants", + version: "8.0.10", + authModes: []mdbcv1.AuthMode{"SCRAM", "SCRAM-SHA-1", "SCRAM-SHA-256"}, + expectError: false, + }, + // Case-insensitive tests (now supported with ToUpper) + { + name: "Lowercase SCRAM", + version: "8.0.10", + authModes: []mdbcv1.AuthMode{"scram-sha-256"}, + expectError: false, + }, + { + name: "Mixed case SCRAM", + version: "8.0.10", + authModes: []mdbcv1.AuthMode{"Scram-Sha-256"}, + expectError: false, + }, + // Edge case tests + { + name: "PLAIN only", + version: "8.0.10", + authModes: []mdbcv1.AuthMode{"PLAIN"}, + expectError: true, + expectedErrMsg: "MongoDBSearch requires SCRAM authentication to be enabled", + }, + // Combined validation tests + { + name: "Invalid version with valid auth", + version: "7.0.0", + authModes: []mdbcv1.AuthMode{"SCRAM-SHA-256"}, + expectError: true, + expectedErrMsg: "MongoDB version must be 8.0.10 or higher", + }, + { + name: "Valid version with invalid auth", + version: "8.0.10", + authModes: []mdbcv1.AuthMode{"X509"}, + expectError: true, + expectedErrMsg: "MongoDBSearch requires SCRAM authentication to be enabled", + }, + { + name: "Invalid version with invalid auth", + version: "7.0.0", + authModes: []mdbcv1.AuthMode{"X509"}, + expectError: true, + expectedErrMsg: "MongoDB version must be 8.0.10 or higher", // Should fail on version first + }, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + src := newCommunitySearchSource(c.version, c.authModes) + err := src.Validate() + + if c.expectError { + assert.Error(t, err) + assert.Contains(t, err.Error(), c.expectedErrMsg) + } else { + assert.NoError(t, err) + } + }) + } +} diff --git a/controllers/search_controller/enterprise_search_source.go b/controllers/search_controller/enterprise_search_source.go new file mode 100644 index 000000000..87ad09e5f --- /dev/null +++ b/controllers/search_controller/enterprise_search_source.go @@ -0,0 +1,78 @@ +package search_controller + +import ( + "fmt" + "strings" + + "github.com/blang/semver" + "golang.org/x/xerrors" + "k8s.io/apimachinery/pkg/types" + + mdbv1 "github.com/mongodb/mongodb-kubernetes/api/v1/mdb" + "github.com/mongodb/mongodb-kubernetes/pkg/util" +) + +type EnterpriseResourceSearchSource struct { + *mdbv1.MongoDB +} + +func NewEnterpriseResourceSearchSource(mdb *mdbv1.MongoDB) SearchSourceDBResource { + return EnterpriseResourceSearchSource{mdb} +} + +func (r EnterpriseResourceSearchSource) HostSeeds() []string { + seeds := make([]string, r.Spec.Members) + for i := range seeds { + seeds[i] = fmt.Sprintf("%s-%d.%s.%s.svc.cluster.local:%d", r.Name, i, r.ServiceName(), r.Namespace, r.Spec.GetAdditionalMongodConfig().GetPortOrDefault()) + } + return seeds +} + +func (r EnterpriseResourceSearchSource) KeyfileSecretName() string { + return fmt.Sprintf("%s-keyfile", r.Name) +} + +func (r EnterpriseResourceSearchSource) IsSecurityTLSConfigEnabled() bool { + return r.Spec.Security.IsTLSEnabled() +} + +func (r EnterpriseResourceSearchSource) TLSOperatorCASecretNamespacedName() types.NamespacedName { + return types.NamespacedName{} +} + +func (r EnterpriseResourceSearchSource) Validate() error { + version, err := semver.ParseTolerant(r.Spec.GetMongoDBVersion()) + if err != nil { + return xerrors.Errorf("error parsing MongoDB version '%s': %w", r.Spec.GetMongoDBVersion(), err) + } else if version.LT(semver.MustParse("8.0.10")) { + return xerrors.New("MongoDB version must be 8.0.10 or higher") + } + + if r.Spec.GetTopology() != mdbv1.ClusterTopologySingleCluster { + return xerrors.Errorf("MongoDBSearch is only supported for %s topology", mdbv1.ClusterTopologySingleCluster) + } + + if r.GetResourceType() != mdbv1.ReplicaSet { + return xerrors.Errorf("MongoDBSearch is only supported for %s resources", mdbv1.ReplicaSet) + } + + authModes := r.Spec.GetSecurityAuthenticationModes() + foundScram := false + for _, authMode := range authModes { + // Check for SCRAM, SCRAM-SHA-1, or SCRAM-SHA-256 + if strings.HasPrefix(strings.ToUpper(authMode), util.SCRAM) { + foundScram = true + break + } + } + + if !foundScram && len(authModes) > 0 { + return xerrors.New("MongoDBSearch requires SCRAM authentication to be enabled") + } + + if r.Spec.Security.GetInternalClusterAuthenticationMode() == util.X509 { + return xerrors.New("MongoDBSearch does not support X.509 internal cluster authentication") + } + + return nil +} diff --git a/controllers/search_controller/enterprise_search_source_test.go b/controllers/search_controller/enterprise_search_source_test.go new file mode 100644 index 000000000..1362ab605 --- /dev/null +++ b/controllers/search_controller/enterprise_search_source_test.go @@ -0,0 +1,290 @@ +package search_controller + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + mdbv1 "github.com/mongodb/mongodb-kubernetes/api/v1/mdb" +) + +func newEnterpriseSearchSource(version string, topology string, resourceType mdbv1.ResourceType, authModes []string, internalClusterAuth string) EnterpriseResourceSearchSource { + authModesList := make([]mdbv1.AuthMode, len(authModes)) + for i, mode := range authModes { + authModesList[i] = mdbv1.AuthMode(mode) + } + + // Create security with authentication if needed + var security *mdbv1.Security + if len(authModes) > 0 || internalClusterAuth != "" { + security = &mdbv1.Security{ + Authentication: &mdbv1.Authentication{ + Enabled: len(authModes) > 0, + Modes: authModesList, + InternalCluster: internalClusterAuth, + }, + } + } + + src := EnterpriseResourceSearchSource{ + MongoDB: &mdbv1.MongoDB{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-mongodb", + Namespace: "test-namespace", + }, + Spec: mdbv1.MongoDbSpec{ + DbCommonSpec: mdbv1.DbCommonSpec{ + Version: version, + ResourceType: resourceType, + Security: security, + }, + }, + }, + } + + // Set topology directly since it's inlined from DbCommonSpec + src.Spec.Topology = topology + return src +} + +func TestEnterpriseResourceSearchSource_Validate(t *testing.T) { + cases := []struct { + name string + version string + topology string + resourceType mdbv1.ResourceType + authModes []string + internalClusterAuth string + expectError bool + expectedErrMsg string + }{ + // Version validation tests + { + name: "Invalid version", + version: "invalid.version", + topology: mdbv1.ClusterTopologySingleCluster, + resourceType: mdbv1.ReplicaSet, + authModes: []string{}, + expectError: true, + expectedErrMsg: "error parsing MongoDB version", + }, + { + name: "Version too old", + version: "7.0.0", + topology: mdbv1.ClusterTopologySingleCluster, + resourceType: mdbv1.ReplicaSet, + authModes: []string{}, + expectError: true, + expectedErrMsg: "MongoDB version must be 8.0.10 or higher", + }, + { + name: "Version just below minimum", + version: "8.0.9", + topology: mdbv1.ClusterTopologySingleCluster, + resourceType: mdbv1.ReplicaSet, + authModes: []string{}, + expectError: true, + expectedErrMsg: "MongoDB version must be 8.0.10 or higher", + }, + { + name: "Valid minimum version", + version: "8.0.10", + topology: mdbv1.ClusterTopologySingleCluster, + resourceType: mdbv1.ReplicaSet, + authModes: []string{}, + expectError: false, + }, + { + name: "Version above minimum", + version: "8.1.0", + topology: mdbv1.ClusterTopologySingleCluster, + resourceType: mdbv1.ReplicaSet, + authModes: []string{}, + expectError: false, + }, + // Topology validation tests + { + name: "Invalid topology - MultiCluster", + version: "8.0.10", + topology: mdbv1.ClusterTopologyMultiCluster, + resourceType: mdbv1.ReplicaSet, + authModes: []string{}, + expectError: true, + expectedErrMsg: "MongoDBSearch is only supported for SingleCluster topology", + }, + { + name: "Valid topology - SingleCluster", + version: "8.0.10", + topology: mdbv1.ClusterTopologySingleCluster, + resourceType: mdbv1.ReplicaSet, + authModes: []string{}, + expectError: false, + }, + { + name: "Empty topology defaults to SingleCluster", + version: "8.0.10", + topology: "", + resourceType: mdbv1.ReplicaSet, + authModes: []string{}, + expectError: false, + }, + // Resource type validation tests + { + name: "Invalid resource type - Standalone", + version: "8.0.10", + topology: mdbv1.ClusterTopologySingleCluster, + resourceType: mdbv1.Standalone, + authModes: []string{}, + expectError: true, + expectedErrMsg: "MongoDBSearch is only supported for ReplicaSet resources", + }, + { + name: "Invalid resource type - ShardedCluster", + version: "8.0.10", + topology: mdbv1.ClusterTopologySingleCluster, + resourceType: mdbv1.ShardedCluster, + authModes: []string{}, + expectError: true, + expectedErrMsg: "MongoDBSearch is only supported for ReplicaSet resources", + }, + { + name: "Valid resource type - ReplicaSet", + version: "8.0.10", + topology: mdbv1.ClusterTopologySingleCluster, + resourceType: mdbv1.ReplicaSet, + authModes: []string{}, + expectError: false, + }, + // Authentication mode tests + { + name: "No SCRAM authentication", + version: "8.0.10", + topology: mdbv1.ClusterTopologySingleCluster, + resourceType: mdbv1.ReplicaSet, + authModes: []string{"X509"}, + expectError: true, + expectedErrMsg: "MongoDBSearch requires SCRAM authentication to be enabled", + }, + { + name: "Empty authentication modes", + version: "8.0.10", + topology: mdbv1.ClusterTopologySingleCluster, + resourceType: mdbv1.ReplicaSet, + authModes: []string{}, + expectError: false, + }, + { + name: "Nil authentication modes", + version: "8.0.10", + topology: mdbv1.ClusterTopologySingleCluster, + resourceType: mdbv1.ReplicaSet, + authModes: nil, + expectError: false, + }, + { + name: "Valid SCRAM authentication", + version: "8.0.10", + topology: mdbv1.ClusterTopologySingleCluster, + resourceType: mdbv1.ReplicaSet, + authModes: []string{"SCRAM-SHA-256"}, + expectError: false, + }, + { + name: "Mixed auth modes with SCRAM", + version: "8.0.10", + topology: mdbv1.ClusterTopologySingleCluster, + resourceType: mdbv1.ReplicaSet, + authModes: []string{"X509", "SCRAM-SHA-256"}, + expectError: false, + }, + { + name: "Case insensitive SCRAM", + version: "8.0.10", + topology: mdbv1.ClusterTopologySingleCluster, + resourceType: mdbv1.ReplicaSet, + authModes: []string{"scram-sha-256"}, + expectError: false, + }, + { + name: "SCRAM variants", + version: "8.0.10", + topology: mdbv1.ClusterTopologySingleCluster, + resourceType: mdbv1.ReplicaSet, + authModes: []string{"SCRAM", "SCRAM-SHA-1", "SCRAM-SHA-256"}, + expectError: false, + }, + // Internal cluster authentication tests + { + name: "X509 internal cluster auth not supported", + version: "8.0.10", + topology: mdbv1.ClusterTopologySingleCluster, + resourceType: mdbv1.ReplicaSet, + authModes: []string{"SCRAM-SHA-256"}, + internalClusterAuth: "X509", + expectError: true, + expectedErrMsg: "MongoDBSearch does not support X.509 internal cluster authentication", + }, + { + name: "Valid internal cluster auth - empty", + version: "8.0.10", + topology: mdbv1.ClusterTopologySingleCluster, + resourceType: mdbv1.ReplicaSet, + authModes: []string{"SCRAM-SHA-256"}, + internalClusterAuth: "", + expectError: false, + }, + { + name: "Valid internal cluster auth - SCRAM", + version: "8.0.10", + topology: mdbv1.ClusterTopologySingleCluster, + resourceType: mdbv1.ReplicaSet, + authModes: []string{"SCRAM-SHA-256"}, + internalClusterAuth: "SCRAM", + expectError: false, + }, + // Combined validation tests + { + name: "Multiple validation failures - version takes precedence", + version: "7.0.0", + topology: mdbv1.ClusterTopologyMultiCluster, + resourceType: mdbv1.Standalone, + authModes: []string{"X509"}, + expectError: true, + expectedErrMsg: "MongoDB version must be 8.0.10 or higher", + }, + { + name: "Valid version, invalid topology", + version: "8.0.10", + topology: mdbv1.ClusterTopologyMultiCluster, + resourceType: mdbv1.ReplicaSet, + authModes: []string{}, + expectError: true, + expectedErrMsg: "MongoDBSearch is only supported for SingleCluster topology", + }, + { + name: "Valid version and topology, invalid resource type", + version: "8.0.10", + topology: mdbv1.ClusterTopologySingleCluster, + resourceType: mdbv1.Standalone, + authModes: []string{}, + expectError: true, + expectedErrMsg: "MongoDBSearch is only supported for ReplicaSet resources", + }, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + src := newEnterpriseSearchSource(c.version, c.topology, c.resourceType, c.authModes, c.internalClusterAuth) + err := src.Validate() + + if c.expectError { + assert.Error(t, err) + assert.Contains(t, err.Error(), c.expectedErrMsg) + } else { + assert.NoError(t, err) + } + }) + } +} diff --git a/controllers/search_controller/external_search_source.go b/controllers/search_controller/external_search_source.go new file mode 100644 index 000000000..1e5fb5ab6 --- /dev/null +++ b/controllers/search_controller/external_search_source.go @@ -0,0 +1,46 @@ +package search_controller + +import ( + "k8s.io/apimachinery/pkg/types" + + searchv1 "github.com/mongodb/mongodb-kubernetes/api/v1/search" +) + +func NewExternalSearchSource(namespace string, spec *searchv1.ExternalMongoDBSource) SearchSourceDBResource { + return &externalSearchResource{namespace: namespace, spec: spec} +} + +// externalSearchResource implements SearchSourceDBResource for deployments managed outside the operator. +type externalSearchResource struct { + namespace string + spec *searchv1.ExternalMongoDBSource +} + +func (r *externalSearchResource) Validate() error { + return nil +} + +func (r *externalSearchResource) KeyfileSecretName() string { + if r.spec.KeyFileSecretKeyRef != nil { + return r.spec.KeyFileSecretKeyRef.Name + } + + return "" +} + +func (r *externalSearchResource) IsSecurityTLSConfigEnabled() bool { + return r.spec.TLS != nil && r.spec.TLS.Enabled +} + +func (r *externalSearchResource) TLSOperatorCASecretNamespacedName() types.NamespacedName { + if r.spec.TLS != nil && r.spec.TLS.CA != nil { + return types.NamespacedName{ + Name: r.spec.TLS.CA.Name, + Namespace: r.namespace, + } + } + + return types.NamespacedName{} +} + +func (r *externalSearchResource) HostSeeds() []string { return r.spec.HostAndPorts } diff --git a/controllers/search_controller/mongodbsearch_reconcile_helper.go b/controllers/search_controller/mongodbsearch_reconcile_helper.go index 1b0f33603..005b56175 100644 --- a/controllers/search_controller/mongodbsearch_reconcile_helper.go +++ b/controllers/search_controller/mongodbsearch_reconcile_helper.go @@ -18,8 +18,10 @@ import ( appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + mdbv1 "github.com/mongodb/mongodb-kubernetes/api/v1/mdb" searchv1 "github.com/mongodb/mongodb-kubernetes/api/v1/search" "github.com/mongodb/mongodb-kubernetes/controllers/operator/workflow" "github.com/mongodb/mongodb-kubernetes/mongodb-community-operator/pkg/automationconfig" @@ -29,6 +31,7 @@ import ( "github.com/mongodb/mongodb-kubernetes/mongodb-community-operator/pkg/kube/service" "github.com/mongodb/mongodb-kubernetes/mongodb-community-operator/pkg/mongot" "github.com/mongodb/mongodb-kubernetes/mongodb-community-operator/pkg/tls" + "github.com/mongodb/mongodb-kubernetes/pkg/kube" "github.com/mongodb/mongodb-kubernetes/pkg/kube/commoncontroller" "github.com/mongodb/mongodb-kubernetes/pkg/statefulset" ) @@ -81,7 +84,7 @@ func (r *MongoDBSearchReconcileHelper) reconcile(ctx context.Context, log *zap.S log = log.With("MongoDBSearch", r.mdbSearch.NamespacedName()) log.Infof("Reconciling MongoDBSearch") - if err := r.db.ValidateMongoDBVersion(); err != nil { + if err := r.db.Validate(); err != nil { return workflow.Failed(err) } @@ -93,6 +96,13 @@ func (r *MongoDBSearchReconcileHelper) reconcile(ctx context.Context, log *zap.S return workflow.Failed(err) } + keyfileStsModification, err := r.ensureSourceKeyfile(ctx, log) + if apierrors.IsNotFound(err) { + return workflow.Pending("Waiting for keyfile secret to be created") + } else if err != nil { + return workflow.Failed(err) + } + if err := r.ensureSearchService(ctx, r.mdbSearch); err != nil { return workflow.Failed(err) } @@ -118,7 +128,7 @@ func (r *MongoDBSearchReconcileHelper) reconcile(ctx context.Context, log *zap.S }, )) - if err := r.createOrUpdateStatefulSet(ctx, log, CreateSearchStatefulSetFunc(r.mdbSearch, r.db, r.buildImageString()), configHashModification, ingressTlsStsModification, egressTlsStsModification); err != nil { + if err := r.createOrUpdateStatefulSet(ctx, log, CreateSearchStatefulSetFunc(r.mdbSearch, r.db, r.buildImageString()), configHashModification, keyfileStsModification, ingressTlsStsModification, egressTlsStsModification); err != nil { return workflow.Failed(err) } @@ -129,6 +139,23 @@ func (r *MongoDBSearchReconcileHelper) reconcile(ctx context.Context, log *zap.S return workflow.OK() } +func (r *MongoDBSearchReconcileHelper) ensureSourceKeyfile(ctx context.Context, log *zap.SugaredLogger) (statefulset.Modification, error) { + keyfileSecretName := kube.ObjectKey(r.mdbSearch.GetNamespace(), r.db.KeyfileSecretName()) + keyfileSecret := &corev1.Secret{} + if err := r.client.Get(ctx, keyfileSecretName, keyfileSecret); err != nil { + return nil, err + } + + return statefulset.Apply( + // make sure mongot pods get restarted if the keyfile changes + statefulset.WithPodSpecTemplate(podtemplatespec.WithAnnotations( + map[string]string{ + "keyfileHash": hashBytes(keyfileSecret.Data["keyfile"]), + }, + )), + ), nil +} + func (r *MongoDBSearchReconcileHelper) buildImageString() string { imageVersion := r.mdbSearch.Spec.Version if imageVersion == "" { @@ -196,7 +223,7 @@ func (r *MongoDBSearchReconcileHelper) ensureMongotConfig(ctx context.Context, l log.Debugf("Updated mongot config yaml config map: %v (%s) with the following configuration: %s", cmName, op, string(configData)) - return hashMongotConfig(configData), nil + return hashBytes(configData), nil } func (r *MongoDBSearchReconcileHelper) ensureIngressTlsConfig(ctx context.Context) (mongot.Modification, statefulset.Modification, error) { @@ -286,8 +313,8 @@ cp /mongot-community/bin/jdk/lib/security/cacerts /java/trust-store/cacerts return mongotModification, statefulsetModification, nil } -func hashMongotConfig(mongotConfigYaml []byte) string { - hashBytes := sha256.Sum256(mongotConfigYaml) +func hashBytes(bytes []byte) string { + hashBytes := sha256.Sum256(bytes) return base32.StdEncoding.WithPadding(base32.NoPadding).EncodeToString(hashBytes[:]) } @@ -381,7 +408,7 @@ func GetMongodConfigParameters(search *searchv1.MongoDBSearch) map[string]any { "mongotHost": mongotHostAndPort(search), "searchIndexManagementHostAndPort": mongotHostAndPort(search), "skipAuthenticationToSearchIndexManagementServer": false, - "searchTLSMode": searchTLSMode, + "searchTLSMode": string(searchTLSMode), }, } } @@ -450,3 +477,51 @@ func (r *MongoDBSearchReconcileHelper) getMongotImage() string { return "" } + +func SearchCoordinatorRole() mdbv1.MongoDBRole { + // direct translation of https://github.com/10gen/mongo/blob/6f8d95a513eea8f91ea9f5d895dd8a288dfcf725/src/mongo/db/auth/builtin_roles.yml#L652 + return mdbv1.MongoDBRole{ + Role: "searchCoordinator", + Db: "admin", + Roles: []mdbv1.InheritedRole{ + { + Role: "clusterMonitor", + Db: "admin", + }, + { + Role: "directShardOperations", + Db: "admin", + }, + { + Role: "readAnyDatabase", + Db: "admin", + }, + }, + Privileges: []mdbv1.Privilege{ + { + Resource: mdbv1.Resource{ + Db: "__mdb_internal_search", + }, + Actions: []string{ + "changeStream", "collStats", "dbHash", "dbStats", "find", + "killCursors", "listCollections", "listIndexes", "listSearchIndexes", + // performRawDataOperations is available only on mongod master + // "performRawDataOperations", + "planCacheRead", "cleanupStructuredEncryptionData", + "compactStructuredEncryptionData", "convertToCapped", "createCollection", + "createIndex", "createSearchIndexes", "dropCollection", "dropIndex", + "dropSearchIndex", "insert", "remove", "renameCollectionSameDB", + "update", "updateSearchIndex", + }, + }, + // TODO: this causes the error "(BadValue) resource: {cluster: true} conflicts with resource type 'db'" + // { + // Resource: mdbv1.Resource{ + // Cluster: ptr.To(true), + // }, + // Actions: []string{"bypassDefaultMaxTimeMS"}, + // }, + }, + AuthenticationRestrictions: nil, + } +} diff --git a/controllers/search_controller/mongodbsearch_reconcile_helper_test.go b/controllers/search_controller/mongodbsearch_reconcile_helper_test.go index c7b4a8dc7..5a2a757ce 100644 --- a/controllers/search_controller/mongodbsearch_reconcile_helper_test.go +++ b/controllers/search_controller/mongodbsearch_reconcile_helper_test.go @@ -15,65 +15,6 @@ import ( kubernetesClient "github.com/mongodb/mongodb-kubernetes/mongodb-community-operator/pkg/kube/client" ) -func TestMongoDBSearchReconcileHelper_ValidateSearchSource(t *testing.T) { - mdbcMeta := metav1.ObjectMeta{ - Name: "test-mongodb", - Namespace: "test", - } - - cases := []struct { - name string - mdbc mdbcv1.MongoDBCommunity - expectedError string - }{ - { - name: "Invalid version", - mdbc: mdbcv1.MongoDBCommunity{ - ObjectMeta: mdbcMeta, - Spec: mdbcv1.MongoDBCommunitySpec{ - Version: "4.4.0", - }, - }, - expectedError: "MongoDB version must be 8.0.10 or higher", - }, - { - name: "Valid version", - mdbc: mdbcv1.MongoDBCommunity{ - ObjectMeta: mdbcMeta, - Spec: mdbcv1.MongoDBCommunitySpec{ - Version: "8.0.10", - }, - }, - }, - { - name: "TLS enabled", - mdbc: mdbcv1.MongoDBCommunity{ - ObjectMeta: mdbcMeta, - Spec: mdbcv1.MongoDBCommunitySpec{ - Version: "8.0.10", - Security: mdbcv1.Security{ - TLS: mdbcv1.TLS{ - Enabled: true, - }, - }, - }, - }, - }, - } - - for _, c := range cases { - t.Run(c.name, func(t *testing.T) { - db := NewSearchSourceDBResourceFromMongoDBCommunity(&c.mdbc) - err := db.ValidateMongoDBVersion() - if c.expectedError == "" { - assert.NoError(t, err) - } else { - assert.EqualError(t, err, c.expectedError) - } - }) - } -} - func TestMongoDBSearchReconcileHelper_ValidateSingleMongoDBSearchForSearchSource(t *testing.T) { mdbSearchSpec := searchv1.MongoDBSearchSpec{ Source: &searchv1.MongoDBSource{ @@ -144,7 +85,7 @@ func TestMongoDBSearchReconcileHelper_ValidateSingleMongoDBSearchForSearchSource clientBuilder.WithObjects(v) } - helper := NewMongoDBSearchReconcileHelper(kubernetesClient.NewClient(clientBuilder.Build()), mdbSearch, NewSearchSourceDBResourceFromMongoDBCommunity(mdbc), OperatorSearchConfig{}) + helper := NewMongoDBSearchReconcileHelper(kubernetesClient.NewClient(clientBuilder.Build()), mdbSearch, NewCommunityResourceSearchSource(mdbc), OperatorSearchConfig{}) err := helper.ValidateSingleMongoDBSearchForSearchSource(t.Context()) if c.expectedError == "" { assert.NoError(t, err) diff --git a/controllers/search_controller/search_construction.go b/controllers/search_controller/search_construction.go index cf622c0d2..af26172c3 100644 --- a/controllers/search_controller/search_construction.go +++ b/controllers/search_controller/search_construction.go @@ -1,10 +1,6 @@ package search_controller import ( - "fmt" - - "github.com/blang/semver" - "golang.org/x/xerrors" "k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/util/intstr" @@ -13,7 +9,6 @@ import ( searchv1 "github.com/mongodb/mongodb-kubernetes/api/v1/search" "github.com/mongodb/mongodb-kubernetes/controllers/operator/construct" - mdbcv1 "github.com/mongodb/mongodb-kubernetes/mongodb-community-operator/api/v1" "github.com/mongodb/mongodb-kubernetes/mongodb-community-operator/api/v1/common" "github.com/mongodb/mongodb-kubernetes/mongodb-community-operator/pkg/kube/container" "github.com/mongodb/mongodb-kubernetes/mongodb-community-operator/pkg/kube/podtemplatespec" @@ -39,113 +34,7 @@ type SearchSourceDBResource interface { IsSecurityTLSConfigEnabled() bool TLSOperatorCASecretNamespacedName() types.NamespacedName HostSeeds() []string - ValidateMongoDBVersion() error -} - -func NewSearchSourceDBResourceFromMongoDBCommunity(mdbc *mdbcv1.MongoDBCommunity) SearchSourceDBResource { - return &mdbcSearchResource{db: mdbc} -} - -func NewSearchSourceDBResourceFromExternal(namespace string, spec *searchv1.ExternalMongoDBSource) SearchSourceDBResource { - return &externalSearchResource{namespace: namespace, spec: spec} -} - -// externalSearchResource implements SearchSourceDBResource for deployments managed outside the operator. -type externalSearchResource struct { - namespace string - spec *searchv1.ExternalMongoDBSource -} - -func (r *externalSearchResource) ValidateMongoDBVersion() error { - return nil -} - -func (r *externalSearchResource) KeyfileSecretName() string { - if r.spec.KeyFileSecretKeyRef != nil { - return r.spec.KeyFileSecretKeyRef.Name - } - - return "" -} - -func (r *externalSearchResource) IsSecurityTLSConfigEnabled() bool { - return r.spec.TLS != nil && r.spec.TLS.Enabled -} - -func (r *externalSearchResource) TLSOperatorCASecretNamespacedName() types.NamespacedName { - if r.spec.TLS != nil && r.spec.TLS.CA != nil { - return types.NamespacedName{ - Name: r.spec.TLS.CA.Name, - Namespace: r.namespace, - } - } - - return types.NamespacedName{} -} - -func (r *externalSearchResource) HostSeeds() []string { return r.spec.HostAndPorts } - -type mdbcSearchResource struct { - db *mdbcv1.MongoDBCommunity -} - -func (r *mdbcSearchResource) ValidateMongoDBVersion() error { - version, err := semver.ParseTolerant(r.db.GetMongoDBVersion()) - if err != nil { - return xerrors.Errorf("error parsing MongoDB version '%s': %w", r.db.GetMongoDBVersion(), err) - } else if version.LT(semver.MustParse("8.0.10")) { - return xerrors.New("MongoDB version must be 8.0.10 or higher") - } - - return nil -} - -func (r *mdbcSearchResource) HostSeeds() []string { - seeds := make([]string, r.db.Spec.Members) - for i := range seeds { - seeds[i] = fmt.Sprintf("%s-%d.%s.%s.svc.cluster.local:%d", r.db.Name, i, r.db.ServiceName(), r.db.Namespace, r.db.GetMongodConfiguration().GetDBPort()) - } - return seeds -} - -func (r *mdbcSearchResource) Members() int { - return r.db.Spec.Members -} - -func (r *mdbcSearchResource) Name() string { - return r.db.Name -} - -func (r *mdbcSearchResource) NamespacedName() types.NamespacedName { - return r.db.NamespacedName() -} - -func (r *mdbcSearchResource) KeyfileSecretName() string { - return r.db.GetAgentKeyfileSecretNamespacedName().Name -} - -func (r *mdbcSearchResource) GetNamespace() string { - return r.db.Namespace -} - -func (r *mdbcSearchResource) HasSeparateDataAndLogsVolumes() bool { - return r.db.HasSeparateDataAndLogsVolumes() -} - -func (r *mdbcSearchResource) DatabaseServiceName() string { - return r.db.ServiceName() -} - -func (r *mdbcSearchResource) IsSecurityTLSConfigEnabled() bool { - return r.db.Spec.Security.TLS.Enabled -} - -func (r *mdbcSearchResource) DatabasePort() int { - return r.db.GetMongodConfiguration().GetDBPort() -} - -func (r *mdbcSearchResource) TLSOperatorCASecretNamespacedName() types.NamespacedName { - return r.db.TLSOperatorCASecretNamespacedName() + Validate() error } // ReplicaSetOptions returns a set of options which will configure a ReplicaSet StatefulSet diff --git a/docker/mongodb-kubernetes-tests/tests/common/search/search_tester.py b/docker/mongodb-kubernetes-tests/tests/common/search/search_tester.py index 8d8334ee9..6dccfb5ae 100644 --- a/docker/mongodb-kubernetes-tests/tests/common/search/search_tester.py +++ b/docker/mongodb-kubernetes-tests/tests/common/search/search_tester.py @@ -32,7 +32,7 @@ def mongorestore_from_url(self, archive_url: str, ns_include: str, mongodb_tools mongorestore_cmd += " --ssl" if ca_path := self.default_opts.get("tlsCAFile"): mongorestore_cmd += " --sslCAFile=" + ca_path - process_run_and_check(mongorestore_cmd.split()) + process_run_and_check(mongorestore_cmd.split(), capture_output=True) def create_search_index(self, database_name: str, collection_name: str): database = self.client[database_name] diff --git a/docker/mongodb-kubernetes-tests/tests/search/fixtures/enterprise-replicaset-sample-mflix.yaml b/docker/mongodb-kubernetes-tests/tests/search/fixtures/enterprise-replicaset-sample-mflix.yaml new file mode 100644 index 000000000..29d455b51 --- /dev/null +++ b/docker/mongodb-kubernetes-tests/tests/search/fixtures/enterprise-replicaset-sample-mflix.yaml @@ -0,0 +1,34 @@ +--- +apiVersion: mongodb.com/v1 +kind: MongoDB +metadata: + name: mdb-rs +spec: + members: 3 + version: 8.0.10 + type: ReplicaSet + opsManager: + configMapRef: + name: my-project + credentials: my-credentials + security: + authentication: + enabled: true + ignoreUnknownUsers: true + modes: + - SCRAM + agent: + logLevel: DEBUG + statefulSet: + spec: + template: + spec: + containers: + - name: mongodb-enterprise-database + resources: + limits: + cpu: "2" + memory: 2Gi + requests: + cpu: "1" + memory: 1Gi diff --git a/docker/mongodb-kubernetes-tests/tests/search/fixtures/mongodbuser-mdb-admin.yaml b/docker/mongodb-kubernetes-tests/tests/search/fixtures/mongodbuser-mdb-admin.yaml new file mode 100644 index 000000000..0b3fe4c77 --- /dev/null +++ b/docker/mongodb-kubernetes-tests/tests/search/fixtures/mongodbuser-mdb-admin.yaml @@ -0,0 +1,16 @@ +# admin user with root role +apiVersion: mongodb.com/v1 +kind: MongoDBUser +metadata: + name: mdb-admin +spec: + username: mdb-admin + db: admin + mongodbResourceRef: + name: mdb-rs + passwordSecretKeyRef: + name: mdb-admin-user-password + key: password + roles: + - name: root + db: admin \ No newline at end of file diff --git a/docker/mongodb-kubernetes-tests/tests/search/fixtures/mongodbuser-mdb-user.yaml b/docker/mongodb-kubernetes-tests/tests/search/fixtures/mongodbuser-mdb-user.yaml new file mode 100644 index 000000000..579cc58d0 --- /dev/null +++ b/docker/mongodb-kubernetes-tests/tests/search/fixtures/mongodbuser-mdb-user.yaml @@ -0,0 +1,16 @@ +# user performing search queries +apiVersion: mongodb.com/v1 +kind: MongoDBUser +metadata: + name: mdb-user +spec: + username: mdb-user + db: admin + mongodbResourceRef: + name: mdb-rs + passwordSecretKeyRef: + name: mdb-user-password + key: password + roles: + - name: readWrite + db: sample_mflix diff --git a/docker/mongodb-kubernetes-tests/tests/search/fixtures/mongodbuser-search-sync-source-user.yaml b/docker/mongodb-kubernetes-tests/tests/search/fixtures/mongodbuser-search-sync-source-user.yaml new file mode 100644 index 000000000..cd1eab1a5 --- /dev/null +++ b/docker/mongodb-kubernetes-tests/tests/search/fixtures/mongodbuser-search-sync-source-user.yaml @@ -0,0 +1,18 @@ +# user used by MongoDB Search to connect to MongoDB database to synchronize data from +# For MongoDB <8.2, the operator will be creating the searchCoordinator custom role automatically +# From MongoDB 8.2, searchCoordinator role will be a built-in role. +apiVersion: mongodb.com/v1 +kind: MongoDBUser +metadata: + name: search-sync-source-user +spec: + username: search-sync-source + db: admin + mongodbResourceRef: + name: mdb-rs + passwordSecretKeyRef: + name: mdb-rs-search-sync-source-password + key: password + roles: + - name: searchCoordinator + db: admin diff --git a/docker/mongodb-kubernetes-tests/tests/search/search_enterprise_basic.py b/docker/mongodb-kubernetes-tests/tests/search/search_enterprise_basic.py new file mode 100644 index 000000000..a3de8dcf8 --- /dev/null +++ b/docker/mongodb-kubernetes-tests/tests/search/search_enterprise_basic.py @@ -0,0 +1,183 @@ +import yaml +from kubetester import create_or_update_secret, try_load +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.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.movies_search_helper import SampleMoviesSearchHelper +from tests.common.search.search_tester import SearchTester +from tests.conftest import get_default_operator + +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-rs" + + +@fixture(scope="function") +def mdb(namespace: 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 + + 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 + + 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"]["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"]["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"]["username"] = MONGOT_USER_NAME + resource["spec"]["passwordSecretKeyRef"]["name"] = f"{resource.name}-password" + + return resource + + +@mark.e2e_search_enterprise_basic +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_basic +def test_create_database_resource(mdb: MongoDB): + mdb.update() + mdb.assert_reaches_phase(Phase.Running, timeout=1000) + + +@mark.e2e_search_enterprise_basic +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.create() + admin_user.assert_reaches_phase(Phase.Updated, timeout=300) + + create_or_update_secret( + namespace, name=user["spec"]["passwordSecretKeyRef"]["name"], data={"password": USER_PASSWORD} + ) + user.create() + 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.create() + # we deliberately don't wait for this user to be ready, because to be reconciled successfully it needs the searchCoordinator role + # which the ReplicaSet reconciler will only define in the automation config after the MongoDBSearch resource is created. + + +@mark.e2e_search_enterprise_basic +def test_create_search_resource(mdbs: MongoDBSearch): + mdbs.update() + mdbs.assert_reaches_phase(Phase.Running, timeout=300) + + +@mark.e2e_search_enterprise_basic +def test_wait_for_database_resource_ready(mdb: MongoDB): + mdb.assert_abandons_phase(Phase.Running, timeout=1800) + mdb.assert_reaches_phase(Phase.Running, timeout=1800) + + 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"] + ) + ) + setParameter = mongod_config.get("setParameter", {}) + assert ( + "mongotHost" in setParameter and "searchIndexManagementHostAndPort" in setParameter + ), "mongot parameters not found in mongod config" + + +@mark.e2e_search_enterprise_basic +def test_search_restore_sample_database(mdb: MongoDB): + sample_movies_helper = movies_search_helper.SampleMoviesSearchHelper( + SearchTester(get_connection_string(mdb, ADMIN_USER_NAME, ADMIN_USER_PASSWORD)) + ) + sample_movies_helper.restore_sample_database() + + +@mark.e2e_search_enterprise_basic +def test_search_create_search_index(mdb: MongoDB): + sample_movies_helper = movies_search_helper.SampleMoviesSearchHelper( + SearchTester(get_connection_string(mdb, USER_NAME, USER_PASSWORD)) + ) + sample_movies_helper.create_search_index() + + +@mark.e2e_search_enterprise_basic +def test_search_assert_search_query(mdb: MongoDB): + sample_movies_helper = movies_search_helper.SampleMoviesSearchHelper( + SearchTester(get_connection_string(mdb, USER_NAME, USER_PASSWORD)) + ) + sample_movies_helper.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}" diff --git a/mongodb-community-operator/controllers/replica_set_controller.go b/mongodb-community-operator/controllers/replica_set_controller.go index 9246e4ebb..67e062648 100644 --- a/mongodb-community-operator/controllers/replica_set_controller.go +++ b/mongodb-community-operator/controllers/replica_set_controller.go @@ -715,8 +715,8 @@ func (r ReplicaSetReconciler) buildAutomationConfig(ctx context.Context, mdb mdb // and that this resource passes search validations. If either fails, proceed without a search target // for the mongod automation config. if len(searchList.Items) == 1 { - searchSource := search_controller.NewSearchSourceDBResourceFromMongoDBCommunity(&mdb) - if searchSource.ValidateMongoDBVersion() == nil { + searchSource := search_controller.NewCommunityResourceSearchSource(&mdb) + if searchSource.Validate() == nil { search = &searchList.Items[0] } } diff --git a/scripts/dev/contexts/e2e_mdb_community b/scripts/dev/contexts/e2e_mdb_community index bc2f0f7f0..b38563179 100644 --- a/scripts/dev/contexts/e2e_mdb_community +++ b/scripts/dev/contexts/e2e_mdb_community @@ -11,11 +11,4 @@ source "${script_dir}/variables/mongodb_latest" # This variable is needed otherwise the `fetch_om_information.sh` script is called and fails the test export OM_EXTERNALLY_CONFIGURED="true" -# Temporary development images built from mongot master -#export MDB_SEARCH_COMMUNITY_VERSION="776d43523d185b6b234289e17c191712a3e6569b" # master -#export MDB_SEARCH_COMMUNITY_VERSION="d6884ae132aab30497af55dbaff05e8274e9775f" # Local->Admin -#export MDB_SEARCH_COMMUNITY_VERSION="ad8acf5c3a045d6e0306ad67d61fcb5be40f57ae" # hardcoded mdbc-rs replicaset name -#export MDB_SEARCH_COMMUNITY_VERSION="b9b80915f5571bfa5fc2aa70acb20d784e68d79b" # hardcoded Local->Admin, handled replicaSetName in config -export MDB_SEARCH_COMMUNITY_VERSION="fbd60fb055dd500058edcb45677ea85d19421f47" # Nolan's fixes -export MDB_SEARCH_COMMUNITY_NAME="mongot/community" -export MDB_SEARCH_COMMUNITY_REPO_URL="268558157000.dkr.ecr.eu-west-1.amazonaws.com" +source "${script_dir}/variables/mongodb_search_dev" diff --git a/scripts/dev/contexts/e2e_mdb_kind_ubi_cloudqa b/scripts/dev/contexts/e2e_mdb_kind_ubi_cloudqa index 73202c211..080292666 100644 --- a/scripts/dev/contexts/e2e_mdb_kind_ubi_cloudqa +++ b/scripts/dev/contexts/e2e_mdb_kind_ubi_cloudqa @@ -16,3 +16,5 @@ export CUSTOM_OM_VERSION export CUSTOM_MDB_VERSION=6.0.5 export CUSTOM_MDB_PREV_VERSION=5.0.7 + +source "${script_dir}/variables/mongodb_search_dev" diff --git a/scripts/dev/contexts/e2e_static_mdb_kind_ubi_cloudqa b/scripts/dev/contexts/e2e_static_mdb_kind_ubi_cloudqa index 0ee88f209..3b6426d35 100644 --- a/scripts/dev/contexts/e2e_static_mdb_kind_ubi_cloudqa +++ b/scripts/dev/contexts/e2e_static_mdb_kind_ubi_cloudqa @@ -17,3 +17,5 @@ export CUSTOM_OM_VERSION export CUSTOM_MDB_PREV_VERSION=6.0.16 export CUSTOM_MDB_VERSION=7.0.5 + +source "${script_dir}/variables/mongodb_search_dev" diff --git a/scripts/dev/contexts/variables/mongodb_search_dev b/scripts/dev/contexts/variables/mongodb_search_dev new file mode 100644 index 000000000..590b3ed69 --- /dev/null +++ b/scripts/dev/contexts/variables/mongodb_search_dev @@ -0,0 +1,13 @@ +#!/usr/bin/env bash + +set -Eeou pipefail + +# Temporary development images built from mongot master +#export MDB_SEARCH_COMMUNITY_VERSION="776d43523d185b6b234289e17c191712a3e6569b" # master +#export MDB_SEARCH_COMMUNITY_VERSION="d6884ae132aab30497af55dbaff05e8274e9775f" # Local->Admin +#export MDB_SEARCH_COMMUNITY_VERSION="ad8acf5c3a045d6e0306ad67d61fcb5be40f57ae" # hardcoded mdbc-rs replicaset name +#export MDB_SEARCH_COMMUNITY_VERSION="b9b80915f5571bfa5fc2aa70acb20d784e68d79b" # hardcoded Local->Admin, handled replicaSetName in config +#export MDB_SEARCH_COMMUNITY_VERSION="fbd60fb055dd500058edcb45677ea85d19421f47" # Nolan's fixes +export MDB_SEARCH_COMMUNITY_VERSION="f3b9cf1cc358c34044a7abb4bf220f6e47e00873" # Aug 26th mongot master +export MDB_SEARCH_COMMUNITY_NAME="mongot/community" +export MDB_SEARCH_COMMUNITY_REPO_URL="268558157000.dkr.ecr.eu-west-1.amazonaws.com" From 459c2e86e9dbf9821300827e55bbabd0e4303fed Mon Sep 17 00:00:00 2001 From: Yavor Georgiev Date: Tue, 2 Sep 2025 18:56:02 +0200 Subject: [PATCH 12/13] CLOUDP-342363 [Search] Enterprise Server with TLS (#395) # Summary ## Proof of Work ## Checklist - [x] Have you linked a jira ticket and/or is the ticket in the title? - [x] Have you checked whether your jira ticket required DOCSP changes? - [x] Have you added changelog file? - use `skip-changelog` label if not needed - refer to [Changelog files and Release Notes](https://github.com/mongodb/mongodb-kubernetes/blob/master/CONTRIBUTING.md#changelog-files-and-release-notes) section in CONTRIBUTING.md for more details --- .evergreen-tasks.yml | 5 + .evergreen.yml | 1 + .../crd/bases/mongodb.com_mongodbsearch.yaml | 16 +- .../operator/mongodbsearch_controller.go | 15 ++ .../community_search_source.go | 29 ++- .../enterprise_search_source.go | 24 +- .../external_search_source.go | 33 +-- .../mongodbsearch_reconcile_helper.go | 19 +- .../search_controller/search_construction.go | 10 +- .../kubetester/mongodb.py | 6 +- .../tests/search/search_enterprise_tls.py | 239 ++++++++++++++++++ .../crds/mongodb.com_mongodbsearch.yaml | 20 +- public/crds.yaml | 20 +- 13 files changed, 376 insertions(+), 61 deletions(-) create mode 100644 docker/mongodb-kubernetes-tests/tests/search/search_enterprise_tls.py diff --git a/.evergreen-tasks.yml b/.evergreen-tasks.yml index 94fd8c8c0..8a0b73fec 100644 --- a/.evergreen-tasks.yml +++ b/.evergreen-tasks.yml @@ -1306,3 +1306,8 @@ tasks: commands: - func: "e2e_test" + - name: e2e_search_enterprise_tls + tags: [ "patch-run" ] + commands: + - func: "e2e_test" + diff --git a/.evergreen.yml b/.evergreen.yml index a8cfeb53a..48d96bee2 100644 --- a/.evergreen.yml +++ b/.evergreen.yml @@ -817,6 +817,7 @@ task_groups: - e2e_sharded_cluster_oidc_m2m_user # MongoDBSearch test group - e2e_search_enterprise_basic + - e2e_search_enterprise_tls <<: *teardown_group # this task group contains just a one task, which is smoke testing whether the operator diff --git a/config/crd/bases/mongodb.com_mongodbsearch.yaml b/config/crd/bases/mongodb.com_mongodbsearch.yaml index 276adf875..62dbfa2bc 100644 --- a/config/crd/bases/mongodb.com_mongodbsearch.yaml +++ b/config/crd/bases/mongodb.com_mongodbsearch.yaml @@ -49,6 +49,8 @@ spec: spec: properties: persistence: + description: Configure MongoDB Search's persistent volume. If not + defined, the operator will request 10GB of storage. properties: multiple: properties: @@ -95,7 +97,8 @@ spec: type: object type: object resourceRequirements: - description: ResourceRequirements describes the compute resource requirements. + description: Configure resource requests and limits for the MongoDB + Search pods. properties: claims: description: |- @@ -150,6 +153,8 @@ spec: type: object type: object security: + description: Configure security settings of the MongoDB Search server + that MongoDB database is connecting to when performing search queries. properties: tls: properties: @@ -181,6 +186,8 @@ spec: type: object type: object source: + description: MongoDB database connection details from which MongoDB + Search will synchronize data to build indexes. properties: external: properties: @@ -199,7 +206,7 @@ spec: name: type: string required: - - name + - name type: object tls: properties: @@ -224,7 +231,7 @@ spec: enabled: type: boolean required: - - enabled + - enabled type: object type: object mongodbResourceRef: @@ -277,6 +284,9 @@ spec: - spec type: object version: + description: Optional version of MongoDB Search component (mongot). + If not set, then the operator will set the most appropriate version + of MongoDB Search. type: string type: object status: diff --git a/controllers/operator/mongodbsearch_controller.go b/controllers/operator/mongodbsearch_controller.go index 7d47f496f..b08e1e159 100644 --- a/controllers/operator/mongodbsearch_controller.go +++ b/controllers/operator/mongodbsearch_controller.go @@ -59,6 +59,21 @@ func (r *MongoDBSearchReconciler) Reconcile(ctx context.Context, request reconci r.watch.AddWatchedResourceIfNotAdded(searchSource.KeyfileSecretName(), mdbSearch.Namespace, watch.Secret, mdbSearch.NamespacedName()) + // Watch for changes in database source CA certificate secrets or configmaps + tlsSourceConfig := searchSource.TLSConfig() + if tlsSourceConfig != nil { + for wType, resources := range tlsSourceConfig.ResourcesToWatch { + for _, resource := range resources { + r.watch.AddWatchedResourceIfNotAdded(resource.Name, resource.Namespace, wType, mdbSearch.NamespacedName()) + } + } + } + + // Watch our own TLS certificate secret for changes + if mdbSearch.Spec.Security.TLS.Enabled { + r.watch.AddWatchedResourceIfNotAdded(mdbSearch.Spec.Security.TLS.CertificateKeySecret.Name, mdbSearch.Namespace, watch.Secret, mdbSearch.NamespacedName()) + } + reconcileHelper := search_controller.NewMongoDBSearchReconcileHelper(kubernetesClient.NewClient(r.kubeClient), mdbSearch, searchSource, r.operatorSearchConfig) return reconcileHelper.Reconcile(ctx, log).ReconcileResult() diff --git a/controllers/search_controller/community_search_source.go b/controllers/search_controller/community_search_source.go index e4f799c83..8b10a1cbf 100644 --- a/controllers/search_controller/community_search_source.go +++ b/controllers/search_controller/community_search_source.go @@ -8,7 +8,11 @@ import ( "golang.org/x/xerrors" "k8s.io/apimachinery/pkg/types" + corev1 "k8s.io/api/core/v1" + + "github.com/mongodb/mongodb-kubernetes/controllers/operator/watch" mdbcv1 "github.com/mongodb/mongodb-kubernetes/mongodb-community-operator/api/v1" + "github.com/mongodb/mongodb-kubernetes/pkg/statefulset" "github.com/mongodb/mongodb-kubernetes/pkg/util" ) @@ -32,12 +36,27 @@ func (r *CommunitySearchSource) KeyfileSecretName() string { return r.MongoDBCommunity.GetAgentKeyfileSecretNamespacedName().Name } -func (r *CommunitySearchSource) IsSecurityTLSConfigEnabled() bool { - return r.Spec.Security.TLS.Enabled -} +func (r *CommunitySearchSource) TLSConfig() *TLSSourceConfig { + if !r.Spec.Security.TLS.Enabled { + return nil + } + + var volume corev1.Volume + watchedResources := make(map[watch.Type][]types.NamespacedName) -func (r *CommunitySearchSource) TLSOperatorCASecretNamespacedName() types.NamespacedName { - return r.MongoDBCommunity.TLSOperatorCASecretNamespacedName() + if r.Spec.Security.TLS.CaCertificateSecret != nil { + volume = statefulset.CreateVolumeFromSecret("ca", r.Spec.Security.TLS.CaCertificateSecret.Name) + watchedResources[watch.Secret] = []types.NamespacedName{r.TLSCaCertificateSecretNamespacedName()} + } else { + volume = statefulset.CreateVolumeFromConfigMap("ca", r.Spec.Security.TLS.CaConfigMap.Name) + watchedResources[watch.ConfigMap] = []types.NamespacedName{r.TLSConfigMapNamespacedName()} + } + + return &TLSSourceConfig{ + CAFileName: "ca.crt", + CAVolume: volume, + ResourcesToWatch: watchedResources, + } } func (r *CommunitySearchSource) Validate() error { diff --git a/controllers/search_controller/enterprise_search_source.go b/controllers/search_controller/enterprise_search_source.go index 87ad09e5f..c1256fbbb 100644 --- a/controllers/search_controller/enterprise_search_source.go +++ b/controllers/search_controller/enterprise_search_source.go @@ -9,6 +9,8 @@ import ( "k8s.io/apimachinery/pkg/types" mdbv1 "github.com/mongodb/mongodb-kubernetes/api/v1/mdb" + "github.com/mongodb/mongodb-kubernetes/controllers/operator/watch" + "github.com/mongodb/mongodb-kubernetes/pkg/statefulset" "github.com/mongodb/mongodb-kubernetes/pkg/util" ) @@ -28,16 +30,24 @@ func (r EnterpriseResourceSearchSource) HostSeeds() []string { return seeds } -func (r EnterpriseResourceSearchSource) KeyfileSecretName() string { - return fmt.Sprintf("%s-keyfile", r.Name) -} +func (r EnterpriseResourceSearchSource) TLSConfig() *TLSSourceConfig { + if !r.Spec.Security.IsTLSEnabled() { + return nil + } -func (r EnterpriseResourceSearchSource) IsSecurityTLSConfigEnabled() bool { - return r.Spec.Security.IsTLSEnabled() + return &TLSSourceConfig{ + CAFileName: "ca-pem", + CAVolume: statefulset.CreateVolumeFromConfigMap("ca", r.Spec.Security.TLSConfig.CA), + ResourcesToWatch: map[watch.Type][]types.NamespacedName{ + watch.ConfigMap: { + {Namespace: r.Namespace, Name: r.Spec.Security.TLSConfig.CA}, + }, + }, + } } -func (r EnterpriseResourceSearchSource) TLSOperatorCASecretNamespacedName() types.NamespacedName { - return types.NamespacedName{} +func (r EnterpriseResourceSearchSource) KeyfileSecretName() string { + return fmt.Sprintf("%s-keyfile", r.Name) } func (r EnterpriseResourceSearchSource) Validate() error { diff --git a/controllers/search_controller/external_search_source.go b/controllers/search_controller/external_search_source.go index 1e5fb5ab6..5a408246e 100644 --- a/controllers/search_controller/external_search_source.go +++ b/controllers/search_controller/external_search_source.go @@ -4,6 +4,8 @@ import ( "k8s.io/apimachinery/pkg/types" searchv1 "github.com/mongodb/mongodb-kubernetes/api/v1/search" + "github.com/mongodb/mongodb-kubernetes/controllers/operator/watch" + "github.com/mongodb/mongodb-kubernetes/pkg/statefulset" ) func NewExternalSearchSource(namespace string, spec *searchv1.ExternalMongoDBSource) SearchSourceDBResource { @@ -20,27 +22,28 @@ func (r *externalSearchResource) Validate() error { return nil } -func (r *externalSearchResource) KeyfileSecretName() string { - if r.spec.KeyFileSecretKeyRef != nil { - return r.spec.KeyFileSecretKeyRef.Name +func (r *externalSearchResource) TLSConfig() *TLSSourceConfig { + if r.spec.TLS == nil || !r.spec.TLS.Enabled { + return nil } - return "" -} - -func (r *externalSearchResource) IsSecurityTLSConfigEnabled() bool { - return r.spec.TLS != nil && r.spec.TLS.Enabled + return &TLSSourceConfig{ + CAFileName: "ca.crt", + CAVolume: statefulset.CreateVolumeFromSecret("ca", r.spec.TLS.CA.Name), + ResourcesToWatch: map[watch.Type][]types.NamespacedName{ + watch.Secret: { + {Namespace: r.namespace, Name: r.spec.TLS.CA.Name}, + }, + }, + } } -func (r *externalSearchResource) TLSOperatorCASecretNamespacedName() types.NamespacedName { - if r.spec.TLS != nil && r.spec.TLS.CA != nil { - return types.NamespacedName{ - Name: r.spec.TLS.CA.Name, - Namespace: r.namespace, - } +func (r *externalSearchResource) KeyfileSecretName() string { + if r.spec.KeyFileSecretKeyRef != nil { + return r.spec.KeyFileSecretKeyRef.Name } - return types.NamespacedName{} + return "" } func (r *externalSearchResource) HostSeeds() []string { return r.spec.HostAndPorts } diff --git a/controllers/search_controller/mongodbsearch_reconcile_helper.go b/controllers/search_controller/mongodbsearch_reconcile_helper.go index 005b56175..4b384a707 100644 --- a/controllers/search_controller/mongodbsearch_reconcile_helper.go +++ b/controllers/search_controller/mongodbsearch_reconcile_helper.go @@ -261,28 +261,17 @@ func (r *MongoDBSearchReconcileHelper) ensureIngressTlsConfig(ctx context.Contex } func (r *MongoDBSearchReconcileHelper) ensureEgressTlsConfig(ctx context.Context) (mongot.Modification, statefulset.Modification, error) { - if !r.db.IsSecurityTLSConfigEnabled() { + tlsSourceConfig := r.db.TLSConfig() + if tlsSourceConfig == nil { return mongot.NOOP(), statefulset.NOOP(), nil } - caSecretName := r.db.TLSOperatorCASecretNamespacedName() - caSecret := &corev1.Secret{} - if err := r.client.Get(ctx, caSecretName, caSecret); err != nil { - return nil, nil, xerrors.Errorf("error getting CA Secret %s: %w", caSecretName, err) - } - - // HACK: find a better way of getting the CA file name - var caFileName string - for k := range caSecret.Data { - caFileName = k - } - mongotModification := func(config *mongot.Config) { config.SyncSource.ReplicaSet.TLS = ptr.To(true) } _, containerSecurityContext := podtemplatespec.WithDefaultSecurityContextsModifications() - caVolume := statefulset.CreateVolumeFromSecret("ca", caSecretName.Name) + caVolume := tlsSourceConfig.CAVolume trustStoreVolume := statefulset.CreateVolumeFromEmptyDir("cacerts") statefulsetModification := statefulset.WithPodSpecTemplate(podtemplatespec.Apply( podtemplatespec.WithVolume(caVolume), @@ -300,7 +289,7 @@ func (r *MongoDBSearchReconcileHelper) ensureEgressTlsConfig(ctx context.Context fmt.Sprintf(` cp /mongot-community/bin/jdk/lib/security/cacerts /java/trust-store/cacerts /mongot-community/bin/jdk/bin/keytool -keystore /java/trust-store/cacerts -storepass changeit -noprompt -trustcacerts -importcert -alias mongodcert -file %s/%s - `, tls.CAMountPath, caFileName), + `, tls.CAMountPath, tlsSourceConfig.CAFileName), }), )), podtemplatespec.WithContainer(MongotContainerName, container.Apply( diff --git a/controllers/search_controller/search_construction.go b/controllers/search_controller/search_construction.go index af26172c3..3ab49f5f2 100644 --- a/controllers/search_controller/search_construction.go +++ b/controllers/search_controller/search_construction.go @@ -9,6 +9,7 @@ import ( searchv1 "github.com/mongodb/mongodb-kubernetes/api/v1/search" "github.com/mongodb/mongodb-kubernetes/controllers/operator/construct" + "github.com/mongodb/mongodb-kubernetes/controllers/operator/watch" "github.com/mongodb/mongodb-kubernetes/mongodb-community-operator/api/v1/common" "github.com/mongodb/mongodb-kubernetes/mongodb-community-operator/pkg/kube/container" "github.com/mongodb/mongodb-kubernetes/mongodb-community-operator/pkg/kube/podtemplatespec" @@ -31,12 +32,17 @@ const ( // TODO check if we could use already existing interface (DbCommon, MongoDBStatefulSetOwner, etc.) type SearchSourceDBResource interface { KeyfileSecretName() string - IsSecurityTLSConfigEnabled() bool - TLSOperatorCASecretNamespacedName() types.NamespacedName + TLSConfig() *TLSSourceConfig HostSeeds() []string Validate() error } +type TLSSourceConfig struct { + CAFileName string + CAVolume corev1.Volume + ResourcesToWatch map[watch.Type][]types.NamespacedName +} + // ReplicaSetOptions returns a set of options which will configure a ReplicaSet StatefulSet func CreateSearchStatefulSetFunc(mdbSearch *searchv1.MongoDBSearch, sourceDBResource SearchSourceDBResource, searchImage string) statefulset.Modification { labels := map[string]string{ diff --git a/docker/mongodb-kubernetes-tests/kubetester/mongodb.py b/docker/mongodb-kubernetes-tests/kubetester/mongodb.py index c9970361a..b3f38239b 100644 --- a/docker/mongodb-kubernetes-tests/kubetester/mongodb.py +++ b/docker/mongodb-kubernetes-tests/kubetester/mongodb.py @@ -278,10 +278,8 @@ def configure_custom_tls( tls_cert_secret_name: str, ): ensure_nested_objects(self, ["spec", "security", "tls"]) - self["spec"]["security"] = { - "certsSecretPrefix": tls_cert_secret_name, - "tls": {"enabled": True, "ca": issuer_ca_configmap_name}, - } + self["spec"]["security"]["certsSecretPrefix"] = tls_cert_secret_name + self["spec"]["security"]["tls"].update({"enabled": True, "ca": issuer_ca_configmap_name}) def build_list_of_hosts(self): """Returns the list of full_fqdn:27017 for every member of the mongodb resource""" diff --git a/docker/mongodb-kubernetes-tests/tests/search/search_enterprise_tls.py b/docker/mongodb-kubernetes-tests/tests/search/search_enterprise_tls.py new file mode 100644 index 000000000..f4c0bc96f --- /dev/null +++ b/docker/mongodb-kubernetes-tests/tests/search/search_enterprise_tls.py @@ -0,0 +1,239 @@ +import pymongo +import yaml +from kubetester import create_or_update_secret, try_load +from kubetester.certs import create_mongodb_tls_certs, create_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.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.movies_search_helper import SampleMoviesSearchHelper +from tests.common.search.search_tester import SearchTester +from tests.conftest import get_default_operator + +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-rs" + +# 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_custom_tls(issuer_ca_configmap, "certs") + + 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": {"enabled": True, "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"]["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"]["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"]["username"] = MONGOT_USER_NAME + resource["spec"]["passwordSecretKeyRef"]["name"] = f"{resource.name}-password" + + return resource + + +@mark.e2e_search_enterprise_tls +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_tls +def test_install_tls_secrets_and_configmaps(namespace: str, mdb: MongoDB, mdbs: MongoDBSearch, issuer: str): + create_mongodb_tls_certs(issuer, namespace, mdb.name, f"certs-{mdb.name}-cert", mdb.get_members()) + + 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"], + secret_name=MDBS_TLS_SECRET_NAME, + ) + + +@mark.e2e_search_enterprise_tls +def test_create_database_resource(mdb: MongoDB): + mdb.update() + mdb.assert_reaches_phase(Phase.Running, timeout=1000) + + +@mark.e2e_search_enterprise_tls +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.create() + admin_user.assert_reaches_phase(Phase.Updated, timeout=300) + + create_or_update_secret( + namespace, name=user["spec"]["passwordSecretKeyRef"]["name"], data={"password": USER_PASSWORD} + ) + user.create() + 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.create() + # we deliberately don't wait for this user to be ready, because to be reconciled successfully it needs the searchCoordinator role + # which the ReplicaSet reconciler will only define in the automation config after the MongoDBSearch resource is created. + + +@mark.e2e_search_enterprise_tls +def test_create_search_resource(mdbs: MongoDBSearch): + mdbs.update() + mdbs.assert_reaches_phase(Phase.Running, timeout=300) + + +@mark.e2e_search_enterprise_tls +def test_wait_for_database_resource_ready(mdb: MongoDB): + mdb.assert_abandons_phase(Phase.Running, timeout=1800) + mdb.assert_reaches_phase(Phase.Running, timeout=1800) + + 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"] + ) + ) + setParameter = mongod_config.get("setParameter", {}) + assert ( + "mongotHost" in setParameter and "searchIndexManagementHostAndPort" in setParameter + ), "mongot parameters not found in mongod config" + + +@mark.e2e_search_enterprise_tls +def test_validate_tls_connections(mdb: MongoDB, mdbs: MongoDBSearch, namespace: str, issuer_ca_filepath: str): + with pymongo.MongoClient( + f"mongodb://{mdb.name}-0.{mdb.name}-svc.{namespace}.svc.cluster.local:27017/?replicaSet={mdb.name}", + tls=True, + tlsCAFile=issuer_ca_filepath, + tlsAllowInvalidHostnames=False, + serverSelectionTimeoutMS=30000, + connectTimeoutMS=20000, + ) as mongodb_client: + mongodb_info = mongodb_client.admin.command("hello") + assert mongodb_info.get("ok") == 1, "MongoDB connection failed" + + with pymongo.MongoClient( + f"mongodb://{mdbs.name}-search-svc.{namespace}.svc.cluster.local:27027", + tls=True, + tlsCAFile=issuer_ca_filepath, + tlsAllowInvalidHostnames=False, + serverSelectionTimeoutMS=10000, + connectTimeoutMS=10000, + ) as search_client: + search_info = search_client.admin.command("hello") + assert search_info.get("ok") == 1, "MongoDBSearch connection failed" + + +@mark.e2e_search_enterprise_tls +def test_search_restore_sample_database(mdb: MongoDB, issuer_ca_filepath: str): + sample_movies_helper = movies_search_helper.SampleMoviesSearchHelper( + SearchTester( + get_connection_string(mdb, ADMIN_USER_NAME, ADMIN_USER_PASSWORD), use_ssl=True, ca_path=issuer_ca_filepath + ) + ) + sample_movies_helper.restore_sample_database() + + +@mark.e2e_search_enterprise_tls +def test_search_create_search_index(mdb: MongoDB, issuer_ca_filepath: str): + sample_movies_helper = movies_search_helper.SampleMoviesSearchHelper( + SearchTester(get_connection_string(mdb, USER_NAME, USER_PASSWORD), use_ssl=True, ca_path=issuer_ca_filepath) + ) + sample_movies_helper.create_search_index() + + +@mark.e2e_search_enterprise_tls +def test_search_assert_search_query(mdb: MongoDB, issuer_ca_filepath: str): + sample_movies_helper = movies_search_helper.SampleMoviesSearchHelper( + SearchTester(get_connection_string(mdb, USER_NAME, USER_PASSWORD), use_ssl=True, ca_path=issuer_ca_filepath) + ) + sample_movies_helper.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}" diff --git a/helm_chart/crds/mongodb.com_mongodbsearch.yaml b/helm_chart/crds/mongodb.com_mongodbsearch.yaml index 0cd70706e..62dbfa2bc 100644 --- a/helm_chart/crds/mongodb.com_mongodbsearch.yaml +++ b/helm_chart/crds/mongodb.com_mongodbsearch.yaml @@ -49,6 +49,8 @@ spec: spec: properties: persistence: + description: Configure MongoDB Search's persistent volume. If not + defined, the operator will request 10GB of storage. properties: multiple: properties: @@ -95,7 +97,8 @@ spec: type: object type: object resourceRequirements: - description: ResourceRequirements describes the compute resource requirements. + description: Configure resource requests and limits for the MongoDB + Search pods. properties: claims: description: |- @@ -150,6 +153,8 @@ spec: type: object type: object security: + description: Configure security settings of the MongoDB Search server + that MongoDB database is connecting to when performing search queries. properties: tls: properties: @@ -181,6 +186,8 @@ spec: type: object type: object source: + description: MongoDB database connection details from which MongoDB + Search will synchronize data to build indexes. properties: external: properties: @@ -199,7 +206,7 @@ spec: name: type: string required: - - name + - name type: object tls: properties: @@ -224,7 +231,7 @@ spec: enabled: type: boolean required: - - enabled + - enabled type: object type: object mongodbResourceRef: @@ -254,8 +261,8 @@ spec: type: object statefulSet: description: |- - StatefulSetConfiguration holds the optional custom StatefulSet - that should be merged into the operator created one. + StatefulSetSpec which the operator will apply to the MongoDB Search StatefulSet at the end of the reconcile loop. Use to provide necessary customizations, + which aren't exposed as fields in the MongoDBSearch.spec. properties: metadata: description: StatefulSetMetadataWrapper is a wrapper around Labels @@ -277,6 +284,9 @@ spec: - spec type: object version: + description: Optional version of MongoDB Search component (mongot). + If not set, then the operator will set the most appropriate version + of MongoDB Search. type: string type: object status: diff --git a/public/crds.yaml b/public/crds.yaml index 4a63814d0..7ccaacee5 100644 --- a/public/crds.yaml +++ b/public/crds.yaml @@ -4079,6 +4079,8 @@ spec: spec: properties: persistence: + description: Configure MongoDB Search's persistent volume. If not + defined, the operator will request 10GB of storage. properties: multiple: properties: @@ -4125,7 +4127,8 @@ spec: type: object type: object resourceRequirements: - description: ResourceRequirements describes the compute resource requirements. + description: Configure resource requests and limits for the MongoDB + Search pods. properties: claims: description: |- @@ -4180,6 +4183,8 @@ spec: type: object type: object security: + description: Configure security settings of the MongoDB Search server + that MongoDB database is connecting to when performing search queries. properties: tls: properties: @@ -4211,6 +4216,8 @@ spec: type: object type: object source: + description: MongoDB database connection details from which MongoDB + Search will synchronize data to build indexes. properties: external: properties: @@ -4229,7 +4236,7 @@ spec: name: type: string required: - - name + - name type: object tls: properties: @@ -4254,7 +4261,7 @@ spec: enabled: type: boolean required: - - enabled + - enabled type: object type: object mongodbResourceRef: @@ -4284,8 +4291,8 @@ spec: type: object statefulSet: description: |- - StatefulSetConfiguration holds the optional custom StatefulSet - that should be merged into the operator created one. + StatefulSetSpec which the operator will apply to the MongoDB Search StatefulSet at the end of the reconcile loop. Use to provide necessary customizations, + which aren't exposed as fields in the MongoDBSearch.spec. properties: metadata: description: StatefulSetMetadataWrapper is a wrapper around Labels @@ -4307,6 +4314,9 @@ spec: - spec type: object version: + description: Optional version of MongoDB Search component (mongot). + If not set, then the operator will set the most appropriate version + of MongoDB Search. type: string type: object status: From e48227be4c9d7379257db61f20550a61ca8f2328 Mon Sep 17 00:00:00 2001 From: Yavor Georgiev Date: Tue, 2 Sep 2025 16:39:30 +0200 Subject: [PATCH 13/13] add ConfigMap watcher --- controllers/operator/mongodbsearch_controller.go | 1 + 1 file changed, 1 insertion(+) diff --git a/controllers/operator/mongodbsearch_controller.go b/controllers/operator/mongodbsearch_controller.go index b08e1e159..a25efed3c 100644 --- a/controllers/operator/mongodbsearch_controller.go +++ b/controllers/operator/mongodbsearch_controller.go @@ -138,6 +138,7 @@ func AddMongoDBSearchController(ctx context.Context, mgr manager.Manager, operat Watches(&mdbv1.MongoDB{}, &watch.ResourcesHandler{ResourceType: watch.MongoDB, ResourceWatcher: r.watch}). Watches(&mdbcv1.MongoDBCommunity{}, &watch.ResourcesHandler{ResourceType: "MongoDBCommunity", ResourceWatcher: r.watch}). Watches(&corev1.Secret{}, &watch.ResourcesHandler{ResourceType: watch.Secret, ResourceWatcher: r.watch}). + Watches(&corev1.ConfigMap{}, &watch.ResourcesHandler{ResourceType: watch.ConfigMap, ResourceWatcher: r.watch}). Owns(&appsv1.StatefulSet{}). Owns(&corev1.Secret{}). Complete(r)