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:
1 parent
8af20349e7
commit
b71d134e06
17 files changed
+500
-273
No files matched your search
+12
-38
@@ -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
@@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in new issue
Block a user