github.com/opentofu/opentofu@v1.7.1/internal/tofu/graph_builder_apply_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  	"fmt"
    10  	"strings"
    11  	"testing"
    12  
    13  	"github.com/google/go-cmp/cmp"
    14  	"github.com/zclconf/go-cty/cty"
    15  
    16  	"github.com/opentofu/opentofu/internal/addrs"
    17  	"github.com/opentofu/opentofu/internal/plans"
    18  	"github.com/opentofu/opentofu/internal/providers"
    19  	"github.com/opentofu/opentofu/internal/states"
    20  )
    21  
    22  func TestApplyGraphBuilder_impl(t *testing.T) {
    23  	var _ GraphBuilder = new(ApplyGraphBuilder)
    24  }
    25  
    26  func TestApplyGraphBuilder(t *testing.T) {
    27  	changes := &plans.Changes{
    28  		Resources: []*plans.ResourceInstanceChangeSrc{
    29  			{
    30  				Addr: mustResourceInstanceAddr("test_object.create"),
    31  				ChangeSrc: plans.ChangeSrc{
    32  					Action: plans.Create,
    33  				},
    34  			},
    35  			{
    36  				Addr: mustResourceInstanceAddr("test_object.other"),
    37  				ChangeSrc: plans.ChangeSrc{
    38  					Action: plans.Update,
    39  				},
    40  			},
    41  			{
    42  				Addr: mustResourceInstanceAddr("module.child.test_object.create"),
    43  				ChangeSrc: plans.ChangeSrc{
    44  					Action: plans.Create,
    45  				},
    46  			},
    47  			{
    48  				Addr: mustResourceInstanceAddr("module.child.test_object.other"),
    49  				ChangeSrc: plans.ChangeSrc{
    50  					Action: plans.Create,
    51  				},
    52  			},
    53  		},
    54  	}
    55  
    56  	b := &ApplyGraphBuilder{
    57  		Config:  testModule(t, "graph-builder-apply-basic"),
    58  		Changes: changes,
    59  		Plugins: simpleMockPluginLibrary(),
    60  	}
    61  
    62  	g, err := b.Build(addrs.RootModuleInstance)
    63  	if err != nil {
    64  		t.Fatalf("err: %s", err)
    65  	}
    66  
    67  	if g.Path.String() != addrs.RootModuleInstance.String() {
    68  		t.Fatalf("wrong path %q", g.Path.String())
    69  	}
    70  
    71  	got := strings.TrimSpace(g.String())
    72  	want := strings.TrimSpace(testApplyGraphBuilderStr)
    73  	if diff := cmp.Diff(want, got); diff != "" {
    74  		t.Fatalf("wrong result\n%s", diff)
    75  	}
    76  }
    77  
    78  // This tests the ordering of two resources where a non-CBD depends
    79  // on a CBD. GH-11349.
    80  func TestApplyGraphBuilder_depCbd(t *testing.T) {
    81  	changes := &plans.Changes{
    82  		Resources: []*plans.ResourceInstanceChangeSrc{
    83  			{
    84  				Addr: mustResourceInstanceAddr("test_object.A"),
    85  				ChangeSrc: plans.ChangeSrc{
    86  					Action: plans.CreateThenDelete,
    87  				},
    88  			},
    89  			{
    90  				Addr: mustResourceInstanceAddr("test_object.B"),
    91  				ChangeSrc: plans.ChangeSrc{
    92  					Action: plans.Update,
    93  				},
    94  			},
    95  		},
    96  	}
    97  
    98  	state := states.NewState()
    99  	root := state.EnsureModule(addrs.RootModuleInstance)
   100  	root.SetResourceInstanceCurrent(
   101  		mustResourceInstanceAddr("test_object.A").Resource,
   102  		&states.ResourceInstanceObjectSrc{
   103  			Status:    states.ObjectReady,
   104  			AttrsJSON: []byte(`{"id":"A"}`),
   105  		},
   106  		mustProviderConfig(`provider["registry.opentofu.org/hashicorp/test"]`),
   107  	)
   108  	root.SetResourceInstanceCurrent(
   109  		mustResourceInstanceAddr("test_object.B").Resource,
   110  		&states.ResourceInstanceObjectSrc{
   111  			Status:       states.ObjectReady,
   112  			AttrsJSON:    []byte(`{"id":"B","test_list":["x"]}`),
   113  			Dependencies: []addrs.ConfigResource{mustConfigResourceAddr("test_object.A")},
   114  		},
   115  		mustProviderConfig(`provider["registry.opentofu.org/hashicorp/test"]`),
   116  	)
   117  
   118  	b := &ApplyGraphBuilder{
   119  		Config:  testModule(t, "graph-builder-apply-dep-cbd"),
   120  		Changes: changes,
   121  		Plugins: simpleMockPluginLibrary(),
   122  		State:   state,
   123  	}
   124  
   125  	g, err := b.Build(addrs.RootModuleInstance)
   126  	if err != nil {
   127  		t.Fatalf("err: %s", err)
   128  	}
   129  
   130  	if g.Path.String() != addrs.RootModuleInstance.String() {
   131  		t.Fatalf("wrong path %q", g.Path.String())
   132  	}
   133  
   134  	// We're going to go hunting for our deposed instance node here, so we
   135  	// can find out its key to use in the assertions below.
   136  	var dk states.DeposedKey
   137  	for _, v := range g.Vertices() {
   138  		tv, ok := v.(*NodeDestroyDeposedResourceInstanceObject)
   139  		if !ok {
   140  			continue
   141  		}
   142  		if dk != states.NotDeposed {
   143  			t.Fatalf("more than one deposed instance node in the graph; want only one")
   144  		}
   145  		dk = tv.DeposedKey
   146  	}
   147  	if dk == states.NotDeposed {
   148  		t.Fatalf("no deposed instance node in the graph; want one")
   149  	}
   150  
   151  	destroyName := fmt.Sprintf("test_object.A (destroy deposed %s)", dk)
   152  
   153  	// Create A, Modify B, Destroy A
   154  	testGraphHappensBefore(
   155  		t, g,
   156  		"test_object.A",
   157  		destroyName,
   158  	)
   159  	testGraphHappensBefore(
   160  		t, g,
   161  		"test_object.A",
   162  		"test_object.B",
   163  	)
   164  	testGraphHappensBefore(
   165  		t, g,
   166  		"test_object.B",
   167  		destroyName,
   168  	)
   169  }
   170  
   171  // This tests the ordering of two resources that are both CBD that
   172  // require destroy/create.
   173  func TestApplyGraphBuilder_doubleCBD(t *testing.T) {
   174  	changes := &plans.Changes{
   175  		Resources: []*plans.ResourceInstanceChangeSrc{
   176  			{
   177  				Addr: mustResourceInstanceAddr("test_object.A"),
   178  				ChangeSrc: plans.ChangeSrc{
   179  					Action: plans.CreateThenDelete,
   180  				},
   181  			},
   182  			{
   183  				Addr: mustResourceInstanceAddr("test_object.B"),
   184  				ChangeSrc: plans.ChangeSrc{
   185  					Action: plans.CreateThenDelete,
   186  				},
   187  			},
   188  		},
   189  	}
   190  
   191  	b := &ApplyGraphBuilder{
   192  		Config:  testModule(t, "graph-builder-apply-double-cbd"),
   193  		Changes: changes,
   194  		Plugins: simpleMockPluginLibrary(),
   195  	}
   196  
   197  	g, err := b.Build(addrs.RootModuleInstance)
   198  	if err != nil {
   199  		t.Fatalf("err: %s", err)
   200  	}
   201  
   202  	if g.Path.String() != addrs.RootModuleInstance.String() {
   203  		t.Fatalf("wrong path %q", g.Path.String())
   204  	}
   205  
   206  	// We're going to go hunting for our deposed instance node here, so we
   207  	// can find out its key to use in the assertions below.
   208  	var destroyA, destroyB string
   209  	for _, v := range g.Vertices() {
   210  		tv, ok := v.(*NodeDestroyDeposedResourceInstanceObject)
   211  		if !ok {
   212  			continue
   213  		}
   214  
   215  		switch tv.Addr.Resource.Resource.Name {
   216  		case "A":
   217  			destroyA = fmt.Sprintf("test_object.A (destroy deposed %s)", tv.DeposedKey)
   218  		case "B":
   219  			destroyB = fmt.Sprintf("test_object.B (destroy deposed %s)", tv.DeposedKey)
   220  		default:
   221  			t.Fatalf("unknown instance: %s", tv.Addr)
   222  		}
   223  	}
   224  
   225  	// Create A, Modify B, Destroy A
   226  	testGraphHappensBefore(
   227  		t, g,
   228  		"test_object.A",
   229  		destroyA,
   230  	)
   231  	testGraphHappensBefore(
   232  		t, g,
   233  		"test_object.A",
   234  		"test_object.B",
   235  	)
   236  	testGraphHappensBefore(
   237  		t, g,
   238  		"test_object.B",
   239  		destroyB,
   240  	)
   241  }
   242  
   243  // This tests the ordering of two resources being destroyed that depend
   244  // on each other from only state. GH-11749
   245  func TestApplyGraphBuilder_destroyStateOnly(t *testing.T) {
   246  	changes := &plans.Changes{
   247  		Resources: []*plans.ResourceInstanceChangeSrc{
   248  			{
   249  				Addr: mustResourceInstanceAddr("module.child.test_object.A"),
   250  				ChangeSrc: plans.ChangeSrc{
   251  					Action: plans.Delete,
   252  				},
   253  			},
   254  			{
   255  				Addr: mustResourceInstanceAddr("module.child.test_object.B"),
   256  				ChangeSrc: plans.ChangeSrc{
   257  					Action: plans.Delete,
   258  				},
   259  			},
   260  		},
   261  	}
   262  
   263  	state := states.NewState()
   264  	root := state.EnsureModule(addrs.RootModuleInstance)
   265  	child := state.EnsureModule(addrs.RootModuleInstance.Child("child", addrs.NoKey))
   266  	root.SetResourceInstanceCurrent(
   267  		mustResourceInstanceAddr("test_object.A").Resource,
   268  		&states.ResourceInstanceObjectSrc{
   269  			Status:    states.ObjectReady,
   270  			AttrsJSON: []byte(`{"id":"foo"}`),
   271  		},
   272  		mustProviderConfig(`provider["registry.opentofu.org/hashicorp/test"]`),
   273  	)
   274  	child.SetResourceInstanceCurrent(
   275  		mustResourceInstanceAddr("test_object.B").Resource,
   276  		&states.ResourceInstanceObjectSrc{
   277  			Status:       states.ObjectReady,
   278  			AttrsJSON:    []byte(`{"id":"bar"}`),
   279  			Dependencies: []addrs.ConfigResource{mustConfigResourceAddr("module.child.test_object.A")},
   280  		},
   281  		mustProviderConfig(`provider["registry.opentofu.org/hashicorp/test"]`),
   282  	)
   283  
   284  	b := &ApplyGraphBuilder{
   285  		Config:  testModule(t, "empty"),
   286  		Changes: changes,
   287  		State:   state,
   288  		Plugins: simpleMockPluginLibrary(),
   289  	}
   290  
   291  	g, diags := b.Build(addrs.RootModuleInstance)
   292  	if diags.HasErrors() {
   293  		t.Fatalf("err: %s", diags.Err())
   294  	}
   295  
   296  	if g.Path.String() != addrs.RootModuleInstance.String() {
   297  		t.Fatalf("wrong path %q", g.Path.String())
   298  	}
   299  
   300  	testGraphHappensBefore(
   301  		t, g,
   302  		"module.child.test_object.B (destroy)",
   303  		"module.child.test_object.A (destroy)")
   304  }
   305  
   306  // This tests the ordering of destroying a single count of a resource.
   307  func TestApplyGraphBuilder_destroyCount(t *testing.T) {
   308  	changes := &plans.Changes{
   309  		Resources: []*plans.ResourceInstanceChangeSrc{
   310  			{
   311  				Addr: mustResourceInstanceAddr("test_object.A[1]"),
   312  				ChangeSrc: plans.ChangeSrc{
   313  					Action: plans.Delete,
   314  				},
   315  			},
   316  			{
   317  				Addr: mustResourceInstanceAddr("test_object.B"),
   318  				ChangeSrc: plans.ChangeSrc{
   319  					Action: plans.Update,
   320  				},
   321  			},
   322  		},
   323  	}
   324  
   325  	state := states.NewState()
   326  	root := state.RootModule()
   327  	addrA := mustResourceInstanceAddr("test_object.A[1]")
   328  	root.SetResourceInstanceCurrent(
   329  		addrA.Resource,
   330  		&states.ResourceInstanceObjectSrc{
   331  			Status:    states.ObjectReady,
   332  			AttrsJSON: []byte(`{"id":"B"}`),
   333  		},
   334  		mustProviderConfig(`provider["registry.opentofu.org/hashicorp/test"]`),
   335  	)
   336  	root.SetResourceInstanceCurrent(
   337  		mustResourceInstanceAddr("test_object.B").Resource,
   338  		&states.ResourceInstanceObjectSrc{
   339  			Status:       states.ObjectReady,
   340  			AttrsJSON:    []byte(`{"id":"B"}`),
   341  			Dependencies: []addrs.ConfigResource{addrA.ContainingResource().Config()},
   342  		},
   343  		mustProviderConfig(`provider["registry.opentofu.org/hashicorp/test"]`),
   344  	)
   345  
   346  	b := &ApplyGraphBuilder{
   347  		Config:  testModule(t, "graph-builder-apply-count"),
   348  		Changes: changes,
   349  		Plugins: simpleMockPluginLibrary(),
   350  		State:   state,
   351  	}
   352  
   353  	g, err := b.Build(addrs.RootModuleInstance)
   354  	if err != nil {
   355  		t.Fatalf("err: %s", err)
   356  	}
   357  
   358  	if g.Path.String() != addrs.RootModuleInstance.String() {
   359  		t.Fatalf("wrong module path %q", g.Path)
   360  	}
   361  
   362  	got := strings.TrimSpace(g.String())
   363  	want := strings.TrimSpace(testApplyGraphBuilderDestroyCountStr)
   364  	if diff := cmp.Diff(want, got); diff != "" {
   365  		t.Fatalf("wrong result\n%s", diff)
   366  	}
   367  }
   368  
   369  func TestApplyGraphBuilder_moduleDestroy(t *testing.T) {
   370  	changes := &plans.Changes{
   371  		Resources: []*plans.ResourceInstanceChangeSrc{
   372  			{
   373  				Addr: mustResourceInstanceAddr("module.A.test_object.foo"),
   374  				ChangeSrc: plans.ChangeSrc{
   375  					Action: plans.Delete,
   376  				},
   377  			},
   378  			{
   379  				Addr: mustResourceInstanceAddr("module.B.test_object.foo"),
   380  				ChangeSrc: plans.ChangeSrc{
   381  					Action: plans.Delete,
   382  				},
   383  			},
   384  		},
   385  	}
   386  
   387  	state := states.NewState()
   388  	modA := state.EnsureModule(addrs.RootModuleInstance.Child("A", addrs.NoKey))
   389  	modA.SetResourceInstanceCurrent(
   390  		mustResourceInstanceAddr("test_object.foo").Resource,
   391  		&states.ResourceInstanceObjectSrc{
   392  			Status:    states.ObjectReady,
   393  			AttrsJSON: []byte(`{"id":"foo"}`),
   394  		},
   395  		mustProviderConfig(`provider["registry.opentofu.org/hashicorp/test"]`),
   396  	)
   397  	modB := state.EnsureModule(addrs.RootModuleInstance.Child("B", addrs.NoKey))
   398  	modB.SetResourceInstanceCurrent(
   399  		mustResourceInstanceAddr("test_object.foo").Resource,
   400  		&states.ResourceInstanceObjectSrc{
   401  			Status:       states.ObjectReady,
   402  			AttrsJSON:    []byte(`{"id":"foo","value":"foo"}`),
   403  			Dependencies: []addrs.ConfigResource{mustConfigResourceAddr("module.A.test_object.foo")},
   404  		},
   405  		mustProviderConfig(`provider["registry.opentofu.org/hashicorp/test"]`),
   406  	)
   407  
   408  	b := &ApplyGraphBuilder{
   409  		Config:  testModule(t, "graph-builder-apply-module-destroy"),
   410  		Changes: changes,
   411  		Plugins: simpleMockPluginLibrary(),
   412  		State:   state,
   413  	}
   414  
   415  	g, err := b.Build(addrs.RootModuleInstance)
   416  	if err != nil {
   417  		t.Fatalf("err: %s", err)
   418  	}
   419  
   420  	testGraphHappensBefore(
   421  		t, g,
   422  		"module.B.test_object.foo (destroy)",
   423  		"module.A.test_object.foo (destroy)",
   424  	)
   425  }
   426  
   427  func TestApplyGraphBuilder_targetModule(t *testing.T) {
   428  	changes := &plans.Changes{
   429  		Resources: []*plans.ResourceInstanceChangeSrc{
   430  			{
   431  				Addr: mustResourceInstanceAddr("test_object.foo"),
   432  				ChangeSrc: plans.ChangeSrc{
   433  					Action: plans.Update,
   434  				},
   435  			},
   436  			{
   437  				Addr: mustResourceInstanceAddr("module.child2.test_object.foo"),
   438  				ChangeSrc: plans.ChangeSrc{
   439  					Action: plans.Update,
   440  				},
   441  			},
   442  		},
   443  	}
   444  
   445  	b := &ApplyGraphBuilder{
   446  		Config:  testModule(t, "graph-builder-apply-target-module"),
   447  		Changes: changes,
   448  		Plugins: simpleMockPluginLibrary(),
   449  		Targets: []addrs.Targetable{
   450  			addrs.RootModuleInstance.Child("child2", addrs.NoKey),
   451  		},
   452  	}
   453  
   454  	g, err := b.Build(addrs.RootModuleInstance)
   455  	if err != nil {
   456  		t.Fatalf("err: %s", err)
   457  	}
   458  
   459  	testGraphNotContains(t, g, "module.child1.output.instance_id")
   460  }
   461  
   462  // Ensure that an update resulting from the removal of a resource happens after
   463  // that resource is destroyed.
   464  func TestApplyGraphBuilder_updateFromOrphan(t *testing.T) {
   465  	schemas := simpleTestSchemas()
   466  	instanceSchema := schemas.Providers[addrs.NewDefaultProvider("test")].ResourceTypes["test_object"]
   467  
   468  	bBefore, _ := plans.NewDynamicValue(
   469  		cty.ObjectVal(map[string]cty.Value{
   470  			"id":          cty.StringVal("b_id"),
   471  			"test_string": cty.StringVal("a_id"),
   472  		}), instanceSchema.Block.ImpliedType())
   473  	bAfter, _ := plans.NewDynamicValue(
   474  		cty.ObjectVal(map[string]cty.Value{
   475  			"id":          cty.StringVal("b_id"),
   476  			"test_string": cty.StringVal("changed"),
   477  		}), instanceSchema.Block.ImpliedType())
   478  
   479  	changes := &plans.Changes{
   480  		Resources: []*plans.ResourceInstanceChangeSrc{
   481  			{
   482  				Addr: mustResourceInstanceAddr("test_object.a"),
   483  				ChangeSrc: plans.ChangeSrc{
   484  					Action: plans.Delete,
   485  				},
   486  			},
   487  			{
   488  				Addr: mustResourceInstanceAddr("test_object.b"),
   489  				ChangeSrc: plans.ChangeSrc{
   490  					Action: plans.Update,
   491  					Before: bBefore,
   492  					After:  bAfter,
   493  				},
   494  			},
   495  		},
   496  	}
   497  
   498  	state := states.NewState()
   499  	root := state.EnsureModule(addrs.RootModuleInstance)
   500  	root.SetResourceInstanceCurrent(
   501  		addrs.Resource{
   502  			Mode: addrs.ManagedResourceMode,
   503  			Type: "test_object",
   504  			Name: "a",
   505  		}.Instance(addrs.NoKey),
   506  		&states.ResourceInstanceObjectSrc{
   507  			Status:    states.ObjectReady,
   508  			AttrsJSON: []byte(`{"id":"a_id"}`),
   509  		},
   510  		addrs.AbsProviderConfig{
   511  			Provider: addrs.NewDefaultProvider("test"),
   512  			Module:   addrs.RootModule,
   513  		},
   514  	)
   515  	root.SetResourceInstanceCurrent(
   516  		addrs.Resource{
   517  			Mode: addrs.ManagedResourceMode,
   518  			Type: "test_object",
   519  			Name: "b",
   520  		}.Instance(addrs.NoKey),
   521  		&states.ResourceInstanceObjectSrc{
   522  			Status:    states.ObjectReady,
   523  			AttrsJSON: []byte(`{"id":"b_id","test_string":"a_id"}`),
   524  			Dependencies: []addrs.ConfigResource{
   525  				{
   526  					Resource: addrs.Resource{
   527  						Mode: addrs.ManagedResourceMode,
   528  						Type: "test_object",
   529  						Name: "a",
   530  					},
   531  					Module: root.Addr.Module(),
   532  				},
   533  			},
   534  		},
   535  		addrs.AbsProviderConfig{
   536  			Provider: addrs.NewDefaultProvider("test"),
   537  			Module:   addrs.RootModule,
   538  		},
   539  	)
   540  
   541  	b := &ApplyGraphBuilder{
   542  		Config:  testModule(t, "graph-builder-apply-orphan-update"),
   543  		Changes: changes,
   544  		Plugins: simpleMockPluginLibrary(),
   545  		State:   state,
   546  	}
   547  
   548  	g, err := b.Build(addrs.RootModuleInstance)
   549  	if err != nil {
   550  		t.Fatalf("err: %s", err)
   551  	}
   552  
   553  	expected := strings.TrimSpace(`
   554  test_object.a (destroy)
   555  test_object.b
   556    test_object.a (destroy)
   557  `)
   558  
   559  	instanceGraph := filterInstances(g)
   560  	got := strings.TrimSpace(instanceGraph.String())
   561  
   562  	if got != expected {
   563  		t.Fatalf("expected:\n%s\ngot:\n%s", expected, got)
   564  	}
   565  }
   566  
   567  // Ensure that an update resulting from the removal of a resource happens before
   568  // a CBD resource is destroyed.
   569  func TestApplyGraphBuilder_updateFromCBDOrphan(t *testing.T) {
   570  	schemas := simpleTestSchemas()
   571  	instanceSchema := schemas.Providers[addrs.NewDefaultProvider("test")].ResourceTypes["test_object"]
   572  
   573  	bBefore, _ := plans.NewDynamicValue(
   574  		cty.ObjectVal(map[string]cty.Value{
   575  			"id":          cty.StringVal("b_id"),
   576  			"test_string": cty.StringVal("a_id"),
   577  		}), instanceSchema.Block.ImpliedType())
   578  	bAfter, _ := plans.NewDynamicValue(
   579  		cty.ObjectVal(map[string]cty.Value{
   580  			"id":          cty.StringVal("b_id"),
   581  			"test_string": cty.StringVal("changed"),
   582  		}), instanceSchema.Block.ImpliedType())
   583  
   584  	changes := &plans.Changes{
   585  		Resources: []*plans.ResourceInstanceChangeSrc{
   586  			{
   587  				Addr: mustResourceInstanceAddr("test_object.a"),
   588  				ChangeSrc: plans.ChangeSrc{
   589  					Action: plans.Delete,
   590  				},
   591  			},
   592  			{
   593  				Addr: mustResourceInstanceAddr("test_object.b"),
   594  				ChangeSrc: plans.ChangeSrc{
   595  					Action: plans.Update,
   596  					Before: bBefore,
   597  					After:  bAfter,
   598  				},
   599  			},
   600  		},
   601  	}
   602  
   603  	state := states.NewState()
   604  	root := state.EnsureModule(addrs.RootModuleInstance)
   605  	root.SetResourceInstanceCurrent(
   606  		addrs.Resource{
   607  			Mode: addrs.ManagedResourceMode,
   608  			Type: "test_object",
   609  			Name: "a",
   610  		}.Instance(addrs.NoKey),
   611  		&states.ResourceInstanceObjectSrc{
   612  			Status:              states.ObjectReady,
   613  			AttrsJSON:           []byte(`{"id":"a_id"}`),
   614  			CreateBeforeDestroy: true,
   615  		},
   616  		mustProviderConfig(`provider["registry.opentofu.org/hashicorp/test"]`),
   617  	)
   618  	root.SetResourceInstanceCurrent(
   619  		addrs.Resource{
   620  			Mode: addrs.ManagedResourceMode,
   621  			Type: "test_object",
   622  			Name: "b",
   623  		}.Instance(addrs.NoKey),
   624  		&states.ResourceInstanceObjectSrc{
   625  			Status:    states.ObjectReady,
   626  			AttrsJSON: []byte(`{"id":"b_id","test_string":"a_id"}`),
   627  			Dependencies: []addrs.ConfigResource{
   628  				{
   629  					Resource: addrs.Resource{
   630  						Mode: addrs.ManagedResourceMode,
   631  						Type: "test_object",
   632  						Name: "a",
   633  					},
   634  					Module: root.Addr.Module(),
   635  				},
   636  			},
   637  		},
   638  		mustProviderConfig(`provider["registry.opentofu.org/hashicorp/test"]`),
   639  	)
   640  
   641  	b := &ApplyGraphBuilder{
   642  		Config:  testModule(t, "graph-builder-apply-orphan-update"),
   643  		Changes: changes,
   644  		Plugins: simpleMockPluginLibrary(),
   645  		State:   state,
   646  	}
   647  
   648  	g, err := b.Build(addrs.RootModuleInstance)
   649  	if err != nil {
   650  		t.Fatalf("err: %s", err)
   651  	}
   652  
   653  	expected := strings.TrimSpace(`
   654  test_object.a (destroy)
   655    test_object.b
   656  test_object.b
   657  `)
   658  
   659  	instanceGraph := filterInstances(g)
   660  	got := strings.TrimSpace(instanceGraph.String())
   661  
   662  	if got != expected {
   663  		t.Fatalf("expected:\n%s\ngot:\n%s", expected, got)
   664  	}
   665  }
   666  
   667  // The orphan clean up node should not be connected to a provider
   668  func TestApplyGraphBuilder_orphanedWithProvider(t *testing.T) {
   669  	changes := &plans.Changes{
   670  		Resources: []*plans.ResourceInstanceChangeSrc{
   671  			{
   672  				Addr: mustResourceInstanceAddr("test_object.A"),
   673  				ChangeSrc: plans.ChangeSrc{
   674  					Action: plans.Delete,
   675  				},
   676  			},
   677  		},
   678  	}
   679  
   680  	state := states.NewState()
   681  	root := state.EnsureModule(addrs.RootModuleInstance)
   682  	root.SetResourceInstanceCurrent(
   683  		mustResourceInstanceAddr("test_object.A").Resource,
   684  		&states.ResourceInstanceObjectSrc{
   685  			Status:    states.ObjectReady,
   686  			AttrsJSON: []byte(`{"id":"A"}`),
   687  		},
   688  		mustProviderConfig(`provider["registry.opentofu.org/hashicorp/test"].foo`),
   689  	)
   690  
   691  	b := &ApplyGraphBuilder{
   692  		Config:  testModule(t, "graph-builder-orphan-alias"),
   693  		Changes: changes,
   694  		Plugins: simpleMockPluginLibrary(),
   695  		State:   state,
   696  	}
   697  
   698  	g, err := b.Build(addrs.RootModuleInstance)
   699  	if err != nil {
   700  		t.Fatal(err)
   701  	}
   702  
   703  	// The cleanup node has no state or config of its own, so would create a
   704  	// default provider which we don't want.
   705  	testGraphNotContains(t, g, "provider.test")
   706  }
   707  
   708  func TestApplyGraphBuilder_withChecks(t *testing.T) {
   709  	awsProvider := mockProviderWithResourceTypeSchema("aws_instance", simpleTestSchema())
   710  
   711  	changes := &plans.Changes{
   712  		Resources: []*plans.ResourceInstanceChangeSrc{
   713  			{
   714  				Addr: mustResourceInstanceAddr("aws_instance.foo"),
   715  				ChangeSrc: plans.ChangeSrc{
   716  					Action: plans.Create,
   717  				},
   718  			},
   719  			{
   720  				Addr: mustResourceInstanceAddr("aws_instance.baz"),
   721  				ChangeSrc: plans.ChangeSrc{
   722  					Action: plans.Create,
   723  				},
   724  			},
   725  			{
   726  				Addr: mustResourceInstanceAddr("data.aws_data_source.bar"),
   727  				ChangeSrc: plans.ChangeSrc{
   728  					Action: plans.Read,
   729  				},
   730  				ActionReason: plans.ResourceInstanceReadBecauseCheckNested,
   731  			},
   732  		},
   733  	}
   734  
   735  	plugins := newContextPluginsForTest(map[addrs.Provider]providers.Factory{
   736  		addrs.NewDefaultProvider("aws"): providers.FactoryFixed(awsProvider),
   737  	}, t)
   738  
   739  	b := &ApplyGraphBuilder{
   740  		Config:    testModule(t, "apply-with-checks"),
   741  		Changes:   changes,
   742  		Plugins:   plugins,
   743  		State:     states.NewState(),
   744  		Operation: walkApply,
   745  	}
   746  
   747  	g, err := b.Build(addrs.RootModuleInstance)
   748  	if err != nil {
   749  		t.Fatalf("err: %s", err)
   750  	}
   751  
   752  	if g.Path.String() != addrs.RootModuleInstance.String() {
   753  		t.Fatalf("wrong path %q", g.Path.String())
   754  	}
   755  
   756  	got := strings.TrimSpace(g.String())
   757  	// We're especially looking for the edge here, where aws_instance.bat
   758  	// has a dependency on aws_instance.boo
   759  	want := strings.TrimSpace(testPlanWithCheckGraphBuilderStr)
   760  	if diff := cmp.Diff(want, got); diff != "" {
   761  		t.Fatalf("\ngot:\n%s\n\nwant:\n%s\n\ndiff:\n%s", got, want, diff)
   762  	}
   763  
   764  }
   765  
   766  const testPlanWithCheckGraphBuilderStr = `
   767  (execute checks)
   768    aws_instance.baz
   769  aws_instance.baz
   770    aws_instance.baz (expand)
   771  aws_instance.baz (expand)
   772    aws_instance.foo
   773  aws_instance.foo
   774    aws_instance.foo (expand)
   775  aws_instance.foo (expand)
   776    provider["registry.opentofu.org/hashicorp/aws"]
   777  check.my_check (expand)
   778    data.aws_data_source.bar
   779  data.aws_data_source.bar
   780    (execute checks)
   781    data.aws_data_source.bar (expand)
   782  data.aws_data_source.bar (expand)
   783    provider["registry.opentofu.org/hashicorp/aws"]
   784  provider["registry.opentofu.org/hashicorp/aws"]
   785  provider["registry.opentofu.org/hashicorp/aws"] (close)
   786    data.aws_data_source.bar
   787  root
   788    check.my_check (expand)
   789    provider["registry.opentofu.org/hashicorp/aws"] (close)
   790  `
   791  
   792  const testApplyGraphBuilderStr = `
   793  module.child (close)
   794    module.child.test_object.other
   795  module.child (expand)
   796  module.child.test_object.create
   797    module.child.test_object.create (expand)
   798  module.child.test_object.create (expand)
   799    module.child (expand)
   800    provider["registry.opentofu.org/hashicorp/test"]
   801  module.child.test_object.other
   802    module.child.test_object.other (expand)
   803  module.child.test_object.other (expand)
   804    module.child.test_object.create
   805  provider["registry.opentofu.org/hashicorp/test"]
   806  provider["registry.opentofu.org/hashicorp/test"] (close)
   807    module.child.test_object.other
   808    test_object.other
   809  root
   810    module.child (close)
   811    provider["registry.opentofu.org/hashicorp/test"] (close)
   812  test_object.create
   813    test_object.create (expand)
   814  test_object.create (expand)
   815    provider["registry.opentofu.org/hashicorp/test"]
   816  test_object.other
   817    test_object.other (expand)
   818  test_object.other (expand)
   819    test_object.create
   820  `
   821  
   822  const testApplyGraphBuilderDestroyCountStr = `
   823  provider["registry.opentofu.org/hashicorp/test"]
   824  provider["registry.opentofu.org/hashicorp/test"] (close)
   825    test_object.B
   826  root
   827    provider["registry.opentofu.org/hashicorp/test"] (close)
   828  test_object.A (expand)
   829    provider["registry.opentofu.org/hashicorp/test"]
   830  test_object.A[1] (destroy)
   831    provider["registry.opentofu.org/hashicorp/test"]
   832  test_object.B
   833    test_object.A[1] (destroy)
   834    test_object.B (expand)
   835  test_object.B (expand)
   836    test_object.A (expand)
   837  `