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