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:
59 files changed
+1032
-857
No files matched your search
@@ -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
|
||||
|
||||
@@ -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
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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
@@ -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])
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user