From 78b1e66bf2edc53e4e614f0eb51f6c30608bfa1f Mon Sep 17 00:00:00 2001 From: Nan Zhong Date: Thu, 17 Mar 2016 00:34:29 -0400 Subject: [PATCH 1/5] Add tagging to godo --- godo.go | 2 + tags.go | 204 +++++++++++++++++++++++++++ tags_test.go | 384 +++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 590 insertions(+) create mode 100644 tags.go create mode 100644 tags_test.go diff --git a/godo.go b/godo.go index 47d61391..40790333 100644 --- a/godo.go +++ b/godo.go @@ -55,6 +55,7 @@ type Client struct { Sizes SizesService FloatingIPs FloatingIPsService FloatingIPActions FloatingIPActionsService + Tags TagsService // Optional function called after every successful request made to the DO APIs onRequestCompleted RequestCompletionCallback @@ -156,6 +157,7 @@ func NewClient(httpClient *http.Client) *Client { c.Sizes = &SizesServiceOp{client: c} c.FloatingIPs = &FloatingIPsServiceOp{client: c} c.FloatingIPActions = &FloatingIPActionsServiceOp{client: c} + c.Tags = &TagsServiceOp{client: c} return c } diff --git a/tags.go b/tags.go new file mode 100644 index 00000000..53d6ea14 --- /dev/null +++ b/tags.go @@ -0,0 +1,204 @@ +package godo + +import "fmt" + +const tagsBasePath = "v2/tags" + +type TagsService interface { + List(*ListOptions) ([]Tag, *Response, error) + Get(string) (*Tag, *Response, error) + Create(*TagCreateRequest) (*Tag, *Response, error) + Update(string, *TagUpdateRequest) (*Response, error) + Delete(string) (*Response, error) + + TagResources(string, *TagResourcesRequest) (*Response, error) + UntagResources(string, *UntagResourcesRequest) (*Response, error) +} + +type TagsServiceOp struct { + client *Client +} + +var _ TagsService = &TagsServiceOp{} + +type Resource struct { + ID string `json:"resource_id,omit_empty"` + Type string `json:"resource_type,omit_empty"` +} + +type TaggedResources struct { + Droplets *TaggedDropletsResources `json:"droplets,omitempty"` +} + +type TaggedDropletsResources struct { + Count int `json:"count,float64,omitempty"` + LastTagged *Droplet `json:"last_tagged,omitempty"` +} + +type Tag struct { + Name string `json:"name,omitempty"` + Resources *TaggedResources `json:"resources,omitempty"` +} + +type TagCreateRequest struct { + Name string `json:"name"` +} + +type TagUpdateRequest struct { + Name string `json:"name"` +} + +type TagResourcesRequest struct { + Resources []Resource `json:"resources"` +} + +type UntagResourcesRequest struct { + Resources []Resource `json:"resources"` +} + +type tagsRoot struct { + Tags []Tag `json:"tags"` + Links *Links `json:"links"` +} + +type tagRoot struct { + Tag *Tag `json:"tag"` +} + +// List all tags +func (s *TagsServiceOp) List(opt *ListOptions) ([]Tag, *Response, error) { + path := tagsBasePath + path, err := addOptions(path, opt) + + if err != nil { + return nil, nil, err + } + + req, err := s.client.NewRequest("GET", path, nil) + if err != nil { + return nil, nil, err + } + + root := new(tagsRoot) + resp, err := s.client.Do(req, root) + if err != nil { + return nil, resp, err + } + if l := root.Links; l != nil { + resp.Links = l + } + + return root.Tags, resp, err +} + +func (s *TagsServiceOp) Get(name string) (*Tag, *Response, error) { + path := fmt.Sprintf("%s/%s", tagsBasePath, name) + + req, err := s.client.NewRequest("GET", path, nil) + if err != nil { + return nil, nil, err + } + + root := new(tagRoot) + resp, err := s.client.Do(req, root) + if err != nil { + return nil, resp, err + } + + return root.Tag, resp, err +} + +func (s *TagsServiceOp) Create(createRequest *TagCreateRequest) (*Tag, *Response, error) { + if createRequest == nil { + return nil, nil, NewArgError("createRequest", "cannot be nil") + } + + req, err := s.client.NewRequest("POST", tagsBasePath, createRequest) + if err != nil { + return nil, nil, err + } + + root := new(tagRoot) + resp, err := s.client.Do(req, root) + if err != nil { + return nil, resp, err + } + + return root.Tag, resp, err +} + +func (s *TagsServiceOp) Update(name string, updateRequest *TagUpdateRequest) (*Response, error) { + if name == "" { + return nil, NewArgError("name", "cannot be empty") + } + + if updateRequest == nil { + return nil, NewArgError("updateRequest", "cannot be nil") + } + + path := fmt.Sprintf("%s/%s", tagsBasePath, name) + req, err := s.client.NewRequest("PUT", path, updateRequest) + if err != nil { + return nil, err + } + + resp, err := s.client.Do(req, nil) + + return resp, err +} + +func (s *TagsServiceOp) Delete(name string) (*Response, error) { + if name == "" { + return nil, NewArgError("name", "cannot be empty") + } + + path := fmt.Sprintf("%s/%s", tagsBasePath, name) + req, err := s.client.NewRequest("DELETE", path, nil) + if err != nil { + return nil, err + } + + resp, err := s.client.Do(req, nil) + + return resp, err +} + +func (s *TagsServiceOp) TagResources(name string, tagRequest *TagResourcesRequest) (*Response, error) { + if name == "" { + return nil, NewArgError("name", "cannot be empty") + } + + if tagRequest == nil { + return nil, NewArgError("tagRequest", "cannot be nil") + } + + path := fmt.Sprintf("%s/%s/resources", tagsBasePath, name) + req, err := s.client.NewRequest("POST", path, tagRequest) + if err != nil { + return nil, err + } + + resp, err := s.client.Do(req, nil) + + return resp, err +} + +func (s *TagsServiceOp) UntagResources(name string, untagRequest *UntagResourcesRequest) (*Response, error) { + if name == "" { + return nil, NewArgError("name", "cannot be empty") + } + + if untagRequest == nil { + return nil, NewArgError("tagRequest", "cannot be nil") + } + + path := fmt.Sprintf("%s/%s/resources", tagsBasePath, name) + req, err := s.client.NewRequest("DELETE", path, untagRequest) + if err != nil { + return nil, err + } + + resp, err := s.client.Do(req, nil) + + return resp, err +} diff --git a/tags_test.go b/tags_test.go new file mode 100644 index 00000000..cc1ad758 --- /dev/null +++ b/tags_test.go @@ -0,0 +1,384 @@ +package godo + +import ( + "encoding/json" + "fmt" + "net/http" + "reflect" + "testing" +) + +var ( + listEmptyJson string =` + { + "tags": [ + ], + "meta": { + "total": 0 + } + } + ` + + listJson string =` + { + "tags": [ + { + "name": "testing-1", + "resources": { + "droplets": { + "count": 0, + "last_tagged": null + } + } + }, + { + "name": "testing-2", + "resources": { + "droplets": { + "count": 0, + "last_tagged": null + } + } + } + ], + "links": { + "pages":{ + "next":"http://example.com/v2/tags/?page=3", + "prev":"http://example.com/v2/tags/?page=1", + "last":"http://example.com/v2/tags/?page=3", + "first":"http://example.com/v2/tags/?page=1" + } + }, + "meta": { + "total": 2 + } + } + ` + + createJson string =` + { + "tag": { + "name": "testing-1", + "resources": { + "droplets": { + "count": 0, + "last_tagged": null + } + } + } + } + ` + + getJson string =` + { + "tag": { + "name": "testing-1", + "resources": { + "droplets": { + "count": 1, + "last_tagged": { + "id": 1, + "name": "test.example.com", + "memory": 1024, + "vcpus": 2, + "disk": 20, + "region": { + "slug": "nyc1", + "name": "New York", + "sizes": [ + "1024mb", + "512mb" + ], + "available": true, + "features": [ + "virtio", + "private_networking", + "backups", + "ipv6" + ] + }, + "image": { + "id": 119192817, + "name": "Ubuntu 13.04", + "distribution": "ubuntu", + "slug": "ubuntu1304", + "public": true, + "regions": [ + "nyc1" + ], + "created_at": "2014-07-29T14:35:37Z" + }, + "size_slug": "1024mb", + "locked": false, + "status": "active", + "networks": { + "v4": [ + { + "ip_address": "10.0.0.19", + "netmask": "255.255.0.0", + "gateway": "10.0.0.1", + "type": "private" + }, + { + "ip_address": "127.0.0.19", + "netmask": "255.255.255.0", + "gateway": "127.0.0.20", + "type": "public" + } + ], + "v6": [ + { + "ip_address": "2001::13", + "cidr": 124, + "gateway": "2400:6180:0000:00D0:0000:0000:0009:7000", + "type": "public" + } + ] + }, + "kernel": { + "id": 485432985, + "name": "DO-recovery-static-fsck", + "version": "3.8.0-25-generic" + }, + "created_at": "2014-07-29T14:35:37Z", + "features": [ + "ipv6" + ], + "backup_ids": [ + 449676382 + ], + "snapshot_ids": [ + 449676383 + ], + "action_ids": [ + ], + "tags": [ + "tag-1", + "tag-2" + ] + } + } + } + } + } + ` +) + +func TestTags_List(t *testing.T) { + setup() + defer teardown() + + mux.HandleFunc("/v2/tags", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "GET") + fmt.Fprint(w, listJson) + }) + + tags, _, err := client.Tags.List(nil) + if err != nil { + t.Error("Tags.List returned error: %v", err) + } + + expected := []Tag{{Name: "testing-1", Resources: &TaggedResources{Droplets: &TaggedDropletsResources{Count: 0, LastTagged: nil}}}, + {Name: "testing-2", Resources: &TaggedResources{Droplets: &TaggedDropletsResources{Count: 0, LastTagged: nil}}}} + if !reflect.DeepEqual(tags, expected) { + t.Errorf("Tags.List returned %+v, expected %+v", tags, expected) + } +} + +func TestTags_ListEmpty(t *testing.T) { + setup() + defer teardown() + + mux.HandleFunc("/v2/tags", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "GET") + fmt.Fprint(w, listEmptyJson) + }) + + tags, _, err := client.Tags.List(nil) + if err != nil { + t.Error("Tags.List returned error: %v", err) + } + + expected := []Tag{} + if !reflect.DeepEqual(tags, expected) { + t.Errorf("Tags.List returned %+v, expected %+v", tags, expected) + } +} + +func TestTags_ListPaging(t *testing.T) { + setup() + defer teardown() + + mux.HandleFunc("/v2/tags", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "GET") + fmt.Fprint(w, listJson) + }) + + _, resp, err := client.Tags.List(nil) + if err != nil { + t.Error("Tags.List returned error: %v", err) + } + checkCurrentPage(t, resp, 2) +} + +func TestTags_Get(t *testing.T) { + setup() + defer teardown() + + mux.HandleFunc("/v2/tags/testing-1", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "GET") + fmt.Fprint(w, getJson) + }) + + tag, _, err := client.Tags.Get("testing-1") + if err != nil { + t.Error("Tags.Get returned error: %v", err) + } + + if tag.Name != "testing-1" { + t.Error("Tags.Get return an incorrect name, got %+v, expected %+v", tag.Name, "testing-1") + } + + if tag.Resources.Droplets.Count != 1 { + t.Error("Tags.Get return an incorrect droplet resource count, got %+v, expected %+v", tag.Resources.Droplets.Count, 1) + } + + if tag.Resources.Droplets.LastTagged.ID != 1 { + t.Error("Tags.Get return an incorrect last tagged droplet %+v, expected %+v", tag.Resources.Droplets.LastTagged.ID, 1) + } +} + +func TestTags_Create(t *testing.T) { + setup() + defer teardown() + + createRequest := &TagCreateRequest{ + Name: "testing-1", + } + + mux.HandleFunc("/v2/tags", func(w http.ResponseWriter, r *http.Request) { + v := new(TagCreateRequest) + err := json.NewDecoder(r.Body).Decode(v) + if err != nil { + t.Fatalf("decode json: %v", err) + } + + testMethod(t, r, "POST") + if !reflect.DeepEqual(v, createRequest) { + t.Errorf("Request body = %+v, expected %+v", v, createRequest) + } + + fmt.Fprintf(w, createJson) + }) + + tag, _, err := client.Tags.Create(createRequest) + if err != nil { + t.Errorf("Tags.Create returned error: %v", err) + } + + expected := &Tag{Name: "testing-1", Resources: &TaggedResources{Droplets: &TaggedDropletsResources{Count: 0, LastTagged: nil}}} + if !reflect.DeepEqual(tag, expected) { + t.Errorf("Tags.Create returned %+v, expected %+v", tag, expected) + } +} + +func TestTags_Update(t *testing.T) { + setup() + defer teardown() + + updateRequest := &TagUpdateRequest{ + Name: "testing-1", + } + + mux.HandleFunc("/v2/tags/old-testing-1", func(w http.ResponseWriter, r *http.Request) { + v := new(TagUpdateRequest) + + err := json.NewDecoder(r.Body).Decode(v) + if err != nil { + t.Fatalf("decode json: %v", err) + } + + testMethod(t, r, "PUT") + if !reflect.DeepEqual(v, updateRequest) { + t.Errorf("Request body = %+v, expected %+v", v, updateRequest) + } + + }) + + _, err := client.Tags.Update("old-testing-1", updateRequest) + if err != nil { + t.Errorf("Tags.Update returned error: %v", err) + } +} + +func TestTags_Delete(t *testing.T) { + setup() + defer teardown() + + mux.HandleFunc("/v2/tags/testing-1", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "DELETE") + }) + + _, err := client.Tags.Delete("testing-1") + if err != nil { + t.Errorf("Tags.Delete returned error: %v", err) + } +} + +func TestTags_TagResource(t *testing.T) { + setup() + defer teardown() + + tagResourcesRequest := &TagResourcesRequest{ + Resources: []Resource{{ID: "1", Type: "droplet"}}, + } + + mux.HandleFunc("/v2/tags/testing-1/resources", func(w http.ResponseWriter, r *http.Request) { + v := new(TagResourcesRequest) + + err := json.NewDecoder(r.Body).Decode(v) + if err != nil { + t.Fatalf("decode json: %v", err) + } + + testMethod(t, r, "POST") + if !reflect.DeepEqual(v, tagResourcesRequest) { + t.Errorf("Request body = %+v, expected %+v", v, tagResourcesRequest) + } + + }) + + _, err := client.Tags.TagResources("testing-1", tagResourcesRequest) + if err != nil { + t.Errorf("Tags.TagResources returned error: %v", err) + } +} + +func TestTags_UntagResource(t *testing.T) { + setup() + defer teardown() + + untagResourcesRequest := &UntagResourcesRequest{ + Resources: []Resource{{ID: "1", Type: "droplet"}}, + } + + mux.HandleFunc("/v2/tags/testing-1/resources", func(w http.ResponseWriter, r *http.Request) { + v := new(UntagResourcesRequest) + + err := json.NewDecoder(r.Body).Decode(v) + if err != nil { + t.Fatalf("decode json: %v", err) + } + + testMethod(t, r, "DELETE") + if !reflect.DeepEqual(v, untagResourcesRequest) { + t.Errorf("Request body = %+v, expected %+v", v, untagResourcesRequest) + } + + }) + + _, err := client.Tags.UntagResources("testing-1", untagResourcesRequest) + if err != nil { + t.Errorf("Tags.UntagResources returned error: %v", err) + } +} From 1eaf6161c0bafbf9f564ef8030acf12a267af0e9 Mon Sep 17 00:00:00 2001 From: Nan Zhong Date: Thu, 17 Mar 2016 01:45:16 -0400 Subject: [PATCH 2/5] Update droplet and actions endpoints with tagging support --- droplet_actions.go | 96 +++++++++++ droplet_actions_test.go | 348 ++++++++++++++++++++++++++++++++++++++++ droplets.go | 59 +++++-- droplets_test.go | 42 +++++ 4 files changed, 532 insertions(+), 13 deletions(-) diff --git a/droplet_actions.go b/droplet_actions.go index 7012aee7..c01ba36a 100644 --- a/droplet_actions.go +++ b/droplet_actions.go @@ -13,22 +13,31 @@ type ActionRequest map[string]interface{} // See: https://developers.digitalocean.com/documentation/v2#droplet-actions type DropletActionsService interface { Shutdown(int) (*Action, *Response, error) + ShutdownByTag(string) (*Action, *Response, error) PowerOff(int) (*Action, *Response, error) + PowerOffByTag(string) (*Action, *Response, error) PowerOn(int) (*Action, *Response, error) + PowerOnByTag(string) (*Action, *Response, error) PowerCycle(int) (*Action, *Response, error) + PowerCycleByTag(string) (*Action, *Response, error) Reboot(int) (*Action, *Response, error) Restore(int, int) (*Action, *Response, error) Resize(int, string, bool) (*Action, *Response, error) Rename(int, string) (*Action, *Response, error) Snapshot(int, string) (*Action, *Response, error) + SnapshotByTag(string, string) (*Action, *Response, error) EnableBackups(int) (*Action, *Response, error) + EnableBackupsByTag(string) (*Action, *Response, error) DisableBackups(int) (*Action, *Response, error) + DisableBackupsByTag(string) (*Action, *Response, error) PasswordReset(int) (*Action, *Response, error) RebuildByImageID(int, int) (*Action, *Response, error) RebuildByImageSlug(int, string) (*Action, *Response, error) ChangeKernel(int, int) (*Action, *Response, error) EnableIPv6(int) (*Action, *Response, error) + EnableIPv6ByTag(string) (*Action, *Response, error) EnablePrivateNetworking(int) (*Action, *Response, error) + EnablePrivateNetworkingByTag(string) (*Action, *Response, error) Upgrade(int) (*Action, *Response, error) Get(int, int) (*Action, *Response, error) GetByURI(string) (*Action, *Response, error) @@ -48,24 +57,48 @@ func (s *DropletActionsServiceOp) Shutdown(id int) (*Action, *Response, error) { return s.doAction(id, request) } +// Shutdown Droplets by Tag +func (s *DropletActionsServiceOp) ShutdownByTag(tag string) (*Action, *Response, error) { + request := &ActionRequest{"type": "shutdown"} + return s.doActionByTag(tag, request) +} + // PowerOff a Droplet func (s *DropletActionsServiceOp) PowerOff(id int) (*Action, *Response, error) { request := &ActionRequest{"type": "power_off"} return s.doAction(id, request) } +// PowerOff a Droplet by Tag +func (s *DropletActionsServiceOp) PowerOffByTag(tag string) (*Action, *Response, error) { + request := &ActionRequest{"type": "power_off"} + return s.doActionByTag(tag, request) +} + // PowerOn a Droplet func (s *DropletActionsServiceOp) PowerOn(id int) (*Action, *Response, error) { request := &ActionRequest{"type": "power_on"} return s.doAction(id, request) } +// PowerOn a Droplet by Tag +func (s *DropletActionsServiceOp) PowerOnByTag(tag string) (*Action, *Response, error) { + request := &ActionRequest{"type": "power_on"} + return s.doActionByTag(tag, request) +} + // PowerCycle a Droplet func (s *DropletActionsServiceOp) PowerCycle(id int) (*Action, *Response, error) { request := &ActionRequest{"type": "power_cycle"} return s.doAction(id, request) } +// PowerCycle a Droplet by Tag +func (s *DropletActionsServiceOp) PowerCycleByTag(tag string) (*Action, *Response, error) { + request := &ActionRequest{"type": "power_cycle"} + return s.doActionByTag(tag, request) +} + // Reboot a Droplet func (s *DropletActionsServiceOp) Reboot(id int) (*Action, *Response, error) { request := &ActionRequest{"type": "reboot"} @@ -113,18 +146,40 @@ func (s *DropletActionsServiceOp) Snapshot(id int, name string) (*Action, *Respo return s.doAction(id, request) } +// Snapshot a Droplet by Tag +func (s *DropletActionsServiceOp) SnapshotByTag(tag string, name string) (*Action, *Response, error) { + requestType := "snapshot" + request := &ActionRequest{ + "type": requestType, + "name": name, + } + return s.doActionByTag(tag, request) +} + // EnableBackups enables backups for a droplet. func (s *DropletActionsServiceOp) EnableBackups(id int) (*Action, *Response, error) { request := &ActionRequest{"type": "enable_backups"} return s.doAction(id, request) } +// EnableBackups enables backups for a droplet by Tag +func (s *DropletActionsServiceOp) EnableBackupsByTag(tag string) (*Action, *Response, error) { + request := &ActionRequest{"type": "enable_backups"} + return s.doActionByTag(tag, request) +} + // DisableBackups disables backups for a droplet. func (s *DropletActionsServiceOp) DisableBackups(id int) (*Action, *Response, error) { request := &ActionRequest{"type": "disable_backups"} return s.doAction(id, request) } +// DisableBackups disables backups for a droplet by tag +func (s *DropletActionsServiceOp) DisableBackupsByTag(tag string) (*Action, *Response, error) { + request := &ActionRequest{"type": "disable_backups"} + return s.doActionByTag(tag, request) +} + // PasswordReset resets the password for a droplet. func (s *DropletActionsServiceOp) PasswordReset(id int) (*Action, *Response, error) { request := &ActionRequest{"type": "password_reset"} @@ -155,12 +210,24 @@ func (s *DropletActionsServiceOp) EnableIPv6(id int) (*Action, *Response, error) return s.doAction(id, request) } +// EnableIPv6 enables IPv6 for a droplet by Tag +func (s *DropletActionsServiceOp) EnableIPv6ByTag(tag string) (*Action, *Response, error) { + request := &ActionRequest{"type": "enable_ipv6"} + return s.doActionByTag(tag, request) +} + // EnablePrivateNetworking enables private networking for a droplet. func (s *DropletActionsServiceOp) EnablePrivateNetworking(id int) (*Action, *Response, error) { request := &ActionRequest{"type": "enable_private_networking"} return s.doAction(id, request) } +// EnablePrivateNetworking enables private networking for a droplet by Tag +func (s *DropletActionsServiceOp) EnablePrivateNetworkingByTag(tag string) (*Action, *Response, error) { + request := &ActionRequest{"type": "enable_private_networking"} + return s.doActionByTag(tag, request) +} + // Upgrade a droplet. func (s *DropletActionsServiceOp) Upgrade(id int) (*Action, *Response, error) { request := &ActionRequest{"type": "upgrade"} @@ -192,6 +259,31 @@ func (s *DropletActionsServiceOp) doAction(id int, request *ActionRequest) (*Act return &root.Event, resp, err } +func (s *DropletActionsServiceOp) doActionByTag(tag string, request *ActionRequest) (*Action, *Response, error) { + if tag == "" { + return nil, nil, NewArgError("tag", "cannot be empty") + } + + if request == nil { + return nil, nil, NewArgError("request", "request can't be nil") + } + + path := dropletActionPathByTag(tag) + + 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 +} + // Get an action for a particular droplet by id. func (s *DropletActionsServiceOp) Get(dropletID, actionID int) (*Action, *Response, error) { if dropletID < 1 { @@ -236,3 +328,7 @@ func (s *DropletActionsServiceOp) get(path string) (*Action, *Response, error) { func dropletActionPath(dropletID int) string { return fmt.Sprintf("v2/droplets/%d/actions", dropletID) } + +func dropletActionPathByTag(tag string) string { + return fmt.Sprintf("v2/droplets/actions?tag_name=%s", tag) +} diff --git a/droplet_actions_test.go b/droplet_actions_test.go index fa063dba..e5a00890 100644 --- a/droplet_actions_test.go +++ b/droplet_actions_test.go @@ -42,6 +42,44 @@ func TestDropletActions_Shutdown(t *testing.T) { } } +func TestDropletActions_ShutdownByTag(t *testing.T) { + setup() + defer teardown() + + request := &ActionRequest{ + "type": "shutdown", + } + + mux.HandleFunc("/v2/droplets/actions", func(w http.ResponseWriter, r *http.Request) { + if r.URL.Query().Get("tag_name") != "testing-1" { + t.Errorf("DropletActions.ShutdownByTag did not request with a tag parameter") + } + + 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, request) { + t.Errorf("Request body = %+v, expected %+v", v, request) + } + + fmt.Fprintf(w, `{"action":{"status":"in-progress"}}`) + }) + + action, _, err := client.DropletActions.ShutdownByTag("testing-1") + if err != nil { + t.Errorf("DropletActions.ShutdownByTag returned error: %v", err) + } + + expected := &Action{Status: "in-progress"} + if !reflect.DeepEqual(action, expected) { + t.Errorf("DropletActions.ShutdownByTag returned %+v, expected %+v", action, expected) + } +} + func TestDropletAction_PowerOff(t *testing.T) { setup() defer teardown() @@ -76,6 +114,44 @@ func TestDropletAction_PowerOff(t *testing.T) { } } +func TestDropletAction_PowerOffByTag(t *testing.T) { + setup() + defer teardown() + + request := &ActionRequest{ + "type": "power_off", + } + + mux.HandleFunc("/v2/droplets/actions", func(w http.ResponseWriter, r *http.Request) { + if r.URL.Query().Get("tag_name") != "testing-1" { + t.Errorf("DropletActions.PowerOffByTag did not request with a tag parameter") + } + + 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, request) { + t.Errorf("Request body = %+v, expected %+v", v, request) + } + + fmt.Fprintf(w, `{"action":{"status":"in-progress"}}`) + }) + + action, _, err := client.DropletActions.PowerOffByTag("testing-1") + if err != nil { + t.Errorf("DropletActions.PowerOffByTag returned error: %v", err) + } + + expected := &Action{Status: "in-progress"} + if !reflect.DeepEqual(action, expected) { + t.Errorf("DropletActions.PoweroffByTag returned %+v, expected %+v", action, expected) + } +} + func TestDropletAction_PowerOn(t *testing.T) { setup() defer teardown() @@ -110,6 +186,43 @@ func TestDropletAction_PowerOn(t *testing.T) { } } +func TestDropletAction_PowerOnByTag(t *testing.T) { + setup() + defer teardown() + + request := &ActionRequest{ + "type": "power_on", + } + + mux.HandleFunc("/v2/droplets/actions", func(w http.ResponseWriter, r *http.Request) { + if r.URL.Query().Get("tag_name") != "testing-1" { + t.Errorf("DropletActions.PowerOnByTag did not request with a tag parameter") + } + + 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, request) { + t.Errorf("Request body = %+v, expected %+v", v, request) + } + + fmt.Fprintf(w, `{"action":{"status":"in-progress"}}`) + }) + + action, _, err := client.DropletActions.PowerOnByTag("testing-1") + if err != nil { + t.Errorf("DropletActions.PowerOnByTag returned error: %v", err) + } + + expected := &Action{Status: "in-progress"} + if !reflect.DeepEqual(action, expected) { + t.Errorf("DropletActions.PowerOnByTag returned %+v, expected %+v", action, expected) + } +} func TestDropletAction_Reboot(t *testing.T) { setup() defer teardown() @@ -291,6 +404,45 @@ func TestDropletAction_PowerCycle(t *testing.T) { } } +func TestDropletAction_PowerCycleByTag(t *testing.T) { + setup() + defer teardown() + + request := &ActionRequest{ + "type": "power_cycle", + } + + mux.HandleFunc("/v2/droplets/actions", func(w http.ResponseWriter, r *http.Request) { + if r.URL.Query().Get("tag_name") != "testing-1" { + t.Errorf("DropletActions.PowerCycleByTag did not request with a tag parameter") + } + + 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, request) { + t.Errorf("Request body = %+v, expected %+v", v, request) + } + + fmt.Fprintf(w, `{"action":{"status":"in-progress"}}`) + + }) + + action, _, err := client.DropletActions.PowerCycleByTag("testing-1") + if err != nil { + t.Errorf("DropletActions.PowerCycleByTag returned error: %v", err) + } + + expected := &Action{Status: "in-progress"} + if !reflect.DeepEqual(action, expected) { + t.Errorf("DropletActions.PowerCycleByTag returned %+v, expected %+v", action, expected) + } +} + func TestDropletAction_Snapshot(t *testing.T) { setup() defer teardown() @@ -327,6 +479,46 @@ func TestDropletAction_Snapshot(t *testing.T) { } } +func TestDropletAction_SnapshotByTag(t *testing.T) { + setup() + defer teardown() + + request := &ActionRequest{ + "type": "snapshot", + "name": "Image-Name", + } + + mux.HandleFunc("/v2/droplets/actions", func(w http.ResponseWriter, r *http.Request) { + if r.URL.Query().Get("tag_name") != "testing-1" { + t.Errorf("DropletActions.SnapshotByTag did not request with a tag parameter") + } + + 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, request) { + t.Errorf("Request body = %+v, expected %+v", v, request) + } + + fmt.Fprintf(w, `{"action":{"status":"in-progress"}}`) + }) + + action, _, err := client.DropletActions.SnapshotByTag("testing-1", "Image-Name") + if err != nil { + t.Errorf("DropletActions.SnapshotByTag returned error: %v", err) + } + + expected := &Action{Status: "in-progress"} + if !reflect.DeepEqual(action, expected) { + t.Errorf("DropletActions.SnapshotByTag returned %+v, expected %+v", action, expected) + } +} + func TestDropletAction_EnableBackups(t *testing.T) { setup() defer teardown() @@ -362,6 +554,45 @@ func TestDropletAction_EnableBackups(t *testing.T) { } } +func TestDropletAction_EnableBackupsByTag(t *testing.T) { + setup() + defer teardown() + + request := &ActionRequest{ + "type": "enable_backups", + } + + mux.HandleFunc("/v2/droplets/actions", func(w http.ResponseWriter, r *http.Request) { + if r.URL.Query().Get("tag_name") != "testing-1" { + t.Errorf("DropletActions.EnableBackupByTag did not request with a tag parameter") + } + + 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, request) { + t.Errorf("Request body = %+v, expected %+v", v, request) + } + + fmt.Fprintf(w, `{"action":{"status":"in-progress"}}`) + }) + + action, _, err := client.DropletActions.EnableBackupsByTag("testing-1") + if err != nil { + t.Errorf("DropletActions.EnableBackupsByTag returned error: %v", err) + } + + expected := &Action{Status: "in-progress"} + if !reflect.DeepEqual(action, expected) { + t.Errorf("DropletActions.EnableBackupsByTag returned %+v, expected %+v", action, expected) + } +} + func TestDropletAction_DisableBackups(t *testing.T) { setup() defer teardown() @@ -397,6 +628,45 @@ func TestDropletAction_DisableBackups(t *testing.T) { } } +func TestDropletAction_DisableBackupsByTag(t *testing.T) { + setup() + defer teardown() + + request := &ActionRequest{ + "type": "disable_backups", + } + + mux.HandleFunc("/v2/droplets/actions", func(w http.ResponseWriter, r *http.Request) { + if r.URL.Query().Get("tag_name") != "testing-1" { + t.Errorf("DropletActions.DisableBackupsByTag did not request with a tag parameter") + } + + 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, request) { + t.Errorf("Request body = %+v, expected %+v", v, request) + } + + fmt.Fprintf(w, `{"action":{"status":"in-progress"}}`) + }) + + action, _, err := client.DropletActions.DisableBackupsByTag("testing-1") + if err != nil { + t.Errorf("DropletActions.DisableBackupsByTag returned error: %v", err) + } + + expected := &Action{Status: "in-progress"} + if !reflect.DeepEqual(action, expected) { + t.Errorf("DropletActions.DisableBackupsByTag returned %+v, expected %+v", action, expected) + } +} + func TestDropletAction_PasswordReset(t *testing.T) { setup() defer teardown() @@ -575,6 +845,45 @@ func TestDropletAction_EnableIPv6(t *testing.T) { } } +func TestDropletAction_EnableIPv6ByTag(t *testing.T) { + setup() + defer teardown() + + request := &ActionRequest{ + "type": "enable_ipv6", + } + + mux.HandleFunc("/v2/droplets/actions", func(w http.ResponseWriter, r *http.Request) { + if r.URL.Query().Get("tag_name") != "testing-1" { + t.Errorf("DropletActions.EnableIPv6ByTag did not request with a tag parameter") + } + + 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, request) { + t.Errorf("Request body = %+v, expected %+v", v, request) + } + + fmt.Fprintf(w, `{"action":{"status":"in-progress"}}`) + }) + + action, _, err := client.DropletActions.EnableIPv6ByTag("testing-1") + if err != nil { + t.Errorf("DropletActions.EnableIPv6ByTag returned error: %v", err) + } + + expected := &Action{Status: "in-progress"} + if !reflect.DeepEqual(action, expected) { + t.Errorf("DropletActions.EnableIPv6byTag returned %+v, expected %+v", action, expected) + } +} + func TestDropletAction_EnablePrivateNetworking(t *testing.T) { setup() defer teardown() @@ -610,6 +919,45 @@ func TestDropletAction_EnablePrivateNetworking(t *testing.T) { } } +func TestDropletAction_EnablePrivateNetworkingByTag(t *testing.T) { + setup() + defer teardown() + + request := &ActionRequest{ + "type": "enable_private_networking", + } + + mux.HandleFunc("/v2/droplets/actions", func(w http.ResponseWriter, r *http.Request) { + if r.URL.Query().Get("tag_name") != "testing-1" { + t.Errorf("DropletActions.EnablePrivateNetworkingByTag did not request with a tag parameter") + } + + 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, request) { + t.Errorf("Request body = %+v, expected %+v", v, request) + } + + fmt.Fprintf(w, `{"action":{"status":"in-progress"}}`) + }) + + action, _, err := client.DropletActions.EnablePrivateNetworkingByTag("testing-1") + if err != nil { + t.Errorf("DropletActions.EnablePrivateNetworkingByTag returned error: %v", err) + } + + expected := &Action{Status: "in-progress"} + if !reflect.DeepEqual(action, expected) { + t.Errorf("DropletActions.EnablePrivateNetworkingByTag returned %+v, expected %+v", action, expected) + } +} + func TestDropletAction_Upgrade(t *testing.T) { setup() defer teardown() diff --git a/droplets.go b/droplets.go index 08703772..03521e14 100644 --- a/droplets.go +++ b/droplets.go @@ -15,10 +15,12 @@ var errNoNetworks = errors.New("no networks have been defined") // See: https://developers.digitalocean.com/documentation/v2#droplets type DropletsService interface { List(*ListOptions) ([]Droplet, *Response, error) + ListByTag(string, *ListOptions) ([]Droplet, *Response, error) Get(int) (*Droplet, *Response, error) Create(*DropletCreateRequest) (*Droplet, *Response, error) CreateMultiple(*DropletMultiCreateRequest) ([]Droplet, *Response, error) Delete(int) (*Response, error) + DeleteByTag(string) (*Response, error) Kernels(int, *ListOptions) ([]Kernel, *Response, error) Snapshots(int, *ListOptions) ([]Image, *Response, error) Backups(int, *ListOptions) ([]Image, *Response, error) @@ -233,14 +235,8 @@ func (n NetworkV6) String() string { return Stringify(n) } -// List all droplets -func (s *DropletsServiceOp) List(opt *ListOptions) ([]Droplet, *Response, error) { - path := dropletBasePath - path, err := addOptions(path, opt) - if err != nil { - return nil, nil, err - } - +// Performs a list request given a path +func (s *DropletsServiceOp) list(path string) ([]Droplet, *Response, error) { req, err := s.client.NewRequest("GET", path, nil) if err != nil { return nil, nil, err @@ -258,6 +254,27 @@ func (s *DropletsServiceOp) List(opt *ListOptions) ([]Droplet, *Response, error) return root.Droplets, resp, err } +// List all droplets +func (s *DropletsServiceOp) List(opt *ListOptions) ([]Droplet, *Response, error) { + path := dropletBasePath + path, err := addOptions(path, opt) + if err != nil { + return nil, nil, err + } + + return s.list(path) +} + +// List all droplets by tag +func (s *DropletsServiceOp) ListByTag(tag string, opt *ListOptions) ([]Droplet, *Response, error) { + path := fmt.Sprintf("%s?tag=%s", dropletBasePath, tag) + path, err := addOptions(path, opt) + if err != nil { + return nil, nil, err + } + + return s.list(path) +} // Get individual droplet func (s *DropletsServiceOp) Get(dropletID int) (*Droplet, *Response, error) { if dropletID < 1 { @@ -330,6 +347,18 @@ func (s *DropletsServiceOp) CreateMultiple(createRequest *DropletMultiCreateRequ return root.Droplets, resp, err } +// Performs a delete request given a path +func (s *DropletsServiceOp) delete(path string) (*Response, error) { + req, err := s.client.NewRequest("DELETE", path, nil) + if err != nil { + return nil, err + } + + resp, err := s.client.Do(req, nil) + + return resp, err +} + // Delete droplet func (s *DropletsServiceOp) Delete(dropletID int) (*Response, error) { if dropletID < 1 { @@ -338,14 +367,18 @@ func (s *DropletsServiceOp) Delete(dropletID int) (*Response, error) { path := fmt.Sprintf("%s/%d", dropletBasePath, dropletID) - req, err := s.client.NewRequest("DELETE", path, nil) - if err != nil { - return nil, err + return s.delete(path) +} + +// Delete droplets by tag +func (s *DropletsServiceOp) DeleteByTag(tag string) (*Response, error) { + if tag == "" { + return nil, NewArgError("tag", "cannot be empty") } - resp, err := s.client.Do(req, nil) + path := fmt.Sprintf("%s?tag=%s", dropletBasePath, tag) - return resp, err + return s.delete(path) } // Kernels lists kernels available for a droplet. diff --git a/droplets_test.go b/droplets_test.go index eb75ac9a..8ab49f2e 100644 --- a/droplets_test.go +++ b/droplets_test.go @@ -28,6 +28,30 @@ func TestDroplets_ListDroplets(t *testing.T) { } } +func TestDroplets_ListDropletsByTag(t *testing.T) { + setup() + defer teardown() + + mux.HandleFunc("/v2/droplets", func(w http.ResponseWriter, r *http.Request) { + if r.URL.Query().Get("tag") != "testing-1" { + t.Errorf("Droplets.ListByTag did not request with a tag parameter") + } + + testMethod(t, r, "GET") + fmt.Fprint(w, `{"droplets": [{"id":1},{"id":2}]}`) + }) + + droplets, _, err := client.Droplets.ListByTag("testing-1", nil) + if err != nil { + t.Errorf("Droplets.ListByTag returned error: %v", err) + } + + expected := []Droplet{{ID: 1}, {ID: 2}} + if !reflect.DeepEqual(droplets, expected) { + t.Errorf("Droplets.ListByTag returned %+v, expected %+v", droplets, expected) + } +} + func TestDroplets_ListDropletsMultiplePages(t *testing.T) { setup() defer teardown() @@ -234,6 +258,24 @@ func TestDroplets_Destroy(t *testing.T) { } } +func TestDroplets_DestroyByTag(t *testing.T) { + setup() + defer teardown() + + mux.HandleFunc("/v2/droplets", func(w http.ResponseWriter, r *http.Request) { + if r.URL.Query().Get("tag") != "testing-1" { + t.Errorf("Droplets.DeleteByTag did not request with a tag parameter") + } + + testMethod(t, r, "DELETE") + }) + + _, err := client.Droplets.DeleteByTag("testing-1") + if err != nil { + t.Errorf("Droplet.Delete returned error: %v", err) + } +} + func TestDroplets_Kernels(t *testing.T) { setup() defer teardown() From 25d8c5d1b6d126e08ce0215e9a87e8417d141e03 Mon Sep 17 00:00:00 2001 From: Nan Zhong Date: Thu, 17 Mar 2016 01:49:14 -0400 Subject: [PATCH 3/5] Add some comments to tags service --- tags.go | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/tags.go b/tags.go index 53d6ea14..707216a9 100644 --- a/tags.go +++ b/tags.go @@ -4,6 +4,9 @@ import "fmt" const tagsBasePath = "v2/tags" +// TagsService is an interface for interfacing with the tags +// endpoints of the DigitalOcean API +// See: https://developers.digitalocean.com/documentation/v2#tags type TagsService interface { List(*ListOptions) ([]Tag, *Response, error) Get(string) (*Tag, *Response, error) @@ -15,26 +18,32 @@ type TagsService interface { UntagResources(string, *UntagResourcesRequest) (*Response, error) } +// TagsServiceOp handles communication with tag related method of the +// DigitalOcean API. type TagsServiceOp struct { client *Client } var _ TagsService = &TagsServiceOp{} +// Resource represent a single resource for associating/dissociating with tags type Resource struct { ID string `json:"resource_id,omit_empty"` Type string `json:"resource_type,omit_empty"` } +// TaggedResources represent the set of resources a tag is attached to type TaggedResources struct { Droplets *TaggedDropletsResources `json:"droplets,omitempty"` } +// TaggedDropletsResources represent the droplet resources a tag is attached to type TaggedDropletsResources struct { Count int `json:"count,float64,omitempty"` LastTagged *Droplet `json:"last_tagged,omitempty"` } +// Tag represent DigitalOcean tag type Tag struct { Name string `json:"name,omitempty"` Resources *TaggedResources `json:"resources,omitempty"` @@ -91,6 +100,7 @@ func (s *TagsServiceOp) List(opt *ListOptions) ([]Tag, *Response, error) { return root.Tags, resp, err } +// Get a single tag func (s *TagsServiceOp) Get(name string) (*Tag, *Response, error) { path := fmt.Sprintf("%s/%s", tagsBasePath, name) @@ -108,6 +118,7 @@ func (s *TagsServiceOp) Get(name string) (*Tag, *Response, error) { return root.Tag, resp, err } +// Create a new tag func (s *TagsServiceOp) Create(createRequest *TagCreateRequest) (*Tag, *Response, error) { if createRequest == nil { return nil, nil, NewArgError("createRequest", "cannot be nil") @@ -127,6 +138,7 @@ func (s *TagsServiceOp) Create(createRequest *TagCreateRequest) (*Tag, *Response return root.Tag, resp, err } +// Update an exsting tag func (s *TagsServiceOp) Update(name string, updateRequest *TagUpdateRequest) (*Response, error) { if name == "" { return nil, NewArgError("name", "cannot be empty") @@ -147,6 +159,7 @@ func (s *TagsServiceOp) Update(name string, updateRequest *TagUpdateRequest) (*R return resp, err } +// Delete an existing tag func (s *TagsServiceOp) Delete(name string) (*Response, error) { if name == "" { return nil, NewArgError("name", "cannot be empty") @@ -163,6 +176,7 @@ func (s *TagsServiceOp) Delete(name string) (*Response, error) { return resp, err } +// Associate resources with a tag func (s *TagsServiceOp) TagResources(name string, tagRequest *TagResourcesRequest) (*Response, error) { if name == "" { return nil, NewArgError("name", "cannot be empty") @@ -183,6 +197,7 @@ func (s *TagsServiceOp) TagResources(name string, tagRequest *TagResourcesReques return resp, err } +// Dissociate resources with a tag func (s *TagsServiceOp) UntagResources(name string, untagRequest *UntagResourcesRequest) (*Response, error) { if name == "" { return nil, NewArgError("name", "cannot be empty") From a294b449f7c6630f9a2404809d800ebbda536763 Mon Sep 17 00:00:00 2001 From: Nan Zhong Date: Thu, 17 Mar 2016 02:18:20 -0400 Subject: [PATCH 4/5] Fix incorrect query param --- droplets.go | 5 +++-- droplets_test.go | 4 ++-- tags_test.go | 10 +++++----- 3 files changed, 10 insertions(+), 9 deletions(-) diff --git a/droplets.go b/droplets.go index 03521e14..57950d1b 100644 --- a/droplets.go +++ b/droplets.go @@ -267,7 +267,7 @@ func (s *DropletsServiceOp) List(opt *ListOptions) ([]Droplet, *Response, error) // List all droplets by tag func (s *DropletsServiceOp) ListByTag(tag string, opt *ListOptions) ([]Droplet, *Response, error) { - path := fmt.Sprintf("%s?tag=%s", dropletBasePath, tag) + path := fmt.Sprintf("%s?tag_name=%s", dropletBasePath, tag) path, err := addOptions(path, opt) if err != nil { return nil, nil, err @@ -275,6 +275,7 @@ func (s *DropletsServiceOp) ListByTag(tag string, opt *ListOptions) ([]Droplet, return s.list(path) } + // Get individual droplet func (s *DropletsServiceOp) Get(dropletID int) (*Droplet, *Response, error) { if dropletID < 1 { @@ -376,7 +377,7 @@ func (s *DropletsServiceOp) DeleteByTag(tag string) (*Response, error) { return nil, NewArgError("tag", "cannot be empty") } - path := fmt.Sprintf("%s?tag=%s", dropletBasePath, tag) + path := fmt.Sprintf("%s?tag_name=%s", dropletBasePath, tag) return s.delete(path) } diff --git a/droplets_test.go b/droplets_test.go index 8ab49f2e..c7187d62 100644 --- a/droplets_test.go +++ b/droplets_test.go @@ -33,7 +33,7 @@ func TestDroplets_ListDropletsByTag(t *testing.T) { defer teardown() mux.HandleFunc("/v2/droplets", func(w http.ResponseWriter, r *http.Request) { - if r.URL.Query().Get("tag") != "testing-1" { + if r.URL.Query().Get("tag_name") != "testing-1" { t.Errorf("Droplets.ListByTag did not request with a tag parameter") } @@ -263,7 +263,7 @@ func TestDroplets_DestroyByTag(t *testing.T) { defer teardown() mux.HandleFunc("/v2/droplets", func(w http.ResponseWriter, r *http.Request) { - if r.URL.Query().Get("tag") != "testing-1" { + if r.URL.Query().Get("tag_name") != "testing-1" { t.Errorf("Droplets.DeleteByTag did not request with a tag parameter") } diff --git a/tags_test.go b/tags_test.go index cc1ad758..f69fc6ae 100644 --- a/tags_test.go +++ b/tags_test.go @@ -9,7 +9,7 @@ import ( ) var ( - listEmptyJson string =` + listEmptyJson string = ` { "tags": [ ], @@ -19,7 +19,7 @@ var ( } ` - listJson string =` + listJson string = ` { "tags": [ { @@ -55,7 +55,7 @@ var ( } ` - createJson string =` + createJson string = ` { "tag": { "name": "testing-1", @@ -69,7 +69,7 @@ var ( } ` - getJson string =` + getJson string = ` { "tag": { "name": "testing-1", @@ -179,7 +179,7 @@ func TestTags_List(t *testing.T) { } expected := []Tag{{Name: "testing-1", Resources: &TaggedResources{Droplets: &TaggedDropletsResources{Count: 0, LastTagged: nil}}}, - {Name: "testing-2", Resources: &TaggedResources{Droplets: &TaggedDropletsResources{Count: 0, LastTagged: nil}}}} + {Name: "testing-2", Resources: &TaggedResources{Droplets: &TaggedDropletsResources{Count: 0, LastTagged: nil}}}} if !reflect.DeepEqual(tags, expected) { t.Errorf("Tags.List returned %+v, expected %+v", tags, expected) } From 960a9c905e827e23c3f5106b6ad2e8c053a275e8 Mon Sep 17 00:00:00 2001 From: Nan Zhong Date: Tue, 22 Mar 2016 10:25:22 -0400 Subject: [PATCH 5/5] Use a enum to represent resource type --- tags.go | 13 ++++++++++--- tags_test.go | 4 ++-- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/tags.go b/tags.go index 707216a9..dd6c638e 100644 --- a/tags.go +++ b/tags.go @@ -26,10 +26,17 @@ type TagsServiceOp struct { var _ TagsService = &TagsServiceOp{} -// Resource represent a single resource for associating/dissociating with tags +// ResourceType represents a class of resource, currently only droplet are supported +type ResourceType string + +const ( + DropletResourceType ResourceType = "droplet" +) + +// Resource represent a single resource for associating/disassociating with tags type Resource struct { - ID string `json:"resource_id,omit_empty"` - Type string `json:"resource_type,omit_empty"` + ID string `json:"resource_id,omit_empty"` + Type ResourceType `json:"resource_type,omit_empty"` } // TaggedResources represent the set of resources a tag is attached to diff --git a/tags_test.go b/tags_test.go index f69fc6ae..4529004f 100644 --- a/tags_test.go +++ b/tags_test.go @@ -330,7 +330,7 @@ func TestTags_TagResource(t *testing.T) { defer teardown() tagResourcesRequest := &TagResourcesRequest{ - Resources: []Resource{{ID: "1", Type: "droplet"}}, + Resources: []Resource{{ID: "1", Type: DropletResourceType}}, } mux.HandleFunc("/v2/tags/testing-1/resources", func(w http.ResponseWriter, r *http.Request) { @@ -359,7 +359,7 @@ func TestTags_UntagResource(t *testing.T) { defer teardown() untagResourcesRequest := &UntagResourcesRequest{ - Resources: []Resource{{ID: "1", Type: "droplet"}}, + Resources: []Resource{{ID: "1", Type: DropletResourceType}}, } mux.HandleFunc("/v2/tags/testing-1/resources", func(w http.ResponseWriter, r *http.Request) {