mirror of
https://github.com/opentofu/opentofu.git
synced 2024-12-28 01:41:48 -06:00
06b31cd26f
Signed-off-by: Christian Mesh <christianmesh1@gmail.com> Co-authored-by: James Humphries <jamesh@spacelift.io>
820 lines
26 KiB
Go
820 lines
26 KiB
Go
// Copyright (c) HashiCorp, Inc.
|
|
// SPDX-License-Identifier: MPL-2.0
|
|
|
|
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/opentofu/opentofu/internal/addrs"
|
|
"github.com/opentofu/opentofu/internal/lang"
|
|
"github.com/opentofu/opentofu/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
|
|
|
|
// Container links a scoped resource back up to the resources that contains
|
|
// it. This field is referenced during static analysis to check whether any
|
|
// references are also made from within the same container.
|
|
//
|
|
// If this is nil, then this resource is essentially public.
|
|
Container Container
|
|
|
|
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 OpenTofu 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, nested 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 && !nested {
|
|
r.Count = attr.Expr
|
|
} else if exists && nested {
|
|
// We don't allow count attributes in nested data blocks.
|
|
diags = append(diags, &hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: `Invalid "count" attribute`,
|
|
Detail: `The "count" and "for_each" meta-arguments are not supported within nested data blocks.`,
|
|
Subject: &attr.NameRange,
|
|
})
|
|
}
|
|
|
|
if attr, exists := content.Attributes["for_each"]; exists && !nested {
|
|
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,
|
|
})
|
|
}
|
|
} else if exists && nested {
|
|
// We don't allow for_each attributes in nested data blocks.
|
|
diags = append(diags, &hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: `Invalid "for_each" attribute`,
|
|
Detail: `The "count" and "for_each" meta-arguments are not supported within nested data blocks.`,
|
|
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 nested {
|
|
// We don't allow lifecycle arguments in nested data blocks,
|
|
// the lifecycle is managed by the parent block.
|
|
diags = append(diags, &hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: "Invalid lifecycle block",
|
|
Detail: `Nested data blocks do not support "lifecycle" blocks as the lifecycle is managed by the containing block.`,
|
|
Subject: block.DefRange.Ptr(),
|
|
})
|
|
}
|
|
|
|
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 OpenTofu 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(addrs.ParseRef, 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",
|
|
},
|
|
}
|
|
|
|
// ResourceBlockSchema is the schema for a resource or data resource type within
|
|
// OpenTofu.
|
|
//
|
|
// This schema is public as it is required elsewhere in order to validate and
|
|
// use generated config.
|
|
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"},
|
|
},
|
|
}
|