github.com/eliastor/durgaform@v0.0.0-20220816172711-d0ab2d17673e/internal/configs/resource.go (about)

     1  package configs
     2  
     3  import (
     4  	"fmt"
     5  
     6  	"github.com/hashicorp/hcl/v2"
     7  	"github.com/hashicorp/hcl/v2/gohcl"
     8  	"github.com/hashicorp/hcl/v2/hclsyntax"
     9  	hcljson "github.com/hashicorp/hcl/v2/json"
    10  
    11  	"github.com/eliastor/durgaform/internal/addrs"
    12  	"github.com/eliastor/durgaform/internal/lang"
    13  	"github.com/eliastor/durgaform/internal/tfdiags"
    14  )
    15  
    16  // Resource represents a "resource" or "data" block in a module or file.
    17  type Resource struct {
    18  	Mode    addrs.ResourceMode
    19  	Name    string
    20  	Type    string
    21  	Config  hcl.Body
    22  	Count   hcl.Expression
    23  	ForEach hcl.Expression
    24  
    25  	ProviderConfigRef *ProviderConfigRef
    26  	Provider          addrs.Provider
    27  
    28  	Preconditions  []*CheckRule
    29  	Postconditions []*CheckRule
    30  
    31  	DependsOn []hcl.Traversal
    32  
    33  	TriggersReplacement []hcl.Expression
    34  
    35  	// Managed is populated only for Mode = addrs.ManagedResourceMode,
    36  	// containing the additional fields that apply to managed resources.
    37  	// For all other resource modes, this field is nil.
    38  	Managed *ManagedResource
    39  
    40  	DeclRange hcl.Range
    41  	TypeRange hcl.Range
    42  }
    43  
    44  // ManagedResource represents a "resource" block in a module or file.
    45  type ManagedResource struct {
    46  	Connection   *Connection
    47  	Provisioners []*Provisioner
    48  
    49  	CreateBeforeDestroy bool
    50  	PreventDestroy      bool
    51  	IgnoreChanges       []hcl.Traversal
    52  	IgnoreAllChanges    bool
    53  
    54  	CreateBeforeDestroySet bool
    55  	PreventDestroySet      bool
    56  }
    57  
    58  func (r *Resource) moduleUniqueKey() string {
    59  	return r.Addr().String()
    60  }
    61  
    62  // Addr returns a resource address for the receiver that is relative to the
    63  // resource's containing module.
    64  func (r *Resource) Addr() addrs.Resource {
    65  	return addrs.Resource{
    66  		Mode: r.Mode,
    67  		Type: r.Type,
    68  		Name: r.Name,
    69  	}
    70  }
    71  
    72  // ProviderConfigAddr returns the address for the provider configuration that
    73  // should be used for this resource. This function returns a default provider
    74  // config addr if an explicit "provider" argument was not provided.
    75  func (r *Resource) ProviderConfigAddr() addrs.LocalProviderConfig {
    76  	if r.ProviderConfigRef == nil {
    77  		// If no specific "provider" argument is given, we want to look up the
    78  		// provider config where the local name matches the implied provider
    79  		// from the resource type. This may be different from the resource's
    80  		// provider type.
    81  		return addrs.LocalProviderConfig{
    82  			LocalName: r.Addr().ImpliedProvider(),
    83  		}
    84  	}
    85  
    86  	return addrs.LocalProviderConfig{
    87  		LocalName: r.ProviderConfigRef.Name,
    88  		Alias:     r.ProviderConfigRef.Alias,
    89  	}
    90  }
    91  
    92  // HasCustomConditions returns true if and only if the resource has at least
    93  // one author-specified custom condition.
    94  func (r *Resource) HasCustomConditions() bool {
    95  	return len(r.Postconditions) != 0 || len(r.Preconditions) != 0
    96  }
    97  
    98  func decodeResourceBlock(block *hcl.Block, override bool) (*Resource, hcl.Diagnostics) {
    99  	var diags hcl.Diagnostics
   100  	r := &Resource{
   101  		Mode:      addrs.ManagedResourceMode,
   102  		Type:      block.Labels[0],
   103  		Name:      block.Labels[1],
   104  		DeclRange: block.DefRange,
   105  		TypeRange: block.LabelRanges[0],
   106  		Managed:   &ManagedResource{},
   107  	}
   108  
   109  	content, remain, moreDiags := block.Body.PartialContent(resourceBlockSchema)
   110  	diags = append(diags, moreDiags...)
   111  	r.Config = remain
   112  
   113  	if !hclsyntax.ValidIdentifier(r.Type) {
   114  		diags = append(diags, &hcl.Diagnostic{
   115  			Severity: hcl.DiagError,
   116  			Summary:  "Invalid resource type name",
   117  			Detail:   badIdentifierDetail,
   118  			Subject:  &block.LabelRanges[0],
   119  		})
   120  	}
   121  	if !hclsyntax.ValidIdentifier(r.Name) {
   122  		diags = append(diags, &hcl.Diagnostic{
   123  			Severity: hcl.DiagError,
   124  			Summary:  "Invalid resource name",
   125  			Detail:   badIdentifierDetail,
   126  			Subject:  &block.LabelRanges[1],
   127  		})
   128  	}
   129  
   130  	if attr, exists := content.Attributes["count"]; exists {
   131  		r.Count = attr.Expr
   132  	}
   133  
   134  	if attr, exists := content.Attributes["for_each"]; exists {
   135  		r.ForEach = attr.Expr
   136  		// Cannot have count and for_each on the same resource block
   137  		if r.Count != nil {
   138  			diags = append(diags, &hcl.Diagnostic{
   139  				Severity: hcl.DiagError,
   140  				Summary:  `Invalid combination of "count" and "for_each"`,
   141  				Detail:   `The "count" and "for_each" meta-arguments are mutually-exclusive, only one should be used to be explicit about the number of resources to be created.`,
   142  				Subject:  &attr.NameRange,
   143  			})
   144  		}
   145  	}
   146  
   147  	if attr, exists := content.Attributes["provider"]; exists {
   148  		var providerDiags hcl.Diagnostics
   149  		r.ProviderConfigRef, providerDiags = decodeProviderConfigRef(attr.Expr, "provider")
   150  		diags = append(diags, providerDiags...)
   151  	}
   152  
   153  	if attr, exists := content.Attributes["depends_on"]; exists {
   154  		deps, depsDiags := decodeDependsOn(attr)
   155  		diags = append(diags, depsDiags...)
   156  		r.DependsOn = append(r.DependsOn, deps...)
   157  	}
   158  
   159  	var seenLifecycle *hcl.Block
   160  	var seenConnection *hcl.Block
   161  	var seenEscapeBlock *hcl.Block
   162  	for _, block := range content.Blocks {
   163  		switch block.Type {
   164  		case "lifecycle":
   165  			if seenLifecycle != nil {
   166  				diags = append(diags, &hcl.Diagnostic{
   167  					Severity: hcl.DiagError,
   168  					Summary:  "Duplicate lifecycle block",
   169  					Detail:   fmt.Sprintf("This resource already has a lifecycle block at %s.", seenLifecycle.DefRange),
   170  					Subject:  &block.DefRange,
   171  				})
   172  				continue
   173  			}
   174  			seenLifecycle = block
   175  
   176  			lcContent, lcDiags := block.Body.Content(resourceLifecycleBlockSchema)
   177  			diags = append(diags, lcDiags...)
   178  
   179  			if attr, exists := lcContent.Attributes["create_before_destroy"]; exists {
   180  				valDiags := gohcl.DecodeExpression(attr.Expr, nil, &r.Managed.CreateBeforeDestroy)
   181  				diags = append(diags, valDiags...)
   182  				r.Managed.CreateBeforeDestroySet = true
   183  			}
   184  
   185  			if attr, exists := lcContent.Attributes["prevent_destroy"]; exists {
   186  				valDiags := gohcl.DecodeExpression(attr.Expr, nil, &r.Managed.PreventDestroy)
   187  				diags = append(diags, valDiags...)
   188  				r.Managed.PreventDestroySet = true
   189  			}
   190  
   191  			if attr, exists := lcContent.Attributes["replace_triggered_by"]; exists {
   192  				exprs, hclDiags := decodeReplaceTriggeredBy(attr.Expr)
   193  				diags = diags.Extend(hclDiags)
   194  
   195  				r.TriggersReplacement = append(r.TriggersReplacement, exprs...)
   196  			}
   197  
   198  			if attr, exists := lcContent.Attributes["ignore_changes"]; exists {
   199  
   200  				// ignore_changes can either be a list of relative traversals
   201  				// or it can be just the keyword "all" to ignore changes to this
   202  				// resource entirely.
   203  				//   ignore_changes = [ami, instance_type]
   204  				//   ignore_changes = all
   205  				// We also allow two legacy forms for compatibility with earlier
   206  				// versions:
   207  				//   ignore_changes = ["ami", "instance_type"]
   208  				//   ignore_changes = ["*"]
   209  
   210  				kw := hcl.ExprAsKeyword(attr.Expr)
   211  
   212  				switch {
   213  				case kw == "all":
   214  					r.Managed.IgnoreAllChanges = true
   215  				default:
   216  					exprs, listDiags := hcl.ExprList(attr.Expr)
   217  					diags = append(diags, listDiags...)
   218  
   219  					var ignoreAllRange hcl.Range
   220  
   221  					for _, expr := range exprs {
   222  
   223  						// our expr might be the literal string "*", which
   224  						// we accept as a deprecated way of saying "all".
   225  						if shimIsIgnoreChangesStar(expr) {
   226  							r.Managed.IgnoreAllChanges = true
   227  							ignoreAllRange = expr.Range()
   228  							diags = append(diags, &hcl.Diagnostic{
   229  								Severity: hcl.DiagError,
   230  								Summary:  "Invalid ignore_changes wildcard",
   231  								Detail:   "The [\"*\"] form of ignore_changes wildcard is was deprecated and is now invalid. Use \"ignore_changes = all\" to ignore changes to all attributes.",
   232  								Subject:  attr.Expr.Range().Ptr(),
   233  							})
   234  							continue
   235  						}
   236  
   237  						expr, shimDiags := shimTraversalInString(expr, false)
   238  						diags = append(diags, shimDiags...)
   239  
   240  						traversal, travDiags := hcl.RelTraversalForExpr(expr)
   241  						diags = append(diags, travDiags...)
   242  						if len(traversal) != 0 {
   243  							r.Managed.IgnoreChanges = append(r.Managed.IgnoreChanges, traversal)
   244  						}
   245  					}
   246  
   247  					if r.Managed.IgnoreAllChanges && len(r.Managed.IgnoreChanges) != 0 {
   248  						diags = append(diags, &hcl.Diagnostic{
   249  							Severity: hcl.DiagError,
   250  							Summary:  "Invalid ignore_changes ruleset",
   251  							Detail:   "Cannot mix wildcard string \"*\" with non-wildcard references.",
   252  							Subject:  &ignoreAllRange,
   253  							Context:  attr.Expr.Range().Ptr(),
   254  						})
   255  					}
   256  
   257  				}
   258  			}
   259  
   260  			for _, block := range lcContent.Blocks {
   261  				switch block.Type {
   262  				case "precondition", "postcondition":
   263  					cr, moreDiags := decodeCheckRuleBlock(block, override)
   264  					diags = append(diags, moreDiags...)
   265  
   266  					moreDiags = cr.validateSelfReferences(block.Type, r.Addr())
   267  					diags = append(diags, moreDiags...)
   268  
   269  					switch block.Type {
   270  					case "precondition":
   271  						r.Preconditions = append(r.Preconditions, cr)
   272  					case "postcondition":
   273  						r.Postconditions = append(r.Postconditions, cr)
   274  					}
   275  				default:
   276  					// The cases above should be exhaustive for all block types
   277  					// defined in the lifecycle schema, so this shouldn't happen.
   278  					panic(fmt.Sprintf("unexpected lifecycle sub-block type %q", block.Type))
   279  				}
   280  			}
   281  
   282  		case "connection":
   283  			if seenConnection != nil {
   284  				diags = append(diags, &hcl.Diagnostic{
   285  					Severity: hcl.DiagError,
   286  					Summary:  "Duplicate connection block",
   287  					Detail:   fmt.Sprintf("This resource already has a connection block at %s.", seenConnection.DefRange),
   288  					Subject:  &block.DefRange,
   289  				})
   290  				continue
   291  			}
   292  			seenConnection = block
   293  
   294  			r.Managed.Connection = &Connection{
   295  				Config:    block.Body,
   296  				DeclRange: block.DefRange,
   297  			}
   298  
   299  		case "provisioner":
   300  			pv, pvDiags := decodeProvisionerBlock(block)
   301  			diags = append(diags, pvDiags...)
   302  			if pv != nil {
   303  				r.Managed.Provisioners = append(r.Managed.Provisioners, pv)
   304  			}
   305  
   306  		case "_":
   307  			if seenEscapeBlock != nil {
   308  				diags = append(diags, &hcl.Diagnostic{
   309  					Severity: hcl.DiagError,
   310  					Summary:  "Duplicate escaping block",
   311  					Detail: fmt.Sprintf(
   312  						"The special block type \"_\" can be used to force particular arguments to be interpreted as resource-type-specific rather than as meta-arguments, but each resource block can have only one such block. The first escaping block was at %s.",
   313  						seenEscapeBlock.DefRange,
   314  					),
   315  					Subject: &block.DefRange,
   316  				})
   317  				continue
   318  			}
   319  			seenEscapeBlock = block
   320  
   321  			// When there's an escaping block its content merges with the
   322  			// existing config we extracted earlier, so later decoding
   323  			// will see a blend of both.
   324  			r.Config = hcl.MergeBodies([]hcl.Body{r.Config, block.Body})
   325  
   326  		default:
   327  			// Any other block types are ones we've reserved for future use,
   328  			// so they get a generic message.
   329  			diags = append(diags, &hcl.Diagnostic{
   330  				Severity: hcl.DiagError,
   331  				Summary:  "Reserved block type name in resource block",
   332  				Detail:   fmt.Sprintf("The block type name %q is reserved for use by Durgaform in a future version.", block.Type),
   333  				Subject:  &block.TypeRange,
   334  			})
   335  		}
   336  	}
   337  
   338  	// Now we can validate the connection block references if there are any destroy provisioners.
   339  	// TODO: should we eliminate standalone connection blocks?
   340  	if r.Managed.Connection != nil {
   341  		for _, p := range r.Managed.Provisioners {
   342  			if p.When == ProvisionerWhenDestroy {
   343  				diags = append(diags, onlySelfRefs(r.Managed.Connection.Config)...)
   344  				break
   345  			}
   346  		}
   347  	}
   348  
   349  	return r, diags
   350  }
   351  
   352  func decodeDataBlock(block *hcl.Block, override bool) (*Resource, hcl.Diagnostics) {
   353  	var diags hcl.Diagnostics
   354  	r := &Resource{
   355  		Mode:      addrs.DataResourceMode,
   356  		Type:      block.Labels[0],
   357  		Name:      block.Labels[1],
   358  		DeclRange: block.DefRange,
   359  		TypeRange: block.LabelRanges[0],
   360  	}
   361  
   362  	content, remain, moreDiags := block.Body.PartialContent(dataBlockSchema)
   363  	diags = append(diags, moreDiags...)
   364  	r.Config = remain
   365  
   366  	if !hclsyntax.ValidIdentifier(r.Type) {
   367  		diags = append(diags, &hcl.Diagnostic{
   368  			Severity: hcl.DiagError,
   369  			Summary:  "Invalid data source name",
   370  			Detail:   badIdentifierDetail,
   371  			Subject:  &block.LabelRanges[0],
   372  		})
   373  	}
   374  	if !hclsyntax.ValidIdentifier(r.Name) {
   375  		diags = append(diags, &hcl.Diagnostic{
   376  			Severity: hcl.DiagError,
   377  			Summary:  "Invalid data resource name",
   378  			Detail:   badIdentifierDetail,
   379  			Subject:  &block.LabelRanges[1],
   380  		})
   381  	}
   382  
   383  	if attr, exists := content.Attributes["count"]; exists {
   384  		r.Count = attr.Expr
   385  	}
   386  
   387  	if attr, exists := content.Attributes["for_each"]; exists {
   388  		r.ForEach = attr.Expr
   389  		// Cannot have count and for_each on the same data block
   390  		if r.Count != nil {
   391  			diags = append(diags, &hcl.Diagnostic{
   392  				Severity: hcl.DiagError,
   393  				Summary:  `Invalid combination of "count" and "for_each"`,
   394  				Detail:   `The "count" and "for_each" meta-arguments are mutually-exclusive, only one should be used to be explicit about the number of resources to be created.`,
   395  				Subject:  &attr.NameRange,
   396  			})
   397  		}
   398  	}
   399  
   400  	if attr, exists := content.Attributes["provider"]; exists {
   401  		var providerDiags hcl.Diagnostics
   402  		r.ProviderConfigRef, providerDiags = decodeProviderConfigRef(attr.Expr, "provider")
   403  		diags = append(diags, providerDiags...)
   404  	}
   405  
   406  	if attr, exists := content.Attributes["depends_on"]; exists {
   407  		deps, depsDiags := decodeDependsOn(attr)
   408  		diags = append(diags, depsDiags...)
   409  		r.DependsOn = append(r.DependsOn, deps...)
   410  	}
   411  
   412  	var seenEscapeBlock *hcl.Block
   413  	var seenLifecycle *hcl.Block
   414  	for _, block := range content.Blocks {
   415  		switch block.Type {
   416  
   417  		case "_":
   418  			if seenEscapeBlock != nil {
   419  				diags = append(diags, &hcl.Diagnostic{
   420  					Severity: hcl.DiagError,
   421  					Summary:  "Duplicate escaping block",
   422  					Detail: fmt.Sprintf(
   423  						"The special block type \"_\" can be used to force particular arguments to be interpreted as resource-type-specific rather than as meta-arguments, but each data block can have only one such block. The first escaping block was at %s.",
   424  						seenEscapeBlock.DefRange,
   425  					),
   426  					Subject: &block.DefRange,
   427  				})
   428  				continue
   429  			}
   430  			seenEscapeBlock = block
   431  
   432  			// When there's an escaping block its content merges with the
   433  			// existing config we extracted earlier, so later decoding
   434  			// will see a blend of both.
   435  			r.Config = hcl.MergeBodies([]hcl.Body{r.Config, block.Body})
   436  
   437  		case "lifecycle":
   438  			if seenLifecycle != nil {
   439  				diags = append(diags, &hcl.Diagnostic{
   440  					Severity: hcl.DiagError,
   441  					Summary:  "Duplicate lifecycle block",
   442  					Detail:   fmt.Sprintf("This resource already has a lifecycle block at %s.", seenLifecycle.DefRange),
   443  					Subject:  block.DefRange.Ptr(),
   444  				})
   445  				continue
   446  			}
   447  			seenLifecycle = block
   448  
   449  			lcContent, lcDiags := block.Body.Content(resourceLifecycleBlockSchema)
   450  			diags = append(diags, lcDiags...)
   451  
   452  			// All of the attributes defined for resource lifecycle are for
   453  			// managed resources only, so we can emit a common error message
   454  			// for any given attributes that HCL accepted.
   455  			for name, attr := range lcContent.Attributes {
   456  				diags = append(diags, &hcl.Diagnostic{
   457  					Severity: hcl.DiagError,
   458  					Summary:  "Invalid data resource lifecycle argument",
   459  					Detail:   fmt.Sprintf("The lifecycle argument %q is defined only for managed resources (\"resource\" blocks), and is not valid for data resources.", name),
   460  					Subject:  attr.NameRange.Ptr(),
   461  				})
   462  			}
   463  
   464  			for _, block := range lcContent.Blocks {
   465  				switch block.Type {
   466  				case "precondition", "postcondition":
   467  					cr, moreDiags := decodeCheckRuleBlock(block, override)
   468  					diags = append(diags, moreDiags...)
   469  
   470  					moreDiags = cr.validateSelfReferences(block.Type, r.Addr())
   471  					diags = append(diags, moreDiags...)
   472  
   473  					switch block.Type {
   474  					case "precondition":
   475  						r.Preconditions = append(r.Preconditions, cr)
   476  					case "postcondition":
   477  						r.Postconditions = append(r.Postconditions, cr)
   478  					}
   479  				default:
   480  					// The cases above should be exhaustive for all block types
   481  					// defined in the lifecycle schema, so this shouldn't happen.
   482  					panic(fmt.Sprintf("unexpected lifecycle sub-block type %q", block.Type))
   483  				}
   484  			}
   485  
   486  		default:
   487  			// Any other block types are ones we're reserving for future use,
   488  			// but don't have any defined meaning today.
   489  			diags = append(diags, &hcl.Diagnostic{
   490  				Severity: hcl.DiagError,
   491  				Summary:  "Reserved block type name in data block",
   492  				Detail:   fmt.Sprintf("The block type name %q is reserved for use by Durgaform in a future version.", block.Type),
   493  				Subject:  block.TypeRange.Ptr(),
   494  			})
   495  		}
   496  	}
   497  
   498  	return r, diags
   499  }
   500  
   501  // decodeReplaceTriggeredBy decodes and does basic validation of the
   502  // replace_triggered_by expressions, ensuring they only contains references to
   503  // a single resource, and the only extra variables are count.index or each.key.
   504  func decodeReplaceTriggeredBy(expr hcl.Expression) ([]hcl.Expression, hcl.Diagnostics) {
   505  	// Since we are manually parsing the replace_triggered_by argument, we
   506  	// need to specially handle json configs, in which case the values will
   507  	// be json strings rather than hcl. To simplify parsing however we will
   508  	// decode the individual list elements, rather than the entire expression.
   509  	isJSON := hcljson.IsJSONExpression(expr)
   510  
   511  	exprs, diags := hcl.ExprList(expr)
   512  
   513  	for i, expr := range exprs {
   514  		if isJSON {
   515  			// We can abuse the hcl json api and rely on the fact that calling
   516  			// Value on a json expression with no EvalContext will return the
   517  			// raw string. We can then parse that as normal hcl syntax, and
   518  			// continue with the decoding.
   519  			v, ds := expr.Value(nil)
   520  			diags = diags.Extend(ds)
   521  			if diags.HasErrors() {
   522  				continue
   523  			}
   524  
   525  			expr, ds = hclsyntax.ParseExpression([]byte(v.AsString()), "", expr.Range().Start)
   526  			diags = diags.Extend(ds)
   527  			if diags.HasErrors() {
   528  				continue
   529  			}
   530  			// make sure to swap out the expression we're returning too
   531  			exprs[i] = expr
   532  		}
   533  
   534  		refs, refDiags := lang.ReferencesInExpr(expr)
   535  		for _, diag := range refDiags {
   536  			severity := hcl.DiagError
   537  			if diag.Severity() == tfdiags.Warning {
   538  				severity = hcl.DiagWarning
   539  			}
   540  
   541  			desc := diag.Description()
   542  
   543  			diags = append(diags, &hcl.Diagnostic{
   544  				Severity: severity,
   545  				Summary:  desc.Summary,
   546  				Detail:   desc.Detail,
   547  				Subject:  expr.Range().Ptr(),
   548  			})
   549  		}
   550  
   551  		if refDiags.HasErrors() {
   552  			continue
   553  		}
   554  
   555  		resourceCount := 0
   556  		for _, ref := range refs {
   557  			switch sub := ref.Subject.(type) {
   558  			case addrs.Resource, addrs.ResourceInstance:
   559  				resourceCount++
   560  
   561  			case addrs.ForEachAttr:
   562  				if sub.Name != "key" {
   563  					diags = append(diags, &hcl.Diagnostic{
   564  						Severity: hcl.DiagError,
   565  						Summary:  "Invalid each reference in replace_triggered_by expression",
   566  						Detail:   "Only each.key may be used in replace_triggered_by.",
   567  						Subject:  expr.Range().Ptr(),
   568  					})
   569  				}
   570  			case addrs.CountAttr:
   571  				if sub.Name != "index" {
   572  					diags = append(diags, &hcl.Diagnostic{
   573  						Severity: hcl.DiagError,
   574  						Summary:  "Invalid count reference in replace_triggered_by expression",
   575  						Detail:   "Only count.index may be used in replace_triggered_by.",
   576  						Subject:  expr.Range().Ptr(),
   577  					})
   578  				}
   579  			default:
   580  				// everything else should be simple traversals
   581  				diags = append(diags, &hcl.Diagnostic{
   582  					Severity: hcl.DiagError,
   583  					Summary:  "Invalid reference in replace_triggered_by expression",
   584  					Detail:   "Only resources, count.index, and each.key may be used in replace_triggered_by.",
   585  					Subject:  expr.Range().Ptr(),
   586  				})
   587  			}
   588  		}
   589  
   590  		switch {
   591  		case resourceCount == 0:
   592  			diags = append(diags, &hcl.Diagnostic{
   593  				Severity: hcl.DiagError,
   594  				Summary:  "Invalid replace_triggered_by expression",
   595  				Detail:   "Missing resource reference in replace_triggered_by expression.",
   596  				Subject:  expr.Range().Ptr(),
   597  			})
   598  		case resourceCount > 1:
   599  			diags = append(diags, &hcl.Diagnostic{
   600  				Severity: hcl.DiagError,
   601  				Summary:  "Invalid replace_triggered_by expression",
   602  				Detail:   "Multiple resource references in replace_triggered_by expression.",
   603  				Subject:  expr.Range().Ptr(),
   604  			})
   605  		}
   606  	}
   607  	return exprs, diags
   608  }
   609  
   610  type ProviderConfigRef struct {
   611  	Name       string
   612  	NameRange  hcl.Range
   613  	Alias      string
   614  	AliasRange *hcl.Range // nil if alias not set
   615  
   616  	// TODO: this may not be set in some cases, so it is not yet suitable for
   617  	// use outside of this package. We currently only use it for internal
   618  	// validation, but once we verify that this can be set in all cases, we can
   619  	// export this so providers don't need to be re-resolved.
   620  	// This same field is also added to the Provider struct.
   621  	providerType addrs.Provider
   622  }
   623  
   624  func decodeProviderConfigRef(expr hcl.Expression, argName string) (*ProviderConfigRef, hcl.Diagnostics) {
   625  	var diags hcl.Diagnostics
   626  
   627  	var shimDiags hcl.Diagnostics
   628  	expr, shimDiags = shimTraversalInString(expr, false)
   629  	diags = append(diags, shimDiags...)
   630  
   631  	traversal, travDiags := hcl.AbsTraversalForExpr(expr)
   632  
   633  	// AbsTraversalForExpr produces only generic errors, so we'll discard
   634  	// the errors given and produce our own with extra context. If we didn't
   635  	// get any errors then we might still have warnings, though.
   636  	if !travDiags.HasErrors() {
   637  		diags = append(diags, travDiags...)
   638  	}
   639  
   640  	if len(traversal) < 1 || len(traversal) > 2 {
   641  		// A provider reference was given as a string literal in the legacy
   642  		// configuration language and there are lots of examples out there
   643  		// showing that usage, so we'll sniff for that situation here and
   644  		// produce a specialized error message for it to help users find
   645  		// the new correct form.
   646  		if exprIsNativeQuotedString(expr) {
   647  			diags = append(diags, &hcl.Diagnostic{
   648  				Severity: hcl.DiagError,
   649  				Summary:  "Invalid provider configuration reference",
   650  				Detail:   "A provider configuration reference must not be given in quotes.",
   651  				Subject:  expr.Range().Ptr(),
   652  			})
   653  			return nil, diags
   654  		}
   655  
   656  		diags = append(diags, &hcl.Diagnostic{
   657  			Severity: hcl.DiagError,
   658  			Summary:  "Invalid provider configuration reference",
   659  			Detail:   fmt.Sprintf("The %s argument requires a provider type name, optionally followed by a period and then a configuration alias.", argName),
   660  			Subject:  expr.Range().Ptr(),
   661  		})
   662  		return nil, diags
   663  	}
   664  
   665  	// verify that the provider local name is normalized
   666  	name := traversal.RootName()
   667  	nameDiags := checkProviderNameNormalized(name, traversal[0].SourceRange())
   668  	diags = append(diags, nameDiags...)
   669  	if diags.HasErrors() {
   670  		return nil, diags
   671  	}
   672  
   673  	ret := &ProviderConfigRef{
   674  		Name:      name,
   675  		NameRange: traversal[0].SourceRange(),
   676  	}
   677  
   678  	if len(traversal) > 1 {
   679  		aliasStep, ok := traversal[1].(hcl.TraverseAttr)
   680  		if !ok {
   681  			diags = append(diags, &hcl.Diagnostic{
   682  				Severity: hcl.DiagError,
   683  				Summary:  "Invalid provider configuration reference",
   684  				Detail:   "Provider name must either stand alone or be followed by a period and then a configuration alias.",
   685  				Subject:  traversal[1].SourceRange().Ptr(),
   686  			})
   687  			return ret, diags
   688  		}
   689  
   690  		ret.Alias = aliasStep.Name
   691  		ret.AliasRange = aliasStep.SourceRange().Ptr()
   692  	}
   693  
   694  	return ret, diags
   695  }
   696  
   697  // Addr returns the provider config address corresponding to the receiving
   698  // config reference.
   699  //
   700  // This is a trivial conversion, essentially just discarding the source
   701  // location information and keeping just the addressing information.
   702  func (r *ProviderConfigRef) Addr() addrs.LocalProviderConfig {
   703  	return addrs.LocalProviderConfig{
   704  		LocalName: r.Name,
   705  		Alias:     r.Alias,
   706  	}
   707  }
   708  
   709  func (r *ProviderConfigRef) String() string {
   710  	if r == nil {
   711  		return "<nil>"
   712  	}
   713  	if r.Alias != "" {
   714  		return fmt.Sprintf("%s.%s", r.Name, r.Alias)
   715  	}
   716  	return r.Name
   717  }
   718  
   719  var commonResourceAttributes = []hcl.AttributeSchema{
   720  	{
   721  		Name: "count",
   722  	},
   723  	{
   724  		Name: "for_each",
   725  	},
   726  	{
   727  		Name: "provider",
   728  	},
   729  	{
   730  		Name: "depends_on",
   731  	},
   732  }
   733  
   734  var resourceBlockSchema = &hcl.BodySchema{
   735  	Attributes: commonResourceAttributes,
   736  	Blocks: []hcl.BlockHeaderSchema{
   737  		{Type: "locals"}, // reserved for future use
   738  		{Type: "lifecycle"},
   739  		{Type: "connection"},
   740  		{Type: "provisioner", LabelNames: []string{"type"}},
   741  		{Type: "_"}, // meta-argument escaping block
   742  	},
   743  }
   744  
   745  var dataBlockSchema = &hcl.BodySchema{
   746  	Attributes: commonResourceAttributes,
   747  	Blocks: []hcl.BlockHeaderSchema{
   748  		{Type: "lifecycle"},
   749  		{Type: "locals"}, // reserved for future use
   750  		{Type: "_"},      // meta-argument escaping block
   751  	},
   752  }
   753  
   754  var resourceLifecycleBlockSchema = &hcl.BodySchema{
   755  	// We tell HCL that these elements are all valid for both "resource"
   756  	// and "data" lifecycle blocks, but the rules are actually more restrictive
   757  	// than that. We deal with that after decoding so that we can return
   758  	// more specific error messages than HCL would typically return itself.
   759  	Attributes: []hcl.AttributeSchema{
   760  		{
   761  			Name: "create_before_destroy",
   762  		},
   763  		{
   764  			Name: "prevent_destroy",
   765  		},
   766  		{
   767  			Name: "ignore_changes",
   768  		},
   769  		{
   770  			Name: "replace_triggered_by",
   771  		},
   772  	},
   773  	Blocks: []hcl.BlockHeaderSchema{
   774  		{Type: "precondition"},
   775  		{Type: "postcondition"},
   776  	},
   777  }