Skip to content

Commit

Permalink
filter action list
Browse files Browse the repository at this point in the history
* filter action list
* fetch more than one page at a time
  • Loading branch information
bryanl committed Mar 16, 2016
1 parent 52e3096 commit 2e915fc
Show file tree
Hide file tree
Showing 5 changed files with 266 additions and 42 deletions.
16 changes: 14 additions & 2 deletions args.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,24 @@ package doit
const (
// ArgActionID is an action id argument.
ArgActionID = "action-id"
// ArgActionAfter is an action after argument.
ArgActionAfter = "after"
// ArgActionBefore is an action before argument.
ArgActionBefore = "before"
// ArgActionResourceType is an action resource type argument.
ArgActionResourceType = "resource-type"
// ArgActionRegion is an action region argument.
ArgActionRegion = "region"
// ArgActionStatus is an action status argument.
ArgActionStatus = "status"
// ArgActionType is an action type argument.
ArgActionType = "action-type"
// ArgCommandWait is a wait for a droplet to be created argument.
ArgCommandWait = "wait"
// ArgDomainName is a domain name argument.
ArgDomainName = "domain-name"
// ArgDropletID is a droplet id argument.
ArgDropletID = "droplet-id"
// ArgDropletWait is a wait for a droplet to be created argument.
ArgCommandWait = "wait"
// ArgKernelID is a ekrnel id argument.
ArgKernelID = "kernel-id"
// ArgImage is an image argument.
Expand Down
121 changes: 117 additions & 4 deletions commands/actions.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package commands

import (
"sort"
"strconv"
"time"

Expand All @@ -20,8 +21,14 @@ func Actions() *cobra.Command {
CmdBuilder(cmd, RunCmdActionGet, "get ACTIONID", "get action", Writer,
aliasOpt("g"), displayerType(&action{}))

CmdBuilder(cmd, RunCmdActionList, "list", "list actions", Writer,
cmdActionList := CmdBuilder(cmd, RunCmdActionList, "list", "list actions", Writer,
aliasOpt("ls"), displayerType(&action{}))
AddStringFlag(cmdActionList, doit.ArgActionResourceType, "", "Action resource type")
AddStringFlag(cmdActionList, doit.ArgActionRegion, "", "Action region")
AddStringFlag(cmdActionList, doit.ArgActionAfter, "", "Action completed after in RFC3339 format")
AddStringFlag(cmdActionList, doit.ArgActionBefore, "", "Action completed before in RFC3339 format")
AddStringFlag(cmdActionList, doit.ArgActionStatus, "", "Action status")
AddStringFlag(cmdActionList, doit.ArgActionType, "", "Action type")

cmdActionWait := CmdBuilder(cmd, RunCmdActionWait, "wait ACTIONID", "wait for action to complete", Writer,
aliasOpt("w"), displayerType(&action{}))
Expand All @@ -33,17 +40,123 @@ func Actions() *cobra.Command {

// RunCmdActionList run action list.
func RunCmdActionList(c *CmdConfig) error {
as := c.Actions()
actions, err := c.Actions().List()
if err != nil {
return err
}

newActions, err := as.List()
actions, err = filterActionList(c, actions)
if err != nil {
return err
}

item := &action{actions: newActions}
sort.Sort(actionsByCompletedAt(actions))

item := &action{actions: actions}
return c.Display(item)
}

type actionsByCompletedAt do.Actions

func (a actionsByCompletedAt) Len() int {
return len(a)
}
func (a actionsByCompletedAt) Swap(i, j int) {
a[i], a[j] = a[j], a[i]
}
func (a actionsByCompletedAt) Less(i, j int) bool {
return a[i].CompletedAt.Before(a[j].CompletedAt.Time)
}

func filterActionList(c *CmdConfig, in do.Actions) (do.Actions, error) {
resourceType, err := c.Doit.GetString(c.NS, doit.ArgActionResourceType)
if err != nil {
return nil, err
}

region, err := c.Doit.GetString(c.NS, doit.ArgActionRegion)
if err != nil {
return nil, err
}

status, err := c.Doit.GetString(c.NS, doit.ArgActionStatus)
if err != nil {
return nil, err
}

actionType, err := c.Doit.GetString(c.NS, doit.ArgActionType)
if err != nil {
return nil, err
}

var before, after time.Time
beforeStr, err := c.Doit.GetString(c.NS, doit.ArgActionBefore)
if err != nil {
return nil, err
}

if beforeStr != "" {
if before, err = time.Parse(time.RFC3339, beforeStr); err != nil {
return nil, err
}
}

afterStr, err := c.Doit.GetString(c.NS, doit.ArgActionAfter)
if err != nil {
return nil, err
}
if afterStr != "" {
if after, err = time.Parse(time.RFC3339, afterStr); err != nil {
return nil, err
}
}

out := do.Actions{}

for _, a := range in {
match := true

if resourceType != "" && a.ResourceType != resourceType {
match = false
}

if match && region != "" && a.RegionSlug != region {
match = false
}

if match && status != "" && a.Status != status {
match = false
}

if match && actionType != "" && a.Type != actionType {
match = false
}

if a.CompletedAt == nil {
match = false
}

if match && !isZeroTime(before) && a.CompletedAt != nil && a.CompletedAt.After(before) {
match = false
}

if match && !isZeroTime(after) && a.CompletedAt != nil && a.CompletedAt.Before(after) {
match = false
}

if match {
out = append(out, a)
}
}

return out, nil
}

func isZeroTime(t time.Time) bool {
z := time.Time{}
return z.Equal(t)
}

// RunCmdActionGet runs action get.
func RunCmdActionGet(c *CmdConfig) error {
if len(c.Args) != 1 {
Expand Down
48 changes: 48 additions & 0 deletions commands/actions_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ package commands

import (
"testing"
"time"

"github.com/bryanl/doit"
"github.com/bryanl/doit/do"
"github.com/digitalocean/godo"
"github.com/stretchr/testify/assert"
Expand Down Expand Up @@ -40,3 +42,49 @@ func TestActionGet(t *testing.T) {
assert.NoError(t, err)
})
}

func Test_filterActions(t *testing.T) {
cases := []struct {
resourceType string
region string
after string
before string
status string
actionType string
len int
desc string
}{
{len: 2, desc: "all actions"},
{len: 1, region: "fra1", desc: "by region"},
{len: 0, region: "dev0", desc: "invalid region"},
{len: 1, before: "2016-01-01T00:00:00-04:00", desc: "before date"},
{len: 1, after: "2016-01-01T00:00:00-04:00", desc: "after date"},
{len: 2, status: "completed", desc: "by status"},
}

actions := do.Actions{
{&godo.Action{
ResourceType: "foo", RegionSlug: "nyc1", Status: "completed", Type: "alpha",
CompletedAt: &godo.Timestamp{Time: time.Date(2015, time.April, 2, 12, 0, 0, 0, time.UTC)},
}},
{&godo.Action{
ResourceType: "bar", RegionSlug: "fra1", Status: "completed", Type: "beta",
CompletedAt: &godo.Timestamp{Time: time.Date(2016, time.April, 2, 12, 0, 0, 0, time.UTC)},
}},
}

for _, c := range cases {
withTestClient(t, func(config *CmdConfig, tm *tcMocks) {
config.Doit.Set(config.NS, doit.ArgActionResourceType, c.resourceType)
config.Doit.Set(config.NS, doit.ArgActionRegion, c.region)
config.Doit.Set(config.NS, doit.ArgActionAfter, c.after)
config.Doit.Set(config.NS, doit.ArgActionBefore, c.before)
config.Doit.Set(config.NS, doit.ArgActionStatus, c.status)
config.Doit.Set(config.NS, doit.ArgActionType, c.actionType)

newActions, err := filterActionList(config, actions)
assert.NoError(t, err)
assert.Len(t, newActions, c.len, c.desc)
})
}
}
6 changes: 5 additions & 1 deletion commands/output.go
Original file line number Diff line number Diff line change
Expand Up @@ -122,11 +122,15 @@ func (a *action) KV() []map[string]interface{} {
out := []map[string]interface{}{}

for _, x := range a.actions {
region := ""
if x.Region != nil {
region = x.Region.Slug
}
o := map[string]interface{}{
"ID": x.ID, "Status": x.Status, "Type": x.Type,
"StartedAt": x.StartedAt, "CompletedAt": x.CompletedAt,
"ResourceID": x.ResourceID, "ResourceType": x.ResourceType,
"Region": x.Region.Slug,
"Region": region,
}
out = append(out, o)
}
Expand Down
117 changes: 82 additions & 35 deletions do/pagination.go
Original file line number Diff line number Diff line change
@@ -1,58 +1,105 @@
package do

import (
"log"
"fmt"
"net/url"
"strconv"
"sync"

"github.com/digitalocean/godo"
"github.com/spf13/viper"
)

const maxFetchPages = 10

type paginatedList struct {
list []interface{}
mu sync.Mutex
}

func (pl *paginatedList) append(items ...interface{}) {
pl.mu.Lock()
defer pl.mu.Unlock()

pl.list = append(pl.list, items...)
}

// Generator is a function that generates the list to be paginated.
type Generator func(*godo.ListOptions) ([]interface{}, *godo.Response, error)

// PaginateResp paginates a Response.
func PaginateResp(gen Generator) ([]interface{}, error) {
opt := &godo.ListOptions{Page: 1, PerPage: 200}
list := []interface{}{}

for {
items, resp, err := gen(opt)
if err != nil {
return nil, err
}

for _, i := range items {
list = append(list, i)
}

if resp == nil || resp.Links.Pages == nil {
break
}

if uStr := resp.Links.Pages.Next; len(uStr) > 0 {
u, err := url.Parse(uStr)
if err != nil {
return nil, err
}

if viper.GetBool("debug") {
log.Printf("page.current=%v page.per=%v", opt.Page, opt.PerPage)
}
pageStr := u.Query().Get("page")
page, err := strconv.Atoi(pageStr)
if err != nil {
return nil, err
l := paginatedList{}

fetchChan := make(chan int, 5)

var wg sync.WaitGroup
for i := 0; i < 4; i++ {
wg.Add(1)
go func() {
for page := range fetchChan {
items, err := fetchPage(gen, page)
if err == nil {
l.append(items...)
}
}
wg.Done()
}()
}

opt.Page = page
continue
}
// fetch first page to get page count (x)
items, resp, err := gen(opt)
if err != nil {
return nil, err
}

l.append(items...)

break
// find last page
lp, err := lastPage(resp)
if err != nil {
return nil, err
}

return list, nil
// start with second page
for i := 2; i < lp; i++ {
fetchChan <- i
}
close(fetchChan)

wg.Wait()

return l.list, nil
}

func fetchPage(gen Generator, page int) ([]interface{}, error) {
opt := &godo.ListOptions{Page: page, PerPage: 200}
items, _, err := gen(opt)
return items, err
}

func lastPage(resp *godo.Response) (int, error) {
if resp.Links.Pages == nil {
// no other pages
return 1, nil
}

uStr := resp.Links.Pages.Last
if uStr == "" {
return 1, nil
}

u, err := url.Parse(uStr)
if err != nil {
return 0, fmt.Errorf("could not parse last page: %v", err)
}

pageStr := u.Query().Get("page")
page, err := strconv.Atoi(pageStr)
if err != nil {
return 0, fmt.Errorf("could not find page param: %v", err)
}

return page, err
}

0 comments on commit 2e915fc

Please sign in to comment.