mirror of
https://github.com/opentofu/opentofu.git
synced 2024-12-28 18:01:01 -06:00
88e76fa9ef
In study of existing providers we've found a pattern we werent previously accounting for of using a nested block type to represent a group of arguments that relate to a particular feature that is always enabled but where it improves configuration readability to group all of its settings together in a nested block. The existing NestingSingle was not a good fit for this because it is designed under the assumption that the presence or absence of the block has some significance in enabling or disabling the relevant feature, and so for these always-active cases we'd generate a misleading plan where the settings for the feature appear totally absent, rather than showing the default values that will be selected. NestingGroup is, therefore, a slight variation of NestingSingle where presence vs. absence of the block is not distinguishable (it's never null) and instead its contents are treated as unset when the block is absent. This then in turn causes any default values associated with the nested arguments to be honored and displayed in the plan whenever the block is not explicitly configured. The current SDK cannot activate this mode, but that's okay because its "legacy type system" opt-out flag allows it to force a block to be processed in this way anyway. We're adding this now so that we can introduce the feature in a future SDK without causing a breaking change to the protocol, since the set of possible block nesting modes is not extensible.
797 lines
26 KiB
Go
797 lines
26 KiB
Go
package configupgrade
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"io"
|
|
"log"
|
|
"regexp"
|
|
"sort"
|
|
"strings"
|
|
|
|
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"
|
|
"github.com/zclconf/go-cty/cty"
|
|
|
|
"github.com/hashicorp/terraform/addrs"
|
|
backendinit "github.com/hashicorp/terraform/backend/init"
|
|
"github.com/hashicorp/terraform/configs/configschema"
|
|
"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
|
|
|
|
log.Printf("[TRACE] configupgrade: Working on %q", filename)
|
|
|
|
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", "data":
|
|
if len(labels) != 2 {
|
|
// Should never happen for valid input.
|
|
diags = diags.Append(&hcl2.Diagnostic{
|
|
Severity: hcl2.DiagError,
|
|
Summary: fmt.Sprintf("Invalid %s block", blockType),
|
|
Detail: fmt.Sprintf("A %s block must have two labels: the type and the name.", blockType),
|
|
Subject: &declRange,
|
|
})
|
|
continue
|
|
}
|
|
|
|
rAddr := addrs.Resource{
|
|
Mode: addrs.ManagedResourceMode,
|
|
Type: labels[0],
|
|
Name: labels[1],
|
|
}
|
|
if blockType == "data" {
|
|
rAddr.Mode = addrs.DataResourceMode
|
|
}
|
|
|
|
log.Printf("[TRACE] configupgrade: Upgrading %s at %s", rAddr, declRange)
|
|
moreDiags := u.upgradeNativeSyntaxResource(filename, &buf, rAddr, item, an, adhocComments)
|
|
diags = diags.Append(moreDiags)
|
|
|
|
case "provider":
|
|
if len(labels) != 1 {
|
|
diags = diags.Append(&hcl2.Diagnostic{
|
|
Severity: hcl2.DiagError,
|
|
Summary: fmt.Sprintf("Invalid %s block", blockType),
|
|
Detail: fmt.Sprintf("A %s block must have one label: the provider type.", blockType),
|
|
Subject: &declRange,
|
|
})
|
|
continue
|
|
}
|
|
|
|
pType := labels[0]
|
|
log.Printf("[TRACE] configupgrade: Upgrading provider.%s at %s", pType, declRange)
|
|
moreDiags := u.upgradeNativeSyntaxProvider(filename, &buf, pType, item, an, adhocComments)
|
|
diags = diags.Append(moreDiags)
|
|
|
|
case "terraform":
|
|
if len(labels) != 0 {
|
|
diags = diags.Append(&hcl2.Diagnostic{
|
|
Severity: hcl2.DiagError,
|
|
Summary: fmt.Sprintf("Invalid %s block", blockType),
|
|
Detail: fmt.Sprintf("A %s block must not have any labels.", blockType),
|
|
Subject: &declRange,
|
|
})
|
|
continue
|
|
}
|
|
moreDiags := u.upgradeNativeSyntaxTerraformBlock(filename, &buf, item, an, adhocComments)
|
|
diags = diags.Append(moreDiags)
|
|
|
|
case "variable":
|
|
if len(labels) != 1 {
|
|
diags = diags.Append(&hcl2.Diagnostic{
|
|
Severity: hcl2.DiagError,
|
|
Summary: fmt.Sprintf("Invalid %s block", blockType),
|
|
Detail: fmt.Sprintf("A %s block must have one label: the variable name.", blockType),
|
|
Subject: &declRange,
|
|
})
|
|
continue
|
|
}
|
|
|
|
printComments(&buf, item.LeadComment)
|
|
printBlockOpen(&buf, blockType, labels, item.LineComment)
|
|
rules := bodyContentRules{
|
|
"description": noInterpAttributeRule(filename, cty.String, an),
|
|
"default": noInterpAttributeRule(filename, cty.DynamicPseudoType, an),
|
|
"type": maybeBareKeywordAttributeRule(filename, an, map[string]string{
|
|
// "list" and "map" in older versions were documented to
|
|
// mean list and map of strings, so we'll migrate to that
|
|
// and let the user adjust it to some other type if desired.
|
|
"list": `list(string)`,
|
|
"map": `map(string)`,
|
|
}),
|
|
}
|
|
log.Printf("[TRACE] configupgrade: Upgrading var.%s at %s", labels[0], declRange)
|
|
bodyDiags := upgradeBlockBody(filename, fmt.Sprintf("var.%s", labels[0]), &buf, body.List.Items, body.Rbrace, rules, adhocComments)
|
|
diags = diags.Append(bodyDiags)
|
|
buf.WriteString("}\n\n")
|
|
|
|
case "output":
|
|
if len(labels) != 1 {
|
|
diags = diags.Append(&hcl2.Diagnostic{
|
|
Severity: hcl2.DiagError,
|
|
Summary: fmt.Sprintf("Invalid %s block", blockType),
|
|
Detail: fmt.Sprintf("A %s block must have one label: the output name.", blockType),
|
|
Subject: &declRange,
|
|
})
|
|
continue
|
|
}
|
|
|
|
printComments(&buf, item.LeadComment)
|
|
if invalidLabel(labels[0]) {
|
|
printLabelTodo(&buf, labels[0])
|
|
}
|
|
printBlockOpen(&buf, blockType, labels, item.LineComment)
|
|
|
|
rules := bodyContentRules{
|
|
"description": noInterpAttributeRule(filename, cty.String, an),
|
|
"value": normalAttributeRule(filename, cty.DynamicPseudoType, an),
|
|
"sensitive": noInterpAttributeRule(filename, cty.Bool, an),
|
|
"depends_on": dependsOnAttributeRule(filename, an),
|
|
}
|
|
log.Printf("[TRACE] configupgrade: Upgrading output.%s at %s", labels[0], declRange)
|
|
bodyDiags := upgradeBlockBody(filename, fmt.Sprintf("output.%s", labels[0]), &buf, body.List.Items, body.Rbrace, rules, adhocComments)
|
|
diags = diags.Append(bodyDiags)
|
|
buf.WriteString("}\n\n")
|
|
|
|
case "module":
|
|
if len(labels) != 1 {
|
|
diags = diags.Append(&hcl2.Diagnostic{
|
|
Severity: hcl2.DiagError,
|
|
Summary: fmt.Sprintf("Invalid %s block", blockType),
|
|
Detail: fmt.Sprintf("A %s block must have one label: the module call name.", blockType),
|
|
Subject: &declRange,
|
|
})
|
|
continue
|
|
}
|
|
|
|
// Since upgrading is a single-module endeavor, we don't have access
|
|
// to the configuration of the child module here, but we know that
|
|
// in practice all arguments that aren't reserved meta-arguments
|
|
// in a module block are normal expression attributes so we'll
|
|
// start with the straightforward mapping of those and override
|
|
// the special lifecycle arguments below.
|
|
rules := justAttributesBodyRules(filename, body, an)
|
|
rules["source"] = moduleSourceRule(filename, an)
|
|
rules["version"] = noInterpAttributeRule(filename, cty.String, an)
|
|
rules["providers"] = func(buf *bytes.Buffer, blockAddr string, item *hcl1ast.ObjectItem) tfdiags.Diagnostics {
|
|
var diags tfdiags.Diagnostics
|
|
subBody, ok := item.Val.(*hcl1ast.ObjectType)
|
|
if !ok {
|
|
diags = diags.Append(&hcl2.Diagnostic{
|
|
Severity: hcl2.DiagError,
|
|
Summary: "Invalid providers argument",
|
|
Detail: `The "providers" argument must be a map from provider addresses in the child module to corresponding provider addresses in this module.`,
|
|
Subject: &declRange,
|
|
})
|
|
return diags
|
|
}
|
|
|
|
// We're gonna cheat here and use justAttributesBodyRules to
|
|
// find all the attribute names but then just rewrite them all
|
|
// to be our specialized traversal-style mapping instead.
|
|
subRules := justAttributesBodyRules(filename, subBody, an)
|
|
for k := range subRules {
|
|
subRules[k] = maybeBareTraversalAttributeRule(filename, an)
|
|
}
|
|
buf.WriteString("providers = {\n")
|
|
bodyDiags := upgradeBlockBody(filename, blockAddr, buf, subBody.List.Items, body.Rbrace, subRules, adhocComments)
|
|
diags = diags.Append(bodyDiags)
|
|
buf.WriteString("}\n")
|
|
|
|
return diags
|
|
}
|
|
|
|
printComments(&buf, item.LeadComment)
|
|
printBlockOpen(&buf, blockType, labels, item.LineComment)
|
|
log.Printf("[TRACE] configupgrade: Upgrading module.%s at %s", labels[0], declRange)
|
|
bodyDiags := upgradeBlockBody(filename, fmt.Sprintf("module.%s", labels[0]), &buf, body.List.Items, body.Rbrace, rules, adhocComments)
|
|
diags = diags.Append(bodyDiags)
|
|
buf.WriteString("}\n\n")
|
|
|
|
case "locals":
|
|
log.Printf("[TRACE] configupgrade: Upgrading locals block at %s", declRange)
|
|
printComments(&buf, item.LeadComment)
|
|
printBlockOpen(&buf, blockType, labels, item.LineComment)
|
|
|
|
// The "locals" block contents are free-form declarations, so
|
|
// we'll just use the default attribute mapping rule for everything
|
|
// inside it.
|
|
rules := justAttributesBodyRules(filename, body, an)
|
|
log.Printf("[TRACE] configupgrade: Upgrading locals block at %s", declRange)
|
|
bodyDiags := upgradeBlockBody(filename, "locals", &buf, body.List.Items, body.Rbrace, rules, adhocComments)
|
|
diags = diags.Append(bodyDiags)
|
|
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 (u *Upgrader) upgradeNativeSyntaxResource(filename string, buf *bytes.Buffer, addr addrs.Resource, item *hcl1ast.ObjectItem, an *analysis, adhocComments *commentQueue) tfdiags.Diagnostics {
|
|
var diags tfdiags.Diagnostics
|
|
|
|
body := item.Val.(*hcl1ast.ObjectType)
|
|
declRange := hcl1PosRange(filename, item.Keys[0].Pos())
|
|
|
|
// 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[addr]
|
|
if !ok {
|
|
panic(fmt.Sprintf("unknown provider type for %s", addr.String()))
|
|
}
|
|
providerSchema, ok := an.ProviderSchemas[providerType]
|
|
if !ok {
|
|
panic(fmt.Sprintf("missing schema for provider type %q", providerType))
|
|
}
|
|
schema, _ := providerSchema.SchemaForResourceAddr(addr)
|
|
if schema == nil {
|
|
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.", addr.Type, providerType),
|
|
Subject: &declRange,
|
|
})
|
|
return diags
|
|
}
|
|
|
|
var blockType string
|
|
switch addr.Mode {
|
|
case addrs.ManagedResourceMode:
|
|
blockType = "resource"
|
|
case addrs.DataResourceMode:
|
|
blockType = "data"
|
|
}
|
|
labels := []string{addr.Type, addr.Name}
|
|
|
|
rules := schemaDefaultBodyRules(filename, schema, an, adhocComments)
|
|
rules["count"] = normalAttributeRule(filename, cty.Number, an)
|
|
rules["depends_on"] = dependsOnAttributeRule(filename, an)
|
|
rules["provider"] = maybeBareTraversalAttributeRule(filename, an)
|
|
rules["lifecycle"] = nestedBlockRule(filename, lifecycleBlockBodyRules(filename, an), an, adhocComments)
|
|
if addr.Mode == addrs.ManagedResourceMode {
|
|
rules["connection"] = connectionBlockRule(filename, addr.Type, an, adhocComments)
|
|
rules["provisioner"] = provisionerBlockRule(filename, addr.Type, an, adhocComments)
|
|
}
|
|
|
|
printComments(buf, item.LeadComment)
|
|
if invalidLabel(labels[1]) {
|
|
printLabelTodo(buf, labels[1])
|
|
}
|
|
printBlockOpen(buf, blockType, labels, item.LineComment)
|
|
bodyDiags := upgradeBlockBody(filename, addr.String(), buf, body.List.Items, body.Rbrace, rules, adhocComments)
|
|
diags = diags.Append(bodyDiags)
|
|
buf.WriteString("}\n\n")
|
|
|
|
return diags
|
|
}
|
|
|
|
func (u *Upgrader) upgradeNativeSyntaxProvider(filename string, buf *bytes.Buffer, typeName string, item *hcl1ast.ObjectItem, an *analysis, adhocComments *commentQueue) tfdiags.Diagnostics {
|
|
var diags tfdiags.Diagnostics
|
|
|
|
body := item.Val.(*hcl1ast.ObjectType)
|
|
|
|
// We should always have a schema for each provider in our analysis
|
|
// object. If not, it's a bug in the analyzer.
|
|
providerSchema, ok := an.ProviderSchemas[typeName]
|
|
if !ok {
|
|
panic(fmt.Sprintf("missing schema for provider type %q", typeName))
|
|
}
|
|
schema := providerSchema.Provider
|
|
rules := schemaDefaultBodyRules(filename, schema, an, adhocComments)
|
|
rules["alias"] = noInterpAttributeRule(filename, cty.String, an)
|
|
rules["version"] = noInterpAttributeRule(filename, cty.String, an)
|
|
|
|
printComments(buf, item.LeadComment)
|
|
printBlockOpen(buf, "provider", []string{typeName}, item.LineComment)
|
|
bodyDiags := upgradeBlockBody(filename, fmt.Sprintf("provider.%s", typeName), buf, body.List.Items, body.Rbrace, rules, adhocComments)
|
|
diags = diags.Append(bodyDiags)
|
|
buf.WriteString("}\n\n")
|
|
|
|
return diags
|
|
}
|
|
|
|
func (u *Upgrader) upgradeNativeSyntaxTerraformBlock(filename string, buf *bytes.Buffer, item *hcl1ast.ObjectItem, an *analysis, adhocComments *commentQueue) tfdiags.Diagnostics {
|
|
var diags tfdiags.Diagnostics
|
|
|
|
body := item.Val.(*hcl1ast.ObjectType)
|
|
|
|
rules := bodyContentRules{
|
|
"required_version": noInterpAttributeRule(filename, cty.String, an),
|
|
"backend": func(buf *bytes.Buffer, blockAddr string, item *hcl1ast.ObjectItem) tfdiags.Diagnostics {
|
|
var diags tfdiags.Diagnostics
|
|
|
|
declRange := hcl1PosRange(filename, item.Keys[0].Pos())
|
|
if len(item.Keys) != 2 {
|
|
diags = diags.Append(&hcl2.Diagnostic{
|
|
Severity: hcl2.DiagError,
|
|
Summary: `Invalid backend block`,
|
|
Detail: `A backend block must have one label: the backend type name.`,
|
|
Subject: &declRange,
|
|
})
|
|
return diags
|
|
}
|
|
|
|
typeName := item.Keys[1].Token.Value().(string)
|
|
beFn := backendinit.Backend(typeName)
|
|
if beFn == nil {
|
|
diags = diags.Append(&hcl2.Diagnostic{
|
|
Severity: hcl2.DiagError,
|
|
Summary: "Unsupported backend type",
|
|
Detail: fmt.Sprintf("Terraform does not support a backend type named %q.", typeName),
|
|
Subject: &declRange,
|
|
})
|
|
return diags
|
|
}
|
|
be := beFn()
|
|
schema := be.ConfigSchema()
|
|
rules := schemaNoInterpBodyRules(filename, schema, an, adhocComments)
|
|
|
|
body := item.Val.(*hcl1ast.ObjectType)
|
|
|
|
printComments(buf, item.LeadComment)
|
|
printBlockOpen(buf, "backend", []string{typeName}, item.LineComment)
|
|
bodyDiags := upgradeBlockBody(filename, fmt.Sprintf("terraform.backend.%s", typeName), buf, body.List.Items, body.Rbrace, rules, adhocComments)
|
|
diags = diags.Append(bodyDiags)
|
|
buf.WriteString("}\n")
|
|
|
|
return diags
|
|
},
|
|
}
|
|
|
|
printComments(buf, item.LeadComment)
|
|
printBlockOpen(buf, "terraform", nil, item.LineComment)
|
|
bodyDiags := upgradeBlockBody(filename, "terraform", buf, body.List.Items, body.Rbrace, rules, adhocComments)
|
|
diags = diags.Append(bodyDiags)
|
|
buf.WriteString("}\n\n")
|
|
|
|
return diags
|
|
}
|
|
|
|
func upgradeBlockBody(filename string, blockAddr string, buf *bytes.Buffer, args []*hcl1ast.ObjectItem, end hcl1token.Pos, rules bodyContentRules, adhocComments *commentQueue) tfdiags.Diagnostics {
|
|
var diags tfdiags.Diagnostics
|
|
|
|
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)
|
|
|
|
rule, expected := rules[name]
|
|
if !expected {
|
|
if arg.Assign.IsValid() {
|
|
diags = diags.Append(&hcl2.Diagnostic{
|
|
Severity: hcl2.DiagError,
|
|
Summary: "Unrecognized attribute name",
|
|
Detail: fmt.Sprintf("No attribute named %q is expected in %s.", name, blockAddr),
|
|
Subject: hcl1PosRange(filename, arg.Keys[0].Pos()).Ptr(),
|
|
})
|
|
} else {
|
|
diags = diags.Append(&hcl2.Diagnostic{
|
|
Severity: hcl2.DiagError,
|
|
Summary: "Unrecognized block type",
|
|
Detail: fmt.Sprintf("Blocks of type %q are not expected in %s.", name, blockAddr),
|
|
Subject: hcl1PosRange(filename, arg.Keys[0].Pos()).Ptr(),
|
|
})
|
|
}
|
|
continue
|
|
}
|
|
|
|
itemDiags := rule(buf, blockAddr, arg)
|
|
diags = diags.Append(itemDiags)
|
|
|
|
// 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 := hcl1NodeEndPos(arg)
|
|
nextPos := next.Pos()
|
|
if nextPos.Line-thisPos.Line > 1 {
|
|
buf.WriteByte('\n')
|
|
}
|
|
}
|
|
}
|
|
|
|
// Before we return, we must also print any remaining adhocComments that
|
|
// appear between our last item and the closing brace.
|
|
comments := adhocComments.TakeBeforePos(end)
|
|
for i, group := range comments {
|
|
printComments(buf, group)
|
|
if i < len(comments)-1 {
|
|
buf.WriteByte('\n') // Extra separator after each group
|
|
}
|
|
}
|
|
|
|
return diags
|
|
}
|
|
|
|
// printDynamicBody prints out a conservative, exhaustive dynamic block body
|
|
// for every attribute and nested block in the given schema, for situations
|
|
// when a dynamic expression was being assigned to a block type name in input
|
|
// configuration and so we can assume it's a list of maps but can't make
|
|
// any assumptions about what subset of the schema-specified keys might be
|
|
// present in the map values.
|
|
func printDynamicBlockBody(buf *bytes.Buffer, iterName string, schema *configschema.Block) tfdiags.Diagnostics {
|
|
var diags tfdiags.Diagnostics
|
|
|
|
attrNames := make([]string, 0, len(schema.Attributes))
|
|
for name := range schema.Attributes {
|
|
attrNames = append(attrNames, name)
|
|
}
|
|
sort.Strings(attrNames)
|
|
for _, name := range attrNames {
|
|
attrS := schema.Attributes[name]
|
|
if !(attrS.Required || attrS.Optional) { // no Computed-only attributes
|
|
continue
|
|
}
|
|
if attrS.Required {
|
|
// For required attributes we can generate a simpler expression
|
|
// that just assumes the presence of the key representing the
|
|
// attribute value.
|
|
printAttribute(buf, name, []byte(fmt.Sprintf(`%s.value.%s`, iterName, name)), nil)
|
|
} else {
|
|
// Otherwise we must be conservative and generate a conditional
|
|
// lookup that will just populate nothing at all if the expected
|
|
// key is not present.
|
|
printAttribute(buf, name, []byte(fmt.Sprintf(`lookup(%s.value, %q, null)`, iterName, name)), nil)
|
|
}
|
|
}
|
|
|
|
blockTypeNames := make([]string, 0, len(schema.BlockTypes))
|
|
for name := range schema.BlockTypes {
|
|
blockTypeNames = append(blockTypeNames, name)
|
|
}
|
|
sort.Strings(blockTypeNames)
|
|
for i, name := range blockTypeNames {
|
|
blockS := schema.BlockTypes[name]
|
|
|
|
// We'll disregard any block type that consists only of computed
|
|
// attributes, since otherwise we'll just create weird empty blocks
|
|
// that do nothing except create confusion.
|
|
if !schemaHasSettableArguments(&blockS.Block) {
|
|
continue
|
|
}
|
|
|
|
if i > 0 || len(attrNames) > 0 {
|
|
buf.WriteByte('\n')
|
|
}
|
|
printBlockOpen(buf, "dynamic", []string{name}, nil)
|
|
switch blockS.Nesting {
|
|
case configschema.NestingMap:
|
|
printAttribute(buf, "for_each", []byte(fmt.Sprintf(`lookup(%s.value, %q, {})`, iterName, name)), nil)
|
|
printAttribute(buf, "labels", []byte(fmt.Sprintf(`[%s.key]`, name)), nil)
|
|
case configschema.NestingSingle, configschema.NestingGroup:
|
|
printAttribute(buf, "for_each", []byte(fmt.Sprintf(`lookup(%s.value, %q, null) != null ? [%s.value.%s] : []`, iterName, name, iterName, name)), nil)
|
|
default:
|
|
printAttribute(buf, "for_each", []byte(fmt.Sprintf(`lookup(%s.value, %q, [])`, iterName, name)), nil)
|
|
}
|
|
printBlockOpen(buf, "content", nil, nil)
|
|
moreDiags := printDynamicBlockBody(buf, name, &blockS.Block)
|
|
diags = diags.Append(moreDiags)
|
|
buf.WriteString("}\n")
|
|
buf.WriteString("}\n")
|
|
}
|
|
|
|
return 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 printHeredocLiteralFromHILOutput(buf *bytes.Buffer, val string) {
|
|
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) TakeBeforeToken(token hcl1token.Token) []*hcl1ast.CommentGroup {
|
|
return q.TakeBeforePos(token.Pos)
|
|
}
|
|
|
|
func (q *commentQueue) TakeBefore(node hcl1ast.Node) []*hcl1ast.CommentGroup {
|
|
return q.TakeBeforePos(node.Pos())
|
|
}
|
|
|
|
func (q *commentQueue) TakeBeforePos(pos hcl1token.Pos) []*hcl1ast.CommentGroup {
|
|
toPos := 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
|
|
}
|
|
|
|
// hcl1NodeEndPos tries to find the latest possible position in the given
|
|
// node. This is primarily to try to find the last line number of a multi-line
|
|
// construct and is a best-effort sort of thing because HCL1 only tracks
|
|
// start positions for tokens and has no generalized way to find the full
|
|
// range for a single node.
|
|
func hcl1NodeEndPos(node hcl1ast.Node) hcl1token.Pos {
|
|
switch tn := node.(type) {
|
|
case *hcl1ast.ObjectItem:
|
|
if tn.LineComment != nil && len(tn.LineComment.List) > 0 {
|
|
return tn.LineComment.List[len(tn.LineComment.List)-1].Start
|
|
}
|
|
return hcl1NodeEndPos(tn.Val)
|
|
case *hcl1ast.ListType:
|
|
return tn.Rbrack
|
|
case *hcl1ast.ObjectType:
|
|
return tn.Rbrace
|
|
default:
|
|
// If all else fails, we'll just return the position of what we were given.
|
|
return tn.Pos()
|
|
}
|
|
}
|
|
|
|
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'})
|
|
}
|
|
|
|
func schemaHasSettableArguments(schema *configschema.Block) bool {
|
|
for _, attrS := range schema.Attributes {
|
|
if attrS.Optional || attrS.Required {
|
|
return true
|
|
}
|
|
}
|
|
for _, blockS := range schema.BlockTypes {
|
|
if schemaHasSettableArguments(&blockS.Block) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func invalidLabel(name string) bool {
|
|
matched, err := regexp.Match(`[0-9]`, []byte{name[0]})
|
|
if err == nil {
|
|
return matched
|
|
}
|
|
// This isn't likely, but if there's an error here we'll just ignore it and
|
|
// move on.
|
|
return false
|
|
}
|
|
|
|
func printLabelTodo(buf *bytes.Buffer, label string) {
|
|
buf.WriteString("# TF-UPGRADE-TODO: In Terraform v0.11 and earlier, it was possible to begin a\n" +
|
|
"# resource name with a number, but it is no longer possible in Terraform v0.12.\n" +
|
|
"#\n" +
|
|
"# Rename the resource and run `terraform state mv` to apply the rename in the\n" +
|
|
"# state. Detailed information on the `state move` command can be found in the\n" +
|
|
"# documentation online: https://www.terraform.io/docs/commands/state/mv.html\n",
|
|
)
|
|
}
|