From 2d7dc605a01307f6c9a98c59b49c331d2bca5a18 Mon Sep 17 00:00:00 2001 From: James Bardin Date: Thu, 15 Mar 2018 10:50:17 -0400 Subject: [PATCH 1/7] get communicator errors from a remote.Cmd The remote.Cmd struct could not convey any transport related error to the caller, meaning that interrupted commands would show that they succeeded. Change Cmd.SetExited to accept an exit status, as well as an error to store for the caller. Make the status and error fields internal, require serialized access through the getter methods. Users of remote.Cmd should not check both Cmd.Err() and Cmd.ExitStatus() until after Wait returns. Require communicators to call Cmd.Init before executing the command. This will indicate incorrect usage of the remote.Cmd by causing a panic in SetExitStatus. --- communicator/communicator_mock.go | 4 +- communicator/remote/command.go | 70 ++++++++++++++++----------- communicator/ssh/communicator.go | 10 ++-- communicator/ssh/communicator_test.go | 50 +++++++++++++++++++ communicator/winrm/communicator.go | 5 +- 5 files changed, 105 insertions(+), 34 deletions(-) diff --git a/communicator/communicator_mock.go b/communicator/communicator_mock.go index f1c5ad5e6f..7f887b4d30 100644 --- a/communicator/communicator_mock.go +++ b/communicator/communicator_mock.go @@ -42,11 +42,13 @@ func (c *MockCommunicator) ScriptPath() string { // Start implementation of communicator.Communicator interface func (c *MockCommunicator) Start(r *remote.Cmd) error { + r.Init() + if !c.Commands[r.Command] { return fmt.Errorf("Command not found!") } - r.SetExited(0) + r.SetExitStatus(0, nil) return nil } diff --git a/communicator/remote/command.go b/communicator/remote/command.go index ae16dfa882..0804e70fa5 100644 --- a/communicator/remote/command.go +++ b/communicator/remote/command.go @@ -23,45 +23,59 @@ type Cmd struct { Stdout io.Writer Stderr io.Writer - // This will be set to true when the remote command has exited. It - // shouldn't be set manually by the user, but there is no harm in - // doing so. - Exited bool - - // Once Exited is true, this will contain the exit code of the process. - ExitStatus int + // Once Wait returns, his will contain the exit code of the process. + exitStatus int // Internal fields exitCh chan struct{} + // err is used to store any error reported by the Communicator during + // execution. + err error + // This thing is a mutex, lock when making modifications concurrently sync.Mutex } -// SetExited is a helper for setting that this process is exited. This -// should be called by communicators who are running a remote command in -// order to set that the command is done. -func (r *Cmd) SetExited(status int) { - r.Lock() - defer r.Unlock() +// Init must be called by the Communicator before executing the command. +func (c *Cmd) Init() { + c.Lock() + defer c.Unlock() + + c.exitCh = make(chan struct{}) +} + +// SetExitStatus stores the exit status of the remote command as well as any +// communicator related error. SetExitStatus then unblocks any pending calls +// to Wait. +// This should only be called by communicators executing the remote.Cmd. +func (c *Cmd) SetExitStatus(status int, err error) { + c.Lock() + defer c.Unlock() + + c.exitStatus = status + c.err = err + + close(c.exitCh) +} + +// Err returns any communicator related error. +func (c *Cmd) Err() error { + c.Lock() + defer c.Unlock() + + return c.err +} - if r.exitCh == nil { - r.exitCh = make(chan struct{}) - } +// ExitStatus returns the exit status of the remote command +func (c *Cmd) ExitStatus() int { + c.Lock() + defer c.Unlock() - r.Exited = true - r.ExitStatus = status - close(r.exitCh) + return c.exitStatus } // Wait waits for the remote command to complete. -func (r *Cmd) Wait() { - // Make sure our condition variable is initialized. - r.Lock() - if r.exitCh == nil { - r.exitCh = make(chan struct{}) - } - r.Unlock() - - <-r.exitCh +func (c *Cmd) Wait() { + <-c.exitCh } diff --git a/communicator/ssh/communicator.go b/communicator/ssh/communicator.go index f9d1839765..4945f5dd8a 100644 --- a/communicator/ssh/communicator.go +++ b/communicator/ssh/communicator.go @@ -243,6 +243,8 @@ func (c *Communicator) ScriptPath() string { // Start implementation of communicator.Communicator interface func (c *Communicator) Start(cmd *remote.Cmd) error { + cmd.Init() + session, err := c.newSession() if err != nil { return err @@ -267,7 +269,7 @@ func (c *Communicator) Start(cmd *remote.Cmd) error { } log.Printf("starting remote command: %s", cmd.Command) - err = session.Start(cmd.Command + "\n") + err = session.Start(strings.TrimSpace(cmd.Command) + "\n") if err != nil { return err } @@ -286,8 +288,8 @@ func (c *Communicator) Start(cmd *remote.Cmd) error { } } + cmd.SetExitStatus(exitStatus, err) log.Printf("remote command exited with '%d': %s", exitStatus, cmd.Command) - cmd.SetExited(exitStatus) }() return nil @@ -358,10 +360,10 @@ func (c *Communicator) UploadScript(path string, input io.Reader) error { "machine: %s", err) } cmd.Wait() - if cmd.ExitStatus != 0 { + if cmd.ExitStatus() != 0 { return fmt.Errorf( "Error chmodding script file to 0777 in remote "+ - "machine %d: %s %s", cmd.ExitStatus, stdout.String(), stderr.String()) + "machine %d: %s %s", cmd.ExitStatus(), stdout.String(), stderr.String()) } return nil diff --git a/communicator/ssh/communicator_test.go b/communicator/ssh/communicator_test.go index 9a2d715f28..4eb3799ce5 100644 --- a/communicator/ssh/communicator_test.go +++ b/communicator/ssh/communicator_test.go @@ -17,6 +17,7 @@ import ( "strconv" "strings" "testing" + "time" "github.com/hashicorp/terraform/communicator/remote" "github.com/hashicorp/terraform/terraform" @@ -178,6 +179,55 @@ func TestStart(t *testing.T) { } } +func TestLostConnection(t *testing.T) { + address := newMockLineServer(t, nil) + parts := strings.Split(address, ":") + + r := &terraform.InstanceState{ + Ephemeral: terraform.EphemeralState{ + ConnInfo: map[string]string{ + "type": "ssh", + "user": "user", + "password": "pass", + "host": parts[0], + "port": parts[1], + "timeout": "30s", + }, + }, + } + + c, err := New(r) + if err != nil { + t.Fatalf("error creating communicator: %s", err) + } + + var cmd remote.Cmd + stdout := new(bytes.Buffer) + cmd.Command = "echo foo" + cmd.Stdout = stdout + + err = c.Start(&cmd) + if err != nil { + t.Fatalf("error executing remote command: %s", err) + } + + // The test server can't execute anything, so Wait will block, unless + // there's an error. Disconnect the communicator transport, to cause the + // command to fail. + go func() { + time.Sleep(100 * time.Millisecond) + c.Disconnect() + }() + + cmd.Wait() + if cmd.Err() == nil { + t.Fatal("expected communicator error") + } + if cmd.ExitStatus() != 0 { + t.Fatal("command should not have returned an exit status") + } +} + func TestHostKey(t *testing.T) { // get the server's public key signer, err := ssh.ParsePrivateKey([]byte(testServerPrivateKey)) diff --git a/communicator/winrm/communicator.go b/communicator/winrm/communicator.go index 729e6eb51b..90b9fe9155 100644 --- a/communicator/winrm/communicator.go +++ b/communicator/winrm/communicator.go @@ -131,6 +131,8 @@ func (c *Communicator) ScriptPath() string { // Start implementation of communicator.Communicator interface func (c *Communicator) Start(rc *remote.Cmd) error { + rc.Init() + err := c.Connect(nil) if err != nil { return err @@ -168,7 +170,8 @@ func runCommand(shell *winrm.Shell, cmd *winrm.Command, rc *remote.Cmd) { cmd.Wait() wg.Wait() - rc.SetExited(cmd.ExitCode()) + + rc.SetExitStatus(cmd.ExitCode(), nil) } // Upload implementation of communicator.Communicator interface From af132a186d1525724682bd99c6c9616a7582f1b6 Mon Sep 17 00:00:00 2001 From: James Bardin Date: Thu, 15 Mar 2018 12:25:44 -0400 Subject: [PATCH 2/7] remove timeout from remote-exec command context The timeout for the remote command was taken from the wrong config field, and the connection timeout was being used which is 5 min. Any remote command taking more than 5 min would be terminated by disconnecting the communicator. Remove the timeout from the context, and rely on the global timeout provided by terraform. There was no way to get the error from the communicator previously, so the broken connection was silently ignored and the provisioner returned successfully. Now we can use the new cmd.Err() method to retrieve any errors encountered during execution. --- .../remote-exec/resource_provisioner.go | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/builtin/provisioners/remote-exec/resource_provisioner.go b/builtin/provisioners/remote-exec/resource_provisioner.go index 378a282ed7..8c3d671b9a 100644 --- a/builtin/provisioners/remote-exec/resource_provisioner.go +++ b/builtin/provisioners/remote-exec/resource_provisioner.go @@ -156,10 +156,6 @@ func runScripts( o terraform.UIOutput, comm communicator.Communicator, scripts []io.ReadCloser) error { - // Wrap out context in a cancelation function that we use to - // kill the connection. - ctx, cancelFunc := context.WithTimeout(ctx, comm.Timeout()) - defer cancelFunc() // Wait for the context to end and then disconnect go func() { @@ -200,10 +196,14 @@ func runScripts( if err := comm.Start(cmd); err != nil { return fmt.Errorf("Error starting script: %v", err) } - cmd.Wait() - if cmd.ExitStatus != 0 { - err = fmt.Errorf("Script exited with non-zero exit status: %d", cmd.ExitStatus) + + if err := cmd.Err(); err != nil { + return fmt.Errorf("Remote command exited with error: %s", err) + } + + if cmd.ExitStatus() != 0 { + err = fmt.Errorf("Script exited with non-zero exit status: %d", cmd.ExitStatus()) } // Upload a blank follow up file in the same path to prevent residual From b21483483415c4267f2a6d42ef0c9adc674d9204 Mon Sep 17 00:00:00 2001 From: James Bardin Date: Thu, 15 Mar 2018 12:30:27 -0400 Subject: [PATCH 3/7] update the vendored winrm release This was updated to see if we can get at any error status from the remote command and transport, which still is not available, but kept the latest version since it fixes a couple race conditions. --- vendor/github.com/masterzen/winrm/client.go | 33 +++++++++++++++++---- vendor/vendor.json | 14 +++++---- 2 files changed, 37 insertions(+), 10 deletions(-) diff --git a/vendor/github.com/masterzen/winrm/client.go b/vendor/github.com/masterzen/winrm/client.go index 732dd61cb9..c19515194a 100644 --- a/vendor/github.com/masterzen/winrm/client.go +++ b/vendor/github.com/masterzen/winrm/client.go @@ -152,10 +152,20 @@ func (c *Client) RunWithString(command string, stdin string) (string, string, in } var outWriter, errWriter bytes.Buffer - go io.Copy(&outWriter, cmd.Stdout) - go io.Copy(&errWriter, cmd.Stderr) + var wg sync.WaitGroup + wg.Add(2) + go func() { + defer wg.Done() + io.Copy(&outWriter, cmd.Stdout) + }() + + go func() { + defer wg.Done() + io.Copy(&errWriter, cmd.Stderr) + }() cmd.Wait() + wg.Wait() return outWriter.String(), errWriter.String(), cmd.ExitCode(), cmd.err } @@ -176,11 +186,24 @@ func (c Client) RunWithInput(command string, stdout, stderr io.Writer, stdin io. return 1, err } - go io.Copy(cmd.Stdin, stdin) - go io.Copy(stdout, cmd.Stdout) - go io.Copy(stderr, cmd.Stderr) + var wg sync.WaitGroup + wg.Add(3) + + go func() { + defer wg.Done() + io.Copy(cmd.Stdin, stdin) + }() + go func() { + defer wg.Done() + io.Copy(stdout, cmd.Stdout) + }() + go func() { + defer wg.Done() + io.Copy(stderr, cmd.Stderr) + }() cmd.Wait() + wg.Wait() return cmd.ExitCode(), cmd.err diff --git a/vendor/vendor.json b/vendor/vendor.json index d6e32b45e4..df59aea56c 100644 --- a/vendor/vendor.json +++ b/vendor/vendor.json @@ -1983,16 +1983,20 @@ "revisionTime": "2016-06-08T18:30:07Z" }, { - "checksumSHA1": "8z5kCCFRsBkhXic9jxxeIV3bBn8=", + "checksumSHA1": "dVQEUn5TxdIAXczK7rh6qUrq44Q=", "path": "github.com/masterzen/winrm", - "revision": "a2df6b1315e6fd5885eb15c67ed259e85854125f", - "revisionTime": "2017-08-14T13:39:27Z" + "revision": "7e40f93ae939004a1ef3bd5ff5c88c756ee762bb", + "revisionTime": "2018-02-24T16:03:50Z", + "version": "master", + "versionExact": "master" }, { "checksumSHA1": "XFSXma+KmkhkIPsh4dTd/eyja5s=", "path": "github.com/masterzen/winrm/soap", - "revision": "a2df6b1315e6fd5885eb15c67ed259e85854125f", - "revisionTime": "2017-08-14T13:39:27Z" + "revision": "7e40f93ae939004a1ef3bd5ff5c88c756ee762bb", + "revisionTime": "2018-02-24T16:03:50Z", + "version": "master", + "versionExact": "master" }, { "checksumSHA1": "rCffFCN6TpDAN3Jylyo8RFzhQ9E=", From a715430d247d84b0a96eca17a7f67076cd26139f Mon Sep 17 00:00:00 2001 From: James Bardin Date: Thu, 15 Mar 2018 12:45:49 -0400 Subject: [PATCH 4/7] fix exit status handling in salt-masterless Convert to the new Cmd.ExitStatus() method in the salt-masterless provisioner. Add calls to Wait and remove race conditions around setting the status. --- .../salt-masterless/resource_provisioner.go | 49 +++++++++++-------- 1 file changed, 28 insertions(+), 21 deletions(-) diff --git a/builtin/provisioners/salt-masterless/resource_provisioner.go b/builtin/provisioners/salt-masterless/resource_provisioner.go index d206423bc6..dd19d419ec 100644 --- a/builtin/provisioners/salt-masterless/resource_provisioner.go +++ b/builtin/provisioners/salt-masterless/resource_provisioner.go @@ -164,8 +164,10 @@ func applyFn(ctx context.Context) error { if err == nil { cmd.Wait() - if cmd.ExitStatus != 0 { - err = fmt.Errorf("Curl exited with non-zero exit status: %d", cmd.ExitStatus) + if cmd.Err() != nil { + err = cmd.Err() + } else if cmd.ExitStatus() != 0 { + err = fmt.Errorf("Curl exited with non-zero exit status: %d", cmd.ExitStatus()) } } @@ -188,8 +190,10 @@ func applyFn(ctx context.Context) error { if err == nil { cmd.Wait() - if cmd.ExitStatus != 0 { - err = fmt.Errorf("install_salt.sh exited with non-zero exit status: %d", cmd.ExitStatus) + if cmd.Err() != nil { + err = cmd.Err() + } else if cmd.ExitStatus() != 0 { + err = fmt.Errorf("install_salt.sh exited with non-zero exit status: %d", cmd.ExitStatus()) } } // Wait for output to clean up @@ -277,17 +281,16 @@ func applyFn(ctx context.Context) error { Stdout: outW, Stderr: errW, } - if err = comm.Start(cmd); err != nil || cmd.ExitStatus != 0 { - if err == nil { - err = fmt.Errorf("Bad exit status: %d", cmd.ExitStatus) - } - + if err = comm.Start(cmd); err != nil { err = fmt.Errorf("Error executing salt-call: %s", err) } + if err == nil { cmd.Wait() - if cmd.ExitStatus != 0 { - err = fmt.Errorf("Script exited with non-zero exit status: %d", cmd.ExitStatus) + if cmd.Err() != nil { + err = cmd.Err() + } else if cmd.ExitStatus() != 0 { + err = fmt.Errorf("Script exited with non-zero exit status: %d", cmd.ExitStatus()) } } // Wait for output to clean up @@ -354,14 +357,15 @@ func (p *provisioner) uploadFile(o terraform.UIOutput, comm communicator.Communi func (p *provisioner) moveFile(o terraform.UIOutput, comm communicator.Communicator, dst, src string) error { o.Output(fmt.Sprintf("Moving %s to %s", src, dst)) cmd := &remote.Cmd{Command: fmt.Sprintf(p.sudo("mv %s %s"), src, dst)} - if err := comm.Start(cmd); err != nil || cmd.ExitStatus != 0 { - if err == nil { - err = fmt.Errorf("Bad exit status: %d", cmd.ExitStatus) - } - + if err := comm.Start(cmd); err != nil { return fmt.Errorf("Unable to move %s to %s: %s", src, dst, err) } - return nil + cmd.Wait() + if cmd.ExitStatus() != 0 { + return fmt.Errorf("Unable to move %s to %s: exit status: %d", src, dst, cmd.ExitStatus()) + } + + return cmd.Err() } func (p *provisioner) createDir(o terraform.UIOutput, comm communicator.Communicator, dir string) error { @@ -372,10 +376,12 @@ func (p *provisioner) createDir(o terraform.UIOutput, comm communicator.Communic if err := comm.Start(cmd); err != nil { return err } - if cmd.ExitStatus != 0 { + + cmd.Wait() + if cmd.ExitStatus() != 0 { return fmt.Errorf("Non-zero exit status.") } - return nil + return cmd.Err() } func (p *provisioner) removeDir(o terraform.UIOutput, comm communicator.Communicator, dir string) error { @@ -386,10 +392,11 @@ func (p *provisioner) removeDir(o terraform.UIOutput, comm communicator.Communic if err := comm.Start(cmd); err != nil { return err } - if cmd.ExitStatus != 0 { + cmd.Wait() + if cmd.ExitStatus() != 0 { return fmt.Errorf("Non-zero exit status.") } - return nil + return cmd.Err() } func (p *provisioner) uploadDir(o terraform.UIOutput, comm communicator.Communicator, dst, src string, ignore []string) error { From a1061ed9311487d0bb16357eb5dd7ed770cc7389 Mon Sep 17 00:00:00 2001 From: James Bardin Date: Thu, 15 Mar 2018 12:55:57 -0400 Subject: [PATCH 5/7] update the chef and habitat error handling Use the new ExitStatus method, and also check the cmd.Err() method for errors. Remove leaks from the output goroutines in both provisioners by deferring their cleanup, and returning early on all error conditions. --- .../provisioners/chef/resource_provisioner.go | 22 ++++++++++-------- .../habitat/resource_provisioner.go | 23 +++++++++++-------- 2 files changed, 26 insertions(+), 19 deletions(-) diff --git a/builtin/provisioners/chef/resource_provisioner.go b/builtin/provisioners/chef/resource_provisioner.go index 6edf973f37..ea44e9fd04 100644 --- a/builtin/provisioners/chef/resource_provisioner.go +++ b/builtin/provisioners/chef/resource_provisioner.go @@ -684,6 +684,13 @@ func (p *provisioner) runCommand(o terraform.UIOutput, comm communicator.Communi errDoneCh := make(chan struct{}) go p.copyOutput(o, outR, outDoneCh) go p.copyOutput(o, errR, errDoneCh) + go func() { + // Wait for output to clean up + outW.Close() + errW.Close() + <-outDoneCh + <-errDoneCh + }() cmd := &remote.Cmd{ Command: command, @@ -697,18 +704,15 @@ func (p *provisioner) runCommand(o terraform.UIOutput, comm communicator.Communi } cmd.Wait() - if cmd.ExitStatus != 0 { - err = fmt.Errorf( - "Command %q exited with non-zero exit status: %d", cmd.Command, cmd.ExitStatus) + if cmd.Err() != nil { + return cmd.Err() } - // Wait for output to clean up - outW.Close() - errW.Close() - <-outDoneCh - <-errDoneCh + if cmd.ExitStatus() != 0 { + return fmt.Errorf("Command %q exited with non-zero exit status: %d", cmd.Command, cmd.ExitStatus()) + } - return err + return nil } func (p *provisioner) copyOutput(o terraform.UIOutput, r io.Reader, doneCh chan<- struct{}) { diff --git a/builtin/provisioners/habitat/resource_provisioner.go b/builtin/provisioners/habitat/resource_provisioner.go index aa404dae1d..ada06a88c8 100644 --- a/builtin/provisioners/habitat/resource_provisioner.go +++ b/builtin/provisioners/habitat/resource_provisioner.go @@ -740,12 +740,17 @@ func (p *provisioner) copyOutput(o terraform.UIOutput, r io.Reader, doneCh chan< func (p *provisioner) runCommand(o terraform.UIOutput, comm communicator.Communicator, command string) error { outR, outW := io.Pipe() errR, errW := io.Pipe() - var err error outDoneCh := make(chan struct{}) errDoneCh := make(chan struct{}) go p.copyOutput(o, outR, outDoneCh) go p.copyOutput(o, errR, errDoneCh) + defer func() { + outW.Close() + errW.Close() + <-outDoneCh + <-errDoneCh + }() cmd := &remote.Cmd{ Command: command, @@ -753,22 +758,20 @@ func (p *provisioner) runCommand(o terraform.UIOutput, comm communicator.Communi Stderr: errW, } - if err = comm.Start(cmd); err != nil { + if err := comm.Start(cmd); err != nil { return fmt.Errorf("Error executing command %q: %v", cmd.Command, err) } cmd.Wait() - if cmd.ExitStatus != 0 { - err = fmt.Errorf( - "Command %q exited with non-zero exit status: %d", cmd.Command, cmd.ExitStatus) + if cmd.Err() != nil { + return cmd.Err() } - outW.Close() - errW.Close() - <-outDoneCh - <-errDoneCh + if cmd.ExitStatus() != 0 { + return fmt.Errorf("Command %q exited with non-zero exit status: %d", cmd.Command, cmd.ExitStatus()) + } - return err + return nil } func getBindFromString(bind string) (Bind, error) { From b2d111c2bd6d64a95f766a622c7c432db6310d55 Mon Sep 17 00:00:00 2001 From: James Bardin Date: Thu, 15 Mar 2018 14:29:15 -0400 Subject: [PATCH 6/7] return provisioner Apply errors EvaApplyProvisioners was not returning errors if there was already a multierror stored in the Error field. Always return the error to the caller. --- terraform/eval_apply.go | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/terraform/eval_apply.go b/terraform/eval_apply.go index 36c98458eb..b9b4806462 100644 --- a/terraform/eval_apply.go +++ b/terraform/eval_apply.go @@ -227,11 +227,8 @@ func (n *EvalApplyProvisioners) Eval(ctx EvalContext) (interface{}, error) { state.Tainted = true } - if n.Error != nil { - *n.Error = multierror.Append(*n.Error, err) - } else { - return nil, err - } + *n.Error = multierror.Append(*n.Error, err) + return nil, err } { From 88e911af458f809fd4d50948b74f78dd04744fd5 Mon Sep 17 00:00:00 2001 From: James Bardin Date: Thu, 15 Mar 2018 14:50:58 -0400 Subject: [PATCH 7/7] fix a provisionerFail test which was incorrect The provisionerFail_createBeforeDestroy test was verifying the incorrect output. The create_before_destroy instance in the state has an ID of "bar" with require_new="abc", and a new instance would get an ID of "foo" with require_new="xyz". The existing test was expecting the following state: aws_instance.bar: (1 deposed) ID = bar provider = provider.aws require_new = abc Deposed ID 1 = foo (tainted) Which showed "bar" still the primary instance in the state, with the new instance "foo" as being the deposed instance, though properly tainted. The new output is: aws_instance.bar: (tainted) (1 deposed) ID = foo provider = provider.aws require_new = xyz type = aws_instance Deposed ID 1 = bar Showing the new "foo instance as being the primary instance in the state, with "bar" as the deposed instance. --- terraform/context_apply_test.go | 2 +- terraform/terraform_test.go | 9 +++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/terraform/context_apply_test.go b/terraform/context_apply_test.go index 69da40cfcb..f53bae1d9d 100644 --- a/terraform/context_apply_test.go +++ b/terraform/context_apply_test.go @@ -4482,7 +4482,7 @@ func TestContext2Apply_provisionerFail_createBeforeDestroy(t *testing.T) { actual := strings.TrimSpace(state.String()) expected := strings.TrimSpace(testTerraformApplyProvisionerFailCreateBeforeDestroyStr) if actual != expected { - t.Fatalf("bad: \n%s", actual) + t.Fatalf("expected:\n%s\n:got\n%s", expected, actual) } } diff --git a/terraform/terraform_test.go b/terraform/terraform_test.go index 2deb441539..26c3f98809 100644 --- a/terraform/terraform_test.go +++ b/terraform/terraform_test.go @@ -636,11 +636,12 @@ const testTerraformApplyProvisionerFailCreateNoIdStr = ` ` const testTerraformApplyProvisionerFailCreateBeforeDestroyStr = ` -aws_instance.bar: (1 deposed) - ID = bar +aws_instance.bar: (tainted) (1 deposed) + ID = foo provider = provider.aws - require_new = abc - Deposed ID 1 = foo (tainted) + require_new = xyz + type = aws_instance + Deposed ID 1 = bar ` const testTerraformApplyProvisionerResourceRefStr = `