1package godo
2
3import (
4	"context"
5	"fmt"
6	"net/http"
7	"time"
8)
9
10const (
11	databaseBasePath        = "/v2/databases"
12	databaseSinglePath      = databaseBasePath + "/%s"
13	databaseResizePath      = databaseBasePath + "/%s/resize"
14	databaseMigratePath     = databaseBasePath + "/%s/migrate"
15	databaseMaintenancePath = databaseBasePath + "/%s/maintenance"
16	databaseBackupsPath     = databaseBasePath + "/%s/backups"
17	databaseUsersPath       = databaseBasePath + "/%s/users"
18	databaseUserPath        = databaseBasePath + "/%s/users/%s"
19	databaseDBPath          = databaseBasePath + "/%s/dbs/%s"
20	databaseDBsPath         = databaseBasePath + "/%s/dbs"
21	databasePoolPath        = databaseBasePath + "/%s/pools/%s"
22	databasePoolsPath       = databaseBasePath + "/%s/pools"
23	databaseReplicaPath     = databaseBasePath + "/%s/replicas/%s"
24	databaseReplicasPath    = databaseBasePath + "/%s/replicas"
25)
26
27// DatabasesService is an interface for interfacing with the databases endpoints
28// of the DigitalOcean API.
29// See: https://developers.digitalocean.com/documentation/v2#databases
30type DatabasesService interface {
31	List(context.Context, *ListOptions) ([]Database, *Response, error)
32	Get(context.Context, string) (*Database, *Response, error)
33	Create(context.Context, *DatabaseCreateRequest) (*Database, *Response, error)
34	Delete(context.Context, string) (*Response, error)
35	Resize(context.Context, string, *DatabaseResizeRequest) (*Response, error)
36	Migrate(context.Context, string, *DatabaseMigrateRequest) (*Response, error)
37	UpdateMaintenance(context.Context, string, *DatabaseUpdateMaintenanceRequest) (*Response, error)
38	ListBackups(context.Context, string, *ListOptions) ([]DatabaseBackup, *Response, error)
39	GetUser(context.Context, string, string) (*DatabaseUser, *Response, error)
40	ListUsers(context.Context, string, *ListOptions) ([]DatabaseUser, *Response, error)
41	CreateUser(context.Context, string, *DatabaseCreateUserRequest) (*DatabaseUser, *Response, error)
42	DeleteUser(context.Context, string, string) (*Response, error)
43	ListDBs(context.Context, string, *ListOptions) ([]DatabaseDB, *Response, error)
44	CreateDB(context.Context, string, *DatabaseCreateDBRequest) (*DatabaseDB, *Response, error)
45	GetDB(context.Context, string, string) (*DatabaseDB, *Response, error)
46	DeleteDB(context.Context, string, string) (*Response, error)
47	ListPools(context.Context, string, *ListOptions) ([]DatabasePool, *Response, error)
48	CreatePool(context.Context, string, *DatabaseCreatePoolRequest) (*DatabasePool, *Response, error)
49	GetPool(context.Context, string, string) (*DatabasePool, *Response, error)
50	DeletePool(context.Context, string, string) (*Response, error)
51	GetReplica(context.Context, string, string) (*DatabaseReplica, *Response, error)
52	ListReplicas(context.Context, string, *ListOptions) ([]DatabaseReplica, *Response, error)
53	CreateReplica(context.Context, string, *DatabaseCreateReplicaRequest) (*DatabaseReplica, *Response, error)
54	DeleteReplica(context.Context, string, string) (*Response, error)
55}
56
57// DatabasesServiceOp handles communication with the Databases related methods
58// of the DigitalOcean API.
59type DatabasesServiceOp struct {
60	client *Client
61}
62
63var _ DatabasesService = &DatabasesServiceOp{}
64
65// Database represents a DigitalOcean managed database product. These managed databases
66// are usually comprised of a cluster of database nodes, a primary and 0 or more replicas.
67// The EngineSlug is a string which indicates the type of database service. Some examples are
68// "pg", "mysql" or "redis". A Database also includes connection information and other
69// properties of the service like region, size and current status.
70type Database struct {
71	ID                string                     `json:"id,omitempty"`
72	Name              string                     `json:"name,omitempty"`
73	EngineSlug        string                     `json:"engine,omitempty"`
74	VersionSlug       string                     `json:"version,omitempty"`
75	Connection        *DatabaseConnection        `json:"connection,omitempty"`
76	Users             []DatabaseUser             `json:"users,omitempty"`
77	NumNodes          int                        `json:"num_nodes,omitempty"`
78	SizeSlug          string                     `json:"size,omitempty"`
79	DBNames           []string                   `json:"db_names,omitempty"`
80	RegionSlug        string                     `json:"region,omitempty"`
81	Status            string                     `json:"status,omitempty"`
82	MaintenanceWindow *DatabaseMaintenanceWindow `json:"maintenance_window,omitempty"`
83	CreatedAt         time.Time                  `json:"created_at,omitempty"`
84}
85
86// DatabaseConnection represents a database connection
87type DatabaseConnection struct {
88	URI      string `json:"uri,omitempty"`
89	Database string `json:"database,omitempty"`
90	Host     string `json:"host,omitempty"`
91	Port     int    `json:"port,omitempty"`
92	User     string `json:"user,omitempty"`
93	Password string `json:"password,omitempty"`
94	SSL      bool   `json:"ssl,omitempty"`
95}
96
97// DatabaseUser represents a user in the database
98type DatabaseUser struct {
99	Name     string `json:"name,omitempty"`
100	Role     string `json:"role,omitempty"`
101	Password string `json:"password,omitempty"`
102}
103
104// DatabaseMaintenanceWindow represents the maintenance_window of a database
105// cluster
106type DatabaseMaintenanceWindow struct {
107	Day         string   `json:"day,omitempty"`
108	Hour        string   `json:"hour,omitempty"`
109	Pending     bool     `json:"pending,omitempty"`
110	Description []string `json:"description,omitempty"`
111}
112
113// DatabaseBackup represents a database backup.
114type DatabaseBackup struct {
115	CreatedAt     time.Time `json:"created_at,omitempty"`
116	SizeGigabytes float64   `json:"size_gigabytes,omitempty"`
117}
118
119// DatabaseCreateRequest represents a request to create a database cluster
120type DatabaseCreateRequest struct {
121	Name       string `json:"name,omitempty"`
122	EngineSlug string `json:"engine,omitempty"`
123	Version    string `json:"version,omitempty"`
124	SizeSlug   string `json:"size,omitempty"`
125	Region     string `json:"region,omitempty"`
126	NumNodes   int    `json:"num_nodes,omitempty"`
127}
128
129// DatabaseResizeRequest can be used to initiate a database resize operation.
130type DatabaseResizeRequest struct {
131	SizeSlug string `json:"size,omitempty"`
132	NumNodes int    `json:"num_nodes,omitempty"`
133}
134
135// DatabaseMigrateRequest can be used to initiate a database migrate operation.
136type DatabaseMigrateRequest struct {
137	Region string `json:"region,omitempty"`
138}
139
140// DatabaseUpdateMaintenanceRequest can be used to update the database's maintenance window.
141type DatabaseUpdateMaintenanceRequest struct {
142	Day  string `json:"day,omitempty"`
143	Hour string `json:"hour,omitempty"`
144}
145
146// DatabaseDB represents an engine-specific database created within a database cluster. For SQL
147// databases like PostgreSQL or MySQL, a "DB" refers to a database created on the RDBMS. For instance,
148// a PostgreSQL database server can contain many database schemas, each with it's own settings, access
149// permissions and data. ListDBs will return all databases present on the server.
150type DatabaseDB struct {
151	Name string `json:"name"`
152}
153
154// DatabaseReplica represents a read-only replica of a particular database
155type DatabaseReplica struct {
156	Name       string              `json:"name"`
157	Connection *DatabaseConnection `json:"connection"`
158	Region     string              `json:"region"`
159	Status     string              `json:"status"`
160	CreatedAt  time.Time           `json:"created_at"`
161}
162
163// DatabasePool represents a database connection pool
164type DatabasePool struct {
165	User       string              `json:"user"`
166	Name       string              `json:"name"`
167	Size       int                 `json:"size"`
168	Database   string              `json:"database"`
169	Mode       string              `json:"mode"`
170	Connection *DatabaseConnection `json:"connection"`
171}
172
173// DatabaseCreatePoolRequest is used to create a new database connection pool
174type DatabaseCreatePoolRequest struct {
175	Pool *DatabasePool `json:"pool"`
176}
177
178// DatabaseCreateUserRequest is used to create a new database user
179type DatabaseCreateUserRequest struct {
180	Name string `json:"name"`
181}
182
183// DatabaseCreateDBRequest is used to create a new engine-specific database within the cluster
184type DatabaseCreateDBRequest struct {
185	Name string `json:"name"`
186}
187
188// DatabaseCreateReplicaRequest is used to create a new read-only replica
189type DatabaseCreateReplicaRequest struct {
190	Name   string `json:"name"`
191	Region string `json:"region"`
192	Size   string `json:"size"`
193}
194
195type databaseUserRoot struct {
196	User *DatabaseUser `json:"user"`
197}
198
199type databaseUsersRoot struct {
200	Users []DatabaseUser `json:"users"`
201}
202
203type databaseDBRoot struct {
204	DB *DatabaseDB `json:"db"`
205}
206
207type databaseDBsRoot struct {
208	DBs []DatabaseDB `json:"dbs"`
209}
210
211type databasesRoot struct {
212	Databases []Database `json:"databases"`
213}
214
215type databaseRoot struct {
216	Database *Database `json:"database"`
217}
218
219type databaseBackupsRoot struct {
220	Backups []DatabaseBackup `json:"backups"`
221}
222
223type databasePoolRoot struct {
224	Pool *DatabasePool `json:"pool"`
225}
226
227type databasePoolsRoot struct {
228	Pools []DatabasePool `json:"pools"`
229}
230
231type databaseReplicaRoot struct {
232	Replica *DatabaseReplica `json:"replica"`
233}
234
235type databaseReplicasRoot struct {
236	Replicas []DatabaseReplica `json:"replicas"`
237}
238
239// List returns a list of the Databases visible with the caller's API token
240func (svc *DatabasesServiceOp) List(ctx context.Context, opts *ListOptions) ([]Database, *Response, error) {
241	path := databaseBasePath
242	path, err := addOptions(path, opts)
243	if err != nil {
244		return nil, nil, err
245	}
246	req, err := svc.client.NewRequest(ctx, http.MethodGet, path, nil)
247	if err != nil {
248		return nil, nil, err
249	}
250	root := new(databasesRoot)
251	resp, err := svc.client.Do(ctx, req, root)
252	if err != nil {
253		return nil, resp, err
254	}
255	return root.Databases, resp, nil
256}
257
258// Get retrieves the details of a database cluster
259func (svc *DatabasesServiceOp) Get(ctx context.Context, databaseID string) (*Database, *Response, error) {
260	path := fmt.Sprintf(databaseSinglePath, databaseID)
261	req, err := svc.client.NewRequest(ctx, http.MethodGet, path, nil)
262	if err != nil {
263		return nil, nil, err
264	}
265	root := new(databaseRoot)
266	resp, err := svc.client.Do(ctx, req, root)
267	if err != nil {
268		return nil, resp, err
269	}
270	return root.Database, resp, nil
271}
272
273// Create creates a database cluster
274func (svc *DatabasesServiceOp) Create(ctx context.Context, create *DatabaseCreateRequest) (*Database, *Response, error) {
275	path := databaseBasePath
276	req, err := svc.client.NewRequest(ctx, http.MethodPost, path, create)
277	if err != nil {
278		return nil, nil, err
279	}
280	root := new(databaseRoot)
281	resp, err := svc.client.Do(ctx, req, root)
282	if err != nil {
283		return nil, resp, err
284	}
285	return root.Database, resp, nil
286}
287
288// Delete deletes a database cluster. There is no way to recover a cluster once
289// it has been destroyed.
290func (svc *DatabasesServiceOp) Delete(ctx context.Context, databaseID string) (*Response, error) {
291	path := fmt.Sprintf("%s/%s", databaseBasePath, databaseID)
292	req, err := svc.client.NewRequest(ctx, http.MethodDelete, path, nil)
293	if err != nil {
294		return nil, err
295	}
296	resp, err := svc.client.Do(ctx, req, nil)
297	if err != nil {
298		return resp, err
299	}
300	return resp, nil
301}
302
303// Resize resizes a database cluster by number of nodes or size
304func (svc *DatabasesServiceOp) Resize(ctx context.Context, databaseID string, resize *DatabaseResizeRequest) (*Response, error) {
305	path := fmt.Sprintf(databaseResizePath, databaseID)
306	req, err := svc.client.NewRequest(ctx, http.MethodPut, path, resize)
307	if err != nil {
308		return nil, err
309	}
310	resp, err := svc.client.Do(ctx, req, nil)
311	if err != nil {
312		return resp, err
313	}
314	return resp, nil
315}
316
317// Migrate migrates a database cluster to a new region
318func (svc *DatabasesServiceOp) Migrate(ctx context.Context, databaseID string, migrate *DatabaseMigrateRequest) (*Response, error) {
319	path := fmt.Sprintf(databaseMigratePath, databaseID)
320	req, err := svc.client.NewRequest(ctx, http.MethodPut, path, migrate)
321	if err != nil {
322		return nil, err
323	}
324	resp, err := svc.client.Do(ctx, req, nil)
325	if err != nil {
326		return resp, err
327	}
328	return resp, nil
329}
330
331// UpdateMaintenance updates the maintenance window on a cluster
332func (svc *DatabasesServiceOp) UpdateMaintenance(ctx context.Context, databaseID string, maintenance *DatabaseUpdateMaintenanceRequest) (*Response, error) {
333	path := fmt.Sprintf(databaseMaintenancePath, databaseID)
334	req, err := svc.client.NewRequest(ctx, http.MethodPut, path, maintenance)
335	if err != nil {
336		return nil, err
337	}
338	resp, err := svc.client.Do(ctx, req, nil)
339	if err != nil {
340		return resp, err
341	}
342	return resp, nil
343}
344
345// ListBackups returns a list of the current backups of a database
346func (svc *DatabasesServiceOp) ListBackups(ctx context.Context, databaseID string, opts *ListOptions) ([]DatabaseBackup, *Response, error) {
347	path := fmt.Sprintf(databaseBackupsPath, databaseID)
348	path, err := addOptions(path, opts)
349	if err != nil {
350		return nil, nil, err
351	}
352	req, err := svc.client.NewRequest(ctx, http.MethodGet, path, nil)
353	if err != nil {
354		return nil, nil, err
355	}
356	root := new(databaseBackupsRoot)
357	resp, err := svc.client.Do(ctx, req, root)
358	if err != nil {
359		return nil, resp, err
360	}
361	return root.Backups, resp, nil
362}
363
364// GetUser returns the database user identified by userID
365func (svc *DatabasesServiceOp) GetUser(ctx context.Context, databaseID, userID string) (*DatabaseUser, *Response, error) {
366	path := fmt.Sprintf(databaseUserPath, databaseID, userID)
367	req, err := svc.client.NewRequest(ctx, http.MethodGet, path, nil)
368	if err != nil {
369		return nil, nil, err
370	}
371	root := new(databaseUserRoot)
372	resp, err := svc.client.Do(ctx, req, root)
373	if err != nil {
374		return nil, resp, err
375	}
376	return root.User, resp, nil
377}
378
379// ListUsers returns all database users for the database
380func (svc *DatabasesServiceOp) ListUsers(ctx context.Context, databaseID string, opts *ListOptions) ([]DatabaseUser, *Response, error) {
381	path := fmt.Sprintf(databaseUsersPath, databaseID)
382	path, err := addOptions(path, opts)
383	if err != nil {
384		return nil, nil, err
385	}
386	req, err := svc.client.NewRequest(ctx, http.MethodGet, path, nil)
387	if err != nil {
388		return nil, nil, err
389	}
390	root := new(databaseUsersRoot)
391	resp, err := svc.client.Do(ctx, req, root)
392	if err != nil {
393		return nil, resp, err
394	}
395	return root.Users, resp, nil
396}
397
398// CreateUser will create a new database user
399func (svc *DatabasesServiceOp) CreateUser(ctx context.Context, databaseID string, createUser *DatabaseCreateUserRequest) (*DatabaseUser, *Response, error) {
400	path := fmt.Sprintf(databaseUsersPath, databaseID)
401	req, err := svc.client.NewRequest(ctx, http.MethodPost, path, createUser)
402	if err != nil {
403		return nil, nil, err
404	}
405	root := new(databaseUserRoot)
406	resp, err := svc.client.Do(ctx, req, root)
407	if err != nil {
408		return nil, resp, err
409	}
410	return root.User, resp, nil
411}
412
413// DeleteUser will delete an existing database user
414func (svc *DatabasesServiceOp) DeleteUser(ctx context.Context, databaseID, userID string) (*Response, error) {
415	path := fmt.Sprintf(databaseUserPath, databaseID, userID)
416	req, err := svc.client.NewRequest(ctx, http.MethodDelete, path, nil)
417	if err != nil {
418		return nil, err
419	}
420	resp, err := svc.client.Do(ctx, req, nil)
421	if err != nil {
422		return resp, err
423	}
424	return resp, nil
425}
426
427// ListDBs returns all databases for a given database cluster
428func (svc *DatabasesServiceOp) ListDBs(ctx context.Context, databaseID string, opts *ListOptions) ([]DatabaseDB, *Response, error) {
429	path := fmt.Sprintf(databaseDBsPath, databaseID)
430	path, err := addOptions(path, opts)
431	if err != nil {
432		return nil, nil, err
433	}
434	req, err := svc.client.NewRequest(ctx, http.MethodGet, path, nil)
435	if err != nil {
436		return nil, nil, err
437	}
438	root := new(databaseDBsRoot)
439	resp, err := svc.client.Do(ctx, req, root)
440	if err != nil {
441		return nil, resp, err
442	}
443	return root.DBs, resp, nil
444}
445
446// GetDB returns a single database by name
447func (svc *DatabasesServiceOp) GetDB(ctx context.Context, databaseID, name string) (*DatabaseDB, *Response, error) {
448	path := fmt.Sprintf(databaseDBPath, databaseID, name)
449	req, err := svc.client.NewRequest(ctx, http.MethodGet, path, nil)
450	if err != nil {
451		return nil, nil, err
452	}
453	root := new(databaseDBRoot)
454	resp, err := svc.client.Do(ctx, req, root)
455	if err != nil {
456		return nil, resp, err
457	}
458	return root.DB, resp, nil
459}
460
461// CreateDB will create a new database
462func (svc *DatabasesServiceOp) CreateDB(ctx context.Context, databaseID string, createDB *DatabaseCreateDBRequest) (*DatabaseDB, *Response, error) {
463	path := fmt.Sprintf(databaseDBsPath, databaseID)
464	req, err := svc.client.NewRequest(ctx, http.MethodPost, path, createDB)
465	if err != nil {
466		return nil, nil, err
467	}
468	root := new(databaseDBRoot)
469	resp, err := svc.client.Do(ctx, req, root)
470	if err != nil {
471		return nil, resp, err
472	}
473	return root.DB, resp, nil
474}
475
476// DeleteDB will delete an existing database
477func (svc *DatabasesServiceOp) DeleteDB(ctx context.Context, databaseID, name string) (*Response, error) {
478	path := fmt.Sprintf(databaseDBPath, databaseID, name)
479	req, err := svc.client.NewRequest(ctx, http.MethodDelete, path, nil)
480	if err != nil {
481		return nil, err
482	}
483	resp, err := svc.client.Do(ctx, req, nil)
484	if err != nil {
485		return resp, err
486	}
487	return resp, nil
488}
489
490// ListPools returns all connection pools for a given database cluster
491func (svc *DatabasesServiceOp) ListPools(ctx context.Context, databaseID string, opts *ListOptions) ([]DatabasePool, *Response, error) {
492	path := fmt.Sprintf(databasePoolsPath, databaseID)
493	path, err := addOptions(path, opts)
494	if err != nil {
495		return nil, nil, err
496	}
497	req, err := svc.client.NewRequest(ctx, http.MethodGet, path, nil)
498	if err != nil {
499		return nil, nil, err
500	}
501	root := new(databasePoolsRoot)
502	resp, err := svc.client.Do(ctx, req, root)
503	if err != nil {
504		return nil, resp, err
505	}
506	return root.Pools, resp, nil
507}
508
509// GetPool returns a single database connection pool by name
510func (svc *DatabasesServiceOp) GetPool(ctx context.Context, databaseID, name string) (*DatabasePool, *Response, error) {
511	path := fmt.Sprintf(databasePoolPath, databaseID, name)
512	req, err := svc.client.NewRequest(ctx, http.MethodGet, path, nil)
513	if err != nil {
514		return nil, nil, err
515	}
516	root := new(databasePoolRoot)
517	resp, err := svc.client.Do(ctx, req, root)
518	if err != nil {
519		return nil, resp, err
520	}
521	return root.Pool, resp, nil
522}
523
524// CreatePool will create a new database connection pool
525func (svc *DatabasesServiceOp) CreatePool(ctx context.Context, databaseID string, createPool *DatabaseCreatePoolRequest) (*DatabasePool, *Response, error) {
526	path := fmt.Sprintf(databasePoolsPath, databaseID)
527	req, err := svc.client.NewRequest(ctx, http.MethodPost, path, createPool)
528	if err != nil {
529		return nil, nil, err
530	}
531	root := new(databasePoolRoot)
532	resp, err := svc.client.Do(ctx, req, root)
533	if err != nil {
534		return nil, resp, err
535	}
536	return root.Pool, resp, nil
537}
538
539// DeletePool will delete an existing database connection pool
540func (svc *DatabasesServiceOp) DeletePool(ctx context.Context, databaseID, name string) (*Response, error) {
541	path := fmt.Sprintf(databasePoolPath, databaseID, name)
542	req, err := svc.client.NewRequest(ctx, http.MethodDelete, path, nil)
543	if err != nil {
544		return nil, err
545	}
546	resp, err := svc.client.Do(ctx, req, nil)
547	if err != nil {
548		return resp, err
549	}
550	return resp, nil
551}
552
553// GetReplica returns a single database replica
554func (svc *DatabasesServiceOp) GetReplica(ctx context.Context, databaseID, name string) (*DatabaseReplica, *Response, error) {
555	path := fmt.Sprintf(databaseReplicaPath, databaseID, name)
556	req, err := svc.client.NewRequest(ctx, http.MethodGet, path, nil)
557	if err != nil {
558		return nil, nil, err
559	}
560	root := new(databaseReplicaRoot)
561	resp, err := svc.client.Do(ctx, req, root)
562	if err != nil {
563		return nil, resp, err
564	}
565	return root.Replica, resp, nil
566}
567
568// ListReplicas returns all read-only replicas for a given database cluster
569func (svc *DatabasesServiceOp) ListReplicas(ctx context.Context, databaseID string, opts *ListOptions) ([]DatabaseReplica, *Response, error) {
570	path := fmt.Sprintf(databaseReplicasPath, databaseID)
571	path, err := addOptions(path, opts)
572	if err != nil {
573		return nil, nil, err
574	}
575	req, err := svc.client.NewRequest(ctx, http.MethodGet, path, nil)
576	if err != nil {
577		return nil, nil, err
578	}
579	root := new(databaseReplicasRoot)
580	resp, err := svc.client.Do(ctx, req, root)
581	if err != nil {
582		return nil, resp, err
583	}
584	return root.Replicas, resp, nil
585}
586
587// CreateReplica will create a new database connection pool
588func (svc *DatabasesServiceOp) CreateReplica(ctx context.Context, databaseID string, createReplica *DatabaseCreateReplicaRequest) (*DatabaseReplica, *Response, error) {
589	path := fmt.Sprintf(databaseReplicasPath, databaseID)
590	req, err := svc.client.NewRequest(ctx, http.MethodPost, path, createReplica)
591	if err != nil {
592		return nil, nil, err
593	}
594	root := new(databaseReplicaRoot)
595	resp, err := svc.client.Do(ctx, req, root)
596	if err != nil {
597		return nil, resp, err
598	}
599	return root.Replica, resp, nil
600}
601
602// DeleteReplica will delete an existing database replica
603func (svc *DatabasesServiceOp) DeleteReplica(ctx context.Context, databaseID, name string) (*Response, error) {
604	path := fmt.Sprintf(databaseReplicaPath, databaseID, name)
605	req, err := svc.client.NewRequest(ctx, http.MethodDelete, path, nil)
606	if err != nil {
607		return nil, err
608	}
609	resp, err := svc.client.Do(ctx, req, nil)
610	if err != nil {
611		return resp, err
612	}
613	return resp, nil
614}
615