Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 0 additions & 10 deletions backend/local/backend.go
Original file line number Diff line number Diff line change
Expand Up @@ -336,16 +336,6 @@ func (b *Local) Operation(ctx context.Context, op *backend.Operation) (*backend.
defer stop()
defer cancel()

// the state was locked during context creation, unlock the state when
// the operation completes
defer func() {
err := op.StateLocker.Unlock(nil)
if err != nil {
b.ShowDiagnostics(err)
runningOp.Result = backend.OperationFailure
}
}()

defer b.opLock.Unlock()
f(stopCtx, cancelCtx, op, runningOp)
}()
Expand Down
9 changes: 9 additions & 0 deletions backend/local/backend_apply.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,15 @@ func (b *Local) opApply(
b.ReportResult(runningOp, diags)
return
}
// 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 {
b.ShowDiagnostics(err)
runningOp.Result = backend.OperationFailure
}
}()

// Before we do anything else we'll take a snapshot of the prior state
// so we can use it for some fixups to our detection of whether the plan
Expand Down
12 changes: 12 additions & 0 deletions backend/local/backend_local.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,18 @@ func (b *Local) context(op *backend.Operation) (*terraform.Context, *configload.
diags = diags.Append(errwrap.Wrapf("Error locking state: {{err}}", err))
return nil, nil, nil, diags
}

defer func() {
// 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))
}
}
}()

log.Printf("[TRACE] backend/local: reading remote state for workspace %q", op.Workspace)
if err := s.RefreshState(); err != nil {
diags = diags.Append(errwrap.Wrapf("Error loading state: {{err}}", err))
Expand Down
64 changes: 64 additions & 0 deletions backend/local/backend_local_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package local
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sweet! New test file!


import (
"testing"

"github.com/hashicorp/terraform/backend"
"github.com/hashicorp/terraform/internal/initwd"
"github.com/hashicorp/terraform/states/statemgr"
)

func TestLocalContext(t *testing.T) {
configDir := "./testdata/empty"
b, cleanup := TestLocal(t)
defer cleanup()

_, configLoader, configCleanup := initwd.MustLoadConfigForTests(t, configDir)
defer configCleanup()

op := &backend.Operation{
ConfigDir: configDir,
ConfigLoader: configLoader,
Workspace: backend.DefaultStateName,
LockState: true,
}

_, _, diags := b.Context(op)
if diags.HasErrors() {
t.Fatalf("unexpected error: %s", diags.Err().Error())
}

// Context() retains a lock on success, so this should fail.
stateMgr, _ := b.StateMgr(backend.DefaultStateName)
if _, err := stateMgr.Lock(statemgr.NewLockInfo()); err == nil {
t.Fatalf("unexpected success locking state")
}
}

func TestLocalContext_error(t *testing.T) {
configDir := "./testdata/apply-error"
b, cleanup := TestLocal(t)
defer cleanup()

_, configLoader, configCleanup := initwd.MustLoadConfigForTests(t, configDir)
defer configCleanup()

op := &backend.Operation{
ConfigDir: configDir,
ConfigLoader: configLoader,
Workspace: backend.DefaultStateName,
LockState: true,
}

_, _, diags := b.Context(op)
if !diags.HasErrors() {
t.Fatal("unexpected success")
}

// When Context() returns an error, it also unlocks the state.
// This should therefore succeed.
stateMgr, _ := b.StateMgr(backend.DefaultStateName)
if _, err := stateMgr.Lock(statemgr.NewLockInfo()); err != nil {
t.Fatalf("unexpected error locking state: %s", err.Error())
}
}
9 changes: 9 additions & 0 deletions backend/local/backend_plan.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,15 @@ func (b *Local) opPlan(
b.ReportResult(runningOp, diags)
return
}
// 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 {
b.ShowDiagnostics(err)
runningOp.Result = backend.OperationFailure
}
}()

// Before we do anything else we'll take a snapshot of the prior state
// so we can use it for some fixups to our detection of whether the plan
Expand Down
10 changes: 10 additions & 0 deletions backend/local/backend_refresh.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,16 @@ func (b *Local) opRefresh(
return
}

