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:
parent
bd70df8392
commit
5ca118b4e6
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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()
|
||||||
|
|
||||||
|
|
|
@ -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]]
|
|
||||||
}
|
|
|
@ -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
|
|
||||||
)
|
|
|
@ -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"
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
@ -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))
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
package local
|
package command
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"sync"
|
"sync"
|
|
@ -1,4 +1,4 @@
|
||||||
package local
|
package command
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"reflect"
|
"reflect"
|
Loading…
Reference in New Issue