* Add generic read/update/delete commands This adds three generic commands that use the resource prefix to determine the subcommand to run and pass through args over to that subcommand. Since this occurs very early on and to avoid loading essentially every internal resource package, I moved subtype definitions to globals. This replaces #3967 Co-authored-by: Johan Brandhorst-Satzkorn <johan.brandhorst@gmail.com> * De-duplicate Subtype code (#4031) Co-authored-by: Timothy Messier <tim.messier@gmail.com> --------- Co-authored-by: Johan Brandhorst-Satzkorn <johan.brandhorst@gmail.com> Co-authored-by: Timothy Messier <tim.messier@gmail.com>
343 lines
8.4 KiB
Go
343 lines
8.4 KiB
Go
// 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) 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"}
|
|
|
|
cli := &cli.CLI{
|
|
Name: "boundary",
|
|
Args: args,
|
|
Commands: Commands,
|
|
HelpFunc: groupedHelpFunc(
|
|
cli.BasicHelpFunc("boundary"),
|
|
),
|
|
HelpWriter: runOpts.Stderr,
|
|
HiddenCommands: hiddenCommands,
|
|
Autocomplete: true,
|
|
AutocompleteNoDefaultFlags: true,
|
|
}
|
|
|
|
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, 6)
|
|
typeSpecificCommands := make([]string, 0, len(commands)-cap(genericCommands)-cap(clientCommands))
|
|
for k := range commands {
|
|
switch k {
|
|
case "authenticate", "config", "connect", "dev", "logout", "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())
|
|
}
|