From 487a37b0dd39f7aa55fae7e35017ddc405e03f69 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 22 Dec 2016 17:55:23 -0800 Subject: [PATCH] helper/schema: PromoteSingle for legacy support of "maybe list" types --- .../remote-exec/resource_provisioner.go | 1 + helper/schema/field_reader_config.go | 25 +++++ helper/schema/schema.go | 21 +++- helper/schema/schema_test.go | 100 ++++++++++++++++++ 4 files changed, 144 insertions(+), 3 deletions(-) diff --git a/builtin/provisioners/remote-exec/resource_provisioner.go b/builtin/provisioners/remote-exec/resource_provisioner.go index 43ab12b0af..cda3c0b698 100644 --- a/builtin/provisioners/remote-exec/resource_provisioner.go +++ b/builtin/provisioners/remote-exec/resource_provisioner.go @@ -23,6 +23,7 @@ func Provisioner() terraform.ResourceProvisioner { "inline": &schema.Schema{ Type: schema.TypeList, Elem: &schema.Schema{Type: schema.TypeString}, + PromoteSingle: true, Optional: true, ConflictsWith: []string{"script", "scripts"}, }, diff --git a/helper/schema/field_reader_config.go b/helper/schema/field_reader_config.go index 53ff5208f9..f958bbcb12 100644 --- a/helper/schema/field_reader_config.go +++ b/helper/schema/field_reader_config.go @@ -79,10 +79,35 @@ func (r *ConfigFieldReader) readField( k := strings.Join(address, ".") schema := schemaList[len(schemaList)-1] + + // If we're getting the single element of a promoted list, then + // check to see if we have a single element we need to promote. + if address[len(address)-1] == "0" && len(schemaList) > 1 { + lastSchema := schemaList[len(schemaList)-2] + if lastSchema.Type == TypeList && lastSchema.PromoteSingle { + k := strings.Join(address[:len(address)-1], ".") + result, err := r.readPrimitive(k, schema) + if err == nil { + return result, nil + } + } + } + switch schema.Type { case TypeBool, TypeFloat, TypeInt, TypeString: return r.readPrimitive(k, schema) case TypeList: + // If we support promotion then we first check if we have a lone + // value that we must promote. + // a value that is alone. + if schema.PromoteSingle { + result, err := r.readPrimitive(k, schema.Elem.(*Schema)) + if err == nil && result.Exists { + result.Value = []interface{}{result.Value} + return result, nil + } + } + return readListField(&nestedConfigFieldReader{r}, address, schema) case TypeMap: return r.readMap(k, schema) diff --git a/helper/schema/schema.go b/helper/schema/schema.go index 25ef5e169b..924049d6c7 100644 --- a/helper/schema/schema.go +++ b/helper/schema/schema.go @@ -118,9 +118,16 @@ type Schema struct { // TypeSet or TypeList. Specific use cases would be if a TypeSet is being // used to wrap a complex structure, however less than one instance would // cause instability. - Elem interface{} - MaxItems int - MinItems int + // + // PromoteSingle, if true, will allow single elements to be standalone + // and promote them to a list. For example "foo" would be promoted to + // ["foo"] automatically. This is primarily for legacy reasons and the + // ambiguity is not recommended for new usage. Promotion is only allowed + // for primitive element types. + Elem interface{} + MaxItems int + MinItems int + PromoteSingle bool // The following fields are only valid for a TypeSet type. // @@ -1140,6 +1147,14 @@ func (m schemaMap) validateList( // We use reflection to verify the slice because you can't // case to []interface{} unless the slice is exactly that type. rawV := reflect.ValueOf(raw) + + // If we support promotion and the raw value isn't a slice, wrap + // it in []interface{} and check again. + if schema.PromoteSingle && rawV.Kind() != reflect.Slice { + raw = []interface{}{raw} + rawV = reflect.ValueOf(raw) + } + if rawV.Kind() != reflect.Slice { return nil, []error{fmt.Errorf( "%s: should be a list", k)} diff --git a/helper/schema/schema_test.go b/helper/schema/schema_test.go index 33ac26f763..dc42215ec5 100644 --- a/helper/schema/schema_test.go +++ b/helper/schema/schema_test.go @@ -582,6 +582,72 @@ func TestSchemaMap_Diff(t *testing.T) { Err: false, }, + { + Name: "List decode with promotion", + Schema: map[string]*Schema{ + "ports": &Schema{ + Type: TypeList, + Required: true, + Elem: &Schema{Type: TypeInt}, + PromoteSingle: true, + }, + }, + + State: nil, + + Config: map[string]interface{}{ + "ports": "5", + }, + + Diff: &terraform.InstanceDiff{ + Attributes: map[string]*terraform.ResourceAttrDiff{ + "ports.#": &terraform.ResourceAttrDiff{ + Old: "0", + New: "1", + }, + "ports.0": &terraform.ResourceAttrDiff{ + Old: "", + New: "5", + }, + }, + }, + + Err: false, + }, + + { + Name: "List decode with promotion with list", + Schema: map[string]*Schema{ + "ports": &Schema{ + Type: TypeList, + Required: true, + Elem: &Schema{Type: TypeInt}, + PromoteSingle: true, + }, + }, + + State: nil, + + Config: map[string]interface{}{ + "ports": []interface{}{"5"}, + }, + + Diff: &terraform.InstanceDiff{ + Attributes: map[string]*terraform.ResourceAttrDiff{ + "ports.#": &terraform.ResourceAttrDiff{ + Old: "0", + New: "1", + }, + "ports.0": &terraform.ResourceAttrDiff{ + Old: "", + New: "5", + }, + }, + }, + + Err: false, + }, + { Schema: map[string]*Schema{ "ports": &Schema{ @@ -3585,6 +3651,40 @@ func TestSchemaMap_Validate(t *testing.T) { Err: true, }, + "List with promotion": { + Schema: map[string]*Schema{ + "ingress": &Schema{ + Type: TypeList, + Elem: &Schema{Type: TypeInt}, + PromoteSingle: true, + Optional: true, + }, + }, + + Config: map[string]interface{}{ + "ingress": "5", + }, + + Err: false, + }, + + "List with promotion set as list": { + Schema: map[string]*Schema{ + "ingress": &Schema{ + Type: TypeList, + Elem: &Schema{Type: TypeInt}, + PromoteSingle: true, + Optional: true, + }, + }, + + Config: map[string]interface{}{ + "ingress": []interface{}{"5"}, + }, + + Err: false, + }, + "Optional sub-resource": { Schema: map[string]*Schema{ "ingress": &Schema{