Repository: digitalocean/doctl
Pull request #1455 (2023-10-28 16:39:21)

Add update configuration subcommand to databases
fixes #1430 


Patch:
diff --git a/args.go b/args.go
index b56b22f1..5f351f21 100644
--- a/args.go
+++ b/args.go
@@ -366,6 +366,8 @@ const (
 	ArgDatabaseRestoreFromTimestamp = "restore-from-timestamp"
 	// ArgDatabaseEngine is a flag for specifying which database engine to use
 	ArgDatabaseEngine = "engine"
+	// ArgDatabaseConfigJson is a flag for specifying the database configuration in JSON format for an update
+	ArgDatabaseConfigJson = "config-json"
 	// ArgDatabaseNumNodes is the number of nodes in the database cluster
 	ArgDatabaseNumNodes = "num-nodes"
 	// ArgDatabaseStorageSizeMib is the amount of disk space, in MiB, that should be allocated to the database cluster
diff --git a/commands/databases.go b/commands/databases.go
index 7a4c5fe6..fc97cad2 100644
--- a/commands/databases.go
+++ b/commands/databases.go
@@ -2246,20 +2246,20 @@ func databaseConfiguration() *Command {
 	cmd := &Command{
 		Command: &cobra.Command{
 			Use:     "configuration",
-			Aliases: []string{"cfg"},
+			Aliases: []string{"cfg", "config"},
 			Short:   "View the configuration of a database cluster given its ID and Engine",
 			Long:    "The subcommands of `doctl databases configuration` are used to view a database cluster's configuration.",
 		},
 	}
-	getMySQLConfigurationLongDesc := `
-		This will get a database cluster's configuration given its ID and Engine
-	`
-	getMySQLCfgCommand := CmdBuilder(
+	getConfigurationLongDesc := "This will get a database cluster's configuration given its ID and Engine"
+	updateConfigurationLongDesc := "This will update a database cluster's configuration given its ID and Engine and Desired Configuration (as JSON string)"
+
+	getDatabaseCfgCommand := CmdBuilder(
 		cmd,
 		RunDatabaseConfigurationGet,
 		"get <db-id>",
 		"Get a database cluster's configuration",
-		getMySQLConfigurationLongDesc,
+		getConfigurationLongDesc,
 		Writer,
 		aliasOpt("g"),
 		displayerType(&displayers.MySQLConfiguration{}),
@@ -2267,7 +2267,7 @@ func databaseConfiguration() *Command {
 		displayerType(&displayers.RedisConfiguration{}),
 	)
 	AddStringFlag(
-		getMySQLCfgCommand,
+		getDatabaseCfgCommand,
 		doctl.ArgDatabaseEngine,
 		"e",
 		"",
@@ -2275,6 +2275,32 @@ func databaseConfiguration() *Command {
 		requiredOpt(),
 	)
 
+	updateDatabaseCfgCommand := CmdBuilder(
+		cmd,
+		RunDatabaseConfigurationUpdate,
+		"update <db-id>",
+		"Update a database cluster's configuration",
+		updateConfigurationLongDesc,
+		Writer,
+		aliasOpt("u"),
+	)
+	AddStringFlag(
+		updateDatabaseCfgCommand,
+		doctl.ArgDatabaseEngine,
+		"e",
+		"",
+		"the engine of the database you want to update the configuration for",
+		requiredOpt(),
+	)
+	AddStringFlag(
+		updateDatabaseCfgCommand,
+		doctl.ArgDatabaseConfigJson,
+		"",
+		"{}",
+		"the desired configuration of the database cluster you want to update",
+		requiredOpt(),
+	)
+
 	return cmd
 }
 
@@ -2335,3 +2361,51 @@ func RunDatabaseConfigurationGet(c *CmdConfig) error {
 	}
 	return nil
 }
+
+func RunDatabaseConfigurationUpdate(c *CmdConfig) error {
+	args := c.Args
+	if len(args) == 0 {
+		return doctl.NewMissingArgsErr(c.NS)
+	}
+	if len(args) > 1 {
+		return doctl.NewTooManyArgsErr(c.NS)
+	}
+
+	engine, err := c.Doit.GetString(c.NS, doctl.ArgDatabaseEngine)
+	if err != nil {
+		return doctl.NewMissingArgsErr(c.NS)
+	}
+
+	allowedEngines := map[string]any{
+		"mysql": nil,
+		"pg":    nil,
+		"redis": nil,
+	}
+	if _, ok := allowedEngines[engine]; !ok {
+		return fmt.Errorf("(%s) command: engine must be one of: 'pg', 'mysql', 'redis'", c.NS)
+	}
+
+	configJson, err := c.Doit.GetString(c.NS, doctl.ArgDatabaseConfigJson)
+	if err != nil {
+		return doctl.NewMissingArgsErr(c.NS)
+	}
+
+	dbId := args[0]
+	if engine == "mysql" {
+		err := c.Databases().UpdateMySQLConfiguration(dbId, configJson)
+		if err != nil {
+			return err
+		}
+	} else if engine == "pg" {
+		err := c.Databases().UpdatePostgreSQLConfiguration(dbId, configJson)
+		if err != nil {
+			return err
+		}
+	} else if engine == "redis" {
+		err := c.Databases().UpdateRedisConfiguration(dbId, configJson)
+		if err != nil {
+			return err
+		}
+	}
+	return nil
+}
diff --git a/do/databases.go b/do/databases.go
index 9eb1b35b..35b5c1c8 100644
--- a/do/databases.go
+++ b/do/databases.go
@@ -15,6 +15,7 @@ package do
 
 import (
 	"context"
+	"encoding/json"
 	"strings"
 
 	"github.com/digitalocean/godo"
@@ -173,6 +174,10 @@ type DatabasesService interface {
 	GetPostgreSQLConfiguration(databaseID string) (*PostgreSQLConfig, error)
 	GetRedisConfiguration(databaseID string) (*RedisConfig, error)
 
+	UpdateMySQLConfiguration(databaseID string, confString string) error
+	UpdatePostgreSQLConfiguration(databaseID string, confString string) error
+	UpdateRedisConfiguration(databaseID string, confString string) error
+
 	ListTopics(string) (DatabaseTopics, error)
 	GetTopic(string, string) (*DatabaseTopic, error)
 	CreateTopic(string, *godo.DatabaseCreateTopicRequest) (*DatabaseTopic, error)
@@ -638,6 +643,51 @@ func (ds *databasesService) GetRedisConfiguration(databaseID string) (*RedisConf
 	}, nil
 }
 
+func (ds *databasesService) UpdateMySQLConfiguration(databaseID string, confString string) error {
+	var conf godo.MySQLConfig
+	err := json.Unmarshal([]byte(confString), &conf)
+	if err != nil {
+		return err
+	}
+
+	_, err = ds.client.Databases.UpdateMySQLConfig(context.TODO(), databaseID, &conf)
+	if err != nil {
+		return err
+	}
+
+	return nil
+}
+
+func (ds *databasesService) UpdatePostgreSQLConfiguration(databaseID string, confString string) error {
+	var conf godo.PostgreSQLConfig
+	err := json.Unmarshal([]byte(confString), &conf)
+	if err != nil {
+		return err
+	}
+
+	_, err = ds.client.Databases.UpdatePostgreSQLConfig(context.TODO(), databaseID, &conf)
+	if err != nil {
+		return err
+	}
+
+	return nil
+}
+
+func (ds *databasesService) UpdateRedisConfiguration(databaseID string, confString string) error {
+	var conf godo.RedisConfig
+	err := json.Unmarshal([]byte(confString), &conf)
+	if err != nil {
+		return err
+	}
+
+	_, err = ds.client.Databases.UpdateRedisConfig(context.TODO(), databaseID, &conf)
+	if err != nil {
+		return err
+	}
+
+	return nil
+}
+
 func (ds *databasesService) ListTopics(databaseID string) (DatabaseTopics, error) {
 	f := func(opt *godo.ListOptions) ([]interface{}, *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 37e926bf..452c28a2 100644
--- a/do/mocks/DatabasesService.go
+++ b/do/mocks/DatabasesService.go
@@ -647,6 +647,48 @@ func (mr *MockDatabasesServiceMockRecorder) UpdateMaintenance(arg0, arg1 any) *g
 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateMaintenance", reflect.TypeOf((*MockDatabasesService)(nil).UpdateMaintenance), arg0, arg1)
 }
 
+// UpdateMySQLConfiguration mocks base method.
+func (m *MockDatabasesService) UpdateMySQLConfiguration(databaseID, confString string) error {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "UpdateMySQLConfiguration", databaseID, confString)
+	ret0, _ := ret[0].(error)
+	return ret0
+}
+
+// UpdateMySQLConfiguration indicates an expected call of UpdateMySQLConfiguration.
+func (mr *MockDatabasesServiceMockRecorder) UpdateMySQLConfiguration(databaseID, confString any) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateMySQLConfiguration", reflect.TypeOf((*MockDatabasesService)(nil).UpdateMySQLConfiguration), databaseID, confString)
+}
+
+// UpdatePostgreSQLConfiguration mocks base method.
+func (m *MockDatabasesService) UpdatePostgreSQLConfiguration(databaseID, confString string) error {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "UpdatePostgreSQLConfiguration", databaseID, confString)
+	ret0, _ := ret[0].(error)
+	return ret0
+}
+
+// UpdatePostgreSQLConfiguration indicates an expected call of UpdatePostgreSQLConfiguration.
+func (mr *MockDatabasesServiceMockRecorder) UpdatePostgreSQLConfiguration(databaseID, confString any) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdatePostgreSQLConfiguration", reflect.TypeOf((*MockDatabasesService)(nil).UpdatePostgreSQLConfiguration), databaseID, confString)
+}
+
+// UpdateRedisConfiguration mocks base method.
+func (m *MockDatabasesService) UpdateRedisConfiguration(databaseID, confString string) error {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "UpdateRedisConfiguration", databaseID, confString)
+	ret0, _ := ret[0].(error)
+	return ret0
+}
+
+// UpdateRedisConfiguration indicates an expected call of UpdateRedisConfiguration.
+func (mr *MockDatabasesServiceMockRecorder) UpdateRedisConfiguration(databaseID, confString any) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateRedisConfiguration", reflect.TypeOf((*MockDatabasesService)(nil).UpdateRedisConfiguration), databaseID, confString)
+}
+
 // UpdateTopic mocks base method.
 func (m *MockDatabasesService) UpdateTopic(arg0, arg1 string, arg2 *godo.DatabaseUpdateTopicRequest) error {
 	m.ctrl.T.Helper()


Test patch:
diff --git a/commands/databases_test.go b/commands/databases_test.go
index 3952f699..7404a880 100644
--- a/commands/databases_test.go
+++ b/commands/databases_test.go
@@ -334,7 +334,7 @@ func TestDatabaseOptionsCommand(t *testing.T) {
 func TestDatabaseConfigurationCommand(t *testing.T) {
 	cmd := databaseConfiguration()
 	assert.NotNil(t, cmd)
-	assertCommandNames(t, cmd, "get")
+	assertCommandNames(t, cmd, "get", "update")
 }
 
 func TestDatabaseKafkaTopicCommand(t *testing.T) {
diff --git a/integration/database_config_get_test.go b/integration/database_config_get_test.go
new file mode 100644
index 00000000..298bb291
--- /dev/null
+++ b/integration/database_config_get_test.go
@@ -0,0 +1,261 @@
+package integration
+
+import (
+	"fmt"
+	"net/http"
+	"net/http/httptest"
+	"net/http/httputil"
+	"os/exec"
+	"strings"
+	"testing"
+
+	"github.com/sclevine/spec"
+	"github.com/stretchr/testify/require"
+)
+
+var _ = suite("database/config/get", func(t *testing.T, when spec.G, it spec.S) {
+	var (
+		expect *require.Assertions
+		server *httptest.Server
+	)
+
+	it.Before(func() {
+		expect = require.New(t)
+
+		server = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
+			switch req.URL.Path {
+			case "/v2/databases/mysql-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(databaseConfigMySQLGetResponse))
+			case "/v2/databases/pg-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(databaseConfigPGGetResponse))
+			case "/v2/databases/redis-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(databaseConfigRedisGetResponse))
+			default:
+				dump, err := httputil.DumpRequest(req, true)
+				if err != nil {
+					t.Fatal("failed to dump request")
+				}
+
+				t.Fatalf("received unknown request: %s", dump)
+			}
+		}))
+	})
+
+	when("all required flags are passed", func() {
+		it("gets the mysql database config", func() {
+			cmd := exec.Command(builtBinaryPath,
+				"-t", "some-magic-token",
+				"-u", server.URL,
+				"database",
+				"configuration",
+				"get",
+				"--engine", "mysql",
+				"mysql-database-id",
+			)
+
+			output, err := cmd.CombinedOutput()
+			expect.NoError(err, fmt.Sprintf("received error output: %s", output))
+			expect.Equal(strings.TrimSpace(databaseConfigMySQLGetOutput), strings.TrimSpace(string(output)))
+		})
+	})
+
+	when("all required flags are passed", func() {
+		it("gets the pg database config", func() {
+			cmd := exec.Command(builtBinaryPath,
+				"-t", "some-magic-token",
+				"-u", server.URL,
+				"database",
+				"configuration",
+				"get",
+				"--engine", "pg",
+				"pg-database-id",
+			)
+
+			output, err := cmd.CombinedOutput()
+			expect.NoError(err, fmt.Sprintf("received error output: %s", output))
+			expect.Equal(strings.TrimSpace(databaseConfigPGGetOutput), strings.TrimSpace(string(output)))
+		})
+	})
+
+	when("all required flags are passed", func() {
+		it("gets the redis database config", func() {
+			cmd := exec.Command(builtBinaryPath,
+				"-t", "some-magic-token",
+				"-u", server.URL,
+				"database",
+				"configuration",
+				"get",
+				"--engine", "redis",
+				"redis-database-id",
+			)
+
+			output, err := cmd.CombinedOutput()
+			expect.NoError(err, fmt.Sprintf("received error output: %s", output))
+			expect.Equal(strings.TrimSpace(databaseConfigRedisGetOutput), strings.TrimSpace(string(output)))
+		})
+	})
+})
+
+const (
+	databaseConfigMySQLGetOutput = `
+key                            value
+DefaultTimeZone                UTC
+MaxAllowedPacket               67108864
+SQLMode                        ANSI,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION,NO_ZERO_DATE,NO_ZERO_IN_DATE,STRICT_ALL_TABLES
+SQLRequirePrimaryKey           true
+InnodbFtMinTokenSize           3
+InnodbFtServerStopwordTable    
+InnodbPrintAllDeadlocks        false
+InnodbRollbackOnTimeout        false
+SlowQueryLog                   false
+LongQueryTime                  10
+BackupHour                     18
+BackupMinute                   3
+`
+
+	databaseConfigMySQLGetResponse = `
+{
+	"config": {
+		"default_time_zone": "UTC",
+		"max_allowed_packet": 67108864,
+		"sql_mode": "ANSI,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION,NO_ZERO_DATE,NO_ZERO_IN_DATE,STRICT_ALL_TABLES",
+		"sql_require_primary_key": true,
+		"innodb_change_buffer_max_size": 25,
+		"innodb_flush_neighbors": 1,
+		"innodb_ft_min_token_size": 3,
+		"innodb_ft_server_stopword_table": "",
+		"innodb_print_all_deadlocks": false,
+		"innodb_read_io_threads": 4,
+		"innodb_rollback_on_timeout": false,
+		"innodb_thread_concurrency": 0,
+		"innodb_write_io_threads": 4,
+		"net_buffer_length": 16384,
+		"slow_query_log": false,
+		"long_query_time": 10,
+		"backup_hour": 18,
+		"backup_minute": 3
+	}
+}`
+
+	databaseConfigPGGetOutput = `
+key                                 value
+AutovacuumNaptime                   60
+AutovacuumVacuumThreshold           50
+AutovacuumAnalyzeThreshold          50
+AutovacuumVacuumScaleFactor         0.2
+AutovacuumAnalyzeScaleFactor        0.2
+AutovacuumVacuumCostDelay           20
+AutovacuumVacuumCostLimit           -1
+BGWriterFlushAfter                  512
+BGWriterLRUMaxpages                 100
+BGWriterLRUMultiplier               2
+IdleInTransactionSessionTimeout     0
+JIT                                 true
+LogAutovacuumMinDuration            -1
+LogMinDurationStatement             -1
+MaxPreparedTransactions             0
+MaxParallelWorkers                  8
+MaxParallelWorkersPerGather         2
+TempFileLimit                       -1
+WalSenderTimeout                    60000
+PgBouncer.ServerResetQueryAlways    false
+PgBouncer.MinPoolSize               0
+PgBouncer.ServerIdleTimeout         0
+PgBouncer.AutodbPoolSize            0
+PgBouncer.AutodbMaxDbConnections    0
+PgBouncer.AutodbIdleTimeout         0
+BackupHour                          18
+BackupMinute                        26`
+
+	databaseConfigPGGetResponse = `{
+"config": {
+	"autovacuum_naptime": 60,
+	"autovacuum_vacuum_threshold": 50,
+	"autovacuum_analyze_threshold": 50,
+	"autovacuum_vacuum_scale_factor": 0.2,
+	"autovacuum_analyze_scale_factor": 0.2,
+	"autovacuum_vacuum_cost_delay": 20,
+	"autovacuum_vacuum_cost_limit": -1,
+	"bgwriter_flush_after": 512,
+	"bgwriter_lru_maxpages": 100,
+	"bgwriter_lru_multiplier": 2,
+	"idle_in_transaction_session_timeout": 0,
+	"jit": true,
+	"log_autovacuum_min_duration": -1,
+	"log_min_duration_statement": -1,
+	"max_prepared_transactions": 0,
+	"max_parallel_workers": 8,
+	"max_parallel_workers_per_gather": 2,
+	"temp_file_limit": -1,
+	"wal_sender_timeout": 60000,
+	"pgbouncer": {
+	"server_reset_query_always": false,
+	"min_pool_size": 0,
+	"server_idle_timeout": 0,
+	"autodb_pool_size": 0,
+	"autodb_max_db_connections": 0,
+	"autodb_idle_timeout": 0
+	},
+	"backup_hour": 18,
+	"backup_minute": 26,
+	"timescaledb": {},
+	"stat_monitor_enable": false
+}
+}`
+
+	databaseConfigRedisGetOutput = `
+key                          value
+RedisMaxmemoryPolicy         volatile-lru
+RedisLFULogFactor            10
+RedisLFUDecayTime            1
+RedisSSL                     true
+RedisTimeout                 600
+RedisNotifyKeyspaceEvents    
+RedisPersistence             rdb
+RedisACLChannelsDefault      allchannels
+`
+
+	databaseConfigRedisGetResponse = `{
+	"config": {
+	  "redis_maxmemory_policy": "volatile-lru",
+	  "redis_lfu_log_factor": 10,
+	  "redis_lfu_decay_time": 1,
+	  "redis_ssl": true,
+	  "redis_timeout": 600,
+	  "redis_notify_keyspace_events": "",
+	  "redis_persistence": "rdb",
+	  "redis_acl_channels_default": "allchannels"
+	}
+  }
+`
+)
diff --git a/integration/database_config_update_test.go b/integration/database_config_update_test.go
new file mode 100644
index 00000000..83793a49
--- /dev/null
+++ b/integration/database_config_update_test.go
@@ -0,0 +1,155 @@
+package integration
+
+import (
+	"fmt"
+	"io"
+	"net/http"
+	"net/http/httptest"
+	"net/http/httputil"
+	"os/exec"
+	"strings"
+	"testing"
+
+	"github.com/sclevine/spec"
+	"github.com/stretchr/testify/require"
+)
+
+var _ = suite("database/config/get", func(t *testing.T, when spec.G, it spec.S) {
+	var (
+		expect *require.Assertions
+		server *httptest.Server
+	)
+
+	it.Before(func() {
+		expect = require.New(t)
+
+		server = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
+			switch req.URL.Path {
+			case "/v2/databases/mysql-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":{"sql_mode":"ANSI"}}`
+				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)
+			case "/v2/databases/pg-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":{"pgbouncer":{"server_reset_query_always":false}}}`
+				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)
+			case "/v2/databases/redis-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":{"redis_timeout":1200}}`
+				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)
+				if err != nil {
+					t.Fatal("failed to dump request")
+				}
+
+				t.Fatalf("received unknown request: %s", dump)
+			}
+		}))
+	})
+
+	when("all required flags are passed", func() {
+		it("updates the mysql database config", func() {
+			cmd := exec.Command(builtBinaryPath,
+				"-t", "some-magic-token",
+				"-u", server.URL,
+				"database",
+				"configuration",
+				"update",
+				"--engine", "mysql",
+				"mysql-database-id",
+				"--config-json", `{"sql_mode": "ANSI"}`,
+			)
+
+			output, err := cmd.CombinedOutput()
+			expect.NoError(err, fmt.Sprintf("received error output: %s", output))
+			expect.Empty(strings.TrimSpace(string(output)))
+		})
+	})
+
+	when("all required flags are passed", func() {
+		it("updates the pg database config", func() {
+			cmd := exec.Command(builtBinaryPath,
+				"-t", "some-magic-token",
+				"-u", server.URL,
+				"database",
+				"configuration",
+				"update",
+				"--engine", "pg",
+				"pg-database-id",
+				"--config-json", `{"pgbouncer":{"server_reset_query_always": false}}`,
+			)
+
+			output, err := cmd.CombinedOutput()
+			expect.NoError(err, fmt.Sprintf("received error output: %s", output))
+			expect.Empty(strings.TrimSpace(string(output)))
+		})
+	})
+
+	when("all required flags are passed", func() {
+		it("updates the redis database config", func() {
+			cmd := exec.Command(builtBinaryPath,
+				"-t", "some-magic-token",
+				"-u", server.URL,
+				"database",
+				"configuration",
+				"update",
+				"--engine", "redis",
+				"redis-database-id",
+				"--config-json", `{"redis_timeout":1200}`,
+			)
+
+			output, err := cmd.CombinedOutput()
+			expect.NoError(err, fmt.Sprintf("received error output: %s", output))
+			expect.Empty(strings.TrimSpace(string(output)))
+		})
+	})
+})