github.com/kanishk98/terraform@v1.3.0-dev.0.20220917174235-661ca8088a6a/internal/terraform/transform_destroy_edge_test.go (about)

     1  package terraform
     2  
     3  import (
     4  	"fmt"
     5  	"strings"
     6  	"testing"
     7  
     8  	"github.com/davecgh/go-spew/spew"
     9  	"github.com/zclconf/go-cty/cty"
    10  
    11  	"github.com/hashicorp/terraform/internal/addrs"
    12  	"github.com/hashicorp/terraform/internal/dag"
    13  	"github.com/hashicorp/terraform/internal/plans"
    14  	"github.com/hashicorp/terraform/internal/states"
    15  )
    16  
    17  func TestDestroyEdgeTransformer_basic(t *testing.T) {
    18  	g := Graph{Path: addrs.RootModuleInstance}
    19  	g.Add(testDestroyNode("test_object.A"))
    20  	g.Add(testDestroyNode("test_object.B"))
    21  
    22  	state := states.NewState()
    23  	root := state.EnsureModule(addrs.RootModuleInstance)
    24  	root.SetResourceInstanceCurrent(
    25  		mustResourceInstanceAddr("test_object.A").Resource,
    26  		&states.ResourceInstanceObjectSrc{
    27  			Status:    states.ObjectReady,
    28  			AttrsJSON: []byte(`{"id":"A"}`),
    29  		},
    30  		mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`),
    31  	)
    32  	root.SetResourceInstanceCurrent(
    33  		mustResourceInstanceAddr("test_object.B").Resource,
    34  		&states.ResourceInstanceObjectSrc{
    35  			Status:       states.ObjectReady,
    36  			AttrsJSON:    []byte(`{"id":"B","test_string":"x"}`),
    37  			Dependencies: []addrs.ConfigResource{mustConfigResourceAddr("test_object.A")},
    38  		},
    39  		mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`),
    40  	)
    41  	if err := (&AttachStateTransformer{State: state}).Transform(&g); err != nil {
    42  		t.Fatal(err)
    43  	}
    44  
    45  	tf := &DestroyEdgeTransformer{}
    46  	if err := tf.Transform(&g); err != nil {
    47  		t.Fatalf("err: %s", err)
    48  	}
    49  
    50  	actual := strings.TrimSpace(g.String())
    51  	expected := strings.TrimSpace(testTransformDestroyEdgeBasicStr)
    52  	if actual != expected {
    53  		t.Fatalf("wrong result\n\ngot:\n%s\n\nwant:\n%s", actual, expected)
    54  	}
    55  }
    56  
    57  func TestDestroyEdgeTransformer_multi(t *testing.T) {
    58  	g := Graph{Path: addrs.RootModuleInstance}
    59  	g.Add(testDestroyNode("test_object.A"))
    60  	g.Add(testDestroyNode("test_object.B"))
    61  	g.Add(testDestroyNode("test_object.C"))
    62  
    63  	state := states.NewState()
    64  	root := state.EnsureModule(addrs.RootModuleInstance)
    65  	root.SetResourceInstanceCurrent(
    66  		mustResourceInstanceAddr("test_object.A").Resource,
    67  		&states.ResourceInstanceObjectSrc{
    68  			Status:    states.ObjectReady,
    69  			AttrsJSON: []byte(`{"id":"A"}`),
    70  		},
    71  		mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`),
    72  	)
    73  	root.SetResourceInstanceCurrent(
    74  		mustResourceInstanceAddr("test_object.B").Resource,
    75  		&states.ResourceInstanceObjectSrc{
    76  			Status:       states.ObjectReady,
    77  			AttrsJSON:    []byte(`{"id":"B","test_string":"x"}`),
    78  			Dependencies: []addrs.ConfigResource{mustConfigResourceAddr("test_object.A")},
    79  		},
    80  		mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`),
    81  	)
    82  	root.SetResourceInstanceCurrent(
    83  		mustResourceInstanceAddr("test_object.C").Resource,
    84  		&states.ResourceInstanceObjectSrc{
    85  			Status:    states.ObjectReady,
    86  			AttrsJSON: []byte(`{"id":"C","test_string":"x"}`),
    87  			Dependencies: []addrs.ConfigResource{
    88  				mustConfigResourceAddr("test_object.A"),
    89  				mustConfigResourceAddr("test_object.B"),
    90  			},
    91  		},
    92  		mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`),
    93  	)
    94  
    95  	if err := (&AttachStateTransformer{State: state}).Transform(&g); err != nil {
    96  		t.Fatal(err)
    97  	}
    98  
    99  	tf := &DestroyEdgeTransformer{}
   100  	if err := tf.Transform(&g); err != nil {
   101  		t.Fatalf("err: %s", err)
   102  	}
   103  
   104  	actual := strings.TrimSpace(g.String())
   105  	expected := strings.TrimSpace(testTransformDestroyEdgeMultiStr)
   106  	if actual != expected {
   107  		t.Fatalf("wrong result\n\ngot:\n%s\n\nwant:\n%s", actual, expected)
   108  	}
   109  }
   110  
   111  func TestDestroyEdgeTransformer_selfRef(t *testing.T) {
   112  	g := Graph{Path: addrs.RootModuleInstance}
   113  	g.Add(testDestroyNode("test_object.A"))
   114  	tf := &DestroyEdgeTransformer{}
   115  	if err := tf.Transform(&g); err != nil {
   116  		t.Fatalf("err: %s", err)
   117  	}
   118  
   119  	actual := strings.TrimSpace(g.String())
   120  	expected := strings.TrimSpace(testTransformDestroyEdgeSelfRefStr)
   121  	if actual != expected {
   122  		t.Fatalf("wrong result\n\ngot:\n%s\n\nwant:\n%s", actual, expected)
   123  	}
   124  }
   125  
   126  func TestDestroyEdgeTransformer_module(t *testing.T) {
   127  	g := Graph{Path: addrs.RootModuleInstance}
   128  	g.Add(testDestroyNode("module.child.test_object.b"))
   129  	g.Add(testDestroyNode("test_object.a"))
   130  	state := states.NewState()
   131  	root := state.EnsureModule(addrs.RootModuleInstance)
   132  	child := state.EnsureModule(addrs.RootModuleInstance.Child("child", addrs.NoKey))
   133  	root.SetResourceInstanceCurrent(
   134  		mustResourceInstanceAddr("test_object.a").Resource,
   135  		&states.ResourceInstanceObjectSrc{
   136  			Status:       states.ObjectReady,
   137  			AttrsJSON:    []byte(`{"id":"a"}`),
   138  			Dependencies: []addrs.ConfigResource{mustConfigResourceAddr("module.child.test_object.b")},
   139  		},
   140  		mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`),
   141  	)
   142  	child.SetResourceInstanceCurrent(
   143  		mustResourceInstanceAddr("test_object.b").Resource,
   144  		&states.ResourceInstanceObjectSrc{
   145  			Status:    states.ObjectReady,
   146  			AttrsJSON: []byte(`{"id":"b","test_string":"x"}`),
   147  		},
   148  		mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`),
   149  	)
   150  
   151  	if err := (&AttachStateTransformer{State: state}).Transform(&g); err != nil {
   152  		t.Fatal(err)
   153  	}
   154  
   155  	tf := &DestroyEdgeTransformer{}
   156  	if err := tf.Transform(&g); err != nil {
   157  		t.Fatalf("err: %s", err)
   158  	}
   159  
   160  	actual := strings.TrimSpace(g.String())
   161  	expected := strings.TrimSpace(testTransformDestroyEdgeModuleStr)
   162  	if actual != expected {
   163  		t.Fatalf("wrong result\n\ngot:\n%s\n\nwant:\n%s", actual, expected)
   164  	}
   165  }
   166  
   167  func TestDestroyEdgeTransformer_moduleOnly(t *testing.T) {
   168  	g := Graph{Path: addrs.RootModuleInstance}
   169  
   170  	state := states.NewState()
   171  	for moduleIdx := 0; moduleIdx < 2; moduleIdx++ {
   172  		g.Add(testDestroyNode(fmt.Sprintf("module.child[%d].test_object.a", moduleIdx)))
   173  		g.Add(testDestroyNode(fmt.Sprintf("module.child[%d].test_object.b", moduleIdx)))
   174  		g.Add(testDestroyNode(fmt.Sprintf("module.child[%d].test_object.c", moduleIdx)))
   175  
   176  		child := state.EnsureModule(addrs.RootModuleInstance.Child("child", addrs.IntKey(moduleIdx)))
   177  		child.SetResourceInstanceCurrent(
   178  			mustResourceInstanceAddr("test_object.a").Resource,
   179  			&states.ResourceInstanceObjectSrc{
   180  				Status:    states.ObjectReady,
   181  				AttrsJSON: []byte(`{"id":"a"}`),
   182  			},
   183  			mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`),
   184  		)
   185  		child.SetResourceInstanceCurrent(
   186  			mustResourceInstanceAddr("test_object.b").Resource,
   187  			&states.ResourceInstanceObjectSrc{
   188  				Status:    states.ObjectReady,
   189  				AttrsJSON: []byte(`{"id":"b","test_string":"x"}`),
   190  				Dependencies: []addrs.ConfigResource{
   191  					mustConfigResourceAddr("module.child.test_object.a"),
   192  				},
   193  			},
   194  			mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`),
   195  		)
   196  		child.SetResourceInstanceCurrent(
   197  			mustResourceInstanceAddr("test_object.c").Resource,
   198  			&states.ResourceInstanceObjectSrc{
   199  				Status:    states.ObjectReady,
   200  				AttrsJSON: []byte(`{"id":"c","test_string":"x"}`),
   201  				Dependencies: []addrs.ConfigResource{
   202  					mustConfigResourceAddr("module.child.test_object.a"),
   203  					mustConfigResourceAddr("module.child.test_object.b"),
   204  				},
   205  			},
   206  			mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`),
   207  		)
   208  	}
   209  
   210  	if err := (&AttachStateTransformer{State: state}).Transform(&g); err != nil {
   211  		t.Fatal(err)
   212  	}
   213  
   214  	tf := &DestroyEdgeTransformer{}
   215  	if err := tf.Transform(&g); err != nil {
   216  		t.Fatalf("err: %s", err)
   217  	}
   218  
   219  	// The analyses done in the destroy edge transformer are between
   220  	// not-yet-expanded objects, which is conservative and so it will generate
   221  	// edges that aren't strictly necessary. As a special case we filter out
   222  	// any edges that are between resources instances that are in different
   223  	// instances of the same module, because those edges are never needed
   224  	// (one instance of a module cannot depend on another instance of the
   225  	// same module) and including them can, in complex cases, cause cycles due
   226  	// to unnecessary interactions between destroyed and created module
   227  	// instances in the same plan.
   228  	//
   229  	// Therefore below we expect to see the dependencies within each instance
   230  	// of module.child reflected, but we should not see any dependencies
   231  	// _between_ instances of module.child.
   232  
   233  	actual := strings.TrimSpace(g.String())
   234  	expected := strings.TrimSpace(`
   235  module.child[0].test_object.a (destroy)
   236    module.child[0].test_object.b (destroy)
   237    module.child[0].test_object.c (destroy)
   238  module.child[0].test_object.b (destroy)
   239    module.child[0].test_object.c (destroy)
   240  module.child[0].test_object.c (destroy)
   241  module.child[1].test_object.a (destroy)
   242    module.child[1].test_object.b (destroy)
   243    module.child[1].test_object.c (destroy)
   244  module.child[1].test_object.b (destroy)
   245    module.child[1].test_object.c (destroy)
   246  module.child[1].test_object.c (destroy)
   247  `)
   248  	if actual != expected {
   249  		t.Fatalf("wrong result\n\ngot:\n%s\n\nwant:\n%s", actual, expected)
   250  	}
   251  }
   252  
   253  func TestDestroyEdgeTransformer_destroyThenUpdate(t *testing.T) {
   254  	g := Graph{Path: addrs.RootModuleInstance}
   255  	g.Add(testUpdateNode("test_object.A"))
   256  	g.Add(testDestroyNode("test_object.B"))
   257  
   258  	state := states.NewState()
   259  	root := state.EnsureModule(addrs.RootModuleInstance)
   260  	root.SetResourceInstanceCurrent(
   261  		mustResourceInstanceAddr("test_object.A").Resource,
   262  		&states.ResourceInstanceObjectSrc{
   263  			Status:    states.ObjectReady,
   264  			AttrsJSON: []byte(`{"id":"A","test_string":"old"}`),
   265  		},
   266  		mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`),
   267  	)
   268  	root.SetResourceInstanceCurrent(
   269  		mustResourceInstanceAddr("test_object.B").Resource,
   270  		&states.ResourceInstanceObjectSrc{
   271  			Status:       states.ObjectReady,
   272  			AttrsJSON:    []byte(`{"id":"B","test_string":"x"}`),
   273  			Dependencies: []addrs.ConfigResource{mustConfigResourceAddr("test_object.A")},
   274  		},
   275  		mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`),
   276  	)
   277  
   278  	if err := (&AttachStateTransformer{State: state}).Transform(&g); err != nil {
   279  		t.Fatal(err)
   280  	}
   281  
   282  	tf := &DestroyEdgeTransformer{}
   283  	if err := tf.Transform(&g); err != nil {
   284  		t.Fatalf("err: %s", err)
   285  	}
   286  
   287  	expected := strings.TrimSpace(`
   288  test_object.A
   289    test_object.B (destroy)
   290  test_object.B (destroy)
   291  `)
   292  	actual := strings.TrimSpace(g.String())
   293  
   294  	if actual != expected {
   295  		t.Fatalf("wrong result\n\ngot:\n%s\n\nwant:\n%s", actual, expected)
   296  	}
   297  }
   298  
   299  func TestPruneUnusedNodesTransformer_rootModuleOutputValues(t *testing.T) {
   300  	// This is a kinda-weird test case covering the very narrow situation
   301  	// where a root module output value depends on a resource, where we
   302  	// need to make sure that the output value doesn't block pruning of
   303  	// the resource from the graph. This special case exists because although
   304  	// root module objects are "expanders", they in practice always expand
   305  	// to exactly one instance and so don't have the usual requirement of
   306  	// needing to stick around in order to support downstream expanders
   307  	// when there are e.g. nested expanding modules.
   308  
   309  	// In order to keep this test focused on the pruneUnusedNodesTransformer
   310  	// as much as possible we're using a minimal graph construction here which
   311  	// is just enough to get the nodes we need, but this does mean that this
   312  	// test might be invalidated by future changes to the apply graph builder,
   313  	// and so if something seems off here it might help to compare the
   314  	// following with the real apply graph transformer and verify whether
   315  	// this smaller construction is still realistic enough to be a valid test.
   316  	// It might be valid to change or remove this test to "make it work", as
   317  	// long as you verify that there is still _something_ upholding the
   318  	// invariant that a root module output value should not block a resource
   319  	// node from being pruned from the graph.
   320  
   321  	concreteResource := func(a *NodeAbstractResource) dag.Vertex {
   322  		return &nodeExpandApplyableResource{
   323  			NodeAbstractResource: a,
   324  		}
   325  	}
   326  
   327  	concreteResourceInstance := func(a *NodeAbstractResourceInstance) dag.Vertex {
   328  		return &NodeApplyableResourceInstance{
   329  			NodeAbstractResourceInstance: a,
   330  		}
   331  	}
   332  
   333  	resourceInstAddr := mustResourceInstanceAddr("test.a")
   334  	providerCfgAddr := addrs.AbsProviderConfig{
   335  		Module:   addrs.RootModule,
   336  		Provider: addrs.MustParseProviderSourceString("foo/test"),
   337  	}
   338  	emptyObjDynamicVal, err := plans.NewDynamicValue(cty.EmptyObjectVal, cty.EmptyObject)
   339  	if err != nil {
   340  		t.Fatal(err)
   341  	}
   342  	nullObjDynamicVal, err := plans.NewDynamicValue(cty.NullVal(cty.EmptyObject), cty.EmptyObject)
   343  	if err != nil {
   344  		t.Fatal(err)
   345  	}
   346  
   347  	config := testModuleInline(t, map[string]string{
   348  		"main.tf": `
   349  			resource "test" "a" {
   350  			}
   351  
   352  			output "test" {
   353  				value = test.a.foo
   354  			}
   355  		`,
   356  	})
   357  	state := states.BuildState(func(s *states.SyncState) {
   358  		s.SetResourceInstanceCurrent(
   359  			resourceInstAddr,
   360  			&states.ResourceInstanceObjectSrc{
   361  				Status:    states.ObjectReady,
   362  				AttrsJSON: []byte(`{}`),
   363  			},
   364  			providerCfgAddr,
   365  		)
   366  	})
   367  	changes := plans.NewChanges()
   368  	changes.SyncWrapper().AppendResourceInstanceChange(&plans.ResourceInstanceChangeSrc{
   369  		Addr:         resourceInstAddr,
   370  		PrevRunAddr:  resourceInstAddr,
   371  		ProviderAddr: providerCfgAddr,
   372  		ChangeSrc: plans.ChangeSrc{
   373  			Action: plans.Delete,
   374  			Before: emptyObjDynamicVal,
   375  			After:  nullObjDynamicVal,
   376  		},
   377  	})
   378  
   379  	builder := &BasicGraphBuilder{
   380  		Steps: []GraphTransformer{
   381  			&ConfigTransformer{
   382  				Concrete: concreteResource,
   383  				Config:   config,
   384  			},
   385  			&OutputTransformer{
   386  				Config:  config,
   387  				Changes: changes,
   388  			},
   389  			&DiffTransformer{
   390  				Concrete: concreteResourceInstance,
   391  				State:    state,
   392  				Changes:  changes,
   393  			},
   394  			&ReferenceTransformer{},
   395  			&AttachDependenciesTransformer{},
   396  			&pruneUnusedNodesTransformer{},
   397  			&CloseRootModuleTransformer{},
   398  		},
   399  	}
   400  	graph, diags := builder.Build(addrs.RootModuleInstance)
   401  	assertNoDiagnostics(t, diags)
   402  
   403  	// At this point, thanks to pruneUnusedNodesTransformer, we should still
   404  	// have the node for the output value, but the "test.a (expand)" node
   405  	// should've been pruned in recognition of the fact that we're performing
   406  	// a destroy and therefore we only need the "test.a (destroy)" node.
   407  
   408  	nodesByName := make(map[string]dag.Vertex)
   409  	nodesByResourceExpand := make(map[string]dag.Vertex)
   410  	for _, n := range graph.Vertices() {
   411  		name := dag.VertexName(n)
   412  		if _, exists := nodesByName[name]; exists {
   413  			t.Fatalf("multiple nodes have name %q", name)
   414  		}
   415  		nodesByName[name] = n
   416  
   417  		if exp, ok := n.(*nodeExpandApplyableResource); ok {
   418  			addr := exp.Addr
   419  			if _, exists := nodesByResourceExpand[addr.String()]; exists {
   420  				t.Fatalf("multiple nodes are expanders for %s", addr)
   421  			}
   422  			nodesByResourceExpand[addr.String()] = exp
   423  		}
   424  	}
   425  
   426  	// NOTE: The following is sensitive to the current name string formats we
   427  	// use for these particular node types. These names are not contractual
   428  	// so if this breaks in future it is fine to update these names to the new
   429  	// names as long as you verify first that the new names correspond to
   430  	// the same meaning as what we're assuming below.
   431  	if _, exists := nodesByName["test.a (destroy)"]; !exists {
   432  		t.Errorf("missing destroy node for resource instance test.a")
   433  	}
   434  	if _, exists := nodesByName["output.test (expand)"]; !exists {
   435  		t.Errorf("missing expand for output value 'test'")
   436  	}
   437  
   438  	// We _must not_ have any node that expands a resource.
   439  	if len(nodesByResourceExpand) != 0 {
   440  		t.Errorf("resource expand nodes remain the graph after transform; should've been pruned\n%s", spew.Sdump(nodesByResourceExpand))
   441  	}
   442  }
   443  
   444  func testDestroyNode(addrString string) GraphNodeDestroyer {
   445  	instAddr := mustResourceInstanceAddr(addrString)
   446  	inst := NewNodeAbstractResourceInstance(instAddr)
   447  	return &NodeDestroyResourceInstance{NodeAbstractResourceInstance: inst}
   448  }
   449  
   450  func testUpdateNode(addrString string) GraphNodeCreator {
   451  	instAddr := mustResourceInstanceAddr(addrString)
   452  	inst := NewNodeAbstractResourceInstance(instAddr)
   453  	return &NodeApplyableResourceInstance{NodeAbstractResourceInstance: inst}
   454  }
   455  
   456  const testTransformDestroyEdgeBasicStr = `
   457  test_object.A (destroy)
   458    test_object.B (destroy)
   459  test_object.B (destroy)
   460  `
   461  
   462  const testTransformDestroyEdgeMultiStr = `
   463  test_object.A (destroy)
   464    test_object.B (destroy)
   465    test_object.C (destroy)
   466  test_object.B (destroy)
   467    test_object.C (destroy)
   468  test_object.C (destroy)
   469  `
   470  
   471  const testTransformDestroyEdgeSelfRefStr = `
   472  test_object.A (destroy)
   473  `
   474  
   475  const testTransformDestroyEdgeModuleStr = `
   476  module.child.test_object.b (destroy)
   477    test_object.a (destroy)
   478  test_object.a (destroy)
   479  `