Repository: digitalocean/doctl
Pull request #1580 (2024-09-11 15:10:47)

databases: add get-ca to retrieve db certificate
This PR allows users to obtain a certificate for connecting to the database.

Related to https://do-internal.atlassian.net/browse/APICLI-2506, https://github.com/digitalocean/doctl/issues/1512



Patch:
diff --git a/commands/databases.go b/commands/databases.go
index 4550de6c..0cef7112 100644
--- a/commands/databases.go
+++ b/commands/databases.go
@@ -70,6 +70,10 @@ func Databases() *Command {
 - The date and time when the database cluster was created`+databaseListDetails, Writer, aliasOpt("g"), displayerType(&displayers.Databases{}))
 	cmdDatabaseGet.Example = `The following example retrieves the details for a database cluster with the ID ` + "`" + `f81d4fae-7dec-11d0-a765-00a0c91e6bf6` + "`" + ` and uses the ` + "`" + `--format` + "`" + ` flag to return only the database's ID, engine, and engine version: doctl databases get f81d4fae-7dec-11d0-a765-00a0c91e6bf6`
 
+	cmdDatabaseGetCA := CmdBuilder(cmd, RunDatabaseGetCA, "get-ca <database-cluster-id>", "Provides the CA certificate for a DigitalOcean database", `Retrieves a database certificate`, Writer, aliasOpt("gc"), displayerType(&displayers.DatabaseCA{}))
+	cmdDatabaseGetCA.Example = `Retrieves the database certificate for the cluster with the ID ` + "`" + `f81d4fae-7dec-11d0-a765-00a0c91e6bf6` + "`" + `: doctl databases get-ca f81d4fae-7dec-11d0-a765-00a0c91e6bf6
+With the ` + "`" + `-o json flag` + "`" + `, the certificate to connect to the database is base64 encoded. To decode it: ` + "`" + `doctl databases get-ca <database-cluster-id> -o json | jq -r .certificate | base64 --decode` + "`"
+
 	nodeSizeDetails := "The size of the nodes in the database cluster, for example `db-s-1vcpu-1gb` indicates a 1 CPU, 1GB node. For a list of available size slugs, visit: https://docs.digitalocean.com/reference/api/api-reference/#tag/Databases"
 	nodeNumberDetails := "The number of nodes in the database cluster. Valid values are 1-3. In addition to the primary node, up to two standby nodes may be added for high availability."
 	storageSizeMiBDetails := "The amount of disk space allocated to the cluster. Applicable for PostgreSQL and MySQL clusters. Each plan size has a default value but can be increased in increments up to a maximum amount. For ranges, visit: https://www.digitalocean.com/pricing/managed-databases"
@@ -188,6 +192,21 @@ func RunDatabaseGet(c *CmdConfig) error {
 	return displayDatabases(c, false, *db)
 }
 
+// RunDatabaseGetCA returns a CA certificate for a database
+func RunDatabaseGetCA(c *CmdConfig) error {
+	if len(c.Args) == 0 {
+		return doctl.NewMissingArgsErr(c.NS)
+	}
+
+	id := c.Args[0]
+	dbCA, err := c.Databases().GetCA(id)
+	if err != nil {
+		return err
+	}
+
+	return displayDatabaseCA(c, dbCA)
+}
+
 // RunDatabaseCreate creates a database cluster
 func RunDatabaseCreate(c *CmdConfig) error {
 	if len(c.Args) == 0 {
@@ -906,6 +925,11 @@ func displayDatabaseUsers(c *CmdConfig, users ...do.DatabaseUser) error {
 	return c.Display(item)
 }
 
+func displayDatabaseCA(c *CmdConfig, dbCA *do.DatabaseCA) error {
+	item := &displayers.DatabaseCA{DatabaseCA: *dbCA}
+	return c.Display(item)
+}
+
 func databaseOptions() *Command {
 	cmd := &Command{
 		Command: &cobra.Command{
diff --git a/commands/displayers/database.go b/commands/displayers/database.go
index 9e349ec4..61df8467 100644
--- a/commands/displayers/database.go
+++ b/commands/displayers/database.go
@@ -155,6 +155,36 @@ func (db *DatabaseBackups) KV() []map[string]any {
 	return out
 }
 
+type DatabaseCA struct {
+	DatabaseCA do.DatabaseCA
+}
+
+var _ Displayable = &DatabaseCA{}
+
+func (dc *DatabaseCA) JSON(out io.Writer) error {
+	return writeJSON(dc.DatabaseCA, out)
+}
+
+func (dc *DatabaseCA) Cols() []string {
+	return []string{
+		"Certificate",
+	}
+}
+
+func (dc *DatabaseCA) ColMap() map[string]string {
+	return map[string]string{
+		"Certificate": "Certificate",
+	}
+}
+
+func (dc *DatabaseCA) KV() []map[string]any {
+	return []map[string]any{
+		{
+			"Certificate": string(dc.DatabaseCA.Certificate),
+		},
+	}
+}
+
 type DatabaseUsers struct {
 	DatabaseUsers do.DatabaseUsers
 }
diff --git a/do/databases.go b/do/databases.go
index fe61b552..87792002 100644
--- a/do/databases.go
+++ b/do/databases.go
@@ -26,6 +26,14 @@ type Database struct {
 	*godo.Database
 }
 
+// DatabaseCA is a wrapper for godo.DatabaseCA
+type DatabaseCA struct {
+	*godo.DatabaseCA
+}
+
+// DatabaseCAs is a slice of DatabaseCA
+type DatabaseCAs []DatabaseCA
+
 // Databases is a slice of Database
 type Databases []Database
 
@@ -145,6 +153,7 @@ type DatabaseIndex struct {
 type DatabasesService interface {
 	List() (Databases, error)
 	Get(string) (*Database, error)
+	GetCA(string) (*DatabaseCA, error)
 	Create(*godo.DatabaseCreateRequest) (*Database, error)
 	Delete(string) error
 	GetConnection(string, bool) (*DatabaseConnection, error)
@@ -257,6 +266,15 @@ func (ds *databasesService) Get(databaseID string) (*Database, error) {
 	return &Database{Database: db}, nil
 }
 
+func (ds *databasesService) GetCA(databaseID string) (*DatabaseCA, error) {
+	dbCA, _, err := ds.client.Databases.GetCA(context.TODO(), databaseID)
+	if err != nil {
+		return nil, err
+	}
+
+	return &DatabaseCA{DatabaseCA: dbCA}, nil
+}
+
 func (ds *databasesService) Create(req *godo.DatabaseCreateRequest) (*Database, error) {
 	db, _, err := ds.client.Databases.Create(context.TODO(), req)
 	if err != nil {
diff --git a/do/mocks/DatabasesService.go b/do/mocks/DatabasesService.go
index bb63b7a4..712f9e38 100644
--- a/do/mocks/DatabasesService.go
+++ b/do/mocks/DatabasesService.go
@@ -243,6 +243,21 @@ func (mr *MockDatabasesServiceMockRecorder) Get(arg0 any) *gomock.Call {
 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockDatabasesService)(nil).Get), arg0)
 }
 
+// GetCA mocks base method.
+func (m *MockDatabasesService) GetCA(arg0 string) (*do.DatabaseCA, error) {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "GetCA", arg0)
+	ret0, _ := ret[0].(*do.DatabaseCA)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
+}
+
+// GetCA indicates an expected call of GetCA.
+func (mr *MockDatabasesServiceMockRecorder) GetCA(arg0 any) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetCA", reflect.TypeOf((*MockDatabasesService)(nil).GetCA), arg0)
+}
+
 // GetConnection mocks base method.
 func (m *MockDatabasesService) GetConnection(arg0 string, arg1 bool) (*do.DatabaseConnection, error) {
 	m.ctrl.T.Helper()


Test patch:
diff --git a/commands/databases_test.go b/commands/databases_test.go
index f2f94a35..dcea7405 100644
--- a/commands/databases_test.go
+++ b/commands/databases_test.go
@@ -66,6 +66,12 @@ var (
 		},
 	}
 
+	testDBClusterCA = do.DatabaseCA{
+		DatabaseCA: &godo.DatabaseCA{
+			Certificate: []byte("QQDDC9jMGIwYzNiZS0wZjY4LTRkY4OCCAaIwDQYJKoZIhvcNAQ"),
+		},
+	}
+
 	testKafkaDBCluster = do.Database{
 		Database: &godo.Database{
 			ID:          "ea93928g-8se0-929e-m1ns-029daj2k3j12",
@@ -244,6 +250,7 @@ func TestDatabasesCommand(t *testing.T) {
 	assertCommandNames(t, cmd,
 		"list",
 		"get",
+		"get-ca",
 		"create",
 		"delete",
 		"connection",
@@ -378,6 +385,31 @@ func TestDatabasesGet(t *testing.T) {
 	})
 }
 
+func TestDatabasesGetCA(t *testing.T) {
+	// Successful call
+	withTestClient(t, func(config *CmdConfig, tm *tcMocks) {
+		tm.databases.EXPECT().GetCA(testDBCluster.ID).Return(&testDBClusterCA, nil)
+		config.Args = append(config.Args, testDBCluster.ID)
+		err := RunDatabaseGetCA(config)
+		assert.NoError(t, err)
+	})
+
+	// Error
+	notFound := "not-found"
+	withTestClient(t, func(config *CmdConfig, tm *tcMocks) {
+		tm.databases.EXPECT().GetCA(notFound).Return(nil, errTest)
+		config.Args = append(config.Args, notFound)
+		err := RunDatabaseGetCA(config)
+		assert.Error(t, err)
+	})
+
+	// ID not provided
+	withTestClient(t, func(config *CmdConfig, tm *tcMocks) {
+		err := RunDatabaseGetCA(config)
+		assert.EqualError(t, doctl.NewMissingArgsErr(config.NS), err.Error())
+	})
+}
+
 func TestDatabasesList(t *testing.T) {
 	// Successful call
 	withTestClient(t, func(config *CmdConfig, tm *tcMocks) {