Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refresh-only plans and forced-replacement #28297

Closed
wants to merge 6 commits into from
Closed
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
11 changes: 6 additions & 5 deletions backend/backend.go
Original file line number Diff line number Diff line change
Expand Up @@ -193,11 +193,12 @@ type Operation struct {

// The options below are more self-explanatory and affect the runtime
// behavior of the operation.
AutoApprove bool
Destroy bool
Parallelism int
Targets []addrs.Targetable
Variables map[string]UnparsedVariableValue
PlanMode plans.Mode
AutoApprove bool
Parallelism int
Targets []addrs.Targetable
ForceReplace []addrs.AbsResourceInstance
Variables map[string]UnparsedVariableValue

// Some operations use root module variables only opportunistically or
// don't need them at all. If this flag is set, the backend must treat
Expand Down
17 changes: 13 additions & 4 deletions backend/local/backend_apply.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"github.com/hashicorp/errwrap"
"github.com/hashicorp/terraform/backend"
"github.com/hashicorp/terraform/command/views"
"github.com/hashicorp/terraform/plans"
"github.com/hashicorp/terraform/states"
"github.com/hashicorp/terraform/states/statefile"
"github.com/hashicorp/terraform/states/statemgr"
Expand All @@ -26,7 +27,7 @@ func (b *Local) opApply(

// If we have a nil module at this point, then set it to an empty tree
// to avoid any potential crashes.
if op.PlanFile == nil && !op.Destroy && !op.HasConfig() {
if op.PlanFile == nil && op.PlanMode != plans.DestroyMode && !op.HasConfig() {
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
"No configuration files",
Expand Down Expand Up @@ -58,6 +59,12 @@ func (b *Local) opApply(
}
}()

// TEMP: We'll keep a snapshot of the original state, prior to any
// refreshing as a temporary way to approximate detecting and reporting
// changes during refresh, until we've integrated that properly into
// the plan model.
initialState := tfCtx.State().DeepCopy()

runningOp.State = tfCtx.State()

// If we weren't given a plan, then we refresh/plan
Expand All @@ -71,12 +78,14 @@ func (b *Local) opApply(
return
}

trivialPlan := plan.Changes.Empty()
refreshFoundChanges := tempRefreshReporting(initialState, plan.State, op.View)

trivialPlan := plan.Changes.Empty() && !refreshFoundChanges
hasUI := op.UIOut != nil && op.UIIn != nil
mustConfirm := hasUI && !op.AutoApprove && !trivialPlan
if mustConfirm {
var desc, query string
if op.Destroy {
if op.PlanMode == plans.DestroyMode {
if op.Workspace != "default" {
query = "Do you really want to destroy all resources in workspace \"" + op.Workspace + "\"?"
} else {
Expand Down Expand Up @@ -116,7 +125,7 @@ func (b *Local) opApply(
return
}
if v != "yes" {
op.View.Cancelled(op.Destroy)
op.View.Cancelled(op.PlanMode)
runningOp.Result = backend.OperationFailure
return
}
Expand Down
3 changes: 2 additions & 1 deletion backend/local/backend_apply_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import (
"github.com/hashicorp/terraform/configs/configschema"
"github.com/hashicorp/terraform/internal/initwd"
"github.com/hashicorp/terraform/internal/terminal"
"github.com/hashicorp/terraform/plans"
"github.com/hashicorp/terraform/providers"
"github.com/hashicorp/terraform/states"
"github.com/hashicorp/terraform/states/statemgr"
Expand Down Expand Up @@ -115,7 +116,7 @@ func TestLocal_applyEmptyDirDestroy(t *testing.T) {

op, configCleanup, done := testOperationApply(t, "./testdata/empty")
defer configCleanup()
op.Destroy = true
op.PlanMode = plans.DestroyMode

run, err := b.Operation(context.Background(), op)
if err != nil {
Expand Down
4 changes: 3 additions & 1 deletion backend/local/backend_local.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,8 +65,9 @@ func (b *Local) context(op *backend.Operation) (*terraform.Context, *configload.
}

// Copy set options from the operation
opts.Destroy = op.Destroy
opts.PlanMode = op.PlanMode
opts.Targets = op.Targets
opts.ForceReplace = op.ForceReplace
opts.UIInput = op.UIIn
opts.Hooks = op.Hooks

Expand Down Expand Up @@ -264,6 +265,7 @@ func (b *Local) contextFromPlanFile(pf *planfile.Reader, opts terraform.ContextO
opts.Variables = variables
opts.Changes = plan.Changes
opts.Targets = plan.TargetAddrs
opts.ForceReplace = plan.ForceReplaceAddrs
opts.ProviderSHA256s = plan.ProviderSHA256s

tfCtx, ctxDiags := terraform.NewContext(&opts)
Expand Down
62 changes: 60 additions & 2 deletions backend/local/backend_plan.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,12 @@ import (
"fmt"
"log"

"github.com/hashicorp/terraform/addrs"
"github.com/hashicorp/terraform/backend"
"github.com/hashicorp/terraform/command/views"
"github.com/hashicorp/terraform/plans"
"github.com/hashicorp/terraform/plans/planfile"
"github.com/hashicorp/terraform/states"
"github.com/hashicorp/terraform/states/statemgr"
"github.com/hashicorp/terraform/terraform"
"github.com/hashicorp/terraform/tfdiags"
Expand Down Expand Up @@ -35,7 +38,7 @@ func (b *Local) opPlan(
}

// Local planning requires a config, unless we're planning to destroy.
if !op.Destroy && !op.HasConfig() {
if op.PlanMode != plans.DestroyMode && !op.HasConfig() {
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
"No configuration files",
Expand Down Expand Up @@ -69,6 +72,12 @@ func (b *Local) opPlan(
}
}()

// TEMP: We'll keep a snapshot of the original state, prior to any
// refreshing as a temporary way to approximate detecting and reporting
// changes during refresh, until we've integrated that properly into
// the plan model.
initialState := tfCtx.State().DeepCopy()

runningOp.State = tfCtx.State()

// Perform the plan in a goroutine so we can be interrupted
Expand Down Expand Up @@ -96,8 +105,10 @@ func (b *Local) opPlan(
return
}

refreshFoundChanges := tempRefreshReporting(initialState, plan.State, op.View)

// Record whether this plan includes any side-effects that could be applied.
runningOp.PlanEmpty = plan.Changes.Empty()
runningOp.PlanEmpty = plan.Changes.Empty() && !refreshFoundChanges

// Save the plan to disk
if path := op.PlanOutPath; path != "" {
Expand Down Expand Up @@ -149,3 +160,50 @@ func (b *Local) opPlan(

op.View.PlanNextStep(op.PlanOutPath)
}

// tempRefreshReporting is a temporary placeholder for what will hopefully be
// a better-integrated and more user-friendly report of any changes detected
// as a result of refreshing existing managed resources.
//
// For now it just prints out a developer-oriented summary of what it found
// and returns true only if there is at least one resource instance difference
// which a user might therefore want to save as part of a new state snapshot.
func tempRefreshReporting(baseState, priorState *states.State, view views.Operation) bool {
if baseState == nil || priorState == nil {
return false
}
changes := false
for _, bms := range baseState.Modules {
for _, brs := range bms.Resources {
if brs.Addr.Resource.Mode != addrs.ManagedResourceMode {
continue // only managed resources can "drift"
}
prs := priorState.Resource(brs.Addr)
if prs == nil {
// Refreshing detected that the remote object has been deleted
var diags tfdiags.Diagnostics
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Warning,
"(Prototype-only refresh result reporting)",
fmt.Sprintf("Apparently %s has been deleted outside of Terraform.", brs.Addr),
))
view.Diagnostics(diags)
changes = true
continue
}
if !prs.Equal(brs) {
// Refreshing detected that the remote object has changed.
var diags tfdiags.Diagnostics
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Warning,
"(Prototype-only refresh result reporting)",
fmt.Sprintf("Apparently %s has been changed outside of Terraform.", brs.Addr),
))
view.Diagnostics(diags)
changes = true
continue
}
}
}
return changes
}
4 changes: 2 additions & 2 deletions backend/local/backend_plan_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -544,7 +544,7 @@ func TestLocal_planDestroy(t *testing.T) {

op, configCleanup, done := testOperationPlan(t, "./testdata/plan")
defer configCleanup()
op.Destroy = true
op.PlanMode = plans.DestroyMode
op.PlanRefresh = true
op.PlanOutPath = planPath
cfg := cty.ObjectVal(map[string]cty.Value{
Expand Down Expand Up @@ -598,7 +598,7 @@ func TestLocal_planDestroy_withDataSources(t *testing.T) {

op, configCleanup, done := testOperationPlan(t, "./testdata/destroy-with-ds")
defer configCleanup()
op.Destroy = true
op.PlanMode = plans.DestroyMode
op.PlanRefresh = true
op.PlanOutPath = planPath
cfg := cty.ObjectVal(map[string]cty.Value{
Expand Down
11 changes: 7 additions & 4 deletions backend/remote/backend_apply.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
tfe "github.com/hashicorp/go-tfe"
version "github.com/hashicorp/go-version"
"github.com/hashicorp/terraform/backend"
"github.com/hashicorp/terraform/plans"
"github.com/hashicorp/terraform/terraform"
"github.com/hashicorp/terraform/tfdiags"
)
Expand Down Expand Up @@ -84,7 +85,7 @@ func (b *Remote) opApply(stopCtx, cancelCtx context.Context, op *backend.Operati
))
}

if !op.HasConfig() && !op.Destroy {
if !op.HasConfig() && op.PlanMode != plans.DestroyMode {
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
"No configuration files found",
Expand Down Expand Up @@ -152,10 +153,12 @@ func (b *Remote) opApply(stopCtx, cancelCtx context.Context, op *backend.Operati
if r.Actions.IsDiscardable {
err = b.client.Runs.Discard(stopCtx, r.ID, tfe.RunDiscardOptions{})
if err != nil {
if op.Destroy {
switch op.PlanMode {
case plans.DestroyMode:
return r, generalError("Failed to discard destroy", err)
default:
return r, generalError("Failed to discard apply", err)
}
return r, generalError("Failed to discard apply", err)
}
}
diags = diags.Append(tfdiags.Sourceless(
Expand All @@ -176,7 +179,7 @@ func (b *Remote) opApply(stopCtx, cancelCtx context.Context, op *backend.Operati
if mustConfirm {
opts := &terraform.InputOpts{Id: "approve"}

if op.Destroy {
if op.PlanMode == plans.DestroyMode {
opts.Query = "\nDo you really want to destroy all resources in workspace \"" + op.Workspace + "\"?"
opts.Description = "Terraform will destroy all your managed infrastructure, as shown above.\n" +
"There is no undo. Only 'yes' will be accepted to confirm."
Expand Down
5 changes: 3 additions & 2 deletions backend/remote/backend_apply_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import (
"github.com/hashicorp/terraform/command/views"
"github.com/hashicorp/terraform/internal/initwd"
"github.com/hashicorp/terraform/internal/terminal"
"github.com/hashicorp/terraform/plans"
"github.com/hashicorp/terraform/plans/planfile"
"github.com/hashicorp/terraform/states/statemgr"
"github.com/hashicorp/terraform/terraform"
Expand Down Expand Up @@ -968,7 +969,7 @@ func TestRemote_applyDestroy(t *testing.T) {
"approve": "yes",
})

op.Destroy = true
op.PlanMode = plans.DestroyMode
op.UIIn = input
op.UIOut = b.CLI
op.Workspace = backend.DefaultStateName
Expand Down Expand Up @@ -1014,7 +1015,7 @@ func TestRemote_applyDestroyNoConfig(t *testing.T) {
defer configCleanup()
defer done(t)

op.Destroy = true
op.PlanMode = plans.DestroyMode
op.UIIn = input
op.UIOut = b.CLI
op.Workspace = backend.DefaultStateName
Expand Down
7 changes: 4 additions & 3 deletions backend/remote/backend_common.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (

tfe "github.com/hashicorp/go-tfe"
"github.com/hashicorp/terraform/backend"
"github.com/hashicorp/terraform/plans"
"github.com/hashicorp/terraform/terraform"
)

Expand Down Expand Up @@ -508,7 +509,7 @@ func (b *Remote) confirm(stopCtx context.Context, op *backend.Operation, opts *t

if err == errRunDiscarded {
err = errApplyDiscarded
if op.Destroy {
if op.PlanMode == plans.DestroyMode {
err = errDestroyDiscarded
}
}
Expand Down Expand Up @@ -551,7 +552,7 @@ func (b *Remote) confirm(stopCtx context.Context, op *backend.Operation, opts *t
if r.Actions.IsDiscardable {
err = b.client.Runs.Discard(stopCtx, r.ID, tfe.RunDiscardOptions{})
if err != nil {
if op.Destroy {
if op.PlanMode == plans.DestroyMode {
return generalError("Failed to discard destroy", err)
}
return generalError("Failed to discard apply", err)
Expand All @@ -560,7 +561,7 @@ func (b *Remote) confirm(stopCtx context.Context, op *backend.Operation, opts *t

// Even if the run was discarded successfully, we still
// return an error as the apply command was canceled.
if op.Destroy {
if op.PlanMode == plans.DestroyMode {
return errDestroyDiscarded
}
return errApplyDiscarded
Expand Down
2 changes: 1 addition & 1 deletion backend/remote/backend_context.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ func (b *Remote) Context(op *backend.Operation) (*terraform.Context, statemgr.Fu
}

// Copy set options from the operation
opts.Destroy = op.Destroy
opts.PlanMode = op.PlanMode
opts.Targets = op.Targets
opts.UIInput = op.UIIn

Expand Down
19 changes: 17 additions & 2 deletions backend/remote/backend_plan.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import (
tfe "github.com/hashicorp/go-tfe"
version "github.com/hashicorp/go-version"
"github.com/hashicorp/terraform/backend"
"github.com/hashicorp/terraform/plans"
"github.com/hashicorp/terraform/tfdiags"
)

Expand Down Expand Up @@ -89,7 +90,7 @@ func (b *Remote) opPlan(stopCtx, cancelCtx context.Context, op *backend.Operatio
))
}

if !op.HasConfig() && !op.Destroy {
if !op.HasConfig() && op.PlanMode != plans.DestroyMode {
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
"No configuration files found",
Expand Down Expand Up @@ -238,12 +239,26 @@ in order to capture the filesystem context the remote workspace expects:
}

runOptions := tfe.RunCreateOptions{
IsDestroy: tfe.Bool(op.Destroy),
Message: tfe.String(queueMessage),
ConfigurationVersion: cv,
Workspace: w,
}

switch op.PlanMode {
case plans.NormalMode:
// okay, but we don't need to do anything special for this
case plans.DestroyMode:
runOptions.IsDestroy = tfe.Bool(true)
default:
// Shouldn't get here because we should update this for each new
// plan mode we add, mapping it to the corresponding RunCreateOptions
// field.
return nil, generalError(
"Invalid plan mode",
fmt.Errorf("remote backend doesn't support %s", op.PlanMode),
)
}

if len(op.Targets) != 0 {
runOptions.TargetAddrs = make([]string, 0, len(op.Targets))
for _, addr := range op.Targets {
Expand Down
Loading