github.com/opentofu/opentofu@v1.7.1/internal/tofu/transform_diff.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 "log" 11 12 "github.com/opentofu/opentofu/internal/addrs" 13 "github.com/opentofu/opentofu/internal/configs" 14 "github.com/opentofu/opentofu/internal/dag" 15 "github.com/opentofu/opentofu/internal/plans" 16 "github.com/opentofu/opentofu/internal/states" 17 "github.com/opentofu/opentofu/internal/tfdiags" 18 ) 19 20 // DiffTransformer is a GraphTransformer that adds graph nodes representing 21 // each of the resource changes described in the given Changes object. 22 type DiffTransformer struct { 23 Concrete ConcreteResourceInstanceNodeFunc 24 State *states.State 25 Changes *plans.Changes 26 Config *configs.Config 27 } 28 29 // return true if the given resource instance has either Preconditions or 30 // Postconditions defined in the configuration. 31 func (t *DiffTransformer) hasConfigConditions(addr addrs.AbsResourceInstance) bool { 32 // unit tests may have no config 33 if t.Config == nil { 34 return false 35 } 36 37 cfg := t.Config.DescendentForInstance(addr.Module) 38 if cfg == nil { 39 return false 40 } 41 42 res := cfg.Module.ResourceByAddr(addr.ConfigResource().Resource) 43 if res == nil { 44 return false 45 } 46 47 return len(res.Preconditions) > 0 || len(res.Postconditions) > 0 48 } 49 50 func (t *DiffTransformer) Transform(g *Graph) error { 51 if t.Changes == nil || len(t.Changes.Resources) == 0 { 52 // Nothing to do! 53 return nil 54 } 55 56 // Go through all the modules in the diff. 57 log.Printf("[TRACE] DiffTransformer starting") 58 59 var diags tfdiags.Diagnostics 60 state := t.State 61 changes := t.Changes 62 63 // DiffTransformer creates resource _instance_ nodes. If there are any 64 // whole-resource nodes already in the graph, we must ensure that they 65 // get evaluated before any of the corresponding instances by creating 66 // dependency edges, so we'll do some prep work here to ensure we'll only 67 // create connections to nodes that existed before we started here. 68 resourceNodes := map[string][]GraphNodeConfigResource{} 69 for _, node := range g.Vertices() { 70 rn, ok := node.(GraphNodeConfigResource) 71 if !ok { 72 continue 73 } 74 // We ignore any instances that _also_ implement 75 // GraphNodeResourceInstance, since in the unlikely event that they 76 // do exist we'd probably end up creating cycles by connecting them. 77 if _, ok := node.(GraphNodeResourceInstance); ok { 78 continue 79 } 80 81 addr := rn.ResourceAddr().String() 82 resourceNodes[addr] = append(resourceNodes[addr], rn) 83 } 84 85 for _, rc := range changes.Resources { 86 addr := rc.Addr 87 dk := rc.DeposedKey 88 89 log.Printf("[TRACE] DiffTransformer: found %s change for %s %s", rc.Action, addr, dk) 90 91 // Depending on the action we'll need some different combinations of 92 // nodes, because destroying uses a special node type separate from 93 // other actions. 94 var update, delete, forget, createBeforeDestroy bool 95 switch rc.Action { 96 case plans.NoOp: 97 // For a no-op change we don't take any action but we still 98 // run any condition checks associated with the object, to 99 // make sure that they still hold when considering the 100 // results of other changes. 101 update = t.hasConfigConditions(addr) 102 case plans.Delete: 103 delete = true 104 case plans.Forget: 105 forget = true 106 case plans.DeleteThenCreate, plans.CreateThenDelete: 107 update = true 108 delete = true 109 createBeforeDestroy = (rc.Action == plans.CreateThenDelete) 110 default: 111 update = true 112 } 113 114 // A deposed instance may only have a change of Delete, Forget or NoOp. 115 // A NoOp can happen if the provider shows it no longer exists during 116 // the most recent ReadResource operation. 117 if dk != states.NotDeposed && !(rc.Action == plans.Delete || rc.Action == plans.NoOp || rc.Action == plans.Forget) { 118 diags = diags.Append(tfdiags.Sourceless( 119 tfdiags.Error, 120 "Invalid planned change for deposed object", 121 fmt.Sprintf("The plan contains a non-removal change for %s deposed object %s. The only valid actions for a deposed object is to destroy it or forget it, so this is a bug in OpenTofu.", addr, dk), 122 )) 123 continue 124 } 125 126 // If we're going to do a create_before_destroy Replace operation then 127 // we need to allocate a DeposedKey to use to retain the 128 // not-yet-destroyed prior object, so that the delete node can destroy 129 // _that_ rather than the newly-created node, which will be current 130 // by the time the delete node is visited. 131 if update && delete && createBeforeDestroy { 132 // In this case, variable dk will be the _pre-assigned_ DeposedKey 133 // that must be used if the update graph node deposes the current 134 // instance, which will then align with the same key we pass 135 // into the destroy node to ensure we destroy exactly the deposed 136 // object we expect. 137 if state != nil { 138 ris := state.ResourceInstance(addr) 139 if ris == nil { 140 // Should never happen, since we don't plan to replace an 141 // instance that doesn't exist yet. 142 diags = diags.Append(tfdiags.Sourceless( 143 tfdiags.Error, 144 "Invalid planned change", 145 fmt.Sprintf("The plan contains a replace change for %s, which doesn't exist yet. This is a bug in OpenTofu.", addr), 146 )) 147 continue 148 } 149 150 // Allocating a deposed key separately from using it can be racy 151 // in general, but we assume here that nothing except the apply 152 // node we instantiate below will actually make new deposed objects 153 // in practice, and so the set of already-used keys will not change 154 // between now and then. 155 dk = ris.FindUnusedDeposedKey() 156 } else { 157 // If we have no state at all yet then we can use _any_ 158 // DeposedKey. 159 dk = states.NewDeposedKey() 160 } 161 } 162 163 if update { 164 // All actions except destroying the node type chosen by t.Concrete 165 abstract := NewNodeAbstractResourceInstance(addr) 166 var node dag.Vertex = abstract 167 if f := t.Concrete; f != nil { 168 node = f(abstract) 169 } 170 171 if createBeforeDestroy { 172 // We'll attach our pre-allocated DeposedKey to the node if 173 // it supports that. NodeApplyableResourceInstance is the 174 // specific concrete node type we are looking for here really, 175 // since that's the only node type that might depose objects. 176 if dn, ok := node.(GraphNodeDeposer); ok { 177 dn.SetPreallocatedDeposedKey(dk) 178 } 179 log.Printf("[TRACE] DiffTransformer: %s will be represented by %s, deposing prior object to %s", addr, dag.VertexName(node), dk) 180 } else { 181 log.Printf("[TRACE] DiffTransformer: %s will be represented by %s", addr, dag.VertexName(node)) 182 } 183 184 g.Add(node) 185 rsrcAddr := addr.ContainingResource().String() 186 for _, rsrcNode := range resourceNodes[rsrcAddr] { 187 g.Connect(dag.BasicEdge(node, rsrcNode)) 188 } 189 } 190 191 if delete { 192 // Destroying always uses a destroy-specific node type, though 193 // which one depends on whether we're destroying a current object 194 // or a deposed object. 195 var node GraphNodeResourceInstance 196 abstract := NewNodeAbstractResourceInstance(addr) 197 if dk == states.NotDeposed { 198 node = &NodeDestroyResourceInstance{ 199 NodeAbstractResourceInstance: abstract, 200 DeposedKey: dk, 201 } 202 } else { 203 node = &NodeDestroyDeposedResourceInstanceObject{ 204 NodeAbstractResourceInstance: abstract, 205 DeposedKey: dk, 206 } 207 } 208 if dk == states.NotDeposed { 209 log.Printf("[TRACE] DiffTransformer: %s will be represented for destruction by %s", addr, dag.VertexName(node)) 210 } else { 211 log.Printf("[TRACE] DiffTransformer: %s deposed object %s will be represented for destruction by %s", addr, dk, dag.VertexName(node)) 212 } 213 g.Add(node) 214 } 215 216 if forget { 217 var node GraphNodeResourceInstance 218 abstract := NewNodeAbstractResourceInstance(addr) 219 if dk == states.NotDeposed { 220 node = &NodeForgetResourceInstance{ 221 NodeAbstractResourceInstance: abstract, 222 DeposedKey: dk, 223 } 224 log.Printf("[TRACE] DiffTransformer: %s will be represented for removal from the state by %s", addr, dag.VertexName(node)) 225 } else { 226 node = &NodeForgetDeposedResourceInstanceObject{ 227 NodeAbstractResourceInstance: abstract, 228 DeposedKey: dk, 229 } 230 log.Printf("[TRACE] DiffTransformer: %s deposed object %s will be represented for removal from the state by %s", addr, dk, dag.VertexName(node)) 231 } 232 233 g.Add(node) 234 } 235 236 } 237 238 log.Printf("[TRACE] DiffTransformer complete") 239 240 return diags.Err() 241 }