command: Simplify push/pull, depend on remote command for setup
This commit is contained in:
parent
1945e2099a
commit
9168a0f1ce
|
@ -6,7 +6,6 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/hashicorp/terraform/remote"
|
"github.com/hashicorp/terraform/remote"
|
||||||
"github.com/hashicorp/terraform/terraform"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type PullCommand struct {
|
type PullCommand struct {
|
||||||
|
@ -14,41 +13,26 @@ type PullCommand struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *PullCommand) Run(args []string) int {
|
func (c *PullCommand) Run(args []string) int {
|
||||||
var remoteConf terraform.RemoteState
|
|
||||||
args = c.Meta.process(args, false)
|
args = c.Meta.process(args, false)
|
||||||
cmdFlags := flag.NewFlagSet("pull", flag.ContinueOnError)
|
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()) }
|
cmdFlags.Usage = func() { c.Ui.Error(c.Help()) }
|
||||||
if err := cmdFlags.Parse(args); err != nil {
|
if err := cmdFlags.Parse(args); err != nil {
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate the remote configuration if given
|
// Recover the local state if any
|
||||||
var conf *terraform.RemoteState
|
local, _, err := remote.ReadLocalState()
|
||||||
if !remoteConf.Empty() {
|
if err != nil {
|
||||||
if err := remote.ValidateConfig(&remoteConf); err != nil {
|
c.Ui.Error(fmt.Sprintf("%s", err))
|
||||||
c.Ui.Error(fmt.Sprintf("%s", err))
|
return 1
|
||||||
return 1
|
}
|
||||||
}
|
if local == nil || local.Remote == nil {
|
||||||
conf = &remoteConf
|
c.Ui.Error("Remote state not enabled!")
|
||||||
} else {
|
return 1
|
||||||
// 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 the state refresh
|
// Attempt the state refresh
|
||||||
change, err := remote.RefreshState(conf)
|
change, err := remote.RefreshState(local.Remote)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.Ui.Error(fmt.Sprintf(
|
c.Ui.Error(fmt.Sprintf(
|
||||||
"Failed to refresh from remote state: %v", err))
|
"Failed to refresh from remote state: %v", err))
|
||||||
|
@ -69,19 +53,7 @@ func (c *PullCommand) Help() string {
|
||||||
helpText := `
|
helpText := `
|
||||||
Usage: terraform pull [options]
|
Usage: terraform pull [options]
|
||||||
|
|
||||||
Refreshes the cached state file from the remote server. It can also
|
Refreshes the cached state file from the remote server.
|
||||||
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.
|
|
||||||
|
|
||||||
`
|
`
|
||||||
return strings.TrimSpace(helpText)
|
return strings.TrimSpace(helpText)
|
||||||
|
|
|
@ -7,7 +7,6 @@ import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"os"
|
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/hashicorp/terraform/remote"
|
"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) {
|
func TestPull_local(t *testing.T) {
|
||||||
tmp, cwd := testCwd(t)
|
tmp, cwd := testCwd(t)
|
||||||
defer testFixCwd(t, tmp, cwd)
|
defer testFixCwd(t, tmp, cwd)
|
||||||
|
|
148
command/push.go
148
command/push.go
|
@ -1,16 +1,11 @@
|
||||||
package command
|
package command
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
|
||||||
"log"
|
|
||||||
"os"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/hashicorp/terraform/remote"
|
"github.com/hashicorp/terraform/remote"
|
||||||
"github.com/hashicorp/terraform/terraform"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type PushCommand struct {
|
type PushCommand struct {
|
||||||
|
@ -19,15 +14,8 @@ type PushCommand struct {
|
||||||
|
|
||||||
func (c *PushCommand) Run(args []string) int {
|
func (c *PushCommand) Run(args []string) int {
|
||||||
var force bool
|
var force bool
|
||||||
var statePath, backupPath string
|
|
||||||
var remoteConf terraform.RemoteState
|
|
||||||
args = c.Meta.process(args, false)
|
args = c.Meta.process(args, false)
|
||||||
cmdFlags := flag.NewFlagSet("push", flag.ContinueOnError)
|
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.BoolVar(&force, "force", false, "")
|
||||||
cmdFlags.Usage = func() { c.Ui.Error(c.Help()) }
|
cmdFlags.Usage = func() { c.Ui.Error(c.Help()) }
|
||||||
if err := cmdFlags.Parse(args); err != nil {
|
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))
|
c.Ui.Error(fmt.Sprintf("%s", err))
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
if local == nil || local.Remote == nil {
|
||||||
// Check for the default state file if not specified
|
c.Ui.Error("Remote state not enabled!")
|
||||||
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
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
// Attempt to push the state
|
// Attempt to push the state
|
||||||
change, err := remote.PushState(local.Remote, force)
|
change, err := remote.PushState(local.Remote, force)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.Ui.Error(fmt.Sprintf(
|
c.Ui.Error(fmt.Sprintf("Failed to push state: %v", err))
|
||||||
"Failed to push state: %v", err))
|
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -177,31 +54,14 @@ func (c *PushCommand) Help() string {
|
||||||
helpText := `
|
helpText := `
|
||||||
Usage: terraform push [options]
|
Usage: terraform push [options]
|
||||||
|
|
||||||
Uploads the latest state to the remote server. This command can
|
Uploads the latest state to the remote server.
|
||||||
also be used to push an existing state file into a remote server and
|
|
||||||
to enable automatic state management.
|
|
||||||
|
|
||||||
Options:
|
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
|
-force Forces the upload of the local state, ignoring any
|
||||||
conflicts. This should be used carefully, as force pushing
|
conflicts. This should be used carefully, as force pushing
|
||||||
can cause remote state information to be lost.
|
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)
|
return strings.TrimSpace(helpText)
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,8 +2,6 @@ package command
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"io/ioutil"
|
|
||||||
"os"
|
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/hashicorp/terraform/remote"
|
"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) {
|
func TestPush_local(t *testing.T) {
|
||||||
tmp, cwd := testCwd(t)
|
tmp, cwd := testCwd(t)
|
||||||
defer testFixCwd(t, tmp, cwd)
|
defer testFixCwd(t, tmp, cwd)
|
||||||
|
|
Loading…
Reference in New Issue