Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for Reserved IP addresses #532

Merged
merged 2 commits into from Jun 15, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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"
}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a pedantic change that has no functional impact. The routes in the returned JSON were incorrect for the resource type.

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