Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions internal/backend/backendrun/operation.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ type Operation struct {
// plan for an apply operation.
PlanId string
PlanRefresh bool // PlanRefresh will do a refresh before a plan
PlanLight bool // PlanLight enables light plan mode, skipping refresh for unchanged resources
PlanOutPath string // PlanOutPath is the path to save the plan

// PlanOutBackend is the backend to store with the plan. This is the
Expand Down
2 changes: 2 additions & 0 deletions internal/backend/local/backend_local.go
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ func (b *Local) localRun(op *backendrun.Operation) (*backendrun.LocalRun, *confi
stateMeta = &m
}
log.Printf("[TRACE] backend/local: populating backendrun.LocalRun from plan file")
// TODO: write light option to plan file
ret, configSnap, ctxDiags = b.localRunForPlanFile(op, lp, ret, &coreOpts, stateMeta)
if ctxDiags.HasErrors() {
diags = diags.Append(ctxDiags)
Expand Down Expand Up @@ -207,6 +208,7 @@ func (b *Local) localRunDirect(op *backendrun.Operation, run *backendrun.LocalRu
ForceReplace: op.ForceReplace,
SetVariables: variables,
SkipRefresh: op.Type != backendrun.OperationTypeRefresh && !op.PlanRefresh,
LightMode: op.PlanLight,
GenerateConfigPath: op.GenerateConfigOut,
DeferralAllowed: op.DeferralAllowed,
Query: op.Query,
Expand Down
9 changes: 9 additions & 0 deletions internal/cloud/backend_plan.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,15 @@ func (b *Cloud) opPlan(stopCtx, cancelCtx context.Context, op *backendrun.Operat
))
}

if op.PlanLight {
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
"Light plan mode is not supported with cloud backends",
fmt.Sprintf("%s does not support the -light flag. ", b.appName)+
"Light plan mode is only available for local operations.",
))
}

if op.PlanFile != nil {
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
Expand Down
4 changes: 4 additions & 0 deletions internal/command/arguments/extended.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,9 @@ type Operation struct {
// state before proceeding. Default is true.
Refresh bool

// Light
Light bool

// Targets allow limiting an operation to a set of resource addresses and
// their dependencies.
Targets []addrs.Targetable
Expand Down Expand Up @@ -287,6 +290,7 @@ func extendedFlagSet(name string, state *State, operation *Operation, vars *Vars
f.IntVar(&operation.Parallelism, "parallelism", DefaultParallelism, "parallelism")
f.BoolVar(&operation.DeferralAllowed, "allow-deferral", false, "allow-deferral")
f.BoolVar(&operation.Refresh, "refresh", true, "refresh")
f.BoolVar(&operation.Light, "light", false, "light")
f.BoolVar(&operation.destroyRaw, "destroy", false, "destroy")
f.BoolVar(&operation.refreshOnlyRaw, "refresh-only", false, "refresh-only")
f.Var((*FlagStringSlice)(&operation.targetsRaw), "target", "target")
Expand Down
25 changes: 25 additions & 0 deletions internal/command/arguments/plan.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
package arguments

import (
"github.com/hashicorp/terraform/internal/plans"
"github.com/hashicorp/terraform/internal/tfdiags"
)

Expand All @@ -30,6 +31,12 @@ type Plan struct {
// be written to.
GenerateConfigPath string

// Light enables "light plan" mode, where Terraform skips reading remote
// state for resources that have not changed in the local configuration
// or local state. The user is telling Terraform to trust that nothing
// has been modified outside of the local configuration.
Light bool

// ViewType specifies which output format to use
ViewType ViewType
}
Expand Down Expand Up @@ -82,6 +89,24 @@ func ParsePlan(args []string) (*Plan, tfdiags.Diagnostics) {

diags = diags.Append(plan.Operation.Parse())

// Validate -light flag compatibility
if plan.Light {
if plan.Operation.PlanMode == plans.RefreshOnlyMode {
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
"Incompatible plan options",
"The -light and -refresh-only options are mutually exclusive. Light mode skips refreshing unchanged resources, while refresh-only mode requires refreshing all resources.",
))
}
if plan.Operation.PlanMode == plans.DestroyMode {
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
"Incompatible plan options",
"The -light and -destroy options are mutually exclusive. A destroy plan requires reading the current state of all resources.",
))
}
}

