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 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