// Copyright (c) HashiCorp, Inc. // SPDX-License-Identifier: BUSL-1.1 package refactoring import ( "fmt" "github.com/hashicorp/hcl/v2" "github.com/hashicorp/terraform/internal/addrs" "github.com/hashicorp/terraform/internal/configs" "github.com/hashicorp/terraform/internal/tfdiags" ) // RemoveStatement is the fully-specified form of addrs.Remove type RemoveStatement struct { // From is the absolute address of the configuration object being removed. From addrs.ConfigMoveable // Destroy indicates that the resource should be destroyed, not just removed // from state. Destroy bool DeclRange tfdiags.SourceRange } // FindRemoveStatements recurses through the modules of the given configuration // and returns a set of all "removed" blocks defined within after deduplication // on the From address. // // Error diagnostics are returned if any resource or module targeted by a remove // block is still defined in configuration. // // A "removed" block in a parent module overrides a removed block in a child // module when both target the same configuration object. func FindRemoveStatements(rootCfg *configs.Config) (addrs.Map[addrs.ConfigMoveable, RemoveStatement], tfdiags.Diagnostics) { stmts := findRemoveStatements(rootCfg, addrs.MakeMap[addrs.ConfigMoveable, RemoveStatement]()) diags := validateRemoveStatements(rootCfg, stmts) return stmts, diags } func validateRemoveStatements(cfg *configs.Config, stmts addrs.Map[addrs.ConfigMoveable, RemoveStatement]) (diags tfdiags.Diagnostics) { for _, rst := range stmts.Keys() { switch rst := rst.(type) { case addrs.ConfigResource: m := cfg.Descendent(rst.Module) if m == nil { break } if r := m.Module.ResourceByAddr(rst.Resource); r != nil { diags = diags.Append(&hcl.Diagnostic{ Severity: hcl.DiagError, Summary: "Removed resource still exists", Detail: fmt.Sprintf("This statement declares that %s was removed, but it is still declared in configuration.", rst), Subject: r.DeclRange.Ptr(), }) } case addrs.Module: if m := cfg.Descendent(rst); m != nil { diags = diags.Append(&hcl.Diagnostic{ Severity: hcl.DiagError, Summary: "Removed module still exists", Detail: fmt.Sprintf("This statement declares that %s was removed, but it is still declared in configuration.", rst), Subject: m.CallRange.Ptr(), }) } } } return diags } func findRemoveStatements(cfg *configs.Config, into addrs.Map[addrs.ConfigMoveable, RemoveStatement]) addrs.Map[addrs.ConfigMoveable, RemoveStatement] { for _, mc := range cfg.Module.Removed { switch mc.From.ObjectKind() { case addrs.RemoveTargetResource: // First, stitch together the module path and the RelSubject to form // the absolute address of the config object being removed. res := mc.From.RelSubject.(addrs.ConfigResource) fromAddr := addrs.ConfigResource{ Module: append(cfg.Path, res.Module...), Resource: res.Resource, } // If we already have a remove statement for this ConfigResource, it // must have come from a parent module, because duplicate removed // blocks in the same module are ignored during parsing. // The removed block in the parent module overrides the block in the // child module. existingStatement, ok := into.GetOk(fromAddr) if ok { if existingResource, ok := existingStatement.From.(addrs.ConfigResource); ok && existingResource.Equal(fromAddr) { continue } } into.Put(fromAddr, RemoveStatement{ From: fromAddr, Destroy: mc.Destroy, DeclRange: tfdiags.SourceRangeFromHCL(mc.DeclRange), }) case addrs.RemoveTargetModule: // First, stitch together the module path and the RelSubject to form // the absolute address of the config object being removed. mod := mc.From.RelSubject.(addrs.Module) absMod := append(cfg.Path, mod...) // If there is already a statement for this Module, it must // have come from a parent module, because duplicate removed blocks // in the same module are ignored during parsing. // The removed block in the parent module overrides the block in the // child module. existingStatement, ok := into.GetOk(mc.From.RelSubject) if ok { if existingModule, ok := existingStatement.From.(addrs.Module); ok && existingModule.Equal(absMod) { continue } } into.Put(absMod, RemoveStatement{ From: absMod, Destroy: mc.Destroy, DeclRange: tfdiags.SourceRangeFromHCL(mc.DeclRange), }) default: panic("Unsupported remove target kind") } } for _, childCfg := range cfg.Children { into = findRemoveStatements(childCfg, into) } return into }