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..57950d1b 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,28 @@ 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_name=%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 +348,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 +368,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_name=%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..c7187d62 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_name") != "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_name") != "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() 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..dd6c638e --- /dev/null +++ b/tags.go @@ -0,0 +1,226 @@ +package godo + +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) + Create(*TagCreateRequest) (*Tag, *Response, error) + Update(string, *TagUpdateRequest) (*Response, error) + Delete(string) (*Response, error) + + TagResources(string, *TagResourcesRequest) (*Response, error) + UntagResources(string, *UntagResourcesRequest) (*Response, error) +} + +// TagsServiceOp handles communication with tag related method of the +// DigitalOcean API. +type TagsServiceOp struct { + client *Client +} + +var _ TagsService = &TagsServiceOp{} + +// 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 ResourceType `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"` +} + +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 +} + +// Get a single tag +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 +} + +// Create a new tag +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 +} + +// Update an exsting tag +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 +} + +// Delete an existing tag +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 +} + +// Associate resources with a tag +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 +} + +// Dissociate resources with a tag +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..4529004f --- /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: DropletResourceType}}, + } + + 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: DropletResourceType}}, + } + + 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) + } +}