Repository: digitalocean/doctl
Pull request #1571 (2024-08-28 11:46:37)

Databases: Add OpenSearch index CRUD support
**This PR aims at adding support for OpenSearch index CRUD in DOCTL.** 

#### Command Details

```
The subcommands under `doctl databases indexes` enable the management of indexes for opensearch clusters

Usage:
  doctl databases indexes [command]

Available Commands:
  delete      Deletes a opensearch index by index name
  list        Retrieve a list of topics for a given kafka database

Flags:
  -h, --help   help for indexes

Global Flags:
  -t, --access-token string   API V2 access token
  -u, --api-url string        Override default API endpoint
  -c, --config string         Specify a custom config file (default "/home/user/.config/doctl/config.yaml")
      --context string        Specify a custom authentication context name
      --http-retry-max int    Set maximum number of retries for requests that fail with a 429 or 500-level error (default 5)
      --interactive           Enable interactive behavior. Defaults to true if the terminal supports it (default true)
  -o, --output string         Desired output format [text|json] (default "text")
      --trace                 Show a log of network activity while performing a command
  -v, --verbose               Enable verbose output

  
```
#### Command to List OpenSearch Indexes

```
This command lists the following details for each index in an opensearch cluster:

	- The Name of the index.
	- The Status of the index.
	- The Health of the index.
	- The Number of Shards in the index.
	- The Number of Replicas in the index.
	- The Number of Documents in the index.
	- The Size of the index.

Usage:
  doctl databases indexes list <database-uuid> [flags]

Aliases:
  list, ls

Flags:
      --format Index Name   Columns for output in a comma-separated list. Possible values: Index Name, `Status`, `Health`, `Size`, `Docs`, `Create At`, `Number of Shards`, `Number of Replica`.
  -h, --help                help for list
      --no-header           Return raw data with no headers

Global Flags:
  -t, --access-token string   API V2 access token
  -u, --api-url string        Override default API endpoint
  -c, --config string         Specify a custom config file (default "/home/user/.config/doctl/config.yaml")
      --context string        Specify a custom authentication context name
      --http-retry-max int    Set maximum number of retries for requests that fail with a 429 or 500-level error (default 5)
      --interactive           Enable interactive behavior. Defaults to true if the terminal supports it (default true)
  -o, --output string         Desired output format [text|json] (default "text")
      --trace                 Show a log of network activity while performing a command
  -v, --verbose               Enable verbose output
```

#### Command Usage

`doctl -t $token databases indexes list 70c35b01-6f6d-40e6-92ec-2b1f3cdfd61a`

#### Ouput

```
Index Name                      Status    Health    Size      Docs    Create At               Number of Shards    Number of Replica
index-test-tiger-2024-08-21     open      green     10694     1       2024-08-21T14:11:07Z    1                   0
index-test-tiger-2024-08-22     open      green     10694     1       2024-08-22T14:11:04Z    1                   0
index-test-tiger-2024-08-24     open      green     10710     1       2024-08-24T14:11:11Z    1                   0
index-test-tiger-2024-08-25     open      green     10710     1       2024-08-25T14:11:10Z    1                   0
index-test-tiger-2024-08-26     open      green     10710     1       2024-08-26T14:11:05Z    1                   0

```

#### Command to Delete OpenSearch Index

```
Deletes an opensearch index by index name

Usage:
  doctl databases indexes delete <database-uuid> <index-name> [flags]

Aliases:
  delete, rm

Flags:
  -f, --force   Deletes the opensearch index without a confirmation prompt
  -h, --help    help for delete

Global Flags:
  -t, --access-token string   API V2 access token
  -u, --api-url string        Override default API endpoint
  -c, --config string         Specify a custom config file (default "/home/user/.config/doctl/config.yaml")
      --context string        Specify a custom authentication context name
      --http-retry-max int    Set maximum number of retries for requests that fail with a 429 or 500-level error (default 5)
      --interactive           Enable interactive behavior. Defaults to true if the terminal supports it (default true)
  -o, --output string         Desired output format [text|json] (default "text")
      --trace                 Show a log of network activity while performing a command
  -v, --verbose               Enable verbose output

```

#### Command Usage

`doctl -t $token databases indexes delete 70c35b01-6f6d-40e6-92ec-2b1f3sdsa61a test_index`



Patch:
diff --git a/Dockerfile b/Dockerfile
index 255b642d..5c862a24 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,5 +1,5 @@
 # Step 1: Build
-FROM golang:1.21-alpine AS build
+FROM golang:1.22-alpine AS build
 
 ARG GOARCH=amd64
 ENV OUT_D /out
diff --git a/commands/databases.go b/commands/databases.go
index 5eba81dc..18ca9a58 100644
--- a/commands/databases.go
+++ b/commands/databases.go
@@ -156,6 +156,7 @@ For PostgreSQL and MySQL clusters, you can also provide a disk size in MiB to sc
 	cmd.AddCommand(databaseConfiguration())
 	cmd.AddCommand(databaseTopic())
 	cmd.AddCommand(databaseEvents())
+	cmd.AddCommand(databaseIndex())
 
 	return cmd
 }
@@ -2548,3 +2549,64 @@ func RunDatabaseEvents(c *CmdConfig) error {
 	item := &displayers.DatabaseEvents{DatabaseEvents: dbEvents}
 	return c.Display(item)
 }
