-
Notifications
You must be signed in to change notification settings - Fork 15
Basic enterprise search support #309
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: search/public-preview
Are you sure you want to change the base?
Changes from all commits
cbd8b1f
ef4880a
bd72b91
799a8dc
22b0670
f783f5d
aca9ee3
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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, agentCertSecretName, prometheusCertHash, true).OnErrorPrepend("Failed to create/update (Ops Manager reconciliation phase):") | ||
automationConfigStatus := r.updateOmDeploymentRs(ctx, conn, rs.Status.Members, rs, sts, log, caFilePath, agentCertSecretName, 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, agentCertSecretName, prometheusCertHash, false).OnErrorPrepend("Failed to create/update (Ops Manager reconciliation phase):") | ||
return r.updateOmDeploymentRs(ctx, conn, rs.Status.Members, rs, sts, log, caFilePath, agentCertSecretName, prometheusCertHash, false, shouldMirrorKeyfile).OnErrorPrepend("Failed to create/update (Ops Manager reconciliation phase):") | ||
}, | ||
func() workflow.Status { | ||
workflowStatus := create.HandlePVCResize(ctx, r.client, &sts, log) | ||
|
@@ -408,14 +417,27 @@ 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 | ||
} | ||
|
||
// 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, agentCertSecretName string, 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, agentCertSecretName string, 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 { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I wonder if we could make it part of the search reconciler. It feels a bit awkward to have this special code for making the search reconciler work properly. I feel we should limit changes only to the setParameters. We know the secret name and we can look and mirror it when we need it while reconciling MongoDBSearch resource. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. After reviewing the whole PR I think it's not even necessary as we already have mirroring on the search side in controllers/search_controller/mongodbsearch_reconcile_helper.go:142 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We need to read the keyfile contents from Ops Manager. Unfortunately in Enterprise the only copy of the keyfile is in the automation config in Ops Manager, unlike in Community where the Operator generates the keyfile and stores it in a secret we can just mount in mongot. I considered mirroring the keyfile into a secret inside the search reconciler, but then we'd need to establish the Ops Manager connection there, so I felt the replica set reconciler would be a smaller pain. FWIW we don't mirror the keyfile on the search side - the |
||
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()), | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do we the assumption that MongoDBSearch must be in the same namespace as MongoDB resource? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What I mean is that we don't copy/mirror it for community, so we don't need it for enterprise. We can just mount it directly. But this will only work if we limit it for the same namespace as we cannot ref secrets across namespaces. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We currently assume that the two resources are in the same namespace in Community as well, this is mostly a copy of the same code in the Community replicaset reconciler. I believe all we need to support different namespaces is to start mirroring the keyfile and CA secrets in the Search namespace in Community. |
||
}); 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 | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -14,29 +14,35 @@ 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/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" | ||
"github.com/mongodb/mongodb-kubernetes/pkg/kube/commoncontroller" | ||
"github.com/mongodb/mongodb-kubernetes/pkg/util" | ||
"github.com/mongodb/mongodb-kubernetes/pkg/util/env" | ||
) | ||
|
||
type MongoDBSearchReconciler struct { | ||
kubeClient kubernetesClient.Client | ||
mdbcWatcher *watch.ResourceWatcher | ||
mdbcWatcher watch.ResourceWatcher | ||
mdbWatcher watch.ResourceWatcher | ||
secretWatcher 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, | ||
mdbcWatcher: watch.New(), | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. blocking: we cannot have these watchers in the controller instance. It must be placed in MongoDBSearchReconcilerHelper as the controller is a singleton executed concurrently by the runtime in case we have more than one reconcile at a time configured. Also the resource watcher here is not made to be thread safe and we should migrate to the one from MEKO packages, which have locks (controllers/operator/watch/resource_watcher.go). Also There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. So, I tried repurposing field indexes for watches as described in Kubebuilder's last-version docs and it works pretty neatly for resource we have a direct relationship against. We can generate reconcile requests for changes in However, where this breaks down for me is watching related resources like the CA or keyfile secrets since we don't build indexes against them. I was looking into transitive events, as in "if the CA or keyfile secrets change, shouldn't that cause at least a temporary change in the MongoDBCommunity status that will trigger the search controller's mdbc field index watch?" but I didn't have the time to verify that assumption. That's why I fell back to using watches. Field indexes aside, I'll switch to the thread-safe watcher used in |
||
mdbWatcher: watch.New(), | ||
secretWatcher: watch.New(), | ||
operatorSearchConfig: operatorSearchConfig, | ||
} | ||
} | ||
|
@@ -51,36 +57,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.secretWatcher.Watch(ctx, kube.ObjectKey(mdbSearch.GetNamespace(), searchSource.KeyfileSecretName()), 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.mdbWatcher.Watch(ctx, sourceName, 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.mdbcWatcher.Watch(ctx, sourceName, 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 +125,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(&mdbv1.MongoDB{}, r.mdbWatcher). | ||
Watches(&mdbcv1.MongoDBCommunity{}, r.mdbcWatcher). | ||
Watches(&corev1.Secret{}, r.secretWatcher). | ||
Owns(&appsv1.StatefulSet{}). | ||
Owns(&corev1.Secret{}). | ||
Complete(r) | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
shouldn't we return false here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure. If we return false then we won't mirror the keyfile from Ops Manager into the secret that mongot will be waiting for. If customers are using a customized MongoDB version it might either not need the
searchCoordinator
role polyfill, or they might apply it themselves in theMongoDB
spec, so not returning false if we can't parse the MongoDB version doesn't prevent that very convoluted scenario from actually working with Search.On the other hand, I can't realistically imagine when we might fail parsing the MongoDB version, so I can't decide if this case is actually worth guarding against.