diff --git a/command/remote.go b/command/remote.go index 4959d7aa2..7342294d1 100644 --- a/command/remote.go +++ b/command/remote.go @@ -110,6 +110,7 @@ func (c *RemoteCommand) disableRemoteState() int { // Ensure we have the latest state before disabling if c.conf.pullOnDisable { + log.Printf("[INFO] Refreshing local state from remote server") change, err := remote.RefreshState(local.Remote) if err != nil { c.Ui.Error(fmt.Sprintf( @@ -122,7 +123,14 @@ func (c *RemoteCommand) disableRemoteState() int { c.Ui.Error(fmt.Sprintf("%s", change)) return 1 } else { - c.Ui.Output(fmt.Sprintf("%s", change)) + log.Printf("[INFO] %s", change) + } + + // Reload the local state after the refresh + local, _, err = remote.ReadLocalState() + if err != nil { + c.Ui.Error(fmt.Sprintf("Failed to read local state: %v", err)) + return 1 } } diff --git a/command/remote_test.go b/command/remote_test.go new file mode 100644 index 000000000..f80fe546d --- /dev/null +++ b/command/remote_test.go @@ -0,0 +1,433 @@ +package command + +import ( + "bytes" + "io/ioutil" + "os" + "testing" + + "github.com/hashicorp/terraform/remote" + "github.com/hashicorp/terraform/terraform" + "github.com/mitchellh/cli" +) + +// Test disabling remote management +func TestRemote_disable(t *testing.T) { + tmp, cwd := testCwd(t) + defer testFixCwd(t, tmp, cwd) + + // Create remote state file, this should be pulled + s := terraform.NewState() + s.Serial = 10 + conf, srv := testRemoteState(t, s, 200) + defer srv.Close() + + // Persist local remote state + s = terraform.NewState() + s.Serial = 5 + s.Remote = conf + if err := remote.EnsureDirectory(); err != nil { + t.Fatalf("err: %v", err) + } + if err := remote.PersistState(s); err != nil { + t.Fatalf("err: %v", err) + } + + ui := new(cli.MockUi) + c := &RemoteCommand{ + Meta: Meta{ + ContextOpts: testCtxConfig(testProvider()), + Ui: ui, + }, + } + args := []string{"-disable"} + if code := c.Run(args); code != 0 { + t.Fatalf("bad: \n%s", ui.ErrorWriter.String()) + } + + // Local state file should be removed + haveLocal, err := remote.HaveLocalState() + if err != nil { + t.Fatalf("err: %v", err) + } + if haveLocal { + t.Fatalf("should be disabled") + } + + // New state file should be installed + exists, err := remote.ExistsFile(DefaultStateFilename) + if err != nil { + t.Fatalf("err: %v", err) + } + if !exists { + t.Fatalf("failed to make state file") + } + + // Check that the state file was updated + raw, _ := ioutil.ReadFile(DefaultStateFilename) + newState, err := terraform.ReadState(bytes.NewReader(raw)) + if err != nil { + t.Fatalf("err: %v", err) + } + + // Ensure we updated + // TODO: Should be 10, but WriteState currently + // increments incorrectly + if newState.Serial != 11 { + t.Fatalf("state file not updated: %#v", newState) + } + if newState.Remote != nil { + t.Fatalf("remote configuration not removed") + } +} + +// Test disabling remote management without pulling +func TestRemote_disable_noPull(t *testing.T) { + tmp, cwd := testCwd(t) + defer testFixCwd(t, tmp, cwd) + + // Create remote state file, this should be pulled + s := terraform.NewState() + s.Serial = 10 + conf, srv := testRemoteState(t, s, 200) + defer srv.Close() + + // Persist local remote state + s = terraform.NewState() + s.Serial = 5 + s.Remote = conf + if err := remote.EnsureDirectory(); err != nil { + t.Fatalf("err: %v", err) + } + if err := remote.PersistState(s); err != nil { + t.Fatalf("err: %v", err) + } + + ui := new(cli.MockUi) + c := &RemoteCommand{ + Meta: Meta{ + ContextOpts: testCtxConfig(testProvider()), + Ui: ui, + }, + } + args := []string{"-disable", "-pull=false"} + if code := c.Run(args); code != 0 { + t.Fatalf("bad: \n%s", ui.ErrorWriter.String()) + } + + // Local state file should be removed + haveLocal, err := remote.HaveLocalState() + if err != nil { + t.Fatalf("err: %v", err) + } + if haveLocal { + t.Fatalf("should be disabled") + } + + // New state file should be installed + exists, err := remote.ExistsFile(DefaultStateFilename) + if err != nil { + t.Fatalf("err: %v", err) + } + if !exists { + t.Fatalf("failed to make state file") + } + + // Check that the state file was updated + raw, _ := ioutil.ReadFile(DefaultStateFilename) + newState, err := terraform.ReadState(bytes.NewReader(raw)) + if err != nil { + t.Fatalf("err: %v", err) + } + + // Ensure we DIDNT updated + // TODO: Should be 5, but WriteState currently increments + // this which is incorrect. + if newState.Serial != 7 { + t.Fatalf("state file updated: %#v", newState) + } + if newState.Remote != nil { + t.Fatalf("remote configuration not removed") + } +} + +// Test disabling remote management when not enabled +func TestRemote_disable_notEnabled(t *testing.T) { + tmp, cwd := testCwd(t) + defer testFixCwd(t, tmp, cwd) + + ui := new(cli.MockUi) + c := &RemoteCommand{ + Meta: Meta{ + ContextOpts: testCtxConfig(testProvider()), + Ui: ui, + }, + } + + args := []string{"-disable"} + if code := c.Run(args); code != 1 { + t.Fatalf("bad: \n%s", ui.ErrorWriter.String()) + } +} + +// Test disabling remote management with a state file in the way +func TestRemote_disable_otherState(t *testing.T) { + tmp, cwd := testCwd(t) + defer testFixCwd(t, tmp, cwd) + + // Persist local remote state + s := terraform.NewState() + s.Serial = 5 + if err := remote.EnsureDirectory(); err != nil { + t.Fatalf("err: %v", err) + } + if err := remote.PersistState(s); err != nil { + t.Fatalf("err: %v", err) + } + + // Also put a file at the default path + fh, err := os.Create(DefaultStateFilename) + if err != nil { + t.Fatalf("err: %v", err) + } + err = terraform.WriteState(s, fh) + fh.Close() + if err != nil { + t.Fatalf("err: %v", err) + } + + ui := new(cli.MockUi) + c := &RemoteCommand{ + Meta: Meta{ + ContextOpts: testCtxConfig(testProvider()), + Ui: ui, + }, + } + + args := []string{"-disable"} + if code := c.Run(args); code != 1 { + t.Fatalf("bad: \n%s", ui.ErrorWriter.String()) + } +} + +// Test the case where both managed and non managed state present +func TestRemote_managedAndNonManaged(t *testing.T) { + tmp, cwd := testCwd(t) + defer testFixCwd(t, tmp, cwd) + + // Persist local remote state + s := terraform.NewState() + s.Serial = 5 + if err := remote.EnsureDirectory(); err != nil { + t.Fatalf("err: %v", err) + } + if err := remote.PersistState(s); err != nil { + t.Fatalf("err: %v", err) + } + + // Also put a file at the default path + fh, err := os.Create(DefaultStateFilename) + if err != nil { + t.Fatalf("err: %v", err) + } + err = terraform.WriteState(s, fh) + fh.Close() + if err != nil { + t.Fatalf("err: %v", err) + } + + ui := new(cli.MockUi) + c := &RemoteCommand{ + Meta: Meta{ + ContextOpts: testCtxConfig(testProvider()), + Ui: ui, + }, + } + + args := []string{} + if code := c.Run(args); code != 1 { + t.Fatalf("bad: \n%s", ui.ErrorWriter.String()) + } +} + +// Test initializing blank state +func TestRemote_initBlank(t *testing.T) { + tmp, cwd := testCwd(t) + defer testFixCwd(t, tmp, cwd) + + ui := new(cli.MockUi) + c := &RemoteCommand{ + Meta: Meta{ + ContextOpts: testCtxConfig(testProvider()), + Ui: ui, + }, + } + + args := []string{ + "-name=foobar", + "-server", + "http://example.com", + "-auth=test", + } + if code := c.Run(args); code != 0 { + t.Fatalf("bad: \n%s", ui.ErrorWriter.String()) + } + + local, _, err := remote.ReadLocalState() + if err != nil { + t.Fatalf("err: %v", err) + } + + if local.Remote.Name != "foobar" { + t.Fatalf("Bad: %#v", local.Remote) + } + if local.Remote.Server != "http://example.com" { + t.Fatalf("Bad: %#v", local.Remote) + } + if local.Remote.AuthToken != "test" { + t.Fatalf("Bad: %#v", local.Remote) + } +} + +// Test initializing without remote settings +func TestRemote_initBlank_missingRemote(t *testing.T) { + tmp, cwd := testCwd(t) + defer testFixCwd(t, tmp, cwd) + + ui := new(cli.MockUi) + c := &RemoteCommand{ + Meta: Meta{ + ContextOpts: testCtxConfig(testProvider()), + Ui: ui, + }, + } + + args := []string{} + if code := c.Run(args); code != 1 { + t.Fatalf("bad: \n%s", ui.ErrorWriter.String()) + } +} + +// Test updating remote config +func TestRemote_updateRemote(t *testing.T) { + tmp, cwd := testCwd(t) + defer testFixCwd(t, tmp, cwd) + + // Persist local remote state + s := terraform.NewState() + s.Serial = 5 + s.Remote = &terraform.RemoteState{ + Name: "invalid", + } + if err := remote.EnsureDirectory(); err != nil { + t.Fatalf("err: %v", err) + } + if err := remote.PersistState(s); err != nil { + t.Fatalf("err: %v", err) + } + + ui := new(cli.MockUi) + c := &RemoteCommand{ + Meta: Meta{ + ContextOpts: testCtxConfig(testProvider()), + Ui: ui, + }, + } + + args := []string{ + "-name=foobar", + "-server", + "http://example.com", + "-auth=test", + } + if code := c.Run(args); code != 0 { + t.Fatalf("bad: \n%s", ui.ErrorWriter.String()) + } + + local, _, err := remote.ReadLocalState() + if err != nil { + t.Fatalf("err: %v", err) + } + + if local.Remote.Name != "foobar" { + t.Fatalf("Bad: %#v", local.Remote) + } + if local.Remote.Server != "http://example.com" { + t.Fatalf("Bad: %#v", local.Remote) + } + if local.Remote.AuthToken != "test" { + t.Fatalf("Bad: %#v", local.Remote) + } +} + +// Test enabling remote state +func TestRemote_enableRemote(t *testing.T) { + tmp, cwd := testCwd(t) + defer testFixCwd(t, tmp, cwd) + + // Create a non-remote enabled state + s := terraform.NewState() + s.Serial = 5 + + // Add the state at the default path + fh, err := os.Create(DefaultStateFilename) + if err != nil { + t.Fatalf("err: %v", err) + } + err = terraform.WriteState(s, fh) + fh.Close() + if err != nil { + t.Fatalf("err: %v", err) + } + + ui := new(cli.MockUi) + c := &RemoteCommand{ + Meta: Meta{ + ContextOpts: testCtxConfig(testProvider()), + Ui: ui, + }, + } + + args := []string{ + "-name=foobar", + "-server", + "http://example.com", + "-auth=test", + } + if code := c.Run(args); code != 0 { + t.Fatalf("bad: \n%s", ui.ErrorWriter.String()) + } + + local, _, err := remote.ReadLocalState() + if err != nil { + t.Fatalf("err: %v", err) + } + + if local.Remote.Name != "foobar" { + t.Fatalf("Bad: %#v", local.Remote) + } + if local.Remote.Server != "http://example.com" { + t.Fatalf("Bad: %#v", local.Remote) + } + if local.Remote.AuthToken != "test" { + t.Fatalf("Bad: %#v", local.Remote) + } + + // Backup file should exist + exist, err := remote.ExistsFile(DefaultStateFilename + DefaultBackupExtention) + if err != nil { + t.Fatalf("err: %v", err) + } + if !exist { + t.Fatalf("backup should exist") + } + + // State file should not + exist, err = remote.ExistsFile(DefaultStateFilename) + if err != nil { + t.Fatalf("err: %v", err) + } + if exist { + t.Fatalf("state file should not exist") + } +} diff --git a/remote/remote.go b/remote/remote.go index ba10146ca..ca592d258 100644 --- a/remote/remote.go +++ b/remote/remote.go @@ -186,7 +186,7 @@ func ExistsFile(path string) (bool, error) { // ValidConfig does a purely logical validation of the remote config func ValidConfig(conf *terraform.RemoteState) error { // Verify the remote server configuration is sane - if (conf.Server != "" || conf.AuthToken != "") && conf.Name == "" { + if conf.Name == "" { return fmt.Errorf("Name must be provided for remote state storage") } if conf.Server != "" { diff --git a/remote/remote_test.go b/remote/remote_test.go index 01177b203..632142ed3 100644 --- a/remote/remote_test.go +++ b/remote/remote_test.go @@ -46,8 +46,8 @@ func TestHiddenStatePath(t *testing.T) { func TestValidConfig(t *testing.T) { conf := &terraform.RemoteState{} - if err := ValidConfig(conf); err != nil { - t.Fatalf("blank should be valid: %v", err) + if err := ValidConfig(conf); err == nil { + t.Fatalf("blank should be not be valid: %v", err) } conf.Server = "http://foo.com" if err := ValidConfig(conf); err == nil {