Skip to content

Commit

Permalink
Add sessions to the client side cache (#3682)
Browse files Browse the repository at this point in the history
* Add sessions to repository

* Search command supports sessions

* adding session related tests to db and refresh
  • Loading branch information
talanknight committed Sep 26, 2023
1 parent d85fa8d commit 2509498
Show file tree
Hide file tree
Showing 15 changed files with 1,845 additions and 629 deletions.
82 changes: 61 additions & 21 deletions internal/cmd/commands/daemon/search_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,9 +58,6 @@ func newSearchTargetsHandlerFunc(ctx context.Context, repo *cache.Repository) (h
case resource == "":
writeError(w, "resource is a required field but was empty", http.StatusBadRequest)
return
case resource != "targets":
writeError(w, fmt.Sprintf("search doesn't support %q resource", resource), http.StatusBadRequest)
return
case tokenName == "":
writeError(w, fmt.Sprintf("%s is a required field but was empty", tokenNameKey), http.StatusBadRequest)
return
Expand All @@ -71,7 +68,7 @@ func newSearchTargetsHandlerFunc(ctx context.Context, repo *cache.Repository) (h
writeError(w, fmt.Sprintf("%s is a required field but was empty", boundaryAddrKey), http.StatusBadRequest)
return
case authTokenId == "":
writeError(w, fmt.Sprintf("%s is a required field but was empty", authTokenId), http.StatusBadRequest)
writeError(w, fmt.Sprintf("%s is a required field but was empty", authTokenIdKey), http.StatusBadRequest)
return
}

Expand All @@ -83,12 +80,16 @@ func newSearchTargetsHandlerFunc(ctx context.Context, repo *cache.Repository) (h
}

query := r.URL.Query().Get(queryKey)
var found []*targets.Target
switch query {
case "":
found, err = repo.ListTargets(r.Context(), p)

var res *SearchResult
switch resource {
case "targets":
res, err = searchTargets(r.Context(), repo, p, query, filter)
case "sessions":
res, err = searchSessions(r.Context(), repo, p, query, filter)
default:
found, err = repo.QueryTargets(r.Context(), p, query)
writeError(w, fmt.Sprintf("search doesn't support %q resource", resource), http.StatusBadRequest)
return
}

if err != nil {
Expand All @@ -98,26 +99,65 @@ func newSearchTargetsHandlerFunc(ctx context.Context, repo *cache.Repository) (h
default:
writeError(w, err.Error(), http.StatusInternalServerError)
}
return
}

w.Header().Set("Content-Type", "application/json")

finalItems := make([]*targets.Target, 0, len(found))
for _, item := range found {
if filter.Match(item) {
finalItems = append(finalItems, item)
}
if res == nil {
writeError(w, "nil SearchResult generated", http.StatusInternalServerError)
}

res := SearchResult{
Targets: finalItems,
}
j, err := json.Marshal(res)
if err != nil {
writeError(w, err.Error(), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
w.Write(j)
}, nil
}

func searchTargets(ctx context.Context, repo *cache.Repository, p *cache.Persona, query string, filter *handlers.Filter) (*SearchResult, error) {
var found []*targets.Target
var err error
switch query {
case "":
found, err = repo.ListTargets(ctx, p)
default:
found, err = repo.QueryTargets(ctx, p, query)
}
if err != nil {
return nil, err
}

finalTars := make([]*targets.Target, 0, len(found))
for _, item := range found {
if filter.Match(item) {
finalTars = append(finalTars, item)
}
}
return &SearchResult{
Targets: finalTars,
}, nil
}

func searchSessions(ctx context.Context, repo *cache.Repository, p *cache.Persona, query string, filter *handlers.Filter) (*SearchResult, error) {
var found []*sessions.Session
var err error
switch query {
case "":
found, err = repo.ListSessions(ctx, p)
default:
found, err = repo.QuerySessions(ctx, p, query)
}
if err != nil {
return nil, err
}

finalSess := make([]*sessions.Session, 0, len(found))
for _, item := range found {
if filter.Match(item) {
finalSess = append(finalSess, item)
}
}
return &SearchResult{
Sessions: finalSess,
}, nil
}
19 changes: 14 additions & 5 deletions internal/cmd/commands/daemon/testing.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"io"
"testing"

"github.com/hashicorp/boundary/api/sessions"
"github.com/hashicorp/boundary/api/targets"
"github.com/hashicorp/boundary/internal/daemon/cache"
"github.com/stretchr/testify/require"
Expand Down Expand Up @@ -65,19 +66,27 @@ func (s *TestServer) Serve(t *testing.T) error {
return s.cacheServer.serve(ctx, s.cmd, l)
}

// AddTargets adds targets to the cache for the provided address, token name, and keyring type.
// They token info must already be known to the server.
func (s *TestServer) AddTargets(t *testing.T, tarAddr string, tarToken string, tars []*targets.Target) {
// AddResources adds targets to the cache for the provided address, token name,
// and keyring type. They token info must already be known to the server.
func (s *TestServer) AddResources(t *testing.T, p *cache.Persona, tars []*targets.Target, sess []*sessions.Session) {
t.Helper()
ctx := context.Background()
r, err := cache.NewRepository(ctx, s.cacheServer.store, s.cmd.ReadTokenFromKeyring)
require.NoError(t, err)

tarFn := func(ctx context.Context, addr string, tok string) ([]*targets.Target, error) {
if addr != tarAddr || tok != tarToken {
at := s.cmd.ReadTokenFromKeyring(p.KeyringType, p.TokenName)
if addr != p.BoundaryAddr || tok != at.Token {
return nil, nil
}
return tars, nil
}
require.NoError(t, r.Refresh(ctx, cache.WithTargetRetrievalFunc(tarFn)))
sessFn := func(ctx context.Context, addr string, tok string) ([]*sessions.Session, error) {
at := s.cmd.ReadTokenFromKeyring(p.KeyringType, p.TokenName)
if addr != p.BoundaryAddr || tok != at.Token {
return nil, nil
}
return sess, nil
}
require.NoError(t, r.Refresh(ctx, cache.WithTargetRetrievalFunc(tarFn), cache.WithSessionRetrievalFunc(sessFn)))
}
80 changes: 79 additions & 1 deletion internal/cmd/commands/search/search.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,10 @@ import (
"net/url"
"os"
"strings"
"time"

"github.com/hashicorp/boundary/api"
"github.com/hashicorp/boundary/api/sessions"
"github.com/hashicorp/boundary/api/targets"
"github.com/hashicorp/boundary/internal/cmd/base"
"github.com/hashicorp/boundary/internal/cmd/commands/daemon"
Expand All @@ -28,6 +30,7 @@ var (

supportedResourceTypes = []string{
"targets",
"sessions",
}
)

Expand Down Expand Up @@ -125,7 +128,14 @@ func (c *SearchCommand) Run(args []string) int {
case "json":
c.UI.Output(string(resp.Body.Bytes()))
default:
c.UI.Output(printTargetListTable(res.Targets))
switch {
case len(res.Targets) > 0:
c.UI.Output(printTargetListTable(res.Targets))
case len(res.Sessions) > 0:
c.UI.Output(printSessionListTable(res.Sessions))
default:
c.UI.Output("No items found")
}
}
return base.CommandSuccess
}
Expand Down Expand Up @@ -259,6 +269,74 @@ func printTargetListTable(items []*targets.Target) string {
return base.WrapForHelpText(output)
}

func printSessionListTable(items []*sessions.Session) string {
if len(items) == 0 {
return "No sessions found"
}
var output []string
output = []string{
"",
"Session information:",
}
for i, item := range items {
if i > 0 {
output = append(output, "")
}
if item.Id != "" {
output = append(output,
fmt.Sprintf(" ID: %s", item.Id),
)
} else {
output = append(output,
fmt.Sprintf(" ID: %s", "(not available)"),
)
}
if item.ScopeId != "" {
output = append(output,
fmt.Sprintf(" Scope ID: %s", item.ScopeId),
)
}
if item.Status != "" {
output = append(output,
fmt.Sprintf(" Status: %s", item.Status),
)
}
if !item.CreatedTime.IsZero() {
output = append(output,
fmt.Sprintf(" Created Time: %s", item.CreatedTime.Local().Format(time.RFC1123)),
)
}
if !item.ExpirationTime.IsZero() {
output = append(output,
fmt.Sprintf(" Expiration Time: %s", item.ExpirationTime.Local().Format(time.RFC1123)),
)
}
if !item.UpdatedTime.IsZero() {
output = append(output,
fmt.Sprintf(" Updated Time: %s", item.UpdatedTime.Local().Format(time.RFC1123)),
)
}
if item.UserId != "" {
output = append(output,
fmt.Sprintf(" User ID: %s", item.UserId),
)
}
if item.TargetId != "" {
output = append(output,
fmt.Sprintf(" Target ID: %s", item.TargetId),
)
}
if len(item.AuthorizedActions) > 0 {
output = append(output,
" Authorized Actions:",
base.WrapSlice(6, item.AuthorizedActions),
)
}
}

return base.WrapForHelpText(output)
}

type filterBy struct {
flagQuery string
tokenName string
Expand Down
65 changes: 61 additions & 4 deletions internal/cmd/commands/search/search_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (

"github.com/hashicorp/boundary/api"
"github.com/hashicorp/boundary/api/authtokens"
"github.com/hashicorp/boundary/api/sessions"
"github.com/hashicorp/boundary/api/targets"
"github.com/hashicorp/boundary/internal/cmd/base"
"github.com/hashicorp/boundary/internal/cmd/commands/daemon"
Expand Down Expand Up @@ -208,12 +209,15 @@ func TestSearch(t *testing.T) {
assert.EqualValues(t, r, daemon.SearchResult{})
})

srv.AddTargets(t, cmd.p.BoundaryAddr, cmd.ReadTokenFromKeyring(cmd.keyring(), cmd.tokenName()).Token, []*targets.Target{
srv.AddResources(t, cmd.p, []*targets.Target{
{Id: "ttcp_1234567890", Name: "name1", Description: "description1"},
{Id: "ttcp_0987654321", Name: "name2", Description: "description2"},
}, []*sessions.Session{
{Id: "sess_1234567890", TargetId: "ttcp_1234567890", Status: "pending"},
{Id: "sess_0987654321", TargetId: "ttcp_0987654321", Status: "pending"},
})

t.Run("response from list", func(t *testing.T) {
t.Run("target response from list", func(t *testing.T) {
resp, err := search(ctx, srv.BaseSocketDir(), filterBy{
boundaryAddr: cmd.p.BoundaryAddr,
keyringType: cmd.keyring(),
Expand All @@ -229,7 +233,7 @@ func TestSearch(t *testing.T) {
assert.NotNil(t, r)
assert.Len(t, r.Targets, 2)
})
t.Run("full response from query", func(t *testing.T) {
t.Run("full target response from query", func(t *testing.T) {
resp, err := search(ctx, srv.BaseSocketDir(), filterBy{
boundaryAddr: cmd.p.BoundaryAddr,
keyringType: cmd.keyring(),
Expand All @@ -246,7 +250,7 @@ func TestSearch(t *testing.T) {
assert.NotNil(t, r)
assert.Len(t, r.Targets, 2)
})
t.Run("partial response from query", func(t *testing.T) {
t.Run("partial target response from query", func(t *testing.T) {
resp, err := search(ctx, srv.BaseSocketDir(), filterBy{
boundaryAddr: cmd.p.BoundaryAddr,
keyringType: cmd.keyring(),
Expand All @@ -261,6 +265,59 @@ func TestSearch(t *testing.T) {
assert.NoError(t, err)
assert.Nil(t, apiErr)
assert.NotNil(t, r)
assert.Len(t, r.Sessions, 0)
assert.Len(t, r.Targets, 1)
})

t.Run("session response from list", func(t *testing.T) {
resp, err := search(ctx, srv.BaseSocketDir(), filterBy{
boundaryAddr: cmd.p.BoundaryAddr,
keyringType: cmd.keyring(),
tokenName: cmd.tokenName(),
authTokenId: p.AuthTokenId,
resource: "sessions",
})
require.NoError(t, err)
r := daemon.SearchResult{}
apiErr, err := resp.Decode(&r)
assert.NoError(t, err)
assert.Nil(t, apiErr)
assert.NotNil(t, r)
assert.Len(t, r.Targets, 0)
assert.Len(t, r.Sessions, 2)
})
t.Run("full session response from query", func(t *testing.T) {
resp, err := search(ctx, srv.BaseSocketDir(), filterBy{
boundaryAddr: cmd.p.BoundaryAddr,
keyringType: cmd.keyring(),
tokenName: cmd.tokenName(),
authTokenId: p.AuthTokenId,
flagQuery: "id % sess",
resource: "sessions",
})
require.NoError(t, err)
r := daemon.SearchResult{}
apiErr, err := resp.Decode(&r)
assert.NoError(t, err)
assert.Nil(t, apiErr)
assert.NotNil(t, r)
assert.Len(t, r.Sessions, 2)
})
t.Run("partial session response from query", func(t *testing.T) {
resp, err := search(ctx, srv.BaseSocketDir(), filterBy{
boundaryAddr: cmd.p.BoundaryAddr,
keyringType: cmd.keyring(),
tokenName: cmd.tokenName(),
authTokenId: p.AuthTokenId,
flagQuery: "id % sess_1234567890",
resource: "sessions",
})
require.NoError(t, err)
r := daemon.SearchResult{}
apiErr, err := resp.Decode(&r)
assert.NoError(t, err)
assert.Nil(t, apiErr)
assert.NotNil(t, r)
assert.Len(t, r.Sessions, 1)
})
}
9 changes: 9 additions & 0 deletions internal/daemon/cache/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ type options struct {
withUpdateLastAccessedTime bool
withDbType dbw.DbType
withTargetRetrievalFunc TargetRetrievalFunc
withSessionRetrievalFunc SessionRetrievalFunc
}

// Option - how options are passed as args
Expand Down Expand Up @@ -84,3 +85,11 @@ func WithTargetRetrievalFunc(fn TargetRetrievalFunc) Option {
return nil
}
}

// WithSessionRetrievalFunc provides an option for specifying a sessionRetrievalFunc
func WithSessionRetrievalFunc(fn SessionRetrievalFunc) Option {
return func(o *options) error {
o.withSessionRetrievalFunc = fn
return nil
}
}
Loading

0 comments on commit 2509498

Please sign in to comment.