Files
opentf/internal/refactoring/remove_statement.go
2025-04-25 06:29:42 -04:00

149 lines
5.6 KiB
Go

// Copyright (c) The OpenTofu Authors
// SPDX-License-Identifier: MPL-2.0
// Copyright (c) 2023 HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package refactoring
import (
"fmt"
"github.com/hashicorp/hcl/v2"
"github.com/opentofu/opentofu/internal/addrs"
"github.com/opentofu/opentofu/internal/configs"
"github.com/opentofu/opentofu/internal/tfdiags"
)
type RemoveStatement struct {
From addrs.ConfigRemovable
Destroy bool
DeclRange tfdiags.SourceRange
// Provisioners here are used only to be able to return these to the transformer that is injecting these
// in the nodes that are supporting this kind of operation. To get a better understanding of the usage of this field,
// check ResourceRemovedProvisioners
Provisioners []*configs.Provisioner
}
// FindRemoveStatements recurses through the modules of the given configuration
// and returns an array of all "removed" addresses within, in a
// deterministic but undefined order.
// We also validate that the removed modules/resources configuration blocks were removed.
func FindRemoveStatements(rootCfg *configs.Config) ([]*RemoveStatement, tfdiags.Diagnostics) {
rm := findRemoveStatements(rootCfg, nil)
diags := validateRemoveStatements(rootCfg, rm)
return rm, diags
}
// FindResourceRemovedStatement returns the RemoveStatement if found for the given resAddr.
// This function is searching in the Config for any "removed" block targeting the given resource.
// This method shouldn't be concerned if resAddr is pointing to a "module" or a "data" block because all of
// these will be validated way before this function is going to be called.
func FindResourceRemovedStatement(rootCfg *configs.Config, resAddr addrs.ConfigResource) *RemoveStatement {
rm := findRemoveStatements(rootCfg, nil)
// no need to call validateRemoveStatements again since these should have been validated in the plan phase
for _, rs := range rm {
if rs.From.TargetContains(resAddr) {
return rs
}
}
return nil
}
// FindResourceRemovedBlockProvisioners is returning the provisioners of the RemoveStatement found by calling FindResourceRemovedStatement
func FindResourceRemovedBlockProvisioners(rootCfg *configs.Config, resAddr addrs.ConfigResource) []*configs.Provisioner {
if rs := FindResourceRemovedStatement(rootCfg, resAddr); rs != nil {
return rs.Provisioners
}
return nil
}
func findRemoveStatements(cfg *configs.Config, into []*RemoveStatement) []*RemoveStatement {
modAddr := cfg.Path
for _, rc := range cfg.Module.Removed {
var removedEndpoint *RemoveStatement
switch FromAddress := rc.From.RelSubject.(type) {
case addrs.ConfigResource:
// Get the absolute address of the resource by appending the module config address
// to the resource's relative address
absModule := make(addrs.Module, 0, len(modAddr)+len(FromAddress.Module))
absModule = append(absModule, modAddr...)
absModule = append(absModule, FromAddress.Module...)
var absConfigResource addrs.ConfigRemovable = addrs.ConfigResource{
Resource: FromAddress.Resource,
Module: absModule,
}
removedEndpoint = &RemoveStatement{From: absConfigResource, Destroy: rc.Destroy, DeclRange: tfdiags.SourceRangeFromHCL(rc.DeclRange), Provisioners: rc.Provisioners}
case addrs.Module:
// Get the absolute address of the module by appending the module config address
// to the module itself
var absModule = make(addrs.Module, 0, len(modAddr)+len(FromAddress))
absModule = append(absModule, modAddr...)
absModule = append(absModule, FromAddress...)
removedEndpoint = &RemoveStatement{From: absModule, Destroy: rc.Destroy, DeclRange: tfdiags.SourceRangeFromHCL(rc.DeclRange), Provisioners: rc.Provisioners}
default:
panic(fmt.Sprintf("unhandled address type %T", FromAddress))
}
into = append(into, removedEndpoint)
}
for _, childCfg := range cfg.Children {
into = findRemoveStatements(childCfg, into)
}
return into
}
// validateRemoveStatements validates that the removed modules/resources configuration blocks were removed.
func validateRemoveStatements(cfg *configs.Config, removeStatements []*RemoveStatement) tfdiags.Diagnostics {
var diags tfdiags.Diagnostics
for _, rs := range removeStatements {
fromAddr := rs.From
if fromAddr == nil {
// Invalid value should've been caught during original
// configuration decoding, in the configs package.
panic(fmt.Sprintf("incompatible Remove endpoint in %s", rs.DeclRange.ToHCL()))
}
// validate that a resource/module with this address doesn't exist in the config
switch fromAddr := fromAddr.(type) {
case addrs.ConfigResource:
moduleConfig := cfg.Descendent(fromAddr.Module)
if moduleConfig != nil && moduleConfig.Module.ResourceByAddr(fromAddr.Resource) != nil {
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Removed resource block still exists",
Detail: fmt.Sprintf(
"This statement declares a removal of the resource %s, but this resource block still exists in the configuration. Please remove the resource block.",
fromAddr,
),
Subject: rs.DeclRange.ToHCL().Ptr(),
})
}
case addrs.Module:
if cfg.Descendent(fromAddr) != nil {
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Removed module block still exists",
Detail: fmt.Sprintf(
"This statement declares a removal of the module %s, but this module block still exists in the configuration. Please remove the module block.",
fromAddr,
),
Subject: rs.DeclRange.ToHCL().Ptr(),
})
}
default:
panic(fmt.Sprintf("incompatible Remove endpoint address type in %s", rs.DeclRange.ToHCL()))
}
}
return diags
}