Skip to content

Commit 9e4e2a5

Browse files
authored
CLOUDP-186802: Add connection string options (#1324)
* Add connection string options field in each struct that requires it. Additionally augmented the methods that create the connection string to include these options. * Add modification to zz_generated.deepcopy.go * Add unit tests * Add e2e tests * Add release notes and sample * Add release notes and sample * Modify release notes * Undo loglevel types * Fix loglevel types * Add e2e test to GitHub workflow * Ignore options replicaSet, tls, ssl * Adapt unit tests to parametric * Add variable for protected connection string options
1 parent 10ca73e commit 9e4e2a5

File tree

13 files changed

+676
-74
lines changed

13 files changed

+676
-74
lines changed

.action_templates/jobs/tests.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,3 +60,5 @@ tests:
6060
distro: ubi
6161
- test-name: replica_set_operator_upgrade
6262
distro: ubi
63+
- test-name: replica_set_connection_string_options
64+
distro: ubi

.github/workflows/e2e-fork.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,8 @@ jobs:
142142
distro: ubi
143143
- test-name: replica_set_operator_upgrade
144144
distro: ubi
145+
- test-name: replica_set_connection_string_options
146+
distro: ubi
145147
steps:
146148
# template: .action_templates/steps/cancel-previous.yaml
147149
- name: Cancel Previous Runs

.github/workflows/e2e.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,8 @@ jobs:
148148
distro: ubi
149149
- test-name: replica_set_operator_upgrade
150150
distro: ubi
151+
- test-name: replica_set_connection_string_options
152+
distro: ubi
151153
steps:
152154
# template: .action_templates/steps/cancel-previous.yaml
153155
- name: Cancel Previous Runs

api/v1/mongodbcommunity_types.go

Lines changed: 164 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -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
6170
type 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

278350
const (
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

286358
type 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.
322390
type 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
364395
func 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

429462
func (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
740833
func (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
745840
func (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
754852
func (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

781886
func (m *MongoDBCommunity) Hosts(clusterDomain string) []string {

0 commit comments

Comments
 (0)