Skip to content

Commit b823e40

Browse files
committed
Search: support multiple mongots
# Conflicts: # .evergreen-tasks.yml # .evergreen.yml # docker/mongodb-kubernetes-tests/tests/common/mongodb_tools_pod/mongodb_tools_pod.py # docker/mongodb-kubernetes-tests/tests/search/fixtures/search-sharded-external-mongod.yaml
1 parent a624e47 commit b823e40

21 files changed

+3631
-44
lines changed

.evergreen-tasks.yml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1360,6 +1360,16 @@ tasks:
13601360
commands:
13611361
- func: "e2e_test"
13621362

1363+
- name: e2e_search_sharded_enterprise_external_lb
1364+
tags: [ "patch-run" ]
1365+
commands:
1366+
- func: "e2e_test"
1367+
1368+
- name: e2e_search_sharded_enterprise_external_mongod
1369+
tags: [ "patch-run" ]
1370+
commands:
1371+
- func: "e2e_test"
1372+
13631373
- name: e2e_search_sharded_external_mongod_single_mongot
13641374
tags: [ "patch-run" ]
13651375
commands:

.evergreen.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -812,6 +812,8 @@ task_groups:
812812
- e2e_search_enterprise_tls
813813
- e2e_search_enterprise_x509_cluster_auth
814814
- e2e_search_sharded_external_mongod_single_mongot
815+
- e2e_search_sharded_enterprise_external_lb
816+
- e2e_search_sharded_enterprise_external_mongod
815817
<<: *teardown_group
816818

817819
# this task group contains just a one task, which is smoke testing whether the operator

api/v1/search/mongodbsearch_types.go

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package search
22

