Add hidden ferry commands and auto adding tokens to ferry daemon (#4572)
* Add boundary ferry command (#4462) * Add boundary ferry status (#4463) * Add sending auth token automatically to ferry daemon (#4465) * Hide ferry cmds, background error msgs, and dont retry background add-token requests (#4571)
This commit is contained in:
15 files changed
+728
-291
No files matched your search
+18
-45
@@ -16,57 +16,31 @@ import (
|
||||
|
||||
"github.com/hashicorp/boundary/internal/clientcache/internal/daemon"
|
||||
"github.com/hashicorp/boundary/internal/cmd/base"
|
||||
"github.com/hashicorp/boundary/internal/cmd/wrapper"
|
||||
"github.com/mitchellh/cli"
|
||||
)
|
||||
|
||||
// Keep this interface aligned with the interface at internal/cmd/commands.go
|
||||
type cacheEnabledCommand interface {
|
||||
cli.Command
|
||||
BaseCommand() *base.Command
|
||||
}
|
||||
|
||||
// CommandWrapper starts the boundary daemon after the command was Run and attempts
|
||||
// to send the current persona to any running daemon.
|
||||
type CommandWrapper struct {
|
||||
cacheEnabledCommand
|
||||
}
|
||||
|
||||
// Wrap returns a cli.CommandFactory that returns a command wrapped in the CommandWrapper.
|
||||
func Wrap(c cacheEnabledCommand) cli.CommandFactory {
|
||||
return func() (cli.Command, error) {
|
||||
return &CommandWrapper{
|
||||
cacheEnabledCommand: c,
|
||||
}, nil
|
||||
func init() {
|
||||
if err := wrapper.RegisterSuccessfulCommandCallback("clientcache", hook); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
// Run runs the wrapped command and then attempts to start the boundary daemon and send
|
||||
// the current persona
|
||||
func (w *CommandWrapper) Run(args []string) int {
|
||||
// potentially intercept the token in case it isn't stored in the keyring
|
||||
var token string
|
||||
w.cacheEnabledCommand.BaseCommand().Opts = append(w.cacheEnabledCommand.BaseCommand().Opts, base.WithInterceptedToken(&token))
|
||||
r := w.cacheEnabledCommand.Run(args)
|
||||
if w.BaseCommand().FlagSkipCacheDaemon {
|
||||
return r
|
||||
// hook is the callback that is registered with the wrapper package to be called.
|
||||
// The daemon is not started and the token is not added to the cache if the flag
|
||||
// SkipCacheDaemon is set.
|
||||
func hook(ctx context.Context, baseCmd *base.Command, token string) {
|
||||
if baseCmd.FlagSkipCacheDaemon {
|
||||
return
|
||||
}
|
||||
|
||||
if r != base.CommandSuccess {
|
||||
// if we were not successful in running our command, do not continue to
|
||||
// start the daemon and add the token.
|
||||
return r
|
||||
if startDaemon(ctx, baseCmd) {
|
||||
addTokenToCache(ctx, baseCmd, token)
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
if w.startDaemon(ctx) {
|
||||
w.addTokenToCache(ctx, token)
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
// startDaemon attempts to start a daemon and returns true if we have attempted to start
|
||||
// the daemon and either it was successful or it was already running.
|
||||
func (w *CommandWrapper) startDaemon(ctx context.Context) bool {
|
||||
func startDaemon(ctx context.Context, baseCmd *base.Command) bool {
|
||||
// Ignore errors related to checking if the process is already running since
|
||||
// this can fall back to running the process.
|
||||
if dotPath, err := DefaultDotDirectory(ctx); err == nil {
|
||||
@@ -79,7 +53,7 @@ func (w *CommandWrapper) startDaemon(ctx context.Context) bool {
|
||||
|
||||
cmdName, err := os.Executable()
|
||||
if err != nil {
|
||||
w.BaseCommand().UI.Error(fmt.Sprintf("unable to find boundary binary for daemon startup: %s", err.Error()))
|
||||
baseCmd.UI.Error(fmt.Sprintf("unable to find boundary binary for daemon startup: %s", err.Error()))
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -105,13 +79,13 @@ func silentUi() *cli.BasicUi {
|
||||
|
||||
// addTokenToCache runs AddTokenCommand with the token used in, or retrieved by
|
||||
// the wrapped command.
|
||||
func (w *CommandWrapper) addTokenToCache(ctx context.Context, token string) bool {
|
||||
com := AddTokenCommand{Command: base.NewCommand(w.BaseCommand().UI)}
|
||||
client, err := w.BaseCommand().Client()
|
||||
func addTokenToCache(ctx context.Context, baseCmd *base.Command, token string) bool {
|
||||
com := AddTokenCommand{Command: base.NewCommand(baseCmd.UI)}
|
||||
client, err := baseCmd.Client()
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
keyringType, tokName, err := w.BaseCommand().DiscoverKeyringTokenInfo()
|
||||
keyringType, tokName, err := baseCmd.DiscoverKeyringTokenInfo()
|
||||
if err != nil && token == "" {
|
||||
return false
|
||||
}
|
||||
@@ -138,7 +112,6 @@ func (w *CommandWrapper) addTokenToCache(ctx context.Context, token string) bool
|
||||
// provided context is done. It returns an error if the unix socket is not found
|
||||
// before the context is done.
|
||||
func waitForDaemon(ctx context.Context) error {
|
||||
const op = "daemon.waitForDaemon"
|
||||
dotPath, err := DefaultDotDirectory(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -68,6 +68,8 @@ const (
|
||||
EnvKeyringType = "BOUNDARY_KEYRING_TYPE"
|
||||
envRecoveryConfig = "BOUNDARY_RECOVERY_CONFIG"
|
||||
envSkipCacheDaemon = "BOUNDARY_SKIP_CACHE_DAEMON"
|
||||
envSkipFerry = "BOUNDARY_SKIP_FERRY_DAEMON"
|
||||
EnvFerryDaemonPort = "BOUNDARY_FERRY_LISTENING_PORT"
|
||||
|
||||
StoredTokenName = "HashiCorp Boundary Auth Token"
|
||||
)
|
||||
@@ -106,6 +108,9 @@ type Command struct {
|
||||
FlagRecoveryConfig string
|
||||
FlagOutputCurlString bool
|
||||
FlagSkipCacheDaemon bool
|
||||
FlagSkipFerry bool
|
||||
|
||||
FlagFerryDaemonPort uint
|
||||
|
||||
FlagScopeId string
|
||||
FlagScopeName string
|
||||
@@ -479,6 +484,22 @@ func (c *Command) FlagSet(bit FlagSetBit) *FlagSets {
|
||||
EnvVar: envSkipCacheDaemon,
|
||||
Usage: "Skips starting the caching daemon or sending the current used/retrieved token to the caching daemon.",
|
||||
})
|
||||
|
||||
f.BoolVar(&BoolVar{
|
||||
Name: "skip-ferry-daemon",
|
||||
Target: &c.FlagSkipFerry,
|
||||
Default: false,
|
||||
EnvVar: envSkipFerry,
|
||||
Usage: "Skips sending the auth token used for this command to the ferry daemon if it is running.",
|
||||
})
|
||||
|
||||
f.UintVar(&UintVar{
|
||||
Name: "ferry-port",
|
||||
Target: &c.FlagFerryDaemonPort,
|
||||
Default: 9300,
|
||||
EnvVar: EnvFerryDaemonPort,
|
||||
Usage: "The port on which the ferry daemon is listening.",
|
||||
})
|
||||
}
|
||||
|
||||
if bit&FlagSetOutputFormat != 0 {
|
||||
|
||||
+205
-211
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,75 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
package ferry
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/hashicorp/boundary/api"
|
||||
"github.com/hashicorp/go-retryablehttp"
|
||||
)
|
||||
|
||||
// userTokenToAdd is the request body to this handler.
|
||||
type UpsertTokenRequest struct {
|
||||
// BoundaryAddr is a required field for all requests
|
||||
BoundaryAddr string `json:"boundary_addr,omitempty"`
|
||||
// The raw auth token for this user.
|
||||
Token string `json:"token,omitempty"`
|
||||
}
|
||||
|
||||
// addToken builds the UpsertTokenRequest using the client's address and token.
|
||||
// It then sends the request to the ferry daemon.
|
||||
// The passed in cli.Ui is used to print out any errors when looking up the
|
||||
// auth token from the keyring. This allows background operations calling this
|
||||
// method to pass in a silent UI to suppress any output.
|
||||
func addToken(ctx context.Context, apiClient *api.Client, port uint) (*api.Response, *api.Error, error) {
|
||||
pa := UpsertTokenRequest{
|
||||
BoundaryAddr: apiClient.Addr(),
|
||||
}
|
||||
token := apiClient.Token()
|
||||
if token == "" {
|
||||
return nil, nil, errors.New("The client auth token is empty.")
|
||||
}
|
||||
if parts := strings.Split(token, "_"); len(parts) != 3 {
|
||||
return nil, nil, errors.New("The client provided auth token is not in the proper format.")
|
||||
}
|
||||
pa.Token = token
|
||||
|
||||
client := retryablehttp.NewClient()
|
||||
client.Logger = nil
|
||||
client.RetryWaitMin = 100 * time.Millisecond
|
||||
client.RetryWaitMax = 1500 * time.Millisecond
|
||||
|
||||
// TODO (ICU-13140): Until we release ferry feature, do not retry attempts
|
||||
// to connect to the ferry daemon since it adds a noticeably long delay to
|
||||
// the command.
|
||||
client.RetryMax = 0
|
||||
|
||||
req, err := retryablehttp.NewRequestWithContext(ctx, "POST", ferryUrl(port, "v1/tokens"),
|
||||
retryablehttp.ReaderFunc(func() (io.Reader, error) {
|
||||
b, err := json.Marshal(&pa)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error marshaling body: %w", err)
|
||||
}
|
||||
return bytes.NewReader(b), nil
|
||||
}))
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
req.Header.Set("content-type", "application/json")
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("Error when sending request to the ferry daemon: %w.", err)
|
||||
}
|
||||
apiResp := api.NewResponse(resp)
|
||||
apiErr, err := apiResp.Decode(nil)
|
||||
return apiResp, apiErr, err
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
package ferry
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/hashicorp/boundary/internal/cmd/base"
|
||||
"github.com/mitchellh/cli"
|
||||
"github.com/posener/complete"
|
||||
)
|
||||
|
||||
var (
|
||||
_ cli.Command = (*FerryCommand)(nil)
|
||||
_ cli.CommandAutocomplete = (*FerryCommand)(nil)
|
||||
)
|
||||
|
||||
type FerryCommand struct {
|
||||
*base.Command
|
||||
}
|
||||
|
||||
func (c *FerryCommand) Synopsis() string {
|
||||
return "Manages the Boundary ferry daemon"
|
||||
}
|
||||
|
||||
func (c *FerryCommand) Help() string {
|
||||
helpText := `
|
||||
Usage: boundary ferry [sub command] [options]
|
||||
|
||||
This command allows interacting with the Boundary ferry daemon.
|
||||
|
||||
Get the status of the daemon:
|
||||
|
||||
$ boundary ferry status
|
||||
|
||||
For a full list of examples, please see the documentation.
|
||||
|
||||
`
|
||||
return strings.TrimSpace(helpText)
|
||||
}
|
||||
|
||||
func (c *FerryCommand) Flags() *base.FlagSets {
|
||||
return c.FlagSet(base.FlagSetNone)
|
||||
}
|
||||
|
||||
func (c *FerryCommand) AutocompleteArgs() complete.Predictor {
|
||||
return complete.PredictNothing
|
||||
}
|
||||
|
||||
func (c *FerryCommand) AutocompleteFlags() complete.Flags {
|
||||
return c.Flags().Completions()
|
||||
}
|
||||
|
||||
func (c *FerryCommand) Run(args []string) int {
|
||||
return cli.RunResultHelp
|
||||
}
|
||||
|
||||
// ferryUrl constructs the full URL for a ferry request given a port and path.
|
||||
func ferryUrl(port uint, path string) string {
|
||||
return fmt.Sprintf("http://127.0.0.1:%d/%s", port, path)
|
||||
}
|
||||
@@ -0,0 +1,175 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
package ferry
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/hashicorp/boundary/api"
|
||||
"github.com/hashicorp/boundary/internal/cmd/base"
|
||||
"github.com/hashicorp/boundary/internal/errors"
|
||||
"github.com/hashicorp/go-retryablehttp"
|
||||
"github.com/mitchellh/cli"
|
||||
"github.com/posener/complete"
|
||||
)
|
||||
|
||||
var (
|
||||
_ cli.Command = (*StatusCommand)(nil)
|
||||
_ cli.CommandAutocomplete = (*StatusCommand)(nil)
|
||||
)
|
||||
|
||||
type StatusCommand struct {
|
||||
*base.Command
|
||||
}
|
||||
|
||||
func (c *StatusCommand) Synopsis() string {
|
||||
return "Get the status information of the running boundary ferry daemon"
|
||||
}
|
||||
|
||||
func (c *StatusCommand) Help() string {
|
||||
helpText := `
|
||||
Usage: boundary ferry status [options]
|
||||
|
||||
Get the status of the boundary ferry daemon:
|
||||
|
||||
$ boundary ferry status
|
||||
|
||||
For a full list of examples, please see the documentation.
|
||||
|
||||
` + c.Flags().Help()
|
||||
return strings.TrimSpace(helpText)
|
||||
}
|
||||
|
||||
func (c *StatusCommand) Flags() *base.FlagSets {
|
||||
set := c.FlagSet(base.FlagSetOutputFormat)
|
||||
f := set.NewFlagSet("Client Options")
|
||||
|
||||
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.UintVar(&base.UintVar{
|
||||
Name: "ferry-port",
|
||||
Target: &c.FlagFerryDaemonPort,
|
||||
Default: 9300,
|
||||
EnvVar: base.EnvFerryDaemonPort,
|
||||
Usage: "The port on which the ferry daemon is listening.",
|
||||
})
|
||||
|
||||
return set
|
||||
}
|
||||
|
||||
func (c *StatusCommand) AutocompleteArgs() complete.Predictor {
|
||||
return complete.PredictNothing
|
||||
}
|
||||
|
||||
func (c *StatusCommand) AutocompleteFlags() complete.Flags {
|
||||
return c.Flags().Completions()
|
||||
}
|
||||
|
||||
func (c *StatusCommand) Run(args []string) int {
|
||||
ctx := c.Context
|
||||
f := c.Flags()
|
||||
if err := f.Parse(args); err != nil {
|
||||
c.PrintCliError(err)
|
||||
return base.CommandUserError
|
||||
}
|
||||
|
||||
resp, result, apiErr, err := c.Status(ctx)
|
||||
if err != nil {
|
||||
c.PrintCliError(err)
|
||||
return base.CommandCliError
|
||||
}
|
||||
if apiErr != nil {
|
||||
c.PrintApiError(apiErr, "Error from ferry daemon when getting its status")
|
||||
return base.CommandApiError
|
||||
}
|
||||
|
||||
switch base.Format(c.UI) {
|
||||
case "json":
|
||||
if ok := c.PrintJsonItem(resp); !ok {
|
||||
return base.CommandCliError
|
||||
}
|
||||
default:
|
||||
c.UI.Output(printStatusTable(result))
|
||||
}
|
||||
return base.CommandSuccess
|
||||
}
|
||||
|
||||
type GetStatusResponse struct {
|
||||
BoundaryAddr string `json:"boundary_addr"`
|
||||
AuthTokenId string `json:"auth_token_id"`
|
||||
AuthTokenExpiry time.Time `json:"auth_token_expiry"`
|
||||
Version string `json:"version"`
|
||||
Errors []string `json:"errors"`
|
||||
Warnings []string `json:"warnings"`
|
||||
}
|
||||
|
||||
func (c *StatusCommand) Status(ctx context.Context) (*api.Response, *GetStatusResponse, *api.Error, error) {
|
||||
const op = "ferry.(StatusCommand).Status"
|
||||
client := retryablehttp.NewClient()
|
||||
client.Logger = nil
|
||||
client.RetryWaitMin = 100 * time.Millisecond
|
||||
client.RetryWaitMax = 1500 * time.Millisecond
|
||||
|
||||
req, err := retryablehttp.NewRequestWithContext(ctx, "GET", ferryUrl(c.FlagFerryDaemonPort, "v1/status"), nil)
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
req.Header.Set("content-type", "application/json")
|
||||
|
||||
if c.FlagOutputCurlString {
|
||||
api.LastOutputStringError = &api.OutputStringError{Request: req}
|
||||
return nil, nil, nil, api.LastOutputStringError
|
||||
}
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return nil, nil, nil, errors.Wrap(ctx, err, op, errors.WithMsg("client do failed"))
|
||||
}
|
||||
apiResp := api.NewResponse(resp)
|
||||
|
||||
res := &GetStatusResponse{}
|
||||
apiErr, err := apiResp.Decode(&res)
|
||||
if err != nil {
|
||||
return nil, nil, nil, fmt.Errorf("Error when sending request to the ferry daemon: %w.", err)
|
||||
}
|
||||
if apiErr != nil {
|
||||
return apiResp, nil, apiErr, nil
|
||||
}
|
||||
return apiResp, res, nil, nil
|
||||
}
|
||||
|
||||
func printStatusTable(status *GetStatusResponse) string {
|
||||
nonAttributeMap := map[string]any{
|
||||
"Address": status.BoundaryAddr,
|
||||
"Auth Token Id": status.AuthTokenId,
|
||||
"Auth Token Expiration": time.Until(status.AuthTokenExpiry).Round(time.Second).String(),
|
||||
"Version": status.Version,
|
||||
}
|
||||
|
||||
maxLength := base.MaxAttributesLength(nonAttributeMap, nil, nil)
|
||||
|
||||
ret := []string{
|
||||
"",
|
||||
"Status:",
|
||||
base.WrapMap(2, maxLength+2, nonAttributeMap),
|
||||
}
|
||||
|
||||
if len(status.Errors) > 0 {
|
||||
ret = append(ret, " Recent errors:")
|
||||
ret = append(ret, base.WrapSlice(4, status.Errors))
|
||||
}
|
||||
|
||||
if len(status.Warnings) > 0 {
|
||||
ret = append(ret, " Recent warnings:")
|
||||
ret = append(ret, base.WrapSlice(4, status.Warnings))
|
||||
}
|
||||
return base.WrapForHelpText(ret)
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
package ferry
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/hashicorp/boundary/internal/cmd/base"
|
||||
"github.com/hashicorp/boundary/internal/cmd/wrapper"
|
||||
)
|
||||
|
||||
// TODO (ICU-13140): Remove this and re-enable error output for background
|
||||
// ferry daemon token sending.
|
||||
const allowErrorOutput = false
|
||||
|
||||
func init() {
|
||||
if err := wrapper.RegisterSuccessfulCommandCallback("ferry", hook); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
func hook(ctx context.Context, baseCmd *base.Command, token string) {
|
||||
if baseCmd.FlagSkipFerry {
|
||||
return
|
||||
}
|
||||
client, err := baseCmd.Client()
|
||||
if err != nil && allowErrorOutput {
|
||||
baseCmd.PrintCliError(err)
|
||||
return
|
||||
}
|
||||
if token != "" {
|
||||
client.SetToken(token)
|
||||
}
|
||||
_, apiErr, err := addToken(ctx, client, baseCmd.FlagFerryDaemonPort)
|
||||
if err != nil && allowErrorOutput {
|
||||
baseCmd.PrintCliError(err)
|
||||
}
|
||||
if apiErr != nil && allowErrorOutput {
|
||||
baseCmd.PrintApiError(apiErr, "sending token to ferry daemon in the background")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"github.com/hashicorp/boundary/internal/cmd/base"
|
||||
"github.com/hashicorp/boundary/internal/cmd/commands/ferry"
|
||||
"github.com/mitchellh/cli"
|
||||
)
|
||||
|
||||
func init() {
|
||||
extraCommandsFuncs = append(extraCommandsFuncs, func(ui, serverCmdUi cli.Ui, runOpts *RunOptions) {
|
||||
Commands["ferry"] = func() (cli.Command, error) {
|
||||
return &ferry.FerryCommand{
|
||||
Command: base.NewCommand(ui),
|
||||
}, nil
|
||||
}
|
||||
Commands["ferry status"] = func() (cli.Command, error) {
|
||||
return &ferry.StatusCommand{
|
||||
Command: base.NewCommand(ui),
|
||||
}, nil
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
//go:build amd64 || arm64
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"github.com/hashicorp/boundary/internal/cmd/base"
|
||||
"github.com/hashicorp/boundary/internal/cmd/commands/ferry"
|
||||
"github.com/mitchellh/cli"
|
||||
)
|
||||
|
||||
func init() {
|
||||
extraCommandsFuncs = append(extraCommandsFuncs, func(ui, serverCmdUi cli.Ui, runOpts *RunOptions) {
|
||||
Commands["ferry"] = func() (cli.Command, error) {
|
||||
return &ferry.FerryCommand{
|
||||
Command: base.NewCommand(ui),
|
||||
}, nil
|
||||
}
|
||||
Commands["ferry status"] = func() (cli.Command, error) {
|
||||
return &ferry.StatusCommand{
|
||||
Command: base.NewCommand(ui),
|
||||
}, nil
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -241,7 +241,7 @@ func RunCustom(args []string, runOpts *RunOptions) (exitCode int) {
|
||||
|
||||
initCommands(ui, serverCmdUi, runOpts)
|
||||
|
||||
hiddenCommands := []string{"version"}
|
||||
hiddenCommands := []string{"version", "ferry", "ferry status"}
|
||||
|
||||
cli := &cli.CLI{
|
||||
Name: "boundary",
|
||||
@@ -294,11 +294,11 @@ func groupedHelpFunc(f cli.HelpFunc) cli.HelpFunc {
|
||||
fmt.Fprintf(tw, "Usage: boundary <command> [args]\n")
|
||||
|
||||
genericCommands := make([]string, 0, 3)
|
||||
clientCommands := make([]string, 0, 6)
|
||||
clientCommands := make([]string, 0, 9)
|
||||
typeSpecificCommands := make([]string, 0, len(commands)-cap(genericCommands)-cap(clientCommands))
|
||||
for k := range commands {
|
||||
switch k {
|
||||
case "authenticate", "config", "connect", "daemon", "dev", "logout", "search", "server":
|
||||
case "authenticate", "config", "connect", "daemon", "dev", "ferry", "logout", "search", "server":
|
||||
clientCommands = append(clientCommands, k)
|
||||
case "read", "update", "delete":
|
||||
genericCommands = append(genericCommands, k)
|
||||
|
||||
@@ -11,8 +11,6 @@ import (
|
||||
)
|
||||
|
||||
func init() {
|
||||
clientCacheWrapper = daemonWrap
|
||||
|
||||
extraCommandsFuncs = append(extraCommandsFuncs, func(ui, serverCmdUi cli.Ui, runOpts *RunOptions) {
|
||||
Commands["daemon"] = func() (cli.Command, error) {
|
||||
return &daemon.DaemonCommand{
|
||||
@@ -46,9 +44,3 @@ func init() {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// daemonWrap wraps the provided cacheEnabledCommand with a daemon writer which
|
||||
// conditionally starts the client side daemon after the command completes
|
||||
func daemonWrap(c cacheEnabledCommand) cli.CommandFactory {
|
||||
return daemon.Wrap(c)
|
||||
}
|
||||
@@ -13,8 +13,6 @@ import (
|
||||
)
|
||||
|
||||
func init() {
|
||||
clientCacheWrapper = daemonWrap
|
||||
|
||||
extraCommandsFuncs = append(extraCommandsFuncs, func(ui, serverCmdUi cli.Ui, runOpts *RunOptions) {
|
||||
Commands["daemon"] = func() (cli.Command, error) {
|
||||
return &daemon.DaemonCommand{
|
||||
@@ -48,9 +46,3 @@ func init() {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// daemonWrap wraps the provided cacheEnabledCommand with a daemon writer which
|
||||
// conditionally starts the client side daemon after the command completes
|
||||
func daemonWrap(c cacheEnabledCommand) cli.CommandFactory {
|
||||
return daemon.Wrap(c)
|
||||
}
|
||||
@@ -11,8 +11,6 @@ import (
|
||||
)
|
||||
|
||||
func init() {
|
||||
clientCacheWrapper = daemonWrap
|
||||
|
||||
extraCommandsFuncs = append(extraCommandsFuncs, func(ui, serverCmdUi cli.Ui, runOpts *RunOptions) {
|
||||
Commands["daemon"] = func() (cli.Command, error) {
|
||||
return &daemon.DaemonCommand{
|
||||
@@ -46,9 +44,3 @@ func init() {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// daemonWrap wraps the provided cacheEnabledCommand with a daemon writer which
|
||||
// conditionally starts the client side daemon after the command completes
|
||||
func daemonWrap(c cacheEnabledCommand) cli.CommandFactory {
|
||||
return daemon.Wrap(c)
|
||||
}
|
||||
@@ -13,8 +13,6 @@ import (
|
||||
)
|
||||
|
||||
func init() {
|
||||
clientCacheWrapper = daemonWrap
|
||||
|
||||
extraCommandsFuncs = append(extraCommandsFuncs, func(ui, serverCmdUi cli.Ui, runOpts *RunOptions) {
|
||||
Commands["daemon"] = func() (cli.Command, error) {
|
||||
return &daemon.DaemonCommand{
|
||||
@@ -48,9 +46,3 @@ func init() {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// daemonWrap wraps the provided cacheEnabledCommand with a daemon writer which
|
||||
// conditionally starts the client side daemon after the command completes
|
||||
func daemonWrap(c cacheEnabledCommand) cli.CommandFactory {
|
||||
return daemon.Wrap(c)
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
package wrapper
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"sync"
|
||||
|
||||
"github.com/hashicorp/boundary/internal/cmd/base"
|
||||
"github.com/mitchellh/cli"
|
||||
)
|
||||
|
||||
var ErrCallbackAlreadyRegistered = errors.New("registered callback already exists")
|
||||
|
||||
// PostSuccessfulCommandCallback is a function that should be run after the
|
||||
// wrapped command completes successfully. token will contain the value for any
|
||||
// new token obtained during the running of the command
|
||||
type PostSuccessfulCommandCallback func(ctx context.Context, badeCmd *base.Command, token string)
|
||||
|
||||
var callbacks sync.Map
|
||||
|
||||
// RegisterSuccessfulCommandCallback registers a callback to be run after a
|
||||
// successful command.
|
||||
func RegisterSuccessfulCommandCallback(name string, fn PostSuccessfulCommandCallback) error {
|
||||
if _, existed := callbacks.Swap(name, fn); existed {
|
||||
return ErrCallbackAlreadyRegistered
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type WrappableCommand interface {
|
||||
cli.Command
|
||||
BaseCommand() *base.Command
|
||||
}
|
||||
|
||||
// CommandWrapper starts the boundary daemon after the command was Run and attempts
|
||||
// to send the current persona to any running daemon.
|
||||
type CommandWrapper struct {
|
||||
WrappableCommand
|
||||
}
|
||||
|
||||
// Wrap returns a cli.CommandFactory that returns a command wrapped in the CommandWrapper.
|
||||
func Wrap(c WrappableCommand) cli.CommandFactory {
|
||||
return func() (cli.Command, error) {
|
||||
return &CommandWrapper{
|
||||
WrappableCommand: c,
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
// Run runs the wrapped command and then attempts to start the boundary daemon and send
|
||||
// the current persona
|
||||
func (w *CommandWrapper) Run(args []string) int {
|
||||
// potentially intercept the token in case it isn't stored in the keyring
|
||||
var token string
|
||||
w.BaseCommand().Opts = append(w.BaseCommand().Opts, base.WithInterceptedToken(&token))
|
||||
r := w.WrappableCommand.Run(args)
|
||||
|
||||
if r != base.CommandSuccess {
|
||||
// if we were not successful in running our command, do not continue to
|
||||
// start the daemon and add the token.
|
||||
return r
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
callbacks.Range(func(key, value any) bool {
|
||||
val := value.(PostSuccessfulCommandCallback)
|
||||
val(ctx, w.BaseCommand(), token)
|
||||
return true
|
||||
})
|
||||
return r
|
||||
}
|
||||
Reference in new issue
Block a user