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  }