2023-06-11 20:40:47 -04:00
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package search
import (
"context"
stderrors "errors"
"fmt"
2024-08-28 10:15:04 -07:00
"math"
2023-06-11 20:40:47 -04:00
"net/url"
"os"
"strings"
"time"
"github.com/hashicorp/boundary/api"
2024-02-27 13:10:20 -08:00
"github.com/hashicorp/boundary/api/aliases"
2024-08-28 21:18:38 -04:00
"github.com/hashicorp/boundary/api/scopes"
2023-06-11 20:40:47 -04:00
"github.com/hashicorp/boundary/api/sessions"
"github.com/hashicorp/boundary/api/targets"
2024-05-22 11:11:48 -07:00
cachecmd "github.com/hashicorp/boundary/internal/clientcache/cmd/cache"
2023-11-13 13:36:28 -08:00
"github.com/hashicorp/boundary/internal/clientcache/internal/client"
2023-06-11 20:40:47 -04:00
"github.com/hashicorp/boundary/internal/clientcache/internal/daemon"
"github.com/hashicorp/boundary/internal/cmd/base"
"github.com/mitchellh/cli"
"github.com/posener/complete"
"golang.org/x/exp/slices"
)
var (
_ cli . Command = ( * SearchCommand )( nil )
_ cli . CommandAutocomplete = ( * SearchCommand )( nil )
supportedResourceTypes = [] string {
2024-04-17 12:37:42 -07:00
"resolvable-aliases" ,
2023-06-11 20:40:47 -04:00
"targets" ,
"sessions" ,
2024-08-28 21:18:38 -04:00
"implicit-scopes" ,
2023-06-11 20:40:47 -04:00
}
2023-11-30 08:20:57 -08:00
2024-05-22 11:11:48 -07:00
errCacheNotRunning = stderrors . New ( "The cache process is not running." )
2023-06-11 20:40:47 -04:00
)
type SearchCommand struct {
* base . Command
2024-08-28 10:15:04 -07:00
flagQuery string
flagResource string
flagForceRefresh bool
flagMaxResultSetSize int64
2023-06-11 20:40:47 -04:00
}
func ( c * SearchCommand ) Synopsis () string {
2024-05-22 11:11:48 -07:00
return "Search Boundary resources using client cache"
2023-06-11 20:40:47 -04:00
}
func ( c * SearchCommand ) Help () string {
helpText := `
Usage: boundary search [options]
Search a boundary resource:
2023-11-09 12:59:34 -08:00
$ boundary search -resource targets -query 'name="foo"'
2023-06-11 20:40:47 -04:00
For a full list of examples, please see the documentation.
` + c . Flags (). Help ()
return strings . TrimSpace ( helpText )
}
func ( c * SearchCommand ) Flags () * base . FlagSets {
2024-01-26 15:56:41 -08:00
set := c . FlagSet ( base . FlagSetOutputFormat )
2023-06-11 20:40:47 -04:00
2024-01-26 15:56:41 -08:00
f := set . NewFlagSet ( "Client Options" )
f . StringVar ( & base . StringVar {
Name : "token-name" ,
Target : & c . FlagTokenName ,
EnvVar : base . EnvTokenName ,
Usage : `If specified, the given value will be used as the name when storing the token in the system credential store. This can allow switching user identities for different commands.` ,
})
f . StringVar ( & base . StringVar {
Name : "keyring-type" ,
Target : & c . FlagKeyringType ,
Default : "auto" ,
EnvVar : base . EnvKeyringType ,
Usage : `The type of keyring to use. Defaults to "auto" which will use the Windows credential manager, OSX keychain, or cross-platform password store depending on platform. Set to "none" to disable keyring functionality. Available types, depending on platform, are: "wincred", "keychain", "pass", and "secret-service".` ,
})
f . StringVar ( & base . StringVar {
Name : "token" ,
Target : & c . FlagToken ,
Usage : `A URL pointing to a file on disk (file://) from which a token will be read or an env var (env://) from which the token will be read. Overrides the "token-name" parameter.` ,
})
f . BoolVar ( & base . BoolVar {
Name : "output-curl-string" ,
Target : & c . FlagOutputCurlString ,
Usage : "Instead of executing the request, print an equivalent cURL command string and exit." ,
})
f = set . NewFlagSet ( "Command Options" )
2023-06-11 20:40:47 -04:00
f . StringVar ( & base . StringVar {
Name : "query" ,
Target : & c . flagQuery ,
2024-01-26 15:56:41 -08:00
Usage : `If set, specifies the resource search query. See https://www.boundaryproject.io/docs/commands/search for more information.` ,
2023-06-11 20:40:47 -04:00
})
2024-01-22 17:39:52 -08:00
f . StringVar ( & base . StringVar {
Name : "filter" ,
Target : & c . FlagFilter ,
Usage : "The filter operates against each item in the response. Using single quotes is recommended as filters contain double quotes. The format is the same as the filters used when performing a list. See https://www.boundaryproject.io/docs/concepts/filtering/resource-listing for details on filters when listing." ,
})
2023-06-11 20:40:47 -04:00
f . StringVar ( & base . StringVar {
Name : "resource" ,
Target : & c . flagResource ,
Usage : `Specifies the resource type to search over` ,
Completion : complete . PredictSet ( supportedResourceTypes ... ),
})
2024-08-28 10:15:04 -07:00
f . Int64Var ( & base . Int64Var {
Name : "max-result-set-size" ,
Target : & c . flagMaxResultSetSize ,
Usage : `Specifies an override to the default maximum result set size. Set to -1 to disable the limit. 0 will use the default.` ,
Completion : complete . PredictNothing ,
})
2023-12-06 08:55:11 -08:00
f . BoolVar ( & base . BoolVar {
Name : "force-refresh" ,
Target : & c . flagForceRefresh ,
Usage : `Forces a refresh to be attempted prior to performing the search` ,
Hidden : true ,
})
2023-06-11 20:40:47 -04:00
return set
}
func ( c * SearchCommand ) AutocompleteArgs () complete . Predictor {
return complete . PredictNothing
}
func ( c * SearchCommand ) AutocompleteFlags () complete . Flags {
return c . Flags (). Completions ()
}
func ( c * SearchCommand ) Run ( args [] string ) int {
ctx := c . Context
f := c . Flags ()
if err := f . Parse ( args ); err != nil {
c . PrintCliError ( err )
return base . CommandUserError
}
switch {
case slices . Contains ( supportedResourceTypes , c . flagResource ):
case c . flagResource == "" :
c . PrintCliError ( stderrors . New ( "Resource is required but not passed in via -resource" ))
return base . CommandUserError
default :
c . PrintCliError ( stderrors . New ( "The value passed in with -resource is not currently supported in search" ))
return base . CommandUserError
}
2024-08-28 10:15:04 -07:00
switch {
case c . flagMaxResultSetSize < - 1 :
c . PrintCliError ( stderrors . New ( "Max result set size must be greater than or equal to -1" ))
return base . CommandUserError
case c . flagMaxResultSetSize > math . MaxInt :
c . PrintCliError ( stderrors . New ( fmt . Sprintf ( "Max result set size must be less than or equal to the %v" , math . MaxInt )))
return base . CommandUserError
}
2023-11-13 13:36:28 -08:00
resp , result , apiErr , err := c . Search ( ctx )
2023-06-11 20:40:47 -04:00
if err != nil {
c . PrintCliError ( err )
return base . CommandCliError
}
if apiErr != nil {
2024-05-22 11:11:48 -07:00
c . PrintApiError ( apiErr , "Error from cache when performing search" )
2023-06-11 20:40:47 -04:00
return base . CommandApiError
}
2023-11-13 13:36:28 -08:00
2023-06-11 20:40:47 -04:00
switch base . Format ( c . UI ) {
case "json" :
2023-11-13 13:36:28 -08:00
if ok := c . PrintJsonItem ( resp ); ! ok {
return base . CommandCliError
}
2023-06-11 20:40:47 -04:00
default :
switch {
2024-04-17 12:37:42 -07:00
case len ( result . ResolvableAliases ) > 0 :
c . UI . Output ( printAliasListTable ( result . ResolvableAliases ))
2023-11-13 13:36:28 -08:00
case len ( result . Targets ) > 0 :
c . UI . Output ( printTargetListTable ( result . Targets ))
case len ( result . Sessions ) > 0 :
c . UI . Output ( printSessionListTable ( result . Sessions ))
2024-08-28 21:18:38 -04:00
case len ( result . ImplicitScopes ) > 0 :
c . UI . Output ( printImplicitScopesListTable ( result . ImplicitScopes ))
2023-06-11 20:40:47 -04:00
default :
c . UI . Output ( "No items found" )
}
2024-08-28 21:18:38 -04:00
// Put this at the end or people may not see it as they may not scroll
// all the way up.
if result . Incomplete {
c . UI . Warn ( "The maximum result set size was reached and the search results are incomplete. Please narrow your search or adjust the -max-result-set-size parameter." )
}
2023-06-11 20:40:47 -04:00
}
return base . CommandSuccess
}
2023-11-13 13:36:28 -08:00
func ( c * SearchCommand ) Search ( ctx context . Context ) ( * api . Response , * daemon . SearchResult , * api . Error , error ) {
2023-11-15 14:40:05 -08:00
cl , err := c . Client ()
2023-06-11 20:40:47 -04:00
if err != nil {
2023-11-13 13:36:28 -08:00
return nil , nil , nil , err
2023-06-11 20:40:47 -04:00
}
2023-11-15 14:40:05 -08:00
t := cl . Token ()
2023-11-07 09:16:57 -08:00
if t == "" {
2023-11-13 13:36:28 -08:00
return nil , nil , nil , fmt . Errorf ( "Auth Token selected for searching is empty." )
2023-11-07 09:16:57 -08:00
}
tSlice := strings . SplitN ( t , "_" , 3 )
if len ( tSlice ) != 3 {
2023-11-13 13:36:28 -08:00
return nil , nil , nil , fmt . Errorf ( "Auth Token selected for searching is in an unexpected format." )
2023-11-07 09:16:57 -08:00
}
2023-06-11 20:40:47 -04:00
tf := filterBy {
2024-01-22 17:39:52 -08:00
flagFilter : c . FlagFilter ,
2023-12-06 08:55:11 -08:00
flagQuery : c . flagQuery ,
resource : c . flagResource ,
authTokenId : strings . Join ( tSlice [: 2 ], "_" ),
forceRefresh : c . flagForceRefresh ,
2023-06-11 20:40:47 -04:00
}
2024-08-28 10:15:04 -07:00
if c . flagMaxResultSetSize != 0 {
tf . maxResultSetSize = int ( c . flagMaxResultSetSize )
}
2023-11-15 14:40:05 -08:00
var opts [] client . Option
if c . FlagOutputCurlString {
opts = append ( opts , client . WithOutputCurlString ())
}
2023-06-11 20:40:47 -04:00
2024-05-22 11:11:48 -07:00
dotPath , err := cachecmd . DefaultDotDirectory ( ctx )
2023-06-11 20:40:47 -04:00
if err != nil {
2023-11-13 13:36:28 -08:00
return nil , nil , nil , err
2023-06-11 20:40:47 -04:00
}
2023-11-15 14:40:05 -08:00
return search ( ctx , dotPath , tf , opts ... )
2023-06-11 20:40:47 -04:00
}
2023-11-15 14:40:05 -08:00
func search ( ctx context . Context , daemonPath string , fb filterBy , opt ... client . Option ) ( * api . Response , * daemon . SearchResult , * api . Error , error ) {
2024-01-02 11:15:45 -08:00
addr := daemon . SocketAddress ( daemonPath )
_ , err := os . Stat ( addr . Path )
2023-11-30 08:20:57 -08:00
if addr . Scheme == "unix" && err != nil {
2024-05-22 11:11:48 -07:00
return nil , nil , nil , errCacheNotRunning
2023-06-11 20:40:47 -04:00
}
2023-11-13 13:36:28 -08:00
c , err := client . New ( ctx , addr )
2023-06-11 20:40:47 -04:00
if err != nil {
2023-11-13 13:36:28 -08:00
return nil , nil , nil , err
2023-06-11 20:40:47 -04:00
}
2023-11-13 13:36:28 -08:00
q := & url . Values {}
2023-06-11 20:40:47 -04:00
q . Add ( "auth_token_id" , fb . authTokenId )
q . Add ( "resource" , fb . resource )
q . Add ( "query" , fb . flagQuery )
2024-01-22 17:39:52 -08:00
q . Add ( "filter" , fb . flagFilter )
2023-12-06 08:55:11 -08:00
if fb . forceRefresh {
q . Add ( "force_refresh" , "true" )
}
2024-08-28 10:15:04 -07:00
if fb . maxResultSetSize != 0 {
q . Add ( "max_result_set_size" , fmt . Sprintf ( "%d" , fb . maxResultSetSize ))
}
2023-11-15 14:40:05 -08:00
resp , err := c . Get ( ctx , "/v1/search" , q , opt ... )
2023-11-13 13:36:28 -08:00
if err != nil {
2024-05-22 11:11:48 -07:00
return nil , nil , nil , fmt . Errorf ( "Error when sending request to the cache: %w." , err )
2023-11-13 13:36:28 -08:00
}
res := & daemon . SearchResult {}
apiErr , err := resp . Decode ( & res )
2023-06-11 20:40:47 -04:00
if err != nil {
2024-05-22 11:11:48 -07:00
return nil , nil , nil , fmt . Errorf ( "Error when decoding request from the cache: %w." , err )
2023-11-13 13:36:28 -08:00
}
if apiErr != nil {
return resp , nil , apiErr , nil
2023-06-11 20:40:47 -04:00
}
2023-11-13 13:36:28 -08:00
return resp , res , nil , nil
2023-06-11 20:40:47 -04:00
}
2024-02-27 13:10:20 -08:00
func printAliasListTable ( items [] * aliases . Alias ) string {
if len ( items ) == 0 {
2024-04-17 12:37:42 -07:00
return "No resolvable aliases found"
2024-02-27 13:10:20 -08:00
}
var output [] string
output = [] string {
"" ,
2024-04-17 12:37:42 -07:00
"Resolvable Alias information:" ,
2024-02-27 13:10:20 -08:00
}
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 . Version > 0 {
output = append ( output ,
fmt . Sprintf ( " Version: %d" , item . Version ),
)
}
if item . Type != "" {
output = append ( output ,
fmt . Sprintf ( " Type: %s" , item . Type ),
)
}
if item . DestinationId != "" {
output = append ( output ,
fmt . Sprintf ( " DestinationId: %s" , item . DestinationId ),
)
}
if item . Value != "" {
output = append ( output ,
fmt . Sprintf ( " Value: %s" , item . Value ),
)
}
}
return base . WrapForHelpText ( output )
}
2023-06-11 20:40:47 -04:00
func printTargetListTable ( items [] * targets . Target ) string {
if len ( items ) == 0 {
return "No targets found"
}
var output [] string
output = [] string {
"" ,
"Target 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 . Version > 0 {
output = append ( output ,
fmt . Sprintf ( " Version: %d" , item . Version ),
)
}
if item . Type != "" {
output = append ( output ,
fmt . Sprintf ( " Type: %s" , item . Type ),
)
}
if item . Name != "" {
output = append ( output ,
fmt . Sprintf ( " Name: %s" , item . Name ),
)
}
if item . Description != "" {
output = append ( output ,
fmt . Sprintf ( " Description: %s" , item . Description ),
)
}
if item . Address != "" {
output = append ( output ,
fmt . Sprintf ( " Address: %s" , item . Address ),
)
}
if len ( item . AuthorizedActions ) > 0 {
output = append ( output ,
" Authorized Actions:" ,
base . WrapSlice ( 6 , item . AuthorizedActions ),
)
}
}
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 )
}
2024-08-28 21:18:38 -04:00
func printImplicitScopesListTable ( items [] * scopes . Scope ) string {
if len ( items ) == 0 {
return "No implicit scopes found"
}
var output [] string
output = [] string {
"" ,
"Scope 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)" ),
)
}
}
return base . WrapForHelpText ( output )
}
2023-06-11 20:40:47 -04:00
type filterBy struct {
2024-08-28 10:15:04 -07:00
flagFilter string
flagQuery string
authTokenId string
resource string
forceRefresh bool
maxResultSetSize int
2023-06-11 20:40:47 -04:00
}