From 1e00443c89dc654b7477b91263947018973ba625 Mon Sep 17 00:00:00 2001 From: Vivek Singh Date: Tue, 6 Jan 2026 21:57:40 +0100 Subject: [PATCH 01/10] Enable auto embeddings for vector search using mongot config --- api/v1/search/mongodbsearch_types.go | 11 +++ api/v1/search/zz_generated.deepcopy.go | 16 ++++ .../crd/bases/mongodb.com_mongodbsearch.yaml | 13 ++++ .../mongodbsearch_reconcile_helper.go | 78 ++++++++++++++++++- .../searchcontroller/search_construction.go | 17 ++-- .../crds/mongodb.com_mongodbsearch.yaml | 13 ++++ .../pkg/mongot/mongot_config.go | 8 ++ public/crds.yaml | 13 ++++ 8 files changed, 162 insertions(+), 7 deletions(-) diff --git a/api/v1/search/mongodbsearch_types.go b/api/v1/search/mongodbsearch_types.go index 3240a25ff3..47029cdd98 100644 --- a/api/v1/search/mongodbsearch_types.go +++ b/api/v1/search/mongodbsearch_types.go @@ -71,6 +71,17 @@ type MongoDBSearchSpec struct { // Configure prometheus metrics endpoint in mongot. If not set, the metrics endpoint will be disabled. // +optional Prometheus *Prometheus `json:"prometheus,omitempty"` + // Configure the embedding model's API endpoint and its details to generate vector auto embeddings. + // `embedding` field of mongot config is generated using the values provided here. + // +optional + AutoEmbedding EmbeddingConfig `json:"autoEmbedding"` +} + +type EmbeddingConfig struct { + ProviderEndpoint string `json:"providerEndpoint,omitempty"` + // EmbeddingModelAPIKeySecret would have the name of the secret that has two keys + // query-key and indexing-key for embedding model's API keys. + EmbeddingModelAPIKeySecret string `json:"embeddingModelAPIKeySecret,omitempty"` } type MongoDBSource struct { diff --git a/api/v1/search/zz_generated.deepcopy.go b/api/v1/search/zz_generated.deepcopy.go index 8a6b356650..ca10ed581a 100644 --- a/api/v1/search/zz_generated.deepcopy.go +++ b/api/v1/search/zz_generated.deepcopy.go @@ -28,6 +28,21 @@ 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 *EmbeddingConfig) DeepCopyInto(out *EmbeddingConfig) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new EmbeddingConfig. +func (in *EmbeddingConfig) DeepCopy() *EmbeddingConfig { + if in == nil { + return nil + } + out := new(EmbeddingConfig) + in.DeepCopyInto(out) + return out +} + // 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 @@ -165,6 +180,7 @@ func (in *MongoDBSearchSpec) DeepCopyInto(out *MongoDBSearchSpec) { *out = new(Prometheus) **out = **in } + out.AutoEmbedding = in.AutoEmbedding } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MongoDBSearchSpec. diff --git a/config/crd/bases/mongodb.com_mongodbsearch.yaml b/config/crd/bases/mongodb.com_mongodbsearch.yaml index c7a318626e..cadc936cdd 100644 --- a/config/crd/bases/mongodb.com_mongodbsearch.yaml +++ b/config/crd/bases/mongodb.com_mongodbsearch.yaml @@ -52,6 +52,19 @@ spec: type: object spec: properties: + autoEmbedding: + description: |- + Configure the embedding model's API endpoint and its details to generate vector auto embeddings. + `embedding` field of mongot config is generated using the values provided here. + properties: + embeddingModelAPIKeySecret: + description: |- + EmbeddingModelAPIKeySecret would have the name of the secret that has two keys + query-key and indexing-key for embedding model's API keys. + type: string + providerEndpoint: + type: string + type: object logLevel: description: Configure verbosity of mongot logs. Defaults to INFO if not set. diff --git a/controllers/searchcontroller/mongodbsearch_reconcile_helper.go b/controllers/searchcontroller/mongodbsearch_reconcile_helper.go index e1b105cf46..236dacbf9a 100644 --- a/controllers/searchcontroller/mongodbsearch_reconcile_helper.go +++ b/controllers/searchcontroller/mongodbsearch_reconcile_helper.go @@ -42,6 +42,20 @@ const ( "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." + + // embeddingKeyFilePath is the path that is used in mongot config to specify the api keys + // this where query and index keys would be available. + embeddingKeyFilePath = "/etc/mongot/secrets" + embeddingKeyVolumeName = "auto-embedding-api-keys" + + indexingKeyName = "indexing-key" + queryKeyName = "query-key" + + apiKeysTempVolumeName = "api-keys-config" + // To overcome the strict requirement of api keys having 0400 permission we mount the api keys + // to a temp location apiKeysTempVolumeMount and then copy it to correct location embeddingKeyFilePath, + // changing the permission to 0400. + apiKeysTempVolumeMount = "/tmp/api-keys" ) type OperatorSearchConfig struct { @@ -119,8 +133,10 @@ func (r *MongoDBSearchReconcileHelper) reconcile(ctx context.Context, log *zap.S egressTlsMongotModification, egressTlsStsModification := r.ensureEgressTlsConfig(ctx) + embeddingConfigMongotModification, embeddingConfigStsModification, err := r.ensureEmbeddingConfig() + // the egress TLS modification needs to always be applied after the ingress one, because it toggles mTLS based on the mode set by the ingress modification - configHash, err := r.ensureMongotConfig(ctx, log, createMongotConfig(r.mdbSearch, r.db), ingressTlsMongotModification, egressTlsMongotModification) + configHash, err := r.ensureMongotConfig(ctx, log, createMongotConfig(r.mdbSearch, r.db), ingressTlsMongotModification, egressTlsMongotModification, embeddingConfigMongotModification) if err != nil { return workflow.Failed(err) } @@ -131,7 +147,15 @@ func (r *MongoDBSearchReconcileHelper) reconcile(ctx context.Context, log *zap.S }, )) - if err := r.createOrUpdateStatefulSet(ctx, log, CreateSearchStatefulSetFunc(r.mdbSearch, r.db, r.buildImageString()), configHashModification, keyfileStsModification, ingressTlsStsModification, egressTlsStsModification); err != nil { + if err := r.createOrUpdateStatefulSet(ctx, + log, + CreateSearchStatefulSetFunc(r.mdbSearch, r.db, r.buildImageString()), + configHashModification, + keyfileStsModification, + ingressTlsStsModification, + egressTlsStsModification, + embeddingConfigStsModification, + ); err != nil { return workflow.Failed(err) } @@ -231,6 +255,56 @@ func (r *MongoDBSearchReconcileHelper) ensureMongotConfig(ctx context.Context, l return hashBytes(configData), nil } +func (r *MongoDBSearchReconcileHelper) ensureEmbeddingConfig() (mongot.Modification, statefulset.Modification, error) { + if r.mdbSearch.Spec.AutoEmbedding.EmbeddingModelAPIKeySecret == "" && r.mdbSearch.Spec.AutoEmbedding.ProviderEndpoint == "" { + return mongot.NOOP(), statefulset.NOOP(), nil + } + + autoEmbeddingViewWriterTrue := true + mongotModification := func(config *mongot.Config) { + if r.mdbSearch.Spec.AutoEmbedding.EmbeddingModelAPIKeySecret != "" { + config.Embedding.IndexingKeyFile = fmt.Sprintf("%s/%s", embeddingKeyFilePath, indexingKeyName) + config.Embedding.QueryKeyFile = fmt.Sprintf("%s/%s", embeddingKeyFilePath, queryKeyName) + + // Since MCK right now installs search with one replica only it's safe to alway set IsAutoEmbeddingViewWriter to true. + // Once we start supporting multiple mongot instances, we need to figure this out and then set here. + config.Embedding.IsAutoEmbeddingViewWriter = &autoEmbeddingViewWriterTrue + } + + if r.mdbSearch.Spec.AutoEmbedding.ProviderEndpoint != "" { + config.Embedding.ProviderEndpoint = r.mdbSearch.Spec.AutoEmbedding.ProviderEndpoint + } + } + readOnlyByOwnerPermission := int32(400) + apiKeyVolume := statefulset.CreateVolumeFromSecret(embeddingKeyVolumeName, r.mdbSearch.Spec.AutoEmbedding.EmbeddingModelAPIKeySecret, statefulset.WithSecretDefaultMode(&readOnlyByOwnerPermission)) + apiKeyVolumeMount := statefulset.CreateVolumeMount(embeddingKeyVolumeName, apiKeysTempVolumeMount, statefulset.WithReadOnly(true)) + + emptyDirVolume := statefulset.CreateVolumeFromEmptyDir(apiKeysTempVolumeName) + emptyDirVolumeMount := statefulset.CreateVolumeMount(apiKeysTempVolumeName, embeddingKeyFilePath) + + stsModification := statefulset.WithPodSpecTemplate(podtemplatespec.Apply( + podtemplatespec.WithVolume(apiKeyVolume), + podtemplatespec.WithVolumeMounts(MongotContainerName, apiKeyVolumeMount), + podtemplatespec.WithVolume(emptyDirVolume), + podtemplatespec.WithVolumeMounts(MongotContainerName, emptyDirVolumeMount), + podtemplatespec.WithContainer(MongotContainerName, setupMongotContainerArgsForAPIKeys()), + )) + return mongotModification, stsModification, nil +} + +func setupMongotContainerArgsForAPIKeys() container.Modification { + return container.Apply( + container.WithName(MongotContainerName), + // Since API keys are expected to have 0400 permission, add the arg into the search container to make + // sure we copy the api keys from temp location (apiKeysTempVolumeMount) to correct location (embeddingKeyFilePath) + // with correct permissions. + // Directly setting the permission in the volume doesn't work because volumes are mounted as symlinks and they would have diff permissions, + // using subpath kind of resolves the probelm but because of fsGroup that we set K8s makes sure that the file is group readable, + // and that's why the file permissions still don't become 0400 (it's -r--r-----). That's why copying is necessary. + prependCommand(sensitiveFilePermissionsForAPIKeys(apiKeysTempVolumeMount, embeddingKeyFilePath, "0400")), + ) +} + func (r *MongoDBSearchReconcileHelper) ensureIngressTlsConfig(ctx context.Context) (mongot.Modification, statefulset.Modification, error) { if r.mdbSearch.Spec.Security.TLS == nil { return mongot.NOOP(), statefulset.NOOP(), nil diff --git a/controllers/searchcontroller/search_construction.go b/controllers/searchcontroller/search_construction.go index c1884c0407..e1b1a193fb 100644 --- a/controllers/searchcontroller/search_construction.go +++ b/controllers/searchcontroller/search_construction.go @@ -141,7 +141,7 @@ func CreateKeyfileModificationFunc(keyfileSecretName string) statefulset.Modific podtemplatespec.WithContainer(MongotContainerName, container.Apply( container.WithVolumeMounts([]corev1.VolumeMount{keyfileVolumeMount}), - prependCommand(sensitiveFilePermissionsWorkaround(MongotKeyfilePath, TempKeyfilePath)), + prependCommand(sensitiveFilePermissionsWorkaround(MongotKeyfilePath, TempKeyfilePath, "0600")), ), ), ), @@ -164,7 +164,7 @@ func mongodbSearchContainer(mdbSearch *searchv1.MongoDBSearch, volumeMounts []co "-c", "/mongot-community/mongot --config /mongot/config.yml", }), - prependCommand(sensitiveFilePermissionsWorkaround(MongotSourceUserPasswordPath, TempSourceUserPasswordPath)), + prependCommand(sensitiveFilePermissionsWorkaround(MongotSourceUserPasswordPath, TempSourceUserPasswordPath, "0600")), containerSecurityContext, ) } @@ -235,10 +235,17 @@ func prependCommand(commands string) container.Modification { // mongot requires certain senstive files to have 600 permissions // but we can't get secret subPaths to have those permissions directly // so we copy them to a temp folder and set the permissions there -func sensitiveFilePermissionsWorkaround(filePath, tempFilePath string) string { +func sensitiveFilePermissionsWorkaround(filePath, tempFilePath, fileMode string) string { return fmt.Sprintf(` cp %[1]s %[2]s chown 2000:2000 %[2]s -chmod 0600 %[2]s -`, filePath, tempFilePath) +chmod %[3]s %[2]s +`, filePath, tempFilePath, fileMode) +} + +func sensitiveFilePermissionsForAPIKeys(srcFilePath, destFilePath, fileMode string) string { + return fmt.Sprintf(` +cp %[1]s/* %[2]s +chmod %[3]s %[2]s/* +`, srcFilePath, destFilePath, fileMode) } diff --git a/helm_chart/crds/mongodb.com_mongodbsearch.yaml b/helm_chart/crds/mongodb.com_mongodbsearch.yaml index c7a318626e..cadc936cdd 100644 --- a/helm_chart/crds/mongodb.com_mongodbsearch.yaml +++ b/helm_chart/crds/mongodb.com_mongodbsearch.yaml @@ -52,6 +52,19 @@ spec: type: object spec: properties: + autoEmbedding: + description: |- + Configure the embedding model's API endpoint and its details to generate vector auto embeddings. + `embedding` field of mongot config is generated using the values provided here. + properties: + embeddingModelAPIKeySecret: + description: |- + EmbeddingModelAPIKeySecret would have the name of the secret that has two keys + query-key and indexing-key for embedding model's API keys. + type: string + providerEndpoint: + type: string + type: object logLevel: description: Configure verbosity of mongot logs. Defaults to INFO if not set. diff --git a/mongodb-community-operator/pkg/mongot/mongot_config.go b/mongodb-community-operator/pkg/mongot/mongot_config.go index fe11a6ac1e..d0b815c08f 100644 --- a/mongodb-community-operator/pkg/mongot/mongot_config.go +++ b/mongodb-community-operator/pkg/mongot/mongot_config.go @@ -21,6 +21,14 @@ type Config struct { Metrics ConfigMetrics `json:"metrics"` HealthCheck ConfigHealthCheck `json:"healthCheck"` Logging ConfigLogging `json:"logging"` + Embedding EmbeddingConfig `json:"embedding"` +} + +type EmbeddingConfig struct { + QueryKeyFile string `json:"queryKeyFile" yaml:"queryKeyFile, omitempty"` + IndexingKeyFile string `json:"indexingKeyFile" yaml:"indexingKeyFile, omitempty"` + ProviderEndpoint string `json:"providerEndpoint" yaml:"providerEndpoint, omitempty"` + IsAutoEmbeddingViewWriter *bool `json:"isAutoEmbeddingViewWriter" yaml:"isAutoEmbeddingViewWriter, omitempty"` } type ConfigSyncSource struct { diff --git a/public/crds.yaml b/public/crds.yaml index 52e2295433..1cf934d670 100644 --- a/public/crds.yaml +++ b/public/crds.yaml @@ -4074,6 +4074,19 @@ spec: type: object spec: properties: + autoEmbedding: + description: |- + Configure the embedding model's API endpoint and its details to generate vector auto embeddings. + `embedding` field of mongot config is generated using the values provided here. + properties: + embeddingModelAPIKeySecret: + description: |- + EmbeddingModelAPIKeySecret would have the name of the secret that has two keys + query-key and indexing-key for embedding model's API keys. + type: string + providerEndpoint: + type: string + type: object logLevel: description: Configure verbosity of mongot logs. Defaults to INFO if not set. From 9961ddf3cb3d62c6d75c8eabb958f04b7d0fd0b3 Mon Sep 17 00:00:00 2001 From: Vivek Singh Date: Wed, 7 Jan 2026 22:40:34 +0100 Subject: [PATCH 02/10] Remove unnecessary error return from the method ensureEmbeddingConfig --- .../searchcontroller/mongodbsearch_reconcile_helper.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/controllers/searchcontroller/mongodbsearch_reconcile_helper.go b/controllers/searchcontroller/mongodbsearch_reconcile_helper.go index 236dacbf9a..0355265911 100644 --- a/controllers/searchcontroller/mongodbsearch_reconcile_helper.go +++ b/controllers/searchcontroller/mongodbsearch_reconcile_helper.go @@ -133,7 +133,7 @@ func (r *MongoDBSearchReconcileHelper) reconcile(ctx context.Context, log *zap.S egressTlsMongotModification, egressTlsStsModification := r.ensureEgressTlsConfig(ctx) - embeddingConfigMongotModification, embeddingConfigStsModification, err := r.ensureEmbeddingConfig() + embeddingConfigMongotModification, embeddingConfigStsModification := r.ensureEmbeddingConfig() // the egress TLS modification needs to always be applied after the ingress one, because it toggles mTLS based on the mode set by the ingress modification configHash, err := r.ensureMongotConfig(ctx, log, createMongotConfig(r.mdbSearch, r.db), ingressTlsMongotModification, egressTlsMongotModification, embeddingConfigMongotModification) @@ -255,9 +255,9 @@ func (r *MongoDBSearchReconcileHelper) ensureMongotConfig(ctx context.Context, l return hashBytes(configData), nil } -func (r *MongoDBSearchReconcileHelper) ensureEmbeddingConfig() (mongot.Modification, statefulset.Modification, error) { +func (r *MongoDBSearchReconcileHelper) ensureEmbeddingConfig() (mongot.Modification, statefulset.Modification) { if r.mdbSearch.Spec.AutoEmbedding.EmbeddingModelAPIKeySecret == "" && r.mdbSearch.Spec.AutoEmbedding.ProviderEndpoint == "" { - return mongot.NOOP(), statefulset.NOOP(), nil + return mongot.NOOP(), statefulset.NOOP() } autoEmbeddingViewWriterTrue := true @@ -289,7 +289,7 @@ func (r *MongoDBSearchReconcileHelper) ensureEmbeddingConfig() (mongot.Modificat podtemplatespec.WithVolumeMounts(MongotContainerName, emptyDirVolumeMount), podtemplatespec.WithContainer(MongotContainerName, setupMongotContainerArgsForAPIKeys()), )) - return mongotModification, stsModification, nil + return mongotModification, stsModification } func setupMongotContainerArgsForAPIKeys() container.Modification { From a040ee92f02ffea151f089b4835f2655e533fe9a Mon Sep 17 00:00:00 2001 From: Vivek Singh Date: Wed, 7 Jan 2026 22:40:46 +0100 Subject: [PATCH 03/10] Add unit tests --- .../mongodbsearch_reconcile_helper_test.go | 161 ++++++++++++++++++ 1 file changed, 161 insertions(+) diff --git a/controllers/searchcontroller/mongodbsearch_reconcile_helper_test.go b/controllers/searchcontroller/mongodbsearch_reconcile_helper_test.go index c9946586b1..e86a5f7a96 100644 --- a/controllers/searchcontroller/mongodbsearch_reconcile_helper_test.go +++ b/controllers/searchcontroller/mongodbsearch_reconcile_helper_test.go @@ -11,6 +11,7 @@ import ( "go.uber.org/zap" "sigs.k8s.io/controller-runtime/pkg/client" + appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -20,6 +21,7 @@ import ( "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" + "github.com/mongodb/mongodb-kubernetes/mongodb-community-operator/pkg/mongot" ) func init() { @@ -316,3 +318,162 @@ func TestMongoDBSearchReconcileHelper_ServiceCreation(t *testing.T) { }) } } + +var testApiKeySecretName = "api-key-secret" +var embeddingWriterTrue = true +var mode = int32(400) +var expectedVolumes = []corev1.Volume{ + { + Name: embeddingKeyVolumeName, + VolumeSource: corev1.VolumeSource{ + Secret: &corev1.SecretVolumeSource{ + SecretName: testApiKeySecretName, + DefaultMode: &mode, + }, + }, + }, + { + Name: apiKeysTempVolumeName, + VolumeSource: corev1.VolumeSource{ + EmptyDir: &corev1.EmptyDirVolumeSource{}, + }, + }, +} +var expectedVolumeMount = []corev1.VolumeMount{ + { + Name: apiKeysTempVolumeName, + MountPath: embeddingKeyFilePath, + ReadOnly: false, + }, + { + Name: embeddingKeyVolumeName, + MountPath: apiKeysTempVolumeMount, + ReadOnly: true, + }, +} + +func TestEnsureEmbeddingConfig_APIKeySecretAndProviderEndpont(t *testing.T) { + providerEndpoint := "https://api.voyageai.com/v1/embeddings" + + search := newTestMongoDBSearch("mdb-searh", "mongodb", func(s *searchv1.MongoDBSearch) { + s.Spec.AutoEmbedding = searchv1.EmbeddingConfig{ + ProviderEndpoint: providerEndpoint, + EmbeddingModelAPIKeySecret: testApiKeySecretName, + } + }) + + conf := &mongot.Config{} + sts := &appsv1.StatefulSet{ + Spec: appsv1.StatefulSetSpec{ + Template: corev1.PodTemplateSpec{ + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + { + Args: []string{"echo", "test"}, + Name: MongotContainerName, + Image: "searchimage:tag", + VolumeMounts: []corev1.VolumeMount{}, + }, + }, + Volumes: []corev1.Volume{}, + }, + }, + }, + } + + embeddingWriterTrue := true + expectedMongotConfig := mongot.Config{ + Embedding: mongot.EmbeddingConfig{ + ProviderEndpoint: providerEndpoint, + IsAutoEmbeddingViewWriter: &embeddingWriterTrue, + QueryKeyFile: fmt.Sprintf("%s/%s", embeddingKeyFilePath, queryKeyName), + IndexingKeyFile: fmt.Sprintf("%s/%s", embeddingKeyFilePath, indexingKeyName), + }, + } + + helper := NewMongoDBSearchReconcileHelper(nil, search, nil, OperatorSearchConfig{}) + mongotModif, stsModif := helper.ensureEmbeddingConfig() + + mongotModif(conf) + stsModif(sts) + + assert.Equal(t, expectedVolumeMount, sts.Spec.Template.Spec.Containers[0].VolumeMounts) + assert.Equal(t, expectedVolumes, sts.Spec.Template.Spec.Volumes) + assert.Equal(t, expectedMongotConfig.Embedding, conf.Embedding) +} + +func TestEnsureEmbeddingConfig_WOAutoEmbedding(t *testing.T) { + search := newTestMongoDBSearch("mdb-searh", "mongodb") + helper := NewMongoDBSearchReconcileHelper(nil, search, nil, OperatorSearchConfig{}) + mongotModif, stsModif := helper.ensureEmbeddingConfig() + + conf := &mongot.Config{} + sts := &appsv1.StatefulSet{ + Spec: appsv1.StatefulSetSpec{ + Template: corev1.PodTemplateSpec{ + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + { + Args: []string{"echo", "test"}, + Name: MongotContainerName, + Image: "searchimage:tag", + VolumeMounts: []corev1.VolumeMount{}, + }, + }, + Volumes: []corev1.Volume{}, + }, + }, + }, + } + + mongotModif(conf) + stsModif(sts) + + // because search CR didn't have autoEmbedding configured, there wont be any change in conf or sts + assert.Equal(t, sts, sts) + assert.Equal(t, conf, conf) +} + +func TestEnsureEmbeddingConfig_JustAPIKeys(t *testing.T) { + search := newTestMongoDBSearch("mdb-search", "mongodb", func(s *searchv1.MongoDBSearch) { + s.Spec.AutoEmbedding = searchv1.EmbeddingConfig{ + EmbeddingModelAPIKeySecret: testApiKeySecretName, + } + }) + helper := NewMongoDBSearchReconcileHelper(nil, search, nil, OperatorSearchConfig{}) + mongotModif, stsModif := helper.ensureEmbeddingConfig() + + conf := &mongot.Config{} + sts := &appsv1.StatefulSet{ + Spec: appsv1.StatefulSetSpec{ + Template: corev1.PodTemplateSpec{ + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + { + Args: []string{"echo", "test"}, + Name: MongotContainerName, + Image: "searchimage:tag", + VolumeMounts: []corev1.VolumeMount{}, + }, + }, + Volumes: []corev1.Volume{}, + }, + }, + }, + } + + mongotModif(conf) + stsModif(sts) + + // We are just providing the autoEmbedding API Key secret, that's why we will only see that in the config + // and we will see the volumes, mounts in sts + assert.Equal(t, mongot.EmbeddingConfig{ + QueryKeyFile: fmt.Sprintf("%s/%s", embeddingKeyFilePath, queryKeyName), + IndexingKeyFile: fmt.Sprintf("%s/%s", embeddingKeyFilePath, indexingKeyName), + IsAutoEmbeddingViewWriter: &embeddingWriterTrue, + ProviderEndpoint: "", + }, conf.Embedding) + + assert.Equal(t, expectedVolumeMount, sts.Spec.Template.Spec.Containers[0].VolumeMounts) + assert.Equal(t, expectedVolumes, sts.Spec.Template.Spec.Volumes) +} From f26ae00c0fbe7b12bed80035e238df760ba62dc8 Mon Sep 17 00:00:00 2001 From: Vivek Singh Date: Thu, 8 Jan 2026 17:24:16 +0100 Subject: [PATCH 04/10] Address review comments 1. Make sure the reconciliation happens for search resource if the data of the secret that has api keys is changed 2. Validate the api key secret is present before reconciliation --- api/v1/search/mongodbsearch_types.go | 7 +- api/v1/search/zz_generated.deepcopy.go | 11 +- .../crd/bases/mongodb.com_mongodbsearch.yaml | 17 ++- .../operator/mongodbsearch_controller.go | 4 + .../mongodbsearch_reconcile_helper.go | 112 ++++++++++++++---- .../mongodbsearch_reconcile_helper_test.go | 26 ++-- .../crds/mongodb.com_mongodbsearch.yaml | 17 ++- public/crds.yaml | 17 ++- 8 files changed, 172 insertions(+), 39 deletions(-) diff --git a/api/v1/search/mongodbsearch_types.go b/api/v1/search/mongodbsearch_types.go index 47029cdd98..0a4038b52f 100644 --- a/api/v1/search/mongodbsearch_types.go +++ b/api/v1/search/mongodbsearch_types.go @@ -71,17 +71,18 @@ type MongoDBSearchSpec struct { // Configure prometheus metrics endpoint in mongot. If not set, the metrics endpoint will be disabled. // +optional Prometheus *Prometheus `json:"prometheus,omitempty"` - // Configure the embedding model's API endpoint and its details to generate vector auto embeddings. + // Configure MongoDB Search's automatic generation of vector embeddings using an embedding model service. // `embedding` field of mongot config is generated using the values provided here. // +optional - AutoEmbedding EmbeddingConfig `json:"autoEmbedding"` + AutoEmbedding *EmbeddingConfig `json:"autoEmbedding,omitempty"` } type EmbeddingConfig struct { ProviderEndpoint string `json:"providerEndpoint,omitempty"` // EmbeddingModelAPIKeySecret would have the name of the secret that has two keys // query-key and indexing-key for embedding model's API keys. - EmbeddingModelAPIKeySecret string `json:"embeddingModelAPIKeySecret,omitempty"` + // +kubebuilder:validation:Required + EmbeddingModelAPIKeySecret *corev1.LocalObjectReference `json:"embeddingModelAPIKeySecret"` } type MongoDBSource struct { diff --git a/api/v1/search/zz_generated.deepcopy.go b/api/v1/search/zz_generated.deepcopy.go index ca10ed581a..6f1e7525dd 100644 --- a/api/v1/search/zz_generated.deepcopy.go +++ b/api/v1/search/zz_generated.deepcopy.go @@ -31,6 +31,11 @@ import ( // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *EmbeddingConfig) DeepCopyInto(out *EmbeddingConfig) { *out = *in + if in.EmbeddingModelAPIKeySecret != nil { + in, out := &in.EmbeddingModelAPIKeySecret, &out.EmbeddingModelAPIKeySecret + *out = new(v1.LocalObjectReference) + **out = **in + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new EmbeddingConfig. @@ -180,7 +185,11 @@ func (in *MongoDBSearchSpec) DeepCopyInto(out *MongoDBSearchSpec) { *out = new(Prometheus) **out = **in } - out.AutoEmbedding = in.AutoEmbedding + if in.AutoEmbedding != nil { + in, out := &in.AutoEmbedding, &out.AutoEmbedding + *out = new(EmbeddingConfig) + (*in).DeepCopyInto(*out) + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MongoDBSearchSpec. diff --git a/config/crd/bases/mongodb.com_mongodbsearch.yaml b/config/crd/bases/mongodb.com_mongodbsearch.yaml index cadc936cdd..cc4abe1d8a 100644 --- a/config/crd/bases/mongodb.com_mongodbsearch.yaml +++ b/config/crd/bases/mongodb.com_mongodbsearch.yaml @@ -54,16 +54,29 @@ spec: properties: autoEmbedding: description: |- - Configure the embedding model's API endpoint and its details to generate vector auto embeddings. + Configure MongoDB Search's automatic generation of vector embeddings using an embedding model service. `embedding` field of mongot config is generated using the values provided here. properties: embeddingModelAPIKeySecret: description: |- EmbeddingModelAPIKeySecret would have the name of the secret that has two keys query-key and indexing-key for embedding model's API keys. - type: string + 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. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + type: object + x-kubernetes-map-type: atomic providerEndpoint: type: string + required: + - embeddingModelAPIKeySecret type: object logLevel: description: Configure verbosity of mongot logs. Defaults to INFO diff --git a/controllers/operator/mongodbsearch_controller.go b/controllers/operator/mongodbsearch_controller.go index 36f9e32fd8..b025da4011 100644 --- a/controllers/operator/mongodbsearch_controller.go +++ b/controllers/operator/mongodbsearch_controller.go @@ -78,6 +78,10 @@ func (r *MongoDBSearchReconciler) Reconcile(ctx context.Context, request reconci r.watch.AddWatchedResourceIfNotAdded(mdbSearch.Spec.Security.TLS.CertificateKeySecret.Name, mdbSearch.Namespace, watch.Secret, mdbSearch.NamespacedName()) } + if mdbSearch.Spec.AutoEmbedding.EmbeddingModelAPIKeySecret != nil { + r.watch.AddWatchedResourceIfNotAdded(mdbSearch.Spec.AutoEmbedding.EmbeddingModelAPIKeySecret.Name, mdbSearch.Namespace, watch.Secret, mdbSearch.NamespacedName()) + } + reconcileHelper := searchcontroller.NewMongoDBSearchReconcileHelper(kubernetesClient.NewClient(r.kubeClient), mdbSearch, searchSource, r.operatorSearchConfig) return reconcileHelper.Reconcile(ctx, log).ReconcileResult() diff --git a/controllers/searchcontroller/mongodbsearch_reconcile_helper.go b/controllers/searchcontroller/mongodbsearch_reconcile_helper.go index 0355265911..3dc04644f6 100644 --- a/controllers/searchcontroller/mongodbsearch_reconcile_helper.go +++ b/controllers/searchcontroller/mongodbsearch_reconcile_helper.go @@ -4,11 +4,13 @@ import ( "context" "crypto/sha256" "encoding/base32" + "encoding/json" "fmt" "strings" "github.com/ghodss/yaml" "go.uber.org/zap" + semver "golang.org/x/mod/semver" "golang.org/x/xerrors" "k8s.io/apimachinery/pkg/fields" "k8s.io/apimachinery/pkg/util/intstr" @@ -27,6 +29,7 @@ import ( 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/secret" "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" @@ -55,7 +58,10 @@ const ( // To overcome the strict requirement of api keys having 0400 permission we mount the api keys // to a temp location apiKeysTempVolumeMount and then copy it to correct location embeddingKeyFilePath, // changing the permission to 0400. - apiKeysTempVolumeMount = "/tmp/api-keys" + apiKeysTempVolumeMount = "/tmp/auto-embedding-api-keys" + + // is the minimum search image version that is required to enable the auto embeddings for vector search + minSearchImageVersionForEmbedding = "0.58.0" ) type OperatorSearchConfig struct { @@ -133,7 +139,10 @@ func (r *MongoDBSearchReconcileHelper) reconcile(ctx context.Context, log *zap.S egressTlsMongotModification, egressTlsStsModification := r.ensureEgressTlsConfig(ctx) - embeddingConfigMongotModification, embeddingConfigStsModification := r.ensureEmbeddingConfig() + embeddingConfigMongotModification, embeddingConfigStsModification, apiKeySecretHash, err := r.ensureEmbeddingConfig(ctx) + if err != nil { + return workflow.Failed(err) + } // the egress TLS modification needs to always be applied after the ingress one, because it toggles mTLS based on the mode set by the ingress modification configHash, err := r.ensureMongotConfig(ctx, log, createMongotConfig(r.mdbSearch, r.db), ingressTlsMongotModification, egressTlsMongotModification, embeddingConfigMongotModification) @@ -147,10 +156,18 @@ func (r *MongoDBSearchReconcileHelper) reconcile(ctx context.Context, log *zap.S }, )) + apiKeySecretHashModification := statefulset.WithPodSpecTemplate(podtemplatespec.WithAnnotations( + map[string]string{ + "autoEmbeddingDetailsHash": apiKeySecretHash, + }, + )) + + image, version := r.searchImageAndVersion() if err := r.createOrUpdateStatefulSet(ctx, log, - CreateSearchStatefulSetFunc(r.mdbSearch, r.db, r.buildImageString()), + CreateSearchStatefulSetFunc(r.mdbSearch, r.db, fmt.Sprintf("%s:%s", image, version)), configHashModification, + apiKeySecretHashModification, keyfileStsModification, ingressTlsStsModification, egressTlsStsModification, @@ -185,12 +202,12 @@ func (r *MongoDBSearchReconcileHelper) ensureSourceKeyfile(ctx context.Context, ), nil } -func (r *MongoDBSearchReconcileHelper) buildImageString() string { +func (r *MongoDBSearchReconcileHelper) searchImageAndVersion() (string, string) { imageVersion := r.mdbSearch.Spec.Version if imageVersion == "" { imageVersion = r.operatorSearchConfig.SearchVersion } - return fmt.Sprintf("%s/%s:%s", r.operatorSearchConfig.SearchRepo, r.operatorSearchConfig.SearchName, imageVersion) + return fmt.Sprintf("%s/%s", r.operatorSearchConfig.SearchRepo, r.operatorSearchConfig.SearchName), imageVersion } func (r *MongoDBSearchReconcileHelper) createOrUpdateStatefulSet(ctx context.Context, log *zap.SugaredLogger, modifications ...statefulset.Modification) error { @@ -255,14 +272,70 @@ func (r *MongoDBSearchReconcileHelper) ensureMongotConfig(ctx context.Context, l return hashBytes(configData), nil } -func (r *MongoDBSearchReconcileHelper) ensureEmbeddingConfig() (mongot.Modification, statefulset.Modification) { - if r.mdbSearch.Spec.AutoEmbedding.EmbeddingModelAPIKeySecret == "" && r.mdbSearch.Spec.AutoEmbedding.ProviderEndpoint == "" { - return mongot.NOOP(), statefulset.NOOP() +// EnsureEmbeddingAPIKeySecret makes sure that the scret that is provided in MDBSearch resource +// for embedding model's keys is present and has expected keys. +func ensureEmbeddingAPIKeySecret(ctx context.Context, client secret.Getter, secretObj client.ObjectKey) (string, error) { + data, err := secret.ReadByteData(ctx, client, secretObj) + if err != nil { + return "", err + } + + if _, ok := data[indexingKeyName]; !ok { + return "", fmt.Errorf(`Required key "%s" is not present in the Secret %s/%s`, indexingKeyName, secretObj.Namespace, secretObj.Name) + } + if _, ok := data[queryKeyName]; !ok { + return "", fmt.Errorf(`Required key "%s" is not present in the Secret %s/%s`, queryKeyName, secretObj.Namespace, secretObj.Name) + } + + d, err := json.Marshal(data) + if err != nil { + return "", err + } + + return hashBytes(d), nil +} + +func validateSearchVesionForEmbedding(_, version string) error { + // https://pkg.go.dev/golang.org/x/mod/semver#Compare + if a := semver.Compare(version, minSearchImageVersionForEmbedding); a == -1 { + return xerrors.Errorf("The MongoDB search version %s doesn't support auto embeddings. Please use version %s or newer.", version, minSearchImageVersionForEmbedding) + } + return nil +} + +func (r *MongoDBSearchReconcileHelper) validateSearchResource(ctx context.Context) (string, error) { + apiKeySecretHash, err := ensureEmbeddingAPIKeySecret(ctx, r.client, client.ObjectKey{ + Name: r.mdbSearch.Spec.AutoEmbedding.EmbeddingModelAPIKeySecret.Name, + Namespace: r.mdbSearch.Namespace, + }) + if err != nil { + return "", err + } + + if err := validateSearchVesionForEmbedding(r.searchImageAndVersion()); err != nil { + return "", err + } + + return apiKeySecretHash, nil +} + +// ensureEmbeddingConfig returns the mongot config and stateful set modification function based on the values provided in the search CR, it +// also returns the hash of the secret that has the embedding API keys so that if the keys are changed the search pod is automatically restarted. +func (r *MongoDBSearchReconcileHelper) ensureEmbeddingConfig(ctx context.Context) (mongot.Modification, statefulset.Modification, string, error) { + if r.mdbSearch.Spec.AutoEmbedding == nil { + return mongot.NOOP(), statefulset.NOOP(), "", nil + } + + // If AutoEmbedding is not nil, it's safe to assume that EmbeddingModelAPIKeySecret would be provided because we have marked it + // a required field. + secretHash, err := r.validateSearchResource(ctx) + if err != nil { + return nil, nil, "", err } autoEmbeddingViewWriterTrue := true mongotModification := func(config *mongot.Config) { - if r.mdbSearch.Spec.AutoEmbedding.EmbeddingModelAPIKeySecret != "" { + if r.mdbSearch.Spec.AutoEmbedding.EmbeddingModelAPIKeySecret != nil && r.mdbSearch.Spec.AutoEmbedding.EmbeddingModelAPIKeySecret.Name != "" { config.Embedding.IndexingKeyFile = fmt.Sprintf("%s/%s", embeddingKeyFilePath, indexingKeyName) config.Embedding.QueryKeyFile = fmt.Sprintf("%s/%s", embeddingKeyFilePath, queryKeyName) @@ -276,7 +349,7 @@ func (r *MongoDBSearchReconcileHelper) ensureEmbeddingConfig() (mongot.Modificat } } readOnlyByOwnerPermission := int32(400) - apiKeyVolume := statefulset.CreateVolumeFromSecret(embeddingKeyVolumeName, r.mdbSearch.Spec.AutoEmbedding.EmbeddingModelAPIKeySecret, statefulset.WithSecretDefaultMode(&readOnlyByOwnerPermission)) + apiKeyVolume := statefulset.CreateVolumeFromSecret(embeddingKeyVolumeName, r.mdbSearch.Spec.AutoEmbedding.EmbeddingModelAPIKeySecret.Name, statefulset.WithSecretDefaultMode(&readOnlyByOwnerPermission)) apiKeyVolumeMount := statefulset.CreateVolumeMount(embeddingKeyVolumeName, apiKeysTempVolumeMount, statefulset.WithReadOnly(true)) emptyDirVolume := statefulset.CreateVolumeFromEmptyDir(apiKeysTempVolumeName) @@ -289,20 +362,17 @@ func (r *MongoDBSearchReconcileHelper) ensureEmbeddingConfig() (mongot.Modificat podtemplatespec.WithVolumeMounts(MongotContainerName, emptyDirVolumeMount), podtemplatespec.WithContainer(MongotContainerName, setupMongotContainerArgsForAPIKeys()), )) - return mongotModification, stsModification + return mongotModification, stsModification, secretHash, nil } func setupMongotContainerArgsForAPIKeys() container.Modification { - return container.Apply( - container.WithName(MongotContainerName), - // Since API keys are expected to have 0400 permission, add the arg into the search container to make - // sure we copy the api keys from temp location (apiKeysTempVolumeMount) to correct location (embeddingKeyFilePath) - // with correct permissions. - // Directly setting the permission in the volume doesn't work because volumes are mounted as symlinks and they would have diff permissions, - // using subpath kind of resolves the probelm but because of fsGroup that we set K8s makes sure that the file is group readable, - // and that's why the file permissions still don't become 0400 (it's -r--r-----). That's why copying is necessary. - prependCommand(sensitiveFilePermissionsForAPIKeys(apiKeysTempVolumeMount, embeddingKeyFilePath, "0400")), - ) + // Since API keys are expected to have 0400 permission, add the arg into the search container to make + // sure we copy the api keys from temp location (apiKeysTempVolumeMount) to correct location (embeddingKeyFilePath) + // with correct permissions. + // Directly setting the permission in the volume doesn't work because volumes are mounted as symlinks and they would have diff permissions, + // using subpath kind of resolves the probelm but because of fsGroup that we set K8s makes sure that the file is group readable, + // and that's why the file permissions still don't become 0400 (it's -r--r-----). That's why copying is necessary. + return prependCommand(sensitiveFilePermissionsForAPIKeys(apiKeysTempVolumeMount, embeddingKeyFilePath, "0400")) } func (r *MongoDBSearchReconcileHelper) ensureIngressTlsConfig(ctx context.Context) (mongot.Modification, statefulset.Modification, error) { diff --git a/controllers/searchcontroller/mongodbsearch_reconcile_helper_test.go b/controllers/searchcontroller/mongodbsearch_reconcile_helper_test.go index e86a5f7a96..857aaa40b0 100644 --- a/controllers/searchcontroller/mongodbsearch_reconcile_helper_test.go +++ b/controllers/searchcontroller/mongodbsearch_reconcile_helper_test.go @@ -356,9 +356,11 @@ func TestEnsureEmbeddingConfig_APIKeySecretAndProviderEndpont(t *testing.T) { providerEndpoint := "https://api.voyageai.com/v1/embeddings" search := newTestMongoDBSearch("mdb-searh", "mongodb", func(s *searchv1.MongoDBSearch) { - s.Spec.AutoEmbedding = searchv1.EmbeddingConfig{ - ProviderEndpoint: providerEndpoint, - EmbeddingModelAPIKeySecret: testApiKeySecretName, + s.Spec.AutoEmbedding = &searchv1.EmbeddingConfig{ + ProviderEndpoint: providerEndpoint, + EmbeddingModelAPIKeySecret: &corev1.LocalObjectReference{ + Name: testApiKeySecretName, + }, } }) @@ -391,8 +393,10 @@ func TestEnsureEmbeddingConfig_APIKeySecretAndProviderEndpont(t *testing.T) { }, } + ctx := context.TODO() helper := NewMongoDBSearchReconcileHelper(nil, search, nil, OperatorSearchConfig{}) - mongotModif, stsModif := helper.ensureEmbeddingConfig() + mongotModif, stsModif, err := helper.ensureEmbeddingConfig(ctx) + assert.Nil(t, err) mongotModif(conf) stsModif(sts) @@ -405,7 +409,9 @@ func TestEnsureEmbeddingConfig_APIKeySecretAndProviderEndpont(t *testing.T) { func TestEnsureEmbeddingConfig_WOAutoEmbedding(t *testing.T) { search := newTestMongoDBSearch("mdb-searh", "mongodb") helper := NewMongoDBSearchReconcileHelper(nil, search, nil, OperatorSearchConfig{}) - mongotModif, stsModif := helper.ensureEmbeddingConfig() + ctx := context.TODO() + mongotModif, stsModif, err := helper.ensureEmbeddingConfig(ctx) + assert.Nil(t, err) conf := &mongot.Config{} sts := &appsv1.StatefulSet{ @@ -436,12 +442,16 @@ func TestEnsureEmbeddingConfig_WOAutoEmbedding(t *testing.T) { func TestEnsureEmbeddingConfig_JustAPIKeys(t *testing.T) { search := newTestMongoDBSearch("mdb-search", "mongodb", func(s *searchv1.MongoDBSearch) { - s.Spec.AutoEmbedding = searchv1.EmbeddingConfig{ - EmbeddingModelAPIKeySecret: testApiKeySecretName, + s.Spec.AutoEmbedding = &searchv1.EmbeddingConfig{ + EmbeddingModelAPIKeySecret: &corev1.LocalObjectReference{ + Name: testApiKeySecretName, + }, } }) helper := NewMongoDBSearchReconcileHelper(nil, search, nil, OperatorSearchConfig{}) - mongotModif, stsModif := helper.ensureEmbeddingConfig() + ctx := context.TODO() + mongotModif, stsModif, err := helper.ensureEmbeddingConfig(ctx) + assert.Nil(t, err) conf := &mongot.Config{} sts := &appsv1.StatefulSet{ diff --git a/helm_chart/crds/mongodb.com_mongodbsearch.yaml b/helm_chart/crds/mongodb.com_mongodbsearch.yaml index cadc936cdd..cc4abe1d8a 100644 --- a/helm_chart/crds/mongodb.com_mongodbsearch.yaml +++ b/helm_chart/crds/mongodb.com_mongodbsearch.yaml @@ -54,16 +54,29 @@ spec: properties: autoEmbedding: description: |- - Configure the embedding model's API endpoint and its details to generate vector auto embeddings. + Configure MongoDB Search's automatic generation of vector embeddings using an embedding model service. `embedding` field of mongot config is generated using the values provided here. properties: embeddingModelAPIKeySecret: description: |- EmbeddingModelAPIKeySecret would have the name of the secret that has two keys query-key and indexing-key for embedding model's API keys. - type: string + 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. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + type: object + x-kubernetes-map-type: atomic providerEndpoint: type: string + required: + - embeddingModelAPIKeySecret type: object logLevel: description: Configure verbosity of mongot logs. Defaults to INFO diff --git a/public/crds.yaml b/public/crds.yaml index 1cf934d670..826a0e50be 100644 --- a/public/crds.yaml +++ b/public/crds.yaml @@ -4076,16 +4076,29 @@ spec: properties: autoEmbedding: description: |- - Configure the embedding model's API endpoint and its details to generate vector auto embeddings. + Configure MongoDB Search's automatic generation of vector embeddings using an embedding model service. `embedding` field of mongot config is generated using the values provided here. properties: embeddingModelAPIKeySecret: description: |- EmbeddingModelAPIKeySecret would have the name of the secret that has two keys query-key and indexing-key for embedding model's API keys. - type: string + 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. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + type: object + x-kubernetes-map-type: atomic providerEndpoint: type: string + required: + - embeddingModelAPIKeySecret type: object logLevel: description: Configure verbosity of mongot logs. Defaults to INFO From a6713ad58d54f336d731c7ea637b2d4aeecf5173 Mon Sep 17 00:00:00 2001 From: Vivek Singh Date: Thu, 8 Jan 2026 19:31:02 +0100 Subject: [PATCH 05/10] Fix the bug in validateSearchVesionForEmbedding --- .../mongodbsearch_reconcile_helper.go | 11 ++++++++--- go.mod | 1 + go.sum | 2 ++ 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/controllers/searchcontroller/mongodbsearch_reconcile_helper.go b/controllers/searchcontroller/mongodbsearch_reconcile_helper.go index 3dc04644f6..38c25a0673 100644 --- a/controllers/searchcontroller/mongodbsearch_reconcile_helper.go +++ b/controllers/searchcontroller/mongodbsearch_reconcile_helper.go @@ -8,9 +8,9 @@ import ( "fmt" "strings" + semver "github.com/Masterminds/semver/v3" "github.com/ghodss/yaml" "go.uber.org/zap" - semver "golang.org/x/mod/semver" "golang.org/x/xerrors" "k8s.io/apimachinery/pkg/fields" "k8s.io/apimachinery/pkg/util/intstr" @@ -296,8 +296,13 @@ func ensureEmbeddingAPIKeySecret(ctx context.Context, client secret.Getter, secr } func validateSearchVesionForEmbedding(_, version string) error { - // https://pkg.go.dev/golang.org/x/mod/semver#Compare - if a := semver.Compare(version, minSearchImageVersionForEmbedding); a == -1 { + searchVersion, err := semver.NewVersion(version) + if err != nil { + return fmt.Errorf("Failed getting semver of search image version. Version %s doesn't seem to be valid semver.", version) + } + minAllowedVersion, _ := semver.NewVersion(minSearchImageVersionForEmbedding) + + if a := searchVersion.Compare(minAllowedVersion); a == -1 { return xerrors.Errorf("The MongoDB search version %s doesn't support auto embeddings. Please use version %s or newer.", version, minSearchImageVersionForEmbedding) } return nil diff --git a/go.mod b/go.mod index d4dc182d3f..8ee6850805 100644 --- a/go.mod +++ b/go.mod @@ -47,6 +47,7 @@ require ( require google.golang.org/protobuf v1.36.10 // indirect require ( + github.com/Masterminds/semver/v3 v3.4.0 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/cenkalti/backoff/v4 v4.3.0 // indirect github.com/cenkalti/backoff/v5 v5.0.3 // indirect diff --git a/go.sum b/go.sum index f12b9199c6..84be1ef38d 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,5 @@ +github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0= +github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= From 5a2c4c620890055ffbda9a8445e12f9a9c0ea769 Mon Sep 17 00:00:00 2001 From: Vivek Singh Date: Thu, 8 Jan 2026 19:31:16 +0100 Subject: [PATCH 06/10] Improve unit test --- .../mongodbsearch_reconcile_helper_test.go | 136 +++++++++++++++++- 1 file changed, 130 insertions(+), 6 deletions(-) diff --git a/controllers/searchcontroller/mongodbsearch_reconcile_helper_test.go b/controllers/searchcontroller/mongodbsearch_reconcile_helper_test.go index 857aaa40b0..49b6a0e51a 100644 --- a/controllers/searchcontroller/mongodbsearch_reconcile_helper_test.go +++ b/controllers/searchcontroller/mongodbsearch_reconcile_helper_test.go @@ -352,6 +352,17 @@ var expectedVolumeMount = []corev1.VolumeMount{ }, } +var apiKeySecret = &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "api-key-secret", + Namespace: "mongodb", + }, + Data: map[string][]byte{ + "indexing-key": []byte(""), + "query-key": []byte(""), + }, +} + func TestEnsureEmbeddingConfig_APIKeySecretAndProviderEndpont(t *testing.T) { providerEndpoint := "https://api.voyageai.com/v1/embeddings" @@ -394,8 +405,9 @@ func TestEnsureEmbeddingConfig_APIKeySecretAndProviderEndpont(t *testing.T) { } ctx := context.TODO() - helper := NewMongoDBSearchReconcileHelper(nil, search, nil, OperatorSearchConfig{}) - mongotModif, stsModif, err := helper.ensureEmbeddingConfig(ctx) + fakeClient := newTestFakeClient(search, apiKeySecret) + helper := NewMongoDBSearchReconcileHelper(fakeClient, search, nil, OperatorSearchConfig{}) + mongotModif, stsModif, _, err := helper.ensureEmbeddingConfig(ctx) assert.Nil(t, err) mongotModif(conf) @@ -408,9 +420,10 @@ func TestEnsureEmbeddingConfig_APIKeySecretAndProviderEndpont(t *testing.T) { func TestEnsureEmbeddingConfig_WOAutoEmbedding(t *testing.T) { search := newTestMongoDBSearch("mdb-searh", "mongodb") - helper := NewMongoDBSearchReconcileHelper(nil, search, nil, OperatorSearchConfig{}) + fakeClient := newTestFakeClient(search) + helper := NewMongoDBSearchReconcileHelper(fakeClient, search, nil, OperatorSearchConfig{}) ctx := context.TODO() - mongotModif, stsModif, err := helper.ensureEmbeddingConfig(ctx) + mongotModif, stsModif, _, err := helper.ensureEmbeddingConfig(ctx) assert.Nil(t, err) conf := &mongot.Config{} @@ -448,9 +461,10 @@ func TestEnsureEmbeddingConfig_JustAPIKeys(t *testing.T) { }, } }) - helper := NewMongoDBSearchReconcileHelper(nil, search, nil, OperatorSearchConfig{}) + fakeClient := newTestFakeClient(search, apiKeySecret) + helper := NewMongoDBSearchReconcileHelper(fakeClient, search, nil, OperatorSearchConfig{}) ctx := context.TODO() - mongotModif, stsModif, err := helper.ensureEmbeddingConfig(ctx) + mongotModif, stsModif, _, err := helper.ensureEmbeddingConfig(ctx) assert.Nil(t, err) conf := &mongot.Config{} @@ -487,3 +501,113 @@ func TestEnsureEmbeddingConfig_JustAPIKeys(t *testing.T) { assert.Equal(t, expectedVolumeMount, sts.Spec.Template.Spec.Containers[0].VolumeMounts) assert.Equal(t, expectedVolumes, sts.Spec.Template.Spec.Volumes) } + +func TestValidateSearchResource(t *testing.T) { + search := newTestMongoDBSearch("mdb-search", "mongodb", func(s *searchv1.MongoDBSearch) { + s.Spec.AutoEmbedding = &searchv1.EmbeddingConfig{ + EmbeddingModelAPIKeySecret: &corev1.LocalObjectReference{ + Name: testApiKeySecretName, + }, + } + }) + ctx := context.TODO() + for _, tc := range []struct { + apiKeySecret *corev1.Secret + errAssertion assert.ErrorAssertionFunc + errMsg string + searchVersion string + }{ + { + apiKeySecret: &corev1.Secret{}, + errAssertion: assert.Error, + errMsg: fmt.Sprintf("secrets \"%s\" not found", testApiKeySecretName), + }, + { + apiKeySecret: &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: testApiKeySecretName, + }, + }, + errAssertion: assert.Error, + errMsg: fmt.Sprintf("secrets \"%s\" not found", testApiKeySecretName), + }, + { + apiKeySecret: &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: testApiKeySecretName, + Namespace: "mongodb", + }, + }, + errAssertion: assert.Error, + errMsg: fmt.Sprintf("Required key \"%s\" is not present in the Secret mongodb/%s", indexingKeyName, testApiKeySecretName), + }, + { + apiKeySecret: &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: testApiKeySecretName, + Namespace: "mongodb", + }, + Data: map[string][]byte{ + "indexing-key": []byte(""), + }, + }, + errAssertion: assert.Error, + errMsg: fmt.Sprintf("Required key \"%s\" is not present in the Secret mongodb/%s", queryKeyName, testApiKeySecretName), + }, + { + apiKeySecret: &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: testApiKeySecretName, + Namespace: "mongodb", + }, + Data: map[string][]byte{ + "indexing-key": []byte(""), + "query-key": []byte(""), + }, + }, + errAssertion: assert.Error, + searchVersion: "0.55.0", + errMsg: "The MongoDB search version 0.55.0 doesn't support auto embeddings. Please use version 0.58.0 or newer.", + }, + { + apiKeySecret: &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: testApiKeySecretName, + Namespace: "mongodb", + }, + Data: map[string][]byte{ + "indexing-key": []byte(""), + "query-key": []byte(""), + }, + }, + errAssertion: assert.NoError, + searchVersion: "0.58.0", + errMsg: "", + }, + { + apiKeySecret: &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: testApiKeySecretName, + Namespace: "mongodb", + }, + Data: map[string][]byte{ + "indexing-key": []byte(""), + "query-key": []byte(""), + }, + }, + errAssertion: assert.NoError, + searchVersion: "1.58.0", + errMsg: "", + }, + } { + fakeClient := newTestFakeClient(search, tc.apiKeySecret) + helper := NewMongoDBSearchReconcileHelper(fakeClient, search, nil, OperatorSearchConfig{ + SearchVersion: tc.searchVersion, + }) + _, err := helper.validateSearchResource(ctx) + tc.errAssertion(t, err) + if tc.errMsg != "" { + assert.Equal(t, tc.errMsg, err.Error()) + } + } +} From 5d910af6d15cf817c104f30041b43519d3b28958 Mon Sep 17 00:00:00 2001 From: Vivek Singh Date: Fri, 9 Jan 2026 11:28:17 +0100 Subject: [PATCH 07/10] Address review comments --- .../mongodbsearch_reconcile_helper.go | 63 ++++++++----------- .../mongodbsearch_reconcile_helper_test.go | 20 +++--- 2 files changed, 39 insertions(+), 44 deletions(-) diff --git a/controllers/searchcontroller/mongodbsearch_reconcile_helper.go b/controllers/searchcontroller/mongodbsearch_reconcile_helper.go index 38c25a0673..5f5859f432 100644 --- a/controllers/searchcontroller/mongodbsearch_reconcile_helper.go +++ b/controllers/searchcontroller/mongodbsearch_reconcile_helper.go @@ -62,6 +62,9 @@ const ( // is the minimum search image version that is required to enable the auto embeddings for vector search minSearchImageVersionForEmbedding = "0.58.0" + + // autoEmbeddingDetailsAnnKey has the annotation key that would be added to search pod with emebdding API Key secret hash + autoEmbeddingDetailsAnnKey = "autoEmbeddingDetailsHash" ) type OperatorSearchConfig struct { @@ -139,7 +142,7 @@ func (r *MongoDBSearchReconcileHelper) reconcile(ctx context.Context, log *zap.S egressTlsMongotModification, egressTlsStsModification := r.ensureEgressTlsConfig(ctx) - embeddingConfigMongotModification, embeddingConfigStsModification, apiKeySecretHash, err := r.ensureEmbeddingConfig(ctx) + embeddingConfigMongotModification, embeddingConfigStsModification, err := r.ensureEmbeddingConfig(ctx) if err != nil { return workflow.Failed(err) } @@ -156,18 +159,11 @@ func (r *MongoDBSearchReconcileHelper) reconcile(ctx context.Context, log *zap.S }, )) - apiKeySecretHashModification := statefulset.WithPodSpecTemplate(podtemplatespec.WithAnnotations( - map[string]string{ - "autoEmbeddingDetailsHash": apiKeySecretHash, - }, - )) - image, version := r.searchImageAndVersion() if err := r.createOrUpdateStatefulSet(ctx, log, CreateSearchStatefulSetFunc(r.mdbSearch, r.db, fmt.Sprintf("%s:%s", image, version)), configHashModification, - apiKeySecretHashModification, keyfileStsModification, ingressTlsStsModification, egressTlsStsModification, @@ -295,7 +291,7 @@ func ensureEmbeddingAPIKeySecret(ctx context.Context, client secret.Getter, secr return hashBytes(d), nil } -func validateSearchVesionForEmbedding(_, version string) error { +func validateSearchVesionForEmbedding(version string) error { searchVersion, err := semver.NewVersion(version) if err != nil { return fmt.Errorf("Failed getting semver of search image version. Version %s doesn't seem to be valid semver.", version) @@ -308,46 +304,36 @@ func validateSearchVesionForEmbedding(_, version string) error { return nil } -func (r *MongoDBSearchReconcileHelper) validateSearchResource(ctx context.Context) (string, error) { - apiKeySecretHash, err := ensureEmbeddingAPIKeySecret(ctx, r.client, client.ObjectKey{ - Name: r.mdbSearch.Spec.AutoEmbedding.EmbeddingModelAPIKeySecret.Name, - Namespace: r.mdbSearch.Namespace, - }) - if err != nil { - return "", err - } - - if err := validateSearchVesionForEmbedding(r.searchImageAndVersion()); err != nil { - return "", err - } - - return apiKeySecretHash, nil -} - // ensureEmbeddingConfig returns the mongot config and stateful set modification function based on the values provided in the search CR, it // also returns the hash of the secret that has the embedding API keys so that if the keys are changed the search pod is automatically restarted. -func (r *MongoDBSearchReconcileHelper) ensureEmbeddingConfig(ctx context.Context) (mongot.Modification, statefulset.Modification, string, error) { +func (r *MongoDBSearchReconcileHelper) ensureEmbeddingConfig(ctx context.Context) (mongot.Modification, statefulset.Modification, error) { if r.mdbSearch.Spec.AutoEmbedding == nil { - return mongot.NOOP(), statefulset.NOOP(), "", nil + return mongot.NOOP(), statefulset.NOOP(), nil } // If AutoEmbedding is not nil, it's safe to assume that EmbeddingModelAPIKeySecret would be provided because we have marked it // a required field. - secretHash, err := r.validateSearchResource(ctx) + apiKeySecretHash, err := ensureEmbeddingAPIKeySecret(ctx, r.client, client.ObjectKey{ + Name: r.mdbSearch.Spec.AutoEmbedding.EmbeddingModelAPIKeySecret.Name, + Namespace: r.mdbSearch.Namespace, + }) if err != nil { - return nil, nil, "", err + return nil, nil, err + } + + _, version := r.searchImageAndVersion() + if err := validateSearchVesionForEmbedding(version); err != nil { + return nil, nil, err } autoEmbeddingViewWriterTrue := true mongotModification := func(config *mongot.Config) { - if r.mdbSearch.Spec.AutoEmbedding.EmbeddingModelAPIKeySecret != nil && r.mdbSearch.Spec.AutoEmbedding.EmbeddingModelAPIKeySecret.Name != "" { - config.Embedding.IndexingKeyFile = fmt.Sprintf("%s/%s", embeddingKeyFilePath, indexingKeyName) - config.Embedding.QueryKeyFile = fmt.Sprintf("%s/%s", embeddingKeyFilePath, queryKeyName) + config.Embedding.IndexingKeyFile = fmt.Sprintf("%s/%s", embeddingKeyFilePath, indexingKeyName) + config.Embedding.QueryKeyFile = fmt.Sprintf("%s/%s", embeddingKeyFilePath, queryKeyName) - // Since MCK right now installs search with one replica only it's safe to alway set IsAutoEmbeddingViewWriter to true. - // Once we start supporting multiple mongot instances, we need to figure this out and then set here. - config.Embedding.IsAutoEmbeddingViewWriter = &autoEmbeddingViewWriterTrue - } + // Since MCK right now installs search with one replica only it's safe to alway set IsAutoEmbeddingViewWriter to true. + // Once we start supporting multiple mongot instances, we need to figure this out and then set here. + config.Embedding.IsAutoEmbeddingViewWriter = &autoEmbeddingViewWriterTrue if r.mdbSearch.Spec.AutoEmbedding.ProviderEndpoint != "" { config.Embedding.ProviderEndpoint = r.mdbSearch.Spec.AutoEmbedding.ProviderEndpoint @@ -366,8 +352,11 @@ func (r *MongoDBSearchReconcileHelper) ensureEmbeddingConfig(ctx context.Context podtemplatespec.WithVolume(emptyDirVolume), podtemplatespec.WithVolumeMounts(MongotContainerName, emptyDirVolumeMount), podtemplatespec.WithContainer(MongotContainerName, setupMongotContainerArgsForAPIKeys()), + podtemplatespec.WithAnnotations(map[string]string{ + autoEmbeddingDetailsAnnKey: apiKeySecretHash, + }), )) - return mongotModification, stsModification, secretHash, nil + return mongotModification, stsModification, nil } func setupMongotContainerArgsForAPIKeys() container.Modification { diff --git a/controllers/searchcontroller/mongodbsearch_reconcile_helper_test.go b/controllers/searchcontroller/mongodbsearch_reconcile_helper_test.go index 49b6a0e51a..e7d0e97a11 100644 --- a/controllers/searchcontroller/mongodbsearch_reconcile_helper_test.go +++ b/controllers/searchcontroller/mongodbsearch_reconcile_helper_test.go @@ -406,8 +406,10 @@ func TestEnsureEmbeddingConfig_APIKeySecretAndProviderEndpont(t *testing.T) { ctx := context.TODO() fakeClient := newTestFakeClient(search, apiKeySecret) - helper := NewMongoDBSearchReconcileHelper(fakeClient, search, nil, OperatorSearchConfig{}) - mongotModif, stsModif, _, err := helper.ensureEmbeddingConfig(ctx) + helper := NewMongoDBSearchReconcileHelper(fakeClient, search, nil, OperatorSearchConfig{ + SearchVersion: "0.58.0", + }) + mongotModif, stsModif, err := helper.ensureEmbeddingConfig(ctx) assert.Nil(t, err) mongotModif(conf) @@ -421,9 +423,11 @@ func TestEnsureEmbeddingConfig_APIKeySecretAndProviderEndpont(t *testing.T) { func TestEnsureEmbeddingConfig_WOAutoEmbedding(t *testing.T) { search := newTestMongoDBSearch("mdb-searh", "mongodb") fakeClient := newTestFakeClient(search) - helper := NewMongoDBSearchReconcileHelper(fakeClient, search, nil, OperatorSearchConfig{}) + helper := NewMongoDBSearchReconcileHelper(fakeClient, search, nil, OperatorSearchConfig{ + SearchVersion: "0.58.0", + }) ctx := context.TODO() - mongotModif, stsModif, _, err := helper.ensureEmbeddingConfig(ctx) + mongotModif, stsModif, err := helper.ensureEmbeddingConfig(ctx) assert.Nil(t, err) conf := &mongot.Config{} @@ -462,9 +466,11 @@ func TestEnsureEmbeddingConfig_JustAPIKeys(t *testing.T) { } }) fakeClient := newTestFakeClient(search, apiKeySecret) - helper := NewMongoDBSearchReconcileHelper(fakeClient, search, nil, OperatorSearchConfig{}) + helper := NewMongoDBSearchReconcileHelper(fakeClient, search, nil, OperatorSearchConfig{ + SearchVersion: "0.58.0", + }) ctx := context.TODO() - mongotModif, stsModif, _, err := helper.ensureEmbeddingConfig(ctx) + mongotModif, stsModif, err := helper.ensureEmbeddingConfig(ctx) assert.Nil(t, err) conf := &mongot.Config{} @@ -604,7 +610,7 @@ func TestValidateSearchResource(t *testing.T) { helper := NewMongoDBSearchReconcileHelper(fakeClient, search, nil, OperatorSearchConfig{ SearchVersion: tc.searchVersion, }) - _, err := helper.validateSearchResource(ctx) + _, _, err := helper.ensureEmbeddingConfig(ctx) tc.errAssertion(t, err) if tc.errMsg != "" { assert.Equal(t, tc.errMsg, err.Error()) From f1db75a693765bd2a703d63311e9eb3daf71e82c Mon Sep 17 00:00:00 2001 From: Vivek Singh Date: Fri, 9 Jan 2026 15:49:07 +0100 Subject: [PATCH 08/10] Address review comments --- api/v1/search/mongodbsearch_types.go | 2 +- api/v1/search/zz_generated.deepcopy.go | 8 ++------ controllers/operator/mongodbsearch_controller.go | 2 +- .../mongodbsearch_reconcile_helper.go | 11 ++++++----- .../mongodbsearch_reconcile_helper_test.go | 14 +++++++------- go.mod | 2 +- 6 files changed, 18 insertions(+), 21 deletions(-) diff --git a/api/v1/search/mongodbsearch_types.go b/api/v1/search/mongodbsearch_types.go index 0a4038b52f..02c4b5ac14 100644 --- a/api/v1/search/mongodbsearch_types.go +++ b/api/v1/search/mongodbsearch_types.go @@ -82,7 +82,7 @@ type EmbeddingConfig struct { // EmbeddingModelAPIKeySecret would have the name of the secret that has two keys // query-key and indexing-key for embedding model's API keys. // +kubebuilder:validation:Required - EmbeddingModelAPIKeySecret *corev1.LocalObjectReference `json:"embeddingModelAPIKeySecret"` + EmbeddingModelAPIKeySecret corev1.LocalObjectReference `json:"embeddingModelAPIKeySecret"` } type MongoDBSource struct { diff --git a/api/v1/search/zz_generated.deepcopy.go b/api/v1/search/zz_generated.deepcopy.go index 6f1e7525dd..09189907bb 100644 --- a/api/v1/search/zz_generated.deepcopy.go +++ b/api/v1/search/zz_generated.deepcopy.go @@ -31,11 +31,7 @@ import ( // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *EmbeddingConfig) DeepCopyInto(out *EmbeddingConfig) { *out = *in - if in.EmbeddingModelAPIKeySecret != nil { - in, out := &in.EmbeddingModelAPIKeySecret, &out.EmbeddingModelAPIKeySecret - *out = new(v1.LocalObjectReference) - **out = **in - } + out.EmbeddingModelAPIKeySecret = in.EmbeddingModelAPIKeySecret } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new EmbeddingConfig. @@ -188,7 +184,7 @@ func (in *MongoDBSearchSpec) DeepCopyInto(out *MongoDBSearchSpec) { if in.AutoEmbedding != nil { in, out := &in.AutoEmbedding, &out.AutoEmbedding *out = new(EmbeddingConfig) - (*in).DeepCopyInto(*out) + **out = **in } } diff --git a/controllers/operator/mongodbsearch_controller.go b/controllers/operator/mongodbsearch_controller.go index b025da4011..adfdf1a61e 100644 --- a/controllers/operator/mongodbsearch_controller.go +++ b/controllers/operator/mongodbsearch_controller.go @@ -78,7 +78,7 @@ func (r *MongoDBSearchReconciler) Reconcile(ctx context.Context, request reconci r.watch.AddWatchedResourceIfNotAdded(mdbSearch.Spec.Security.TLS.CertificateKeySecret.Name, mdbSearch.Namespace, watch.Secret, mdbSearch.NamespacedName()) } - if mdbSearch.Spec.AutoEmbedding.EmbeddingModelAPIKeySecret != nil { + if mdbSearch.Spec.AutoEmbedding != nil { r.watch.AddWatchedResourceIfNotAdded(mdbSearch.Spec.AutoEmbedding.EmbeddingModelAPIKeySecret.Name, mdbSearch.Namespace, watch.Secret, mdbSearch.NamespacedName()) } diff --git a/controllers/searchcontroller/mongodbsearch_reconcile_helper.go b/controllers/searchcontroller/mongodbsearch_reconcile_helper.go index 5f5859f432..6a8bc98e42 100644 --- a/controllers/searchcontroller/mongodbsearch_reconcile_helper.go +++ b/controllers/searchcontroller/mongodbsearch_reconcile_helper.go @@ -142,7 +142,7 @@ func (r *MongoDBSearchReconcileHelper) reconcile(ctx context.Context, log *zap.S egressTlsMongotModification, egressTlsStsModification := r.ensureEgressTlsConfig(ctx) - embeddingConfigMongotModification, embeddingConfigStsModification, err := r.ensureEmbeddingConfig(ctx) + embeddingConfigMongotModification, embeddingConfigStsModification, err := r.ensureEmbeddingConfig(ctx, log) if err != nil { return workflow.Failed(err) } @@ -291,10 +291,11 @@ func ensureEmbeddingAPIKeySecret(ctx context.Context, client secret.Getter, secr return hashBytes(d), nil } -func validateSearchVesionForEmbedding(version string) error { +func validateSearchVesionForEmbedding(version string, log *zap.SugaredLogger) error { searchVersion, err := semver.NewVersion(version) if err != nil { - return fmt.Errorf("Failed getting semver of search image version. Version %s doesn't seem to be valid semver.", version) + log.Debugf("Failed getting semver of search image version. Version %s doesn't seem to be valid semver.", version) + return nil } minAllowedVersion, _ := semver.NewVersion(minSearchImageVersionForEmbedding) @@ -306,7 +307,7 @@ func validateSearchVesionForEmbedding(version string) error { // ensureEmbeddingConfig returns the mongot config and stateful set modification function based on the values provided in the search CR, it // also returns the hash of the secret that has the embedding API keys so that if the keys are changed the search pod is automatically restarted. -func (r *MongoDBSearchReconcileHelper) ensureEmbeddingConfig(ctx context.Context) (mongot.Modification, statefulset.Modification, error) { +func (r *MongoDBSearchReconcileHelper) ensureEmbeddingConfig(ctx context.Context, log *zap.SugaredLogger) (mongot.Modification, statefulset.Modification, error) { if r.mdbSearch.Spec.AutoEmbedding == nil { return mongot.NOOP(), statefulset.NOOP(), nil } @@ -322,7 +323,7 @@ func (r *MongoDBSearchReconcileHelper) ensureEmbeddingConfig(ctx context.Context } _, version := r.searchImageAndVersion() - if err := validateSearchVesionForEmbedding(version); err != nil { + if err := validateSearchVesionForEmbedding(version, log); err != nil { return nil, nil, err } diff --git a/controllers/searchcontroller/mongodbsearch_reconcile_helper_test.go b/controllers/searchcontroller/mongodbsearch_reconcile_helper_test.go index e7d0e97a11..5e5fe1780a 100644 --- a/controllers/searchcontroller/mongodbsearch_reconcile_helper_test.go +++ b/controllers/searchcontroller/mongodbsearch_reconcile_helper_test.go @@ -369,7 +369,7 @@ func TestEnsureEmbeddingConfig_APIKeySecretAndProviderEndpont(t *testing.T) { search := newTestMongoDBSearch("mdb-searh", "mongodb", func(s *searchv1.MongoDBSearch) { s.Spec.AutoEmbedding = &searchv1.EmbeddingConfig{ ProviderEndpoint: providerEndpoint, - EmbeddingModelAPIKeySecret: &corev1.LocalObjectReference{ + EmbeddingModelAPIKeySecret: corev1.LocalObjectReference{ Name: testApiKeySecretName, }, } @@ -409,7 +409,7 @@ func TestEnsureEmbeddingConfig_APIKeySecretAndProviderEndpont(t *testing.T) { helper := NewMongoDBSearchReconcileHelper(fakeClient, search, nil, OperatorSearchConfig{ SearchVersion: "0.58.0", }) - mongotModif, stsModif, err := helper.ensureEmbeddingConfig(ctx) + mongotModif, stsModif, err := helper.ensureEmbeddingConfig(ctx, nil) assert.Nil(t, err) mongotModif(conf) @@ -427,7 +427,7 @@ func TestEnsureEmbeddingConfig_WOAutoEmbedding(t *testing.T) { SearchVersion: "0.58.0", }) ctx := context.TODO() - mongotModif, stsModif, err := helper.ensureEmbeddingConfig(ctx) + mongotModif, stsModif, err := helper.ensureEmbeddingConfig(ctx, nil) assert.Nil(t, err) conf := &mongot.Config{} @@ -460,7 +460,7 @@ func TestEnsureEmbeddingConfig_WOAutoEmbedding(t *testing.T) { func TestEnsureEmbeddingConfig_JustAPIKeys(t *testing.T) { search := newTestMongoDBSearch("mdb-search", "mongodb", func(s *searchv1.MongoDBSearch) { s.Spec.AutoEmbedding = &searchv1.EmbeddingConfig{ - EmbeddingModelAPIKeySecret: &corev1.LocalObjectReference{ + EmbeddingModelAPIKeySecret: corev1.LocalObjectReference{ Name: testApiKeySecretName, }, } @@ -470,7 +470,7 @@ func TestEnsureEmbeddingConfig_JustAPIKeys(t *testing.T) { SearchVersion: "0.58.0", }) ctx := context.TODO() - mongotModif, stsModif, err := helper.ensureEmbeddingConfig(ctx) + mongotModif, stsModif, err := helper.ensureEmbeddingConfig(ctx, nil) assert.Nil(t, err) conf := &mongot.Config{} @@ -511,7 +511,7 @@ func TestEnsureEmbeddingConfig_JustAPIKeys(t *testing.T) { func TestValidateSearchResource(t *testing.T) { search := newTestMongoDBSearch("mdb-search", "mongodb", func(s *searchv1.MongoDBSearch) { s.Spec.AutoEmbedding = &searchv1.EmbeddingConfig{ - EmbeddingModelAPIKeySecret: &corev1.LocalObjectReference{ + EmbeddingModelAPIKeySecret: corev1.LocalObjectReference{ Name: testApiKeySecretName, }, } @@ -610,7 +610,7 @@ func TestValidateSearchResource(t *testing.T) { helper := NewMongoDBSearchReconcileHelper(fakeClient, search, nil, OperatorSearchConfig{ SearchVersion: tc.searchVersion, }) - _, _, err := helper.ensureEmbeddingConfig(ctx) + _, _, err := helper.ensureEmbeddingConfig(ctx, nil) tc.errAssertion(t, err) if tc.errMsg != "" { assert.Equal(t, tc.errMsg, err.Error()) diff --git a/go.mod b/go.mod index 8ee6850805..ead7d8b38d 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module github.com/mongodb/mongodb-kubernetes require ( + github.com/Masterminds/semver/v3 v3.4.0 github.com/blang/semver v3.5.1+incompatible github.com/ghodss/yaml v1.0.0 github.com/go-logr/logr v1.4.3 @@ -47,7 +48,6 @@ require ( require google.golang.org/protobuf v1.36.10 // indirect require ( - github.com/Masterminds/semver/v3 v3.4.0 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/cenkalti/backoff/v4 v4.3.0 // indirect github.com/cenkalti/backoff/v5 v5.0.3 // indirect From 2b562cae4cbac94e69892ba4c8c0fff19e7e71a1 Mon Sep 17 00:00:00 2001 From: Vivek Singh Date: Fri, 9 Jan 2026 16:14:18 +0100 Subject: [PATCH 09/10] Commit changes to third party licenses --- LICENSE-THIRD-PARTY | 1 + 1 file changed, 1 insertion(+) diff --git a/LICENSE-THIRD-PARTY b/LICENSE-THIRD-PARTY index 4bad126ce9..a183015fd0 100644 --- a/LICENSE-THIRD-PARTY +++ b/LICENSE-THIRD-PARTY @@ -1,4 +1,5 @@ +github.com/Masterminds/semver/v3,v3.4.0,https://github.com/Masterminds/semver/blob/v3.4.0/LICENSE.txt,MIT github.com/beorn7/perks/quantile,v1.0.1,https://github.com/beorn7/perks/blob/v1.0.1/LICENSE,MIT github.com/blang/semver,v3.5.1,https://github.com/blang/semver/blob/v3.5.1/LICENSE,MIT github.com/cenkalti/backoff/v4,v4.3.0,https://github.com/cenkalti/backoff/blob/v4.3.0/LICENSE,MIT From 273712a903508735de2ab335e9544f3030c58ed2 Mon Sep 17 00:00:00 2001 From: Vivek Singh Date: Fri, 9 Jan 2026 17:06:42 +0100 Subject: [PATCH 10/10] Small improvements 1. Imrpove test to make sure even zero values are not present in mongot config if autoEmbedding is not provided in CR --- .../mongodbsearch_reconcile_helper.go | 6 +++-- .../mongodbsearch_reconcile_helper_test.go | 27 +++++++++++++++++-- .../pkg/mongot/mongot_config.go | 10 +++---- 3 files changed, 34 insertions(+), 9 deletions(-) diff --git a/controllers/searchcontroller/mongodbsearch_reconcile_helper.go b/controllers/searchcontroller/mongodbsearch_reconcile_helper.go index 6a8bc98e42..6d888b604d 100644 --- a/controllers/searchcontroller/mongodbsearch_reconcile_helper.go +++ b/controllers/searchcontroller/mongodbsearch_reconcile_helper.go @@ -329,8 +329,10 @@ func (r *MongoDBSearchReconcileHelper) ensureEmbeddingConfig(ctx context.Context autoEmbeddingViewWriterTrue := true mongotModification := func(config *mongot.Config) { - config.Embedding.IndexingKeyFile = fmt.Sprintf("%s/%s", embeddingKeyFilePath, indexingKeyName) - config.Embedding.QueryKeyFile = fmt.Sprintf("%s/%s", embeddingKeyFilePath, queryKeyName) + config.Embedding = &mongot.EmbeddingConfig{ + IndexingKeyFile: fmt.Sprintf("%s/%s", embeddingKeyFilePath, indexingKeyName), + QueryKeyFile: fmt.Sprintf("%s/%s", embeddingKeyFilePath, queryKeyName), + } // Since MCK right now installs search with one replica only it's safe to alway set IsAutoEmbeddingViewWriter to true. // Once we start supporting multiple mongot instances, we need to figure this out and then set here. diff --git a/controllers/searchcontroller/mongodbsearch_reconcile_helper_test.go b/controllers/searchcontroller/mongodbsearch_reconcile_helper_test.go index 5e5fe1780a..873b249e7e 100644 --- a/controllers/searchcontroller/mongodbsearch_reconcile_helper_test.go +++ b/controllers/searchcontroller/mongodbsearch_reconcile_helper_test.go @@ -6,6 +6,7 @@ import ( "strings" "testing" + "github.com/ghodss/yaml" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.uber.org/zap" @@ -396,7 +397,7 @@ func TestEnsureEmbeddingConfig_APIKeySecretAndProviderEndpont(t *testing.T) { embeddingWriterTrue := true expectedMongotConfig := mongot.Config{ - Embedding: mongot.EmbeddingConfig{ + Embedding: &mongot.EmbeddingConfig{ ProviderEndpoint: providerEndpoint, IsAutoEmbeddingViewWriter: &embeddingWriterTrue, QueryKeyFile: fmt.Sprintf("%s/%s", embeddingKeyFilePath, queryKeyName), @@ -421,6 +422,22 @@ func TestEnsureEmbeddingConfig_APIKeySecretAndProviderEndpont(t *testing.T) { } func TestEnsureEmbeddingConfig_WOAutoEmbedding(t *testing.T) { + mongotCMWithoutEmbedding := `healthCheck: + address: "" +logging: + verbosity: "" +metrics: + address: "" + enabled: false +server: {} +storage: + dataPath: "" +syncSource: + replicaSet: + hostAndPort: null + passwordFile: "" + username: ""` + search := newTestMongoDBSearch("mdb-searh", "mongodb") fakeClient := newTestFakeClient(search) helper := NewMongoDBSearchReconcileHelper(fakeClient, search, nil, OperatorSearchConfig{ @@ -452,6 +469,12 @@ func TestEnsureEmbeddingConfig_WOAutoEmbedding(t *testing.T) { mongotModif(conf) stsModif(sts) + // verify that if the embedding config is not provided in the CR, the mongot config's YAML representation + // doesn't have even zero values of embedding config. + cm, err := yaml.Marshal(conf) + assert.Nil(t, err) + assert.YAMLEq(t, mongotCMWithoutEmbedding, string(cm)) + // because search CR didn't have autoEmbedding configured, there wont be any change in conf or sts assert.Equal(t, sts, sts) assert.Equal(t, conf, conf) @@ -497,7 +520,7 @@ func TestEnsureEmbeddingConfig_JustAPIKeys(t *testing.T) { // We are just providing the autoEmbedding API Key secret, that's why we will only see that in the config // and we will see the volumes, mounts in sts - assert.Equal(t, mongot.EmbeddingConfig{ + assert.Equal(t, &mongot.EmbeddingConfig{ QueryKeyFile: fmt.Sprintf("%s/%s", embeddingKeyFilePath, queryKeyName), IndexingKeyFile: fmt.Sprintf("%s/%s", embeddingKeyFilePath, indexingKeyName), IsAutoEmbeddingViewWriter: &embeddingWriterTrue, diff --git a/mongodb-community-operator/pkg/mongot/mongot_config.go b/mongodb-community-operator/pkg/mongot/mongot_config.go index d0b815c08f..68c0f59563 100644 --- a/mongodb-community-operator/pkg/mongot/mongot_config.go +++ b/mongodb-community-operator/pkg/mongot/mongot_config.go @@ -21,14 +21,14 @@ type Config struct { Metrics ConfigMetrics `json:"metrics"` HealthCheck ConfigHealthCheck `json:"healthCheck"` Logging ConfigLogging `json:"logging"` - Embedding EmbeddingConfig `json:"embedding"` + Embedding *EmbeddingConfig `json:"embedding,omitempty"` } type EmbeddingConfig struct { - QueryKeyFile string `json:"queryKeyFile" yaml:"queryKeyFile, omitempty"` - IndexingKeyFile string `json:"indexingKeyFile" yaml:"indexingKeyFile, omitempty"` - ProviderEndpoint string `json:"providerEndpoint" yaml:"providerEndpoint, omitempty"` - IsAutoEmbeddingViewWriter *bool `json:"isAutoEmbeddingViewWriter" yaml:"isAutoEmbeddingViewWriter, omitempty"` + QueryKeyFile string `json:"queryKeyFile" yaml:"queryKeyFile,omitempty"` + IndexingKeyFile string `json:"indexingKeyFile" yaml:"indexingKeyFile,omitempty"` + ProviderEndpoint string `json:"providerEndpoint" yaml:"providerEndpoint,omitempty"` + IsAutoEmbeddingViewWriter *bool `json:"isAutoEmbeddingViewWriter" yaml:"isAutoEmbeddingViewWriter,omitempty"` } type ConfigSyncSource struct {