command/remote: Working on the details

This commit is contained in:
Armon Dadgar 2014-10-09 16:31:56 -07:00 committed by Mitchell Hashimoto
parent 8c6e4e564f
commit 4e44443aa3
1 changed files with 296 additions and 6 deletions

View File

@ -1,14 +1,290 @@
package command
import "strings"
import (
"bytes"
"flag"
"fmt"
"io/ioutil"
"log"
"os"
"strings"
"github.com/hashicorp/terraform/remote"
"github.com/hashicorp/terraform/terraform"
)
// remoteCommandConfig is used to encapsulate our configuration
type remoteCommandConfig struct {
disableRemote bool
pullOnDisable bool
statePath string
backupPath string
}
// RemoteCommand is a Command implementation that is used to
// enable and disable remote state management
type RemoteCommand struct {
Meta
conf remoteCommandConfig
remoteConf terraform.RemoteState
}
func (c *RemoteCommand) Run(args []string) int {
args = c.Meta.process(args, false)
cmdFlags := flag.NewFlagSet("remote", flag.ContinueOnError)
cmdFlags.BoolVar(&c.conf.disableRemote, "disable", false, "")
cmdFlags.BoolVar(&c.conf.pullOnDisable, "pull", true, "")
cmdFlags.StringVar(&c.conf.statePath, "state", "", "path")
cmdFlags.StringVar(&c.conf.backupPath, "backup", "", "path")
cmdFlags.StringVar(&c.remoteConf.AuthToken, "auth", "", "")
cmdFlags.StringVar(&c.remoteConf.Name, "name", "", "")
cmdFlags.StringVar(&c.remoteConf.Server, "server", "", "")
cmdFlags.Usage = func() { c.Ui.Error(c.Help()) }
if err := cmdFlags.Parse(args); err != nil {
return 1
}
// Check if have an existing local state file
haveLocal, err := remote.HaveLocalState()
if err != nil {
c.Ui.Error(fmt.Sprintf("Failed to check for local state: %v", err))
return 1
}
// Check if we have the non-managed state file
haveNonManaged, err := remote.ExistsFile(c.conf.statePath)
if err != nil {
c.Ui.Error(fmt.Sprintf("Failed to check for state file: %v", err))
return 1
}
// Check if remote state is being disabled
if c.conf.disableRemote {
if !haveLocal {
c.Ui.Error(fmt.Sprintf("Remote state management not enabled! Aborting."))
return 1
}
if haveNonManaged {
c.Ui.Error(fmt.Sprintf("State file already exists at '%s'. Aborting.",
c.conf.statePath))
return 1
}
return c.disableRemoteState()
}
// Ensure there is no conflict
switch {
case haveLocal && haveNonManaged:
c.Ui.Error(fmt.Sprintf("Remote state is enabled, but non-managed state file '%s' is also present!",
c.conf.statePath))
return 1
case !haveLocal && !haveNonManaged:
// If we don't have either state file, initialize a blank state file
return c.initBlankState()
case haveLocal && !haveNonManaged:
// Update the remote state target potentially
return c.updateRemoteConfig()
case !haveLocal && haveNonManaged:
// Enable remote state management
return c.enableRemoteState()
default:
panic("unhandled case")
}
return 0
}
// pullState is used to refresh our state before disabling remote
// state management
func (c *RemoteCommand) pullState() error {
// TODO
return nil
}
// disableRemoteState is used to disable remote state management,
// and move the state file into place.
func (c *RemoteCommand) disableRemoteState() int {
// Ensure we have the latest state before disabling
if c.conf.pullOnDisable {
if err := c.pullState(); err != nil {
c.Ui.Error(fmt.Sprintf("%s", err))
return 1
}
}
// Get the local state
local, _, err := remote.ReadLocalState()
if err != nil {
c.Ui.Error(fmt.Sprintf("Failed to read local state: %v", err))
return 1
}
// Clear the remote management, and copy into place
local.Remote = nil
fh, err := os.Create(c.conf.statePath)
if err != nil {
c.Ui.Error(fmt.Sprintf("Failed to create state file '%s': %v",
c.conf.statePath, err))
return 1
}
defer fh.Close()
if err := terraform.WriteState(local, fh); err != nil {
c.Ui.Error(fmt.Sprintf("Failed to encode state file: %v",
c.conf.statePath, err))
return 1
}
// Remove the old state file
path, err := remote.HiddenStatePath()
if err != nil {
c.Ui.Error(fmt.Sprintf("Failed to get local state path: %v", err))
return 1
}
if err := os.Remove(path); err != nil {
c.Ui.Error(fmt.Sprintf("Failed to remove the local state file: %v", err))
return 1
}
return 0
}
// validateRemoteConfig is used to verify that the remote configuration
// we have is valid
func (c *RemoteCommand) validateRemoteConfig() error {
err := remote.ValidConfig(&c.remoteConf)
if err != nil {
c.Ui.Error(fmt.Sprintf("%s", err))
}
return err
}
// initBlank state is used to initialize a blank state that is
// remote enabled
func (c *RemoteCommand) initBlankState() int {
// Validate the remote configuration
if err := c.validateRemoteConfig(); err != nil {
return 1
}
// Make the hidden directory
if err := remote.EnsureDirectory(); err != nil {
c.Ui.Error(fmt.Sprintf("%s", err))
return 1
}
// Make a blank state, attach the remote configuration
blank := terraform.NewState()
blank.Remote = &c.remoteConf
// Persist the state
buf := bytes.NewBuffer(nil)
if err := terraform.WriteState(blank, buf); err != nil {
c.Ui.Error(fmt.Sprintf("Failed to encode state: %v", err))
return 1
}
if err := remote.Persist(buf); err != nil {
c.Ui.Error(fmt.Sprintf("Failed to initialize state file: %v", err))
return 1
}
// Success!
c.Ui.Output("Initialized blank state with remote state enabled!")
return 0
}
// updateRemoteConfig is used to update the configuration of the
// remote state store
func (c *RemoteCommand) updateRemoteConfig() int {
// Validate the remote configuration
if err := c.validateRemoteConfig(); err != nil {
return 1
}
// Read in the local state
local, _, err := remote.ReadLocalState()
if err != nil {
c.Ui.Error(fmt.Sprintf("Failed to read local state: %v", err))
return 1
}
// Update the configuration
local.Remote = &c.remoteConf
if err := remote.PersistState(local); err != nil {
c.Ui.Error(fmt.Sprintf("%s", err))
return 1
}
// Success!
c.Ui.Output("Remote configuration updated")
return 0
}
// enableRemoteState is used to enable remote state management
// and to move a state file into place
func (c *RemoteCommand) enableRemoteState() int {
// Validate the remote configuration
if err := c.validateRemoteConfig(); err != nil {
return 1
}
// Make the hidden directory
if err := remote.EnsureDirectory(); err != nil {
c.Ui.Error(fmt.Sprintf("%s", err))
return 1
}
// Read the provided state file
raw, err := ioutil.ReadFile(c.conf.statePath)
if err != nil {
c.Ui.Error(fmt.Sprintf("Failed to read '%s': %v", c.conf.statePath, err))
return 1
}
state, err := terraform.ReadState(bytes.NewReader(raw))
if err != nil {
c.Ui.Error(fmt.Sprintf("Failed to decode '%s': %v", c.conf.statePath, err))
return 1
}
// Backup the state file before we modify it
backupPath := c.conf.backupPath
if backupPath != "-" {
// Provide default backup path if none provided
if backupPath == "" {
backupPath = c.conf.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("Error writing backup state file: %s", err))
return 1
}
}
// Update the local configuration, move into place
state.Remote = &c.remoteConf
if err := remote.PersistState(state); err != nil {
c.Ui.Error(fmt.Sprintf("%s", err))
return 1
}
// Remove the state file
log.Printf("[INFO] Removing state file: %s", c.conf.statePath)
if err := os.Remove(c.conf.statePath); err != nil {
c.Ui.Error(fmt.Sprintf("Failed to remove state file '%s': %v",
c.conf.statePath, err))
return 1
}
// Success!
c.Ui.Output("Remote state management enabled")
return 0
}
@ -23,13 +299,27 @@ Usage: terraform remote [options]
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.
-auth=token Authentication token for state storage server.
Optional, defaults to blank.
-remote-server=url URL of the remote storage server.
-backup=path Path to backup the existing state file before
modifying. Defaults to the "-state" path with
".backup" extension. Set to "-" to disable backup.
-disable Disables remote state management and migrates the state
to the -state path.
-pull=true Controls if the remote state is pulled before disabling.
This defaults to true to ensure the latest state is cached
before disabling.
-name=name Name of the state file in the state storage server.
Optional, default does not use remote storage.
-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)