From 111b8fbecfa17c646cf9d12c232537b4c88dd791 Mon Sep 17 00:00:00 2001 From: Johan Brandhorst-Satzkorn Date: Wed, 15 May 2024 13:10:18 -0700 Subject: [PATCH] internal/cmd/ferry: add sessions command (#4800) --- internal/cmd/commands/ferry/ferry.go | 9 + internal/cmd/commands/ferry/sessions.go | 246 ++++++++++++++++++++++++ internal/cmd/ferry_cmd_darwin.go | 5 + internal/cmd/ferry_cmd_windows.go | 5 + 4 files changed, 265 insertions(+) create mode 100644 internal/cmd/commands/ferry/sessions.go diff --git a/internal/cmd/commands/ferry/ferry.go b/internal/cmd/commands/ferry/ferry.go index 895516355f..0c8574583b 100644 --- a/internal/cmd/commands/ferry/ferry.go +++ b/internal/cmd/commands/ferry/ferry.go @@ -35,6 +35,15 @@ Usage: boundary ferry [sub command] [options] $ boundary ferry status + Pause and resume the daemon: + + $ boundary ferry pause + $ boundary ferry resume + + List active transparent sessions: + + $ boundary ferry sessions + For a full list of examples, please see the documentation. ` diff --git a/internal/cmd/commands/ferry/sessions.go b/internal/cmd/commands/ferry/sessions.go new file mode 100644 index 0000000000..114c1e7e4a --- /dev/null +++ b/internal/cmd/commands/ferry/sessions.go @@ -0,0 +1,246 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package ferry + +import ( + "bytes" + "context" + "encoding/base64" + "encoding/json" + "fmt" + "strings" + "time" + + "github.com/hashicorp/boundary/api" + "github.com/hashicorp/boundary/api/targets" + "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 = (*SessionsCommand)(nil) + _ cli.CommandAutocomplete = (*SessionsCommand)(nil) +) + +type SessionsCommand struct { + *base.Command +} + +func (c *SessionsCommand) Synopsis() string { + return "List active transparent sessions managed by the Ferry daemon." +} + +func (c *SessionsCommand) Help() string { + helpText := ` +Usage: boundary ferry sessions [options] + + List the active transparent sessions: + + $ boundary ferry sessions + + For a full list of examples, please see the documentation. + +` + c.Flags().Help() + return strings.TrimSpace(helpText) +} + +func (c *SessionsCommand) 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 *SessionsCommand) AutocompleteArgs() complete.Predictor { + return complete.PredictNothing +} + +func (c *SessionsCommand) AutocompleteFlags() complete.Flags { + return c.Flags().Completions() +} + +func (c *SessionsCommand) 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.Sessions(ctx) + if err != nil { + c.PrintCliError(err) + return base.CommandCliError + } + if apiErr != nil { + c.PrintApiError(apiErr, "Error from ferry daemon when getting its Sessions") + return base.CommandApiError + } + + switch base.Format(c.UI) { + case "json": + if ok := c.PrintJsonItems(resp); !ok { + return base.CommandCliError + } + default: + c.UI.Output(c.printListTable(result.Items)) + } + return base.CommandSuccess +} + +type Session struct { + Alias string `json:"alias"` + SessionAuthorization struct { + SessionId string `json:"session_id"` + CreatedTime time.Time `json:"created_time"` + Credentials []*targets.SessionCredential `json:"credentials,omitempty"` + } `json:"session_authorization"` +} + +type ListSessionsResponse struct { + Items []*Session `json:"items"` +} + +func (c *SessionsCommand) Sessions(ctx context.Context) (*api.Response, *ListSessionsResponse, *api.Error, error) { + const op = "ferry.(SessionsCommand).Sessions" + 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/sessions"), 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 := &ListSessionsResponse{} + 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 generateCredentialTableOutputSlice(prefixIndent int, creds []*targets.SessionCredential) []string { + var ret []string + prefixString := strings.Repeat(" ", prefixIndent) + + if len(creds) > 0 { + // Add credential header + ret = append(ret, fmt.Sprintf("%sCredentials:", prefixString)) + } + for _, crd := range creds { + credMap := map[string]any{ + "Credential Store ID": crd.CredentialSource.CredentialStoreId, + "Credential Source ID": crd.CredentialSource.Id, + "Credential Store Type": crd.CredentialSource.Type, + } + if crd.CredentialSource.Name != "" { + credMap["Credential Source Name"] = crd.CredentialSource.Name + } + if crd.CredentialSource.Description != "" { + credMap["Credential Source Description"] = crd.CredentialSource.Description + } + if crd.CredentialSource.CredentialType != "" { + credMap["Credential Type"] = crd.CredentialSource.CredentialType + } + maxLength := base.MaxAttributesLength(credMap, nil, nil) + ret = append(ret, + base.WrapMap(2+prefixIndent, maxLength, credMap), + fmt.Sprintf("%s Secret:", prefixString)) + ret = append(ret, + fmtSecretForTable(2+prefixIndent, crd)..., + ) + ret = append(ret, "") + } + + return ret +} + +func fmtSecretForTable(indent int, sc *targets.SessionCredential) []string { + prefixStr := strings.Repeat(" ", indent) + origSecret := []string{fmt.Sprintf("%s %s", prefixStr, sc.Secret.Raw)} + if sc.Credential != nil { + maxLength := 0 + for k := range sc.Credential { + if len(k) > maxLength { + maxLength = len(k) + } + } + return []string{fmt.Sprintf("%s %s", prefixStr, base.WrapMap(2, maxLength+2, sc.Credential))} + } + + in, err := base64.StdEncoding.DecodeString(strings.Trim(string(sc.Secret.Raw), `"`)) + if err != nil { + return origSecret + } + dst := new(bytes.Buffer) + if err := json.Indent(dst, in, fmt.Sprintf("%s ", prefixStr), fmt.Sprintf("%s ", prefixStr)); err != nil { + return origSecret + } + secretStr := strings.Split(dst.String(), "\n") + if len(secretStr) > 0 { + secretStr[0] = fmt.Sprintf("%s %s", prefixStr, secretStr[0]) + } + return secretStr +} + +func (c *SessionsCommand) printListTable(items []*Session) string { + if len(items) == 0 { + return "No sessions found" + } + var output []string + output = []string{ + "", + "Session information:", + } + for i, item := range items { + if i > 0 { + output = append(output, "") + } + output = append(output, + fmt.Sprintf(" Alias: %s", item.Alias), + " Session authorization:", + fmt.Sprintf(" Session ID: %s", item.SessionAuthorization.SessionId), + fmt.Sprintf(" Created time: %s", item.SessionAuthorization.CreatedTime), + ) + if len(item.SessionAuthorization.Credentials) > 0 { + output = append(output, generateCredentialTableOutputSlice(4, item.SessionAuthorization.Credentials)...) + } + } + + return base.WrapForHelpText(output) +} diff --git a/internal/cmd/ferry_cmd_darwin.go b/internal/cmd/ferry_cmd_darwin.go index e7e7d742f4..31fcc83ffa 100644 --- a/internal/cmd/ferry_cmd_darwin.go +++ b/internal/cmd/ferry_cmd_darwin.go @@ -31,5 +31,10 @@ func init() { Command: base.NewCommand(ui), }, nil } + Commands["ferry sessions"] = func() (cli.Command, error) { + return &ferry.SessionsCommand{ + Command: base.NewCommand(ui), + }, nil + } }) } diff --git a/internal/cmd/ferry_cmd_windows.go b/internal/cmd/ferry_cmd_windows.go index 1919873103..b9240d043e 100644 --- a/internal/cmd/ferry_cmd_windows.go +++ b/internal/cmd/ferry_cmd_windows.go @@ -33,5 +33,10 @@ func init() { Command: base.NewCommand(ui), }, nil } + Commands["ferry sessions"] = func() (cli.Command, error) { + return &ferry.SessionsCommand{ + Command: base.NewCommand(ui), + }, nil + } }) }