// 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 {
b.ShowDiagnostics(err)
runningOp.Result = backend.OperationFailure
}
}()

// Set our state
runningOp.State = opState.State()
if !runningOp.State.HasResources() {
Expand Down
4 changes: 2 additions & 2 deletions backend/remote/backend.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ import (
"github.com/hashicorp/terraform-svchost/disco"
"github.com/hashicorp/terraform/backend"
"github.com/hashicorp/terraform/configs/configschema"
"github.com/hashicorp/terraform/state"
"github.com/hashicorp/terraform/state/remote"
"github.com/hashicorp/terraform/states/statemgr"
"github.com/hashicorp/terraform/terraform"
"github.com/hashicorp/terraform/tfdiags"
tfversion "github.com/hashicorp/terraform/version"
Expand Down Expand Up @@ -591,7 +591,7 @@ func (b *Remote) DeleteWorkspace(name string) error {
}

// StateMgr implements backend.Enhanced.
func (b *Remote) StateMgr(name string) (state.State, error) {
func (b *Remote) StateMgr(name string) (statemgr.Full, error) {
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sneaking in an unrelated change to start removing references to the deprecated state package:

// State is a deprecated alias for statemgr.Full

if b.workspace == "" && name == backend.DefaultStateName {
return nil, backend.ErrDefaultWorkspaceNotSupported
}
Expand Down
18 changes: 18 additions & 0 deletions backend/remote/backend_apply_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (
"github.com/hashicorp/terraform/backend"
"github.com/hashicorp/terraform/internal/initwd"
"github.com/hashicorp/terraform/plans/planfile"
"github.com/hashicorp/terraform/states/statemgr"
"github.com/hashicorp/terraform/terraform"
"github.com/mitchellh/cli"
)
Expand Down Expand Up @@ -75,6 +76,12 @@ func TestRemote_applyBasic(t *testing.T) {
if !strings.Contains(output, "1 added, 0 changed, 0 destroyed") {
t.Fatalf("expected apply summery in output: %s", output)
}

stateMgr, _ := b.StateMgr(backend.DefaultStateName)
// An error suggests that the state was not unlocked after apply
if _, err := stateMgr.Lock(statemgr.NewLockInfo()); err != nil {
t.Fatalf("unexpected error locking state after apply: %s", err.Error())
}
}

func TestRemote_applyCanceled(t *testing.T) {
Expand All @@ -98,6 +105,11 @@ func TestRemote_applyCanceled(t *testing.T) {
if run.Result == backend.OperationSuccess {
t.Fatal("expected apply operation to fail")
}

stateMgr, _ := b.StateMgr(backend.DefaultStateName)
if _, err := stateMgr.Lock(statemgr.NewLockInfo()); err != nil {
t.Fatalf("unexpected error locking state after cancelling apply: %s", err.Error())
}
}

func TestRemote_applyWithoutPermissions(t *testing.T) {
Expand Down Expand Up @@ -386,6 +398,12 @@ func TestRemote_applyNoConfig(t *testing.T) {
if !strings.Contains(errOutput, "configuration files found") {
t.Fatalf("expected configuration files error, got: %v", errOutput)
}

stateMgr, _ := b.StateMgr(backend.DefaultStateName)
// An error suggests that the state was not unlocked after apply
if _, err := stateMgr.Lock(statemgr.NewLockInfo()); err != nil {
t.Fatalf("unexpected error locking state after failed apply: %s", err.Error())
}
}

func TestRemote_applyNoChanges(t *testing.T) {
Expand Down
13 changes: 13 additions & 0 deletions backend/remote/backend_context_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"github.com/hashicorp/terraform/backend"
"github.com/hashicorp/terraform/configs"
"github.com/hashicorp/terraform/internal/initwd"
"github.com/hashicorp/terraform/states/statemgr"
"github.com/zclconf/go-cty/cty"
)

Expand Down Expand Up @@ -186,6 +187,7 @@ func TestRemoteContextWithVars(t *testing.T) {
ConfigDir: configDir,
ConfigLoader: configLoader,
Workspace: backend.DefaultStateName,
LockState: true,
}

v := test.Opts
Expand All @@ -205,10 +207,21 @@ func TestRemoteContextWithVars(t *testing.T) {
if errStr != test.WantError {
t.Fatalf("wrong error\ngot: %s\nwant: %s", errStr, test.WantError)
}
// When Context() returns an error, it should unlock the state,
// so re-locking it is expected to succeed.
stateMgr, _ := b.StateMgr(backend.DefaultStateName)
if _, err := stateMgr.Lock(statemgr.NewLockInfo()); err != nil {
t.Fatalf("unexpected error locking state: %s", err.Error())
}
} else {
if diags.HasErrors() {
t.Fatalf("unexpected error\ngot: %s\nwant: <no error>", diags.Err().Error())
}
// When Context() succeeds, this should fail w/ "workspace already locked"
stateMgr, _ := b.StateMgr(backend.DefaultStateName)
if _, err := stateMgr.Lock(statemgr.NewLockInfo()); err == nil {
t.Fatal("unexpected success locking state after Context")
}
}
})
}
Expand Down
13 changes: 13 additions & 0 deletions backend/remote/backend_plan_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (
"github.com/hashicorp/terraform/backend"
"github.com/hashicorp/terraform/internal/initwd"
"github.com/hashicorp/terraform/plans/planfile"
"github.com/hashicorp/terraform/states/statemgr"
"github.com/hashicorp/terraform/terraform"
"github.com/mitchellh/cli"
)
Expand Down Expand Up @@ -62,6 +63,12 @@ func TestRemote_planBasic(t *testing.T) {
if !strings.Contains(output, "1 to add, 0 to change, 0 to destroy") {
t.Fatalf("expected plan summary in output: %s", output)
}

stateMgr, _ := b.StateMgr(backend.DefaultStateName)
// An error suggests that the state was not unlocked after the operation finished
if _, err := stateMgr.Lock(statemgr.NewLockInfo()); err != nil {
t.Fatalf("unexpected error locking state after successful plan: %s", err.Error())
}
}

func TestRemote_planCanceled(t *testing.T) {
Expand All @@ -85,6 +92,12 @@ func TestRemote_planCanceled(t *testing.T) {
if run.Result == backend.OperationSuccess {
t.Fatal("expected plan operation to fail")
}

stateMgr, _ := b.StateMgr(backend.DefaultStateName)
// An error suggests that the state was not unlocked after the operation finished
if _, err := stateMgr.Lock(statemgr.NewLockInfo()); err != nil {
t.Fatalf("unexpected error locking state after cancelled plan: %s", err.Error())
}
}

func TestRemote_planLongLine(t *testing.T) {
Expand Down
13 changes: 6 additions & 7 deletions command/console.go
Original file line number Diff line number Diff line change
Expand Up @@ -92,21 +92,20 @@ func (c *ConsoleCommand) Run(args []string) int {

// Get the context
ctx, _, ctxDiags := local.Context(opReq)
diags = diags.Append(ctxDiags)
if ctxDiags.HasErrors() {
c.showDiagnostics(diags)
return 1
}

// Creating the context can result in a lock, so ensure we release it
// 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 = diags.Append(ctxDiags)
if ctxDiags.HasErrors() {
c.showDiagnostics(diags)
return 1
}

// Setup the UI so we can output directly to stdout
ui := &cli.BasicUi{
Writer: wrappedstreams.Stdout(),
Expand Down
13 changes: 6 additions & 7 deletions command/import.go
Original file line number Diff line number Diff line change
Expand Up @@ -200,21 +200,20 @@ func (c *ImportCommand) Run(args []string) int {

// Get the context
ctx, state, ctxDiags := local.Context(opReq)
diags = diags.Append(ctxDiags)
if ctxDiags.HasErrors() {
c.showDiagnostics(diags)
return 1
}

// Creating the context can result in a lock, so ensure we release it
// 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 = diags.Append(ctxDiags)
if ctxDiags.HasErrors() {
c.showDiagnostics(diags)
return 1
}

// Perform the import. Note that as you can see it is possible for this
// API to import more than one resource at once. For now, we only allow
// one while we stabilize this feature.
Expand Down