github.com/opentofu/opentofu@v1.7.1/internal/tofu/context_apply.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/zclconf/go-cty/cty"
    13  
    14  	"github.com/opentofu/opentofu/internal/addrs"
    15  	"github.com/opentofu/opentofu/internal/configs"
    16  	"github.com/opentofu/opentofu/internal/plans"
    17  	"github.com/opentofu/opentofu/internal/states"
    18  	"github.com/opentofu/opentofu/internal/tfdiags"
    19  )
    20  
    21  // Apply performs the actions described by the given Plan object and returns
    22  // the resulting updated state.
    23  //
    24  // The given configuration *must* be the same configuration that was passed
    25  // earlier to Context.Plan in order to create this plan.
    26  //
    27  // Even if the returned diagnostics contains errors, Apply always returns the
    28  // resulting state which is likely to have been partially-updated.
    29  func (c *Context) Apply(plan *plans.Plan, config *configs.Config) (*states.State, tfdiags.Diagnostics) {
    30  	defer c.acquireRun("apply")()
    31  
    32  	log.Printf("[DEBUG] Building and walking apply graph for %s plan", plan.UIMode)
    33  
    34  	if plan.Errored {
    35  		var diags tfdiags.Diagnostics
    36  		diags = diags.Append(tfdiags.Sourceless(
    37  			tfdiags.Error,
    38  			"Cannot apply failed plan",
    39  			`The given plan is incomplete due to errors during planning, and so it cannot be applied.`,
    40  		))
    41  		return nil, diags
    42  	}
    43  
    44  	for _, rc := range plan.Changes.Resources {
    45  		// Import is a no-op change during an apply (all the real action happens during the plan) but we'd
    46  		// like to show some helpful output that mirrors the way we show other changes.
    47  		if rc.Importing != nil {
    48  			for _, h := range c.hooks {
    49  				// In future, we may need to call PostApplyImport separately elsewhere in the apply
    50  				// operation. For now, though, we'll call Pre and Post hooks together.
    51  				h.PreApplyImport(rc.Addr, *rc.Importing)
    52  				h.PostApplyImport(rc.Addr, *rc.Importing)
    53  			}
    54  		}
    55  	}
    56  
    57  	graph, operation, diags := c.applyGraph(plan, config, true)
    58  	if diags.HasErrors() {
    59  		return nil, diags
    60  	}
    61  
    62  	workingState := plan.PriorState.DeepCopy()
    63  	walker, walkDiags := c.walk(graph, operation, &graphWalkOpts{
    64  		Config:     config,
    65  		InputState: workingState,
    66  		Changes:    plan.Changes,
    67  
    68  		// We need to propagate the check results from the plan phase,
    69  		// because that will tell us which checkable objects we're expecting
    70  		// to see updated results from during the apply step.
    71  		PlanTimeCheckResults: plan.Checks,
    72  
    73  		// We also want to propagate the timestamp from the plan file.
    74  		PlanTimeTimestamp: plan.Timestamp,
    75  	})
    76  	diags = diags.Append(walker.NonFatalDiagnostics)
    77  	diags = diags.Append(walkDiags)
    78  
    79  	// After the walk is finished, we capture a simplified snapshot of the
    80  	// check result data as part of the new state.
    81  	walker.State.RecordCheckResults(walker.Checks)
    82  
    83  	newState := walker.State.Close()
    84  	if plan.UIMode == plans.DestroyMode && !diags.HasErrors() {
    85  		// NOTE: This is a vestigial violation of the rule that we mustn't
    86  		// use plan.UIMode to affect apply-time behavior.
    87  		// We ideally ought to just call newState.PruneResourceHusks
    88  		// unconditionally here, but we historically didn't and haven't yet
    89  		// verified that it'd be safe to do so.
    90  		newState.PruneResourceHusks()
    91  	}
    92  
    93  	if len(plan.TargetAddrs) > 0 {
    94  		diags = diags.Append(tfdiags.Sourceless(
    95  			tfdiags.Warning,
    96  			"Applied changes may be incomplete",
    97  			`The plan was created with the -target option in effect, so some changes requested in the configuration may have been ignored and the output values may not be fully updated. Run the following command to verify that no other changes are pending:
    98      tofu plan
    99  	
   100  Note that the -target option is not suitable for routine use, and is provided only for exceptional situations such as recovering from errors or mistakes, or when OpenTofu specifically suggests to use it as part of an error message.`,
   101  		))
   102  	}
   103  
   104  	// FIXME: we cannot check for an empty plan for refresh-only, because root
   105  	// outputs are always stored as changes. The final condition of the state
   106  	// also depends on some cleanup which happens during the apply walk. It
   107  	// would probably make more sense if applying a refresh-only plan were
   108  	// simply just returning the planned state and checks, but some extra
   109  	// cleanup is going to be needed to make the plan state match what apply
   110  	// would do. For now we can copy the checks over which were overwritten
   111  	// during the apply walk.
   112  	// Despite the intent of UIMode, it must still be used for apply-time
   113  	// differences in destroy plans too, so we can make use of that here as
   114  	// well.
   115  	if plan.UIMode == plans.RefreshOnlyMode {
   116  		newState.CheckResults = plan.Checks.DeepCopy()
   117  	}
   118  
   119  	return newState, diags
   120  }
   121  
   122  func (c *Context) applyGraph(plan *plans.Plan, config *configs.Config, validate bool) (*Graph, walkOperation, tfdiags.Diagnostics) {
   123  	var diags tfdiags.Diagnostics
   124  
   125  	variables := InputValues{}
   126  	for name, dyVal := range plan.VariableValues {
   127  		val, err := dyVal.Decode(cty.DynamicPseudoType)
   128  		if err != nil {
   129  			diags = diags.Append(tfdiags.Sourceless(
   130  				tfdiags.Error,
   131  				"Invalid variable value in plan",
   132  				fmt.Sprintf("Invalid value for variable %q recorded in plan file: %s.", name, err),
   133  			))
   134  			continue
   135  		}
   136  
   137  		variables[name] = &InputValue{
   138  			Value:      val,
   139  			SourceType: ValueFromPlan,
   140  		}
   141  	}
   142  	if diags.HasErrors() {
   143  		return nil, walkApply, diags
   144  	}
   145  
   146  	// The plan.VariableValues field only records variables that were actually
   147  	// set by the caller in the PlanOpts, so we may need to provide
   148  	// placeholders for any other variables that the user didn't set, in
   149  	// which case OpenTofu will once again use the default value from the
   150  	// configuration when we visit these variables during the graph walk.
   151  	for name := range config.Module.Variables {
   152  		if _, ok := variables[name]; ok {
   153  			continue
   154  		}
   155  		variables[name] = &InputValue{
   156  			Value:      cty.NilVal,
   157  			SourceType: ValueFromPlan,
   158  		}
   159  	}
   160  
   161  	operation := walkApply
   162  	if plan.UIMode == plans.DestroyMode {
   163  		// FIXME: Due to differences in how objects must be handled in the
   164  		// graph and evaluated during a complete destroy, we must continue to
   165  		// use plans.DestroyMode to switch on this behavior. If all objects
   166  		// which require special destroy handling can be tracked in the plan,
   167  		// then this switch will no longer be needed and we can remove the
   168  		// walkDestroy operation mode.
   169  		// TODO: Audit that and remove walkDestroy as an operation mode.
   170  		operation = walkDestroy
   171  	}
   172  
   173  	graph, moreDiags := (&ApplyGraphBuilder{
   174  		Config:             config,
   175  		Changes:            plan.Changes,
   176  		State:              plan.PriorState,
   177  		RootVariableValues: variables,
   178  		Plugins:            c.plugins,
   179  		Targets:            plan.TargetAddrs,
   180  		ForceReplace:       plan.ForceReplaceAddrs,
   181  		Operation:          operation,
   182  		ExternalReferences: plan.ExternalReferences,
   183  	}).Build(addrs.RootModuleInstance)
   184  	diags = diags.Append(moreDiags)
   185  	if moreDiags.HasErrors() {
   186  		return nil, walkApply, diags
   187  	}
   188  
   189  	return graph, operation, diags
   190  }
   191  
   192  // ApplyGraphForUI is a last vestage of graphs in the public interface of
   193  // Context (as opposed to graphs as an implementation detail) intended only for
   194  // use by the "tofu graph" command when asked to render an apply-time
   195  // graph.
   196  //
   197  // The result of this is intended only for rendering ot the user as a dot
   198  // graph, and so may change in future in order to make the result more useful
   199  // in that context, even if drifts away from the physical graph that OpenTofu
   200  // Core currently uses as an implementation detail of planning.
   201  func (c *Context) ApplyGraphForUI(plan *plans.Plan, config *configs.Config) (*Graph, tfdiags.Diagnostics) {
   202  	// For now though, this really is just the internal graph, confusing
   203  	// implementation details and all.
   204  
   205  	var diags tfdiags.Diagnostics
   206  
   207  	graph, _, moreDiags := c.applyGraph(plan, config, false)
   208  	diags = diags.Append(moreDiags)
   209  	return graph, diags
   210  }