mirror of
https://github.com/opentofu/opentofu.git
synced 2025-02-25 18:45:20 -06:00
Add support for removed block (#1158)
Signed-off-by: Ronny Orot <ronny.orot@gmail.com>
This commit is contained in:
parent
851391f2e6
commit
e9fe0f1118
@ -19,6 +19,7 @@ ENHANCEMENTS:
|
||||
* Allow test run blocks to reference previous run block's module outputs ([#1129](https://github.com/opentofu/opentofu/pull/1129))
|
||||
* Support the XDG Base Directory Specification ([#1200](https://github.com/opentofu/opentofu/pull/1200))
|
||||
* Allow referencing the output from a test run in the local variables block of another run (tofu test). ([#1254](https://github.com/opentofu/opentofu/pull/1254))
|
||||
* Add support for a `removed` block that allows users to remove resources or modules from the state without destroying them. ([#1158](https://github.com/opentofu/opentofu/pull/1158))
|
||||
|
||||
BUG FIXES:
|
||||
* `tofu test` resources cleanup at the end of tests changed to use simple reverse run block order. ([#1043](https://github.com/opentofu/opentofu/pull/1043))
|
||||
|
@ -7,6 +7,9 @@ package addrs
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/hashicorp/hcl/v2"
|
||||
"github.com/opentofu/opentofu/internal/tfdiags"
|
||||
)
|
||||
|
||||
// Module is an address for a module call within configuration. This is
|
||||
@ -170,3 +173,125 @@ func (m Module) Ancestors() []Module {
|
||||
func (m Module) configMoveableSigil() {
|
||||
// ModuleInstance is moveable
|
||||
}
|
||||
func (m Module) configRemovableSigil() {
|
||||
// Empty function so Module will fulfill the requirements of the removable interface
|
||||
}
|
||||
|
||||
// parseModulePrefix parses a module address from the given traversal,
|
||||
// returning the module address and the remaining traversal.
|
||||
// For example, if the input traversal is ["module","a","module","b",
|
||||
// "null_resource", example_resource"], the output module will be ["a", "b"]
|
||||
// and the output remaining traversal will be ["null_resource",
|
||||
// "example_resource"].
|
||||
// This function only supports module addresses without instance keys (as the
|
||||
// returned Module struct doesn't support instance keys) and will return an
|
||||
// error if it encounters one.
|
||||
func parseModulePrefix(traversal hcl.Traversal) (Module, hcl.Traversal, tfdiags.Diagnostics) {
|
||||
remain := traversal
|
||||
var module Module
|
||||
var diags tfdiags.Diagnostics
|
||||
|
||||
for len(remain) > 0 {
|
||||
moduleName, isModule, moduleNameDiags := getModuleName(remain)
|
||||
diags = diags.Append(moduleNameDiags)
|
||||
|
||||
if !isModule || diags.HasErrors() {
|
||||
break
|
||||
}
|
||||
|
||||
// Because this is a valid module address, we can safely assume that
|
||||
// the first two elements are "module" and the module name
|
||||
remain = remain[2:]
|
||||
|
||||
if len(remain) > 0 {
|
||||
// We don't allow module instances as part of the module address
|
||||
if _, ok := remain[0].(hcl.TraverseIndex); ok {
|
||||
diags = diags.Append(&hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: "Module instance address with keys is not allowed",
|
||||
Detail: "Module address cannot be a module instance (e.g. \"module.a[0]\"), it must be a module instead (e.g. \"module.a\").",
|
||||
Subject: remain[0].SourceRange().Ptr(),
|
||||
})
|
||||
|
||||
return module, remain, diags
|
||||
}
|
||||
}
|
||||
|
||||
module = append(module, moduleName)
|
||||
}
|
||||
|
||||
var retRemain hcl.Traversal
|
||||
if len(remain) > 0 {
|
||||
retRemain = make(hcl.Traversal, len(remain))
|
||||
copy(retRemain, remain)
|
||||
// The first element here might be either a TraverseRoot or a
|
||||
// TraverseAttr, depending on whether we had a module address on the
|
||||
// front. To make life easier for callers, we'll normalize to always
|
||||
// start with a TraverseRoot.
|
||||
if tt, ok := retRemain[0].(hcl.TraverseAttr); ok {
|
||||
retRemain[0] = hcl.TraverseRoot{
|
||||
Name: tt.Name,
|
||||
SrcRange: tt.SrcRange,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return module, retRemain, diags
|
||||
}
|
||||
|
||||
func getModuleName(remain hcl.Traversal) (moduleName string, isModule bool, diags tfdiags.Diagnostics) {
|
||||
if len(remain) == 0 {
|
||||
// If the address is empty, then we can't possibly have a module address
|
||||
return moduleName, false, diags
|
||||
}
|
||||
|
||||
var next string
|
||||
switch tt := remain[0].(type) {
|
||||
case hcl.TraverseRoot:
|
||||
next = tt.Name
|
||||
case hcl.TraverseAttr:
|
||||
next = tt.Name
|
||||
default:
|
||||
diags = diags.Append(&hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: "Invalid address operator",
|
||||
Detail: "Module address prefix must be followed by dot and then a name.",
|
||||
Subject: remain[0].SourceRange().Ptr(),
|
||||
})
|
||||
|
||||
return moduleName, false, diags
|
||||
}
|
||||
|
||||
if next != "module" {
|
||||
return moduleName, false, diags
|
||||
}
|
||||
|
||||
kwRange := remain[0].SourceRange()
|
||||
remain = remain[1:]
|
||||
// If we have the prefix "module" then we should be followed by a
|
||||
// module call name, as an attribute
|
||||
if len(remain) == 0 {
|
||||
diags = diags.Append(&hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: "Invalid address operator",
|
||||
Detail: "Prefix \"module.\" must be followed by a module name.",
|
||||
Subject: &kwRange,
|
||||
})
|
||||
|
||||
return moduleName, false, diags
|
||||
}
|
||||
|
||||
switch tt := remain[0].(type) {
|
||||
case hcl.TraverseAttr:
|
||||
moduleName = tt.Name
|
||||
default:
|
||||
diags = diags.Append(&hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: "Invalid address operator",
|
||||
Detail: "Prefix \"module.\" must be followed by a module name.",
|
||||
Subject: remain[0].SourceRange().Ptr(),
|
||||
})
|
||||
return moduleName, false, diags
|
||||
}
|
||||
return moduleName, true, diags
|
||||
}
|
||||
|
@ -82,66 +82,31 @@ func ParseModuleInstanceStr(str string) (ModuleInstance, tfdiags.Diagnostics) {
|
||||
return addr, diags
|
||||
}
|
||||
|
||||
// parseModuleInstancePrefix parses a module instance address from the given
|
||||
// traversal, returning the module instance address and the remaining
|
||||
// traversal.
|
||||
// This function supports module addresses with and without instance keys.
|
||||
func parseModuleInstancePrefix(traversal hcl.Traversal) (ModuleInstance, hcl.Traversal, tfdiags.Diagnostics) {
|
||||
remain := traversal
|
||||
var mi ModuleInstance
|
||||
var diags tfdiags.Diagnostics
|
||||
|
||||
LOOP:
|
||||
for len(remain) > 0 {
|
||||
var next string
|
||||
switch tt := remain[0].(type) {
|
||||
case hcl.TraverseRoot:
|
||||
next = tt.Name
|
||||
case hcl.TraverseAttr:
|
||||
next = tt.Name
|
||||
default:
|
||||
diags = diags.Append(&hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: "Invalid address operator",
|
||||
Detail: "Module address prefix must be followed by dot and then a name.",
|
||||
Subject: remain[0].SourceRange().Ptr(),
|
||||
})
|
||||
break LOOP
|
||||
}
|
||||
moduleName, isModule, moduleNameDiags := getModuleName(remain)
|
||||
diags = diags.Append(moduleNameDiags)
|
||||
|
||||
if next != "module" {
|
||||
if !isModule || diags.HasErrors() {
|
||||
break
|
||||
}
|
||||
|
||||
kwRange := remain[0].SourceRange()
|
||||
remain = remain[1:]
|
||||
// If we have the prefix "module" then we should be followed by an
|
||||
// module call name, as an attribute, and then optionally an index step
|
||||
// giving the instance key.
|
||||
if len(remain) == 0 {
|
||||
diags = diags.Append(&hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: "Invalid address operator",
|
||||
Detail: "Prefix \"module.\" must be followed by a module name.",
|
||||
Subject: &kwRange,
|
||||
})
|
||||
break
|
||||
}
|
||||
|
||||
var moduleName string
|
||||
switch tt := remain[0].(type) {
|
||||
case hcl.TraverseAttr:
|
||||
moduleName = tt.Name
|
||||
default:
|
||||
diags = diags.Append(&hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: "Invalid address operator",
|
||||
Detail: "Prefix \"module.\" must be followed by a module name.",
|
||||
Subject: remain[0].SourceRange().Ptr(),
|
||||
})
|
||||
break LOOP
|
||||
}
|
||||
remain = remain[1:]
|
||||
// Because this is a valid module address, we can safely assume that
|
||||
// the first two elements are "module" and the module name
|
||||
remain = remain[2:]
|
||||
step := ModuleInstanceStep{
|
||||
Name: moduleName,
|
||||
}
|
||||
|
||||
// Check for optional module instance key
|
||||
if len(remain) > 0 {
|
||||
if idx, ok := remain[0].(hcl.TraverseIndex); ok {
|
||||
remain = remain[1:]
|
||||
|
@ -80,54 +80,8 @@ func parseResourceInstanceUnderModule(moduleAddr ModuleInstance, remain hcl.Trav
|
||||
remain = remain[1:]
|
||||
}
|
||||
|
||||
if len(remain) < 2 {
|
||||
diags = diags.Append(&hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: "Invalid address",
|
||||
Detail: "Resource specification must include a resource type and name.",
|
||||
Subject: remain.SourceRange().Ptr(),
|
||||
})
|
||||
return AbsResourceInstance{}, diags
|
||||
}
|
||||
|
||||
var typeName, name string
|
||||
switch tt := remain[0].(type) {
|
||||
case hcl.TraverseRoot:
|
||||
typeName = tt.Name
|
||||
case hcl.TraverseAttr:
|
||||
typeName = tt.Name
|
||||
default:
|
||||
switch mode {
|
||||
case ManagedResourceMode:
|
||||
diags = diags.Append(&hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: "Invalid address",
|
||||
Detail: "A resource type name is required.",
|
||||
Subject: remain[0].SourceRange().Ptr(),
|
||||
})
|
||||
case DataResourceMode:
|
||||
diags = diags.Append(&hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: "Invalid address",
|
||||
Detail: "A data source name is required.",
|
||||
Subject: remain[0].SourceRange().Ptr(),
|
||||
})
|
||||
default:
|
||||
panic("unknown mode")
|
||||
}
|
||||
return AbsResourceInstance{}, diags
|
||||
}
|
||||
|
||||
switch tt := remain[1].(type) {
|
||||
case hcl.TraverseAttr:
|
||||
name = tt.Name
|
||||
default:
|
||||
diags = diags.Append(&hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: "Invalid address",
|
||||
Detail: "A resource name is required.",
|
||||
Subject: remain[1].SourceRange().Ptr(),
|
||||
})
|
||||
typeName, name, diags := parseResourceTypeAndName(remain, mode)
|
||||
if diags.HasErrors() {
|
||||
return AbsResourceInstance{}, diags
|
||||
}
|
||||
|
||||
@ -169,6 +123,111 @@ func parseResourceInstanceUnderModule(moduleAddr ModuleInstance, remain hcl.Trav
|
||||
}
|
||||
}
|
||||
|
||||
// parseResourceUnderModule is a helper function that parses a traversal, which
|
||||
// is an address (or a part of an address) that describes a resource (e.g.
|
||||
// ["null_resource," "boop"] or ["data", "null_data_source," "bip"]), under a
|
||||
// module. It returns the ConfigResource that represents the resource address.
|
||||
// It does not support addresses of resources with instance keys, and will
|
||||
// return an error if it encounters one (unlike
|
||||
// parseResourceInstanceUnderModule).
|
||||
// This function does not expect to encounter a module prefix in the traversal,
|
||||
// as it should be processed by parseModulePrefix first.
|
||||
func parseResourceUnderModule(moduleAddr Module, remain hcl.Traversal) (ConfigResource, tfdiags.Diagnostics) {
|
||||
var diags tfdiags.Diagnostics
|
||||
|
||||
mode := ManagedResourceMode
|
||||
if remain.RootName() == "data" {
|
||||
mode = DataResourceMode
|
||||
remain = remain[1:]
|
||||
}
|
||||
|
||||
typeName, name, diags := parseResourceTypeAndName(remain, mode)
|
||||
if diags.HasErrors() {
|
||||
return ConfigResource{}, diags
|
||||
}
|
||||
|
||||
remain = remain[2:]
|
||||
switch len(remain) {
|
||||
case 0:
|
||||
return moduleAddr.Resource(mode, typeName, name), diags
|
||||
case 1:
|
||||
diags = diags.Append(&hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: "Resource instance address with keys is not allowed",
|
||||
Detail: "Resource address cannot be a resource instance (e.g. \"null_resource.a[0]\"), it must be a resource instead (e.g. \"null_resource.a\").",
|
||||
Subject: remain[0].SourceRange().Ptr(),
|
||||
})
|
||||
return ConfigResource{}, diags
|
||||
default:
|
||||
diags = diags.Append(&hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: "Invalid address",
|
||||
Detail: "Unexpected extra operators after address.",
|
||||
Subject: remain[1].SourceRange().Ptr(),
|
||||
})
|
||||
return ConfigResource{}, diags
|
||||
}
|
||||
}
|
||||
|
||||
// parseResourceTypeAndName is a helper function that parses a traversal, which
|
||||
// is an address (or a part of an address) that describes a resource (e.g.
|
||||
// ["null_resource," "boop"]) and returns its type and name.
|
||||
// It is used in parseResourceUnderModule and parseResourceInstanceUnderModule,
|
||||
// and does not expect to encounter a module prefix in the traversal.
|
||||
func parseResourceTypeAndName(remain hcl.Traversal, mode ResourceMode) (typeName, name string, diags tfdiags.Diagnostics) {
|
||||
if len(remain) < 2 {
|
||||
diags = diags.Append(&hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: "Invalid address",
|
||||
Detail: "Resource specification must include a resource type and name.",
|
||||
Subject: remain.SourceRange().Ptr(),
|
||||
})
|
||||
return typeName, name, diags
|
||||
}
|
||||
|
||||
switch tt := remain[0].(type) {
|
||||
case hcl.TraverseRoot:
|
||||
typeName = tt.Name
|
||||
case hcl.TraverseAttr:
|
||||
typeName = tt.Name
|
||||
default:
|
||||
switch mode {
|
||||
case ManagedResourceMode:
|
||||
diags = diags.Append(&hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: "Invalid address",
|
||||
Detail: "A resource type name is required.",
|
||||
Subject: remain[0].SourceRange().Ptr(),
|
||||
})
|
||||
case DataResourceMode:
|
||||
diags = diags.Append(&hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: "Invalid address",
|
||||
Detail: "A data source name is required.",
|
||||
Subject: remain[0].SourceRange().Ptr(),
|
||||
})
|
||||
default:
|
||||
panic("unknown mode")
|
||||
}
|
||||
return typeName, name, diags
|
||||
}
|
||||
|
||||
switch tt := remain[1].(type) {
|
||||
case hcl.TraverseAttr:
|
||||
name = tt.Name
|
||||
default:
|
||||
diags = diags.Append(&hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: "Invalid address",
|
||||
Detail: "A resource name is required.",
|
||||
Subject: remain[1].SourceRange().Ptr(),
|
||||
})
|
||||
return typeName, name, diags
|
||||
}
|
||||
|
||||
return typeName, name, diags
|
||||
}
|
||||
|
||||
// ParseTargetStr is a helper wrapper around ParseTarget that takes a string
|
||||
// and parses it with the HCL native syntax traversal parser before
|
||||
// interpreting it.
|
||||
|
26
internal/addrs/removable.go
Normal file
26
internal/addrs/removable.go
Normal file
@ -0,0 +1,26 @@
|
||||
// Copyright (c) The OpenTofu Authors
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
// Copyright (c) 2023 HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package addrs
|
||||
|
||||
// ConfigRemovable is an interface implemented by address types that represents
|
||||
// the destination of a "removed" statement in configuration.
|
||||
//
|
||||
// Note that ConfigRemovable might represent:
|
||||
// 1. An absolute address relative to the root of the configuration.
|
||||
// 2. A direct representation of these in configuration where the author gives an
|
||||
// address relative to the current module where the address is defined.
|
||||
type ConfigRemovable interface {
|
||||
Targetable
|
||||
configRemovableSigil()
|
||||
|
||||
String() string
|
||||
}
|
||||
|
||||
// The following are all the possible ConfigRemovable address types:
|
||||
var (
|
||||
_ ConfigRemovable = ConfigResource{}
|
||||
_ ConfigRemovable = Module(nil)
|
||||
)
|
78
internal/addrs/remove_endpoint.go
Normal file
78
internal/addrs/remove_endpoint.go
Normal file
@ -0,0 +1,78 @@
|
||||
// Copyright (c) The OpenTofu Authors
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
// Copyright (c) 2023 HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package addrs
|
||||
|
||||
import (
|
||||
"github.com/hashicorp/hcl/v2"
|
||||
"github.com/opentofu/opentofu/internal/tfdiags"
|
||||
)
|
||||
|
||||
// RemoveEndpoint is to ConfigRemovable what Target is to Targetable:
|
||||
// a wrapping struct that captures the result of decoding an HCL
|
||||
// traversal representing a relative path from the current module to
|
||||
// a removable object. It is very similar to MoveEndpoint.
|
||||
//
|
||||
// Its purpose is to represent the "from" address in a "removed" block
|
||||
// in the configuration.
|
||||
//
|
||||
// To obtain a full address from a RemoveEndpoint we need to combine it
|
||||
// with any ancestor modules in the configuration
|
||||
type RemoveEndpoint struct {
|
||||
// SourceRange is the location of the physical endpoint address
|
||||
// in configuration, if this RemoveEndpoint was decoded from a
|
||||
// configuration expression.
|
||||
SourceRange tfdiags.SourceRange
|
||||
|
||||
// the representation of our relative address as a ConfigRemovable
|
||||
RelSubject ConfigRemovable
|
||||
}
|
||||
|
||||
// ParseRemoveEndpoint attempts to interpret the given traversal as a
|
||||
// "remove endpoint" address, which is a relative path from the module containing
|
||||
// the traversal to a removable object in either the same module or in some
|
||||
// child module.
|
||||
//
|
||||
// This deals only with the syntactic element of a remove endpoint expression
|
||||
// in configuration. Before the result will be useful you'll need to combine
|
||||
// it with the address of the module where it was declared in order to get
|
||||
// an absolute address relative to the root module.
|
||||
func ParseRemoveEndpoint(traversal hcl.Traversal) (*RemoveEndpoint, tfdiags.Diagnostics) {
|
||||
path, remain, diags := parseModulePrefix(traversal)
|
||||
if diags.HasErrors() {
|
||||
return nil, diags
|
||||
}
|
||||
|
||||
rng := tfdiags.SourceRangeFromHCL(traversal.SourceRange())
|
||||
|
||||
if len(remain) == 0 {
|
||||
return &RemoveEndpoint{
|
||||
RelSubject: path,
|
||||
SourceRange: rng,
|
||||
}, diags
|
||||
}
|
||||
|
||||
riAddr, moreDiags := parseResourceUnderModule(path, remain)
|
||||
diags = diags.Append(moreDiags)
|
||||
if diags.HasErrors() {
|
||||
return nil, diags
|
||||
}
|
||||
|
||||
if riAddr.Resource.Mode == DataResourceMode {
|
||||
diags = diags.Append(&hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: "Data source address is not allowed",
|
||||
Detail: "Data sources cannot be destroyed, and therefore, 'removed' blocks are not allowed to target them. To remove data sources from the state, you should remove the data source block from the configuration.",
|
||||
Subject: traversal.SourceRange().Ptr(),
|
||||
})
|
||||
|
||||
return nil, diags
|
||||
}
|
||||
|
||||
return &RemoveEndpoint{
|
||||
RelSubject: riAddr,
|
||||
SourceRange: rng,
|
||||
}, diags
|
||||
}
|
213
internal/addrs/remove_endpoint_test.go
Normal file
213
internal/addrs/remove_endpoint_test.go
Normal file
@ -0,0 +1,213 @@
|
||||
// Copyright (c) The OpenTofu Authors
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
// Copyright (c) 2023 HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package addrs
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/hashicorp/hcl/v2"
|
||||
"github.com/hashicorp/hcl/v2/hclsyntax"
|
||||
)
|
||||
|
||||
func TestParseRemoveEndpoint(t *testing.T) {
|
||||
tests := []struct {
|
||||
Input string
|
||||
WantRel ConfigRemovable
|
||||
WantErr string
|
||||
}{
|
||||
{
|
||||
`foo.bar`,
|
||||
ConfigResource{
|
||||
Module: RootModule,
|
||||
Resource: Resource{
|
||||
|
||||
Mode: ManagedResourceMode,
|
||||
Type: "foo",
|
||||
Name: "bar",
|
||||
},
|
||||
},
|
||||
``,
|
||||
},
|
||||
{
|
||||
`module.boop`,
|
||||
Module{"boop"},
|
||||
``,
|
||||
},
|
||||
{
|
||||
`module.boop.foo.bar`,
|
||||
ConfigResource{
|
||||
Module: Module{"boop"},
|
||||
Resource: Resource{
|
||||
Mode: ManagedResourceMode,
|
||||
Type: "foo",
|
||||
Name: "bar",
|
||||
},
|
||||
},
|
||||
``,
|
||||
},
|
||||
{
|
||||
`module.foo.module.bar`,
|
||||
Module{"foo", "bar"},
|
||||
``,
|
||||
},
|
||||
{
|
||||
`module.boop.module.bip.foo.bar`,
|
||||
ConfigResource{
|
||||
Module: Module{"boop", "bip"},
|
||||
Resource: Resource{
|
||||
Mode: ManagedResourceMode,
|
||||
Type: "foo",
|
||||
Name: "bar",
|
||||
},
|
||||
},
|
||||
``,
|
||||
},
|
||||
{
|
||||
`foo.bar[0]`,
|
||||
nil,
|
||||
`Resource instance address with keys is not allowed: Resource address cannot be a resource instance (e.g. "null_resource.a[0]"), it must be a resource instead (e.g. "null_resource.a").`,
|
||||
},
|
||||
{
|
||||
`foo.bar["a"]`,
|
||||
nil,
|
||||
`Resource instance address with keys is not allowed: Resource address cannot be a resource instance (e.g. "null_resource.a[0]"), it must be a resource instead (e.g. "null_resource.a").`,
|
||||
},
|
||||
{
|
||||
`module.boop.foo.bar[0]`,
|
||||
nil,
|
||||
`Resource instance address with keys is not allowed: Resource address cannot be a resource instance (e.g. "null_resource.a[0]"), it must be a resource instead (e.g. "null_resource.a").`,
|
||||
},
|
||||
{
|
||||
`module.boop.foo.bar["a"]`,
|
||||
nil,
|
||||
|
||||
`Resource instance address with keys is not allowed: Resource address cannot be a resource instance (e.g. "null_resource.a[0]"), it must be a resource instead (e.g. "null_resource.a").`,
|
||||
},
|
||||
{
|
||||
`data.foo.bar`,
|
||||
nil,
|
||||
`Data source address is not allowed: Data sources cannot be destroyed, and therefore, 'removed' blocks are not allowed to target them. To remove data sources from the state, you should remove the data source block from the configuration.`,
|
||||
},
|
||||
{
|
||||
`data.foo.bar[0]`,
|
||||
nil,
|
||||
`Resource instance address with keys is not allowed: Resource address cannot be a resource instance (e.g. "null_resource.a[0]"), it must be a resource instead (e.g. "null_resource.a").`,
|
||||
},
|
||||
{
|
||||
`data.foo.bar["a"]`,
|
||||
nil,
|
||||
`Resource instance address with keys is not allowed: Resource address cannot be a resource instance (e.g. "null_resource.a[0]"), it must be a resource instead (e.g. "null_resource.a").`,
|
||||
},
|
||||
{
|
||||
`module.boop.data.foo.bar[0]`,
|
||||
nil,
|
||||
`Resource instance address with keys is not allowed: Resource address cannot be a resource instance (e.g. "null_resource.a[0]"), it must be a resource instead (e.g. "null_resource.a").`,
|
||||
},
|
||||
{
|
||||
`module.boop.data.foo.bar["a"]`,
|
||||
nil,
|
||||
`Resource instance address with keys is not allowed: Resource address cannot be a resource instance (e.g. "null_resource.a[0]"), it must be a resource instead (e.g. "null_resource.a").`,
|
||||
},
|
||||
{
|
||||
`module.foo[0]`,
|
||||
nil,
|
||||
`Module instance address with keys is not allowed: Module address cannot be a module instance (e.g. "module.a[0]"), it must be a module instead (e.g. "module.a").`,
|
||||
},
|
||||
{
|
||||
`module.foo["a"]`,
|
||||
nil,
|
||||
`Module instance address with keys is not allowed: Module address cannot be a module instance (e.g. "module.a[0]"), it must be a module instead (e.g. "module.a").`,
|
||||
},
|
||||
{
|
||||
`module.foo[1].module.bar`,
|
||||
nil,
|
||||
`Module instance address with keys is not allowed: Module address cannot be a module instance (e.g. "module.a[0]"), it must be a module instead (e.g. "module.a").`,
|
||||
},
|
||||
{
|
||||
`module.foo.module.bar[1]`,
|
||||
nil,
|
||||
`Module instance address with keys is not allowed: Module address cannot be a module instance (e.g. "module.a[0]"), it must be a module instead (e.g. "module.a").`,
|
||||
},
|
||||
{
|
||||
`module.foo[0].module.bar[1]`,
|
||||
nil,
|
||||
`Module instance address with keys is not allowed: Module address cannot be a module instance (e.g. "module.a[0]"), it must be a module instead (e.g. "module.a").`,
|
||||
},
|
||||
{
|
||||
`module`,
|
||||
nil,
|
||||
`Invalid address operator: Prefix "module." must be followed by a module name.`,
|
||||
},
|
||||
{
|
||||
`module[0]`,
|
||||
nil,
|
||||
`Invalid address operator: Prefix "module." must be followed by a module name.`,
|
||||
},
|
||||
{
|
||||
`module.foo.data`,
|
||||
nil,
|
||||
`Invalid address: Resource specification must include a resource type and name.`,
|
||||
},
|
||||
{
|
||||
`module.foo.data.bar`,
|
||||
nil,
|
||||
`Invalid address: Resource specification must include a resource type and name.`,
|
||||
},
|
||||
{
|
||||
`module.foo.data[0]`,
|
||||
nil,
|
||||
`Invalid address: Resource specification must include a resource type and name.`,
|
||||
},
|
||||
{
|
||||
`module.foo.data.bar[0]`,
|
||||
nil,
|
||||
`Invalid address: A resource name is required.`,
|
||||
},
|
||||
{
|
||||
`module.foo.bar`,
|
||||
nil,
|
||||
`Invalid address: Resource specification must include a resource type and name.`,
|
||||
},
|
||||
{
|
||||
`module.foo.bar[0]`,
|
||||
nil,
|
||||
`Invalid address: A resource name is required.`,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.Input, func(t *testing.T) {
|
||||
traversal, hclDiags := hclsyntax.ParseTraversalAbs([]byte(test.Input), "", hcl.InitialPos)
|
||||
if hclDiags.HasErrors() {
|
||||
// We're not trying to test the HCL parser here, so any
|
||||
// failures at this point are likely to be bugs in the
|
||||
// test case itself.
|
||||
t.Fatalf("syntax error: %s", hclDiags.Error())
|
||||
}
|
||||
|
||||
moveEp, diags := ParseRemoveEndpoint(traversal)
|
||||
|
||||
switch {
|
||||
case test.WantErr != "":
|
||||
if !diags.HasErrors() {
|
||||
t.Fatalf("unexpected success\nwant error: %s", test.WantErr)
|
||||
}
|
||||
gotErr := diags.Err().Error()
|
||||
if gotErr != test.WantErr {
|
||||
t.Fatalf("wrong error\ngot: %s\nwant: %s", gotErr, test.WantErr)
|
||||
}
|
||||
default:
|
||||
if diags.HasErrors() {
|
||||
t.Fatalf("unexpected error: %s", diags.Err().Error())
|
||||
}
|
||||
if diff := cmp.Diff(test.WantRel, moveEp.RelSubject); diff != "" {
|
||||
t.Errorf("wrong result\n%s", diff)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
@ -459,6 +459,10 @@ func (v ConfigResource) CheckableKind() CheckableKind {
|
||||
return CheckableResource
|
||||
}
|
||||
|
||||
func (r ConfigResource) configRemovableSigil() {
|
||||
// Empty function so ConfigResource will fulfill the requirements of the removable interface
|
||||
}
|
||||
|
||||
type configResourceKey string
|
||||
|
||||
func (k configResourceKey) uniqueKeySigil() {}
|
||||
|
@ -34,6 +34,8 @@ func DiffActionSymbol(action plans.Action) string {
|
||||
return " [yellow]~[reset]"
|
||||
case plans.NoOp:
|
||||
return " "
|
||||
case plans.Forget:
|
||||
return " [red].[reset]"
|
||||
default:
|
||||
return " ?"
|
||||
}
|
||||
|
@ -203,6 +203,9 @@ func (plan Plan) renderHuman(renderer Renderer, mode plans.Mode, opts ...plans.Q
|
||||
if counts[plans.Read] > 0 {
|
||||
renderer.Streams.Println(renderer.Colorize.Color(actionDescription(plans.Read)))
|
||||
}
|
||||
if counts[plans.Forget] > 0 {
|
||||
renderer.Streams.Println(renderer.Colorize.Color(actionDescription(plans.Forget)))
|
||||
}
|
||||
}
|
||||
|
||||
if len(changes) > 0 {
|
||||
@ -354,6 +357,11 @@ func renderHumanDiff(renderer Renderer, diff diff, cause string) (string, bool)
|
||||
buf.WriteString(renderer.Colorize.Color(resourceChangeComment(diff.change, action, cause)))
|
||||
|
||||
opts := computed.NewRenderHumanOpts(renderer.Colorize)
|
||||
|
||||
if action == plans.Forget {
|
||||
opts.HideDiffActionSymbols = true
|
||||
opts.OverrideNullSuffix = true
|
||||
}
|
||||
opts.ShowUnchangedChildren = diff.Importing()
|
||||
|
||||
buf.WriteString(fmt.Sprintf("%s %s %s", renderer.Colorize.Color(format.DiffActionSymbol(action)), resourceChangeHeader(diff.change), diff.diff.RenderHuman(0, opts)))
|
||||
@ -453,7 +461,20 @@ func resourceChangeComment(resource jsonplan.ResourceChange, action plans.Action
|
||||
buf.WriteString(fmt.Sprintf("\n # (because key [%s] is not in for_each map)", resource.Index))
|
||||
}
|
||||
if len(resource.Deposed) != 0 {
|
||||
// Some extra context about this unusual situation.
|
||||
// In the case where we partially failed to replace a resource
|
||||
// configured with 'create_before_destroy' in a previous apply and
|
||||
// the deposed instance is still in the state, we give some extra
|
||||
// context about this unusual situation.
|
||||
buf.WriteString("\n # (left over from a partially-failed replacement of this instance)")
|
||||
}
|
||||
case plans.Forget:
|
||||
buf.WriteString(fmt.Sprintf("[bold] # %s[reset] will be removed from the OpenTofu state [bold][red]but will not be destroyed[reset]", dispAddr))
|
||||
|
||||
if len(resource.Deposed) != 0 {
|
||||
// In the case where we partially failed to replace a resource
|
||||
// configured with 'create_before_destroy' in a previous apply and
|
||||
// the deposed instance is still in the state, we give some extra
|
||||
// context about this unusual situation.
|
||||
buf.WriteString("\n # (left over from a partially-failed replacement of this instance)")
|
||||
}
|
||||
case plans.NoOp:
|
||||
@ -524,6 +545,9 @@ func actionDescription(action plans.Action) string {
|
||||
return "[red]-[reset]/[green]+[reset] destroy and then create replacement"
|
||||
case plans.Read:
|
||||
return " [cyan]<=[reset] read (data resources)"
|
||||
case plans.Forget:
|
||||
return " [red].[reset] forget"
|
||||
|
||||
default:
|
||||
panic(fmt.Sprintf("unrecognized change type: %s", action.String()))
|
||||
}
|
||||
|
@ -569,6 +569,44 @@ func TestResourceChange_primitiveTypes(t *testing.T) {
|
||||
- resource "test_instance" "example" {
|
||||
- id = "i-02ae66f368e8518a9" -> null
|
||||
}`,
|
||||
},
|
||||
"forget": {
|
||||
Action: plans.Forget,
|
||||
Mode: addrs.ManagedResourceMode,
|
||||
Before: cty.ObjectVal(map[string]cty.Value{
|
||||
"id": cty.StringVal("i-02ae66f368e8518a9"),
|
||||
}),
|
||||
After: cty.NullVal(cty.EmptyObject),
|
||||
Schema: &configschema.Block{
|
||||
Attributes: map[string]*configschema.Attribute{
|
||||
"id": {Type: cty.String, Computed: true},
|
||||
},
|
||||
},
|
||||
RequiredReplace: cty.NewPathSet(),
|
||||
ExpectedOutput: ` # test_instance.example will be removed from the OpenTofu state but will not be destroyed
|
||||
. resource "test_instance" "example" {
|
||||
id = "i-02ae66f368e8518a9"
|
||||
}`,
|
||||
},
|
||||
"forget a deposed object": {
|
||||
Action: plans.Forget,
|
||||
Mode: addrs.ManagedResourceMode,
|
||||
DeposedKey: states.DeposedKey("byebye"),
|
||||
Before: cty.ObjectVal(map[string]cty.Value{
|
||||
"id": cty.StringVal("i-02ae66f368e8518a9"),
|
||||
}),
|
||||
After: cty.NullVal(cty.EmptyObject),
|
||||
Schema: &configschema.Block{
|
||||
Attributes: map[string]*configschema.Attribute{
|
||||
"id": {Type: cty.String, Computed: true},
|
||||
},
|
||||
},
|
||||
RequiredReplace: cty.NewPathSet(),
|
||||
ExpectedOutput: ` # test_instance.example (deposed object byebye) will be removed from the OpenTofu state but will not be destroyed
|
||||
# (left over from a partially-failed replacement of this instance)
|
||||
. resource "test_instance" "example" {
|
||||
id = "i-02ae66f368e8518a9"
|
||||
}`,
|
||||
},
|
||||
"string in-place update": {
|
||||
Action: plans.Update,
|
||||
@ -5888,6 +5926,41 @@ func TestResourceChange_actionReason(t *testing.T) {
|
||||
ExpectedOutput: ` # test_instance.example must be replaced
|
||||
+/- resource "test_instance" "example" {}`,
|
||||
},
|
||||
"forget for no particular reason": {
|
||||
Action: plans.Forget,
|
||||
ActionReason: plans.ResourceInstanceChangeNoReason,
|
||||
Mode: addrs.ManagedResourceMode,
|
||||
Before: emptyVal,
|
||||
After: nullVal,
|
||||
Schema: emptySchema,
|
||||
RequiredReplace: cty.NewPathSet(),
|
||||
ExpectedOutput: ` # test_instance.example will be removed from the OpenTofu state but will not be destroyed
|
||||
. resource "test_instance" "example" {}`,
|
||||
},
|
||||
"forget because no resource configuration": {
|
||||
Action: plans.Forget,
|
||||
ActionReason: plans.ResourceInstanceDeleteBecauseNoResourceConfig,
|
||||
ModuleInst: addrs.RootModuleInstance.Child("foo", addrs.NoKey),
|
||||
Mode: addrs.ManagedResourceMode,
|
||||
Before: emptyVal,
|
||||
After: nullVal,
|
||||
Schema: emptySchema,
|
||||
RequiredReplace: cty.NewPathSet(),
|
||||
ExpectedOutput: ` # module.foo.test_instance.example will be removed from the OpenTofu state but will not be destroyed
|
||||
. resource "test_instance" "example" {}`,
|
||||
},
|
||||
"forget because no module": {
|
||||
Action: plans.Forget,
|
||||
ActionReason: plans.ResourceInstanceDeleteBecauseNoModule,
|
||||
ModuleInst: addrs.RootModuleInstance.Child("foo", addrs.IntKey(1)),
|
||||
Mode: addrs.ManagedResourceMode,
|
||||
Before: emptyVal,
|
||||
After: nullVal,
|
||||
Schema: emptySchema,
|
||||
RequiredReplace: cty.NewPathSet(),
|
||||
ExpectedOutput: ` # module.foo[1].test_instance.example will be removed from the OpenTofu state but will not be destroyed
|
||||
. resource "test_instance" "example" {}`,
|
||||
},
|
||||
}
|
||||
|
||||
runTestCases(t, testCases)
|
||||
@ -6683,6 +6756,105 @@ func TestResourceChange_sensitiveVariable(t *testing.T) {
|
||||
# so its contents will not be displayed.
|
||||
}
|
||||
}`,
|
||||
},
|
||||
"forget": {
|
||||
Action: plans.Forget,
|
||||
Mode: addrs.ManagedResourceMode,
|
||||
Before: cty.ObjectVal(map[string]cty.Value{
|
||||
"id": cty.StringVal("i-02ae66f368e8518a9"),
|
||||
"ami": cty.StringVal("ami-BEFORE"),
|
||||
"list_field": cty.ListVal([]cty.Value{
|
||||
cty.StringVal("hello"),
|
||||
cty.StringVal("friends"),
|
||||
}),
|
||||
"map_key": cty.MapVal(map[string]cty.Value{
|
||||
"breakfast": cty.NumberIntVal(800),
|
||||
"dinner": cty.NumberIntVal(2000), // sensitive key
|
||||
}),
|
||||
"map_whole": cty.MapVal(map[string]cty.Value{
|
||||
"breakfast": cty.StringVal("pizza"),
|
||||
"dinner": cty.StringVal("pizza"),
|
||||
}),
|
||||
"nested_block": cty.ListVal([]cty.Value{
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"an_attr": cty.StringVal("secret"),
|
||||
"another": cty.StringVal("not secret"),
|
||||
}),
|
||||
}),
|
||||
"nested_block_set": cty.ListVal([]cty.Value{
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"an_attr": cty.StringVal("secret"),
|
||||
"another": cty.StringVal("not secret"),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
After: cty.NullVal(cty.EmptyObject),
|
||||
BeforeValMarks: []cty.PathValueMarks{
|
||||
{
|
||||
Path: cty.Path{cty.GetAttrStep{Name: "ami"}},
|
||||
Marks: cty.NewValueMarks(marks.Sensitive),
|
||||
},
|
||||
{
|
||||
Path: cty.Path{cty.GetAttrStep{Name: "list_field"}, cty.IndexStep{Key: cty.NumberIntVal(1)}},
|
||||
Marks: cty.NewValueMarks(marks.Sensitive),
|
||||
},
|
||||
{
|
||||
Path: cty.Path{cty.GetAttrStep{Name: "map_key"}, cty.IndexStep{Key: cty.StringVal("dinner")}},
|
||||
Marks: cty.NewValueMarks(marks.Sensitive),
|
||||
},
|
||||
{
|
||||
Path: cty.Path{cty.GetAttrStep{Name: "map_whole"}},
|
||||
Marks: cty.NewValueMarks(marks.Sensitive),
|
||||
},
|
||||
{
|
||||
Path: cty.Path{cty.GetAttrStep{Name: "nested_block"}},
|
||||
Marks: cty.NewValueMarks(marks.Sensitive),
|
||||
},
|
||||
{
|
||||
Path: cty.Path{cty.GetAttrStep{Name: "nested_block_set"}},
|
||||
Marks: cty.NewValueMarks(marks.Sensitive),
|
||||
},
|
||||
},
|
||||
RequiredReplace: cty.NewPathSet(),
|
||||
Schema: &configschema.Block{
|
||||
Attributes: map[string]*configschema.Attribute{
|
||||
"id": {Type: cty.String, Optional: true, Computed: true},
|
||||
"ami": {Type: cty.String, Optional: true},
|
||||
"list_field": {Type: cty.List(cty.String), Optional: true},
|
||||
"map_key": {Type: cty.Map(cty.Number), Optional: true},
|
||||
"map_whole": {Type: cty.Map(cty.String), Optional: true},
|
||||
},
|
||||
BlockTypes: map[string]*configschema.NestedBlock{
|
||||
"nested_block_set": {
|
||||
Block: configschema.Block{
|
||||
Attributes: map[string]*configschema.Attribute{
|
||||
"an_attr": {Type: cty.String, Optional: true},
|
||||
"another": {Type: cty.String, Optional: true},
|
||||
},
|
||||
},
|
||||
Nesting: configschema.NestingSet,
|
||||
},
|
||||
},
|
||||
},
|
||||
ExpectedOutput: ` # test_instance.example will be removed from the OpenTofu state but will not be destroyed
|
||||
. resource "test_instance" "example" {
|
||||
ami = (sensitive value)
|
||||
id = "i-02ae66f368e8518a9"
|
||||
list_field = [
|
||||
"hello",
|
||||
(sensitive value),
|
||||
]
|
||||
map_key = {
|
||||
"breakfast" = 800
|
||||
"dinner" = (sensitive value)
|
||||
}
|
||||
map_whole = (sensitive value)
|
||||
|
||||
nested_block_set {
|
||||
# At least one attribute in this block is (or was) sensitive,
|
||||
# so its contents will not be displayed.
|
||||
}
|
||||
}`,
|
||||
},
|
||||
"update with sensitive value forcing replacement": {
|
||||
Action: plans.DeleteThenCreate,
|
||||
@ -6900,6 +7072,32 @@ func TestResourceChange_moved(t *testing.T) {
|
||||
# (2 unchanged attributes hidden)
|
||||
}`,
|
||||
},
|
||||
"moved and forgotten": {
|
||||
PrevRunAddr: prevRunAddr,
|
||||
Action: plans.Forget,
|
||||
Mode: addrs.ManagedResourceMode,
|
||||
Before: cty.ObjectVal(map[string]cty.Value{
|
||||
"id": cty.StringVal("12345"),
|
||||
"foo": cty.StringVal("hello"),
|
||||
"bar": cty.StringVal("baz"),
|
||||
}),
|
||||
After: cty.ObjectVal(map[string]cty.Value{
|
||||
"id": cty.StringVal("12345"),
|
||||
"foo": cty.StringVal("hello"),
|
||||
"bar": cty.StringVal("boop"),
|
||||
}),
|
||||
Schema: &configschema.Block{
|
||||
Attributes: map[string]*configschema.Attribute{
|
||||
"id": {Type: cty.String, Computed: true},
|
||||
},
|
||||
},
|
||||
RequiredReplace: cty.NewPathSet(),
|
||||
ExpectedOutput: ` # test_instance.example will be removed from the OpenTofu state but will not be destroyed
|
||||
# (moved from test_instance.previous)
|
||||
. resource "test_instance" "example" {
|
||||
id = "12345"
|
||||
}`,
|
||||
},
|
||||
}
|
||||
|
||||
runTestCases(t, testCases)
|
||||
|
@ -92,6 +92,7 @@ type Change struct {
|
||||
// ["delete", "create"]
|
||||
// ["create", "delete"]
|
||||
// ["delete"]
|
||||
// ["forget"]
|
||||
// The two "replace" actions are represented in this way to allow callers to
|
||||
// e.g. just scan the list for "delete" to recognize all three situations
|
||||
// where the object will be deleted, allowing for any new deletion
|
||||
@ -99,10 +100,11 @@ type Change struct {
|
||||
Actions []string `json:"actions,omitempty"`
|
||||
|
||||
// Before and After are representations of the object value both before and
|
||||
// after the action. For ["create"] and ["delete"] actions, either "before"
|
||||
// or "after" is unset (respectively). For ["no-op"], the before and after
|
||||
// values are identical. The "after" value will be incomplete if there are
|
||||
// values within it that won't be known until after apply.
|
||||
// after the action. For ["delete"] and ["forget"] actions, the "after"
|
||||
// value is unset. For ["create"] the "before" is unset. For ["no-op"], the
|
||||
// before and after values are identical. The "after" value will be
|
||||
// incomplete if there are values within it that won't be known until after
|
||||
// apply.
|
||||
Before json.RawMessage `json:"before,omitempty"`
|
||||
After json.RawMessage `json:"after,omitempty"`
|
||||
|
||||
@ -841,6 +843,8 @@ func actionString(action string) []string {
|
||||
return []string{"read"}
|
||||
case action == "DeleteThenCreate":
|
||||
return []string{"delete", "create"}
|
||||
case action == "Forget":
|
||||
return []string{"forget"}
|
||||
default:
|
||||
return []string{action}
|
||||
}
|
||||
@ -870,6 +874,8 @@ func UnmarshalActions(actions []string) plans.Action {
|
||||
return plans.Read
|
||||
case "no-op":
|
||||
return plans.NoOp
|
||||
case "forget":
|
||||
return plans.Forget
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -72,6 +72,7 @@ const (
|
||||
ActionReplace ChangeAction = "replace"
|
||||
ActionDelete ChangeAction = "delete"
|
||||
ActionImport ChangeAction = "import"
|
||||
ActionForget ChangeAction = "remove"
|
||||
)
|
||||
|
||||
func changeAction(action plans.Action) ChangeAction {
|
||||
@ -88,6 +89,8 @@ func changeAction(action plans.Action) ChangeAction {
|
||||
return ActionReplace
|
||||
case plans.Delete:
|
||||
return ActionDelete
|
||||
case plans.Forget:
|
||||
return ActionForget
|
||||
default:
|
||||
return ActionNoOp
|
||||
}
|
||||
|
@ -319,6 +319,8 @@ func startActionVerb(action plans.Action) string {
|
||||
// This is not currently possible to reach, as we receive separate
|
||||
// passes for create and delete
|
||||
return "Replacing"
|
||||
case plans.Forget:
|
||||
return "Removing"
|
||||
case plans.NoOp:
|
||||
// This should never be possible: a no-op planned change should not
|
||||
// be applied. We'll fall back to "Applying".
|
||||
@ -345,6 +347,8 @@ func progressActionVerb(action plans.Action) string {
|
||||
// This is not currently possible to reach, as we receive separate
|
||||
// passes for create and delete
|
||||
return "replacing"
|
||||
case plans.Forget:
|
||||
return "removing"
|
||||
case plans.NoOp:
|
||||
// This should never be possible: a no-op planned change should not
|
||||
// be applied. We'll fall back to "applying".
|
||||
@ -371,6 +375,8 @@ func actionNoun(action plans.Action) string {
|
||||
// This is not currently possible to reach, as we receive separate
|
||||
// passes for create and delete
|
||||
return "Replacement"
|
||||
case plans.Forget:
|
||||
return "Removal"
|
||||
case plans.NoOp:
|
||||
// This should never be possible: a no-op planned change should not
|
||||
// be applied. We'll fall back to "Apply".
|
||||
|
@ -51,8 +51,9 @@ type Module struct {
|
||||
ManagedResources map[string]*Resource
|
||||
DataResources map[string]*Resource
|
||||
|
||||
Moved []*Moved
|
||||
Import []*Import
|
||||
Moved []*Moved
|
||||
Import []*Import
|
||||
Removed []*Removed
|
||||
|
||||
Checks map[string]*Check
|
||||
|
||||
@ -90,8 +91,9 @@ type File struct {
|
||||
ManagedResources []*Resource
|
||||
DataResources []*Resource
|
||||
|
||||
Moved []*Moved
|
||||
Import []*Import
|
||||
Moved []*Moved
|
||||
Import []*Import
|
||||
Removed []*Removed
|
||||
|
||||
Checks []*Check
|
||||
}
|
||||
@ -468,6 +470,8 @@ func (m *Module) appendFile(file *File) hcl.Diagnostics {
|
||||
m.Import = append(m.Import, i)
|
||||
}
|
||||
|
||||
m.Removed = append(m.Removed, file.Removed...)
|
||||
|
||||
return diags
|
||||
}
|
||||
|
||||
@ -658,6 +662,15 @@ func (m *Module) mergeFile(file *File) hcl.Diagnostics {
|
||||
})
|
||||
}
|
||||
|
||||
for _, m := range file.Removed {
|
||||
diags = append(diags, &hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: "Cannot override 'Removed' blocks",
|
||||
Detail: "Removed blocks can appear only in normal files, not in override files.",
|
||||
Subject: m.DeclRange.Ptr(),
|
||||
})
|
||||
}
|
||||
|
||||
return diags
|
||||
}
|
||||
|
||||
|
@ -196,6 +196,13 @@ func (p *Parser) loadConfigFile(path string, override bool) (*File, hcl.Diagnost
|
||||
file.Checks = append(file.Checks, cfg)
|
||||
}
|
||||
|
||||
case "removed":
|
||||
cfg, cfgDiags := decodeRemovedBlock(block)
|
||||
diags = append(diags, cfgDiags...)
|
||||
if cfg != nil {
|
||||
file.Removed = append(file.Removed, cfg)
|
||||
}
|
||||
|
||||
default:
|
||||
// Should never happen because the above cases should be exhaustive
|
||||
// for all block type names in our schema.
|
||||
@ -293,6 +300,9 @@ var configFileSchema = &hcl.BodySchema{
|
||||
Type: "check",
|
||||
LabelNames: []string{"name"},
|
||||
},
|
||||
{
|
||||
Type: "removed",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
49
internal/configs/removed.go
Normal file
49
internal/configs/removed.go
Normal file
@ -0,0 +1,49 @@
|
||||
// Copyright (c) The OpenTofu Authors
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
// Copyright (c) 2023 HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package configs
|
||||
|
||||
import (
|
||||
"github.com/hashicorp/hcl/v2"
|
||||
"github.com/opentofu/opentofu/internal/addrs"
|
||||
)
|
||||
|
||||
// Removed represents a removed block in the configuration.
|
||||
type Removed struct {
|
||||
From *addrs.RemoveEndpoint
|
||||
|
||||
DeclRange hcl.Range
|
||||
}
|
||||
|
||||
func decodeRemovedBlock(block *hcl.Block) (*Removed, hcl.Diagnostics) {
|
||||
var diags hcl.Diagnostics
|
||||
removed := &Removed{
|
||||
DeclRange: block.DefRange,
|
||||
}
|
||||
|
||||
content, moreDiags := block.Body.Content(removedBlockSchema)
|
||||
diags = append(diags, moreDiags...)
|
||||
|
||||
if attr, exists := content.Attributes["from"]; exists {
|
||||
from, traversalDiags := hcl.AbsTraversalForExpr(attr.Expr)
|
||||
diags = append(diags, traversalDiags...)
|
||||
if !traversalDiags.HasErrors() {
|
||||
from, fromDiags := addrs.ParseRemoveEndpoint(from)
|
||||
diags = append(diags, fromDiags.ToHCL()...)
|
||||
removed.From = from
|
||||
}
|
||||
}
|
||||
|
||||
return removed, diags
|
||||
}
|
||||
|
||||
var removedBlockSchema = &hcl.BodySchema{
|
||||
Attributes: []hcl.AttributeSchema{
|
||||
{
|
||||
Name: "from",
|
||||
Required: true,
|
||||
},
|
||||
},
|
||||
}
|
198
internal/configs/removed_test.go
Normal file
198
internal/configs/removed_test.go
Normal file
@ -0,0 +1,198 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package configs
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/hashicorp/hcl/v2"
|
||||
"github.com/hashicorp/hcl/v2/hcltest"
|
||||
"github.com/opentofu/opentofu/internal/addrs"
|
||||
)
|
||||
|
||||
func TestRemovedBlock_decode(t *testing.T) {
|
||||
blockRange := hcl.Range{
|
||||
Filename: "mock.tf",
|
||||
Start: hcl.Pos{Line: 3, Column: 12, Byte: 27},
|
||||
End: hcl.Pos{Line: 3, Column: 19, Byte: 34},
|
||||
}
|
||||
|
||||
foo_expr := hcltest.MockExprTraversalSrc("test_instance.foo")
|
||||
mod_foo_expr := hcltest.MockExprTraversalSrc("module.foo")
|
||||
foo_index_expr := hcltest.MockExprTraversalSrc("test_instance.foo[1]")
|
||||
mod_boop_index_foo_expr := hcltest.MockExprTraversalSrc("module.boop[1].test_instance.foo")
|
||||
data_foo_expr := hcltest.MockExprTraversalSrc("data.test_instance.foo")
|
||||
|
||||
tests := map[string]struct {
|
||||
input *hcl.Block
|
||||
want *Removed
|
||||
err string
|
||||
}{
|
||||
"success": {
|
||||
&hcl.Block{
|
||||
Type: "removed",
|
||||
Body: hcltest.MockBody(&hcl.BodyContent{
|
||||
Attributes: hcl.Attributes{
|
||||
"from": {
|
||||
Name: "from",
|
||||
Expr: foo_expr,
|
||||
},
|
||||
},
|
||||
}),
|
||||
DefRange: blockRange,
|
||||
},
|
||||
&Removed{
|
||||
From: mustRemoveEndpointFromExpr(foo_expr),
|
||||
DeclRange: blockRange,
|
||||
},
|
||||
``,
|
||||
},
|
||||
"modules": {
|
||||
&hcl.Block{
|
||||
Type: "removed",
|
||||
Body: hcltest.MockBody(&hcl.BodyContent{
|
||||
Attributes: hcl.Attributes{
|
||||
"from": {
|
||||
Name: "from",
|
||||
Expr: mod_foo_expr,
|
||||
},
|
||||
},
|
||||
}),
|
||||
DefRange: blockRange,
|
||||
},
|
||||
&Removed{
|
||||
From: mustRemoveEndpointFromExpr(mod_foo_expr),
|
||||
DeclRange: blockRange,
|
||||
},
|
||||
``,
|
||||
},
|
||||
"error: missing argument": {
|
||||
&hcl.Block{
|
||||
Type: "removed",
|
||||
Body: hcltest.MockBody(&hcl.BodyContent{
|
||||
Attributes: hcl.Attributes{},
|
||||
}),
|
||||
DefRange: blockRange,
|
||||
},
|
||||
&Removed{
|
||||
DeclRange: blockRange,
|
||||
},
|
||||
"Missing required argument",
|
||||
},
|
||||
"error: indexed resources": {
|
||||
&hcl.Block{
|
||||
Type: "removed",
|
||||
Body: hcltest.MockBody(&hcl.BodyContent{
|
||||
Attributes: hcl.Attributes{
|
||||
"from": {
|
||||
Name: "from",
|
||||
Expr: foo_index_expr,
|
||||
},
|
||||
},
|
||||
}),
|
||||
DefRange: blockRange,
|
||||
},
|
||||
&Removed{
|
||||
DeclRange: blockRange,
|
||||
},
|
||||
"Resource instance address with keys is not allowed",
|
||||
},
|
||||
"error: indexed modules": {
|
||||
&hcl.Block{
|
||||
Type: "removed",
|
||||
Body: hcltest.MockBody(&hcl.BodyContent{
|
||||
Attributes: hcl.Attributes{
|
||||
"from": {
|
||||
Name: "from",
|
||||
Expr: mod_boop_index_foo_expr,
|
||||
},
|
||||
},
|
||||
}),
|
||||
DefRange: blockRange,
|
||||
},
|
||||
&Removed{
|
||||
DeclRange: blockRange,
|
||||
},
|
||||
"Module instance address with keys is not allowed",
|
||||
},
|
||||
"error: data address": {
|
||||
&hcl.Block{
|
||||
Type: "moved",
|
||||
Body: hcltest.MockBody(&hcl.BodyContent{
|
||||
Attributes: hcl.Attributes{
|
||||
"from": {
|
||||
Name: "from",
|
||||
Expr: data_foo_expr,
|
||||
},
|
||||
},
|
||||
}),
|
||||
DefRange: blockRange,
|
||||
},
|
||||
&Removed{
|
||||
DeclRange: blockRange,
|
||||
},
|
||||
"Data source address is not allowed",
|
||||
},
|
||||
}
|
||||
|
||||
for name, test := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
got, diags := decodeRemovedBlock(test.input)
|
||||
|
||||
if diags.HasErrors() {
|
||||
if test.err == "" {
|
||||
t.Fatalf("unexpected error: %s", diags.Errs())
|
||||
}
|
||||
if gotErr := diags[0].Summary; gotErr != test.err {
|
||||
t.Errorf("wrong error, got %q, want %q", gotErr, test.err)
|
||||
}
|
||||
} else if test.err != "" {
|
||||
t.Fatal("expected error")
|
||||
}
|
||||
|
||||
if !cmp.Equal(got, test.want, cmp.AllowUnexported(addrs.MoveEndpoint{})) {
|
||||
t.Fatalf("wrong result: %s", cmp.Diff(got, test.want))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRemovedBlock_inModule(t *testing.T) {
|
||||
parser := NewParser(nil)
|
||||
mod, diags := parser.LoadConfigDir("testdata/valid-modules/removed-blocks")
|
||||
if diags.HasErrors() {
|
||||
t.Errorf("unexpected error: %s", diags.Error())
|
||||
}
|
||||
|
||||
var got []string
|
||||
for _, mc := range mod.Removed {
|
||||
got = append(got, mc.From.RelSubject.String())
|
||||
}
|
||||
want := []string{
|
||||
`test.foo`,
|
||||
`test.foo`,
|
||||
`module.a`,
|
||||
`module.a`,
|
||||
`test.foo`,
|
||||
`test.boop`,
|
||||
}
|
||||
if diff := cmp.Diff(want, got); diff != "" {
|
||||
t.Errorf("wrong addresses\n%s", diff)
|
||||
}
|
||||
}
|
||||
|
||||
func mustRemoveEndpointFromExpr(expr hcl.Expression) *addrs.RemoveEndpoint {
|
||||
traversal, hcldiags := hcl.AbsTraversalForExpr(expr)
|
||||
if hcldiags.HasErrors() {
|
||||
panic(hcldiags.Errs())
|
||||
}
|
||||
|
||||
ep, diags := addrs.ParseRemoveEndpoint(traversal)
|
||||
if diags.HasErrors() {
|
||||
panic(diags.Err())
|
||||
}
|
||||
|
||||
return ep
|
||||
}
|
13
internal/configs/testdata/valid-files/refactoring.tf
vendored
Normal file
13
internal/configs/testdata/valid-files/refactoring.tf
vendored
Normal file
@ -0,0 +1,13 @@
|
||||
import {
|
||||
to = aws_instance.import
|
||||
id = 1
|
||||
}
|
||||
|
||||
moved {
|
||||
from = aws_instance.moved_from
|
||||
to = aws_instance.moved_to
|
||||
}
|
||||
|
||||
removed {
|
||||
from = aws_instance.removed
|
||||
}
|
19
internal/configs/testdata/valid-modules/removed-blocks/removed-blocks-1.tf
vendored
Normal file
19
internal/configs/testdata/valid-modules/removed-blocks/removed-blocks-1.tf
vendored
Normal file
@ -0,0 +1,19 @@
|
||||
removed {
|
||||
from = test.foo
|
||||
}
|
||||
|
||||
removed {
|
||||
from = test.foo
|
||||
}
|
||||
|
||||
removed {
|
||||
from = module.a
|
||||
}
|
||||
|
||||
removed {
|
||||
from = module.a
|
||||
}
|
||||
|
||||
removed {
|
||||
from = test.foo
|
||||
}
|
5
internal/configs/testdata/valid-modules/removed-blocks/removed-blocks-2.tf
vendored
Normal file
5
internal/configs/testdata/valid-modules/removed-blocks/removed-blocks-2.tf
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
# One more removed block in a separate file just to make sure the
|
||||
# appending of multiple files works properly.
|
||||
removed {
|
||||
from = test.boop
|
||||
}
|
@ -15,6 +15,7 @@ const (
|
||||
DeleteThenCreate Action = '∓'
|
||||
CreateThenDelete Action = '±'
|
||||
Delete Action = '-'
|
||||
Forget Action = '.'
|
||||
)
|
||||
|
||||
//go:generate go run golang.org/x/tools/cmd/stringer -type Action
|
||||
|
@ -15,26 +15,32 @@ func _() {
|
||||
_ = x[DeleteThenCreate-8723]
|
||||
_ = x[CreateThenDelete-177]
|
||||
_ = x[Delete-45]
|
||||
_ = x[Forget-46]
|
||||
}
|
||||
|
||||
const (
|
||||
_Action_name_0 = "NoOp"
|
||||
_Action_name_1 = "Create"
|
||||
_Action_name_2 = "Delete"
|
||||
_Action_name_2 = "DeleteForget"
|
||||
_Action_name_3 = "Update"
|
||||
_Action_name_4 = "CreateThenDelete"
|
||||
_Action_name_5 = "Read"
|
||||
_Action_name_6 = "DeleteThenCreate"
|
||||
)
|
||||
|
||||
var (
|
||||
_Action_index_2 = [...]uint8{0, 6, 12}
|
||||
)
|
||||
|
||||
func (i Action) String() string {
|
||||
switch {
|
||||
case i == 0:
|
||||
return _Action_name_0
|
||||
case i == 43:
|
||||
return _Action_name_1
|
||||
case i == 45:
|
||||
return _Action_name_2
|
||||
case 45 <= i && i <= 46:
|
||||
i -= 45
|
||||
return _Action_name_2[_Action_index_2[i]:_Action_index_2[i+1]]
|
||||
case i == 126:
|
||||
return _Action_name_3
|
||||
case i == 177:
|
||||
|
@ -87,6 +87,7 @@ const (
|
||||
Action_DELETE Action = 5
|
||||
Action_DELETE_THEN_CREATE Action = 6
|
||||
Action_CREATE_THEN_DELETE Action = 7
|
||||
Action_FORGET Action = 8
|
||||
)
|
||||
|
||||
// Enum value maps for Action.
|
||||
@ -99,6 +100,7 @@ var (
|
||||
5: "DELETE",
|
||||
6: "DELETE_THEN_CREATE",
|
||||
7: "CREATE_THEN_DELETE",
|
||||
8: "FORGET",
|
||||
}
|
||||
Action_value = map[string]int32{
|
||||
"NOOP": 0,
|
||||
@ -108,6 +110,7 @@ var (
|
||||
"DELETE": 5,
|
||||
"DELETE_THEN_CREATE": 6,
|
||||
"CREATE_THEN_DELETE": 7,
|
||||
"FORGET": 8,
|
||||
}
|
||||
)
|
||||
|
||||
@ -729,7 +732,7 @@ type ResourceInstanceChange struct {
|
||||
// apply it.
|
||||
Provider string `protobuf:"bytes,8,opt,name=provider,proto3" json:"provider,omitempty"`
|
||||
// Description of the proposed change. May use "create", "read", "update",
|
||||
// "replace", "delete" and "no-op" actions.
|
||||
// "replace", "delete", "forget" and "no-op" actions.
|
||||
Change *Change `protobuf:"bytes,9,opt,name=change,proto3" json:"change,omitempty"`
|
||||
// raw blob value provided by the provider as additional context for the
|
||||
// change. Must be considered an opaque value for any consumer other than
|
||||
@ -1504,47 +1507,48 @@ var file_planfile_proto_rawDesc = []byte{
|
||||
0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x2a, 0x31, 0x0a, 0x04, 0x4d, 0x6f, 0x64, 0x65, 0x12, 0x0a,
|
||||
0x0a, 0x06, 0x4e, 0x4f, 0x52, 0x4d, 0x41, 0x4c, 0x10, 0x00, 0x12, 0x0b, 0x0a, 0x07, 0x44, 0x45,
|
||||
0x53, 0x54, 0x52, 0x4f, 0x59, 0x10, 0x01, 0x12, 0x10, 0x0a, 0x0c, 0x52, 0x45, 0x46, 0x52, 0x45,
|
||||
0x53, 0x48, 0x5f, 0x4f, 0x4e, 0x4c, 0x59, 0x10, 0x02, 0x2a, 0x70, 0x0a, 0x06, 0x41, 0x63, 0x74,
|
||||
0x53, 0x48, 0x5f, 0x4f, 0x4e, 0x4c, 0x59, 0x10, 0x02, 0x2a, 0x7c, 0x0a, 0x06, 0x41, 0x63, 0x74,
|
||||
0x69, 0x6f, 0x6e, 0x12, 0x08, 0x0a, 0x04, 0x4e, 0x4f, 0x4f, 0x50, 0x10, 0x00, 0x12, 0x0a, 0x0a,
|
||||
0x06, 0x43, 0x52, 0x45, 0x41, 0x54, 0x45, 0x10, 0x01, 0x12, 0x08, 0x0a, 0x04, 0x52, 0x45, 0x41,
|
||||
0x44, 0x10, 0x02, 0x12, 0x0a, 0x0a, 0x06, 0x55, 0x50, 0x44, 0x41, 0x54, 0x45, 0x10, 0x03, 0x12,
|
||||
0x0a, 0x0a, 0x06, 0x44, 0x45, 0x4c, 0x45, 0x54, 0x45, 0x10, 0x05, 0x12, 0x16, 0x0a, 0x12, 0x44,
|
||||
0x45, 0x4c, 0x45, 0x54, 0x45, 0x5f, 0x54, 0x48, 0x45, 0x4e, 0x5f, 0x43, 0x52, 0x45, 0x41, 0x54,
|
||||
0x45, 0x10, 0x06, 0x12, 0x16, 0x0a, 0x12, 0x43, 0x52, 0x45, 0x41, 0x54, 0x45, 0x5f, 0x54, 0x48,
|
||||
0x45, 0x4e, 0x5f, 0x44, 0x45, 0x4c, 0x45, 0x54, 0x45, 0x10, 0x07, 0x2a, 0xc8, 0x03, 0x0a, 0x1c,
|
||||
0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65,
|
||||
0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x12, 0x08, 0x0a, 0x04,
|
||||
0x4e, 0x4f, 0x4e, 0x45, 0x10, 0x00, 0x12, 0x1b, 0x0a, 0x17, 0x52, 0x45, 0x50, 0x4c, 0x41, 0x43,
|
||||
0x45, 0x5f, 0x42, 0x45, 0x43, 0x41, 0x55, 0x53, 0x45, 0x5f, 0x54, 0x41, 0x49, 0x4e, 0x54, 0x45,
|
||||
0x44, 0x10, 0x01, 0x12, 0x16, 0x0a, 0x12, 0x52, 0x45, 0x50, 0x4c, 0x41, 0x43, 0x45, 0x5f, 0x42,
|
||||
0x59, 0x5f, 0x52, 0x45, 0x51, 0x55, 0x45, 0x53, 0x54, 0x10, 0x02, 0x12, 0x21, 0x0a, 0x1d, 0x52,
|
||||
0x45, 0x50, 0x4c, 0x41, 0x43, 0x45, 0x5f, 0x42, 0x45, 0x43, 0x41, 0x55, 0x53, 0x45, 0x5f, 0x43,
|
||||
0x41, 0x4e, 0x4e, 0x4f, 0x54, 0x5f, 0x55, 0x50, 0x44, 0x41, 0x54, 0x45, 0x10, 0x03, 0x12, 0x25,
|
||||
0x0a, 0x21, 0x44, 0x45, 0x4c, 0x45, 0x54, 0x45, 0x5f, 0x42, 0x45, 0x43, 0x41, 0x55, 0x53, 0x45,
|
||||
0x5f, 0x4e, 0x4f, 0x5f, 0x52, 0x45, 0x53, 0x4f, 0x55, 0x52, 0x43, 0x45, 0x5f, 0x43, 0x4f, 0x4e,
|
||||
0x46, 0x49, 0x47, 0x10, 0x04, 0x12, 0x23, 0x0a, 0x1f, 0x44, 0x45, 0x4c, 0x45, 0x54, 0x45, 0x5f,
|
||||
0x42, 0x45, 0x43, 0x41, 0x55, 0x53, 0x45, 0x5f, 0x57, 0x52, 0x4f, 0x4e, 0x47, 0x5f, 0x52, 0x45,
|
||||
0x50, 0x45, 0x54, 0x49, 0x54, 0x49, 0x4f, 0x4e, 0x10, 0x05, 0x12, 0x1e, 0x0a, 0x1a, 0x44, 0x45,
|
||||
0x4c, 0x45, 0x54, 0x45, 0x5f, 0x42, 0x45, 0x43, 0x41, 0x55, 0x53, 0x45, 0x5f, 0x43, 0x4f, 0x55,
|
||||
0x4e, 0x54, 0x5f, 0x49, 0x4e, 0x44, 0x45, 0x58, 0x10, 0x06, 0x12, 0x1b, 0x0a, 0x17, 0x44, 0x45,
|
||||
0x4c, 0x45, 0x54, 0x45, 0x5f, 0x42, 0x45, 0x43, 0x41, 0x55, 0x53, 0x45, 0x5f, 0x45, 0x41, 0x43,
|
||||
0x48, 0x5f, 0x4b, 0x45, 0x59, 0x10, 0x07, 0x12, 0x1c, 0x0a, 0x18, 0x44, 0x45, 0x4c, 0x45, 0x54,
|
||||
0x45, 0x5f, 0x42, 0x45, 0x43, 0x41, 0x55, 0x53, 0x45, 0x5f, 0x4e, 0x4f, 0x5f, 0x4d, 0x4f, 0x44,
|
||||
0x55, 0x4c, 0x45, 0x10, 0x08, 0x12, 0x17, 0x0a, 0x13, 0x52, 0x45, 0x50, 0x4c, 0x41, 0x43, 0x45,
|
||||
0x5f, 0x42, 0x59, 0x5f, 0x54, 0x52, 0x49, 0x47, 0x47, 0x45, 0x52, 0x53, 0x10, 0x09, 0x12, 0x1f,
|
||||
0x0a, 0x1b, 0x52, 0x45, 0x41, 0x44, 0x5f, 0x42, 0x45, 0x43, 0x41, 0x55, 0x53, 0x45, 0x5f, 0x43,
|
||||
0x4f, 0x4e, 0x46, 0x49, 0x47, 0x5f, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x0a, 0x12,
|
||||
0x23, 0x0a, 0x1f, 0x52, 0x45, 0x41, 0x44, 0x5f, 0x42, 0x45, 0x43, 0x41, 0x55, 0x53, 0x45, 0x5f,
|
||||
0x44, 0x45, 0x50, 0x45, 0x4e, 0x44, 0x45, 0x4e, 0x43, 0x59, 0x5f, 0x50, 0x45, 0x4e, 0x44, 0x49,
|
||||
0x4e, 0x47, 0x10, 0x0b, 0x12, 0x1d, 0x0a, 0x19, 0x52, 0x45, 0x41, 0x44, 0x5f, 0x42, 0x45, 0x43,
|
||||
0x41, 0x55, 0x53, 0x45, 0x5f, 0x43, 0x48, 0x45, 0x43, 0x4b, 0x5f, 0x4e, 0x45, 0x53, 0x54, 0x45,
|
||||
0x44, 0x10, 0x0d, 0x12, 0x21, 0x0a, 0x1d, 0x44, 0x45, 0x4c, 0x45, 0x54, 0x45, 0x5f, 0x42, 0x45,
|
||||
0x43, 0x41, 0x55, 0x53, 0x45, 0x5f, 0x4e, 0x4f, 0x5f, 0x4d, 0x4f, 0x56, 0x45, 0x5f, 0x54, 0x41,
|
||||
0x52, 0x47, 0x45, 0x54, 0x10, 0x0c, 0x42, 0x40, 0x5a, 0x3e, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62,
|
||||
0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x6f, 0x70, 0x65, 0x6e, 0x74, 0x6f, 0x66, 0x75, 0x2f, 0x6f, 0x70,
|
||||
0x65, 0x6e, 0x74, 0x6f, 0x66, 0x75, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f,
|
||||
0x70, 0x6c, 0x61, 0x6e, 0x73, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x70,
|
||||
0x6c, 0x61, 0x6e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
|
||||
0x45, 0x4e, 0x5f, 0x44, 0x45, 0x4c, 0x45, 0x54, 0x45, 0x10, 0x07, 0x12, 0x0a, 0x0a, 0x06, 0x46,
|
||||
0x4f, 0x52, 0x47, 0x45, 0x54, 0x10, 0x08, 0x2a, 0xc8, 0x03, 0x0a, 0x1c, 0x52, 0x65, 0x73, 0x6f,
|
||||
0x75, 0x72, 0x63, 0x65, 0x49, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x41, 0x63, 0x74, 0x69,
|
||||
0x6f, 0x6e, 0x52, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x12, 0x08, 0x0a, 0x04, 0x4e, 0x4f, 0x4e, 0x45,
|
||||
0x10, 0x00, 0x12, 0x1b, 0x0a, 0x17, 0x52, 0x45, 0x50, 0x4c, 0x41, 0x43, 0x45, 0x5f, 0x42, 0x45,
|
||||
0x43, 0x41, 0x55, 0x53, 0x45, 0x5f, 0x54, 0x41, 0x49, 0x4e, 0x54, 0x45, 0x44, 0x10, 0x01, 0x12,
|
||||
0x16, 0x0a, 0x12, 0x52, 0x45, 0x50, 0x4c, 0x41, 0x43, 0x45, 0x5f, 0x42, 0x59, 0x5f, 0x52, 0x45,
|
||||
0x51, 0x55, 0x45, 0x53, 0x54, 0x10, 0x02, 0x12, 0x21, 0x0a, 0x1d, 0x52, 0x45, 0x50, 0x4c, 0x41,
|
||||
0x43, 0x45, 0x5f, 0x42, 0x45, 0x43, 0x41, 0x55, 0x53, 0x45, 0x5f, 0x43, 0x41, 0x4e, 0x4e, 0x4f,
|
||||
0x54, 0x5f, 0x55, 0x50, 0x44, 0x41, 0x54, 0x45, 0x10, 0x03, 0x12, 0x25, 0x0a, 0x21, 0x44, 0x45,
|
||||
0x4c, 0x45, 0x54, 0x45, 0x5f, 0x42, 0x45, 0x43, 0x41, 0x55, 0x53, 0x45, 0x5f, 0x4e, 0x4f, 0x5f,
|
||||
0x52, 0x45, 0x53, 0x4f, 0x55, 0x52, 0x43, 0x45, 0x5f, 0x43, 0x4f, 0x4e, 0x46, 0x49, 0x47, 0x10,
|
||||
0x04, 0x12, 0x23, 0x0a, 0x1f, 0x44, 0x45, 0x4c, 0x45, 0x54, 0x45, 0x5f, 0x42, 0x45, 0x43, 0x41,
|
||||
0x55, 0x53, 0x45, 0x5f, 0x57, 0x52, 0x4f, 0x4e, 0x47, 0x5f, 0x52, 0x45, 0x50, 0x45, 0x54, 0x49,
|
||||
0x54, 0x49, 0x4f, 0x4e, 0x10, 0x05, 0x12, 0x1e, 0x0a, 0x1a, 0x44, 0x45, 0x4c, 0x45, 0x54, 0x45,
|
||||
0x5f, 0x42, 0x45, 0x43, 0x41, 0x55, 0x53, 0x45, 0x5f, 0x43, 0x4f, 0x55, 0x4e, 0x54, 0x5f, 0x49,
|
||||
0x4e, 0x44, 0x45, 0x58, 0x10, 0x06, 0x12, 0x1b, 0x0a, 0x17, 0x44, 0x45, 0x4c, 0x45, 0x54, 0x45,
|
||||
0x5f, 0x42, 0x45, 0x43, 0x41, 0x55, 0x53, 0x45, 0x5f, 0x45, 0x41, 0x43, 0x48, 0x5f, 0x4b, 0x45,
|
||||
0x59, 0x10, 0x07, 0x12, 0x1c, 0x0a, 0x18, 0x44, 0x45, 0x4c, 0x45, 0x54, 0x45, 0x5f, 0x42, 0x45,
|
||||
0x43, 0x41, 0x55, 0x53, 0x45, 0x5f, 0x4e, 0x4f, 0x5f, 0x4d, 0x4f, 0x44, 0x55, 0x4c, 0x45, 0x10,
|
||||
0x08, 0x12, 0x17, 0x0a, 0x13, 0x52, 0x45, 0x50, 0x4c, 0x41, 0x43, 0x45, 0x5f, 0x42, 0x59, 0x5f,
|
||||
0x54, 0x52, 0x49, 0x47, 0x47, 0x45, 0x52, 0x53, 0x10, 0x09, 0x12, 0x1f, 0x0a, 0x1b, 0x52, 0x45,
|
||||
0x41, 0x44, 0x5f, 0x42, 0x45, 0x43, 0x41, 0x55, 0x53, 0x45, 0x5f, 0x43, 0x4f, 0x4e, 0x46, 0x49,
|
||||
0x47, 0x5f, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x0a, 0x12, 0x23, 0x0a, 0x1f, 0x52,
|
||||
0x45, 0x41, 0x44, 0x5f, 0x42, 0x45, 0x43, 0x41, 0x55, 0x53, 0x45, 0x5f, 0x44, 0x45, 0x50, 0x45,
|
||||
0x4e, 0x44, 0x45, 0x4e, 0x43, 0x59, 0x5f, 0x50, 0x45, 0x4e, 0x44, 0x49, 0x4e, 0x47, 0x10, 0x0b,
|
||||
0x12, 0x1d, 0x0a, 0x19, 0x52, 0x45, 0x41, 0x44, 0x5f, 0x42, 0x45, 0x43, 0x41, 0x55, 0x53, 0x45,
|
||||
0x5f, 0x43, 0x48, 0x45, 0x43, 0x4b, 0x5f, 0x4e, 0x45, 0x53, 0x54, 0x45, 0x44, 0x10, 0x0d, 0x12,
|
||||
0x21, 0x0a, 0x1d, 0x44, 0x45, 0x4c, 0x45, 0x54, 0x45, 0x5f, 0x42, 0x45, 0x43, 0x41, 0x55, 0x53,
|
||||
0x45, 0x5f, 0x4e, 0x4f, 0x5f, 0x4d, 0x4f, 0x56, 0x45, 0x5f, 0x54, 0x41, 0x52, 0x47, 0x45, 0x54,
|
||||
0x10, 0x0c, 0x42, 0x40, 0x5a, 0x3e, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d,
|
||||
0x2f, 0x6f, 0x70, 0x65, 0x6e, 0x74, 0x6f, 0x66, 0x75, 0x2f, 0x6f, 0x70, 0x65, 0x6e, 0x74, 0x6f,
|
||||
0x66, 0x75, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x70, 0x6c, 0x61, 0x6e,
|
||||
0x73, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x70, 0x6c, 0x61, 0x6e, 0x70,
|
||||
0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
|
||||
}
|
||||
|
||||
var (
|
||||
|
@ -116,6 +116,7 @@ enum Action {
|
||||
DELETE = 5;
|
||||
DELETE_THEN_CREATE = 6;
|
||||
CREATE_THEN_DELETE = 7;
|
||||
FORGET = 8;
|
||||
}
|
||||
|
||||
// Change represents a change made to some object, transforming it from an old
|
||||
@ -206,7 +207,7 @@ message ResourceInstanceChange {
|
||||
string provider = 8;
|
||||
|
||||
// Description of the proposed change. May use "create", "read", "update",
|
||||
// "replace", "delete" and "no-op" actions.
|
||||
// "replace", "delete", "forget" and "no-op" actions.
|
||||
Change change = 9;
|
||||
|
||||
// raw blob value provided by the provider as additional context for the
|
||||
|
@ -391,6 +391,9 @@ func changeFromTfplan(rawChange *planproto.Change) (*plans.ChangeSrc, error) {
|
||||
case planproto.Action_DELETE:
|
||||
ret.Action = plans.Delete
|
||||
beforeIdx = 0
|
||||
case planproto.Action_FORGET:
|
||||
ret.Action = plans.Forget
|
||||
beforeIdx = 0
|
||||
case planproto.Action_CREATE_THEN_DELETE:
|
||||
ret.Action = plans.CreateThenDelete
|
||||
beforeIdx = 0
|
||||
@ -795,6 +798,9 @@ func changeToTfplan(change *plans.ChangeSrc) (*planproto.Change, error) {
|
||||
case plans.Delete:
|
||||
ret.Action = planproto.Action_DELETE
|
||||
ret.Values = []*planproto.DynamicValue{before}
|
||||
case plans.Forget:
|
||||
ret.Action = planproto.Action_FORGET
|
||||
ret.Values = []*planproto.DynamicValue{before}
|
||||
case plans.DeleteThenCreate:
|
||||
ret.Action = planproto.Action_DELETE_THEN_CREATE
|
||||
ret.Values = []*planproto.DynamicValue{before, after}
|
||||
|
@ -124,6 +124,28 @@ func TestTFPlanRoundTrip(t *testing.T) {
|
||||
}), objTy),
|
||||
},
|
||||
},
|
||||
{
|
||||
Addr: addrs.Resource{
|
||||
Mode: addrs.ManagedResourceMode,
|
||||
Type: "test_thing",
|
||||
Name: "forget",
|
||||
}.Instance(addrs.IntKey(1)).Absolute(addrs.RootModuleInstance),
|
||||
PrevRunAddr: addrs.Resource{
|
||||
Mode: addrs.ManagedResourceMode,
|
||||
Type: "test_thing",
|
||||
Name: "forget",
|
||||
}.Instance(addrs.IntKey(1)).Absolute(addrs.RootModuleInstance),
|
||||
ProviderAddr: addrs.AbsProviderConfig{
|
||||
Provider: addrs.NewDefaultProvider("test"),
|
||||
Module: addrs.RootModule,
|
||||
},
|
||||
ChangeSrc: plans.ChangeSrc{
|
||||
Action: plans.Forget,
|
||||
Before: mustNewDynamicValue(cty.ObjectVal(map[string]cty.Value{
|
||||
"id": cty.StringVal("bar-baz-forget"),
|
||||
}), objTy),
|
||||
},
|
||||
},
|
||||
{
|
||||
Addr: addrs.Resource{
|
||||
Mode: addrs.ManagedResourceMode,
|
||||
|
124
internal/refactoring/remove_statement.go
Normal file
124
internal/refactoring/remove_statement.go
Normal file
@ -0,0 +1,124 @@
|
||||
// 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
|
||||
DeclRange tfdiags.SourceRange
|
||||
}
|
||||
|
||||
// GetEndpointsToRemove 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 GetEndpointsToRemove(rootCfg *configs.Config) ([]addrs.ConfigRemovable, tfdiags.Diagnostics) {
|
||||
rm := findRemoveStatements(rootCfg, nil)
|
||||
diags := validateRemoveStatements(rootCfg, rm)
|
||||
removedAddresses := make([]addrs.ConfigRemovable, len(rm))
|
||||
for i, rs := range rm {
|
||||
removedAddresses[i] = rs.From
|
||||
}
|
||||
return removedAddresses, diags
|
||||
}
|
||||
|
||||
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, DeclRange: tfdiags.SourceRangeFromHCL(rc.DeclRange)}
|
||||
|
||||
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, DeclRange: tfdiags.SourceRangeFromHCL(rc.DeclRange)}
|
||||
|
||||
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
|
||||
}
|
85
internal/refactoring/remove_statement_test.go
Normal file
85
internal/refactoring/remove_statement_test.go
Normal file
@ -0,0 +1,85 @@
|
||||
// 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 (
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
|
||||
"github.com/opentofu/opentofu/internal/addrs"
|
||||
)
|
||||
|
||||
func TestGetEndpointsToRemove(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
fixtureName string
|
||||
want []addrs.ConfigRemovable
|
||||
wantError string
|
||||
}{
|
||||
{
|
||||
name: "Valid cases",
|
||||
fixtureName: "testdata/remove-statement/valid-remove-statements",
|
||||
want: []addrs.ConfigRemovable{
|
||||
interface{}(mustConfigResourceAddr("foo.basic_resource")).(addrs.ConfigRemovable),
|
||||
interface{}(addrs.Module{"basic_module"}).(addrs.ConfigRemovable),
|
||||
interface{}(mustConfigResourceAddr("module.child.foo.removed_resource_from_root_module")).(addrs.ConfigRemovable),
|
||||
interface{}(mustConfigResourceAddr("module.child.foo.removed_resource_from_child_module")).(addrs.ConfigRemovable),
|
||||
interface{}(addrs.Module{"child", "removed_module_from_child_module"}).(addrs.ConfigRemovable),
|
||||
interface{}(mustConfigResourceAddr("module.child.module.grandchild.foo.removed_resource_from_grandchild_module")).(addrs.ConfigRemovable),
|
||||
interface{}(addrs.Module{"child", "grandchild", "removed_module_from_grandchild_module"}).(addrs.ConfigRemovable),
|
||||
},
|
||||
wantError: ``,
|
||||
},
|
||||
{
|
||||
name: "Error - resource block still exist",
|
||||
fixtureName: "testdata/remove-statement/not-valid-resource-block-still-exist",
|
||||
want: []addrs.ConfigRemovable{
|
||||
interface{}(mustConfigResourceAddr("foo.basic_resource")).(addrs.ConfigRemovable),
|
||||
},
|
||||
wantError: `Removed resource block still exists: This statement declares a removal of the resource foo.basic_resource, but this resource block still exists in the configuration. Please remove the resource block.`,
|
||||
},
|
||||
{
|
||||
name: "Error - module block still exist",
|
||||
fixtureName: "testdata/remove-statement/not-valid-module-block-still-exist",
|
||||
want: []addrs.ConfigRemovable{},
|
||||
wantError: `Removed module block still exists: This statement declares a removal of the module module.child, but this module block still exists in the configuration. Please remove the module block.`,
|
||||
},
|
||||
{
|
||||
name: "Error - nested resource block still exist",
|
||||
fixtureName: "testdata/remove-statement/not-valid-nested-resource-block-still-exist",
|
||||
want: []addrs.ConfigRemovable{},
|
||||
wantError: `Removed resource block still exists: This statement declares a removal of the resource module.child.foo.basic_resource, but this resource block still exists in the configuration. Please remove the resource block.`,
|
||||
}}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
rootCfg, _ := loadRefactoringFixture(t, tt.fixtureName)
|
||||
got, diags := GetEndpointsToRemove(rootCfg)
|
||||
|
||||
if tt.wantError != "" {
|
||||
if !diags.HasErrors() {
|
||||
t.Fatalf("missing expected error\ngot: <no error>\nwant: %s", tt.wantError)
|
||||
}
|
||||
errStr := diags.Err().Error()
|
||||
if errStr != tt.wantError {
|
||||
t.Fatalf("wrong error\ngot: %s\nwant: %s", errStr, tt.wantError)
|
||||
}
|
||||
} else {
|
||||
if diff := cmp.Diff(tt.want, got); diff != "" {
|
||||
t.Errorf("wrong result\n%s", diff)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func mustConfigResourceAddr(s string) addrs.ConfigResource {
|
||||
addr, diags := addrs.ParseAbsResourceStr(s)
|
||||
if diags.HasErrors() {
|
||||
panic(diags.Err())
|
||||
}
|
||||
return addr.Config()
|
||||
}
|
@ -0,0 +1,2 @@
|
||||
resource "foo" "basic_resource" {
|
||||
}
|
7
internal/refactoring/testdata/remove-statement/not-valid-module-block-still-exist/main.tf
vendored
Normal file
7
internal/refactoring/testdata/remove-statement/not-valid-module-block-still-exist/main.tf
vendored
Normal file
@ -0,0 +1,7 @@
|
||||
module "child" {
|
||||
source = "./child"
|
||||
}
|
||||
|
||||
removed {
|
||||
from = module.child
|
||||
}
|
@ -0,0 +1,2 @@
|
||||
resource "foo" "basic_resource" {
|
||||
}
|
@ -0,0 +1,7 @@
|
||||
removed {
|
||||
from = module.child.foo.basic_resource
|
||||
}
|
||||
|
||||
module "child" {
|
||||
source = "./child"
|
||||
}
|
6
internal/refactoring/testdata/remove-statement/not-valid-resource-block-still-exist/main.tf
vendored
Normal file
6
internal/refactoring/testdata/remove-statement/not-valid-resource-block-still-exist/main.tf
vendored
Normal file
@ -0,0 +1,6 @@
|
||||
resource "foo" "basic_resource" {
|
||||
}
|
||||
|
||||
removed {
|
||||
from = foo.basic_resource
|
||||
}
|
@ -0,0 +1,7 @@
|
||||
removed {
|
||||
from = foo.removed_resource_from_grandchild_module
|
||||
}
|
||||
|
||||
removed {
|
||||
from = module.removed_module_from_grandchild_module
|
||||
}
|
11
internal/refactoring/testdata/remove-statement/valid-remove-statements/child/main.tf
vendored
Normal file
11
internal/refactoring/testdata/remove-statement/valid-remove-statements/child/main.tf
vendored
Normal file
@ -0,0 +1,11 @@
|
||||
removed {
|
||||
from = foo.removed_resource_from_child_module
|
||||
}
|
||||
|
||||
module "grandchild" {
|
||||
source = "./grandchild"
|
||||
}
|
||||
|
||||
removed {
|
||||
from = module.removed_module_from_child_module
|
||||
}
|
15
internal/refactoring/testdata/remove-statement/valid-remove-statements/main.tf
vendored
Normal file
15
internal/refactoring/testdata/remove-statement/valid-remove-statements/main.tf
vendored
Normal file
@ -0,0 +1,15 @@
|
||||
removed {
|
||||
from = foo.basic_resource
|
||||
}
|
||||
|
||||
removed {
|
||||
from = module.basic_module
|
||||
}
|
||||
|
||||
removed {
|
||||
from = module.child.foo.removed_resource_from_root_module
|
||||
}
|
||||
|
||||
module "child" {
|
||||
source = "./child"
|
||||
}
|
@ -2246,3 +2246,64 @@ locals {
|
||||
t.Errorf("expected local value to be \"foo\" but was \"%s\"", module.LocalValues["local_value"].AsString())
|
||||
}
|
||||
}
|
||||
|
||||
func TestContext2Apply_forgetOrphanAndDeposed(t *testing.T) {
|
||||
desposedKey := states.DeposedKey("deposed")
|
||||
addr := "aws_instance.baz"
|
||||
m := testModuleInline(t, map[string]string{
|
||||
"main.tf": `
|
||||
removed {
|
||||
from = aws_instance.baz
|
||||
}
|
||||
`,
|
||||
})
|
||||
hook := new(MockHook)
|
||||
p := testProvider("aws")
|
||||
state := states.NewState()
|
||||
root := state.EnsureModule(addrs.RootModuleInstance)
|
||||
root.SetResourceInstanceCurrent(
|
||||
mustResourceInstanceAddr(addr).Resource,
|
||||
&states.ResourceInstanceObjectSrc{
|
||||
Status: states.ObjectReady,
|
||||
AttrsJSON: []byte(`{"id":"bar"}`),
|
||||
},
|
||||
mustProviderConfig(`provider["registry.opentofu.org/hashicorp/aws"]`),
|
||||
)
|
||||
root.SetResourceInstanceDeposed(
|
||||
mustResourceInstanceAddr(addr).Resource,
|
||||
desposedKey,
|
||||
&states.ResourceInstanceObjectSrc{
|
||||
Status: states.ObjectTainted,
|
||||
AttrsJSON: []byte(`{"id":"bar"}`),
|
||||
Dependencies: []addrs.ConfigResource{},
|
||||
},
|
||||
mustProviderConfig(`provider["registry.opentofu.org/hashicorp/aws"]`),
|
||||
)
|
||||
ctx := testContext2(t, &ContextOpts{
|
||||
Providers: map[addrs.Provider]providers.Factory{
|
||||
addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p),
|
||||
},
|
||||
})
|
||||
|
||||
p.PlanResourceChangeFn = testDiffFn
|
||||
|
||||
plan, diags := ctx.Plan(m, state, DefaultPlanOpts)
|
||||
assertNoErrors(t, diags)
|
||||
|
||||
s, diags := ctx.Apply(plan, m)
|
||||
if diags.HasErrors() {
|
||||
t.Fatalf("diags: %s", diags.Err())
|
||||
}
|
||||
|
||||
if !s.Empty() {
|
||||
t.Fatalf("State should be empty")
|
||||
}
|
||||
|
||||
if p.ApplyResourceChangeCalled {
|
||||
t.Fatalf("When we forget we don't call the provider's ApplyResourceChange unlike in destroy")
|
||||
}
|
||||
|
||||
if hook.PostApplyCalled {
|
||||
t.Fatalf("PostApply hook should not be called as part of forget")
|
||||
}
|
||||
}
|
||||
|
@ -87,6 +87,10 @@ type PlanOpts struct {
|
||||
// will be added to the plan graph.
|
||||
ImportTargets []*ImportTarget
|
||||
|
||||
// EndpointsToRemove are the list of resources and modules to forget from
|
||||
// the state.
|
||||
EndpointsToRemove []addrs.ConfigRemovable
|
||||
|
||||
// GenerateConfig tells OpenTofu where to write any generated configuration
|
||||
// for any ImportTargets that do not have configuration already.
|
||||
//
|
||||
@ -310,6 +314,15 @@ func (c *Context) plan(config *configs.Config, prevRunState *states.State, opts
|
||||
opts.ImportTargets = c.findImportTargets(config, prevRunState)
|
||||
importTargetDiags := c.validateImportTargets(config, opts.ImportTargets)
|
||||
diags = diags.Append(importTargetDiags)
|
||||
|
||||
var endpointsToRemoveDiags tfdiags.Diagnostics
|
||||
opts.EndpointsToRemove, endpointsToRemoveDiags = refactoring.GetEndpointsToRemove(config)
|
||||
diags = diags.Append(endpointsToRemoveDiags)
|
||||
|
||||
if diags.HasErrors() {
|
||||
return nil, diags
|
||||
}
|
||||
|
||||
plan, walkDiags := c.planWalk(config, prevRunState, opts)
|
||||
diags = diags.Append(walkDiags)
|
||||
|
||||
@ -694,6 +707,7 @@ func (c *Context) planGraph(config *configs.Config, prevRunState *states.State,
|
||||
ExternalReferences: opts.ExternalReferences,
|
||||
ImportTargets: opts.ImportTargets,
|
||||
GenerateConfigPath: opts.GenerateConfigPath,
|
||||
EndpointsToRemove: opts.EndpointsToRemove,
|
||||
}).Build(addrs.RootModuleInstance)
|
||||
return graph, walkPlan, diags
|
||||
case plans.RefreshOnlyMode:
|
||||
|
@ -5410,3 +5410,761 @@ locals {
|
||||
t.Errorf("expected resource to be in planned state")
|
||||
}
|
||||
}
|
||||
|
||||
func TestContext2Plan_removedResourceBasic(t *testing.T) {
|
||||
desposedKey := states.DeposedKey("deposed")
|
||||
addr := mustResourceInstanceAddr("test_object.a")
|
||||
m := testModuleInline(t, map[string]string{
|
||||
"main.tf": `
|
||||
removed {
|
||||
from = test_object.a
|
||||
}
|
||||
`,
|
||||
})
|
||||
|
||||
state := states.BuildState(func(s *states.SyncState) {
|
||||
// The prior state tracks test_object.a, which we should be
|
||||
// removed from the state by the "removed" block in the config.
|
||||
s.SetResourceInstanceCurrent(addr, &states.ResourceInstanceObjectSrc{
|
||||
AttrsJSON: []byte(`{}`),
|
||||
Status: states.ObjectReady,
|
||||
}, mustProviderConfig(`provider["registry.opentofu.org/hashicorp/test"]`))
|
||||
s.SetResourceInstanceDeposed(
|
||||
mustResourceInstanceAddr(addr.String()),
|
||||
desposedKey,
|
||||
&states.ResourceInstanceObjectSrc{
|
||||
Status: states.ObjectTainted,
|
||||
AttrsJSON: []byte(`{"test_string":"old"}`),
|
||||
Dependencies: []addrs.ConfigResource{},
|
||||
},
|
||||
mustProviderConfig(`provider["registry.opentofu.org/hashicorp/test"]`),
|
||||
)
|
||||
})
|
||||
|
||||
p := simpleMockProvider()
|
||||
ctx := testContext2(t, &ContextOpts{
|
||||
Providers: map[addrs.Provider]providers.Factory{
|
||||
addrs.NewDefaultProvider("test"): testProviderFuncFixed(p),
|
||||
},
|
||||
})
|
||||
|
||||
plan, diags := ctx.Plan(m, state, &PlanOpts{
|
||||
Mode: plans.NormalMode,
|
||||
ForceReplace: []addrs.AbsResourceInstance{
|
||||
addr,
|
||||
},
|
||||
})
|
||||
if diags.HasErrors() {
|
||||
t.Fatalf("unexpected errors\n%s", diags.Err().Error())
|
||||
}
|
||||
|
||||
for _, test := range []struct {
|
||||
deposedKey states.DeposedKey
|
||||
wantReason plans.ResourceInstanceChangeActionReason
|
||||
}{{desposedKey, plans.ResourceInstanceChangeNoReason}, {states.NotDeposed, plans.ResourceInstanceDeleteBecauseNoResourceConfig}} {
|
||||
t.Run(addr.String(), func(t *testing.T) {
|
||||
var instPlan *plans.ResourceInstanceChangeSrc
|
||||
|
||||
if test.deposedKey == states.NotDeposed {
|
||||
instPlan = plan.Changes.ResourceInstance(addr)
|
||||
} else {
|
||||
instPlan = plan.Changes.ResourceInstanceDeposed(addr, test.deposedKey)
|
||||
}
|
||||
|
||||
if instPlan == nil {
|
||||
t.Fatalf("no plan for %s at all", addr)
|
||||
}
|
||||
|
||||
if got, want := instPlan.Addr, addr; !got.Equal(want) {
|
||||
t.Errorf("wrong current address\ngot: %s\nwant: %s", got, want)
|
||||
}
|
||||
if got, want := instPlan.PrevRunAddr, addr; !got.Equal(want) {
|
||||
t.Errorf("wrong previous run address\ngot: %s\nwant: %s", got, want)
|
||||
}
|
||||
if got, want := instPlan.Action, plans.Forget; got != want {
|
||||
t.Errorf("wrong planned action\ngot: %s\nwant: %s", got, want)
|
||||
}
|
||||
if got, want := instPlan.ActionReason, test.wantReason; got != want {
|
||||
t.Errorf("wrong action reason\ngot: %s\nwant: %s", got, want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestContext2Plan_removedModuleBasic(t *testing.T) {
|
||||
desposedKey := states.DeposedKey("deposed")
|
||||
addr := mustResourceInstanceAddr("module.mod.test_object.a")
|
||||
m := testModuleInline(t, map[string]string{
|
||||
"main.tf": `
|
||||
removed {
|
||||
from = module.mod
|
||||
}
|
||||
`,
|
||||
})
|
||||
|
||||
state := states.BuildState(func(s *states.SyncState) {
|
||||
// The prior state tracks module.mod.test_object.a, which should be
|
||||
// removed from the state by the module's "removed" block in the root module config.
|
||||
s.SetResourceInstanceCurrent(addr, &states.ResourceInstanceObjectSrc{
|
||||
AttrsJSON: []byte(`{}`),
|
||||
Status: states.ObjectReady,
|
||||
}, mustProviderConfig(`provider["registry.opentofu.org/hashicorp/test"]`))
|
||||
s.SetResourceInstanceDeposed(
|
||||
mustResourceInstanceAddr(addr.String()),
|
||||
desposedKey,
|
||||
&states.ResourceInstanceObjectSrc{
|
||||
Status: states.ObjectTainted,
|
||||
AttrsJSON: []byte(`{"test_string":"old"}`),
|
||||
Dependencies: []addrs.ConfigResource{},
|
||||
},
|
||||
mustProviderConfig(`provider["registry.opentofu.org/hashicorp/test"]`),
|
||||
)
|
||||
})
|
||||
|
||||
p := simpleMockProvider()
|
||||
ctx := testContext2(t, &ContextOpts{
|
||||
Providers: map[addrs.Provider]providers.Factory{
|
||||
addrs.NewDefaultProvider("test"): testProviderFuncFixed(p),
|
||||
},
|
||||
})
|
||||
|
||||
plan, diags := ctx.Plan(m, state, &PlanOpts{
|
||||
Mode: plans.NormalMode,
|
||||
ForceReplace: []addrs.AbsResourceInstance{
|
||||
addr,
|
||||
},
|
||||
})
|
||||
if diags.HasErrors() {
|
||||
t.Fatalf("unexpected errors\n%s", diags.Err().Error())
|
||||
}
|
||||
|
||||
for _, test := range []struct {
|
||||
deposedKey states.DeposedKey
|
||||
wantReason plans.ResourceInstanceChangeActionReason
|
||||
}{{desposedKey, plans.ResourceInstanceChangeNoReason}, {states.NotDeposed, plans.ResourceInstanceDeleteBecauseNoResourceConfig}} {
|
||||
t.Run(addr.String(), func(t *testing.T) {
|
||||
var instPlan *plans.ResourceInstanceChangeSrc
|
||||
|
||||
if test.deposedKey == states.NotDeposed {
|
||||
instPlan = plan.Changes.ResourceInstance(addr)
|
||||
} else {
|
||||
instPlan = plan.Changes.ResourceInstanceDeposed(addr, test.deposedKey)
|
||||
}
|
||||
|
||||
if instPlan == nil {
|
||||
t.Fatalf("no plan for %s at all", addr)
|
||||
}
|
||||
|
||||
if got, want := instPlan.Addr, addr; !got.Equal(want) {
|
||||
t.Errorf("wrong current address\ngot: %s\nwant: %s", got, want)
|
||||
}
|
||||
if got, want := instPlan.PrevRunAddr, addr; !got.Equal(want) {
|
||||
t.Errorf("wrong previous run address\ngot: %s\nwant: %s", got, want)
|
||||
}
|
||||
if got, want := instPlan.Action, plans.Forget; got != want {
|
||||
t.Errorf("wrong planned action\ngot: %s\nwant: %s", got, want)
|
||||
}
|
||||
if got, want := instPlan.ActionReason, test.wantReason; got != want {
|
||||
t.Errorf("wrong action reason\ngot: %s\nwant: %s", got, want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestContext2Plan_removedModuleForgetsAllInstances(t *testing.T) {
|
||||
addrFirst := mustResourceInstanceAddr("module.mod[0].test_object.a")
|
||||
addrSecond := mustResourceInstanceAddr("module.mod[1].test_object.a")
|
||||
|
||||
m := testModuleInline(t, map[string]string{
|
||||
"main.tf": `
|
||||
removed {
|
||||
from = module.mod
|
||||
}
|
||||
`,
|
||||
})
|
||||
|
||||
state := states.BuildState(func(s *states.SyncState) {
|
||||
// The prior state tracks module.mod[0].test_object.a and
|
||||
// module.mod[1].test_object.a, which we should be removed
|
||||
// from the state by the "removed" block in the config.
|
||||
s.SetResourceInstanceCurrent(addrFirst, &states.ResourceInstanceObjectSrc{
|
||||
AttrsJSON: []byte(`{}`),
|
||||
Status: states.ObjectReady,
|
||||
}, mustProviderConfig(`provider["registry.opentofu.org/hashicorp/test"]`))
|
||||
s.SetResourceInstanceCurrent(addrSecond, &states.ResourceInstanceObjectSrc{
|
||||
AttrsJSON: []byte(`{}`),
|
||||
Status: states.ObjectReady,
|
||||
}, mustProviderConfig(`provider["registry.opentofu.org/hashicorp/test"]`))
|
||||
})
|
||||
|
||||
p := simpleMockProvider()
|
||||
ctx := testContext2(t, &ContextOpts{
|
||||
Providers: map[addrs.Provider]providers.Factory{
|
||||
addrs.NewDefaultProvider("test"): testProviderFuncFixed(p),
|
||||
},
|
||||
})
|
||||
|
||||
plan, diags := ctx.Plan(m, state, &PlanOpts{
|
||||
Mode: plans.NormalMode,
|
||||
ForceReplace: []addrs.AbsResourceInstance{
|
||||
addrFirst, addrSecond,
|
||||
},
|
||||
})
|
||||
if diags.HasErrors() {
|
||||
t.Fatalf("unexpected errors\n%s", diags.Err().Error())
|
||||
}
|
||||
|
||||
for _, resourceInstance := range []addrs.AbsResourceInstance{addrFirst, addrSecond} {
|
||||
t.Run(resourceInstance.String(), func(t *testing.T) {
|
||||
instPlan := plan.Changes.ResourceInstance(resourceInstance)
|
||||
if instPlan == nil {
|
||||
t.Fatalf("no plan for %s at all", resourceInstance)
|
||||
}
|
||||
|
||||
if got, want := instPlan.Addr, resourceInstance; !got.Equal(want) {
|
||||
t.Errorf("wrong current address\ngot: %s\nwant: %s", got, want)
|
||||
}
|
||||
if got, want := instPlan.PrevRunAddr, resourceInstance; !got.Equal(want) {
|
||||
t.Errorf("wrong previous run address\ngot: %s\nwant: %s", got, want)
|
||||
}
|
||||
if got, want := instPlan.Action, plans.Forget; got != want {
|
||||
t.Errorf("wrong planned action\ngot: %s\nwant: %s", got, want)
|
||||
}
|
||||
if got, want := instPlan.ActionReason, plans.ResourceInstanceDeleteBecauseNoResourceConfig; got != want {
|
||||
t.Errorf("wrong action reason\ngot: %s\nwant: %s", got, want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestContext2Plan_removedResourceForgetsAllInstances(t *testing.T) {
|
||||
addrFirst := mustResourceInstanceAddr("test_object.a[0]")
|
||||
addrSecond := mustResourceInstanceAddr("test_object.a[1]")
|
||||
|
||||
m := testModuleInline(t, map[string]string{
|
||||
"main.tf": `
|
||||
removed {
|
||||
from = test_object.a
|
||||
}
|
||||
`,
|
||||
})
|
||||
|
||||
state := states.BuildState(func(s *states.SyncState) {
|
||||
// The prior state tracks test_object.a[0] and
|
||||
// test_object.a[1], which we should be removed from
|
||||
// the state by the "removed" block in the config.
|
||||
s.SetResourceInstanceCurrent(addrFirst, &states.ResourceInstanceObjectSrc{
|
||||
AttrsJSON: []byte(`{}`),
|
||||
Status: states.ObjectReady,
|
||||
}, mustProviderConfig(`provider["registry.opentofu.org/hashicorp/test"]`))
|
||||
s.SetResourceInstanceCurrent(addrSecond, &states.ResourceInstanceObjectSrc{
|
||||
AttrsJSON: []byte(`{}`),
|
||||
Status: states.ObjectReady,
|
||||
}, mustProviderConfig(`provider["registry.opentofu.org/hashicorp/test"]`))
|
||||
})
|
||||
|
||||
p := simpleMockProvider()
|
||||
ctx := testContext2(t, &ContextOpts{
|
||||
Providers: map[addrs.Provider]providers.Factory{
|
||||
addrs.NewDefaultProvider("test"): testProviderFuncFixed(p),
|
||||
},
|
||||
})
|
||||
|
||||
plan, diags := ctx.Plan(m, state, &PlanOpts{
|
||||
Mode: plans.NormalMode,
|
||||
ForceReplace: []addrs.AbsResourceInstance{
|
||||
addrFirst, addrSecond,
|
||||
},
|
||||
})
|
||||
if diags.HasErrors() {
|
||||
t.Fatalf("unexpected errors\n%s", diags.Err().Error())
|
||||
}
|
||||
|
||||
for _, resourceInstance := range []addrs.AbsResourceInstance{addrFirst, addrSecond} {
|
||||
t.Run(resourceInstance.String(), func(t *testing.T) {
|
||||
instPlan := plan.Changes.ResourceInstance(resourceInstance)
|
||||
if instPlan == nil {
|
||||
t.Fatalf("no plan for %s at all", resourceInstance)
|
||||
}
|
||||
|
||||
if got, want := instPlan.Addr, resourceInstance; !got.Equal(want) {
|
||||
t.Errorf("wrong current address\ngot: %s\nwant: %s", got, want)
|
||||
}
|
||||
if got, want := instPlan.PrevRunAddr, resourceInstance; !got.Equal(want) {
|
||||
t.Errorf("wrong previous run address\ngot: %s\nwant: %s", got, want)
|
||||
}
|
||||
if got, want := instPlan.Action, plans.Forget; got != want {
|
||||
t.Errorf("wrong planned action\ngot: %s\nwant: %s", got, want)
|
||||
}
|
||||
if got, want := instPlan.ActionReason, plans.ResourceInstanceDeleteBecauseNoResourceConfig; got != want {
|
||||
t.Errorf("wrong action reason\ngot: %s\nwant: %s", got, want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestContext2Plan_removedResourceInChildModuleFromParentModule(t *testing.T) {
|
||||
addr := mustResourceInstanceAddr("module.mod.test_object.a")
|
||||
m := testModuleInline(t, map[string]string{
|
||||
"main.tf": `
|
||||
module "mod" {
|
||||
source = "./mod"
|
||||
}
|
||||
|
||||
removed {
|
||||
from = module.mod.test_object.a
|
||||
}
|
||||
`,
|
||||
"mod/main.tf": ``,
|
||||
})
|
||||
|
||||
state := states.BuildState(func(s *states.SyncState) {
|
||||
// The prior state tracks module.mod.test_object.a.a, which we should be
|
||||
// removed from the state by the "removed" block in the root config.
|
||||
s.SetResourceInstanceCurrent(addr, &states.ResourceInstanceObjectSrc{
|
||||
AttrsJSON: []byte(`{}`),
|
||||
Status: states.ObjectReady,
|
||||
}, mustProviderConfig(`provider["registry.opentofu.org/hashicorp/test"]`))
|
||||
})
|
||||
|
||||
p := simpleMockProvider()
|
||||
ctx := testContext2(t, &ContextOpts{
|
||||
Providers: map[addrs.Provider]providers.Factory{
|
||||
addrs.NewDefaultProvider("test"): testProviderFuncFixed(p),
|
||||
},
|
||||
})
|
||||
|
||||
plan, diags := ctx.Plan(m, state, &PlanOpts{
|
||||
Mode: plans.NormalMode,
|
||||
ForceReplace: []addrs.AbsResourceInstance{
|
||||
addr,
|
||||
},
|
||||
})
|
||||
if diags.HasErrors() {
|
||||
t.Fatalf("unexpected errors\n%s", diags.Err().Error())
|
||||
}
|
||||
|
||||
t.Run(addr.String(), func(t *testing.T) {
|
||||
instPlan := plan.Changes.ResourceInstance(addr)
|
||||
if instPlan == nil {
|
||||
t.Fatalf("no plan for %s at all", addr)
|
||||
}
|
||||
|
||||
if got, want := instPlan.Addr, addr; !got.Equal(want) {
|
||||
t.Errorf("wrong current address\ngot: %s\nwant: %s", got, want)
|
||||
}
|
||||
if got, want := instPlan.PrevRunAddr, addr; !got.Equal(want) {
|
||||
t.Errorf("wrong previous run address\ngot: %s\nwant: %s", got, want)
|
||||
}
|
||||
if got, want := instPlan.Action, plans.Forget; got != want {
|
||||
t.Errorf("wrong planned action\ngot: %s\nwant: %s", got, want)
|
||||
}
|
||||
if got, want := instPlan.ActionReason, plans.ResourceInstanceDeleteBecauseNoResourceConfig; got != want {
|
||||
t.Errorf("wrong action reason\ngot: %s\nwant: %s", got, want)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestContext2Plan_removedResourceInChildModuleFromChildModule(t *testing.T) {
|
||||
addr := mustResourceInstanceAddr("module.mod.test_object.a")
|
||||
m := testModuleInline(t, map[string]string{
|
||||
"main.tf": `
|
||||
module "mod" {
|
||||
source = "./mod"
|
||||
}
|
||||
`,
|
||||
"mod/main.tf": `
|
||||
removed {
|
||||
from = test_object.a
|
||||
}
|
||||
`,
|
||||
})
|
||||
|
||||
state := states.BuildState(func(s *states.SyncState) {
|
||||
// The prior state tracks module.mod.test_object.a.a, which we should be
|
||||
// removed from the state by the "removed" block in the child mofule config.
|
||||
s.SetResourceInstanceCurrent(addr, &states.ResourceInstanceObjectSrc{
|
||||
AttrsJSON: []byte(`{}`),
|
||||
Status: states.ObjectReady,
|
||||
}, mustProviderConfig(`provider["registry.opentofu.org/hashicorp/test"]`))
|
||||
})
|
||||
|
||||
p := simpleMockProvider()
|
||||
ctx := testContext2(t, &ContextOpts{
|
||||
Providers: map[addrs.Provider]providers.Factory{
|
||||
addrs.NewDefaultProvider("test"): testProviderFuncFixed(p),
|
||||
},
|
||||
})
|
||||
|
||||
plan, diags := ctx.Plan(m, state, &PlanOpts{
|
||||
Mode: plans.NormalMode,
|
||||
ForceReplace: []addrs.AbsResourceInstance{
|
||||
addr,
|
||||
},
|
||||
})
|
||||
if diags.HasErrors() {
|
||||
t.Fatalf("unexpected errors\n%s", diags.Err().Error())
|
||||
}
|
||||
|
||||
t.Run(addr.String(), func(t *testing.T) {
|
||||
instPlan := plan.Changes.ResourceInstance(addr)
|
||||
if instPlan == nil {
|
||||
t.Fatalf("no plan for %s at all", addr)
|
||||
}
|
||||
|
||||
if got, want := instPlan.Addr, addr; !got.Equal(want) {
|
||||
t.Errorf("wrong current address\ngot: %s\nwant: %s", got, want)
|
||||
}
|
||||
if got, want := instPlan.PrevRunAddr, addr; !got.Equal(want) {
|
||||
t.Errorf("wrong previous run address\ngot: %s\nwant: %s", got, want)
|
||||
}
|
||||
if got, want := instPlan.Action, plans.Forget; got != want {
|
||||
t.Errorf("wrong planned action\ngot: %s\nwant: %s", got, want)
|
||||
}
|
||||
if got, want := instPlan.ActionReason, plans.ResourceInstanceDeleteBecauseNoResourceConfig; got != want {
|
||||
t.Errorf("wrong action reason\ngot: %s\nwant: %s", got, want)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestContext2Plan_removedResourceInGrandchildModuleFromRootModule(t *testing.T) {
|
||||
addr := mustResourceInstanceAddr("module.child.module.grandchild.test_object.a")
|
||||
m := testModuleInline(t, map[string]string{
|
||||
"main.tf": `
|
||||
module "child" {
|
||||
source = "./child"
|
||||
}
|
||||
|
||||
removed {
|
||||
from = module.child.module.grandchild.test_object.a
|
||||
}
|
||||
`,
|
||||
"child/main.tf": ``,
|
||||
})
|
||||
|
||||
state := states.BuildState(func(s *states.SyncState) {
|
||||
// The prior state tracks module.child.module.grandchild.test_object.a,
|
||||
// which we should be removed from the state by the "removed" block in
|
||||
// the root config.
|
||||
s.SetResourceInstanceCurrent(addr, &states.ResourceInstanceObjectSrc{
|
||||
AttrsJSON: []byte(`{}`),
|
||||
Status: states.ObjectReady,
|
||||
}, mustProviderConfig(`provider["registry.opentofu.org/hashicorp/test"]`))
|
||||
})
|
||||
|
||||
p := simpleMockProvider()
|
||||
ctx := testContext2(t, &ContextOpts{
|
||||
Providers: map[addrs.Provider]providers.Factory{
|
||||
addrs.NewDefaultProvider("test"): testProviderFuncFixed(p),
|
||||
},
|
||||
})
|
||||
|
||||
plan, diags := ctx.Plan(m, state, &PlanOpts{
|
||||
Mode: plans.NormalMode,
|
||||
ForceReplace: []addrs.AbsResourceInstance{
|
||||
addr,
|
||||
},
|
||||
})
|
||||
if diags.HasErrors() {
|
||||
t.Fatalf("unexpected errors\n%s", diags.Err().Error())
|
||||
}
|
||||
|
||||
t.Run(addr.String(), func(t *testing.T) {
|
||||
instPlan := plan.Changes.ResourceInstance(addr)
|
||||
if instPlan == nil {
|
||||
t.Fatalf("no plan for %s at all", addr)
|
||||
}
|
||||
|
||||
if got, want := instPlan.Addr, addr; !got.Equal(want) {
|
||||
t.Errorf("wrong current address\ngot: %s\nwant: %s", got, want)
|
||||
}
|
||||
if got, want := instPlan.PrevRunAddr, addr; !got.Equal(want) {
|
||||
t.Errorf("wrong previous run address\ngot: %s\nwant: %s", got, want)
|
||||
}
|
||||
if got, want := instPlan.Action, plans.Forget; got != want {
|
||||
t.Errorf("wrong planned action\ngot: %s\nwant: %s", got, want)
|
||||
}
|
||||
if got, want := instPlan.ActionReason, plans.ResourceInstanceDeleteBecauseNoResourceConfig; got != want {
|
||||
t.Errorf("wrong action reason\ngot: %s\nwant: %s", got, want)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestContext2Plan_removedChildModuleForgetsResourceInGrandchildModule(t *testing.T) {
|
||||
addr := mustResourceInstanceAddr("module.child.module.grandchild.test_object.a")
|
||||
m := testModuleInline(t, map[string]string{
|
||||
"main.tf": `
|
||||
module "child" {
|
||||
source = "./child"
|
||||
}
|
||||
|
||||
removed {
|
||||
from = module.child.module.grandchild
|
||||
}
|
||||
`,
|
||||
"child/main.tf": ``,
|
||||
})
|
||||
|
||||
state := states.BuildState(func(s *states.SyncState) {
|
||||
// The prior state tracks module.child.module.grandchild.test_object.a,
|
||||
// which we should be removed from the state by the "removed" block
|
||||
// in the root config.
|
||||
s.SetResourceInstanceCurrent(addr, &states.ResourceInstanceObjectSrc{
|
||||
AttrsJSON: []byte(`{}`),
|
||||
Status: states.ObjectReady,
|
||||
}, mustProviderConfig(`provider["registry.opentofu.org/hashicorp/test"]`))
|
||||
})
|
||||
|
||||
p := simpleMockProvider()
|
||||
ctx := testContext2(t, &ContextOpts{
|
||||
Providers: map[addrs.Provider]providers.Factory{
|
||||
addrs.NewDefaultProvider("test"): testProviderFuncFixed(p),
|
||||
},
|
||||
})
|
||||
|
||||
plan, diags := ctx.Plan(m, state, &PlanOpts{
|
||||
Mode: plans.NormalMode,
|
||||
ForceReplace: []addrs.AbsResourceInstance{
|
||||
addr,
|
||||
},
|
||||
})
|
||||
if diags.HasErrors() {
|
||||
t.Fatalf("unexpected errors\n%s", diags.Err().Error())
|
||||
}
|
||||
|
||||
t.Run(addr.String(), func(t *testing.T) {
|
||||
instPlan := plan.Changes.ResourceInstance(addr)
|
||||
if instPlan == nil {
|
||||
t.Fatalf("no plan for %s at all", addr)
|
||||
}
|
||||
|
||||
if got, want := instPlan.Addr, addr; !got.Equal(want) {
|
||||
t.Errorf("wrong current address\ngot: %s\nwant: %s", got, want)
|
||||
}
|
||||
if got, want := instPlan.PrevRunAddr, addr; !got.Equal(want) {
|
||||
t.Errorf("wrong previous run address\ngot: %s\nwant: %s", got, want)
|
||||
}
|
||||
if got, want := instPlan.Action, plans.Forget; got != want {
|
||||
t.Errorf("wrong planned action\ngot: %s\nwant: %s", got, want)
|
||||
}
|
||||
if got, want := instPlan.ActionReason, plans.ResourceInstanceDeleteBecauseNoResourceConfig; got != want {
|
||||
t.Errorf("wrong action reason\ngot: %s\nwant: %s", got, want)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestContext2Plan_movedAndRemovedResourceAtTheSameTime(t *testing.T) {
|
||||
// This is the only scenario where the "moved" and "removed" blocks can
|
||||
// coexist while referencing the same resource. In this case, the "moved" logic
|
||||
// will run first, trying to move the resource to a non-existing target.
|
||||
// Usually ,it will cause the resource to be destroyed, but because the
|
||||
// "removed" block is also present, it will be removed from the state instead.
|
||||
addrA := mustResourceInstanceAddr("test_object.a")
|
||||
addrB := mustResourceInstanceAddr("test_object.b")
|
||||
m := testModuleInline(t, map[string]string{
|
||||
"main.tf": `
|
||||
removed {
|
||||
from = test_object.b
|
||||
}
|
||||
|
||||
moved {
|
||||
from = test_object.a
|
||||
to = test_object.b
|
||||
}
|
||||
`,
|
||||
})
|
||||
|
||||
state := states.BuildState(func(s *states.SyncState) {
|
||||
// The prior state tracks test_object.a, which we should treat as
|
||||
// test_object.b because of the "moved" block in the config.
|
||||
s.SetResourceInstanceCurrent(addrA, &states.ResourceInstanceObjectSrc{
|
||||
AttrsJSON: []byte(`{}`),
|
||||
Status: states.ObjectReady,
|
||||
}, mustProviderConfig(`provider["registry.opentofu.org/hashicorp/test"]`))
|
||||
})
|
||||
|
||||
p := simpleMockProvider()
|
||||
ctx := testContext2(t, &ContextOpts{
|
||||
Providers: map[addrs.Provider]providers.Factory{
|
||||
addrs.NewDefaultProvider("test"): testProviderFuncFixed(p),
|
||||
},
|
||||
})
|
||||
|
||||
plan, diags := ctx.Plan(m, state, &PlanOpts{
|
||||
Mode: plans.NormalMode,
|
||||
ForceReplace: []addrs.AbsResourceInstance{
|
||||
addrA,
|
||||
},
|
||||
})
|
||||
if diags.HasErrors() {
|
||||
t.Fatalf("unexpected errors\n%s", diags.Err().Error())
|
||||
}
|
||||
|
||||
t.Run(addrA.String(), func(t *testing.T) {
|
||||
instPlan := plan.Changes.ResourceInstance(addrA)
|
||||
if instPlan != nil {
|
||||
t.Fatalf("unexpected plan for %s; should've moved to %s", addrA, addrB)
|
||||
}
|
||||
})
|
||||
t.Run(addrB.String(), func(t *testing.T) {
|
||||
instPlan := plan.Changes.ResourceInstance(addrB)
|
||||
if instPlan == nil {
|
||||
t.Fatalf("no plan for %s at all", addrB)
|
||||
}
|
||||
|
||||
if got, want := instPlan.Addr, addrB; !got.Equal(want) {
|
||||
t.Errorf("wrong current address\ngot: %s\nwant: %s", got, want)
|
||||
}
|
||||
if got, want := instPlan.PrevRunAddr, addrA; !got.Equal(want) {
|
||||
t.Errorf("wrong previous run address\ngot: %s\nwant: %s", got, want)
|
||||
}
|
||||
if got, want := instPlan.Action, plans.Forget; got != want {
|
||||
t.Errorf("wrong planned action\ngot: %s\nwant: %s", got, want)
|
||||
}
|
||||
if got, want := instPlan.ActionReason, plans.ResourceInstanceDeleteBecauseNoMoveTarget; got != want {
|
||||
t.Errorf("wrong action reason\ngot: %s\nwant: %s", got, want)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestContext2Plan_removedResourceButResourceBlockStillExists(t *testing.T) {
|
||||
addr := mustResourceInstanceAddr("test_object.a")
|
||||
m := testModuleInline(t, map[string]string{
|
||||
"main.tf": `
|
||||
resource "test_object" "a" {
|
||||
test_string = "foo"
|
||||
}
|
||||
|
||||
removed {
|
||||
from = test_object.a
|
||||
}
|
||||
`,
|
||||
})
|
||||
|
||||
state := states.BuildState(func(s *states.SyncState) {
|
||||
s.SetResourceInstanceCurrent(addr, &states.ResourceInstanceObjectSrc{
|
||||
AttrsJSON: []byte(`{}`),
|
||||
Status: states.ObjectReady,
|
||||
}, mustProviderConfig(`provider["registry.opentofu.org/hashicorp/test"]`))
|
||||
})
|
||||
|
||||
p := simpleMockProvider()
|
||||
ctx := testContext2(t, &ContextOpts{
|
||||
Providers: map[addrs.Provider]providers.Factory{
|
||||
addrs.NewDefaultProvider("test"): testProviderFuncFixed(p),
|
||||
},
|
||||
})
|
||||
|
||||
_, diags := ctx.Plan(m, state, &PlanOpts{
|
||||
Mode: plans.NormalMode,
|
||||
ForceReplace: []addrs.AbsResourceInstance{
|
||||
addr,
|
||||
},
|
||||
})
|
||||
|
||||
if !diags.HasErrors() {
|
||||
t.Fatal("succeeded; want errors")
|
||||
}
|
||||
|
||||
if got, want := diags.Err().Error(), "Removed resource block still exists"; !strings.Contains(got, want) {
|
||||
t.Fatalf("wrong error:\ngot: %s\nwant: message containing %q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestContext2Plan_removedResourceButResourceBlockStillExistsInChildModule(t *testing.T) {
|
||||
addr := mustResourceInstanceAddr("module.mod.test_object.a")
|
||||
m := testModuleInline(t, map[string]string{
|
||||
"main.tf": `
|
||||
module "mod" {
|
||||
source = "./mod"
|
||||
}
|
||||
|
||||
removed {
|
||||
from = module.mod.test_object.a
|
||||
}
|
||||
`,
|
||||
"mod/main.tf": `
|
||||
resource "test_object" "a" {
|
||||
test_string = "foo"
|
||||
}
|
||||
`,
|
||||
})
|
||||
|
||||
state := states.BuildState(func(s *states.SyncState) {
|
||||
s.SetResourceInstanceCurrent(addr, &states.ResourceInstanceObjectSrc{
|
||||
AttrsJSON: []byte(`{}`),
|
||||
Status: states.ObjectReady,
|
||||
}, mustProviderConfig(`provider["registry.opentofu.org/hashicorp/test"]`))
|
||||
})
|
||||
|
||||
p := simpleMockProvider()
|
||||
ctx := testContext2(t, &ContextOpts{
|
||||
Providers: map[addrs.Provider]providers.Factory{
|
||||
addrs.NewDefaultProvider("test"): testProviderFuncFixed(p),
|
||||
},
|
||||
})
|
||||
|
||||
_, diags := ctx.Plan(m, state, &PlanOpts{
|
||||
Mode: plans.NormalMode,
|
||||
ForceReplace: []addrs.AbsResourceInstance{
|
||||
addr,
|
||||
},
|
||||
})
|
||||
|
||||
if !diags.HasErrors() {
|
||||
t.Fatal("succeeded; want errors")
|
||||
}
|
||||
|
||||
if got, want := diags.Err().Error(), "Removed resource block still exists"; !strings.Contains(got, want) {
|
||||
t.Fatalf("wrong error:\ngot: %s\nwant: message containing %q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestContext2Plan_removedModuleButModuleBlockStillExists(t *testing.T) {
|
||||
addr := mustResourceInstanceAddr("module.mod.test_object.a")
|
||||
m := testModuleInline(t, map[string]string{
|
||||
"main.tf": `
|
||||
module "mod" {
|
||||
source = "./mod"
|
||||
}
|
||||
|
||||
removed {
|
||||
from = module.mod
|
||||
}
|
||||
`,
|
||||
"mod/main.tf": `
|
||||
resource "test_object" "a" {
|
||||
test_string = "foo"
|
||||
}
|
||||
`,
|
||||
})
|
||||
|
||||
state := states.BuildState(func(s *states.SyncState) {
|
||||
s.SetResourceInstanceCurrent(addr, &states.ResourceInstanceObjectSrc{
|
||||
AttrsJSON: []byte(`{}`),
|
||||
Status: states.ObjectReady,
|
||||
}, mustProviderConfig(`provider["registry.opentofu.org/hashicorp/test"]`))
|
||||
})
|
||||
|
||||
p := simpleMockProvider()
|
||||
ctx := testContext2(t, &ContextOpts{
|
||||
Providers: map[addrs.Provider]providers.Factory{
|
||||
addrs.NewDefaultProvider("test"): testProviderFuncFixed(p),
|
||||
},
|
||||
})
|
||||
|
||||
_, diags := ctx.Plan(m, state, &PlanOpts{
|
||||
Mode: plans.NormalMode,
|
||||
ForceReplace: []addrs.AbsResourceInstance{
|
||||
addr,
|
||||
},
|
||||
})
|
||||
|
||||
if !diags.HasErrors() {
|
||||
t.Fatal("succeeded; want errors")
|
||||
}
|
||||
|
||||
if got, want := diags.Err().Error(), "Removed module block still exists"; !strings.Contains(got, want) {
|
||||
t.Fatalf("wrong error:\ngot: %s\nwant: message containing %q", got, want)
|
||||
}
|
||||
}
|
||||
|
@ -83,6 +83,10 @@ type PlanGraphBuilder struct {
|
||||
// ImportTargets are the list of resources to import.
|
||||
ImportTargets []*ImportTarget
|
||||
|
||||
// EndpointsToRemove are the list of resources and modules to forget from
|
||||
// the state.
|
||||
EndpointsToRemove []addrs.ConfigRemovable
|
||||
|
||||
// GenerateConfig tells OpenTofu where to write and generated config for
|
||||
// any import targets that do not already have configuration.
|
||||
//
|
||||
@ -266,6 +270,7 @@ func (b *PlanGraphBuilder) initPlan() {
|
||||
NodeAbstractResourceInstance: a,
|
||||
skipRefresh: b.skipRefresh,
|
||||
skipPlanChanges: b.skipPlanChanges,
|
||||
EndpointsToRemove: b.EndpointsToRemove,
|
||||
}
|
||||
}
|
||||
|
||||
@ -274,8 +279,9 @@ func (b *PlanGraphBuilder) initPlan() {
|
||||
NodeAbstractResourceInstance: a,
|
||||
DeposedKey: key,
|
||||
|
||||
skipRefresh: b.skipRefresh,
|
||||
skipPlanChanges: b.skipPlanChanges,
|
||||
skipRefresh: b.skipRefresh,
|
||||
skipPlanChanges: b.skipPlanChanges,
|
||||
EndpointsToRemove: b.EndpointsToRemove,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -348,6 +348,31 @@ func (n *NodeAbstractResourceInstance) writeResourceInstanceStateImpl(ctx EvalCo
|
||||
return nil
|
||||
}
|
||||
|
||||
// planForget returns a removed from state diff.
|
||||
func (n *NodeAbstractResourceInstance) planForget(ctx EvalContext, currentState *states.ResourceInstanceObject, deposedKey states.DeposedKey) *plans.ResourceInstanceChange {
|
||||
var plan *plans.ResourceInstanceChange
|
||||
|
||||
unmarkedPriorVal, _ := currentState.Value.UnmarkDeep()
|
||||
|
||||
// The config and new value are null to signify that this is a forget
|
||||
// operation.
|
||||
nullVal := cty.NullVal(unmarkedPriorVal.Type())
|
||||
|
||||
plan = &plans.ResourceInstanceChange{
|
||||
Addr: n.Addr,
|
||||
PrevRunAddr: n.prevRunAddr(ctx),
|
||||
DeposedKey: deposedKey,
|
||||
Change: plans.Change{
|
||||
Action: plans.Forget,
|
||||
Before: currentState.Value,
|
||||
After: nullVal,
|
||||
},
|
||||
ProviderAddr: n.ResolvedProvider,
|
||||
}
|
||||
|
||||
return plan
|
||||
}
|
||||
|
||||
// planDestroy returns a plain destroy diff.
|
||||
func (n *NodeAbstractResourceInstance) planDestroy(ctx EvalContext, currentState *states.ResourceInstanceObject, deposedKey states.DeposedKey) (*plans.ResourceInstanceChange, tfdiags.Diagnostics) {
|
||||
var diags tfdiags.Diagnostics
|
||||
|
@ -43,6 +43,12 @@ type NodePlanDeposedResourceInstanceObject struct {
|
||||
// skipPlanChanges indicates we should skip trying to plan change actions
|
||||
// for any instances.
|
||||
skipPlanChanges bool
|
||||
|
||||
// EndpointsToRemove are resource instance addresses where the user wants to
|
||||
// forget from the state. This set isn't pre-filtered, so
|
||||
// it might contain addresses that have nothing to do with the resource
|
||||
// that this node represents, which the node itself must therefore ignore.
|
||||
EndpointsToRemove []addrs.ConfigRemovable
|
||||
}
|
||||
|
||||
var (
|
||||
@ -132,8 +138,23 @@ func (n *NodePlanDeposedResourceInstanceObject) Execute(ctx EvalContext, op walk
|
||||
|
||||
if !n.skipPlanChanges {
|
||||
var change *plans.ResourceInstanceChange
|
||||
change, destroyPlanDiags := n.planDestroy(ctx, state, n.DeposedKey)
|
||||
diags = diags.Append(destroyPlanDiags)
|
||||
var planDiags tfdiags.Diagnostics
|
||||
|
||||
shouldForget := false
|
||||
|
||||
for _, etf := range n.EndpointsToRemove {
|
||||
if etf.TargetContains(n.Addr) {
|
||||
shouldForget = true
|
||||
}
|
||||
}
|
||||
|
||||
if shouldForget {
|
||||
change = n.planForget(ctx, state, n.DeposedKey)
|
||||
} else {
|
||||
change, planDiags = n.planDestroy(ctx, state, n.DeposedKey)
|
||||
}
|
||||
|
||||
diags = diags.Append(planDiags)
|
||||
if diags.HasErrors() {
|
||||
return diags
|
||||
}
|
||||
@ -333,3 +354,63 @@ func (n *NodeDestroyDeposedResourceInstanceObject) writeResourceInstanceState(ct
|
||||
state.SetResourceInstanceDeposed(absAddr, key, src, n.ResolvedProvider)
|
||||
return nil
|
||||
}
|
||||
|
||||
// NodeForgetDeposedResourceInstanceObject represents deposed resource
|
||||
// instance objects during apply. Nodes of this type are inserted by
|
||||
// DiffTransformer when the planned changeset contains "forget" changes for
|
||||
// deposed instance objects, and its only supported operation is to forget
|
||||
// the associated object from the state.
|
||||
type NodeForgetDeposedResourceInstanceObject struct {
|
||||
*NodeAbstractResourceInstance
|
||||
DeposedKey states.DeposedKey
|
||||
}
|
||||
|
||||
var (
|
||||
_ GraphNodeDeposedResourceInstanceObject = (*NodeForgetDeposedResourceInstanceObject)(nil)
|
||||
_ GraphNodeConfigResource = (*NodeForgetDeposedResourceInstanceObject)(nil)
|
||||
_ GraphNodeResourceInstance = (*NodeForgetDeposedResourceInstanceObject)(nil)
|
||||
_ GraphNodeReferenceable = (*NodeForgetDeposedResourceInstanceObject)(nil)
|
||||
_ GraphNodeReferencer = (*NodeForgetDeposedResourceInstanceObject)(nil)
|
||||
_ GraphNodeExecutable = (*NodeForgetDeposedResourceInstanceObject)(nil)
|
||||
_ GraphNodeProviderConsumer = (*NodeForgetDeposedResourceInstanceObject)(nil)
|
||||
_ GraphNodeProvisionerConsumer = (*NodeForgetDeposedResourceInstanceObject)(nil)
|
||||
)
|
||||
|
||||
func (n *NodeForgetDeposedResourceInstanceObject) Name() string {
|
||||
return fmt.Sprintf("%s (forget deposed %s)", n.ResourceInstanceAddr(), n.DeposedKey)
|
||||
}
|
||||
|
||||
func (n *NodeForgetDeposedResourceInstanceObject) DeposedInstanceObjectKey() states.DeposedKey {
|
||||
return n.DeposedKey
|
||||
}
|
||||
|
||||
// GraphNodeReferenceable implementation, overriding the one from NodeAbstractResourceInstance
|
||||
func (n *NodeForgetDeposedResourceInstanceObject) ReferenceableAddrs() []addrs.Referenceable {
|
||||
// Deposed objects don't participate in references.
|
||||
return nil
|
||||
}
|
||||
|
||||
// GraphNodeReferencer implementation, overriding the one from NodeAbstractResourceInstance
|
||||
func (n *NodeForgetDeposedResourceInstanceObject) References() []*addrs.Reference {
|
||||
// We don't evaluate configuration for deposed objects, so they effectively
|
||||
// make no references.
|
||||
return nil
|
||||
}
|
||||
|
||||
// GraphNodeExecutable impl.
|
||||
func (n *NodeForgetDeposedResourceInstanceObject) Execute(ctx EvalContext, op walkOperation) (diags tfdiags.Diagnostics) {
|
||||
// Read the state for the deposed resource instance
|
||||
state, err := n.readResourceInstanceStateDeposed(ctx, n.Addr, n.DeposedKey)
|
||||
if err != nil {
|
||||
return diags.Append(err)
|
||||
}
|
||||
|
||||
if state == nil {
|
||||
log.Printf("[WARN] NodeForgetDeposedResourceInstanceObject for %s (%s) with no state", n.Addr, n.DeposedKey)
|
||||
}
|
||||
|
||||
contextState := ctx.State()
|
||||
contextState.ForgetResourceInstanceDeposed(n.Addr, n.DeposedKey)
|
||||
|
||||
return diags.Append(updateStateHook(ctx))
|
||||
}
|
@ -17,120 +17,118 @@ import (
|
||||
)
|
||||
|
||||
func TestNodePlanDeposedResourceInstanceObject_Execute(t *testing.T) {
|
||||
deposedKey := states.NewDeposedKey()
|
||||
state := states.NewState()
|
||||
absResource := mustResourceInstanceAddr("test_instance.foo")
|
||||
state.Module(addrs.RootModuleInstance).SetResourceInstanceDeposed(
|
||||
absResource.Resource,
|
||||
deposedKey,
|
||||
&states.ResourceInstanceObjectSrc{
|
||||
Status: states.ObjectTainted,
|
||||
AttrsJSON: []byte(`{"id":"bar"}`),
|
||||
tests := []struct {
|
||||
description string
|
||||
nodeAddress string
|
||||
nodeEndpointsToRemove []addrs.ConfigRemovable
|
||||
wantAction plans.Action
|
||||
}{
|
||||
{
|
||||
nodeAddress: "test_instance.foo",
|
||||
nodeEndpointsToRemove: make([]addrs.ConfigRemovable, 0),
|
||||
wantAction: plans.Delete,
|
||||
},
|
||||
{
|
||||
nodeAddress: "test_instance.foo",
|
||||
nodeEndpointsToRemove: []addrs.ConfigRemovable{
|
||||
interface{}(mustConfigResourceAddr("test_instance.bar")).(addrs.ConfigRemovable),
|
||||
},
|
||||
wantAction: plans.Delete,
|
||||
},
|
||||
{
|
||||
nodeAddress: "test_instance.foo",
|
||||
nodeEndpointsToRemove: []addrs.ConfigRemovable{
|
||||
interface{}(addrs.Module{"boop"}).(addrs.ConfigRemovable),
|
||||
},
|
||||
wantAction: plans.Delete,
|
||||
},
|
||||
{
|
||||
nodeAddress: "test_instance.foo",
|
||||
nodeEndpointsToRemove: []addrs.ConfigRemovable{
|
||||
interface{}(mustConfigResourceAddr("test_instance.foo")).(addrs.ConfigRemovable),
|
||||
},
|
||||
wantAction: plans.Forget,
|
||||
},
|
||||
{
|
||||
nodeAddress: "test_instance.foo[1]",
|
||||
nodeEndpointsToRemove: []addrs.ConfigRemovable{
|
||||
interface{}(mustConfigResourceAddr("test_instance.foo")).(addrs.ConfigRemovable),
|
||||
},
|
||||
wantAction: plans.Forget,
|
||||
},
|
||||
{
|
||||
nodeAddress: "module.boop.test_instance.foo",
|
||||
nodeEndpointsToRemove: []addrs.ConfigRemovable{
|
||||
interface{}(mustConfigResourceAddr("module.boop.test_instance.foo")).(addrs.ConfigRemovable),
|
||||
},
|
||||
wantAction: plans.Forget,
|
||||
},
|
||||
{
|
||||
nodeAddress: "module.boop[1].test_instance.foo[1]",
|
||||
nodeEndpointsToRemove: []addrs.ConfigRemovable{
|
||||
interface{}(mustConfigResourceAddr("module.boop.test_instance.foo")).(addrs.ConfigRemovable),
|
||||
},
|
||||
wantAction: plans.Forget,
|
||||
},
|
||||
{
|
||||
nodeAddress: "module.boop.test_instance.foo",
|
||||
nodeEndpointsToRemove: []addrs.ConfigRemovable{
|
||||
interface{}(addrs.Module{"boop"}).(addrs.ConfigRemovable),
|
||||
},
|
||||
wantAction: plans.Forget,
|
||||
},
|
||||
{
|
||||
nodeAddress: "module.boop[1].test_instance.foo",
|
||||
nodeEndpointsToRemove: []addrs.ConfigRemovable{
|
||||
interface{}(addrs.Module{"boop"}).(addrs.ConfigRemovable),
|
||||
},
|
||||
wantAction: plans.Forget,
|
||||
},
|
||||
mustProviderConfig(`provider["registry.opentofu.org/hashicorp/test"]`),
|
||||
)
|
||||
|
||||
p := testProvider("test")
|
||||
p.ConfigureProvider(providers.ConfigureProviderRequest{})
|
||||
p.UpgradeResourceStateResponse = &providers.UpgradeResourceStateResponse{
|
||||
UpgradedState: cty.ObjectVal(map[string]cty.Value{
|
||||
"id": cty.StringVal("bar"),
|
||||
}),
|
||||
}
|
||||
ctx := &MockEvalContext{
|
||||
StateState: state.SyncWrapper(),
|
||||
PrevRunStateState: state.DeepCopy().SyncWrapper(),
|
||||
RefreshStateState: state.DeepCopy().SyncWrapper(),
|
||||
ProviderProvider: p,
|
||||
ProviderSchemaSchema: providers.ProviderSchema{
|
||||
ResourceTypes: map[string]providers.Schema{
|
||||
"test_instance": {
|
||||
Block: &configschema.Block{
|
||||
Attributes: map[string]*configschema.Attribute{
|
||||
"id": {
|
||||
Type: cty.String,
|
||||
Computed: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
for _, test := range tests {
|
||||
deposedKey := states.NewDeposedKey()
|
||||
absResource := mustResourceInstanceAddr(test.nodeAddress)
|
||||
|
||||
ctx, p := initMockEvalContext(test.nodeAddress, deposedKey)
|
||||
|
||||
node := NodePlanDeposedResourceInstanceObject{
|
||||
NodeAbstractResourceInstance: &NodeAbstractResourceInstance{
|
||||
Addr: absResource,
|
||||
NodeAbstractResource: NodeAbstractResource{
|
||||
ResolvedProvider: mustProviderConfig(`provider["registry.opentofu.org/hashicorp/test"]`),
|
||||
},
|
||||
},
|
||||
},
|
||||
ChangesChanges: plans.NewChanges().SyncWrapper(),
|
||||
}
|
||||
DeposedKey: deposedKey,
|
||||
EndpointsToRemove: test.nodeEndpointsToRemove,
|
||||
}
|
||||
|
||||
node := NodePlanDeposedResourceInstanceObject{
|
||||
NodeAbstractResourceInstance: &NodeAbstractResourceInstance{
|
||||
Addr: absResource,
|
||||
NodeAbstractResource: NodeAbstractResource{
|
||||
ResolvedProvider: mustProviderConfig(`provider["registry.opentofu.org/hashicorp/test"]`),
|
||||
},
|
||||
},
|
||||
DeposedKey: deposedKey,
|
||||
}
|
||||
err := node.Execute(ctx, walkPlan)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %s", err)
|
||||
}
|
||||
err := node.Execute(ctx, walkPlan)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %s", err)
|
||||
}
|
||||
|
||||
if !p.UpgradeResourceStateCalled {
|
||||
t.Errorf("UpgradeResourceState wasn't called; should've been called to upgrade the previous run's object")
|
||||
}
|
||||
if !p.ReadResourceCalled {
|
||||
t.Errorf("ReadResource wasn't called; should've been called to refresh the deposed object")
|
||||
}
|
||||
if !p.UpgradeResourceStateCalled {
|
||||
t.Errorf("UpgradeResourceState wasn't called; should've been called to upgrade the previous run's object")
|
||||
}
|
||||
if !p.ReadResourceCalled {
|
||||
t.Errorf("ReadResource wasn't called; should've been called to refresh the deposed object")
|
||||
}
|
||||
|
||||
change := ctx.Changes().GetResourceInstanceChange(absResource, deposedKey)
|
||||
if got, want := change.ChangeSrc.Action, plans.Delete; got != want {
|
||||
t.Fatalf("wrong planned action\ngot: %s\nwant: %s", got, want)
|
||||
change := ctx.Changes().GetResourceInstanceChange(absResource, deposedKey)
|
||||
if got, want := change.ChangeSrc.Action, test.wantAction; got != want {
|
||||
t.Fatalf("wrong planned action\ngot: %s\nwant: %s", got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestNodeDestroyDeposedResourceInstanceObject_Execute(t *testing.T) {
|
||||
deposedKey := states.NewDeposedKey()
|
||||
state := states.NewState()
|
||||
absResource := mustResourceInstanceAddr("test_instance.foo")
|
||||
state.Module(addrs.RootModuleInstance).SetResourceInstanceDeposed(
|
||||
absResource.Resource,
|
||||
deposedKey,
|
||||
&states.ResourceInstanceObjectSrc{
|
||||
Status: states.ObjectTainted,
|
||||
AttrsJSON: []byte(`{"id":"bar"}`),
|
||||
},
|
||||
mustProviderConfig(`provider["registry.opentofu.org/hashicorp/test"]`),
|
||||
)
|
||||
|
||||
schema := providers.ProviderSchema{
|
||||
ResourceTypes: map[string]providers.Schema{
|
||||
"test_instance": {
|
||||
Block: &configschema.Block{
|
||||
Attributes: map[string]*configschema.Attribute{
|
||||
"id": {
|
||||
Type: cty.String,
|
||||
Computed: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
p := testProvider("test")
|
||||
p.ConfigureProvider(providers.ConfigureProviderRequest{})
|
||||
p.GetProviderSchemaResponse = &schema
|
||||
|
||||
p.UpgradeResourceStateResponse = &providers.UpgradeResourceStateResponse{
|
||||
UpgradedState: cty.ObjectVal(map[string]cty.Value{
|
||||
"id": cty.StringVal("bar"),
|
||||
}),
|
||||
}
|
||||
ctx := &MockEvalContext{
|
||||
StateState: state.SyncWrapper(),
|
||||
ProviderProvider: p,
|
||||
ProviderSchemaSchema: schema,
|
||||
ChangesChanges: plans.NewChanges().SyncWrapper(),
|
||||
}
|
||||
absResourceAddr := "test_instance.foo"
|
||||
ctx, _ := initMockEvalContext(absResourceAddr, deposedKey)
|
||||
|
||||
absResource := mustResourceInstanceAddr(absResourceAddr)
|
||||
node := NodeDestroyDeposedResourceInstanceObject{
|
||||
NodeAbstractResourceInstance: &NodeAbstractResourceInstance{
|
||||
Addr: absResource,
|
||||
@ -219,3 +217,82 @@ func TestNodeDestroyDeposedResourceInstanceObject_ExecuteMissingState(t *testing
|
||||
t.Fatal("expected error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNodeForgetDeposedResourceInstanceObject_Execute(t *testing.T) {
|
||||
deposedKey := states.NewDeposedKey()
|
||||
state := states.NewState()
|
||||
absResourceAddr := "test_instance.foo"
|
||||
ctx, _ := initMockEvalContext(absResourceAddr, deposedKey)
|
||||
|
||||
absResource := mustResourceInstanceAddr(absResourceAddr)
|
||||
node := NodeForgetDeposedResourceInstanceObject{
|
||||
NodeAbstractResourceInstance: &NodeAbstractResourceInstance{
|
||||
Addr: absResource,
|
||||
NodeAbstractResource: NodeAbstractResource{
|
||||
ResolvedProvider: mustProviderConfig(`provider["registry.opentofu.org/hashicorp/test"]`),
|
||||
},
|
||||
},
|
||||
DeposedKey: deposedKey,
|
||||
}
|
||||
err := node.Execute(ctx, walkApply)
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %s", err)
|
||||
}
|
||||
|
||||
if !state.Empty() {
|
||||
t.Fatalf("resources left in state after forget")
|
||||
}
|
||||
}
|
||||
|
||||
func initMockEvalContext(resourceAddrs string, deposedKey states.DeposedKey) (*MockEvalContext, *MockProvider) {
|
||||
state := states.NewState()
|
||||
absResource := mustResourceInstanceAddr(resourceAddrs)
|
||||
|
||||
if !absResource.Module.Module().Equal(addrs.RootModule) {
|
||||
state.EnsureModule(addrs.RootModuleInstance.Child(absResource.Module[0].Name, absResource.Module[0].InstanceKey))
|
||||
}
|
||||
|
||||
state.Module(absResource.Module).SetResourceInstanceDeposed(
|
||||
absResource.Resource,
|
||||
deposedKey,
|
||||
&states.ResourceInstanceObjectSrc{
|
||||
Status: states.ObjectTainted,
|
||||
AttrsJSON: []byte(`{"id":"bar"}`),
|
||||
},
|
||||
mustProviderConfig(`provider["registry.opentofu.org/hashicorp/test"]`),
|
||||
)
|
||||
|
||||
schema := providers.ProviderSchema{
|
||||
ResourceTypes: map[string]providers.Schema{
|
||||
"test_instance": {
|
||||
Block: &configschema.Block{
|
||||
Attributes: map[string]*configschema.Attribute{
|
||||
"id": {
|
||||
Type: cty.String,
|
||||
Computed: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
p := testProvider("test")
|
||||
p.ConfigureProvider(providers.ConfigureProviderRequest{})
|
||||
p.GetProviderSchemaResponse = &schema
|
||||
|
||||
p.UpgradeResourceStateResponse = &providers.UpgradeResourceStateResponse{
|
||||
UpgradedState: cty.ObjectVal(map[string]cty.Value{
|
||||
"id": cty.StringVal("bar"),
|
||||
}),
|
||||
}
|
||||
return &MockEvalContext{
|
||||
PrevRunStateState: state.DeepCopy().SyncWrapper(),
|
||||
RefreshStateState: state.DeepCopy().SyncWrapper(),
|
||||
StateState: state.SyncWrapper(),
|
||||
ProviderProvider: p,
|
||||
ProviderSchemaSchema: schema,
|
||||
ChangesChanges: plans.NewChanges().SyncWrapper(),
|
||||
}, p
|
||||
}
|
75
internal/tofu/node_resource_forget.go
Normal file
75
internal/tofu/node_resource_forget.go
Normal file
@ -0,0 +1,75 @@
|
||||
// Copyright (c) The OpenTofu Authors
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
// Copyright (c) 2023 HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package tofu
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
"github.com/opentofu/opentofu/internal/tfdiags"
|
||||
|
||||
"github.com/opentofu/opentofu/internal/states"
|
||||
)
|
||||
|
||||
// NodeForgetResourceInstance represents a resource instance that is to be
|
||||
// forgotten from the state.
|
||||
type NodeForgetResourceInstance struct {
|
||||
*NodeAbstractResourceInstance
|
||||
|
||||
// If DeposedKey is set to anything other than states.NotDeposed then
|
||||
// this node forgets a deposed object of the associated instance
|
||||
// rather than its current object.
|
||||
DeposedKey states.DeposedKey
|
||||
}
|
||||
|
||||
var (
|
||||
_ GraphNodeModuleInstance = (*NodeForgetResourceInstance)(nil)
|
||||
_ GraphNodeConfigResource = (*NodeForgetResourceInstance)(nil)
|
||||
_ GraphNodeResourceInstance = (*NodeForgetResourceInstance)(nil)
|
||||
_ GraphNodeReferenceable = (*NodeForgetResourceInstance)(nil)
|
||||
_ GraphNodeReferencer = (*NodeForgetResourceInstance)(nil)
|
||||
_ GraphNodeExecutable = (*NodeForgetResourceInstance)(nil)
|
||||
_ GraphNodeProviderConsumer = (*NodeForgetResourceInstance)(nil)
|
||||
_ GraphNodeProvisionerConsumer = (*NodeForgetResourceInstance)(nil)
|
||||
)
|
||||
|
||||
func (n *NodeForgetResourceInstance) Name() string {
|
||||
if n.DeposedKey != states.NotDeposed {
|
||||
return fmt.Sprintf("%s (forget deposed %s)", n.ResourceInstanceAddr(), n.DeposedKey)
|
||||
}
|
||||
return n.ResourceInstanceAddr().String() + " (forget)"
|
||||
}
|
||||
|
||||
// GraphNodeExecutable
|
||||
func (n *NodeForgetResourceInstance) Execute(ctx EvalContext, op walkOperation) (diags tfdiags.Diagnostics) {
|
||||
addr := n.ResourceInstanceAddr()
|
||||
|
||||
// Get our state
|
||||
is := n.instanceState
|
||||
if is == nil {
|
||||
log.Printf("[WARN] NodeForgetResourceInstance for %s with no state", addr)
|
||||
}
|
||||
|
||||
var state *states.ResourceInstanceObject
|
||||
|
||||
state, readDiags := n.readResourceInstanceState(ctx, addr)
|
||||
diags = diags.Append(readDiags)
|
||||
if diags.HasErrors() {
|
||||
return diags
|
||||
}
|
||||
|
||||
// Exit early if the state object is null after reading the state
|
||||
if state == nil || state.Value.IsNull() {
|
||||
return diags
|
||||
}
|
||||
|
||||
contextState := ctx.State()
|
||||
contextState.ForgetResourceInstanceAll(n.Addr)
|
||||
|
||||
diags = diags.Append(updateStateHook(ctx))
|
||||
|
||||
return diags
|
||||
}
|
@ -26,6 +26,12 @@ type NodePlannableResourceInstanceOrphan struct {
|
||||
// skipPlanChanges indicates we should skip trying to plan change actions
|
||||
// for any instances.
|
||||
skipPlanChanges bool
|
||||
|
||||
// EndpointsToRemove are resource instance addresses where the user wants to
|
||||
// forget from the state. This set isn't pre-filtered, so
|
||||
// it might contain addresses that have nothing to do with the resource
|
||||
// that this node represents, which the node itself must therefore ignore.
|
||||
EndpointsToRemove []addrs.ConfigRemovable
|
||||
}
|
||||
|
||||
var (
|
||||
@ -135,8 +141,23 @@ func (n *NodePlannableResourceInstanceOrphan) managedResourceExecute(ctx EvalCon
|
||||
}
|
||||
|
||||
var change *plans.ResourceInstanceChange
|
||||
change, destroyPlanDiags := n.planDestroy(ctx, oldState, "")
|
||||
diags = diags.Append(destroyPlanDiags)
|
||||
var planDiags tfdiags.Diagnostics
|
||||
|
||||
shouldForget := false
|
||||
|
||||
for _, etf := range n.EndpointsToRemove {
|
||||
if etf.TargetContains(n.Addr) {
|
||||
shouldForget = true
|
||||
}
|
||||
}
|
||||
|
||||
if shouldForget {
|
||||
change = n.planForget(ctx, oldState, "")
|
||||
} else {
|
||||
change, planDiags = n.planDestroy(ctx, oldState, "")
|
||||
}
|
||||
|
||||
diags = diags.Append(planDiags)
|
||||
if diags.HasErrors() {
|
||||
return diags
|
||||
}
|
||||
|
@ -8,6 +8,8 @@ package tofu
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/opentofu/opentofu/internal/configs/configschema"
|
||||
|
||||
"github.com/opentofu/opentofu/internal/addrs"
|
||||
"github.com/opentofu/opentofu/internal/instances"
|
||||
"github.com/opentofu/opentofu/internal/plans"
|
||||
@ -16,61 +18,153 @@ import (
|
||||
"github.com/zclconf/go-cty/cty"
|
||||
)
|
||||
|
||||
func TestNodeResourcePlanOrphanExecute(t *testing.T) {
|
||||
state := states.NewState()
|
||||
state.Module(addrs.RootModuleInstance).SetResourceInstanceCurrent(
|
||||
addrs.Resource{
|
||||
Mode: addrs.ManagedResourceMode,
|
||||
Type: "test_object",
|
||||
Name: "foo",
|
||||
}.Instance(addrs.NoKey),
|
||||
&states.ResourceInstanceObjectSrc{
|
||||
AttrsFlat: map[string]string{
|
||||
"test_string": "foo",
|
||||
func TestNodeResourcePlanOrphan_Execute(t *testing.T) {
|
||||
tests := []struct {
|
||||
description string
|
||||
nodeAddress string
|
||||
nodeEndpointsToRemove []addrs.ConfigRemovable
|
||||
wantAction plans.Action
|
||||
}{
|
||||
{
|
||||
nodeAddress: "test_instance.foo",
|
||||
nodeEndpointsToRemove: make([]addrs.ConfigRemovable, 0),
|
||||
wantAction: plans.Delete,
|
||||
},
|
||||
{
|
||||
nodeAddress: "test_instance.foo",
|
||||
nodeEndpointsToRemove: []addrs.ConfigRemovable{
|
||||
interface{}(mustConfigResourceAddr("test_instance.bar")).(addrs.ConfigRemovable),
|
||||
},
|
||||
Status: states.ObjectReady,
|
||||
wantAction: plans.Delete,
|
||||
},
|
||||
addrs.AbsProviderConfig{
|
||||
Provider: addrs.NewDefaultProvider("test"),
|
||||
Module: addrs.RootModule,
|
||||
{
|
||||
nodeAddress: "test_instance.foo",
|
||||
nodeEndpointsToRemove: []addrs.ConfigRemovable{
|
||||
interface{}(addrs.Module{"boop"}).(addrs.ConfigRemovable),
|
||||
},
|
||||
wantAction: plans.Delete,
|
||||
},
|
||||
)
|
||||
{
|
||||
nodeAddress: "test_instance.foo",
|
||||
nodeEndpointsToRemove: []addrs.ConfigRemovable{
|
||||
interface{}(mustConfigResourceAddr("test_instance.foo")).(addrs.ConfigRemovable),
|
||||
},
|
||||
wantAction: plans.Forget,
|
||||
},
|
||||
{
|
||||
nodeAddress: "test_instance.foo[1]",
|
||||
nodeEndpointsToRemove: []addrs.ConfigRemovable{
|
||||
interface{}(mustConfigResourceAddr("test_instance.foo")).(addrs.ConfigRemovable),
|
||||
},
|
||||
wantAction: plans.Forget,
|
||||
},
|
||||
{
|
||||
nodeAddress: "module.boop.test_instance.foo",
|
||||
nodeEndpointsToRemove: []addrs.ConfigRemovable{
|
||||
interface{}(mustConfigResourceAddr("module.boop.test_instance.foo")).(addrs.ConfigRemovable),
|
||||
},
|
||||
wantAction: plans.Forget,
|
||||
},
|
||||
{
|
||||
nodeAddress: "module.boop[1].test_instance.foo[1]",
|
||||
nodeEndpointsToRemove: []addrs.ConfigRemovable{
|
||||
interface{}(mustConfigResourceAddr("module.boop.test_instance.foo")).(addrs.ConfigRemovable),
|
||||
},
|
||||
wantAction: plans.Forget,
|
||||
},
|
||||
{
|
||||
nodeAddress: "module.boop.test_instance.foo",
|
||||
nodeEndpointsToRemove: []addrs.ConfigRemovable{
|
||||
interface{}(addrs.Module{"boop"}).(addrs.ConfigRemovable),
|
||||
},
|
||||
wantAction: plans.Forget,
|
||||
},
|
||||
{
|
||||
nodeAddress: "module.boop[1].test_instance.foo",
|
||||
nodeEndpointsToRemove: []addrs.ConfigRemovable{
|
||||
interface{}(addrs.Module{"boop"}).(addrs.ConfigRemovable),
|
||||
},
|
||||
wantAction: plans.Forget,
|
||||
},
|
||||
}
|
||||
|
||||
p := simpleMockProvider()
|
||||
p.ConfigureProvider(providers.ConfigureProviderRequest{})
|
||||
ctx := &MockEvalContext{
|
||||
StateState: state.SyncWrapper(),
|
||||
RefreshStateState: state.DeepCopy().SyncWrapper(),
|
||||
PrevRunStateState: state.DeepCopy().SyncWrapper(),
|
||||
InstanceExpanderExpander: instances.NewExpander(),
|
||||
ProviderProvider: p,
|
||||
ProviderSchemaSchema: providers.ProviderSchema{
|
||||
for _, test := range tests {
|
||||
state := states.NewState()
|
||||
absResource := mustResourceInstanceAddr(test.nodeAddress)
|
||||
|
||||
if !absResource.Module.Module().Equal(addrs.RootModule) {
|
||||
state.EnsureModule(addrs.RootModuleInstance.Child(absResource.Module[0].Name, absResource.Module[0].InstanceKey))
|
||||
}
|
||||
|
||||
state.Module(absResource.Module).SetResourceInstanceCurrent(
|
||||
absResource.Resource,
|
||||
&states.ResourceInstanceObjectSrc{
|
||||
AttrsFlat: map[string]string{
|
||||
"test_string": "foo",
|
||||
},
|
||||
Status: states.ObjectReady,
|
||||
},
|
||||
addrs.AbsProviderConfig{
|
||||
Provider: addrs.NewDefaultProvider("test"),
|
||||
Module: addrs.RootModule,
|
||||
},
|
||||
)
|
||||
|
||||
schema := providers.ProviderSchema{
|
||||
ResourceTypes: map[string]providers.Schema{
|
||||
"test_object": {
|
||||
Block: simpleTestSchema(),
|
||||
"test_instance": {
|
||||
Block: &configschema.Block{
|
||||
Attributes: map[string]*configschema.Attribute{
|
||||
"id": {
|
||||
Type: cty.String,
|
||||
Computed: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
ChangesChanges: plans.NewChanges().SyncWrapper(),
|
||||
}
|
||||
}
|
||||
|
||||
node := NodePlannableResourceInstanceOrphan{
|
||||
NodeAbstractResourceInstance: &NodeAbstractResourceInstance{
|
||||
NodeAbstractResource: NodeAbstractResource{
|
||||
ResolvedProvider: addrs.AbsProviderConfig{
|
||||
Provider: addrs.NewDefaultProvider("test"),
|
||||
Module: addrs.RootModule,
|
||||
p := simpleMockProvider()
|
||||
p.ConfigureProvider(providers.ConfigureProviderRequest{})
|
||||
p.GetProviderSchemaResponse = &schema
|
||||
|
||||
ctx := &MockEvalContext{
|
||||
StateState: state.SyncWrapper(),
|
||||
RefreshStateState: state.DeepCopy().SyncWrapper(),
|
||||
PrevRunStateState: state.DeepCopy().SyncWrapper(),
|
||||
InstanceExpanderExpander: instances.NewExpander(),
|
||||
ProviderProvider: p,
|
||||
ProviderSchemaSchema: schema,
|
||||
ChangesChanges: plans.NewChanges().SyncWrapper(),
|
||||
}
|
||||
|
||||
node := NodePlannableResourceInstanceOrphan{
|
||||
NodeAbstractResourceInstance: &NodeAbstractResourceInstance{
|
||||
NodeAbstractResource: NodeAbstractResource{
|
||||
ResolvedProvider: addrs.AbsProviderConfig{
|
||||
Provider: addrs.NewDefaultProvider("test"),
|
||||
Module: addrs.RootModule,
|
||||
},
|
||||
},
|
||||
Addr: absResource,
|
||||
},
|
||||
Addr: mustResourceInstanceAddr("test_object.foo"),
|
||||
},
|
||||
}
|
||||
diags := node.Execute(ctx, walkApply)
|
||||
if diags.HasErrors() {
|
||||
t.Fatalf("unexpected error: %s", diags.Err())
|
||||
}
|
||||
if !state.Empty() {
|
||||
t.Fatalf("expected empty state, got %s", state.String())
|
||||
EndpointsToRemove: test.nodeEndpointsToRemove,
|
||||
}
|
||||
|
||||
err := node.Execute(ctx, walkPlan)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %s", err)
|
||||
}
|
||||
|
||||
change := ctx.Changes().GetResourceInstanceChange(absResource, states.NotDeposed)
|
||||
if got, want := change.ChangeSrc.Action, test.wantAction; got != want {
|
||||
t.Fatalf("wrong planned action\ngot: %s\nwant: %s", got, want)
|
||||
}
|
||||
|
||||
if !state.Empty() {
|
||||
t.Fatalf("expected empty state, got %s", state.String())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -91,7 +91,7 @@ func (t *DiffTransformer) Transform(g *Graph) error {
|
||||
// Depending on the action we'll need some different combinations of
|
||||
// nodes, because destroying uses a special node type separate from
|
||||
// other actions.
|
||||
var update, delete, createBeforeDestroy bool
|
||||
var update, delete, forget, createBeforeDestroy bool
|
||||
switch rc.Action {
|
||||
case plans.NoOp:
|
||||
// For a no-op change we don't take any action but we still
|
||||
@ -101,6 +101,8 @@ func (t *DiffTransformer) Transform(g *Graph) error {
|
||||
update = t.hasConfigConditions(addr)
|
||||
case plans.Delete:
|
||||
delete = true
|
||||
case plans.Forget:
|
||||
forget = true
|
||||
case plans.DeleteThenCreate, plans.CreateThenDelete:
|
||||
update = true
|
||||
delete = true
|
||||
@ -109,14 +111,14 @@ func (t *DiffTransformer) Transform(g *Graph) error {
|
||||
update = true
|
||||
}
|
||||
|
||||
// A deposed instance may only have a change of Delete or NoOp. A NoOp
|
||||
// can happen if the provider shows it no longer exists during the most
|
||||
// recent ReadResource operation.
|
||||
if dk != states.NotDeposed && !(rc.Action == plans.Delete || rc.Action == plans.NoOp) {
|
||||
// A deposed instance may only have a change of Delete, Forget or NoOp.
|
||||
// A NoOp can happen if the provider shows it no longer exists during
|
||||
// the most recent ReadResource operation.
|
||||
if dk != states.NotDeposed && !(rc.Action == plans.Delete || rc.Action == plans.NoOp || rc.Action == plans.Forget) {
|
||||
diags = diags.Append(tfdiags.Sourceless(
|
||||
tfdiags.Error,
|
||||
"Invalid planned change for deposed object",
|
||||
fmt.Sprintf("The plan contains a non-delete change for %s deposed object %s. The only valid action for a deposed object is to destroy it, so this is a bug in OpenTofu.", addr, dk),
|
||||
fmt.Sprintf("The plan contains a non-removal change for %s deposed object %s. The only valid actions for a deposed object is to destroy it or forget it, so this is a bug in OpenTofu.", addr, dk),
|
||||
))
|
||||
continue
|
||||
}
|
||||
@ -211,6 +213,26 @@ func (t *DiffTransformer) Transform(g *Graph) error {
|
||||
g.Add(node)
|
||||
}
|
||||
|
||||
if forget {
|
||||
var node GraphNodeResourceInstance
|
||||
abstract := NewNodeAbstractResourceInstance(addr)
|
||||
if dk == states.NotDeposed {
|
||||
node = &NodeForgetResourceInstance{
|
||||
NodeAbstractResourceInstance: abstract,
|
||||
DeposedKey: dk,
|
||||
}
|
||||
log.Printf("[TRACE] DiffTransformer: %s will be represented for removal from the state by %s", addr, dag.VertexName(node))
|
||||
} else {
|
||||
node = &NodeForgetDeposedResourceInstanceObject{
|
||||
NodeAbstractResourceInstance: abstract,
|
||||
DeposedKey: dk,
|
||||
}
|
||||
log.Printf("[TRACE] DiffTransformer: %s deposed object %s will be represented for removal from the state by %s", addr, dk, dag.VertexName(node))
|
||||
}
|
||||
|
||||
g.Add(node)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
log.Printf("[TRACE] DiffTransformer complete")
|
||||
|
@ -569,17 +569,19 @@ A `<change-representation>` describes the change to the indicated object.
|
||||
// ["delete", "create"]
|
||||
// ["create", "delete"]
|
||||
// ["delete"]
|
||||
// ["forget"]
|
||||
// The two "replace" actions are represented in this way to allow callers to
|
||||
// e.g. just scan the list for "delete" to recognize all three situations
|
||||
// where the object will be deleted, allowing for any new deletion
|
||||
// combinations that might be added in future.
|
||||
"actions": ["update"],
|
||||
|
||||
// "before" and "after" are representations of the object value both before
|
||||
// and after the action. For ["create"] and ["delete"] actions, either
|
||||
// "before" or "after" is unset (respectively). For ["no-op"], the before and
|
||||
// after values are identical. The "after" value will be incomplete if there
|
||||
// are values within it that won't be known until after apply.
|
||||
// Before and After are representations of the object value both before and
|
||||
// after the action. For ["delete"] and ["forget"] actions, the "after"
|
||||
// value is unset. For ["create"] the "before" is unset. For ["no-op"], the
|
||||
// before and after values are identical. The "after" value will be
|
||||
// incomplete if there are values within it that won't be known until after
|
||||
// apply.
|
||||
"before": <value-representation>,
|
||||
"after": <value-representation>,
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user