github.com/opentofu/opentofu@v1.7.1/internal/tofu/node_resource_deposed.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/dag"
    14  	"github.com/opentofu/opentofu/internal/instances"
    15  	"github.com/opentofu/opentofu/internal/plans"
    16  	"github.com/opentofu/opentofu/internal/states"
    17  	"github.com/opentofu/opentofu/internal/tfdiags"
    18  )
    19  
    20  // ConcreteResourceInstanceDeposedNodeFunc is a callback type used to convert
    21  // an abstract resource instance to a concrete one of some type that has
    22  // an associated deposed object key.
    23  type ConcreteResourceInstanceDeposedNodeFunc func(*NodeAbstractResourceInstance, states.DeposedKey) dag.Vertex
    24  
    25  type GraphNodeDeposedResourceInstanceObject interface {
    26  	DeposedInstanceObjectKey() states.DeposedKey
    27  }
    28  
    29  // NodePlanDeposedResourceInstanceObject represents deposed resource
    30  // instance objects during plan. These are distinct from the primary object
    31  // for each resource instance since the only valid operation to do with them
    32  // is to destroy them.
    33  //
    34  // This node type is also used during the refresh walk to ensure that the
    35  // record of a deposed object is up-to-date before we plan to destroy it.
    36  type NodePlanDeposedResourceInstanceObject struct {
    37  	*NodeAbstractResourceInstance
    38  	DeposedKey states.DeposedKey
    39  
    40  	// skipRefresh indicates that we should skip refreshing individual instances
    41  	skipRefresh bool
    42  
    43  	// skipPlanChanges indicates we should skip trying to plan change actions
    44  	// for any instances.
    45  	skipPlanChanges bool
    46  
    47  	// EndpointsToRemove are resource instance addresses where the user wants to
    48  	// forget from the state. This set isn't pre-filtered, so
    49  	// it might contain addresses that have nothing to do with the resource
    50  	// that this node represents, which the node itself must therefore ignore.
    51  	EndpointsToRemove []addrs.ConfigRemovable
    52  }
    53  
    54  var (
    55  	_ GraphNodeDeposedResourceInstanceObject = (*NodePlanDeposedResourceInstanceObject)(nil)
    56  	_ GraphNodeConfigResource                = (*NodePlanDeposedResourceInstanceObject)(nil)
    57  	_ GraphNodeResourceInstance              = (*NodePlanDeposedResourceInstanceObject)(nil)
    58  	_ GraphNodeReferenceable                 = (*NodePlanDeposedResourceInstanceObject)(nil)
    59  	_ GraphNodeReferencer                    = (*NodePlanDeposedResourceInstanceObject)(nil)
    60  	_ GraphNodeExecutable                    = (*NodePlanDeposedResourceInstanceObject)(nil)
    61  	_ GraphNodeProviderConsumer              = (*NodePlanDeposedResourceInstanceObject)(nil)
    62  	_ GraphNodeProvisionerConsumer           = (*NodePlanDeposedResourceInstanceObject)(nil)
    63  )
    64  
    65  func (n *NodePlanDeposedResourceInstanceObject) Name() string {
    66  	return fmt.Sprintf("%s (deposed %s)", n.ResourceInstanceAddr().String(), n.DeposedKey)
    67  }
    68  
    69  func (n *NodePlanDeposedResourceInstanceObject) DeposedInstanceObjectKey() states.DeposedKey {
    70  	return n.DeposedKey
    71  }
    72  
    73  // GraphNodeReferenceable implementation, overriding the one from NodeAbstractResourceInstance
    74  func (n *NodePlanDeposedResourceInstanceObject) ReferenceableAddrs() []addrs.Referenceable {
    75  	// Deposed objects don't participate in references.
    76  	return nil
    77  }
    78  
    79  // GraphNodeReferencer implementation, overriding the one from NodeAbstractResourceInstance
    80  func (n *NodePlanDeposedResourceInstanceObject) References() []*addrs.Reference {
    81  	// We don't evaluate configuration for deposed objects, so they effectively
    82  	// make no references.
    83  	return nil
    84  }
    85  
    86  // GraphNodeEvalable impl.
    87  func (n *NodePlanDeposedResourceInstanceObject) Execute(ctx EvalContext, op walkOperation) (diags tfdiags.Diagnostics) {
    88  	log.Printf("[TRACE] NodePlanDeposedResourceInstanceObject: planning %s deposed object %s", n.Addr, n.DeposedKey)
    89  
    90  	// Read the state for the deposed resource instance
    91  	state, err := n.readResourceInstanceStateDeposed(ctx, n.Addr, n.DeposedKey)
    92  	diags = diags.Append(err)
    93  	if diags.HasErrors() {
    94  		return diags
    95  	}
    96  
    97  	// Note any upgrades that readResourceInstanceState might've done in the
    98  	// prevRunState, so that it'll conform to current schema.
    99  	diags = diags.Append(n.writeResourceInstanceStateDeposed(ctx, n.DeposedKey, state, prevRunState))
   100  	if diags.HasErrors() {
   101  		return diags
   102  	}
   103  	// Also the refreshState, because that should still reflect schema upgrades
   104  	// even if not refreshing.
   105  	diags = diags.Append(n.writeResourceInstanceStateDeposed(ctx, n.DeposedKey, state, refreshState))
   106  	if diags.HasErrors() {
   107  		return diags
   108  	}
   109  
   110  	// We don't refresh during the planDestroy walk, since that is only adding
   111  	// the destroy changes to the plan and the provider will not be configured
   112  	// at this point. The other nodes use separate types for plan and destroy,
   113  	// while deposed instances are always a destroy operation, so the logic
   114  	// here is a bit overloaded.
   115  	if !n.skipRefresh && op != walkPlanDestroy {
   116  		// Refresh this object even though it is going to be destroyed, in
   117  		// case it's already been deleted outside OpenTofu. If this is a
   118  		// normal plan, providers expect a Read request to remove missing
   119  		// resources from the plan before apply, and may not handle a missing
   120  		// resource during Delete correctly. If this is a simple refresh,
   121  		// OpenTofu is expected to remove the missing resource from the state
   122  		// entirely
   123  		refreshedState, refreshDiags := n.refresh(ctx, n.DeposedKey, state)
   124  		diags = diags.Append(refreshDiags)
   125  		if diags.HasErrors() {
   126  			return diags
   127  		}
   128  
   129  		diags = diags.Append(n.writeResourceInstanceStateDeposed(ctx, n.DeposedKey, refreshedState, refreshState))
   130  		if diags.HasErrors() {
   131  			return diags
   132  		}
   133  
   134  		// If we refreshed then our subsequent planning should be in terms of
   135  		// the new object, not the original object.
   136  		state = refreshedState
   137  	}
   138  
   139  	if !n.skipPlanChanges {
   140  		var change *plans.ResourceInstanceChange
   141  		var planDiags tfdiags.Diagnostics
   142  
   143  		shouldForget := false
   144  
   145  		for _, etf := range n.EndpointsToRemove {
   146  			if etf.TargetContains(n.Addr) {
   147  				shouldForget = true
   148  			}
   149  		}
   150  
   151  		if shouldForget {
   152  			change = n.planForget(ctx, state, n.DeposedKey)
   153  		} else {
   154  			change, planDiags = n.planDestroy(ctx, state, n.DeposedKey)
   155  		}
   156  
   157  		diags = diags.Append(planDiags)
   158  		if diags.HasErrors() {
   159  			return diags
   160  		}
   161  
   162  		// NOTE: We don't check prevent_destroy for deposed objects, even
   163  		// though we would do so here for a "current" object, because
   164  		// if we've reached a point where an object is already deposed then
   165  		// we've already planned and partially-executed a create_before_destroy
   166  		// replace and we would've checked prevent_destroy at that point. We're
   167  		// now just need to get the deposed object destroyed, because there
   168  		// should be a new object already serving as its replacement.
   169  
   170  		diags = diags.Append(n.writeChange(ctx, change, n.DeposedKey))
   171  		if diags.HasErrors() {
   172  			return diags
   173  		}
   174  
   175  		diags = diags.Append(n.writeResourceInstanceStateDeposed(ctx, n.DeposedKey, nil, workingState))
   176  	} else {
   177  		// The working state should at least be updated with the result
   178  		// of upgrading and refreshing from above.
   179  		diags = diags.Append(n.writeResourceInstanceStateDeposed(ctx, n.DeposedKey, state, workingState))
   180  	}
   181  
   182  	return diags
   183  }
   184  
   185  // NodeDestroyDeposedResourceInstanceObject represents deposed resource
   186  // instance objects during apply. Nodes of this type are inserted by
   187  // DiffTransformer when the planned changeset contains "delete" changes for
   188  // deposed instance objects, and its only supported operation is to destroy
   189  // and then forget the associated object.
   190  type NodeDestroyDeposedResourceInstanceObject struct {
   191  	*NodeAbstractResourceInstance
   192  	DeposedKey states.DeposedKey
   193  }
   194  
   195  var (
   196  	_ GraphNodeDeposedResourceInstanceObject = (*NodeDestroyDeposedResourceInstanceObject)(nil)
   197  	_ GraphNodeConfigResource                = (*NodeDestroyDeposedResourceInstanceObject)(nil)
   198  	_ GraphNodeResourceInstance              = (*NodeDestroyDeposedResourceInstanceObject)(nil)
   199  	_ GraphNodeDestroyer                     = (*NodeDestroyDeposedResourceInstanceObject)(nil)
   200  	_ GraphNodeDestroyerCBD                  = (*NodeDestroyDeposedResourceInstanceObject)(nil)
   201  	_ GraphNodeReferenceable                 = (*NodeDestroyDeposedResourceInstanceObject)(nil)
   202  	_ GraphNodeReferencer                    = (*NodeDestroyDeposedResourceInstanceObject)(nil)
   203  	_ GraphNodeExecutable                    = (*NodeDestroyDeposedResourceInstanceObject)(nil)
   204  	_ GraphNodeProviderConsumer              = (*NodeDestroyDeposedResourceInstanceObject)(nil)
   205  	_ GraphNodeProvisionerConsumer           = (*NodeDestroyDeposedResourceInstanceObject)(nil)
   206  )
   207  
   208  func (n *NodeDestroyDeposedResourceInstanceObject) Name() string {
   209  	return fmt.Sprintf("%s (destroy deposed %s)", n.ResourceInstanceAddr(), n.DeposedKey)
   210  }
   211  
   212  func (n *NodeDestroyDeposedResourceInstanceObject) DeposedInstanceObjectKey() states.DeposedKey {
   213  	return n.DeposedKey
   214  }
   215  
   216  // GraphNodeReferenceable implementation, overriding the one from NodeAbstractResourceInstance
   217  func (n *NodeDestroyDeposedResourceInstanceObject) ReferenceableAddrs() []addrs.Referenceable {
   218  	// Deposed objects don't participate in references.
   219  	return nil
   220  }
   221  
   222  // GraphNodeReferencer implementation, overriding the one from NodeAbstractResourceInstance
   223  func (n *NodeDestroyDeposedResourceInstanceObject) References() []*addrs.Reference {
   224  	// We don't evaluate configuration for deposed objects, so they effectively
   225  	// make no references.
   226  	return nil
   227  }
   228  
   229  // GraphNodeDestroyer
   230  func (n *NodeDestroyDeposedResourceInstanceObject) DestroyAddr() *addrs.AbsResourceInstance {
   231  	addr := n.ResourceInstanceAddr()
   232  	return &addr
   233  }
   234  
   235  // GraphNodeDestroyerCBD
   236  func (n *NodeDestroyDeposedResourceInstanceObject) CreateBeforeDestroy() bool {
   237  	// A deposed instance is always CreateBeforeDestroy by definition, since
   238  	// we use deposed only to handle create-before-destroy.
   239  	return true
   240  }
   241  
   242  // GraphNodeDestroyerCBD
   243  func (n *NodeDestroyDeposedResourceInstanceObject) ModifyCreateBeforeDestroy(v bool) error {
   244  	if !v {
   245  		// Should never happen: deposed instances are _always_ create_before_destroy.
   246  		return fmt.Errorf("can't deactivate create_before_destroy for a deposed instance")
   247  	}
   248  	return nil
   249  }
   250  
   251  // GraphNodeExecutable impl.
   252  func (n *NodeDestroyDeposedResourceInstanceObject) Execute(ctx EvalContext, op walkOperation) (diags tfdiags.Diagnostics) {
   253  	var change *plans.ResourceInstanceChange
   254  
   255  	// Read the state for the deposed resource instance
   256  	state, err := n.readResourceInstanceStateDeposed(ctx, n.Addr, n.DeposedKey)
   257  	if err != nil {
   258  		return diags.Append(err)
   259  	}
   260  
   261  	if state == nil {
   262  		diags = diags.Append(fmt.Errorf("missing deposed state for %s (%s)", n.Addr, n.DeposedKey))
   263  		return diags
   264  	}
   265  
   266  	change, destroyPlanDiags := n.planDestroy(ctx, state, n.DeposedKey)
   267  	diags = diags.Append(destroyPlanDiags)
   268  	if diags.HasErrors() {
   269  		return diags
   270  	}
   271  
   272  	// Call pre-apply hook
   273  	diags = diags.Append(n.preApplyHook(ctx, change))
   274  	if diags.HasErrors() {
   275  		return diags
   276  	}
   277  
   278  	// we pass a nil configuration to apply because we are destroying
   279  	state, applyDiags := n.apply(ctx, state, change, nil, instances.RepetitionData{}, false)
   280  	diags = diags.Append(applyDiags)
   281  	// don't return immediately on errors, we need to handle the state
   282  
   283  	// Always write the resource back to the state deposed. If it
   284  	// was successfully destroyed it will be pruned. If it was not, it will
   285  	// be caught on the next run.
   286  	writeDiags := n.writeResourceInstanceState(ctx, state)
   287  	diags.Append(writeDiags)
   288  	if diags.HasErrors() {
   289  		return diags
   290  	}
   291  
   292  	diags = diags.Append(n.postApplyHook(ctx, state, diags.Err()))
   293  
   294  	return diags.Append(updateStateHook(ctx))
   295  }
   296  
   297  // GraphNodeDeposer is an optional interface implemented by graph nodes that
   298  // might create a single new deposed object for a specific associated resource
   299  // instance, allowing a caller to optionally pre-allocate a DeposedKey for
   300  // it.
   301  type GraphNodeDeposer interface {
   302  	// SetPreallocatedDeposedKey will be called during graph construction
   303  	// if a particular node must use a pre-allocated deposed key if/when it
   304  	// "deposes" the current object of its associated resource instance.
   305  	SetPreallocatedDeposedKey(key states.DeposedKey)
   306  }
   307  
   308  // graphNodeDeposer is an embeddable implementation of GraphNodeDeposer.
   309  // Embed it in a node type to get automatic support for it, and then access
   310  // the field PreallocatedDeposedKey to access any pre-allocated key.
   311  type graphNodeDeposer struct {
   312  	PreallocatedDeposedKey states.DeposedKey
   313  }
   314  
   315  func (n *graphNodeDeposer) SetPreallocatedDeposedKey(key states.DeposedKey) {
   316  	n.PreallocatedDeposedKey = key
   317  }
   318  
   319  func (n *NodeDestroyDeposedResourceInstanceObject) writeResourceInstanceState(ctx EvalContext, obj *states.ResourceInstanceObject) error {
   320  	absAddr := n.Addr
   321  	key := n.DeposedKey
   322  	state := ctx.State()
   323  
   324  	if key == states.NotDeposed {
   325  		// should never happen
   326  		return fmt.Errorf("can't save deposed object for %s without a deposed key; this is a bug in OpenTofu that should be reported", absAddr)
   327  	}
   328  
   329  	if obj == nil {
   330  		// No need to encode anything: we'll just write it directly.
   331  		state.SetResourceInstanceDeposed(absAddr, key, nil, n.ResolvedProvider)
   332  		log.Printf("[TRACE] writeResourceInstanceStateDeposed: removing state object for %s deposed %s", absAddr, key)
   333  		return nil
   334  	}
   335  
   336  	_, providerSchema, err := getProvider(ctx, n.ResolvedProvider)
   337  	if err != nil {
   338  		return err
   339  	}
   340  
   341  	schema, currentVersion := providerSchema.SchemaForResourceAddr(absAddr.ContainingResource().Resource)
   342  	if schema == nil {
   343  		// It shouldn't be possible to get this far in any real scenario
   344  		// without a schema, but we might end up here in contrived tests that
   345  		// fail to set up their world properly.
   346  		return fmt.Errorf("failed to encode %s in state: no resource type schema available", absAddr)
   347  	}
   348  	src, err := obj.Encode(schema.ImpliedType(), currentVersion)
   349  	if err != nil {
   350  		return fmt.Errorf("failed to encode %s in state: %w", absAddr, err)
   351  	}
   352  
   353  	log.Printf("[TRACE] writeResourceInstanceStateDeposed: writing state object for %s deposed %s", absAddr, key)
   354  	state.SetResourceInstanceDeposed(absAddr, key, src, n.ResolvedProvider)
   355  	return nil
   356  }
   357  
   358  // NodeForgetDeposedResourceInstanceObject represents deposed resource
   359  // instance objects during apply. Nodes of this type are inserted by
   360  // DiffTransformer when the planned changeset contains "forget" changes for
   361  // deposed instance objects, and its only supported operation is to forget
   362  // the associated object from the state.
   363  type NodeForgetDeposedResourceInstanceObject struct {
   364  	*NodeAbstractResourceInstance
   365  	DeposedKey states.DeposedKey
   366  }
   367  
   368  var (
   369  	_ GraphNodeDeposedResourceInstanceObject = (*NodeForgetDeposedResourceInstanceObject)(nil)
   370  	_ GraphNodeConfigResource                = (*NodeForgetDeposedResourceInstanceObject)(nil)
   371  	_ GraphNodeResourceInstance              = (*NodeForgetDeposedResourceInstanceObject)(nil)
   372  	_ GraphNodeReferenceable                 = (*NodeForgetDeposedResourceInstanceObject)(nil)
   373  	_ GraphNodeReferencer                    = (*NodeForgetDeposedResourceInstanceObject)(nil)
   374  	_ GraphNodeExecutable                    = (*NodeForgetDeposedResourceInstanceObject)(nil)
   375  	_ GraphNodeProviderConsumer              = (*NodeForgetDeposedResourceInstanceObject)(nil)
   376  	_ GraphNodeProvisionerConsumer           = (*NodeForgetDeposedResourceInstanceObject)(nil)
   377  )
   378  
   379  func (n *NodeForgetDeposedResourceInstanceObject) Name() string {
   380  	return fmt.Sprintf("%s (forget deposed %s)", n.ResourceInstanceAddr(), n.DeposedKey)
   381  }
   382  
   383  func (n *NodeForgetDeposedResourceInstanceObject) DeposedInstanceObjectKey() states.DeposedKey {
   384  	return n.DeposedKey
   385  }
   386  
   387  // GraphNodeReferenceable implementation, overriding the one from NodeAbstractResourceInstance
   388  func (n *NodeForgetDeposedResourceInstanceObject) ReferenceableAddrs() []addrs.Referenceable {
   389  	// Deposed objects don't participate in references.
   390  	return nil
   391  }
   392  
   393  // GraphNodeReferencer implementation, overriding the one from NodeAbstractResourceInstance
   394  func (n *NodeForgetDeposedResourceInstanceObject) References() []*addrs.Reference {
   395  	// We don't evaluate configuration for deposed objects, so they effectively
   396  	// make no references.
   397  	return nil
   398  }
   399  
   400  // GraphNodeExecutable impl.
   401  func (n *NodeForgetDeposedResourceInstanceObject) Execute(ctx EvalContext, op walkOperation) (diags tfdiags.Diagnostics) {
   402  	// Read the state for the deposed resource instance
   403  	state, err := n.readResourceInstanceStateDeposed(ctx, n.Addr, n.DeposedKey)
   404  	if err != nil {
   405  		return diags.Append(err)
   406  	}
   407  
   408  	if state == nil {
   409  		log.Printf("[WARN] NodeForgetDeposedResourceInstanceObject for %s (%s) with no state", n.Addr, n.DeposedKey)
   410  	}
   411  
   412  	contextState := ctx.State()
   413  	contextState.ForgetResourceInstanceDeposed(n.Addr, n.DeposedKey)
   414  
   415  	return diags.Append(updateStateHook(ctx))
   416  }