diff --git a/commands/databases.go b/commands/databases.go index ee2a0d661..5ebb8529e 100644 --- a/commands/databases.go +++ b/commands/databases.go @@ -2424,8 +2424,11 @@ func databaseConfiguration() *Command { Long: "The subcommands of `doctl databases configuration` are used to view a database cluster's configuration.", }, } - getConfigurationLongDesc := "Retrieves the configuration for the specified cluster, including its backup settings, temporary file limit, and session timeout values." - updateConfigurationLongDesc := "Updates the specified database cluster's configuration. Using this command, you can update varioous settings like backup times, temporary file limits, and session timeouts." + getConfigurationLongDesc := "Retrieves the advanced configuration for the specified cluster, including its backup settings, temporary file limit, and session timeout values." + updateConfigurationLongDesc := `Updates the specified database cluster's advanced configuration. Using this command, you can update various settings like backup times, temporary file limits, and session timeouts. Available settings vary by database engine. + +This command functions as a PATCH request, meaning that only the specified fields are updated. If a field is not specified, it will not be changed. The settings are passed using the ` + "`" + `--config-json` + "`" + ` flag, which takes a JSON object as its value. +` getDatabaseCfgCommand := CmdBuilder( @@ -2439,6 +2442,7 @@ func databaseConfiguration() *Command { displayerType(&displayers.MySQLConfiguration{}), displayerType(&displayers.PostgreSQLConfiguration{}), displayerType(&displayers.RedisConfiguration{}), + displayerType(&displayers.MongoDBConfiguration{}), ) AddStringFlag( getDatabaseCfgCommand, @@ -2493,12 +2497,13 @@ func RunDatabaseConfigurationGet(c *CmdConfig) error { } allowedEngines := map[string]any{ - "mysql": nil, - "pg": nil, - "redis": nil, + "mysql": nil, + "pg": nil, + "redis": nil, + "mongodb": nil, } if _, ok := allowedEngines[engine]; !ok { - return fmt.Errorf("(%s) command: engine must be one of: 'pg', 'mysql', 'redis'", c.NS) + return fmt.Errorf("(%s) command: engine must be one of: 'pg', 'mysql', 'redis', 'mongodb'", c.NS) } dbId := args[0] @@ -2532,7 +2537,18 @@ func RunDatabaseConfigurationGet(c *CmdConfig) error { RedisConfig: *config, } return c.Display(&displayer) + } else if engine == "mongodb" { + config, err := c.Databases().GetMongoDBConfiguration(dbId) + if err != nil { + return err + } + + displayer := displayers.MongoDBConfiguration{ + MongoDBConfig: *config, + } + return c.Display(&displayer) } + return nil } @@ -2551,12 +2567,13 @@ func RunDatabaseConfigurationUpdate(c *CmdConfig) error { } allowedEngines := map[string]any{ - "mysql": nil, - "pg": nil, - "redis": nil, + "mysql": nil, + "pg": nil, + "redis": nil, + "mongodb": nil, } if _, ok := allowedEngines[engine]; !ok { - return fmt.Errorf("(%s) command: engine must be one of: 'pg', 'mysql', 'redis'", c.NS) + return fmt.Errorf("(%s) command: engine must be one of: 'pg', 'mysql', 'redis', 'mongodb'", c.NS) } configJson, err := c.Doit.GetString(c.NS, doctl.ArgDatabaseConfigJson) @@ -2580,7 +2597,13 @@ func RunDatabaseConfigurationUpdate(c *CmdConfig) error { if err != nil { return err } + } else if engine == "mongodb" { + err := c.Databases().UpdateMongoDBConfiguration(dbId, configJson) + if err != nil { + return err + } } + return nil } diff --git a/commands/databases_test.go b/commands/databases_test.go index 9bd7af2c4..410fc08a7 100644 --- a/commands/databases_test.go +++ b/commands/databases_test.go @@ -211,6 +211,10 @@ var ( RedisConfig: &godo.RedisConfig{}, } + testMongoDBConfiguration = do.MongoDBConfig{ + MongoDBConfig: &godo.MongoDBConfig{}, + } + topicReplicationFactor = uint32(3) testKafkaTopic = do.DatabaseTopic{ DatabaseTopic: &godo.DatabaseTopic{ @@ -1652,6 +1656,16 @@ func TestDatabaseConfigurationGet(t *testing.T) { assert.NoError(t, err) }) + withTestClient(t, func(config *CmdConfig, tm *tcMocks) { + tm.databases.EXPECT().GetMongoDBConfiguration(testDBCluster.ID).Return(&testMongoDBConfiguration, nil) + config.Args = append(config.Args, testDBCluster.ID) + config.Doit.Set(config.NS, doctl.ArgDatabaseEngine, "mongodb") + + err := RunDatabaseConfigurationGet(config) + + assert.NoError(t, err) + }) + withTestClient(t, func(config *CmdConfig, tm *tcMocks) { err := RunDatabaseConfigurationGet(config) @@ -1674,3 +1688,67 @@ func TestDatabaseConfigurationGet(t *testing.T) { assert.Error(t, err) }) } + +func TestDatabaseConfigurationUpdate(t *testing.T) { + withTestClient(t, func(config *CmdConfig, tm *tcMocks) { + tm.databases.EXPECT().UpdateMySQLConfiguration(testDBCluster.ID, "").Return(nil) + config.Args = append(config.Args, testDBCluster.ID) + config.Doit.Set(config.NS, doctl.ArgDatabaseEngine, "mysql") + + err := RunDatabaseConfigurationUpdate(config) + + assert.NoError(t, err) + }) + + withTestClient(t, func(config *CmdConfig, tm *tcMocks) { + tm.databases.EXPECT().UpdatePostgreSQLConfiguration(testDBCluster.ID, "").Return(nil) + config.Args = append(config.Args, testDBCluster.ID) + config.Doit.Set(config.NS, doctl.ArgDatabaseEngine, "pg") + + err := RunDatabaseConfigurationUpdate(config) + + assert.NoError(t, err) + }) + + withTestClient(t, func(config *CmdConfig, tm *tcMocks) { + tm.databases.EXPECT().UpdateRedisConfiguration(testDBCluster.ID, "").Return(nil) + config.Args = append(config.Args, testDBCluster.ID) + config.Doit.Set(config.NS, doctl.ArgDatabaseEngine, "redis") + + err := RunDatabaseConfigurationUpdate(config) + + assert.NoError(t, err) + }) + + withTestClient(t, func(config *CmdConfig, tm *tcMocks) { + tm.databases.EXPECT().UpdateMongoDBConfiguration(testDBCluster.ID, "").Return(nil) + config.Args = append(config.Args, testDBCluster.ID) + config.Doit.Set(config.NS, doctl.ArgDatabaseEngine, "mongodb") + + err := RunDatabaseConfigurationUpdate(config) + + assert.NoError(t, err) + }) + + withTestClient(t, func(config *CmdConfig, tm *tcMocks) { + err := RunDatabaseConfigurationUpdate(config) + + assert.Equal(t, err, doctl.NewMissingArgsErr(config.NS)) + }) + + withTestClient(t, func(config *CmdConfig, tm *tcMocks) { + config.Args = append(config.Args, testDBCluster.ID, "extra arg") + + err := RunDatabaseConfigurationUpdate(config) + + assert.Equal(t, err, doctl.NewTooManyArgsErr(config.NS)) + }) + + withTestClient(t, func(config *CmdConfig, tm *tcMocks) { + config.Args = append(config.Args, testDBCluster.ID) + + err := RunDatabaseConfigurationUpdate(config) + + assert.Error(t, err) + }) +} diff --git a/commands/displayers/database.go b/commands/displayers/database.go index 61df84679..639fe3959 100644 --- a/commands/displayers/database.go +++ b/commands/displayers/database.go @@ -1678,6 +1678,67 @@ func (dc *RedisConfiguration) KV() []map[string]any { return o } +type MongoDBConfiguration struct { + MongoDBConfig do.MongoDBConfig +} + +var _ Displayable = &MongoDBConfiguration{} + +func (dc *MongoDBConfiguration) JSON(out io.Writer) error { + return writeJSON(dc.MongoDBConfig, out) +} + +func (dc *MongoDBConfiguration) Cols() []string { + return []string{ + "key", + "value", + } +} + +func (dc *MongoDBConfiguration) ColMap() map[string]string { + return map[string]string{ + "key": "key", + "value": "value", + } +} + +func (dc *MongoDBConfiguration) KV() []map[string]any { + c := dc.MongoDBConfig + o := []map[string]any{} + if c.DefaultReadConcern != nil { + o = append(o, map[string]any{ + "key": "DefaultReadConcern", + "value": *c.DefaultReadConcern, + }) + } + if c.DefaultWriteConcern != nil { + o = append(o, map[string]any{ + "key": "DefaultWriteConcern", + "value": *c.DefaultWriteConcern, + }) + } + if c.SlowOpThresholdMs != nil { + o = append(o, map[string]any{ + "key": "SlowOpThresholdMs", + "value": *c.SlowOpThresholdMs, + }) + } + if c.TransactionLifetimeLimitSeconds != nil { + o = append(o, map[string]any{ + "key": "TransactionLifetimeLimitSeconds", + "value": *c.TransactionLifetimeLimitSeconds, + }) + } + if c.Verbosity != nil { + o = append(o, map[string]any{ + "key": "Verbosity", + "value": *c.Verbosity, + }) + } + + return o +} + type DatabaseEvents struct { DatabaseEvents do.DatabaseEvents } diff --git a/do/databases.go b/do/databases.go index d839c41e2..db6892d54 100644 --- a/do/databases.go +++ b/do/databases.go @@ -120,6 +120,11 @@ type RedisConfig struct { *godo.RedisConfig } +// MongoDBConfig is a wrapper for godo.MongoDBConfig +type MongoDBConfig struct { + *godo.MongoDBConfig +} + // DatabaseTopics is a slice of DatabaseTopic type DatabaseTopics []DatabaseTopic @@ -200,10 +205,12 @@ type DatabasesService interface { GetMySQLConfiguration(databaseID string) (*MySQLConfig, error) GetPostgreSQLConfiguration(databaseID string) (*PostgreSQLConfig, error) GetRedisConfiguration(databaseID string) (*RedisConfig, error) + GetMongoDBConfiguration(databaseID string) (*MongoDBConfig, error) UpdateMySQLConfiguration(databaseID string, confString string) error UpdatePostgreSQLConfiguration(databaseID string, confString string) error UpdateRedisConfiguration(databaseID string, confString string) error + UpdateMongoDBConfiguration(databaseID string, confString string) error ListTopics(string) (DatabaseTopics, error) GetTopic(string, string) (*DatabaseTopic, error) @@ -695,6 +702,17 @@ func (ds *databasesService) GetRedisConfiguration(databaseID string) (*RedisConf }, nil } +func (ds *databasesService) GetMongoDBConfiguration(databaseID string) (*MongoDBConfig, error) { + cfg, _, err := ds.client.Databases.GetMongoDBConfig(context.TODO(), databaseID) + if err != nil { + return nil, err + } + + return &MongoDBConfig{ + MongoDBConfig: cfg, + }, nil +} + func (ds *databasesService) UpdateMySQLConfiguration(databaseID string, confString string) error { var conf godo.MySQLConfig err := json.Unmarshal([]byte(confString), &conf) @@ -740,6 +758,21 @@ func (ds *databasesService) UpdateRedisConfiguration(databaseID string, confStri return nil } +func (ds *databasesService) UpdateMongoDBConfiguration(databaseID string, confString string) error { + var conf godo.MongoDBConfig + err := json.Unmarshal([]byte(confString), &conf) + if err != nil { + return err + } + + _, err = ds.client.Databases.UpdateMongoDBConfig(context.TODO(), databaseID, &conf) + if err != nil { + return err + } + + return nil +} + func (ds *databasesService) ListTopics(databaseID string) (DatabaseTopics, error) { f := func(opt *godo.ListOptions) ([]any, *godo.Response, error) { list, resp, err := ds.client.Databases.ListTopics(context.TODO(), databaseID, opt) diff --git a/do/mocks/DatabasesService.go b/do/mocks/DatabasesService.go index e14fdbc75..8de2fcff4 100644 --- a/do/mocks/DatabasesService.go +++ b/do/mocks/DatabasesService.go @@ -318,6 +318,21 @@ func (mr *MockDatabasesServiceMockRecorder) GetMaintenance(arg0 any) *gomock.Cal return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetMaintenance", reflect.TypeOf((*MockDatabasesService)(nil).GetMaintenance), arg0) } +// GetMongoDBConfiguration mocks base method. +func (m *MockDatabasesService) GetMongoDBConfiguration(databaseID string) (*do.MongoDBConfig, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetMongoDBConfiguration", databaseID) + ret0, _ := ret[0].(*do.MongoDBConfig) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetMongoDBConfiguration indicates an expected call of GetMongoDBConfiguration. +func (mr *MockDatabasesServiceMockRecorder) GetMongoDBConfiguration(databaseID any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetMongoDBConfiguration", reflect.TypeOf((*MockDatabasesService)(nil).GetMongoDBConfiguration), databaseID) +} + // GetMySQLConfiguration mocks base method. func (m *MockDatabasesService) GetMySQLConfiguration(databaseID string) (*do.MySQLConfig, error) { m.ctrl.T.Helper() @@ -721,6 +736,20 @@ func (mr *MockDatabasesServiceMockRecorder) UpdateMaintenance(arg0, arg1 any) *g return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateMaintenance", reflect.TypeOf((*MockDatabasesService)(nil).UpdateMaintenance), arg0, arg1) } +// UpdateMongoDBConfiguration mocks base method. +func (m *MockDatabasesService) UpdateMongoDBConfiguration(databaseID, confString string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "UpdateMongoDBConfiguration", databaseID, confString) + ret0, _ := ret[0].(error) + return ret0 +} + +// UpdateMongoDBConfiguration indicates an expected call of UpdateMongoDBConfiguration. +func (mr *MockDatabasesServiceMockRecorder) UpdateMongoDBConfiguration(databaseID, confString any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateMongoDBConfiguration", reflect.TypeOf((*MockDatabasesService)(nil).UpdateMongoDBConfiguration), databaseID, confString) +} + // UpdateMySQLConfiguration mocks base method. func (m *MockDatabasesService) UpdateMySQLConfiguration(databaseID, confString string) error { m.ctrl.T.Helper() diff --git a/integration/database_config_get_test.go b/integration/database_config_get_test.go index 298bb2919..a4f1f88ea 100644 --- a/integration/database_config_get_test.go +++ b/integration/database_config_get_test.go @@ -60,6 +60,18 @@ var _ = suite("database/config/get", func(t *testing.T, when spec.G, it spec.S) } w.Write([]byte(databaseConfigRedisGetResponse)) + case "/v2/databases/mongodb-database-id/config": + auth := req.Header.Get("Authorization") + if auth != "Bearer some-magic-token" { + w.WriteHeader(http.StatusTeapot) + } + + if req.Method != http.MethodGet { + w.WriteHeader(http.StatusMethodNotAllowed) + return + } + + w.Write([]byte(databaseConfigMongoDBGetResponse)) default: dump, err := httputil.DumpRequest(req, true) if err != nil { @@ -256,6 +268,15 @@ RedisACLChannelsDefault allchannels "redis_persistence": "rdb", "redis_acl_channels_default": "allchannels" } - } -` + }` + + databaseConfigMongoDBGetResponse = `{ + "config": { + "default_read_concern": "local", + "default_write_concern": "majority", + "slow_op_threshold_ms": 100, + "transaction_lifetime_limit_seconds": 60, + "verbosity": 1 + } + }` ) diff --git a/integration/database_config_update_test.go b/integration/database_config_update_test.go index 83793a496..ed5c660c6 100644 --- a/integration/database_config_update_test.go +++ b/integration/database_config_update_test.go @@ -84,6 +84,26 @@ var _ = suite("database/config/get", func(t *testing.T, when spec.G, it spec.S) } expect.Equal(expected, strings.TrimSpace(string(b))) + w.WriteHeader(http.StatusOK) + case "/v2/databases/mongodb-database-id/config": + auth := req.Header.Get("Authorization") + if auth != "Bearer some-magic-token" { + w.WriteHeader(http.StatusTeapot) + } + + if req.Method != http.MethodPatch { + w.WriteHeader(http.StatusMethodNotAllowed) + return + } + + expected := `{"config":{"verbosity":2}}` + b, err := io.ReadAll(req.Body) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + return + } + expect.Equal(expected, strings.TrimSpace(string(b))) + w.WriteHeader(http.StatusOK) default: dump, err := httputil.DumpRequest(req, true) @@ -152,4 +172,23 @@ var _ = suite("database/config/get", func(t *testing.T, when spec.G, it spec.S) expect.Empty(strings.TrimSpace(string(output))) }) }) + + when("all required flags are passed", func() { + it("updates the mongodb database config", func() { + cmd := exec.Command(builtBinaryPath, + "-t", "some-magic-token", + "-u", server.URL, + "database", + "configuration", + "update", + "--engine", "mongodb", + "mongodb-database-id", + "--config-json", `{"verbosity":2}`, + ) + + output, err := cmd.CombinedOutput() + expect.NoError(err, fmt.Sprintf("received error output: %s", output)) + expect.Empty(strings.TrimSpace(string(output))) + }) + }) })