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

command + backend/local: -refresh-only and drift detection #28634

Merged
merged 4 commits into from
May 13, 2021
Merged
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
27 changes: 14 additions & 13 deletions backend/local/backend_apply.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,20 +72,31 @@ func (b *Local) opApply(
return
}

trivialPlan := plan.Changes.Empty()
trivialPlan := !plan.CanApply()
hasUI := op.UIOut != nil && op.UIIn != nil
mustConfirm := hasUI && !op.AutoApprove && !trivialPlan
op.View.Plan(plan, tfCtx.Schemas())

if mustConfirm {
var desc, query string
if op.PlanMode == plans.DestroyMode {
switch op.PlanMode {
case plans.DestroyMode:
if op.Workspace != "default" {
query = "Do you really want to destroy all resources in workspace \"" + op.Workspace + "\"?"
} else {
query = "Do you really want to destroy all resources?"
}
desc = "Terraform will destroy all your managed infrastructure, as shown above.\n" +
"There is no undo. Only 'yes' will be accepted to confirm."
} else {
case plans.RefreshOnlyMode:
if op.Workspace != "default" {
query = "Would you like to update the Terraform state for \"" + op.Workspace + "\" to reflect these detected changes?"
} else {
query = "Would you like to update the Terraform state to reflect these detected changes?"
}
desc = "Terraform will write these changes to the state without modifying any real infrastructure.\n" +
"There is no undo. Only 'yes' will be accepted to confirm."
default:
if op.Workspace != "default" {
query = "Do you want to perform these actions in workspace \"" + op.Workspace + "\"?"
} else {
Expand All @@ -95,10 +106,6 @@ func (b *Local) opApply(
"Only 'yes' will be accepted to approve."
}

if !trivialPlan {
op.View.Plan(plan, tfCtx.Schemas())
}

// We'll show any accumulated warnings before we display the prompt,
// so the user can consider them when deciding how to answer.
if len(diags) > 0 {
Expand All @@ -121,12 +128,6 @@ func (b *Local) opApply(
runningOp.Result = backend.OperationFailure
return
}
} else {
for _, change := range plan.Changes.Resources {
if change.Action != plans.NoOp {
op.View.PlannedChange(change)
}
}
}
} else {
plan, err := op.PlanFile.ReadPlan()
Expand Down
15 changes: 4 additions & 11 deletions backend/local/backend_plan.go
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ func (b *Local) opPlan(
}

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

// Save the plan to disk
if path := op.PlanOutPath; path != "" {
Expand Down Expand Up @@ -143,15 +143,6 @@ func (b *Local) opPlan(
}
}

// Perform some output tasks
if runningOp.PlanEmpty {
op.View.PlanNoChanges()

// Even if there are no changes, there still could be some warnings
op.View.Diagnostics(diags)
return
}

// Render the plan
op.View.Plan(plan, tfCtx.Schemas())

Expand All @@ -160,5 +151,7 @@ func (b *Local) opPlan(
// errors then we would've returned early at some other point above.
op.View.Diagnostics(diags)

op.View.PlanNextStep(op.PlanOutPath)
if !runningOp.PlanEmpty {
op.View.PlanNextStep(op.PlanOutPath)
}
}
18 changes: 10 additions & 8 deletions backend/local/backend_plan_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -202,22 +202,23 @@ func TestLocal_planOutputsChanged(t *testing.T) {
t.Fatalf("plan operation failed")
}
if run.PlanEmpty {
t.Fatal("plan should not be empty")
t.Error("plan should not be empty")
}

expectedOutput := strings.TrimSpace(`
Plan: 0 to add, 0 to change, 0 to destroy.

Changes to Outputs:
+ added = "after"
~ changed = "before" -> "after"
- removed = "before" -> null
~ sensitive_after = (sensitive value)
~ sensitive_before = (sensitive value)

You can apply this plan to save these new output values to the Terraform
state, without changing any real infrastructure.
`)

if output := done(t).Stdout(); !strings.Contains(output, expectedOutput) {
t.Fatalf("Unexpected output:\n%s\n\nwant output containing:\n%s", output, expectedOutput)
t.Errorf("Unexpected output:\n%s\n\nwant output containing:\n%s", output, expectedOutput)
}
}

Expand Down Expand Up @@ -262,7 +263,7 @@ func TestLocal_planModuleOutputsChanged(t *testing.T) {
}

expectedOutput := strings.TrimSpace(`
No changes. Infrastructure is up-to-date.
No changes. Your infrastructure matches the configuration.
`)
if output := done(t).Stdout(); !strings.Contains(output, expectedOutput) {
t.Fatalf("Unexpected output:\n%s\n\nwant output containing:\n%s", output, expectedOutput)
Expand Down Expand Up @@ -323,7 +324,7 @@ Terraform will perform the following actions:

Plan: 1 to add, 0 to change, 1 to destroy.`
if output := done(t).Stdout(); !strings.Contains(output, expectedOutput) {
t.Fatalf("Unexpected output:\n%s", output)
t.Fatalf("Unexpected output\ngot\n%s\n\nwant:\n%s", output, expectedOutput)
}
}

Expand Down Expand Up @@ -382,8 +383,8 @@ func TestLocal_planDeposedOnly(t *testing.T) {
if run.Result != backend.OperationSuccess {
t.Fatalf("plan operation failed")
}
if p.ReadResourceCalled {
t.Fatal("ReadResource should not be called")
if !p.ReadResourceCalled {
t.Fatal("ReadResource should've been called to refresh the deposed object")
}
if run.PlanEmpty {
t.Fatal("plan should not be empty")
Expand Down Expand Up @@ -426,6 +427,7 @@ Terraform will perform the following actions:
}

# test_instance.foo (deposed object 00000000) will be destroyed
# (left over from a partially-failed replacement of this instance)
- resource "test_instance" "foo" {
- ami = "bar" -> null

Expand Down
16 changes: 16 additions & 0 deletions backend/remote/backend_plan.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,14 @@ func (b *Remote) opPlan(stopCtx, cancelCtx context.Context, op *backend.Operatio
))
}

if op.PlanMode == plans.RefreshOnlyMode {
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
"Refresh-only mode is currently not supported",
`The "remote" backend does not currently support the refresh-only planning mode.`,
))
}

if b.hasExplicitVariableValues(op) {
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
Expand Down Expand Up @@ -102,6 +110,14 @@ func (b *Remote) opPlan(stopCtx, cancelCtx context.Context, op *backend.Operatio
))
}

if len(op.ForceReplace) != 0 {
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
"Forced replacement is currently not supported",
`The "remote" backend does not currently support the -replace=... planning option.`,
))
}

if len(op.Targets) != 0 {
// For API versions prior to 2.3, RemoteAPIVersion will return an empty string,
// so if there's an error when parsing the RemoteAPIVersion, it's handled as
Expand Down
34 changes: 19 additions & 15 deletions command/apply_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2103,7 +2103,7 @@ func TestApply_jsonGoldenReference(t *testing.T) {
wantLines := strings.Split(want, "\n")

if len(gotLines) != len(wantLines) {
t.Fatalf("unexpected number of log lines: got %d, want %d", len(gotLines), len(wantLines))
t.Errorf("unexpected number of log lines: got %d, want %d", len(gotLines), len(wantLines))
}

// Verify that the log starts with a version message
Expand All @@ -2130,26 +2130,30 @@ func TestApply_jsonGoldenReference(t *testing.T) {
}

// Compare the rest of the lines against the golden reference
for i := range gotLines[1:] {
var gotLineMaps []map[string]interface{}
for i, line := range gotLines[1:] {
index := i + 1
var gotMap, wantMap map[string]interface{}
if err := json.Unmarshal([]byte(gotLines[index]), &gotMap); err != nil {
t.Errorf("failed to unmarshal got line %d: %s\n%s", index, err, gotLines[i])
var gotMap map[string]interface{}
if err := json.Unmarshal([]byte(line), &gotMap); err != nil {
t.Errorf("failed to unmarshal got line %d: %s\n%s", index, err, gotLines[index])
}
if err := json.Unmarshal([]byte(wantLines[index]), &wantMap); err != nil {
t.Errorf("failed to unmarshal want line %d: %s\n%s", index, err, wantLines[i])
}

// The timestamp field is the only one that should change, so we drop
// it from the comparison
if _, ok := gotMap["@timestamp"]; !ok {
t.Errorf("missing @timestamp field in log: %s", gotLines[i])
t.Errorf("missing @timestamp field in log: %s", gotLines[index])
}
delete(gotMap, "@timestamp")

if !cmp.Equal(wantMap, gotMap) {
t.Errorf("unexpected log:\n%s", cmp.Diff(wantMap, gotMap))
gotLineMaps = append(gotLineMaps, gotMap)
}
var wantLineMaps []map[string]interface{}
for i, line := range wantLines[1:] {
index := i + 1
var wantMap map[string]interface{}
if err := json.Unmarshal([]byte(line), &wantMap); err != nil {
t.Errorf("failed to unmarshal want line %d: %s\n%s", index, err, gotLines[index])
}
wantLineMaps = append(wantLineMaps, wantMap)
}
if diff := cmp.Diff(wantLineMaps, gotLineMaps); diff != "" {
t.Errorf("wrong output lines\n%s", diff)
}
}

Expand Down
17 changes: 17 additions & 0 deletions command/arguments/extended.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ type Operation struct {
targetsRaw []string
forceReplaceRaw []string
destroyRaw bool
refreshOnlyRaw bool
}

// Parse must be called on Operation after initial flag parse. This processes
Expand Down Expand Up @@ -151,8 +152,23 @@ func (o *Operation) Parse() tfdiags.Diagnostics {
// If you add a new possible value for o.PlanMode here, consider also
// adding a specialized error message for it in ParseApplyDestroy.
switch {
case o.destroyRaw && o.refreshOnlyRaw:
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
"Incompatible plan mode options",
"The -destroy and -refresh-only options are mutually-exclusive.",
))
case o.destroyRaw:
o.PlanMode = plans.DestroyMode
case o.refreshOnlyRaw:
o.PlanMode = plans.RefreshOnlyMode
if !o.Refresh {
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
"Incompatible refresh options",
"It doesn't make sense to use -refresh-only at the same time as -refresh=false, because Terraform would have nothing to do.",
))
}
default:
o.PlanMode = plans.NormalMode
}
Expand Down Expand Up @@ -206,6 +222,7 @@ func extendedFlagSet(name string, state *State, operation *Operation, vars *Vars
f.IntVar(&operation.Parallelism, "parallelism", DefaultParallelism, "parallelism")
f.BoolVar(&operation.Refresh, "refresh", true, "refresh")
f.BoolVar(&operation.destroyRaw, "destroy", false, "destroy")
f.BoolVar(&operation.refreshOnlyRaw, "refresh-only", false, "refresh-only")
f.Var((*flagStringSlice)(&operation.targetsRaw), "target", "target")
f.Var((*flagStringSlice)(&operation.forceReplaceRaw), "replace", "replace")
}
Expand Down
4 changes: 2 additions & 2 deletions command/e2etest/primary_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -165,8 +165,8 @@ func TestPrimaryChdirOption(t *testing.T) {
t.Fatalf("unexpected plan error: %s\nstderr:\n%s", err, stderr)
}

if !strings.Contains(stdout, "0 to add, 0 to change, 0 to destroy") {
t.Errorf("incorrect plan tally; want 0 to add:\n%s", stdout)
if want := "You can apply this plan to save these new output values"; !strings.Contains(stdout, want) {
t.Errorf("missing expected message for an outputs-only plan\ngot:\n%s\n\nwant substring: %s", stdout, want)
}

if !strings.Contains(stdout, "Saved the plan to: tfplan") {
Expand Down
Loading