| // Copyright (c) HashiCorp, Inc. |
| // SPDX-License-Identifier: MPL-2.0 |
| |
| package command |
| |
| import ( |
| "bufio" |
| "bytes" |
| "fmt" |
| "io" |
| "os" |
| "sort" |
| "strconv" |
| "strings" |
| "text/tabwriter" |
| |
| "github.com/fatih/color" |
| "github.com/hashicorp/vault/api" |
| "github.com/hashicorp/vault/command/token" |
| colorable "github.com/mattn/go-colorable" |
| "github.com/mitchellh/cli" |
| ) |
| |
| type VaultUI struct { |
| cli.Ui |
| format string |
| detailed bool |
| } |
| |
| const ( |
| globalFlagOutputCurlString = "output-curl-string" |
| globalFlagOutputPolicy = "output-policy" |
| globalFlagFormat = "format" |
| globalFlagDetailed = "detailed" |
| ) |
| |
| var globalFlags = []string{ |
| globalFlagOutputCurlString, globalFlagOutputPolicy, globalFlagFormat, globalFlagDetailed, |
| } |
| |
| // 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, detailed bool, outputCurlString bool, outputPolicy bool) { |
| var err error |
| var nextArgFormat bool |
| var haveDetailed bool |
| |
| for _, arg := range args { |
| if nextArgFormat { |
| nextArgFormat = false |
| format = arg |
| continue |
| } |
| |
| if arg == "--" { |
| break |
| } |
| |
| if len(args) == 1 && (arg == "-v" || arg == "-version" || arg == "--version") { |
| args = []string{"version"} |
| break |
| } |
| |
| if isGlobalFlag(arg, globalFlagOutputCurlString) { |
| outputCurlString = true |
| continue |
| } |
| |
| if isGlobalFlag(arg, globalFlagOutputPolicy) { |
| outputPolicy = true |
| continue |
| } |
| |
| // Parse a given flag here, which overrides the env var |
| if isGlobalFlagWithValue(arg, globalFlagFormat) { |
| format = getGlobalFlagValue(arg) |
| } |
| // For backwards compat, it could be specified without an equal sign |
| if isGlobalFlag(arg, globalFlagFormat) { |
| nextArgFormat = true |
| } |
| |
| // Parse a given flag here, which overrides the env var |
| if isGlobalFlagWithValue(arg, globalFlagDetailed) { |
| detailed, err = strconv.ParseBool(getGlobalFlagValue(globalFlagDetailed)) |
| if err != nil { |
| detailed = false |
| } |
| haveDetailed = true |
| } |
| // For backwards compat, it could be specified without an equal sign to enable |
| // detailed output. |
| if isGlobalFlag(arg, globalFlagDetailed) { |
| detailed = true |
| haveDetailed = true |
| } |
| } |
| |
| envVaultFormat := os.Getenv(EnvVaultFormat) |
| // If we did not parse a value, fetch the env var |
| if format == "" && envVaultFormat != "" { |
| format = envVaultFormat |
| } |
| // Lowercase for consistency |
| format = strings.ToLower(format) |
| if format == "" { |
| format = "table" |
| } |
| |
| envVaultDetailed := os.Getenv(EnvVaultDetailed) |
| // If we did not parse a value, fetch the env var |
| if !haveDetailed && envVaultDetailed != "" { |
| detailed, err = strconv.ParseBool(envVaultDetailed) |
| if err != nil { |
| detailed = false |
| } |
| } |
| |
| return args, format, detailed, outputCurlString, outputPolicy |
| } |
| |
| func isGlobalFlag(arg string, flag string) bool { |
| return arg == "-"+flag || arg == "--"+flag |
| } |
| |
| func isGlobalFlagWithValue(arg string, flag string) bool { |
| return strings.HasPrefix(arg, "--"+flag+"=") || strings.HasPrefix(arg, "-"+flag+"=") |
| } |
| |
| func getGlobalFlagValue(arg string) string { |
| _, value, _ := strings.Cut(arg, "=") |
| |
| return value |
| } |
| |
| type RunOptions struct { |
| TokenHelper token.TokenHelper |
| Stdout io.Writer |
| Stderr io.Writer |
| Address string |
| Client *api.Client |
| } |
| |
| func Run(args []string) int { |
| return RunCustom(args, nil) |
| } |
| |
| // RunCustom allows passing in a base command template to pass to other |
| // commands. Currently, this is only used for setting a custom token helper. |
| func RunCustom(args []string, runOpts *RunOptions) int { |
| if runOpts == nil { |
| runOpts = &RunOptions{} |
| } |
| |
| var format string |
| var detailed bool |
| var outputCurlString bool |
| var outputPolicy bool |
| args, format, detailed, outputCurlString, outputPolicy = setupEnv(args) |
| |
| // Don't use color if disabled |
| useColor := true |
| if os.Getenv(EnvVaultCLINoColor) != "" || color.NoColor { |
| useColor = false |
| } |
| |
| if runOpts.Stdout == nil { |
| runOpts.Stdout = os.Stdout |
| } |
| if runOpts.Stderr == nil { |
| runOpts.Stderr = os.Stderr |
| } |
| |
| // Only use colored UI if stdout is a tty, and not disabled |
| if useColor && format == "table" { |
| if f, ok := runOpts.Stdout.(*os.File); ok { |
| runOpts.Stdout = colorable.NewColorable(f) |
| } |
| if f, ok := runOpts.Stderr.(*os.File); ok { |
| runOpts.Stderr = colorable.NewColorable(f) |
| } |
| } else { |
| runOpts.Stdout = colorable.NewNonColorable(runOpts.Stdout) |
| runOpts.Stderr = colorable.NewNonColorable(runOpts.Stderr) |
| } |
| |
| uiErrWriter := runOpts.Stderr |
| if outputCurlString || outputPolicy { |
| uiErrWriter = &bytes.Buffer{} |
| } |
| |
| ui := &VaultUI{ |
| Ui: &cli.ColoredUi{ |
| ErrorColor: cli.UiColorRed, |
| WarnColor: cli.UiColorYellow, |
| Ui: &cli.BasicUi{ |
| Reader: bufio.NewReader(os.Stdin), |
| Writer: runOpts.Stdout, |
| ErrorWriter: uiErrWriter, |
| }, |
| }, |
| format: format, |
| detailed: detailed, |
| } |
| |
| serverCmdUi := &VaultUI{ |
| Ui: &cli.ColoredUi{ |
| ErrorColor: cli.UiColorRed, |
| WarnColor: cli.UiColorYellow, |
| Ui: &cli.BasicUi{ |
| Reader: bufio.NewReader(os.Stdin), |
| Writer: runOpts.Stdout, |
| }, |
| }, |
| format: format, |
| } |
| |
| if _, ok := Formatters[format]; !ok { |
| ui.Error(fmt.Sprintf("Invalid output format: %s", format)) |
| return 1 |
| } |
| |
| commands := initCommands(ui, serverCmdUi, runOpts) |
| |
| hiddenCommands := []string{"version"} |
| |
| cli := &cli.CLI{ |
| Name: "vault", |
| Args: args, |
| Commands: commands, |
| HelpFunc: groupedHelpFunc( |
| cli.BasicHelpFunc("vault"), |
| ), |
| HelpWriter: runOpts.Stdout, |
| ErrorWriter: runOpts.Stderr, |
| HiddenCommands: hiddenCommands, |
| Autocomplete: true, |
| AutocompleteNoDefaultFlags: true, |
| } |
| |
| exitCode, err := cli.Run() |
| if outputCurlString { |
| return generateCurlString(exitCode, runOpts, uiErrWriter.(*bytes.Buffer)) |
| } else if outputPolicy { |
| return generatePolicy(exitCode, runOpts, uiErrWriter.(*bytes.Buffer)) |
| } else if err != nil { |
| fmt.Fprintf(runOpts.Stderr, "Error executing CLI: %s\n", err.Error()) |
| return 1 |
| } |
| |
| return exitCode |
| } |
| |
| var commonCommands = []string{ |
| "read", |
| "write", |
| "delete", |
| "list", |
| "login", |
| "agent", |
| "server", |
| "status", |
| "unwrap", |
| } |
| |
| func groupedHelpFunc(f cli.HelpFunc) cli.HelpFunc { |
| return func(commands map[string]cli.CommandFactory) string { |
| var b bytes.Buffer |
| tw := tabwriter.NewWriter(&b, 0, 2, 6, ' ', 0) |
| |
| fmt.Fprintf(tw, "Usage: vault <command> [args]\n\n") |
| fmt.Fprintf(tw, "Common commands:\n") |
| for _, v := range commonCommands { |
| printCommand(tw, v, commands[v]) |
| } |
| |
| otherCommands := make([]string, 0, len(commands)) |
| for k := range commands { |
| found := false |
| for _, v := range commonCommands { |
| if k == v { |
| found = true |
| break |
| } |
| } |
| |
| if !found { |
| otherCommands = append(otherCommands, k) |
| } |
| } |
| sort.Strings(otherCommands) |
| |
| fmt.Fprintf(tw, "\n") |
| fmt.Fprintf(tw, "Other commands:\n") |
| for _, v := range otherCommands { |
| printCommand(tw, v, commands[v]) |
| } |
| |
| tw.Flush() |
| |
| return strings.TrimSpace(b.String()) |
| } |
| } |
| |
| func printCommand(w io.Writer, name string, cmdFn cli.CommandFactory) { |
| cmd, err := cmdFn() |
| if err != nil { |
| panic(fmt.Sprintf("failed to load %q command: %s", name, err)) |
| } |
| fmt.Fprintf(w, " %s\t%s\n", name, cmd.Synopsis()) |
| } |
| |
| func generateCurlString(exitCode int, runOpts *RunOptions, preParsingErrBuf *bytes.Buffer) int { |
| if exitCode == 0 { |
| fmt.Fprint(runOpts.Stderr, "Could not generate cURL command") |
| return 1 |
| } |
| |
| if api.LastOutputStringError == nil { |
| if exitCode == 127 { |
| // Usage, just pass it through |
| return exitCode |
| } |
| runOpts.Stderr.Write(preParsingErrBuf.Bytes()) |
| runOpts.Stderr.Write([]byte("Unable to generate cURL string from command\n")) |
| return exitCode |
| } |
| |
| cs, err := api.LastOutputStringError.CurlString() |
| if err != nil { |
| runOpts.Stderr.Write([]byte(fmt.Sprintf("Error creating request string: %s\n", err))) |
| return 1 |
| } |
| |
| runOpts.Stdout.Write([]byte(fmt.Sprintf("%s\n", cs))) |
| return 0 |
| } |
| |
| func generatePolicy(exitCode int, runOpts *RunOptions, preParsingErrBuf *bytes.Buffer) int { |
| if exitCode == 0 { |
| fmt.Fprint(runOpts.Stderr, "Could not generate policy") |
| return 1 |
| } |
| |
| if api.LastOutputPolicyError == nil { |
| if exitCode == 127 { |
| // Usage, just pass it through |
| return exitCode |
| } |
| runOpts.Stderr.Write(preParsingErrBuf.Bytes()) |
| runOpts.Stderr.Write([]byte("Unable to generate policy from command\n")) |
| return exitCode |
| } |
| |
| hcl, err := api.LastOutputPolicyError.HCLString() |
| if err != nil { |
| runOpts.Stderr.Write([]byte(fmt.Sprintf("Error assembling policy HCL: %s\n", err))) |
| return 1 |
| } |
| |
| runOpts.Stdout.Write([]byte(fmt.Sprintf("%s\n", hcl))) |
| return 0 |
| } |