371 lines
10 KiB
Go
371 lines
10 KiB
Go
package command
|
|
|
|
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)
|
|
var address, accessToken, name, path, region, securityToken string
|
|
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")
|
|
cmdFlags.StringVar(&c.conf.backupPath, "backup", "", "path")
|
|
cmdFlags.StringVar(&c.remoteConf.Type, "backend", "atlas", "")
|
|
cmdFlags.StringVar(&address, "address", "", "")
|
|
cmdFlags.StringVar(&accessToken, "access-token", "", "")
|
|
cmdFlags.StringVar(&securityToken, "security-token", "", "")
|
|
cmdFlags.StringVar(®ion, "region", "", "")
|
|
cmdFlags.StringVar(&name, "name", "", "")
|
|
cmdFlags.StringVar(&path, "path", "", "")
|
|
cmdFlags.Usage = func() { c.Ui.Error(c.Help()) }
|
|
if err := cmdFlags.Parse(args); err != nil {
|
|
return 1
|
|
}
|
|
|
|
// Show help if given no inputs
|
|
if !c.conf.disableRemote && c.remoteConf.Type == "atlas" &&
|
|
name == "" && accessToken == "" {
|
|
cmdFlags.Usage()
|
|
return 1
|
|
}
|
|
|
|
// Populate the various configurations
|
|
c.remoteConf.Config = map[string]string{
|
|
"address": address,
|
|
"access_token": accessToken,
|
|
"security_token": securityToken,
|
|
"name": name,
|
|
"path": path,
|
|
"region": region,
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
// disableRemoteState is used to disable remote state management,
|
|
// and move the state file into place.
|
|
func (c *RemoteCommand) disableRemoteState() int {
|
|
// 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
|
|
}
|
|
|
|
// Ensure we have the latest state before disabling
|
|
if c.conf.pullOnDisable {
|
|
log.Printf("[INFO] Refreshing local state from remote server")
|
|
change, err := remote.RefreshState(local.Remote)
|
|
if err != nil {
|
|
c.Ui.Error(fmt.Sprintf(
|
|
"Failed to refresh from remote state: %v", err))
|
|
return 1
|
|
}
|
|
|
|
// Exit if we were unable to update
|
|
if !change.SuccessfulPull() {
|
|
c.Ui.Error(fmt.Sprintf("%s", change))
|
|
return 1
|
|
} else {
|
|
log.Printf("[INFO] %s", change)
|
|
}
|
|
|
|
// Reload the local state after the refresh
|
|
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 '%s': %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
|
|
if err := remote.PersistState(blank); 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
|
|
}
|
|
|
|
func (c *RemoteCommand) Help() string {
|
|
helpText := `
|
|
Usage: terraform remote [options]
|
|
|
|
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:
|
|
|
|
-address=url URL of the remote storage server.
|
|
Required for HTTP and S3 backend, optional for Atlas and Consul.
|
|
|
|
-access-token=token Authentication token for state storage server.
|
|
Required for Atlas backend, optional for Consul.
|
|
|
|
-security-token=token Security token. Specific to S3 (required).
|
|
|
|
-backend=Atlas Specifies the type of remote backend. Must be one
|
|
of Atlas, Consul,HTTP or S3. Defaults to Atlas.
|
|
|
|
-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.
|
|
|
|
-name=name Name of the state file in the state storage server.
|
|
Required for Atlas backend.
|
|
|
|
-path=path Path of the remote state in Consul. Required for the
|
|
Consul.
|
|
|
|
-pull=true Controls if the remote state is pulled before disabling.
|
|
This defaults to true to ensure the latest state is cached
|
|
before disabling.
|
|
|
|
-region=region AWS region to use. Specific for S3 (not required if AWS_DEFAULT_REGION
|
|
env variable is set).
|
|
|
|
-state=path Path to read state. Defaults to "terraform.tfstate"
|
|
unless remote state is enabled.
|
|
|
|
`
|
|
return strings.TrimSpace(helpText)
|
|
}
|
|
|
|
func (c *RemoteCommand) Synopsis() string {
|
|
return "Configures remote state management"
|
|
}
|