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'}) }