Repository: digitalocean/doctl
Pull request #1579 (2024-09-11 13:48:19)

[DBAAS] | Add API endpoint for applying cluster patches
Add 'doctl databases install_update <database_uuid>' command and unit tests.

loosla: Thanks for your contribution :rocket:  

I believe we are using hyphen instead of underscores (install_update) in commands names. 
Maybe just `update` can be clear enough as a name? 
I'd rely on @andrewsomething as he may share more thoughts about the naming. 
loosla: Looks good to me :+1: 

Patch:
diff --git a/commands/databases.go b/commands/databases.go
index 0cef7112..ee2a0d66 100644
--- a/commands/databases.go
+++ b/commands/databases.go
@@ -425,7 +425,7 @@ func convertUTCtoISO8601(restoreFromTimestamp string) (string, error) {
 	// accepts UTC time format from user (to match db list output) and converts it to ISO8601 for api parity.
 	date, error := time.Parse("2006-01-02 15:04:05 +0000 UTC", restoreFromTimestamp)
 	if error != nil {
-		return "", fmt.Errorf("Invalid format for --restore-from-timestamp. Must be in UTC format: 2006-01-02 15:04:05 +0000 UTC")
+		return "", fmt.Errorf("invalid format for --restore-from-timestamp. Must be in UTC format: 2006-01-02 15:04:05 +0000 UTC")
 	}
 	dateFormatted := date.Format(time.RFC3339)
 
@@ -586,7 +586,9 @@ func databaseMaintenanceWindow() *Command {
 			Short:   "Display commands for scheduling automatic maintenance on your database cluster",
 			Long: `The ` + "`" + `doctl databases maintenance-window` + "`" + ` commands allow you to schedule, and check the schedule of, maintenance windows for your databases.
 
-Maintenance windows are hour-long blocks of time during which DigitalOcean performs automatic maintenance on databases every week. During this time, health checks, security updates, version upgrades, and more are performed.`,
+Maintenance windows are hour-long blocks of time during which DigitalOcean performs automatic maintenance on databases every week. During this time, health checks, security updates, version upgrades, and more are performed.
+
+To install an update outside of a maintenance window, use the ` + "`" + `doctl databases maintenance-window install` + "`" + ` command.`,
 		},
 	}
 
@@ -615,6 +617,9 @@ To see a list of your databases and their IDs, run `+"`"+`doctl databases list`+
 		"The hour when maintenance updates are applied, in UTC 24-hour format. Example: '16:00')", requiredOpt())
 	cmdDatabaseCreate.Example = `The following example updates the maintenance window for a database cluster with the ID ` + "`" + `ca9f591d-f38h-5555-a0ef-1c02d1d1e35` + "`" + `: doctl databases maintenance-window update ca9f591d-f38h-5555-a0ef-1c02d1d1e35 --day tuesday --hour 16:00`
 
+	cmdDatabaseInstallUpdate := CmdBuilder(cmd, RunDatabaseInstallUpdate, "install <database-cluster-id>", "Start installation of updates immediately", "Starts the installation of updates for the specified database cluster immediately outside of a maintenance window.", Writer, aliasOpt("i"))
+	cmdDatabaseInstallUpdate.Example = `The following example starts installation of updates for your databases with the ID ` + "`" + `ca9f591d-f38h-5555-a0ef-1c02d1d1e35` + "`" + `: doctl databases maintenance-window install ca9f591d-f38h-5555-a0ef-1c02d1d1e35`
+
 	return cmd
 }
 
@@ -656,6 +661,15 @@ func RunDatabaseMaintenanceUpdate(c *CmdConfig) error {
 	return c.Databases().UpdateMaintenance(id, r)
 }
 
