command/remote: no more remote package

This commit is contained in:
Mitchell Hashimoto 2015-02-23 10:20:40 -08:00
parent 3bf59183b8
commit 2c2b560d7f
4 changed files with 213 additions and 147 deletions

View File

@ -23,7 +23,8 @@ type Meta struct {
// State read when calling `Context`. This is available after calling
// `Context`.
state state.State
state state.State
stateResult *StateResult
// This can be set by the command itself to provide extra hooks.
extraHooks []terraform.Hook
@ -174,25 +175,45 @@ func (m *Meta) State() (state.State, error) {
return m.state, nil
}
result, err := State(m.StateOpts())
if err != nil {
return nil, err
}
m.state = result.State
m.stateOutPath = result.StatePath
m.stateResult = result
return m.state, nil
}
// StateRaw is used to setup the state manually.
func (m *Meta) StateRaw(opts *StateOpts) (*StateResult, error) {
result, err := State(opts)
if err != nil {
return nil, err
}
m.state = result.State
m.stateOutPath = result.StatePath
m.stateResult = result
return result, nil
}
// StateOpts returns the default state options
func (m *Meta) StateOpts() *StateOpts {
localPath := m.statePath
if localPath == "" {
localPath = DefaultStateFilename
}
remotePath := filepath.Join(DefaultDataDir, DefaultStateFilename)
state, statePath, err := State(&StateOpts{
LocalPath: localPath,
LocalPathOut: m.stateOutPath,
RemotePath: remotePath,
BackupPath: m.backupPath,
})
if err != nil {
return nil, err
return &StateOpts{
LocalPath: localPath,
LocalPathOut: m.stateOutPath,
RemotePath: remotePath,
RemoteRefresh: true,
BackupPath: m.backupPath,
}
m.state = state
m.stateOutPath = statePath
return state, nil
}
// UIInput returns a UIInput object to be used for asking for input.

View File

@ -1,15 +1,14 @@
package command
import (
"bytes"
"flag"
"fmt"
"io/ioutil"
"log"
"os"
"strings"
"github.com/hashicorp/terraform/remote"
"github.com/hashicorp/terraform/state"
"github.com/hashicorp/terraform/state/remote"
"github.com/hashicorp/terraform/terraform"
)
@ -55,6 +54,9 @@ func (c *RemoteCommand) Run(args []string) int {
return 1
}
// Set the local state path
c.statePath = c.conf.statePath
// Populate the various configurations
c.remoteConf.Config = map[string]string{
"address": address,
@ -63,50 +65,56 @@ func (c *RemoteCommand) Run(args []string) int {
"path": path,
}
// 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))
// 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))
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
// 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()
}
// Check if remote state is being disabled
if c.conf.disableRemote {
if !haveLocal {
if !remoteState.IsRemote() {
c.Ui.Error(fmt.Sprintf("Remote state management not enabled! Aborting."))
return 1
}
if haveNonManaged {
if !localState.Empty() {
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
haveCache := !remoteState.Empty()
haveLocal := !localState.Empty()
switch {
case haveLocal && haveNonManaged:
case haveCache && haveLocal:
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:
case !haveCache && !haveLocal:
// If we don't have either state file, initialize a blank state file
return c.initBlankState()
case haveLocal && !haveNonManaged:
case haveCache && !haveLocal:
// Update the remote state target potentially
return c.updateRemoteConfig()
case !haveLocal && haveNonManaged:
case !haveCache && haveLocal:
// Enable remote state management
return c.enableRemoteState()
}
@ -117,71 +125,66 @@ func (c *RemoteCommand) Run(args []string) int {
// 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))
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."))
return 1
}
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
// 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 {
if err := remote.RefreshState(); err != nil {
c.Ui.Error(fmt.Sprintf(
"Failed to refresh from remote state: %v", err))
"Failed to refresh from remote state: %s", err))
return 1
}
// Exit if we were unable to update
if !change.SuccessfulPull() {
if change := remote.RefreshResult(); !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",
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",
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",
if err := local.PersistState(); err != nil {
c.Ui.Error(fmt.Sprintf("Failed to encode state file '%s': %s",
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 {
if err := os.Remove(c.stateResult.RemotePath); 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)
conf := c.remoteConf
_, err := remote.NewClient(conf.Type, conf.Config)
if err != nil {
c.Ui.Error(fmt.Sprintf("%s", err))
}
@ -196,18 +199,17 @@ func (c *RemoteCommand) initBlankState() int {
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 {
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 {
c.Ui.Error(fmt.Sprintf("Failed to initialize state file: %v", err))
return 1
}
@ -225,16 +227,17 @@ func (c *RemoteCommand) updateRemoteConfig() int {
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
}
// Read in the local state, which is just the cache of the remote state
remote := c.stateResult.Remote.Cache
// Update the configuration
local.Remote = &c.remoteConf
if err := remote.PersistState(local); err != nil {
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 {
c.Ui.Error(fmt.Sprintf("%s", err))
return 1
}
@ -252,21 +255,10 @@ func (c *RemoteCommand) enableRemoteState() int {
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))
// 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))
return 1
}
@ -279,25 +271,31 @@ func (c *RemoteCommand) enableRemoteState() int {
}
log.Printf("[INFO] Writing backup state to: %s", backupPath)
f, err := os.Create(backupPath)
if err == nil {
err = terraform.WriteState(state, f)
f.Close()
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
}
if err != nil {
if err := backup.PersistState(); err != nil {
c.Ui.Error(fmt.Sprintf("Error writing backup state file: %s", err))
return 1
}
}
// Update the local configuration, move into place
state := local.State()
state.Remote = &c.remoteConf
if err := remote.PersistState(state); err != nil {
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 {
c.Ui.Error(fmt.Sprintf("%s", err))
return 1
}
// Remove the state file
// Remove the original, local 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",

View File

@ -4,9 +4,11 @@ import (
"bytes"
"io/ioutil"
"os"
"path/filepath"
"testing"
"github.com/hashicorp/terraform/remote"
"github.com/hashicorp/terraform/state"
"github.com/hashicorp/terraform/terraform"
"github.com/mitchellh/cli"
)
@ -71,11 +73,6 @@ func TestRemote_disable(t *testing.T) {
}
// Ensure we updated
// TODO: Should be 10, but WriteState currently
// increments incorrectly
if newState.Serial != 11 {
t.Fatalf("state file not updated: %#v", newState)
}
if newState.Remote != nil {
t.Fatalf("remote configuration not removed")
}
@ -96,11 +93,15 @@ func TestRemote_disable_noPull(t *testing.T) {
s = terraform.NewState()
s.Serial = 5
s.Remote = conf
if err := remote.EnsureDirectory(); err != nil {
t.Fatalf("err: %v", err)
// Write the state
statePath := filepath.Join(tmp, DefaultDataDir, DefaultStateFilename)
state := &state.LocalState{Path: statePath}
if err := state.WriteState(s); err != nil {
t.Fatalf("err: %s", err)
}
if err := remote.PersistState(s); err != nil {
t.Fatalf("err: %v", err)
if err := state.PersistState(); err != nil {
t.Fatalf("err: %s", err)
}
ui := new(cli.MockUi)
@ -140,12 +141,6 @@ func TestRemote_disable_noPull(t *testing.T) {
t.Fatalf("err: %v", err)
}
// Ensure we DIDNT updated
// TODO: Should be 5, but WriteState currently increments
// this which is incorrect.
if newState.Serial != 7 {
t.Fatalf("state file updated: %#v", newState)
}
if newState.Remote != nil {
t.Fatalf("remote configuration not removed")
}

View File

@ -21,7 +21,12 @@ type StateOpts struct {
LocalPathOut string
// RemotePath is the path where the remote state cache would be.
RemotePath string
//
// RemoteCache, if true, will set the result to only be the cache
// and not backed by any real durable storage.
RemotePath string
RemoteCacheOnly bool
RemoteRefresh bool
// BackupPath is the path where the backup will be placed. If not set,
// it is assumed to be the path where the state is stored locally
@ -29,25 +34,74 @@ type StateOpts struct {
BackupPath string
}
// StateResult is the result of calling State and holds various different
// State implementations so they can be accessed directly.
type StateResult struct {
// State is the final outer state that should be used for all
// _real_ reads/writes.
//
// StatePath is the local path where the state will be stored or
// cached, no matter whether State is local or remote.
State state.State
StatePath string
// Local and Remote are the local/remote state implementations, raw
// and unwrapped by any backups. The paths here are the paths where
// these state files would be saved.
Local *state.LocalState
LocalPath string
Remote *state.CacheState
RemotePath string
}
// State returns the proper state.State implementation to represent the
// current environment.
//
// localPath is the path to where state would be if stored locally.
// dataDir is the path to the local data directory where the remote state
// cache would be stored.
func State(opts *StateOpts) (state.State, string, error) {
var result state.State
var resultPath string
func State(opts *StateOpts) (*StateResult, error) {
result := new(StateResult)
// Get the remote state cache path
if opts.RemotePath != "" {
if _, err := os.Stat(opts.RemotePath); err == nil {
// We have a remote state, initialize that.
result, err = remoteStateFromPath(opts.RemotePath)
if err != nil {
return nil, "", err
result.RemotePath = opts.RemotePath
var remote *state.CacheState
if opts.RemoteCacheOnly {
// Setup the in-memory state
ls := &state.LocalState{Path: opts.RemotePath}
if err := ls.RefreshState(); err != nil {
return nil, err
}
resultPath = opts.RemotePath
is := &state.InmemState{}
is.WriteState(ls.State())
// Setupt he remote state, cache-only, and refresh it so that
// we have access to the state right away.
remote = &state.CacheState{
Cache: ls,
Durable: is,
}
if err := remote.RefreshState(); err != nil {
return nil, err
}
} else {
if _, err := os.Stat(opts.RemotePath); err == nil {
// We have a remote state, initialize that.
remote, err = remoteStateFromPath(
opts.RemotePath,
opts.RemoteRefresh)
if err != nil {
return nil, err
}
}
}
if remote != nil {
result.State = remote
result.StatePath = opts.RemotePath
result.Remote = remote
}
}
@ -57,22 +111,20 @@ func State(opts *StateOpts) (state.State, string, error) {
Path: opts.LocalPath,
PathOut: opts.LocalPathOut,
}
// Always store it in the result even if we're not using it
result.Local = local
result.LocalPath = local.Path
if local.PathOut != "" {
result.LocalPath = local.PathOut
}
err := local.RefreshState()
if err != nil {
isNotExist := false
errwrap.Walk(err, func(e error) {
if !isNotExist && os.IsNotExist(e) {
isNotExist = true
}
})
if isNotExist {
err = nil
}
} else {
if result != nil {
if err == nil {
if result.State != nil && !result.State.State().Empty() {
if !local.State().Empty() {
// We already have a remote state... that is an error.
return nil, "", fmt.Errorf(
return nil, fmt.Errorf(
"Remote state found, but state file '%s' also present.",
opts.LocalPath)
}
@ -82,34 +134,34 @@ func State(opts *StateOpts) (state.State, string, error) {
}
}
if err != nil {
return nil, "", errwrap.Wrapf(
return nil, errwrap.Wrapf(
"Error reading local state: {{err}}", err)
}
if local != nil {
result = local
resultPath = opts.LocalPath
result.State = local
result.StatePath = opts.LocalPath
if opts.LocalPathOut != "" {
resultPath = opts.LocalPathOut
result.StatePath = opts.LocalPathOut
}
}
}
// If we have a result, make sure to back it up
if result != nil {
backupPath := resultPath + DefaultBackupExtention
if result.State != nil {
backupPath := result.StatePath + DefaultBackupExtention
if opts.BackupPath != "" {
backupPath = opts.BackupPath
}
result = &state.BackupState{
Real: result,
result.State = &state.BackupState{
Real: result.State,
Path: backupPath,
}
}
// Return whatever state we have
return result, resultPath, nil
return result, nil
}
// StateFromPlan gets our state from the plan.
@ -147,7 +199,7 @@ func StateFromPlan(
func remoteState(
local *terraform.State,
localPath string, refresh bool) (state.State, error) {
localPath string, refresh bool) (*state.CacheState, error) {
// If there is no remote settings, it is an error
if local.Remote == nil {
return nil, fmt.Errorf("Remote state cache has no remote info")
@ -199,7 +251,7 @@ func remoteState(
return cache, nil
}
func remoteStateFromPath(path string) (state.State, error) {
func remoteStateFromPath(path string, refresh bool) (*state.CacheState, error) {
// First create the local state for the path
local := &state.LocalState{Path: path}
if err := local.RefreshState(); err != nil {
@ -207,5 +259,5 @@ func remoteStateFromPath(path string) (state.State, error) {
}
localState := local.State()
return remoteState(localState, path, true)
return remoteState(localState, path, refresh)
}