Allow forced refresh attempt when searching (#4077)

* Allow forced refresh attempt when searching
This commit is contained in:
Todd authored and Johan Brandhorst-Satzkorn committed 2024-01-05 07:43:20 -08:00
1 parent cdc6fb0973
commit c74579cead
6 files changed
+163 -16

No files matched your search

+20 -8
View File
@@ -38,8 +38,9 @@ var (
type SearchCommand struct {
*base.Command
flagQuery string
flagResource string
flagQuery string
flagResource string
flagForceRefresh bool
}
func (c *SearchCommand) Synopsis() string {
@@ -75,6 +76,12 @@ func (c *SearchCommand) Flags() *base.FlagSets {
Usage: `Specifies the resource type to search over`,
Completion: complete.PredictSet(supportedResourceTypes...),
})
f.BoolVar(&base.BoolVar{
Name: "force-refresh",
Target: &c.flagForceRefresh,
Usage: `Forces a refresh to be attempted prior to performing the search`,
Hidden: true,
})
return set
}
@@ -148,9 +155,10 @@ func (c *SearchCommand) Search(ctx context.Context) (*api.Response, *daemon.Sear
}
tf := filterBy{
flagQuery: c.flagQuery,
resource: c.flagResource,
authTokenId: strings.Join(tSlice[:2], "_"),
flagQuery: c.flagQuery,
resource: c.flagResource,
authTokenId: strings.Join(tSlice[:2], "_"),
forceRefresh: c.flagForceRefresh,
}
var opts []client.Option
if c.FlagOutputCurlString {
@@ -182,6 +190,9 @@ func search(ctx context.Context, daemonPath string, fb filterBy, opt ...client.O
q.Add("auth_token_id", fb.authTokenId)
q.Add("resource", fb.resource)
q.Add("query", fb.flagQuery)
if fb.forceRefresh {
q.Add("force_refresh", "true")
}
resp, err := c.Get(ctx, "/v1/search", q, opt...)
if err != nil {
return nil, nil, nil, fmt.Errorf("Error when sending request to the daemon: %w.", err)
@@ -329,7 +340,8 @@ func printSessionListTable(items []*sessions.Session) string {
}
type filterBy struct {
flagQuery string
authTokenId string
resource string
flagQuery string
authTokenId string
resource string
forceRefresh bool
}
+10
View File
@@ -14,6 +14,7 @@ type options struct {
withUserId string
withTargetRetrievalFunc TargetRetrievalFunc
withSessionRetrievalFunc SessionRetrievalFunc
withIgnoreSearchStaleness bool
}
// Option - how options are passed as args
@@ -75,3 +76,12 @@ func WithSessionRetrievalFunc(fn SessionRetrievalFunc) Option {
return nil
}
}
// WithIgnoreSearchStaleness provides an option for ignoring the resource
// staleness when performing a search.
func WithIgnoreSearchStaleness(b bool) Option {
return func(o *options) error {
o.withIgnoreSearchStaleness = b
return nil
}
}
+7
View File
@@ -72,4 +72,11 @@ func Test_GetOpts(t *testing.T) {
testOpts := getDefaultOptions()
assert.Equal(t, opts, testOpts)
})
t.Run("withIgnoreSearchStaleness", func(t *testing.T) {
opts, err := getOpts(WithIgnoreSearchStaleness(true))
require.NoError(t, err)
testOpts := getDefaultOptions()
testOpts.withIgnoreSearchStaleness = true
assert.Equal(t, opts, testOpts)
})
}
+7 -2
View File
@@ -200,13 +200,18 @@ func (r *RefreshService) RefreshForSearch(ctx context.Context, authTokenid strin
return errors.Wrap(ctx, err, op)
}
opts, err := getOpts(opt...)
if err != nil {
return errors.Wrap(ctx, err, op)
}
switch resourceType {
case Targets:
rtv, err := r.repo.lookupRefreshToken(ctx, u, targetResourceType)
if err != nil {
return errors.Wrap(ctx, err, op)
}
if rtv != nil && time.Since(rtv.UpdateTime) > r.maxSearchStaleness {
if opts.withIgnoreSearchStaleness || rtv != nil && time.Since(rtv.UpdateTime) > r.maxSearchStaleness {
if err := r.repo.refreshTargets(ctx, u, tokens, opt...); err != nil {
return errors.Wrap(ctx, err, op)
}
@@ -216,7 +221,7 @@ func (r *RefreshService) RefreshForSearch(ctx context.Context, authTokenid strin
if err != nil {
return errors.Wrap(ctx, err, op)
}
if rtv != nil && time.Since(rtv.UpdateTime) > r.maxSearchStaleness {
if opts.withIgnoreSearchStaleness || rtv != nil && time.Since(rtv.UpdateTime) > r.maxSearchStaleness {
if err := r.repo.refreshSessions(ctx, u, tokens, opt...); err != nil {
return errors.Wrap(ctx, err, op)
}
+109
View File
@@ -400,6 +400,61 @@ func TestRefreshForSearch(t *testing.T) {
assert.ElementsMatch(t, retTargets[2:], cachedTargets)
})
t.Run("targets forced refreshed for searching", func(t *testing.T) {
s, err := db.Open(ctx)
require.NoError(t, err)
r, err := NewRepository(ctx, s, &sync.Map{}, mapBasedAuthTokenKeyringLookup(atMap), sliceBasedAuthTokenBoundaryReader(boundaryAuthTokens))
require.NoError(t, err)
// Everything can stay stale for an hour
rs, err := NewRefreshService(ctx, r, time.Hour, 0)
require.NoError(t, err)
require.NoError(t, r.AddKeyringToken(ctx, boundaryAddr, KeyringToken{KeyringType: "k", TokenName: "t", AuthTokenId: at.Id}))
retTargets := []*targets.Target{
target("1"),
target("2"),
target("3"),
target("4"),
}
opts := []Option{
WithSessionRetrievalFunc(testStaticResourceRetrievalFunc[*sessions.Session](t, nil, nil)),
WithTargetRetrievalFunc(testStaticResourceRetrievalFunc[*targets.Target](t,
[][]*targets.Target{
retTargets[:3],
retTargets[3:],
},
[][]string{
nil,
{retTargets[0].Id, retTargets[1].Id},
},
)),
}
assert.NoError(t, rs.RefreshForSearch(ctx, at.Id, Targets, opts...))
cachedTargets, err := r.ListTargets(ctx, at.Id)
assert.NoError(t, err)
assert.Empty(t, cachedTargets)
// Now load up a few resources and a token, and trying again should
// see the RefreshForSearch update more fields.
assert.NoError(t, rs.Refresh(ctx, opts...))
cachedTargets, err = r.ListTargets(ctx, at.Id)
assert.NoError(t, err)
assert.ElementsMatch(t, retTargets[:3], cachedTargets)
// No refresh happened because it is not considered stale
assert.NoError(t, rs.RefreshForSearch(ctx, at.Id, Targets, opts...))
cachedTargets, err = r.ListTargets(ctx, at.Id)
assert.NoError(t, err)
assert.ElementsMatch(t, retTargets[:3], cachedTargets)
// Now force refresh
assert.NoError(t, rs.RefreshForSearch(ctx, at.Id, Targets, append(opts, WithIgnoreSearchStaleness(true))...))
cachedTargets, err = r.ListTargets(ctx, at.Id)
assert.NoError(t, err)
assert.ElementsMatch(t, retTargets[2:], cachedTargets)
})
t.Run("no refresh token no refresh for search", func(t *testing.T) {
s, err := db.Open(ctx)
require.NoError(t, err)
@@ -492,6 +547,60 @@ func TestRefreshForSearch(t *testing.T) {
assert.NoError(t, err)
assert.ElementsMatch(t, retSess[2:], cachedSessions)
})
t.Run("sessions forced refreshed for searching", func(t *testing.T) {
s, err := db.Open(ctx)
require.NoError(t, err)
r, err := NewRepository(ctx, s, &sync.Map{}, mapBasedAuthTokenKeyringLookup(atMap), sliceBasedAuthTokenBoundaryReader(boundaryAuthTokens))
require.NoError(t, err)
// Everything stays fresh for 1 hour
rs, err := NewRefreshService(ctx, r, time.Hour, 0)
require.NoError(t, err)
require.NoError(t, r.AddKeyringToken(ctx, boundaryAddr, KeyringToken{KeyringType: "k", TokenName: "t", AuthTokenId: at.Id}))
retSess := []*sessions.Session{
session("1"),
session("2"),
session("3"),
session("4"),
}
opts := []Option{
WithTargetRetrievalFunc(testStaticResourceRetrievalFunc[*targets.Target](t, nil, nil)),
WithSessionRetrievalFunc(testStaticResourceRetrievalFunc[*sessions.Session](t,
[][]*sessions.Session{
retSess[:3],
retSess[3:],
},
[][]string{
nil,
{retSess[0].Id, retSess[1].Id},
},
)),
}
// First call doesn't sync anything because no sessions were already synced yet
assert.NoError(t, rs.RefreshForSearch(ctx, at.Id, Sessions, opts...))
cachedSessions, err := r.ListSessions(ctx, at.Id)
assert.NoError(t, err)
assert.Empty(t, cachedSessions)
assert.NoError(t, rs.Refresh(ctx, opts...))
cachedSessions, err = r.ListSessions(ctx, at.Id)
assert.NoError(t, err)
assert.ElementsMatch(t, retSess[:3], cachedSessions)
// Refresh for search doesn't refresh anything because it isn't stale
assert.NoError(t, rs.RefreshForSearch(ctx, at.Id, Sessions, opts...))
cachedSessions, err = r.ListSessions(ctx, at.Id)
assert.NoError(t, err)
assert.ElementsMatch(t, retSess[:3], cachedSessions)
// Now force the refresh and see things get updated
assert.NoError(t, rs.RefreshForSearch(ctx, at.Id, Sessions, append(opts, WithIgnoreSearchStaleness(true))...))
cachedSessions, err = r.ListSessions(ctx, at.Id)
assert.NoError(t, err)
assert.ElementsMatch(t, retSess[2:], cachedSessions)
})
}
func TestRefresh(t *testing.T) {
@@ -8,6 +8,7 @@ import (
"encoding/json"
"fmt"
"net/http"
"strconv"
"github.com/hashicorp/boundary/api/sessions"
"github.com/hashicorp/boundary/api/targets"
@@ -24,11 +25,10 @@ type SearchResult struct {
}
const (
filterKey = "filter"
queryKey = "query"
resourceKey = "resource"
boundaryAddrKey = "boundary_addr"
filterKey = "filter"
queryKey = "query"
resourceKey = "resource"
forceRefreshKey = "force_refresh"
authTokenIdKey = "auth_token_id"
)
@@ -70,10 +70,14 @@ func newSearchHandlerFunc(ctx context.Context, repo *cache.Repository, refreshSe
return
}
var opts []cache.Option
if b, err := strconv.ParseBool(r.URL.Query().Get(forceRefreshKey)); err == nil && b {
opts = append(opts, cache.WithIgnoreSearchStaleness(true))
}
// Refresh the resources for the provided user, if possible. This is best
// effort, so if there is any problem refreshing, we just log the error
// and move on to handling the search request.
if err := refreshService.RefreshForSearch(ctx, authTokenId, searchableResource); err != nil {
if err := refreshService.RefreshForSearch(ctx, authTokenId, searchableResource, opts...); err != nil {
// we don't stop the search, we just log that the inline refresh failed
event.WriteError(ctx, op, err, event.WithInfoMsg("when refreshing the resources inline for search", "auth_token_id", authTokenId, "resource", searchableResource))
}