command/push: Allow existing state file to enable remote
This commit is contained in:
parent
38002904f4
commit
7ba0c003f2
151
command/push.go
151
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)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
Loading…
Reference in New Issue