| // Copyright (c) The OpenTofu Authors | |
| // SPDX-License-Identifier: MPL-2.0 | |
| // Copyright (c) 2023 HashiCorp, Inc. | |
| // SPDX-License-Identifier: MPL-2.0 | |
| package tofu | |
| import ( | |
| "context" | |
| "fmt" | |
| "log" | |
| "github.com/opentofu/opentofu/internal/addrs" | |
| "github.com/opentofu/opentofu/internal/configs" | |
| "github.com/opentofu/opentofu/internal/dag" | |
| "github.com/opentofu/opentofu/internal/plans" | |
| "github.com/opentofu/opentofu/internal/refactoring" | |
| "github.com/opentofu/opentofu/internal/states" | |
| "github.com/opentofu/opentofu/internal/tfdiags" | |
| ) | |
| // DiffTransformer is a GraphTransformer that adds graph nodes representing | |
| // each of the resource changes described in the given Changes object. | |
| type DiffTransformer struct { | |
| Concrete ConcreteResourceInstanceNodeFunc | |
| State *states.State | |
| Changes *plans.Changes | |
| Config *configs.Config | |
| } | |
| // return true if the given resource instance has either Preconditions or | |
| // Postconditions defined in the configuration. | |
| func (t *DiffTransformer) hasConfigConditions(addr addrs.AbsResourceInstance) bool { | |
| // unit tests may have no config | |
| if t.Config == nil { | |
| return false | |
| } | |
| cfg := t.Config.DescendentForInstance(addr.Module) | |
| if cfg == nil { | |
| return false | |
| } | |
| res := cfg.Module.ResourceByAddr(addr.ConfigResource().Resource) | |
| if res == nil { | |
| return false | |
| } | |
| return len(res.Preconditions) > 0 || len(res.Postconditions) > 0 | |
| } | |
| func (t *DiffTransformer) Transform(_ context.Context, g *Graph) error { | |
| if t.Changes == nil || len(t.Changes.Resources) == 0 { | |
| // Nothing to do! | |
| return nil | |
| } | |
| // Go through all the modules in the diff. | |
| log.Printf("[TRACE] DiffTransformer starting") | |
| var diags tfdiags.Diagnostics | |
| state := t.State | |
| changes := t.Changes | |
| // DiffTransformer creates resource _instance_ nodes. If there are any | |
| // whole-resource nodes already in the graph, we must ensure that they | |
| // get evaluated before any of the corresponding instances by creating | |
| // dependency edges, so we'll do some prep work here to ensure we'll only | |
| // create connections to nodes that existed before we started here. | |
| resourceNodes := map[string][]GraphNodeConfigResource{} | |
| for _, node := range g.Vertices() { | |
| rn, ok := node.(GraphNodeConfigResource) | |
| if !ok { | |
| continue | |
| } | |
| // We ignore any instances that _also_ implement | |
| // GraphNodeResourceInstance, since in the unlikely event that they | |
| // do exist we'd probably end up creating cycles by connecting them. | |
| if _, ok := node.(GraphNodeResourceInstance); ok { | |
| continue | |
| } | |
| addr := rn.ResourceAddr().String() | |
| resourceNodes[addr] = append(resourceNodes[addr], rn) | |
| } | |
| for _, rc := range changes.Resources { | |
| addr := rc.Addr | |
| dk := rc.DeposedKey | |
| log.Printf("[TRACE] DiffTransformer: found %s change for %s %s", rc.Action, addr, dk) | |
| // Depending on the action we'll need some different combinations of | |
| // nodes, because destroying uses a special node type separate from | |
| // other actions. | |
| var update, delete, forget, createBeforeDestroy bool | |
| switch rc.Action { | |
| case plans.NoOp: | |
| // For a no-op change we don't take any action but we still | |
| // run any condition checks associated with the object, to | |
| // make sure that they still hold when considering the | |
| // results of other changes. | |
| update = t.hasConfigConditions(addr) | |
| case plans.Delete: | |
| delete = true | |
| case plans.Forget: | |
| forget = true | |
| case plans.ForgetThenCreate: | |
| update = true | |
| forget = true | |
| case plans.DeleteThenCreate, plans.CreateThenDelete: | |
| update = true | |
| delete = true | |
| createBeforeDestroy = (rc.Action == plans.CreateThenDelete) | |
| default: | |
| update = true | |
| } | |
| // A deposed instance may only have a change of Delete, Forget or NoOp. | |
| // A NoOp can happen if the provider shows it no longer exists during | |
| // the most recent ReadResource operation. | |
| if dk != states.NotDeposed && rc.Action != plans.Delete && rc.Action != plans.NoOp && rc.Action != plans.Forget { | |
| diags = diags.Append(tfdiags.Sourceless( | |
| tfdiags.Error, | |
| "Invalid planned change for deposed object", | |
| 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), | |
| )) | |
| continue | |
| } | |
| // If we're going to do a create_before_destroy Replace operation then | |
| // we need to allocate a DeposedKey to use to retain the | |
| // not-yet-destroyed prior object, so that the delete node can destroy | |
| // _that_ rather than the newly-created node, which will be current | |
| // by the time the delete node is visited. | |
| if update && delete && createBeforeDestroy { | |
| // In this case, variable dk will be the _pre-assigned_ DeposedKey | |
| // that must be used if the update graph node deposes the current | |
| // instance, which will then align with the same key we pass | |
| // into the destroy node to ensure we destroy exactly the deposed | |
| // object we expect. | |
| if state != nil { | |
| ris := state.ResourceInstance(addr) | |
| if ris == nil { | |
| // Should never happen, since we don't plan to replace an | |
| // instance that doesn't exist yet. | |
| diags = diags.Append(tfdiags.Sourceless( | |
| tfdiags.Error, | |
| "Invalid planned change", | |
| fmt.Sprintf("The plan contains a replace change for %s, which doesn't exist yet. This is a bug in OpenTofu.", addr), | |
| )) | |
| continue | |
| } | |
| // Allocating a deposed key separately from using it can be racy | |
| // in general, but we assume here that nothing except the apply | |
| // node we instantiate below will actually make new deposed objects | |
| // in practice, and so the set of already-used keys will not change | |
| // between now and then. | |
| dk = ris.FindUnusedDeposedKey() | |
| } else { | |
| // If we have no state at all yet then we can use _any_ | |
| // DeposedKey. | |
| dk = states.NewDeposedKey() | |
| } | |
| } | |
| if update { | |
| // All actions except destroying the node type chosen by t.Concrete | |
| abstract := NewNodeAbstractResourceInstance(addr) | |
| var node dag.Vertex = abstract | |
| if t.Concrete != nil { | |
| node = t.Concrete(abstract) | |
| } | |
| if createBeforeDestroy { | |
| // We'll attach our pre-allocated DeposedKey to the node if | |
| // it supports that. NodeApplyableResourceInstance is the | |
| // specific concrete node type we are looking for here really, | |
| // since that's the only node type that might depose objects. | |
| if dn, ok := node.(GraphNodeDeposer); ok { | |
| dn.SetPreallocatedDeposedKey(dk) | |
| } | |
| // We need to set CBD to the node here, otherwise if CBD flag was caused | |
| // by the CBD descendant of the node (and not the config) and this is the sole node being updated | |
| // in the current apply operation, we will lose the CBD flag in the state file and cause the "cycle" error down the line. | |
| // For more details, see the issue https://github.com/opentofu/opentofu/issues/2398 | |
| if cn, ok := node.(GraphNodeDestroyerCBD); ok { | |
| log.Printf("[TRACE] DiffTransformer: %s implements GraphNodeDestroyerCBD, setting CBD to true", addr) | |
| // Setting CBD to true | |
| // Error handling here is just for future-proofing, since none of the GraphNodeDestroyerCBD current implementations | |
| // should return an error when setting CBD to true | |
| if err := cn.ModifyCreateBeforeDestroy(true); err != nil { | |
| diags = diags.Append(tfdiags.Sourceless( | |
| tfdiags.Error, | |
| "Invalid planned change", | |
| fmt.Sprintf("%s: wasn't able to set CBD, error occurred %s", dag.VertexName(node), err.Error()))) | |
| log.Printf("[TRACE] DiffTransformer: %s: wasn't able to set CBD, error occurred %s", addr, err.Error()) | |
| continue | |
| } | |
| } | |
| log.Printf("[TRACE] DiffTransformer: %s will be represented by %s, deposing prior object to %s", addr, dag.VertexName(node), dk) | |
| } else { | |
| log.Printf("[TRACE] DiffTransformer: %s will be represented by %s", addr, dag.VertexName(node)) | |
| } | |
| g.Add(node) | |
| rsrcAddr := addr.ContainingResource().String() | |
| for _, rsrcNode := range resourceNodes[rsrcAddr] { | |
| g.Connect(dag.BasicEdge(node, rsrcNode)) | |
| } | |
| if forget { | |
| // We need to first forget the resource instance and make anew | |
| forgetNode := produceForgetNode(addr, dk) | |
| g.Add(forgetNode) | |
| g.Connect(dag.BasicEdge(node, forgetNode)) | |
| } | |
| } | |
| if delete { | |
| // Destroying always uses a destroy-specific node type, though | |
| // which one depends on whether we're destroying a current object | |
| // or a deposed object. | |
| var node GraphNodeResourceInstance | |
| abstract := NewNodeAbstractResourceInstance(addr) | |
| if dk == states.NotDeposed { | |
| // If any removed block is targeting the resource in this node, ensure that any provisioners defined in that block are going to be | |
| // executed before actual resource destruction. | |
| abstract.removedBlockProvisioners = refactoring.FindResourceRemovedBlockProvisioners(t.Config, abstract.Addr.ConfigResource()) | |
| node = &NodeDestroyResourceInstance{ | |
| NodeAbstractResourceInstance: abstract, | |
| DeposedKey: dk, | |
| } | |
| } else { | |
| node = &NodeDestroyDeposedResourceInstanceObject{ | |
| NodeAbstractResourceInstance: abstract, | |
| DeposedKey: dk, | |
| } | |
| } | |
| if dk == states.NotDeposed { | |
| log.Printf("[TRACE] DiffTransformer: %s will be represented for destruction by %s", addr, dag.VertexName(node)) | |
| } else { | |
| log.Printf("[TRACE] DiffTransformer: %s deposed object %s will be represented for destruction by %s", addr, dk, dag.VertexName(node)) | |
| } | |
| g.Add(node) | |
| } | |
| if forget && !update { | |
| g.Add(produceForgetNode(addr, dk)) | |
| } | |
| } | |
| log.Printf("[TRACE] DiffTransformer complete") | |
| return diags.Err() | |
| } | |
| func produceForgetNode(addr addrs.AbsResourceInstance, deposedKey states.DeposedKey) GraphNodeResourceInstance { | |
| var node GraphNodeResourceInstance | |
| abstract := NewNodeAbstractResourceInstance(addr) | |
| if deposedKey == states.NotDeposed { | |
| node = &NodeForgetResourceInstance{ | |
| NodeAbstractResourceInstance: abstract, | |
| DeposedKey: deposedKey, | |
| } | |
| log.Printf("[TRACE] DiffTransformer: %s will be represented for removal from the state by %s", addr, dag.VertexName(node)) | |
| } else { | |
| node = &NodeForgetDeposedResourceInstanceObject{ | |
| NodeAbstractResourceInstance: abstract, | |
| DeposedKey: deposedKey, | |
| } | |
| log.Printf("[TRACE] DiffTransformer: %s deposed object %s will be represented for removal from the state by %s", addr, deposedKey, dag.VertexName(node)) | |
| } | |
| return node | |
| } |