Repository: digitalocean/doctl
Pull request #1412 (2023-09-01 21:30:01)

databases: Support displaying private connection.
While doing https://github.com/digitalocean/doctl/pull/1411, I noticed another database feature we do not support well. Database clusters now return connection details for both their public hostnames and private hostnames only accessible via VPC. This adds a `--private` flag to the existing `databases connection` command that will return the private connection details instead of the public ones when passed. 


Patch:
diff --git a/args.go b/args.go
index ee69446a..496e1ca7 100644
--- a/args.go
+++ b/args.go
@@ -365,6 +365,8 @@ const (
 	ArgDatabasePoolMode = "mode"
 	// ArgDatabaseUserMySQLAuthPlugin is a flag for setting the MySQL user auth plugin
 	ArgDatabaseUserMySQLAuthPlugin = "mysql-auth-plugin"
+	// ArgDatabasePrivateConnectionBool determine if the private connection details should be shown
+	ArgDatabasePrivateConnectionBool = "private"
 
 	// ArgPrivateNetworkUUID is the flag for VPC UUID
 	ArgPrivateNetworkUUID = "private-network-uuid"
diff --git a/commands/databases.go b/commands/databases.go
index 0404eb4c..0ebb5993 100644
--- a/commands/databases.go
+++ b/commands/databases.go
@@ -90,7 +90,7 @@ To retrieve a list of your database clusters and their IDs, call `+"`"+`doctl da
 		aliasOpt("rm"))
 	AddBoolFlag(cmdDatabaseDelete, doctl.ArgForce, doctl.ArgShortForce, false, "Delete the database cluster without a confirmation prompt")
 
