github.com/opentofu/opentofu@v1.7.1/internal/tofu/transform_destroy_cbd_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  	"regexp"
    10  	"strings"
    11  	"testing"
    12  
    13  	"github.com/opentofu/opentofu/internal/addrs"
    14  	"github.com/opentofu/opentofu/internal/dag"
    15  	"github.com/opentofu/opentofu/internal/plans"
    16  	"github.com/opentofu/opentofu/internal/states"
    17  )
    18  
    19  func cbdTestGraph(t *testing.T, mod string, changes *plans.Changes, state *states.State) *Graph {
    20  	module := testModule(t, mod)
    21  
    22  	applyBuilder := &ApplyGraphBuilder{
    23  		Config:  module,
    24  		Changes: changes,
    25  		Plugins: simpleMockPluginLibrary(),
    26  		State:   state,
    27  	}
    28  	g, err := (&BasicGraphBuilder{
    29  		Steps: cbdTestSteps(applyBuilder.Steps()),
    30  		Name:  "ApplyGraphBuilder",
    31  	}).Build(addrs.RootModuleInstance)
    32  	if err != nil {
    33  		t.Fatalf("err: %s", err)
    34  	}
    35  
    36  	return filterInstances(g)
    37  }
    38  
    39  // override the apply graph builder to halt the process after CBD
    40  func cbdTestSteps(steps []GraphTransformer) []GraphTransformer {
    41  	found := false
    42  	var i int
    43  	var t GraphTransformer
    44  	for i, t = range steps {
    45  		if _, ok := t.(*CBDEdgeTransformer); ok {
    46  			found = true
    47  			break
    48  		}
    49  	}
    50  
    51  	if !found {
    52  		panic("CBDEdgeTransformer not found")
    53  	}
    54  
    55  	// re-add the root node so we have a valid graph for a walk, then reduce
    56  	// the graph for less output
    57  	steps = append(steps[:i+1], &CloseRootModuleTransformer{})
    58  	steps = append(steps, &TransitiveReductionTransformer{})
    59  
    60  	return steps
    61  }
    62  
    63  // remove extra nodes for easier test comparisons
    64  func filterInstances(g *Graph) *Graph {
    65  	for _, v := range g.Vertices() {
    66  		if _, ok := v.(GraphNodeResourceInstance); !ok {
    67  			// connect around the node to remove it without breaking deps
    68  			for _, down := range g.DownEdges(v) {
    69  				for _, up := range g.UpEdges(v) {
    70  					g.Connect(dag.BasicEdge(up, down))
    71  				}
    72  			}
    73  
    74  			g.Remove(v)
    75  		}
    76  
    77  	}
    78  	return g
    79  }
    80  
    81  func TestCBDEdgeTransformer(t *testing.T) {
    82  	changes := &plans.Changes{
    83  		Resources: []*plans.ResourceInstanceChangeSrc{
    84  			{
    85  				Addr: mustResourceInstanceAddr("test_object.A"),
    86  				ChangeSrc: plans.ChangeSrc{
    87  					Action: plans.CreateThenDelete,
    88  				},
    89  			},
    90  			{
    91  				Addr: mustResourceInstanceAddr("test_object.B"),
    92  				ChangeSrc: plans.ChangeSrc{
    93  					Action: plans.Update,
    94  				},
    95  			},
    96  		},
    97  	}
    98  
    99  	state := states.NewState()
   100  	root := state.EnsureModule(addrs.RootModuleInstance)
   101  	root.SetResourceInstanceCurrent(
   102  		mustResourceInstanceAddr("test_object.A").Resource,
   103  		&states.ResourceInstanceObjectSrc{
   104  			Status:    states.ObjectReady,
   105  			AttrsJSON: []byte(`{"id":"A"}`),
   106  		},
   107  		mustProviderConfig(`provider["registry.opentofu.org/hashicorp/test"]`),
   108  	)
   109  	root.SetResourceInstanceCurrent(
   110  		mustResourceInstanceAddr("test_object.B").Resource,
   111  		&states.ResourceInstanceObjectSrc{
   112  			Status:       states.ObjectReady,
   113  			AttrsJSON:    []byte(`{"id":"B","test_list":["x"]}`),
   114  			Dependencies: []addrs.ConfigResource{mustConfigResourceAddr("test_object.A")},
   115  		},
   116  		mustProviderConfig(`provider["registry.opentofu.org/hashicorp/test"]`),
   117  	)
   118  
   119  	g := cbdTestGraph(t, "transform-destroy-cbd-edge-basic", changes, state)
   120  	g = filterInstances(g)
   121  
   122  	actual := strings.TrimSpace(g.String())
   123  	expected := regexp.MustCompile(strings.TrimSpace(`
   124  (?m)test_object.A
   125  test_object.A \(destroy deposed \w+\)
   126    test_object.B
   127  test_object.B
   128    test_object.A
   129  `))
   130  
   131  	if !expected.MatchString(actual) {
   132  		t.Fatalf("wrong result\n\ngot:\n%s\n\nwant:\n%s", actual, expected)
   133  	}
   134  }
   135  
   136  func TestCBDEdgeTransformerMulti(t *testing.T) {
   137  	changes := &plans.Changes{
   138  		Resources: []*plans.ResourceInstanceChangeSrc{
   139  			{
   140  				Addr: mustResourceInstanceAddr("test_object.A"),
   141  				ChangeSrc: plans.ChangeSrc{
   142  					Action: plans.CreateThenDelete,
   143  				},
   144  			},
   145  			{
   146  				Addr: mustResourceInstanceAddr("test_object.B"),
   147  				ChangeSrc: plans.ChangeSrc{
   148  					Action: plans.CreateThenDelete,
   149  				},
   150  			},
   151  			{
   152  				Addr: mustResourceInstanceAddr("test_object.C"),
   153  				ChangeSrc: plans.ChangeSrc{
   154  					Action: plans.Update,
   155  				},
   156  			},
   157  		},
   158  	}
   159  
   160  	state := states.NewState()
   161  	root := state.EnsureModule(addrs.RootModuleInstance)
   162  	root.SetResourceInstanceCurrent(
   163  		mustResourceInstanceAddr("test_object.A").Resource,
   164  		&states.ResourceInstanceObjectSrc{
   165  			Status:    states.ObjectReady,
   166  			AttrsJSON: []byte(`{"id":"A"}`),
   167  		},
   168  		mustProviderConfig(`provider["registry.opentofu.org/hashicorp/test"]`),
   169  	)
   170  	root.SetResourceInstanceCurrent(
   171  		mustResourceInstanceAddr("test_object.B").Resource,
   172  		&states.ResourceInstanceObjectSrc{
   173  			Status:    states.ObjectReady,
   174  			AttrsJSON: []byte(`{"id":"B"}`),
   175  		},
   176  		mustProviderConfig(`provider["registry.opentofu.org/hashicorp/test"]`),
   177  	)
   178  	root.SetResourceInstanceCurrent(
   179  		mustResourceInstanceAddr("test_object.C").Resource,
   180  		&states.ResourceInstanceObjectSrc{
   181  			Status:    states.ObjectReady,
   182  			AttrsJSON: []byte(`{"id":"C","test_list":["x"]}`),
   183  			Dependencies: []addrs.ConfigResource{
   184  				mustConfigResourceAddr("test_object.A"),
   185  				mustConfigResourceAddr("test_object.B"),
   186  			},
   187  		},
   188  		mustProviderConfig(`provider["registry.opentofu.org/hashicorp/test"]`),
   189  	)
   190  
   191  	g := cbdTestGraph(t, "transform-destroy-cbd-edge-multi", changes, state)
   192  	g = filterInstances(g)
   193  
   194  	actual := strings.TrimSpace(g.String())
   195  	expected := regexp.MustCompile(strings.TrimSpace(`
   196  (?m)test_object.A
   197  test_object.A \(destroy deposed \w+\)
   198    test_object.C
   199  test_object.B
   200  test_object.B \(destroy deposed \w+\)
   201    test_object.C
   202  test_object.C
   203    test_object.A
   204    test_object.B
   205  `))
   206  
   207  	if !expected.MatchString(actual) {
   208  		t.Fatalf("wrong result\n\ngot:\n%s\n\nwant:\n%s", actual, expected)
   209  	}
   210  }
   211  
   212  func TestCBDEdgeTransformer_depNonCBDCount(t *testing.T) {
   213  	changes := &plans.Changes{
   214  		Resources: []*plans.ResourceInstanceChangeSrc{
   215  			{
   216  				Addr: mustResourceInstanceAddr("test_object.A"),
   217  				ChangeSrc: plans.ChangeSrc{
   218  					Action: plans.CreateThenDelete,
   219  				},
   220  			},
   221  			{
   222  				Addr: mustResourceInstanceAddr("test_object.B[0]"),
   223  				ChangeSrc: plans.ChangeSrc{
   224  					Action: plans.Update,
   225  				},
   226  			},
   227  			{
   228  				Addr: mustResourceInstanceAddr("test_object.B[1]"),
   229  				ChangeSrc: plans.ChangeSrc{
   230  					Action: plans.Update,
   231  				},
   232  			},
   233  		},
   234  	}
   235  
   236  	state := states.NewState()
   237  	root := state.EnsureModule(addrs.RootModuleInstance)
   238  	root.SetResourceInstanceCurrent(
   239  		mustResourceInstanceAddr("test_object.A").Resource,
   240  		&states.ResourceInstanceObjectSrc{
   241  			Status:    states.ObjectReady,
   242  			AttrsJSON: []byte(`{"id":"A"}`),
   243  		},
   244  		mustProviderConfig(`provider["registry.opentofu.org/hashicorp/test"]`),
   245  	)
   246  	root.SetResourceInstanceCurrent(
   247  		mustResourceInstanceAddr("test_object.B[0]").Resource,
   248  		&states.ResourceInstanceObjectSrc{
   249  			Status:       states.ObjectReady,
   250  			AttrsJSON:    []byte(`{"id":"B","test_list":["x"]}`),
   251  			Dependencies: []addrs.ConfigResource{mustConfigResourceAddr("test_object.A")},
   252  		},
   253  		mustProviderConfig(`provider["registry.opentofu.org/hashicorp/test"]`),
   254  	)
   255  	root.SetResourceInstanceCurrent(
   256  		mustResourceInstanceAddr("test_object.B[1]").Resource,
   257  		&states.ResourceInstanceObjectSrc{
   258  			Status:       states.ObjectReady,
   259  			AttrsJSON:    []byte(`{"id":"B","test_list":["x"]}`),
   260  			Dependencies: []addrs.ConfigResource{mustConfigResourceAddr("test_object.A")},
   261  		},
   262  		mustProviderConfig(`provider["registry.opentofu.org/hashicorp/test"]`),
   263  	)
   264  
   265  	g := cbdTestGraph(t, "transform-cbd-destroy-edge-count", changes, state)
   266  
   267  	actual := strings.TrimSpace(g.String())
   268  	expected := regexp.MustCompile(strings.TrimSpace(`
   269  (?m)test_object.A
   270  test_object.A \(destroy deposed \w+\)
   271    test_object.B\[0\]
   272    test_object.B\[1\]
   273  test_object.B\[0\]
   274    test_object.A
   275  test_object.B\[1\]
   276    test_object.A`))
   277  
   278  	if !expected.MatchString(actual) {
   279  		t.Fatalf("wrong result\n\ngot:\n%s\n\nwant:\n%s", actual, expected)
   280  	}
   281  }
   282  
   283  func TestCBDEdgeTransformer_depNonCBDCountBoth(t *testing.T) {
   284  	changes := &plans.Changes{
   285  		Resources: []*plans.ResourceInstanceChangeSrc{
   286  			{
   287  				Addr: mustResourceInstanceAddr("test_object.A[0]"),
   288  				ChangeSrc: plans.ChangeSrc{
   289  					Action: plans.CreateThenDelete,
   290  				},
   291  			},
   292  			{
   293  				Addr: mustResourceInstanceAddr("test_object.A[1]"),
   294  				ChangeSrc: plans.ChangeSrc{
   295  					Action: plans.CreateThenDelete,
   296  				},
   297  			},
   298  			{
   299  				Addr: mustResourceInstanceAddr("test_object.B[0]"),
   300  				ChangeSrc: plans.ChangeSrc{
   301  					Action: plans.Update,
   302  				},
   303  			},
   304  			{
   305  				Addr: mustResourceInstanceAddr("test_object.B[1]"),
   306  				ChangeSrc: plans.ChangeSrc{
   307  					Action: plans.Update,
   308  				},
   309  			},
   310  		},
   311  	}
   312  
   313  	state := states.NewState()
   314  	root := state.EnsureModule(addrs.RootModuleInstance)
   315  	root.SetResourceInstanceCurrent(
   316  		mustResourceInstanceAddr("test_object.A[0]").Resource,
   317  		&states.ResourceInstanceObjectSrc{
   318  			Status:    states.ObjectReady,
   319  			AttrsJSON: []byte(`{"id":"A"}`),
   320  		},
   321  		mustProviderConfig(`provider["registry.opentofu.org/hashicorp/test"]`),
   322  	)
   323  	root.SetResourceInstanceCurrent(
   324  		mustResourceInstanceAddr("test_object.A[1]").Resource,
   325  		&states.ResourceInstanceObjectSrc{
   326  			Status:    states.ObjectReady,
   327  			AttrsJSON: []byte(`{"id":"A"}`),
   328  		},
   329  		mustProviderConfig(`provider["registry.opentofu.org/hashicorp/test"]`),
   330  	)
   331  	root.SetResourceInstanceCurrent(
   332  		mustResourceInstanceAddr("test_object.B[0]").Resource,
   333  		&states.ResourceInstanceObjectSrc{
   334  			Status:       states.ObjectReady,
   335  			AttrsJSON:    []byte(`{"id":"B","test_list":["x"]}`),
   336  			Dependencies: []addrs.ConfigResource{mustConfigResourceAddr("test_object.A")},
   337  		},
   338  		mustProviderConfig(`provider["registry.opentofu.org/hashicorp/test"]`),
   339  	)
   340  	root.SetResourceInstanceCurrent(
   341  		mustResourceInstanceAddr("test_object.B[1]").Resource,
   342  		&states.ResourceInstanceObjectSrc{
   343  			Status:       states.ObjectReady,
   344  			AttrsJSON:    []byte(`{"id":"B","test_list":["x"]}`),
   345  			Dependencies: []addrs.ConfigResource{mustConfigResourceAddr("test_object.A")},
   346  		},
   347  		mustProviderConfig(`provider["registry.opentofu.org/hashicorp/test"]`),
   348  	)
   349  
   350  	g := cbdTestGraph(t, "transform-cbd-destroy-edge-both-count", changes, state)
   351  
   352  	actual := strings.TrimSpace(g.String())
   353  	expected := regexp.MustCompile(strings.TrimSpace(`
   354  test_object.A\[0\]
   355  test_object.A\[0\] \(destroy deposed \w+\)
   356    test_object.B\[0\]
   357    test_object.B\[1\]
   358  test_object.A\[1\]
   359  test_object.A\[1\] \(destroy deposed \w+\)
   360    test_object.B\[0\]
   361    test_object.B\[1\]
   362  test_object.B\[0\]
   363    test_object.A\[0\]
   364    test_object.A\[1\]
   365  test_object.B\[1\]
   366    test_object.A\[0\]
   367    test_object.A\[1\]
   368  `))
   369  
   370  	if !expected.MatchString(actual) {
   371  		t.Fatalf("wrong result\n\ngot:\n%s\n\nwant:\n%s", actual, expected)
   372  	}
   373  }