Repository: digitalocean/doctl (go)

Issue:
Add --wait flag to database create command
**What is the problem this feature would solve? Please describe.**

`doctl` aims to be useful in scripting. One way it does this is by providing a `--wait` flag for long running, async operations to block until until they are complete rather than making the user implement polling in their own script. Some examples include:

- `doctl compute droplet create --wait`
- `doctl kubernetes cluster create --wait`
- `doctl apps create --wait`

Currently the `doctl database create` command does not have a `--wait` flag.

**Describe the solution you'd like**

Add a `--wait` flag to the `doctl database create` command. It should poll the [`/v2/databases/$DATABASE_ID` endpoint](https://docs.digitalocean.com/reference/api/api-reference/#operation/databases_get_cluster), blocking until the database cluster's `status` attribute moves from `creating` to `online`.

**Additional context**

Following the code path starting here demonstrates how `doctl apps create`'s  `--wait` flag is implemented:

https://github.com/digitalocean/doctl/blob/de1918ad1ee4b1e523c335fa9254e85d415d18aa/commands/apps.go#L282

One important difference for databases is that some database engines (mongodb now, but potentially others) only return the password in the initial response to the create request. It is _not_ included in subsequent GET responses. So that value must be retained. 

Pull request:
Feature/add wait flag to database create
## Description

Adds `--wait` flag to the doctl database create command. 

It polls the [/v2/databases/$DATABASE_ID endpoint](https://docs.digitalocean.com/reference/api/api-reference/#operation/databases_get_cluster), blocking until the database enters the `online` state.

## Motivation and context

Closes #1263 

## Types of changes

Feature enhancement.

## Checklist:

- [x] I have read the **[CONTRIBUTING](CONTRIBUTING.md)** document.
- [x] My pull request addresses exactly one patch/feature.
- [x] I have created a branch for this patch/feature.
- [x] Each individual commit in the pull request is meaningful.
- [x] I have added tests to cover my changes.
- [ ] If my change requires a change to the documentation, I have updated it accordingly.

Patch:
diff --git a/commands/databases.go b/commands/databases.go
index 61e955eb..7bda6710 100644
--- a/commands/databases.go
+++ b/commands/databases.go
@@ -16,7 +16,9 @@ package commands
 import (
 	"errors"
 	"fmt"
+	"os"
 	"strings"
+	"time"
 
 	"github.com/digitalocean/doctl"
 	"github.com/digitalocean/doctl/commands/displayers"
@@ -76,6 +78,7 @@ There are a number of flags that customize the configuration, all of which are o
 	AddStringFlag(cmdDatabaseCreate, doctl.ArgDatabaseEngine, "", defaultDatabaseEngine, "The database engine to be used for the cluster. Possible values are: `pg` for PostgreSQL, `mysql`, `redis`, and `mongodb`.")
 	AddStringFlag(cmdDatabaseCreate, doctl.ArgVersion, "", "", "The database engine version, e.g. 14 for PostgreSQL version 14")
 	AddStringFlag(cmdDatabaseCreate, doctl.ArgPrivateNetworkUUID, "", "", "The UUID of a VPC to create the database cluster in; the default VPC for the region will be used if excluded")
+	AddBoolFlag(cmdDatabaseCreate, doctl.ArgCommandWait, "", false, "Boolean that specifies whether to wait for a database to complete before returning control to the terminal")
 
 	cmdDatabaseDelete := CmdBuilder(cmd, RunDatabaseDelete, "delete <database-id>", "Delete a database cluster", `This command deletes the database cluster with the given ID.
 
@@ -167,11 +170,37 @@ func RunDatabaseCreate(c *CmdConfig) error {
 		return err
 	}
 
-	db, err := c.Databases().Create(r)
+	dbs := c.Databases()
+
+	db, err := dbs.Create(r)
+	if err != nil {
+		return err
+	}
+
+	wait, err := c.Doit.GetBool(c.NS, doctl.ArgCommandWait)
 	if err != nil {
 		return err
 	}
 
+	if wait {
+		connection := db.Connection
+		dbs := c.Databases()
+		notice("Database creation is in progress, waiting for database to be online")
+
+		err := waitForDatabaseReady(dbs, db.ID)
+		if err != nil {
+			return fmt.Errorf(
+				"database couldn't enter the `online` state: %v",
+				err,
+			)
+		}
+
+		db, _ = dbs.Get(db.ID)
+		db.Connection = connection
+	}
+
+	notice("Database created")
+
 	return displayDatabases(c, false, *db)
 }
 
@@ -1636,3 +1665,39 @@ func RunDatabaseFirewallRulesRemove(c *CmdConfig) error {
 
 	return displayDatabaseFirewallRules(c, true, databaseID)
 }
+
+func waitForDatabaseReady(dbs do.DatabasesService, dbID string) error {
+	const (
+		maxAttempts = 180
+		wantStatus  = "online"
+	)
+	attempts := 0
+	printNewLineSet := false
+
+	for i := 0; i < maxAttempts; i++ {
+		if attempts != 0 {
+			fmt.Fprint(os.Stderr, ".")
+			if !printNewLineSet {
+				printNewLineSet = true
+				defer fmt.Fprintln(os.Stderr)
+			}
+		}
+
+		db, err := dbs.Get(dbID)
+		if err != nil {
+			return err
+		}
+
+		if db.Status == wantStatus {
+			return nil
+		}
+
+		attempts++
+		time.Sleep(10 * time.Second)
+	}
+
+	return fmt.Errorf(
+		"timeout waiting for database (%s) to enter `online` state",
+		dbID,
+	)
+}


Test patch:
diff --git a/integration/database_create_test.go b/integration/database_create_test.go
index 6256514d..e97ada5c 100644
--- a/integration/database_create_test.go
+++ b/integration/database_create_test.go
@@ -23,6 +23,8 @@ var _ = suite("database/create", func(t *testing.T, when spec.G, it spec.S) {
 		server *httptest.Server
 	)
 
+	const testUUID = "some-id"
+
 	it.Before(func() {
 		expect = require.New(t)
 
@@ -63,6 +65,19 @@ var _ = suite("database/create", func(t *testing.T, when spec.G, it spec.S) {
 				expect.NoError(err)
 
 				w.Write(buffer.Bytes())
+			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(databaseWaitGetResponseNoConnection))
 			default:
 				dump, err := httputil.DumpRequest(req, true)
 				if err != nil {
@@ -97,12 +112,43 @@ var _ = suite("database/create", func(t *testing.T, when spec.G, it spec.S) {
 		})
 	})
 
+	when("all flags are passed including wait", func() {
+		it("creates the databases and outputs the correct URI", func() {
+			cmd := exec.Command(builtBinaryPath,
+				"-t", "some-magic-token",
+				"-u", server.URL,
+				"databases",
+				"create",
+				"my-database-name",
+				"--engine", "mysql",
+				"--num-nodes", "100",
+				"--private-network-uuid", "some-uuid",
+				"--region", "nyc3",
+				"--size", "biggest",
+				"--version", "what-version",
+				"--wait",
+			)
+
+			output, err := cmd.CombinedOutput()
+			expect.NoError(err, fmt.Sprintf("received error output: %s", output))
+			expect.Equal(strings.TrimSpace(databasesWaitCreateOutput), strings.TrimSpace(string(output)))
+
+		})
+	})
+
 })
 
 const (
 	databasesCreateOutput = `
-ID         Name                Engine    Version         Number of Nodes    Region    Status      Size       URI    Created At
-some-id    my-database-name    mysql     what-version    100                nyc3      creating    biggest           2019-01-11 18:37:36 +0000 UTC
+Notice: Database created
+ID         Name                Engine    Version         Number of Nodes    Region    Status      Size       URI                                                                                     Created At
+some-id    my-database-name    mysql     what-version    100                nyc3      creating    biggest    mysql://doadmin:secret@aaa-bbb-ccc-111-222-333.db.ondigitalocean.com:25060/defaultdb    2019-01-11 18:37:36 +0000 UTC
+`
+	databasesWaitCreateOutput = `
+Notice: Database creation is in progress, waiting for database to be online
+Notice: Database created
+ID         Name                Engine    Version         Number of Nodes    Region    Status    Size       URI                                                                                     Created At
+some-id    my-database-name    mysql     what-version    100                nyc3      online    biggest    mysql://doadmin:secret@aaa-bbb-ccc-111-222-333.db.ondigitalocean.com:25060/defaultdb    2019-01-11 18:37:36 +0000 UTC
 `
 	databaseCreateResponse = `
 {
@@ -111,7 +157,9 @@ some-id    my-database-name    mysql     what-version    100                nyc3
     "name": "{{.Name}}",
     "engine": "{{.Engine}}",
     "version": "{{.Version}}",
-    "connection": {},
+    "connection": {
+      "uri": "{{.Engine}}://doadmin:secret@aaa-bbb-ccc-111-222-333.db.ondigitalocean.com:25060/defaultdb"
+    },
     "private_connection": {},
     "users": null,
     "db_names": null,
@@ -126,4 +174,27 @@ some-id    my-database-name    mysql     what-version    100                nyc3
     ]
   }
 }`
+
+	databaseWaitGetResponseNoConnection = `
+{
+  "database": {
+    "id": "some-id",
+    "name": "my-database-name",
+    "engine": "mysql",
+    "version": "what-version",
+    "connection": {},
+    "private_connection": {},
+    "users": null,
+    "db_names": null,
+    "num_nodes": 100,
+    "region": "nyc3",
+    "status": "online",
+    "created_at": "2019-01-11T18:37:36Z",
+    "maintenance_window": null,
+    "size": "biggest",
+    "tags": [
+      "production"
+    ]
+  }
+}`
 )