configs/configupgrade: Prefer block syntax for list-of-object attributes

In order to preserve pre-v0.12 idiom for list-of-object attributes, we'll
prefer to use block syntax for them except for the special situation where
the user explicitly assigns an empty list, where attribute syntax is
required in order to allow existing provider logic to differentiate from
an implicit lack of blocks.
This commit is contained in:
Martin Atkins 2019-03-29 16:24:02 -07:00
parent 39ef97beff
commit 6c5819f910
8 changed files with 97 additions and 10 deletions

View File

@ -0,0 +1,15 @@
resource "test_instance" "from_list" {
list_of_obj = [
{},
{},
]
}
resource "test_instance" "already_blocks" {
list_of_obj {}
list_of_obj {}
}
resource "test_instance" "empty" {
list_of_obj = []
}

View File

@ -0,0 +1,17 @@
resource "test_instance" "from_list" {
list_of_obj {
}
list_of_obj {
}
}
resource "test_instance" "already_blocks" {
list_of_obj {
}
list_of_obj {
}
}
resource "test_instance" "empty" {
list_of_obj = []
}

View File

@ -0,0 +1,3 @@
terraform {
required_version = ">= 0.12"
}

View File

@ -13,12 +13,12 @@ import (
hcl1token "github.com/hashicorp/hcl/hcl/token"
hcl2 "github.com/hashicorp/hcl2/hcl"
hcl2syntax "github.com/hashicorp/hcl2/hcl/hclsyntax"
"github.com/zclconf/go-cty/cty"
"github.com/hashicorp/terraform/configs/configschema"
"github.com/hashicorp/terraform/lang/blocktoattr"
"github.com/hashicorp/terraform/registry/regsrc"
"github.com/hashicorp/terraform/terraform"
"github.com/hashicorp/terraform/tfdiags"
"github.com/zclconf/go-cty/cty"
)
// bodyContentRules is a mapping from item names (argument names and block type
@ -318,7 +318,7 @@ func nestedBlockRule(filename string, nestedRules bodyContentRules, an *analysis
}
}
func nestedBlockRuleWithDynamic(filename string, nestedRules bodyContentRules, nestedSchema *configschema.NestedBlock, an *analysis, adhocComments *commentQueue) bodyItemRule {
func nestedBlockRuleWithDynamic(filename string, nestedRules bodyContentRules, nestedSchema *configschema.NestedBlock, emptyAsAttr bool, an *analysis, adhocComments *commentQueue) bodyItemRule {
return func(buf *bytes.Buffer, blockAddr string, item *hcl1ast.ObjectItem) tfdiags.Diagnostics {
// In Terraform v0.11 it was possible in some cases to trick Terraform
// and providers into accepting HCL's attribute syntax and some HIL
@ -389,6 +389,17 @@ func nestedBlockRuleWithDynamic(filename string, nestedRules bodyContentRules, n
blockItems = append(blockItems, item.Val)
}
if len(blockItems) == 0 && emptyAsAttr {
// Terraform v0.12's config decoder allows using block syntax for
// certain attribute types, which we prefer as idiomatic usage
// causing us to end up in this function in such cases, but as
// a special case users can still use the attribute syntax to
// explicitly write an empty list. For more information, see
// the lang/blocktoattr package.
printAttribute(buf, item.Keys[0].Token.Value().(string), []byte{'[', ']'}, item.LineComment)
return diags
}
for _, blockItem := range blockItems {
switch ti := blockItem.(type) {
case *hcl1ast.ObjectType:
@ -455,11 +466,23 @@ func schemaDefaultBodyRules(filename string, schema *configschema.Block, an *ana
}
for name, attrS := range schema.Attributes {
if aty := attrS.Type; blocktoattr.TypeCanBeBlocks(aty) {
// Terraform's standard body processing rules for arbitrary schemas
// have a special case where list-of-object or set-of-object
// attributes can be specified as a sequence of nested blocks
// instead of a single list attribute. We prefer that form during
// upgrade for historical reasons, to avoid making large changes
// to existing configurations that were following documented idiom.
synthSchema := blocktoattr.SchemaForCtyContainerType(aty)
nestedRules := schemaDefaultBodyRules(filename, &synthSchema.Block, an, adhocComments)
ret[name] = nestedBlockRuleWithDynamic(filename, nestedRules, synthSchema, true, an, adhocComments)
continue
}
ret[name] = normalAttributeRule(filename, attrS.Type, an)
}
for name, blockS := range schema.BlockTypes {
nestedRules := schemaDefaultBodyRules(filename, &blockS.Block, an, adhocComments)
ret[name] = nestedBlockRuleWithDynamic(filename, nestedRules, blockS, an, adhocComments)
ret[name] = nestedBlockRuleWithDynamic(filename, nestedRules, blockS, false, an, adhocComments)
}
return ret

View File

@ -198,6 +198,7 @@ var testProviders = map[string]providers.Factory{
"tags": {Type: cty.Map(cty.String), Optional: true},
"security_groups": {Type: cty.List(cty.String), Optional: true},
"subnet_ids": {Type: cty.Set(cty.String), Optional: true},
"list_of_obj": {Type: cty.List(cty.EmptyObject), Optional: true},
},
BlockTypes: map[string]*configschema.NestedBlock{
"network": {

View File

@ -146,7 +146,7 @@ func (e *fixupBlocksExpr) Value(ctx *hcl.EvalContext) (cty.Value, hcl.Diagnostic
// the result is imprecise and in particular will just consider all
// the attributes to be optional and let the provider eventually decide
// whether to return errors if they turn out to be null when required.
schema := schemaForCtyType(e.ety) // this schema's ImpliedType will match e.ety
schema := SchemaForCtyElementType(e.ety) // this schema's ImpliedType will match e.ety
spec := schema.DecoderSpec()
vals := make([]cty.Value, len(e.blocks))
@ -167,7 +167,7 @@ func (e *fixupBlocksExpr) Value(ctx *hcl.EvalContext) (cty.Value, hcl.Diagnostic
func (e *fixupBlocksExpr) Variables() []hcl.Traversal {
var ret []hcl.Traversal
schema := schemaForCtyType(e.ety)
schema := SchemaForCtyElementType(e.ety)
spec := schema.DecoderSpec()
for _, block := range e.blocks {
ret = append(ret, hcldec.Variables(block.Body, spec)...)

View File

@ -99,10 +99,11 @@ func effectiveSchema(given *hcl.BodySchema, body hcl.Body, ambiguousNames map[st
return ret
}
// schemaForCtyType converts a cty object type into an approximately-equivalent
// configschema.Block. If the given type is not an object type then this
// SchemaForCtyElementType converts a cty object type into an
// approximately-equivalent configschema.Block representing the element of
// a list or set. If the given type is not an object type then this
// function will panic.
func schemaForCtyType(ty cty.Type) *configschema.Block {
func SchemaForCtyElementType(ty cty.Type) *configschema.Block {
atys := ty.AttributeTypes()
ret := &configschema.Block{
Attributes: make(map[string]*configschema.Attribute, len(atys)),
@ -115,3 +116,30 @@ func schemaForCtyType(ty cty.Type) *configschema.Block {
}
return ret
}
// SchemaForCtyContainerType converts a cty list-of-object or set-of-object type
// into an approximately-equivalent configschema.NestedBlock. If the given type
// is not of the expected kind then this function will panic.
func SchemaForCtyContainerType(ty cty.Type) *configschema.NestedBlock {
var nesting configschema.NestingMode
switch {
case ty.IsListType():
nesting = configschema.NestingList
case ty.IsSetType():
nesting = configschema.NestingSet
default:
panic("unsuitable type")
}
nested := SchemaForCtyElementType(ty.ElementType())
return &configschema.NestedBlock{
Nesting: nesting,
Block: *nested,
}
}
// TypeCanBeBlocks returns true if the given type is a list-of-object or
// set-of-object type, and would thus be subject to the blocktoattr fixup
// if used as an attribute type.
func TypeCanBeBlocks(ty cty.Type) bool {
return (ty.IsListType() || ty.IsSetType()) && ty.ElementType().IsObjectType()
}

View File

@ -34,7 +34,7 @@ func walkVariables(node dynblock.WalkVariablesNode, body hcl.Body, schema *confi
if blockS, exists := schema.BlockTypes[child.BlockTypeName]; exists {
vars = append(vars, walkVariables(child.Node, child.Body(), &blockS.Block)...)
} else if attrS, exists := schema.Attributes[child.BlockTypeName]; exists {
synthSchema := schemaForCtyType(attrS.Type.ElementType())
synthSchema := SchemaForCtyElementType(attrS.Type.ElementType())
vars = append(vars, walkVariables(child.Node, child.Body(), synthSchema)...)
}
}