github.com/hashicorp/terraform-plugin-sdk@v1.17.2/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  
    10  	"github.com/hashicorp/terraform-plugin-sdk/internal/addrs"
    11  )
    12  
    13  // Resource represents a "resource" or "data" block in a module or file.
    14  type Resource struct {
    15  	Mode    addrs.ResourceMode
    16  	Name    string
    17  	Type    string
    18  	Config  hcl.Body
    19  	Count   hcl.Expression
    20  	ForEach hcl.Expression
    21  
    22  	ProviderConfigRef *ProviderConfigRef
    23  
    24  	DependsOn []hcl.Traversal
    25  
    26  	// Managed is populated only for Mode = addrs.ManagedResourceMode,
    27  	// containing the additional fields that apply to managed resources.
    28  	// For all other resource modes, this field is nil.
    29  	Managed *ManagedResource
    30  
    31  	DeclRange hcl.Range
    32  	TypeRange hcl.Range
    33  }
    34  
    35  // ManagedResource represents a "resource" block in a module or file.
    36  type ManagedResource struct {
    37  	Connection   *Connection
    38  	Provisioners []*Provisioner
    39  
    40  	CreateBeforeDestroy bool
    41  	PreventDestroy      bool
    42  	IgnoreChanges       []hcl.Traversal
    43  	IgnoreAllChanges    bool
    44  
    45  	CreateBeforeDestroySet bool
    46  	PreventDestroySet      bool
    47  }
    48  
    49  func (r *Resource) moduleUniqueKey() string {
    50  	return r.Addr().String()
    51  }
    52  
    53  // Addr returns a resource address for the receiver that is relative to the
    54  // resource's containing module.
    55  func (r *Resource) Addr() addrs.Resource {
    56  	return addrs.Resource{
    57  		Mode: r.Mode,
    58  		Type: r.Type,
    59  		Name: r.Name,
    60  	}
    61  }
    62  
    63  // ProviderConfigAddr returns the address for the provider configuration
    64  // that should be used for this resource. This function implements the
    65  // default behavior of extracting the type from the resource type name if
    66  // an explicit "provider" argument was not provided.
    67  func (r *Resource) ProviderConfigAddr() addrs.ProviderConfig {
    68  	if r.ProviderConfigRef == nil {
    69  		return r.Addr().DefaultProviderConfig()
    70  	}
    71  
    72  	return addrs.ProviderConfig{
    73  		Type:  r.ProviderConfigRef.Name,
    74  		Alias: r.ProviderConfigRef.Alias,
    75  	}
    76  }
    77  
    78  func decodeResourceBlock(block *hcl.Block) (*Resource, hcl.Diagnostics) {
    79  	r := &Resource{
    80  		Mode:      addrs.ManagedResourceMode,
    81  		Type:      block.Labels[0],
    82  		Name:      block.Labels[1],
    83  		DeclRange: block.DefRange,
    84  		TypeRange: block.LabelRanges[0],
    85  		Managed:   &ManagedResource{},
    86  	}
    87  
    88  	content, remain, diags := block.Body.PartialContent(resourceBlockSchema)
    89  	r.Config = remain
    90  
    91  	if !hclsyntax.ValidIdentifier(r.Type) {
    92  		diags = append(diags, &hcl.Diagnostic{
    93  			Severity: hcl.DiagError,
    94  			Summary:  "Invalid resource type name",
    95  			Detail:   badIdentifierDetail,
    96  			Subject:  &block.LabelRanges[0],
    97  		})
    98  	}
    99  	if !hclsyntax.ValidIdentifier(r.Name) {
   100  		diags = append(diags, &hcl.Diagnostic{
   101  			Severity: hcl.DiagError,
   102  			Summary:  "Invalid resource name",
   103  			Detail:   badIdentifierDetail,
   104  			Subject:  &block.LabelRanges[1],
   105  		})
   106  	}
   107  
   108  	if attr, exists := content.Attributes["count"]; exists {
   109  		r.Count = attr.Expr
   110  	}
   111  
   112  	if attr, exists := content.Attributes["for_each"]; exists {
   113  		r.ForEach = attr.Expr
   114  		// Cannot have count and for_each on the same resource block
   115  		if r.Count != nil {
   116  			diags = append(diags, &hcl.Diagnostic{
   117  				Severity: hcl.DiagError,
   118  				Summary:  `Invalid combination of "count" and "for_each"`,
   119  				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.`,
   120  				Subject:  &attr.NameRange,
   121  			})
   122  		}
   123  	}
   124  
   125  	if attr, exists := content.Attributes["provider"]; exists {
   126  		var providerDiags hcl.Diagnostics
   127  		r.ProviderConfigRef, providerDiags = decodeProviderConfigRef(attr.Expr, "provider")
   128  		diags = append(diags, providerDiags...)
   129  	}
   130  
   131  	if attr, exists := content.Attributes["depends_on"]; exists {
   132  		deps, depsDiags := decodeDependsOn(attr)
   133  		diags = append(diags, depsDiags...)
   134  		r.DependsOn = append(r.DependsOn, deps...)
   135  	}
   136  
   137  	var seenLifecycle *hcl.Block
   138  	var seenConnection *hcl.Block
   139  	for _, block := range content.Blocks {
   140  		switch block.Type {
   141  		case "lifecycle":
   142  			if seenLifecycle != nil {
   143  				diags = append(diags, &hcl.Diagnostic{
   144  					Severity: hcl.DiagError,
   145  					Summary:  "Duplicate lifecycle block",
   146  					Detail:   fmt.Sprintf("This resource already has a lifecycle block at %s.", seenLifecycle.DefRange),
   147  					Subject:  &block.DefRange,
   148  				})
   149  				continue
   150  			}
   151  			seenLifecycle = block
   152  
   153  			lcContent, lcDiags := block.Body.Content(resourceLifecycleBlockSchema)
   154  			diags = append(diags, lcDiags...)
   155  
   156  			if attr, exists := lcContent.Attributes["create_before_destroy"]; exists {
   157  				valDiags := gohcl.DecodeExpression(attr.Expr, nil, &r.Managed.CreateBeforeDestroy)
   158  				diags = append(diags, valDiags...)
   159  				r.Managed.CreateBeforeDestroySet = true
   160  			}
   161  
   162  			if attr, exists := lcContent.Attributes["prevent_destroy"]; exists {
   163  				valDiags := gohcl.DecodeExpression(attr.Expr, nil, &r.Managed.PreventDestroy)
   164  				diags = append(diags, valDiags...)
   165  				r.Managed.PreventDestroySet = true
   166  			}
   167  
   168  			if attr, exists := lcContent.Attributes["ignore_changes"]; exists {
   169  
   170  				// ignore_changes can either be a list of relative traversals
   171  				// or it can be just the keyword "all" to ignore changes to this
   172  				// resource entirely.
   173  				//   ignore_changes = [ami, instance_type]
   174  				//   ignore_changes = all
   175  				// We also allow two legacy forms for compatibility with earlier
   176  				// versions:
   177  				//   ignore_changes = ["ami", "instance_type"]
   178  				//   ignore_changes = ["*"]
   179  
   180  				kw := hcl.ExprAsKeyword(attr.Expr)
   181  
   182  				switch {
   183  				case kw == "all":
   184  					r.Managed.IgnoreAllChanges = true
   185  				default:
   186  					exprs, listDiags := hcl.ExprList(attr.Expr)
   187  					diags = append(diags, listDiags...)
   188  
   189  					var ignoreAllRange hcl.Range
   190  
   191  					for _, expr := range exprs {
   192  
   193  						// our expr might be the literal string "*", which
   194  						// we accept as a deprecated way of saying "all".
   195  						if shimIsIgnoreChangesStar(expr) {
   196  							r.Managed.IgnoreAllChanges = true
   197  							ignoreAllRange = expr.Range()
   198  							diags = append(diags, &hcl.Diagnostic{
   199  								Severity: hcl.DiagWarning,
   200  								Summary:  "Deprecated ignore_changes wildcard",
   201  								Detail:   "The [\"*\"] form of ignore_changes wildcard is deprecated. Use \"ignore_changes = all\" to ignore changes to all attributes.",
   202  								Subject:  attr.Expr.Range().Ptr(),
   203  							})
   204  							continue
   205  						}
   206  
   207  						expr, shimDiags := shimTraversalInString(expr, false)
   208  						diags = append(diags, shimDiags...)
   209  
   210  						traversal, travDiags := hcl.RelTraversalForExpr(expr)
   211  						diags = append(diags, travDiags...)
   212  						if len(traversal) != 0 {
   213  							r.Managed.IgnoreChanges = append(r.Managed.IgnoreChanges, traversal)
   214  						}
   215  					}
   216  
   217  					if r.Managed.IgnoreAllChanges && len(r.Managed.IgnoreChanges) != 0 {
   218  						diags = append(diags, &hcl.Diagnostic{
   219  							Severity: hcl.DiagError,
   220  							Summary:  "Invalid ignore_changes ruleset",
   221  							Detail:   "Cannot mix wildcard string \"*\" with non-wildcard references.",
   222  							Subject:  &ignoreAllRange,
   223  							Context:  attr.Expr.Range().Ptr(),
   224  						})
   225  					}
   226  
   227  				}
   228  
   229  			}
   230  
   231  		case "connection":
   232  			if seenConnection != nil {
   233  				diags = append(diags, &hcl.Diagnostic{
   234  					Severity: hcl.DiagError,
   235  					Summary:  "Duplicate connection block",
   236  					Detail:   fmt.Sprintf("This resource already has a connection block at %s.", seenConnection.DefRange),
   237  					Subject:  &block.DefRange,
   238  				})
   239  				continue
   240  			}
   241  			seenConnection = block
   242  
   243  			r.Managed.Connection = &Connection{
   244  				Config:    block.Body,
   245  				DeclRange: block.DefRange,
   246  			}
   247  
   248  		case "provisioner":
   249  			pv, pvDiags := decodeProvisionerBlock(block)
   250  			diags = append(diags, pvDiags...)
   251  			if pv != nil {
   252  				r.Managed.Provisioners = append(r.Managed.Provisioners, pv)
   253  			}
   254  
   255  		default:
   256  			// Any other block types are ones we've reserved for future use,
   257  			// so they get a generic message.
   258  			diags = append(diags, &hcl.Diagnostic{
   259  				Severity: hcl.DiagError,
   260  				Summary:  "Reserved block type name in resource block",
   261  				Detail:   fmt.Sprintf("The block type name %q is reserved for use by Terraform in a future version.", block.Type),
   262  				Subject:  &block.TypeRange,
   263  			})
   264  		}
   265  	}
   266  
   267  	return r, diags
   268  }
   269  
   270  func decodeDataBlock(block *hcl.Block) (*Resource, hcl.Diagnostics) {
   271  	r := &Resource{
   272  		Mode:      addrs.DataResourceMode,
   273  		Type:      block.Labels[0],
   274  		Name:      block.Labels[1],
   275  		DeclRange: block.DefRange,
   276  		TypeRange: block.LabelRanges[0],
   277  	}
   278  
   279  	content, remain, diags := block.Body.PartialContent(dataBlockSchema)
   280  	r.Config = remain
   281  
   282  	if !hclsyntax.ValidIdentifier(r.Type) {
   283  		diags = append(diags, &hcl.Diagnostic{
   284  			Severity: hcl.DiagError,
   285  			Summary:  "Invalid data source name",
   286  			Detail:   badIdentifierDetail,
   287  			Subject:  &block.LabelRanges[0],
   288  		})
   289  	}
   290  	if !hclsyntax.ValidIdentifier(r.Name) {
   291  		diags = append(diags, &hcl.Diagnostic{
   292  			Severity: hcl.DiagError,
   293  			Summary:  "Invalid data resource name",
   294  			Detail:   badIdentifierDetail,
   295  			Subject:  &block.LabelRanges[1],
   296  		})
   297  	}
   298  
   299  	if attr, exists := content.Attributes["count"]; exists {
   300  		r.Count = attr.Expr
   301  	}
   302  
   303  	if attr, exists := content.Attributes["for_each"]; exists {
   304  		r.ForEach = attr.Expr
   305  		// Cannot have count and for_each on the same data block
   306  		if r.Count != nil {
   307  			diags = append(diags, &hcl.Diagnostic{
   308  				Severity: hcl.DiagError,
   309  				Summary:  `Invalid combination of "count" and "for_each"`,
   310  				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.`,
   311  				Subject:  &attr.NameRange,
   312  			})
   313  		}
   314  	}
   315  
   316  	if attr, exists := content.Attributes["provider"]; exists {
   317  		var providerDiags hcl.Diagnostics
   318  		r.ProviderConfigRef, providerDiags = decodeProviderConfigRef(attr.Expr, "provider")
   319  		diags = append(diags, providerDiags...)
   320  	}
   321  
   322  	if attr, exists := content.Attributes["depends_on"]; exists {
   323  		deps, depsDiags := decodeDependsOn(attr)
   324  		diags = append(diags, depsDiags...)
   325  		r.DependsOn = append(r.DependsOn, deps...)
   326  	}
   327  
   328  	for _, block := range content.Blocks {
   329  		// All of the block types we accept are just reserved for future use, but some get a specialized error message.
   330  		switch block.Type {
   331  		case "lifecycle":
   332  			diags = append(diags, &hcl.Diagnostic{
   333  				Severity: hcl.DiagError,
   334  				Summary:  "Unsupported lifecycle block",
   335  				Detail:   "Data resources do not have lifecycle settings, so a lifecycle block is not allowed.",
   336  				Subject:  &block.DefRange,
   337  			})
   338  		default:
   339  			diags = append(diags, &hcl.Diagnostic{
   340  				Severity: hcl.DiagError,
   341  				Summary:  "Reserved block type name in data block",
   342  				Detail:   fmt.Sprintf("The block type name %q is reserved for use by Terraform in a future version.", block.Type),
   343  				Subject:  &block.TypeRange,
   344  			})
   345  		}
   346  	}
   347  
   348  	return r, diags
   349  }
   350  
   351  type ProviderConfigRef struct {
   352  	Name       string
   353  	NameRange  hcl.Range
   354  	Alias      string
   355  	AliasRange *hcl.Range // nil if alias not set
   356  }
   357  
   358  func decodeProviderConfigRef(expr hcl.Expression, argName string) (*ProviderConfigRef, hcl.Diagnostics) {
   359  	var diags hcl.Diagnostics
   360  
   361  	var shimDiags hcl.Diagnostics
   362  	expr, shimDiags = shimTraversalInString(expr, false)
   363  	diags = append(diags, shimDiags...)
   364  
   365  	traversal, travDiags := hcl.AbsTraversalForExpr(expr)
   366  
   367  	// AbsTraversalForExpr produces only generic errors, so we'll discard
   368  	// the errors given and produce our own with extra context. If we didn't
   369  	// get any errors then we might still have warnings, though.
   370  	if !travDiags.HasErrors() {
   371  		diags = append(diags, travDiags...)
   372  	}
   373  
   374  	if len(traversal) < 1 || len(traversal) > 2 {
   375  		// A provider reference was given as a string literal in the legacy
   376  		// configuration language and there are lots of examples out there
   377  		// showing that usage, so we'll sniff for that situation here and
   378  		// produce a specialized error message for it to help users find
   379  		// the new correct form.
   380  		if exprIsNativeQuotedString(expr) {
   381  			diags = append(diags, &hcl.Diagnostic{
   382  				Severity: hcl.DiagError,
   383  				Summary:  "Invalid provider configuration reference",
   384  				Detail:   "A provider configuration reference must not be given in quotes.",
   385  				Subject:  expr.Range().Ptr(),
   386  			})
   387  			return nil, diags
   388  		}
   389  
   390  		diags = append(diags, &hcl.Diagnostic{
   391  			Severity: hcl.DiagError,
   392  			Summary:  "Invalid provider configuration reference",
   393  			Detail:   fmt.Sprintf("The %s argument requires a provider type name, optionally followed by a period and then a configuration alias.", argName),
   394  			Subject:  expr.Range().Ptr(),
   395  		})
   396  		return nil, diags
   397  	}
   398  
   399  	ret := &ProviderConfigRef{
   400  		Name:      traversal.RootName(),
   401  		NameRange: traversal[0].SourceRange(),
   402  	}
   403  
   404  	if len(traversal) > 1 {
   405  		aliasStep, ok := traversal[1].(hcl.TraverseAttr)
   406  		if !ok {
   407  			diags = append(diags, &hcl.Diagnostic{
   408  				Severity: hcl.DiagError,
   409  				Summary:  "Invalid provider configuration reference",
   410  				Detail:   "Provider name must either stand alone or be followed by a period and then a configuration alias.",
   411  				Subject:  traversal[1].SourceRange().Ptr(),
   412  			})
   413  			return ret, diags
   414  		}
   415  
   416  		ret.Alias = aliasStep.Name
   417  		ret.AliasRange = aliasStep.SourceRange().Ptr()
   418  	}
   419  
   420  	return ret, diags
   421  }
   422  
   423  // Addr returns the provider config address corresponding to the receiving
   424  // config reference.
   425  //
   426  // This is a trivial conversion, essentially just discarding the source
   427  // location information and keeping just the addressing information.
   428  func (r *ProviderConfigRef) Addr() addrs.ProviderConfig {
   429  	return addrs.ProviderConfig{
   430  		Type:  r.Name,
   431  		Alias: r.Alias,
   432  	}
   433  }
   434  
   435  func (r *ProviderConfigRef) String() string {
   436  	if r == nil {
   437  		return "<nil>"
   438  	}
   439  	if r.Alias != "" {
   440  		return fmt.Sprintf("%s.%s", r.Name, r.Alias)
   441  	}
   442  	return r.Name
   443  }
   444  
   445  var commonResourceAttributes = []hcl.AttributeSchema{
   446  	{
   447  		Name: "count",
   448  	},
   449  	{
   450  		Name: "for_each",
   451  	},
   452  	{
   453  		Name: "provider",
   454  	},
   455  	{
   456  		Name: "depends_on",
   457  	},
   458  }
   459  
   460  var resourceBlockSchema = &hcl.BodySchema{
   461  	Attributes: commonResourceAttributes,
   462  	Blocks: []hcl.BlockHeaderSchema{
   463  		{Type: "locals"}, // reserved for future use
   464  		{Type: "lifecycle"},
   465  		{Type: "connection"},
   466  		{Type: "provisioner", LabelNames: []string{"type"}},
   467  	},
   468  }
   469  
   470  var dataBlockSchema = &hcl.BodySchema{
   471  	Attributes: commonResourceAttributes,
   472  	Blocks: []hcl.BlockHeaderSchema{
   473  		{Type: "lifecycle"}, // reserved for future use
   474  		{Type: "locals"},    // reserved for future use
   475  	},
   476  }
   477  
   478  var resourceLifecycleBlockSchema = &hcl.BodySchema{
   479  	Attributes: []hcl.AttributeSchema{
   480  		{
   481  			Name: "create_before_destroy",
   482  		},
   483  		{
   484  			Name: "prevent_destroy",
   485  		},
   486  		{
   487  			Name: "ignore_changes",
   488  		},
   489  	},
   490  }