helper/schema: PromoteSingle for legacy support of "maybe list" types

This commit is contained in:
Mitchell Hashimoto 2016-12-22 17:55:23 -08:00
parent f29845e54e
commit 487a37b0dd
No known key found for this signature in database
GPG Key ID: 744E147AA52F5B0A
4 changed files with 144 additions and 3 deletions

View File

@ -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"},
},

View File

@ -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)

View File

@ -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)}

View File

@ -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{