// JSON view currently does not support input, so we disable it here
if json {
plan.InputEnabled = false
Expand Down
37 changes: 37 additions & 0 deletions internal/command/arguments/plan_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,23 @@ func TestParsePlan_basicValid(t *testing.T) {
},
},
},
"light mode": {
[]string{"-light"},
&Plan{
DetailedExitCode: false,
InputEnabled: true,
Light: true,
OutPath: "",
ViewType: ViewHuman,
State: &State{Lock: true},
Vars: &Vars{},
Operation: &Operation{
PlanMode: plans.NormalMode,
Parallelism: 10,
Refresh: true,
},
},
},
"JSON view disables input": {
[]string{"-json"},
&Plan{
Expand Down Expand Up @@ -96,6 +113,26 @@ func TestParsePlan_invalid(t *testing.T) {
}
}

func TestParsePlan_lightWithDestroy(t *testing.T) {
_, diags := ParsePlan([]string{"-light", "-destroy"})
if len(diags) == 0 {
t.Fatal("expected diags but got none")
}
if got, want := diags.Err().Error(), "mutually exclusive"; !strings.Contains(got, want) {
t.Fatalf("wrong diags\n got: %s\nwant: %s", got, want)
}
}

func TestParsePlan_lightWithRefreshOnly(t *testing.T) {
_, diags := ParsePlan([]string{"-light", "-refresh-only"})
if len(diags) == 0 {
t.Fatal("expected diags but got none")
}
if got, want := diags.Err().Error(), "mutually exclusive"; !strings.Contains(got, want) {
t.Fatalf("wrong diags\n got: %s\nwant: %s", got, want)
}
}

func TestParsePlan_tooManyArguments(t *testing.T) {
got, diags := ParsePlan([]string{"saved.tfplan"})
if len(diags) == 0 {
Expand Down
12 changes: 12 additions & 0 deletions internal/command/plan.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,9 @@ func (c *PlanCommand) Run(rawArgs []string) int {
return 1
}

// Light plan mode: skip refreshing unchanged resources
opReq.PlanLight = args.Light

// Collect variable value and add them to the operation request
diags = diags.Append(c.GatherVariables(opReq, args.Vars))
if diags.HasErrors() {
Expand Down Expand Up @@ -150,6 +153,7 @@ func (c *PlanCommand) OperationRequest(
opReq.Hooks = view.Hooks()
opReq.PlanRefresh = args.Refresh
opReq.PlanOutPath = planOutPath
opReq.PlanLight = args.Light
opReq.GenerateConfigOut = generateConfigOut
opReq.Targets = args.Targets
opReq.ForceReplace = args.ForceReplace
Expand Down Expand Up @@ -234,6 +238,14 @@ Plan Customization Options:
planning faster, but at the expense of possibly planning
against a stale record of the remote system state.

-light Enable light plan mode. In this mode, Terraform skips
reading remote state for resources that have not changed
in the local configuration or local state. This is useful
when you trust that nothing has been modified outside of
the local Terraform configuration, and can significantly
speed up planning for large configurations. Incompatible
with -refresh-only and -destroy.

-replace=resource Force replacement of a particular resource instance using
its resource address. If the plan would've normally
produced an update or no-op action for this instance,
Expand Down
5 changes: 5 additions & 0 deletions internal/plans/plan.go
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,11 @@ type Plan struct {
// and builtin calls which may access external state so that calls during
// apply can be checked for consistency.
FunctionResults []lang.FunctionResultHash

// Light is true if this plan was created in "light" mode, where
// Terraform skipped reading remote state for resources that have not
// changed in the local configuration and the state dependents of those resources.
Light bool
}

// ProviderAddrs returns a list of all of the provider configuration addresses
Expand Down
1 change: 1 addition & 0 deletions internal/terraform/context_apply_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10426,6 +10426,7 @@ func TestContext2Apply_ProviderMeta_refresh_set(t *testing.T) {
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("test"): testProviderFuncFixed(p),
},
Parallelism: 1,
})

plan, diags := ctx.Plan(m, states.NewState(), DefaultPlanOpts)
Expand Down
84 changes: 79 additions & 5 deletions internal/terraform/context_plan.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,38 @@
"github.com/hashicorp/terraform/internal/tfdiags"
)

// nodePlanContext holds contextual flags that influence how individual resource
// nodes behave during the plan walk. It is derived from PlanOpts and
// copied into every resource node. Each node may further modify its own copy of this
// struct, or/and pass it to child nodes.
type nodePlanContext struct {
lightMode bool
skipPlanChanges bool

// skipRefresh indicates that we should skip refreshing managed resources
skipRefresh bool

// preDestroyRefresh indicates that we are executing the refresh which
// happens immediately before a destroy plan, which happens to use the
// normal planing mode so skipPlanChanges cannot be set.
preDestroyRefresh bool
}

func (pc nodePlanContext) withSkipPlanChanges(v bool) nodePlanContext {
pc.skipPlanChanges = v
return pc
}

func (pc nodePlanContext) withSkipRefresh(v bool) nodePlanContext {
pc.skipRefresh = v
return pc
}

func (pc nodePlanContext) withPreDestroyRefresh(v bool) nodePlanContext {

Check failure on line 57 in internal/terraform/context_plan.go

View workflow job for this annotation

GitHub Actions / Code Consistency Checks

func nodePlanContext.withPreDestroyRefresh is unused (U1000)
pc.preDestroyRefresh = v
return pc
}

// PlanOpts are the various options that affect the details of how Terraform
// will build a plan.
type PlanOpts struct {
Expand All @@ -41,6 +73,12 @@
// instance using its corresponding provider.
SkipRefresh bool

// LightMode enables terraform to plan each resource against local state first,
// if the result is a NoOp the expensive remote-state refresh is skipped entirely.
// Resources whose local plan shows changes are still refreshed and
// re-planned so the final diff is accurate.
LightMode bool

// PreDestroyRefresh indicated that this is being passed to a plan used to
// refresh the state immediately before a destroy plan.
// FIXME: This is a temporary fix to allow the pre-destroy refresh to
Expand Down Expand Up @@ -253,6 +291,22 @@
))
return nil, nil, diags
}
if opts.LightMode && opts.Mode != plans.NormalMode {
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
"Incompatible plan options",
"Light plan mode is only compatible with normal planning mode. It cannot be used with -refresh-only or -destroy.",
))
return nil, nil, diags
}
if opts.LightMode {
log.Printf("[INFO] Light plan mode enabled: skipping refresh for all resources")
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Warning,
"Light plan mode is in effect",
"You are creating a plan with the -light option, which skips reading the current state of remote resources. The resulting plan may not detect changes made outside of Terraform (drift). Use a normal plan to get a complete view of all changes.",
))
}
if len(opts.ForceReplace) > 0 && opts.Mode != plans.NormalMode {
// The other modes don't generate no-op or update actions that we might
// upgrade to be "replace", so doesn't make sense to combine those.
Expand Down Expand Up @@ -335,6 +389,14 @@
}
}

