1package godo
2
3import (
4	"context"
5	"encoding/json"
6	"errors"
7	"fmt"
8	"net/http"
9)
10
11const dropletBasePath = "v2/droplets"
12
13var errNoNetworks = errors.New("no networks have been defined")
14
15// DropletsService is an interface for interfacing with the Droplet
16// endpoints of the DigitalOcean API
17// See: https://docs.digitalocean.com/reference/api/api-reference/#tag/Droplets
18type DropletsService interface {
19	List(context.Context, *ListOptions) ([]Droplet, *Response, error)
20	ListByTag(context.Context, string, *ListOptions) ([]Droplet, *Response, error)
21	Get(context.Context, int) (*Droplet, *Response, error)
22	Create(context.Context, *DropletCreateRequest) (*Droplet, *Response, error)
23	CreateMultiple(context.Context, *DropletMultiCreateRequest) ([]Droplet, *Response, error)
24	Delete(context.Context, int) (*Response, error)
25	DeleteByTag(context.Context, string) (*Response, error)
26	Kernels(context.Context, int, *ListOptions) ([]Kernel, *Response, error)
27	Snapshots(context.Context, int, *ListOptions) ([]Image, *Response, error)
28	Backups(context.Context, int, *ListOptions) ([]Image, *Response, error)
29	Actions(context.Context, int, *ListOptions) ([]Action, *Response, error)
30	Neighbors(context.Context, int) ([]Droplet, *Response, error)
31}
32
33// DropletsServiceOp handles communication with the Droplet related methods of the
34// DigitalOcean API.
35type DropletsServiceOp struct {
36	client *Client
37}
38
39var _ DropletsService = &DropletsServiceOp{}
40
41// Droplet represents a DigitalOcean Droplet
42type Droplet struct {
43	ID               int           `json:"id,float64,omitempty"`
44	Name             string        `json:"name,omitempty"`
45	Memory           int           `json:"memory,omitempty"`
46	Vcpus            int           `json:"vcpus,omitempty"`
47	Disk             int           `json:"disk,omitempty"`
48	Region           *Region       `json:"region,omitempty"`
49	Image            *Image        `json:"image,omitempty"`
50	Size             *Size         `json:"size,omitempty"`
51	SizeSlug         string        `json:"size_slug,omitempty"`
52	BackupIDs        []int         `json:"backup_ids,omitempty"`
53	NextBackupWindow *BackupWindow `json:"next_backup_window,omitempty"`
54	SnapshotIDs      []int         `json:"snapshot_ids,omitempty"`
55	Features         []string      `json:"features,omitempty"`
56	Locked           bool          `json:"locked,bool,omitempty"`
57	Status           string        `json:"status,omitempty"`
58	Networks         *Networks     `json:"networks,omitempty"`
59	Created          string        `json:"created_at,omitempty"`
60	Kernel           *Kernel       `json:"kernel,omitempty"`
61	Tags             []string      `json:"tags,omitempty"`
62	VolumeIDs        []string      `json:"volume_ids"`
63	VPCUUID          string        `json:"vpc_uuid,omitempty"`
64}
65
66// PublicIPv4 returns the public IPv4 address for the Droplet.
67func (d *Droplet) PublicIPv4() (string, error) {
68	if d.Networks == nil {
69		return "", errNoNetworks
70	}
71
72	for _, v4 := range d.Networks.V4 {
73		if v4.Type == "public" {
74			return v4.IPAddress, nil
75		}
76	}
77
78	return "", nil
79}
80
81// PrivateIPv4 returns the private IPv4 address for the Droplet.
82func (d *Droplet) PrivateIPv4() (string, error) {
83	if d.Networks == nil {
84		return "", errNoNetworks
85	}
86
87	for _, v4 := range d.Networks.V4 {
88		if v4.Type == "private" {
89			return v4.IPAddress, nil
90		}
91	}
92
93	return "", nil
94}
95
96// PublicIPv6 returns the public IPv6 address for the Droplet.
97func (d *Droplet) PublicIPv6() (string, error) {
98	if d.Networks == nil {
99		return "", errNoNetworks
100	}
101
102	for _, v6 := range d.Networks.V6 {
103		if v6.Type == "public" {
104			return v6.IPAddress, nil
105		}
106	}
107
108	return "", nil
109}
110
111// Kernel object
112type Kernel struct {
113	ID      int    `json:"id,float64,omitempty"`
114	Name    string `json:"name,omitempty"`
115	Version string `json:"version,omitempty"`
116}
117
118// BackupWindow object
119type BackupWindow struct {
120	Start *Timestamp `json:"start,omitempty"`
121	End   *Timestamp `json:"end,omitempty"`
122}
123
124// Convert Droplet to a string
125func (d Droplet) String() string {
126	return Stringify(d)
127}
128
129// URN returns the droplet ID in a valid DO API URN form.
130func (d Droplet) URN() string {
131	return ToURN("Droplet", d.ID)
132}
133
134// DropletRoot represents a Droplet root
135type dropletRoot struct {
136	Droplet *Droplet `json:"droplet"`
137	Links   *Links   `json:"links,omitempty"`
138}
139
140type dropletsRoot struct {
141	Droplets []Droplet `json:"droplets"`
142	Links    *Links    `json:"links"`
143	Meta     *Meta     `json:"meta"`
144}
145
146type kernelsRoot struct {
147	Kernels []Kernel `json:"kernels,omitempty"`
148	Links   *Links   `json:"links"`
149	Meta    *Meta    `json:"meta"`
150}
151
152type dropletSnapshotsRoot struct {
153	Snapshots []Image `json:"snapshots,omitempty"`
154	Links     *Links  `json:"links"`
155	Meta      *Meta   `json:"meta"`
156}
157
158type backupsRoot struct {
159	Backups []Image `json:"backups,omitempty"`
160	Links   *Links  `json:"links"`
161	Meta    *Meta   `json:"meta"`
162}
163
164// DropletCreateImage identifies an image for the create request. It prefers slug over ID.
165type DropletCreateImage struct {
166	ID   int
167	Slug string
168}
169
170// MarshalJSON returns either the slug or id of the image. It returns the id
171// if the slug is empty.
172func (d DropletCreateImage) MarshalJSON() ([]byte, error) {
173	if d.Slug != "" {
174		return json.Marshal(d.Slug)
175	}
176
177	return json.Marshal(d.ID)
178}
179
180// DropletCreateVolume identifies a volume to attach for the create request.
181type DropletCreateVolume struct {
182	ID string
183	// Deprecated: You must pass a the volume's ID when creating a Droplet.
184	Name string
185}
186
187// MarshalJSON returns an object with either the ID or name of the volume. It
188// prefers the ID over the name.
189func (d DropletCreateVolume) MarshalJSON() ([]byte, error) {
190	if d.ID != "" {
191		return json.Marshal(struct {
192			ID string `json:"id"`
193		}{ID: d.ID})
194	}
195
196	return json.Marshal(struct {
197		Name string `json:"name"`
198	}{Name: d.Name})
199}
200
201// DropletCreateSSHKey identifies a SSH Key for the create request. It prefers fingerprint over ID.
202type DropletCreateSSHKey struct {
203	ID          int
204	Fingerprint string
205}
206
207// MarshalJSON returns either the fingerprint or id of the ssh key. It returns
208// the id if the fingerprint is empty.
209func (d DropletCreateSSHKey) MarshalJSON() ([]byte, error) {
210	if d.Fingerprint != "" {
211		return json.Marshal(d.Fingerprint)
212	}
213
214	return json.Marshal(d.ID)
215}
216
217// DropletCreateRequest represents a request to create a Droplet.
218type DropletCreateRequest struct {
219	Name              string                `json:"name"`
220	Region            string                `json:"region"`
221	Size              string                `json:"size"`
222	Image             DropletCreateImage    `json:"image"`
223	SSHKeys           []DropletCreateSSHKey `json:"ssh_keys"`
224	Backups           bool                  `json:"backups"`
225	IPv6              bool                  `json:"ipv6"`
226	PrivateNetworking bool                  `json:"private_networking"`
227	Monitoring        bool                  `json:"monitoring"`
228	UserData          string                `json:"user_data,omitempty"`
229	Volumes           []DropletCreateVolume `json:"volumes,omitempty"`
230	Tags              []string              `json:"tags"`
231	VPCUUID           string                `json:"vpc_uuid,omitempty"`
232	WithDropletAgent  *bool                 `json:"with_droplet_agent,omitempty"`
233}
234
235// DropletMultiCreateRequest is a request to create multiple Droplets.
236type DropletMultiCreateRequest struct {
237	Names             []string              `json:"names"`
238	Region            string                `json:"region"`
239	Size              string                `json:"size"`
240	Image             DropletCreateImage    `json:"image"`
241	SSHKeys           []DropletCreateSSHKey `json:"ssh_keys"`
242	Backups           bool                  `json:"backups"`
243	IPv6              bool                  `json:"ipv6"`
244	PrivateNetworking bool                  `json:"private_networking"`
245	Monitoring        bool                  `json:"monitoring"`
246	UserData          string                `json:"user_data,omitempty"`
247	Tags              []string              `json:"tags"`
248	VPCUUID           string                `json:"vpc_uuid,omitempty"`
249	WithDropletAgent  *bool                 `json:"with_droplet_agent,omitempty"`
250}
251
252func (d DropletCreateRequest) String() string {
253	return Stringify(d)
254}
255
256func (d DropletMultiCreateRequest) String() string {
257	return Stringify(d)
258}
259
260// Networks represents the Droplet's Networks.
261type Networks struct {
262	V4 []NetworkV4 `json:"v4,omitempty"`
263	V6 []NetworkV6 `json:"v6,omitempty"`
264}
265
266// NetworkV4 represents a DigitalOcean IPv4 Network.
267type NetworkV4 struct {
268	IPAddress string `json:"ip_address,omitempty"`
269	Netmask   string `json:"netmask,omitempty"`
270	Gateway   string `json:"gateway,omitempty"`
271	Type      string `json:"type,omitempty"`
272}
273
274func (n NetworkV4) String() string {
275	return Stringify(n)
276}
277
278// NetworkV6 represents a DigitalOcean IPv6 network.
279type NetworkV6 struct {
280	IPAddress string `json:"ip_address,omitempty"`
281	Netmask   int    `json:"netmask,omitempty"`
282	Gateway   string `json:"gateway,omitempty"`
283	Type      string `json:"type,omitempty"`
284}
285
286func (n NetworkV6) String() string {
287	return Stringify(n)
288}
289
290// Performs a list request given a path.
291func (s *DropletsServiceOp) list(ctx context.Context, path string) ([]Droplet, *Response, error) {
292	req, err := s.client.NewRequest(ctx, http.MethodGet, path, nil)
293	if err != nil {
294		return nil, nil, err
295	}
296
297	root := new(dropletsRoot)
298	resp, err := s.client.Do(ctx, req, root)
299	if err != nil {
300		return nil, resp, err
301	}
302	if l := root.Links; l != nil {
303		resp.Links = l
304	}
305	if m := root.Meta; m != nil {
306		resp.Meta = m
307	}
308
309	return root.Droplets, resp, err
310}
311
312// List all Droplets.
313func (s *DropletsServiceOp) List(ctx context.Context, opt *ListOptions) ([]Droplet, *Response, error) {
314	path := dropletBasePath
315	path, err := addOptions(path, opt)
316	if err != nil {
317		return nil, nil, err
318	}
319
320	return s.list(ctx, path)
321}
322
323// ListByTag lists all Droplets matched by a Tag.
324func (s *DropletsServiceOp) ListByTag(ctx context.Context, tag string, opt *ListOptions) ([]Droplet, *Response, error) {
325	path := fmt.Sprintf("%s?tag_name=%s", dropletBasePath, tag)
326	path, err := addOptions(path, opt)
327	if err != nil {
328		return nil, nil, err
329	}
330
331	return s.list(ctx, path)
332}
333
334// Get individual Droplet.
335func (s *DropletsServiceOp) Get(ctx context.Context, dropletID int) (*Droplet, *Response, error) {
336	if dropletID < 1 {
337		return nil, nil, NewArgError("dropletID", "cannot be less than 1")
338	}
339
340	path := fmt.Sprintf("%s/%d", dropletBasePath, dropletID)
341
342	req, err := s.client.NewRequest(ctx, http.MethodGet, path, nil)
343	if err != nil {
344		return nil, nil, err
345	}
346
347	root := new(dropletRoot)
348	resp, err := s.client.Do(ctx, req, root)
349	if err != nil {
350		return nil, resp, err
351	}
352
353	return root.Droplet, resp, err
354}
355
356// Create Droplet
357func (s *DropletsServiceOp) Create(ctx context.Context, createRequest *DropletCreateRequest) (*Droplet, *Response, error) {
358	if createRequest == nil {
359		return nil, nil, NewArgError("createRequest", "cannot be nil")
360	}
361
362	path := dropletBasePath
363
364	req, err := s.client.NewRequest(ctx, http.MethodPost, path, createRequest)
365	if err != nil {
366		return nil, nil, err
367	}
368
369	root := new(dropletRoot)
370	resp, err := s.client.Do(ctx, req, root)
371	if err != nil {
372		return nil, resp, err
373	}
374	if l := root.Links; l != nil {
375		resp.Links = l
376	}
377
378	return root.Droplet, resp, err
379}
380
381// CreateMultiple creates multiple Droplets.
382func (s *DropletsServiceOp) CreateMultiple(ctx context.Context, createRequest *DropletMultiCreateRequest) ([]Droplet, *Response, error) {
383	if createRequest == nil {
384		return nil, nil, NewArgError("createRequest", "cannot be nil")
385	}
386
387	path := dropletBasePath
388
389	req, err := s.client.NewRequest(ctx, http.MethodPost, path, createRequest)
390	if err != nil {
391		return nil, nil, err
392	}
393
394	root := new(dropletsRoot)
395	resp, err := s.client.Do(ctx, req, root)
396	if err != nil {
397		return nil, resp, err
398	}
399	if l := root.Links; l != nil {
400		resp.Links = l
401	}
402
403	return root.Droplets, resp, err
404}
405
406// Performs a delete request given a path
407func (s *DropletsServiceOp) delete(ctx context.Context, path string) (*Response, error) {
408	req, err := s.client.NewRequest(ctx, http.MethodDelete, path, nil)
409	if err != nil {
410		return nil, err
411	}
412
413	resp, err := s.client.Do(ctx, req, nil)
414
415	return resp, err
416}
417
418// Delete Droplet.
419func (s *DropletsServiceOp) Delete(ctx context.Context, dropletID int) (*Response, error) {
420	if dropletID < 1 {
421		return nil, NewArgError("dropletID", "cannot be less than 1")
422	}
423
424	path := fmt.Sprintf("%s/%d", dropletBasePath, dropletID)
425
426	return s.delete(ctx, path)
427}
428
429// DeleteByTag deletes Droplets matched by a Tag.
430func (s *DropletsServiceOp) DeleteByTag(ctx context.Context, tag string) (*Response, error) {
431	if tag == "" {
432		return nil, NewArgError("tag", "cannot be empty")
433	}
434
435	path := fmt.Sprintf("%s?tag_name=%s", dropletBasePath, tag)
436
437	return s.delete(ctx, path)
438}
439
440// Kernels lists kernels available for a Droplet.
441func (s *DropletsServiceOp) Kernels(ctx context.Context, dropletID int, opt *ListOptions) ([]Kernel, *Response, error) {
442	if dropletID < 1 {
443		return nil, nil, NewArgError("dropletID", "cannot be less than 1")
444	}
445
446	path := fmt.Sprintf("%s/%d/kernels", dropletBasePath, dropletID)
447	path, err := addOptions(path, opt)
448	if err != nil {
449		return nil, nil, err
450	}
451
452	req, err := s.client.NewRequest(ctx, http.MethodGet, path, nil)
453	if err != nil {
454		return nil, nil, err
455	}
456
457	root := new(kernelsRoot)
458	resp, err := s.client.Do(ctx, req, root)
459	if l := root.Links; l != nil {
460		resp.Links = l
461	}
462	if m := root.Meta; m != nil {
463		resp.Meta = m
464	}
465
466	return root.Kernels, resp, err
467}
468
469// Actions lists the actions for a Droplet.
470func (s *DropletsServiceOp) Actions(ctx context.Context, dropletID int, opt *ListOptions) ([]Action, *Response, error) {
471	if dropletID < 1 {
472		return nil, nil, NewArgError("dropletID", "cannot be less than 1")
473	}
474
475	path := fmt.Sprintf("%s/%d/actions", dropletBasePath, dropletID)
476	path, err := addOptions(path, opt)
477	if err != nil {
478		return nil, nil, err
479	}
480
481	req, err := s.client.NewRequest(ctx, http.MethodGet, path, nil)
482	if err != nil {
483		return nil, nil, err
484	}
485
486	root := new(actionsRoot)
487	resp, err := s.client.Do(ctx, req, root)
488	if err != nil {
489		return nil, resp, err
490	}
491	if l := root.Links; l != nil {
492		resp.Links = l
493	}
494	if m := root.Meta; m != nil {
495		resp.Meta = m
496	}
497
498	return root.Actions, resp, err
499}
500
501// Backups lists the backups for a Droplet.
502func (s *DropletsServiceOp) Backups(ctx context.Context, dropletID int, opt *ListOptions) ([]Image, *Response, error) {
503	if dropletID < 1 {
504		return nil, nil, NewArgError("dropletID", "cannot be less than 1")
505	}
506
507	path := fmt.Sprintf("%s/%d/backups", dropletBasePath, dropletID)
508	path, err := addOptions(path, opt)
509	if err != nil {
510		return nil, nil, err
511	}
512
513	req, err := s.client.NewRequest(ctx, http.MethodGet, path, nil)
514	if err != nil {
515		return nil, nil, err
516	}
517
518	root := new(backupsRoot)
519	resp, err := s.client.Do(ctx, req, root)
520	if err != nil {
521		return nil, resp, err
522	}
523	if l := root.Links; l != nil {
524		resp.Links = l
525	}
526	if m := root.Meta; m != nil {
527		resp.Meta = m
528	}
529
530	return root.Backups, resp, err
531}
532
533// Snapshots lists the snapshots available for a Droplet.
534func (s *DropletsServiceOp) Snapshots(ctx context.Context, dropletID int, opt *ListOptions) ([]Image, *Response, error) {
535	if dropletID < 1 {
536		return nil, nil, NewArgError("dropletID", "cannot be less than 1")
537	}
538
539	path := fmt.Sprintf("%s/%d/snapshots", dropletBasePath, dropletID)
540	path, err := addOptions(path, opt)
541	if err != nil {
542		return nil, nil, err
543	}
544
545	req, err := s.client.NewRequest(ctx, http.MethodGet, path, nil)
546	if err != nil {
547		return nil, nil, err
548	}
549
550	root := new(dropletSnapshotsRoot)
551	resp, err := s.client.Do(ctx, req, root)
552	if err != nil {
553		return nil, resp, err
554	}
555	if l := root.Links; l != nil {
556		resp.Links = l
557	}
558	if m := root.Meta; m != nil {
559		resp.Meta = m
560	}
561
562	return root.Snapshots, resp, err
563}
564
565// Neighbors lists the neighbors for a Droplet.
566func (s *DropletsServiceOp) Neighbors(ctx context.Context, dropletID int) ([]Droplet, *Response, error) {
567	if dropletID < 1 {
568		return nil, nil, NewArgError("dropletID", "cannot be less than 1")
569	}
570
571	path := fmt.Sprintf("%s/%d/neighbors", dropletBasePath, dropletID)
572
573	req, err := s.client.NewRequest(ctx, http.MethodGet, path, nil)
574	if err != nil {
575		return nil, nil, err
576	}
577
578	root := new(dropletsRoot)
579	resp, err := s.client.Do(ctx, req, root)
580	if err != nil {
581		return nil, resp, err
582	}
583
584	return root.Droplets, resp, err
585}
586
587func (s *DropletsServiceOp) dropletActionStatus(ctx context.Context, uri string) (string, error) {
588	action, _, err := s.client.DropletActions.GetByURI(ctx, uri)
589
590	if err != nil {
591		return "", err
592	}
593
594	return action.Status, nil
595}
596