From 8efdae67e50e220033eb34d82fb016bf9ca206da Mon Sep 17 00:00:00 2001 From: Chris Hoffman Date: Tue, 18 Apr 2017 16:02:31 -0400 Subject: [PATCH] List Handling in API and CLI (#2584) --- helper/kv-builder/builder.go | 12 ++++ helper/kv-builder/builder_test.go | 33 ++++++++++ helper/strutil/strutil.go | 10 +++ helper/strutil/strutil_test.go | 9 +++ logical/framework/backend.go | 6 +- logical/framework/field_data.go | 37 ++++++++++- logical/framework/field_data_test.go | 99 ++++++++++++++++++++++++++++ logical/framework/field_type.go | 12 ++++ 8 files changed, 215 insertions(+), 3 deletions(-) diff --git a/helper/kv-builder/builder.go b/helper/kv-builder/builder.go index e5a0ad781d..7ecf7540df 100644 --- a/helper/kv-builder/builder.go +++ b/helper/kv-builder/builder.go @@ -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 } diff --git a/helper/kv-builder/builder_test.go b/helper/kv-builder/builder_test.go index f64c8e46e2..9b0cffbc41 100644 --- a/helper/kv-builder/builder_test.go +++ b/helper/kv-builder/builder_test.go @@ -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) + } +} diff --git a/helper/strutil/strutil.go b/helper/strutil/strutil.go index df65db0c17..2d8f88b409 100644 --- a/helper/strutil/strutil.go +++ b/helper/strutil/strutil.go @@ -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 { diff --git a/helper/strutil/strutil_test.go b/helper/strutil/strutil_test.go index 85ccd8b4c0..9fd3bef393 100644 --- a/helper/strutil/strutil_test.go +++ b/helper/strutil/strutil_test.go @@ -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) + } +} diff --git a/logical/framework/backend.go b/logical/framework/backend.go index 91ff3358d9..e94ea04528 100644 --- a/logical/framework/backend.go +++ b/logical/framework/backend.go @@ -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()) } diff --git a/logical/framework/field_data.go b/logical/framework/field_data.go index 9a62b0b1ab..97838028c6 100644 --- a/logical/framework/field_data.go +++ b/logical/framework/field_data.go @@ -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)) } diff --git a/logical/framework/field_data_test.go b/logical/framework/field_data_test.go index 44bde2b462..a801f9c68d 100644 --- a/logical/framework/field_data_test.go +++ b/logical/framework/field_data_test.go @@ -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 { diff --git a/logical/framework/field_type.go b/logical/framework/field_type.go index d9d0ef3d24..034d0fe596 100644 --- a/logical/framework/field_type.go +++ b/logical/framework/field_type.go @@ -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" }