Merge pull request #27633 from hashicorp/alisdair/count-hook-refactor

cli: Move resource count code to command package
This commit is contained in:
Alisdair McDiarmid 2021-01-29 15:49:34 -05:00 committed by GitHub
commit 5df11f2013
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
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"