if opts.LightMode {
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Warning,
"Light plan mode is in effect",
`You are creating a plan with the light mode, which means that the result of this plan may not represent all of the changes that may have occurred outside of Terraform.`,
))
}

var plan *plans.Plan
var planDiags tfdiags.Diagnostics
var evalScope *lang.Scope
Expand Down Expand Up @@ -459,6 +521,17 @@
return diags
}

func (opts *PlanOpts) nodeContext() nodePlanContext {
if opts == nil {
return nodePlanContext{}
}
return nodePlanContext{
lightMode: opts.LightMode,
skipRefresh: opts.SkipRefresh,
preDestroyRefresh: opts.PreDestroyRefresh,
}
}

var DefaultPlanOpts = &PlanOpts{
Mode: plans.NormalMode,
}
Expand Down Expand Up @@ -880,6 +953,7 @@
Checks: states.NewCheckResults(walker.Checks),
Timestamp: timestamp,
FunctionResults: funcResults.GetHashes(),
Light: opts.LightMode,

// Other fields get populated by Context.Plan after we return
}
Expand Down Expand Up @@ -1006,8 +1080,7 @@
Plugins: c.plugins,
Targets: opts.Targets,
ForceReplace: opts.ForceReplace,
skipRefresh: opts.SkipRefresh,
preDestroyRefresh: opts.PreDestroyRefresh,
planCtx: opts.nodeContext(),
Operation: walkPlan,
ExternalReferences: opts.ExternalReferences,
Overrides: opts.Overrides,
Expand All @@ -1022,6 +1095,8 @@
}).Build(addrs.RootModuleInstance)
return graph, walkPlan, diags
case plans.RefreshOnlyMode:
nodeCtx := opts.nodeContext().
withSkipPlanChanges(true) // this activates "refresh only" mode.
graph, diags := (&PlanGraphBuilder{
Config: config,
State: prevRunState,
Expand All @@ -1030,8 +1105,7 @@
Plugins: c.plugins,
Targets: append(opts.Targets, opts.ActionTargets...),
ActionTargets: opts.ActionTargets,
skipRefresh: opts.SkipRefresh,
skipPlanChanges: true, // this activates "refresh only" mode.
planCtx: nodeCtx,
Operation: walkPlan,
ExternalReferences: opts.ExternalReferences,
Overrides: opts.Overrides,
Expand All @@ -1047,7 +1121,7 @@
ExternalProviderConfigs: externalProviderConfigs,
Plugins: c.plugins,
Targets: opts.Targets,
skipRefresh: opts.SkipRefresh,
planCtx: opts.nodeContext(),
Operation: walkPlanDestroy,
Overrides: opts.Overrides,
SkipGraphValidation: c.graphOpts.SkipGraphValidation,
Expand Down
Loading
Loading