Pull over CombinedSliceFlagValue from WIP plugin-hostcatalogs (#1582)

I've pulled over files as-is for the most part so this also pulls over
some common attribute/secret flags but it's not worth dealing with the
merge later...
pull/1583/head
Jeff Mitchell 5 years ago committed by GitHub
parent 5dd49871f9
commit 217adfc4d2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -73,6 +73,8 @@ type Command struct {
FlagScopeId string
FlagScopeName string
FlagPluginId string
FlagPluginName string
FlagId string
FlagName string
FlagDescription string
@ -83,6 +85,14 @@ type Command struct {
FlagRecursive bool
FlagFilter string
// Attribute values
FlagAttributes string
FlagAttrs []CombinedSliceFlagValue
// Secret values
FlagSecrets string
FlagScrts []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 }

@ -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"
)
@ -27,6 +34,20 @@ func PopulateCommonFlags(c *base.Command, f *base.FlagSet, resourceType string,
Completion: complete.PredictAnything,
Usage: `Scope in which to make the request, identified by name.`,
})
case "plugin-id":
f.StringVar(&base.StringVar{
Name: "plugin-id",
Target: &c.FlagPluginId,
Completion: complete.PredictAnything,
Usage: `ID of a plugin being referenced in the request.`,
})
case "plugin-name":
f.StringVar(&base.StringVar{
Name: "plugin-name",
Target: &c.FlagPluginName,
Completion: complete.PredictAnything,
Usage: `Name of a plugin being referenced in the request.`,
})
case "id":
f.StringVar(&base.StringVar{
Name: "id",
@ -93,3 +114,290 @@ 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 attributes map. Usually this will be sourced 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 attributes 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 attributes 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 attributes 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 attributes map. Can be specified multiple times. Supports sourcing values from files via "file://" and env vars via "env://"`,
})
}
}
}
func PopulateSecretFlags(c *base.Command, f *base.FlagSet, flagNames map[string][]string, command string) {
keyDelimiter := "."
for _, name := range flagNames[command] {
switch name {
case "secrets":
f.StringVar(&base.StringVar{
Name: "secrets",
Target: &c.FlagSecrets,
Usage: `A JSON map value to use as the entirety of the request's secrets map. Usually this will be sourced from a file via "file://" syntax.`,
})
case "secret":
f.CombinationSliceVar(&base.CombinationSliceVar{
Name: "secret",
Target: &c.FlagScrts,
KvSplit: true,
KeyDelimiter: &keyDelimiter,
ProtoCompatKey: true,
Usage: `A key=value secret to add to the request's secrets map. The type is automatically inferred. Use -string-secret, -bool-secret, or -num-secret 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-secret":
f.CombinationSliceVar(&base.CombinationSliceVar{
Name: "string-secret",
Target: &c.FlagScrts,
KvSplit: true,
KeyDelimiter: &keyDelimiter,
ProtoCompatKey: true,
Usage: `A key=value string secret to add to the request's secrets map. Can be specified multiple times. Supports sourcing values from files via "file://" and env vars via "env://"`,
})
case "bool-secret":
f.CombinationSliceVar(&base.CombinationSliceVar{
Name: "bool-secret",
Target: &c.FlagScrts,
KvSplit: true,
KeyDelimiter: &keyDelimiter,
ProtoCompatKey: true,
Usage: `A key=value bool secret to add to the request's secrets map. Can be specified multiple times. Supports sourcing values from files via "file://" and env vars via "env://"`,
})
case "num-secret":
f.CombinationSliceVar(&base.CombinationSliceVar{
Name: "num-secret",
Target: &c.FlagScrts,
KvSplit: true,
KeyDelimiter: &keyDelimiter,
ProtoCompatKey: true,
Usage: `A key=value numeric secret to add to the request's secrets 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, fullField string, sepFields []base.CombinedSliceFlagValue, defaultFunc func(), setFunc func(map[string]interface{})) error {
// If we were given a fullly defined field, use that as-is
switch fullField {
case "":
// Nothing, continue on
case "null":
defaultFunc()
return nil
default:
parsedString, err := parseutil.ParsePath(fullField)
if err != nil && !errors.Is(err, parseutil.ErrNotAUrl) {
return fmt.Errorf("error parsing %s flag as a URL: %w", suffix, 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 %s flag as JSON: %w", suffix, err)
}
setFunc(setMap)
return nil
}
setMap := map[string]interface{}{}
for _, field := range sepFields {
if len(field.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 field.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(field.Value, ".") {
val, err = strconv.ParseFloat(field.Value, 64)
if err != nil {
return fmt.Errorf("error parsing value %q as a float: %w", field.Value, err)
}
} else {
val, err = strconv.ParseInt(field.Value, 10, 64)
if err != nil {
return fmt.Errorf("error parsing value %q as an integer: %w", field.Value, err)
}
}
case "string-" + suffix:
val = strings.Trim(field.Value, `"`)
case "bool-" + suffix:
switch field.Value {
case "true":
val = true
case "false":
val = false
default:
return fmt.Errorf("error parsing value %q as a bool", field.Value)
}
case suffix:
// In this case, use heuristics to just do the right thing the vast
// majority of the time
switch {
case field.Value == "null": // Explicit null, we want to set to a null value to clear it
val = nil
case field.Value == "true": // bool true
val = true
case field.Value == "false": // bool false
val = false
case strings.HasPrefix(field.Value, `"`): // explicitly quoted string
val = strings.Trim(field.Value, `"`)
case jsonNumberRegex.Match([]byte(strings.Trim(field.Value, `"`))): // number
// Same logic as above
if strings.Contains(field.Value, ".") {
val, err = strconv.ParseFloat(field.Value, 64)
if err != nil {
return fmt.Errorf("error parsing value %q as a float: %w", field.Value, err)
}
} else {
val, err = strconv.ParseInt(field.Value, 10, 64)
if err != nil {
return fmt.Errorf("error parsing value %q as an integer: %w", field.Value, err)
}
}
case strings.HasPrefix(field.Value, "["): // serialized JSON array
var s []interface{}
u := json.NewDecoder(bytes.NewBufferString(field.Value))
u.UseNumber()
if err := u.Decode(&s); err != nil {
return fmt.Errorf("error parsing value %q as a json array: %w", field.Value, err)
}
val = s
case strings.HasPrefix(field.Value, "{"): // serialized JSON map
var m map[string]interface{}
u := json.NewDecoder(bytes.NewBufferString(field.Value))
u.UseNumber()
if err := u.Decode(&m); err != nil {
return fmt.Errorf("error parsing value %q as a json map: %w", field.Value, err)
}
val = m
default:
// Default is to treat as a string value
val = field.Value
}
default:
return fmt.Errorf("unknown flag %q", field.Name)
}
// Now we have to insert it in the right position in the final map
currMap := setMap
for i, segment := range field.Keys {
if segment == "" {
return fmt.Errorf("key segment %q for value %q is empty", segment, field.Value)
}
switch {
case i == len(field.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,413 @@
package common
import (
"encoding/json"
"fmt"
"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-%s",
Keys: []string{"foo"},
Value: "bar",
},
{
Name: "string-%s",
Keys: []string{"bar"},
Value: `"baz"`,
},
},
expectedMap: map[string]interface{}{
"foo": "bar",
"bar": "baz",
},
},
{
name: "nums-only",
args: []base.CombinedSliceFlagValue{
{
Name: "num-%s",
Keys: []string{"foo"},
Value: "-1.2",
},
{
Name: "num-%s",
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-%s",
Keys: []string{"foo"},
Value: "-15d.2",
},
},
expectedErr: "as a float",
},
{
name: "bad-int-num",
args: []base.CombinedSliceFlagValue{
{
Name: "num-%s",
Keys: []string{"foo"},
Value: "-15d3",
},
},
expectedErr: "as an int",
},
{
name: "bools-only",
args: []base.CombinedSliceFlagValue{
{
Name: "bool-%s",
Keys: []string{"foo"},
Value: "true",
},
{
Name: "bool-%s",
Keys: []string{"bar"},
Value: "false",
},
},
expectedMap: map[string]interface{}{
"foo": true,
"bar": false,
},
},
{
name: "bad-bool",
args: []base.CombinedSliceFlagValue{
{
Name: "bool-%s",
Keys: []string{"foo"},
Value: "t",
},
},
expectedErr: "as a bool",
},
{
name: "attr-only",
args: []base.CombinedSliceFlagValue{
{
Name: "%s",
Keys: []string{"b1"},
Value: "true",
},
{
Name: "%s",
Keys: []string{"b2"},
Value: "false",
},
{
Name: "%s",
Keys: []string{"s1"},
Value: "scoopde",
},
{
Name: "%s",
Keys: []string{"s2"},
Value: `"woop"`,
},
{
Name: "%s",
Keys: []string{"n1"},
Value: "-1.2",
},
{
Name: "%s",
Keys: []string{"n2"},
Value: "5",
},
{
Name: "%s",
Keys: []string{"a"},
Value: `["foo", 1.5, true, ["bar"], {"hip": "hop"}]`,
},
{
Name: "%s",
Keys: []string{"nil"},
Value: "null",
},
{
Name: "%s",
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: "%s",
Keys: []string{"bools"},
Value: "true",
},
{
Name: "%s",
Keys: []string{"bools"},
Value: "false",
},
{
Name: "%s",
Keys: []string{"strings", "s1"},
Value: "scoopde",
},
{
Name: "%s",
Keys: []string{"strings", "s2"},
Value: `"woop"`,
},
{
Name: "%s",
Keys: []string{"numbers", "reps"},
Value: "-1.2",
},
{
Name: "%s",
Keys: []string{"numbers", "reps"},
Value: "5",
},
{
Name: "%s",
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 {
for _, typ := range []string{"attr", "secret"} {
t.Run(fmt.Sprintf("%s-%s", tt.name, typ), 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)
var outMap map[string]interface{}
args := make([]base.CombinedSliceFlagValue, 0, len(tt.args))
for _, arg := range tt.args {
arg.Name = fmt.Sprintf(arg.Name, typ)
args = append(args, arg)
}
err := HandleAttributeFlags(c, typ, "", args, 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,17 @@ 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
// HasGenericSecrets controls whether to generate flags for -secrets,
// -secret, -string-secret, etc.
HasGenericSecrets bool
// IsPluginType controls whether standard plugin flags are generated
IsPluginType bool
}
var inputStructs = map[string][]*cmdInfo{

@ -169,15 +169,17 @@ 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\"" }}
{{ with $secretFlags := ", \"secrets\", \"secret\", \"string-secret\", \"bool-secret\", \"num-secret\"" }}
{{ 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.IsPluginType }} , "plugin-id", "plugin-name" {{ end }} {{ if $input.HasGenericAttributes }} {{ $attrFlags }} {{ end }} {{ if $input.HasGenericSecrets }} {{ $secretFlags }} {{ 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 }} {{ if $input.HasGenericSecrets }} {{ $secretFlags }} {{ end }} },
{{ end }}
{{ if eq $action "delete" }}
"delete": {"id"},
@ -186,6 +188,8 @@ var flags{{ camelCase .SubActionPrefix }}Map = map[string][]string{
"list": { "{{ kebabCase $input.Container }}-id", "filter" {{ if (eq $input.Container "Scope") }}, "recursive"{{ end }} },
{{ end }}
{{ end }}
{{ end }}
{{ end }}
}
func (c *{{ camelCase .SubActionPrefix }}Command) Flags() *base.FlagSets {
@ -197,6 +201,16 @@ 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 }}
{{ if .HasGenericSecrets }}
f = set.NewFlagSet("Secrets Options")
common.PopulateSecretFlags(c.Command, f, flags{{ camelCase .SubActionPrefix }}Map, c.Func)
{{ end }}
extra{{ camelCase .SubActionPrefix }}FlagsFunc(c, set, f)
return set
@ -307,6 +321,19 @@ func (c *{{ camelCase .SubActionPrefix }}Command) Run(args []string) int {
opts = append(opts, {{ .Pkg }}.WithScopeName(c.FlagScopeName))
}
{{ end }}
{{ if .IsPluginType }}
switch c.FlagPluginId {
case "":
default:
opts = append(opts, {{ .Pkg }}.WithPluginId(c.FlagPluginId))
}
switch c.FlagPluginName {
case "":
default:
opts = append(opts, {{ .Pkg }}.WithPluginName(c.FlagPluginName))
}
{{ end }}
var version uint32
{{ if .VersionedActions }}
@ -323,7 +350,41 @@ 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",
c.FlagAttributes,
c.FlagAttrs,
func() {
opts = append(opts, {{ .Pkg }}.DefaultAttributes())
},
func(in map[string]interface{}) {
opts = append(opts, {{ .Pkg }}.WithAttributes(in))
}); err != nil {
c.PrintCliError(fmt.Errorf("Error evaluating attribute flags to: %s", err.Error()))
return base.CommandCliError
}
{{ end }}
{{ if .HasGenericSecrets }}
if err := common.HandleAttributeFlags(
c.Command,
"secret",
c.FlagSecrets,
c.FlagScrts,
func() {
opts = append(opts, {{ .Pkg }}.DefaultSecrets())
},
func(in map[string]interface{}) {
opts = append(opts, {{ .Pkg }}.WithSecrets(in))
}); err != nil {
c.PrintCliError(fmt.Errorf("Error evaluating secret flags to: %s", err.Error()))
return base.CommandCliError
}
{{ end }}
if ok := extra{{ camelCase .SubActionPrefix }}FlagsHandlingFunc(c, f, &opts, ); !ok {
return base.CommandUserError
}

Loading…
Cancel
Save