diff --git a/.evergreen-tasks.yml b/.evergreen-tasks.yml index b5366b2a0..8a0b73fec 100644 --- a/.evergreen-tasks.yml +++ b/.evergreen-tasks.yml @@ -1285,3 +1285,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 b1ab8a9bf..48d96bee2 100644 --- a/.evergreen.yml +++ b/.evergreen.yml @@ -688,6 +688,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 @@ -812,6 +815,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..791c5b1dd 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,65 @@ 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"` + KeyFileSecretKeyRef *userv1.SecretKeyRef `json:"keyFileSecretRef,omitempty"` // This is the mongod credential used to connect to the external MongoDB deployment + // +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 +153,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 +185,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 +205,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_mongodbsearch.yaml b/config/crd/bases/mongodb.com_mongodbsearch.yaml index 72ea0e50e..38dc38f2a 100644 --- a/config/crd/bases/mongodb.com_mongodbsearch.yaml +++ b/config/crd/bases/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/config/manager/manager.yaml b/config/manager/manager.yaml index 409cf1afd..7e6fc2633 100644 --- a/config/manager/manager.yaml +++ b/config/manager/manager.yaml @@ -296,10 +296,10 @@ spec: - name: RELATED_IMAGE_MONGODB_IMAGE_8_0_0_ubi9 value: "quay.io/mongodb/mongodb-enterprise-server:8.0.0-ubi9" - name: RELATED_IMAGE_MDB_SEARCH_IMAGE_1_47_0 - value: "quay.io/mongodb/mongodb-search-community:1.47.0" + value: "268558157000.dkr.ecr.eu-west-1.amazonaws.com/mongot/community:1.47.0" - name: MDB_SEARCH_COMMUNITY_REPO_URL - value: "quay.io/mongodb" + value: "268558157000.dkr.ecr.eu-west-1.amazonaws.com" - name: MDB_SEARCH_COMMUNITY_NAME - value: "mongodb-search-community" + value: "mongot/community" - name: MDB_SEARCH_COMMUNITY_VERSION value: "1.47.0" 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..3ab49f5f2 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,7 +19,9 @@ 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 @@ -29,63 +31,20 @@ const ( // // 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 +54,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 +83,14 @@ func CreateSearchStatefulSetFunc(mdbSearch *searchv1.MongoDBSearch, sourceDBReso keyfileVolumeMount, tmpVolumeMount, mongotConfigVolumeMount, + sourceUserPasswordVolumeMount, } volumes := []corev1.Volume{ tmpVolume, keyfileVolume, mongotConfigVolume, + sourceUserPasswordVolume, } stsModifications := []statefulset.Modification{ @@ -142,11 +107,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 +131,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/docs/community-search/quick-start/README.md b/docs/community-search/quick-start/README.md index 3934db1ab..7f6b6af99 100644 --- a/docs/community-search/quick-start/README.md +++ b/docs/community-search/quick-start/README.md @@ -4,11 +4,9 @@ This guide provides instructions for deploying MongoDB Community Edition along w ## Prerequisites -Community Search is currently in private preview, and access to the image requires a secret to pull the search container image from Quay.io. This secret is specified during the first step of the process below, and must be obtained from MongoDB when requesting access to the private preview. - Before you begin, ensure you have the following tools and configurations in place: -- **Kubernetes cluster**: A running Kubernetes cluster (e.g., Minikube, Kind, GKE, EKS, AKS). +- **Kubernetes cluster**: A running Kubernetes cluster (e.g., Minikube, Kind, GKE, EKS, AKS) with kubeconfig available locally. - **kubectl**: The Kubernetes command-line tool, configured to communicate with your cluster. - **Helm**: The package manager for Kubernetes, used here to install the MongoDB Kubernetes Operator. - **Bash 5.1+**: All shell commands in this guide are intended to be run in Bash. Scripts in this guide are automatically tested on Linux with Bash 5.1. @@ -29,15 +27,18 @@ Download or copy the content of `env_variables.sh`: # set it to the context name of the k8s cluster export K8S_CLUSTER_0_CONTEXT_NAME="" -# At the private preview stage the community search image is accessible only from a private repository. -# Please contact MongoDB Support to get access. -export PRIVATE_PREVIEW_IMAGE_PULLSECRET="<.dockerconfigjson>" - # the following namespace will be created if not exists export MDB_NAMESPACE="mongodb" +# minimum required MongoDB version for running MongoDB Search is 8.0.10 +export MDB_VERSION="8.0.10" + +# root admin user for convenience, not used here at all in this guide export MDB_ADMIN_USER_PASSWORD="admin-user-password-CHANGE-ME" -export MDB_SEARCH_USER_PASSWORD="search-user-password-CHANGE-ME" +# regular user performing restore and search queries on sample mflix database +export MDB_USER_PASSWORD="mdb-user-password-CHANGE-ME" +# user for MongoDB Search to connect to the replica set to synchronise data from +export MDB_SEARCH_SYNC_USER_PASSWORD="search-sync-user-password-CHANGE-ME" export OPERATOR_HELM_CHART="mongodb/mongodb-kubernetes" # comma-separated key=value pairs for additional parameters passed to the helm-chart installing the operator @@ -69,83 +70,39 @@ helm upgrade --install --debug --kube-context "${K8S_CLUSTER_0_CONTEXT_NAME}" \ --set "${OPERATOR_ADDITIONAL_HELM_VALUES:-"dummy=value"}" \ "${OPERATOR_HELM_CHART}" ``` -This command installs the operator in the `mongodb` namespace (creating it if it doesn't exist) and names the release `community-operator`. - -### 4. Configure Pull Secret for MongoDB Community Search - -To use MongoDB Search, your Kubernetes cluster needs to pull the necessary container images. This step creates a Kubernetes secret named `community-private-preview-pullsecret`. This secret stores the credentials required to access the image repository for MongoDB Search. The script then patches the `mongodb-kubernetes-database-pods` service account to include this pull secret, allowing pods managed by this service account to pull the required images. - -[code_snippets/0200_configure_community_search_pullsecret.sh](code_snippets/0200_configure_community_search_pullsecret.sh) -```shell copy -kubectl apply --context "${K8S_CLUSTER_0_CONTEXT_NAME}" -n "${MDB_NAMESPACE}" -f - < /tmp/mdb_script.js -mongosh --quiet "mongodb://search-user:${MDB_SEARCH_USER_PASSWORD}@mdbc-rs-0.mdbc-rs-svc.${MDB_NAMESPACE}.svc.cluster.local:27017/?replicaSet=mdbc-rs" < /tmp/mdb_script.js +mongosh --quiet "mongodb://mdb-user:${MDB_USER_PASSWORD}@mdbc-rs-0.mdbc-rs-svc.${MDB_NAMESPACE}.svc.cluster.local:27017/?replicaSet=mdbc-rs" < /tmp/mdb_script.js EOF )" -``` \ No newline at end of file +``` diff --git a/docs/community-search/quick-start/README.md.j2 b/docs/community-search/quick-start/README.md.j2 index 0e3125075..de32c88b1 100644 --- a/docs/community-search/quick-start/README.md.j2 +++ b/docs/community-search/quick-start/README.md.j2 @@ -4,11 +4,9 @@ This guide provides instructions for deploying MongoDB Community Edition along w ## Prerequisites -Community Search is currently in private preview, and access to the image requires a secret to pull the search container image from Quay.io. This secret is specified during the first step of the process below, and must be obtained from MongoDB when requesting access to the private preview. - Before you begin, ensure you have the following tools and configurations in place: -- **Kubernetes cluster**: A running Kubernetes cluster (e.g., Minikube, Kind, GKE, EKS, AKS). +- **Kubernetes cluster**: A running Kubernetes cluster (e.g., Minikube, Kind, GKE, EKS, AKS) with kubeconfig available locally. - **kubectl**: The Kubernetes command-line tool, configured to communicate with your cluster. - **Helm**: The package manager for Kubernetes, used here to install the MongoDB Kubernetes Operator. - **Bash 5.1+**: All shell commands in this guide are intended to be run in Bash. Scripts in this guide are automatically tested on Linux with Bash 5.1. @@ -47,33 +45,13 @@ Next, install the MongoDB Kubernetes Operator from the Helm repository you just ```shell copy {% include "code_snippets/0100_install_operator.sh" %} ``` -This command installs the operator in the `mongodb` namespace (creating it if it doesn't exist) and names the release `community-operator`. - -### 4. Configure Pull Secret for MongoDB Community Search - -To use MongoDB Search, your Kubernetes cluster needs to pull the necessary container images. This step creates a Kubernetes secret named `community-private-preview-pullsecret`. This secret stores the credentials required to access the image repository for MongoDB Search. The script then patches the `mongodb-kubernetes-database-pods` service account to include this pull secret, allowing pods managed by this service account to pull the required images. - -[code_snippets/0200_configure_community_search_pullsecret.sh](code_snippets/0200_configure_community_search_pullsecret.sh) -```shell copy -{% include "code_snippets/0200_configure_community_search_pullsecret.sh" %} -``` -This script creates a `community-private-preview-pullsecret` secret in your Kubernetes namespace and associates it with the service account used for MongoDB pods. - -### 5. Verify Pull Secret Configuration - -Confirm that the `community-private-preview-pullsecret` has been successfully added to the `mongodb-kubernetes-database-pods` service account. This ensures that Kubernetes can authenticate with the container registry when pulling images for MongoDB Search pods. - -[code_snippets/0210_verify_community_search_pullsecret.sh](code_snippets/0210_verify_community_search_pullsecret.sh) -```shell copy -{% include "code_snippets/0210_verify_community_search_pullsecret.sh" %} -``` -This command checks the `mongodb-kubernetes-database-pods` service account to confirm the presence of `community-private-preview-pullsecret`. +This command installs the operator in the `mongodb` namespace (creating it if it doesn't exist). ## Creating a MongoDB Community Search Deployment With the prerequisites and initial setup complete, you can now deploy MongoDB Community Edition and enable Search. -### 6. Create MongoDB User Secrets +### 4. Create MongoDB User Secrets MongoDB requires authentication for secure access. This step creates two Kubernetes secrets: `admin-user-password` and `search-user-password`. These secrets store the credentials for the MongoDB administrative user and a dedicated search user, respectively. These secrets will be mounted into the MongoDB pods. @@ -83,16 +61,19 @@ MongoDB requires authentication for secure access. This step creates two Kuberne ``` Ensure these secrets are created in the same namespace where you plan to deploy MongoDB. -### 7. Create MongoDB Community Resource +### 5. Create MongoDB Community Resource -Now, deploy MongoDB Community by creating a `MongoDBCommunity` custom resource named `mdbc-rs`. This resource definition instructs the MongoDB Kubernetes Operator to configure a MongoDB replica set with 3 members, running version 8.0.6. MongoDB Community Search is supported only from MongoDB Community Server version 8.0. It also defines CPU and memory resources for the `mongod` and `mongodb-agent` containers, and sets up two users (`admin-user` and `search-user`) with their respective roles and password secrets. User `search-user` will be used to restore, connect and perform search queries on the `sample_mflix` database. +Now, deploy MongoDB Community by creating a `MongoDBCommunity` custom resource named `mdbc-rs`. This resource definition instructs the MongoDB Kubernetes Operator to configure a MongoDB replica set with 3 members, running version 8.0.10. MongoDB Community Search is supported only from MongoDB Community Server version 8.0.10. It also defines CPU and memory resources for the `mongod` and `mongodb-agent` containers, and sets up three users: +* `mdb-user` - a regular user used to that will perform restore of `sample_mflix` database and execute search queries. +* `search-sync-source` - user that MongoDB Search is using to connect to MongoDB database in order to manage and build indexes. This user uses `searchCoordinator` role, which for MongoDB <8.2 is created automatically by the operator. +* `admin-user` and ``) with their respective roles and password secrets. User `search-user` will be used to restore, connect and perform search queries on the `sample_mflix` database. [code_snippets/0310_create_mongodb_community_resource.sh](code_snippets/0310_create_mongodb_community_resource.sh) ```yaml copy {% include "code_snippets/0310_create_mongodb_community_resource.sh" %} ``` -### 8. Wait for MongoDB Community Resource to be Ready +### 6. Wait for MongoDB Community Resource to be Ready After applying the `MongoDBCommunity` custom resource, the operator begins deploying the MongoDB nodes (pods). This step uses `kubectl wait` to pause execution until the `mdbc-rs` resource's status phase becomes `Running`, indicating that the MongoDB Community replica set is operational. @@ -101,13 +82,12 @@ After applying the `MongoDBCommunity` custom resource, the operator begins deplo {% include "code_snippets/0315_wait_for_community_resource.sh" %} ``` -### 9. Create MongoDB Search Resource +### 7. Create MongoDB Search Resource Once your MongoDB deployment is ready, enable Search capabilities by creating a `MongoDBSearch` custom resource, also named `mdbc-rs` to associate it with the MongoDB instance. This resource specifies the CPU and memory resource requirements for the search nodes. -Note: Private preview of MongoDB Community Search comes with some limitations, and it is not suitable for production use: -* TLS cannot be enabled in MongoDB Community deployment (MongoD communicates with MongoT with plain text). -* Only one node of search node is supported (load balancing not supported) +Note: Public Preview of MongoDB Community Search comes with some limitations, and it is not suitable for production use: +* Only one instance of the search node is supported (load balancing is not supported) [code_snippets/0320_create_mongodb_search_resource.sh](code_snippets/0320_create_mongodb_search_resource.sh) ```shell copy @@ -126,7 +106,7 @@ requests: memory: 2G ``` -### 10. Wait for Search Resource to be Ready +### 8. Wait for Search Resource to be Ready Similar to the MongoDB deployment, the Search deployment needs time to initialize. This step uses `kubectl wait` to pause until the `MongoDBSearch` resource `mdbc-rs` reports a `Running` status in its `.status.phase` field, indicating that the search nodes are operational and integrated. @@ -136,7 +116,7 @@ Similar to the MongoDB deployment, the Search deployment needs time to initializ ``` This command polls the status of the `MongoDBSearch` resource `mdbc-rs`. -### 11. Verify MongoDB Community Resource Status +### 9. Verify MongoDB Community Resource Status Double-check the status of your `MongoDBCommunity` resource to ensure it remains healthy and that the integration with the Search resource is reflected if applicable. @@ -146,7 +126,7 @@ Double-check the status of your `MongoDBCommunity` resource to ensure it remains ``` This provides a final confirmation that the core database is operational. -### 12. List Running Pods +### 10. List Running Pods View all the running pods in your namespace. You should see pods for the MongoDB replica set members, the MongoDB Kubernetes Operator, and the MongoDB Search nodes. @@ -159,7 +139,7 @@ View all the running pods in your namespace. You should see pods for the MongoDB Now that your MongoDB Community database with Search is deployed, you can start using its search capabilities. -### 13. Deploy MongoDB Tools Pod +### 11. Deploy MongoDB Tools Pod To interact with your MongoDB deployment, this step deploys a utility pod named `mongodb-tools-pod`. This pod runs a MongoDB Community Server image and is kept running with a `sleep infinity` command, allowing you to use `kubectl exec` to run MongoDB client tools like `mongosh` and `mongorestore` from within the Kubernetes cluster. Running steps in a pod inside the cluster simplifies connectivity to mongodb without neeeding to expose the database externally (provided steps directly connect to the *.cluster.local hostnames). @@ -168,7 +148,7 @@ To interact with your MongoDB deployment, this step deploys a utility pod named {% include "code_snippets/0410_run_mongodb_tools_pod.sh" %} ``` -### 14. Import Sample Data +### 12. Import Sample Data To test the search functionality, this step imports the `sample_mflix.movies` collection. It downloads the sample dataset and uses `mongorestore` to load the data into the `sample_mflix` database in your MongoDB deployment, connecting as the `search-user`. @@ -178,7 +158,7 @@ To test the search functionality, this step imports the `sample_mflix.movies` co ``` This command uses `mongorestore` from the `mongodb-tools-pod` to load data from the downloaded `sample_mflix.archive` file. -### 15. Create Search Index +### 13. Create Search Index Before performing search queries, create a search index. This step uses `kubectl exec` to run `mongosh` in the `mongodb-tools-pod`. It connects to the `sample_mflix` database as `search-user` and calls `db.movies.createSearchIndex()` to create a search index named "default" with dynamic mappings on the `movies` collection. Dynamic mapping automatically indexes all fields with supported types. MongoDB Search offers flexible index definitions, allowing for dynamic and static field mappings, various analyzer types (standard, language-specific, custom), and features like synonyms and faceted search. @@ -187,7 +167,7 @@ Before performing search queries, create a search index. This step uses `kubectl {% include "code_snippets/0430_create_search_index.sh" %} ``` -### 16. Wait for Search Index to be Ready +### 14. Wait for Search Index to be Ready Creating a search index is an asynchronous operation. This script polls periodically the status by executing `db.movies.getSearchIndexes("default")`. @@ -196,7 +176,7 @@ Creating a search index is an asynchronous operation. This script polls periodic {% include "code_snippets/0440_wait_for_search_index_ready.sh" %} ``` -### 17. Execute a Search Query +### 15. Execute a Search Query Once the search index is ready, execute search queries using the `$search` aggregation pipeline stage. MongoDB Search supports a query language, allowing for various types of queries such as text search, autocomplete, faceting, and more. You can combine `$search` with other aggregation stages to further refine and process your results. diff --git a/docs/community-search/quick-start/code_snippets/0100_install_operator.sh b/docs/community-search/quick-start/code_snippets/0100_install_operator.sh old mode 100644 new mode 100755 diff --git a/docs/community-search/quick-start/code_snippets/0200_configure_community_search_pullsecret.sh b/docs/community-search/quick-start/code_snippets/0200_configure_community_search_pullsecret.sh deleted file mode 100644 index 59310c81f..000000000 --- a/docs/community-search/quick-start/code_snippets/0200_configure_community_search_pullsecret.sh +++ /dev/null @@ -1,25 +0,0 @@ -kubectl apply --context "${K8S_CLUSTER_0_CONTEXT_NAME}" -n "${MDB_NAMESPACE}" -f - < /tmp/mdb_script.js -mongosh --quiet "mongodb://search-user:${MDB_SEARCH_USER_PASSWORD}@mdbc-rs-0.mdbc-rs-svc.${MDB_NAMESPACE}.svc.cluster.local:27017/?replicaSet=mdbc-rs" < /tmp/mdb_script.js +mongosh --quiet "mongodb://mdb-user:${MDB_USER_PASSWORD}@mdbc-rs-0.mdbc-rs-svc.${MDB_NAMESPACE}.svc.cluster.local:27017/?replicaSet=mdbc-rs" < /tmp/mdb_script.js EOF )" diff --git a/docs/community-search/quick-start/code_snippets/090_helm_add_mogodb_repo.sh b/docs/community-search/quick-start/code_snippets/090_helm_add_mogodb_repo.sh old mode 100644 new mode 100755 diff --git a/docs/community-search/quick-start/code_snippets/9010_delete_namespace.sh b/docs/community-search/quick-start/code_snippets/9010_delete_namespace.sh old mode 100644 new mode 100755 index 96db9a4b6..ed2d3046d --- a/docs/community-search/quick-start/code_snippets/9010_delete_namespace.sh +++ b/docs/community-search/quick-start/code_snippets/9010_delete_namespace.sh @@ -1 +1 @@ -kubectl --context +kubectl --context "${K8S_CLUSTER_0_CONTEXT_NAME}" delete namespace "${MDB_NAMESPACE}" diff --git a/docs/community-search/quick-start/community_search_snippets_test.sh.run.log b/docs/community-search/quick-start/community_search_snippets_test.sh.run.log deleted file mode 100644 index 643009e51..000000000 --- a/docs/community-search/quick-start/community_search_snippets_test.sh.run.log +++ /dev/null @@ -1,16 +0,0 @@ -090_helm_add_mogodb_repo -0100_install_operator -0200_configure_community_search_pullsecret -0210_verify_community_search_pullsecret -0305_create_mongodb_community_user_secrets -0310_create_mongodb_community_resource -0315_wait_for_community_resource -0320_create_mongodb_search_resource -0325_wait_for_search_resource -0330_wait_for_community_resource -0335_show_running_pods -0410_run_mongodb_tools_pod -0420_import_movies_mflix_database -0430_create_search_index -0440_wait_for_search_index_ready -0450_execute_search_query diff --git a/docs/community-search/quick-start/env_variables.sh b/docs/community-search/quick-start/env_variables.sh index 0d0a98a5f..613b1461b 100644 --- a/docs/community-search/quick-start/env_variables.sh +++ b/docs/community-search/quick-start/env_variables.sh @@ -1,15 +1,18 @@ # set it to the context name of the k8s cluster export K8S_CLUSTER_0_CONTEXT_NAME="" -# At the private preview stage the community search image is accessible only from a private repository. -# Please contact MongoDB Support to get access. -export PRIVATE_PREVIEW_IMAGE_PULLSECRET="<.dockerconfigjson>" - # the following namespace will be created if not exists export MDB_NAMESPACE="mongodb" +# minimum required MongoDB version for running MongoDB Search is 8.0.10 +export MDB_VERSION="8.0.10" + +# root admin user for convenience, not used here at all in this guide export MDB_ADMIN_USER_PASSWORD="admin-user-password-CHANGE-ME" -export MDB_SEARCH_USER_PASSWORD="search-user-password-CHANGE-ME" +# regular user performing restore and search queries on sample mflix database +export MDB_USER_PASSWORD="mdb-user-password-CHANGE-ME" +# user for MongoDB Search to connect to the replica set to synchronise data from +export MDB_SEARCH_SYNC_USER_PASSWORD="search-sync-user-password-CHANGE-ME" export OPERATOR_HELM_CHART="mongodb/mongodb-kubernetes" # comma-separated key=value pairs for additional parameters passed to the helm-chart installing the operator 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..5b528437c 100644 --- a/docs/community-search/quick-start/env_variables_e2e_private.sh +++ b/docs/community-search/quick-start/env_variables_e2e_private.sh @@ -1,8 +1,7 @@ export K8S_CLUSTER_0_CONTEXT_NAME="${CLUSTER_NAME}" -export PRIVATE_PREVIEW_IMAGE_PULLSECRET="${COMMUNITY_PRIVATE_PREVIEW_PULLSECRET_DOCKERCONFIGJSON}" - -source scripts/funcs/operator_deployment +source "${PROJECT_DIR}/scripts/funcs/operator_deployment" +source "${PROJECT_DIR}/scripts/dev/contexts/e2e_mdb_community" OPERATOR_ADDITIONAL_HELM_VALUES="$(get_operator_helm_values | tr ' ' ',')" 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_dev.sh b/docs/community-search/quick-start/env_variables_e2e_private_dev.sh new file mode 100644 index 000000000..89f8a3932 --- /dev/null +++ b/docs/community-search/quick-start/env_variables_e2e_private_dev.sh @@ -0,0 +1,33 @@ +export K8S_CLUSTER_0_CONTEXT_NAME="kind-kind" + +# patch id from evergreen patch +version_id="68876175f5ad6d0007fdc1d4" + +search_image_repo="268558157000.dkr.ecr.eu-west-1.amazonaws.com/mongot" +search_image_hash="fbd60fb055dd500058edcb45677ea85d19421f47" + +ecr="268558157000.dkr.ecr.us-east-1.amazonaws.com" +declare -a helm_values=( +"registry.imagePullSecrets=image-registries-secret" +"registry.operator=${ecr}/dev" +"registry.initOpsManager=${ecr}/dev" +"registry.initAppDb=${ecr}/dev" +"registry.initDatabase=${ecr}/dev" +"registry.agent=${ecr}/dev" +"registry.opsManager=quay.io/mongodb" +"registry.appDb=quay.io/mongodb" +"registry.database=${ecr}/dev" +"operator.version=${version_id}" +"initOpsManager.version=${version_id}" +"initAppDb.version=${version_id}" +"initDatabase.version=${version_id}" +"database.version=${version_id}" +"search.community.repo=${search_image_repo}" +"search.community.name=community" +"search.community.version=${search_image_hash}" +) + +OPERATOR_ADDITIONAL_HELM_VALUES="$(echo -n "${helm_values[@]}" | tr ' ' ',')" +export OPERATOR_ADDITIONAL_HELM_VALUES +OPERATOR_HELM_CHART="$(realpath "../../../helm_chart")" +export OPERATOR_HELM_CHART diff --git a/docs/community-search/quick-start/output/0100_install_operator.out b/docs/community-search/quick-start/output/0100_install_operator.out index c54b9c92e..075f69619 100644 --- a/docs/community-search/quick-start/output/0100_install_operator.out +++ b/docs/community-search/quick-start/output/0100_install_operator.out @@ -1,12 +1,36 @@ Release "mongodb-kubernetes" does not exist. Installing it now. NAME: mongodb-kubernetes -LAST DEPLOYED: Tue Jul 8 07:04:51 2025 +LAST DEPLOYED: Mon Jul 28 15:07:59 2025 NAMESPACE: mongodb STATUS: deployed REVISION: 1 TEST SUITE: None USER-SUPPLIED VALUES: -dummy: value +database: + version: 68876175f5ad6d0007fdc1d4 +initAppDb: + version: 68876175f5ad6d0007fdc1d4 +initDatabase: + version: 68876175f5ad6d0007fdc1d4 +initOpsManager: + version: 68876175f5ad6d0007fdc1d4 +operator: + version: 68876175f5ad6d0007fdc1d4 +registry: + agent: 268558157000.dkr.ecr.us-east-1.amazonaws.com/dev + appDb: quay.io/mongodb + database: 268558157000.dkr.ecr.us-east-1.amazonaws.com/dev + imagePullSecrets: image-registries-secret + initAppDb: 268558157000.dkr.ecr.us-east-1.amazonaws.com/dev + initDatabase: 268558157000.dkr.ecr.us-east-1.amazonaws.com/dev + initOpsManager: 268558157000.dkr.ecr.us-east-1.amazonaws.com/dev + operator: 268558157000.dkr.ecr.us-east-1.amazonaws.com/dev + opsManager: quay.io/mongodb +search: + community: + name: community + repo: 268558157000.dkr.ecr.eu-west-1.amazonaws.com/mongot + version: fbd60fb055dd500058edcb45677ea85d19421f47 COMPUTED VALUES: agent: @@ -39,17 +63,16 @@ community: version: 4.4.0 database: name: mongodb-kubernetes-database - version: 1.2.0 -dummy: value + version: 68876175f5ad6d0007fdc1d4 initAppDb: name: mongodb-kubernetes-init-appdb - version: 1.2.0 + version: 68876175f5ad6d0007fdc1d4 initDatabase: name: mongodb-kubernetes-init-database - version: 1.2.0 + version: 68876175f5ad6d0007fdc1d4 initOpsManager: name: mongodb-kubernetes-init-ops-manager - version: 1.2.0 + version: 68876175f5ad6d0007fdc1d4 managedSecurityContext: false mongodb: appdbAssumeOldFormat: false @@ -96,7 +119,7 @@ operator: vaultSecretBackend: enabled: false tlsSecretRef: "" - version: 1.2.0 + version: 68876175f5ad6d0007fdc1d4 watchedResources: - mongodb - opsmanagers @@ -112,24 +135,23 @@ readinessProbe: name: mongodb-kubernetes-readinessprobe version: 1.0.22 registry: - agent: quay.io/mongodb + agent: 268558157000.dkr.ecr.us-east-1.amazonaws.com/dev appDb: quay.io/mongodb - database: quay.io/mongodb - imagePullSecrets: null - initAppDb: quay.io/mongodb - initDatabase: quay.io/mongodb - initOpsManager: quay.io/mongodb - operator: quay.io/mongodb + database: 268558157000.dkr.ecr.us-east-1.amazonaws.com/dev + imagePullSecrets: image-registries-secret + initAppDb: 268558157000.dkr.ecr.us-east-1.amazonaws.com/dev + initDatabase: 268558157000.dkr.ecr.us-east-1.amazonaws.com/dev + initOpsManager: 268558157000.dkr.ecr.us-east-1.amazonaws.com/dev + operator: 268558157000.dkr.ecr.us-east-1.amazonaws.com/dev opsManager: quay.io/mongodb pullPolicy: Always readinessProbe: quay.io/mongodb versionUpgradeHook: quay.io/mongodb search: community: - name: mongodb-search-community - repo: quay.io/mongodb - version: 1.47.0 -subresourceEnabled: true + name: community + repo: 268558157000.dkr.ecr.eu-west-1.amazonaws.com/mongot + version: fbd60fb055dd500058edcb45677ea85d19421f47 versionUpgradeHook: name: mongodb-kubernetes-operator-version-upgrade-post-start-hook version: 1.0.9 @@ -143,6 +165,8 @@ kind: ServiceAccount metadata: name: mongodb-kubernetes-appdb namespace: mongodb +imagePullSecrets: + - name: image-registries-secret --- # Source: mongodb-kubernetes/templates/database-roles.yaml apiVersion: v1 @@ -150,6 +174,8 @@ kind: ServiceAccount metadata: name: mongodb-kubernetes-database-pods namespace: mongodb +imagePullSecrets: + - name: image-registries-secret --- # Source: mongodb-kubernetes/templates/database-roles.yaml apiVersion: v1 @@ -157,6 +183,8 @@ kind: ServiceAccount metadata: name: mongodb-kubernetes-ops-manager namespace: mongodb +imagePullSecrets: + - name: image-registries-secret --- # Source: mongodb-kubernetes/templates/operator-sa.yaml apiVersion: v1 @@ -164,8 +192,10 @@ kind: ServiceAccount metadata: name: mongodb-kubernetes-operator namespace: mongodb +imagePullSecrets: + - name: image-registries-secret --- -# Source: mongodb-kubernetes/templates/operator-roles.yaml +# Source: mongodb-kubernetes/templates/operator-roles-clustermongodbroles.yaml kind: ClusterRole apiVersion: rbac.authorization.k8s.io/v1 metadata: @@ -178,34 +208,7 @@ rules: resources: - clustermongodbroles --- -# Source: mongodb-kubernetes/templates/operator-roles.yaml -kind: ClusterRole -apiVersion: rbac.authorization.k8s.io/v1 -metadata: - name: mongodb-kubernetes-operator-mongodb-webhook -rules: - - apiGroups: - - "admissionregistration.k8s.io" - resources: - - validatingwebhookconfigurations - verbs: - - get - - create - - update - - delete - - apiGroups: - - "" - resources: - - services - verbs: - - get - - list - - watch - - create - - update - - delete ---- -# Source: mongodb-kubernetes/templates/operator-roles.yaml +# Source: mongodb-kubernetes/templates/operator-roles-telemetry.yaml # Additional ClusterRole for clusterVersionDetection kind: ClusterRole apiVersion: rbac.authorization.k8s.io/v1 @@ -233,7 +236,34 @@ rules: verbs: - list --- -# Source: mongodb-kubernetes/templates/operator-roles.yaml +# Source: mongodb-kubernetes/templates/operator-roles-webhook.yaml +kind: ClusterRole +apiVersion: rbac.authorization.k8s.io/v1 +metadata: + name: mongodb-kubernetes-operator-mongodb-webhook +rules: + - apiGroups: + - "admissionregistration.k8s.io" + resources: + - validatingwebhookconfigurations + verbs: + - get + - create + - update + - delete + - apiGroups: + - "" + resources: + - services + verbs: + - get + - list + - watch + - create + - update + - delete +--- +# Source: mongodb-kubernetes/templates/operator-roles-clustermongodbroles.yaml kind: ClusterRoleBinding apiVersion: rbac.authorization.k8s.io/v1 metadata: @@ -247,30 +277,30 @@ subjects: name: mongodb-kubernetes-operator namespace: mongodb --- -# Source: mongodb-kubernetes/templates/operator-roles.yaml +# Source: mongodb-kubernetes/templates/operator-roles-telemetry.yaml +# ClusterRoleBinding for clusterVersionDetection kind: ClusterRoleBinding apiVersion: rbac.authorization.k8s.io/v1 metadata: - name: mongodb-kubernetes-operator-mongodb-webhook-binding + name: mongodb-kubernetes-operator-mongodb-cluster-telemetry-binding roleRef: apiGroup: rbac.authorization.k8s.io kind: ClusterRole - name: mongodb-kubernetes-operator-mongodb-webhook + name: mongodb-kubernetes-operator-cluster-telemetry subjects: - kind: ServiceAccount name: mongodb-kubernetes-operator namespace: mongodb --- -# Source: mongodb-kubernetes/templates/operator-roles.yaml -# ClusterRoleBinding for clusterVersionDetection +# Source: mongodb-kubernetes/templates/operator-roles-webhook.yaml kind: ClusterRoleBinding apiVersion: rbac.authorization.k8s.io/v1 metadata: - name: mongodb-kubernetes-operator-mongodb-cluster-telemetry-binding + name: mongodb-kubernetes-operator-mongodb-webhook-binding roleRef: apiGroup: rbac.authorization.k8s.io kind: ClusterRole - name: mongodb-kubernetes-operator-cluster-telemetry + name: mongodb-kubernetes-operator-mongodb-webhook subjects: - kind: ServiceAccount name: mongodb-kubernetes-operator @@ -298,7 +328,7 @@ rules: - delete - get --- -# Source: mongodb-kubernetes/templates/operator-roles.yaml +# Source: mongodb-kubernetes/templates/operator-roles-base.yaml kind: Role apiVersion: rbac.authorization.k8s.io/v1 metadata: @@ -378,7 +408,14 @@ rules: - opsmanagers/status - mongodbmulticluster/status - mongodbsearch/status - +--- +# Source: mongodb-kubernetes/templates/operator-roles-pvc-resize.yaml +kind: Role +apiVersion: rbac.authorization.k8s.io/v1 +metadata: + name: mongodb-kubernetes-operator-pvc-resize + namespace: mongodb +rules: - apiGroups: - '' resources: @@ -406,7 +443,7 @@ subjects: name: mongodb-kubernetes-appdb namespace: mongodb --- -# Source: mongodb-kubernetes/templates/operator-roles.yaml +# Source: mongodb-kubernetes/templates/operator-roles-base.yaml kind: RoleBinding apiVersion: rbac.authorization.k8s.io/v1 metadata: @@ -421,6 +458,21 @@ subjects: name: mongodb-kubernetes-operator namespace: mongodb --- +# Source: mongodb-kubernetes/templates/operator-roles-pvc-resize.yaml +kind: RoleBinding +apiVersion: rbac.authorization.k8s.io/v1 +metadata: + name: mongodb-kubernetes-operator-pvc-resize-binding + namespace: mongodb +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: mongodb-kubernetes-operator-pvc-resize +subjects: + - kind: ServiceAccount + name: mongodb-kubernetes-operator + namespace: mongodb +--- # Source: mongodb-kubernetes/templates/operator.yaml apiVersion: apps/v1 kind: Deployment @@ -445,9 +497,11 @@ spec: securityContext: runAsNonRoot: true runAsUser: 2000 + imagePullSecrets: + - name: image-registries-secret containers: - name: mongodb-kubernetes-operator - image: "quay.io/mongodb/mongodb-kubernetes:1.2.0" + image: "268558157000.dkr.ecr.us-east-1.amazonaws.com/dev/mongodb-kubernetes:68876175f5ad6d0007fdc1d4" imagePullPolicy: Always args: - -watch-resource=mongodb @@ -488,31 +542,31 @@ spec: value: Always # Database - name: MONGODB_ENTERPRISE_DATABASE_IMAGE - value: quay.io/mongodb/mongodb-kubernetes-database + value: 268558157000.dkr.ecr.us-east-1.amazonaws.com/dev/mongodb-kubernetes-database - name: INIT_DATABASE_IMAGE_REPOSITORY - value: quay.io/mongodb/mongodb-kubernetes-init-database + value: 268558157000.dkr.ecr.us-east-1.amazonaws.com/dev/mongodb-kubernetes-init-database - name: INIT_DATABASE_VERSION - value: 1.2.0 + value: 68876175f5ad6d0007fdc1d4 - name: DATABASE_VERSION - value: 1.2.0 + value: 68876175f5ad6d0007fdc1d4 # Ops Manager - name: OPS_MANAGER_IMAGE_REPOSITORY value: quay.io/mongodb/mongodb-enterprise-ops-manager-ubi - name: INIT_OPS_MANAGER_IMAGE_REPOSITORY - value: quay.io/mongodb/mongodb-kubernetes-init-ops-manager + value: 268558157000.dkr.ecr.us-east-1.amazonaws.com/dev/mongodb-kubernetes-init-ops-manager - name: INIT_OPS_MANAGER_VERSION - value: 1.2.0 + value: 68876175f5ad6d0007fdc1d4 # AppDB - name: INIT_APPDB_IMAGE_REPOSITORY - value: quay.io/mongodb/mongodb-kubernetes-init-appdb + value: 268558157000.dkr.ecr.us-east-1.amazonaws.com/dev/mongodb-kubernetes-init-appdb - name: INIT_APPDB_VERSION - value: 1.2.0 + value: 68876175f5ad6d0007fdc1d4 - name: OPS_MANAGER_IMAGE_PULL_POLICY value: Always - name: AGENT_IMAGE - value: "quay.io/mongodb/mongodb-agent:108.0.2.8729-1" + value: "268558157000.dkr.ecr.us-east-1.amazonaws.com/dev/mongodb-agent:108.0.2.8729-1" - name: MDB_AGENT_IMAGE_REPOSITORY - value: "quay.io/mongodb/mongodb-agent" + value: "268558157000.dkr.ecr.us-east-1.amazonaws.com/dev/mongodb-agent" - name: MONGODB_IMAGE value: mongodb-enterprise-server - name: MONGODB_REPO_URL @@ -521,6 +575,8 @@ spec: value: ubi8 - name: PERFORM_FAILOVER value: 'true' + - name: IMAGE_PULL_SECRETS + value: image-registries-secret - name: MDB_MAX_CONCURRENT_RECONCILES value: "1" - name: POD_NAME @@ -544,9 +600,9 @@ spec: value: "ubi8" # Community Env Vars End - name: MDB_SEARCH_COMMUNITY_REPO_URL - value: "quay.io/mongodb" + value: "268558157000.dkr.ecr.eu-west-1.amazonaws.com/mongot" - name: MDB_SEARCH_COMMUNITY_NAME - value: "mongodb-search-community" + value: "community" - name: MDB_SEARCH_COMMUNITY_VERSION - value: "1.47.0" + value: "fbd60fb055dd500058edcb45677ea85d19421f47" diff --git a/docs/community-search/quick-start/output/0200_configure_community_search_pullsecret.out b/docs/community-search/quick-start/output/0200_configure_community_search_pullsecret.out deleted file mode 100644 index c04db5317..000000000 --- a/docs/community-search/quick-start/output/0200_configure_community_search_pullsecret.out +++ /dev/null @@ -1,18 +0,0 @@ -secret/community-private-preview-pullsecret created -serviceaccount/mongodb-kubernetes-database-pods patched -ServiceAccount mongodb-kubernetes-database-pods has been patched: -apiVersion: v1 -imagePullSecrets: -- name: community-private-preview-pullsecret -kind: ServiceAccount -metadata: - annotations: - meta.helm.sh/release-name: mongodb-kubernetes - meta.helm.sh/release-namespace: mongodb - creationTimestamp: "2025-07-08T07:04:51Z" - labels: - app.kubernetes.io/managed-by: Helm - name: mongodb-kubernetes-database-pods - namespace: mongodb - resourceVersion: "902" - uid: 69ba91c9-2032-48dd-8490-b1b6b1c82625 diff --git a/docs/community-search/quick-start/output/0210_verify_community_search_pullsecret.out b/docs/community-search/quick-start/output/0210_verify_community_search_pullsecret.out deleted file mode 100644 index 80aafdc2d..000000000 --- a/docs/community-search/quick-start/output/0210_verify_community_search_pullsecret.out +++ /dev/null @@ -1,3 +0,0 @@ -Verifying mongodb-kubernetes-database-pods service account contains proper pull secret -{"name":"community-private-preview-pullsecret"} -SUCCESS: mongodb-kubernetes-database-pods service account contains proper pull secret diff --git a/docs/community-search/quick-start/output/0335_show_running_pods.out b/docs/community-search/quick-start/output/0335_show_running_pods.out index bca15125b..cd7017404 100644 --- a/docs/community-search/quick-start/output/0335_show_running_pods.out +++ b/docs/community-search/quick-start/output/0335_show_running_pods.out @@ -1,16 +1,20 @@ MongoDBCommunity resource NAME PHASE VERSION -mdbc-rs Running 8.0.6 +mdbc-rs Running 8.0.10 MongoDBSearch resource NAME PHASE AGE -mdbc-rs Running 5m21s +mdbc-rs Running 7m58s Pods running in cluster kind-kind NAME READY STATUS RESTARTS AGE -mdbc-rs-0 2/2 Running 1 (35s ago) 7m51s -mdbc-rs-1 2/2 Running 1 (3m11s ago) 6m45s -mdbc-rs-2 2/2 Running 1 (114s ago) 5m57s -mdbc-rs-search-0 1/1 Running 0 5m21s -mongodb-kubernetes-operator-64d5b47b46-fd6nr 1/1 Running 0 7m54s +mdb-debug-mdbc-rs-0-0 1/1 Running 0 10m +mdb-debug-mdbc-rs-1-0 1/1 Running 0 10m +mdb-debug-mdbc-rs-2-0 1/1 Running 0 10m +mdb-debug-mdbc-rs-search-0-0 1/1 Running 0 7m57s +mdbc-rs-0 2/2 Running 1 (3m13s ago) 10m +mdbc-rs-1 2/2 Running 1 (5m49s ago) 9m23s +mdbc-rs-2 2/2 Running 1 (4m32s ago) 8m35s +mdbc-rs-search-0 1/1 Running 0 25s +mongodb-kubernetes-operator-5776c8b4df-wm82f 1/1 Running 0 35s diff --git a/docs/community-search/quick-start/output/0440_wait_for_search_index_ready.out b/docs/community-search/quick-start/output/0440_wait_for_search_index_ready.out index 17d0d593b..b264580af 100644 --- a/docs/community-search/quick-start/output/0440_wait_for_search_index_ready.out +++ b/docs/community-search/quick-start/output/0440_wait_for_search_index_ready.out @@ -1,4 +1 @@ -Search index is not ready yet: status=BUILDING -Search index is not ready yet: status=BUILDING -Search index is not ready yet: status=BUILDING -Search index is ready. +Sleeping to wait for search indexes to be created diff --git a/docs/community-search/quick-start/output/090_helm_add_mogodb_repo.out b/docs/community-search/quick-start/output/090_helm_add_mogodb_repo.out index e8630fdff..b1576c236 100644 --- a/docs/community-search/quick-start/output/090_helm_add_mogodb_repo.out +++ b/docs/community-search/quick-start/output/090_helm_add_mogodb_repo.out @@ -1,4 +1,4 @@ -"mongodb" has been added to your repositories +"mongodb" already exists with the same configuration, skipping Hang tight while we grab the latest from your chart repositories... ...Successfully got an update from the "mongodb" chart repository Update Complete. ⎈Happy Helming!⎈ diff --git a/docs/community-search/quick-start/test.sh b/docs/community-search/quick-start/test.sh index 15caf5fe4..86fb6b23e 100755 --- a/docs/community-search/quick-start/test.sh +++ b/docs/community-search/quick-start/test.sh @@ -5,7 +5,7 @@ set -eou pipefail script_name=$(readlink -f "${BASH_SOURCE[0]}") script_dir=$(dirname "${script_name}") -source scripts/code_snippets/sample_test_runner.sh +source "${script_dir}/../../../scripts/code_snippets/sample_test_runner.sh" cd "${script_dir}" @@ -16,8 +16,6 @@ run 0046_create_image_pull_secrets.sh run_for_output 090_helm_add_mogodb_repo.sh run_for_output 0100_install_operator.sh -run_for_output 0200_configure_community_search_pullsecret.sh -run_for_output 0210_verify_community_search_pullsecret.sh run 0305_create_mongodb_community_user_secrets.sh run 0310_create_mongodb_community_resource.sh run 0315_wait_for_community_resource.sh 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/helm_chart/values.yaml b/helm_chart/values.yaml index b11aca350..07167acba 100644 --- a/helm_chart/values.yaml +++ b/helm_chart/values.yaml @@ -222,7 +222,11 @@ community: search: community: # Full Search container image url used for the MongoDB Community Search container will be constructed as {search.community.repo}/{search.community.name}:{search.community.version} - repo: quay.io/mongodb - name: mongodb-search-community +# repo: quay.io/mongodb +# name: mongodb-search-community +# # default MongoDB Search version used; can be overridden by setting MongoDBSearch.spec.version field. +# version: 1.47.0 + repo: 268558157000.dkr.ecr.eu-west-1.amazonaws.com + name: mongot/community # default MongoDB Search version used; can be overridden by setting MongoDBSearch.spec.version field. version: 1.47.0 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/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/public/mongodb-kubernetes-multi-cluster.yaml b/public/mongodb-kubernetes-multi-cluster.yaml index c28306670..b4cad9e51 100644 --- a/public/mongodb-kubernetes-multi-cluster.yaml +++ b/public/mongodb-kubernetes-multi-cluster.yaml @@ -432,9 +432,9 @@ spec: value: "ubi8" # Community Env Vars End - name: MDB_SEARCH_COMMUNITY_REPO_URL - value: "quay.io/mongodb" + value: "268558157000.dkr.ecr.eu-west-1.amazonaws.com" - name: MDB_SEARCH_COMMUNITY_NAME - value: "mongodb-search-community" + value: "mongot/community" - name: MDB_SEARCH_COMMUNITY_VERSION value: "1.47.0" volumes: diff --git a/public/mongodb-kubernetes-openshift.yaml b/public/mongodb-kubernetes-openshift.yaml index 54fa8b396..4286a0102 100644 --- a/public/mongodb-kubernetes-openshift.yaml +++ b/public/mongodb-kubernetes-openshift.yaml @@ -600,10 +600,10 @@ spec: - name: RELATED_IMAGE_MONGODB_IMAGE_8_0_0_ubi9 value: "quay.io/mongodb/mongodb-enterprise-server:8.0.0-ubi9" - name: RELATED_IMAGE_MDB_SEARCH_IMAGE_1_47_0 - value: "quay.io/mongodb/mongodb-search-community:1.47.0" + value: "268558157000.dkr.ecr.eu-west-1.amazonaws.com/mongot/community:1.47.0" - name: MDB_SEARCH_COMMUNITY_REPO_URL - value: "quay.io/mongodb" + value: "268558157000.dkr.ecr.eu-west-1.amazonaws.com" - name: MDB_SEARCH_COMMUNITY_NAME - value: "mongodb-search-community" + value: "mongot/community" - name: MDB_SEARCH_COMMUNITY_VERSION value: "1.47.0" diff --git a/public/mongodb-kubernetes.yaml b/public/mongodb-kubernetes.yaml index f3c727ad6..eb88ccbff 100644 --- a/public/mongodb-kubernetes.yaml +++ b/public/mongodb-kubernetes.yaml @@ -428,8 +428,8 @@ spec: value: "ubi8" # Community Env Vars End - name: MDB_SEARCH_COMMUNITY_REPO_URL - value: "quay.io/mongodb" + value: "268558157000.dkr.ecr.eu-west-1.amazonaws.com" - name: MDB_SEARCH_COMMUNITY_NAME - value: "mongodb-search-community" + value: "mongot/community" - name: MDB_SEARCH_COMMUNITY_VERSION value: "1.47.0" diff --git a/scripts/code_snippets/task_kind_community_search_snippets_test.sh b/scripts/code_snippets/task_kind_community_search_snippets_test.sh index 4b4501c2c..4f23a3e37 100755 --- a/scripts/code_snippets/task_kind_community_search_snippets_test.sh +++ b/scripts/code_snippets/task_kind_community_search_snippets_test.sh @@ -5,7 +5,9 @@ source scripts/dev/set_env_context.sh dump_logs() { source scripts/evergreen/e2e/dump_diagnostic_information.sh - dump_all_non_default_namespaces "$@" + if [[ "${SKIP_DUMP:-"false"}" != "true" ]]; then + dump_all_non_default_namespaces "$@" + fi } trap dump_logs EXIT 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 73202c211..080292666 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 0ee88f209..3b6426d35 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/private_kind_code_snippets b/scripts/dev/contexts/private_kind_code_snippets index 704957a6f..970f0dc3f 100644 --- a/scripts/dev/contexts/private_kind_code_snippets +++ b/scripts/dev/contexts/private_kind_code_snippets @@ -8,6 +8,7 @@ script_name=$(readlink -f "${BASH_SOURCE[0]}") script_dir=$(dirname "${script_name}") source "${script_dir}/root-context" +source "${script_dir}/e2e_mdb_community" export NAMESPACE=mongodb export CODE_SNIPPETS_FLAVOR=e2e_private diff --git a/scripts/dev/contexts/root-context b/scripts/dev/contexts/root-context index 888039232..e0110f1a4 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..590b3ed69 --- /dev/null +++ b/scripts/dev/contexts/variables/mongodb_search_dev @@ -0,0 +1,13 @@ +#!/usr/bin/env bash + +set -Eeou pipefail + +# Temporary development images built from mongot master +#export MDB_SEARCH_COMMUNITY_VERSION="776d43523d185b6b234289e17c191712a3e6569b" # master +#export MDB_SEARCH_COMMUNITY_VERSION="d6884ae132aab30497af55dbaff05e8274e9775f" # Local->Admin +#export MDB_SEARCH_COMMUNITY_VERSION="ad8acf5c3a045d6e0306ad67d61fcb5be40f57ae" # hardcoded mdbc-rs replicaset name +#export MDB_SEARCH_COMMUNITY_VERSION="b9b80915f5571bfa5fc2aa70acb20d784e68d79b" # hardcoded Local->Admin, handled replicaSetName in config +#export MDB_SEARCH_COMMUNITY_VERSION="fbd60fb055dd500058edcb45677ea85d19421f47" # Nolan's fixes +export MDB_SEARCH_COMMUNITY_VERSION="f3b9cf1cc358c34044a7abb4bf220f6e47e00873" # Aug 26th 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/dev/update_docs_snippets.sh b/scripts/dev/update_docs_snippets.sh index b9a45b378..bdec61db2 100755 --- a/scripts/dev/update_docs_snippets.sh +++ b/scripts/dev/update_docs_snippets.sh @@ -46,17 +46,16 @@ function prepare_repositories() { } function copy_files() { - samples_dir=$1 - dst_dir="${DOCS_DIR}/source/includes/code-examples/reference-architectures/${samples_dir}" - src_dir="${MCK_DIR}/public/architectures/${samples_dir}" + local src_dir="$1" + local dst_dir="$2" rm -rf "${dst_dir}" mkdir -p "${dst_dir}" - cp -r "${src_dir}/code_snippets" "${dst_dir}" - cp -r "${src_dir}/output" "${dst_dir}" || true - cp "${src_dir}/env_variables.sh" "${dst_dir}" || true - cp -r "${src_dir}/yamls" "${dst_dir}" || true + cp -r "${src_dir}/code_snippets" "${dst_dir}" 2>/dev/null || true + cp -r "${src_dir}/output" "${dst_dir}" 2>/dev/null || true + cp "${src_dir}/env_variables.sh" "${dst_dir}" 2>/dev/null || true + cp -r "${src_dir}/yamls" "${dst_dir}" 2>/dev/null || true } function prepare_docs_pr() { @@ -74,17 +73,26 @@ function prepare_docs_pr() { pushd ../ prepare_repositories -copy_files "ops-manager-multi-cluster" -copy_files "ops-manager-mc-no-mesh" -copy_files "mongodb-sharded-multi-cluster" -copy_files "mongodb-sharded-mc-no-mesh" -copy_files "mongodb-replicaset-multi-cluster" -copy_files "mongodb-replicaset-mc-no-mesh" -copy_files "setup-multi-cluster/verify-connectivity" -copy_files "setup-multi-cluster/setup-gke" -copy_files "setup-multi-cluster/setup-istio" -copy_files "setup-multi-cluster/setup-operator" -copy_files "setup-multi-cluster/setup-cert-manager" -copy_files "setup-multi-cluster/setup-externaldns" + +REF_ARCH_SRC_DIR="${MCK_DIR}/public/architectures" +REF_ARCH_DST_DIR="${DOCS_DIR}/source/includes/code-examples/reference-architectures" + +copy_files "${REF_ARCH_SRC_DIR}/ops-manager-multi-cluster" "${REF_ARCH_DST_DIR}/ops-manager-multi-cluster" +copy_files "${REF_ARCH_SRC_DIR}/ops-manager-mc-no-mesh" "${REF_ARCH_DST_DIR}/ops-manager-mc-no-mesh" +copy_files "${REF_ARCH_SRC_DIR}/mongodb-sharded-multi-cluster" "${REF_ARCH_DST_DIR}/mongodb-sharded-multi-cluster" +copy_files "${REF_ARCH_SRC_DIR}/mongodb-sharded-mc-no-mesh" "${REF_ARCH_DST_DIR}/mongodb-sharded-mc-no-mesh" +copy_files "${REF_ARCH_SRC_DIR}/mongodb-replicaset-multi-cluster" "${REF_ARCH_DST_DIR}/mongodb-replicaset-multi-cluster" +copy_files "${REF_ARCH_SRC_DIR}/mongodb-replicaset-mc-no-mesh" "${REF_ARCH_DST_DIR}/mongodb-replicaset-mc-no-mesh" +copy_files "${REF_ARCH_SRC_DIR}/setup-multi-cluster/verify-connectivity" "${REF_ARCH_DST_DIR}/setup-multi-cluster/verify-connectivity" +copy_files "${REF_ARCH_SRC_DIR}/setup-multi-cluster/setup-gke" "${REF_ARCH_DST_DIR}/setup-multi-cluster/setup-gke" +copy_files "${REF_ARCH_SRC_DIR}/setup-multi-cluster/setup-istio" "${REF_ARCH_DST_DIR}/setup-multi-cluster/setup-istio" +copy_files "${REF_ARCH_SRC_DIR}/setup-multi-cluster/setup-operator" "${REF_ARCH_DST_DIR}/setup-multi-cluster/setup-operator" +copy_files "${REF_ARCH_SRC_DIR}/setup-multi-cluster/setup-cert-manager" "${REF_ARCH_DST_DIR}/setup-multi-cluster/setup-cert-manager" +copy_files "${REF_ARCH_SRC_DIR}/setup-multi-cluster/setup-externaldns" "${REF_ARCH_DST_DIR}/setup-multi-cluster/setup-externaldns" + +DOCS_SNIPPETS_SRC_DIR="${MCK_DIR}/docs" +DOCS_SNIPPEES_DST_DIR="${DOCS_DIR}/source/includes/code-examples" +copy_files "${DOCS_SNIPPETS_SRC_DIR}/community-search/quick-start" "${DOCS_SNIPPEES_DST_DIR}/community-search/quick-start" + prepare_docs_pr popd diff --git a/scripts/funcs/operator_deployment b/scripts/funcs/operator_deployment index f13b31f23..43a507cc9 100644 --- a/scripts/funcs/operator_deployment +++ b/scripts/funcs/operator_deployment @@ -34,6 +34,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}}" )