mirror of
https://github.com/opentffoundation/opentf.git
synced 2025-12-19 17:59:05 -05:00
Retain resource instances with a new lifecycle argument - destroy (#3409)
Signed-off-by: Ilia Gogotchuri <ilia.gogotchuri0@gmail.com> Co-authored-by: Andrei Ciobanu <andrei.ciobanu@opentofu.org>
This commit is contained in:
@@ -9,6 +9,7 @@ UPGRADE NOTES:
|
||||
ENHANCEMENTS:
|
||||
|
||||
- `prevent_destroy` arguments in the `lifecycle` block for managed resources can now use references to other symbols in the same module, such as to a module's input variables. ([#3474](https://github.com/opentofu/opentofu/issues/3474), [#3507](https://github.com/opentofu/opentofu/issues/3507))
|
||||
- New `lifecycle` meta-argument `destroy` for altering resource destruction behavior. When set to `false` OpenTofu will not retain resources when they are planned for destruction. ([#3409](https://github.com/opentofu/opentofu/pull/3409))
|
||||
- OpenTofu now uses the `BROWSER` environment variable when launching a web browser on Unix platforms, as long as it's set to a single command that can accept a URL to open as its first and only argument. ([#3456](https://github.com/opentofu/opentofu/issues/3456))
|
||||
- Improve performance around provider checking and schema management. ([#2730](https://github.com/opentofu/opentofu/pull/2730))
|
||||
- `tofu init` now fetches providers and their metadata in parallel. Depending on provider size and network properties, this can reduce provider installation and checking time. ([#2729](https://github.com/opentofu/opentofu/pull/2729))
|
||||
|
||||
@@ -391,6 +391,219 @@ func TestApply_destroyPath(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestApply_destroySkipInConfigAndState(t *testing.T) {
|
||||
// Create a temporary working directory that is empty
|
||||
td := t.TempDir()
|
||||
testCopyDir(t, testFixturePath("skip-destroy"), td)
|
||||
t.Chdir(td)
|
||||
|
||||
// Create some existing state
|
||||
originalState := states.BuildState(func(s *states.SyncState) {
|
||||
s.SetResourceInstanceCurrent(
|
||||
addrs.Resource{
|
||||
Mode: addrs.ManagedResourceMode,
|
||||
Type: "test_instance",
|
||||
Name: "foo",
|
||||
}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance),
|
||||
&states.ResourceInstanceObjectSrc{
|
||||
AttrsJSON: []byte(`{"id":"baz"}`),
|
||||
Status: states.ObjectReady,
|
||||
SkipDestroy: true,
|
||||
},
|
||||
addrs.AbsProviderConfig{
|
||||
Provider: addrs.NewDefaultProvider("test"),
|
||||
Module: addrs.RootModule,
|
||||
},
|
||||
addrs.NoKey,
|
||||
)
|
||||
})
|
||||
statePath := testStateFile(t, originalState)
|
||||
|
||||
p := applyFixtureProvider()
|
||||
|
||||
view, done := testView(t)
|
||||
c := &ApplyCommand{
|
||||
Destroy: true,
|
||||
Meta: Meta{
|
||||
testingOverrides: metaOverridesForProvider(p),
|
||||
View: view,
|
||||
},
|
||||
}
|
||||
|
||||
args := []string{
|
||||
"-state", statePath,
|
||||
}
|
||||
code := c.Run(args)
|
||||
output := done(t)
|
||||
if code != 1 {
|
||||
t.Log(output.Stdout())
|
||||
t.Fatalf("bad: %d\n\n%s", code, output.Stderr())
|
||||
}
|
||||
|
||||
if !strings.Contains(output.Stderr(), "OpenTofu has not deleted some remote objects") {
|
||||
t.Fatalf("did not expect skip-destroy message in output:\n\n%s", output.Stderr())
|
||||
}
|
||||
|
||||
if _, err := os.Stat(statePath); err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
|
||||
state := testStateRead(t, statePath)
|
||||
if state == nil {
|
||||
t.Fatal("state should not be nil")
|
||||
}
|
||||
|
||||
actualStr := strings.TrimSpace(state.String())
|
||||
expectedStr := strings.TrimSpace(testApplyDestroyStr)
|
||||
if actualStr != expectedStr {
|
||||
t.Fatalf("bad:\n\n%s\n\n%s", actualStr, expectedStr)
|
||||
}
|
||||
}
|
||||
|
||||
// In this case, the user has removed skip-destroy from config, but it's still set in state.
|
||||
// We will plan a new state first, which will remove the skip-destroy attribute from state and then proceed to destroy the resource
|
||||
func TestApply_destroySkipInStateNotInConfig(t *testing.T) {
|
||||
// Create a temporary working directory that is empty
|
||||
td := t.TempDir()
|
||||
testCopyDir(t, testFixturePath("skip-destroy/no-skip-in-config"), td)
|
||||
t.Chdir(td)
|
||||
|
||||
// Create some existing state
|
||||
originalState := states.BuildState(func(s *states.SyncState) {
|
||||
s.SetResourceInstanceCurrent(
|
||||
addrs.Resource{
|
||||
Mode: addrs.ManagedResourceMode,
|
||||
Type: "test_instance",
|
||||
Name: "foo",
|
||||
}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance),
|
||||
&states.ResourceInstanceObjectSrc{
|
||||
AttrsJSON: []byte(`{"id":"baz"}`),
|
||||
Status: states.ObjectReady,
|
||||
SkipDestroy: true,
|
||||
},
|
||||
addrs.AbsProviderConfig{
|
||||
Provider: addrs.NewDefaultProvider("test"),
|
||||
Module: addrs.RootModule,
|
||||
},
|
||||
addrs.NoKey,
|
||||
)
|
||||
})
|
||||
statePath := testStateFile(t, originalState)
|
||||
|
||||
p := applyFixtureProvider()
|
||||
|
||||
view, done := testView(t)
|
||||
c := &ApplyCommand{
|
||||
Destroy: true,
|
||||
Meta: Meta{
|
||||
testingOverrides: metaOverridesForProvider(p),
|
||||
View: view,
|
||||
},
|
||||
}
|
||||
|
||||
args := []string{
|
||||
"-state", statePath,
|
||||
}
|
||||
code := c.Run(args)
|
||||
output := done(t)
|
||||
if code != 0 {
|
||||
t.Log(output.Stdout())
|
||||
t.Fatalf("bad: %d\n\n%s", code, output.Stderr())
|
||||
}
|
||||
// We will be destroying the resource above
|
||||
if !strings.Contains(output.Stdout(), "1 destroyed") {
|
||||
t.Fatalf("resource should be destroyed, output:\n\n%s", output.Stdout())
|
||||
}
|
||||
|
||||
if _, err := os.Stat(statePath); err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
|
||||
state := testStateRead(t, statePath)
|
||||
if state == nil {
|
||||
t.Fatal("state should not be nil")
|
||||
}
|
||||
|
||||
actualStr := strings.TrimSpace(state.String())
|
||||
expectedStr := strings.TrimSpace(testApplyDestroyStr)
|
||||
if actualStr != expectedStr {
|
||||
t.Fatalf("bad:\n\n%s\n\n%s", actualStr, expectedStr)
|
||||
}
|
||||
|
||||
}
|
||||
func TestApply_destroySkipInStateOrphaned(t *testing.T) {
|
||||
// Create a temporary working directory that is empty
|
||||
td := t.TempDir()
|
||||
testCopyDir(t, testFixturePath("skip-destroy/empty"), td)
|
||||
t.Chdir(td)
|
||||
|
||||
// Create some existing state
|
||||
originalState := states.BuildState(func(s *states.SyncState) {
|
||||
s.SetResourceInstanceCurrent(
|
||||
addrs.Resource{
|
||||
Mode: addrs.ManagedResourceMode,
|
||||
Type: "test_instance",
|
||||
Name: "foo",
|
||||
}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance),
|
||||
&states.ResourceInstanceObjectSrc{
|
||||
AttrsJSON: []byte(`{"id":"baz"}`),
|
||||
Status: states.ObjectReady,
|
||||
SkipDestroy: true,
|
||||
},
|
||||
addrs.AbsProviderConfig{
|
||||
Provider: addrs.NewDefaultProvider("test"),
|
||||
Module: addrs.RootModule,
|
||||
},
|
||||
addrs.NoKey,
|
||||
)
|
||||
})
|
||||
statePath := testStateFile(t, originalState)
|
||||
|
||||
p := applyFixtureProvider()
|
||||
|
||||
view, done := testView(t)
|
||||
c := &ApplyCommand{
|
||||
Destroy: true,
|
||||
Meta: Meta{
|
||||
testingOverrides: metaOverridesForProvider(p),
|
||||
View: view,
|
||||
},
|
||||
}
|
||||
|
||||
args := []string{
|
||||
"-state", statePath,
|
||||
}
|
||||
code := c.Run(args)
|
||||
output := done(t)
|
||||
if code != 1 {
|
||||
t.Log(output.Stdout())
|
||||
t.Fatalf("bad: %d\n\n%s", code, output.Stderr())
|
||||
}
|
||||
|
||||
if !strings.Contains(output.Stderr(), "OpenTofu has not deleted some remote objects") {
|
||||
t.Fatalf("did not expect skip-destroy message in output:\n\n%s", output.Stderr())
|
||||
}
|
||||
|
||||
// Check action reason - we must clarify to user that the attribute is stored in state even if not in config
|
||||
if !strings.Contains(output.Stdout(), "lifecycle.destroy = false") {
|
||||
t.Fatalf("did not find expected lifecycle.destroy reason in output:\n\n%s", output.Stdout())
|
||||
}
|
||||
|
||||
if _, err := os.Stat(statePath); err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
|
||||
state := testStateRead(t, statePath)
|
||||
if state == nil {
|
||||
t.Fatal("state should not be nil")
|
||||
}
|
||||
|
||||
actualStr := strings.TrimSpace(state.String())
|
||||
expectedStr := strings.TrimSpace(testApplyDestroyStr)
|
||||
if actualStr != expectedStr {
|
||||
t.Fatalf("bad:\n\n%s\n\n%s", actualStr, expectedStr)
|
||||
}
|
||||
}
|
||||
func TestApply_targetedDestroy(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
|
||||
@@ -89,6 +89,8 @@ func DiffActionSymbol(action plans.Action) string {
|
||||
return "[red]-[reset]/[green]+[reset]"
|
||||
case plans.CreateThenDelete:
|
||||
return "[green]+[reset]/[red]-[reset]"
|
||||
case plans.ForgetThenCreate:
|
||||
return " [red].[reset]/[green]+[reset]"
|
||||
case plans.Create:
|
||||
return " [green]+[reset]"
|
||||
case plans.Delete:
|
||||
|
||||
@@ -89,7 +89,7 @@ func (plan Plan) renderHuman(renderer Renderer, mode plans.Mode, opts ...plans.Q
|
||||
importingCount++
|
||||
}
|
||||
|
||||
if action == plans.Forget {
|
||||
if action == plans.Forget || action == plans.ForgetThenCreate {
|
||||
forgettingCount++
|
||||
}
|
||||
|
||||
@@ -214,6 +214,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.ForgetThenCreate] > 0 {
|
||||
renderer.Streams.Println(renderer.Colorize.Color(actionDescription(plans.ForgetThenCreate)))
|
||||
}
|
||||
if counts[plans.Forget] > 0 {
|
||||
renderer.Streams.Println(renderer.Colorize.Color(actionDescription(plans.Forget)))
|
||||
}
|
||||
@@ -234,36 +237,43 @@ func (plan Plan) renderHuman(renderer Renderer, mode plans.Mode, opts ...plans.Q
|
||||
}
|
||||
}
|
||||
|
||||
toAdd := counts[plans.Create] + counts[plans.DeleteThenCreate] + counts[plans.CreateThenDelete] + counts[plans.ForgetThenCreate]
|
||||
toDestroy := counts[plans.Delete] + counts[plans.DeleteThenCreate] + counts[plans.CreateThenDelete]
|
||||
|
||||
if importingCount > 0 {
|
||||
if forgettingCount > 0 {
|
||||
renderer.Streams.Printf(
|
||||
renderer.Colorize.Color("\n[bold]Plan:[reset] %d to import, %d to add, %d to change, %d to destroy, %d to forget.\n"),
|
||||
importingCount,
|
||||
counts[plans.Create]+counts[plans.DeleteThenCreate]+counts[plans.CreateThenDelete],
|
||||
toAdd,
|
||||
counts[plans.Update],
|
||||
counts[plans.Delete]+counts[plans.DeleteThenCreate]+counts[plans.CreateThenDelete],
|
||||
forgettingCount)
|
||||
toDestroy,
|
||||
forgettingCount,
|
||||
)
|
||||
} else {
|
||||
renderer.Streams.Printf(
|
||||
renderer.Colorize.Color("\n[bold]Plan:[reset] %d to import, %d to add, %d to change, %d to destroy.\n"),
|
||||
importingCount,
|
||||
counts[plans.Create]+counts[plans.DeleteThenCreate]+counts[plans.CreateThenDelete],
|
||||
toAdd,
|
||||
counts[plans.Update],
|
||||
counts[plans.Delete]+counts[plans.DeleteThenCreate]+counts[plans.CreateThenDelete])
|
||||
toDestroy,
|
||||
)
|
||||
}
|
||||
} else if forgettingCount > 0 {
|
||||
renderer.Streams.Printf(
|
||||
renderer.Colorize.Color("\n[bold]Plan:[reset] %d to add, %d to change, %d to destroy, %d to forget.\n"),
|
||||
counts[plans.Create]+counts[plans.DeleteThenCreate]+counts[plans.CreateThenDelete],
|
||||
toAdd,
|
||||
counts[plans.Update],
|
||||
counts[plans.Delete]+counts[plans.DeleteThenCreate]+counts[plans.CreateThenDelete],
|
||||
forgettingCount)
|
||||
toDestroy,
|
||||
forgettingCount,
|
||||
)
|
||||
} else {
|
||||
renderer.Streams.Printf(
|
||||
renderer.Colorize.Color("\n[bold]Plan:[reset] %d to add, %d to change, %d to destroy.\n"),
|
||||
counts[plans.Create]+counts[plans.DeleteThenCreate]+counts[plans.CreateThenDelete],
|
||||
toAdd,
|
||||
counts[plans.Update],
|
||||
counts[plans.Delete]+counts[plans.DeleteThenCreate]+counts[plans.CreateThenDelete])
|
||||
toDestroy,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -504,6 +514,39 @@ func resourceChangeComment(resource jsonplan.ResourceChange, action plans.Action
|
||||
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))
|
||||
|
||||
// We need to identify a special case where the resource is being forgotten instead of destroyed due to lifecycle.destroy attribute
|
||||
// There are two cases where this can happen: when lifecycle.destroy = false is set in the configuration, and when lifecycle.destroy = false is persisted in state.
|
||||
//
|
||||
// Since the attribute might no longer be present in the configuration, and we need to inform the user to avoid confusion.
|
||||
// We don't need to separate this case in ForgetThenCreate, because the message already contains this information.
|
||||
// Note that ForgetThenCreate is only used when lifecycle.destroy is set to false and the resource needs to be replaced.
|
||||
switch resource.ActionReason {
|
||||
case jsonplan.ResourceInstanceForgotBecauseOfLifecycleDestroyInState:
|
||||
buf.WriteString(" \n (because [bold]lifecycle.destroy = false[reset] was configured before this resource was removed from the configuration)")
|
||||
case jsonplan.ResourceInstanceForgotBecauseOfLifecycleDestroyInConfig:
|
||||
buf.WriteString(" \n (because [bold]lifecycle.destroy = false[reset] is set in the configuration)")
|
||||
}
|
||||
|
||||
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.ForgetThenCreate:
|
||||
switch resource.ActionReason {
|
||||
case jsonplan.ResourceInstanceReplaceBecauseTainted:
|
||||
buf.WriteString(fmt.Sprintf("[bold] # %s[reset] is tainted, so it must be [bold][red]replaced[reset]", dispAddr))
|
||||
case jsonplan.ResourceInstanceReplaceByRequest:
|
||||
buf.WriteString(fmt.Sprintf("[bold] # %s[reset] will be [bold][red]replaced[reset], as requested", dispAddr))
|
||||
case jsonplan.ResourceInstanceReplaceByTriggers:
|
||||
buf.WriteString(fmt.Sprintf("[bold] # %s[reset] will be [bold][red]replaced[reset] due to changes in replace_triggered_by", dispAddr))
|
||||
default:
|
||||
buf.WriteString(fmt.Sprintf("[bold] # %s[reset] must be [bold][red]replaced[reset]", dispAddr))
|
||||
}
|
||||
buf.WriteString(" - [yellow]older instance will [bold]not[reset][yellow] be destroyed [reset]([bold]lifecycle.destroy = false[reset])")
|
||||
|
||||
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
|
||||
@@ -577,6 +620,8 @@ func actionDescription(action plans.Action) string {
|
||||
return "[green]+[reset]/[red]-[reset] create replacement and then destroy"
|
||||
case plans.DeleteThenCreate:
|
||||
return "[red]-[reset]/[green]+[reset] destroy and then create replacement"
|
||||
case plans.ForgetThenCreate:
|
||||
return "[red].[reset]/[green]+[reset] forget and then create replacement"
|
||||
case plans.Read:
|
||||
return " [cyan]<=[reset] read (data resources)"
|
||||
case plans.Forget:
|
||||
|
||||
@@ -34,20 +34,22 @@ import (
|
||||
const (
|
||||
FormatVersion = "1.2"
|
||||
|
||||
ResourceInstanceReplaceBecauseCannotUpdate = "replace_because_cannot_update"
|
||||
ResourceInstanceReplaceBecauseTainted = "replace_because_tainted"
|
||||
ResourceInstanceReplaceByRequest = "replace_by_request"
|
||||
ResourceInstanceReplaceByTriggers = "replace_by_triggers"
|
||||
ResourceInstanceDeleteBecauseNoResourceConfig = "delete_because_no_resource_config"
|
||||
ResourceInstanceDeleteBecauseWrongRepetition = "delete_because_wrong_repetition"
|
||||
ResourceInstanceDeleteBecauseCountIndex = "delete_because_count_index"
|
||||
ResourceInstanceDeleteBecauseEnabledFalse = "delete_because_enabled_false"
|
||||
ResourceInstanceDeleteBecauseEachKey = "delete_because_each_key"
|
||||
ResourceInstanceDeleteBecauseNoModule = "delete_because_no_module"
|
||||
ResourceInstanceDeleteBecauseNoMoveTarget = "delete_because_no_move_target"
|
||||
ResourceInstanceReadBecauseConfigUnknown = "read_because_config_unknown"
|
||||
ResourceInstanceReadBecauseDependencyPending = "read_because_dependency_pending"
|
||||
ResourceInstanceReadBecauseCheckNested = "read_because_check_nested"
|
||||
ResourceInstanceReplaceBecauseCannotUpdate = "replace_because_cannot_update"
|
||||
ResourceInstanceReplaceBecauseTainted = "replace_because_tainted"
|
||||
ResourceInstanceReplaceByRequest = "replace_by_request"
|
||||
ResourceInstanceReplaceByTriggers = "replace_by_triggers"
|
||||
ResourceInstanceDeleteBecauseNoResourceConfig = "delete_because_no_resource_config"
|
||||
ResourceInstanceDeleteBecauseWrongRepetition = "delete_because_wrong_repetition"
|
||||
ResourceInstanceDeleteBecauseCountIndex = "delete_because_count_index"
|
||||
ResourceInstanceDeleteBecauseEnabledFalse = "delete_because_enabled_false"
|
||||
ResourceInstanceDeleteBecauseEachKey = "delete_because_each_key"
|
||||
ResourceInstanceDeleteBecauseNoModule = "delete_because_no_module"
|
||||
ResourceInstanceDeleteBecauseNoMoveTarget = "delete_because_no_move_target"
|
||||
ResourceInstanceReadBecauseConfigUnknown = "read_because_config_unknown"
|
||||
ResourceInstanceReadBecauseDependencyPending = "read_because_dependency_pending"
|
||||
ResourceInstanceReadBecauseCheckNested = "read_because_check_nested"
|
||||
ResourceInstanceForgotBecauseOfLifecycleDestroyInState = "forgot_because_of_lifecycle_destroy_in_state"
|
||||
ResourceInstanceForgotBecauseOfLifecycleDestroyInConfig = "forgot_because_of_lifecycle_destroy_in_config"
|
||||
)
|
||||
|
||||
// Plan is the top-level representation of the json format of a plan. It includes
|
||||
@@ -573,6 +575,10 @@ func MarshalResourceChanges(resources []*plans.ResourceInstanceChangeSrc, schema
|
||||
r.ActionReason = ResourceInstanceReadBecauseDependencyPending
|
||||
case plans.ResourceInstanceReadBecauseCheckNested:
|
||||
r.ActionReason = ResourceInstanceReadBecauseCheckNested
|
||||
case plans.ResourceInstanceForgotBecauseOfLifecycleDestroyInState:
|
||||
r.ActionReason = ResourceInstanceForgotBecauseOfLifecycleDestroyInState
|
||||
case plans.ResourceInstanceForgotBecauseOfLifecycleDestroyInConfig:
|
||||
r.ActionReason = ResourceInstanceForgotBecauseOfLifecycleDestroyInConfig
|
||||
default:
|
||||
return nil, fmt.Errorf("resource %s has an unsupported action reason %s", r.Address, rc.ActionReason)
|
||||
}
|
||||
@@ -896,6 +902,8 @@ func actionString(action string) []string {
|
||||
return []string{"update"}
|
||||
case "CreateThenDelete":
|
||||
return []string{"create", "delete"}
|
||||
case "ForgetThenCreate":
|
||||
return []string{"forget", "create"}
|
||||
case "Read":
|
||||
return []string{"read"}
|
||||
case "DeleteThenCreate":
|
||||
@@ -919,6 +927,10 @@ func UnmarshalActions(actions []string) plans.Action {
|
||||
if actions[0] == "delete" && actions[1] == "create" {
|
||||
return plans.DeleteThenCreate
|
||||
}
|
||||
|
||||
if actions[0] == "forget" && actions[1] == "create" {
|
||||
return plans.ForgetThenCreate
|
||||
}
|
||||
}
|
||||
|
||||
if len(actions) == 1 {
|
||||
|
||||
0
internal/command/testdata/skip-destroy/empty/.gitignore
vendored
Normal file
0
internal/command/testdata/skip-destroy/empty/.gitignore
vendored
Normal file
7
internal/command/testdata/skip-destroy/main.tf
vendored
Normal file
7
internal/command/testdata/skip-destroy/main.tf
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
resource "test_instance" "foo" {
|
||||
id = "baz"
|
||||
|
||||
lifecycle {
|
||||
destroy = false
|
||||
}
|
||||
}
|
||||
3
internal/command/testdata/skip-destroy/no-skip-in-config/main.tf
vendored
Normal file
3
internal/command/testdata/skip-destroy/no-skip-in-config/main.tf
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
resource "test_instance" "foo" {
|
||||
id = "baz"
|
||||
}
|
||||
@@ -249,6 +249,9 @@ func (v *OperationJSON) Plan(plan *plans.Plan, schemas *tofu.Schemas) {
|
||||
case plans.CreateThenDelete, plans.DeleteThenCreate:
|
||||
cs.Add++
|
||||
cs.Remove++
|
||||
case plans.ForgetThenCreate:
|
||||
cs.Add++
|
||||
cs.Forget++
|
||||
case plans.Forget:
|
||||
cs.Forget++
|
||||
}
|
||||
|
||||
@@ -265,6 +265,11 @@ func (r *Resource) merge(or *Resource, rps map[string]*RequiredProvider) hcl.Dia
|
||||
if or.Managed.PreventDestroy != nil {
|
||||
r.Managed.PreventDestroy = or.Managed.PreventDestroy
|
||||
}
|
||||
|
||||
if or.Managed.Destroy != nil {
|
||||
r.Managed.Destroy = or.Managed.Destroy
|
||||
}
|
||||
|
||||
if len(or.Managed.Provisioners) != 0 {
|
||||
r.Managed.Provisioners = or.Managed.Provisioners
|
||||
}
|
||||
|
||||
@@ -71,8 +71,13 @@ type ManagedResource struct {
|
||||
|
||||
CreateBeforeDestroy bool
|
||||
PreventDestroy hcl.Expression
|
||||
IgnoreChanges []hcl.Traversal
|
||||
IgnoreAllChanges bool
|
||||
// Destroy attribute indicates if the resource should be destroy once it is planned for destruction. This attribute corresponds to the `lifecycle.destroy` attribute.
|
||||
// The default behavior is to destroy the resource when it is planned for destruction, so the value of false will skip destroying the resource.
|
||||
// Note that the resource will still be removed from the state file even if Destroy is set to false but won't call the underlying provider for destruction.
|
||||
// This field will accept only constant boolean expressions. This is of type hcl.Expression to make future extensions of dynamic evaluation easier.
|
||||
Destroy hcl.Expression
|
||||
IgnoreChanges []hcl.Traversal
|
||||
IgnoreAllChanges bool
|
||||
|
||||
CreateBeforeDestroySet bool
|
||||
}
|
||||
@@ -211,6 +216,10 @@ func decodeResourceBlock(block *hcl.Block, override bool) (*Resource, hcl.Diagno
|
||||
r.Managed.PreventDestroy = attr.Expr
|
||||
}
|
||||
|
||||
if attr, exists := lcContent.Attributes["destroy"]; exists {
|
||||
r.Managed.Destroy = attr.Expr
|
||||
}
|
||||
|
||||
if attr, exists := lcContent.Attributes["replace_triggered_by"]; exists {
|
||||
exprs, hclDiags := decodeReplaceTriggeredBy(attr.Expr)
|
||||
diags = diags.Extend(hclDiags)
|
||||
@@ -683,6 +692,9 @@ func decodeEphemeralBlock(block *hcl.Block, override bool) (*Resource, hcl.Diagn
|
||||
if _, exists := lcContent.Attributes["create_before_destroy"]; exists {
|
||||
diags = append(diags, invalidEphemeralLifecycleAttributeDiag("create_before_destroy", block.DefRange))
|
||||
}
|
||||
if _, exists := lcContent.Attributes["destroy"]; exists {
|
||||
diags = append(diags, invalidEphemeralLifecycleAttributeDiag("destroy", block.DefRange))
|
||||
}
|
||||
if _, exists := lcContent.Attributes["prevent_destroy"]; exists {
|
||||
diags = append(diags, invalidEphemeralLifecycleAttributeDiag("prevent_destroy", block.DefRange))
|
||||
}
|
||||
@@ -1127,6 +1139,9 @@ var resourceLifecycleBlockSchema = &hcl.BodySchema{
|
||||
{
|
||||
Name: "prevent_destroy",
|
||||
},
|
||||
{
|
||||
Name: "destroy",
|
||||
},
|
||||
{
|
||||
Name: "ignore_changes",
|
||||
},
|
||||
|
||||
@@ -16,6 +16,7 @@ const (
|
||||
CreateThenDelete Action = '±'
|
||||
Delete Action = '-'
|
||||
Forget Action = '.'
|
||||
ForgetThenCreate Action = '⊘'
|
||||
Open Action = '⁐'
|
||||
// NOTE: Renew and Close missing on purpose.
|
||||
// Those are not meant to be stored in the plan.
|
||||
|
||||
@@ -16,6 +16,7 @@ func _() {
|
||||
_ = x[CreateThenDelete-177]
|
||||
_ = x[Delete-45]
|
||||
_ = x[Forget-46]
|
||||
_ = x[ForgetThenCreate-8856]
|
||||
_ = x[Open-8272]
|
||||
}
|
||||
|
||||
@@ -28,6 +29,7 @@ const (
|
||||
_Action_name_5 = "Open"
|
||||
_Action_name_6 = "Read"
|
||||
_Action_name_7 = "DeleteThenCreate"
|
||||
_Action_name_8 = "ForgetThenCreate"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -53,6 +55,8 @@ func (i Action) String() string {
|
||||
return _Action_name_6
|
||||
case i == 8723:
|
||||
return _Action_name_7
|
||||
case i == 8856:
|
||||
return _Action_name_8
|
||||
default:
|
||||
return "Action(" + strconv.FormatInt(int64(i), 10) + ")"
|
||||
}
|
||||
|
||||
@@ -364,7 +364,7 @@ func (rc *ResourceInstanceChange) Simplify(destroying bool) *ResourceInstanceCha
|
||||
GeneratedConfig: rc.GeneratedConfig,
|
||||
},
|
||||
}
|
||||
case CreateThenDelete, DeleteThenCreate:
|
||||
case CreateThenDelete, DeleteThenCreate, ForgetThenCreate:
|
||||
return &ResourceInstanceChange{
|
||||
Addr: rc.Addr,
|
||||
DeposedKey: rc.DeposedKey,
|
||||
@@ -487,6 +487,18 @@ const (
|
||||
// a check block and when the check assertions execute we want them to use
|
||||
// the most up-to-date data.
|
||||
ResourceInstanceReadBecauseCheckNested ResourceInstanceChangeActionReason = '#'
|
||||
|
||||
// ResourceInstanceForgotBecauseOfLifecycleDestroyInState indicates that the resource
|
||||
// instance is being forgotten because the resource has a lifecycle configuration with "destroy" set to false in the state.
|
||||
// This is used to avoid confusion for users not having the "lifecycle.destroy" attribute set in the current configuration (or the resource has been removed entirely).
|
||||
// Used to differentiate between the different origins of the `forgot` action.
|
||||
// However, we still have the attribute in the state to avoid deleting resources that should have been retained.
|
||||
ResourceInstanceForgotBecauseOfLifecycleDestroyInState ResourceInstanceChangeActionReason = 'X'
|
||||
|
||||
// ResourceInstanceForgotBecauseOfLifecycleDestroyInConfig indicates that the resource
|
||||
// instance is being forgotten because the resource has a lifecycle configuration with "destroy" set to false.
|
||||
// Used to differentiate between the different origins of the `forgot` action.
|
||||
ResourceInstanceForgotBecauseOfLifecycleDestroyInConfig ResourceInstanceChangeActionReason = 'Y'
|
||||
)
|
||||
|
||||
// OutputChange describes a change to an output value.
|
||||
|
||||
@@ -23,6 +23,8 @@ func _() {
|
||||
_ = x[ResourceInstanceReadBecauseConfigUnknown-63]
|
||||
_ = x[ResourceInstanceReadBecauseDependencyPending-33]
|
||||
_ = x[ResourceInstanceReadBecauseCheckNested-35]
|
||||
_ = x[ResourceInstanceForgotBecauseOfLifecycleDestroyInState-88]
|
||||
_ = x[ResourceInstanceForgotBecauseOfLifecycleDestroyInConfig-89]
|
||||
}
|
||||
|
||||
const (
|
||||
@@ -35,12 +37,13 @@ const (
|
||||
_ResourceInstanceChangeActionReason_name_6 = "ResourceInstanceDeleteBecauseEnabledFalseResourceInstanceDeleteBecauseNoModuleResourceInstanceDeleteBecauseNoResourceConfig"
|
||||
_ResourceInstanceChangeActionReason_name_7 = "ResourceInstanceReplaceByRequest"
|
||||
_ResourceInstanceChangeActionReason_name_8 = "ResourceInstanceReplaceBecauseTainted"
|
||||
_ResourceInstanceChangeActionReason_name_9 = "ResourceInstanceDeleteBecauseWrongRepetition"
|
||||
_ResourceInstanceChangeActionReason_name_9 = "ResourceInstanceDeleteBecauseWrongRepetitionResourceInstanceForgotBecauseOfLifecycleDestroyInStateResourceInstanceForgotBecauseOfLifecycleDestroyInConfig"
|
||||
)
|
||||
|
||||
var (
|
||||
_ResourceInstanceChangeActionReason_index_5 = [...]uint8{0, 39, 72, 108, 150}
|
||||
_ResourceInstanceChangeActionReason_index_6 = [...]uint8{0, 41, 78, 123}
|
||||
_ResourceInstanceChangeActionReason_index_9 = [...]uint8{0, 44, 98, 153}
|
||||
)
|
||||
|
||||
func (i ResourceInstanceChangeActionReason) String() string {
|
||||
@@ -65,8 +68,9 @@ func (i ResourceInstanceChangeActionReason) String() string {
|
||||
return _ResourceInstanceChangeActionReason_name_7
|
||||
case i == 84:
|
||||
return _ResourceInstanceChangeActionReason_name_8
|
||||
case i == 87:
|
||||
return _ResourceInstanceChangeActionReason_name_9
|
||||
case 87 <= i && i <= 89:
|
||||
i -= 87
|
||||
return _ResourceInstanceChangeActionReason_name_9[_ResourceInstanceChangeActionReason_index_9[i]:_ResourceInstanceChangeActionReason_index_9[i+1]]
|
||||
default:
|
||||
return "ResourceInstanceChangeActionReason(" + strconv.FormatInt(int64(i), 10) + ")"
|
||||
}
|
||||
|
||||
@@ -50,6 +50,8 @@ type ResourceInstanceObject struct {
|
||||
// destroy operations, we need to record the status to ensure a resource
|
||||
// removed from the config will still be destroyed in the same manner.
|
||||
CreateBeforeDestroy bool
|
||||
|
||||
SkipDestroy bool
|
||||
}
|
||||
|
||||
// ObjectStatus represents the status of a RemoteObject.
|
||||
@@ -148,6 +150,7 @@ func (o *ResourceInstanceObject) Encode(ty cty.Type, schemaVersion uint64) (*Res
|
||||
Status: o.Status,
|
||||
Dependencies: dependencies,
|
||||
CreateBeforeDestroy: o.CreateBeforeDestroy,
|
||||
SkipDestroy: o.SkipDestroy,
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -72,6 +72,7 @@ type ResourceInstanceObjectSrc struct {
|
||||
Status ObjectStatus
|
||||
Dependencies []addrs.ConfigResource
|
||||
CreateBeforeDestroy bool
|
||||
SkipDestroy bool
|
||||
}
|
||||
|
||||
// Compare two lists using an given element equal function, ignoring order and duplicates
|
||||
@@ -161,6 +162,10 @@ func (os *ResourceInstanceObjectSrc) Equal(other *ResourceInstanceObjectSrc) boo
|
||||
return false
|
||||
}
|
||||
|
||||
if os.SkipDestroy != other.SkipDestroy {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -211,6 +216,7 @@ func (os *ResourceInstanceObjectSrc) Decode(ty cty.Type) (*ResourceInstanceObjec
|
||||
Dependencies: os.Dependencies,
|
||||
Private: os.Private,
|
||||
CreateBeforeDestroy: os.CreateBeforeDestroy,
|
||||
SkipDestroy: os.SkipDestroy,
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -187,6 +187,7 @@ func (os *ResourceInstanceObjectSrc) DeepCopy() *ResourceInstanceObjectSrc {
|
||||
TransientPathValueMarks: allAttrPaths,
|
||||
Dependencies: dependencies,
|
||||
CreateBeforeDestroy: os.CreateBeforeDestroy,
|
||||
SkipDestroy: os.SkipDestroy,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -223,6 +224,7 @@ func (o *ResourceInstanceObject) DeepCopy() *ResourceInstanceObject {
|
||||
Private: private,
|
||||
Dependencies: dependencies,
|
||||
CreateBeforeDestroy: o.CreateBeforeDestroy,
|
||||
SkipDestroy: o.SkipDestroy,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -182,6 +182,7 @@ func prepareStateV4(sV4 *stateV4) (*File, tfdiags.Diagnostics) {
|
||||
obj := &states.ResourceInstanceObjectSrc{
|
||||
SchemaVersion: isV4.SchemaVersion,
|
||||
CreateBeforeDestroy: isV4.CreateBeforeDestroy,
|
||||
SkipDestroy: isV4.SkipDestroy,
|
||||
}
|
||||
|
||||
{
|
||||
@@ -588,6 +589,7 @@ func appendInstanceObjectStateV4(rs *states.Resource, is *states.ResourceInstanc
|
||||
PrivateRaw: privateRaw,
|
||||
Dependencies: deps,
|
||||
CreateBeforeDestroy: obj.CreateBeforeDestroy,
|
||||
SkipDestroy: obj.SkipDestroy,
|
||||
}), diags
|
||||
}
|
||||
|
||||
@@ -805,6 +807,7 @@ type instanceObjectStateV4 struct {
|
||||
Dependencies []string `json:"dependencies,omitempty"`
|
||||
|
||||
CreateBeforeDestroy bool `json:"create_before_destroy,omitempty"`
|
||||
SkipDestroy bool `json:"skip_destroy,omitempty"`
|
||||
}
|
||||
|
||||
type checkResultsV4 struct {
|
||||
|
||||
@@ -71,6 +71,8 @@ func (c *Context) Apply(ctx context.Context, plan *plans.Plan, config *configs.C
|
||||
return nil, diags
|
||||
}
|
||||
|
||||
var forgetCount int
|
||||
|
||||
for _, rc := range plan.Changes.Resources {
|
||||
// Import is a no-op change during an apply (all the real action happens during the plan) but we'd
|
||||
// like to show some helpful output that mirrors the way we show other changes.
|
||||
@@ -91,6 +93,7 @@ func (c *Context) Apply(ctx context.Context, plan *plans.Plan, config *configs.C
|
||||
|
||||
// Following the same logic, we want to show helpful output for forget operations as well.
|
||||
if rc.Action == plans.Forget {
|
||||
forgetCount++
|
||||
for _, h := range c.hooks {
|
||||
_, err := h.PreApplyForget(rc.Addr)
|
||||
if err != nil {
|
||||
@@ -141,6 +144,18 @@ func (c *Context) Apply(ctx context.Context, plan *plans.Plan, config *configs.C
|
||||
// unconditionally here, but we historically didn't and haven't yet
|
||||
// verified that it'd be safe to do so.
|
||||
newState.PruneResourceHusks()
|
||||
|
||||
// If this was a destroy operation, and everything else succeeded, but
|
||||
// there are instances that were forgotten (not destroyed).
|
||||
// Even though this was the intended outcome, some automations may depend on the success of destroy operation
|
||||
// to indicate the complete removal of resources
|
||||
if forgetCount > 0 {
|
||||
diags = diags.Append(tfdiags.Sourceless(
|
||||
tfdiags.Error,
|
||||
"Destroy was successful but left behind forgotten instances",
|
||||
"As requested, OpenTofu has not deleted some remote objects that are no longer managed by this configuration. Those objects continue to exist in their remote system and so may continue to incur charges. Refer to the original plan for more information.",
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
if len(plan.TargetAddrs) > 0 || len(plan.ExcludeAddrs) > 0 {
|
||||
|
||||
@@ -20,6 +20,7 @@ import (
|
||||
"github.com/opentofu/opentofu/internal/lang"
|
||||
"github.com/opentofu/opentofu/internal/states"
|
||||
"github.com/opentofu/opentofu/internal/tfdiags"
|
||||
"github.com/zclconf/go-cty/cty"
|
||||
)
|
||||
|
||||
// traceNameValidateResource is a standardized trace span name we use for the
|
||||
@@ -697,6 +698,49 @@ func (n *NodeAbstractResourceInstance) readResourceInstanceStateDeposed(ctx cont
|
||||
return obj, diags
|
||||
}
|
||||
|
||||
// checkSkipDestroy checks if the resource should be forgotten instead of destroyed
|
||||
func (n *NodeAbstractResource) shouldSkipDestroy() (bool, tfdiags.Diagnostics) {
|
||||
var diags tfdiags.Diagnostics
|
||||
if n.Config == nil || n.Config.Managed == nil {
|
||||
return false, diags
|
||||
}
|
||||
|
||||
skipDestroy, skipDestroyDiags := skipDestroyValueFromConstantExpression(n.Config.Managed.Destroy)
|
||||
diags = diags.Append(skipDestroyDiags)
|
||||
if diags.HasErrors() {
|
||||
return false, diags
|
||||
}
|
||||
|
||||
return skipDestroy, diags
|
||||
}
|
||||
|
||||
// skipDestroyValueFromConstantExpression evaluates (lifecycle.)destroy expression coming from the config and returns !destroy (Corresponding to SkipDestroy)
|
||||
// As of now, this can only be a constant expression of a boolean type. We will likely extend this in the future to make dynamic values possible
|
||||
func skipDestroyValueFromConstantExpression(destroyExpr hcl.Expression) (bool, hcl.Diagnostics) {
|
||||
var diags hcl.Diagnostics
|
||||
// If destroyExpr is nil, we do not need to set SkipDestroy, as its zero value is false, which results in the desired default behavior (of not skipping destruction)
|
||||
if destroyExpr == nil {
|
||||
return false, diags
|
||||
}
|
||||
|
||||
destroyVal, valDiags := destroyExpr.Value(nil)
|
||||
if diags.HasErrors() {
|
||||
return false, valDiags
|
||||
}
|
||||
|
||||
if destroyVal.Type() != cty.Bool {
|
||||
diags = append(diags, &hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: "Invalid lifecycle destroy expression",
|
||||
Detail: "The lifecycle destroy expression must be a boolean constant.",
|
||||
Subject: destroyExpr.Range().Ptr(),
|
||||
})
|
||||
return false, diags
|
||||
}
|
||||
|
||||
return destroyVal.False(), diags
|
||||
}
|
||||
|
||||
// graphNodesAreResourceInstancesInDifferentInstancesOfSameModule is an
|
||||
// annoyingly-task-specific helper function that returns true if and only if
|
||||
// the following conditions hold:
|
||||
|
||||
@@ -1256,6 +1256,13 @@ func (n *NodeAbstractResourceInstance) plan(
|
||||
return nil, nil, keyData, diags
|
||||
}
|
||||
|
||||
skipDestroy, skipDiags := n.shouldSkipDestroy()
|
||||
diags = diags.Append(skipDiags)
|
||||
if diags.HasErrors() {
|
||||
return nil, nil, keyData, diags
|
||||
}
|
||||
log.Printf("[TRACE] plan: %s lifecycle.destroy evaluation result: skipDestroy=%t", n.Addr, skipDestroy)
|
||||
|
||||
resp := provider.PlanResourceChange(ctx, providers.PlanResourceChangeRequest{
|
||||
TypeName: n.Addr.Resource.Resource.Type,
|
||||
Config: unmarkedConfigVal,
|
||||
@@ -1566,6 +1573,15 @@ func (n *NodeAbstractResourceInstance) plan(
|
||||
actionReason = plans.ResourceInstanceReplaceBecauseTainted
|
||||
}
|
||||
|
||||
// We check here if user declared lifecycle destroy attribute as false, intending to retain this resource even if
|
||||
// so far we thought the action was "replace".
|
||||
// As mentioned above, we are not concerned with the "delete" action in this flow; the pure delete is handled elsewhere
|
||||
if action.IsReplace() && skipDestroy {
|
||||
// We alter the action to "forget" and "create" to not trigger resource destruction
|
||||
action = plans.ForgetThenCreate
|
||||
log.Printf("[DEBUG] plan: %s changing action from %s to ForgetThenCreate due to lifecycle.destroy=false", n.Addr, action)
|
||||
}
|
||||
|
||||
// compare the marks between the prior and the new value, there may have been a change of sensitivity
|
||||
// in the new value that requires an update
|
||||
_, plannedNewValMarks := plannedNewVal.UnmarkDeepWithPaths()
|
||||
@@ -1629,9 +1645,10 @@ func (n *NodeAbstractResourceInstance) plan(
|
||||
// must _also_ record the returned change in the active plan,
|
||||
// which the expression evaluator will use in preference to this
|
||||
// incomplete value recorded in the state.
|
||||
Status: states.ObjectPlanned,
|
||||
Value: plannedNewVal,
|
||||
Private: plannedPrivate,
|
||||
Status: states.ObjectPlanned,
|
||||
Value: plannedNewVal,
|
||||
Private: plannedPrivate,
|
||||
SkipDestroy: skipDestroy,
|
||||
}
|
||||
|
||||
return plan, state, keyData, diags
|
||||
@@ -2941,6 +2958,7 @@ func (n *NodeAbstractResourceInstance) apply(
|
||||
// Copy the previous state, changing only the value
|
||||
newState := &states.ResourceInstanceObject{
|
||||
CreateBeforeDestroy: state.CreateBeforeDestroy,
|
||||
SkipDestroy: state.SkipDestroy,
|
||||
Dependencies: state.Dependencies,
|
||||
Private: state.Private,
|
||||
Status: state.Status,
|
||||
@@ -3063,6 +3081,10 @@ func (n *NodeAbstractResourceInstance) apply(
|
||||
newVal = cty.UnknownAsNull(newVal)
|
||||
}
|
||||
|
||||
skipDestroy, skipDiags := n.shouldSkipDestroy()
|
||||
diags = diags.Append(skipDiags)
|
||||
log.Printf("[TRACE] apply: %s lifecycle.destroy evaluation result: skipDestroy=%t", n.Addr, skipDestroy)
|
||||
|
||||
if change.Action != plans.Delete && !diags.HasErrors() {
|
||||
// Only values that were marked as unknown in the planned value are allowed
|
||||
// to change during the apply operation. (We do this after the unknown-ness
|
||||
@@ -3155,6 +3177,7 @@ func (n *NodeAbstractResourceInstance) apply(
|
||||
Value: newVal,
|
||||
Private: resp.Private,
|
||||
CreateBeforeDestroy: createBeforeDestroy,
|
||||
SkipDestroy: state.SkipDestroy,
|
||||
}
|
||||
|
||||
// if the resource was being deleted, the dependencies are not going to
|
||||
@@ -3172,11 +3195,12 @@ func (n *NodeAbstractResourceInstance) apply(
|
||||
Value: newVal,
|
||||
Private: resp.Private,
|
||||
CreateBeforeDestroy: createBeforeDestroy,
|
||||
SkipDestroy: skipDestroy,
|
||||
}
|
||||
return newState, diags
|
||||
|
||||
default:
|
||||
// Non error case, were the object was deleted
|
||||
// Non-error case, where the object was deleted
|
||||
return nil, diags
|
||||
}
|
||||
}
|
||||
|
||||
@@ -481,6 +481,10 @@ func (n *NodeApplyableResourceInstance) checkPlannedChange(evalCtx EvalContext,
|
||||
|
||||
if plannedChange.Action != actualChange.Action {
|
||||
switch {
|
||||
case plannedChange.Action == plans.ForgetThenCreate && actualChange.Action == plans.Create:
|
||||
// This is an expected alteration of the action, since we are, first - forgetting the resource and then calling
|
||||
// the diffApply plan, with no state for the resource, we are generating the Create action instead of ForgetThenCreate
|
||||
log.Printf("[DEBUG] For apply the action ForgetThenCreate was changed to Create for resource %s", absAddr)
|
||||
case plannedChange.Action == plans.Update && actualChange.Action == plans.NoOp:
|
||||
// It's okay for an update to become a NoOp once we've filled in
|
||||
// all of the unknown values, since the final values might actually
|
||||
|
||||
@@ -162,30 +162,44 @@ func (n *NodePlanDeposedResourceInstanceObject) Execute(ctx context.Context, eva
|
||||
if !n.skipPlanChanges {
|
||||
var change *plans.ResourceInstanceChange
|
||||
var planDiags tfdiags.Diagnostics
|
||||
skipDestroy, skipDiags := n.shouldSkipDestroy()
|
||||
diags = diags.Append(skipDiags)
|
||||
if diags.HasErrors() {
|
||||
return diags
|
||||
}
|
||||
|
||||
shouldForget := false
|
||||
shouldDestroy := false
|
||||
// We skip destroy for a depose instance in 2 cases:
|
||||
// 1) Resource had lifecycle attribute destroy explicitly set to false
|
||||
// 2) Removed block is declared to remove the resource from the state without it's destroy set to true
|
||||
// For every other case, we should destroy the resource
|
||||
// If the deposed instance has skip_destroy set in state, we skip destroying
|
||||
shouldDestroy := !skipDestroy && !state.SkipDestroy
|
||||
|
||||
log.Printf("[TRACE] NodePlanDeposedResourceInstanceObject.Execute: %s (deposed %s): skipDestroy based on config=%t; based on state.SkipDestroy=%t; shouldDestroy=%t", n.Addr, n.DeposedKey, skipDestroy, state.SkipDestroy, shouldDestroy)
|
||||
// Note that removed statements take precedence, since it is the latest intent the user declared
|
||||
// As opposed to the lifecycle attribute, which might have been altered after the resource got deposed
|
||||
for _, rs := range n.RemoveStatements {
|
||||
if rs.From.TargetContains(n.Addr) {
|
||||
shouldForget = true
|
||||
shouldDestroy = rs.Destroy
|
||||
log.Printf("[DEBUG] NodePlanDeposedResourceInstanceObject.Execute: %s (deposed %s) removed block found, overriding shouldDestroy to %t", n.Addr, n.DeposedKey, shouldDestroy)
|
||||
}
|
||||
}
|
||||
|
||||
if shouldForget {
|
||||
if shouldDestroy {
|
||||
change, planDiags = n.planDestroy(ctx, evalCtx, state, n.DeposedKey)
|
||||
} else {
|
||||
diags = diags.Append(&hcl.Diagnostic{
|
||||
Severity: hcl.DiagWarning,
|
||||
Summary: "Resource going to be removed from the state",
|
||||
Detail: fmt.Sprintf("After this plan gets applied, the resource %s will not be managed anymore by OpenTofu.\n\nIn case you want to manage the resource again, you will have to import it.", n.Addr),
|
||||
})
|
||||
change = n.planForget(ctx, evalCtx, state, n.DeposedKey)
|
||||
}
|
||||
} else {
|
||||
if shouldDestroy {
|
||||
change, planDiags = n.planDestroy(ctx, evalCtx, state, n.DeposedKey)
|
||||
} else {
|
||||
diags = diags.Append(&hcl.Diagnostic{
|
||||
Severity: hcl.DiagWarning,
|
||||
Summary: "Resource will be removed from the state",
|
||||
Detail: fmt.Sprintf("After this plan is applied, the resource %s will not be managed anymore by OpenTofu.\n\nIn case you want to manage the resource again, you will have to import it.", n.Addr),
|
||||
})
|
||||
log.Printf("[DEBUG] NodePlanDeposedResourceInstanceObject.Execute: %s (deposed %s) planning forget instead of destroy", n.Addr, n.DeposedKey)
|
||||
change = n.planForget(ctx, evalCtx, state, n.DeposedKey)
|
||||
if skipDestroy {
|
||||
change.ActionReason = plans.ResourceInstanceForgotBecauseOfLifecycleDestroyInConfig
|
||||
} else if state.SkipDestroy {
|
||||
change.ActionReason = plans.ResourceInstanceForgotBecauseOfLifecycleDestroyInState
|
||||
}
|
||||
}
|
||||
|
||||
diags = diags.Append(planDiags)
|
||||
|
||||
@@ -66,8 +66,8 @@ func TestNodePlanDeposedResourceInstanceObject_Execute(t *testing.T) {
|
||||
wantAction: plans.Forget,
|
||||
wantDiags: tfdiags.Diagnostics{}.Append(&hcl.Diagnostic{
|
||||
Severity: hcl.DiagWarning,
|
||||
Summary: "Resource going to be removed from the state",
|
||||
Detail: fmt.Sprintf("After this plan gets applied, the resource %s will not be managed anymore by OpenTofu.\n\nIn case you want to manage the resource again, you will have to import it.", "test_instance.foo"),
|
||||
Summary: "Resource will be removed from the state",
|
||||
Detail: fmt.Sprintf("After this plan is applied, the resource %s will not be managed anymore by OpenTofu.\n\nIn case you want to manage the resource again, you will have to import it.", "test_instance.foo"),
|
||||
}),
|
||||
},
|
||||
{
|
||||
@@ -92,8 +92,8 @@ func TestNodePlanDeposedResourceInstanceObject_Execute(t *testing.T) {
|
||||
wantAction: plans.Forget,
|
||||
wantDiags: tfdiags.Diagnostics{}.Append(&hcl.Diagnostic{
|
||||
Severity: hcl.DiagWarning,
|
||||
Summary: "Resource going to be removed from the state",
|
||||
Detail: fmt.Sprintf("After this plan gets applied, the resource %s will not be managed anymore by OpenTofu.\n\nIn case you want to manage the resource again, you will have to import it.", "test_instance.foo[1]"),
|
||||
Summary: "Resource will be removed from the state",
|
||||
Detail: fmt.Sprintf("After this plan is applied, the resource %s will not be managed anymore by OpenTofu.\n\nIn case you want to manage the resource again, you will have to import it.", "test_instance.foo[1]"),
|
||||
}),
|
||||
},
|
||||
{
|
||||
@@ -118,8 +118,8 @@ func TestNodePlanDeposedResourceInstanceObject_Execute(t *testing.T) {
|
||||
wantAction: plans.Forget,
|
||||
wantDiags: tfdiags.Diagnostics{}.Append(&hcl.Diagnostic{
|
||||
Severity: hcl.DiagWarning,
|
||||
Summary: "Resource going to be removed from the state",
|
||||
Detail: fmt.Sprintf("After this plan gets applied, the resource %s will not be managed anymore by OpenTofu.\n\nIn case you want to manage the resource again, you will have to import it.", "module.boop.test_instance.foo"),
|
||||
Summary: "Resource will be removed from the state",
|
||||
Detail: fmt.Sprintf("After this plan is applied, the resource %s will not be managed anymore by OpenTofu.\n\nIn case you want to manage the resource again, you will have to import it.", "module.boop.test_instance.foo"),
|
||||
}),
|
||||
},
|
||||
{
|
||||
@@ -155,8 +155,8 @@ func TestNodePlanDeposedResourceInstanceObject_Execute(t *testing.T) {
|
||||
wantAction: plans.Forget,
|
||||
wantDiags: tfdiags.Diagnostics{}.Append(&hcl.Diagnostic{
|
||||
Severity: hcl.DiagWarning,
|
||||
Summary: "Resource going to be removed from the state",
|
||||
Detail: fmt.Sprintf("After this plan gets applied, the resource %s will not be managed anymore by OpenTofu.\n\nIn case you want to manage the resource again, you will have to import it.", "module.boop[1].test_instance.foo[1]"),
|
||||
Summary: "Resource will be removed from the state",
|
||||
Detail: fmt.Sprintf("After this plan is applied, the resource %s will not be managed anymore by OpenTofu.\n\nIn case you want to manage the resource again, you will have to import it.", "module.boop[1].test_instance.foo[1]"),
|
||||
}),
|
||||
},
|
||||
{
|
||||
@@ -170,8 +170,8 @@ func TestNodePlanDeposedResourceInstanceObject_Execute(t *testing.T) {
|
||||
wantAction: plans.Forget,
|
||||
wantDiags: tfdiags.Diagnostics{}.Append(&hcl.Diagnostic{
|
||||
Severity: hcl.DiagWarning,
|
||||
Summary: "Resource going to be removed from the state",
|
||||
Detail: fmt.Sprintf("After this plan gets applied, the resource %s will not be managed anymore by OpenTofu.\n\nIn case you want to manage the resource again, you will have to import it.", "module.boop.test_instance.foo"),
|
||||
Summary: "Resource will be removed from the state",
|
||||
Detail: fmt.Sprintf("After this plan is applied, the resource %s will not be managed anymore by OpenTofu.\n\nIn case you want to manage the resource again, you will have to import it.", "module.boop.test_instance.foo"),
|
||||
}),
|
||||
},
|
||||
{
|
||||
@@ -185,8 +185,8 @@ func TestNodePlanDeposedResourceInstanceObject_Execute(t *testing.T) {
|
||||
wantAction: plans.Forget,
|
||||
wantDiags: tfdiags.Diagnostics{}.Append(&hcl.Diagnostic{
|
||||
Severity: hcl.DiagWarning,
|
||||
Summary: "Resource going to be removed from the state",
|
||||
Detail: fmt.Sprintf("After this plan gets applied, the resource %s will not be managed anymore by OpenTofu.\n\nIn case you want to manage the resource again, you will have to import it.", "module.boop[1].test_instance.foo"),
|
||||
Summary: "Resource will be removed from the state",
|
||||
Detail: fmt.Sprintf("After this plan is applied, the resource %s will not be managed anymore by OpenTofu.\n\nIn case you want to manage the resource again, you will have to import it.", "module.boop[1].test_instance.foo"),
|
||||
}),
|
||||
},
|
||||
}
|
||||
|
||||
@@ -132,8 +132,24 @@ func (n *NodePlanDestroyableResourceInstance) managedResourceExecute(ctx context
|
||||
}
|
||||
}
|
||||
|
||||
change, destroyPlanDiags := n.planDestroy(ctx, evalCtx, state, "")
|
||||
diags = diags.Append(destroyPlanDiags)
|
||||
var planDiags tfdiags.Diagnostics
|
||||
skipDestroy, skipDiags := n.shouldSkipDestroy()
|
||||
diags = diags.Append(skipDiags)
|
||||
if diags.HasErrors() {
|
||||
return diags
|
||||
}
|
||||
if skipDestroy {
|
||||
log.Printf("[DEBUG] NodePlanDestroyableResourceInstance.managedResourceExecute: %s planning forget instead of destroy due to lifecycle.destroy=false in configuration", addr)
|
||||
change = n.planForget(ctx, evalCtx, state, "")
|
||||
change.ActionReason = plans.ResourceInstanceForgotBecauseOfLifecycleDestroyInConfig
|
||||
} else if state.SkipDestroy {
|
||||
log.Printf("[DEBUG] NodePlanDestroyableResourceInstance.managedResourceExecute: %s planning forget instead of destroy due to lifecycle.destroy=false in state", addr)
|
||||
change = n.planForget(ctx, evalCtx, state, "")
|
||||
change.ActionReason = plans.ResourceInstanceForgotBecauseOfLifecycleDestroyInState
|
||||
} else {
|
||||
change, planDiags = n.planDestroy(ctx, evalCtx, state, "")
|
||||
}
|
||||
diags = diags.Append(planDiags)
|
||||
if diags.HasErrors() {
|
||||
return diags
|
||||
}
|
||||
@@ -142,7 +158,6 @@ func (n *NodePlanDestroyableResourceInstance) managedResourceExecute(ctx context
|
||||
if diags.HasErrors() {
|
||||
return diags
|
||||
}
|
||||
|
||||
diags = diags.Append(n.checkPreventDestroy(ctx, evalCtx, change))
|
||||
return diags
|
||||
}
|
||||
|
||||
@@ -320,18 +320,28 @@ func (n *NodePlannableResourceInstance) managedResourceExecute(ctx context.Conte
|
||||
} else {
|
||||
if instanceRefreshState != nil {
|
||||
prevCreateBeforeDestroy := instanceRefreshState.CreateBeforeDestroy
|
||||
prevSkipDestroy := instanceRefreshState.SkipDestroy
|
||||
|
||||
// This change is usually written to the refreshState and then
|
||||
// updated value used for further graph execution. However, with
|
||||
// "refresh=false", refreshState is not being written and then
|
||||
// "refresh=false", refreshState is not being written, and then
|
||||
// some resources with updated configuration could be detached
|
||||
// due to missaligned create_before_destroy in different graph nodes.
|
||||
// due to misaligned create_before_destroy and skip_destroy in different graph nodes.
|
||||
instanceRefreshState.CreateBeforeDestroy = n.Config.Managed.CreateBeforeDestroy || n.ForceCreateBeforeDestroy
|
||||
// Destroy coming from the config is an hcl.Expression, so we need to evaluate it here, currently this only supports constant booleans
|
||||
skipDestroy, skipDiags := n.shouldSkipDestroy()
|
||||
diags = diags.Append(skipDiags)
|
||||
if diags.HasErrors() {
|
||||
return diags
|
||||
}
|
||||
instanceRefreshState.SkipDestroy = skipDestroy
|
||||
|
||||
if prevCreateBeforeDestroy != instanceRefreshState.CreateBeforeDestroy && n.skipRefresh {
|
||||
diags = diags.Append(n.writeResourceInstanceState(ctx, evalCtx, instanceRefreshState, refreshState))
|
||||
if diags.HasErrors() {
|
||||
return diags
|
||||
if n.skipRefresh {
|
||||
if prevCreateBeforeDestroy != instanceRefreshState.CreateBeforeDestroy || prevSkipDestroy != instanceRefreshState.SkipDestroy {
|
||||
diags = diags.Append(n.writeResourceInstanceState(ctx, evalCtx, instanceRefreshState, refreshState))
|
||||
if diags.HasErrors() {
|
||||
return diags
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -187,29 +187,45 @@ func (n *NodePlannableResourceInstanceOrphan) managedResourceExecute(ctx context
|
||||
var change *plans.ResourceInstanceChange
|
||||
var planDiags tfdiags.Diagnostics
|
||||
|
||||
shouldForget := false
|
||||
shouldDestroy := false // NOTE: false for backwards compatibility. This is not the same behavior that the other system is having.
|
||||
skipDestroy, skipDiags := n.shouldSkipDestroy()
|
||||
diags = diags.Append(skipDiags)
|
||||
if diags.HasErrors() {
|
||||
return diags
|
||||
}
|
||||
|
||||
// We skip destroy for an orphaned resource instance in 2 cases:
|
||||
// 1) Resource had lifecycle attribute destroy explicitly set to false (either in config or in state)
|
||||
// Config case in case of orphans only applies for multi-instance resources (count/for_each)
|
||||
// 2) Removed block is declared to remove the resource from the state without it's destroy set to true
|
||||
// For every other case, we should destroy the resource
|
||||
// If the orphan instance has skip_destroy set in state, we skip destroying
|
||||
shouldDestroy := !skipDestroy && !oldState.SkipDestroy
|
||||
|
||||
log.Printf("[TRACE] NodePlannableResourceInstanceOrphan.managedResourceExecute: %s (orphan): shouldDestroy=%t (based on config)", n.Addr, shouldDestroy)
|
||||
// Note that removed statements take precedence, since it is the latest intent the user declared
|
||||
// As opposed to the lifecycle attribute, which was the previous intention declared on the orphaned resource
|
||||
for _, rs := range n.RemoveStatements {
|
||||
if rs.From.TargetContains(n.Addr) {
|
||||
shouldForget = true
|
||||
shouldDestroy = rs.Destroy
|
||||
log.Printf("[DEBUG] NodePlannableResourceInstanceOrphan.managedResourceExecute: %s (orphan) removed block found, overriding shouldDestroy to %t", addr, shouldDestroy)
|
||||
}
|
||||
}
|
||||
|
||||
if shouldForget {
|
||||
if shouldDestroy {
|
||||
change, planDiags = n.planDestroy(ctx, evalCtx, oldState, "")
|
||||
} else {
|
||||
diags = diags.Append(&hcl.Diagnostic{
|
||||
Severity: hcl.DiagWarning,
|
||||
Summary: "Resource going to be removed from the state",
|
||||
Detail: fmt.Sprintf("After this plan gets applied, the resource %s will not be managed anymore by OpenTofu.\n\nIn case you want to manage the resource again, you will have to import it.", n.Addr),
|
||||
})
|
||||
change = n.planForget(ctx, evalCtx, oldState, "")
|
||||
}
|
||||
} else {
|
||||
if shouldDestroy {
|
||||
change, planDiags = n.planDestroy(ctx, evalCtx, oldState, "")
|
||||
} else {
|
||||
diags = diags.Append(&hcl.Diagnostic{
|
||||
Severity: hcl.DiagWarning,
|
||||
Summary: "Resource will be removed from the state",
|
||||
Detail: fmt.Sprintf("After this plan is applied, the resource %s will not be managed anymore by OpenTofu.\n\nIn case you want to manage the resource again, you will have to import it.", n.Addr),
|
||||
})
|
||||
log.Printf("[DEBUG] NodePlannableResourceInstanceOrphan.managedResourceExecute: %s (orphan) planning forget instead of destroy", addr)
|
||||
change = n.planForget(ctx, evalCtx, oldState, "")
|
||||
if skipDestroy {
|
||||
change.ActionReason = plans.ResourceInstanceForgotBecauseOfLifecycleDestroyInConfig
|
||||
} else if oldState.SkipDestroy {
|
||||
change.ActionReason = plans.ResourceInstanceForgotBecauseOfLifecycleDestroyInState
|
||||
}
|
||||
}
|
||||
|
||||
diags = diags.Append(planDiags)
|
||||
|
||||
@@ -60,8 +60,8 @@ func TestNodeResourcePlanOrphan_Execute(t *testing.T) {
|
||||
wantAction: plans.Forget,
|
||||
wantDiags: tfdiags.Diagnostics{}.Append(&hcl.Diagnostic{
|
||||
Severity: hcl.DiagWarning,
|
||||
Summary: "Resource going to be removed from the state",
|
||||
Detail: fmt.Sprintf("After this plan gets applied, the resource %s will not be managed anymore by OpenTofu.\n\nIn case you want to manage the resource again, you will have to import it.", "test_instance.foo"),
|
||||
Summary: "Resource will be removed from the state",
|
||||
Detail: fmt.Sprintf("After this plan is applied, the resource %s will not be managed anymore by OpenTofu.\n\nIn case you want to manage the resource again, you will have to import it.", "test_instance.foo"),
|
||||
}),
|
||||
},
|
||||
{
|
||||
@@ -84,8 +84,8 @@ func TestNodeResourcePlanOrphan_Execute(t *testing.T) {
|
||||
wantAction: plans.Forget,
|
||||
wantDiags: tfdiags.Diagnostics{}.Append(&hcl.Diagnostic{
|
||||
Severity: hcl.DiagWarning,
|
||||
Summary: "Resource going to be removed from the state",
|
||||
Detail: fmt.Sprintf("After this plan gets applied, the resource %s will not be managed anymore by OpenTofu.\n\nIn case you want to manage the resource again, you will have to import it.", "test_instance.foo[1]"),
|
||||
Summary: "Resource will be removed from the state",
|
||||
Detail: fmt.Sprintf("After this plan is applied, the resource %s will not be managed anymore by OpenTofu.\n\nIn case you want to manage the resource again, you will have to import it.", "test_instance.foo[1]"),
|
||||
}),
|
||||
},
|
||||
{
|
||||
@@ -108,8 +108,8 @@ func TestNodeResourcePlanOrphan_Execute(t *testing.T) {
|
||||
wantAction: plans.Forget,
|
||||
wantDiags: tfdiags.Diagnostics{}.Append(&hcl.Diagnostic{
|
||||
Severity: hcl.DiagWarning,
|
||||
Summary: "Resource going to be removed from the state",
|
||||
Detail: fmt.Sprintf("After this plan gets applied, the resource %s will not be managed anymore by OpenTofu.\n\nIn case you want to manage the resource again, you will have to import it.", "module.boop.test_instance.foo"),
|
||||
Summary: "Resource will be removed from the state",
|
||||
Detail: fmt.Sprintf("After this plan is applied, the resource %s will not be managed anymore by OpenTofu.\n\nIn case you want to manage the resource again, you will have to import it.", "module.boop.test_instance.foo"),
|
||||
}),
|
||||
},
|
||||
{
|
||||
@@ -143,8 +143,8 @@ func TestNodeResourcePlanOrphan_Execute(t *testing.T) {
|
||||
wantAction: plans.Forget,
|
||||
wantDiags: tfdiags.Diagnostics{}.Append(&hcl.Diagnostic{
|
||||
Severity: hcl.DiagWarning,
|
||||
Summary: "Resource going to be removed from the state",
|
||||
Detail: fmt.Sprintf("After this plan gets applied, the resource %s will not be managed anymore by OpenTofu.\n\nIn case you want to manage the resource again, you will have to import it.", "module.boop[1].test_instance.foo[1]"),
|
||||
Summary: "Resource will be removed from the state",
|
||||
Detail: fmt.Sprintf("After this plan is applied, the resource %s will not be managed anymore by OpenTofu.\n\nIn case you want to manage the resource again, you will have to import it.", "module.boop[1].test_instance.foo[1]"),
|
||||
}),
|
||||
},
|
||||
{
|
||||
@@ -156,8 +156,8 @@ func TestNodeResourcePlanOrphan_Execute(t *testing.T) {
|
||||
wantAction: plans.Forget,
|
||||
wantDiags: tfdiags.Diagnostics{}.Append(&hcl.Diagnostic{
|
||||
Severity: hcl.DiagWarning,
|
||||
Summary: "Resource going to be removed from the state",
|
||||
Detail: fmt.Sprintf("After this plan gets applied, the resource %s will not be managed anymore by OpenTofu.\n\nIn case you want to manage the resource again, you will have to import it.", "module.boop.test_instance.foo"),
|
||||
Summary: "Resource will be removed from the state",
|
||||
Detail: fmt.Sprintf("After this plan is applied, the resource %s will not be managed anymore by OpenTofu.\n\nIn case you want to manage the resource again, you will have to import it.", "module.boop.test_instance.foo"),
|
||||
}),
|
||||
},
|
||||
{
|
||||
@@ -169,8 +169,8 @@ func TestNodeResourcePlanOrphan_Execute(t *testing.T) {
|
||||
wantAction: plans.Forget,
|
||||
wantDiags: tfdiags.Diagnostics{}.Append(&hcl.Diagnostic{
|
||||
Severity: hcl.DiagWarning,
|
||||
Summary: "Resource going to be removed from the state",
|
||||
Detail: fmt.Sprintf("After this plan gets applied, the resource %s will not be managed anymore by OpenTofu.\n\nIn case you want to manage the resource again, you will have to import it.", "module.boop[1].test_instance.foo"),
|
||||
Summary: "Resource will be removed from the state",
|
||||
Detail: fmt.Sprintf("After this plan is applied, the resource %s will not be managed anymore by OpenTofu.\n\nIn case you want to manage the resource again, you will have to import it.", "module.boop[1].test_instance.foo"),
|
||||
}),
|
||||
},
|
||||
}
|
||||
|
||||
1417
internal/tofu/skip_destroy_test.go
Normal file
1417
internal/tofu/skip_destroy_test.go
Normal file
File diff suppressed because it is too large
Load Diff
2
internal/tofu/testdata/simple/main.tf
vendored
Normal file
2
internal/tofu/testdata/simple/main.tf
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
resource "aws_instance" "foo" {
|
||||
}
|
||||
5
internal/tofu/testdata/skip-destroy/main.tf
vendored
Normal file
5
internal/tofu/testdata/skip-destroy/main.tf
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
resource "aws_instance" "foo" {
|
||||
lifecycle {
|
||||
destroy = false
|
||||
}
|
||||
}
|
||||
@@ -105,6 +105,9 @@ func (t *DiffTransformer) Transform(_ context.Context, g *Graph) error {
|
||||
delete = true
|
||||
case plans.Forget:
|
||||
forget = true
|
||||
case plans.ForgetThenCreate:
|
||||
update = true
|
||||
forget = true
|
||||
case plans.DeleteThenCreate, plans.CreateThenDelete:
|
||||
update = true
|
||||
delete = true
|
||||
@@ -239,6 +242,13 @@ func (t *DiffTransformer) Transform(_ context.Context, g *Graph) error {
|
||||
for _, rsrcNode := range resourceNodes[rsrcAddr] {
|
||||
g.Connect(dag.BasicEdge(node, rsrcNode))
|
||||
}
|
||||
|
||||
if forget {
|
||||
// We need to first forget the resource instance and make anew
|
||||
forgetNode := produceForgetNode(addr, dk)
|
||||
g.Add(forgetNode)
|
||||
g.Connect(dag.BasicEdge(node, forgetNode))
|
||||
}
|
||||
}
|
||||
|
||||
if delete {
|
||||
@@ -269,24 +279,8 @@ func (t *DiffTransformer) Transform(_ context.Context, 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)
|
||||
if forget && !update {
|
||||
g.Add(produceForgetNode(addr, dk))
|
||||
}
|
||||
|
||||
}
|
||||
@@ -295,3 +289,22 @@ func (t *DiffTransformer) Transform(_ context.Context, g *Graph) error {
|
||||
|
||||
return diags.Err()
|
||||
}
|
||||
|
||||
func produceForgetNode(addr addrs.AbsResourceInstance, deposedKey states.DeposedKey) GraphNodeResourceInstance {
|
||||
var node GraphNodeResourceInstance
|
||||
abstract := NewNodeAbstractResourceInstance(addr)
|
||||
if deposedKey == states.NotDeposed {
|
||||
node = &NodeForgetResourceInstance{
|
||||
NodeAbstractResourceInstance: abstract,
|
||||
DeposedKey: deposedKey,
|
||||
}
|
||||
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: deposedKey,
|
||||
}
|
||||
log.Printf("[TRACE] DiffTransformer: %s deposed object %s will be represented for removal from the state by %s", addr, deposedKey, dag.VertexName(node))
|
||||
}
|
||||
return node
|
||||
}
|
||||
|
||||
@@ -39,3 +39,32 @@ tofu plan -destroy
|
||||
|
||||
This will run [`tofu plan`](plan.mdx) in _destroy_ mode, showing
|
||||
you the proposed destroy changes without executing them.
|
||||
|
||||
## Forgotten Resources
|
||||
|
||||
<span id="forgotten-resources"></span>
|
||||
|
||||
When you run `tofu destroy`, OpenTofu will attempt to destroy all resources
|
||||
managed by the configuration. However, if any resources have the
|
||||
[`lifecycle.destroy`](../../language/resources/behavior.mdx#lifecycle-customizations)
|
||||
meta-argument set to `false`, those resources will be "forgotten" instead of
|
||||
destroyed.
|
||||
|
||||
When resources are forgotten:
|
||||
|
||||
- They are removed from the OpenTofu state file
|
||||
- The actual infrastructure objects remain intact in your cloud provider or remote system
|
||||
- The `tofu destroy` command exits with a non-zero status code to indicate
|
||||
that not all resources were fully removed
|
||||
|
||||
This exit code behavior might be important for automation and CI/CD pipelines, as it
|
||||
signals that the destroy operation did not complete as a typical destroy would.
|
||||
|
||||
:::warning
|
||||
The `destroy` attribute is persisted in the state file, even when resources are removed from the configuration.
|
||||
Meaning that resources will **not** be destroyed even when removed from the configuration.
|
||||
If you want to fully destroy such resources (still in the state), you must first add the resource configuration back and
|
||||
remove the `lifecycle.destroy` attribute (or set it to `true`).
|
||||
|
||||
Alternatively, you can add the `removed` block for the removed resource with `lifecycle.destroy = true`, which will override the `destroy` attribute in the state file.
|
||||
:::
|
||||
|
||||
@@ -162,6 +162,63 @@ block for a managed resource:
|
||||
case, the `prevent_destroy` setting is removed along with it, and so OpenTofu
|
||||
would allow the destroy operation to succeed.
|
||||
|
||||
In case you want to retain the infrastructure object even after removing the
|
||||
`resource` block from configuration, consider using the `destroy` argument
|
||||
documented below instead. Note that, if you set `destroy = false`, OpenTofu will
|
||||
change the `destroy` actions to a variant of `forget` action, and `prevent_destroy`
|
||||
will not have any effect.
|
||||
|
||||
* `destroy` (bool) - By default, when a resource is removed from the configuration,
|
||||
requires replacement, or is explicitly destroyed using the `tofu destroy` command,
|
||||
OpenTofu will destroy the corresponding infrastructure object. Setting this
|
||||
meta-argument to `false` changes this behavior so that OpenTofu will instead
|
||||
"forget" the resource instance, removing it from the state without destroying the actual
|
||||
infrastructure object.
|
||||
|
||||
:::note
|
||||
`lifecycle.destroy` only accepts constant boolean values (`true` or `false`).
|
||||
:::
|
||||
|
||||
When a resource with `destroy = false` is removed from the configuration or requires replacement,
|
||||
OpenTofu will plan to **forget** it rather than **destroy** it. The resource will
|
||||
be removed from the state file, but the actual infrastructure object will
|
||||
remain unchanged in your cloud provider or other remote system. If the resource requires replacement,
|
||||
OpenTofu will then create a new resource instance as per the current configuration.
|
||||
|
||||
This is useful when you want to keep the resource in configuration but alter its destruction behavior. Incomplete list of use cases includes:
|
||||
- Retain critical or compliance resources even when destroying the rest of the environment, without manually removing them from configuration beforehand.
|
||||
Enables more automation with reviewable configuration changes.
|
||||
- Retain old resource instances when performing resource replacements or infrastructure upgrades,
|
||||
avoiding downtime or data loss. Making potential rollbacks faster and easier.
|
||||
- Certain resource instances might be impossible or impractical to destroy due to external dependencies or constraints.
|
||||
|
||||
Note that this argument is **persisted in the state**, once you set and apply `destroy = false` for a resource
|
||||
OpenTofu will not plan the resource destruction unless you explicitly change it back to `true` or remove the option from the corresponding resource configuration block.
|
||||
OpenTofu errs on the side of caution and avoids destroying resources that were marked with `destroy = false` in the last applied configuration for the resource instance.
|
||||
If you are using single instance resources (no count or for_each), you can override this attribute in the state by writing explicit `removed` block for the resource instance with `destroy = true` option.
|
||||
|
||||
:::note
|
||||
This argument can also be used in [`removed` blocks](syntax.mdx#removing-resources)
|
||||
to control whether resources should be destroyed or forgotten when explicitly
|
||||
removing them from OpenTofu management. The behavior is identical in both contexts.
|
||||
Generally, prefer to use `removed` blocks when you want to remove resources from your configurations as a method of refactoring.
|
||||
Use `destroy` lifecycle argument when you want to control the destruction behavior of resources that are still present in your configuration.
|
||||
:::
|
||||
|
||||
The `destroy` argument also applies when using the `tofu destroy` command.
|
||||
Resources with `destroy = false` will be forgotten rather than destroyed,
|
||||
and the command will exit with a non-zero status code to indicate that some
|
||||
resources were not fully removed. See the [`tofu destroy` command documentation](../../cli/commands/destroy.mdx#forgotten-resources)
|
||||
for more details.
|
||||
|
||||
:::warning
|
||||
Once a resource is forgotten (removed from state), OpenTofu will no longer
|
||||
track it. If you later add the resource back to your configuration with the
|
||||
same address, OpenTofu will attempt to create a new resource, which may fail
|
||||
if the infrastructure object still exists. You may need to import the existing
|
||||
resource or use a different resource address.
|
||||
:::
|
||||
|
||||
* <span id="ignore_changes">`ignore_changes`</span> (list of attribute names) - By default, OpenTofu detects
|
||||
any difference in the current settings of a real infrastructure object
|
||||
and plans to update the remote object to match configuration.
|
||||
|
||||
Reference in New Issue
Block a user