terraform/command/remote_config.go

381 lines
11 KiB
Go
Raw Normal View History

2014-10-10 00:05:53 +02:00
package command
2014-10-10 01:31:56 +02:00
import (
"flag"
"fmt"
"log"
"os"
"strings"
2015-02-23 19:20:40 +01:00
"github.com/hashicorp/terraform/state"
"github.com/hashicorp/terraform/state/remote"
2014-10-10 01:31:56 +02:00
"github.com/hashicorp/terraform/terraform"
)
// remoteCommandConfig is used to encapsulate our configuration
type remoteCommandConfig struct {
disableRemote bool
pullOnDisable bool
statePath string
backupPath string
}
2014-10-10 00:05:53 +02:00
// RemoteConfigCommand is a Command implementation that is used to
2014-10-10 00:05:53 +02:00
// enable and disable remote state management
type RemoteConfigCommand struct {
2014-10-10 00:05:53 +02:00
Meta
2014-10-10 01:31:56 +02:00
conf remoteCommandConfig
remoteConf terraform.RemoteState
2014-10-10 00:05:53 +02:00
}
func (c *RemoteConfigCommand) Run(args []string) int {
2014-10-10 01:31:56 +02:00
args = c.Meta.process(args, false)
2015-02-23 19:51:31 +01:00
config := make(map[string]string)
2014-10-10 01:31:56 +02:00
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", DefaultStateFilename, "path")
2014-10-10 01:31:56 +02:00
cmdFlags.StringVar(&c.conf.backupPath, "backup", "", "path")
cmdFlags.StringVar(&c.remoteConf.Type, "backend", "atlas", "")
core: Allow lists and maps as variable overrides Terraform 0.7 introduces lists and maps as first-class values for variables, in addition to string values which were previously available. However, there was previously no way to override the default value of a list or map, and the functionality for overriding specific map keys was broken. Using the environment variable method for setting variable values, there was previously no way to give a variable a value of a list or map. These now support HCL for individual values - specifying: TF_VAR_test='["Hello", "World"]' will set the variable `test` to a two-element list containing "Hello" and "World". Specifying TF_VAR_test_map='{"Hello = "World", "Foo" = "bar"}' will set the variable `test_map` to a two-element map with keys "Hello" and "Foo", and values "World" and "bar" respectively. The same logic is applied to `-var` flags, and the file parsed by `-var-files` ("autoVariables"). Note that care must be taken to not run into shell expansion for `-var-` flags and environment variables. We also merge map keys where appropriate. The override syntax has changed (to be noted in CHANGELOG as a breaking change), so several tests needed their syntax updating from the old `amis.us-east-1 = "newValue"` style to `amis = "{ "us-east-1" = "newValue"}"` style as defined in TF-002. In order to continue supporting the `-var "foo=bar"` type of variable flag (which is not valid HCL), a special case error is checked after HCL parsing fails, and the old code path runs instead.
2016-07-21 03:38:26 +02:00
cmdFlags.Var((*FlagStringKV)(&config), "backend-config", "config")
2014-10-10 01:31:56 +02:00
cmdFlags.Usage = func() { c.Ui.Error(c.Help()) }
if err := cmdFlags.Parse(args); err != nil {
c.Ui.Error(fmt.Sprintf("\nError parsing CLI flags: %s", err))
2014-10-10 01:31:56 +02:00
return 1
}
// Lowercase the type
c.remoteConf.Type = strings.ToLower(c.remoteConf.Type)
2015-02-23 19:20:40 +01:00
// Set the local state path
c.statePath = c.conf.statePath
// Populate the various configurations
2015-02-23 19:51:31 +01:00
c.remoteConf.Config = config
2015-02-23 19:20:40 +01:00
// Get the state information. We specifically request the cache only
// for the remote state here because it is possible the remote state
// is invalid and we don't want to error.
stateOpts := c.StateOpts()
stateOpts.RemoteCacheOnly = true
if _, err := c.StateRaw(stateOpts); err != nil {
c.Ui.Error(fmt.Sprintf("Error loading local state: %s", err))
2014-10-10 01:31:56 +02:00
return 1
}
2015-02-23 19:20:40 +01:00
// Get the local and remote [cached] state
localState := c.stateResult.Local.State()
var remoteState *terraform.State
if remote := c.stateResult.Remote; remote != nil {
remoteState = remote.State()
2014-10-10 01:31:56 +02:00
}
// Check if remote state is being disabled
if c.conf.disableRemote {
2015-02-23 19:20:40 +01:00
if !remoteState.IsRemote() {
2014-10-10 01:31:56 +02:00
c.Ui.Error(fmt.Sprintf("Remote state management not enabled! Aborting."))
return 1
}
2015-02-23 19:20:40 +01:00
if !localState.Empty() {
2014-10-10 01:31:56 +02:00
c.Ui.Error(fmt.Sprintf("State file already exists at '%s'. Aborting.",
c.conf.statePath))
return 1
}
2015-02-23 19:20:40 +01:00
2014-10-10 01:31:56 +02:00
return c.disableRemoteState()
}
// Ensure there is no conflict, and then do the correct operation
var result int
2015-02-23 19:20:40 +01:00
haveCache := !remoteState.Empty()
haveLocal := !localState.Empty()
2014-10-10 01:31:56 +02:00
switch {
2015-02-23 19:20:40 +01:00
case haveCache && haveLocal:
2014-10-10 01:31:56 +02:00
c.Ui.Error(fmt.Sprintf("Remote state is enabled, but non-managed state file '%s' is also present!",
c.conf.statePath))
result = 1
2014-10-10 01:31:56 +02:00
2015-02-23 19:20:40 +01:00
case !haveCache && !haveLocal:
2014-10-10 01:31:56 +02:00
// If we don't have either state file, initialize a blank state file
result = c.initBlankState()
2014-10-10 01:31:56 +02:00
2015-02-23 19:20:40 +01:00
case haveCache && !haveLocal:
2014-10-10 01:31:56 +02:00
// Update the remote state target potentially
result = c.updateRemoteConfig()
2014-10-10 01:31:56 +02:00
2015-02-23 19:20:40 +01:00
case !haveCache && haveLocal:
2014-10-10 01:31:56 +02:00
// Enable remote state management
result = c.enableRemoteState()
}
// If there was an error, return right away
if result != 0 {
return result
}
// If we're not pulling, then do nothing
if !c.conf.pullOnDisable {
return result
2014-10-10 01:31:56 +02:00
}
2015-02-18 19:25:07 +01:00
// Otherwise, refresh the state
stateResult, err := c.StateRaw(c.StateOpts())
if err != nil {
c.Ui.Error(fmt.Sprintf(
"Error while performing the initial pull. The error message is shown\n"+
"below. Note that remote state was properly configured, so you don't\n"+
"need to reconfigure. You can now use `push` and `pull` directly.\n"+
"\n%s", err))
return 1
}
state := stateResult.State
if err := state.RefreshState(); err != nil {
c.Ui.Error(fmt.Sprintf(
"Error while performing the initial pull. The error message is shown\n"+
"below. Note that remote state was properly configured, so you don't\n"+
"need to reconfigure. You can now use `push` and `pull` directly.\n"+
"\n%s", err))
return 1
}
c.Ui.Output(c.Colorize().Color(fmt.Sprintf(
"[reset][bold][green]Remote state configured and pulled.")))
return 0
2014-10-10 01:31:56 +02:00
}
// disableRemoteState is used to disable remote state management,
// and move the state file into place.
func (c *RemoteConfigCommand) disableRemoteState() int {
2015-02-23 19:20:40 +01:00
if c.stateResult == nil {
c.Ui.Error(fmt.Sprintf(
"Internal error. State() must be called internally before remote\n" +
"state can be disabled. Please report this as a bug."))
2014-10-10 01:31:56 +02:00
return 1
}
2015-02-23 19:20:40 +01:00
if !c.stateResult.State.State().IsRemote() {
c.Ui.Error(fmt.Sprintf(
"Remote state is not enabled. Can't disable remote state."))
return 1
}
local := c.stateResult.Local
remote := c.stateResult.Remote
2014-10-10 01:31:56 +02:00
// Ensure we have the latest state before disabling
if c.conf.pullOnDisable {
2014-10-11 01:20:42 +02:00
log.Printf("[INFO] Refreshing local state from remote server")
2015-02-23 19:20:40 +01:00
if err := remote.RefreshState(); err != nil {
c.Ui.Error(fmt.Sprintf(
2015-02-23 19:20:40 +01:00
"Failed to refresh from remote state: %s", err))
return 1
}
// Exit if we were unable to update
2015-02-23 19:20:40 +01:00
if change := remote.RefreshResult(); !change.SuccessfulPull() {
c.Ui.Error(fmt.Sprintf("%s", change))
return 1
} else {
2014-10-11 01:20:42 +02:00
log.Printf("[INFO] %s", change)
}
}
2014-10-10 01:31:56 +02:00
// Clear the remote management, and copy into place
2015-02-23 19:20:40 +01:00
newState := remote.State()
newState.Remote = nil
if err := local.WriteState(newState); err != nil {
c.Ui.Error(fmt.Sprintf("Failed to encode state file '%s': %s",
2014-10-10 01:31:56 +02:00
c.conf.statePath, err))
return 1
}
2015-02-23 19:20:40 +01:00
if err := local.PersistState(); err != nil {
c.Ui.Error(fmt.Sprintf("Failed to encode state file '%s': %s",
2014-10-10 01:31:56 +02:00
c.conf.statePath, err))
return 1
}
// Remove the old state file
2015-02-23 19:20:40 +01:00
if err := os.Remove(c.stateResult.RemotePath); err != nil {
2014-10-10 01:31:56 +02:00
c.Ui.Error(fmt.Sprintf("Failed to remove the local state file: %v", err))
return 1
}
2015-02-23 19:20:40 +01:00
2014-10-10 01:31:56 +02:00
return 0
}
// validateRemoteConfig is used to verify that the remote configuration
// we have is valid
func (c *RemoteConfigCommand) validateRemoteConfig() error {
2015-02-23 19:20:40 +01:00
conf := c.remoteConf
_, err := remote.NewClient(conf.Type, conf.Config)
2014-10-10 01:31:56 +02:00
if err != nil {
c.Ui.Error(fmt.Sprintf(
"%s\n\n"+
2015-03-25 23:26:38 +01:00
"If the error message above mentions requiring or modifying configuration\n"+
"options, these are set using the `-backend-config` flag. Example:\n"+
"-backend-config=\"name=foo\" to set the `name` configuration",
err))
2014-10-10 01:31:56 +02:00
}
return err
}
// initBlank state is used to initialize a blank state that is
// remote enabled
func (c *RemoteConfigCommand) initBlankState() int {
2014-10-10 01:31:56 +02:00
// Validate the remote configuration
if err := c.validateRemoteConfig(); err != nil {
return 1
}
// Make a blank state, attach the remote configuration
blank := terraform.NewState()
blank.Remote = &c.remoteConf
// Persist the state
2015-02-23 19:20:40 +01:00
remote := &state.LocalState{Path: c.stateResult.RemotePath}
if err := remote.WriteState(blank); err != nil {
c.Ui.Error(fmt.Sprintf("Failed to initialize state file: %v", err))
return 1
}
if err := remote.PersistState(); err != nil {
2014-10-10 01:31:56 +02:00
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 *RemoteConfigCommand) updateRemoteConfig() int {
2014-10-10 01:31:56 +02:00
// Validate the remote configuration
if err := c.validateRemoteConfig(); err != nil {
return 1
}
2015-02-23 19:20:40 +01:00
// Read in the local state, which is just the cache of the remote state
remote := c.stateResult.Remote.Cache
2014-10-10 01:31:56 +02:00
// Update the configuration
2015-02-23 19:20:40 +01:00
state := remote.State()
state.Remote = &c.remoteConf
if err := remote.WriteState(state); err != nil {
c.Ui.Error(fmt.Sprintf("%s", err))
return 1
}
if err := remote.PersistState(); err != nil {
2014-10-10 01:31:56 +02:00
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 *RemoteConfigCommand) enableRemoteState() int {
2014-10-10 01:31:56 +02:00
// Validate the remote configuration
if err := c.validateRemoteConfig(); err != nil {
return 1
}
2015-02-23 19:20:40 +01:00
// Read the local state
local := c.stateResult.Local
if err := local.RefreshState(); err != nil {
c.Ui.Error(fmt.Sprintf("Failed to read local state: %s", err))
2014-10-10 01:31:56 +02:00
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 == "" {
2015-09-11 20:56:20 +02:00
backupPath = c.conf.statePath + DefaultBackupExtension
2014-10-10 01:31:56 +02:00
}
log.Printf("[INFO] Writing backup state to: %s", backupPath)
2015-02-23 19:20:40 +01:00
backup := &state.LocalState{Path: backupPath}
if err := backup.WriteState(local.State()); err != nil {
c.Ui.Error(fmt.Sprintf("Error writing backup state file: %s", err))
return 1
2014-10-10 01:31:56 +02:00
}
2015-02-23 19:20:40 +01:00
if err := backup.PersistState(); err != nil {
2014-10-10 01:31:56 +02:00
c.Ui.Error(fmt.Sprintf("Error writing backup state file: %s", err))
return 1
}
}
// Update the local configuration, move into place
2015-02-23 19:20:40 +01:00
state := local.State()
2014-10-10 01:31:56 +02:00
state.Remote = &c.remoteConf
2015-02-23 19:20:40 +01:00
remote := c.stateResult.Remote
if err := remote.WriteState(state); err != nil {
c.Ui.Error(fmt.Sprintf("%s", err))
return 1
}
if err := remote.PersistState(); err != nil {
2014-10-10 01:31:56 +02:00
c.Ui.Error(fmt.Sprintf("%s", err))
return 1
}
2015-02-23 19:20:40 +01:00
// Remove the original, local state file
2014-10-10 01:31:56 +02:00
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")
2014-10-10 00:05:53 +02:00
return 0
}
func (c *RemoteConfigCommand) Help() string {
2014-10-10 00:05:53 +02:00
helpText := `
Usage: terraform remote config [options]
2014-10-10 00:05:53 +02:00
Configures Terraform to use a remote state server. This allows state
to be pulled down when necessary and then pushed to the server when
updated. In this mode, the state file does not need to be stored durably
since the remote server provides the durability.
Options:
-backend=Atlas Specifies the type of remote backend. Must be one
of Atlas, Consul, Etcd, GCS, HTTP, MAS, S3, or Swift.
Defaults to Atlas.
2014-10-10 01:31:56 +02:00
2015-02-23 19:58:39 +01:00
-backend-config="k=v" Specifies configuration for the remote storage
backend. This can be specified multiple times.
2014-10-10 01:31:56 +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.
-disable Disables remote state management and migrates the state
to the -state path.
-pull=true If disabling, this controls if the remote state is
pulled before disabling. If enabling, this controls
if the remote state is pulled after enabling. This
defaults to true.
2014-10-10 01:31:56 +02:00
-state=path Path to read state. Defaults to "terraform.tfstate"
unless remote state is enabled.
2014-10-10 00:05:53 +02:00
2015-06-22 14:14:01 +02:00
-no-color If specified, output won't contain any color.
2014-10-10 00:05:53 +02:00
`
return strings.TrimSpace(helpText)
}
func (c *RemoteConfigCommand) Synopsis() string {
2014-10-10 00:05:53 +02:00
return "Configures remote state management"
}