cli: Move resource count code to command package

CountHook is an implementation of terraform.Hook which is used to
calculate how many resources were added, changed, or destroyed during an
apply. This hook was previously injected in the local backend code,
which means that the apply command code has no access to these counts.

This commit moves the CountHook code into the command package, and
removes an unused instance of the hook in the plan code path. The goal
here is moving UI code into the command package.
This commit is contained in:
Alisdair McDiarmid 2021-01-29 14:39:06 -05:00
parent bd70df8392
commit 5ca118b4e6
10 changed files with 55 additions and 140 deletions

View File

@ -39,15 +39,13 @@ func (b *Local) opApply(
return return
} }
// Set up our count hook that keeps track of resource changes
countHook := new(CountHook)
stateHook := new(StateHook) stateHook := new(StateHook)
if b.ContextOpts == nil { if b.ContextOpts == nil {
b.ContextOpts = new(terraform.ContextOpts) b.ContextOpts = new(terraform.ContextOpts)
} }
old := b.ContextOpts.Hooks old := b.ContextOpts.Hooks
defer func() { b.ContextOpts.Hooks = old }() defer func() { b.ContextOpts.Hooks = old }()
b.ContextOpts.Hooks = append(b.ContextOpts.Hooks, countHook, stateHook) b.ContextOpts.Hooks = append(b.ContextOpts.Hooks, stateHook)
// Get our context // Get our context
tfCtx, _, opState, contextDiags := b.context(op) tfCtx, _, opState, contextDiags := b.context(op)
@ -183,35 +181,6 @@ func (b *Local) opApply(
// here just before we show the summary and next steps. If we encountered // here just before we show the summary and next steps. If we encountered
// errors then we would've returned early at some other point above. // errors then we would've returned early at some other point above.
b.ShowDiagnostics(diags) b.ShowDiagnostics(diags)
// If we have a UI, output the results
if b.CLI != nil {
if op.Destroy {
b.CLI.Output(b.Colorize().Color(fmt.Sprintf(
"[reset][bold][green]\n"+
"Destroy complete! Resources: %d destroyed.",
countHook.Removed)))
} else {
b.CLI.Output(b.Colorize().Color(fmt.Sprintf(
"[reset][bold][green]\n"+
"Apply complete! Resources: %d added, %d changed, %d destroyed.",
countHook.Added,
countHook.Changed,
countHook.Removed)))
}
// only show the state file help message if the state is local.
if (countHook.Added > 0 || countHook.Changed > 0) && b.StateOutPath != "" {
b.CLI.Output(b.Colorize().Color(fmt.Sprintf(
"[reset]\n"+
"The state of your infrastructure has been saved to the path\n"+
"below. This state is required to modify and destroy your\n"+
"infrastructure, so keep it safe. To inspect the complete state\n"+
"use the `terraform show` command.\n\n"+
"State path: %s",
b.StateOutPath)))
}
}
} }
// backupStateForError is called in a scenario where we're unable to persist the // backupStateForError is called in a scenario where we're unable to persist the

View File

@ -59,14 +59,9 @@ func (b *Local) opPlan(
return return
} }
// Set up our count hook that keeps track of resource changes
countHook := new(CountHook)
if b.ContextOpts == nil { if b.ContextOpts == nil {
b.ContextOpts = new(terraform.ContextOpts) b.ContextOpts = new(terraform.ContextOpts)
} }
old := b.ContextOpts.Hooks
defer func() { b.ContextOpts.Hooks = old }()
b.ContextOpts.Hooks = append(b.ContextOpts.Hooks, countHook)
// Get our context // Get our context
tfCtx, configSnap, opState, ctxDiags := b.context(op) tfCtx, configSnap, opState, ctxDiags := b.context(op)

View File

