Merge pull request #27760 from hashicorp/alisdair/command-views-ui-hook

cli: Migrate Terraform UI hook to command views
This commit is contained in:
Alisdair McDiarmid 2021-02-16 09:36:47 -05:00 committed by GitHub
commit 1ae3d30383
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 177 additions and 114 deletions

View File

@ -183,6 +183,10 @@ type Operation struct {
// configuration from ConfigDir.
ConfigLoader *configload.Loader
// Hooks can be used to perform actions triggered by various events during
// the operation's lifecycle.
Hooks []terraform.Hook
// Plan is a plan that was passed as an argument. This is valid for
// plan and apply arguments but may not work for all backends.
PlanFile *planfile.Reader

View File

@ -40,12 +40,7 @@ func (b *Local) opApply(
}
stateHook := new(StateHook)
if b.ContextOpts == nil {
b.ContextOpts = new(terraform.ContextOpts)
}
old := b.ContextOpts.Hooks
defer func() { b.ContextOpts.Hooks = old }()
b.ContextOpts.Hooks = append(b.ContextOpts.Hooks, stateHook)
op.Hooks = append(op.Hooks, stateHook)
// Get our context
tfCtx, _, opState, contextDiags := b.context(op)

View File

@ -77,6 +77,7 @@ func (b *Local) context(op *backend.Operation) (*terraform.Context, *configload.
opts.Destroy = op.Destroy
opts.Targets = op.Targets
opts.UIInput = op.UIIn
opts.Hooks = op.Hooks
opts.SkipRefresh = op.Type != backend.OperationTypeRefresh && !op.PlanRefresh
if opts.SkipRefresh {

View File

@ -8,6 +8,7 @@ import (
"github.com/hashicorp/terraform/command/arguments"
"github.com/hashicorp/terraform/command/views"
"github.com/hashicorp/terraform/plans/planfile"
"github.com/hashicorp/terraform/terraform"
"github.com/hashicorp/terraform/tfdiags"
)
@ -110,7 +111,6 @@ 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
var be backend.Enhanced
@ -171,6 +171,7 @@ func (c *ApplyCommand) Run(args []string) int {
opReq.AutoApprove = autoApprove
opReq.ConfigDir = configPath
opReq.Destroy = c.Destroy
opReq.Hooks = []terraform.Hook{countHook, c.uiHook()}
opReq.PlanFile = planFile
opReq.PlanRefresh = refresh
opReq.ShowDiagnostics = c.showDiagnostics

View File

@ -941,10 +941,12 @@ func TestApply_planNoModuleFiles(t *testing.T) {
p := applyFixtureProvider()
planPath := applyFixturePlanFile(t)
view, _ := testView(t)
apply := &ApplyCommand{
Meta: Meta{
testingOverrides: metaOverridesForProvider(p),
Ui: new(cli.MockUi),
View: view,
},
}
args := []string{

View File

@ -189,6 +189,7 @@ func (c *ImportCommand) Run(args []string) int {
c.showDiagnostics(diags)
return 1
}
opReq.Hooks = []terraform.Hook{c.uiHook()}
{
var moreDiags tfdiags.Diagnostics
opReq.Variables, moreDiags = c.collectVariableValues()

View File

@ -24,10 +24,12 @@ func TestImport(t *testing.T) {
p := testProvider()
ui := new(cli.MockUi)
view, _ := testView(t)
c := &ImportCommand{
Meta: Meta{
testingOverrides: metaOverridesForProvider(p),
Ui: ui,
View: view,
},
}
@ -77,10 +79,12 @@ func TestImport_providerConfig(t *testing.T) {
p := testProvider()
ui := new(cli.MockUi)
view, _ := testView(t)
c := &ImportCommand{
Meta: Meta{
testingOverrides: metaOverridesForProvider(p),
Ui: ui,
View: view,
},
}
@ -170,9 +174,11 @@ func TestImport_remoteState(t *testing.T) {
// init our backend
ui := cli.NewMockUi()
view, _ := testView(t)
m := Meta{
testingOverrides: metaOverridesForProvider(testProvider()),
Ui: ui,
View: view,
ProviderSource: providerSource,
}
@ -192,6 +198,7 @@ func TestImport_remoteState(t *testing.T) {
Meta: Meta{
testingOverrides: metaOverridesForProvider(p),
Ui: ui,
View: view,
},
}
@ -280,9 +287,11 @@ func TestImport_initializationErrorShouldUnlock(t *testing.T) {
// init our backend
ui := cli.NewMockUi()
view, _ := testView(t)
m := Meta{
testingOverrides: metaOverridesForProvider(testProvider()),
Ui: ui,
View: view,
ProviderSource: providerSource,
}
@ -305,6 +314,7 @@ func TestImport_initializationErrorShouldUnlock(t *testing.T) {
Meta: Meta{
testingOverrides: metaOverridesForProvider(p),
Ui: ui,
View: view,
},
}
@ -339,10 +349,12 @@ func TestImport_providerConfigWithVar(t *testing.T) {
p := testProvider()
ui := new(cli.MockUi)
view, _ := testView(t)
c := &ImportCommand{
Meta: Meta{
testingOverrides: metaOverridesForProvider(p),
Ui: ui,
View: view,
},
}
@ -417,10 +429,12 @@ func TestImport_providerConfigWithDataSource(t *testing.T) {
p := testProvider()
ui := new(cli.MockUi)
view, _ := testView(t)
c := &ImportCommand{
Meta: Meta{
testingOverrides: metaOverridesForProvider(p),
Ui: ui,
View: view,
},
}
@ -480,10 +494,12 @@ func TestImport_providerConfigWithVarDefault(t *testing.T) {
p := testProvider()
ui := new(cli.MockUi)
view, _ := testView(t)
c := &ImportCommand{
Meta: Meta{
testingOverrides: metaOverridesForProvider(p),
Ui: ui,
View: view,
},
}
@ -557,10 +573,12 @@ func TestImport_providerConfigWithVarFile(t *testing.T) {
p := testProvider()
ui := new(cli.MockUi)
view, _ := testView(t)
c := &ImportCommand{
Meta: Meta{
testingOverrides: metaOverridesForProvider(p),
Ui: ui,
View: view,
},
}
@ -635,10 +653,12 @@ func TestImport_allowMissingResourceConfig(t *testing.T) {
p := testProvider()
ui := new(cli.MockUi)
view, _ := testView(t)
c := &ImportCommand{
Meta: Meta{
testingOverrides: metaOverridesForProvider(p),
Ui: ui,
View: view,
},
}
@ -690,10 +710,12 @@ func TestImport_emptyConfig(t *testing.T) {
p := testProvider()
ui := new(cli.MockUi)
view, _ := testView(t)
c := &ImportCommand{
Meta: Meta{
testingOverrides: metaOverridesForProvider(p),
Ui: ui,
View: view,
},
}
@ -720,10 +742,12 @@ func TestImport_missingResourceConfig(t *testing.T) {
p := testProvider()
ui := new(cli.MockUi)
view, _ := testView(t)
c := &ImportCommand{
Meta: Meta{
testingOverrides: metaOverridesForProvider(p),
Ui: ui,
View: view,
},
}
@ -750,10 +774,12 @@ func TestImport_missingModuleConfig(t *testing.T) {
p := testProvider()
ui := new(cli.MockUi)
view, _ := testView(t)
c := &ImportCommand{
Meta: Meta{
testingOverrides: metaOverridesForProvider(p),
Ui: ui,
View: view,
},
}
@ -801,9 +827,11 @@ func TestImportModuleVarFile(t *testing.T) {
// init to install the module
ui := new(cli.MockUi)
view, _ := testView(t)
m := Meta{
testingOverrides: metaOverridesForProvider(testProvider()),
Ui: ui,
View: view,
ProviderSource: providerSource,
}
@ -820,6 +848,7 @@ func TestImportModuleVarFile(t *testing.T) {
Meta: Meta{
testingOverrides: metaOverridesForProvider(p),
Ui: ui,
View: view,
},
}
args := []string{
@ -873,9 +902,11 @@ func TestImportModuleInputVariableEvaluation(t *testing.T) {
// init to install the module
ui := new(cli.MockUi)
view, _ := testView(t)
m := Meta{
testingOverrides: metaOverridesForProvider(testProvider()),
Ui: ui,
View: view,
ProviderSource: providerSource,
}
@ -892,6 +923,7 @@ func TestImportModuleInputVariableEvaluation(t *testing.T) {
Meta: Meta{
testingOverrides: metaOverridesForProvider(p),
Ui: ui,
View: view,
},
}
args := []string{
@ -912,10 +944,12 @@ func TestImport_dataResource(t *testing.T) {
p := testProvider()
ui := new(cli.MockUi)
view, _ := testView(t)
c := &ImportCommand{
Meta: Meta{
testingOverrides: metaOverridesForProvider(p),
Ui: ui,
View: view,
},
}
@ -942,10 +976,12 @@ func TestImport_invalidResourceAddr(t *testing.T) {
p := testProvider()
ui := new(cli.MockUi)
view, _ := testView(t)
c := &ImportCommand{
Meta: Meta{
testingOverrides: metaOverridesForProvider(p),
Ui: ui,
View: view,
},
}
@ -972,10 +1008,12 @@ func TestImport_targetIsModule(t *testing.T) {
p := testProvider()
ui := new(cli.MockUi)
view, _ := testView(t)
c := &ImportCommand{
Meta: Meta{
testingOverrides: metaOverridesForProvider(p),
Ui: ui,
View: view,
},
}

View File

@ -21,6 +21,7 @@ import (
"github.com/hashicorp/terraform/addrs"
"github.com/hashicorp/terraform/backend"
"github.com/hashicorp/terraform/backend/local"
"github.com/hashicorp/terraform/command/arguments"
"github.com/hashicorp/terraform/command/format"
"github.com/hashicorp/terraform/command/views"
"github.com/hashicorp/terraform/command/webbrowser"
@ -69,9 +70,6 @@ type Meta struct {
GlobalPluginDirs []string // Additional paths to search for plugins
Ui cli.Ui // Ui for output
// ExtraHooks are extra hooks to add to the context.
ExtraHooks []terraform.Hook
// Services provides access to remote endpoint information for
// "terraform-native' services running at a specific user-facing hostname.
Services *disco.Disco
@ -434,8 +432,6 @@ func (m *Meta) contextOpts() (*terraform.ContextOpts, error) {
}
var opts terraform.ContextOpts
opts.Hooks = []terraform.Hook{m.uiHook()}
opts.Hooks = append(opts.Hooks, m.ExtraHooks...)
opts.Targets = m.targets
opts.UIInput = m.UIInput()
@ -621,15 +617,21 @@ func (m *Meta) process(args []string) []string {
},
}
// Reconfigure the view. This is necessary for commands which use both
// views.View and cli.Ui during the migration phase.
if m.View != nil {
m.View.Configure(&arguments.View{
CompactWarnings: m.compactWarnings,
NoColor: !m.Color,
})
}
return args
}
// uiHook returns the UiHook to use with the context.
func (m *Meta) uiHook() *UiHook {
return &UiHook{
Colorize: m.Colorize(),
Ui: m.Ui,
}
func (m *Meta) uiHook() *views.UiHook {
return views.NewUiHook(m.View)
}
// confirm asks a yes/no confirmation.

View File

@ -7,6 +7,7 @@ import (
"github.com/hashicorp/terraform/backend"
"github.com/hashicorp/terraform/configs"
"github.com/hashicorp/terraform/plans"
"github.com/hashicorp/terraform/terraform"
"github.com/hashicorp/terraform/tfdiags"
)
@ -82,6 +83,7 @@ func (c *PlanCommand) Run(args []string) int {
opReq := c.Operation(b)
opReq.ConfigDir = configPath
opReq.Destroy = destroy
opReq.Hooks = []terraform.Hook{c.uiHook()}
opReq.PlanOutPath = outPath
opReq.PlanRefresh = refresh
opReq.ShowDiagnostics = c.showDiagnostics

View File

@ -32,10 +32,12 @@ func TestPlan(t *testing.T) {
p := planFixtureProvider()
ui := new(cli.MockUi)
view, _ := testView(t)
c := &PlanCommand{
Meta: Meta{
testingOverrides: metaOverridesForProvider(p),
Ui: ui,
View: view,
},
}
@ -59,10 +61,12 @@ func TestPlan_lockedState(t *testing.T) {
p := planFixtureProvider()
ui := new(cli.MockUi)
view, _ := testView(t)
c := &PlanCommand{
Meta: Meta{
testingOverrides: metaOverridesForProvider(p),
Ui: ui,
View: view,
},
}
@ -85,10 +89,12 @@ func TestPlan_plan(t *testing.T) {
p := testProvider()
ui := new(cli.MockUi)
view, _ := testView(t)
c := &PlanCommand{
Meta: Meta{
testingOverrides: metaOverridesForProvider(p),
Ui: ui,
View: view,
},
}
@ -126,10 +132,12 @@ func TestPlan_destroy(t *testing.T) {
p := planFixtureProvider()
ui := new(cli.MockUi)
view, _ := testView(t)
c := &PlanCommand{
Meta: Meta{
testingOverrides: metaOverridesForProvider(p),
Ui: ui,
View: view,
},
}
@ -158,10 +166,12 @@ func TestPlan_noState(t *testing.T) {
p := planFixtureProvider()
ui := new(cli.MockUi)
view, _ := testView(t)
c := &PlanCommand{
Meta: Meta{
testingOverrides: metaOverridesForProvider(p),
Ui: ui,
View: view,
},
}
@ -193,10 +203,12 @@ func TestPlan_outPath(t *testing.T) {
p := planFixtureProvider()
ui := new(cli.MockUi)
view, _ := testView(t)
c := &PlanCommand{
Meta: Meta{
testingOverrides: metaOverridesForProvider(p),
Ui: ui,
View: view,
},
}
@ -246,10 +258,12 @@ func TestPlan_outPathNoChange(t *testing.T) {
p := planFixtureProvider()
ui := new(cli.MockUi)
view, _ := testView(t)
c := &PlanCommand{
Meta: Meta{
testingOverrides: metaOverridesForProvider(p),
Ui: ui,
View: view,
},
}
@ -324,10 +338,12 @@ func TestPlan_outBackend(t *testing.T) {
}
}
ui := cli.NewMockUi()
view, _ := testView(t)
c := &PlanCommand{
Meta: Meta{
testingOverrides: metaOverridesForProvider(p),
Ui: ui,
View: view,
},
}
@ -376,10 +392,12 @@ func TestPlan_refreshFalse(t *testing.T) {
p := planFixtureProvider()
ui := new(cli.MockUi)
view, _ := testView(t)
c := &PlanCommand{
Meta: Meta{
testingOverrides: metaOverridesForProvider(p),
Ui: ui,
View: view,
},
}
@ -407,10 +425,12 @@ func TestPlan_state(t *testing.T) {
p := planFixtureProvider()
ui := new(cli.MockUi)
view, _ := testView(t)
c := &PlanCommand{
Meta: Meta{
testingOverrides: metaOverridesForProvider(p),
Ui: ui,
View: view,
},
}
@ -450,10 +470,12 @@ func TestPlan_stateDefault(t *testing.T) {
p := planFixtureProvider()
ui := new(cli.MockUi)
view, _ := testView(t)
c := &PlanCommand{
Meta: Meta{
testingOverrides: metaOverridesForProvider(p),
Ui: ui,
View: view,
},
}
@ -505,10 +527,12 @@ func TestPlan_validate(t *testing.T) {
}
}
ui := new(cli.MockUi)
view, _ := testView(t)
c := &PlanCommand{
Meta: Meta{
testingOverrides: metaOverridesForProvider(p),
Ui: ui,
View: view,
},
}
@ -532,10 +556,12 @@ func TestPlan_vars(t *testing.T) {
p := planVarsFixtureProvider()
ui := new(cli.MockUi)
view, _ := testView(t)
c := &PlanCommand{
Meta: Meta{
testingOverrides: metaOverridesForProvider(p),
Ui: ui,
View: view,
},
}
@ -577,10 +603,12 @@ func TestPlan_varsUnset(t *testing.T) {
p := planVarsFixtureProvider()
ui := new(cli.MockUi)
view, _ := testView(t)
c := &PlanCommand{
Meta: Meta{
testingOverrides: metaOverridesForProvider(p),
Ui: ui,
View: view,
},
}
@ -640,10 +668,12 @@ func TestPlan_providerArgumentUnset(t *testing.T) {
},
}
ui := new(cli.MockUi)
view, _ := testView(t)
c := &PlanCommand{
Meta: Meta{
testingOverrides: metaOverridesForProvider(p),
Ui: ui,
View: view,
},
}
@ -667,10 +697,12 @@ func TestPlan_varFile(t *testing.T) {
p := planVarsFixtureProvider()
ui := new(cli.MockUi)
view, _ := testView(t)
c := &PlanCommand{
Meta: Meta{
testingOverrides: metaOverridesForProvider(p),
Ui: ui,
View: view,
},
}
@ -707,10 +739,12 @@ func TestPlan_varFileDefault(t *testing.T) {
p := planVarsFixtureProvider()
ui := new(cli.MockUi)
view, _ := testView(t)
c := &PlanCommand{
Meta: Meta{
testingOverrides: metaOverridesForProvider(p),
Ui: ui,
View: view,
},
}
@ -745,10 +779,12 @@ func TestPlan_varFileWithDecls(t *testing.T) {
p := planVarsFixtureProvider()
ui := cli.NewMockUi()
view, _ := testView(t)
c := &PlanCommand{
Meta: Meta{
testingOverrides: metaOverridesForProvider(p),
Ui: ui,
View: view,
},
}
@ -773,10 +809,12 @@ func TestPlan_detailedExitcode(t *testing.T) {
p := planFixtureProvider()
ui := new(cli.MockUi)
view, _ := testView(t)
c := &PlanCommand{
Meta: Meta{
testingOverrides: metaOverridesForProvider(p),
Ui: ui,
View: view,
},
}
@ -794,10 +832,12 @@ func TestPlan_detailedExitcode_emptyDiff(t *testing.T) {
p := testProvider()
ui := new(cli.MockUi)
view, _ := testView(t)
c := &PlanCommand{
Meta: Meta{
testingOverrides: metaOverridesForProvider(p),
Ui: ui,
View: view,
},
}
@ -819,10 +859,12 @@ func TestPlan_shutdown(t *testing.T) {
p := testProvider()
ui := new(cli.MockUi)
view, _ := testView(t)
c := &PlanCommand{
Meta: Meta{
testingOverrides: metaOverridesForProvider(p),
Ui: ui,
View: view,
ShutdownCh: shutdownCh,
},
}
@ -884,10 +926,12 @@ func TestPlan_init_required(t *testing.T) {
defer testChdir(t, td)()
ui := new(cli.MockUi)
view, _ := testView(t)
c := &PlanCommand{
Meta: Meta{
// Running plan without setting testingOverrides is similar to plan without init
Ui: ui,
Ui: ui,
View: view,
},
}
@ -927,10 +971,12 @@ func TestPlan_targeted(t *testing.T) {
}
ui := new(cli.MockUi)
view, _ := testView(t)
c := &PlanCommand{
Meta: Meta{
testingOverrides: metaOverridesForProvider(p),
Ui: ui,
View: view,
},
}
@ -961,9 +1007,11 @@ func TestPlan_targetFlagsDiags(t *testing.T) {
defer testChdir(t, td)()
ui := new(cli.MockUi)
view, _ := testView(t)
c := &PlanCommand{
Meta: Meta{
Ui: ui,
Ui: ui,
View: view,
},
}

View File

@ -7,6 +7,7 @@ import (
"github.com/hashicorp/terraform/backend"
"github.com/hashicorp/terraform/command/arguments"
"github.com/hashicorp/terraform/command/views"
"github.com/hashicorp/terraform/terraform"
"github.com/hashicorp/terraform/tfdiags"
)
@ -75,6 +76,7 @@ func (c *RefreshCommand) Run(args []string) int {
// Build the operation
opReq := c.Operation(b)
opReq.ConfigDir = configPath
opReq.Hooks = []terraform.Hook{c.uiHook()}
opReq.ShowDiagnostics = c.showDiagnostics
opReq.Type = backend.OperationTypeRefresh

View File

@ -791,7 +791,7 @@ func TestRefresh_targeted(t *testing.T) {
}
ui := new(cli.MockUi)
view, _ := testView(t)
view, done := testView(t)
c := &RefreshCommand{
Meta: Meta{
testingOverrides: metaOverridesForProvider(p),
@ -808,7 +808,7 @@ func TestRefresh_targeted(t *testing.T) {
t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String())
}
got := ui.OutputWriter.String()
got := done(t).Stdout()
if want := "test_instance.foo: Refreshing"; !strings.Contains(got, want) {
t.Fatalf("expected output to contain %q, got:\n%s", want, got)
}

View File

@ -255,9 +255,11 @@ func TestShow_json_output(t *testing.T) {
p := showFixtureProvider()
ui := new(cli.MockUi)
view, _ := testView(t)
m := Meta{
testingOverrides: metaOverridesForProvider(p),
Ui: ui,
View: view,
ProviderSource: providerSource,
}

View File

@ -1,4 +1,4 @@
package command
package views
import (
"bufio"
@ -9,8 +9,6 @@ import (
"time"
"unicode"
"github.com/mitchellh/cli"
"github.com/mitchellh/colorstring"
"github.com/zclconf/go-cty/cty"
"github.com/hashicorp/terraform/addrs"
@ -24,17 +22,24 @@ import (
const defaultPeriodicUiTimer = 10 * time.Second
const maxIdLen = 80
func NewUiHook(view *View) *UiHook {
return &UiHook{
view: view,
periodicUiTimer: defaultPeriodicUiTimer,
resources: make(map[string]uiResourceState),
}
}
type UiHook struct {
terraform.NilHook
Colorize *colorstring.Colorize
Ui cli.Ui
PeriodicUiTimer time.Duration
view *View
viewLock sync.Mutex
l sync.Mutex
once sync.Once
resources map[string]uiResourceState
ui cli.Ui
periodicUiTimer time.Duration
resources map[string]uiResourceState
resourcesLock sync.Mutex
}
var _ terraform.Hook = (*UiHook)(nil)
@ -63,8 +68,6 @@ const (
)
func (h *UiHook) PreApply(addr addrs.AbsResourceInstance, gen states.Generation, action plans.Action, priorState, plannedNewState cty.Value) (terraform.HookAction, error) {
h.once.Do(h.init)
dispAddr := addr.String()
if gen != states.CurrentGen {
dispAddr = fmt.Sprintf("%s (%s)", dispAddr, gen)
@ -89,7 +92,7 @@ func (h *UiHook) PreApply(addr addrs.AbsResourceInstance, gen states.Generation,
default:
// We don't expect any other actions in here, so anything else is a
// bug in the caller but we'll ignore it in order to be robust.
h.ui.Output(fmt.Sprintf("(Unknown action %s for %s)", action, dispAddr))
h.println(fmt.Sprintf("(Unknown action %s for %s)", action, dispAddr))
return terraform.HookActionContinue, nil
}
@ -103,7 +106,7 @@ func (h *UiHook) PreApply(addr addrs.AbsResourceInstance, gen states.Generation,
idValue = ""
}
h.ui.Output(h.Colorize.Color(fmt.Sprintf(
h.println(h.view.colorize.Color(fmt.Sprintf(
"[reset][bold]%s: %s%s[reset]",
dispAddr,
operation,
@ -121,9 +124,9 @@ func (h *UiHook) PreApply(addr addrs.AbsResourceInstance, gen states.Generation,
done: make(chan struct{}),
}
h.l.Lock()
h.resourcesLock.Lock()
h.resources[key] = uiState
h.l.Unlock()
h.resourcesLock.Unlock()
// Start goroutine that shows progress
go h.stillApplying(uiState)
@ -138,7 +141,7 @@ func (h *UiHook) stillApplying(state uiResourceState) {
case <-state.DoneCh:
return
case <-time.After(h.PeriodicUiTimer):
case <-time.After(h.periodicUiTimer):
// Timer up, show status
}
@ -161,7 +164,7 @@ func (h *UiHook) stillApplying(state uiResourceState) {
idSuffix = fmt.Sprintf("%s=%s, ", state.IDKey, truncateId(state.IDValue, maxIdLen))
}
h.ui.Output(h.Colorize.Color(fmt.Sprintf(
h.println(h.view.colorize.Color(fmt.Sprintf(
"[reset][bold]%s: %s [%s%s elapsed][reset]",
state.DispAddr,
msg,
@ -172,17 +175,16 @@ func (h *UiHook) stillApplying(state uiResourceState) {
}
func (h *UiHook) PostApply(addr addrs.AbsResourceInstance, gen states.Generation, newState cty.Value, applyerr error) (terraform.HookAction, error) {
id := addr.String()
h.l.Lock()
h.resourcesLock.Lock()
state := h.resources[id]
if state.DoneCh != nil {
close(state.DoneCh)
}
delete(h.resources, id)
h.l.Unlock()
h.resourcesLock.Unlock()
var stateIdSuffix string
if k, v := format.ObjectValueID(newState); k != "" && v != "" {
@ -208,21 +210,17 @@ func (h *UiHook) PostApply(addr addrs.AbsResourceInstance, gen states.Generation
return terraform.HookActionContinue, nil
}
colorized := h.Colorize.Color(fmt.Sprintf(
colorized := h.view.colorize.Color(fmt.Sprintf(
"[reset][bold]%s: %s after %s%s[reset]",
addr, msg, time.Now().Round(time.Second).Sub(state.Start), stateIdSuffix))
h.ui.Output(colorized)
h.println(colorized)
return terraform.HookActionContinue, nil
}
func (h *UiHook) PreDiff(addr addrs.AbsResourceInstance, gen states.Generation, priorState, proposedNewState cty.Value) (terraform.HookAction, error) {
return terraform.HookActionContinue, nil
}
func (h *UiHook) PreProvisionInstanceStep(addr addrs.AbsResourceInstance, typeName string) (terraform.HookAction, error) {
h.ui.Output(h.Colorize.Color(fmt.Sprintf(
h.println(h.view.colorize.Color(fmt.Sprintf(
"[reset][bold]%s: Provisioning with '%s'...[reset]",
addr, typeName,
)))
@ -231,7 +229,7 @@ func (h *UiHook) PreProvisionInstanceStep(addr addrs.AbsResourceInstance, typeNa
func (h *UiHook) ProvisionOutput(addr addrs.AbsResourceInstance, typeName string, msg string) {
var buf bytes.Buffer
buf.WriteString(h.Colorize.Color("[reset]"))
buf.WriteString(h.view.colorize.Color("[reset]"))
prefix := fmt.Sprintf("%s (%s): ", addr, typeName)
s := bufio.NewScanner(strings.NewReader(msg))
@ -243,27 +241,23 @@ func (h *UiHook) ProvisionOutput(addr addrs.AbsResourceInstance, typeName string
}
}
h.ui.Output(strings.TrimSpace(buf.String()))
h.println(strings.TrimSpace(buf.String()))
}
func (h *UiHook) PreRefresh(addr addrs.AbsResourceInstance, gen states.Generation, priorState cty.Value) (terraform.HookAction, error) {
h.once.Do(h.init)
var stateIdSuffix string
if k, v := format.ObjectValueID(priorState); k != "" && v != "" {
stateIdSuffix = fmt.Sprintf(" [%s=%s]", k, v)
}
h.ui.Output(h.Colorize.Color(fmt.Sprintf(
h.println(h.view.colorize.Color(fmt.Sprintf(
"[reset][bold]%s: Refreshing state...%s",
addr, stateIdSuffix)))
return terraform.HookActionContinue, nil
}
func (h *UiHook) PreImportState(addr addrs.AbsResourceInstance, importID string) (terraform.HookAction, error) {
h.once.Do(h.init)
h.ui.Output(h.Colorize.Color(fmt.Sprintf(
h.println(h.view.colorize.Color(fmt.Sprintf(
"[reset][bold]%s: Importing from ID %q...",
addr, importID,
)))
@ -271,12 +265,10 @@ func (h *UiHook) PreImportState(addr addrs.AbsResourceInstance, importID string)
}
func (h *UiHook) PostImportState(addr addrs.AbsResourceInstance, imported []providers.ImportedResource) (terraform.HookAction, error) {
h.once.Do(h.init)
h.ui.Output(h.Colorize.Color(fmt.Sprintf(
h.println(h.view.colorize.Color(fmt.Sprintf(
"[reset][bold][green]%s: Import prepared!", addr)))
for _, s := range imported {
h.ui.Output(h.Colorize.Color(fmt.Sprintf(
h.println(h.view.colorize.Color(fmt.Sprintf(
"[reset][green] Prepared %s for import",
s.TypeName,
)))
@ -285,19 +277,11 @@ func (h *UiHook) PostImportState(addr addrs.AbsResourceInstance, imported []prov
return terraform.HookActionContinue, nil
}
func (h *UiHook) init() {
if h.Colorize == nil {
panic("colorize not given")
}
if h.PeriodicUiTimer == 0 {
h.PeriodicUiTimer = defaultPeriodicUiTimer
}
h.resources = make(map[string]uiResourceState)
// Wrap the ui so that it is safe for concurrency regardless of the
// underlying reader/writer that is in place.
h.ui = &cli.ConcurrentUi{Ui: h.Ui}
// Wrap calls to the view so that concurrent calls do not interleave println.
func (h *UiHook) println(s string) {
h.viewLock.Lock()
defer h.viewLock.Unlock()
h.view.streams.Println(s)
}
// scanLines is basically copied from the Go standard library except

View File

@ -1,4 +1,4 @@
package command
package views
import (
"fmt"
@ -6,28 +6,20 @@ import (
"testing"
"time"
"github.com/mitchellh/cli"
"github.com/mitchellh/colorstring"
"github.com/zclconf/go-cty/cty"
"github.com/hashicorp/terraform/addrs"
"github.com/hashicorp/terraform/internal/terminal"
"github.com/hashicorp/terraform/plans"
"github.com/hashicorp/terraform/states"
"github.com/hashicorp/terraform/terraform"
)
func TestUiHookPreApply_periodicTimer(t *testing.T) {
ui := cli.NewMockUi()
h := &UiHook{
Colorize: &colorstring.Colorize{
Colors: colorstring.DefaultColors,
Disable: true,
Reset: true,
},
Ui: ui,
PeriodicUiTimer: 1 * time.Second,
}
h.init()
streams, done := terminal.StreamsForTesting(t)
view := NewView(streams)
h := NewUiHook(view)
h.periodicUiTimer = 1 * time.Second
h.resources = map[string]uiResourceState{
"data.aws_availability_zones.available": uiResourceState{
Op: uiResourceDestroy,
@ -75,29 +67,23 @@ data.aws_availability_zones.available: Still destroying... [id=2017-03-05 10:56:
data.aws_availability_zones.available: Still destroying... [id=2017-03-05 10:56:59.298784526 +0000 UTC, 2s elapsed]
data.aws_availability_zones.available: Still destroying... [id=2017-03-05 10:56:59.298784526 +0000 UTC, 3s elapsed]
`
output := ui.OutputWriter.String()
result := done(t)
output := result.Stdout()
if output != expectedOutput {
t.Fatalf("Output didn't match.\nExpected: %q\nGiven: %q", expectedOutput, output)
}
expectedErrOutput := ""
errOutput := ui.ErrorWriter.String()
errOutput := result.Stderr()
if errOutput != expectedErrOutput {
t.Fatalf("Error output didn't match.\nExpected: %q\nGiven: %q", expectedErrOutput, errOutput)
}
}
func TestUiHookPreApply_destroy(t *testing.T) {
ui := cli.NewMockUi()
h := &UiHook{
Colorize: &colorstring.Colorize{
Colors: colorstring.DefaultColors,
Disable: true,
Reset: true,
},
Ui: ui,
}
h.init()
streams, done := terminal.StreamsForTesting(t)
view := NewView(streams)
h := NewUiHook(view)
h.resources = map[string]uiResourceState{
"data.aws_availability_zones.available": uiResourceState{
Op: uiResourceDestroy,
@ -138,30 +124,24 @@ func TestUiHookPreApply_destroy(t *testing.T) {
close(uiState.DoneCh)
<-uiState.done
result := done(t)
expectedOutput := "data.aws_availability_zones.available: Destroying... [id=2017-03-05 10:56:59.298784526 +0000 UTC]\n"
output := ui.OutputWriter.String()
output := result.Stdout()
if output != expectedOutput {
t.Fatalf("Output didn't match.\nExpected: %q\nGiven: %q", expectedOutput, output)
}
expectedErrOutput := ""
errOutput := ui.ErrorWriter.String()
errOutput := result.Stderr()
if errOutput != expectedErrOutput {
t.Fatalf("Error output didn't match.\nExpected: %q\nGiven: %q", expectedErrOutput, errOutput)
}
}
func TestUiHookPostApply_emptyState(t *testing.T) {
ui := cli.NewMockUi()
h := &UiHook{
Colorize: &colorstring.Colorize{
Colors: colorstring.DefaultColors,
Disable: true,
Reset: true,
},
Ui: ui,
}
h.init()
streams, done := terminal.StreamsForTesting(t)
view := NewView(streams)
h := NewUiHook(view)
h.resources = map[string]uiResourceState{
"data.google_compute_zones.available": uiResourceState{
Op: uiResourceDestroy,
@ -187,15 +167,16 @@ func TestUiHookPostApply_emptyState(t *testing.T) {
if action != terraform.HookActionContinue {
t.Fatalf("Expected hook to continue, given: %#v", action)
}
result := done(t)
expectedRegexp := "^data.google_compute_zones.available: Destruction complete after -?[a-z0-9µ.]+\n$"
output := ui.OutputWriter.String()
output := result.Stdout()
if matched, _ := regexp.MatchString(expectedRegexp, output); !matched {
t.Fatalf("Output didn't match regexp.\nExpected: %q\nGiven: %q", expectedRegexp, output)
}
expectedErrOutput := ""
errOutput := ui.ErrorWriter.String()
errOutput := result.Stderr()
if errOutput != expectedErrOutput {
t.Fatalf("Error output didn't match.\nExpected: %q\nGiven: %q", expectedErrOutput, errOutput)
}