-	CmdBuilder(cmd, RunDatabaseConnectionGet, "connection <database-id>", "Retrieve connection details for a database cluster", `This command retrieves the following connection details for a database cluster:
+	cmdDatabaseGetConn := CmdBuilder(cmd, RunDatabaseConnectionGet, "connection <database-id>", "Retrieve connection details for a database cluster", `This command retrieves the following connection details for a database cluster:
 
 - The connection string for the database cluster
 - The default database name
@@ -102,6 +102,7 @@ To retrieve a list of your database clusters and their IDs, call `+"`"+`doctl da
 
 While these connection details will work, you may wish to use different connection details, such as the private hostname, a custom username, or a different database.`, Writer,
 		aliasOpt("conn"), displayerType(&displayers.DatabaseConnection{}))
+	AddBoolFlag(cmdDatabaseGetConn, doctl.ArgDatabasePrivateConnectionBool, "", false, "Provide connection details using the private hostname")
 
 	CmdBuilder(cmd, RunDatabaseBackupsList, "backups <database-id>", "List database cluster backups", `This command retrieves a list of backups created for the specified database cluster.
 
@@ -423,7 +424,12 @@ func RunDatabaseConnectionGet(c *CmdConfig) error {
 	}
 
 	id := c.Args[0]
-	connInfo, err := c.Databases().GetConnection(id)
+	private, err := c.Doit.GetBool(c.NS, doctl.ArgDatabasePrivateConnectionBool)
+	if err != nil {
+		return err
+	}
+
+	connInfo, err := c.Databases().GetConnection(id, private)
 	if err != nil {
 		return err
 	}
diff --git a/do/databases.go b/do/databases.go
index c90828e6..7e03f61a 100644
--- a/do/databases.go
+++ b/do/databases.go
@@ -102,7 +102,7 @@ type DatabasesService interface {
 	Get(string) (*Database, error)
 	Create(*godo.DatabaseCreateRequest) (*Database, error)
 	Delete(string) error
-	GetConnection(string) (*DatabaseConnection, error)
+	GetConnection(string, bool) (*DatabaseConnection, error)
 	ListBackups(string) (DatabaseBackups, error)
 	Resize(string, *godo.DatabaseResizeRequest) error
 	Migrate(string, *godo.DatabaseMigrateRequest) error
@@ -207,12 +207,18 @@ func (ds *databasesService) Delete(databaseID string) error {
 	return err
 }
 
-func (ds *databasesService) GetConnection(databaseID string) (*DatabaseConnection, error) {
+func (ds *databasesService) GetConnection(databaseID string, private bool) (*DatabaseConnection, error) {
 	db, err := ds.Get(databaseID)
 	if err != nil {
 		return nil, err
 	}
 
+	if private {
+		return &DatabaseConnection{
+			DatabaseConnection: db.PrivateConnection,
+		}, nil
+	}
+
 	return &DatabaseConnection{
 		DatabaseConnection: db.Connection,
 	}, nil
diff --git a/do/mocks/DatabasesService.go b/do/mocks/DatabasesService.go
index e4a8707d..714b44af 100644
--- a/do/mocks/DatabasesService.go
+++ b/do/mocks/DatabasesService.go
@@ -196,18 +196,18 @@ func (mr *MockDatabasesServiceMockRecorder) Get(arg0 interface{}) *gomock.Call {
 }
 
 // GetConnection mocks base method.
-func (m *MockDatabasesService) GetConnection(arg0 string) (*do.DatabaseConnection, error) {
+func (m *MockDatabasesService) GetConnection(arg0 string, arg1 bool) (*do.DatabaseConnection, error) {
 	m.ctrl.T.Helper()
-	ret := m.ctrl.Call(m, "GetConnection", arg0)
+	ret := m.ctrl.Call(m, "GetConnection", arg0, arg1)
 	ret0, _ := ret[0].(*do.DatabaseConnection)
 	ret1, _ := ret[1].(error)
 	return ret0, ret1
 }
 
 // GetConnection indicates an expected call of GetConnection.
-func (mr *MockDatabasesServiceMockRecorder) GetConnection(arg0 interface{}) *gomock.Call {
+func (mr *MockDatabasesServiceMockRecorder) GetConnection(arg0, arg1 interface{}) *gomock.Call {
 	mr.mock.ctrl.T.Helper()
-	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetConnection", reflect.TypeOf((*MockDatabasesService)(nil).GetConnection), arg0)
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetConnection", reflect.TypeOf((*MockDatabasesService)(nil).GetConnection), arg0, arg1)
 }
 
 // GetDB mocks base method.


Test patch:
diff --git a/commands/databases_test.go b/commands/databases_test.go
index 09a8cdd2..8672afdb 100644
--- a/commands/databases_test.go
+++ b/commands/databases_test.go
@@ -538,7 +538,7 @@ func TestDatabaseListBackups(t *testing.T) {
 func TestDatabaseConnectionGet(t *testing.T) {
 	// Success
 	withTestClient(t, func(config *CmdConfig, tm *tcMocks) {
-		tm.databases.EXPECT().GetConnection(testDBCluster.ID).Return(&testDBConnection, nil)
+		tm.databases.EXPECT().GetConnection(testDBCluster.ID, false).Return(&testDBConnection, nil)
 		config.Args = append(config.Args, testDBCluster.ID)
 
 		err := RunDatabaseConnectionGet(config)
@@ -547,7 +547,7 @@ func TestDatabaseConnectionGet(t *testing.T) {
 
 	// Error
 	withTestClient(t, func(config *CmdConfig, tm *tcMocks) {
-		tm.databases.EXPECT().GetConnection(testDBCluster.ID).Return(nil, errTest)
+		tm.databases.EXPECT().GetConnection(testDBCluster.ID, false).Return(nil, errTest)
 		config.Args = append(config.Args, testDBCluster.ID)
 
 		err := RunDatabaseConnectionGet(config)
@@ -555,6 +555,28 @@ func TestDatabaseConnectionGet(t *testing.T) {
 	})
 }
 
+func TestDatabaseConnectionGetPrivate(t *testing.T) {
+	// Success
+	withTestClient(t, func(config *CmdConfig, tm *tcMocks) {
+		tm.databases.EXPECT().GetConnection(testDBCluster.ID, true).Return(&testDBConnection, nil)
+		config.Args = append(config.Args, testDBCluster.ID)
+		config.Doit.Set(config.NS, doctl.ArgDatabasePrivateConnectionBool, true)
+
+		err := RunDatabaseConnectionGet(config)
+		assert.NoError(t, err)
+	})
+
+	// Error
+	withTestClient(t, func(config *CmdConfig, tm *tcMocks) {
+		tm.databases.EXPECT().GetConnection(testDBCluster.ID, true).Return(nil, errTest)
+		config.Args = append(config.Args, testDBCluster.ID)
+		config.Doit.Set(config.NS, doctl.ArgDatabasePrivateConnectionBool, true)
+
+		err := RunDatabaseConnectionGet(config)
+		assert.EqualError(t, err, errTest.Error())
+	})
+}
+
 func TestDatabaseGetMaintenance(t *testing.T) {
 	// Success
 	withTestClient(t, func(config *CmdConfig, tm *tcMocks) {
diff --git a/integration/database_connection_test.go b/integration/database_connection_test.go
new file mode 100644
index 00000000..4b70df3e
--- /dev/null
+++ b/integration/database_connection_test.go
@@ -0,0 +1,135 @@
+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.Focus("database/connection", func(t *testing.T, when spec.G, it spec.S) {
+	var (
+		expect *require.Assertions
+		server *httptest.Server
+	)
+
+	const testUUID = "aaa-bbb-111-222-ccc-333"
+
+	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/" + testUUID:
+				auth := req.Header.Get("Authorization")
+				if auth != "Bearer some-magic-token" {
+					w.WriteHeader(http.StatusUnauthorized)
+					return
+				}
+
+				if req.Method != http.MethodGet {
+					w.WriteHeader(http.StatusMethodNotAllowed)
+					return
+				}
+
+				w.Write([]byte(databaseGetResponseWithConnection))
+			default:
+				dump, err := httputil.DumpRequest(req, true)
+				if err != nil {
+					t.Fatal("failed to dump request")
+				}
+
+				t.Fatalf("received unknown request: %s", dump)
+			}
+		}))
+	})
+
+	when("private flag not passed", func() {
+		it("public connection details are returned", func() {
+			cmd := exec.Command(builtBinaryPath,
+				"-t", "some-magic-token",
+				"-u", server.URL,
+				"databases",
+				"connection",
+				testUUID,
+				"--no-header",
+				"--format", "Host",
+			)
+
+			output, err := cmd.CombinedOutput()
+			expect.NoError(err, fmt.Sprintf("received error output: %s", output))
+			expect.Equal("db-postgresql-nyc3-test.db.ondigitalocean.com", strings.TrimSpace(string(output)))
+
+		})
+	})
+
+	when("private flag is passed", func() {
+		it("private connection details are returned", func() {
+			cmd := exec.Command(builtBinaryPath,
+				"-t", "some-magic-token",
+				"-u", server.URL,
+				"databases",
+				"connection",
+				testUUID,
+				"--private",
+				"--no-header",
+				"--format", "Host",
+			)
+
+			output, err := cmd.CombinedOutput()
+			expect.NoError(err, fmt.Sprintf("received error output: %s", output))
+			expect.Equal("private-db-postgresql-nyc3-test.db.ondigitalocean.com", strings.TrimSpace(string(output)))
+
+		})
+	})
+
+})
+
+const (
+	databaseGetResponseWithConnection = `
+{
+  "database": {
+    "id": "aaa-bbb-111-222-ccc-333",
+    "name": "test",
+    "engine": "pg",
+    "version": "13",
+    "connection": {
+        "protocol": "postgresql",
+        "uri": "postgresql://doadmin:secret@db-postgresql-nyc3-test.db.ondigitalocean.com:25060/defaultdb?sslmode=require",
+        "database": "defaultdb",
+        "host": "db-postgresql-nyc3-test.db.ondigitalocean.com",
+        "port": 25060,
+        "user": "doadmin",
+        "password": "secret",
+        "ssl": true
+    },
+    "private_connection": {
+        "protocol": "postgresql",
+        "uri": "postgresql://doadmin:secret@private-db-postgresql-nyc3-test.db.ondigitalocean.com:25060/defaultdb?sslmode=require",
+        "database": "defaultdb",
+        "host": "private-db-postgresql-nyc3-test.db.ondigitalocean.com",
+        "port": 25060,
+        "user": "doadmin",
+        "password": "secret",
+        "ssl": true
+    },
+    "users": null,
+    "db_names": null,
+    "num_nodes": 1,
+    "region": "nyc3",
+    "status": "creating",
+    "created_at": "2019-01-11T18:37:36Z",
+    "maintenance_window": null,
+    "size": "biggest",
+    "tags": [
+      "production"
+    ]
+  }
+}`
+)