From 290653f512e0264d2796c19dc33d08d87816853a Mon Sep 17 00:00:00 2001 From: Antoine Grondin Date: Mon, 18 Apr 2016 11:38:29 +0200 Subject: [PATCH] storage: support for beta storage API --- droplets.go | 23 +++ droplets_test.go | 30 ++- godo.go | 13 +- storage.go | 241 ++++++++++++++++++++++++ storage_actions.go | 61 +++++++ storage_actions_test.go | 73 ++++++++ storage_test.go | 394 ++++++++++++++++++++++++++++++++++++++++ 7 files changed, 824 insertions(+), 11 deletions(-) create mode 100644 storage.go create mode 100644 storage_actions.go create mode 100644 storage_actions_test.go create mode 100644 storage_test.go diff --git a/droplets.go b/droplets.go index 2ec2bbe4..87c870c3 100644 --- a/droplets.go +++ b/droplets.go @@ -55,6 +55,7 @@ type Droplet struct { Created string `json:"created_at,omitempty"` Kernel *Kernel `json:"kernel,omitempty"` Tags []string `json:"tags,ommitempty"` + DriveIDs []string `json:"drives"` } // PublicIPv4 returns the public IPv4 address for the Droplet. @@ -146,6 +147,27 @@ type DropletCreateImage struct { Slug string } +// DropletCreateDrive identifies a drive to attach for the create request. It +// prefers Name over ID, +type DropletCreateDrive struct { + ID string + Name string +} + +// MarshalJSON returns an object with either the name or id of the drive. It +// returns the id if the name is empty. +func (d DropletCreateDrive) MarshalJSON() ([]byte, error) { + if d.Name != "" { + return json.Marshal(struct { + Name string `json:"name"` + }{Name: d.Name}) + } + + return json.Marshal(struct { + ID string `json:"id"` + }{ID: d.ID}) +} + // MarshalJSON returns either the slug or id of the image. It returns the id // if the slug is empty. func (d DropletCreateImage) MarshalJSON() ([]byte, error) { @@ -183,6 +205,7 @@ type DropletCreateRequest struct { IPv6 bool `json:"ipv6"` PrivateNetworking bool `json:"private_networking"` UserData string `json:"user_data,omitempty"` + Drives []DropletCreateDrive `json:"drives,omitempty"` } // DropletMultiCreateRequest is a request to create multiple droplets. diff --git a/droplets_test.go b/droplets_test.go index 60423dbe..30dfb75a 100644 --- a/droplets_test.go +++ b/droplets_test.go @@ -24,7 +24,7 @@ func TestDroplets_ListDroplets(t *testing.T) { expected := []Droplet{{ID: 1}, {ID: 2}} if !reflect.DeepEqual(droplets, expected) { - t.Errorf("Droplets.List returned %+v, expected %+v", droplets, expected) + t.Errorf("Droplets.List\n got=%#v\nwant=%#v", droplets, expected) } } @@ -132,7 +132,7 @@ func TestDroplets_GetDroplet(t *testing.T) { expected := &Droplet{ID: 12345} if !reflect.DeepEqual(droplets, expected) { - t.Errorf("Droplets.Get returned %+v, expected %+v", droplets, expected) + t.Errorf("Droplets.Get\n got=%#v\nwant=%#v", droplets, expected) } } @@ -147,6 +147,11 @@ func TestDroplets_Create(t *testing.T) { Image: DropletCreateImage{ ID: 1, }, + Drives: []DropletCreateDrive{ + {Name: "hello-im-a-drive"}, + {ID: "hello-im-another-drive"}, + {Name: "hello-im-still-a-drive", ID: "should be ignored due to Name"}, + }, } mux.HandleFunc("/v2/droplets", func(w http.ResponseWriter, r *http.Request) { @@ -159,6 +164,11 @@ func TestDroplets_Create(t *testing.T) { "backups": false, "ipv6": false, "private_networking": false, + "drives": []interface{}{ + map[string]interface{}{"name": "hello-im-a-drive"}, + map[string]interface{}{"id": "hello-im-another-drive"}, + map[string]interface{}{"name": "hello-im-still-a-drive"}, + }, } var v map[string]interface{} @@ -168,7 +178,7 @@ func TestDroplets_Create(t *testing.T) { } if !reflect.DeepEqual(v, expected) { - t.Errorf("Request body = %#v, expected %#v", v, expected) + t.Errorf("Request body\n got=%#v\nwant=%#v", v, expected) } fmt.Fprintf(w, `{"droplet":{"id":1}, "links":{"actions": [{"id": 1, "href": "http://example.com", "rel": "create"}]}}`) @@ -293,7 +303,7 @@ func TestDroplets_Kernels(t *testing.T) { expected := []Kernel{{ID: 1}, {ID: 2}} if !reflect.DeepEqual(kernels, expected) { - t.Errorf("Droplets.Kernels returned %+v, expected %+v", kernels, expected) + t.Errorf("Droplets.Kernels\n got=%#v\nwant=%#v", kernels, expected) } } @@ -314,7 +324,7 @@ func TestDroplets_Snapshots(t *testing.T) { expected := []Image{{ID: 1}, {ID: 2}} if !reflect.DeepEqual(snapshots, expected) { - t.Errorf("Droplets.Snapshots returned %+v, expected %+v", snapshots, expected) + t.Errorf("Droplets.Snapshots\n got=%#v\nwant=%#v", snapshots, expected) } } @@ -335,7 +345,7 @@ func TestDroplets_Backups(t *testing.T) { expected := []Image{{ID: 1}, {ID: 2}} if !reflect.DeepEqual(backups, expected) { - t.Errorf("Droplets.Backups returned %+v, expected %+v", backups, expected) + t.Errorf("Droplets.Backups\n got=%#v\nwant=%#v", backups, expected) } } @@ -356,7 +366,7 @@ func TestDroplets_Actions(t *testing.T) { expected := []Action{{ID: 1}, {ID: 2}} if !reflect.DeepEqual(actions, expected) { - t.Errorf("Droplets.Actions returned %+v, expected %+v", actions, expected) + t.Errorf("Droplets.Actions\n got=%#v\nwant=%#v", actions, expected) } } @@ -376,7 +386,7 @@ func TestDroplets_Neighbors(t *testing.T) { expected := []Droplet{{ID: 1}, {ID: 2}} if !reflect.DeepEqual(neighbors, expected) { - t.Errorf("Droplets.Neighbors returned %+v, expected %+v", neighbors, expected) + t.Errorf("Droplets.Neighbors\n got=%#v\nwant=%#v", neighbors, expected) } } @@ -390,7 +400,7 @@ func TestNetworkV4_String(t *testing.T) { stringified := network.String() expected := `godo.NetworkV4{IPAddress:"192.168.1.2", Netmask:"255.255.255.0", Gateway:"192.168.1.1", Type:""}` if expected != stringified { - t.Errorf("NetworkV4.String returned %+v, expected %+v", stringified, expected) + t.Errorf("NetworkV4.String\n got=%#v\nwant=%#v", stringified, expected) } } @@ -404,7 +414,7 @@ func TestNetworkV6_String(t *testing.T) { stringified := network.String() expected := `godo.NetworkV6{IPAddress:"2604:A880:0800:0010:0000:0000:02DD:4001", Netmask:64, Gateway:"2604:A880:0800:0010:0000:0000:0000:0001", Type:""}` if expected != stringified { - t.Errorf("NetworkV6.String returned %+v, expected %+v", stringified, expected) + t.Errorf("NetworkV6.String\n got=%#v\nwant=%#v", stringified, expected) } } diff --git a/godo.go b/godo.go index c3aaedfb..ebcb7e8a 100644 --- a/godo.go +++ b/godo.go @@ -55,6 +55,8 @@ type Client struct { Sizes SizesService FloatingIPs FloatingIPsService FloatingIPActions FloatingIPActionsService + Storage StorageService + StorageActions StorageActionsService Tags TagsService // Optional function called after every successful request made to the DO APIs @@ -94,7 +96,10 @@ type ErrorResponse struct { Response *http.Response // Error message - Message string + Message string `json:"message"` + + // RequestID returned from the API, useful to contact support. + RequestID string `json:"request_id"` } // Rate contains the rate limit for the current client. @@ -157,6 +162,8 @@ func NewClient(httpClient *http.Client) *Client { c.Sizes = &SizesServiceOp{client: c} c.FloatingIPs = &FloatingIPsServiceOp{client: c} c.FloatingIPActions = &FloatingIPActionsServiceOp{client: c} + c.Storage = &StorageServiceOp{client: c} + c.StorageActions = &StorageActionsServiceOp{client: c} c.Tags = &TagsServiceOp{client: c} return c @@ -324,6 +331,10 @@ func (c *Client) Do(req *http.Request, v interface{}) (*Response, error) { return response, err } func (r *ErrorResponse) Error() string { + if r.RequestID != "" { + return fmt.Sprintf("%v %v: %d (request %q) %v", + r.Response.Request.Method, r.Response.Request.URL, r.Response.StatusCode, r.RequestID, r.Message) + } return fmt.Sprintf("%v %v: %d %v", r.Response.Request.Method, r.Response.Request.URL, r.Response.StatusCode, r.Message) } diff --git a/storage.go b/storage.go new file mode 100644 index 00000000..1c56a466 --- /dev/null +++ b/storage.go @@ -0,0 +1,241 @@ +package godo + +import ( + "fmt" + "time" +) + +const ( + storageBasePath = "v2" + storageAllocPath = storageBasePath + "/drives" + storageSnapPath = storageBasePath + "/snapshots" +) + +// StorageService is an interface for interfacing with the storage +// endpoints of the Digital Ocean API. +// See: https://developers.digitalocean.com/documentation/v2#storage +type StorageService interface { + ListDrives(*ListOptions) ([]Drive, *Response, error) + GetDrive(string) (*Drive, *Response, error) + CreateDrive(*DriveCreateRequest) (*Drive, *Response, error) + DeleteDrive(string) (*Response, error) + + ListSnapshots(driveID string, opts *ListOptions) ([]Snapshot, *Response, error) + GetSnapshot(string) (*Snapshot, *Response, error) + CreateSnapshot(*SnapshotCreateRequest) (*Snapshot, *Response, error) + DeleteSnapshot(string) (*Response, error) +} + +// StorageServiceOp handles communication with the storage drives related methods of the +// DigitalOcean API. +type StorageServiceOp struct { + client *Client +} + +var _ StorageService = &StorageServiceOp{} + +// Drive represents a Digital Ocean block store drive. +type Drive struct { + ID string `json:"id"` + Region *Region `json:"region"` + Name string `json:"name"` + SizeGigaBytes int64 `json:"size_gigabytes"` + Description string `json:"description"` + DropletIDs []int `json:"droplet_ids"` + CreatedAt time.Time `json:"created_at"` +} + +func (f Drive) String() string { + return Stringify(f) +} + +type storageDrivesRoot struct { + Drives []Drive `json:"drives"` + Links *Links `json:"links"` +} + +type storageDriveRoot struct { + Drive *Drive `json:"drive"` + Links *Links `json:"links,omitempty"` +} + +// DriveCreateRequest represents a request to create a block store +// drive. +type DriveCreateRequest struct { + Region string `json:"region"` + Name string `json:"name"` + Description string `json:"description"` + SizeGibiBytes int64 `json:"size_gigabytes"` +} + +// ListDrives lists all storage drives. +func (svc *StorageServiceOp) ListDrives(opt *ListOptions) ([]Drive, *Response, error) { + path, err := addOptions(storageAllocPath, opt) + if err != nil { + return nil, nil, err + } + + req, err := svc.client.NewRequest("GET", path, nil) + if err != nil { + return nil, nil, err + } + + root := new(storageDrivesRoot) + resp, err := svc.client.Do(req, root) + if err != nil { + return nil, resp, err + } + + if l := root.Links; l != nil { + resp.Links = l + } + + return root.Drives, resp, nil +} + +// CreateDrive creates a storage drive. The name must be unique. +func (svc *StorageServiceOp) CreateDrive(createRequest *DriveCreateRequest) (*Drive, *Response, error) { + path := storageAllocPath + + req, err := svc.client.NewRequest("POST", path, createRequest) + if err != nil { + return nil, nil, err + } + + root := new(storageDriveRoot) + resp, err := svc.client.Do(req, root) + if err != nil { + return nil, resp, err + } + return root.Drive, resp, nil +} + +// GetDrive retrieves an individual storage drive. +func (svc *StorageServiceOp) GetDrive(id string) (*Drive, *Response, error) { + path := fmt.Sprintf("%s/%s", storageAllocPath, id) + + req, err := svc.client.NewRequest("GET", path, nil) + if err != nil { + return nil, nil, err + } + + root := new(storageDriveRoot) + resp, err := svc.client.Do(req, root) + if err != nil { + return nil, resp, err + } + + return root.Drive, resp, nil +} + +// DeleteDrive deletes a storage drive. +func (svc *StorageServiceOp) DeleteDrive(id string) (*Response, error) { + path := fmt.Sprintf("%s/%s", storageAllocPath, id) + + req, err := svc.client.NewRequest("DELETE", path, nil) + if err != nil { + return nil, err + } + return svc.client.Do(req, nil) +} + +// Snapshot represents a Digital Ocean block store snapshot. +type Snapshot struct { + ID string `json:"id"` + DriveID string `json:"drive_id"` + Region *Region `json:"region"` + Name string `json:"name"` + SizeGibiBytes int64 `json:"size_gigabytes"` + Description string `json:"description"` + CreatedAt time.Time `json:"created_at"` +} + +type storageSnapsRoot struct { + Snapshots []Snapshot `json:"snapshots"` + Links *Links `json:"links"` +} + +type storageSnapRoot struct { + Snapshot *Snapshot `json:"snapshot"` + Links *Links `json:"links,omitempty"` +} + +// SnapshotCreateRequest represents a request to create a block store +// drive. +type SnapshotCreateRequest struct { + DriveID string `json:"drive_id"` + Name string `json:"name"` + Description string `json:"description"` +} + +// ListSnapshots lists all snapshots related to a storage drive. +func (svc *StorageServiceOp) ListSnapshots(driveID string, opt *ListOptions) ([]Snapshot, *Response, error) { + path := fmt.Sprintf("%s/%s/snapshots", storageAllocPath, driveID) + path, err := addOptions(path, opt) + if err != nil { + return nil, nil, err + } + + req, err := svc.client.NewRequest("GET", path, nil) + if err != nil { + return nil, nil, err + } + + root := new(storageSnapsRoot) + resp, err := svc.client.Do(req, root) + if err != nil { + return nil, resp, err + } + + if l := root.Links; l != nil { + resp.Links = l + } + + return root.Snapshots, resp, nil +} + +// CreateSnapshot creates a snapshot of a storage drive. +func (svc *StorageServiceOp) CreateSnapshot(createRequest *SnapshotCreateRequest) (*Snapshot, *Response, error) { + path := fmt.Sprintf("%s/%s/snapshots", storageAllocPath, createRequest.DriveID) + + req, err := svc.client.NewRequest("POST", path, createRequest) + if err != nil { + return nil, nil, err + } + + root := new(storageSnapRoot) + resp, err := svc.client.Do(req, root) + if err != nil { + return nil, resp, err + } + return root.Snapshot, resp, nil +} + +// GetSnapshot retrieves an individual snapshot. +func (svc *StorageServiceOp) GetSnapshot(id string) (*Snapshot, *Response, error) { + path := fmt.Sprintf("%s/%s", storageSnapPath, id) + + req, err := svc.client.NewRequest("GET", path, nil) + if err != nil { + return nil, nil, err + } + + root := new(storageSnapRoot) + resp, err := svc.client.Do(req, root) + if err != nil { + return nil, resp, err + } + + return root.Snapshot, resp, nil +} + +// DeleteSnapshot deletes a snapshot. +func (svc *StorageServiceOp) DeleteSnapshot(id string) (*Response, error) { + path := fmt.Sprintf("%s/%s", storageSnapPath, id) + + req, err := svc.client.NewRequest("DELETE", path, nil) + if err != nil { + return nil, err + } + return svc.client.Do(req, nil) +} diff --git a/storage_actions.go b/storage_actions.go new file mode 100644 index 00000000..9e5576c9 --- /dev/null +++ b/storage_actions.go @@ -0,0 +1,61 @@ +package godo + +import "fmt" + +// StorageActionsService is an interface for interfacing with the +// storage actions endpoints of the Digital Ocean API. +// See: https://developers.digitalocean.com/documentation/v2#storage-actions +type StorageActionsService interface { + Attach(driveID string, dropletID int) (*Action, *Response, error) + Detach(driveID string) (*Action, *Response, error) +} + +// StorageActionsServiceOp handles communication with the floating IPs +// action related methods of the DigitalOcean API. +type StorageActionsServiceOp struct { + client *Client +} + +// StorageAttachment represents the attachement of a block storage +// drive to a specific droplet under the device name. +type StorageAttachment struct { + DropletID int `json:"droplet_id"` +} + +// Attach a storage drive to a droplet. +func (s *StorageActionsServiceOp) Attach(driveID string, dropletID int) (*Action, *Response, error) { + request := &ActionRequest{ + "type": "attach", + "droplet_id": dropletID, + } + return s.doAction(driveID, request) +} + +// Detach a storage drive from a droplet. +func (s *StorageActionsServiceOp) Detach(driveID string) (*Action, *Response, error) { + request := &ActionRequest{ + "type": "detach", + } + return s.doAction(driveID, request) +} + +func (s *StorageActionsServiceOp) doAction(driveID string, request *ActionRequest) (*Action, *Response, error) { + path := storageAllocationActionPath(driveID) + + req, err := s.client.NewRequest("POST", path, request) + if err != nil { + return nil, nil, err + } + + root := new(actionRoot) + resp, err := s.client.Do(req, root) + if err != nil { + return nil, resp, err + } + + return &root.Event, resp, err +} + +func storageAllocationActionPath(driveID string) string { + return fmt.Sprintf("%s/%s/actions", storageAllocPath, driveID) +} diff --git a/storage_actions_test.go b/storage_actions_test.go new file mode 100644 index 00000000..67b3ed1c --- /dev/null +++ b/storage_actions_test.go @@ -0,0 +1,73 @@ +package godo + +import ( + "encoding/json" + "fmt" + "net/http" + "reflect" + "testing" +) + +func TestStoragesActions_Attach(t *testing.T) { + setup() + defer teardown() + const ( + driveID = "98d414c6-295e-4e3a-ac58-eb9456c1e1d1" + dropletID = 12345 + ) + + attachRequest := &ActionRequest{ + "type": "attach", + "droplet_id": float64(dropletID), // encoding/json decodes numbers as floats + } + + mux.HandleFunc("/v2/drives/"+driveID+"/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, "POST") + if !reflect.DeepEqual(v, attachRequest) { + t.Errorf("want=%#v", attachRequest) + t.Errorf("got=%#v", v) + } + fmt.Fprintf(w, `{"action":{"status":"in-progress"}}`) + }) + + _, _, err := client.StorageActions.Attach(driveID, dropletID) + if err != nil { + t.Errorf("StoragesActions.Attach returned error: %v", err) + } +} + +func TestStoragesActions_Detach(t *testing.T) { + setup() + defer teardown() + driveID := "98d414c6-295e-4e3a-ac58-eb9456c1e1d1" + + detachRequest := &ActionRequest{ + "type": "detach", + } + + mux.HandleFunc("/v2/drives/"+driveID+"/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, "POST") + if !reflect.DeepEqual(v, detachRequest) { + t.Errorf("want=%#v", detachRequest) + t.Errorf("got=%#v", v) + } + fmt.Fprintf(w, `{"action":{"status":"in-progress"}}`) + }) + + _, _, err := client.StorageActions.Detach(driveID) + if err != nil { + t.Errorf("StoragesActions.Detach returned error: %v", err) + } +} diff --git a/storage_test.go b/storage_test.go new file mode 100644 index 00000000..c58e022e --- /dev/null +++ b/storage_test.go @@ -0,0 +1,394 @@ +package godo + +import ( + "encoding/json" + "fmt" + "net/http" + "reflect" + "testing" + "time" +) + +func TestStorageDrives_ListStorageDrives(t *testing.T) { + setup() + defer teardown() + + jBlob := ` + { + "drives": [ + { + "user_id": 42, + "region": {"slug": "nyc3"}, + "id": "80d414c6-295e-4e3a-ac58-eb9456c1e1d1", + "name": "my drive", + "description": "my description", + "size_gigabytes": 100, + "droplet_ids": [10], + "created_at": "2002-10-02T15:00:00.05Z" + }, + { + "user_id": 42, + "region": {"slug": "nyc3"}, + "id": "96d414c6-295e-4e3a-ac59-eb9456c1e1d1", + "name": "my other drive", + "description": "my other description", + "size_gigabytes": 100, + "created_at": "2012-10-03T15:00:01.05Z" + } + ], + "links": { + "pages": { + "last": "https://api.digitalocean.com/v2/drives?page=2", + "next": "https://api.digitalocean.com/v2/drives?page=2" + } + }, + "meta": { + "total": 28 + } + }` + + mux.HandleFunc("/v2/drives/", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "GET") + fmt.Fprint(w, jBlob) + }) + + drives, _, err := client.Storage.ListDrives(nil) + if err != nil { + t.Errorf("Storage.ListDrives returned error: %v", err) + } + + expected := []Drive{ + { + Region: &Region{Slug: "nyc3"}, + ID: "80d414c6-295e-4e3a-ac58-eb9456c1e1d1", + Name: "my drive", + Description: "my description", + SizeGigaBytes: 100, + DropletIDs: []int{10}, + CreatedAt: time.Date(2002, 10, 02, 15, 00, 00, 50000000, time.UTC), + }, + { + Region: &Region{Slug: "nyc3"}, + ID: "96d414c6-295e-4e3a-ac59-eb9456c1e1d1", + Name: "my other drive", + Description: "my other description", + SizeGigaBytes: 100, + CreatedAt: time.Date(2012, 10, 03, 15, 00, 01, 50000000, time.UTC), + }, + } + if !reflect.DeepEqual(drives, expected) { + t.Errorf("Storage.ListDrives returned %+v, expected %+v", drives, expected) + } +} + +func TestStorageDrives_Get(t *testing.T) { + setup() + defer teardown() + want := &Drive{ + Region: &Region{Slug: "nyc3"}, + ID: "80d414c6-295e-4e3a-ac58-eb9456c1e1d1", + Name: "my drive", + Description: "my description", + SizeGigaBytes: 100, + CreatedAt: time.Date(2002, 10, 02, 15, 00, 00, 50000000, time.UTC), + } + jBlob := `{ + "drive":{ + "region": {"slug":"nyc3"}, + "attached_to_droplet": null, + "id": "80d414c6-295e-4e3a-ac58-eb9456c1e1d1", + "name": "my drive", + "description": "my description", + "size_gigabytes": 100, + "created_at": "2002-10-02T15:00:00.05Z" + }, + "links": { + "pages": { + "last": "https://api.digitalocean.com/v2/drives?page=2", + "next": "https://api.digitalocean.com/v2/drives?page=2" + } + }, + "meta": { + "total": 28 + } + }` + + mux.HandleFunc("/v2/drives/80d414c6-295e-4e3a-ac58-eb9456c1e1d1", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "GET") + fmt.Fprint(w, jBlob) + }) + + got, _, err := client.Storage.GetDrive("80d414c6-295e-4e3a-ac58-eb9456c1e1d1") + if err != nil { + t.Errorf("Storage.GetDrive returned error: %v", err) + } + if !reflect.DeepEqual(got, want) { + t.Errorf("Storage.GetDrive returned %+v, want %+v", got, want) + } +} + +func TestStorageDrives_Create(t *testing.T) { + setup() + defer teardown() + + createRequest := &DriveCreateRequest{ + Region: "nyc3", + Name: "my drive", + Description: "my description", + SizeGibiBytes: 100, + } + + want := &Drive{ + Region: &Region{Slug: "nyc3"}, + ID: "80d414c6-295e-4e3a-ac58-eb9456c1e1d1", + Name: "my drive", + Description: "my description", + SizeGigaBytes: 100, + CreatedAt: time.Date(2002, 10, 02, 15, 00, 00, 50000000, time.UTC), + } + jBlob := `{ + "drive":{ + "region": {"slug":"nyc3"}, + "id": "80d414c6-295e-4e3a-ac58-eb9456c1e1d1", + "name": "my drive", + "description": "my description", + "size_gigabytes": 100, + "created_at": "2002-10-02T15:00:00.05Z" + }, + "links": {} + }` + + mux.HandleFunc("/v2/drives", func(w http.ResponseWriter, r *http.Request) { + v := new(DriveCreateRequest) + err := json.NewDecoder(r.Body).Decode(v) + if err != nil { + t.Fatal(err) + } + + testMethod(t, r, "POST") + if !reflect.DeepEqual(v, createRequest) { + t.Errorf("Request body = %+v, expected %+v", v, createRequest) + } + + fmt.Fprint(w, jBlob) + }) + + got, _, err := client.Storage.CreateDrive(createRequest) + if err != nil { + t.Errorf("Storage.CreateDrive returned error: %v", err) + } + if !reflect.DeepEqual(got, want) { + t.Errorf("Storage.CreateDrive returned %+v, want %+v", got, want) + } +} + +func TestStorageDrives_Destroy(t *testing.T) { + setup() + defer teardown() + + mux.HandleFunc("/v2/drives/80d414c6-295e-4e3a-ac58-eb9456c1e1d1", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "DELETE") + }) + + _, err := client.Storage.DeleteDrive("80d414c6-295e-4e3a-ac58-eb9456c1e1d1") + if err != nil { + t.Errorf("Storage.DeleteDrive returned error: %v", err) + } +} + +func TestStorageSnapshots_ListStorageSnapshots(t *testing.T) { + setup() + defer teardown() + + jBlob := ` + { + "snapshots": [ + { + "region": {"slug": "nyc3"}, + "id": "80d414c6-295e-4e3a-ac58-eb9456c1e1d1", + "drive_id": "98d414c6-295e-4e3a-ac58-eb9456c1e1d1", + "name": "my snapshot", + "description": "my description", + "size_gigabytes": 100, + "created_at": "2002-10-02T15:00:00.05Z" + }, + { + "region": {"slug": "nyc3"}, + "id": "96d414c6-295e-4e3a-ac59-eb9456c1e1d1", + "drive_id": "98d414c6-295e-4e3a-ac58-eb9456c1e1d1", + "name": "my other snapshot", + "description": "my other description", + "size_gigabytes": 100, + "created_at": "2012-10-03T15:00:01.05Z" + } + ], + "links": { + "pages": { + "last": "https://api.digitalocean.com/v2/drives?page=2", + "next": "https://api.digitalocean.com/v2/drives?page=2" + } + }, + "meta": { + "total": 28 + } + }` + + mux.HandleFunc("/v2/drives/98d414c6-295e-4e3a-ac58-eb9456c1e1d1/snapshots", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "GET") + fmt.Fprint(w, jBlob) + }) + + drives, _, err := client.Storage.ListSnapshots("98d414c6-295e-4e3a-ac58-eb9456c1e1d1", nil) + if err != nil { + t.Errorf("Storage.ListSnapshots returned error: %v", err) + } + + expected := []Snapshot{ + { + Region: &Region{Slug: "nyc3"}, + ID: "80d414c6-295e-4e3a-ac58-eb9456c1e1d1", + DriveID: "98d414c6-295e-4e3a-ac58-eb9456c1e1d1", + Name: "my snapshot", + Description: "my description", + SizeGibiBytes: 100, + CreatedAt: time.Date(2002, 10, 02, 15, 00, 00, 50000000, time.UTC), + }, + { + Region: &Region{Slug: "nyc3"}, + ID: "96d414c6-295e-4e3a-ac59-eb9456c1e1d1", + DriveID: "98d414c6-295e-4e3a-ac58-eb9456c1e1d1", + Name: "my other snapshot", + Description: "my other description", + SizeGibiBytes: 100, + CreatedAt: time.Date(2012, 10, 03, 15, 00, 01, 50000000, time.UTC), + }, + } + if !reflect.DeepEqual(drives, expected) { + t.Errorf("Storage.ListSnapshots returned %+v, expected %+v", drives, expected) + } +} + +func TestStorageSnapshots_Get(t *testing.T) { + setup() + defer teardown() + want := &Snapshot{ + Region: &Region{Slug: "nyc3"}, + ID: "80d414c6-295e-4e3a-ac58-eb9456c1e1d1", + DriveID: "98d414c6-295e-4e3a-ac58-eb9456c1e1d1", + Name: "my snapshot", + Description: "my description", + SizeGibiBytes: 100, + CreatedAt: time.Date(2002, 10, 02, 15, 00, 00, 50000000, time.UTC), + } + jBlob := `{ + "snapshot":{ + "region": {"slug": "nyc3"}, + "id": "80d414c6-295e-4e3a-ac58-eb9456c1e1d1", + "drive_id": "98d414c6-295e-4e3a-ac58-eb9456c1e1d1", + "name": "my snapshot", + "description": "my description", + "size_gigabytes": 100, + "created_at": "2002-10-02T15:00:00.05Z" + }, + "links": { + "pages": { + "last": "https://api.digitalocean.com/v2/drives/98d414c6-295e-4e3a-ac58-eb9456c1e1d1/snapshots?page=2", + "next": "https://api.digitalocean.com/v2/drives/98d414c6-295e-4e3a-ac58-eb9456c1e1d1/snapshots?page=2" + } + }, + "meta": { + "total": 28 + } + }` + + mux.HandleFunc("/v2/snapshots/80d414c6-295e-4e3a-ac58-eb9456c1e1d1", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "GET") + fmt.Fprint(w, jBlob) + }) + + got, _, err := client.Storage.GetSnapshot("80d414c6-295e-4e3a-ac58-eb9456c1e1d1") + if err != nil { + t.Errorf("Storage.GetSnapshot returned error: %v", err) + } + if !reflect.DeepEqual(got, want) { + t.Errorf("Storage.GetSnapshot returned %+v, want %+v", got, want) + } +} + +func TestStorageSnapshots_Create(t *testing.T) { + setup() + defer teardown() + + createRequest := &SnapshotCreateRequest{ + DriveID: "98d414c6-295e-4e3a-ac58-eb9456c1e1d1", + Name: "my snapshot", + Description: "my description", + } + + want := &Snapshot{ + Region: &Region{Slug: "nyc3"}, + ID: "80d414c6-295e-4e3a-ac58-eb9456c1e1d1", + DriveID: "98d414c6-295e-4e3a-ac58-eb9456c1e1d1", + Name: "my snapshot", + Description: "my description", + SizeGibiBytes: 100, + CreatedAt: time.Date(2002, 10, 02, 15, 00, 00, 50000000, time.UTC), + } + jBlob := `{ + "snapshot":{ + "region": {"slug": "nyc3"}, + "id": "80d414c6-295e-4e3a-ac58-eb9456c1e1d1", + "drive_id": "98d414c6-295e-4e3a-ac58-eb9456c1e1d1", + "name": "my snapshot", + "description": "my description", + "size_gigabytes": 100, + "created_at": "2002-10-02T15:00:00.05Z" + }, + "links": { + "pages": { + "last": "https://api.digitalocean.com/v2/drives/98d414c6-295e-4e3a-ac58-eb9456c1e1d1/snapshots?page=2", + "next": "https://api.digitalocean.com/v2/drives/98d414c6-295e-4e3a-ac58-eb9456c1e1d1/snapshots?page=2" + } + }, + "meta": { + "total": 28 + } + }` + + mux.HandleFunc("/v2/drives/98d414c6-295e-4e3a-ac58-eb9456c1e1d1/snapshots", func(w http.ResponseWriter, r *http.Request) { + v := new(SnapshotCreateRequest) + err := json.NewDecoder(r.Body).Decode(v) + if err != nil { + t.Fatal(err) + } + + testMethod(t, r, "POST") + if !reflect.DeepEqual(v, createRequest) { + t.Errorf("Request body = %+v, expected %+v", v, createRequest) + } + + fmt.Fprint(w, jBlob) + }) + + got, _, err := client.Storage.CreateSnapshot(createRequest) + if err != nil { + t.Errorf("Storage.CreateSnapshot returned error: %v", err) + } + if !reflect.DeepEqual(got, want) { + t.Errorf("Storage.CreateSnapshot returned %+v, want %+v", got, want) + } +} + +func TestStorageSnapshots_Destroy(t *testing.T) { + setup() + defer teardown() + + mux.HandleFunc("/v2/snapshots/80d414c6-295e-4e3a-ac58-eb9456c1e1d1", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "DELETE") + }) + + _, err := client.Storage.DeleteSnapshot("80d414c6-295e-4e3a-ac58-eb9456c1e1d1") + if err != nil { + t.Errorf("Storage.DeleteSnapshot returned error: %v", err) + } +}