github.com/jaredpalmer/terraform@v1.1.0-alpha20210908.0.20210911170307-88705c943a03/internal/terraform/context_plan.go (about)

     1  package terraform
     2  
     3  import (
     4  	"fmt"
     5  	"log"
     6  
     7  	"github.com/zclconf/go-cty/cty"
     8  
     9  	"github.com/hashicorp/terraform/internal/addrs"
    10  	"github.com/hashicorp/terraform/internal/configs"
    11  	"github.com/hashicorp/terraform/internal/instances"
    12  	"github.com/hashicorp/terraform/internal/plans"
    13  	"github.com/hashicorp/terraform/internal/refactoring"
    14  	"github.com/hashicorp/terraform/internal/states"
    15  	"github.com/hashicorp/terraform/internal/tfdiags"
    16  )
    17  
    18  // PlanOpts are the various options that affect the details of how Terraform
    19  // will build a plan.
    20  type PlanOpts struct {
    21  	Mode         plans.Mode
    22  	SkipRefresh  bool
    23  	SetVariables InputValues
    24  	Targets      []addrs.Targetable
    25  	ForceReplace []addrs.AbsResourceInstance
    26  }
    27  
    28  // Plan generates an execution plan for the given context, and returns the
    29  // refreshed state.
    30  //
    31  // The execution plan encapsulates the context and can be stored
    32  // in order to reinstantiate a context later for Apply.
    33  //
    34  // Plan also updates the diff of this context to be the diff generated
    35  // by the plan, so Apply can be called after.
    36  func (c *Context) Plan(config *configs.Config, prevRunState *states.State, opts *PlanOpts) (*plans.Plan, tfdiags.Diagnostics) {
    37  	defer c.acquireRun("plan")()
    38  	var diags tfdiags.Diagnostics
    39  
    40  	// Save the downstream functions from needing to deal with these broken situations.
    41  	// No real callers should rely on these, but we have a bunch of old and
    42  	// sloppy tests that don't always populate arguments properly.
    43  	if config == nil {
    44  		config = configs.NewEmptyConfig()
    45  	}
    46  	if prevRunState == nil {
    47  		prevRunState = states.NewState()
    48  	}
    49  	if opts == nil {
    50  		opts = &PlanOpts{
    51  			Mode: plans.NormalMode,
    52  		}
    53  	}
    54  
    55  	moreDiags := CheckCoreVersionRequirements(config)
    56  	diags = diags.Append(moreDiags)
    57  	// If version constraints are not met then we'll bail early since otherwise
    58  	// we're likely to just see a bunch of other errors related to
    59  	// incompatibilities, which could be overwhelming for the user.
    60  	if diags.HasErrors() {
    61  		return nil, diags
    62  	}
    63  
    64  	switch opts.Mode {
    65  	case plans.NormalMode, plans.DestroyMode:
    66  		// OK
    67  	case plans.RefreshOnlyMode:
    68  		if opts.SkipRefresh {
    69  			// The CLI layer (and other similar callers) should prevent this
    70  			// combination of options.
    71  			diags = diags.Append(tfdiags.Sourceless(
    72  				tfdiags.Error,
    73  				"Incompatible plan options",
    74  				"Cannot skip refreshing in refresh-only mode. This is a bug in Terraform.",
    75  			))
    76  			return nil, diags
    77  		}
    78  	default:
    79  		// The CLI layer (and other similar callers) should not try to
    80  		// create a context for a mode that Terraform Core doesn't support.
    81  		diags = diags.Append(tfdiags.Sourceless(
    82  			tfdiags.Error,
    83  			"Unsupported plan mode",
    84  			fmt.Sprintf("Terraform Core doesn't know how to handle plan mode %s. This is a bug in Terraform.", opts.Mode),
    85  		))
    86  		return nil, diags
    87  	}
    88  	if len(opts.ForceReplace) > 0 && opts.Mode != plans.NormalMode {
    89  		// The other modes don't generate no-op or update actions that we might
    90  		// upgrade to be "replace", so doesn't make sense to combine those.
    91  		diags = diags.Append(tfdiags.Sourceless(
    92  			tfdiags.Error,
    93  			"Unsupported plan mode",
    94  			"Forcing resource instance replacement (with -replace=...) is allowed only in normal planning mode.",
    95  		))
    96  		return nil, diags
    97  	}
    98  
    99  	variables := mergeDefaultInputVariableValues(opts.SetVariables, config.Module.Variables)
   100  
   101  	// By the time we get here, we should have values defined for all of
   102  	// the root module variables, even if some of them are "unknown". It's the
   103  	// caller's responsibility to have already handled the decoding of these
   104  	// from the various ways the CLI allows them to be set and to produce
   105  	// user-friendly error messages if they are not all present, and so
   106  	// the error message from checkInputVariables should never be seen and
   107  	// includes language asking the user to report a bug.
   108  	varDiags := checkInputVariables(config.Module.Variables, variables)
   109  	diags = diags.Append(varDiags)
   110  
   111  	if len(opts.Targets) > 0 {
   112  		diags = diags.Append(tfdiags.Sourceless(
   113  			tfdiags.Warning,
   114  			"Resource targeting is in effect",
   115  			`You are creating a plan with the -target option, which means that the result of this plan may not represent all of the changes requested by the current configuration.
   116  		
   117  The -target option is not for routine use, and is provided only for exceptional situations such as recovering from errors or mistakes, or when Terraform specifically suggests to use it as part of an error message.`,
   118  		))
   119  	}
   120  
   121  	var plan *plans.Plan
   122  	var planDiags tfdiags.Diagnostics
   123  	switch opts.Mode {
   124  	case plans.NormalMode:
   125  		plan, planDiags = c.plan(config, prevRunState, variables, opts)
   126  	case plans.DestroyMode:
   127  		plan, planDiags = c.destroyPlan(config, prevRunState, variables, opts)
   128  	case plans.RefreshOnlyMode:
   129  		plan, planDiags = c.refreshOnlyPlan(config, prevRunState, variables, opts)
   130  	default:
   131  		panic(fmt.Sprintf("unsupported plan mode %s", opts.Mode))
   132  	}
   133  	diags = diags.Append(planDiags)
   134  	if diags.HasErrors() {
   135  		return nil, diags
   136  	}
   137  
   138  	// convert the variables into the format expected for the plan
   139  	varVals := make(map[string]plans.DynamicValue, len(variables))
   140  	for k, iv := range variables {
   141  		// We use cty.DynamicPseudoType here so that we'll save both the
   142  		// value _and_ its dynamic type in the plan, so we can recover
   143  		// exactly the same value later.
   144  		dv, err := plans.NewDynamicValue(iv.Value, cty.DynamicPseudoType)
   145  		if err != nil {
   146  			diags = diags.Append(tfdiags.Sourceless(
   147  				tfdiags.Error,
   148  				"Failed to prepare variable value for plan",
   149  				fmt.Sprintf("The value for variable %q could not be serialized to store in the plan: %s.", k, err),
   150  			))
   151  			continue
   152  		}
   153  		varVals[k] = dv
   154  	}
   155  
   156  	// insert the run-specific data from the context into the plan; variables,
   157  	// targets and provider SHAs.
   158  	if plan != nil {
   159  		plan.VariableValues = varVals
   160  		plan.TargetAddrs = opts.Targets
   161  		plan.ProviderSHA256s = c.providerSHA256s
   162  	} else if !diags.HasErrors() {
   163  		panic("nil plan but no errors")
   164  	}
   165  
   166  	return plan, diags
   167  }
   168  
   169  var DefaultPlanOpts = &PlanOpts{
   170  	Mode: plans.NormalMode,
   171  }
   172  
   173  func (c *Context) plan(config *configs.Config, prevRunState *states.State, rootVariables InputValues, opts *PlanOpts) (*plans.Plan, tfdiags.Diagnostics) {
   174  	var diags tfdiags.Diagnostics
   175  
   176  	if opts.Mode != plans.NormalMode {
   177  		panic(fmt.Sprintf("called Context.plan with %s", opts.Mode))
   178  	}
   179  
   180  	plan, walkDiags := c.planWalk(config, prevRunState, rootVariables, opts)
   181  	diags = diags.Append(walkDiags)
   182  	if diags.HasErrors() {
   183  		return nil, diags
   184  	}
   185  
   186  	// The refreshed state ends up with some placeholder objects in it for
   187  	// objects pending creation. We only really care about those being in
   188  	// the working state, since that's what we're going to use when applying,
   189  	// so we'll prune them all here.
   190  	plan.PriorState.SyncWrapper().RemovePlannedResourceInstanceObjects()
   191  
   192  	return plan, diags
   193  }
   194  
   195  func (c *Context) refreshOnlyPlan(config *configs.Config, prevRunState *states.State, rootVariables InputValues, opts *PlanOpts) (*plans.Plan, tfdiags.Diagnostics) {
   196  	var diags tfdiags.Diagnostics
   197  
   198  	if opts.Mode != plans.RefreshOnlyMode {
   199  		panic(fmt.Sprintf("called Context.refreshOnlyPlan with %s", opts.Mode))
   200  	}
   201  
   202  	plan, walkDiags := c.planWalk(config, prevRunState, rootVariables, opts)
   203  	diags = diags.Append(walkDiags)
   204  	if diags.HasErrors() {
   205  		return nil, diags
   206  	}
   207  
   208  	// If the graph builder and graph nodes correctly obeyed our directive
   209  	// to refresh only, the set of resource changes should always be empty.
   210  	// We'll safety-check that here so we can return a clear message about it,
   211  	// rather than probably just generating confusing output at the UI layer.
   212  	if len(plan.Changes.Resources) != 0 {
   213  		// Some extra context in the logs in case the user reports this message
   214  		// as a bug, as a starting point for debugging.
   215  		for _, rc := range plan.Changes.Resources {
   216  			if depKey := rc.DeposedKey; depKey == states.NotDeposed {
   217  				log.Printf("[DEBUG] Refresh-only plan includes %s change for %s", rc.Action, rc.Addr)
   218  			} else {
   219  				log.Printf("[DEBUG] Refresh-only plan includes %s change for %s deposed object %s", rc.Action, rc.Addr, depKey)
   220  			}
   221  		}
   222  		diags = diags.Append(tfdiags.Sourceless(
   223  			tfdiags.Error,
   224  			"Invalid refresh-only plan",
   225  			"Terraform generated planned resource changes in a refresh-only plan. This is a bug in Terraform.",
   226  		))
   227  	}
   228  
   229  	// Prune out any placeholder objects we put in the state to represent
   230  	// objects that would need to be created.
   231  	plan.PriorState.SyncWrapper().RemovePlannedResourceInstanceObjects()
   232  
   233  	return plan, diags
   234  }
   235  
   236  func (c *Context) destroyPlan(config *configs.Config, prevRunState *states.State, rootVariables InputValues, opts *PlanOpts) (*plans.Plan, tfdiags.Diagnostics) {
   237  	var diags tfdiags.Diagnostics
   238  	pendingPlan := &plans.Plan{}
   239  
   240  	if opts.Mode != plans.DestroyMode {
   241  		panic(fmt.Sprintf("called Context.destroyPlan with %s", opts.Mode))
   242  	}
   243  
   244  	priorState := prevRunState
   245  
   246  	// A destroy plan starts by running Refresh to read any pending data
   247  	// sources, and remove missing managed resources. This is required because
   248  	// a "destroy plan" is only creating delete changes, and is essentially a
   249  	// local operation.
   250  	//
   251  	// NOTE: if skipRefresh _is_ set then we'll rely on the destroy-plan walk
   252  	// below to upgrade the prevRunState and priorState both to the latest
   253  	// resource type schemas, so NodePlanDestroyableResourceInstance.Execute
   254  	// must coordinate with this by taking that action only when c.skipRefresh
   255  	// _is_ set. This coupling between the two is unfortunate but necessary
   256  	// to work within our current structure.
   257  	if !opts.SkipRefresh {
   258  		log.Printf("[TRACE] Context.destroyPlan: calling Context.plan to get the effect of refreshing the prior state")
   259  		normalOpts := *opts
   260  		normalOpts.Mode = plans.NormalMode
   261  		refreshPlan, refreshDiags := c.plan(config, prevRunState, rootVariables, &normalOpts)
   262  		diags = diags.Append(refreshDiags)
   263  		if diags.HasErrors() {
   264  			return nil, diags
   265  		}
   266  
   267  		// insert the refreshed state into the destroy plan result, and ignore
   268  		// the changes recorded from the refresh.
   269  		pendingPlan.PriorState = refreshPlan.PriorState.DeepCopy()
   270  		pendingPlan.PrevRunState = refreshPlan.PrevRunState.DeepCopy()
   271  		log.Printf("[TRACE] Context.destroyPlan: now _really_ creating a destroy plan")
   272  
   273  		// We'll use the refreshed state -- which is the  "prior state" from
   274  		// the perspective of this "pending plan" -- as the starting state
   275  		// for our destroy-plan walk, so it can take into account if we
   276  		// detected during refreshing that anything was already deleted outside
   277  		// of Terraform.
   278  		priorState = pendingPlan.PriorState
   279  	}
   280  
   281  	destroyPlan, walkDiags := c.planWalk(config, priorState, rootVariables, opts)
   282  	diags = diags.Append(walkDiags)
   283  	if walkDiags.HasErrors() {
   284  		return nil, diags
   285  	}
   286  
   287  	if !opts.SkipRefresh {
   288  		// If we didn't skip refreshing then we want the previous run state
   289  		// prior state to be the one we originally fed into the c.plan call
   290  		// above, not the refreshed version we used for the destroy walk.
   291  		destroyPlan.PrevRunState = pendingPlan.PrevRunState
   292  	}
   293  
   294  	return destroyPlan, diags
   295  }
   296  
   297  func (c *Context) prePlanFindAndApplyMoves(config *configs.Config, prevRunState *states.State, targets []addrs.Targetable) ([]refactoring.MoveStatement, map[addrs.UniqueKey]refactoring.MoveResult) {
   298  	moveStmts := refactoring.FindMoveStatements(config)
   299  	moveResults := refactoring.ApplyMoves(moveStmts, prevRunState)
   300  	if len(targets) > 0 {
   301  		for _, result := range moveResults {
   302  			matchesTarget := false
   303  			for _, targetAddr := range targets {
   304  				if targetAddr.TargetContains(result.From) {
   305  					matchesTarget = true
   306  					break
   307  				}
   308  			}
   309  			//lint:ignore SA9003 TODO
   310  			if !matchesTarget {
   311  				// TODO: Return an error stating that a targeted plan is
   312  				// only valid if it includes this address that was moved.
   313  			}
   314  		}
   315  	}
   316  	return moveStmts, moveResults
   317  }
   318  
   319  func (c *Context) postPlanValidateMoves(config *configs.Config, stmts []refactoring.MoveStatement, allInsts instances.Set) tfdiags.Diagnostics {
   320  	return refactoring.ValidateMoves(stmts, config, allInsts)
   321  }
   322  
   323  func (c *Context) planWalk(config *configs.Config, prevRunState *states.State, rootVariables InputValues, opts *PlanOpts) (*plans.Plan, tfdiags.Diagnostics) {
   324  	var diags tfdiags.Diagnostics
   325  	log.Printf("[DEBUG] Building and walking plan graph for %s", opts.Mode)
   326  
   327  	prevRunState = prevRunState.DeepCopy() // don't modify the caller's object when we process the moves
   328  	moveStmts, moveResults := c.prePlanFindAndApplyMoves(config, prevRunState, opts.Targets)
   329  
   330  	graph, walkOp, moreDiags := c.planGraph(config, prevRunState, opts, true)
   331  	diags = diags.Append(moreDiags)
   332  	if diags.HasErrors() {
   333  		return nil, diags
   334  	}
   335  
   336  	// If we get here then we should definitely have a non-nil "graph", which
   337  	// we can now walk.
   338  	changes := plans.NewChanges()
   339  	walker, walkDiags := c.walk(graph, walkOp, &graphWalkOpts{
   340  		Config:             config,
   341  		InputState:         prevRunState,
   342  		Changes:            changes,
   343  		MoveResults:        moveResults,
   344  		RootVariableValues: rootVariables,
   345  	})
   346  	diags = diags.Append(walker.NonFatalDiagnostics)
   347  	diags = diags.Append(walkDiags)
   348  	diags = diags.Append(c.postPlanValidateMoves(config, moveStmts, walker.InstanceExpander.AllInstances()))
   349  
   350  	plan := &plans.Plan{
   351  		UIMode:       opts.Mode,
   352  		Changes:      changes,
   353  		PriorState:   walker.RefreshState.Close(),
   354  		PrevRunState: walker.PrevRunState.Close(),
   355  
   356  		// Other fields get populated by Context.Plan after we return
   357  	}
   358  	return plan, diags
   359  }
   360  
   361  func (c *Context) planGraph(config *configs.Config, prevRunState *states.State, opts *PlanOpts, validate bool) (*Graph, walkOperation, tfdiags.Diagnostics) {
   362  	switch mode := opts.Mode; mode {
   363  	case plans.NormalMode:
   364  		graph, diags := (&PlanGraphBuilder{
   365  			Config:       config,
   366  			State:        prevRunState,
   367  			Plugins:      c.plugins,
   368  			Targets:      opts.Targets,
   369  			ForceReplace: opts.ForceReplace,
   370  			Validate:     validate,
   371  			skipRefresh:  opts.SkipRefresh,
   372  		}).Build(addrs.RootModuleInstance)
   373  		return graph, walkPlan, diags
   374  	case plans.RefreshOnlyMode:
   375  		graph, diags := (&PlanGraphBuilder{
   376  			Config:          config,
   377  			State:           prevRunState,
   378  			Plugins:         c.plugins,
   379  			Targets:         opts.Targets,
   380  			Validate:        validate,
   381  			skipRefresh:     opts.SkipRefresh,
   382  			skipPlanChanges: true, // this activates "refresh only" mode.
   383  		}).Build(addrs.RootModuleInstance)
   384  		return graph, walkPlan, diags
   385  	case plans.DestroyMode:
   386  		graph, diags := (&DestroyPlanGraphBuilder{
   387  			Config:      config,
   388  			State:       prevRunState,
   389  			Plugins:     c.plugins,
   390  			Targets:     opts.Targets,
   391  			Validate:    validate,
   392  			skipRefresh: opts.SkipRefresh,
   393  		}).Build(addrs.RootModuleInstance)
   394  		return graph, walkPlanDestroy, diags
   395  	default:
   396  		// The above should cover all plans.Mode values
   397  		panic(fmt.Sprintf("unsupported plan mode %s", mode))
   398  	}
   399  }
   400  
   401  // PlanGraphForUI is a last vestage of graphs in the public interface of Context
   402  // (as opposed to graphs as an implementation detail) intended only for use
   403  // by the "terraform graph" command when asked to render a plan-time graph.
   404  //
   405  // The result of this is intended only for rendering ot the user as a dot
   406  // graph, and so may change in future in order to make the result more useful
   407  // in that context, even if drifts away from the physical graph that Terraform
   408  // Core currently uses as an implementation detail of planning.
   409  func (c *Context) PlanGraphForUI(config *configs.Config, prevRunState *states.State, mode plans.Mode) (*Graph, tfdiags.Diagnostics) {
   410  	// For now though, this really is just the internal graph, confusing
   411  	// implementation details and all.
   412  
   413  	var diags tfdiags.Diagnostics
   414  
   415  	opts := &PlanOpts{Mode: mode}
   416  
   417  	graph, _, moreDiags := c.planGraph(config, prevRunState, opts, false)
   418  	diags = diags.Append(moreDiags)
   419  	return graph, diags
   420  }