blob: 94f2e3f392e71b362227fae720512ced988d51bc [file] [log] [blame] [edit]
// 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.Descendant(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.Descendant(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
}