33
import (
44
"fmt"
5+
"strings"
56

67
"k8s.io/apimachinery/pkg/runtime/schema"
78
"k8s.io/apimachinery/pkg/types"
@@ -16,6 +17,9 @@ import (
1617
"github.com/mongodb/mongodb-kubernetes/mongodb-community-operator/api/v1/common"
1718
)
1819

20+
// ShardNamePlaceholder is the placeholder used in endpoint templates for sharded clusters
21+
const ShardNamePlaceholder = "{shardName}"
22+
1923
const (
2024
MongotDefaultWireprotoPort int32 = 27027
2125
MongotDefaultGrpcPort int32 = 27028
@@ -51,6 +55,15 @@ type MongoDBSearchSpec struct {
5155
// MongoDB database connection details from which MongoDB Search will synchronize data to build indexes.
5256
// +optional
5357
Source *MongoDBSource `json:"source"`
58+
// Replicas is the number of mongot pods to deploy.
59+
// For ReplicaSet source: this many mongot pods total.
60+
// For Sharded source: this many mongot pods per shard.
61+
// When Replicas > 1, a load balancer configuration (lb.mode: Unmanaged with lb.endpoint)
62+
// is required to distribute traffic across mongot instances.
63+
// +optional
64+
// +kubebuilder:validation:Minimum=1
65+
// +kubebuilder:default=1
66+
Replicas int `json:"replicas,omitempty"`
5467
// StatefulSetSpec which the operator will apply to the MongoDB Search StatefulSet at the end of the reconcile loop. Use to provide necessary customizations,
5568
// which aren't exposed as fields in the MongoDBSearch.spec.
5669
// +optional
@@ -75,6 +88,41 @@ type MongoDBSearchSpec struct {
7588
// `embedding` field of mongot config is generated using the values provided here.
7689
// +optional
7790
AutoEmbedding *EmbeddingConfig `json:"autoEmbedding,omitempty"`
91+
// LoadBalancer configures how mongod/mongos connect to mongot (Managed vs Unmanaged/BYO LB).
92+
// +optional
93+
LoadBalancer *LoadBalancerConfig `json:"lb,omitempty"`
94+
}
95+
96+
// LBMode defines the load balancer mode for Search
97+
// +kubebuilder:validation:Enum=Managed;Unmanaged
98+
type LBMode string
99+
100+
const (
101+
// LBModeManaged indicates operator-managed Envoy load balancer
102+
LBModeManaged LBMode = "Managed"
103+
// LBModeUnmanaged indicates user-provided L7 load balancer (BYO LB)
104+
LBModeUnmanaged LBMode = "Unmanaged"
105+
)
106+
107+
// LoadBalancerConfig configures how mongod/mongos connect to mongot
108+
type LoadBalancerConfig struct {
109+
// Mode specifies the load balancer mode: Managed (operator-managed) or Unmanaged (BYO L7 LB)
110+
// +kubebuilder:validation:Required
111+
Mode LBMode `json:"mode"`
112+
// Endpoint is the LB endpoint for ReplicaSet, or a template for sharded clusters.
113+
// For sharded clusters, use {shardName} as a placeholder substituted with the actual shard name.
114+
// Example: "lb-{shardName}.example.com:27028"
115+
// +optional
116+
Endpoint string `json:"endpoint,omitempty"`
117+
// Envoy contains configuration for operator-managed Envoy load balancer
118+
// +optional
119+
Envoy *EnvoyConfig `json:"envoy,omitempty"`
120+
}
121+
122+
// EnvoyConfig contains configuration for operator-managed Envoy load balancer
123+
// Placeholder for future Envoy configuration options
124+
type EnvoyConfig struct {
125+
// Placeholder for future Envoy configuration
78126
}
79127

80128
type EmbeddingConfig struct {
@@ -400,6 +448,59 @@ func (s *MongoDBSearch) GetPrometheus() *Prometheus {
400448
return s.Spec.Prometheus
401449
}
402450

451+
func (s *MongoDBSearch) IsLBModeUnmanaged() bool {
452+
return s.Spec.LoadBalancer != nil && s.Spec.LoadBalancer.Mode == LBModeUnmanaged
453+
}
454+
455+
// IsReplicaSetUnmanagedLB returns true if this is a ReplicaSet unmanaged LB configuration.
456+
// An endpoint with a template placeholder ({shardName}) is NOT considered a ReplicaSet endpoint.
457+
func (s *MongoDBSearch) IsReplicaSetUnmanagedLB() bool {
458+
return s.IsLBModeUnmanaged() &&
459+
s.Spec.LoadBalancer.Endpoint != "" &&
460+
!s.HasEndpointTemplate()
461+
}
462+
463+
func (s *MongoDBSearch) GetReplicaSetUnmanagedLBEndpoint() string {
464+
if !s.IsReplicaSetUnmanagedLB() {
465+
return ""
466+
}
467+
return s.Spec.LoadBalancer.Endpoint
468+
}
469+
470+
// HasEndpointTemplate returns true if the endpoint contains the {shardName} template placeholder
471+
func (s *MongoDBSearch) HasEndpointTemplate() bool {
472+
if s.Spec.LoadBalancer == nil {
473+
return false
474+
}
475+
return strings.Contains(s.Spec.LoadBalancer.Endpoint, ShardNamePlaceholder)
476+
}
477+
478+
// IsShardedUnmanagedLB returns true if this is a sharded unmanaged LB configuration
479+
// identified by the presence of the {shardName} template placeholder in the endpoint.
480+
func (s *MongoDBSearch) IsShardedUnmanagedLB() bool {
481+
return s.IsLBModeUnmanaged() && s.HasEndpointTemplate()
482+
}
483+
484+
// GetEndpointForShard returns the endpoint for a specific shard by substituting
485+
// the {shardName} placeholder in the endpoint template.
486+
func (s *MongoDBSearch) GetEndpointForShard(shardName string) string {
487+
if !s.IsShardedUnmanagedLB() {
488+
return ""
489+
}
490+
return strings.ReplaceAll(s.Spec.LoadBalancer.Endpoint, ShardNamePlaceholder, shardName)
491+
}
492+
493+
func (s *MongoDBSearch) GetReplicas() int {
494+
if s.Spec.Replicas > 0 {
495+
return s.Spec.Replicas
496+
}
497+
return 1
498+
}
499+
500+
func (s *MongoDBSearch) HasMultipleReplicas() bool {
501+
return s.GetReplicas() > 1
502+
}
503+
403504
func (s *MongoDBSearch) MongotStatefulSetNamespacedNameForShard(shardName string) types.NamespacedName {
404505
return types.NamespacedName{Name: fmt.Sprintf("%s-mongot-%s", s.Name, shardName), Namespace: s.Namespace}
405506
}
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
package search
2+
3+
import (
4+
"errors"
5+
"strings"
6+
7+
v1 "github.com/mongodb/mongodb-kubernetes/api/v1"
8+
)
9+
10+
// ValidateSpec validates the MongoDBSearch spec
11+
func (s *MongoDBSearch) ValidateSpec() error {
12+
for _, res := range s.RunValidations() {
13+
if res.Level == v1.ErrorLevel {
14+
return errors.New(res.Msg)
15+
}
16+
}
17+
return nil
18+
}
19+
20+
// RunValidations runs all validation rules and returns the results
21+
func (s *MongoDBSearch) RunValidations() []v1.ValidationResult {
22+
validators := []func(*MongoDBSearch) v1.ValidationResult{
23+
validateLBConfig,
24+
validateUnmanagedLBConfig,
25+
validateEndpointTemplate,
26+
validateTLSConfig,
27+
}
28+
29+
var results []v1.ValidationResult
30+
for _, validator := range validators {
31+
res := validator(s)
32+
if res.Level > 0 {
33+
results = append(results, res)
34+
}
35+
}
36+
return results
37+
}
38+
39+
// validateLBConfig validates the load balancer configuration
40+
func validateLBConfig(s *MongoDBSearch) v1.ValidationResult {
41+
if s.Spec.LoadBalancer == nil {
42+
// LB config is optional
43+
return v1.ValidationSuccess()
44+
}
45+
46+
// Mode must be specified if LB config is present
47+
if s.Spec.LoadBalancer.Mode == "" {
48+
return v1.ValidationError("spec.lb.mode must be specified when spec.lb is configured")
49+
}
50+
51+
// Mode must be either Managed or Unmanaged
52+
if s.Spec.LoadBalancer.Mode != LBModeManaged && s.Spec.LoadBalancer.Mode != LBModeUnmanaged {
53+
return v1.ValidationError("spec.lb.mode must be either 'Managed' or 'Unmanaged', got '%s'", s.Spec.LoadBalancer.Mode)
54+
}
55+
56+
return v1.ValidationSuccess()
57+
}
58+
59+
// validateUnmanagedLBConfig validates that an endpoint is specified when mode is Unmanaged
60+
func validateUnmanagedLBConfig(s *MongoDBSearch) v1.ValidationResult {
61+
if s.Spec.LoadBalancer == nil || s.Spec.LoadBalancer.Mode != LBModeUnmanaged {
62+
return v1.ValidationSuccess()
63+
}
64+
65+
if s.Spec.LoadBalancer.Endpoint == "" {
66+
return v1.ValidationError("spec.lb.endpoint must be specified when spec.lb.mode is 'Unmanaged'")
67+
}
68+
69+
return v1.ValidationSuccess()
70+
}
71+
72+
// validateEndpointTemplate validates the endpoint template format
73+
func validateEndpointTemplate(s *MongoDBSearch) v1.ValidationResult {
74+
if !s.HasEndpointTemplate() {
75+
return v1.ValidationSuccess()
76+
}
77+
78+
endpoint := s.Spec.LoadBalancer.Endpoint
79+
80+
// Template must contain exactly one {shardName} placeholder
81+
count := strings.Count(endpoint, ShardNamePlaceholder)
82+
if count != 1 {
83+
return v1.ValidationError("spec.lb.endpoint template must contain exactly one %s placeholder, found %d", ShardNamePlaceholder, count)
84+
}
85+
86+
// Template should have some content before or after the placeholder
87+
if endpoint == ShardNamePlaceholder {
88+
return v1.ValidationError("spec.lb.endpoint template must contain more than just the %s placeholder", ShardNamePlaceholder)
89+
}
90+
91+
return v1.ValidationSuccess()
92+
}
93+
94+
// validateTLSConfig validates the TLS configuration
95+
func validateTLSConfig(s *MongoDBSearch) v1.ValidationResult {
96+
if s.Spec.Security.TLS == nil {
97+
return v1.ValidationSuccess()
98+
}
99+
100+
// TLS is valid in all cases:
101+
// 1. CertificateKeySecret.Name is specified - use explicit secret name
102+
// 2. CertsSecretPrefix is specified - use {prefix}-{resourceName}-search-cert
103+
// 3. Both are empty - use default {resourceName}-search-cert
104+
// No validation error needed as we always have a valid fallback
105+
106+
return v1.ValidationSuccess()
107+
}

api/v1/search/zz_generated.deepcopy.go

Lines changed: 40 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

config/crd/bases/mongodb.com_mongodbsearch.yaml

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,30 @@ spec:
7878
required:
7979
- embeddingModelAPIKeySecret
8080
type: object
81+
lb:
82+
description: LoadBalancer configures how mongod/mongos connect to
83+
mongot (Managed vs Unmanaged/BYO LB).
84+
properties:
85+
endpoint:
86+
description: |-
87+
Endpoint is the LB endpoint for ReplicaSet, or a template for sharded clusters.
88+
For sharded clusters, use {shardName} as a placeholder substituted with the actual shard name.
89+
Example: "lb-{shardName}.example.com:27028"
90+
type: string
91+
envoy:
92+
description: Envoy contains configuration for operator-managed
93+
Envoy load balancer
94+
type: object
95+
mode:
96+
description: 'Mode specifies the load balancer mode: Managed (operator-managed)
97+
or Unmanaged (BYO L7 LB)'
98+
enum:
99+
- Managed
100+
- Unmanaged
101+
type: string
102+
required:
103+
- mode
104+
type: object
81105
logLevel:
82106
description: Configure verbosity of mongot logs. Defaults to INFO
83107
if not set.
@@ -148,6 +172,16 @@ spec:
148172
minimum: 0
149173
type: integer
150174
type: object
175+
replicas:
176+
default: 1
177+
description: |-
178+
Replicas is the number of mongot pods to deploy.
179+
For ReplicaSet source: this many mongot pods total.
180+
For Sharded source: this many mongot pods per shard.
181+
When Replicas > 1, a load balancer configuration (lb.mode: Unmanaged with lb.endpoint)
182+
is required to distribute traffic across mongot instances.
183+
minimum: 1
184+
type: integer
151185
resourceRequirements:
152186
description: Configure resource requests and limits for the MongoDB
153187
Search pods.

controllers/operator/mongodbsearch_controller.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,9 @@ func (r *MongoDBSearchReconciler) getSourceMongoDBForSearch(ctx context.Context,
135135
}
136136
} else {
137137
r.watch.AddWatchedResourceIfNotAdded(sourceMongoDBResourceRef.Name, sourceMongoDBResourceRef.Namespace, watch.MongoDB, search.NamespacedName())
138-
if mdb.GetResourceType() == mdbv1.ShardedCluster {
138+
// For sharded clusters with unmanaged LB mode, use ShardedEnterpriseSearchSource
139+
// which provides per-shard host seeds and endpoint mapping.
140+
if mdb.GetResourceType() == mdbv1.ShardedCluster && search.IsShardedUnmanagedLB() {
139141
return searchcontroller.NewShardedEnterpriseSearchSource(mdb, search), nil
140142
}
141143
return searchcontroller.NewEnterpriseResourceSearchSource(mdb), nil

0 commit comments

Comments
 (0)