github.com/opentofu/opentofu@v1.7.1/internal/tofu/node_resource_plan_orphan_test.go (about) 1 // Copyright (c) The OpenTofu Authors 2 // SPDX-License-Identifier: MPL-2.0 3 // Copyright (c) 2023 HashiCorp, Inc. 4 // SPDX-License-Identifier: MPL-2.0 5 6 package tofu 7 8 import ( 9 "testing" 10 11 "github.com/opentofu/opentofu/internal/configs/configschema" 12 13 "github.com/opentofu/opentofu/internal/addrs" 14 "github.com/opentofu/opentofu/internal/instances" 15 "github.com/opentofu/opentofu/internal/plans" 16 "github.com/opentofu/opentofu/internal/providers" 17 "github.com/opentofu/opentofu/internal/states" 18 "github.com/zclconf/go-cty/cty" 19 ) 20 21 func TestNodeResourcePlanOrphan_Execute(t *testing.T) { 22 tests := []struct { 23 description string 24 nodeAddress string 25 nodeEndpointsToRemove []addrs.ConfigRemovable 26 wantAction plans.Action 27 }{ 28 { 29 nodeAddress: "test_instance.foo", 30 nodeEndpointsToRemove: make([]addrs.ConfigRemovable, 0), 31 wantAction: plans.Delete, 32 }, 33 { 34 nodeAddress: "test_instance.foo", 35 nodeEndpointsToRemove: []addrs.ConfigRemovable{ 36 interface{}(mustConfigResourceAddr("test_instance.bar")).(addrs.ConfigRemovable), 37 }, 38 wantAction: plans.Delete, 39 }, 40 { 41 nodeAddress: "test_instance.foo", 42 nodeEndpointsToRemove: []addrs.ConfigRemovable{ 43 interface{}(addrs.Module{"boop"}).(addrs.ConfigRemovable), 44 }, 45 wantAction: plans.Delete, 46 }, 47 { 48 nodeAddress: "test_instance.foo", 49 nodeEndpointsToRemove: []addrs.ConfigRemovable{ 50 interface{}(mustConfigResourceAddr("test_instance.foo")).(addrs.ConfigRemovable), 51 }, 52 wantAction: plans.Forget, 53 }, 54 { 55 nodeAddress: "test_instance.foo[1]", 56 nodeEndpointsToRemove: []addrs.ConfigRemovable{ 57 interface{}(mustConfigResourceAddr("test_instance.foo")).(addrs.ConfigRemovable), 58 }, 59 wantAction: plans.Forget, 60 }, 61 { 62 nodeAddress: "module.boop.test_instance.foo", 63 nodeEndpointsToRemove: []addrs.ConfigRemovable{ 64 interface{}(mustConfigResourceAddr("module.boop.test_instance.foo")).(addrs.ConfigRemovable), 65 }, 66 wantAction: plans.Forget, 67 }, 68 { 69 nodeAddress: "module.boop[1].test_instance.foo[1]", 70 nodeEndpointsToRemove: []addrs.ConfigRemovable{ 71 interface{}(mustConfigResourceAddr("module.boop.test_instance.foo")).(addrs.ConfigRemovable), 72 }, 73 wantAction: plans.Forget, 74 }, 75 { 76 nodeAddress: "module.boop.test_instance.foo", 77 nodeEndpointsToRemove: []addrs.ConfigRemovable{ 78 interface{}(addrs.Module{"boop"}).(addrs.ConfigRemovable), 79 }, 80 wantAction: plans.Forget, 81 }, 82 { 83 nodeAddress: "module.boop[1].test_instance.foo", 84 nodeEndpointsToRemove: []addrs.ConfigRemovable{ 85 interface{}(addrs.Module{"boop"}).(addrs.ConfigRemovable), 86 }, 87 wantAction: plans.Forget, 88 }, 89 } 90 91 for _, test := range tests { 92 state := states.NewState() 93 absResource := mustResourceInstanceAddr(test.nodeAddress) 94 95 if !absResource.Module.Module().Equal(addrs.RootModule) { 96 state.EnsureModule(addrs.RootModuleInstance.Child(absResource.Module[0].Name, absResource.Module[0].InstanceKey)) 97 } 98 99 state.Module(absResource.Module).SetResourceInstanceCurrent( 100 absResource.Resource, 101 &states.ResourceInstanceObjectSrc{ 102 AttrsFlat: map[string]string{ 103 "test_string": "foo", 104 }, 105 Status: states.ObjectReady, 106 }, 107 addrs.AbsProviderConfig{ 108 Provider: addrs.NewDefaultProvider("test"), 109 Module: addrs.RootModule, 110 }, 111 ) 112 113 schema := providers.ProviderSchema{ 114 ResourceTypes: map[string]providers.Schema{ 115 "test_instance": { 116 Block: &configschema.Block{ 117 Attributes: map[string]*configschema.Attribute{ 118 "id": { 119 Type: cty.String, 120 Computed: true, 121 }, 122 }, 123 }, 124 }, 125 }, 126 } 127 128 p := simpleMockProvider() 129 p.ConfigureProvider(providers.ConfigureProviderRequest{}) 130 p.GetProviderSchemaResponse = &schema 131 132 ctx := &MockEvalContext{ 133 StateState: state.SyncWrapper(), 134 RefreshStateState: state.DeepCopy().SyncWrapper(), 135 PrevRunStateState: state.DeepCopy().SyncWrapper(), 136 InstanceExpanderExpander: instances.NewExpander(), 137 ProviderProvider: p, 138 ProviderSchemaSchema: schema, 139 ChangesChanges: plans.NewChanges().SyncWrapper(), 140 } 141 142 node := NodePlannableResourceInstanceOrphan{ 143 NodeAbstractResourceInstance: &NodeAbstractResourceInstance{ 144 NodeAbstractResource: NodeAbstractResource{ 145 ResolvedProvider: addrs.AbsProviderConfig{ 146 Provider: addrs.NewDefaultProvider("test"), 147 Module: addrs.RootModule, 148 }, 149 }, 150 Addr: absResource, 151 }, 152 EndpointsToRemove: test.nodeEndpointsToRemove, 153 } 154 155 err := node.Execute(ctx, walkPlan) 156 if err != nil { 157 t.Fatalf("unexpected error: %s", err) 158 } 159 160 change := ctx.Changes().GetResourceInstanceChange(absResource, states.NotDeposed) 161 if got, want := change.ChangeSrc.Action, test.wantAction; got != want { 162 t.Fatalf("wrong planned action\ngot: %s\nwant: %s", got, want) 163 } 164 165 if !state.Empty() { 166 t.Fatalf("expected empty state, got %s", state.String()) 167 } 168 } 169 } 170 171 func TestNodeResourcePlanOrphanExecute_alreadyDeleted(t *testing.T) { 172 addr := addrs.Resource{ 173 Mode: addrs.ManagedResourceMode, 174 Type: "test_object", 175 Name: "foo", 176 }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance) 177 178 state := states.NewState() 179 state.Module(addrs.RootModuleInstance).SetResourceInstanceCurrent( 180 addr.Resource, 181 &states.ResourceInstanceObjectSrc{ 182 AttrsFlat: map[string]string{ 183 "test_string": "foo", 184 }, 185 Status: states.ObjectReady, 186 }, 187 addrs.AbsProviderConfig{ 188 Provider: addrs.NewDefaultProvider("test"), 189 Module: addrs.RootModule, 190 }, 191 ) 192 refreshState := state.DeepCopy() 193 prevRunState := state.DeepCopy() 194 changes := plans.NewChanges() 195 196 p := simpleMockProvider() 197 p.ConfigureProvider(providers.ConfigureProviderRequest{}) 198 p.ReadResourceResponse = &providers.ReadResourceResponse{ 199 NewState: cty.NullVal(p.GetProviderSchemaResponse.ResourceTypes["test_string"].Block.ImpliedType()), 200 } 201 ctx := &MockEvalContext{ 202 StateState: state.SyncWrapper(), 203 RefreshStateState: refreshState.SyncWrapper(), 204 PrevRunStateState: prevRunState.SyncWrapper(), 205 InstanceExpanderExpander: instances.NewExpander(), 206 ProviderProvider: p, 207 ProviderSchemaSchema: providers.ProviderSchema{ 208 ResourceTypes: map[string]providers.Schema{ 209 "test_object": { 210 Block: simpleTestSchema(), 211 }, 212 }, 213 }, 214 ChangesChanges: changes.SyncWrapper(), 215 } 216 217 node := NodePlannableResourceInstanceOrphan{ 218 NodeAbstractResourceInstance: &NodeAbstractResourceInstance{ 219 NodeAbstractResource: NodeAbstractResource{ 220 ResolvedProvider: addrs.AbsProviderConfig{ 221 Provider: addrs.NewDefaultProvider("test"), 222 Module: addrs.RootModule, 223 }, 224 }, 225 Addr: mustResourceInstanceAddr("test_object.foo"), 226 }, 227 } 228 diags := node.Execute(ctx, walkPlan) 229 if diags.HasErrors() { 230 t.Fatalf("unexpected error: %s", diags.Err()) 231 } 232 if !state.Empty() { 233 t.Fatalf("expected empty state, got %s", state.String()) 234 } 235 236 if got := prevRunState.ResourceInstance(addr); got == nil { 237 t.Errorf("no entry for %s in the prev run state; should still be present", addr) 238 } 239 if got := refreshState.ResourceInstance(addr); got != nil { 240 t.Errorf("refresh state has entry for %s; should've been removed", addr) 241 } 242 if got := changes.ResourceInstance(addr); got != nil { 243 t.Errorf("there should be no change for the %s instance, got %s", addr, got.Action) 244 } 245 } 246 247 // This test describes a situation which should not be possible, as this node 248 // should never work on deposed instances. However, a bug elsewhere resulted in 249 // this code path being exercised and triggered a panic. As a result, the 250 // assertions at the end of the test are minimal, as the behaviour (aside from 251 // not panicking) is unspecified. 252 func TestNodeResourcePlanOrphanExecute_deposed(t *testing.T) { 253 addr := addrs.Resource{ 254 Mode: addrs.ManagedResourceMode, 255 Type: "test_object", 256 Name: "foo", 257 }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance) 258 259 state := states.NewState() 260 state.Module(addrs.RootModuleInstance).SetResourceInstanceDeposed( 261 addr.Resource, 262 states.NewDeposedKey(), 263 &states.ResourceInstanceObjectSrc{ 264 AttrsFlat: map[string]string{ 265 "test_string": "foo", 266 }, 267 Status: states.ObjectReady, 268 }, 269 addrs.AbsProviderConfig{ 270 Provider: addrs.NewDefaultProvider("test"), 271 Module: addrs.RootModule, 272 }, 273 ) 274 refreshState := state.DeepCopy() 275 prevRunState := state.DeepCopy() 276 changes := plans.NewChanges() 277 278 p := simpleMockProvider() 279 p.ConfigureProvider(providers.ConfigureProviderRequest{}) 280 p.ReadResourceResponse = &providers.ReadResourceResponse{ 281 NewState: cty.NullVal(p.GetProviderSchemaResponse.ResourceTypes["test_string"].Block.ImpliedType()), 282 } 283 ctx := &MockEvalContext{ 284 StateState: state.SyncWrapper(), 285 RefreshStateState: refreshState.SyncWrapper(), 286 PrevRunStateState: prevRunState.SyncWrapper(), 287 InstanceExpanderExpander: instances.NewExpander(), 288 ProviderProvider: p, 289 ProviderSchemaSchema: providers.ProviderSchema{ 290 ResourceTypes: map[string]providers.Schema{ 291 "test_object": { 292 Block: simpleTestSchema(), 293 }, 294 }, 295 }, 296 ChangesChanges: changes.SyncWrapper(), 297 } 298 299 node := NodePlannableResourceInstanceOrphan{ 300 NodeAbstractResourceInstance: &NodeAbstractResourceInstance{ 301 NodeAbstractResource: NodeAbstractResource{ 302 ResolvedProvider: addrs.AbsProviderConfig{ 303 Provider: addrs.NewDefaultProvider("test"), 304 Module: addrs.RootModule, 305 }, 306 }, 307 Addr: mustResourceInstanceAddr("test_object.foo"), 308 }, 309 } 310 diags := node.Execute(ctx, walkPlan) 311 if diags.HasErrors() { 312 t.Fatalf("unexpected error: %s", diags.Err()) 313 } 314 }