// 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"}, }, }