Add attribute flag building and parsing (#1562)
This adds a few command sketches but that's not the main point of the PR and filling those out will come in a different PR. This adds a new flag type that can combine inputs to a single target slice, retaining the ability to distinguish where it came from but also keep ordering across different flag names. It also implements a function that does the heuristic parsing with fallback.
This commit is contained in:
1 parent
14cc97b645
commit
a834a14785
13 files changed
+1148
-18
No files matched your search
@@ -74,6 +74,18 @@ func WithFilter(filter string) Option {
|
||||
}
|
||||
}
|
||||
|
||||
func WithAttributes(inAttributes map[string]interface{}) Option {
|
||||
return func(o *options) {
|
||||
o.postMap["attributes"] = inAttributes
|
||||
}
|
||||
}
|
||||
|
||||
func DefaultAttributes() Option {
|
||||
return func(o *options) {
|
||||
o.postMap["attributes"] = nil
|
||||
}
|
||||
}
|
||||
|
||||
func WithDescription(inDescription string) Option {
|
||||
return func(o *options) {
|
||||
o.postMap["description"] = inDescription
|
||||
|
||||
@@ -83,6 +83,10 @@ type Command struct {
|
||||
FlagRecursive bool
|
||||
FlagFilter string
|
||||
|
||||
// Attribute values
|
||||
FlagAttributes string
|
||||
FlagAttrs []CombinedSliceFlagValue
|
||||
|
||||
client *api.Client
|
||||
}
|
||||
|
||||
|
||||
@@ -4,11 +4,14 @@ import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/hashicorp/go-secure-stdlib/parseutil"
|
||||
"github.com/kr/pretty"
|
||||
"github.com/posener/complete"
|
||||
)
|
||||
|
||||
@@ -748,3 +751,118 @@ func (f *FlagSet) Var(value flag.Value, name, usage string) {
|
||||
f.mainSet.Var(value, name, usage)
|
||||
f.flagSet.Var(value, name, usage)
|
||||
}
|
||||
|
||||
// CombinationSliceVar uses a wrapped value to allow storing values from
|
||||
// different flags in one slice. This is useful if you need ordering to be
|
||||
// maintained across flags. It does not currently support env vars.
|
||||
//
|
||||
// If KvSplit is set true, each value will be split on the first = into Key and
|
||||
// Value parts so that validation can happen at parsing time. If you don't want
|
||||
// this kind of behavior, simply combine them, or set KvSplit to false.
|
||||
//
|
||||
// If KeyDelimiter is non-nil (along with KvSplit being true), the string will
|
||||
// be used to split the key. Otherwise, the Keys will be a single-element slice
|
||||
// containing the full value.
|
||||
//
|
||||
// If ProtoCompat is true, the key will be validated against proto3 syntax
|
||||
// requirements for identifiers. If the string is split via KeyDelimiter, each
|
||||
// segment will be evaluated independently.
|
||||
type CombinationSliceVar struct {
|
||||
Name string
|
||||
Aliases []string
|
||||
Usage string
|
||||
Hidden bool
|
||||
Target *[]CombinedSliceFlagValue
|
||||
Completion complete.Predictor
|
||||
KvSplit bool
|
||||
KeyDelimiter *string
|
||||
ProtoCompatKey bool
|
||||
}
|
||||
|
||||
func (f *FlagSet) CombinationSliceVar(i *CombinationSliceVar) {
|
||||
f.VarFlag(&VarFlag{
|
||||
Name: i.Name,
|
||||
Aliases: i.Aliases,
|
||||
Usage: i.Usage,
|
||||
Value: newCombinedSliceValue(i.Name, i.Target, i.Hidden, i.KvSplit, i.KeyDelimiter, i.ProtoCompatKey),
|
||||
Completion: i.Completion,
|
||||
})
|
||||
}
|
||||
|
||||
// CombinedSliceValue holds the raw value (as a string) and the name of the flag
|
||||
// that added it.
|
||||
type CombinedSliceFlagValue struct {
|
||||
Name string
|
||||
Keys []string
|
||||
Value string
|
||||
}
|
||||
|
||||
type combinedSliceValue struct {
|
||||
name string
|
||||
hidden bool
|
||||
kvSplit bool
|
||||
keyDelimiter *string
|
||||
protoCompatKey bool
|
||||
target *[]CombinedSliceFlagValue
|
||||
}
|
||||
|
||||
func newCombinedSliceValue(name string, target *[]CombinedSliceFlagValue, hidden, kvSplit bool, keyDelimiter *string, protoCompatKey bool) *combinedSliceValue {
|
||||
return &combinedSliceValue{
|
||||
name: name,
|
||||
hidden: hidden,
|
||||
kvSplit: kvSplit,
|
||||
keyDelimiter: keyDelimiter,
|
||||
protoCompatKey: protoCompatKey,
|
||||
target: target,
|
||||
}
|
||||
}
|
||||
|
||||
var protoIdentifierRegex = regexp.MustCompile("^[a-zA-Z][A-Za-z0-9_]*$")
|
||||
|
||||
func (c *combinedSliceValue) Set(val string) error {
|
||||
ret := CombinedSliceFlagValue{
|
||||
Name: c.name,
|
||||
Value: strings.TrimSpace(val),
|
||||
}
|
||||
|
||||
if c.kvSplit {
|
||||
kv := strings.SplitN(ret.Value, "=", 2)
|
||||
switch len(kv) {
|
||||
case 0:
|
||||
case 1:
|
||||
ret.Value = strings.TrimSpace(kv[0])
|
||||
default:
|
||||
ret.Keys = []string{kv[0]}
|
||||
if c.keyDelimiter != nil {
|
||||
ret.Keys = strings.Split(kv[0], *c.keyDelimiter)
|
||||
}
|
||||
ret.Value = strings.TrimSpace(kv[1])
|
||||
}
|
||||
}
|
||||
|
||||
// Trim keys
|
||||
for i, key := range ret.Keys {
|
||||
ret.Keys[i] = strings.TrimSpace(key)
|
||||
}
|
||||
|
||||
if c.protoCompatKey {
|
||||
for _, key := range ret.Keys {
|
||||
if !protoIdentifierRegex.Match([]byte(key)) {
|
||||
return fmt.Errorf("key segment %q is invalid", key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var err error
|
||||
if ret.Value, err = parseutil.ParsePath(ret.Value); err != nil && err != parseutil.ErrNotAUrl {
|
||||
return fmt.Errorf("error checking if value is a path: %w", err)
|
||||
}
|
||||
|
||||
*c.target = append(*c.target, ret)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *combinedSliceValue) Get() interface{} { return *c.target }
|
||||
func (c *combinedSliceValue) String() string { return pretty.Sprint(*c.target) }
|
||||
func (c *combinedSliceValue) Example() string { return "" }
|
||||
func (c *combinedSliceValue) Hidden() bool { return c.hidden }
|
||||
@@ -512,6 +512,12 @@ func initCommands(ui, serverCmdUi cli.Ui, runOpts *RunOptions) {
|
||||
Func: "create",
|
||||
}, nil
|
||||
},
|
||||
"host-catalogs create plugin": func() (cli.Command, error) {
|
||||
return &hostcatalogscmd.PluginCommand{
|
||||
Command: base.NewCommand(ui),
|
||||
Func: "create",
|
||||
}, nil
|
||||
},
|
||||
"host-catalogs update": func() (cli.Command, error) {
|
||||
return &hostcatalogscmd.Command{
|
||||
Command: base.NewCommand(ui),
|
||||
@@ -524,6 +530,12 @@ func initCommands(ui, serverCmdUi cli.Ui, runOpts *RunOptions) {
|
||||
Func: "update",
|
||||
}, nil
|
||||
},
|
||||
"host-catalogs update plugin": func() (cli.Command, error) {
|
||||
return &hostcatalogscmd.PluginCommand{
|
||||
Command: base.NewCommand(ui),
|
||||
Func: "update",
|
||||
}, nil
|
||||
},
|
||||
|
||||
"host-sets": func() (cli.Command, error) {
|
||||
return &hostsetscmd.Command{
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
package hostcatalogscmd
|
||||
|
||||
import (
|
||||
"github.com/hashicorp/boundary/internal/cmd/base"
|
||||
)
|
||||
|
||||
func (c *PluginCommand) extraPluginHelpFunc(helpMap map[string]func() string) string {
|
||||
var helpStr string
|
||||
switch c.Func {
|
||||
case "create":
|
||||
helpStr = base.WrapForHelpText([]string{
|
||||
"Usage: boundary host-catalogs create plugin [options] [args]",
|
||||
"",
|
||||
" Create a plugin-type host catalog. Example:",
|
||||
"",
|
||||
` $ boundary host-catalogs create plugin -scope-id p_1234567890 -name prodops -description "Plugin host-catalog for ProdOps"`,
|
||||
"",
|
||||
"",
|
||||
})
|
||||
|
||||
case "update":
|
||||
helpStr = base.WrapForHelpText([]string{
|
||||
"Usage: boundary host-catalogs update plugin [options] [args]",
|
||||
"",
|
||||
" Update a plugin-type host catalog given its ID. Example:",
|
||||
"",
|
||||
` $ boundary host-catalogs update plugin -id hc_1234567890 -name "devops" -description "Plugin host-catalog for DevOps"`,
|
||||
"",
|
||||
"",
|
||||
})
|
||||
}
|
||||
return helpStr + c.Flags().Help()
|
||||
}
|
||||
@@ -0,0 +1,260 @@
|
||||
// Code generated by "make cli"; DO NOT EDIT.
|
||||
package hostcatalogscmd
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/hashicorp/boundary/api"
|
||||
"github.com/hashicorp/boundary/api/hostcatalogs"
|
||||
"github.com/hashicorp/boundary/internal/cmd/base"
|
||||
"github.com/hashicorp/boundary/internal/cmd/common"
|
||||
"github.com/hashicorp/go-secure-stdlib/strutil"
|
||||
"github.com/mitchellh/cli"
|
||||
"github.com/posener/complete"
|
||||
)
|
||||
|
||||
func initPluginFlags() {
|
||||
flagsOnce.Do(func() {
|
||||
extraFlags := extraPluginActionsFlagsMapFunc()
|
||||
for k, v := range extraFlags {
|
||||
flagsPluginMap[k] = append(flagsPluginMap[k], v...)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
var (
|
||||
_ cli.Command = (*PluginCommand)(nil)
|
||||
_ cli.CommandAutocomplete = (*PluginCommand)(nil)
|
||||
)
|
||||
|
||||
type PluginCommand struct {
|
||||
*base.Command
|
||||
|
||||
Func string
|
||||
|
||||
plural string
|
||||
}
|
||||
|
||||
func (c *PluginCommand) AutocompleteArgs() complete.Predictor {
|
||||
initPluginFlags()
|
||||
return complete.PredictAnything
|
||||
}
|
||||
|
||||
func (c *PluginCommand) AutocompleteFlags() complete.Flags {
|
||||
initPluginFlags()
|
||||
return c.Flags().Completions()
|
||||
}
|
||||
|
||||
func (c *PluginCommand) Synopsis() string {
|
||||
if extra := extraPluginSynopsisFunc(c); extra != "" {
|
||||
return extra
|
||||
}
|
||||
|
||||
synopsisStr := "host catalog"
|
||||
|
||||
synopsisStr = fmt.Sprintf("%s %s", "plugin-type", synopsisStr)
|
||||
|
||||
return common.SynopsisFunc(c.Func, synopsisStr)
|
||||
}
|
||||
|
||||
func (c *PluginCommand) Help() string {
|
||||
initPluginFlags()
|
||||
|
||||
var helpStr string
|
||||
helpMap := common.HelpMap("host catalog")
|
||||
|
||||
switch c.Func {
|
||||
default:
|
||||
|
||||
helpStr = c.extraPluginHelpFunc(helpMap)
|
||||
}
|
||||
|
||||
// Keep linter from complaining if we don't actually generate code using it
|
||||
_ = helpMap
|
||||
return helpStr
|
||||
}
|
||||
|
||||
var flagsPluginMap = map[string][]string{
|
||||
|
||||
"create": {"scope-id", "name", "description", "attributes", "attr", "string-attr", "bool-attr", "num-attr"},
|
||||
|
||||
"update": {"id", "name", "description", "version", "attributes", "attr", "string-attr", "bool-attr", "num-attr"},
|
||||
}
|
||||
|
||||
func (c *PluginCommand) Flags() *base.FlagSets {
|
||||
if len(flagsPluginMap[c.Func]) == 0 {
|
||||
return c.FlagSet(base.FlagSetNone)
|
||||
}
|
||||
|
||||
set := c.FlagSet(base.FlagSetHTTP | base.FlagSetClient | base.FlagSetOutputFormat)
|
||||
f := set.NewFlagSet("Command Options")
|
||||
common.PopulateCommonFlags(c.Command, f, "plugin-type host catalog", flagsPluginMap, c.Func)
|
||||
|
||||
f = set.NewFlagSet("Attribute Options")
|
||||
common.PopulateAttributeFlags(c.Command, f, flagsPluginMap, c.Func)
|
||||
|
||||
extraPluginFlagsFunc(c, set, f)
|
||||
|
||||
return set
|
||||
}
|
||||
|
||||
func (c *PluginCommand) Run(args []string) int {
|
||||
initPluginFlags()
|
||||
|
||||
switch c.Func {
|
||||
case "":
|
||||
return cli.RunResultHelp
|
||||
}
|
||||
|
||||
c.plural = "plugin-type host catalog"
|
||||
switch c.Func {
|
||||
case "list":
|
||||
c.plural = "plugin-type host catalogs"
|
||||
}
|
||||
|
||||
f := c.Flags()
|
||||
|
||||
if err := f.Parse(args); err != nil {
|
||||
c.PrintCliError(err)
|
||||
return base.CommandUserError
|
||||
}
|
||||
|
||||
if strutil.StrListContains(flagsPluginMap[c.Func], "id") && c.FlagId == "" {
|
||||
c.PrintCliError(errors.New("ID is required but not passed in via -id"))
|
||||
return base.CommandUserError
|
||||
}
|
||||
|
||||
var opts []hostcatalogs.Option
|
||||
|
||||
if strutil.StrListContains(flagsPluginMap[c.Func], "scope-id") {
|
||||
switch c.Func {
|
||||
case "create":
|
||||
if c.FlagScopeId == "" {
|
||||
c.PrintCliError(errors.New("Scope ID must be passed in via -scope-id or BOUNDARY_SCOPE_ID"))
|
||||
return base.CommandUserError
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
client, err := c.Client()
|
||||
if err != nil {
|
||||
c.PrintCliError(fmt.Errorf("Error creating API client: %s", err.Error()))
|
||||
return base.CommandCliError
|
||||
}
|
||||
hostcatalogsClient := hostcatalogs.NewClient(client)
|
||||
|
||||
switch c.FlagName {
|
||||
case "":
|
||||
case "null":
|
||||
opts = append(opts, hostcatalogs.DefaultName())
|
||||
default:
|
||||
opts = append(opts, hostcatalogs.WithName(c.FlagName))
|
||||
}
|
||||
|
||||
switch c.FlagDescription {
|
||||
case "":
|
||||
case "null":
|
||||
opts = append(opts, hostcatalogs.DefaultDescription())
|
||||
default:
|
||||
opts = append(opts, hostcatalogs.WithDescription(c.FlagDescription))
|
||||
}
|
||||
|
||||
switch c.FlagRecursive {
|
||||
case true:
|
||||
opts = append(opts, hostcatalogs.WithRecursive(true))
|
||||
}
|
||||
|
||||
if c.FlagFilter != "" {
|
||||
opts = append(opts, hostcatalogs.WithFilter(c.FlagFilter))
|
||||
}
|
||||
|
||||
var version uint32
|
||||
|
||||
switch c.Func {
|
||||
case "update":
|
||||
switch c.FlagVersion {
|
||||
case 0:
|
||||
opts = append(opts, hostcatalogs.WithAutomaticVersioning(true))
|
||||
default:
|
||||
version = uint32(c.FlagVersion)
|
||||
}
|
||||
}
|
||||
|
||||
if err := common.HandleAttributeFlags(
|
||||
c.Command,
|
||||
"attr",
|
||||
func() {
|
||||
hostcatalogs.DefaultAttributes()
|
||||
},
|
||||
func(in map[string]interface{}) {
|
||||
hostcatalogs.WithAttributes(in)
|
||||
}); err != nil {
|
||||
c.PrintCliError(fmt.Errorf("Error evaluating attribute flags to: %s", err.Error()))
|
||||
return base.CommandCliError
|
||||
}
|
||||
|
||||
if ok := extraPluginFlagsHandlingFunc(c, f, &opts); !ok {
|
||||
return base.CommandUserError
|
||||
}
|
||||
|
||||
var result api.GenericResult
|
||||
|
||||
switch c.Func {
|
||||
|
||||
case "create":
|
||||
result, err = hostcatalogsClient.Create(c.Context, "plugin", c.FlagScopeId, opts...)
|
||||
|
||||
case "update":
|
||||
result, err = hostcatalogsClient.Update(c.Context, c.FlagId, version, opts...)
|
||||
|
||||
}
|
||||
|
||||
result, err = executeExtraPluginActions(c, result, err, hostcatalogsClient, version, opts)
|
||||
|
||||
if err != nil {
|
||||
if apiErr := api.AsServerError(err); apiErr != nil {
|
||||
var opts []base.Option
|
||||
|
||||
c.PrintApiError(apiErr, fmt.Sprintf("Error from controller when performing %s on %s", c.Func, c.plural), opts...)
|
||||
return base.CommandApiError
|
||||
}
|
||||
c.PrintCliError(fmt.Errorf("Error trying to %s %s: %s", c.Func, c.plural, err.Error()))
|
||||
return base.CommandCliError
|
||||
}
|
||||
|
||||
output, err := printCustomPluginActionOutput(c)
|
||||
if err != nil {
|
||||
c.PrintCliError(err)
|
||||
return base.CommandUserError
|
||||
}
|
||||
if output {
|
||||
return base.CommandSuccess
|
||||
}
|
||||
|
||||
switch c.Func {
|
||||
}
|
||||
|
||||
switch base.Format(c.UI) {
|
||||
case "table":
|
||||
c.UI.Output(printItemTable(result))
|
||||
|
||||
case "json":
|
||||
if ok := c.PrintJsonItem(result); !ok {
|
||||
return base.CommandCliError
|
||||
}
|
||||
}
|
||||
|
||||
return base.CommandSuccess
|
||||
}
|
||||
|
||||
var (
|
||||
extraPluginActionsFlagsMapFunc = func() map[string][]string { return nil }
|
||||
extraPluginSynopsisFunc = func(*PluginCommand) string { return "" }
|
||||
extraPluginFlagsFunc = func(*PluginCommand, *base.FlagSets, *base.FlagSet) {}
|
||||
extraPluginFlagsHandlingFunc = func(*PluginCommand, *base.FlagSets, *[]hostcatalogs.Option) bool { return true }
|
||||
executeExtraPluginActions = func(_ *PluginCommand, inResult api.GenericResult, inErr error, _ *hostcatalogs.Client, _ uint32, _ []hostcatalogs.Option) (api.GenericResult, error) {
|
||||
return inResult, inErr
|
||||
}
|
||||
printCustomPluginActionOutput = func(*PluginCommand) (bool, error) { return false, nil }
|
||||
)
|
||||
@@ -13,7 +13,7 @@ func (c *StaticCommand) extraStaticHelpFunc(helpMap map[string]func() string) st
|
||||
"",
|
||||
" Create a static-type host catalog. Example:",
|
||||
"",
|
||||
` $ boundary host-catalogs create static -name prodops -description "Static host-catalog for ProdOps"`,
|
||||
` $ boundary host-catalogs create static -scope-id p_1234567890 -name prodops -description "Static host-catalog for ProdOps"`,
|
||||
"",
|
||||
"",
|
||||
})
|
||||
|
||||
@@ -1,9 +1,16 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/hashicorp/boundary/internal/cmd/base"
|
||||
"github.com/hashicorp/go-secure-stdlib/parseutil"
|
||||
"github.com/posener/complete"
|
||||
)
|
||||
|
||||
@@ -93,3 +100,240 @@ func PopulateCommonFlags(c *base.Command, f *base.FlagSet, resourceType string,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func PopulateAttributeFlags(c *base.Command, f *base.FlagSet, flagNames map[string][]string, command string) {
|
||||
keyDelimiter := "."
|
||||
for _, name := range flagNames[command] {
|
||||
switch name {
|
||||
case "attributes":
|
||||
f.StringVar(&base.StringVar{
|
||||
Name: "attributes",
|
||||
Target: &c.FlagAttributes,
|
||||
Usage: `A JSON map value to use as the entirety of the request's attribute map. Usually this will be soured from a file via "file://" syntax.`,
|
||||
})
|
||||
case "attr":
|
||||
f.CombinationSliceVar(&base.CombinationSliceVar{
|
||||
Name: "attr",
|
||||
Target: &c.FlagAttrs,
|
||||
KvSplit: true,
|
||||
KeyDelimiter: &keyDelimiter,
|
||||
ProtoCompatKey: true,
|
||||
Usage: `A key=value attribute to add to the request's attribute map. The type is automatically inferred. Use -string-attr, -bool-attr, or -num-attr if the type needs to be overridden. Can be specified multiple times. Supports sourcing values from files via "file://" and env vars via "env://"`,
|
||||
})
|
||||
case "string-attr":
|
||||
f.CombinationSliceVar(&base.CombinationSliceVar{
|
||||
Name: "string-attr",
|
||||
Target: &c.FlagAttrs,
|
||||
KvSplit: true,
|
||||
KeyDelimiter: &keyDelimiter,
|
||||
ProtoCompatKey: true,
|
||||
Usage: `A key=value string attribute to add to the request's attribute map. Can be specified multiple times. Supports sourcing values from files via "file://" and env vars via "env://"`,
|
||||
})
|
||||
case "bool-attr":
|
||||
f.CombinationSliceVar(&base.CombinationSliceVar{
|
||||
Name: "bool-attr",
|
||||
Target: &c.FlagAttrs,
|
||||
KvSplit: true,
|
||||
KeyDelimiter: &keyDelimiter,
|
||||
ProtoCompatKey: true,
|
||||
Usage: `A key=value bool attribute to add to the request's attribute map. Can be specified multiple times. Supports sourcing values from files via "file://" and env vars via "env://"`,
|
||||
})
|
||||
case "num-attr":
|
||||
f.CombinationSliceVar(&base.CombinationSliceVar{
|
||||
Name: "num-attr",
|
||||
Target: &c.FlagAttrs,
|
||||
KvSplit: true,
|
||||
KeyDelimiter: &keyDelimiter,
|
||||
ProtoCompatKey: true,
|
||||
Usage: `A key=value numeric attribute to add to the request's attribute map. Can be specified multiple times. Supports sourcing values from files via "file://" and env vars via "env://"`,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// From https://stackoverflow.com/a/13340826, modified to remove exponents
|
||||
var jsonNumberRegex = regexp.MustCompile(`^-?(?:0|[1-9]\d*)(?:\.\d+)?$`)
|
||||
|
||||
// HandleAttributeFlags takes in a command and a func to call for default (that
|
||||
// is, set to nil) and non-default values. Suffix can be used to allow this
|
||||
// logic to be used for various needs, e.g. -attr vs -secret.
|
||||
func HandleAttributeFlags(c *base.Command, suffix string, defaultFunc func(), setFunc func(map[string]interface{})) error {
|
||||
// If we were given -attributes, use that as-is
|
||||
switch c.FlagAttributes {
|
||||
case "":
|
||||
// Nothing, continue on
|
||||
case "null":
|
||||
defaultFunc()
|
||||
return nil
|
||||
default:
|
||||
parsedString, err := parseutil.ParsePath(c.FlagAttributes)
|
||||
if err != nil && !errors.Is(err, parseutil.ErrNotAUrl) {
|
||||
return fmt.Errorf("error parsing attributes flag as a URL: %w", err)
|
||||
}
|
||||
// We should be able to parse the string as a JSON object
|
||||
var setMap map[string]interface{}
|
||||
if err := json.Unmarshal([]byte(parsedString), &setMap); err != nil {
|
||||
return fmt.Errorf("error parsing attributes flag as JSON: %w", err)
|
||||
}
|
||||
setFunc(setMap)
|
||||
return nil
|
||||
}
|
||||
|
||||
setMap := map[string]interface{}{}
|
||||
|
||||
for _, attr := range c.FlagAttrs {
|
||||
if len(attr.Keys) == 0 {
|
||||
// No idea why this would happen, but skip it
|
||||
continue
|
||||
}
|
||||
|
||||
var val interface{}
|
||||
var err error
|
||||
|
||||
// First, perform any needed parsing if we are given the type
|
||||
switch attr.Name {
|
||||
case "num-" + suffix:
|
||||
// JSON treats all numbers equally, however, we will try to be a
|
||||
// little better so that we don't include decimals if we don't need
|
||||
// to (and don't have to worry about precision if not necessary)
|
||||
if strings.Contains(attr.Value, ".") {
|
||||
val, err = strconv.ParseFloat(attr.Value, 64)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error parsing attribute %q as a float: %w", attr.Value, err)
|
||||
}
|
||||
} else {
|
||||
val, err = strconv.ParseInt(attr.Value, 10, 64)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error parsing attribute %q as an integer: %w", attr.Value, err)
|
||||
}
|
||||
}
|
||||
|
||||
case "string-" + suffix:
|
||||
val = strings.Trim(attr.Value, `"`)
|
||||
|
||||
case "bool-" + suffix:
|
||||
switch attr.Value {
|
||||
case "true":
|
||||
val = true
|
||||
case "false":
|
||||
val = false
|
||||
default:
|
||||
return fmt.Errorf("error parsing attribute %q as a bool", attr.Value)
|
||||
}
|
||||
|
||||
case suffix:
|
||||
// In this case, use heuristics to just do the right thing the vast
|
||||
// majority of the time
|
||||
switch {
|
||||
case attr.Value == "null": // Explicit null, we want to set to a null value to clear it
|
||||
val = nil
|
||||
|
||||
case attr.Value == "true": // bool true
|
||||
val = true
|
||||
|
||||
case attr.Value == "false": // bool false
|
||||
val = false
|
||||
|
||||
case strings.HasPrefix(attr.Value, `"`): // explicitly quoted string
|
||||
val = strings.Trim(attr.Value, `"`)
|
||||
|
||||
case jsonNumberRegex.Match([]byte(strings.Trim(attr.Value, `"`))): // number
|
||||
// Same logic as above
|
||||
if strings.Contains(attr.Value, ".") {
|
||||
val, err = strconv.ParseFloat(attr.Value, 64)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error parsing attribute %q as a float: %w", attr.Value, err)
|
||||
}
|
||||
} else {
|
||||
val, err = strconv.ParseInt(attr.Value, 10, 64)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error parsing attribute %q as an integer: %w", attr.Value, err)
|
||||
}
|
||||
}
|
||||
|
||||
case strings.HasPrefix(attr.Value, "["): // serialized JSON array
|
||||
var s []interface{}
|
||||
u := json.NewDecoder(bytes.NewBufferString(attr.Value))
|
||||
u.UseNumber()
|
||||
if err := u.Decode(&s); err != nil {
|
||||
return fmt.Errorf("error parsing attribute %q as a json array: %w", attr.Value, err)
|
||||
}
|
||||
val = s
|
||||
|
||||
case strings.HasPrefix(attr.Value, "{"): // serialized JSON map
|
||||
var m map[string]interface{}
|
||||
u := json.NewDecoder(bytes.NewBufferString(attr.Value))
|
||||
u.UseNumber()
|
||||
if err := u.Decode(&m); err != nil {
|
||||
return fmt.Errorf("error parsing attribute %q as a json map: %w", attr.Value, err)
|
||||
}
|
||||
val = m
|
||||
|
||||
default:
|
||||
// Default is to treat as a string value
|
||||
val = attr.Value
|
||||
}
|
||||
default:
|
||||
return fmt.Errorf("unknown attribute flag %q", attr.Name)
|
||||
}
|
||||
|
||||
// Now we have to insert it in the right position in the attribute map
|
||||
currMap := setMap
|
||||
for i, segment := range attr.Keys {
|
||||
if segment == "" {
|
||||
return fmt.Errorf("key segment %q for value %q is empty", segment, attr.Value)
|
||||
}
|
||||
|
||||
switch {
|
||||
case i == len(attr.Keys)-1:
|
||||
// If we get an explicit "null" override whatever is currently
|
||||
// there
|
||||
if val == nil {
|
||||
currMap[segment] = nil
|
||||
break
|
||||
}
|
||||
// We're at the last hop, do the actual insertion
|
||||
switch t := currMap[segment].(type) {
|
||||
case nil:
|
||||
// Nothing currently exists
|
||||
currMap[segment] = val
|
||||
|
||||
case []interface{}:
|
||||
// It's already a slice, so just append
|
||||
currMap[segment] = append(t, val)
|
||||
|
||||
default:
|
||||
// It's not a slice, so create a new slice with the
|
||||
// exisitng and new values
|
||||
currMap[segment] = []interface{}{t, val}
|
||||
}
|
||||
|
||||
default:
|
||||
// We need to keep traversing
|
||||
switch t := currMap[segment].(type) {
|
||||
case nil:
|
||||
// We haven't hit this segment before, so create a new
|
||||
// object leading off of it and set it to current
|
||||
newMap := map[string]interface{}{}
|
||||
currMap[segment] = newMap
|
||||
currMap = newMap
|
||||
|
||||
case map[string]interface{}:
|
||||
// We've seen this before and already have a map so just set
|
||||
// that as our new location
|
||||
currMap = t
|
||||
|
||||
default:
|
||||
// We should only ever be seeing maps if we're not at the
|
||||
// final location
|
||||
return fmt.Errorf("unexpected type for key segment %q: %T", segment, t)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(setMap) > 0 {
|
||||
setFunc(setMap)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,405 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/boundary/internal/cmd/base"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// TestPopulateAttrFlags tests common patterns we'll actually be using. Note
|
||||
// that this is not an exhaustive test of the full CombinationSliceVar
|
||||
// functionality, it's a bit higher level test based on what we'll actually need
|
||||
// and what we'll actually have set.
|
||||
func TestPopulateAttrFlags(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
args []string
|
||||
expected []base.CombinedSliceFlagValue
|
||||
expectedErr string
|
||||
}{
|
||||
{
|
||||
name: "strings-only",
|
||||
args: []string{"-string-attr", "foo=bar", "-string-attr", `bar="baz"`},
|
||||
expected: []base.CombinedSliceFlagValue{
|
||||
{
|
||||
Name: "string-attr",
|
||||
Keys: []string{"foo"},
|
||||
Value: "bar",
|
||||
},
|
||||
{
|
||||
Name: "string-attr",
|
||||
Keys: []string{"bar"},
|
||||
Value: `"baz"`,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "nums-only",
|
||||
args: []string{"-num-attr", "foo=-1.2", "-num-attr", "bar=5"},
|
||||
expected: []base.CombinedSliceFlagValue{
|
||||
{
|
||||
Name: "num-attr",
|
||||
Keys: []string{"foo"},
|
||||
Value: "-1.2",
|
||||
},
|
||||
{
|
||||
Name: "num-attr",
|
||||
Keys: []string{"bar"},
|
||||
Value: "5",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "bools-only",
|
||||
args: []string{"-bool-attr", "foo=true", "-bool-attr", "bar=false"},
|
||||
expected: []base.CombinedSliceFlagValue{
|
||||
{
|
||||
Name: "bool-attr",
|
||||
Keys: []string{"foo"},
|
||||
Value: "true",
|
||||
},
|
||||
{
|
||||
Name: "bool-attr",
|
||||
Keys: []string{"bar"},
|
||||
Value: "false",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "mixed",
|
||||
args: []string{"-num-attr", "foo=9820", "-string-attr", "bar=9820", "-attr", "baz=9820"},
|
||||
expected: []base.CombinedSliceFlagValue{
|
||||
{
|
||||
Name: "num-attr",
|
||||
Keys: []string{"foo"},
|
||||
Value: "9820",
|
||||
},
|
||||
{
|
||||
Name: "string-attr",
|
||||
Keys: []string{"bar"},
|
||||
Value: "9820",
|
||||
},
|
||||
{
|
||||
Name: "attr",
|
||||
Keys: []string{"baz"},
|
||||
Value: "9820",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "mixed-segments",
|
||||
args: []string{"-num-attr", "foo.bar.baz=9820", "-string-attr", "bar.baz.foo=9820", "-attr", "baz.foo.bar=9820"},
|
||||
expected: []base.CombinedSliceFlagValue{
|
||||
{
|
||||
Name: "num-attr",
|
||||
Keys: []string{"foo", "bar", "baz"},
|
||||
Value: "9820",
|
||||
},
|
||||
{
|
||||
Name: "string-attr",
|
||||
Keys: []string{"bar", "baz", "foo"},
|
||||
Value: "9820",
|
||||
},
|
||||
{
|
||||
Name: "attr",
|
||||
Keys: []string{"baz", "foo", "bar"},
|
||||
Value: "9820",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "bad-key-name",
|
||||
args: []string{"-num-attr", "fo-oo=5"},
|
||||
expectedErr: "invalid value",
|
||||
},
|
||||
{
|
||||
name: "bad-key-name-in-segment",
|
||||
args: []string{"-num-attr", "fo.oo-o.o=5"},
|
||||
expectedErr: "invalid value",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
assert, require := assert.New(t), require.New(t)
|
||||
|
||||
// Note: we do the setup on each run to make sure we aren't carrying
|
||||
// state over; just like in the real CLI where each run would have
|
||||
// pristine state.
|
||||
c := new(base.Command)
|
||||
flagSet := c.FlagSet(base.FlagSetNone)
|
||||
f := flagSet.NewFlagSet("Attribute Options")
|
||||
cmd := "create"
|
||||
flagNames := map[string][]string{
|
||||
cmd: {
|
||||
"attributes",
|
||||
"attr",
|
||||
"string-attr",
|
||||
"bool-attr",
|
||||
"num-attr",
|
||||
},
|
||||
}
|
||||
|
||||
PopulateAttributeFlags(c, f, flagNames, cmd)
|
||||
err := flagSet.Parse(tt.args)
|
||||
if tt.expectedErr != "" {
|
||||
require.Error(err)
|
||||
assert.Contains(err.Error(), tt.expectedErr)
|
||||
return
|
||||
}
|
||||
require.NoError(err)
|
||||
assert.Equal(tt.expected, c.FlagAttrs)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestHandleAttributeFlags tests the function that parses types based on
|
||||
// incoming data. It assumes we're coming in with CombinedSliceFlagValues and
|
||||
// validates what comes out -- whether nil func was called or the map function
|
||||
// was called (and its contents).
|
||||
func TestHandleAttributeFlags(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
args []base.CombinedSliceFlagValue
|
||||
expectedMap map[string]interface{}
|
||||
expectedErr string
|
||||
}{
|
||||
{
|
||||
name: "strings-only",
|
||||
args: []base.CombinedSliceFlagValue{
|
||||
{
|
||||
Name: "string-attr",
|
||||
Keys: []string{"foo"},
|
||||
Value: "bar",
|
||||
},
|
||||
{
|
||||
Name: "string-attr",
|
||||
Keys: []string{"bar"},
|
||||
Value: `"baz"`,
|
||||
},
|
||||
},
|
||||
expectedMap: map[string]interface{}{
|
||||
"foo": "bar",
|
||||
"bar": "baz",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "nums-only",
|
||||
args: []base.CombinedSliceFlagValue{
|
||||
{
|
||||
Name: "num-attr",
|
||||
Keys: []string{"foo"},
|
||||
Value: "-1.2",
|
||||
},
|
||||
{
|
||||
Name: "num-attr",
|
||||
Keys: []string{"bar"},
|
||||
Value: "5",
|
||||
},
|
||||
},
|
||||
expectedMap: map[string]interface{}{
|
||||
"foo": float64(-1.2),
|
||||
"bar": int64(5),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "bad-float-num",
|
||||
args: []base.CombinedSliceFlagValue{
|
||||
{
|
||||
Name: "num-attr",
|
||||
Keys: []string{"foo"},
|
||||
Value: "-15d.2",
|
||||
},
|
||||
},
|
||||
expectedErr: "as a float",
|
||||
},
|
||||
{
|
||||
name: "bad-int-num",
|
||||
args: []base.CombinedSliceFlagValue{
|
||||
{
|
||||
Name: "num-attr",
|
||||
Keys: []string{"foo"},
|
||||
Value: "-15d3",
|
||||
},
|
||||
},
|
||||
expectedErr: "as an int",
|
||||
},
|
||||
{
|
||||
name: "bools-only",
|
||||
args: []base.CombinedSliceFlagValue{
|
||||
{
|
||||
Name: "bool-attr",
|
||||
Keys: []string{"foo"},
|
||||
Value: "true",
|
||||
},
|
||||
{
|
||||
Name: "bool-attr",
|
||||
Keys: []string{"bar"},
|
||||
Value: "false",
|
||||
},
|
||||
},
|
||||
expectedMap: map[string]interface{}{
|
||||
"foo": true,
|
||||
"bar": false,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "bad-bool",
|
||||
args: []base.CombinedSliceFlagValue{
|
||||
{
|
||||
Name: "bool-attr",
|
||||
Keys: []string{"foo"},
|
||||
Value: "t",
|
||||
},
|
||||
},
|
||||
expectedErr: "as a bool",
|
||||
},
|
||||
{
|
||||
name: "attr-only",
|
||||
args: []base.CombinedSliceFlagValue{
|
||||
{
|
||||
Name: "attr",
|
||||
Keys: []string{"b1"},
|
||||
Value: "true",
|
||||
},
|
||||
{
|
||||
Name: "attr",
|
||||
Keys: []string{"b2"},
|
||||
Value: "false",
|
||||
},
|
||||
{
|
||||
Name: "attr",
|
||||
Keys: []string{"s1"},
|
||||
Value: "scoopde",
|
||||
},
|
||||
{
|
||||
Name: "attr",
|
||||
Keys: []string{"s2"},
|
||||
Value: `"woop"`,
|
||||
},
|
||||
{
|
||||
Name: "attr",
|
||||
Keys: []string{"n1"},
|
||||
Value: "-1.2",
|
||||
},
|
||||
{
|
||||
Name: "attr",
|
||||
Keys: []string{"n2"},
|
||||
Value: "5",
|
||||
},
|
||||
{
|
||||
Name: "attr",
|
||||
Keys: []string{"a"},
|
||||
Value: `["foo", 1.5, true, ["bar"], {"hip": "hop"}]`,
|
||||
},
|
||||
{
|
||||
Name: "attr",
|
||||
Keys: []string{"nil"},
|
||||
Value: "null",
|
||||
},
|
||||
{
|
||||
Name: "attr",
|
||||
Keys: []string{"m"},
|
||||
Value: `{"b": true, "n": 6, "s": "scoopde", "a": ["bar"], "m": {"hip": "hop"}}`,
|
||||
},
|
||||
},
|
||||
expectedMap: map[string]interface{}{
|
||||
"b1": true,
|
||||
"b2": false,
|
||||
"s1": "scoopde",
|
||||
"s2": "woop",
|
||||
"n1": float64(-1.2),
|
||||
"n2": int64(5),
|
||||
"a": []interface{}{
|
||||
"foo",
|
||||
json.Number("1.5"),
|
||||
true,
|
||||
[]interface{}{"bar"},
|
||||
map[string]interface{}{"hip": "hop"},
|
||||
},
|
||||
"m": map[string]interface{}{
|
||||
"b": true,
|
||||
"n": json.Number("6"),
|
||||
"s": "scoopde",
|
||||
"a": []interface{}{"bar"},
|
||||
"m": map[string]interface{}{"hip": "hop"},
|
||||
},
|
||||
"nil": nil,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "map-array-structure",
|
||||
args: []base.CombinedSliceFlagValue{
|
||||
{
|
||||
Name: "attr",
|
||||
Keys: []string{"bools"},
|
||||
Value: "true",
|
||||
},
|
||||
{
|
||||
Name: "attr",
|
||||
Keys: []string{"bools"},
|
||||
Value: "false",
|
||||
},
|
||||
{
|
||||
Name: "attr",
|
||||
Keys: []string{"strings", "s1"},
|
||||
Value: "scoopde",
|
||||
},
|
||||
{
|
||||
Name: "attr",
|
||||
Keys: []string{"strings", "s2"},
|
||||
Value: `"woop"`,
|
||||
},
|
||||
{
|
||||
Name: "attr",
|
||||
Keys: []string{"numbers", "reps"},
|
||||
Value: "-1.2",
|
||||
},
|
||||
{
|
||||
Name: "attr",
|
||||
Keys: []string{"numbers", "reps"},
|
||||
Value: "5",
|
||||
},
|
||||
{
|
||||
Name: "attr",
|
||||
Keys: []string{"strings", "s2"}, // This will overwrite above!
|
||||
Value: "null",
|
||||
},
|
||||
},
|
||||
expectedMap: map[string]interface{}{
|
||||
"bools": []interface{}{true, false},
|
||||
"strings": map[string]interface{}{
|
||||
"s1": "scoopde",
|
||||
"s2": nil,
|
||||
},
|
||||
"numbers": map[string]interface{}{
|
||||
"reps": []interface{}{float64(-1.2), int64(5)},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
assert, require := assert.New(t), require.New(t)
|
||||
|
||||
// Note: we do the setup on each run to make sure we aren't carrying
|
||||
// state over; just like in the real CLI where each run would have
|
||||
// pristine state.
|
||||
c := new(base.Command)
|
||||
c.FlagAttrs = tt.args
|
||||
var outMap map[string]interface{}
|
||||
|
||||
err := HandleAttributeFlags(c, "attr", func() {}, func(in map[string]interface{}) { outMap = in })
|
||||
if tt.expectedErr != "" {
|
||||
require.Error(err)
|
||||
assert.Contains(err.Error(), tt.expectedErr)
|
||||
return
|
||||
}
|
||||
|
||||
require.NoError(err)
|
||||
assert.Equal(tt.expectedMap, outMap)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -68,6 +68,10 @@ type cmdInfo struct {
|
||||
// This allows the flags to be defined differently from the the attribute
|
||||
// names in the API.
|
||||
PrefixAttributeFieldErrorsWithSubactionPrefix bool
|
||||
|
||||
// HasGenericAttributes controls whether to generate flags for -attributes,
|
||||
// -attr, -string-attr, etc.
|
||||
HasGenericAttributes bool
|
||||
}
|
||||
|
||||
var inputStructs = map[string][]*cmdInfo{
|
||||
@@ -254,6 +258,21 @@ var inputStructs = map[string][]*cmdInfo{
|
||||
VersionedActions: []string{"update"},
|
||||
NeedsSubtypeInCreate: true,
|
||||
},
|
||||
{
|
||||
ResourceType: resource.HostCatalog.String(),
|
||||
Pkg: "hostcatalogs",
|
||||
StdActions: []string{"create", "update"},
|
||||
SubActionPrefix: "plugin",
|
||||
SkipNormalHelp: true,
|
||||
HasExtraHelpFunc: true,
|
||||
HasId: true,
|
||||
HasName: true,
|
||||
HasDescription: true,
|
||||
Container: "Scope",
|
||||
VersionedActions: []string{"update"},
|
||||
NeedsSubtypeInCreate: true,
|
||||
HasGenericAttributes: true,
|
||||
},
|
||||
},
|
||||
"hostsets": {
|
||||
{
|
||||
|
||||
@@ -169,15 +169,16 @@ func (c *{{ camelCase .SubActionPrefix }}Command) Help() string {
|
||||
}
|
||||
|
||||
var flags{{ camelCase .SubActionPrefix }}Map = map[string][]string{
|
||||
{{ range $i, $action := .StdActions }}
|
||||
{{ with $attrFlags := ", \"attributes\", \"attr\", \"string-attr\", \"bool-attr\", \"num-attr\"" }}
|
||||
{{ range $i, $action := $input.StdActions }}
|
||||
{{ if eq $action "create" }}
|
||||
"create": { "{{ kebabCase $input.Container }}-id", "name", "description" },
|
||||
"create": { "{{ kebabCase $input.Container }}-id", "name", "description" {{ if $input.HasGenericAttributes }} {{ $attrFlags }} {{ end }} },
|
||||
{{ end }}
|
||||
{{ if eq $action "read" }}
|
||||
"read": {"id"},
|
||||
{{ end }}
|
||||
{{ if eq $action "update" }}
|
||||
"update": {"id", "name", "description" {{ if hasAction $input.VersionedActions "update" }}, "version" {{ end }} },
|
||||
"update": {"id", "name", "description" {{ if hasAction $input.VersionedActions "update" }}, "version" {{ end }} {{ if $input.HasGenericAttributes }} {{ $attrFlags }} {{ end }} },
|
||||
{{ end }}
|
||||
{{ if eq $action "delete" }}
|
||||
"delete": {"id"},
|
||||
@@ -186,6 +187,7 @@ var flags{{ camelCase .SubActionPrefix }}Map = map[string][]string{
|
||||
"list": { "{{ kebabCase $input.Container }}-id", "filter" {{ if (eq $input.Container "Scope") }}, "recursive"{{ end }} },
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
}
|
||||
|
||||
func (c *{{ camelCase .SubActionPrefix }}Command) Flags() *base.FlagSets {
|
||||
@@ -197,6 +199,11 @@ func (c *{{ camelCase .SubActionPrefix }}Command) Flags() *base.FlagSets {
|
||||
f := set.NewFlagSet("Command Options")
|
||||
common.PopulateCommonFlags(c.Command, f, "{{ if .SubActionPrefix }}{{ .SubActionPrefix }}-type {{ end }}{{ lowerSpaceCase .ResourceType }}", flags{{ camelCase .SubActionPrefix }}Map, c.Func)
|
||||
|
||||
{{ if .HasGenericAttributes }}
|
||||
f = set.NewFlagSet("Attribute Options")
|
||||
common.PopulateAttributeFlags(c.Command, f, flags{{ camelCase .SubActionPrefix }}Map, c.Func)
|
||||
{{ end }}
|
||||
|
||||
extra{{ camelCase .SubActionPrefix }}FlagsFunc(c, set, f)
|
||||
|
||||
return set
|
||||
@@ -323,7 +330,22 @@ func (c *{{ camelCase .SubActionPrefix }}Command) Run(args []string) int {
|
||||
}
|
||||
{{ end }}
|
||||
|
||||
if ok := extra{{ camelCase .SubActionPrefix }}FlagsHandlingFunc(c, f, &opts); !ok {
|
||||
{{ if .HasGenericAttributes }}
|
||||
if err := common.HandleAttributeFlags(
|
||||
c.Command,
|
||||
"attr",
|
||||
func() {
|
||||
{{ .Pkg }}.DefaultAttributes()
|
||||
},
|
||||
func(in map[string]interface{}) {
|
||||
{{ .Pkg }}.WithAttributes(in)
|
||||
}); err != nil {
|
||||
c.PrintCliError(fmt.Errorf("Error evaluating attribute flags to: %s", err.Error()))
|
||||
return base.CommandCliError
|
||||
}
|
||||
{{ end }}
|
||||
|
||||
if ok := extra{{ camelCase .SubActionPrefix }}FlagsHandlingFunc(c, f, &opts, ); !ok {
|
||||
return base.CommandUserError
|
||||
}
|
||||
|
||||
|
||||
@@ -52,7 +52,7 @@ message HostSet {
|
||||
repeated string preferred_endpoints = 101 [json_name="preferred_endpoints", (custom_options.v1.generate_sdk_option) = true];
|
||||
|
||||
// The attributes that are applicable for the specific Host Set type.
|
||||
google.protobuf.Struct attributes = 110;
|
||||
google.protobuf.Struct attributes = 110 [(custom_options.v1.generate_sdk_option) = true];
|
||||
|
||||
// Output only. The available actions on this resource for this user.
|
||||
repeated string authorized_actions = 300 [json_name="authorized_actions"];
|
||||
|
||||
@@ -217,7 +217,7 @@ var file_controller_api_resources_hostsets_v1_host_set_proto_rawDesc = []byte{
|
||||
0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x2a, 0x63, 0x6f,
|
||||
0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2f, 0x63, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x5f,
|
||||
0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2f, 0x76, 0x31, 0x2f, 0x6f, 0x70, 0x74, 0x69, 0x6f,
|
||||
0x6e, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xe9, 0x05, 0x0a, 0x07, 0x48, 0x6f, 0x73,
|
||||
0x6e, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xef, 0x05, 0x0a, 0x07, 0x48, 0x6f, 0x73,
|
||||
0x74, 0x53, 0x65, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09,
|
||||
0x52, 0x02, 0x69, 0x64, 0x12, 0x28, 0x0a, 0x0f, 0x68, 0x6f, 0x73, 0x74, 0x5f, 0x63, 0x61, 0x74,
|
||||
0x61, 0x6c, 0x6f, 0x67, 0x5f, 0x69, 0x64, 0x18, 0x14, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0f, 0x68,
|
||||
@@ -257,19 +257,20 @@ var file_controller_api_resources_hostsets_v1_host_set_proto_rawDesc = []byte{
|
||||
0x64, 0x73, 0x12, 0x36, 0x0a, 0x13, 0x70, 0x72, 0x65, 0x66, 0x65, 0x72, 0x72, 0x65, 0x64, 0x5f,
|
||||
0x65, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x73, 0x18, 0x65, 0x20, 0x03, 0x28, 0x09, 0x42,
|
||||
0x04, 0xa0, 0xda, 0x29, 0x01, 0x52, 0x13, 0x70, 0x72, 0x65, 0x66, 0x65, 0x72, 0x72, 0x65, 0x64,
|
||||
0x5f, 0x65, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x73, 0x12, 0x37, 0x0a, 0x0a, 0x61, 0x74,
|
||||
0x5f, 0x65, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x73, 0x12, 0x3d, 0x0a, 0x0a, 0x61, 0x74,
|
||||
0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x18, 0x6e, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17,
|
||||
0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66,
|
||||
0x2e, 0x53, 0x74, 0x72, 0x75, 0x63, 0x74, 0x52, 0x0a, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75,
|
||||
0x74, 0x65, 0x73, 0x12, 0x2f, 0x0a, 0x12, 0x61, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x65,
|
||||
0x64, 0x5f, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0xac, 0x02, 0x20, 0x03, 0x28, 0x09,
|
||||
0x52, 0x12, 0x61, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x65, 0x64, 0x5f, 0x61, 0x63, 0x74,
|
||||
0x69, 0x6f, 0x6e, 0x73, 0x42, 0x52, 0x5a, 0x50, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63,
|
||||
0x6f, 0x6d, 0x2f, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2f, 0x62, 0x6f, 0x75,
|
||||
0x6e, 0x64, 0x61, 0x72, 0x79, 0x2f, 0x73, 0x64, 0x6b, 0x2f, 0x70, 0x62, 0x73, 0x2f, 0x63, 0x6f,
|
||||
0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x72, 0x65, 0x73,
|
||||
0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x2f, 0x68, 0x6f, 0x73, 0x74, 0x73, 0x65, 0x74, 0x73, 0x3b,
|
||||
0x68, 0x6f, 0x73, 0x74, 0x73, 0x65, 0x74, 0x73, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
|
||||
0x2e, 0x53, 0x74, 0x72, 0x75, 0x63, 0x74, 0x42, 0x04, 0xa0, 0xda, 0x29, 0x01, 0x52, 0x0a, 0x61,
|
||||
0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x12, 0x2f, 0x0a, 0x12, 0x61, 0x75, 0x74,
|
||||
0x68, 0x6f, 0x72, 0x69, 0x7a, 0x65, 0x64, 0x5f, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18,
|
||||
0xac, 0x02, 0x20, 0x03, 0x28, 0x09, 0x52, 0x12, 0x61, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a,
|
||||
0x65, 0x64, 0x5f, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x42, 0x52, 0x5a, 0x50, 0x67, 0x69,
|
||||
0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f,
|
||||
0x72, 0x70, 0x2f, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x61, 0x72, 0x79, 0x2f, 0x73, 0x64, 0x6b, 0x2f,
|
||||
0x70, 0x62, 0x73, 0x2f, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2f, 0x61,
|
||||
0x70, 0x69, 0x2f, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x2f, 0x68, 0x6f, 0x73,
|
||||
0x74, 0x73, 0x65, 0x74, 0x73, 0x3b, 0x68, 0x6f, 0x73, 0x74, 0x73, 0x65, 0x74, 0x73, 0x62, 0x06,
|
||||
0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
|
||||
}
|
||||
|
||||
var (
|
||||
|
||||
Reference in new issue
Block a user