command/push: Allow existing state file to enable remote

This commit is contained in:
Armon Dadgar 2014-10-08 17:39:08 -07:00 committed by Mitchell Hashimoto
parent 38002904f4
commit 7ba0c003f2
2 changed files with 179 additions and 20 deletions

View File

@ -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
}
conf = &remoteConf
} else {
// 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
}
}
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 {
return fmt.Errorf("Error writing backup state file: %s", err)
}
}
// 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
}
if local == nil || local.Remote == nil {
c.Ui.Error("No remote state server configured")
return 1
}
conf = local.Remote
}
// 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)
}

View File

@ -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)