From 88c129077d61c3c3ff4d2d2f331b4fdef1c5cd16 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Sierant?= Date: Mon, 8 Sep 2025 22:36:19 +0200 Subject: [PATCH 01/29] CLOUDP-312564: MongoDB Search - Public Preview Co-authored-by: Anand Singh Co-authored-by: Yavor Georgiev --- .evergreen-tasks.yml | 26 ++ .evergreen.yml | 6 + api/v1/search/mongodbsearch_types.go | 99 ++++- api/v1/search/zz_generated.deepcopy.go | 98 +++++ .../mongodb.com_clustermongodbroles.yaml | 2 +- config/crd/bases/mongodb.com_mongodb.yaml | 48 +-- .../mongodb.com_mongodbmulticluster.yaml | 48 +-- .../crd/bases/mongodb.com_mongodbsearch.yaml | 130 ++++--- .../crd/bases/mongodb.com_mongodbusers.yaml | 2 +- config/crd/bases/mongodb.com_opsmanagers.yaml | 112 +----- ...ommunity.mongodb.com_mongodbcommunity.yaml | 54 +-- .../operator/mongodbreplicaset_controller.go | 100 ++++- .../operator/mongodbsearch_controller.go | 80 +++- .../operator/mongodbsearch_controller_test.go | 153 ++++++-- .../operator/watch/config_change_handler.go | 12 +- .../community_search_source.go | 84 +++++ .../community_search_source_test.go | 208 +++++++++++ .../enterprise_search_source.go | 88 +++++ .../enterprise_search_source_test.go | 290 +++++++++++++++ .../external_search_source.go | 49 +++ .../mongodbsearch_reconcile_helper.go | 347 +++++++++++++++--- .../mongodbsearch_reconcile_helper_test.go | 62 +--- .../search_controller/search_construction.go | 136 +++---- .../edit_mms_configuration.go | 4 +- .../kubetester/helm.py | 3 + .../kubetester/mongodb.py | 6 +- .../common/search/movies_search_helper.py | 42 ++- .../tests/common/search/search_tester.py | 14 +- ...nity-replicaset-sample-mflix-external.yaml | 112 ++++++ .../community-replicaset-sample-mflix.yaml | 69 ++-- .../enterprise-replicaset-sample-mflix.yaml | 34 ++ .../fixtures/mongodbuser-mdb-admin.yaml | 16 + .../search/fixtures/mongodbuser-mdb-user.yaml | 16 + .../mongodbuser-search-sync-source-user.yaml | 18 + .../fixtures/search-with-user-password.yaml | 5 + .../tests/search/search_community_basic.py | 42 ++- .../search_community_external_mongod_basic.py | 144 ++++++++ .../search_community_external_mongod_tls.py | 190 ++++++++++ .../tests/search/search_community_tls.py | 173 +++++++++ .../tests/search/search_enterprise_basic.py | 183 +++++++++ .../tests/search/search_enterprise_tls.py | 239 ++++++++++++ .../crds/mongodb.com_mongodbsearch.yaml | 107 +++++- .../controllers/replica_set_controller.go | 76 +++- .../pkg/mongot/mongot_config.go | 89 +++-- mongodb-community-operator/pkg/tls/tls.go | 113 ++++++ pkg/telemetry/collector.go | 52 ++- pkg/telemetry/collector_test.go | 113 ++++-- public/crds.yaml | 107 +++++- scripts/dev/contexts/e2e_mdb_community | 2 + scripts/dev/contexts/e2e_mdb_kind_ubi_cloudqa | 2 + .../contexts/e2e_static_mdb_kind_ubi_cloudqa | 2 + scripts/dev/contexts/evg-private-context | 4 +- scripts/dev/contexts/root-context | 8 +- .../dev/contexts/variables/mongodb_search_dev | 8 + scripts/funcs/operator_deployment | 3 + 55 files changed, 3538 insertions(+), 692 deletions(-) create mode 100644 controllers/search_controller/community_search_source.go create mode 100644 controllers/search_controller/community_search_source_test.go create mode 100644 controllers/search_controller/enterprise_search_source.go create mode 100644 controllers/search_controller/enterprise_search_source_test.go create mode 100644 controllers/search_controller/external_search_source.go create mode 100644 docker/mongodb-kubernetes-tests/tests/search/fixtures/community-replicaset-sample-mflix-external.yaml create mode 100644 docker/mongodb-kubernetes-tests/tests/search/fixtures/enterprise-replicaset-sample-mflix.yaml create mode 100644 docker/mongodb-kubernetes-tests/tests/search/fixtures/mongodbuser-mdb-admin.yaml create mode 100644 docker/mongodb-kubernetes-tests/tests/search/fixtures/mongodbuser-mdb-user.yaml create mode 100644 docker/mongodb-kubernetes-tests/tests/search/fixtures/mongodbuser-search-sync-source-user.yaml create mode 100644 docker/mongodb-kubernetes-tests/tests/search/fixtures/search-with-user-password.yaml create mode 100644 docker/mongodb-kubernetes-tests/tests/search/search_community_external_mongod_basic.py create mode 100644 docker/mongodb-kubernetes-tests/tests/search/search_community_external_mongod_tls.py create mode 100644 docker/mongodb-kubernetes-tests/tests/search/search_community_tls.py create mode 100644 docker/mongodb-kubernetes-tests/tests/search/search_enterprise_basic.py create mode 100644 docker/mongodb-kubernetes-tests/tests/search/search_enterprise_tls.py create mode 100644 mongodb-community-operator/pkg/tls/tls.go create mode 100644 scripts/dev/contexts/variables/mongodb_search_dev diff --git a/.evergreen-tasks.yml b/.evergreen-tasks.yml index 9b6cfd893..6ec158a41 100644 --- a/.evergreen-tasks.yml +++ b/.evergreen-tasks.yml @@ -1288,3 +1288,29 @@ tasks: tags: ["patch-run"] commands: - func: "e2e_test" + + - name: e2e_search_community_tls + tags: ["patch-run"] + commands: + - func: "e2e_test" + + - name: e2e_search_external_basic + tags: [ "patch-run" ] + commands: + - func: "e2e_test" + + - name: e2e_search_external_tls + tags: [ "patch-run" ] + commands: + - func: "e2e_test" + + - name: e2e_search_enterprise_basic + tags: [ "patch-run" ] + commands: + - func: "e2e_test" + + - name: e2e_search_enterprise_tls + tags: [ "patch-run" ] + commands: + - func: "e2e_test" + diff --git a/.evergreen.yml b/.evergreen.yml index ddf1ad3e8..dd3d3eaed 100644 --- a/.evergreen.yml +++ b/.evergreen.yml @@ -567,6 +567,9 @@ task_groups: tasks: - e2e_community_replicaset_scale - e2e_search_community_basic + - e2e_search_community_tls + - e2e_search_external_basic + - e2e_search_external_tls # This is the task group that contains all the tests run in the e2e_mdb_kind_ubuntu_cloudqa build variant - name: e2e_mdb_kind_cloudqa_task_group @@ -691,6 +694,9 @@ task_groups: - e2e_replica_set_oidc_workforce - e2e_sharded_cluster_oidc_m2m_group - e2e_sharded_cluster_oidc_m2m_user + # MongoDBSearch test group + - e2e_search_enterprise_basic + - e2e_search_enterprise_tls <<: *teardown_group # this task group contains just a one task, which is smoke testing whether the operator diff --git a/api/v1/search/mongodbsearch_types.go b/api/v1/search/mongodbsearch_types.go index 43e9be1be..5a8037b51 100644 --- a/api/v1/search/mongodbsearch_types.go +++ b/api/v1/search/mongodbsearch_types.go @@ -1,6 +1,8 @@ package search import ( + "fmt" + "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/types" @@ -14,8 +16,10 @@ import ( ) const ( - MongotDefaultPort = 27027 - MongotDefaultMetricsPort = 9946 + MongotDefaultPort = 27027 + MongotDefaultMetricsPort = 9946 + MongotDefautHealthCheckPort = 8080 + MongotDefaultSyncSourceUsername = "search-sync-source" ) func init() { @@ -23,21 +27,66 @@ func init() { } type MongoDBSearchSpec struct { + // Optional version of MongoDB Search component (mongot). If not set, then the operator will set the most appropriate version of MongoDB Search. // +optional Version string `json:"version"` + // MongoDB database connection details from which MongoDB Search will synchronize data to build indexes. // +optional Source *MongoDBSource `json:"source"` + // StatefulSetSpec which the operator will apply to the MongoDB Search StatefulSet at the end of the reconcile loop. Use to provide necessary customizations, + // which aren't exposed as fields in the MongoDBSearch.spec. // +optional StatefulSetConfiguration *common.StatefulSetConfiguration `json:"statefulSet,omitempty"` + // Configure MongoDB Search's persistent volume. If not defined, the operator will request 10GB of storage. // +optional Persistence *common.Persistence `json:"persistence,omitempty"` + // Configure resource requests and limits for the MongoDB Search pods. // +optional ResourceRequirements *corev1.ResourceRequirements `json:"resourceRequirements,omitempty"` + // Configure security settings of the MongoDB Search server that MongoDB database is connecting to when performing search queries. + // +optional + Security Security `json:"security"` } type MongoDBSource struct { // +optional MongoDBResourceRef *userv1.MongoDBResourceRef `json:"mongodbResourceRef,omitempty"` + // +optional + ExternalMongoDBSource *ExternalMongoDBSource `json:"external,omitempty"` + // +optional + PasswordSecretRef *userv1.SecretKeyRef `json:"passwordSecretRef,omitempty"` + // +optional + Username *string `json:"username,omitempty"` +} + +type ExternalMongoDBSource struct { + HostAndPorts []string `json:"hostAndPorts,omitempty"` + // mongod keyfile used to connect to the external MongoDB deployment + KeyFileSecretKeyRef *userv1.SecretKeyRef `json:"keyFileSecretRef,omitempty"` + // +optional + TLS *ExternalMongodTLS `json:"tls,omitempty"` // TLS configuration for the external MongoDB deployment +} + +type ExternalMongodTLS struct { + Enabled bool `json:"enabled"` + // +optional + CA *corev1.LocalObjectReference `json:"ca,omitempty"` +} + +type Security struct { + // +optional + TLS TLS `json:"tls"` +} + +type TLS struct { + Enabled bool `json:"enabled"` + // CertificateKeySecret is a reference to a Secret containing a private key and certificate to use for TLS. + // The key and cert are expected to be PEM encoded and available at "tls.key" and "tls.crt". + // This is the same format used for the standard "kubernetes.io/tls" Secret type, but no specific type is required. + // Alternatively, an entry tls.pem, containing the concatenation of cert and key, can be provided. + // If all of tls.pem, tls.crt and tls.key are present, the tls.pem one needs to be equal to the concatenation of tls.crt and tls.key + // +optional + CertificateKeySecret corev1.LocalObjectReference `json:"certificateKeySecretRef"` } type MongoDBSearchStatus struct { @@ -105,6 +154,25 @@ func (s *MongoDBSearch) MongotConfigConfigMapNamespacedName() types.NamespacedNa return types.NamespacedName{Name: s.Name + "-search-config", Namespace: s.Namespace} } +func (s *MongoDBSearch) SourceUserPasswordSecretRef() *userv1.SecretKeyRef { + if s.Spec.Source != nil && s.Spec.Source.PasswordSecretRef != nil { + return s.Spec.Source.PasswordSecretRef + } + + return &userv1.SecretKeyRef{ + Name: fmt.Sprintf("%s-%s-password", s.Name, MongotDefaultSyncSourceUsername), + Key: "password", + } +} + +func (s *MongoDBSearch) SourceUsername() string { + if s.Spec.Source != nil && s.Spec.Source.Username != nil { + return *s.Spec.Source.Username + } + + return MongotDefaultSyncSourceUsername +} + func (s *MongoDBSearch) StatefulSetNamespacedName() types.NamespacedName { return types.NamespacedName{Name: s.Name + "-search", Namespace: s.Namespace} } @@ -118,13 +186,17 @@ func (s *MongoDBSearch) GetOwnerReferences() []metav1.OwnerReference { return []metav1.OwnerReference{ownerReference} } -func (s *MongoDBSearch) GetMongoDBResourceRef() userv1.MongoDBResourceRef { +func (s *MongoDBSearch) GetMongoDBResourceRef() *userv1.MongoDBResourceRef { + if s.IsExternalMongoDBSource() { + return nil + } + mdbResourceRef := userv1.MongoDBResourceRef{Namespace: s.Namespace, Name: s.Name} if s.Spec.Source != nil && s.Spec.Source.MongoDBResourceRef != nil && s.Spec.Source.MongoDBResourceRef.Name != "" { mdbResourceRef.Name = s.Spec.Source.MongoDBResourceRef.Name } - return mdbResourceRef + return &mdbResourceRef } func (s *MongoDBSearch) GetMongotPort() int32 { @@ -134,3 +206,22 @@ func (s *MongoDBSearch) GetMongotPort() int32 { func (s *MongoDBSearch) GetMongotMetricsPort() int32 { return MongotDefaultMetricsPort } + +// TLSSecretNamespacedName will get the namespaced name of the Secret containing the server certificate and key +func (s *MongoDBSearch) TLSSecretNamespacedName() types.NamespacedName { + return types.NamespacedName{Name: s.Spec.Security.TLS.CertificateKeySecret.Name, Namespace: s.Namespace} +} + +// TLSOperatorSecretNamespacedName will get the namespaced name of the Secret created by the operator +// containing the combined certificate and key. +func (s *MongoDBSearch) TLSOperatorSecretNamespacedName() types.NamespacedName { + return types.NamespacedName{Name: s.Name + "-search-certificate-key", Namespace: s.Namespace} +} + +func (s *MongoDBSearch) GetMongotHealthCheckPort() int32 { + return MongotDefautHealthCheckPort +} + +func (s *MongoDBSearch) IsExternalMongoDBSource() bool { + return s.Spec.Source != nil && s.Spec.Source.ExternalMongoDBSource != nil +} diff --git a/api/v1/search/zz_generated.deepcopy.go b/api/v1/search/zz_generated.deepcopy.go index e9384e3de..c66322146 100644 --- a/api/v1/search/zz_generated.deepcopy.go +++ b/api/v1/search/zz_generated.deepcopy.go @@ -28,6 +28,56 @@ import ( runtime "k8s.io/apimachinery/pkg/runtime" ) +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ExternalMongoDBSource) DeepCopyInto(out *ExternalMongoDBSource) { + *out = *in + if in.HostAndPorts != nil { + in, out := &in.HostAndPorts, &out.HostAndPorts + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.KeyFileSecretKeyRef != nil { + in, out := &in.KeyFileSecretKeyRef, &out.KeyFileSecretKeyRef + *out = new(user.SecretKeyRef) + **out = **in + } + if in.TLS != nil { + in, out := &in.TLS, &out.TLS + *out = new(ExternalMongodTLS) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ExternalMongoDBSource. +func (in *ExternalMongoDBSource) DeepCopy() *ExternalMongoDBSource { + if in == nil { + return nil + } + out := new(ExternalMongoDBSource) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ExternalMongodTLS) DeepCopyInto(out *ExternalMongodTLS) { + *out = *in + if in.CA != nil { + in, out := &in.CA, &out.CA + *out = new(v1.LocalObjectReference) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ExternalMongodTLS. +func (in *ExternalMongodTLS) DeepCopy() *ExternalMongodTLS { + if in == nil { + return nil + } + out := new(ExternalMongodTLS) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *MongoDBSearch) DeepCopyInto(out *MongoDBSearch) { *out = *in @@ -109,6 +159,7 @@ func (in *MongoDBSearchSpec) DeepCopyInto(out *MongoDBSearchSpec) { *out = new(v1.ResourceRequirements) (*in).DeepCopyInto(*out) } + out.Security = in.Security } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MongoDBSearchSpec. @@ -150,6 +201,21 @@ func (in *MongoDBSource) DeepCopyInto(out *MongoDBSource) { *out = new(user.MongoDBResourceRef) **out = **in } + if in.ExternalMongoDBSource != nil { + in, out := &in.ExternalMongoDBSource, &out.ExternalMongoDBSource + *out = new(ExternalMongoDBSource) + (*in).DeepCopyInto(*out) + } + if in.PasswordSecretRef != nil { + in, out := &in.PasswordSecretRef, &out.PasswordSecretRef + *out = new(user.SecretKeyRef) + **out = **in + } + if in.Username != nil { + in, out := &in.Username, &out.Username + *out = new(string) + **out = **in + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MongoDBSource. @@ -161,3 +227,35 @@ func (in *MongoDBSource) DeepCopy() *MongoDBSource { in.DeepCopyInto(out) return out } + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Security) DeepCopyInto(out *Security) { + *out = *in + out.TLS = in.TLS +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Security. +func (in *Security) DeepCopy() *Security { + if in == nil { + return nil + } + out := new(Security) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *TLS) DeepCopyInto(out *TLS) { + *out = *in + out.CertificateKeySecret = in.CertificateKeySecret +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TLS. +func (in *TLS) DeepCopy() *TLS { + if in == nil { + return nil + } + out := new(TLS) + in.DeepCopyInto(out) + return out +} diff --git a/config/crd/bases/mongodb.com_clustermongodbroles.yaml b/config/crd/bases/mongodb.com_clustermongodbroles.yaml index 3d583bcfd..9241b7dad 100644 --- a/config/crd/bases/mongodb.com_clustermongodbroles.yaml +++ b/config/crd/bases/mongodb.com_clustermongodbroles.yaml @@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.18.0 + controller-gen.kubebuilder.io/version: v0.15.0 name: clustermongodbroles.mongodb.com spec: group: mongodb.com diff --git a/config/crd/bases/mongodb.com_mongodb.yaml b/config/crd/bases/mongodb.com_mongodb.yaml index d421d8837..2a7076877 100644 --- a/config/crd/bases/mongodb.com_mongodb.yaml +++ b/config/crd/bases/mongodb.com_mongodb.yaml @@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.18.0 + controller-gen.kubebuilder.io/version: v0.15.0 name: mongodb.mongodb.com spec: group: mongodb.com @@ -1410,29 +1410,7 @@ spec: automationLdapGroupDN: type: string automationPasswordSecretRef: - description: SecretKeySelector selects a key of a Secret. - properties: - key: - description: The key of the secret to select from. Must - be a valid secret key. - type: string - 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 - optional: - description: Specify whether the Secret or its key - must be defined - type: boolean - required: - - key - type: object - x-kubernetes-map-type: atomic + $ref: '#/definitions/k8s.io~1api~1core~1v1~0SecretKeySelector' automationUserName: type: string clientCertificateSecretRef: @@ -1467,29 +1445,9 @@ spec: bindQueryUser: type: string caConfigMapRef: + $ref: '#/definitions/k8s.io~1api~1core~1v1~0ConfigMapKeySelector' description: Allows to point at a ConfigMap/key with a CA file to mount on the Pod - properties: - key: - description: The key to select. - type: string - 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 - optional: - description: Specify whether the ConfigMap or its - key must be defined - type: boolean - required: - - key - type: object - x-kubernetes-map-type: atomic servers: items: type: string diff --git a/config/crd/bases/mongodb.com_mongodbmulticluster.yaml b/config/crd/bases/mongodb.com_mongodbmulticluster.yaml index 3f1fa05c9..01fe3f2e6 100644 --- a/config/crd/bases/mongodb.com_mongodbmulticluster.yaml +++ b/config/crd/bases/mongodb.com_mongodbmulticluster.yaml @@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.18.0 + controller-gen.kubebuilder.io/version: v0.15.0 name: mongodbmulticluster.mongodb.com spec: group: mongodb.com @@ -670,29 +670,7 @@ spec: automationLdapGroupDN: type: string automationPasswordSecretRef: - description: SecretKeySelector selects a key of a Secret. - properties: - key: - description: The key of the secret to select from. Must - be a valid secret key. - type: string - 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 - optional: - description: Specify whether the Secret or its key - must be defined - type: boolean - required: - - key - type: object - x-kubernetes-map-type: atomic + $ref: '#/definitions/k8s.io~1api~1core~1v1~0SecretKeySelector' automationUserName: type: string clientCertificateSecretRef: @@ -727,29 +705,9 @@ spec: bindQueryUser: type: string caConfigMapRef: + $ref: '#/definitions/k8s.io~1api~1core~1v1~0ConfigMapKeySelector' description: Allows to point at a ConfigMap/key with a CA file to mount on the Pod - properties: - key: - description: The key to select. - type: string - 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 - optional: - description: Specify whether the ConfigMap or its - key must be defined - type: boolean - required: - - key - type: object - x-kubernetes-map-type: atomic servers: items: type: string diff --git a/config/crd/bases/mongodb.com_mongodbsearch.yaml b/config/crd/bases/mongodb.com_mongodbsearch.yaml index 72ea0e50e..7c53c195c 100644 --- a/config/crd/bases/mongodb.com_mongodbsearch.yaml +++ b/config/crd/bases/mongodb.com_mongodbsearch.yaml @@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.18.0 + controller-gen.kubebuilder.io/version: v0.15.0 name: mongodbsearch.mongodb.com spec: group: mongodb.com @@ -49,6 +49,8 @@ spec: spec: properties: persistence: + description: Configure MongoDB Search's persistent volume. If not + defined, the operator will request 10GB of storage. properties: multiple: properties: @@ -95,66 +97,60 @@ spec: type: object type: object resourceRequirements: - description: ResourceRequirements describes the compute resource requirements. + $ref: '#/definitions/k8s.io~1api~1core~1v1~0ResourceRequirements' + description: Configure resource requests and limits for the MongoDB + Search pods. + security: + description: Configure security settings of the MongoDB Search server + that MongoDB database is connecting to when performing search queries. properties: - claims: - description: |- - Claims lists the names of resources, defined in spec.resourceClaims, - that are used by this container. - - This is an alpha field and requires enabling the - DynamicResourceAllocation feature gate. - - This field is immutable. It can only be set for containers. - items: - description: ResourceClaim references one entry in PodSpec.ResourceClaims. - properties: - name: - description: |- - Name must match the name of one entry in pod.spec.resourceClaims of - the Pod where this field is used. It makes that resource available - inside a container. - type: string - request: - description: |- - Request is the name chosen for a request in the referenced claim. - If empty, everything from the claim is made available, otherwise - only the result of this request. - type: string - required: - - name - type: object - type: array - x-kubernetes-list-map-keys: - - name - x-kubernetes-list-type: map - limits: - additionalProperties: - anyOf: - - type: integer - - type: string - pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ - x-kubernetes-int-or-string: true - description: |- - Limits describes the maximum amount of compute resources allowed. - More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ - type: object - requests: - additionalProperties: - anyOf: - - type: integer - - type: string - pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ - x-kubernetes-int-or-string: true - description: |- - Requests describes the minimum amount of compute resources required. - If Requests is omitted for a container, it defaults to Limits if that is explicitly specified, - otherwise to an implementation-defined value. Requests cannot exceed Limits. - More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + tls: + properties: + certificateKeySecretRef: + $ref: '#/definitions/k8s.io~1api~1core~1v1~0LocalObjectReference' + description: |- + CertificateKeySecret is a reference to a Secret containing a private key and certificate to use for TLS. + The key and cert are expected to be PEM encoded and available at "tls.key" and "tls.crt". + This is the same format used for the standard "kubernetes.io/tls" Secret type, but no specific type is required. + Alternatively, an entry tls.pem, containing the concatenation of cert and key, can be provided. + If all of tls.pem, tls.crt and tls.key are present, the tls.pem one needs to be equal to the concatenation of tls.crt and tls.key + enabled: + type: boolean + required: + - enabled type: object type: object source: + description: MongoDB database connection details from which MongoDB + Search will synchronize data to build indexes. properties: + external: + properties: + hostAndPorts: + items: + type: string + type: array + keyFileSecretRef: + description: mongod keyfile used to connect to the external + MongoDB deployment + properties: + key: + type: string + name: + type: string + required: + - name + type: object + tls: + properties: + ca: + $ref: '#/definitions/k8s.io~1api~1core~1v1~0LocalObjectReference' + enabled: + type: boolean + required: + - enabled + type: object + type: object mongodbResourceRef: properties: name: @@ -164,11 +160,26 @@ spec: required: - name type: object + passwordSecretRef: + description: |- + SecretKeyRef is a reference to a value in a given secret in the same + namespace. Based on: + https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.15/#secretkeyselector-v1-core + properties: + key: + type: string + name: + type: string + required: + - name + type: object + username: + type: string type: object statefulSet: description: |- - StatefulSetConfiguration holds the optional custom StatefulSet - that should be merged into the operator created one. + StatefulSetSpec which the operator will apply to the MongoDB Search StatefulSet at the end of the reconcile loop. Use to provide necessary customizations, + which aren't exposed as fields in the MongoDBSearch.spec. properties: metadata: description: StatefulSetMetadataWrapper is a wrapper around Labels @@ -190,6 +201,9 @@ spec: - spec type: object version: + description: Optional version of MongoDB Search component (mongot). + If not set, then the operator will set the most appropriate version + of MongoDB Search. type: string type: object status: diff --git a/config/crd/bases/mongodb.com_mongodbusers.yaml b/config/crd/bases/mongodb.com_mongodbusers.yaml index 89713ce7f..a81f0d449 100644 --- a/config/crd/bases/mongodb.com_mongodbusers.yaml +++ b/config/crd/bases/mongodb.com_mongodbusers.yaml @@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.18.0 + controller-gen.kubebuilder.io/version: v0.15.0 name: mongodbusers.mongodb.com spec: group: mongodb.com diff --git a/config/crd/bases/mongodb.com_opsmanagers.yaml b/config/crd/bases/mongodb.com_opsmanagers.yaml index 3ace001da..c830b9a24 100644 --- a/config/crd/bases/mongodb.com_opsmanagers.yaml +++ b/config/crd/bases/mongodb.com_opsmanagers.yaml @@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.18.0 + controller-gen.kubebuilder.io/version: v0.15.0 name: opsmanagers.mongodb.com spec: group: mongodb.com @@ -730,30 +730,7 @@ spec: automationLdapGroupDN: type: string automationPasswordSecretRef: - description: SecretKeySelector selects a key of a - Secret. - properties: - key: - description: The key of the secret to select from. Must - be a valid secret key. - type: string - 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 - optional: - description: Specify whether the Secret or its - key must be defined - type: boolean - required: - - key - type: object - x-kubernetes-map-type: atomic + $ref: '#/definitions/k8s.io~1api~1core~1v1~0SecretKeySelector' automationUserName: type: string clientCertificateSecretRef: @@ -789,29 +766,9 @@ spec: bindQueryUser: type: string caConfigMapRef: + $ref: '#/definitions/k8s.io~1api~1core~1v1~0ConfigMapKeySelector' description: Allows to point at a ConfigMap/key with a CA file to mount on the Pod - properties: - key: - description: The key to select. - type: string - 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 - optional: - description: Specify whether the ConfigMap or - its key must be defined - type: boolean - required: - - key - type: object - x-kubernetes-map-type: atomic servers: items: type: string @@ -1236,29 +1193,7 @@ spec: CustomCertificateSecretRefs is a list of valid Certificate Authority certificate secrets that apply to the associated S3 bucket. items: - description: SecretKeySelector selects a key of a Secret. - properties: - key: - description: The key of the secret to select from. Must - be a valid secret key. - type: string - 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 - optional: - description: Specify whether the Secret or its key - must be defined - type: boolean - required: - - key - type: object - x-kubernetes-map-type: atomic + $ref: '#/definitions/k8s.io~1api~1core~1v1~0SecretKeySelector' type: array irsaEnabled: description: |- @@ -1328,29 +1263,7 @@ spec: CustomCertificateSecretRefs is a list of valid Certificate Authority certificate secrets that apply to the associated S3 bucket. items: - description: SecretKeySelector selects a key of a Secret. - properties: - key: - description: The key of the secret to select from. Must - be a valid secret key. - type: string - 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 - optional: - description: Specify whether the Secret or its key - must be defined - type: boolean - required: - - key - type: object - x-kubernetes-map-type: atomic + $ref: '#/definitions/k8s.io~1api~1core~1v1~0SecretKeySelector' type: array irsaEnabled: description: |- @@ -1495,8 +1408,6 @@ spec: required: - spec type: object - required: - - members type: object clusterDomain: description: Cluster domain to override the default *.svc.cluster.local @@ -1536,13 +1447,13 @@ spec: Service when creating a ClusterIP type Service type: string externalTrafficPolicy: + $ref: '#/definitions/k8s.io~1api~1core~1v1~0ServiceExternalTrafficPolicy' description: |- ExternalTrafficPolicy mechanism to preserve the client source IP. Only supported on GCE and Google Kubernetes Engine. enum: - Cluster - Local - type: string loadBalancerIP: description: LoadBalancerIP IP that will be assigned to this LoadBalancer. @@ -1553,12 +1464,12 @@ spec: format: int32 type: integer type: + $ref: '#/definitions/k8s.io~1api~1core~1v1~0ServiceType' description: Type of the `Service` to be created. enum: - LoadBalancer - NodePort - ClusterIP - type: string required: - type type: object @@ -1600,7 +1511,6 @@ spec: - spec type: object required: - - clusterName - members type: object type: array @@ -1626,13 +1536,13 @@ spec: when creating a ClusterIP type Service type: string externalTrafficPolicy: + $ref: '#/definitions/k8s.io~1api~1core~1v1~0ServiceExternalTrafficPolicy' description: |- ExternalTrafficPolicy mechanism to preserve the client source IP. Only supported on GCE and Google Kubernetes Engine. enum: - Cluster - Local - type: string loadBalancerIP: description: LoadBalancerIP IP that will be assigned to this LoadBalancer. type: string @@ -1642,12 +1552,12 @@ spec: format: int32 type: integer type: + $ref: '#/definitions/k8s.io~1api~1core~1v1~0ServiceType' description: Type of the `Service` to be created. enum: - LoadBalancer - NodePort - ClusterIP - type: string required: - type type: object @@ -1667,13 +1577,13 @@ spec: when creating a ClusterIP type Service type: string externalTrafficPolicy: + $ref: '#/definitions/k8s.io~1api~1core~1v1~0ServiceExternalTrafficPolicy' description: |- ExternalTrafficPolicy mechanism to preserve the client source IP. Only supported on GCE and Google Kubernetes Engine. enum: - Cluster - Local - type: string loadBalancerIP: description: LoadBalancerIP IP that will be assigned to this LoadBalancer. type: string @@ -1683,12 +1593,12 @@ spec: format: int32 type: integer type: + $ref: '#/definitions/k8s.io~1api~1core~1v1~0ServiceType' description: Type of the `Service` to be created. enum: - LoadBalancer - NodePort - ClusterIP - type: string required: - type type: object diff --git a/config/crd/bases/mongodbcommunity.mongodb.com_mongodbcommunity.yaml b/config/crd/bases/mongodbcommunity.mongodb.com_mongodbcommunity.yaml index 36d5c892d..a0004e22e 100644 --- a/config/crd/bases/mongodbcommunity.mongodb.com_mongodbcommunity.yaml +++ b/config/crd/bases/mongodbcommunity.mongodb.com_mongodbcommunity.yaml @@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.18.0 + controller-gen.kubebuilder.io/version: v0.15.0 service.binding: path={.metadata.name}-{.spec.users[0].db}-{.spec.users[0].name},objectType=Secret service.binding/connectionString: path={.metadata.name}-{.spec.users[0].db}-{.spec.users[0].name},objectType=Secret,sourceKey=connectionString.standardSrv service.binding/password: path={.metadata.name}-{.spec.users[0].db}-{.spec.users[0].name},objectType=Secret,sourceKey=password @@ -330,24 +330,13 @@ spec: authentication: properties: agentCertificateSecretRef: + $ref: '#/definitions/k8s.io~1api~1core~1v1~0LocalObjectReference' description: |- AgentCertificateSecret is a reference to a Secret containing the certificate and the key for the automation agent The secret needs to have available: - certificate under key: "tls.crt" - private key under key: "tls.key" If additionally, tls.pem is present, then it needs to be equal to the concatenation of tls.crt and tls.key - properties: - name: - default: "" - description: |- - Name of the referent. - This field is effectively required, but due to backwards compatibility is - allowed to be empty. Instances of this type with an empty value here are - almost certainly wrong. - More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names - type: string - type: object - x-kubernetes-map-type: atomic agentMode: description: AgentMode contains the authentication mode used by the automation agent. @@ -466,57 +455,24 @@ spec: communication properties: caCertificateSecretRef: + $ref: '#/definitions/k8s.io~1api~1core~1v1~0LocalObjectReference' description: |- CaCertificateSecret is a reference to a Secret containing the certificate for the CA which signed the server certificates The certificate is expected to be available under the key "ca.crt" - 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 caConfigMapRef: + $ref: '#/definitions/k8s.io~1api~1core~1v1~0LocalObjectReference' description: |- CaConfigMap is a reference to a ConfigMap containing the certificate for the CA which signed the server certificates The certificate is expected to be available under the key "ca.crt" This field is ignored when CaCertificateSecretRef is configured - 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 certificateKeySecretRef: + $ref: '#/definitions/k8s.io~1api~1core~1v1~0LocalObjectReference' description: |- CertificateKeySecret is a reference to a Secret containing a private key and certificate to use for TLS. The key and cert are expected to be PEM encoded and available at "tls.key" and "tls.crt". This is the same format used for the standard "kubernetes.io/tls" Secret type, but no specific type is required. Alternatively, an entry tls.pem, containing the concatenation of cert and key, can be provided. If all of tls.pem, tls.crt and tls.key are present, the tls.pem one needs to be equal to the concatenation of tls.crt and tls.key - properties: - name: - default: "" - description: |- - Name of the referent. - This field is effectively required, but due to backwards compatibility is - allowed to be empty. Instances of this type with an empty value here are - almost certainly wrong. - More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names - type: string - type: object - x-kubernetes-map-type: atomic enabled: type: boolean optional: diff --git a/controllers/operator/mongodbreplicaset_controller.go b/controllers/operator/mongodbreplicaset_controller.go index bf6b2d2b7..fad1ec6b4 100644 --- a/controllers/operator/mongodbreplicaset_controller.go +++ b/controllers/operator/mongodbreplicaset_controller.go @@ -4,12 +4,16 @@ import ( "context" "fmt" + "github.com/blang/semver" "go.uber.org/zap" "golang.org/x/xerrors" "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/fields" "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/controller" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" "sigs.k8s.io/controller-runtime/pkg/event" "sigs.k8s.io/controller-runtime/pkg/handler" "sigs.k8s.io/controller-runtime/pkg/manager" @@ -22,6 +26,7 @@ import ( mdbv1 "github.com/mongodb/mongodb-kubernetes/api/v1/mdb" rolev1 "github.com/mongodb/mongodb-kubernetes/api/v1/role" + searchv1 "github.com/mongodb/mongodb-kubernetes/api/v1/search" mdbstatus "github.com/mongodb/mongodb-kubernetes/api/v1/status" "github.com/mongodb/mongodb-kubernetes/controllers/om" "github.com/mongodb/mongodb-kubernetes/controllers/om/backup" @@ -39,6 +44,7 @@ import ( "github.com/mongodb/mongodb-kubernetes/controllers/operator/recovery" "github.com/mongodb/mongodb-kubernetes/controllers/operator/watch" "github.com/mongodb/mongodb-kubernetes/controllers/operator/workflow" + "github.com/mongodb/mongodb-kubernetes/controllers/search_controller" mcoConstruct "github.com/mongodb/mongodb-kubernetes/mongodb-community-operator/controllers/construct" "github.com/mongodb/mongodb-kubernetes/mongodb-community-operator/pkg/kube/annotations" "github.com/mongodb/mongodb-kubernetes/mongodb-community-operator/pkg/kube/configmap" @@ -52,6 +58,7 @@ import ( "github.com/mongodb/mongodb-kubernetes/pkg/util/architectures" "github.com/mongodb/mongodb-kubernetes/pkg/util/env" util_int "github.com/mongodb/mongodb-kubernetes/pkg/util/int" + "github.com/mongodb/mongodb-kubernetes/pkg/util/maputil" "github.com/mongodb/mongodb-kubernetes/pkg/vault" "github.com/mongodb/mongodb-kubernetes/pkg/vault/vaultwatcher" ) @@ -222,6 +229,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) @@ -251,7 +260,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, tlsCertPath, internalClusterCertPath, agentCertSecretSelector, prometheusCertHash, true).OnErrorPrepend("Failed to create/update (Ops Manager reconciliation phase):") + automationConfigStatus := r.updateOmDeploymentRs(ctx, conn, rs.Status.Members, rs, sts, log, caFilePath, tlsCertPath, internalClusterCertPath, agentCertSecretSelector, prometheusCertHash, true, shouldMirrorKeyfile).OnErrorPrepend("Failed to create/update (Ops Manager reconciliation phase):") deploymentError := create.DatabaseInKubernetes(ctx, r.client, *rs, sts, rsConfig, log) if deploymentError != nil { log.Errorf("Recovery failed because of deployment errors, %w", deploymentError) @@ -267,7 +276,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, tlsCertPath, internalClusterCertPath, agentCertSecretSelector, prometheusCertHash, false).OnErrorPrepend("Failed to create/update (Ops Manager reconciliation phase):") + return r.updateOmDeploymentRs(ctx, conn, rs.Status.Members, rs, sts, log, caFilePath, tlsCertPath, internalClusterCertPath, agentCertSecretSelector, prometheusCertHash, false, shouldMirrorKeyfile).OnErrorPrepend("Failed to create/update (Ops Manager reconciliation phase):") }, func() workflow.Status { workflowStatus := create.HandlePVCResize(ctx, r.client, &sts, log) @@ -421,6 +430,19 @@ func AddReplicaSetController(ctx context.Context, mgr manager.Manager, imageUrls zap.S().Errorf("Failed to watch for vault secret changes: %w", err) } } + + err = c.Watch(source.Kind(mgr.GetCache(), &searchv1.MongoDBSearch{}, + handler.TypedEnqueueRequestsFromMapFunc(func(ctx context.Context, search *searchv1.MongoDBSearch) []reconcile.Request { + source := search.GetMongoDBResourceRef() + if source == nil { + return []reconcile.Request{} + } + return []reconcile.Request{{NamespacedName: types.NamespacedName{Namespace: source.Namespace, Name: source.Name}}} + }))) + if err != nil { + return err + } + zap.S().Infof("Registered controller %s", util.MongoDbReplicaSetController) return nil @@ -428,7 +450,7 @@ func AddReplicaSetController(ctx context.Context, mgr manager.Manager, imageUrls // updateOmDeploymentRs performs OM registration operation for the replicaset. So the changes will be finally propagated // to automation agents in containers -func (r *ReconcileMongoDbReplicaSet) updateOmDeploymentRs(ctx context.Context, conn om.Connection, membersNumberBefore int, rs *mdbv1.MongoDB, set appsv1.StatefulSet, log *zap.SugaredLogger, caFilePath, tlsCertPath, internalClusterCertPath string, agentCertSecretSelector corev1.SecretKeySelector, prometheusCertHash string, isRecovering bool) workflow.Status { +func (r *ReconcileMongoDbReplicaSet) updateOmDeploymentRs(ctx context.Context, conn om.Connection, membersNumberBefore int, rs *mdbv1.MongoDB, set appsv1.StatefulSet, log *zap.SugaredLogger, caFilePath, tlsCertPath, internalClusterCertPath string, agentCertSecretSelector corev1.SecretKeySelector, prometheusCertHash string, isRecovering bool, shouldMirrorKeyfileForMongot bool) workflow.Status { log.Debug("Entering UpdateOMDeployments") // Only "concrete" RS members should be observed // - if scaling down, let's observe only members that will remain after scale-down operation @@ -477,6 +499,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, internalClusterCertPath, &p, log) }, log, @@ -617,3 +644,70 @@ func getAllHostsRs(set appsv1.StatefulSet, clusterName string, membersCount int, hostnames, _ := dns.GetDnsForStatefulSetReplicasSpecified(set, clusterName, membersCount, externalDomain) return hostnames } + +func (r *ReconcileMongoDbReplicaSet) applySearchOverrides(ctx context.Context, rs *mdbv1.MongoDB, log *zap.SugaredLogger) bool { + search := r.lookupCorrespondingSearchResource(ctx, rs, log) + if search == nil { + log.Debugf("No MongoDBSearch resource found, skipping search overrides") + return false + } + + log.Infof("Applying search overrides from MongoDBSearch %s", search.NamespacedName()) + + if rs.Spec.AdditionalMongodConfig == nil { + rs.Spec.AdditionalMongodConfig = mdbv1.NewEmptyAdditionalMongodConfig() + } + searchMongodConfig := search_controller.GetMongodConfigParameters(search) + rs.Spec.AdditionalMongodConfig.AddOption("setParameter", searchMongodConfig["setParameter"]) + + mdbVersion, err := semver.ParseTolerant(rs.Spec.Version) + if err != nil { + log.Warnf("Failed to parse MongoDB version %q: %w. Proceeding without the automatic creation of the searchCoordinator role that's necessary for MongoDB <8.2", rs.Spec.Version, err) + } else if semver.MustParse("8.2.0").GT(mdbVersion) { + log.Infof("Polyfilling the searchCoordinator role for MongoDB %s", rs.Spec.Version) + + if rs.Spec.Security == nil { + rs.Spec.Security = &mdbv1.Security{} + } + rs.Spec.Security.Roles = append(rs.Spec.Security.Roles, search_controller.SearchCoordinatorRole()) + } + + return true +} + +func (r *ReconcileMongoDbReplicaSet) mirrorKeyfileIntoSecretForMongot(ctx context.Context, d om.Deployment, rs *mdbv1.MongoDB, log *zap.SugaredLogger) error { + keyfileContents := maputil.ReadMapValueAsString(d, "auth", "key") + keyfileSecret := &corev1.Secret{ObjectMeta: metav1.ObjectMeta{Name: fmt.Sprintf("%s-keyfile", rs.Name), Namespace: rs.Namespace}} + + log.Infof("Mirroring the replicaset %s's keyfile into the secret %s", rs.ObjectKey(), kube.ObjectKeyFromApiObject(keyfileSecret)) + + _, err := controllerutil.CreateOrUpdate(ctx, r.client, keyfileSecret, func() error { + keyfileSecret.StringData = map[string]string{"keyfile": keyfileContents} + return controllerutil.SetOwnerReference(rs, keyfileSecret, r.client.Scheme()) + }) + if err != nil { + return xerrors.Errorf("Failed to mirror the replicaset's keyfile into a secret: %w", err) + } else { + return nil + } +} + +func (r *ReconcileMongoDbReplicaSet) lookupCorrespondingSearchResource(ctx context.Context, rs *mdbv1.MongoDB, log *zap.SugaredLogger) *searchv1.MongoDBSearch { + var search *searchv1.MongoDBSearch + searchList := &searchv1.MongoDBSearchList{} + if err := r.client.List(ctx, searchList, &client.ListOptions{ + FieldSelector: fields.OneTermEqualSelector(search_controller.MongoDBSearchIndexFieldName, rs.GetNamespace()+"/"+rs.GetName()), + }); err != nil { + log.Debugf("Failed to list MongoDBSearch resources: %v", err) + } + // this validates that there is exactly one MongoDBSearch pointing to this resource, + // and that this resource passes search validations. If either fails, proceed without a search target + // for the mongod automation config. + if len(searchList.Items) == 1 { + searchSource := search_controller.NewEnterpriseResourceSearchSource(rs) + if searchSource.Validate() == nil { + search = &searchList.Items[0] + } + } + return search +} diff --git a/controllers/operator/mongodbsearch_controller.go b/controllers/operator/mongodbsearch_controller.go index 63ed00a5f..a25efed3c 100644 --- a/controllers/operator/mongodbsearch_controller.go +++ b/controllers/operator/mongodbsearch_controller.go @@ -13,12 +13,15 @@ import ( "sigs.k8s.io/controller-runtime/pkg/reconcile" appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" ctrl "sigs.k8s.io/controller-runtime" + mdbv1 "github.com/mongodb/mongodb-kubernetes/api/v1/mdb" searchv1 "github.com/mongodb/mongodb-kubernetes/api/v1/search" + "github.com/mongodb/mongodb-kubernetes/controllers/operator/watch" "github.com/mongodb/mongodb-kubernetes/controllers/search_controller" mdbcv1 "github.com/mongodb/mongodb-kubernetes/mongodb-community-operator/api/v1" - "github.com/mongodb/mongodb-kubernetes/mongodb-community-operator/controllers/watch" kubernetesClient "github.com/mongodb/mongodb-kubernetes/mongodb-community-operator/pkg/kube/client" "github.com/mongodb/mongodb-kubernetes/pkg/kube/commoncontroller" "github.com/mongodb/mongodb-kubernetes/pkg/util" @@ -27,15 +30,14 @@ import ( type MongoDBSearchReconciler struct { kubeClient kubernetesClient.Client - mdbcWatcher *watch.ResourceWatcher + watch *watch.ResourceWatcher operatorSearchConfig search_controller.OperatorSearchConfig } func newMongoDBSearchReconciler(client client.Client, operatorSearchConfig search_controller.OperatorSearchConfig) *MongoDBSearchReconciler { - mdbcWatcher := watch.New() return &MongoDBSearchReconciler{ kubeClient: kubernetesClient.NewClient(client), - mdbcWatcher: &mdbcWatcher, + watch: watch.NewResourceWatcher(), operatorSearchConfig: operatorSearchConfig, } } @@ -50,31 +52,77 @@ func (r *MongoDBSearchReconciler) Reconcile(ctx context.Context, request reconci return result, err } - sourceResource, 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 } - r.mdbcWatcher.Watch(ctx, sourceResource.NamespacedName(), request.NamespacedName) + r.watch.AddWatchedResourceIfNotAdded(searchSource.KeyfileSecretName(), mdbSearch.Namespace, watch.Secret, mdbSearch.NamespacedName()) - reconcileHelper := search_controller.NewMongoDBSearchReconcileHelper(kubernetesClient.NewClient(r.kubeClient), mdbSearch, sourceResource, r.operatorSearchConfig) + // Watch for changes in database source CA certificate secrets or configmaps + tlsSourceConfig := searchSource.TLSConfig() + if tlsSourceConfig != nil { + for wType, resources := range tlsSourceConfig.ResourcesToWatch { + for _, resource := range resources { + r.watch.AddWatchedResourceIfNotAdded(resource.Name, resource.Namespace, wType, mdbSearch.NamespacedName()) + } + } + } + + // Watch our own TLS certificate secret for changes + if mdbSearch.Spec.Security.TLS.Enabled { + r.watch.AddWatchedResourceIfNotAdded(mdbSearch.Spec.Security.TLS.CertificateKeySecret.Name, mdbSearch.Namespace, watch.Secret, mdbSearch.NamespacedName()) + } + + reconcileHelper := search_controller.NewMongoDBSearchReconcileHelper(kubernetesClient.NewClient(r.kubeClient), mdbSearch, searchSource, r.operatorSearchConfig) return reconcileHelper.Reconcile(ctx, log).ReconcileResult() } -func getSourceMongoDBForSearch(ctx context.Context, kubeClient client.Client, search *searchv1.MongoDBSearch) (search_controller.SearchSourceDBResource, 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.NewExternalSearchSource(search.Namespace, search.Spec.Source.ExternalMongoDBSource), nil + } + sourceMongoDBResourceRef := search.GetMongoDBResourceRef() - mdbcName := types.NamespacedName{Namespace: search.GetNamespace(), Name: sourceMongoDBResourceRef.Name} + if sourceMongoDBResourceRef == nil { + return nil, xerrors.New("MongoDBSearch source MongoDB resource reference is not set") + } + + sourceName := types.NamespacedName{Namespace: search.GetNamespace(), Name: sourceMongoDBResourceRef.Name} + log.Infof("Looking up Search source %s", sourceName) + + mdb := &mdbv1.MongoDB{} + if err := kubeClient.Get(ctx, sourceName, mdb); err != nil { + if !apierrors.IsNotFound(err) { + return nil, xerrors.Errorf("error getting MongoDB %s: %w", sourceName, err) + } + } else { + r.watch.AddWatchedResourceIfNotAdded(sourceMongoDBResourceRef.Name, sourceMongoDBResourceRef.Namespace, watch.MongoDB, search.NamespacedName()) + return search_controller.NewEnterpriseResourceSearchSource(mdb), nil + } + mdbc := &mdbcv1.MongoDBCommunity{} - if err := kubeClient.Get(ctx, mdbcName, mdbc); err != nil { - return nil, xerrors.Errorf("error getting MongoDBCommunity %s", mdbcName) + if err := kubeClient.Get(ctx, sourceName, mdbc); err != nil { + if !apierrors.IsNotFound(err) { + return nil, xerrors.Errorf("error getting MongoDBCommunity %s: %w", sourceName, err) + } + } else { + r.watch.AddWatchedResourceIfNotAdded(sourceMongoDBResourceRef.Name, sourceMongoDBResourceRef.Namespace, "MongoDBCommunity", search.NamespacedName()) + return search_controller.NewCommunityResourceSearchSource(mdbc), nil } - return search_controller.NewSearchSourceDBResourceFromMongoDBCommunity(mdbc), nil + + return nil, xerrors.Errorf("No database resource named %s found", sourceName) } func mdbcSearchIndexBuilder(rawObj client.Object) []string { mdbSearch := rawObj.(*searchv1.MongoDBSearch) - return []string{mdbSearch.GetMongoDBResourceRef().Namespace + "/" + mdbSearch.GetMongoDBResourceRef().Name} + resourceRef := mdbSearch.GetMongoDBResourceRef() + if resourceRef == nil { + return []string{} + } + + return []string{resourceRef.Namespace + "/" + resourceRef.Name} } func AddMongoDBSearchController(ctx context.Context, mgr manager.Manager, operatorSearchConfig search_controller.OperatorSearchConfig) error { @@ -87,7 +135,11 @@ func AddMongoDBSearchController(ctx context.Context, mgr manager.Manager, operat return ctrl.NewControllerManagedBy(mgr). WithOptions(controller.Options{MaxConcurrentReconciles: env.ReadIntOrDefault(util.MaxConcurrentReconcilesEnv, 1)}). // nolint:forbidigo For(&searchv1.MongoDBSearch{}). - Watches(&mdbcv1.MongoDBCommunity{}, r.mdbcWatcher). + Watches(&mdbv1.MongoDB{}, &watch.ResourcesHandler{ResourceType: watch.MongoDB, ResourceWatcher: r.watch}). + Watches(&mdbcv1.MongoDBCommunity{}, &watch.ResourcesHandler{ResourceType: "MongoDBCommunity", ResourceWatcher: r.watch}). + Watches(&corev1.Secret{}, &watch.ResourcesHandler{ResourceType: watch.Secret, ResourceWatcher: r.watch}). + Watches(&corev1.ConfigMap{}, &watch.ResourcesHandler{ResourceType: watch.ConfigMap, ResourceWatcher: r.watch}). Owns(&appsv1.StatefulSet{}). + Owns(&corev1.Secret{}). Complete(r) } diff --git a/controllers/operator/mongodbsearch_controller_test.go b/controllers/operator/mongodbsearch_controller_test.go index 0c7ddcce5..28f966ff8 100644 --- a/controllers/operator/mongodbsearch_controller_test.go +++ b/controllers/operator/mongodbsearch_controller_test.go @@ -8,9 +8,8 @@ import ( "github.com/ghodss/yaml" "github.com/stretchr/testify/assert" "k8s.io/apimachinery/pkg/types" - "k8s.io/client-go/util/workqueue" + "k8s.io/utils/ptr" "sigs.k8s.io/controller-runtime/pkg/client" - "sigs.k8s.io/controller-runtime/pkg/event" "sigs.k8s.io/controller-runtime/pkg/reconcile" appsv1 "k8s.io/api/apps/v1" @@ -25,7 +24,9 @@ import ( "github.com/mongodb/mongodb-kubernetes/controllers/operator/workflow" "github.com/mongodb/mongodb-kubernetes/controllers/search_controller" mdbcv1 "github.com/mongodb/mongodb-kubernetes/mongodb-community-operator/api/v1" + "github.com/mongodb/mongodb-kubernetes/mongodb-community-operator/api/v1/common" "github.com/mongodb/mongodb-kubernetes/mongodb-community-operator/pkg/mongot" + "github.com/mongodb/mongodb-kubernetes/mongodb-community-operator/pkg/util/constants" ) func newMongoDBCommunity(name, namespace string) *mdbcv1.MongoDBCommunity { @@ -34,7 +35,7 @@ func newMongoDBCommunity(name, namespace string) *mdbcv1.MongoDBCommunity { Spec: mdbcv1.MongoDBCommunitySpec{ Type: mdbcv1.ReplicaSet, Members: 1, - Version: "8.0", + Version: "8.0.10", }, } } @@ -50,15 +51,25 @@ func newMongoDBSearch(name, namespace, mdbcName string) *searchv1.MongoDBSearch } } -func newSearchReconciler( +func newSearchReconcilerWithOperatorConfig( mdbc *mdbcv1.MongoDBCommunity, + operatorConfig search_controller.OperatorSearchConfig, searches ...*searchv1.MongoDBSearch, ) (*MongoDBSearchReconciler, client.Client) { builder := mock.NewEmptyFakeClientBuilder() builder.WithIndex(&searchv1.MongoDBSearch{}, search_controller.MongoDBSearchIndexFieldName, mdbcSearchIndexBuilder) if mdbc != nil { - builder.WithObjects(mdbc) + keyfileSecret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: mdbc.GetAgentKeyfileSecretNamespacedName().Name, + Namespace: mdbc.Namespace, + }, + StringData: map[string]string{ + constants.AgentKeyfileKey: "keyfile", + }, + } + builder.WithObjects(mdbc, keyfileSecret) } for _, search := range searches { @@ -68,25 +79,58 @@ func newSearchReconciler( } fakeClient := builder.Build() - return newMongoDBSearchReconciler(fakeClient, search_controller.OperatorSearchConfig{}), fakeClient + + return newMongoDBSearchReconciler(fakeClient, operatorConfig), fakeClient +} + +func newSearchReconciler( + mdbc *mdbcv1.MongoDBCommunity, + searches ...*searchv1.MongoDBSearch, +) (*MongoDBSearchReconciler, client.Client) { + return newSearchReconcilerWithOperatorConfig(mdbc, search_controller.OperatorSearchConfig{}, searches...) } func buildExpectedMongotConfig(search *searchv1.MongoDBSearch, mdbc *mdbcv1.MongoDBCommunity) mongot.Config { - return mongot.Config{CommunityPrivatePreview: mongot.CommunityPrivatePreview{ - MongodHostAndPort: fmt.Sprintf( - "%s.%s.svc.cluster.local:%d", - mdbc.ServiceName(), mdbc.Namespace, - mdbc.GetMongodConfiguration().GetDBPort(), - ), - QueryServerAddress: fmt.Sprintf("localhost:%d", search.GetMongotPort()), - KeyFilePath: "/mongot/keyfile/keyfile", - DataPath: "/mongot/data/config.yml", - Metrics: mongot.Metrics{ + var hostAndPorts []string + for i := range mdbc.Spec.Members { + hostAndPorts = append(hostAndPorts, fmt.Sprintf("%s-%d.%s.%s.svc.cluster.local:%d", mdbc.Name, i, mdbc.Name+"-svc", search.Namespace, 27017)) + } + return mongot.Config{ + SyncSource: mongot.ConfigSyncSource{ + ReplicaSet: mongot.ConfigReplicaSet{ + HostAndPort: hostAndPorts, + Username: searchv1.MongotDefaultSyncSourceUsername, + PasswordFile: "/tmp/sourceUserPassword", + TLS: ptr.To(false), + ReadPreference: ptr.To("secondaryPreferred"), + AuthSource: ptr.To("admin"), + }, + }, + Storage: mongot.ConfigStorage{ + DataPath: "/mongot/data/config.yml", + }, + Server: mongot.ConfigServer{ + Wireproto: &mongot.ConfigWireproto{ + Address: "0.0.0.0:27027", + Authentication: &mongot.ConfigAuthentication{ + Mode: "keyfile", + KeyFile: "/tmp/keyfile", + }, + TLS: mongot.ConfigTLS{Mode: mongot.ConfigTLSModeDisabled}, + }, + }, + Metrics: mongot.ConfigMetrics{ Enabled: true, - Address: fmt.Sprintf("localhost:%d", search.GetMongotMetricsPort()), + Address: fmt.Sprintf("0.0.0.0:%d", search.GetMongotMetricsPort()), + }, + HealthCheck: mongot.ConfigHealthCheck{ + Address: fmt.Sprintf("0.0.0.0:%d", search.GetMongotHealthCheckPort()), + }, + Logging: mongot.ConfigLogging{ + Verbosity: "TRACE", + LogPath: nil, }, - Logging: mongot.Logging{Verbosity: "DEBUG"}, - }} + } } func TestMongoDBSearchReconcile_NotFound(t *testing.T) { @@ -146,10 +190,6 @@ func TestMongoDBSearchReconcile_Success(t *testing.T) { sts := &appsv1.StatefulSet{} err = c.Get(ctx, search.StatefulSetNamespacedName(), sts) assert.NoError(t, err) - - queue := workqueue.NewTypedRateLimitingQueue(workqueue.DefaultTypedControllerRateLimiter[reconcile.Request]()) - reconciler.mdbcWatcher.Create(ctx, event.CreateEvent{Object: mdbc}, queue) - assert.Equal(t, 1, queue.Len()) } func checkSearchReconcileFailed( @@ -183,16 +223,6 @@ func TestMongoDBSearchReconcile_InvalidVersion(t *testing.T) { checkSearchReconcileFailed(ctx, t, reconciler, c, search, "MongoDB version") } -func TestMongoDBSearchReconcile_TLSNotSupported(t *testing.T) { - ctx := context.Background() - search := newMongoDBSearch("search", mock.TestNamespace, "mdb") - mdbc := newMongoDBCommunity("mdb", mock.TestNamespace) - mdbc.Spec.Security.TLS.Enabled = true - reconciler, c := newSearchReconciler(mdbc, search) - - checkSearchReconcileFailed(ctx, t, reconciler, c, search, "TLS-enabled") -} - func TestMongoDBSearchReconcile_MultipleSearchResources(t *testing.T) { ctx := context.Background() search1 := newMongoDBSearch("search1", mock.TestNamespace, "mdb") @@ -202,3 +232,60 @@ func TestMongoDBSearchReconcile_MultipleSearchResources(t *testing.T) { checkSearchReconcileFailed(ctx, t, reconciler, c, search1, "multiple MongoDBSearch") } + +func TestMongoDBSearchReconcile_InvalidSearchImageVersion(t *testing.T) { + ctx := context.Background() + expectedMsg := "MongoDBSearch version 1.47.0 is not supported because of breaking changes. The operator will ignore this resource: it will not reconcile or reconfigure the workload. Existing deployments will continue to run, but cannot be managed by the operator. To regain operator management, you must delete and recreate the MongoDBSearch resource." + + tests := []struct { + name string + specVersion string + operatorVersion string + statefulSetConfig *common.StatefulSetConfiguration + }{ + { + name: "unsupported version in Spec.Version", + specVersion: "1.47.0", + }, + { + name: "unsupported version in operator config", + operatorVersion: "1.47.0", + }, + { + name: "unsupported version in StatefulSetConfiguration", + statefulSetConfig: &common.StatefulSetConfiguration{ + SpecWrapper: common.StatefulSetSpecWrapper{ + Spec: appsv1.StatefulSetSpec{ + Template: corev1.PodTemplateSpec{ + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + { + Name: search_controller.MongotContainerName, + Image: "testrepo/mongot:1.47.0", + }, + }, + }, + }, + }, + }, + }, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + search := newMongoDBSearch("search", mock.TestNamespace, "mdb") + mdbc := newMongoDBCommunity("mdb", mock.TestNamespace) + + search.Spec.Version = tc.specVersion + search.Spec.StatefulSetConfiguration = tc.statefulSetConfig + + operatorConfig := search_controller.OperatorSearchConfig{ + SearchVersion: tc.operatorVersion, + } + reconciler, _ := newSearchReconcilerWithOperatorConfig(mdbc, operatorConfig, search) + + checkSearchReconcileFailed(ctx, t, reconciler, reconciler.kubeClient, search, expectedMsg) + }) + } +} diff --git a/controllers/operator/watch/config_change_handler.go b/controllers/operator/watch/config_change_handler.go index 7b53ee4fe..77f725f20 100644 --- a/controllers/operator/watch/config_change_handler.go +++ b/controllers/operator/watch/config_change_handler.go @@ -14,8 +14,6 @@ import ( "sigs.k8s.io/controller-runtime/pkg/reconcile" corev1 "k8s.io/api/core/v1" - - rolev1 "github.com/mongodb/mongodb-kubernetes/api/v1/role" ) // Type is an enum for all kubernetes types watched by controller for changes for configuration @@ -87,10 +85,14 @@ func (c *ResourcesHandler) doHandle(namespace, name string, q workqueue.TypedRat // Seems we don't need to react on config map/secret removal.. func (c *ResourcesHandler) Delete(ctx context.Context, e event.TypedDeleteEvent[client.Object], q workqueue.TypedRateLimitingInterface[reconcile.Request]) { - switch v := e.Object.(type) { - case *rolev1.ClusterMongoDBRole: - c.doHandle(v.GetNamespace(), v.GetName(), q) + switch e.Object.(type) { + case *corev1.ConfigMap: + return + case *corev1.Secret: + return } + + c.doHandle(e.Object.GetNamespace(), e.Object.GetName(), q) } func (c *ResourcesHandler) Generic(context.Context, event.TypedGenericEvent[client.Object], workqueue.TypedRateLimitingInterface[reconcile.Request]) { diff --git a/controllers/search_controller/community_search_source.go b/controllers/search_controller/community_search_source.go new file mode 100644 index 000000000..8b10a1cbf --- /dev/null +++ b/controllers/search_controller/community_search_source.go @@ -0,0 +1,84 @@ +package search_controller + +import ( + "fmt" + "strings" + + "github.com/blang/semver" + "golang.org/x/xerrors" + "k8s.io/apimachinery/pkg/types" + + corev1 "k8s.io/api/core/v1" + + "github.com/mongodb/mongodb-kubernetes/controllers/operator/watch" + mdbcv1 "github.com/mongodb/mongodb-kubernetes/mongodb-community-operator/api/v1" + "github.com/mongodb/mongodb-kubernetes/pkg/statefulset" + "github.com/mongodb/mongodb-kubernetes/pkg/util" +) + +func NewCommunityResourceSearchSource(mdbc *mdbcv1.MongoDBCommunity) SearchSourceDBResource { + return &CommunitySearchSource{MongoDBCommunity: mdbc} +} + +type CommunitySearchSource struct { + *mdbcv1.MongoDBCommunity +} + +func (r *CommunitySearchSource) HostSeeds() []string { + seeds := make([]string, r.Spec.Members) + for i := range seeds { + seeds[i] = fmt.Sprintf("%s-%d.%s.%s.svc.cluster.local:%d", r.Name, i, r.ServiceName(), r.Namespace, r.GetMongodConfiguration().GetDBPort()) + } + return seeds +} + +func (r *CommunitySearchSource) KeyfileSecretName() string { + return r.MongoDBCommunity.GetAgentKeyfileSecretNamespacedName().Name +} + +func (r *CommunitySearchSource) TLSConfig() *TLSSourceConfig { + if !r.Spec.Security.TLS.Enabled { + return nil + } + + var volume corev1.Volume + watchedResources := make(map[watch.Type][]types.NamespacedName) + + if r.Spec.Security.TLS.CaCertificateSecret != nil { + volume = statefulset.CreateVolumeFromSecret("ca", r.Spec.Security.TLS.CaCertificateSecret.Name) + watchedResources[watch.Secret] = []types.NamespacedName{r.TLSCaCertificateSecretNamespacedName()} + } else { + volume = statefulset.CreateVolumeFromConfigMap("ca", r.Spec.Security.TLS.CaConfigMap.Name) + watchedResources[watch.ConfigMap] = []types.NamespacedName{r.TLSConfigMapNamespacedName()} + } + + return &TLSSourceConfig{ + CAFileName: "ca.crt", + CAVolume: volume, + ResourcesToWatch: watchedResources, + } +} + +func (r *CommunitySearchSource) Validate() error { + version, err := semver.ParseTolerant(r.GetMongoDBVersion()) + if err != nil { + return xerrors.Errorf("error parsing MongoDB version '%s': %w", r.Spec.Version, err) + } else if version.LT(semver.MustParse("8.0.10")) { + return xerrors.New("MongoDB version must be 8.0.10 or higher") + } + + foundScram := false + for _, authMode := range r.Spec.Security.Authentication.Modes { + // Check for SCRAM, SCRAM-SHA-1, or SCRAM-SHA-256 + if strings.HasPrefix(strings.ToUpper(string(authMode)), util.SCRAM) { + foundScram = true + break + } + } + + if !foundScram && len(r.Spec.Security.Authentication.Modes) > 0 { + return xerrors.New("MongoDBSearch requires SCRAM authentication to be enabled") + } + + return nil +} diff --git a/controllers/search_controller/community_search_source_test.go b/controllers/search_controller/community_search_source_test.go new file mode 100644 index 000000000..90bf9967e --- /dev/null +++ b/controllers/search_controller/community_search_source_test.go @@ -0,0 +1,208 @@ +package search_controller + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + mdbcv1 "github.com/mongodb/mongodb-kubernetes/mongodb-community-operator/api/v1" +) + +func newCommunitySearchSource(version string, authModes []mdbcv1.AuthMode) *CommunitySearchSource { + return &CommunitySearchSource{ + MongoDBCommunity: &mdbcv1.MongoDBCommunity{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-mongodb", + Namespace: "test-namespace", + }, + Spec: mdbcv1.MongoDBCommunitySpec{ + Version: version, + Security: mdbcv1.Security{ + Authentication: mdbcv1.Authentication{ + Modes: authModes, + }, + }, + }, + }, + } +} + +func TestCommunitySearchSource_Validate(t *testing.T) { + cases := []struct { + name string + version string + authModes []mdbcv1.AuthMode + expectError bool + expectedErrMsg string + }{ + // Version validation tests + { + name: "Invalid version", + version: "invalid.version", + authModes: []mdbcv1.AuthMode{"SCRAM-SHA-256"}, + expectError: true, + expectedErrMsg: "error parsing MongoDB version", + }, + { + name: "Version too old", + version: "7.0.0", + authModes: []mdbcv1.AuthMode{"SCRAM-SHA-256"}, + expectError: true, + expectedErrMsg: "MongoDB version must be 8.0.10 or higher", + }, + { + name: "Version just below minimum", + version: "8.0.9", + authModes: []mdbcv1.AuthMode{"SCRAM-SHA-256"}, + expectError: true, + expectedErrMsg: "MongoDB version must be 8.0.10 or higher", + }, + { + name: "Valid minimum version", + version: "8.0.10", + authModes: []mdbcv1.AuthMode{"SCRAM-SHA-256"}, + expectError: false, + }, + { + name: "Version above minimum", + version: "8.1.0", + authModes: []mdbcv1.AuthMode{"SCRAM-SHA-256"}, + expectError: false, + }, + { + name: "Version with build number", + version: "8.1.0-rc1", + authModes: []mdbcv1.AuthMode{"SCRAM-SHA-256"}, + expectError: false, + }, + // Authentication mode tests - empty/nil cases + { + name: "Empty auth modes", + version: "8.0.10", + authModes: []mdbcv1.AuthMode{}, + expectError: false, + }, + { + name: "Nil auth modes", + version: "8.0.10", + authModes: nil, + expectError: false, + }, + { + name: "X509 mode only", + version: "8.0.10", + authModes: []mdbcv1.AuthMode{"X509"}, + expectError: true, + expectedErrMsg: "MongoDBSearch requires SCRAM authentication to be enabled", + }, + { + name: "X509 and SCRAM", + version: "8.0.10", + authModes: []mdbcv1.AuthMode{"X509", "SCRAM-SHA-256"}, + expectError: false, + }, + { + name: "Multiple auth modes with SCRAM first", + version: "8.0.10", + authModes: []mdbcv1.AuthMode{"SCRAM-SHA-1", "X509"}, + expectError: false, + }, + { + name: "Multiple auth modes with SCRAM last", + version: "8.0.10", + authModes: []mdbcv1.AuthMode{"PLAIN", "X509", "SCRAM-SHA-256"}, + expectError: false, + }, + { + name: "Multiple non-SCRAM modes", + version: "8.0.10", + authModes: []mdbcv1.AuthMode{"PLAIN", "X509"}, + expectError: true, + expectedErrMsg: "MongoDBSearch requires SCRAM authentication to be enabled", + }, + // SCRAM variant tests + { + name: "SCRAM only", + version: "8.0.10", + authModes: []mdbcv1.AuthMode{"SCRAM"}, + expectError: false, + }, + { + name: "SCRAM-SHA-1 only", + version: "8.0.10", + authModes: []mdbcv1.AuthMode{"SCRAM-SHA-1"}, + expectError: false, + }, + { + name: "SCRAM-SHA-256 only", + version: "8.0.10", + authModes: []mdbcv1.AuthMode{"SCRAM-SHA-256"}, + expectError: false, + }, + { + name: "All SCRAM variants", + version: "8.0.10", + authModes: []mdbcv1.AuthMode{"SCRAM", "SCRAM-SHA-1", "SCRAM-SHA-256"}, + expectError: false, + }, + // Case-insensitive tests (now supported with ToUpper) + { + name: "Lowercase SCRAM", + version: "8.0.10", + authModes: []mdbcv1.AuthMode{"scram-sha-256"}, + expectError: false, + }, + { + name: "Mixed case SCRAM", + version: "8.0.10", + authModes: []mdbcv1.AuthMode{"Scram-Sha-256"}, + expectError: false, + }, + // Edge case tests + { + name: "PLAIN only", + version: "8.0.10", + authModes: []mdbcv1.AuthMode{"PLAIN"}, + expectError: true, + expectedErrMsg: "MongoDBSearch requires SCRAM authentication to be enabled", + }, + // Combined validation tests + { + name: "Invalid version with valid auth", + version: "7.0.0", + authModes: []mdbcv1.AuthMode{"SCRAM-SHA-256"}, + expectError: true, + expectedErrMsg: "MongoDB version must be 8.0.10 or higher", + }, + { + name: "Valid version with invalid auth", + version: "8.0.10", + authModes: []mdbcv1.AuthMode{"X509"}, + expectError: true, + expectedErrMsg: "MongoDBSearch requires SCRAM authentication to be enabled", + }, + { + name: "Invalid version with invalid auth", + version: "7.0.0", + authModes: []mdbcv1.AuthMode{"X509"}, + expectError: true, + expectedErrMsg: "MongoDB version must be 8.0.10 or higher", // Should fail on version first + }, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + src := newCommunitySearchSource(c.version, c.authModes) + err := src.Validate() + + if c.expectError { + assert.Error(t, err) + assert.Contains(t, err.Error(), c.expectedErrMsg) + } else { + assert.NoError(t, err) + } + }) + } +} diff --git a/controllers/search_controller/enterprise_search_source.go b/controllers/search_controller/enterprise_search_source.go new file mode 100644 index 000000000..c1256fbbb --- /dev/null +++ b/controllers/search_controller/enterprise_search_source.go @@ -0,0 +1,88 @@ +package search_controller + +import ( + "fmt" + "strings" + + "github.com/blang/semver" + "golang.org/x/xerrors" + "k8s.io/apimachinery/pkg/types" + + mdbv1 "github.com/mongodb/mongodb-kubernetes/api/v1/mdb" + "github.com/mongodb/mongodb-kubernetes/controllers/operator/watch" + "github.com/mongodb/mongodb-kubernetes/pkg/statefulset" + "github.com/mongodb/mongodb-kubernetes/pkg/util" +) + +type EnterpriseResourceSearchSource struct { + *mdbv1.MongoDB +} + +func NewEnterpriseResourceSearchSource(mdb *mdbv1.MongoDB) SearchSourceDBResource { + return EnterpriseResourceSearchSource{mdb} +} + +func (r EnterpriseResourceSearchSource) HostSeeds() []string { + seeds := make([]string, r.Spec.Members) + for i := range seeds { + seeds[i] = fmt.Sprintf("%s-%d.%s.%s.svc.cluster.local:%d", r.Name, i, r.ServiceName(), r.Namespace, r.Spec.GetAdditionalMongodConfig().GetPortOrDefault()) + } + return seeds +} + +func (r EnterpriseResourceSearchSource) TLSConfig() *TLSSourceConfig { + if !r.Spec.Security.IsTLSEnabled() { + return nil + } + + return &TLSSourceConfig{ + CAFileName: "ca-pem", + CAVolume: statefulset.CreateVolumeFromConfigMap("ca", r.Spec.Security.TLSConfig.CA), + ResourcesToWatch: map[watch.Type][]types.NamespacedName{ + watch.ConfigMap: { + {Namespace: r.Namespace, Name: r.Spec.Security.TLSConfig.CA}, + }, + }, + } +} + +func (r EnterpriseResourceSearchSource) KeyfileSecretName() string { + return fmt.Sprintf("%s-keyfile", r.Name) +} + +func (r EnterpriseResourceSearchSource) Validate() error { + version, err := semver.ParseTolerant(r.Spec.GetMongoDBVersion()) + if err != nil { + return xerrors.Errorf("error parsing MongoDB version '%s': %w", r.Spec.GetMongoDBVersion(), err) + } else if version.LT(semver.MustParse("8.0.10")) { + return xerrors.New("MongoDB version must be 8.0.10 or higher") + } + + if r.Spec.GetTopology() != mdbv1.ClusterTopologySingleCluster { + return xerrors.Errorf("MongoDBSearch is only supported for %s topology", mdbv1.ClusterTopologySingleCluster) + } + + if r.GetResourceType() != mdbv1.ReplicaSet { + return xerrors.Errorf("MongoDBSearch is only supported for %s resources", mdbv1.ReplicaSet) + } + + authModes := r.Spec.GetSecurityAuthenticationModes() + foundScram := false + for _, authMode := range authModes { + // Check for SCRAM, SCRAM-SHA-1, or SCRAM-SHA-256 + if strings.HasPrefix(strings.ToUpper(authMode), util.SCRAM) { + foundScram = true + break + } + } + + if !foundScram && len(authModes) > 0 { + return xerrors.New("MongoDBSearch requires SCRAM authentication to be enabled") + } + + if r.Spec.Security.GetInternalClusterAuthenticationMode() == util.X509 { + return xerrors.New("MongoDBSearch does not support X.509 internal cluster authentication") + } + + return nil +} diff --git a/controllers/search_controller/enterprise_search_source_test.go b/controllers/search_controller/enterprise_search_source_test.go new file mode 100644 index 000000000..1362ab605 --- /dev/null +++ b/controllers/search_controller/enterprise_search_source_test.go @@ -0,0 +1,290 @@ +package search_controller + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + mdbv1 "github.com/mongodb/mongodb-kubernetes/api/v1/mdb" +) + +func newEnterpriseSearchSource(version string, topology string, resourceType mdbv1.ResourceType, authModes []string, internalClusterAuth string) EnterpriseResourceSearchSource { + authModesList := make([]mdbv1.AuthMode, len(authModes)) + for i, mode := range authModes { + authModesList[i] = mdbv1.AuthMode(mode) + } + + // Create security with authentication if needed + var security *mdbv1.Security + if len(authModes) > 0 || internalClusterAuth != "" { + security = &mdbv1.Security{ + Authentication: &mdbv1.Authentication{ + Enabled: len(authModes) > 0, + Modes: authModesList, + InternalCluster: internalClusterAuth, + }, + } + } + + src := EnterpriseResourceSearchSource{ + MongoDB: &mdbv1.MongoDB{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-mongodb", + Namespace: "test-namespace", + }, + Spec: mdbv1.MongoDbSpec{ + DbCommonSpec: mdbv1.DbCommonSpec{ + Version: version, + ResourceType: resourceType, + Security: security, + }, + }, + }, + } + + // Set topology directly since it's inlined from DbCommonSpec + src.Spec.Topology = topology + return src +} + +func TestEnterpriseResourceSearchSource_Validate(t *testing.T) { + cases := []struct { + name string + version string + topology string + resourceType mdbv1.ResourceType + authModes []string + internalClusterAuth string + expectError bool + expectedErrMsg string + }{ + // Version validation tests + { + name: "Invalid version", + version: "invalid.version", + topology: mdbv1.ClusterTopologySingleCluster, + resourceType: mdbv1.ReplicaSet, + authModes: []string{}, + expectError: true, + expectedErrMsg: "error parsing MongoDB version", + }, + { + name: "Version too old", + version: "7.0.0", + topology: mdbv1.ClusterTopologySingleCluster, + resourceType: mdbv1.ReplicaSet, + authModes: []string{}, + expectError: true, + expectedErrMsg: "MongoDB version must be 8.0.10 or higher", + }, + { + name: "Version just below minimum", + version: "8.0.9", + topology: mdbv1.ClusterTopologySingleCluster, + resourceType: mdbv1.ReplicaSet, + authModes: []string{}, + expectError: true, + expectedErrMsg: "MongoDB version must be 8.0.10 or higher", + }, + { + name: "Valid minimum version", + version: "8.0.10", + topology: mdbv1.ClusterTopologySingleCluster, + resourceType: mdbv1.ReplicaSet, + authModes: []string{}, + expectError: false, + }, + { + name: "Version above minimum", + version: "8.1.0", + topology: mdbv1.ClusterTopologySingleCluster, + resourceType: mdbv1.ReplicaSet, + authModes: []string{}, + expectError: false, + }, + // Topology validation tests + { + name: "Invalid topology - MultiCluster", + version: "8.0.10", + topology: mdbv1.ClusterTopologyMultiCluster, + resourceType: mdbv1.ReplicaSet, + authModes: []string{}, + expectError: true, + expectedErrMsg: "MongoDBSearch is only supported for SingleCluster topology", + }, + { + name: "Valid topology - SingleCluster", + version: "8.0.10", + topology: mdbv1.ClusterTopologySingleCluster, + resourceType: mdbv1.ReplicaSet, + authModes: []string{}, + expectError: false, + }, + { + name: "Empty topology defaults to SingleCluster", + version: "8.0.10", + topology: "", + resourceType: mdbv1.ReplicaSet, + authModes: []string{}, + expectError: false, + }, + // Resource type validation tests + { + name: "Invalid resource type - Standalone", + version: "8.0.10", + topology: mdbv1.ClusterTopologySingleCluster, + resourceType: mdbv1.Standalone, + authModes: []string{}, + expectError: true, + expectedErrMsg: "MongoDBSearch is only supported for ReplicaSet resources", + }, + { + name: "Invalid resource type - ShardedCluster", + version: "8.0.10", + topology: mdbv1.ClusterTopologySingleCluster, + resourceType: mdbv1.ShardedCluster, + authModes: []string{}, + expectError: true, + expectedErrMsg: "MongoDBSearch is only supported for ReplicaSet resources", + }, + { + name: "Valid resource type - ReplicaSet", + version: "8.0.10", + topology: mdbv1.ClusterTopologySingleCluster, + resourceType: mdbv1.ReplicaSet, + authModes: []string{}, + expectError: false, + }, + // Authentication mode tests + { + name: "No SCRAM authentication", + version: "8.0.10", + topology: mdbv1.ClusterTopologySingleCluster, + resourceType: mdbv1.ReplicaSet, + authModes: []string{"X509"}, + expectError: true, + expectedErrMsg: "MongoDBSearch requires SCRAM authentication to be enabled", + }, + { + name: "Empty authentication modes", + version: "8.0.10", + topology: mdbv1.ClusterTopologySingleCluster, + resourceType: mdbv1.ReplicaSet, + authModes: []string{}, + expectError: false, + }, + { + name: "Nil authentication modes", + version: "8.0.10", + topology: mdbv1.ClusterTopologySingleCluster, + resourceType: mdbv1.ReplicaSet, + authModes: nil, + expectError: false, + }, + { + name: "Valid SCRAM authentication", + version: "8.0.10", + topology: mdbv1.ClusterTopologySingleCluster, + resourceType: mdbv1.ReplicaSet, + authModes: []string{"SCRAM-SHA-256"}, + expectError: false, + }, + { + name: "Mixed auth modes with SCRAM", + version: "8.0.10", + topology: mdbv1.ClusterTopologySingleCluster, + resourceType: mdbv1.ReplicaSet, + authModes: []string{"X509", "SCRAM-SHA-256"}, + expectError: false, + }, + { + name: "Case insensitive SCRAM", + version: "8.0.10", + topology: mdbv1.ClusterTopologySingleCluster, + resourceType: mdbv1.ReplicaSet, + authModes: []string{"scram-sha-256"}, + expectError: false, + }, + { + name: "SCRAM variants", + version: "8.0.10", + topology: mdbv1.ClusterTopologySingleCluster, + resourceType: mdbv1.ReplicaSet, + authModes: []string{"SCRAM", "SCRAM-SHA-1", "SCRAM-SHA-256"}, + expectError: false, + }, + // Internal cluster authentication tests + { + name: "X509 internal cluster auth not supported", + version: "8.0.10", + topology: mdbv1.ClusterTopologySingleCluster, + resourceType: mdbv1.ReplicaSet, + authModes: []string{"SCRAM-SHA-256"}, + internalClusterAuth: "X509", + expectError: true, + expectedErrMsg: "MongoDBSearch does not support X.509 internal cluster authentication", + }, + { + name: "Valid internal cluster auth - empty", + version: "8.0.10", + topology: mdbv1.ClusterTopologySingleCluster, + resourceType: mdbv1.ReplicaSet, + authModes: []string{"SCRAM-SHA-256"}, + internalClusterAuth: "", + expectError: false, + }, + { + name: "Valid internal cluster auth - SCRAM", + version: "8.0.10", + topology: mdbv1.ClusterTopologySingleCluster, + resourceType: mdbv1.ReplicaSet, + authModes: []string{"SCRAM-SHA-256"}, + internalClusterAuth: "SCRAM", + expectError: false, + }, + // Combined validation tests + { + name: "Multiple validation failures - version takes precedence", + version: "7.0.0", + topology: mdbv1.ClusterTopologyMultiCluster, + resourceType: mdbv1.Standalone, + authModes: []string{"X509"}, + expectError: true, + expectedErrMsg: "MongoDB version must be 8.0.10 or higher", + }, + { + name: "Valid version, invalid topology", + version: "8.0.10", + topology: mdbv1.ClusterTopologyMultiCluster, + resourceType: mdbv1.ReplicaSet, + authModes: []string{}, + expectError: true, + expectedErrMsg: "MongoDBSearch is only supported for SingleCluster topology", + }, + { + name: "Valid version and topology, invalid resource type", + version: "8.0.10", + topology: mdbv1.ClusterTopologySingleCluster, + resourceType: mdbv1.Standalone, + authModes: []string{}, + expectError: true, + expectedErrMsg: "MongoDBSearch is only supported for ReplicaSet resources", + }, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + src := newEnterpriseSearchSource(c.version, c.topology, c.resourceType, c.authModes, c.internalClusterAuth) + err := src.Validate() + + if c.expectError { + assert.Error(t, err) + assert.Contains(t, err.Error(), c.expectedErrMsg) + } else { + assert.NoError(t, err) + } + }) + } +} diff --git a/controllers/search_controller/external_search_source.go b/controllers/search_controller/external_search_source.go new file mode 100644 index 000000000..5a408246e --- /dev/null +++ b/controllers/search_controller/external_search_source.go @@ -0,0 +1,49 @@ +package search_controller + +import ( + "k8s.io/apimachinery/pkg/types" + + searchv1 "github.com/mongodb/mongodb-kubernetes/api/v1/search" + "github.com/mongodb/mongodb-kubernetes/controllers/operator/watch" + "github.com/mongodb/mongodb-kubernetes/pkg/statefulset" +) + +func NewExternalSearchSource(namespace string, spec *searchv1.ExternalMongoDBSource) SearchSourceDBResource { + return &externalSearchResource{namespace: namespace, spec: spec} +} + +// externalSearchResource implements SearchSourceDBResource for deployments managed outside the operator. +type externalSearchResource struct { + namespace string + spec *searchv1.ExternalMongoDBSource +} + +func (r *externalSearchResource) Validate() error { + return nil +} + +func (r *externalSearchResource) TLSConfig() *TLSSourceConfig { + if r.spec.TLS == nil || !r.spec.TLS.Enabled { + return nil + } + + return &TLSSourceConfig{ + CAFileName: "ca.crt", + CAVolume: statefulset.CreateVolumeFromSecret("ca", r.spec.TLS.CA.Name), + ResourcesToWatch: map[watch.Type][]types.NamespacedName{ + watch.Secret: { + {Namespace: r.namespace, Name: r.spec.TLS.CA.Name}, + }, + }, + } +} + +func (r *externalSearchResource) KeyfileSecretName() string { + if r.spec.KeyFileSecretKeyRef != nil { + return r.spec.KeyFileSecretKeyRef.Name + } + + return "" +} + +func (r *externalSearchResource) HostSeeds() []string { return r.spec.HostAndPorts } diff --git a/controllers/search_controller/mongodbsearch_reconcile_helper.go b/controllers/search_controller/mongodbsearch_reconcile_helper.go index 59523ce8e..4b384a707 100644 --- a/controllers/search_controller/mongodbsearch_reconcile_helper.go +++ b/controllers/search_controller/mongodbsearch_reconcile_helper.go @@ -7,30 +7,42 @@ import ( "fmt" "strings" - "github.com/blang/semver" "github.com/ghodss/yaml" "go.uber.org/zap" "golang.org/x/xerrors" "k8s.io/apimachinery/pkg/fields" "k8s.io/apimachinery/pkg/util/intstr" + "k8s.io/utils/ptr" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + mdbv1 "github.com/mongodb/mongodb-kubernetes/api/v1/mdb" searchv1 "github.com/mongodb/mongodb-kubernetes/api/v1/search" "github.com/mongodb/mongodb-kubernetes/controllers/operator/workflow" + "github.com/mongodb/mongodb-kubernetes/mongodb-community-operator/pkg/automationconfig" kubernetesClient "github.com/mongodb/mongodb-kubernetes/mongodb-community-operator/pkg/kube/client" + "github.com/mongodb/mongodb-kubernetes/mongodb-community-operator/pkg/kube/container" + "github.com/mongodb/mongodb-kubernetes/mongodb-community-operator/pkg/kube/podtemplatespec" "github.com/mongodb/mongodb-kubernetes/mongodb-community-operator/pkg/kube/service" "github.com/mongodb/mongodb-kubernetes/mongodb-community-operator/pkg/mongot" + "github.com/mongodb/mongodb-kubernetes/mongodb-community-operator/pkg/tls" + "github.com/mongodb/mongodb-kubernetes/pkg/kube" "github.com/mongodb/mongodb-kubernetes/pkg/kube/commoncontroller" "github.com/mongodb/mongodb-kubernetes/pkg/statefulset" ) const ( - MongoDBSearchIndexFieldName = "mdbsearch-for-mongodbresourceref-index" + MongoDBSearchIndexFieldName = "mdbsearch-for-mongodbresourceref-index" + unsupportedSearchVersion = "1.47.0" + unsupportedSearchVersionErrorFmt = "MongoDBSearch version %s is not supported because of breaking changes. " + + "The operator will ignore this resource: it will not reconcile or reconfigure the workload. " + + "Existing deployments will continue to run, but cannot be managed by the operator. " + + "To regain operator management, you must delete and recreate the MongoDBSearch resource." ) type OperatorSearchConfig struct { @@ -72,7 +84,11 @@ func (r *MongoDBSearchReconcileHelper) reconcile(ctx context.Context, log *zap.S log = log.With("MongoDBSearch", r.mdbSearch.NamespacedName()) log.Infof("Reconciling MongoDBSearch") - if err := ValidateSearchSource(r.db); err != nil { + if err := r.db.Validate(); err != nil { + return workflow.Failed(err) + } + + if err := r.ValidateSearchImageVersion(); err != nil { return workflow.Failed(err) } @@ -80,39 +96,79 @@ func (r *MongoDBSearchReconcileHelper) reconcile(ctx context.Context, log *zap.S return workflow.Failed(err) } + keyfileStsModification, err := r.ensureSourceKeyfile(ctx, log) + if apierrors.IsNotFound(err) { + return workflow.Pending("Waiting for keyfile secret to be created") + } else if err != nil { + return workflow.Failed(err) + } + if err := r.ensureSearchService(ctx, r.mdbSearch); err != nil { return workflow.Failed(err) } - mongotConfig := createMongotConfig(r.mdbSearch, r.db) - configHash, err := r.ensureMongotConfig(ctx, mongotConfig) + ingressTlsMongotModification, ingressTlsStsModification, err := r.ensureIngressTlsConfig(ctx) if err != nil { return workflow.Failed(err) } - if err := r.createOrUpdateStatefulSet(ctx, log, configHash); err != nil { + egressTlsMongotModification, egressTlsStsModification, err := r.ensureEgressTlsConfig(ctx) + if err != nil { + return workflow.Failed(err) + } + + configHash, err := r.ensureMongotConfig(ctx, log, createMongotConfig(r.mdbSearch, r.db), ingressTlsMongotModification, egressTlsMongotModification) + if err != nil { return workflow.Failed(err) } - if statefulSetStatus := statefulset.GetStatefulSetStatus(ctx, r.db.NamespacedName().Namespace, r.mdbSearch.StatefulSetNamespacedName().Name, r.client); !statefulSetStatus.IsOK() { + configHashModification := statefulset.WithPodSpecTemplate(podtemplatespec.WithAnnotations( + map[string]string{ + "mongotConfigHash": configHash, + }, + )) + + if err := r.createOrUpdateStatefulSet(ctx, log, CreateSearchStatefulSetFunc(r.mdbSearch, r.db, r.buildImageString()), configHashModification, keyfileStsModification, ingressTlsStsModification, egressTlsStsModification); err != nil { + return workflow.Failed(err) + } + + if statefulSetStatus := statefulset.GetStatefulSetStatus(ctx, r.mdbSearch.Namespace, r.mdbSearch.StatefulSetNamespacedName().Name, r.client); !statefulSetStatus.IsOK() { return statefulSetStatus } return workflow.OK() } -func (r *MongoDBSearchReconcileHelper) createOrUpdateStatefulSet(ctx context.Context, log *zap.SugaredLogger, mongotConfigHash string) error { +func (r *MongoDBSearchReconcileHelper) ensureSourceKeyfile(ctx context.Context, log *zap.SugaredLogger) (statefulset.Modification, error) { + keyfileSecretName := kube.ObjectKey(r.mdbSearch.GetNamespace(), r.db.KeyfileSecretName()) + keyfileSecret := &corev1.Secret{} + if err := r.client.Get(ctx, keyfileSecretName, keyfileSecret); err != nil { + return nil, err + } + + return statefulset.Apply( + // make sure mongot pods get restarted if the keyfile changes + statefulset.WithPodSpecTemplate(podtemplatespec.WithAnnotations( + map[string]string{ + "keyfileHash": hashBytes(keyfileSecret.Data["keyfile"]), + }, + )), + ), nil +} + +func (r *MongoDBSearchReconcileHelper) buildImageString() string { imageVersion := r.mdbSearch.Spec.Version if imageVersion == "" { imageVersion = r.operatorSearchConfig.SearchVersion } - searchImage := fmt.Sprintf("%s/%s:%s", r.operatorSearchConfig.SearchRepo, r.operatorSearchConfig.SearchName, imageVersion) + return fmt.Sprintf("%s/%s:%s", r.operatorSearchConfig.SearchRepo, r.operatorSearchConfig.SearchName, imageVersion) +} +func (r *MongoDBSearchReconcileHelper) createOrUpdateStatefulSet(ctx context.Context, log *zap.SugaredLogger, modifications ...statefulset.Modification) error { stsName := r.mdbSearch.StatefulSetNamespacedName() sts := &appsv1.StatefulSet{ObjectMeta: metav1.ObjectMeta{Name: stsName.Name, Namespace: stsName.Namespace}} op, err := controllerutil.CreateOrUpdate(ctx, r.client, sts, func() error { - stsModification := CreateSearchStatefulSetFunc(r.mdbSearch, r.db, searchImage, mongotConfigHash) - stsModification(sts) + statefulset.Apply(modifications...)(sts) return nil }) if err != nil { @@ -142,7 +198,9 @@ func (r *MongoDBSearchReconcileHelper) ensureSearchService(ctx context.Context, return nil } -func (r *MongoDBSearchReconcileHelper) ensureMongotConfig(ctx context.Context, mongotConfig mongot.Config) (string, error) { +func (r *MongoDBSearchReconcileHelper) ensureMongotConfig(ctx context.Context, log *zap.SugaredLogger, modifications ...mongot.Modification) (string, error) { + mongotConfig := mongot.Config{} + mongot.Apply(modifications...)(&mongotConfig) configData, err := yaml.Marshal(mongotConfig) if err != nil { return "", err @@ -163,13 +221,89 @@ func (r *MongoDBSearchReconcileHelper) ensureMongotConfig(ctx context.Context, m return "", err } - zap.S().Debugf("Updated mongot config yaml config map: %v (%s) with the following configuration: %s", cmName, op, string(configData)) + log.Debugf("Updated mongot config yaml config map: %v (%s) with the following configuration: %s", cmName, op, string(configData)) - return hashMongotConfig(configData), nil + return hashBytes(configData), nil } -func hashMongotConfig(mongotConfigYaml []byte) string { - hashBytes := sha256.Sum256(mongotConfigYaml) +func (r *MongoDBSearchReconcileHelper) ensureIngressTlsConfig(ctx context.Context) (mongot.Modification, statefulset.Modification, error) { + if !r.mdbSearch.Spec.Security.TLS.Enabled { + mongotModification := func(config *mongot.Config) { + config.Server.Wireproto.TLS.Mode = mongot.ConfigTLSModeDisabled + } + return mongotModification, statefulset.NOOP(), nil + } + + // TODO: validate that the certificate in the user-provided Secret in .spec.security.tls.certificateKeySecret is issued by the CA in the operator's CA Secret + + certFileName, err := tls.EnsureTLSSecret(ctx, r.client, r.mdbSearch) + if err != nil { + return nil, nil, err + } + + mongotModification := func(config *mongot.Config) { + certPath := tls.OperatorSecretMountPath + certFileName + config.Server.Wireproto.TLS.Mode = mongot.ConfigTLSModeTLS + config.Server.Wireproto.TLS.CertificateKeyFile = ptr.To(certPath) + } + + tlsSecret := r.mdbSearch.TLSOperatorSecretNamespacedName() + tlsVolume := statefulset.CreateVolumeFromSecret("tls", tlsSecret.Name) + tlsVolumeMount := statefulset.CreateVolumeMount("tls", tls.OperatorSecretMountPath, statefulset.WithReadOnly(true)) + statefulsetModification := statefulset.WithPodSpecTemplate(podtemplatespec.Apply( + podtemplatespec.WithVolume(tlsVolume), + podtemplatespec.WithContainer(MongotContainerName, container.Apply( + container.WithVolumeMounts([]corev1.VolumeMount{tlsVolumeMount}), + )), + )) + + return mongotModification, statefulsetModification, nil +} + +func (r *MongoDBSearchReconcileHelper) ensureEgressTlsConfig(ctx context.Context) (mongot.Modification, statefulset.Modification, error) { + tlsSourceConfig := r.db.TLSConfig() + if tlsSourceConfig == nil { + return mongot.NOOP(), statefulset.NOOP(), nil + } + + mongotModification := func(config *mongot.Config) { + config.SyncSource.ReplicaSet.TLS = ptr.To(true) + } + + _, containerSecurityContext := podtemplatespec.WithDefaultSecurityContextsModifications() + caVolume := tlsSourceConfig.CAVolume + trustStoreVolume := statefulset.CreateVolumeFromEmptyDir("cacerts") + statefulsetModification := statefulset.WithPodSpecTemplate(podtemplatespec.Apply( + podtemplatespec.WithVolume(caVolume), + podtemplatespec.WithVolume(trustStoreVolume), + podtemplatespec.WithInitContainer("init-cacerts", container.Apply( + container.WithImage(r.buildImageString()), + containerSecurityContext, + container.WithVolumeMounts([]corev1.VolumeMount{ + statefulset.CreateVolumeMount(caVolume.Name, tls.CAMountPath, statefulset.WithReadOnly(true)), + statefulset.CreateVolumeMount(trustStoreVolume.Name, "/java/trust-store", statefulset.WithReadOnly(false)), + }), + container.WithCommand([]string{"sh"}), + container.WithArgs([]string{ + "-c", + fmt.Sprintf(` +cp /mongot-community/bin/jdk/lib/security/cacerts /java/trust-store/cacerts +/mongot-community/bin/jdk/bin/keytool -keystore /java/trust-store/cacerts -storepass changeit -noprompt -trustcacerts -importcert -alias mongodcert -file %s/%s + `, tls.CAMountPath, tlsSourceConfig.CAFileName), + }), + )), + podtemplatespec.WithContainer(MongotContainerName, container.Apply( + container.WithVolumeMounts([]corev1.VolumeMount{ + statefulset.CreateVolumeMount(trustStoreVolume.Name, "/mongot-community/bin/jdk/lib/security/cacerts", statefulset.WithReadOnly(true), statefulset.WithSubPath("cacerts")), + }), + )), + )) + + return mongotModification, statefulsetModification, nil +} + +func hashBytes(bytes []byte) string { + hashBytes := sha256.Sum256(bytes) return base32.StdEncoding.WithPadding(base32.NoPadding).EncodeToString(hashBytes[:]) } @@ -203,30 +337,67 @@ func buildSearchHeadlessService(search *searchv1.MongoDBSearch) corev1.Service { TargetPort: intstr.FromInt32(search.GetMongotMetricsPort()), }) + serviceBuilder.AddPort(&corev1.ServicePort{ + Name: "healthcheck", + Protocol: corev1.ProtocolTCP, + Port: search.GetMongotHealthCheckPort(), + TargetPort: intstr.FromInt32(search.GetMongotHealthCheckPort()), + }) + return serviceBuilder.Build() } -func createMongotConfig(search *searchv1.MongoDBSearch, db SearchSourceDBResource) mongot.Config { - return mongot.Config{CommunityPrivatePreview: mongot.CommunityPrivatePreview{ - MongodHostAndPort: fmt.Sprintf("%s.%s.svc.cluster.local:%d", db.DatabaseServiceName(), db.GetNamespace(), db.DatabasePort()), - QueryServerAddress: fmt.Sprintf("localhost:%d", search.GetMongotPort()), - KeyFilePath: "/mongot/keyfile/keyfile", - DataPath: "/mongot/data/config.yml", - Metrics: mongot.Metrics{ +func createMongotConfig(search *searchv1.MongoDBSearch, db SearchSourceDBResource) mongot.Modification { + return func(config *mongot.Config) { + hostAndPorts := db.HostSeeds() + + config.SyncSource = mongot.ConfigSyncSource{ + ReplicaSet: mongot.ConfigReplicaSet{ + HostAndPort: hostAndPorts, + Username: search.SourceUsername(), + PasswordFile: "/tmp/sourceUserPassword", + TLS: ptr.To(false), + ReadPreference: ptr.To("secondaryPreferred"), + AuthSource: ptr.To("admin"), + }, + } + config.Storage = mongot.ConfigStorage{ + DataPath: "/mongot/data/config.yml", + } + config.Server = mongot.ConfigServer{ + Wireproto: &mongot.ConfigWireproto{ + Address: "0.0.0.0:27027", + Authentication: &mongot.ConfigAuthentication{ + Mode: "keyfile", + KeyFile: "/tmp/keyfile", + }, + }, + } + config.Metrics = mongot.ConfigMetrics{ Enabled: true, - Address: fmt.Sprintf("localhost:%d", search.GetMongotMetricsPort()), - }, - Logging: mongot.Logging{ - Verbosity: "DEBUG", - }, - }} + Address: fmt.Sprintf("0.0.0.0:%d", search.GetMongotMetricsPort()), + } + config.HealthCheck = mongot.ConfigHealthCheck{ + Address: fmt.Sprintf("0.0.0.0:%d", search.GetMongotHealthCheckPort()), + } + config.Logging = mongot.ConfigLogging{ + Verbosity: "TRACE", + LogPath: nil, + } + } } -func GetMongodConfigParameters(search *searchv1.MongoDBSearch) map[string]interface{} { - return map[string]interface{}{ - "setParameter": map[string]interface{}{ - "mongotHost": mongotHostAndPort(search), - "searchIndexManagementHostAndPort": mongotHostAndPort(search), +func GetMongodConfigParameters(search *searchv1.MongoDBSearch) map[string]any { + searchTLSMode := automationconfig.TLSModeDisabled + if search.Spec.Security.TLS.Enabled { + searchTLSMode = automationconfig.TLSModeRequired + } + return map[string]any{ + "setParameter": map[string]any{ + "mongotHost": mongotHostAndPort(search), + "searchIndexManagementHostAndPort": mongotHostAndPort(search), + "skipAuthenticationToSearchIndexManagementServer": false, + "searchTLSMode": string(searchTLSMode), }, } } @@ -236,27 +407,17 @@ func mongotHostAndPort(search *searchv1.MongoDBSearch) string { return fmt.Sprintf("%s.%s.svc.cluster.local:%d", svcName.Name, svcName.Namespace, search.GetMongotPort()) } -func ValidateSearchSource(db SearchSourceDBResource) error { - version, err := semver.ParseTolerant(db.GetMongoDBVersion()) - if err != nil { - return xerrors.Errorf("error parsing MongoDB version '%s': %w", db.GetMongoDBVersion(), err) - } else if version.Major < 8 { - return xerrors.New("MongoDB version must be 8.0 or higher") - } - - if db.IsSecurityTLSConfigEnabled() { - return xerrors.New("MongoDBSearch does not support TLS-enabled sources") +func (r *MongoDBSearchReconcileHelper) ValidateSingleMongoDBSearchForSearchSource(ctx context.Context) error { + if r.mdbSearch.Spec.Source != nil && r.mdbSearch.Spec.Source.ExternalMongoDBSource != nil { + return nil } - return nil -} - -func (r *MongoDBSearchReconcileHelper) ValidateSingleMongoDBSearchForSearchSource(ctx context.Context) error { + ref := r.mdbSearch.GetMongoDBResourceRef() searchList := &searchv1.MongoDBSearchList{} if err := r.client.List(ctx, searchList, &client.ListOptions{ - FieldSelector: fields.OneTermEqualSelector(MongoDBSearchIndexFieldName, r.db.GetNamespace()+"/"+r.db.Name()), + FieldSelector: fields.OneTermEqualSelector(MongoDBSearchIndexFieldName, ref.Namespace+"/"+ref.Name), }); err != nil { - return xerrors.Errorf("Error listing MongoDBSearch resources for search source '%s': %w", r.db.Name(), err) + return xerrors.Errorf("Error listing MongoDBSearch resources for search source '%s': %w", ref.Name, err) } if len(searchList.Items) > 1 { @@ -264,8 +425,92 @@ func (r *MongoDBSearchReconcileHelper) ValidateSingleMongoDBSearchForSearchSourc for i, search := range searchList.Items { resourceNames[i] = search.Name } - return xerrors.Errorf("Found multiple MongoDBSearch resources for search source '%s': %s", r.db.Name(), strings.Join(resourceNames, ", ")) + return xerrors.Errorf( + "Found multiple MongoDBSearch resources for search source '%s': %s", ref.Name, + strings.Join(resourceNames, ", "), + ) } return nil } + +func (r *MongoDBSearchReconcileHelper) ValidateSearchImageVersion() error { + version := r.getMongotImage() + + if strings.Contains(version, unsupportedSearchVersion) { + return xerrors.Errorf(unsupportedSearchVersionErrorFmt, unsupportedSearchVersion) + } + + return nil +} + +func (r *MongoDBSearchReconcileHelper) getMongotImage() string { + version := strings.TrimSpace(r.mdbSearch.Spec.Version) + if version != "" { + return version + } + + if r.operatorSearchConfig.SearchVersion != "" { + return r.operatorSearchConfig.SearchVersion + } + + if r.mdbSearch.Spec.StatefulSetConfiguration == nil { + return "" + } + + for _, container := range r.mdbSearch.Spec.StatefulSetConfiguration.SpecWrapper.Spec.Template.Spec.Containers { + if container.Name == MongotContainerName { + return container.Image + } + } + + return "" +} + +func SearchCoordinatorRole() mdbv1.MongoDBRole { + // direct translation of https://github.com/10gen/mongo/blob/6f8d95a513eea8f91ea9f5d895dd8a288dfcf725/src/mongo/db/auth/builtin_roles.yml#L652 + return mdbv1.MongoDBRole{ + Role: "searchCoordinator", + Db: "admin", + Roles: []mdbv1.InheritedRole{ + { + Role: "clusterMonitor", + Db: "admin", + }, + { + Role: "directShardOperations", + Db: "admin", + }, + { + Role: "readAnyDatabase", + Db: "admin", + }, + }, + Privileges: []mdbv1.Privilege{ + { + Resource: mdbv1.Resource{ + Db: "__mdb_internal_search", + }, + Actions: []string{ + "changeStream", "collStats", "dbHash", "dbStats", "find", + "killCursors", "listCollections", "listIndexes", "listSearchIndexes", + // performRawDataOperations is available only on mongod master + // "performRawDataOperations", + "planCacheRead", "cleanupStructuredEncryptionData", + "compactStructuredEncryptionData", "convertToCapped", "createCollection", + "createIndex", "createSearchIndexes", "dropCollection", "dropIndex", + "dropSearchIndex", "insert", "remove", "renameCollectionSameDB", + "update", "updateSearchIndex", + }, + }, + // TODO: this causes the error "(BadValue) resource: {cluster: true} conflicts with resource type 'db'" + // { + // Resource: mdbv1.Resource{ + // Cluster: ptr.To(true), + // }, + // Actions: []string{"bypassDefaultMaxTimeMS"}, + // }, + }, + AuthenticationRestrictions: nil, + } +} diff --git a/controllers/search_controller/mongodbsearch_reconcile_helper_test.go b/controllers/search_controller/mongodbsearch_reconcile_helper_test.go index fe03ceb70..5a2a757ce 100644 --- a/controllers/search_controller/mongodbsearch_reconcile_helper_test.go +++ b/controllers/search_controller/mongodbsearch_reconcile_helper_test.go @@ -15,66 +15,6 @@ import ( kubernetesClient "github.com/mongodb/mongodb-kubernetes/mongodb-community-operator/pkg/kube/client" ) -func TestMongoDBSearchReconcileHelper_ValidateSearchSource(t *testing.T) { - mdbcMeta := metav1.ObjectMeta{ - Name: "test-mongodb", - Namespace: "test", - } - - cases := []struct { - name string - mdbc mdbcv1.MongoDBCommunity - expectedError string - }{ - { - name: "Invalid version", - mdbc: mdbcv1.MongoDBCommunity{ - ObjectMeta: mdbcMeta, - Spec: mdbcv1.MongoDBCommunitySpec{ - Version: "4.4.0", - }, - }, - expectedError: "MongoDB version must be 8.0 or higher", - }, - { - name: "Valid version", - mdbc: mdbcv1.MongoDBCommunity{ - ObjectMeta: mdbcMeta, - Spec: mdbcv1.MongoDBCommunitySpec{ - Version: "8.0", - }, - }, - }, - { - name: "TLS enabled", - mdbc: mdbcv1.MongoDBCommunity{ - ObjectMeta: mdbcMeta, - Spec: mdbcv1.MongoDBCommunitySpec{ - Version: "8.0", - Security: mdbcv1.Security{ - TLS: mdbcv1.TLS{ - Enabled: true, - }, - }, - }, - }, - expectedError: "MongoDBSearch does not support TLS-enabled sources", - }, - } - - for _, c := range cases { - t.Run(c.name, func(t *testing.T) { - db := NewSearchSourceDBResourceFromMongoDBCommunity(&c.mdbc) - err := ValidateSearchSource(db) - if c.expectedError == "" { - assert.NoError(t, err) - } else { - assert.EqualError(t, err, c.expectedError) - } - }) - } -} - func TestMongoDBSearchReconcileHelper_ValidateSingleMongoDBSearchForSearchSource(t *testing.T) { mdbSearchSpec := searchv1.MongoDBSearchSpec{ Source: &searchv1.MongoDBSource{ @@ -145,7 +85,7 @@ func TestMongoDBSearchReconcileHelper_ValidateSingleMongoDBSearchForSearchSource clientBuilder.WithObjects(v) } - helper := NewMongoDBSearchReconcileHelper(kubernetesClient.NewClient(clientBuilder.Build()), mdbSearch, NewSearchSourceDBResourceFromMongoDBCommunity(mdbc), OperatorSearchConfig{}) + helper := NewMongoDBSearchReconcileHelper(kubernetesClient.NewClient(clientBuilder.Build()), mdbSearch, NewCommunityResourceSearchSource(mdbc), OperatorSearchConfig{}) err := helper.ValidateSingleMongoDBSearchForSearchSource(t.Context()) if c.expectedError == "" { assert.NoError(t, err) diff --git a/controllers/search_controller/search_construction.go b/controllers/search_controller/search_construction.go index ea169339d..ffaa72358 100644 --- a/controllers/search_controller/search_construction.go +++ b/controllers/search_controller/search_construction.go @@ -9,7 +9,7 @@ import ( searchv1 "github.com/mongodb/mongodb-kubernetes/api/v1/search" "github.com/mongodb/mongodb-kubernetes/controllers/operator/construct" - mdbcv1 "github.com/mongodb/mongodb-kubernetes/mongodb-community-operator/api/v1" + "github.com/mongodb/mongodb-kubernetes/controllers/operator/watch" "github.com/mongodb/mongodb-kubernetes/mongodb-community-operator/api/v1/common" "github.com/mongodb/mongodb-kubernetes/mongodb-community-operator/pkg/kube/container" "github.com/mongodb/mongodb-kubernetes/mongodb-community-operator/pkg/kube/podtemplatespec" @@ -19,73 +19,30 @@ import ( ) const ( - MongotContainerName = "mongot" + MongotContainerName = "mongot" + SearchLivenessProbePath = "/health" + SearchReadinessProbePath = "/health" // Todo: Update this when search GA is available ) // SearchSourceDBResource is an object wrapping a MongoDBCommunity object // Its purpose is to: // - isolate and identify all the data we need to get from the CR in order to reconcile search resources // - implement search reconcile logic in a generic way that is working for any types of MongoDB databases (all database CRs). -// -// TODO check if we could use already existing interface (DbCommon, MongoDBStatefulSetOwner, etc.) type SearchSourceDBResource interface { - Name() string - NamespacedName() types.NamespacedName KeyfileSecretName() string - GetNamespace() string - HasSeparateDataAndLogsVolumes() bool - DatabaseServiceName() string - DatabasePort() int - GetMongoDBVersion() string - IsSecurityTLSConfigEnabled() bool + TLSConfig() *TLSSourceConfig + HostSeeds() []string + Validate() error } -func NewSearchSourceDBResourceFromMongoDBCommunity(mdbc *mdbcv1.MongoDBCommunity) SearchSourceDBResource { - return &mdbcSearchResource{db: mdbc} -} - -type mdbcSearchResource struct { - db *mdbcv1.MongoDBCommunity -} - -func (r *mdbcSearchResource) Name() string { - return r.db.Name -} - -func (r *mdbcSearchResource) NamespacedName() types.NamespacedName { - return r.db.NamespacedName() -} - -func (r *mdbcSearchResource) KeyfileSecretName() string { - return r.db.GetAgentKeyfileSecretNamespacedName().Name -} - -func (r *mdbcSearchResource) GetNamespace() string { - return r.db.Namespace -} - -func (r *mdbcSearchResource) HasSeparateDataAndLogsVolumes() bool { - return r.db.HasSeparateDataAndLogsVolumes() -} - -func (r *mdbcSearchResource) DatabaseServiceName() string { - return r.db.ServiceName() -} - -func (r *mdbcSearchResource) GetMongoDBVersion() string { - return r.db.Spec.Version -} - -func (r *mdbcSearchResource) IsSecurityTLSConfigEnabled() bool { - return r.db.Spec.Security.TLS.Enabled -} - -func (r *mdbcSearchResource) DatabasePort() int { - return r.db.GetMongodConfiguration().GetDBPort() +type TLSSourceConfig struct { + CAFileName string + CAVolume corev1.Volume + ResourcesToWatch map[watch.Type][]types.NamespacedName } // ReplicaSetOptions returns a set of options which will configure a ReplicaSet StatefulSet -func CreateSearchStatefulSetFunc(mdbSearch *searchv1.MongoDBSearch, sourceDBResource SearchSourceDBResource, searchImage string, mongotConfigHash string) statefulset.Modification { +func CreateSearchStatefulSetFunc(mdbSearch *searchv1.MongoDBSearch, sourceDBResource SearchSourceDBResource, searchImage string) statefulset.Modification { labels := map[string]string{ "app": mdbSearch.SearchServiceNamespacedName().Name, } @@ -95,14 +52,18 @@ func CreateSearchStatefulSetFunc(mdbSearch *searchv1.MongoDBSearch, sourceDBReso dataVolumeName := "data" keyfileVolumeName := "keyfile" + sourceUserPasswordVolumeName := "password" mongotConfigVolumeName := "config" pvcVolumeMount := statefulset.CreateVolumeMount(dataVolumeName, "/mongot/data", statefulset.WithSubPath("data")) - keyfileVolume := statefulset.CreateVolumeFromSecret("keyfile", sourceDBResource.KeyfileSecretName()) + keyfileVolume := statefulset.CreateVolumeFromSecret(keyfileVolumeName, sourceDBResource.KeyfileSecretName()) keyfileVolumeMount := statefulset.CreateVolumeMount(keyfileVolumeName, "/mongot/keyfile", statefulset.WithReadOnly(true)) - mongotConfigVolume := statefulset.CreateVolumeFromConfigMap("config", mdbSearch.MongotConfigConfigMapNamespacedName().Name) + sourceUserPasswordVolume := statefulset.CreateVolumeFromSecret(sourceUserPasswordVolumeName, mdbSearch.SourceUserPasswordSecretRef().Name) + sourceUserPasswordVolumeMount := statefulset.CreateVolumeMount(sourceUserPasswordVolumeName, "/mongot/sourceUserPassword", statefulset.WithReadOnly(true)) + + mongotConfigVolume := statefulset.CreateVolumeFromConfigMap(mongotConfigVolumeName, mdbSearch.MongotConfigConfigMapNamespacedName().Name) mongotConfigVolumeMount := statefulset.CreateVolumeMount(mongotConfigVolumeName, "/mongot/config", statefulset.WithReadOnly(true)) var persistenceConfig *common.PersistenceConfig @@ -120,12 +81,14 @@ func CreateSearchStatefulSetFunc(mdbSearch *searchv1.MongoDBSearch, sourceDBReso keyfileVolumeMount, tmpVolumeMount, mongotConfigVolumeMount, + sourceUserPasswordVolumeMount, } volumes := []corev1.Volume{ tmpVolume, keyfileVolume, mongotConfigVolume, + sourceUserPasswordVolume, } stsModifications := []statefulset.Modification{ @@ -142,11 +105,7 @@ func CreateSearchStatefulSetFunc(mdbSearch *searchv1.MongoDBSearch, sourceDBReso podtemplatespec.Apply( podSecurityContext, podtemplatespec.WithPodLabels(labels), - podtemplatespec.WithAnnotations(map[string]string{ - "mongotConfigHash": mongotConfigHash, - }), podtemplatespec.WithVolumes(volumes), - podtemplatespec.WithServiceAccount(sourceDBResource.DatabaseServiceName()), podtemplatespec.WithServiceAccount(util.MongoDBServiceAccount), podtemplatespec.WithContainer(MongotContainerName, mongodbSearchContainer(mdbSearch, volumeMounts, searchImage)), ), @@ -170,22 +129,65 @@ func mongodbSearchContainer(mdbSearch *searchv1.MongoDBSearch, volumeMounts []co container.WithName(MongotContainerName), container.WithImage(searchImage), container.WithImagePullPolicy(corev1.PullAlways), - container.WithReadinessProbe(probes.Apply( - probes.WithTCPSocket("", intstr.FromInt32(mdbSearch.GetMongotPort())), - probes.WithInitialDelaySeconds(20), - probes.WithPeriodSeconds(10), - )), + container.WithLivenessProbe(mongotLivenessProbe(mdbSearch)), + container.WithReadinessProbe(mongotReadinessProbe(mdbSearch)), container.WithResourceRequirements(createSearchResourceRequirements(mdbSearch.Spec.ResourceRequirements)), container.WithVolumeMounts(volumeMounts), container.WithCommand([]string{"sh"}), container.WithArgs([]string{ "-c", - "/mongot-community/mongot --config /mongot/config/config.yml", + ` +cp /mongot/keyfile/keyfile /tmp/keyfile +chown 2000:2000 /tmp/keyfile +chmod 0600 /tmp/keyfile + +cp /mongot/sourceUserPassword/password /tmp/sourceUserPassword +chown 2000:2000 /tmp/sourceUserPassword +chmod 0600 /tmp/sourceUserPassword + +/mongot-community/mongot --config /mongot/config/config.yml +`, }), containerSecurityContext, ) } +func mongotLivenessProbe(search *searchv1.MongoDBSearch) func(*corev1.Probe) { + return probes.Apply( + probes.WithHandler(corev1.ProbeHandler{ + HTTPGet: &corev1.HTTPGetAction{ + Scheme: corev1.URISchemeHTTP, + Port: intstr.FromInt32(search.GetMongotHealthCheckPort()), + Path: SearchLivenessProbePath, + }, + }), + probes.WithInitialDelaySeconds(10), + probes.WithPeriodSeconds(10), + probes.WithTimeoutSeconds(5), + probes.WithSuccessThreshold(1), + probes.WithFailureThreshold(10), + ) +} + +// mongotReadinessProbe just uses the endpoint intended for liveness checks; +// readiness check endpoint may be available in search GA. +func mongotReadinessProbe(search *searchv1.MongoDBSearch) func(*corev1.Probe) { + return probes.Apply( + probes.WithHandler(corev1.ProbeHandler{ + HTTPGet: &corev1.HTTPGetAction{ + Scheme: corev1.URISchemeHTTP, + Port: intstr.FromInt32(search.GetMongotHealthCheckPort()), + Path: SearchReadinessProbePath, + }, + }), + probes.WithInitialDelaySeconds(20), + probes.WithPeriodSeconds(10), + probes.WithTimeoutSeconds(5), + probes.WithSuccessThreshold(1), + probes.WithFailureThreshold(3), + ) +} + func createSearchResourceRequirements(requirements *corev1.ResourceRequirements) corev1.ResourceRequirements { if requirements != nil { return *requirements diff --git a/docker/mongodb-kubernetes-init-ops-manager/mmsconfiguration/edit_mms_configuration.go b/docker/mongodb-kubernetes-init-ops-manager/mmsconfiguration/edit_mms_configuration.go index 793914d3a..ca7b9a3b0 100755 --- a/docker/mongodb-kubernetes-init-ops-manager/mmsconfiguration/edit_mms_configuration.go +++ b/docker/mongodb-kubernetes-init-ops-manager/mmsconfiguration/edit_mms_configuration.go @@ -145,7 +145,7 @@ func readLinesFromFile(name string) ([]string, error) { func writeLinesToFile(name string, lines []string) error { output := strings.Join(lines, lineBreak) - err := os.WriteFile(name, []byte(output), 0o775) + err := os.WriteFile(name, []byte(output), 0o644) if err != nil { return xerrors.Errorf("error writing to file %s: %w", name, err) } @@ -168,7 +168,7 @@ func appendLinesToFile(name string, lines string) error { func getOmPropertiesFromEnvVars() map[string]string { props := map[string]string{} - for _, pair := range os.Environ() { + for _, pair := range os.Environ() { // nolint:forbidigo if !strings.HasPrefix(pair, omPropertyPrefix) { continue } diff --git a/docker/mongodb-kubernetes-tests/kubetester/helm.py b/docker/mongodb-kubernetes-tests/kubetester/helm.py index da6887b81..9e0cca8fe 100644 --- a/docker/mongodb-kubernetes-tests/kubetester/helm.py +++ b/docker/mongodb-kubernetes-tests/kubetester/helm.py @@ -120,6 +120,9 @@ def helm_repo_add(repo_name: str, url: str): def process_run_and_check(args, **kwargs): + if "check" not in kwargs: + kwargs["check"] = True + try: logger.debug(f"subprocess.run: {args}") completed_process = subprocess.run(args, **kwargs) diff --git a/docker/mongodb-kubernetes-tests/kubetester/mongodb.py b/docker/mongodb-kubernetes-tests/kubetester/mongodb.py index c9970361a..b3f38239b 100644 --- a/docker/mongodb-kubernetes-tests/kubetester/mongodb.py +++ b/docker/mongodb-kubernetes-tests/kubetester/mongodb.py @@ -278,10 +278,8 @@ def configure_custom_tls( tls_cert_secret_name: str, ): ensure_nested_objects(self, ["spec", "security", "tls"]) - self["spec"]["security"] = { - "certsSecretPrefix": tls_cert_secret_name, - "tls": {"enabled": True, "ca": issuer_ca_configmap_name}, - } + self["spec"]["security"]["certsSecretPrefix"] = tls_cert_secret_name + self["spec"]["security"]["tls"].update({"enabled": True, "ca": issuer_ca_configmap_name}) def build_list_of_hosts(self): """Returns the list of full_fqdn:27017 for every member of the mongodb resource""" diff --git a/docker/mongodb-kubernetes-tests/tests/common/search/movies_search_helper.py b/docker/mongodb-kubernetes-tests/tests/common/search/movies_search_helper.py index e5629bda0..be2151158 100644 --- a/docker/mongodb-kubernetes-tests/tests/common/search/movies_search_helper.py +++ b/docker/mongodb-kubernetes-tests/tests/common/search/movies_search_helper.py @@ -1,3 +1,7 @@ +import logging + +import pymongo.errors +from kubetester import kubetester from tests import test_logger from tests.common.search.search_tester import SearchTester @@ -26,9 +30,33 @@ def create_search_index(self): def wait_for_search_indexes(self): self.search_tester.wait_for_search_indexes_ready(self.db_name, self.col_name) - def assert_search_query(self): - # sample taken from: https://www.mongodb.com/docs/atlas/atlas-search/tutorial/run-query/#run-a-complex-query-on-the-movies-collection-7 - result = self.search_tester.client[self.db_name][self.col_name].aggregate( + def assert_search_query(self, retry_timeout: int = 1): + def wait_for_search_results(): + # sample taken from: https://www.mongodb.com/docs/atlas/atlas-search/tutorial/run-query/#run-a-complex-query-on-the-movies-collection-7 + count = 0 + status_msg = "" + try: + result = self.execute_example_search_query() + status_msg = f"{self.db_name}/{self.col_name}: search query results:\n" + for r in result: + status_msg += f"{r}\n" + count += 1 + status_msg += f"Count: {count}" + logger.debug(status_msg) + except pymongo.errors.PyMongoError as e: + logger.debug(f"error: {e}") + + return count == 4, status_msg + + kubetester.run_periodically( + fn=wait_for_search_results, + timeout=retry_timeout, + sleep_time=1, + msg="Search query to return correct data", + ) + + def execute_example_search_query(self): + return self.search_tester.client[self.db_name][self.col_name].aggregate( [ { "$search": { @@ -47,11 +75,3 @@ def assert_search_query(self): {"$project": {"title": 1, "plot": 1, "genres": 1, "_id": 0}}, ] ) - - logger.debug(f"{self.db_name}/{self.col_name}: search query results:") - count = 0 - for r in result: - logger.debug(r) - count += 1 - - assert count == 4 diff --git a/docker/mongodb-kubernetes-tests/tests/common/search/search_tester.py b/docker/mongodb-kubernetes-tests/tests/common/search/search_tester.py index bbca34b3b..6dccfb5ae 100644 --- a/docker/mongodb-kubernetes-tests/tests/common/search/search_tester.py +++ b/docker/mongodb-kubernetes-tests/tests/common/search/search_tester.py @@ -15,8 +15,10 @@ class SearchTester(MongoTester): def __init__( self, connection_string: str, + use_ssl: bool = False, + ca_path: str | None = None, ): - super().__init__(connection_string, False) + super().__init__(connection_string, use_ssl, ca_path) def mongorestore_from_url(self, archive_url: str, ns_include: str, mongodb_tools_dir: str = ""): logger.debug(f"running mongorestore from {archive_url}") @@ -26,7 +28,11 @@ def mongorestore_from_url(self, archive_url: str, ns_include: str, mongodb_tools logger.debug(f"Downloaded sample file from {archive_url} to {sample_file.name} (size: {size})") mongorestore_path = os.path.join(mongodb_tools_dir, "mongorestore") mongorestore_cmd = f"{mongorestore_path} --archive={sample_file.name} --verbose=1 --drop --nsInclude {ns_include} --uri={self.cnx_string}" - process_run_and_check(mongorestore_cmd.split()) + if self.default_opts.get("tls", False): + mongorestore_cmd += " --ssl" + if ca_path := self.default_opts.get("tlsCAFile"): + mongorestore_cmd += " --sslCAFile=" + ca_path + process_run_and_check(mongorestore_cmd.split(), capture_output=True) def create_search_index(self, database_name: str, collection_name: str): database = self.client[database_name] @@ -49,6 +55,10 @@ def wait_for_search_indexes_ready(self, database_name: str, collection_name: str def search_indexes_ready(self, database_name: str, collection_name: str): search_indexes = self.get_search_indexes(database_name, collection_name) + if len(search_indexes) == 0: + logger.error(f"There are no search indexes available in {database_name}.{collection_name}") + return False + for idx in search_indexes: if idx.get("status") != "READY": logger.debug(f"{database_name}/{collection_name}: search index {idx} is not ready") diff --git a/docker/mongodb-kubernetes-tests/tests/search/fixtures/community-replicaset-sample-mflix-external.yaml b/docker/mongodb-kubernetes-tests/tests/search/fixtures/community-replicaset-sample-mflix-external.yaml new file mode 100644 index 000000000..45e332fd3 --- /dev/null +++ b/docker/mongodb-kubernetes-tests/tests/search/fixtures/community-replicaset-sample-mflix-external.yaml @@ -0,0 +1,112 @@ +--- +apiVersion: mongodbcommunity.mongodb.com/v1 +kind: MongoDBCommunity +metadata: + name: mdbc-rs +spec: + version: 8.0.10 + type: ReplicaSet + members: 3 + security: + authentication: + ignoreUnknownUsers: true + modes: + - SCRAM + roles: + - role: searchCoordinator + db: admin + roles: + - name: clusterMonitor + db: admin + - name: directShardOperations + db: admin + - name: readAnyDatabase + db: admin + privileges: + - resource: + db: "__mdb_internal_search" + collection: "" + actions: + - "changeStream" + - "collStats" + - "dbHash" + - "dbStats" + - "find" + - "killCursors" + - "listCollections" + - "listIndexes" + - "listSearchIndexes" + - "planCacheRead" + - "cleanupStructuredEncryptionData" + - "compactStructuredEncryptionData" + - "convertToCapped" + - "createCollection" + - "createIndex" + - "createSearchIndexes" + - "dropCollection" + - "dropIndex" + - "dropSearchIndex" + - "insert" + - "remove" + - "renameCollectionSameDB" + - "update" + - "updateSearchIndex" + - resource: + cluster: true + actions: + - "bypassDefaultMaxTimeMS" + agent: + logLevel: DEBUG + statefulSet: + spec: + template: + spec: + containers: + - name: mongod + resources: + limits: + cpu: "2" + memory: 2Gi + requests: + cpu: "1" + memory: 1Gi + - name: mongodb-agent + resources: + limits: + cpu: "1" + memory: 2Gi + requests: + cpu: "0.5" + memory: 1Gi + users: + # admin user with root role + - name: mdb-admin + db: admin + passwordSecretRef: # a reference to the secret containing user password + name: mdb-admin-user-password + scramCredentialsSecretName: mdb-admin-user + roles: + - name: root + db: admin + # user performing search queries + - name: mdb-user + db: admin + passwordSecretRef: # a reference to the secret containing user password + name: mdb-user-password + scramCredentialsSecretName: mdb-user-scram + roles: + - name: restore + db: sample_mflix + - name: readWrite + db: sample_mflix + # user used by MongoDB Search to connect to MongoDB database to synchronize data from + # For MongoDB <8.2, the operator will be creating the searchCoordinator custom role automatically + # From MongoDB 8.2, searchCoordinator role will be a built-in role. + - name: search-sync-source + db: admin + passwordSecretRef: # a reference to the secret that will be used to generate the user's password + name: mdbc-rs-search-sync-source-password + scramCredentialsSecretName: mdbc-rs-search-sync-source + roles: + - name: searchCoordinator + db: admin diff --git a/docker/mongodb-kubernetes-tests/tests/search/fixtures/community-replicaset-sample-mflix.yaml b/docker/mongodb-kubernetes-tests/tests/search/fixtures/community-replicaset-sample-mflix.yaml index 4ba3d40b6..fe54d614d 100644 --- a/docker/mongodb-kubernetes-tests/tests/search/fixtures/community-replicaset-sample-mflix.yaml +++ b/docker/mongodb-kubernetes-tests/tests/search/fixtures/community-replicaset-sample-mflix.yaml @@ -4,27 +4,16 @@ kind: MongoDBCommunity metadata: name: mdbc-rs spec: - members: 3 + version: 8.0.10 type: ReplicaSet - version: "8.0.5" + members: 3 security: authentication: - modes: ["SCRAM"] + ignoreUnknownUsers: true + modes: + - SCRAM agent: logLevel: DEBUG - users: - - name: my-user - db: admin - passwordSecretRef: # a reference to the secret that will be used to generate the user's password - name: my-user-password - roles: - - name: clusterAdmin - db: admin - - name: userAdminAnyDatabase - db: admin - - name: readWrite - db: sample_mflix - scramCredentialsSecretName: my-scram statefulSet: spec: template: @@ -33,16 +22,48 @@ spec: - name: mongod resources: limits: - cpu: "3" - memory: 5Gi - requests: cpu: "2" - memory: 5Gi + memory: 2Gi + requests: + cpu: "1" + memory: 1Gi - name: mongodb-agent resources: limits: - cpu: "3" - memory: 5Gi + cpu: "1" + memory: 2Gi requests: - cpu: "2" - memory: 5Gi + cpu: "0.5" + memory: 1Gi + users: + # admin user with root role + - name: mdb-admin + db: admin + passwordSecretRef: # a reference to the secret containing user password + name: mdb-admin-user-password + scramCredentialsSecretName: mdb-admin-user + roles: + - name: root + db: admin + # user performing search queries + - name: mdb-user + db: admin + passwordSecretRef: # a reference to the secret containing user password + name: mdb-user-password + scramCredentialsSecretName: mdb-user-scram + roles: + - name: restore + db: sample_mflix + - name: readWrite + db: sample_mflix + # user used by MongoDB Search to connect to MongoDB database to synchronize data from + # For MongoDB <8.2, the operator will be creating the searchCoordinator custom role automatically + # From MongoDB 8.2, searchCoordinator role will be a built-in role. + - name: search-sync-source + db: admin + passwordSecretRef: # a reference to the secret that will be used to generate the user's password + name: mdbc-rs-search-sync-source-password + scramCredentialsSecretName: mdbc-rs-search-sync-source + roles: + - name: searchCoordinator + db: admin diff --git a/docker/mongodb-kubernetes-tests/tests/search/fixtures/enterprise-replicaset-sample-mflix.yaml b/docker/mongodb-kubernetes-tests/tests/search/fixtures/enterprise-replicaset-sample-mflix.yaml new file mode 100644 index 000000000..29d455b51 --- /dev/null +++ b/docker/mongodb-kubernetes-tests/tests/search/fixtures/enterprise-replicaset-sample-mflix.yaml @@ -0,0 +1,34 @@ +--- +apiVersion: mongodb.com/v1 +kind: MongoDB +metadata: + name: mdb-rs +spec: + members: 3 + version: 8.0.10 + type: ReplicaSet + opsManager: + configMapRef: + name: my-project + credentials: my-credentials + security: + authentication: + enabled: true + ignoreUnknownUsers: true + modes: + - SCRAM + agent: + logLevel: DEBUG + statefulSet: + spec: + template: + spec: + containers: + - name: mongodb-enterprise-database + resources: + limits: + cpu: "2" + memory: 2Gi + requests: + cpu: "1" + memory: 1Gi diff --git a/docker/mongodb-kubernetes-tests/tests/search/fixtures/mongodbuser-mdb-admin.yaml b/docker/mongodb-kubernetes-tests/tests/search/fixtures/mongodbuser-mdb-admin.yaml new file mode 100644 index 000000000..0b3fe4c77 --- /dev/null +++ b/docker/mongodb-kubernetes-tests/tests/search/fixtures/mongodbuser-mdb-admin.yaml @@ -0,0 +1,16 @@ +# admin user with root role +apiVersion: mongodb.com/v1 +kind: MongoDBUser +metadata: + name: mdb-admin +spec: + username: mdb-admin + db: admin + mongodbResourceRef: + name: mdb-rs + passwordSecretKeyRef: + name: mdb-admin-user-password + key: password + roles: + - name: root + db: admin \ No newline at end of file diff --git a/docker/mongodb-kubernetes-tests/tests/search/fixtures/mongodbuser-mdb-user.yaml b/docker/mongodb-kubernetes-tests/tests/search/fixtures/mongodbuser-mdb-user.yaml new file mode 100644 index 000000000..579cc58d0 --- /dev/null +++ b/docker/mongodb-kubernetes-tests/tests/search/fixtures/mongodbuser-mdb-user.yaml @@ -0,0 +1,16 @@ +# user performing search queries +apiVersion: mongodb.com/v1 +kind: MongoDBUser +metadata: + name: mdb-user +spec: + username: mdb-user + db: admin + mongodbResourceRef: + name: mdb-rs + passwordSecretKeyRef: + name: mdb-user-password + key: password + roles: + - name: readWrite + db: sample_mflix diff --git a/docker/mongodb-kubernetes-tests/tests/search/fixtures/mongodbuser-search-sync-source-user.yaml b/docker/mongodb-kubernetes-tests/tests/search/fixtures/mongodbuser-search-sync-source-user.yaml new file mode 100644 index 000000000..cd1eab1a5 --- /dev/null +++ b/docker/mongodb-kubernetes-tests/tests/search/fixtures/mongodbuser-search-sync-source-user.yaml @@ -0,0 +1,18 @@ +# user used by MongoDB Search to connect to MongoDB database to synchronize data from +# For MongoDB <8.2, the operator will be creating the searchCoordinator custom role automatically +# From MongoDB 8.2, searchCoordinator role will be a built-in role. +apiVersion: mongodb.com/v1 +kind: MongoDBUser +metadata: + name: search-sync-source-user +spec: + username: search-sync-source + db: admin + mongodbResourceRef: + name: mdb-rs + passwordSecretKeyRef: + name: mdb-rs-search-sync-source-password + key: password + roles: + - name: searchCoordinator + db: admin diff --git a/docker/mongodb-kubernetes-tests/tests/search/fixtures/search-with-user-password.yaml b/docker/mongodb-kubernetes-tests/tests/search/fixtures/search-with-user-password.yaml new file mode 100644 index 000000000..33648e04a --- /dev/null +++ b/docker/mongodb-kubernetes-tests/tests/search/fixtures/search-with-user-password.yaml @@ -0,0 +1,5 @@ +apiVersion: mongodb.com/v1 +kind: MongoDBSearch +metadata: + name: mdbc-rs +spec: {} diff --git a/docker/mongodb-kubernetes-tests/tests/search/search_community_basic.py b/docker/mongodb-kubernetes-tests/tests/search/search_community_basic.py index 69cbca895..97e9c1af7 100644 --- a/docker/mongodb-kubernetes-tests/tests/search/search_community_basic.py +++ b/docker/mongodb-kubernetes-tests/tests/search/search_community_basic.py @@ -12,21 +12,26 @@ logger = test_logger.get_test_logger(__name__) -USER_PASSWORD = "Passw0rd." -USER_NAME = "my-user" +ADMIN_USER_NAME = "mdb-admin-user" +ADMIN_USER_PASSWORD = "mdb-admin-user-pass" + +MONGOT_USER_NAME = "search-sync-source" +MONGOT_USER_PASSWORD = "search-sync-source-user-password" + +USER_NAME = "mdb-user" +USER_PASSWORD = "mdb-user-pass" + MDBC_RESOURCE_NAME = "mdbc-rs" @fixture(scope="function") -def mdbc(namespace: str, custom_mdb_version: str) -> MongoDBCommunity: +def mdbc(namespace: str) -> MongoDBCommunity: resource = MongoDBCommunity.from_yaml( yaml_fixture("community-replicaset-sample-mflix.yaml"), name=MDBC_RESOURCE_NAME, namespace=namespace, ) - resource["spec"]["version"] = custom_mdb_version - if try_load(resource): return resource @@ -53,8 +58,14 @@ def test_install_operator(namespace: str, operator_installation_config: dict[str @mark.e2e_search_community_basic -def test_install_secret(namespace: str): - create_or_update_secret(namespace=namespace, name="my-user-password", data={"password": USER_PASSWORD}) +def test_install_secrets(namespace: str, mdbs: MongoDBSearch): + create_or_update_secret(namespace=namespace, name=f"{USER_NAME}-password", data={"password": USER_PASSWORD}) + create_or_update_secret( + namespace=namespace, name=f"{ADMIN_USER_NAME}-password", data={"password": ADMIN_USER_PASSWORD} + ) + create_or_update_secret( + namespace=namespace, name=f"{mdbs.name}-{MONGOT_USER_NAME}-password", data={"password": MONGOT_USER_PASSWORD} + ) @mark.e2e_search_community_basic @@ -64,7 +75,7 @@ def test_create_database_resource(mdbc: MongoDBCommunity): @mark.e2e_search_community_basic -def test_create_search_resource(mdbs: MongoDBSearch, mdbc: MongoDBCommunity): +def test_create_search_resource(mdbs: MongoDBSearch): mdbs.update() mdbs.assert_reaches_phase(Phase.Running, timeout=300) @@ -76,7 +87,9 @@ def test_wait_for_community_resource_ready(mdbc: MongoDBCommunity): @fixture(scope="function") def sample_movies_helper(mdbc: MongoDBCommunity) -> SampleMoviesSearchHelper: - return movies_search_helper.SampleMoviesSearchHelper(SearchTester(get_connection_string(mdbc))) + return movies_search_helper.SampleMoviesSearchHelper( + SearchTester(get_connection_string(mdbc, USER_NAME, USER_PASSWORD)) + ) @mark.e2e_search_community_basic @@ -89,15 +102,10 @@ def test_search_create_search_index(sample_movies_helper: SampleMoviesSearchHelp sample_movies_helper.create_search_index() -@mark.e2e_search_community_basic -def test_search_wait_for_search_indexes(sample_movies_helper: SampleMoviesSearchHelper): - sample_movies_helper.wait_for_search_indexes() - - @mark.e2e_search_community_basic def test_search_assert_search_query(sample_movies_helper: SampleMoviesSearchHelper): - sample_movies_helper.assert_search_query() + sample_movies_helper.assert_search_query(retry_timeout=60) -def get_connection_string(mdbc: MongoDBCommunity) -> str: - return f"mongodb://{USER_NAME}:{USER_PASSWORD}@{mdbc.name}-0.{mdbc.name}-svc.{mdbc.namespace}.svc.cluster.local:27017/?replicaSet={mdbc.name}" +def get_connection_string(mdbc: MongoDBCommunity, user_name: str, user_password: str) -> str: + return f"mongodb://{user_name}:{user_password}@{mdbc.name}-0.{mdbc.name}-svc.{mdbc.namespace}.svc.cluster.local:27017/?replicaSet={mdbc.name}" diff --git a/docker/mongodb-kubernetes-tests/tests/search/search_community_external_mongod_basic.py b/docker/mongodb-kubernetes-tests/tests/search/search_community_external_mongod_basic.py new file mode 100644 index 000000000..110e134dc --- /dev/null +++ b/docker/mongodb-kubernetes-tests/tests/search/search_community_external_mongod_basic.py @@ -0,0 +1,144 @@ +from kubetester import create_or_update_secret, try_load +from kubetester.kubetester import fixture as yaml_fixture +from kubetester.mongodb_community import MongoDBCommunity +from kubetester.mongodb_search import MongoDBSearch +from kubetester.phase import Phase +from pytest import fixture, mark +from tests import test_logger +from tests.common.search import movies_search_helper +from tests.common.search.movies_search_helper import SampleMoviesSearchHelper +from tests.common.search.search_tester import SearchTester +from tests.conftest import get_default_operator + +logger = test_logger.get_test_logger(__name__) + +ADMIN_USER_NAME = "mdb-admin-user" +ADMIN_USER_PASSWORD = "mdb-admin-user-pass" + +MONGOT_USER_NAME = "search-sync-source" +MONGOT_USER_PASSWORD = "search-sync-source-password" + +USER_NAME = "mdb-user" +USER_PASSWORD = "mdb-user-pass" + +MDBC_RESOURCE_NAME = "mdbc-rs" +MDBS_RESOURCE_NAME = "mdbs" + + +@fixture(scope="function") +def mdbc(namespace: str) -> MongoDBCommunity: + resource = MongoDBCommunity.from_yaml( + yaml_fixture("community-replicaset-sample-mflix-external.yaml"), + name=MDBC_RESOURCE_NAME, + namespace=namespace, + ) + + if try_load(resource): + return resource + + mongot_host = f"{MDBS_RESOURCE_NAME}-search-svc.{namespace}.svc.cluster.local:27027" + if "additionalMongodConfig" not in resource["spec"]: + resource["spec"]["additionalMongodConfig"] = {} + if "setParameter" not in resource["spec"]["additionalMongodConfig"]: + resource["spec"]["additionalMongodConfig"]["setParameter"] = {} + + resource["spec"]["additionalMongodConfig"]["setParameter"].update( + { + "mongotHost": mongot_host, + "searchIndexManagementHostAndPort": mongot_host, + "skipAuthenticationToSearchIndexManagementServer": False, + "searchTLSMode": "disabled", + } + ) + + return resource + + +@fixture(scope="function") +def mdbs(namespace: str, mdbc: MongoDBCommunity) -> MongoDBSearch: + resource = MongoDBSearch.from_yaml( + yaml_fixture("search-minimal.yaml"), + name=MDBS_RESOURCE_NAME, + namespace=namespace, + ) + + seeds = [ + f"{mdbc.name}-{i}.{mdbc.name}-svc.{namespace}.svc.cluster.local:27017" for i in range(mdbc["spec"]["members"]) + ] + + resource["spec"] = { + "source": { + "external": { + "hostAndPorts": seeds, + "keyFileSecretRef": {"name": f"{mdbc.name}-keyfile", "key": "keyfile"}, + "tls": {"enabled": False}, + }, + "passwordSecretRef": {"name": f"{MDBC_RESOURCE_NAME}-{MONGOT_USER_NAME}-password", "key": "password"}, + "username": MONGOT_USER_NAME, + } + } + + return resource + + +@mark.e2e_search_external_basic +def test_install_operator(namespace: str, operator_installation_config: dict[str, str]): + operator = get_default_operator(namespace, operator_installation_config=operator_installation_config) + operator.assert_is_running() + + +@mark.e2e_search_external_basic +def test_install_secrets(namespace: str, mdbs: MongoDBSearch): + create_or_update_secret(namespace=namespace, name=f"{USER_NAME}-password", data={"password": USER_PASSWORD}) + create_or_update_secret( + namespace=namespace, name=f"{ADMIN_USER_NAME}-password", data={"password": ADMIN_USER_PASSWORD} + ) + + create_or_update_secret( + namespace=namespace, + name=f"{MDBC_RESOURCE_NAME}-{MONGOT_USER_NAME}-password", + data={"password": MONGOT_USER_PASSWORD}, + ) + + +@mark.e2e_search_external_basic +def test_create_database_resource(mdbc: MongoDBCommunity): + mdbc.update() + mdbc.assert_reaches_phase(Phase.Running, timeout=1000) + + +@mark.e2e_search_external_basic +def test_create_search_resource(mdbs: MongoDBSearch, mdbc: MongoDBCommunity): + mdbs.update() + mdbs.assert_reaches_phase(Phase.Running, timeout=1000) + + +@mark.e2e_search_external_basic +def test_wait_for_community_resource_ready(mdbc: MongoDBCommunity): + mdbc.assert_reaches_phase(Phase.Running, timeout=1800) + + +@fixture(scope="function") +def sample_movies_helper(mdbc: MongoDBCommunity) -> SampleMoviesSearchHelper: + return movies_search_helper.SampleMoviesSearchHelper( + SearchTester(get_connection_string(mdbc, USER_NAME, USER_PASSWORD)) + ) + + +@mark.e2e_search_external_basic +def test_search_restore_sample_database(sample_movies_helper: SampleMoviesSearchHelper): + sample_movies_helper.restore_sample_database() + + +@mark.e2e_search_external_basic +def test_search_create_search_index(sample_movies_helper: SampleMoviesSearchHelper): + sample_movies_helper.create_search_index() + + +@mark.e2e_search_external_basic +def test_search_assert_search_query(sample_movies_helper: SampleMoviesSearchHelper): + sample_movies_helper.assert_search_query(retry_timeout=60) + + +def get_connection_string(mdbc: MongoDBCommunity, user_name: str, user_password: str) -> str: + return f"mongodb://{user_name}:{user_password}@{mdbc.name}-0.{mdbc.name}-svc.{mdbc.namespace}.svc.cluster.local:27017/?replicaSet={mdbc.name}" diff --git a/docker/mongodb-kubernetes-tests/tests/search/search_community_external_mongod_tls.py b/docker/mongodb-kubernetes-tests/tests/search/search_community_external_mongod_tls.py new file mode 100644 index 000000000..f6a270a48 --- /dev/null +++ b/docker/mongodb-kubernetes-tests/tests/search/search_community_external_mongod_tls.py @@ -0,0 +1,190 @@ +from kubetester import create_or_update_secret, try_load +from kubetester.certs import create_tls_certs +from kubetester.kubetester import fixture as yaml_fixture +from kubetester.mongodb_community import MongoDBCommunity +from kubetester.mongodb_search import MongoDBSearch +from kubetester.phase import Phase +from pytest import fixture, mark +from tests import test_logger +from tests.common.search import movies_search_helper +from tests.common.search.movies_search_helper import SampleMoviesSearchHelper +from tests.common.search.search_tester import SearchTester +from tests.conftest import get_default_operator + +logger = test_logger.get_test_logger(__name__) + +ADMIN_USER_NAME = "mdb-admin-user" +ADMIN_USER_PASSWORD = "mdb-admin-user-pass" + +MONGOT_USER_NAME = "search-sync-source" +MONGOT_USER_PASSWORD = "search-sync-source-user-password" + +USER_NAME = "mdb-user" +USER_PASSWORD = "mdb-user-pass" + +MDBC_RESOURCE_NAME = "mdbc-rs" +MDBS_RESOURCE_NAME = "mdbs" +TLS_SECRET_NAME = "tls-secret" +TLS_CA_SECRET_NAME = "tls-ca-secret" +MDBS_TLS_SECRET_NAME = "mdbs-tls-secret" + + +@fixture(scope="function") +def mdbc(namespace: str) -> MongoDBCommunity: + resource = MongoDBCommunity.from_yaml( + yaml_fixture("community-replicaset-sample-mflix-external.yaml"), + name=MDBC_RESOURCE_NAME, + namespace=namespace, + ) + + if try_load(resource): + return resource + + mongot_host = f"{MDBS_RESOURCE_NAME}-search-svc.{namespace}.svc.cluster.local:27027" + if "additionalMongodConfig" not in resource["spec"]: + resource["spec"]["additionalMongodConfig"] = {} + if "setParameter" not in resource["spec"]["additionalMongodConfig"]: + resource["spec"]["additionalMongodConfig"]["setParameter"] = {} + + resource["spec"]["additionalMongodConfig"]["setParameter"].update( + { + "mongotHost": mongot_host, + "searchIndexManagementHostAndPort": mongot_host, + "skipAuthenticationToSearchIndexManagementServer": False, + "searchTLSMode": "requireTLS", + } + ) + + resource["spec"]["security"]["tls"] = { + "enabled": True, + "certificateKeySecretRef": {"name": TLS_SECRET_NAME}, + "caCertificateSecretRef": {"name": TLS_SECRET_NAME}, + } + + return resource + + +@fixture(scope="function") +def mdbs(namespace: str, mdbc: MongoDBCommunity) -> MongoDBSearch: + resource = MongoDBSearch.from_yaml( + yaml_fixture("search-minimal.yaml"), + name=MDBS_RESOURCE_NAME, + namespace=namespace, + ) + + if try_load(resource): + return resource + + return resource + + +@mark.e2e_search_external_tls +def test_install_operator(namespace: str, operator_installation_config: dict[str, str]): + operator = get_default_operator(namespace, operator_installation_config=operator_installation_config) + operator.assert_is_running() + + +@mark.e2e_search_external_tls +def test_install_secrets(namespace: str, mdbs: MongoDBSearch): + create_or_update_secret(namespace=namespace, name=f"{USER_NAME}-password", data={"password": USER_PASSWORD}) + create_or_update_secret( + namespace=namespace, name=f"{ADMIN_USER_NAME}-password", data={"password": ADMIN_USER_PASSWORD} + ) + create_or_update_secret( + namespace=namespace, + name=f"{MDBC_RESOURCE_NAME}-{MONGOT_USER_NAME}-password", + data={"password": MONGOT_USER_PASSWORD}, + ) + + +@mark.e2e_search_external_tls +def test_install_tls_secrets_and_configmaps( + namespace: str, mdbc: MongoDBCommunity, mdbs: MongoDBSearch, issuer: str, issuer_ca_filepath: str +): + create_tls_certs(issuer, namespace, mdbc.name, mdbc["spec"]["members"], secret_name=TLS_SECRET_NAME) + + search_service_name = f"{mdbs.name}-search-svc" + create_tls_certs( + issuer, + namespace, + f"{mdbs.name}-search", + replicas=1, + service_name=search_service_name, + additional_domains=[f"{search_service_name}.{namespace}.svc.cluster.local"], + secret_name=MDBS_TLS_SECRET_NAME, + ) + + ca = open(issuer_ca_filepath).read() + + ca_secret_name = f"{mdbc.name}-ca" + create_or_update_secret(namespace=namespace, name=ca_secret_name, data={"ca.crt": ca}) + + +@mark.e2e_search_external_tls +def test_create_database_resource(mdbc: MongoDBCommunity): + mdbc.update() + mdbc.assert_reaches_phase(Phase.Running, timeout=1000) + + +@mark.e2e_search_external_tls +def test_create_search_resource(mdbs: MongoDBSearch, mdbc: MongoDBCommunity): + seeds = [ + f"{mdbc.name}-{i}.{mdbc.name}-svc.{mdbc.namespace}.svc.cluster.local:27017" + for i in range(mdbc["spec"]["members"]) + ] + + mdbs["spec"]["source"] = { + "external": { + "hostAndPorts": seeds, + "keyFileSecretRef": {"name": f"{mdbc.name}-keyfile"}, + "tls": { + "enabled": True, + "ca": {"name": f"{mdbc.name}-ca"}, + }, + }, + "passwordSecretRef": {"name": f"{MDBC_RESOURCE_NAME}-{MONGOT_USER_NAME}-password", "key": "password"}, + "username": MONGOT_USER_NAME, + } + + mdbs["spec"]["security"] = {"tls": {"enabled": True, "certificateKeySecretRef": {"name": MDBS_TLS_SECRET_NAME}}} + + mdbs.update() + mdbs.assert_reaches_phase(Phase.Running, timeout=300) + + +@mark.e2e_search_external_tls +def test_wait_for_community_resource_ready(mdbc: MongoDBCommunity): + mdbc.assert_reaches_phase(Phase.Running, timeout=1800) + + +@fixture(scope="function") +def sample_movies_helper(mdbc: MongoDBCommunity, issuer_ca_filepath: str) -> SampleMoviesSearchHelper: + return movies_search_helper.SampleMoviesSearchHelper( + SearchTester( + get_connection_string(mdbc, USER_NAME, USER_PASSWORD), + use_ssl=True, + ca_path=issuer_ca_filepath, + ) + ) + + +@mark.e2e_search_external_tls +def test_search_restore_sample_database(sample_movies_helper: SampleMoviesSearchHelper): + sample_movies_helper.restore_sample_database() + + +@mark.e2e_search_external_tls +def test_search_create_search_index(sample_movies_helper: SampleMoviesSearchHelper): + sample_movies_helper.create_search_index() + + +@mark.e2e_search_external_tls +def test_search_assert_search_query(sample_movies_helper: SampleMoviesSearchHelper): + sample_movies_helper.assert_search_query(retry_timeout=60) + + +def get_connection_string(mdbc: MongoDBCommunity, user_name: str, user_password: str) -> str: + return ( + f"mongodb://{user_name}:{user_password}@{mdbc.name}-0.{mdbc.name}-svc.{mdbc.namespace}.svc.cluster.local:27017/" + f"?replicaSet={mdbc.name}" + ) diff --git a/docker/mongodb-kubernetes-tests/tests/search/search_community_tls.py b/docker/mongodb-kubernetes-tests/tests/search/search_community_tls.py new file mode 100644 index 000000000..44418b793 --- /dev/null +++ b/docker/mongodb-kubernetes-tests/tests/search/search_community_tls.py @@ -0,0 +1,173 @@ +import pymongo +from kubetester import create_or_update_secret, try_load +from kubetester.certs import create_tls_certs +from kubetester.kubetester import fixture as yaml_fixture +from kubetester.mongodb_community import MongoDBCommunity +from kubetester.mongodb_search import MongoDBSearch +from kubetester.phase import Phase +from pytest import fixture, mark +from tests import test_logger +from tests.common.search import movies_search_helper +from tests.common.search.movies_search_helper import SampleMoviesSearchHelper +from tests.common.search.search_tester import SearchTester +from tests.conftest import get_default_operator + +logger = test_logger.get_test_logger(__name__) + +ADMIN_USER_NAME = "mdb-admin-user" +ADMIN_USER_PASSWORD = "mdb-admin-user-pass" + +MONGOT_USER_NAME = "search-sync-source" +MONGOT_USER_PASSWORD = "search-sync-source-user-password" + +USER_NAME = "mdb-user" +USER_PASSWORD = "mdb-user-pass" + +MDBC_RESOURCE_NAME = "mdbc-rs" + +TLS_SECRET_NAME = "tls-secret" + +# MongoDBSearch TLS configuration +MDBS_TLS_SECRET_NAME = "mdbs-tls-secret" + + +@fixture(scope="function") +def mdbc(namespace: str) -> MongoDBCommunity: + resource = MongoDBCommunity.from_yaml( + yaml_fixture("community-replicaset-sample-mflix.yaml"), + name=MDBC_RESOURCE_NAME, + namespace=namespace, + ) + + if try_load(resource): + return resource + + # Add TLS configuration + resource["spec"]["security"]["tls"] = { + "enabled": True, + "certificateKeySecretRef": {"name": TLS_SECRET_NAME}, + "caCertificateSecretRef": {"name": TLS_SECRET_NAME}, + } + + return resource + + +@fixture(scope="function") +def mdbs(namespace: str) -> MongoDBSearch: + resource = MongoDBSearch.from_yaml( + yaml_fixture("search-minimal.yaml"), + namespace=namespace, + ) + + if try_load(resource): + return resource + + # Add TLS configuration to MongoDBSearch + if "spec" not in resource: + resource["spec"] = {} + + resource["spec"]["security"] = {"tls": {"enabled": True, "certificateKeySecretRef": {"name": MDBS_TLS_SECRET_NAME}}} + + return resource + + +@mark.e2e_search_community_tls +def test_install_operator(namespace: str, operator_installation_config: dict[str, str]): + operator = get_default_operator(namespace, operator_installation_config=operator_installation_config) + operator.assert_is_running() + + +@mark.e2e_search_community_tls +def test_install_secrets(namespace: str, mdbs: MongoDBSearch): + # Create user password secrets + create_or_update_secret(namespace=namespace, name=f"{USER_NAME}-password", data={"password": USER_PASSWORD}) + create_or_update_secret( + namespace=namespace, name=f"{ADMIN_USER_NAME}-password", data={"password": ADMIN_USER_PASSWORD} + ) + create_or_update_secret( + namespace=namespace, name=f"{mdbs.name}-{MONGOT_USER_NAME}-password", data={"password": MONGOT_USER_PASSWORD} + ) + + +@mark.e2e_search_community_tls +def test_install_tls_secrets_and_configmaps(namespace: str, mdbc: MongoDBCommunity, mdbs: MongoDBSearch, issuer: str): + create_tls_certs(issuer, namespace, mdbc.name, mdbc["spec"]["members"], secret_name=TLS_SECRET_NAME) + + search_service_name = f"{mdbs.name}-search-svc" + create_tls_certs( + issuer, + namespace, + f"{mdbs.name}-search", + replicas=1, + service_name=search_service_name, + additional_domains=[f"{search_service_name}.{namespace}.svc.cluster.local"], + secret_name=MDBS_TLS_SECRET_NAME, + ) + + +@mark.e2e_search_community_tls +def test_create_database_resource(mdbc: MongoDBCommunity): + mdbc.update() + mdbc.assert_reaches_phase(Phase.Running, timeout=1000) + + +@mark.e2e_search_community_tls +def test_create_search_resource(mdbs: MongoDBSearch): + mdbs.update() + mdbs.assert_reaches_phase(Phase.Running, timeout=300) + + +@mark.e2e_search_community_tls +def test_wait_for_community_resource_ready(mdbc: MongoDBCommunity): + mdbc.assert_reaches_phase(Phase.Running, timeout=1800) + + +@mark.e2e_search_community_tls +def test_validate_tls_connections(mdbc: MongoDBCommunity, mdbs: MongoDBSearch, namespace: str, issuer_ca_filepath: str): + with pymongo.MongoClient( + f"mongodb://{mdbc.name}-0.{mdbc.name}-svc.{namespace}.svc.cluster.local:27017/?replicaSet={mdbc.name}", + tls=True, + tlsCAFile=issuer_ca_filepath, + tlsAllowInvalidHostnames=False, + serverSelectionTimeoutMS=30000, + connectTimeoutMS=20000, + ) as mongodb_client: + mongodb_info = mongodb_client.admin.command("hello") + assert mongodb_info.get("ok") == 1, "MongoDBCommunity connection failed" + + with pymongo.MongoClient( + f"mongodb://{mdbs.name}-search-svc.{namespace}.svc.cluster.local:27027", + tls=True, + tlsCAFile=issuer_ca_filepath, + tlsAllowInvalidHostnames=False, + serverSelectionTimeoutMS=10000, + connectTimeoutMS=10000, + ) as search_client: + search_info = search_client.admin.command("hello") + assert search_info.get("ok") == 1, "MongoDBSearch connection failed" + + +@fixture(scope="function") +def sample_movies_helper(mdbc: MongoDBCommunity, issuer_ca_filepath: str) -> SampleMoviesSearchHelper: + return movies_search_helper.SampleMoviesSearchHelper( + SearchTester(get_connection_string(mdbc, USER_NAME, USER_PASSWORD), use_ssl=True, ca_path=issuer_ca_filepath), + ) + + +@mark.e2e_search_community_tls +def test_search_restore_sample_database(sample_movies_helper: SampleMoviesSearchHelper): + sample_movies_helper.restore_sample_database() + + +@mark.e2e_search_community_tls +def test_search_create_search_index(sample_movies_helper: SampleMoviesSearchHelper): + sample_movies_helper.create_search_index() + + +@mark.e2e_search_community_tls +def test_search_assert_search_query(sample_movies_helper: SampleMoviesSearchHelper): + sample_movies_helper.assert_search_query(retry_timeout=60) + + +def get_connection_string(mdbc: MongoDBCommunity, user_name: str, user_password: str) -> str: + return f"mongodb://{user_name}:{user_password}@{mdbc.name}-0.{mdbc.name}-svc.{mdbc.namespace}.svc.cluster.local:27017/?replicaSet={mdbc.name}" diff --git a/docker/mongodb-kubernetes-tests/tests/search/search_enterprise_basic.py b/docker/mongodb-kubernetes-tests/tests/search/search_enterprise_basic.py new file mode 100644 index 000000000..a3de8dcf8 --- /dev/null +++ b/docker/mongodb-kubernetes-tests/tests/search/search_enterprise_basic.py @@ -0,0 +1,183 @@ +import yaml +from kubetester import create_or_update_secret, try_load +from kubetester.kubetester import KubernetesTester +from kubetester.kubetester import fixture as yaml_fixture +from kubetester.mongodb import MongoDB +from kubetester.mongodb_search import MongoDBSearch +from kubetester.mongodb_user import MongoDBUser +from kubetester.phase import Phase +from pytest import fixture, mark +from tests import test_logger +from tests.common.search import movies_search_helper +from tests.common.search.movies_search_helper import SampleMoviesSearchHelper +from tests.common.search.search_tester import SearchTester +from tests.conftest import get_default_operator + +logger = test_logger.get_test_logger(__name__) + +ADMIN_USER_NAME = "mdb-admin-user" +ADMIN_USER_PASSWORD = f"{ADMIN_USER_NAME}-password" + +MONGOT_USER_NAME = "search-sync-source" +MONGOT_USER_PASSWORD = f"{MONGOT_USER_NAME}-password" + +USER_NAME = "mdb-user" +USER_PASSWORD = f"{USER_NAME}-password" + +MDB_RESOURCE_NAME = "mdb-rs" + + +@fixture(scope="function") +def mdb(namespace: str) -> MongoDB: + resource = MongoDB.from_yaml( + yaml_fixture("enterprise-replicaset-sample-mflix.yaml"), + name=MDB_RESOURCE_NAME, + namespace=namespace, + ) + + if try_load(resource): + return resource + + return resource + + +@fixture(scope="function") +def mdbs(namespace: str) -> MongoDBSearch: + resource = MongoDBSearch.from_yaml(yaml_fixture("search-minimal.yaml"), namespace=namespace, name=MDB_RESOURCE_NAME) + + if try_load(resource): + return resource + + return resource + + +@fixture(scope="function") +def admin_user(namespace: str) -> MongoDBUser: + resource = MongoDBUser.from_yaml( + yaml_fixture("mongodbuser-mdb-admin.yaml"), namespace=namespace, name=ADMIN_USER_NAME + ) + + if try_load(resource): + return resource + + resource["spec"]["username"] = resource.name + resource["spec"]["passwordSecretKeyRef"]["name"] = f"{resource.name}-password" + + return resource + + +@fixture(scope="function") +def user(namespace: str) -> MongoDBUser: + resource = MongoDBUser.from_yaml(yaml_fixture("mongodbuser-mdb-user.yaml"), namespace=namespace, name=USER_NAME) + + if try_load(resource): + return resource + + resource["spec"]["username"] = resource.name + resource["spec"]["passwordSecretKeyRef"]["name"] = f"{resource.name}-password" + + return resource + + +@fixture(scope="function") +def mongot_user(namespace: str, mdbs: MongoDBSearch) -> MongoDBUser: + resource = MongoDBUser.from_yaml( + yaml_fixture("mongodbuser-search-sync-source-user.yaml"), + namespace=namespace, + name=f"{mdbs.name}-{MONGOT_USER_NAME}", + ) + + if try_load(resource): + return resource + + resource["spec"]["username"] = MONGOT_USER_NAME + resource["spec"]["passwordSecretKeyRef"]["name"] = f"{resource.name}-password" + + return resource + + +@mark.e2e_search_enterprise_basic +def test_install_operator(namespace: str, operator_installation_config: dict[str, str]): + operator = get_default_operator(namespace, operator_installation_config=operator_installation_config) + operator.assert_is_running() + + +@mark.e2e_search_enterprise_basic +def test_create_database_resource(mdb: MongoDB): + mdb.update() + mdb.assert_reaches_phase(Phase.Running, timeout=1000) + + +@mark.e2e_search_enterprise_basic +def test_create_users( + namespace: str, admin_user: MongoDBUser, user: MongoDBUser, mongot_user: MongoDBUser, mdb: MongoDB +): + create_or_update_secret( + namespace, name=admin_user["spec"]["passwordSecretKeyRef"]["name"], data={"password": ADMIN_USER_PASSWORD} + ) + admin_user.create() + admin_user.assert_reaches_phase(Phase.Updated, timeout=300) + + create_or_update_secret( + namespace, name=user["spec"]["passwordSecretKeyRef"]["name"], data={"password": USER_PASSWORD} + ) + user.create() + user.assert_reaches_phase(Phase.Updated, timeout=300) + + create_or_update_secret( + namespace, name=mongot_user["spec"]["passwordSecretKeyRef"]["name"], data={"password": MONGOT_USER_PASSWORD} + ) + mongot_user.create() + # we deliberately don't wait for this user to be ready, because to be reconciled successfully it needs the searchCoordinator role + # which the ReplicaSet reconciler will only define in the automation config after the MongoDBSearch resource is created. + + +@mark.e2e_search_enterprise_basic +def test_create_search_resource(mdbs: MongoDBSearch): + mdbs.update() + mdbs.assert_reaches_phase(Phase.Running, timeout=300) + + +@mark.e2e_search_enterprise_basic +def test_wait_for_database_resource_ready(mdb: MongoDB): + mdb.assert_abandons_phase(Phase.Running, timeout=1800) + mdb.assert_reaches_phase(Phase.Running, timeout=1800) + + for idx in range(mdb.get_members()): + mongod_config = yaml.safe_load( + KubernetesTester.run_command_in_pod_container( + f"{mdb.name}-{idx}", mdb.namespace, ["cat", "/data/automation-mongod.conf"] + ) + ) + setParameter = mongod_config.get("setParameter", {}) + assert ( + "mongotHost" in setParameter and "searchIndexManagementHostAndPort" in setParameter + ), "mongot parameters not found in mongod config" + + +@mark.e2e_search_enterprise_basic +def test_search_restore_sample_database(mdb: MongoDB): + sample_movies_helper = movies_search_helper.SampleMoviesSearchHelper( + SearchTester(get_connection_string(mdb, ADMIN_USER_NAME, ADMIN_USER_PASSWORD)) + ) + sample_movies_helper.restore_sample_database() + + +@mark.e2e_search_enterprise_basic +def test_search_create_search_index(mdb: MongoDB): + sample_movies_helper = movies_search_helper.SampleMoviesSearchHelper( + SearchTester(get_connection_string(mdb, USER_NAME, USER_PASSWORD)) + ) + sample_movies_helper.create_search_index() + + +@mark.e2e_search_enterprise_basic +def test_search_assert_search_query(mdb: MongoDB): + sample_movies_helper = movies_search_helper.SampleMoviesSearchHelper( + SearchTester(get_connection_string(mdb, USER_NAME, USER_PASSWORD)) + ) + sample_movies_helper.assert_search_query(retry_timeout=60) + + +def get_connection_string(mdb: MongoDB, user_name: str, user_password: str) -> str: + return f"mongodb://{user_name}:{user_password}@{mdb.name}-0.{mdb.name}-svc.{mdb.namespace}.svc.cluster.local:27017/?replicaSet={mdb.name}" diff --git a/docker/mongodb-kubernetes-tests/tests/search/search_enterprise_tls.py b/docker/mongodb-kubernetes-tests/tests/search/search_enterprise_tls.py new file mode 100644 index 000000000..f4c0bc96f --- /dev/null +++ b/docker/mongodb-kubernetes-tests/tests/search/search_enterprise_tls.py @@ -0,0 +1,239 @@ +import pymongo +import yaml +from kubetester import create_or_update_secret, try_load +from kubetester.certs import create_mongodb_tls_certs, create_tls_certs +from kubetester.kubetester import KubernetesTester +from kubetester.kubetester import fixture as yaml_fixture +from kubetester.mongodb import MongoDB +from kubetester.mongodb_search import MongoDBSearch +from kubetester.mongodb_user import MongoDBUser +from kubetester.phase import Phase +from pytest import fixture, mark +from tests import test_logger +from tests.common.search import movies_search_helper +from tests.common.search.movies_search_helper import SampleMoviesSearchHelper +from tests.common.search.search_tester import SearchTester +from tests.conftest import get_default_operator + +logger = test_logger.get_test_logger(__name__) + +ADMIN_USER_NAME = "mdb-admin-user" +ADMIN_USER_PASSWORD = f"{ADMIN_USER_NAME}-password" + +MONGOT_USER_NAME = "search-sync-source" +MONGOT_USER_PASSWORD = f"{MONGOT_USER_NAME}-password" + +USER_NAME = "mdb-user" +USER_PASSWORD = f"{USER_NAME}-password" + +MDB_RESOURCE_NAME = "mdb-rs" + +# MongoDBSearch TLS configuration +MDBS_TLS_SECRET_NAME = "mdbs-tls-secret" + + +@fixture(scope="function") +def mdb(namespace: str, issuer_ca_configmap: str) -> MongoDB: + resource = MongoDB.from_yaml( + yaml_fixture("enterprise-replicaset-sample-mflix.yaml"), + name=MDB_RESOURCE_NAME, + namespace=namespace, + ) + + if try_load(resource): + return resource + + resource.configure_custom_tls(issuer_ca_configmap, "certs") + + return resource + + +@fixture(scope="function") +def mdbs(namespace: str) -> MongoDBSearch: + resource = MongoDBSearch.from_yaml(yaml_fixture("search-minimal.yaml"), namespace=namespace, name=MDB_RESOURCE_NAME) + + if try_load(resource): + return resource + + # Add TLS configuration to MongoDBSearch + if "spec" not in resource: + resource["spec"] = {} + + resource["spec"]["security"] = {"tls": {"enabled": True, "certificateKeySecretRef": {"name": MDBS_TLS_SECRET_NAME}}} + + return resource + + +@fixture(scope="function") +def admin_user(namespace: str) -> MongoDBUser: + resource = MongoDBUser.from_yaml( + yaml_fixture("mongodbuser-mdb-admin.yaml"), namespace=namespace, name=ADMIN_USER_NAME + ) + + if try_load(resource): + return resource + + resource["spec"]["username"] = resource.name + resource["spec"]["passwordSecretKeyRef"]["name"] = f"{resource.name}-password" + + return resource + + +@fixture(scope="function") +def user(namespace: str) -> MongoDBUser: + resource = MongoDBUser.from_yaml(yaml_fixture("mongodbuser-mdb-user.yaml"), namespace=namespace, name=USER_NAME) + + if try_load(resource): + return resource + + resource["spec"]["username"] = resource.name + resource["spec"]["passwordSecretKeyRef"]["name"] = f"{resource.name}-password" + + return resource + + +@fixture(scope="function") +def mongot_user(namespace: str, mdbs: MongoDBSearch) -> MongoDBUser: + resource = MongoDBUser.from_yaml( + yaml_fixture("mongodbuser-search-sync-source-user.yaml"), + namespace=namespace, + name=f"{mdbs.name}-{MONGOT_USER_NAME}", + ) + + if try_load(resource): + return resource + + resource["spec"]["username"] = MONGOT_USER_NAME + resource["spec"]["passwordSecretKeyRef"]["name"] = f"{resource.name}-password" + + return resource + + +@mark.e2e_search_enterprise_tls +def test_install_operator(namespace: str, operator_installation_config: dict[str, str]): + operator = get_default_operator(namespace, operator_installation_config=operator_installation_config) + operator.assert_is_running() + + +@mark.e2e_search_enterprise_tls +def test_install_tls_secrets_and_configmaps(namespace: str, mdb: MongoDB, mdbs: MongoDBSearch, issuer: str): + create_mongodb_tls_certs(issuer, namespace, mdb.name, f"certs-{mdb.name}-cert", mdb.get_members()) + + search_service_name = f"{mdbs.name}-search-svc" + create_tls_certs( + issuer, + namespace, + f"{mdbs.name}-search", + replicas=1, + service_name=search_service_name, + additional_domains=[f"{search_service_name}.{namespace}.svc.cluster.local"], + secret_name=MDBS_TLS_SECRET_NAME, + ) + + +@mark.e2e_search_enterprise_tls +def test_create_database_resource(mdb: MongoDB): + mdb.update() + mdb.assert_reaches_phase(Phase.Running, timeout=1000) + + +@mark.e2e_search_enterprise_tls +def test_create_users( + namespace: str, admin_user: MongoDBUser, user: MongoDBUser, mongot_user: MongoDBUser, mdb: MongoDB +): + create_or_update_secret( + namespace, name=admin_user["spec"]["passwordSecretKeyRef"]["name"], data={"password": ADMIN_USER_PASSWORD} + ) + admin_user.create() + admin_user.assert_reaches_phase(Phase.Updated, timeout=300) + + create_or_update_secret( + namespace, name=user["spec"]["passwordSecretKeyRef"]["name"], data={"password": USER_PASSWORD} + ) + user.create() + user.assert_reaches_phase(Phase.Updated, timeout=300) + + create_or_update_secret( + namespace, name=mongot_user["spec"]["passwordSecretKeyRef"]["name"], data={"password": MONGOT_USER_PASSWORD} + ) + mongot_user.create() + # we deliberately don't wait for this user to be ready, because to be reconciled successfully it needs the searchCoordinator role + # which the ReplicaSet reconciler will only define in the automation config after the MongoDBSearch resource is created. + + +@mark.e2e_search_enterprise_tls +def test_create_search_resource(mdbs: MongoDBSearch): + mdbs.update() + mdbs.assert_reaches_phase(Phase.Running, timeout=300) + + +@mark.e2e_search_enterprise_tls +def test_wait_for_database_resource_ready(mdb: MongoDB): + mdb.assert_abandons_phase(Phase.Running, timeout=1800) + mdb.assert_reaches_phase(Phase.Running, timeout=1800) + + for idx in range(mdb.get_members()): + mongod_config = yaml.safe_load( + KubernetesTester.run_command_in_pod_container( + f"{mdb.name}-{idx}", mdb.namespace, ["cat", "/data/automation-mongod.conf"] + ) + ) + setParameter = mongod_config.get("setParameter", {}) + assert ( + "mongotHost" in setParameter and "searchIndexManagementHostAndPort" in setParameter + ), "mongot parameters not found in mongod config" + + +@mark.e2e_search_enterprise_tls +def test_validate_tls_connections(mdb: MongoDB, mdbs: MongoDBSearch, namespace: str, issuer_ca_filepath: str): + with pymongo.MongoClient( + f"mongodb://{mdb.name}-0.{mdb.name}-svc.{namespace}.svc.cluster.local:27017/?replicaSet={mdb.name}", + tls=True, + tlsCAFile=issuer_ca_filepath, + tlsAllowInvalidHostnames=False, + serverSelectionTimeoutMS=30000, + connectTimeoutMS=20000, + ) as mongodb_client: + mongodb_info = mongodb_client.admin.command("hello") + assert mongodb_info.get("ok") == 1, "MongoDB connection failed" + + with pymongo.MongoClient( + f"mongodb://{mdbs.name}-search-svc.{namespace}.svc.cluster.local:27027", + tls=True, + tlsCAFile=issuer_ca_filepath, + tlsAllowInvalidHostnames=False, + serverSelectionTimeoutMS=10000, + connectTimeoutMS=10000, + ) as search_client: + search_info = search_client.admin.command("hello") + assert search_info.get("ok") == 1, "MongoDBSearch connection failed" + + +@mark.e2e_search_enterprise_tls +def test_search_restore_sample_database(mdb: MongoDB, issuer_ca_filepath: str): + sample_movies_helper = movies_search_helper.SampleMoviesSearchHelper( + SearchTester( + get_connection_string(mdb, ADMIN_USER_NAME, ADMIN_USER_PASSWORD), use_ssl=True, ca_path=issuer_ca_filepath + ) + ) + sample_movies_helper.restore_sample_database() + + +@mark.e2e_search_enterprise_tls +def test_search_create_search_index(mdb: MongoDB, issuer_ca_filepath: str): + sample_movies_helper = movies_search_helper.SampleMoviesSearchHelper( + SearchTester(get_connection_string(mdb, USER_NAME, USER_PASSWORD), use_ssl=True, ca_path=issuer_ca_filepath) + ) + sample_movies_helper.create_search_index() + + +@mark.e2e_search_enterprise_tls +def test_search_assert_search_query(mdb: MongoDB, issuer_ca_filepath: str): + sample_movies_helper = movies_search_helper.SampleMoviesSearchHelper( + SearchTester(get_connection_string(mdb, USER_NAME, USER_PASSWORD), use_ssl=True, ca_path=issuer_ca_filepath) + ) + sample_movies_helper.assert_search_query(retry_timeout=60) + + +def get_connection_string(mdb: MongoDB, user_name: str, user_password: str) -> str: + return f"mongodb://{user_name}:{user_password}@{mdb.name}-0.{mdb.name}-svc.{mdb.namespace}.svc.cluster.local:27017/?replicaSet={mdb.name}" diff --git a/helm_chart/crds/mongodb.com_mongodbsearch.yaml b/helm_chart/crds/mongodb.com_mongodbsearch.yaml index 72ea0e50e..38dc38f2a 100644 --- a/helm_chart/crds/mongodb.com_mongodbsearch.yaml +++ b/helm_chart/crds/mongodb.com_mongodbsearch.yaml @@ -49,6 +49,8 @@ spec: spec: properties: persistence: + description: Configure MongoDB Search's persistent volume. If not + defined, the operator will request 10GB of storage. properties: multiple: properties: @@ -95,7 +97,8 @@ spec: type: object type: object resourceRequirements: - description: ResourceRequirements describes the compute resource requirements. + description: Configure resource requests and limits for the MongoDB + Search pods. properties: claims: description: |- @@ -153,8 +156,88 @@ spec: More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ type: object type: object + security: + description: Configure security settings of the MongoDB Search server + that MongoDB database is connecting to when performing search queries. + properties: + tls: + properties: + certificateKeySecretRef: + description: |- + CertificateKeySecret is a reference to a Secret containing a private key and certificate to use for TLS. + The key and cert are expected to be PEM encoded and available at "tls.key" and "tls.crt". + This is the same format used for the standard "kubernetes.io/tls" Secret type, but no specific type is required. + Alternatively, an entry tls.pem, containing the concatenation of cert and key, can be provided. + If all of tls.pem, tls.crt and tls.key are present, the tls.pem one needs to be equal to the concatenation of tls.crt and tls.key + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + TODO: Add other useful fields. apiVersion, kind, uid? + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Drop `kubebuilder:default` when controller-gen doesn't need it https://github.com/kubernetes-sigs/kubebuilder/issues/3896. + type: string + type: object + x-kubernetes-map-type: atomic + enabled: + type: boolean + required: + - enabled + type: object + type: object source: + description: MongoDB database connection details from which MongoDB + Search will synchronize data to build indexes. properties: + external: + properties: + hostAndPorts: + items: + type: string + type: array + keyFileSecretRef: + description: |- + SecretKeyRef is a reference to a value in a given secret in the same + namespace. Based on: + https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.15/#secretkeyselector-v1-core + properties: + key: + type: string + name: + type: string + required: + - name + type: object + tls: + properties: + ca: + description: |- + LocalObjectReference contains enough information to let you locate the + referenced object inside the same namespace. + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + TODO: Add other useful fields. apiVersion, kind, uid? + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Drop `kubebuilder:default` when controller-gen doesn't need it https://github.com/kubernetes-sigs/kubebuilder/issues/3896. + type: string + type: object + x-kubernetes-map-type: atomic + enabled: + type: boolean + required: + - enabled + type: object + type: object mongodbResourceRef: properties: name: @@ -164,11 +247,26 @@ spec: required: - name type: object + passwordSecretRef: + description: |- + SecretKeyRef is a reference to a value in a given secret in the same + namespace. Based on: + https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.15/#secretkeyselector-v1-core + properties: + key: + type: string + name: + type: string + required: + - name + type: object + username: + type: string type: object statefulSet: description: |- - StatefulSetConfiguration holds the optional custom StatefulSet - that should be merged into the operator created one. + StatefulSetSpec which the operator will apply to the MongoDB Search StatefulSet at the end of the reconcile loop. Use to provide necessary customizations, + which aren't exposed as fields in the MongoDBSearch.spec. properties: metadata: description: StatefulSetMetadataWrapper is a wrapper around Labels @@ -190,6 +288,9 @@ spec: - spec type: object version: + description: Optional version of MongoDB Search component (mongot). + If not set, then the operator will set the most appropriate version + of MongoDB Search. type: string type: object status: diff --git a/mongodb-community-operator/controllers/replica_set_controller.go b/mongodb-community-operator/controllers/replica_set_controller.go index ba3e5b168..67e062648 100644 --- a/mongodb-community-operator/controllers/replica_set_controller.go +++ b/mongodb-community-operator/controllers/replica_set_controller.go @@ -14,6 +14,7 @@ import ( "k8s.io/apimachinery/pkg/fields" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/types" + "k8s.io/utils/ptr" "sigs.k8s.io/controller-runtime/pkg/builder" "sigs.k8s.io/controller-runtime/pkg/controller" "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" @@ -88,6 +89,9 @@ func NewReconciler(mgr manager.Manager, mongodbRepoUrl, mongodbImage, mongodbIma func findMdbcForSearch(ctx context.Context, rawObj k8sClient.Object) []reconcile.Request { mdbSearch := rawObj.(*searchv1.MongoDBSearch) + if mdbSearch.GetMongoDBResourceRef() == nil { + return nil + } return []reconcile.Request{ {NamespacedName: types.NamespacedName{Namespace: mdbSearch.GetMongoDBResourceRef().Namespace, Name: mdbSearch.GetMongoDBResourceRef().Name}}, } @@ -711,8 +715,8 @@ func (r ReplicaSetReconciler) buildAutomationConfig(ctx context.Context, mdb mdb // and that this resource passes search validations. If either fails, proceed without a search target // for the mongod automation config. if len(searchList.Items) == 1 { - searchSource := search_controller.NewSearchSourceDBResourceFromMongoDBCommunity(&mdb) - if search_controller.ValidateSearchSource(searchSource) == nil { + searchSource := search_controller.NewCommunityResourceSearchSource(&mdb) + if searchSource.Validate() == nil { search = &searchList.Items[0] } } @@ -727,6 +731,7 @@ func (r ReplicaSetReconciler) buildAutomationConfig(ctx context.Context, mdb mdb prometheusModification, processPortManager.GetPortsModification(), getMongodConfigSearchModification(search), + searchCoordinatorCustomRoleModification(search), ) if err != nil { return automationconfig.AutomationConfig{}, fmt.Errorf("could not create an automation config: %s", err) @@ -739,6 +744,66 @@ func (r ReplicaSetReconciler) buildAutomationConfig(ctx context.Context, mdb mdb return automationConfig, nil } +// TODO: remove this as soon as searchCoordinator builtin role is backported +func searchCoordinatorCustomRoleModification(search *searchv1.MongoDBSearch) automationconfig.Modification { + if search == nil { + return automationconfig.NOOP() + } + + return func(ac *automationconfig.AutomationConfig) { + searchCoordinatorRole := searchCoordinatorCustomRoleStruct() + ac.Roles = append(ac.Roles, searchCoordinatorRole) + } +} + +func searchCoordinatorCustomRoleStruct() automationconfig.CustomRole { + // direct translation of https://github.com/10gen/mongo/blob/6f8d95a513eea8f91ea9f5d895dd8a288dfcf725/src/mongo/db/auth/builtin_roles.yml#L652 + return automationconfig.CustomRole{ + Role: "searchCoordinator", + DB: "admin", + Roles: []automationconfig.Role{ + { + Role: "clusterMonitor", + Database: "admin", + }, + { + Role: "directShardOperations", + Database: "admin", + }, + { + Role: "readAnyDatabase", + Database: "admin", + }, + }, + Privileges: []automationconfig.Privilege{ + { + Resource: automationconfig.Resource{ + DB: ptr.To("__mdb_internal_search"), + Collection: ptr.To(""), + }, + Actions: []string{ + "changeStream", "collStats", "dbHash", "dbStats", "find", + "killCursors", "listCollections", "listIndexes", "listSearchIndexes", + // performRawDataOperations is available only on mongod master + // "performRawDataOperations", + "planCacheRead", "cleanupStructuredEncryptionData", + "compactStructuredEncryptionData", "convertToCapped", "createCollection", + "createIndex", "createSearchIndexes", "dropCollection", "dropIndex", + "dropSearchIndex", "insert", "remove", "renameCollectionSameDB", + "update", "updateSearchIndex", + }, + }, + { + Resource: automationconfig.Resource{ + Cluster: true, + }, + Actions: []string{"bypassDefaultMaxTimeMS"}, + }, + }, + AuthenticationRestrictions: nil, + } +} + // OverrideToAutomationConfig turns an automation config override from the resource spec into an automation config // which can be used to merge. func OverrideToAutomationConfig(override mdbv1.AutomationConfigOverride) automationconfig.AutomationConfig { @@ -772,10 +837,9 @@ func getMongodConfigModification(mdb mdbv1.MongoDBCommunity) automationconfig.Mo // getMongodConfigModification will merge the additional configuration in the CRD // into the configuration set up by the operator. func getMongodConfigSearchModification(search *searchv1.MongoDBSearch) automationconfig.Modification { - if search == nil { - return func(config *automationconfig.AutomationConfig) { - // do nothing - } + // Condition for skipping add parameter if it is external mongod + if search == nil || search.IsExternalMongoDBSource() { + return automationconfig.NOOP() } searchConfigParameters := search_controller.GetMongodConfigParameters(search) diff --git a/mongodb-community-operator/pkg/mongot/mongot_config.go b/mongodb-community-operator/pkg/mongot/mongot_config.go index 80d4c1421..c8126b173 100644 --- a/mongodb-community-operator/pkg/mongot/mongot_config.go +++ b/mongodb-community-operator/pkg/mongot/mongot_config.go @@ -1,41 +1,82 @@ package mongot -type Config struct { - CommunityPrivatePreview CommunityPrivatePreview `json:"communityPrivatePreview"` +type Modification func(*Config) + +func NOOP() Modification { + return func(config *Config) {} } -// CommunityPrivatePreview structure reflects private preview configuration from mongot: -// https://github.com/10gen/mongot/blob/060ec179af062ac2639678f4a613b8ab02c21597/src/main/java/com/xgen/mongot/config/provider/community/CommunityConfig.java#L100 -// Comments are from the default config file: https://github.com/10gen/mongot/blob/375379e56a580916695a2f53e12fd4a99aa24f0b/deploy/community-resources/config.default.yml#L1-L0 -type CommunityPrivatePreview struct { - // Socket (IPv4/6) address of the sync source mongod - MongodHostAndPort string `json:"mongodHostAndPort"` +func Apply(modifications ...Modification) func(*Config) { + return func(config *Config) { + for _, mod := range modifications { + mod(config) + } + } +} - // Socket (IPv4/6) address on which to listen for wire protocol connections - QueryServerAddress string `json:"queryServerAddress"` +type Config struct { + SyncSource ConfigSyncSource `json:"syncSource"` + Storage ConfigStorage `json:"storage"` + Server ConfigServer `json:"server"` + Metrics ConfigMetrics `json:"metrics"` + HealthCheck ConfigHealthCheck `json:"healthCheck"` + Logging ConfigLogging `json:"logging"` +} - // Keyfile used for mongod -> mongot authentication - KeyFilePath string `json:"keyFilePath"` +type ConfigSyncSource struct { + ReplicaSet ConfigReplicaSet `json:"replicaSet"` +} + +type ConfigReplicaSet struct { + HostAndPort []string `json:"hostAndPort"` + Username string `json:"username"` + PasswordFile string `json:"passwordFile"` + TLS *bool `json:"tls,omitempty"` + ReadPreference *string `json:"readPreference,omitempty"` + AuthSource *string `json:"authSource,omitempty"` +} - // Filesystem path that all mongot data will be stored at +type ConfigStorage struct { DataPath string `json:"dataPath"` +} - // Options for metrics - Metrics Metrics `json:"metrics,omitempty"` +type ConfigServer struct { + Wireproto *ConfigWireproto `json:"wireproto,omitempty"` +} + +type ConfigWireproto struct { + Address string `json:"address"` + Authentication *ConfigAuthentication `json:"authentication,omitempty"` + TLS ConfigTLS `json:"tls"` +} - // Options for logging - Logging Logging `json:"logging,omitempty"` +type ConfigAuthentication struct { + Mode string `json:"mode"` + KeyFile string `json:"keyFile"` } -type Metrics struct { - // Whether to enable the Prometheus metrics endpoint - Enabled bool `json:"enabled"` +type ConfigTLSMode string + +const ( + ConfigTLSModeTLS ConfigTLSMode = "TLS" + ConfigTLSModeDisabled ConfigTLSMode = "Disabled" +) + +type ConfigTLS struct { + Mode ConfigTLSMode `json:"mode"` + CertificateKeyFile *string `json:"certificateKeyFile,omitempty"` +} + +type ConfigMetrics struct { + Enabled bool `json:"enabled"` + Address string `json:"address"` +} - // Socket address (IPv4/6) on which the Prometheus /metrics endpoint will be exposed +type ConfigHealthCheck struct { Address string `json:"address"` } -type Logging struct { - // Log level - Verbosity string `json:"verbosity"` +type ConfigLogging struct { + Verbosity string `json:"verbosity"` + LogPath *string `json:"logPath,omitempty"` } diff --git a/mongodb-community-operator/pkg/tls/tls.go b/mongodb-community-operator/pkg/tls/tls.go new file mode 100644 index 000000000..77c49ed2a --- /dev/null +++ b/mongodb-community-operator/pkg/tls/tls.go @@ -0,0 +1,113 @@ +package tls + +import ( + "context" + "crypto/sha256" + "fmt" + "strings" + + "k8s.io/apimachinery/pkg/types" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "github.com/mongodb/mongodb-kubernetes/mongodb-community-operator/pkg/kube/secret" +) + +const ( + CAMountPath = "/var/lib/tls/ca/" + OperatorSecretMountPath = "/var/lib/tls/server/" //nolint + + tlsSecretCertName = "tls.crt" + tlsSecretKeyName = "tls.key" + tlsSecretPemName = "tls.pem" +) + +type TLSConfigurableResource interface { + metav1.Object + TLSSecretNamespacedName() types.NamespacedName + TLSOperatorSecretNamespacedName() types.NamespacedName +} + +// ensureTLSSecret will create or update the operator-managed Secret containing +// the concatenated certificate and key from the user-provided Secret. +// Returns the file name of the concatenated certificate and key +func EnsureTLSSecret(ctx context.Context, getUpdateCreator secret.GetUpdateCreator, resource TLSConfigurableResource) (string, error) { + certKey, err := getPemOrConcatenatedCrtAndKey(ctx, getUpdateCreator, resource.TLSSecretNamespacedName()) + if err != nil { + return "", err + } + // Calculate file name from certificate and key + fileName := OperatorSecretFileName(certKey) + + operatorSecret := secret.Builder(). + SetName(resource.TLSOperatorSecretNamespacedName().Name). + SetNamespace(resource.TLSOperatorSecretNamespacedName().Namespace). + SetField(fileName, certKey). + SetOwnerReferences(resource.GetOwnerReferences()). + Build() + + return fileName, secret.CreateOrUpdate(ctx, getUpdateCreator, operatorSecret) +} + +// getCertAndKey will fetch the certificate and key from the user-provided Secret. +func getCertAndKey(ctx context.Context, getter secret.Getter, secretName types.NamespacedName) string { + cert, err := secret.ReadKey(ctx, getter, tlsSecretCertName, secretName) + if err != nil { + return "" + } + + key, err := secret.ReadKey(ctx, getter, tlsSecretKeyName, secretName) + if err != nil { + return "" + } + + return combineCertificateAndKey(cert, key) +} + +// getPem will fetch the pem from the user-provided secret +func getPem(ctx context.Context, getter secret.Getter, secretName types.NamespacedName) string { + pem, err := secret.ReadKey(ctx, getter, tlsSecretPemName, secretName) + if err != nil { + return "" + } + return pem +} + +func combineCertificateAndKey(cert, key string) string { + trimmedCert := strings.TrimRight(cert, "\n") + trimmedKey := strings.TrimRight(key, "\n") + return fmt.Sprintf("%s\n%s", trimmedCert, trimmedKey) +} + +// getPemOrConcatenatedCrtAndKey will get the final PEM to write to the secret. +// This is either the tls.pem entry in the given secret, or the concatenation +// of tls.crt and tls.key +// It performs a basic validation on the entries. +func getPemOrConcatenatedCrtAndKey(ctx context.Context, getter secret.Getter, secretName types.NamespacedName) (string, error) { + certKey := getCertAndKey(ctx, getter, secretName) + pem := getPem(ctx, getter, secretName) + if certKey == "" && pem == "" { + return "", fmt.Errorf(`neither "%s" nor the pair "%s"/"%s" were present in the TLS secret`, tlsSecretPemName, tlsSecretCertName, tlsSecretKeyName) + } + if certKey == "" { + return pem, nil + } + if pem == "" { + return certKey, nil + } + if certKey != pem { + return "", fmt.Errorf(`if all of "%s", "%s" and "%s" are present in the secret, the entry for "%s" must be equal to the concatenation of "%s" with "%s"`, tlsSecretCertName, tlsSecretKeyName, tlsSecretPemName, tlsSecretPemName, tlsSecretCertName, tlsSecretKeyName) + } + return certKey, nil +} + +// OperatorSecretFileName calculates the file name to use for the mounted +// certificate-key file. The name is based on the hash of the combined cert and key. +// If the certificate or key changes, the file path changes as well which will trigger +// the agent to perform a restart. +// The user-provided secret is being watched and will trigger a reconciliation +// on changes. This enables the operator to automatically handle cert rotations. +func OperatorSecretFileName(certKey string) string { + hash := sha256.Sum256([]byte(certKey)) + return fmt.Sprintf("%x.pem", hash) +} diff --git a/pkg/telemetry/collector.go b/pkg/telemetry/collector.go index f1d0e8712..1e6a2ed09 100644 --- a/pkg/telemetry/collector.go +++ b/pkg/telemetry/collector.go @@ -21,6 +21,7 @@ import ( mdbmultiv1 "github.com/mongodb/mongodb-kubernetes/api/v1/mdbmulti" omv1 "github.com/mongodb/mongodb-kubernetes/api/v1/om" searchv1 "github.com/mongodb/mongodb-kubernetes/api/v1/search" + userv1 "github.com/mongodb/mongodb-kubernetes/api/v1/user" mcov1 "github.com/mongodb/mongodb-kubernetes/mongodb-community-operator/api/v1" "github.com/mongodb/mongodb-kubernetes/mongodb-community-operator/pkg/util/envvar" "github.com/mongodb/mongodb-kubernetes/pkg/images" @@ -364,25 +365,50 @@ func addCommunityEvents(ctx context.Context, operatorClusterClient kubeclient.Cl return events } +func resolveSearchSource(ctx context.Context, operatorClusterClient kubeclient.Client, source *userv1.MongoDBResourceRef) (architecture string, isEnterprise bool, ok bool) { + if source == nil { + return "external", false, true // we cheat and hijack the Architecture field to indicate this Search resource is configured with an external MongoDB source + } + + key := kubeclient.ObjectKey{Namespace: source.Namespace, Name: source.Name} + + mdb := &mdbv1.MongoDB{} + if err := operatorClusterClient.Get(ctx, key, mdb); err == nil { + return string(architectures.GetArchitecture(mdb.Annotations)), true, true + } + + mdbc := &mcov1.MongoDBCommunity{} + if err := operatorClusterClient.Get(ctx, key, mdbc); err == nil { + return "static", false, true // Community is always static + } + + return "", false, false // likely the database resource doesn't exist yet, skip telemetry for this item for now +} + func addSearchEvents(ctx context.Context, operatorClusterClient kubeclient.Client, operatorUUID string, now time.Time) []Event { var events []Event searchList := &searchv1.MongoDBSearchList{} if err := operatorClusterClient.List(ctx, searchList); err != nil { Logger.Warnf("failed to fetch MongoDBSearchList from Kubernetes: %v", err) - } else { - for _, item := range searchList.Items { - properties := DeploymentUsageSnapshotProperties{ - DeploymentUID: string(item.UID), - OperatorID: operatorUUID, - Architecture: string(architectures.Static), // Community Search is always static - IsMultiCluster: false, // Community Search doesn't support multi-cluster - Type: "Search", - IsRunningEnterpriseImage: false, // Community search doesn't run enterprise - } - if event := createEvent(properties, now, Deployments); event != nil { - events = append(events, *event) - } + return nil + } + + for _, item := range searchList.Items { + architecture, isEnterprise, ok := resolveSearchSource(ctx, operatorClusterClient, item.GetMongoDBResourceRef()) + if !ok { // search source doesn't exist yet, don't generate a telemetry event + continue + } + properties := DeploymentUsageSnapshotProperties{ + DeploymentUID: string(item.UID), + OperatorID: operatorUUID, + Architecture: architecture, + IsMultiCluster: false, // Search doesn't support multi-cluster + Type: "Search", + IsRunningEnterpriseImage: isEnterprise, + } + if event := createEvent(properties, now, Deployments); event != nil { + events = append(events, *event) } } return events diff --git a/pkg/telemetry/collector_test.go b/pkg/telemetry/collector_test.go index 9b3a8c638..f0ca25986 100644 --- a/pkg/telemetry/collector_test.go +++ b/pkg/telemetry/collector_test.go @@ -2,7 +2,9 @@ package telemetry import ( "context" + "encoding/json" "errors" + "reflect" "runtime" "testing" "time" @@ -21,6 +23,7 @@ import ( "github.com/mongodb/mongodb-kubernetes/api/v1/mdbmulti" omv1 "github.com/mongodb/mongodb-kubernetes/api/v1/om" searchv1 "github.com/mongodb/mongodb-kubernetes/api/v1/search" + userv1 "github.com/mongodb/mongodb-kubernetes/api/v1/user" "github.com/mongodb/mongodb-kubernetes/controllers/operator/mock" mcov1 "github.com/mongodb/mongodb-kubernetes/mongodb-community-operator/api/v1" mockClient "github.com/mongodb/mongodb-kubernetes/mongodb-community-operator/pkg/kube/client" @@ -1157,12 +1160,17 @@ func findEventWithDeploymentUID(events []Event, deploymentUID string) *Event { type MockClient struct { client.Client MockList func(ctx context.Context, list client.ObjectList, opts ...client.ListOption) error + MockGet func(ctx context.Context, key client.ObjectKey, obj client.Object, opts ...client.GetOption) error } func (m *MockClient) List(ctx context.Context, list client.ObjectList, opts ...client.ListOption) error { return m.MockList(ctx, list, opts...) } +func (m *MockClient) Get(ctx context.Context, key client.ObjectKey, obj client.Object, opts ...client.GetOption) error { + return m.MockGet(ctx, key, obj, opts...) +} + func TestAddCommunityEvents(t *testing.T) { operatorUUID := "test-operator-uuid" @@ -1261,55 +1269,67 @@ func TestAddCommunityEvents(t *testing.T) { func TestAddSearchEvents(t *testing.T) { operatorUUID := "test-operator-uuid" - now := time.Now() + mdbStatic := &mdbv1.MongoDB{ObjectMeta: metav1.ObjectMeta{Namespace: "default", Name: "mdb-static", Annotations: map[string]string{architectures.ArchitectureAnnotation: string(architectures.Static)}}} + mdbNonStatic := &mdbv1.MongoDB{ObjectMeta: metav1.ObjectMeta{Namespace: "default", Name: "mdb-nonstatic", Annotations: map[string]string{architectures.ArchitectureAnnotation: string(architectures.NonStatic)}}} + community := &mcov1.MongoDBCommunity{ObjectMeta: metav1.ObjectMeta{Namespace: "default", Name: "community-db"}} + testCases := []struct { - name string - resources searchv1.MongoDBSearchList - events []DeploymentUsageSnapshotProperties + name string + searchItems []searchv1.MongoDBSearch + events []DeploymentUsageSnapshotProperties + sources map[reflect.Type][]client.Object }{ { - name: "With resources", - resources: searchv1.MongoDBSearchList{ - Items: []searchv1.MongoDBSearch{ - { - ObjectMeta: metav1.ObjectMeta{ - UID: types.UID("search-1"), - Name: "test-search-1", - }, - }, - { - ObjectMeta: metav1.ObjectMeta{ - UID: types.UID("search-2"), - Name: "test-search-2", - }, - }, - }, + name: "External source", + searchItems: []searchv1.MongoDBSearch{ + {ObjectMeta: metav1.ObjectMeta{UID: types.UID("search-external"), Name: "search-external", Namespace: "default"}, Spec: searchv1.MongoDBSearchSpec{Source: &searchv1.MongoDBSource{ExternalMongoDBSource: &searchv1.ExternalMongoDBSource{}}}}, + }, + events: []DeploymentUsageSnapshotProperties{{ + DeploymentUID: "search-external", + OperatorID: operatorUUID, + Architecture: "external", + IsMultiCluster: false, + Type: "Search", + IsRunningEnterpriseImage: false, + }}, + }, + { + name: "Enterprise static and non-static", + searchItems: []searchv1.MongoDBSearch{ + {ObjectMeta: metav1.ObjectMeta{UID: types.UID("search-static"), Name: "search-static", Namespace: "default"}, Spec: searchv1.MongoDBSearchSpec{Source: &searchv1.MongoDBSource{MongoDBResourceRef: &userv1.MongoDBResourceRef{Name: "mdb-static"}}}}, + {ObjectMeta: metav1.ObjectMeta{UID: types.UID("search-nonstatic"), Name: "search-nonstatic", Namespace: "default"}, Spec: searchv1.MongoDBSearchSpec{Source: &searchv1.MongoDBSource{MongoDBResourceRef: &userv1.MongoDBResourceRef{Name: "mdb-nonstatic"}}}}, }, events: []DeploymentUsageSnapshotProperties{ - { - DeploymentUID: "search-1", - OperatorID: operatorUUID, - Architecture: string(architectures.Static), - IsMultiCluster: false, - Type: "Search", - IsRunningEnterpriseImage: false, - }, - { - DeploymentUID: "search-2", - OperatorID: operatorUUID, - Architecture: string(architectures.Static), - IsMultiCluster: false, - Type: "Search", - IsRunningEnterpriseImage: false, - }, + {DeploymentUID: "search-static", OperatorID: operatorUUID, Architecture: string(architectures.Static), IsMultiCluster: false, Type: "Search", IsRunningEnterpriseImage: true}, + {DeploymentUID: "search-nonstatic", OperatorID: operatorUUID, Architecture: string(architectures.NonStatic), IsMultiCluster: false, Type: "Search", IsRunningEnterpriseImage: true}, + }, + sources: map[reflect.Type][]client.Object{ + reflect.TypeOf(&mdbv1.MongoDB{}): {mdbStatic, mdbNonStatic}, }, }, { - name: "With no resources", - resources: searchv1.MongoDBSearchList{}, - events: []DeploymentUsageSnapshotProperties{}, + name: "Community source", + searchItems: []searchv1.MongoDBSearch{ + {ObjectMeta: metav1.ObjectMeta{UID: types.UID("search-community"), Name: "search-community", Namespace: "default"}, Spec: searchv1.MongoDBSearchSpec{Source: &searchv1.MongoDBSource{MongoDBResourceRef: &userv1.MongoDBResourceRef{Name: "community-db"}}}}, + }, + events: []DeploymentUsageSnapshotProperties{{DeploymentUID: "search-community", OperatorID: operatorUUID, Architecture: "static", IsMultiCluster: false, Type: "Search", IsRunningEnterpriseImage: false}}, + sources: map[reflect.Type][]client.Object{ + reflect.TypeOf(&mcov1.MongoDBCommunity{}): {community}, + }, + }, + { + name: "Missing underlying resource (skipped)", + searchItems: []searchv1.MongoDBSearch{ + {ObjectMeta: metav1.ObjectMeta{UID: types.UID("search-missing"), Name: "search-missing", Namespace: "default"}, Spec: searchv1.MongoDBSearchSpec{Source: &searchv1.MongoDBSource{MongoDBResourceRef: &userv1.MongoDBResourceRef{Name: "does-not-exist"}}}}, + }, + events: []DeploymentUsageSnapshotProperties{}, + }, + { + name: "No search resources", + searchItems: []searchv1.MongoDBSearch{}, + events: []DeploymentUsageSnapshotProperties{}, }, } @@ -1318,10 +1338,23 @@ func TestAddSearchEvents(t *testing.T) { mc := &MockClient{ MockList: func(ctx context.Context, list client.ObjectList, opts ...client.ListOption) error { if l, ok := list.(*searchv1.MongoDBSearchList); ok { - *l = tc.resources + *l = searchv1.MongoDBSearchList{Items: tc.searchItems} } return nil }, + MockGet: func(ctx context.Context, key client.ObjectKey, obj client.Object, opts ...client.GetOption) error { + objects := tc.sources[reflect.TypeOf(obj)] + for _, o := range objects { + if o.GetName() == key.Name && o.GetNamespace() == key.Namespace { + // copy the arranged object into the obj pointer like controller-runtime's pkg/client/fake does + bytes, _ := json.Marshal(o) + json.Unmarshal(bytes, obj) + return nil + } + } + + return errors.New("not found") + }, } events := addSearchEvents(context.Background(), mc, operatorUUID, now) diff --git a/public/crds.yaml b/public/crds.yaml index 0bdd5e315..4afc63e97 100644 --- a/public/crds.yaml +++ b/public/crds.yaml @@ -4071,6 +4071,8 @@ spec: spec: properties: persistence: + description: Configure MongoDB Search's persistent volume. If not + defined, the operator will request 10GB of storage. properties: multiple: properties: @@ -4117,7 +4119,8 @@ spec: type: object type: object resourceRequirements: - description: ResourceRequirements describes the compute resource requirements. + description: Configure resource requests and limits for the MongoDB + Search pods. properties: claims: description: |- @@ -4175,8 +4178,88 @@ spec: More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ type: object type: object + security: + description: Configure security settings of the MongoDB Search server + that MongoDB database is connecting to when performing search queries. + properties: + tls: + properties: + certificateKeySecretRef: + description: |- + CertificateKeySecret is a reference to a Secret containing a private key and certificate to use for TLS. + The key and cert are expected to be PEM encoded and available at "tls.key" and "tls.crt". + This is the same format used for the standard "kubernetes.io/tls" Secret type, but no specific type is required. + Alternatively, an entry tls.pem, containing the concatenation of cert and key, can be provided. + If all of tls.pem, tls.crt and tls.key are present, the tls.pem one needs to be equal to the concatenation of tls.crt and tls.key + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + TODO: Add other useful fields. apiVersion, kind, uid? + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Drop `kubebuilder:default` when controller-gen doesn't need it https://github.com/kubernetes-sigs/kubebuilder/issues/3896. + type: string + type: object + x-kubernetes-map-type: atomic + enabled: + type: boolean + required: + - enabled + type: object + type: object source: + description: MongoDB database connection details from which MongoDB + Search will synchronize data to build indexes. properties: + external: + properties: + hostAndPorts: + items: + type: string + type: array + keyFileSecretRef: + description: |- + SecretKeyRef is a reference to a value in a given secret in the same + namespace. Based on: + https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.15/#secretkeyselector-v1-core + properties: + key: + type: string + name: + type: string + required: + - name + type: object + tls: + properties: + ca: + description: |- + LocalObjectReference contains enough information to let you locate the + referenced object inside the same namespace. + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + TODO: Add other useful fields. apiVersion, kind, uid? + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Drop `kubebuilder:default` when controller-gen doesn't need it https://github.com/kubernetes-sigs/kubebuilder/issues/3896. + type: string + type: object + x-kubernetes-map-type: atomic + enabled: + type: boolean + required: + - enabled + type: object + type: object mongodbResourceRef: properties: name: @@ -4186,11 +4269,26 @@ spec: required: - name type: object + passwordSecretRef: + description: |- + SecretKeyRef is a reference to a value in a given secret in the same + namespace. Based on: + https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.15/#secretkeyselector-v1-core + properties: + key: + type: string + name: + type: string + required: + - name + type: object + username: + type: string type: object statefulSet: description: |- - StatefulSetConfiguration holds the optional custom StatefulSet - that should be merged into the operator created one. + StatefulSetSpec which the operator will apply to the MongoDB Search StatefulSet at the end of the reconcile loop. Use to provide necessary customizations, + which aren't exposed as fields in the MongoDBSearch.spec. properties: metadata: description: StatefulSetMetadataWrapper is a wrapper around Labels @@ -4212,6 +4310,9 @@ spec: - spec type: object version: + description: Optional version of MongoDB Search component (mongot). + If not set, then the operator will set the most appropriate version + of MongoDB Search. type: string type: object status: diff --git a/scripts/dev/contexts/e2e_mdb_community b/scripts/dev/contexts/e2e_mdb_community index 4f096c2d7..b38563179 100644 --- a/scripts/dev/contexts/e2e_mdb_community +++ b/scripts/dev/contexts/e2e_mdb_community @@ -10,3 +10,5 @@ source "${script_dir}/variables/mongodb_latest" # This variable is needed otherwise the `fetch_om_information.sh` script is called and fails the test export OM_EXTERNALLY_CONFIGURED="true" + +source "${script_dir}/variables/mongodb_search_dev" diff --git a/scripts/dev/contexts/e2e_mdb_kind_ubi_cloudqa b/scripts/dev/contexts/e2e_mdb_kind_ubi_cloudqa index e1f13453c..886ecb8be 100644 --- a/scripts/dev/contexts/e2e_mdb_kind_ubi_cloudqa +++ b/scripts/dev/contexts/e2e_mdb_kind_ubi_cloudqa @@ -16,3 +16,5 @@ export CUSTOM_OM_VERSION export CUSTOM_MDB_VERSION=6.0.5 export CUSTOM_MDB_PREV_VERSION=5.0.7 + +source "${script_dir}/variables/mongodb_search_dev" diff --git a/scripts/dev/contexts/e2e_static_mdb_kind_ubi_cloudqa b/scripts/dev/contexts/e2e_static_mdb_kind_ubi_cloudqa index c007157d7..f4fe1ebdb 100644 --- a/scripts/dev/contexts/e2e_static_mdb_kind_ubi_cloudqa +++ b/scripts/dev/contexts/e2e_static_mdb_kind_ubi_cloudqa @@ -17,3 +17,5 @@ export CUSTOM_OM_VERSION export CUSTOM_MDB_PREV_VERSION=6.0.16 export CUSTOM_MDB_VERSION=7.0.5 + +source "${script_dir}/variables/mongodb_search_dev" diff --git a/scripts/dev/contexts/evg-private-context b/scripts/dev/contexts/evg-private-context index ed92f125c..1eb2ef0f5 100644 --- a/scripts/dev/contexts/evg-private-context +++ b/scripts/dev/contexts/evg-private-context @@ -109,8 +109,8 @@ export CODE_SNIPPETS_COMMIT_OUTPUT=${code_snippets_commit_output:-"false"} export READINESS_PROBE_IMAGE="${BASE_REPO_URL}/mongodb-kubernetes-readinessprobe:${version_id}" export VERSION_UPGRADE_HOOK_IMAGE="${BASE_REPO_URL}/mongodb-kubernetes-operator-version-upgrade-post-start-hook:${version_id}" -# TODO to be removed at public preview stage of community-search -export COMMUNITY_PRIVATE_PREVIEW_PULLSECRET_DOCKERCONFIGJSON="${community_private_preview_pullsecret_dockerconfigjson}" +# shellcheck disable=SC2154 +export PRERELEASE_PULLSECRET_DOCKERCONFIGJSON="${community_private_preview_pullsecret_dockerconfigjson}" export cognito_user_pool_id="${cognito_user_pool_id}" export cognito_workload_federation_client_id="${cognito_workload_federation_client_id}" diff --git a/scripts/dev/contexts/root-context b/scripts/dev/contexts/root-context index 24570ac93..542190d4e 100644 --- a/scripts/dev/contexts/root-context +++ b/scripts/dev/contexts/root-context @@ -6,10 +6,12 @@ script_name=$(readlink -f "${BASH_SOURCE[0]}") script_dir=$(dirname "${script_name}") source "${script_dir}/private-context" -export PROJECT_DIR="${PWD}" +PROJECT_DIR="$(realpath "${script_dir}/../../..")" +export PROJECT_DIR export IMAGE_TYPE=ubi export UBI_IMAGE_WITHOUT_SUFFIX=true export WATCH_NAMESPACE=${WATCH_NAMESPACE:-${NAMESPACE}} +export OPERATOR_NAME="mongodb-kubernetes-operator" # # changing variables below should not be necessary @@ -35,7 +37,7 @@ fi export OPERATOR_ENV=${OPERATOR_ENV:-"dev"} -AGENT_VERSION="$(jq -r '.agentVersion' release.json)" +AGENT_VERSION="$(jq -r '.agentVersion' "${PROJECT_DIR}/release.json")" export AGENT_VERSION export AGENT_IMAGE="${MDB_AGENT_IMAGE_REPOSITORY}:${AGENT_VERSION}" @@ -111,7 +113,7 @@ export MDB_COMMUNITY_REPO_URL=quay.io/mongodb export MDB_COMMUNITY_AGENT_IMAGE=${AGENT_IMAGE} export MDB_COMMUNITY_IMAGE_TYPE=ubi8 -MDB_SEARCH_COMMUNITY_VERSION="$(jq -r '.search.community.version' release.json)" +MDB_SEARCH_COMMUNITY_VERSION="$(jq -r '.search.community.version' "${PROJECT_DIR}/release.json")" export MDB_SEARCH_COMMUNITY_VERSION export MDB_SEARCH_COMMUNITY_NAME="mongodb-search-community" diff --git a/scripts/dev/contexts/variables/mongodb_search_dev b/scripts/dev/contexts/variables/mongodb_search_dev new file mode 100644 index 000000000..1eda41398 --- /dev/null +++ b/scripts/dev/contexts/variables/mongodb_search_dev @@ -0,0 +1,8 @@ +#!/usr/bin/env bash + +set -Eeou pipefail + +# Temporary development images built from mongot master +export MDB_SEARCH_COMMUNITY_VERSION="1.53.0-78-g227f2593f" # Sep 2nd mongot master +export MDB_SEARCH_COMMUNITY_NAME="mongot/community" +export MDB_SEARCH_COMMUNITY_REPO_URL="268558157000.dkr.ecr.eu-west-1.amazonaws.com" diff --git a/scripts/funcs/operator_deployment b/scripts/funcs/operator_deployment index 60f44ff0d..77075a39a 100644 --- a/scripts/funcs/operator_deployment +++ b/scripts/funcs/operator_deployment @@ -33,6 +33,9 @@ get_operator_helm_values() { "operator.telemetry.send.enabled=${MDB_OPERATOR_TELEMETRY_SEND_ENABLED:-false}" # lets collect and save in the configmap as frequently as we can "operator.telemetry.collection.frequency=${MDB_OPERATOR_TELEMETRY_COLLECTION_FREQUENCY:-1m}" + "search.community.repo=${MDB_SEARCH_COMMUNITY_REPO_URL}" + "search.community.name=${MDB_SEARCH_COMMUNITY_NAME}" + "search.community.version=${MDB_SEARCH_COMMUNITY_VERSION}" "community.registry.agent=${AGENT_BASE_REGISTRY:-${REGISTRY}}" "search.community.repo=${MDB_SEARCH_COMMUNITY_REPO_URL}" "search.community.name=${MDB_SEARCH_COMMUNITY_NAME}" From ba7910669c1d85398b6b5831df8b3aea3ee8b4ce Mon Sep 17 00:00:00 2001 From: Yavor Georgiev Date: Tue, 9 Sep 2025 21:41:46 +0300 Subject: [PATCH 02/29] Apply suggestions from code review Co-authored-by: Vivek Singh --- api/v1/search/mongodbsearch_types.go | 3 ++- controllers/search_controller/external_search_source.go | 2 +- .../tests/search/fixtures/mongodbuser-mdb-admin.yaml | 3 ++- .../search/fixtures/mongodbuser-search-sync-source-user.yaml | 2 +- 4 files changed, 6 insertions(+), 4 deletions(-) diff --git a/api/v1/search/mongodbsearch_types.go b/api/v1/search/mongodbsearch_types.go index 5a8037b51..c20884f8d 100644 --- a/api/v1/search/mongodbsearch_types.go +++ b/api/v1/search/mongodbsearch_types.go @@ -63,8 +63,9 @@ type ExternalMongoDBSource struct { HostAndPorts []string `json:"hostAndPorts,omitempty"` // mongod keyfile used to connect to the external MongoDB deployment KeyFileSecretKeyRef *userv1.SecretKeyRef `json:"keyFileSecretRef,omitempty"` + // TLS configuration for the external MongoDB deployment // +optional - TLS *ExternalMongodTLS `json:"tls,omitempty"` // TLS configuration for the external MongoDB deployment + TLS *ExternalMongodTLS `json:"tls,omitempty"` } type ExternalMongodTLS struct { diff --git a/controllers/search_controller/external_search_source.go b/controllers/search_controller/external_search_source.go index 5a408246e..d15f49799 100644 --- a/controllers/search_controller/external_search_source.go +++ b/controllers/search_controller/external_search_source.go @@ -12,7 +12,7 @@ func NewExternalSearchSource(namespace string, spec *searchv1.ExternalMongoDBSou return &externalSearchResource{namespace: namespace, spec: spec} } -// externalSearchResource implements SearchSourceDBResource for deployments managed outside the operator. +// externalSearchResource implements SearchSourceDBResource for deployments managed outside the Kubernetes cluster. type externalSearchResource struct { namespace string spec *searchv1.ExternalMongoDBSource diff --git a/docker/mongodb-kubernetes-tests/tests/search/fixtures/mongodbuser-mdb-admin.yaml b/docker/mongodb-kubernetes-tests/tests/search/fixtures/mongodbuser-mdb-admin.yaml index 0b3fe4c77..950f2e5a6 100644 --- a/docker/mongodb-kubernetes-tests/tests/search/fixtures/mongodbuser-mdb-admin.yaml +++ b/docker/mongodb-kubernetes-tests/tests/search/fixtures/mongodbuser-mdb-admin.yaml @@ -13,4 +13,5 @@ spec: key: password roles: - name: root - db: admin \ No newline at end of file + db: admin + \ No newline at end of file diff --git a/docker/mongodb-kubernetes-tests/tests/search/fixtures/mongodbuser-search-sync-source-user.yaml b/docker/mongodb-kubernetes-tests/tests/search/fixtures/mongodbuser-search-sync-source-user.yaml index cd1eab1a5..d6ce56d8f 100644 --- a/docker/mongodb-kubernetes-tests/tests/search/fixtures/mongodbuser-search-sync-source-user.yaml +++ b/docker/mongodb-kubernetes-tests/tests/search/fixtures/mongodbuser-search-sync-source-user.yaml @@ -1,4 +1,4 @@ -# user used by MongoDB Search to connect to MongoDB database to synchronize data from +# user used by MongoDB Search to connect to MongoDB database to synchronize data from. # For MongoDB <8.2, the operator will be creating the searchCoordinator custom role automatically # From MongoDB 8.2, searchCoordinator role will be a built-in role. apiVersion: mongodb.com/v1 From 1aa6e3e8ef37f27fedc7c897aef808de8f416d79 Mon Sep 17 00:00:00 2001 From: Yavor Georgiev Date: Thu, 11 Sep 2025 01:28:26 +0300 Subject: [PATCH 03/29] regenerate CRDs with the latest controller-gen --- .../mongodb.com_clustermongodbroles.yaml | 2 +- config/crd/bases/mongodb.com_mongodb.yaml | 48 +++++++- .../mongodb.com_mongodbmulticluster.yaml | 48 +++++++- .../crd/bases/mongodb.com_mongodbsearch.yaml | 90 +++++++++++++- .../crd/bases/mongodb.com_mongodbusers.yaml | 2 +- config/crd/bases/mongodb.com_opsmanagers.yaml | 112 ++++++++++++++++-- ...ommunity.mongodb.com_mongodbcommunity.yaml | 54 ++++++++- .../crds/mongodb.com_mongodbsearch.yaml | 11 +- public/crds.yaml | 11 +- 9 files changed, 334 insertions(+), 44 deletions(-) diff --git a/config/crd/bases/mongodb.com_clustermongodbroles.yaml b/config/crd/bases/mongodb.com_clustermongodbroles.yaml index 9241b7dad..3d583bcfd 100644 --- a/config/crd/bases/mongodb.com_clustermongodbroles.yaml +++ b/config/crd/bases/mongodb.com_clustermongodbroles.yaml @@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.15.0 + controller-gen.kubebuilder.io/version: v0.18.0 name: clustermongodbroles.mongodb.com spec: group: mongodb.com diff --git a/config/crd/bases/mongodb.com_mongodb.yaml b/config/crd/bases/mongodb.com_mongodb.yaml index 2a7076877..d421d8837 100644 --- a/config/crd/bases/mongodb.com_mongodb.yaml +++ b/config/crd/bases/mongodb.com_mongodb.yaml @@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.15.0 + controller-gen.kubebuilder.io/version: v0.18.0 name: mongodb.mongodb.com spec: group: mongodb.com @@ -1410,7 +1410,29 @@ spec: automationLdapGroupDN: type: string automationPasswordSecretRef: - $ref: '#/definitions/k8s.io~1api~1core~1v1~0SecretKeySelector' + description: SecretKeySelector selects a key of a Secret. + properties: + key: + description: The key of the secret to select from. Must + be a valid secret key. + type: string + 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 + optional: + description: Specify whether the Secret or its key + must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic automationUserName: type: string clientCertificateSecretRef: @@ -1445,9 +1467,29 @@ spec: bindQueryUser: type: string caConfigMapRef: - $ref: '#/definitions/k8s.io~1api~1core~1v1~0ConfigMapKeySelector' description: Allows to point at a ConfigMap/key with a CA file to mount on the Pod + properties: + key: + description: The key to select. + type: string + 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 + optional: + description: Specify whether the ConfigMap or its + key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic servers: items: type: string diff --git a/config/crd/bases/mongodb.com_mongodbmulticluster.yaml b/config/crd/bases/mongodb.com_mongodbmulticluster.yaml index 01fe3f2e6..3f1fa05c9 100644 --- a/config/crd/bases/mongodb.com_mongodbmulticluster.yaml +++ b/config/crd/bases/mongodb.com_mongodbmulticluster.yaml @@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.15.0 + controller-gen.kubebuilder.io/version: v0.18.0 name: mongodbmulticluster.mongodb.com spec: group: mongodb.com @@ -670,7 +670,29 @@ spec: automationLdapGroupDN: type: string automationPasswordSecretRef: - $ref: '#/definitions/k8s.io~1api~1core~1v1~0SecretKeySelector' + description: SecretKeySelector selects a key of a Secret. + properties: + key: + description: The key of the secret to select from. Must + be a valid secret key. + type: string + 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 + optional: + description: Specify whether the Secret or its key + must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic automationUserName: type: string clientCertificateSecretRef: @@ -705,9 +727,29 @@ spec: bindQueryUser: type: string caConfigMapRef: - $ref: '#/definitions/k8s.io~1api~1core~1v1~0ConfigMapKeySelector' description: Allows to point at a ConfigMap/key with a CA file to mount on the Pod + properties: + key: + description: The key to select. + type: string + 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 + optional: + description: Specify whether the ConfigMap or its + key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic servers: items: type: string diff --git a/config/crd/bases/mongodb.com_mongodbsearch.yaml b/config/crd/bases/mongodb.com_mongodbsearch.yaml index 7c53c195c..08b4fa173 100644 --- a/config/crd/bases/mongodb.com_mongodbsearch.yaml +++ b/config/crd/bases/mongodb.com_mongodbsearch.yaml @@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.15.0 + controller-gen.kubebuilder.io/version: v0.18.0 name: mongodbsearch.mongodb.com spec: group: mongodb.com @@ -97,9 +97,65 @@ spec: type: object type: object resourceRequirements: - $ref: '#/definitions/k8s.io~1api~1core~1v1~0ResourceRequirements' description: Configure resource requests and limits for the MongoDB Search pods. + properties: + claims: + description: |- + Claims lists the names of resources, defined in spec.resourceClaims, + that are used by this container. + + This is an alpha field and requires enabling the + DynamicResourceAllocation feature gate. + + This field is immutable. It can only be set for containers. + items: + description: ResourceClaim references one entry in PodSpec.ResourceClaims. + properties: + name: + description: |- + Name must match the name of one entry in pod.spec.resourceClaims of + the Pod where this field is used. It makes that resource available + inside a container. + type: string + request: + description: |- + Request is the name chosen for a request in the referenced claim. + If empty, everything from the claim is made available, otherwise + only the result of this request. + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Limits describes the maximum amount of compute resources allowed. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Requests describes the minimum amount of compute resources required. + If Requests is omitted for a container, it defaults to Limits if that is explicitly specified, + otherwise to an implementation-defined value. Requests cannot exceed Limits. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + type: object + type: object security: description: Configure security settings of the MongoDB Search server that MongoDB database is connecting to when performing search queries. @@ -107,13 +163,24 @@ spec: tls: properties: certificateKeySecretRef: - $ref: '#/definitions/k8s.io~1api~1core~1v1~0LocalObjectReference' description: |- CertificateKeySecret is a reference to a Secret containing a private key and certificate to use for TLS. The key and cert are expected to be PEM encoded and available at "tls.key" and "tls.crt". This is the same format used for the standard "kubernetes.io/tls" Secret type, but no specific type is required. Alternatively, an entry tls.pem, containing the concatenation of cert and key, can be provided. If all of tls.pem, tls.crt and tls.key are present, the tls.pem one needs to be equal to the concatenation of tls.crt and tls.key + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + type: object + x-kubernetes-map-type: atomic enabled: type: boolean required: @@ -142,9 +209,24 @@ spec: - name type: object tls: + description: TLS configuration for the external MongoDB deployment properties: ca: - $ref: '#/definitions/k8s.io~1api~1core~1v1~0LocalObjectReference' + description: |- + LocalObjectReference contains enough information to let you locate the + referenced object inside the same namespace. + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + type: object + x-kubernetes-map-type: atomic enabled: type: boolean required: diff --git a/config/crd/bases/mongodb.com_mongodbusers.yaml b/config/crd/bases/mongodb.com_mongodbusers.yaml index a81f0d449..89713ce7f 100644 --- a/config/crd/bases/mongodb.com_mongodbusers.yaml +++ b/config/crd/bases/mongodb.com_mongodbusers.yaml @@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.15.0 + controller-gen.kubebuilder.io/version: v0.18.0 name: mongodbusers.mongodb.com spec: group: mongodb.com diff --git a/config/crd/bases/mongodb.com_opsmanagers.yaml b/config/crd/bases/mongodb.com_opsmanagers.yaml index c830b9a24..3ace001da 100644 --- a/config/crd/bases/mongodb.com_opsmanagers.yaml +++ b/config/crd/bases/mongodb.com_opsmanagers.yaml @@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.15.0 + controller-gen.kubebuilder.io/version: v0.18.0 name: opsmanagers.mongodb.com spec: group: mongodb.com @@ -730,7 +730,30 @@ spec: automationLdapGroupDN: type: string automationPasswordSecretRef: - $ref: '#/definitions/k8s.io~1api~1core~1v1~0SecretKeySelector' + description: SecretKeySelector selects a key of a + Secret. + properties: + key: + description: The key of the secret to select from. Must + be a valid secret key. + type: string + 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 + optional: + description: Specify whether the Secret or its + key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic automationUserName: type: string clientCertificateSecretRef: @@ -766,9 +789,29 @@ spec: bindQueryUser: type: string caConfigMapRef: - $ref: '#/definitions/k8s.io~1api~1core~1v1~0ConfigMapKeySelector' description: Allows to point at a ConfigMap/key with a CA file to mount on the Pod + properties: + key: + description: The key to select. + type: string + 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 + optional: + description: Specify whether the ConfigMap or + its key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic servers: items: type: string @@ -1193,7 +1236,29 @@ spec: CustomCertificateSecretRefs is a list of valid Certificate Authority certificate secrets that apply to the associated S3 bucket. items: - $ref: '#/definitions/k8s.io~1api~1core~1v1~0SecretKeySelector' + description: SecretKeySelector selects a key of a Secret. + properties: + key: + description: The key of the secret to select from. Must + be a valid secret key. + type: string + 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 + optional: + description: Specify whether the Secret or its key + must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic type: array irsaEnabled: description: |- @@ -1263,7 +1328,29 @@ spec: CustomCertificateSecretRefs is a list of valid Certificate Authority certificate secrets that apply to the associated S3 bucket. items: - $ref: '#/definitions/k8s.io~1api~1core~1v1~0SecretKeySelector' + description: SecretKeySelector selects a key of a Secret. + properties: + key: + description: The key of the secret to select from. Must + be a valid secret key. + type: string + 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 + optional: + description: Specify whether the Secret or its key + must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic type: array irsaEnabled: description: |- @@ -1408,6 +1495,8 @@ spec: required: - spec type: object + required: + - members type: object clusterDomain: description: Cluster domain to override the default *.svc.cluster.local @@ -1447,13 +1536,13 @@ spec: Service when creating a ClusterIP type Service type: string externalTrafficPolicy: - $ref: '#/definitions/k8s.io~1api~1core~1v1~0ServiceExternalTrafficPolicy' description: |- ExternalTrafficPolicy mechanism to preserve the client source IP. Only supported on GCE and Google Kubernetes Engine. enum: - Cluster - Local + type: string loadBalancerIP: description: LoadBalancerIP IP that will be assigned to this LoadBalancer. @@ -1464,12 +1553,12 @@ spec: format: int32 type: integer type: - $ref: '#/definitions/k8s.io~1api~1core~1v1~0ServiceType' description: Type of the `Service` to be created. enum: - LoadBalancer - NodePort - ClusterIP + type: string required: - type type: object @@ -1511,6 +1600,7 @@ spec: - spec type: object required: + - clusterName - members type: object type: array @@ -1536,13 +1626,13 @@ spec: when creating a ClusterIP type Service type: string externalTrafficPolicy: - $ref: '#/definitions/k8s.io~1api~1core~1v1~0ServiceExternalTrafficPolicy' description: |- ExternalTrafficPolicy mechanism to preserve the client source IP. Only supported on GCE and Google Kubernetes Engine. enum: - Cluster - Local + type: string loadBalancerIP: description: LoadBalancerIP IP that will be assigned to this LoadBalancer. type: string @@ -1552,12 +1642,12 @@ spec: format: int32 type: integer type: - $ref: '#/definitions/k8s.io~1api~1core~1v1~0ServiceType' description: Type of the `Service` to be created. enum: - LoadBalancer - NodePort - ClusterIP + type: string required: - type type: object @@ -1577,13 +1667,13 @@ spec: when creating a ClusterIP type Service type: string externalTrafficPolicy: - $ref: '#/definitions/k8s.io~1api~1core~1v1~0ServiceExternalTrafficPolicy' description: |- ExternalTrafficPolicy mechanism to preserve the client source IP. Only supported on GCE and Google Kubernetes Engine. enum: - Cluster - Local + type: string loadBalancerIP: description: LoadBalancerIP IP that will be assigned to this LoadBalancer. type: string @@ -1593,12 +1683,12 @@ spec: format: int32 type: integer type: - $ref: '#/definitions/k8s.io~1api~1core~1v1~0ServiceType' description: Type of the `Service` to be created. enum: - LoadBalancer - NodePort - ClusterIP + type: string required: - type type: object diff --git a/config/crd/bases/mongodbcommunity.mongodb.com_mongodbcommunity.yaml b/config/crd/bases/mongodbcommunity.mongodb.com_mongodbcommunity.yaml index a0004e22e..36d5c892d 100644 --- a/config/crd/bases/mongodbcommunity.mongodb.com_mongodbcommunity.yaml +++ b/config/crd/bases/mongodbcommunity.mongodb.com_mongodbcommunity.yaml @@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.15.0 + controller-gen.kubebuilder.io/version: v0.18.0 service.binding: path={.metadata.name}-{.spec.users[0].db}-{.spec.users[0].name},objectType=Secret service.binding/connectionString: path={.metadata.name}-{.spec.users[0].db}-{.spec.users[0].name},objectType=Secret,sourceKey=connectionString.standardSrv service.binding/password: path={.metadata.name}-{.spec.users[0].db}-{.spec.users[0].name},objectType=Secret,sourceKey=password @@ -330,13 +330,24 @@ spec: authentication: properties: agentCertificateSecretRef: - $ref: '#/definitions/k8s.io~1api~1core~1v1~0LocalObjectReference' description: |- AgentCertificateSecret is a reference to a Secret containing the certificate and the key for the automation agent The secret needs to have available: - certificate under key: "tls.crt" - private key under key: "tls.key" If additionally, tls.pem is present, then it needs to be equal to the concatenation of tls.crt and tls.key + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + type: object + x-kubernetes-map-type: atomic agentMode: description: AgentMode contains the authentication mode used by the automation agent. @@ -455,24 +466,57 @@ spec: communication properties: caCertificateSecretRef: - $ref: '#/definitions/k8s.io~1api~1core~1v1~0LocalObjectReference' description: |- CaCertificateSecret is a reference to a Secret containing the certificate for the CA which signed the server certificates The certificate is expected to be available under the key "ca.crt" + 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 caConfigMapRef: - $ref: '#/definitions/k8s.io~1api~1core~1v1~0LocalObjectReference' description: |- CaConfigMap is a reference to a ConfigMap containing the certificate for the CA which signed the server certificates The certificate is expected to be available under the key "ca.crt" This field is ignored when CaCertificateSecretRef is configured + 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 certificateKeySecretRef: - $ref: '#/definitions/k8s.io~1api~1core~1v1~0LocalObjectReference' description: |- CertificateKeySecret is a reference to a Secret containing a private key and certificate to use for TLS. The key and cert are expected to be PEM encoded and available at "tls.key" and "tls.crt". This is the same format used for the standard "kubernetes.io/tls" Secret type, but no specific type is required. Alternatively, an entry tls.pem, containing the concatenation of cert and key, can be provided. If all of tls.pem, tls.crt and tls.key are present, the tls.pem one needs to be equal to the concatenation of tls.crt and tls.key + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + type: object + x-kubernetes-map-type: atomic enabled: type: boolean optional: diff --git a/helm_chart/crds/mongodb.com_mongodbsearch.yaml b/helm_chart/crds/mongodb.com_mongodbsearch.yaml index 38dc38f2a..08b4fa173 100644 --- a/helm_chart/crds/mongodb.com_mongodbsearch.yaml +++ b/helm_chart/crds/mongodb.com_mongodbsearch.yaml @@ -177,9 +177,7 @@ spec: This field is effectively required, but due to backwards compatibility is allowed to be empty. Instances of this type with an empty value here are almost certainly wrong. - TODO: Add other useful fields. apiVersion, kind, uid? More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names - TODO: Drop `kubebuilder:default` when controller-gen doesn't need it https://github.com/kubernetes-sigs/kubebuilder/issues/3896. type: string type: object x-kubernetes-map-type: atomic @@ -200,10 +198,8 @@ spec: type: string type: array keyFileSecretRef: - description: |- - SecretKeyRef is a reference to a value in a given secret in the same - namespace. Based on: - https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.15/#secretkeyselector-v1-core + description: mongod keyfile used to connect to the external + MongoDB deployment properties: key: type: string @@ -213,6 +209,7 @@ spec: - name type: object tls: + description: TLS configuration for the external MongoDB deployment properties: ca: description: |- @@ -226,9 +223,7 @@ spec: This field is effectively required, but due to backwards compatibility is allowed to be empty. Instances of this type with an empty value here are almost certainly wrong. - TODO: Add other useful fields. apiVersion, kind, uid? More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names - TODO: Drop `kubebuilder:default` when controller-gen doesn't need it https://github.com/kubernetes-sigs/kubebuilder/issues/3896. type: string type: object x-kubernetes-map-type: atomic diff --git a/public/crds.yaml b/public/crds.yaml index 4afc63e97..42ca6a290 100644 --- a/public/crds.yaml +++ b/public/crds.yaml @@ -4199,9 +4199,7 @@ spec: This field is effectively required, but due to backwards compatibility is allowed to be empty. Instances of this type with an empty value here are almost certainly wrong. - TODO: Add other useful fields. apiVersion, kind, uid? More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names - TODO: Drop `kubebuilder:default` when controller-gen doesn't need it https://github.com/kubernetes-sigs/kubebuilder/issues/3896. type: string type: object x-kubernetes-map-type: atomic @@ -4222,10 +4220,8 @@ spec: type: string type: array keyFileSecretRef: - description: |- - SecretKeyRef is a reference to a value in a given secret in the same - namespace. Based on: - https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.15/#secretkeyselector-v1-core + description: mongod keyfile used to connect to the external + MongoDB deployment properties: key: type: string @@ -4235,6 +4231,7 @@ spec: - name type: object tls: + description: TLS configuration for the external MongoDB deployment properties: ca: description: |- @@ -4248,9 +4245,7 @@ spec: This field is effectively required, but due to backwards compatibility is allowed to be empty. Instances of this type with an empty value here are almost certainly wrong. - TODO: Add other useful fields. apiVersion, kind, uid? More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names - TODO: Drop `kubebuilder:default` when controller-gen doesn't need it https://github.com/kubernetes-sigs/kubebuilder/issues/3896. type: string type: object x-kubernetes-map-type: atomic From 94df6129495a1371bf4a8cadafd64e61cb88bec2 Mon Sep 17 00:00:00 2001 From: Yavor Georgiev Date: Thu, 11 Sep 2025 01:46:49 +0300 Subject: [PATCH 04/29] replace most literals with constants --- api/v1/search/mongodbsearch_types.go | 14 ++++-- .../operator/mongodbreplicaset_controller.go | 4 +- .../enterprise_search_source.go | 2 +- .../mongodbsearch_reconcile_helper.go | 10 ++-- .../search_controller/search_construction.go | 48 ++++++++++++------- 5 files changed, 48 insertions(+), 30 deletions(-) diff --git a/api/v1/search/mongodbsearch_types.go b/api/v1/search/mongodbsearch_types.go index c20884f8d..d11980acb 100644 --- a/api/v1/search/mongodbsearch_types.go +++ b/api/v1/search/mongodbsearch_types.go @@ -156,14 +156,20 @@ func (s *MongoDBSearch) MongotConfigConfigMapNamespacedName() types.NamespacedNa } func (s *MongoDBSearch) SourceUserPasswordSecretRef() *userv1.SecretKeyRef { + var syncUserPasswordSecretKey *userv1.SecretKeyRef if s.Spec.Source != nil && s.Spec.Source.PasswordSecretRef != nil { - return s.Spec.Source.PasswordSecretRef + syncUserPasswordSecretKey = s.Spec.Source.PasswordSecretRef + } else { + syncUserPasswordSecretKey = &userv1.SecretKeyRef{ + Name: fmt.Sprintf("%s-%s-password", s.Name, s.SourceUsername()), + } } - return &userv1.SecretKeyRef{ - Name: fmt.Sprintf("%s-%s-password", s.Name, MongotDefaultSyncSourceUsername), - Key: "password", + if syncUserPasswordSecretKey.Key == "" { + syncUserPasswordSecretKey.Key = "password" } + + return syncUserPasswordSecretKey } func (s *MongoDBSearch) SourceUsername() string { diff --git a/controllers/operator/mongodbreplicaset_controller.go b/controllers/operator/mongodbreplicaset_controller.go index fad1ec6b4..621ec6dd2 100644 --- a/controllers/operator/mongodbreplicaset_controller.go +++ b/controllers/operator/mongodbreplicaset_controller.go @@ -677,12 +677,12 @@ func (r *ReconcileMongoDbReplicaSet) applySearchOverrides(ctx context.Context, r func (r *ReconcileMongoDbReplicaSet) mirrorKeyfileIntoSecretForMongot(ctx context.Context, d om.Deployment, rs *mdbv1.MongoDB, log *zap.SugaredLogger) error { keyfileContents := maputil.ReadMapValueAsString(d, "auth", "key") - keyfileSecret := &corev1.Secret{ObjectMeta: metav1.ObjectMeta{Name: fmt.Sprintf("%s-keyfile", rs.Name), Namespace: rs.Namespace}} + keyfileSecret := &corev1.Secret{ObjectMeta: metav1.ObjectMeta{Name: fmt.Sprintf("%s-%s", rs.Name, search_controller.MongotKeyfileFilename), 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} + keyfileSecret.StringData = map[string]string{search_controller.MongotKeyfileFilename: keyfileContents} return controllerutil.SetOwnerReference(rs, keyfileSecret, r.client.Scheme()) }) if err != nil { diff --git a/controllers/search_controller/enterprise_search_source.go b/controllers/search_controller/enterprise_search_source.go index c1256fbbb..45e2d0f2d 100644 --- a/controllers/search_controller/enterprise_search_source.go +++ b/controllers/search_controller/enterprise_search_source.go @@ -47,7 +47,7 @@ func (r EnterpriseResourceSearchSource) TLSConfig() *TLSSourceConfig { } func (r EnterpriseResourceSearchSource) KeyfileSecretName() string { - return fmt.Sprintf("%s-keyfile", r.Name) + return fmt.Sprintf("%s-%s", r.Name, MongotKeyfileFilename) } func (r EnterpriseResourceSearchSource) Validate() error { diff --git a/controllers/search_controller/mongodbsearch_reconcile_helper.go b/controllers/search_controller/mongodbsearch_reconcile_helper.go index 4b384a707..635ebe7fc 100644 --- a/controllers/search_controller/mongodbsearch_reconcile_helper.go +++ b/controllers/search_controller/mongodbsearch_reconcile_helper.go @@ -150,7 +150,7 @@ func (r *MongoDBSearchReconcileHelper) ensureSourceKeyfile(ctx context.Context, // make sure mongot pods get restarted if the keyfile changes statefulset.WithPodSpecTemplate(podtemplatespec.WithAnnotations( map[string]string{ - "keyfileHash": hashBytes(keyfileSecret.Data["keyfile"]), + "keyfileHash": hashBytes(keyfileSecret.Data[MongotKeyfileFilename]), }, )), ), nil @@ -211,7 +211,7 @@ func (r *MongoDBSearchReconcileHelper) ensureMongotConfig(ctx context.Context, l op, err := controllerutil.CreateOrUpdate(ctx, r.client, cm, func() error { resourceVersion := cm.ResourceVersion - cm.Data["config.yml"] = string(configData) + cm.Data[MongotConfigFilename] = string(configData) cm.ResourceVersion = resourceVersion @@ -355,21 +355,21 @@ func createMongotConfig(search *searchv1.MongoDBSearch, db SearchSourceDBResourc ReplicaSet: mongot.ConfigReplicaSet{ HostAndPort: hostAndPorts, Username: search.SourceUsername(), - PasswordFile: "/tmp/sourceUserPassword", + PasswordFile: TempSourceUserPasswordPath, TLS: ptr.To(false), ReadPreference: ptr.To("secondaryPreferred"), AuthSource: ptr.To("admin"), }, } config.Storage = mongot.ConfigStorage{ - DataPath: "/mongot/data/config.yml", + DataPath: MongotDataPath, } config.Server = mongot.ConfigServer{ Wireproto: &mongot.ConfigWireproto{ Address: "0.0.0.0:27027", Authentication: &mongot.ConfigAuthentication{ Mode: "keyfile", - KeyFile: "/tmp/keyfile", + KeyFile: TempKeyfilePath, }, }, } diff --git a/controllers/search_controller/search_construction.go b/controllers/search_controller/search_construction.go index ffaa72358..1d1e1ca5c 100644 --- a/controllers/search_controller/search_construction.go +++ b/controllers/search_controller/search_construction.go @@ -1,6 +1,8 @@ package search_controller import ( + "fmt" + "k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/util/intstr" @@ -19,9 +21,18 @@ import ( ) const ( - MongotContainerName = "mongot" - SearchLivenessProbePath = "/health" - SearchReadinessProbePath = "/health" // Todo: Update this when search GA is available + MongotContainerName = "mongot" + MongotConfigFilename = "config.yml" + MongotConfigPath = "/mongot/" + MongotConfigFilename + MongotDataPath = "/mongot/data" + MongotKeyfileFilename = "keyfile" + MongotKeyfilePath = "/mongot/" + MongotKeyfileFilename + tempVolumePath = "/tmp" + TempKeyfilePath = tempVolumePath + "/" + MongotKeyfileFilename + MongotSourceUserPasswordPath = "/mongot/sourceUserPassword" // #nosec G101 -- This is not a hardcoded password, just a path to a file containing the password + TempSourceUserPasswordPath = tempVolumePath + "/" + "sourceUserPassword" + SearchLivenessProbePath = "/health" + SearchReadinessProbePath = "/health" // Todo: Update this when search GA is available ) // SearchSourceDBResource is an object wrapping a MongoDBCommunity object @@ -48,23 +59,24 @@ func CreateSearchStatefulSetFunc(mdbSearch *searchv1.MongoDBSearch, sourceDBReso } tmpVolume := statefulset.CreateVolumeFromEmptyDir("tmp") - tmpVolumeMount := statefulset.CreateVolumeMount(tmpVolume.Name, "/tmp", statefulset.WithReadOnly(false)) + tmpVolumeMount := statefulset.CreateVolumeMount(tmpVolume.Name, tempVolumePath, statefulset.WithReadOnly(false)) dataVolumeName := "data" keyfileVolumeName := "keyfile" sourceUserPasswordVolumeName := "password" mongotConfigVolumeName := "config" - pvcVolumeMount := statefulset.CreateVolumeMount(dataVolumeName, "/mongot/data", statefulset.WithSubPath("data")) + pvcVolumeMount := statefulset.CreateVolumeMount(dataVolumeName, MongotDataPath, statefulset.WithSubPath("data")) keyfileVolume := statefulset.CreateVolumeFromSecret(keyfileVolumeName, sourceDBResource.KeyfileSecretName()) - keyfileVolumeMount := statefulset.CreateVolumeMount(keyfileVolumeName, "/mongot/keyfile", statefulset.WithReadOnly(true)) + keyfileVolumeMount := statefulset.CreateVolumeMount(keyfileVolumeName, MongotKeyfilePath, statefulset.WithReadOnly(true), statefulset.WithSubPath(MongotKeyfileFilename)) - sourceUserPasswordVolume := statefulset.CreateVolumeFromSecret(sourceUserPasswordVolumeName, mdbSearch.SourceUserPasswordSecretRef().Name) - sourceUserPasswordVolumeMount := statefulset.CreateVolumeMount(sourceUserPasswordVolumeName, "/mongot/sourceUserPassword", statefulset.WithReadOnly(true)) + sourceUserPasswordSecretKey := mdbSearch.SourceUserPasswordSecretRef() + sourceUserPasswordVolume := statefulset.CreateVolumeFromSecret(sourceUserPasswordVolumeName, sourceUserPasswordSecretKey.Name) + sourceUserPasswordVolumeMount := statefulset.CreateVolumeMount(sourceUserPasswordVolumeName, MongotSourceUserPasswordPath, statefulset.WithReadOnly(true), statefulset.WithSubPath(sourceUserPasswordSecretKey.Key)) mongotConfigVolume := statefulset.CreateVolumeFromConfigMap(mongotConfigVolumeName, mdbSearch.MongotConfigConfigMapNamespacedName().Name) - mongotConfigVolumeMount := statefulset.CreateVolumeMount(mongotConfigVolumeName, "/mongot/config", statefulset.WithReadOnly(true)) + mongotConfigVolumeMount := statefulset.CreateVolumeMount(mongotConfigVolumeName, MongotConfigPath, statefulset.WithReadOnly(true), statefulset.WithSubPath(MongotConfigFilename)) var persistenceConfig *common.PersistenceConfig if mdbSearch.Spec.Persistence != nil && mdbSearch.Spec.Persistence.SingleConfig != nil { @@ -136,17 +148,17 @@ func mongodbSearchContainer(mdbSearch *searchv1.MongoDBSearch, volumeMounts []co container.WithCommand([]string{"sh"}), container.WithArgs([]string{ "-c", - ` -cp /mongot/keyfile/keyfile /tmp/keyfile -chown 2000:2000 /tmp/keyfile -chmod 0600 /tmp/keyfile + fmt.Sprintf(` +cp %[1]s %[2]s +chown 2000:2000 %[2]s +chmod 0600 %[2]s -cp /mongot/sourceUserPassword/password /tmp/sourceUserPassword -chown 2000:2000 /tmp/sourceUserPassword -chmod 0600 /tmp/sourceUserPassword +cp %[3]s %[4]s +chown 2000:2000 %[4]s +chmod 0600 %[4]s -/mongot-community/mongot --config /mongot/config/config.yml -`, +/mongot-community/mongot --config /mongot/config.yml +`, MongotKeyfilePath, TempKeyfilePath, MongotSourceUserPasswordPath, TempSourceUserPasswordPath), }), containerSecurityContext, ) From 84a27c864693716aeef74c84078df7bafde95df3 Mon Sep 17 00:00:00 2001 From: Yavor Georgiev Date: Thu, 11 Sep 2025 02:17:23 +0300 Subject: [PATCH 05/29] rename search_controller to searchcontroller --- .../operator/mongodbreplicaset_controller.go | 14 ++++++------- .../operator/mongodbsearch_controller.go | 20 +++++++++---------- .../operator/mongodbsearch_controller_test.go | 12 +++++------ .../community_search_source.go | 2 +- .../community_search_source_test.go | 2 +- .../enterprise_search_source.go | 2 +- .../enterprise_search_source_test.go | 2 +- .../external_search_source.go | 2 +- .../mongodbsearch_reconcile_helper.go | 2 +- .../mongodbsearch_reconcile_helper_test.go | 2 +- .../search_construction.go | 2 +- main.go | 4 ++-- .../controllers/replica_set_controller.go | 8 ++++---- 13 files changed, 37 insertions(+), 37 deletions(-) rename controllers/{search_controller => searchcontroller}/community_search_source.go (99%) rename controllers/{search_controller => searchcontroller}/community_search_source_test.go (99%) rename controllers/{search_controller => searchcontroller}/enterprise_search_source.go (99%) rename controllers/{search_controller => searchcontroller}/enterprise_search_source_test.go (99%) rename controllers/{search_controller => searchcontroller}/external_search_source.go (98%) rename controllers/{search_controller => searchcontroller}/mongodbsearch_reconcile_helper.go (99%) rename controllers/{search_controller => searchcontroller}/mongodbsearch_reconcile_helper_test.go (99%) rename controllers/{search_controller => searchcontroller}/search_construction.go (99%) diff --git a/controllers/operator/mongodbreplicaset_controller.go b/controllers/operator/mongodbreplicaset_controller.go index 621ec6dd2..66f392250 100644 --- a/controllers/operator/mongodbreplicaset_controller.go +++ b/controllers/operator/mongodbreplicaset_controller.go @@ -44,7 +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" + "github.com/mongodb/mongodb-kubernetes/controllers/searchcontroller" 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" @@ -657,7 +657,7 @@ func (r *ReconcileMongoDbReplicaSet) applySearchOverrides(ctx context.Context, r if rs.Spec.AdditionalMongodConfig == nil { rs.Spec.AdditionalMongodConfig = mdbv1.NewEmptyAdditionalMongodConfig() } - searchMongodConfig := search_controller.GetMongodConfigParameters(search) + searchMongodConfig := searchcontroller.GetMongodConfigParameters(search) rs.Spec.AdditionalMongodConfig.AddOption("setParameter", searchMongodConfig["setParameter"]) mdbVersion, err := semver.ParseTolerant(rs.Spec.Version) @@ -669,7 +669,7 @@ func (r *ReconcileMongoDbReplicaSet) applySearchOverrides(ctx context.Context, r if rs.Spec.Security == nil { rs.Spec.Security = &mdbv1.Security{} } - rs.Spec.Security.Roles = append(rs.Spec.Security.Roles, search_controller.SearchCoordinatorRole()) + rs.Spec.Security.Roles = append(rs.Spec.Security.Roles, searchcontroller.SearchCoordinatorRole()) } return true @@ -677,12 +677,12 @@ func (r *ReconcileMongoDbReplicaSet) applySearchOverrides(ctx context.Context, r func (r *ReconcileMongoDbReplicaSet) mirrorKeyfileIntoSecretForMongot(ctx context.Context, d om.Deployment, rs *mdbv1.MongoDB, log *zap.SugaredLogger) error { keyfileContents := maputil.ReadMapValueAsString(d, "auth", "key") - keyfileSecret := &corev1.Secret{ObjectMeta: metav1.ObjectMeta{Name: fmt.Sprintf("%s-%s", rs.Name, search_controller.MongotKeyfileFilename), Namespace: rs.Namespace}} + keyfileSecret := &corev1.Secret{ObjectMeta: metav1.ObjectMeta{Name: fmt.Sprintf("%s-%s", rs.Name, searchcontroller.MongotKeyfileFilename), 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{search_controller.MongotKeyfileFilename: keyfileContents} + keyfileSecret.StringData = map[string]string{searchcontroller.MongotKeyfileFilename: keyfileContents} return controllerutil.SetOwnerReference(rs, keyfileSecret, r.client.Scheme()) }) if err != nil { @@ -696,7 +696,7 @@ func (r *ReconcileMongoDbReplicaSet) lookupCorrespondingSearchResource(ctx conte 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()), + FieldSelector: fields.OneTermEqualSelector(searchcontroller.MongoDBSearchIndexFieldName, rs.GetNamespace()+"/"+rs.GetName()), }); err != nil { log.Debugf("Failed to list MongoDBSearch resources: %v", err) } @@ -704,7 +704,7 @@ func (r *ReconcileMongoDbReplicaSet) lookupCorrespondingSearchResource(ctx conte // 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) + searchSource := searchcontroller.NewEnterpriseResourceSearchSource(rs) if searchSource.Validate() == nil { search = &searchList.Items[0] } diff --git a/controllers/operator/mongodbsearch_controller.go b/controllers/operator/mongodbsearch_controller.go index a25efed3c..99d8254ba 100644 --- a/controllers/operator/mongodbsearch_controller.go +++ b/controllers/operator/mongodbsearch_controller.go @@ -20,7 +20,7 @@ import ( mdbv1 "github.com/mongodb/mongodb-kubernetes/api/v1/mdb" searchv1 "github.com/mongodb/mongodb-kubernetes/api/v1/search" "github.com/mongodb/mongodb-kubernetes/controllers/operator/watch" - "github.com/mongodb/mongodb-kubernetes/controllers/search_controller" + "github.com/mongodb/mongodb-kubernetes/controllers/searchcontroller" 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/pkg/kube/commoncontroller" @@ -31,10 +31,10 @@ import ( type MongoDBSearchReconciler struct { kubeClient kubernetesClient.Client watch *watch.ResourceWatcher - operatorSearchConfig search_controller.OperatorSearchConfig + operatorSearchConfig searchcontroller.OperatorSearchConfig } -func newMongoDBSearchReconciler(client client.Client, operatorSearchConfig search_controller.OperatorSearchConfig) *MongoDBSearchReconciler { +func newMongoDBSearchReconciler(client client.Client, operatorSearchConfig searchcontroller.OperatorSearchConfig) *MongoDBSearchReconciler { return &MongoDBSearchReconciler{ kubeClient: kubernetesClient.NewClient(client), watch: watch.NewResourceWatcher(), @@ -74,14 +74,14 @@ func (r *MongoDBSearchReconciler) Reconcile(ctx context.Context, request reconci r.watch.AddWatchedResourceIfNotAdded(mdbSearch.Spec.Security.TLS.CertificateKeySecret.Name, mdbSearch.Namespace, watch.Secret, mdbSearch.NamespacedName()) } - reconcileHelper := search_controller.NewMongoDBSearchReconcileHelper(kubernetesClient.NewClient(r.kubeClient), mdbSearch, searchSource, r.operatorSearchConfig) + reconcileHelper := searchcontroller.NewMongoDBSearchReconcileHelper(kubernetesClient.NewClient(r.kubeClient), mdbSearch, searchSource, r.operatorSearchConfig) return reconcileHelper.Reconcile(ctx, log).ReconcileResult() } -func (r *MongoDBSearchReconciler) getSourceMongoDBForSearch(ctx context.Context, kubeClient client.Client, search *searchv1.MongoDBSearch, log *zap.SugaredLogger) (search_controller.SearchSourceDBResource, error) { +func (r *MongoDBSearchReconciler) getSourceMongoDBForSearch(ctx context.Context, kubeClient client.Client, search *searchv1.MongoDBSearch, log *zap.SugaredLogger) (searchcontroller.SearchSourceDBResource, error) { if search.IsExternalMongoDBSource() { - return search_controller.NewExternalSearchSource(search.Namespace, search.Spec.Source.ExternalMongoDBSource), nil + return searchcontroller.NewExternalSearchSource(search.Namespace, search.Spec.Source.ExternalMongoDBSource), nil } sourceMongoDBResourceRef := search.GetMongoDBResourceRef() @@ -99,7 +99,7 @@ func (r *MongoDBSearchReconciler) getSourceMongoDBForSearch(ctx context.Context, } } else { r.watch.AddWatchedResourceIfNotAdded(sourceMongoDBResourceRef.Name, sourceMongoDBResourceRef.Namespace, watch.MongoDB, search.NamespacedName()) - return search_controller.NewEnterpriseResourceSearchSource(mdb), nil + return searchcontroller.NewEnterpriseResourceSearchSource(mdb), nil } mdbc := &mdbcv1.MongoDBCommunity{} @@ -109,7 +109,7 @@ func (r *MongoDBSearchReconciler) getSourceMongoDBForSearch(ctx context.Context, } } else { r.watch.AddWatchedResourceIfNotAdded(sourceMongoDBResourceRef.Name, sourceMongoDBResourceRef.Namespace, "MongoDBCommunity", search.NamespacedName()) - return search_controller.NewCommunityResourceSearchSource(mdbc), nil + return searchcontroller.NewCommunityResourceSearchSource(mdbc), nil } return nil, xerrors.Errorf("No database resource named %s found", sourceName) @@ -125,8 +125,8 @@ func mdbcSearchIndexBuilder(rawObj client.Object) []string { return []string{resourceRef.Namespace + "/" + resourceRef.Name} } -func AddMongoDBSearchController(ctx context.Context, mgr manager.Manager, operatorSearchConfig search_controller.OperatorSearchConfig) error { - if err := mgr.GetFieldIndexer().IndexField(ctx, &searchv1.MongoDBSearch{}, search_controller.MongoDBSearchIndexFieldName, mdbcSearchIndexBuilder); err != nil { +func AddMongoDBSearchController(ctx context.Context, mgr manager.Manager, operatorSearchConfig searchcontroller.OperatorSearchConfig) error { + if err := mgr.GetFieldIndexer().IndexField(ctx, &searchv1.MongoDBSearch{}, searchcontroller.MongoDBSearchIndexFieldName, mdbcSearchIndexBuilder); err != nil { return err } diff --git a/controllers/operator/mongodbsearch_controller_test.go b/controllers/operator/mongodbsearch_controller_test.go index 28f966ff8..789d7ccf4 100644 --- a/controllers/operator/mongodbsearch_controller_test.go +++ b/controllers/operator/mongodbsearch_controller_test.go @@ -22,7 +22,7 @@ import ( userv1 "github.com/mongodb/mongodb-kubernetes/api/v1/user" "github.com/mongodb/mongodb-kubernetes/controllers/operator/mock" "github.com/mongodb/mongodb-kubernetes/controllers/operator/workflow" - "github.com/mongodb/mongodb-kubernetes/controllers/search_controller" + "github.com/mongodb/mongodb-kubernetes/controllers/searchcontroller" mdbcv1 "github.com/mongodb/mongodb-kubernetes/mongodb-community-operator/api/v1" "github.com/mongodb/mongodb-kubernetes/mongodb-community-operator/api/v1/common" "github.com/mongodb/mongodb-kubernetes/mongodb-community-operator/pkg/mongot" @@ -53,11 +53,11 @@ func newMongoDBSearch(name, namespace, mdbcName string) *searchv1.MongoDBSearch func newSearchReconcilerWithOperatorConfig( mdbc *mdbcv1.MongoDBCommunity, - operatorConfig search_controller.OperatorSearchConfig, + operatorConfig searchcontroller.OperatorSearchConfig, searches ...*searchv1.MongoDBSearch, ) (*MongoDBSearchReconciler, client.Client) { builder := mock.NewEmptyFakeClientBuilder() - builder.WithIndex(&searchv1.MongoDBSearch{}, search_controller.MongoDBSearchIndexFieldName, mdbcSearchIndexBuilder) + builder.WithIndex(&searchv1.MongoDBSearch{}, searchcontroller.MongoDBSearchIndexFieldName, mdbcSearchIndexBuilder) if mdbc != nil { keyfileSecret := &corev1.Secret{ @@ -87,7 +87,7 @@ func newSearchReconciler( mdbc *mdbcv1.MongoDBCommunity, searches ...*searchv1.MongoDBSearch, ) (*MongoDBSearchReconciler, client.Client) { - return newSearchReconcilerWithOperatorConfig(mdbc, search_controller.OperatorSearchConfig{}, searches...) + return newSearchReconcilerWithOperatorConfig(mdbc, searchcontroller.OperatorSearchConfig{}, searches...) } func buildExpectedMongotConfig(search *searchv1.MongoDBSearch, mdbc *mdbcv1.MongoDBCommunity) mongot.Config { @@ -260,7 +260,7 @@ func TestMongoDBSearchReconcile_InvalidSearchImageVersion(t *testing.T) { Spec: corev1.PodSpec{ Containers: []corev1.Container{ { - Name: search_controller.MongotContainerName, + Name: searchcontroller.MongotContainerName, Image: "testrepo/mongot:1.47.0", }, }, @@ -280,7 +280,7 @@ func TestMongoDBSearchReconcile_InvalidSearchImageVersion(t *testing.T) { search.Spec.Version = tc.specVersion search.Spec.StatefulSetConfiguration = tc.statefulSetConfig - operatorConfig := search_controller.OperatorSearchConfig{ + operatorConfig := searchcontroller.OperatorSearchConfig{ SearchVersion: tc.operatorVersion, } reconciler, _ := newSearchReconcilerWithOperatorConfig(mdbc, operatorConfig, search) diff --git a/controllers/search_controller/community_search_source.go b/controllers/searchcontroller/community_search_source.go similarity index 99% rename from controllers/search_controller/community_search_source.go rename to controllers/searchcontroller/community_search_source.go index 8b10a1cbf..81bd38003 100644 --- a/controllers/search_controller/community_search_source.go +++ b/controllers/searchcontroller/community_search_source.go @@ -1,4 +1,4 @@ -package search_controller +package searchcontroller import ( "fmt" diff --git a/controllers/search_controller/community_search_source_test.go b/controllers/searchcontroller/community_search_source_test.go similarity index 99% rename from controllers/search_controller/community_search_source_test.go rename to controllers/searchcontroller/community_search_source_test.go index 90bf9967e..68cf5e449 100644 --- a/controllers/search_controller/community_search_source_test.go +++ b/controllers/searchcontroller/community_search_source_test.go @@ -1,4 +1,4 @@ -package search_controller +package searchcontroller import ( "testing" diff --git a/controllers/search_controller/enterprise_search_source.go b/controllers/searchcontroller/enterprise_search_source.go similarity index 99% rename from controllers/search_controller/enterprise_search_source.go rename to controllers/searchcontroller/enterprise_search_source.go index 45e2d0f2d..6b35f5e48 100644 --- a/controllers/search_controller/enterprise_search_source.go +++ b/controllers/searchcontroller/enterprise_search_source.go @@ -1,4 +1,4 @@ -package search_controller +package searchcontroller import ( "fmt" diff --git a/controllers/search_controller/enterprise_search_source_test.go b/controllers/searchcontroller/enterprise_search_source_test.go similarity index 99% rename from controllers/search_controller/enterprise_search_source_test.go rename to controllers/searchcontroller/enterprise_search_source_test.go index 1362ab605..ac9eaae5b 100644 --- a/controllers/search_controller/enterprise_search_source_test.go +++ b/controllers/searchcontroller/enterprise_search_source_test.go @@ -1,4 +1,4 @@ -package search_controller +package searchcontroller import ( "testing" diff --git a/controllers/search_controller/external_search_source.go b/controllers/searchcontroller/external_search_source.go similarity index 98% rename from controllers/search_controller/external_search_source.go rename to controllers/searchcontroller/external_search_source.go index d15f49799..70025f305 100644 --- a/controllers/search_controller/external_search_source.go +++ b/controllers/searchcontroller/external_search_source.go @@ -1,4 +1,4 @@ -package search_controller +package searchcontroller import ( "k8s.io/apimachinery/pkg/types" diff --git a/controllers/search_controller/mongodbsearch_reconcile_helper.go b/controllers/searchcontroller/mongodbsearch_reconcile_helper.go similarity index 99% rename from controllers/search_controller/mongodbsearch_reconcile_helper.go rename to controllers/searchcontroller/mongodbsearch_reconcile_helper.go index 635ebe7fc..13e4bc26d 100644 --- a/controllers/search_controller/mongodbsearch_reconcile_helper.go +++ b/controllers/searchcontroller/mongodbsearch_reconcile_helper.go @@ -1,4 +1,4 @@ -package search_controller +package searchcontroller import ( "context" diff --git a/controllers/search_controller/mongodbsearch_reconcile_helper_test.go b/controllers/searchcontroller/mongodbsearch_reconcile_helper_test.go similarity index 99% rename from controllers/search_controller/mongodbsearch_reconcile_helper_test.go rename to controllers/searchcontroller/mongodbsearch_reconcile_helper_test.go index 5a2a757ce..7fa9a05f0 100644 --- a/controllers/search_controller/mongodbsearch_reconcile_helper_test.go +++ b/controllers/searchcontroller/mongodbsearch_reconcile_helper_test.go @@ -1,4 +1,4 @@ -package search_controller +package searchcontroller import ( "testing" diff --git a/controllers/search_controller/search_construction.go b/controllers/searchcontroller/search_construction.go similarity index 99% rename from controllers/search_controller/search_construction.go rename to controllers/searchcontroller/search_construction.go index 1d1e1ca5c..a85c66379 100644 --- a/controllers/search_controller/search_construction.go +++ b/controllers/searchcontroller/search_construction.go @@ -1,4 +1,4 @@ -package search_controller +package searchcontroller import ( "fmt" diff --git a/main.go b/main.go index 37ae65f94..6a2d755b0 100644 --- a/main.go +++ b/main.go @@ -43,7 +43,7 @@ import ( omv1 "github.com/mongodb/mongodb-kubernetes/api/v1/om" "github.com/mongodb/mongodb-kubernetes/controllers/operator" "github.com/mongodb/mongodb-kubernetes/controllers/operator/construct" - "github.com/mongodb/mongodb-kubernetes/controllers/search_controller" + "github.com/mongodb/mongodb-kubernetes/controllers/searchcontroller" mcov1 "github.com/mongodb/mongodb-kubernetes/mongodb-community-operator/api/v1" mcoController "github.com/mongodb/mongodb-kubernetes/mongodb-community-operator/controllers" mcoConstruct "github.com/mongodb/mongodb-kubernetes/mongodb-community-operator/controllers/construct" @@ -390,7 +390,7 @@ func setupMongoDBMultiClusterCRD(ctx context.Context, mgr manager.Manager, image } func setupMongoDBSearchCRD(ctx context.Context, mgr manager.Manager) error { - return operator.AddMongoDBSearchController(ctx, mgr, search_controller.OperatorSearchConfig{ + return operator.AddMongoDBSearchController(ctx, mgr, searchcontroller.OperatorSearchConfig{ SearchRepo: env.ReadOrPanic("MDB_SEARCH_COMMUNITY_REPO_URL"), SearchName: env.ReadOrPanic("MDB_SEARCH_COMMUNITY_NAME"), SearchVersion: env.ReadOrPanic("MDB_SEARCH_COMMUNITY_VERSION"), diff --git a/mongodb-community-operator/controllers/replica_set_controller.go b/mongodb-community-operator/controllers/replica_set_controller.go index 67e062648..f042aa1c2 100644 --- a/mongodb-community-operator/controllers/replica_set_controller.go +++ b/mongodb-community-operator/controllers/replica_set_controller.go @@ -30,7 +30,7 @@ import ( k8sClient "sigs.k8s.io/controller-runtime/pkg/client" searchv1 "github.com/mongodb/mongodb-kubernetes/api/v1/search" - "github.com/mongodb/mongodb-kubernetes/controllers/search_controller" + "github.com/mongodb/mongodb-kubernetes/controllers/searchcontroller" mdbv1 "github.com/mongodb/mongodb-kubernetes/mongodb-community-operator/api/v1" "github.com/mongodb/mongodb-kubernetes/mongodb-community-operator/controllers/construct" "github.com/mongodb/mongodb-kubernetes/mongodb-community-operator/controllers/predicates" @@ -707,7 +707,7 @@ func (r ReplicaSetReconciler) buildAutomationConfig(ctx context.Context, mdb mdb var search *searchv1.MongoDBSearch searchList := &searchv1.MongoDBSearchList{} if err := r.client.List(ctx, searchList, &k8sClient.ListOptions{ - FieldSelector: fields.OneTermEqualSelector(search_controller.MongoDBSearchIndexFieldName, mdb.Namespace+"/"+mdb.Name), + FieldSelector: fields.OneTermEqualSelector(searchcontroller.MongoDBSearchIndexFieldName, mdb.Namespace+"/"+mdb.Name), }); err != nil { r.log.Debug(err) } @@ -715,7 +715,7 @@ func (r ReplicaSetReconciler) buildAutomationConfig(ctx context.Context, mdb mdb // and that this resource passes search validations. If either fails, proceed without a search target // for the mongod automation config. if len(searchList.Items) == 1 { - searchSource := search_controller.NewCommunityResourceSearchSource(&mdb) + searchSource := searchcontroller.NewCommunityResourceSearchSource(&mdb) if searchSource.Validate() == nil { search = &searchList.Items[0] } @@ -842,7 +842,7 @@ func getMongodConfigSearchModification(search *searchv1.MongoDBSearch) automatio return automationconfig.NOOP() } - searchConfigParameters := search_controller.GetMongodConfigParameters(search) + searchConfigParameters := searchcontroller.GetMongodConfigParameters(search) return func(ac *automationconfig.AutomationConfig) { for i := range ac.Processes { err := mergo.Merge(&ac.Processes[i].Args26, objx.New(searchConfigParameters), mergo.WithOverride) From fee1e847f9ea8ee49c8b48b1e4f656ff4e9c8ef4 Mon Sep 17 00:00:00 2001 From: Yavor Georgiev Date: Thu, 11 Sep 2025 02:21:48 +0300 Subject: [PATCH 06/29] lower test timeouts --- .../tests/search/search_community_basic.py | 4 ++-- .../tests/search/search_community_external_mongod_basic.py | 6 +++--- .../tests/search/search_community_external_mongod_tls.py | 4 ++-- .../tests/search/search_community_tls.py | 4 ++-- .../tests/search/search_enterprise_basic.py | 6 +++--- .../tests/search/search_enterprise_tls.py | 6 +++--- 6 files changed, 15 insertions(+), 15 deletions(-) diff --git a/docker/mongodb-kubernetes-tests/tests/search/search_community_basic.py b/docker/mongodb-kubernetes-tests/tests/search/search_community_basic.py index 97e9c1af7..aa581bca8 100644 --- a/docker/mongodb-kubernetes-tests/tests/search/search_community_basic.py +++ b/docker/mongodb-kubernetes-tests/tests/search/search_community_basic.py @@ -71,7 +71,7 @@ def test_install_secrets(namespace: str, mdbs: MongoDBSearch): @mark.e2e_search_community_basic def test_create_database_resource(mdbc: MongoDBCommunity): mdbc.update() - mdbc.assert_reaches_phase(Phase.Running, timeout=1000) + mdbc.assert_reaches_phase(Phase.Running, timeout=300) @mark.e2e_search_community_basic @@ -82,7 +82,7 @@ def test_create_search_resource(mdbs: MongoDBSearch): @mark.e2e_search_community_basic def test_wait_for_community_resource_ready(mdbc: MongoDBCommunity): - mdbc.assert_reaches_phase(Phase.Running, timeout=1800) + mdbc.assert_reaches_phase(Phase.Running, timeout=300) @fixture(scope="function") diff --git a/docker/mongodb-kubernetes-tests/tests/search/search_community_external_mongod_basic.py b/docker/mongodb-kubernetes-tests/tests/search/search_community_external_mongod_basic.py index 110e134dc..e6f096460 100644 --- a/docker/mongodb-kubernetes-tests/tests/search/search_community_external_mongod_basic.py +++ b/docker/mongodb-kubernetes-tests/tests/search/search_community_external_mongod_basic.py @@ -104,18 +104,18 @@ def test_install_secrets(namespace: str, mdbs: MongoDBSearch): @mark.e2e_search_external_basic def test_create_database_resource(mdbc: MongoDBCommunity): mdbc.update() - mdbc.assert_reaches_phase(Phase.Running, timeout=1000) + mdbc.assert_reaches_phase(Phase.Running, timeout=300) @mark.e2e_search_external_basic def test_create_search_resource(mdbs: MongoDBSearch, mdbc: MongoDBCommunity): mdbs.update() - mdbs.assert_reaches_phase(Phase.Running, timeout=1000) + mdbs.assert_reaches_phase(Phase.Running, timeout=300) @mark.e2e_search_external_basic def test_wait_for_community_resource_ready(mdbc: MongoDBCommunity): - mdbc.assert_reaches_phase(Phase.Running, timeout=1800) + mdbc.assert_reaches_phase(Phase.Running, timeout=300) @fixture(scope="function") diff --git a/docker/mongodb-kubernetes-tests/tests/search/search_community_external_mongod_tls.py b/docker/mongodb-kubernetes-tests/tests/search/search_community_external_mongod_tls.py index f6a270a48..7a30ae9ed 100644 --- a/docker/mongodb-kubernetes-tests/tests/search/search_community_external_mongod_tls.py +++ b/docker/mongodb-kubernetes-tests/tests/search/search_community_external_mongod_tls.py @@ -123,7 +123,7 @@ def test_install_tls_secrets_and_configmaps( @mark.e2e_search_external_tls def test_create_database_resource(mdbc: MongoDBCommunity): mdbc.update() - mdbc.assert_reaches_phase(Phase.Running, timeout=1000) + mdbc.assert_reaches_phase(Phase.Running, timeout=300) @mark.e2e_search_external_tls @@ -154,7 +154,7 @@ def test_create_search_resource(mdbs: MongoDBSearch, mdbc: MongoDBCommunity): @mark.e2e_search_external_tls def test_wait_for_community_resource_ready(mdbc: MongoDBCommunity): - mdbc.assert_reaches_phase(Phase.Running, timeout=1800) + mdbc.assert_reaches_phase(Phase.Running, timeout=300) @fixture(scope="function") diff --git a/docker/mongodb-kubernetes-tests/tests/search/search_community_tls.py b/docker/mongodb-kubernetes-tests/tests/search/search_community_tls.py index 44418b793..af4e9121e 100644 --- a/docker/mongodb-kubernetes-tests/tests/search/search_community_tls.py +++ b/docker/mongodb-kubernetes-tests/tests/search/search_community_tls.py @@ -108,7 +108,7 @@ def test_install_tls_secrets_and_configmaps(namespace: str, mdbc: MongoDBCommuni @mark.e2e_search_community_tls def test_create_database_resource(mdbc: MongoDBCommunity): mdbc.update() - mdbc.assert_reaches_phase(Phase.Running, timeout=1000) + mdbc.assert_reaches_phase(Phase.Running, timeout=300) @mark.e2e_search_community_tls @@ -119,7 +119,7 @@ def test_create_search_resource(mdbs: MongoDBSearch): @mark.e2e_search_community_tls def test_wait_for_community_resource_ready(mdbc: MongoDBCommunity): - mdbc.assert_reaches_phase(Phase.Running, timeout=1800) + mdbc.assert_reaches_phase(Phase.Running, timeout=300) @mark.e2e_search_community_tls diff --git a/docker/mongodb-kubernetes-tests/tests/search/search_enterprise_basic.py b/docker/mongodb-kubernetes-tests/tests/search/search_enterprise_basic.py index a3de8dcf8..2acbb37de 100644 --- a/docker/mongodb-kubernetes-tests/tests/search/search_enterprise_basic.py +++ b/docker/mongodb-kubernetes-tests/tests/search/search_enterprise_basic.py @@ -105,7 +105,7 @@ def test_install_operator(namespace: str, operator_installation_config: dict[str @mark.e2e_search_enterprise_basic def test_create_database_resource(mdb: MongoDB): mdb.update() - mdb.assert_reaches_phase(Phase.Running, timeout=1000) + mdb.assert_reaches_phase(Phase.Running, timeout=300) @mark.e2e_search_enterprise_basic @@ -140,8 +140,8 @@ def test_create_search_resource(mdbs: MongoDBSearch): @mark.e2e_search_enterprise_basic def test_wait_for_database_resource_ready(mdb: MongoDB): - mdb.assert_abandons_phase(Phase.Running, timeout=1800) - mdb.assert_reaches_phase(Phase.Running, timeout=1800) + mdb.assert_abandons_phase(Phase.Running, timeout=300) + mdb.assert_reaches_phase(Phase.Running, timeout=300) for idx in range(mdb.get_members()): mongod_config = yaml.safe_load( diff --git a/docker/mongodb-kubernetes-tests/tests/search/search_enterprise_tls.py b/docker/mongodb-kubernetes-tests/tests/search/search_enterprise_tls.py index f4c0bc96f..8050ccd5c 100644 --- a/docker/mongodb-kubernetes-tests/tests/search/search_enterprise_tls.py +++ b/docker/mongodb-kubernetes-tests/tests/search/search_enterprise_tls.py @@ -134,7 +134,7 @@ def test_install_tls_secrets_and_configmaps(namespace: str, mdb: MongoDB, mdbs: @mark.e2e_search_enterprise_tls def test_create_database_resource(mdb: MongoDB): mdb.update() - mdb.assert_reaches_phase(Phase.Running, timeout=1000) + mdb.assert_reaches_phase(Phase.Running, timeout=300) @mark.e2e_search_enterprise_tls @@ -169,8 +169,8 @@ def test_create_search_resource(mdbs: MongoDBSearch): @mark.e2e_search_enterprise_tls def test_wait_for_database_resource_ready(mdb: MongoDB): - mdb.assert_abandons_phase(Phase.Running, timeout=1800) - mdb.assert_reaches_phase(Phase.Running, timeout=1800) + mdb.assert_abandons_phase(Phase.Running, timeout=300) + mdb.assert_reaches_phase(Phase.Running, timeout=300) for idx in range(mdb.get_members()): mongod_config = yaml.safe_load( From e2fe9cd0d00990813150f4ce161ab877d74a092d Mon Sep 17 00:00:00 2001 From: Yavor Georgiev Date: Thu, 11 Sep 2025 02:28:49 +0300 Subject: [PATCH 07/29] added source resolution comments --- controllers/operator/mongodbsearch_controller.go | 7 +++++++ controllers/searchcontroller/external_search_source.go | 3 +++ 2 files changed, 10 insertions(+) diff --git a/controllers/operator/mongodbsearch_controller.go b/controllers/operator/mongodbsearch_controller.go index 99d8254ba..b633fe9b4 100644 --- a/controllers/operator/mongodbsearch_controller.go +++ b/controllers/operator/mongodbsearch_controller.go @@ -80,6 +80,13 @@ func (r *MongoDBSearchReconciler) Reconcile(ctx context.Context, request reconci } func (r *MongoDBSearchReconciler) getSourceMongoDBForSearch(ctx context.Context, kubeClient client.Client, search *searchv1.MongoDBSearch, log *zap.SugaredLogger) (searchcontroller.SearchSourceDBResource, error) { + // Resolve the source database for this Search instance. + // If .spec.source.external is defined immediately return the external search source. + // Otherwise, read .spec.source.mongodbResourceRef or use the implicit database resource name (same as the Search resource's name). + // Try to get a MongoDB CR with the computed name and return the enterprise search source if successful. + // Otherwise, try to get a MongoDBCommunity CR with the same name and return the community search source. + // If everything fails just error out and the controller will retry reconciliation. + if search.IsExternalMongoDBSource() { return searchcontroller.NewExternalSearchSource(search.Namespace, search.Spec.Source.ExternalMongoDBSource), nil } diff --git a/controllers/searchcontroller/external_search_source.go b/controllers/searchcontroller/external_search_source.go index 70025f305..8b5cc2d56 100644 --- a/controllers/searchcontroller/external_search_source.go +++ b/controllers/searchcontroller/external_search_source.go @@ -19,6 +19,9 @@ type externalSearchResource struct { } func (r *externalSearchResource) Validate() error { + // We don't know anything about the external MongoDB deployment, so we can't validate it. + // Perhaps in the future the Operator could attempt to connect to the external MongoDB instance + // and validate its configuration. return nil } From d238db4f84fcaa7898a40e9014e927fee33a07ed Mon Sep 17 00:00:00 2001 From: Yavor Georgiev Date: Thu, 11 Sep 2025 02:46:34 +0300 Subject: [PATCH 08/29] storage and requests constants --- controllers/searchcontroller/search_construction.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/controllers/searchcontroller/search_construction.go b/controllers/searchcontroller/search_construction.go index a85c66379..4c9181e44 100644 --- a/controllers/searchcontroller/search_construction.go +++ b/controllers/searchcontroller/search_construction.go @@ -83,7 +83,7 @@ func CreateSearchStatefulSetFunc(mdbSearch *searchv1.MongoDBSearch, sourceDBReso persistenceConfig = mdbSearch.Spec.Persistence.SingleConfig } - defaultPersistenceConfig := common.PersistenceConfig{Storage: "10G"} + defaultPersistenceConfig := common.PersistenceConfig{Storage: util.DefaultMongodStorageSize} dataVolumeClaim := statefulset.WithVolumeClaim(dataVolumeName, construct.PvcFunc(dataVolumeName, persistenceConfig, defaultPersistenceConfig, nil)) podSecurityContext, _ := podtemplatespec.WithDefaultSecurityContextsModifications() @@ -209,6 +209,7 @@ func createSearchResourceRequirements(requirements *corev1.ResourceRequirements) } func newSearchDefaultRequirements() corev1.ResourceRequirements { + // TODO: add default limits once there is an official mongot sizing guide return corev1.ResourceRequirements{ Requests: corev1.ResourceList{ corev1.ResourceCPU: construct.ParseQuantityOrZero("2"), From 72a7340d5241ac7874db90e94d4e882309a0adb0 Mon Sep 17 00:00:00 2001 From: Yavor Georgiev Date: Thu, 11 Sep 2025 02:51:24 +0300 Subject: [PATCH 09/29] remove comment mentioning tls.pem --- api/v1/search/mongodbsearch_types.go | 2 -- config/crd/bases/mongodb.com_mongodbsearch.yaml | 2 -- helm_chart/crds/mongodb.com_mongodbsearch.yaml | 2 -- public/crds.yaml | 2 -- 4 files changed, 8 deletions(-) diff --git a/api/v1/search/mongodbsearch_types.go b/api/v1/search/mongodbsearch_types.go index d11980acb..9dedaee4f 100644 --- a/api/v1/search/mongodbsearch_types.go +++ b/api/v1/search/mongodbsearch_types.go @@ -84,8 +84,6 @@ type TLS struct { // CertificateKeySecret is a reference to a Secret containing a private key and certificate to use for TLS. // The key and cert are expected to be PEM encoded and available at "tls.key" and "tls.crt". // This is the same format used for the standard "kubernetes.io/tls" Secret type, but no specific type is required. - // Alternatively, an entry tls.pem, containing the concatenation of cert and key, can be provided. - // If all of tls.pem, tls.crt and tls.key are present, the tls.pem one needs to be equal to the concatenation of tls.crt and tls.key // +optional CertificateKeySecret corev1.LocalObjectReference `json:"certificateKeySecretRef"` } diff --git a/config/crd/bases/mongodb.com_mongodbsearch.yaml b/config/crd/bases/mongodb.com_mongodbsearch.yaml index 08b4fa173..524b9b4cb 100644 --- a/config/crd/bases/mongodb.com_mongodbsearch.yaml +++ b/config/crd/bases/mongodb.com_mongodbsearch.yaml @@ -167,8 +167,6 @@ spec: CertificateKeySecret is a reference to a Secret containing a private key and certificate to use for TLS. The key and cert are expected to be PEM encoded and available at "tls.key" and "tls.crt". This is the same format used for the standard "kubernetes.io/tls" Secret type, but no specific type is required. - Alternatively, an entry tls.pem, containing the concatenation of cert and key, can be provided. - If all of tls.pem, tls.crt and tls.key are present, the tls.pem one needs to be equal to the concatenation of tls.crt and tls.key properties: name: default: "" diff --git a/helm_chart/crds/mongodb.com_mongodbsearch.yaml b/helm_chart/crds/mongodb.com_mongodbsearch.yaml index 08b4fa173..524b9b4cb 100644 --- a/helm_chart/crds/mongodb.com_mongodbsearch.yaml +++ b/helm_chart/crds/mongodb.com_mongodbsearch.yaml @@ -167,8 +167,6 @@ spec: CertificateKeySecret is a reference to a Secret containing a private key and certificate to use for TLS. The key and cert are expected to be PEM encoded and available at "tls.key" and "tls.crt". This is the same format used for the standard "kubernetes.io/tls" Secret type, but no specific type is required. - Alternatively, an entry tls.pem, containing the concatenation of cert and key, can be provided. - If all of tls.pem, tls.crt and tls.key are present, the tls.pem one needs to be equal to the concatenation of tls.crt and tls.key properties: name: default: "" diff --git a/public/crds.yaml b/public/crds.yaml index 42ca6a290..02988192c 100644 --- a/public/crds.yaml +++ b/public/crds.yaml @@ -4189,8 +4189,6 @@ spec: CertificateKeySecret is a reference to a Secret containing a private key and certificate to use for TLS. The key and cert are expected to be PEM encoded and available at "tls.key" and "tls.crt". This is the same format used for the standard "kubernetes.io/tls" Secret type, but no specific type is required. - Alternatively, an entry tls.pem, containing the concatenation of cert and key, can be provided. - If all of tls.pem, tls.crt and tls.key are present, the tls.pem one needs to be equal to the concatenation of tls.crt and tls.key properties: name: default: "" From 5c3ea5b1b8b904b88aa15646bfe31306869486d3 Mon Sep 17 00:00:00 2001 From: Yavor Georgiev Date: Thu, 11 Sep 2025 10:26:19 +0300 Subject: [PATCH 10/29] fix broken unit test --- controllers/operator/mongodbsearch_controller_test.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/controllers/operator/mongodbsearch_controller_test.go b/controllers/operator/mongodbsearch_controller_test.go index 789d7ccf4..736f08e37 100644 --- a/controllers/operator/mongodbsearch_controller_test.go +++ b/controllers/operator/mongodbsearch_controller_test.go @@ -100,21 +100,21 @@ func buildExpectedMongotConfig(search *searchv1.MongoDBSearch, mdbc *mdbcv1.Mong ReplicaSet: mongot.ConfigReplicaSet{ HostAndPort: hostAndPorts, Username: searchv1.MongotDefaultSyncSourceUsername, - PasswordFile: "/tmp/sourceUserPassword", + PasswordFile: searchcontroller.TempSourceUserPasswordPath, TLS: ptr.To(false), ReadPreference: ptr.To("secondaryPreferred"), AuthSource: ptr.To("admin"), }, }, Storage: mongot.ConfigStorage{ - DataPath: "/mongot/data/config.yml", + DataPath: searchcontroller.MongotDataPath, }, Server: mongot.ConfigServer{ Wireproto: &mongot.ConfigWireproto{ Address: "0.0.0.0:27027", Authentication: &mongot.ConfigAuthentication{ Mode: "keyfile", - KeyFile: "/tmp/keyfile", + KeyFile: searchcontroller.TempKeyfilePath, }, TLS: mongot.ConfigTLS{Mode: mongot.ConfigTLSModeDisabled}, }, @@ -185,7 +185,7 @@ func TestMongoDBSearchReconcile_Success(t *testing.T) { expectedConfig := buildExpectedMongotConfig(search, mdbc) configYaml, err := yaml.Marshal(expectedConfig) assert.NoError(t, err) - assert.Equal(t, string(configYaml), cm.Data["config.yml"]) + assert.Equal(t, string(configYaml), cm.Data[searchcontroller.MongotConfigFilename]) sts := &appsv1.StatefulSet{} err = c.Get(ctx, search.StatefulSetNamespacedName(), sts) From 28f660289428c94af7a111b082ece8d1e5f55bd8 Mon Sep 17 00:00:00 2001 From: Anand Singh Date: Thu, 11 Sep 2025 10:38:47 +0200 Subject: [PATCH 11/29] update the keyFileSecretRef constant --- api/v1/search/mongodbsearch_types.go | 2 +- config/crd/bases/mongodb.com_mongodbsearch.yaml | 2 +- .../tests/search/search_community_external_mongod_basic.py | 2 +- .../tests/search/search_community_external_mongod_tls.py | 2 +- helm_chart/crds/mongodb.com_mongodbsearch.yaml | 2 +- public/crds.yaml | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/api/v1/search/mongodbsearch_types.go b/api/v1/search/mongodbsearch_types.go index 9dedaee4f..ce90a8d13 100644 --- a/api/v1/search/mongodbsearch_types.go +++ b/api/v1/search/mongodbsearch_types.go @@ -62,7 +62,7 @@ type MongoDBSource struct { type ExternalMongoDBSource struct { HostAndPorts []string `json:"hostAndPorts,omitempty"` // mongod keyfile used to connect to the external MongoDB deployment - KeyFileSecretKeyRef *userv1.SecretKeyRef `json:"keyFileSecretRef,omitempty"` + KeyFileSecretKeyRef *userv1.SecretKeyRef `json:"keyfileSecretRef,omitempty"` // TLS configuration for the external MongoDB deployment // +optional TLS *ExternalMongodTLS `json:"tls,omitempty"` diff --git a/config/crd/bases/mongodb.com_mongodbsearch.yaml b/config/crd/bases/mongodb.com_mongodbsearch.yaml index 524b9b4cb..6cdf7d1dc 100644 --- a/config/crd/bases/mongodb.com_mongodbsearch.yaml +++ b/config/crd/bases/mongodb.com_mongodbsearch.yaml @@ -195,7 +195,7 @@ spec: items: type: string type: array - keyFileSecretRef: + keyfileSecretRef: description: mongod keyfile used to connect to the external MongoDB deployment properties: diff --git a/docker/mongodb-kubernetes-tests/tests/search/search_community_external_mongod_basic.py b/docker/mongodb-kubernetes-tests/tests/search/search_community_external_mongod_basic.py index e6f096460..3a084b7a6 100644 --- a/docker/mongodb-kubernetes-tests/tests/search/search_community_external_mongod_basic.py +++ b/docker/mongodb-kubernetes-tests/tests/search/search_community_external_mongod_basic.py @@ -70,7 +70,7 @@ def mdbs(namespace: str, mdbc: MongoDBCommunity) -> MongoDBSearch: "source": { "external": { "hostAndPorts": seeds, - "keyFileSecretRef": {"name": f"{mdbc.name}-keyfile", "key": "keyfile"}, + "keyfileSecretRef": {"name": f"{mdbc.name}-keyfile", "key": "keyfile"}, "tls": {"enabled": False}, }, "passwordSecretRef": {"name": f"{MDBC_RESOURCE_NAME}-{MONGOT_USER_NAME}-password", "key": "password"}, diff --git a/docker/mongodb-kubernetes-tests/tests/search/search_community_external_mongod_tls.py b/docker/mongodb-kubernetes-tests/tests/search/search_community_external_mongod_tls.py index 7a30ae9ed..8ccd5298c 100644 --- a/docker/mongodb-kubernetes-tests/tests/search/search_community_external_mongod_tls.py +++ b/docker/mongodb-kubernetes-tests/tests/search/search_community_external_mongod_tls.py @@ -136,7 +136,7 @@ def test_create_search_resource(mdbs: MongoDBSearch, mdbc: MongoDBCommunity): mdbs["spec"]["source"] = { "external": { "hostAndPorts": seeds, - "keyFileSecretRef": {"name": f"{mdbc.name}-keyfile"}, + "keyfileSecretRef": {"name": f"{mdbc.name}-keyfile"}, "tls": { "enabled": True, "ca": {"name": f"{mdbc.name}-ca"}, diff --git a/helm_chart/crds/mongodb.com_mongodbsearch.yaml b/helm_chart/crds/mongodb.com_mongodbsearch.yaml index 524b9b4cb..6cdf7d1dc 100644 --- a/helm_chart/crds/mongodb.com_mongodbsearch.yaml +++ b/helm_chart/crds/mongodb.com_mongodbsearch.yaml @@ -195,7 +195,7 @@ spec: items: type: string type: array - keyFileSecretRef: + keyfileSecretRef: description: mongod keyfile used to connect to the external MongoDB deployment properties: diff --git a/public/crds.yaml b/public/crds.yaml index 02988192c..9fa6a9207 100644 --- a/public/crds.yaml +++ b/public/crds.yaml @@ -4217,7 +4217,7 @@ spec: items: type: string type: array - keyFileSecretRef: + keyfileSecretRef: description: mongod keyfile used to connect to the external MongoDB deployment properties: From 807c1f593dc9bb8fae6110fd983bc18f02644680 Mon Sep 17 00:00:00 2001 From: Yavor Georgiev Date: Thu, 11 Sep 2025 12:45:39 +0300 Subject: [PATCH 12/29] searchCoordinator role detection supports -pre and -ent versions --- .../operator/mongodbreplicaset_controller.go | 8 +--- .../mongodbsearch_reconcile_helper.go | 14 ++++++ .../mongodbsearch_reconcile_helper_test.go | 46 +++++++++++++++++++ .../controllers/replica_set_controller.go | 6 +-- 4 files changed, 65 insertions(+), 9 deletions(-) diff --git a/controllers/operator/mongodbreplicaset_controller.go b/controllers/operator/mongodbreplicaset_controller.go index 66f392250..fd2ed2cb8 100644 --- a/controllers/operator/mongodbreplicaset_controller.go +++ b/controllers/operator/mongodbreplicaset_controller.go @@ -4,7 +4,6 @@ import ( "context" "fmt" - "github.com/blang/semver" "go.uber.org/zap" "golang.org/x/xerrors" "k8s.io/apimachinery/pkg/api/errors" @@ -660,11 +659,8 @@ func (r *ReconcileMongoDbReplicaSet) applySearchOverrides(ctx context.Context, r searchMongodConfig := searchcontroller.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 searchcontroller.NeedsSearchCoordinatorRolePolyfill(rs.Spec.GetMongoDBVersion()) { + log.Infof("Polyfilling the searchCoordinator role for MongoDB %s", rs.Spec.GetMongoDBVersion()) if rs.Spec.Security == nil { rs.Spec.Security = &mdbv1.Security{} diff --git a/controllers/searchcontroller/mongodbsearch_reconcile_helper.go b/controllers/searchcontroller/mongodbsearch_reconcile_helper.go index 13e4bc26d..a8a4cc838 100644 --- a/controllers/searchcontroller/mongodbsearch_reconcile_helper.go +++ b/controllers/searchcontroller/mongodbsearch_reconcile_helper.go @@ -7,6 +7,7 @@ import ( "fmt" "strings" + "github.com/blang/semver" "github.com/ghodss/yaml" "go.uber.org/zap" "golang.org/x/xerrors" @@ -514,3 +515,16 @@ func SearchCoordinatorRole() mdbv1.MongoDBRole { AuthenticationRestrictions: nil, } } + +// Because the first Search Public Preview support MongoDB Server 8.0.10 we need to polyfill the searchCoordinator role +// TODO: Remove once we drop support for <8.2 in Search +func NeedsSearchCoordinatorRolePolyfill(mongodbVersion string) bool { + version, err := semver.ParseTolerant(mongodbVersion) + if err != nil { + // if we can't determine the version, assume no need to polyfill + return false + } + + // the searchCoordinator role is built-in from MongoDB 8.2 + return version.Major <= 8 && version.Minor < 2 +} diff --git a/controllers/searchcontroller/mongodbsearch_reconcile_helper_test.go b/controllers/searchcontroller/mongodbsearch_reconcile_helper_test.go index 7fa9a05f0..0de5e0488 100644 --- a/controllers/searchcontroller/mongodbsearch_reconcile_helper_test.go +++ b/controllers/searchcontroller/mongodbsearch_reconcile_helper_test.go @@ -95,3 +95,49 @@ func TestMongoDBSearchReconcileHelper_ValidateSingleMongoDBSearchForSearchSource }) } } + +func TestNeedsSearchCoordinatorRolePolyfill(t *testing.T) { + cases := []struct { + name string + version string + expected bool + }{ + { + name: "MongoDB 8.0.x requires polyfill", + version: "8.0.10", + expected: true, + }, + { + name: "MongoDB 8.1.x requires polyfill", + version: "8.1.0", + expected: true, + }, + { + name: "MongoDB 8.2.0-rc0 treated as 8.2 (no polyfill)", + version: "8.2.0-rc0", + expected: false, + }, + { + name: "MongoDB 8.2.0 and above do not require polyfill", + version: "8.2.0", + expected: false, + }, + { + name: "MongoDB 8.2.0-ent treated as 8.2 (no polyfill)", + version: "8.2.0-ent", + expected: false, + }, + { + name: "MongoDB 9.0.0 and above do not require polyfill", + version: "9.0.0", + expected: false, + }, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + actual := NeedsSearchCoordinatorRolePolyfill(c.version) + assert.Equal(t, c.expected, actual) + }) + } +} diff --git a/mongodb-community-operator/controllers/replica_set_controller.go b/mongodb-community-operator/controllers/replica_set_controller.go index f042aa1c2..0f02b8946 100644 --- a/mongodb-community-operator/controllers/replica_set_controller.go +++ b/mongodb-community-operator/controllers/replica_set_controller.go @@ -731,7 +731,7 @@ func (r ReplicaSetReconciler) buildAutomationConfig(ctx context.Context, mdb mdb prometheusModification, processPortManager.GetPortsModification(), getMongodConfigSearchModification(search), - searchCoordinatorCustomRoleModification(search), + searchCoordinatorCustomRoleModification(search, mdb.GetMongoDBVersion()), ) if err != nil { return automationconfig.AutomationConfig{}, fmt.Errorf("could not create an automation config: %s", err) @@ -745,8 +745,8 @@ func (r ReplicaSetReconciler) buildAutomationConfig(ctx context.Context, mdb mdb } // TODO: remove this as soon as searchCoordinator builtin role is backported -func searchCoordinatorCustomRoleModification(search *searchv1.MongoDBSearch) automationconfig.Modification { - if search == nil { +func searchCoordinatorCustomRoleModification(search *searchv1.MongoDBSearch, mongodbVersion string) automationconfig.Modification { + if search == nil || searchcontroller.NeedsSearchCoordinatorRolePolyfill(mongodbVersion) { return automationconfig.NOOP() } From b99d02645198a7d7bea4f456f33667034bc40835 Mon Sep 17 00:00:00 2001 From: Anand Singh Date: Thu, 11 Sep 2025 14:17:57 +0200 Subject: [PATCH 13/29] remove external.tls.enabled --- api/v1/search/mongodbsearch_types.go | 3 +-- config/crd/bases/mongodb.com_mongodbsearch.yaml | 4 ---- controllers/searchcontroller/external_search_source.go | 2 +- .../tests/search/search_community_external_mongod_basic.py | 1 - .../tests/search/search_community_external_mongod_tls.py | 1 - helm_chart/crds/mongodb.com_mongodbsearch.yaml | 4 ---- public/crds.yaml | 4 ---- 7 files changed, 2 insertions(+), 17 deletions(-) diff --git a/api/v1/search/mongodbsearch_types.go b/api/v1/search/mongodbsearch_types.go index ce90a8d13..b1d770944 100644 --- a/api/v1/search/mongodbsearch_types.go +++ b/api/v1/search/mongodbsearch_types.go @@ -69,8 +69,7 @@ type ExternalMongoDBSource struct { } type ExternalMongodTLS struct { - Enabled bool `json:"enabled"` - // +optional + // Optional CA *corev1.LocalObjectReference `json:"ca,omitempty"` } diff --git a/config/crd/bases/mongodb.com_mongodbsearch.yaml b/config/crd/bases/mongodb.com_mongodbsearch.yaml index 6cdf7d1dc..d2002f957 100644 --- a/config/crd/bases/mongodb.com_mongodbsearch.yaml +++ b/config/crd/bases/mongodb.com_mongodbsearch.yaml @@ -225,10 +225,6 @@ spec: type: string type: object x-kubernetes-map-type: atomic - enabled: - type: boolean - required: - - enabled type: object type: object mongodbResourceRef: diff --git a/controllers/searchcontroller/external_search_source.go b/controllers/searchcontroller/external_search_source.go index 8b5cc2d56..077277921 100644 --- a/controllers/searchcontroller/external_search_source.go +++ b/controllers/searchcontroller/external_search_source.go @@ -26,7 +26,7 @@ func (r *externalSearchResource) Validate() error { } func (r *externalSearchResource) TLSConfig() *TLSSourceConfig { - if r.spec.TLS == nil || !r.spec.TLS.Enabled { + if r.spec.TLS == nil { return nil } diff --git a/docker/mongodb-kubernetes-tests/tests/search/search_community_external_mongod_basic.py b/docker/mongodb-kubernetes-tests/tests/search/search_community_external_mongod_basic.py index 3a084b7a6..e9facccb2 100644 --- a/docker/mongodb-kubernetes-tests/tests/search/search_community_external_mongod_basic.py +++ b/docker/mongodb-kubernetes-tests/tests/search/search_community_external_mongod_basic.py @@ -71,7 +71,6 @@ def mdbs(namespace: str, mdbc: MongoDBCommunity) -> MongoDBSearch: "external": { "hostAndPorts": seeds, "keyfileSecretRef": {"name": f"{mdbc.name}-keyfile", "key": "keyfile"}, - "tls": {"enabled": False}, }, "passwordSecretRef": {"name": f"{MDBC_RESOURCE_NAME}-{MONGOT_USER_NAME}-password", "key": "password"}, "username": MONGOT_USER_NAME, diff --git a/docker/mongodb-kubernetes-tests/tests/search/search_community_external_mongod_tls.py b/docker/mongodb-kubernetes-tests/tests/search/search_community_external_mongod_tls.py index 8ccd5298c..7c80569e3 100644 --- a/docker/mongodb-kubernetes-tests/tests/search/search_community_external_mongod_tls.py +++ b/docker/mongodb-kubernetes-tests/tests/search/search_community_external_mongod_tls.py @@ -138,7 +138,6 @@ def test_create_search_resource(mdbs: MongoDBSearch, mdbc: MongoDBCommunity): "hostAndPorts": seeds, "keyfileSecretRef": {"name": f"{mdbc.name}-keyfile"}, "tls": { - "enabled": True, "ca": {"name": f"{mdbc.name}-ca"}, }, }, diff --git a/helm_chart/crds/mongodb.com_mongodbsearch.yaml b/helm_chart/crds/mongodb.com_mongodbsearch.yaml index 6cdf7d1dc..d2002f957 100644 --- a/helm_chart/crds/mongodb.com_mongodbsearch.yaml +++ b/helm_chart/crds/mongodb.com_mongodbsearch.yaml @@ -225,10 +225,6 @@ spec: type: string type: object x-kubernetes-map-type: atomic - enabled: - type: boolean - required: - - enabled type: object type: object mongodbResourceRef: diff --git a/public/crds.yaml b/public/crds.yaml index 9fa6a9207..4069c47ee 100644 --- a/public/crds.yaml +++ b/public/crds.yaml @@ -4247,10 +4247,6 @@ spec: type: string type: object x-kubernetes-map-type: atomic - enabled: - type: boolean - required: - - enabled type: object type: object mongodbResourceRef: From ee2df3bd7958b02c032e40209438aa1145bbc0db Mon Sep 17 00:00:00 2001 From: Anand Singh Date: Thu, 11 Sep 2025 14:20:21 +0200 Subject: [PATCH 14/29] update --- api/v1/search/mongodbsearch_types.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/v1/search/mongodbsearch_types.go b/api/v1/search/mongodbsearch_types.go index b1d770944..51b8e9e3c 100644 --- a/api/v1/search/mongodbsearch_types.go +++ b/api/v1/search/mongodbsearch_types.go @@ -74,7 +74,7 @@ type ExternalMongodTLS struct { } type Security struct { - // +optional + // Optional TLS TLS `json:"tls"` } From abb02ee892e7094bcac760eb744f7bcb79345428 Mon Sep 17 00:00:00 2001 From: Anand Singh Date: Thu, 11 Sep 2025 14:21:42 +0200 Subject: [PATCH 15/29] update --- api/v1/search/mongodbsearch_types.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/api/v1/search/mongodbsearch_types.go b/api/v1/search/mongodbsearch_types.go index 51b8e9e3c..687efad27 100644 --- a/api/v1/search/mongodbsearch_types.go +++ b/api/v1/search/mongodbsearch_types.go @@ -69,12 +69,12 @@ type ExternalMongoDBSource struct { } type ExternalMongodTLS struct { - // Optional + // +optional CA *corev1.LocalObjectReference `json:"ca,omitempty"` } type Security struct { - // Optional + // +optional TLS TLS `json:"tls"` } From 29b2f5646553ae3d8dc24b28d2f9ea9e9921d42c Mon Sep 17 00:00:00 2001 From: Anand Singh Date: Thu, 11 Sep 2025 14:51:25 +0200 Subject: [PATCH 16/29] remove enabled from search.spec.security.tls --- api/v1/search/mongodbsearch_types.go | 3 +-- config/crd/bases/mongodb.com_mongodbsearch.yaml | 4 ---- controllers/operator/mongodbsearch_controller.go | 2 +- .../searchcontroller/mongodbsearch_reconcile_helper.go | 4 ++-- .../tests/search/search_community_external_mongod_tls.py | 2 +- helm_chart/crds/mongodb.com_mongodbsearch.yaml | 4 ---- public/crds.yaml | 4 ---- 7 files changed, 5 insertions(+), 18 deletions(-) diff --git a/api/v1/search/mongodbsearch_types.go b/api/v1/search/mongodbsearch_types.go index 687efad27..8f69838e5 100644 --- a/api/v1/search/mongodbsearch_types.go +++ b/api/v1/search/mongodbsearch_types.go @@ -75,11 +75,10 @@ type ExternalMongodTLS struct { type Security struct { // +optional - TLS TLS `json:"tls"` + TLS *TLS `json:"tls,omitempty"` } type TLS struct { - Enabled bool `json:"enabled"` // CertificateKeySecret is a reference to a Secret containing a private key and certificate to use for TLS. // The key and cert are expected to be PEM encoded and available at "tls.key" and "tls.crt". // This is the same format used for the standard "kubernetes.io/tls" Secret type, but no specific type is required. diff --git a/config/crd/bases/mongodb.com_mongodbsearch.yaml b/config/crd/bases/mongodb.com_mongodbsearch.yaml index d2002f957..7aca591ad 100644 --- a/config/crd/bases/mongodb.com_mongodbsearch.yaml +++ b/config/crd/bases/mongodb.com_mongodbsearch.yaml @@ -179,10 +179,6 @@ spec: type: string type: object x-kubernetes-map-type: atomic - enabled: - type: boolean - required: - - enabled type: object type: object source: diff --git a/controllers/operator/mongodbsearch_controller.go b/controllers/operator/mongodbsearch_controller.go index b633fe9b4..16db88027 100644 --- a/controllers/operator/mongodbsearch_controller.go +++ b/controllers/operator/mongodbsearch_controller.go @@ -70,7 +70,7 @@ func (r *MongoDBSearchReconciler) Reconcile(ctx context.Context, request reconci } // Watch our own TLS certificate secret for changes - if mdbSearch.Spec.Security.TLS.Enabled { + if mdbSearch.Spec.Security.TLS != nil { r.watch.AddWatchedResourceIfNotAdded(mdbSearch.Spec.Security.TLS.CertificateKeySecret.Name, mdbSearch.Namespace, watch.Secret, mdbSearch.NamespacedName()) } diff --git a/controllers/searchcontroller/mongodbsearch_reconcile_helper.go b/controllers/searchcontroller/mongodbsearch_reconcile_helper.go index a8a4cc838..2f3ba59af 100644 --- a/controllers/searchcontroller/mongodbsearch_reconcile_helper.go +++ b/controllers/searchcontroller/mongodbsearch_reconcile_helper.go @@ -228,7 +228,7 @@ func (r *MongoDBSearchReconcileHelper) ensureMongotConfig(ctx context.Context, l } func (r *MongoDBSearchReconcileHelper) ensureIngressTlsConfig(ctx context.Context) (mongot.Modification, statefulset.Modification, error) { - if !r.mdbSearch.Spec.Security.TLS.Enabled { + if r.mdbSearch.Spec.Security.TLS == nil { mongotModification := func(config *mongot.Config) { config.Server.Wireproto.TLS.Mode = mongot.ConfigTLSModeDisabled } @@ -390,7 +390,7 @@ func createMongotConfig(search *searchv1.MongoDBSearch, db SearchSourceDBResourc func GetMongodConfigParameters(search *searchv1.MongoDBSearch) map[string]any { searchTLSMode := automationconfig.TLSModeDisabled - if search.Spec.Security.TLS.Enabled { + if search.Spec.Security.TLS != nil { searchTLSMode = automationconfig.TLSModeRequired } return map[string]any{ diff --git a/docker/mongodb-kubernetes-tests/tests/search/search_community_external_mongod_tls.py b/docker/mongodb-kubernetes-tests/tests/search/search_community_external_mongod_tls.py index 7c80569e3..a3d367636 100644 --- a/docker/mongodb-kubernetes-tests/tests/search/search_community_external_mongod_tls.py +++ b/docker/mongodb-kubernetes-tests/tests/search/search_community_external_mongod_tls.py @@ -145,7 +145,7 @@ def test_create_search_resource(mdbs: MongoDBSearch, mdbc: MongoDBCommunity): "username": MONGOT_USER_NAME, } - mdbs["spec"]["security"] = {"tls": {"enabled": True, "certificateKeySecretRef": {"name": MDBS_TLS_SECRET_NAME}}} + mdbs["spec"]["security"] = {"tls": {"certificateKeySecretRef": {"name": MDBS_TLS_SECRET_NAME}}} mdbs.update() mdbs.assert_reaches_phase(Phase.Running, timeout=300) diff --git a/helm_chart/crds/mongodb.com_mongodbsearch.yaml b/helm_chart/crds/mongodb.com_mongodbsearch.yaml index d2002f957..7aca591ad 100644 --- a/helm_chart/crds/mongodb.com_mongodbsearch.yaml +++ b/helm_chart/crds/mongodb.com_mongodbsearch.yaml @@ -179,10 +179,6 @@ spec: type: string type: object x-kubernetes-map-type: atomic - enabled: - type: boolean - required: - - enabled type: object type: object source: diff --git a/public/crds.yaml b/public/crds.yaml index 4069c47ee..9b31d5ac6 100644 --- a/public/crds.yaml +++ b/public/crds.yaml @@ -4201,10 +4201,6 @@ spec: type: string type: object x-kubernetes-map-type: atomic - enabled: - type: boolean - required: - - enabled type: object type: object source: From 3daaea7cc89931194268bd0fbaf5fb1a1b512d8a Mon Sep 17 00:00:00 2001 From: Anand Singh Date: Thu, 11 Sep 2025 14:55:23 +0200 Subject: [PATCH 17/29] remove enabled from search.spec.security.tls --- .../tests/search/search_enterprise_tls.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/mongodb-kubernetes-tests/tests/search/search_enterprise_tls.py b/docker/mongodb-kubernetes-tests/tests/search/search_enterprise_tls.py index 8050ccd5c..887a8e5d0 100644 --- a/docker/mongodb-kubernetes-tests/tests/search/search_enterprise_tls.py +++ b/docker/mongodb-kubernetes-tests/tests/search/search_enterprise_tls.py @@ -59,7 +59,7 @@ def mdbs(namespace: str) -> MongoDBSearch: if "spec" not in resource: resource["spec"] = {} - resource["spec"]["security"] = {"tls": {"enabled": True, "certificateKeySecretRef": {"name": MDBS_TLS_SECRET_NAME}}} + resource["spec"]["security"] = {"tls": {"certificateKeySecretRef": {"name": MDBS_TLS_SECRET_NAME}}} return resource From 6e15d39552ce56277958ff1f55f6f00efbfc94be Mon Sep 17 00:00:00 2001 From: Yavor Georgiev Date: Thu, 11 Sep 2025 16:16:17 +0300 Subject: [PATCH 18/29] fix community tests --- .../controllers/replica_set_controller.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mongodb-community-operator/controllers/replica_set_controller.go b/mongodb-community-operator/controllers/replica_set_controller.go index 0f02b8946..d38387a33 100644 --- a/mongodb-community-operator/controllers/replica_set_controller.go +++ b/mongodb-community-operator/controllers/replica_set_controller.go @@ -746,7 +746,7 @@ func (r ReplicaSetReconciler) buildAutomationConfig(ctx context.Context, mdb mdb // TODO: remove this as soon as searchCoordinator builtin role is backported func searchCoordinatorCustomRoleModification(search *searchv1.MongoDBSearch, mongodbVersion string) automationconfig.Modification { - if search == nil || searchcontroller.NeedsSearchCoordinatorRolePolyfill(mongodbVersion) { + if search == nil || !searchcontroller.NeedsSearchCoordinatorRolePolyfill(mongodbVersion) { return automationconfig.NOOP() } From e0fd04c55d09a634dd390dfc057adc463abeb329 Mon Sep 17 00:00:00 2001 From: Yavor Georgiev Date: Thu, 11 Sep 2025 16:29:15 +0300 Subject: [PATCH 19/29] fix NeedsSearchCoordinatorRolePolyfill for MongoDB older than 8 --- .../searchcontroller/mongodbsearch_reconcile_helper.go | 5 +++-- .../searchcontroller/mongodbsearch_reconcile_helper_test.go | 5 +++++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/controllers/searchcontroller/mongodbsearch_reconcile_helper.go b/controllers/searchcontroller/mongodbsearch_reconcile_helper.go index a8a4cc838..50c0e3987 100644 --- a/controllers/searchcontroller/mongodbsearch_reconcile_helper.go +++ b/controllers/searchcontroller/mongodbsearch_reconcile_helper.go @@ -525,6 +525,7 @@ func NeedsSearchCoordinatorRolePolyfill(mongodbVersion string) bool { return false } - // the searchCoordinator role is built-in from MongoDB 8.2 - return version.Major <= 8 && version.Minor < 2 + // 8.0.10+ and 8.1.x need the polyfill, anything older is not supported and execution will never reach here, + // and anything newer already has the role built-in + return version.Major == 8 && version.Minor < 2 } diff --git a/controllers/searchcontroller/mongodbsearch_reconcile_helper_test.go b/controllers/searchcontroller/mongodbsearch_reconcile_helper_test.go index 0de5e0488..dec84f2ad 100644 --- a/controllers/searchcontroller/mongodbsearch_reconcile_helper_test.go +++ b/controllers/searchcontroller/mongodbsearch_reconcile_helper_test.go @@ -102,6 +102,11 @@ func TestNeedsSearchCoordinatorRolePolyfill(t *testing.T) { version string expected bool }{ + { + name: "MongoDB 7.x and below do not require polyfill (unsupported by Search)", + version: "7.3.0", + expected: false, + }, { name: "MongoDB 8.0.x requires polyfill", version: "8.0.10", From 6d7046cffe05c0008dce173f110c5c11bdb126a1 Mon Sep 17 00:00:00 2001 From: Anand Singh Date: Thu, 11 Sep 2025 17:26:36 +0200 Subject: [PATCH 20/29] make certificate authority fields mandatory --- api/v1/search/mongodbsearch_types.go | 4 +-- .../crd/bases/mongodb.com_mongodbsearch.yaml | 33 ++++--------------- 2 files changed, 7 insertions(+), 30 deletions(-) diff --git a/api/v1/search/mongodbsearch_types.go b/api/v1/search/mongodbsearch_types.go index 8f69838e5..7c45fbc0d 100644 --- a/api/v1/search/mongodbsearch_types.go +++ b/api/v1/search/mongodbsearch_types.go @@ -69,8 +69,7 @@ type ExternalMongoDBSource struct { } type ExternalMongodTLS struct { - // +optional - CA *corev1.LocalObjectReference `json:"ca,omitempty"` + CA *corev1.LocalObjectReference `json:"ca"` } type Security struct { @@ -82,7 +81,6 @@ type TLS struct { // CertificateKeySecret is a reference to a Secret containing a private key and certificate to use for TLS. // The key and cert are expected to be PEM encoded and available at "tls.key" and "tls.crt". // This is the same format used for the standard "kubernetes.io/tls" Secret type, but no specific type is required. - // +optional CertificateKeySecret corev1.LocalObjectReference `json:"certificateKeySecretRef"` } diff --git a/config/crd/bases/mongodb.com_mongodbsearch.yaml b/config/crd/bases/mongodb.com_mongodbsearch.yaml index 7aca591ad..e800bd096 100644 --- a/config/crd/bases/mongodb.com_mongodbsearch.yaml +++ b/config/crd/bases/mongodb.com_mongodbsearch.yaml @@ -163,22 +163,13 @@ spec: tls: properties: certificateKeySecretRef: + $ref: '#/definitions/k8s.io~1api~1core~1v1~0LocalObjectReference' description: |- CertificateKeySecret is a reference to a Secret containing a private key and certificate to use for TLS. The key and cert are expected to be PEM encoded and available at "tls.key" and "tls.crt". This is the same format used for the standard "kubernetes.io/tls" Secret type, but no specific type is required. - 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 + required: + - certificateKeySecretRef type: object type: object source: @@ -206,21 +197,9 @@ spec: description: TLS configuration for the external MongoDB deployment properties: ca: - description: |- - LocalObjectReference contains enough information to let you locate the - referenced object inside the same namespace. - properties: - name: - default: "" - description: |- - Name of the referent. - This field is effectively required, but due to backwards compatibility is - allowed to be empty. Instances of this type with an empty value here are - almost certainly wrong. - More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names - type: string - type: object - x-kubernetes-map-type: atomic + $ref: '#/definitions/k8s.io~1api~1core~1v1~0LocalObjectReference' + required: + - ca type: object type: object mongodbResourceRef: From 72f871d3385eeb8a4af5d60f37cdb9cc1fefa147 Mon Sep 17 00:00:00 2001 From: Yavor Georgiev Date: Fri, 12 Sep 2025 01:59:36 +0300 Subject: [PATCH 21/29] remove duplicated helm args --- scripts/funcs/operator_deployment | 3 --- 1 file changed, 3 deletions(-) diff --git a/scripts/funcs/operator_deployment b/scripts/funcs/operator_deployment index 77075a39a..1571ef925 100644 --- a/scripts/funcs/operator_deployment +++ b/scripts/funcs/operator_deployment @@ -37,9 +37,6 @@ get_operator_helm_values() { "search.community.name=${MDB_SEARCH_COMMUNITY_NAME}" "search.community.version=${MDB_SEARCH_COMMUNITY_VERSION}" "community.registry.agent=${AGENT_BASE_REGISTRY:-${REGISTRY}}" - "search.community.repo=${MDB_SEARCH_COMMUNITY_REPO_URL}" - "search.community.name=${MDB_SEARCH_COMMUNITY_NAME}" - "search.community.version=${MDB_SEARCH_COMMUNITY_VERSION}" ) if [[ "${MDB_OPERATOR_TELEMETRY_INSTALL_CLUSTER_ROLE_INSTALLATION:-}" != "" ]]; then From bd491a8707170888d36bc64ab7160493e0702d70 Mon Sep 17 00:00:00 2001 From: Yavor Georgiev Date: Fri, 12 Sep 2025 11:03:33 +0300 Subject: [PATCH 22/29] rename COMMUNITY_PRIVATE_PREVIEW_PULLSECRET_DOCKERCONFIGJSON --- .../quick-start/env_variables_e2e_prerelease.sh | 2 +- .../community-search/quick-start/env_variables_e2e_private.sh | 2 +- docs/community-search/quick-start/env_variables_e2e_public.sh | 2 +- scripts/dev/configure_container_auth.sh | 4 ++-- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/community-search/quick-start/env_variables_e2e_prerelease.sh b/docs/community-search/quick-start/env_variables_e2e_prerelease.sh index b708d7786..bac17a48b 100644 --- a/docs/community-search/quick-start/env_variables_e2e_prerelease.sh +++ b/docs/community-search/quick-start/env_variables_e2e_prerelease.sh @@ -1,6 +1,6 @@ export K8S_CLUSTER_0_CONTEXT_NAME="${CLUSTER_NAME}" -export PRIVATE_PREVIEW_IMAGE_PULLSECRET="${COMMUNITY_PRIVATE_PREVIEW_PULLSECRET_DOCKERCONFIGJSON}" +export PRIVATE_PREVIEW_IMAGE_PULLSECRET="${PRERELEASE_PULLSECRET_DOCKERCONFIGJSON}" export OPERATOR_ADDITIONAL_HELM_VALUES="" export OPERATOR_HELM_CHART="${PROJECT_DIR}/helm_chart" diff --git a/docs/community-search/quick-start/env_variables_e2e_private.sh b/docs/community-search/quick-start/env_variables_e2e_private.sh index 7996f92a9..be09ad0ad 100644 --- a/docs/community-search/quick-start/env_variables_e2e_private.sh +++ b/docs/community-search/quick-start/env_variables_e2e_private.sh @@ -1,6 +1,6 @@ export K8S_CLUSTER_0_CONTEXT_NAME="${CLUSTER_NAME}" -export PRIVATE_PREVIEW_IMAGE_PULLSECRET="${COMMUNITY_PRIVATE_PREVIEW_PULLSECRET_DOCKERCONFIGJSON}" +export PRIVATE_PREVIEW_IMAGE_PULLSECRET="${PRERELEASE_PULLSECRET_DOCKERCONFIGJSON}" source scripts/funcs/operator_deployment OPERATOR_ADDITIONAL_HELM_VALUES="$(get_operator_helm_values | tr ' ' ',')" diff --git a/docs/community-search/quick-start/env_variables_e2e_public.sh b/docs/community-search/quick-start/env_variables_e2e_public.sh index 8a835d9b7..a699139c3 100644 --- a/docs/community-search/quick-start/env_variables_e2e_public.sh +++ b/docs/community-search/quick-start/env_variables_e2e_public.sh @@ -1,2 +1,2 @@ export K8S_CLUSTER_0_CONTEXT_NAME="${CLUSTER_NAME}" -export PRIVATE_PREVIEW_IMAGE_PULLSECRET="${COMMUNITY_PRIVATE_PREVIEW_PULLSECRET_DOCKERCONFIGJSON}" +export PRIVATE_PREVIEW_IMAGE_PULLSECRET="${PRERELEASE_PULLSECRET_DOCKERCONFIGJSON}" diff --git a/scripts/dev/configure_container_auth.sh b/scripts/dev/configure_container_auth.sh index 1afd8429d..c221603a2 100755 --- a/scripts/dev/configure_container_auth.sh +++ b/scripts/dev/configure_container_auth.sh @@ -158,12 +158,12 @@ fi aws ecr get-login-password --region "eu-west-1" | registry_login "AWS" "268558157000.dkr.ecr.eu-west-1.amazonaws.com" -if [[ -n "${COMMUNITY_PRIVATE_PREVIEW_PULLSECRET_DOCKERCONFIGJSON:-}" ]]; then +if [[ -n "${PRERELEASE_PULLSECRET_DOCKERCONFIGJSON:-}" ]]; then # log in to quay.io for the mongodb/mongodb-search-community private repo # TODO remove once we switch to the official repo in Public Preview quay_io_auth_file=$(mktemp) config_tmp=$(mktemp) - echo "${COMMUNITY_PRIVATE_PREVIEW_PULLSECRET_DOCKERCONFIGJSON}" | base64 -d > "${quay_io_auth_file}" + echo "${PRERELEASE_PULLSECRET_DOCKERCONFIGJSON}" | base64 -d > "${quay_io_auth_file}" exec_cmd jq -s '.[0] * .[1]' "${quay_io_auth_file}" "${CONFIG_PATH}" > "${config_tmp}" exec_cmd mv "${config_tmp}" "${CONFIG_PATH}" rm "${quay_io_auth_file}" From 1ffaab5b0de2f7bb809c2ee0c5437e9e55cf1f70 Mon Sep 17 00:00:00 2001 From: Anand Singh Date: Fri, 12 Sep 2025 11:06:54 +0200 Subject: [PATCH 23/29] run make manifests --- .../crd/bases/mongodb.com_mongodbsearch.yaml | 29 +++++++++++++++++-- .../crds/mongodb.com_mongodbsearch.yaml | 4 +++ public/crds.yaml | 4 +++ 3 files changed, 35 insertions(+), 2 deletions(-) diff --git a/config/crd/bases/mongodb.com_mongodbsearch.yaml b/config/crd/bases/mongodb.com_mongodbsearch.yaml index e800bd096..4d9a6fbe1 100644 --- a/config/crd/bases/mongodb.com_mongodbsearch.yaml +++ b/config/crd/bases/mongodb.com_mongodbsearch.yaml @@ -163,11 +163,22 @@ spec: tls: properties: certificateKeySecretRef: - $ref: '#/definitions/k8s.io~1api~1core~1v1~0LocalObjectReference' description: |- CertificateKeySecret is a reference to a Secret containing a private key and certificate to use for TLS. The key and cert are expected to be PEM encoded and available at "tls.key" and "tls.crt". This is the same format used for the standard "kubernetes.io/tls" Secret type, but no specific type is required. + 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 required: - certificateKeySecretRef type: object @@ -197,7 +208,21 @@ spec: description: TLS configuration for the external MongoDB deployment properties: ca: - $ref: '#/definitions/k8s.io~1api~1core~1v1~0LocalObjectReference' + description: |- + LocalObjectReference contains enough information to let you locate the + referenced object inside the same namespace. + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + type: object + x-kubernetes-map-type: atomic required: - ca type: object diff --git a/helm_chart/crds/mongodb.com_mongodbsearch.yaml b/helm_chart/crds/mongodb.com_mongodbsearch.yaml index 7aca591ad..4d9a6fbe1 100644 --- a/helm_chart/crds/mongodb.com_mongodbsearch.yaml +++ b/helm_chart/crds/mongodb.com_mongodbsearch.yaml @@ -179,6 +179,8 @@ spec: type: string type: object x-kubernetes-map-type: atomic + required: + - certificateKeySecretRef type: object type: object source: @@ -221,6 +223,8 @@ spec: type: string type: object x-kubernetes-map-type: atomic + required: + - ca type: object type: object mongodbResourceRef: diff --git a/public/crds.yaml b/public/crds.yaml index 9b31d5ac6..943b3ba1a 100644 --- a/public/crds.yaml +++ b/public/crds.yaml @@ -4201,6 +4201,8 @@ spec: type: string type: object x-kubernetes-map-type: atomic + required: + - certificateKeySecretRef type: object type: object source: @@ -4243,6 +4245,8 @@ spec: type: string type: object x-kubernetes-map-type: atomic + required: + - ca type: object type: object mongodbResourceRef: From d263c3f90e1cd2d36724c0c2331ce52e62ad446e Mon Sep 17 00:00:00 2001 From: Anand Singh Date: Fri, 12 Sep 2025 11:31:09 +0200 Subject: [PATCH 24/29] add comment --- api/v1/search/mongodbsearch_types.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/api/v1/search/mongodbsearch_types.go b/api/v1/search/mongodbsearch_types.go index 7c45fbc0d..47ddd5186 100644 --- a/api/v1/search/mongodbsearch_types.go +++ b/api/v1/search/mongodbsearch_types.go @@ -69,6 +69,8 @@ type ExternalMongoDBSource struct { } type ExternalMongodTLS struct { + // CA is a reference to a Secret containing the CA certificate that issued mongod's TLS certificate. + // The CA certificate is expected to be PEM encoded and available at the "ca.crt" key. CA *corev1.LocalObjectReference `json:"ca"` } From 554fcfbfd9c55c96118edb4caa3426523346a0d9 Mon Sep 17 00:00:00 2001 From: Anand Singh Date: Fri, 12 Sep 2025 17:07:38 +0200 Subject: [PATCH 25/29] remove enabled field from snippets --- docs/search/04-search-external-mongod/README.md | 2 -- .../code_snippets/04_0320_create_mongodb_search_resource.sh | 2 -- 2 files changed, 4 deletions(-) diff --git a/docs/search/04-search-external-mongod/README.md b/docs/search/04-search-external-mongod/README.md index 9d1f059c6..c84b20ae2 100644 --- a/docs/search/04-search-external-mongod/README.md +++ b/docs/search/04-search-external-mongod/README.md @@ -182,8 +182,6 @@ spec: keyfileSecretRef: name: ${MDB_EXTERNAL_KEYFILE_SECRET_NAME} key: keyfile - tls: - enabled: false username: search-sync-source passwordSecretRef: name: mdbc-rs-search-sync-source-password diff --git a/docs/search/04-search-external-mongod/code_snippets/04_0320_create_mongodb_search_resource.sh b/docs/search/04-search-external-mongod/code_snippets/04_0320_create_mongodb_search_resource.sh index 1670172a8..85ce3f4d5 100644 --- a/docs/search/04-search-external-mongod/code_snippets/04_0320_create_mongodb_search_resource.sh +++ b/docs/search/04-search-external-mongod/code_snippets/04_0320_create_mongodb_search_resource.sh @@ -13,8 +13,6 @@ spec: keyfileSecretRef: name: ${MDB_EXTERNAL_KEYFILE_SECRET_NAME} key: keyfile - tls: - enabled: false username: search-sync-source passwordSecretRef: name: mdbc-rs-search-sync-source-password From f234fb48132201636c78f2439cd7c347d0ae1925 Mon Sep 17 00:00:00 2001 From: Anand Singh Date: Fri, 12 Sep 2025 17:34:39 +0200 Subject: [PATCH 26/29] run external snippets in evergreen --- .evergreen-snippets.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.evergreen-snippets.yml b/.evergreen-snippets.yml index c5a209920..c6ed37af3 100644 --- a/.evergreen-snippets.yml +++ b/.evergreen-snippets.yml @@ -121,6 +121,12 @@ tasks: - func: test_code_snippets - func: sample_commit_output + - name: test_kind_search_external_mongod_snippets.sh + tags: [ "code_snippets", "patch-run" ] + commands: + - func: test_code_snippets + - func: sample_commit_output + task_groups: - name: gke_code_snippets_task_group <<: *setup_and_teardown_group_gke_code_snippets @@ -135,6 +141,7 @@ task_groups: tasks: - test_kind_search_community_snippets.sh - test_kind_search_enterprise_snippets.sh + - test_kind_search_external_mongod_snippets.sh buildvariants: # These variants are used to test the code snippets and each one can be used in patches From 14b007012789b239f36ac8a5097008b077a9fbde Mon Sep 17 00:00:00 2001 From: Anand Singh Date: Fri, 12 Sep 2025 18:00:02 +0200 Subject: [PATCH 27/29] make file executable --- .../tests/test_kind_search_external_mongod_snippets.sh | 0 1 file changed, 0 insertions(+), 0 deletions(-) mode change 100644 => 100755 scripts/code_snippets/tests/test_kind_search_external_mongod_snippets.sh diff --git a/scripts/code_snippets/tests/test_kind_search_external_mongod_snippets.sh b/scripts/code_snippets/tests/test_kind_search_external_mongod_snippets.sh old mode 100644 new mode 100755 From 3b14c342ae81c05d4ef74a18ffeffa7a433dac90 Mon Sep 17 00:00:00 2001 From: Anand Singh Date: Fri, 12 Sep 2025 18:57:18 +0200 Subject: [PATCH 28/29] make file executable --- docs/search/04-search-external-mongod/test.sh | 0 1 file changed, 0 insertions(+), 0 deletions(-) mode change 100644 => 100755 docs/search/04-search-external-mongod/test.sh diff --git a/docs/search/04-search-external-mongod/test.sh b/docs/search/04-search-external-mongod/test.sh old mode 100644 new mode 100755 From f509ba04f255cd213de1d03a181e31680df0e831 Mon Sep 17 00:00:00 2001 From: Anand Singh Date: Fri, 12 Sep 2025 20:00:28 +0200 Subject: [PATCH 29/29] remove-extraneous-change --- RELEASE_NOTES.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 71cf858fa..fab81e28d 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -1,7 +1,6 @@ > [!IMPORTANT] > This file is no longer maintained and updated. -> All future Release Notes for MCK are available only -> in [GitHub Releases Page](https://github.com/mongodb/mongodb-kubernetes/releases). +> All future Release Notes for MCK are available only in [GitHub Releases Page](https://github.com/mongodb/mongodb-kubernetes/releases). # MCK 1.2.0 Release Notes