Create Client and update Errors for client daemon commands (#4011)

* Create a client cache daemon client

* Fix error formatting for the client cache cmds
This commit is contained in:
Todd authored and Johan Brandhorst-Satzkorn committed 2024-01-05 07:43:20 -08:00
1 parent 8af20349e7
commit b71d134e06
17 files changed
+500 -273

No files matched your search

+12 -38
View File
@@ -16,7 +16,6 @@ import (
"net/url"
"os"
"path"
"path/filepath"
"strconv"
"strings"
"sync"
@@ -58,12 +57,7 @@ type Config struct {
// HttpClient.
Addr string
// Set when the Addr is a unix socket. The parsing of this field takes
// a filename approach instead of a URL approach since parsing file names
// as a URL may error out on some platforms.
socketAddr string
// Token is the client token that results from authentication and can be
// Token is the client token that reuslts from authentication and can be
// used to make calls into Boundary
Token string
@@ -260,16 +254,6 @@ func (c *Config) ConfigureTLS() error {
// This also removes any trailing "/v1"; we'll use that in our commands so we
// don't require it from users.
func (c *Config) setAddr(addr string) error {
if strings.HasPrefix(addr, "unix://") {
c.socketAddr = strings.TrimPrefix(addr, "unix://")
c.socketAddr = filepath.Clean(c.socketAddr)
// Update the specified address to the cleaned up version
c.Addr = fmt.Sprintf("unix://%s", c.socketAddr)
return nil
}
c.socketAddr = ""
u, err := url.Parse(addr)
if err != nil {
return fmt.Errorf("error parsing address: %w", err)
@@ -598,7 +582,6 @@ func (c *Client) Clone() *Client {
config := c.config
newConfig := &Config{
socketAddr: config.socketAddr,
Addr: config.Addr,
Token: config.Token,
RecoveryKmsWrapper: config.RecoveryKmsWrapper,
@@ -662,29 +645,31 @@ func (c *Client) NewRequest(ctx context.Context, method, requestPath string, bod
}
c.modifyLock.RLock()
addr := c.config.Addr
srvLookup := c.config.SRVLookup
token := c.config.Token
httpClient := c.config.HttpClient
headers := copyHeaders(c.config.Headers)
c.modifyLock.RUnlock()
var hostHeader string
var u *url.URL
switch {
case c.config.socketAddr != "":
u, err := url.Parse(addr)
if err != nil {
return nil, err
}
if strings.HasPrefix(addr, "unix://") {
socket := strings.TrimPrefix(addr, "unix://")
transport := httpClient.Transport.(*http.Transport)
transport.DialContext = func(context.Context, string, string) (net.Conn, error) {
dialer := net.Dialer{}
return dialer.DialContext(ctx, "unix", c.config.socketAddr)
return dialer.DialContext(ctx, "unix", socket)
}
// Since the address points to a unix domain socket, the scheme in the
// *URL would be set to `unix`. The *URL in the client is expected to
// be pointing to the protocol used in the application layer and not to
// the transport layer. Hence, setting the fields accordingly.
u = &url.URL{}
u.Scheme = "http"
u.Host = c.config.socketAddr
u.Path = ""
// Go 1.21.0 introduced strict host header validation for clients.
@@ -695,17 +680,7 @@ func (c *Client) NewRequest(ctx context.Context, method, requestPath string, bod
// that isn't the actual unix domain socket address. Following
// Docker's lead (https://github.com/moby/moby/pull/45942),
// use a localhost TLD.
hostHeader = "api.boundary.localhost"
case c.config.Addr != "":
addr := c.config.Addr
var err error
u, err = url.Parse(addr)
if err != nil {
return nil, err
}
hostHeader = u.Host
default:
return nil, errors.New("no valid address discovered")
u.Host = "api.boundary.localhost"
}
host := u.Host
@@ -732,9 +707,8 @@ func (c *Client) NewRequest(ctx context.Context, method, requestPath string, bod
Host: host,
Path: path.Join(u.Path, "/v1/", requestPath),
},
Host: hostHeader,
Host: u.Host,
}
req.Header = headers
req.Header.Set("authorization", "Bearer "+token)
req.Header.Set("content-type", "application/json")
+39 -48
View File
@@ -11,70 +11,60 @@ import (
func TestConfigSetAddress(t *testing.T) {
type test struct {
name string
input string
address string
socketAddr string
name string
input string
address string
err string
}
tests := []test{
{
name: "bare",
input: "http://127.0.0.1:9200",
address: "http://127.0.0.1:9200",
"bare",
"http://127.0.0.1:9200",
"http://127.0.0.1:9200",
"",
},
{
name: "bare with version",
input: "http://127.0.0.1:9200/v1",
address: "http://127.0.0.1:9200",
"bare with version",
"http://127.0.0.1:9200/v1",
"http://127.0.0.1:9200",
"",
},
{
name: "bare with version and trailing slash",
input: "http://127.0.0.1:9200/v1/",
address: "http://127.0.0.1:9200",
"bare with version and trailing slash",
"http://127.0.0.1:9200/v1/",
"http://127.0.0.1:9200",
"",
},
{
name: "valid top level scope",
input: "http://127.0.0.1:9200/v1/scopes",
address: "http://127.0.0.1:9200",
"valid top level scope",
"http://127.0.0.1:9200/v1/scopes",
"http://127.0.0.1:9200",
"",
},
{
name: "valid scope",
input: "http://127.0.0.1:9200/v1/scopes/scopeid",
address: "http://127.0.0.1:9200",
"valid scope",
"http://127.0.0.1:9200/v1/scopes/scopeid",
"http://127.0.0.1:9200",
"",
},
{
name: "longer path project",
input: "http://127.0.0.1:9200/v1/auth-methods",
address: "http://127.0.0.1:9200",
"longer path project",
"http://127.0.0.1:9200/v1/auth-methods",
"http://127.0.0.1:9200",
"",
},
{
name: "valid project",
input: "http://127.0.0.1:9200/my-install",
address: "http://127.0.0.1:9200/my-install",
"valid project",
"http://127.0.0.1:9200/my-install",
"http://127.0.0.1:9200/my-install",
"",
},
{
name: "valid project path containing v1",
input: "http://127.0.0.1:9200/randomPathHasv1InIt",
address: "http://127.0.0.1:9200/randomPathHasv1InIt",
},
{
name: "unix socket with linux pathing",
input: "unix:///home/username/.boundary/socketdir/boundary.sock",
address: "unix:///home/username/.boundary/socketdir/boundary.sock",
socketAddr: "/home/username/.boundary/socketdir/boundary.sock",
},
{
name: "unix socket with weird linux pathing",
input: "unix:///home/username/.boundary/something/../socketdir/boundary.sock",
address: "unix:///home/username/.boundary/socketdir/boundary.sock",
socketAddr: "/home/username/.boundary/socketdir/boundary.sock",
},
{
name: "unix socket with windows pathing",
input: "unix://C:\\Users\\Admin\\AppData\\boundary.sock",
address: "unix://C:\\Users\\Admin\\AppData\\boundary.sock",
socketAddr: "C:\\Users\\Admin\\AppData\\boundary.sock",
"valid project path containing v1",
"http://127.0.0.1:9200/randomPathHasv1InIt",
"http://127.0.0.1:9200/randomPathHasv1InIt",
"",
},
}
@@ -82,9 +72,10 @@ func TestConfigSetAddress(t *testing.T) {
t.Run(v.name, func(t *testing.T) {
var c Config
err := c.setAddr(v.input)
assert.NoError(t, err)
if err != nil {
assert.Equal(t, v.err, err.Error())
}
assert.Equal(t, v.address, c.Addr)
assert.Equal(t, v.socketAddr, c.socketAddr)
})
}
}
+7
View File
@@ -20,6 +20,13 @@ type Response struct {
Map map[string]any
}
// NewResponse returns a new *Response based on the provided http.Response.
// Just as when constructing the Response directly, Body and Map will be
// populated after Decode is called.
func NewResponse(r *http.Response) *Response {
return &Response{resp: r}
}
// HttpResponse returns the underlying HTTP response
func (r *Response) HttpResponse() *http.Response {
return r.resp
+32 -39
View File
@@ -5,14 +5,15 @@ package daemon
import (
"context"
"errors"
"fmt"
"os"
"strings"
"github.com/hashicorp/boundary/api"
"github.com/hashicorp/boundary/internal/clientcache/internal/client"
"github.com/hashicorp/boundary/internal/clientcache/internal/daemon"
"github.com/hashicorp/boundary/internal/cmd/base"
"github.com/hashicorp/boundary/internal/errors"
"github.com/hashicorp/boundary/version"
"github.com/mitchellh/cli"
"github.com/posener/complete"
)
@@ -76,7 +77,7 @@ func (c *AddTokenCommand) Run(args []string) int {
return base.CommandCliError
}
apiErr, err := c.Add(ctx, client, keyringType, tokenName)
resp, apiErr, err := c.Add(ctx, client, keyringType, tokenName)
if err != nil {
c.PrintCliError(err)
return base.CommandCliError
@@ -84,30 +85,35 @@ func (c *AddTokenCommand) Run(args []string) int {
if apiErr != nil {
c.PrintApiError(apiErr, "Error from daemon when adding a token")
return base.CommandApiError
}
switch base.Format(c.UI) {
case "json":
if ok := c.PrintJsonItem(resp); !ok {
return base.CommandCliError
}
case "table":
c.UI.Output("The daemon add-token operation completed successfully.")
}
return base.CommandSuccess
}
func (c *AddTokenCommand) Add(ctx context.Context, client *api.Client, keyringType, tokenName string) (*api.Error, error) {
const op = "daemon.(AddTokenCommand).Add"
func (c *AddTokenCommand) Add(ctx context.Context, apiClient *api.Client, keyringType, tokenName string) (*api.Response, *api.Error, error) {
pa := daemon.UpsertTokenRequest{
BoundaryAddr: client.Addr(),
BoundaryAddr: apiClient.Addr(),
}
switch keyringType {
case "", base.NoneKeyring:
token := client.Token()
token := apiClient.Token()
if parts := strings.SplitN(token, "_", 4); len(parts) == 3 {
pa.AuthTokenId = strings.Join(parts[:2], "_")
} else {
return nil, errors.New(ctx, errors.InvalidParameter, op, "found auth token is not in the proper format")
return nil, nil, errors.New("The found auth token is not in the proper format.")
}
pa.AuthToken = token
default:
at := c.ReadTokenFromKeyring(keyringType, tokenName)
if at == nil {
return nil, errors.New(ctx, errors.Conflict, op, "no auth token available to send to daemon")
return nil, nil, errors.New("No auth token could be read from the keyring to send to daemon.")
}
pa.Keyring = &daemon.KeyringToken{
KeyringType: keyringType,
@@ -118,42 +124,29 @@ func (c *AddTokenCommand) Add(ctx context.Context, client *api.Client, keyringTy
dotPath, err := DefaultDotDirectory(ctx)
if err != nil {
return nil, err
return nil, nil, err
}
return addToken(ctx, dotPath, &pa)
}
func addToken(ctx context.Context, daemonPath string, p *daemon.UpsertTokenRequest) (*api.Error, error) {
const op = "daemon.addToken"
client, err := api.NewClient(nil)
if err != nil {
return nil, errors.Wrap(ctx, err, op)
}
func addToken(ctx context.Context, daemonPath string, p *daemon.UpsertTokenRequest) (*api.Response, *api.Error, error) {
addr, err := daemon.SocketAddress(daemonPath)
if err != nil {
return nil, errors.Wrap(ctx, err, op)
return nil, nil, fmt.Errorf("Error when retrieving the socket address: %w", err)
}
_, err = os.Stat(addr.Path)
if strings.EqualFold(addr.Scheme, "unix") && err != nil {
return nil, errors.Wrap(ctx, err, op)
}
if err := client.SetAddr(addr.String()); err != nil {
return nil, errors.Wrap(ctx, err, op)
}
// Because this is using the real lib it can pick up from stored locations
// like the system keychain. Explicitly clear the token for now
client.SetToken("")
req, err := client.NewRequest(ctx, "POST", "/tokens", p)
if err != nil {
return nil, err
}
req.Header.Add(daemon.VersionHeaderKey, version.Get().VersionNumber())
resp, err := client.Do(req)
if err != nil {
return nil, err
if addr.Scheme == "unix" && err != nil {
return nil, nil, fmt.Errorf("Error when detecting if the domain socket is present: %w.", err)
}
return resp.Decode(&struct{}{})
c, err := client.New(ctx, addr)
if err != nil {
return nil, nil, fmt.Errorf("Error when making a new client: %w.", err)
}
resp, err := c.Post(ctx, "/v1/tokens", p)
if err != nil {
return nil, nil, fmt.Errorf("Error when sending request to the daemon: %w.", err)
}
apiErr, err := resp.Decode(nil)
return resp, apiErr, err
}
@@ -103,7 +103,7 @@ func (w *CommandWrapper) addTokenToCache(ctx context.Context, token string) bool
return false
}
apiErr, err := com.Add(ctx, client, keyringType, tokName)
_, apiErr, err := com.Add(ctx, client, keyringType, tokName)
return err == nil && apiErr == nil
}
+3 -3
View File
@@ -5,6 +5,7 @@ package daemon
import (
"context"
stderrors "errors"
"fmt"
"io"
"net"
@@ -136,7 +137,6 @@ func (c *StartCommand) AutocompleteFlags() complete.Flags {
}
func (c *StartCommand) Run(args []string) int {
const op = "daemon.(StartCommand).Run"
ctx, cancel := context.WithCancel(c.Context)
defer cancel()
@@ -265,9 +265,9 @@ func makeBackground(ctx context.Context, dotDir string, runBackgroundFlag bool)
writers := []io.Writer{}
pidPath := filepath.Join(dotDir, pidFileName)
if running, err := pidFileInUse(ctx, pidPath); running != nil {
return false, writers, noopPidCleanup, errors.New(ctx, errors.Conflict, op, "daemon already running")
return false, writers, noopPidCleanup, stderrors.New("The daemon is already running.")
} else if err != nil && !errors.Match(errors.T(errors.NotFound), err) {
return false, writers, noopPidCleanup, errors.Wrap(ctx, err, op)
return false, writers, noopPidCleanup, fmt.Errorf("Error when checking if the daemon pid is in use: %w.", err)
}
if !runBackgroundFlag && os.Getenv(backgroundEnvName) != backgroundEnvVal {
+27 -32
View File
@@ -12,9 +12,10 @@ import (
"time"
"github.com/hashicorp/boundary/api"
"github.com/hashicorp/boundary/internal/clientcache/internal/client"
"github.com/hashicorp/boundary/internal/clientcache/internal/daemon"
"github.com/hashicorp/boundary/internal/cmd/base"
"github.com/hashicorp/boundary/version"
"github.com/hashicorp/boundary/internal/errors"
"github.com/mitchellh/cli"
"github.com/posener/complete"
)
@@ -69,14 +70,7 @@ func (c *StatusCommand) Run(args []string) int {
return base.CommandUserError
}
resp, err := c.Status(ctx)
if err != nil {
c.PrintCliError(err)
return base.CommandCliError
}
res := &daemon.StatusResult{}
apiErr, err := resp.Decode(res)
resp, result, apiErr, err := c.Status(ctx)
if err != nil {
c.PrintCliError(err)
return base.CommandCliError
@@ -84,56 +78,57 @@ func (c *StatusCommand) Run(args []string) int {
if apiErr != nil {
c.PrintApiError(apiErr, "Error from daemon when getting the status")
return base.CommandApiError
}
switch base.Format(c.UI) {
case "json":
c.UI.Output(resp.Body.String())
if ok := c.PrintJsonItem(resp); !ok {
return base.CommandCliError
}
default:
c.UI.Output(printStatusTable(res))
c.UI.Output(printStatusTable(result))
}
return base.CommandSuccess
}
func (c *StatusCommand) Status(ctx context.Context) (*api.Response, error) {
func (c *StatusCommand) Status(ctx context.Context) (*api.Response, *daemon.StatusResult, *api.Error, error) {
dotPath, err := DefaultDotDirectory(ctx)
if err != nil {
return nil, err
return nil, nil, nil, err
}
return status(ctx, dotPath)
}
func status(ctx context.Context, daemonPath string) (*api.Response, error) {
client, err := api.NewClient(nil)
if err != nil {
return nil, fmt.Errorf("Error creating a new API client: %w", err)
}
func status(ctx context.Context, daemonPath string) (*api.Response, *daemon.StatusResult, *api.Error, error) {
const op = "daemon.status"
addr, err := daemon.SocketAddress(daemonPath)
if err != nil {
return nil, fmt.Errorf("Error getting socket address: %w", err)
return nil, nil, nil, fmt.Errorf("Error getting socket address: %w", err)
}
_, err = os.Stat(addr.Path)
if addr.Scheme == "unix" && err != nil {
return nil, errDaemonNotRunning
return nil, nil, nil, errDaemonNotRunning
}
if err := client.SetAddr(addr.String()); err != nil {
return nil, fmt.Errorf("Error when setting the client's address: %w", err)
c, err := client.New(ctx, addr)
if err != nil {
return nil, nil, nil, err
}
// Because this is using the real lib it can pick up from stored locations
// like the system keychain. Explicitly clear the token for now
client.SetToken("")
req, err := client.NewRequest(ctx, "GET", "/status", nil)
resp, err := c.Get(ctx, "/v1/status", nil)
if err != nil {
return nil, err
return nil, nil, nil, errors.Wrap(ctx, err, op, errors.WithMsg("client do failed"))
}
req.Header.Add(daemon.VersionHeaderKey, version.Get().VersionNumber())
resp, err := client.Do(req)
res := &daemon.StatusResult{}
apiErr, err := resp.Decode(&res)
if err != nil {
return nil, err
return nil, nil, nil, fmt.Errorf("Error when sending request to the daemon: %w.", err)
}
return resp, nil
if apiErr != nil {
return resp, nil, apiErr, nil
}
return resp, res, nil, nil
}
func printStatusTable(status *daemon.StatusResult) string {
+1 -1
View File
@@ -60,7 +60,7 @@ func (c *StopCommand) Run(args []string) int {
}
if err := c.stop(c.Context); err != nil {
if errors.Match(errors.T(errors.NotFound), err) {
c.PrintCliError(stderr.New("deamon process is not running"))
c.PrintCliError(stderr.New("The deamon process is not running."))
} else {
c.PrintCliError(err)
}
@@ -8,35 +8,35 @@ package daemon
import (
"context"
"errors"
"fmt"
"path/filepath"
"syscall"
"github.com/hashicorp/boundary/internal/errors"
"github.com/hashicorp/boundary/internal/util"
)
// stop will send a term signal to the daemon to shut down.
func (s *StopCommand) stop(ctx context.Context) error {
const op = "daemon.(StopCommand).stop"
switch {
case util.IsNil(ctx):
return errors.New(ctx, errors.InvalidParameter, op, "context is missing")
return errors.New("Invalid parameter provided to stop: context is missing")
}
dotPath, err := DefaultDotDirectory(ctx)
if err != nil {
return errors.Wrap(ctx, err, op)
return fmt.Errorf("Error when getting default dot directory: %w", err)
}
pidPath := filepath.Join(dotPath, pidFileName)
p, err := pidFileInUse(ctx, pidPath)
if err != nil {
return errors.Wrap(ctx, err, op)
return fmt.Errorf("Error when checking if the daemon's pid file is in use: %w", err)
}
if p == nil {
return errors.New(ctx, errors.NotFound, op, "daemon not running")
return errors.New("The daemon is not running.")
}
if err := p.Signal(syscall.SIGTERM); err != nil {
return errors.Wrap(ctx, err, op, errors.WithMsg("sending sigterm to process"))
return fmt.Errorf("Error when sending sigterm to daemon process: %w", err)
}
return nil
}
+16 -29
View File
@@ -8,28 +8,27 @@ package daemon
import (
"context"
"errors"
"fmt"
"os"
"path/filepath"
"github.com/hashicorp/boundary/api"
"github.com/hashicorp/boundary/internal/clientcache/internal/client"
"github.com/hashicorp/boundary/internal/clientcache/internal/daemon"
"github.com/hashicorp/boundary/internal/errors"
"github.com/hashicorp/boundary/internal/util"
"github.com/hashicorp/boundary/version"
)
// stop will send a term signal to the daemon to shut down.
func (s *StopCommand) stop(ctx context.Context) error {
const op = "daemon.(StopCommand).stop"
switch {
case util.IsNil(ctx):
return errors.New(ctx, errors.InvalidParameter, op, "context is missing")
return errors.New("Invalid parameter provided to stop: context is missing")
}
dotPath, err := DefaultDotDirectory(ctx)
if err != nil {
return errors.Wrap(ctx, err, op)
return fmt.Errorf("Error when getting default dot directory: %w", err)
}
apiErr, err := stopThroughHandler(ctx, dotPath)
@@ -41,7 +40,7 @@ func (s *StopCommand) stop(ctx context.Context) error {
} else if apiErr != nil {
errMsg = apiErr.Message
}
s.UI.Warn(fmt.Sprintf("failed stopping the daemon through the handler: %q, now killing the process", errMsg))
s.UI.Warn(fmt.Sprintf("Failed stopping the daemon through the handler: %q, now killing the process", errMsg))
default:
// there wasn't an error stopping it through the handler. No need to
// force kill the process
@@ -52,46 +51,34 @@ func (s *StopCommand) stop(ctx context.Context) error {
p, err := pidFileInUse(ctx, pidPath)
if err != nil {
if os.IsNotExist(err) {
return errors.Wrap(ctx, err, op, errors.WithCode(errors.NotFound))
return errors.New("Unable to stop the daemon: pid file not found.")
}
return errors.Wrap(ctx, err, op, errors.WithMsg("Unable to stop the daemon"))
return fmt.Errorf("Error when trying to stop the daemon: %w", err)
}
if p == nil {
return errors.New(ctx, errors.NotFound, op, "daemon process was not found")
return errors.New("Daemon process was not found.")
}
if err := p.Kill(); err != nil {
return errors.Wrap(ctx, err, op, errors.WithMsg("killing the daemon"))
return fmt.Errorf("Error when killing the process: %w.", err)
}
return nil
}
func stopThroughHandler(ctx context.Context, dotPath string) (*api.Error, error) {
const op = "daemon.stopThroughHandler"
sockAddr, err := daemon.SocketAddress(dotPath)
addr, err := daemon.SocketAddress(dotPath)
if err != nil {
return nil, errors.Wrap(ctx, err, op)
return nil, fmt.Errorf("Error when retrieving the socket address: %w", err)
}
client, err := api.NewClient(nil)
if err != nil {
return nil, errors.Wrap(ctx, err, op)
}
if err := client.SetAddr(sockAddr.String()); err != nil {
return nil, errors.Wrap(ctx, err, op)
}
// Because this is using the real lib it can pick up from stored locations
// like the system keychain. Explicitly clear the token for now
client.SetToken("")
req, err := client.NewRequest(ctx, "POST", "/stop", nil)
c, err := client.New(ctx, addr)
if err != nil {
return nil, err
}
req.Header.Add(daemon.VersionHeaderKey, version.Get().VersionNumber())
resp, err := client.Do(req)
resp, err := c.Post(ctx, "/v1/stop", nil)
if err != nil {
return nil, err
return nil, fmt.Errorf("Error when sending request to the daemon: %w.", err)
}
return resp.Decode(&struct{}{})
apiErr, err := resp.Decode(nil)
return apiErr, err
}
+33 -39
View File
@@ -16,10 +16,9 @@ import (
"github.com/hashicorp/boundary/api/sessions"
"github.com/hashicorp/boundary/api/targets"
daemoncmd "github.com/hashicorp/boundary/internal/clientcache/cmd/daemon"
"github.com/hashicorp/boundary/internal/clientcache/internal/client"
"github.com/hashicorp/boundary/internal/clientcache/internal/daemon"
"github.com/hashicorp/boundary/internal/cmd/base"
"github.com/hashicorp/boundary/internal/errors"
"github.com/hashicorp/boundary/version"
"github.com/mitchellh/cli"
"github.com/posener/complete"
"golang.org/x/exp/slices"
@@ -104,13 +103,7 @@ func (c *SearchCommand) Run(args []string) int {
return base.CommandUserError
}
resp, err := c.Search(ctx)
if err != nil {
c.PrintCliError(err)
return base.CommandCliError
}
res := &daemon.SearchResult{}
apiErr, err := resp.Decode(res)
resp, result, apiErr, err := c.Search(ctx)
if err != nil {
c.PrintCliError(err)
return base.CommandCliError
@@ -119,15 +112,18 @@ func (c *SearchCommand) Run(args []string) int {
c.PrintApiError(apiErr, "Error from daemon when performing search")
return base.CommandApiError
}
switch base.Format(c.UI) {
case "json":
c.UI.Output(resp.Body.String())
if ok := c.PrintJsonItem(resp); !ok {
return base.CommandCliError
}
default:
switch {
case len(res.Targets) > 0:
c.UI.Output(printTargetListTable(res.Targets))
case len(res.Sessions) > 0:
c.UI.Output(printSessionListTable(res.Sessions))
case len(result.Targets) > 0:
c.UI.Output(printTargetListTable(result.Targets))
case len(result.Sessions) > 0:
c.UI.Output(printSessionListTable(result.Sessions))
default:
c.UI.Output("No items found")
}
@@ -135,18 +131,18 @@ func (c *SearchCommand) Run(args []string) int {
return base.CommandSuccess
}
func (c *SearchCommand) Search(ctx context.Context) (*api.Response, error) {
func (c *SearchCommand) Search(ctx context.Context) (*api.Response, *daemon.SearchResult, *api.Error, error) {
client, err := c.Client()
if err != nil {
return nil, err
return nil, nil, nil, err
}
t := client.Token()
if t == "" {
return nil, fmt.Errorf("Auth Token selected for searching is empty.")
return nil, nil, nil, fmt.Errorf("Auth Token selected for searching is empty.")
}
tSlice := strings.SplitN(t, "_", 3)
if len(tSlice) != 3 {
return nil, fmt.Errorf("Auth Token selected for searching is in an unexpected format.")
return nil, nil, nil, fmt.Errorf("Auth Token selected for searching is in an unexpected format.")
}
tf := filterBy{
@@ -157,43 +153,41 @@ func (c *SearchCommand) Search(ctx context.Context) (*api.Response, error) {
dotPath, err := daemoncmd.DefaultDotDirectory(ctx)
if err != nil {
return nil, err
return nil, nil, nil, err
}
return search(ctx, dotPath, tf)
}
func search(ctx context.Context, daemonPath string, fb filterBy) (*api.Response, error) {
const op = "search.search"
client, err := api.NewClient(nil)
if err != nil {
return nil, errors.Wrap(ctx, err, op)
}
func search(ctx context.Context, daemonPath string, fb filterBy) (*api.Response, *daemon.SearchResult, *api.Error, error) {
addr, err := daemon.SocketAddress(daemonPath)
if err != nil {
return nil, errors.Wrap(ctx, err, op)
return nil, nil, nil, fmt.Errorf("Error when retrieving the socket address: %w", err)
}
if _, err := os.Stat(addr.Path); addr.Scheme == "unix" && err == os.ErrNotExist {
return nil, errors.New(ctx, errors.Internal, op, "daemon unix socket is not setup")
return nil, nil, nil, fmt.Errorf("Error when detecting if the domain socket is present: %w.", err)
}
if err := client.SetAddr(addr.String()); err != nil {
return nil, errors.Wrap(ctx, err, op)
c, err := client.New(ctx, addr)
if err != nil {
return nil, nil, nil, err
}
req, err := client.NewRequest(ctx, "GET", "/search", nil)
if err != nil {
return nil, errors.Wrap(ctx, err, op, errors.WithMsg("new client request error"))
}
req.Header.Add(daemon.VersionHeaderKey, version.Get().VersionNumber())
q := url.Values{}
q := &url.Values{}
q.Add("auth_token_id", fb.authTokenId)
q.Add("resource", fb.resource)
q.Add("query", fb.flagQuery)
req.URL.RawQuery = q.Encode()
resp, err := client.Do(req)
resp, err := c.Get(ctx, "/v1/search", q)
if err != nil {
return nil, errors.Wrap(ctx, err, op, errors.WithMsg("client do failed"))
return nil, nil, nil, fmt.Errorf("Error when sending request to the daemon: %w.", err)
}
return resp, err
res := &daemon.SearchResult{}
apiErr, err := resp.Decode(&res)
if err != nil {
return nil, nil, nil, fmt.Errorf("Error when decoding request from the daemon: %w.", err)
}
if apiErr != nil {
return resp, nil, apiErr, nil
}
return resp, res, nil, nil
}
func printTargetListTable(items []*targets.Target) string {
+26 -35
View File
@@ -106,43 +106,40 @@ func TestSearch(t *testing.T) {
for _, tc := range errorCases {
t.Run(tc.name, func(t *testing.T) {
resp, err := search(ctx, srv.BaseDotDir(), tc.fb)
resp, r, apiErr, err := search(ctx, srv.BaseDotDir(), tc.fb)
require.NoError(t, err)
r := daemon.SearchResult{}
apiErr, err := resp.Decode(&r)
assert.NoError(t, err)
assert.NotNil(t, apiErr)
assert.Contains(t, apiErr.Message, tc.apiErrContains)
assert.NotNil(t, resp)
assert.Nil(t, r)
})
}
t.Run("empty response from list", func(t *testing.T) {
resp, err := search(ctx, srv.BaseDotDir(), filterBy{
resp, r, apiErr, err := search(ctx, srv.BaseDotDir(), filterBy{
authTokenId: at.Id,
resource: "targets",
})
require.NoError(t, err)
r := daemon.SearchResult{}
apiErr, err := resp.Decode(&r)
assert.NoError(t, err)
assert.Nil(t, apiErr)
assert.NotNil(t, resp)
assert.NotNil(t, r)
assert.EqualValues(t, r, daemon.SearchResult{})
assert.EqualValues(t, r, &daemon.SearchResult{})
})
t.Run("empty response from query", func(t *testing.T) {
resp, err := search(ctx, srv.BaseDotDir(), filterBy{
resp, r, apiErr, err := search(ctx, srv.BaseDotDir(), filterBy{
authTokenId: at.Id,
flagQuery: "name=name",
flagQuery: "name='name'",
resource: "targets",
})
require.NoError(t, err)
r := daemon.SearchResult{}
apiErr, err := resp.Decode(&r)
assert.NoError(t, err)
assert.Nil(t, apiErr)
assert.NotNil(t, resp)
assert.NotNil(t, r)
assert.EqualValues(t, r, daemon.SearchResult{})
assert.EqualValues(t, r, &daemon.SearchResult{})
})
srv.AddResources(t, cmd.at, []*targets.Target{
@@ -154,87 +151,81 @@ func TestSearch(t *testing.T) {
}, boundaryTokenReaderFn)
t.Run("target response from list", func(t *testing.T) {
resp, err := search(ctx, srv.BaseDotDir(), filterBy{
resp, r, apiErr, err := search(ctx, srv.BaseDotDir(), filterBy{
authTokenId: at.Id,
resource: "targets",
})
require.NoError(t, err)
r := daemon.SearchResult{}
apiErr, err := resp.Decode(&r)
assert.NoError(t, err)
assert.Nil(t, apiErr)
assert.NotNil(t, resp)
assert.NotNil(t, r)
assert.Len(t, r.Targets, 2)
})
t.Run("full target response from query", func(t *testing.T) {
resp, err := search(ctx, srv.BaseDotDir(), filterBy{
resp, r, apiErr, err := search(ctx, srv.BaseDotDir(), filterBy{
authTokenId: at.Id,
flagQuery: "id % ttcp",
flagQuery: "id % 'ttcp'",
resource: "targets",
})
require.NoError(t, err)
r := daemon.SearchResult{}
apiErr, err := resp.Decode(&r)
assert.NoError(t, err)
assert.Nil(t, apiErr)
assert.NotNil(t, resp)
assert.NotNil(t, r)
assert.Len(t, r.Targets, 2)
})
t.Run("partial target response from query", func(t *testing.T) {
resp, err := search(ctx, srv.BaseDotDir(), filterBy{
resp, r, apiErr, err := search(ctx, srv.BaseDotDir(), filterBy{
authTokenId: at.Id,
flagQuery: "id % ttcp_1234567890",
flagQuery: "id % 'ttcp_1234567890'",
resource: "targets",
})
require.NoError(t, err)
r := daemon.SearchResult{}
apiErr, err := resp.Decode(&r)
assert.NoError(t, err)
assert.Nil(t, apiErr)
assert.NotNil(t, resp)
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.BaseDotDir(), filterBy{
resp, r, apiErr, err := search(ctx, srv.BaseDotDir(), filterBy{
authTokenId: at.Id,
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, resp)
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.BaseDotDir(), filterBy{
resp, r, apiErr, err := search(ctx, srv.BaseDotDir(), filterBy{
authTokenId: at.Id,
flagQuery: "id % sess",
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, resp)
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.BaseDotDir(), filterBy{
resp, r, apiErr, err := search(ctx, srv.BaseDotDir(), filterBy{
authTokenId: at.Id,
flagQuery: "id % sess_1234567890",
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, resp)
assert.NotNil(t, r)
assert.Len(t, r.Sessions, 1)
})
@@ -350,7 +350,7 @@ func TestRepository_QueryTargets(t *testing.T) {
require.NoError(t, r.AddKeyringToken(ctx, addr, kt1))
require.NoError(t, r.AddKeyringToken(ctx, addr, kt2))
query := `(name % name1 or name % name2) and scope_id = "p_123"`
query := `(name % 'name1' or name % 'name2') and scope_id = "p_123"`
errorCases := []struct {
name string
@@ -0,0 +1,108 @@
package client
import (
"context"
"encoding/json"
"fmt"
"net"
"net/http"
"net/url"
"time"
"github.com/hashicorp/boundary/api"
"github.com/hashicorp/boundary/internal/clientcache/internal/daemon"
"github.com/hashicorp/boundary/internal/errors"
"github.com/hashicorp/boundary/version"
"github.com/hashicorp/go-cleanhttp"
"github.com/hashicorp/go-retryablehttp"
)
const hostHeader = "api.boundary.localhost"
type Client struct {
client *retryablehttp.Client
domainSocketPath string
}
// New returns a client which will always send requests over the unix socket
// specified in the provided address. If the address doesn't have the "unix"
// schema or the path is unset, an error is returned.
func New(ctx context.Context, address *url.URL) (*Client, error) {
const op = "client.New"
switch {
case address == nil:
return nil, errors.New(ctx, errors.InvalidParameter, op, "address is nil")
case address.Scheme != "unix":
return nil, errors.New(ctx, errors.InvalidParameter, op, "address does not have a unix schema")
case address.Path == "":
return nil, errors.New(ctx, errors.InvalidParameter, op, "address path is empty")
}
c := &retryablehttp.Client{
HTTPClient: cleanhttp.DefaultClient(),
RetryWaitMin: 100 * time.Millisecond,
RetryWaitMax: 1500 * time.Millisecond,
RetryMax: 6,
CheckRetry: retryablehttp.DefaultRetryPolicy,
Backoff: retryablehttp.DefaultBackoff,
ErrorHandler: retryablehttp.PassthroughErrorHandler,
}
transport := c.HTTPClient.Transport.(*http.Transport)
transport.DialContext = func(ctx context.Context, n string, a string) (net.Conn, error) {
dialer := net.Dialer{}
return dialer.DialContext(ctx, "unix", address.Path)
}
return &Client{client: c, domainSocketPath: address.Path}, nil
}
// Get sends a GET http request to the provided path. The vals provided are
// encoded and attached to the request if present.
func (c *Client) Get(ctx context.Context, path string, vals *url.Values) (*api.Response, error) {
req := request(ctx, "GET", path)
if vals != nil {
req.URL.RawQuery = vals.Encode()
}
resp, err := c.client.Do(req)
if err != nil {
return nil, err
}
return api.NewResponse(resp), nil
}
// Post sends a POST http request to the provided path. The body is marshaled
// to json and added to the request body.
func (c *Client) Post(ctx context.Context, path string, body any) (*api.Response, error) {
req := request(ctx, "POST", path)
if body != nil {
b, err := json.Marshal(body)
if err != nil {
return nil, fmt.Errorf("error marshaling body: %w", err)
}
req.SetBody(b)
}
resp, err := c.client.Do(req)
if err != nil {
return nil, err
}
return api.NewResponse(resp), nil
}
// request returns a retryablehttp.Request with the url set to the domain socket
// associated with this client and proper headers set for client daemon requests.
func request(ctx context.Context, method, path string) *retryablehttp.Request {
req := &http.Request{
Method: method,
URL: &url.URL{
Scheme: "http",
Host: hostHeader,
Path: path,
},
Host: hostHeader,
}
req.Header = http.Header{}
req.Header.Set(daemon.VersionHeaderKey, version.Get().VersionNumber())
req.Header.Set("content-type", "application/json")
return &retryablehttp.Request{
Request: req.Clone(ctx),
}
}
@@ -0,0 +1,168 @@
package client
import (
"context"
"fmt"
"io"
"net"
"net/http"
"net/url"
"path/filepath"
"sync"
"testing"
"github.com/hashicorp/boundary/internal/clientcache/internal/daemon"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestClient_New(t *testing.T) {
ctx := context.Background()
t.Run("nil addr", func(t *testing.T) {
_, err := New(ctx, nil)
assert.ErrorContains(t, err, "address is nil")
})
t.Run("schema not unix", func(t *testing.T) {
u, err := url.Parse("http://127.0.0.1/v2/something")
require.NoError(t, err)
_, err = New(ctx, u)
assert.ErrorContains(t, err, "address does not have a unix schema")
})
t.Run("schema not unix", func(t *testing.T) {
u, err := url.Parse("unix://")
require.NoError(t, err)
_, err = New(ctx, u)
assert.ErrorContains(t, err, "address path is empty")
})
t.Run("success", func(t *testing.T) {
u, err := url.Parse("unix:///User/boundary/test.sock")
require.NoError(t, err)
c, err := New(ctx, u)
assert.NoError(t, err)
assert.NotNil(t, c)
})
}
func TestClient(t *testing.T) {
ctx := context.Background()
testPath := "/v1/test_endpoint"
tmpDir := t.TempDir()
unixSocket := filepath.Join(tmpDir, "test.sock")
l, err := net.Listen("unix", unixSocket)
require.NoError(t, err)
var assertRequest func(*http.Request)
var writeResp func(http.ResponseWriter) = func(w http.ResponseWriter) {
w.WriteHeader(http.StatusNoContent)
}
mux := http.NewServeMux()
mux.HandleFunc(testPath, http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { // Mock for the proxy handler.
assertRequest(req)
writeResp(rw)
}))
var serverShutdown sync.WaitGroup
serverShutdown.Add(1)
go func() {
defer serverShutdown.Done()
http.Serve(l, mux)
}()
u, err := url.Parse(fmt.Sprintf("unix://%s", unixSocket))
require.NoError(t, err)
c, err := New(ctx, u)
require.NoError(t, err)
t.Run("not found", func(t *testing.T) {
r, err := c.Get(ctx, "/unknown/path", nil)
assert.NoError(t, err)
assert.NotNil(t, r)
assert.Equal(t, http.StatusNotFound, r.HttpResponse().StatusCode)
})
t.Run("empty response", func(t *testing.T) {
assertRequest = func(r *http.Request) {
assert.Equal(t, r.Method, http.MethodGet)
assert.NotEmpty(t, r.Header.Get(daemon.VersionHeaderKey))
}
r, err := c.Get(ctx, testPath, nil)
assert.NoError(t, err)
apiErr, err := r.Decode(nil)
assert.NoError(t, err)
assert.Nil(t, apiErr)
assert.NotNil(t, r)
})
t.Run("empty response", func(t *testing.T) {
assertRequest = func(r *http.Request) {
assert.Equal(t, r.Method, http.MethodGet)
assert.NotEmpty(t, r.Header.Get(daemon.VersionHeaderKey))
}
r, err := c.Get(ctx, testPath, nil)
assert.NoError(t, err)
assert.NotNil(t, r)
apiErr, err := r.Decode(nil)
assert.NoError(t, err)
assert.Nil(t, apiErr)
assert.Empty(t, r.Body)
})
t.Run("empty json object", func(t *testing.T) {
assertRequest = func(r *http.Request) {
assert.Equal(t, r.Method, http.MethodGet)
assert.NotEmpty(t, r.Header.Get(daemon.VersionHeaderKey))
}
writeResp = func(w http.ResponseWriter) {
w.Write([]byte("{}"))
w.WriteHeader(http.StatusOK)
}
r, err := c.Get(ctx, testPath, nil)
assert.NoError(t, err)
assert.NotNil(t, r)
apiErr, err := r.Decode(nil)
assert.NoError(t, err)
assert.Nil(t, apiErr)
assert.Equal(t, "{}", string(r.Body.Bytes()))
})
t.Run("query params", func(t *testing.T) {
v := url.Values{}
v.Add("foo", "bar")
assertRequest = func(r *http.Request) {
assert.Equal(t, r.Method, http.MethodGet)
assert.Equal(t, v, r.URL.Query())
assert.NotEmpty(t, r.Header.Get(daemon.VersionHeaderKey))
}
r, err := c.Get(ctx, testPath, &v)
assert.NoError(t, err)
apiErr, err := r.Decode(nil)
assert.NoError(t, err)
assert.Nil(t, apiErr)
assert.NotNil(t, r)
})
t.Run("empty json object", func(t *testing.T) {
assertRequest = func(r *http.Request) {
assert.Equal(t, r.Method, http.MethodPost)
assert.NotEmpty(t, r.Header.Get(daemon.VersionHeaderKey))
b, err := io.ReadAll(r.Body)
require.NoError(t, err)
assert.Equal(t, string(b), `{"foo":"bar"}`)
}
r, err := c.Post(ctx, testPath, map[string]interface{}{"foo": "bar"})
assert.NoError(t, err)
assert.NotNil(t, r)
apiErr, err := r.Decode(nil)
assert.NoError(t, err)
assert.Nil(t, apiErr)
})
l.Close()
serverShutdown.Wait()
}
@@ -0,0 +1,16 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package daemon
import (
"context"
"net/http"
)
// new404Func creates a handler that returns a custom 404 error message.
func new404Func(ctx context.Context) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
writeError(w, "Not found", http.StatusNotFound)
}
}
@@ -260,6 +260,9 @@ func (s *CacheServer) Serve(ctx context.Context, cmd Commander, opt ...Option) e
}
mux.Handle("/v1/stop", versionEnforcement(stopFn))
// Return custom 404 message when requests don't map to any known path.
mux.Handle("/", new404Func(ctx))
logger, err := event.SysEventer().StandardLogger(ctx, "daemon.serve: ", event.ErrorType)
if err != nil {
return errors.Wrap(ctx, err, op)