blob: 79d1f445a3105dc022d38509bb49ce865fa976ba [file] [log] [blame] [edit]
// Copyright (c) HashiCorp, Inc.
// 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 {
FromComponent stackaddrs.Component
FromIndex hcl.Expression
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.
ProviderConfigs map[addrs.LocalProviderConfig]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.
component, index, moreDiags := stackaddrs.ParseRemovedFrom(content.Attributes["from"].Expr)
diags = diags.Append(moreDiags)
if moreDiags.HasErrors() {
return nil, diags
}
ret.FromComponent = component
ret.FromIndex = index
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 {
if ret.FromIndex == nil {
// if we have a for_each expression, then we must have an index
// otherwise we'll try and remove the same thing multiple times.
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(),
})
} else {
matches := false
for _, variable := range ret.FromIndex.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 {
var providerDiags tfdiags.Diagnostics
ret.ProviderConfigs, providerDiags = decodeProvidersAttribute(attr)
diags = diags.Append(providerDiags)
}
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},
},
}
var removedLifecycleBlockSchema = &hcl.BodySchema{
Attributes: []hcl.AttributeSchema{
{Name: "destroy"},
},
}