config/hcl2shim: ConfigValueFromHCL2Block function

This is a more specialized version of ConfigValueFromHCL2 which is
specifically for config values that represent the content of a block
body in the configuration.

By using the schema of that block we can more precisely emulate the old
HCL1/HIL behaviors by distinguishing attributes from blocks and applying
some slightly different behaviors for the handling of null values and
of empty collections that are representing the absense of blocks of a
particular type.
This commit is contained in:
Martin Atkins 2018-10-16 12:02:32 -07:00
parent 46e168a682
commit 93630cf95f
2 changed files with 340 additions and 0 deletions

View File

@ -6,6 +6,8 @@ import (
"github.com/hashicorp/hil/ast"
"github.com/zclconf/go-cty/cty"
"github.com/hashicorp/terraform/configs/configschema"
)
// UnknownVariableValue is a sentinel value that can be used
@ -14,6 +16,108 @@ import (
// unknown keys.
const UnknownVariableValue = "74D93920-ED26-11E3-AC10-0800200C9A66"
// ConfigValueFromHCL2Block is like ConfigValueFromHCL2 but it works only for
// known object values and uses the provided block schema to perform some
// additional normalization to better mimic the shape of value that the old
// HCL1/HIL-based codepaths would've produced.
//
// In particular, it discards the collections that we use to represent nested
// blocks (other than NestingSingle) if they are empty, which better mimics
// the HCL1 behavior because HCL1 had no knowledge of the schema and so didn't
// know that an unspecified block _could_ exist.
//
// The given object value must conform to the schema's implied type or this
// function will panic or produce incorrect results.
//
// This is primarily useful for the final transition from new-style values to
// terraform.ResourceConfig before calling to a legacy provider, since
// helper/schema (the old provider SDK) is particularly sensitive to these
// subtle differences within its validation code.
func ConfigValueFromHCL2Block(v cty.Value, schema *configschema.Block) map[string]interface{} {
if v.IsNull() {
return nil
}
if !v.IsKnown() {
panic("ConfigValueFromHCL2Block used with unknown value")
}
if !v.Type().IsObjectType() {
panic(fmt.Sprintf("ConfigValueFromHCL2Block used with non-object value %#v", v))
}
atys := v.Type().AttributeTypes()
ret := make(map[string]interface{})
for name := range schema.Attributes {
if _, exists := atys[name]; !exists {
continue
}
av := v.GetAttr(name)
if av.IsNull() {
// Skip nulls altogether, to better mimic how HCL1 would behave
continue
}
ret[name] = ConfigValueFromHCL2(av)
}
for name, blockS := range schema.BlockTypes {
if _, exists := atys[name]; !exists {
continue
}
bv := v.GetAttr(name)
if !bv.IsKnown() {
ret[name] = UnknownVariableValue
continue
}
if bv.IsNull() {
continue
}
switch blockS.Nesting {
case configschema.NestingSingle:
ret[name] = ConfigValueFromHCL2Block(bv, &blockS.Block)
case configschema.NestingList, configschema.NestingSet:
l := bv.LengthInt()
if l == 0 {
// skip empty collections to better mimic how HCL1 would behave
continue
}
elems := make([]interface{}, 0, l)
for it := bv.ElementIterator(); it.Next(); {
_, ev := it.Element()
if !ev.IsKnown() {
elems = append(elems, UnknownVariableValue)
continue
}
elems = append(elems, ConfigValueFromHCL2Block(ev, &blockS.Block))
}
ret[name] = elems
case configschema.NestingMap:
if bv.LengthInt() == 0 {
// skip empty collections to better mimic how HCL1 would behave
continue
}
elems := make(map[string]interface{})
for it := bv.ElementIterator(); it.Next(); {
ek, ev := it.Element()
if !ev.IsKnown() {
elems[ek.AsString()] = UnknownVariableValue
continue
}
elems[ek.AsString()] = ConfigValueFromHCL2Block(ev, &blockS.Block)
}
ret[name] = elems
}
}
return ret
}
// ConfigValueFromHCL2 converts a value from HCL2 (really, from the cty dynamic
// types library that HCL2 uses) to a value type that matches what would've
// been produced from the HCL-based interpolator for an equivalent structure.

View File

@ -5,9 +5,245 @@ import (
"reflect"
"testing"
"github.com/hashicorp/terraform/configs/configschema"
"github.com/zclconf/go-cty/cty"
)
func TestConfigValueFromHCL2Block(t *testing.T) {
tests := []struct {
Input cty.Value
Schema *configschema.Block
Want map[string]interface{}
}{
{
cty.ObjectVal(map[string]cty.Value{
"name": cty.StringVal("Ermintrude"),
"age": cty.NumberIntVal(19),
"address": cty.ObjectVal(map[string]cty.Value{
"street": cty.ListVal([]cty.Value{cty.StringVal("421 Shoreham Loop")}),
"city": cty.StringVal("Fridgewater"),
"state": cty.StringVal("MA"),
"zip": cty.StringVal("91037"),
}),
}),
&configschema.Block{
Attributes: map[string]*configschema.Attribute{
"name": {Type: cty.String, Optional: true},
"age": {Type: cty.Number, Optional: true},
},
BlockTypes: map[string]*configschema.NestedBlock{
"address": {
Nesting: configschema.NestingSingle,
Block: configschema.Block{
Attributes: map[string]*configschema.Attribute{
"street": {Type: cty.List(cty.String), Optional: true},
"city": {Type: cty.String, Optional: true},
"state": {Type: cty.String, Optional: true},
"zip": {Type: cty.String, Optional: true},
},
},
},
},
},
map[string]interface{}{
"name": "Ermintrude",
"age": int(19),
"address": map[string]interface{}{
"street": []interface{}{"421 Shoreham Loop"},
"city": "Fridgewater",
"state": "MA",
"zip": "91037",
},
},
},
{
cty.ObjectVal(map[string]cty.Value{
"name": cty.StringVal("Ermintrude"),
"age": cty.NumberIntVal(19),
"address": cty.NullVal(cty.Object(map[string]cty.Type{
"street": cty.List(cty.String),
"city": cty.String,
"state": cty.String,
"zip": cty.String,
})),
}),
&configschema.Block{
Attributes: map[string]*configschema.Attribute{
"name": {Type: cty.String, Optional: true},
"age": {Type: cty.Number, Optional: true},
},
BlockTypes: map[string]*configschema.NestedBlock{
"address": {
Nesting: configschema.NestingSingle,
Block: configschema.Block{
Attributes: map[string]*configschema.Attribute{
"street": {Type: cty.List(cty.String), Optional: true},
"city": {Type: cty.String, Optional: true},
"state": {Type: cty.String, Optional: true},
"zip": {Type: cty.String, Optional: true},
},
},
},
},
},
map[string]interface{}{
"name": "Ermintrude",
"age": int(19),
},
},
{
cty.ObjectVal(map[string]cty.Value{
"name": cty.StringVal("Ermintrude"),
"age": cty.NumberIntVal(19),
"address": cty.ObjectVal(map[string]cty.Value{
"street": cty.ListVal([]cty.Value{cty.StringVal("421 Shoreham Loop")}),
"city": cty.StringVal("Fridgewater"),
"state": cty.StringVal("MA"),
"zip": cty.NullVal(cty.String), // should be omitted altogether in result
}),
}),
&configschema.Block{
Attributes: map[string]*configschema.Attribute{
"name": {Type: cty.String, Optional: true},
"age": {Type: cty.Number, Optional: true},
},
BlockTypes: map[string]*configschema.NestedBlock{
"address": {
Nesting: configschema.NestingSingle,
Block: configschema.Block{
Attributes: map[string]*configschema.Attribute{
"street": {Type: cty.List(cty.String), Optional: true},
"city": {Type: cty.String, Optional: true},
"state": {Type: cty.String, Optional: true},
"zip": {Type: cty.String, Optional: true},
},
},
},
},
},
map[string]interface{}{
"name": "Ermintrude",
"age": int(19),
"address": map[string]interface{}{
"street": []interface{}{"421 Shoreham Loop"},
"city": "Fridgewater",
"state": "MA",
},
},
},
{
cty.ObjectVal(map[string]cty.Value{
"address": cty.ListVal([]cty.Value{cty.EmptyObjectVal}),
}),
&configschema.Block{
BlockTypes: map[string]*configschema.NestedBlock{
"address": {
Nesting: configschema.NestingList,
Block: configschema.Block{},
},
},
},
map[string]interface{}{
"address": []interface{}{
map[string]interface{}{},
},
},
},
{
cty.ObjectVal(map[string]cty.Value{
"address": cty.ListValEmpty(cty.EmptyObject), // should be omitted altogether in result
}),
&configschema.Block{
BlockTypes: map[string]*configschema.NestedBlock{
"address": {
Nesting: configschema.NestingList,
Block: configschema.Block{},
},
},
},
map[string]interface{}{},
},
{
cty.ObjectVal(map[string]cty.Value{
"address": cty.SetVal([]cty.Value{cty.EmptyObjectVal}),
}),
&configschema.Block{
BlockTypes: map[string]*configschema.NestedBlock{
"address": {
Nesting: configschema.NestingSet,
Block: configschema.Block{},
},
},
},
map[string]interface{}{
"address": []interface{}{
map[string]interface{}{},
},
},
},
{
cty.ObjectVal(map[string]cty.Value{
"address": cty.SetValEmpty(cty.EmptyObject),
}),
&configschema.Block{
BlockTypes: map[string]*configschema.NestedBlock{
"address": {
Nesting: configschema.NestingSet,
Block: configschema.Block{},
},
},
},
map[string]interface{}{},
},
{
cty.ObjectVal(map[string]cty.Value{
"address": cty.MapVal(map[string]cty.Value{"foo": cty.EmptyObjectVal}),
}),
&configschema.Block{
BlockTypes: map[string]*configschema.NestedBlock{
"address": {
Nesting: configschema.NestingMap,
Block: configschema.Block{},
},
},
},
map[string]interface{}{
"address": map[string]interface{}{
"foo": map[string]interface{}{},
},
},
},
{
cty.ObjectVal(map[string]cty.Value{
"address": cty.MapValEmpty(cty.EmptyObject),
}),
&configschema.Block{
BlockTypes: map[string]*configschema.NestedBlock{
"address": {
Nesting: configschema.NestingMap,
Block: configschema.Block{},
},
},
},
map[string]interface{}{},
},
{
cty.NullVal(cty.EmptyObject),
&configschema.Block{},
nil,
},
}
for _, test := range tests {
t.Run(fmt.Sprintf("%#v", test.Input), func(t *testing.T) {
got := ConfigValueFromHCL2Block(test.Input, test.Schema)
if !reflect.DeepEqual(got, test.Want) {
t.Errorf("wrong result\ninput: %#v\ngot: %#v\nwant: %#v", test.Input, got, test.Want)
}
})
}
}
func TestConfigValueFromHCL2(t *testing.T) {
tests := []struct {
Input cty.Value