570 lines
17 KiB
Go
570 lines
17 KiB
Go
package configupgrade
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"io"
|
|
"sort"
|
|
"strings"
|
|
|
|
"github.com/hashicorp/terraform/addrs"
|
|
|
|
version "github.com/hashicorp/go-version"
|
|
|
|
hcl1ast "github.com/hashicorp/hcl/hcl/ast"
|
|
hcl1parser "github.com/hashicorp/hcl/hcl/parser"
|
|
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/hashicorp/terraform/tfdiags"
|
|
)
|
|
|
|
type upgradeFileResult struct {
|
|
Content []byte
|
|
ProviderRequirements map[string]version.Constraints
|
|
}
|
|
|
|
func (u *Upgrader) upgradeNativeSyntaxFile(filename string, src []byte, an *analysis) (upgradeFileResult, tfdiags.Diagnostics) {
|
|
var result upgradeFileResult
|
|
var diags tfdiags.Diagnostics
|
|
|
|
var buf bytes.Buffer
|
|
|
|
f, err := hcl1parser.Parse(src)
|
|
if err != nil {
|
|
return result, diags.Append(&hcl2.Diagnostic{
|
|
Severity: hcl2.DiagError,
|
|
Summary: "Syntax error in configuration file",
|
|
Detail: fmt.Sprintf("Error while parsing: %s", err),
|
|
Subject: hcl1ErrSubjectRange(filename, err),
|
|
})
|
|
}
|
|
|
|
rootList := f.Node.(*hcl1ast.ObjectList)
|
|
rootItems := rootList.Items
|
|
adhocComments := collectAdhocComments(f)
|
|
|
|
for _, item := range rootItems {
|
|
comments := adhocComments.TakeBefore(item)
|
|
for _, group := range comments {
|
|
printComments(&buf, group)
|
|
buf.WriteByte('\n') // Extra separator after each group
|
|
}
|
|
|
|
blockType := item.Keys[0].Token.Value().(string)
|
|
labels := make([]string, len(item.Keys)-1)
|
|
for i, key := range item.Keys[1:] {
|
|
labels[i] = key.Token.Value().(string)
|
|
}
|
|
body, isObject := item.Val.(*hcl1ast.ObjectType)
|
|
if !isObject {
|
|
// Should never happen for valid input, since we don't expect
|
|
// any non-block items at our top level.
|
|
diags = diags.Append(&hcl2.Diagnostic{
|
|
Severity: hcl2.DiagWarning,
|
|
Summary: "Unsupported top-level attribute",
|
|
Detail: fmt.Sprintf("Attribute %q is not expected here, so its expression was not upgraded.", blockType),
|
|
Subject: hcl1PosRange(filename, item.Keys[0].Pos()).Ptr(),
|
|
})
|
|
// Preserve the item as-is, using the hcl1printer package.
|
|
buf.WriteString("# TF-UPGRADE-TODO: Top-level attributes are not valid, so this was not automatically upgraded.\n")
|
|
hcl1printer.Fprint(&buf, item)
|
|
buf.WriteString("\n\n")
|
|
continue
|
|
}
|
|
declRange := hcl1PosRange(filename, item.Keys[0].Pos())
|
|
|
|
switch blockType {
|
|
|
|
case "resource":
|
|
if len(labels) != 2 {
|
|
// Should never happen for valid input.
|
|
diags = diags.Append(&hcl2.Diagnostic{
|
|
Severity: hcl2.DiagError,
|
|
Summary: "Invalid resource block",
|
|
Detail: "A resource block must have two labels: the resource type and name.",
|
|
Subject: &declRange,
|
|
})
|
|
continue
|
|
}
|
|
|
|
rAddr := addrs.Resource{
|
|
Mode: addrs.ManagedResourceMode,
|
|
Type: labels[0],
|
|
Name: labels[1],
|
|
}
|
|
|
|
// We should always have a schema for each provider in our analysis
|
|
// object. If not, it's a bug in the analyzer.
|
|
providerType, ok := an.ResourceProviderType[rAddr]
|
|
if !ok {
|
|
panic(fmt.Sprintf("unknown provider type for %s", rAddr.String()))
|
|
}
|
|
providerSchema, ok := an.ProviderSchemas[providerType]
|
|
if !ok {
|
|
panic(fmt.Sprintf("missing schema for provider type %q", providerType))
|
|
}
|
|
schema, ok := providerSchema.ResourceTypes[rAddr.Type]
|
|
if !ok {
|
|
diags = diags.Append(&hcl2.Diagnostic{
|
|
Severity: hcl2.DiagError,
|
|
Summary: "Unknown resource type",
|
|
Detail: fmt.Sprintf("The resource type %q is not known to the currently-selected version of provider %q.", rAddr.Type, providerType),
|
|
Subject: &declRange,
|
|
})
|
|
continue
|
|
}
|
|
|
|
printComments(&buf, item.LeadComment)
|
|
printBlockOpen(&buf, blockType, labels, item.LineComment)
|
|
args := body.List.Items
|
|
for i, arg := range args {
|
|
comments := adhocComments.TakeBefore(arg)
|
|
for _, group := range comments {
|
|
printComments(&buf, group)
|
|
buf.WriteByte('\n') // Extra separator after each group
|
|
}
|
|
|
|
printComments(&buf, arg.LeadComment)
|
|
|
|
name := arg.Keys[0].Token.Value().(string)
|
|
//labelKeys := arg.Keys[1:]
|
|
|
|
switch name {
|
|
// TODO: Special case for all of the "meta-arguments" allowed
|
|
// in a resource block, such as "count", "lifecycle",
|
|
// "provisioner", etc.
|
|
|
|
default:
|
|
// We'll consult the schema to see how we ought to interpret
|
|
// this item.
|
|
|
|
if _, isAttr := schema.Attributes[name]; isAttr {
|
|
// 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(arg.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.", rAddr.Type, name),
|
|
Subject: hcl1PosRange(filename, arg.Keys[0].Pos()).Ptr(),
|
|
})
|
|
continue
|
|
}
|
|
|
|
valSrc, valDiags := upgradeExpr(arg.Val, filename, true, an)
|
|
diags = diags.Append(valDiags)
|
|
printAttribute(&buf, arg.Keys[0].Token.Value().(string), valSrc, arg.LineComment)
|
|
} else if _, isBlock := schema.BlockTypes[name]; isBlock {
|
|
// TODO: Also upgrade blocks.
|
|
// 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, arg)
|
|
buf.WriteByte('\n')
|
|
} else {
|
|
if arg.Assign.IsValid() {
|
|
diags = diags.Append(&hcl2.Diagnostic{
|
|
Severity: hcl2.DiagError,
|
|
Summary: "Unrecognized attribute name",
|
|
Detail: fmt.Sprintf("Resource type %s does not expect an attribute named %q.", rAddr.Type, name),
|
|
Subject: hcl1PosRange(filename, arg.Keys[0].Pos()).Ptr(),
|
|
})
|
|
} else {
|
|
diags = diags.Append(&hcl2.Diagnostic{
|
|
Severity: hcl2.DiagError,
|
|
Summary: "Unrecognized block type",
|
|
Detail: fmt.Sprintf("Resource type %s does not expect blocks of type %q.", rAddr.Type, name),
|
|
Subject: hcl1PosRange(filename, arg.Keys[0].Pos()).Ptr(),
|
|
})
|
|
}
|
|
continue
|
|
}
|
|
}
|
|
|
|
// If we have another item and it's more than one line away
|
|
// from the current one then we'll print an extra blank line
|
|
// to retain that separation.
|
|
if (i + 1) < len(args) {
|
|
next := args[i+1]
|
|
thisPos := arg.Pos()
|
|
nextPos := next.Pos()
|
|
if nextPos.Line-thisPos.Line > 1 {
|
|
buf.WriteByte('\n')
|
|
}
|
|
}
|
|
}
|
|
buf.WriteString("}\n\n")
|
|
|
|
case "variable":
|
|
printComments(&buf, item.LeadComment)
|
|
printBlockOpen(&buf, blockType, labels, item.LineComment)
|
|
args := body.List.Items
|
|
for i, arg := range args {
|
|
if len(arg.Keys) != 1 {
|
|
// Should never happen for valid input, since there are no nested blocks expected here.
|
|
diags = diags.Append(&hcl2.Diagnostic{
|
|
Severity: hcl2.DiagWarning,
|
|
Summary: "Invalid nested block",
|
|
Detail: fmt.Sprintf("Blocks of type %q are not expected here, so this was not automatically upgraded.", arg.Keys[0].Token.Value().(string)),
|
|
Subject: hcl1PosRange(filename, arg.Keys[0].Pos()).Ptr(),
|
|
})
|
|
// Preserve the item as-is, using the hcl1printer package.
|
|
buf.WriteString("\n# TF-UPGRADE-TODO: Blocks are not expected here, so this was not automatically upgraded.\n")
|
|
hcl1printer.Fprint(&buf, arg)
|
|
buf.WriteString("\n\n")
|
|
continue
|
|
}
|
|
|
|
comments := adhocComments.TakeBefore(arg)
|
|
for _, group := range comments {
|
|
printComments(&buf, group)
|
|
buf.WriteByte('\n') // Extra separator after each group
|
|
}
|
|
|
|
printComments(&buf, arg.LeadComment)
|
|
|
|
switch arg.Keys[0].Token.Value() {
|
|
case "type":
|
|
// It is no longer idiomatic to place the type keyword in quotes,
|
|
// so we'll unquote it here as long as it looks like the result
|
|
// will be valid.
|
|
if lit, isLit := arg.Val.(*hcl1ast.LiteralType); isLit {
|
|
if lit.Token.Type == hcl1token.STRING {
|
|
kw := lit.Token.Value().(string)
|
|
if hcl2syntax.ValidIdentifier(kw) {
|
|
|
|
// "list" and "map" in older versions really meant
|
|
// list and map of strings, so we'll migrate to
|
|
// that and let the user adjust to "any" as
|
|
// the element type if desired.
|
|
switch strings.TrimSpace(kw) {
|
|
case "list":
|
|
kw = "list(string)"
|
|
case "map":
|
|
kw = "map(string)"
|
|
}
|
|
|
|
printAttribute(&buf, "type", []byte(kw), arg.LineComment)
|
|
break
|
|
}
|
|
}
|
|
}
|
|
// If we got something invalid there then we'll just fall through
|
|
// into the default case and migrate it as a normal expression.
|
|
fallthrough
|
|
default:
|
|
valSrc, valDiags := upgradeExpr(arg.Val, filename, false, an)
|
|
diags = diags.Append(valDiags)
|
|
printAttribute(&buf, arg.Keys[0].Token.Value().(string), valSrc, arg.LineComment)
|
|
}
|
|
|
|
// If we have another item and it's more than one line away
|
|
// from the current one then we'll print an extra blank line
|
|
// to retain that separation.
|
|
if (i + 1) < len(args) {
|
|
next := args[i+1]
|
|
thisPos := arg.Pos()
|
|
nextPos := next.Pos()
|
|
if nextPos.Line-thisPos.Line > 1 {
|
|
buf.WriteByte('\n')
|
|
}
|
|
}
|
|
}
|
|
buf.WriteString("}\n\n")
|
|
|
|
case "output":
|
|
printComments(&buf, item.LeadComment)
|
|
printBlockOpen(&buf, blockType, labels, item.LineComment)
|
|
args := body.List.Items
|
|
for i, arg := range args {
|
|
if len(arg.Keys) != 1 {
|
|
// Should never happen for valid input, since there are no nested blocks expected here.
|
|
diags = diags.Append(&hcl2.Diagnostic{
|
|
Severity: hcl2.DiagWarning,
|
|
Summary: "Invalid nested block",
|
|
Detail: fmt.Sprintf("Blocks of type %q are not expected here, so this was not automatically upgraded.", arg.Keys[0].Token.Value().(string)),
|
|
Subject: hcl1PosRange(filename, arg.Keys[0].Pos()).Ptr(),
|
|
})
|
|
// Preserve the item as-is, using the hcl1printer package.
|
|
buf.WriteString("\n# TF-UPGRADE-TODO: Blocks are not expected here, so this was not automatically upgraded.\n")
|
|
hcl1printer.Fprint(&buf, arg)
|
|
buf.WriteString("\n\n")
|
|
continue
|
|
}
|
|
|
|
comments := adhocComments.TakeBefore(arg)
|
|
for _, group := range comments {
|
|
printComments(&buf, group)
|
|
buf.WriteByte('\n') // Extra separator after each group
|
|
}
|
|
|
|
printComments(&buf, arg.LeadComment)
|
|
|
|
interp := false
|
|
switch arg.Keys[0].Token.Value() {
|
|
case "value":
|
|
interp = true
|
|
}
|
|
|
|
valSrc, valDiags := upgradeExpr(arg.Val, filename, interp, an)
|
|
diags = diags.Append(valDiags)
|
|
printAttribute(&buf, arg.Keys[0].Token.Value().(string), valSrc, arg.LineComment)
|
|
|
|
// If we have another item and it's more than one line away
|
|
// from the current one then we'll print an extra blank line
|
|
// to retain that separation.
|
|
if (i + 1) < len(args) {
|
|
next := args[i+1]
|
|
thisPos := arg.Pos()
|
|
nextPos := next.Pos()
|
|
if nextPos.Line-thisPos.Line > 1 {
|
|
buf.WriteByte('\n')
|
|
}
|
|
}
|
|
}
|
|
buf.WriteString("}\n\n")
|
|
|
|
case "locals":
|
|
printComments(&buf, item.LeadComment)
|
|
printBlockOpen(&buf, blockType, labels, item.LineComment)
|
|
|
|
args := body.List.Items
|
|
for i, arg := range args {
|
|
if len(arg.Keys) != 1 {
|
|
// Should never happen for valid input, since there are no nested blocks expected here.
|
|
diags = diags.Append(&hcl2.Diagnostic{
|
|
Severity: hcl2.DiagWarning,
|
|
Summary: "Invalid nested block",
|
|
Detail: fmt.Sprintf("Blocks of type %q are not expected here, so this was not automatically upgraded.", arg.Keys[0].Token.Value().(string)),
|
|
Subject: hcl1PosRange(filename, arg.Keys[0].Pos()).Ptr(),
|
|
})
|
|
// Preserve the item as-is, using the hcl1printer package.
|
|
buf.WriteString("\n# TF-UPGRADE-TODO: Blocks are not expected here, so this was not automatically upgraded.\n")
|
|
hcl1printer.Fprint(&buf, arg)
|
|
buf.WriteString("\n\n")
|
|
continue
|
|
}
|
|
|
|
comments := adhocComments.TakeBefore(arg)
|
|
for _, group := range comments {
|
|
printComments(&buf, group)
|
|
buf.WriteByte('\n') // Extra separator after each group
|
|
}
|
|
|
|
printComments(&buf, arg.LeadComment)
|
|
|
|
name := arg.Keys[0].Token.Value().(string)
|
|
expr := arg.Val
|
|
exprSrc, exprDiags := upgradeExpr(expr, filename, true, an)
|
|
diags = diags.Append(exprDiags)
|
|
printAttribute(&buf, name, exprSrc, arg.LineComment)
|
|
|
|
// If we have another item and it's more than one line away
|
|
// from the current one then we'll print an extra blank line
|
|
// to retain that separation.
|
|
if (i + 1) < len(args) {
|
|
next := args[i+1]
|
|
thisPos := arg.Pos()
|
|
nextPos := next.Pos()
|
|
if nextPos.Line-thisPos.Line > 1 {
|
|
buf.WriteByte('\n')
|
|
}
|
|
}
|
|
}
|
|
buf.WriteString("}\n\n")
|
|
|
|
default:
|
|
// Should never happen for valid input, because the above cases
|
|
// are exhaustive for valid blocks as of Terraform 0.11.
|
|
diags = diags.Append(&hcl2.Diagnostic{
|
|
Severity: hcl2.DiagWarning,
|
|
Summary: "Unsupported root block type",
|
|
Detail: fmt.Sprintf("The block type %q is not expected here, so its content was not upgraded.", blockType),
|
|
Subject: hcl1PosRange(filename, item.Keys[0].Pos()).Ptr(),
|
|
})
|
|
|
|
// Preserve the block as-is, using the hcl1printer package.
|
|
buf.WriteString("# TF-UPGRADE-TODO: Block type was not recognized, so this block and its contents were not automatically upgraded.\n")
|
|
hcl1printer.Fprint(&buf, item)
|
|
buf.WriteString("\n\n")
|
|
continue
|
|
}
|
|
}
|
|
|
|
// Print out any leftover comments
|
|
for _, group := range *adhocComments {
|
|
printComments(&buf, group)
|
|
}
|
|
|
|
result.Content = buf.Bytes()
|
|
|
|
return result, diags
|
|
}
|
|
|
|
func printComments(buf *bytes.Buffer, group *hcl1ast.CommentGroup) {
|
|
if group == nil {
|
|
return
|
|
}
|
|
for _, comment := range group.List {
|
|
buf.WriteString(comment.Text)
|
|
buf.WriteByte('\n')
|
|
}
|
|
}
|
|
|
|
func printBlockOpen(buf *bytes.Buffer, blockType string, labels []string, commentGroup *hcl1ast.CommentGroup) {
|
|
buf.WriteString(blockType)
|
|
for _, label := range labels {
|
|
buf.WriteByte(' ')
|
|
printQuotedString(buf, label)
|
|
}
|
|
buf.WriteString(" {")
|
|
if commentGroup != nil {
|
|
for _, c := range commentGroup.List {
|
|
buf.WriteByte(' ')
|
|
buf.WriteString(c.Text)
|
|
}
|
|
}
|
|
buf.WriteByte('\n')
|
|
}
|
|
|
|
func printAttribute(buf *bytes.Buffer, name string, valSrc []byte, commentGroup *hcl1ast.CommentGroup) {
|
|
buf.WriteString(name)
|
|
buf.WriteString(" = ")
|
|
buf.Write(valSrc)
|
|
if commentGroup != nil {
|
|
for _, c := range commentGroup.List {
|
|
buf.WriteByte(' ')
|
|
buf.WriteString(c.Text)
|
|
}
|
|
}
|
|
buf.WriteByte('\n')
|
|
}
|
|
|
|
func printQuotedString(buf *bytes.Buffer, val string) {
|
|
buf.WriteByte('"')
|
|
printStringLiteralFromHILOutput(buf, val)
|
|
buf.WriteByte('"')
|
|
}
|
|
|
|
func printStringLiteralFromHILOutput(buf *bytes.Buffer, val string) {
|
|
val = strings.Replace(val, `\`, `\\`, -1)
|
|
val = strings.Replace(val, `"`, `\"`, -1)
|
|
val = strings.Replace(val, "\n", `\n`, -1)
|
|
val = strings.Replace(val, "\r", `\r`, -1)
|
|
val = strings.Replace(val, `${`, `$${`, -1)
|
|
val = strings.Replace(val, `%{`, `%%{`, -1)
|
|
buf.WriteString(val)
|
|
}
|
|
|
|
func collectAdhocComments(f *hcl1ast.File) *commentQueue {
|
|
comments := make(map[hcl1token.Pos]*hcl1ast.CommentGroup)
|
|
for _, c := range f.Comments {
|
|
comments[c.Pos()] = c
|
|
}
|
|
|
|
// We'll remove from our map any comments that are attached to specific
|
|
// nodes as lead or line comments, since we'll find those during our
|
|
// walk anyway.
|
|
hcl1ast.Walk(f, func(nn hcl1ast.Node) (hcl1ast.Node, bool) {
|
|
switch t := nn.(type) {
|
|
case *hcl1ast.LiteralType:
|
|
if t.LeadComment != nil {
|
|
for _, comment := range t.LeadComment.List {
|
|
delete(comments, comment.Pos())
|
|
}
|
|
}
|
|
|
|
if t.LineComment != nil {
|
|
for _, comment := range t.LineComment.List {
|
|
delete(comments, comment.Pos())
|
|
}
|
|
}
|
|
case *hcl1ast.ObjectItem:
|
|
if t.LeadComment != nil {
|
|
for _, comment := range t.LeadComment.List {
|
|
delete(comments, comment.Pos())
|
|
}
|
|
}
|
|
|
|
if t.LineComment != nil {
|
|
for _, comment := range t.LineComment.List {
|
|
delete(comments, comment.Pos())
|
|
}
|
|
}
|
|
}
|
|
|
|
return nn, true
|
|
})
|
|
|
|
if len(comments) == 0 {
|
|
var ret commentQueue
|
|
return &ret
|
|
}
|
|
|
|
ret := make([]*hcl1ast.CommentGroup, 0, len(comments))
|
|
for _, c := range comments {
|
|
ret = append(ret, c)
|
|
}
|
|
sort.Slice(ret, func(i, j int) bool {
|
|
return ret[i].Pos().Before(ret[j].Pos())
|
|
})
|
|
queue := commentQueue(ret)
|
|
return &queue
|
|
}
|
|
|
|
type commentQueue []*hcl1ast.CommentGroup
|
|
|
|
func (q *commentQueue) TakeBefore(node hcl1ast.Node) []*hcl1ast.CommentGroup {
|
|
toPos := node.Pos()
|
|
var i int
|
|
for i = 0; i < len(*q); i++ {
|
|
if (*q)[i].Pos().After(toPos) {
|
|
break
|
|
}
|
|
}
|
|
if i == 0 {
|
|
return nil
|
|
}
|
|
|
|
ret := (*q)[:i]
|
|
*q = (*q)[i:]
|
|
|
|
return ret
|
|
}
|
|
|
|
func hcl1ErrSubjectRange(filename string, err error) *hcl2.Range {
|
|
if pe, isPos := err.(*hcl1parser.PosError); isPos {
|
|
return hcl1PosRange(filename, pe.Pos).Ptr()
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func hcl1PosRange(filename string, pos hcl1token.Pos) hcl2.Range {
|
|
return hcl2.Range{
|
|
Filename: filename,
|
|
Start: hcl2.Pos{
|
|
Line: pos.Line,
|
|
Column: pos.Column,
|
|
Byte: pos.Offset,
|
|
},
|
|
End: hcl2.Pos{
|
|
Line: pos.Line,
|
|
Column: pos.Column,
|
|
Byte: pos.Offset,
|
|
},
|
|
}
|
|
}
|
|
|
|
func passthruBlockTodo(w io.Writer, node hcl1ast.Node, msg string) {
|
|
fmt.Fprintf(w, "\n# TF-UPGRADE-TODO: %s\n", msg)
|
|
hcl1printer.Fprint(w, node)
|
|
w.Write([]byte{'\n', '\n'})
|
|
}
|