command: Simplify push/pull, depend on remote command for setup

This commit is contained in:
Armon Dadgar 2014-10-09 16:52:29 -07:00 committed by Mitchell Hashimoto
parent 1945e2099a
commit 9168a0f1ce
4 changed files with 15 additions and 281 deletions

View File

@ -6,7 +6,6 @@ import (
"strings"
"github.com/hashicorp/terraform/remote"
"github.com/hashicorp/terraform/terraform"
)
type PullCommand struct {
@ -14,41 +13,26 @@ type PullCommand struct {
}
func (c *PullCommand) Run(args []string) int {
var remoteConf terraform.RemoteState
args = c.Meta.process(args, false)
cmdFlags := flag.NewFlagSet("pull", flag.ContinueOnError)
cmdFlags.StringVar(&remoteConf.Name, "remote", "", "")
cmdFlags.StringVar(&remoteConf.Server, "remote-server", "", "")
cmdFlags.StringVar(&remoteConf.AuthToken, "remote-auth", "", "")
cmdFlags.Usage = func() { c.Ui.Error(c.Help()) }
if err := cmdFlags.Parse(args); err != nil {
return 1
}
// Validate the remote configuration if given
var conf *terraform.RemoteState
if !remoteConf.Empty() {
if err := remote.ValidateConfig(&remoteConf); err != nil {
c.Ui.Error(fmt.Sprintf("%s", err))
return 1
}
conf = &remoteConf
} else {
// 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
// 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("Remote state not enabled!")
return 1
}
// Attempt the state refresh
change, err := remote.RefreshState(conf)
change, err := remote.RefreshState(local.Remote)
if err != nil {
c.Ui.Error(fmt.Sprintf(
"Failed to refresh from remote state: %v", err))
@ -69,19 +53,7 @@ func (c *PullCommand) Help() string {
helpText := `
Usage: terraform pull [options]
Refreshes the cached state file from the remote server. It can also
be used to perform the initial clone of the state file and setup the
remote server configuration to use remote state storage.
Options:
-remote=name Name of the state file in the state storage server.
Optional, default does not use remote storage.
-remote-auth=token Authentication token for state storage server.
Optional, defaults to blank.
-remote-server=url URL of the remote storage server.
Refreshes the cached state file from the remote server.
`
return strings.TrimSpace(helpText)

View File

@ -7,7 +7,6 @@ import (
"encoding/json"
"net/http"
"net/http/httptest"
"os"
"testing"
"github.com/hashicorp/terraform/remote"
@ -33,34 +32,6 @@ func TestPull_noRemote(t *testing.T) {
}
}
func TestPull_cliRemote(t *testing.T) {
tmp, cwd := testCwd(t)
defer testFixCwd(t, tmp, cwd)
s := terraform.NewState()
conf, srv := testRemoteState(t, s, 200)
defer srv.Close()
ui := new(cli.MockUi)
c := &PullCommand{
Meta: Meta{
ContextOpts: testCtxConfig(testProvider()),
Ui: ui,
},
}
args := []string{"-remote", conf.Name, "-remote-server", conf.Server}
if code := c.Run(args); code != 0 {
t.Fatalf("bad: \n%s", ui.ErrorWriter.String())
}
path, _ := remote.HiddenStatePath()
_, err := os.Stat(path)
if err != nil {
t.Fatalf("missing state")
}
}
func TestPull_local(t *testing.T) {
tmp, cwd := testCwd(t)
defer testFixCwd(t, tmp, cwd)

View File

@ -1,16 +1,11 @@
package command
import (
"bytes"
"flag"
"fmt"
"io/ioutil"
"log"
"os"
"strings"
"github.com/hashicorp/terraform/remote"
"github.com/hashicorp/terraform/terraform"
)
type PushCommand struct {
@ -19,15 +14,8 @@ 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", "", "")
cmdFlags.BoolVar(&force, "force", false, "")
cmdFlags.Usage = func() { c.Ui.Error(c.Help()) }
if err := cmdFlags.Parse(args); err != nil {
@ -40,126 +28,15 @@ func (c *PushCommand) Run(args []string) int {
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
}
}
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))
if local == nil || local.Remote == nil {
c.Ui.Error("Remote state not enabled!")
return 1
}
// Attempt to push the state
change, err := remote.PushState(local.Remote, force)
if err != nil {
c.Ui.Error(fmt.Sprintf(
"Failed to push state: %v", err))
c.Ui.Error(fmt.Sprintf("Failed to push state: %v", err))
return 1
}
@ -177,31 +54,14 @@ func (c *PushCommand) Help() string {
helpText := `
Usage: terraform push [options]
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.
Uploads the latest state to the remote server.
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.
-remote=name Name of the state file in the state storage server.
Optional, default does not use remote storage.
-remote-auth=token Authentication token for state storage server.
Optional, defaults to blank.
-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,8 +2,6 @@ package command
import (
"bytes"
"io/ioutil"
"os"
"testing"
"github.com/hashicorp/terraform/remote"
@ -29,73 +27,6 @@ func TestPush_noRemote(t *testing.T) {
}
}
func TestPush_cliRemote_noState(t *testing.T) {
tmp, cwd := testCwd(t)
defer testFixCwd(t, tmp, cwd)
s := terraform.NewState()
conf, srv := testRemoteState(t, s, 200)
defer srv.Close()
ui := new(cli.MockUi)
c := &PushCommand{
Meta: Meta{
ContextOpts: testCtxConfig(testProvider()),
Ui: ui,
},
}
// Remote with no local state!
args := []string{"-remote", conf.Name, "-remote-server", conf.Server}
if code := c.Run(args); code != 1 {
t.Fatalf("bad: \n%s", ui.ErrorWriter.String())
}
}
func TestPush_cliRemote_withState(t *testing.T) {
tmp, cwd := testCwd(t)
defer testFixCwd(t, 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 testFixCwd(t, tmp, cwd)