diff --git a/internal/cmd/commands/ferry/pause.go b/internal/cmd/commands/ferry/pause.go new file mode 100644 index 0000000000..05fb462bce --- /dev/null +++ b/internal/cmd/commands/ferry/pause.go @@ -0,0 +1,136 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package ferry + +import ( + "context" + "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 = (*PauseCommand)(nil) + _ cli.CommandAutocomplete = (*PauseCommand)(nil) +) + +type PauseCommand struct { + *base.Command +} + +func (c *PauseCommand) Synopsis() string { + return "Pauses the running boundary ferry daemon" +} + +func (c *PauseCommand) Help() string { + helpText := ` +Usage: boundary ferry pause + + Pause the boundary ferry daemon: + + $ boundary ferry pause + + For a full list of examples, please see the documentation. + +` + c.Flags().Help() + return strings.TrimSpace(helpText) +} + +func (c *PauseCommand) 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 *PauseCommand) AutocompleteArgs() complete.Predictor { + return complete.PredictNothing +} + +func (c *PauseCommand) AutocompleteFlags() complete.Flags { + return c.Flags().Completions() +} + +func (c *PauseCommand) Run(args []string) int { + ctx := c.Context + f := c.Flags() + if err := f.Parse(args); err != nil { + c.PrintCliError(err) + return base.CommandUserError + } + + resp, apiErr, err := c.Pause(ctx) + if err != nil { + c.PrintCliError(err) + return base.CommandCliError + } + if apiErr != nil { + c.PrintApiError(apiErr, "Error from ferry daemon when attempting to pause") + return base.CommandApiError + } + + switch base.Format(c.UI) { + case "json": + if ok := c.PrintJsonItem(resp); !ok { + return base.CommandCliError + } + default: + c.UI.Output("Ferry has been successfully paused.") + } + return base.CommandSuccess +} + +func (c *PauseCommand) Pause(ctx context.Context) (*api.Response, *api.Error, error) { + const op = "ferry.(PauseCommand).Pause" + client := retryablehttp.NewClient() + client.Logger = nil + client.RetryWaitMin = 100 * time.Millisecond + client.RetryWaitMax = 1000 * time.Millisecond + + req, err := retryablehttp.NewRequestWithContext(ctx, "POST", ferryUrl(c.FlagFerryDaemonPort, "v1/pause"), nil) + if err != nil { + return nil, nil, err + } + + if c.FlagOutputCurlString { + api.LastOutputStringError = &api.OutputStringError{Request: req} + return nil, nil, api.LastOutputStringError + } + + resp, err := client.Do(req) + if err != nil { + return nil, nil, errors.Wrap(ctx, err, op, errors.WithMsg("client do failed")) + } + apiResp := api.NewResponse(resp) + + apiErr, err := apiResp.Decode(nil) + if err != nil { + return nil, nil, errors.Wrap(ctx, err, op, errors.WithMsg("error decoding Resume response")) + } + if apiErr != nil { + return nil, apiErr, nil + } + + return apiResp, nil, nil +} diff --git a/internal/cmd/commands/ferry/resume.go b/internal/cmd/commands/ferry/resume.go new file mode 100644 index 0000000000..c274f8870a --- /dev/null +++ b/internal/cmd/commands/ferry/resume.go @@ -0,0 +1,136 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package ferry + +import ( + "context" + "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 = (*ResumeCommand)(nil) + _ cli.CommandAutocomplete = (*ResumeCommand)(nil) +) + +type ResumeCommand struct { + *base.Command +} + +func (c *ResumeCommand) Synopsis() string { + return "Resumes the paused boundary ferry daemon" +} + +func (c *ResumeCommand) Help() string { + helpText := ` +Usage: boundary ferry resume + + Resume the boundary ferry daemon: + + $ boundary ferry resume + + For a full list of examples, please see the documentation. + +` + c.Flags().Help() + return strings.TrimSpace(helpText) +} + +func (c *ResumeCommand) 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 *ResumeCommand) AutocompleteArgs() complete.Predictor { + return complete.PredictNothing +} + +func (c *ResumeCommand) AutocompleteFlags() complete.Flags { + return c.Flags().Completions() +} + +func (c *ResumeCommand) Run(args []string) int { + ctx := c.Context + f := c.Flags() + if err := f.Parse(args); err != nil { + c.PrintCliError(err) + return base.CommandUserError + } + + resp, apiErr, err := c.Resume(ctx) + if err != nil { + c.PrintCliError(err) + return base.CommandCliError + } + if apiErr != nil { + c.PrintApiError(apiErr, "Error from ferry daemon when attempting to resume") + return base.CommandApiError + } + + switch base.Format(c.UI) { + case "json": + if ok := c.PrintJsonItem(resp); !ok { + return base.CommandCliError + } + default: + c.UI.Output("Ferry has been successfully resumed.") + } + return base.CommandSuccess +} + +func (c *ResumeCommand) Resume(ctx context.Context) (*api.Response, *api.Error, error) { + const op = "ferry.(ResumeCommand).Resume" + client := retryablehttp.NewClient() + client.Logger = nil + client.RetryWaitMin = 100 * time.Millisecond + client.RetryWaitMax = 1000 * time.Millisecond + + req, err := retryablehttp.NewRequestWithContext(ctx, "POST", ferryUrl(c.FlagFerryDaemonPort, "v1/resume"), nil) + if err != nil { + return nil, nil, err + } + + if c.FlagOutputCurlString { + api.LastOutputStringError = &api.OutputStringError{Request: req} + return nil, nil, api.LastOutputStringError + } + + resp, err := client.Do(req) + if err != nil { + return nil, nil, errors.Wrap(ctx, err, op, errors.WithMsg("client do failed")) + } + apiResp := api.NewResponse(resp) + + apiErr, err := apiResp.Decode(nil) + if err != nil { + return nil, nil, errors.Wrap(ctx, err, op, errors.WithMsg("error decoding Resume response")) + } + if apiErr != nil { + return nil, apiErr, nil + } + + return apiResp, nil, nil +} diff --git a/internal/cmd/commands/ferry/status.go b/internal/cmd/commands/ferry/status.go index d04a732257..c3f9fae59f 100644 --- a/internal/cmd/commands/ferry/status.go +++ b/internal/cmd/commands/ferry/status.go @@ -107,6 +107,7 @@ type GetStatusResponse struct { AuthTokenId string `json:"auth_token_id"` AuthTokenExpiry time.Time `json:"auth_token_expiry"` Version string `json:"version"` + Status string `json:"status"` Errors []string `json:"errors"` Warnings []string `json:"warnings"` } @@ -152,6 +153,7 @@ func printStatusTable(status *GetStatusResponse) string { "Auth Token Id": status.AuthTokenId, "Auth Token Expiration": time.Until(status.AuthTokenExpiry).Round(time.Second).String(), "Version": status.Version, + "Status": status.Status, } maxLength := base.MaxAttributesLength(nonAttributeMap, nil, nil) diff --git a/internal/cmd/ferry_cmd_darwin.go b/internal/cmd/ferry_cmd_darwin.go index 91568037fd..e7e7d742f4 100644 --- a/internal/cmd/ferry_cmd_darwin.go +++ b/internal/cmd/ferry_cmd_darwin.go @@ -21,5 +21,15 @@ func init() { Command: base.NewCommand(ui), }, nil } + Commands["ferry pause"] = func() (cli.Command, error) { + return &ferry.PauseCommand{ + Command: base.NewCommand(ui), + }, nil + } + Commands["ferry resume"] = func() (cli.Command, error) { + return &ferry.ResumeCommand{ + Command: base.NewCommand(ui), + }, nil + } }) } diff --git a/internal/cmd/ferry_cmd_windows.go b/internal/cmd/ferry_cmd_windows.go index 55df18e3b5..1919873103 100644 --- a/internal/cmd/ferry_cmd_windows.go +++ b/internal/cmd/ferry_cmd_windows.go @@ -23,5 +23,15 @@ func init() { Command: base.NewCommand(ui), }, nil } + Commands["ferry pause"] = func() (cli.Command, error) { + return &ferry.PauseCommand{ + Command: base.NewCommand(ui), + }, nil + } + Commands["ferry resume"] = func() (cli.Command, error) { + return &ferry.ResumeCommand{ + Command: base.NewCommand(ui), + }, nil + } }) } diff --git a/internal/cmd/main.go b/internal/cmd/main.go index a266a90a8e..27cc984e39 100644 --- a/internal/cmd/main.go +++ b/internal/cmd/main.go @@ -241,7 +241,7 @@ func RunCustom(args []string, runOpts *RunOptions) (exitCode int) { initCommands(ui, serverCmdUi, runOpts) - hiddenCommands := []string{"version", "ferry", "ferry status"} + hiddenCommands := []string{"version", "ferry", "ferry status", "ferry pause", "ferry resume"} cli := &cli.CLI{ Name: "boundary",