+// RunDatabaseInstallUpdate starts installation of updates
+func RunDatabaseInstallUpdate(c *CmdConfig) error {
+	if len(c.Args) == 0 {
+		return doctl.NewMissingArgsErr(c.NS)
+	}
+	id := c.Args[0]
+	return c.Databases().InstallUpdate(id)
+}
+
 func buildDatabaseUpdateMaintenanceRequestFromArgs(c *CmdConfig) (*godo.DatabaseUpdateMaintenanceRequest, error) {
 	r := &godo.DatabaseUpdateMaintenanceRequest{}
 
@@ -829,7 +843,7 @@ func buildDatabaseCreateKafkaUserACls(c *CmdConfig) (kafkaACls []*godo.KafkaACL,
 	for _, acl := range acls {
 		pair := strings.SplitN(acl, ":", 2)
 		if len(pair) != 2 {
-			return nil, fmt.Errorf("Unexpected input value [%v], must be a topic:permission pair", pair)
+			return nil, fmt.Errorf("unexpected input value [%v], must be a topic:permission pair", pair)
 		}
 
 		kafkaACl := new(godo.KafkaACL)
diff --git a/do/databases.go b/do/databases.go
index 87792002..d839c41e 100644
--- a/do/databases.go
+++ b/do/databases.go
@@ -163,6 +163,7 @@ type DatabasesService interface {
 
 	GetMaintenance(string) (*DatabaseMaintenanceWindow, error)
 	UpdateMaintenance(string, *godo.DatabaseUpdateMaintenanceRequest) error
+	InstallUpdate(string) error
 
 	GetUser(string, string) (*DatabaseUser, error)
 	ListUsers(string) (DatabaseUsers, error)
@@ -336,6 +337,12 @@ func (ds *databasesService) UpdateMaintenance(databaseID string, req *godo.Datab
 	return err
 }
 
+func (ds *databasesService) InstallUpdate(databaseID string) error {
+	_, err := ds.client.Databases.InstallUpdate(context.TODO(), databaseID)
+
+	return err
+}
+
 func (ds *databasesService) ListBackups(databaseID string) (DatabaseBackups, error) {
 	f := func(opt *godo.ListOptions) ([]any, *godo.Response, error) {
 		list, resp, err := ds.client.Databases.ListBackups(context.TODO(), databaseID, opt)
diff --git a/do/mocks/DatabasesService.go b/do/mocks/DatabasesService.go
index 712f9e38..e14fdbc7 100644
--- a/do/mocks/DatabasesService.go
+++ b/do/mocks/DatabasesService.go
@@ -453,6 +453,20 @@ func (mr *MockDatabasesServiceMockRecorder) GetUser(arg0, arg1 any) *gomock.Call
 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUser", reflect.TypeOf((*MockDatabasesService)(nil).GetUser), arg0, arg1)
 }
 
+// InstallUpdate mocks base method.
+func (m *MockDatabasesService) InstallUpdate(arg0 string) error {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "InstallUpdate", arg0)
+	ret0, _ := ret[0].(error)
+	return ret0
+}
+
+// InstallUpdate indicates an expected call of InstallUpdate.
+func (mr *MockDatabasesServiceMockRecorder) InstallUpdate(arg0 any) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InstallUpdate", reflect.TypeOf((*MockDatabasesService)(nil).InstallUpdate), arg0)
+}
+
 // List mocks base method.
 func (m *MockDatabasesService) List() (do.Databases, error) {
 	m.ctrl.T.Helper()
diff --git a/go.mod b/go.mod
index bc85ef22..ca63dc6b 100644
--- a/go.mod
+++ b/go.mod
@@ -5,7 +5,7 @@ go 1.22
 require (
 	github.com/blang/semver v3.5.1+incompatible
 	github.com/creack/pty v1.1.21
-	github.com/digitalocean/godo v1.123.0
+	github.com/digitalocean/godo v1.124.0
 	github.com/docker/cli v24.0.5+incompatible
 	github.com/docker/docker v25.0.6+incompatible
 	github.com/docker/docker-credential-helpers v0.7.0 // indirect
diff --git a/go.sum b/go.sum
index 4e68d2ab..3243805f 100644
--- a/go.sum
+++ b/go.sum
@@ -93,6 +93,8 @@ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1
 github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/digitalocean/godo v1.123.0 h1:EowFmnVevXIKn9svPDTz0NK4+f+eE3v5easKD9hjc1k=
 github.com/digitalocean/godo v1.123.0/go.mod h1:WQVH83OHUy6gC4gXpEVQKtxTd4L5oCp+5OialidkPLY=
+github.com/digitalocean/godo v1.124.0 h1:qroI1QdtcgnXF/pefq9blZRbXqBw1Ry/aHh2pnu/328=
+github.com/digitalocean/godo v1.124.0/go.mod h1:WQVH83OHUy6gC4gXpEVQKtxTd4L5oCp+5OialidkPLY=
 github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
 github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
 github.com/docker/cli v24.0.5+incompatible h1:WeBimjvS0eKdH4Ygx+ihVq1Q++xg36M/rMi4aXAvodc=
diff --git a/integration/database_create_restore_from_cluster.go b/integration/database_create_restore_from_cluster.go
index 2e358562..f57b55ba 100644
--- a/integration/database_create_restore_from_cluster.go
+++ b/integration/database_create_restore_from_cluster.go
@@ -127,7 +127,7 @@ var _ = suite("database/create/backup-restore", func(t *testing.T, when spec.G,
 })
 
 const (
-	restoreFromTimestampError          = "Error: Invalid format for --restore-from-timestamp. Must be in UTC format: 2006-01-02 15:04:05 +0000 UTC"
+	restoreFromTimestampError          = "Error: invalid format for --restore-from-timestamp. Must be in UTC format: 2006-01-02 15:04:05 +0000 UTC"
 	databasesCreateRestoreBackUpOutput = `
 Notice: Database created
 ID         Name           Engine    Version         Number of Nodes    Region    Status      Size       URI                                                                                     Created At                       Storage (MiB)
diff --git a/vendor/github.com/digitalocean/godo/CHANGELOG.md b/vendor/github.com/digitalocean/godo/CHANGELOG.md
index fabeb100..3112c9ff 100644
--- a/vendor/github.com/digitalocean/godo/CHANGELOG.md
+++ b/vendor/github.com/digitalocean/godo/CHANGELOG.md
@@ -1,5 +1,9 @@
 # Change Log
 
+## [v1.124.0] - 2024-09-10
+
+- #721 - @vsharma6855 - [DBAAS] | Add API endpoint for applying cluster patches
+
 ## [v1.123.0] - 2024-09-06
 
 - #719 - @andrewsomething - apps: mark ListTiers and GetTier as deprecated
diff --git a/vendor/github.com/digitalocean/godo/databases.go b/vendor/github.com/digitalocean/godo/databases.go
index 3b6869cf..8305770c 100644
--- a/vendor/github.com/digitalocean/godo/databases.go
+++ b/vendor/github.com/digitalocean/godo/databases.go
@@ -16,6 +16,7 @@ const (
 	databaseResizePath                  = databaseBasePath + "/%s/resize"
 	databaseMigratePath                 = databaseBasePath + "/%s/migrate"
 	databaseMaintenancePath             = databaseBasePath + "/%s/maintenance"
+	databaseUpdateInstallationPath      = databaseBasePath + "/%s/install_update"
 	databaseBackupsPath                 = databaseBasePath + "/%s/backups"
 	databaseUsersPath                   = databaseBasePath + "/%s/users"
 	databaseUserPath                    = databaseBasePath + "/%s/users/%s"
@@ -120,6 +121,7 @@ type DatabasesService interface {
 	Resize(context.Context, string, *DatabaseResizeRequest) (*Response, error)
 	Migrate(context.Context, string, *DatabaseMigrateRequest) (*Response, error)
 	UpdateMaintenance(context.Context, string, *DatabaseUpdateMaintenanceRequest) (*Response, error)
+	InstallUpdate(context.Context, string) (*Response, error)
 	ListBackups(context.Context, string, *ListOptions) ([]DatabaseBackup, *Response, error)
 	GetUser(context.Context, string, string) (*DatabaseUser, *Response, error)
 	ListUsers(context.Context, string, *ListOptions) ([]DatabaseUser, *Response, error)
@@ -940,6 +942,20 @@ func (svc *DatabasesServiceOp) UpdateMaintenance(ctx context.Context, databaseID
 	return resp, nil
 }
 
+// InstallUpdate starts installation of updates
+func (svc *DatabasesServiceOp) InstallUpdate(ctx context.Context, databaseID string) (*Response, error) {
+	path := fmt.Sprintf(databaseUpdateInstallationPath, databaseID)
+	req, err := svc.client.NewRequest(ctx, http.MethodPut, path, nil)
+	if err != nil {
+		return nil, err
+	}
+	resp, err := svc.client.Do(ctx, req, nil)
+	if err != nil {
+		return resp, err
+	}
+	return resp, nil
+}
+
 // ListBackups returns a list of the current backups of a database
 func (svc *DatabasesServiceOp) ListBackups(ctx context.Context, databaseID string, opts *ListOptions) ([]DatabaseBackup, *Response, error) {
 	path := fmt.Sprintf(databaseBackupsPath, databaseID)
diff --git a/vendor/github.com/digitalocean/godo/godo.go b/vendor/github.com/digitalocean/godo/godo.go
index f28dab88..adad738b 100644
--- a/vendor/github.com/digitalocean/godo/godo.go
+++ b/vendor/github.com/digitalocean/godo/godo.go
@@ -21,7 +21,7 @@ import (
 )
 
 const (
-	libraryVersion = "1.123.0"
+	libraryVersion = "1.124.0"
 	defaultBaseURL = "https://api.digitalocean.com/"
 	userAgent      = "godo/" + libraryVersion
 	mediaType      = "application/json"
diff --git a/vendor/modules.txt b/vendor/modules.txt
index 68a1ed46..5c563c45 100644
--- a/vendor/modules.txt
+++ b/vendor/modules.txt
@@ -61,7 +61,7 @@ github.com/creack/pty
 # github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc
 ## explicit
 github.com/davecgh/go-spew/spew
-# github.com/digitalocean/godo v1.123.0
+# github.com/digitalocean/godo v1.124.0
 ## explicit; go 1.20
 github.com/digitalocean/godo
 github.com/digitalocean/godo/metrics


Test patch:
diff --git a/commands/databases_test.go b/commands/databases_test.go
index dcea7405..9bd7af2c 100644
--- a/commands/databases_test.go
+++ b/commands/databases_test.go
@@ -279,6 +279,7 @@ func TestDatabaseMaintenanceWindowCommand(t *testing.T) {
 	assertCommandNames(t, cmd,
 		"update",
 		"get",
+		"install",
 	)
 }
 
@@ -894,6 +895,26 @@ func TestDatabaseUpdateMaintenance(t *testing.T) {
 	})
 }
 
+func TestDatabaseInstallUpdate(t *testing.T) {
+
+	// Success
+	withTestClient(t, func(config *CmdConfig, tm *tcMocks) {
+		tm.databases.EXPECT().InstallUpdate(testDBCluster.ID).Return(nil)
+		config.Args = append(config.Args, testDBCluster.ID)
+		err := RunDatabaseInstallUpdate(config)
+		assert.NoError(t, err)
+	})
+
+	// Error
+	withTestClient(t, func(config *CmdConfig, tm *tcMocks) {
+		tm.databases.EXPECT().InstallUpdate(testDBCluster.ID).Return(errTest)
+		config.Args = append(config.Args, testDBCluster.ID)
+
+		err := RunDatabaseInstallUpdate(config)
+		assert.EqualError(t, err, errTest.Error())
+	})
+}
+
 func TestDatabasesUserGet(t *testing.T) {
 	// Successful call
 	withTestClient(t, func(config *CmdConfig, tm *tcMocks) {