Add pagination to List* endpoints (#309)

- Introduce kolide.ListOptions to store pagination params (in the future it can
  also store ordering/filtering params)
- Refactor service/datastore methods to take kolide.ListOptions
- Implement pagination
This commit is contained in:
Zachary Wasserman 2016-10-13 11:21:47 -07:00 committed by GitHub
parent f9fa3e289f
commit 7f636aef4f
50 changed files with 478 additions and 100 deletions

View file

@ -84,3 +84,15 @@ func openGORM(driver, conn string, maxAttempts int) (*gorm.DB, error) {
}
return db, nil
}
// applyLimitOffset applies the appropriate limit and offset parameters to the
// gorm.DB instance, returning a DB that can be chained as usual with *gorm.DB.
func (orm *gormDB) applyListOptions(opt kolide.ListOptions) *gorm.DB {
if opt.PerPage == 0 {
// PerPage value of 0 indicates unlimited
return orm.DB
}
offset := opt.Page * opt.PerPage
return orm.DB.Limit(opt.PerPage).Offset(offset)
}

View file

@ -100,9 +100,9 @@ func (orm gormDB) Host(id uint) (*kolide.Host, error) {
return host, nil
}
func (orm gormDB) Hosts() ([]*kolide.Host, error) {
func (orm gormDB) Hosts(opt kolide.ListOptions) ([]*kolide.Host, error) {
var hosts []*kolide.Host
err := orm.DB.Find(&hosts).Error
err := orm.applyListOptions(opt).Find(&hosts).Error
if err != nil {
return nil, err
}

View file

@ -21,9 +21,9 @@ func (orm gormDB) InviteByEmail(email string) (*kolide.Invite, error) {
return invite, nil
}
func (orm gormDB) Invites() ([]*kolide.Invite, error) {
func (orm gormDB) Invites(opt kolide.ListOptions) ([]*kolide.Invite, error) {
var invites []*kolide.Invite
err := orm.DB.Find(&invites).Error
err := orm.applyListOptions(opt).Find(&invites).Error
if err != nil {
return nil, err
}

View file

@ -54,9 +54,9 @@ func (orm gormDB) Label(lid uint) (*kolide.Label, error) {
return label, nil
}
func (orm gormDB) Labels() ([]*kolide.Label, error) {
func (orm gormDB) Labels(opt kolide.ListOptions) ([]*kolide.Label, error) {
var labels []*kolide.Label
err := orm.DB.Find(&labels).Error
err := orm.applyListOptions(opt).Find(&labels).Error
return labels, err
}

View file

@ -50,9 +50,9 @@ func (orm gormDB) Pack(pid uint) (*kolide.Pack, error) {
return pack, nil
}
func (orm gormDB) Packs() ([]*kolide.Pack, error) {
func (orm gormDB) Packs(opt kolide.ListOptions) ([]*kolide.Pack, error) {
var packs []*kolide.Pack
err := orm.DB.Find(&packs).Error
err := orm.applyListOptions(opt).Find(&packs).Error
return packs, err
}
@ -151,7 +151,7 @@ func (orm gormDB) ActivePacksForHost(hid uint) ([]*kolide.Pack, error) {
// we will need to give some subset of packs to this host based on the
// labels which this host is known to belong to
allPacks, err := orm.Packs()
allPacks, err := orm.Packs(kolide.ListOptions{})
if err != nil {
return nil, err
}

View file

@ -50,8 +50,8 @@ func (orm gormDB) Query(id uint) (*kolide.Query, error) {
return query, nil
}
func (orm gormDB) Queries() ([]*kolide.Query, error) {
func (orm gormDB) Queries(opt kolide.ListOptions) ([]*kolide.Query, error) {
var queries []*kolide.Query
err := orm.DB.Find(&queries).Error
err := orm.applyListOptions(opt).Find(&queries).Error
return queries, err
}

View file

@ -23,9 +23,9 @@ func (orm gormDB) User(username string) (*kolide.User, error) {
return user, nil
}
func (orm gormDB) Users() ([]*kolide.User, error) {
func (orm gormDB) Users(opt kolide.ListOptions) ([]*kolide.User, error) {
var users []*kolide.User
err := orm.DB.Find(&users).Error
err := orm.applyListOptions(opt).Find(&users).Error
if err != nil {
return nil, err
}

View file

@ -44,3 +44,23 @@ func (orm *inmem) Migrate() error {
func (orm *inmem) Drop() error {
return orm.Migrate()
}
// getLimitOffsetSliceBounds returns the bounds that should be used for
// re-slicing the results to comply with the requested ListOptions. Lack of
// generics forces us to do this rather than reslicing in this method.
func (orm *inmem) getLimitOffsetSliceBounds(opt kolide.ListOptions, length int) (low uint, high uint) {
if opt.PerPage == 0 {
// PerPage value of 0 indicates unlimited
return 0, uint(length)
}
offset := opt.Page * opt.PerPage
max := offset + opt.PerPage
if offset > uint(length) {
offset = uint(length)
}
if max > uint(length) {
max = uint(length)
}
return offset, max
}

View file

@ -2,6 +2,7 @@ package datastore
import (
"errors"
"sort"
"time"
"github.com/kolide/kolide-ose/server/kolide"
@ -59,14 +60,25 @@ func (orm *inmem) Host(id uint) (*kolide.Host, error) {
return host, nil
}
func (orm *inmem) Hosts() ([]*kolide.Host, error) {
func (orm *inmem) Hosts(opt kolide.ListOptions) ([]*kolide.Host, error) {
orm.mtx.Lock()
defer orm.mtx.Unlock()
hosts := []*kolide.Host{}
for _, host := range orm.hosts {
hosts = append(hosts, host)
// We need to sort by keys to provide reliable ordering
keys := []int{}
for k, _ := range orm.hosts {
keys = append(keys, int(k))
}
sort.Ints(keys)
hosts := []*kolide.Host{}
for _, k := range keys {
hosts = append(hosts, orm.hosts[uint(k)])
}
// Apply limit/offset
low, high := orm.getLimitOffsetSliceBounds(opt, len(hosts))
hosts = hosts[low:high]
return hosts, nil
}

View file

@ -1,6 +1,10 @@
package datastore
import "github.com/kolide/kolide-ose/server/kolide"
import (
"sort"
"github.com/kolide/kolide-ose/server/kolide"
)
// NewInvite creates and stores a new invitation in a DB.
func (orm *inmem) NewInvite(invite *kolide.Invite) (*kolide.Invite, error) {
@ -19,14 +23,25 @@ func (orm *inmem) NewInvite(invite *kolide.Invite) (*kolide.Invite, error) {
}
// Invites lists all invites in the datastore.
func (orm *inmem) Invites() ([]*kolide.Invite, error) {
func (orm *inmem) Invites(opt kolide.ListOptions) ([]*kolide.Invite, error) {
orm.mtx.Lock()
defer orm.mtx.Unlock()
var invites []*kolide.Invite
for _, invite := range orm.invites {
invites = append(invites, invite)
// We need to sort by keys to provide reliable ordering
keys := []int{}
for k, _ := range orm.invites {
keys = append(keys, int(k))
}
sort.Ints(keys)
invites := []*kolide.Invite{}
for _, k := range keys {
invites = append(invites, orm.invites[uint(k)])
}
// Apply limit/offset
low, high := orm.getLimitOffsetSliceBounds(opt, len(invites))
invites = invites[low:high]
return invites, nil
}

View file

@ -1,6 +1,8 @@
package datastore
import (
"sort"
"github.com/kolide/kolide-ose/server/kolide"
)
@ -58,14 +60,25 @@ func (orm *inmem) Pack(id uint) (*kolide.Pack, error) {
return pack, nil
}
func (orm *inmem) Packs() ([]*kolide.Pack, error) {
func (orm *inmem) Packs(opt kolide.ListOptions) ([]*kolide.Pack, error) {
orm.mtx.Lock()
defer orm.mtx.Unlock()
packs := []*kolide.Pack{}
for _, pack := range orm.packs {
packs = append(packs, pack)
// We need to sort by keys to provide reliable ordering
keys := []int{}
for k, _ := range orm.packs {
keys = append(keys, int(k))
}
sort.Ints(keys)
packs := []*kolide.Pack{}
for _, k := range keys {
packs = append(packs, orm.packs[uint(k)])
}
// Apply limit/offset
low, high := orm.getLimitOffsetSliceBounds(opt, len(packs))
packs = packs[low:high]
return packs, nil
}

View file

@ -1,6 +1,8 @@
package datastore
import (
"sort"
"github.com/kolide/kolide-ose/server/kolide"
)
@ -58,14 +60,25 @@ func (orm *inmem) Query(id uint) (*kolide.Query, error) {
return query, nil
}
func (orm *inmem) Queries() ([]*kolide.Query, error) {
func (orm *inmem) Queries(opt kolide.ListOptions) ([]*kolide.Query, error) {
orm.mtx.Lock()
defer orm.mtx.Unlock()
queries := []*kolide.Query{}
for _, query := range orm.queries {
queries = append(queries, query)
// We need to sort by keys to provide reliable ordering
keys := []int{}
for k, _ := range orm.queries {
keys = append(keys, int(k))
}
sort.Ints(keys)
queries := []*kolide.Query{}
for _, k := range keys {
queries = append(queries, orm.queries[uint(k)])
}
// Apply limit/offset
low, high := orm.getLimitOffsetSliceBounds(opt, len(queries))
queries = queries[low:high]
return queries, nil
}

View file

@ -0,0 +1,68 @@
package datastore
import (
"testing"
"github.com/kolide/kolide-ose/server/kolide"
"github.com/stretchr/testify/assert"
)
func TestApplyLimitOffset(t *testing.T) {
im := inmem{}
data := []int{}
// should work with empty
low, high := im.getLimitOffsetSliceBounds(kolide.ListOptions{}, len(data))
result := data[low:high]
assert.Len(t, result, 0)
low, high = im.getLimitOffsetSliceBounds(kolide.ListOptions{Page: 1, PerPage: 20}, len(data))
result = data[low:high]
assert.Len(t, result, 0)
// insert some data
for i := 0; i < 100; i++ {
data = append(data, i)
}
// unlimited
low, high = im.getLimitOffsetSliceBounds(kolide.ListOptions{}, len(data))
result = data[low:high]
assert.Len(t, result, 100)
assert.Equal(t, data, result)
// reasonable limit page 0
low, high = im.getLimitOffsetSliceBounds(kolide.ListOptions{PerPage: 20}, len(data))
result = data[low:high]
assert.Len(t, result, 20)
assert.Equal(t, data[:20], result)
// too many per page
low, high = im.getLimitOffsetSliceBounds(kolide.ListOptions{PerPage: 200}, len(data))
result = data[low:high]
assert.Len(t, result, 100)
assert.Equal(t, data, result)
// offset should be past end (zero results)
low, high = im.getLimitOffsetSliceBounds(kolide.ListOptions{Page: 1, PerPage: 200}, len(data))
result = data[low:high]
assert.Len(t, result, 0)
// all pages appended should equal the original data
result = []int{}
for i := 0; i < 5; i++ { // 5 used intentionally
low, high = im.getLimitOffsetSliceBounds(kolide.ListOptions{Page: uint(i), PerPage: 25}, len(data))
result = append(result, data[low:high]...)
}
assert.Len(t, result, 100)
assert.Equal(t, data, result)
// again with different params
result = []int{}
for i := 0; i < 100; i++ { // 5 used intentionally
low, high = im.getLimitOffsetSliceBounds(kolide.ListOptions{Page: uint(i), PerPage: 1}, len(data))
result = append(result, data[low:high]...)
}
assert.Len(t, result, 100)
assert.Equal(t, data, result)
}

View file

@ -1,6 +1,10 @@
package datastore
import "github.com/kolide/kolide-ose/server/kolide"
import (
"sort"
"github.com/kolide/kolide-ose/server/kolide"
)
func (orm *inmem) NewUser(user *kolide.User) (*kolide.User, error) {
orm.mtx.Lock()
@ -31,14 +35,25 @@ func (orm *inmem) User(username string) (*kolide.User, error) {
return nil, ErrNotFound
}
func (orm *inmem) Users() ([]*kolide.User, error) {
func (orm *inmem) Users(opt kolide.ListOptions) ([]*kolide.User, error) {
orm.mtx.Lock()
defer orm.mtx.Unlock()
var users []*kolide.User
for _, user := range orm.users {
users = append(users, user)
// We need to sort by keys to provide reliable ordering
keys := []int{}
for k, _ := range orm.users {
keys = append(keys, int(k))
}
sort.Ints(keys)
users := []*kolide.User{}
for _, k := range keys {
users = append(users, orm.users[uint(k)])
}
// Apply limit/offset
low, high := orm.getLimitOffsetSliceBounds(opt, len(users))
users = users[low:high]
return users, nil
}

View file

@ -32,3 +32,13 @@ type OrgInfoPayload struct {
OrgName *string `json:"org_name"`
OrgLogoURL *string `json:"org_logo_url"`
}
// ListOptions defines options related to paging and ordering to be used when
// listing objects
type ListOptions struct {
// Which page to return (must be positive integer)
Page uint
// How many results per page (must be positive integer, 0 indicates
// unlimited)
PerPage uint
}

View file

@ -11,14 +11,14 @@ type HostStore interface {
SaveHost(host *Host) error
DeleteHost(host *Host) error
Host(id uint) (*Host, error)
Hosts() ([]*Host, error)
Hosts(opt ListOptions) ([]*Host, error)
EnrollHost(uuid, hostname, ip, platform string, nodeKeySize int) (*Host, error)
AuthenticateHost(nodeKey string) (*Host, error)
MarkHostSeen(host *Host, t time.Time) error
}
type HostService interface {
ListHosts(ctx context.Context) ([]*Host, error)
ListHosts(ctx context.Context, opt ListOptions) ([]*Host, error)
GetHost(ctx context.Context, id uint) (*Host, error)
HostStatus(ctx context.Context, host Host) string
DeleteHost(ctx context.Context, id uint) error

View file

@ -15,7 +15,7 @@ type InviteStore interface {
NewInvite(i *Invite) (*Invite, error)
// Invites lists all invites in the datastore.
Invites() ([]*Invite, error)
Invites(opt ListOptions) ([]*Invite, error)
// Invite retrieves an invite by it's ID.
Invite(id uint) (*Invite, error)
@ -40,7 +40,7 @@ type InviteService interface {
DeleteInvite(ctx context.Context, id uint) (err error)
// Invites returns a list of all invites.
Invites(ctx context.Context) (invites []*Invite, err error)
ListInvites(ctx context.Context, opt ListOptions) (invites []*Invite, err error)
// VerifyInvite verifies that an invite exists and that it matches the
// invite token.

View file

@ -12,7 +12,7 @@ type LabelStore interface {
SaveLabel(Label *Label) error
DeleteLabel(lid uint) error
Label(lid uint) (*Label, error)
Labels() ([]*Label, error)
Labels(opt ListOptions) ([]*Label, error)
// LabelQueriesForHost returns the label queries that should be executed
// for the given host. The cutoff is the minimum timestamp a query
@ -32,7 +32,7 @@ type LabelStore interface {
}
type LabelService interface {
ListLabels(ctx context.Context) ([]*Label, error)
ListLabels(ctx context.Context, opt ListOptions) ([]*Label, error)
GetLabel(ctx context.Context, id uint) (*Label, error)
NewLabel(ctx context.Context, p LabelPayload) (*Label, error)
ModifyLabel(ctx context.Context, id uint, p LabelPayload) (*Label, error)

View file

@ -12,7 +12,7 @@ type PackStore interface {
SavePack(pack *Pack) error
DeletePack(pid uint) error
Pack(pid uint) (*Pack, error)
Packs() ([]*Pack, error)
Packs(opt ListOptions) ([]*Pack, error)
// Modifying the queries in packs
AddQueryToPack(qid uint, pid uint) error
@ -29,7 +29,7 @@ type PackStore interface {
}
type PackService interface {
ListPacks(ctx context.Context) ([]*Pack, error)
ListPacks(ctx context.Context, opt ListOptions) ([]*Pack, error)
GetPack(ctx context.Context, id uint) (*Pack, error)
NewPack(ctx context.Context, p PackPayload) (*Pack, error)
ModifyPack(ctx context.Context, id uint, p PackPayload) (*Pack, error)

View file

@ -12,11 +12,11 @@ type QueryStore interface {
SaveQuery(query *Query) error
DeleteQuery(query *Query) error
Query(id uint) (*Query, error)
Queries() ([]*Query, error)
Queries(opt ListOptions) ([]*Query, error)
}
type QueryService interface {
ListQueries(ctx context.Context) ([]*Query, error)
ListQueries(ctx context.Context, opt ListOptions) ([]*Query, error)
GetQuery(ctx context.Context, id uint) (*Query, error)
NewQuery(ctx context.Context, p QueryPayload) (*Query, error)
ModifyQuery(ctx context.Context, id uint, p QueryPayload) (*Query, error)

View file

@ -14,7 +14,7 @@ import (
type UserStore interface {
NewUser(user *User) (*User, error)
User(username string) (*User, error)
Users() ([]*User, error)
Users(opt ListOptions) ([]*User, error)
UserByEmail(email string) (*User, error)
UserByID(id uint) (*User, error)
SaveUser(user *User) error
@ -33,7 +33,7 @@ type UserService interface {
AuthenticatedUser(ctx context.Context) (user *User, err error)
// Users returns all users
Users(ctx context.Context) (users []*User, err error)
ListUsers(ctx context.Context, opt ListOptions) (users []*User, err error)
// RequestPasswordReset generates a password reset request for
// a user. The request results in a token emailed to the user.

View file

@ -41,6 +41,10 @@ func makeGetHostEndpoint(svc kolide.Service) endpoint.Endpoint {
// List Hosts
////////////////////////////////////////////////////////////////////////////////
type listHostsRequest struct {
ListOptions kolide.ListOptions
}
type listHostsResponse struct {
Hosts []hostResponse `json:"hosts"`
Err error `json:"error,omitempty"`
@ -50,7 +54,8 @@ func (r listHostsResponse) error() error { return r.Err }
func makeListHostsEndpoint(svc kolide.Service) endpoint.Endpoint {
return func(ctx context.Context, request interface{}) (interface{}, error) {
hosts, err := svc.ListHosts(ctx)
req := request.(listHostsRequest)
hosts, err := svc.ListHosts(ctx, req.ListOptions)
if err != nil {
return listHostsResponse{Err: err}, nil
}

View file

@ -28,6 +28,10 @@ func makeCreateInviteEndpoint(svc kolide.Service) endpoint.Endpoint {
}
}
type listInvitesRequest struct {
ListOptions kolide.ListOptions
}
type listInvitesResponse struct {
Invites []kolide.Invite `json:"invites"`
Err error `json:"error,omitempty"`
@ -37,7 +41,8 @@ func (r listInvitesResponse) error() error { return r.Err }
func makeListInvitesEndpoint(svc kolide.Service) endpoint.Endpoint {
return func(ctx context.Context, request interface{}) (interface{}, error) {
invites, err := svc.Invites(ctx)
req := request.(listInvitesRequest)
invites, err := svc.ListInvites(ctx, req.ListOptions)
if err != nil {
return listInvitesResponse{Err: err}, nil
}

View file

@ -36,6 +36,10 @@ func makeGetLabelEndpoint(svc kolide.Service) endpoint.Endpoint {
// List Labels
////////////////////////////////////////////////////////////////////////////////
type listLabelsRequest struct {
ListOptions kolide.ListOptions
}
type listLabelsResponse struct {
Labels []kolide.Label `json:"labels"`
Err error `json:"error,omitempty"`
@ -45,7 +49,8 @@ func (r listLabelsResponse) error() error { return r.Err }
func makeListLabelsEndpoint(svc kolide.Service) endpoint.Endpoint {
return func(ctx context.Context, request interface{}) (interface{}, error) {
labels, err := svc.ListLabels(ctx)
req := request.(listLabelsRequest)
labels, err := svc.ListLabels(ctx, req.ListOptions)
if err != nil {
return listLabelsResponse{Err: err}, nil
}

View file

@ -36,6 +36,10 @@ func makeGetPackEndpoint(svc kolide.Service) endpoint.Endpoint {
// List Packs
////////////////////////////////////////////////////////////////////////////////
type listPacksRequest struct {
ListOptions kolide.ListOptions
}
type listPacksResponse struct {
Packs []kolide.Pack `json:"packs"`
Err error `json:"error,omitempty"`
@ -45,7 +49,8 @@ func (r listPacksResponse) error() error { return r.Err }
func makeListPacksEndpoint(svc kolide.Service) endpoint.Endpoint {
return func(ctx context.Context, request interface{}) (interface{}, error) {
packs, err := svc.ListPacks(ctx)
req := request.(listPacksRequest)
packs, err := svc.ListPacks(ctx, req.ListOptions)
if err != nil {
return getPackResponse{Err: err}, nil
}

View file

@ -35,6 +35,9 @@ func makeGetQueryEndpoint(svc kolide.Service) endpoint.Endpoint {
////////////////////////////////////////////////////////////////////////////////
// List Queries
////////////////////////////////////////////////////////////////////////////////
type listQueriesRequest struct {
ListOptions kolide.ListOptions
}
type listQueriesResponse struct {
Queries []kolide.Query `json:"queries"`
@ -45,7 +48,8 @@ func (r listQueriesResponse) error() error { return r.Err }
func makeListQueriesEndpoint(svc kolide.Service) endpoint.Endpoint {
return func(ctx context.Context, request interface{}) (interface{}, error) {
queries, err := svc.ListQueries(ctx)
req := request.(listQueriesRequest)
queries, err := svc.ListQueries(ctx, req.ListOptions)
if err != nil {
return listQueriesResponse{Err: err}, nil
}

View file

@ -74,6 +74,10 @@ func makeGetSessionUserEndpoint(svc kolide.Service) endpoint.Endpoint {
// List Users
////////////////////////////////////////////////////////////////////////////////
type listUsersRequest struct {
ListOptions kolide.ListOptions
}
type listUsersResponse struct {
Users []kolide.User `json:"users"`
Err error `json:"error,omitempty"`
@ -83,7 +87,8 @@ func (r listUsersResponse) error() error { return r.Err }
func makeListUsersEndpoint(svc kolide.Service) endpoint.Endpoint {
return func(ctx context.Context, request interface{}) (interface{}, error) {
users, err := svc.Users(ctx)
req := request.(listUsersRequest)
users, err := svc.ListUsers(ctx, req.ListOptions)
if err != nil {
return listUsersResponse{Err: err}, nil
}

View file

@ -181,7 +181,7 @@ func makeKolideKitHandlers(ctx context.Context, e KolideEndpoints, opts []kithtt
Me: newServer(e.Me, decodeNoParamsRequest),
CreateUser: newServer(e.CreateUser, decodeCreateUserRequest),
GetUser: newServer(e.GetUser, decodeGetUserRequest),
ListUsers: newServer(e.ListUsers, decodeNoParamsRequest),
ListUsers: newServer(e.ListUsers, decodeListUsersRequest),
ModifyUser: newServer(e.ModifyUser, decodeModifyUserRequest),
GetSessionsForUserInfo: newServer(e.GetSessionsForUserInfo, decodeGetInfoAboutSessionsForUserRequest),
DeleteSessionsForUser: newServer(e.DeleteSessionsForUser, decodeDeleteSessionsForUserRequest),
@ -190,15 +190,15 @@ func makeKolideKitHandlers(ctx context.Context, e KolideEndpoints, opts []kithtt
GetAppConfig: newServer(e.GetAppConfig, decodeNoParamsRequest),
ModifyAppConfig: newServer(e.ModifyAppConfig, decodeModifyAppConfigRequest),
CreateInvite: newServer(e.CreateInvite, decodeCreateInviteRequest),
ListInvites: newServer(e.ListInvites, decodeNoParamsRequest),
ListInvites: newServer(e.ListInvites, decodeListInvitesRequest),
DeleteInvite: newServer(e.DeleteInvite, decodeDeleteInviteRequest),
GetQuery: newServer(e.GetQuery, decodeGetQueryRequest),
ListQueries: newServer(e.ListQueries, decodeNoParamsRequest),
ListQueries: newServer(e.ListQueries, decodeListQueriesRequest),
CreateQuery: newServer(e.CreateQuery, decodeCreateQueryRequest),
ModifyQuery: newServer(e.ModifyQuery, decodeModifyQueryRequest),
DeleteQuery: newServer(e.DeleteQuery, decodeDeleteQueryRequest),
GetPack: newServer(e.GetPack, decodeGetPackRequest),
ListPacks: newServer(e.ListPacks, decodeNoParamsRequest),
ListPacks: newServer(e.ListPacks, decodeListPacksRequest),
CreatePack: newServer(e.CreatePack, decodeCreatePackRequest),
ModifyPack: newServer(e.ModifyPack, decodeModifyPackRequest),
DeletePack: newServer(e.DeletePack, decodeDeletePackRequest),
@ -211,7 +211,7 @@ func makeKolideKitHandlers(ctx context.Context, e KolideEndpoints, opts []kithtt
SubmitDistributedQueryResults: newServer(e.SubmitDistributedQueryResults, decodeSubmitDistributedQueryResultsRequest),
SubmitLogs: newServer(e.SubmitLogs, decodeSubmitLogsRequest),
GetLabel: newServer(e.GetLabel, decodeGetLabelRequest),
ListLabels: newServer(e.ListLabels, decodeNoParamsRequest),
ListLabels: newServer(e.ListLabels, decodeListLabelsRequest),
CreateLabel: newServer(e.CreateLabel, decodeCreateLabelRequest),
ModifyLabel: newServer(e.ModifyLabel, decodeModifyLabelRequest),
DeleteLabel: newServer(e.DeleteLabel, decodeDeleteLabelRequest),
@ -220,7 +220,7 @@ func makeKolideKitHandlers(ctx context.Context, e KolideEndpoints, opts []kithtt
DeleteLabelFromPack: newServer(e.DeleteLabelFromPack, decodeDeleteLabelFromPackRequest),
GetHost: newServer(e.GetHost, decodeGetHostRequest),
DeleteHost: newServer(e.DeleteHost, decodeDeleteHostRequest),
ListHosts: newServer(e.ListHosts, decodeNoParamsRequest),
ListHosts: newServer(e.ListHosts, decodeListHostsRequest),
}
}

View file

@ -68,7 +68,7 @@ func (mw loggingMiddleware) Invites(ctx context.Context) ([]*kolide.Invite, erro
"took", time.Since(begin),
)
}(time.Now())
invites, err = mw.Service.Invites(ctx)
invites, err = mw.Service.ListInvites(ctx, kolide.ListOptions{})
return invites, err
}

View file

@ -35,7 +35,7 @@ func (mw metricsMiddleware) DeleteInvite(ctx context.Context, id uint) error {
return err
}
func (mw metricsMiddleware) Invites(ctx context.Context) ([]*kolide.Invite, error) {
func (mw metricsMiddleware) ListInvites(ctx context.Context, opt kolide.ListOptions) ([]*kolide.Invite, error) {
var (
invites []*kolide.Invite
err error
@ -45,7 +45,7 @@ func (mw metricsMiddleware) Invites(ctx context.Context) ([]*kolide.Invite, erro
mw.requestCount.With(lvs...).Add(1)
mw.requestLatency.With(lvs...).Observe(time.Since(begin).Seconds())
}(time.Now())
invites, err = mw.Service.Invites(ctx)
invites, err = mw.Service.ListInvites(ctx, opt)
return invites, err
}

View file

@ -55,7 +55,7 @@ func (mw metricsMiddleware) User(ctx context.Context, id uint) (*kolide.User, er
return user, err
}
func (mw metricsMiddleware) Users(ctx context.Context) ([]*kolide.User, error) {
func (mw metricsMiddleware) ListUsers(ctx context.Context, opt kolide.ListOptions) ([]*kolide.User, error) {
var (
users []*kolide.User
@ -67,7 +67,7 @@ func (mw metricsMiddleware) Users(ctx context.Context) ([]*kolide.User, error) {
mw.requestLatency.With(lvs...).Observe(time.Since(begin).Seconds())
}(time.Now())
users, err = mw.Service.Users(ctx)
users, err = mw.Service.ListUsers(ctx, opt)
return users, err
}

View file

@ -7,8 +7,8 @@ import (
"golang.org/x/net/context"
)
func (svc service) ListHosts(ctx context.Context) ([]*kolide.Host, error) {
return svc.ds.Hosts()
func (svc service) ListHosts(ctx context.Context, opt kolide.ListOptions) ([]*kolide.Host, error) {
return svc.ds.Hosts(opt)
}
func (svc service) GetHost(ctx context.Context, id uint) (*kolide.Host, error) {

View file

@ -18,7 +18,7 @@ func TestListHosts(t *testing.T) {
ctx := context.Background()
hosts, err := svc.ListHosts(ctx)
hosts, err := svc.ListHosts(ctx, kolide.ListOptions{})
assert.Nil(t, err)
assert.Len(t, hosts, 0)
@ -27,7 +27,7 @@ func TestListHosts(t *testing.T) {
})
assert.Nil(t, err)
hosts, err = svc.ListHosts(ctx)
hosts, err = svc.ListHosts(ctx, kolide.ListOptions{})
assert.Nil(t, err)
assert.Len(t, hosts, 1)
}
@ -71,7 +71,7 @@ func TestDeleteHost(t *testing.T) {
err = svc.DeleteHost(ctx, host.ID)
assert.Nil(t, err)
hosts, err := ds.Hosts()
hosts, err := ds.Hosts(kolide.ListOptions{})
assert.Nil(t, err)
assert.Len(t, hosts, 0)

View file

@ -61,8 +61,8 @@ func (svc service) InviteNewUser(ctx context.Context, payload kolide.InvitePaylo
return invite, nil
}
func (svc service) Invites(ctx context.Context) ([]*kolide.Invite, error) {
return svc.ds.Invites()
func (svc service) ListInvites(ctx context.Context, opt kolide.ListOptions) ([]*kolide.Invite, error) {
return svc.ds.Invites(opt)
}
func (svc service) VerifyInvite(ctx context.Context, email, token string) error {

View file

@ -5,8 +5,8 @@ import (
"golang.org/x/net/context"
)
func (svc service) ListLabels(ctx context.Context) ([]*kolide.Label, error) {
return svc.ds.Labels()
func (svc service) ListLabels(ctx context.Context, opt kolide.ListOptions) ([]*kolide.Label, error) {
return svc.ds.Labels(opt)
}
func (svc service) GetLabel(ctx context.Context, id uint) (*kolide.Label, error) {

View file

@ -18,7 +18,7 @@ func TestListLabels(t *testing.T) {
ctx := context.Background()
labels, err := svc.ListLabels(ctx)
labels, err := svc.ListLabels(ctx, kolide.ListOptions{})
assert.Nil(t, err)
assert.Len(t, labels, 0)
@ -28,7 +28,7 @@ func TestListLabels(t *testing.T) {
})
assert.Nil(t, err)
labels, err = svc.ListLabels(ctx)
labels, err = svc.ListLabels(ctx, kolide.ListOptions{})
assert.Nil(t, err)
assert.Len(t, labels, 1)
assert.Equal(t, "foo", labels[0].Name)
@ -75,7 +75,7 @@ func TestNewLabel(t *testing.T) {
assert.Nil(t, err)
labels, err := ds.Labels()
labels, err := ds.Labels(kolide.ListOptions{})
assert.Nil(t, err)
assert.Len(t, labels, 1)
assert.Equal(t, "foo", labels[0].Name)
@ -127,7 +127,7 @@ func TestDeleteLabel(t *testing.T) {
err = svc.DeleteLabel(ctx, label.ID)
assert.Nil(t, err)
labels, err := ds.Labels()
labels, err := ds.Labels(kolide.ListOptions{})
assert.Nil(t, err)
assert.Len(t, labels, 0)
}

View file

@ -26,7 +26,7 @@ func TestEnrollAgent(t *testing.T) {
ctx := context.Background()
hosts, err := ds.Hosts()
hosts, err := ds.Hosts(kolide.ListOptions{})
assert.Nil(t, err)
assert.Len(t, hosts, 0)
@ -34,7 +34,7 @@ func TestEnrollAgent(t *testing.T) {
assert.Nil(t, err)
assert.NotEmpty(t, nodeKey)
hosts, err = ds.Hosts()
hosts, err = ds.Hosts(kolide.ListOptions{})
assert.Nil(t, err)
assert.Len(t, hosts, 1)
}
@ -48,7 +48,7 @@ func TestEnrollAgentIncorrectEnrollSecret(t *testing.T) {
ctx := context.Background()
hosts, err := ds.Hosts()
hosts, err := ds.Hosts(kolide.ListOptions{})
assert.Nil(t, err)
assert.Len(t, hosts, 0)
@ -56,7 +56,7 @@ func TestEnrollAgentIncorrectEnrollSecret(t *testing.T) {
assert.NotNil(t, err)
assert.Empty(t, nodeKey)
hosts, err = ds.Hosts()
hosts, err = ds.Hosts(kolide.ListOptions{})
assert.Nil(t, err)
assert.Len(t, hosts, 0)
}
@ -75,7 +75,7 @@ func TestSubmitStatusLogs(t *testing.T) {
_, err = svc.EnrollAgent(ctx, "", "host123")
assert.Nil(t, err)
hosts, err := ds.Hosts()
hosts, err := ds.Hosts(kolide.ListOptions{})
require.Nil(t, err)
require.Len(t, hosts, 1)
host := hosts[0]
@ -147,7 +147,7 @@ func TestSubmitResultLogs(t *testing.T) {
_, err = svc.EnrollAgent(ctx, "", "host123")
assert.Nil(t, err)
hosts, err := ds.Hosts()
hosts, err := ds.Hosts(kolide.ListOptions{})
require.Nil(t, err)
require.Len(t, hosts, 1)
host := hosts[0]
@ -248,7 +248,7 @@ func TestLabelQueries(t *testing.T) {
_, err = svc.EnrollAgent(ctx, "", "host123")
assert.Nil(t, err)
hosts, err := ds.Hosts()
hosts, err := ds.Hosts(kolide.ListOptions{})
require.Nil(t, err)
require.Len(t, hosts, 1)
host := hosts[0]
@ -410,14 +410,14 @@ func TestGetClientConfig(t *testing.T) {
ctx := context.Background()
hosts, err := ds.Hosts()
hosts, err := ds.Hosts(kolide.ListOptions{})
require.Nil(t, err)
require.Len(t, hosts, 0)
_, err = svc.EnrollAgent(ctx, "", "user.local")
assert.Nil(t, err)
hosts, err = ds.Hosts()
hosts, err = ds.Hosts(kolide.ListOptions{})
require.Nil(t, err)
require.Len(t, hosts, 1)
host := hosts[0]

View file

@ -5,8 +5,8 @@ import (
"golang.org/x/net/context"
)
func (svc service) ListPacks(ctx context.Context) ([]*kolide.Pack, error) {
return svc.ds.Packs()
func (svc service) ListPacks(ctx context.Context, opt kolide.ListOptions) ([]*kolide.Pack, error) {
return svc.ds.Packs(opt)
}
func (svc service) GetPack(ctx context.Context, id uint) (*kolide.Pack, error) {

View file

@ -18,7 +18,7 @@ func TestListPacks(t *testing.T) {
ctx := context.Background()
queries, err := svc.ListPacks(ctx)
queries, err := svc.ListPacks(ctx, kolide.ListOptions{})
assert.Nil(t, err)
assert.Len(t, queries, 0)
@ -27,7 +27,7 @@ func TestListPacks(t *testing.T) {
})
assert.Nil(t, err)
queries, err = svc.ListPacks(ctx)
queries, err = svc.ListPacks(ctx, kolide.ListOptions{})
assert.Nil(t, err)
assert.Len(t, queries, 1)
}
@ -70,7 +70,7 @@ func TestNewPack(t *testing.T) {
assert.Nil(t, err)
queries, err := ds.Packs()
queries, err := ds.Packs(kolide.ListOptions{})
assert.Nil(t, err)
assert.Len(t, queries, 1)
}
@ -120,7 +120,7 @@ func TestDeletePack(t *testing.T) {
err = svc.DeletePack(ctx, pack.ID)
assert.Nil(t, err)
queries, err := ds.Packs()
queries, err := ds.Packs(kolide.ListOptions{})
assert.Nil(t, err)
assert.Len(t, queries, 0)

View file

@ -5,8 +5,8 @@ import (
"golang.org/x/net/context"
)
func (svc service) ListQueries(ctx context.Context) ([]*kolide.Query, error) {
return svc.ds.Queries()
func (svc service) ListQueries(ctx context.Context, opt kolide.ListOptions) ([]*kolide.Query, error) {
return svc.ds.Queries(opt)
}
func (svc service) GetQuery(ctx context.Context, id uint) (*kolide.Query, error) {

View file

@ -18,7 +18,7 @@ func TestListQueries(t *testing.T) {
ctx := context.Background()
queries, err := svc.ListQueries(ctx)
queries, err := svc.ListQueries(ctx, kolide.ListOptions{})
assert.Nil(t, err)
assert.Len(t, queries, 0)
@ -28,7 +28,7 @@ func TestListQueries(t *testing.T) {
})
assert.Nil(t, err)
queries, err = svc.ListQueries(ctx)
queries, err = svc.ListQueries(ctx, kolide.ListOptions{})
assert.Nil(t, err)
assert.Len(t, queries, 1)
}
@ -74,7 +74,7 @@ func TestNewQuery(t *testing.T) {
assert.Nil(t, err)
queries, err := ds.Queries()
queries, err := ds.Queries(kolide.ListOptions{})
assert.Nil(t, err)
assert.Len(t, queries, 1)
}
@ -126,7 +126,7 @@ func TestDeleteQuery(t *testing.T) {
err = svc.DeleteQuery(ctx, query.ID)
assert.Nil(t, err)
queries, err := ds.Queries()
queries, err := ds.Queries(kolide.ListOptions{})
assert.Nil(t, err)
assert.Len(t, queries, 0)

View file

@ -115,8 +115,8 @@ func (svc service) AuthenticatedUser(ctx context.Context) (*kolide.User, error)
return vc.User, nil
}
func (svc service) Users(ctx context.Context) ([]*kolide.User, error) {
return svc.ds.Users()
func (svc service) ListUsers(ctx context.Context, opt kolide.ListOptions) ([]*kolide.User, error) {
return svc.ds.Users(opt)
}
func (svc service) ResetPassword(ctx context.Context, token, password string) error {

View file

@ -7,6 +7,7 @@ import (
"strconv"
"github.com/gorilla/mux"
"github.com/kolide/kolide-ose/server/kolide"
"golang.org/x/net/context"
)
@ -52,6 +53,50 @@ func idFromRequest(r *http.Request, name string) (uint, error) {
return uint(uid), nil
}
// default number of items to include per page
const defaultPerPage = 20
// listOptionsFromRequest parses the list options from the request parameters
func listOptionsFromRequest(r *http.Request) (kolide.ListOptions, error) {
var err error
pageString := r.URL.Query().Get("page")
perPageString := r.URL.Query().Get("per_page")
var page int = 0
if pageString != "" {
page, err = strconv.Atoi(pageString)
if err != nil {
return kolide.ListOptions{}, errors.New("non-int page value")
}
if page < 0 {
return kolide.ListOptions{}, errors.New("negative page value")
}
}
// We default to 0 for per_page so that not specifying any paging
// information gets all results
var perPage int = 0
if perPageString != "" {
perPage, err = strconv.Atoi(perPageString)
if err != nil {
return kolide.ListOptions{}, errors.New("non-int per_page value")
}
if perPage <= 0 {
return kolide.ListOptions{}, errors.New("invalid per_page value")
}
}
if perPage == 0 && pageString != "" {
// We explicitly set a non-zero default if a page is specified
// (because the client probably intended for paging, and
// leaving the 0 would turn that off)
perPage = defaultPerPage
}
return kolide.ListOptions{Page: uint(page), PerPage: uint(perPage)}, nil
}
func decodeNoParamsRequest(ctx context.Context, r *http.Request) (interface{}, error) {
return nil, nil
}

View file

@ -21,3 +21,11 @@ func decodeDeleteHostRequest(ctx context.Context, r *http.Request) (interface{},
}
return deleteHostRequest{ID: id}, nil
}
func decodeListHostsRequest(ctx context.Context, r *http.Request) (interface{}, error) {
opt, err := listOptionsFromRequest(r)
if err != nil {
return nil, err
}
return listHostsRequest{ListOptions: opt}, nil
}

View file

@ -32,3 +32,11 @@ func decodeDeleteInviteRequest(ctx context.Context, r *http.Request) (interface{
req.ID = id
return req, nil
}
func decodeListInvitesRequest(ctx context.Context, r *http.Request) (interface{}, error) {
opt, err := listOptionsFromRequest(r)
if err != nil {
return nil, err
}
return listInvitesRequest{ListOptions: opt}, nil
}

View file

@ -47,3 +47,11 @@ func decodeGetLabelRequest(ctx context.Context, r *http.Request) (interface{}, e
req.ID = id
return req, nil
}
func decodeListLabelsRequest(ctx context.Context, r *http.Request) (interface{}, error) {
opt, err := listOptionsFromRequest(r)
if err != nil {
return nil, err
}
return listLabelsRequest{ListOptions: opt}, nil
}

View file

@ -49,6 +49,14 @@ func decodeGetPackRequest(ctx context.Context, r *http.Request) (interface{}, er
return req, nil
}
func decodeListPacksRequest(ctx context.Context, r *http.Request) (interface{}, error) {
opt, err := listOptionsFromRequest(r)
if err != nil {
return nil, err
}
return listPacksRequest{ListOptions: opt}, nil
}
func decodeAddQueryToPackRequest(ctx context.Context, r *http.Request) (interface{}, error) {
qid, err := idFromRequest(r, "qid")
if err != nil {

View file

@ -47,3 +47,11 @@ func decodeGetQueryRequest(ctx context.Context, r *http.Request) (interface{}, e
req.ID = id
return req, nil
}
func decodeListQueriesRequest(ctx context.Context, r *http.Request) (interface{}, error) {
opt, err := listOptionsFromRequest(r)
if err != nil {
return nil, err
}
return listQueriesRequest{ListOptions: opt}, nil
}

View file

@ -0,0 +1,78 @@
package service
import (
"net/http"
"net/url"
"testing"
"github.com/kolide/kolide-ose/server/kolide"
"github.com/stretchr/testify/assert"
)
func TestListOptionsFromRequest(t *testing.T) {
var listOptionsTests = []struct {
// url string to parse
url string
// expected list options
listOptions kolide.ListOptions
// should cause an error
shouldErr bool
}{
// both params provided
{
url: "/foo?page=1&per_page=10",
listOptions: kolide.ListOptions{Page: 1, PerPage: 10},
},
// only per_page (page should default to 0)
{
url: "/foo?per_page=10",
listOptions: kolide.ListOptions{Page: 0, PerPage: 10},
},
// only page (per_page should default to defaultPerPage
{
url: "/foo?page=10",
listOptions: kolide.ListOptions{Page: 10, PerPage: defaultPerPage},
},
// no params provided (defaults to empty ListOptions indicating
// unlimited)
{
url: "/foo?unrelated=foo",
listOptions: kolide.ListOptions{},
},
// various error cases
{
url: "/foo?page=foo&per_page=10",
shouldErr: true,
},
{
url: "/foo?page=1&per_page=foo",
shouldErr: true,
},
{
url: "/foo?page=-1",
shouldErr: true,
},
{
url: "/foo?page=-1&per_page=-10",
shouldErr: true,
},
}
for _, tt := range listOptionsTests {
t.Run(tt.url, func(t *testing.T) {
url, _ := url.Parse(tt.url)
req := &http.Request{URL: url}
opt, err := listOptionsFromRequest(req)
if tt.shouldErr {
assert.NotNil(t, err)
return
}
assert.Nil(t, err)
assert.Equal(t, tt.listOptions, opt)
})
}
}

View file

@ -24,6 +24,14 @@ func decodeGetUserRequest(ctx context.Context, r *http.Request) (interface{}, er
return getUserRequest{ID: id}, nil
}
func decodeListUsersRequest(ctx context.Context, r *http.Request) (interface{}, error) {
opt, err := listOptionsFromRequest(r)
if err != nil {
return nil, err
}
return listUsersRequest{ListOptions: opt}, nil
}
func decodeChangePasswordRequest(ctx context.Context, r *http.Request) (interface{}, error) {
var req resetPasswordRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {