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

     1  package configupgrade
     2  
     3  import (
     4  	"bytes"
     5  	"fmt"
     6  	"os"
     7  	"path/filepath"
     8  	"sort"
     9  	"strconv"
    10  	"strings"
    11  
    12  	hcl1ast "github.com/hashicorp/hcl/hcl/ast"
    13  	hcl1token "github.com/hashicorp/hcl/hcl/token"
    14  	hcl2 "github.com/hashicorp/hcl/v2"
    15  	hcl2syntax "github.com/hashicorp/hcl/v2/hclsyntax"
    16  	"github.com/hashicorp/terraform/configs/configschema"
    17  	"github.com/hashicorp/terraform/lang/blocktoattr"
    18  	"github.com/hashicorp/terraform/registry/regsrc"
    19  	"github.com/hashicorp/terraform/terraform"
    20  	"github.com/hashicorp/terraform/tfdiags"
    21  	"github.com/zclconf/go-cty/cty"
    22  )
    23  
    24  // bodyContentRules is a mapping from item names (argument names and block type
    25  // names) to a "rule" function defining what to do with an item of that type.
    26  type bodyContentRules map[string]bodyItemRule
    27  
    28  // bodyItemRule is just a function to write an upgraded representation of a
    29  // particular given item to the given buffer. This is generic to handle various
    30  // different mapping rules, though most values will be those constructed by
    31  // other helper functions below.
    32  type bodyItemRule func(buf *bytes.Buffer, blockAddr string, item *hcl1ast.ObjectItem) tfdiags.Diagnostics
    33  
    34  func normalAttributeRule(filename string, wantTy cty.Type, an *analysis) bodyItemRule {
    35  	exprRule := func(val interface{}) ([]byte, tfdiags.Diagnostics) {
    36  		return upgradeExpr(val, filename, true, an)
    37  	}
    38  	return attributeRule(filename, wantTy, an, exprRule)
    39  }
    40  
    41  func noInterpAttributeRule(filename string, wantTy cty.Type, an *analysis) bodyItemRule {
    42  	exprRule := func(val interface{}) ([]byte, tfdiags.Diagnostics) {
    43  		return upgradeExpr(val, filename, false, an)
    44  	}
    45  	return attributeRule(filename, wantTy, an, exprRule)
    46  }
    47  
    48  func maybeBareKeywordAttributeRule(filename string, an *analysis, specials map[string]string) bodyItemRule {
    49  	exprRule := func(val interface{}) ([]byte, tfdiags.Diagnostics) {
    50  		// If the expression is a literal that would be valid as a naked keyword
    51  		// then we'll turn it into one.
    52  		if lit, isLit := val.(*hcl1ast.LiteralType); isLit {
    53  			if lit.Token.Type == hcl1token.STRING {
    54  				kw := lit.Token.Value().(string)
    55  				if hcl2syntax.ValidIdentifier(kw) {
    56  
    57  					// If we have a special mapping rule for this keyword,
    58  					// we'll let that override what the user gave.
    59  					if override := specials[kw]; override != "" {
    60  						kw = override
    61  					}
    62  
    63  					return []byte(kw), nil
    64  				}
    65  			}
    66  		}
    67  
    68  		return upgradeExpr(val, filename, false, an)
    69  	}
    70  	return attributeRule(filename, cty.String, an, exprRule)
    71  }
    72  
    73  func maybeBareTraversalAttributeRule(filename string, an *analysis) bodyItemRule {
    74  	exprRule := func(val interface{}) ([]byte, tfdiags.Diagnostics) {
    75  		return upgradeTraversalExpr(val, filename, an)
    76  	}
    77  	return attributeRule(filename, cty.String, an, exprRule)
    78  }
    79  
    80  func dependsOnAttributeRule(filename string, an *analysis) bodyItemRule {
    81  	return func(buf *bytes.Buffer, blockAddr string, item *hcl1ast.ObjectItem) tfdiags.Diagnostics {
    82  		var diags tfdiags.Diagnostics
    83  		val, ok := item.Val.(*hcl1ast.ListType)
    84  		if !ok {
    85  			diags = diags.Append(&hcl2.Diagnostic{
    86  				Severity: hcl2.DiagError,
    87  				Summary:  "Invalid depends_on argument",
    88  				Detail:   `The "depends_on" argument must be a list of strings containing references to resources and modules.`,
    89  				Subject:  hcl1PosRange(filename, item.Keys[0].Pos()).Ptr(),
    90  			})
    91  			return diags
    92  		}
    93  
    94  		var exprBuf bytes.Buffer
    95  		multiline := len(val.List) > 1
    96  		exprBuf.WriteByte('[')
    97  		if multiline {
    98  			exprBuf.WriteByte('\n')
    99  		}
   100  		for _, node := range val.List {
   101  			lit, ok := node.(*hcl1ast.LiteralType)
   102  			if (!ok) || lit.Token.Type != hcl1token.STRING {
   103  				diags = diags.Append(&hcl2.Diagnostic{
   104  					Severity: hcl2.DiagError,
   105  					Summary:  "Invalid depends_on argument",
   106  					Detail:   `The "depends_on" argument must be a list of strings containing references to resources and modules.`,
   107  					Subject:  hcl1PosRange(filename, item.Keys[0].Pos()).Ptr(),
   108  				})
   109  				continue
   110  			}
   111  			refStr := lit.Token.Value().(string)
   112  			if refStr == "" {
   113  				continue
   114  			}
   115  			refParts := strings.Split(refStr, ".")
   116  			var maxNames int
   117  			switch refParts[0] {
   118  			case "data", "module":
   119  				maxNames = 3
   120  			default: // resource references
   121  				maxNames = 2
   122  			}
   123  
   124  			exprBuf.WriteString(refParts[0])
   125  			for i, part := range refParts[1:] {
   126  				if part == "*" {
   127  					// We used to allow test_instance.foo.* as a reference
   128  					// but now that's expressed instead as test_instance.foo,
   129  					// referring to the tuple of instances. This also
   130  					// always marks the end of the reference part of the
   131  					// traversal, so anything after this would be resource
   132  					// attributes that don't belong on depends_on.
   133  					break
   134  				}
   135  				if i, err := strconv.Atoi(part); err == nil {
   136  					fmt.Fprintf(&exprBuf, "[%d]", i)
   137  					// An index always marks the end of the reference part.
   138  					break
   139  				}
   140  				if (i + 1) >= maxNames {
   141  					// We've reached the end of the reference part, so anything
   142  					// after this would be invalid in 0.12.
   143  					break
   144  				}
   145  				exprBuf.WriteByte('.')
   146  				exprBuf.WriteString(part)
   147  			}
   148  
   149  			if multiline {
   150  				exprBuf.WriteString(",\n")
   151  			}
   152  		}
   153  		exprBuf.WriteByte(']')
   154  
   155  		printAttribute(buf, item.Keys[0].Token.Value().(string), exprBuf.Bytes(), item.LineComment)
   156  
   157  		return diags
   158  	}
   159  }
   160  
   161  func attributeRule(filename string, wantTy cty.Type, an *analysis, exprRule func(val interface{}) ([]byte, tfdiags.Diagnostics)) bodyItemRule {
   162  	return func(buf *bytes.Buffer, blockAddr string, item *hcl1ast.ObjectItem) tfdiags.Diagnostics {
   163  		var diags tfdiags.Diagnostics
   164  
   165  		name := item.Keys[0].Token.Value().(string)
   166  
   167  		// We'll tolerate a block with no labels here as a degenerate
   168  		// way to assign a map, but we can't migrate a block that has
   169  		// labels. In practice this should never happen because
   170  		// nested blocks in resource blocks did not accept labels
   171  		// prior to v0.12.
   172  		if len(item.Keys) != 1 {
   173  			diags = diags.Append(&hcl2.Diagnostic{
   174  				Severity: hcl2.DiagError,
   175  				Summary:  "Block where attribute was expected",
   176  				Detail:   fmt.Sprintf("Within %s the name %q is an attribute name, not a block type.", blockAddr, name),
   177  				Subject:  hcl1PosRange(filename, item.Keys[0].Pos()).Ptr(),
   178  			})
   179  			return diags
   180  		}
   181  
   182  		val := item.Val
   183  
   184  		if typeIsSettableFromTupleCons(wantTy) && !typeIsSettableFromTupleCons(wantTy.ElementType()) {
   185  			// In Terraform circa 0.10 it was required to wrap any expression
   186  			// that produces a list in HCL list brackets to allow type analysis
   187  			// to complete successfully, even though logically that ought to
   188  			// have produced a list of lists.
   189  			//
   190  			// In Terraform 0.12 this construct _does_ produce a list of lists, so
   191  			// we need to update expressions that look like older usage. We can't
   192  			// do this exactly with static analysis, but we can make a best-effort
   193  			// and produce a warning if type inference is impossible for a
   194  			// particular expression. This should give good results for common
   195  			// simple examples, like splat expressions.
   196  			//
   197  			// There are four possible cases here:
   198  			// - The value isn't an HCL1 list expression at all, or is one that
   199  			//   contains more than one item, in which case this special case
   200  			//   does not apply.
   201  			// - The inner expression after upgrading can be proven to return
   202  			//   a sequence type, in which case we must definitely remove
   203  			//   the wrapping brackets.
   204  			// - The inner expression after upgrading can be proven to return
   205  			//   a non-sequence type, in which case we fall through and treat
   206  			//   the whole item like a normal expression.
   207  			// - Static type analysis is impossible (it returns cty.DynamicPseudoType),
   208  			//   in which case we will make no changes but emit a warning and
   209  			//   a TODO comment for the user to decide whether a change needs
   210  			//   to be made in practice.
   211  			if list, ok := val.(*hcl1ast.ListType); ok {
   212  				if len(list.List) == 1 {
   213  					maybeAlsoList := list.List[0]
   214  					if exprSrc, diags := upgradeExpr(maybeAlsoList, filename, true, an); !diags.HasErrors() {
   215  						// Ideally we would set "self" here but we don't have
   216  						// enough context to set it and in practice not setting
   217  						// it only affects expressions inside provisioner and
   218  						// connection blocks, and the list-wrapping thing isn't
   219  						// common there.
   220  						gotTy := an.InferExpressionType(exprSrc, nil)
   221  						if typeIsSettableFromTupleCons(gotTy) {
   222  							// Since this expression was already inside HCL list brackets,
   223  							// the ultimate result would be a list of lists and so we
   224  							// need to unwrap it by taking just the portion within
   225  							// the brackets here.
   226  							val = maybeAlsoList
   227  						}
   228  						if gotTy == cty.DynamicPseudoType {
   229  							// User must decide.
   230  							diags = diags.Append(&hcl2.Diagnostic{
   231  								Severity: hcl2.DiagError,
   232  								Summary:  "Possible legacy dynamic list usage",
   233  								Detail:   "This list may be using the legacy redundant-list expression style from Terraform v0.10 and earlier. If the expression within these brackets returns a list itself, remove these brackets.",
   234  								Subject:  hcl1PosRange(filename, list.Lbrack).Ptr(),
   235  							})
   236  							buf.WriteString(
   237  								"# TF-UPGRADE-TODO: In Terraform v0.10 and earlier, it was sometimes necessary to\n" +
   238  									"# force an interpolation expression to be interpreted as a list by wrapping it\n" +
   239  									"# in an extra set of list brackets. That form was supported for compatibility in\n" +
   240  									"# v0.11, but is no longer supported in Terraform v0.12.\n" +
   241  									"#\n" +
   242  									"# If the expression in the following list itself returns a list, remove the\n" +
   243  									"# brackets to avoid interpretation as a list of lists. If the expression\n" +
   244  									"# returns a single list item then leave it as-is and remove this TODO comment.\n",
   245  							)
   246  						}
   247  					}
   248  				}
   249  			}
   250  		}
   251  
   252  		valSrc, valDiags := exprRule(val)
   253  		diags = diags.Append(valDiags)
   254  		printAttribute(buf, item.Keys[0].Token.Value().(string), valSrc, item.LineComment)
   255  
   256  		return diags
   257  	}
   258  }
   259  
   260  func nestedBlockRule(filename string, nestedRules bodyContentRules, an *analysis, adhocComments *commentQueue) bodyItemRule {
   261  	return func(buf *bytes.Buffer, blockAddr string, item *hcl1ast.ObjectItem) tfdiags.Diagnostics {
   262  		// This simpler nestedBlockRule is for contexts where the special
   263  		// "dynamic" block type is not accepted and so only HCL1 object
   264  		// constructs can be accepted. Attempts to assign arbitrary HIL
   265  		// expressions will be rejected as errors.
   266  
   267  		var diags tfdiags.Diagnostics
   268  		declRange := hcl1PosRange(filename, item.Keys[0].Pos())
   269  		blockType := item.Keys[0].Token.Value().(string)
   270  		labels := make([]string, len(item.Keys)-1)
   271  		for i, key := range item.Keys[1:] {
   272  			labels[i] = key.Token.Value().(string)
   273  		}
   274  
   275  		var blockItems []*hcl1ast.ObjectType
   276  
   277  		switch val := item.Val.(type) {
   278  
   279  		case *hcl1ast.ObjectType:
   280  			blockItems = []*hcl1ast.ObjectType{val}
   281  
   282  		case *hcl1ast.ListType:
   283  			for _, node := range val.List {
   284  				switch listItem := node.(type) {
   285  				case *hcl1ast.ObjectType:
   286  					blockItems = append(blockItems, listItem)
   287  				default:
   288  					diags = diags.Append(&hcl2.Diagnostic{
   289  						Severity: hcl2.DiagError,
   290  						Summary:  "Invalid value for nested block",
   291  						Detail:   fmt.Sprintf("In %s the name %q is a nested block type, so any value assigned to it must be an object.", blockAddr, blockType),
   292  						Subject:  hcl1PosRange(filename, node.Pos()).Ptr(),
   293  					})
   294  				}
   295  			}
   296  
   297  		default:
   298  			diags = diags.Append(&hcl2.Diagnostic{
   299  				Severity: hcl2.DiagError,
   300  				Summary:  "Invalid value for nested block",
   301  				Detail:   fmt.Sprintf("In %s the name %q is a nested block type, so any value assigned to it must be an object.", blockAddr, blockType),
   302  				Subject:  &declRange,
   303  			})
   304  			return diags
   305  		}
   306  
   307  		for _, blockItem := range blockItems {
   308  			printBlockOpen(buf, blockType, labels, item.LineComment)
   309  			bodyDiags := upgradeBlockBody(
   310  				filename, fmt.Sprintf("%s.%s", blockAddr, blockType), buf,
   311  				blockItem.List.Items, blockItem.Rbrace, nestedRules, adhocComments,
   312  			)
   313  			diags = diags.Append(bodyDiags)
   314  			buf.WriteString("}\n")
   315  		}
   316  
   317  		return diags
   318  	}
   319  }
   320  
   321  func nestedBlockRuleWithDynamic(filename string, nestedRules bodyContentRules, nestedSchema *configschema.NestedBlock, emptyAsAttr bool, an *analysis, adhocComments *commentQueue) bodyItemRule {
   322  	return func(buf *bytes.Buffer, blockAddr string, item *hcl1ast.ObjectItem) tfdiags.Diagnostics {
   323  		// In Terraform v0.11 it was possible in some cases to trick Terraform
   324  		// and providers into accepting HCL's attribute syntax and some HIL
   325  		// expressions in places where blocks or sequences of blocks were
   326  		// expected, since the information about the heritage of the values
   327  		// was lost during decoding and interpolation.
   328  		//
   329  		// In order to avoid all of the weird rough edges that resulted from
   330  		// those misinterpretations, Terraform v0.12 is stricter and requires
   331  		// the use of block syntax for blocks in all cases. However, because
   332  		// various abuses of attribute syntax _did_ work (with some caveats)
   333  		// in v0.11 we will upgrade them as best we can to use proper block
   334  		// syntax.
   335  		//
   336  		// There are a few different permutations supported by this code:
   337  		//
   338  		// - Assigning a single HCL1 "object" using attribute syntax. This is
   339  		//   straightforward to migrate just by dropping the equals sign.
   340  		//
   341  		// - Assigning a HCL1 list of objects using attribute syntax. Each
   342  		//   object in that list can be translated to a separate block.
   343  		//
   344  		// - Assigning a HCL1 list containing HIL expressions that evaluate
   345  		//   to maps. This is a hard case because we can't know the internal
   346  		//   structure of those maps during static analysis, and so we must
   347  		//   generate a worst-case dynamic block structure for it.
   348  		//
   349  		// - Assigning a single HIL expression that evaluates to a list of
   350  		//   maps. This is just like the previous case except additionally
   351  		//   we cannot even predict the number of generated blocks, so we must
   352  		//   generate a single "dynamic" block to iterate over the list at
   353  		//   runtime.
   354  
   355  		var diags tfdiags.Diagnostics
   356  		blockType := item.Keys[0].Token.Value().(string)
   357  		labels := make([]string, len(item.Keys)-1)
   358  		for i, key := range item.Keys[1:] {
   359  			labels[i] = key.Token.Value().(string)
   360  		}
   361  
   362  		var blockItems []hcl1ast.Node
   363  
   364  		switch val := item.Val.(type) {
   365  
   366  		case *hcl1ast.ObjectType:
   367  			blockItems = append(blockItems, val)
   368  
   369  		case *hcl1ast.ListType:
   370  			for _, node := range val.List {
   371  				switch listItem := node.(type) {
   372  				case *hcl1ast.ObjectType:
   373  					blockItems = append(blockItems, listItem)
   374  				default:
   375  					// We're going to cheat a bit here and construct a synthetic
   376  					// HCL1 list just because that makes our logic
   377  					// simpler below where we can just treat all non-objects
   378  					// in the same way when producing "dynamic" blocks.
   379  					synthList := &hcl1ast.ListType{
   380  						List:   []hcl1ast.Node{listItem},
   381  						Lbrack: listItem.Pos(),
   382  						Rbrack: hcl1NodeEndPos(listItem),
   383  					}
   384  					blockItems = append(blockItems, synthList)
   385  				}
   386  			}
   387  
   388  		default:
   389  			blockItems = append(blockItems, item.Val)
   390  		}
   391  
   392  		if len(blockItems) == 0 && emptyAsAttr {
   393  			// Terraform v0.12's config decoder allows using block syntax for
   394  			// certain attribute types, which we prefer as idiomatic usage
   395  			// causing us to end up in this function in such cases, but as
   396  			// a special case users can still use the attribute syntax to
   397  			// explicitly write an empty list. For more information, see
   398  			// the lang/blocktoattr package.
   399  			printAttribute(buf, item.Keys[0].Token.Value().(string), []byte{'[', ']'}, item.LineComment)
   400  			return diags
   401  		}
   402  
   403  		for _, blockItem := range blockItems {
   404  			switch ti := blockItem.(type) {
   405  			case *hcl1ast.ObjectType:
   406  				// If we have an object then we'll pass through its content
   407  				// as a block directly. This is the most straightforward mapping
   408  				// from the source input, since we know exactly which keys
   409  				// are present.
   410  				printBlockOpen(buf, blockType, labels, item.LineComment)
   411  				bodyDiags := upgradeBlockBody(
   412  					filename, fmt.Sprintf("%s.%s", blockAddr, blockType), buf,
   413  					ti.List.Items, ti.Rbrace, nestedRules, adhocComments,
   414  				)
   415  				diags = diags.Append(bodyDiags)
   416  				buf.WriteString("}\n")
   417  			default:
   418  				// For any other sort of value we can't predict what shape it
   419  				// will have at runtime, so we must generate a very conservative
   420  				// "dynamic" block that tries to assign everything from the
   421  				// schema. The result of this is likely to be pretty ugly.
   422  				printBlockOpen(buf, "dynamic", []string{blockType}, item.LineComment)
   423  				eachSrc, eachDiags := upgradeExpr(blockItem, filename, true, an)
   424  				diags = diags.Append(eachDiags)
   425  				printAttribute(buf, "for_each", eachSrc, nil)
   426  				if nestedSchema.Nesting == configschema.NestingMap {
   427  					// This is a pretty odd situation since map-based blocks
   428  					// didn't exist prior to Terraform v0.12, but we'll support
   429  					// this anyway in case we decide to add support in a later
   430  					// SDK release that is still somehow compatible with
   431  					// Terraform v0.11.
   432  					printAttribute(buf, "labels", []byte(fmt.Sprintf(`[%s.key]`, blockType)), nil)
   433  				}
   434  				printBlockOpen(buf, "content", nil, nil)
   435  				buf.WriteString("# TF-UPGRADE-TODO: The automatic upgrade tool can't predict\n")
   436  				buf.WriteString("# which keys might be set in maps assigned here, so it has\n")
   437  				buf.WriteString("# produced a comprehensive set here. Consider simplifying\n")
   438  				buf.WriteString("# this after confirming which keys can be set in practice.\n\n")
   439  				printDynamicBlockBody(buf, blockType, &nestedSchema.Block)
   440  				buf.WriteString("}\n")
   441  				buf.WriteString("}\n")
   442  				diags = diags.Append(&hcl2.Diagnostic{
   443  					Severity: hcl2.DiagWarning,
   444  					Summary:  "Approximate migration of invalid block type assignment",
   445  					Detail:   fmt.Sprintf("In %s the name %q is a nested block type, but this configuration is exploiting some missing validation rules from Terraform v0.11 and prior to trick Terraform into creating blocks dynamically.\n\nThis has been upgraded to use the new Terraform v0.12 dynamic blocks feature, but since the upgrade tool cannot predict which map keys will be present a fully-comprehensive set has been generated.", blockAddr, blockType),
   446  					Subject:  hcl1PosRange(filename, blockItem.Pos()).Ptr(),
   447  				})
   448  			}
   449  		}
   450  
   451  		return diags
   452  	}
   453  }
   454  
   455  // schemaDefaultBodyRules constructs standard body content rules for the given
   456  // schema. Each call is guaranteed to produce a distinct object so that
   457  // callers can safely mutate the result in order to impose custom rules
   458  // in addition to or instead of those created by default, for situations
   459  // where schema-based and predefined items mix in a single body.
   460  func schemaDefaultBodyRules(filename string, schema *configschema.Block, an *analysis, adhocComments *commentQueue) bodyContentRules {
   461  	ret := make(bodyContentRules)
   462  	if schema == nil {
   463  		// Shouldn't happen in any real case, but often crops up in tests
   464  		// where the mock schemas tend to be incomplete.
   465  		return ret
   466  	}
   467  
   468  	for name, attrS := range schema.Attributes {
   469  		if aty := attrS.Type; blocktoattr.TypeCanBeBlocks(aty) {
   470  			// Terraform's standard body processing rules for arbitrary schemas
   471  			// have a special case where list-of-object or set-of-object
   472  			// attributes can be specified as a sequence of nested blocks
   473  			// instead of a single list attribute. We prefer that form during
   474  			// upgrade for historical reasons, to avoid making large changes
   475  			// to existing configurations that were following documented idiom.
   476  			synthSchema := blocktoattr.SchemaForCtyContainerType(aty)
   477  			nestedRules := schemaDefaultBodyRules(filename, &synthSchema.Block, an, adhocComments)
   478  			ret[name] = nestedBlockRuleWithDynamic(filename, nestedRules, synthSchema, true, an, adhocComments)
   479  			continue
   480  		}
   481  		ret[name] = normalAttributeRule(filename, attrS.Type, an)
   482  	}
   483  	for name, blockS := range schema.BlockTypes {
   484  		nestedRules := schemaDefaultBodyRules(filename, &blockS.Block, an, adhocComments)
   485  		ret[name] = nestedBlockRuleWithDynamic(filename, nestedRules, blockS, false, an, adhocComments)
   486  	}
   487  
   488  	return ret
   489  }
   490  
   491  // schemaNoInterpBodyRules constructs standard body content rules for the given
   492  // schema. Each call is guaranteed to produce a distinct object so that
   493  // callers can safely mutate the result in order to impose custom rules
   494  // in addition to or instead of those created by default, for situations
   495  // where schema-based and predefined items mix in a single body.
   496  func schemaNoInterpBodyRules(filename string, schema *configschema.Block, an *analysis, adhocComments *commentQueue) bodyContentRules {
   497  	ret := make(bodyContentRules)
   498  	if schema == nil {
   499  		// Shouldn't happen in any real case, but often crops up in tests
   500  		// where the mock schemas tend to be incomplete.
   501  		return ret
   502  	}
   503  
   504  	for name, attrS := range schema.Attributes {
   505  		ret[name] = noInterpAttributeRule(filename, attrS.Type, an)
   506  	}
   507  	for name, blockS := range schema.BlockTypes {
   508  		nestedRules := schemaDefaultBodyRules(filename, &blockS.Block, an, adhocComments)
   509  		ret[name] = nestedBlockRule(filename, nestedRules, an, adhocComments)
   510  	}
   511  
   512  	return ret
   513  }
   514  
   515  // justAttributesBodyRules constructs body content rules that just use the
   516  // standard interpolated attribute mapping for every name already present
   517  // in the given body object.
   518  //
   519  // This is a little weird vs. just processing directly the attributes, but
   520  // has the advantage that the caller can then apply overrides to the result
   521  // as necessary to deal with any known names that need special handling.
   522  //
   523  // Any attribute rules created by this function do not have a specific wanted
   524  // value type specified, instead setting it to just cty.DynamicPseudoType.
   525  func justAttributesBodyRules(filename string, body *hcl1ast.ObjectType, an *analysis) bodyContentRules {
   526  	rules := make(bodyContentRules, len(body.List.Items))
   527  	args := body.List.Items
   528  	for _, arg := range args {
   529  		name := arg.Keys[0].Token.Value().(string)
   530  		rules[name] = normalAttributeRule(filename, cty.DynamicPseudoType, an)
   531  	}
   532  	return rules
   533  }
   534  
   535  func lifecycleBlockBodyRules(filename string, an *analysis) bodyContentRules {
   536  	return bodyContentRules{
   537  		"create_before_destroy": noInterpAttributeRule(filename, cty.Bool, an),
   538  		"prevent_destroy":       noInterpAttributeRule(filename, cty.Bool, an),
   539  		"ignore_changes": func(buf *bytes.Buffer, blockAddr string, item *hcl1ast.ObjectItem) tfdiags.Diagnostics {
   540  			var diags tfdiags.Diagnostics
   541  			val, ok := item.Val.(*hcl1ast.ListType)
   542  			if !ok {
   543  				diags = diags.Append(&hcl2.Diagnostic{
   544  					Severity: hcl2.DiagError,
   545  					Summary:  "Invalid ignore_changes argument",
   546  					Detail:   `The "ignore_changes" argument must be a list of attribute expressions relative to this resource.`,
   547  					Subject:  hcl1PosRange(filename, item.Keys[0].Pos()).Ptr(),
   548  				})
   549  				return diags
   550  			}
   551  
   552  			// As a special case, we'll map the single-element list ["*"] to
   553  			// the new keyword "all".
   554  			if len(val.List) == 1 {
   555  				if lit, ok := val.List[0].(*hcl1ast.LiteralType); ok {
   556  					if lit.Token.Value() == "*" {
   557  						printAttribute(buf, item.Keys[0].Token.Value().(string), []byte("all"), item.LineComment)
   558  						return diags
   559  					}
   560  				}
   561  			}
   562  
   563  			var exprBuf bytes.Buffer
   564  			multiline := len(val.List) > 1
   565  			exprBuf.WriteByte('[')
   566  			if multiline {
   567  				exprBuf.WriteByte('\n')
   568  			}
   569  			for _, node := range val.List {
   570  				itemSrc, moreDiags := upgradeTraversalExpr(node, filename, an)
   571  				diags = diags.Append(moreDiags)
   572  				exprBuf.Write(itemSrc)
   573  				if multiline {
   574  					exprBuf.WriteString(",\n")
   575  				}
   576  			}
   577  			exprBuf.WriteByte(']')
   578  
   579  			printAttribute(buf, item.Keys[0].Token.Value().(string), exprBuf.Bytes(), item.LineComment)
   580  
   581  			return diags
   582  		},
   583  	}
   584  }
   585  
   586  func provisionerBlockRule(filename string, resourceType string, an *analysis, adhocComments *commentQueue) bodyItemRule {
   587  	// Unlike some other examples above, this is a rule for the entire
   588  	// provisioner block, rather than just for its contents. Therefore it must
   589  	// also produce the block header and body delimiters.
   590  	return func(buf *bytes.Buffer, blockAddr string, item *hcl1ast.ObjectItem) tfdiags.Diagnostics {
   591  		var diags tfdiags.Diagnostics
   592  		body := item.Val.(*hcl1ast.ObjectType)
   593  		declRange := hcl1PosRange(filename, item.Keys[0].Pos())
   594  
   595  		if len(item.Keys) < 2 {
   596  			diags = diags.Append(&hcl2.Diagnostic{
   597  				Severity: hcl2.DiagError,
   598  				Summary:  "Invalid provisioner block",
   599  				Detail:   "A provisioner block must have one label: the provisioner type.",
   600  				Subject:  &declRange,
   601  			})
   602  			return diags
   603  		}
   604  
   605  		typeName := item.Keys[1].Token.Value().(string)
   606  		schema := an.ProvisionerSchemas[typeName]
   607  		if schema == nil {
   608  			// This message is assuming that if the user _is_ using a third-party
   609  			// provisioner plugin they already know how to install it for normal
   610  			// use and so we don't need to spell out those instructions in detail
   611  			// here.
   612  			diags = diags.Append(&hcl2.Diagnostic{
   613  				Severity: hcl2.DiagError,
   614  				Summary:  "Unknown provisioner type",
   615  				Detail:   fmt.Sprintf("The provisioner type %q is not supported. If this is a third-party plugin, make sure its plugin executable is available in one of the usual plugin search paths.", typeName),
   616  				Subject:  &declRange,
   617  			})
   618  			return diags
   619  		}
   620  
   621  		rules := schemaDefaultBodyRules(filename, schema, an, adhocComments)
   622  		rules["when"] = maybeBareTraversalAttributeRule(filename, an)
   623  		rules["on_failure"] = maybeBareTraversalAttributeRule(filename, an)
   624  		rules["connection"] = connectionBlockRule(filename, resourceType, an, adhocComments)
   625  
   626  		printComments(buf, item.LeadComment)
   627  		printBlockOpen(buf, "provisioner", []string{typeName}, item.LineComment)
   628  		bodyDiags := upgradeBlockBody(filename, fmt.Sprintf("%s.provisioner[%q]", blockAddr, typeName), buf, body.List.Items, body.Rbrace, rules, adhocComments)
   629  		diags = diags.Append(bodyDiags)
   630  		buf.WriteString("}\n")
   631  
   632  		return diags
   633  	}
   634  }
   635  
   636  func connectionBlockRule(filename string, resourceType string, an *analysis, adhocComments *commentQueue) bodyItemRule {
   637  	// Unlike some other examples above, this is a rule for the entire
   638  	// connection block, rather than just for its contents. Therefore it must
   639  	// also produce the block header and body delimiters.
   640  	return func(buf *bytes.Buffer, blockAddr string, item *hcl1ast.ObjectItem) tfdiags.Diagnostics {
   641  		var diags tfdiags.Diagnostics
   642  		body := item.Val.(*hcl1ast.ObjectType)
   643  
   644  		// TODO: For the few resource types that were setting ConnInfo in
   645  		// state after create/update in prior versions, generate the additional
   646  		// explicit connection settings that are now required if and only if
   647  		// there's at least one provisioner block.
   648  		// For now, we just pass this through as-is.
   649  
   650  		schema := terraform.ConnectionBlockSupersetSchema()
   651  		rules := schemaDefaultBodyRules(filename, schema, an, adhocComments)
   652  		rules["type"] = noInterpAttributeRule(filename, cty.String, an) // type is processed early in the config loader, so cannot interpolate
   653  
   654  		printComments(buf, item.LeadComment)
   655  		printBlockOpen(buf, "connection", nil, item.LineComment)
   656  
   657  		// Terraform 0.12 no longer supports "magical" configuration of defaults
   658  		// in the connection block from logic in the provider because explicit
   659  		// is better than implicit, but for backward-compatibility we'll populate
   660  		// an existing connection block with any settings that would've been
   661  		// previously set automatically for a set of instance types we know
   662  		// had this behavior in versions prior to the v0.12 release.
   663  		if defaults := resourceTypeAutomaticConnectionExprs[resourceType]; len(defaults) > 0 {
   664  			names := make([]string, 0, len(defaults))
   665  			for name := range defaults {
   666  				names = append(names, name)
   667  			}
   668  			sort.Strings(names)
   669  			for _, name := range names {
   670  				exprSrc := defaults[name]
   671  				if existing := body.List.Filter(name); len(existing.Items) > 0 {
   672  					continue // Existing explicit value, so no need for a default
   673  				}
   674  				printAttribute(buf, name, []byte(exprSrc), nil)
   675  			}
   676  		}
   677  
   678  		bodyDiags := upgradeBlockBody(filename, fmt.Sprintf("%s.connection", blockAddr), buf, body.List.Items, body.Rbrace, rules, adhocComments)
   679  		diags = diags.Append(bodyDiags)
   680  		buf.WriteString("}\n")
   681  
   682  		return diags
   683  	}
   684  }
   685  
   686  func moduleSourceRule(filename string, an *analysis) bodyItemRule {
   687  	return func(buf *bytes.Buffer, blockAddr string, item *hcl1ast.ObjectItem) tfdiags.Diagnostics {
   688  		var diags tfdiags.Diagnostics
   689  		val, ok := item.Val.(*hcl1ast.LiteralType)
   690  		if !ok {
   691  			diags = diags.Append(&hcl2.Diagnostic{
   692  				Severity: hcl2.DiagError,
   693  				Summary:  "Invalid source argument",
   694  				Detail:   `The "source" argument must be a single string containing the module source.`,
   695  				Subject:  hcl1PosRange(filename, item.Keys[0].Pos()).Ptr(),
   696  			})
   697  			return diags
   698  		}
   699  		if val.Token.Type != hcl1token.STRING {
   700  			diags = diags.Append(&hcl2.Diagnostic{
   701  				Severity: hcl2.DiagError,
   702  				Summary:  "Invalid source argument",
   703  				Detail:   `The "source" argument must be a single string containing the module source.`,
   704  				Subject:  hcl1PosRange(filename, item.Keys[0].Pos()).Ptr(),
   705  			})
   706  			return diags
   707  		}
   708  
   709  		litVal := val.Token.Value().(string)
   710  
   711  		if isMaybeRelativeLocalPath(litVal, an.ModuleDir) {
   712  			diags = diags.Append(&hcl2.Diagnostic{
   713  				Severity: hcl2.DiagWarning,
   714  				Summary:  "Possible relative module source",
   715  				Detail:   "Terraform cannot determine the given module source, but it appears to be a relative path",
   716  				Subject:  hcl1PosRange(filename, item.Keys[0].Pos()).Ptr(),
   717  			})
   718  			buf.WriteString(
   719  				"# TF-UPGRADE-TODO: In Terraform v0.11 and earlier, it was possible to\n" +
   720  					"# reference a relative module source without a preceding ./, but it is no\n" +
   721  					"# longer supported in Terraform v0.12.\n" +
   722  					"#\n" +
   723  					"# If the below module source is indeed a relative local path, add ./ to the\n" +
   724  					"# start of the source string. If that is not the case, then leave it as-is\n" +
   725  					"# and remove this TODO comment.\n",
   726  			)
   727  		}
   728  		newVal, exprDiags := upgradeExpr(val, filename, false, an)
   729  		diags = diags.Append(exprDiags)
   730  		buf.WriteString("source = " + string(newVal) + "\n")
   731  		return diags
   732  	}
   733  }
   734  
   735  // Prior to Terraform 0.12 providers were able to supply default connection
   736  // settings that would partially populate the "connection" block with
   737  // automatically-selected values.
   738  //
   739  // In practice, this feature was often confusing in that the provider would not
   740  // have enough information to select a suitable host address or protocol from
   741  // multiple possible options and so would just make an arbitrary decision.
   742  //
   743  // With our principle of "explicit is better than implicit", as of Terraform 0.12
   744  // we now require all connection settings to be configured explicitly by the
   745  // user so that it's clear and explicit in the configuration which protocol and
   746  // IP address are being selected. To avoid generating errors immediately after
   747  // upgrade, though, we'll make a best effort to populate something functionally
   748  // equivalent to what the provider would've done automatically for any resource
   749  // types we know about in this table.
   750  //
   751  // The leaf values in this data structure are raw expressions to be inserted,
   752  // and so they must use valid expression syntax as understood by Terraform 0.12.
   753  // They should generally be expressions using only constant values or expressions
   754  // in terms of attributes accessed via the special "self" object. These should
   755  // mimic as closely as possible the logic that the provider itself used to
   756  // implement.
   757  //
   758  // NOTE: Because provider releases are independent from Terraform Core releases,
   759  // there could potentially be new 0.11-compatible provider releases that
   760  // introduce new uses of default connection info that this map doesn't know
   761  // about. The upgrade tool will not handle these, and so we will advise
   762  // provider developers that this mechanism is not to be used for any new
   763  // resource types, even in 0.11 mode.
   764  var resourceTypeAutomaticConnectionExprs = map[string]map[string]string{
   765  	"aws_instance": map[string]string{
   766  		"type": `"ssh"`,
   767  		"host": `coalesce(self.public_ip, self.private_ip)`,
   768  	},
   769  	"aws_spot_instance_request": map[string]string{
   770  		"type":     `"ssh"`,
   771  		"host":     `coalesce(self.public_ip, self.private_ip)`,
   772  		"user":     `self.username != "" ? self.username : null`,
   773  		"password": `self.password != "" ? self.password : null`,
   774  	},
   775  	"azure_instance": map[string]string{
   776  		"type": `"ssh" # TF-UPGRADE-TODO: If this is a windows instance without an SSH server, change to "winrm"`,
   777  		"host": `coalesce(self.vip_address, self.ip_address)`,
   778  	},
   779  	"azurerm_virtual_machine": map[string]string{
   780  		"type": `"ssh" # TF-UPGRADE-TODO: If this is a windows instance without an SSH server, change to "winrm"`,
   781  		// The azurerm_virtual_machine resource type does not expose a remote
   782  		// access IP address directly, instead requring the user to separately
   783  		// fetch the network interface.
   784  		// (If we can add a "default_ssh_ip" or similar attribute to this
   785  		// resource type before its first 0.12-compatible release then we should
   786  		// update this to use that instead, for simplicity's sake.)
   787  		"host": `"" # TF-UPGRADE-TODO: Set this to the IP address of the machine's primary network interface`,
   788  	},
   789  	"brightbox_server": map[string]string{
   790  		"type": `"ssh"`,
   791  		"host": `coalesce(self.public_hostname, self.ipv6_hostname, self.fqdn)`,
   792  	},
   793  	"cloudscale_server": map[string]string{
   794  		"type": `"ssh"`,
   795  		// The logic for selecting this is pretty complicated for this resource
   796  		// type, and the result is not exposed as an isolated attribute, so
   797  		// the conversion here is a little messy. We include newlines in this
   798  		// one so that the auto-formatter can indent it nicely for readability.
   799  		// NOTE: In v1.0.1 of this provider (the latest at the time of this
   800  		// writing) it has an possible bug where it selects _public_ IPv4
   801  		// addresses but _private_ IPv6 addresses. That behavior is followed
   802  		// here to maximize compatibility with existing configurations.
   803  		"host": `coalesce( # TF-UPGRADE-TODO: Simplify this to reference a specific desired IP address, if possible.
   804  			concat(
   805  				flatten([
   806  					for i in self.network_interface : [
   807  						for a in i.addresses : a.address
   808  						if a.version == 4
   809  					]
   810  					if i.type == "public"
   811  				]),
   812  				flatten([
   813  					for i in self.network_interface : [
   814  						for a in i.addresses : a.address
   815  						if a.version == 6
   816  					]
   817  					if i.type == "private"
   818  				]),
   819  			)...
   820  		)`,
   821  	},
   822  	"cloudstack_instance": map[string]string{
   823  		"type": `"ssh"`,
   824  		"host": `self.ip_address`,
   825  	},
   826  	"digitalocean_droplet": map[string]string{
   827  		"type": `"ssh"`,
   828  		"host": `self.ipv4_address`,
   829  	},
   830  	"flexibleengine_compute_bms_server_v2": map[string]string{
   831  		"type": `"ssh"`,
   832  		"host": `coalesce(self.access_ip_v4, self.access_ip_v6)`,
   833  	},
   834  	"flexibleengine_compute_instance_v2": map[string]string{
   835  		"type": `"ssh"`,
   836  		"host": `coalesce(self.access_ip_v4, self.access_ip_v6)`,
   837  	},
   838  	"google_compute_instance": map[string]string{
   839  		"type": `"ssh"`,
   840  		// The logic for selecting this is pretty complicated for this resource
   841  		// type, and the result is not exposed as an isolated attribute, so
   842  		// the conversion here is a little messy. We include newlines in this
   843  		// one so that the auto-formatter can indent it nicely for readability.
   844  		// (If we can add a "default_ssh_ip" or similar attribute to this
   845  		// resource type before its first 0.12-compatible release then we should
   846  		// update this to use that instead, for simplicity's sake.)
   847  		"host": `coalesce( # TF-UPGRADE-TODO: Simplify this to reference a specific desired IP address, if possible.
   848  			concat(
   849  				# Prefer any available NAT IP address
   850  				flatten([
   851  					for ni in self.network_interface : [
   852  						for ac in ni.access_config : ac.nat_ip
   853  					]
   854  				]),
   855  
   856  				# Otherwise, use the first available LAN IP address
   857  				[
   858  					for ni in self.network_interface : ni.network_ip
   859  				],
   860  			)...
   861  		)`,
   862  	},
   863  	"hcloud_server": map[string]string{
   864  		"type": `"ssh"`,
   865  		"host": `self.ipv4_address`,
   866  	},
   867  	"huaweicloud_compute_instance_v2": map[string]string{
   868  		"type": `"ssh"`,
   869  		"host": `coalesce(self.access_ip_v4, self.access_ip_v6)`,
   870  	},
   871  	"linode_instance": map[string]string{
   872  		"type": `"ssh"`,
   873  		"host": `self.ipv4[0]`,
   874  	},
   875  	"oneandone_baremetal_server": map[string]string{
   876  		"type":        `ssh`,
   877  		"host":        `self.ips[0].ip`,
   878  		"password":    `self.password != "" ? self.password : null`,
   879  		"private_key": `self.ssh_key_path != "" ? file(self.ssh_key_path) : null`,
   880  	},
   881  	"oneandone_server": map[string]string{
   882  		"type":        `ssh`,
   883  		"host":        `self.ips[0].ip`,
   884  		"password":    `self.password != "" ? self.password : null`,
   885  		"private_key": `self.ssh_key_path != "" ? file(self.ssh_key_path) : null`,
   886  	},
   887  	"openstack_compute_instance_v2": map[string]string{
   888  		"type": `"ssh"`,
   889  		"host": `coalesce(self.access_ip_v4, self.access_ip_v6)`,
   890  	},
   891  	"opentelekomcloud_compute_bms_server_v2": map[string]string{
   892  		"type": `"ssh"`,
   893  		"host": `coalesce(self.access_ip_v4, self.access_ip_v6)`,
   894  	},
   895  	"opentelekomcloud_compute_instance_v2": map[string]string{
   896  		"type": `"ssh"`,
   897  		"host": `coalesce(self.access_ip_v4, self.access_ip_v6)`,
   898  	},
   899  	"packet_device": map[string]string{
   900  		"type": `"ssh"`,
   901  		"host": `self.access_public_ipv4`,
   902  	},
   903  	"profitbricks_server": map[string]string{
   904  		"type": `"ssh"`,
   905  		"host": `coalesce(self.primary_nic.ips...)`,
   906  		// The value for this isn't exported anywhere on the object, so we'll
   907  		// need to have the user fix it up manually.
   908  		"password": `"" # TF-UPGRADE-TODO: set this to a suitable value, such as the boot image password`,
   909  	},
   910  	"scaleway_server": map[string]string{
   911  		"type": `"ssh"`,
   912  		"host": `self.public_ip`,
   913  	},
   914  	"telefonicaopencloud_compute_bms_server_v2": map[string]string{
   915  		"type": `"ssh"`,
   916  		"host": `coalesce(self.access_ip_v4, self.access_ip_v6)`,
   917  	},
   918  	"telefonicaopencloud_compute_instance_v2": map[string]string{
   919  		"type": `"ssh"`,
   920  		"host": `coalesce(self.access_ip_v4, self.access_ip_v6)`,
   921  	},
   922  	"triton_machine": map[string]string{
   923  		"type": `"ssh"`,
   924  		"host": `self.primaryip`, // convention would call for this to be named "primary_ip", but "primaryip" is the name this resource type uses
   925  	},
   926  	"vsphere_virtual_machine": map[string]string{
   927  		"type": `"ssh"`,
   928  		"host": `self.default_ip_address`,
   929  	},
   930  	"yandex_compute_instance": map[string]string{
   931  		"type": `"ssh"`,
   932  		// The logic for selecting this is pretty complicated for this resource
   933  		// type, and the result is not exposed as an isolated attribute, so
   934  		// the conversion here is a little messy. We include newlines in this
   935  		// one so that the auto-formatter can indent it nicely for readability.
   936  		"host": `coalesce( # TF-UPGRADE-TODO: Simplify this to reference a specific desired IP address, if possible.
   937  			concat(
   938  				# Prefer any available NAT IP address
   939  				for i in self.network_interface: [
   940  					i.nat_ip_address
   941  				],
   942  
   943  				# Otherwise, use the first available internal IP address
   944  				for i in self.network_interface: [
   945  					i.ip_address
   946  				],
   947  			)...
   948  		)`,
   949  	},
   950  }
   951  
   952  // copied directly from internal/initwd/getter.go
   953  var localSourcePrefixes = []string{
   954  	"./",
   955  	"../",
   956  	".\\",
   957  	"..\\",
   958  }
   959  
   960  // isMaybeRelativeLocalPath tries to catch situations where a module source is
   961  // an improperly-referenced relative path, such as "module" instead of
   962  // "./module". This is a simple check that could return a false positive in the
   963  // unlikely-yet-plausible case that a module source is for eg. a github
   964  // repository that also looks exactly like an existing relative path. This
   965  // should only be used to return a warning.
   966  func isMaybeRelativeLocalPath(addr, dir string) bool {
   967  	for _, prefix := range localSourcePrefixes {
   968  		if strings.HasPrefix(addr, prefix) {
   969  			// it is _definitely_ a relative path
   970  			return false
   971  		}
   972  	}
   973  
   974  	_, err := regsrc.ParseModuleSource(addr)
   975  	if err == nil {
   976  		// it is a registry source
   977  		return false
   978  	}
   979  
   980  	possibleRelPath := filepath.Join(dir, addr)
   981  	_, err = os.Stat(possibleRelPath)
   982  	if err == nil {
   983  		// If there is no error, something exists at what would be the relative
   984  		// path, if the module source started with ./
   985  		return true
   986  	}
   987  
   988  	return false
   989  }