mirror of https://github.com/hashicorp/boundary
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
318 lines
13 KiB
318 lines
13 KiB
// Copyright (c) HashiCorp, Inc.
|
|
// SPDX-License-Identifier: BUSL-1.1
|
|
|
|
package plugin
|
|
|
|
import (
|
|
"context"
|
|
"time"
|
|
|
|
"github.com/hashicorp/boundary/internal/boundary"
|
|
"github.com/hashicorp/boundary/internal/errors"
|
|
"github.com/hashicorp/boundary/internal/listtoken"
|
|
"github.com/hashicorp/boundary/internal/pagination"
|
|
"github.com/hashicorp/boundary/internal/plugin"
|
|
)
|
|
|
|
// ListPluginsFilterFunc is a callback used to filter out resources that don't match
|
|
// some criteria. The function must return true for items that should be included in the final
|
|
// result. Returning an error results in an error being returned from the pagination.
|
|
type ListPluginsFilterFunc[T boundary.Resource] func(ctx context.Context, item T, plugin map[string]*plugin.Plugin) (bool, error)
|
|
|
|
// ListPluginsItemsFunc returns a slice of T that are ordered after prevPageLastItem according to
|
|
// the implementation of the function. If prevPageLastItem is empty, it should return
|
|
// a slice of T from the start, as defined by the function. It also returns the timestamp
|
|
// of the DB transaction used to list the items.
|
|
type ListPluginsItemsFunc[T boundary.Resource] func(ctx context.Context, prevPageLastItem T, limit int) ([]T, []*plugin.Plugin, time.Time, error)
|
|
|
|
// ListPlugins returns a ListResponse and a map of plugin id to the plugins associated
|
|
// with the returned resources. The map may contain a superset of the plugins associated with
|
|
// the plugins. The response will contain at most pageSize number of items.
|
|
// Items are fetched using the listItemsFn and checked using
|
|
// the filterItemFn to determine if they should be included in the response.
|
|
// The response includes a new list token used to continue pagination or refresh.
|
|
// The estimatedCountFn is used to provide an estimated total number of
|
|
// items that can be returned by making additional requests using the returned
|
|
// list token.
|
|
func ListPlugins[T boundary.Resource](
|
|
ctx context.Context,
|
|
grantsHash []byte,
|
|
pageSize int,
|
|
filterItemFn ListPluginsFilterFunc[T],
|
|
listItemsFn ListPluginsItemsFunc[T],
|
|
estimatedCountFn pagination.EstimatedCountFunc,
|
|
) (*pagination.ListResponse[T], map[string]*plugin.Plugin, error) {
|
|
const op = "pagination.ListsPlugin"
|
|
|
|
switch {
|
|
case len(grantsHash) == 0:
|
|
return nil, nil, errors.New(ctx, errors.InvalidParameter, op, "missing grants hash")
|
|
case pageSize < 1:
|
|
return nil, nil, errors.New(ctx, errors.InvalidParameter, op, "page size must be at least 1")
|
|
case filterItemFn == nil:
|
|
return nil, nil, errors.New(ctx, errors.InvalidParameter, op, "missing filter item callback")
|
|
case listItemsFn == nil:
|
|
return nil, nil, errors.New(ctx, errors.InvalidParameter, op, "missing list items callback")
|
|
case estimatedCountFn == nil:
|
|
return nil, nil, errors.New(ctx, errors.InvalidParameter, op, "missing estimated count callback")
|
|
}
|
|
|
|
items, plgs, completeListing, listTime, err := listPlugins(ctx, pageSize, filterItemFn, listItemsFn)
|
|
if err != nil {
|
|
return nil, nil, errors.Wrap(ctx, err, op)
|
|
}
|
|
|
|
resp, err := buildListResp(ctx, grantsHash, items, completeListing, listTime, estimatedCountFn)
|
|
if err != nil {
|
|
return nil, nil, errors.Wrap(ctx, err, op)
|
|
}
|
|
|
|
return resp, plgs, nil
|
|
}
|
|
|
|
// ListPluginsPage returns a ListResponse and a map of plugin id to the plugins associated
|
|
// with the returned resources. The map may contain a superset of the plugins associated with
|
|
// the plugins. The response will contain at most pageSize number of items.
|
|
// Items are fetched using the listItemsFn and checked using
|
|
// the filterItemFn to determine if they should be included in the response.
|
|
// Items will be fetched based on the contents of the list token. The list
|
|
// token must contain a PaginationToken component.
|
|
// The response includes a new list token used to continue pagination or refresh.
|
|
// The estimatedCountFn is used to provide an estimated total number of
|
|
// items that can be returned by making additional requests using the returned
|
|
// list token.
|
|
func ListPluginsPage[T boundary.Resource](
|
|
ctx context.Context,
|
|
grantsHash []byte,
|
|
pageSize int,
|
|
filterItemFn ListPluginsFilterFunc[T],
|
|
listItemsFn ListPluginsItemsFunc[T],
|
|
estimatedCountFn pagination.EstimatedCountFunc,
|
|
tok *listtoken.Token,
|
|
) (*pagination.ListResponse[T], map[string]*plugin.Plugin, error) {
|
|
const op = "pagination.ListPluginsPage"
|
|
|
|
switch {
|
|
case len(grantsHash) == 0:
|
|
return nil, nil, errors.New(ctx, errors.InvalidParameter, op, "missing grants hash")
|
|
case pageSize < 1:
|
|
return nil, nil, errors.New(ctx, errors.InvalidParameter, op, "page size must be at least 1")
|
|
case filterItemFn == nil:
|
|
return nil, nil, errors.New(ctx, errors.InvalidParameter, op, "missing filter item callback")
|
|
case listItemsFn == nil:
|
|
return nil, nil, errors.New(ctx, errors.InvalidParameter, op, "missing list items callback")
|
|
case estimatedCountFn == nil:
|
|
return nil, nil, errors.New(ctx, errors.InvalidParameter, op, "missing estimated count callback")
|
|
case tok == nil:
|
|
return nil, nil, errors.New(ctx, errors.InvalidParameter, op, "missing list token")
|
|
}
|
|
if _, ok := tok.Subtype.(*listtoken.PaginationToken); !ok {
|
|
return nil, nil, errors.New(ctx, errors.InvalidParameter, op, "token did not have a pagination token component")
|
|
}
|
|
|
|
items, plgs, completeListing, listTime, err := listPlugins(ctx, pageSize, filterItemFn, listItemsFn)
|
|
if err != nil {
|
|
return nil, nil, errors.Wrap(ctx, err, op)
|
|
}
|
|
|
|
resp, err := buildListPageResp(ctx, completeListing, nil, time.Time{} /* no deleted ids time */, items, listTime, tok, estimatedCountFn)
|
|
if err != nil {
|
|
return nil, nil, errors.Wrap(ctx, err, op)
|
|
}
|
|
|
|
return resp, plgs, nil
|
|
}
|
|
|
|
// ListPluginsRefresh returns a ListResponse and a map of plugin id to the plugins associated
|
|
// with the returned resources. The map may contain a superset of the plugins associated with
|
|
// the plugins. The response will contain at most pageSize number of items.
|
|
// Items are fetched using the listItemsFn and checked using
|
|
// the filterItemFn to determine if they should be included in the response.
|
|
// Items will be fetched based on the contents of the list token. The list
|
|
// token must contain a StartRefreshToken component.
|
|
// The response includes a new list token used to continue pagination or refresh.
|
|
// The estimatedCountFn is used to provide an estimated total number of
|
|
// items that can be returned by making additional requests using the returned
|
|
// list token. The listDeletedIDsFn is used to list the IDs of any
|
|
// resources that have been deleted since the list token was last used.
|
|
func ListPluginsRefresh[T boundary.Resource](
|
|
ctx context.Context,
|
|
grantsHash []byte,
|
|
pageSize int,
|
|
filterItemFn ListPluginsFilterFunc[T],
|
|
listItemsFn ListPluginsItemsFunc[T],
|
|
estimatedCountFn pagination.EstimatedCountFunc,
|
|
listDeletedIDsFn pagination.ListDeletedIDsFunc,
|
|
tok *listtoken.Token,
|
|
) (*pagination.ListResponse[T], map[string]*plugin.Plugin, error) {
|
|
const op = "pagination.ListPluginsRefresh"
|
|
|
|
switch {
|
|
case len(grantsHash) == 0:
|
|
return nil, nil, errors.New(ctx, errors.InvalidParameter, op, "missing grants hash")
|
|
case pageSize < 1:
|
|
return nil, nil, errors.New(ctx, errors.InvalidParameter, op, "page size must be at least 1")
|
|
case filterItemFn == nil:
|
|
return nil, nil, errors.New(ctx, errors.InvalidParameter, op, "missing filter item callback")
|
|
case listItemsFn == nil:
|
|
return nil, nil, errors.New(ctx, errors.InvalidParameter, op, "missing list items callback")
|
|
case estimatedCountFn == nil:
|
|
return nil, nil, errors.New(ctx, errors.InvalidParameter, op, "missing estimated count callback")
|
|
case listDeletedIDsFn == nil:
|
|
return nil, nil, errors.New(ctx, errors.InvalidParameter, op, "missing list deleted IDs callback")
|
|
case tok == nil:
|
|
return nil, nil, errors.New(ctx, errors.InvalidParameter, op, "missing list token")
|
|
}
|
|
srt, ok := tok.Subtype.(*listtoken.StartRefreshToken)
|
|
if !ok {
|
|
return nil, nil, errors.New(ctx, errors.InvalidParameter, op, "token did not have a start-refresh token component")
|
|
}
|
|
|
|
deletedIds, deletedIdsTime, err := listDeletedIDsFn(ctx, srt.PreviousDeletedIdsTime)
|
|
if err != nil {
|
|
return nil, nil, errors.Wrap(ctx, err, op)
|
|
}
|
|
|
|
items, plgs, completeListing, listTime, err := listPlugins(ctx, pageSize, filterItemFn, listItemsFn)
|
|
if err != nil {
|
|
return nil, nil, errors.Wrap(ctx, err, op)
|
|
}
|
|
|
|
resp, err := buildListPageResp(ctx, completeListing, deletedIds, deletedIdsTime, items, listTime, tok, estimatedCountFn)
|
|
if err != nil {
|
|
return nil, nil, errors.Wrap(ctx, err, op)
|
|
}
|
|
|
|
return resp, plgs, nil
|
|
}
|
|
|
|
// ListPluginsRefreshPage returns a ListResponse and a map of plugin id to the plugins associated
|
|
// with the returned resources. The map may contain a superset of the plugins associated with
|
|
// the plugins. The response will contain at most pageSize number of items.
|
|
// Items are fetched using the listItemsFn and checked using
|
|
// the filterItemFn to determine if they should be included in the response.
|
|
// Items will be fetched based on the contents of the list token. The list
|
|
// token must contain a RefreshToken component.
|
|
// The response includes a new list token used to continue pagination or refresh.
|
|
// The estimatedCountFn is used to provide an estimated total number of
|
|
// items that can be returned by making additional requests using the returned
|
|
// list token. The listDeletedIDsFn is used to list the IDs of any
|
|
// resources that have been deleted since the list token was last used.
|
|
func ListPluginsRefreshPage[T boundary.Resource](
|
|
ctx context.Context,
|
|
grantsHash []byte,
|
|
pageSize int,
|
|
filterItemFn ListPluginsFilterFunc[T],
|
|
listItemsFn ListPluginsItemsFunc[T],
|
|
estimatedCountFn pagination.EstimatedCountFunc,
|
|
listDeletedIDsFn pagination.ListDeletedIDsFunc,
|
|
tok *listtoken.Token,
|
|
) (*pagination.ListResponse[T], map[string]*plugin.Plugin, error) {
|
|
const op = "pagination.ListPluginsRefreshPage"
|
|
|
|
switch {
|
|
case len(grantsHash) == 0:
|
|
return nil, nil, errors.New(ctx, errors.InvalidParameter, op, "missing grants hash")
|
|
case pageSize < 1:
|
|
return nil, nil, errors.New(ctx, errors.InvalidParameter, op, "page size must be at least 1")
|
|
case filterItemFn == nil:
|
|
return nil, nil, errors.New(ctx, errors.InvalidParameter, op, "missing filter item callback")
|
|
case listItemsFn == nil:
|
|
return nil, nil, errors.New(ctx, errors.InvalidParameter, op, "missing list items callback")
|
|
case estimatedCountFn == nil:
|
|
return nil, nil, errors.New(ctx, errors.InvalidParameter, op, "missing estimated count callback")
|
|
case listDeletedIDsFn == nil:
|
|
return nil, nil, errors.New(ctx, errors.InvalidParameter, op, "missing list deleted IDs callback")
|
|
case tok == nil:
|
|
return nil, nil, errors.New(ctx, errors.InvalidParameter, op, "missing list token")
|
|
}
|
|
rt, ok := tok.Subtype.(*listtoken.RefreshToken)
|
|
if !ok {
|
|
return nil, nil, errors.New(ctx, errors.InvalidParameter, op, "token did not have a refresh token component")
|
|
}
|
|
|
|
deletedIds, deletedIdsTime, err := listDeletedIDsFn(ctx, rt.PreviousDeletedIdsTime)
|
|
if err != nil {
|
|
return nil, nil, errors.Wrap(ctx, err, op)
|
|
}
|
|
|
|
items, plgs, completeListing, listTime, err := listPlugins(ctx, pageSize, filterItemFn, listItemsFn)
|
|
if err != nil {
|
|
return nil, nil, errors.Wrap(ctx, err, op)
|
|
}
|
|
|
|
resp, err := buildListPageResp(ctx, completeListing, deletedIds, deletedIdsTime, items, listTime, tok, estimatedCountFn)
|
|
if err != nil {
|
|
return nil, nil, errors.Wrap(ctx, err, op)
|
|
}
|
|
|
|
return resp, plgs, nil
|
|
}
|
|
|
|
func listPlugins[T boundary.Resource](
|
|
ctx context.Context,
|
|
pageSize int,
|
|
filterItemFn ListPluginsFilterFunc[T],
|
|
listItemsFn ListPluginsItemsFunc[T],
|
|
) ([]T, map[string]*plugin.Plugin, bool, time.Time, error) {
|
|
const op = "pagination.list"
|
|
|
|
var lastItem T
|
|
plgs := map[string]*plugin.Plugin{}
|
|
var firstListTime time.Time
|
|
limit := pageSize + 1
|
|
items := make([]T, 0, limit)
|
|
dbLoop:
|
|
for {
|
|
// Request another page from the DB until we fill the final items
|
|
page, newPlgs, listTime, err := listItemsFn(ctx, lastItem, limit)
|
|
if err != nil {
|
|
return nil, nil, false, time.Time{}, errors.Wrap(ctx, err, op)
|
|
}
|
|
// Assign the firstListTime once, to ensure we always store the start of listing,
|
|
// rather the timestamp of the last listing.
|
|
if firstListTime.IsZero() {
|
|
firstListTime = listTime
|
|
}
|
|
for _, plg := range newPlgs {
|
|
if _, ok := plgs[plg.PublicId]; !ok {
|
|
plgs[plg.PublicId] = plg
|
|
}
|
|
}
|
|
for _, item := range page {
|
|
ok, err := filterItemFn(ctx, item, plgs)
|
|
if err != nil {
|
|
return nil, nil, false, time.Time{}, errors.Wrap(ctx, err, op)
|
|
}
|
|
if ok {
|
|
items = append(items, item)
|
|
// If we filled the items after filtering,
|
|
// we're done.
|
|
if len(items) == cap(items) {
|
|
break dbLoop
|
|
}
|
|
}
|
|
}
|
|
// If the current page was shorter than the limit, stop iterating
|
|
if len(page) < limit {
|
|
break dbLoop
|
|
}
|
|
|
|
lastItem = page[len(page)-1]
|
|
}
|
|
// If we couldn't fill the items, it was a complete listing.
|
|
completeListing := len(items) < cap(items)
|
|
if !completeListing {
|
|
// Items is of size pageSize+1, so
|
|
// truncate if it was filled.
|
|
items = items[:pageSize]
|
|
}
|
|
|
|
// Note that plgs can contain a superset of the plugins associated with the items,
|
|
// since we get plugins from the listFn based on a limit of pageSize+1. If the last
|
|
// item that we request is removed, there is no way to tell if a plugin was associated
|
|
// with that item only. This is fine in practice, as callers should never loop over
|
|
// the map, they should look up plugins by ID from the resource used.
|
|
return items, plgs, completeListing, firstListTime, nil
|
|
}
|