diff --git a/command/apply.go b/command/apply.go index 8ca8a9098..80b48e926 100644 --- a/command/apply.go +++ b/command/apply.go @@ -3,7 +3,6 @@ package command import ( "bytes" "fmt" - "log" "os" "sort" "strings" @@ -27,8 +26,6 @@ type ApplyCommand struct { func (c *ApplyCommand) Run(args []string) int { var destroyForce, refresh bool - var statePath, stateOutPath, backupPath string - args = c.Meta.process(args, true) cmdName := "apply" @@ -41,9 +38,9 @@ func (c *ApplyCommand) Run(args []string) int { cmdFlags.BoolVar(&destroyForce, "force", false, "force") } cmdFlags.BoolVar(&refresh, "refresh", true, "refresh") - cmdFlags.StringVar(&statePath, "state", DefaultStateFilename, "path") - cmdFlags.StringVar(&stateOutPath, "state-out", "", "path") - cmdFlags.StringVar(&backupPath, "backup", "", "path") + cmdFlags.StringVar(&c.Meta.statePath, "state", DefaultStateFilename, "path") + cmdFlags.StringVar(&c.Meta.stateOutPath, "state-out", "", "path") + cmdFlags.StringVar(&c.Meta.backupPath, "backup", "", "path") cmdFlags.Usage = func() { c.Ui.Error(c.Help()) } if err := cmdFlags.Parse(args); err != nil { return 1 @@ -71,18 +68,6 @@ func (c *ApplyCommand) Run(args []string) int { countHook := new(CountHook) c.Meta.extraHooks = []terraform.Hook{countHook} - // If we don't specify an output path, default to out normal state - // path. - if stateOutPath == "" { - stateOutPath = statePath - } - - // If we don't specify a backup path, default to state out with - // the extension - if backupPath == "" { - backupPath = stateOutPath + DefaultBackupExtention - } - if !c.Destroy { // Do a detect to determine if we need to do an init + apply. if detected, err := module.Detect(configPath, pwd); err != nil { @@ -106,7 +91,7 @@ func (c *ApplyCommand) Run(args []string) int { // Build the context based on the arguments given ctx, planned, err := c.Context(contextOpts{ Path: configPath, - StatePath: statePath, + StatePath: c.Meta.statePath, }) if err != nil { c.Ui.Error(err.Error()) @@ -143,20 +128,6 @@ func (c *ApplyCommand) Run(args []string) int { } } - // Create a backup of the state before updating - if backupPath != "-" && c.state != nil { - log.Printf("[INFO] Writing backup state to: %s", backupPath) - f, err := os.Create(backupPath) - if err == nil { - err = terraform.WriteState(c.state, f) - f.Close() - } - if err != nil { - c.Ui.Error(fmt.Sprintf("Error writing backup state file: %s", err)) - return 1 - } - } - // Plan if we haven't already if !planned { if refresh { @@ -209,14 +180,9 @@ func (c *ApplyCommand) Run(args []string) int { case <-doneCh: } + // Persist the state if state != nil { - // Write state out to the file - f, err := os.Create(stateOutPath) - if err == nil { - err = terraform.WriteState(state, f) - f.Close() - } - if err != nil { + if err := c.Meta.PersistState(state); err != nil { c.Ui.Error(fmt.Sprintf("Failed to save state: %s", err)) return 1 } @@ -249,7 +215,7 @@ func (c *ApplyCommand) Run(args []string) int { "infrastructure, so keep it safe. To inspect the complete state\n"+ "use the `terraform show` command.\n\n"+ "State path: %s", - stateOutPath))) + c.Meta.stateOutPath))) } // If we have outputs, then output those at the end. diff --git a/command/meta.go b/command/meta.go index 7ebffedb4..5612f8810 100644 --- a/command/meta.go +++ b/command/meta.go @@ -5,10 +5,12 @@ import ( "flag" "fmt" "io" + "log" "os" "path/filepath" "github.com/hashicorp/terraform/config/module" + "github.com/hashicorp/terraform/remote" "github.com/hashicorp/terraform/terraform" "github.com/mitchellh/cli" "github.com/mitchellh/colorstring" @@ -38,6 +40,38 @@ type Meta struct { color bool oldUi cli.Ui + + // useRemoteState is enabled if we are using remote state storage + useRemoteState bool + + // statePath is the path to the state file. If this is empty, then + // no state will be loaded. It is also okay for this to be a path to + // a file that doesn't exist; it is assumed that this means that there + // is simply no state. + statePath string + + // stateOutPath is used to override the output path for the state. + // If not provided, the StatePath is used causing the old state to + // be overriden. + stateOutPath string + + // backupPath is used to backup the state file before writing a modified + // version. It defaults to stateOutPath + DefaultBackupExtention + backupPath string +} + +// initStatePaths is used to initialize the default values for +// statePath, stateOutPath, and backupPath +func (m *Meta) initStatePaths() { + if m.statePath == "" { + m.statePath = DefaultStateFilename + } + if m.stateOutPath == "" { + m.stateOutPath = m.statePath + } + if m.backupPath == "" { + m.backupPath = m.stateOutPath + DefaultBackupExtention + } } // Colorize returns the colorization structure for a command. @@ -137,6 +171,65 @@ func (m *Meta) UIInput() terraform.UIInput { } } +// PersistState is used to write out the state, handling backup of +// the existing state file and respecting path configurations. +func (m *Meta) PersistState(s *terraform.State) error { + if m.useRemoteState { + return m.persistRemoteState(s) + } + return m.persistLocalState(s) +} + +// persistRemoteState is used to handle persisting a state file +// when remote state management is enabled +func (m *Meta) persistRemoteState(s *terraform.State) error { + log.Printf("[INFO] Persisting state to local cache") + if err := remote.PersistState(s); err != nil { + return err + } + log.Printf("[INFO] Uploading state to remote store") + change, err := remote.PushState(s.Remote, false) + if err != nil { + return err + } + if !change.SuccessfulPush() { + return fmt.Errorf("Failed to upload state: %s", change) + } + return nil +} + +// persistLocalState is used to handle persisting a state file +// when remote state management is disabled. +func (m *Meta) persistLocalState(s *terraform.State) error { + m.initStatePaths() + + // Create a backup of the state before updating + if m.backupPath != "-" { + log.Printf("[INFO] Writing backup state to: %s", m.backupPath) + if err := remote.CopyFile(m.statePath, m.backupPath); err != nil { + return fmt.Errorf("Failed to backup state: %v", err) + } + } + + // Open the new state file + fh, err := os.Create(m.stateOutPath) + if err != nil { + return fmt.Errorf("Failed to open state file: %v", err) + } + defer fh.Close() + + // Write out the state + if err := terraform.WriteState(s, fh); err != nil { + return fmt.Errorf("Failed to encode the state: %v", err) + } + return nil +} + +// Input returns true if we should ask for input for context. +func (m *Meta) Input() bool { + return !test && m.input && len(m.variables) == 0 +} + // contextOpts returns the options to use to initialize a Terraform // context with the settings from this Meta. func (m *Meta) contextOpts() *terraform.ContextOpts { diff --git a/command/meta_test.go b/command/meta_test.go index ec4840f34..dcc8cd80b 100644 --- a/command/meta_test.go +++ b/command/meta_test.go @@ -7,6 +7,7 @@ import ( "reflect" "testing" + "github.com/hashicorp/terraform/remote" "github.com/hashicorp/terraform/terraform" ) @@ -144,3 +145,113 @@ func TestMetaInputMode_vars(t *testing.T) { t.Fatalf("bad: %#v", m.InputMode()) } } + +func TestMeta_initStatePaths(t *testing.T) { + m := new(Meta) + m.initStatePaths() + + if m.statePath != DefaultStateFilename { + t.Fatalf("bad: %#v", m) + } + if m.stateOutPath != DefaultStateFilename { + t.Fatalf("bad: %#v", m) + } + if m.backupPath != DefaultStateFilename+DefaultBackupExtention { + t.Fatalf("bad: %#v", m) + } + + m = new(Meta) + m.statePath = "foo" + m.initStatePaths() + + if m.stateOutPath != "foo" { + t.Fatalf("bad: %#v", m) + } + if m.backupPath != "foo"+DefaultBackupExtention { + t.Fatalf("bad: %#v", m) + } + + m = new(Meta) + m.stateOutPath = "foo" + m.initStatePaths() + + if m.statePath != DefaultStateFilename { + t.Fatalf("bad: %#v", m) + } + if m.backupPath != "foo"+DefaultBackupExtention { + t.Fatalf("bad: %#v", m) + } +} + +func TestMeta_persistLocal(t *testing.T) { + tmp, cwd := testCwd(t) + defer testFixCwd(t, tmp, cwd) + + m := new(Meta) + s := terraform.NewState() + if err := m.persistLocalState(s); err != nil { + t.Fatalf("err: %v", err) + } + + exists, err := remote.ExistsFile(m.stateOutPath) + if err != nil { + t.Fatalf("err: %v", err) + } + if !exists { + t.Fatalf("state should exist") + } + + // Write again, shoudl backup + if err := m.persistLocalState(s); err != nil { + t.Fatalf("err: %v", err) + } + + exists, err = remote.ExistsFile(m.backupPath) + if err != nil { + t.Fatalf("err: %v", err) + } + if !exists { + t.Fatalf("backup should exist") + } +} + +func TestMeta_persistRemote(t *testing.T) { + tmp, cwd := testCwd(t) + defer testFixCwd(t, tmp, cwd) + + err := remote.EnsureDirectory() + if err != nil { + t.Fatalf("err: %v", err) + } + + s := terraform.NewState() + conf, srv := testRemoteState(t, s, 200) + s.Remote = conf + defer srv.Close() + + m := new(Meta) + if err := m.persistRemoteState(s); err != nil { + t.Fatalf("err: %v", err) + } + + local, _, err := remote.ReadLocalState() + if err != nil { + t.Fatalf("err: %v", err) + } + if local == nil { + t.Fatalf("state should exist") + } + + if err := m.persistRemoteState(s); err != nil { + t.Fatalf("err: %v", err) + } + + backup := remote.LocalDirectory + "/" + remote.BackupHiddenStateFile + exists, err := remote.ExistsFile(backup) + if err != nil { + t.Fatalf("err: %v", err) + } + if !exists { + t.Fatalf("backup should exist") + } +}