command/remote: Working on the details
This commit is contained in:
parent
8c6e4e564f
commit
4e44443aa3
|
@ -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)
|
||||
|
|
Loading…
Reference in New Issue