From e1b159b015152a5bc6fbf942a3a8ee8a79c2da4d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Di=C3=B3genes=20Fernandes?= Date: Wed, 10 Dec 2025 12:31:50 -0300 Subject: [PATCH] fix: bug when deleting a resource using enabled on `tofu plan -out` (#3566) Signed-off-by: Diogenes Fernandes --- .../plans/internal/planproto/planfile.pb.go | 8 +- .../plans/internal/planproto/planfile.proto | 1 + internal/plans/planfile/tfplan.go | 4 + internal/plans/planfile/tfplan_test.go | 108 ++++++++++++++++++ 4 files changed, 119 insertions(+), 2 deletions(-) diff --git a/internal/plans/internal/planproto/planfile.pb.go b/internal/plans/internal/planproto/planfile.pb.go index 70d048c457..b94d75ceb2 100644 --- a/internal/plans/internal/planproto/planfile.pb.go +++ b/internal/plans/internal/planproto/planfile.pb.go @@ -166,6 +166,7 @@ const ( ResourceInstanceActionReason_READ_BECAUSE_DEPENDENCY_PENDING ResourceInstanceActionReason = 11 ResourceInstanceActionReason_READ_BECAUSE_CHECK_NESTED ResourceInstanceActionReason = 13 ResourceInstanceActionReason_DELETE_BECAUSE_NO_MOVE_TARGET ResourceInstanceActionReason = 12 + ResourceInstanceActionReason_DELETE_BECAUSE_ENABLED_FALSE ResourceInstanceActionReason = 14 ) // Enum value maps for ResourceInstanceActionReason. @@ -185,6 +186,7 @@ var ( 11: "READ_BECAUSE_DEPENDENCY_PENDING", 13: "READ_BECAUSE_CHECK_NESTED", 12: "DELETE_BECAUSE_NO_MOVE_TARGET", + 14: "DELETE_BECAUSE_ENABLED_FALSE", } ResourceInstanceActionReason_value = map[string]int32{ "NONE": 0, @@ -201,6 +203,7 @@ var ( "READ_BECAUSE_DEPENDENCY_PENDING": 11, "READ_BECAUSE_CHECK_NESTED": 13, "DELETE_BECAUSE_NO_MOVE_TARGET": 12, + "DELETE_BECAUSE_ENABLED_FALSE": 14, } ) @@ -1444,7 +1447,7 @@ const file_planfile_proto_rawDesc = "" + "\x12CREATE_THEN_DELETE\x10\a\x12\n" + "\n" + "\x06FORGET\x10\b\x12\b\n" + - "\x04OPEN\x10\t*\xc8\x03\n" + + "\x04OPEN\x10\t*\xea\x03\n" + "\x1cResourceInstanceActionReason\x12\b\n" + "\x04NONE\x10\x00\x12\x1b\n" + "\x17REPLACE_BECAUSE_TAINTED\x10\x01\x12\x16\n" + @@ -1460,7 +1463,8 @@ const file_planfile_proto_rawDesc = "" + "\x12#\n" + "\x1fREAD_BECAUSE_DEPENDENCY_PENDING\x10\v\x12\x1d\n" + "\x19READ_BECAUSE_CHECK_NESTED\x10\r\x12!\n" + - "\x1dDELETE_BECAUSE_NO_MOVE_TARGET\x10\fB@Z>github.com/opentofu/opentofu/internal/plans/internal/planprotob\x06proto3" + "\x1dDELETE_BECAUSE_NO_MOVE_TARGET\x10\f\x12 \n" + + "\x1cDELETE_BECAUSE_ENABLED_FALSE\x10\x0eB@Z>github.com/opentofu/opentofu/internal/plans/internal/planprotob\x06proto3" var ( file_planfile_proto_rawDescOnce sync.Once diff --git a/internal/plans/internal/planproto/planfile.proto b/internal/plans/internal/planproto/planfile.proto index be9b4be602..84c75b2ca5 100644 --- a/internal/plans/internal/planproto/planfile.proto +++ b/internal/plans/internal/planproto/planfile.proto @@ -185,6 +185,7 @@ enum ResourceInstanceActionReason { READ_BECAUSE_DEPENDENCY_PENDING = 11; READ_BECAUSE_CHECK_NESTED = 13; DELETE_BECAUSE_NO_MOVE_TARGET = 12; + DELETE_BECAUSE_ENABLED_FALSE = 14; } message ResourceInstanceChange { diff --git a/internal/plans/planfile/tfplan.go b/internal/plans/planfile/tfplan.go index 88801741cf..2efe259902 100644 --- a/internal/plans/planfile/tfplan.go +++ b/internal/plans/planfile/tfplan.go @@ -364,6 +364,8 @@ func resourceChangeFromTfplan(rawChange *planproto.ResourceInstanceChange) (*pla ret.ActionReason = plans.ResourceInstanceReadBecauseCheckNested case planproto.ResourceInstanceActionReason_DELETE_BECAUSE_NO_MOVE_TARGET: ret.ActionReason = plans.ResourceInstanceDeleteBecauseNoMoveTarget + case planproto.ResourceInstanceActionReason_DELETE_BECAUSE_ENABLED_FALSE: + ret.ActionReason = plans.ResourceInstanceDeleteBecauseEnabledFalse default: return nil, fmt.Errorf("resource has invalid action reason %s", rawChange.ActionReason) } @@ -771,6 +773,8 @@ func resourceChangeToTfplan(change *plans.ResourceInstanceChangeSrc) (*planproto ret.ActionReason = planproto.ResourceInstanceActionReason_READ_BECAUSE_CHECK_NESTED case plans.ResourceInstanceDeleteBecauseNoMoveTarget: ret.ActionReason = planproto.ResourceInstanceActionReason_DELETE_BECAUSE_NO_MOVE_TARGET + case plans.ResourceInstanceDeleteBecauseEnabledFalse: + ret.ActionReason = planproto.ResourceInstanceActionReason_DELETE_BECAUSE_ENABLED_FALSE default: return nil, fmt.Errorf("resource %s has unsupported action reason %s", change.Addr, change.ActionReason) } diff --git a/internal/plans/planfile/tfplan_test.go b/internal/plans/planfile/tfplan_test.go index 8ee63f421f..2a5f30d9ba 100644 --- a/internal/plans/planfile/tfplan_test.go +++ b/internal/plans/planfile/tfplan_test.go @@ -473,3 +473,111 @@ func TestTFPlanRoundTripDestroy(t *testing.T) { } } } + +func TestTFPlanChangeReasonsEncoding(t *testing.T) { + tests := []struct { + name string + action plans.Action + actionReason plans.ResourceInstanceChangeActionReason + }{ + { + name: "ResourceInstanceDeleteBecauseEnabledFalse", + action: plans.Delete, + actionReason: plans.ResourceInstanceDeleteBecauseEnabledFalse, + }, + { + name: "ResourceInstanceDeleteBecauseNoResourceConfig", + action: plans.Delete, + actionReason: plans.ResourceInstanceDeleteBecauseNoResourceConfig, + }, + { + name: "ResourceInstanceDeleteBecauseWrongRepetition", + action: plans.Delete, + actionReason: plans.ResourceInstanceDeleteBecauseWrongRepetition, + }, + { + name: "ResourceInstanceDeleteBecauseCountIndex", + action: plans.Delete, + actionReason: plans.ResourceInstanceDeleteBecauseCountIndex, + }, + { + name: "ResourceInstanceDeleteBecauseEachKey", + action: plans.Delete, + actionReason: plans.ResourceInstanceDeleteBecauseEachKey, + }, + { + name: "ResourceInstanceDeleteBecauseNoModule", + action: plans.Delete, + actionReason: plans.ResourceInstanceDeleteBecauseNoModule, + }, + { + name: "ResourceInstanceDeleteBecauseNoMoveTarget", + action: plans.Delete, + actionReason: plans.ResourceInstanceDeleteBecauseNoMoveTarget, + }, + } + + for _, test := range tests { + objTy := cty.Object(map[string]cty.Type{ + "id": cty.String, + }) + + plan := &plans.Plan{ + Backend: plans.Backend{ + Type: "local", + Config: mustNewDynamicValue( + cty.ObjectVal(map[string]cty.Value{ + "foo": cty.StringVal("bar"), + }), + cty.Object(map[string]cty.Type{ + "foo": cty.String, + }), + ), + Workspace: "default", + }, + Changes: &plans.Changes{ + Resources: []*plans.ResourceInstanceChangeSrc{ + { + Addr: addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "test_thing", + Name: "woot", + }.Instance(addrs.IntKey(0)).Absolute(addrs.RootModuleInstance), + PrevRunAddr: addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "test_thing", + Name: "woot", + }.Instance(addrs.IntKey(0)).Absolute(addrs.RootModuleInstance), + ProviderAddr: addrs.AbsProviderConfig{ + Provider: addrs.NewDefaultProvider("test"), + Module: addrs.RootModule, + }, + ChangeSrc: plans.ChangeSrc{ + Action: test.action, + Before: mustNewDynamicValue(cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("foo-bar-baz"), + }), objTy), + After: mustNewDynamicValue(cty.NullVal(objTy), objTy), + }, + ActionReason: test.actionReason, + }, + }, + }, + } + + var buf bytes.Buffer + err := writeTfplan(plan, &buf) + if err != nil { + t.Fatal(err) + } + + _, err = readTfplan(&buf) + if err != nil { + t.Fatal(err) + } + + if err != nil { + t.Fatal("should've succeeded, got error: ", err) + } + } +}