command: Fix stale lock when exiting early

If an error occurs on creating the context for console or import, we
would fail to unlock the state. Fix this by unlocking slightly earlier.
Affects console and import commands.

Fixes #23318
This commit is contained in:
Alisdair McDiarmid 2020-02-14 13:47:29 -05:00
parent 980af1cd21
commit 701d095808
4 changed files with 96 additions and 11 deletions

View File

@ -96,11 +96,6 @@ 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
}
defer func() {
err := opReq.StateLocker.Unlock(nil)
@ -109,6 +104,12 @@ func (c *ConsoleCommand) Run(args []string) int {
}
}()
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(),

View File

@ -203,13 +203,7 @@ 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
}
// Make sure to unlock the state
defer func() {
err := opReq.StateLocker.Unlock(nil)
if err != nil {
@ -217,6 +211,12 @@ func (c *ImportCommand) Run(args []string) int {
}
}()
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.

View File

@ -258,6 +258,75 @@ func TestImport_remoteState(t *testing.T) {
testStateOutput(t, statePath, testImportStr)
}
// early failure on import should not leave stale lock
func TestImport_initializationErrorShouldUnlock(t *testing.T) {
td := tempDir(t)
copy.CopyDir(testFixturePath("import-provider-remote-state"), td)
defer os.RemoveAll(td)
defer testChdir(t, td)()
statePath := "imported.tfstate"
// init our backend
ui := cli.NewMockUi()
m := Meta{
testingOverrides: metaOverridesForProvider(testProvider()),
Ui: ui,
}
ic := &InitCommand{
Meta: m,
providerInstaller: &mockProviderInstaller{
Providers: map[string][]string{
"test": []string{"1.2.3"},
},
Dir: m.pluginDir(),
},
}
// (Using log here rather than t.Log so that these messages interleave with other trace logs)
log.Print("[TRACE] TestImport_initializationErrorShouldUnlock running: terraform init")
if code := ic.Run([]string{}); code != 0 {
t.Fatalf("init failed\n%s", ui.ErrorWriter)
}
// overwrite the config with one including a resource from an invalid provider
copy.CopyFile(filepath.Join(testFixturePath("import-provider-invalid"), "main.tf"), filepath.Join(td, "main.tf"))
p := testProvider()
ui = new(cli.MockUi)
c := &ImportCommand{
Meta: Meta{
testingOverrides: metaOverridesForProvider(p),
Ui: ui,
},
}
args := []string{
"unknown_instance.baz",
"bar",
}
log.Printf("[TRACE] TestImport_initializationErrorShouldUnlock running: terraform import %s %s", args[0], args[1])
// this should fail
if code := c.Run(args); code != 1 {
fmt.Println(ui.OutputWriter)
t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String())
}
// specifically, it should fail due to a missing provider
msg := ui.ErrorWriter.String()
if want := "Could not satisfy plugin requirements"; !strings.Contains(msg, want) {
t.Errorf("incorrect message\nwant substring: %s\ngot:\n%s", want, msg)
}
// verify that the local state was unlocked after initialization error
if _, err := os.Stat(filepath.Join(td, fmt.Sprintf(".%s.lock.info", statePath))); !os.IsNotExist(err) {
t.Fatal("state left locked after import")
}
}
func TestImport_providerConfigWithVar(t *testing.T) {
defer testChdir(t, testFixturePath("import-provider-var"))()

View File

@ -0,0 +1,15 @@
terraform {
backend "local" {
path = "imported.tfstate"
}
}
provider "test" {
foo = "bar"
}
resource "test_instance" "foo" {
}
resource "unknown_instance" "baz" {
}