mirror of
https://github.com/optim-enterprises-bv/vault.git
synced 2025-11-01 02:57:59 +00:00
List Handling in API and CLI (#2584)
This commit is contained in:
@@ -9,6 +9,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/hashicorp/vault/helper/jsonutil"
|
||||
"github.com/mitchellh/mapstructure"
|
||||
)
|
||||
|
||||
// Builder is a struct to build a key/value mapping based on a list
|
||||
@@ -107,6 +108,17 @@ func (b *Builder) add(raw string) error {
|
||||
}
|
||||
}
|
||||
|
||||
// Repeated keys will be converted into a slice
|
||||
if existingValue, ok := b.result[key]; ok {
|
||||
var sliceValue []interface{}
|
||||
if err := mapstructure.WeakDecode(existingValue, &sliceValue); err != nil {
|
||||
return err
|
||||
}
|
||||
sliceValue = append(sliceValue, value)
|
||||
b.result[key] = sliceValue
|
||||
return nil
|
||||
}
|
||||
|
||||
b.result[key] = value
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -85,3 +85,36 @@ func TestBuilder_stdinTwice(t *testing.T) {
|
||||
t.Fatal("should error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuilder_sameKeyTwice(t *testing.T) {
|
||||
var b Builder
|
||||
err := b.Add("foo=bar", "foo=baz")
|
||||
if err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
|
||||
expected := map[string]interface{}{
|
||||
"foo": []interface{}{"bar", "baz"},
|
||||
}
|
||||
actual := b.Map()
|
||||
if !reflect.DeepEqual(actual, expected) {
|
||||
t.Fatalf("bad: %#v", actual)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuilder_sameKeyMultipleTimes(t *testing.T) {
|
||||
var b Builder
|
||||
err := b.Add("foo=bar", "foo=baz", "foo=bay", "foo=bax", "bar=baz")
|
||||
if err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
|
||||
expected := map[string]interface{}{
|
||||
"foo": []interface{}{"bar", "baz", "bay", "bax"},
|
||||
"bar": "baz",
|
||||
}
|
||||
actual := b.Map()
|
||||
if !reflect.DeepEqual(actual, expected) {
|
||||
t.Fatalf("bad: %#v", actual)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -174,6 +174,16 @@ func ParseArbitraryStringSlice(input string, sep string) []string {
|
||||
return ret
|
||||
}
|
||||
|
||||
// TrimStrings takes a slice of strings and returns a slice of strings
|
||||
// with trimmed spaces
|
||||
func TrimStrings(items []string) []string {
|
||||
ret := make([]string, len(items))
|
||||
for i, item := range items {
|
||||
ret[i] = strings.TrimSpace(item)
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
// Removes duplicate and empty elements from a slice of strings. This also may
|
||||
// convert the items in the slice to lower case and returns a sorted slice.
|
||||
func RemoveDuplicates(items []string, lowercase bool) []string {
|
||||
|
||||
@@ -315,3 +315,12 @@ func TestGlobbedStringsMatch(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestTrimStrings(t *testing.T) {
|
||||
input := []string{"abc", "123", "abcd ", "123 "}
|
||||
expected := []string{"abc", "123", "abcd", "123"}
|
||||
actual := TrimStrings(input)
|
||||
if !reflect.DeepEqual(expected, actual) {
|
||||
t.Fatalf("Bad TrimStrings: expected:%#v, got:%#v", expected, actual)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,9 +13,9 @@ import (
|
||||
log "github.com/mgutz/logxi/v1"
|
||||
|
||||
"github.com/hashicorp/go-multierror"
|
||||
"github.com/hashicorp/vault/helper/parseutil"
|
||||
"github.com/hashicorp/vault/helper/errutil"
|
||||
"github.com/hashicorp/vault/helper/logformat"
|
||||
"github.com/hashicorp/vault/helper/parseutil"
|
||||
"github.com/hashicorp/vault/logical"
|
||||
)
|
||||
|
||||
@@ -587,6 +587,10 @@ func (t FieldType) Zero() interface{} {
|
||||
return map[string]interface{}{}
|
||||
case TypeDurationSecond:
|
||||
return 0
|
||||
case TypeSlice:
|
||||
return []interface{}{}
|
||||
case TypeStringSlice, TypeCommaStringSlice:
|
||||
return []string{}
|
||||
default:
|
||||
panic("unknown type: " + t.String())
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"fmt"
|
||||
|
||||
"github.com/hashicorp/vault/helper/parseutil"
|
||||
"github.com/hashicorp/vault/helper/strutil"
|
||||
"github.com/mitchellh/mapstructure"
|
||||
)
|
||||
|
||||
@@ -30,7 +31,8 @@ func (d *FieldData) Validate() error {
|
||||
}
|
||||
|
||||
switch schema.Type {
|
||||
case TypeBool, TypeInt, TypeMap, TypeDurationSecond, TypeString:
|
||||
case TypeBool, TypeInt, TypeMap, TypeDurationSecond, TypeString, TypeSlice,
|
||||
TypeStringSlice, TypeCommaStringSlice:
|
||||
_, _, err := d.getPrimitive(field, schema)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Error converting input %v for field %s: %s", value, field, err)
|
||||
@@ -105,7 +107,8 @@ func (d *FieldData) GetOkErr(k string) (interface{}, bool, error) {
|
||||
}
|
||||
|
||||
switch schema.Type {
|
||||
case TypeBool, TypeInt, TypeMap, TypeDurationSecond, TypeString:
|
||||
case TypeBool, TypeInt, TypeMap, TypeDurationSecond, TypeString,
|
||||
TypeSlice, TypeStringSlice, TypeCommaStringSlice:
|
||||
return d.getPrimitive(k, schema)
|
||||
default:
|
||||
return nil, false,
|
||||
@@ -177,6 +180,36 @@ func (d *FieldData) getPrimitive(
|
||||
}
|
||||
return result, true, nil
|
||||
|
||||
case TypeSlice:
|
||||
var result []interface{}
|
||||
if err := mapstructure.WeakDecode(raw, &result); err != nil {
|
||||
return nil, true, err
|
||||
}
|
||||
return result, true, nil
|
||||
|
||||
case TypeStringSlice:
|
||||
var result []string
|
||||
if err := mapstructure.WeakDecode(raw, &result); err != nil {
|
||||
return nil, true, err
|
||||
}
|
||||
return strutil.TrimStrings(result), true, nil
|
||||
|
||||
case TypeCommaStringSlice:
|
||||
var result []string
|
||||
config := &mapstructure.DecoderConfig{
|
||||
Result: &result,
|
||||
WeaklyTypedInput: true,
|
||||
DecodeHook: mapstructure.StringToSliceHookFunc(","),
|
||||
}
|
||||
decoder, err := mapstructure.NewDecoder(config)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
if err := decoder.Decode(raw); err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
return strutil.TrimStrings(result), true, nil
|
||||
|
||||
default:
|
||||
panic(fmt.Sprintf("Unknown type: %s", schema.Type))
|
||||
}
|
||||
|
||||
@@ -146,6 +146,105 @@ func TestFieldDataGet(t *testing.T) {
|
||||
"foo",
|
||||
0,
|
||||
},
|
||||
|
||||
"slice type, empty slice": {
|
||||
map[string]*FieldSchema{
|
||||
"foo": &FieldSchema{Type: TypeSlice},
|
||||
},
|
||||
map[string]interface{}{
|
||||
"foo": []interface{}{},
|
||||
},
|
||||
"foo",
|
||||
[]interface{}{},
|
||||
},
|
||||
|
||||
"slice type, filled, mixed slice": {
|
||||
map[string]*FieldSchema{
|
||||
"foo": &FieldSchema{Type: TypeSlice},
|
||||
},
|
||||
map[string]interface{}{
|
||||
"foo": []interface{}{123, "abc"},
|
||||
},
|
||||
"foo",
|
||||
[]interface{}{123, "abc"},
|
||||
},
|
||||
|
||||
"string slice type, filled slice": {
|
||||
map[string]*FieldSchema{
|
||||
"foo": &FieldSchema{Type: TypeStringSlice},
|
||||
},
|
||||
map[string]interface{}{
|
||||
"foo": []interface{}{123, "abc"},
|
||||
},
|
||||
"foo",
|
||||
[]string{"123", "abc"},
|
||||
},
|
||||
|
||||
"comma string slice type, comma string with one value": {
|
||||
map[string]*FieldSchema{
|
||||
"foo": &FieldSchema{Type: TypeCommaStringSlice},
|
||||
},
|
||||
map[string]interface{}{
|
||||
"foo": "value1",
|
||||
},
|
||||
"foo",
|
||||
[]string{"value1"},
|
||||
},
|
||||
|
||||
"comma string slice type, comma string with multi value": {
|
||||
map[string]*FieldSchema{
|
||||
"foo": &FieldSchema{Type: TypeCommaStringSlice},
|
||||
},
|
||||
map[string]interface{}{
|
||||
"foo": "value1,value2,value3",
|
||||
},
|
||||
"foo",
|
||||
[]string{"value1", "value2", "value3"},
|
||||
},
|
||||
|
||||
"comma string slice type, nil string slice value": {
|
||||
map[string]*FieldSchema{
|
||||
"foo": &FieldSchema{Type: TypeCommaStringSlice},
|
||||
},
|
||||
map[string]interface{}{
|
||||
"foo": "",
|
||||
},
|
||||
"foo",
|
||||
[]string{},
|
||||
},
|
||||
|
||||
"commma string slice type, string slice with one value": {
|
||||
map[string]*FieldSchema{
|
||||
"foo": &FieldSchema{Type: TypeCommaStringSlice},
|
||||
},
|
||||
map[string]interface{}{
|
||||
"foo": []interface{}{"value1"},
|
||||
},
|
||||
"foo",
|
||||
[]string{"value1"},
|
||||
},
|
||||
|
||||
"comma string slice type, string slice with multi value": {
|
||||
map[string]*FieldSchema{
|
||||
"foo": &FieldSchema{Type: TypeCommaStringSlice},
|
||||
},
|
||||
map[string]interface{}{
|
||||
"foo": []interface{}{"value1", "value2", "value3"},
|
||||
},
|
||||
"foo",
|
||||
[]string{"value1", "value2", "value3"},
|
||||
},
|
||||
|
||||
"comma string slice type, empty string slice value": {
|
||||
map[string]*FieldSchema{
|
||||
"foo": &FieldSchema{Type: TypeCommaStringSlice},
|
||||
},
|
||||
map[string]interface{}{
|
||||
"foo": []interface{}{},
|
||||
},
|
||||
"foo",
|
||||
[]string{},
|
||||
},
|
||||
}
|
||||
|
||||
for name, tc := range cases {
|
||||
|
||||
@@ -13,6 +13,16 @@ const (
|
||||
// TypeDurationSecond represent as seconds, this can be either an
|
||||
// integer or go duration format string (e.g. 24h)
|
||||
TypeDurationSecond
|
||||
|
||||
// TypeSlice represents a slice of any type
|
||||
TypeSlice
|
||||
// TypeStringSlice is a helper for TypeSlice that returns a sanitized
|
||||
// slice of strings
|
||||
TypeStringSlice
|
||||
// TypeCommaStringSlice is a helper for TypeSlice that returns a sanitized
|
||||
// slice of strings and also supports parsing a comma-separated list in
|
||||
// a string field
|
||||
TypeCommaStringSlice
|
||||
)
|
||||
|
||||
func (t FieldType) String() string {
|
||||
@@ -27,6 +37,8 @@ func (t FieldType) String() string {
|
||||
return "map"
|
||||
case TypeDurationSecond:
|
||||
return "duration (sec)"
|
||||
case TypeSlice, TypeStringSlice, TypeCommaStringSlice:
|
||||
return "slice"
|
||||
default:
|
||||
return "unknown type"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user