Skip to content

Commit

Permalink
internal/host: add host catalog pagination
Browse files Browse the repository at this point in the history
  • Loading branch information
johanbrandhorst committed Nov 9, 2023
1 parent a973259 commit 6bdba65
Show file tree
Hide file tree
Showing 6 changed files with 765 additions and 3 deletions.
43 changes: 40 additions & 3 deletions internal/host/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,14 @@

package host

import (
"errors"

"github.com/hashicorp/boundary/internal/db"
"github.com/hashicorp/boundary/internal/pagination"
"github.com/hashicorp/boundary/internal/util"
)

// GetOpts - iterate the inbound Options and return a struct
func GetOpts(opt ...Option) (options, error) {
opts := getDefaultOptions()
Expand All @@ -19,9 +27,12 @@ type Option func(*options) error

// options = how options are represented
type options struct {
WithLimit int
WithOrderByCreateTime bool
Ascending bool
WithLimit int
WithOrderByCreateTime bool
Ascending bool
WithStartPageAfterItem pagination.Item
WithReader db.Reader
WithWriter db.Writer
}

func getDefaultOptions() options {
Expand All @@ -47,3 +58,29 @@ func WithOrderByCreateTime(ascending bool) Option {
return nil
}
}

// WithStartPageAfterItem is used to paginate over the results.
// The next page will start after the provided item.
func WithStartPageAfterItem(item pagination.Item) Option {
return func(o *options) error {
o.WithStartPageAfterItem = item
return nil
}
}

// WithReaderWriter allows the caller to pass an inflight transaction to be used
// for all database operations. If WithReaderWriter(...) is used, then the
// caller is responsible for managing the transaction.
func WithReaderWriter(r db.Reader, w db.Writer) Option {
return func(o *options) error {
switch {
case util.IsNil(r):
return errors.New("nil reader")
case util.IsNil(w):
return errors.New("nil writer")
}
o.WithReader = r
o.WithWriter = w
return nil
}
}
72 changes: 72 additions & 0 deletions internal/host/options_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,37 @@ package host

import (
"testing"
"time"

"github.com/hashicorp/boundary/internal/db"
"github.com/hashicorp/boundary/internal/db/timestamp"
"github.com/hashicorp/boundary/internal/pagination"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

type fakeWriter struct {
db.Writer
}

type fakeReader struct {
db.Reader
}

type fakeItem struct {
pagination.Item
publicId string
updateTime time.Time
}

func (p *fakeItem) GetPublicId() string {
return p.publicId
}

func (p *fakeItem) GetUpdateTime() *timestamp.Timestamp {
return timestamp.New(p.updateTime)
}

func Test_GetOpts(t *testing.T) {
t.Parallel()
t.Run("WithLimit", func(t *testing.T) {
Expand All @@ -34,4 +60,50 @@ func Test_GetOpts(t *testing.T) {
testOpts.Ascending = true
assert.Equal(t, opts, testOpts)
})
t.Run("WithStartPageAfterItem", func(t *testing.T) {
assert := assert.New(t)
updateTime := time.Now()
opts, err := GetOpts(WithStartPageAfterItem(&fakeItem{nil, "s_1", updateTime}))
require.NoError(t, err)
assert.Equal(opts.WithStartPageAfterItem.GetPublicId(), "s_1")
assert.Equal(opts.WithStartPageAfterItem.GetUpdateTime(), timestamp.New(updateTime))
})
t.Run("WithReaderWriter", func(t *testing.T) {
t.Parallel()
t.Run("success", func(t *testing.T) {
t.Parallel()
opts := getDefaultOptions()
assert.Empty(t, opts.WithReader)
assert.Empty(t, opts.WithWriter)
r, w := &fakeReader{}, &fakeWriter{}
opts, err := GetOpts(WithReaderWriter(r, w))
require.NoError(t, err)
assert.Equal(t, r, opts.WithReader)
assert.Equal(t, w, opts.WithWriter)
})
t.Run("nil reader", func(t *testing.T) {
t.Parallel()
w := &fakeWriter{}
_, err := GetOpts(WithReaderWriter(nil, w))
require.Error(t, err)
})
t.Run("nil interface reader", func(t *testing.T) {
t.Parallel()
w := &fakeWriter{}
_, err := GetOpts(WithReaderWriter((*fakeReader)(nil), w))
require.Error(t, err)
})
t.Run("nil writer", func(t *testing.T) {
t.Parallel()
r := &fakeReader{}
_, err := GetOpts(WithReaderWriter(r, nil))
require.Error(t, err)
})
t.Run("nil interface writer", func(t *testing.T) {
t.Parallel()
r := &fakeReader{}
_, err := GetOpts(WithReaderWriter(r, (*fakeWriter)(nil)))
require.Error(t, err)
})
})
}
56 changes: 56 additions & 0 deletions internal/host/service_catalog.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1

package host

import (
"context"
"time"

"github.com/hashicorp/boundary/internal/db"
"github.com/hashicorp/boundary/internal/errors"
"github.com/hashicorp/boundary/internal/plugin"
"github.com/hashicorp/boundary/internal/util"
)

// PluginCatalogService defines the interface expected
// to gather information about plugin host catalogs.
type PluginCatalogService interface {
EstimatedCatalogCount(context.Context) (int, error)
ListDeletedCatalogIds(context.Context, time.Time, ...Option) ([]string, error)
ListCatalogs(context.Context, []string, ...Option) ([]Catalog, []*plugin.Plugin, error)
}

// StaticCatalogService defines the interface expected
// to gather information about static host catalogs.
type StaticCatalogService interface {
EstimatedCatalogCount(context.Context) (int, error)
ListDeletedCatalogIds(context.Context, time.Time, ...Option) ([]string, error)
ListCatalogs(context.Context, []string, ...Option) ([]Catalog, error)
}

// CatalogService coordinates calls across different subtype repositories
// to gather information about all host catalogs.
type CatalogService struct {
pluginService PluginCatalogService
staticService StaticCatalogService
writer db.Writer
}

// NewCatalogService returns a new host catalog service.
func NewCatalogService(ctx context.Context, writer db.Writer, pluginService PluginCatalogService, staticService StaticCatalogService) (*CatalogService, error) {
const op = "host.NewCatalogService"
switch {
case util.IsNil(writer):
return nil, errors.New(ctx, errors.InvalidParameter, op, "missing DB writer")
case util.IsNil(pluginService):
return nil, errors.New(ctx, errors.InvalidParameter, op, "missing plugin service")
case util.IsNil(staticService):
return nil, errors.New(ctx, errors.InvalidParameter, op, "missing static service")
}
return &CatalogService{
staticService: staticService,
pluginService: pluginService,
writer: writer,
}, nil
}
79 changes: 79 additions & 0 deletions internal/host/service_list_catalogs.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1

package host

import (
"context"
"slices"

"github.com/hashicorp/boundary/internal/errors"
"github.com/hashicorp/boundary/internal/pagination"
"github.com/hashicorp/boundary/internal/plugin"
)

// List lists host catalogs according to the page size,
// filtering out entries that do not pass the filter item fn.
// It returns a new refresh token based on the grants hash and the returned catalogs.
func (s *CatalogService) List(
ctx context.Context,
grantsHash []byte,
pageSize int,
filterItemFn pagination.ListPluginsFilterFunc[Catalog],
projectIds []string,
) (*pagination.ListResponse[Catalog], []*plugin.Plugin, error) {
const op = "host.(*CatalogService).List"

if len(grantsHash) == 0 {
return nil, nil, errors.New(ctx, errors.InvalidParameter, op, "missing grants hash")
}
if pageSize < 1 {
return nil, nil, errors.New(ctx, errors.InvalidParameter, op, "page size must be at least 1")
}
if filterItemFn == nil {
return nil, nil, errors.New(ctx, errors.InvalidParameter, op, "missing filter item callback")
}
if len(projectIds) == 0 {
return nil, nil, errors.New(ctx, errors.InvalidParameter, op, "missing project ids")
}

listItemsFn := func(ctx context.Context, lastPageItem Catalog, limit int) ([]Catalog, []*plugin.Plugin, error) {
opts := []Option{
WithLimit(limit),
}
if lastPageItem != nil {
opts = append(opts, WithStartPageAfterItem(lastPageItem))
}
// Request another page from the DB until we fill the final items
pluginPage, plgs, err := s.pluginService.ListCatalogs(ctx, projectIds, opts...)
if err != nil {
return nil, nil, err
}
staticPage, err := s.staticService.ListCatalogs(ctx, projectIds, opts...)
if err != nil {
return nil, nil, err
}
page := append(pluginPage, staticPage...)
slices.SortFunc(page, func(i, j Catalog) int {
return i.GetUpdateTime().AsTime().Compare(j.GetUpdateTime().AsTime())
})
// Truncate slice to at most limit number of elements
if len(page) > limit {
page = page[:limit]
}
return page, plgs, nil
}
estimatedCountFn := func(ctx context.Context) (int, error) {
pluginCount, err := s.pluginService.EstimatedCatalogCount(ctx)
if err != nil {
return 0, err
}
staticCount, err := s.staticService.EstimatedCatalogCount(ctx)
if err != nil {
return 0, err
}
return pluginCount + staticCount, nil
}

return pagination.ListPlugins(ctx, grantsHash, pageSize, filterItemFn, listItemsFn, estimatedCountFn)
}
Loading

0 comments on commit 6bdba65

Please sign in to comment.