+
+func databaseIndex() *Command {
+	cmd := &Command{
+		Command: &cobra.Command{
+			Use:   "indexes",
+			Short: `Display commands to manage indexes for opensearch clusters`,
+			Long:  `The subcommands under ` + "`" + `doctl databases indexes` + "`" + ` enable the management of indexes for opensearch clusters`,
+		},
+	}
+
+	indexListDetails := `
+This command lists the following details for each index in an opensearch cluster:
+
+	- The Name of the index.
+	- The Status of the index.
+	- The Health of the index.
+	- The Number of Shards in the index.
+	- The Number of Replicas in the index.
+	- The Number of Documents in the index.
+	- The Size of the index.
+	`
+
+	CmdBuilder(cmd, RunDatabaseIndexList, "list <database-uuid>", "Retrieve a list of indexes for a given opensearch cluster", indexListDetails, Writer, displayerType(&displayers.DatabaseOpenSearchIndexes{}), aliasOpt("ls"))
+	cmdDatabaseIndexDelete := CmdBuilder(cmd, RunDatabaseIndexDelete, "delete <database-uuid> <index-name>", "Deletes an opensearch index by index name", "", Writer, aliasOpt("rm"))
+	AddBoolFlag(cmdDatabaseIndexDelete, doctl.ArgForce, doctl.ArgShortForce, false, "Deletes the opensearch index without a confirmation prompt")
+
+	return cmd
+}
+
+func RunDatabaseIndexList(c *CmdConfig) error {
+	if len(c.Args) == 0 {
+		return doctl.NewMissingArgsErr(c.NS)
+	}
+
+	databaseID := c.Args[0]
+	indexes, err := c.Databases().ListIndexes(databaseID)
+	if err != nil {
+		return err
+	}
+	item := &displayers.DatabaseOpenSearchIndexes{DatabaseIndexes: indexes}
+	return c.Display(item)
+}
+
+func RunDatabaseIndexDelete(c *CmdConfig) error {
+	if len(c.Args) < 2 {
+		return doctl.NewMissingArgsErr(c.NS)
+	}
+
+	force, err := c.Doit.GetBool(c.NS, doctl.ArgForce)
+	if err != nil {
+		return err
+	}
+
+	if force || AskForConfirmDelete("opensearch index", 1) == nil {
+		databaseID := c.Args[0]
+		indexName := c.Args[1]
+		return c.Databases().DeleteIndex(databaseID, indexName)
+	}
+
+	return errOperationAborted
+}
diff --git a/commands/displayers/database.go b/commands/displayers/database.go
index 51ccce77..9e349ec4 100644
--- a/commands/displayers/database.go
+++ b/commands/displayers/database.go
@@ -1691,3 +1691,60 @@ func (dr *DatabaseEvents) KV() []map[string]any {
 	}
 	return out
 }
+
+type DatabaseOpenSearchIndexes struct {
+	DatabaseIndexes do.DatabaseIndexes
+}
+
+var _ Displayable = &DatabaseOpenSearchIndexes{}
+
+func (dt *DatabaseOpenSearchIndexes) JSON(out io.Writer) error {
+	return writeJSON(dt.DatabaseIndexes, out)
+}
+
+func (dt *DatabaseOpenSearchIndexes) Cols() []string {
+	return []string{
+		"Index Name",
+		"Status",
+		"Health",
+		"Size",
+		"Docs",
+		"Create At",
+		"Number of Shards",
+		"Number of Replica",
+	}
+}
+
+func (dt *DatabaseOpenSearchIndexes) ColMap() map[string]string {
+
+	return map[string]string{
+		"Index Name":        "Index Name",
+		"Status":            "Status",
+		"Health":            "Health",
+		"Size":              "Size",
+		"Docs":              "Docs",
+		"Create At":         "Create At",
+		"Number of Shards":  "Number of Shards",
+		"Number of Replica": "Number of Replica",
+	}
+}
+
+func (dt *DatabaseOpenSearchIndexes) KV() []map[string]any {
+	out := make([]map[string]any, 0, len(dt.DatabaseIndexes))
+
+	for _, t := range dt.DatabaseIndexes {
+		o := map[string]any{
+			"Index Name":        t.IndexName,
+			"Number of Shards":  t.NumberofShards,
+			"Number of Replica": t.NumberofReplicas,
+			"Status":            t.Status,
+			"Health":            t.Health,
+			"Size":              t.Size,
+			"Docs":              t.Docs,
+			"Create At":         t.CreateTime,
+		}
+		out = append(out, o)
+	}
+
+	return out
+}
diff --git a/do/databases.go b/do/databases.go
index cf5f9ae4..fe61b552 100644
--- a/do/databases.go
+++ b/do/databases.go
@@ -133,6 +133,14 @@ type DatabaseEvent struct {
 // DatabaseEvents is a slice of DatabaseEvent
 type DatabaseEvents []DatabaseEvent
 
+// DatabaseIndexes is a slice of DatabaseIndex
+type DatabaseIndexes []DatabaseIndex
+
+// DatabaseIndex is a wrapper for godo.DatabaseIndex
+type DatabaseIndex struct {
+	*godo.DatabaseIndex
+}
+
 // DatabasesService is an interface for interacting with DigitalOcean's Database API
 type DatabasesService interface {
 	List() (Databases, error)
@@ -194,6 +202,9 @@ type DatabasesService interface {
 	DeleteTopic(string, string) error
 
 	ListDatabaseEvents(string) (DatabaseEvents, error)
+
+	ListIndexes(string) (DatabaseIndexes, error)
+	DeleteIndex(string, string) error
 }
 
 type databasesService struct {
@@ -789,3 +800,37 @@ func (ds *databasesService) ListDatabaseEvents(databaseID string) (DatabaseEvent
 	}
 	return list, nil
 }
+
+func (ds *databasesService) ListIndexes(databaseID string) (DatabaseIndexes, error) {
+	f := func(opt *godo.ListOptions) ([]any, *godo.Response, error) {
+		list, resp, err := ds.client.Databases.ListIndexes(context.TODO(), databaseID, opt)
+		if err != nil {
+			return nil, nil, err
+		}
+
+		si := make([]any, len(list))
+		for i := range list {
+			si[i] = list[i]
+		}
+
+		return si, resp, err
+	}
+
+	si, err := PaginateResp(f)
+	if err != nil {
+		return nil, err
+	}
+
+	list := make(DatabaseIndexes, len(si))
+	for i := range si {
+		t := si[i].(godo.DatabaseIndex)
+		list[i] = DatabaseIndex{DatabaseIndex: &t}
+	}
+	return list, nil
+}
+
+func (ds *databasesService) DeleteIndex(databaseID, indexName string) error {
+	_, err := ds.client.Databases.DeleteIndex(context.TODO(), databaseID, indexName)
+
+	return err
+}
diff --git a/do/mocks/DatabasesService.go b/do/mocks/DatabasesService.go
index 6ebecf28..bb63b7a4 100644
--- a/do/mocks/DatabasesService.go
+++ b/do/mocks/DatabasesService.go
@@ -158,6 +158,20 @@ func (mr *MockDatabasesServiceMockRecorder) DeleteDB(arg0, arg1 any) *gomock.Cal
 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteDB", reflect.TypeOf((*MockDatabasesService)(nil).DeleteDB), arg0, arg1)
 }
 
+// DeleteIndex mocks base method.
+func (m *MockDatabasesService) DeleteIndex(arg0, arg1 string) error {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "DeleteIndex", arg0, arg1)
+	ret0, _ := ret[0].(error)
+	return ret0
+}
+
+// DeleteIndex indicates an expected call of DeleteIndex.
+func (mr *MockDatabasesServiceMockRecorder) DeleteIndex(arg0, arg1 any) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteIndex", reflect.TypeOf((*MockDatabasesService)(nil).DeleteIndex), arg0, arg1)
+}
+
 // DeletePool mocks base method.
 func (m *MockDatabasesService) DeletePool(arg0, arg1 string) error {
 	m.ctrl.T.Helper()
@@ -484,6 +498,21 @@ func (mr *MockDatabasesServiceMockRecorder) ListDatabaseEvents(arg0 any) *gomock
 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListDatabaseEvents", reflect.TypeOf((*MockDatabasesService)(nil).ListDatabaseEvents), arg0)
 }
 
