mirror of https://github.com/hashicorp/boundary
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
344 lines
8.5 KiB
344 lines
8.5 KiB
// Copyright (c) HashiCorp, Inc.
|
|
// SPDX-License-Identifier: BUSL-1.1
|
|
|
|
package cmd
|
|
|
|
import (
|
|
"bufio"
|
|
"bytes"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
"text/tabwriter"
|
|
|
|
"github.com/fatih/color"
|
|
"github.com/hashicorp/boundary/api"
|
|
"github.com/hashicorp/boundary/globals"
|
|
"github.com/hashicorp/boundary/internal/cmd/base"
|
|
"github.com/hashicorp/boundary/internal/types/resource"
|
|
colorable "github.com/mattn/go-colorable"
|
|
"github.com/mitchellh/cli"
|
|
)
|
|
|
|
// 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, outputCurlString bool) {
|
|
// handle the workaround for autocomplete install/uninstall not being exported
|
|
if len(args) == 3 &&
|
|
args[0] == "config" &&
|
|
args[1] == "autocomplete" {
|
|
switch args[2] {
|
|
case "install":
|
|
return []string{"-autocomplete-install"}, "table", false
|
|
case "uninstall":
|
|
return []string{"-autocomplete-uninstall"}, "table", false
|
|
}
|
|
}
|
|
|
|
var nextArgFormat bool
|
|
|
|
for _, arg := range args {
|
|
if nextArgFormat {
|
|
nextArgFormat = false
|
|
format = arg
|
|
continue
|
|
}
|
|
|
|
if arg == "--" {
|
|
break
|
|
}
|
|
|
|
if len(args) == 1 &&
|
|
(arg == "-version" ||
|
|
arg == "-v") {
|
|
args = []string{"version"}
|
|
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=")
|
|
}
|
|
// Handle the case where it is specified without an equal sign
|
|
if arg == "-format" {
|
|
nextArgFormat = true
|
|
}
|
|
}
|
|
|
|
envBoundaryCLIFormat := os.Getenv(base.EnvBoundaryCLIFormat)
|
|
// If we did not parse a value, fetch the env var
|
|
if format == "" && envBoundaryCLIFormat != "" {
|
|
format = envBoundaryCLIFormat
|
|
}
|
|
// Lowercase for consistency
|
|
format = strings.ToLower(format)
|
|
if format == "" {
|
|
format = "table"
|
|
}
|
|
|
|
return args, format, outputCurlString
|
|
}
|
|
|
|
func handleHighLevelShortcuts(args []string, runOpts *RunOptions) []string {
|
|
switch len(args) {
|
|
case 0, 1:
|
|
return args
|
|
}
|
|
|
|
switch strings.ToLower(args[0]) {
|
|
case "read", "update", "delete":
|
|
default:
|
|
return args
|
|
}
|
|
|
|
// At this point we know it's something we want to handle in this function
|
|
info := globals.ResourceInfoFromPrefix(args[1])
|
|
if info.Type == resource.Unknown {
|
|
return args
|
|
}
|
|
|
|
newArgs := make([]string, 0, len(args)+5)
|
|
newArgs = append(newArgs,
|
|
// Start with the plural of the resource type ("credential-libraries")
|
|
info.Type.PluralString(),
|
|
// Add the action to take ("update")
|
|
args[0],
|
|
)
|
|
// If it's update, add the subtype
|
|
if info.Subtype != globals.UnknownSubtype &&
|
|
strings.ToLower(args[0]) == "update" {
|
|
newArgs = append(newArgs,
|
|
strings.ReplaceAll(info.Subtype.String(), "_", "-"),
|
|
)
|
|
}
|
|
|
|
// Make sure we override the ID in commands
|
|
runOpts.ImplicitId = args[1]
|
|
|
|
// Now add the rest of the args
|
|
newArgs = append(newArgs,
|
|
args[2:]...,
|
|
)
|
|
|
|
return newArgs
|
|
}
|
|
|
|
type RunOptions struct {
|
|
Stdout io.Writer
|
|
Stderr io.Writer
|
|
Address string
|
|
ImplicitId string
|
|
}
|
|
|
|
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) (exitCode int) {
|
|
if runOpts == nil {
|
|
runOpts = &RunOptions{}
|
|
}
|
|
|
|
var format string
|
|
var outputCurlString bool
|
|
args, format, outputCurlString = setupEnv(args)
|
|
|
|
// Don't use color if disabled
|
|
useColor := true
|
|
if os.Getenv(base.EnvBoundaryCLINoColor) != "" || 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 {
|
|
uiErrWriter = io.Discard
|
|
}
|
|
|
|
ui := &base.BoundaryUI{
|
|
Ui: &cli.ColoredUi{
|
|
ErrorColor: cli.UiColorRed,
|
|
WarnColor: cli.UiColorYellow,
|
|
Ui: &cli.BasicUi{
|
|
Reader: bufio.NewReader(os.Stdin),
|
|
Writer: runOpts.Stdout,
|
|
ErrorWriter: uiErrWriter,
|
|
},
|
|
},
|
|
Format: format,
|
|
}
|
|
|
|
serverCmdUi := &base.BoundaryUI{
|
|
Ui: &cli.ColoredUi{
|
|
ErrorColor: cli.UiColorRed,
|
|
WarnColor: cli.UiColorYellow,
|
|
Ui: &cli.BasicUi{
|
|
Reader: bufio.NewReader(os.Stdin),
|
|
Writer: runOpts.Stdout,
|
|
},
|
|
},
|
|
Format: format,
|
|
}
|
|
|
|
switch format {
|
|
case "table", "json":
|
|
default:
|
|
ui.Error(fmt.Sprintf("Invalid output format: %s", format))
|
|
return 1
|
|
}
|
|
|
|
// For autocompletion we need to manage the COMP_LINE var. That means
|
|
// reading args out of it now and then setting updated args back.
|
|
compLine := os.Getenv("COMP_LINE")
|
|
if compLine != "" {
|
|
point, err := strconv.Atoi(os.Getenv("COMP_POINT"))
|
|
if err != nil {
|
|
point = len(compLine)
|
|
}
|
|
if point != 0 && point < len(compLine) {
|
|
compLine = compLine[:point]
|
|
}
|
|
args = strings.Split(compLine, " ")
|
|
args = args[1:] // elide "boundary" since the function below expects it to not be there
|
|
}
|
|
|
|
args = handleHighLevelShortcuts(args, runOpts)
|
|
|
|
// Set args back
|
|
if compLine != "" {
|
|
// We need to add back "boundary" as well
|
|
os.Setenv("COMP_LINE", strings.Join(append([]string{"boundary"}, args...), " "))
|
|
}
|
|
|
|
initCommands(ui, serverCmdUi, runOpts)
|
|
|
|
hiddenCommands := []string{"version", "ferry", "ferry status", "ferry pause", "ferry resume"}
|
|
|
|
cli := &cli.CLI{
|
|
Name: "boundary",
|
|
Args: args,
|
|
Commands: Commands,
|
|
HelpFunc: groupedHelpFunc(
|
|
cli.BasicHelpFunc("boundary"),
|
|
),
|
|
HelpWriter: runOpts.Stderr,
|
|
HiddenCommands: hiddenCommands,
|
|
Autocomplete: true,
|
|
AutocompleteNoDefaultFlags: true,
|
|
}
|
|
|
|
var err error
|
|
exitCode, err = cli.Run()
|
|
if outputCurlString {
|
|
if exitCode == 0 {
|
|
fmt.Fprint(runOpts.Stderr, "Could not generate cURL command\n")
|
|
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 !strings.Contains(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
|
|
}
|
|
|
|
return exitCode
|
|
}
|
|
|
|
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: boundary <command> [args]\n")
|
|
|
|
genericCommands := make([]string, 0, 3)
|
|
clientCommands := make([]string, 0, 9)
|
|
typeSpecificCommands := make([]string, 0, len(commands)-cap(genericCommands)-cap(clientCommands))
|
|
for k := range commands {
|
|
switch k {
|
|
case "authenticate", "config", "connect", "daemon", "dev", "ferry", "logout", "search", "server":
|
|
clientCommands = append(clientCommands, k)
|
|
case "read", "update", "delete":
|
|
genericCommands = append(genericCommands, k)
|
|
default:
|
|
typeSpecificCommands = append(typeSpecificCommands, k)
|
|
}
|
|
}
|
|
|
|
sort.Strings(clientCommands)
|
|
fmt.Fprintf(tw, "\n")
|
|
fmt.Fprintf(tw, "Local/Client Commands:\n")
|
|
for _, v := range clientCommands {
|
|
printCommand(tw, v, commands[v])
|
|
}
|
|
|
|
sort.Strings(genericCommands)
|
|
fmt.Fprintf(tw, "\n")
|
|
fmt.Fprintf(tw, "Generic Commands:\n")
|
|
for _, v := range genericCommands {
|
|
printCommand(tw, v, commands[v])
|
|
}
|
|
|
|
sort.Strings(typeSpecificCommands)
|
|
fmt.Fprintf(tw, "\n")
|
|
fmt.Fprintf(tw, "Type-Specific Commands:\n")
|
|
for _, v := range typeSpecificCommands {
|
|
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())
|
|
}
|