From 73ff37fe5ff9cb5bcf97f9cdd4c63ee0e6e4ef7c Mon Sep 17 00:00:00 2001 From: Ashwani Date: Fri, 22 Nov 2024 21:21:03 +0530 Subject: [PATCH] Add reserved ipv6 changes as Beta (#759) * Add reserved ipv6 changes * Resolved review comments * added reserved ipv6 actions --------- Co-authored-by: Andrew Starr-Bochicchio --- godo.go | 78 ++++++++++--------- reserved_ipv6.go | 132 +++++++++++++++++++++++++++++++ reserved_ipv6_actions.go | 57 ++++++++++++++ reserved_ipv6_actions_test.go | 79 +++++++++++++++++++ reserved_ipv6_test.go | 143 ++++++++++++++++++++++++++++++++++ 5 files changed, 452 insertions(+), 37 deletions(-) create mode 100644 reserved_ipv6.go create mode 100644 reserved_ipv6_actions.go create mode 100644 reserved_ipv6_actions_test.go create mode 100644 reserved_ipv6_test.go diff --git a/godo.go b/godo.go index 4b9ad6b6..dc36e608 100644 --- a/godo.go +++ b/godo.go @@ -54,43 +54,45 @@ type Client struct { ratemtx sync.Mutex // Services used for communicating with the API - Account AccountService - Actions ActionsService - Apps AppsService - Balance BalanceService - BillingHistory BillingHistoryService - CDNs CDNService - Certificates CertificatesService - Databases DatabasesService - Domains DomainsService - Droplets DropletsService - DropletActions DropletActionsService - DropletAutoscale DropletAutoscaleService - Firewalls FirewallsService - FloatingIPs FloatingIPsService - FloatingIPActions FloatingIPActionsService - Functions FunctionsService - Images ImagesService - ImageActions ImageActionsService - Invoices InvoicesService - Keys KeysService - Kubernetes KubernetesService - LoadBalancers LoadBalancersService - Monitoring MonitoringService - OneClick OneClickService - Projects ProjectsService - Regions RegionsService - Registry RegistryService - Registries RegistriesService - ReservedIPs ReservedIPsService - ReservedIPActions ReservedIPActionsService - Sizes SizesService - Snapshots SnapshotsService - Storage StorageService - StorageActions StorageActionsService - Tags TagsService - UptimeChecks UptimeChecksService - VPCs VPCsService + Account AccountService + Actions ActionsService + Apps AppsService + Balance BalanceService + BillingHistory BillingHistoryService + CDNs CDNService + Certificates CertificatesService + Databases DatabasesService + Domains DomainsService + Droplets DropletsService + DropletActions DropletActionsService + DropletAutoscale DropletAutoscaleService + Firewalls FirewallsService + FloatingIPs FloatingIPsService + FloatingIPActions FloatingIPActionsService + Functions FunctionsService + Images ImagesService + ImageActions ImageActionsService + Invoices InvoicesService + Keys KeysService + Kubernetes KubernetesService + LoadBalancers LoadBalancersService + Monitoring MonitoringService + OneClick OneClickService + Projects ProjectsService + Regions RegionsService + Registry RegistryService + Registries RegistriesService + ReservedIPs ReservedIPsService + ReservedIPV6s ReservedIPV6sService + ReservedIPActions ReservedIPActionsService + ReservedIPV6Actions ReservedIPV6ActionsService + Sizes SizesService + Snapshots SnapshotsService + Storage StorageService + StorageActions StorageActionsService + Tags TagsService + UptimeChecks UptimeChecksService + VPCs VPCsService // Optional function called after every successful request made to the DO APIs onRequestCompleted RequestCompletionCallback @@ -295,7 +297,9 @@ func NewClient(httpClient *http.Client) *Client { c.Registry = &RegistryServiceOp{client: c} c.Registries = &RegistriesServiceOp{client: c} c.ReservedIPs = &ReservedIPsServiceOp{client: c} + c.ReservedIPV6s = &ReservedIPV6sServiceOp{client: c} c.ReservedIPActions = &ReservedIPActionsServiceOp{client: c} + c.ReservedIPV6Actions = &ReservedIPV6ActionsServiceOp{client: c} c.Sizes = &SizesServiceOp{client: c} c.Snapshots = &SnapshotsServiceOp{client: c} c.Storage = &StorageServiceOp{client: c} diff --git a/reserved_ipv6.go b/reserved_ipv6.go new file mode 100644 index 00000000..aa265635 --- /dev/null +++ b/reserved_ipv6.go @@ -0,0 +1,132 @@ +package godo + +import ( + "context" + "fmt" + "net/http" + "time" +) + +const resourceV6Type = "ReservedIPv6" +const reservedIPV6sBasePath = "v2/reserved_ipv6" + +// ReservedIPV6sService is an interface for interfacing with the reserved IPV6s +// endpoints of the Digital Ocean API. +type ReservedIPV6sService interface { + List(context.Context, *ListOptions) ([]ReservedIPV6, *Response, error) + Get(context.Context, string) (*ReservedIPV6, *Response, error) + Create(context.Context, *ReservedIPV6CreateRequest) (*ReservedIPV6, *Response, error) + Delete(context.Context, string) (*Response, error) +} + +// ReservedIPV6sServiceOp handles communication with the reserved IPs related methods of the +// DigitalOcean API. +type ReservedIPV6sServiceOp struct { + client *Client +} + +var _ ReservedIPV6sService = (*ReservedIPV6sServiceOp)(nil) + +// ReservedIPV6 represents a Digital Ocean reserved IP. +type ReservedIPV6 struct { + RegionSlug string `json:"region_slug"` + IP string `json:"ip"` + ReservedAt time.Time `json:"reserved_at"` + Droplet *Droplet `json:"droplet,omitempty"` +} + +func (f ReservedIPV6) String() string { + return Stringify(f) +} + +// URN returns the reserved IP in a valid DO API URN form. +func (f ReservedIPV6) URN() string { + return ToURN(resourceV6Type, f.IP) +} + +type reservedIPV6sRoot struct { + ReservedIPs []ReservedIPV6 `json:"reserved_ips"` + Links *Links `json:"links"` + Meta *Meta `json:"meta"` +} + +// ReservedIPV6CreateRequest represents a request to reserve a reserved IP. +type ReservedIPV6CreateRequest struct { + Region string `json:"region_slug,omitempty"` +} + +// List all reserved IPV6s. +func (r *ReservedIPV6sServiceOp) List(ctx context.Context, opt *ListOptions) ([]ReservedIPV6, *Response, error) { + path := reservedIPV6sBasePath + 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(reservedIPV6sRoot) + resp, err := r.client.Do(ctx, req, root) + if err != nil { + return nil, nil, 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 IPv6. +func (r *ReservedIPV6sServiceOp) Get(ctx context.Context, ip string) (*ReservedIPV6, *Response, error) { + path := fmt.Sprintf("%s/%s", reservedIPV6sBasePath, ip) + + req, err := r.client.NewRequest(ctx, http.MethodGet, path, nil) + if err != nil { + return nil, nil, err + } + + root := new(ReservedIPV6) + resp, err := r.client.Do(ctx, req, root) + if err != nil { + return nil, resp, err + } + + return root, resp, err +} + +// Create a new IPv6 +func (r *ReservedIPV6sServiceOp) Create(ctx context.Context, reserveRequest *ReservedIPV6CreateRequest) (*ReservedIPV6, *Response, error) { + path := reservedIPV6sBasePath + + req, err := r.client.NewRequest(ctx, http.MethodPost, path, reserveRequest) + if err != nil { + return nil, nil, err + } + + root := new(ReservedIPV6) + resp, err := r.client.Do(ctx, req, root) + if err != nil { + return nil, resp, err + } + + return root, resp, err +} + +// Delete a reserved IPv6. +func (r *ReservedIPV6sServiceOp) Delete(ctx context.Context, ip string) (*Response, error) { + path := fmt.Sprintf("%s/%s", reservedIPV6sBasePath, ip) + + req, err := r.client.NewRequest(ctx, http.MethodDelete, path, nil) + if err != nil { + return nil, err + } + + return r.client.Do(ctx, req, nil) +} diff --git a/reserved_ipv6_actions.go b/reserved_ipv6_actions.go new file mode 100644 index 00000000..dd14bc58 --- /dev/null +++ b/reserved_ipv6_actions.go @@ -0,0 +1,57 @@ +package godo + +import ( + "context" + "fmt" + "net/http" +) + +// ReservedIPActionsService is an interface for interfacing with the +// reserved IPs actions endpoints of the Digital Ocean API. +// See: https://docs.digitalocean.com/reference/api/api-reference/#tag/Reserved-IP-Actions +type ReservedIPV6ActionsService interface { + Assign(ctx context.Context, ip string, dropletID int) (*Action, *Response, error) + Unassign(ctx context.Context, ip string) (*Action, *Response, error) +} + +// ReservedIPActionsServiceOp handles communication with the reserved IPs +// action related methods of the DigitalOcean API. +type ReservedIPV6ActionsServiceOp struct { + client *Client +} + +// Assign a reserved IP to a droplet. +func (s *ReservedIPV6ActionsServiceOp) Assign(ctx context.Context, ip string, dropletID int) (*Action, *Response, error) { + request := &ActionRequest{ + "type": "assign", + "droplet_id": dropletID, + } + return s.doV6Action(ctx, ip, request) +} + +// Unassign a rerserved IP from the droplet it is currently assigned to. +func (s *ReservedIPV6ActionsServiceOp) Unassign(ctx context.Context, ip string) (*Action, *Response, error) { + request := &ActionRequest{"type": "unassign"} + return s.doV6Action(ctx, ip, request) +} + +func (s *ReservedIPV6ActionsServiceOp) doV6Action(ctx context.Context, ip string, request *ActionRequest) (*Action, *Response, error) { + path := reservedIPV6ActionPath(ip) + + req, err := s.client.NewRequest(ctx, http.MethodPost, path, request) + if err != nil { + return nil, nil, err + } + + root := new(actionRoot) + resp, err := s.client.Do(ctx, req, root) + if err != nil { + return nil, resp, err + } + + return root.Event, resp, err +} + +func reservedIPV6ActionPath(ip string) string { + return fmt.Sprintf("%s/%s/actions", reservedIPV6sBasePath, ip) +} diff --git a/reserved_ipv6_actions_test.go b/reserved_ipv6_actions_test.go new file mode 100644 index 00000000..3a71c6f9 --- /dev/null +++ b/reserved_ipv6_actions_test.go @@ -0,0 +1,79 @@ +package godo + +import ( + "encoding/json" + "fmt" + "net/http" + "reflect" + "testing" +) + +func TestReservedIPV6sActions_Assign(t *testing.T) { + setup() + defer teardown() + dropletID := 12345 + assignRequest := &ActionRequest{ + "droplet_id": float64(dropletID), + "type": "assign", + } + + mux.HandleFunc("/v2/reserved_ipv6/2604:a880:800:14::42c3:d000/actions", func(w http.ResponseWriter, r *http.Request) { + v := new(ActionRequest) + err := json.NewDecoder(r.Body).Decode(v) + if err != nil { + t.Fatalf("decode json: %v", err) + } + + testMethod(t, r, http.MethodPost) + if !reflect.DeepEqual(v, assignRequest) { + t.Errorf("Request body = %#v, expected %#v", v, assignRequest) + } + + fmt.Fprintf(w, `{"action":{"status":"in-progress","id":1,"type":"assign_ip","resource_type":"reserved_ipv6"}}`) + + }) + + assign, _, err := client.ReservedIPV6Actions.Assign(ctx, "2604:a880:800:14::42c3:d000", 12345) + if err != nil { + t.Errorf("ReservedIPV6sActions.Assign returned error: %v", err) + } + + expected := &Action{Status: "in-progress", ID: 1, Type: "assign_ip", ResourceType: "reserved_ipv6"} + if !reflect.DeepEqual(assign, expected) { + t.Errorf("ReservedIPV6sActions.Assign returned %+v, expected %+v", assign, expected) + } +} + +func TestReservedIPV6sActions_Unassign(t *testing.T) { + setup() + defer teardown() + + unassignRequest := &ActionRequest{ + "type": "unassign", + } + + mux.HandleFunc("/v2/reserved_ipv6/2604:a880:800:14::42c3:d000/actions", func(w http.ResponseWriter, r *http.Request) { + v := new(ActionRequest) + err := json.NewDecoder(r.Body).Decode(v) + if err != nil { + t.Fatalf("decode json: %v", err) + } + + testMethod(t, r, http.MethodPost) + if !reflect.DeepEqual(v, unassignRequest) { + t.Errorf("Request body = %+v, expected %+v", v, unassignRequest) + } + + fmt.Fprintf(w, `{"action":{"status":"in-progress","id":1,"type":"unassign_ip","resource_type":"reserved_ipv6"}}`) + }) + + action, _, err := client.ReservedIPV6Actions.Unassign(ctx, "2604:a880:800:14::42c3:d000") + if err != nil { + t.Errorf("ReservedIPV6sActions.Unassign returned error: %v", err) + } + + expected := &Action{Status: "in-progress", ID: 1, Type: "unassign_ip", ResourceType: "reserved_ipv6"} + if !reflect.DeepEqual(action, expected) { + t.Errorf("ReservedIPV6sActions.Unassign returned %+v, expected %+v", action, expected) + } +} diff --git a/reserved_ipv6_test.go b/reserved_ipv6_test.go new file mode 100644 index 00000000..a7351690 --- /dev/null +++ b/reserved_ipv6_test.go @@ -0,0 +1,143 @@ +package godo + +import ( + "encoding/json" + "fmt" + "net/http" + "reflect" + "testing" + "time" +) + +func TestReservedIPV6s_Create(t *testing.T) { + setup() + defer teardown() + + reserveRequest := &ReservedIPV6CreateRequest{ + Region: "nyc3", + } + nowTime := time.Now() + + mux.HandleFunc("/v2/reserved_ipv6", func(w http.ResponseWriter, r *http.Request) { + v := new(ReservedIPV6CreateRequest) + err := json.NewDecoder(r.Body).Decode(v) + if err != nil { + t.Fatal(err) + } + + testMethod(t, r, http.MethodPost) + if !reflect.DeepEqual(v, reserveRequest) { + t.Errorf("Request body = %+v, expected %+v", v, reserveRequest) + } + + fmt.Fprint(w, `{"ip":"2604:a880:800:14::42c3:d000","region_slug":"nyc3","reserved_at":"`+nowTime.Format(time.RFC3339Nano)+`"}`) + }) + + reservedIP, _, err := client.ReservedIPV6s.Create(ctx, reserveRequest) + if err != nil { + t.Errorf("ReservedIPV6s.Create returned error: %v", err) + } + + expected := &ReservedIPV6{RegionSlug: "nyc3", IP: "2604:a880:800:14::42c3:d000", ReservedAt: nowTime} + + if !equalReserveIPv6Objects(reservedIP, expected) { + t.Errorf("ReservedIPs.Create returned %+v, expected %+v", reservedIP, expected) + } +} + +func equalReserveIPv6Objects(a, b *ReservedIPV6) bool { + return a.IP == b.IP && + a.RegionSlug == b.RegionSlug && + a.ReservedAt.Equal(b.ReservedAt) && + reflect.DeepEqual(a.Droplet, b.Droplet) +} + +func TestReservedIPV6s_List(t *testing.T) { + setup() + defer teardown() + + mux.HandleFunc("/v2/reserved_ipv6", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, http.MethodGet) + fmt.Fprint(w, `{"reserved_ips": [ + {"region_slug":"nyc3","droplet":{"id":1},"ip":"2604:a880:800:14::42c3:d000"}, + {"region_slug":"nyc3","droplet":{"id":2},"ip":"2604:a880:800:14::42c3:d001"} + ], + "meta":{"total":2} + }`) + }) + + reservedIPs, resp, err := client.ReservedIPV6s.List(ctx, nil) + if err != nil { + t.Errorf("ReservedIPs.List returned error: %v", err) + } + + expectedReservedIPs := []ReservedIPV6{ + {RegionSlug: "nyc3", Droplet: &Droplet{ID: 1}, IP: "2604:a880:800:14::42c3:d000"}, + {RegionSlug: "nyc3", Droplet: &Droplet{ID: 2}, IP: "2604:a880:800:14::42c3:d001"}, + } + if !reflect.DeepEqual(reservedIPs, expectedReservedIPs) { + t.Errorf("ReservedIPV6s.List returned reserved IPs %+v, expected %+v", reservedIPs, expectedReservedIPs) + } + + expectedMeta := &Meta{ + Total: 2, + } + if !reflect.DeepEqual(resp.Meta, expectedMeta) { + t.Errorf("ReservedIPs.List returned meta %+v, expected %+v", resp.Meta, expectedMeta) + } +} + +func TestReservedIPV6s_ListReservedIPsMultiplePages(t *testing.T) { + setup() + defer teardown() + + mux.HandleFunc("/v2/reserved_ipv6", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, http.MethodGet) + fmt.Fprint(w, `{"reserved_ips": [ + {"region_slug":"nyc3","droplet":{"id":1},"ip":"2604:a880:800:14::42c3:d001"}, + {"region":{"slug":"nyc3"},"droplet":{"id":2},"ip":"2604:a880:800:14::42c3:d002"}], + "links":{"pages":{"next":"http://example.com/v2/reserved_ipv6/?page=2"}}} + `) + }) + + _, resp, err := client.ReservedIPV6s.List(ctx, nil) + if err != nil { + t.Fatal(err) + } + + checkCurrentPage(t, resp, 1) +} + +func TestReservedIPV6s_Get(t *testing.T) { + setup() + defer teardown() + nowTime := time.Now() + mux.HandleFunc("/v2/reserved_ipv6/2604:a880:800:14::42c3:d001", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, http.MethodGet) + fmt.Fprint(w, `{"region_slug":"nyc3","droplet":{"id":1},"ip":"2604:a880:800:14::42c3:d001", "reserved_at":"`+nowTime.Format(time.RFC3339Nano)+`"}`) + }) + + reservedIP, _, err := client.ReservedIPV6s.Get(ctx, "2604:a880:800:14::42c3:d001") + if err != nil { + t.Errorf("ReservedIPV6s.Get returned error: %v", err) + } + + expected := &ReservedIPV6{RegionSlug: "nyc3", Droplet: &Droplet{ID: 1}, IP: "2604:a880:800:14::42c3:d001", ReservedAt: nowTime} + if !equalReserveIPv6Objects(reservedIP, expected) { + t.Errorf("ReservedIPV6s.Get returned %+v, expected %+v", reservedIP, expected) + } +} + +func TestReservedIPV6s_Delete(t *testing.T) { + setup() + defer teardown() + + mux.HandleFunc("/v2/reserved_ipv6/2604:a880:800:14::42c3:d001", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, http.MethodDelete) + }) + + _, err := client.ReservedIPV6s.Delete(ctx, "2604:a880:800:14::42c3:d001") + if err != nil { + t.Errorf("ReservedIPV6s.Release returned error: %v", err) + } +}