Files
threshold/internal/cmd/main.go
T
b6df5693b5 Add generic read/update/delete commands (#3992)
* 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>
2023-11-15 19:55:46 -05:00

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