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>
This commit is contained in:
authored and GitHub committed 2023-11-15 19:55:46 -05:00
1 parent 5c84a8861a
commit b6df5693b5
59 files changed
+1032 -857

No files matched your search

+3 -1
View File
@@ -142,12 +142,14 @@ type Command struct {
}
// New returns a new instance of a base.Command type
func NewCommand(ui cli.Ui) *Command {
func NewCommand(ui cli.Ui, opt ...Option) *Command {
opts := getOpts(opt...)
ctx, cancel := context.WithCancel(context.Background())
ret := &Command{
UI: ui,
ShutdownCh: MakeShutdownCh(),
Context: ctx,
FlagId: opts.withImplicitId,
}
go func() {
<-ret.ShutdownCh
+9
View File
@@ -44,6 +44,7 @@ type Options struct {
withStatusCode int
withHostPlugin func() (string, plugin.HostPluginServiceClient)
withEventGating bool
withImplicitId string
}
func getDefaultOptions() Options {
@@ -200,3 +201,11 @@ func WithEventGating(with bool) Option {
o.withEventGating = with
}
}
// WithImplicitId is used when creating the command if we are implicitly
// overriding the ID via a top-level read/update/delete command
func WithImplicitId(with string) Option {
return func(o *Options) {
o.withImplicitId = with
}
}
+237 -223
View File
File diff suppressed because it is too large. Load diff
@@ -24,7 +24,7 @@ type Command struct {
}
func (c *Command) Synopsis() string {
return wordwrap.WrapString("Authenticate the Boundary command-line client", base.TermWidth)
return wordwrap.WrapString("Authenticate the local client", base.TermWidth)
}
func (c *Command) Help() string {
+1 -1
View File
@@ -15,7 +15,7 @@ type Command struct {
}
func (c *Command) Synopsis() string {
return "Manage resources related to Boundary's local configuration"
return "Manage the local client's configuration"
}
func (c *Command) Help() string {
@@ -0,0 +1,44 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package genericcmd
import (
"fmt"
"github.com/hashicorp/boundary/internal/cmd/base"
"github.com/mitchellh/cli"
)
var _ cli.Command = (*Command)(nil)
type Command struct {
*base.Command
Func string
}
func (c *Command) Synopsis() string {
return fmt.Sprintf("Run a generic %s command against a resource", c.Func)
}
func (c *Command) Help() string {
aAn := "a"
if c.Func == "update" {
aAn = "an"
}
return base.WrapForHelpText([]string{
fmt.Sprintf("Usage: boundary %s [resource ID] [args]", c.Func),
"",
fmt.Sprintf(" This command runs %s %s command against the given resource ID. Arguments are the same as the type-specific command.", aAn, c.Func),
"",
fmt.Sprintf(" Note: this command forwards to a type-specific command; help/error/cURL output from that command may show the full command syntax."),
})
}
func (c *Command) Run(args []string) int {
if len(args) > 0 {
return cli.RunResultHelp
}
return base.CommandSuccess
}
+4 -3
View File
@@ -54,9 +54,10 @@ func PopulateCommonFlags(c *base.Command, f *base.FlagSet, resourceType string,
})
case "id":
f.StringVar(&base.StringVar{
Name: "id",
Target: &c.FlagId,
Usage: fmt.Sprintf("ID of the %s on which to operate.", resourceType),
Name: "id",
Target: &c.FlagId,
Default: c.FlagId,
Usage: fmt.Sprintf("ID of the %s on which to operate.", resourceType),
})
case "name":
f.StringVar(&base.StringVar{
+103 -10
View File
@@ -8,15 +8,17 @@ import (
"bytes"
"fmt"
"io"
"io/ioutil"
"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"
)
@@ -85,10 +87,55 @@ func setupEnv(args []string) (retArgs []string, format string, outputCurlString
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
Stdout io.Writer
Stderr io.Writer
Address string
ImplicitId string
}
func Run(args []string) int {
@@ -134,7 +181,7 @@ func RunCustom(args []string, runOpts *RunOptions) int {
uiErrWriter := runOpts.Stderr
if outputCurlString {
uiErrWriter = ioutil.Discard
uiErrWriter = io.Discard
}
ui := &base.BoundaryUI{
@@ -169,6 +216,29 @@ func RunCustom(args []string, runOpts *RunOptions) int {
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"}
@@ -222,15 +292,38 @@ func groupedHelpFunc(f cli.HelpFunc) cli.HelpFunc {
fmt.Fprintf(tw, "Usage: boundary <command> [args]\n")
otherCommands := make([]string, 0, len(commands))
genericCommands := make([]string, 0, 3)
clientCommands := make([]string, 0, 6)
typeSpecificCommands := make([]string, 0, len(commands)-cap(genericCommands)-cap(clientCommands))
for k := range commands {
otherCommands = append(otherCommands, k)
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(otherCommands)
sort.Strings(clientCommands)
fmt.Fprintf(tw, "\n")
fmt.Fprintf(tw, "Commands:\n")
for _, v := range otherCommands {
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])
}
+73
View File
@@ -0,0 +1,73 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package cmd
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestHandleHighLevelShortcuts(t *testing.T) {
t.Parallel()
tests := []struct {
name string
in []string
out []string
expRunOpts bool
}{
{
name: "zero length",
},
{
name: "one length",
in: []string{"foo"},
out: []string{"foo"},
},
{
name: "not a match",
in: []string{"foo", "bar"},
out: []string{"foo", "bar"},
},
{
name: "unhandled prefix",
in: []string{"read", "bar"},
out: []string{"read", "bar"},
},
{
name: "handled read",
in: []string{"read", "hst_1234567890"},
out: []string{"hosts", "read"},
expRunOpts: true,
},
{
name: "handled delete",
in: []string{"delete", "clvlt_1234567890"},
out: []string{"credential-libraries", "delete"},
expRunOpts: true,
},
{
name: "handled update",
in: []string{"update", "g_1234567890"},
out: []string{"groups", "update"},
expRunOpts: true,
},
{
name: "handled subtype update",
in: []string{"update", "credspk_1234567890"},
out: []string{"credentials", "update", "ssh-private-key"},
expRunOpts: true,
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
var runOpts RunOptions
out := handleHighLevelShortcuts(tc.in, &runOpts)
assert.EqualValues(t, tc.out, out)
if tc.expRunOpts {
assert.Equal(t, tc.in[1], runOpts.ImplicitId)
}
})
}
}