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:
parent
46e168a682
commit
93630cf95f
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue