From 2d1976bbda89b4b4a9c4a1cc8c977fc6137f3428 Mon Sep 17 00:00:00 2001 From: Alisdair McDiarmid Date: Tue, 16 Feb 2021 07:19:22 -0500 Subject: [PATCH] clistate: Update clistate.Locker for command views The clistate package includes a Locker interface which provides a simple way for the local backend to lock and unlock state, while providing feedback to the user if there is a delay while waiting for the lock. Prior to this commit, the backend was responsible for initializing the Locker, passing through direct access to the cli.Ui instance. This structure prevented commands from implementing different implementations of the state locker UI. In this commit, we: - Move the responsibility of creating the appropriate Locker to the source of the Operation; - Add the ability to set the context for a Locker via a WithContext method; - Replace the Locker's cli.Ui and Colorize members with a StateLocker view; - Implement views.StateLocker for human-readable UI; - Update the Locker interface to return detailed diagnostics instead of errors, reducing its direct interactions with UI; - Add a Timeout() method on Locker to allow the remote backend to continue to misuse the -lock-timeout flag to cancel pending runs. When an Operation is created, the StateLocker field must now be populated with an implementation of Locker. For situations where locking is disabled, this can be a no-op locker. This change has no significant effect on the operation of Terraform, with the exception of slightly different formatting of errors when state locking or unlocking fails. --- backend/backend.go | 13 +-- backend/local/backend.go | 7 +- backend/local/backend_apply.go | 6 +- backend/local/backend_apply_test.go | 2 + backend/local/backend_local.go | 15 +-- backend/local/backend_local_test.go | 17 +++- backend/local/backend_plan.go | 6 +- backend/local/backend_plan_test.go | 2 + backend/local/backend_refresh.go | 6 +- backend/local/backend_refresh_test.go | 3 +- backend/remote/backend_apply_test.go | 17 +++- backend/remote/backend_context.go | 15 +-- backend/remote/backend_context_test.go | 9 +- backend/remote/backend_plan.go | 7 +- backend/remote/backend_plan_test.go | 17 +++- command/apply_test.go | 2 + command/clistate/state.go | 126 ++++++++++++++----------- command/clistate/state_test.go | 17 ++-- command/console.go | 6 +- command/console_test.go | 10 ++ command/import.go | 6 +- command/import_test.go | 38 ++++++++ command/init_test.go | 86 +++++++++++++++++ command/meta_backend.go | 33 ++++--- command/meta_backend_migrate.go | 23 +++-- command/meta_backend_test.go | 2 + command/plan_test.go | 52 +++++++++- command/show_test.go | 20 ++++ command/state_mv.go | 27 ++++-- command/state_mv_test.go | 39 ++++++++ command/state_push.go | 15 ++- command/state_push_test.go | 21 ++++- command/state_replace_provider.go | 15 ++- command/state_replace_provider_test.go | 28 ++++-- command/state_rm.go | 15 ++- command/state_rm_test.go | 16 ++++ command/taint.go | 15 ++- command/taint_test.go | 45 ++++++--- command/unlock_test.go | 12 ++- command/untaint.go | 15 ++- command/untaint_test.go | 43 ++++++--- command/views/state_locker.go | 40 ++++++++ command/workspace_command_test.go | 40 +++++--- command/workspace_delete.go | 15 +-- command/workspace_new.go | 15 ++- 45 files changed, 732 insertions(+), 247 deletions(-) create mode 100644 command/views/state_locker.go diff --git a/backend/backend.go b/backend/backend.go index 73d529ca1..4d92e0bcb 100644 --- a/backend/backend.go +++ b/backend/backend.go @@ -10,7 +10,6 @@ import ( "io/ioutil" "log" "os" - "time" "github.com/hashicorp/terraform/addrs" "github.com/hashicorp/terraform/command/clistate" @@ -212,17 +211,13 @@ type Operation struct { // ShowDiagnostics prints diagnostic messages to the UI. ShowDiagnostics func(vals ...interface{}) - // If LockState is true, the Operation must Lock any - // statemgr.Lockers for its duration, and Unlock when complete. - LockState bool - // StateLocker is used to lock the state while providing UI feedback to the - // user. This will be supplied by the Backend itself. + // user. This will be replaced by the Backend to update the context. + // + // If state locking is not necessary, this should be set to a no-op + // implementation of clistate.Locker. StateLocker clistate.Locker - // The duration to retry obtaining a State lock. - StateLockTimeout time.Duration - // Workspace is the name of the workspace that this operation should run // in, which controls which named state is used. Workspace string diff --git a/backend/local/backend.go b/backend/local/backend.go index a82013b51..279be8691 100644 --- a/backend/local/backend.go +++ b/backend/local/backend.go @@ -12,7 +12,6 @@ import ( "sync" "github.com/hashicorp/terraform/backend" - "github.com/hashicorp/terraform/command/clistate" "github.com/hashicorp/terraform/configs/configschema" "github.com/hashicorp/terraform/internal/terminal" "github.com/hashicorp/terraform/states/statemgr" @@ -326,11 +325,7 @@ func (b *Local) Operation(ctx context.Context, op *backend.Operation) (*backend. cancelCtx, cancel := context.WithCancel(context.Background()) runningOp.Cancel = cancel - if op.LockState { - op.StateLocker = clistate.NewLocker(stopCtx, op.StateLockTimeout, b.CLI, b.Colorize()) - } else { - op.StateLocker = clistate.NewNoopLocker() - } + op.StateLocker = op.StateLocker.WithContext(stopCtx) // Do it go func() { diff --git a/backend/local/backend_apply.go b/backend/local/backend_apply.go index 936f9fe99..32219db90 100644 --- a/backend/local/backend_apply.go +++ b/backend/local/backend_apply.go @@ -57,9 +57,9 @@ func (b *Local) opApply( // the state was locked during succesfull context creation; unlock the state // when the operation completes defer func() { - err := op.StateLocker.Unlock(nil) - if err != nil { - op.ShowDiagnostics(err) + diags := op.StateLocker.Unlock() + if diags.HasErrors() { + op.ShowDiagnostics(diags) runningOp.Result = backend.OperationFailure } }() diff --git a/backend/local/backend_apply_test.go b/backend/local/backend_apply_test.go index 1fe762248..61a075073 100644 --- a/backend/local/backend_apply_test.go +++ b/backend/local/backend_apply_test.go @@ -13,6 +13,7 @@ import ( "github.com/zclconf/go-cty/cty" "github.com/hashicorp/terraform/backend" + "github.com/hashicorp/terraform/command/clistate" "github.com/hashicorp/terraform/configs/configschema" "github.com/hashicorp/terraform/internal/initwd" "github.com/hashicorp/terraform/providers" @@ -293,6 +294,7 @@ func testOperationApply(t *testing.T, configDir string) (*backend.Operation, fun ConfigDir: configDir, ConfigLoader: configLoader, ShowDiagnostics: testLogDiagnostics(t), + StateLocker: clistate.NewNoopLocker(), }, configCleanup } diff --git a/backend/local/backend_local.go b/backend/local/backend_local.go index 1b67939af..9cdbebe01 100644 --- a/backend/local/backend_local.go +++ b/backend/local/backend_local.go @@ -8,7 +8,6 @@ import ( "github.com/hashicorp/errwrap" "github.com/hashicorp/terraform/backend" - "github.com/hashicorp/terraform/command/clistate" "github.com/hashicorp/terraform/configs" "github.com/hashicorp/terraform/configs/configload" "github.com/hashicorp/terraform/plans/planfile" @@ -24,11 +23,7 @@ func (b *Local) Context(op *backend.Operation) (*terraform.Context, statemgr.Ful // to ask for input/validate. op.Type = backend.OperationTypeInvalid - if op.LockState { - op.StateLocker = clistate.NewLocker(context.Background(), op.StateLockTimeout, b.CLI, b.Colorize()) - } else { - op.StateLocker = clistate.NewNoopLocker() - } + op.StateLocker = op.StateLocker.WithContext(context.Background()) ctx, _, stateMgr, diags := b.context(op) return ctx, stateMgr, diags @@ -45,8 +40,7 @@ func (b *Local) context(op *backend.Operation) (*terraform.Context, *configload. return nil, nil, nil, diags } log.Printf("[TRACE] backend/local: requesting state lock for workspace %q", op.Workspace) - if err := op.StateLocker.Lock(s, op.Type.String()); err != nil { - diags = diags.Append(errwrap.Wrapf("Error locking state: {{err}}", err)) + if diags := op.StateLocker.Lock(s, op.Type.String()); diags.HasErrors() { return nil, nil, nil, diags } @@ -54,10 +48,7 @@ func (b *Local) context(op *backend.Operation) (*terraform.Context, *configload. // If we're returning with errors, and thus not producing a valid // context, we'll want to avoid leaving the workspace locked. if diags.HasErrors() { - err := op.StateLocker.Unlock(nil) - if err != nil { - diags = diags.Append(errwrap.Wrapf("Error unlocking state: {{err}}", err)) - } + diags = diags.Append(op.StateLocker.Unlock()) } }() diff --git a/backend/local/backend_local_test.go b/backend/local/backend_local_test.go index 3354b709d..97f18129d 100644 --- a/backend/local/backend_local_test.go +++ b/backend/local/backend_local_test.go @@ -4,7 +4,11 @@ import ( "testing" "github.com/hashicorp/terraform/backend" + "github.com/hashicorp/terraform/command/arguments" + "github.com/hashicorp/terraform/command/clistate" + "github.com/hashicorp/terraform/command/views" "github.com/hashicorp/terraform/internal/initwd" + "github.com/hashicorp/terraform/internal/terminal" ) func TestLocalContext(t *testing.T) { @@ -15,11 +19,15 @@ func TestLocalContext(t *testing.T) { _, configLoader, configCleanup := initwd.MustLoadConfigForTests(t, configDir) defer configCleanup() + streams, _ := terminal.StreamsForTesting(t) + view := views.NewView(streams) + stateLocker := clistate.NewLocker(0, views.NewStateLocker(arguments.ViewHuman, view)) + op := &backend.Operation{ ConfigDir: configDir, ConfigLoader: configLoader, Workspace: backend.DefaultStateName, - LockState: true, + StateLocker: stateLocker, } _, _, diags := b.Context(op) @@ -39,11 +47,15 @@ func TestLocalContext_error(t *testing.T) { _, configLoader, configCleanup := initwd.MustLoadConfigForTests(t, configDir) defer configCleanup() + streams, _ := terminal.StreamsForTesting(t) + view := views.NewView(streams) + stateLocker := clistate.NewLocker(0, views.NewStateLocker(arguments.ViewHuman, view)) + op := &backend.Operation{ ConfigDir: configDir, ConfigLoader: configLoader, Workspace: backend.DefaultStateName, - LockState: true, + StateLocker: stateLocker, } _, _, diags := b.Context(op) @@ -53,5 +65,4 @@ func TestLocalContext_error(t *testing.T) { // Context() unlocks the state on failure assertBackendStateUnlocked(t, b) - } diff --git a/backend/local/backend_plan.go b/backend/local/backend_plan.go index 50ea3420b..0be65a645 100644 --- a/backend/local/backend_plan.go +++ b/backend/local/backend_plan.go @@ -73,9 +73,9 @@ func (b *Local) opPlan( // the state was locked during succesfull context creation; unlock the state // when the operation completes defer func() { - err := op.StateLocker.Unlock(nil) - if err != nil { - op.ShowDiagnostics(err) + diags := op.StateLocker.Unlock() + if diags.HasErrors() { + op.ShowDiagnostics(diags) runningOp.Result = backend.OperationFailure } }() diff --git a/backend/local/backend_plan_test.go b/backend/local/backend_plan_test.go index 5e7b06ead..690e4873d 100644 --- a/backend/local/backend_plan_test.go +++ b/backend/local/backend_plan_test.go @@ -9,6 +9,7 @@ import ( "github.com/hashicorp/terraform/addrs" "github.com/hashicorp/terraform/backend" + "github.com/hashicorp/terraform/command/clistate" "github.com/hashicorp/terraform/configs/configschema" "github.com/hashicorp/terraform/internal/initwd" "github.com/hashicorp/terraform/plans" @@ -740,6 +741,7 @@ func testOperationPlan(t *testing.T, configDir string) (*backend.Operation, func ConfigDir: configDir, ConfigLoader: configLoader, ShowDiagnostics: testLogDiagnostics(t), + StateLocker: clistate.NewNoopLocker(), }, configCleanup } diff --git a/backend/local/backend_refresh.go b/backend/local/backend_refresh.go index 83ce3ef31..e94d153ca 100644 --- a/backend/local/backend_refresh.go +++ b/backend/local/backend_refresh.go @@ -56,9 +56,9 @@ func (b *Local) opRefresh( // the state was locked during succesfull context creation; unlock the state // when the operation completes defer func() { - err := op.StateLocker.Unlock(nil) - if err != nil { - op.ShowDiagnostics(err) + diags := op.StateLocker.Unlock() + if diags.HasErrors() { + op.ShowDiagnostics(diags) runningOp.Result = backend.OperationFailure } }() diff --git a/backend/local/backend_refresh_test.go b/backend/local/backend_refresh_test.go index 14210f790..43005284e 100644 --- a/backend/local/backend_refresh_test.go +++ b/backend/local/backend_refresh_test.go @@ -7,6 +7,7 @@ import ( "github.com/hashicorp/terraform/addrs" "github.com/hashicorp/terraform/backend" + "github.com/hashicorp/terraform/command/clistate" "github.com/hashicorp/terraform/configs/configschema" "github.com/hashicorp/terraform/internal/initwd" "github.com/hashicorp/terraform/providers" @@ -223,8 +224,8 @@ func testOperationRefresh(t *testing.T, configDir string) (*backend.Operation, f Type: backend.OperationTypeRefresh, ConfigDir: configDir, ConfigLoader: configLoader, - LockState: true, ShowDiagnostics: testLogDiagnostics(t), + StateLocker: clistate.NewNoopLocker(), }, configCleanup } diff --git a/backend/remote/backend_apply_test.go b/backend/remote/backend_apply_test.go index 54e3afd74..1665271d9 100644 --- a/backend/remote/backend_apply_test.go +++ b/backend/remote/backend_apply_test.go @@ -14,7 +14,11 @@ import ( version "github.com/hashicorp/go-version" "github.com/hashicorp/terraform/addrs" "github.com/hashicorp/terraform/backend" + "github.com/hashicorp/terraform/command/arguments" + "github.com/hashicorp/terraform/command/clistate" + "github.com/hashicorp/terraform/command/views" "github.com/hashicorp/terraform/internal/initwd" + "github.com/hashicorp/terraform/internal/terminal" "github.com/hashicorp/terraform/plans/planfile" "github.com/hashicorp/terraform/states/statemgr" "github.com/hashicorp/terraform/terraform" @@ -26,14 +30,24 @@ import ( func testOperationApply(t *testing.T, configDir string) (*backend.Operation, func()) { t.Helper() + return testOperationApplyWithTimeout(t, configDir, 0) +} + +func testOperationApplyWithTimeout(t *testing.T, configDir string, timeout time.Duration) (*backend.Operation, func()) { + t.Helper() + _, configLoader, configCleanup := initwd.MustLoadConfigForTests(t, configDir) + streams, _ := terminal.StreamsForTesting(t) + view := views.NewStateLocker(arguments.ViewHuman, views.NewView(streams)) + return &backend.Operation{ ConfigDir: configDir, ConfigLoader: configLoader, Parallelism: defaultParallelism, PlanRefresh: true, ShowDiagnostics: testLogDiagnostics(t), + StateLocker: clistate.NewLocker(timeout, view), Type: backend.OperationTypeApply, }, configCleanup } @@ -878,7 +892,7 @@ func TestRemote_applyLockTimeout(t *testing.T) { t.Fatalf("error creating pending run: %v", err) } - op, configCleanup := testOperationApply(t, "./testdata/apply") + op, configCleanup := testOperationApplyWithTimeout(t, "./testdata/apply", 50*time.Millisecond) defer configCleanup() input := testInput(t, map[string]string{ @@ -886,7 +900,6 @@ func TestRemote_applyLockTimeout(t *testing.T) { "approve": "yes", }) - op.StateLockTimeout = 50 * time.Millisecond op.UIIn = input op.UIOut = b.CLI op.Workspace = backend.DefaultStateName diff --git a/backend/remote/backend_context.go b/backend/remote/backend_context.go index 577c92d92..1c75a4ba6 100644 --- a/backend/remote/backend_context.go +++ b/backend/remote/backend_context.go @@ -11,7 +11,6 @@ import ( "github.com/hashicorp/hcl/v2" "github.com/hashicorp/hcl/v2/hclsyntax" "github.com/hashicorp/terraform/backend" - "github.com/hashicorp/terraform/command/clistate" "github.com/hashicorp/terraform/configs" "github.com/hashicorp/terraform/states/statemgr" "github.com/hashicorp/terraform/terraform" @@ -23,11 +22,7 @@ import ( func (b *Remote) Context(op *backend.Operation) (*terraform.Context, statemgr.Full, tfdiags.Diagnostics) { var diags tfdiags.Diagnostics - if op.LockState { - op.StateLocker = clistate.NewLocker(context.Background(), op.StateLockTimeout, b.CLI, b.cliColorize()) - } else { - op.StateLocker = clistate.NewNoopLocker() - } + op.StateLocker = op.StateLocker.WithContext(context.Background()) // Get the remote workspace name. remoteWorkspaceName := b.getRemoteWorkspaceName(op.Workspace) @@ -41,8 +36,7 @@ func (b *Remote) Context(op *backend.Operation) (*terraform.Context, statemgr.Fu } log.Printf("[TRACE] backend/remote: requesting state lock for workspace %q", remoteWorkspaceName) - if err := op.StateLocker.Lock(stateMgr, op.Type.String()); err != nil { - diags = diags.Append(errwrap.Wrapf("Error locking state: {{err}}", err)) + if diags := op.StateLocker.Lock(stateMgr, op.Type.String()); diags.HasErrors() { return nil, nil, diags } @@ -50,10 +44,7 @@ func (b *Remote) Context(op *backend.Operation) (*terraform.Context, statemgr.Fu // If we're returning with errors, and thus not producing a valid // context, we'll want to avoid leaving the remote workspace locked. if diags.HasErrors() { - err := op.StateLocker.Unlock(nil) - if err != nil { - diags = diags.Append(errwrap.Wrapf("Error unlocking state: {{err}}", err)) - } + diags = diags.Append(op.StateLocker.Unlock()) } }() diff --git a/backend/remote/backend_context_test.go b/backend/remote/backend_context_test.go index 1a214deb9..3fa61f142 100644 --- a/backend/remote/backend_context_test.go +++ b/backend/remote/backend_context_test.go @@ -6,8 +6,12 @@ import ( tfe "github.com/hashicorp/go-tfe" "github.com/hashicorp/terraform/backend" + "github.com/hashicorp/terraform/command/arguments" + "github.com/hashicorp/terraform/command/clistate" + "github.com/hashicorp/terraform/command/views" "github.com/hashicorp/terraform/configs" "github.com/hashicorp/terraform/internal/initwd" + "github.com/hashicorp/terraform/internal/terminal" "github.com/hashicorp/terraform/states/statemgr" "github.com/zclconf/go-cty/cty" ) @@ -183,11 +187,14 @@ func TestRemoteContextWithVars(t *testing.T) { t.Fatal(err) } + streams, _ := terminal.StreamsForTesting(t) + view := views.NewStateLocker(arguments.ViewHuman, views.NewView(streams)) + op := &backend.Operation{ ConfigDir: configDir, ConfigLoader: configLoader, + StateLocker: clistate.NewLocker(0, view), Workspace: backend.DefaultStateName, - LockState: true, } v := test.Opts diff --git a/backend/remote/backend_plan.go b/backend/remote/backend_plan.go index e42ec9576..6e016b7fc 100644 --- a/backend/remote/backend_plan.go +++ b/backend/remote/backend_plan.go @@ -260,15 +260,16 @@ in order to capture the filesystem context the remote workspace expects: return r, generalError("Failed to create run", err) } - // When the lock timeout is set, - if op.StateLockTimeout > 0 { + // When the lock timeout is set, if the run is still pending and + // cancellable after that period, we attempt to cancel it. + if lockTimeout := op.StateLocker.Timeout(); lockTimeout > 0 { go func() { select { case <-stopCtx.Done(): return case <-cancelCtx.Done(): return - case <-time.After(op.StateLockTimeout): + case <-time.After(lockTimeout): // Retrieve the run to get its current status. r, err := b.client.Runs.Read(cancelCtx, r.ID) if err != nil { diff --git a/backend/remote/backend_plan_test.go b/backend/remote/backend_plan_test.go index f93972770..d273c46d1 100644 --- a/backend/remote/backend_plan_test.go +++ b/backend/remote/backend_plan_test.go @@ -13,7 +13,11 @@ import ( tfe "github.com/hashicorp/go-tfe" "github.com/hashicorp/terraform/addrs" "github.com/hashicorp/terraform/backend" + "github.com/hashicorp/terraform/command/arguments" + "github.com/hashicorp/terraform/command/clistate" + "github.com/hashicorp/terraform/command/views" "github.com/hashicorp/terraform/internal/initwd" + "github.com/hashicorp/terraform/internal/terminal" "github.com/hashicorp/terraform/plans/planfile" "github.com/hashicorp/terraform/states/statemgr" "github.com/hashicorp/terraform/terraform" @@ -24,14 +28,24 @@ import ( func testOperationPlan(t *testing.T, configDir string) (*backend.Operation, func()) { t.Helper() + return testOperationPlanWithTimeout(t, configDir, 0) +} + +func testOperationPlanWithTimeout(t *testing.T, configDir string, timeout time.Duration) (*backend.Operation, func()) { + t.Helper() + _, configLoader, configCleanup := initwd.MustLoadConfigForTests(t, configDir) + streams, _ := terminal.StreamsForTesting(t) + view := views.NewStateLocker(arguments.ViewHuman, views.NewView(streams)) + return &backend.Operation{ ConfigDir: configDir, ConfigLoader: configLoader, Parallelism: defaultParallelism, PlanRefresh: true, ShowDiagnostics: testLogDiagnostics(t), + StateLocker: clistate.NewLocker(timeout, view), Type: backend.OperationTypePlan, }, configCleanup } @@ -625,7 +639,7 @@ func TestRemote_planLockTimeout(t *testing.T) { t.Fatalf("error creating pending run: %v", err) } - op, configCleanup := testOperationPlan(t, "./testdata/plan") + op, configCleanup := testOperationPlanWithTimeout(t, "./testdata/plan", 50) defer configCleanup() input := testInput(t, map[string]string{ @@ -633,7 +647,6 @@ func TestRemote_planLockTimeout(t *testing.T) { "approve": "yes", }) - op.StateLockTimeout = 50 * time.Millisecond op.UIIn = input op.UIOut = b.CLI op.Workspace = backend.DefaultStateName diff --git a/command/apply_test.go b/command/apply_test.go index 2980bd61d..37bd8ce19 100644 --- a/command/apply_test.go +++ b/command/apply_test.go @@ -940,11 +940,13 @@ 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{ diff --git a/command/clistate/state.go b/command/clistate/state.go index c04f7d70b..5aa3f1d29 100644 --- a/command/clistate/state.go +++ b/command/clistate/state.go @@ -7,33 +7,25 @@ package clistate import ( "context" "fmt" - "strings" "sync" "time" - "github.com/hashicorp/errwrap" - multierror "github.com/hashicorp/go-multierror" + "github.com/hashicorp/terraform/command/views" "github.com/hashicorp/terraform/internal/helper/slowmessage" "github.com/hashicorp/terraform/states/statemgr" - "github.com/mitchellh/cli" - "github.com/mitchellh/colorstring" + "github.com/hashicorp/terraform/tfdiags" ) const ( LockThreshold = 400 * time.Millisecond - LockMessage = "Acquiring state lock. This may take a few moments..." - LockErrorMessage = `Error acquiring the state lock: {{err}} + LockErrorMessage = `Error message: %s Terraform acquires a state lock to protect the state from being written by multiple users at the same time. Please resolve the issue above and try again. For most commands, you can disable locking with the "-lock=false" flag, but this is not recommended.` - UnlockMessage = "Releasing state lock. This may take a few moments..." - UnlockErrorMessage = ` -[reset][bold][red]Error releasing the state lock![reset][red] - -Error message: %s + UnlockErrorMessage = `Error message: %s Terraform acquires a lock when accessing your state to prevent others running Terraform to potentially modify the state at the same time. An @@ -46,8 +38,7 @@ In this scenario, please call the "force-unlock" command to unlock the state manually. This is a very dangerous operation since if it is done erroneously it could result in two people modifying state at the same time. Only call this command if you're certain that the unlock above failed and -that no one else is holding a lock. -` +that no one else is holding a lock.` ) // Locker allows for more convenient usage of the lower-level statemgr.Locker @@ -60,12 +51,17 @@ that no one else is holding a lock. // Unlock, which is at a minimum the LockID string returned by the // statemgr.Locker. type Locker interface { + // Returns a shallow copy of the locker with its context changed to ctx. + WithContext(ctx context.Context) Locker + // Lock the provided state manager, storing the reason string in the LockInfo. - Lock(s statemgr.Locker, reason string) error + Lock(s statemgr.Locker, reason string) tfdiags.Diagnostics + // Unlock the previously locked state. - // An optional error can be passed in, and will be combined with any error - // from the Unlock operation. - Unlock(error) error + Unlock() tfdiags.Diagnostics + + // Timeout returns the configured timeout duration + Timeout() time.Duration } type locker struct { @@ -73,34 +69,43 @@ type locker struct { ctx context.Context timeout time.Duration state statemgr.Locker - ui cli.Ui - color *colorstring.Colorize + view views.StateLocker lockID string } +var _ Locker = (*locker)(nil) + // Create a new Locker. // This Locker uses state.LockWithContext to retry the lock until the provided // timeout is reached, or the context is canceled. Lock progress will be be // reported to the user through the provided UI. -func NewLocker( - ctx context.Context, - timeout time.Duration, - ui cli.Ui, - color *colorstring.Colorize) Locker { - - l := &locker{ - ctx: ctx, +func NewLocker(timeout time.Duration, view views.StateLocker) Locker { + return &locker{ + ctx: context.Background(), timeout: timeout, - ui: ui, - color: color, + view: view, + } +} + +// WithContext returns a new Locker with the specified context, copying the +// timeout and view parameters from the original Locker. +func (l *locker) WithContext(ctx context.Context) Locker { + if ctx == nil { + panic("nil context") + } + return &locker{ + ctx: ctx, + timeout: l.timeout, + view: l.view, } - return l } // Locker locks the given state and outputs to the user if locking is taking // longer than the threshold. The lock is retried until the context is // cancelled. -func (l *locker) Lock(s statemgr.Locker, reason string) error { +func (l *locker) Lock(s statemgr.Locker, reason string) tfdiags.Diagnostics { + var diags tfdiags.Diagnostics + l.mu.Lock() defer l.mu.Unlock() @@ -116,46 +121,49 @@ func (l *locker) Lock(s statemgr.Locker, reason string) error { id, err := statemgr.LockWithContext(ctx, s, lockInfo) l.lockID = id return err - }, func() { - if l.ui != nil { - l.ui.Output(l.color.Color(LockMessage)) - } - }) + }, l.view.Locking) if err != nil { - return errwrap.Wrapf(strings.TrimSpace(LockErrorMessage), err) + diags = diags.Append(tfdiags.Sourceless( + tfdiags.Error, + "Error acquiring the state lock", + fmt.Sprintf(LockErrorMessage, err), + )) } - return nil + return diags } -func (l *locker) Unlock(parentErr error) error { +func (l *locker) Unlock() tfdiags.Diagnostics { + var diags tfdiags.Diagnostics + l.mu.Lock() defer l.mu.Unlock() if l.lockID == "" { - return parentErr + return diags } err := slowmessage.Do(LockThreshold, func() error { return l.state.Unlock(l.lockID) - }, func() { - if l.ui != nil { - l.ui.Output(l.color.Color(UnlockMessage)) - } - }) + }, l.view.Unlocking) if err != nil { - l.ui.Output(l.color.Color(fmt.Sprintf( - "\n"+strings.TrimSpace(UnlockErrorMessage)+"\n", err))) - - parentErr = multierror.Append(parentErr, err) + diags = diags.Append(tfdiags.Sourceless( + tfdiags.Error, + "Error releasing the state lock", + fmt.Sprintf(UnlockErrorMessage, err), + )) } - return parentErr + return diags } +func (l *locker) Timeout() time.Duration { + return l.timeout +} + type noopLocker struct{} // NewNoopLocker returns a valid Locker that does nothing. @@ -163,10 +171,20 @@ func NewNoopLocker() Locker { return noopLocker{} } -func (l noopLocker) Lock(statemgr.Locker, string) error { +var _ Locker = noopLocker{} + +func (l noopLocker) WithContext(ctx context.Context) Locker { + return l +} + +func (l noopLocker) Lock(statemgr.Locker, string) tfdiags.Diagnostics { return nil } -func (l noopLocker) Unlock(err error) error { - return err +func (l noopLocker) Unlock() tfdiags.Diagnostics { + return nil +} + +func (l noopLocker) Timeout() time.Duration { + return 0 } diff --git a/command/clistate/state_test.go b/command/clistate/state_test.go index 7162538f1..b455b58b5 100644 --- a/command/clistate/state_test.go +++ b/command/clistate/state_test.go @@ -1,23 +1,24 @@ package clistate import ( - "context" "testing" + "github.com/hashicorp/terraform/command/arguments" + "github.com/hashicorp/terraform/command/views" + "github.com/hashicorp/terraform/internal/terminal" "github.com/hashicorp/terraform/states/statemgr" - "github.com/mitchellh/cli" - "github.com/mitchellh/colorstring" ) func TestUnlock(t *testing.T) { - ui := new(cli.MockUi) + streams, _ := terminal.StreamsForTesting(t) + view := views.NewView(streams) - l := NewLocker(context.Background(), 0, ui, &colorstring.Colorize{Disable: true}) + l := NewLocker(0, views.NewStateLocker(arguments.ViewHuman, view)) l.Lock(statemgr.NewUnlockErrorFull(nil, nil), "test-lock") - err := l.Unlock(nil) - if err != nil { - t.Log(err.Error()) + diags := l.Unlock() + if diags.HasErrors() { + t.Log(diags.Err().Error()) } else { t.Error("expected error") } diff --git a/command/console.go b/command/console.go index 75d8edc64..98cbb8bd0 100644 --- a/command/console.go +++ b/command/console.go @@ -104,9 +104,9 @@ func (c *ConsoleCommand) Run(args []string) int { // Successfully creating the context can result in a lock, so ensure we release it defer func() { - err := opReq.StateLocker.Unlock(nil) - if err != nil { - c.Ui.Error(err.Error()) + diags := opReq.StateLocker.Unlock() + if diags.HasErrors() { + c.showDiagnostics(diags) } }() diff --git a/command/console_test.go b/command/console_test.go index 8fb63ea7e..dc16fc07d 100644 --- a/command/console_test.go +++ b/command/console_test.go @@ -27,10 +27,12 @@ func TestConsole_basic(t *testing.T) { p := testProvider() ui := cli.NewMockUi() + view, _ := testView(t) c := &ConsoleCommand{ Meta: Meta{ testingOverrides: metaOverridesForProvider(p), Ui: ui, + View: view, }, } @@ -76,10 +78,12 @@ func TestConsole_tfvars(t *testing.T) { }, } ui := cli.NewMockUi() + view, _ := testView(t) c := &ConsoleCommand{ Meta: Meta{ testingOverrides: metaOverridesForProvider(p), Ui: ui, + View: view, }, } @@ -127,10 +131,12 @@ func TestConsole_unsetRequiredVars(t *testing.T) { }, } ui := cli.NewMockUi() + view, _ := testView(t) c := &ConsoleCommand{ Meta: Meta{ testingOverrides: metaOverridesForProvider(p), Ui: ui, + View: view, }, } @@ -159,10 +165,12 @@ func TestConsole_variables(t *testing.T) { p := testProvider() ui := cli.NewMockUi() + view, _ := testView(t) c := &ConsoleCommand{ Meta: Meta{ testingOverrides: metaOverridesForProvider(p), Ui: ui, + View: view, }, } @@ -200,11 +208,13 @@ func TestConsole_modules(t *testing.T) { p := applyFixtureProvider() ui := cli.NewMockUi() + view, _ := testView(t) c := &ConsoleCommand{ Meta: Meta{ testingOverrides: metaOverridesForProvider(p), Ui: ui, + View: view, }, } diff --git a/command/import.go b/command/import.go index 275349408..54801f85e 100644 --- a/command/import.go +++ b/command/import.go @@ -217,9 +217,9 @@ func (c *ImportCommand) Run(args []string) int { // Successfully creating the context can result in a lock, so ensure we release it defer func() { - err := opReq.StateLocker.Unlock(nil) - if err != nil { - c.Ui.Error(err.Error()) + diags := opReq.StateLocker.Unlock() + if diags.HasErrors() { + c.showDiagnostics(diags) } }() diff --git a/command/import_test.go b/command/import_test.go index 354b20536..e3840b8df 100644 --- a/command/import_test.go +++ b/command/import_test.go @@ -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, }, } diff --git a/command/init_test.go b/command/init_test.go index c72be0456..c0f19b3c3 100644 --- a/command/init_test.go +++ b/command/init_test.go @@ -35,10 +35,12 @@ func TestInit_empty(t *testing.T) { defer testChdir(t, td)() ui := new(cli.MockUi) + view, _ := testView(t) c := &InitCommand{ Meta: Meta{ testingOverrides: metaOverridesForProvider(testProvider()), Ui: ui, + View: view, }, } @@ -56,10 +58,12 @@ func TestInit_multipleArgs(t *testing.T) { defer testChdir(t, td)() ui := new(cli.MockUi) + view, _ := testView(t) c := &InitCommand{ Meta: Meta{ testingOverrides: metaOverridesForProvider(testProvider()), Ui: ui, + View: view, }, } @@ -80,10 +84,12 @@ func TestInit_fromModule_cwdDest(t *testing.T) { defer testChdir(t, td)() ui := new(cli.MockUi) + view, _ := testView(t) c := &InitCommand{ Meta: Meta{ testingOverrides: metaOverridesForProvider(testProvider()), Ui: ui, + View: view, }, } @@ -130,10 +136,12 @@ func TestInit_fromModule_dstInSrc(t *testing.T) { } ui := new(cli.MockUi) + view, _ := testView(t) c := &InitCommand{ Meta: Meta{ testingOverrides: metaOverridesForProvider(testProvider()), Ui: ui, + View: view, }, } @@ -157,10 +165,12 @@ func TestInit_get(t *testing.T) { defer testChdir(t, td)() ui := new(cli.MockUi) + view, _ := testView(t) c := &InitCommand{ Meta: Meta{ testingOverrides: metaOverridesForProvider(testProvider()), Ui: ui, + View: view, }, } @@ -184,10 +194,12 @@ func TestInit_getUpgradeModules(t *testing.T) { defer testChdir(t, td)() ui := new(cli.MockUi) + view, _ := testView(t) c := &InitCommand{ Meta: Meta{ testingOverrides: metaOverridesForProvider(testProvider()), Ui: ui, + View: view, }, } @@ -214,10 +226,12 @@ func TestInit_backend(t *testing.T) { defer testChdir(t, td)() ui := new(cli.MockUi) + view, _ := testView(t) c := &InitCommand{ Meta: Meta{ testingOverrides: metaOverridesForProvider(testProvider()), Ui: ui, + View: view, }, } @@ -242,10 +256,12 @@ func TestInit_backendUnset(t *testing.T) { log.Printf("[TRACE] TestInit_backendUnset: beginning first init") ui := cli.NewMockUi() + view, _ := testView(t) c := &InitCommand{ Meta: Meta{ testingOverrides: metaOverridesForProvider(testProvider()), Ui: ui, + View: view, }, } @@ -272,10 +288,12 @@ func TestInit_backendUnset(t *testing.T) { } ui := cli.NewMockUi() + view, _ := testView(t) c := &InitCommand{ Meta: Meta{ testingOverrides: metaOverridesForProvider(testProvider()), Ui: ui, + View: view, }, } @@ -303,10 +321,12 @@ func TestInit_backendConfigFile(t *testing.T) { t.Run("good-config-file", func(t *testing.T) { ui := new(cli.MockUi) + view, _ := testView(t) c := &InitCommand{ Meta: Meta{ testingOverrides: metaOverridesForProvider(testProvider()), Ui: ui, + View: view, }, } args := []string{"-backend-config", "input.config"} @@ -324,10 +344,12 @@ func TestInit_backendConfigFile(t *testing.T) { // the backend config file must not be a full terraform block t.Run("full-backend-config-file", func(t *testing.T) { ui := new(cli.MockUi) + view, _ := testView(t) c := &InitCommand{ Meta: Meta{ testingOverrides: metaOverridesForProvider(testProvider()), Ui: ui, + View: view, }, } args := []string{"-backend-config", "backend.config"} @@ -342,10 +364,12 @@ func TestInit_backendConfigFile(t *testing.T) { // the backend config file must match the schema for the backend t.Run("invalid-config-file", func(t *testing.T) { ui := new(cli.MockUi) + view, _ := testView(t) c := &InitCommand{ Meta: Meta{ testingOverrides: metaOverridesForProvider(testProvider()), Ui: ui, + View: view, }, } args := []string{"-backend-config", "invalid.config"} @@ -360,10 +384,12 @@ func TestInit_backendConfigFile(t *testing.T) { // missing file is an error t.Run("missing-config-file", func(t *testing.T) { ui := new(cli.MockUi) + view, _ := testView(t) c := &InitCommand{ Meta: Meta{ testingOverrides: metaOverridesForProvider(testProvider()), Ui: ui, + View: view, }, } args := []string{"-backend-config", "missing.config"} @@ -378,10 +404,12 @@ func TestInit_backendConfigFile(t *testing.T) { // blank filename clears the backend config t.Run("blank-config-file", func(t *testing.T) { ui := new(cli.MockUi) + view, _ := testView(t) c := &InitCommand{ Meta: Meta{ testingOverrides: metaOverridesForProvider(testProvider()), Ui: ui, + View: view, }, } args := []string{"-backend-config="} @@ -429,10 +457,12 @@ func TestInit_backendConfigFilePowershellConfusion(t *testing.T) { defer testChdir(t, td)() ui := new(cli.MockUi) + view, _ := testView(t) c := &InitCommand{ Meta: Meta{ testingOverrides: metaOverridesForProvider(testProvider()), Ui: ui, + View: view, }, } @@ -468,10 +498,12 @@ func TestInit_backendConfigFileChange(t *testing.T) { })() ui := new(cli.MockUi) + view, _ := testView(t) c := &InitCommand{ Meta: Meta{ testingOverrides: metaOverridesForProvider(testProvider()), Ui: ui, + View: view, }, } @@ -495,10 +527,12 @@ func TestInit_backendConfigKV(t *testing.T) { defer testChdir(t, td)() ui := new(cli.MockUi) + view, _ := testView(t) c := &InitCommand{ Meta: Meta{ testingOverrides: metaOverridesForProvider(testProvider()), Ui: ui, + View: view, }, } @@ -522,10 +556,12 @@ func TestInit_backendConfigKVReInit(t *testing.T) { defer testChdir(t, td)() ui := new(cli.MockUi) + view, _ := testView(t) c := &InitCommand{ Meta: Meta{ testingOverrides: metaOverridesForProvider(testProvider()), Ui: ui, + View: view, }, } @@ -539,6 +575,7 @@ func TestInit_backendConfigKVReInit(t *testing.T) { Meta: Meta{ testingOverrides: metaOverridesForProvider(testProvider()), Ui: ui, + View: view, }, } @@ -583,10 +620,12 @@ func TestInit_backendConfigKVReInitWithConfigDiff(t *testing.T) { defer testChdir(t, td)() ui := new(cli.MockUi) + view, _ := testView(t) c := &InitCommand{ Meta: Meta{ testingOverrides: metaOverridesForProvider(testProvider()), Ui: ui, + View: view, }, } @@ -600,6 +639,7 @@ func TestInit_backendConfigKVReInitWithConfigDiff(t *testing.T) { Meta: Meta{ testingOverrides: metaOverridesForProvider(testProvider()), Ui: ui, + View: view, }, } @@ -629,10 +669,12 @@ func TestInit_backendCli_no_config_block(t *testing.T) { defer testChdir(t, td)() ui := new(cli.MockUi) + view, _ := testView(t) c := &InitCommand{ Meta: Meta{ testingOverrides: metaOverridesForProvider(testProvider()), Ui: ui, + View: view, }, } @@ -667,10 +709,12 @@ func TestInit_backendReinitWithExtra(t *testing.T) { } ui := new(cli.MockUi) + view, _ := testView(t) c := &InitCommand{ Meta: Meta{ testingOverrides: metaOverridesForProvider(testProvider()), Ui: ui, + View: view, }, } @@ -710,10 +754,12 @@ func TestInit_backendReinitConfigToExtra(t *testing.T) { defer testChdir(t, td)() ui := new(cli.MockUi) + view, _ := testView(t) c := &InitCommand{ Meta: Meta{ testingOverrides: metaOverridesForProvider(testProvider()), Ui: ui, + View: view, }, } @@ -741,6 +787,7 @@ func TestInit_backendReinitConfigToExtra(t *testing.T) { Meta: Meta{ testingOverrides: metaOverridesForProvider(testProvider()), Ui: ui, + View: view, }, } @@ -766,10 +813,12 @@ func TestInit_inputFalse(t *testing.T) { defer testChdir(t, td)() ui := new(cli.MockUi) + view, _ := testView(t) c := &InitCommand{ Meta: Meta{ testingOverrides: metaOverridesForProvider(testProvider()), Ui: ui, + View: view, }, } @@ -805,6 +854,7 @@ func TestInit_inputFalse(t *testing.T) { Meta: Meta{ testingOverrides: metaOverridesForProvider(testProvider()), Ui: ui, + View: view, }, } @@ -823,6 +873,7 @@ func TestInit_inputFalse(t *testing.T) { Meta: Meta{ testingOverrides: metaOverridesForProvider(testProvider()), Ui: ui, + View: view, }, } @@ -842,6 +893,7 @@ func TestInit_getProvider(t *testing.T) { overrides := metaOverridesForProvider(testProvider()) ui := new(cli.MockUi) + view, _ := testView(t) providerSource, close := newMockProviderSource(t, map[string][]string{ // looking for an exact version "exact": {"1.2.3"}, @@ -854,6 +906,7 @@ func TestInit_getProvider(t *testing.T) { m := Meta{ testingOverrides: overrides, Ui: ui, + View: view, ProviderSource: providerSource, } @@ -919,7 +972,9 @@ func TestInit_getProvider(t *testing.T) { } ui := new(cli.MockUi) + view, _ := testView(t) m.Ui = ui + m.View = view c := &InitCommand{ Meta: m, } @@ -944,6 +999,7 @@ func TestInit_getProviderSource(t *testing.T) { overrides := metaOverridesForProvider(testProvider()) ui := new(cli.MockUi) + view, _ := testView(t) providerSource, close := newMockProviderSource(t, map[string][]string{ // looking for an exact version "acme/alpha": {"1.2.3"}, @@ -955,6 +1011,7 @@ func TestInit_getProviderSource(t *testing.T) { m := Meta{ testingOverrides: overrides, Ui: ui, + View: view, ProviderSource: providerSource, } @@ -993,6 +1050,7 @@ func TestInit_getProviderLegacyFromState(t *testing.T) { overrides := metaOverridesForProvider(testProvider()) ui := new(cli.MockUi) + view, _ := testView(t) providerSource, close := newMockProviderSource(t, map[string][]string{ "acme/alpha": {"1.2.3"}, }) @@ -1000,6 +1058,7 @@ func TestInit_getProviderLegacyFromState(t *testing.T) { m := Meta{ testingOverrides: overrides, Ui: ui, + View: view, ProviderSource: providerSource, } @@ -1033,6 +1092,7 @@ func TestInit_getProviderInvalidPackage(t *testing.T) { overrides := metaOverridesForProvider(testProvider()) ui := new(cli.MockUi) + view, _ := testView(t) // create a provider source which allows installing an invalid package addr := addrs.MustParseProviderSourceString("invalid/package") @@ -1053,6 +1113,7 @@ func TestInit_getProviderInvalidPackage(t *testing.T) { m := Meta{ testingOverrides: overrides, Ui: ui, + View: view, ProviderSource: providerSource, } @@ -1109,8 +1170,10 @@ func TestInit_getProviderDetectedLegacy(t *testing.T) { } ui := new(cli.MockUi) + view, _ := testView(t) m := Meta{ Ui: ui, + View: view, ProviderSource: multiSource, } @@ -1166,9 +1229,11 @@ func TestInit_providerSource(t *testing.T) { defer close() ui := new(cli.MockUi) + view, _ := testView(t) m := Meta{ testingOverrides: metaOverridesForProvider(testProvider()), Ui: ui, + View: view, ProviderSource: providerSource, } @@ -1277,9 +1342,11 @@ func TestInit_cancel(t *testing.T) { close(shutdownCh) ui := cli.NewMockUi() + view, _ := testView(t) m := Meta{ testingOverrides: metaOverridesForProvider(testProvider()), Ui: ui, + View: view, ProviderSource: providerSource, ShutdownCh: shutdownCh, } @@ -1320,9 +1387,11 @@ func TestInit_getUpgradePlugins(t *testing.T) { defer close() ui := new(cli.MockUi) + view, _ := testView(t) m := Meta{ testingOverrides: metaOverridesForProvider(testProvider()), Ui: ui, + View: view, ProviderSource: providerSource, } @@ -1444,9 +1513,11 @@ func TestInit_getProviderMissing(t *testing.T) { defer close() ui := new(cli.MockUi) + view, _ := testView(t) m := Meta{ testingOverrides: metaOverridesForProvider(testProvider()), Ui: ui, + View: view, ProviderSource: providerSource, } @@ -1472,10 +1543,12 @@ func TestInit_checkRequiredVersion(t *testing.T) { defer testChdir(t, td)() ui := cli.NewMockUi() + view, _ := testView(t) c := &InitCommand{ Meta: Meta{ testingOverrides: metaOverridesForProvider(testProvider()), Ui: ui, + View: view, }, } @@ -1505,9 +1578,11 @@ func TestInit_providerLockFile(t *testing.T) { defer close() ui := new(cli.MockUi) + view, _ := testView(t) m := Meta{ testingOverrides: metaOverridesForProvider(testProvider()), Ui: ui, + View: view, ProviderSource: providerSource, } @@ -1555,10 +1630,12 @@ func TestInit_pluginDirReset(t *testing.T) { defer close() ui := new(cli.MockUi) + view, _ := testView(t) c := &InitCommand{ Meta: Meta{ testingOverrides: metaOverridesForProvider(testProvider()), Ui: ui, + View: view, ProviderSource: providerSource, }, } @@ -1591,6 +1668,7 @@ func TestInit_pluginDirReset(t *testing.T) { Meta: Meta{ testingOverrides: metaOverridesForProvider(testProvider()), Ui: ui, + View: view, ProviderSource: providerSource, // still empty }, } @@ -1623,9 +1701,11 @@ func TestInit_pluginDirProviders(t *testing.T) { defer close() ui := new(cli.MockUi) + view, _ := testView(t) m := Meta{ testingOverrides: metaOverridesForProvider(testProvider()), Ui: ui, + View: view, ProviderSource: providerSource, } @@ -1723,9 +1803,11 @@ func TestInit_pluginDirProvidersDoesNotGet(t *testing.T) { defer close() ui := cli.NewMockUi() + view, _ := testView(t) m := Meta{ testingOverrides: metaOverridesForProvider(testProvider()), Ui: ui, + View: view, ProviderSource: providerSource, } @@ -1795,9 +1877,11 @@ func TestInit_pluginDirWithBuiltIn(t *testing.T) { defer close() ui := cli.NewMockUi() + view, _ := testView(t) m := Meta{ testingOverrides: metaOverridesForProvider(testProvider()), Ui: ui, + View: view, ProviderSource: providerSource, } @@ -1832,9 +1916,11 @@ func TestInit_invalidBuiltInProviders(t *testing.T) { defer close() ui := cli.NewMockUi() + view, _ := testView(t) m := Meta{ testingOverrides: metaOverridesForProvider(testProvider()), Ui: ui, + View: view, ProviderSource: providerSource, } diff --git a/command/meta_backend.go b/command/meta_backend.go index 2e01a428f..329e5f84e 100644 --- a/command/meta_backend.go +++ b/command/meta_backend.go @@ -18,7 +18,9 @@ import ( "github.com/hashicorp/hcl/v2/hcldec" "github.com/hashicorp/terraform/backend" remoteBackend "github.com/hashicorp/terraform/backend/remote" + "github.com/hashicorp/terraform/command/arguments" "github.com/hashicorp/terraform/command/clistate" + "github.com/hashicorp/terraform/command/views" "github.com/hashicorp/terraform/configs" "github.com/hashicorp/terraform/plans" "github.com/hashicorp/terraform/states/statemgr" @@ -341,15 +343,20 @@ func (m *Meta) Operation(b backend.Backend) *backend.Operation { panic(fmt.Sprintf("failed to encode backend configuration for plan: %s", err)) } + stateLocker := clistate.NewNoopLocker() + if m.stateLock { + view := views.NewStateLocker(arguments.ViewHuman, m.View) + stateLocker = clistate.NewLocker(m.stateLockTimeout, view) + } + return &backend.Operation{ - PlanOutBackend: planOutBackend, - Parallelism: m.parallelism, - Targets: m.targets, - UIIn: m.UIInput(), - UIOut: m.Ui, - Workspace: workspace, - LockState: m.stateLock, - StateLockTimeout: m.stateLockTimeout, + PlanOutBackend: planOutBackend, + Parallelism: m.parallelism, + Targets: m.targets, + UIIn: m.UIInput(), + UIOut: m.Ui, + Workspace: workspace, + StateLocker: stateLocker, } } @@ -802,12 +809,13 @@ func (m *Meta) backend_C_r_s(c *configs.Backend, cHash int, sMgr *clistate.Local } if m.stateLock { - stateLocker := clistate.NewLocker(context.Background(), m.stateLockTimeout, m.Ui, m.Colorize()) + view := views.NewStateLocker(arguments.ViewHuman, m.View) + stateLocker := clistate.NewLocker(m.stateLockTimeout, view) if err := stateLocker.Lock(sMgr, "backend from plan"); err != nil { diags = diags.Append(fmt.Errorf("Error locking state: %s", err)) return nil, diags } - defer stateLocker.Unlock(nil) + defer stateLocker.Unlock() } configJSON, err := ctyjson.Marshal(configVal, b.ConfigSchema().ImpliedType()) @@ -886,12 +894,13 @@ func (m *Meta) backend_C_r_S_changed(c *configs.Backend, cHash int, sMgr *clista } if m.stateLock { - stateLocker := clistate.NewLocker(context.Background(), m.stateLockTimeout, m.Ui, m.Colorize()) + view := views.NewStateLocker(arguments.ViewHuman, m.View) + stateLocker := clistate.NewLocker(m.stateLockTimeout, view) if err := stateLocker.Lock(sMgr, "backend from plan"); err != nil { diags = diags.Append(fmt.Errorf("Error locking state: %s", err)) return nil, diags } - defer stateLocker.Unlock(nil) + defer stateLocker.Unlock() } configJSON, err := ctyjson.Marshal(configVal, b.ConfigSchema().ImpliedType()) diff --git a/command/meta_backend_migrate.go b/command/meta_backend_migrate.go index 35fda9042..85644508c 100644 --- a/command/meta_backend_migrate.go +++ b/command/meta_backend_migrate.go @@ -12,7 +12,9 @@ import ( "strings" "github.com/hashicorp/terraform/backend" + "github.com/hashicorp/terraform/command/arguments" "github.com/hashicorp/terraform/command/clistate" + "github.com/hashicorp/terraform/command/views" "github.com/hashicorp/terraform/states" "github.com/hashicorp/terraform/states/statemgr" "github.com/hashicorp/terraform/terraform" @@ -325,17 +327,20 @@ func (m *Meta) backendMigrateState_s_s(opts *backendMigrateOpts) error { if m.stateLock { lockCtx := context.Background() - lockerOne := clistate.NewLocker(lockCtx, m.stateLockTimeout, m.Ui, m.Colorize()) - if err := lockerOne.Lock(stateOne, "migration source state"); err != nil { - return fmt.Errorf("Error locking source state: %s", err) - } - defer lockerOne.Unlock(nil) + view := views.NewStateLocker(arguments.ViewHuman, m.View) + locker := clistate.NewLocker(m.stateLockTimeout, view) - lockerTwo := clistate.NewLocker(lockCtx, m.stateLockTimeout, m.Ui, m.Colorize()) - if err := lockerTwo.Lock(stateTwo, "migration destination state"); err != nil { - return fmt.Errorf("Error locking destination state: %s", err) + lockerOne := locker.WithContext(lockCtx) + if diags := lockerOne.Lock(stateOne, "migration source state"); diags.HasErrors() { + return diags.Err() } - defer lockerTwo.Unlock(nil) + defer lockerOne.Unlock() + + lockerTwo := locker.WithContext(lockCtx) + if diags := lockerTwo.Lock(stateTwo, "migration destination state"); diags.HasErrors() { + return diags.Err() + } + defer lockerTwo.Unlock() // We now own a lock, so double check that we have the version // corresponding to the lock. diff --git a/command/meta_backend_test.go b/command/meta_backend_test.go index 39b20a0a1..cbd642ed7 100644 --- a/command/meta_backend_test.go +++ b/command/meta_backend_test.go @@ -1876,6 +1876,8 @@ func TestBackendFromState(t *testing.T) { func testMetaBackend(t *testing.T, args []string) *Meta { var m Meta m.Ui = new(cli.MockUi) + view, _ := testView(t) + m.View = view m.process(args) f := m.extendedFlagSet("test") if err := f.Parse(args); err != nil { diff --git a/command/plan_test.go b/command/plan_test.go index c9c09476e..07ab17d61 100644 --- a/command/plan_test.go +++ b/command/plan_test.go @@ -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, }, } diff --git a/command/show_test.go b/command/show_test.go index aec6e22ec..25b181252 100644 --- a/command/show_test.go +++ b/command/show_test.go @@ -22,10 +22,12 @@ import ( func TestShow(t *testing.T) { ui := new(cli.MockUi) + view, _ := testView(t) c := &ShowCommand{ Meta: Meta{ testingOverrides: metaOverridesForProvider(testProvider()), Ui: ui, + View: view, }, } @@ -46,10 +48,12 @@ func TestShow_noArgs(t *testing.T) { defer testChdir(t, stateDir)() ui := new(cli.MockUi) + view, _ := testView(t) c := &ShowCommand{ Meta: Meta{ testingOverrides: metaOverridesForProvider(testProvider()), Ui: ui, + View: view, }, } @@ -93,10 +97,12 @@ func TestShow_aliasedProvider(t *testing.T) { defer testChdir(t, stateDir)() ui := new(cli.MockUi) + view, _ := testView(t) c := &ShowCommand{ Meta: Meta{ testingOverrides: metaOverridesForProvider(testProvider()), Ui: ui, + View: view, }, } @@ -121,10 +127,12 @@ func TestShow_noArgsNoState(t *testing.T) { defer testChdir(t, stateDir)() ui := new(cli.MockUi) + view, _ := testView(t) c := &ShowCommand{ Meta: Meta{ testingOverrides: metaOverridesForProvider(testProvider()), Ui: ui, + View: view, }, } @@ -139,10 +147,12 @@ func TestShow_plan(t *testing.T) { planPath := testPlanFileNoop(t) ui := cli.NewMockUi() + view, _ := testView(t) c := &ShowCommand{ Meta: Meta{ testingOverrides: metaOverridesForProvider(testProvider()), Ui: ui, + View: view, }, } @@ -164,10 +174,12 @@ func TestShow_planWithChanges(t *testing.T) { planPathWithChanges := showFixturePlanFile(t, plans.DeleteThenCreate) ui := cli.NewMockUi() + view, _ := testView(t) c := &ShowCommand{ Meta: Meta{ testingOverrides: metaOverridesForProvider(showFixtureProvider()), Ui: ui, + View: view, }, } @@ -190,10 +202,12 @@ func TestShow_plan_json(t *testing.T) { planPath := showFixturePlanFile(t, plans.Create) ui := new(cli.MockUi) + view, _ := testView(t) c := &ShowCommand{ Meta: Meta{ testingOverrides: metaOverridesForProvider(showFixtureProvider()), Ui: ui, + View: view, }, } @@ -212,10 +226,12 @@ func TestShow_state(t *testing.T) { defer os.RemoveAll(filepath.Dir(statePath)) ui := new(cli.MockUi) + view, _ := testView(t) c := &ShowCommand{ Meta: Meta{ testingOverrides: metaOverridesForProvider(testProvider()), Ui: ui, + View: view, }, } @@ -255,9 +271,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, } @@ -352,9 +370,11 @@ func TestShow_json_output_state(t *testing.T) { p := showFixtureProvider() ui := new(cli.MockUi) + view, _ := testView(t) m := Meta{ testingOverrides: metaOverridesForProvider(p), Ui: ui, + View: view, ProviderSource: providerSource, } diff --git a/command/state_mv.go b/command/state_mv.go index 451034199..8b432a48a 100644 --- a/command/state_mv.go +++ b/command/state_mv.go @@ -1,12 +1,13 @@ package command import ( - "context" "fmt" "strings" "github.com/hashicorp/terraform/addrs" + "github.com/hashicorp/terraform/command/arguments" "github.com/hashicorp/terraform/command/clistate" + "github.com/hashicorp/terraform/command/views" "github.com/hashicorp/terraform/states" "github.com/hashicorp/terraform/tfdiags" "github.com/mitchellh/cli" @@ -49,12 +50,16 @@ func (c *StateMvCommand) Run(args []string) int { } if c.stateLock { - stateLocker := clistate.NewLocker(context.Background(), c.stateLockTimeout, c.Ui, c.Colorize()) - if err := stateLocker.Lock(stateFromMgr, "state-mv"); err != nil { - c.Ui.Error(fmt.Sprintf("Error locking source state: %s", err)) + stateLocker := clistate.NewLocker(c.stateLockTimeout, views.NewStateLocker(arguments.ViewHuman, c.View)) + if diags := stateLocker.Lock(stateFromMgr, "state-mv"); diags.HasErrors() { + c.showDiagnostics(diags) return 1 } - defer stateLocker.Unlock(nil) + defer func() { + if diags := stateLocker.Unlock(); diags.HasErrors() { + c.showDiagnostics(diags) + } + }() } if err := stateFromMgr.RefreshState(); err != nil { @@ -83,12 +88,16 @@ func (c *StateMvCommand) Run(args []string) int { } if c.stateLock { - stateLocker := clistate.NewLocker(context.Background(), c.stateLockTimeout, c.Ui, c.Colorize()) - if err := stateLocker.Lock(stateToMgr, "state-mv"); err != nil { - c.Ui.Error(fmt.Sprintf("Error locking destination state: %s", err)) + stateLocker := clistate.NewLocker(c.stateLockTimeout, views.NewStateLocker(arguments.ViewHuman, c.View)) + if diags := stateLocker.Lock(stateToMgr, "state-mv"); diags.HasErrors() { + c.showDiagnostics(diags) return 1 } - defer stateLocker.Unlock(nil) + defer func() { + if diags := stateLocker.Unlock(); diags.HasErrors() { + c.showDiagnostics(diags) + } + }() } if err := stateToMgr.RefreshState(); err != nil { diff --git a/command/state_mv_test.go b/command/state_mv_test.go index 1685d01a2..a651876bb 100644 --- a/command/state_mv_test.go +++ b/command/state_mv_test.go @@ -58,11 +58,13 @@ func TestStateMv(t *testing.T) { p := testProvider() ui := new(cli.MockUi) + view, _ := testView(t) c := &StateMvCommand{ StateMeta{ Meta: Meta{ testingOverrides: metaOverridesForProvider(p), Ui: ui, + View: view, }, }, } @@ -204,11 +206,13 @@ func TestStateMv_resourceToInstance(t *testing.T) { p := testProvider() ui := new(cli.MockUi) + view, _ := testView(t) c := &StateMvCommand{ StateMeta{ Meta: Meta{ testingOverrides: metaOverridesForProvider(p), Ui: ui, + View: view, }, }, } @@ -277,12 +281,14 @@ func TestStateMv_resourceToInstanceErr(t *testing.T) { p := testProvider() ui := cli.NewMockUi() + view, _ := testView(t) c := &StateMvCommand{ StateMeta{ Meta: Meta{ testingOverrides: metaOverridesForProvider(p), Ui: ui, + View: view, }, }, } @@ -344,11 +350,13 @@ func TestStateMv_resourceToInstanceErrInAutomation(t *testing.T) { p := testProvider() ui := new(cli.MockUi) + view, _ := testView(t) c := &StateMvCommand{ StateMeta{ Meta: Meta{ testingOverrides: metaOverridesForProvider(p), Ui: ui, + View: view, RunningInAutomation: true, }, }, @@ -416,11 +424,13 @@ func TestStateMv_instanceToResource(t *testing.T) { p := testProvider() ui := new(cli.MockUi) + view, _ := testView(t) c := &StateMvCommand{ StateMeta{ Meta: Meta{ testingOverrides: metaOverridesForProvider(p), Ui: ui, + View: view, }, }, } @@ -489,11 +499,13 @@ func TestStateMv_instanceToNewResource(t *testing.T) { p := testProvider() ui := new(cli.MockUi) + view, _ := testView(t) c := &StateMvCommand{ StateMeta{ Meta: Meta{ testingOverrides: metaOverridesForProvider(p), Ui: ui, + View: view, }, }, } @@ -560,11 +572,13 @@ func TestStateMv_differentResourceTypes(t *testing.T) { p := testProvider() ui := new(cli.MockUi) + view, _ := testView(t) c := &StateMvCommand{ StateMeta{ Meta: Meta{ testingOverrides: metaOverridesForProvider(p), Ui: ui, + View: view, }, }, } @@ -636,10 +650,12 @@ func TestStateMv_explicitWithBackend(t *testing.T) { // init our backend ui := new(cli.MockUi) + view, _ := testView(t) ic := &InitCommand{ Meta: Meta{ testingOverrides: metaOverridesForProvider(testProvider()), Ui: ui, + View: view, }, } @@ -656,6 +672,7 @@ func TestStateMv_explicitWithBackend(t *testing.T) { Meta: Meta{ testingOverrides: metaOverridesForProvider(p), Ui: ui, + View: view, }, }, } @@ -713,11 +730,13 @@ func TestStateMv_backupExplicit(t *testing.T) { p := testProvider() ui := new(cli.MockUi) + view, _ := testView(t) c := &StateMvCommand{ StateMeta{ Meta: Meta{ testingOverrides: metaOverridesForProvider(p), Ui: ui, + View: view, }, }, } @@ -762,11 +781,13 @@ func TestStateMv_stateOutNew(t *testing.T) { p := testProvider() ui := new(cli.MockUi) + view, _ := testView(t) c := &StateMvCommand{ StateMeta{ Meta: Meta{ testingOverrides: metaOverridesForProvider(p), Ui: ui, + View: view, }, }, } @@ -834,11 +855,13 @@ func TestStateMv_stateOutExisting(t *testing.T) { p := testProvider() ui := new(cli.MockUi) + view, _ := testView(t) c := &StateMvCommand{ StateMeta{ Meta: Meta{ testingOverrides: metaOverridesForProvider(p), Ui: ui, + View: view, }, }, } @@ -877,11 +900,13 @@ func TestStateMv_noState(t *testing.T) { p := testProvider() ui := new(cli.MockUi) + view, _ := testView(t) c := &StateMvCommand{ StateMeta{ Meta: Meta{ testingOverrides: metaOverridesForProvider(p), Ui: ui, + View: view, }, }, } @@ -945,11 +970,13 @@ func TestStateMv_stateOutNew_count(t *testing.T) { p := testProvider() ui := new(cli.MockUi) + view, _ := testView(t) c := &StateMvCommand{ StateMeta{ Meta: Meta{ testingOverrides: metaOverridesForProvider(p), Ui: ui, + View: view, }, }, } @@ -1019,11 +1046,13 @@ func TestStateMv_stateOutNew_largeCount(t *testing.T) { p := testProvider() ui := new(cli.MockUi) + view, _ := testView(t) c := &StateMvCommand{ StateMeta{ Meta: Meta{ testingOverrides: metaOverridesForProvider(p), Ui: ui, + View: view, }, }, } @@ -1089,11 +1118,13 @@ func TestStateMv_stateOutNew_nestedModule(t *testing.T) { p := testProvider() ui := new(cli.MockUi) + view, _ := testView(t) c := &StateMvCommand{ StateMeta{ Meta: Meta{ testingOverrides: metaOverridesForProvider(p), Ui: ui, + View: view, }, }, } @@ -1145,11 +1176,13 @@ func TestStateMv_toNewModule(t *testing.T) { p := testProvider() ui := new(cli.MockUi) + view, _ := testView(t) c := &StateMvCommand{ StateMeta{ Meta: Meta{ testingOverrides: metaOverridesForProvider(p), Ui: ui, + View: view, }, }, } @@ -1244,11 +1277,13 @@ func TestStateMv_withinBackend(t *testing.T) { p := testProvider() ui := new(cli.MockUi) + view, _ := testView(t) c := &StateMvCommand{ StateMeta{ Meta: Meta{ testingOverrides: metaOverridesForProvider(p), Ui: ui, + View: view, }, }, } @@ -1314,11 +1349,13 @@ func TestStateMv_fromBackendToLocal(t *testing.T) { p := testProvider() ui := new(cli.MockUi) + view, _ := testView(t) c := &StateMvCommand{ StateMeta{ Meta: Meta{ testingOverrides: metaOverridesForProvider(p), Ui: ui, + View: view, }, }, } @@ -1365,11 +1402,13 @@ func TestStateMv_onlyResourceInModule(t *testing.T) { p := testProvider() ui := new(cli.MockUi) + view, _ := testView(t) c := &StateMvCommand{ StateMeta{ Meta: Meta{ testingOverrides: metaOverridesForProvider(p), Ui: ui, + View: view, }, }, } diff --git a/command/state_push.go b/command/state_push.go index 66bfbdaf7..4fb9194ee 100644 --- a/command/state_push.go +++ b/command/state_push.go @@ -1,13 +1,14 @@ package command import ( - "context" "fmt" "io" "os" "strings" + "github.com/hashicorp/terraform/command/arguments" "github.com/hashicorp/terraform/command/clistate" + "github.com/hashicorp/terraform/command/views" "github.com/hashicorp/terraform/states/statefile" "github.com/hashicorp/terraform/states/statemgr" "github.com/mitchellh/cli" @@ -93,12 +94,16 @@ func (c *StatePushCommand) Run(args []string) int { } if c.stateLock { - stateLocker := clistate.NewLocker(context.Background(), c.stateLockTimeout, c.Ui, c.Colorize()) - if err := stateLocker.Lock(stateMgr, "state-push"); err != nil { - c.Ui.Error(fmt.Sprintf("Error locking state: %s", err)) + stateLocker := clistate.NewLocker(c.stateLockTimeout, views.NewStateLocker(arguments.ViewHuman, c.View)) + if diags := stateLocker.Lock(stateMgr, "state-push"); diags.HasErrors() { + c.showDiagnostics(diags) return 1 } - defer stateLocker.Unlock(nil) + defer func() { + if diags := stateLocker.Unlock(); diags.HasErrors() { + c.showDiagnostics(diags) + } + }() } if err := stateMgr.RefreshState(); err != nil { diff --git a/command/state_push_test.go b/command/state_push_test.go index c04ed24bf..d906053da 100644 --- a/command/state_push_test.go +++ b/command/state_push_test.go @@ -23,10 +23,12 @@ func TestStatePush_empty(t *testing.T) { p := testProvider() ui := new(cli.MockUi) + view, _ := testView(t) c := &StatePushCommand{ Meta: Meta{ testingOverrides: metaOverridesForProvider(p), Ui: ui, + View: view, }, } @@ -50,10 +52,12 @@ func TestStatePush_lockedState(t *testing.T) { p := testProvider() ui := new(cli.MockUi) + view, _ := testView(t) c := &StatePushCommand{ Meta: Meta{ testingOverrides: metaOverridesForProvider(p), Ui: ui, + View: view, }, } @@ -83,10 +87,12 @@ func TestStatePush_replaceMatch(t *testing.T) { p := testProvider() ui := new(cli.MockUi) + view, _ := testView(t) c := &StatePushCommand{ Meta: Meta{ testingOverrides: metaOverridesForProvider(p), Ui: ui, + View: view, }, } @@ -119,10 +125,12 @@ func TestStatePush_replaceMatchStdin(t *testing.T) { p := testProvider() ui := new(cli.MockUi) + view, _ := testView(t) c := &StatePushCommand{ Meta: Meta{ testingOverrides: metaOverridesForProvider(p), Ui: ui, + View: view, }, } @@ -148,10 +156,12 @@ func TestStatePush_lineageMismatch(t *testing.T) { p := testProvider() ui := cli.NewMockUi() + view, _ := testView(t) c := &StatePushCommand{ Meta: Meta{ testingOverrides: metaOverridesForProvider(p), Ui: ui, + View: view, }, } @@ -177,10 +187,12 @@ func TestStatePush_serialNewer(t *testing.T) { p := testProvider() ui := new(cli.MockUi) + view, _ := testView(t) c := &StatePushCommand{ Meta: Meta{ testingOverrides: metaOverridesForProvider(p), Ui: ui, + View: view, }, } @@ -206,10 +218,12 @@ func TestStatePush_serialOlder(t *testing.T) { p := testProvider() ui := new(cli.MockUi) + view, _ := testView(t) c := &StatePushCommand{ Meta: Meta{ testingOverrides: metaOverridesForProvider(p), Ui: ui, + View: view, }, } @@ -236,8 +250,9 @@ func TestStatePush_forceRemoteState(t *testing.T) { // init the backend ui := new(cli.MockUi) + view, _ := testView(t) initCmd := &InitCommand{ - Meta: Meta{Ui: ui}, + Meta: Meta{Ui: ui, View: view}, } if code := initCmd.Run([]string{}); code != 0 { t.Fatalf("bad: \n%s", ui.ErrorWriter.String()) @@ -246,7 +261,7 @@ func TestStatePush_forceRemoteState(t *testing.T) { // create a new workspace ui = new(cli.MockUi) newCmd := &WorkspaceNewCommand{ - Meta: Meta{Ui: ui}, + Meta: Meta{Ui: ui, View: view}, } if code := newCmd.Run([]string{"test"}); code != 0 { t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter) @@ -268,7 +283,7 @@ func TestStatePush_forceRemoteState(t *testing.T) { // push our local state to that new workspace ui = new(cli.MockUi) c := &StatePushCommand{ - Meta: Meta{Ui: ui}, + Meta: Meta{Ui: ui, View: view}, } args := []string{"-force", statePath} diff --git a/command/state_replace_provider.go b/command/state_replace_provider.go index 750a90582..d8d7d3d70 100644 --- a/command/state_replace_provider.go +++ b/command/state_replace_provider.go @@ -1,12 +1,13 @@ package command import ( - "context" "fmt" "strings" "github.com/hashicorp/terraform/addrs" + "github.com/hashicorp/terraform/command/arguments" "github.com/hashicorp/terraform/command/clistate" + "github.com/hashicorp/terraform/command/views" "github.com/hashicorp/terraform/states" "github.com/hashicorp/terraform/tfdiags" "github.com/mitchellh/cli" @@ -74,12 +75,16 @@ func (c *StateReplaceProviderCommand) Run(args []string) int { // Acquire lock if requested if c.stateLock { - stateLocker := clistate.NewLocker(context.Background(), c.stateLockTimeout, c.Ui, c.Colorize()) - if err := stateLocker.Lock(stateMgr, "state-replace-provider"); err != nil { - c.Ui.Error(fmt.Sprintf("Error locking source state: %s", err)) + stateLocker := clistate.NewLocker(c.stateLockTimeout, views.NewStateLocker(arguments.ViewHuman, c.View)) + if diags := stateLocker.Lock(stateMgr, "state-replace-provider"); diags.HasErrors() { + c.showDiagnostics(diags) return 1 } - defer stateLocker.Unlock(nil) + defer func() { + if diags := stateLocker.Unlock(); diags.HasErrors() { + c.showDiagnostics(diags) + } + }() } // Refresh and load state diff --git a/command/state_replace_provider_test.go b/command/state_replace_provider_test.go index 563af4fdd..fa3c8505d 100644 --- a/command/state_replace_provider_test.go +++ b/command/state_replace_provider_test.go @@ -65,10 +65,12 @@ func TestStateReplaceProvider(t *testing.T) { statePath := testStateFile(t, state) ui := new(cli.MockUi) + view, _ := testView(t) c := &StateReplaceProviderCommand{ StateMeta{ Meta: Meta{ - Ui: ui, + Ui: ui, + View: view, }, }, } @@ -99,10 +101,12 @@ func TestStateReplaceProvider(t *testing.T) { statePath := testStateFile(t, state) ui := new(cli.MockUi) + view, _ := testView(t) c := &StateReplaceProviderCommand{ StateMeta{ Meta: Meta{ - Ui: ui, + Ui: ui, + View: view, }, }, } @@ -133,10 +137,12 @@ func TestStateReplaceProvider(t *testing.T) { statePath := testStateFile(t, state) ui := new(cli.MockUi) + view, _ := testView(t) c := &StateReplaceProviderCommand{ StateMeta{ Meta: Meta{ - Ui: ui, + Ui: ui, + View: view, }, }, } @@ -166,10 +172,12 @@ func TestStateReplaceProvider(t *testing.T) { statePath := testStateFile(t, state) ui := new(cli.MockUi) + view, _ := testView(t) c := &StateReplaceProviderCommand{ StateMeta{ Meta: Meta{ - Ui: ui, + Ui: ui, + View: view, }, }, } @@ -193,10 +201,12 @@ func TestStateReplaceProvider(t *testing.T) { t.Run("invalid flags", func(t *testing.T) { ui := new(cli.MockUi) + view, _ := testView(t) c := &StateReplaceProviderCommand{ StateMeta{ Meta: Meta{ - Ui: ui, + Ui: ui, + View: view, }, }, } @@ -217,10 +227,12 @@ func TestStateReplaceProvider(t *testing.T) { t.Run("wrong number of arguments", func(t *testing.T) { ui := new(cli.MockUi) + view, _ := testView(t) c := &StateReplaceProviderCommand{ StateMeta{ Meta: Meta{ - Ui: ui, + Ui: ui, + View: view, }, }, } @@ -237,10 +249,12 @@ func TestStateReplaceProvider(t *testing.T) { t.Run("invalid provider strings", func(t *testing.T) { ui := new(cli.MockUi) + view, _ := testView(t) c := &StateReplaceProviderCommand{ StateMeta{ Meta: Meta{ - Ui: ui, + Ui: ui, + View: view, }, }, } diff --git a/command/state_rm.go b/command/state_rm.go index 14160bece..2beeca63e 100644 --- a/command/state_rm.go +++ b/command/state_rm.go @@ -1,12 +1,13 @@ package command import ( - "context" "fmt" "strings" "github.com/hashicorp/terraform/addrs" + "github.com/hashicorp/terraform/command/arguments" "github.com/hashicorp/terraform/command/clistate" + "github.com/hashicorp/terraform/command/views" "github.com/hashicorp/terraform/tfdiags" "github.com/mitchellh/cli" ) @@ -44,12 +45,16 @@ func (c *StateRmCommand) Run(args []string) int { } if c.stateLock { - stateLocker := clistate.NewLocker(context.Background(), c.stateLockTimeout, c.Ui, c.Colorize()) - if err := stateLocker.Lock(stateMgr, "state-rm"); err != nil { - c.Ui.Error(fmt.Sprintf("Error locking state: %s", err)) + stateLocker := clistate.NewLocker(c.stateLockTimeout, views.NewStateLocker(arguments.ViewHuman, c.View)) + if diags := stateLocker.Lock(stateMgr, "state-rm"); diags.HasErrors() { + c.showDiagnostics(diags) return 1 } - defer stateLocker.Unlock(nil) + defer func() { + if diags := stateLocker.Unlock(); diags.HasErrors() { + c.showDiagnostics(diags) + } + }() } if err := stateMgr.RefreshState(); err != nil { diff --git a/command/state_rm_test.go b/command/state_rm_test.go index d0f2c4fdd..e88cfff8b 100644 --- a/command/state_rm_test.go +++ b/command/state_rm_test.go @@ -49,11 +49,13 @@ func TestStateRm(t *testing.T) { p := testProvider() ui := new(cli.MockUi) + view, _ := testView(t) c := &StateRmCommand{ StateMeta{ Meta: Meta{ testingOverrides: metaOverridesForProvider(p), Ui: ui, + View: view, }, }, } @@ -117,11 +119,13 @@ func TestStateRmNotChildModule(t *testing.T) { p := testProvider() ui := new(cli.MockUi) + view, _ := testView(t) c := &StateRmCommand{ StateMeta{ Meta: Meta{ testingOverrides: metaOverridesForProvider(p), Ui: ui, + View: view, }, }, } @@ -203,11 +207,13 @@ func TestStateRmNoArgs(t *testing.T) { p := testProvider() ui := new(cli.MockUi) + view, _ := testView(t) c := &StateRmCommand{ StateMeta{ Meta: Meta{ testingOverrides: metaOverridesForProvider(p), Ui: ui, + View: view, }, }, } @@ -262,11 +268,13 @@ func TestStateRmNonExist(t *testing.T) { p := testProvider() ui := new(cli.MockUi) + view, _ := testView(t) c := &StateRmCommand{ StateMeta{ Meta: Meta{ testingOverrides: metaOverridesForProvider(p), Ui: ui, + View: view, }, }, } @@ -318,11 +326,13 @@ func TestStateRm_backupExplicit(t *testing.T) { p := testProvider() ui := new(cli.MockUi) + view, _ := testView(t) c := &StateRmCommand{ StateMeta{ Meta: Meta{ testingOverrides: metaOverridesForProvider(p), Ui: ui, + View: view, }, }, } @@ -349,11 +359,13 @@ func TestStateRm_noState(t *testing.T) { p := testProvider() ui := new(cli.MockUi) + view, _ := testView(t) c := &StateRmCommand{ StateMeta{ Meta: Meta{ testingOverrides: metaOverridesForProvider(p), Ui: ui, + View: view, }, }, } @@ -372,11 +384,13 @@ func TestStateRm_needsInit(t *testing.T) { p := testProvider() ui := new(cli.MockUi) + view, _ := testView(t) c := &StateRmCommand{ StateMeta{ Meta: Meta{ testingOverrides: metaOverridesForProvider(p), Ui: ui, + View: view, }, }, } @@ -446,11 +460,13 @@ func TestStateRm_backendState(t *testing.T) { p := testProvider() ui := new(cli.MockUi) + view, _ := testView(t) c := &StateRmCommand{ StateMeta{ Meta: Meta{ testingOverrides: metaOverridesForProvider(p), Ui: ui, + View: view, }, }, } diff --git a/command/taint.go b/command/taint.go index 443831112..4367c303c 100644 --- a/command/taint.go +++ b/command/taint.go @@ -1,13 +1,14 @@ package command import ( - "context" "fmt" "os" "strings" "github.com/hashicorp/terraform/addrs" + "github.com/hashicorp/terraform/command/arguments" "github.com/hashicorp/terraform/command/clistate" + "github.com/hashicorp/terraform/command/views" "github.com/hashicorp/terraform/states" "github.com/hashicorp/terraform/terraform" "github.com/hashicorp/terraform/tfdiags" @@ -116,12 +117,16 @@ func (c *TaintCommand) Run(args []string) int { } if c.stateLock { - stateLocker := clistate.NewLocker(context.Background(), c.stateLockTimeout, c.Ui, c.Colorize()) - if err := stateLocker.Lock(stateMgr, "taint"); err != nil { - c.Ui.Error(fmt.Sprintf("Error locking state: %s", err)) + stateLocker := clistate.NewLocker(c.stateLockTimeout, views.NewStateLocker(arguments.ViewHuman, c.View)) + if diags := stateLocker.Lock(stateMgr, "taint"); diags.HasErrors() { + c.showDiagnostics(diags) return 1 } - defer stateLocker.Unlock(nil) + defer func() { + if diags := stateLocker.Unlock(); diags.HasErrors() { + c.showDiagnostics(diags) + } + }() } if err := stateMgr.RefreshState(); err != nil { diff --git a/command/taint_test.go b/command/taint_test.go index 2e91a88b7..27171aec3 100644 --- a/command/taint_test.go +++ b/command/taint_test.go @@ -33,9 +33,11 @@ func TestTaint(t *testing.T) { statePath := testStateFile(t, state) ui := new(cli.MockUi) + view, _ := testView(t) c := &TaintCommand{ Meta: Meta{ - Ui: ui, + Ui: ui, + View: view, }, } @@ -76,9 +78,11 @@ func TestTaint_lockedState(t *testing.T) { } defer unlock() ui := new(cli.MockUi) + view, _ := testView(t) c := &TaintCommand{ Meta: Meta{ - Ui: ui, + Ui: ui, + View: view, }, } @@ -122,9 +126,11 @@ func TestTaint_backup(t *testing.T) { testStateFileDefault(t, state) ui := new(cli.MockUi) + view, _ := testView(t) c := &TaintCommand{ Meta: Meta{ - Ui: ui, + Ui: ui, + View: view, }, } @@ -165,9 +171,11 @@ func TestTaint_backupDisable(t *testing.T) { testStateFileDefault(t, state) ui := new(cli.MockUi) + view, _ := testView(t) c := &TaintCommand{ Meta: Meta{ - Ui: ui, + Ui: ui, + View: view, }, } @@ -188,9 +196,11 @@ func TestTaint_backupDisable(t *testing.T) { func TestTaint_badState(t *testing.T) { ui := new(cli.MockUi) + view, _ := testView(t) c := &TaintCommand{ Meta: Meta{ - Ui: ui, + Ui: ui, + View: view, }, } @@ -229,9 +239,11 @@ func TestTaint_defaultState(t *testing.T) { testStateFileDefault(t, state) ui := new(cli.MockUi) + view, _ := testView(t) c := &TaintCommand{ Meta: Meta{ - Ui: ui, + Ui: ui, + View: view, }, } @@ -271,7 +283,8 @@ func TestTaint_defaultWorkspaceState(t *testing.T) { path := testStateFileWorkspaceDefault(t, testWorkspace, state) ui := new(cli.MockUi) - meta := Meta{Ui: ui} + view, _ := testView(t) + meta := Meta{Ui: ui, View: view} meta.SetWorkspace(testWorkspace) c := &TaintCommand{ Meta: meta, @@ -308,9 +321,11 @@ func TestTaint_missing(t *testing.T) { statePath := testStateFile(t, state) ui := new(cli.MockUi) + view, _ := testView(t) c := &TaintCommand{ Meta: Meta{ - Ui: ui, + Ui: ui, + View: view, }, } @@ -344,9 +359,11 @@ func TestTaint_missingAllow(t *testing.T) { statePath := testStateFile(t, state) ui := new(cli.MockUi) + view, _ := testView(t) c := &TaintCommand{ Meta: Meta{ - Ui: ui, + Ui: ui, + View: view, }, } @@ -399,9 +416,11 @@ func TestTaint_stateOut(t *testing.T) { testStateFileDefault(t, state) ui := new(cli.MockUi) + view, _ := testView(t) c := &TaintCommand{ Meta: Meta{ - Ui: ui, + Ui: ui, + View: view, }, } @@ -453,9 +472,11 @@ func TestTaint_module(t *testing.T) { statePath := testStateFile(t, state) ui := new(cli.MockUi) + view, _ := testView(t) c := &TaintCommand{ Meta: Meta{ - Ui: ui, + Ui: ui, + View: view, }, } @@ -498,10 +519,12 @@ func TestTaint_checkRequiredVersion(t *testing.T) { path := testStateFile(t, state) ui := cli.NewMockUi() + view, _ := testView(t) c := &TaintCommand{ Meta: Meta{ testingOverrides: metaOverridesForProvider(testProvider()), Ui: ui, + View: view, }, } diff --git a/command/unlock_test.go b/command/unlock_test.go index 70f14c65a..0b42b0cbe 100644 --- a/command/unlock_test.go +++ b/command/unlock_test.go @@ -34,10 +34,12 @@ func TestUnlock(t *testing.T) { p := testProvider() ui := new(cli.MockUi) + view, _ := testView(t) c := &UnlockCommand{ Meta: Meta{ testingOverrides: metaOverridesForProvider(p), Ui: ui, + View: view, }, } @@ -72,9 +74,11 @@ func TestUnlock_inmemBackend(t *testing.T) { // init backend ui := new(cli.MockUi) + view, _ := testView(t) ci := &InitCommand{ Meta: Meta{ - Ui: ui, + Ui: ui, + View: view, }, } if code := ci.Run(nil); code != 0 { @@ -84,7 +88,8 @@ func TestUnlock_inmemBackend(t *testing.T) { ui = new(cli.MockUi) c := &UnlockCommand{ Meta: Meta{ - Ui: ui, + Ui: ui, + View: view, }, } @@ -101,7 +106,8 @@ func TestUnlock_inmemBackend(t *testing.T) { ui = new(cli.MockUi) c = &UnlockCommand{ Meta: Meta{ - Ui: ui, + Ui: ui, + View: view, }, } diff --git a/command/untaint.go b/command/untaint.go index bbc93294d..610fd4750 100644 --- a/command/untaint.go +++ b/command/untaint.go @@ -1,12 +1,13 @@ package command import ( - "context" "fmt" "strings" "github.com/hashicorp/terraform/addrs" + "github.com/hashicorp/terraform/command/arguments" "github.com/hashicorp/terraform/command/clistate" + "github.com/hashicorp/terraform/command/views" "github.com/hashicorp/terraform/states" "github.com/hashicorp/terraform/tfdiags" ) @@ -81,12 +82,16 @@ func (c *UntaintCommand) Run(args []string) int { } if c.stateLock { - stateLocker := clistate.NewLocker(context.Background(), c.stateLockTimeout, c.Ui, c.Colorize()) - if err := stateLocker.Lock(stateMgr, "untaint"); err != nil { - c.Ui.Error(fmt.Sprintf("Error locking state: %s", err)) + stateLocker := clistate.NewLocker(c.stateLockTimeout, views.NewStateLocker(arguments.ViewHuman, c.View)) + if diags := stateLocker.Lock(stateMgr, "untaint"); diags.HasErrors() { + c.showDiagnostics(diags) return 1 } - defer stateLocker.Unlock(nil) + defer func() { + if diags := stateLocker.Unlock(); diags.HasErrors() { + c.showDiagnostics(diags) + } + }() } if err := stateMgr.RefreshState(); err != nil { diff --git a/command/untaint_test.go b/command/untaint_test.go index b1b365fdb..5eba15a0b 100644 --- a/command/untaint_test.go +++ b/command/untaint_test.go @@ -32,9 +32,11 @@ func TestUntaint(t *testing.T) { statePath := testStateFile(t, state) ui := new(cli.MockUi) + view, _ := testView(t) c := &UntaintCommand{ Meta: Meta{ - Ui: ui, + Ui: ui, + View: view, }, } @@ -80,9 +82,11 @@ func TestUntaint_lockedState(t *testing.T) { defer unlock() ui := new(cli.MockUi) + view, _ := testView(t) c := &UntaintCommand{ Meta: Meta{ - Ui: ui, + Ui: ui, + View: view, }, } @@ -126,9 +130,11 @@ func TestUntaint_backup(t *testing.T) { testStateFileDefault(t, state) ui := new(cli.MockUi) + view, _ := testView(t) c := &UntaintCommand{ Meta: Meta{ - Ui: ui, + Ui: ui, + View: view, }, } @@ -180,9 +186,11 @@ func TestUntaint_backupDisable(t *testing.T) { testStateFileDefault(t, state) ui := new(cli.MockUi) + view, _ := testView(t) c := &UntaintCommand{ Meta: Meta{ - Ui: ui, + Ui: ui, + View: view, }, } @@ -207,9 +215,11 @@ test_instance.foo: func TestUntaint_badState(t *testing.T) { ui := new(cli.MockUi) + view, _ := testView(t) c := &UntaintCommand{ Meta: Meta{ - Ui: ui, + Ui: ui, + View: view, }, } @@ -248,9 +258,11 @@ func TestUntaint_defaultState(t *testing.T) { testStateFileDefault(t, state) ui := new(cli.MockUi) + view, _ := testView(t) c := &UntaintCommand{ Meta: Meta{ - Ui: ui, + Ui: ui, + View: view, }, } @@ -295,7 +307,8 @@ func TestUntaint_defaultWorkspaceState(t *testing.T) { path := testStateFileWorkspaceDefault(t, testWorkspace, state) ui := new(cli.MockUi) - meta := Meta{Ui: ui} + view, _ := testView(t) + meta := Meta{Ui: ui, View: view} meta.SetWorkspace(testWorkspace) c := &UntaintCommand{ Meta: meta, @@ -336,9 +349,11 @@ func TestUntaint_missing(t *testing.T) { statePath := testStateFile(t, state) ui := new(cli.MockUi) + view, _ := testView(t) c := &UntaintCommand{ Meta: Meta{ - Ui: ui, + Ui: ui, + View: view, }, } @@ -372,9 +387,11 @@ func TestUntaint_missingAllow(t *testing.T) { statePath := testStateFile(t, state) ui := new(cli.MockUi) + view, _ := testView(t) c := &UntaintCommand{ Meta: Meta{ - Ui: ui, + Ui: ui, + View: view, }, } @@ -427,9 +444,11 @@ func TestUntaint_stateOut(t *testing.T) { testStateFileDefault(t, state) ui := new(cli.MockUi) + view, _ := testView(t) c := &UntaintCommand{ Meta: Meta{ - Ui: ui, + Ui: ui, + View: view, }, } @@ -489,9 +508,11 @@ func TestUntaint_module(t *testing.T) { statePath := testStateFile(t, state) ui := new(cli.MockUi) + view, _ := testView(t) c := &UntaintCommand{ Meta: Meta{ - Ui: ui, + Ui: ui, + View: view, }, } diff --git a/command/views/state_locker.go b/command/views/state_locker.go new file mode 100644 index 000000000..3568c44e4 --- /dev/null +++ b/command/views/state_locker.go @@ -0,0 +1,40 @@ +package views + +import ( + "fmt" + + "github.com/hashicorp/terraform/command/arguments" +) + +// The StateLocker view is used to display locking/unlocking status messages +// if the state lock process takes longer than expected. +type StateLocker interface { + Locking() + Unlocking() +} + +// NewStateLocker returns an initialized StateLocker implementation for the given ViewType. +func NewStateLocker(vt arguments.ViewType, view *View) StateLocker { + switch vt { + case arguments.ViewHuman: + return &StateLockerHuman{View: *view} + default: + panic(fmt.Sprintf("unknown view type %v", vt)) + } +} + +// StateLockerHuman is an implementation of StateLocker which prints status to +// a terminal. +type StateLockerHuman struct { + View +} + +var _ StateLocker = (*StateLockerHuman)(nil) + +func (v *StateLockerHuman) Locking() { + v.streams.Println("Acquiring state lock. This may take a few moments...") +} + +func (v *StateLockerHuman) Unlocking() { + v.streams.Println("Releasing state lock. This may take a few moments...") +} diff --git a/command/workspace_command_test.go b/command/workspace_command_test.go index 0ffc7b8fe..99c156bbc 100644 --- a/command/workspace_command_test.go +++ b/command/workspace_command_test.go @@ -34,7 +34,8 @@ func TestWorkspace_createAndChange(t *testing.T) { args := []string{"test"} ui := new(cli.MockUi) - newCmd.Meta = Meta{Ui: ui} + view, _ := testView(t) + newCmd.Meta = Meta{Ui: ui, View: view} if code := newCmd.Run(args); code != 0 { t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter) } @@ -47,7 +48,7 @@ func TestWorkspace_createAndChange(t *testing.T) { selCmd := &WorkspaceSelectCommand{} args = []string{backend.DefaultStateName} ui = new(cli.MockUi) - selCmd.Meta = Meta{Ui: ui} + selCmd.Meta = Meta{Ui: ui, View: view} if code := selCmd.Run(args); code != 0 { t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter) } @@ -83,8 +84,9 @@ func TestWorkspace_createAndList(t *testing.T) { // create multiple workspaces for _, env := range envs { ui := new(cli.MockUi) + view, _ := testView(t) newCmd := &WorkspaceNewCommand{ - Meta: Meta{Ui: ui}, + Meta: Meta{Ui: ui, View: view}, } if code := newCmd.Run([]string{env}); code != 0 { t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter) @@ -93,7 +95,8 @@ func TestWorkspace_createAndList(t *testing.T) { listCmd := &WorkspaceListCommand{} ui := new(cli.MockUi) - listCmd.Meta = Meta{Ui: ui} + view, _ := testView(t) + listCmd.Meta = Meta{Ui: ui, View: view} if code := listCmd.Run(nil); code != 0 { t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter) @@ -128,7 +131,8 @@ func TestWorkspace_createAndShow(t *testing.T) { // make sure current workspace show outputs "default" showCmd := &WorkspaceShowCommand{} ui := new(cli.MockUi) - showCmd.Meta = Meta{Ui: ui} + view, _ := testView(t) + showCmd.Meta = Meta{Ui: ui, View: view} if code := showCmd.Run(nil); code != 0 { t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter) @@ -147,21 +151,21 @@ func TestWorkspace_createAndShow(t *testing.T) { // create test_a workspace ui = new(cli.MockUi) - newCmd.Meta = Meta{Ui: ui} + newCmd.Meta = Meta{Ui: ui, View: view} if code := newCmd.Run(env); code != 0 { t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter) } selCmd := &WorkspaceSelectCommand{} ui = new(cli.MockUi) - selCmd.Meta = Meta{Ui: ui} + selCmd.Meta = Meta{Ui: ui, View: view} if code := selCmd.Run(env); code != 0 { t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter) } showCmd = &WorkspaceShowCommand{} ui = new(cli.MockUi) - showCmd.Meta = Meta{Ui: ui} + showCmd.Meta = Meta{Ui: ui, View: view} if code := showCmd.Run(nil); code != 0 { t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter) @@ -188,8 +192,9 @@ func TestWorkspace_createInvalid(t *testing.T) { // create multiple workspaces for _, env := range envs { ui := new(cli.MockUi) + view, _ := testView(t) newCmd := &WorkspaceNewCommand{ - Meta: Meta{Ui: ui}, + Meta: Meta{Ui: ui, View: view}, } if code := newCmd.Run([]string{env}); code == 0 { t.Fatalf("expected failure: \n%s", ui.OutputWriter) @@ -199,7 +204,8 @@ func TestWorkspace_createInvalid(t *testing.T) { // list workspaces to make sure none were created listCmd := &WorkspaceListCommand{} ui := new(cli.MockUi) - listCmd.Meta = Meta{Ui: ui} + view, _ := testView(t) + listCmd.Meta = Meta{Ui: ui, View: view} if code := listCmd.Run(nil); code != 0 { t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter) @@ -222,8 +228,9 @@ func TestWorkspace_createWithState(t *testing.T) { // init the backend ui := new(cli.MockUi) + view, _ := testView(t) initCmd := &InitCommand{ - Meta: Meta{Ui: ui}, + Meta: Meta{Ui: ui, View: view}, } if code := initCmd.Run([]string{}); code != 0 { t.Fatalf("bad: \n%s", ui.ErrorWriter.String()) @@ -257,7 +264,7 @@ func TestWorkspace_createWithState(t *testing.T) { args := []string{"-state", "test.tfstate", workspace} ui = new(cli.MockUi) newCmd := &WorkspaceNewCommand{ - Meta: Meta{Ui: ui}, + Meta: Meta{Ui: ui, View: view}, } if code := newCmd.Run(args); code != 0 { t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter) @@ -303,8 +310,9 @@ func TestWorkspace_delete(t *testing.T) { } ui := new(cli.MockUi) + view, _ := testView(t) delCmd := &WorkspaceDeleteCommand{ - Meta: Meta{Ui: ui}, + Meta: Meta{Ui: ui, View: view}, } current, _ := delCmd.Workspace() @@ -352,8 +360,9 @@ func TestWorkspace_deleteInvalid(t *testing.T) { } ui := new(cli.MockUi) + view, _ := testView(t) delCmd := &WorkspaceDeleteCommand{ - Meta: Meta{Ui: ui}, + Meta: Meta{Ui: ui, View: view}, } // delete the workspace @@ -406,8 +415,9 @@ func TestWorkspace_deleteWithState(t *testing.T) { } ui := new(cli.MockUi) + view, _ := testView(t) delCmd := &WorkspaceDeleteCommand{ - Meta: Meta{Ui: ui}, + Meta: Meta{Ui: ui, View: view}, } args := []string{"test"} if code := delCmd.Run(args); code == 0 { diff --git a/command/workspace_delete.go b/command/workspace_delete.go index 578ac77a8..675795cec 100644 --- a/command/workspace_delete.go +++ b/command/workspace_delete.go @@ -1,12 +1,13 @@ package command import ( - "context" "fmt" "strings" "time" + "github.com/hashicorp/terraform/command/arguments" "github.com/hashicorp/terraform/command/clistate" + "github.com/hashicorp/terraform/command/views" "github.com/hashicorp/terraform/tfdiags" "github.com/mitchellh/cli" "github.com/posener/complete" @@ -107,9 +108,9 @@ func (c *WorkspaceDeleteCommand) Run(args []string) int { var stateLocker clistate.Locker if stateLock { - stateLocker = clistate.NewLocker(context.Background(), stateLockTimeout, c.Ui, c.Colorize()) - if err := stateLocker.Lock(stateMgr, "workspace_delete"); err != nil { - c.Ui.Error(fmt.Sprintf("Error locking state: %s", err)) + stateLocker = clistate.NewLocker(c.stateLockTimeout, views.NewStateLocker(arguments.ViewHuman, c.View)) + if diags := stateLocker.Lock(stateMgr, "state-replace-provider"); diags.HasErrors() { + c.showDiagnostics(diags) return 1 } } else { @@ -118,7 +119,7 @@ func (c *WorkspaceDeleteCommand) Run(args []string) int { if err := stateMgr.RefreshState(); err != nil { // We need to release the lock before exit - stateLocker.Unlock(nil) + stateLocker.Unlock() c.Ui.Error(err.Error()) return 1 } @@ -127,7 +128,7 @@ func (c *WorkspaceDeleteCommand) Run(args []string) int { if hasResources && !force { // We need to release the lock before exit - stateLocker.Unlock(nil) + stateLocker.Unlock() c.Ui.Error(fmt.Sprintf(strings.TrimSpace(envNotEmpty), workspace)) return 1 } @@ -141,7 +142,7 @@ func (c *WorkspaceDeleteCommand) Run(args []string) int { // state deletion, i.e. in a CI environment. Adding Delete() as a // required method of States would allow the removal of the resource to // be delegated from the Backend to the State itself. - stateLocker.Unlock(nil) + stateLocker.Unlock() err = b.DeleteWorkspace(workspace) if err != nil { diff --git a/command/workspace_new.go b/command/workspace_new.go index def5e50a8..051220939 100644 --- a/command/workspace_new.go +++ b/command/workspace_new.go @@ -1,13 +1,14 @@ package command import ( - "context" "fmt" "os" "strings" "time" + "github.com/hashicorp/terraform/command/arguments" "github.com/hashicorp/terraform/command/clistate" + "github.com/hashicorp/terraform/command/views" "github.com/hashicorp/terraform/states/statefile" "github.com/hashicorp/terraform/tfdiags" "github.com/mitchellh/cli" @@ -124,12 +125,16 @@ func (c *WorkspaceNewCommand) Run(args []string) int { } if stateLock { - stateLocker := clistate.NewLocker(context.Background(), stateLockTimeout, c.Ui, c.Colorize()) - if err := stateLocker.Lock(stateMgr, "workspace_new"); err != nil { - c.Ui.Error(fmt.Sprintf("Error locking state: %s", err)) + stateLocker := clistate.NewLocker(c.stateLockTimeout, views.NewStateLocker(arguments.ViewHuman, c.View)) + if diags := stateLocker.Lock(stateMgr, "workspace-new"); diags.HasErrors() { + c.showDiagnostics(diags) return 1 } - defer stateLocker.Unlock(nil) + defer func() { + if diags := stateLocker.Unlock(); diags.HasErrors() { + c.showDiagnostics(diags) + } + }() } // read the existing state file