From 7ba0c003f2ca74d90b845e2b5b0a1faec3f9a193 Mon Sep 17 00:00:00 2001 From: Armon Dadgar Date: Wed, 8 Oct 2014 17:39:08 -0700 Subject: [PATCH] command/push: Allow existing state file to enable remote --- command/push.go | 151 +++++++++++++++++++++++++++++++++++++------ command/push_test.go | 48 +++++++++++++- 2 files changed, 179 insertions(+), 20 deletions(-) diff --git a/command/push.go b/command/push.go index b186576c2..711c31bcd 100644 --- a/command/push.go +++ b/command/push.go @@ -1,8 +1,12 @@ package command import ( + "bytes" "flag" "fmt" + "io/ioutil" + "log" + "os" "strings" "github.com/hashicorp/terraform/remote" @@ -15,9 +19,12 @@ type PushCommand struct { func (c *PushCommand) Run(args []string) int { var force bool + var statePath, backupPath string var remoteConf terraform.RemoteState args = c.Meta.process(args, false) cmdFlags := flag.NewFlagSet("push", flag.ContinueOnError) + cmdFlags.StringVar(&statePath, "state", "", "path") + cmdFlags.StringVar(&backupPath, "backup", "", "path") cmdFlags.StringVar(&remoteConf.Name, "remote", "", "") cmdFlags.StringVar(&remoteConf.Server, "remote-server", "", "") cmdFlags.StringVar(&remoteConf.AuthToken, "remote-auth", "", "") @@ -27,30 +34,129 @@ func (c *PushCommand) Run(args []string) int { return 1 } - // Validate the remote configuration if given - var conf *terraform.RemoteState - if !remoteConf.Empty() { - if err := remote.ValidateConfig(&remoteConf); err != nil { + // Check for a remote state file + local, _, err := remote.ReadLocalState() + if err != nil { + c.Ui.Error(fmt.Sprintf("%s", err)) + return 1 + } + + // Check for the default state file if not specified + if statePath == "" { + statePath = DefaultStateFilename + } + + // Check if an alternative state file exists + raw, err := ioutil.ReadFile(statePath) + if err != nil { + // Ignore if the state path does not exist if it is the default + // state file path, since that means the user didn't provide any + // input. + if !(os.IsNotExist(err) && statePath == DefaultStateFilename) { + c.Ui.Error(fmt.Sprintf("Failed to open state file at '%s': %v", + statePath, err)) + return 1 + } + } + + // Check if both state files are provided! + if local != nil && raw != nil { + c.Ui.Error(fmt.Sprintf(`Remote state enabled and default state file is also present. +Please rename the state file at '%s' to prevent a conflict.`, statePath)) + return 1 + } + + // Check if there is no state to push! + if local == nil && raw == nil { + c.Ui.Error("No state to push") + return 1 + } + + // Handle the initial enabling of remote state + if local == nil && raw != nil { + if err := c.enableRemote(&remoteConf, raw, statePath, backupPath); err != nil { c.Ui.Error(fmt.Sprintf("%s", err)) return 1 } - conf = &remoteConf - } else { - // Recover the local state if any - local, _, err := remote.ReadLocalState() + } + + return c.doPush(force) +} + +// enableRemote is used when we get a state file that is not remote enabled, +// and need to move it into the hidden directory and enable remote storage. +func (c *PushCommand) enableRemote(conf *terraform.RemoteState, rawState []byte, + statePath, backupPath string) error { + // If there is no local file, ensure we have the remote + // state is properly configured + if conf.Empty() { + return fmt.Errorf("Missing remote configuration") + } + if err := remote.ValidateConfig(conf); err != nil { + return err + } + + // Decode the state + state, err := terraform.ReadState(bytes.NewReader(rawState)) + if err != nil { + return fmt.Errorf("Failed to decode state file at '%s': %v", + statePath, err) + } + + // Backup the state file before we remove it + if backupPath != "-" { + // If we don't specify a backup path, default to state out with + // the extension + if backupPath == "" { + backupPath = statePath + DefaultBackupExtention + } + + log.Printf("[INFO] Writing backup state to: %s", backupPath) + f, err := os.Create(backupPath) + if err == nil { + err = terraform.WriteState(state, f) + f.Close() + } if err != nil { - c.Ui.Error(fmt.Sprintf("%s", err)) - return 1 + return fmt.Errorf("Error writing backup state file: %s", err) } - if local == nil || local.Remote == nil { - c.Ui.Error("No remote state server configured") - return 1 - } - conf = local.Remote + } + + // Get the target path for the remote state file + path, err := remote.HiddenStatePath() + if err != nil { + return nil + } + + // Install the state file in the hidden directory + state.Remote = conf + f, err := os.Create(path) + if err == nil { + err = terraform.WriteState(state, f) + f.Close() + } + if err != nil { + return fmt.Errorf("Error copying state file: %s", err) + } + + // Remove the old state file + if err := os.Remove(statePath); err != nil { + return fmt.Errorf("Error removing state file: %s", err) + } + return nil +} + +// doPush is used to attempt the state push +func (c *PushCommand) doPush(force bool) int { + // Recover the local state if any + local, _, err := remote.ReadLocalState() + if err != nil { + c.Ui.Error(fmt.Sprintf("%s", err)) + return 1 } // Attempt to push the state - change, err := remote.PushState(conf, force) + change, err := remote.PushState(local.Remote, force) if err != nil { c.Ui.Error(fmt.Sprintf( "Failed to push state: %v", err)) @@ -71,12 +177,16 @@ func (c *PushCommand) Help() string { helpText := ` Usage: terraform push [options] - Uploads the local state file to the remote server. This is done automatically - by commands when remote state if configured, but can also be done manually - using this command. + Uploads the latest state to the remote server. This command can + also be used to push an existing state file into a remote server and + to enable automatic state management. Options: + -backup=path Path to backup the existing state file before + modifying. Defaults to the "-state" path with + ".backup" extension. Set to "-" to disable backup. + -force Forces the upload of the local state, ignoring any conflicts. This should be used carefully, as force pushing can cause remote state information to be lost. @@ -89,6 +199,9 @@ Options: -remote-server=url URL of the remote storage server. + -state=path Path to read state. Defaults to "terraform.tfstate" + unless remote state is enabled. + ` return strings.TrimSpace(helpText) } diff --git a/command/push_test.go b/command/push_test.go index f603f8f79..0801228d7 100644 --- a/command/push_test.go +++ b/command/push_test.go @@ -2,6 +2,8 @@ package command import ( "bytes" + "io/ioutil" + "os" "testing" "github.com/hashicorp/terraform/remote" @@ -50,6 +52,50 @@ func TestPush_cliRemote_noState(t *testing.T) { } } +func TestPush_cliRemote_withState(t *testing.T) { + tmp, cwd := testCwd(t) + defer fixDir(tmp, cwd) + + s := terraform.NewState() + conf, srv := testRemoteState(t, s, 200) + defer srv.Close() + + s = terraform.NewState() + s.Serial = 10 + + // Store the local state + buf := bytes.NewBuffer(nil) + terraform.WriteState(s, buf) + err := ioutil.WriteFile(DefaultStateFilename, buf.Bytes(), 0777) + if err != nil { + t.Fatalf("Err: %v", err) + } + + ui := new(cli.MockUi) + c := &PushCommand{ + Meta: Meta{ + ContextOpts: testCtxConfig(testProvider()), + Ui: ui, + }, + } + + // Remote with default state file + args := []string{"-remote", conf.Name, "-remote-server", conf.Server} + if code := c.Run(args); code != 0 { + t.Fatalf("bad: \n%s", ui.ErrorWriter.String()) + } + + // Should backup state + if _, err := os.Stat(DefaultStateFilename + DefaultBackupExtention); err != nil { + t.Fatalf("err: %v", err) + } + + // Should enable remote state + if _, err := os.Stat(remote.LocalDirectory + "/" + remote.HiddenStateFile); err != nil { + t.Fatalf("err: %v", err) + } +} + func TestPush_local(t *testing.T) { tmp, cwd := testCwd(t) defer fixDir(tmp, cwd) @@ -57,11 +103,11 @@ func TestPush_local(t *testing.T) { s := terraform.NewState() s.Serial = 5 conf, srv := testRemoteState(t, s, 200) + defer srv.Close() s = terraform.NewState() s.Serial = 10 s.Remote = conf - defer srv.Close() // Store the local state buf := bytes.NewBuffer(nil)