2014-10-09 00:15:14 +02:00
|
|
|
package command
|
|
|
|
|
|
|
|
import (
|
2014-10-09 02:39:08 +02:00
|
|
|
"bytes"
|
2014-10-09 00:15:14 +02:00
|
|
|
"flag"
|
|
|
|
"fmt"
|
2014-10-09 02:39:08 +02:00
|
|
|
"io/ioutil"
|
|
|
|
"log"
|
|
|
|
"os"
|
2014-10-09 00:15:14 +02:00
|
|
|
"strings"
|
|
|
|
|
|
|
|
"github.com/hashicorp/terraform/remote"
|
|
|
|
"github.com/hashicorp/terraform/terraform"
|
|
|
|
)
|
|
|
|
|
|
|
|
type PushCommand struct {
|
|
|
|
Meta
|
|
|
|
}
|
|
|
|
|
|
|
|
func (c *PushCommand) Run(args []string) int {
|
|
|
|
var force bool
|
2014-10-09 02:39:08 +02:00
|
|
|
var statePath, backupPath string
|
2014-10-09 00:15:14 +02:00
|
|
|
var remoteConf terraform.RemoteState
|
|
|
|
args = c.Meta.process(args, false)
|
|
|
|
cmdFlags := flag.NewFlagSet("push", flag.ContinueOnError)
|
2014-10-09 02:39:08 +02:00
|
|
|
cmdFlags.StringVar(&statePath, "state", "", "path")
|
|
|
|
cmdFlags.StringVar(&backupPath, "backup", "", "path")
|
2014-10-09 00:15:14 +02:00
|
|
|
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 {
|
|
|
|
return 1
|
|
|
|
}
|
|
|
|
|
2014-10-09 02:39:08 +02:00
|
|
|
// 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))
|
2014-10-09 00:15:14 +02:00
|
|
|
return 1
|
|
|
|
}
|
2014-10-09 02:39:08 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
// 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 {
|
2014-10-09 00:15:14 +02:00
|
|
|
c.Ui.Error(fmt.Sprintf("%s", err))
|
|
|
|
return 1
|
|
|
|
}
|
2014-10-09 02:39:08 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
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()
|
2014-10-09 00:15:14 +02:00
|
|
|
}
|
2014-10-09 02:39:08 +02:00
|
|
|
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
|
2014-10-09 00:15:14 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
// Attempt to push the state
|
2014-10-09 02:39:08 +02:00
|
|
|
change, err := remote.PushState(local.Remote, force)
|
2014-10-09 00:15:14 +02:00
|
|
|
if err != nil {
|
|
|
|
c.Ui.Error(fmt.Sprintf(
|
|
|
|
"Failed to push state: %v", err))
|
|
|
|
return 1
|
|
|
|
}
|
|
|
|
|
|
|
|
// Use an error exit code if the update was not a success
|
|
|
|
if !change.SuccessfulPush() {
|
|
|
|
c.Ui.Error(fmt.Sprintf("%s", change))
|
|
|
|
return 1
|
|
|
|
} else {
|
|
|
|
c.Ui.Output(fmt.Sprintf("%s", change))
|
|
|
|
}
|
|
|
|
return 0
|
|
|
|
}
|
|
|
|
|
|
|
|
func (c *PushCommand) Help() string {
|
|
|
|
helpText := `
|
|
|
|
Usage: terraform push [options]
|
|
|
|
|
2014-10-09 02:39:08 +02:00
|
|
|
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.
|
2014-10-09 00:15:14 +02:00
|
|
|
|
|
|
|
Options:
|
|
|
|
|
2014-10-09 02:39:08 +02:00
|
|
|
-backup=path Path to backup the existing state file before
|
|
|
|
modifying. Defaults to the "-state" path with
|
|
|
|
".backup" extension. Set to "-" to disable backup.
|
|
|
|
|
2014-10-09 00:15:14 +02:00
|
|
|
-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.
|
|
|
|
|
2014-10-09 02:39:08 +02:00
|
|
|
-state=path Path to read state. Defaults to "terraform.tfstate"
|
|
|
|
unless remote state is enabled.
|
|
|
|
|
2014-10-09 00:15:14 +02:00
|
|
|
`
|
|
|
|
return strings.TrimSpace(helpText)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (c *PushCommand) Synopsis() string {
|
|
|
|
return "Uploads the the local state to the remote server"
|
|
|
|
}
|