configschema: Fix ConfigSchema bugs with nested blocks

We were iterating over the wrong value to recursively coerce content for
nested blocks, and also incorrectly constructing the cty.Path used in
errors.
This commit is contained in:
Martin Atkins 2018-07-19 15:15:00 -07:00
parent 0120d53baf
commit d8bf3cc4e0
2 changed files with 173 additions and 29 deletions

View File

@ -108,7 +108,7 @@ func (b *Block) coerceValue(in cty.Value, path cty.Path) (cty.Value, error) {
} }
if !coll.CanIterateElements() { if !coll.CanIterateElements() {
return cty.UnknownVal(b.ImpliedType()), path.NewErrorf("attribute %q must be a list", typeName) return cty.UnknownVal(b.ImpliedType()), path.NewErrorf("must be a list")
} }
l := coll.LengthInt() l := coll.LengthInt()
if l < blockS.MinItems { if l < blockS.MinItems {
@ -122,15 +122,18 @@ func (b *Block) coerceValue(in cty.Value, path cty.Path) (cty.Value, error) {
continue continue
} }
elems := make([]cty.Value, 0, l) elems := make([]cty.Value, 0, l)
for it := in.ElementIterator(); it.Next(); { {
path = append(path, cty.GetAttrStep{Name: typeName})
for it := coll.ElementIterator(); it.Next(); {
var err error var err error
_, val := it.Element() idx, val := it.Element()
val, err = blockS.coerceValue(val, append(path, cty.GetAttrStep{Name: typeName})) val, err = blockS.coerceValue(val, append(path, cty.IndexStep{Key: idx}))
if err != nil { if err != nil {
return cty.UnknownVal(b.ImpliedType()), err return cty.UnknownVal(b.ImpliedType()), err
} }
elems = append(elems, val) elems = append(elems, val)
} }
}
attrs[typeName] = cty.ListVal(elems) attrs[typeName] = cty.ListVal(elems)
case blockS.MinItems == 0: case blockS.MinItems == 0:
attrs[typeName] = cty.ListValEmpty(blockS.ImpliedType()) attrs[typeName] = cty.ListValEmpty(blockS.ImpliedType())
@ -153,7 +156,7 @@ func (b *Block) coerceValue(in cty.Value, path cty.Path) (cty.Value, error) {
} }
if !coll.CanIterateElements() { if !coll.CanIterateElements() {
return cty.UnknownVal(b.ImpliedType()), path.NewErrorf("attribute %q must be a set", typeName) return cty.UnknownVal(b.ImpliedType()), path.NewErrorf("must be a set")
} }
l := coll.LengthInt() l := coll.LengthInt()
if l < blockS.MinItems { if l < blockS.MinItems {
@ -167,15 +170,18 @@ func (b *Block) coerceValue(in cty.Value, path cty.Path) (cty.Value, error) {
continue continue
} }
elems := make([]cty.Value, 0, l) elems := make([]cty.Value, 0, l)
for it := in.ElementIterator(); it.Next(); { {
path = append(path, cty.GetAttrStep{Name: typeName})
for it := coll.ElementIterator(); it.Next(); {
var err error var err error
_, val := it.Element() idx, val := it.Element()
val, err = blockS.coerceValue(val, append(path, cty.GetAttrStep{Name: typeName})) val, err = blockS.coerceValue(val, append(path, cty.IndexStep{Key: idx}))
if err != nil { if err != nil {
return cty.UnknownVal(b.ImpliedType()), err return cty.UnknownVal(b.ImpliedType()), err
} }
elems = append(elems, val) elems = append(elems, val)
} }
}
attrs[typeName] = cty.SetVal(elems) attrs[typeName] = cty.SetVal(elems)
case blockS.MinItems == 0: case blockS.MinItems == 0:
attrs[typeName] = cty.SetValEmpty(blockS.ImpliedType()) attrs[typeName] = cty.SetValEmpty(blockS.ImpliedType())
@ -198,7 +204,7 @@ func (b *Block) coerceValue(in cty.Value, path cty.Path) (cty.Value, error) {
} }
if !coll.CanIterateElements() { if !coll.CanIterateElements() {
return cty.UnknownVal(b.ImpliedType()), path.NewErrorf("attribute %q must be a map", typeName) return cty.UnknownVal(b.ImpliedType()), path.NewErrorf("must be a map")
} }
l := coll.LengthInt() l := coll.LengthInt()
if l == 0 { if l == 0 {
@ -206,18 +212,21 @@ func (b *Block) coerceValue(in cty.Value, path cty.Path) (cty.Value, error) {
continue continue
} }
elems := make(map[string]cty.Value) elems := make(map[string]cty.Value)
for it := in.ElementIterator(); it.Next(); { {
path = append(path, cty.GetAttrStep{Name: typeName})
for it := coll.ElementIterator(); it.Next(); {
var err error var err error
key, val := it.Element() key, val := it.Element()
if key.Type() != cty.String || key.IsNull() || !key.IsKnown() { if key.Type() != cty.String || key.IsNull() || !key.IsKnown() {
return cty.UnknownVal(b.ImpliedType()), path.NewErrorf("attribute %q must be a map", typeName) return cty.UnknownVal(b.ImpliedType()), path.NewErrorf("must be a map")
} }
val, err = blockS.coerceValue(val, append(path, cty.GetAttrStep{Name: typeName})) val, err = blockS.coerceValue(val, append(path, cty.IndexStep{Key: key}))
if err != nil { if err != nil {
return cty.UnknownVal(b.ImpliedType()), err return cty.UnknownVal(b.ImpliedType()), err
} }
elems[key.AsString()] = val elems[key.AsString()] = val
} }
}
attrs[typeName] = cty.MapVal(elems) attrs[typeName] = cty.MapVal(elems)
default: default:
attrs[typeName] = cty.MapValEmpty(blockS.ImpliedType()) attrs[typeName] = cty.MapValEmpty(blockS.ImpliedType())

View File

@ -4,6 +4,8 @@ import (
"testing" "testing"
"github.com/zclconf/go-cty/cty" "github.com/zclconf/go-cty/cty"
"github.com/hashicorp/terraform/tfdiags"
) )
func TestCoerceValue(t *testing.T) { func TestCoerceValue(t *testing.T) {
@ -66,7 +68,125 @@ func TestCoerceValue(t *testing.T) {
"foo": cty.True, "foo": cty.True,
}), }),
cty.DynamicVal, cty.DynamicVal,
`an object is required`, `.foo: an object is required`,
},
"list block with one item": {
&Block{
BlockTypes: map[string]*NestedBlock{
"foo": {
Block: Block{},
Nesting: NestingList,
},
},
},
cty.ObjectVal(map[string]cty.Value{
"foo": cty.ListVal([]cty.Value{cty.EmptyObjectVal}),
}),
cty.ObjectVal(map[string]cty.Value{
"foo": cty.ListVal([]cty.Value{cty.EmptyObjectVal}),
}),
``,
},
"set block with one item": {
&Block{
BlockTypes: map[string]*NestedBlock{
"foo": {
Block: Block{},
Nesting: NestingSet,
},
},
},
cty.ObjectVal(map[string]cty.Value{
"foo": cty.ListVal([]cty.Value{cty.EmptyObjectVal}), // can implicitly convert to set
}),
cty.ObjectVal(map[string]cty.Value{
"foo": cty.SetVal([]cty.Value{cty.EmptyObjectVal}),
}),
``,
},
"map block with one item": {
&Block{
BlockTypes: map[string]*NestedBlock{
"foo": {
Block: Block{},
Nesting: NestingMap,
},
},
},
cty.ObjectVal(map[string]cty.Value{
"foo": cty.MapVal(map[string]cty.Value{"foo": cty.EmptyObjectVal}),
}),
cty.ObjectVal(map[string]cty.Value{
"foo": cty.MapVal(map[string]cty.Value{"foo": cty.EmptyObjectVal}),
}),
``,
},
"list block with one item having an attribute": {
&Block{
BlockTypes: map[string]*NestedBlock{
"foo": {
Block: Block{
Attributes: map[string]*Attribute{
"bar": {
Type: cty.String,
Required: true,
},
},
},
Nesting: NestingList,
},
},
},
cty.ObjectVal(map[string]cty.Value{
"foo": cty.ListVal([]cty.Value{cty.ObjectVal(map[string]cty.Value{
"bar": cty.StringVal("hello"),
})}),
}),
cty.ObjectVal(map[string]cty.Value{
"foo": cty.ListVal([]cty.Value{cty.ObjectVal(map[string]cty.Value{
"bar": cty.StringVal("hello"),
})}),
}),
``,
},
"list block with one item having a missing attribute": {
&Block{
BlockTypes: map[string]*NestedBlock{
"foo": {
Block: Block{
Attributes: map[string]*Attribute{
"bar": {
Type: cty.String,
Required: true,
},
},
},
Nesting: NestingList,
},
},
},
cty.ObjectVal(map[string]cty.Value{
"foo": cty.ListVal([]cty.Value{cty.EmptyObjectVal}),
}),
cty.DynamicVal,
`.foo[0]: attribute "bar" is required`,
},
"list block with one item having an extraneous attribute": {
&Block{
BlockTypes: map[string]*NestedBlock{
"foo": {
Block: Block{},
Nesting: NestingList,
},
},
},
cty.ObjectVal(map[string]cty.Value{
"foo": cty.ListVal([]cty.Value{cty.ObjectVal(map[string]cty.Value{
"bar": cty.StringVal("hello"),
})}),
}),
cty.DynamicVal,
`.foo[0]: unexpected attribute "bar"`,
}, },
"missing optional attribute": { "missing optional attribute": {
&Block{ &Block{
@ -207,6 +327,21 @@ func TestCoerceValue(t *testing.T) {
cty.DynamicVal, cty.DynamicVal,
`unexpected attribute "foo"`, `unexpected attribute "foo"`,
}, },
"wrong attribute type": {
&Block{
Attributes: map[string]*Attribute{
"foo": {
Type: cty.Number,
Required: true,
},
},
},
cty.ObjectVal(map[string]cty.Value{
"foo": cty.False,
}),
cty.DynamicVal,
`.foo: number required`,
},
} }
for name, test := range tests { for name, test := range tests {
@ -218,7 +353,7 @@ func TestCoerceValue(t *testing.T) {
t.Fatalf("coersion succeeded; want error: %q", test.WantErr) t.Fatalf("coersion succeeded; want error: %q", test.WantErr)
} }
} else { } else {
gotErr := gotErrObj.Error() gotErr := tfdiags.FormatError(gotErrObj)
if gotErr != test.WantErr { if gotErr != test.WantErr {
t.Fatalf("wrong error\ngot: %s\nwant: %s", gotErr, test.WantErr) t.Fatalf("wrong error\ngot: %s\nwant: %s", gotErr, test.WantErr)
} }