Add the ability to print curl commands from CLI (#6113)

This commit is contained in:
Jeff Mitchell
2019-02-01 17:13:51 -05:00
committed by GitHub
parent d647681a37
commit f404e0acd2
7 changed files with 218 additions and 7 deletions

View File

@@ -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
@@ -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 {
@@ -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()
@@ -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)
}

69
api/output_string.go Normal file
View File

@@ -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
}

View File

@@ -52,6 +52,7 @@ type BaseCommand struct {
flagFormat string
flagField string
flagOutputCurlString bool
flagMFA []string
@@ -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 {
@@ -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 {

View File

@@ -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(),

View File

@@ -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)

View File

@@ -4,6 +4,7 @@ import (
"bytes"
"fmt"
"io"
"io/ioutil"
"os"
"sort"
"strings"
@@ -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 {
@@ -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=")
@@ -66,7 +72,7 @@ func setupEnv(args []string) (retArgs []string, format string) {
format = "table"
}
return args, format
return args, format, outputCurlString
}
type RunOptions struct {
@@ -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
@@ -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,
@@ -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
}

56
command/print_token.go Normal file
View File

@@ -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
}