Skip to content

Commit

Permalink
Add support for Reserved IP addresses (#532)
Browse files Browse the repository at this point in the history
Floating IPs are being renamed to Reserved IPs. We'll need to
support both for awhile, treating them as distinct resource
types until Floating IPs are deprecated or removed.

This change allows the caller to use either or both at the same
time.

Signed-off-by: Chris Cummer <chriscummer@me.com>
  • Loading branch information
senorprogrammer committed Jun 15, 2022
1 parent 475e227 commit 67d5985
Show file tree
Hide file tree
Showing 10 changed files with 646 additions and 14 deletions.
1 change: 1 addition & 0 deletions account.go
Expand Up @@ -24,6 +24,7 @@ var _ AccountService = &AccountServiceOp{}
type Account struct {
DropletLimit int `json:"droplet_limit,omitempty"`
FloatingIPLimit int `json:"floating_ip_limit,omitempty"`
ReservedIPLimit int `json:"reserved_ip_limit,omitempty"`
VolumeLimit int `json:"volume_limit,omitempty"`
Email string `json:"email,omitempty"`
UUID string `json:"uuid,omitempty"`
Expand Down
17 changes: 10 additions & 7 deletions account_test.go
Expand Up @@ -18,6 +18,7 @@ func TestAccountGet(t *testing.T) {
{ "account": {
"droplet_limit": 25,
"floating_ip_limit": 25,
"reserved_ip_limit": 25,
"volume_limit": 22,
"email": "sammy@digitalocean.com",
"uuid": "b6fr89dbf6d9156cace5f3c78dc9851d957381ef",
Expand All @@ -33,7 +34,7 @@ func TestAccountGet(t *testing.T) {
t.Errorf("Account.Get returned error: %v", err)
}

expected := &Account{DropletLimit: 25, FloatingIPLimit: 25, Email: "sammy@digitalocean.com",
expected := &Account{DropletLimit: 25, FloatingIPLimit: 25, ReservedIPLimit: 25, Email: "sammy@digitalocean.com",
UUID: "b6fr89dbf6d9156cace5f3c78dc9851d957381ef", EmailVerified: true, VolumeLimit: 22}
if !reflect.DeepEqual(acct, expected) {
t.Errorf("Account.Get returned %+v, expected %+v", acct, expected)
Expand All @@ -44,18 +45,19 @@ func TestAccountString(t *testing.T) {
acct := &Account{
DropletLimit: 25,
FloatingIPLimit: 25,
ReservedIPLimit: 25,
VolumeLimit: 22,
Email: "sammy@digitalocean.com",
UUID: "b6fr89dbf6d9156cace5f3c78dc9851d957381ef",
EmailVerified: true,
Status: "active",
StatusMessage: "message",
VolumeLimit: 22,
}

stringified := acct.String()
expected := `godo.Account{DropletLimit:25, FloatingIPLimit:25, VolumeLimit:22, Email:"sammy@digitalocean.com", UUID:"b6fr89dbf6d9156cace5f3c78dc9851d957381ef", EmailVerified:true, Status:"active", StatusMessage:"message"}`
expected := `godo.Account{DropletLimit:25, FloatingIPLimit:25, ReservedIPLimit:25, VolumeLimit:22, Email:"sammy@digitalocean.com", UUID:"b6fr89dbf6d9156cace5f3c78dc9851d957381ef", EmailVerified:true, Status:"active", StatusMessage:"message"}`
if expected != stringified {
t.Errorf("Account.String returned %+v, expected %+v", stringified, expected)
t.Errorf("\n got %+v\nexpected %+v", stringified, expected)
}

}
Expand Down Expand Up @@ -111,22 +113,23 @@ func TestAccountStringWithTeam(t *testing.T) {
acct := &Account{
DropletLimit: 25,
FloatingIPLimit: 25,
ReservedIPLimit: 25,
VolumeLimit: 22,
Email: "sammy@digitalocean.com",
UUID: "b6fr89dbf6d9156cace5f3c78dc9851d957381ef",
EmailVerified: true,
Status: "active",
StatusMessage: "message",
VolumeLimit: 22,
Team: &TeamInfo{
Name: "My Team",
UUID: "b6fr89dbf6d9156cace5f3c78dc9851d957381ef",
},
}

stringified := acct.String()
expected := `godo.Account{DropletLimit:25, FloatingIPLimit:25, VolumeLimit:22, Email:"sammy@digitalocean.com", UUID:"b6fr89dbf6d9156cace5f3c78dc9851d957381ef", EmailVerified:true, Status:"active", StatusMessage:"message", Team:godo.TeamInfo{Name:"My Team", UUID:"b6fr89dbf6d9156cace5f3c78dc9851d957381ef"}}`
expected := `godo.Account{DropletLimit:25, FloatingIPLimit:25, ReservedIPLimit:25, VolumeLimit:22, Email:"sammy@digitalocean.com", UUID:"b6fr89dbf6d9156cace5f3c78dc9851d957381ef", EmailVerified:true, Status:"active", StatusMessage:"message", Team:godo.TeamInfo{Name:"My Team", UUID:"b6fr89dbf6d9156cace5f3c78dc9851d957381ef"}}`
if expected != stringified {
t.Errorf("Account.String returned %+v, expected %+v", stringified, expected)
t.Errorf("\n got %+v\nexpected %+v", stringified, expected)
}

}
8 changes: 4 additions & 4 deletions floating_ips_actions_test.go
Expand Up @@ -144,10 +144,10 @@ func TestFloatingIPsActions_ListPageByNumber(t *testing.T) {
"actions":[{"status":"in-progress"}],
"links":{
"pages":{
"next":"http://example.com/v2/regions/?page=3",
"prev":"http://example.com/v2/regions/?page=1",
"last":"http://example.com/v2/regions/?page=3",
"first":"http://example.com/v2/regions/?page=1"
"next":"http://example.com/v2/floating_ips/?page=3",
"prev":"http://example.com/v2/floating_ips/?page=1",
"last":"http://example.com/v2/floating_ips/?page=3",
"first":"http://example.com/v2/floating_ips/?page=1"
}
}
}`
Expand Down
4 changes: 4 additions & 0 deletions godo.go
Expand Up @@ -64,6 +64,8 @@ type Client struct {
Sizes SizesService
FloatingIPs FloatingIPsService
FloatingIPActions FloatingIPActionsService
ReservedIPs ReservedIPsService
ReservedIPActions ReservedIPActionsService
Snapshots SnapshotsService
Storage StorageService
StorageActions StorageActionsService
Expand Down Expand Up @@ -219,6 +221,8 @@ func NewClient(httpClient *http.Client) *Client {
c.Firewalls = &FirewallsServiceOp{client: c}
c.FloatingIPs = &FloatingIPsServiceOp{client: c}
c.FloatingIPActions = &FloatingIPActionsServiceOp{client: c}
c.ReservedIPs = &ReservedIPsServiceOp{client: c}
c.ReservedIPActions = &ReservedIPActionsServiceOp{client: c}
c.Images = &ImagesServiceOp{client: c}
c.ImageActions = &ImageActionsServiceOp{client: c}
c.Invoices = &InvoicesServiceOp{client: c}
Expand Down
2 changes: 2 additions & 0 deletions godo_test.go
Expand Up @@ -90,6 +90,8 @@ func testClientServices(t *testing.T, c *Client) {
"Sizes",
"FloatingIPs",
"FloatingIPActions",
"ReservedIPs",
"ReservedIPActions",
"Tags",
}

Expand Down
51 changes: 48 additions & 3 deletions projects_test.go
Expand Up @@ -335,6 +335,13 @@ func TestProjects_ListResources(t *testing.T) {
Self: "http://example.com/v2/floating_ips/1.2.3.4",
},
},
{
URN: "do:reservedip:1.2.3.4",
AssignedAt: "2018-09-27 00:00:00",
Links: &ProjectResourceLinks{
Self: "http://example.com/v2/reserved_ips/1.2.3.4",
},
},
}

mux.HandleFunc("/v2/projects/project-1/resources", func(w http.ResponseWriter, r *http.Request) {
Expand Down Expand Up @@ -378,6 +385,13 @@ func TestProjects_ListResourcesWithMultiplePages(t *testing.T) {
"links": {
"self": "http://example.com/v2/floating_ips/1.2.3.4"
}
},
{
"urn": "do:reservedip:1.2.3.4",
"assigned_at": "2018-09-27 00:00:00",
"links": {
"self": "http://example.com/v2/reserved_ips/1.2.3.4"
}
}
],
"links": {
Expand Down Expand Up @@ -420,6 +434,13 @@ func TestProjects_ListResourcesWithPageNumber(t *testing.T) {
"links": {
"self": "http://example.com/v2/floating_ips/1.2.3.4"
}
},
{
"urn": "do:reservedip:1.2.3.4",
"assigned_at": "2018-09-27 00:00:00",
"links": {
"self": "http://example.com/v2/reserved_ips/1.2.3.4"
}
}
],
"links": {
Expand Down Expand Up @@ -452,6 +473,7 @@ func TestProjects_AssignFleetResourcesWithTypes(t *testing.T) {
assignableResources := []interface{}{
&Droplet{ID: 1234},
&FloatingIP{IP: "1.2.3.4"},
&ReservedIP{IP: "1.2.3.4"},
}

mockResp := `
Expand All @@ -470,6 +492,13 @@ func TestProjects_AssignFleetResourcesWithTypes(t *testing.T) {
"links": {
"self": "http://example.com/v2/floating_ips/1.2.3.4"
}
},
{
"urn": "do:reservedip:1.2.3.4",
"assigned_at": "2018-09-27 00:00:00",
"links": {
"self": "http://example.com/v2/reserved_ips/1.2.3.4"
}
}
]
}`
Expand All @@ -482,7 +511,7 @@ func TestProjects_AssignFleetResourcesWithTypes(t *testing.T) {
}

req := strings.TrimSuffix(string(reqBytes), "\n")
expectedReq := `{"resources":["do:droplet:1234","do:floatingip:1.2.3.4"]}`
expectedReq := `{"resources":["do:droplet:1234","do:floatingip:1.2.3.4","do:reservedip:1.2.3.4"]}`
if req != expectedReq {
t.Errorf("projects assign req didn't match up:\n expected %+v\n got %+v\n", expectedReq, req)
}
Expand All @@ -503,6 +532,7 @@ func TestProjects_AssignFleetResourcesWithStrings(t *testing.T) {
assignableResources := []interface{}{
"do:droplet:1234",
"do:floatingip:1.2.3.4",
"do:reservedip:1.2.3.4",
}

mockResp := `
Expand All @@ -521,6 +551,13 @@ func TestProjects_AssignFleetResourcesWithStrings(t *testing.T) {
"links": {
"self": "http://example.com/v2/floating_ips/1.2.3.4"
}
},
{
"urn": "do:reservedip:1.2.3.4",
"assigned_at": "2018-09-27 00:00:00",
"links": {
"self": "http://example.com/v2/reserved_ips/1.2.3.4"
}
}
]
}`
Expand All @@ -533,7 +570,7 @@ func TestProjects_AssignFleetResourcesWithStrings(t *testing.T) {
}

req := strings.TrimSuffix(string(reqBytes), "\n")
expectedReq := `{"resources":["do:droplet:1234","do:floatingip:1.2.3.4"]}`
expectedReq := `{"resources":["do:droplet:1234","do:floatingip:1.2.3.4","do:reservedip:1.2.3.4"]}`
if req != expectedReq {
t.Errorf("projects assign req didn't match up:\n expected %+v\n got %+v\n", expectedReq, req)
}
Expand All @@ -554,6 +591,7 @@ func TestProjects_AssignFleetResourcesWithStringsAndTypes(t *testing.T) {
assignableResources := []interface{}{
"do:droplet:1234",
&FloatingIP{IP: "1.2.3.4"},
&ReservedIP{IP: "1.2.3.4"},
}

mockResp := `
Expand All @@ -572,6 +610,13 @@ func TestProjects_AssignFleetResourcesWithStringsAndTypes(t *testing.T) {
"links": {
"self": "http://example.com/v2/floating_ips/1.2.3.4"
}
},
{
"urn": "do:reservedip:1.2.3.4",
"assigned_at": "2018-09-27 00:00:00",
"links": {
"self": "http://example.com/v2/reserved_ips/1.2.3.4"
}
}
]
}`
Expand All @@ -584,7 +629,7 @@ func TestProjects_AssignFleetResourcesWithStringsAndTypes(t *testing.T) {
}

req := strings.TrimSuffix(string(reqBytes), "\n")
expectedReq := `{"resources":["do:droplet:1234","do:floatingip:1.2.3.4"]}`
expectedReq := `{"resources":["do:droplet:1234","do:floatingip:1.2.3.4","do:reservedip:1.2.3.4"]}`
if req != expectedReq {
t.Errorf("projects assign req didn't match up:\n expected %+v\n got %+v\n", expectedReq, req)
}
Expand Down
145 changes: 145 additions & 0 deletions reserved_ips.go
@@ -0,0 +1,145 @@
package godo

import (
"context"
"fmt"
"net/http"
)

const resourceType = "ReservedIP"
const reservedIPsBasePath = "v2/reserved_ips"

// ReservedIPsService is an interface for interfacing with the reserved IPs
// endpoints of the Digital Ocean API.
// See: https://docs.digitalocean.com/reference/api/api-reference/#tag/Reserved-IPs
type ReservedIPsService interface {
List(context.Context, *ListOptions) ([]ReservedIP, *Response, error)
Get(context.Context, string) (*ReservedIP, *Response, error)
Create(context.Context, *ReservedIPCreateRequest) (*ReservedIP, *Response, error)
Delete(context.Context, string) (*Response, error)
}

// ReservedIPsServiceOp handles communication with the reserved IPs related methods of the
// DigitalOcean API.
type ReservedIPsServiceOp struct {
client *Client
}

var _ ReservedIPsService = &ReservedIPsServiceOp{}

// ReservedIP represents a Digital Ocean reserved IP.
type ReservedIP struct {
Region *Region `json:"region"`
Droplet *Droplet `json:"droplet"`
IP string `json:"ip"`
}

func (f ReservedIP) String() string {
return Stringify(f)
}

// URN returns the reserved IP in a valid DO API URN form.
func (f ReservedIP) URN() string {
return ToURN(resourceType, f.IP)
}

type reservedIPsRoot struct {
ReservedIPs []ReservedIP `json:"reserved_ips"`
Links *Links `json:"links"`
Meta *Meta `json:"meta"`
}

type reservedIPRoot struct {
ReservedIP *ReservedIP `json:"reserved_ip"`
Links *Links `json:"links,omitempty"`
}

// ReservedIPCreateRequest represents a request to create a reserved IP.
// Specify DropletID to assign the reserved IP to a Droplet or Region
// to reserve it to the region.
type ReservedIPCreateRequest struct {
Region string `json:"region,omitempty"`
DropletID int `json:"droplet_id,omitempty"`
}

// List all reserved IPs.
func (r *ReservedIPsServiceOp) List(ctx context.Context, opt *ListOptions) ([]ReservedIP, *Response, error) {
path := reservedIPsBasePath
path, err := addOptions(path, opt)
if err != nil {
return nil, nil, err
}

req, err := r.client.NewRequest(ctx, http.MethodGet, path, nil)
if err != nil {
return nil, nil, err
}

root := new(reservedIPsRoot)
resp, err := r.client.Do(ctx, req, root)
if err != nil {
return nil, resp, err
}
if l := root.Links; l != nil {
resp.Links = l
}
if m := root.Meta; m != nil {
resp.Meta = m
}

return root.ReservedIPs, resp, err
}

// Get an individual reserved IP.
func (r *ReservedIPsServiceOp) Get(ctx context.Context, ip string) (*ReservedIP, *Response, error) {
path := fmt.Sprintf("%s/%s", reservedIPsBasePath, ip)

req, err := r.client.NewRequest(ctx, http.MethodGet, path, nil)
if err != nil {
return nil, nil, err
}

root := new(reservedIPRoot)
resp, err := r.client.Do(ctx, req, root)
if err != nil {
return nil, resp, err
}

return root.ReservedIP, resp, err
}

// Create a reserved IP. If the DropletID field of the request is not empty,
// the reserved IP will also be assigned to the droplet.
func (r *ReservedIPsServiceOp) Create(ctx context.Context, createRequest *ReservedIPCreateRequest) (*ReservedIP, *Response, error) {
path := reservedIPsBasePath

req, err := r.client.NewRequest(ctx, http.MethodPost, path, createRequest)
if err != nil {
return nil, nil, err
}

root := new(reservedIPRoot)
resp, err := r.client.Do(ctx, req, root)
if err != nil {
return nil, resp, err
}
if l := root.Links; l != nil {
resp.Links = l
}

return root.ReservedIP, resp, err
}

// Delete a reserved IP.
func (r *ReservedIPsServiceOp) Delete(ctx context.Context, ip string) (*Response, error) {
path := fmt.Sprintf("%s/%s", reservedIPsBasePath, ip)

req, err := r.client.NewRequest(ctx, http.MethodDelete, path, nil)
if err != nil {
return nil, err
}

resp, err := r.client.Do(ctx, req, nil)

return resp, err
}

0 comments on commit 67d5985

Please sign in to comment.