github.com/eliastor/durgaform@v0.0.0-20220816172711-d0ab2d17673e/internal/durgaform/node_resource_apply_instance.go (about)

     1  package durgaform
     2  
     3  import (
     4  	"fmt"
     5  	"log"
     6  
     7  	"github.com/eliastor/durgaform/internal/addrs"
     8  	"github.com/eliastor/durgaform/internal/configs"
     9  	"github.com/eliastor/durgaform/internal/plans"
    10  	"github.com/eliastor/durgaform/internal/plans/objchange"
    11  	"github.com/eliastor/durgaform/internal/states"
    12  	"github.com/eliastor/durgaform/internal/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/eliastor/durgaform/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 Durgaform'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  	if change.Action != plans.Read && change.Action != plans.NoOp {
   160  		diags = diags.Append(fmt.Errorf("nonsensical planned action %#v for %s; this is a bug in Durgaform", change.Action, n.Addr))
   161  	}
   162  
   163  	// In this particular call to applyDataSource we include our planned
   164  	// change, which signals that we expect this read to complete fully
   165  	// with no unknown values; it'll produce an error if not.
   166  	state, repeatData, applyDiags := n.applyDataSource(ctx, change)
   167  	diags = diags.Append(applyDiags)
   168  	if diags.HasErrors() {
   169  		return diags
   170  	}
   171  
   172  	if state != nil {
   173  		// If n.applyDataSource returned a nil state object with no accompanying
   174  		// errors then it determined that the given change doesn't require
   175  		// actually reading the data (e.g. because it was already read during
   176  		// the plan phase) and so we're only running through here to get the
   177  		// extra details like precondition/postcondition checks.
   178  		diags = diags.Append(n.writeResourceInstanceState(ctx, state, workingState))
   179  		if diags.HasErrors() {
   180  			return diags
   181  		}
   182  	}
   183  
   184  	diags = diags.Append(n.writeChange(ctx, nil, ""))
   185  
   186  	diags = diags.Append(updateStateHook(ctx))
   187  
   188  	// Post-conditions might block further progress. We intentionally do this
   189  	// _after_ writing the state/diff because we want to check against
   190  	// the result of the operation, and to fail on future operations
   191  	// until the user makes the condition succeed.
   192  	checkDiags := evalCheckRules(
   193  		addrs.ResourcePostcondition,
   194  		n.Config.Postconditions,
   195  		ctx, n.ResourceInstanceAddr(),
   196  		repeatData,
   197  		tfdiags.Error,
   198  	)
   199  	diags = diags.Append(checkDiags)
   200  
   201  	return diags
   202  }
   203  
   204  func (n *NodeApplyableResourceInstance) managedResourceExecute(ctx EvalContext) (diags tfdiags.Diagnostics) {
   205  	// Declare a bunch of variables that are used for state during
   206  	// evaluation. Most of this are written to by-address below.
   207  	var state *states.ResourceInstanceObject
   208  	var createBeforeDestroyEnabled bool
   209  	var deposedKey states.DeposedKey
   210  
   211  	addr := n.ResourceInstanceAddr().Resource
   212  	_, providerSchema, err := getProvider(ctx, n.ResolvedProvider)
   213  	diags = diags.Append(err)
   214  	if diags.HasErrors() {
   215  		return diags
   216  	}
   217  
   218  	// Get the saved diff for apply
   219  	diffApply, err := n.readDiff(ctx, providerSchema)
   220  	diags = diags.Append(err)
   221  	if diags.HasErrors() {
   222  		return diags
   223  	}
   224  
   225  	// We don't want to do any destroys
   226  	// (these are handled by NodeDestroyResourceInstance instead)
   227  	if diffApply == nil || diffApply.Action == plans.Delete {
   228  		return diags
   229  	}
   230  	if diffApply.Action == plans.Read {
   231  		diags = diags.Append(fmt.Errorf("nonsensical planned action %#v for %s; this is a bug in Durgaform", diffApply.Action, n.Addr))
   232  	}
   233  
   234  	destroy := (diffApply.Action == plans.Delete || diffApply.Action.IsReplace())
   235  	// Get the stored action for CBD if we have a plan already
   236  	createBeforeDestroyEnabled = diffApply.Change.Action == plans.CreateThenDelete
   237  
   238  	if destroy && n.CreateBeforeDestroy() {
   239  		createBeforeDestroyEnabled = true
   240  	}
   241  
   242  	if createBeforeDestroyEnabled {
   243  		state := ctx.State()
   244  		if n.PreallocatedDeposedKey == states.NotDeposed {
   245  			deposedKey = state.DeposeResourceInstanceObject(n.Addr)
   246  		} else {
   247  			deposedKey = n.PreallocatedDeposedKey
   248  			state.DeposeResourceInstanceObjectForceKey(n.Addr, deposedKey)
   249  		}
   250  		log.Printf("[TRACE] managedResourceExecute: prior object for %s now deposed with key %s", n.Addr, deposedKey)
   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  	// Get the saved diff
   260  	diff, err := n.readDiff(ctx, providerSchema)
   261  	diags = diags.Append(err)
   262  	if diags.HasErrors() {
   263  		return diags
   264  	}
   265  
   266  	// Make a new diff, in case we've learned new values in the state
   267  	// during apply which we can now incorporate.
   268  	diffApply, _, _, planDiags := n.plan(ctx, diff, state, false, n.forceReplace)
   269  	diags = diags.Append(planDiags)
   270  	if diags.HasErrors() {
   271  		return diags
   272  	}
   273  
   274  	// Compare the diffs
   275  	diags = diags.Append(n.checkPlannedChange(ctx, diff, diffApply, providerSchema))
   276  	if diags.HasErrors() {
   277  		return diags
   278  	}
   279  
   280  	diffApply = reducePlan(addr, diffApply, false)
   281  	// reducePlan may have simplified our planned change
   282  	// into a NoOp if it only requires destroying, since destroying
   283  	// is handled by NodeDestroyResourceInstance. If so, we'll
   284  	// still run through most of the logic here because we do still
   285  	// need to deal with other book-keeping such as marking the
   286  	// change as "complete", and running the author's postconditions.
   287  
   288  	diags = diags.Append(n.preApplyHook(ctx, diffApply))
   289  	if diags.HasErrors() {
   290  		return diags
   291  	}
   292  
   293  	state, repeatData, applyDiags := n.apply(ctx, state, diffApply, n.Config, n.CreateBeforeDestroy())
   294  	diags = diags.Append(applyDiags)
   295  
   296  	// We clear the change out here so that future nodes don't see a change
   297  	// that is already complete.
   298  	err = n.writeChange(ctx, nil, "")
   299  	if err != nil {
   300  		return diags.Append(err)
   301  	}
   302  
   303  	state = maybeTainted(addr.Absolute(ctx.Path()), state, diffApply, diags.Err())
   304  
   305  	if state != nil {
   306  		// dependencies are always updated to match the configuration during apply
   307  		state.Dependencies = n.Dependencies
   308  	}
   309  	err = n.writeResourceInstanceState(ctx, state, workingState)
   310  	if err != nil {
   311  		return diags.Append(err)
   312  	}
   313  
   314  	// Run Provisioners
   315  	createNew := (diffApply.Action == plans.Create || diffApply.Action.IsReplace())
   316  	applyProvisionersDiags := n.evalApplyProvisioners(ctx, state, createNew, configs.ProvisionerWhenCreate)
   317  	// the provisioner errors count as port of the apply error, so we can bundle the diags
   318  	diags = diags.Append(applyProvisionersDiags)
   319  
   320  	state = maybeTainted(addr.Absolute(ctx.Path()), state, diffApply, diags.Err())
   321  
   322  	err = n.writeResourceInstanceState(ctx, state, workingState)
   323  	if err != nil {
   324  		return diags.Append(err)
   325  	}
   326  
   327  	if createBeforeDestroyEnabled && diags.HasErrors() {
   328  		if deposedKey == states.NotDeposed {
   329  			// This should never happen, and so it always indicates a bug.
   330  			// We should evaluate this node only if we've previously deposed
   331  			// an object as part of the same operation.
   332  			if diffApply != nil {
   333  				diags = diags.Append(tfdiags.Sourceless(
   334  					tfdiags.Error,
   335  					"Attempt to restore non-existent deposed object",
   336  					fmt.Sprintf(
   337  						"Durgaform 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!",
   338  						addr, diffApply.Action,
   339  					),
   340  				))
   341  			} else {
   342  				diags = diags.Append(tfdiags.Sourceless(
   343  					tfdiags.Error,
   344  					"Attempt to restore non-existent deposed object",
   345  					fmt.Sprintf(
   346  						"Durgaform 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!",
   347  						addr,
   348  					),
   349  				))
   350  			}
   351  		} else {
   352  			restored := ctx.State().MaybeRestoreResourceInstanceDeposed(addr.Absolute(ctx.Path()), deposedKey)
   353  			if restored {
   354  				log.Printf("[TRACE] managedResourceExecute: %s deposed object %s was restored as the current object", addr, deposedKey)
   355  			} else {
   356  				log.Printf("[TRACE] managedResourceExecute: %s deposed object %s remains deposed", addr, deposedKey)
   357  			}
   358  		}
   359  	}
   360  
   361  	diags = diags.Append(n.postApplyHook(ctx, state, diags.Err()))
   362  	diags = diags.Append(updateStateHook(ctx))
   363  
   364  	// Post-conditions might block further progress. We intentionally do this
   365  	// _after_ writing the state because we want to check against
   366  	// the result of the operation, and to fail on future operations
   367  	// until the user makes the condition succeed.
   368  	checkDiags := evalCheckRules(
   369  		addrs.ResourcePostcondition,
   370  		n.Config.Postconditions,
   371  		ctx, n.ResourceInstanceAddr(), repeatData,
   372  		tfdiags.Error,
   373  	)
   374  	diags = diags.Append(checkDiags)
   375  
   376  	return diags
   377  }
   378  
   379  // checkPlannedChange produces errors if the _actual_ expected value is not
   380  // compatible with what was recorded in the plan.
   381  //
   382  // Errors here are most often indicative of a bug in the provider, so our error
   383  // messages will report with that in mind. It's also possible that there's a bug
   384  // in Durgaform's Core's own "proposed new value" code in EvalDiff.
   385  func (n *NodeApplyableResourceInstance) checkPlannedChange(ctx EvalContext, plannedChange, actualChange *plans.ResourceInstanceChange, providerSchema *ProviderSchema) tfdiags.Diagnostics {
   386  	var diags tfdiags.Diagnostics
   387  	addr := n.ResourceInstanceAddr().Resource
   388  
   389  	schema, _ := providerSchema.SchemaForResourceAddr(addr.ContainingResource())
   390  	if schema == nil {
   391  		// Should be caught during validation, so we don't bother with a pretty error here
   392  		diags = diags.Append(fmt.Errorf("provider does not support %q", addr.Resource.Type))
   393  		return diags
   394  	}
   395  
   396  	absAddr := addr.Absolute(ctx.Path())
   397  
   398  	log.Printf("[TRACE] checkPlannedChange: Verifying that actual change (action %s) matches planned change (action %s)", actualChange.Action, plannedChange.Action)
   399  
   400  	if plannedChange.Action != actualChange.Action {
   401  		switch {
   402  		case plannedChange.Action == plans.Update && actualChange.Action == plans.NoOp:
   403  			// It's okay for an update to become a NoOp once we've filled in
   404  			// all of the unknown values, since the final values might actually
   405  			// match what was there before after all.
   406  			log.Printf("[DEBUG] After incorporating new values learned so far during apply, %s change has become NoOp", absAddr)
   407  
   408  		case (plannedChange.Action == plans.CreateThenDelete && actualChange.Action == plans.DeleteThenCreate) ||
   409  			(plannedChange.Action == plans.DeleteThenCreate && actualChange.Action == plans.CreateThenDelete):
   410  			// If the order of replacement changed, then that is a bug in durgaform
   411  			diags = diags.Append(tfdiags.Sourceless(
   412  				tfdiags.Error,
   413  				"Durgaform produced inconsistent final plan",
   414  				fmt.Sprintf(
   415  					"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 Durgaform and should be reported.",
   416  					absAddr, plannedChange.Action, actualChange.Action,
   417  				),
   418  			))
   419  		default:
   420  			diags = diags.Append(tfdiags.Sourceless(
   421  				tfdiags.Error,
   422  				"Provider produced inconsistent final plan",
   423  				fmt.Sprintf(
   424  					"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.",
   425  					absAddr, n.ResolvedProvider.Provider.String(),
   426  					plannedChange.Action, actualChange.Action,
   427  				),
   428  			))
   429  		}
   430  	}
   431  
   432  	errs := objchange.AssertObjectCompatible(schema, plannedChange.After, actualChange.After)
   433  	for _, err := range errs {
   434  		diags = diags.Append(tfdiags.Sourceless(
   435  			tfdiags.Error,
   436  			"Provider produced inconsistent final plan",
   437  			fmt.Sprintf(
   438  				"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.",
   439  				absAddr, n.ResolvedProvider.Provider.String(), tfdiags.FormatError(err),
   440  			),
   441  		))
   442  	}
   443  	return diags
   444  }
   445  
   446  // maybeTainted takes the resource addr, new value, planned change, and possible
   447  // error from an apply operation and return a new instance object marked as
   448  // tainted if it appears that a create operation has failed.
   449  func maybeTainted(addr addrs.AbsResourceInstance, state *states.ResourceInstanceObject, change *plans.ResourceInstanceChange, err error) *states.ResourceInstanceObject {
   450  	if state == nil || change == nil || err == nil {
   451  		return state
   452  	}
   453  	if state.Status == states.ObjectTainted {
   454  		log.Printf("[TRACE] maybeTainted: %s was already tainted, so nothing to do", addr)
   455  		return state
   456  	}
   457  	if change.Action == plans.Create {
   458  		// If there are errors during a _create_ then the object is
   459  		// in an undefined state, and so we'll mark it as tainted so
   460  		// we can try again on the next run.
   461  		//
   462  		// We don't do this for other change actions because errors
   463  		// during updates will often not change the remote object at all.
   464  		// If there _were_ changes prior to the error, it's the provider's
   465  		// responsibility to record the effect of those changes in the
   466  		// object value it returned.
   467  		log.Printf("[TRACE] maybeTainted: %s encountered an error during creation, so it is now marked as tainted", addr)
   468  		return state.AsTainted()
   469  	}
   470  	return state
   471  }