185 lines
6.8 KiB
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
|
|
}
|