@@ -57,6 +57,15 @@ const (
5757 defaultClusterDomain = "cluster.local"
5858)
5959
60+ // Connection string options that should be ignored as they are set through other means.
61+ var (
62+ protectedConnectionStringOptions = map [string ]struct {}{
63+ "replicaSet" : {},
64+ "ssl" : {},
65+ "tls" : {},
66+ }
67+ )
68+
6069// MongoDBCommunitySpec defines the desired state of MongoDB
6170type MongoDBCommunitySpec struct {
6271 // Members is the number of members in the replica set
@@ -119,6 +128,69 @@ type MongoDBCommunitySpec struct {
119128 // Prometheus configurations.
120129 // +optional
121130 Prometheus * Prometheus `json:"prometheus,omitempty"`
131+
132+ // Additional options to be appended to the connection string. These options apply to the entire resource and to each user.
133+ // +kubebuilder:validation:Type=object
134+ // +optional
135+ // +kubebuilder:pruning:PreserveUnknownFields
136+ // +nullable
137+ AdditionalConnectionStringConfig MapWrapper `json:"additionalConnectionStringConfig,omitempty"`
138+ }
139+
140+ // Wrapper for a map to be used by other structs.
141+ // The CRD generator does not support map[string]interface{}
142+ // on the top level and hence we need to work around this with
143+ // a wrapping struct.
144+ type MapWrapper struct {
145+ Object map [string ]interface {} `json:"-"`
146+ }
147+
148+ // MarshalJSON defers JSON encoding to the wrapped map
149+ func (m * MapWrapper ) MarshalJSON () ([]byte , error ) {
150+ return json .Marshal (m .Object )
151+ }
152+
153+ // UnmarshalJSON will decode the data into the wrapped map
154+ func (m * MapWrapper ) UnmarshalJSON (data []byte ) error {
155+ if m .Object == nil {
156+ m .Object = map [string ]interface {}{}
157+ }
158+
159+ // Handle keys like net.port to be set as nested maps.
160+ // Without this after unmarshalling there is just key "net.port" which is not
161+ // a nested map and methods like GetPort() cannot access the value.
162+ tmpMap := map [string ]interface {}{}
163+ err := json .Unmarshal (data , & tmpMap )
164+ if err != nil {
165+ return err
166+ }
167+
168+ for k , v := range tmpMap {
169+ m .SetOption (k , v )
170+ }
171+
172+ return nil
173+ }
174+
175+ func (m * MapWrapper ) DeepCopy () * MapWrapper {
176+ if m != nil && m .Object != nil {
177+ return & MapWrapper {
178+ Object : runtime .DeepCopyJSON (m .Object ),
179+ }
180+ }
181+ c := NewMapWrapper ()
182+ return & c
183+ }
184+
185+ // NewMapWrapper returns an empty MapWrapper
186+ func NewMapWrapper () MapWrapper {
187+ return MapWrapper {Object : map [string ]interface {}{}}
188+ }
189+
190+ // SetOption updated the MapWrapper with a new option
191+ func (m MapWrapper ) SetOption (key string , value interface {}) MapWrapper {
192+ m .Object = objx .New (m .Object ).Set (key , value )
193+ return m
122194}
123195
124196// ReplicaSetHorizonConfiguration holds the split horizon DNS settings for
@@ -277,10 +349,10 @@ type LogLevel string
277349
278350const (
279351 LogLevelDebug LogLevel = "DEBUG"
280- LogLevelInfo = "INFO"
281- LogLevelWarn = "WARN"
282- LogLevelError = "ERROR"
283- LogLevelFatal = "FATAL"
352+ LogLevelInfo string = "INFO"
353+ LogLevelWarn string = "WARN"
354+ LogLevelError string = "ERROR"
355+ LogLevelFatal string = "FATAL"
284356)
285357
286358type AgentConfiguration struct {
@@ -315,60 +387,13 @@ func (m *StatefulSetSpecWrapper) DeepCopy() *StatefulSetSpecWrapper {
315387
316388// MongodConfiguration holds the optional mongod configuration
317389// that should be merged with the operator created one.
318- //
319- // The CRD generator does not support map[string]interface{}
320- // on the top level and hence we need to work around this with
321- // a wrapping struct.
322390type MongodConfiguration struct {
323- Object map [string ]interface {} `json:"-"`
324- }
325-
326- // MarshalJSON defers JSON encoding to the wrapped map
327- func (m * MongodConfiguration ) MarshalJSON () ([]byte , error ) {
328- return json .Marshal (m .Object )
329- }
330-
331- // UnmarshalJSON will decode the data into the wrapped map
332- func (m * MongodConfiguration ) UnmarshalJSON (data []byte ) error {
333- if m .Object == nil {
334- m .Object = map [string ]interface {}{}
335- }
336-
337- // Handle keys like net.port to be set as nested maps.
338- // Without this after unmarshalling there is just key "net.port" which is not
339- // a nested map and methods like GetPort() cannot access the value.
340- tmpMap := map [string ]interface {}{}
341- err := json .Unmarshal (data , & tmpMap )
342- if err != nil {
343- return err
344- }
345-
346- for k , v := range tmpMap {
347- m .SetOption (k , v )
348- }
349-
350- return nil
351- }
352-
353- func (m * MongodConfiguration ) DeepCopy () * MongodConfiguration {
354- if m != nil && m .Object != nil {
355- return & MongodConfiguration {
356- Object : runtime .DeepCopyJSON (m .Object ),
357- }
358- }
359- c := NewMongodConfiguration ()
360- return & c
391+ MapWrapper `json:"-"`
361392}
362393
363394// NewMongodConfiguration returns an empty MongodConfiguration
364395func NewMongodConfiguration () MongodConfiguration {
365- return MongodConfiguration {Object : map [string ]interface {}{}}
366- }
367-
368- // SetOption updated the MongodConfiguration with a new option
369- func (m MongodConfiguration ) SetOption (key string , value interface {}) MongodConfiguration {
370- m .Object = objx .New (m .Object ).Set (key , value )
371- return m
396+ return MongodConfiguration {MapWrapper {map [string ]interface {}{}}}
372397}
373398
374399// GetDBDataDir returns the db path which should be used.
@@ -424,6 +449,14 @@ type MongoDBUser struct {
424449 // If provided, this secret must be different for each user in a deployment.
425450 // +optional
426451 ConnectionStringSecretName string `json:"connectionStringSecretName"`
452+
453+ // Additional options to be appended to the connection string.
454+ // These options apply only to this user and will override any existing options in the resource.
455+ // +kubebuilder:validation:Type=object
456+ // +optional
457+ // +kubebuilder:pruning:PreserveUnknownFields
458+ // +nullable
459+ AdditionalConnectionStringConfig MapWrapper `json:"additionalConnectionStringConfig,omitempty"`
427460}
428461
429462func (m MongoDBUser ) GetPasswordSecretKey () string {
@@ -692,6 +725,7 @@ func (m *MongoDBCommunity) GetScramUsers() []scram.User {
692725 PasswordSecretName : u .PasswordSecretRef .Name ,
693726 ScramCredentialsSecretName : u .GetScramCredentialsSecretName (),
694727 ConnectionStringSecretName : u .GetConnectionStringSecretName (m .Name ),
728+ ConnectionStringOptions : u .AdditionalConnectionStringConfig .Object ,
695729 }
696730 }
697731 return users
@@ -736,29 +770,96 @@ func (m *MongoDBCommunity) AutomationConfigArbitersThisReconciliation() int {
736770 })
737771}
738772
773+ // GetOptionsString return a string format of the connection string
774+ // options that can be appended directly to the connection string.
775+ //
776+ // Only takes into account options for the resource and not any user.
777+ func (m * MongoDBCommunity ) GetOptionsString () string {
778+ generalOptionsMap := m .Spec .AdditionalConnectionStringConfig .Object
779+ optionValues := make ([]string , len (generalOptionsMap ))
780+ i := 0
781+
782+ for key , value := range generalOptionsMap {
783+ if _ , protected := protectedConnectionStringOptions [key ]; ! protected {
784+ optionValues [i ] = fmt .Sprintf ("%s=%v" , key , value )
785+ i += 1
786+ }
787+ }
788+
789+ optionValues = optionValues [:i ]
790+
791+ optionsString := ""
792+ if i > 0 {
793+ optionsString = "&" + strings .Join (optionValues , "&" )
794+ }
795+ return optionsString
796+ }
797+
798+ // GetUserOptionsString return a string format of the connection string
799+ // options that can be appended directly to the connection string.
800+ //
801+ // Takes into account both user options and resource options.
802+ // User options will override any existing options in the resource.
803+ func (m * MongoDBCommunity ) GetUserOptionsString (user scram.User ) string {
804+ generalOptionsMap := m .Spec .AdditionalConnectionStringConfig .Object
805+ userOptionsMap := user .ConnectionStringOptions
806+ optionValues := make ([]string , len (generalOptionsMap )+ len (userOptionsMap ))
807+ i := 0
808+ for key , value := range userOptionsMap {
809+ if _ , protected := protectedConnectionStringOptions [key ]; ! protected {
810+ optionValues [i ] = fmt .Sprintf ("%s=%v" , key , value )
811+ i += 1
812+ }
813+ }
814+
815+ for key , value := range generalOptionsMap {
816+ _ , ok := userOptionsMap [key ]
817+ if _ , protected := protectedConnectionStringOptions [key ]; ! ok && ! protected {
818+ optionValues [i ] = fmt .Sprintf ("%s=%v" , key , value )
819+ i += 1
820+ }
821+ }
822+
823+ optionValues = optionValues [:i ]
824+
825+ optionsString := ""
826+ if i > 0 {
827+ optionsString = "&" + strings .Join (optionValues , "&" )
828+ }
829+ return optionsString
830+ }
831+
739832// MongoURI returns a mongo uri which can be used to connect to this deployment
740833func (m * MongoDBCommunity ) MongoURI (clusterDomain string ) string {
741- return fmt .Sprintf ("mongodb://%s/?replicaSet=%s" , strings .Join (m .Hosts (clusterDomain ), "," ), m .Name )
834+ optionsString := m .GetOptionsString ()
835+
836+ return fmt .Sprintf ("mongodb://%s/?replicaSet=%s%s" , strings .Join (m .Hosts (clusterDomain ), "," ), m .Name , optionsString )
742837}
743838
744839// MongoSRVURI returns a mongo srv uri which can be used to connect to this deployment
745840func (m * MongoDBCommunity ) MongoSRVURI (clusterDomain string ) string {
746841 if clusterDomain == "" {
747842 clusterDomain = defaultClusterDomain
748843 }
749- return fmt .Sprintf ("mongodb+srv://%s.%s.svc.%s/?replicaSet=%s" , m .ServiceName (), m .Namespace , clusterDomain , m .Name )
844+
845+ optionsString := m .GetOptionsString ()
846+
847+ return fmt .Sprintf ("mongodb+srv://%s.%s.svc.%s/?replicaSet=%s%s" , m .ServiceName (), m .Namespace , clusterDomain , m .Name , optionsString )
750848}
751849
752850// MongoAuthUserURI returns a mongo uri which can be used to connect to this deployment
753851// and includes the authentication data for the user
754852func (m * MongoDBCommunity ) MongoAuthUserURI (user scram.User , password string , clusterDomain string ) string {
755- return fmt .Sprintf ("mongodb://%s:%s@%s/%s?replicaSet=%s&ssl=%t" ,
853+ optionsString := m .GetUserOptionsString (user )
854+
855+ return fmt .Sprintf ("mongodb://%s:%s@%s/%s?replicaSet=%s&ssl=%t%s" ,
756856 url .QueryEscape (user .Username ),
757857 url .QueryEscape (password ),
758858 strings .Join (m .Hosts (clusterDomain ), "," ),
759859 user .Database ,
760860 m .Name ,
761- m .Spec .Security .TLS .Enabled )
861+ m .Spec .Security .TLS .Enabled ,
862+ optionsString )
762863}
763864
764865// MongoAuthUserSRVURI returns a mongo srv uri which can be used to connect to this deployment
@@ -767,15 +868,19 @@ func (m *MongoDBCommunity) MongoAuthUserSRVURI(user scram.User, password string,
767868 if clusterDomain == "" {
768869 clusterDomain = defaultClusterDomain
769870 }
770- return fmt .Sprintf ("mongodb+srv://%s:%s@%s.%s.svc.%s/%s?replicaSet=%s&ssl=%t" ,
871+
872+ optionsString := m .GetUserOptionsString (user )
873+
874+ return fmt .Sprintf ("mongodb+srv://%s:%s@%s.%s.svc.%s/%s?replicaSet=%s&ssl=%t%s" ,
771875 url .QueryEscape (user .Username ),
772876 url .QueryEscape (password ),
773877 m .ServiceName (),
774878 m .Namespace ,
775879 clusterDomain ,
776880 user .Database ,
777881 m .Name ,
778- m .Spec .Security .TLS .Enabled )
882+ m .Spec .Security .TLS .Enabled ,
883+ optionsString )
779884}
780885
781886func (m * MongoDBCommunity ) Hosts (clusterDomain string ) []string {
0 commit comments