github.com/rstandt/terraform@v0.12.32-0.20230710220336-b1063613405c/configs/configupgrade/upgrade_native.go (about)

     1  package configupgrade
     2  
     3  import (
     4  	"bytes"
     5  	"fmt"
     6  	"io"
     7  	"log"
     8  	"regexp"
     9  	"sort"
    10  	"strings"
    11  
    12  	version "github.com/hashicorp/go-version"
    13  
    14  	hcl1ast "github.com/hashicorp/hcl/hcl/ast"
    15  	hcl1parser "github.com/hashicorp/hcl/hcl/parser"
    16  	hcl1printer "github.com/hashicorp/hcl/hcl/printer"
    17  	hcl1token "github.com/hashicorp/hcl/hcl/token"
    18  
    19  	hcl2 "github.com/hashicorp/hcl/v2"
    20  	"github.com/zclconf/go-cty/cty"
    21  
    22  	"github.com/hashicorp/terraform/addrs"
    23  	backendinit "github.com/hashicorp/terraform/backend/init"
    24  	"github.com/hashicorp/terraform/configs/configschema"
    25  	"github.com/hashicorp/terraform/tfdiags"
    26  )
    27  
    28  type upgradeFileResult struct {
    29  	Content              []byte
    30  	ProviderRequirements map[string]version.Constraints
    31  }
    32  
    33  func (u *Upgrader) upgradeNativeSyntaxFile(filename string, src []byte, an *analysis) (upgradeFileResult, tfdiags.Diagnostics) {
    34  	var result upgradeFileResult
    35  	var diags tfdiags.Diagnostics
    36  
    37  	log.Printf("[TRACE] configupgrade: Working on %q", filename)
    38  
    39  	var buf bytes.Buffer
    40  
    41  	f, err := hcl1parser.Parse(src)
    42  	if err != nil {
    43  		return result, diags.Append(&hcl2.Diagnostic{
    44  			Severity: hcl2.DiagError,
    45  			Summary:  "Syntax error in configuration file",
    46  			Detail:   fmt.Sprintf("Error while parsing: %s", err),
    47  			Subject:  hcl1ErrSubjectRange(filename, err),
    48  		})
    49  	}
    50  
    51  	rootList := f.Node.(*hcl1ast.ObjectList)
    52  	rootItems := rootList.Items
    53  	adhocComments := collectAdhocComments(f)
    54  
    55  	for _, item := range rootItems {
    56  		comments := adhocComments.TakeBefore(item)
    57  		for _, group := range comments {
    58  			printComments(&buf, group)
    59  			buf.WriteByte('\n') // Extra separator after each group
    60  		}
    61  
    62  		blockType := item.Keys[0].Token.Value().(string)
    63  		labels := make([]string, len(item.Keys)-1)
    64  		for i, key := range item.Keys[1:] {
    65  			labels[i] = key.Token.Value().(string)
    66  		}
    67  		body, isObject := item.Val.(*hcl1ast.ObjectType)
    68  		if !isObject {
    69  			// Should never happen for valid input, since we don't expect
    70  			// any non-block items at our top level.
    71  			diags = diags.Append(&hcl2.Diagnostic{
    72  				Severity: hcl2.DiagWarning,
    73  				Summary:  "Unsupported top-level attribute",
    74  				Detail:   fmt.Sprintf("Attribute %q is not expected here, so its expression was not upgraded.", blockType),
    75  				Subject:  hcl1PosRange(filename, item.Keys[0].Pos()).Ptr(),
    76  			})
    77  			// Preserve the item as-is, using the hcl1printer package.
    78  			buf.WriteString("# TF-UPGRADE-TODO: Top-level attributes are not valid, so this was not automatically upgraded.\n")
    79  			hcl1printer.Fprint(&buf, item)
    80  			buf.WriteString("\n\n")
    81  			continue
    82  		}
    83  		declRange := hcl1PosRange(filename, item.Keys[0].Pos())
    84  
    85  		switch blockType {
    86  
    87  		case "resource", "data":
    88  			if len(labels) != 2 {
    89  				// Should never happen for valid input.
    90  				diags = diags.Append(&hcl2.Diagnostic{
    91  					Severity: hcl2.DiagError,
    92  					Summary:  fmt.Sprintf("Invalid %s block", blockType),
    93  					Detail:   fmt.Sprintf("A %s block must have two labels: the type and the name.", blockType),
    94  					Subject:  &declRange,
    95  				})
    96  				continue
    97  			}
    98  
    99  			rAddr := addrs.Resource{
   100  				Mode: addrs.ManagedResourceMode,
   101  				Type: labels[0],
   102  				Name: labels[1],
   103  			}
   104  			if blockType == "data" {
   105  				rAddr.Mode = addrs.DataResourceMode
   106  			}
   107  
   108  			log.Printf("[TRACE] configupgrade: Upgrading %s at %s", rAddr, declRange)
   109  			moreDiags := u.upgradeNativeSyntaxResource(filename, &buf, rAddr, item, an, adhocComments)
   110  			diags = diags.Append(moreDiags)
   111  
   112  		case "provider":
   113  			if len(labels) != 1 {
   114  				diags = diags.Append(&hcl2.Diagnostic{
   115  					Severity: hcl2.DiagError,
   116  					Summary:  fmt.Sprintf("Invalid %s block", blockType),
   117  					Detail:   fmt.Sprintf("A %s block must have one label: the provider type.", blockType),
   118  					Subject:  &declRange,
   119  				})
   120  				continue
   121  			}
   122  
   123  			pType := labels[0]
   124  			log.Printf("[TRACE] configupgrade: Upgrading provider.%s at %s", pType, declRange)
   125  			moreDiags := u.upgradeNativeSyntaxProvider(filename, &buf, pType, item, an, adhocComments)
   126  			diags = diags.Append(moreDiags)
   127  
   128  		case "terraform":
   129  			if len(labels) != 0 {
   130  				diags = diags.Append(&hcl2.Diagnostic{
   131  					Severity: hcl2.DiagError,
   132  					Summary:  fmt.Sprintf("Invalid %s block", blockType),
   133  					Detail:   fmt.Sprintf("A %s block must not have any labels.", blockType),
   134  					Subject:  &declRange,
   135  				})
   136  				continue
   137  			}
   138  			moreDiags := u.upgradeNativeSyntaxTerraformBlock(filename, &buf, item, an, adhocComments)
   139  			diags = diags.Append(moreDiags)
   140  
   141  		case "variable":
   142  			if len(labels) != 1 {
   143  				diags = diags.Append(&hcl2.Diagnostic{
   144  					Severity: hcl2.DiagError,
   145  					Summary:  fmt.Sprintf("Invalid %s block", blockType),
   146  					Detail:   fmt.Sprintf("A %s block must have one label: the variable name.", blockType),
   147  					Subject:  &declRange,
   148  				})
   149  				continue
   150  			}
   151  
   152  			printComments(&buf, item.LeadComment)
   153  			printBlockOpen(&buf, blockType, labels, item.LineComment)
   154  			rules := bodyContentRules{
   155  				"description": noInterpAttributeRule(filename, cty.String, an),
   156  				"default":     noInterpAttributeRule(filename, cty.DynamicPseudoType, an),
   157  				"type": maybeBareKeywordAttributeRule(filename, an, map[string]string{
   158  					// "list" and "map" in older versions were documented to
   159  					// mean list and map of strings, so we'll migrate to that
   160  					// and let the user adjust it to some other type if desired.
   161  					"list": `list(string)`,
   162  					"map":  `map(string)`,
   163  				}),
   164  			}
   165  			log.Printf("[TRACE] configupgrade: Upgrading var.%s at %s", labels[0], declRange)
   166  			bodyDiags := upgradeBlockBody(filename, fmt.Sprintf("var.%s", labels[0]), &buf, body.List.Items, body.Rbrace, rules, adhocComments)
   167  			diags = diags.Append(bodyDiags)
   168  			buf.WriteString("}\n\n")
   169  
   170  		case "output":
   171  			if len(labels) != 1 {
   172  				diags = diags.Append(&hcl2.Diagnostic{
   173  					Severity: hcl2.DiagError,
   174  					Summary:  fmt.Sprintf("Invalid %s block", blockType),
   175  					Detail:   fmt.Sprintf("A %s block must have one label: the output name.", blockType),
   176  					Subject:  &declRange,
   177  				})
   178  				continue
   179  			}
   180  
   181  			printComments(&buf, item.LeadComment)
   182  			if invalidLabel(labels[0]) {
   183  				printLabelTodo(&buf, labels[0])
   184  			}
   185  			printBlockOpen(&buf, blockType, labels, item.LineComment)
   186  
   187  			rules := bodyContentRules{
   188  				"description": noInterpAttributeRule(filename, cty.String, an),
   189  				"value":       normalAttributeRule(filename, cty.DynamicPseudoType, an),
   190  				"sensitive":   noInterpAttributeRule(filename, cty.Bool, an),
   191  				"depends_on":  dependsOnAttributeRule(filename, an),
   192  			}
   193  			log.Printf("[TRACE] configupgrade: Upgrading output.%s at %s", labels[0], declRange)
   194  			bodyDiags := upgradeBlockBody(filename, fmt.Sprintf("output.%s", labels[0]), &buf, body.List.Items, body.Rbrace, rules, adhocComments)
   195  			diags = diags.Append(bodyDiags)
   196  			buf.WriteString("}\n\n")
   197  
   198  		case "module":
   199  			if len(labels) != 1 {
   200  				diags = diags.Append(&hcl2.Diagnostic{
   201  					Severity: hcl2.DiagError,
   202  					Summary:  fmt.Sprintf("Invalid %s block", blockType),
   203  					Detail:   fmt.Sprintf("A %s block must have one label: the module call name.", blockType),
   204  					Subject:  &declRange,
   205  				})
   206  				continue
   207  			}
   208  
   209  			// Since upgrading is a single-module endeavor, we don't have access
   210  			// to the configuration of the child module here, but we know that
   211  			// in practice all arguments that aren't reserved meta-arguments
   212  			// in a module block are normal expression attributes so we'll
   213  			// start with the straightforward mapping of those and override
   214  			// the special lifecycle arguments below.
   215  			rules := justAttributesBodyRules(filename, body, an)
   216  			rules["source"] = moduleSourceRule(filename, an)
   217  			rules["version"] = noInterpAttributeRule(filename, cty.String, an)
   218  			rules["providers"] = func(buf *bytes.Buffer, blockAddr string, item *hcl1ast.ObjectItem) tfdiags.Diagnostics {
   219  				var diags tfdiags.Diagnostics
   220  				subBody, ok := item.Val.(*hcl1ast.ObjectType)
   221  				if !ok {
   222  					diags = diags.Append(&hcl2.Diagnostic{
   223  						Severity: hcl2.DiagError,
   224  						Summary:  "Invalid providers argument",
   225  						Detail:   `The "providers" argument must be a map from provider addresses in the child module to corresponding provider addresses in this module.`,
   226  						Subject:  &declRange,
   227  					})
   228  					return diags
   229  				}
   230  
   231  				// We're gonna cheat here and use justAttributesBodyRules to
   232  				// find all the attribute names but then just rewrite them all
   233  				// to be our specialized traversal-style mapping instead.
   234  				subRules := justAttributesBodyRules(filename, subBody, an)
   235  				for k := range subRules {
   236  					subRules[k] = maybeBareTraversalAttributeRule(filename, an)
   237  				}
   238  				buf.WriteString("providers = {\n")
   239  				bodyDiags := upgradeBlockBody(filename, blockAddr, buf, subBody.List.Items, body.Rbrace, subRules, adhocComments)
   240  				diags = diags.Append(bodyDiags)
   241  				buf.WriteString("}\n")
   242  
   243  				return diags
   244  			}
   245  
   246  			printComments(&buf, item.LeadComment)
   247  			printBlockOpen(&buf, blockType, labels, item.LineComment)
   248  			log.Printf("[TRACE] configupgrade: Upgrading module.%s at %s", labels[0], declRange)
   249  			bodyDiags := upgradeBlockBody(filename, fmt.Sprintf("module.%s", labels[0]), &buf, body.List.Items, body.Rbrace, rules, adhocComments)
   250  			diags = diags.Append(bodyDiags)
   251  			buf.WriteString("}\n\n")
   252  
   253  		case "locals":
   254  			log.Printf("[TRACE] configupgrade: Upgrading locals block at %s", declRange)
   255  			printComments(&buf, item.LeadComment)
   256  			printBlockOpen(&buf, blockType, labels, item.LineComment)
   257  
   258  			// The "locals" block contents are free-form declarations, so
   259  			// we'll just use the default attribute mapping rule for everything
   260  			// inside it.
   261  			rules := justAttributesBodyRules(filename, body, an)
   262  			log.Printf("[TRACE] configupgrade: Upgrading locals block at %s", declRange)
   263  			bodyDiags := upgradeBlockBody(filename, "locals", &buf, body.List.Items, body.Rbrace, rules, adhocComments)
   264  			diags = diags.Append(bodyDiags)
   265  			buf.WriteString("}\n\n")
   266  
   267  		default:
   268  			// Should never happen for valid input, because the above cases
   269  			// are exhaustive for valid blocks as of Terraform 0.11.
   270  			diags = diags.Append(&hcl2.Diagnostic{
   271  				Severity: hcl2.DiagWarning,
   272  				Summary:  "Unsupported root block type",
   273  				Detail:   fmt.Sprintf("The block type %q is not expected here, so its content was not upgraded.", blockType),
   274  				Subject:  hcl1PosRange(filename, item.Keys[0].Pos()).Ptr(),
   275  			})
   276  
   277  			// Preserve the block as-is, using the hcl1printer package.
   278  			buf.WriteString("# TF-UPGRADE-TODO: Block type was not recognized, so this block and its contents were not automatically upgraded.\n")
   279  			hcl1printer.Fprint(&buf, item)
   280  			buf.WriteString("\n\n")
   281  			continue
   282  		}
   283  	}
   284  
   285  	// Print out any leftover comments
   286  	for _, group := range *adhocComments {
   287  		printComments(&buf, group)
   288  	}
   289  
   290  	result.Content = buf.Bytes()
   291  
   292  	return result, diags
   293  }
   294  
   295  func (u *Upgrader) upgradeNativeSyntaxResource(filename string, buf *bytes.Buffer, addr addrs.Resource, item *hcl1ast.ObjectItem, an *analysis, adhocComments *commentQueue) tfdiags.Diagnostics {
   296  	var diags tfdiags.Diagnostics
   297  
   298  	body := item.Val.(*hcl1ast.ObjectType)
   299  	declRange := hcl1PosRange(filename, item.Keys[0].Pos())
   300  
   301  	// We should always have a schema for each provider in our analysis
   302  	// object. If not, it's a bug in the analyzer.
   303  	providerType, ok := an.ResourceProviderType[addr]
   304  	if !ok {
   305  		panic(fmt.Sprintf("unknown provider type for %s", addr.String()))
   306  	}
   307  	providerSchema, ok := an.ProviderSchemas[providerType]
   308  	if !ok {
   309  		panic(fmt.Sprintf("missing schema for provider type %q", providerType))
   310  	}
   311  	schema, _ := providerSchema.SchemaForResourceAddr(addr)
   312  	if schema == nil {
   313  		diags = diags.Append(&hcl2.Diagnostic{
   314  			Severity: hcl2.DiagError,
   315  			Summary:  "Unknown resource type",
   316  			Detail:   fmt.Sprintf("The resource type %q is not known to the currently-selected version of provider %q.", addr.Type, providerType),
   317  			Subject:  &declRange,
   318  		})
   319  		return diags
   320  	}
   321  
   322  	var blockType string
   323  	switch addr.Mode {
   324  	case addrs.ManagedResourceMode:
   325  		blockType = "resource"
   326  	case addrs.DataResourceMode:
   327  		blockType = "data"
   328  	}
   329  	labels := []string{addr.Type, addr.Name}
   330  
   331  	rules := schemaDefaultBodyRules(filename, schema, an, adhocComments)
   332  	rules["count"] = normalAttributeRule(filename, cty.Number, an)
   333  	rules["depends_on"] = dependsOnAttributeRule(filename, an)
   334  	rules["provider"] = maybeBareTraversalAttributeRule(filename, an)
   335  	rules["lifecycle"] = nestedBlockRule(filename, lifecycleBlockBodyRules(filename, an), an, adhocComments)
   336  	if addr.Mode == addrs.ManagedResourceMode {
   337  		rules["connection"] = connectionBlockRule(filename, addr.Type, an, adhocComments)
   338  		rules["provisioner"] = provisionerBlockRule(filename, addr.Type, an, adhocComments)
   339  	}
   340  
   341  	printComments(buf, item.LeadComment)
   342  	if invalidLabel(labels[1]) {
   343  		printLabelTodo(buf, labels[1])
   344  	}
   345  	printBlockOpen(buf, blockType, labels, item.LineComment)
   346  	bodyDiags := upgradeBlockBody(filename, addr.String(), buf, body.List.Items, body.Rbrace, rules, adhocComments)
   347  	diags = diags.Append(bodyDiags)
   348  	buf.WriteString("}\n\n")
   349  
   350  	return diags
   351  }
   352  
   353  func (u *Upgrader) upgradeNativeSyntaxProvider(filename string, buf *bytes.Buffer, typeName string, item *hcl1ast.ObjectItem, an *analysis, adhocComments *commentQueue) tfdiags.Diagnostics {
   354  	var diags tfdiags.Diagnostics
   355  
   356  	body := item.Val.(*hcl1ast.ObjectType)
   357  
   358  	// We should always have a schema for each provider in our analysis
   359  	// object. If not, it's a bug in the analyzer.
   360  	providerSchema, ok := an.ProviderSchemas[typeName]
   361  	if !ok {
   362  		panic(fmt.Sprintf("missing schema for provider type %q", typeName))
   363  	}
   364  	schema := providerSchema.Provider
   365  	rules := schemaDefaultBodyRules(filename, schema, an, adhocComments)
   366  	rules["alias"] = noInterpAttributeRule(filename, cty.String, an)
   367  	rules["version"] = noInterpAttributeRule(filename, cty.String, an)
   368  
   369  	printComments(buf, item.LeadComment)
   370  	printBlockOpen(buf, "provider", []string{typeName}, item.LineComment)
   371  	bodyDiags := upgradeBlockBody(filename, fmt.Sprintf("provider.%s", typeName), buf, body.List.Items, body.Rbrace, rules, adhocComments)
   372  	diags = diags.Append(bodyDiags)
   373  	buf.WriteString("}\n\n")
   374  
   375  	return diags
   376  }
   377  
   378  func (u *Upgrader) upgradeNativeSyntaxTerraformBlock(filename string, buf *bytes.Buffer, item *hcl1ast.ObjectItem, an *analysis, adhocComments *commentQueue) tfdiags.Diagnostics {
   379  	var diags tfdiags.Diagnostics
   380  
   381  	body := item.Val.(*hcl1ast.ObjectType)
   382  
   383  	rules := bodyContentRules{
   384  		"required_version": noInterpAttributeRule(filename, cty.String, an),
   385  		"backend": func(buf *bytes.Buffer, blockAddr string, item *hcl1ast.ObjectItem) tfdiags.Diagnostics {
   386  			var diags tfdiags.Diagnostics
   387  
   388  			declRange := hcl1PosRange(filename, item.Keys[0].Pos())
   389  			if len(item.Keys) != 2 {
   390  				diags = diags.Append(&hcl2.Diagnostic{
   391  					Severity: hcl2.DiagError,
   392  					Summary:  `Invalid backend block`,
   393  					Detail:   `A backend block must have one label: the backend type name.`,
   394  					Subject:  &declRange,
   395  				})
   396  				return diags
   397  			}
   398  
   399  			typeName := item.Keys[1].Token.Value().(string)
   400  			beFn := backendinit.Backend(typeName)
   401  			if beFn == nil {
   402  				diags = diags.Append(&hcl2.Diagnostic{
   403  					Severity: hcl2.DiagError,
   404  					Summary:  "Unsupported backend type",
   405  					Detail:   fmt.Sprintf("Terraform does not support a backend type named %q.", typeName),
   406  					Subject:  &declRange,
   407  				})
   408  				return diags
   409  			}
   410  			be := beFn()
   411  			schema := be.ConfigSchema()
   412  			rules := schemaNoInterpBodyRules(filename, schema, an, adhocComments)
   413  
   414  			body := item.Val.(*hcl1ast.ObjectType)
   415  
   416  			printComments(buf, item.LeadComment)
   417  			printBlockOpen(buf, "backend", []string{typeName}, item.LineComment)
   418  			bodyDiags := upgradeBlockBody(filename, fmt.Sprintf("terraform.backend.%s", typeName), buf, body.List.Items, body.Rbrace, rules, adhocComments)
   419  			diags = diags.Append(bodyDiags)
   420  			buf.WriteString("}\n")
   421  
   422  			return diags
   423  		},
   424  	}
   425  
   426  	printComments(buf, item.LeadComment)
   427  	printBlockOpen(buf, "terraform", nil, item.LineComment)
   428  	bodyDiags := upgradeBlockBody(filename, "terraform", buf, body.List.Items, body.Rbrace, rules, adhocComments)
   429  	diags = diags.Append(bodyDiags)
   430  	buf.WriteString("}\n\n")
   431  
   432  	return diags
   433  }
   434  
   435  func upgradeBlockBody(filename string, blockAddr string, buf *bytes.Buffer, args []*hcl1ast.ObjectItem, end hcl1token.Pos, rules bodyContentRules, adhocComments *commentQueue) tfdiags.Diagnostics {
   436  	var diags tfdiags.Diagnostics
   437  
   438  	for i, arg := range args {
   439  		comments := adhocComments.TakeBefore(arg)
   440  		for _, group := range comments {
   441  			printComments(buf, group)
   442  			buf.WriteByte('\n') // Extra separator after each group
   443  		}
   444  
   445  		printComments(buf, arg.LeadComment)
   446  
   447  		name := arg.Keys[0].Token.Value().(string)
   448  
   449  		rule, expected := rules[name]
   450  		if !expected {
   451  			if arg.Assign.IsValid() {
   452  				diags = diags.Append(&hcl2.Diagnostic{
   453  					Severity: hcl2.DiagError,
   454  					Summary:  "Unrecognized attribute name",
   455  					Detail:   fmt.Sprintf("No attribute named %q is expected in %s.", name, blockAddr),
   456  					Subject:  hcl1PosRange(filename, arg.Keys[0].Pos()).Ptr(),
   457  				})
   458  			} else {
   459  				diags = diags.Append(&hcl2.Diagnostic{
   460  					Severity: hcl2.DiagError,
   461  					Summary:  "Unrecognized block type",
   462  					Detail:   fmt.Sprintf("Blocks of type %q are not expected in %s.", name, blockAddr),
   463  					Subject:  hcl1PosRange(filename, arg.Keys[0].Pos()).Ptr(),
   464  				})
   465  			}
   466  			continue
   467  		}
   468  
   469  		itemDiags := rule(buf, blockAddr, arg)
   470  		diags = diags.Append(itemDiags)
   471  
   472  		// If we have another item and it's more than one line away
   473  		// from the current one then we'll print an extra blank line
   474  		// to retain that separation.
   475  		if (i + 1) < len(args) {
   476  			next := args[i+1]
   477  			thisPos := hcl1NodeEndPos(arg)
   478  			nextPos := next.Pos()
   479  			if nextPos.Line-thisPos.Line > 1 {
   480  				buf.WriteByte('\n')
   481  			}
   482  		}
   483  	}
   484  
   485  	// Before we return, we must also print any remaining adhocComments that
   486  	// appear between our last item and the closing brace.
   487  	comments := adhocComments.TakeBeforePos(end)
   488  	for i, group := range comments {
   489  		printComments(buf, group)
   490  		if i < len(comments)-1 {
   491  			buf.WriteByte('\n') // Extra separator after each group
   492  		}
   493  	}
   494  
   495  	return diags
   496  }
   497  
   498  // printDynamicBody prints out a conservative, exhaustive dynamic block body
   499  // for every attribute and nested block in the given schema, for situations
   500  // when a dynamic expression was being assigned to a block type name in input
   501  // configuration and so we can assume it's a list of maps but can't make
   502  // any assumptions about what subset of the schema-specified keys might be
   503  // present in the map values.
   504  func printDynamicBlockBody(buf *bytes.Buffer, iterName string, schema *configschema.Block) tfdiags.Diagnostics {
   505  	var diags tfdiags.Diagnostics
   506  
   507  	attrNames := make([]string, 0, len(schema.Attributes))
   508  	for name := range schema.Attributes {
   509  		attrNames = append(attrNames, name)
   510  	}
   511  	sort.Strings(attrNames)
   512  	for _, name := range attrNames {
   513  		attrS := schema.Attributes[name]
   514  		if !(attrS.Required || attrS.Optional) { // no Computed-only attributes
   515  			continue
   516  		}
   517  		if attrS.Required {
   518  			// For required attributes we can generate a simpler expression
   519  			// that just assumes the presence of the key representing the
   520  			// attribute value.
   521  			printAttribute(buf, name, []byte(fmt.Sprintf(`%s.value.%s`, iterName, name)), nil)
   522  		} else {
   523  			// Otherwise we must be conservative and generate a conditional
   524  			// lookup that will just populate nothing at all if the expected
   525  			// key is not present.
   526  			printAttribute(buf, name, []byte(fmt.Sprintf(`lookup(%s.value, %q, null)`, iterName, name)), nil)
   527  		}
   528  	}
   529  
   530  	blockTypeNames := make([]string, 0, len(schema.BlockTypes))
   531  	for name := range schema.BlockTypes {
   532  		blockTypeNames = append(blockTypeNames, name)
   533  	}
   534  	sort.Strings(blockTypeNames)
   535  	for i, name := range blockTypeNames {
   536  		blockS := schema.BlockTypes[name]
   537  
   538  		// We'll disregard any block type that consists only of computed
   539  		// attributes, since otherwise we'll just create weird empty blocks
   540  		// that do nothing except create confusion.
   541  		if !schemaHasSettableArguments(&blockS.Block) {
   542  			continue
   543  		}
   544  
   545  		if i > 0 || len(attrNames) > 0 {
   546  			buf.WriteByte('\n')
   547  		}
   548  		printBlockOpen(buf, "dynamic", []string{name}, nil)
   549  		switch blockS.Nesting {
   550  		case configschema.NestingMap:
   551  			printAttribute(buf, "for_each", []byte(fmt.Sprintf(`lookup(%s.value, %q, {})`, iterName, name)), nil)
   552  			printAttribute(buf, "labels", []byte(fmt.Sprintf(`[%s.key]`, name)), nil)
   553  		case configschema.NestingSingle, configschema.NestingGroup:
   554  			printAttribute(buf, "for_each", []byte(fmt.Sprintf(`lookup(%s.value, %q, null) != null ? [%s.value.%s] : []`, iterName, name, iterName, name)), nil)
   555  		default:
   556  			printAttribute(buf, "for_each", []byte(fmt.Sprintf(`lookup(%s.value, %q, [])`, iterName, name)), nil)
   557  		}
   558  		printBlockOpen(buf, "content", nil, nil)
   559  		moreDiags := printDynamicBlockBody(buf, name, &blockS.Block)
   560  		diags = diags.Append(moreDiags)
   561  		buf.WriteString("}\n")
   562  		buf.WriteString("}\n")
   563  	}
   564  
   565  	return diags
   566  }
   567  
   568  func printComments(buf *bytes.Buffer, group *hcl1ast.CommentGroup) {
   569  	if group == nil {
   570  		return
   571  	}
   572  	for _, comment := range group.List {
   573  		buf.WriteString(comment.Text)
   574  		buf.WriteByte('\n')
   575  	}
   576  }
   577  
   578  func printBlockOpen(buf *bytes.Buffer, blockType string, labels []string, commentGroup *hcl1ast.CommentGroup) {
   579  	buf.WriteString(blockType)
   580  	for _, label := range labels {
   581  		buf.WriteByte(' ')
   582  		printQuotedString(buf, label)
   583  	}
   584  	buf.WriteString(" {")
   585  	if commentGroup != nil {
   586  		for _, c := range commentGroup.List {
   587  			buf.WriteByte(' ')
   588  			buf.WriteString(c.Text)
   589  		}
   590  	}
   591  	buf.WriteByte('\n')
   592  }
   593  
   594  func printAttribute(buf *bytes.Buffer, name string, valSrc []byte, commentGroup *hcl1ast.CommentGroup) {
   595  	buf.WriteString(name)
   596  	buf.WriteString(" = ")
   597  	buf.Write(valSrc)
   598  	if commentGroup != nil {
   599  		for _, c := range commentGroup.List {
   600  			buf.WriteByte(' ')
   601  			buf.WriteString(c.Text)
   602  		}
   603  	}
   604  	buf.WriteByte('\n')
   605  }
   606  
   607  func printQuotedString(buf *bytes.Buffer, val string) {
   608  	buf.WriteByte('"')
   609  	printStringLiteralFromHILOutput(buf, val)
   610  	buf.WriteByte('"')
   611  }
   612  
   613  func printStringLiteralFromHILOutput(buf *bytes.Buffer, val string) {
   614  	val = strings.Replace(val, `\`, `\\`, -1)
   615  	val = strings.Replace(val, `"`, `\"`, -1)
   616  	val = strings.Replace(val, "\n", `\n`, -1)
   617  	val = strings.Replace(val, "\r", `\r`, -1)
   618  	val = strings.Replace(val, `${`, `$${`, -1)
   619  	val = strings.Replace(val, `%{`, `%%{`, -1)
   620  	buf.WriteString(val)
   621  }
   622  
   623  func printHeredocLiteralFromHILOutput(buf *bytes.Buffer, val string) {
   624  	val = strings.Replace(val, `${`, `$${`, -1)
   625  	val = strings.Replace(val, `%{`, `%%{`, -1)
   626  	buf.WriteString(val)
   627  }
   628  
   629  func collectAdhocComments(f *hcl1ast.File) *commentQueue {
   630  	comments := make(map[hcl1token.Pos]*hcl1ast.CommentGroup)
   631  	for _, c := range f.Comments {
   632  		comments[c.Pos()] = c
   633  	}
   634  
   635  	// We'll remove from our map any comments that are attached to specific
   636  	// nodes as lead or line comments, since we'll find those during our
   637  	// walk anyway.
   638  	hcl1ast.Walk(f, func(nn hcl1ast.Node) (hcl1ast.Node, bool) {
   639  		switch t := nn.(type) {
   640  		case *hcl1ast.LiteralType:
   641  			if t.LeadComment != nil {
   642  				for _, comment := range t.LeadComment.List {
   643  					delete(comments, comment.Pos())
   644  				}
   645  			}
   646  
   647  			if t.LineComment != nil {
   648  				for _, comment := range t.LineComment.List {
   649  					delete(comments, comment.Pos())
   650  				}
   651  			}
   652  		case *hcl1ast.ObjectItem:
   653  			if t.LeadComment != nil {
   654  				for _, comment := range t.LeadComment.List {
   655  					delete(comments, comment.Pos())
   656  				}
   657  			}
   658  
   659  			if t.LineComment != nil {
   660  				for _, comment := range t.LineComment.List {
   661  					delete(comments, comment.Pos())
   662  				}
   663  			}
   664  		}
   665  
   666  		return nn, true
   667  	})
   668  
   669  	if len(comments) == 0 {
   670  		var ret commentQueue
   671  		return &ret
   672  	}
   673  
   674  	ret := make([]*hcl1ast.CommentGroup, 0, len(comments))
   675  	for _, c := range comments {
   676  		ret = append(ret, c)
   677  	}
   678  	sort.Slice(ret, func(i, j int) bool {
   679  		return ret[i].Pos().Before(ret[j].Pos())
   680  	})
   681  	queue := commentQueue(ret)
   682  	return &queue
   683  }
   684  
   685  type commentQueue []*hcl1ast.CommentGroup
   686  
   687  func (q *commentQueue) TakeBeforeToken(token hcl1token.Token) []*hcl1ast.CommentGroup {
   688  	return q.TakeBeforePos(token.Pos)
   689  }
   690  
   691  func (q *commentQueue) TakeBefore(node hcl1ast.Node) []*hcl1ast.CommentGroup {
   692  	return q.TakeBeforePos(node.Pos())
   693  }
   694  
   695  func (q *commentQueue) TakeBeforePos(pos hcl1token.Pos) []*hcl1ast.CommentGroup {
   696  	toPos := pos
   697  	var i int
   698  	for i = 0; i < len(*q); i++ {
   699  		if (*q)[i].Pos().After(toPos) {
   700  			break
   701  		}
   702  	}
   703  	if i == 0 {
   704  		return nil
   705  	}
   706  
   707  	ret := (*q)[:i]
   708  	*q = (*q)[i:]
   709  
   710  	return ret
   711  }
   712  
   713  // hcl1NodeEndPos tries to find the latest possible position in the given
   714  // node. This is primarily to try to find the last line number of a multi-line
   715  // construct and is a best-effort sort of thing because HCL1 only tracks
   716  // start positions for tokens and has no generalized way to find the full
   717  // range for a single node.
   718  func hcl1NodeEndPos(node hcl1ast.Node) hcl1token.Pos {
   719  	switch tn := node.(type) {
   720  	case *hcl1ast.ObjectItem:
   721  		if tn.LineComment != nil && len(tn.LineComment.List) > 0 {
   722  			return tn.LineComment.List[len(tn.LineComment.List)-1].Start
   723  		}
   724  		return hcl1NodeEndPos(tn.Val)
   725  	case *hcl1ast.ListType:
   726  		return tn.Rbrack
   727  	case *hcl1ast.ObjectType:
   728  		return tn.Rbrace
   729  	default:
   730  		// If all else fails, we'll just return the position of what we were given.
   731  		return tn.Pos()
   732  	}
   733  }
   734  
   735  func hcl1ErrSubjectRange(filename string, err error) *hcl2.Range {
   736  	if pe, isPos := err.(*hcl1parser.PosError); isPos {
   737  		return hcl1PosRange(filename, pe.Pos).Ptr()
   738  	}
   739  	return nil
   740  }
   741  
   742  func hcl1PosRange(filename string, pos hcl1token.Pos) hcl2.Range {
   743  	return hcl2.Range{
   744  		Filename: filename,
   745  		Start: hcl2.Pos{
   746  			Line:   pos.Line,
   747  			Column: pos.Column,
   748  			Byte:   pos.Offset,
   749  		},
   750  		End: hcl2.Pos{
   751  			Line:   pos.Line,
   752  			Column: pos.Column,
   753  			Byte:   pos.Offset,
   754  		},
   755  	}
   756  }
   757  
   758  func passthruBlockTodo(w io.Writer, node hcl1ast.Node, msg string) {
   759  	fmt.Fprintf(w, "\n# TF-UPGRADE-TODO: %s\n", msg)
   760  	hcl1printer.Fprint(w, node)
   761  	w.Write([]byte{'\n', '\n'})
   762  }
   763  
   764  func schemaHasSettableArguments(schema *configschema.Block) bool {
   765  	for _, attrS := range schema.Attributes {
   766  		if attrS.Optional || attrS.Required {
   767  			return true
   768  		}
   769  	}
   770  	for _, blockS := range schema.BlockTypes {
   771  		if schemaHasSettableArguments(&blockS.Block) {
   772  			return true
   773  		}
   774  	}
   775  	return false
   776  }
   777  
   778  func invalidLabel(name string) bool {
   779  	matched, err := regexp.Match(`[0-9]`, []byte{name[0]})
   780  	if err == nil {
   781  		return matched
   782  	}
   783  	// This isn't likely, but if there's an error here we'll just ignore it and
   784  	// move on.
   785  	return false
   786  }
   787  
   788  func printLabelTodo(buf *bytes.Buffer, label string) {
   789  	buf.WriteString("# TF-UPGRADE-TODO: In Terraform v0.11 and earlier, it was possible to begin a\n" +
   790  		"# resource name with a number, but it is no longer possible in Terraform v0.12.\n" +
   791  		"#\n" +
   792  		"# Rename the resource and run `terraform state mv` to apply the rename in the\n" +
   793  		"# state. Detailed information on the `state move` command can be found in the\n" +
   794  		"# documentation online: https://www.terraform.io/docs/commands/state/mv.html\n",
   795  	)
   796  }