1/*
2Copyright 2018 The Doctl Authors All rights reserved.
3Licensed under the Apache License, Version 2.0 (the "License");
4you may not use this file except in compliance with the License.
5You may obtain a copy of the License at
6    http://www.apache.org/licenses/LICENSE-2.0
7Unless required by applicable law or agreed to in writing, software
8distributed under the License is distributed on an "AS IS" BASIS,
9WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10See the License for the specific language governing permissions and
11limitations under the License.
12*/
13
14package commands
15
16import (
17	"errors"
18	"fmt"
19	"strings"
20
21	"github.com/digitalocean/doctl"
22	"github.com/digitalocean/doctl/commands/displayers"
23	"github.com/digitalocean/doctl/do"
24	"github.com/digitalocean/godo"
25	"github.com/spf13/cobra"
26)
27
28const (
29	defaultDatabaseNodeSize  = "db-s-1vcpu-1gb"
30	defaultDatabaseNodeCount = 1
31	defaultDatabaseRegion    = "nyc1"
32	defaultDatabaseEngine    = "pg"
33	databaseListDetails      = `
34
35This command requires the ID of a database cluster, which you can retrieve by calling:
36
37	doctl databases list`
38)
39
40// Databases creates the databases command
41func Databases() *Command {
42	cmd := &Command{
43		Command: &cobra.Command{
44			Use:     "databases",
45			Aliases: []string{"db", "dbs", "d", "database"},
46			Short:   "Display commands that manage databases",
47			Long:    "The commands under `doctl databases` are for managing your MySQL, Redis, and PostgreSQL database services.",
48		},
49	}
50
51	clusterDetails := `
52
53- The database ID, in UUID format
54- The name you gave the database cluster
55- The database engine (e.g. ` + "`" + `redis` + "`" + `, ` + "`" + `pg` + "`" + `, ` + "`" + `mysql` + "`" + `)
56- The engine version (e.g. ` + "`" + `11` + "`" + ` for PostgreSQL version 11)
57- The number of nodes in the database cluster
58- The region the database cluster resides in (e.g. ` + "`" + `sfo2` + "`" + `, ` + "`" + `nyc1` + "`" + `)
59- The current status of the database cluster (e.g. ` + "`" + `online` + "`" + `)
60- The size of the machine running the database instance (e.g. ` + "`" + `db-s-1vcpu-1gb` + "`" + `)`
61
62	CmdBuilder(cmd, RunDatabaseList, "list", "List your database clusters", `This command lists the database clusters associated with your account. The following details are provided:`+clusterDetails, Writer, aliasOpt("ls"), displayerType(&displayers.Databases{}))
63	CmdBuilder(cmd, RunDatabaseGet, "get <database-id>", "Get details for a database cluster", `This command retrieves the following details about the specified database cluster: `+clusterDetails+`
64- A connection string for the database cluster
65- The date and time when the database cluster was created`+databaseListDetails, Writer, aliasOpt("g"), displayerType(&displayers.Databases{}))
66
67	nodeSizeDetails := "The size of the nodes in the database cluster, e.g. `db-s-1vcpu-1gb`` for a 1 CPU, 1GB node"
68	nodeNumberDetails := "The number of nodes in the database cluster. Valid values are are 1-3. In addition to the primary node, up to two standby nodes may be added for high availability."
69	cmdDatabaseCreate := CmdBuilder(cmd, RunDatabaseCreate, "create <name>", "Create a database cluster", `This command creates a database cluster with the specified name.
70
71There are a number of flags that customize the configuration, all of which are optional. Without any flags set, a single-node, single-CPU PostgreSQL database cluster will be created.`, Writer,
72		aliasOpt("c"))
73	AddIntFlag(cmdDatabaseCreate, doctl.ArgDatabaseNumNodes, "", defaultDatabaseNodeCount, nodeNumberDetails)
74	AddStringFlag(cmdDatabaseCreate, doctl.ArgRegionSlug, "", defaultDatabaseRegion, "The region where the database cluster will be created, e.g. `nyc1` or `sfo2`")
75	AddStringFlag(cmdDatabaseCreate, doctl.ArgSizeSlug, "", defaultDatabaseNodeSize, nodeSizeDetails)
76	AddStringFlag(cmdDatabaseCreate, doctl.ArgDatabaseEngine, "", defaultDatabaseEngine, "The database engine to be used for the cluster. Possible values are: `pg` for PostgreSQL, `mysql`, and `redis`.")
77	AddStringFlag(cmdDatabaseCreate, doctl.ArgVersion, "", "", "The database engine version, e.g. 11 for PostgreSQL version 11")
78	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")
79
80	cmdDatabaseDelete := CmdBuilder(cmd, RunDatabaseDelete, "delete <database-id>", "Delete a database cluster", `This command deletes the database cluster with the given ID.
81
82To retrieve a list of your database clusters and their IDs, call `+"`"+`doctl databases list`+"`"+`.`, Writer,
83		aliasOpt("rm"))
84	AddBoolFlag(cmdDatabaseDelete, doctl.ArgForce, doctl.ArgShortForce, false, "Delete the database cluster without a confirmation prompt")
85
86	CmdBuilder(cmd, RunDatabaseConnectionGet, "connection <database-id>", "Retrieve connection details for a database cluster", `This command retrieves the following connection details for a database cluster:
87
88- The connection string for the database cluster
89- The default database name
90- The fully-qualified domain name of the publicly-connectable host
91- The port on which the database is listening for connections
92- The default username
93- The randomly-generated password for the default username
94- A boolean indicating if the connection should be made over SSL
95
96While 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,
97		aliasOpt("conn"), displayerType(&displayers.DatabaseConnection{}))
98
99	CmdBuilder(cmd, RunDatabaseBackupsList, "backups <database-id>", "List database cluster backups", `This command retrieves a list of backups created for the specified database cluster.
100
101The list contains the size in GB, and the date and time the backup was taken.`, Writer,
102		aliasOpt("bu"), displayerType(&displayers.DatabaseBackups{}))
103
104	cmdDatabaseResize := CmdBuilder(cmd, RunDatabaseResize, "resize <database-id>", "Resize a database cluster", `This command resizes the specified database cluster.
105
106You must specify the size of the machines you wish to use as nodes as well as how many nodes you would like. For example:
107
108	doctl databases resize ca9f591d-9999-5555-a0ef-1c02d1d1e352 --num-nodes 2 --size db-s-16vcpu-64gb`, Writer,
109		aliasOpt("rs"))
110	AddIntFlag(cmdDatabaseResize, doctl.ArgDatabaseNumNodes, "", 0, nodeNumberDetails, requiredOpt())
111	AddStringFlag(cmdDatabaseResize, doctl.ArgSizeSlug, "", "", nodeSizeDetails, requiredOpt())
112
113	cmdDatabaseMigrate := CmdBuilder(cmd, RunDatabaseMigrate, "migrate <database-id>", "Migrate a database cluster to a new region", `This command migrates the specified database cluster to a new region`, Writer,
114		aliasOpt("m"))
115	AddStringFlag(cmdDatabaseMigrate, doctl.ArgRegionSlug, "", "", "The region to which the database cluster should be migrated, e.g. `sfo2` or `nyc3`.", requiredOpt())
116	AddStringFlag(cmdDatabaseMigrate, doctl.ArgPrivateNetworkUUID, "", "", "The UUID of a VPC to create the database cluster in; the default VPC for the region will be used if excluded")
117
118	cmd.AddCommand(databaseReplica())
119	cmd.AddCommand(databaseMaintenanceWindow())
120	cmd.AddCommand(databaseUser())
121	cmd.AddCommand(databaseDB())
122	cmd.AddCommand(databasePool())
123	cmd.AddCommand(sqlMode())
124	cmd.AddCommand(databaseFirewalls())
125
126	return cmd
127}
128
129// Clusters
130
131// RunDatabaseList returns a list of database clusters.
132func RunDatabaseList(c *CmdConfig) error {
133	dbs, err := c.Databases().List()
134	if err != nil {
135		return err
136	}
137
138	return displayDatabases(c, true, dbs...)
139}
140
141// RunDatabaseGet returns an individual database cluster
142func RunDatabaseGet(c *CmdConfig) error {
143	if len(c.Args) == 0 {
144		return doctl.NewMissingArgsErr(c.NS)
145	}
146
147	id := c.Args[0]
148	db, err := c.Databases().Get(id)
149	if err != nil {
150		return err
151	}
152
153	return displayDatabases(c, false, *db)
154}
155
156// RunDatabaseCreate creates a database cluster
157func RunDatabaseCreate(c *CmdConfig) error {
158	if len(c.Args) == 0 {
159		return doctl.NewMissingArgsErr(c.NS)
160	}
161
162	r, err := buildDatabaseCreateRequestFromArgs(c)
163	if err != nil {
164		return err
165	}
166
167	db, err := c.Databases().Create(r)
168	if err != nil {
169		return err
170	}
171
172	return displayDatabases(c, false, *db)
173}
174
175func buildDatabaseCreateRequestFromArgs(c *CmdConfig) (*godo.DatabaseCreateRequest, error) {
176	r := &godo.DatabaseCreateRequest{Name: c.Args[0]}
177
178	region, err := c.Doit.GetString(c.NS, doctl.ArgRegionSlug)
179	if err != nil {
180		return nil, err
181	}
182	r.Region = region
183
184	numNodes, err := c.Doit.GetInt(c.NS, doctl.ArgDatabaseNumNodes)
185	if err != nil {
186		return nil, err
187	}
188	r.NumNodes = numNodes
189
190	size, err := c.Doit.GetString(c.NS, doctl.ArgSizeSlug)
191	if err != nil {
192		return nil, err
193	}
194	r.SizeSlug = size
195
196	engine, err := c.Doit.GetString(c.NS, doctl.ArgDatabaseEngine)
197	if err != nil {
198		return nil, err
199	}
200	r.EngineSlug = engine
201
202	version, err := c.Doit.GetString(c.NS, doctl.ArgVersion)
203	if err != nil {
204		return nil, err
205	}
206	r.Version = version
207
208	privateNetworkUUID, err := c.Doit.GetString(c.NS, doctl.ArgPrivateNetworkUUID)
209	if err != nil {
210		return nil, err
211	}
212	r.PrivateNetworkUUID = privateNetworkUUID
213
214	return r, nil
215}
216
217// RunDatabaseDelete deletes a database cluster
218func RunDatabaseDelete(c *CmdConfig) error {
219	if len(c.Args) == 0 {
220		return doctl.NewMissingArgsErr(c.NS)
221	}
222
223	force, err := c.Doit.GetBool(c.NS, doctl.ArgForce)
224	if err != nil {
225		return err
226	}
227
228	if force || AskForConfirmDelete("database cluster", 1) == nil {
229		id := c.Args[0]
230		return c.Databases().Delete(id)
231	}
232
233	return errOperationAborted
234}
235
236func displayDatabases(c *CmdConfig, short bool, dbs ...do.Database) error {
237	item := &displayers.Databases{
238		Databases: do.Databases(dbs),
239		Short:     short,
240	}
241	return c.Display(item)
242}
243
244// RunDatabaseConnectionGet gets database connection info
245func RunDatabaseConnectionGet(c *CmdConfig) error {
246	if len(c.Args) == 0 {
247		return doctl.NewMissingArgsErr(c.NS)
248	}
249
250	id := c.Args[0]
251	connInfo, err := c.Databases().GetConnection(id)
252	if err != nil {
253		return err
254	}
255
256	return displayDatabaseConnection(c, *connInfo)
257}
258
259func displayDatabaseConnection(c *CmdConfig, conn do.DatabaseConnection) error {
260	item := &displayers.DatabaseConnection{DatabaseConnection: conn}
261	return c.Display(item)
262}
263
264// RunDatabaseBackupsList lists all the backups for a database cluster
265func RunDatabaseBackupsList(c *CmdConfig) error {
266	if len(c.Args) == 0 {
267		return doctl.NewMissingArgsErr(c.NS)
268	}
269
270	id := c.Args[0]
271	backups, err := c.Databases().ListBackups(id)
272	if err != nil {
273		return err
274	}
275
276	return displayDatabaseBackups(c, backups)
277}
278
279func displayDatabaseBackups(c *CmdConfig, bu do.DatabaseBackups) error {
280	item := &displayers.DatabaseBackups{DatabaseBackups: bu}
281	return c.Display(item)
282}
283
284// RunDatabaseResize resizes a database cluster
285func RunDatabaseResize(c *CmdConfig) error {
286	if len(c.Args) == 0 {
287		return doctl.NewMissingArgsErr(c.NS)
288	}
289
290	id := c.Args[0]
291
292	r, err := buildDatabaseResizeRequestFromArgs(c)
293	if err != nil {
294		return err
295	}
296
297	return c.Databases().Resize(id, r)
298}
299
300func buildDatabaseResizeRequestFromArgs(c *CmdConfig) (*godo.DatabaseResizeRequest, error) {
301	r := &godo.DatabaseResizeRequest{}
302
303	numNodes, err := c.Doit.GetInt(c.NS, doctl.ArgDatabaseNumNodes)
304	if err != nil {
305		return nil, err
306	}
307	r.NumNodes = numNodes
308
309	size, err := c.Doit.GetString(c.NS, doctl.ArgSizeSlug)
310	if err != nil {
311		return nil, err
312	}
313	r.SizeSlug = size
314
315	return r, nil
316}
317
318// RunDatabaseMigrate migrates a database cluster to a new region
319func RunDatabaseMigrate(c *CmdConfig) error {
320	if len(c.Args) == 0 {
321		return doctl.NewMissingArgsErr(c.NS)
322	}
323
324	id := c.Args[0]
325
326	r, err := buildDatabaseMigrateRequestFromArgs(c)
327	if err != nil {
328		return err
329	}
330
331	return c.Databases().Migrate(id, r)
332}
333
334func buildDatabaseMigrateRequestFromArgs(c *CmdConfig) (*godo.DatabaseMigrateRequest, error) {
335	r := &godo.DatabaseMigrateRequest{}
336
337	region, err := c.Doit.GetString(c.NS, doctl.ArgRegionSlug)
338	if err != nil {
339		return nil, err
340	}
341	r.Region = region
342
343	privateNetworkUUID, err := c.Doit.GetString(c.NS, doctl.ArgPrivateNetworkUUID)
344	if err != nil {
345		return nil, err
346	}
347	r.PrivateNetworkUUID = privateNetworkUUID
348
349	return r, nil
350}
351
352func databaseMaintenanceWindow() *Command {
353	cmd := &Command{
354		Command: &cobra.Command{
355			Use:     "maintenance-window",
356			Aliases: []string{"maintenance", "mw", "main"},
357			Short:   "Display commands for scheduling automatic maintenance on your database cluster",
358			Long: `The ` + "`" + `doctl databases maintenance-window` + "`" + ` commands allow you to schedule, and check the schedule of, maintenance windows for your databases.
359
360Maintenance 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.`,
361		},
362	}
363
364	CmdBuilder(cmd, RunDatabaseMaintenanceGet, "get <database-id>",
365		"Retrieve details about a database cluster's maintenance windows", `This command retrieves the following information on currently-scheduled maintenance windows for the specified database cluster:
366
367- The day of the week the maintenance window occurs
368- The hour in UTC when maintenance updates will be applied, in 24 hour format (e.g. "16:00")
369- A boolean representing whether maintence updates are currently pending
370
371To see a list of your databases and their IDs, run `+"`"+`doctl databases list`+"`"+`.`, Writer, aliasOpt("g"),
372		displayerType(&displayers.DatabaseMaintenanceWindow{}))
373
374	cmdDatabaseCreate := CmdBuilder(cmd, RunDatabaseMaintenanceUpdate,
375		"update <database-id>", "Update the maintenance window for a database cluster", `This command allows you to update the maintenance window for the specified database cluster.
376
377Maintenance 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.
378
379To change the maintenance window for your database cluster, specify a day of the week and an hour of that day during which you would prefer such maintenance would occur.
380
381	doctl databases maintenance-window ca9f591d-f38h-5555-a0ef-1c02d1d1e35 update --day tuesday --hour 16:00
382
383To see a list of your databases and their IDs, run `+"`"+`doctl databases list`+"`"+`.`, Writer, aliasOpt("u"))
384	AddStringFlag(cmdDatabaseCreate, doctl.ArgDatabaseMaintenanceDay, "", "",
385		"The day of the week the maintenance window occurs (e.g. 'tuesday')", requiredOpt())
386	AddStringFlag(cmdDatabaseCreate, doctl.ArgDatabaseMaintenanceHour, "", "",
387		"The hour in UTC when maintenance updates will be applied, in 24 hour format (e.g. '16:00')", requiredOpt())
388
389	return cmd
390}
391
392// Database Maintenance Window
393
394// RunDatabaseMaintenanceGet retrieves the maintenance window info for a database cluster
395func RunDatabaseMaintenanceGet(c *CmdConfig) error {
396	if len(c.Args) == 0 {
397		return doctl.NewMissingArgsErr(c.NS)
398	}
399
400	id := c.Args[0]
401
402	window, err := c.Databases().GetMaintenance(id)
403	if err != nil {
404		return err
405	}
406
407	return displayDatabaseMaintenanceWindow(c, *window)
408}
409
410func displayDatabaseMaintenanceWindow(c *CmdConfig, mw do.DatabaseMaintenanceWindow) error {
411	item := &displayers.DatabaseMaintenanceWindow{DatabaseMaintenanceWindow: mw}
412	return c.Display(item)
413}
414
415// RunDatabaseMaintenanceUpdate updates the maintenance window info for a database cluster
416func RunDatabaseMaintenanceUpdate(c *CmdConfig) error {
417	if len(c.Args) == 0 {
418		return doctl.NewMissingArgsErr(c.NS)
419	}
420
421	id := c.Args[0]
422	r, err := buildDatabaseUpdateMaintenanceRequestFromArgs(c)
423	if err != nil {
424		return err
425	}
426
427	return c.Databases().UpdateMaintenance(id, r)
428}
429
430func buildDatabaseUpdateMaintenanceRequestFromArgs(c *CmdConfig) (*godo.DatabaseUpdateMaintenanceRequest, error) {
431	r := &godo.DatabaseUpdateMaintenanceRequest{}
432
433	day, err := c.Doit.GetString(c.NS, doctl.ArgDatabaseMaintenanceDay)
434	if err != nil {
435		return nil, err
436	}
437	r.Day = strings.ToLower(day)
438
439	hour, err := c.Doit.GetString(c.NS, doctl.ArgDatabaseMaintenanceHour)
440	if err != nil {
441		return nil, err
442	}
443	r.Hour = hour
444
445	return r, nil
446}
447
448func databaseUser() *Command {
449	cmd := &Command{
450		Command: &cobra.Command{
451			Use:     "user",
452			Aliases: []string{"u"},
453			Short:   "Display commands for managing database users",
454			Long: `The commands under ` + "`" + `doctl databases user` + "`" + ` allow you to view details for, and create, database users.
455
456Database user accounts are scoped to one database cluster, to which they have full admin access, and are given an automatically-generated password.`,
457		},
458	}
459
460	userDetailsDesc := `
461
462- The username for the user
463- The password for the user
464- The user's role. The value will be either "primary" or "normal".
465
466Primary user accounts are created by DigitalOcean at database cluster creation time and can't be deleted. Normal user accounts are created by you. Both have administrative privileges on the database cluster.
467
468To retrieve a list of your databases and their IDs, call ` + "`" + `doctl databases list` + "`" + `.`
469	CmdBuilder(cmd, RunDatabaseUserList, "list <database-id>", "Retrieve list of database users",
470		`This command retrieves a list of users for the specified database with the following details:`+userDetailsDesc, Writer, aliasOpt("ls"), displayerType(&displayers.DatabaseUsers{}))
471	CmdBuilder(cmd, RunDatabaseUserGet, "get <database-id> <user-name>",
472		"Retrieve details about a database user", `This command retrieves the following details about the specified user:`+userDetailsDesc+`
473
474To retrieve a list of database users for a database, call `+"`"+`doctl databases user list <database-id>`+"`"+`.`, Writer, aliasOpt("g"),
475		displayerType(&displayers.DatabaseUsers{}))
476	cmdDatabaseUserCreate := CmdBuilder(cmd, RunDatabaseUserCreate, "create <database-id> <user-name>",
477		"Create a database user", `This command creates a user with the username you specify, who will be granted access to the database cluster you specify.
478
479The user will be created with the role set to `+"`"+`normal`+"`"+`, and given an automatically-generated password.
480
481To retrieve a list of your databases and their IDs, call `+"`"+`doctl databases list`+"`"+`.`, Writer, aliasOpt("c"))
482
483	AddStringFlag(cmdDatabaseUserCreate, doctl.ArgDatabaseUserMySQLAuthPlugin, "", "",
484		"set auth mode for MySQL users")
485
486	CmdBuilder(cmd, RunDatabaseUserResetAuth, "reset <database-id> <user-name> <new-auth-mode>",
487		"Resets a user's auth", "This command resets the auth password or the MySQL auth plugin for a given user. It will return the new user credentials. When resetting MySQL auth, valid values for `<new-auth-mode>` are `caching_sha2_password` and `mysql_native_password`.", Writer, aliasOpt("rs"))
488
489	cmdDatabaseUserDelete := CmdBuilder(cmd, RunDatabaseUserDelete,
490		"delete <database-id> <user-id>", "Delete a database user", `This command deletes the user with the username you specify, whose account was given access to the database cluster you specify.
491
492To retrieve a list of your databases and their IDs, call `+"`"+`doctl databases list`+"`"+`.`, Writer, aliasOpt("rm"))
493	AddBoolFlag(cmdDatabaseUserDelete, doctl.ArgForce, doctl.ArgShortForce, false, "Delete the user without a confirmation prompt")
494
495	return cmd
496}
497
498// Database Users
499
500// RunDatabaseUserList retrieves a list of users for specific database cluster
501func RunDatabaseUserList(c *CmdConfig) error {
502	if len(c.Args) == 0 {
503		return doctl.NewMissingArgsErr(c.NS)
504	}
505
506	id := c.Args[0]
507
508	users, err := c.Databases().ListUsers(id)
509	if err != nil {
510		return err
511	}
512
513	return displayDatabaseUsers(c, users...)
514}
515
516// RunDatabaseUserGet retrieves a database user for a specific database cluster
517func RunDatabaseUserGet(c *CmdConfig) error {
518	if len(c.Args) < 2 {
519		return doctl.NewMissingArgsErr(c.NS)
520	}
521
522	databaseID := c.Args[0]
523	userID := c.Args[1]
524
525	user, err := c.Databases().GetUser(databaseID, userID)
526	if err != nil {
527		return err
528	}
529
530	return displayDatabaseUsers(c, *user)
531}
532
533// RunDatabaseUserCreate creates a database user for a database cluster
534func RunDatabaseUserCreate(c *CmdConfig) error {
535	if len(c.Args) < 2 {
536		return doctl.NewMissingArgsErr(c.NS)
537	}
538
539	var (
540		databaseID = c.Args[0]
541		userName   = c.Args[1]
542	)
543
544	req := &godo.DatabaseCreateUserRequest{Name: userName}
545
546	authMode, err := c.Doit.GetString(c.NS, doctl.ArgDatabaseUserMySQLAuthPlugin)
547	if err != nil {
548		return err
549	}
550
551	if authMode != "" {
552		req.MySQLSettings = &godo.DatabaseMySQLUserSettings{
553			AuthPlugin: authMode,
554		}
555	}
556
557	user, err := c.Databases().CreateUser(databaseID, req)
558	if err != nil {
559		return err
560	}
561
562	return displayDatabaseUsers(c, *user)
563}
564
565func RunDatabaseUserResetAuth(c *CmdConfig) error {
566	if len(c.Args) < 2 {
567		return doctl.NewMissingArgsErr(c.NS)
568	}
569
570	var (
571		databaseID = c.Args[0]
572		userName   = c.Args[1]
573	)
574
575	database, err := c.Databases().Get(databaseID)
576
577	if err != nil {
578		return err
579	}
580
581	var req *godo.DatabaseResetUserAuthRequest
582	if strings.ToLower(database.EngineSlug) == "mysql" {
583		if len(c.Args) < 3 {
584			return doctl.NewMissingArgsErr(c.NS)
585		}
586		authMode := c.Args[2]
587		req = &godo.DatabaseResetUserAuthRequest{
588			MySQLSettings: &godo.DatabaseMySQLUserSettings{
589				AuthPlugin: authMode,
590			},
591		}
592	} else {
593		req = &godo.DatabaseResetUserAuthRequest{}
594	}
595
596	user, err := c.Databases().ResetUserAuth(databaseID, userName, req)
597	if err != nil {
598		return err
599	}
600
601	return displayDatabaseUsers(c, *user)
602}
603
604// RunDatabaseUserDelete deletes a database user
605func RunDatabaseUserDelete(c *CmdConfig) error {
606	if len(c.Args) < 2 {
607		return doctl.NewMissingArgsErr(c.NS)
608	}
609
610	force, err := c.Doit.GetBool(c.NS, doctl.ArgForce)
611	if err != nil {
612		return err
613	}
614
615	if force || AskForConfirmDelete("database user", 1) == nil {
616		databaseID := c.Args[0]
617		userID := c.Args[1]
618		return c.Databases().DeleteUser(databaseID, userID)
619	}
620
621	return errOperationAborted
622}
623
624func displayDatabaseUsers(c *CmdConfig, users ...do.DatabaseUser) error {
625	item := &displayers.DatabaseUsers{DatabaseUsers: users}
626	return c.Display(item)
627}
628
629func databasePool() *Command {
630	cmd := &Command{
631		Command: &cobra.Command{
632			Use:     "pool",
633			Aliases: []string{"p"},
634			Short:   "Display commands for managing connection pools",
635			Long: `The subcommands under ` + "`" + `doctl databases pool` + "`" + ` are for managing connection pools for your database cluster.
636
637A connection pool may be useful if your database:
638
639- Typically handles a large number of idle connections,
640- Has wide variability in the possible number of connections at any given time,
641- Drops connections due to max connection limits, or
642- Experiences performance issues due to high CPU usage.
643
644Connection pools can be created and deleted with these commands, or you can simply retrieve information about them.`,
645		},
646	}
647
648	connectionPoolDetails := `
649
650- The username of the database user account that the connection pool uses
651- The name of the connection pool
652- The size of the connection pool, i.e. the number of connections that will be allocated
653- The database within the cluster for which the connection pool is used
654- The pool mode for the connection pool, which can be 'session', 'transaction', or 'statement'
655- A connection string for the connection pool`
656	getPoolDetails := `
657
658You can get a list of existing connection pools by calling:
659
660	doctl databases pool list
661
662You can get a list of existing database clusters and their IDs by calling:
663
664	doctl databases list`
665	CmdBuilder(cmd, RunDatabasePoolList, "list <database-id>", "List connection pools for a database cluster", `This command lists the existing connection pools for the specified database. The following information will be returned:`+connectionPoolDetails,
666		Writer, aliasOpt("ls"), displayerType(&displayers.DatabasePools{}))
667	CmdBuilder(cmd, RunDatabasePoolGet, "get <database-id> <pool-name>",
668		"Retrieve information about a database connection pool", `This command retrieves the following information about the specified connection pool for the specified database cluster:`+connectionPoolDetails+getPoolDetails, Writer, aliasOpt("g"),
669		displayerType(&displayers.DatabasePools{}))
670	cmdDatabasePoolCreate := CmdBuilder(cmd, RunDatabasePoolCreate,
671		"create <database-id> <pool-name>", "Create a connection pool for a database", `This command creates a connection pool for the specified database cluster and gives it the specified name.
672
673You must also use flags to specify the target database, pool size, and database user's username that will be used for the pool. An example call would be:
674
675	pool create ca9f591d-fb58-5555-a0ef-1c02d1d1e352 mypool --db defaultdb --size 10 --user doadmin
676
677The pool size is the minimum number of connections the pool can handle. The maximum pool size varies based on the size of the cluster.
678
679Theres no perfect formula to determine how large your pool should be, but there are a few good guidelines to keep in mind:
680
681- A large pool will stress your database at similar levels as that number of clients would alone.
682- A pool thats much smaller than the number of clients communicating with the database can act as a bottleneck, reducing the rate when your database receives and responds to transactions.
683
684We recommend starting with a pool size of about half your available connections and adjusting later based on performance. If you see slow query responses, check the CPU usage on the databases Overview tab. We recommend decreasing your pool size if CPU usage is high, and increasing your pool size if its low.`+getPoolDetails, Writer,
685		aliasOpt("c"))
686	AddStringFlag(cmdDatabasePoolCreate, doctl.ArgDatabasePoolMode, "",
687		"transaction", "The pool mode for the connection pool, e.g. `session`, `transaction`, and `statement`")
688	AddIntFlag(cmdDatabasePoolCreate, doctl.ArgSizeSlug, "", 0, "pool size",
689		requiredOpt())
690	AddStringFlag(cmdDatabasePoolCreate, doctl.ArgDatabasePoolUserName, "", "",
691		"The username for the database user", requiredOpt())
692	AddStringFlag(cmdDatabasePoolCreate, doctl.ArgDatabasePoolDBName, "", "",
693		"The name of the specific database within the database cluster", requiredOpt())
694
695	cmdDatabasePoolDelete := CmdBuilder(cmd, RunDatabasePoolDelete,
696		"delete <database-id> <pool-name>", "Delete a connection pool for a database", `This command deletes the specified connection pool for the specified database cluster.`+getPoolDetails, Writer,
697		aliasOpt("rm"))
698	AddBoolFlag(cmdDatabasePoolDelete, doctl.ArgForce, doctl.ArgShortForce,
699		false, "Delete connection pool without confirmation prompt")
700
701	return cmd
702}
703
704// Database Pools
705
706// RunDatabasePoolList retrieves a list of pools for specific database cluster
707func RunDatabasePoolList(c *CmdConfig) error {
708	if len(c.Args) == 0 {
709		return doctl.NewMissingArgsErr(c.NS)
710	}
711
712	id := c.Args[0]
713
714	pools, err := c.Databases().ListPools(id)
715	if err != nil {
716		return err
717	}
718
719	return displayDatabasePools(c, pools...)
720}
721
722// RunDatabasePoolGet retrieves a database pool for a specific database cluster
723func RunDatabasePoolGet(c *CmdConfig) error {
724	if len(c.Args) < 2 {
725		return doctl.NewMissingArgsErr(c.NS)
726	}
727
728	databaseID := c.Args[0]
729	poolID := c.Args[1]
730
731	pool, err := c.Databases().GetPool(databaseID, poolID)
732	if err != nil {
733		return err
734	}
735
736	return displayDatabasePools(c, *pool)
737}
738
739// RunDatabasePoolCreate creates a database pool for a database cluster
740func RunDatabasePoolCreate(c *CmdConfig) error {
741	if len(c.Args) < 2 {
742		return doctl.NewMissingArgsErr(c.NS)
743	}
744
745	databaseID := c.Args[0]
746	r, err := buildDatabaseCreatePoolRequestFromArgs(c)
747	if err != nil {
748		return err
749	}
750
751	pool, err := c.Databases().CreatePool(databaseID, r)
752	if err != nil {
753		return err
754	}
755
756	return displayDatabasePools(c, *pool)
757}
758
759func buildDatabaseCreatePoolRequestFromArgs(c *CmdConfig) (*godo.DatabaseCreatePoolRequest, error) {
760	req := &godo.DatabaseCreatePoolRequest{Name: c.Args[1]}
761
762	mode, err := c.Doit.GetString(c.NS, doctl.ArgDatabasePoolMode)
763	if err != nil {
764		return nil, err
765	}
766	req.Mode = mode
767
768	size, err := c.Doit.GetInt(c.NS, doctl.ArgDatabasePoolSize)
769	if err != nil {
770		return nil, err
771	}
772	req.Size = size
773
774	db, err := c.Doit.GetString(c.NS, doctl.ArgDatabasePoolDBName)
775	if err != nil {
776		return nil, err
777	}
778	req.Database = db
779
780	user, err := c.Doit.GetString(c.NS, doctl.ArgDatabasePoolUserName)
781	if err != nil {
782		return nil, err
783	}
784	req.User = user
785
786	return req, nil
787}
788
789// RunDatabasePoolDelete deletes a database pool
790func RunDatabasePoolDelete(c *CmdConfig) error {
791	if len(c.Args) < 2 {
792		return doctl.NewMissingArgsErr(c.NS)
793	}
794
795	force, err := c.Doit.GetBool(c.NS, doctl.ArgForce)
796	if err != nil {
797		return err
798	}
799
800	if force || AskForConfirmDelete("database pool", 1) == nil {
801		databaseID := c.Args[0]
802		poolID := c.Args[1]
803		return c.Databases().DeletePool(databaseID, poolID)
804	}
805
806	return errOperationAborted
807}
808
809func displayDatabasePools(c *CmdConfig, pools ...do.DatabasePool) error {
810	item := &displayers.DatabasePools{DatabasePools: pools}
811	return c.Display(item)
812}
813
814func databaseDB() *Command {
815	getClusterList := `
816
817You can get a list of existing database clusters and their IDs by calling:
818
819	doctl databases list`
820	getDBList := `
821
822You can get a list of existing databases that are hosted within a cluster by calling:
823
824	doctl databases db list {cluster-id}`
825	cmd := &Command{
826		Command: &cobra.Command{
827			Use:   "db",
828			Short: "Display commands for managing individual databases within a cluster",
829			Long: `The subcommands under ` + "`" + `doctl databases db` + "`" + ` are for managing specific databases that are served by a database cluster.
830
831You can use these commands to create and delete databases within a cluster, or simply get information about them.` + getClusterList,
832		},
833	}
834
835	CmdBuilder(cmd, RunDatabaseDBList, "list <database-id>", "Retrieve a list of databases within a cluster", "This command retrieves the names of all databases being hosted in the specified database cluster."+getClusterList, Writer,
836		aliasOpt("ls"), displayerType(&displayers.DatabaseDBs{}))
837	CmdBuilder(cmd, RunDatabaseDBGet, "get <database-id> <db-name>", "Retrieve the name of a database within a cluster", "This command retrieves the name of the specified database hosted in the specified database cluster."+getClusterList+getDBList,
838		Writer, aliasOpt("g"), displayerType(&displayers.DatabaseDBs{}))
839	CmdBuilder(cmd, RunDatabaseDBCreate, "create <database-id> <db-name>",
840		"Create a database within a cluster", "This command creates a database with the specified name in the specified database cluster."+getClusterList, Writer, aliasOpt("c"))
841
842	cmdDatabaseDBDelete := CmdBuilder(cmd, RunDatabaseDBDelete,
843		"delete <database-id> <db-name>", "Delete the specified database from the cluster", "This command deletes the specified database from the specified database cluster."+getClusterList+getDBList, Writer, aliasOpt("rm"))
844	AddBoolFlag(cmdDatabaseDBDelete, doctl.ArgForce, doctl.ArgShortForce,
845		false, "Delete the database without a confirmation prompt")
846
847	return cmd
848}
849
850// Database DBs
851
852// RunDatabaseDBList retrieves a list of databases for specific database cluster
853func RunDatabaseDBList(c *CmdConfig) error {
854	if len(c.Args) == 0 {
855		return doctl.NewMissingArgsErr(c.NS)
856	}
857
858	id := c.Args[0]
859
860	dbs, err := c.Databases().ListDBs(id)
861	if err != nil {
862		return err
863	}
864
865	return displayDatabaseDBs(c, dbs...)
866}
867
868// RunDatabaseDBGet retrieves a database for a specific database cluster
869func RunDatabaseDBGet(c *CmdConfig) error {
870	if len(c.Args) < 2 {
871		return doctl.NewMissingArgsErr(c.NS)
872	}
873
874	databaseID := c.Args[0]
875	dbID := c.Args[1]
876
877	db, err := c.Databases().GetDB(databaseID, dbID)
878	if err != nil {
879		return err
880	}
881
882	return displayDatabaseDBs(c, *db)
883}
884
885// RunDatabaseDBCreate creates a database for a database cluster
886func RunDatabaseDBCreate(c *CmdConfig) error {
887	if len(c.Args) < 2 {
888		return doctl.NewMissingArgsErr(c.NS)
889	}
890
891	databaseID := c.Args[0]
892	req := &godo.DatabaseCreateDBRequest{Name: c.Args[1]}
893
894	db, err := c.Databases().CreateDB(databaseID, req)
895	if err != nil {
896		return err
897	}
898
899	return displayDatabaseDBs(c, *db)
900}
901
902// RunDatabaseDBDelete deletes a database
903func RunDatabaseDBDelete(c *CmdConfig) error {
904	if len(c.Args) < 2 {
905		return doctl.NewMissingArgsErr(c.NS)
906	}
907
908	force, err := c.Doit.GetBool(c.NS, doctl.ArgForce)
909	if err != nil {
910		return err
911	}
912
913	if force || AskForConfirmDelete("database", 1) == nil {
914		databaseID := c.Args[0]
915		dbID := c.Args[1]
916		return c.Databases().DeleteDB(databaseID, dbID)
917	}
918
919	return errOperationAborted
920}
921
922func displayDatabaseDBs(c *CmdConfig, dbs ...do.DatabaseDB) error {
923	item := &displayers.DatabaseDBs{DatabaseDBs: dbs}
924	return c.Display(item)
925}
926
927func databaseReplica() *Command {
928	cmd := &Command{
929		Command: &cobra.Command{
930			Use:     "replica",
931			Aliases: []string{"rep", "r"},
932			Short:   "Display commands to manage read-only database replicas",
933			Long: `The subcommands under ` + "`" + `doctl databases replica` + "`" + ` enable the management of read-only replicas associated with a database cluster.
934
935In addition to primary nodes in a database cluster, you can create up to 2 read-only replica nodes (also referred to as "standby nodes") to maintain high availability.`,
936		},
937	}
938	howToGetReplica := `
939
940This command requires that you pass in the replica's name, which you can retrieve by querying a database ID:
941
942	doctl databases replica list ca9f591d-5555-5555-a0ef-1c02d1d1e352`
943	replicaDetails := `
944
945- The name of the replica
946- The region where the database cluster is located (e.g. ` + "`" + `nyc3` + "`" + `, ` + "`" + `sfo2` + "`" + `)
947- The status of the replica (possible values are ` + "`" + `forking` + "`" + ` and ` + "`" + `active` + "`" + `)
948`
949	CmdBuilder(cmd, RunDatabaseReplicaList, "list <database-id>", "Retrieve list of read-only database replicas", `Lists the following details for read-only replicas for the specified database cluster.`+replicaDetails+databaseListDetails,
950		Writer, aliasOpt("ls"),
951		displayerType(&displayers.DatabaseReplicas{}))
952	CmdBuilder(cmd, RunDatabaseReplicaGet, "get <database-id> <replica-name>", "Retrieve information about a read-only database replica",
953		`Gets the following details for the specified read-only replica for the specified database cluster:
954
955- The name of the replica
956- Information required to connect to the read-only replica
957- The region where the database cluster is located (e.g. `+"`"+`nyc3`+"`"+`, `+"`"+`sfo2`+"`"+`)
958- The status of the replica (possible values are `+"`"+`creating`+"`"+`, `+"`"+`forking`+"`"+`, or `+"`"+`active`+"`"+`)
959- A time value given in ISO8601 combined date and time format that represents when the read-only replica was created.`+howToGetReplica+databaseListDetails,
960		Writer, aliasOpt("g"),
961		displayerType(&displayers.DatabaseReplicas{}))
962
963	cmdDatabaseReplicaCreate := CmdBuilder(cmd, RunDatabaseReplicaCreate,
964		"create <database-id> <replica-name>", "Create a read-only database replica", `This command creates a read-only database replica for the specified database cluster, giving it the specified name.`+databaseListDetails,
965		Writer, aliasOpt("c"))
966	AddStringFlag(cmdDatabaseReplicaCreate, doctl.ArgRegionSlug, "",
967		defaultDatabaseRegion, "Specifies the region (e.g. nyc3, sfo2) in which to create the replica")
968	AddStringFlag(cmdDatabaseReplicaCreate, doctl.ArgSizeSlug, "",
969		defaultDatabaseNodeSize, "Specifies the machine size for the replica (e.g. db-s-1vcpu-1gb). Must be the same or equal to the original.")
970	AddStringFlag(cmdDatabaseReplicaCreate, doctl.ArgPrivateNetworkUUID, "",
971		"", "The UUID of a VPC to create the replica in; the default VPC for the region will be used if excluded")
972
973	cmdDatabaseReplicaDelete := CmdBuilder(cmd, RunDatabaseReplicaDelete,
974		"delete <database-id> <replica-name>", "Delete a read-only database replica",
975		`Delete the specified read-only replica for the specified database cluster.`+howToGetReplica+databaseListDetails,
976		Writer, aliasOpt("rm"))
977	AddBoolFlag(cmdDatabaseReplicaDelete, doctl.ArgForce, doctl.ArgShortForce,
978		false, "Deletes the replica without a confirmation prompt")
979
980	CmdBuilder(cmd, RunDatabaseReplicaConnectionGet,
981		"connection <database-id> <replica-name>",
982		"Retrieve information for connecting to a read-only database replica",
983		`This command retrieves information for connecting to the specified read-only database replica in the specified database cluster`+howToGetReplica+databaseListDetails, Writer, aliasOpt("conn"))
984
985	return cmd
986}
987
988// Database Replicas
989
990// RunDatabaseReplicaList retrieves a list of replicas for specific database cluster
991func RunDatabaseReplicaList(c *CmdConfig) error {
992	if len(c.Args) == 0 {
993		return doctl.NewMissingArgsErr(c.NS)
994	}
995
996	id := c.Args[0]
997
998	replicas, err := c.Databases().ListReplicas(id)
999	if err != nil {
1000		return err
1001	}
1002
1003	return displayDatabaseReplicas(c, true, replicas...)
1004}
1005
1006// RunDatabaseReplicaGet retrieves a read-only replica for a specific database cluster
1007func RunDatabaseReplicaGet(c *CmdConfig) error {
1008	if len(c.Args) < 2 {
1009		return doctl.NewMissingArgsErr(c.NS)
1010	}
1011
1012	databaseID := c.Args[0]
1013	replicaID := c.Args[1]
1014
1015	replica, err := c.Databases().GetReplica(databaseID, replicaID)
1016	if err != nil {
1017		return err
1018	}
1019
1020	return displayDatabaseReplicas(c, false, *replica)
1021}
1022
1023// RunDatabaseReplicaCreate creates a read-only replica for a database cluster
1024func RunDatabaseReplicaCreate(c *CmdConfig) error {
1025	if len(c.Args) < 2 {
1026		return doctl.NewMissingArgsErr(c.NS)
1027	}
1028
1029	databaseID := c.Args[0]
1030	r, err := buildDatabaseCreateReplicaRequestFromArgs(c)
1031	if err != nil {
1032		return err
1033	}
1034
1035	replica, err := c.Databases().CreateReplica(databaseID, r)
1036	if err != nil {
1037		return err
1038	}
1039
1040	return displayDatabaseReplicas(c, false, *replica)
1041}
1042
1043func buildDatabaseCreateReplicaRequestFromArgs(c *CmdConfig) (*godo.DatabaseCreateReplicaRequest, error) {
1044	r := &godo.DatabaseCreateReplicaRequest{Name: c.Args[1]}
1045
1046	size, err := c.Doit.GetString(c.NS, doctl.ArgSizeSlug)
1047	if err != nil {
1048		return nil, err
1049	}
1050	r.Size = size
1051
1052	region, err := c.Doit.GetString(c.NS, doctl.ArgRegionSlug)
1053	if err != nil {
1054		return nil, err
1055	}
1056	r.Region = region
1057
1058	privateNetworkUUID, err := c.Doit.GetString(c.NS, doctl.ArgPrivateNetworkUUID)
1059	if err != nil {
1060		return nil, err
1061	}
1062	r.PrivateNetworkUUID = privateNetworkUUID
1063
1064	return r, nil
1065}
1066
1067// RunDatabaseReplicaDelete deletes a read-only replica
1068func RunDatabaseReplicaDelete(c *CmdConfig) error {
1069	if len(c.Args) < 2 {
1070		return doctl.NewMissingArgsErr(c.NS)
1071	}
1072
1073	force, err := c.Doit.GetBool(c.NS, doctl.ArgForce)
1074	if err != nil {
1075		return err
1076	}
1077
1078	if force || AskForConfirmDelete("database replica", 1) == nil {
1079		databaseID := c.Args[0]
1080		replicaID := c.Args[1]
1081		return c.Databases().DeleteReplica(databaseID, replicaID)
1082	}
1083
1084	return errOperationAborted
1085}
1086
1087func displayDatabaseReplicas(c *CmdConfig, short bool, replicas ...do.DatabaseReplica) error {
1088	item := &displayers.DatabaseReplicas{
1089		DatabaseReplicas: replicas,
1090		Short:            short,
1091	}
1092	return c.Display(item)
1093}
1094
1095// RunDatabaseReplicaConnectionGet gets read-only replica connection info
1096func RunDatabaseReplicaConnectionGet(c *CmdConfig) error {
1097	if len(c.Args) == 0 {
1098		return doctl.NewMissingArgsErr(c.NS)
1099	}
1100
1101	databaseID := c.Args[0]
1102	replicaID := c.Args[1]
1103	connInfo, err := c.Databases().GetReplicaConnection(databaseID, replicaID)
1104	if err != nil {
1105		return err
1106	}
1107
1108	return displayDatabaseReplicaConnection(c, *connInfo)
1109}
1110
1111func displayDatabaseReplicaConnection(c *CmdConfig, conn do.DatabaseConnection) error {
1112	item := &displayers.DatabaseConnection{DatabaseConnection: conn}
1113	return c.Display(item)
1114}
1115
1116func sqlMode() *Command {
1117	cmd := &Command{
1118		Command: &cobra.Command{
1119			Use:     "sql-mode",
1120			Aliases: []string{"sm"},
1121			Short:   "Display commands to configure a MySQL database cluster's SQL modes",
1122			Long:    "The subcommands of `doctl databases sql-mode` are used to view and configure a MySQL database cluster's global SQL modes.",
1123		},
1124	}
1125
1126	getSqlModeDesc := "This command displays the the configured SQL modes for the specified MySQL database cluster."
1127	CmdBuilder(cmd, RunDatabaseGetSQLModes, "get <database-id>",
1128		"Get a MySQL database cluster's SQL modes", getSqlModeDesc, Writer,
1129		displayerType(&displayers.DatabaseSQLModes{}), aliasOpt("g"))
1130	setSqlModeDesc := `This command configures the SQL modes for the specified MySQL database cluster. The SQL modes should be provided as a space separated list.
1131
1132This will replace the existing SQL mode configuration completely. Include all of the current values when adding a new one.
1133`
1134	CmdBuilder(cmd, RunDatabaseSetSQLModes, "set <database-id> <sql-mode-1> ... <sql-mode-n>",
1135		"Set a MySQL database cluster's SQL modes", setSqlModeDesc, Writer, aliasOpt("s"))
1136
1137	return cmd
1138}
1139
1140// RunDatabaseGetSQLModes gets the sql modes set on the database
1141func RunDatabaseGetSQLModes(c *CmdConfig) error {
1142	err := ensureOneArg(c)
1143	if err != nil {
1144		return err
1145	}
1146
1147	databaseID := c.Args[0]
1148	sqlModes, err := c.Databases().GetSQLMode(databaseID)
1149	if err != nil {
1150		return err
1151	}
1152	return displaySQLModes(c, sqlModes)
1153}
1154
1155func displaySQLModes(c *CmdConfig, sqlModes []string) error {
1156	return c.Display(&displayers.DatabaseSQLModes{
1157		DatabaseSQLModes: sqlModes,
1158	})
1159}
1160
1161// RunDatabaseSetSQLModes sets the sql modes on the database
1162func RunDatabaseSetSQLModes(c *CmdConfig) error {
1163	if len(c.Args) < 2 {
1164		return doctl.NewMissingArgsErr(c.NS)
1165	}
1166
1167	databaseID := c.Args[0]
1168	sqlModes := c.Args[1:]
1169
1170	return c.Databases().SetSQLMode(databaseID, sqlModes...)
1171}
1172
1173func databaseFirewalls() *Command {
1174	cmd := &Command{
1175		Command: &cobra.Command{
1176			Use:     "firewalls",
1177			Aliases: []string{"fw"},
1178			Short:   `Display commands to manage firewall rules (called` + "`" + `trusted sources` + "`" + ` in the control panel) for database clusters`,
1179			Long:    `The subcommands under ` + "`" + `doctl databases firewalls` + "`" + ` enable the management of firewalls for database clusters`,
1180		},
1181	}
1182
1183	firewallRuleDetails := `
1184This command lists the following details for each firewall rule in a given database:
1185
1186	- The UUID of the firewall rule.
1187	- The Cluster UUID for the database cluster to which the rule is applied.
1188	- The Type of resource that the firewall rule allows to access the database cluster. The possible values are: "droplet", "k8s", "ip_addr", "tag", or "app".
1189	- The Value, which is either the ID of the specific resource, the name of a tag applied to a group of resources, or the IP address that the firewall rule allows to access the database cluster.
1190	- The Time value given in ISO8601 combined date and time format that represents when the firewall rule was created.
1191	`
1192	databaseFirewallRuleDetails := `
1193
1194This command requires the ID of a database cluster, which you can retrieve by calling:
1195
1196	doctl databases list`
1197
1198	databaseFirewallRulesTxt := "A comma-separated list of firewall rules of format type:value, e.g.: `type:value`"
1199
1200	databaseFirewallUpdateDetails := `
1201Use this command to replace the firewall rules of a given database. This command requires the ID of a database cluster, which you can retrieve by calling:
1202
1203	doctl databases list
1204
1205This command also requires a --rule flag. You can pass in multiple --rule flags. Each rule passed in to the --rule flag must be of format type:value
1206	- "type" is the type of resource that the firewall rule allows to access the database cluster. The possible values for type are:  "droplet", "k8s", "ip_addr", "tag", or "app"
1207	- "value" is either the ID of the specific resource, the name of a tag applied to a group of resources, or the IP address that the firewall rule allows to access the database cluster
1208
1209For example:
1210
1211	doctl databases firewalls replace d1234-1c12-1234-b123-12345c4789 --rule tag:backend --rule ip_addr:0.0.0.0
1212
1213	or
1214
1215	databases firewalls replace d1234-1c12-1234-b123-12345c4789 --rule tag:backend,ip_addr:0.0.0.0
1216
1217This would replace the firewall rules for database of id d1234-1c12-1234-b123-12345c4789 with the two rules passed above (tag:backend, ip_addr:0.0.0.0)
1218	`
1219
1220	databaseFirewallAddDetails :=
1221		`
1222Use this command to append a single rule to the existing firewall rules of a given database. This command requires the ID of a database cluster, which you can retrieve by calling:
1223
1224	doctl databases list
1225
1226This command also requires a --rule flag. Each rule passed in to the --rule flag must be of format type:value
1227	- "type" is the type of resource that the firewall rule allows to access the database cluster. The possible values for type are:  "droplet", "k8s", "ip_addr", "tag", or "app"
1228	- "value" is either the ID of the specific resource, the name of a tag applied to a group of resources, or the IP address that the firewall rule allows to access the database cluster
1229
1230For example:
1231
1232	doctl databases firewalls append d1234-1c12-1234-b123-12345c4789 --rule tag:backend
1233
1234This would append the firewall rule "tag:backend" for database of id d1234-1c12-1234-b123-12345c4789`
1235
1236	databaseFirewallRemoveDetails :=
1237		`
1238Use this command to remove an existing, single rule from the list of firewall rules for a given database. This command requires the ID of a database cluster, which you can retrieve by calling:
1239
1240	doctl databases list
1241
1242This command also requires a --uuid flag. You must pass in the UUID of the firewall rule you'd like to remove. You can retrieve the firewall rule's UUIDs by calling:
1243
1244	doctl database firewalls list <db-id>
1245
1246For example:
1247
1248	doctl databases firewalls remove d1234-1c12-1234-b123-12345c4789 --uuid 12345d-1234-123d-123x-123eee456e
1249
1250This would remove the firewall rule of uuid 12345d-1234-123d-123x-123eee456e for database of id d1234-1c12-1234-b123-12345c4789
1251			`
1252
1253	CmdBuilder(cmd, RunDatabaseFirewallRulesList, "list <database-id>", "Retrieve a list of firewall rules for a given database", firewallRuleDetails+databaseFirewallRuleDetails,
1254		Writer, aliasOpt("ls"))
1255
1256	cmdDatabaseFirewallUpdate := CmdBuilder(cmd, RunDatabaseFirewallRulesUpdate, "replace <db-id> --rules type:value [--rule type:value]", "Replaces the firewall rules for a given database. The rules passed in to the --rules flag will replace the firewall rules previously assigned to the database,", databaseFirewallUpdateDetails,
1257		Writer, aliasOpt("r"))
1258	AddStringSliceFlag(cmdDatabaseFirewallUpdate, doctl.ArgDatabaseFirewallRule, "", []string{}, databaseFirewallRulesTxt, requiredOpt())
1259
1260	cmdDatabaseFirewallCreate := CmdBuilder(cmd, RunDatabaseFirewallRulesAppend, "append <db-id> --rule type:value", "Add a database firewall rule to a given database", databaseFirewallAddDetails,
1261		Writer, aliasOpt("a"))
1262	AddStringFlag(cmdDatabaseFirewallCreate, doctl.ArgDatabaseFirewallRule, "", "", "", requiredOpt())
1263
1264	cmdDatabaseFirewallRemove := CmdBuilder(cmd, RunDatabaseFirewallRulesRemove, "remove <firerule-uuid>", "Remove a firewall rule for a given database", databaseFirewallRemoveDetails,
1265		Writer, aliasOpt("rm"))
1266	AddStringFlag(cmdDatabaseFirewallRemove, doctl.ArgDatabaseFirewallRuleUUID, "", "", "", requiredOpt())
1267
1268	return cmd
1269
1270}
1271
1272// displayDatabaseFirewallRules calls Get Firewall Rules to list all current rules.
1273func displayDatabaseFirewallRules(c *CmdConfig, short bool, id string) error {
1274	firewallRules, err := c.Databases().GetFirewallRules(id)
1275	if err != nil {
1276		return err
1277	}
1278
1279	item := &displayers.DatabaseFirewallRules{
1280		DatabaseFirewallRules: firewallRules,
1281	}
1282
1283	return c.Display(item)
1284}
1285
1286// All firewall rules require the databaseID
1287func firewallRulesArgumentCheck(c *CmdConfig) error {
1288	if len(c.Args) == 0 {
1289		return doctl.NewMissingArgsErr(c.NS)
1290	}
1291	if len(c.Args) > 1 {
1292		return doctl.NewTooManyArgsErr(c.NS)
1293	}
1294	return nil
1295}
1296
1297// RunDatabaseFirewallRulesList retrieves a list of firewalls for specific database cluster
1298func RunDatabaseFirewallRulesList(c *CmdConfig) error {
1299	err := firewallRulesArgumentCheck(c)
1300	if err != nil {
1301		return err
1302	}
1303
1304	id := c.Args[0]
1305
1306	return displayDatabaseFirewallRules(c, true, id)
1307}
1308
1309// RunDatabaseFirewallRulesUpdate replaces previous rules with the rules passed in to --rules
1310func RunDatabaseFirewallRulesUpdate(c *CmdConfig) error {
1311	err := firewallRulesArgumentCheck(c)
1312	if err != nil {
1313		return err
1314	}
1315
1316	id := c.Args[0]
1317	r, err := buildDatabaseUpdateFirewallRulesRequestFromArgs(c)
1318	if err != nil {
1319		return err
1320	}
1321
1322	err = c.Databases().UpdateFirewallRules(id, r)
1323	if err != nil {
1324		return err
1325	}
1326
1327	return displayDatabaseFirewallRules(c, true, id)
1328
1329}
1330
1331// buildDatabaseUpdateFirewallRulesRequestFromArgs will ingest the --rules arguments into a DatabaseUpdateFirewallRulesRequest object.
1332func buildDatabaseUpdateFirewallRulesRequestFromArgs(c *CmdConfig) (*godo.DatabaseUpdateFirewallRulesRequest, error) {
1333	r := &godo.DatabaseUpdateFirewallRulesRequest{}
1334
1335	firewallRules, err := c.Doit.GetStringSlice(c.NS, doctl.ArgDatabaseFirewallRule)
1336	if err != nil {
1337		return nil, err
1338	}
1339
1340	if len(firewallRules) == 0 {
1341		return nil, errors.New("Must pass in a key:value pair for the --rule flag")
1342	}
1343
1344	firewallRulesList, err := extractFirewallRules(firewallRules)
1345	if err != nil {
1346		return nil, err
1347	}
1348	r.Rules = firewallRulesList
1349
1350	return r, nil
1351
1352}
1353
1354// extractFirewallRules will ingest the --rules arguments into a list of DatabaseFirewallRule objects.
1355func extractFirewallRules(rulesStringList []string) (rules []*godo.DatabaseFirewallRule, err error) {
1356	for _, rule := range rulesStringList {
1357		pair := strings.SplitN(rule, ":", 2)
1358		if len(pair) != 2 {
1359			return nil, fmt.Errorf("Unexpected input value [%v], must be a key:value pair", pair)
1360		}
1361
1362		firewallRule := new(godo.DatabaseFirewallRule)
1363		firewallRule.Type = pair[0]
1364		firewallRule.Value = pair[1]
1365
1366		rules = append(rules, firewallRule)
1367	}
1368
1369	return rules, nil
1370
1371}
1372
1373// RunDatabaseFirewallRulesAppend creates a firewall rule for a database cluster.
1374//
1375// Any new rules will be appended to the existing rules. If you want to replace
1376// rules, use RunDatabaseFirewallRulesUpdate.
1377func RunDatabaseFirewallRulesAppend(c *CmdConfig) error {
1378	err := firewallRulesArgumentCheck(c)
1379	if err != nil {
1380		return err
1381	}
1382
1383	databaseID := c.Args[0]
1384	firewallRuleArg, err := c.Doit.GetString(c.NS, doctl.ArgDatabaseFirewallRule)
1385	if err != nil {
1386		return err
1387	}
1388
1389	pair := strings.SplitN(firewallRuleArg, ":", 2)
1390	if len(pair) != 2 {
1391		return fmt.Errorf("Unexpected input value [%v], must be a key:value pair", pair)
1392	}
1393
1394	// Slice will house old rules and new rule
1395	allRules := []*godo.DatabaseFirewallRule{}
1396
1397	// Adding new rule to slice.
1398	allRules = append(allRules, &godo.DatabaseFirewallRule{
1399		Type:        pair[0],
1400		Value:       pair[1],
1401		ClusterUUID: databaseID,
1402	})
1403
1404	// Retrieve any existing firewall rules so that we don't destroy existing
1405	// rules in the create request.
1406	oldRules, err := c.Databases().GetFirewallRules(databaseID)
1407	if err != nil {
1408		return err
1409	}
1410
1411	// Add old rules to allRules slice.
1412	for _, rule := range oldRules {
1413
1414		firewallRule := new(godo.DatabaseFirewallRule)
1415		firewallRule.Type = rule.Type
1416		firewallRule.Value = rule.Value
1417		firewallRule.ClusterUUID = rule.ClusterUUID
1418		firewallRule.UUID = rule.UUID
1419
1420		allRules = append(allRules, firewallRule)
1421	}
1422
1423	// Run update firewall rules with old rules + new rule
1424	if err := c.Databases().UpdateFirewallRules(databaseID, &godo.DatabaseUpdateFirewallRulesRequest{
1425		Rules: allRules,
1426	}); err != nil {
1427		return err
1428	}
1429
1430	return displayDatabaseFirewallRules(c, true, databaseID)
1431}
1432
1433// RunDatabaseFirewallRulesRemove removes a firewall rule for a database cluster via Firewall rule UUID
1434func RunDatabaseFirewallRulesRemove(c *CmdConfig) error {
1435	err := firewallRulesArgumentCheck(c)
1436	if err != nil {
1437		return err
1438	}
1439
1440	databaseID := c.Args[0]
1441
1442	firewallRuleUUIDArg, err := c.Doit.GetString(c.NS, doctl.ArgDatabaseFirewallRuleUUID)
1443	if err != nil {
1444		return err
1445	}
1446
1447	// Retrieve any existing firewall rules so that we don't destroy existing
1448	// rules in the create request.
1449	rules, err := c.Databases().GetFirewallRules(databaseID)
1450	if err != nil {
1451		return err
1452	}
1453
1454	// Create a slice of database firewall rules containing only the new rule.
1455	firewallRules := []*godo.DatabaseFirewallRule{}
1456
1457	// only append rules that do not match the firewall rule with uuid to be removed.
1458	for _, rule := range rules {
1459		if rule.UUID != firewallRuleUUIDArg {
1460			firewallRules = append(firewallRules, &godo.DatabaseFirewallRule{
1461				UUID:        rule.UUID,
1462				ClusterUUID: rule.ClusterUUID,
1463				Type:        rule.Type,
1464				Value:       rule.Value,
1465			})
1466		}
1467	}
1468
1469	if err := c.Databases().UpdateFirewallRules(databaseID, &godo.DatabaseUpdateFirewallRulesRequest{
1470		Rules: firewallRules,
1471	}); err != nil {
1472		return err
1473	}
1474
1475	return displayDatabaseFirewallRules(c, true, databaseID)
1476}
1477