terraform/configs/configupgrade/upgrade_body.go

185 lines
6.8 KiB
Go

package configupgrade
import (
"bytes"
"fmt"
hcl1ast "github.com/hashicorp/hcl/hcl/ast"
hcl1printer "github.com/hashicorp/hcl/hcl/printer"
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/tfdiags"
)
// bodyContentRules is a mapping from item names (argument names and block type
// names) to a "rule" function defining what to do with an item of that type.
type bodyContentRules map[string]bodyItemRule
// bodyItemRule is just a function to write an upgraded representation of a
// particular given item to the given buffer. This is generic to handle various
// different mapping rules, though most values will be those constructed by
// other helper functions below.
type bodyItemRule func(buf *bytes.Buffer, blockAddr string, item *hcl1ast.ObjectItem) tfdiags.Diagnostics
func normalAttributeRule(filename string, wantTy cty.Type, an *analysis) bodyItemRule {
exprRule := func(val interface{}) ([]byte, tfdiags.Diagnostics) {
return upgradeExpr(val, filename, true, an)
}
return attributeRule(filename, wantTy, an, exprRule)
}
func noInterpAttributeRule(filename string, wantTy cty.Type, an *analysis) bodyItemRule {
exprRule := func(val interface{}) ([]byte, tfdiags.Diagnostics) {
return upgradeExpr(val, filename, false, an)
}
return attributeRule(filename, wantTy, an, exprRule)
}
func maybeBareKeywordAttributeRule(filename string, an *analysis, specials map[string]string) bodyItemRule {
exprRule := func(val interface{}) ([]byte, tfdiags.Diagnostics) {
// If the expression is a literal that would be valid as a naked keyword
// then we'll turn it into one.
if lit, isLit := val.(*hcl1ast.LiteralType); isLit {
if lit.Token.Type == hcl1token.STRING {
kw := lit.Token.Value().(string)
if hcl2syntax.ValidIdentifier(kw) {
// If we have a special mapping rule for this keyword,
// we'll let that override what the user gave.
if override := specials[kw]; override != "" {
kw = override
}
return []byte(kw), nil
}
}
}
return upgradeExpr(val, filename, false, an)
}
return attributeRule(filename, cty.String, an, exprRule)
}
func maybeBareTraversalAttributeRule(filename string, an *analysis) bodyItemRule {
exprRule := func(val interface{}) ([]byte, tfdiags.Diagnostics) {
// If the expression is a literal that would be valid as a naked
// absolute traversal then we'll turn it into one.
if lit, isLit := val.(*hcl1ast.LiteralType); isLit {
if lit.Token.Type == hcl1token.STRING {
trStr := lit.Token.Value().(string)
trSrc := []byte(trStr)
_, trDiags := hcl2syntax.ParseTraversalAbs(trSrc, "", hcl2.Pos{})
if !trDiags.HasErrors() {
return trSrc, nil
}
}
}
return upgradeExpr(val, filename, false, an)
}
return attributeRule(filename, cty.String, an, exprRule)
}
func dependsOnAttributeRule(filename string, an *analysis) bodyItemRule {
// FIXME: Should dig into the individual list items here and try to unwrap
// them as naked references, as well as upgrading any legacy-style index
// references like aws_instance.foo.0 to be aws_instance.foo[0] instead.
exprRule := func(val interface{}) ([]byte, tfdiags.Diagnostics) {
return upgradeExpr(val, filename, false, an)
}
return attributeRule(filename, cty.List(cty.String), an, exprRule)
}
func attributeRule(filename string, wantTy cty.Type, an *analysis, upgradeExpr func(val interface{}) ([]byte, tfdiags.Diagnostics)) bodyItemRule {
return func(buf *bytes.Buffer, blockAddr string, item *hcl1ast.ObjectItem) tfdiags.Diagnostics {
var diags tfdiags.Diagnostics
name := item.Keys[0].Token.Value().(string)
// We'll tolerate a block with no labels here as a degenerate
// way to assign a map, but we can't migrate a block that has
// labels. In practice this should never happen because
// nested blocks in resource blocks did not accept labels
// prior to v0.12.
if len(item.Keys) != 1 {
diags = diags.Append(&hcl2.Diagnostic{
Severity: hcl2.DiagError,
Summary: "Block where attribute was expected",
Detail: fmt.Sprintf("Within %s the name %q is an attribute name, not a block type.", blockAddr, name),
Subject: hcl1PosRange(filename, item.Keys[0].Pos()).Ptr(),
})
return diags
}
valSrc, valDiags := upgradeExpr(item.Val)
diags = diags.Append(valDiags)
printAttribute(buf, item.Keys[0].Token.Value().(string), valSrc, item.LineComment)
return diags
}
}
func nestedBlockRule(filename string, nestedRules bodyContentRules, an *analysis) bodyItemRule {
return func(buf *bytes.Buffer, blockAddr string, item *hcl1ast.ObjectItem) tfdiags.Diagnostics {
// TODO: Deal with this.
// In particular we need to handle the tricky case where
// a user attempts to treat a block type name like it's
// an attribute, by producing a "dynamic" block.
hcl1printer.Fprint(buf, item)
buf.WriteByte('\n')
return nil
}
}
// schemaDefaultBodyRules constructs standard body content rules for the given
// schema. Each call is guaranteed to produce a distinct object so that
// callers can safely mutate the result in order to impose custom rules
// in addition to or instead of those created by default, for situations
// where schema-based and predefined items mix in a single body.
func schemaDefaultBodyRules(filename string, schema *configschema.Block, an *analysis) bodyContentRules {
ret := make(bodyContentRules)
if schema == nil {
// Shouldn't happen in any real case, but often crops up in tests
// where the mock schemas tend to be incomplete.
return ret
}
for name, attrS := range schema.Attributes {
ret[name] = normalAttributeRule(filename, attrS.Type, an)
}
for name, blockS := range schema.BlockTypes {
nestedRules := schemaDefaultBodyRules(filename, &blockS.Block, an)
ret[name] = nestedBlockRule(filename, nestedRules, an)
}
return ret
}
// schemaNoInterpBodyRules constructs standard body content rules for the given
// schema. Each call is guaranteed to produce a distinct object so that
// callers can safely mutate the result in order to impose custom rules
// in addition to or instead of those created by default, for situations
// where schema-based and predefined items mix in a single body.
func schemaNoInterpBodyRules(filename string, schema *configschema.Block, an *analysis) bodyContentRules {
ret := make(bodyContentRules)
if schema == nil {
// Shouldn't happen in any real case, but often crops up in tests
// where the mock schemas tend to be incomplete.
return ret
}
for name, attrS := range schema.Attributes {
ret[name] = noInterpAttributeRule(filename, attrS.Type, an)
}
for name, blockS := range schema.BlockTypes {
nestedRules := schemaDefaultBodyRules(filename, &blockS.Block, an)
ret[name] = nestedBlockRule(filename, nestedRules, an)
}
return ret
}