Skip to content

Commit a1e5af5

Browse files
committed
prevent cycles when connecting destroy nodes
When adding destroy edges between resources from different providers, and a provider itself depends on the other provider's resources, we can get cycles in the final dependency graph. The problem is a little deeper than simply not connecting these nodes, since the edges are still needed when doing a full destroy operation. For now we can get by assuming the edges are required, and reverting them only if they result in a cycle. This works because destroy edges are the last edges added to managed resources during graph building. This was rarely a problem before v1.3, because noop nodes were not added to the apply graph, and unused values were aggressively pruned. In v1.3 however all nodes are kept in the graph so that postcondition blocks are always evaluated during apply, increasing the chances of the cycles appearing.
1 parent 4153685 commit a1e5af5

File tree

2 files changed

+70
-2
lines changed

2 files changed

+70
-2
lines changed

internal/terraform/context_apply2_test.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1201,6 +1201,20 @@ output "out" {
12011201
state, diags := ctx.Apply(plan, m)
12021202
assertNoErrors(t, diags)
12031203

1204+
// Add some changes to make the planning more complex.
1205+
// Taint the object to make sure a replacement works in the plan.
1206+
otherObjAddr := mustResourceInstanceAddr("other_object.other")
1207+
otherObj := state.ResourceInstance(otherObjAddr)
1208+
otherObj.Current.Status = states.ObjectTainted
1209+
// Force a change which needs to be reverted.
1210+
testObjAddr := mustResourceInstanceAddr(`module.mod["a"].test_object.a`)
1211+
testObjA := state.ResourceInstance(testObjAddr)
1212+
testObjA.Current.AttrsJSON = []byte(`{"test_bool":null,"test_list":null,"test_map":null,"test_number":null,"test_string":"changed"}`)
1213+
1214+
_, diags = ctx.Plan(m, state, opts)
1215+
assertNoErrors(t, diags)
1216+
return
1217+
12041218
otherProvider.ConfigureProviderCalled = false
12051219
otherProvider.ConfigureProviderFn = func(req providers.ConfigureProviderRequest) (resp providers.ConfigureProviderResponse) {
12061220
// check that our config is complete, even during a destroy plan

internal/terraform/transform_destroy_edge.go

Lines changed: 56 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,60 @@ type GraphNodeCreator interface {
3939
// still subnets.
4040
type DestroyEdgeTransformer struct{}
4141

42+
// tryInterProviderDestroyEdge checks if we're inserting a destroy edge
43+
// across a provider boundary, and only adds the edge if it results in no cycles.
44+
//
45+
// FIXME: The cycles can arise in valid configurations when a provider depends
46+
// on resources from another provider. In the future we may want to inspect
47+
// the dependencies of the providers themselves, to avoid needing to use the
48+
// blunt hammer of checking for cycles.
49+
//
50+
// A reduced example of this dependency problem looks something like:
51+
//
52+
// createA <- createB
53+
// | \ / |
54+
// | providerB <- |
55+
// v \ v
56+
// destroyA -------------> destroyB
57+
//
58+
// The edge from destroyA to destroyB would be skipped in this case, but there
59+
// are still other combinations of changes which could connect the A and B
60+
// groups around providerB in various ways.
61+
//
62+
// The most difficult problem here happens during a full destroy operation.
63+
// That creates a special case where resources on which a provider depends must
64+
// exist for evaluation before they are destroyed. This means that any provider
65+
// dependencies must wait until all that provider's resources have first been
66+
// destroyed. This is where these cross-provider edges are still required to
67+
// ensure the correct order.
68+
func (t *DestroyEdgeTransformer) tryInterProviderDestroyEdge(g *Graph, from, to dag.Vertex) {
69+
e := dag.BasicEdge(from, to)
70+
g.Connect(e)
71+
72+
pc, ok := from.(GraphNodeProviderConsumer)
73+
if !ok {
74+
return
75+
}
76+
fromProvider := pc.Provider()
77+
78+
pc, ok = to.(GraphNodeProviderConsumer)
79+
if !ok {
80+
return
81+
}
82+
toProvider := pc.Provider()
83+
84+
sameProvider := fromProvider.Equals(toProvider)
85+
86+
// Check for cycles, and back out the edge if there are any.
87+
// The cycles we are looking for only appears between providers, so don't
88+
// waste time checking for cycles if both nodes use the same provider.
89+
if !sameProvider && len(g.Cycles()) > 0 {
90+
log.Printf("[DEBUG] DestroyEdgeTransformer: skipping inter-provider edge %s->%s which creates a cycle",
91+
dag.VertexName(from), dag.VertexName(to))
92+
g.RemoveEdge(e)
93+
}
94+
}
95+
4296
func (t *DestroyEdgeTransformer) Transform(g *Graph) error {
4397
// Build a map of what is being destroyed (by address string) to
4498
// the list of destroyers.
@@ -93,7 +147,7 @@ func (t *DestroyEdgeTransformer) Transform(g *Graph) error {
93147
for _, desDep := range destroyersByResource[resAddr.String()] {
94148
if !graphNodesAreResourceInstancesInDifferentInstancesOfSameModule(desDep, des) {
95149
log.Printf("[TRACE] DestroyEdgeTransformer: %s has stored dependency of %s\n", dag.VertexName(desDep), dag.VertexName(des))
96-
g.Connect(dag.BasicEdge(desDep, des))
150+
t.tryInterProviderDestroyEdge(g, desDep, des)
97151
} else {
98152
log.Printf("[TRACE] DestroyEdgeTransformer: skipping %s => %s inter-module-instance dependency\n", dag.VertexName(desDep), dag.VertexName(des))
99153
}
@@ -105,7 +159,7 @@ func (t *DestroyEdgeTransformer) Transform(g *Graph) error {
105159
for _, createDep := range creators[resAddr.String()] {
106160
if !graphNodesAreResourceInstancesInDifferentInstancesOfSameModule(createDep, des) {
107161
log.Printf("[DEBUG] DestroyEdgeTransformer: %s has stored dependency of %s\n", dag.VertexName(createDep), dag.VertexName(des))
108-
g.Connect(dag.BasicEdge(createDep, des))
162+
t.tryInterProviderDestroyEdge(g, createDep, des)
109163
} else {
110164
log.Printf("[TRACE] DestroyEdgeTransformer: skipping %s => %s inter-module-instance dependency\n", dag.VertexName(createDep), dag.VertexName(des))
111165
}

0 commit comments

Comments
 (0)