@ -4,7 +4,6 @@ import (
"context" "context"
"os" "os"
"path/filepath" "path/filepath"
"reflect"
"strings" "strings"
"testing" "testing"
@ -728,49 +727,6 @@ func TestLocal_planOutPathNoChange(t *testing.T) {
} }
} }
// TestLocal_planScaleOutNoDupeCount tests a Refresh/Plan sequence when a
// resource count is scaled out. The scaled out node needs to exist in the
// graph and run through a plan-style sequence during the refresh phase, but
// can conflate the count if its post-diff count hooks are not skipped. This
// checks to make sure the correct resource count is ultimately given to the
// UI.
func TestLocal_planScaleOutNoDupeCount(t *testing.T) {
b, cleanup := TestLocal(t)
defer cleanup()
TestLocalProvider(t, b, "test", planFixtureSchema())
testStateFile(t, b.StatePath, testPlanState())
actual := new(CountHook)
b.ContextOpts.Hooks = append(b.ContextOpts.Hooks, actual)
outDir := testTempDir(t)
defer os.RemoveAll(outDir)
op, configCleanup := testOperationPlan(t, "./testdata/plan-scaleout")
defer configCleanup()
op.PlanRefresh = true
run, err := b.Operation(context.Background(), op)
if err != nil {
t.Fatalf("bad: %s", err)
}
<-run.Done()
if run.Result != backend.OperationSuccess {
t.Fatalf("plan operation failed")
}
expected := new(CountHook)
expected.ToAdd = 1
expected.ToChange = 0
expected.ToRemoveAndAdd = 0
expected.ToRemove = 0
if !reflect.DeepEqual(expected, actual) {
t.Fatalf("Expected %#v, got %#v instead.",
expected, actual)
}
}
func testOperationPlan(t *testing.T, configDir string) (*backend.Operation, func()) { func testOperationPlan(t *testing.T, configDir string) (*backend.Operation, func()) {
t.Helper() t.Helper()

View File

@ -1,25 +0,0 @@
// Code generated by "stringer -type=countHookAction hook_count_action.go"; DO NOT EDIT.
package local
import "strconv"
func _() {
// An "invalid array index" compiler error signifies that the constant values have changed.
// Re-run the stringer command to generate them again.
var x [1]struct{}
_ = x[countHookActionAdd-0]
_ = x[countHookActionChange-1]
_ = x[countHookActionRemove-2]
}
const _countHookAction_name = "countHookActionAddcountHookActionChangecountHookActionRemove"
var _countHookAction_index = [...]uint8{0, 18, 39, 60}
func (i countHookAction) String() string {
if i >= countHookAction(len(_countHookAction_index)-1) {
return "countHookAction(" + strconv.FormatInt(int64(i), 10) + ")"
}
return _countHookAction_name[_countHookAction_index[i]:_countHookAction_index[i+1]]
}

View File

@ -1,11 +0,0 @@
package local
//go:generate go run golang.org/x/tools/cmd/stringer -type=countHookAction hook_count_action.go
type countHookAction byte
const (
countHookActionAdd countHookAction = iota
countHookActionChange
countHookActionRemove
)

View File

@ -1,10 +0,0 @@
resource "test_instance" "foo" {
count = 2
ami = "bar"
# This is here because at some point it caused a test failure
network_interface {
device_index = 0
description = "Main network interface"
}
}

View File

@ -775,8 +775,8 @@ func TestRemote_applyForceLocal(t *testing.T) {
if !strings.Contains(output, "1 to add, 0 to change, 0 to destroy") { if !strings.Contains(output, "1 to add, 0 to change, 0 to destroy") {
t.Fatalf("expected plan summery in output: %s", output) t.Fatalf("expected plan summery in output: %s", output)
} }
if !strings.Contains(output, "1 added, 0 changed, 0 destroyed") { if !run.State.HasResources() {
t.Fatalf("expected apply summery in output: %s", output) t.Fatalf("expected resources in state")
} }
} }
@ -833,8 +833,8 @@ func TestRemote_applyWorkspaceWithoutOperations(t *testing.T) {
if !strings.Contains(output, "1 to add, 0 to change, 0 to destroy") { if !strings.Contains(output, "1 to add, 0 to change, 0 to destroy") {
t.Fatalf("expected plan summery in output: %s", output) t.Fatalf("expected plan summery in output: %s", output)
} }
if !strings.Contains(output, "1 added, 0 changed, 0 destroyed") { if !run.State.HasResources() {
t.Fatalf("expected apply summery in output: %s", output) t.Fatalf("expected resources in state")
} }
} }
@ -1396,13 +1396,22 @@ func TestRemote_applyVersionCheck(t *testing.T) {
} }
output := b.CLI.(*cli.MockUi).OutputWriter.String() output := b.CLI.(*cli.MockUi).OutputWriter.String()
hasRemote := strings.Contains(output, "Running apply in the remote backend") hasRemote := strings.Contains(output, "Running apply in the remote backend")
if !tc.forceLocal && tc.hasOperations && !hasRemote { hasSummary := strings.Contains(output, "1 added, 0 changed, 0 destroyed")
t.Fatalf("missing remote backend header in output: %s", output) hasResources := run.State.HasResources()
} else if (tc.forceLocal || !tc.hasOperations) && hasRemote { if !tc.forceLocal && tc.hasOperations {
t.Fatalf("unexpected remote backend header in output: %s", output) if !hasRemote {
} t.Errorf("missing remote backend header in output: %s", output)
if !strings.Contains(output, "1 added, 0 changed, 0 destroyed") { }
t.Fatalf("expected apply summary in output: %s", output) if !hasSummary {
t.Errorf("expected apply summary in output: %s", output)
}
} else {
if hasRemote {
t.Errorf("unexpected remote backend header in output: %s", output)
}
if !hasResources {
t.Errorf("expected resources in state")
}
} }
} }
}) })

