github.com/cycloidio/terraform@v1.1.10-0.20220513142504-76d5c768dc63/terraform/node_resource_apply_instance.go (about)

     1  package terraform
     2  
     3  import (
     4  	"fmt"
     5  	"log"
     6  
     7  	"github.com/cycloidio/terraform/addrs"
     8  	"github.com/cycloidio/terraform/configs"
     9  	"github.com/cycloidio/terraform/plans"
    10  	"github.com/cycloidio/terraform/plans/objchange"
    11  	"github.com/cycloidio/terraform/states"
    12  	"github.com/cycloidio/terraform/tfdiags"
    13  )
    14  
    15  // NodeApplyableResourceInstance represents a resource instance that is
    16  // "applyable": it is ready to be applied and is represented by a diff.
    17  //
    18  // This node is for a specific instance of a resource. It will usually be
    19  // accompanied in the graph by a NodeApplyableResource representing its
    20  // containing resource, and should depend on that node to ensure that the
    21  // state is properly prepared to receive changes to instances.
    22  type NodeApplyableResourceInstance struct {
    23  	*NodeAbstractResourceInstance
    24  
    25  	graphNodeDeposer // implementation of GraphNodeDeposerConfig
    26  
    27  	// If this node is forced to be CreateBeforeDestroy, we need to record that
    28  	// in the state to.
    29  	ForceCreateBeforeDestroy bool
    30  
    31  	// forceReplace are resource instance addresses where the user wants to
    32  	// force generating a replace action. This set isn't pre-filtered, so
    33  	// it might contain addresses that have nothing to do with the resource
    34  	// that this node represents, which the node itself must therefore ignore.
    35  	forceReplace []addrs.AbsResourceInstance
    36  }
    37  
    38  var (
    39  	_ GraphNodeConfigResource     = (*NodeApplyableResourceInstance)(nil)
    40  	_ GraphNodeResourceInstance   = (*NodeApplyableResourceInstance)(nil)
    41  	_ GraphNodeCreator            = (*NodeApplyableResourceInstance)(nil)
    42  	_ GraphNodeReferencer         = (*NodeApplyableResourceInstance)(nil)
    43  	_ GraphNodeDeposer            = (*NodeApplyableResourceInstance)(nil)
    44  	_ GraphNodeExecutable         = (*NodeApplyableResourceInstance)(nil)
    45  	_ GraphNodeAttachDependencies = (*NodeApplyableResourceInstance)(nil)
    46  )
    47  
    48  // CreateBeforeDestroy returns this node's CreateBeforeDestroy status.
    49  func (n *NodeApplyableResourceInstance) CreateBeforeDestroy() bool {
    50  	if n.ForceCreateBeforeDestroy {
    51  		return n.ForceCreateBeforeDestroy
    52  	}
    53  
    54  	if n.Config != nil && n.Config.Managed != nil {
    55  		return n.Config.Managed.CreateBeforeDestroy
    56  	}
    57  
    58  	return false
    59  }
    60  
    61  func (n *NodeApplyableResourceInstance) ModifyCreateBeforeDestroy(v bool) error {
    62  	n.ForceCreateBeforeDestroy = v
    63  	return nil
    64  }
    65  
    66  // GraphNodeCreator
    67  func (n *NodeApplyableResourceInstance) CreateAddr() *addrs.AbsResourceInstance {
    68  	addr := n.ResourceInstanceAddr()
    69  	return &addr
    70  }
    71  
    72  // GraphNodeReferencer, overriding NodeAbstractResourceInstance
    73  func (n *NodeApplyableResourceInstance) References() []*addrs.Reference {
    74  	// Start with the usual resource instance implementation
    75  	ret := n.NodeAbstractResourceInstance.References()
    76  
    77  	// Applying a resource must also depend on the destruction of any of its
    78  	// dependencies, since this may for example affect the outcome of
    79  	// evaluating an entire list of resources with "count" set (by reducing
    80  	// the count).
    81  	//
    82  	// However, we can't do this in create_before_destroy mode because that
    83  	// would create a dependency cycle. We make a compromise here of requiring
    84  	// changes to be updated across two applies in this case, since the first
    85  	// plan will use the old values.
    86  	if !n.CreateBeforeDestroy() {
    87  		for _, ref := range ret {
    88  			switch tr := ref.Subject.(type) {
    89  			case addrs.ResourceInstance:
    90  				newRef := *ref // shallow copy so we can mutate
    91  				newRef.Subject = tr.Phase(addrs.ResourceInstancePhaseDestroy)
    92  				newRef.Remaining = nil // can't access attributes of something being destroyed
    93  				ret = append(ret, &newRef)
    94  			case addrs.Resource:
    95  				newRef := *ref // shallow copy so we can mutate
    96  				newRef.Subject = tr.Phase(addrs.ResourceInstancePhaseDestroy)
    97  				newRef.Remaining = nil // can't access attributes of something being destroyed
    98  				ret = append(ret, &newRef)
    99  			}
   100  		}
   101  	}
   102  
   103  	return ret
   104  }
   105  
   106  // GraphNodeAttachDependencies
   107  func (n *NodeApplyableResourceInstance) AttachDependencies(deps []addrs.ConfigResource) {
   108  	n.Dependencies = deps
   109  }
   110  
   111  // GraphNodeExecutable
   112  func (n *NodeApplyableResourceInstance) Execute(ctx EvalContext, op walkOperation) (diags tfdiags.Diagnostics) {
   113  	addr := n.ResourceInstanceAddr()
   114  
   115  	if n.Config == nil {
   116  		// This should not be possible, but we've got here in at least one
   117  		// case as discussed in the following issue:
   118  		//    https://github.com/cycloidio/terraform/issues/21258
   119  		// To avoid an outright crash here, we'll instead return an explicit
   120  		// error.
   121  		diags = diags.Append(tfdiags.Sourceless(
   122  			tfdiags.Error,
   123  			"Resource node has no configuration attached",
   124  			fmt.Sprintf(
   125  				"The graph node for %s has no configuration attached to it. This suggests a bug in Terraform's apply graph builder; please report it!",
   126  				addr,
   127  			),
   128  		))
   129  		return diags
   130  	}
   131  
   132  	// Eval info is different depending on what kind of resource this is
   133  	switch n.Config.Mode {
   134  	case addrs.ManagedResourceMode:
   135  		return n.managedResourceExecute(ctx)
   136  	case addrs.DataResourceMode:
   137  		return n.dataResourceExecute(ctx)
   138  	default:
   139  		panic(fmt.Errorf("unsupported resource mode %s", n.Config.Mode))
   140  	}
   141  }
   142  
   143  func (n *NodeApplyableResourceInstance) dataResourceExecute(ctx EvalContext) (diags tfdiags.Diagnostics) {
   144  	_, providerSchema, err := getProvider(ctx, n.ResolvedProvider)
   145  	diags = diags.Append(err)
   146  	if diags.HasErrors() {
   147  		return diags
   148  	}
   149  
   150  	change, err := n.readDiff(ctx, providerSchema)
   151  	diags = diags.Append(err)
   152  	if diags.HasErrors() {
   153  		return diags
   154  	}
   155  	// Stop early if we don't actually have a diff
   156  	if change == nil {
   157  		return diags
   158  	}
   159  
   160  	// In this particular call to applyDataSource we include our planned
   161  	// change, which signals that we expect this read to complete fully
   162  	// with no unknown values; it'll produce an error if not.
   163  	state, applyDiags := n.applyDataSource(ctx, change)
   164  	diags = diags.Append(applyDiags)
   165  	if diags.HasErrors() {
   166  		return diags
   167  	}
   168  
   169  	diags = diags.Append(n.writeResourceInstanceState(ctx, state, workingState))
   170  	if diags.HasErrors() {
   171  		return diags
   172  	}
   173  
   174  	diags = diags.Append(n.writeChange(ctx, nil, ""))
   175  
   176  	diags = diags.Append(updateStateHook(ctx))
   177  	return diags
   178  }
   179  
   180  func (n *NodeApplyableResourceInstance) managedResourceExecute(ctx EvalContext) (diags tfdiags.Diagnostics) {
   181  	// Declare a bunch of variables that are used for state during
   182  	// evaluation. Most of this are written to by-address below.
   183  	var state *states.ResourceInstanceObject
   184  	var createBeforeDestroyEnabled bool
   185  	var deposedKey states.DeposedKey
   186  
   187  	addr := n.ResourceInstanceAddr().Resource
   188  	_, providerSchema, err := getProvider(ctx, n.ResolvedProvider)
   189  	diags = diags.Append(err)
   190  	if diags.HasErrors() {
   191  		return diags
   192  	}
   193  
   194  	// Get the saved diff for apply
   195  	diffApply, err := n.readDiff(ctx, providerSchema)
   196  	diags = diags.Append(err)
   197  	if diags.HasErrors() {
   198  		return diags
   199  	}
   200  
   201  	// We don't want to do any destroys
   202  	// (these are handled by NodeDestroyResourceInstance instead)
   203  	if diffApply == nil || diffApply.Action == plans.Delete {
   204  		return diags
   205  	}
   206  
   207  	destroy := (diffApply.Action == plans.Delete || diffApply.Action.IsReplace())
   208  	// Get the stored action for CBD if we have a plan already
   209  	createBeforeDestroyEnabled = diffApply.Change.Action == plans.CreateThenDelete
   210  
   211  	if destroy && n.CreateBeforeDestroy() {
   212  		createBeforeDestroyEnabled = true
   213  	}
   214  
   215  	if createBeforeDestroyEnabled {
   216  		state := ctx.State()
   217  		if n.PreallocatedDeposedKey == states.NotDeposed {
   218  			deposedKey = state.DeposeResourceInstanceObject(n.Addr)
   219  		} else {
   220  			deposedKey = n.PreallocatedDeposedKey
   221  			state.DeposeResourceInstanceObjectForceKey(n.Addr, deposedKey)
   222  		}
   223  		log.Printf("[TRACE] managedResourceExecute: prior object for %s now deposed with key %s", n.Addr, deposedKey)
   224  	}
   225  
   226  	state, readDiags := n.readResourceInstanceState(ctx, n.ResourceInstanceAddr())
   227  	diags = diags.Append(readDiags)
   228  	if diags.HasErrors() {
   229  		return diags
   230  	}
   231  
   232  	// Get the saved diff
   233  	diff, err := n.readDiff(ctx, providerSchema)
   234  	diags = diags.Append(err)
   235  	if diags.HasErrors() {
   236  		return diags
   237  	}
   238  
   239  	// Make a new diff, in case we've learned new values in the state
   240  	// during apply which we can now incorporate.
   241  	diffApply, _, planDiags := n.plan(ctx, diff, state, false, n.forceReplace)
   242  	diags = diags.Append(planDiags)
   243  	if diags.HasErrors() {
   244  		return diags
   245  	}
   246  
   247  	// Compare the diffs
   248  	diags = diags.Append(n.checkPlannedChange(ctx, diff, diffApply, providerSchema))
   249  	if diags.HasErrors() {
   250  		return diags
   251  	}
   252  
   253  	state, readDiags = n.readResourceInstanceState(ctx, n.ResourceInstanceAddr())
   254  	diags = diags.Append(readDiags)
   255  	if diags.HasErrors() {
   256  		return diags
   257  	}
   258  
   259  	diffApply = reducePlan(addr, diffApply, false)
   260  	// reducePlan may have simplified our planned change
   261  	// into a NoOp if it only requires destroying, since destroying
   262  	// is handled by NodeDestroyResourceInstance.
   263  	if diffApply == nil || diffApply.Action == plans.NoOp {
   264  		return diags
   265  	}
   266  
   267  	diags = diags.Append(n.preApplyHook(ctx, diffApply))
   268  	if diags.HasErrors() {
   269  		return diags
   270  	}
   271  
   272  	state, applyDiags := n.apply(ctx, state, diffApply, n.Config, n.CreateBeforeDestroy())
   273  	diags = diags.Append(applyDiags)
   274  
   275  	// We clear the change out here so that future nodes don't see a change
   276  	// that is already complete.
   277  	err = n.writeChange(ctx, nil, "")
   278  	if err != nil {
   279  		return diags.Append(err)
   280  	}
   281  
   282  	state = maybeTainted(addr.Absolute(ctx.Path()), state, diffApply, diags.Err())
   283  
   284  	if state != nil {
   285  		// dependencies are always updated to match the configuration during apply
   286  		state.Dependencies = n.Dependencies
   287  	}
   288  	err = n.writeResourceInstanceState(ctx, state, workingState)
   289  	if err != nil {
   290  		return diags.Append(err)
   291  	}
   292  
   293  	// Run Provisioners
   294  	createNew := (diffApply.Action == plans.Create || diffApply.Action.IsReplace())
   295  	applyProvisionersDiags := n.evalApplyProvisioners(ctx, state, createNew, configs.ProvisionerWhenCreate)
   296  	// the provisioner errors count as port of the apply error, so we can bundle the diags
   297  	diags = diags.Append(applyProvisionersDiags)
   298  
   299  	state = maybeTainted(addr.Absolute(ctx.Path()), state, diffApply, diags.Err())
   300  
   301  	err = n.writeResourceInstanceState(ctx, state, workingState)
   302  	if err != nil {
   303  		return diags.Append(err)
   304  	}
   305  
   306  	if createBeforeDestroyEnabled && diags.HasErrors() {
   307  		if deposedKey == states.NotDeposed {
   308  			// This should never happen, and so it always indicates a bug.
   309  			// We should evaluate this node only if we've previously deposed
   310  			// an object as part of the same operation.
   311  			if diffApply != nil {
   312  				diags = diags.Append(tfdiags.Sourceless(
   313  					tfdiags.Error,
   314  					"Attempt to restore non-existent deposed object",
   315  					fmt.Sprintf(
   316  						"Terraform has encountered a bug where it would need to restore a deposed object for %s without knowing a deposed object key for that object. This occurred during a %s action. This is a bug in Terraform; please report it!",
   317  						addr, diffApply.Action,
   318  					),
   319  				))
   320  			} else {
   321  				diags = diags.Append(tfdiags.Sourceless(
   322  					tfdiags.Error,
   323  					"Attempt to restore non-existent deposed object",
   324  					fmt.Sprintf(
   325  						"Terraform has encountered a bug where it would need to restore a deposed object for %s without knowing a deposed object key for that object. This is a bug in Terraform; please report it!",
   326  						addr,
   327  					),
   328  				))
   329  			}
   330  		} else {
   331  			restored := ctx.State().MaybeRestoreResourceInstanceDeposed(addr.Absolute(ctx.Path()), deposedKey)
   332  			if restored {
   333  				log.Printf("[TRACE] managedResourceExecute: %s deposed object %s was restored as the current object", addr, deposedKey)
   334  			} else {
   335  				log.Printf("[TRACE] managedResourceExecute: %s deposed object %s remains deposed", addr, deposedKey)
   336  			}
   337  		}
   338  	}
   339  
   340  	diags = diags.Append(n.postApplyHook(ctx, state, diags.Err()))
   341  	diags = diags.Append(updateStateHook(ctx))
   342  	return diags
   343  }
   344  
   345  // checkPlannedChange produces errors if the _actual_ expected value is not
   346  // compatible with what was recorded in the plan.
   347  //
   348  // Errors here are most often indicative of a bug in the provider, so our error
   349  // messages will report with that in mind. It's also possible that there's a bug
   350  // in Terraform's Core's own "proposed new value" code in EvalDiff.
   351  func (n *NodeApplyableResourceInstance) checkPlannedChange(ctx EvalContext, plannedChange, actualChange *plans.ResourceInstanceChange, providerSchema *ProviderSchema) tfdiags.Diagnostics {
   352  	var diags tfdiags.Diagnostics
   353  	addr := n.ResourceInstanceAddr().Resource
   354  
   355  	schema, _ := providerSchema.SchemaForResourceAddr(addr.ContainingResource())
   356  	if schema == nil {
   357  		// Should be caught during validation, so we don't bother with a pretty error here
   358  		diags = diags.Append(fmt.Errorf("provider does not support %q", addr.Resource.Type))
   359  		return diags
   360  	}
   361  
   362  	absAddr := addr.Absolute(ctx.Path())
   363  
   364  	log.Printf("[TRACE] checkPlannedChange: Verifying that actual change (action %s) matches planned change (action %s)", actualChange.Action, plannedChange.Action)
   365  
   366  	if plannedChange.Action != actualChange.Action {
   367  		switch {
   368  		case plannedChange.Action == plans.Update && actualChange.Action == plans.NoOp:
   369  			// It's okay for an update to become a NoOp once we've filled in
   370  			// all of the unknown values, since the final values might actually
   371  			// match what was there before after all.
   372  			log.Printf("[DEBUG] After incorporating new values learned so far during apply, %s change has become NoOp", absAddr)
   373  
   374  		case (plannedChange.Action == plans.CreateThenDelete && actualChange.Action == plans.DeleteThenCreate) ||
   375  			(plannedChange.Action == plans.DeleteThenCreate && actualChange.Action == plans.CreateThenDelete):
   376  			// If the order of replacement changed, then that is a bug in terraform
   377  			diags = diags.Append(tfdiags.Sourceless(
   378  				tfdiags.Error,
   379  				"Terraform produced inconsistent final plan",
   380  				fmt.Sprintf(
   381  					"When expanding the plan for %s to include new values learned so far during apply, the planned action changed from %s to %s.\n\nThis is a bug in Terraform and should be reported.",
   382  					absAddr, plannedChange.Action, actualChange.Action,
   383  				),
   384  			))
   385  		default:
   386  			diags = diags.Append(tfdiags.Sourceless(
   387  				tfdiags.Error,
   388  				"Provider produced inconsistent final plan",
   389  				fmt.Sprintf(
   390  					"When expanding the plan for %s to include new values learned so far during apply, provider %q changed the planned action from %s to %s.\n\nThis is a bug in the provider, which should be reported in the provider's own issue tracker.",
   391  					absAddr, n.ResolvedProvider.Provider.String(),
   392  					plannedChange.Action, actualChange.Action,
   393  				),
   394  			))
   395  		}
   396  	}
   397  
   398  	errs := objchange.AssertObjectCompatible(schema, plannedChange.After, actualChange.After)
   399  	for _, err := range errs {
   400  		diags = diags.Append(tfdiags.Sourceless(
   401  			tfdiags.Error,
   402  			"Provider produced inconsistent final plan",
   403  			fmt.Sprintf(
   404  				"When expanding the plan for %s to include new values learned so far during apply, provider %q produced an invalid new value for %s.\n\nThis is a bug in the provider, which should be reported in the provider's own issue tracker.",
   405  				absAddr, n.ResolvedProvider.Provider.String(), tfdiags.FormatError(err),
   406  			),
   407  		))
   408  	}
   409  	return diags
   410  }
   411  
   412  // maybeTainted takes the resource addr, new value, planned change, and possible
   413  // error from an apply operation and return a new instance object marked as
   414  // tainted if it appears that a create operation has failed.
   415  func maybeTainted(addr addrs.AbsResourceInstance, state *states.ResourceInstanceObject, change *plans.ResourceInstanceChange, err error) *states.ResourceInstanceObject {
   416  	if state == nil || change == nil || err == nil {
   417  		return state
   418  	}
   419  	if state.Status == states.ObjectTainted {
   420  		log.Printf("[TRACE] maybeTainted: %s was already tainted, so nothing to do", addr)
   421  		return state
   422  	}
   423  	if change.Action == plans.Create {
   424  		// If there are errors during a _create_ then the object is
   425  		// in an undefined state, and so we'll mark it as tainted so
   426  		// we can try again on the next run.
   427  		//
   428  		// We don't do this for other change actions because errors
   429  		// during updates will often not change the remote object at all.
   430  		// If there _were_ changes prior to the error, it's the provider's
   431  		// responsibility to record the effect of those changes in the
   432  		// object value it returned.
   433  		log.Printf("[TRACE] maybeTainted: %s encountered an error during creation, so it is now marked as tainted", addr)
   434  		return state.AsTainted()
   435  	}
   436  	return state
   437  }