Skip to content

Commit

Permalink
Add the ability to print curl commands from CLI (#6113)
Browse files Browse the repository at this point in the history
  • Loading branch information
jefferai authored Feb 1, 2019
1 parent d647681 commit f404e0a
Show file tree
Hide file tree
Showing 7 changed files with 218 additions and 7 deletions.
32 changes: 32 additions & 0 deletions api/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,14 @@ type Config struct {
// then that limiter will be used. Note that an empty Limiter
// is equivalent blocking all events.
Limiter *rate.Limiter

// OutputCurlString causes the actual request to return an error of type
// *OutputStringError. Type asserting the error message will allow
// fetching a cURL-compatible string for the operation.
//
// Note: It is not thread-safe to set this and make concurrent requests
// with the same client. Cloning a client will not clone this value.
OutputCurlString bool
}

// TLSConfig contains the parameters needed to configure TLS on the HTTP client
Expand Down Expand Up @@ -438,6 +446,24 @@ func (c *Client) SetClientTimeout(timeout time.Duration) {
c.config.Timeout = timeout
}

func (c *Client) OutputCurlString() bool {
c.modifyLock.RLock()
c.config.modifyLock.RLock()
defer c.config.modifyLock.RUnlock()
c.modifyLock.RUnlock()

return c.config.OutputCurlString
}

func (c *Client) SetOutputCurlString(curl bool) {
c.modifyLock.RLock()
c.config.modifyLock.Lock()
defer c.config.modifyLock.Unlock()
c.modifyLock.RUnlock()

c.config.OutputCurlString = curl
}

// CurrentWrappingLookupFunc sets a lookup function that returns desired wrap TTLs
// for a given operation and path
func (c *Client) CurrentWrappingLookupFunc() WrappingLookupFunc {
Expand Down Expand Up @@ -662,6 +688,7 @@ func (c *Client) RawRequestWithContext(ctx context.Context, r *Request) (*Respon
backoff := c.config.Backoff
httpClient := c.config.HttpClient
timeout := c.config.Timeout
outputCurlString := c.config.OutputCurlString
c.config.modifyLock.RUnlock()

c.modifyLock.RUnlock()
Expand All @@ -688,6 +715,11 @@ START:
return nil, fmt.Errorf("nil request created")
}

if outputCurlString {
LastOutputStringError = &OutputStringError{Request: req}
return nil, LastOutputStringError
}

if timeout != 0 {
ctx, _ = context.WithTimeout(ctx, timeout)
}
Expand Down
69 changes: 69 additions & 0 deletions api/output_string.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package api

import (
"fmt"
"strings"

retryablehttp "github.com/hashicorp/go-retryablehttp"
)

const (
ErrOutputStringRequest = "output a string, please"
)

var (
LastOutputStringError *OutputStringError
)

type OutputStringError struct {
*retryablehttp.Request
parsingError error
parsedCurlString string
}

func (d *OutputStringError) Error() string {
if d.parsedCurlString == "" {
d.parseRequest()
if d.parsingError != nil {
return d.parsingError.Error()
}
}

return ErrOutputStringRequest
}

func (d *OutputStringError) parseRequest() {
body, err := d.Request.BodyBytes()
if err != nil {
d.parsingError = err
return
}

// Build cURL string
d.parsedCurlString = "curl "
d.parsedCurlString = fmt.Sprintf("%s-X %s ", d.parsedCurlString, d.Request.Method)
for k, v := range d.Request.Header {
for _, h := range v {
if strings.ToLower(k) == "x-vault-token" {
h = `$(vault print token)`
}
d.parsedCurlString = fmt.Sprintf("%s-H \"%s: %s\" ", d.parsedCurlString, k, h)
}
}

if len(body) > 0 {
// We need to escape single quotes since that's what we're using to
// quote the body
escapedBody := strings.Replace(string(body), "'", "'\"'\"'", -1)
d.parsedCurlString = fmt.Sprintf("%s-d '%s' ", d.parsedCurlString, escapedBody)
}

d.parsedCurlString = fmt.Sprintf("%s%s", d.parsedCurlString, d.Request.URL.String())
}

func (d *OutputStringError) CurlString() string {
if d.parsedCurlString == "" {
d.parseRequest()
}
return d.parsedCurlString
}
18 changes: 16 additions & 2 deletions command/base.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,9 @@ type BaseCommand struct {
flagTLSSkipVerify bool
flagWrapTTL time.Duration

flagFormat string
flagField string
flagFormat string
flagField string
flagOutputCurlString bool

flagMFA []string

Expand All @@ -78,6 +79,10 @@ func (c *BaseCommand) Client() (*api.Client, error) {
config.Address = c.flagAddress
}

if c.flagOutputCurlString {
config.OutputCurlString = c.flagOutputCurlString
}

// If we need custom TLS configuration, then set it
if c.flagCACert != "" || c.flagCAPath != "" || c.flagClientCert != "" ||
c.flagClientKey != "" || c.flagTLSServerName != "" || c.flagTLSSkipVerify {
Expand Down Expand Up @@ -325,6 +330,15 @@ func (c *BaseCommand) flagSet(bit FlagSetBit) *FlagSets {
Completion: complete.PredictAnything,
Usage: "Supply MFA credentials as part of X-Vault-MFA header.",
})

f.BoolVar(&BoolVar{
Name: "output-curl-string",
Target: &c.flagOutputCurlString,
Default: false,
Usage: "Instead of executing the request, print an equivalent cURL " +
"command string and exit.",
})

}

if bit&(FlagSetOutputField|FlagSetOutputFormat) != 0 {
Expand Down
5 changes: 5 additions & 0 deletions command/commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -423,6 +423,11 @@ func initCommands(ui, serverCmdUi cli.Ui, runOpts *RunOptions) {
BaseCommand: getBaseCommand(),
}, nil
},
"print token": func() (cli.Command, error) {
return &PrintTokenCommand{
BaseCommand: getBaseCommand(),
}, nil
},
"read": func() (cli.Command, error) {
return &ReadCommand{
BaseCommand: getBaseCommand(),
Expand Down
3 changes: 3 additions & 0 deletions command/kv_helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,9 @@ func kvPreflightVersionRequest(client *api.Client, path string) (string, int, er
currentWrappingLookupFunc := client.CurrentWrappingLookupFunc()
client.SetWrappingLookupFunc(nil)
defer client.SetWrappingLookupFunc(currentWrappingLookupFunc)
currentOutputCurlString := client.OutputCurlString()
client.SetOutputCurlString(false)
defer client.SetOutputCurlString(currentOutputCurlString)

r := client.NewRequest("GET", "/v1/sys/internal/ui/mounts/"+path)
resp, err := client.RawRequest(r)
Expand Down
42 changes: 37 additions & 5 deletions command/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"bytes"
"fmt"
"io"
"io/ioutil"
"os"
"sort"
"strings"
Expand All @@ -23,7 +24,7 @@ type VaultUI struct {

// setupEnv parses args and may replace them and sets some env vars to known
// values based on format options
func setupEnv(args []string) (retArgs []string, format string) {
func setupEnv(args []string) (retArgs []string, format string, outputCurlString bool) {
var nextArgFormat bool

for _, arg := range args {
Expand All @@ -42,6 +43,11 @@ func setupEnv(args []string) (retArgs []string, format string) {
break
}

if arg == "-output-curl-string" {
outputCurlString = true
continue
}

// Parse a given flag here, which overrides the env var
if strings.HasPrefix(arg, "--format=") {
format = strings.TrimPrefix(arg, "--format=")
Expand All @@ -66,7 +72,7 @@ func setupEnv(args []string) (retArgs []string, format string) {
format = "table"
}

return args, format
return args, format, outputCurlString
}

type RunOptions struct {
Expand All @@ -89,7 +95,8 @@ func RunCustom(args []string, runOpts *RunOptions) int {
}

var format string
args, format = setupEnv(args)
var outputCurlString bool
args, format, outputCurlString = setupEnv(args)

// Don't use color if disabled
useColor := true
Expand Down Expand Up @@ -117,13 +124,18 @@ func RunCustom(args []string, runOpts *RunOptions) int {
runOpts.Stderr = colorable.NewNonColorable(runOpts.Stderr)
}

uiErrWriter := runOpts.Stderr
if outputCurlString {
uiErrWriter = ioutil.Discard
}

ui := &VaultUI{
Ui: &cli.ColoredUi{
ErrorColor: cli.UiColorRed,
WarnColor: cli.UiColorYellow,
Ui: &cli.BasicUi{
Writer: runOpts.Stdout,
ErrorWriter: runOpts.Stderr,
ErrorWriter: uiErrWriter,
},
},
format: format,
Expand Down Expand Up @@ -168,7 +180,27 @@ func RunCustom(args []string, runOpts *RunOptions) int {
}

exitCode, err := cli.Run()
if err != nil {
if outputCurlString {
if exitCode == 0 {
fmt.Fprint(runOpts.Stderr, "Could not generate cURL command")
return 1
} else {
if api.LastOutputStringError == nil {
if exitCode == 127 {
// Usage, just pass it through
return exitCode
}
fmt.Fprint(runOpts.Stderr, "cURL command not set by API operation; run without -output-curl-string to see the generated error\n")
return exitCode
}
if api.LastOutputStringError.Error() != api.ErrOutputStringRequest {
runOpts.Stdout.Write([]byte(fmt.Sprintf("Error creating request string: %s\n", api.LastOutputStringError.Error())))
return 1
}
runOpts.Stdout.Write([]byte(fmt.Sprintf("%s\n", api.LastOutputStringError.CurlString())))
return 0
}
} else if err != nil {
fmt.Fprintf(runOpts.Stderr, "Error executing CLI: %s\n", err.Error())
return 1
}
Expand Down
56 changes: 56 additions & 0 deletions command/print_token.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package command

import (
"strings"

"github.com/mitchellh/cli"
"github.com/posener/complete"
)

var _ cli.Command = (*PrintTokenCommand)(nil)
var _ cli.CommandAutocomplete = (*PrintTokenCommand)(nil)

type PrintTokenCommand struct {
*BaseCommand
}

func (c *PrintTokenCommand) Synopsis() string {
return "Prints the contents of a policy"
}

func (c *PrintTokenCommand) Help() string {
helpText := `
Usage: vault print token
Prints the value of the Vault token that will be used for commands, after
taking into account the configured token-helper and the environment.
$ vault print token
` + c.Flags().Help()

return strings.TrimSpace(helpText)
}

func (c *PrintTokenCommand) Flags() *FlagSets {
return nil
}

func (c *PrintTokenCommand) AutocompleteArgs() complete.Predictor {
return nil
}

func (c *PrintTokenCommand) AutocompleteFlags() complete.Flags {
return nil
}

func (c *PrintTokenCommand) Run(args []string) int {
client, err := c.Client()
if err != nil {
c.UI.Error(err.Error())
return 2
}

c.UI.Output(client.Token())
return 0
}

0 comments on commit f404e0a

Please sign in to comment.