mirror of
https://github.com/hashicorp/terraform.git
synced 2026-03-21 18:10:30 -04:00
221 lines
7.8 KiB
Go
221 lines
7.8 KiB
Go
// Copyright IBM Corp. 2014, 2026
|
|
// SPDX-License-Identifier: BUSL-1.1
|
|
|
|
package stackconfig
|
|
|
|
import (
|
|
"github.com/apparentlymart/go-versions/versions/constraints"
|
|
"github.com/hashicorp/go-slug/sourceaddrs"
|
|
"github.com/hashicorp/hcl/v2"
|
|
"github.com/hashicorp/hcl/v2/gohcl"
|
|
|
|
"github.com/hashicorp/terraform/internal/addrs"
|
|
"github.com/hashicorp/terraform/internal/stacks/stackaddrs"
|
|
"github.com/hashicorp/terraform/internal/tfdiags"
|
|
)
|
|
|
|
// Removed represents a component that was removed from the configuration.
|
|
//
|
|
// Removed blocks don't have labels associated with them, instead they have
|
|
// a "from" attribute that points directly to the old component that was
|
|
// removed. Removed blocks can also point to component instances specifically,
|
|
// using an index expression. The "for_each" attribute also means that the
|
|
// "from" attribute can't always be evaluated statically.
|
|
//
|
|
// Removed blocks are, therefore, represented by the FromComponent and FromIndex
|
|
// fields, which together represent the address of the removed component. The
|
|
// FromComponent field is the address of the component itself, and the FromIndex
|
|
// field is the index expression that will be evaluated to determine the
|
|
// specific instance of the component that was removed.
|
|
//
|
|
// FromIndex can be null if either the removed block is pointing to a component
|
|
// that was not instanced, or is pointing to all the instances of a removed
|
|
// component.
|
|
//
|
|
// For this reason, multiple Removed blocks can be associated with the same
|
|
// FromComponent, but with different FromIndex values. When the FromIndex values
|
|
// are evaluated, during the planning stage, we will validate that the FromIndex
|
|
// values are unique.
|
|
type Removed struct {
|
|
From stackaddrs.RemovedFrom
|
|
|
|
SourceAddr sourceaddrs.Source
|
|
VersionConstraints constraints.IntersectionSpec
|
|
SourceAddrRange, VersionConstraintsRange tfdiags.SourceRange
|
|
|
|
// FinalSourceAddr is populated only when a configuration is loaded
|
|
// through [LoadConfigDir], and in that case contains the finalized
|
|
// address produced by resolving the SourceAddr field relative to
|
|
// the address of the file where the component was declared. This
|
|
// is the address to use if you intend to load the component's
|
|
// root module from a source bundle.
|
|
FinalSourceAddr sourceaddrs.FinalSource
|
|
|
|
ForEach hcl.Expression
|
|
|
|
// ProviderConfigs describes the mapping between the static provider
|
|
// configuration slots declared in the component's root module and the
|
|
// dynamic provider configuration objects in scope in the calling
|
|
// stack configuration.
|
|
//
|
|
// This map deals with the slight schism between the stacks language's
|
|
// treatment of provider configurations as regular values of a special
|
|
// data type vs. the main Terraform language's treatment of provider
|
|
// configurations as something special passed out of band from the
|
|
// input variables. The overall structure and the map keys are fixed
|
|
// statically during decoding, but the final provider configuration objects
|
|
// are determined only at runtime by normal expression evaluation.
|
|
//
|
|
// The keys of this map refer to provider configuration slots inside
|
|
// the module being called, but use the local names defined in the
|
|
// calling stack configuration. The stacks language runtime will
|
|
// translate the caller's local names into the callee's declared provider
|
|
// configurations by using the stack configuration's table of local
|
|
// provider names.
|
|
//
|
|
// This will only be populated if From points to a component.
|
|
ProviderConfigs map[addrs.LocalProviderConfig]hcl.Expression
|
|
|
|
// Inputs describes the inputs that will be used to destroy all components
|
|
// within the target stack.
|
|
//
|
|
// This will only be populated if From points to a stack.
|
|
Inputs hcl.Expression
|
|
|
|
// Destroy controls whether this removed block will actually destroy all
|
|
// instances of resources within this component, or just removed them from
|
|
// the state. Defaults to true.
|
|
Destroy bool
|
|
|
|
DeclRange tfdiags.SourceRange
|
|
}
|
|
|
|
func decodeRemovedBlock(block *hcl.Block) (*Removed, tfdiags.Diagnostics) {
|
|
var diags tfdiags.Diagnostics
|
|
ret := &Removed{
|
|
DeclRange: tfdiags.SourceRangeFromHCL(block.DefRange),
|
|
}
|
|
|
|
content, hclDiags := block.Body.Content(removedBlockSchema)
|
|
diags = diags.Append(hclDiags)
|
|
if hclDiags.HasErrors() {
|
|
return nil, diags
|
|
}
|
|
|
|
// We're splitting out the component and the index now, as we can decode and
|
|
// analyse the component now. The index might be referencing the for_each
|
|
// variable, which we can't decode yet.
|
|
from, moreDiags := stackaddrs.ParseRemovedFrom(content.Attributes["from"].Expr)
|
|
diags = diags.Append(moreDiags)
|
|
if moreDiags.HasErrors() {
|
|
return nil, diags
|
|
}
|
|
ret.From = from
|
|
|
|
sourceAddr, versionConstraints, moreDiags := decodeSourceAddrArguments(
|
|
content.Attributes["source"],
|
|
content.Attributes["version"],
|
|
)
|
|
diags = diags.Append(moreDiags)
|
|
if moreDiags.HasErrors() {
|
|
return nil, diags
|
|
}
|
|
|
|
ret.SourceAddr = sourceAddr
|
|
ret.VersionConstraints = versionConstraints
|
|
ret.SourceAddrRange = tfdiags.SourceRangeFromHCL(content.Attributes["source"].Range)
|
|
if content.Attributes["version"] != nil {
|
|
ret.VersionConstraintsRange = tfdiags.SourceRangeFromHCL(content.Attributes["version"].Range)
|
|
}
|
|
// Now that we've populated the mandatory source location fields we can
|
|
// safely return a partial ret if we encounter any further errors, as
|
|
// long as we leave the other fields either unset or in some other
|
|
// reasonable state for careful partial analysis.
|
|
|
|
if attr, ok := content.Attributes["for_each"]; ok {
|
|
matches := false
|
|
for _, variable := range ret.From.Variables() {
|
|
if root, ok := variable[0].(hcl.TraverseRoot); ok {
|
|
if root.Name == "each" {
|
|
matches = true
|
|
break
|
|
}
|
|
}
|
|
}
|
|
if !matches {
|
|
// You have to refer to the for_each attribute somewhere in the
|
|
// from attribute.
|
|
diags = diags.Append(&hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: "Invalid for_each expression",
|
|
Detail: "A removed block with a for_each expression must reference that expression within the `from` attribute.",
|
|
Subject: attr.NameRange.Ptr(),
|
|
})
|
|
}
|
|
|
|
ret.ForEach = attr.Expr
|
|
}
|
|
if attr, ok := content.Attributes["providers"]; ok {
|
|
if ret.From.Component == nil {
|
|
diags = diags.Append(&hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: "Invalid providers attribute",
|
|
Detail: "A removed block that does not target a component should not specify any providers.",
|
|
Subject: attr.NameRange.Ptr(),
|
|
})
|
|
}
|
|
|
|
var providerDiags tfdiags.Diagnostics
|
|
ret.ProviderConfigs, providerDiags = decodeProvidersAttribute(attr)
|
|
diags = diags.Append(providerDiags)
|
|
}
|
|
|
|
if attr, ok := content.Attributes["inputs"]; ok {
|
|
if ret.From.Component != nil {
|
|
diags = diags.Append(&hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: "Invalid inputs attribute",
|
|
Detail: "A removed block that does not target an embedded stack should not specify any inputs.",
|
|
Subject: attr.NameRange.Ptr(),
|
|
})
|
|
}
|
|
|
|
ret.Inputs = attr.Expr
|
|
}
|
|
|
|
ret.Destroy = true // default to true
|
|
for _, block := range content.Blocks {
|
|
switch block.Type {
|
|
case "lifecycle":
|
|
lcContent, lcDiags := block.Body.Content(removedLifecycleBlockSchema)
|
|
diags = diags.Append(lcDiags)
|
|
|
|
if attr, ok := lcContent.Attributes["destroy"]; ok {
|
|
valDiags := gohcl.DecodeExpression(attr.Expr, nil, &ret.Destroy)
|
|
diags = diags.Append(valDiags)
|
|
}
|
|
}
|
|
}
|
|
|
|
return ret, diags
|
|
}
|
|
|
|
var removedBlockSchema = &hcl.BodySchema{
|
|
Blocks: []hcl.BlockHeaderSchema{
|
|
{Type: "lifecycle"},
|
|
},
|
|
Attributes: []hcl.AttributeSchema{
|
|
{Name: "from", Required: true},
|
|
{Name: "source", Required: true},
|
|
{Name: "version", Required: false},
|
|
{Name: "for_each", Required: false},
|
|
{Name: "providers", Required: false},
|
|
{Name: "inputs", Required: false},
|
|
},
|
|
}
|
|
|
|
var removedLifecycleBlockSchema = &hcl.BodySchema{
|
|
Attributes: []hcl.AttributeSchema{
|
|
{Name: "destroy"},
|
|
},
|
|
}
|