diff --git a/api/v1/search/mongodbsearch_types.go b/api/v1/search/mongodbsearch_types.go index 602cf467f..7179d3dca 100644 --- a/api/v1/search/mongodbsearch_types.go +++ b/api/v1/search/mongodbsearch_types.go @@ -17,11 +17,11 @@ import ( ) const ( - MongotDefaultWireprotoPort = 27027 - MongotDefaultGrpcPort = 27028 - MongotDefaultMetricsPort = 9946 - MongotDefautHealthCheckPort = 8080 - MongotDefaultSyncSourceUsername = "search-sync-source" + MongotDefaultWireprotoPort int32 = 27027 + MongotDefaultGrpcPort int32 = 27028 + MongotDefaultPrometheusPort int32 = 9946 + MongotDefautHealthCheckPort int32 = 8080 + MongotDefaultSyncSourceUsername = "search-sync-source" ForceWireprotoAnnotation = "mongodb.com/v1.force-search-wireproto" ) @@ -30,6 +30,23 @@ func init() { v1.SchemeBuilder.Register(&MongoDBSearch{}, &MongoDBSearchList{}) } +type Prometheus struct { + // Port where metrics endpoint will be exposed on. Defaults to 9946. + // +optional + // +kubebuilder:validation:Minimum=0 + // +kubebuilder:validation:Maximum=65535 + Port int `json:"port,omitempty"` +} + +func (p *Prometheus) GetPort() int32 { + if p.Port == 0 { + return MongotDefaultPrometheusPort + } + + //nolint:gosec + return int32(p.Port) +} + 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 @@ -54,6 +71,9 @@ type MongoDBSearchSpec struct { // +kubebuilder:validation:Enum=TRACE;DEBUG;INFO;WARN;ERROR // +optional LogLevel mdb.LogLevel `json:"logLevel,omitempty"` + // Configure prometheus metrics endpoint in mongot. If not set, the metrics endpoint will be disabled. + // +optional + Prometheus *Prometheus `json:"prometheus,omitempty"` } type MongoDBSource struct { @@ -218,10 +238,6 @@ func (s *MongoDBSearch) GetMongotGrpcPort() int32 { return MongotDefaultGrpcPort } -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} @@ -263,3 +279,7 @@ func (s *MongoDBSearch) GetEffectiveMongotPort() int32 { } return s.GetMongotGrpcPort() } + +func (s *MongoDBSearch) GetPrometheus() *Prometheus { + return s.Spec.Prometheus +} diff --git a/api/v1/search/zz_generated.deepcopy.go b/api/v1/search/zz_generated.deepcopy.go index d18d025f8..2064e07c4 100644 --- a/api/v1/search/zz_generated.deepcopy.go +++ b/api/v1/search/zz_generated.deepcopy.go @@ -160,6 +160,11 @@ func (in *MongoDBSearchSpec) DeepCopyInto(out *MongoDBSearchSpec) { (*in).DeepCopyInto(*out) } in.Security.DeepCopyInto(&out.Security) + if in.Prometheus != nil { + in, out := &in.Prometheus, &out.Prometheus + *out = new(Prometheus) + **out = **in + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MongoDBSearchSpec. @@ -228,6 +233,21 @@ func (in *MongoDBSource) DeepCopy() *MongoDBSource { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Prometheus) DeepCopyInto(out *Prometheus) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Prometheus. +func (in *Prometheus) DeepCopy() *Prometheus { + if in == nil { + return nil + } + out := new(Prometheus) + 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 diff --git a/changelog/20251015_other_remove_legacy_search_coordinator_polyfill.md b/changelog/20251015_other_remove_legacy_search_coordinator_polyfill.md deleted file mode 100644 index 0616cc062..000000000 --- a/changelog/20251015_other_remove_legacy_search_coordinator_polyfill.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -kind: other -date: 2025-10-15 ---- - -* Simplified MongoDB Search setup: Removed the custom Search Coordinator polyfill (a piece of compatibility code previously needed to add the required permissions), as MongoDB 8.2.0 and later now include the necessary permissions via the built-in searchCoordinator role. diff --git a/changelog/20251030_feature_update_mongodb_search_2nd_preview.md b/changelog/20251030_feature_update_mongodb_search_2nd_preview.md new file mode 100644 index 000000000..61a61849f --- /dev/null +++ b/changelog/20251030_feature_update_mongodb_search_2nd_preview.md @@ -0,0 +1,14 @@ +--- +kind: feature +date: 2025-10-30 +--- + +* **MongoDBSearch**: + * Switched to gRPC and mTLS for internal communication between mongod and mongot. + * Since MCK 1.4 the `mongod` and `mongot` processess communicated using the MongoDB Wire Protocol and used keyfile authentication. This release switches that to gRPC with mTLS authentication. gRPC will allow for load-balancing search queries against multiple `mongot` processes in the future, and mTLS decouples the internal cluster authentication mode and credentials among `mongod` processes from the connection to the `mongot` process. The Operator will automatically enable gRPC for existing and new workloads, and will enable mTLS authentication if both Database Server and `MongoDBSearch` resource are configured for TLS. + * Exposed configuration settings for mongot's prometheus metrics endpoint. + * By default, if `spec.prometheus` field is not provided then metrics endpoint in mongot is disabled. **This is a breaking change**. Previously the metrics endpoing was always enabled on port 9946. + * To enable prometheus metrics endpoint specify empty `spec.prometheus:` field. It will enable metrics endpoint on a default port (9946). To change the port, set it in `spec.prometheus.port` field. + * Simplified MongoDB Search setup: Removed the custom Search Coordinator polyfill (a piece of compatibility code previously needed to add the required permissions), as MongoDB 8.2.0 and later now include the necessary permissions via the built-in searchCoordinator role. + * Updated the default `mongodb/mongodb-search` image version to 0.55.0. This is the version MCK uses if `.spec.version` is not specified. + * MongoDB deployments using X509 internal cluster authentication are now supported. Previously MongoDB Search required SCRAM authentication among members of a MongoDB replica set. Note: SCRAM client authentication is still required, this change merely relaxes the requirements on internal cluster authentication. diff --git a/changelog/20251030_feature_update_mongodb_search_to_use_grpc_and_mtls_for.md b/changelog/20251030_feature_update_mongodb_search_to_use_grpc_and_mtls_for.md deleted file mode 100644 index 1bcecd340..000000000 --- a/changelog/20251030_feature_update_mongodb_search_to_use_grpc_and_mtls_for.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -kind: feature -date: 2025-10-30 ---- - -* **MongoDBSearch**: Switch to gRPC and mTLS for internal communication - Since MCK 1.4 the `mongod` and `mongot` processess communicated using the MongoDB Wire Protocol and used keyfile authentication. This release switches that to gRPC with mTLS authentication. gRPC will allow for load-balancing search queries against multiple `mongot` processes in the future, and mTLS decouples the internal cluster authentication mode and credentials among `mongod` processes from the connection to the `mongot` process. The Operator will automatically enable gRPC for existing and new workloads, and will enable mTLS authentication if both Database Server and `MongoDBSearch` resource are configured for TLS. \ No newline at end of file diff --git a/changelog/20251103_feature_mongodbsearch_mongodb_deployments_using_x509.md b/changelog/20251103_feature_mongodbsearch_mongodb_deployments_using_x509.md deleted file mode 100644 index 01362c4c0..000000000 --- a/changelog/20251103_feature_mongodbsearch_mongodb_deployments_using_x509.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -kind: feature -date: 2025-11-03 ---- - -* **MongoDBSearch**: MongoDB deployments using X509 internal cluster authentication are now supported. Previously MongoDB Search required SCRAM authentication among members of a MongoDB replica set. Note: SCRAM client authentication is still required, this change merely relaxes the requirements on internal cluster authentication. - diff --git a/changelog/20251106_feature_mongodbsearch_updated_the_default_mongodbmongodb.md b/changelog/20251106_feature_mongodbsearch_updated_the_default_mongodbmongodb.md index 540075ffd..583437307 100644 --- a/changelog/20251106_feature_mongodbsearch_updated_the_default_mongodbmongodb.md +++ b/changelog/20251106_feature_mongodbsearch_updated_the_default_mongodbmongodb.md @@ -2,5 +2,3 @@ kind: feature date: 2025-11-06 --- - -* **MongoDBSearch**: Updated the default `mongodb/mongodb-search` image version to 0.55.0. This is the version MCK uses if `.spec.version` is not specified. diff --git a/config/crd/bases/mongodb.com_mongodbsearch.yaml b/config/crd/bases/mongodb.com_mongodbsearch.yaml index 15153ba25..eacc0e71d 100644 --- a/config/crd/bases/mongodb.com_mongodbsearch.yaml +++ b/config/crd/bases/mongodb.com_mongodbsearch.yaml @@ -106,6 +106,17 @@ spec: type: string type: object type: object + prometheus: + description: Configure prometheus metrics endpoint in mongot. If not + set, the metrics endpoint will be disabled. + properties: + port: + description: Port where metrics endpoint will be exposed on. Defaults + to 9946. + maximum: 65535 + minimum: 0 + type: integer + type: object resourceRequirements: description: Configure resource requests and limits for the MongoDB Search pods. diff --git a/controllers/operator/appdbreplicaset_controller_test.go b/controllers/operator/appdbreplicaset_controller_test.go index 947e40218..951603ea1 100644 --- a/controllers/operator/appdbreplicaset_controller_test.go +++ b/controllers/operator/appdbreplicaset_controller_test.go @@ -3,10 +3,10 @@ package operator import ( "context" "encoding/json" + "errors" "fmt" "os" "path/filepath" - "strings" "testing" "time" @@ -54,7 +54,6 @@ func init() { // getReleaseJsonPath searches for a specified target directory by traversing the directory tree backwards from the current working directory func getReleaseJsonPath() (string, error) { - repositoryRootDirName := "mongodb-kubernetes" releaseFileName := "release.json" currentDir, err := os.Getwd() @@ -62,7 +61,7 @@ func getReleaseJsonPath() (string, error) { return "", err } for currentDir != "/" { - if strings.HasSuffix(currentDir, repositoryRootDirName) { + if _, err := os.Stat(filepath.Join(currentDir, releaseFileName)); !errors.Is(err, os.ErrNotExist) { return filepath.Join(currentDir, releaseFileName), nil } currentDir = filepath.Dir(currentDir) diff --git a/controllers/operator/mongodbsearch_controller_test.go b/controllers/operator/mongodbsearch_controller_test.go index 904424df5..ad1fc7a14 100644 --- a/controllers/operator/mongodbsearch_controller_test.go +++ b/controllers/operator/mongodbsearch_controller_test.go @@ -8,6 +8,7 @@ import ( "github.com/ghodss/yaml" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "k8s.io/apimachinery/pkg/types" "k8s.io/utils/ptr" "sigs.k8s.io/controller-runtime/pkg/client" @@ -22,12 +23,12 @@ import ( "github.com/mongodb/mongodb-kubernetes/api/v1/status" userv1 "github.com/mongodb/mongodb-kubernetes/api/v1/user" "github.com/mongodb/mongodb-kubernetes/controllers/operator/mock" - "github.com/mongodb/mongodb-kubernetes/controllers/operator/workflow" "github.com/mongodb/mongodb-kubernetes/controllers/searchcontroller" 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" + "github.com/mongodb/mongodb-kubernetes/pkg/util" ) func newMongoDBCommunity(name, namespace string) *mdbcv1.MongoDBCommunity { @@ -135,10 +136,6 @@ func buildExpectedMongotConfig(search *searchv1.MongoDBSearch, mdbc *mdbcv1.Mong }, Wireproto: wireprotoServer, }, - Metrics: mongot.ConfigMetrics{ - Enabled: true, - Address: fmt.Sprintf("0.0.0.0:%d", search.GetMongotMetricsPort()), - }, HealthCheck: mongot.ConfigHealthCheck{ Address: fmt.Sprintf("0.0.0.0:%d", search.GetMongotHealthCheckPort()), }, @@ -205,22 +202,16 @@ func TestMongoDBSearchReconcile_Success(t *testing.T) { mdbc := newMongoDBCommunity("mdb", mock.TestNamespace) reconciler, c := newSearchReconciler(mdbc, search) - res, err := reconciler.Reconcile( - ctx, - reconcile.Request{NamespacedName: types.NamespacedName{Name: search.Name, Namespace: search.Namespace}}, - ) - expected, _ := workflow.OK().ReconcileResult() - assert.NoError(t, err) - assert.Equal(t, expected, res) + checkSearchReconcileSuccessful(ctx, t, reconciler, c, search) svc := &corev1.Service{} - err = c.Get(ctx, search.SearchServiceNamespacedName(), svc) + err := c.Get(ctx, search.SearchServiceNamespacedName(), svc) assert.NoError(t, err) servicePortNames := []string{} for _, port := range svc.Spec.Ports { servicePortNames = append(servicePortNames, port.Name) } - expectedPortNames := []string{"mongot-grpc", "metrics", "healthcheck"} + expectedPortNames := []string{"mongot-grpc", "healthcheck"} if tc.withWireproto { expectedPortNames = append(expectedPortNames, "mongot-wireproto") } @@ -254,7 +245,7 @@ func checkSearchReconcileFailed( reconcile.Request{NamespacedName: types.NamespacedName{Name: search.Name, Namespace: search.Namespace}}, ) assert.NoError(t, err) - assert.True(t, res.RequeueAfter > 0) + assert.Less(t, res.RequeueAfter, util.TWENTY_FOUR_HOURS) updated := &searchv1.MongoDBSearch{} assert.NoError(t, c.Get(ctx, types.NamespacedName{Name: search.Name, Namespace: search.Namespace}, updated)) @@ -262,6 +253,34 @@ func checkSearchReconcileFailed( assert.Contains(t, updated.Status.Message, expectedMsg) } +// checkSearchReconcileSuccessful performs reconcile to check if it gets to a Running state. +// In case it's a first reconcile and still Pending it's retried with mocked sts simulated as ready. +func checkSearchReconcileSuccessful( + ctx context.Context, + t *testing.T, + reconciler *MongoDBSearchReconciler, + c client.Client, + search *searchv1.MongoDBSearch, +) { + namespacedName := types.NamespacedName{Name: search.Name, Namespace: search.Namespace} + res, err := reconciler.Reconcile(ctx, reconcile.Request{NamespacedName: namespacedName}) + require.NoError(t, err) + mdbs := &searchv1.MongoDBSearch{} + require.NoError(t, c.Get(ctx, namespacedName, mdbs)) + if mdbs.Status.Phase == status.PhasePending { + // mark mocked search statefulset as ready to not return Pending this time + require.NoError(t, mock.MarkAllStatefulSetsAsReady(ctx, search.Namespace, c)) + + res, err = reconciler.Reconcile(ctx, reconcile.Request{NamespacedName: namespacedName}) + require.NoError(t, err) + mdbs = &searchv1.MongoDBSearch{} + require.NoError(t, c.Get(ctx, namespacedName, mdbs)) + } + + require.Equal(t, util.TWENTY_FOUR_HOURS, res.RequeueAfter) + require.Equal(t, status.PhaseRunning, mdbs.Status.Phase) +} + func TestMongoDBSearchReconcile_InvalidVersion(t *testing.T) { ctx := context.Background() search := newMongoDBSearch("search", mock.TestNamespace, "mdb") diff --git a/controllers/searchcontroller/mongodbsearch_reconcile_helper.go b/controllers/searchcontroller/mongodbsearch_reconcile_helper.go index d3a5c32a0..1548850e8 100644 --- a/controllers/searchcontroller/mongodbsearch_reconcile_helper.go +++ b/controllers/searchcontroller/mongodbsearch_reconcile_helper.go @@ -76,7 +76,7 @@ func (r *MongoDBSearchReconcileHelper) Reconcile(ctx context.Context, log *zap.S if _, err := commoncontroller.UpdateStatus(ctx, r.client, r.mdbSearch, workflowStatus, log); err != nil { return workflow.Failed(err) } - return workflow.OK() + return workflowStatus } func (r *MongoDBSearchReconcileHelper) reconcile(ctx context.Context, log *zap.SugaredLogger) workflow.Status { @@ -312,7 +312,7 @@ func buildSearchHeadlessService(search *searchv1.MongoDBSearch) corev1.Service { SetLabels(labels). SetServiceType(corev1.ServiceTypeClusterIP). SetClusterIP("None"). - SetPublishNotReadyAddresses(true). + SetPublishNotReadyAddresses(false). SetOwnerReferences(search.GetOwnerReferences()) if search.IsWireprotoEnabled() { @@ -331,12 +331,14 @@ func buildSearchHeadlessService(search *searchv1.MongoDBSearch) corev1.Service { TargetPort: intstr.FromInt32(search.GetMongotGrpcPort()), }) - serviceBuilder.AddPort(&corev1.ServicePort{ - Name: "metrics", - Protocol: corev1.ProtocolTCP, - Port: search.GetMongotMetricsPort(), - TargetPort: intstr.FromInt32(search.GetMongotMetricsPort()), - }) + if prometheus := search.GetPrometheus(); prometheus != nil { + serviceBuilder.AddPort(&corev1.ServicePort{ + Name: "prometheus", + Protocol: corev1.ProtocolTCP, + Port: prometheus.GetPort(), + TargetPort: intstr.FromInt32(prometheus.GetPort()), + }) + } serviceBuilder.AddPort(&corev1.ServicePort{ Name: "healthcheck", @@ -385,10 +387,14 @@ func createMongotConfig(search *searchv1.MongoDBSearch, db SearchSourceDBResourc }, } } - config.Metrics = mongot.ConfigMetrics{ - Enabled: true, - Address: fmt.Sprintf("0.0.0.0:%d", search.GetMongotMetricsPort()), + + if prometheus := search.GetPrometheus(); prometheus != nil { + config.Metrics = mongot.ConfigMetrics{ + Enabled: true, + Address: fmt.Sprintf("0.0.0.0:%d", prometheus.GetPort()), + } } + config.HealthCheck = mongot.ConfigHealthCheck{ Address: fmt.Sprintf("0.0.0.0:%d", search.GetMongotHealthCheckPort()), } @@ -473,9 +479,9 @@ func (r *MongoDBSearchReconcileHelper) getMongotImage() string { return "" } - for _, container := range r.mdbSearch.Spec.StatefulSetConfiguration.SpecWrapper.Spec.Template.Spec.Containers { - if container.Name == MongotContainerName { - return container.Image + for _, c := range r.mdbSearch.Spec.StatefulSetConfiguration.SpecWrapper.Spec.Template.Spec.Containers { + if c.Name == MongotContainerName { + return c.Image } } diff --git a/controllers/searchcontroller/mongodbsearch_reconcile_helper_test.go b/controllers/searchcontroller/mongodbsearch_reconcile_helper_test.go index affc30c52..2017df5ad 100644 --- a/controllers/searchcontroller/mongodbsearch_reconcile_helper_test.go +++ b/controllers/searchcontroller/mongodbsearch_reconcile_helper_test.go @@ -1,22 +1,104 @@ package searchcontroller import ( + "context" "fmt" "strings" "testing" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "go.uber.org/zap" "sigs.k8s.io/controller-runtime/pkg/client" + corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" searchv1 "github.com/mongodb/mongodb-kubernetes/api/v1/search" userv1 "github.com/mongodb/mongodb-kubernetes/api/v1/user" "github.com/mongodb/mongodb-kubernetes/controllers/operator/mock" + "github.com/mongodb/mongodb-kubernetes/controllers/operator/workflow" mdbcv1 "github.com/mongodb/mongodb-kubernetes/mongodb-community-operator/api/v1" kubernetesClient "github.com/mongodb/mongodb-kubernetes/mongodb-community-operator/pkg/kube/client" ) +func init() { + logger, _ := zap.NewDevelopment() + zap.ReplaceGlobals(logger) +} + +func newTestMongoDBSearch(name, namespace string, modifications ...func(*searchv1.MongoDBSearch)) *searchv1.MongoDBSearch { + mdbSearch := &searchv1.MongoDBSearch{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + }, + Spec: searchv1.MongoDBSearchSpec{ + Source: &searchv1.MongoDBSource{ + MongoDBResourceRef: &userv1.MongoDBResourceRef{ + Name: "test-mongodb", + }, + }, + }, + } + + for _, modify := range modifications { + modify(mdbSearch) + } + + return mdbSearch +} + +func newTestMongoDBCommunity(name, namespace string, modifications ...func(*mdbcv1.MongoDBCommunity)) *mdbcv1.MongoDBCommunity { + mdbc := &mdbcv1.MongoDBCommunity{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + }, + Spec: mdbcv1.MongoDBCommunitySpec{ + Version: "8.2.0", + Members: 3, + }, + } + + for _, modify := range modifications { + modify(mdbc) + } + + return mdbc +} + +func newTestOperatorSearchConfig() OperatorSearchConfig { + config := OperatorSearchConfig{ + SearchRepo: "test-repo", + SearchName: "mongot", + SearchVersion: "0.0.0", + } + + return config +} + +func newTestFakeClient(objects ...client.Object) kubernetesClient.Client { + clientBuilder := mock.NewEmptyFakeClientBuilder() + clientBuilder.WithIndex(&searchv1.MongoDBSearch{}, MongoDBSearchIndexFieldName, func(obj client.Object) []string { + mdbResource := obj.(*searchv1.MongoDBSearch).GetMongoDBResourceRef() + return []string{mdbResource.Namespace + "/" + mdbResource.Name} + }) + clientBuilder.WithObjects(objects...) + return kubernetesClient.NewClient(clientBuilder.Build()) +} + +func reconcileMongoDBSearch(ctx context.Context, fakeClient kubernetesClient.Client, mdbSearch *searchv1.MongoDBSearch, mdbc *mdbcv1.MongoDBCommunity, operatorConfig OperatorSearchConfig) workflow.Status { + helper := NewMongoDBSearchReconcileHelper( + fakeClient, + mdbSearch, + NewCommunityResourceSearchSource(mdbc), + operatorConfig, + ) + + return helper.Reconcile(ctx, zap.S()) +} + func TestMongoDBSearchReconcileHelper_ValidateSingleMongoDBSearchForSearchSource(t *testing.T) { mdbSearchSpec := searchv1.MongoDBSearchSpec{ Source: &searchv1.MongoDBSource{ @@ -152,3 +234,96 @@ func TestGetMongodConfigParameters_TransportAndPorts(t *testing.T) { }) } } + +func assertServiceBasicProperties(t *testing.T, svc corev1.Service, mdbSearch *searchv1.MongoDBSearch) { + t.Helper() + svcName := mdbSearch.SearchServiceNamespacedName() + + assert.Equal(t, svcName.Name, svc.Name) + assert.Equal(t, svcName.Namespace, svc.Namespace) + assert.Equal(t, "ClusterIP", string(svc.Spec.Type)) + assert.Equal(t, "None", svc.Spec.ClusterIP) + assert.False(t, svc.Spec.PublishNotReadyAddresses) + + expectedAppLabel := svcName.Name + assert.Equal(t, expectedAppLabel, svc.Labels["app"]) + assert.Equal(t, expectedAppLabel, svc.Spec.Selector["app"]) +} + +func assertServicePorts(t *testing.T, svc corev1.Service, expectedPorts map[string]int32) { + t.Helper() + + portMap := make(map[string]int32) + for _, port := range svc.Spec.Ports { + portMap[port.Name] = port.Port + } + + assert.Len(t, svc.Spec.Ports, len(expectedPorts), "Expected %d ports but got %d", len(expectedPorts), len(svc.Spec.Ports)) + + for portName, expectedPort := range expectedPorts { + actualPort, exists := portMap[portName] + assert.True(t, exists, "Expected port %s to exist", portName) + assert.Equal(t, expectedPort, actualPort, "Port %s has wrong value", portName) + } +} + +func TestMongoDBSearchReconcileHelper_ServiceCreation(t *testing.T) { + cases := []struct { + name string + modifySearch func(*searchv1.MongoDBSearch) + expectedPorts map[string]int32 + }{ + { + name: "Default configuration with prometheus enabled", + modifySearch: func(search *searchv1.MongoDBSearch) { + search.Spec.Prometheus = &searchv1.Prometheus{} + }, + expectedPorts: map[string]int32{ + "mongot-grpc": searchv1.MongotDefaultGrpcPort, + "prometheus": searchv1.MongotDefaultPrometheusPort, + "healthcheck": searchv1.MongotDefautHealthCheckPort, + }, + }, + { + name: "Prometheus enabled with custom port", + modifySearch: func(search *searchv1.MongoDBSearch) { + search.Spec.Prometheus = &searchv1.Prometheus{ + Port: 9999, + } + }, + expectedPorts: map[string]int32{ + "mongot-grpc": searchv1.MongotDefaultGrpcPort, + "prometheus": 9999, + "healthcheck": searchv1.MongotDefautHealthCheckPort, + }, + }, + { + name: "Prometheus disabled", + modifySearch: func(search *searchv1.MongoDBSearch) { + search.Spec.Prometheus = nil + }, + expectedPorts: map[string]int32{ + "mongot-grpc": searchv1.MongotDefaultGrpcPort, + "healthcheck": searchv1.MongotDefautHealthCheckPort, + }, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + mdbSearch := newTestMongoDBSearch("test-mongodb-search", "test", tc.modifySearch) + mdbc := newTestMongoDBCommunity("test-mongodb", "test") + fakeClient := newTestFakeClient(mdbSearch, mdbc) + + reconcileMongoDBSearch(t.Context(), fakeClient, mdbSearch, mdbc, newTestOperatorSearchConfig()) + + svcName := mdbSearch.SearchServiceNamespacedName() + svc, err := fakeClient.GetService(t.Context(), svcName) + require.NoError(t, err) + require.NotNil(t, svc) + + assertServiceBasicProperties(t, svc, mdbSearch) + assertServicePorts(t, svc, tc.expectedPorts) + }) + } +} diff --git a/docker/mongodb-kubernetes-tests/tests/search/search_enterprise_tls.py b/docker/mongodb-kubernetes-tests/tests/search/search_enterprise_tls.py index b6870b507..117c1e69d 100644 --- a/docker/mongodb-kubernetes-tests/tests/search/search_enterprise_tls.py +++ b/docker/mongodb-kubernetes-tests/tests/search/search_enterprise_tls.py @@ -1,5 +1,11 @@ import yaml -from kubetester import create_or_update_secret, run_periodically, try_load +from kubetester import ( + create_or_update_secret, + get_service, + kubetester, + run_periodically, + 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 @@ -212,6 +218,43 @@ def check_mongod_parameters(): run_periodically(check_mongod_parameters, timeout=600) +@mark.e2e_search_enterprise_tls +def test_search_deploy_tools_pod(namespace: str): + deploy_mongodb_tools_pod(namespace) + + +@mark.e2e_search_enterprise_tls +def test_search_verify_prometheus_disabled_initially(mdbs: MongoDBSearch): + assert_search_service_prometheus_port(mdbs, should_exist=False) + assert_search_pod_prometheus_endpoint(mdbs, should_be_accessible=False) + + +@mark.e2e_search_enterprise_tls +def test_search_enable_prometheus_on_default_port(mdbs: MongoDBSearch): + mdbs["spec"]["prometheus"] = {} + mdbs.update() + mdbs.assert_reaches_phase(Phase.Running, timeout=300) + + +@mark.e2e_search_enterprise_tls +def test_search_verify_prometheus_enabled(mdbs: MongoDBSearch): + assert_search_service_prometheus_port(mdbs, should_exist=True, expected_port=9946) + assert_search_pod_prometheus_endpoint(mdbs, should_be_accessible=True, port=9946) + + +@mark.e2e_search_enterprise_tls +def test_search_change_prometheus_to_custom_port(mdbs: MongoDBSearch): + mdbs["spec"]["prometheus"] = {"port": 10000} + mdbs.update() + mdbs.assert_reaches_phase(Phase.Running, timeout=300) + + +@mark.e2e_search_enterprise_tls +def test_search_verify_prometheus_enabled_on_custom_port(mdbs: MongoDBSearch): + assert_search_service_prometheus_port(mdbs, should_exist=True, expected_port=10000) + assert_search_pod_prometheus_endpoint(mdbs, should_be_accessible=True, port=10000) + + @mark.e2e_search_enterprise_tls def test_search_restore_sample_database(mdb: MongoDB): get_admin_sample_movies_helper(mdb).restore_sample_database() @@ -247,3 +290,80 @@ def get_user_sample_movies_helper(mdb): get_connection_string(mdb, USER_NAME, USER_PASSWORD), use_ssl=True, ca_path=get_issuer_ca_filepath() ) ) + + +def assert_search_service_prometheus_port(mdbs: MongoDBSearch, should_exist: bool, expected_port: int = 9946): + service_name = f"{mdbs.name}-search-svc" + service = get_service(mdbs.namespace, service_name) + assert service is not None + + ports = {p.name: p.port for p in service.spec.ports} + + if should_exist: + assert "prometheus" in ports + assert ports["prometheus"] == expected_port + else: + assert "prometheus" not in ports + + +def deploy_mongodb_tools_pod(namespace: str): + """ + Deploys a bastion pod to perform connectivty checks using pod exec. It's similar to how we + run connectivity checks in snippets. + """ + from kubetester import get_pod_when_ready + + pod_body = { + "apiVersion": "v1", + "kind": "Pod", + "metadata": { + "name": "mongodb-tools-pod", + "labels": {"app": "mongodb-tools"}, + }, + "spec": { + "containers": [ + { + "name": "mongodb-tools", + "image": "mongodb/mongodb-community-server:8.0-ubi8", + "command": ["/bin/bash", "-c"], + "args": ["sleep infinity"], + } + ], + "restartPolicy": "Never", + }, + } + + try: + KubernetesTester.create_pod(namespace, pod_body) + logger.info(f"Created mongodb-tools-pod in namespace {namespace}") + except Exception as e: + logger.info(f"Pod may already exist: {e}") + + get_pod_when_ready(namespace, "app=mongodb-tools", default_retry=60) + logger.info("mongodb-tools-pod is ready") + + +def assert_search_pod_prometheus_endpoint(mdbs: MongoDBSearch, should_be_accessible: bool, port: int = 9946): + service_fqdn = f"{mdbs.name}-search-svc.{mdbs.namespace}.svc.cluster.local" + url = f"http://{service_fqdn}:{port}/metrics" + + if should_be_accessible: + # We don't necessarily need the connectivity test to run via a bastion pod as we could connect to it directly when running test in pod. + # But it's not requiring forwarding when running locally. + result = KubernetesTester.run_command_in_pod_container( + "mongodb-tools-pod", mdbs.namespace, ["curl", "-f", "-s", url], container="mongodb-tools" + ) + assert "# HELP" in result or "# TYPE" in result + + logger.info(f"Prometheus endpoint is accessible at {url} and returning metrics") + else: + try: + result = KubernetesTester.run_command_in_pod_container( + "mongodb-tools-pod", + mdbs.namespace, + ["curl", "-f", "-s", "--max-time", "5", url], + container="mongodb-tools", + ) + assert False, f"Prometheus endpoint should not be accessible but got: {result}" + except Exception as e: + logger.info(f"Expected failure: Prometheus endpoint is not accessible at {url}: {e}") diff --git a/helm_chart/crds/mongodb.com_mongodbsearch.yaml b/helm_chart/crds/mongodb.com_mongodbsearch.yaml index 15153ba25..eacc0e71d 100644 --- a/helm_chart/crds/mongodb.com_mongodbsearch.yaml +++ b/helm_chart/crds/mongodb.com_mongodbsearch.yaml @@ -106,6 +106,17 @@ spec: type: string type: object type: object + prometheus: + description: Configure prometheus metrics endpoint in mongot. If not + set, the metrics endpoint will be disabled. + properties: + port: + description: Port where metrics endpoint will be exposed on. Defaults + to 9946. + maximum: 65535 + minimum: 0 + type: integer + type: object resourceRequirements: description: Configure resource requests and limits for the MongoDB Search pods. diff --git a/main.go b/main.go index 5e5185825..0f4223b46 100644 --- a/main.go +++ b/main.go @@ -30,6 +30,7 @@ import ( corev1 "k8s.io/api/core/v1" utilruntime "k8s.io/apimachinery/pkg/util/runtime" clientgoscheme "k8s.io/client-go/kubernetes/scheme" + golog "log" localruntime "runtime" ctrl "sigs.k8s.io/controller-runtime" runtime_cluster "sigs.k8s.io/controller-runtime/pkg/cluster" @@ -528,8 +529,8 @@ func getOperatorEnv() util.OperatorEnvironment { operatorEnv := util.OperatorEnvironment(operatorFromEnv) if !validateOperatorEnv(operatorEnv) { operatorEnvOnce.Do(func() { - log.Infof("Configured environment %s, not recognized. Must be one of %v", operatorEnv, operatorEnvironments) - log.Infof("Using default environment, %s, instead", util.OperatorEnvironmentDev) + golog.Printf("Configured environment %s, not recognized. Must be one of %v", operatorEnv, operatorEnvironments) + golog.Printf("Using default environment, %s, instead", util.OperatorEnvironmentDev) }) operatorEnv = util.OperatorEnvironmentDev } diff --git a/public/crds.yaml b/public/crds.yaml index beeaf741d..b9b9a94ad 100644 --- a/public/crds.yaml +++ b/public/crds.yaml @@ -4128,6 +4128,17 @@ spec: type: string type: object type: object + prometheus: + description: Configure prometheus metrics endpoint in mongot. If not + set, the metrics endpoint will be disabled. + properties: + port: + description: Port where metrics endpoint will be exposed on. Defaults + to 9946. + maximum: 65535 + minimum: 0 + type: integer + type: object resourceRequirements: description: Configure resource requests and limits for the MongoDB Search pods.