+// ListIndexes mocks base method.
+func (m *MockDatabasesService) ListIndexes(arg0 string) (do.DatabaseIndexes, error) {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "ListIndexes", arg0)
+	ret0, _ := ret[0].(do.DatabaseIndexes)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
+}
+
+// ListIndexes indicates an expected call of ListIndexes.
+func (mr *MockDatabasesServiceMockRecorder) ListIndexes(arg0 any) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListIndexes", reflect.TypeOf((*MockDatabasesService)(nil).ListIndexes), arg0)
+}
+
 // ListOptions mocks base method.
 func (m *MockDatabasesService) ListOptions() (*do.DatabaseOptions, error) {
 	m.ctrl.T.Helper()
diff --git a/go.mod b/go.mod
index 79e3289b..b1895f4b 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.118.0
+	github.com/digitalocean/godo v1.121.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
@@ -30,7 +30,7 @@ require (
 	github.com/stretchr/testify v1.8.4
 	golang.org/x/crypto v0.22.0
 	golang.org/x/net v0.24.0 // indirect
-	golang.org/x/oauth2 v0.19.0
+	golang.org/x/oauth2 v0.22.0
 	golang.org/x/sys v0.20.0 // indirect
 	gopkg.in/yaml.v2 v2.4.0
 	k8s.io/api v0.26.2
@@ -121,7 +121,7 @@ require (
 	go.opentelemetry.io/otel/trace v1.19.0 // indirect
 	golang.org/x/mod v0.11.0 // indirect
 	golang.org/x/text v0.14.0 // indirect
-	golang.org/x/time v0.5.0 // indirect
+	golang.org/x/time v0.6.0 // indirect
 	golang.org/x/tools v0.10.0 // indirect
 	gopkg.in/inf.v0 v0.9.1 // indirect
 	gopkg.in/ini.v1 v1.66.4 // indirect
diff --git a/go.sum b/go.sum
index b5b0aa4a..978041c9 100644
--- a/go.sum
+++ b/go.sum
@@ -91,8 +91,8 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
 github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
-github.com/digitalocean/godo v1.118.0 h1:lkzGFQmACrVCp7UqH1sAi4JK/PWwlc5aaxubgorKmC4=
-github.com/digitalocean/godo v1.118.0/go.mod h1:Vk0vpCot2HOAJwc5WE8wljZGtJ3ZtWIc8MQ8rF38sdo=
+github.com/digitalocean/godo v1.121.0 h1:ilXiHuEnhbJs2fmFEPX0r/QQ6KfiOIMAhJN3f8NiCfI=
+github.com/digitalocean/godo v1.121.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=
@@ -517,8 +517,8 @@ golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ
 golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
 golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
 golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
-golang.org/x/oauth2 v0.19.0 h1:9+E/EZBCbTLNrbN35fHv/a/d/mOBatymz1zbtQrXpIg=
-golang.org/x/oauth2 v0.19.0/go.mod h1:vYi7skDa1x015PmRRYZ7+s1cWyPgrPiSYRe4rnsexc8=
+golang.org/x/oauth2 v0.22.0 h1:BzDx2FehcG7jJwgWLELCdmLuxk2i+x9UDpSiss2u0ZA=
+golang.org/x/oauth2 v0.22.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
 golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -599,8 +599,8 @@ golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
 golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
 golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
 golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
-golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
-golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
+golang.org/x/time v0.6.0 h1:eTDhh4ZXt5Qf0augr54TN6suAUudPcawVZeIAPU7D4U=
+golang.org/x/time v0.6.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
 golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
 golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
 golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
diff --git a/vendor/github.com/digitalocean/godo/CHANGELOG.md b/vendor/github.com/digitalocean/godo/CHANGELOG.md
index 331dadb9..7203f1ed 100644
--- a/vendor/github.com/digitalocean/godo/CHANGELOG.md
+++ b/vendor/github.com/digitalocean/godo/CHANGELOG.md
@@ -1,5 +1,23 @@
 # Change Log
 
+## [v1.121.0] - 2024-08-20
+
+- #715 - @danaelhe - Databases: Bring back Logsink Support
+- #710 - @bhardwajRahul - Update GODO to include new Openseach index crud changes
+- #712 - @danaelhe - Database: Namespace logsink
+- #711 - @danaelhe - Databases: Add Logsinks CRUD support
+
+## [v1.120.0] - 2024-08-08
+
+- #708 - @markusthoemmes - APPS-9201 Add `UpdateAllSourceVersions` parameter to update app calls
+- #706 - @andrewsomething - database: Add Size to DatabaseReplica struct
+
+## [v1.119.0] - 2024-07-24
+
+- #704 - @ElanHasson - APPS-9133 - Add support for OPENSEARCH as a database engine option
+- #703 - @dependabot[bot] - Bump github.com/hashicorp/go-retryablehttp from 0.7.4 to 0.7.7
+- #699 - @ElanHasson - APPS-8790 Add support to App Platform Log Forwarding for an OpenSearch DBaaS cluster destination.
+
 ## [v1.118.0] - 2024-06-04
 
 **Note**: This release contains features in closed beta (#700).
diff --git a/vendor/github.com/digitalocean/godo/apps.gen.go b/vendor/github.com/digitalocean/godo/apps.gen.go
index f5be2992..22970540 100644
--- a/vendor/github.com/digitalocean/godo/apps.gen.go
+++ b/vendor/github.com/digitalocean/godo/apps.gen.go
@@ -193,7 +193,7 @@ type AppBuildConfigCNBVersioning struct {
 
 // AppDatabaseSpec struct for AppDatabaseSpec
 type AppDatabaseSpec struct {
-	// The name. Must be unique across all components within the same app.
+	// The database's name. The name must be unique across all components within the same app and cannot use capital letters.
 	Name    string                `json:"name"`
 	Engine  AppDatabaseSpecEngine `json:"engine,omitempty"`
 	Version string                `json:"version,omitempty"`
@@ -216,12 +216,13 @@ type AppDatabaseSpecEngine string
 
 // List of AppDatabaseSpecEngine
 const (
-	AppDatabaseSpecEngine_Unset   AppDatabaseSpecEngine = "UNSET"
-	AppDatabaseSpecEngine_MySQL   AppDatabaseSpecEngine = "MYSQL"
-	AppDatabaseSpecEngine_PG      AppDatabaseSpecEngine = "PG"
-	AppDatabaseSpecEngine_Redis   AppDatabaseSpecEngine = "REDIS"
-	AppDatabaseSpecEngine_MongoDB AppDatabaseSpecEngine = "MONGODB"
-	AppDatabaseSpecEngine_Kafka   AppDatabaseSpecEngine = "KAFKA"
+	AppDatabaseSpecEngine_Unset      AppDatabaseSpecEngine = "UNSET"
+	AppDatabaseSpecEngine_MySQL      AppDatabaseSpecEngine = "MYSQL"
+	AppDatabaseSpecEngine_PG         AppDatabaseSpecEngine = "PG"
+	AppDatabaseSpecEngine_Redis      AppDatabaseSpecEngine = "REDIS"
+	AppDatabaseSpecEngine_MongoDB    AppDatabaseSpecEngine = "MONGODB"
+	AppDatabaseSpecEngine_Kafka      AppDatabaseSpecEngine = "KAFKA"
+	AppDatabaseSpecEngine_Opensearch AppDatabaseSpecEngine = "OPENSEARCH"
 )
 
 // AppDedicatedIp Represents a dedicated egress ip.
@@ -446,11 +447,13 @@ type AppLogDestinationSpecLogtail struct {
 
 // AppLogDestinationSpecOpenSearch OpenSearch configuration.
 type AppLogDestinationSpecOpenSearch struct {
-	// OpenSearch API Endpoint. Only HTTPS is supported. Format: https://<host>:<port>.
-	Endpoint  string               `json:"endpoint"`
+	// OpenSearch API Endpoint. Only HTTPS is supported. Format: https://<host>:<port>. Cannot be specified if `cluster_name` is also specified.
+	Endpoint  string               `json:"endpoint,omitempty"`
 	BasicAuth *OpenSearchBasicAuth `json:"basic_auth,omitempty"`
 	// The index name to use for the logs. If not set, the default index name is \"logs\".
 	IndexName string `json:"index_name,omitempty"`
+	// The name of a DigitalOcean DBaaS OpenSearch cluster to use as a log forwarding destination. Cannot be specified if `endpoint` is also specified.
+	ClusterName string `json:"cluster_name,omitempty"`
 }
 
 // AppLogDestinationSpecPapertrail Papertrail configuration.
@@ -1156,7 +1159,8 @@ type AppInstanceSize struct {
 	// (Deprecated) The slug of the corresponding downgradable instance size on the lower tier.
 	TierDowngradeTo string `json:"tier_downgrade_to,omitempty"`
 	// Indicates if the tier instance size can enable autoscaling.
-	Scalable       bool `json:"scalable,omitempty"`
+	Scalable bool `json:"scalable,omitempty"`
+	// (Deprecated) Indicates if the tier instance size is in feature preview state.
 	FeaturePreview bool `json:"feature_preview,omitempty"`
 	// Indicates if the tier instance size allows more than one instance.
 	SingleInstanceOnly bool `json:"single_instance_only,omitempty"`
@@ -1184,10 +1188,10 @@ type ListBuildpacksResponse struct {
 
 // OpenSearchBasicAuth Configure Username and/or Password for Basic authentication.
 type OpenSearchBasicAuth struct {
-	// Username to authenticate with.
-	User string `json:"user"`
-	// Password for user defined in User.
-	Password string `json:"password"`
+	// Username to authenticate with. Only required when `endpoint` is set. Defaults to `doadmin` when `cluster_name` is set.
+	User string `json:"user,omitempty"`
+	// Password for user defined in User. Is required when `endpoint` is set. Cannot be set if using a DigitalOcean DBaaS OpenSearch cluster.
+	Password string `json:"password,omitempty"`
 }
 
 // AppProposeRequest struct for AppProposeRequest
diff --git a/vendor/github.com/digitalocean/godo/apps.go b/vendor/github.com/digitalocean/godo/apps.go
index cd72f740..ebf341c0 100644
--- a/vendor/github.com/digitalocean/godo/apps.go
+++ b/vendor/github.com/digitalocean/godo/apps.go
@@ -80,6 +80,8 @@ type AppLogs struct {
 // AppUpdateRequest represents a request to update an app.
 type AppUpdateRequest struct {
 	Spec *AppSpec `json:"spec"`
+	// Whether or not to update the source versions (for example fetching a new commit or image digest) of all components. By default (when this is false) only newly added sources will be updated to avoid changes like updating the scale of a component from also updating the respective code.
+	UpdateAllSourceVersions bool `json:"update_all_source_versions"`
 }
 
 // DeploymentCreateRequest represents a request to create a deployment.
diff --git a/vendor/github.com/digitalocean/godo/apps_accessors.go b/vendor/github.com/digitalocean/godo/apps_accessors.go
index 0bbba2dd..05c5ac4b 100644
--- a/vendor/github.com/digitalocean/godo/apps_accessors.go
+++ b/vendor/github.com/digitalocean/godo/apps_accessors.go
@@ -1389,6 +1389,14 @@ func (a *AppLogDestinationSpecOpenSearch) GetBasicAuth() *OpenSearchBasicAuth {
 	return a.BasicAuth
 }
 
+// GetClusterName returns the ClusterName field.
+func (a *AppLogDestinationSpecOpenSearch) GetClusterName() string {
+	if a == nil {
+		return ""
+	}
+	return a.ClusterName
+}
+
 // GetEndpoint returns the Endpoint field.
 func (a *AppLogDestinationSpecOpenSearch) GetEndpoint() string {
 	if a == nil {
diff --git a/vendor/github.com/digitalocean/godo/databases.go b/vendor/github.com/digitalocean/godo/databases.go
index b915391c..8c406551 100644
--- a/vendor/github.com/digitalocean/godo/databases.go
+++ b/vendor/github.com/digitalocean/godo/databases.go
@@ -36,6 +36,10 @@ const (
 	databaseTopicsPath                  = databaseBasePath + "/%s/topics"
 	databaseMetricsCredentialsPath      = databaseBasePath + "/metrics/credentials"
 	databaseEvents                      = databaseBasePath + "/%s/events"
+	databaseIndexesPath                 = databaseBasePath + "/%s/indexes"
+	databaseIndexPath                   = databaseBasePath + "/%s/indexes/%s"
+	databaseLogsinkPath                 = databaseBasePath + "/%s/logsink/%s"
+	databaseLogsinksPath                = databaseBasePath + "/%s/logsink"
 )
 
 // SQL Mode constants allow for MySQL-specific SQL flavor configuration.
@@ -159,6 +163,13 @@ type DatabasesService interface {
 	GetMetricsCredentials(context.Context) (*DatabaseMetricsCredentials, *Response, error)
 	UpdateMetricsCredentials(context.Context, *DatabaseUpdateMetricsCredentialsRequest) (*Response, error)
 	ListDatabaseEvents(context.Context, string, *ListOptions) ([]DatabaseEvent, *Response, error)
+	ListIndexes(context.Context, string, *ListOptions) ([]DatabaseIndex, *Response, error)
+	DeleteIndex(context.Context, string, string) (*Response, error)
+	CreateLogsink(ctx context.Context, databaseID string, createLogsink *DatabaseCreateLogsinkRequest) (*DatabaseLogsink, *Response, error)
+	GetLogsink(ctx context.Context, databaseID string, logsinkID string) (*DatabaseLogsink, *Response, error)
+	ListLogsinks(ctx context.Context, databaseID string, opts *ListOptions) ([]DatabaseLogsink, *Response, error)
+	UpdateLogsink(ctx context.Context, databaseID string, logsinkID string, updateLogsink *DatabaseUpdateLogsinkRequest) (*Response, error)
+	DeleteLogsink(ctx context.Context, databaseID, logsinkID string) (*Response, error)
 }
 
 // DatabasesServiceOp handles communication with the Databases related methods
@@ -323,6 +334,14 @@ type DatabaseTopic struct {
 	Config            *TopicConfig      `json:"config,omitempty"`
 }
 
+// DatabaseLogsink represents a logsink
+type DatabaseLogsink struct {
+	ID     string                 `json:"sink_id"`
+	Name   string                 `json:"sink_name,omitempty"`
+	Type   string                 `json:"sink_type,omitempty"`
+	Config *DatabaseLogsinkConfig `json:"config,omitempty"`
+}
+
 // TopicPartition represents the state of a Kafka topic partition
 type TopicPartition struct {
 	EarliestOffset uint64                `json:"earliest_offset,omitempty"`
@@ -392,6 +411,7 @@ type DatabaseReplica struct {
 	PrivateNetworkUUID string              `json:"private_network_uuid,omitempty"`
 	Tags               []string            `json:"tags,omitempty"`
 	StorageSizeMib     uint64              `json:"storage_size_mib,omitempty"`
+	Size               string              `json:"size"`
 }
 
 // DatabasePool represents a database connection pool
@@ -471,6 +491,35 @@ type DatabaseFirewallRule struct {
 	CreatedAt   time.Time `json:"created_at"`
 }
 
+// DatabaseCreateLogsinkRequest is used to create logsink for a database cluster
+type DatabaseCreateLogsinkRequest struct {
+	Name   string                 `json:"sink_name"`
+	Type   string                 `json:"sink_type"`
+	Config *DatabaseLogsinkConfig `json:"config"`
+}
+
+// DatabaseUpdateLogsinkRequest is used to update logsink for a database cluster
+type DatabaseUpdateLogsinkRequest struct {
+	Config *DatabaseLogsinkConfig `json:"config"`
+}
+
+// DatabaseLogsinkConfig represents one of the configurable options (rsyslog_logsink, elasticsearch_logsink, or opensearch_logsink) for a logsink.
+type DatabaseLogsinkConfig struct {
+	URL          string `json:"url,omitempty"`
+	IndexPrefix  string `json:"index_prefix,omitempty"`
+	IndexDaysMax string `json:"index_days_max,omitempty"`
+	Timeout      string `json:"timeout,omitempty"`
+	Server       string `json:"server,omitempty"`
+	Port         int    `json:"port,omitempty"`
+	TLS          bool   `json:"tls,omitempty"`
+	Format       string `json:"format,omitempty"`
+	Logline      string `json:"logline,omitempty"`
+	SD           string `json:"sd,omitempty"`
+	CA           string `json:"ca,omitempty"`
+	Key          string `json:"key,omitempty"`
+	Cert         string `json:"cert,omitempty"`
+}
+
 // PostgreSQLConfig holds advanced configurations for PostgreSQL database clusters.
 type PostgreSQLConfig struct {
 	AutovacuumFreezeMaxAge          *int                         `json:"autovacuum_freeze_max_age,omitempty"`
@@ -679,6 +728,10 @@ type databaseTopicsRoot struct {
 	Topics []DatabaseTopic `json:"topics"`
 }
 
+type databaseLogsinksRoot struct {
+	Sinks []DatabaseLogsink `json:"sinks"`
+}
+
 type databaseMetricsCredentialsRoot struct {
 	Credentials *DatabaseMetricsCredentials `json:"credentials"`
 }
@@ -732,6 +785,28 @@ type ListDatabaseEventsRoot struct {
 	Events []DatabaseEvent `json:"events"`
 }
 
+type DatabaseIndex struct {
+	IndexName        string            `json:"index_name"`
+	NumberofShards   uint64            `json:"number_of_shards"`
+	NumberofReplicas uint64            `json:"number_of_replicas"`
+	Size             int64             `json:"size,omitempty"`
+	Health           string            `json:"health,omitempty"`
+	Status           string            `json:"status,omitempty"`
+	Docs             int64             `json:"docs,omitempty"`
+	CreateTime       string            `json:"create_time"`
+	Replication      *IndexReplication `json:"replication,omitempty"`
+}
+
+type IndexReplication struct {
+	LeaderIndex   string `json:"leader_index,omitempty"`
+	LeaderProject string `json:"leader_project,omitempty"`
+	LeaderService string `json:"leader_service,omitempty"`
+}
+
+type databaseIndexesRoot struct {
+	Indexes []DatabaseIndex `json:"indexes"`
+}
+
 // URN returns a URN identifier for the database
 func (d Database) URN() string {
 	return ToURN("dbaas", d.ID)
@@ -1559,3 +1634,116 @@ func (svc *DatabasesServiceOp) ListDatabaseEvents(ctx context.Context, databaseI
 
 	return root.Events, resp, nil
 }
+
+// ListIndexes returns all indexes for a given opensearch cluster
+func (svc *DatabasesServiceOp) ListIndexes(ctx context.Context, databaseID string, opts *ListOptions) ([]DatabaseIndex, *Response, error) {
+	path := fmt.Sprintf(databaseIndexesPath, databaseID)
+	path, err := addOptions(path, opts)
+	if err != nil {
+		return nil, nil, err
+	}
+	req, err := svc.client.NewRequest(ctx, http.MethodGet, path, nil)
+	if err != nil {
+		return nil, nil, err
+	}
+	root := new(databaseIndexesRoot)
+	resp, err := svc.client.Do(ctx, req, root)
+	if err != nil {
+		return nil, resp, err
+	}
+	return root.Indexes, resp, nil
+}
+
+// DeleteIndex will delete an existing opensearch index
+func (svc *DatabasesServiceOp) DeleteIndex(ctx context.Context, databaseID, name string) (*Response, error) {
+	path := fmt.Sprintf(databaseIndexPath, databaseID, name)
+	req, err := svc.client.NewRequest(ctx, http.MethodDelete, 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
+}
+
+// CreateLogsink creates a new logsink for a database
+func (svc *DatabasesServiceOp) CreateLogsink(ctx context.Context, databaseID string, createLogsink *DatabaseCreateLogsinkRequest) (*DatabaseLogsink, *Response, error) {
+	path := fmt.Sprintf(databaseLogsinksPath, databaseID)
+	req, err := svc.client.NewRequest(ctx, http.MethodPost, path, createLogsink)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	root := new(DatabaseLogsink)
+	resp, err := svc.client.Do(ctx, req, root)
+	if err != nil {
+		return nil, resp, err
+	}
+	return root, resp, nil
+}
+
+// GetLogsink gets a logsink for a database
+func (svc *DatabasesServiceOp) GetLogsink(ctx context.Context, databaseID string, logsinkID string) (*DatabaseLogsink, *Response, error) {
+	path := fmt.Sprintf(databaseLogsinkPath, databaseID, logsinkID)
+	req, err := svc.client.NewRequest(ctx, http.MethodGet, path, nil)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	root := new(DatabaseLogsink)
+	resp, err := svc.client.Do(ctx, req, root)
+	if err != nil {
+		return nil, resp, err
+	}
+	return root, resp, nil
+}
+
+// ListTopics returns all topics for a given kafka cluster
+func (svc *DatabasesServiceOp) ListLogsinks(ctx context.Context, databaseID string, opts *ListOptions) ([]DatabaseLogsink, *Response, error) {
+	path := fmt.Sprintf(databaseLogsinksPath, databaseID)
+	path, err := addOptions(path, opts)
+	if err != nil {
+		return nil, nil, err
+	}
+	req, err := svc.client.NewRequest(ctx, http.MethodGet, path, nil)
+	if err != nil {
+		return nil, nil, err
+	}
+	root := new(databaseLogsinksRoot)
+	resp, err := svc.client.Do(ctx, req, root)
+	if err != nil {
+		return nil, resp, err
+	}
+	return root.Sinks, resp, nil
+}
+
+// UpdateLogsink updates a logsink for a database cluster
+func (svc *DatabasesServiceOp) UpdateLogsink(ctx context.Context, databaseID string, logsinkID string, updateLogsink *DatabaseUpdateLogsinkRequest) (*Response, error) {
+	path := fmt.Sprintf(databaseLogsinkPath, databaseID, logsinkID)
+	req, err := svc.client.NewRequest(ctx, http.MethodPut, path, updateLogsink)
+	if err != nil {
+		return nil, err
+	}
+
+	resp, err := svc.client.Do(ctx, req, nil)
+	if err != nil {
+		return resp, err
+	}
+	return resp, nil
+}
+
+// DeleteLogsink deletes a logsink for a database cluster
+func (svc *DatabasesServiceOp) DeleteLogsink(ctx context.Context, databaseID, logsinkID string) (*Response, error) {
+	path := fmt.Sprintf(databaseLogsinkPath, databaseID, logsinkID)
+	req, err := svc.client.NewRequest(ctx, http.MethodDelete, 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
+}
diff --git a/vendor/github.com/digitalocean/godo/godo.go b/vendor/github.com/digitalocean/godo/godo.go
index 6d69ece7..9362e118 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.118.0"
+	libraryVersion = "1.121.0"
 	defaultBaseURL = "https://api.digitalocean.com/"
 	userAgent      = "godo/" + libraryVersion
 	mediaType      = "application/json"
diff --git a/vendor/golang.org/x/oauth2/LICENSE b/vendor/golang.org/x/oauth2/LICENSE
index 6a66aea5..2a7cf70d 100644
--- a/vendor/golang.org/x/oauth2/LICENSE
+++ b/vendor/golang.org/x/oauth2/LICENSE
@@ -1,4 +1,4 @@
-Copyright (c) 2009 The Go Authors. All rights reserved.
+Copyright 2009 The Go Authors.
 
 Redistribution and use in source and binary forms, with or without
 modification, are permitted provided that the following conditions are
@@ -10,7 +10,7 @@ notice, this list of conditions and the following disclaimer.
 copyright notice, this list of conditions and the following disclaimer
 in the documentation and/or other materials provided with the
 distribution.
-   * Neither the name of Google Inc. nor the names of its
+   * Neither the name of Google LLC nor the names of its
 contributors may be used to endorse or promote products derived from
 this software without specific prior written permission.
 
diff --git a/vendor/golang.org/x/oauth2/oauth2.go b/vendor/golang.org/x/oauth2/oauth2.go
index 90a2c3d6..09f6a49b 100644
--- a/vendor/golang.org/x/oauth2/oauth2.go
+++ b/vendor/golang.org/x/oauth2/oauth2.go
@@ -393,7 +393,7 @@ func ReuseTokenSource(t *Token, src TokenSource) TokenSource {
 	}
 }
 
-// ReuseTokenSource returns a TokenSource that acts in the same manner as the
+// ReuseTokenSourceWithExpiry returns a TokenSource that acts in the same manner as the
 // TokenSource returned by ReuseTokenSource, except the expiry buffer is
 // configurable. The expiration time of a token is calculated as
 // t.Expiry.Add(-earlyExpiry).
diff --git a/vendor/golang.org/x/time/LICENSE b/vendor/golang.org/x/time/LICENSE
index 6a66aea5..2a7cf70d 100644
--- a/vendor/golang.org/x/time/LICENSE
+++ b/vendor/golang.org/x/time/LICENSE
@@ -1,4 +1,4 @@
-Copyright (c) 2009 The Go Authors. All rights reserved.
+Copyright 2009 The Go Authors.
 
 Redistribution and use in source and binary forms, with or without
 modification, are permitted provided that the following conditions are
@@ -10,7 +10,7 @@ notice, this list of conditions and the following disclaimer.
 copyright notice, this list of conditions and the following disclaimer
 in the documentation and/or other materials provided with the
 distribution.
-   * Neither the name of Google Inc. nor the names of its
+   * Neither the name of Google LLC nor the names of its
 contributors may be used to endorse or promote products derived from
 this software without specific prior written permission.
 
diff --git a/vendor/modules.txt b/vendor/modules.txt
index 2a3908e4..c1aa5716 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.118.0
+# github.com/digitalocean/godo v1.121.0
 ## explicit; go 1.20
 github.com/digitalocean/godo
 github.com/digitalocean/godo/metrics
@@ -439,7 +439,7 @@ golang.org/x/net/http2/hpack
 golang.org/x/net/idna
 golang.org/x/net/internal/socks
 golang.org/x/net/proxy
-# golang.org/x/oauth2 v0.19.0
+# golang.org/x/oauth2 v0.22.0
 ## explicit; go 1.18
 golang.org/x/oauth2
 golang.org/x/oauth2/internal
@@ -459,7 +459,7 @@ golang.org/x/text/secure/bidirule
 golang.org/x/text/transform
 golang.org/x/text/unicode/bidi
 golang.org/x/text/unicode/norm
-# golang.org/x/time v0.5.0
+# golang.org/x/time v0.6.0
 ## explicit; go 1.18
 golang.org/x/time/rate
 # golang.org/x/tools v0.10.0


Test patch:
diff --git a/commands/databases_test.go b/commands/databases_test.go
index ebe39ffc..5a2c92af 100644
--- a/commands/databases_test.go
+++ b/commands/databases_test.go
@@ -262,6 +262,7 @@ func TestDatabasesCommand(t *testing.T) {
 		"sql-mode",
 		"configuration",
 		"topics",
+		"indexes",
 	)
 }
 
diff --git a/integration/database_opensearch_index_delete_test.go b/integration/database_opensearch_index_delete_test.go
new file mode 100644
index 00000000..66f67554
--- /dev/null
+++ b/integration/database_opensearch_index_delete_test.go
@@ -0,0 +1,69 @@
+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/index/delete", 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/some-database-id/indexes/some-index-id":
+				auth := req.Header.Get("Authorization")
+				if auth != "Bearer some-magic-token" {
+					w.WriteHeader(http.StatusTeapot)
+				}
+
+				if req.Method != http.MethodDelete {
+					w.WriteHeader(http.StatusMethodNotAllowed)
+					return
+				}
+
+				w.WriteHeader(http.StatusNoContent)
+			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("deletes the database", func() {
+			cmd := exec.Command(builtBinaryPath,
+				"-t", "some-magic-token",
+				"-u", server.URL,
+				"database",
+				"indexes",
+				"delete",
+				"some-database-id",
+				"some-index-id",
+				"-f",
+			)
+
+			output, err := cmd.CombinedOutput()
+			expect.NoError(err, fmt.Sprintf("received error output: %s", output))
+			expect.Equal(strings.TrimSpace(""), strings.TrimSpace(string(output)))
+			expect.Empty(output)
+		})
+	})
+})
diff --git a/integration/database_opensearch_index_list_test.go b/integration/database_opensearch_index_list_test.go
new file mode 100644
index 00000000..8639b491
--- /dev/null
+++ b/integration/database_opensearch_index_list_test.go
@@ -0,0 +1,88 @@
+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/index/list", 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/some-database-id/indexes":
+				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(databaseOpenSearchIndexListResponse))
+			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("lists indexes for an opensearch cluster", func() {
+			cmd := exec.Command(builtBinaryPath,
+				"-t", "some-magic-token",
+				"-u", server.URL,
+				"database",
+				"indexes",
+				"list",
+				"some-database-id",
+			)
+
+			output, err := cmd.CombinedOutput()
+			expect.NoError(err, fmt.Sprintf("received error output: %s", output))
+			expect.Equal(strings.TrimSpace(databaseOpenSearchIndexListOutput), strings.TrimSpace(string(output)))
+		})
+	})
+})
+
+const (
+	databaseOpenSearchIndexListOutput = `
+Index Name         Status    Health    Size    Docs    Create At               Number of Shards    Number of Replica
+sql-datasources    open      green     624     0       2024-05-24T07:44:45Z    1                   0`
+	databaseOpenSearchIndexListResponse = `
+{
+  "indexes": [
+    {
+		"index_name": "sql-datasources",
+		"status": "open",
+		"health": "green",
+		"size": 624,
+		"docs": 0,
+		"create_time": "2024-05-24T07:44:45Z",
+		"number_of_shards": 1,
+		"number_of_replica": 0
+    }
+  ]
+}
+`
+)