View File

@ -87,6 +87,10 @@ func (c *ApplyCommand) Run(args []string) int {
} }
} }
// Set up our count hook that keeps track of resource changes
countHook := new(CountHook)
c.ExtraHooks = append(c.ExtraHooks, countHook)
// Load the backend // Load the backend
var be backend.Enhanced var be backend.Enhanced
var beDiags tfdiags.Diagnostics var beDiags tfdiags.Diagnostics
@ -172,10 +176,38 @@ func (c *ApplyCommand) Run(args []string) int {
c.showDiagnostics(err) c.showDiagnostics(err)
return 1 return 1
} }
if op.Result != backend.OperationSuccess { if op.Result != backend.OperationSuccess {
return op.Result.ExitStatus() return op.Result.ExitStatus()
} }
// Show the count results from the operation
if c.Destroy {
c.Ui.Output(c.Colorize().Color(fmt.Sprintf(
"[reset][bold][green]\n"+
"Destroy complete! Resources: %d destroyed.",
countHook.Removed)))
} else {
c.Ui.Output(c.Colorize().Color(fmt.Sprintf(
"[reset][bold][green]\n"+
"Apply complete! Resources: %d added, %d changed, %d destroyed.",
countHook.Added,
countHook.Changed,
countHook.Removed)))
}
// only show the state file help message if the state is local.
if (countHook.Added > 0 || countHook.Changed > 0) && c.Meta.stateOutPath != "" {
c.Ui.Output(c.Colorize().Color(fmt.Sprintf(
"[reset]\n"+
"The state of your infrastructure has been saved to the path\n"+
"below. This state is required to modify and destroy your\n"+
"infrastructure, so keep it safe. To inspect the complete state\n"+
"use the `terraform show` command.\n\n"+
"State path: %s",
c.Meta.stateOutPath)))
}
if !c.Destroy { if !c.Destroy {
if outputs := outputsAsString(op.State, true); outputs != "" { if outputs := outputsAsString(op.State, true); outputs != "" {
c.Ui.Output(c.Colorize().Color(outputs)) c.Ui.Output(c.Colorize().Color(outputs))

View File

@ -1,4 +1,4 @@
package local package command
import ( import (
"sync" "sync"

View File

@ -1,4 +1,4 @@
package local package command
import ( import (
"reflect" "reflect"