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