mirror of
https://github.com/opentofu/opentofu.git
synced 2025-02-25 18:45:20 -06:00
When a data resource is used for the purposes of verifying a condition about an object managed elsewhere (e.g. if the managed resource doesn't directly export all of the information required for the condition) it's important that we defer the data resource read to the apply step if the corresponding managed resource has any changes pending. Typically we'd expect that to come "for free" but unfortunately we have a pragmatic special case in our handling of data resources where we normally defer to the apply step only if a _direct_ dependency of the data resource has a change pending, and allow a plan-time read if there's a pending change in an indirect dependency. This allowed us to preserve some compatibility with the questionable historical behavior of always reading data resources proactively unless the configuration contains unknown values, since the arguably-more-correct behavior would've been a regression for anyone who had been depending on that before. Since preconditions and postconditions didn't exist until now, we are not constrained in the same way by backward compatibility, and so we can adopt the more correct behavior in the case where a data resource has conditions specified. This does unfortunately make the handling of data resources with conditions subtly inconsistent with those that don't, but this is a better situation than the alternative where it would be easy to get into a trapped situation where the remote system is invalid and it's impossible to plan the change that would make it valid again because the conditions evaluate too soon, prior to the fix being applied.
778 lines
24 KiB
Go
778 lines
24 KiB
Go
package configs
|
|
|
|
import (
|
|
"fmt"
|
|
|
|
"github.com/hashicorp/hcl/v2"
|
|
"github.com/hashicorp/hcl/v2/gohcl"
|
|
"github.com/hashicorp/hcl/v2/hclsyntax"
|
|
hcljson "github.com/hashicorp/hcl/v2/json"
|
|
|
|
"github.com/hashicorp/terraform/internal/addrs"
|
|
"github.com/hashicorp/terraform/internal/lang"
|
|
"github.com/hashicorp/terraform/internal/tfdiags"
|
|
)
|
|
|
|
// Resource represents a "resource" or "data" block in a module or file.
|
|
type Resource struct {
|
|
Mode addrs.ResourceMode
|
|
Name string
|
|
Type string
|
|
Config hcl.Body
|
|
Count hcl.Expression
|
|
ForEach hcl.Expression
|
|
|
|
ProviderConfigRef *ProviderConfigRef
|
|
Provider addrs.Provider
|
|
|
|
Preconditions []*CheckRule
|
|
Postconditions []*CheckRule
|
|
|
|
DependsOn []hcl.Traversal
|
|
|
|
TriggersReplacement []hcl.Expression
|
|
|
|
// Managed is populated only for Mode = addrs.ManagedResourceMode,
|
|
// containing the additional fields that apply to managed resources.
|
|
// For all other resource modes, this field is nil.
|
|
Managed *ManagedResource
|
|
|
|
DeclRange hcl.Range
|
|
TypeRange hcl.Range
|
|
}
|
|
|
|
// ManagedResource represents a "resource" block in a module or file.
|
|
type ManagedResource struct {
|
|
Connection *Connection
|
|
Provisioners []*Provisioner
|
|
|
|
CreateBeforeDestroy bool
|
|
PreventDestroy bool
|
|
IgnoreChanges []hcl.Traversal
|
|
IgnoreAllChanges bool
|
|
|
|
CreateBeforeDestroySet bool
|
|
PreventDestroySet bool
|
|
}
|
|
|
|
func (r *Resource) moduleUniqueKey() string {
|
|
return r.Addr().String()
|
|
}
|
|
|
|
// Addr returns a resource address for the receiver that is relative to the
|
|
// resource's containing module.
|
|
func (r *Resource) Addr() addrs.Resource {
|
|
return addrs.Resource{
|
|
Mode: r.Mode,
|
|
Type: r.Type,
|
|
Name: r.Name,
|
|
}
|
|
}
|
|
|
|
// ProviderConfigAddr returns the address for the provider configuration that
|
|
// should be used for this resource. This function returns a default provider
|
|
// config addr if an explicit "provider" argument was not provided.
|
|
func (r *Resource) ProviderConfigAddr() addrs.LocalProviderConfig {
|
|
if r.ProviderConfigRef == nil {
|
|
// If no specific "provider" argument is given, we want to look up the
|
|
// provider config where the local name matches the implied provider
|
|
// from the resource type. This may be different from the resource's
|
|
// provider type.
|
|
return addrs.LocalProviderConfig{
|
|
LocalName: r.Addr().ImpliedProvider(),
|
|
}
|
|
}
|
|
|
|
return addrs.LocalProviderConfig{
|
|
LocalName: r.ProviderConfigRef.Name,
|
|
Alias: r.ProviderConfigRef.Alias,
|
|
}
|
|
}
|
|
|
|
// HasCustomConditions returns true if and only if the resource has at least
|
|
// one author-specified custom condition.
|
|
func (r *Resource) HasCustomConditions() bool {
|
|
return len(r.Postconditions) != 0 || len(r.Preconditions) != 0
|
|
}
|
|
|
|
func decodeResourceBlock(block *hcl.Block, override bool) (*Resource, hcl.Diagnostics) {
|
|
var diags hcl.Diagnostics
|
|
r := &Resource{
|
|
Mode: addrs.ManagedResourceMode,
|
|
Type: block.Labels[0],
|
|
Name: block.Labels[1],
|
|
DeclRange: block.DefRange,
|
|
TypeRange: block.LabelRanges[0],
|
|
Managed: &ManagedResource{},
|
|
}
|
|
|
|
content, remain, moreDiags := block.Body.PartialContent(resourceBlockSchema)
|
|
diags = append(diags, moreDiags...)
|
|
r.Config = remain
|
|
|
|
if !hclsyntax.ValidIdentifier(r.Type) {
|
|
diags = append(diags, &hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: "Invalid resource type name",
|
|
Detail: badIdentifierDetail,
|
|
Subject: &block.LabelRanges[0],
|
|
})
|
|
}
|
|
if !hclsyntax.ValidIdentifier(r.Name) {
|
|
diags = append(diags, &hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: "Invalid resource name",
|
|
Detail: badIdentifierDetail,
|
|
Subject: &block.LabelRanges[1],
|
|
})
|
|
}
|
|
|
|
if attr, exists := content.Attributes["count"]; exists {
|
|
r.Count = attr.Expr
|
|
}
|
|
|
|
if attr, exists := content.Attributes["for_each"]; exists {
|
|
r.ForEach = attr.Expr
|
|
// Cannot have count and for_each on the same resource block
|
|
if r.Count != nil {
|
|
diags = append(diags, &hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: `Invalid combination of "count" and "for_each"`,
|
|
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.`,
|
|
Subject: &attr.NameRange,
|
|
})
|
|
}
|
|
}
|
|
|
|
if attr, exists := content.Attributes["provider"]; exists {
|
|
var providerDiags hcl.Diagnostics
|
|
r.ProviderConfigRef, providerDiags = decodeProviderConfigRef(attr.Expr, "provider")
|
|
diags = append(diags, providerDiags...)
|
|
}
|
|
|
|
if attr, exists := content.Attributes["depends_on"]; exists {
|
|
deps, depsDiags := decodeDependsOn(attr)
|
|
diags = append(diags, depsDiags...)
|
|
r.DependsOn = append(r.DependsOn, deps...)
|
|
}
|
|
|
|
var seenLifecycle *hcl.Block
|
|
var seenConnection *hcl.Block
|
|
var seenEscapeBlock *hcl.Block
|
|
for _, block := range content.Blocks {
|
|
switch block.Type {
|
|
case "lifecycle":
|
|
if seenLifecycle != nil {
|
|
diags = append(diags, &hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: "Duplicate lifecycle block",
|
|
Detail: fmt.Sprintf("This resource already has a lifecycle block at %s.", seenLifecycle.DefRange),
|
|
Subject: &block.DefRange,
|
|
})
|
|
continue
|
|
}
|
|
seenLifecycle = block
|
|
|
|
lcContent, lcDiags := block.Body.Content(resourceLifecycleBlockSchema)
|
|
diags = append(diags, lcDiags...)
|
|
|
|
if attr, exists := lcContent.Attributes["create_before_destroy"]; exists {
|
|
valDiags := gohcl.DecodeExpression(attr.Expr, nil, &r.Managed.CreateBeforeDestroy)
|
|
diags = append(diags, valDiags...)
|
|
r.Managed.CreateBeforeDestroySet = true
|
|
}
|
|
|
|
if attr, exists := lcContent.Attributes["prevent_destroy"]; exists {
|
|
valDiags := gohcl.DecodeExpression(attr.Expr, nil, &r.Managed.PreventDestroy)
|
|
diags = append(diags, valDiags...)
|
|
r.Managed.PreventDestroySet = true
|
|
}
|
|
|
|
if attr, exists := lcContent.Attributes["replace_triggered_by"]; exists {
|
|
exprs, hclDiags := decodeReplaceTriggeredBy(attr.Expr)
|
|
diags = diags.Extend(hclDiags)
|
|
|
|
r.TriggersReplacement = append(r.TriggersReplacement, exprs...)
|
|
}
|
|
|
|
if attr, exists := lcContent.Attributes["ignore_changes"]; exists {
|
|
|
|
// ignore_changes can either be a list of relative traversals
|
|
// or it can be just the keyword "all" to ignore changes to this
|
|
// resource entirely.
|
|
// ignore_changes = [ami, instance_type]
|
|
// ignore_changes = all
|
|
// We also allow two legacy forms for compatibility with earlier
|
|
// versions:
|
|
// ignore_changes = ["ami", "instance_type"]
|
|
// ignore_changes = ["*"]
|
|
|
|
kw := hcl.ExprAsKeyword(attr.Expr)
|
|
|
|
switch {
|
|
case kw == "all":
|
|
r.Managed.IgnoreAllChanges = true
|
|
default:
|
|
exprs, listDiags := hcl.ExprList(attr.Expr)
|
|
diags = append(diags, listDiags...)
|
|
|
|
var ignoreAllRange hcl.Range
|
|
|
|
for _, expr := range exprs {
|
|
|
|
// our expr might be the literal string "*", which
|
|
// we accept as a deprecated way of saying "all".
|
|
if shimIsIgnoreChangesStar(expr) {
|
|
r.Managed.IgnoreAllChanges = true
|
|
ignoreAllRange = expr.Range()
|
|
diags = append(diags, &hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: "Invalid ignore_changes wildcard",
|
|
Detail: "The [\"*\"] form of ignore_changes wildcard is was deprecated and is now invalid. Use \"ignore_changes = all\" to ignore changes to all attributes.",
|
|
Subject: attr.Expr.Range().Ptr(),
|
|
})
|
|
continue
|
|
}
|
|
|
|
expr, shimDiags := shimTraversalInString(expr, false)
|
|
diags = append(diags, shimDiags...)
|
|
|
|
traversal, travDiags := hcl.RelTraversalForExpr(expr)
|
|
diags = append(diags, travDiags...)
|
|
if len(traversal) != 0 {
|
|
r.Managed.IgnoreChanges = append(r.Managed.IgnoreChanges, traversal)
|
|
}
|
|
}
|
|
|
|
if r.Managed.IgnoreAllChanges && len(r.Managed.IgnoreChanges) != 0 {
|
|
diags = append(diags, &hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: "Invalid ignore_changes ruleset",
|
|
Detail: "Cannot mix wildcard string \"*\" with non-wildcard references.",
|
|
Subject: &ignoreAllRange,
|
|
Context: attr.Expr.Range().Ptr(),
|
|
})
|
|
}
|
|
|
|
}
|
|
}
|
|
|
|
for _, block := range lcContent.Blocks {
|
|
switch block.Type {
|
|
case "precondition", "postcondition":
|
|
cr, moreDiags := decodeCheckRuleBlock(block, override)
|
|
diags = append(diags, moreDiags...)
|
|
|
|
moreDiags = cr.validateSelfReferences(block.Type, r.Addr())
|
|
diags = append(diags, moreDiags...)
|
|
|
|
switch block.Type {
|
|
case "precondition":
|
|
r.Preconditions = append(r.Preconditions, cr)
|
|
case "postcondition":
|
|
r.Postconditions = append(r.Postconditions, cr)
|
|
}
|
|
default:
|
|
// The cases above should be exhaustive for all block types
|
|
// defined in the lifecycle schema, so this shouldn't happen.
|
|
panic(fmt.Sprintf("unexpected lifecycle sub-block type %q", block.Type))
|
|
}
|
|
}
|
|
|
|
case "connection":
|
|
if seenConnection != nil {
|
|
diags = append(diags, &hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: "Duplicate connection block",
|
|
Detail: fmt.Sprintf("This resource already has a connection block at %s.", seenConnection.DefRange),
|
|
Subject: &block.DefRange,
|
|
})
|
|
continue
|
|
}
|
|
seenConnection = block
|
|
|
|
r.Managed.Connection = &Connection{
|
|
Config: block.Body,
|
|
DeclRange: block.DefRange,
|
|
}
|
|
|
|
case "provisioner":
|
|
pv, pvDiags := decodeProvisionerBlock(block)
|
|
diags = append(diags, pvDiags...)
|
|
if pv != nil {
|
|
r.Managed.Provisioners = append(r.Managed.Provisioners, pv)
|
|
}
|
|
|
|
case "_":
|
|
if seenEscapeBlock != nil {
|
|
diags = append(diags, &hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: "Duplicate escaping block",
|
|
Detail: fmt.Sprintf(
|
|
"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.",
|
|
seenEscapeBlock.DefRange,
|
|
),
|
|
Subject: &block.DefRange,
|
|
})
|
|
continue
|
|
}
|
|
seenEscapeBlock = block
|
|
|
|
// When there's an escaping block its content merges with the
|
|
// existing config we extracted earlier, so later decoding
|
|
// will see a blend of both.
|
|
r.Config = hcl.MergeBodies([]hcl.Body{r.Config, block.Body})
|
|
|
|
default:
|
|
// Any other block types are ones we've reserved for future use,
|
|
// so they get a generic message.
|
|
diags = append(diags, &hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: "Reserved block type name in resource block",
|
|
Detail: fmt.Sprintf("The block type name %q is reserved for use by Terraform in a future version.", block.Type),
|
|
Subject: &block.TypeRange,
|
|
})
|
|
}
|
|
}
|
|
|
|
// Now we can validate the connection block references if there are any destroy provisioners.
|
|
// TODO: should we eliminate standalone connection blocks?
|
|
if r.Managed.Connection != nil {
|
|
for _, p := range r.Managed.Provisioners {
|
|
if p.When == ProvisionerWhenDestroy {
|
|
diags = append(diags, onlySelfRefs(r.Managed.Connection.Config)...)
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
return r, diags
|
|
}
|
|
|
|
func decodeDataBlock(block *hcl.Block, override bool) (*Resource, hcl.Diagnostics) {
|
|
var diags hcl.Diagnostics
|
|
r := &Resource{
|
|
Mode: addrs.DataResourceMode,
|
|
Type: block.Labels[0],
|
|
Name: block.Labels[1],
|
|
DeclRange: block.DefRange,
|
|
TypeRange: block.LabelRanges[0],
|
|
}
|
|
|
|
content, remain, moreDiags := block.Body.PartialContent(dataBlockSchema)
|
|
diags = append(diags, moreDiags...)
|
|
r.Config = remain
|
|
|
|
if !hclsyntax.ValidIdentifier(r.Type) {
|
|
diags = append(diags, &hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: "Invalid data source name",
|
|
Detail: badIdentifierDetail,
|
|
Subject: &block.LabelRanges[0],
|
|
})
|
|
}
|
|
if !hclsyntax.ValidIdentifier(r.Name) {
|
|
diags = append(diags, &hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: "Invalid data resource name",
|
|
Detail: badIdentifierDetail,
|
|
Subject: &block.LabelRanges[1],
|
|
})
|
|
}
|
|
|
|
if attr, exists := content.Attributes["count"]; exists {
|
|
r.Count = attr.Expr
|
|
}
|
|
|
|
if attr, exists := content.Attributes["for_each"]; exists {
|
|
r.ForEach = attr.Expr
|
|
// Cannot have count and for_each on the same data block
|
|
if r.Count != nil {
|
|
diags = append(diags, &hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: `Invalid combination of "count" and "for_each"`,
|
|
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.`,
|
|
Subject: &attr.NameRange,
|
|
})
|
|
}
|
|
}
|
|
|
|
if attr, exists := content.Attributes["provider"]; exists {
|
|
var providerDiags hcl.Diagnostics
|
|
r.ProviderConfigRef, providerDiags = decodeProviderConfigRef(attr.Expr, "provider")
|
|
diags = append(diags, providerDiags...)
|
|
}
|
|
|
|
if attr, exists := content.Attributes["depends_on"]; exists {
|
|
deps, depsDiags := decodeDependsOn(attr)
|
|
diags = append(diags, depsDiags...)
|
|
r.DependsOn = append(r.DependsOn, deps...)
|
|
}
|
|
|
|
var seenEscapeBlock *hcl.Block
|
|
var seenLifecycle *hcl.Block
|
|
for _, block := range content.Blocks {
|
|
switch block.Type {
|
|
|
|
case "_":
|
|
if seenEscapeBlock != nil {
|
|
diags = append(diags, &hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: "Duplicate escaping block",
|
|
Detail: fmt.Sprintf(
|
|
"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.",
|
|
seenEscapeBlock.DefRange,
|
|
),
|
|
Subject: &block.DefRange,
|
|
})
|
|
continue
|
|
}
|
|
seenEscapeBlock = block
|
|
|
|
// When there's an escaping block its content merges with the
|
|
// existing config we extracted earlier, so later decoding
|
|
// will see a blend of both.
|
|
r.Config = hcl.MergeBodies([]hcl.Body{r.Config, block.Body})
|
|
|
|
case "lifecycle":
|
|
if seenLifecycle != nil {
|
|
diags = append(diags, &hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: "Duplicate lifecycle block",
|
|
Detail: fmt.Sprintf("This resource already has a lifecycle block at %s.", seenLifecycle.DefRange),
|
|
Subject: block.DefRange.Ptr(),
|
|
})
|
|
continue
|
|
}
|
|
seenLifecycle = block
|
|
|
|
lcContent, lcDiags := block.Body.Content(resourceLifecycleBlockSchema)
|
|
diags = append(diags, lcDiags...)
|
|
|
|
// All of the attributes defined for resource lifecycle are for
|
|
// managed resources only, so we can emit a common error message
|
|
// for any given attributes that HCL accepted.
|
|
for name, attr := range lcContent.Attributes {
|
|
diags = append(diags, &hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: "Invalid data resource lifecycle argument",
|
|
Detail: fmt.Sprintf("The lifecycle argument %q is defined only for managed resources (\"resource\" blocks), and is not valid for data resources.", name),
|
|
Subject: attr.NameRange.Ptr(),
|
|
})
|
|
}
|
|
|
|
for _, block := range lcContent.Blocks {
|
|
switch block.Type {
|
|
case "precondition", "postcondition":
|
|
cr, moreDiags := decodeCheckRuleBlock(block, override)
|
|
diags = append(diags, moreDiags...)
|
|
|
|
moreDiags = cr.validateSelfReferences(block.Type, r.Addr())
|
|
diags = append(diags, moreDiags...)
|
|
|
|
switch block.Type {
|
|
case "precondition":
|
|
r.Preconditions = append(r.Preconditions, cr)
|
|
case "postcondition":
|
|
r.Postconditions = append(r.Postconditions, cr)
|
|
}
|
|
default:
|
|
// The cases above should be exhaustive for all block types
|
|
// defined in the lifecycle schema, so this shouldn't happen.
|
|
panic(fmt.Sprintf("unexpected lifecycle sub-block type %q", block.Type))
|
|
}
|
|
}
|
|
|
|
default:
|
|
// Any other block types are ones we're reserving for future use,
|
|
// but don't have any defined meaning today.
|
|
diags = append(diags, &hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: "Reserved block type name in data block",
|
|
Detail: fmt.Sprintf("The block type name %q is reserved for use by Terraform in a future version.", block.Type),
|
|
Subject: block.TypeRange.Ptr(),
|
|
})
|
|
}
|
|
}
|
|
|
|
return r, diags
|
|
}
|
|
|
|
// decodeReplaceTriggeredBy decodes and does basic validation of the
|
|
// replace_triggered_by expressions, ensuring they only contains references to
|
|
// a single resource, and the only extra variables are count.index or each.key.
|
|
func decodeReplaceTriggeredBy(expr hcl.Expression) ([]hcl.Expression, hcl.Diagnostics) {
|
|
// Since we are manually parsing the replace_triggered_by argument, we
|
|
// need to specially handle json configs, in which case the values will
|
|
// be json strings rather than hcl. To simplify parsing however we will
|
|
// decode the individual list elements, rather than the entire expression.
|
|
isJSON := hcljson.IsJSONExpression(expr)
|
|
|
|
exprs, diags := hcl.ExprList(expr)
|
|
|
|
for i, expr := range exprs {
|
|
if isJSON {
|
|
// We can abuse the hcl json api and rely on the fact that calling
|
|
// Value on a json expression with no EvalContext will return the
|
|
// raw string. We can then parse that as normal hcl syntax, and
|
|
// continue with the decoding.
|
|
v, ds := expr.Value(nil)
|
|
diags = diags.Extend(ds)
|
|
if diags.HasErrors() {
|
|
continue
|
|
}
|
|
|
|
expr, ds = hclsyntax.ParseExpression([]byte(v.AsString()), "", expr.Range().Start)
|
|
diags = diags.Extend(ds)
|
|
if diags.HasErrors() {
|
|
continue
|
|
}
|
|
// make sure to swap out the expression we're returning too
|
|
exprs[i] = expr
|
|
}
|
|
|
|
refs, refDiags := lang.ReferencesInExpr(expr)
|
|
for _, diag := range refDiags {
|
|
severity := hcl.DiagError
|
|
if diag.Severity() == tfdiags.Warning {
|
|
severity = hcl.DiagWarning
|
|
}
|
|
|
|
desc := diag.Description()
|
|
|
|
diags = append(diags, &hcl.Diagnostic{
|
|
Severity: severity,
|
|
Summary: desc.Summary,
|
|
Detail: desc.Detail,
|
|
Subject: expr.Range().Ptr(),
|
|
})
|
|
}
|
|
|
|
if refDiags.HasErrors() {
|
|
continue
|
|
}
|
|
|
|
resourceCount := 0
|
|
for _, ref := range refs {
|
|
switch sub := ref.Subject.(type) {
|
|
case addrs.Resource, addrs.ResourceInstance:
|
|
resourceCount++
|
|
|
|
case addrs.ForEachAttr:
|
|
if sub.Name != "key" {
|
|
diags = append(diags, &hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: "Invalid each reference in replace_triggered_by expression",
|
|
Detail: "Only each.key may be used in replace_triggered_by.",
|
|
Subject: expr.Range().Ptr(),
|
|
})
|
|
}
|
|
case addrs.CountAttr:
|
|
if sub.Name != "index" {
|
|
diags = append(diags, &hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: "Invalid count reference in replace_triggered_by expression",
|
|
Detail: "Only count.index may be used in replace_triggered_by.",
|
|
Subject: expr.Range().Ptr(),
|
|
})
|
|
}
|
|
default:
|
|
// everything else should be simple traversals
|
|
diags = append(diags, &hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: "Invalid reference in replace_triggered_by expression",
|
|
Detail: "Only resources, count.index, and each.key may be used in replace_triggered_by.",
|
|
Subject: expr.Range().Ptr(),
|
|
})
|
|
}
|
|
}
|
|
|
|
switch {
|
|
case resourceCount == 0:
|
|
diags = append(diags, &hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: "Invalid replace_triggered_by expression",
|
|
Detail: "Missing resource reference in replace_triggered_by expression.",
|
|
Subject: expr.Range().Ptr(),
|
|
})
|
|
case resourceCount > 1:
|
|
diags = append(diags, &hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: "Invalid replace_triggered_by expression",
|
|
Detail: "Multiple resource references in replace_triggered_by expression.",
|
|
Subject: expr.Range().Ptr(),
|
|
})
|
|
}
|
|
}
|
|
return exprs, diags
|
|
}
|
|
|
|
type ProviderConfigRef struct {
|
|
Name string
|
|
NameRange hcl.Range
|
|
Alias string
|
|
AliasRange *hcl.Range // nil if alias not set
|
|
|
|
// TODO: this may not be set in some cases, so it is not yet suitable for
|
|
// use outside of this package. We currently only use it for internal
|
|
// validation, but once we verify that this can be set in all cases, we can
|
|
// export this so providers don't need to be re-resolved.
|
|
// This same field is also added to the Provider struct.
|
|
providerType addrs.Provider
|
|
}
|
|
|
|
func decodeProviderConfigRef(expr hcl.Expression, argName string) (*ProviderConfigRef, hcl.Diagnostics) {
|
|
var diags hcl.Diagnostics
|
|
|
|
var shimDiags hcl.Diagnostics
|
|
expr, shimDiags = shimTraversalInString(expr, false)
|
|
diags = append(diags, shimDiags...)
|
|
|
|
traversal, travDiags := hcl.AbsTraversalForExpr(expr)
|
|
|
|
// AbsTraversalForExpr produces only generic errors, so we'll discard
|
|
// the errors given and produce our own with extra context. If we didn't
|
|
// get any errors then we might still have warnings, though.
|
|
if !travDiags.HasErrors() {
|
|
diags = append(diags, travDiags...)
|
|
}
|
|
|
|
if len(traversal) < 1 || len(traversal) > 2 {
|
|
// A provider reference was given as a string literal in the legacy
|
|
// configuration language and there are lots of examples out there
|
|
// showing that usage, so we'll sniff for that situation here and
|
|
// produce a specialized error message for it to help users find
|
|
// the new correct form.
|
|
if exprIsNativeQuotedString(expr) {
|
|
diags = append(diags, &hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: "Invalid provider configuration reference",
|
|
Detail: "A provider configuration reference must not be given in quotes.",
|
|
Subject: expr.Range().Ptr(),
|
|
})
|
|
return nil, diags
|
|
}
|
|
|
|
diags = append(diags, &hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: "Invalid provider configuration reference",
|
|
Detail: fmt.Sprintf("The %s argument requires a provider type name, optionally followed by a period and then a configuration alias.", argName),
|
|
Subject: expr.Range().Ptr(),
|
|
})
|
|
return nil, diags
|
|
}
|
|
|
|
// verify that the provider local name is normalized
|
|
name := traversal.RootName()
|
|
nameDiags := checkProviderNameNormalized(name, traversal[0].SourceRange())
|
|
diags = append(diags, nameDiags...)
|
|
if diags.HasErrors() {
|
|
return nil, diags
|
|
}
|
|
|
|
ret := &ProviderConfigRef{
|
|
Name: name,
|
|
NameRange: traversal[0].SourceRange(),
|
|
}
|
|
|
|
if len(traversal) > 1 {
|
|
aliasStep, ok := traversal[1].(hcl.TraverseAttr)
|
|
if !ok {
|
|
diags = append(diags, &hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: "Invalid provider configuration reference",
|
|
Detail: "Provider name must either stand alone or be followed by a period and then a configuration alias.",
|
|
Subject: traversal[1].SourceRange().Ptr(),
|
|
})
|
|
return ret, diags
|
|
}
|
|
|
|
ret.Alias = aliasStep.Name
|
|
ret.AliasRange = aliasStep.SourceRange().Ptr()
|
|
}
|
|
|
|
return ret, diags
|
|
}
|
|
|
|
// Addr returns the provider config address corresponding to the receiving
|
|
// config reference.
|
|
//
|
|
// This is a trivial conversion, essentially just discarding the source
|
|
// location information and keeping just the addressing information.
|
|
func (r *ProviderConfigRef) Addr() addrs.LocalProviderConfig {
|
|
return addrs.LocalProviderConfig{
|
|
LocalName: r.Name,
|
|
Alias: r.Alias,
|
|
}
|
|
}
|
|
|
|
func (r *ProviderConfigRef) String() string {
|
|
if r == nil {
|
|
return "<nil>"
|
|
}
|
|
if r.Alias != "" {
|
|
return fmt.Sprintf("%s.%s", r.Name, r.Alias)
|
|
}
|
|
return r.Name
|
|
}
|
|
|
|
var commonResourceAttributes = []hcl.AttributeSchema{
|
|
{
|
|
Name: "count",
|
|
},
|
|
{
|
|
Name: "for_each",
|
|
},
|
|
{
|
|
Name: "provider",
|
|
},
|
|
{
|
|
Name: "depends_on",
|
|
},
|
|
}
|
|
|
|
var resourceBlockSchema = &hcl.BodySchema{
|
|
Attributes: commonResourceAttributes,
|
|
Blocks: []hcl.BlockHeaderSchema{
|
|
{Type: "locals"}, // reserved for future use
|
|
{Type: "lifecycle"},
|
|
{Type: "connection"},
|
|
{Type: "provisioner", LabelNames: []string{"type"}},
|
|
{Type: "_"}, // meta-argument escaping block
|
|
},
|
|
}
|
|
|
|
var dataBlockSchema = &hcl.BodySchema{
|
|
Attributes: commonResourceAttributes,
|
|
Blocks: []hcl.BlockHeaderSchema{
|
|
{Type: "lifecycle"},
|
|
{Type: "locals"}, // reserved for future use
|
|
{Type: "_"}, // meta-argument escaping block
|
|
},
|
|
}
|
|
|
|
var resourceLifecycleBlockSchema = &hcl.BodySchema{
|
|
// We tell HCL that these elements are all valid for both "resource"
|
|
// and "data" lifecycle blocks, but the rules are actually more restrictive
|
|
// than that. We deal with that after decoding so that we can return
|
|
// more specific error messages than HCL would typically return itself.
|
|
Attributes: []hcl.AttributeSchema{
|
|
{
|
|
Name: "create_before_destroy",
|
|
},
|
|
{
|
|
Name: "prevent_destroy",
|
|
},
|
|
{
|
|
Name: "ignore_changes",
|
|
},
|
|
{
|
|
Name: "replace_triggered_by",
|
|
},
|
|
},
|
|
Blocks: []hcl.BlockHeaderSchema{
|
|
{Type: "precondition"},
|
|
{Type: "postcondition"},
|
|
},
|
|
}
|